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.
- package/package.json +3 -2
- package/scripts/heartbeat-hook +149 -0
- package/scripts/install-git-hooks.sh +66 -0
- package/scripts/notify-complete +79 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/pre-tool-hook +60 -0
- package/scripts/record-cost-event.js +94 -0
- package/scripts/record-cost-event.ts +113 -0
- package/scripts/restart-dashboard.sh +59 -0
- package/scripts/setup-certs.sh +66 -0
- package/scripts/specialist-stop-hook +106 -0
- package/scripts/stop-hook +58 -0
- package/scripts/validate-merge.sh +119 -0
- package/skills/beads/README.md +120 -0
- package/skills/beads/SKILL.md +214 -0
- package/skills/beads/adr/0001-bd-prime-as-source-of-truth.md +59 -0
- package/skills/beads/resources/AGENTS.md +62 -0
- package/skills/beads/resources/ASYNC_GATES.md +168 -0
- package/skills/beads/resources/BOUNDARIES.md +469 -0
- package/skills/beads/resources/CHEMISTRY_PATTERNS.md +197 -0
- package/skills/beads/resources/CLI_REFERENCE.md +558 -0
- package/skills/beads/resources/DEPENDENCIES.md +747 -0
- package/skills/beads/resources/INTEGRATION_PATTERNS.md +407 -0
- package/skills/beads/resources/ISSUE_CREATION.md +139 -0
- package/skills/beads/resources/MOLECULES.md +354 -0
- package/skills/beads/resources/PATTERNS.md +341 -0
- package/skills/beads/resources/RESUMABILITY.md +207 -0
- package/skills/beads/resources/STATIC_DATA.md +54 -0
- package/skills/beads/resources/TROUBLESHOOTING.md +489 -0
- package/skills/beads/resources/WORKFLOWS.md +623 -0
- package/skills/beads/resources/WORKTREES.md +94 -0
- package/skills/beads-completion-check/SKILL.md +90 -0
- package/skills/beads-panopticon-guide/SKILL.md +171 -0
- package/skills/bug-fix/SKILL.md +32 -0
- package/skills/clear-writing/SKILL.md +105 -0
- package/skills/clear-writing/references/elements-of-style/01-introductory.md +3 -0
- package/skills/clear-writing/references/elements-of-style/02-elementary-rules-of-usage.md +214 -0
- package/skills/clear-writing/references/elements-of-style/03-elementary-principles-of-composition.md +398 -0
- package/skills/clear-writing/references/elements-of-style/04-a-few-matters-of-form.md +89 -0
- package/skills/clear-writing/references/elements-of-style/05-words-and-expressions-commonly-misused.md +342 -0
- package/skills/clear-writing/references/signs-of-ai-writing.md +901 -0
- package/skills/code-review/SKILL.md +37 -0
- package/skills/code-review-performance/SKILL.md +53 -0
- package/skills/code-review-security/SKILL.md +35 -0
- package/skills/dependency-update/SKILL.md +30 -0
- package/skills/feature-work/SKILL.md +39 -0
- package/skills/incident-response/SKILL.md +32 -0
- package/skills/knowledge-capture/SKILL.md +463 -0
- package/skills/onboard-codebase/SKILL.md +34 -0
- package/skills/opus-plan/SKILL.md +400 -0
- package/skills/pan-approve/SKILL.md +136 -0
- package/skills/pan-code-review/SKILL.md +249 -0
- package/skills/pan-config/SKILL.md +164 -0
- package/skills/pan-convoy-synthesis/SKILL.md +249 -0
- package/skills/pan-diagnose/SKILL.md +360 -0
- package/skills/pan-docker/SKILL.md +279 -0
- package/skills/pan-docs/SKILL.md +113 -0
- package/skills/pan-down/SKILL.md +434 -0
- package/skills/pan-health/SKILL.md +240 -0
- package/skills/pan-help/SKILL.md +237 -0
- package/skills/pan-install/SKILL.md +339 -0
- package/skills/pan-issue/SKILL.md +596 -0
- package/skills/pan-kill/SKILL.md +172 -0
- package/skills/pan-logs/SKILL.md +255 -0
- package/skills/pan-network/SKILL.md +320 -0
- package/skills/pan-oversee/SKILL.md +290 -0
- package/skills/pan-plan/SKILL.md +521 -0
- package/skills/pan-projects/SKILL.md +239 -0
- package/skills/pan-quickstart/SKILL.md +440 -0
- package/skills/pan-reload/SKILL.md +44 -0
- package/skills/pan-rescue/SKILL.md +271 -0
- package/skills/pan-restart/SKILL.md +53 -0
- package/skills/pan-setup/SKILL.md +478 -0
- package/skills/pan-skill-creator/SKILL.md +168 -0
- package/skills/pan-skill-creator/references/output-patterns.md +141 -0
- package/skills/pan-skill-creator/references/workflows.md +90 -0
- package/skills/pan-skill-creator/scripts/init_skill.py +176 -0
- package/skills/pan-status/SKILL.md +493 -0
- package/skills/pan-subagent-creator/SKILL.md +295 -0
- package/skills/pan-subagent-creator/assets/validate-readonly-query.sh +35 -0
- package/skills/pan-subagent-creator/references/example-agents.md +308 -0
- package/skills/pan-subagent-creator/scripts/init_agent.py +126 -0
- package/skills/pan-sync/SKILL.md +272 -0
- package/skills/pan-tell/SKILL.md +157 -0
- package/skills/pan-test-config/SKILL.md +208 -0
- package/skills/pan-tracker/SKILL.md +288 -0
- package/skills/pan-up/SKILL.md +458 -0
- package/skills/pan-workspace-config/SKILL.md +303 -0
- package/skills/refactor/SKILL.md +30 -0
- package/skills/refactor-radar/SKILL.md +475 -0
- package/skills/release/SKILL.md +25 -0
- package/skills/send-feedback-to-agent/SKILL.md +98 -0
- package/skills/session-health/SKILL.md +76 -0
- package/skills/session-health/scripts/check_sessions.py +166 -0
- package/skills/skill-creator/SKILL.md +92 -0
- package/skills/skill-creator/scripts/init_skill.py +152 -0
- package/skills/skill-creator/scripts/package_skill.py +123 -0
- package/skills/stitch-design-md/README.md +34 -0
- package/skills/stitch-design-md/SKILL.md +172 -0
- package/skills/stitch-design-md/examples/DESIGN.md +154 -0
- package/skills/stitch-react-components/README.md +36 -0
- package/skills/stitch-react-components/SKILL.md +47 -0
- package/skills/stitch-react-components/examples/gold-standard-card.tsx +80 -0
- package/skills/stitch-react-components/package-lock.json +231 -0
- package/skills/stitch-react-components/package.json +16 -0
- package/skills/stitch-react-components/resources/architecture-checklist.md +15 -0
- package/skills/stitch-react-components/resources/component-template.tsx +37 -0
- package/skills/stitch-react-components/resources/stitch-api-reference.md +14 -0
- package/skills/stitch-react-components/resources/style-guide.json +27 -0
- package/skills/stitch-react-components/scripts/fetch-stitch.sh +30 -0
- package/skills/stitch-react-components/scripts/validate.js +68 -0
- package/skills/stitch-setup/SKILL.md +94 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- 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.
|
|
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
|
-
"
|
|
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
|