@getpawl/setup 1.1.2 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +62 -13
  2. package/dist/index.js +299 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,29 +1,78 @@
1
- # @fixhbone/agentmap-setup
1
+ # @getpawl/setup
2
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.
3
+ One-shot setup for [Pawl](https://github.com/0xfishbone/agentMap) connects any repo to your Pawl project so spec context is automatically synced during AI coding sessions.
4
4
 
5
- ## Usage
5
+ ## Install
6
6
 
7
7
  ```sh
8
- npx @fixhbone/agentmap-setup <PROJECT_KEY>
8
+ npm install -g @getpawl/setup
9
9
  ```
10
10
 
11
- Get your `<PROJECT_KEY>` from the AgentMap web dashboard under **Settings > Setup**.
11
+ Then the `pawl` command will be available globally (`pawl sync`, `pawl connect`, `pawl migrate`).
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Initialize
16
+
17
+ ```sh
18
+ npx @getpawl/setup init
19
+ ```
20
+
21
+ Creates `.pawl/` directory with sync scripts, configures Claude Code hooks, and sets up git post-commit hook.
22
+
23
+ ### 2. Connect
24
+
25
+ ```sh
26
+ npx @getpawl/setup connect
27
+ ```
28
+
29
+ Opens your browser to link this repo to a Pawl project. Writes `.pawl/.env` automatically — no manual key copy-paste.
30
+
31
+ ### 3. Sync
32
+
33
+ ```sh
34
+ pawl sync # push current session to dashboard
35
+ pawl sync --pull # pull latest context from dashboard
36
+ ```
37
+
38
+ Sync happens automatically via hooks. Use these commands for manual sync or agents without lifecycle hooks (Codex, etc.).
39
+
40
+ ## Legacy Setup
41
+
42
+ Existing users can still use the one-step flow:
43
+
44
+ ```sh
45
+ npx @getpawl/setup <PROJECT_KEY>
46
+ ```
47
+
48
+ Get your `<PROJECT_KEY>` from the Pawl dashboard under **Settings > Setup**.
12
49
 
13
50
  ## What it does
14
51
 
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`
52
+ 1. Creates `.pawl/.env` with API credentials
53
+ 2. Generates `.pawl/sync.mjs` — Node.js sync script (no curl/jq needed)
54
+ 3. Generates `.pawl/track-file.mjs` Node.js file tracker (replaces jq in PostToolUse hook)
55
+ 4. Generates `.pawl/sync.sh` bash fallback for environments without Node.js
56
+ 5. Copies `.pawl/parse-cc-session.js`extracts token usage and tasks from CC sessions
57
+ 6. Configures `.claude/settings.json` with lifecycle hooks (SessionStart, PostToolUse, Stop)
58
+ 7. Configures `.gemini/settings.json` with lifecycle hooks (if Gemini CLI detected)
59
+ 8. Installs `.git/hooks/post-commit` for universal agent support
60
+ 9. Updates `CLAUDE.md` and `AGENTS.md` with Pawl context blocks
61
+ 10. Adds sensitive paths to `.gitignore`
62
+
63
+ ## Supported Agents
64
+
65
+ | Agent | Hook Method |
66
+ |-------|-------------|
67
+ | Claude Code | `.claude/settings.json` lifecycle hooks |
68
+ | Gemini CLI | `.gemini/settings.json` lifecycle hooks |
69
+ | Codex / Others | `git post-commit` hook fallback |
22
70
 
23
71
  ## Requirements
24
72
 
25
73
  - Node.js >= 18
26
- - [Claude Code](https://claude.ai/claude-code) CLI installed
74
+ - Git repository
75
+ - `curl` and `jq` are **no longer required** — `pawl init` generates Node.js sync scripts that use native `fetch()`. Bash scripts are kept as a fallback.
27
76
 
28
77
  ## License
29
78
 
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ async function main() {
19
19
  } else if (arg === "init") {
20
20
  const key = process.argv[3];
21
21
  pawlInit(key);
22
+ } else if (arg === "migrate") {
23
+ pawlMigrate();
22
24
  } else {
23
25
  legacySetup(arg);
24
26
  }
@@ -29,6 +31,7 @@ Usage:
29
31
  pawl init Initialize Pawl in this repo
30
32
  pawl connect Link this repo to a Pawl project (opens browser)
31
33
  pawl sync [--pull] Sync with Pawl dashboard (push by default)
34
+ pawl migrate Migrate .pawl/sync.sh from AGENTMAP_* to PAWL_* vars
32
35
  pawl-setup <PROJECT_KEY> Legacy setup (still supported)
33
36
 
34
37
  Dashboard: https://pawl.dev
@@ -36,9 +39,10 @@ Dashboard: https://pawl.dev
36
39
  }
37
40
  function pawlSync(flag) {
38
41
  const cwd = process.cwd();
39
- const syncScript = (0, import_node_path.join)(cwd, ".pawl", "sync.sh");
40
- if (!(0, import_node_fs.existsSync)(syncScript)) {
41
- console.error("Error: .pawl/sync.sh not found \u2014 run `pawl init` first.");
42
+ const syncMjs = (0, import_node_path.join)(cwd, ".pawl", "sync.mjs");
43
+ const syncSh = (0, import_node_path.join)(cwd, ".pawl", "sync.sh");
44
+ if (!(0, import_node_fs.existsSync)(syncMjs) && !(0, import_node_fs.existsSync)(syncSh)) {
45
+ console.error("Error: .pawl/sync.mjs not found \u2014 run `pawl init` first.");
42
46
  process.exit(1);
43
47
  }
44
48
  const envFile = (0, import_node_path.join)(cwd, ".pawl", ".env");
@@ -47,7 +51,43 @@ function pawlSync(flag) {
47
51
  process.exit(1);
48
52
  }
49
53
  const mode = flag === "--pull" ? "pull" : "push";
50
- (0, import_node_child_process.execSync)(`.pawl/sync.sh ${mode}`, { stdio: "inherit", cwd });
54
+ if ((0, import_node_fs.existsSync)(syncMjs)) {
55
+ (0, import_node_child_process.execSync)(`node .pawl/sync.mjs ${mode}`, { stdio: "inherit", cwd });
56
+ } else {
57
+ (0, import_node_child_process.execSync)(`.pawl/sync.sh ${mode}`, { stdio: "inherit", cwd });
58
+ }
59
+ }
60
+ function pawlMigrate() {
61
+ const cwd = process.cwd();
62
+ const syncPath = (0, import_node_path.join)(cwd, ".pawl", "sync.sh");
63
+ if (!(0, import_node_fs.existsSync)(syncPath)) {
64
+ console.log(" Nothing to migrate \u2014 run `pawl init` first.");
65
+ process.exit(0);
66
+ }
67
+ let content = (0, import_node_fs.readFileSync)(syncPath, "utf-8");
68
+ if (!content.includes("AGENTMAP_")) {
69
+ console.log(" Already up to date.");
70
+ process.exit(0);
71
+ }
72
+ content = content.replace(/AGENTMAP_API_URL/g, "PAWL_API_URL").replace(/AGENTMAP_PROJECT_ID/g, "PAWL_PROJECT_ID").replace(/AGENTMAP_API_KEY/g, "PAWL_API_KEY");
73
+ (0, import_node_fs.writeFileSync)(syncPath, content, "utf-8");
74
+ const envPath = (0, import_node_path.join)(cwd, ".pawl", ".env");
75
+ if ((0, import_node_fs.existsSync)(envPath)) {
76
+ let envContent = (0, import_node_fs.readFileSync)(envPath, "utf-8");
77
+ if (envContent.includes("AGENTMAP_")) {
78
+ envContent = envContent.replace(/AGENTMAP_API_URL/g, "PAWL_API_URL").replace(/AGENTMAP_PROJECT_ID/g, "PAWL_PROJECT_ID").replace(/AGENTMAP_API_KEY/g, "PAWL_API_KEY");
79
+ (0, import_node_fs.writeFileSync)(envPath, envContent, "utf-8");
80
+ console.log(" .pawl/.env also migrated.");
81
+ }
82
+ }
83
+ if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".pawl", "sync.mjs"))) {
84
+ writePawlSyncMjs(cwd);
85
+ }
86
+ if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, ".pawl", "track-file.mjs"))) {
87
+ writeTrackFileScript(cwd);
88
+ }
89
+ mergeClaudeSettings(cwd, ".pawl");
90
+ console.log(" Migration complete \u2014 .pawl/sync.sh updated to PAWL_* vars.");
51
91
  }
52
92
  function generateCode() {
53
93
  const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -138,7 +178,7 @@ async function pawlConnect() {
138
178
  (0, import_node_fs.mkdirSync)((0, import_node_path.join)(cwd, ".pawl"), { recursive: true });
139
179
  writePawlEnvFile(cwd, result);
140
180
  console.log(" Connected! .pawl/.env written.\n");
141
- console.log(` Project: ${result.apiUrl}/projects/${result.projectId}`);
181
+ console.log(` Dashboard: https://agentmap-mimy.onrender.com/projects/${result.projectId}`);
142
182
  console.log(" Run `pawl sync` to push your first session.\n");
143
183
  }
