panopticon-cli 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panopticon-cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -39,7 +39,7 @@
39
39
  "dist",
40
40
  "templates",
41
41
  "skills",
42
- "scripts/git-hooks",
42
+ "scripts",
43
43
  "README.md",
44
44
  "LICENSE"
45
45
  ],
@@ -0,0 +1,149 @@
1
+ #!/bin/bash
2
+ # ~/.panopticon/bin/heartbeat-hook
3
+ # Called by Claude Code after every tool use with JSON on stdin
4
+ #
5
+ # This hook receives PostToolUse event data from Claude Code and writes
6
+ # rich heartbeat information to enable real-time agent monitoring.
7
+
8
+ # Don't use set -e - we want the hook to be resilient to failures
9
+ # and never break Claude Code execution
10
+
11
+ # Parse tool info from stdin
12
+ TOOL_INFO=$(cat 2>/dev/null || echo '{}')
13
+
14
+ # Check if jq is available
15
+ if ! command -v jq &> /dev/null; then
16
+ echo "Warning: jq not found. Heartbeat hook requires jq to parse tool data." >&2
17
+ exit 0 # Silent failure - don't break Claude Code execution
18
+ fi
19
+
20
+ # Extract tool name and input (truncate input to 100 chars to avoid huge files)
21
+ TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo "unknown")
22
+ TOOL_INPUT=$(echo "$TOOL_INFO" | jq -r '.tool_input | tostring | .[0:100] // ""' 2>/dev/null || echo "")
23
+
24
+ # Get agent ID from env (set by pan work issue) or tmux session name
25
+ # Only use tmux session name if we're actually INSIDE a tmux session ($TMUX is set)
26
+ if [ -n "$PANOPTICON_AGENT_ID" ]; then
27
+ AGENT_ID="$PANOPTICON_AGENT_ID"
28
+ elif [ -n "$TMUX" ]; then
29
+ AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
30
+ else
31
+ AGENT_ID="main-cli"
32
+ fi
33
+ AGENT_ID="${AGENT_ID:-unknown}"
34
+
35
+ # Get current beads task from cache (if exists)
36
+ TASK_CACHE="$HOME/.panopticon/agents/$AGENT_ID/current-task.json"
37
+ CURRENT_TASK=""
38
+ if [ -f "$TASK_CACHE" ]; then
39
+ CURRENT_TASK=$(jq -r '.title // ""' "$TASK_CACHE" 2>/dev/null || echo "")
40
+ fi
41
+
42
+ # Get git branch (fast, single command) - suppress all errors
43
+ GIT_BRANCH=$(git branch --show-current 2>/dev/null || true)
44
+ GIT_BRANCH="${GIT_BRANCH:-unknown}"
45
+
46
+ # Get workspace from pwd
47
+ WORKSPACE=$(pwd)
48
+
49
+ # Ensure heartbeat directory exists
50
+ HEARTBEAT_DIR="$HOME/.panopticon/heartbeats"
51
+ mkdir -p "$HEARTBEAT_DIR"
52
+
53
+ # Ensure agent state directory exists for activity log
54
+ AGENT_DIR="$HOME/.panopticon/agents/$AGENT_ID"
55
+ mkdir -p "$AGENT_DIR"
56
+
57
+ # Write heartbeat atomically (write to temp file, then move)
58
+ # Use jq to properly escape all strings in the JSON output
59
+ TEMP_FILE="$HEARTBEAT_DIR/$AGENT_ID.json.tmp"
60
+ jq -n \
61
+ --arg timestamp "$(date -Iseconds)" \
62
+ --arg agent_id "$AGENT_ID" \
63
+ --arg tool_name "$TOOL_NAME" \
64
+ --arg last_action "$TOOL_INPUT" \
65
+ --arg current_task "$CURRENT_TASK" \
66
+ --arg git_branch "$GIT_BRANCH" \
67
+ --arg workspace "$WORKSPACE" \
68
+ --argjson pid $$ \
69
+ '{
70
+ timestamp: $timestamp,
71
+ agent_id: $agent_id,
72
+ tool_name: $tool_name,
73
+ last_action: $last_action,
74
+ current_task: $current_task,
75
+ git_branch: $git_branch,
76
+ workspace: $workspace,
77
+ pid: $pid
78
+ }' > "$TEMP_FILE" 2>/dev/null || true
79
+
80
+ # Atomic move to avoid race conditions
81
+ mv "$TEMP_FILE" "$HEARTBEAT_DIR/$AGENT_ID.json" 2>/dev/null || true
82
+
83
+ # Append to activity log (JSONL format)
84
+ ACTIVITY_FILE="$AGENT_DIR/activity.jsonl"
85
+
86
+ # Create activity entry
87
+ ACTIVITY_ENTRY=$(jq -n \
88
+ --arg ts "$(date -Iseconds)" \
89
+ --arg tool "$TOOL_NAME" \
90
+ --arg action "$TOOL_INPUT" \
91
+ '{
92
+ ts: $ts,
93
+ tool: $tool,
94
+ action: $action
95
+ }' 2>/dev/null || echo "")
96
+
97
+ # Append to activity log if we successfully created the entry
98
+ if [ -n "$ACTIVITY_ENTRY" ]; then
99
+ echo "$ACTIVITY_ENTRY" >> "$ACTIVITY_FILE" 2>/dev/null || true
100
+
101
+ # Prune to last 100 entries (use tail for atomic operation)
102
+ if [ -f "$ACTIVITY_FILE" ]; then
103
+ TEMP_ACTIVITY="$ACTIVITY_FILE.tmp"
104
+ tail -n 100 "$ACTIVITY_FILE" > "$TEMP_ACTIVITY" 2>/dev/null && mv "$TEMP_ACTIVITY" "$ACTIVITY_FILE" 2>/dev/null || true
105
+ fi
106
+ fi
107
+
108
+ # Update agent runtime state with latest activity timestamp (PAN-119)
109
+ STATE_FILE="$AGENT_DIR/state.json"
110
+ TIMESTAMP="$(date -Iseconds)"
111
+
112
+ if [ -f "$STATE_FILE" ]; then
113
+ # Update existing state file's lastActivity field
114
+ TEMP_STATE="$STATE_FILE.tmp"
115
+ jq --arg ts "$TIMESTAMP" --arg tool "$TOOL_NAME" \
116
+ '.lastActivity = $ts | .currentTool = $tool | .state = "active"' \
117
+ "$STATE_FILE" > "$TEMP_STATE" 2>/dev/null && mv "$TEMP_STATE" "$STATE_FILE" 2>/dev/null || true
118
+ else
119
+ # Create initial state file
120
+ jq -n \
121
+ --arg ts "$TIMESTAMP" \
122
+ --arg tool "$TOOL_NAME" \
123
+ '{
124
+ state: "active",
125
+ lastActivity: $ts,
126
+ currentTool: $tool
127
+ }' > "$STATE_FILE" 2>/dev/null || true
128
+ fi
129
+
130
+ # Record cost event from tool usage (PAN-81)
131
+ # Find the record-cost-event.js script - check multiple locations
132
+ COST_SCRIPT=""
133
+ if [ -f "$HOME/.panopticon/bin/record-cost-event.js" ]; then
134
+ COST_SCRIPT="$HOME/.panopticon/bin/record-cost-event.js"
135
+ elif [ -f "$(dirname "$0")/record-cost-event.js" ]; then
136
+ COST_SCRIPT="$(dirname "$0")/record-cost-event.js"
137
+ fi
138
+
139
+ if [ -n "$COST_SCRIPT" ] && [ -f "$COST_SCRIPT" ]; then
140
+ # Export issue context for the cost recording script
141
+ export PANOPTICON_ISSUE_ID="${PANOPTICON_ISSUE_ID:-UNKNOWN}"
142
+ export PANOPTICON_SESSION_TYPE="${PANOPTICON_SESSION_TYPE:-implementation}"
143
+
144
+ # Call cost recording script with tool info on stdin
145
+ echo "$TOOL_INFO" | node "$COST_SCRIPT" 2>/dev/null || true
146
+ fi
147
+
148
+ # Always exit successfully - never break Claude Code execution
149
+ exit 0
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ #
3
+ # Install Panopticon git hooks in a project's repos
4
+ # Usage: ./install-git-hooks.sh /path/to/project
5
+ #
6
+ # For poly-repos, this will find all .git directories and install hooks in each.
7
+
8
+ set -e
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ HOOKS_DIR="$SCRIPT_DIR/git-hooks"
12
+ TARGET_DIR="${1:-.}"
13
+
14
+ if [ ! -d "$TARGET_DIR" ]; then
15
+ echo "Error: Directory does not exist: $TARGET_DIR"
16
+ exit 1
17
+ fi
18
+
19
+ echo "Installing Panopticon git hooks in: $TARGET_DIR"
20
+ echo ""
21
+
22
+ # Find all .git directories (not files - those are worktrees)
23
+ # Also exclude node_modules, target, etc.
24
+ find "$TARGET_DIR" -maxdepth 4 -type d -name ".git" \
25
+ -not -path "*/node_modules/*" \
26
+ -not -path "*/target/*" \
27
+ -not -path "*/.git/*" \
28
+ -not -path "*/workspaces/*" \
29
+ 2>/dev/null | while read git_dir; do
30
+
31
+ hooks_target="$git_dir/hooks"
32
+ repo_dir="$(dirname "$git_dir")"
33
+
34
+ echo "Installing hooks in: $repo_dir"
35
+
36
+ # Create hooks directory if it doesn't exist
37
+ mkdir -p "$hooks_target"
38
+
39
+ # Install each hook
40
+ for hook in "$HOOKS_DIR"/*; do
41
+ if [ -f "$hook" ]; then
42
+ hook_name=$(basename "$hook")
43
+ target_hook="$hooks_target/$hook_name"
44
+
45
+ # Check if hook already exists
46
+ if [ -f "$target_hook" ] && [ ! -L "$target_hook" ]; then
47
+ echo " ⚠️ $hook_name: existing hook found, creating backup"
48
+ mv "$target_hook" "$target_hook.backup"
49
+ fi
50
+
51
+ # Create symlink to our hook
52
+ ln -sf "$hook" "$target_hook"
53
+ echo " ✓ $hook_name installed"
54
+ fi
55
+ done
56
+
57
+ echo ""
58
+ done
59
+
60
+ echo "Done! Git hooks installed."
61
+ echo ""
62
+ echo "The post-checkout hook will warn if the main project directory"
63
+ echo "is checked out to a branch other than 'main'."
64
+ echo ""
65
+ echo "To enable auto-revert (automatically switch back to main):"
66
+ echo " export PANOPTICON_AUTO_REVERT_CHECKOUT=1"
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # notify-complete: Send desktop notification when agent completes work
3
+ # Part of Panopticon - works on WSL2/Windows, Linux, and macOS
4
+ #
5
+ # Usage: notify-complete <issue-id> <title> [mr-url]
6
+ #
7
+ # Examples:
8
+ # notify-complete MIN-665 "Fixed login button"
9
+ # notify-complete PAN-96 "Implemented templates" "https://gitlab.com/mr/123"
10
+
11
+ ISSUE_ID="${1:-UNKNOWN}"
12
+ TITLE="${2:-Agent completed work}"
13
+ MR_URL="${3:-}"
14
+
15
+ # Log to completion file
16
+ COMPLETION_LOG="$HOME/.panopticon/agent-completed.log"
17
+ mkdir -p "$(dirname "$COMPLETION_LOG")"
18
+ echo "$(date '+%Y-%m-%d %H:%M:%S') | ${ISSUE_ID} | ${TITLE} | ${MR_URL}" >> "$COMPLETION_LOG"
19
+
20
+ # Detect platform and send notification
21
+ send_notification() {
22
+ local title="$1"
23
+ local message="$2"
24
+
25
+ # WSL2/Windows - use PowerShell toast notifications
26
+ if command -v powershell.exe &>/dev/null; then
27
+ powershell.exe -Command "
28
+ \$xml = @\"
29
+ <toast>
30
+ <visual>
31
+ <binding template=\"ToastText02\">
32
+ <text id=\"1\">${title}</text>
33
+ <text id=\"2\">${message}</text>
34
+ </binding>
35
+ </visual>
36
+ <audio src=\"ms-winsoundevent:Notification.Default\"/>
37
+ </toast>
38
+ \"@
39
+
40
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
41
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
42
+
43
+ \$template = New-Object Windows.Data.Xml.Dom.XmlDocument
44
+ \$template.LoadXml(\$xml)
45
+ \$toast = New-Object Windows.UI.Notifications.ToastNotification \$template
46
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Panopticon').Show(\$toast)
47
+ " 2>/dev/null && return 0
48
+
49
+ # Fallback: MessageBox
50
+ powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}', '${title}', 'OK', 'Information')" 2>/dev/null &
51
+ return 0
52
+ fi
53
+
54
+ # macOS - use osascript
55
+ if command -v osascript &>/dev/null; then
56
+ osascript -e "display notification \"${message}\" with title \"${title}\"" 2>/dev/null
57
+ return 0
58
+ fi
59
+
60
+ # Linux - use notify-send
61
+ if command -v notify-send &>/dev/null; then
62
+ notify-send "${title}" "${message}" 2>/dev/null
63
+ return 0
64
+ fi
65
+
66
+ # Fallback - just print to console
67
+ echo "[NOTIFICATION] ${title}: ${message}"
68
+ }
69
+
70
+ # Build notification message
71
+ if [ -n "$MR_URL" ]; then
72
+ MESSAGE="${TITLE} - Ready for review"
73
+ else
74
+ MESSAGE="${TITLE} - Work complete"
75
+ fi
76
+
77
+ send_notification "Panopticon: ${ISSUE_ID}" "$MESSAGE"
78
+
79
+ echo "Notification sent for ${ISSUE_ID}"
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall script for Panopticon
4
+ *
5
+ * Automatically syncs hooks after npm install/upgrade if Panopticon
6
+ * has been initialized (bin dir exists).
7
+ */
8
+
9
+ import { existsSync, readdirSync, copyFileSync, chmodSync, mkdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { homedir } from 'os';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const BIN_DIR = join(homedir(), '.panopticon', 'bin');
16
+ const SCRIPTS_DIR = __dirname;
17
+
18
+ // Only run if Panopticon has been initialized
19
+ if (!existsSync(join(homedir(), '.panopticon'))) {
20
+ console.log('Panopticon not initialized yet. Run `pan init` to set up.');
21
+ process.exit(0);
22
+ }
23
+
24
+ // Ensure bin directory exists
25
+ mkdirSync(BIN_DIR, { recursive: true });
26
+
27
+ // Copy all scripts from scripts/ to ~/.panopticon/bin/
28
+ const scripts = readdirSync(SCRIPTS_DIR)
29
+ .filter(f => !f.startsWith('.') && !f.endsWith('.mjs') && !f.endsWith('.js'));
30
+
31
+ let synced = 0;
32
+ for (const script of scripts) {
33
+ try {
34
+ const source = join(SCRIPTS_DIR, script);
35
+ const target = join(BIN_DIR, script);
36
+ copyFileSync(source, target);
37
+ chmodSync(target, 0o755);
38
+ synced++;
39
+ } catch (e) {
40
+ // Ignore errors, hooks are non-critical
41
+ }
42
+ }
43
+
44
+ if (synced > 0) {
45
+ console.log(`✓ Synced ${synced} hooks to ~/.panopticon/bin/`);
46
+ }
47
+
48
+ // Suggest running full sync
49
+ console.log('Run `pan sync` to sync skills and commands to AI tools.');
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # ~/.panopticon/bin/pre-tool-hook
3
+ # Called by Claude Code before every tool use
4
+ #
5
+ # This hook sets agent state to "active" and optionally sends
6
+ # heartbeat to API server for real-time dashboard updates
7
+
8
+ # Don't use set -e - we want the hook to be resilient to failures
9
+ # and never break Claude Code execution
10
+
11
+ # Get agent ID from env (set by pan work issue) or tmux session name
12
+ if [ -n "$PANOPTICON_AGENT_ID" ]; then
13
+ AGENT_ID="$PANOPTICON_AGENT_ID"
14
+ elif [ -n "$TMUX" ]; then
15
+ AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
16
+ else
17
+ AGENT_ID="main-cli"
18
+ fi
19
+ AGENT_ID="${AGENT_ID:-unknown}"
20
+
21
+ # Parse tool info from stdin
22
+ TOOL_INFO=$(cat 2>/dev/null || echo '{}')
23
+
24
+ # Check if jq is available
25
+ if ! command -v jq &> /dev/null; then
26
+ exit 0 # Silent failure - don't break Claude Code execution
27
+ fi
28
+
29
+ # Extract tool name
30
+ TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo "unknown")
31
+
32
+ # Ensure state directory exists
33
+ STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
34
+ mkdir -p "$STATE_DIR"
35
+
36
+ # Write state to file (atomic write via temp file)
37
+ TEMP_FILE="$STATE_DIR/state.json.tmp"
38
+ jq -n \
39
+ --arg timestamp "$(date -Iseconds)" \
40
+ --arg state "active" \
41
+ --arg tool "$TOOL_NAME" \
42
+ '{
43
+ state: $state,
44
+ lastActivity: $timestamp,
45
+ currentTool: $tool
46
+ }' > "$TEMP_FILE" 2>/dev/null || true
47
+
48
+ mv "$TEMP_FILE" "$STATE_DIR/state.json" 2>/dev/null || true
49
+
50
+ # Optionally send heartbeat to API server (non-blocking)
51
+ # Only if dashboard is running
52
+ if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
53
+ curl -s -X POST "http://localhost:3011/api/agents/$AGENT_ID/heartbeat" \
54
+ -H "Content-Type: application/json" \
55
+ -d "{\"state\":\"active\",\"tool\":\"$TOOL_NAME\",\"timestamp\":\"$(date -Iseconds)\"}" \
56
+ > /dev/null 2>&1 &
57
+ fi
58
+
59
+ # Always exit successfully
60
+ exit 0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Record a cost event from Claude Code tool usage
4
+ * Called by heartbeat-hook with JSON input on stdin
5
+ */
6
+
7
+ import { readFileSync } from 'fs';
8
+ import { calculateCost, getPricing } from '../src/lib/cost.js';
9
+ import { appendCostEvent } from '../src/lib/costs/events.js';
10
+
11
+ // Read tool info from stdin
12
+ let toolInfo;
13
+ try {
14
+ const input = readFileSync(0, 'utf-8');
15
+ toolInfo = JSON.parse(input);
16
+ } catch (err) {
17
+ // Silent failure - don't break Claude Code execution
18
+ process.exit(0);
19
+ }
20
+
21
+ // Extract usage data from tool info
22
+ const usage = toolInfo?.usage || toolInfo?.message?.usage;
23
+ if (!usage) {
24
+ // No usage data - not a Claude API call
25
+ process.exit(0);
26
+ }
27
+
28
+ // Extract token counts
29
+ const inputTokens = usage.input_tokens || 0;
30
+ const outputTokens = usage.output_tokens || 0;
31
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
32
+ const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
33
+
34
+ // Must have at least some tokens to record
35
+ if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
36
+ process.exit(0);
37
+ }
38
+
39
+ // Extract model name
40
+ const model = toolInfo?.model || toolInfo?.message?.model || 'claude-sonnet-4';
41
+
42
+ // Determine provider from model name
43
+ let provider = 'anthropic';
44
+ if (model.includes('gpt')) {
45
+ provider = 'openai';
46
+ } else if (model.includes('gemini')) {
47
+ provider = 'google';
48
+ }
49
+
50
+ // Get pricing and calculate cost
51
+ const pricing = getPricing(provider, model);
52
+ if (!pricing) {
53
+ console.warn(`No pricing found for ${provider}/${model}`);
54
+ process.exit(0);
55
+ }
56
+
57
+ const cost = calculateCost({
58
+ inputTokens,
59
+ outputTokens,
60
+ cacheReadTokens,
61
+ cacheWriteTokens,
62
+ cacheTTL: '5m',
63
+ }, pricing);
64
+
65
+ // Get agent and issue context from environment
66
+ // PANOPTICON_AGENT_ID should always be set by pan work or heartbeat-hook
67
+ // If not set, use a fallback that makes it clear costs are unattributed
68
+ const agentId = process.env.PANOPTICON_AGENT_ID || 'unattributed';
69
+
70
+ const issueId = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
71
+ const sessionType = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
72
+
73
+ // Record cost event
74
+ try {
75
+ appendCostEvent({
76
+ ts: new Date().toISOString(),
77
+ type: 'cost',
78
+ agentId,
79
+ issueId,
80
+ sessionType,
81
+ provider,
82
+ model,
83
+ input: inputTokens,
84
+ output: outputTokens,
85
+ cacheRead: cacheReadTokens,
86
+ cacheWrite: cacheWriteTokens,
87
+ cost,
88
+ });
89
+ } catch (err) {
90
+ // Silent failure - don't break Claude Code execution
91
+ console.error('Failed to record cost event:', err);
92
+ }
93
+
94
+ process.exit(0);
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Record a cost event from Claude Code tool usage
4
+ * Called by heartbeat-hook with JSON input on stdin
5
+ */
6
+
7
+ import { readFileSync } from 'fs';
8
+ import { calculateCost, getPricing, AIProvider } from '../src/lib/cost.js';
9
+ import { appendCostEvent } from '../src/lib/costs/events.js';
10
+
11
+ // ============== Types ==============
12
+
13
+ interface UsageData {
14
+ input_tokens?: number;
15
+ output_tokens?: number;
16
+ cache_read_input_tokens?: number;
17
+ cache_creation_input_tokens?: number;
18
+ }
19
+
20
+ interface ToolInfo {
21
+ model?: string;
22
+ usage?: UsageData;
23
+ message?: {
24
+ model?: string;
25
+ usage?: UsageData;
26
+ };
27
+ }
28
+
29
+ // ============== Main ==============
30
+
31
+ // Read tool info from stdin
32
+ let toolInfo: ToolInfo;
33
+ try {
34
+ const input = readFileSync(0, 'utf-8');
35
+ toolInfo = JSON.parse(input) as ToolInfo;
36
+ } catch (err) {
37
+ // Silent failure - don't break Claude Code execution
38
+ process.exit(0);
39
+ }
40
+
41
+ // Extract usage data from tool info
42
+ const usage: UsageData | undefined = toolInfo?.usage || toolInfo?.message?.usage;
43
+ if (!usage) {
44
+ // No usage data - not a Claude API call
45
+ process.exit(0);
46
+ }
47
+
48
+ // Extract token counts
49
+ const inputTokens = usage.input_tokens || 0;
50
+ const outputTokens = usage.output_tokens || 0;
51
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
52
+ const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
53
+
54
+ // Must have at least some tokens to record
55
+ if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
56
+ process.exit(0);
57
+ }
58
+
59
+ // Extract model name
60
+ const model: string = toolInfo?.model || toolInfo?.message?.model || 'claude-sonnet-4';
61
+
62
+ // Determine provider from model name
63
+ let provider: AIProvider = 'anthropic';
64
+ if (model.includes('gpt')) {
65
+ provider = 'openai';
66
+ } else if (model.includes('gemini')) {
67
+ provider = 'google';
68
+ }
69
+
70
+ // Get pricing and calculate cost
71
+ const pricing = getPricing(provider, model);
72
+ if (!pricing) {
73
+ console.warn(`No pricing found for ${provider}/${model}`);
74
+ process.exit(0);
75
+ }
76
+
77
+ const cost = calculateCost({
78
+ inputTokens,
79
+ outputTokens,
80
+ cacheReadTokens,
81
+ cacheWriteTokens,
82
+ cacheTTL: '5m',
83
+ }, pricing);
84
+
85
+ // Get agent and issue context from environment
86
+ // PANOPTICON_AGENT_ID should always be set by pan work or heartbeat-hook
87
+ // If not set, use a fallback that makes it clear costs are unattributed
88
+ const agentId: string = process.env.PANOPTICON_AGENT_ID || 'unattributed';
89
+ const issueId: string = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
90
+ const sessionType: string = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
91
+
92
+ // Record cost event
93
+ try {
94
+ appendCostEvent({
95
+ ts: new Date().toISOString(),
96
+ type: 'cost',
97
+ agentId,
98
+ issueId,
99
+ sessionType,
100
+ provider,
101
+ model,
102
+ input: inputTokens,
103
+ output: outputTokens,
104
+ cacheRead: cacheReadTokens,
105
+ cacheWrite: cacheWriteTokens,
106
+ cost,
107
+ });
108
+ } catch (err) {
109
+ // Silent failure - don't break Claude Code execution
110
+ console.error('Failed to record cost event:', err);
111
+ }
112
+
113
+ process.exit(0);
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # Restart Panopticon dashboard cleanly
3
+ # Usage: ./restart-dashboard.sh
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ DASHBOARD_DIR="$SCRIPT_DIR/../src/dashboard"
9
+ LOG_FILE="/tmp/panopticon-dashboard.log"
10
+
11
+ echo "Stopping dashboard..."
12
+
13
+ # Most reliable: kill by port using fuser -k
14
+ for port in 3010 3011 3012; do
15
+ fuser -k ${port}/tcp 2>/dev/null || true
16
+ done
17
+
18
+ # Also kill any npm/node processes that might be orphaned
19
+ pkill -9 -f "npm.*dashboard" 2>/dev/null || true
20
+ pkill -9 -f "node.*panopticon.*dashboard" 2>/dev/null || true
21
+ pkill -9 -f "vite.*301" 2>/dev/null || true
22
+ pkill -9 -f "concurrently.*dev:server" 2>/dev/null || true
23
+
24
+ sleep 2
25
+
26
+ # Verify ports are clear
27
+ if lsof -i :3010,:3011,:3012 >/dev/null 2>&1; then
28
+ echo "Warning: Ports still in use, force killing..."
29
+ lsof -ti :3010,:3011,:3012 | xargs -r kill -9 2>/dev/null || true
30
+ sleep 1
31
+ fi
32
+
33
+ echo "Starting dashboard..."
34
+
35
+ cd "$DASHBOARD_DIR"
36
+ rm -f "$LOG_FILE"
37
+
38
+ # Use setsid to fully detach from terminal
39
+ setsid npm run dev > "$LOG_FILE" 2>&1 &
40
+
41
+ # Wait for API to be ready
42
+ echo -n "Waiting for API"
43
+ for i in {1..30}; do
44
+ if curl -s --max-time 2 http://localhost:3011/api/health > /dev/null 2>&1; then
45
+ echo ""
46
+ echo "Dashboard ready!"
47
+ echo " Frontend: http://localhost:3010"
48
+ echo " API: http://localhost:3011"
49
+ echo " Logs: $LOG_FILE"
50
+ exit 0
51
+ fi
52
+ echo -n "."
53
+ sleep 1
54
+ done
55
+
56
+ echo ""
57
+ echo "ERROR: API not responding after 30s"
58
+ echo "Check logs: tail -50 $LOG_FILE"
59
+ exit 1
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # Setup local HTTPS certificates for Panopticon using mkcert
3
+ # This script generates trusted certificates for *.pan.localhost
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ CERTS_DIR="${SCRIPT_DIR}/../templates/traefik/certs"
9
+
10
+ # Colors for output
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[1;33m'
14
+ NC='\033[0m' # No Color
15
+
16
+ echo "Setting up local HTTPS certificates for Panopticon..."
17
+
18
+ # Check if mkcert is installed
19
+ if ! command -v mkcert &> /dev/null; then
20
+ echo -e "${RED}Error: mkcert is not installed.${NC}"
21
+ echo ""
22
+ echo "Install mkcert first:"
23
+ echo " macOS: brew install mkcert"
24
+ echo " Linux: apt install mkcert OR brew install mkcert"
25
+ echo " Windows: choco install mkcert"
26
+ echo ""
27
+ echo "More info: https://github.com/FiloSottile/mkcert"
28
+ exit 1
29
+ fi
30
+
31
+ echo -e "${GREEN}mkcert found at $(which mkcert)${NC}"
32
+
33
+ # Check if CA is installed
34
+ CAROOT=$(mkcert -CAROOT 2>/dev/null)
35
+ if [ -f "$CAROOT/rootCA.pem" ]; then
36
+ echo -e "${GREEN}Local CA already exists at $CAROOT${NC}"
37
+ else
38
+ echo -e "${YELLOW}Installing local CA...${NC}"
39
+ echo "This may require sudo password to add CA to system trust store."
40
+ mkcert -install
41
+ echo -e "${GREEN}CA installed successfully${NC}"
42
+ fi
43
+
44
+ # Create certs directory if it doesn't exist
45
+ mkdir -p "$CERTS_DIR"
46
+
47
+ # Generate certificates for pan.localhost
48
+ echo ""
49
+ echo "Generating certificates for *.pan.localhost..."
50
+ cd "$CERTS_DIR"
51
+
52
+ # Generate wildcard cert for pan.localhost
53
+ mkcert -cert-file "_wildcard.pan.localhost.pem" \
54
+ -key-file "_wildcard.pan.localhost-key.pem" \
55
+ "pan.localhost" "*.pan.localhost"
56
+
57
+ echo ""
58
+ echo -e "${GREEN}Certificates generated successfully!${NC}"
59
+ echo ""
60
+ echo "Certificate files:"
61
+ ls -la "$CERTS_DIR"/*.pem
62
+ echo ""
63
+ echo "You can now start Traefik:"
64
+ echo " cd templates/traefik && docker compose up -d"
65
+ echo ""
66
+ echo "Access the dashboard at: https://pan.localhost"
@@ -0,0 +1,106 @@
1
+ #!/bin/bash
2
+ # ~/.panopticon/bin/specialist-stop-hook
3
+ # Called when a specialist agent goes idle - detects completion patterns
4
+ #
5
+ # This hook is chained from stop-hook and only runs for specialist agents.
6
+ # It captures the last 50 lines of terminal output and looks for completion
7
+ # patterns to auto-trigger status updates.
8
+
9
+ # Don't use set -e - we want the hook to be resilient to failures
10
+ # and never break Claude Code execution
11
+
12
+ # Get agent ID from env or tmux session name
13
+ if [ -n "$PANOPTICON_AGENT_ID" ]; then
14
+ AGENT_ID="$PANOPTICON_AGENT_ID"
15
+ elif [ -n "$TMUX" ]; then
16
+ AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
17
+ else
18
+ exit 0 # Not in a tracked session
19
+ fi
20
+
21
+ # Check if this is a specialist agent
22
+ case "$AGENT_ID" in
23
+ specialist-review-agent|specialist-test-agent|specialist-merge-agent)
24
+ ;;
25
+ *)
26
+ exit 0 # Not a specialist, skip
27
+ ;;
28
+ esac
29
+
30
+ # Get the specialist type from agent ID
31
+ SPECIALIST_TYPE="${AGENT_ID#specialist-}"
32
+
33
+ # Capture the last 50 lines of terminal output
34
+ OUTPUT=$(tmux capture-pane -t "$AGENT_ID" -p -S -50 2>/dev/null || echo "")
35
+
36
+ if [ -z "$OUTPUT" ]; then
37
+ exit 0 # No output to analyze
38
+ fi
39
+
40
+ # Define completion patterns for each specialist (case-insensitive grep -E patterns)
41
+ case "$SPECIALIST_TYPE" in
42
+ review-agent)
43
+ # Patterns that indicate review passed
44
+ PASS_PATTERN="[Rr]eview (passed|complete|PASSED|COMPLETE)|LGTM|[Nn]o issues found|[Cc]ode review complete|✓.*[Pp]assed"
45
+ # Patterns that indicate review failed/blocked
46
+ FAIL_PATTERN="[Rr]eview (failed|blocked|FAILED|BLOCKED)|[Ii]ssues found|[Nn]eeds changes|[Bb]locked"
47
+ STATUS_FIELD="reviewStatus"
48
+ ;;
49
+ test-agent)
50
+ # Patterns that indicate tests passed
51
+ PASS_PATTERN="(all )?[Tt]ests? passed|test suite passed|0 failed|✓.*[Pp]assed|Tests PASSED"
52
+ # Patterns that indicate tests failed
53
+ FAIL_PATTERN="[Tt]ests? failed|test failures|[0-9]+ failed|✗.*[Ff]ailed|Tests FAILED"
54
+ STATUS_FIELD="testStatus"
55
+ ;;
56
+ merge-agent)
57
+ # Patterns that indicate merge complete
58
+ PASS_PATTERN="[Mm]erge (complete|successful)|[Pp]ushed to|[Mm]erged to main|[Mm]erged successfully"
59
+ # Patterns that indicate merge failed
60
+ FAIL_PATTERN="[Mm]erge conflict|[Cc]annot merge|[Mm]erge failed"
61
+ STATUS_FIELD="mergeStatus"
62
+ ;;
63
+ *)
64
+ exit 0
65
+ ;;
66
+ esac
67
+
68
+ # Check for completion patterns (look at output)
69
+ STATUS=""
70
+ if echo "$OUTPUT" | grep -qE "$PASS_PATTERN"; then
71
+ STATUS="passed"
72
+ elif echo "$OUTPUT" | grep -qE "$FAIL_PATTERN"; then
73
+ STATUS="failed"
74
+ fi
75
+
76
+ if [ -z "$STATUS" ]; then
77
+ exit 0 # No recognized completion pattern - specialist is just idle
78
+ fi
79
+
80
+ # Get current issue from specialist state file
81
+ STATE_FILE="$HOME/.panopticon/agents/$AGENT_ID/state.json"
82
+ ISSUE_ID=""
83
+
84
+ if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
85
+ ISSUE_ID=$(jq -r '.currentIssue // ""' "$STATE_FILE" 2>/dev/null || echo "")
86
+ fi
87
+
88
+ if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" = "null" ]; then
89
+ # No issue ID - cannot report completion
90
+ exit 0
91
+ fi
92
+
93
+ # Report completion to dashboard API (non-blocking background process)
94
+ # Only if dashboard is running
95
+ if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
96
+ curl -s -X POST "http://localhost:3011/api/specialists/$SPECIALIST_TYPE/auto-complete" \
97
+ -H "Content-Type: application/json" \
98
+ -d "{\"issueId\":\"$ISSUE_ID\",\"status\":\"$STATUS\"}" \
99
+ > /dev/null 2>&1 &
100
+
101
+ # Log the completion detection
102
+ echo "[specialist-stop-hook] Detected completion: $SPECIALIST_TYPE -> $ISSUE_ID -> $STATUS" \
103
+ >> "$HOME/.panopticon/logs/hooks.log" 2>/dev/null || true
104
+ fi
105
+
106
+ exit 0
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # ~/.panopticon/bin/stop-hook
3
+ # Called by Claude Code when it finishes responding (waiting for input)
4
+ #
5
+ # This hook sets agent state to "idle" for auto-suspend detection
6
+
7
+ # Don't use set -e - we want the hook to be resilient to failures
8
+ # and never break Claude Code execution
9
+
10
+ # Get agent ID from env (set by pan work issue) or tmux session name
11
+ if [ -n "$PANOPTICON_AGENT_ID" ]; then
12
+ AGENT_ID="$PANOPTICON_AGENT_ID"
13
+ elif [ -n "$TMUX" ]; then
14
+ AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
15
+ else
16
+ AGENT_ID="main-cli"
17
+ fi
18
+ AGENT_ID="${AGENT_ID:-unknown}"
19
+
20
+ # Check if jq is available
21
+ if ! command -v jq &> /dev/null; then
22
+ exit 0 # Silent failure - don't break Claude Code execution
23
+ fi
24
+
25
+ # Ensure state directory exists
26
+ STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
27
+ mkdir -p "$STATE_DIR"
28
+
29
+ # Write state to file (atomic write via temp file)
30
+ TEMP_FILE="$STATE_DIR/state.json.tmp"
31
+ jq -n \
32
+ --arg timestamp "$(date -Iseconds)" \
33
+ --arg state "idle" \
34
+ '{
35
+ state: $state,
36
+ lastActivity: $timestamp
37
+ }' > "$TEMP_FILE" 2>/dev/null || true
38
+
39
+ mv "$TEMP_FILE" "$STATE_DIR/state.json" 2>/dev/null || true
40
+
41
+ # Optionally send heartbeat to API server (non-blocking)
42
+ # Only if dashboard is running
43
+ if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
44
+ curl -s -X POST "http://localhost:3011/api/agents/$AGENT_ID/heartbeat" \
45
+ -H "Content-Type: application/json" \
46
+ -d "{\"state\":\"idle\",\"timestamp\":\"$(date -Iseconds)\"}" \
47
+ > /dev/null 2>&1 &
48
+ fi
49
+
50
+ # Chain to specialist-specific hook for auto-completion detection
51
+ # This runs in background and only affects specialist agents
52
+ SPECIALIST_HOOK="$HOME/.panopticon/bin/specialist-stop-hook"
53
+ if [ -x "$SPECIALIST_HOOK" ]; then
54
+ "$SPECIALIST_HOOK" &
55
+ fi
56
+
57
+ # Always exit successfully
58
+ exit 0
@@ -0,0 +1,119 @@
1
+ #!/bin/bash
2
+ # validate-merge.sh - Validation script for merge completeness
3
+ # Checks for conflict markers, runs build, and runs tests
4
+ # Usage: validate-merge.sh [PROJECT_ROOT]
5
+ # Exit codes: 0 = validation passed, 1 = validation failed
6
+
7
+ set -e
8
+
9
+ PROJECT_ROOT="${1:-.}"
10
+ cd "$PROJECT_ROOT"
11
+
12
+ echo "=== Merge Validation ==="
13
+ echo "Project root: $(pwd)"
14
+ echo ""
15
+
16
+ # 1. Check for conflict markers
17
+ echo "Checking for conflict markers..."
18
+ CONFLICT_FOUND=false
19
+
20
+ # Check for <<<<<<< markers (at start of line only)
21
+ if git grep -l '^<<<<<<< ' 2>/dev/null; then
22
+ echo "ERROR: Conflict start markers found in files:"
23
+ git grep -l '^<<<<<<< '
24
+ CONFLICT_FOUND=true
25
+ fi
26
+
27
+ # Check for ======= markers (at start of line)
28
+ if git grep -l '^=======$' 2>/dev/null; then
29
+ echo "ERROR: Conflict separator markers found in files:"
30
+ git grep -l '^=======$'
31
+ CONFLICT_FOUND=true
32
+ fi
33
+
34
+ # Check for >>>>>>> markers (at start of line only)
35
+ if git grep -l '^>>>>>>> ' 2>/dev/null; then
36
+ echo "ERROR: Conflict end markers found in files:"
37
+ git grep -l '^>>>>>>> '
38
+ CONFLICT_FOUND=true
39
+ fi
40
+
41
+ if [ "$CONFLICT_FOUND" = true ]; then
42
+ echo ""
43
+ echo "VALIDATION FAILED: Conflict markers detected"
44
+ exit 1
45
+ fi
46
+
47
+ echo "✓ No conflict markers found"
48
+ echo ""
49
+
50
+ # 2. Run build
51
+ echo "Running build..."
52
+ if [ -f "package.json" ]; then
53
+ if npm run build 2>&1; then
54
+ echo "✓ Build passed"
55
+ else
56
+ echo ""
57
+ echo "VALIDATION FAILED: Build errors detected"
58
+ exit 1
59
+ fi
60
+ elif [ -f "pom.xml" ]; then
61
+ if mvn compile 2>&1; then
62
+ echo "✓ Build passed"
63
+ else
64
+ echo ""
65
+ echo "VALIDATION FAILED: Build errors detected"
66
+ exit 1
67
+ fi
68
+ else
69
+ echo "⚠ No build system detected (no package.json or pom.xml), skipping build check"
70
+ fi
71
+ echo ""
72
+
73
+ # 3. Run tests
74
+ # BASELINE_FAILURES env var enables baseline comparison mode:
75
+ # If set, only fail when NEW failures appear (failures > baseline)
76
+ echo "Running tests..."
77
+ if [ -n "$BASELINE_FAILURES" ]; then
78
+ echo "Baseline comparison mode: $BASELINE_FAILURES pre-existing failures"
79
+ fi
80
+
81
+ if [ -f "package.json" ]; then
82
+ TEST_OUTPUT=$(npm test 2>&1) || true
83
+ echo "$TEST_OUTPUT"
84
+
85
+ # Check if tests passed cleanly
86
+ if echo "$TEST_OUTPUT" | grep -q "Tests.*passed\|0 failed"; then
87
+ echo "✓ Tests passed"
88
+ elif [ -n "$BASELINE_FAILURES" ]; then
89
+ # Baseline mode: extract failure count and compare
90
+ # Match vitest output format: "X failed" or "X tests failed"
91
+ ACTUAL_FAILURES=$(echo "$TEST_OUTPUT" | grep -oP '(\d+)\s+failed' | head -1 | grep -oP '\d+' || echo "0")
92
+ if [ "$ACTUAL_FAILURES" -le "$BASELINE_FAILURES" ]; then
93
+ echo ""
94
+ echo "✓ Tests passed (baseline comparison: $ACTUAL_FAILURES failures <= $BASELINE_FAILURES baseline)"
95
+ else
96
+ echo ""
97
+ echo "VALIDATION FAILED: New test failures detected ($ACTUAL_FAILURES > baseline $BASELINE_FAILURES)"
98
+ exit 1
99
+ fi
100
+ else
101
+ echo ""
102
+ echo "VALIDATION FAILED: Test failures detected"
103
+ exit 1
104
+ fi
105
+ elif [ -f "pom.xml" ]; then
106
+ if mvn test 2>&1; then
107
+ echo "✓ Tests passed"
108
+ else
109
+ echo ""
110
+ echo "VALIDATION FAILED: Test failures detected"
111
+ exit 1
112
+ fi
113
+ else
114
+ echo "⚠ No test system detected, skipping test check"
115
+ fi
116
+ echo ""
117
+
118
+ echo "=== VALIDATION PASSED ==="
119
+ exit 0