@codebyplan/cli 2.0.2 → 2.2.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 +437 -101
  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.2.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: {
@@ -23429,63 +23526,232 @@ var init_read = __esm({
23429
23526
  }
23430
23527
  });
23431
23528
 
23432
- // src/lib/auto-push.ts
23529
+ // src/lib/git-pr.ts
23433
23530
  import { exec as execCb } from "node:child_process";
23434
23531
  import { promisify } from "node:util";
23435
- async function autoPushToMain(repoId) {
23436
- let repo;
23532
+ async function createPR(options) {
23533
+ const { repoPath, head, base, title, body } = options;
23437
23534
  try {
23438
- const res = await apiGet(`/repos/${repoId}`);
23439
- repo = res.data;
23440
- } catch {
23535
+ try {
23536
+ const { stdout: existing } = await exec(
23537
+ `gh pr list --head "${head}" --base "${base}" --json number,url --jq '.[0]'`,
23538
+ { cwd: repoPath }
23539
+ );
23540
+ if (existing.trim()) {
23541
+ const pr = JSON.parse(existing.trim());
23542
+ return { pr_url: pr.url, pr_number: pr.number };
23543
+ }
23544
+ } catch {
23545
+ }
23546
+ const { stdout: stdout5 } = await exec(
23547
+ `gh pr create --head "${head}" --base "${base}" --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}"`,
23548
+ { cwd: repoPath }
23549
+ );
23550
+ const prUrl = stdout5.trim();
23551
+ const prNumber = parseInt(prUrl.split("/").pop() ?? "0", 10);
23552
+ return { pr_url: prUrl, pr_number: prNumber || null };
23553
+ } catch (err) {
23554
+ const errorMessage = err instanceof Error ? err.message : String(err);
23555
+ return { pr_url: null, pr_number: null, error: errorMessage };
23556
+ }
23557
+ }
23558
+ async function getPRStatus(repoPath, prNumber) {
23559
+ try {
23560
+ const { stdout: stdout5 } = await exec(
23561
+ `gh pr view ${prNumber} --json state,mergeable,title,url,number`,
23562
+ { cwd: repoPath }
23563
+ );
23564
+ const pr = JSON.parse(stdout5.trim());
23441
23565
  return {
23442
- pushed: false,
23443
- message: "Repo not found",
23444
- error: "Repo not found"
23566
+ state: pr.state,
23567
+ mergeable: pr.mergeable,
23568
+ title: pr.title,
23569
+ url: pr.url,
23570
+ number: pr.number
23445
23571
  };
23446
- }
23447
- if (!repo.auto_push_enabled) {
23572
+ } catch (err) {
23573
+ const errorMessage = err instanceof Error ? err.message : String(err);
23448
23574
  return {
23449
- pushed: false,
23450
- message: "Auto-push is not enabled for this repo"
23575
+ state: "UNKNOWN",
23576
+ mergeable: "UNKNOWN",
23577
+ title: "",
23578
+ url: "",
23579
+ number: prNumber,
23580
+ error: errorMessage
23451
23581
  };
23452
23582
  }
23453
- if (!repo.path) {
23583
+ }
23584
+ var exec;
23585
+ var init_git_pr = __esm({
23586
+ "src/lib/git-pr.ts"() {
23587
+ "use strict";
23588
+ exec = promisify(execCb);
23589
+ }
23590
+ });
23591
+
23592
+ // src/lib/promotion.ts
23593
+ async function promoteCheckpoint(checkpointId) {
23594
+ try {
23595
+ const checkpointRes = await apiGet(`/checkpoints/${checkpointId}`);
23596
+ const checkpoint = checkpointRes.data;
23597
+ if (!checkpoint.branch_name) {
23598
+ return {
23599
+ promoted: false,
23600
+ pr_url: null,
23601
+ pr_number: null,
23602
+ checklist_id: null,
23603
+ message: "Checkpoint has no branch_name set",
23604
+ error: "No branch_name on checkpoint"
23605
+ };
23606
+ }
23607
+ const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
23608
+ const repo = repoRes.data;
23609
+ if (!repo.path) {
23610
+ return {
23611
+ promoted: false,
23612
+ pr_url: null,
23613
+ pr_number: null,
23614
+ checklist_id: null,
23615
+ message: "Repo path not configured",
23616
+ error: "Repo path not configured"
23617
+ };
23618
+ }
23619
+ const baseBranch = repo.git_branch ?? "development";
23620
+ const chkNumber = checkpoint.number.toString().padStart(3, "0");
23621
+ const checklist = await createChecklistFromTemplates(
23622
+ checkpoint.repo_id,
23623
+ checkpointId,
23624
+ "feat_to_development",
23625
+ `CHK-${chkNumber}: ${checkpoint.title ?? "Untitled"} \u2192 ${baseBranch}`
23626
+ );
23627
+ const prResult = await createPR({
23628
+ repoPath: repo.path,
23629
+ head: checkpoint.branch_name,
23630
+ base: baseBranch,
23631
+ title: `CHK-${chkNumber}: ${checkpoint.title ?? "Checkpoint completion"}`,
23632
+ body: `## Checkpoint CHK-${chkNumber}
23633
+
23634
+ **Goal**: ${checkpoint.goal ?? "N/A"}
23635
+
23636
+ Automatically created by CodeByPlan promotion engine.`
23637
+ });
23638
+ if (checklist && prResult.pr_url) {
23639
+ await apiPut(`/merge-checklists/${checklist.id}`, {
23640
+ pr_url: prResult.pr_url,
23641
+ pr_number: prResult.pr_number,
23642
+ status: "in_progress"
23643
+ });
23644
+ }
23645
+ return {
23646
+ promoted: true,
23647
+ pr_url: prResult.pr_url,
23648
+ pr_number: prResult.pr_number,
23649
+ checklist_id: checklist?.id ?? null,
23650
+ message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}`
23651
+ };
23652
+ } catch (err) {
23653
+ const errorMessage = err instanceof Error ? err.message : String(err);
23454
23654
  return {
23455
- pushed: false,
23456
- message: "Repo path is not configured",
23457
- error: "Repo path is not configured"
23655
+ promoted: false,
23656
+ pr_url: null,
23657
+ pr_number: null,
23658
+ checklist_id: null,
23659
+ message: "Promotion failed",
23660
+ error: errorMessage
23458
23661
  };
23459
23662
  }
23460
- const sourceBranch = repo.git_branch ?? "development";
23663
+ }
23664
+ async function promoteToMain(repoId) {
23461
23665
  try {
23462
- await exec("git checkout main", { cwd: repo.path });
23463
- await exec(`git merge ${sourceBranch} --ff-only`, { cwd: repo.path });
23464
- await exec("git push origin main", { cwd: repo.path });
23465
- await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
23666
+ const repoRes = await apiGet(`/repos/${repoId}`);
23667
+ const repo = repoRes.data;
23668
+ if (!repo.path) {
23669
+ return {
23670
+ promoted: false,
23671
+ pr_url: null,
23672
+ pr_number: null,
23673
+ checklist_id: null,
23674
+ message: "Repo path not configured",
23675
+ error: "Repo path not configured"
23676
+ };
23677
+ }
23678
+ const sourceBranch = repo.git_branch ?? "development";
23679
+ const prResult = await createPR({
23680
+ repoPath: repo.path,
23681
+ head: sourceBranch,
23682
+ base: "main",
23683
+ title: `Promote ${sourceBranch} \u2192 main`,
23684
+ body: `Promotion from ${sourceBranch} to main.
23685
+
23686
+ Automatically created by CodeByPlan promotion engine.`
23687
+ });
23466
23688
  return {
23467
- pushed: true,
23468
- message: `Merged ${sourceBranch} -> main and pushed to origin`
23689
+ promoted: true,
23690
+ pr_url: prResult.pr_url,
23691
+ pr_number: prResult.pr_number,
23692
+ checklist_id: null,
23693
+ message: prResult.error ? `PR creation issue: ${prResult.error}` : `PR created: ${prResult.pr_url}`
23469
23694
  };
23470
23695
  } catch (err) {
23471
- try {
23472
- await exec(`git checkout ${sourceBranch}`, { cwd: repo.path });
23473
- } catch {
23474
- }
23475
23696
  const errorMessage = err instanceof Error ? err.message : String(err);
23476
23697
  return {
23477
- pushed: false,
23478
- message: "Auto-push failed",
23698
+ promoted: false,
23699
+ pr_url: null,
23700
+ pr_number: null,
23701
+ checklist_id: null,
23702
+ message: "Promotion to main failed",
23479
23703
  error: errorMessage
23480
23704
  };
23481
23705
  }
23482
23706
  }
23483
- var exec;
23484
- var init_auto_push = __esm({
23485
- "src/lib/auto-push.ts"() {
23707
+ async function createChecklistFromTemplates(repoId, checkpointId, branchLevel, title) {
23708
+ try {
23709
+ const templatesRes = await apiGet(
23710
+ `/repos/${repoId}/checklist-templates`,
23711
+ { branch_level: branchLevel }
23712
+ );
23713
+ const templates = templatesRes.data ?? [];
23714
+ const items = templates.map((t) => ({
23715
+ title: t.title,
23716
+ description: t.description,
23717
+ is_required: t.is_required,
23718
+ checked: false,
23719
+ checked_at: null
23720
+ }));
23721
+ const checklistRes = await apiPost(
23722
+ "/merge-checklists",
23723
+ {
23724
+ checkpoint_id: checkpointId,
23725
+ branch_level: branchLevel,
23726
+ title,
23727
+ status: "pending",
23728
+ items
23729
+ }
23730
+ );
23731
+ return checklistRes.data;
23732
+ } catch {
23733
+ try {
23734
+ const checklistRes = await apiPost(
23735
+ "/merge-checklists",
23736
+ {
23737
+ checkpoint_id: checkpointId,
23738
+ branch_level: branchLevel,
23739
+ title,
23740
+ status: "pending",
23741
+ items: []
23742
+ }
23743
+ );
23744
+ return checklistRes.data;
23745
+ } catch {
23746
+ return null;
23747
+ }
23748
+ }
23749
+ }
23750
+ var init_promotion = __esm({
23751
+ "src/lib/promotion.ts"() {
23486
23752
  "use strict";
23487
23753
  init_api();
23488
- exec = promisify(execCb);
23754
+ init_git_pr();
23489
23755
  }
23490
23756
  });
23491
23757
 
@@ -23514,27 +23780,33 @@ function registerWriteTools(server) {
23514
23780
  description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
23515
23781
  inputSchema: {
23516
23782
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
23517
- title: external_exports.string().describe("Checkpoint title"),
23783
+ title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
23518
23784
  number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
23519
- goal: external_exports.string().optional().describe("Checkpoint goal description"),
23785
+ goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
23520
23786
  deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
23521
23787
  status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
23522
23788
  launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
23789
+ ideas: external_exports.array(external_exports.object({
23790
+ description: external_exports.string().describe("Idea description"),
23791
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23792
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23793
+ })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23523
23794
  context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23524
23795
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23525
23796
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23526
23797
  }
23527
- }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, context, research, qa }) => {
23798
+ }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
23528
23799
  try {
23529
23800
  const body = {
23530
23801
  repo_id,
23531
- title,
23802
+ title: title ?? null,
23532
23803
  number: number3,
23533
23804
  goal: goal ?? null,
23534
23805
  deadline: deadline ?? null,
23535
23806
  status: status ?? "pending",
23536
23807
  launch_id: launch_id ?? null
23537
23808
  };
23809
+ if (ideas !== void 0) body.ideas = ideas;
23538
23810
  if (context !== void 0) body.context = context;
23539
23811
  if (research !== void 0) body.research = research;
23540
23812
  if (qa !== void 0) body.qa = qa;
@@ -23548,19 +23820,25 @@ function registerWriteTools(server) {
23548
23820
  description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
23549
23821
  inputSchema: {
23550
23822
  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"),
23823
+ title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
23824
+ goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
23553
23825
  status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
23554
23826
  deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
23555
23827
  completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
23556
23828
  launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23557
23829
  worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23558
23830
  assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
23831
+ branch_name: external_exports.string().nullable().optional().describe("Git branch name for this checkpoint (e.g. feat/CHK-061-git-overhaul)"),
23832
+ ideas: external_exports.array(external_exports.object({
23833
+ description: external_exports.string().describe("Idea description"),
23834
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23835
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23836
+ })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23559
23837
  context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23560
23838
  research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23561
23839
  qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23562
23840
  }
23563
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, context, research, qa }) => {
23841
+ }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, branch_name, ideas, context, research, qa }) => {
23564
23842
  const update = {};
23565
23843
  if (title !== void 0) update.title = title;
23566
23844
  if (goal !== void 0) update.goal = goal;
@@ -23570,6 +23848,8 @@ function registerWriteTools(server) {
23570
23848
  if (launch_id !== void 0) update.launch_id = launch_id;
23571
23849
  if (worktree_id !== void 0) update.worktree_id = worktree_id;
23572
23850
  if (assigned_to !== void 0) update.assigned_to = assigned_to;
23851
+ if (branch_name !== void 0) update.branch_name = branch_name;
23852
+ if (ideas !== void 0) update.ideas = ideas;
23573
23853
  if (context !== void 0) update.context = context;
23574
23854
  if (research !== void 0) update.research = research;
23575
23855
  if (qa !== void 0) update.qa = qa;
@@ -23584,7 +23864,7 @@ function registerWriteTools(server) {
23584
23864
  }
23585
23865
  });
23586
23866
  server.registerTool("complete_checkpoint", {
23587
- description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers auto-push to main if enabled for the repo.",
23867
+ description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers promotion (creates PR from feat branch to development).",
23588
23868
  inputSchema: {
23589
23869
  checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
23590
23870
  }
@@ -23595,8 +23875,13 @@ function registerWriteTools(server) {
23595
23875
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
23596
23876
  });
23597
23877
  const checkpoint = res.data;
23598
- const pushResult = await autoPushToMain(checkpoint.repo_id);
23599
- return { content: [{ type: "text", text: JSON.stringify({ checkpoint, autoPush: pushResult }, null, 2) }] };
23878
+ const featToDevResult = await promoteCheckpoint(checkpoint_id);
23879
+ let devToMainResult = null;
23880
+ const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
23881
+ if (repoRes.data.auto_push_enabled) {
23882
+ devToMainResult = await promoteToMain(checkpoint.repo_id);
23883
+ }
23884
+ return { content: [{ type: "text", text: JSON.stringify({ checkpoint, promotion: { feat_to_development: featToDevResult, development_to_main: devToMainResult } }, null, 2) }] };
23600
23885
  } catch (err) {
23601
23886
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23602
23887
  }
@@ -23958,31 +24243,6 @@ function registerWriteTools(server) {
23958
24243
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23959
24244
  }
23960
24245
  });
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
24246
  server.registerTool("create_worktree", {
23987
24247
  description: "Create a new worktree for a repo.",
23988
24248
  inputSchema: {
@@ -24017,13 +24277,74 @@ function registerWriteTools(server) {
24017
24277
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24018
24278
  }
24019
24279
  });
24280
+ server.registerTool("create_pr", {
24281
+ description: "Create a GitHub PR for a repo. Uses gh CLI.",
24282
+ inputSchema: {
24283
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24284
+ head: external_exports.string().describe("Source branch name"),
24285
+ base: external_exports.string().describe("Target branch name"),
24286
+ title: external_exports.string().describe("PR title"),
24287
+ body: external_exports.string().optional().describe("PR description body")
24288
+ }
24289
+ }, async ({ repo_id, head, base, title, body }) => {
24290
+ try {
24291
+ const repoRes = await apiGet(`/repos/${repo_id}`);
24292
+ const repo = repoRes.data;
24293
+ if (!repo.path) {
24294
+ return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24295
+ }
24296
+ const result = await createPR({
24297
+ repoPath: repo.path,
24298
+ head,
24299
+ base,
24300
+ title,
24301
+ body: body ?? ""
24302
+ });
24303
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24304
+ } catch (err) {
24305
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24306
+ }
24307
+ });
24308
+ server.registerTool("get_pr_status", {
24309
+ description: "Get the status of a GitHub PR by number.",
24310
+ inputSchema: {
24311
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24312
+ pr_number: external_exports.number().int().describe("The PR number")
24313
+ }
24314
+ }, async ({ repo_id, pr_number }) => {
24315
+ try {
24316
+ const repoRes = await apiGet(`/repos/${repo_id}`);
24317
+ const repo = repoRes.data;
24318
+ if (!repo.path) {
24319
+ return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24320
+ }
24321
+ const status = await getPRStatus(repo.path, pr_number);
24322
+ return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
24323
+ } catch (err) {
24324
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24325
+ }
24326
+ });
24327
+ server.registerTool("promote_checkpoint", {
24328
+ description: "Trigger full promotion flow for a checkpoint. Creates merge checklist from templates and GitHub PR (feat branch \u2192 development).",
24329
+ inputSchema: {
24330
+ checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
24331
+ }
24332
+ }, async ({ checkpoint_id }) => {
24333
+ try {
24334
+ const result = await promoteCheckpoint(checkpoint_id);
24335
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24336
+ } catch (err) {
24337
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24338
+ }
24339
+ });
24020
24340
  }
24021
24341
  var init_write = __esm({
24022
24342
  "src/tools/write.ts"() {
24023
24343
  "use strict";
24024
24344
  init_zod();
24025
24345
  init_api();
24026
- init_auto_push();
24346
+ init_promotion();
24347
+ init_git_pr();
24027
24348
  init_sync_engine();
24028
24349
  }
24029
24350
  });
@@ -24033,30 +24354,45 @@ import { exec as execCb2 } from "node:child_process";
24033
24354
  import { promisify as promisify2 } from "node:util";
24034
24355
  function registerFileGenTools(server) {
24035
24356
  server.registerTool("git_commit", {
24036
- description: "Stage files and create a git commit in a repo. If files are specified, stages only those files. Otherwise stages all changes.",
24357
+ description: "Stage files and create a git commit in a repo. If files are specified, stages only those files. Otherwise stages all changes. Optionally verifies the current branch matches branch_name.",
24037
24358
  inputSchema: {
24038
24359
  repo_id: external_exports.string().uuid().describe("The repo UUID"),
24039
24360
  message: external_exports.string().describe("Commit message"),
24040
- files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes.")
24361
+ files: external_exports.array(external_exports.string()).optional().describe("Specific file paths to stage (relative to repo root). If omitted, stages all changes."),
24362
+ branch_name: external_exports.string().optional().describe("Expected branch name. If provided, verifies the repo is on this branch before committing.")
24041
24363
  }
24042
- }, async ({ repo_id, message, files }) => {
24364
+ }, async ({ repo_id, message, files, branch_name }) => {
24043
24365
  try {
24044
24366
  const repoRes = await apiGet(`/repos/${repo_id}`);
24045
24367
  const repo = repoRes.data;
24046
24368
  if (!repo.path) {
24047
24369
  return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
24048
24370
  }
24371
+ if (branch_name) {
24372
+ const { stdout: currentBranch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
24373
+ if (currentBranch.trim() !== branch_name) {
24374
+ return {
24375
+ content: [{
24376
+ type: "text",
24377
+ text: `Error: Expected branch "${branch_name}" but currently on "${currentBranch.trim()}".`
24378
+ }],
24379
+ isError: true
24380
+ };
24381
+ }
24382
+ }
24049
24383
  const addCmd = files && files.length > 0 ? `git add ${files.map((f) => `"${f}"`).join(" ")}` : "git add .";
24050
24384
  await exec2(addCmd, { cwd: repo.path });
24051
24385
  const { stdout: stdout5, stderr } = await exec2(
24052
24386
  `git commit -m "${message.replace(/"/g, '\\"')}"`,
24053
24387
  { cwd: repo.path }
24054
24388
  );
24389
+ const { stdout: branch } = await exec2("git rev-parse --abbrev-ref HEAD", { cwd: repo.path });
24055
24390
  return {
24056
24391
  content: [{
24057
24392
  type: "text",
24058
24393
  text: JSON.stringify({
24059
24394
  status: "committed",
24395
+ branch: branch.trim(),
24060
24396
  output: stdout5.trim(),
24061
24397
  warnings: stderr.trim() || void 0
24062
24398
  }, null, 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebyplan/cli",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {