+
+[](https://star-history.com/#danielmiessler/Personal_AI_Infrastructure&Date)
+
+
+
+---
+
**Built with ❤️ by [Daniel Miessler](https://danielmiessler.com) and the PAI community**
diff --git a/Releases/v2.3/.claude/VoiceServer/server.ts b/Releases/v2.3/.claude/VoiceServer/server.ts
index 0ec6b6f5c..3af2f034b 100755
--- a/Releases/v2.3/.claude/VoiceServer/server.ts
+++ b/Releases/v2.3/.claude/VoiceServer/server.ts
@@ -199,33 +199,43 @@ function getVolumeSetting(): number {
return 1.0; // Default to full volume
}
-// Play audio using afplay (macOS)
+// Audio playback queue - prevents overlapping speech from concurrent notifications
+let audioQueue: Promise
= Promise.resolve();
+
+function enqueueAudio(fn: () => Promise): Promise {
+ audioQueue = audioQueue.then(fn, fn);
+ return audioQueue;
+}
+
+// Play audio using afplay (macOS) - queued to prevent overlap
async function playAudio(audioBuffer: ArrayBuffer): Promise {
- const tempFile = `/tmp/voice-${Date.now()}.mp3`;
+ return enqueueAudio(async () => {
+ const tempFile = `/tmp/voice-${Date.now()}.mp3`;
- // Write audio to temp file
- await Bun.write(tempFile, audioBuffer);
+ // Write audio to temp file
+ await Bun.write(tempFile, audioBuffer);
- const volume = getVolumeSetting();
+ const volume = getVolumeSetting();
- return new Promise((resolve, reject) => {
- // afplay -v takes a value from 0.0 to 1.0
- const proc = spawn('/usr/bin/afplay', ['-v', volume.toString(), tempFile]);
+ return new Promise((resolve, reject) => {
+ // afplay -v takes a value from 0.0 to 1.0
+ const proc = spawn('/usr/bin/afplay', ['-v', volume.toString(), tempFile]);
- proc.on('error', (error) => {
- console.error('Error playing audio:', error);
- reject(error);
- });
+ proc.on('error', (error) => {
+ console.error('Error playing audio:', error);
+ reject(error);
+ });
- proc.on('exit', (code) => {
- // Clean up temp file
- spawn('/bin/rm', [tempFile]);
+ proc.on('exit', (code) => {
+ // Clean up temp file
+ spawn('/bin/rm', [tempFile]);
- if (code === 0) {
- resolve();
- } else {
- reject(new Error(`afplay exited with code ${code}`));
- }
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`afplay exited with code ${code}`));
+ }
+ });
});
});
}
diff --git a/Releases/v2.4/.claude/VoiceServer/server.ts b/Releases/v2.4/.claude/VoiceServer/server.ts
index 314eb9ff7..d9abe61c0 100755
--- a/Releases/v2.4/.claude/VoiceServer/server.ts
+++ b/Releases/v2.4/.claude/VoiceServer/server.ts
@@ -274,33 +274,43 @@ function getVolumeSetting(requestVolume?: number): number {
return 1.0; // Default to full volume
}
-// Play audio using afplay (macOS)
+// Audio playback queue - prevents overlapping speech from concurrent notifications
+let audioQueue: Promise = Promise.resolve();
+
+function enqueueAudio(fn: () => Promise): Promise {
+ audioQueue = audioQueue.then(fn, fn);
+ return audioQueue;
+}
+
+// Play audio using afplay (macOS) - queued to prevent overlap
async function playAudio(audioBuffer: ArrayBuffer, requestVolume?: number): Promise {
- const tempFile = `/tmp/voice-${Date.now()}.mp3`;
+ return enqueueAudio(async () => {
+ const tempFile = `/tmp/voice-${Date.now()}.mp3`;
- // Write audio to temp file
- await Bun.write(tempFile, audioBuffer);
+ // Write audio to temp file
+ await Bun.write(tempFile, audioBuffer);
- const volume = getVolumeSetting(requestVolume);
+ const volume = getVolumeSetting(requestVolume);
- return new Promise((resolve, reject) => {
- // afplay -v takes a value from 0.0 to 1.0
- const proc = spawn('/usr/bin/afplay', ['-v', volume.toString(), tempFile]);
+ return new Promise((resolve, reject) => {
+ // afplay -v takes a value from 0.0 to 1.0
+ const proc = spawn('/usr/bin/afplay', ['-v', volume.toString(), tempFile]);
- proc.on('error', (error) => {
- console.error('Error playing audio:', error);
- reject(error);
- });
+ proc.on('error', (error) => {
+ console.error('Error playing audio:', error);
+ reject(error);
+ });
- proc.on('exit', (code) => {
- // Clean up temp file
- spawn('/bin/rm', [tempFile]);
+ proc.on('exit', (code) => {
+ // Clean up temp file
+ spawn('/bin/rm', [tempFile]);
- if (code === 0) {
- resolve();
- } else {
- reject(new Error(`afplay exited with code ${code}`));
- }
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`afplay exited with code ${code}`));
+ }
+ });
});
});
}
diff --git a/Releases/v3.0/.claude/.gitignore b/Releases/v3.0/.claude/.gitignore
index 962376628..6bf6ebc47 100644
--- a/Releases/v3.0/.claude/.gitignore
+++ b/Releases/v3.0/.claude/.gitignore
@@ -33,7 +33,13 @@ node_modules/
*.cache
*.log
-# Accidental npm/bun debris
-/package.json
+# Private data safety net (pai-sync excludes these, gitignore is defense-in-depth)
+COLLECTIVE/
+MEMORY/LEARNING/
+MEMORY/STATE/
+MEMORY/VOICE/
+MEMORY/WORK/
+
+# Accidental npm/bun debris (keep package.json — hooks need dependencies)
/package-lock.json
/bun.lock
diff --git a/Releases/v3.0/.claude/CLAUDE.md b/Releases/v3.0/.claude/CLAUDE.md
old mode 100755
new mode 100644
index 4a7e7c36d..58c898188
--- a/Releases/v3.0/.claude/CLAUDE.md
+++ b/Releases/v3.0/.claude/CLAUDE.md
@@ -1,4 +1 @@
-This file does nothing.
-
-# Read the PAI system for system understanding and initiation
-`read skills/PAI/SKILL.md`
\ No newline at end of file
+This file does nothing.
\ No newline at end of file
diff --git a/Releases/v3.0/.claude/MEMORY/README.md b/Releases/v3.0/.claude/MEMORY/README.md
old mode 100755
new mode 100644
diff --git a/Releases/v3.0/.claude/Observability/.gitignore b/Releases/v3.0/.claude/Observability/.gitignore
new file mode 100644
index 000000000..f912c5ba5
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+*.app/
+dist/
+.DS_Store
diff --git a/Releases/v3.0/.claude/Observability/MenuBarApp/Info.plist b/Releases/v3.0/.claude/Observability/MenuBarApp/Info.plist
new file mode 100644
index 000000000..715b15c9a
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/MenuBarApp/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ Observability
+ CFBundleIconFile
+ AppIcon
+ CFBundleIconName
+ AppIcon
+ CFBundleIdentifier
+ com.kai.observability
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Observability
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 12.0
+ LSUIElement
+
+ NSHighResolutionCapable
+
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/Releases/v3.0/.claude/Observability/MenuBarApp/ObservabilityApp.swift b/Releases/v3.0/.claude/Observability/MenuBarApp/ObservabilityApp.swift
new file mode 100644
index 000000000..7ae222ab1
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/MenuBarApp/ObservabilityApp.swift
@@ -0,0 +1,333 @@
+import Cocoa
+import ServiceManagement
+
+class AppDelegate: NSObject, NSApplicationDelegate {
+ private var statusItem: NSStatusItem!
+ private var statusMenuItem: NSMenuItem!
+ private var startStopMenuItem: NSMenuItem!
+ private var timer: Timer?
+ private var isRunning = false
+
+ private let manageScriptPath = NSHomeDirectory() + "/.claude/Observability/manage.sh"
+ private let serverPort = 4000
+ private let clientPort = 5172
+
+ func applicationDidFinishLaunching(_ notification: Notification) {
+ // Create the status bar item
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
+
+ if let button = statusItem.button {
+ button.image = NSImage(systemSymbolName: "eye.circle", accessibilityDescription: "Observability")
+ button.image?.isTemplate = true
+ }
+
+ setupMenu()
+
+ // Start checking status periodically
+ checkStatus()
+ timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
+ self?.checkStatus()
+ }
+
+ // Auto-start on launch
+ autoStart()
+ }
+
+ func applicationWillTerminate(_ notification: Notification) {
+ timer?.invalidate()
+ }
+
+ private func setupMenu() {
+ let menu = NSMenu()
+
+ // Status indicator
+ statusMenuItem = NSMenuItem(title: "Status: Checking...", action: nil, keyEquivalent: "")
+ statusMenuItem.isEnabled = false
+ menu.addItem(statusMenuItem)
+
+ menu.addItem(NSMenuItem.separator())
+
+ // Start/Stop toggle
+ startStopMenuItem = NSMenuItem(title: "Start", action: #selector(toggleService), keyEquivalent: "s")
+ startStopMenuItem.target = self
+ menu.addItem(startStopMenuItem)
+
+ // Restart
+ let restartItem = NSMenuItem(title: "Restart", action: #selector(restartService), keyEquivalent: "r")
+ restartItem.target = self
+ menu.addItem(restartItem)
+
+ menu.addItem(NSMenuItem.separator())
+
+ // Open Dashboard
+ let openItem = NSMenuItem(title: "Open Dashboard", action: #selector(openDashboard), keyEquivalent: "o")
+ openItem.target = self
+ menu.addItem(openItem)
+
+ menu.addItem(NSMenuItem.separator())
+
+ // Launch at Login
+ let launchAtLoginItem = NSMenuItem(title: "Launch at Login", action: #selector(toggleLaunchAtLogin), keyEquivalent: "")
+ launchAtLoginItem.target = self
+ launchAtLoginItem.state = isLaunchAtLoginEnabled() ? .on : .off
+ menu.addItem(launchAtLoginItem)
+
+ menu.addItem(NSMenuItem.separator())
+
+ // Quit
+ let quitItem = NSMenuItem(title: "Quit Observability", action: #selector(quitApp), keyEquivalent: "q")
+ quitItem.target = self
+ menu.addItem(quitItem)
+
+ statusItem.menu = menu
+ }
+
+ private func autoStart() {
+ // Run autostart on a slight delay to ensure app is fully initialized
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ guard let self = self else { return }
+ if !self.isServerRunning() {
+ self.startService()
+ }
+ }
+ }
+
+ private func checkStatus() {
+ let running = isServerRunning()
+ isRunning = running
+
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+
+ if running {
+ self.statusMenuItem.title = "Status: Running"
+ self.startStopMenuItem.title = "Stop"
+ if let button = self.statusItem.button {
+ button.image = NSImage(systemSymbolName: "eye.circle.fill", accessibilityDescription: "Observability Running")
+ button.image?.isTemplate = true
+ }
+ } else {
+ self.statusMenuItem.title = "Status: Stopped"
+ self.startStopMenuItem.title = "Start"
+ if let button = self.statusItem.button {
+ button.image = NSImage(systemSymbolName: "eye.circle", accessibilityDescription: "Observability Stopped")
+ button.image?.isTemplate = true
+ }
+ }
+ }
+ }
+
+ private func isServerRunning() -> Bool {
+ let task = Process()
+ task.launchPath = "/usr/sbin/lsof"
+ task.arguments = ["-i", ":\(serverPort)", "-sTCP:LISTEN"]
+
+ let pipe = Pipe()
+ task.standardOutput = pipe
+ task.standardError = pipe
+
+ do {
+ try task.run()
+ task.waitUntilExit()
+ return task.terminationStatus == 0
+ } catch {
+ return false
+ }
+ }
+
+ @objc private func toggleService() {
+ if isRunning {
+ stopService()
+ } else {
+ startService()
+ }
+ }
+
+ private func startService() {
+ // Run in background thread but wait for completion
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ guard let self = self else { return }
+
+ let homePath = NSHomeDirectory()
+ let scriptPath = self.manageScriptPath
+ let workDir = "\(homePath)/.claude/Observability"
+
+ // Set up environment with PATH including bun
+ var env = ProcessInfo.processInfo.environment
+ let additionalPaths = [
+ "\(homePath)/.bun/bin",
+ "\(homePath)/.local/bin",
+ "/opt/homebrew/bin",
+ "/usr/local/bin"
+ ]
+ env["PATH"] = additionalPaths.joined(separator: ":") + ":/usr/bin:/bin"
+ env["HOME"] = homePath
+
+ let task = Process()
+ task.executableURL = URL(fileURLWithPath: "/bin/bash")
+ task.arguments = [scriptPath, "start-detached"]
+ task.currentDirectoryURL = URL(fileURLWithPath: workDir)
+ task.environment = env
+ task.standardOutput = FileHandle.nullDevice
+ task.standardError = FileHandle.nullDevice
+
+ do {
+ try task.run()
+ task.waitUntilExit()
+ } catch {
+ // Silently fail
+ }
+
+ // Update status on main thread
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ self.checkStatus()
+ }
+ }
+ }
+
+ private func stopService() {
+ runManageScript(with: "stop", waitForCompletion: true)
+
+ // Delay status check to allow service to stop
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
+ self?.checkStatus()
+ }
+ }
+
+ @objc private func restartService() {
+ runManageScript(with: "stop", waitForCompletion: true)
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
+ self?.startService()
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ self?.checkStatus()
+ }
+ }
+ }
+
+ private func runManageScript(with command: String, waitForCompletion: Bool = false) {
+ let homePath = NSHomeDirectory()
+ let scriptPath = self.manageScriptPath
+ let workDir = "\(homePath)/.claude/Observability"
+
+ // Set up environment with PATH including bun
+ var env = ProcessInfo.processInfo.environment
+ let additionalPaths = [
+ "\(homePath)/.bun/bin",
+ "\(homePath)/.local/bin",
+ "/opt/homebrew/bin",
+ "/usr/local/bin"
+ ]
+ env["PATH"] = additionalPaths.joined(separator: ":") + ":/usr/bin:/bin"
+ env["HOME"] = homePath
+
+ DispatchQueue.global(qos: .userInitiated).async {
+ let task = Process()
+ task.executableURL = URL(fileURLWithPath: "/bin/bash")
+ task.arguments = [scriptPath, command]
+ task.currentDirectoryURL = URL(fileURLWithPath: workDir)
+ task.environment = env
+
+ task.standardOutput = FileHandle.nullDevice
+ task.standardError = FileHandle.nullDevice
+
+ do {
+ try task.run()
+ if waitForCompletion {
+ task.waitUntilExit()
+ }
+ } catch {
+ // Ignore errors
+ }
+ }
+ }
+
+ @objc private func openDashboard() {
+ if let url = URL(string: "http://localhost:\(clientPort)") {
+ NSWorkspace.shared.open(url)
+ }
+ }
+
+ @objc private func toggleLaunchAtLogin(_ sender: NSMenuItem) {
+ let newState = sender.state == .off
+ setLaunchAtLogin(enabled: newState)
+ sender.state = newState ? .on : .off
+ }
+
+ private func isLaunchAtLoginEnabled() -> Bool {
+ // Check if LaunchAgent plist exists
+ let launchAgentPath = NSHomeDirectory() + "/Library/LaunchAgents/com.kai.observability.plist"
+ return FileManager.default.fileExists(atPath: launchAgentPath)
+ }
+
+ private func setLaunchAtLogin(enabled: Bool) {
+ let launchAgentPath = NSHomeDirectory() + "/Library/LaunchAgents/com.kai.observability.plist"
+ let appPath = Bundle.main.bundlePath
+
+ if enabled {
+ // Create LaunchAgent plist
+ let plistContent = """
+
+
+
+
+ Label
+ com.kai.observability
+ ProgramArguments
+
+ /usr/bin/open
+ -a
+ \(appPath)
+
+ RunAtLoad
+
+ KeepAlive
+
+
+
+ """
+
+ do {
+ // Ensure LaunchAgents directory exists
+ let launchAgentsDir = NSHomeDirectory() + "/Library/LaunchAgents"
+ try FileManager.default.createDirectory(atPath: launchAgentsDir, withIntermediateDirectories: true)
+
+ try plistContent.write(toFile: launchAgentPath, atomically: true, encoding: .utf8)
+
+ // Load the launch agent
+ let task = Process()
+ task.launchPath = "/bin/launchctl"
+ task.arguments = ["load", launchAgentPath]
+ try task.run()
+ task.waitUntilExit()
+ } catch {
+ // Ignore errors
+ }
+ } else {
+ // Remove LaunchAgent plist
+ do {
+ // Unload first
+ let task = Process()
+ task.launchPath = "/bin/launchctl"
+ task.arguments = ["unload", launchAgentPath]
+ try task.run()
+ task.waitUntilExit()
+
+ try FileManager.default.removeItem(atPath: launchAgentPath)
+ } catch {
+ // Ignore errors
+ }
+ }
+ }
+
+ @objc private func quitApp() {
+ NSApplication.shared.terminate(self)
+ }
+}
+
+// Main entry point - properly initialize the app with delegate
+let app = NSApplication.shared
+let delegate = AppDelegate()
+app.delegate = delegate
+app.run()
diff --git a/Releases/v3.0/.claude/Observability/MenuBarApp/build.sh b/Releases/v3.0/.claude/Observability/MenuBarApp/build.sh
new file mode 100755
index 000000000..cc27dc8e7
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/MenuBarApp/build.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+# Build script for Observability menu bar app
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+APP_NAME="Observability"
+APP_BUNDLE="$SCRIPT_DIR/$APP_NAME.app"
+INSTALL_PATH="/Applications/$APP_NAME.app"
+
+echo "Building $APP_NAME..."
+
+# Clean previous build
+rm -rf "$APP_BUNDLE"
+
+# Create app bundle structure
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+mkdir -p "$APP_BUNDLE/Contents/Resources"
+
+# Copy Info.plist
+cp "$SCRIPT_DIR/Info.plist" "$APP_BUNDLE/Contents/"
+
+# Compile Swift source
+swiftc -O \
+ -sdk $(xcrun --show-sdk-path) \
+ -target arm64-apple-macosx12.0 \
+ -o "$APP_BUNDLE/Contents/MacOS/$APP_NAME" \
+ "$SCRIPT_DIR/ObservabilityApp.swift"
+
+# Also compile for x86_64 and create universal binary (for Intel Macs)
+swiftc -O \
+ -sdk $(xcrun --show-sdk-path) \
+ -target x86_64-apple-macosx12.0 \
+ -o "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_x86" \
+ "$SCRIPT_DIR/ObservabilityApp.swift" 2>/dev/null || true
+
+# Create universal binary if x86 build succeeded
+if [ -f "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_x86" ]; then
+ lipo -create \
+ "$APP_BUNDLE/Contents/MacOS/$APP_NAME" \
+ "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_x86" \
+ -output "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_universal"
+ mv "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_universal" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
+ rm "$APP_BUNDLE/Contents/MacOS/${APP_NAME}_x86"
+fi
+
+# Create PkgInfo
+echo -n "APPL????" > "$APP_BUNDLE/Contents/PkgInfo"
+
+echo "Build complete: $APP_BUNDLE"
+
+# Optionally install to /Applications
+read -p "Install to /Applications? [y/N] " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Installing to /Applications..."
+ rm -rf "$INSTALL_PATH"
+ cp -R "$APP_BUNDLE" "$INSTALL_PATH"
+ echo "Installed to $INSTALL_PATH"
+ echo ""
+ echo "To start the app: open /Applications/$APP_NAME.app"
+ echo "To enable launch at login: Use the menu bar icon -> 'Launch at Login'"
+fi
diff --git a/Releases/v3.0/.claude/Observability/Tools/ManageServer.ts b/Releases/v3.0/.claude/Observability/Tools/ManageServer.ts
new file mode 100644
index 000000000..ffba51c59
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/Tools/ManageServer.ts
@@ -0,0 +1,259 @@
+#!/usr/bin/env bun
+/**
+ * ManageServer.ts - Observability Dashboard Manager
+ *
+ * A CLI tool for managing the PAI Observability Dashboard (server + client)
+ *
+ * Usage:
+ * bun ~/.claude/Observability/Tools/ManageServer.ts
+ *
+ * Commands:
+ * start Start the observability dashboard
+ * stop Stop the observability dashboard
+ * restart Restart the observability dashboard
+ * status Check if dashboard is running
+ * logs Show recent server output
+ * open Open dashboard in browser
+ *
+ * @author PAI (Personal AI Infrastructure)
+ */
+
+import { $ } from "bun";
+import { existsSync } from "fs";
+import { join } from "path";
+
+const CONFIG = {
+ basePath: join(process.env.HOME || "", ".claude/Observability"),
+ serverPort: 4000,
+ clientPort: 5172,
+ logFile: join(process.env.HOME || "", "Library/Logs/pai-observability.log"),
+};
+
+const colors = {
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
+ blue: (s: string) => `\x1b[34m${s}\x1b[0m`,
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
+};
+
+async function isPortInUse(port: number): Promise {
+ try {
+ const result = await $`lsof -Pi :${port} -sTCP:LISTEN -t`.quiet().nothrow();
+ return result.exitCode === 0;
+ } catch {
+ return false;
+ }
+}
+
+async function isServerHealthy(): Promise {
+ try {
+ const response = await fetch(`http://localhost:${CONFIG.serverPort}/events/filter-options`);
+ return response.ok;
+ } catch {
+ return false;
+ }
+}
+
+async function isClientHealthy(): Promise {
+ try {
+ const response = await fetch(`http://localhost:${CONFIG.clientPort}`);
+ return response.ok;
+ } catch {
+ return false;
+ }
+}
+
+async function startServer(): Promise {
+ const serverRunning = await isPortInUse(CONFIG.serverPort);
+ const clientRunning = await isPortInUse(CONFIG.clientPort);
+
+ if (serverRunning && clientRunning) {
+ console.log(colors.yellow("Observability dashboard already running."));
+ console.log(colors.bold(`URL: http://localhost:${CONFIG.clientPort}`));
+ return;
+ }
+
+ if (serverRunning || clientRunning) {
+ console.log(colors.yellow("Partial state detected. Cleaning up..."));
+ await stopServer();
+ }
+
+ console.log(colors.blue("Starting observability dashboard..."));
+
+ // Start server
+ const serverCmd = `cd "${CONFIG.basePath}/apps/server" && nohup bun run dev >> "${CONFIG.logFile}" 2>&1 &`;
+ await $`sh -c ${serverCmd}`.quiet();
+
+ // Wait for server
+ for (let i = 0; i < 15; i++) {
+ await Bun.sleep(500);
+ if (await isServerHealthy()) break;
+ }
+
+ if (!await isServerHealthy()) {
+ console.error(colors.red("Server failed to start. Check logs."));
+ process.exit(1);
+ }
+
+ // Start client
+ const clientCmd = `cd "${CONFIG.basePath}/apps/client" && nohup bun run dev >> "${CONFIG.logFile}" 2>&1 &`;
+ await $`sh -c ${clientCmd}`.quiet();
+
+ // Wait for client
+ for (let i = 0; i < 15; i++) {
+ await Bun.sleep(500);
+ if (await isClientHealthy()) break;
+ }
+
+ if (await isClientHealthy()) {
+ console.log(colors.green("Observability dashboard started!"));
+ console.log(colors.dim(`Server: http://localhost:${CONFIG.serverPort}`));
+ console.log(colors.bold(`Dashboard: http://localhost:${CONFIG.clientPort}`));
+ } else {
+ console.error(colors.red("Client failed to start. Check logs."));
+ process.exit(1);
+ }
+}
+
+async function stopServer(): Promise {
+ console.log(colors.blue("Stopping observability dashboard..."));
+
+ // Kill by port
+ for (const port of [CONFIG.serverPort, CONFIG.clientPort]) {
+ const result = await $`lsof -ti :${port}`.quiet().nothrow();
+ if (result.exitCode === 0) {
+ const pids = result.stdout.toString().trim().split("\n");
+ for (const pid of pids) {
+ if (pid) await $`kill -9 ${pid}`.quiet().nothrow();
+ }
+ }
+ }
+
+ // Kill any remaining bun processes for observability
+ await $`pkill -f "Observability/apps/(server|client)"`.quiet().nothrow();
+
+ // Clean SQLite WAL files
+ const walPath = join(CONFIG.basePath, "apps/server/events.db-wal");
+ const shmPath = join(CONFIG.basePath, "apps/server/events.db-shm");
+ if (existsSync(walPath)) await $`rm -f ${walPath}`.quiet().nothrow();
+ if (existsSync(shmPath)) await $`rm -f ${shmPath}`.quiet().nothrow();
+
+ await Bun.sleep(500);
+ console.log(colors.green("Observability dashboard stopped."));
+}
+
+async function restartServer(): Promise {
+ console.log(colors.blue("Restarting observability dashboard..."));
+ await stopServer();
+ await Bun.sleep(500);
+ await startServer();
+}
+
+async function showStatus(): Promise {
+ const serverUp = await isPortInUse(CONFIG.serverPort);
+ const clientUp = await isPortInUse(CONFIG.clientPort);
+ const serverHealthy = await isServerHealthy();
+ const clientHealthy = await isClientHealthy();
+
+ if (serverUp && clientUp && serverHealthy && clientHealthy) {
+ console.log(colors.green("Status: RUNNING"));
+ console.log(colors.dim(`Server: http://localhost:${CONFIG.serverPort} (healthy)`));
+ console.log(colors.bold(`Dashboard: http://localhost:${CONFIG.clientPort}`));
+ } else if (serverUp || clientUp) {
+ console.log(colors.yellow("Status: PARTIAL"));
+ console.log(colors.dim(`Server port ${CONFIG.serverPort}: ${serverUp ? (serverHealthy ? "healthy" : "unhealthy") : "down"}`));
+ console.log(colors.dim(`Client port ${CONFIG.clientPort}: ${clientUp ? (clientHealthy ? "healthy" : "unhealthy") : "down"}`));
+ } else {
+ console.log(colors.red("Status: NOT RUNNING"));
+ }
+}
+
+async function showLogs(): Promise {
+ if (!existsSync(CONFIG.logFile)) {
+ console.log(colors.yellow("No log file found."));
+ return;
+ }
+
+ const result = await $`tail -40 ${CONFIG.logFile}`.quiet();
+ console.log(colors.bold("Recent observability logs:"));
+ console.log(colors.dim("─".repeat(50)));
+ console.log(result.stdout.toString());
+}
+
+async function openDashboard(): Promise {
+ const healthy = await isClientHealthy();
+ if (!healthy) {
+ console.log(colors.yellow("Dashboard not running. Starting..."));
+ await startServer();
+ }
+
+ console.log(colors.blue("Opening dashboard in browser..."));
+ await $`open http://localhost:${CONFIG.clientPort}`.quiet();
+}
+
+function showHelp(): void {
+ console.log(colors.bold("ManageServer.ts - Observability Dashboard Manager"));
+ console.log(colors.dim("─".repeat(50)));
+ console.log(`
+${colors.bold("Usage:")}
+ bun ManageServer.ts
+
+${colors.bold("Commands:")}
+ ${colors.green("start")} Start the observability dashboard
+ ${colors.green("stop")} Stop the observability dashboard
+ ${colors.green("restart")} Restart the observability dashboard
+ ${colors.green("status")} Check dashboard status
+ ${colors.green("logs")} Show recent logs
+ ${colors.green("open")} Open dashboard in browser
+
+${colors.bold("Examples:")}
+ ${colors.dim("# Quick restart")}
+ bun ManageServer.ts restart
+
+ ${colors.dim("# Open in browser")}
+ bun ManageServer.ts open
+
+${colors.bold("Dashboard:")} http://localhost:${CONFIG.clientPort}
+${colors.bold("API:")} http://localhost:${CONFIG.serverPort}
+`);
+}
+
+async function main(): Promise {
+ const command = process.argv[2] || "help";
+
+ switch (command) {
+ case "start":
+ await startServer();
+ break;
+ case "stop":
+ await stopServer();
+ break;
+ case "restart":
+ await restartServer();
+ break;
+ case "status":
+ await showStatus();
+ break;
+ case "logs":
+ await showLogs();
+ break;
+ case "open":
+ await openDashboard();
+ break;
+ case "help":
+ case "--help":
+ showHelp();
+ break;
+ default:
+ console.error(colors.red(`Unknown command: ${command}`));
+ showHelp();
+ process.exit(1);
+ }
+}
+
+main().catch((error) => {
+ console.error(colors.red("Fatal error:"), error);
+ process.exit(1);
+});
diff --git a/Releases/v3.0/.claude/Observability/Tools/obs-cmds.ts b/Releases/v3.0/.claude/Observability/Tools/obs-cmds.ts
new file mode 100644
index 000000000..f4fc34a0c
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/Tools/obs-cmds.ts
@@ -0,0 +1,436 @@
+#!/usr/bin/env bun
+/**
+ * obs-cmds — Extract command output from Claude Code agent sessions
+ *
+ * Watches JSONL session transcripts and extracts Bash command invocations
+ * with their full stdout/stderr, formatted as a clean terminal transcript.
+ *
+ * Output is both displayed in terminal AND written to a log file so you
+ * can screenshot/review it after the task completes.
+ *
+ * Usage:
+ * obs-cmds # Watch current project, live stream
+ * obs-cmds --session abc123 # Extract from specific session
+ * obs-cmds --last N # Show last N commands from most recent session
+ * obs-cmds --all # Watch all project dirs
+ * obs-cmds --no-log # Don't write to log file
+ * obs-cmds --log-dir PATH # Custom log directory (default: ./outputs/cmd-logs/)
+ * obs-cmds --ssh-only # Only show SSH/remote commands
+ */
+
+import { watch, existsSync, readdirSync, statSync, readFileSync, mkdirSync, appendFileSync } from "fs";
+import { join, basename } from "path";
+import { homedir } from "os";
+
+// ── Config ──────────────────────────────────────────────────────────
+
+const PROJECTS_BASE = join(homedir(), ".claude", "projects");
+const args = process.argv.slice(2);
+const watchAll = args.includes("--all");
+const noLog = args.includes("--no-log");
+const sshOnly = args.includes("--ssh-only");
+const specificSession = args.includes("--session")
+ ? args[args.indexOf("--session") + 1]
+ : null;
+const lastN = args.includes("--last")
+ ? parseInt(args[args.indexOf("--last") + 1]) || 10
+ : null;
+const logDir = args.includes("--log-dir")
+ ? args[args.indexOf("--log-dir") + 1]
+ : join(process.cwd(), "outputs", "cmd-logs");
+
+// ── Colors ──────────────────────────────────────────────────────────
+
+const c = {
+ reset: "\x1b[0m",
+ dim: "\x1b[2m",
+ bold: "\x1b[1m",
+ red: "\x1b[31m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ blue: "\x1b[34m",
+ cyan: "\x1b[36m",
+ gray: "\x1b[90m",
+ bgRed: "\x1b[41m",
+ white: "\x1b[37m",
+};
+
+// ── State ───────────────────────────────────────────────────────────
+
+const filePositions = new Map();
+const watchedFiles = new Set();
+let logFile: string | null = null;
+let cmdCount = 0;
+
+// Pending tool_use blocks waiting for their results
+const pendingTools = new Map();
+
+// ── Helpers ─────────────────────────────────────────────────────────
+
+function timestamp(): string {
+ const now = new Date();
+ return now.toISOString().replace("T", " ").slice(0, 19);
+}
+
+function shortTimestamp(): string {
+ const now = new Date();
+ const h = now.getHours().toString().padStart(2, "0");
+ const m = now.getMinutes().toString().padStart(2, "0");
+ const s = now.getSeconds().toString().padStart(2, "0");
+ return `${h}:${m}:${s}`;
+}
+
+function initLogFile(): void {
+ if (noLog) return;
+
+ if (!existsSync(logDir)) {
+ mkdirSync(logDir, { recursive: true });
+ }
+
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
+ logFile = join(logDir, `cmds-${ts}.log`);
+
+ const header = `# Claude Code Command Log
+# Started: ${timestamp()}
+# Project: ${process.cwd()}
+${"=".repeat(72)}
+
+`;
+ appendFileSync(logFile, header);
+ console.log(`${c.dim}Log file: ${logFile}${c.reset}`);
+}
+
+function log(text: string): void {
+ if (logFile && !noLog) {
+ // Strip ANSI codes for the log file
+ const clean = text.replace(/\x1b\[[0-9;]*m/g, "");
+ appendFileSync(logFile, clean + "\n");
+ }
+}
+
+function displayCommand(cmd: string, output: string, isError: boolean, sessionShort: string, entryTimestamp?: string): void {
+ const ts = entryTimestamp || shortTimestamp();
+
+ // Filter SSH-only if requested
+ if (sshOnly && !cmd.includes("ssh") && !cmd.includes("sshpass")) {
+ return;
+ }
+
+ cmdCount++;
+ const separator = `${c.dim}${"─".repeat(72)}${c.reset}`;
+ const errorTag = isError ? ` ${c.red}[ERROR]${c.reset}` : "";
+ const sessionTag = `${c.dim}[${sessionShort}]${c.reset}`;
+
+ // Command header
+ console.log(separator);
+ console.log(`${c.gray}${ts}${c.reset} ${sessionTag}${errorTag}`);
+ console.log(`${c.bold}${c.green}$${c.reset} ${c.bold}${cmd}${c.reset}`);
+ console.log();
+
+ // Output
+ if (output.trim()) {
+ console.log(output);
+ } else {
+ console.log(`${c.dim}(no output)${c.reset}`);
+ }
+
+ console.log();
+
+ // Log to file
+ log(`${"─".repeat(72)}`);
+ log(`${ts} [${sessionShort}]${isError ? " [ERROR]" : ""}`);
+ log(`$ ${cmd}`);
+ log("");
+ log(output.trim() || "(no output)");
+ log("");
+}
+
+// ── JSONL Processing ────────────────────────────────────────────────
+
+function processEntry(entry: any, sessionShort: string): void {
+ // Capture tool_use (command invocations)
+ if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
+ for (const block of entry.message.content) {
+ if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
+ const entryTime = entry.timestamp
+ ? new Date(entry.timestamp).toLocaleTimeString("en-US", { hour12: false })
+ : shortTimestamp();
+
+ pendingTools.set(block.id, {
+ name: block.name,
+ command: block.input.command,
+ timestamp: entryTime,
+ sessionShort,
+ });
+ }
+ }
+ }
+
+ // Match tool results back to their commands
+ if (entry.type === "user" && Array.isArray(entry.message?.content)) {
+ for (const block of entry.message.content) {
+ if (block.type === "tool_result" && block.tool_use_id) {
+ const pending = pendingTools.get(block.tool_use_id);
+ if (pending) {
+ // Extract the output text
+ let output = "";
+ const isError = block.is_error === true;
+
+ if (typeof block.content === "string") {
+ output = block.content;
+ } else if (Array.isArray(block.content)) {
+ output = block.content
+ .filter((c: any) => c.type === "text")
+ .map((c: any) => c.text || "")
+ .join("\n");
+ }
+
+ displayCommand(pending.command, output, isError, pending.sessionShort, pending.timestamp);
+ pendingTools.delete(block.tool_use_id);
+ }
+ }
+ }
+ }
+}
+
+function processFile(filePath: string, sessionShort: string, fromStart: boolean = false): void {
+ if (!existsSync(filePath)) return;
+
+ const content = readFileSync(filePath, "utf-8");
+
+ if (!fromStart) {
+ const lastPos = filePositions.get(filePath) || 0;
+ const newContent = content.slice(lastPos);
+ filePositions.set(filePath, content.length);
+
+ if (!newContent.trim()) return;
+
+ for (const line of newContent.trim().split("\n")) {
+ if (!line.trim()) continue;
+ try {
+ const entry = JSON.parse(line);
+ processEntry(entry, sessionShort);
+ } catch {}
+ }
+ } else {
+ // Process from beginning (for --last or --session)
+ filePositions.set(filePath, content.length);
+
+ for (const line of content.trim().split("\n")) {
+ if (!line.trim()) continue;
+ try {
+ const entry = JSON.parse(line);
+ processEntry(entry, sessionShort);
+ } catch {}
+ }
+ }
+}
+
+// ── Watch Logic ─────────────────────────────────────────────────────
+
+function getProjectDirs(): string[] {
+ if (watchAll) {
+ return readdirSync(PROJECTS_BASE)
+ .filter((d) => d.startsWith("-"))
+ .map((d) => join(PROJECTS_BASE, d))
+ .filter((d) => statSync(d).isDirectory());
+ }
+
+ const cwd = process.cwd().replace(/\//g, "-").replace(/^-/, "-");
+ const projectDir = join(PROJECTS_BASE, cwd);
+ if (existsSync(projectDir)) {
+ return [projectDir];
+ }
+
+ const dirs = readdirSync(PROJECTS_BASE)
+ .filter((d) => d.startsWith("-"))
+ .map((d) => ({
+ name: d,
+ path: join(PROJECTS_BASE, d),
+ mtime: statSync(join(PROJECTS_BASE, d)).mtime.getTime(),
+ }))
+ .filter((d) => statSync(d.path).isDirectory())
+ .sort((a, b) => b.mtime - a.mtime);
+
+ if (dirs.length > 0) {
+ return [dirs[0].path];
+ }
+ return [];
+}
+
+function getRecentJsonl(dir: string, limit: number = 10): string[] {
+ return readdirSync(dir)
+ .filter((f) => f.endsWith(".jsonl"))
+ .map((f) => ({
+ name: f,
+ path: join(dir, f),
+ mtime: statSync(join(dir, f)).mtime.getTime(),
+ }))
+ .sort((a, b) => b.mtime - a.mtime)
+ .slice(0, limit)
+ .map((f) => f.path);
+}
+
+function findSession(dirs: string[], sessionId: string): string | null {
+ for (const dir of dirs) {
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
+ const match = files.find((f) => f.startsWith(sessionId));
+ if (match) return join(dir, match);
+ }
+ return null;
+}
+
+function watchFile(filePath: string): void {
+ if (watchedFiles.has(filePath)) return;
+ watchedFiles.add(filePath);
+
+ const sessionShort = basename(filePath, ".jsonl").slice(0, 8);
+
+ // Set position to end — only capture NEW commands
+ const content = readFileSync(filePath, "utf-8");
+ filePositions.set(filePath, content.length);
+
+ const watcher = watch(filePath, (eventType) => {
+ if (eventType === "change") {
+ processFile(filePath, sessionShort);
+ }
+ });
+
+ watcher.on("error", () => {
+ watchedFiles.delete(filePath);
+ });
+}
+
+function watchDir(dir: string): void {
+ watch(dir, (eventType, filename) => {
+ if (filename && filename.endsWith(".jsonl")) {
+ const filePath = join(dir, filename);
+ if (existsSync(filePath) && !watchedFiles.has(filePath)) {
+ console.log(`${c.dim}+ New session: ${basename(filePath, ".jsonl").slice(0, 8)}${c.reset}`);
+ watchFile(filePath);
+ }
+ }
+ });
+}
+
+// ── Main ────────────────────────────────────────────────────────────
+
+function main(): void {
+ // Header
+ console.log(`\n${c.bold}${c.blue}obs-cmds${c.reset} ${c.dim}— Claude Code Command Output Log${c.reset}`);
+ console.log(`${c.dim}${"═".repeat(72)}${c.reset}`);
+
+ const dirs = getProjectDirs();
+ if (dirs.length === 0) {
+ console.error(`${c.red}No Claude Code project directories found.${c.reset}`);
+ process.exit(1);
+ }
+
+ // Mode: Extract specific session
+ if (specificSession) {
+ const file = findSession(dirs, specificSession);
+ if (!file) {
+ console.error(`${c.red}Session ${specificSession} not found.${c.reset}`);
+ process.exit(1);
+ }
+
+ console.log(`${c.dim}Extracting commands from session: ${specificSession}${c.reset}\n`);
+ initLogFile();
+ processFile(file, specificSession.slice(0, 8), true);
+ console.log(`\n${c.dim}${cmdCount} commands extracted.${c.reset}`);
+ if (logFile) console.log(`${c.dim}Saved to: ${logFile}${c.reset}`);
+ process.exit(0);
+ }
+
+ // Mode: Show last N commands
+ if (lastN) {
+ const files = getRecentJsonl(dirs[0], 1);
+ if (files.length === 0) {
+ console.error(`${c.red}No session files found.${c.reset}`);
+ process.exit(1);
+ }
+
+ const sessionShort = basename(files[0], ".jsonl").slice(0, 8);
+ console.log(`${c.dim}Last ${lastN} commands from session: ${sessionShort}${c.reset}\n`);
+
+ // Process entire file to collect all commands, then show last N
+ const allCmds: { cmd: string; output: string; isError: boolean; ts: string }[] = [];
+ const content = readFileSync(files[0], "utf-8");
+ const localPending = new Map();
+
+ for (const line of content.trim().split("\n")) {
+ if (!line.trim()) continue;
+ try {
+ const entry = JSON.parse(line);
+
+ if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
+ for (const block of entry.message.content) {
+ if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
+ const entryTime = entry.timestamp
+ ? new Date(entry.timestamp).toLocaleTimeString("en-US", { hour12: false })
+ : "";
+ localPending.set(block.id, { command: block.input.command, timestamp: entryTime });
+ }
+ }
+ }
+
+ if (entry.type === "user" && Array.isArray(entry.message?.content)) {
+ for (const block of entry.message.content) {
+ if (block.type === "tool_result" && block.tool_use_id) {
+ const p = localPending.get(block.tool_use_id);
+ if (p) {
+ let output = "";
+ if (typeof block.content === "string") output = block.content;
+ else if (Array.isArray(block.content))
+ output = block.content.filter((x: any) => x.type === "text").map((x: any) => x.text || "").join("\n");
+
+ allCmds.push({
+ cmd: p.command,
+ output,
+ isError: block.is_error === true,
+ ts: p.timestamp,
+ });
+ localPending.delete(block.tool_use_id);
+ }
+ }
+ }
+ }
+ } catch {}
+ }
+
+ const toShow = allCmds.slice(-lastN);
+ for (const { cmd, output, isError, ts } of toShow) {
+ if (sshOnly && !cmd.includes("ssh") && !cmd.includes("sshpass")) continue;
+ displayCommand(cmd, output, isError, sessionShort, ts);
+ }
+
+ console.log(`\n${c.dim}${toShow.length}/${allCmds.length} commands shown.${c.reset}`);
+ process.exit(0);
+ }
+
+ // Mode: Live stream
+ initLogFile();
+
+ if (sshOnly) console.log(`${c.yellow}Filter: SSH/remote commands only${c.reset}`);
+ console.log(`${c.dim}Watching for new commands...${c.reset}\n`);
+
+ for (const dir of dirs) {
+ const files = getRecentJsonl(dir, 10);
+
+ for (const file of files) {
+ watchFile(file);
+ }
+
+ watchDir(dir);
+ }
+
+ console.log(`${c.dim}Streaming... (Ctrl+C to stop)${c.reset}\n`);
+
+ process.on("SIGINT", () => {
+ console.log(`\n${c.dim}${cmdCount} commands captured.${c.reset}`);
+ if (logFile) console.log(`${c.dim}Log saved: ${logFile}${c.reset}`);
+ process.exit(0);
+ });
+}
+
+main();
diff --git a/Releases/v3.0/.claude/Observability/Tools/obs-tui.ts b/Releases/v3.0/.claude/Observability/Tools/obs-tui.ts
new file mode 100644
index 000000000..959d2d4a2
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/Tools/obs-tui.ts
@@ -0,0 +1,448 @@
+#!/usr/bin/env bun
+/**
+ * obs-tui — Terminal UI for Claude Code Observability
+ *
+ * Watches Claude Code JSONL session transcripts in real-time and displays
+ * tool calls, responses, and user prompts with color-coded output.
+ *
+ * Usage:
+ * bun ~/.claude/Observability/Tools/obs-tui.ts [options]
+ *
+ * --all Watch ALL project dirs (default: current dir's project only)
+ * --recent N Show last N events on startup (default: 20)
+ * --no-color Disable colors
+ * --tools-only Only show tool use events
+ * --filter TYPE Filter by event type (e.g. PreToolUse, Stop, UserPromptSubmit)
+ */
+
+import { watch, existsSync, readdirSync, statSync, readFileSync } from "fs";
+import { join, basename } from "path";
+import { homedir } from "os";
+
+// ── Config ──────────────────────────────────────────────────────────
+
+const PROJECTS_BASE = join(homedir(), ".claude", "projects");
+const args = process.argv.slice(2);
+const watchAll = args.includes("--all");
+const noColor = args.includes("--no-color");
+const toolsOnly = args.includes("--tools-only");
+const filterType = args.includes("--filter")
+ ? args[args.indexOf("--filter") + 1]
+ : null;
+const recentCount = args.includes("--recent")
+ ? parseInt(args[args.indexOf("--recent") + 1]) || 20
+ : 20;
+
+// ── Colors ──────────────────────────────────────────────────────────
+
+const c = noColor
+ ? {
+ reset: "", dim: "", bold: "", italic: "",
+ red: "", green: "", yellow: "", blue: "", magenta: "", cyan: "", white: "", gray: "",
+ bgRed: "", bgGreen: "", bgYellow: "", bgBlue: "", bgMagenta: "", bgCyan: "",
+ }
+ : {
+ reset: "\x1b[0m",
+ dim: "\x1b[2m",
+ bold: "\x1b[1m",
+ italic: "\x1b[3m",
+ red: "\x1b[31m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ blue: "\x1b[34m",
+ magenta: "\x1b[35m",
+ cyan: "\x1b[36m",
+ white: "\x1b[37m",
+ gray: "\x1b[90m",
+ bgRed: "\x1b[41m",
+ bgGreen: "\x1b[42m",
+ bgYellow: "\x1b[43m",
+ bgBlue: "\x1b[44m",
+ bgMagenta: "\x1b[45m",
+ bgCyan: "\x1b[46m",
+ };
+
+// ── Tool color map ──────────────────────────────────────────────────
+
+const toolColors: Record = {
+ Bash: c.red,
+ Read: c.blue,
+ Write: c.magenta,
+ Edit: c.yellow,
+ Grep: c.cyan,
+ Glob: c.cyan,
+ Task: c.green,
+ WebFetch: c.magenta,
+ WebSearch: c.magenta,
+ TaskCreate: c.green,
+ TaskUpdate: c.green,
+ TaskList: c.green,
+ AskUserQuestion: c.yellow,
+ Skill: c.magenta,
+ NotebookEdit: c.yellow,
+};
+
+// ── State ───────────────────────────────────────────────────────────
+
+const filePositions = new Map();
+const watchedFiles = new Set();
+let eventCount = 0;
+
+// ── Helpers ─────────────────────────────────────────────────────────
+
+function timestamp(): string {
+ const now = new Date();
+ const h = now.getHours().toString().padStart(2, "0");
+ const m = now.getMinutes().toString().padStart(2, "0");
+ const s = now.getSeconds().toString().padStart(2, "0");
+ return `${c.dim}${h}:${m}:${s}${c.reset}`;
+}
+
+function truncate(s: string, max: number): string {
+ if (!s) return "";
+ const oneLine = s.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
+ return oneLine.length > max ? oneLine.slice(0, max) + "..." : oneLine;
+}
+
+function formatToolUse(toolName: string, input: any): string {
+ const color = toolColors[toolName] || c.white;
+ const tag = `${color}${c.bold}${toolName}${c.reset}`;
+
+ switch (toolName) {
+ case "Bash":
+ return `${tag} ${c.dim}$${c.reset} ${truncate(input?.command || "", 120)}`;
+ case "Read":
+ return `${tag} ${c.dim}${input?.file_path?.replace(homedir(), "~") || ""}${c.reset}`;
+ case "Write":
+ return `${tag} ${c.dim}${input?.file_path?.replace(homedir(), "~") || ""}${c.reset} ${c.yellow}(${(input?.content?.length || 0)} chars)${c.reset}`;
+ case "Edit":
+ return `${tag} ${c.dim}${input?.file_path?.replace(homedir(), "~") || ""}${c.reset}`;
+ case "Grep":
+ return `${tag} ${c.dim}/${input?.pattern || ""}/${c.reset} ${input?.path?.replace(homedir(), "~") || ""}`;
+ case "Glob":
+ return `${tag} ${c.dim}${input?.pattern || ""}${c.reset} ${input?.path?.replace(homedir(), "~") || ""}`;
+ case "Task":
+ return `${tag} ${c.green}[${input?.subagent_type || "?"}]${c.reset} ${truncate(input?.description || input?.prompt || "", 80)}`;
+ case "TaskCreate":
+ return `${tag} ${c.green}+${c.reset} ${truncate(input?.subject || "", 80)}`;
+ case "TaskUpdate":
+ return `${tag} ${c.dim}#${input?.taskId}${c.reset} ${input?.status || ""}`;
+ case "WebSearch":
+ return `${tag} ${c.dim}q=${c.reset}${truncate(input?.query || "", 80)}`;
+ case "WebFetch":
+ return `${tag} ${c.dim}${truncate(input?.url || "", 80)}${c.reset}`;
+ case "AskUserQuestion":
+ const q = input?.questions?.[0]?.question || "";
+ return `${tag} ${c.yellow}?${c.reset} ${truncate(q, 80)}`;
+ case "Skill":
+ return `${tag} ${c.magenta}/${input?.skill || ""}${c.reset}`;
+ default:
+ return `${tag} ${c.dim}${truncate(JSON.stringify(input || {}), 100)}${c.reset}`;
+ }
+}
+
+// ── Event Display ───────────────────────────────────────────────────
+
+function displayEvent(entry: any, sessionShort: string): void {
+ const ts = timestamp();
+ const sid = `${c.dim}[${sessionShort}]${c.reset}`;
+
+ // User message
+ if (entry.type === "user" && entry.message?.role === "user") {
+ if (toolsOnly) return;
+
+ const content = entry.message.content;
+
+ // Check for tool result
+ if (Array.isArray(content)) {
+ const toolResult = content.find((c: any) => c.type === "tool_result");
+ if (toolResult) {
+ if (filterType && filterType !== "PostToolUse") return;
+ const resultText =
+ typeof toolResult.content === "string"
+ ? toolResult.content
+ : JSON.stringify(toolResult.content);
+ const isError = toolResult.is_error;
+ const statusIcon = isError ? `${c.red}ERR${c.reset}` : `${c.green}OK${c.reset}`;
+ console.log(
+ `${ts} ${sid} ${c.dim} <- ${statusIcon} ${truncate(resultText, 120)}${c.reset}`
+ );
+ return;
+ }
+ }
+
+ if (filterType && filterType !== "UserPromptSubmit") return;
+
+ let userText = "";
+ if (typeof content === "string") {
+ userText = content;
+ } else if (Array.isArray(content)) {
+ userText = content
+ .filter((c: any) => c.type === "text")
+ .map((c: any) => c.text)
+ .join(" ");
+ }
+
+ // Skip system-reminder content
+ if (userText.includes("") && userText.length > 500) {
+ console.log(
+ `${ts} ${sid} ${c.bold}${c.cyan}USER${c.reset} ${c.dim}(system-reminder + prompt)${c.reset}`
+ );
+ return;
+ }
+
+ console.log(
+ `${ts} ${sid} ${c.bold}${c.cyan}USER${c.reset} ${truncate(userText, 120)}`
+ );
+ eventCount++;
+ return;
+ }
+
+ // Assistant message
+ if (entry.type === "assistant" && entry.message?.role === "assistant") {
+ const content = entry.message.content;
+ if (!Array.isArray(content)) return;
+
+ for (const block of content) {
+ // Tool use
+ if (block.type === "tool_use") {
+ if (filterType && filterType !== "PreToolUse") return;
+ console.log(
+ `${ts} ${sid} ${c.bold}->${c.reset} ${formatToolUse(block.name, block.input)}`
+ );
+ eventCount++;
+ continue;
+ }
+
+ // Thinking
+ if (block.type === "thinking") {
+ if (toolsOnly) continue;
+ if (filterType && filterType !== "Thinking") continue;
+ console.log(
+ `${ts} ${sid} ${c.dim}${c.italic} THINK ${truncate(block.thinking || "", 100)}${c.reset}`
+ );
+ continue;
+ }
+
+ // Text response
+ if (block.type === "text") {
+ if (toolsOnly) continue;
+ if (filterType && filterType !== "Stop") continue;
+ const text = block.text || "";
+ if (text.length < 5) continue;
+ // Show first line of response
+ const firstLine = text.split("\n").find((l: string) => l.trim().length > 0) || "";
+ console.log(
+ `${ts} ${sid} ${c.bold}${c.green}RESP${c.reset} ${truncate(firstLine, 120)}`
+ );
+ eventCount++;
+ continue;
+ }
+ }
+ return;
+ }
+
+ // System/result messages
+ if (entry.type === "system") {
+ if (toolsOnly) return;
+ console.log(`${ts} ${sid} ${c.dim}SYS${c.reset} ${truncate(JSON.stringify(entry), 100)}`);
+ return;
+ }
+}
+
+// ── File Processing ─────────────────────────────────────────────────
+
+function processNewLines(filePath: string, sessionShort: string): void {
+ if (!existsSync(filePath)) return;
+
+ const lastPos = filePositions.get(filePath) || 0;
+ const content = readFileSync(filePath, "utf-8");
+ const newContent = content.slice(lastPos);
+
+ filePositions.set(filePath, content.length);
+
+ if (!newContent.trim()) return;
+
+ for (const line of newContent.trim().split("\n")) {
+ if (!line.trim()) continue;
+ try {
+ const entry = JSON.parse(line);
+ // Skip queue-operation and summary
+ if (entry.type === "queue-operation" || entry.type === "summary") continue;
+ displayEvent(entry, sessionShort);
+ } catch {
+ // Skip malformed lines
+ }
+ }
+}
+
+function showRecentEvents(filePath: string, sessionShort: string, count: number): void {
+ if (!existsSync(filePath)) return;
+
+ const content = readFileSync(filePath, "utf-8");
+ const lines = content.trim().split("\n").filter((l) => l.trim());
+
+ // Parse all lines first to count displayable events
+ const entries: any[] = [];
+ for (const line of lines) {
+ try {
+ const entry = JSON.parse(line);
+ if (entry.type === "queue-operation" || entry.type === "summary") continue;
+ entries.push(entry);
+ } catch {}
+ }
+
+ // Show last N entries
+ const recent = entries.slice(-count);
+ for (const entry of recent) {
+ displayEvent(entry, sessionShort);
+ }
+
+ // Set position to end of file so we only get new events going forward
+ filePositions.set(filePath, content.length);
+}
+
+// ── Watch Logic ─────────────────────────────────────────────────────
+
+function getProjectDirs(): string[] {
+ if (watchAll) {
+ return readdirSync(PROJECTS_BASE)
+ .filter((d) => d.startsWith("-"))
+ .map((d) => join(PROJECTS_BASE, d))
+ .filter((d) => statSync(d).isDirectory());
+ }
+
+ // Auto-detect from CWD
+ const cwd = process.cwd().replace(/\//g, "-").replace(/^-/, "-");
+ const projectDir = join(PROJECTS_BASE, cwd);
+ if (existsSync(projectDir)) {
+ return [projectDir];
+ }
+
+ // Fallback: find the most recently modified project dir
+ const dirs = readdirSync(PROJECTS_BASE)
+ .filter((d) => d.startsWith("-"))
+ .map((d) => ({
+ name: d,
+ path: join(PROJECTS_BASE, d),
+ mtime: statSync(join(PROJECTS_BASE, d)).mtime.getTime(),
+ }))
+ .filter((d) => statSync(d.path).isDirectory())
+ .sort((a, b) => b.mtime - a.mtime);
+
+ if (dirs.length > 0) {
+ console.log(
+ `${c.yellow}No project dir for CWD. Using most recent: ${dirs[0].name}${c.reset}`
+ );
+ return [dirs[0].path];
+ }
+
+ return [];
+}
+
+function getRecentJsonl(dir: string, limit: number = 10): string[] {
+ return readdirSync(dir)
+ .filter((f) => f.endsWith(".jsonl"))
+ .map((f) => ({
+ name: f,
+ path: join(dir, f),
+ mtime: statSync(join(dir, f)).mtime.getTime(),
+ }))
+ .sort((a, b) => b.mtime - a.mtime)
+ .slice(0, limit)
+ .map((f) => f.path);
+}
+
+function watchFile(filePath: string): void {
+ if (watchedFiles.has(filePath)) return;
+ watchedFiles.add(filePath);
+
+ const sessionShort = basename(filePath, ".jsonl").slice(0, 8);
+
+ const watcher = watch(filePath, (eventType) => {
+ if (eventType === "change") {
+ processNewLines(filePath, sessionShort);
+ }
+ });
+
+ watcher.on("error", () => {
+ watchedFiles.delete(filePath);
+ });
+}
+
+function watchDir(dir: string): void {
+ watch(dir, (eventType, filename) => {
+ if (filename && filename.endsWith(".jsonl")) {
+ const filePath = join(dir, filename);
+ if (existsSync(filePath) && !watchedFiles.has(filePath)) {
+ console.log(
+ `${timestamp()} ${c.green}+ New session${c.reset} ${c.dim}${basename(filePath, ".jsonl").slice(0, 8)}${c.reset}`
+ );
+ watchFile(filePath);
+ }
+ }
+ });
+}
+
+// ── Main ────────────────────────────────────────────────────────────
+
+function main(): void {
+ const dirs = getProjectDirs();
+
+ if (dirs.length === 0) {
+ console.error(`${c.red}No Claude Code project directories found.${c.reset}`);
+ process.exit(1);
+ }
+
+ // Header
+ console.log(
+ `\n${c.bold}${c.blue}obs-tui${c.reset} ${c.dim}— Claude Code Live Event Stream${c.reset}`
+ );
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}`);
+ console.log(
+ `${c.dim}Watching ${dirs.length} project dir(s) | Recent: ${recentCount} events${c.reset}`
+ );
+ if (toolsOnly) console.log(`${c.yellow}Filter: tools only${c.reset}`);
+ if (filterType) console.log(`${c.yellow}Filter: ${filterType}${c.reset}`);
+ console.log(`${c.dim}${"─".repeat(50)}${c.reset}\n`);
+
+ for (const dir of dirs) {
+ const files = getRecentJsonl(dir, 5);
+ const dirShort = basename(dir);
+
+ console.log(
+ `${c.dim}Project: ${dirShort} (${files.length} recent sessions)${c.reset}`
+ );
+
+ // Show recent events from the MOST recent file only
+ if (files.length > 0 && recentCount > 0) {
+ const sessionShort = basename(files[0], ".jsonl").slice(0, 8);
+ console.log(`${c.dim}─── Recent events from ${sessionShort} ───${c.reset}`);
+ showRecentEvents(files[0], sessionShort, recentCount);
+ console.log(`${c.dim}─── Live stream ───${c.reset}\n`);
+ }
+
+ // Watch all recent files for changes
+ for (const file of files) {
+ // Set position to end for files we didn't show recent events from
+ if (file !== files[0]) {
+ const content = readFileSync(file, "utf-8");
+ filePositions.set(file, content.length);
+ }
+ watchFile(file);
+ }
+
+ // Watch for new session files
+ watchDir(dir);
+ }
+
+ console.log(`${c.dim}Streaming... (Ctrl+C to stop)${c.reset}\n`);
+
+ // Keep alive
+ process.on("SIGINT", () => {
+ console.log(`\n${c.dim}${eventCount} events displayed. Goodbye.${c.reset}`);
+ process.exit(0);
+ });
+}
+
+main();
diff --git a/Releases/v3.0/.claude/Observability/apps/client/README.md b/Releases/v3.0/.claude/Observability/apps/client/README.md
new file mode 100644
index 000000000..33895ab20
--- /dev/null
+++ b/Releases/v3.0/.claude/Observability/apps/client/README.md
@@ -0,0 +1,5 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
+