@codebyplan/cli 2.0.2 → 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 +144 -60
  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.2";
40
+ VERSION = "2.1.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -384,7 +384,36 @@ function mergeSettings(template, local) {
384
384
  }
385
385
  return merged;
386
386
  }
387
- 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;
388
417
  var init_settings_merge = __esm({
389
418
  "src/lib/settings-merge.ts"() {
390
419
  "use strict";
@@ -398,6 +427,7 @@ var init_settings_merge = __esm({
398
427
  "ask",
399
428
  "additionalDirectories"
400
429
  ];
430
+ ARRAY_PERMISSION_KEYS = ["deny", "ask"];
401
431
  }
402
432
  });
403
433
 
@@ -460,6 +490,25 @@ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hook
460
490
  }
461
491
  return merged;
462
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
+ }
463
512
  var init_hook_registry = __esm({
464
513
  "src/lib/hook-registry.ts"() {
465
514
  "use strict";
@@ -687,9 +736,15 @@ async function executeSyncToLocal(options) {
687
736
  }
688
737
  byType[typeName] = result;
689
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
+ }
690
745
  const specialTypes = {
691
746
  claude_md: () => join4(projectPath, "CLAUDE.md"),
692
- settings: () => join4(projectPath, ".claude", "settings.local.json")
747
+ settings: () => join4(projectPath, ".claude", "settings.json")
693
748
  };
694
749
  for (const [typeName, getPath] of Object.entries(specialTypes)) {
695
750
  const remoteFiles = syncData[typeName] ?? [];
@@ -703,25 +758,28 @@ async function executeSyncToLocal(options) {
703
758
  } catch {
704
759
  }
705
760
  if (typeName === "settings") {
706
- const templateSettings = JSON.parse(remoteContent);
761
+ const repoSettings = JSON.parse(remoteContent);
762
+ const combinedTemplate = mergeGlobalAndRepoSettings(globalSettings, repoSettings);
707
763
  const hooksDir = join4(projectPath, ".claude", "hooks");
708
764
  const discovered = await discoverHooks(hooksDir);
709
765
  if (localContent === void 0) {
766
+ let finalSettings = stripPermissionsAllow(combinedTemplate);
710
767
  if (discovered.size > 0) {
711
- templateSettings.hooks = mergeDiscoveredHooks(
712
- templateSettings.hooks ?? {},
768
+ finalSettings.hooks = mergeDiscoveredHooks(
769
+ finalSettings.hooks ?? {},
713
770
  discovered
714
771
  );
715
772
  }
716
773
  if (!dryRun) {
717
774
  await mkdir(dirname(targetPath), { recursive: true });
718
- await writeFile2(targetPath, JSON.stringify(templateSettings, null, 2) + "\n", "utf-8");
775
+ await writeFile2(targetPath, JSON.stringify(finalSettings, null, 2) + "\n", "utf-8");
719
776
  }
720
777
  result.created.push(remote.name);
721
778
  totals.created++;
722
779
  } else {
723
780
  const localSettings = JSON.parse(localContent);
724
- const merged = mergeSettings(templateSettings, localSettings);
781
+ let merged = mergeSettings(combinedTemplate, localSettings);
782
+ merged = stripPermissionsAllow(merged);
725
783
  if (discovered.size > 0) {
726
784
  merged.hooks = mergeDiscoveredHooks(
727
785
  merged.hooks ?? {},
@@ -1107,7 +1165,7 @@ import { join as join6, extname } from "node:path";
1107
1165
  function compositeKey(type, name, category) {
1108
1166
  return category ? `${type}:${category}/${name}` : `${type}:${name}`;
1109
1167
  }
1110
- async function scanLocalFiles(claudeDir) {
1168
+ async function scanLocalFiles(claudeDir, projectPath) {
1111
1169
  const result = /* @__PURE__ */ new Map();
1112
1170
  await scanCommands(join6(claudeDir, "commands", "cbp"), result);
1113
1171
  await scanSubfolderType(join6(claudeDir, "agents"), "agent", "AGENT.md", result);
@@ -1115,6 +1173,7 @@ async function scanLocalFiles(claudeDir) {
1115
1173
  await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
1116
1174
  await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
1117
1175
  await scanTemplates(join6(claudeDir, "templates"), result);
1176
+ await scanSettings(claudeDir, projectPath, result);
1118
1177
  return result;
1119
1178
  }
1120
1179
  async function scanCommands(dir, result) {
@@ -1190,9 +1249,43 @@ async function scanTemplates(dir, result) {
1190
1249
  }
1191
1250
  }
1192
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
+ }
1193
1284
  var init_fileMapper = __esm({
1194
1285
  "src/cli/fileMapper.ts"() {
1195
1286
  "use strict";
1287
+ init_settings_merge();
1288
+ init_hook_registry();
1196
1289
  }
1197
1290
  });
1198
1291
 
@@ -1275,6 +1368,7 @@ async function runPush() {
1275
1368
  const flags = parseFlags(3);
1276
1369
  const dryRun = hasFlag("dry-run", 3);
1277
1370
  const force = hasFlag("force", 3);
1371
+ const isGlobal = hasFlag("global", 3);
1278
1372
  validateApiKey();
1279
1373
  const config2 = await resolveConfig(flags);
1280
1374
  const { repoId, projectPath } = config2;
@@ -1293,7 +1387,7 @@ async function runPush() {
1293
1387
  return;
1294
1388
  }
1295
1389
  console.log(" Scanning local files...");
1296
- const localFiles = await scanLocalFiles(claudeDir);
1390
+ const localFiles = await scanLocalFiles(claudeDir, projectPath);
1297
1391
  console.log(` Found ${localFiles.size} local files.`);
1298
1392
  console.log(" Fetching remote state...");
1299
1393
  const [syncRes, repoRes] = await Promise.all([
@@ -1382,7 +1476,8 @@ async function runPush() {
1382
1476
  type: f.type,
1383
1477
  name: f.name,
1384
1478
  category: f.category,
1385
- content: f.content
1479
+ content: f.content,
1480
+ ...f.type === "settings" ? { scope: isGlobal ? "global" : "repo" } : {}
1386
1481
  })),
1387
1482
  delete_keys: toDelete
1388
1483
  });
@@ -1416,7 +1511,8 @@ function flattenSyncData(data) {
1416
1511
  skills: "skill",
1417
1512
  rules: "rule",
1418
1513
  hooks: "hook",
1419
- templates: "template"
1514
+ templates: "template",
1515
+ settings: "settings"
1420
1516
  };
1421
1517
  for (const [syncKey, typeName] of Object.entries(typeMap)) {
1422
1518
  const files = data[syncKey] ?? [];
@@ -1596,7 +1692,7 @@ async function runInit() {
1596
1692
  allFiles.push({ type: "claude_md", name: file.name, content: file.content });
1597
1693
  }
1598
1694
  for (const file of settingsFiles) {
1599
- 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" });
1600
1696
  }
1601
1697
  if (allFiles.length > 0) {
1602
1698
  await apiPost("/sync/files", {
@@ -23380,21 +23476,6 @@ function registerReadTools(server) {
23380
23476
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23381
23477
  }
23382
23478
  });
23383
- server.registerTool("get_handoff", {
23384
- description: "Get handoff state for a repo (status, summary, resume command/context).",
23385
- inputSchema: {
23386
- repo_id: external_exports.string().uuid().describe("The repo UUID")
23387
- }
23388
- }, async ({ repo_id }) => {
23389
- try {
23390
- const res = await apiGet(`/repos/${repo_id}`);
23391
- const { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at } = res.data;
23392
- const data = { id, name, handoff_status, handoff_summary, handoff_resume_command, handoff_resume_context, handoff_updated_at };
23393
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23394
- } catch (err) {
23395
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23396
- }
23397
- });
23398
23479
  server.registerTool("get_sync_status", {
23399
23480
  description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
23400
23481
  inputSchema: {}
@@ -23406,6 +23487,22 @@ function registerReadTools(server) {
23406
23487
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23407
23488
  }
23408
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
+ });
23409
23506
  server.registerTool("get_worktrees", {
23410
23507
  description: "List worktrees for a repo. Optionally filter by status.",
23411
23508
  inputSchema: {
@@ -23514,27 +23611,33 @@ function registerWriteTools(server) {
23514
23611
  description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
23515
23612
  inputSchema: {
23516
23613
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
23517
- title: external_exports.string().describe("Checkpoint title"),
23614
+ title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
23518
23615
  number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
23519
- goal: external_exports.string().optional().describe("Checkpoint goal description"),
23616
+ goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
23520
23617
  deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
23521
23618
  status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
23522
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[]"),
23523
23625
  context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23524
23626
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23525
23627
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23526
23628
  }
23527
- }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, context, research, qa }) => {
23629
+ }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
23528
23630
  try {
23529
23631
  const body = {
23530
23632
  repo_id,
23531
- title,
23633
+ title: title ?? null,
23532
23634
  number: number3,
23533
23635
  goal: goal ?? null,
23534
23636
  deadline: deadline ?? null,
23535
23637
  status: status ?? "pending",
23536
23638
  launch_id: launch_id ?? null
23537
23639
  };
23640
+ if (ideas !== void 0) body.ideas = ideas;
23538
23641
  if (context !== void 0) body.context = context;
23539
23642
  if (research !== void 0) body.research = research;
23540
23643
  if (qa !== void 0) body.qa = qa;
@@ -23548,19 +23651,24 @@ function registerWriteTools(server) {
23548
23651
  description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
23549
23652
  inputSchema: {
23550
23653
  checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
23551
- title: external_exports.string().optional().describe("New title"),
23552
- 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)"),
23553
23656
  status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
23554
23657
  deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
23555
23658
  completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
23556
23659
  launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23557
23660
  worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23558
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[]"),
23559
23667
  context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23560
23668
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23561
23669
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23562
23670
  }
23563
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, context, research, qa }) => {
23671
+ }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, ideas, context, research, qa }) => {
23564
23672
  const update = {};
23565
23673
  if (title !== void 0) update.title = title;
23566
23674
  if (goal !== void 0) update.goal = goal;
@@ -23570,6 +23678,7 @@ function registerWriteTools(server) {
23570
23678
  if (launch_id !== void 0) update.launch_id = launch_id;
23571
23679
  if (worktree_id !== void 0) update.worktree_id = worktree_id;
23572
23680
  if (assigned_to !== void 0) update.assigned_to = assigned_to;
23681
+ if (ideas !== void 0) update.ideas = ideas;
23573
23682
  if (context !== void 0) update.context = context;
23574
23683
  if (research !== void 0) update.research = research;
23575
23684
  if (qa !== void 0) update.qa = qa;
@@ -23958,31 +24067,6 @@ function registerWriteTools(server) {
23958
24067
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23959
24068
  }
23960
24069
  });
23961
- server.registerTool("update_handoff", {
23962
- description: "Update handoff state for a repo (status, summary, resume command/context). Automatically sets handoff_updated_at.",
23963
- inputSchema: {
23964
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23965
- status: external_exports.string().optional().describe("Handoff status"),
23966
- summary: external_exports.string().optional().describe("Handoff summary"),
23967
- resume_command: external_exports.string().optional().describe("Resume command"),
23968
- resume_context: external_exports.string().optional().describe("Resume context")
23969
- }
23970
- }, async ({ repo_id, status, summary, resume_command, resume_context }) => {
23971
- const body = {};
23972
- if (status !== void 0) body.status = status;
23973
- if (summary !== void 0) body.summary = summary;
23974
- if (resume_command !== void 0) body.resume_command = resume_command;
23975
- if (resume_context !== void 0) body.resume_context = resume_context;
23976
- if (Object.keys(body).length === 0) {
23977
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
23978
- }
23979
- try {
23980
- const res = await apiPatch(`/repos/${repo_id}/handoff`, body);
23981
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23982
- } catch (err) {
23983
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23984
- }
23985
- });
23986
24070
  server.registerTool("create_worktree", {
23987
24071
  description: "Create a new worktree for a repo.",
23988
24072
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "2.0.2",
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": {