@getpawl/setup 1.0.0 → 1.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 +30 -0
- package/dist/index.js +762 -0
- package/dist/parse-cc-session.js +214 -0
- package/package.json +31 -8
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @fixhbone/agentmap-setup
|
|
2
|
+
|
|
3
|
+
One-shot setup for [AgentMap](https://github.com/0xfishbone/agentMap) + Claude Code hooks. Connects any repo to your AgentMap project so spec context is automatically synced during Claude Code sessions.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @fixhbone/agentmap-setup <PROJECT_KEY>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Get your `<PROJECT_KEY>` from the AgentMap web dashboard under **Settings > Setup**.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
1. Creates `.agentmap/.env` with API credentials
|
|
16
|
+
2. Generates `.agentmap/sync.sh` — pull context + push diffs
|
|
17
|
+
3. Configures `.claude/settings.json` with three lifecycle hooks:
|
|
18
|
+
- **SessionStart** — pulls latest project context
|
|
19
|
+
- **PostToolUse** (Write/Edit) — tracks modified files
|
|
20
|
+
- **Stop** — pushes session diff to AgentMap
|
|
21
|
+
4. Adds sensitive paths to `.gitignore`
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Node.js >= 18
|
|
26
|
+
- [Claude Code](https://claude.ai/claude-code) CLI installed
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var import_node_fs = require("fs");
|
|
6
|
+
var import_node_path = require("path");
|
|
7
|
+
function main() {
|
|
8
|
+
const arg = process.argv[2];
|
|
9
|
+
if (!arg) {
|
|
10
|
+
printUsage();
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
if (arg === "init") {
|
|
14
|
+
const key = process.argv[3];
|
|
15
|
+
pawlInit(key);
|
|
16
|
+
} else {
|
|
17
|
+
legacySetup(arg);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function printUsage() {
|
|
21
|
+
console.log(`
|
|
22
|
+
Usage:
|
|
23
|
+
pawl init [PROJECT_KEY] Initialize Pawl in this repo
|
|
24
|
+
pawl-setup <PROJECT_KEY> Legacy setup (still supported)
|
|
25
|
+
|
|
26
|
+
Get your project key at: https://pawl.dev/settings
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
function legacySetup(encoded) {
|
|
30
|
+
let config;
|
|
31
|
+
try {
|
|
32
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
33
|
+
config = JSON.parse(decoded);
|
|
34
|
+
} catch {
|
|
35
|
+
console.error("Error: Invalid project key \u2014 could not decode.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!config.apiKey || !config.projectId || !config.apiUrl) {
|
|
39
|
+
console.error(
|
|
40
|
+
"Error: Project key is missing required fields (apiKey, projectId, apiUrl)."
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
writeEnvFile(cwd, config);
|
|
46
|
+
writeSyncScript(cwd, config);
|
|
47
|
+
copyParserScript(cwd);
|
|
48
|
+
mergeClaudeSettings(cwd);
|
|
49
|
+
writeClaudeMd(cwd);
|
|
50
|
+
updateGitignore(cwd);
|
|
51
|
+
console.log("\n Pawl setup complete!\n");
|
|
52
|
+
console.log(` Project: ${config.apiUrl}/projects/${config.projectId}`);
|
|
53
|
+
console.log(" Created: .agentmap/.env, .agentmap/sync.sh, .agentmap/parse-cc-session.js");
|
|
54
|
+
console.log(" Updated: .claude/settings.json, CLAUDE.md, .gitignore\n");
|
|
55
|
+
}
|
|
56
|
+
function pawlInit(key) {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const detected = detectAgents(cwd);
|
|
59
|
+
migrateIfNeeded(cwd, detected.hasAgentMapDir);
|
|
60
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(cwd, ".pawl"), { recursive: true });
|
|
61
|
+
if (key) {
|
|
62
|
+
let config;
|
|
63
|
+
try {
|
|
64
|
+
const decoded = Buffer.from(key, "base64").toString("utf-8");
|
|
65
|
+
config = JSON.parse(decoded);
|
|
66
|
+
} catch {
|
|
67
|
+
console.error("Error: Invalid project key \u2014 could not decode.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
writePawlEnvFile(cwd, config);
|
|
71
|
+
}
|
|
72
|
+
writePawlSyncScript(cwd);
|
|
73
|
+
copyParserScript(cwd, ".pawl");
|
|
74
|
+
if (detected.hasCC) {
|
|
75
|
+
mergeClaudeSettings(cwd, ".pawl");
|
|
76
|
+
}
|
|
77
|
+
if (detected.hasGemini) {
|
|
78
|
+
mergeGeminiSettings(cwd);
|
|
79
|
+
}
|
|
80
|
+
writeGitHook(cwd, detected.hasGitDir);
|
|
81
|
+
writePawlClaudeMd(cwd);
|
|
82
|
+
writeAgentsMd(cwd);
|
|
83
|
+
updateGitignore(cwd);
|
|
84
|
+
printSummary(detected, !!key);
|
|
85
|
+
}
|
|
86
|
+
function detectAgents(cwd) {
|
|
87
|
+
return {
|
|
88
|
+
hasCC: (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".claude")),
|
|
89
|
+
hasGemini: (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".gemini")),
|
|
90
|
+
hasAgentMapDir: (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".agentmap")),
|
|
91
|
+
hasGitDir: (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".git"))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function migrateIfNeeded(cwd, hasAgentMapDir) {
|
|
95
|
+
if (!hasAgentMapDir) return;
|
|
96
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".pawl"))) return;
|
|
97
|
+
console.log(" Migrating .agentmap/ \u2192 .pawl/...");
|
|
98
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.join)(cwd, ".pawl"), { recursive: true });
|
|
99
|
+
const filesToCopy = [".env", "sync.sh", "parse-cc-session.js", "context.md", "progress.md"];
|
|
100
|
+
for (const file of filesToCopy) {
|
|
101
|
+
const src = (0, import_node_path.join)(cwd, ".agentmap", file);
|
|
102
|
+
if ((0, import_node_fs.existsSync)(src)) {
|
|
103
|
+
(0, import_node_fs.copyFileSync)(src, (0, import_node_path.join)(cwd, ".pawl", file));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const specsDir = (0, import_node_path.join)(cwd, ".agentmap", "specs");
|
|
107
|
+
if ((0, import_node_fs.existsSync)(specsDir)) {
|
|
108
|
+
(0, import_node_fs.cpSync)(specsDir, (0, import_node_path.join)(cwd, ".pawl", "specs"), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
const envPath = (0, import_node_path.join)(cwd, ".pawl", ".env");
|
|
111
|
+
if ((0, import_node_fs.existsSync)(envPath)) {
|
|
112
|
+
let envContent = (0, import_node_fs.readFileSync)(envPath, "utf-8");
|
|
113
|
+
envContent = envContent.replace(/AGENTMAP_/g, "PAWL_");
|
|
114
|
+
(0, import_node_fs.writeFileSync)(envPath, envContent, "utf-8");
|
|
115
|
+
}
|
|
116
|
+
console.log(" .agentmap/ preserved \u2014 safe to delete manually when ready.\n");
|
|
117
|
+
}
|
|
118
|
+
function writePawlEnvFile(cwd, config) {
|
|
119
|
+
const content = [
|
|
120
|
+
`PAWL_API_KEY=${config.apiKey}`,
|
|
121
|
+
`PAWL_PROJECT_ID=${config.projectId}`,
|
|
122
|
+
`PAWL_API_URL=${config.apiUrl}`,
|
|
123
|
+
""
|
|
124
|
+
].join("\n");
|
|
125
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(cwd, ".pawl", ".env"), content, "utf-8");
|
|
126
|
+
}
|
|
127
|
+
function writePawlSyncScript(cwd) {
|
|
128
|
+
const script = `#!/usr/bin/env bash
|
|
129
|
+
set -euo pipefail
|
|
130
|
+
|
|
131
|
+
# Pawl sync script \u2014 generated by pawl init
|
|
132
|
+
# Do not edit manually; re-run pawl init to update.
|
|
133
|
+
|
|
134
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
135
|
+
|
|
136
|
+
# Source env only if it exists (pawl init without key skips .env)
|
|
137
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
138
|
+
source "$SCRIPT_DIR/.env"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
BASE_URL="\${PAWL_API_URL:-}/api/projects/\${PAWL_PROJECT_ID:-}"
|
|
142
|
+
|
|
143
|
+
# \u2500\u2500 pull mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
144
|
+
pull() {
|
|
145
|
+
if [ -z "\${PAWL_API_KEY:-}" ]; then
|
|
146
|
+
echo "Warning: PAWL_API_KEY not set \u2014 sync disabled" >&2
|
|
147
|
+
if [ -f "$SCRIPT_DIR/context.md" ]; then
|
|
148
|
+
cat "$SCRIPT_DIR/context.md"
|
|
149
|
+
fi
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Recovery: if tracked-files exist from a previous incomplete session, push first
|
|
154
|
+
if [ -f "$SCRIPT_DIR/.tracked-files" ]; then
|
|
155
|
+
push >/dev/null 2>&1
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Ensure specs directory exists
|
|
159
|
+
mkdir -p "$SCRIPT_DIR/specs"
|
|
160
|
+
|
|
161
|
+
# \u2500\u2500 Level 1: New file-based endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
162
|
+
RESPONSE=$(curl -sf -X GET "$BASE_URL/context/files" \\
|
|
163
|
+
-H "Authorization: Bearer $PAWL_API_KEY" \\
|
|
164
|
+
-H "Accept: application/json" 2>/dev/null) || true
|
|
165
|
+
|
|
166
|
+
if [ -n "$RESPONSE" ] && echo "$RESPONSE" | jq -e '.files' >/dev/null 2>&1; then
|
|
167
|
+
echo "$RESPONSE" | jq -r '.files[] | @base64' | while read -r encoded; do
|
|
168
|
+
FILE_PATH=$(echo "$encoded" | base64 -d | jq -r '.path')
|
|
169
|
+
FILE_CONTENT=$(echo "$encoded" | base64 -d | jq -r '.content')
|
|
170
|
+
|
|
171
|
+
# Ensure parent directory exists
|
|
172
|
+
mkdir -p "$SCRIPT_DIR/$(dirname "$FILE_PATH")"
|
|
173
|
+
echo "$FILE_CONTENT" > "$SCRIPT_DIR/$FILE_PATH"
|
|
174
|
+
done
|
|
175
|
+
|
|
176
|
+
# Output context.md to stdout (for SessionStart hook)
|
|
177
|
+
if [ -f "$SCRIPT_DIR/context.md" ]; then
|
|
178
|
+
cat "$SCRIPT_DIR/context.md"
|
|
179
|
+
fi
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# \u2500\u2500 Level 2: Legacy monolithic endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
184
|
+
RESPONSE=$(curl -sf -X GET "$BASE_URL/context" \\
|
|
185
|
+
-H "Authorization: Bearer $PAWL_API_KEY" \\
|
|
186
|
+
-H "Accept: application/json" 2>/dev/null) || true
|
|
187
|
+
|
|
188
|
+
if [ -n "$RESPONSE" ] && echo "$RESPONSE" | jq -e '.formatted_context' >/dev/null 2>&1; then
|
|
189
|
+
echo "$RESPONSE" | jq -r '.formatted_context // empty' > "$SCRIPT_DIR/context.md"
|
|
190
|
+
cat "$SCRIPT_DIR/context.md"
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# \u2500\u2500 Level 3: Cached file fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
195
|
+
if [ -f "$SCRIPT_DIR/context.md" ]; then
|
|
196
|
+
echo "# (cached \u2014 API unreachable)" | cat - "$SCRIPT_DIR/context.md"
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo "Warning: Pawl context unavailable" >&2
|
|
201
|
+
return 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# \u2500\u2500 push mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
205
|
+
push() {
|
|
206
|
+
if [ -z "\${PAWL_API_KEY:-}" ]; then
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
LAST_SHA=""
|
|
211
|
+
if [ -f "$SCRIPT_DIR/.last-sync-sha" ]; then
|
|
212
|
+
LAST_SHA=$(cat "$SCRIPT_DIR/.last-sync-sha")
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
CURRENT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
216
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
217
|
+
|
|
218
|
+
# Compute diff
|
|
219
|
+
if [ -n "$LAST_SHA" ]; then
|
|
220
|
+
DIFF=$(git diff "$LAST_SHA"..HEAD 2>/dev/null || echo "")
|
|
221
|
+
else
|
|
222
|
+
DIFF=$(git diff HEAD~1..HEAD 2>/dev/null || echo "")
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Collect tracked files (dedup)
|
|
226
|
+
FILES_CHANGED="[]"
|
|
227
|
+
if [ -f "$SCRIPT_DIR/.tracked-files" ]; then
|
|
228
|
+
FILES_CHANGED=$(sort -u "$SCRIPT_DIR/.tracked-files" | jq -R . | jq -s .)
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# Commit info
|
|
232
|
+
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "")
|
|
233
|
+
|
|
234
|
+
# Session ID from Claude Code (if available)
|
|
235
|
+
SESSION_ID="\${CLAUDE_SESSION_ID:-}"
|
|
236
|
+
|
|
237
|
+
# Repo root for CC session lookup
|
|
238
|
+
REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
239
|
+
|
|
240
|
+
# Parse CC session data locally (if node + parser available)
|
|
241
|
+
CC_SESSION="{}"
|
|
242
|
+
if command -v node &> /dev/null; then
|
|
243
|
+
PARSER="$SCRIPT_DIR/parse-cc-session.js"
|
|
244
|
+
if [ -f "$PARSER" ]; then
|
|
245
|
+
CC_SESSION=$(node "$PARSER" "$REPO_PATH" 2>/dev/null || echo "{}")
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Extract cc_tasks from parser output
|
|
250
|
+
CC_TASKS=$(echo "$CC_SESSION" | jq -c '.tasks // []')
|
|
251
|
+
|
|
252
|
+
PAYLOAD=$(jq -n \\
|
|
253
|
+
--arg diff "$DIFF" \\
|
|
254
|
+
--arg branch "$BRANCH" \\
|
|
255
|
+
--arg commit_sha "$CURRENT_SHA" \\
|
|
256
|
+
--arg commit_message "$COMMIT_MSG" \\
|
|
257
|
+
--arg session_id "$SESSION_ID" \\
|
|
258
|
+
--arg last_sync_sha "$LAST_SHA" \\
|
|
259
|
+
--arg repo_path "$REPO_PATH" \\
|
|
260
|
+
--argjson files_changed "$FILES_CHANGED" \\
|
|
261
|
+
--argjson cc_session "$CC_SESSION" \\
|
|
262
|
+
--argjson cc_tasks "$CC_TASKS" \\
|
|
263
|
+
'{
|
|
264
|
+
diff: $diff,
|
|
265
|
+
branch: $branch,
|
|
266
|
+
commit_sha: $commit_sha,
|
|
267
|
+
commit_message: $commit_message,
|
|
268
|
+
session_id: $session_id,
|
|
269
|
+
last_sync_sha: $last_sync_sha,
|
|
270
|
+
repo_path: $repo_path,
|
|
271
|
+
files_changed: $files_changed,
|
|
272
|
+
cc_session: $cc_session,
|
|
273
|
+
cc_tasks: $cc_tasks
|
|
274
|
+
}')
|
|
275
|
+
|
|
276
|
+
RESPONSE=$(curl -sf -X POST "$BASE_URL/sync" \\
|
|
277
|
+
-H "Authorization: Bearer $PAWL_API_KEY" \\
|
|
278
|
+
-H "Content-Type: application/json" \\
|
|
279
|
+
-d "$PAYLOAD")
|
|
280
|
+
|
|
281
|
+
echo "$RESPONSE" | jq .
|
|
282
|
+
|
|
283
|
+
# Update last sync SHA
|
|
284
|
+
echo "$CURRENT_SHA" > "$SCRIPT_DIR/.last-sync-sha"
|
|
285
|
+
|
|
286
|
+
# Cleanup tracked files
|
|
287
|
+
rm -f "$SCRIPT_DIR/.tracked-files"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# \u2500\u2500 dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
291
|
+
case "\${1:-}" in
|
|
292
|
+
pull) pull ;;
|
|
293
|
+
push) push ;;
|
|
294
|
+
*)
|
|
295
|
+
echo "Usage: sync.sh [pull|push]"
|
|
296
|
+
exit 1
|
|
297
|
+
;;
|
|
298
|
+
esac
|
|
299
|
+
`;
|
|
300
|
+
const scriptPath = (0, import_node_path.join)(cwd, ".pawl", "sync.sh");
|
|
301
|
+
(0, import_node_fs.writeFileSync)(scriptPath, script, "utf-8");
|
|
302
|
+
(0, import_node_fs.chmodSync)(scriptPath, 493);
|
|
303
|
+
}
|
|
304
|
+
function mergeGeminiSettings(cwd) {
|
|
305
|
+
const geminiDir = (0, import_node_path.join)(cwd, ".gemini");
|
|
306
|
+
(0, import_node_fs.mkdirSync)(geminiDir, { recursive: true });
|
|
307
|
+
const settingsPath = (0, import_node_path.join)(geminiDir, "settings.json");
|
|
308
|
+
let settings = {};
|
|
309
|
+
if ((0, import_node_fs.existsSync)(settingsPath)) {
|
|
310
|
+
try {
|
|
311
|
+
settings = JSON.parse((0, import_node_fs.readFileSync)(settingsPath, "utf-8"));
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const hooksToInstall = {
|
|
316
|
+
SessionStart: { command: ".pawl/sync.sh pull" },
|
|
317
|
+
AfterAgentLoop: { command: ".pawl/sync.sh push" }
|
|
318
|
+
};
|
|
319
|
+
const hooksObj = settings.hooks || {};
|
|
320
|
+
for (const [event, entry] of Object.entries(hooksToInstall)) {
|
|
321
|
+
const existing = hooksObj[event] || [];
|
|
322
|
+
const alreadyExists = existing.some((e) => e.command === entry.command);
|
|
323
|
+
if (!alreadyExists) {
|
|
324
|
+
existing.push(entry);
|
|
325
|
+
}
|
|
326
|
+
hooksObj[event] = existing;
|
|
327
|
+
}
|
|
328
|
+
settings.hooks = hooksObj;
|
|
329
|
+
(0, import_node_fs.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
330
|
+
}
|
|
331
|
+
function writeGitHook(cwd, hasGitDir) {
|
|
332
|
+
if (!hasGitDir) return;
|
|
333
|
+
const hooksDir = (0, import_node_path.join)(cwd, ".git", "hooks");
|
|
334
|
+
(0, import_node_fs.mkdirSync)(hooksDir, { recursive: true });
|
|
335
|
+
const hookPath = (0, import_node_path.join)(hooksDir, "post-commit");
|
|
336
|
+
const hookBlock = `#!/bin/sh
|
|
337
|
+
# Pawl sync hook \u2014 fires after every commit
|
|
338
|
+
# Provides universal sync fallback for agents without lifecycle hooks (Codex, etc.)
|
|
339
|
+
if [ -f ".pawl/sync.sh" ]; then
|
|
340
|
+
.pawl/sync.sh push
|
|
341
|
+
fi`;
|
|
342
|
+
if (!(0, import_node_fs.existsSync)(hookPath)) {
|
|
343
|
+
(0, import_node_fs.writeFileSync)(hookPath, hookBlock + "\n", "utf-8");
|
|
344
|
+
(0, import_node_fs.chmodSync)(hookPath, 493);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const content = (0, import_node_fs.readFileSync)(hookPath, "utf-8");
|
|
348
|
+
if (content.includes("Pawl sync hook")) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
(0, import_node_fs.writeFileSync)(hookPath, content.trimEnd() + "\n\n" + hookBlock + "\n", "utf-8");
|
|
352
|
+
(0, import_node_fs.chmodSync)(hookPath, 493);
|
|
353
|
+
}
|
|
354
|
+
function writePawlClaudeMd(cwd) {
|
|
355
|
+
const claudeMdPath = (0, import_node_path.join)(cwd, "CLAUDE.md");
|
|
356
|
+
const PAWL_START = "<!-- pawl:start -->";
|
|
357
|
+
const PAWL_END = "<!-- pawl:end -->";
|
|
358
|
+
const AGENTMAP_START = "<!-- agentmap:start -->";
|
|
359
|
+
const AGENTMAP_END = "<!-- agentmap:end -->";
|
|
360
|
+
const block = [
|
|
361
|
+
PAWL_START,
|
|
362
|
+
"# Pawl Context",
|
|
363
|
+
"",
|
|
364
|
+
"This project uses Pawl for spec management and AI session tracking.",
|
|
365
|
+
"",
|
|
366
|
+
"Context files are in `.pawl/`:",
|
|
367
|
+
"- `.pawl/context.md` \u2014 project index, spec map, health",
|
|
368
|
+
"- `.pawl/specs/` \u2014 individual spec files (one per feature area)",
|
|
369
|
+
"- `.pawl/progress.md` \u2014 last session summary",
|
|
370
|
+
"",
|
|
371
|
+
"Read `.pawl/context.md` at session start, then load relevant specs from `.pawl/specs/` based on the task.",
|
|
372
|
+
PAWL_END
|
|
373
|
+
].join("\n");
|
|
374
|
+
if (!(0, import_node_fs.existsSync)(claudeMdPath)) {
|
|
375
|
+
(0, import_node_fs.writeFileSync)(claudeMdPath, block + "\n", "utf-8");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
let content = (0, import_node_fs.readFileSync)(claudeMdPath, "utf-8");
|
|
379
|
+
let startIdx = content.indexOf(PAWL_START);
|
|
380
|
+
let endIdx = content.indexOf(PAWL_END);
|
|
381
|
+
let endMarkerLen = PAWL_END.length;
|
|
382
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
383
|
+
startIdx = content.indexOf(AGENTMAP_START);
|
|
384
|
+
endIdx = content.indexOf(AGENTMAP_END);
|
|
385
|
+
endMarkerLen = AGENTMAP_END.length;
|
|
386
|
+
}
|
|
387
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
388
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + endMarkerLen);
|
|
389
|
+
} else {
|
|
390
|
+
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
391
|
+
}
|
|
392
|
+
(0, import_node_fs.writeFileSync)(claudeMdPath, content, "utf-8");
|
|
393
|
+
}
|
|
394
|
+
function writeAgentsMd(cwd) {
|
|
395
|
+
const agentsMdPath = (0, import_node_path.join)(cwd, "AGENTS.md");
|
|
396
|
+
const START_MARKER = "<!-- pawl:start -->";
|
|
397
|
+
const END_MARKER = "<!-- pawl:end -->";
|
|
398
|
+
const block = [
|
|
399
|
+
START_MARKER,
|
|
400
|
+
"## Pawl Project Context",
|
|
401
|
+
"",
|
|
402
|
+
"This project uses Pawl for spec management and AI session tracking.",
|
|
403
|
+
"Structured context files are available at:",
|
|
404
|
+
"",
|
|
405
|
+
"- `.pawl/context.md` \u2014 project index, spec map, health overview",
|
|
406
|
+
"- `.pawl/specs/` \u2014 individual spec files (one per feature area)",
|
|
407
|
+
"- `.pawl/progress.md` \u2014 last session summary, recent decisions",
|
|
408
|
+
"",
|
|
409
|
+
"**Start here**: Read `.pawl/context.md` first, then load relevant",
|
|
410
|
+
"spec files from `.pawl/specs/` based on the task at hand.",
|
|
411
|
+
END_MARKER
|
|
412
|
+
].join("\n");
|
|
413
|
+
if (!(0, import_node_fs.existsSync)(agentsMdPath)) {
|
|
414
|
+
(0, import_node_fs.writeFileSync)(agentsMdPath, block + "\n", "utf-8");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
let content = (0, import_node_fs.readFileSync)(agentsMdPath, "utf-8");
|
|
418
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
419
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
420
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
421
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
|
|
422
|
+
} else {
|
|
423
|
+
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
424
|
+
}
|
|
425
|
+
(0, import_node_fs.writeFileSync)(agentsMdPath, content, "utf-8");
|
|
426
|
+
}
|
|
427
|
+
function printSummary(detected, hasKey) {
|
|
428
|
+
console.log("\n Pawl initialized.\n");
|
|
429
|
+
console.log(" Detected agents:");
|
|
430
|
+
if (detected.hasCC) {
|
|
431
|
+
console.log(" Claude Code \u2713 hooks installed (.claude/settings.json)");
|
|
432
|
+
}
|
|
433
|
+
if (detected.hasGemini) {
|
|
434
|
+
console.log(" Gemini CLI \u2713 hooks installed (.gemini/settings.json)");
|
|
435
|
+
}
|
|
436
|
+
console.log(" Codex / other no lifecycle hooks \u2014 git post-commit fallback active");
|
|
437
|
+
console.log("\n Files written:");
|
|
438
|
+
console.log(" .pawl/sync.sh");
|
|
439
|
+
console.log(" .pawl/parse-cc-session.js");
|
|
440
|
+
console.log(" CLAUDE.md (pawl context block updated)");
|
|
441
|
+
console.log(" AGENTS.md (pawl context block \u2014 readable by Codex, Kiro, and others)");
|
|
442
|
+
if (detected.hasGitDir) {
|
|
443
|
+
console.log(" .git/hooks/post-commit (universal sync fallback)");
|
|
444
|
+
}
|
|
445
|
+
console.log(" .gitignore (updated)");
|
|
446
|
+
if (detected.hasGitDir) {
|
|
447
|
+
console.log("\n Note: .git/hooks/post-commit is per-machine \u2014 not committed to the repo.");
|
|
448
|
+
console.log(" Each team member should run: pawl init");
|
|
449
|
+
} else {
|
|
450
|
+
console.log("\n Note: git repo not detected \u2014 post-commit hook skipped.");
|
|
451
|
+
}
|
|
452
|
+
if (!hasKey) {
|
|
453
|
+
console.log("\n Sync is not yet connected to the Pawl dashboard.");
|
|
454
|
+
console.log(" To enable: pawl init <YOUR_PROJECT_KEY>");
|
|
455
|
+
console.log(" Get your key at: https://pawl.dev/settings");
|
|
456
|
+
} else {
|
|
457
|
+
console.log("\n .pawl/.env written \u2014 sync is connected.");
|
|
458
|
+
}
|
|
459
|
+
console.log("");
|
|
460
|
+
}
|
|
461
|
+
function writeEnvFile(cwd, config) {
|
|
462
|
+
const dir = (0, import_node_path.join)(cwd, ".agentmap");
|
|
463
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
464
|
+
const content = [
|
|
465
|
+
`AGENTMAP_API_KEY=${config.apiKey}`,
|
|
466
|
+
`AGENTMAP_PROJECT_ID=${config.projectId}`,
|
|
467
|
+
`AGENTMAP_API_URL=${config.apiUrl}`,
|
|
468
|
+
""
|
|
469
|
+
].join("\n");
|
|
470
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, ".env"), content, "utf-8");
|
|
471
|
+
}
|
|
472
|
+
function writeSyncScript(cwd, config) {
|
|
473
|
+
const dir = (0, import_node_path.join)(cwd, ".agentmap");
|
|
474
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
475
|
+
const script = `#!/usr/bin/env bash
|
|
476
|
+
set -euo pipefail
|
|
477
|
+
|
|
478
|
+
# Pawl sync script \u2014 generated by pawl-setup
|
|
479
|
+
# Do not edit manually; re-run npx @getpawl/setup to update.
|
|
480
|
+
|
|
481
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
482
|
+
source "$SCRIPT_DIR/.env"
|
|
483
|
+
|
|
484
|
+
BASE_URL="$AGENTMAP_API_URL/api/projects/$AGENTMAP_PROJECT_ID"
|
|
485
|
+
|
|
486
|
+
# \u2500\u2500 pull mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
487
|
+
pull() {
|
|
488
|
+
# Recovery: if tracked-files exist from a previous incomplete session, push first
|
|
489
|
+
if [ -f "$SCRIPT_DIR/.tracked-files" ]; then
|
|
490
|
+
push >/dev/null 2>&1
|
|
491
|
+
fi
|
|
492
|
+
|
|
493
|
+
# Ensure specs directory exists
|
|
494
|
+
mkdir -p "$SCRIPT_DIR/specs"
|
|
495
|
+
|
|
496
|
+
# \u2500\u2500 Level 1: New file-based endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
497
|
+
RESPONSE=$(curl -sf -X GET "$BASE_URL/context/files" \\
|
|
498
|
+
-H "Authorization: Bearer $AGENTMAP_API_KEY" \\
|
|
499
|
+
-H "Accept: application/json" 2>/dev/null) || true
|
|
500
|
+
|
|
501
|
+
if [ -n "$RESPONSE" ] && echo "$RESPONSE" | jq -e '.files' >/dev/null 2>&1; then
|
|
502
|
+
echo "$RESPONSE" | jq -r '.files[] | @base64' | while read -r encoded; do
|
|
503
|
+
FILE_PATH=$(echo "$encoded" | base64 -d | jq -r '.path')
|
|
504
|
+
FILE_CONTENT=$(echo "$encoded" | base64 -d | jq -r '.content')
|
|
505
|
+
|
|
506
|
+
# Ensure parent directory exists
|
|
507
|
+
mkdir -p "$SCRIPT_DIR/$(dirname "$FILE_PATH")"
|
|
508
|
+
echo "$FILE_CONTENT" > "$SCRIPT_DIR/$FILE_PATH"
|
|
509
|
+
done
|
|
510
|
+
|
|
511
|
+
# Output context.md to stdout (for SessionStart hook)
|
|
512
|
+
if [ -f "$SCRIPT_DIR/context.md" ]; then
|
|
513
|
+
cat "$SCRIPT_DIR/context.md"
|
|
514
|
+
fi
|
|
515
|
+
return 0
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# \u2500\u2500 Level 2: Legacy monolithic endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
519
|
+
RESPONSE=$(curl -sf -X GET "$BASE_URL/context" \\
|
|
520
|
+
-H "Authorization: Bearer $AGENTMAP_API_KEY" \\
|
|
521
|
+
-H "Accept: application/json" 2>/dev/null) || true
|
|
522
|
+
|
|
523
|
+
if [ -n "$RESPONSE" ] && echo "$RESPONSE" | jq -e '.formatted_context' >/dev/null 2>&1; then
|
|
524
|
+
echo "$RESPONSE" | jq -r '.formatted_context // empty' > "$SCRIPT_DIR/context.md"
|
|
525
|
+
cat "$SCRIPT_DIR/context.md"
|
|
526
|
+
return 0
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
# \u2500\u2500 Level 3: Cached file fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
530
|
+
if [ -f "$SCRIPT_DIR/context.md" ]; then
|
|
531
|
+
echo "# (cached \u2014 API unreachable)" | cat - "$SCRIPT_DIR/context.md"
|
|
532
|
+
return 0
|
|
533
|
+
fi
|
|
534
|
+
|
|
535
|
+
echo "Warning: Pawl context unavailable" >&2
|
|
536
|
+
return 1
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# \u2500\u2500 push mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
540
|
+
push() {
|
|
541
|
+
LAST_SHA=""
|
|
542
|
+
if [ -f "$SCRIPT_DIR/.last-sync-sha" ]; then
|
|
543
|
+
LAST_SHA=$(cat "$SCRIPT_DIR/.last-sync-sha")
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
CURRENT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
547
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
548
|
+
|
|
549
|
+
# Compute diff
|
|
550
|
+
if [ -n "$LAST_SHA" ]; then
|
|
551
|
+
DIFF=$(git diff "$LAST_SHA"..HEAD 2>/dev/null || echo "")
|
|
552
|
+
else
|
|
553
|
+
DIFF=$(git diff HEAD~1..HEAD 2>/dev/null || echo "")
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
# Collect tracked files (dedup)
|
|
557
|
+
FILES_CHANGED="[]"
|
|
558
|
+
if [ -f "$SCRIPT_DIR/.tracked-files" ]; then
|
|
559
|
+
FILES_CHANGED=$(sort -u "$SCRIPT_DIR/.tracked-files" | jq -R . | jq -s .)
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
# Commit info
|
|
563
|
+
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "")
|
|
564
|
+
|
|
565
|
+
# Session ID from Claude Code (if available)
|
|
566
|
+
SESSION_ID="\${CLAUDE_SESSION_ID:-}"
|
|
567
|
+
|
|
568
|
+
# Repo root for CC session lookup
|
|
569
|
+
REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
570
|
+
|
|
571
|
+
# Parse CC session data locally (if node + parser available)
|
|
572
|
+
CC_SESSION="{}"
|
|
573
|
+
if command -v node &> /dev/null; then
|
|
574
|
+
PARSER="$SCRIPT_DIR/parse-cc-session.js"
|
|
575
|
+
if [ -f "$PARSER" ]; then
|
|
576
|
+
CC_SESSION=$(node "$PARSER" "$REPO_PATH" 2>/dev/null || echo "{}")
|
|
577
|
+
fi
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
# Extract cc_tasks from parser output (P14-02)
|
|
581
|
+
CC_TASKS=$(echo "$CC_SESSION" | jq -c '.tasks // []')
|
|
582
|
+
|
|
583
|
+
PAYLOAD=$(jq -n \\
|
|
584
|
+
--arg diff "$DIFF" \\
|
|
585
|
+
--arg branch "$BRANCH" \\
|
|
586
|
+
--arg commit_sha "$CURRENT_SHA" \\
|
|
587
|
+
--arg commit_message "$COMMIT_MSG" \\
|
|
588
|
+
--arg session_id "$SESSION_ID" \\
|
|
589
|
+
--arg last_sync_sha "$LAST_SHA" \\
|
|
590
|
+
--arg repo_path "$REPO_PATH" \\
|
|
591
|
+
--argjson files_changed "$FILES_CHANGED" \\
|
|
592
|
+
--argjson cc_session "$CC_SESSION" \\
|
|
593
|
+
--argjson cc_tasks "$CC_TASKS" \\
|
|
594
|
+
'{
|
|
595
|
+
diff: $diff,
|
|
596
|
+
branch: $branch,
|
|
597
|
+
commit_sha: $commit_sha,
|
|
598
|
+
commit_message: $commit_message,
|
|
599
|
+
session_id: $session_id,
|
|
600
|
+
last_sync_sha: $last_sync_sha,
|
|
601
|
+
repo_path: $repo_path,
|
|
602
|
+
files_changed: $files_changed,
|
|
603
|
+
cc_session: $cc_session,
|
|
604
|
+
cc_tasks: $cc_tasks
|
|
605
|
+
}')
|
|
606
|
+
|
|
607
|
+
RESPONSE=$(curl -sf -X POST "$BASE_URL/sync" \\
|
|
608
|
+
-H "Authorization: Bearer $AGENTMAP_API_KEY" \\
|
|
609
|
+
-H "Content-Type: application/json" \\
|
|
610
|
+
-d "$PAYLOAD")
|
|
611
|
+
|
|
612
|
+
echo "$RESPONSE" | jq .
|
|
613
|
+
|
|
614
|
+
# Update last sync SHA
|
|
615
|
+
echo "$CURRENT_SHA" > "$SCRIPT_DIR/.last-sync-sha"
|
|
616
|
+
|
|
617
|
+
# Cleanup tracked files
|
|
618
|
+
rm -f "$SCRIPT_DIR/.tracked-files"
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# \u2500\u2500 dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
622
|
+
case "\${1:-}" in
|
|
623
|
+
pull) pull ;;
|
|
624
|
+
push) push ;;
|
|
625
|
+
*)
|
|
626
|
+
echo "Usage: sync.sh [pull|push]"
|
|
627
|
+
exit 1
|
|
628
|
+
;;
|
|
629
|
+
esac
|
|
630
|
+
`;
|
|
631
|
+
const scriptPath = (0, import_node_path.join)(dir, "sync.sh");
|
|
632
|
+
(0, import_node_fs.writeFileSync)(scriptPath, script, "utf-8");
|
|
633
|
+
(0, import_node_fs.chmodSync)(scriptPath, 493);
|
|
634
|
+
}
|
|
635
|
+
function copyParserScript(cwd, destDir = ".agentmap") {
|
|
636
|
+
const src = (0, import_node_path.join)(__dirname, "parse-cc-session.js");
|
|
637
|
+
const dest = (0, import_node_path.join)(cwd, destDir, "parse-cc-session.js");
|
|
638
|
+
if ((0, import_node_fs.existsSync)(src)) {
|
|
639
|
+
(0, import_node_fs.copyFileSync)(src, dest);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function mergeClaudeSettings(cwd, basePath = ".agentmap") {
|
|
643
|
+
const claudeDir = (0, import_node_path.join)(cwd, ".claude");
|
|
644
|
+
(0, import_node_fs.mkdirSync)(claudeDir, { recursive: true });
|
|
645
|
+
const settingsPath = (0, import_node_path.join)(claudeDir, "settings.json");
|
|
646
|
+
let settings = {};
|
|
647
|
+
if ((0, import_node_fs.existsSync)(settingsPath)) {
|
|
648
|
+
try {
|
|
649
|
+
settings = JSON.parse((0, import_node_fs.readFileSync)(settingsPath, "utf-8"));
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const hooks = {
|
|
654
|
+
SessionStart: [
|
|
655
|
+
{
|
|
656
|
+
hooks: [
|
|
657
|
+
{ type: "command", command: `${basePath}/sync.sh pull` }
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
],
|
|
661
|
+
PostToolUse: [
|
|
662
|
+
{
|
|
663
|
+
matcher: "Write|Edit",
|
|
664
|
+
hooks: [
|
|
665
|
+
{
|
|
666
|
+
type: "command",
|
|
667
|
+
command: `jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
],
|
|
672
|
+
Stop: [
|
|
673
|
+
{
|
|
674
|
+
hooks: [
|
|
675
|
+
{ type: "command", command: `${basePath}/sync.sh push` }
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
]
|
|
679
|
+
};
|
|
680
|
+
const hooksObj = settings.hooks || {};
|
|
681
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
682
|
+
const existing = hooksObj[event] || [];
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
const cmd = entry.hooks[0].command;
|
|
685
|
+
const alreadyExists = existing.some(
|
|
686
|
+
(e) => e.hooks?.[0]?.command === cmd
|
|
687
|
+
);
|
|
688
|
+
if (!alreadyExists) {
|
|
689
|
+
existing.push(entry);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
hooksObj[event] = existing;
|
|
693
|
+
}
|
|
694
|
+
settings.hooks = hooksObj;
|
|
695
|
+
(0, import_node_fs.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
696
|
+
}
|
|
697
|
+
function writeClaudeMd(cwd) {
|
|
698
|
+
const claudeMdPath = (0, import_node_path.join)(cwd, "CLAUDE.md");
|
|
699
|
+
const START_MARKER = "<!-- agentmap:start -->";
|
|
700
|
+
const END_MARKER = "<!-- agentmap:end -->";
|
|
701
|
+
const block = [
|
|
702
|
+
START_MARKER,
|
|
703
|
+
"# Pawl Context",
|
|
704
|
+
"",
|
|
705
|
+
"This project uses Pawl for spec management. Context files are in `.agentmap/`.",
|
|
706
|
+
"",
|
|
707
|
+
"## Context Files",
|
|
708
|
+
"- `.agentmap/context.md` \u2014 Project index, spec map, work items",
|
|
709
|
+
"- `.agentmap/specs/*.md` \u2014 Individual spec files with vision, implementation, dependencies",
|
|
710
|
+
"- `.agentmap/progress.md` \u2014 Last session summary, project health, recent decisions",
|
|
711
|
+
"",
|
|
712
|
+
"Read these files at the start of a session for full project context.",
|
|
713
|
+
"When asked about a spec, read the corresponding file in `.agentmap/specs/`.",
|
|
714
|
+
END_MARKER
|
|
715
|
+
].join("\n");
|
|
716
|
+
if (!(0, import_node_fs.existsSync)(claudeMdPath)) {
|
|
717
|
+
(0, import_node_fs.writeFileSync)(claudeMdPath, block + "\n", "utf-8");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
let content = (0, import_node_fs.readFileSync)(claudeMdPath, "utf-8");
|
|
721
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
722
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
723
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
724
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
|
|
725
|
+
} else {
|
|
726
|
+
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
727
|
+
}
|
|
728
|
+
(0, import_node_fs.writeFileSync)(claudeMdPath, content, "utf-8");
|
|
729
|
+
}
|
|
730
|
+
function updateGitignore(cwd) {
|
|
731
|
+
const gitignorePath = (0, import_node_path.join)(cwd, ".gitignore");
|
|
732
|
+
let content = "";
|
|
733
|
+
if ((0, import_node_fs.existsSync)(gitignorePath)) {
|
|
734
|
+
content = (0, import_node_fs.readFileSync)(gitignorePath, "utf-8");
|
|
735
|
+
}
|
|
736
|
+
const lines = content.split("\n");
|
|
737
|
+
const toAdd = [
|
|
738
|
+
".agentmap/.env",
|
|
739
|
+
".agentmap/.tracked-files",
|
|
740
|
+
".agentmap/.last-sync-sha",
|
|
741
|
+
".agentmap/context.md",
|
|
742
|
+
".agentmap/specs/",
|
|
743
|
+
".agentmap/progress.md",
|
|
744
|
+
".pawl/.env",
|
|
745
|
+
".pawl/.tracked-files",
|
|
746
|
+
".pawl/.last-sync-sha",
|
|
747
|
+
".pawl/context.md",
|
|
748
|
+
".pawl/specs/",
|
|
749
|
+
".pawl/progress.md"
|
|
750
|
+
];
|
|
751
|
+
const missing = toAdd.filter((line) => !lines.includes(line));
|
|
752
|
+
if (missing.length > 0) {
|
|
753
|
+
const section = [];
|
|
754
|
+
if (!lines.includes("# pawl")) {
|
|
755
|
+
section.push("", "# pawl");
|
|
756
|
+
}
|
|
757
|
+
section.push(...missing);
|
|
758
|
+
const newContent = content.trimEnd() + "\n" + section.join("\n") + "\n";
|
|
759
|
+
(0, import_node_fs.writeFileSync)(gitignorePath, newContent, "utf-8");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
main();
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/parse-cc-session.ts
|
|
26
|
+
var import_node_fs = require("fs");
|
|
27
|
+
var import_node_readline = require("readline");
|
|
28
|
+
var import_node_os = require("os");
|
|
29
|
+
var import_node_path = __toESM(require("path"));
|
|
30
|
+
var CC_MODEL_PRICING = {
|
|
31
|
+
"claude-opus-4-6": { input: 15, output: 75, cacheRead: 1.5 },
|
|
32
|
+
"claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheRead: 0.3 },
|
|
33
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3 },
|
|
34
|
+
"claude-haiku-4-5-20251001": { input: 0.8, output: 4, cacheRead: 0.08 }
|
|
35
|
+
};
|
|
36
|
+
function getProjectDir(repoPath) {
|
|
37
|
+
const encoded = "-" + repoPath.slice(1).replace(/\//g, "-");
|
|
38
|
+
const projectDir = import_node_path.default.join((0, import_node_os.homedir)(), ".claude", "projects", encoded);
|
|
39
|
+
try {
|
|
40
|
+
const s = (0, import_node_fs.statSync)(projectDir);
|
|
41
|
+
if (s.isDirectory()) return projectDir;
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
async function parseSessionFile(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
49
|
+
input: (0, import_node_fs.createReadStream)(filePath),
|
|
50
|
+
crlfDelay: Infinity
|
|
51
|
+
});
|
|
52
|
+
let model = "";
|
|
53
|
+
let firstTimestamp = "";
|
|
54
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
55
|
+
const filesRead = /* @__PURE__ */ new Set();
|
|
56
|
+
const filesWritten = /* @__PURE__ */ new Set();
|
|
57
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
58
|
+
const pendingCreates = /* @__PURE__ */ new Map();
|
|
59
|
+
let currentTaskId = null;
|
|
60
|
+
let totalInputTokens = 0;
|
|
61
|
+
let totalOutputTokens = 0;
|
|
62
|
+
let cacheReadTokens = 0;
|
|
63
|
+
let cacheWriteTokens = 0;
|
|
64
|
+
for await (const line of rl) {
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(line);
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!firstTimestamp && parsed.timestamp) {
|
|
72
|
+
firstTimestamp = parsed.timestamp;
|
|
73
|
+
}
|
|
74
|
+
if (parsed.type === "assistant" && parsed.message) {
|
|
75
|
+
const msg = parsed.message;
|
|
76
|
+
const msgId = msg.id;
|
|
77
|
+
if (!model && msg.model) model = msg.model;
|
|
78
|
+
if (msgId && !seenMessageIds.has(msgId)) {
|
|
79
|
+
seenMessageIds.add(msgId);
|
|
80
|
+
const u = msg.usage;
|
|
81
|
+
if (u) {
|
|
82
|
+
totalInputTokens += u.input_tokens ?? 0;
|
|
83
|
+
totalOutputTokens += u.output_tokens ?? 0;
|
|
84
|
+
cacheReadTokens += u.cache_read_input_tokens ?? 0;
|
|
85
|
+
cacheWriteTokens += u.cache_creation_input_tokens ?? 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const content = msg.content;
|
|
89
|
+
if (Array.isArray(content)) {
|
|
90
|
+
for (const block of content) {
|
|
91
|
+
if (block.type !== "tool_use") continue;
|
|
92
|
+
const fp = block.input?.file_path;
|
|
93
|
+
if (fp) {
|
|
94
|
+
if (block.name === "Read") {
|
|
95
|
+
filesRead.add(fp);
|
|
96
|
+
} else if (block.name === "Write" || block.name === "Edit") {
|
|
97
|
+
filesWritten.add(fp);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (block.name === "TaskCreate") {
|
|
101
|
+
pendingCreates.set(block.id, {
|
|
102
|
+
subject: block.input?.subject || "",
|
|
103
|
+
description: block.input?.description || ""
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (block.name === "TaskUpdate") {
|
|
107
|
+
const taskId = block.input?.taskId;
|
|
108
|
+
const task = taskId ? tasks.get(taskId) : void 0;
|
|
109
|
+
if (task && block.input?.status) {
|
|
110
|
+
task.status = block.input.status;
|
|
111
|
+
if (block.input.status === "in_progress")
|
|
112
|
+
currentTaskId = taskId;
|
|
113
|
+
if (block.input.status === "completed" || block.input.status === "deleted") {
|
|
114
|
+
if (currentTaskId === taskId) currentTaskId = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (currentTaskId && (block.name === "Write" || block.name === "Edit") && fp) {
|
|
119
|
+
tasks.get(currentTaskId)?.filesAssociated.add(fp);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (parsed.type === "user" && parsed.toolUseResult?.task?.id) {
|
|
125
|
+
const tr = parsed.toolUseResult;
|
|
126
|
+
const content = parsed.message?.content;
|
|
127
|
+
const toolUseId = Array.isArray(content) ? content.find((b) => b.type === "tool_result")?.tool_use_id : void 0;
|
|
128
|
+
const pending = toolUseId ? pendingCreates.get(toolUseId) : void 0;
|
|
129
|
+
tasks.set(tr.task.id, {
|
|
130
|
+
id: tr.task.id,
|
|
131
|
+
subject: tr.task.subject || pending?.subject || "",
|
|
132
|
+
description: pending?.description || "",
|
|
133
|
+
status: "pending",
|
|
134
|
+
filesAssociated: /* @__PURE__ */ new Set()
|
|
135
|
+
});
|
|
136
|
+
if (toolUseId) pendingCreates.delete(toolUseId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (totalInputTokens === 0 && totalOutputTokens === 0) return null;
|
|
140
|
+
const pricing = CC_MODEL_PRICING[model] ?? {
|
|
141
|
+
input: 3,
|
|
142
|
+
output: 15,
|
|
143
|
+
cacheRead: 0.3
|
|
144
|
+
};
|
|
145
|
+
const realCost = (totalInputTokens * pricing.input + totalOutputTokens * pricing.output + cacheReadTokens * pricing.cacheRead + cacheWriteTokens * pricing.input) / 1e6;
|
|
146
|
+
let sessionDurationMs = 0;
|
|
147
|
+
if (firstTimestamp) {
|
|
148
|
+
try {
|
|
149
|
+
const mtime = (0, import_node_fs.statSync)(filePath).mtime.getTime();
|
|
150
|
+
const start = new Date(firstTimestamp).getTime();
|
|
151
|
+
if (mtime > start) sessionDurationMs = mtime - start;
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
filesRead: [...filesRead],
|
|
157
|
+
filesWritten: [...filesWritten],
|
|
158
|
+
totalInputTokens,
|
|
159
|
+
totalOutputTokens,
|
|
160
|
+
cacheReadTokens,
|
|
161
|
+
cacheWriteTokens,
|
|
162
|
+
model,
|
|
163
|
+
realCost,
|
|
164
|
+
sessionDurationMs,
|
|
165
|
+
tasks: Array.from(tasks.values()).map((t) => ({
|
|
166
|
+
id: t.id,
|
|
167
|
+
subject: t.subject,
|
|
168
|
+
description: t.description,
|
|
169
|
+
status: t.status,
|
|
170
|
+
filesAssociated: [...t.filesAssociated]
|
|
171
|
+
}))
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function getMostRecentSession(repoPath) {
|
|
178
|
+
const projectDir = getProjectDir(repoPath);
|
|
179
|
+
if (!projectDir) return null;
|
|
180
|
+
let entries;
|
|
181
|
+
try {
|
|
182
|
+
entries = (0, import_node_fs.readdirSync)(projectDir);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
|
|
187
|
+
if (jsonlFiles.length === 0) return null;
|
|
188
|
+
const withStats = jsonlFiles.map((f) => {
|
|
189
|
+
const fp = import_node_path.default.join(projectDir, f);
|
|
190
|
+
try {
|
|
191
|
+
const s = (0, import_node_fs.statSync)(fp);
|
|
192
|
+
return { file: f, mtime: s.mtime.getTime(), path: fp };
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}).filter((x) => x !== null);
|
|
197
|
+
if (withStats.length === 0) return null;
|
|
198
|
+
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
199
|
+
return parseSessionFile(withStats[0].path);
|
|
200
|
+
}
|
|
201
|
+
async function main() {
|
|
202
|
+
const repoPath = process.argv[2];
|
|
203
|
+
if (!repoPath) {
|
|
204
|
+
process.stdout.write("{}");
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const result = await getMostRecentSession(repoPath);
|
|
209
|
+
process.stdout.write(JSON.stringify(result ?? {}));
|
|
210
|
+
} catch {
|
|
211
|
+
process.stdout.write("{}");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpawl/setup",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"description": "One-shot setup for Pawl + Claude Code hooks",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pawl": "./dist/index.js",
|
|
8
|
+
"pawl-setup": "./dist/index.js"
|
|
9
|
+
},
|
|
6
10
|
"scripts": {
|
|
7
|
-
"
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"test": "node --test test/e2e.test.mjs",
|
|
13
|
+
"lint": "tsc --noEmit",
|
|
14
|
+
"prepublishOnly": "pnpm build"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/0xfishbone/agentMap.git",
|
|
26
|
+
"directory": "packages/setup"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.4.0",
|
|
30
|
+
"typescript": "^5.7.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
8
34
|
},
|
|
9
|
-
"
|
|
10
|
-
"author": "",
|
|
11
|
-
"license": "ISC",
|
|
12
|
-
"type": "commonjs"
|
|
35
|
+
"license": "MIT"
|
|
13
36
|
}
|