144
184
  function legacySetup(encoded) {
@@ -185,6 +225,8 @@ function pawlInit(key) {
185
225
  writePawlEnvFile(cwd, config);
186
226
  }
187
227
  writePawlSyncScript(cwd);
228
+ writePawlSyncMjs(cwd);
229
+ writeTrackFileScript(cwd);
188
230
  copyParserScript(cwd, ".pawl");
189
231
  if (detected.hasCC) {
190
232
  mergeClaudeSettings(cwd, ".pawl");
@@ -416,6 +458,211 @@ esac
416
458
  (0, import_node_fs.writeFileSync)(scriptPath, script, "utf-8");
417
459
  (0, import_node_fs.chmodSync)(scriptPath, 493);
418
460
  }
461
+ function writePawlSyncMjs(cwd) {
462
+ const script = `#!/usr/bin/env node
463
+ // Pawl sync script (Node.js) \u2014 generated by pawl init
464
+ // Do not edit manually; re-run pawl init to update.
465
+
466
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
467
+ import { execSync } from 'node:child_process';
468
+ import { dirname, join } from 'node:path';
469
+ import { fileURLToPath } from 'node:url';
470
+
471
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
472
+
473
+ // Parse .env file (no dotenv dependency)
474
+ function loadEnv() {
475
+ const envPath = join(SCRIPT_DIR, '.env');
476
+ if (!existsSync(envPath)) return {};
477
+ const env = {};
478
+ for (const line of readFileSync(envPath, 'utf-8').split('\\n')) {
479
+ const trimmed = line.trim();
480
+ if (!trimmed || trimmed.startsWith('#')) continue;
481
+ const eq = trimmed.indexOf('=');
482
+ if (eq === -1) continue;
483
+ env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
484
+ }
485
+ return env;
486
+ }
487
+
488
+ const env = loadEnv();
489
+ const API_KEY = env.PAWL_API_KEY || '';
490
+ const PROJECT_ID = env.PAWL_PROJECT_ID || '';
491
+ const API_URL = env.PAWL_API_URL || '';
492
+ const BASE_URL = \`\${API_URL}/api/projects/\${PROJECT_ID}\`;
493
+
494
+ async function pull() {
495
+ if (!API_KEY) {
496
+ process.stderr.write('Warning: PAWL_API_KEY not set \u2014 sync disabled\\n');
497
+ const cached = join(SCRIPT_DIR, 'context.md');
498
+ if (existsSync(cached)) process.stdout.write(readFileSync(cached, 'utf-8'));
499
+ return;
500
+ }
501
+
502
+ // Recovery: if tracked-files exist from a previous incomplete session, push first
503
+ if (existsSync(join(SCRIPT_DIR, '.tracked-files'))) {
504
+ try { await push(); } catch {}
505
+ }
506
+
507
+ mkdirSync(join(SCRIPT_DIR, 'specs'), { recursive: true });
508
+
509
+ // Level 1: File-based endpoint
510
+ try {
511
+ const res = await fetch(\`\${BASE_URL}/context/files\`, {
512
+ headers: { 'Authorization': \`Bearer \${API_KEY}\`, 'Accept': 'application/json' },
513
+ });
514
+ if (res.ok) {
515
+ const data = await res.json();
516
+ if (data.files) {
517
+ for (const file of data.files) {
518
+ const filePath = join(SCRIPT_DIR, file.path);
519
+ mkdirSync(dirname(filePath), { recursive: true });
520
+ writeFileSync(filePath, file.content, 'utf-8');
521
+ }
522
+ const ctxPath = join(SCRIPT_DIR, 'context.md');
523
+ if (existsSync(ctxPath)) process.stdout.write(readFileSync(ctxPath, 'utf-8'));
524
+ return;
525
+ }
526
+ }
527
+ } catch {}
528
+
529
+ // Level 2: Legacy monolithic endpoint
530
+ try {
531
+ const res = await fetch(\`\${BASE_URL}/context\`, {
532
+ headers: { 'Authorization': \`Bearer \${API_KEY}\`, 'Accept': 'application/json' },
533
+ });
534
+ if (res.ok) {
535
+ const data = await res.json();
536
+ if (data.formatted_context) {
537
+ writeFileSync(join(SCRIPT_DIR, 'context.md'), data.formatted_context, 'utf-8');
538
+ process.stdout.write(data.formatted_context);
539
+ return;
540
+ }
541
+ }
542
+ } catch {}
543
+
544
+ // Level 3: Cached fallback
545
+ const cached = join(SCRIPT_DIR, 'context.md');
546
+ if (existsSync(cached)) {
547
+ process.stdout.write('# (cached \u2014 API unreachable)\\n' + readFileSync(cached, 'utf-8'));
548
+ return;
549
+ }
550
+
551
+ process.stderr.write('Warning: Pawl context unavailable\\n');
552
+ process.exitCode = 1;
553
+ }
554
+
555
+ async function push() {
556
+ if (!API_KEY) return;
557
+
558
+ let lastSha = '';
559
+ const lastShaPath = join(SCRIPT_DIR, '.last-sync-sha');
560
+ if (existsSync(lastShaPath)) lastSha = readFileSync(lastShaPath, 'utf-8').trim();
561
+
562
+ const git = (cmd) => { try { return execSync(cmd, { encoding: 'utf-8' }).trim(); } catch { return ''; } };
563
+
564
+ const currentSha = git('git rev-parse HEAD');
565
+ const branch = git('git rev-parse --abbrev-ref HEAD') || 'unknown';
566
+
567
+ let diff = '';
568
+ if (lastSha) {
569
+ diff = git(\`git diff \${lastSha}..HEAD\`);
570
+ } else {
571
+ diff = git('git diff HEAD~1..HEAD');
572
+ }
573
+
574
+ // Cap diff at 500K
575
+ if (diff.length > 500000) diff = diff.slice(0, 500000);
576
+
577
+ // Collect tracked files
578
+ let filesChanged = [];
579
+ const trackedPath = join(SCRIPT_DIR, '.tracked-files');
580
+ if (existsSync(trackedPath)) {
581
+ filesChanged = [...new Set(readFileSync(trackedPath, 'utf-8').split('\\n').filter(Boolean))];
582
+ }
583
+
584
+ const commitMessage = git('git log -1 --pretty=format:"%s"');
585
+ const sessionId = process.env.CLAUDE_SESSION_ID || '';
586
+ const repoPath = git('git rev-parse --show-toplevel') || process.cwd();
587
+
588
+ // Parse CC session data
589
+ let ccSession = {};
590
+ const parserPath = join(SCRIPT_DIR, 'parse-cc-session.js');
591
+ if (existsSync(parserPath)) {
592
+ try {
593
+ const raw = execSync(\`node "\${parserPath}" "\${repoPath}"\`, { encoding: 'utf-8' });
594
+ ccSession = JSON.parse(raw);
595
+ } catch {}
596
+ }
597
+
598
+ const ccTasks = ccSession.tasks || [];
599
+
600
+ const payload = {
601
+ diff,
602
+ branch,
603
+ commit_sha: currentSha,
604
+ commit_message: commitMessage,
605
+ session_id: sessionId,
606
+ last_sync_sha: lastSha,
607
+ repo_path: repoPath,
608
+ files_changed: filesChanged,
609
+ cc_session: ccSession,
610
+ cc_tasks: ccTasks,
611
+ };
612
+
613
+ try {
614
+ const res = await fetch(\`\${BASE_URL}/sync\`, {
615
+ method: 'POST',
616
+ headers: {
617
+ 'Authorization': \`Bearer \${API_KEY}\`,
618
+ 'Content-Type': 'application/json',
619
+ },
620
+ body: JSON.stringify(payload),
621
+ });
622
+ if (res.ok) {
623
+ const data = await res.json();
624
+ process.stdout.write(JSON.stringify(data, null, 2) + '\\n');
625
+ }
626
+ } catch {}
627
+
628
+ writeFileSync(lastShaPath, currentSha, 'utf-8');
629
+ if (existsSync(trackedPath)) unlinkSync(trackedPath);
630
+ }
631
+
632
+ // Dispatch
633
+ const mode = process.argv[2];
634
+ if (mode === 'pull') {
635
+ pull().catch(() => { process.exitCode = 1; });
636
+ } else if (mode === 'push') {
637
+ push().catch(() => { process.exitCode = 1; });
638
+ } else {
639
+ process.stderr.write('Usage: sync.mjs [pull|push]\\n');
640
+ process.exitCode = 1;
641
+ }
642
+ `;
643
+ const scriptPath = (0, import_node_path.join)(cwd, ".pawl", "sync.mjs");
644
+ (0, import_node_fs.writeFileSync)(scriptPath, script, "utf-8");
645
+ }
646
+ function writeTrackFileScript(cwd) {
647
+ const script = `// Pawl file tracker \u2014 reads tool JSON from stdin, appends file_path to .tracked-files
648
+ import { appendFileSync } from 'node:fs';
649
+ import { dirname, join } from 'node:path';
650
+ import { fileURLToPath } from 'node:url';
651
+ let d = '';
652
+ process.stdin.on('data', c => d += c);
653
+ process.stdin.on('end', () => {
654
+ try {
655
+ const p = JSON.parse(d).tool_input.file_path;
656
+ if (p) {
657
+ const dir = dirname(fileURLToPath(import.meta.url));
658
+ appendFileSync(join(dir, '.tracked-files'), p + '\\n');
659
+ }
660
+ } catch {}
661
+ });
662
+ `;
663
+ const scriptPath = (0, import_node_path.join)(cwd, ".pawl", "track-file.mjs");
664
+ (0, import_node_fs.writeFileSync)(scriptPath, script, "utf-8");
665
+ }
419
666
  function mergeGeminiSettings(cwd) {
420
667
  const geminiDir = (0, import_node_path.join)(cwd, ".gemini");
421
668
  (0, import_node_fs.mkdirSync)(geminiDir, { recursive: true });
@@ -428,12 +675,19 @@ function mergeGeminiSettings(cwd) {
428
675
  }
429
676
  }
430
677
  const hooksToInstall = {
431
- SessionStart: { command: ".pawl/sync.sh pull" },
432
- AfterAgentLoop: { command: ".pawl/sync.sh push" }
678
+ SessionStart: { command: "node .pawl/sync.mjs pull 2>/dev/null || .pawl/sync.sh pull" },
679
+ AfterAgentLoop: { command: "node .pawl/sync.mjs push 2>/dev/null || .pawl/sync.sh push" }
433
680
  };
681
+ const oldCommands = [".pawl/sync.sh pull", ".pawl/sync.sh push"];
434
682
  const hooksObj = settings.hooks || {};
435
683
  for (const [event, entry] of Object.entries(hooksToInstall)) {
436
- const existing = hooksObj[event] || [];
684
+ let existing = hooksObj[event] || [];
685
+ existing = existing.map((e) => {
686
+ if (oldCommands.includes(e.command)) {
687
+ return { ...e, command: entry.command };
688
+ }
689
+ return e;
690
+ });
437
691
  const alreadyExists = existing.some((e) => e.command === entry.command);
438
692
  if (!alreadyExists) {
439
693
  existing.push(entry);
@@ -451,7 +705,9 @@ function writeGitHook(cwd, hasGitDir) {
451
705
  const hookBlock = `#!/bin/sh
452
706
  # Pawl sync hook \u2014 fires after every commit
453
707
  # Provides universal sync fallback for agents without lifecycle hooks (Codex, etc.)
454
- if [ -f ".pawl/sync.sh" ]; then
708
+ if command -v node >/dev/null 2>&1 && [ -f ".pawl/sync.mjs" ]; then
709
+ node .pawl/sync.mjs push
710
+ elif [ -f ".pawl/sync.sh" ]; then
455
711
  .pawl/sync.sh push
456
712
  fi`;
457
713
  if (!(0, import_node_fs.existsSync)(hookPath)) {
@@ -459,8 +715,20 @@ fi`;
459
715
  (0, import_node_fs.chmodSync)(hookPath, 493);
460
716
  return;
461
717
  }
462
- const content = (0, import_node_fs.readFileSync)(hookPath, "utf-8");
718
+ let content = (0, import_node_fs.readFileSync)(hookPath, "utf-8");
463
719
  if (content.includes("Pawl sync hook")) {
720
+ const markerStart = content.indexOf("# Pawl sync hook");
721
+ if (markerStart !== -1) {
722
+ let blockStart = markerStart;
723
+ const before = content.lastIndexOf("\n", markerStart - 1);
724
+ const fiIdx = content.indexOf("\nfi", markerStart);
725
+ if (fiIdx !== -1) {
726
+ const blockEnd = fiIdx + "\nfi".length;
727
+ content = content.slice(0, blockStart) + hookBlock.slice(hookBlock.indexOf("# Pawl sync hook")) + content.slice(blockEnd);
728
+ (0, import_node_fs.writeFileSync)(hookPath, content, "utf-8");
729
+ (0, import_node_fs.chmodSync)(hookPath, 493);
730
+ }
731
+ }
464
732
  return;
465
733
  }
466
734
  (0, import_node_fs.writeFileSync)(hookPath, content.trimEnd() + "\n\n" + hookBlock + "\n", "utf-8");
@@ -550,7 +818,9 @@ function printSummary(detected, hasKey) {
550
818
  }
551
819
  console.log(" Codex / other no lifecycle hooks \u2014 git post-commit fallback active");
552
820
  console.log("\n Files written:");
553
- console.log(" .pawl/sync.sh");
821
+ console.log(" .pawl/sync.mjs (Node.js sync \u2014 no curl/jq needed)");
822
+ console.log(" .pawl/track-file.mjs (Node.js file tracker)");
823
+ console.log(" .pawl/sync.sh (bash fallback)");
554
824
  console.log(" .pawl/parse-cc-session.js");
555
825
  console.log(" CLAUDE.md (pawl context block updated)");
556
826
  console.log(" AGENTS.md (pawl context block \u2014 readable by Codex, Kiro, and others)");
@@ -769,7 +1039,7 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
769
1039
  SessionStart: [
770
1040
  {
771
1041
  hooks: [
772
- { type: "command", command: `${basePath}/sync.sh pull` }
1042
+ { type: "command", command: `node ${basePath}/sync.mjs pull 2>/dev/null || ${basePath}/sync.sh pull` }
773
1043
  ]
774
1044
  }
775
1045
  ],
@@ -779,7 +1049,7 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
779
1049
  hooks: [
780
1050
  {
781
1051
  type: "command",
782
- command: `jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
1052
+ command: `node ${basePath}/track-file.mjs 2>/dev/null || jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
783
1053
  }
784
1054
  ]
785
1055
  }
@@ -787,18 +1057,30 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
787
1057
  Stop: [
788
1058
  {
789
1059
  hooks: [
790
- { type: "command", command: `${basePath}/sync.sh push` }
1060
+ { type: "command", command: `node ${basePath}/sync.mjs push 2>/dev/null || ${basePath}/sync.sh push` }
791
1061
  ]
792
1062
  }
793
1063
  ]
794
1064
  };
1065
+ const oldPatterns = [
1066
+ `${basePath}/sync.sh pull`,
1067
+ `${basePath}/sync.sh push`,
1068
+ `jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
1069
+ ];
795
1070
  const hooksObj = settings.hooks || {};
796
1071
  for (const [event, entries] of Object.entries(hooks)) {
797
- const existing = hooksObj[event] || [];
1072
+ let existing = hooksObj[event] || [];
798
1073
  for (const entry of entries) {
799
- const cmd = entry.hooks[0].command;
1074
+ const newCmd = entry.hooks[0].command;
1075
+ existing = existing.map((e) => {
1076
+ const existingCmd = e.hooks?.[0]?.command;
1077
+ if (existingCmd && oldPatterns.includes(existingCmd)) {
1078
+ return { ...e, hooks: [{ ...e.hooks[0], command: newCmd }] };
1079
+ }
1080
+ return e;
1081
+ });
800
1082
  const alreadyExists = existing.some(
801
- (e) => e.hooks?.[0]?.command === cmd
1083
+ (e) => e.hooks?.[0]?.command === newCmd
802
1084
  );
803
1085
  if (!alreadyExists) {
804
1086
  existing.push(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpawl/setup",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "type": "commonjs",
5
5
  "description": "One-shot setup for Pawl + Claude Code hooks",
6
6
  "bin": {