@getpawl/setup 1.1.3 → 1.3.1
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 +62 -13
- package/dist/index.js +300 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,29 +1,78 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @getpawl/setup
|
|
2
2
|
|
|
3
|
-
One-shot setup for [
|
|
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
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
|
|
8
|
+
npm install -g @getpawl/setup
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
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 `.
|
|
16
|
-
2. Generates `.
|
|
17
|
-
3.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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,
|
|
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";
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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");
|
|
@@ -482,6 +750,7 @@ function writePawlClaudeMd(cwd) {
|
|
|
482
750
|
"- `.pawl/context.md` \u2014 project index, spec map, health",
|
|
483
751
|
"- `.pawl/specs/` \u2014 individual spec files (one per feature area)",
|
|
484
752
|
"- `.pawl/progress.md` \u2014 last session summary",
|
|
753
|
+
"- `.pawl/decisions.json` \u2014 architectural decisions log (read before starting work)",
|
|
485
754
|
"",
|
|
486
755
|
"Read `.pawl/context.md` at session start, then load relevant specs from `.pawl/specs/` based on the task.",
|
|
487
756
|
PAWL_END
|
|
@@ -520,6 +789,7 @@ function writeAgentsMd(cwd) {
|
|
|
520
789
|
"- `.pawl/context.md` \u2014 project index, spec map, health overview",
|
|
521
790
|
"- `.pawl/specs/` \u2014 individual spec files (one per feature area)",
|
|
522
791
|
"- `.pawl/progress.md` \u2014 last session summary, recent decisions",
|
|
792
|
+
"- `.pawl/decisions.json` \u2014 architectural decisions log, read before starting work",
|
|
523
793
|
"",
|
|
524
794
|
"**Start here**: Read `.pawl/context.md` first, then load relevant",
|
|
525
795
|
"spec files from `.pawl/specs/` based on the task at hand.",
|
|
@@ -550,7 +820,9 @@ function printSummary(detected, hasKey) {
|
|
|
550
820
|
}
|
|
551
821
|
console.log(" Codex / other no lifecycle hooks \u2014 git post-commit fallback active");
|
|
552
822
|
console.log("\n Files written:");
|
|
553
|
-
console.log(" .pawl/sync.
|
|
823
|
+
console.log(" .pawl/sync.mjs (Node.js sync \u2014 no curl/jq needed)");
|
|
824
|
+
console.log(" .pawl/track-file.mjs (Node.js file tracker)");
|
|
825
|
+
console.log(" .pawl/sync.sh (bash fallback)");
|
|
554
826
|
console.log(" .pawl/parse-cc-session.js");
|
|
555
827
|
console.log(" CLAUDE.md (pawl context block updated)");
|
|
556
828
|
console.log(" AGENTS.md (pawl context block \u2014 readable by Codex, Kiro, and others)");
|
|
@@ -769,7 +1041,7 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
|
|
|
769
1041
|
SessionStart: [
|
|
770
1042
|
{
|
|
771
1043
|
hooks: [
|
|
772
|
-
{ type: "command", command:
|
|
1044
|
+
{ type: "command", command: `node ${basePath}/sync.mjs pull 2>/dev/null || ${basePath}/sync.sh pull` }
|
|
773
1045
|
]
|
|
774
1046
|
}
|
|
775
1047
|
],
|
|
@@ -779,7 +1051,7 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
|
|
|
779
1051
|
hooks: [
|
|
780
1052
|
{
|
|
781
1053
|
type: "command",
|
|
782
|
-
command: `jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
|
|
1054
|
+
command: `node ${basePath}/track-file.mjs 2>/dev/null || jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
|
|
783
1055
|
}
|
|
784
1056
|
]
|
|
785
1057
|
}
|
|
@@ -787,18 +1059,30 @@ function mergeClaudeSettings(cwd, basePath = ".agentmap") {
|
|
|
787
1059
|
Stop: [
|
|
788
1060
|
{
|
|
789
1061
|
hooks: [
|
|
790
|
-
{ type: "command", command:
|
|
1062
|
+
{ type: "command", command: `node ${basePath}/sync.mjs push 2>/dev/null || ${basePath}/sync.sh push` }
|
|
791
1063
|
]
|
|
792
1064
|
}
|
|
793
1065
|
]
|
|
794
1066
|
};
|
|
1067
|
+
const oldPatterns = [
|
|
1068
|
+
`${basePath}/sync.sh pull`,
|
|
1069
|
+
`${basePath}/sync.sh push`,
|
|
1070
|
+
`jq -r '.tool_input.file_path' >> ${basePath}/.tracked-files`
|
|
1071
|
+
];
|
|
795
1072
|
const hooksObj = settings.hooks || {};
|
|
796
1073
|
for (const [event, entries] of Object.entries(hooks)) {
|
|
797
|
-
|
|
1074
|
+
let existing = hooksObj[event] || [];
|
|
798
1075
|
for (const entry of entries) {
|
|
799
|
-
const
|
|
1076
|
+
const newCmd = entry.hooks[0].command;
|
|
1077
|
+
existing = existing.map((e) => {
|
|
1078
|
+
const existingCmd = e.hooks?.[0]?.command;
|
|
1079
|
+
if (existingCmd && oldPatterns.includes(existingCmd)) {
|
|
1080
|
+
return { ...e, hooks: [{ ...e.hooks[0], command: newCmd }] };
|
|
1081
|
+
}
|
|
1082
|
+
return e;
|
|
1083
|
+
});
|
|
800
1084
|
const alreadyExists = existing.some(
|
|
801
|
-
(e) => e.hooks?.[0]?.command ===
|
|
1085
|
+
(e) => e.hooks?.[0]?.command === newCmd
|
|
802
1086
|
);
|
|
803
1087
|
if (!alreadyExists) {
|
|
804
1088
|
existing.push(entry);
|