@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 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.0.0",
4
- "description": "",
5
- "main": "index.js",
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
- "test": "echo \"Error: no test specified\" && exit 1"
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
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
35
+ "license": "MIT"
13
36
  }