@paths.design/caws-cli 9.3.2 → 10.1.0
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/README.md +71 -32
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +41 -15
- package/dist/commands/gates.js +149 -0
- package/dist/commands/init.js +150 -19
- package/dist/commands/iterate.js +81 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +74 -0
- package/dist/commands/specs.js +381 -45
- package/dist/commands/status.js +117 -9
- package/dist/commands/templates.js +0 -8
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +70 -6
- package/dist/commands/verify-acs.js +48 -76
- package/dist/commands/waivers.js +212 -13
- package/dist/commands/worktree.js +131 -26
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +121 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +109 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +157 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +51 -17
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +105 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +112 -0
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
- package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/dist/templates/.claude/settings.json +31 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +77 -8
- package/dist/templates/agents.md +50 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +50 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +530 -0
- package/dist/validation/spec-validation.js +191 -31
- package/dist/waivers-manager.js +144 -6
- package/dist/worktree/worktree-manager.js +598 -95
- package/package.json +9 -8
- package/templates/.caws/schemas/policy.schema.json +112 -0
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +96 -20
- package/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +10 -4
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +136 -55
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/templates/.claude/settings.json +31 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +77 -8
- package/templates/{AGENTS.md → agents.md} +50 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Session Logger for Cursor IDE
|
|
3
|
+
#
|
|
4
|
+
# Dual-mode session logging:
|
|
5
|
+
# 1. Incremental: accumulates events from Cursor hook invocations during session
|
|
6
|
+
# 2. Workspace DB: on stop, reads Cursor's state.vscdb to enrich with metadata
|
|
7
|
+
# (composer name, model, mode, prompts, generation history)
|
|
8
|
+
# 3. Global DB: on stop, reads cursorDiskKV bubbleId entries to reconstruct
|
|
9
|
+
# full conversation including assistant responses, code blocks, file actions
|
|
10
|
+
#
|
|
11
|
+
# Generates:
|
|
12
|
+
# session.txt — lightweight index (header + turn list + exploration + audit)
|
|
13
|
+
# turn-001.txt — per-turn narrative (user + assistant exchange + tool calls)
|
|
14
|
+
# turn-001.json — per-turn structured data (tools + edits + results + responses)
|
|
15
|
+
#
|
|
16
|
+
# Cursor stores data across:
|
|
17
|
+
# - globalStorage/state.vscdb → cursorDiskKV composerData:<id> (bubble headers)
|
|
18
|
+
# - globalStorage/state.vscdb → cursorDiskKV bubbleId:<conv_id>:<bubble_id> (message content)
|
|
19
|
+
# - workspaceStorage/*/state.vscdb → ItemTable composer.composerData (session metadata)
|
|
20
|
+
# - workspaceStorage/*/state.vscdb → ItemTable aiService.prompts / aiService.generations
|
|
21
|
+
# - ~/.cursor/ai-tracking/ai-code-tracking.db → conversation_summaries (tldr, overview)
|
|
22
|
+
#
|
|
23
|
+
# Output: ./tmp/<conversation-id>/
|
|
24
|
+
#
|
|
25
|
+
# Wired into: beforeSubmitPrompt, afterFileEdit, beforeShellExecution,
|
|
26
|
+
# beforeReadFile, afterAgentResponse, afterAgentThought, stop
|
|
27
|
+
#
|
|
28
|
+
# @author @darianrosebrook
|
|
29
|
+
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
INPUT=$(cat)
|
|
33
|
+
|
|
34
|
+
# --- Parse common fields ---
|
|
35
|
+
CONVERSATION_ID=$(echo "$INPUT" | jq -r '.conversation_id // "unknown"')
|
|
36
|
+
GENERATION_ID=$(echo "$INPUT" | jq -r '.generation_id // "none"')
|
|
37
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
|
|
38
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
39
|
+
CWD=$(pwd)
|
|
40
|
+
|
|
41
|
+
# --- Log directory ---
|
|
42
|
+
LOG_DIR="${CWD}/tmp/${CONVERSATION_ID}"
|
|
43
|
+
mkdir -p "$LOG_DIR"
|
|
44
|
+
|
|
45
|
+
EVENTS_FILE="$LOG_DIR/.events.jsonl"
|
|
46
|
+
META_FILE="$LOG_DIR/.meta.json"
|
|
47
|
+
SESSION_MD="$LOG_DIR/session.txt"
|
|
48
|
+
|
|
49
|
+
# ============================================================
|
|
50
|
+
# Helper: append an event to the accumulator
|
|
51
|
+
# ============================================================
|
|
52
|
+
append_event() {
|
|
53
|
+
local event_json="$1"
|
|
54
|
+
echo "$event_json" >> "$EVENTS_FILE"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ============================================================
|
|
58
|
+
# Helper: resolve Cursor workspace state.vscdb path
|
|
59
|
+
# ============================================================
|
|
60
|
+
resolve_workspace_db() {
|
|
61
|
+
local cursor_data=""
|
|
62
|
+
|
|
63
|
+
if [ -d "$HOME/Library/Application Support/Cursor/User/workspaceStorage" ]; then
|
|
64
|
+
cursor_data="$HOME/Library/Application Support/Cursor/User/workspaceStorage"
|
|
65
|
+
elif [ -d "$HOME/.config/Cursor/User/workspaceStorage" ]; then
|
|
66
|
+
cursor_data="$HOME/.config/Cursor/User/workspaceStorage"
|
|
67
|
+
elif [ -d "${APPDATA:-}/Cursor/User/workspaceStorage" ] 2>/dev/null; then
|
|
68
|
+
cursor_data="${APPDATA}/Cursor/User/workspaceStorage"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ -z "$cursor_data" ]; then
|
|
72
|
+
echo ""
|
|
73
|
+
return
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
for ws_dir in "$cursor_data"/*/; do
|
|
77
|
+
local ws_json="${ws_dir}workspace.json"
|
|
78
|
+
if [ -f "$ws_json" ]; then
|
|
79
|
+
local folder
|
|
80
|
+
folder=$(python3 -c "
|
|
81
|
+
import json, sys
|
|
82
|
+
try:
|
|
83
|
+
d = json.load(open(sys.argv[1]))
|
|
84
|
+
print(d.get('folder', ''))
|
|
85
|
+
except: pass
|
|
86
|
+
" "$ws_json" 2>/dev/null)
|
|
87
|
+
|
|
88
|
+
local expected="file://${CWD}"
|
|
89
|
+
if [ "$folder" = "$expected" ]; then
|
|
90
|
+
local db="${ws_dir}state.vscdb"
|
|
91
|
+
if [ -f "$db" ]; then
|
|
92
|
+
echo "$db"
|
|
93
|
+
return
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
echo ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ============================================================
|
|
103
|
+
# Helper: resolve Cursor global state.vscdb path
|
|
104
|
+
# ============================================================
|
|
105
|
+
resolve_global_db() {
|
|
106
|
+
local candidates=(
|
|
107
|
+
"$HOME/Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
|
108
|
+
"$HOME/.config/Cursor/User/globalStorage/state.vscdb"
|
|
109
|
+
)
|
|
110
|
+
for db in "${candidates[@]}"; do
|
|
111
|
+
if [ -f "$db" ]; then
|
|
112
|
+
echo "$db"
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
done
|
|
116
|
+
echo ""
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ============================================================
|
|
120
|
+
# Helper: resolve Cursor ai-tracking database
|
|
121
|
+
# ============================================================
|
|
122
|
+
resolve_tracking_db() {
|
|
123
|
+
local candidates=(
|
|
124
|
+
"$HOME/.cursor/ai-tracking/ai-code-tracking.db"
|
|
125
|
+
"$HOME/.config/Cursor/ai-tracking/ai-code-tracking.db"
|
|
126
|
+
)
|
|
127
|
+
for db in "${candidates[@]}"; do
|
|
128
|
+
if [ -f "$db" ]; then
|
|
129
|
+
echo "$db"
|
|
130
|
+
return
|
|
131
|
+
fi
|
|
132
|
+
done
|
|
133
|
+
echo ""
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ============================================================
|
|
137
|
+
# EVENT: beforeSubmitPrompt — capture user prompts (turn boundaries)
|
|
138
|
+
# ============================================================
|
|
139
|
+
handle_before_submit_prompt() {
|
|
140
|
+
local prompt
|
|
141
|
+
prompt=$(echo "$INPUT" | jq -r '.prompt // ""')
|
|
142
|
+
|
|
143
|
+
if [ -z "$prompt" ] || [ "$prompt" = "null" ]; then
|
|
144
|
+
return
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
append_event "$(jq -cn \
|
|
148
|
+
--arg ev "user_text" \
|
|
149
|
+
--arg text "$prompt" \
|
|
150
|
+
--arg ts "$TIMESTAMP" \
|
|
151
|
+
--arg gen "$GENERATION_ID" \
|
|
152
|
+
'{ev: $ev, text: $text, ts: $ts, generation_id: $gen}')"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ============================================================
|
|
156
|
+
# EVENT: afterFileEdit — capture file edits
|
|
157
|
+
# ============================================================
|
|
158
|
+
handle_after_file_edit() {
|
|
159
|
+
local file_path action_type
|
|
160
|
+
file_path=$(echo "$INPUT" | jq -r '.file_path // ""')
|
|
161
|
+
action_type=$(echo "$INPUT" | jq -r '.action // "edit"')
|
|
162
|
+
|
|
163
|
+
file_path=$(echo "$file_path" | sed "s|${CWD}/||")
|
|
164
|
+
|
|
165
|
+
append_event "$(jq -cn \
|
|
166
|
+
--arg ev "file_edit" \
|
|
167
|
+
--arg file "$file_path" \
|
|
168
|
+
--arg action "$action_type" \
|
|
169
|
+
--arg ts "$TIMESTAMP" \
|
|
170
|
+
--arg gen "$GENERATION_ID" \
|
|
171
|
+
'{ev: $ev, file: $file, action: $action, ts: $ts, generation_id: $gen}')"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ============================================================
|
|
175
|
+
# EVENT: afterAgentResponse — capture assistant response text at turn end
|
|
176
|
+
# ============================================================
|
|
177
|
+
handle_after_agent_response() {
|
|
178
|
+
local response
|
|
179
|
+
response=$(echo "$INPUT" | jq -r '.response // ""')
|
|
180
|
+
|
|
181
|
+
if [ -z "$response" ] || [ "$response" = "null" ]; then
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
append_event "$(jq -cn \
|
|
186
|
+
--arg ev "agent_response" \
|
|
187
|
+
--arg text "$response" \
|
|
188
|
+
--arg ts "$TIMESTAMP" \
|
|
189
|
+
--arg gen "$GENERATION_ID" \
|
|
190
|
+
'{ev: $ev, text: $text, ts: $ts, generation_id: $gen}')"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# ============================================================
|
|
194
|
+
# EVENT: afterAgentThought — capture agent reasoning steps
|
|
195
|
+
# ============================================================
|
|
196
|
+
handle_after_agent_thought() {
|
|
197
|
+
local thought
|
|
198
|
+
thought=$(echo "$INPUT" | jq -r '.thought // ""')
|
|
199
|
+
|
|
200
|
+
if [ -z "$thought" ] || [ "$thought" = "null" ]; then
|
|
201
|
+
return
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
append_event "$(jq -cn \
|
|
205
|
+
--arg ev "agent_thought" \
|
|
206
|
+
--arg text "$thought" \
|
|
207
|
+
--arg ts "$TIMESTAMP" \
|
|
208
|
+
--arg gen "$GENERATION_ID" \
|
|
209
|
+
'{ev: $ev, text: $text, ts: $ts, generation_id: $gen}')"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# ============================================================
|
|
213
|
+
# EVENT: beforeShellExecution — capture shell commands
|
|
214
|
+
# ============================================================
|
|
215
|
+
handle_before_shell_execution() {
|
|
216
|
+
local command
|
|
217
|
+
command=$(echo "$INPUT" | jq -r '.command // ""')
|
|
218
|
+
|
|
219
|
+
if [ -z "$command" ] || [ "$command" = "null" ]; then
|
|
220
|
+
return
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
append_event "$(jq -cn \
|
|
224
|
+
--arg ev "shell_command" \
|
|
225
|
+
--arg cmd "$command" \
|
|
226
|
+
--arg ts "$TIMESTAMP" \
|
|
227
|
+
--arg gen "$GENERATION_ID" \
|
|
228
|
+
'{ev: $ev, command: $cmd, ts: $ts, generation_id: $gen}')"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# ============================================================
|
|
232
|
+
# EVENT: beforeReadFile — capture file reads
|
|
233
|
+
# ============================================================
|
|
234
|
+
handle_before_read_file() {
|
|
235
|
+
local file_path
|
|
236
|
+
file_path=$(echo "$INPUT" | jq -r '.file_path // ""')
|
|
237
|
+
|
|
238
|
+
file_path=$(echo "$file_path" | sed "s|${CWD}/||")
|
|
239
|
+
|
|
240
|
+
append_event "$(jq -cn \
|
|
241
|
+
--arg ev "file_read" \
|
|
242
|
+
--arg file "$file_path" \
|
|
243
|
+
--arg ts "$TIMESTAMP" \
|
|
244
|
+
--arg gen "$GENERATION_ID" \
|
|
245
|
+
'{ev: $ev, file: $file, ts: $ts, generation_id: $gen}')"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# ============================================================
|
|
249
|
+
# EVENT: stop — generate session output
|
|
250
|
+
# ============================================================
|
|
251
|
+
handle_stop() {
|
|
252
|
+
local branch head_sha dirty_count
|
|
253
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
254
|
+
head_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
255
|
+
dirty_count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
256
|
+
|
|
257
|
+
local started_at model
|
|
258
|
+
if [ -f "$META_FILE" ]; then
|
|
259
|
+
started_at=$(jq -r '.local_time // "unknown"' "$META_FILE")
|
|
260
|
+
model=$(jq -r '.model // "cursor-agent"' "$META_FILE")
|
|
261
|
+
else
|
|
262
|
+
started_at="(unknown)"
|
|
263
|
+
model="cursor-agent"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
local workspace_db tracking_db global_db
|
|
267
|
+
workspace_db=$(resolve_workspace_db)
|
|
268
|
+
tracking_db=$(resolve_tracking_db)
|
|
269
|
+
global_db=$(resolve_global_db)
|
|
270
|
+
|
|
271
|
+
if [ ! -f "$EVENTS_FILE" ] || [ ! -s "$EVENTS_FILE" ]; then
|
|
272
|
+
cat > "$SESSION_MD" << MDEOF
|
|
273
|
+
# Session Log: $(basename "$CWD")
|
|
274
|
+
|
|
275
|
+
| Field | Value |
|
|
276
|
+
|-------|-------|
|
|
277
|
+
| Conversation ID | \`${CONVERSATION_ID}\` |
|
|
278
|
+
| Started | ${started_at} |
|
|
279
|
+
| Model | ${model} |
|
|
280
|
+
| Branch | \`${branch}\` @ \`${head_sha}\` |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
_No events captured. Session log unavailable._
|
|
285
|
+
MDEOF
|
|
286
|
+
return
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
local pyscript
|
|
290
|
+
pyscript=$(mktemp "${TMPDIR:-/tmp}/cursor-session-log-XXXX.py")
|
|
291
|
+
trap "rm -f '$pyscript'" RETURN
|
|
292
|
+
cat > "$pyscript" << 'PYEOF'
|
|
293
|
+
import json, sys, os, sqlite3, base64
|
|
294
|
+
from datetime import datetime
|
|
295
|
+
|
|
296
|
+
log_dir = sys.argv[1]
|
|
297
|
+
cwd = sys.argv[2]
|
|
298
|
+
conversation_id = sys.argv[3]
|
|
299
|
+
started_at = sys.argv[4]
|
|
300
|
+
model = sys.argv[5]
|
|
301
|
+
branch = sys.argv[6]
|
|
302
|
+
head_sha = sys.argv[7]
|
|
303
|
+
dirty_count = sys.argv[8]
|
|
304
|
+
workspace_db = sys.argv[9] if len(sys.argv) > 9 else ""
|
|
305
|
+
tracking_db = sys.argv[10] if len(sys.argv) > 10 else ""
|
|
306
|
+
global_db = sys.argv[11] if len(sys.argv) > 11 else ""
|
|
307
|
+
|
|
308
|
+
def rel(path):
|
|
309
|
+
if path and path.startswith(cwd + "/"):
|
|
310
|
+
return path[len(cwd) + 1:]
|
|
311
|
+
return path or ""
|
|
312
|
+
|
|
313
|
+
def open_ro(path):
|
|
314
|
+
return sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
315
|
+
|
|
316
|
+
def decode_value(value):
|
|
317
|
+
"""Decode a DB value: JSON string, bytes, or base64-encoded JSON."""
|
|
318
|
+
if value is None:
|
|
319
|
+
return None
|
|
320
|
+
if isinstance(value, bytes):
|
|
321
|
+
try:
|
|
322
|
+
return json.loads(value.decode("utf-8"))
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
try:
|
|
326
|
+
return json.loads(base64.b64decode(value))
|
|
327
|
+
except Exception:
|
|
328
|
+
return None
|
|
329
|
+
if isinstance(value, str):
|
|
330
|
+
try:
|
|
331
|
+
return json.loads(value)
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
try:
|
|
335
|
+
return json.loads(base64.b64decode(value))
|
|
336
|
+
except Exception:
|
|
337
|
+
return None
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
# ---- Enrich from workspace state.vscdb (session metadata) ----
|
|
341
|
+
composer_meta = {}
|
|
342
|
+
if workspace_db and os.path.isfile(workspace_db):
|
|
343
|
+
try:
|
|
344
|
+
conn = open_ro(workspace_db)
|
|
345
|
+
cur = conn.cursor()
|
|
346
|
+
cur.execute("SELECT value FROM ItemTable WHERE key = 'composer.composerData'")
|
|
347
|
+
row = cur.fetchone()
|
|
348
|
+
if row:
|
|
349
|
+
data = decode_value(row[0])
|
|
350
|
+
if data:
|
|
351
|
+
for c in data.get("allComposers", []):
|
|
352
|
+
if c.get("composerId") == conversation_id:
|
|
353
|
+
composer_meta = c
|
|
354
|
+
break
|
|
355
|
+
conn.close()
|
|
356
|
+
except Exception:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
# ---- Enrich from ai-tracking DB (tldr, overview, model) ----
|
|
360
|
+
conversation_summary = {}
|
|
361
|
+
if tracking_db and os.path.isfile(tracking_db):
|
|
362
|
+
try:
|
|
363
|
+
conn = open_ro(tracking_db)
|
|
364
|
+
cur = conn.cursor()
|
|
365
|
+
cur.execute(
|
|
366
|
+
"SELECT title, tldr, overview, model, mode FROM conversation_summaries WHERE conversationId = ?",
|
|
367
|
+
(conversation_id,)
|
|
368
|
+
)
|
|
369
|
+
row = cur.fetchone()
|
|
370
|
+
if row:
|
|
371
|
+
conversation_summary = {
|
|
372
|
+
"title": row[0], "tldr": row[1], "overview": row[2],
|
|
373
|
+
"model": row[3], "mode": row[4],
|
|
374
|
+
}
|
|
375
|
+
conn.close()
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
# ---- Pull full bubble conversation from global cursorDiskKV ----
|
|
380
|
+
# Structure:
|
|
381
|
+
# composerData:<conv_id> → { fullConversationHeadersOnly: [{bubbleId, type}, ...], ... }
|
|
382
|
+
# bubbleId:<conv_id>:<bubble_id> → { type, text, codeBlocks, fileActions, timingInfo, ... }
|
|
383
|
+
#
|
|
384
|
+
# type == 1 → User message
|
|
385
|
+
# type != 1 → Assistant message
|
|
386
|
+
bubbles = []
|
|
387
|
+
if global_db and os.path.isfile(global_db):
|
|
388
|
+
try:
|
|
389
|
+
conn = open_ro(global_db)
|
|
390
|
+
cur = conn.cursor()
|
|
391
|
+
|
|
392
|
+
# Get ordered bubble headers
|
|
393
|
+
cur.execute(
|
|
394
|
+
"SELECT value FROM cursorDiskKV WHERE key = ?",
|
|
395
|
+
(f"composerData:{conversation_id}",)
|
|
396
|
+
)
|
|
397
|
+
row = cur.fetchone()
|
|
398
|
+
headers = []
|
|
399
|
+
if row:
|
|
400
|
+
data = decode_value(row[0])
|
|
401
|
+
if data:
|
|
402
|
+
headers = data.get("fullConversationHeadersOnly", [])
|
|
403
|
+
|
|
404
|
+
# Fetch each bubble's content
|
|
405
|
+
for header in headers:
|
|
406
|
+
bubble_id = header.get("bubbleId", "")
|
|
407
|
+
bubble_type = header.get("type")
|
|
408
|
+
if not bubble_id:
|
|
409
|
+
continue
|
|
410
|
+
cur.execute(
|
|
411
|
+
"SELECT value FROM cursorDiskKV WHERE key = ?",
|
|
412
|
+
(f"bubbleId:{conversation_id}:{bubble_id}",)
|
|
413
|
+
)
|
|
414
|
+
brow = cur.fetchone()
|
|
415
|
+
if not brow:
|
|
416
|
+
continue
|
|
417
|
+
bdata = decode_value(brow[0])
|
|
418
|
+
if not bdata:
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
role = "user" if bubble_type == 1 else "assistant"
|
|
422
|
+
text = bdata.get("text", "")
|
|
423
|
+
|
|
424
|
+
# Extract code blocks
|
|
425
|
+
code_blocks = []
|
|
426
|
+
for cb in bdata.get("codeBlocks", []):
|
|
427
|
+
lang = cb.get("language", "")
|
|
428
|
+
code = cb.get("code", "")
|
|
429
|
+
if code:
|
|
430
|
+
code_blocks.append({"language": lang, "code": code})
|
|
431
|
+
|
|
432
|
+
# Extract file actions
|
|
433
|
+
file_actions = []
|
|
434
|
+
for fa in bdata.get("fileActions", []):
|
|
435
|
+
fa_type = fa.get("type", "")
|
|
436
|
+
fa_path = fa.get("path", "")
|
|
437
|
+
if fa_path:
|
|
438
|
+
file_actions.append({"type": fa_type, "path": fa_path})
|
|
439
|
+
|
|
440
|
+
# Timing
|
|
441
|
+
timing = bdata.get("timingInfo", {})
|
|
442
|
+
ts_ms = timing.get("clientStartTime") or timing.get("clientEndTime")
|
|
443
|
+
ts_str = ""
|
|
444
|
+
if ts_ms and isinstance(ts_ms, (int, float)):
|
|
445
|
+
ts_str = datetime.fromtimestamp(ts_ms / 1000).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
446
|
+
|
|
447
|
+
bubbles.append({
|
|
448
|
+
"bubble_id": bubble_id,
|
|
449
|
+
"role": role,
|
|
450
|
+
"text": text,
|
|
451
|
+
"code_blocks": code_blocks,
|
|
452
|
+
"file_actions": file_actions,
|
|
453
|
+
"ts": ts_str,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
conn.close()
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
# Index bubbles by role sequence for turn correlation
|
|
461
|
+
user_bubbles = [b for b in bubbles if b["role"] == "user"]
|
|
462
|
+
assistant_bubbles = [b for b in bubbles if b["role"] == "assistant"]
|
|
463
|
+
|
|
464
|
+
# ---- Override metadata from DB ----
|
|
465
|
+
conv_name = (
|
|
466
|
+
composer_meta.get("name")
|
|
467
|
+
or conversation_summary.get("title")
|
|
468
|
+
or ""
|
|
469
|
+
)
|
|
470
|
+
conv_model = (
|
|
471
|
+
conversation_summary.get("model")
|
|
472
|
+
or model
|
|
473
|
+
)
|
|
474
|
+
conv_mode = (
|
|
475
|
+
composer_meta.get("unifiedMode")
|
|
476
|
+
or conversation_summary.get("mode")
|
|
477
|
+
or ""
|
|
478
|
+
)
|
|
479
|
+
conv_created = composer_meta.get("createdAt")
|
|
480
|
+
if conv_created and isinstance(conv_created, (int, float)):
|
|
481
|
+
started_at = datetime.fromtimestamp(conv_created / 1000).strftime("%Y-%m-%d %H:%M:%S")
|
|
482
|
+
context_pct = composer_meta.get("contextUsagePercent")
|
|
483
|
+
lines_added = composer_meta.get("totalLinesAdded", 0)
|
|
484
|
+
lines_removed = composer_meta.get("totalLinesRemoved", 0)
|
|
485
|
+
files_changed = composer_meta.get("filesChangedCount", 0)
|
|
486
|
+
conv_branch = composer_meta.get("createdOnBranch", "")
|
|
487
|
+
|
|
488
|
+
# ---- Accumulate turns from .events.jsonl ----
|
|
489
|
+
turns = []
|
|
490
|
+
|
|
491
|
+
def new_turn(user_text, generation_id=None):
|
|
492
|
+
return {
|
|
493
|
+
"user": user_text,
|
|
494
|
+
"generation_id": generation_id,
|
|
495
|
+
"agent_response": "",
|
|
496
|
+
"agent_thoughts": [],
|
|
497
|
+
"edits": [],
|
|
498
|
+
"reads": [],
|
|
499
|
+
"commands": [],
|
|
500
|
+
"events": [],
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
current = new_turn(None)
|
|
504
|
+
|
|
505
|
+
for line in sys.stdin:
|
|
506
|
+
try:
|
|
507
|
+
entry = json.loads(line)
|
|
508
|
+
except json.JSONDecodeError:
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
ev = entry.get("ev")
|
|
512
|
+
ts = entry.get("ts", "")
|
|
513
|
+
|
|
514
|
+
if ev == "user_text":
|
|
515
|
+
text = entry.get("text", "")
|
|
516
|
+
if not text.strip():
|
|
517
|
+
continue
|
|
518
|
+
if current["user"] or current["events"]:
|
|
519
|
+
turns.append(current)
|
|
520
|
+
current = new_turn(text, entry.get("generation_id"))
|
|
521
|
+
current["events"].append({"kind": "user_prompt", "text": text, "ts": ts})
|
|
522
|
+
|
|
523
|
+
elif ev == "file_edit":
|
|
524
|
+
f = rel(entry.get("file", ""))
|
|
525
|
+
action = entry.get("action", "edit")
|
|
526
|
+
if f and f not in current["edits"]:
|
|
527
|
+
current["edits"].append(f)
|
|
528
|
+
current["events"].append({"kind": "file_edit", "file": f, "action": action, "ts": ts})
|
|
529
|
+
|
|
530
|
+
elif ev == "file_read":
|
|
531
|
+
f = rel(entry.get("file", ""))
|
|
532
|
+
if f and f not in current["reads"]:
|
|
533
|
+
current["reads"].append(f)
|
|
534
|
+
current["events"].append({"kind": "file_read", "file": f, "ts": ts})
|
|
535
|
+
|
|
536
|
+
elif ev == "shell_command":
|
|
537
|
+
cmd = entry.get("command", "")
|
|
538
|
+
if cmd:
|
|
539
|
+
current["commands"].append(cmd)
|
|
540
|
+
current["events"].append({"kind": "shell_command", "command": cmd, "ts": ts})
|
|
541
|
+
|
|
542
|
+
elif ev == "agent_response":
|
|
543
|
+
text = entry.get("text", "")
|
|
544
|
+
if text:
|
|
545
|
+
current["agent_response"] = text
|
|
546
|
+
current["events"].append({"kind": "agent_response", "text": text, "ts": ts})
|
|
547
|
+
|
|
548
|
+
elif ev == "agent_thought":
|
|
549
|
+
text = entry.get("text", "")
|
|
550
|
+
if text:
|
|
551
|
+
current.setdefault("agent_thoughts", []).append(text)
|
|
552
|
+
current["events"].append({"kind": "agent_thought", "text": text, "ts": ts})
|
|
553
|
+
|
|
554
|
+
if current["user"] or current["events"]:
|
|
555
|
+
turns.append(current)
|
|
556
|
+
|
|
557
|
+
# ---- Correlate bubble content into turns ----
|
|
558
|
+
# Match user bubbles to turns by text similarity, then pair with the
|
|
559
|
+
# following assistant bubble(s). Falls back gracefully if DB unavailable.
|
|
560
|
+
def bubble_for_turn(turn_idx):
|
|
561
|
+
"""Return (user_bubble, [assistant_bubbles]) for a turn index."""
|
|
562
|
+
if turn_idx < len(user_bubbles):
|
|
563
|
+
ub = user_bubbles[turn_idx]
|
|
564
|
+
# Collect all assistant bubbles that follow this user bubble
|
|
565
|
+
ub_pos = bubbles.index(ub)
|
|
566
|
+
ab_list = []
|
|
567
|
+
for b in bubbles[ub_pos + 1:]:
|
|
568
|
+
if b["role"] == "assistant":
|
|
569
|
+
ab_list.append(b)
|
|
570
|
+
elif b["role"] == "user":
|
|
571
|
+
break
|
|
572
|
+
return ub, ab_list
|
|
573
|
+
return None, []
|
|
574
|
+
|
|
575
|
+
# ---- Write per-turn files ----
|
|
576
|
+
turn_index = []
|
|
577
|
+
|
|
578
|
+
def group_by_ext(paths):
|
|
579
|
+
groups = {}
|
|
580
|
+
for p in paths:
|
|
581
|
+
ext = os.path.splitext(p)[1] or "(no ext)"
|
|
582
|
+
groups.setdefault(ext, []).append(p)
|
|
583
|
+
return groups
|
|
584
|
+
|
|
585
|
+
for i, turn in enumerate(turns):
|
|
586
|
+
num = i + 1
|
|
587
|
+
padded = f"{num:03d}"
|
|
588
|
+
|
|
589
|
+
user_bubble, asst_bubbles = bubble_for_turn(i)
|
|
590
|
+
|
|
591
|
+
# Prefer bubble text (richer) over hook-captured prompt when available
|
|
592
|
+
user_text = (
|
|
593
|
+
(user_bubble["text"] if user_bubble and user_bubble["text"] else None)
|
|
594
|
+
or turn["user"]
|
|
595
|
+
or ""
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Prefer hook-captured response (afterAgentResponse) — available mid-session.
|
|
599
|
+
# Fall back to DB bubble extraction used at stop time.
|
|
600
|
+
hook_response = turn.get("agent_response", "")
|
|
601
|
+
asst_text = (
|
|
602
|
+
hook_response
|
|
603
|
+
or "\n\n".join(b["text"] for b in asst_bubbles if b["text"])
|
|
604
|
+
)
|
|
605
|
+
asst_code_blocks = [cb for b in asst_bubbles for cb in b["code_blocks"]]
|
|
606
|
+
asst_file_actions = [fa for b in asst_bubbles for fa in b["file_actions"]]
|
|
607
|
+
asst_thoughts = turn.get("agent_thoughts", [])
|
|
608
|
+
|
|
609
|
+
# ---- turn-NNN.txt ----
|
|
610
|
+
md_lines = [f"# Turn {num}", ""]
|
|
611
|
+
|
|
612
|
+
# User block
|
|
613
|
+
if user_text:
|
|
614
|
+
md_lines.append("> **User**")
|
|
615
|
+
md_lines.append(">")
|
|
616
|
+
for line in user_text.splitlines():
|
|
617
|
+
md_lines.append(f"> {line}" if line else ">")
|
|
618
|
+
md_lines.append("")
|
|
619
|
+
|
|
620
|
+
# Tool activity (from hook events)
|
|
621
|
+
tool_lines = []
|
|
622
|
+
for event in turn["events"]:
|
|
623
|
+
kind = event["kind"]
|
|
624
|
+
if kind == "user_prompt":
|
|
625
|
+
pass
|
|
626
|
+
elif kind == "file_edit":
|
|
627
|
+
f = event.get("file", "")
|
|
628
|
+
action = event.get("action", "edit")
|
|
629
|
+
tool_lines.append(f"`{action.title()}` {f}")
|
|
630
|
+
elif kind == "file_read":
|
|
631
|
+
f = event.get("file", "")
|
|
632
|
+
tool_lines.append(f"`Read` {f}")
|
|
633
|
+
elif kind == "shell_command":
|
|
634
|
+
cmd = event.get("command", "")
|
|
635
|
+
if len(cmd) > 120:
|
|
636
|
+
tool_lines.extend(["`Bash`", "```", cmd, "```"])
|
|
637
|
+
else:
|
|
638
|
+
tool_lines.append(f"`Bash` `{cmd}`")
|
|
639
|
+
|
|
640
|
+
if tool_lines:
|
|
641
|
+
md_lines.append("**Tool activity**")
|
|
642
|
+
md_lines.append("")
|
|
643
|
+
md_lines.extend(tool_lines)
|
|
644
|
+
md_lines.append("")
|
|
645
|
+
|
|
646
|
+
# Agent thoughts (reasoning steps, if captured)
|
|
647
|
+
if asst_thoughts:
|
|
648
|
+
md_lines.append("**Thoughts**")
|
|
649
|
+
md_lines.append("")
|
|
650
|
+
for thought in asst_thoughts:
|
|
651
|
+
md_lines.append(f"_{thought}_")
|
|
652
|
+
md_lines.append("")
|
|
653
|
+
|
|
654
|
+
# Assistant response block
|
|
655
|
+
if asst_text:
|
|
656
|
+
md_lines.append("> **Assistant**")
|
|
657
|
+
md_lines.append(">")
|
|
658
|
+
for line in asst_text.splitlines():
|
|
659
|
+
md_lines.append(f"> {line}" if line else ">")
|
|
660
|
+
md_lines.append("")
|
|
661
|
+
|
|
662
|
+
if asst_code_blocks:
|
|
663
|
+
md_lines.append("**Code blocks**")
|
|
664
|
+
md_lines.append("")
|
|
665
|
+
for cb in asst_code_blocks:
|
|
666
|
+
lang = cb.get("language", "")
|
|
667
|
+
md_lines.append(f"```{lang}")
|
|
668
|
+
md_lines.append(cb.get("code", ""))
|
|
669
|
+
md_lines.append("```")
|
|
670
|
+
md_lines.append("")
|
|
671
|
+
|
|
672
|
+
if asst_file_actions:
|
|
673
|
+
md_lines.append("**File actions**")
|
|
674
|
+
md_lines.append("")
|
|
675
|
+
for fa in asst_file_actions:
|
|
676
|
+
md_lines.append(f"- `{fa['type']}` {fa['path']}")
|
|
677
|
+
md_lines.append("")
|
|
678
|
+
|
|
679
|
+
with open(os.path.join(log_dir, f"turn-{padded}.txt"), "w") as fh:
|
|
680
|
+
fh.write("\n".join(md_lines))
|
|
681
|
+
|
|
682
|
+
# ---- turn-NNN.json ----
|
|
683
|
+
turn_json = {
|
|
684
|
+
"turn": num,
|
|
685
|
+
"user": user_text,
|
|
686
|
+
"generation_id": turn["generation_id"],
|
|
687
|
+
"assistant": {
|
|
688
|
+
"text": asst_text,
|
|
689
|
+
"thoughts": asst_thoughts,
|
|
690
|
+
"code_blocks": asst_code_blocks,
|
|
691
|
+
"file_actions": asst_file_actions,
|
|
692
|
+
"source": "hook" if hook_response else "db",
|
|
693
|
+
},
|
|
694
|
+
"events": turn["events"],
|
|
695
|
+
"files_edited": group_by_ext(turn["edits"]),
|
|
696
|
+
"files_read": group_by_ext(turn["reads"]),
|
|
697
|
+
"commands": turn["commands"],
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
with open(os.path.join(log_dir, f"turn-{padded}.json"), "w") as fh:
|
|
701
|
+
json.dump(turn_json, fh, indent=2)
|
|
702
|
+
|
|
703
|
+
user_preview = (user_text or "(no user message)")[:120]
|
|
704
|
+
event_count = len(turn["events"])
|
|
705
|
+
turn_index.append({
|
|
706
|
+
"num": num,
|
|
707
|
+
"padded": padded,
|
|
708
|
+
"user_preview": user_preview,
|
|
709
|
+
"event_count": event_count,
|
|
710
|
+
"edits": turn["edits"],
|
|
711
|
+
"has_response": bool(asst_text),
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
# ---- Write session.txt index ----
|
|
715
|
+
with open(os.path.join(log_dir, "session.txt"), "w") as fh:
|
|
716
|
+
fh.write(f"# Session Log: {os.path.basename(cwd)}\n\n")
|
|
717
|
+
fh.write("| Field | Value |\n")
|
|
718
|
+
fh.write("|-------|-------|\n")
|
|
719
|
+
fh.write(f"| Conversation ID | `{conversation_id}` |\n")
|
|
720
|
+
if conv_name:
|
|
721
|
+
fh.write(f"| Name | {conv_name} |\n")
|
|
722
|
+
fh.write(f"| Started | {started_at} |\n")
|
|
723
|
+
fh.write(f"| Model | {conv_model} |\n")
|
|
724
|
+
if conv_mode:
|
|
725
|
+
fh.write(f"| Mode | {conv_mode} |\n")
|
|
726
|
+
fh.write(f"| Branch | `{conv_branch or branch}` @ `{head_sha}` |\n")
|
|
727
|
+
fh.write(f"| Turns | {len(turn_index)} |\n")
|
|
728
|
+
if files_changed:
|
|
729
|
+
fh.write(f"| Files changed | {files_changed} |\n")
|
|
730
|
+
if lines_added or lines_removed:
|
|
731
|
+
fh.write(f"| Lines | +{lines_added} / -{lines_removed} |\n")
|
|
732
|
+
if context_pct is not None:
|
|
733
|
+
fh.write(f"| Context usage | {context_pct:.1f}% |\n")
|
|
734
|
+
|
|
735
|
+
tldr = conversation_summary.get("tldr", "")
|
|
736
|
+
if tldr:
|
|
737
|
+
fh.write(f"\n> {tldr}\n")
|
|
738
|
+
|
|
739
|
+
fh.write("\n---\n\n")
|
|
740
|
+
fh.write("## Turns\n\n")
|
|
741
|
+
|
|
742
|
+
for t in turn_index:
|
|
743
|
+
edits_str = ", ".join(f"`{e}`" for e in t["edits"][:3])
|
|
744
|
+
if len(t["edits"]) > 3:
|
|
745
|
+
edits_str += f" +{len(t['edits'])-3} more"
|
|
746
|
+
summary = f"{t['event_count']} events"
|
|
747
|
+
if edits_str:
|
|
748
|
+
summary += f" | {edits_str}"
|
|
749
|
+
if t["has_response"]:
|
|
750
|
+
summary += " | response captured"
|
|
751
|
+
fh.write(f"- **[Turn {t['num']}](turn-{t['padded']}.txt)** — {t['user_preview']}\n")
|
|
752
|
+
fh.write(f" _{summary}_\n")
|
|
753
|
+
|
|
754
|
+
fh.write("\n---\n\n")
|
|
755
|
+
|
|
756
|
+
all_reads = []
|
|
757
|
+
all_edits = []
|
|
758
|
+
all_commands = []
|
|
759
|
+
for turn in turns:
|
|
760
|
+
all_reads.extend(turn["reads"])
|
|
761
|
+
all_edits.extend(turn["edits"])
|
|
762
|
+
all_commands.extend(turn["commands"])
|
|
763
|
+
|
|
764
|
+
fh.write("## Exploration\n")
|
|
765
|
+
fh.write("_Files read and commands executed (deduplicated)._\n\n")
|
|
766
|
+
for r in sorted(set(all_reads)):
|
|
767
|
+
fh.write(f"- READ `{r}`\n")
|
|
768
|
+
fh.write("\n")
|
|
769
|
+
|
|
770
|
+
fh.write("## Audit\n")
|
|
771
|
+
fh.write("_Edits, commands, git activity._\n\n")
|
|
772
|
+
for e in sorted(set(all_edits)):
|
|
773
|
+
fh.write(f"- EDIT `{e}`\n")
|
|
774
|
+
for cmd in all_commands:
|
|
775
|
+
short = cmd[:120]
|
|
776
|
+
meaningful = any(kw in short for kw in [
|
|
777
|
+
"pytest", "cargo test", "ruff", "mypy", "npm test",
|
|
778
|
+
"git log", "git diff", "git status", "git add", "git commit",
|
|
779
|
+
"git merge", "caws ", "pip install", "make", "cargo build"
|
|
780
|
+
])
|
|
781
|
+
if meaningful:
|
|
782
|
+
fh.write(f"- BASH `{short}`\n")
|
|
783
|
+
fh.write("\n")
|
|
784
|
+
|
|
785
|
+
fh.write("## Session Snapshot\n\n")
|
|
786
|
+
fh.write("| Field | Value |\n")
|
|
787
|
+
fh.write("|-------|-------|\n")
|
|
788
|
+
fh.write(f"| Branch | `{branch}` @ `{head_sha}` |\n")
|
|
789
|
+
fh.write(f"| Dirty files | {dirty_count} |\n")
|
|
790
|
+
fh.write(f"| Total turns | {len(turn_index)} |\n")
|
|
791
|
+
fh.write(f"| Bubbles fetched | {len(bubbles)} |\n")
|
|
792
|
+
|
|
793
|
+
overview = conversation_summary.get("overview", "")
|
|
794
|
+
if overview:
|
|
795
|
+
fh.write(f"\n## Overview\n\n{overview}\n")
|
|
796
|
+
|
|
797
|
+
PYEOF
|
|
798
|
+
|
|
799
|
+
python3 "$pyscript" "$LOG_DIR" "$CWD" "$CONVERSATION_ID" "$started_at" "$model" \
|
|
800
|
+
"$branch" "$head_sha" "$dirty_count" "$workspace_db" "$tracking_db" "$global_db" \
|
|
801
|
+
< "$EVENTS_FILE"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# ============================================================
|
|
805
|
+
# Metadata capture (first invocation creates meta)
|
|
806
|
+
# ============================================================
|
|
807
|
+
ensure_meta() {
|
|
808
|
+
if [ ! -f "$META_FILE" ]; then
|
|
809
|
+
local full_time
|
|
810
|
+
full_time=$(date +"%Y-%m-%d %H:%M:%S %Z")
|
|
811
|
+
jq -cn \
|
|
812
|
+
--arg cid "$CONVERSATION_ID" \
|
|
813
|
+
--arg ts "$TIMESTAMP" \
|
|
814
|
+
--arg lt "$full_time" \
|
|
815
|
+
--arg model "cursor-agent" \
|
|
816
|
+
--arg project "$(basename "$CWD")" \
|
|
817
|
+
'{conversation_id: $cid, started_at: $ts, local_time: $lt, model: $model, project: $project}' \
|
|
818
|
+
> "$META_FILE"
|
|
819
|
+
fi
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
# ============================================================
|
|
823
|
+
# Agent registry heartbeat — register this agent with CAWS
|
|
824
|
+
# ============================================================
|
|
825
|
+
AGENTS_REGISTRY="${CWD}/.caws/agents.json"
|
|
826
|
+
|
|
827
|
+
heartbeat_agent() {
|
|
828
|
+
[ "$CONVERSATION_ID" = "unknown" ] && return
|
|
829
|
+
|
|
830
|
+
mkdir -p "$(dirname "$AGENTS_REGISTRY")"
|
|
831
|
+
|
|
832
|
+
# Read existing registry or start fresh
|
|
833
|
+
local registry
|
|
834
|
+
if [ -f "$AGENTS_REGISTRY" ]; then
|
|
835
|
+
registry=$(cat "$AGENTS_REGISTRY" 2>/dev/null || echo '{"version":1,"agents":{}}')
|
|
836
|
+
else
|
|
837
|
+
registry='{"version":1,"agents":{}}'
|
|
838
|
+
fi
|
|
839
|
+
|
|
840
|
+
# Prune stale entries (older than 30 minutes) and upsert this agent
|
|
841
|
+
registry=$(echo "$registry" | python3 -c "
|
|
842
|
+
import json, sys
|
|
843
|
+
from datetime import datetime, timedelta, timezone
|
|
844
|
+
|
|
845
|
+
TTL = timedelta(minutes=30)
|
|
846
|
+
now = datetime.now(timezone.utc)
|
|
847
|
+
conv_id = '$CONVERSATION_ID'
|
|
848
|
+
|
|
849
|
+
data = json.load(sys.stdin)
|
|
850
|
+
agents = data.get('agents', {})
|
|
851
|
+
|
|
852
|
+
# Prune stale
|
|
853
|
+
pruned = {}
|
|
854
|
+
for sid, entry in agents.items():
|
|
855
|
+
try:
|
|
856
|
+
last = datetime.fromisoformat(entry['lastSeen'].replace('Z', '+00:00'))
|
|
857
|
+
if now - last < TTL:
|
|
858
|
+
pruned[sid] = entry
|
|
859
|
+
except (KeyError, ValueError):
|
|
860
|
+
pass
|
|
861
|
+
|
|
862
|
+
# Upsert current agent
|
|
863
|
+
existing = pruned.get(conv_id, {})
|
|
864
|
+
pruned[conv_id] = {
|
|
865
|
+
'sessionId': conv_id,
|
|
866
|
+
'platform': 'cursor',
|
|
867
|
+
'model': existing.get('model'),
|
|
868
|
+
'specId': existing.get('specId'),
|
|
869
|
+
'ttl': 1800000,
|
|
870
|
+
'firstSeen': existing.get('firstSeen', now.strftime('%Y-%m-%dT%H:%M:%SZ')),
|
|
871
|
+
'lastSeen': now.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
data['agents'] = pruned
|
|
875
|
+
json.dump(data, sys.stdout, indent=2)
|
|
876
|
+
" 2>/dev/null)
|
|
877
|
+
|
|
878
|
+
[ -n "$registry" ] && echo "$registry" > "$AGENTS_REGISTRY"
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
remove_agent() {
|
|
882
|
+
[ "$CONVERSATION_ID" = "unknown" ] && return
|
|
883
|
+
[ ! -f "$AGENTS_REGISTRY" ] && return
|
|
884
|
+
|
|
885
|
+
# Remove this agent from registry
|
|
886
|
+
python3 -c "
|
|
887
|
+
import json, sys
|
|
888
|
+
|
|
889
|
+
conv_id = '$CONVERSATION_ID'
|
|
890
|
+
with open('$AGENTS_REGISTRY', 'r') as f:
|
|
891
|
+
data = json.load(f)
|
|
892
|
+
|
|
893
|
+
agents = data.get('agents', {})
|
|
894
|
+
agents.pop(conv_id, None)
|
|
895
|
+
data['agents'] = agents
|
|
896
|
+
|
|
897
|
+
with open('$AGENTS_REGISTRY', 'w') as f:
|
|
898
|
+
json.dump(data, f, indent=2)
|
|
899
|
+
" 2>/dev/null || true
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
# ============================================================
|
|
903
|
+
# DISPATCH
|
|
904
|
+
# ============================================================
|
|
905
|
+
ensure_meta
|
|
906
|
+
|
|
907
|
+
case "$HOOK_EVENT" in
|
|
908
|
+
beforeSubmitPrompt) handle_before_submit_prompt ;;
|
|
909
|
+
afterFileEdit) handle_after_file_edit ;;
|
|
910
|
+
beforeShellExecution) handle_before_shell_execution ;;
|
|
911
|
+
beforeReadFile) handle_before_read_file ;;
|
|
912
|
+
afterAgentResponse) handle_after_agent_response ;;
|
|
913
|
+
afterAgentThought) handle_after_agent_thought ;;
|
|
914
|
+
stop) handle_stop; remove_agent ;;
|
|
915
|
+
*) ;;
|
|
916
|
+
esac
|
|
917
|
+
|
|
918
|
+
# Heartbeat on every event (keeps TTL fresh while agent is active)
|
|
919
|
+
heartbeat_agent
|
|
920
|
+
|
|
921
|
+
# Always allow — this is observation only
|
|
922
|
+
echo '{"permission":"allow"}' 2>/dev/null || true
|
|
923
|
+
exit 0
|
|
924
|
+
|