@codebyplan/cli 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +2787 -920
  2. package/package.json +1 -1
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 = "3.2.0";
40
+ VERSION = "3.3.0";
41
41
  PACKAGE_NAME = "@codebyplan/cli";
42
42
  }
43
43
  });
@@ -71,13 +71,17 @@ async function validateConnectivity() {
71
71
  );
72
72
  }
73
73
  if (!res.ok) {
74
- console.error(`Warning: API returned status ${res.status} during connectivity check`);
74
+ console.error(
75
+ `Warning: API returned status ${res.status} during connectivity check`
76
+ );
75
77
  }
76
78
  } catch (err) {
77
79
  if (err instanceof Error && err.message.includes("Invalid API key")) {
78
80
  throw err;
79
81
  }
80
- console.error("Warning: Could not reach CodeByPlan API. Requests may fail.");
82
+ console.error(
83
+ "Warning: Could not reach CodeByPlan API. Requests may fail."
84
+ );
81
85
  }
82
86
  }
83
87
  function buildUrl(path, params) {
@@ -166,8 +170,8 @@ async function apiPut(path, body) {
166
170
  async function apiPatch(path, body) {
167
171
  return request("PATCH", path, { body });
168
172
  }
169
- async function apiDelete(path) {
170
- await request("DELETE", path);
173
+ async function apiDelete(path, params) {
174
+ await request("DELETE", path, { params });
171
175
  }
172
176
  var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
173
177
  var init_api = __esm({
@@ -175,10 +179,7 @@ var init_api = __esm({
175
179
  "use strict";
176
180
  init_version();
177
181
  API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
178
- BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(
179
- /\/$/,
180
- ""
181
- );
182
+ BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(/\/$/, "");
182
183
  REQUEST_TIMEOUT_MS = 3e4;
183
184
  MAX_RETRIES = 3;
184
185
  BASE_DELAY_MS = 1e3;
@@ -665,7 +666,51 @@ async function executeSyncToLocal(options) {
665
666
  byType[typeName] = result;
666
667
  }
667
668
  if (!dryRun) {
668
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
669
+ await apiPost("/sync/state", {
670
+ repo_id: repoId,
671
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
672
+ was_skipped: false,
673
+ files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
674
+ files_pushed: 0,
675
+ files_pulled: totals.created + totals.updated,
676
+ files_deleted: totals.deleted,
677
+ files_skipped: 0
678
+ });
679
+ const fileRepoUpdates = [];
680
+ const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
681
+ for (const [syncKey] of Object.entries(syncKeyToType)) {
682
+ const remoteFiles = syncData[syncKey] ?? [];
683
+ for (const file of remoteFiles) {
684
+ if (file.id) {
685
+ fileRepoUpdates.push({
686
+ claude_file_id: file.id,
687
+ last_synced_at: syncTimestamp,
688
+ sync_status: "synced"
689
+ });
690
+ }
691
+ }
692
+ }
693
+ for (const typeName of ["claude_md", "settings"]) {
694
+ const remoteFiles = syncData[typeName] ?? [];
695
+ for (const file of remoteFiles) {
696
+ if (file.id) {
697
+ fileRepoUpdates.push({
698
+ claude_file_id: file.id,
699
+ last_synced_at: syncTimestamp,
700
+ sync_status: "synced"
701
+ });
702
+ }
703
+ }
704
+ }
705
+ if (fileRepoUpdates.length > 0) {
706
+ try {
707
+ await apiPost("/sync/file-repos", {
708
+ repo_id: repoId,
709
+ file_repos: fileRepoUpdates
710
+ });
711
+ } catch {
712
+ }
713
+ }
669
714
  }
670
715
  return { byType, totals, dbOnlyFiles };
671
716
  }
@@ -683,7 +728,8 @@ var init_sync_engine = __esm({
683
728
  skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
684
729
  rule: { dir: "rules", ext: ".md" },
685
730
  hook: { dir: "hooks", ext: ".sh" },
686
- template: { dir: "templates", ext: "" }
731
+ template: { dir: "templates", ext: "" },
732
+ context: { dir: "context", ext: ".md" }
687
733
  };
688
734
  syncKeyToType = {
689
735
  commands: "command",
@@ -691,7 +737,8 @@ var init_sync_engine = __esm({
691
737
  skills: "skill",
692
738
  rules: "rule",
693
739
  hooks: "hook",
694
- templates: "template"
740
+ templates: "template",
741
+ contexts: "context"
695
742
  };
696
743
  }
697
744
  });
@@ -1051,58 +1098,176 @@ var init_fileMapper = __esm({
1051
1098
  });
1052
1099
 
1053
1100
  // src/cli/confirm.ts
1101
+ var confirm_exports = {};
1102
+ __export(confirm_exports, {
1103
+ SyncCancelledError: () => SyncCancelledError,
1104
+ confirmEach: () => confirmEach,
1105
+ confirmProceed: () => confirmProceed,
1106
+ promptChoice: () => promptChoice,
1107
+ promptReviewMode: () => promptReviewMode,
1108
+ reviewFilesOneByOne: () => reviewFilesOneByOne,
1109
+ reviewFolder: () => reviewFolder
1110
+ });
1054
1111
  import { createInterface as createInterface2 } from "node:readline/promises";
1055
1112
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
1113
+ function isAbortError(err) {
1114
+ return err instanceof Error && err.code === "ABORT_ERR";
1115
+ }
1056
1116
  async function confirmProceed(message) {
1057
1117
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1058
1118
  try {
1059
- const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1119
+ while (true) {
1120
+ const answer = await rl.question(message ?? " Proceed? [Y/n] ");
1121
+ const a = answer.trim().toLowerCase();
1122
+ if (a === "" || a === "y" || a === "yes") return true;
1123
+ if (a === "n" || a === "no") return false;
1124
+ console.log(` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`);
1125
+ }
1126
+ } catch (err) {
1127
+ if (isAbortError(err)) throw new SyncCancelledError();
1128
+ throw err;
1129
+ } finally {
1130
+ rl.close();
1131
+ }
1132
+ }
1133
+ async function promptChoice(message, options) {
1134
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1135
+ try {
1136
+ const answer = await rl.question(message);
1060
1137
  const a = answer.trim().toLowerCase();
1061
- return a !== "n" && a !== "no";
1138
+ return options.includes(a) ? a : options[0];
1139
+ } catch (err) {
1140
+ if (isAbortError(err)) throw new SyncCancelledError();
1141
+ throw err;
1142
+ } finally {
1143
+ rl.close();
1144
+ }
1145
+ }
1146
+ async function confirmEach(items, label) {
1147
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
1148
+ const accepted = [];
1149
+ try {
1150
+ for (const item of items) {
1151
+ const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
1152
+ const a = answer.trim().toLowerCase();
1153
+ if (a === "a") {
1154
+ accepted.push(item, ...items.slice(items.indexOf(item) + 1));
1155
+ break;
1156
+ }
1157
+ if (a === "y" || a === "yes" || a === "") {
1158
+ accepted.push(item);
1159
+ }
1160
+ }
1161
+ } catch (err) {
1162
+ if (isAbortError(err)) throw new SyncCancelledError();
1163
+ throw err;
1062
1164
  } finally {
1063
1165
  rl.close();
1064
1166
  }
1167
+ return accepted;
1065
1168
  }
1066
- function parseReviewAction(input, fallback) {
1169
+ function parseReviewAction(input) {
1067
1170
  const a = input.trim().toLowerCase();
1068
1171
  switch (a) {
1069
1172
  case "d":
1070
1173
  case "delete":
1071
- return { action: "delete", all: false };
1174
+ return { action: "delete", all: false, special: null };
1072
1175
  case "p":
1073
1176
  case "pull":
1074
- return { action: "pull", all: false };
1177
+ return { action: "pull", all: false, special: null };
1075
1178
  case "s":
1076
1179
  case "push":
1077
- return { action: "push", all: false };
1180
+ return { action: "push", all: false, special: null };
1078
1181
  case "k":
1079
1182
  case "skip":
1080
- return { action: "skip", all: false };
1183
+ return { action: "skip", all: false, special: null };
1081
1184
  case "da":
1082
- return { action: "delete", all: true };
1185
+ return { action: "delete", all: true, special: null };
1083
1186
  case "pa":
1084
- return { action: "pull", all: true };
1187
+ return { action: "pull", all: true, special: null };
1085
1188
  case "sa":
1086
- return { action: "push", all: true };
1189
+ return { action: "push", all: true, special: null };
1087
1190
  case "ka":
1088
- return { action: "skip", all: true };
1191
+ return { action: "skip", all: true, special: null };
1192
+ case "v":
1193
+ case "view":
1194
+ return { action: null, all: false, special: "view" };
1195
+ case "r":
1196
+ case "recommended":
1197
+ return { action: null, all: false, special: "recommended" };
1089
1198
  case "":
1090
- return { action: fallback, all: false };
1199
+ return { action: null, all: false, special: "recommended" };
1200
+ // Enter = recommended
1091
1201
  default:
1092
- return { action: fallback, all: false };
1202
+ return { action: null, all: false, special: null };
1203
+ }
1204
+ }
1205
+ function formatActionPrompt(recommended, includeView, includeRecommended) {
1206
+ const actions = [
1207
+ `[d]elete${recommended === "delete" ? "\u2605" : ""}`,
1208
+ `[p]ull${recommended === "pull" ? "\u2605" : ""}`,
1209
+ `pu[s]h${recommended === "push" ? "\u2605" : ""}`,
1210
+ `s[k]ip${recommended === "skip" ? "\u2605" : ""}`
1211
+ ];
1212
+ if (includeView) actions.push("[v]iew");
1213
+ if (includeRecommended) actions.push("[r]ecommended");
1214
+ return actions.join(" ");
1215
+ }
1216
+ function showDiff(local, remote, displayPath) {
1217
+ console.log(`
1218
+ --- ${displayPath} (diff) ---`);
1219
+ if (local === null && remote !== null) {
1220
+ console.log(" (no local file \u2014 remote content below)");
1221
+ for (const line of remote.split("\n").slice(0, 30)) {
1222
+ console.log(` + ${line}`);
1223
+ }
1224
+ if (remote.split("\n").length > 30) console.log(" ... (truncated)");
1225
+ } else if (local !== null && remote === null) {
1226
+ console.log(" (no remote file \u2014 local content below)");
1227
+ for (const line of local.split("\n").slice(0, 30)) {
1228
+ console.log(` - ${line}`);
1229
+ }
1230
+ if (local.split("\n").length > 30) console.log(" ... (truncated)");
1231
+ } else if (local !== null && remote !== null) {
1232
+ const localLines = local.split("\n");
1233
+ const remoteLines = remote.split("\n");
1234
+ let shown = 0;
1235
+ const maxLines = 40;
1236
+ for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
1237
+ const l = localLines[i];
1238
+ const r = remoteLines[i];
1239
+ if (l === r) {
1240
+ console.log(` ${l ?? ""}`);
1241
+ } else {
1242
+ if (l !== void 0) console.log(` - ${l}`);
1243
+ if (r !== void 0) console.log(` + ${r}`);
1244
+ }
1245
+ shown++;
1246
+ }
1247
+ if (Math.max(localLines.length, remoteLines.length) > maxLines) {
1248
+ console.log(" ... (truncated)");
1249
+ }
1093
1250
  }
1251
+ console.log();
1094
1252
  }
1095
1253
  async function promptReviewMode() {
1096
1254
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1097
1255
  try {
1098
- const answer = await rl.question(" Review [o]ne-by-one or [f]older-by-folder? ");
1099
- const a = answer.trim().toLowerCase();
1100
- return a === "f" || a === "folder" ? "folder" : "file";
1256
+ while (true) {
1257
+ const answer = await rl.question(" Review [o]ne-by-one or [f]older-by-folder? ");
1258
+ const a = answer.trim().toLowerCase();
1259
+ if (a === "o" || a === "one-by-one" || a === "one" || a === "file") return "file";
1260
+ if (a === "f" || a === "folder") return "folder";
1261
+ console.log(` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`);
1262
+ }
1263
+ } catch (err) {
1264
+ if (isAbortError(err)) throw new SyncCancelledError();
1265
+ throw err;
1101
1266
  } finally {
1102
1267
  rl.close();
1103
1268
  }
1104
1269
  }
1105
- async function reviewFilesOneByOne(items, label, plannedAction) {
1270
+ async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
1106
1271
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1107
1272
  const results = [];
1108
1273
  try {
@@ -1113,49 +1278,112 @@ async function reviewFilesOneByOne(items, label, plannedAction) {
1113
1278
  continue;
1114
1279
  }
1115
1280
  const planned = plannedAction(item);
1116
- const answer = await rl.question(
1117
- ` ${label(item)} (${planned}) \u2014 [d]elete [p]ull pu[s]h s[k]ip: `
1118
- );
1119
- const { action, all } = parseReviewAction(answer, planned);
1120
- results.push(action);
1121
- if (all) applyAll = action;
1281
+ const rec = recommendedAction ? recommendedAction(item) : planned;
1282
+ const hasContent = content != null;
1283
+ const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
1284
+ while (true) {
1285
+ const answer = await rl.question(prompt);
1286
+ const result = parseReviewAction(answer);
1287
+ if (result.special === "view") {
1288
+ if (content) {
1289
+ showDiff(content.local(item), content.remote(item), label(item));
1290
+ } else {
1291
+ console.log(" No content available for diff.");
1292
+ }
1293
+ continue;
1294
+ }
1295
+ if (result.special === "recommended") {
1296
+ results.push(rec);
1297
+ break;
1298
+ }
1299
+ if (result.action === null) {
1300
+ console.log(` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`);
1301
+ continue;
1302
+ }
1303
+ results.push(result.action);
1304
+ if (result.all) applyAll = result.action;
1305
+ break;
1306
+ }
1122
1307
  }
1308
+ } catch (err) {
1309
+ if (isAbortError(err)) throw new SyncCancelledError();
1310
+ throw err;
1123
1311
  } finally {
1124
1312
  rl.close();
1125
1313
  }
1126
1314
  return results;
1127
1315
  }
1128
- async function reviewFolder(folderName, items, label, plannedAction) {
1316
+ async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
1129
1317
  console.log(`
1130
1318
  ${folderName} (${items.length} files):`);
1131
1319
  for (const item of items) {
1132
- console.log(` ${label(item)} (${plannedAction(item)})`);
1320
+ const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
1321
+ const actionLabel = plannedAction(item);
1322
+ const star = actionLabel === rec ? "\u2605" : "";
1323
+ console.log(` ${label(item)} (${actionLabel}${star})`);
1133
1324
  }
1134
1325
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1135
- let answer;
1136
1326
  try {
1137
- answer = await rl.question(
1138
- ` Action for all: [d]elete [p]ull pu[s]h s[k]ip [o]ne-by-one: `
1139
- );
1327
+ while (true) {
1328
+ const promptStr = ` Action for all: ${formatActionPrompt(
1329
+ recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1330
+ false,
1331
+ true
1332
+ )} [o]ne-by-one: `;
1333
+ const answer = await rl.question(promptStr);
1334
+ const a = answer.trim().toLowerCase();
1335
+ if (a === "o" || a === "one-by-one") {
1336
+ rl.close();
1337
+ return reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content);
1338
+ }
1339
+ if (a === "r" || a === "recommended") {
1340
+ return items.map(
1341
+ (item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
1342
+ );
1343
+ }
1344
+ if (a === "v" || a === "view") {
1345
+ if (content) {
1346
+ for (const item of items) {
1347
+ showDiff(content.local(item), content.remote(item), label(item));
1348
+ }
1349
+ } else {
1350
+ console.log(" No content available for diff.");
1351
+ }
1352
+ continue;
1353
+ }
1354
+ const result = parseReviewAction(a);
1355
+ if (result.action !== null) {
1356
+ return items.map(() => result.action);
1357
+ }
1358
+ console.log(` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
1359
+ recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
1360
+ false,
1361
+ true
1362
+ )} [o]ne-by-one`);
1363
+ }
1364
+ } catch (err) {
1365
+ if (isAbortError(err)) throw new SyncCancelledError();
1366
+ throw err;
1140
1367
  } finally {
1141
1368
  rl.close();
1142
1369
  }
1143
- const a = answer.trim().toLowerCase();
1144
- if (a === "o" || a === "one-by-one") {
1145
- return reviewFilesOneByOne(items, label, plannedAction);
1146
- }
1147
- const { action } = parseReviewAction(a, "skip");
1148
- return items.map(() => action);
1149
1370
  }
1371
+ var SyncCancelledError;
1150
1372
  var init_confirm = __esm({
1151
1373
  "src/cli/confirm.ts"() {
1152
1374
  "use strict";
1375
+ SyncCancelledError = class extends Error {
1376
+ constructor() {
1377
+ super("Sync cancelled");
1378
+ this.name = "SyncCancelledError";
1379
+ }
1380
+ };
1153
1381
  }
1154
1382
  });
1155
1383
 
1156
1384
  // src/lib/tech-detect.ts
1157
1385
  import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
1158
- import { join as join6 } from "node:path";
1386
+ import { join as join6, relative } from "node:path";
1159
1387
  async function fileExists(filePath) {
1160
1388
  try {
1161
1389
  await access(filePath);
@@ -1335,7 +1563,69 @@ function parseTechStackResult(raw) {
1335
1563
  const flat = parseTechStack(raw);
1336
1564
  return { repo: flat, apps: [], flat };
1337
1565
  }
1338
- var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP;
1566
+ function categorizeDependency(depName) {
1567
+ const rule = PACKAGE_MAP[depName];
1568
+ if (rule) return rule.category;
1569
+ for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
1570
+ if (depName.startsWith(prefix)) return prefixRule.category;
1571
+ }
1572
+ return "other";
1573
+ }
1574
+ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
1575
+ if (depth > 4) return [];
1576
+ const results = [];
1577
+ const pkgPath = join6(dir, "package.json");
1578
+ if (await fileExists(pkgPath)) {
1579
+ results.push(pkgPath);
1580
+ }
1581
+ try {
1582
+ const entries = await readdir4(dir, { withFileTypes: true });
1583
+ for (const entry of entries) {
1584
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
1585
+ const subResults = await findPackageJsonFiles(
1586
+ join6(dir, entry.name),
1587
+ projectPath,
1588
+ depth + 1
1589
+ );
1590
+ results.push(...subResults);
1591
+ }
1592
+ } catch {
1593
+ }
1594
+ return results;
1595
+ }
1596
+ async function scanAllDependencies(projectPath) {
1597
+ const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
1598
+ const dependencies = [];
1599
+ for (const pkgPath of packageJsonPaths) {
1600
+ try {
1601
+ const raw = await readFile6(pkgPath, "utf-8");
1602
+ const pkg = JSON.parse(raw);
1603
+ const sourcePath = relative(projectPath, pkgPath);
1604
+ const depSections = [
1605
+ { deps: pkg.dependencies, depType: "production", isDev: false },
1606
+ { deps: pkg.devDependencies, depType: "dev", isDev: true },
1607
+ { deps: pkg.peerDependencies, depType: "peer", isDev: false },
1608
+ { deps: pkg.optionalDependencies, depType: "optional", isDev: false }
1609
+ ];
1610
+ for (const { deps, depType, isDev } of depSections) {
1611
+ if (!deps) continue;
1612
+ for (const [name, version2] of Object.entries(deps)) {
1613
+ dependencies.push({
1614
+ name,
1615
+ version: version2,
1616
+ category: categorizeDependency(name),
1617
+ source_path: sourcePath,
1618
+ is_dev: isDev,
1619
+ dep_type: depType
1620
+ });
1621
+ }
1622
+ }
1623
+ } catch {
1624
+ }
1625
+ }
1626
+ return { dependencies };
1627
+ }
1628
+ var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP, SKIP_DIRS;
1339
1629
  var init_tech_detect = __esm({
1340
1630
  "src/lib/tech-detect.ts"() {
1341
1631
  "use strict";
@@ -1441,6 +1731,18 @@ var init_tech_detect = __esm({
1441
1731
  { file: "nx.json", rule: { name: "Nx", category: "build" } },
1442
1732
  { file: "lerna.json", rule: { name: "Lerna", category: "build" } }
1443
1733
  ];
1734
+ SKIP_DIRS = /* @__PURE__ */ new Set([
1735
+ "node_modules",
1736
+ ".next",
1737
+ "dist",
1738
+ ".turbo",
1739
+ ".git",
1740
+ "coverage",
1741
+ "build",
1742
+ "out",
1743
+ ".vercel",
1744
+ ".expo"
1745
+ ]);
1444
1746
  }
1445
1747
  });
1446
1748
 
@@ -1465,6 +1767,49 @@ async function runSync() {
1465
1767
  if (dryRun) console.log(` Mode: dry-run`);
1466
1768
  if (force) console.log(` Mode: force`);
1467
1769
  console.log();
1770
+ if (!dryRun) {
1771
+ console.log(" Acquiring sync lock...");
1772
+ try {
1773
+ await apiPost("/sync/lock", {
1774
+ repo_id: repoId,
1775
+ locked_by: `cli-sync`,
1776
+ reason: "Bidirectional sync",
1777
+ ttl_minutes: 10
1778
+ });
1779
+ console.log(" Lock acquired.\n");
1780
+ } catch (lockErr) {
1781
+ const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
1782
+ if (lockStatus.data.locked && lockStatus.data.lock) {
1783
+ const lock = lockStatus.data.lock;
1784
+ console.log(` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`);
1785
+ console.log(` Expires: ${lock.expires_at}`);
1786
+ console.log(` Use --force to override, or wait for lock to expire.
1787
+ `);
1788
+ if (!force) return;
1789
+ await apiPost("/sync/lock", {
1790
+ repo_id: repoId,
1791
+ locked_by: `cli-sync`,
1792
+ reason: "Bidirectional sync (forced)",
1793
+ ttl_minutes: 10
1794
+ });
1795
+ console.log(" Lock acquired (forced).\n");
1796
+ } else {
1797
+ throw lockErr;
1798
+ }
1799
+ }
1800
+ }
1801
+ try {
1802
+ await runSyncInner(repoId, projectPath, dryRun, force);
1803
+ } finally {
1804
+ if (!dryRun) {
1805
+ try {
1806
+ await apiDelete("/sync/lock", { repo_id: repoId });
1807
+ } catch {
1808
+ }
1809
+ }
1810
+ }
1811
+ }
1812
+ async function runSyncInner(repoId, projectPath, dryRun, force) {
1468
1813
  console.log(" Reading local and remote state...");
1469
1814
  const claudeDir = join7(projectPath, ".claude");
1470
1815
  let localFiles = /* @__PURE__ */ new Map();
@@ -1472,14 +1817,17 @@ async function runSync() {
1472
1817
  localFiles = await scanLocalFiles(claudeDir, projectPath);
1473
1818
  } catch {
1474
1819
  }
1475
- const [defaultsRes, repoSyncRes, repoRes] = await Promise.all([
1820
+ const [defaultsRes, repoSyncRes, repoRes, syncStateRes] = await Promise.all([
1476
1821
  apiGet("/sync/defaults"),
1477
1822
  apiGet("/sync/files", { repo_id: repoId }),
1478
- apiGet(`/repos/${repoId}`)
1823
+ apiGet(`/repos/${repoId}`),
1824
+ apiGet("/sync/state", { repo_id: repoId })
1479
1825
  ]);
1826
+ const syncStartTime = Date.now();
1480
1827
  const repoData = repoRes.data;
1481
1828
  const remoteDefaults = flattenSyncData(defaultsRes.data);
1482
1829
  const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
1830
+ const syncState = syncStateRes.data;
1483
1831
  const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
1484
1832
  console.log(` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
1485
1833
  `);
@@ -1493,6 +1841,7 @@ async function runSync() {
1493
1841
  key,
1494
1842
  displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1495
1843
  action: "push",
1844
+ recommended: "push",
1496
1845
  localContent: local.content,
1497
1846
  remoteContent: null,
1498
1847
  pushContent: reverseSubstituteVariables(local.content, repoData),
@@ -1500,14 +1849,19 @@ async function runSync() {
1500
1849
  type: local.type,
1501
1850
  name: local.name,
1502
1851
  category: local.category,
1503
- isHook: local.type === "hook"
1852
+ isHook: local.type === "hook",
1853
+ claudeFileId: null
1504
1854
  });
1505
1855
  } else if (!local && remote) {
1506
1856
  const resolvedContent = substituteVariables(remote.content, repoData);
1857
+ const isDefaultOnly = remoteDefaults.has(key) && !remoteRepoFiles.has(key);
1858
+ const hasSyncedBefore = syncState?.last_synced_at != null;
1859
+ const recommended = !isDefaultOnly && hasSyncedBefore ? "delete" : "pull";
1507
1860
  plan.push({
1508
1861
  key,
1509
1862
  displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
1510
- action: "pull",
1863
+ action: recommended,
1864
+ recommended,
1511
1865
  localContent: null,
1512
1866
  remoteContent: resolvedContent,
1513
1867
  pushContent: null,
@@ -1515,28 +1869,30 @@ async function runSync() {
1515
1869
  type: remote.type,
1516
1870
  name: remote.name,
1517
1871
  category: remote.category ?? null,
1518
- isHook: remote.type === "hook"
1872
+ isHook: remote.type === "hook",
1873
+ claudeFileId: remote.id ?? null
1519
1874
  });
1520
1875
  } else if (local && remote) {
1521
1876
  const resolvedRemote = substituteVariables(remote.content, repoData);
1522
1877
  if (local.content === resolvedRemote) {
1523
1878
  continue;
1524
1879
  }
1525
- const repoSyncAt = repoRes.data.claude_sync_at;
1880
+ const lastSyncedAt = syncState?.last_synced_at;
1526
1881
  const remoteUpdatedAt = remote.updated_at;
1527
- const remoteChanged = remoteUpdatedAt && repoSyncAt ? new Date(remoteUpdatedAt) > new Date(repoSyncAt) : true;
1882
+ const remoteChanged = remoteUpdatedAt && lastSyncedAt ? new Date(remoteUpdatedAt) > new Date(lastSyncedAt) : true;
1528
1883
  let action;
1529
1884
  if (remoteChanged && force) {
1530
1885
  action = "pull";
1531
1886
  } else if (!remoteChanged) {
1532
1887
  action = "push";
1533
1888
  } else {
1534
- action = "pull";
1889
+ action = "conflict";
1535
1890
  }
1536
1891
  plan.push({
1537
1892
  key,
1538
1893
  displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
1539
1894
  action,
1895
+ recommended: action === "conflict" ? "pull" : action,
1540
1896
  localContent: local.content,
1541
1897
  remoteContent: resolvedRemote,
1542
1898
  pushContent: reverseSubstituteVariables(local.content, repoData),
@@ -1544,14 +1900,17 @@ async function runSync() {
1544
1900
  type: local.type,
1545
1901
  name: local.name,
1546
1902
  category: local.category,
1547
- isHook: local.type === "hook"
1903
+ isHook: local.type === "hook",
1904
+ claudeFileId: remote.id ?? null
1548
1905
  });
1549
1906
  }
1550
1907
  }
1551
1908
  const pulls = plan.filter((p) => p.action === "pull");
1552
1909
  const pushes = plan.filter((p) => p.action === "push");
1553
- const remoteOnly = plan.filter((p) => p.action === "pull" && p.localContent === null);
1910
+ const conflicts = plan.filter((p) => p.action === "conflict");
1554
1911
  const contentPulls = pulls.filter((p) => p.localContent !== null);
1912
+ const dbOnlyPull = plan.filter((p) => p.localContent === null && p.action === "pull");
1913
+ const dbOnlyDelete = plan.filter((p) => p.localContent === null && p.action === "delete");
1555
1914
  if (contentPulls.length > 0) {
1556
1915
  console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
1557
1916
  for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
@@ -1560,12 +1919,22 @@ async function runSync() {
1560
1919
  console.log(` Push (local \u2192 DB): ${pushes.length}`);
1561
1920
  for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
1562
1921
  }
1563
- if (remoteOnly.length > 0) {
1922
+ if (dbOnlyPull.length > 0) {
1923
+ console.log(`
1924
+ DB-only (new, will pull): ${dbOnlyPull.length}`);
1925
+ for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
1926
+ }
1927
+ if (dbOnlyDelete.length > 0) {
1928
+ console.log(`
1929
+ DB-only (previously synced, will delete): ${dbOnlyDelete.length}`);
1930
+ for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
1931
+ }
1932
+ if (conflicts.length > 0) {
1564
1933
  console.log(`
1565
- DB-only (not on disk): ${remoteOnly.length}`);
1566
- for (const p of remoteOnly) console.log(` \u2715 ${p.displayPath}`);
1934
+ Conflicts (both sides changed): ${conflicts.length}`);
1935
+ for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
1567
1936
  }
1568
- if (contentPulls.length === 0 && pushes.length === 0 && remoteOnly.length === 0) {
1937
+ if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
1569
1938
  console.log(" All .claude/ files in sync.");
1570
1939
  }
1571
1940
  if (plan.length > 0 && !dryRun) {
@@ -1574,11 +1943,17 @@ async function runSync() {
1574
1943
  Agree with sync? [Y/n] `);
1575
1944
  if (!agreed) {
1576
1945
  const mode = await promptReviewMode();
1946
+ const contentProvider = {
1947
+ local: (p) => p.localContent,
1948
+ remote: (p) => p.remoteContent
1949
+ };
1577
1950
  if (mode === "file") {
1578
1951
  const actions = await reviewFilesOneByOne(
1579
1952
  plan,
1580
1953
  (p) => p.displayPath,
1581
- (p) => p.action
1954
+ (p) => p.action,
1955
+ (p) => p.recommended,
1956
+ contentProvider
1582
1957
  );
1583
1958
  for (let i = 0; i < plan.length; i++) {
1584
1959
  plan[i].action = actions[i];
@@ -1590,7 +1965,9 @@ async function runSync() {
1590
1965
  typeName,
1591
1966
  items,
1592
1967
  (p) => p.displayPath,
1593
- (p) => p.action
1968
+ (p) => p.action,
1969
+ (p) => p.recommended,
1970
+ contentProvider
1594
1971
  );
1595
1972
  for (let i = 0; i < items.length; i++) {
1596
1973
  items[i].action = actions[i];
@@ -1622,7 +1999,8 @@ async function runSync() {
1622
1999
  if (toUpsert.length > 0) {
1623
2000
  await apiPost("/sync/files", {
1624
2001
  repo_id: repoId,
1625
- files: toUpsert
2002
+ files: toUpsert,
2003
+ changed_by_repo_id: repoId
1626
2004
  });
1627
2005
  }
1628
2006
  if (toDelete.length > 0) {
@@ -1644,7 +2022,64 @@ async function runSync() {
1644
2022
  }
1645
2023
  }
1646
2024
  }
1647
- await apiPut(`/repos/${repoId}`, { claude_sync_at: (/* @__PURE__ */ new Date()).toISOString() });
2025
+ const unresolvedConflicts = plan.filter(
2026
+ (p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
2027
+ );
2028
+ if (unresolvedConflicts.length > 0) {
2029
+ let stored = 0;
2030
+ for (const p of unresolvedConflicts) {
2031
+ if (p.claudeFileId) {
2032
+ try {
2033
+ await apiPost("/sync/conflicts", {
2034
+ repo_id: repoId,
2035
+ claude_file_id: p.claudeFileId,
2036
+ conflict_type: "both_modified",
2037
+ local_content: p.localContent,
2038
+ remote_content: p.remoteContent
2039
+ });
2040
+ stored++;
2041
+ } catch {
2042
+ }
2043
+ }
2044
+ }
2045
+ if (stored > 0) {
2046
+ console.log(`
2047
+ ${stored} conflict(s) stored in DB for later resolution.`);
2048
+ }
2049
+ }
2050
+ const syncDurationMs = Date.now() - syncStartTime;
2051
+ await apiPost("/sync/state", {
2052
+ repo_id: repoId,
2053
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
2054
+ was_skipped: skipped.length > 0,
2055
+ files_synced_count: toPull.length + toPush.length + toDelete.length,
2056
+ files_pushed: toPush.length,
2057
+ files_pulled: toPull.length,
2058
+ files_deleted: toDelete.length,
2059
+ files_skipped: skipped.length,
2060
+ sync_duration_ms: syncDurationMs,
2061
+ sync_version: getSyncVersion()
2062
+ });
2063
+ const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
2064
+ const fileRepoUpdates = [];
2065
+ for (const p of [...toPull, ...toPush]) {
2066
+ if (p.claudeFileId) {
2067
+ fileRepoUpdates.push({
2068
+ claude_file_id: p.claudeFileId,
2069
+ last_synced_at: syncTimestamp,
2070
+ sync_status: "synced"
2071
+ });
2072
+ }
2073
+ }
2074
+ if (fileRepoUpdates.length > 0) {
2075
+ try {
2076
+ await apiPost("/sync/file-repos", {
2077
+ repo_id: repoId,
2078
+ file_repos: fileRepoUpdates
2079
+ });
2080
+ } catch {
2081
+ }
2082
+ }
1648
2083
  console.log(
1649
2084
  `
1650
2085
  Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
@@ -1723,14 +2158,18 @@ async function syncConfig(repoId, projectPath, dryRun) {
1723
2158
  let portAllocations = [];
1724
2159
  try {
1725
2160
  const portsRes = await apiGet(`/port-allocations`, { repo_id: repoId });
1726
- portAllocations = portsRes.data ?? [];
2161
+ const allAllocations = portsRes.data ?? [];
2162
+ const worktreeId2 = currentConfig.worktree_id;
2163
+ portAllocations = worktreeId2 ? allAllocations.filter((a) => a.worktree_id === worktreeId2) : allAllocations.filter((a) => !a.worktree_id);
1727
2164
  } catch {
1728
2165
  }
2166
+ const worktreeId = currentConfig.worktree_id;
2167
+ const matchingAlloc = portAllocations[0];
1729
2168
  const newConfig = {
1730
2169
  repo_id: repoId,
1731
- ...currentConfig.worktree_id ? { worktree_id: currentConfig.worktree_id } : {},
1732
- server_port: repo.server_port,
1733
- server_type: repo.server_type,
2170
+ ...worktreeId ? { worktree_id: worktreeId } : {},
2171
+ server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
2172
+ server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
1734
2173
  git_branch: repo.git_branch ?? "development",
1735
2174
  auto_push_enabled: repo.auto_push_enabled,
1736
2175
  ...portAllocations.length > 0 ? { port_allocations: portAllocations } : {}
@@ -1750,23 +2189,33 @@ async function syncConfig(repoId, projectPath, dryRun) {
1750
2189
  }
1751
2190
  async function syncTechStack(repoId, projectPath, dryRun) {
1752
2191
  try {
1753
- const detected = await detectTechStack(projectPath);
1754
- if (detected.flat.length === 0) {
1755
- console.log(" No tech stack detected.");
2192
+ const { dependencies } = await scanAllDependencies(projectPath);
2193
+ if (dependencies.length === 0) {
2194
+ console.log(" No dependencies found.");
1756
2195
  return;
1757
2196
  }
1758
- const repoRes = await apiGet(`/repos/${repoId}`);
1759
- const remote = parseTechStackResult(repoRes.data.tech_stack);
1760
- const { merged, added } = mergeTechStack(remote, detected);
1761
- console.log(` ${detected.flat.length} detected${added.length > 0 ? ` (${added.length} new)` : ""}`);
1762
- if (detected.apps.length > 0) {
1763
- console.log(` Apps: ${detected.apps.map((a) => a.name).join(", ")}`);
1764
- }
1765
- for (const entry of added) {
1766
- console.log(` + ${entry.name} (${entry.category})`);
2197
+ const sourcePaths = new Set(dependencies.map((d) => d.source_path));
2198
+ console.log(` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`);
2199
+ if (!dryRun) {
2200
+ const result = await apiPost(
2201
+ `/repos/${repoId}/tech-stack`,
2202
+ { dependencies }
2203
+ );
2204
+ if (result.data.stale_removed > 0) {
2205
+ console.log(` ${result.data.stale_removed} stale dependencies removed`);
2206
+ }
1767
2207
  }
1768
- if (added.length > 0 && !dryRun) {
1769
- await apiPut(`/repos/${repoId}`, { tech_stack: merged });
2208
+ const detected = await detectTechStack(projectPath);
2209
+ if (detected.flat.length > 0) {
2210
+ const repoRes = await apiGet(`/repos/${repoId}`);
2211
+ const remote = parseTechStackResult(repoRes.data.tech_stack);
2212
+ const { merged, added } = mergeTechStack(remote, detected);
2213
+ if (added.length > 0) {
2214
+ console.log(` ${added.length} new tech entries`);
2215
+ if (!dryRun) {
2216
+ await apiPut(`/repos/${repoId}`, { tech_stack: merged });
2217
+ }
2218
+ }
1770
2219
  }
1771
2220
  } catch {
1772
2221
  console.log(" Tech stack detection skipped.");
@@ -1812,6 +2261,13 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
1812
2261
  if (remote.type === "template") return join7(typeDir, remote.name);
1813
2262
  return join7(typeDir, `${remote.name}${cfg.ext}`);
1814
2263
  }
2264
+ function getSyncVersion() {
2265
+ try {
2266
+ return "3.3.0";
2267
+ } catch {
2268
+ return "unknown";
2269
+ }
2270
+ }
1815
2271
  function flattenSyncData(data) {
1816
2272
  const result = /* @__PURE__ */ new Map();
1817
2273
  const typeMap = {
@@ -1828,6 +2284,7 @@ function flattenSyncData(data) {
1828
2284
  for (const file of files) {
1829
2285
  const key = compositeKey(typeName, file.name, file.category ?? null);
1830
2286
  result.set(key, {
2287
+ id: file.id,
1831
2288
  type: typeName,
1832
2289
  name: file.name,
1833
2290
  content: file.content,
@@ -18588,49 +19045,49 @@ var require_fast_uri = __commonJS({
18588
19045
  schemelessOptions.skipEscape = true;
18589
19046
  return serialize(resolved, schemelessOptions);
18590
19047
  }
18591
- function resolveComponent(base, relative, options, skipNormalization) {
19048
+ function resolveComponent(base, relative2, options, skipNormalization) {
18592
19049
  const target = {};
18593
19050
  if (!skipNormalization) {
18594
19051
  base = parse3(serialize(base, options), options);
18595
- relative = parse3(serialize(relative, options), options);
19052
+ relative2 = parse3(serialize(relative2, options), options);
18596
19053
  }
18597
19054
  options = options || {};
18598
- if (!options.tolerant && relative.scheme) {
18599
- target.scheme = relative.scheme;
18600
- target.userinfo = relative.userinfo;
18601
- target.host = relative.host;
18602
- target.port = relative.port;
18603
- target.path = removeDotSegments(relative.path || "");
18604
- target.query = relative.query;
19055
+ if (!options.tolerant && relative2.scheme) {
19056
+ target.scheme = relative2.scheme;
19057
+ target.userinfo = relative2.userinfo;
19058
+ target.host = relative2.host;
19059
+ target.port = relative2.port;
19060
+ target.path = removeDotSegments(relative2.path || "");
19061
+ target.query = relative2.query;
18605
19062
  } else {
18606
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
18607
- target.userinfo = relative.userinfo;
18608
- target.host = relative.host;
18609
- target.port = relative.port;
18610
- target.path = removeDotSegments(relative.path || "");
18611
- target.query = relative.query;
19063
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
19064
+ target.userinfo = relative2.userinfo;
19065
+ target.host = relative2.host;
19066
+ target.port = relative2.port;
19067
+ target.path = removeDotSegments(relative2.path || "");
19068
+ target.query = relative2.query;
18612
19069
  } else {
18613
- if (!relative.path) {
19070
+ if (!relative2.path) {
18614
19071
  target.path = base.path;
18615
- if (relative.query !== void 0) {
18616
- target.query = relative.query;
19072
+ if (relative2.query !== void 0) {
19073
+ target.query = relative2.query;
18617
19074
  } else {
18618
19075
  target.query = base.query;
18619
19076
  }
18620
19077
  } else {
18621
- if (relative.path[0] === "/") {
18622
- target.path = removeDotSegments(relative.path);
19078
+ if (relative2.path[0] === "/") {
19079
+ target.path = removeDotSegments(relative2.path);
18623
19080
  } else {
18624
19081
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
18625
- target.path = "/" + relative.path;
19082
+ target.path = "/" + relative2.path;
18626
19083
  } else if (!base.path) {
18627
- target.path = relative.path;
19084
+ target.path = relative2.path;
18628
19085
  } else {
18629
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
19086
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
18630
19087
  }
18631
19088
  target.path = removeDotSegments(target.path);
18632
19089
  }
18633
- target.query = relative.query;
19090
+ target.query = relative2.query;
18634
19091
  }
18635
19092
  target.userinfo = base.userinfo;
18636
19093
  target.host = base.host;
@@ -18638,7 +19095,7 @@ var require_fast_uri = __commonJS({
18638
19095
  }
18639
19096
  target.scheme = base.scheme;
18640
19097
  }
18641
- target.fragment = relative.fragment;
19098
+ target.fragment = relative2.fragment;
18642
19099
  return target;
18643
19100
  }
18644
19101
  function equal(uriA, uriB, options) {
@@ -23388,258 +23845,689 @@ var init_stdio2 = __esm({
23388
23845
 
23389
23846
  // src/tools/read.ts
23390
23847
  function registerReadTools(server) {
23391
- server.registerTool("get_repos", {
23392
- description: "List all repos.",
23393
- inputSchema: {}
23394
- }, async () => {
23395
- try {
23396
- const res = await apiGet("/repos");
23397
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23398
- } catch (err) {
23399
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23400
- }
23401
- });
23402
- server.registerTool("get_work_plan", {
23403
- description: "Get the work plan for a specific repo, week, and year.",
23404
- inputSchema: {
23405
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23406
- week_number: external_exports.number().int().min(1).max(53).describe("ISO week number"),
23407
- year: external_exports.number().int().describe("Year (e.g. 2026)")
23408
- }
23409
- }, async ({ repo_id, week_number, year }) => {
23410
- try {
23411
- const res = await apiGet("/work-plans", {
23412
- repo_id,
23413
- week_number: String(week_number),
23414
- year: String(year)
23415
- });
23416
- const plan = res.data[0] ?? null;
23417
- if (!plan) {
23418
- return { content: [{ type: "text", text: "Error: No work plan found for the specified repo, week, and year" }], isError: true };
23848
+ server.registerTool(
23849
+ "get_repos",
23850
+ {
23851
+ description: "List all repos.",
23852
+ inputSchema: {}
23853
+ },
23854
+ async () => {
23855
+ try {
23856
+ const res = await apiGet("/repos");
23857
+ return {
23858
+ content: [
23859
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
23860
+ ]
23861
+ };
23862
+ } catch (err) {
23863
+ return {
23864
+ content: [
23865
+ {
23866
+ type: "text",
23867
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
23868
+ }
23869
+ ],
23870
+ isError: true
23871
+ };
23419
23872
  }
23420
- return { content: [{ type: "text", text: JSON.stringify(plan, null, 2) }] };
23421
- } catch (err) {
23422
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23423
- }
23424
- });
23425
- server.registerTool("get_checkpoints", {
23426
- description: "List checkpoints for a repo. Optionally filter by status and/or worktree assignment.",
23427
- inputSchema: {
23428
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23429
- status: external_exports.string().optional().describe("Filter by status (draft, pending, active, completed)"),
23430
- worktree_id: external_exports.string().uuid().optional().describe("Filter by worktree UUID assignment")
23431
23873
  }
23432
- }, async ({ repo_id, status, worktree_id }) => {
23433
- try {
23434
- const res = await apiGet("/checkpoints", { repo_id, status, worktree_id });
23435
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23436
- } catch (err) {
23437
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23438
- }
23439
- });
23440
- server.registerTool("get_tasks", {
23441
- description: "List tasks for a checkpoint. Optionally filter by status.",
23442
- inputSchema: {
23443
- checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
23444
- status: external_exports.string().optional().describe("Filter by status (pending, in_progress, completed)")
23445
- }
23446
- }, async ({ checkpoint_id, status }) => {
23447
- try {
23448
- const res = await apiGet("/tasks", { checkpoint_id, status });
23449
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23450
- } catch (err) {
23451
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23452
- }
23453
- });
23454
- server.registerTool("get_rounds", {
23455
- description: "List rounds for a task. Optionally filter by status.",
23456
- inputSchema: {
23457
- task_id: external_exports.string().uuid().describe("The task UUID"),
23458
- status: external_exports.string().optional().describe("Filter by status (pending, in_progress, completed)")
23459
- }
23460
- }, async ({ task_id, status }) => {
23461
- try {
23462
- const res = await apiGet("/rounds", { task_id, status });
23463
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23464
- } catch (err) {
23465
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23466
- }
23467
- });
23468
- server.registerTool("get_current_task", {
23469
- description: "Get the current in-progress task for a repo. Finds the active checkpoint, then the in-progress task within it.",
23470
- inputSchema: {
23471
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23472
- worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter checkpoints by assignment")
23473
- }
23474
- }, async ({ repo_id, worktree_id }) => {
23475
- try {
23476
- const checkpointsRes = await apiGet("/checkpoints", {
23477
- repo_id,
23478
- status: "active"
23479
- });
23480
- let activeCheckpoints = checkpointsRes.data;
23481
- if (worktree_id) {
23482
- activeCheckpoints = activeCheckpoints.filter((c) => c.worktree_id === worktree_id);
23874
+ );
23875
+ server.registerTool(
23876
+ "get_work_plan",
23877
+ {
23878
+ description: "Get the work plan for a specific repo, week, and year.",
23879
+ inputSchema: {
23880
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
23881
+ week_number: external_exports.number().int().min(1).max(53).describe("ISO week number"),
23882
+ year: external_exports.number().int().describe("Year (e.g. 2026)")
23483
23883
  }
23484
- if (activeCheckpoints.length === 0) {
23884
+ },
23885
+ async ({ repo_id, week_number, year }) => {
23886
+ try {
23887
+ const res = await apiGet("/work-plans", {
23888
+ repo_id,
23889
+ week_number: String(week_number),
23890
+ year: String(year)
23891
+ });
23892
+ const plan = res.data[0] ?? null;
23893
+ if (!plan) {
23894
+ return {
23895
+ content: [
23896
+ {
23897
+ type: "text",
23898
+ text: "Error: No work plan found for the specified repo, week, and year"
23899
+ }
23900
+ ],
23901
+ isError: true
23902
+ };
23903
+ }
23904
+ return {
23905
+ content: [
23906
+ { type: "text", text: JSON.stringify(plan, null, 2) }
23907
+ ]
23908
+ };
23909
+ } catch (err) {
23485
23910
  return {
23486
- content: [{ type: "text", text: "No active checkpoint found for this repo." }]
23911
+ content: [
23912
+ {
23913
+ type: "text",
23914
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
23915
+ }
23916
+ ],
23917
+ isError: true
23487
23918
  };
23488
23919
  }
23489
- const checkpoint = activeCheckpoints[0];
23490
- const tasksRes = await apiGet("/tasks", {
23491
- checkpoint_id: checkpoint.id,
23492
- status: "in_progress"
23493
- });
23494
- if (tasksRes.data.length === 0) {
23920
+ }
23921
+ );
23922
+ server.registerTool(
23923
+ "get_checkpoints",
23924
+ {
23925
+ description: "List checkpoints for a repo. Optionally filter by status and/or worktree assignment.",
23926
+ inputSchema: {
23927
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
23928
+ status: external_exports.string().optional().describe("Filter by status (draft, pending, active, completed)"),
23929
+ worktree_id: external_exports.string().uuid().optional().describe("Filter by worktree UUID assignment")
23930
+ }
23931
+ },
23932
+ async ({ repo_id, status, worktree_id }) => {
23933
+ try {
23934
+ const res = await apiGet("/checkpoints", {
23935
+ repo_id,
23936
+ status,
23937
+ worktree_id
23938
+ });
23495
23939
  return {
23496
- content: [{
23497
- type: "text",
23498
- text: JSON.stringify({
23499
- checkpoint,
23500
- task: null,
23501
- message: "Active checkpoint found but no in-progress task."
23502
- }, null, 2)
23503
- }]
23940
+ content: [
23941
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
23942
+ ]
23943
+ };
23944
+ } catch (err) {
23945
+ return {
23946
+ content: [
23947
+ {
23948
+ type: "text",
23949
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
23950
+ }
23951
+ ],
23952
+ isError: true
23504
23953
  };
23505
23954
  }
23506
- return {
23507
- content: [{
23508
- type: "text",
23509
- text: JSON.stringify({ checkpoint, task: tasksRes.data[0] }, null, 2)
23510
- }]
23511
- };
23512
- } catch (err) {
23513
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23514
23955
  }
23515
- });
23516
- server.registerTool("get_launches", {
23517
- description: "List launches for a repo. Optionally filter by status or type.",
23518
- inputSchema: {
23519
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23520
- status: external_exports.string().optional().describe("Filter by status"),
23521
- type: external_exports.string().optional().describe("Filter by type")
23522
- }
23523
- }, async ({ repo_id, status, type }) => {
23524
- try {
23525
- const res = await apiGet("/launches", { repo_id, status, type });
23526
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23527
- } catch (err) {
23528
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23529
- }
23530
- });
23531
- server.registerTool("get_launch", {
23532
- description: "Get a single launch by ID.",
23533
- inputSchema: {
23534
- launch_id: external_exports.string().uuid().describe("The launch UUID")
23956
+ );
23957
+ server.registerTool(
23958
+ "get_tasks",
23959
+ {
23960
+ description: "List tasks for a checkpoint or repo. Filter by checkpoint_id for checkpoint tasks, or repo_id with standalone=true for standalone tasks.",
23961
+ inputSchema: {
23962
+ checkpoint_id: external_exports.string().uuid().optional().describe("The checkpoint UUID (for checkpoint-bound tasks)"),
23963
+ repo_id: external_exports.string().uuid().optional().describe("The repo UUID (for standalone tasks)"),
23964
+ status: external_exports.string().optional().describe("Filter by status (pending, in_progress, completed)"),
23965
+ standalone: external_exports.boolean().optional().describe(
23966
+ "If true, only return tasks with no checkpoint (standalone)"
23967
+ )
23968
+ }
23969
+ },
23970
+ async ({ checkpoint_id, repo_id, status, standalone }) => {
23971
+ try {
23972
+ const params = {
23973
+ checkpoint_id,
23974
+ repo_id,
23975
+ status
23976
+ };
23977
+ if (standalone) params.standalone = "true";
23978
+ const res = await apiGet("/tasks", params);
23979
+ return {
23980
+ content: [
23981
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
23982
+ ]
23983
+ };
23984
+ } catch (err) {
23985
+ return {
23986
+ content: [
23987
+ {
23988
+ type: "text",
23989
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
23990
+ }
23991
+ ],
23992
+ isError: true
23993
+ };
23994
+ }
23535
23995
  }
23536
- }, async ({ launch_id }) => {
23537
- try {
23538
- const res = await apiGet(`/launches/${launch_id}`);
23539
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23540
- } catch (err) {
23541
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23996
+ );
23997
+ server.registerTool(
23998
+ "get_rounds",
23999
+ {
24000
+ description: "List rounds for a task. Optionally filter by status.",
24001
+ inputSchema: {
24002
+ task_id: external_exports.string().uuid().describe("The task UUID"),
24003
+ status: external_exports.string().optional().describe("Filter by status (pending, in_progress, completed)")
24004
+ }
24005
+ },
24006
+ async ({ task_id, status }) => {
24007
+ try {
24008
+ const res = await apiGet("/rounds", {
24009
+ task_id,
24010
+ status
24011
+ });
24012
+ return {
24013
+ content: [
24014
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24015
+ ]
24016
+ };
24017
+ } catch (err) {
24018
+ return {
24019
+ content: [
24020
+ {
24021
+ type: "text",
24022
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24023
+ }
24024
+ ],
24025
+ isError: true
24026
+ };
24027
+ }
23542
24028
  }
23543
- });
23544
- server.registerTool("get_session_logs", {
23545
- description: "List session logs for a repo. Optionally filter by date.",
23546
- inputSchema: {
23547
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23548
- session_date: external_exports.string().optional().describe("Filter by session date (YYYY-MM-DD)")
24029
+ );
24030
+ server.registerTool(
24031
+ "get_current_task",
24032
+ {
24033
+ description: "Get the current in-progress task for a repo. Finds the active checkpoint, then the in-progress task within it.",
24034
+ inputSchema: {
24035
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24036
+ worktree_id: external_exports.string().uuid().optional().describe(
24037
+ "Optional worktree UUID to filter checkpoints by assignment"
24038
+ )
24039
+ }
24040
+ },
24041
+ async ({ repo_id, worktree_id }) => {
24042
+ try {
24043
+ const checkpointsRes = await apiGet(
24044
+ "/checkpoints",
24045
+ {
24046
+ repo_id,
24047
+ status: "active"
24048
+ }
24049
+ );
24050
+ let activeCheckpoints = checkpointsRes.data;
24051
+ if (worktree_id) {
24052
+ activeCheckpoints = activeCheckpoints.filter(
24053
+ (c) => c.worktree_id === worktree_id
24054
+ );
24055
+ }
24056
+ if (activeCheckpoints.length === 0) {
24057
+ return {
24058
+ content: [
24059
+ {
24060
+ type: "text",
24061
+ text: "No active checkpoint found for this repo."
24062
+ }
24063
+ ]
24064
+ };
24065
+ }
24066
+ const checkpoint = activeCheckpoints[0];
24067
+ const tasksRes = await apiGet("/tasks", {
24068
+ checkpoint_id: checkpoint.id,
24069
+ status: "in_progress"
24070
+ });
24071
+ if (tasksRes.data.length === 0) {
24072
+ return {
24073
+ content: [
24074
+ {
24075
+ type: "text",
24076
+ text: JSON.stringify(
24077
+ {
24078
+ checkpoint,
24079
+ task: null,
24080
+ message: "Active checkpoint found but no in-progress task."
24081
+ },
24082
+ null,
24083
+ 2
24084
+ )
24085
+ }
24086
+ ]
24087
+ };
24088
+ }
24089
+ return {
24090
+ content: [
24091
+ {
24092
+ type: "text",
24093
+ text: JSON.stringify(
24094
+ { checkpoint, task: tasksRes.data[0] },
24095
+ null,
24096
+ 2
24097
+ )
24098
+ }
24099
+ ]
24100
+ };
24101
+ } catch (err) {
24102
+ return {
24103
+ content: [
24104
+ {
24105
+ type: "text",
24106
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24107
+ }
24108
+ ],
24109
+ isError: true
24110
+ };
24111
+ }
23549
24112
  }
23550
- }, async ({ repo_id, session_date }) => {
23551
- try {
23552
- const res = await apiGet("/session-logs", { repo_id, session_date });
23553
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23554
- } catch (err) {
23555
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24113
+ );
24114
+ server.registerTool(
24115
+ "get_launches",
24116
+ {
24117
+ description: "List launches for a repo. Optionally filter by status or type.",
24118
+ inputSchema: {
24119
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24120
+ status: external_exports.string().optional().describe("Filter by status"),
24121
+ type: external_exports.string().optional().describe("Filter by type")
24122
+ }
24123
+ },
24124
+ async ({ repo_id, status, type }) => {
24125
+ try {
24126
+ const res = await apiGet("/launches", {
24127
+ repo_id,
24128
+ status,
24129
+ type
24130
+ });
24131
+ return {
24132
+ content: [
24133
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24134
+ ]
24135
+ };
24136
+ } catch (err) {
24137
+ return {
24138
+ content: [
24139
+ {
24140
+ type: "text",
24141
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24142
+ }
24143
+ ],
24144
+ isError: true
24145
+ };
24146
+ }
23556
24147
  }
23557
- });
23558
- server.registerTool("get_session_log", {
23559
- description: "Get a single session log by ID.",
23560
- inputSchema: {
23561
- session_log_id: external_exports.string().uuid().describe("The session log UUID")
24148
+ );
24149
+ server.registerTool(
24150
+ "get_launch",
24151
+ {
24152
+ description: "Get a single launch by ID.",
24153
+ inputSchema: {
24154
+ launch_id: external_exports.string().uuid().describe("The launch UUID")
24155
+ }
24156
+ },
24157
+ async ({ launch_id }) => {
24158
+ try {
24159
+ const res = await apiGet(
24160
+ `/launches/${launch_id}`
24161
+ );
24162
+ return {
24163
+ content: [
24164
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24165
+ ]
24166
+ };
24167
+ } catch (err) {
24168
+ return {
24169
+ content: [
24170
+ {
24171
+ type: "text",
24172
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24173
+ }
24174
+ ],
24175
+ isError: true
24176
+ };
24177
+ }
23562
24178
  }
23563
- }, async ({ session_log_id }) => {
23564
- try {
23565
- const res = await apiGet(`/session-logs/${session_log_id}`);
23566
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23567
- } catch (err) {
23568
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24179
+ );
24180
+ server.registerTool(
24181
+ "get_session_logs",
24182
+ {
24183
+ description: "List session logs for a repo. Optionally filter by date.",
24184
+ inputSchema: {
24185
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24186
+ session_date: external_exports.string().optional().describe("Filter by session date (YYYY-MM-DD)")
24187
+ }
24188
+ },
24189
+ async ({ repo_id, session_date }) => {
24190
+ try {
24191
+ const res = await apiGet("/session-logs", {
24192
+ repo_id,
24193
+ session_date
24194
+ });
24195
+ return {
24196
+ content: [
24197
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24198
+ ]
24199
+ };
24200
+ } catch (err) {
24201
+ return {
24202
+ content: [
24203
+ {
24204
+ type: "text",
24205
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24206
+ }
24207
+ ],
24208
+ isError: true
24209
+ };
24210
+ }
23569
24211
  }
23570
- });
23571
- server.registerTool("get_session_state", {
23572
- description: "Get session state for a repo (active, paused, inactive, needs_refresh, last_session_at).",
23573
- inputSchema: {
23574
- repo_id: external_exports.string().uuid().describe("The repo UUID")
24212
+ );
24213
+ server.registerTool(
24214
+ "get_session_log",
24215
+ {
24216
+ description: "Get a single session log by ID.",
24217
+ inputSchema: {
24218
+ session_log_id: external_exports.string().uuid().describe("The session log UUID")
24219
+ }
24220
+ },
24221
+ async ({ session_log_id }) => {
24222
+ try {
24223
+ const res = await apiGet(
24224
+ `/session-logs/${session_log_id}`
24225
+ );
24226
+ return {
24227
+ content: [
24228
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24229
+ ]
24230
+ };
24231
+ } catch (err) {
24232
+ return {
24233
+ content: [
24234
+ {
24235
+ type: "text",
24236
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24237
+ }
24238
+ ],
24239
+ isError: true
24240
+ };
24241
+ }
23575
24242
  }
23576
- }, async ({ repo_id }) => {
23577
- try {
23578
- const res = await apiGet(`/repos/${repo_id}`);
23579
- const { id, name, is_session_active, session_status, needs_refresh, last_session_at } = res.data;
23580
- const data = { id, name, is_session_active, session_status, needs_refresh, last_session_at };
23581
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23582
- } catch (err) {
23583
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24243
+ );
24244
+ server.registerTool(
24245
+ "get_session_state",
24246
+ {
24247
+ description: "Get session state for a repo (active, paused, inactive, needs_refresh, last_session_at).",
24248
+ inputSchema: {
24249
+ repo_id: external_exports.string().uuid().describe("The repo UUID")
24250
+ }
24251
+ },
24252
+ async ({ repo_id }) => {
24253
+ try {
24254
+ const res = await apiGet(`/repos/${repo_id}`);
24255
+ const {
24256
+ id,
24257
+ name,
24258
+ is_session_active,
24259
+ session_status,
24260
+ needs_refresh,
24261
+ last_session_at
24262
+ } = res.data;
24263
+ const data = {
24264
+ id,
24265
+ name,
24266
+ is_session_active,
24267
+ session_status,
24268
+ needs_refresh,
24269
+ last_session_at
24270
+ };
24271
+ return {
24272
+ content: [
24273
+ { type: "text", text: JSON.stringify(data, null, 2) }
24274
+ ]
24275
+ };
24276
+ } catch (err) {
24277
+ return {
24278
+ content: [
24279
+ {
24280
+ type: "text",
24281
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24282
+ }
24283
+ ],
24284
+ isError: true
24285
+ };
24286
+ }
23584
24287
  }
23585
- });
23586
- server.registerTool("get_server_config", {
23587
- description: "Get server configuration for a repo (port, type, active servers).",
23588
- inputSchema: {
23589
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23590
- worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter port allocations by worktree")
24288
+ );
24289
+ server.registerTool(
24290
+ "get_server_config",
24291
+ {
24292
+ description: "Get server configuration for a repo (port, type, active servers).",
24293
+ inputSchema: {
24294
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24295
+ worktree_id: external_exports.string().uuid().optional().describe(
24296
+ "Optional worktree UUID to filter port allocations by worktree"
24297
+ )
24298
+ }
24299
+ },
24300
+ async ({ repo_id, worktree_id }) => {
24301
+ try {
24302
+ const params = {};
24303
+ if (worktree_id) params.worktree_id = worktree_id;
24304
+ const res = await apiGet(
24305
+ `/repos/${repo_id}/server`,
24306
+ params
24307
+ );
24308
+ return {
24309
+ content: [
24310
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24311
+ ]
24312
+ };
24313
+ } catch (err) {
24314
+ return {
24315
+ content: [
24316
+ {
24317
+ type: "text",
24318
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24319
+ }
24320
+ ],
24321
+ isError: true
24322
+ };
24323
+ }
23591
24324
  }
23592
- }, async ({ repo_id, worktree_id }) => {
23593
- try {
23594
- const params = {};
23595
- if (worktree_id) params.worktree_id = worktree_id;
23596
- const res = await apiGet(`/repos/${repo_id}/server`, params);
23597
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23598
- } catch (err) {
23599
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24325
+ );
24326
+ server.registerTool(
24327
+ "get_sync_status",
24328
+ {
24329
+ description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
24330
+ inputSchema: {}
24331
+ },
24332
+ async () => {
24333
+ try {
24334
+ const res = await apiGet("/sync/status");
24335
+ return {
24336
+ content: [
24337
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24338
+ ]
24339
+ };
24340
+ } catch (err) {
24341
+ return {
24342
+ content: [
24343
+ {
24344
+ type: "text",
24345
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24346
+ }
24347
+ ],
24348
+ isError: true
24349
+ };
24350
+ }
23600
24351
  }
23601
- });
23602
- server.registerTool("get_sync_status", {
23603
- description: "Get cross-repo sync status. Shows which repos need a claude files sync based on latest updates.",
23604
- inputSchema: {}
23605
- }, async () => {
23606
- try {
23607
- const res = await apiGet("/sync/status");
23608
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23609
- } catch (err) {
23610
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24352
+ );
24353
+ server.registerTool(
24354
+ "get_next_action",
24355
+ {
24356
+ description: "Compute the next action for a repo based on current workflow state. Returns command, instructions, state, and context.",
24357
+ inputSchema: {
24358
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24359
+ worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter by assignment")
24360
+ }
24361
+ },
24362
+ async ({ repo_id, worktree_id }) => {
24363
+ try {
24364
+ const params = {};
24365
+ if (worktree_id) params.worktree_id = worktree_id;
24366
+ const res = await apiGet(
24367
+ `/repos/${repo_id}/next-action`,
24368
+ params
24369
+ );
24370
+ return {
24371
+ content: [
24372
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24373
+ ]
24374
+ };
24375
+ } catch (err) {
24376
+ return {
24377
+ content: [
24378
+ {
24379
+ type: "text",
24380
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24381
+ }
24382
+ ],
24383
+ isError: true
24384
+ };
24385
+ }
23611
24386
  }
23612
- });
23613
- server.registerTool("get_next_action", {
23614
- description: "Compute the next action for a repo based on current workflow state. Returns command, instructions, state, and context.",
23615
- inputSchema: {
23616
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23617
- worktree_id: external_exports.string().uuid().optional().describe("Optional worktree UUID to filter by assignment")
24387
+ );
24388
+ server.registerTool(
24389
+ "get_worktrees",
24390
+ {
24391
+ description: "List worktrees for a repo. Optionally filter by status.",
24392
+ inputSchema: {
24393
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24394
+ status: external_exports.string().optional().describe("Filter by status (active, inactive, archived)")
24395
+ }
24396
+ },
24397
+ async ({ repo_id, status }) => {
24398
+ try {
24399
+ const res = await apiGet("/worktrees", {
24400
+ repo_id,
24401
+ status
24402
+ });
24403
+ return {
24404
+ content: [
24405
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24406
+ ]
24407
+ };
24408
+ } catch (err) {
24409
+ return {
24410
+ content: [
24411
+ {
24412
+ type: "text",
24413
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24414
+ }
24415
+ ],
24416
+ isError: true
24417
+ };
24418
+ }
23618
24419
  }
23619
- }, async ({ repo_id, worktree_id }) => {
23620
- try {
23621
- const params = {};
23622
- if (worktree_id) params.worktree_id = worktree_id;
23623
- const res = await apiGet(`/repos/${repo_id}/next-action`, params);
23624
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23625
- } catch (err) {
23626
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24420
+ );
24421
+ server.registerTool(
24422
+ "get_sync_lock_status",
24423
+ {
24424
+ description: "Check if a sync lock is currently held for a repo. Cleans up expired locks.",
24425
+ inputSchema: {
24426
+ repo_id: external_exports.string().uuid().describe("The repo UUID")
24427
+ }
24428
+ },
24429
+ async ({ repo_id }) => {
24430
+ try {
24431
+ const res = await apiGet("/sync/lock", { repo_id });
24432
+ return {
24433
+ content: [
24434
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24435
+ ]
24436
+ };
24437
+ } catch (err) {
24438
+ return {
24439
+ content: [
24440
+ {
24441
+ type: "text",
24442
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24443
+ }
24444
+ ],
24445
+ isError: true
24446
+ };
24447
+ }
23627
24448
  }
23628
- });
23629
- server.registerTool("get_worktrees", {
23630
- description: "List worktrees for a repo. Optionally filter by status.",
23631
- inputSchema: {
23632
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23633
- status: external_exports.string().optional().describe("Filter by status (active, inactive, archived)")
24449
+ );
24450
+ server.registerTool(
24451
+ "get_sync_conflicts",
24452
+ {
24453
+ description: "List sync conflicts for a repo. Optionally filter by status.",
24454
+ inputSchema: {
24455
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24456
+ status: external_exports.string().optional().describe("Filter by status (unresolved, resolved, skipped)")
24457
+ }
24458
+ },
24459
+ async ({ repo_id, status }) => {
24460
+ try {
24461
+ const res = await apiGet("/sync/conflicts", {
24462
+ repo_id,
24463
+ status
24464
+ });
24465
+ return {
24466
+ content: [
24467
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24468
+ ]
24469
+ };
24470
+ } catch (err) {
24471
+ return {
24472
+ content: [
24473
+ {
24474
+ type: "text",
24475
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24476
+ }
24477
+ ],
24478
+ isError: true
24479
+ };
24480
+ }
23634
24481
  }
23635
- }, async ({ repo_id, status }) => {
23636
- try {
23637
- const res = await apiGet("/worktrees", { repo_id, status });
23638
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23639
- } catch (err) {
23640
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24482
+ );
24483
+ server.registerTool(
24484
+ "get_file_changes",
24485
+ {
24486
+ description: "List file changes for a repo. Filter by checkpoint, task, round, file path, or source. Use aggregate='task' with task_id to get latest entry per file (task-level view).",
24487
+ inputSchema: {
24488
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24489
+ checkpoint_id: external_exports.string().uuid().optional().describe("Filter by checkpoint"),
24490
+ task_id: external_exports.string().uuid().optional().describe("Filter by task"),
24491
+ round_id: external_exports.string().uuid().optional().describe("Filter by round"),
24492
+ file_path: external_exports.string().optional().describe("Filter by exact file path"),
24493
+ source: external_exports.enum(["round", "fix"]).optional().describe("Filter by source (round or fix)"),
24494
+ aggregate: external_exports.enum(["task"]).optional().describe(
24495
+ "Aggregation mode. 'task' returns latest entry per file_path for a task (requires task_id)"
24496
+ )
24497
+ }
24498
+ },
24499
+ async ({ repo_id, checkpoint_id, task_id, round_id, file_path, source, aggregate }) => {
24500
+ try {
24501
+ const res = await apiGet(
24502
+ "/file-changes",
24503
+ {
24504
+ repo_id,
24505
+ checkpoint_id,
24506
+ task_id,
24507
+ round_id,
24508
+ file_path,
24509
+ source,
24510
+ aggregate
24511
+ }
24512
+ );
24513
+ return {
24514
+ content: [
24515
+ { type: "text", text: JSON.stringify(res, null, 2) }
24516
+ ]
24517
+ };
24518
+ } catch (err) {
24519
+ return {
24520
+ content: [
24521
+ {
24522
+ type: "text",
24523
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24524
+ }
24525
+ ],
24526
+ isError: true
24527
+ };
24528
+ }
23641
24529
  }
23642
- });
24530
+ );
23643
24531
  }
23644
24532
  var init_read = __esm({
23645
24533
  "src/tools/read.ts"() {
@@ -23917,621 +24805,1591 @@ var init_promotion = __esm({
23917
24805
 
23918
24806
  // src/tools/write.ts
23919
24807
  function registerWriteTools(server) {
23920
- server.registerTool("create_repo", {
23921
- description: "Create a new repo entry.",
23922
- inputSchema: {
23923
- name: external_exports.string().describe("Repo name (must be unique)"),
23924
- path: external_exports.string().optional().describe("Local filesystem path to the repo"),
23925
- git_branch: external_exports.string().optional().describe("Default git branch (default: development)")
23926
- }
23927
- }, async ({ name, path, git_branch }) => {
23928
- try {
23929
- const res = await apiPost("/repos", {
23930
- name,
23931
- path: path ?? null,
23932
- git_branch: git_branch ?? "development"
23933
- });
23934
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23935
- } catch (err) {
23936
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23937
- }
23938
- });
23939
- server.registerTool("create_checkpoint", {
23940
- description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
23941
- inputSchema: {
23942
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
23943
- title: external_exports.string().optional().describe("Checkpoint title (optional \u2014 Claude can generate if missing)"),
23944
- number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
23945
- goal: external_exports.string().optional().describe("Checkpoint goal description (max 300 chars, brief overview)"),
23946
- deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
23947
- status: external_exports.string().optional().describe("Initial status (default: pending). Use 'draft' for checkpoints not ready for development."),
23948
- launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
23949
- ideas: external_exports.array(external_exports.object({
23950
- description: external_exports.string().describe("Idea description"),
23951
- requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23952
- images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23953
- })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23954
- context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23955
- research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23956
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
23957
- }
23958
- }, async ({ repo_id, title, number: number3, goal, deadline, status, launch_id, ideas, context, research, qa }) => {
23959
- try {
23960
- const body = {
23961
- repo_id,
23962
- title: title ?? null,
23963
- number: number3,
23964
- goal: goal ?? null,
23965
- deadline: deadline ?? null,
23966
- status: status ?? "pending",
23967
- launch_id: launch_id ?? null
23968
- };
23969
- if (ideas !== void 0) body.ideas = ideas;
23970
- if (context !== void 0) body.context = context;
23971
- if (research !== void 0) body.research = research;
23972
- if (qa !== void 0) body.qa = qa;
23973
- const res = await apiPost("/checkpoints", body);
23974
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23975
- } catch (err) {
23976
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23977
- }
23978
- });
23979
- server.registerTool("update_checkpoint", {
23980
- description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
23981
- inputSchema: {
23982
- checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
23983
- title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
23984
- goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
23985
- status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
23986
- deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
23987
- completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
23988
- launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
23989
- worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
23990
- assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
23991
- branch_name: external_exports.string().nullable().optional().describe("Git branch name for this checkpoint (e.g. feat/CHK-061-git-overhaul)"),
23992
- ideas: external_exports.array(external_exports.object({
23993
- description: external_exports.string().describe("Idea description"),
23994
- requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
23995
- images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
23996
- })).optional().describe("Ideas array \u2014 each idea has description, requirements[], images[]"),
23997
- context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"),
23998
- research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
23999
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
24000
- }
24001
- }, async ({ checkpoint_id, title, goal, status, deadline, completed_at, launch_id, worktree_id, assigned_to, branch_name, ideas, context, research, qa }) => {
24002
- const update = {};
24003
- if (title !== void 0) update.title = title;
24004
- if (goal !== void 0) update.goal = goal;
24005
- if (status !== void 0) update.status = status;
24006
- if (deadline !== void 0) update.deadline = deadline;
24007
- if (completed_at !== void 0) update.completed_at = completed_at;
24008
- if (launch_id !== void 0) update.launch_id = launch_id;
24009
- if (worktree_id !== void 0) update.worktree_id = worktree_id;
24010
- if (assigned_to !== void 0) update.assigned_to = assigned_to;
24011
- if (branch_name !== void 0) update.branch_name = branch_name;
24012
- if (ideas !== void 0) update.ideas = ideas;
24013
- if (context !== void 0) update.context = context;
24014
- if (research !== void 0) update.research = research;
24015
- if (qa !== void 0) update.qa = qa;
24016
- if (Object.keys(update).length === 0) {
24017
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
24018
- }
24019
- try {
24020
- const res = await apiPut(`/checkpoints/${checkpoint_id}`, update);
24021
- return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
24022
- } catch (err) {
24023
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24024
- }
24025
- });
24026
- server.registerTool("complete_checkpoint", {
24027
- description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers promotion (creates PR from feat branch to development).",
24028
- inputSchema: {
24029
- checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
24030
- }
24031
- }, async ({ checkpoint_id }) => {
24032
- try {
24033
- const res = await apiPut(`/checkpoints/${checkpoint_id}`, {
24034
- status: "completed",
24035
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
24036
- });
24037
- const checkpoint = res.data;
24038
- const featToDevResult = await promoteCheckpoint(checkpoint_id);
24039
- let devToMainResult = null;
24040
- const repoRes = await apiGet(`/repos/${checkpoint.repo_id}`);
24041
- if (repoRes.data.auto_push_enabled) {
24042
- devToMainResult = await promoteToMain(checkpoint.repo_id);
24043
- }
24044
- return { content: [{ type: "text", text: JSON.stringify({ checkpoint, promotion: { feat_to_development: featToDevResult, development_to_main: devToMainResult } }, null, 2) }] };
24045
- } catch (err) {
24046
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24047
- }
24048
- });
24049
- server.registerTool("create_task", {
24050
- description: "Create a new task within a checkpoint.",
24051
- inputSchema: {
24052
- checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
24053
- title: external_exports.string().describe("Task title"),
24054
- number: external_exports.number().int().describe("Task number (e.g. 1 for TASK-1)"),
24055
- requirements: external_exports.string().optional().describe("Task requirements text"),
24056
- status: external_exports.string().optional().describe("Initial status (default: pending)"),
24057
- context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
24058
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
24059
- research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
24060
- }
24061
- }, async ({ checkpoint_id, title, number: number3, requirements, status, context, qa, research }) => {
24062
- try {
24063
- const body = {
24064
- checkpoint_id,
24065
- title,
24066
- number: number3,
24067
- requirements: requirements ?? null,
24068
- status: status ?? "pending"
24069
- };
24070
- if (context !== void 0) body.context = context;
24071
- if (qa !== void 0) body.qa = qa;
24072
- if (research !== void 0) body.research = research;
24073
- const res = await apiPost("/tasks", body);
24074
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24075
- } catch (err) {
24076
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24077
- }
24078
- });
24079
- server.registerTool("update_task", {
24080
- description: "Update an existing task. Use to update status, requirements, or file tracking. Pass claim_worktree_id when setting status to in_progress to auto-claim the parent checkpoint for the worktree.",
24081
- inputSchema: {
24082
- task_id: external_exports.string().uuid().describe("The task UUID"),
24083
- title: external_exports.string().optional().describe("New title"),
24084
- requirements: external_exports.string().optional().describe("New requirements text"),
24085
- status: external_exports.string().optional().describe("New status (pending, in_progress, completed)"),
24086
- files_changed: external_exports.array(external_exports.object({
24087
- path: external_exports.string().describe("File path relative to repo root"),
24088
- action: external_exports.string().describe("File action (new, modified, deleted)"),
24089
- status: external_exports.string().describe("Approval status (approved, not_approved)"),
24090
- claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
24091
- user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
24092
- })).optional().describe("Files changed across all rounds"),
24093
- claim_worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress"),
24094
- context: external_exports.any().optional().describe("Context JSONB (decisions, discoveries, dependencies, constraints)"),
24095
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
24096
- research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)")
24097
- }
24098
- }, async ({ task_id, title, requirements, status, files_changed, claim_worktree_id, context, qa, research }) => {
24099
- const update = {};
24100
- if (title !== void 0) update.title = title;
24101
- if (requirements !== void 0) update.requirements = requirements;
24102
- if (status !== void 0) update.status = status;
24103
- if (files_changed !== void 0) update.files_changed = files_changed;
24104
- if (claim_worktree_id !== void 0) update.claim_worktree_id = claim_worktree_id;
24105
- if (context !== void 0) update.context = context;
24106
- if (qa !== void 0) update.qa = qa;
24107
- if (research !== void 0) update.research = research;
24108
- if (Object.keys(update).length === 0) {
24109
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
24110
- }
24111
- try {
24112
- const res = await apiPut(`/tasks/${task_id}`, update);
24113
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24114
- } catch (err) {
24115
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24116
- }
24117
- });
24118
- server.registerTool("complete_task", {
24119
- description: "Mark a task as completed. Sets status to 'completed' and completed_at to now.",
24120
- inputSchema: {
24121
- task_id: external_exports.string().uuid().describe("The task UUID")
24122
- }
24123
- }, async ({ task_id }) => {
24124
- try {
24125
- const res = await apiPut(`/tasks/${task_id}`, {
24126
- status: "completed",
24127
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
24128
- });
24129
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24130
- } catch (err) {
24131
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24132
- }
24133
- });
24134
- server.registerTool("add_round", {
24135
- description: "Add a round to a task.",
24136
- inputSchema: {
24137
- task_id: external_exports.string().uuid().describe("The task UUID"),
24138
- number: external_exports.number().int().describe("Round number"),
24139
- requirements: external_exports.string().optional().describe("Round requirements text"),
24140
- status: external_exports.string().optional().describe("Initial status (default: pending)"),
24141
- started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
24142
- context: external_exports.any().optional().describe("Context JSONB"),
24143
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
24144
- }
24145
- }, async ({ task_id, number: number3, requirements, status, started_at, context, qa }) => {
24146
- try {
24147
- const body = {
24148
- task_id,
24149
- number: number3,
24150
- requirements: requirements ?? null,
24151
- status: status ?? "pending",
24152
- started_at: started_at ?? null
24153
- };
24154
- if (context !== void 0) body.context = context;
24155
- if (qa !== void 0) body.qa = qa;
24156
- const res = await apiPost("/rounds", body);
24157
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24158
- } catch (err) {
24159
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24160
- }
24161
- });
24162
- server.registerTool("update_round", {
24163
- description: "Update an existing round.",
24164
- inputSchema: {
24165
- round_id: external_exports.string().uuid().describe("The round UUID"),
24166
- requirements: external_exports.string().optional().describe("Round requirements text"),
24167
- status: external_exports.string().optional().describe("New status (pending, in_progress, completed)"),
24168
- started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
24169
- completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
24170
- duration_minutes: external_exports.number().int().optional().describe("Duration in minutes"),
24171
- files_changed: external_exports.array(external_exports.object({
24172
- path: external_exports.string().describe("File path relative to repo root"),
24173
- action: external_exports.string().describe("File action (new, modified, deleted)"),
24174
- status: external_exports.string().describe("Approval status (approved, not_approved)"),
24175
- claude_approved: external_exports.boolean().optional().describe("Whether Claude's automated checks passed for this file"),
24176
- user_approved: external_exports.boolean().optional().describe("Whether the user has approved this file (via git add or web UI)")
24177
- })).optional().describe("Files changed in this round with approval status"),
24178
- context: external_exports.any().optional().describe("Context JSONB"),
24179
- qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)")
24180
- }
24181
- }, async ({ round_id, requirements, status, started_at, completed_at, duration_minutes, files_changed, context, qa }) => {
24182
- const update = {};
24183
- if (requirements !== void 0) update.requirements = requirements;
24184
- if (status !== void 0) update.status = status;
24185
- if (started_at !== void 0) update.started_at = started_at;
24186
- if (completed_at !== void 0) update.completed_at = completed_at;
24187
- if (duration_minutes !== void 0) update.duration_minutes = duration_minutes;
24188
- if (files_changed !== void 0) update.files_changed = files_changed;
24189
- if (context !== void 0) update.context = context;
24190
- if (qa !== void 0) update.qa = qa;
24191
- if (Object.keys(update).length === 0) {
24192
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
24193
- }
24194
- try {
24195
- const res = await apiPut(`/rounds/${round_id}`, update);
24196
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24197
- } catch (err) {
24198
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24808
+ server.registerTool(
24809
+ "create_repo",
24810
+ {
24811
+ description: "Create a new repo entry.",
24812
+ inputSchema: {
24813
+ name: external_exports.string().describe("Repo name (must be unique)"),
24814
+ path: external_exports.string().optional().describe("Local filesystem path to the repo"),
24815
+ git_branch: external_exports.string().optional().describe("Default git branch (default: development)")
24816
+ }
24817
+ },
24818
+ async ({ name, path, git_branch }) => {
24819
+ try {
24820
+ const res = await apiPost("/repos", {
24821
+ name,
24822
+ path: path ?? null,
24823
+ git_branch: git_branch ?? "development"
24824
+ });
24825
+ return {
24826
+ content: [
24827
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24828
+ ]
24829
+ };
24830
+ } catch (err) {
24831
+ return {
24832
+ content: [
24833
+ {
24834
+ type: "text",
24835
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24836
+ }
24837
+ ],
24838
+ isError: true
24839
+ };
24840
+ }
24199
24841
  }
24200
- });
24201
- server.registerTool("complete_round", {
24202
- description: "Mark a round as completed. Sets status to 'completed' and completed_at to now.",
24203
- inputSchema: {
24204
- round_id: external_exports.string().uuid().describe("The round UUID"),
24205
- duration_minutes: external_exports.number().int().optional().describe("Duration in minutes")
24842
+ );
24843
+ server.registerTool(
24844
+ "create_checkpoint",
24845
+ {
24846
+ description: "Create a new checkpoint for a repo. Optionally connect it to a launch via launch_id.",
24847
+ inputSchema: {
24848
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
24849
+ title: external_exports.string().optional().describe(
24850
+ "Checkpoint title (optional \u2014 Claude can generate if missing)"
24851
+ ),
24852
+ number: external_exports.number().int().describe("Checkpoint number (e.g. 1 for CHK-001)"),
24853
+ goal: external_exports.string().optional().describe(
24854
+ "Checkpoint goal description (max 300 chars, brief overview)"
24855
+ ),
24856
+ deadline: external_exports.string().optional().describe("Deadline date (ISO format)"),
24857
+ status: external_exports.string().optional().describe(
24858
+ "Initial status (default: pending). Use 'draft' for checkpoints not ready for development."
24859
+ ),
24860
+ launch_id: external_exports.string().uuid().optional().describe("Optional launch UUID to connect this checkpoint to"),
24861
+ ideas: external_exports.array(
24862
+ external_exports.object({
24863
+ description: external_exports.string().describe("Idea description"),
24864
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
24865
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
24866
+ })
24867
+ ).optional().describe(
24868
+ "Ideas array \u2014 each idea has description, requirements[], images[]"
24869
+ ),
24870
+ context: external_exports.any().optional().describe(
24871
+ "Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"
24872
+ ),
24873
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
24874
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
24875
+ plan: external_exports.any().optional().describe("Plan JSONB (steps with title, description, scope)"),
24876
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
24877
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
24878
+ context_development: external_exports.any().optional().describe(
24879
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
24880
+ ),
24881
+ resources: external_exports.any().optional().describe(
24882
+ "Resources JSONB array [{url, description, type, added_by}]"
24883
+ )
24884
+ }
24885
+ },
24886
+ async ({
24887
+ repo_id,
24888
+ title,
24889
+ number: number3,
24890
+ goal,
24891
+ deadline,
24892
+ status,
24893
+ launch_id,
24894
+ ideas,
24895
+ context,
24896
+ research,
24897
+ qa,
24898
+ plan,
24899
+ user_context,
24900
+ is_claude_written,
24901
+ context_development,
24902
+ resources
24903
+ }) => {
24904
+ try {
24905
+ const body = {
24906
+ repo_id,
24907
+ title: title ?? null,
24908
+ number: number3,
24909
+ goal: goal ?? null,
24910
+ deadline: deadline ?? null,
24911
+ status: status ?? "pending",
24912
+ launch_id: launch_id ?? null
24913
+ };
24914
+ if (ideas !== void 0) body.ideas = ideas;
24915
+ if (context !== void 0) body.context = context;
24916
+ if (research !== void 0) body.research = research;
24917
+ if (qa !== void 0) body.qa = qa;
24918
+ if (plan !== void 0) body.plan = plan;
24919
+ if (user_context !== void 0) body.user_context = user_context;
24920
+ if (is_claude_written !== void 0)
24921
+ body.is_claude_written = is_claude_written;
24922
+ if (context_development !== void 0)
24923
+ body.context_development = context_development;
24924
+ if (resources !== void 0) body.resources = resources;
24925
+ const res = await apiPost(
24926
+ "/checkpoints",
24927
+ body
24928
+ );
24929
+ return {
24930
+ content: [
24931
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
24932
+ ]
24933
+ };
24934
+ } catch (err) {
24935
+ return {
24936
+ content: [
24937
+ {
24938
+ type: "text",
24939
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
24940
+ }
24941
+ ],
24942
+ isError: true
24943
+ };
24944
+ }
24206
24945
  }
24207
- }, async ({ round_id, duration_minutes }) => {
24208
- try {
24209
- const update = {
24210
- status: "completed",
24211
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
24212
- };
24213
- if (duration_minutes !== void 0) update.duration_minutes = duration_minutes;
24214
- const res = await apiPut(`/rounds/${round_id}`, update);
24215
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24216
- } catch (err) {
24217
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24946
+ );
24947
+ server.registerTool(
24948
+ "update_checkpoint",
24949
+ {
24950
+ description: "Update an existing checkpoint. Can connect or disconnect a launch via launch_id.",
24951
+ inputSchema: {
24952
+ checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID"),
24953
+ title: external_exports.string().nullable().optional().describe("New title (or null to clear)"),
24954
+ goal: external_exports.string().optional().describe("New goal (max 300 chars, brief overview)"),
24955
+ status: external_exports.string().optional().describe("New status (draft, pending, active, completed)"),
24956
+ deadline: external_exports.string().optional().describe("New deadline (ISO format)"),
24957
+ completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
24958
+ launch_id: external_exports.string().uuid().nullable().optional().describe("Launch UUID to connect (or null to disconnect)"),
24959
+ worktree_id: external_exports.string().uuid().nullable().optional().describe("Worktree UUID to assign (or null to unassign)"),
24960
+ assigned_to: external_exports.string().nullable().optional().describe("Who/what claimed this checkpoint"),
24961
+ branch_name: external_exports.string().nullable().optional().describe(
24962
+ "Git branch name for this checkpoint (e.g. feat/CHK-061-git-overhaul)"
24963
+ ),
24964
+ ideas: external_exports.array(
24965
+ external_exports.object({
24966
+ description: external_exports.string().describe("Idea description"),
24967
+ requirements: external_exports.array(external_exports.string()).optional().describe("List of requirements for this idea"),
24968
+ images: external_exports.array(external_exports.string()).optional().describe("Image URLs for this idea")
24969
+ })
24970
+ ).optional().describe(
24971
+ "Ideas array \u2014 each idea has description, requirements[], images[]"
24972
+ ),
24973
+ context: external_exports.any().optional().describe(
24974
+ "Context JSONB (decisions, discoveries, dependencies, constraints, qa_answers)"
24975
+ ),
24976
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
24977
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
24978
+ plan: external_exports.any().optional().describe("Plan JSONB (steps with title, description, scope)"),
24979
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
24980
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
24981
+ context_development: external_exports.any().optional().describe(
24982
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
24983
+ ),
24984
+ resources: external_exports.any().optional().describe(
24985
+ "Resources JSONB array [{url, description, type, added_by}]"
24986
+ )
24987
+ }
24988
+ },
24989
+ async ({
24990
+ checkpoint_id,
24991
+ title,
24992
+ goal,
24993
+ status,
24994
+ deadline,
24995
+ completed_at,
24996
+ launch_id,
24997
+ worktree_id,
24998
+ assigned_to,
24999
+ branch_name,
25000
+ ideas,
25001
+ context,
25002
+ research,
25003
+ qa,
25004
+ plan,
25005
+ user_context,
25006
+ is_claude_written,
25007
+ context_development,
25008
+ resources
25009
+ }) => {
25010
+ const update = {};
25011
+ if (title !== void 0) update.title = title;
25012
+ if (goal !== void 0) update.goal = goal;
25013
+ if (status !== void 0) update.status = status;
25014
+ if (deadline !== void 0) update.deadline = deadline;
25015
+ if (completed_at !== void 0) update.completed_at = completed_at;
25016
+ if (launch_id !== void 0) update.launch_id = launch_id;
25017
+ if (worktree_id !== void 0) update.worktree_id = worktree_id;
25018
+ if (assigned_to !== void 0) update.assigned_to = assigned_to;
25019
+ if (branch_name !== void 0) update.branch_name = branch_name;
25020
+ if (ideas !== void 0) update.ideas = ideas;
25021
+ if (context !== void 0) update.context = context;
25022
+ if (research !== void 0) update.research = research;
25023
+ if (qa !== void 0) update.qa = qa;
25024
+ if (plan !== void 0) update.plan = plan;
25025
+ if (user_context !== void 0) update.user_context = user_context;
25026
+ if (is_claude_written !== void 0)
25027
+ update.is_claude_written = is_claude_written;
25028
+ if (context_development !== void 0)
25029
+ update.context_development = context_development;
25030
+ if (resources !== void 0) update.resources = resources;
25031
+ if (Object.keys(update).length === 0) {
25032
+ return {
25033
+ content: [
25034
+ { type: "text", text: "Error: No fields to update" }
25035
+ ],
25036
+ isError: true
25037
+ };
25038
+ }
25039
+ try {
25040
+ const res = await apiPut(`/checkpoints/${checkpoint_id}`, update);
25041
+ return {
25042
+ content: [
25043
+ { type: "text", text: JSON.stringify(res, null, 2) }
25044
+ ]
25045
+ };
25046
+ } catch (err) {
25047
+ return {
25048
+ content: [
25049
+ {
25050
+ type: "text",
25051
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25052
+ }
25053
+ ],
25054
+ isError: true
25055
+ };
25056
+ }
24218
25057
  }
24219
- });
24220
- server.registerTool("create_launch", {
24221
- description: "Create a new launch for a repo.",
24222
- inputSchema: {
24223
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24224
- title: external_exports.string().describe("Launch title"),
24225
- type: external_exports.string().describe("Launch type"),
24226
- status: external_exports.string().optional().describe("Initial status (default: pending)"),
24227
- version: external_exports.string().optional().describe("Version string"),
24228
- user_requirements: external_exports.any().optional().describe("User requirements (JSON)")
25058
+ );
25059
+ server.registerTool(
25060
+ "complete_checkpoint",
25061
+ {
25062
+ description: "Mark a checkpoint as completed. Sets status to 'completed', completed_at to now, and triggers promotion (creates PR from feat branch to development).",
25063
+ inputSchema: {
25064
+ checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
25065
+ }
25066
+ },
25067
+ async ({ checkpoint_id }) => {
25068
+ try {
25069
+ const res = await apiPut(
25070
+ `/checkpoints/${checkpoint_id}`,
25071
+ {
25072
+ status: "completed",
25073
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
25074
+ }
25075
+ );
25076
+ const checkpoint = res.data;
25077
+ const featToDevResult = await promoteCheckpoint(checkpoint_id);
25078
+ let devToMainResult = null;
25079
+ const repoRes = await apiGet(
25080
+ `/repos/${checkpoint.repo_id}`
25081
+ );
25082
+ if (repoRes.data.auto_push_enabled) {
25083
+ devToMainResult = await promoteToMain(checkpoint.repo_id);
25084
+ }
25085
+ return {
25086
+ content: [
25087
+ {
25088
+ type: "text",
25089
+ text: JSON.stringify(
25090
+ {
25091
+ checkpoint,
25092
+ promotion: {
25093
+ feat_to_development: featToDevResult,
25094
+ development_to_main: devToMainResult
25095
+ }
25096
+ },
25097
+ null,
25098
+ 2
25099
+ )
25100
+ }
25101
+ ]
25102
+ };
25103
+ } catch (err) {
25104
+ return {
25105
+ content: [
25106
+ {
25107
+ type: "text",
25108
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25109
+ }
25110
+ ],
25111
+ isError: true
25112
+ };
25113
+ }
24229
25114
  }
24230
- }, async ({ repo_id, title, type, status, version: version2, user_requirements }) => {
24231
- try {
24232
- const res = await apiPost("/launches", {
24233
- repo_id,
24234
- title,
24235
- type,
24236
- status: status ?? "pending",
24237
- version: version2 ?? null,
24238
- user_requirements: user_requirements ?? null
24239
- });
24240
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24241
- } catch (err) {
24242
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25115
+ );
25116
+ server.registerTool(
25117
+ "create_task",
25118
+ {
25119
+ description: "Create a new task within a checkpoint or as a standalone task. Provide checkpoint_id for checkpoint tasks, or repo_id for standalone tasks.",
25120
+ inputSchema: {
25121
+ checkpoint_id: external_exports.string().uuid().optional().describe("The checkpoint UUID (for checkpoint-bound tasks)"),
25122
+ repo_id: external_exports.string().uuid().optional().describe("The repo UUID (for standalone tasks without checkpoint)"),
25123
+ title: external_exports.string().describe("Task title"),
25124
+ number: external_exports.number().int().optional().describe(
25125
+ "Task number (e.g. 1 for TASK-1). Auto-generated for standalone tasks if omitted."
25126
+ ),
25127
+ requirements: external_exports.string().optional().describe("Task requirements text"),
25128
+ status: external_exports.string().optional().describe("Initial status (default: pending)"),
25129
+ context: external_exports.any().optional().describe(
25130
+ "Context JSONB (decisions, discoveries, dependencies, constraints)"
25131
+ ),
25132
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
25133
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
25134
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
25135
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
25136
+ context_development: external_exports.any().optional().describe(
25137
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
25138
+ ),
25139
+ resources: external_exports.any().optional().describe(
25140
+ "Resources JSONB array [{url, description, type, added_by}]"
25141
+ )
25142
+ }
25143
+ },
25144
+ async ({
25145
+ checkpoint_id,
25146
+ repo_id,
25147
+ title,
25148
+ number: number3,
25149
+ requirements,
25150
+ status,
25151
+ context,
25152
+ qa,
25153
+ research,
25154
+ user_context,
25155
+ is_claude_written,
25156
+ context_development,
25157
+ resources
25158
+ }) => {
25159
+ try {
25160
+ const body = {
25161
+ title,
25162
+ requirements: requirements ?? null,
25163
+ status: status ?? "pending"
25164
+ };
25165
+ if (checkpoint_id) body.checkpoint_id = checkpoint_id;
25166
+ if (repo_id) body.repo_id = repo_id;
25167
+ if (number3 !== void 0) body.number = number3;
25168
+ if (context !== void 0) body.context = context;
25169
+ if (qa !== void 0) body.qa = qa;
25170
+ if (research !== void 0) body.research = research;
25171
+ if (user_context !== void 0) body.user_context = user_context;
25172
+ if (is_claude_written !== void 0)
25173
+ body.is_claude_written = is_claude_written;
25174
+ if (context_development !== void 0)
25175
+ body.context_development = context_development;
25176
+ if (resources !== void 0) body.resources = resources;
25177
+ const res = await apiPost("/tasks", body);
25178
+ return {
25179
+ content: [
25180
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25181
+ ]
25182
+ };
25183
+ } catch (err) {
25184
+ return {
25185
+ content: [
25186
+ {
25187
+ type: "text",
25188
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25189
+ }
25190
+ ],
25191
+ isError: true
25192
+ };
25193
+ }
24243
25194
  }
24244
- });
24245
- server.registerTool("update_launch", {
24246
- description: "Update an existing launch.",
24247
- inputSchema: {
24248
- launch_id: external_exports.string().uuid().describe("The launch UUID"),
24249
- title: external_exports.string().optional().describe("New title"),
24250
- type: external_exports.string().optional().describe("New type"),
24251
- status: external_exports.string().optional().describe("New status"),
24252
- version: external_exports.string().optional().describe("New version"),
24253
- user_requirements: external_exports.any().optional().describe("New user requirements (JSON)")
24254
- }
24255
- }, async ({ launch_id, title, type, status, version: version2, user_requirements }) => {
24256
- const update = {};
24257
- if (title !== void 0) update.title = title;
24258
- if (type !== void 0) update.type = type;
24259
- if (status !== void 0) update.status = status;
24260
- if (version2 !== void 0) update.version = version2;
24261
- if (user_requirements !== void 0) update.user_requirements = user_requirements;
24262
- if (Object.keys(update).length === 0) {
24263
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
25195
+ );
25196
+ server.registerTool(
25197
+ "update_task",
25198
+ {
25199
+ description: "Update an existing task. Use to update status, requirements, or file tracking. Pass claim_worktree_id when setting status to in_progress to auto-claim the parent checkpoint for the worktree.",
25200
+ inputSchema: {
25201
+ task_id: external_exports.string().uuid().describe("The task UUID"),
25202
+ title: external_exports.string().optional().describe("New title"),
25203
+ requirements: external_exports.string().optional().describe("New requirements text"),
25204
+ status: external_exports.string().optional().describe("New status (pending, in_progress, completed)"),
25205
+ files_changed: external_exports.array(
25206
+ external_exports.object({
25207
+ path: external_exports.string().describe("File path relative to repo root"),
25208
+ action: external_exports.string().describe("File action (new, modified, deleted)"),
25209
+ status: external_exports.string().describe("Approval status (approved, not_approved)"),
25210
+ claude_approved: external_exports.boolean().optional().describe(
25211
+ "Whether Claude's automated checks passed for this file"
25212
+ ),
25213
+ user_approved: external_exports.boolean().optional().describe(
25214
+ "Whether the user has approved this file (via git add or web UI)"
25215
+ ),
25216
+ app_approved: external_exports.boolean().optional().describe(
25217
+ "Whether the app has approved this file (via automated checks)"
25218
+ )
25219
+ })
25220
+ ).optional().describe("Files changed across all rounds"),
25221
+ claim_worktree_id: external_exports.string().uuid().optional().describe(
25222
+ "Worktree UUID to auto-claim the parent checkpoint when setting status to in_progress"
25223
+ ),
25224
+ context: external_exports.any().optional().describe(
25225
+ "Context JSONB (decisions, discoveries, dependencies, constraints)"
25226
+ ),
25227
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
25228
+ research: external_exports.any().optional().describe("Research JSONB (topics with findings and sources)"),
25229
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
25230
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
25231
+ context_development: external_exports.any().optional().describe(
25232
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
25233
+ ),
25234
+ resources: external_exports.any().optional().describe(
25235
+ "Resources JSONB array [{url, description, type, added_by}]"
25236
+ ),
25237
+ app_file_approval_by_user: external_exports.boolean().optional().describe(
25238
+ "Whether user interacted with file approvals via web UI. CLI resets to false after processing."
25239
+ )
25240
+ }
25241
+ },
25242
+ async ({
25243
+ task_id,
25244
+ title,
25245
+ requirements,
25246
+ status,
25247
+ files_changed,
25248
+ claim_worktree_id,
25249
+ context,
25250
+ qa,
25251
+ research,
25252
+ user_context,
25253
+ is_claude_written,
25254
+ context_development,
25255
+ resources,
25256
+ app_file_approval_by_user
25257
+ }) => {
25258
+ const update = {};
25259
+ if (title !== void 0) update.title = title;
25260
+ if (requirements !== void 0) update.requirements = requirements;
25261
+ if (status !== void 0) update.status = status;
25262
+ if (files_changed !== void 0) update.files_changed = files_changed;
25263
+ if (claim_worktree_id !== void 0)
25264
+ update.claim_worktree_id = claim_worktree_id;
25265
+ if (context !== void 0) update.context = context;
25266
+ if (qa !== void 0) update.qa = qa;
25267
+ if (research !== void 0) update.research = research;
25268
+ if (user_context !== void 0) update.user_context = user_context;
25269
+ if (is_claude_written !== void 0)
25270
+ update.is_claude_written = is_claude_written;
25271
+ if (context_development !== void 0)
25272
+ update.context_development = context_development;
25273
+ if (resources !== void 0) update.resources = resources;
25274
+ if (app_file_approval_by_user !== void 0)
25275
+ update.app_file_approval_by_user = app_file_approval_by_user;
25276
+ if (Object.keys(update).length === 0) {
25277
+ return {
25278
+ content: [
25279
+ { type: "text", text: "Error: No fields to update" }
25280
+ ],
25281
+ isError: true
25282
+ };
25283
+ }
25284
+ try {
25285
+ const res = await apiPut(
25286
+ `/tasks/${task_id}`,
25287
+ update
25288
+ );
25289
+ return {
25290
+ content: [
25291
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25292
+ ]
25293
+ };
25294
+ } catch (err) {
25295
+ return {
25296
+ content: [
25297
+ {
25298
+ type: "text",
25299
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25300
+ }
25301
+ ],
25302
+ isError: true
25303
+ };
25304
+ }
24264
25305
  }
24265
- try {
24266
- const res = await apiPut(`/launches/${launch_id}`, update);
24267
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24268
- } catch (err) {
24269
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25306
+ );
25307
+ server.registerTool(
25308
+ "complete_task",
25309
+ {
25310
+ description: "Mark a task as completed. Sets status to 'completed' and completed_at to now.",
25311
+ inputSchema: {
25312
+ task_id: external_exports.string().uuid().describe("The task UUID")
25313
+ }
25314
+ },
25315
+ async ({ task_id }) => {
25316
+ try {
25317
+ let hasUnapproved = false;
25318
+ let unapprovedCount = 0;
25319
+ let resolvedRepoId = null;
25320
+ try {
25321
+ const taskRes = await apiGet(
25322
+ `/tasks/${task_id}`
25323
+ );
25324
+ const checkpointRes = await apiGet(
25325
+ `/checkpoints/${taskRes.data.checkpoint_id}`
25326
+ );
25327
+ resolvedRepoId = checkpointRes.data.repo_id;
25328
+ const fileChangesRes = await apiGet("/file-changes", {
25329
+ repo_id: resolvedRepoId,
25330
+ task_id
25331
+ });
25332
+ const unapproved = (fileChangesRes.data ?? []).filter(
25333
+ (f) => !f.user_approved
25334
+ );
25335
+ unapprovedCount = unapproved.length;
25336
+ hasUnapproved = unapprovedCount > 0;
25337
+ } catch {
25338
+ }
25339
+ if (hasUnapproved) {
25340
+ return {
25341
+ content: [
25342
+ {
25343
+ type: "text",
25344
+ text: `Error: Cannot complete task \u2014 ${unapprovedCount} file(s) are not approved. All files must be user_approved before task completion.`
25345
+ }
25346
+ ],
25347
+ isError: true
25348
+ };
25349
+ }
25350
+ const res = await apiPut(`/tasks/${task_id}`, {
25351
+ status: "completed",
25352
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
25353
+ });
25354
+ if (resolvedRepoId) {
25355
+ try {
25356
+ await apiPatch("/file-changes", {
25357
+ action: "lock_task",
25358
+ task_id,
25359
+ repo_id: resolvedRepoId
25360
+ });
25361
+ } catch {
25362
+ }
25363
+ }
25364
+ return {
25365
+ content: [
25366
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25367
+ ]
25368
+ };
25369
+ } catch (err) {
25370
+ return {
25371
+ content: [
25372
+ {
25373
+ type: "text",
25374
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25375
+ }
25376
+ ],
25377
+ isError: true
25378
+ };
25379
+ }
24270
25380
  }
24271
- });
24272
- server.registerTool("delete_launch", {
24273
- description: "Delete a launch by ID.",
24274
- inputSchema: {
24275
- launch_id: external_exports.string().uuid().describe("The launch UUID")
25381
+ );
25382
+ server.registerTool(
25383
+ "add_round",
25384
+ {
25385
+ description: "Add a round to a task.",
25386
+ inputSchema: {
25387
+ task_id: external_exports.string().uuid().describe("The task UUID"),
25388
+ number: external_exports.number().int().describe("Round number"),
25389
+ requirements: external_exports.string().optional().describe("Round requirements text"),
25390
+ status: external_exports.string().optional().describe("Initial status (default: pending)"),
25391
+ started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
25392
+ triggered_by: external_exports.enum(["user", "claude"]).optional().describe("Who triggered the round (user or claude)"),
25393
+ context: external_exports.any().optional().describe("Context JSONB"),
25394
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
25395
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
25396
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
25397
+ context_development: external_exports.any().optional().describe(
25398
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
25399
+ ),
25400
+ resources: external_exports.any().optional().describe(
25401
+ "Resources JSONB array [{url, description, type, added_by}]"
25402
+ )
25403
+ }
25404
+ },
25405
+ async ({
25406
+ task_id,
25407
+ number: number3,
25408
+ requirements,
25409
+ status,
25410
+ started_at,
25411
+ triggered_by,
25412
+ context,
25413
+ qa,
25414
+ user_context,
25415
+ is_claude_written,
25416
+ context_development,
25417
+ resources
25418
+ }) => {
25419
+ try {
25420
+ const body = {
25421
+ task_id,
25422
+ number: number3,
25423
+ requirements: requirements ?? null,
25424
+ status: status ?? "pending",
25425
+ started_at: started_at ?? null
25426
+ };
25427
+ if (triggered_by !== void 0) body.triggered_by = triggered_by;
25428
+ if (context !== void 0) body.context = context;
25429
+ if (qa !== void 0) body.qa = qa;
25430
+ if (user_context !== void 0) body.user_context = user_context;
25431
+ if (is_claude_written !== void 0)
25432
+ body.is_claude_written = is_claude_written;
25433
+ if (context_development !== void 0)
25434
+ body.context_development = context_development;
25435
+ if (resources !== void 0) body.resources = resources;
25436
+ const res = await apiPost("/rounds", body);
25437
+ return {
25438
+ content: [
25439
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25440
+ ]
25441
+ };
25442
+ } catch (err) {
25443
+ return {
25444
+ content: [
25445
+ {
25446
+ type: "text",
25447
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25448
+ }
25449
+ ],
25450
+ isError: true
25451
+ };
25452
+ }
24276
25453
  }
24277
- }, async ({ launch_id }) => {
24278
- try {
24279
- await apiDelete(`/launches/${launch_id}`);
24280
- return { content: [{ type: "text", text: "Launch deleted successfully" }] };
24281
- } catch (err) {
24282
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25454
+ );
25455
+ server.registerTool(
25456
+ "update_round",
25457
+ {
25458
+ description: "Update an existing round.",
25459
+ inputSchema: {
25460
+ round_id: external_exports.string().uuid().describe("The round UUID"),
25461
+ requirements: external_exports.string().optional().describe("Round requirements text"),
25462
+ status: external_exports.string().optional().describe("New status (pending, in_progress, completed)"),
25463
+ started_at: external_exports.string().optional().describe("Start timestamp (ISO format)"),
25464
+ completed_at: external_exports.string().optional().describe("Completion timestamp (ISO format)"),
25465
+ duration_minutes: external_exports.number().int().optional().describe("Duration in minutes"),
25466
+ files_changed: external_exports.array(
25467
+ external_exports.object({
25468
+ path: external_exports.string().describe("File path relative to repo root"),
25469
+ action: external_exports.string().describe("File action (new, modified, deleted)"),
25470
+ status: external_exports.string().describe("Approval status (approved, not_approved)"),
25471
+ claude_approved: external_exports.boolean().optional().describe(
25472
+ "Whether Claude's automated checks passed for this file"
25473
+ ),
25474
+ user_approved: external_exports.boolean().optional().describe(
25475
+ "Whether the user has approved this file (via git add or web UI)"
25476
+ ),
25477
+ app_approved: external_exports.boolean().optional().describe(
25478
+ "Whether the app has approved this file (via automated checks)"
25479
+ )
25480
+ })
25481
+ ).optional().describe("Files changed in this round with approval status"),
25482
+ context: external_exports.any().optional().describe("Context JSONB"),
25483
+ qa: external_exports.any().optional().describe("QA JSONB (checklist items with type, check, status)"),
25484
+ user_context: external_exports.string().optional().describe("Original user input text (never overwritten once set)"),
25485
+ is_claude_written: external_exports.boolean().optional().describe("Whether Claude wrote the user_context"),
25486
+ context_development: external_exports.any().optional().describe(
25487
+ "Context development JSONB (discussion entries, Q&A, assessment decisions)"
25488
+ ),
25489
+ resources: external_exports.any().optional().describe(
25490
+ "Resources JSONB array [{url, description, type, added_by}]"
25491
+ )
25492
+ }
25493
+ },
25494
+ async ({
25495
+ round_id,
25496
+ requirements,
25497
+ status,
25498
+ started_at,
25499
+ completed_at,
25500
+ duration_minutes,
25501
+ files_changed,
25502
+ context,
25503
+ qa,
25504
+ user_context,
25505
+ is_claude_written,
25506
+ context_development,
25507
+ resources
25508
+ }) => {
25509
+ const update = {};
25510
+ if (requirements !== void 0) update.requirements = requirements;
25511
+ if (status !== void 0) update.status = status;
25512
+ if (started_at !== void 0) update.started_at = started_at;
25513
+ if (completed_at !== void 0) update.completed_at = completed_at;
25514
+ if (duration_minutes !== void 0)
25515
+ update.duration_minutes = duration_minutes;
25516
+ if (files_changed !== void 0) update.files_changed = files_changed;
25517
+ if (context !== void 0) update.context = context;
25518
+ if (qa !== void 0) update.qa = qa;
25519
+ if (user_context !== void 0) update.user_context = user_context;
25520
+ if (is_claude_written !== void 0)
25521
+ update.is_claude_written = is_claude_written;
25522
+ if (context_development !== void 0)
25523
+ update.context_development = context_development;
25524
+ if (resources !== void 0) update.resources = resources;
25525
+ if (Object.keys(update).length === 0) {
25526
+ return {
25527
+ content: [
25528
+ { type: "text", text: "Error: No fields to update" }
25529
+ ],
25530
+ isError: true
25531
+ };
25532
+ }
25533
+ try {
25534
+ const res = await apiPut(
25535
+ `/rounds/${round_id}`,
25536
+ update
25537
+ );
25538
+ if (files_changed && files_changed.length > 0) {
25539
+ try {
25540
+ const taskRes = await apiGet(
25541
+ `/tasks/${res.data.task_id}`
25542
+ );
25543
+ const checkpointRes = await apiGet(
25544
+ `/checkpoints/${taskRes.data.checkpoint_id}`
25545
+ );
25546
+ await apiPatch("/file-changes", {
25547
+ action: "replace_round_files",
25548
+ round_id,
25549
+ repo_id: checkpointRes.data.repo_id,
25550
+ checkpoint_id: taskRes.data.checkpoint_id,
25551
+ task_id: res.data.task_id,
25552
+ source: "round",
25553
+ files: files_changed.map(
25554
+ (f) => ({
25555
+ file_path: f.path,
25556
+ action: f.action,
25557
+ claude_approved: f.claude_approved ?? false,
25558
+ user_approved: f.user_approved ?? false
25559
+ })
25560
+ )
25561
+ });
25562
+ } catch {
25563
+ }
25564
+ }
25565
+ return {
25566
+ content: [
25567
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25568
+ ]
25569
+ };
25570
+ } catch (err) {
25571
+ return {
25572
+ content: [
25573
+ {
25574
+ type: "text",
25575
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25576
+ }
25577
+ ],
25578
+ isError: true
25579
+ };
25580
+ }
24283
25581
  }
24284
- });
24285
- server.registerTool("create_session_log", {
24286
- description: "Create a new session log for a repo.",
24287
- inputSchema: {
24288
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24289
- session_date: external_exports.string().describe("Session date (YYYY-MM-DD)"),
24290
- day_number: external_exports.number().int().describe("Day number (e.g. 41 for D-41)"),
24291
- session_number: external_exports.number().int().optional().describe("Session number within the day (default: 1)"),
24292
- content: external_exports.any().optional().describe("Session log content (JSON)"),
24293
- worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID that created this session log")
25582
+ );
25583
+ server.registerTool(
25584
+ "complete_round",
25585
+ {
25586
+ description: "Mark a round as completed. Sets status to 'completed' and completed_at to now.",
25587
+ inputSchema: {
25588
+ round_id: external_exports.string().uuid().describe("The round UUID"),
25589
+ duration_minutes: external_exports.number().int().optional().describe("Duration in minutes")
25590
+ }
25591
+ },
25592
+ async ({ round_id, duration_minutes }) => {
25593
+ try {
25594
+ const update = {
25595
+ status: "completed",
25596
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
25597
+ };
25598
+ if (duration_minutes !== void 0)
25599
+ update.duration_minutes = duration_minutes;
25600
+ const res = await apiPut(
25601
+ `/rounds/${round_id}`,
25602
+ update
25603
+ );
25604
+ let unapprovedFiles = [];
25605
+ try {
25606
+ const taskRes = await apiGet(
25607
+ `/tasks/${res.data.task_id}`
25608
+ );
25609
+ const checkpointRes = await apiGet(
25610
+ `/checkpoints/${taskRes.data.checkpoint_id}`
25611
+ );
25612
+ const repoId = checkpointRes.data.repo_id;
25613
+ await apiPatch("/file-changes", {
25614
+ action: "lock_round",
25615
+ round_id,
25616
+ repo_id: repoId
25617
+ });
25618
+ const fileChangesRes = await apiGet("/file-changes", {
25619
+ repo_id: repoId,
25620
+ round_id
25621
+ });
25622
+ unapprovedFiles = (fileChangesRes.data ?? []).filter((f) => !f.user_approved).map((f) => ({ file_path: f.file_path, action: f.action }));
25623
+ } catch {
25624
+ }
25625
+ const result = {
25626
+ ...res.data,
25627
+ unapproved_files: unapprovedFiles,
25628
+ unapproved_count: unapprovedFiles.length
25629
+ };
25630
+ return {
25631
+ content: [
25632
+ { type: "text", text: JSON.stringify(result, null, 2) }
25633
+ ]
25634
+ };
25635
+ } catch (err) {
25636
+ return {
25637
+ content: [
25638
+ {
25639
+ type: "text",
25640
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25641
+ }
25642
+ ],
25643
+ isError: true
25644
+ };
25645
+ }
24294
25646
  }
24295
- }, async ({ repo_id, session_date, day_number, session_number, content, worktree_id }) => {
24296
- try {
24297
- const body = {
24298
- repo_id,
24299
- session_date,
24300
- day_number,
24301
- session_number: session_number ?? 1,
24302
- content: content ?? null
24303
- };
24304
- if (worktree_id) body.worktree_id = worktree_id;
24305
- const res = await apiPost("/session-logs", body);
24306
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24307
- } catch (err) {
24308
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25647
+ );
25648
+ server.registerTool(
25649
+ "create_launch",
25650
+ {
25651
+ description: "Create a new launch for a repo.",
25652
+ inputSchema: {
25653
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
25654
+ title: external_exports.string().describe("Launch title"),
25655
+ type: external_exports.string().describe("Launch type"),
25656
+ status: external_exports.string().optional().describe("Initial status (default: pending)"),
25657
+ version: external_exports.string().optional().describe("Version string"),
25658
+ user_requirements: external_exports.any().optional().describe("User requirements (JSON)")
25659
+ }
25660
+ },
25661
+ async ({ repo_id, title, type, status, version: version2, user_requirements }) => {
25662
+ try {
25663
+ const res = await apiPost("/launches", {
25664
+ repo_id,
25665
+ title,
25666
+ type,
25667
+ status: status ?? "pending",
25668
+ version: version2 ?? null,
25669
+ user_requirements: user_requirements ?? null
25670
+ });
25671
+ return {
25672
+ content: [
25673
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25674
+ ]
25675
+ };
25676
+ } catch (err) {
25677
+ return {
25678
+ content: [
25679
+ {
25680
+ type: "text",
25681
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25682
+ }
25683
+ ],
25684
+ isError: true
25685
+ };
25686
+ }
24309
25687
  }
24310
- });
24311
- server.registerTool("update_session_log", {
24312
- description: "Update an existing session log.",
24313
- inputSchema: {
24314
- session_log_id: external_exports.string().uuid().describe("The session log UUID"),
24315
- session_date: external_exports.string().optional().describe("New session date"),
24316
- day_number: external_exports.number().int().optional().describe("New day number"),
24317
- session_number: external_exports.number().int().optional().describe("New session number"),
24318
- content: external_exports.any().optional().describe("New content (JSON)")
24319
- }
24320
- }, async ({ session_log_id, session_date, day_number, session_number, content }) => {
24321
- const update = {};
24322
- if (session_date !== void 0) update.session_date = session_date;
24323
- if (day_number !== void 0) update.day_number = day_number;
24324
- if (session_number !== void 0) update.session_number = session_number;
24325
- if (content !== void 0) update.content = content;
24326
- if (Object.keys(update).length === 0) {
24327
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
25688
+ );
25689
+ server.registerTool(
25690
+ "update_launch",
25691
+ {
25692
+ description: "Update an existing launch.",
25693
+ inputSchema: {
25694
+ launch_id: external_exports.string().uuid().describe("The launch UUID"),
25695
+ title: external_exports.string().optional().describe("New title"),
25696
+ type: external_exports.string().optional().describe("New type"),
25697
+ status: external_exports.string().optional().describe("New status"),
25698
+ version: external_exports.string().optional().describe("New version"),
25699
+ user_requirements: external_exports.any().optional().describe("New user requirements (JSON)")
25700
+ }
25701
+ },
25702
+ async ({ launch_id, title, type, status, version: version2, user_requirements }) => {
25703
+ const update = {};
25704
+ if (title !== void 0) update.title = title;
25705
+ if (type !== void 0) update.type = type;
25706
+ if (status !== void 0) update.status = status;
25707
+ if (version2 !== void 0) update.version = version2;
25708
+ if (user_requirements !== void 0)
25709
+ update.user_requirements = user_requirements;
25710
+ if (Object.keys(update).length === 0) {
25711
+ return {
25712
+ content: [
25713
+ { type: "text", text: "Error: No fields to update" }
25714
+ ],
25715
+ isError: true
25716
+ };
25717
+ }
25718
+ try {
25719
+ const res = await apiPut(
25720
+ `/launches/${launch_id}`,
25721
+ update
25722
+ );
25723
+ return {
25724
+ content: [
25725
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25726
+ ]
25727
+ };
25728
+ } catch (err) {
25729
+ return {
25730
+ content: [
25731
+ {
25732
+ type: "text",
25733
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25734
+ }
25735
+ ],
25736
+ isError: true
25737
+ };
25738
+ }
24328
25739
  }
24329
- try {
24330
- const res = await apiPut(`/session-logs/${session_log_id}`, update);
24331
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24332
- } catch (err) {
24333
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25740
+ );
25741
+ server.registerTool(
25742
+ "delete_launch",
25743
+ {
25744
+ description: "Delete a launch by ID.",
25745
+ inputSchema: {
25746
+ launch_id: external_exports.string().uuid().describe("The launch UUID")
25747
+ }
25748
+ },
25749
+ async ({ launch_id }) => {
25750
+ try {
25751
+ await apiDelete(`/launches/${launch_id}`);
25752
+ return {
25753
+ content: [
25754
+ { type: "text", text: "Launch deleted successfully" }
25755
+ ]
25756
+ };
25757
+ } catch (err) {
25758
+ return {
25759
+ content: [
25760
+ {
25761
+ type: "text",
25762
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25763
+ }
25764
+ ],
25765
+ isError: true
25766
+ };
25767
+ }
24334
25768
  }
24335
- });
24336
- server.registerTool("delete_session_log", {
24337
- description: "Delete a session log by ID.",
24338
- inputSchema: {
24339
- session_log_id: external_exports.string().uuid().describe("The session log UUID")
25769
+ );
25770
+ server.registerTool(
25771
+ "create_session_log",
25772
+ {
25773
+ description: "Create a new session log for a repo.",
25774
+ inputSchema: {
25775
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
25776
+ session_date: external_exports.string().describe("Session date (YYYY-MM-DD)"),
25777
+ day_number: external_exports.number().int().describe("Day number (e.g. 41 for D-41)"),
25778
+ session_number: external_exports.number().int().optional().describe("Session number within the day (default: 1)"),
25779
+ content: external_exports.any().optional().describe("Session log content (JSON)"),
25780
+ worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID that created this session log")
25781
+ }
25782
+ },
25783
+ async ({
25784
+ repo_id,
25785
+ session_date,
25786
+ day_number,
25787
+ session_number,
25788
+ content,
25789
+ worktree_id
25790
+ }) => {
25791
+ try {
25792
+ const body = {
25793
+ repo_id,
25794
+ session_date,
25795
+ day_number,
25796
+ session_number: session_number ?? 1,
25797
+ content: content ?? null
25798
+ };
25799
+ if (worktree_id) body.worktree_id = worktree_id;
25800
+ const res = await apiPost(
25801
+ "/session-logs",
25802
+ body
25803
+ );
25804
+ return {
25805
+ content: [
25806
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25807
+ ]
25808
+ };
25809
+ } catch (err) {
25810
+ return {
25811
+ content: [
25812
+ {
25813
+ type: "text",
25814
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25815
+ }
25816
+ ],
25817
+ isError: true
25818
+ };
25819
+ }
24340
25820
  }
24341
- }, async ({ session_log_id }) => {
24342
- try {
24343
- await apiDelete(`/session-logs/${session_log_id}`);
24344
- return { content: [{ type: "text", text: "Session log deleted successfully" }] };
24345
- } catch (err) {
24346
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25821
+ );
25822
+ server.registerTool(
25823
+ "update_session_log",
25824
+ {
25825
+ description: "Update an existing session log.",
25826
+ inputSchema: {
25827
+ session_log_id: external_exports.string().uuid().describe("The session log UUID"),
25828
+ session_date: external_exports.string().optional().describe("New session date"),
25829
+ day_number: external_exports.number().int().optional().describe("New day number"),
25830
+ session_number: external_exports.number().int().optional().describe("New session number"),
25831
+ content: external_exports.any().optional().describe("New content (JSON)")
25832
+ }
25833
+ },
25834
+ async ({
25835
+ session_log_id,
25836
+ session_date,
25837
+ day_number,
25838
+ session_number,
25839
+ content
25840
+ }) => {
25841
+ const update = {};
25842
+ if (session_date !== void 0) update.session_date = session_date;
25843
+ if (day_number !== void 0) update.day_number = day_number;
25844
+ if (session_number !== void 0) update.session_number = session_number;
25845
+ if (content !== void 0) update.content = content;
25846
+ if (Object.keys(update).length === 0) {
25847
+ return {
25848
+ content: [
25849
+ { type: "text", text: "Error: No fields to update" }
25850
+ ],
25851
+ isError: true
25852
+ };
25853
+ }
25854
+ try {
25855
+ const res = await apiPut(
25856
+ `/session-logs/${session_log_id}`,
25857
+ update
25858
+ );
25859
+ return {
25860
+ content: [
25861
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
25862
+ ]
25863
+ };
25864
+ } catch (err) {
25865
+ return {
25866
+ content: [
25867
+ {
25868
+ type: "text",
25869
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25870
+ }
25871
+ ],
25872
+ isError: true
25873
+ };
25874
+ }
24347
25875
  }
24348
- });
24349
- server.registerTool("sync_claude_files", {
24350
- 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.",
24351
- inputSchema: {
24352
- repo_id: external_exports.string().uuid().describe("Repository ID to sync files for"),
24353
- project_path: external_exports.string().describe("Absolute path to the project root directory")
25876
+ );
25877
+ server.registerTool(
25878
+ "delete_session_log",
25879
+ {
25880
+ description: "Delete a session log by ID.",
25881
+ inputSchema: {
25882
+ session_log_id: external_exports.string().uuid().describe("The session log UUID")
25883
+ }
25884
+ },
25885
+ async ({ session_log_id }) => {
25886
+ try {
25887
+ await apiDelete(`/session-logs/${session_log_id}`);
25888
+ return {
25889
+ content: [
25890
+ { type: "text", text: "Session log deleted successfully" }
25891
+ ]
25892
+ };
25893
+ } catch (err) {
25894
+ return {
25895
+ content: [
25896
+ {
25897
+ type: "text",
25898
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25899
+ }
25900
+ ],
25901
+ isError: true
25902
+ };
25903
+ }
24354
25904
  }
24355
- }, async ({ repo_id, project_path }) => {
24356
- try {
24357
- const syncResult = await executeSyncToLocal({ repoId: repo_id, projectPath: project_path });
24358
- const { byType, totals, dbOnlyFiles } = syncResult;
24359
- const summary = {
24360
- ...byType,
24361
- totals: { created: totals.created, updated: totals.updated, deleted: totals.deleted },
24362
- message: totals.created + totals.updated + totals.deleted === 0 ? "All files up to date" : `Synced: ${totals.created} created, ${totals.updated} updated, ${totals.deleted} deleted`
24363
- };
24364
- if (dbOnlyFiles.length > 0) {
24365
- summary.db_only_files = dbOnlyFiles;
24366
- summary.message += `
25905
+ );
25906
+ server.registerTool(
25907
+ "sync_claude_files",
25908
+ {
25909
+ 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.",
25910
+ inputSchema: {
25911
+ repo_id: external_exports.string().uuid().describe("Repository ID to sync files for"),
25912
+ project_path: external_exports.string().describe("Absolute path to the project root directory")
25913
+ }
25914
+ },
25915
+ async ({ repo_id, project_path }) => {
25916
+ try {
25917
+ const syncResult = await executeSyncToLocal({
25918
+ repoId: repo_id,
25919
+ projectPath: project_path
25920
+ });
25921
+ const { byType, totals, dbOnlyFiles } = syncResult;
25922
+ const summary = {
25923
+ ...byType,
25924
+ totals: {
25925
+ created: totals.created,
25926
+ updated: totals.updated,
25927
+ deleted: totals.deleted
25928
+ },
25929
+ message: totals.created + totals.updated + totals.deleted === 0 ? "All files up to date" : `Synced: ${totals.created} created, ${totals.updated} updated, ${totals.deleted} deleted`
25930
+ };
25931
+ if (dbOnlyFiles.length > 0) {
25932
+ summary.db_only_files = dbOnlyFiles;
25933
+ summary.message += `
24367
25934
 
24368
25935
  ${dbOnlyFiles.length} file(s) exist in DB but were missing locally (recreated). Use delete_claude_files to remove them from DB if they were intentionally deleted.`;
25936
+ }
25937
+ return {
25938
+ content: [
25939
+ { type: "text", text: JSON.stringify(summary, null, 2) }
25940
+ ]
25941
+ };
25942
+ } catch (err) {
25943
+ return {
25944
+ content: [
25945
+ {
25946
+ type: "text",
25947
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
25948
+ }
25949
+ ],
25950
+ isError: true
25951
+ };
24369
25952
  }
24370
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
24371
- } catch (err) {
24372
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24373
- }
24374
- });
24375
- server.registerTool("delete_claude_files", {
24376
- description: "Soft-delete claude files from the CodeByPlan DB. Use after sync_claude_files reports db_only_files that should be removed. Each file is identified by type, name, and optional category.",
24377
- inputSchema: {
24378
- repo_id: external_exports.string().uuid().describe("Repository ID"),
24379
- files: external_exports.array(external_exports.object({
24380
- type: external_exports.string().describe("File type: command, agent, skill, rule, hook, template, docs_stack"),
24381
- name: external_exports.string().describe("File name"),
24382
- category: external_exports.string().nullable().optional().describe("Category (for commands: e.g. 'development/checkpoint')")
24383
- })).describe("Files to soft-delete from DB")
24384
- }
24385
- }, async ({ repo_id, files }) => {
24386
- try {
24387
- const res = await apiPost("/sync/files", {
24388
- repo_id,
24389
- delete_keys: files
24390
- });
24391
- return {
24392
- content: [{
24393
- type: "text",
24394
- text: JSON.stringify({
24395
- deleted: res.data.deleted,
24396
- message: `Soft-deleted ${res.data.deleted} file(s) from DB. Run sync_claude_files to clean up local copies.`
24397
- }, null, 2)
24398
- }]
24399
- };
24400
- } catch (err) {
24401
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24402
25953
  }
24403
- });
24404
- server.registerTool("update_session_state", {
24405
- description: "Update session state for a repo. Actions: activate (deactivates other repos), deactivate, pause, refresh, clear_refresh.",
24406
- inputSchema: {
24407
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24408
- action: external_exports.enum(["activate", "deactivate", "pause", "refresh", "clear_refresh"]).describe("Session action to perform")
24409
- }
24410
- }, async ({ repo_id, action }) => {
24411
- try {
24412
- const res = await apiPatch(`/repos/${repo_id}/session`, { action });
24413
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24414
- } catch (err) {
24415
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24416
- }
24417
- });
24418
- server.registerTool("update_server_config", {
24419
- description: "Update server configuration for a repo (port, type, active servers). Active server entries can include worktree_id to track which worktree is running the server.",
24420
- inputSchema: {
24421
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24422
- server_port: external_exports.number().int().optional().describe("Server port number"),
24423
- server_type: external_exports.string().optional().describe("Server type"),
24424
- active_servers: external_exports.any().optional().describe("Active servers (JSON array)")
24425
- }
24426
- }, async ({ repo_id, server_port, server_type, active_servers }) => {
24427
- const body = {};
24428
- if (server_port !== void 0) body.server_port = server_port;
24429
- if (server_type !== void 0) body.server_type = server_type;
24430
- if (active_servers !== void 0) body.active_servers = active_servers;
24431
- if (Object.keys(body).length === 0) {
24432
- return { content: [{ type: "text", text: "Error: No fields to update" }], isError: true };
24433
- }
24434
- try {
24435
- const res = await apiPatch(`/repos/${repo_id}/server`, body);
24436
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24437
- } catch (err) {
24438
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
25954
+ );
25955
+ server.registerTool(
25956
+ "delete_claude_files",
25957
+ {
25958
+ description: "Soft-delete claude files from the CodeByPlan DB. Use after sync_claude_files reports db_only_files that should be removed. Each file is identified by type, name, and optional category.",
25959
+ inputSchema: {
25960
+ repo_id: external_exports.string().uuid().describe("Repository ID"),
25961
+ files: external_exports.array(
25962
+ external_exports.object({
25963
+ type: external_exports.string().describe(
25964
+ "File type: command, agent, skill, rule, hook, template, docs_stack, context"
25965
+ ),
25966
+ name: external_exports.string().describe("File name"),
25967
+ category: external_exports.string().nullable().optional().describe(
25968
+ "Category (for commands: e.g. 'development/checkpoint')"
25969
+ )
25970
+ })
25971
+ ).describe("Files to soft-delete from DB")
25972
+ }
25973
+ },
25974
+ async ({ repo_id, files }) => {
25975
+ try {
25976
+ const res = await apiPost("/sync/files", {
25977
+ repo_id,
25978
+ delete_keys: files
25979
+ });
25980
+ return {
25981
+ content: [
25982
+ {
25983
+ type: "text",
25984
+ text: JSON.stringify(
25985
+ {
25986
+ deleted: res.data.deleted,
25987
+ message: `Soft-deleted ${res.data.deleted} file(s) from DB. Run sync_claude_files to clean up local copies.`
25988
+ },
25989
+ null,
25990
+ 2
25991
+ )
25992
+ }
25993
+ ]
25994
+ };
25995
+ } catch (err) {
25996
+ return {
25997
+ content: [
25998
+ {
25999
+ type: "text",
26000
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26001
+ }
26002
+ ],
26003
+ isError: true
26004
+ };
26005
+ }
24439
26006
  }
24440
- });
24441
- server.registerTool("create_worktree", {
24442
- description: "Create a new worktree for a repo.",
24443
- inputSchema: {
24444
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24445
- name: external_exports.string().describe("Worktree name (unique per repo)"),
24446
- path: external_exports.string().optional().describe("Local filesystem path"),
24447
- status: external_exports.string().optional().describe("Initial status (default: active)")
26007
+ );
26008
+ server.registerTool(
26009
+ "update_session_state",
26010
+ {
26011
+ description: "Update session state for a repo. Actions: activate (deactivates other repos), deactivate, pause, refresh, clear_refresh.",
26012
+ inputSchema: {
26013
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26014
+ action: external_exports.enum(["activate", "deactivate", "pause", "refresh", "clear_refresh"]).describe("Session action to perform")
26015
+ }
26016
+ },
26017
+ async ({ repo_id, action }) => {
26018
+ try {
26019
+ const res = await apiPatch(
26020
+ `/repos/${repo_id}/session`,
26021
+ { action }
26022
+ );
26023
+ return {
26024
+ content: [
26025
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
26026
+ ]
26027
+ };
26028
+ } catch (err) {
26029
+ return {
26030
+ content: [
26031
+ {
26032
+ type: "text",
26033
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26034
+ }
26035
+ ],
26036
+ isError: true
26037
+ };
26038
+ }
24448
26039
  }
24449
- }, async ({ repo_id, name, path, status }) => {
24450
- try {
24451
- const res = await apiPost("/worktrees", {
24452
- repo_id,
24453
- name,
24454
- path: path ?? null,
24455
- status: status ?? "active"
24456
- });
24457
- return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
24458
- } catch (err) {
24459
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
26040
+ );
26041
+ server.registerTool(
26042
+ "update_server_config",
26043
+ {
26044
+ description: "Update server configuration for a repo (port, type, active servers). Active server entries can include worktree_id to track which worktree is running the server.",
26045
+ inputSchema: {
26046
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26047
+ server_port: external_exports.number().int().optional().describe("Server port number"),
26048
+ server_type: external_exports.string().optional().describe("Server type"),
26049
+ active_servers: external_exports.any().optional().describe("Active servers (JSON array)")
26050
+ }
26051
+ },
26052
+ async ({ repo_id, server_port, server_type, active_servers }) => {
26053
+ const body = {};
26054
+ if (server_port !== void 0) body.server_port = server_port;
26055
+ if (server_type !== void 0) body.server_type = server_type;
26056
+ if (active_servers !== void 0) body.active_servers = active_servers;
26057
+ if (Object.keys(body).length === 0) {
26058
+ return {
26059
+ content: [
26060
+ { type: "text", text: "Error: No fields to update" }
26061
+ ],
26062
+ isError: true
26063
+ };
26064
+ }
26065
+ try {
26066
+ const res = await apiPatch(
26067
+ `/repos/${repo_id}/server`,
26068
+ body
26069
+ );
26070
+ return {
26071
+ content: [
26072
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
26073
+ ]
26074
+ };
26075
+ } catch (err) {
26076
+ return {
26077
+ content: [
26078
+ {
26079
+ type: "text",
26080
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26081
+ }
26082
+ ],
26083
+ isError: true
26084
+ };
26085
+ }
24460
26086
  }
24461
- });
24462
- server.registerTool("delete_worktree", {
24463
- description: "Delete a worktree by ID.",
24464
- inputSchema: {
24465
- worktree_id: external_exports.string().uuid().describe("The worktree UUID")
26087
+ );
26088
+ server.registerTool(
26089
+ "create_worktree",
26090
+ {
26091
+ description: "Create a new worktree for a repo.",
26092
+ inputSchema: {
26093
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26094
+ name: external_exports.string().describe("Worktree name (unique per repo)"),
26095
+ path: external_exports.string().optional().describe("Local filesystem path"),
26096
+ status: external_exports.string().optional().describe("Initial status (default: active)")
26097
+ }
26098
+ },
26099
+ async ({ repo_id, name, path, status }) => {
26100
+ try {
26101
+ const res = await apiPost("/worktrees", {
26102
+ repo_id,
26103
+ name,
26104
+ path: path ?? null,
26105
+ status: status ?? "active"
26106
+ });
26107
+ return {
26108
+ content: [
26109
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
26110
+ ]
26111
+ };
26112
+ } catch (err) {
26113
+ return {
26114
+ content: [
26115
+ {
26116
+ type: "text",
26117
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26118
+ }
26119
+ ],
26120
+ isError: true
26121
+ };
26122
+ }
24466
26123
  }
24467
- }, async ({ worktree_id }) => {
24468
- try {
24469
- await apiDelete(`/worktrees/${worktree_id}`);
24470
- return { content: [{ type: "text", text: "Worktree deleted successfully" }] };
24471
- } catch (err) {
24472
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
26124
+ );
26125
+ server.registerTool(
26126
+ "delete_worktree",
26127
+ {
26128
+ description: "Delete a worktree by ID.",
26129
+ inputSchema: {
26130
+ worktree_id: external_exports.string().uuid().describe("The worktree UUID")
26131
+ }
26132
+ },
26133
+ async ({ worktree_id }) => {
26134
+ try {
26135
+ await apiDelete(`/worktrees/${worktree_id}`);
26136
+ return {
26137
+ content: [
26138
+ { type: "text", text: "Worktree deleted successfully" }
26139
+ ]
26140
+ };
26141
+ } catch (err) {
26142
+ return {
26143
+ content: [
26144
+ {
26145
+ type: "text",
26146
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26147
+ }
26148
+ ],
26149
+ isError: true
26150
+ };
26151
+ }
24473
26152
  }
24474
- });
24475
- server.registerTool("create_pr", {
24476
- description: "Create a GitHub PR for a repo. Uses gh CLI.",
24477
- inputSchema: {
24478
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24479
- head: external_exports.string().describe("Source branch name"),
24480
- base: external_exports.string().describe("Target branch name"),
24481
- title: external_exports.string().describe("PR title"),
24482
- body: external_exports.string().optional().describe("PR description body")
26153
+ );
26154
+ server.registerTool(
26155
+ "create_pr",
26156
+ {
26157
+ description: "Create a GitHub PR for a repo. Uses gh CLI.",
26158
+ inputSchema: {
26159
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26160
+ head: external_exports.string().describe("Source branch name"),
26161
+ base: external_exports.string().describe("Target branch name"),
26162
+ title: external_exports.string().describe("PR title"),
26163
+ body: external_exports.string().optional().describe("PR description body")
26164
+ }
26165
+ },
26166
+ async ({ repo_id, head, base, title, body }) => {
26167
+ try {
26168
+ const repoRes = await apiGet(`/repos/${repo_id}`);
26169
+ const repo = repoRes.data;
26170
+ if (!repo.path) {
26171
+ return {
26172
+ content: [
26173
+ {
26174
+ type: "text",
26175
+ text: "Error: Repo path is not configured."
26176
+ }
26177
+ ],
26178
+ isError: true
26179
+ };
26180
+ }
26181
+ const result = await createPR({
26182
+ repoPath: repo.path,
26183
+ head,
26184
+ base,
26185
+ title,
26186
+ body: body ?? ""
26187
+ });
26188
+ return {
26189
+ content: [
26190
+ { type: "text", text: JSON.stringify(result, null, 2) }
26191
+ ]
26192
+ };
26193
+ } catch (err) {
26194
+ return {
26195
+ content: [
26196
+ {
26197
+ type: "text",
26198
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26199
+ }
26200
+ ],
26201
+ isError: true
26202
+ };
26203
+ }
24483
26204
  }
24484
- }, async ({ repo_id, head, base, title, body }) => {
24485
- try {
24486
- const repoRes = await apiGet(`/repos/${repo_id}`);
24487
- const repo = repoRes.data;
24488
- if (!repo.path) {
24489
- return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
26205
+ );
26206
+ server.registerTool(
26207
+ "get_pr_status",
26208
+ {
26209
+ description: "Get the status of a GitHub PR by number.",
26210
+ inputSchema: {
26211
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26212
+ pr_number: external_exports.number().int().describe("The PR number")
26213
+ }
26214
+ },
26215
+ async ({ repo_id, pr_number }) => {
26216
+ try {
26217
+ const repoRes = await apiGet(`/repos/${repo_id}`);
26218
+ const repo = repoRes.data;
26219
+ if (!repo.path) {
26220
+ return {
26221
+ content: [
26222
+ {
26223
+ type: "text",
26224
+ text: "Error: Repo path is not configured."
26225
+ }
26226
+ ],
26227
+ isError: true
26228
+ };
26229
+ }
26230
+ const status = await getPRStatus(repo.path, pr_number);
26231
+ return {
26232
+ content: [
26233
+ { type: "text", text: JSON.stringify(status, null, 2) }
26234
+ ]
26235
+ };
26236
+ } catch (err) {
26237
+ return {
26238
+ content: [
26239
+ {
26240
+ type: "text",
26241
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26242
+ }
26243
+ ],
26244
+ isError: true
26245
+ };
24490
26246
  }
24491
- const result = await createPR({
24492
- repoPath: repo.path,
24493
- head,
24494
- base,
24495
- title,
24496
- body: body ?? ""
24497
- });
24498
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24499
- } catch (err) {
24500
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24501
26247
  }
24502
- });
24503
- server.registerTool("get_pr_status", {
24504
- description: "Get the status of a GitHub PR by number.",
24505
- inputSchema: {
24506
- repo_id: external_exports.string().uuid().describe("The repo UUID"),
24507
- pr_number: external_exports.number().int().describe("The PR number")
26248
+ );
26249
+ server.registerTool(
26250
+ "promote_checkpoint",
26251
+ {
26252
+ description: "Trigger full promotion flow for a checkpoint. Creates merge checklist from templates and GitHub PR (feat branch \u2192 development).",
26253
+ inputSchema: {
26254
+ checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
26255
+ }
26256
+ },
26257
+ async ({ checkpoint_id }) => {
26258
+ try {
26259
+ const result = await promoteCheckpoint(checkpoint_id);
26260
+ return {
26261
+ content: [
26262
+ { type: "text", text: JSON.stringify(result, null, 2) }
26263
+ ]
26264
+ };
26265
+ } catch (err) {
26266
+ return {
26267
+ content: [
26268
+ {
26269
+ type: "text",
26270
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26271
+ }
26272
+ ],
26273
+ isError: true
26274
+ };
26275
+ }
24508
26276
  }
24509
- }, async ({ repo_id, pr_number }) => {
24510
- try {
24511
- const repoRes = await apiGet(`/repos/${repo_id}`);
24512
- const repo = repoRes.data;
24513
- if (!repo.path) {
24514
- return { content: [{ type: "text", text: "Error: Repo path is not configured." }], isError: true };
26277
+ );
26278
+ server.registerTool(
26279
+ "acquire_sync_lock",
26280
+ {
26281
+ description: "Acquire a sync lock for a repo. User-scoped: only one active lock per user. Returns conflict if lock already held on a different repo.",
26282
+ inputSchema: {
26283
+ repo_id: external_exports.string().uuid().describe("The repo UUID"),
26284
+ locked_by: external_exports.string().describe(
26285
+ "Identifier of the client acquiring the lock (e.g. repo name)"
26286
+ ),
26287
+ reason: external_exports.string().optional().describe("Why the lock is being acquired"),
26288
+ ttl_minutes: external_exports.number().int().optional().describe("Lock TTL in minutes (default 10)"),
26289
+ worktree_id: external_exports.string().uuid().optional().describe("Worktree UUID if syncing from a worktree")
26290
+ }
26291
+ },
26292
+ async ({ repo_id, locked_by, reason, ttl_minutes, worktree_id }) => {
26293
+ try {
26294
+ const res = await apiPost("/sync/lock", {
26295
+ repo_id,
26296
+ locked_by,
26297
+ reason: reason ?? void 0,
26298
+ ttl_minutes: ttl_minutes ?? 10,
26299
+ worktree_id: worktree_id ?? void 0
26300
+ });
26301
+ return {
26302
+ content: [
26303
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
26304
+ ]
26305
+ };
26306
+ } catch (err) {
26307
+ return {
26308
+ content: [
26309
+ {
26310
+ type: "text",
26311
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26312
+ }
26313
+ ],
26314
+ isError: true
26315
+ };
24515
26316
  }
24516
- const status = await getPRStatus(repo.path, pr_number);
24517
- return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
24518
- } catch (err) {
24519
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
24520
26317
  }
24521
- });
24522
- server.registerTool("promote_checkpoint", {
24523
- description: "Trigger full promotion flow for a checkpoint. Creates merge checklist from templates and GitHub PR (feat branch \u2192 development).",
24524
- inputSchema: {
24525
- checkpoint_id: external_exports.string().uuid().describe("The checkpoint UUID")
26318
+ );
26319
+ server.registerTool(
26320
+ "release_sync_lock",
26321
+ {
26322
+ description: "Release a sync lock for a repo.",
26323
+ inputSchema: {
26324
+ repo_id: external_exports.string().uuid().describe("The repo UUID")
26325
+ }
26326
+ },
26327
+ async ({ repo_id }) => {
26328
+ try {
26329
+ await apiDelete("/sync/lock", { repo_id });
26330
+ return {
26331
+ content: [
26332
+ {
26333
+ type: "text",
26334
+ text: JSON.stringify({ released: true }, null, 2)
26335
+ }
26336
+ ]
26337
+ };
26338
+ } catch (err) {
26339
+ return {
26340
+ content: [
26341
+ {
26342
+ type: "text",
26343
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26344
+ }
26345
+ ],
26346
+ isError: true
26347
+ };
26348
+ }
24526
26349
  }
24527
- }, async ({ checkpoint_id }) => {
24528
- try {
24529
- const result = await promoteCheckpoint(checkpoint_id);
24530
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
24531
- } catch (err) {
24532
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
26350
+ );
26351
+ server.registerTool(
26352
+ "resolve_sync_conflict",
26353
+ {
26354
+ description: "Resolve a sync conflict. Choose use_local, use_remote, merge (with resolved_content), or skip.",
26355
+ inputSchema: {
26356
+ conflict_id: external_exports.string().uuid().describe("The conflict UUID"),
26357
+ resolution_type: external_exports.enum(["use_local", "use_remote", "merge", "skip"]).describe("Resolution strategy"),
26358
+ resolved_content: external_exports.string().optional().describe("Merged content (required when resolution_type is merge)"),
26359
+ resolved_by_repo_id: external_exports.string().uuid().optional().describe("Repo UUID that resolved the conflict")
26360
+ }
26361
+ },
26362
+ async ({
26363
+ conflict_id,
26364
+ resolution_type,
26365
+ resolved_content,
26366
+ resolved_by_repo_id
26367
+ }) => {
26368
+ try {
26369
+ const res = await apiPatch("/sync/conflicts", {
26370
+ conflict_id,
26371
+ resolution_type,
26372
+ resolved_content: resolved_content ?? void 0,
26373
+ resolved_by_repo_id: resolved_by_repo_id ?? void 0
26374
+ });
26375
+ return {
26376
+ content: [
26377
+ { type: "text", text: JSON.stringify(res.data, null, 2) }
26378
+ ]
26379
+ };
26380
+ } catch (err) {
26381
+ return {
26382
+ content: [
26383
+ {
26384
+ type: "text",
26385
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
26386
+ }
26387
+ ],
26388
+ isError: true
26389
+ };
26390
+ }
24533
26391
  }
24534
- });
26392
+ );
24535
26393
  }
24536
26394
  var init_write = __esm({
24537
26395
  "src/tools/write.ts"() {
@@ -24715,7 +26573,16 @@ if (arg === "setup") {
24715
26573
  }
24716
26574
  if (arg === "sync") {
24717
26575
  const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
24718
- await runSync2();
26576
+ const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
26577
+ try {
26578
+ await runSync2();
26579
+ } catch (err) {
26580
+ if (err instanceof SyncCancelledError2) {
26581
+ console.log("\n Sync cancelled.\n");
26582
+ process.exit(0);
26583
+ }
26584
+ throw err;
26585
+ }
24719
26586
  process.exit(0);
24720
26587
  }
24721
26588
  if (arg === "help" || arg === "--help" || arg === "-h") {