@codebyplan/cli 2.0.1 → 2.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +400 -192
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,6 +71,17 @@ claude mcp add codebyplan -e CODEBYPLAN_API_KEY=your_key_here -- npx -y @codebyp
71
71
  |------|-------------|
72
72
  | `health_check` | Check server version, API connectivity, and latency |
73
73
 
74
+ ### Settings File Model
75
+
76
+ The `sync_claude_files` tool writes a `settings.json` file to the project's `.claude/` directory. This file contains shared settings (hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories) merged from global and repo-specific scopes in the database.
77
+
78
+ A separate `settings.local.json` file (not managed by sync) holds local-only configuration: `permissions.allow` and MCP server definitions. Claude Code merges both files at runtime, with `settings.local.json` taking precedence.
79
+
80
+ | File | Managed By | Contains |
81
+ |------|------------|----------|
82
+ | `settings.json` | `sync_claude_files` (synced from DB) | Shared settings: hooks, statusLine, attribution, permissions.deny/ask/additionalDirectories |
83
+ | `settings.local.json` | User (local-only) | Machine-specific: permissions.allow, MCP server config |
84
+
74
85
  ## Environment Variables
75
86
 
76
87
  | Variable | Required | Description |
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ var VERSION, PACKAGE_NAME;
37
37
  var init_version = __esm({
38
38
  "src/lib/version.ts"() {
39
39
  "use strict";
40
- VERSION = "2.0.1";
40
+ VERSION = "2.1.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -49,7 +49,52 @@ __export(setup_exports, {
49
49
  });
50
50
  import { createInterface } from "node:readline/promises";
51
51
  import { stdin, stdout } from "node:process";
52
- import { execFile } from "node:child_process";
52
+ import { readFile, writeFile } from "node:fs/promises";
53
+ import { homedir } from "node:os";
54
+ import { join } from "node:path";
55
+ function getConfigPath(scope) {
56
+ return scope === "user" ? join(homedir(), ".claude.json") : join(process.cwd(), ".mcp.json");
57
+ }
58
+ async function readConfig(path) {
59
+ try {
60
+ const raw = await readFile(path, "utf-8");
61
+ const parsed = JSON.parse(raw);
62
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
63
+ return parsed;
64
+ }
65
+ return {};
66
+ } catch {
67
+ return {};
68
+ }
69
+ }
70
+ function buildMcpEntry(apiKey) {
71
+ return {
72
+ command: "npx",
73
+ args: ["-y", PACKAGE_NAME],
74
+ env: { CODEBYPLAN_API_KEY: apiKey }
75
+ };
76
+ }
77
+ async function writeMcpConfig(scope, apiKey) {
78
+ const configPath = getConfigPath(scope);
79
+ const config2 = await readConfig(configPath);
80
+ if (typeof config2.mcpServers !== "object" || config2.mcpServers === null || Array.isArray(config2.mcpServers)) {
81
+ config2.mcpServers = {};
82
+ }
83
+ config2.mcpServers.codebyplan = buildMcpEntry(apiKey);
84
+ await writeFile(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
85
+ return configPath;
86
+ }
87
+ async function verifyMcpConfig(scope, apiKey) {
88
+ try {
89
+ const config2 = await readConfig(getConfigPath(scope));
90
+ const servers = config2.mcpServers;
91
+ if (!servers) return false;
92
+ const entry = servers.codebyplan;
93
+ return entry?.env?.CODEBYPLAN_API_KEY === apiKey;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
53
98
  async function runSetup() {
54
99
  const rl = createInterface({ input: stdin, output: stdout });
55
100
  console.log("\n CodeByPlan MCP Server Setup\n");
@@ -76,53 +121,38 @@ async function runSetup() {
76
121
  console.log(` Warning: API returned status ${res.status}, but continuing.
77
122
  `);
78
123
  } else {
79
- console.log(" API key is valid!\n");
80
- }
81
- const addCmd = `claude mcp add --scope user codebyplan -e CODEBYPLAN_API_KEY=${apiKey} -- npx -y ${PACKAGE_NAME}`;
82
- console.log(" Run this command to add the MCP server to Claude Code:\n");
83
- console.log(` ${addCmd}
84
- `);
85
- const autoAdd = (await rl.question(" Run it now? (Y/n): ")).trim().toLowerCase();
86
- if (autoAdd === "" || autoAdd === "y" || autoAdd === "yes") {
87
- console.log("\n Adding MCP server to Claude Code...\n");
88
- await new Promise((resolve2) => {
89
- execFile("claude", ["mcp", "remove", "--scope", "user", "codebyplan"], () => resolve2());
90
- });
91
- await new Promise((resolve2) => {
92
- execFile(
93
- "claude",
94
- [
95
- "mcp",
96
- "add",
97
- "--scope",
98
- "user",
99
- "codebyplan",
100
- "-e",
101
- `CODEBYPLAN_API_KEY=${apiKey}`,
102
- "--",
103
- "npx",
104
- "-y",
105
- PACKAGE_NAME
106
- ],
107
- (err, _stdout, stderr) => {
108
- if (err) {
109
- console.log(` Could not run 'claude mcp add' automatically.`);
110
- console.log(` Error: ${stderr || err.message}`);
111
- console.log(`
112
- Run it manually:
113
- ${addCmd}
124
+ try {
125
+ const body = await res.json();
126
+ if (Array.isArray(body.data) && body.data.length === 0) {
127
+ console.log(" API key is valid but no repositories found.");
128
+ console.log(" Create one at https://codebyplan.com after setup.\n");
129
+ } else {
130
+ console.log(" API key is valid!\n");
131
+ }
132
+ } catch {
133
+ console.log(" API key is valid!\n");
134
+ }
135
+ }
136
+ console.log(" Where should the MCP server be configured?\n");
137
+ console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
138
+ console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
139
+ const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
140
+ const scope = scopeInput === "2" ? "project" : "user";
141
+ console.log("\n Configuring MCP server...");
142
+ const configPath = await writeMcpConfig(scope, apiKey);
143
+ const verified = await verifyMcpConfig(scope, apiKey);
144
+ if (verified) {
145
+ console.log(` Done! Config written to ${configPath}
114
146
  `);
115
- resolve2();
116
- } else {
117
- console.log(" Done! CodeByPlan MCP server is now configured.\n");
118
- console.log(" Start a new Claude Code session to begin using it.\n");
119
- resolve2();
120
- }
121
- }
122
- );
123
- });
147
+ if (scope === "project") {
148
+ console.log(" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n");
149
+ }
150
+ console.log(" Start a new Claude Code session to begin using it.\n");
124
151
  } else {
125
- console.log("\n Run the command above when you're ready.\n");
152
+ console.log(" Warning: Could not verify the saved configuration.\n");
153
+ console.log(" You can configure manually by adding to your Claude config:\n");
154
+ console.log(` claude mcp add codebyplan -e CODEBYPLAN_API_KEY=${apiKey} -- npx -y ${PACKAGE_NAME}
155
+ `);
126
156
  }
127
157
  } finally {
128
158
  rl.close();
@@ -136,8 +166,8 @@ var init_setup = __esm({
136
166
  });
137
167
 
138
168
  // src/cli/config.ts
139
- import { readFile } from "node:fs/promises";
140
- import { join } from "node:path";
169
+ import { readFile as readFile2 } from "node:fs/promises";
170
+ import { join as join2 } from "node:path";
141
171
  function parseFlags(startIndex) {
142
172
  const flags = {};
143
173
  const args = process.argv.slice(startIndex);
@@ -159,8 +189,8 @@ async function resolveConfig(flags) {
159
189
  let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
160
190
  if (!repoId || !worktreeId) {
161
191
  try {
162
- const configPath = join(projectPath, ".codebyplan.json");
163
- const raw = await readFile(configPath, "utf-8");
192
+ const configPath = join2(projectPath, ".codebyplan.json");
193
+ const raw = await readFile2(configPath, "utf-8");
164
194
  const config2 = JSON.parse(raw);
165
195
  if (!repoId) repoId = config2.repo_id;
166
196
  if (!worktreeId) worktreeId = config2.worktree_id;
@@ -354,7 +384,36 @@ function mergeSettings(template, local) {
354
384
  }
355
385
  return merged;
356
386
  }
357
- var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS;
387
+ function mergeGlobalAndRepoSettings(global, repo) {
388
+ const merged = { ...global, ...repo };
389
+ const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
390
+ const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
391
+ if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
392
+ const mergedPerms = { ...globalPerms, ...repoPerms };
393
+ for (const key of ARRAY_PERMISSION_KEYS) {
394
+ const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
395
+ const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
396
+ if (globalArr.length > 0 || repoArr.length > 0) {
397
+ mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
398
+ }
399
+ }
400
+ merged.permissions = mergedPerms;
401
+ }
402
+ return merged;
403
+ }
404
+ function stripPermissionsAllow(settings) {
405
+ if (!settings.permissions || typeof settings.permissions !== "object") {
406
+ return settings;
407
+ }
408
+ const perms = { ...settings.permissions };
409
+ delete perms.allow;
410
+ if (Object.keys(perms).length === 0) {
411
+ const { permissions: _, ...rest } = settings;
412
+ return rest;
413
+ }
414
+ return { ...settings, permissions: perms };
415
+ }
416
+ var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
358
417
  var init_settings_merge = __esm({
359
418
  "src/lib/settings-merge.ts"() {
360
419
  "use strict";
@@ -368,12 +427,13 @@ var init_settings_merge = __esm({
368
427
  "ask",
369
428
  "additionalDirectories"
370
429
  ];
430
+ ARRAY_PERMISSION_KEYS = ["deny", "ask"];
371
431
  }
372
432
  });
373
433
 
374
434
  // src/lib/hook-registry.ts
375
- import { readdir, readFile as readFile2 } from "node:fs/promises";
376
- import { join as join2 } from "node:path";
435
+ import { readdir, readFile as readFile3 } from "node:fs/promises";
436
+ import { join as join3 } from "node:path";
377
437
  function parseHookMeta(content) {
378
438
  const match = content.match(/^#\s*@hook:\s*(\S+)(?:\s+(.+))?$/m);
379
439
  if (!match) return null;
@@ -392,7 +452,7 @@ async function discoverHooks(hooksDir) {
392
452
  return discovered;
393
453
  }
394
454
  for (const filename of filenames) {
395
- const content = await readFile2(join2(hooksDir, filename), "utf-8");
455
+ const content = await readFile3(join3(hooksDir, filename), "utf-8");
396
456
  const meta = parseHookMeta(content);
397
457
  if (meta) {
398
458
  discovered.set(filename.replace(/\.sh$/, ""), meta);
@@ -430,6 +490,25 @@ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hook
430
490
  }
431
491
  return merged;
432
492
  }
493
+ function stripDiscoveredHooks(config2, hooksRelPath = ".claude/hooks") {
494
+ const prefix = `bash ${hooksRelPath}/`;
495
+ const stripped = {};
496
+ for (const [event, matchers] of Object.entries(config2)) {
497
+ const filteredMatchers = [];
498
+ for (const matcher of matchers) {
499
+ const filteredHooks = matcher.hooks.filter(
500
+ (h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
501
+ );
502
+ if (filteredHooks.length > 0) {
503
+ filteredMatchers.push({ matcher: matcher.matcher, hooks: filteredHooks });
504
+ }
505
+ }
506
+ if (filteredMatchers.length > 0) {
507
+ stripped[event] = filteredMatchers;
508
+ }
509
+ }
510
+ return stripped;
511
+ }
433
512
  var init_hook_registry = __esm({
434
513
  "src/lib/hook-registry.ts"() {
435
514
  "use strict";
@@ -478,38 +557,38 @@ var init_variables = __esm({
478
557
  });
479
558
 
480
559
  // src/lib/sync-engine.ts
481
- import { readdir as readdir2, readFile as readFile3, writeFile, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
482
- import { join as join3, dirname } from "node:path";
560
+ import { readdir as readdir2, readFile as readFile4, writeFile as writeFile2, unlink, mkdir, rmdir, chmod, stat } from "node:fs/promises";
561
+ import { join as join4, dirname } from "node:path";
483
562
  function getTypeDir(claudeDir, dir) {
484
- if (dir === "commands") return join3(claudeDir, dir, "cbp");
485
- return join3(claudeDir, dir);
563
+ if (dir === "commands") return join4(claudeDir, dir, "cbp");
564
+ return join4(claudeDir, dir);
486
565
  }
487
566
  function getFilePath(claudeDir, typeName, file) {
488
567
  const cfg = typeConfig[typeName];
489
568
  const typeDir = getTypeDir(claudeDir, cfg.dir);
490
569
  if (cfg.subfolder) {
491
- return join3(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
570
+ return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
492
571
  }
493
572
  if (typeName === "command" && file.category) {
494
- return join3(typeDir, file.category, `${file.name}${cfg.ext}`);
573
+ return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
495
574
  }
496
575
  if (typeName === "template") {
497
- return join3(typeDir, file.name);
576
+ return join4(typeDir, file.name);
498
577
  }
499
- return join3(typeDir, `${file.name}${cfg.ext}`);
578
+ return join4(typeDir, `${file.name}${cfg.ext}`);
500
579
  }
501
580
  async function readDirRecursive(dir, base = dir) {
502
581
  const result = /* @__PURE__ */ new Map();
503
582
  try {
504
583
  const entries = await readdir2(dir, { withFileTypes: true });
505
584
  for (const entry of entries) {
506
- const fullPath = join3(dir, entry.name);
585
+ const fullPath = join4(dir, entry.name);
507
586
  if (entry.isDirectory()) {
508
587
  const sub = await readDirRecursive(fullPath, base);
509
588
  for (const [k, v] of sub) result.set(k, v);
510
589
  } else {
511
590
  const relPath = fullPath.slice(base.length + 1);
512
- const fileContent = await readFile3(fullPath, "utf-8");
591
+ const fileContent = await readFile4(fullPath, "utf-8");
513
592
  result.set(relPath, fileContent);
514
593
  }
515
594
  }
@@ -519,7 +598,7 @@ async function readDirRecursive(dir, base = dir) {
519
598
  }
520
599
  async function isGitWorktree(projectPath) {
521
600
  try {
522
- const gitPath = join3(projectPath, ".git");
601
+ const gitPath = join4(projectPath, ".git");
523
602
  const info = await stat(gitPath);
524
603
  return info.isFile();
525
604
  } catch {
@@ -546,7 +625,7 @@ async function executeSyncToLocal(options) {
546
625
  const syncData = syncRes.data;
547
626
  const repoData = repoRes.data;
548
627
  syncData.claude_md = [];
549
- const claudeDir = join3(projectPath, ".claude");
628
+ const claudeDir = join4(projectPath, ".claude");
550
629
  const worktree = await isGitWorktree(projectPath);
551
630
  const byType = {};
552
631
  const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
@@ -571,19 +650,19 @@ async function executeSyncToLocal(options) {
571
650
  remotePathMap.set(relPath, { content: substituted, name: remote.name });
572
651
  }
573
652
  for (const [relPath, { content, name }] of remotePathMap) {
574
- const fullPath = join3(targetDir, relPath);
653
+ const fullPath = join4(targetDir, relPath);
575
654
  const localContent = localFiles.get(relPath);
576
655
  if (localContent === void 0) {
577
656
  if (!dryRun) {
578
657
  await mkdir(dirname(fullPath), { recursive: true });
579
- await writeFile(fullPath, content, "utf-8");
658
+ await writeFile2(fullPath, content, "utf-8");
580
659
  if (typeName === "hook") await chmod(fullPath, 493);
581
660
  }
582
661
  result.created.push(name);
583
662
  totals.created++;
584
663
  } else if (localContent !== content) {
585
664
  if (!dryRun) {
586
- await writeFile(fullPath, content, "utf-8");
665
+ await writeFile2(fullPath, content, "utf-8");
587
666
  if (typeName === "hook") await chmod(fullPath, 493);
588
667
  }
589
668
  result.updated.push(name);
@@ -595,7 +674,7 @@ async function executeSyncToLocal(options) {
595
674
  }
596
675
  for (const [relPath] of localFiles) {
597
676
  if (!remotePathMap.has(relPath)) {
598
- const fullPath = join3(targetDir, relPath);
677
+ const fullPath = join4(targetDir, relPath);
599
678
  if (!dryRun) {
600
679
  await unlink(fullPath);
601
680
  await removeEmptyParents(fullPath, targetDir);
@@ -607,9 +686,65 @@ async function executeSyncToLocal(options) {
607
686
  }
608
687
  byType[`${typeName}s`] = result;
609
688
  }
689
+ {
690
+ const typeName = "docs_stack";
691
+ const syncKey = "docs_stack";
692
+ const targetDir = join4(projectPath, "docs", "stack");
693
+ const remoteFiles = syncData[syncKey] ?? [];
694
+ const result = { created: [], updated: [], deleted: [], unchanged: [] };
695
+ if (remoteFiles.length > 0 && !dryRun) {
696
+ await mkdir(targetDir, { recursive: true });
697
+ }
698
+ const localFiles = await readDirRecursive(targetDir);
699
+ const remotePathMap = /* @__PURE__ */ new Map();
700
+ for (const remote of remoteFiles) {
701
+ const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
702
+ const substituted = substituteVariables(remote.content, repoData);
703
+ remotePathMap.set(relPath, { content: substituted, name: `${remote.category ?? ""}/${remote.name}` });
704
+ }
705
+ for (const [relPath, { content, name }] of remotePathMap) {
706
+ const fullPath = join4(targetDir, relPath);
707
+ const localContent = localFiles.get(relPath);
708
+ if (localContent === void 0) {
709
+ if (!dryRun) {
710
+ await mkdir(dirname(fullPath), { recursive: true });
711
+ await writeFile2(fullPath, content, "utf-8");
712
+ }
713
+ result.created.push(name);
714
+ totals.created++;
715
+ } else if (localContent !== content) {
716
+ if (!dryRun) {
717
+ await writeFile2(fullPath, content, "utf-8");
718
+ }
719
+ result.updated.push(name);
720
+ totals.updated++;
721
+ } else {
722
+ result.unchanged.push(name);
723
+ totals.unchanged++;
724
+ }
725
+ }
726
+ for (const [relPath] of localFiles) {
727
+ if (!remotePathMap.has(relPath)) {
728
+ const fullPath = join4(targetDir, relPath);
729
+ if (!dryRun) {
730
+ await unlink(fullPath);
731
+ await removeEmptyParents(fullPath, targetDir);
732
+ }
733
+ result.deleted.push(relPath);
734
+ totals.deleted++;
735
+ }
736
+ }
737
+ byType[typeName] = result;
738
+ }
739
+ const globalSettingsFiles = syncData.global_settings ?? [];
740
+ let globalSettings = {};
741
+ for (const gf of globalSettingsFiles) {
742
+ const parsed = JSON.parse(substituteVariables(gf.content, repoData));
743
+ globalSettings = { ...globalSettings, ...parsed };
744
+ }
610
745
  const specialTypes = {
611
- claude_md: () => join3(projectPath, "CLAUDE.md"),
612
- settings: () => join3(projectPath, ".claude", "settings.local.json")
746
+ claude_md: () => join4(projectPath, "CLAUDE.md"),
747
+ settings: () => join4(projectPath, ".claude", "settings.json")
613
748
  };
614
749
  for (const [typeName, getPath] of Object.entries(specialTypes)) {
615
750
  const remoteFiles = syncData[typeName] ?? [];
@@ -619,29 +754,32 @@ async function executeSyncToLocal(options) {
619
754
  const remoteContent = substituteVariables(remote.content, repoData);
620
755
  let localContent;
621
756
  try {
622
- localContent = await readFile3(targetPath, "utf-8");
757
+ localContent = await readFile4(targetPath, "utf-8");
623
758
  } catch {
624
759
  }
625
760
  if (typeName === "settings") {
626
- const templateSettings = JSON.parse(remoteContent);
627
- const hooksDir = join3(projectPath, ".claude", "hooks");
761
+ const repoSettings = JSON.parse(remoteContent);
762
+ const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
763
+ const hooksDir = join4(projectPath, ".claude", "hooks");
628
764
  const discovered = await discoverHooks(hooksDir);
629
765
  if (localContent === void 0) {
766
+ let finalSettings = stripPermissionsAllow(combinedTemplate);
630
767
  if (discovered.size > 0) {
631
- templateSettings.hooks = mergeDiscoveredHooks(
632
- templateSettings.hooks ?? {},
768
+ finalSettings.hooks = mergeDiscoveredHooks(
769
+ finalSettings.hooks ?? {},
633
770
  discovered
634
771
  );
635
772
  }
636
773
  if (!dryRun) {
637
774
  await mkdir(dirname(targetPath), { recursive: true });
638
- await writeFile(targetPath, JSON.stringify(templateSettings, null, 2) + "\n", "utf-8");
775
+ await writeFile2(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
639
776
  }
640
777
  result.created.push(remote.name);
641
778
  totals.created++;
642
779
  } else {
643
780
  const localSettings = JSON.parse(localContent);
644
- const merged = mergeSettings(templateSettings, localSettings);
781
+ let merged = mergeSettings(combinedTemplate, localSettings);
782
+ merged = stripPermissionsAllow(merged);
645
783
  if (discovered.size > 0) {
646
784
  merged.hooks = mergeDiscoveredHooks(
647
785
  merged.hooks ?? {},
@@ -651,7 +789,7 @@ async function executeSyncToLocal(options) {
651
789
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
652
790
  if (localContent !== mergedContent) {
653
791
  if (!dryRun) {
654
- await writeFile(targetPath, mergedContent, "utf-8");
792
+ await writeFile2(targetPath, mergedContent, "utf-8");
655
793
  }
656
794
  result.updated.push(remote.name);
657
795
  totals.updated++;
@@ -664,13 +802,13 @@ async function executeSyncToLocal(options) {
664
802
  if (localContent === void 0) {
665
803
  if (!dryRun) {
666
804
  await mkdir(dirname(targetPath), { recursive: true });
667
- await writeFile(targetPath, remoteContent, "utf-8");
805
+ await writeFile2(targetPath, remoteContent, "utf-8");
668
806
  }
669
807
  result.created.push(remote.name);
670
808
  totals.created++;
671
809
  } else if (localContent !== remoteContent) {
672
810
  if (!dryRun) {
673
- await writeFile(targetPath, remoteContent, "utf-8");
811
+ await writeFile2(targetPath, remoteContent, "utf-8");
674
812
  }
675
813
  result.updated.push(remote.name);
676
814
  totals.updated++;
@@ -734,8 +872,8 @@ var init_confirm = __esm({
734
872
  });
735
873
 
736
874
  // src/lib/tech-detect.ts
737
- import { readFile as readFile4, access } from "node:fs/promises";
738
- import { join as join4 } from "node:path";
875
+ import { readFile as readFile5, access } from "node:fs/promises";
876
+ import { join as join5 } from "node:path";
739
877
  async function fileExists(filePath) {
740
878
  try {
741
879
  await access(filePath);
@@ -747,7 +885,7 @@ async function fileExists(filePath) {
747
885
  async function detectTechStack(projectPath) {
748
886
  const seen = /* @__PURE__ */ new Map();
749
887
  try {
750
- const raw = await readFile4(join4(projectPath, "package.json"), "utf-8");
888
+ const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
751
889
  const pkg = JSON.parse(raw);
752
890
  const allDeps = {
753
891
  ...pkg.dependencies,
@@ -766,7 +904,7 @@ async function detectTechStack(projectPath) {
766
904
  }
767
905
  for (const { file, rule } of CONFIG_FILE_MAP) {
768
906
  const key = rule.name.toLowerCase();
769
- if (!seen.has(key) && await fileExists(join4(projectPath, file))) {
907
+ if (!seen.has(key) && await fileExists(join5(projectPath, file))) {
770
908
  seen.set(key, { name: rule.name, category: rule.category });
771
909
  }
772
910
  }
@@ -1022,19 +1160,20 @@ var init_pull = __esm({
1022
1160
  });
1023
1161
 
1024
1162
  // src/cli/fileMapper.ts
1025
- import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
1026
- import { join as join5, extname } from "node:path";
1163
+ import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
1164
+ import { join as join6, extname } from "node:path";
1027
1165
  function compositeKey(type, name, category) {
1028
1166
  return category ? `${type}:${category}/${name}` : `${type}:${name}`;
1029
1167
  }
1030
- async function scanLocalFiles(claudeDir) {
1168
+ async function scanLocalFiles(claudeDir, projectPath) {
1031
1169
  const result = /* @__PURE__ */ new Map();
1032
- await scanCommands(join5(claudeDir, "commands", "cbp"), result);
1033
- await scanSubfolderType(join5(claudeDir, "agents"), "agent", "AGENT.md", result);
1034
- await scanSubfolderType(join5(claudeDir, "skills"), "skill", "SKILL.md", result);
1035
- await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
1036
- await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
1037
- await scanTemplates(join5(claudeDir, "templates"), result);
1170
+ await scanCommands(join6(claudeDir, "commands", "cbp"), result);
1171
+ await scanSubfolderType(join6(claudeDir, "agents"), "agent", "AGENT.md", result);
1172
+ await scanSubfolderType(join6(claudeDir, "skills"), "skill", "SKILL.md", result);
1173
+ await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
1174
+ await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
1175
+ await scanTemplates(join6(claudeDir, "templates"), result);
1176
+ await scanSettings(claudeDir, projectPath, result);
1038
1177
  return result;
1039
1178
  }
1040
1179
  async function scanCommands(dir, result) {
@@ -1049,10 +1188,10 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
1049
1188
  }
1050
1189
  for (const entry of entries) {
1051
1190
  if (entry.isDirectory()) {
1052
- await scanCommandsRecursive(baseDir, join5(currentDir, entry.name), result);
1191
+ await scanCommandsRecursive(baseDir, join6(currentDir, entry.name), result);
1053
1192
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1054
1193
  const name = entry.name.slice(0, -3);
1055
- const content = await readFile5(join5(currentDir, entry.name), "utf-8");
1194
+ const content = await readFile6(join6(currentDir, entry.name), "utf-8");
1056
1195
  const relDir = currentDir.slice(baseDir.length + 1);
1057
1196
  const category = relDir || null;
1058
1197
  const key = compositeKey("command", name, category);
@@ -1069,9 +1208,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
1069
1208
  }
1070
1209
  for (const entry of entries) {
1071
1210
  if (entry.isDirectory()) {
1072
- const filePath = join5(dir, entry.name, fileName);
1211
+ const filePath = join6(dir, entry.name, fileName);
1073
1212
  try {
1074
- const content = await readFile5(filePath, "utf-8");
1213
+ const content = await readFile6(filePath, "utf-8");
1075
1214
  const key = compositeKey(type, entry.name, null);
1076
1215
  result.set(key, { type, name: entry.name, category: null, content });
1077
1216
  } catch {
@@ -1089,7 +1228,7 @@ async function scanFlatType(dir, type, ext, result) {
1089
1228
  for (const entry of entries) {
1090
1229
  if (entry.isFile() && entry.name.endsWith(ext)) {
1091
1230
  const name = entry.name.slice(0, -ext.length);
1092
- const content = await readFile5(join5(dir, entry.name), "utf-8");
1231
+ const content = await readFile6(join6(dir, entry.name), "utf-8");
1093
1232
  const key = compositeKey(type, name, null);
1094
1233
  result.set(key, { type, name, category: null, content });
1095
1234
  }
@@ -1104,15 +1243,49 @@ async function scanTemplates(dir, result) {
1104
1243
  }
1105
1244
  for (const entry of entries) {
1106
1245
  if (entry.isFile() && extname(entry.name)) {
1107
- const content = await readFile5(join5(dir, entry.name), "utf-8");
1246
+ const content = await readFile6(join6(dir, entry.name), "utf-8");
1108
1247
  const key = compositeKey("template", entry.name, null);
1109
1248
  result.set(key, { type: "template", name: entry.name, category: null, content });
1110
1249
  }
1111
1250
  }
1112
1251
  }
1252
+ async function scanSettings(claudeDir, projectPath, result) {
1253
+ const settingsPath = join6(claudeDir, "settings.json");
1254
+ let raw;
1255
+ try {
1256
+ raw = await readFile6(settingsPath, "utf-8");
1257
+ } catch {
1258
+ return;
1259
+ }
1260
+ let parsed;
1261
+ try {
1262
+ parsed = JSON.parse(raw);
1263
+ } catch {
1264
+ return;
1265
+ }
1266
+ parsed = stripPermissionsAllow(parsed);
1267
+ if (parsed.hooks && typeof parsed.hooks === "object") {
1268
+ const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
1269
+ const discovered = await discoverHooks(hooksDir);
1270
+ if (discovered.size > 0) {
1271
+ parsed.hooks = stripDiscoveredHooks(
1272
+ parsed.hooks,
1273
+ ".claude/hooks"
1274
+ );
1275
+ if (Object.keys(parsed.hooks).length === 0) {
1276
+ delete parsed.hooks;
1277
+ }
1278
+ }
1279
+ }
1280
+ const content = JSON.stringify(parsed, null, 2) + "\n";
1281
+ const key = compositeKey("settings", "settings", null);
1282
+ result.set(key, { type: "settings", name: "settings", category: null, content });
1283
+ }
1113
1284
  var init_fileMapper = __esm({
1114
1285
  "src/cli/fileMapper.ts"() {
1115
1286
  "use strict";
1287
+ init_settings_merge();
1288
+ init_hook_registry();
1116
1289
  }
1117
1290
  });
1118
1291
 
@@ -1190,11 +1363,12 @@ __export(push_exports, {
1190
1363
  runPush: () => runPush
1191
1364
  });
1192
1365
  import { stat as stat2 } from "node:fs/promises";
1193
- import { join as join6 } from "node:path";
1366
+ import { join as join7 } from "node:path";
1194
1367
  async function runPush() {
1195
1368
  const flags = parseFlags(3);
1196
1369
  const dryRun = hasFlag("dry-run", 3);
1197
1370
  const force = hasFlag("force", 3);
1371
+ const isGlobal = hasFlag("global", 3);
1198
1372
  validateApiKey();
1199
1373
  const config2 = await resolveConfig(flags);
1200
1374
  const { repoId, projectPath } = config2;
@@ -1205,7 +1379,7 @@ async function runPush() {
1205
1379
  if (dryRun) console.log(` Mode: dry-run (no changes will be made)`);
1206
1380
  if (force) console.log(` Mode: force (no conflict prompts)`);
1207
1381
  console.log();
1208
- const claudeDir = join6(projectPath, ".claude");
1382
+ const claudeDir = join7(projectPath, ".claude");
1209
1383
  try {
1210
1384
  await stat2(claudeDir);
1211
1385
  } catch {
@@ -1213,7 +1387,7 @@ async function runPush() {
1213
1387
  return;
1214
1388
  }
1215
1389
  console.log(" Scanning local files...");
1216
- const localFiles = await scanLocalFiles(claudeDir);
1390
+ const localFiles = await scanLocalFiles(claudeDir, projectPath);
1217
1391
  console.log(` Found ${localFiles.size} local files.`);
1218
1392
  console.log(" Fetching remote state...");
1219
1393
  const [syncRes, repoRes] = await Promise.all([
@@ -1302,7 +1476,8 @@ async function runPush() {
1302
1476
  type: f.type,
1303
1477
  name: f.name,
1304
1478
  category: f.category,
1305
- content: f.content
1479
+ content: f.content,
1480
+ ...f.type === "settings" ? { scope: isGlobal ? "global" : "repo" } : {}
1306
1481
  })),
1307
1482
  delete_keys: toDelete
1308
1483
  });
@@ -1336,7 +1511,8 @@ function flattenSyncData(data) {
1336
1511
  skills: "skill",
1337
1512
  rules: "rule",
1338
1513
  hooks: "hook",
1339
- templates: "template"
1514
+ templates: "template",
1515
+ settings: "settings"
1340
1516
  };
1341
1517
  for (const [syncKey, typeName] of Object.entries(typeMap)) {
1342
1518
  const files = data[syncKey] ?? [];
@@ -1377,8 +1553,8 @@ __export(init_exports, {
1377
1553
  });
1378
1554
  import { createInterface as createInterface4 } from "node:readline/promises";
1379
1555
  import { stdin as stdin4, stdout as stdout4 } from "node:process";
1380
- import { writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1381
- import { join as join7, dirname as dirname2 } from "node:path";
1556
+ import { writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
1557
+ import { join as join8, dirname as dirname2 } from "node:path";
1382
1558
  async function runInit() {
1383
1559
  const flags = parseFlags(3);
1384
1560
  const projectPath = flags["path"] ?? process.cwd();
@@ -1417,27 +1593,27 @@ async function runInit() {
1417
1593
  if (match) worktreeId = match.id;
1418
1594
  } catch {
1419
1595
  }
1420
- const configPath = join7(projectPath, ".codebyplan.json");
1596
+ const configPath = join8(projectPath, ".codebyplan.json");
1421
1597
  const configData = { repo_id: repoId };
1422
1598
  if (worktreeId) configData.worktree_id = worktreeId;
1423
1599
  const configContent = JSON.stringify(configData, null, 2) + "\n";
1424
- await writeFile2(configPath, configContent, "utf-8");
1600
+ await writeFile3(configPath, configContent, "utf-8");
1425
1601
  console.log(` Created ${configPath}`);
1426
1602
  const seedAnswer = (await rl.question("\n Seed with CodeByPlan defaults? (Y/n): ")).trim().toLowerCase();
1427
1603
  if (seedAnswer === "" || seedAnswer === "y" || seedAnswer === "yes") {
1428
1604
  let getFilePath3 = function(typeName, file) {
1429
1605
  const cfg = typeConfig2[typeName];
1430
- const typeDir = typeName === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
1606
+ const typeDir = typeName === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
1431
1607
  if (cfg.subfolder) {
1432
- return join7(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
1608
+ return join8(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
1433
1609
  }
1434
1610
  if (typeName === "command" && file.category) {
1435
- return join7(typeDir, file.category, `${file.name}${cfg.ext}`);
1611
+ return join8(typeDir, file.category, `${file.name}${cfg.ext}`);
1436
1612
  }
1437
1613
  if (typeName === "template") {
1438
- return join7(typeDir, file.name);
1614
+ return join8(typeDir, file.name);
1439
1615
  }
1440
- return join7(typeDir, `${file.name}${cfg.ext}`);
1616
+ return join8(typeDir, `${file.name}${cfg.ext}`);
1441
1617
  };
1442
1618
  var getFilePath2 = getFilePath3;
1443
1619
  console.log("\n Fetching default files...");
@@ -1452,7 +1628,7 @@ async function runInit() {
1452
1628
  printNextSteps(projectPath);
1453
1629
  return;
1454
1630
  }
1455
- const claudeDir = join7(projectPath, ".claude");
1631
+ const claudeDir = join8(projectPath, ".claude");
1456
1632
  let written = 0;
1457
1633
  const typeConfig2 = {
1458
1634
  command: { dir: "commands", ext: ".md" },
@@ -1475,7 +1651,7 @@ async function runInit() {
1475
1651
  for (const file of files) {
1476
1652
  const filePath = getFilePath3(typeName, file);
1477
1653
  await mkdir2(dirname2(filePath), { recursive: true });
1478
- await writeFile2(filePath, file.content, "utf-8");
1654
+ await writeFile3(filePath, file.content, "utf-8");
1479
1655
  if (typeName === "hook") await chmod2(filePath, 493);
1480
1656
  written++;
1481
1657
  }
@@ -1484,16 +1660,16 @@ async function runInit() {
1484
1660
  ...defaultsData.claude_md ?? []
1485
1661
  ];
1486
1662
  for (const file of specialFiles) {
1487
- const targetPath = join7(projectPath, "CLAUDE.md");
1663
+ const targetPath = join8(projectPath, "CLAUDE.md");
1488
1664
  await mkdir2(dirname2(targetPath), { recursive: true });
1489
- await writeFile2(targetPath, file.content, "utf-8");
1665
+ await writeFile3(targetPath, file.content, "utf-8");
1490
1666
  written++;
1491
1667
  }
1492
1668
  const settingsFiles = defaultsData.settings ?? [];
1493
1669
  for (const file of settingsFiles) {
1494
- const targetPath = join7(claudeDir, "settings.json");
1670
+ const targetPath = join8(claudeDir, "settings.json");
1495
1671
  await mkdir2(dirname2(targetPath), { recursive: true });
1496
- await writeFile2(targetPath, file.content, "utf-8");
1672
+ await writeFile3(targetPath, file.content, "utf-8");
1497
1673
  written++;
1498
1674
  }
1499
1675
  console.log(` Wrote ${written} files to .claude/
@@ -1516,7 +1692,7 @@ async function runInit() {
1516
1692
  allFiles.push({ type: "claude_md", name: file.name, content: file.content });
1517
1693
  }
1518
1694
  for (const file of settingsFiles) {
1519
- allFiles.push({ type: "settings", name: file.name, content: file.content });
1695
+ allFiles.push({ type: "settings", name: file.name, content: file.content, scope: file.scope ?? "repo" });
1520
1696
  }
1521
1697
  if (allFiles.length > 0) {
1522
1698
  await apiPost("/sync/files", {
@@ -23300,21 +23476,6 @@ function registerReadTools(server) {
23300
23476
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23301
23477
  }
23302
23478
  });
23303
- server.registerTool("get_handoff", {
23304
- description: "Get handoff state for a repo (status, summary, resume command/context).",
23305
- inputSchema: {
23306
- repo_id: external_exports.string().uuid().describe("The repo UUID")
23307
- }
23308
- }, async ({ repo_id }) => {
23309
- try {
23310
- const res = await apiGet(`/repos/${repo_id}`);
23311
- const { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at } = res.data;
23312
- const data = { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at };
23313
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23314
- } catch (err) {
23315
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23316
- }
23317
- });
23318
23479
  server.registerTool("get_sync_status", {
23319
23480
  description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
23320
23481
  inputSchema: {}
@@ -23326,6 +23487,22 @@ function registerReadTools(server) {
23326
23487
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23327
23488
  }
23328
23489
  });
23490
+ server.registerTool("get_next_action", {
23491
+ description: "Compute the next action for a repo based on current workflow state. Returns command, instructions, state, and context.",
23492
+ inputSchema: {
23493
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
23494
+ worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter by assignment")
23495
+ }
23496
+ }, async ({ repo_id, worktree_id }) => {
23497
+ try {
23498
+ const params = {};
23499
+ if (worktree_id) params.worktree_id = worktree_id;
23500
+ const res = await apiGet(`/repos/${repo_id}/next-action`, params);
23501
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23502
+ } catch (err) {
23503
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23504
+ }
23505
+ });
23329
23506
  server.registerTool("get_worktrees", {
23330
23507
  description: "List worktrees for a repo. Optionally filter by status.",
23331
23508
  inputSchema: {
@@ -23434,24 +23611,37 @@ function registerWriteTools(server) {
23434
23611
  description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
23435
23612
  inputSchema: {
23436
23613
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
23437
- title: external_exports.string().describe("Checkpoint title"),
23614
+ title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
23438
23615
  number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
23439
- goal: external_exports.string().optional().describe("Checkpoint goal description"),
23616
+ goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
23440
23617
  deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
23441
23618
  status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
23442
- launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to")
23443
- }
23444
- }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id }) => {
23619
+ launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
23620
+ ideas: external_exports.array(external_exports.object({
23621
+ description: external_exports.string().describe("Idea description"),
23622
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23623
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23624
+ })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23625
+ context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23626
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23627
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23628
+ }
23629
+ }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
23445
23630
  try {
23446
- const res = await apiPost("/checkpoints", {
23631
+ const body = {
23447
23632
  repo_id,
23448
- title,
23633
+ title: title ?? null,
23449
23634
  number: number3,
23450
23635
  goal: goal ?? null,
23451
23636
  deadline: deadline ?? null,
23452
23637
  status: status ?? "pending",
23453
23638
  launch_id: launch_id ?? null
23454
- });
23639
+ };
23640
+ if (ideas !== void 0) body.ideas = ideas;
23641
+ if (context !== void 0) body.context = context;
23642
+ if (research !== void 0) body.research = research;
23643
+ if (qa !== void 0) body.qa = qa;
23644
+ const res = await apiPost("/checkpoints", body);
23455
23645
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23456
23646
  } catch (err) {
23457
23647
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
@@ -23461,16 +23651,24 @@ function registerWriteTools(server) {
23461
23651
  description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
23462
23652
  inputSchema: {
23463
23653
  checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
23464
- title: external_exports.string().optional().describe("New title"),
23465
- goal: external_exports.string().optional().describe("New goal"),
23654
+ title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
23655
+ goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
23466
23656
  status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
23467
23657
  deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
23468
23658
  completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
23469
23659
  launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23470
23660
  worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23471
- assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint")
23472
- }
23473
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to }) => {
23661
+ assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
23662
+ ideas: external_exports.array(external_exports.object({
23663
+ description: external_exports.string().describe("Idea description"),
23664
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23665
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23666
+ })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23667
+ context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23668
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23669
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23670
+ }
23671
+ }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, ideas, context, research, qa }) => {
23474
23672
  const update = {};
23475
23673
  if (title !== void 0) update.title = title;
23476
23674
  if (goal !== void 0) update.goal = goal;
@@ -23480,6 +23678,10 @@ function registerWriteTools(server) {
23480
23678
  if (launch_id !== void 0) update.launch_id = launch_id;
23481
23679
  if (worktree_id !== void 0) update.worktree_id = worktree_id;
23482
23680
  if (assigned_to !== void 0) update.assigned_to = assigned_to;
23681
+ if (ideas !== void 0) update.ideas = ideas;
23682
+ if (context !== void 0) update.context = context;
23683
+ if (research !== void 0) update.research = research;
23684
+ if (qa !== void 0) update.qa = qa;
23483
23685
  if (Object.keys(update).length === 0) {
23484
23686
  return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
23485
23687
  }
@@ -23515,17 +23717,24 @@ function registerWriteTools(server) {
23515
23717
  title: external_exports.string().describe("Task title"),
23516
23718
  number: external_exports.number().int().describe("Task number (e.g. 1 for TASK-1)"),
23517
23719
  requirements: external_exports.string().optional().describe("Task requirements text"),
23518
- status: external_exports.string().optional().describe("Initial status (default: pending)")
23720
+ status: external_exports.string().optional().describe("Initial status (default: pending)"),
23721
+ context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
23722
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
23723
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
23519
23724
  }
23520
- }, async ({ checkpoint_id, title, number: number3, requirements, status }) => {
23725
+ }, async ({ checkpoint_id, title, number: number3, requirements, status, context, qa, research }) => {
23521
23726
  try {
23522
- const res = await apiPost("/tasks", {
23727
+ const body = {
23523
23728
  checkpoint_id,
23524
23729
  title,
23525
23730
  number: number3,
23526
23731
  requirements: requirements ?? null,
23527
23732
  status: status ?? "pending"
23528
- });
23733
+ };
23734
+ if (context !== void 0) body.context = context;
23735
+ if (qa !== void 0) body.qa = qa;
23736
+ if (research !== void 0) body.research = research;
23737
+ const res = await apiPost("/tasks", body);
23529
23738
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23530
23739
  } catch (err) {
23531
23740
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
@@ -23541,17 +23750,25 @@ function registerWriteTools(server) {
23541
23750
  files_changed: external_exports.array(external_exports.object({
23542
23751
  path: external_exports.string().describe("File path relative to repo root"),
23543
23752
  action: external_exports.string().describe("File action (new, modified, deleted)"),
23544
- status: external_exports.string().describe("Approval status (approved, not_approved)")
23753
+ status: external_exports.string().describe("Approval status (approved, not_approved)"),
23754
+ claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
23755
+ user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
23545
23756
  })).optional().describe("Files changed across all rounds"),
23546
- claim_worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress")
23757
+ claim_worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress"),
23758
+ context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
23759
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
23760
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
23547
23761
  }
23548
- }, async ({ task_id, title, requirements, status, files_changed, claim_worktree_id }) => {
23762
+ }, async ({ task_id, title, requirements, status, files_changed, claim_worktree_id, context, qa, research }) => {
23549
23763
  const update = {};
23550
23764
  if (title !== void 0) update.title = title;
23551
23765
  if (requirements !== void 0) update.requirements = requirements;
23552
23766
  if (status !== void 0) update.status = status;
23553
23767
  if (files_changed !== void 0) update.files_changed = files_changed;
23554
23768
  if (claim_worktree_id !== void 0) update.claim_worktree_id = claim_worktree_id;
23769
+ if (context !== void 0) update.context = context;
23770
+ if (qa !== void 0) update.qa = qa;
23771
+ if (research !== void 0) update.research = research;
23555
23772
  if (Object.keys(update).length === 0) {
23556
23773
  return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
23557
23774
  }
@@ -23585,17 +23802,22 @@ function registerWriteTools(server) {
23585
23802
  number: external_exports.number().int().describe("Round number"),
23586
23803
  requirements: external_exports.string().optional().describe("Round requirements text"),
23587
23804
  status: external_exports.string().optional().describe("Initial status (default: pending)"),
23588
- started_at: external_exports.string().optional().describe("Start timestamp (ISO format)")
23805
+ started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
23806
+ context: external_exports.any().optional().describe("Context JSONB"),
23807
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23589
23808
  }
23590
- }, async ({ task_id, number: number3, requirements, status, started_at }) => {
23809
+ }, async ({ task_id, number: number3, requirements, status, started_at, context, qa }) => {
23591
23810
  try {
23592
- const res = await apiPost("/rounds", {
23811
+ const body = {
23593
23812
  task_id,
23594
23813
  number: number3,
23595
23814
  requirements: requirements ?? null,
23596
23815
  status: status ?? "pending",
23597
23816
  started_at: started_at ?? null
23598
- });
23817
+ };
23818
+ if (context !== void 0) body.context = context;
23819
+ if (qa !== void 0) body.qa = qa;
23820
+ const res = await apiPost("/rounds", body);
23599
23821
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23600
23822
  } catch (err) {
23601
23823
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
@@ -23613,10 +23835,14 @@ function registerWriteTools(server) {
23613
23835
  files_changed: external_exports.array(external_exports.object({
23614
23836
  path: external_exports.string().describe("File path relative to repo root"),
23615
23837
  action: external_exports.string().describe("File action (new, modified, deleted)"),
23616
- status: external_exports.string().describe("Approval status (approved, not_approved)")
23617
- })).optional().describe("Files changed in this round with approval status")
23618
- }
23619
- }, async ({ round_id, requirements, status, started_at, completed_at, duration_minutes, files_changed }) => {
23838
+ status: external_exports.string().describe("Approval status (approved, not_approved)"),
23839
+ claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
23840
+ user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
23841
+ })).optional().describe("Files changed in this round with approval status"),
23842
+ context: external_exports.any().optional().describe("Context JSONB"),
23843
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23844
+ }
23845
+ }, async ({ round_id, requirements, status, started_at, completed_at, duration_minutes, files_changed, context, qa }) => {
23620
23846
  const update = {};
23621
23847
  if (requirements !== void 0) update.requirements = requirements;
23622
23848
  if (status !== void 0) update.status = status;
@@ -23624,6 +23850,8 @@ function registerWriteTools(server) {
23624
23850
  if (completed_at !== void 0) update.completed_at = completed_at;
23625
23851
  if (duration_minutes !== void 0) update.duration_minutes = duration_minutes;
23626
23852
  if (files_changed !== void 0) update.files_changed = files_changed;
23853
+ if (context !== void 0) update.context = context;
23854
+ if (qa !== void 0) update.qa = qa;
23627
23855
  if (Object.keys(update).length === 0) {
23628
23856
  return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
23629
23857
  }
@@ -23783,7 +24011,7 @@ function registerWriteTools(server) {
23783
24011
  }
23784
24012
  });
23785
24013
  server.registerTool("sync_claude_files", {
23786
- description: "Sync .claude infrastructure from CodeByPlan DB to local project. Uses aggregated defaults (latest version across all repos) for shared files (commands, agents, skills, rules, hooks, templates). Repo-specific files (CLAUDE.md, settings) are not overwritten.",
24014
+ description: "Sync .claude infrastructure from CodeByPlan DB to local project. Uses aggregated defaults (latest version across all repos) for shared files (commands, agents, skills, rules, hooks, templates, stack docs). Repo-specific files (CLAUDE.md, settings) are not overwritten.",
23787
24015
  inputSchema: {
23788
24016
  repo_id: external_exports.string().uuid().describe("Repository ID to sync files for"),
23789
24017
  project_path: external_exports.string().describe("Absolute path to the project root directory")
@@ -23839,31 +24067,6 @@ function registerWriteTools(server) {
23839
24067
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23840
24068
  }
23841
24069
  });
23842
- server.registerTool("update_handoff", {
23843
- description: "Update handoff state for a repo (status, summary, resume command/context). Automatically sets handoff_updated_at.",
23844
- inputSchema: {
23845
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23846
- status: external_exports.string().optional().describe("Handoff status"),
23847
- summary: external_exports.string().optional().describe("Handoff summary"),
23848
- resume_command: external_exports.string().optional().describe("Resume command"),
23849
- resume_context: external_exports.string().optional().describe("Resume context")
23850
- }
23851
- }, async ({ repo_id, status, summary, resume_command, resume_context }) => {
23852
- const body = {};
23853
- if (status !== void 0) body.status = status;
23854
- if (summary !== void 0) body.summary = summary;
23855
- if (resume_command !== void 0) body.resume_command = resume_command;
23856
- if (resume_context !== void 0) body.resume_context = resume_context;
23857
- if (Object.keys(body).length === 0) {
23858
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
23859
- }
23860
- try {
23861
- const res = await apiPatch(`/repos/${repo_id}/handoff`, body);
23862
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23863
- } catch (err) {
23864
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23865
- }
23866
- });
23867
24070
  server.registerTool("create_worktree", {
23868
24071
  description: "Create a new worktree for a repo.",
23869
24072
  inputSchema: {
@@ -24048,6 +24251,11 @@ if (!process.env.CODEBYPLAN_API_KEY) {
24048
24251
  } catch {
24049
24252
  }
24050
24253
  }
24254
+ if (process.env.CODEBYPLAN_API_KEY?.startsWith("CODEBYPLAN_API_KEY=")) {
24255
+ process.env.CODEBYPLAN_API_KEY = process.env.CODEBYPLAN_API_KEY.slice(
24256
+ "CODEBYPLAN_API_KEY=".length
24257
+ );
24258
+ }
24051
24259
  var arg = process.argv[2];
24052
24260
  if (arg === "--version" || arg === "-v") {
24053
24261
  console.log(VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {