panopticon-cli 0.4.6 → 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.
Files changed (114) hide show
  1. package/package.json +3 -2
  2. package/scripts/heartbeat-hook +149 -0
  3. package/scripts/install-git-hooks.sh +66 -0
  4. package/scripts/notify-complete +79 -0
  5. package/scripts/postinstall.mjs +49 -0
  6. package/scripts/pre-tool-hook +60 -0
  7. package/scripts/record-cost-event.js +94 -0
  8. package/scripts/record-cost-event.ts +113 -0
  9. package/scripts/restart-dashboard.sh +59 -0
  10. package/scripts/setup-certs.sh +66 -0
  11. package/scripts/specialist-stop-hook +106 -0
  12. package/scripts/stop-hook +58 -0
  13. package/scripts/validate-merge.sh +119 -0
  14. package/skills/beads/README.md +120 -0
  15. package/skills/beads/SKILL.md +214 -0
  16. package/skills/beads/adr/0001-bd-prime-as-source-of-truth.md +59 -0
  17. package/skills/beads/resources/AGENTS.md +62 -0
  18. package/skills/beads/resources/ASYNC_GATES.md +168 -0
  19. package/skills/beads/resources/BOUNDARIES.md +469 -0
  20. package/skills/beads/resources/CHEMISTRY_PATTERNS.md +197 -0
  21. package/skills/beads/resources/CLI_REFERENCE.md +558 -0
  22. package/skills/beads/resources/DEPENDENCIES.md +747 -0
  23. package/skills/beads/resources/INTEGRATION_PATTERNS.md +407 -0
  24. package/skills/beads/resources/ISSUE_CREATION.md +139 -0
  25. package/skills/beads/resources/MOLECULES.md +354 -0
  26. package/skills/beads/resources/PATTERNS.md +341 -0
  27. package/skills/beads/resources/RESUMABILITY.md +207 -0
  28. package/skills/beads/resources/STATIC_DATA.md +54 -0
  29. package/skills/beads/resources/TROUBLESHOOTING.md +489 -0
  30. package/skills/beads/resources/WORKFLOWS.md +623 -0
  31. package/skills/beads/resources/WORKTREES.md +94 -0
  32. package/skills/beads-completion-check/SKILL.md +90 -0
  33. package/skills/beads-panopticon-guide/SKILL.md +171 -0
  34. package/skills/bug-fix/SKILL.md +32 -0
  35. package/skills/clear-writing/SKILL.md +105 -0
  36. package/skills/clear-writing/references/elements-of-style/01-introductory.md +3 -0
  37. package/skills/clear-writing/references/elements-of-style/02-elementary-rules-of-usage.md +214 -0
  38. package/skills/clear-writing/references/elements-of-style/03-elementary-principles-of-composition.md +398 -0
  39. package/skills/clear-writing/references/elements-of-style/04-a-few-matters-of-form.md +89 -0
  40. package/skills/clear-writing/references/elements-of-style/05-words-and-expressions-commonly-misused.md +342 -0
  41. package/skills/clear-writing/references/signs-of-ai-writing.md +901 -0
  42. package/skills/code-review/SKILL.md +37 -0
  43. package/skills/code-review-performance/SKILL.md +53 -0
  44. package/skills/code-review-security/SKILL.md +35 -0
  45. package/skills/dependency-update/SKILL.md +30 -0
  46. package/skills/feature-work/SKILL.md +39 -0
  47. package/skills/incident-response/SKILL.md +32 -0
  48. package/skills/knowledge-capture/SKILL.md +463 -0
  49. package/skills/onboard-codebase/SKILL.md +34 -0
  50. package/skills/opus-plan/SKILL.md +400 -0
  51. package/skills/pan-approve/SKILL.md +136 -0
  52. package/skills/pan-code-review/SKILL.md +249 -0
  53. package/skills/pan-config/SKILL.md +164 -0
  54. package/skills/pan-convoy-synthesis/SKILL.md +249 -0
  55. package/skills/pan-diagnose/SKILL.md +360 -0
  56. package/skills/pan-docker/SKILL.md +279 -0
  57. package/skills/pan-docs/SKILL.md +113 -0
  58. package/skills/pan-down/SKILL.md +434 -0
  59. package/skills/pan-health/SKILL.md +240 -0
  60. package/skills/pan-help/SKILL.md +237 -0
  61. package/skills/pan-install/SKILL.md +339 -0
  62. package/skills/pan-issue/SKILL.md +596 -0
  63. package/skills/pan-kill/SKILL.md +172 -0
  64. package/skills/pan-logs/SKILL.md +255 -0
  65. package/skills/pan-network/SKILL.md +320 -0
  66. package/skills/pan-oversee/SKILL.md +290 -0
  67. package/skills/pan-plan/SKILL.md +521 -0
  68. package/skills/pan-projects/SKILL.md +239 -0
  69. package/skills/pan-quickstart/SKILL.md +440 -0
  70. package/skills/pan-reload/SKILL.md +44 -0
  71. package/skills/pan-rescue/SKILL.md +271 -0
  72. package/skills/pan-restart/SKILL.md +53 -0
  73. package/skills/pan-setup/SKILL.md +478 -0
  74. package/skills/pan-skill-creator/SKILL.md +168 -0
  75. package/skills/pan-skill-creator/references/output-patterns.md +141 -0
  76. package/skills/pan-skill-creator/references/workflows.md +90 -0
  77. package/skills/pan-skill-creator/scripts/init_skill.py +176 -0
  78. package/skills/pan-status/SKILL.md +493 -0
  79. package/skills/pan-subagent-creator/SKILL.md +295 -0
  80. package/skills/pan-subagent-creator/assets/validate-readonly-query.sh +35 -0
  81. package/skills/pan-subagent-creator/references/example-agents.md +308 -0
  82. package/skills/pan-subagent-creator/scripts/init_agent.py +126 -0
  83. package/skills/pan-sync/SKILL.md +272 -0
  84. package/skills/pan-tell/SKILL.md +157 -0
  85. package/skills/pan-test-config/SKILL.md +208 -0
  86. package/skills/pan-tracker/SKILL.md +288 -0
  87. package/skills/pan-up/SKILL.md +458 -0
  88. package/skills/pan-workspace-config/SKILL.md +303 -0
  89. package/skills/refactor/SKILL.md +30 -0
  90. package/skills/refactor-radar/SKILL.md +475 -0
  91. package/skills/release/SKILL.md +25 -0
  92. package/skills/send-feedback-to-agent/SKILL.md +98 -0
  93. package/skills/session-health/SKILL.md +76 -0
  94. package/skills/session-health/scripts/check_sessions.py +166 -0
  95. package/skills/skill-creator/SKILL.md +92 -0
  96. package/skills/skill-creator/scripts/init_skill.py +152 -0
  97. package/skills/skill-creator/scripts/package_skill.py +123 -0
  98. package/skills/stitch-design-md/README.md +34 -0
  99. package/skills/stitch-design-md/SKILL.md +172 -0
  100. package/skills/stitch-design-md/examples/DESIGN.md +154 -0
  101. package/skills/stitch-react-components/README.md +36 -0
  102. package/skills/stitch-react-components/SKILL.md +47 -0
  103. package/skills/stitch-react-components/examples/gold-standard-card.tsx +80 -0
  104. package/skills/stitch-react-components/package-lock.json +231 -0
  105. package/skills/stitch-react-components/package.json +16 -0
  106. package/skills/stitch-react-components/resources/architecture-checklist.md +15 -0
  107. package/skills/stitch-react-components/resources/component-template.tsx +37 -0
  108. package/skills/stitch-react-components/resources/stitch-api-reference.md +14 -0
  109. package/skills/stitch-react-components/resources/style-guide.json +27 -0
  110. package/skills/stitch-react-components/scripts/fetch-stitch.sh +30 -0
  111. package/skills/stitch-react-components/scripts/validate.js +68 -0
  112. package/skills/stitch-setup/SKILL.md +94 -0
  113. package/skills/web-design-guidelines/SKILL.md +39 -0
  114. package/skills/work-complete/SKILL.md +79 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panopticon-cli",
3
- "version": "0.4.6",
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",
@@ -38,7 +38,8 @@
38
38
  "files": [
39
39
  "dist",
40
40
  "templates",
41
- "scripts/git-hooks",
41
+ "skills",
42
+ "scripts",
42
43
  "README.md",
43
44
  "LICENSE"
44
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