diff --git a/PLATFORM.md b/PLATFORM.md
index ff7bca6a0a..f22042544d 100644
--- a/PLATFORM.md
+++ b/PLATFORM.md
@@ -28,7 +28,7 @@ This document tracks all platform-specific code and dependencies across PAI, pro
- **Status:** Fixed with conditional `uname -s` detection
2. ✅ `/opt/homebrew/bin` hardcoded in PATH
- - **Files:** `pai-observability-server/src/observability/manage.sh:8`, `pai-observability-server.md:1316`
+ - **Files:** `pai-observability-server/src/Observability/manage.sh:8`, `pai-observability-server.md:1316`
- **Fix:** Conditional PATH based on directory existence
- **Status:** Fixed with `[ -d "/opt/homebrew/bin" ]` check
diff --git a/Packs/pai-observability-server/src/observability/apps/client/index.html b/Packs/pai-observability-server/src/observability/apps/client/index.html
deleted file mode 100755
index 3e68f0c340..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Agents
-
-
-
-
-
-
diff --git a/Packs/pai-observability-server/src/observability/apps/client/package.json b/Packs/pai-observability-server/src/observability/apps/client/package.json
deleted file mode 100755
index 47a522146e..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/package.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "name": "multi-agent-observability-client",
- "private": true,
- "version": "1.2.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vue-tsc -b && vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "lucide-vue-next": "^0.548.0",
- "vue": "^3.5.17"
- },
- "devDependencies": {
- "@types/node": "^22.11.2",
- "@vitejs/plugin-vue": "^6.0.0",
- "@vue/tsconfig": "^0.7.0",
- "autoprefixer": "^10.4.20",
- "postcss": "^8.5.3",
- "tailwindcss": "^3.4.16",
- "typescript": "~5.8.3",
- "vite": "^7.0.4",
- "vue-tsc": "^2.2.12"
- }
-}
diff --git a/Packs/pai-observability-server/src/observability/apps/client/postcss.config.js b/Packs/pai-observability-server/src/observability/apps/client/postcss.config.js
deleted file mode 100755
index e99ebc2c0e..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
\ No newline at end of file
diff --git a/Packs/pai-observability-server/src/observability/apps/client/src/App.vue b/Packs/pai-observability-server/src/observability/apps/client/src/App.vue
deleted file mode 100755
index 80f3c63318..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/src/App.vue
+++ /dev/null
@@ -1,223 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Packs/pai-observability-server/src/observability/apps/client/src/main.ts b/Packs/pai-observability-server/src/observability/apps/client/src/main.ts
deleted file mode 100755
index 064c7a1994..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/src/main.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createApp } from 'vue'
-import './assets/fonts.css'
-import './styles/main.css'
-import './styles/themes.css'
-import './styles/compact.css'
-import App from './App.vue'
-
-createApp(App).mount('#app')
diff --git a/Packs/pai-observability-server/src/observability/apps/client/src/style.css b/Packs/pai-observability-server/src/observability/apps/client/src/style.css
deleted file mode 100755
index 41e4388427..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/src/style.css
+++ /dev/null
@@ -1,104 +0,0 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
- overflow-x: hidden; /* Prevent horizontal scrolling on mobile */
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-.card {
- padding: 2em;
-}
-
-#app {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
-
-/* Mobile optimizations */
-@media (max-width: 699px) {
- body {
- place-items: stretch; /* Allow full width on mobile */
- }
-
- #app {
- max-width: none; /* Remove max-width constraint on mobile */
- padding: 0; /* Remove default padding on mobile */
- text-align: left; /* Left-align text on mobile for better readability */
- }
-
- /* Improve touch targets on mobile */
- button, select, input[type="button"], input[type="submit"] {
- min-height: 44px;
- min-width: 44px;
- }
-
- /* Optimize text selection on mobile */
- * {
- -webkit-tap-highlight-color: transparent;
- }
-}
diff --git a/Packs/pai-observability-server/src/observability/apps/client/tailwind.config.js b/Packs/pai-observability-server/src/observability/apps/client/tailwind.config.js
deleted file mode 100755
index 53cbf2afde..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/tailwind.config.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: [
- "./index.html",
- "./src/**/*.{vue,js,ts,jsx,tsx}",
- ],
- darkMode: 'class',
- theme: {
- extend: {
- fontFamily: {
- "equity-text-b": ["equity-text-b", "Georgia", "serif"],
- "concourse-t3": ["concourse-t3", "sans-serif"],
- "concourse-c3": ["concourse-c3", "sans-serif"],
- "advocate": ["advocate", "sans-serif"],
- "valkyrie-text": ["valkyrie-text", "Georgia", "serif"],
- "sans": ["concourse-t3", "system-ui", "-apple-system", "sans-serif"],
- "serif": ["valkyrie-text", "Georgia", "serif"],
- "mono": ["ui-monospace", "SFMono-Regular", "monospace"],
- },
- screens: {
- 'mobile': {'max': '699px'}, // Custom mobile breakpoint for < 700px
- 'short': {'raw': '(max-height: 400px)'}, // Custom breakpoint for height <= 400px
- },
- colors: {
- // Theme-aware colors using CSS custom properties
- 'theme': {
- 'primary': 'var(--theme-primary)',
- 'primary-hover': 'var(--theme-primary-hover)',
- 'primary-light': 'var(--theme-primary-light)',
- 'primary-dark': 'var(--theme-primary-dark)',
- 'bg': {
- 'primary': 'var(--theme-bg-primary)',
- 'secondary': 'var(--theme-bg-secondary)',
- 'tertiary': 'var(--theme-bg-tertiary)',
- 'quaternary': 'var(--theme-bg-quaternary)',
- },
- 'text': {
- 'primary': 'var(--theme-text-primary)',
- 'secondary': 'var(--theme-text-secondary)',
- 'tertiary': 'var(--theme-text-tertiary)',
- 'quaternary': 'var(--theme-text-quaternary)',
- },
- 'border': {
- 'primary': 'var(--theme-border-primary)',
- 'secondary': 'var(--theme-border-secondary)',
- 'tertiary': 'var(--theme-border-tertiary)',
- },
- 'accent': {
- 'success': 'var(--theme-accent-success)',
- 'warning': 'var(--theme-accent-warning)',
- 'error': 'var(--theme-accent-error)',
- 'info': 'var(--theme-accent-info)',
- }
- }
- },
- boxShadow: {
- 'theme': 'var(--theme-shadow)',
- 'theme-lg': 'var(--theme-shadow-lg)',
- },
- transitionProperty: {
- 'theme': 'var(--theme-transition)',
- 'theme-fast': 'var(--theme-transition-fast)',
- }
- },
- },
- plugins: [],
- safelist: [
- // Background colors
- 'bg-blue-500',
- 'bg-green-500',
- 'bg-yellow-500',
- 'bg-purple-500',
- 'bg-pink-500',
- 'bg-indigo-500',
- 'bg-red-500',
- 'bg-orange-500',
- 'bg-teal-500',
- 'bg-cyan-500',
- // Border colors
- 'border-blue-500',
- 'border-green-500',
- 'border-yellow-500',
- 'border-purple-500',
- 'border-pink-500',
- 'border-indigo-500',
- 'border-red-500',
- 'border-orange-500',
- 'border-teal-500',
- 'border-cyan-500',
- // Gradient colors
- 'from-blue-500',
- 'to-blue-600',
- 'from-green-500',
- 'to-green-600',
- 'from-yellow-500',
- 'to-yellow-600',
- 'from-purple-500',
- 'to-purple-600',
- 'from-pink-500',
- 'to-pink-600',
- 'from-indigo-500',
- 'to-indigo-600',
- 'from-red-500',
- 'to-red-600',
- 'from-orange-500',
- 'to-orange-600',
- 'from-teal-500',
- 'to-teal-600',
- 'from-cyan-500',
- 'to-cyan-600',
- // Theme classes
- 'theme-bg-primary',
- 'theme-bg-secondary',
- 'theme-bg-tertiary',
- 'theme-bg-quaternary',
- 'theme-text-primary',
- 'theme-text-secondary',
- 'theme-text-tertiary',
- 'theme-text-quaternary',
- 'theme-border-primary',
- 'theme-border-secondary',
- 'theme-border-tertiary',
- 'theme-primary',
- 'theme-primary-bg',
- 'theme-primary-border',
- 'theme-accent-success',
- 'theme-accent-warning',
- 'theme-accent-error',
- 'theme-accent-info',
- 'theme-shadow',
- 'theme-shadow-lg',
- 'theme-transition',
- 'theme-transition-fast',
- 'theme-hover',
- 'theme-active',
- 'theme-focus',
- 'backdrop-blur',
- // Theme class names
- 'theme-light',
- 'theme-dark',
- 'theme-modern',
- 'theme-earth',
- 'theme-glass',
- 'theme-high-contrast',
- ]
-}
\ No newline at end of file
diff --git a/Packs/pai-observability-server/src/observability/apps/client/vite.config.ts b/Packs/pai-observability-server/src/observability/apps/client/vite.config.ts
deleted file mode 100755
index 08eed6896b..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/client/vite.config.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [vue()],
- server: {
- port: 5172,
- strictPort: true,
- },
-})
diff --git a/Packs/pai-observability-server/src/observability/apps/server/package.json b/Packs/pai-observability-server/src/observability/apps/server/package.json
deleted file mode 100755
index cadaeb2b7c..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/server/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "multi-agent-observability-server",
- "version": "1.2.0",
- "module": "src/index.ts",
- "type": "module",
- "private": true,
- "scripts": {
- "dev": "bun --watch src/index.ts",
- "start": "bun src/index.ts",
- "typecheck": "tsc --noEmit"
- },
- "devDependencies": {
- "@types/bun": "latest",
- "@types/ws": "^8.5.13",
- "typescript": "^5.8.3"
- },
- "dependencies": {}
-}
diff --git a/Packs/pai-observability-server/src/observability/apps/server/src/file-ingest.ts b/Packs/pai-observability-server/src/observability/apps/server/src/file-ingest.ts
deleted file mode 100755
index 01a1bfc644..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/server/src/file-ingest.ts
+++ /dev/null
@@ -1,504 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Projects-based Event Streaming (In-Memory Only)
- * Watches Claude Code's native projects/ directory for session transcripts
- * NO DATABASE - streams directly to WebSocket clients
- * Fresh start each time - no persistence
- *
- * Replaces RAW-based ingestion - reads from native Claude Code storage
- */
-
-import { watch, existsSync, readdirSync, statSync } from 'fs';
-import { readFileSync } from 'fs';
-import { join } from 'path';
-import { homedir } from 'os';
-import type { HookEvent } from './types';
-
-// In-memory event store (last N events only)
-const MAX_EVENTS = 1000;
-const events: HookEvent[] = [];
-
-// Track the last read position for each file
-const filePositions = new Map();
-
-// Track which files we're currently watching
-const watchedFiles = new Set();
-
-// Callback for when new events arrive (for WebSocket broadcasting)
-let onEventsReceived: ((events: HookEvent[]) => void) | null = null;
-
-// Agent session mapping (session_id -> agent_name)
-const agentSessions = new Map();
-
-// Todo tracking per session (session_id -> current todos)
-const sessionTodos = new Map();
-
-// Projects directory path - dynamically constructed from username
-const PROJECTS_DIR = join(homedir(), '.claude', 'projects', `-Users-${process.env.USER || 'user'}--claude`);
-
-/**
- * Get the most recently modified JSONL files in projects/
- */
-function getRecentSessionFiles(limit: number = 50): string[] {
- if (!existsSync(PROJECTS_DIR)) {
- console.log('⚠️ Projects directory not found:', PROJECTS_DIR);
- return [];
- }
-
- const files = readdirSync(PROJECTS_DIR)
- .filter(f => f.endsWith('.jsonl'))
- .map(f => ({
- name: f,
- path: join(PROJECTS_DIR, f),
- mtime: statSync(join(PROJECTS_DIR, f)).mtime.getTime()
- }))
- .sort((a, b) => b.mtime - a.mtime)
- .slice(0, limit);
-
- return files.map(f => f.path);
-}
-
-/**
- * Parse a Claude Code projects JSONL entry and convert to HookEvent format
- */
-function parseProjectsEntry(entry: any): HookEvent | null {
- // Skip queue operations
- if (entry.type === 'queue-operation') {
- return null;
- }
-
- // Skip summary entries
- if (entry.type === 'summary') {
- return null;
- }
-
- const rawTimestamp = entry.timestamp || new Date().toISOString();
- const sessionId = entry.sessionId || 'unknown';
-
- // Convert timestamp to numeric (ms since epoch) for client chart compatibility
- const timestamp = typeof rawTimestamp === 'string'
- ? new Date(rawTimestamp).getTime()
- : rawTimestamp;
-
- // Base event structure
- const baseEvent: Partial = {
- source_app: 'claude-code',
- session_id: sessionId,
- timestamp: timestamp,
- timestamp_pst: new Date(timestamp).toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }),
- };
-
- // User message -> UserPromptSubmit
- if (entry.type === 'user' && entry.message?.role === 'user') {
- const content = entry.message.content;
- let userText = '';
-
- if (typeof content === 'string') {
- userText = content;
- } else if (Array.isArray(content)) {
- // Check if it's a tool result
- const toolResult = content.find((c: any) => c.type === 'tool_result');
- if (toolResult) {
- return {
- ...baseEvent,
- hook_event_type: 'PostToolUse',
- payload: {
- tool_use_id: toolResult.tool_use_id,
- tool_result: typeof toolResult.content === 'string'
- ? toolResult.content.slice(0, 500)
- : JSON.stringify(toolResult.content).slice(0, 500)
- },
- summary: `Tool result received`
- } as HookEvent;
- }
-
- // Regular text content
- userText = content
- .filter((c: any) => c.type === 'text')
- .map((c: any) => c.text)
- .join(' ');
- }
-
- return {
- ...baseEvent,
- hook_event_type: 'UserPromptSubmit',
- payload: {
- prompt: userText.slice(0, 500)
- },
- summary: userText.slice(0, 100)
- } as HookEvent;
- }
-
- // Assistant message -> Stop or PreToolUse
- if (entry.type === 'assistant' && entry.message?.role === 'assistant') {
- const content = entry.message.content;
-
- if (Array.isArray(content)) {
- // Check for tool_use
- const toolUse = content.find((c: any) => c.type === 'tool_use');
- if (toolUse) {
- return {
- ...baseEvent,
- hook_event_type: 'PreToolUse',
- payload: {
- tool_name: toolUse.name,
- tool_input: toolUse.input
- },
- summary: `${toolUse.name}: ${JSON.stringify(toolUse.input).slice(0, 100)}`
- } as HookEvent;
- }
-
- // Text response -> Stop event
- const textContent = content.find((c: any) => c.type === 'text');
- if (textContent) {
- return {
- ...baseEvent,
- hook_event_type: 'Stop',
- payload: {
- response: textContent.text?.slice(0, 500)
- },
- summary: textContent.text?.slice(0, 100)
- } as HookEvent;
- }
- }
- }
-
- return null;
-}
-
-/**
- * Read new events from a JSONL file starting from a given position
- */
-function readNewEvents(filePath: string): HookEvent[] {
- if (!existsSync(filePath)) {
- return [];
- }
-
- const lastPosition = filePositions.get(filePath) || 0;
-
- try {
- const content = readFileSync(filePath, 'utf-8');
- const newContent = content.slice(lastPosition);
-
- // Update position to end of file
- filePositions.set(filePath, content.length);
-
- if (!newContent.trim()) {
- return [];
- }
-
- // Parse JSONL - one JSON object per line
- const lines = newContent.trim().split('\n');
- const newEvents: HookEvent[] = [];
-
- for (const line of lines) {
- if (!line.trim()) continue;
-
- try {
- const entry = JSON.parse(line);
- const event = parseProjectsEntry(entry);
-
- if (event) {
- // Add auto-incrementing ID for UI
- event.id = events.length + newEvents.length + 1;
- // Enrich with agent name
- const enrichedEvent = enrichEventWithAgentName(event);
- // Process todo events (returns array of events)
- const processedEvents = processTodoEvent(enrichedEvent);
- // Reassign IDs for any synthetic events
- for (let i = 0; i < processedEvents.length; i++) {
- processedEvents[i].id = events.length + newEvents.length + i + 1;
- }
- newEvents.push(...processedEvents);
- }
- } catch (error) {
- // Skip malformed lines silently
- }
- }
-
- return newEvents;
- } catch (error) {
- console.error(`Error reading file ${filePath}:`, error);
- return [];
- }
-}
-
-/**
- * Add events to in-memory store (keeping last MAX_EVENTS only)
- */
-function storeEvents(newEvents: HookEvent[]): void {
- if (newEvents.length === 0) return;
-
- // Add to in-memory array
- events.push(...newEvents);
-
- // Keep only last MAX_EVENTS
- if (events.length > MAX_EVENTS) {
- events.splice(0, events.length - MAX_EVENTS);
- }
-
- console.log(`✅ Received ${newEvents.length} event(s) (${events.length} in memory)`);
-
- // Notify subscribers (WebSocket clients)
- if (onEventsReceived) {
- onEventsReceived(newEvents);
- }
-}
-
-/**
- * Load agent sessions from agent-sessions.json
- */
-function loadAgentSessions(): void {
- const sessionsFile = join(homedir(), '.claude', 'MEMORY', 'STATE', 'agent-sessions.json');
-
- if (!existsSync(sessionsFile)) {
- console.log('⚠️ agent-sessions.json not found, agent names will be "unknown"');
- return;
- }
-
- try {
- const content = readFileSync(sessionsFile, 'utf-8');
- const data = JSON.parse(content);
-
- agentSessions.clear();
- Object.entries(data).forEach(([sessionId, agentName]) => {
- agentSessions.set(sessionId, agentName as string);
- });
-
- console.log(`✅ Loaded ${agentSessions.size} agent sessions`);
- } catch (error) {
- console.error('❌ Error loading agent-sessions.json:', error);
- }
-}
-
-/**
- * Watch agent-sessions.json for changes
- */
-function watchAgentSessions(): void {
- const sessionsFile = join(homedir(), '.claude', 'MEMORY', 'STATE', 'agent-sessions.json');
-
- if (!existsSync(sessionsFile)) {
- console.log('⚠️ agent-sessions.json not found, skipping watch');
- return;
- }
-
- console.log('👀 Watching agent-sessions.json for changes');
-
- const watcher = watch(sessionsFile, (eventType) => {
- if (eventType === 'change') {
- console.log('🔄 agent-sessions.json changed, reloading...');
- loadAgentSessions();
- }
- });
-
- watcher.on('error', (error) => {
- console.error('❌ Error watching agent-sessions.json:', error);
- });
-}
-
-/**
- * Enrich event with agent name from session mapping
- */
-function enrichEventWithAgentName(event: HookEvent): HookEvent {
- // Special case: UserPromptSubmit events are from Daniel, not the agent
- if (event.hook_event_type === 'UserPromptSubmit') {
- return {
- ...event,
- agent_name: 'Daniel'
- };
- }
-
- // Default to DA name for main agent sessions (from settings.json env)
- const mainAgentName = process.env.DA || 'PAI';
-
- // If source_app is set to a sub-agent type (not the main agent), respect it
- const subAgentTypes = ['artist', 'intern', 'engineer', 'pentester', 'architect', 'designer', 'qatester', 'researcher'];
- if (event.source_app && subAgentTypes.includes(event.source_app.toLowerCase())) {
- const capitalizedName = event.source_app.charAt(0).toUpperCase() + event.source_app.slice(1);
- return {
- ...event,
- agent_name: capitalizedName
- };
- }
-
- const agentName = agentSessions.get(event.session_id) || mainAgentName;
- return {
- ...event,
- agent_name: agentName
- };
-}
-
-/**
- * Process todo events and detect completions
- */
-function processTodoEvent(event: HookEvent): HookEvent[] {
- // Only process TodoWrite tool events
- if (event.payload?.tool_name !== 'TodoWrite') {
- return [event];
- }
-
- const currentTodos = event.payload.tool_input?.todos || [];
- const previousTodos = sessionTodos.get(event.session_id) || [];
-
- // Find newly completed todos
- const completedTodos = [];
-
- for (const currentTodo of currentTodos) {
- if (currentTodo.status === 'completed') {
- const prevTodo = previousTodos.find((t: any) => t.content === currentTodo.content);
- if (!prevTodo || prevTodo.status !== 'completed') {
- completedTodos.push(currentTodo);
- }
- }
- }
-
- // Update session todos
- sessionTodos.set(event.session_id, currentTodos);
-
- // Create synthetic completion events
- const resultEvents: HookEvent[] = [event];
-
- for (const completedTodo of completedTodos) {
- const completionEvent: HookEvent = {
- ...event,
- id: event.id,
- hook_event_type: 'Completed',
- payload: {
- task: completedTodo.content
- },
- summary: undefined,
- timestamp: event.timestamp
- };
- resultEvents.push(completionEvent);
- }
-
- return resultEvents;
-}
-
-/**
- * Watch a file for changes and stream new events
- */
-function watchFile(filePath: string): void {
- if (watchedFiles.has(filePath)) {
- return;
- }
-
- console.log(`👀 Watching: ${filePath.split('/').pop()}`);
- watchedFiles.add(filePath);
-
- // Set file position to END - only read NEW events from now on
- if (existsSync(filePath)) {
- const content = readFileSync(filePath, 'utf-8');
- filePositions.set(filePath, content.length);
- }
-
- // Watch for changes
- const watcher = watch(filePath, (eventType) => {
- if (eventType === 'change') {
- const newEvents = readNewEvents(filePath);
- storeEvents(newEvents);
- }
- });
-
- watcher.on('error', (error) => {
- console.error(`Error watching ${filePath}:`, error);
- watchedFiles.delete(filePath);
- });
-}
-
-/**
- * Watch the projects directory for new session files
- */
-function watchProjectsDirectory(): void {
- if (!existsSync(PROJECTS_DIR)) {
- console.log('⚠️ Projects directory not found, skipping watch');
- return;
- }
-
- console.log('👀 Watching projects directory for new sessions');
-
- const watcher = watch(PROJECTS_DIR, (eventType, filename) => {
- if (filename && filename.endsWith('.jsonl')) {
- const filePath = join(PROJECTS_DIR, filename);
- if (existsSync(filePath) && !watchedFiles.has(filePath)) {
- // New session file appeared, start watching it
- watchFile(filePath);
- }
- }
- });
-
- watcher.on('error', (error) => {
- console.error('❌ Error watching projects directory:', error);
- });
-}
-
-/**
- * Start watching for events
- * @param callback Optional callback to be notified when new events arrive
- */
-export function startFileIngestion(callback?: (events: HookEvent[]) => void): void {
- console.log('🚀 Starting projects-based event streaming (in-memory only)');
- console.log(`📂 Reading from ${PROJECTS_DIR}/`);
-
- // Set the callback for event notifications
- if (callback) {
- onEventsReceived = callback;
- }
-
- // Load and watch agent sessions for name enrichment
- loadAgentSessions();
- watchAgentSessions();
-
- // Get recent session files and watch them
- const recentFiles = getRecentSessionFiles(20);
- console.log(`📁 Found ${recentFiles.length} recent session files`);
-
- for (const filePath of recentFiles) {
- watchFile(filePath);
- }
-
- // Watch for new session files
- watchProjectsDirectory();
-
- console.log('✅ Projects streaming started');
-}
-
-/**
- * Get all events currently in memory
- */
-export function getRecentEvents(limit: number = 100): HookEvent[] {
- return events.slice(-limit).reverse();
-}
-
-/**
- * Get filter options from in-memory events
- */
-export function getFilterOptions() {
- const sourceApps = new Set();
- const sessionIds = new Set();
- const hookEventTypes = new Set();
-
- for (const event of events) {
- if (event.source_app) sourceApps.add(event.source_app);
- if (event.session_id) sessionIds.add(event.session_id);
- if (event.hook_event_type) hookEventTypes.add(event.hook_event_type);
- }
-
- return {
- source_apps: Array.from(sourceApps).sort(),
- session_ids: Array.from(sessionIds).slice(0, 100),
- hook_event_types: Array.from(hookEventTypes).sort()
- };
-}
-
-// For testing - can be run directly
-if (import.meta.main) {
- startFileIngestion();
-
- console.log('Press Ctrl+C to stop');
-
- process.on('SIGINT', () => {
- console.log('\n👋 Shutting down...');
- process.exit(0);
- });
-}
diff --git a/Packs/pai-observability-server/src/observability/apps/server/src/index.ts b/Packs/pai-observability-server/src/observability/apps/server/src/index.ts
deleted file mode 100755
index b80be344d7..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/server/src/index.ts
+++ /dev/null
@@ -1,509 +0,0 @@
-import type { HookEvent } from './types';
-import {
- createTheme,
- updateThemeById,
- getThemeById,
- searchThemes,
- deleteThemeById,
- exportThemeById,
- importTheme,
- getThemeStats
-} from './theme';
-import { startFileIngestion, getRecentEvents, getFilterOptions } from './file-ingest';
-import { startTaskWatcher, getAllTasks, getTask, getTaskOutput, type BackgroundTask } from './task-watcher';
-
-// Store WebSocket clients
-const wsClients = new Set();
-
-// Start file-based ingestion (reads from ~/.claude/projects/)
-// Pass a callback to broadcast new events to connected WebSocket clients
-startFileIngestion((events) => {
- // Broadcast each event to all connected WebSocket clients
- events.forEach(event => {
- const message = JSON.stringify({ type: 'event', data: event });
- wsClients.forEach(client => {
- try {
- client.send(message);
- } catch (err) {
- // Client disconnected, remove from set
- wsClients.delete(client);
- }
- });
- });
-});
-
-// Start background task watcher
-startTaskWatcher((task: BackgroundTask) => {
- // Broadcast task updates to all connected WebSocket clients
- const message = JSON.stringify({ type: 'task_update', data: task });
- wsClients.forEach(client => {
- try {
- client.send(message);
- } catch (err) {
- wsClients.delete(client);
- }
- });
-});
-
-// Create Bun server with HTTP and WebSocket support
-const server = Bun.serve({
- port: 4000,
-
- async fetch(req: Request) {
- const url = new URL(req.url);
-
- // Handle CORS
- const headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type',
- };
-
- // Handle preflight
- if (req.method === 'OPTIONS') {
- return new Response(null, { headers });
- }
-
- // GET /events/filter-options - Get available filter options
- if (url.pathname === '/events/filter-options' && req.method === 'GET') {
- const options = getFilterOptions();
- return new Response(JSON.stringify(options), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /events/recent - Get recent events
- if (url.pathname === '/events/recent' && req.method === 'GET') {
- const limit = parseInt(url.searchParams.get('limit') || '100');
- const events = getRecentEvents(limit);
- return new Response(JSON.stringify(events), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /events/by-agent/:agentName - Get events for specific agent
- if (url.pathname.startsWith('/events/by-agent/') && req.method === 'GET') {
- const agentName = decodeURIComponent(url.pathname.split('/')[3]);
- const limit = parseInt(url.searchParams.get('limit') || '100');
-
- if (!agentName) {
- return new Response(JSON.stringify({
- error: 'Agent name is required'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const allEvents = getRecentEvents(limit);
- const agentEvents = allEvents.filter(e => e.agent_name === agentName);
-
- return new Response(JSON.stringify(agentEvents), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // Theme API endpoints
-
- // POST /api/themes - Create a new theme
- if (url.pathname === '/api/themes' && req.method === 'POST') {
- try {
- const themeData = await req.json();
- const result = await createTheme(themeData);
-
- const status = result.success ? 201 : 400;
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- } catch (error) {
- console.error('Error creating theme:', error);
- return new Response(JSON.stringify({
- success: false,
- error: 'Invalid request body'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
- }
-
- // GET /api/themes - Search themes
- if (url.pathname === '/api/themes' && req.method === 'GET') {
- const query = {
- query: url.searchParams.get('query') || undefined,
- isPublic: url.searchParams.get('isPublic') ? url.searchParams.get('isPublic') === 'true' : undefined,
- authorId: url.searchParams.get('authorId') || undefined,
- sortBy: url.searchParams.get('sortBy') as any || undefined,
- sortOrder: url.searchParams.get('sortOrder') as any || undefined,
- limit: url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit')!) : undefined,
- offset: url.searchParams.get('offset') ? parseInt(url.searchParams.get('offset')!) : undefined,
- };
-
- const result = await searchThemes(query);
- return new Response(JSON.stringify(result), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /api/themes/:id - Get a specific theme
- if (url.pathname.startsWith('/api/themes/') && req.method === 'GET') {
- const id = url.pathname.split('/')[3];
- if (!id) {
- return new Response(JSON.stringify({
- success: false,
- error: 'Theme ID is required'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const result = await getThemeById(id);
- const status = result.success ? 200 : 404;
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // PUT /api/themes/:id - Update a theme
- if (url.pathname.startsWith('/api/themes/') && req.method === 'PUT') {
- const id = url.pathname.split('/')[3];
- if (!id) {
- return new Response(JSON.stringify({
- success: false,
- error: 'Theme ID is required'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- try {
- const updates = await req.json();
- const result = await updateThemeById(id, updates);
-
- const status = result.success ? 200 : 400;
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- } catch (error) {
- console.error('Error updating theme:', error);
- return new Response(JSON.stringify({
- success: false,
- error: 'Invalid request body'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
- }
-
- // DELETE /api/themes/:id - Delete a theme
- if (url.pathname.startsWith('/api/themes/') && req.method === 'DELETE') {
- const id = url.pathname.split('/')[3];
- if (!id) {
- return new Response(JSON.stringify({
- success: false,
- error: 'Theme ID is required'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const authorId = url.searchParams.get('authorId');
- const result = await deleteThemeById(id, authorId || undefined);
-
- const status = result.success ? 200 : (result.error?.includes('not found') ? 404 : 403);
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /api/themes/:id/export - Export a theme
- if (url.pathname.match(/^\/api\/themes\/[^\/]+\/export$/) && req.method === 'GET') {
- const id = url.pathname.split('/')[3];
-
- const result = await exportThemeById(id);
- if (!result.success) {
- const status = result.error?.includes('not found') ? 404 : 400;
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- return new Response(JSON.stringify(result.data), {
- headers: {
- ...headers,
- 'Content-Type': 'application/json',
- 'Content-Disposition': `attachment; filename="${result.data.theme.name}.json"`
- }
- });
- }
-
- // POST /api/themes/import - Import a theme
- if (url.pathname === '/api/themes/import' && req.method === 'POST') {
- try {
- const importData = await req.json();
- const authorId = url.searchParams.get('authorId');
-
- const result = await importTheme(importData, authorId || undefined);
-
- const status = result.success ? 201 : 400;
- return new Response(JSON.stringify(result), {
- status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- } catch (error) {
- console.error('Error importing theme:', error);
- return new Response(JSON.stringify({
- success: false,
- error: 'Invalid import data'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
- }
-
- // GET /api/themes/stats - Get theme statistics
- if (url.pathname === '/api/themes/stats' && req.method === 'GET') {
- const result = await getThemeStats();
- return new Response(JSON.stringify(result), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /api/activities - Get current activities from Kitty tab titles
- if (url.pathname === '/api/activities' && req.method === 'GET') {
- try {
- // Run kitty @ ls to get tab/window info
- const proc = Bun.spawn(['kitty', '@', 'ls'], {
- stdout: 'pipe',
- stderr: 'pipe'
- });
-
- const stdout = await new Response(proc.stdout).text();
- const exitCode = await proc.exited;
-
- if (exitCode !== 0) {
- return new Response(JSON.stringify([]), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const kittyData = JSON.parse(stdout);
- const activities: { agent: string; activity: string; timestamp: string }[] = [];
-
- // Parse ALL Kitty tabs - just return their titles as-is
- for (const osWindow of kittyData) {
- for (const tab of osWindow.tabs || []) {
- // Strip trailing ellipsis and leading "N: " tab number prefix
- const title = (tab.title || '')
- .replace(/\.{3}$/, '')
- .replace(/^\d+:\s*/, '')
- .trim();
-
- if (!title) continue;
-
- activities.push({
- agent: process.env.DA || 'main',
- activity: title,
- timestamp: new Date().toISOString()
- });
- }
- }
-
- return new Response(JSON.stringify(activities), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- } catch (error) {
- console.error('Error fetching Kitty activities:', error);
- return new Response(JSON.stringify([]), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
- }
-
- // POST /api/haiku/summarize - Proxy for Haiku summarization (reads API key from ${PAI_DIR}/.env)
- if (url.pathname === '/api/haiku/summarize' && req.method === 'POST') {
- try {
- // Load .env from ${PAI_DIR}/.env
- const homeDir = process.env.HOME || '';
- const paiDir = process.env.PAI_DIR || `${homeDir}/.claude`;
- const envPath = `${paiDir}/.env`;
-
- let apiKey = '';
- try {
- const envFile = await Bun.file(envPath).text();
- const match = envFile.match(/ANTHROPIC_API_KEY=(.+)/);
- if (match) {
- apiKey = match[1].trim();
- }
- } catch (err) {
- console.error('Failed to read .env:', err);
- }
-
- if (!apiKey) {
- return new Response(JSON.stringify({
- success: false,
- error: 'ANTHROPIC_API_KEY not configured in ${PAI_DIR}/.env'
- }), {
- status: 500,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const body = await req.json();
- const { prompt } = body;
-
- if (!prompt) {
- return new Response(JSON.stringify({
- success: false,
- error: 'Missing prompt'
- }), {
- status: 400,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // Call Anthropic API
- const response = await fetch('https://api.anthropic.com/v1/messages', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'x-api-key': apiKey,
- 'anthropic-version': '2023-06-01'
- },
- body: JSON.stringify({
- model: 'claude-haiku-4-5',
- max_tokens: 50,
- messages: [{
- role: 'user',
- content: prompt
- }]
- })
- });
-
- if (!response.ok) {
- const error = await response.text();
- return new Response(JSON.stringify({
- success: false,
- error: `Haiku API error: ${response.status}`,
- details: error
- }), {
- status: response.status,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- const data = await response.json();
- return new Response(JSON.stringify({
- success: true,
- text: data.content?.[0]?.text || ''
- }), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- } catch (error) {
- console.error('Error in Haiku proxy:', error);
- return new Response(JSON.stringify({
- success: false,
- error: 'Internal server error'
- }), {
- status: 500,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
- }
-
- // GET /api/tasks - List all background tasks
- if (url.pathname === '/api/tasks' && req.method === 'GET') {
- const tasks = getAllTasks();
- return new Response(JSON.stringify(tasks), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /api/tasks/:taskId - Get a specific task
- if (url.pathname.match(/^\/api\/tasks\/[^\/]+$/) && req.method === 'GET') {
- const taskId = url.pathname.split('/')[3];
- const task = getTask(taskId);
-
- if (!task) {
- return new Response(JSON.stringify({ error: 'Task not found' }), {
- status: 404,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- return new Response(JSON.stringify(task), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // GET /api/tasks/:taskId/output - Get full task output
- if (url.pathname.match(/^\/api\/tasks\/[^\/]+\/output$/) && req.method === 'GET') {
- const taskId = url.pathname.split('/')[3];
- const output = getTaskOutput(taskId);
-
- if (!output) {
- return new Response(JSON.stringify({ error: 'Task output not found' }), {
- status: 404,
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- return new Response(JSON.stringify({ output }), {
- headers: { ...headers, 'Content-Type': 'application/json' }
- });
- }
-
- // WebSocket upgrade
- if (url.pathname === '/stream') {
- const success = server.upgrade(req);
- if (success) {
- return undefined;
- }
- }
-
- // Default response
- return new Response('Multi-Agent Observability Server', {
- headers: { ...headers, 'Content-Type': 'text/plain' }
- });
- },
-
- websocket: {
- open(ws) {
- console.log('WebSocket client connected');
- wsClients.add(ws);
-
- // Send recent events on connection
- const events = getRecentEvents(50);
- ws.send(JSON.stringify({ type: 'initial', data: events }));
- },
-
- message(ws, message) {
- // Handle any client messages if needed
- console.log('Received message:', message);
- },
-
- close(ws) {
- console.log('WebSocket client disconnected');
- wsClients.delete(ws);
- },
-
- error(ws, error) {
- console.error('WebSocket error:', error);
- wsClients.delete(ws);
- }
- }
-});
-
-console.log(`🚀 Server running on http://localhost:${server.port}`);
-console.log(`📊 WebSocket endpoint: ws://localhost:${server.port}/stream`);
-console.log(`📮 POST events to: http://localhost:${server.port}/events`);
\ No newline at end of file
diff --git a/Packs/pai-observability-server/src/observability/apps/server/src/types.ts b/Packs/pai-observability-server/src/observability/apps/server/src/types.ts
deleted file mode 100755
index 585f61f9b5..0000000000
--- a/Packs/pai-observability-server/src/observability/apps/server/src/types.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-// Todo item interface
-export interface TodoItem {
- content: string;
- status: 'pending' | 'in_progress' | 'completed';
- activeForm: string;
-}
-
-// New interface for human-in-the-loop requests
-export interface HumanInTheLoop {
- question: string;
- responseWebSocketUrl: string;
- type: 'question' | 'permission' | 'choice';
- choices?: string[]; // For multiple choice questions
- timeout?: number; // Optional timeout in seconds
- requiresResponse?: boolean; // Whether response is required or optional
-}
-
-// Response interface
-export interface HumanInTheLoopResponse {
- response?: string;
- permission?: boolean;
- choice?: string; // Selected choice from options
- hookEvent: HookEvent;
- respondedAt: number;
- respondedBy?: string; // Optional user identifier
-}
-
-// Status tracking interface
-export interface HumanInTheLoopStatus {
- status: 'pending' | 'responded' | 'timeout' | 'error';
- respondedAt?: number;
- response?: HumanInTheLoopResponse;
-}
-
-export interface HookEvent {
- id?: number;
- source_app: string;
- session_id: string;
- agent_name?: string; // Agent name enriched from MEMORY/STATE/agent-sessions.json
- hook_event_type: string;
- payload: Record;
- chat?: any[];
- summary?: string;
- timestamp?: number;
- model_name?: string;
-
- // NEW: Optional HITL data
- humanInTheLoop?: HumanInTheLoop;
- humanInTheLoopStatus?: HumanInTheLoopStatus;
-
- // NEW: Optional Todo data
- todos?: TodoItem[];
- completedTodos?: TodoItem[]; // Todos that were completed in this event
-}
-
-export interface FilterOptions {
- source_apps: string[];
- session_ids: string[];
- hook_event_types: string[];
-}
-
-// Theme-related interfaces for server-side storage and API
-export interface ThemeColors {
- primary: string;
- primaryHover: string;
- primaryLight: string;
- primaryDark: string;
- bgPrimary: string;
- bgSecondary: string;
- bgTertiary: string;
- bgQuaternary: string;
- textPrimary: string;
- textSecondary: string;
- textTertiary: string;
- textQuaternary: string;
- borderPrimary: string;
- borderSecondary: string;
- borderTertiary: string;
- accentSuccess: string;
- accentWarning: string;
- accentError: string;
- accentInfo: string;
- shadow: string;
- shadowLg: string;
- hoverBg: string;
- activeBg: string;
- focusRing: string;
-}
-
-export interface Theme {
- id: string;
- name: string;
- displayName: string;
- description?: string;
- colors: ThemeColors;
- isPublic: boolean;
- authorId?: string;
- authorName?: string;
- createdAt: number;
- updatedAt: number;
- tags: string[];
- downloadCount?: number;
- rating?: number;
- ratingCount?: number;
-}
-
-export interface ThemeSearchQuery {
- query?: string;
- tags?: string[];
- authorId?: string;
- isPublic?: boolean;
- sortBy?: 'name' | 'created' | 'updated' | 'downloads' | 'rating';
- sortOrder?: 'asc' | 'desc';
- limit?: number;
- offset?: number;
-}
-
-export interface ThemeShare {
- id: string;
- themeId: string;
- shareToken: string;
- expiresAt?: number;
- isPublic: boolean;
- allowedUsers: string[];
- createdAt: number;
- accessCount: number;
-}
-
-export interface ThemeRating {
- id: string;
- themeId: string;
- userId: string;
- rating: number; // 1-5
- comment?: string;
- createdAt: number;
-}
-
-export interface ThemeValidationError {
- field: string;
- message: string;
- code: string;
-}
-
-export interface ApiResponse {
- success: boolean;
- data?: T;
- error?: string;
- message?: string;
- validationErrors?: ThemeValidationError[];
-}
\ No newline at end of file