@girardmedia/bootspring 2.5.9 → 2.5.11

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.
@@ -31156,6 +31156,13 @@ ${COLORS.bold}${msg}${COLORS.reset}`);
31156
31156
  console.log(`${indent}- ${formatListItem(item)}`);
31157
31157
  }
31158
31158
  };
31159
+ printImpl.apiError = (prefix, err) => {
31160
+ if (err && typeof err === "object" && err.authHandled) {
31161
+ return;
31162
+ }
31163
+ const message = err instanceof Error ? err.message : String(err);
31164
+ console.log(`${COLORS.red}\u2717${COLORS.reset} ${prefix}: ${message}`);
31165
+ };
31159
31166
  print = printImpl;
31160
31167
  }
31161
31168
  });
@@ -31370,7 +31377,7 @@ var init_release = __esm({
31370
31377
  "../../packages/shared/src/release.ts"() {
31371
31378
  "use strict";
31372
31379
  init_cjs_shims();
31373
- BOOTSPRING_VERSION = "2.5.7";
31380
+ BOOTSPRING_VERSION = "2.5.11";
31374
31381
  BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
31375
31382
  }
31376
31383
  });
@@ -48932,6 +48939,7 @@ var require_dist2 = __commonJS({
48932
48939
  presence: () => presence_exports,
48933
48940
  readNearestProjectConfig: () => readNearestProjectConfig,
48934
48941
  refreshSession: () => refreshSession,
48942
+ registerMcpForAssistant: () => registerMcpForAssistant,
48935
48943
  remoteLogout: () => remoteLogout,
48936
48944
  request: () => request,
48937
48945
  resolvePolicyProfile: () => resolvePolicyProfile,
@@ -49151,6 +49159,7 @@ var require_dist2 = __commonJS({
49151
49159
  if (reauthOk) {
49152
49160
  return rawRequest(method, path10, data, { ...options, _authRetried: true, noCache: true });
49153
49161
  }
49162
+ err.authHandled = true;
49154
49163
  }
49155
49164
  throw err;
49156
49165
  }
@@ -49380,6 +49389,7 @@ var require_dist2 = __commonJS({
49380
49389
  var self_heal_exports = {};
49381
49390
  __export2(self_heal_exports, {
49382
49391
  classifyError: () => classifyError,
49392
+ registerMcpForAssistant: () => registerMcpForAssistant,
49383
49393
  runDiagnostics: () => runDiagnostics,
49384
49394
  tryHeal: () => tryHeal
49385
49395
  });
@@ -49627,6 +49637,30 @@ var require_dist2 = __commonJS({
49627
49637
  } else {
49628
49638
  results.push({ id: "deps", status: "ok", message: "Dependencies installed." });
49629
49639
  }
49640
+ const lockPath = import_path3.default.join(cwd, "package-lock.json");
49641
+ const pnpmLock = import_path3.default.join(cwd, "pnpm-lock.yaml");
49642
+ const nmPath = import_path3.default.join(cwd, "node_modules");
49643
+ const lockFile = import_fs3.default.existsSync(pnpmLock) ? pnpmLock : import_fs3.default.existsSync(lockPath) ? lockPath : null;
49644
+ if (lockFile && import_fs3.default.existsSync(nmPath)) {
49645
+ try {
49646
+ const lockMtime = import_fs3.default.statSync(lockFile).mtimeMs;
49647
+ const nmMtime = import_fs3.default.statSync(nmPath).mtimeMs;
49648
+ if (lockMtime > nmMtime) {
49649
+ const installCmd = lockFile.endsWith(".yaml") ? "pnpm install" : "npm install";
49650
+ if (autoFix) {
49651
+ try {
49652
+ (0, import_child_process.execSync)(installCmd, { cwd, timeout: 12e4, stdio: "pipe" });
49653
+ results.push({ id: "deps-stale", status: "healed", message: `Lockfile newer than node_modules \u2014 ran ${installCmd}.` });
49654
+ } catch {
49655
+ results.push({ id: "deps-stale", status: "action-needed", message: "Lockfile newer than node_modules.", fix: installCmd });
49656
+ }
49657
+ } else {
49658
+ results.push({ id: "deps-stale", status: "action-needed", message: "Lockfile newer than node_modules \u2014 dependencies may be stale.", fix: `${installCmd}` });
49659
+ }
49660
+ }
49661
+ } catch {
49662
+ }
49663
+ }
49630
49664
  const mcpTargets = {
49631
49665
  claude: import_path3.default.join(homeDir, ".claude.json"),
49632
49666
  codex: import_path3.default.join(homeDir, ".codex", "config.toml"),
@@ -49677,7 +49711,9 @@ var require_dist2 = __commonJS({
49677
49711
  if (autoFix) {
49678
49712
  const backup = buildStatePath + ".bak";
49679
49713
  import_fs3.default.copyFileSync(buildStatePath, backup);
49680
- results.push({ id: "build-state", status: "action-needed", message: "BUILD_STATE.json has invalid structure.", fix: "bootspring build start" });
49714
+ state.implementationQueue = state.implementationQueue || [];
49715
+ import_fs3.default.writeFileSync(buildStatePath, JSON.stringify(state, null, 2));
49716
+ results.push({ id: "build-state", status: "healed", message: `BUILD_STATE.json repaired (backed up to ${import_path3.default.basename(backup)}).` });
49681
49717
  } else {
49682
49718
  results.push({ id: "build-state", status: "action-needed", message: "BUILD_STATE.json has invalid structure.", fix: "bootspring doctor --fix" });
49683
49719
  }
@@ -49773,6 +49809,29 @@ ${block}` : block;
49773
49809
  return false;
49774
49810
  }
49775
49811
  }
49812
+ var MCP_PATHS = {
49813
+ claude: () => import_path3.default.join(import_os2.default.homedir(), ".claude.json"),
49814
+ cursor: () => import_path3.default.join(import_os2.default.homedir(), ".cursor", "mcp.json"),
49815
+ codex: () => import_path3.default.join(import_os2.default.homedir(), ".codex", "config.toml"),
49816
+ gemini: () => import_path3.default.join(import_os2.default.homedir(), ".gemini", "settings.json")
49817
+ };
49818
+ function registerMcpForAssistant(target) {
49819
+ const configPath = MCP_PATHS[target]();
49820
+ let success = false;
49821
+ switch (target) {
49822
+ case "claude":
49823
+ case "cursor":
49824
+ success = writeClaudeMcp(configPath);
49825
+ break;
49826
+ case "codex":
49827
+ success = writeCodexMcp(configPath);
49828
+ break;
49829
+ case "gemini":
49830
+ success = writeGeminiMcp(configPath);
49831
+ break;
49832
+ }
49833
+ return { success, path: configPath };
49834
+ }
49776
49835
  var presence_exports = {};
49777
49836
  __export2(presence_exports, {
49778
49837
  maybeAutoUploadTelemetry: () => maybeAutoUploadTelemetry,
@@ -52219,7 +52278,7 @@ var require_package = __commonJS({
52219
52278
  "../../../package.json"(exports2, module2) {
52220
52279
  module2.exports = {
52221
52280
  name: "bootspring-workspace",
52222
- version: "2.5.9",
52281
+ version: "2.5.11",
52223
52282
  private: true,
52224
52283
  description: "Workspace tooling for the Bootspring monorepo",
52225
52284
  keywords: [
@@ -52249,7 +52308,9 @@ var require_package = __commonJS({
52249
52308
  mcp: "node monorepo/apps/cli/dist/mcp-server.js",
52250
52309
  "version:sync": "node scripts/sync-version-metadata.js",
52251
52310
  "verify:version-sync": "node scripts/sync-version-metadata.js --check",
52252
- "release:prepare": "npm run version:sync && npm run build && npm test && npm run lint --if-present && npm run verify:package && npm run verify:thin-client-contract && npm run verify:mcp-contract && npm run verify:monorepo-assets && npm run verify:release-gates",
52311
+ "release:prepare": "npm run version:sync && npm run build && npm test && npm run lint --if-present && npm run verify:package && npm run verify:thin-client-contract && npm run verify:mcp-contract && npm run verify:monorepo-assets && npm run verify:release-gates && npm run smoke:publish",
52312
+ "release:dry-run": "npm run version:sync && npm run build && npm test && npm run lint --if-present && npm run verify:package && npm run verify:thin-client-contract && npm run verify:mcp-contract && npm run verify:monorepo-assets && npm run verify:release-gates && npm run smoke:publish && echo '\\n--- DRY RUN COMPLETE (no publish) ---'",
52313
+ "smoke:publish": "node scripts/smoke-publish.js",
52253
52314
  pretest: "npm run build",
52254
52315
  test: "vitest run",
52255
52316
  "test:launch-smoke": "vitest run __tests__/unit/cli-first-run-smoke.test.js __tests__/unit/health-fresh-start.test.ts __tests__/unit/session-project-scope.test.ts __tests__/unit/auth-cli-mixed-states.test.ts __tests__/unit/api-client-project-auth-fallback.test.js __tests__/unit/cli-help-surface.test.js __tests__/unit/cli-command-manifest.test.js",
@@ -52276,7 +52337,10 @@ var require_package = __commonJS({
52276
52337
  "server:bundle:configure-shared-billing-gh": "bash monorepo/apps/server/compat-runtime/scripts/configure-shared-billing-smoke-gh.sh",
52277
52338
  "build:cli": "npm --prefix monorepo/apps/cli run build",
52278
52339
  "build:watch": "npm --prefix monorepo/apps/cli run dev",
52279
- dev: "npm --prefix monorepo/apps/cli run dev",
52340
+ dev: "cd monorepo && pnpm turbo dev --filter=@girardmedia/bootspring --filter=@bootspring/server --concurrency=10",
52341
+ "dev:cli": "npm --prefix monorepo/apps/cli run dev",
52342
+ "dev:server": "npm --prefix monorepo/apps/server run dev",
52343
+ "dev:test": "vitest",
52280
52344
  "verify:lint-budget": "node scripts/check-lint-budgets.js",
52281
52345
  "verify:release-gates": "node scripts/release-gate-check.js",
52282
52346
  "verify:shared-contracts": "node scripts/verify-shared-contracts.js",
@@ -52345,6 +52409,577 @@ var core = require_dist2();
52345
52409
  var api2 = core.api;
52346
52410
  var auth2 = core.auth;
52347
52411
  var VERSION = require_package().version;
52412
+ function getProjectRoot() {
52413
+ return process.cwd();
52414
+ }
52415
+ function loadBuildState() {
52416
+ const fs3 = require("fs");
52417
+ const path3 = require("path");
52418
+ const stateFile = path3.join(getProjectRoot(), "planning", "BUILD_STATE.json");
52419
+ if (!fs3.existsSync(stateFile)) return null;
52420
+ try {
52421
+ return JSON.parse(fs3.readFileSync(stateFile, "utf-8"));
52422
+ } catch {
52423
+ return null;
52424
+ }
52425
+ }
52426
+ function saveBuildState(state) {
52427
+ const fs3 = require("fs");
52428
+ const path3 = require("path");
52429
+ const planDir = path3.join(getProjectRoot(), "planning");
52430
+ if (!fs3.existsSync(planDir)) fs3.mkdirSync(planDir, { recursive: true });
52431
+ state.metadata = state.metadata || {};
52432
+ state.metadata.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52433
+ if (state.loopSession) state.loopSession.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
52434
+ fs3.writeFileSync(path3.join(planDir, "BUILD_STATE.json"), JSON.stringify(state, null, 2));
52435
+ }
52436
+ function buildGetStats(state) {
52437
+ if (!state) return null;
52438
+ const q = state.implementationQueue || [];
52439
+ const completed = q.filter((t) => t.status === "completed").length;
52440
+ const pending = q.filter((t) => t.status === "pending").length;
52441
+ const inProgress = q.filter((t) => t.status === "in_progress").length;
52442
+ const blocked = q.filter((t) => t.status === "blocked").length;
52443
+ const skipped = q.filter((t) => t.status === "skipped").length;
52444
+ const total = q.length;
52445
+ const percent = total > 0 ? Math.round(completed / total * 100) : 0;
52446
+ return { total, completed, pending, inProgress, blocked, skipped, percent };
52447
+ }
52448
+ function buildGetNextTask(state) {
52449
+ if (!state) return null;
52450
+ const q = state.implementationQueue || [];
52451
+ const ip = q.find((t) => t.status === "in_progress");
52452
+ if (ip) return ip;
52453
+ for (const task of q.filter((t) => t.status === "pending")) {
52454
+ if (!task.dependencies || task.dependencies.length === 0) return task;
52455
+ const allDone = task.dependencies.every((depId) => {
52456
+ const dep = q.find((t) => t.id === depId);
52457
+ return dep && dep.status === "completed";
52458
+ });
52459
+ if (allDone) return task;
52460
+ }
52461
+ return null;
52462
+ }
52463
+ function buildUpdateTaskStatus(state, taskId, status) {
52464
+ const task = (state.implementationQueue || []).find((t) => t.id === taskId);
52465
+ if (!task) return false;
52466
+ task.status = status;
52467
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52468
+ if (status === "completed") task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
52469
+ if (status === "completed" || status === "skipped") {
52470
+ state.loopSession = state.loopSession || {};
52471
+ state.loopSession.currentIteration = (state.loopSession.currentIteration || 0) + 1;
52472
+ }
52473
+ return true;
52474
+ }
52475
+ function formatBuildTask(task) {
52476
+ if (!task) return null;
52477
+ return {
52478
+ id: task.id,
52479
+ title: task.title,
52480
+ description: task.description,
52481
+ phase: task.phase,
52482
+ source: task.source,
52483
+ sourceSection: task.sourceSection,
52484
+ acceptanceCriteria: task.acceptanceCriteria || []
52485
+ };
52486
+ }
52487
+ async function executeLocalBuild(args) {
52488
+ const action = args?.action || "status";
52489
+ const state = loadBuildState();
52490
+ switch (action) {
52491
+ case "status": {
52492
+ if (!state) {
52493
+ return { content: [{ type: "text", text: JSON.stringify({ initialized: false, message: "No build state found. Create planning/TODO.md and run action=sync, or use action=init.", hint: "Run: bootspring seed go" }, null, 2) }] };
52494
+ }
52495
+ const stats = buildGetStats(state);
52496
+ const currentTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52497
+ return { content: [{ type: "text", text: JSON.stringify({
52498
+ project: state.projectName,
52499
+ phase: state.currentPhase,
52500
+ status: state.status,
52501
+ progress: { completed: stats.completed, pending: stats.pending, inProgress: stats.inProgress, total: stats.total, percent: stats.percent },
52502
+ currentTask: currentTask ? { id: currentTask.id, title: currentTask.title } : null,
52503
+ nextAction: currentTask ? "Complete the current task, then use action=done" : "Use action=next to get the next task"
52504
+ }, null, 2) }] };
52505
+ }
52506
+ case "next": {
52507
+ if (!state) {
52508
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found", hint: "Use action=init to initialize" }, null, 2) }] };
52509
+ }
52510
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52511
+ if (inProgress) {
52512
+ return { content: [{ type: "text", text: JSON.stringify({
52513
+ message: "Task already in progress",
52514
+ state: "in_progress",
52515
+ task: formatBuildTask(inProgress),
52516
+ hint: "Complete this task with action=done, or use action=skip to skip it"
52517
+ }, null, 2) }] };
52518
+ }
52519
+ const nextTask = buildGetNextTask(state);
52520
+ if (!nextTask) {
52521
+ const stats2 = buildGetStats(state);
52522
+ return { content: [{ type: "text", text: JSON.stringify({ message: "All tasks complete!", progress: { completed: stats2.completed, total: stats2.total }, celebration: "Build finished!" }, null, 2) }] };
52523
+ }
52524
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52525
+ saveBuildState(state);
52526
+ const stats = buildGetStats(state);
52527
+ return { content: [{ type: "text", text: JSON.stringify({
52528
+ task: formatBuildTask(nextTask),
52529
+ state: "task_ready",
52530
+ file: "planning/TODO.md",
52531
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent },
52532
+ instructions: [
52533
+ `Find ${nextTask.id} in planning/TODO.md for full details`,
52534
+ "Implement the task in the codebase",
52535
+ "Ensure acceptance criteria are met",
52536
+ "Run quality checks (lint, test)",
52537
+ "Commit changes with descriptive message",
52538
+ "Use action=done when complete"
52539
+ ]
52540
+ }, null, 2) }] };
52541
+ }
52542
+ case "current": {
52543
+ if (!state) {
52544
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52545
+ }
52546
+ const currentTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52547
+ if (!currentTask) {
52548
+ return { content: [{ type: "text", text: JSON.stringify({ message: "No task currently in progress", hint: "Use action=next to get the next task" }, null, 2) }] };
52549
+ }
52550
+ return { content: [{ type: "text", text: JSON.stringify({
52551
+ task: formatBuildTask(currentTask),
52552
+ instructions: ["Implement this task in the codebase", "Run quality checks (lint, test)", "Commit your changes", "Use action=done to mark complete"]
52553
+ }, null, 2) }] };
52554
+ }
52555
+ case "done": {
52556
+ if (!state) {
52557
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52558
+ }
52559
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52560
+ if (!inProgress) {
52561
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No task currently in progress", hint: "Use action=next to get a task first" }, null, 2) }] };
52562
+ }
52563
+ buildUpdateTaskStatus(state, inProgress.id, "completed");
52564
+ const nextTask = buildGetNextTask(state);
52565
+ if (nextTask) {
52566
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52567
+ }
52568
+ saveBuildState(state);
52569
+ const stats = buildGetStats(state);
52570
+ return { content: [{ type: "text", text: JSON.stringify({
52571
+ completed: { id: inProgress.id, title: inProgress.title },
52572
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent },
52573
+ nextTask: nextTask ? { ...formatBuildTask(nextTask), file: "planning/TODO.md" } : null,
52574
+ allComplete: !nextTask,
52575
+ message: nextTask ? `Complete! Next: implement ${nextTask.id} \u2014 ${nextTask.title}` : "Build complete! All tasks finished."
52576
+ }, null, 2) }] };
52577
+ }
52578
+ case "skip": {
52579
+ if (!state) {
52580
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52581
+ }
52582
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52583
+ if (!inProgress) {
52584
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No task to skip", hint: "Use action=next to get a task first" }, null, 2) }] };
52585
+ }
52586
+ buildUpdateTaskStatus(state, inProgress.id, "skipped");
52587
+ const nextTask = buildGetNextTask(state);
52588
+ if (nextTask) {
52589
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52590
+ }
52591
+ saveBuildState(state);
52592
+ return { content: [{ type: "text", text: JSON.stringify({
52593
+ skipped: { id: inProgress.id, title: inProgress.title, reason: args?.reason || "Skipped" },
52594
+ nextTask: nextTask ? formatBuildTask(nextTask) : null,
52595
+ message: nextTask ? `Skipped. Next: ${nextTask.title}` : "No more tasks available"
52596
+ }, null, 2) }] };
52597
+ }
52598
+ case "list": {
52599
+ if (!state) {
52600
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52601
+ }
52602
+ const tasks = (state.implementationQueue || []).map((t) => ({ id: t.id, title: t.title, status: t.status, phase: t.phase }));
52603
+ return { content: [{ type: "text", text: JSON.stringify({ project: state.projectName, tasks }, null, 2) }] };
52604
+ }
52605
+ case "advance": {
52606
+ if (!state) {
52607
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found", hint: "Use action=init or action=sync first" }, null, 2) }] };
52608
+ }
52609
+ const activeTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52610
+ if (activeTask) {
52611
+ if (args?.autoDone) {
52612
+ try {
52613
+ const { execSync } = require("child_process");
52614
+ const gitStatus = execSync("git status --porcelain", { cwd: getProjectRoot(), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
52615
+ if (gitStatus.trim().length === 0) {
52616
+ buildUpdateTaskStatus(state, activeTask.id, "completed");
52617
+ const nextTask2 = buildGetNextTask(state);
52618
+ if (nextTask2) buildUpdateTaskStatus(state, nextTask2.id, "in_progress");
52619
+ saveBuildState(state);
52620
+ const stats = buildGetStats(state);
52621
+ return { content: [{ type: "text", text: JSON.stringify({
52622
+ state: nextTask2 ? "advanced" : "all_complete",
52623
+ autoCompleted: true,
52624
+ completed: { id: activeTask.id, title: activeTask.title },
52625
+ nextTask: nextTask2 ? formatBuildTask(nextTask2) : null,
52626
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent }
52627
+ }, null, 2) }] };
52628
+ }
52629
+ } catch {
52630
+ }
52631
+ }
52632
+ return { content: [{ type: "text", text: JSON.stringify({
52633
+ state: "in_progress",
52634
+ task: formatBuildTask(activeTask),
52635
+ hint: "Finish implementation and use action=done, or call action=advance with autoDone=true after commit"
52636
+ }, null, 2) }] };
52637
+ }
52638
+ const nextTask = buildGetNextTask(state);
52639
+ if (!nextTask) {
52640
+ const stats = buildGetStats(state);
52641
+ return { content: [{ type: "text", text: JSON.stringify({ state: "all_complete", message: "All tasks complete!", progress: { completed: stats.completed, total: stats.total, percent: stats.percent } }, null, 2) }] };
52642
+ }
52643
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52644
+ saveBuildState(state);
52645
+ return { content: [{ type: "text", text: JSON.stringify({
52646
+ state: "task_ready",
52647
+ task: formatBuildTask(nextTask),
52648
+ file: "planning/TODO.md",
52649
+ hint: "Implement task, then call action=advance (or action=done) to continue loop"
52650
+ }, null, 2) }] };
52651
+ }
52652
+ case "sync": {
52653
+ const fs3 = require("fs");
52654
+ const pathMod = require("path");
52655
+ const projectRoot = getProjectRoot();
52656
+ const todoPath = pathMod.join(projectRoot, "planning", "TODO.md");
52657
+ if (!fs3.existsSync(todoPath)) {
52658
+ return { content: [{ type: "text", text: JSON.stringify({ error: "planning/TODO.md not found", hint: "Create planning/TODO.md with tasks first" }, null, 2) }] };
52659
+ }
52660
+ const todoContent = fs3.readFileSync(todoPath, "utf-8");
52661
+ const taskRegex = /^-\s*\[([ xX])\]\s*(?:\*\*)?([a-zA-Z]+-\d+)(?:\*\*)?[:\s]+(.+?)$/gm;
52662
+ const tasks = [];
52663
+ let match;
52664
+ while ((match = taskRegex.exec(todoContent)) !== null) {
52665
+ const checked = match[1].toLowerCase() === "x";
52666
+ tasks.push({
52667
+ id: match[2],
52668
+ title: match[3].trim().replace(/\*\*/g, ""),
52669
+ status: checked ? "completed" : "pending",
52670
+ phase: "mvp",
52671
+ source: "TODO.md"
52672
+ });
52673
+ }
52674
+ if (tasks.length === 0) {
52675
+ const simpleLine = /(?:bs|task)-\d+[:\s]+(.+)/gi;
52676
+ let lineMatch;
52677
+ while ((lineMatch = simpleLine.exec(todoContent)) !== null) {
52678
+ tasks.push({
52679
+ id: lineMatch[0].split(/[:\s]/)[0],
52680
+ title: lineMatch[1]?.trim() || lineMatch[0],
52681
+ status: "pending",
52682
+ phase: "mvp",
52683
+ source: "TODO.md"
52684
+ });
52685
+ }
52686
+ }
52687
+ const currentState = state || {
52688
+ version: "1.0.0",
52689
+ projectName: "Project",
52690
+ status: "pending",
52691
+ currentPhase: "mvp",
52692
+ phases: {},
52693
+ implementationQueue: [],
52694
+ mvpCriteria: { features: [], completionPercentage: 0, allCriteriaMet: false },
52695
+ loopSession: { sessionId: `build-${Date.now()}`, currentIteration: 0, maxIterations: 500, startedAt: (/* @__PURE__ */ new Date()).toISOString(), lastUpdated: null, pausedAt: null },
52696
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), seedSource: "TODO.md", preseedDocs: [] }
52697
+ };
52698
+ if (args?.replace) {
52699
+ currentState.implementationQueue = tasks;
52700
+ } else {
52701
+ const existingIds = new Set((currentState.implementationQueue || []).map((t) => t.id));
52702
+ for (const task of tasks) {
52703
+ if (!existingIds.has(task.id)) {
52704
+ currentState.implementationQueue = currentState.implementationQueue || [];
52705
+ currentState.implementationQueue.push(task);
52706
+ }
52707
+ }
52708
+ }
52709
+ saveBuildState(currentState);
52710
+ const stats = buildGetStats(currentState);
52711
+ return { content: [{ type: "text", text: JSON.stringify({
52712
+ success: true,
52713
+ source: "TODO.md",
52714
+ tasksFound: tasks.length,
52715
+ mode: args?.replace ? "replace" : "merge",
52716
+ progress: { total: stats.total, completed: stats.completed, pending: stats.pending },
52717
+ nextStep: "Use action=next to get the first task"
52718
+ }, null, 2) }] };
52719
+ }
52720
+ case "init": {
52721
+ const fs3 = require("fs");
52722
+ const pathMod = require("path");
52723
+ const projectRoot = getProjectRoot();
52724
+ const todoPath = pathMod.join(projectRoot, "planning", "TODO.md");
52725
+ if (fs3.existsSync(todoPath)) {
52726
+ return executeLocalBuild({ ...args, action: "sync", replace: true });
52727
+ }
52728
+ const newState = {
52729
+ version: "1.0.0",
52730
+ projectName: pathMod.basename(projectRoot),
52731
+ status: "pending",
52732
+ currentPhase: null,
52733
+ phases: { foundation: { status: "pending", tasks: [] }, mvp: { status: "pending", tasks: [] }, launch: { status: "pending", tasks: [] } },
52734
+ implementationQueue: [],
52735
+ mvpCriteria: { features: [], completionPercentage: 0, allCriteriaMet: false },
52736
+ loopSession: { sessionId: `build-${Date.now()}`, currentIteration: 0, maxIterations: 500, startedAt: (/* @__PURE__ */ new Date()).toISOString(), lastUpdated: null, pausedAt: null },
52737
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), seedSource: null, preseedDocs: [] }
52738
+ };
52739
+ saveBuildState(newState);
52740
+ return { content: [{ type: "text", text: JSON.stringify({
52741
+ success: true,
52742
+ message: "Build state initialized",
52743
+ project: newState.projectName,
52744
+ hint: "Create planning/TODO.md with tasks, then use action=sync to load them"
52745
+ }, null, 2) }] };
52746
+ }
52747
+ case "scan": {
52748
+ if (!state) {
52749
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found", hint: "Use action=init or action=sync first" }, null, 2) }] };
52750
+ }
52751
+ const { execSync } = require("child_process");
52752
+ const projectRoot = getProjectRoot();
52753
+ let gitLog;
52754
+ try {
52755
+ const since = state.loopSession?.startedAt;
52756
+ const sinceArg = since ? ` --since="${since}"` : " -100";
52757
+ gitLog = execSync(`git log --oneline${sinceArg}`, { cwd: projectRoot, encoding: "utf-8", timeout: 1e4 }).trim();
52758
+ } catch {
52759
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to read git log \u2014 not a git repo or git not available" }, null, 2) }] };
52760
+ }
52761
+ if (!gitLog) {
52762
+ return { content: [{ type: "text", text: JSON.stringify({ scanned: 0, marked: 0, message: "No commits found to scan" }, null, 2) }] };
52763
+ }
52764
+ const lines = gitLog.split("\n").filter(Boolean);
52765
+ const bsIdPattern = /\bbs-(\d+)\b/gi;
52766
+ const foundIds = /* @__PURE__ */ new Set();
52767
+ for (const line of lines) {
52768
+ for (const match of line.matchAll(bsIdPattern)) {
52769
+ foundIds.add(`bs-${match[1]}`);
52770
+ }
52771
+ }
52772
+ const queue = state.implementationQueue || [];
52773
+ const marked = [];
52774
+ for (const id of foundIds) {
52775
+ const task = queue.find((t) => t.id === id && (t.status === "pending" || t.status === "in_progress"));
52776
+ if (task) {
52777
+ task.status = "completed";
52778
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
52779
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52780
+ task.completedBy = "commit-back-scan";
52781
+ marked.push(id);
52782
+ }
52783
+ }
52784
+ if (marked.length > 0) {
52785
+ saveBuildState(state);
52786
+ }
52787
+ const stats = buildGetStats(state);
52788
+ return { content: [{ type: "text", text: JSON.stringify({
52789
+ scanned: lines.length,
52790
+ bsIdsFound: foundIds.size,
52791
+ marked: marked.length,
52792
+ markedIds: marked,
52793
+ progress: stats,
52794
+ message: marked.length > 0 ? `Marked ${marked.length} task${marked.length === 1 ? "" : "s"} as completed from git history` : `Scanned ${lines.length} commits, found ${foundIds.size} bs-IDs, but none matched pending/in-progress tasks`
52795
+ }, null, 2) }] };
52796
+ }
52797
+ default:
52798
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan"] }, null, 2) }] };
52799
+ }
52800
+ }
52801
+ async function executeLocalSeed(args) {
52802
+ const fs3 = require("fs");
52803
+ const pathMod = require("path");
52804
+ const action = args?.action || "status";
52805
+ const projectRoot = getProjectRoot();
52806
+ const CONTEXT_DIR = ".bootspring/context";
52807
+ const contextDir = pathMod.join(projectRoot, CONTEXT_DIR);
52808
+ const CONTEXT_DOCS = {
52809
+ vision: { name: "VISION.md", title: "Vision", description: "Problem, solution, core values", required: true },
52810
+ audience: { name: "AUDIENCE.md", title: "Audience", description: "Target audience, personas, ICP", required: true },
52811
+ market: { name: "MARKET.md", title: "Market", description: "TAM/SAM/SOM, market opportunity", required: false },
52812
+ competitors: { name: "COMPETITORS.md", title: "Competitors", description: "Competitive landscape, differentiation", required: false },
52813
+ "business-model": { name: "BUSINESS_MODEL.md", title: "Business Model", description: "Revenue, pricing, unit economics", required: true },
52814
+ prd: { name: "PRD.md", title: "PRD", description: "Product requirements, user stories", required: true },
52815
+ "technical-spec": { name: "TECHNICAL_SPEC.md", title: "Technical Spec", description: "Architecture, tech stack, data model", required: false },
52816
+ roadmap: { name: "ROADMAP.md", title: "Roadmap", description: "Phases, milestones, timeline", required: false }
52817
+ };
52818
+ const PRESETS = {
52819
+ essential: ["vision", "audience", "business-model", "prd"],
52820
+ startup: ["vision", "audience", "market", "competitors", "business-model", "prd", "roadmap"],
52821
+ full: Object.keys(CONTEXT_DOCS),
52822
+ technical: ["vision", "prd", "technical-spec", "roadmap"],
52823
+ investor: ["vision", "audience", "market", "competitors", "business-model", "roadmap"]
52824
+ };
52825
+ switch (action) {
52826
+ case "setup":
52827
+ case "init": {
52828
+ const preset = args?.preset || "startup";
52829
+ if (!fs3.existsSync(contextDir)) fs3.mkdirSync(contextDir, { recursive: true });
52830
+ let projectName = pathMod.basename(projectRoot);
52831
+ try {
52832
+ const pkg = JSON.parse(fs3.readFileSync(pathMod.join(projectRoot, "package.json"), "utf-8"));
52833
+ projectName = pkg.name || projectName;
52834
+ } catch {
52835
+ }
52836
+ const docs = PRESETS[preset] || PRESETS.startup;
52837
+ let generated = 0;
52838
+ for (const docType of docs) {
52839
+ const meta = CONTEXT_DOCS[docType];
52840
+ if (!meta) continue;
52841
+ const filePath = pathMod.join(contextDir, meta.name);
52842
+ if (!fs3.existsSync(filePath)) {
52843
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
52844
+ fs3.writeFileSync(filePath, `# ${projectName} \u2014 ${meta.title}
52845
+
52846
+ **Generated:** ${date}
52847
+ **Status:** Draft
52848
+
52849
+ ## ${meta.title}
52850
+
52851
+ *${meta.description}*
52852
+
52853
+ ---
52854
+
52855
+ *Edit this file with your project details.*
52856
+ `);
52857
+ generated++;
52858
+ }
52859
+ }
52860
+ return { content: [{ type: "text", text: JSON.stringify({
52861
+ success: true,
52862
+ action: "init",
52863
+ preset,
52864
+ generated,
52865
+ contextDir: CONTEXT_DIR,
52866
+ docs: docs.map((d) => CONTEXT_DOCS[d]?.name).filter(Boolean),
52867
+ nextSteps: ["Edit the .md files in .bootspring/context/", 'Run bootspring_seed with action: "generate" or run bootspring seed merge']
52868
+ }, null, 2) }] };
52869
+ }
52870
+ case "status": {
52871
+ const seedPath = pathMod.join(projectRoot, "planning", "SEED.md");
52872
+ const hasSeed = fs3.existsSync(seedPath);
52873
+ let docs = [];
52874
+ if (fs3.existsSync(contextDir)) {
52875
+ docs = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md");
52876
+ }
52877
+ return { content: [{ type: "text", text: JSON.stringify({
52878
+ contextDocs: docs,
52879
+ total: docs.length,
52880
+ contextDir: CONTEXT_DIR,
52881
+ seedMd: hasSeed,
52882
+ seedPath: hasSeed ? "planning/SEED.md" : null,
52883
+ nextStep: docs.length === 0 ? 'Run bootspring_seed with action: "init"' : !hasSeed ? "Edit docs, then merge" : "Ready to build"
52884
+ }, null, 2) }] };
52885
+ }
52886
+ case "generate": {
52887
+ if (!fs3.existsSync(contextDir)) {
52888
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No context docs found", hint: 'Run bootspring_seed with action: "init" first' }, null, 2) }] };
52889
+ }
52890
+ const validDocs = Object.values(CONTEXT_DOCS).map((d) => d.name);
52891
+ const files = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md").sort((a, b) => {
52892
+ const ai = validDocs.indexOf(a);
52893
+ const bi = validDocs.indexOf(b);
52894
+ return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
52895
+ });
52896
+ if (files.length === 0) {
52897
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No .md files in context folder" }, null, 2) }] };
52898
+ }
52899
+ const sections = files.map((f) => fs3.readFileSync(pathMod.join(contextDir, f), "utf-8"));
52900
+ const planDir = pathMod.join(projectRoot, "planning");
52901
+ if (!fs3.existsSync(planDir)) fs3.mkdirSync(planDir, { recursive: true });
52902
+ fs3.writeFileSync(pathMod.join(planDir, "SEED.md"), sections.join("\n\n---\n\n"));
52903
+ return { content: [{ type: "text", text: JSON.stringify({
52904
+ success: true,
52905
+ action: "generate",
52906
+ merged: files.length,
52907
+ output: "planning/SEED.md",
52908
+ files,
52909
+ nextStep: 'Create planning/TODO.md with tasks, then use bootspring_build action: "sync"'
52910
+ }, null, 2) }] };
52911
+ }
52912
+ case "export": {
52913
+ if (!fs3.existsSync(contextDir)) {
52914
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No context docs found" }, null, 2) }] };
52915
+ }
52916
+ const docs = {};
52917
+ const files = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md");
52918
+ for (const file of files) {
52919
+ docs[file.replace(".md", "")] = fs3.readFileSync(pathMod.join(contextDir, file), "utf-8");
52920
+ }
52921
+ return { content: [{ type: "text", text: JSON.stringify({ context: docs, exportedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) }] };
52922
+ }
52923
+ default:
52924
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["setup", "init", "generate", "status", "export"] }, null, 2) }] };
52925
+ }
52926
+ }
52927
+ async function executeLocalTodo(args) {
52928
+ const fs3 = require("fs");
52929
+ const pathMod = require("path");
52930
+ const action = args?.action || "list";
52931
+ const projectRoot = getProjectRoot();
52932
+ const todoFile = pathMod.join(projectRoot, ".bootspring", "todos.json");
52933
+ function loadTodos() {
52934
+ if (!fs3.existsSync(todoFile)) return [];
52935
+ try {
52936
+ return JSON.parse(fs3.readFileSync(todoFile, "utf-8"));
52937
+ } catch {
52938
+ return [];
52939
+ }
52940
+ }
52941
+ function saveTodos(todos) {
52942
+ const dir = pathMod.dirname(todoFile);
52943
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
52944
+ fs3.writeFileSync(todoFile, JSON.stringify(todos, null, 2));
52945
+ }
52946
+ switch (action) {
52947
+ case "list": {
52948
+ const todos = loadTodos();
52949
+ return { content: [{ type: "text", text: JSON.stringify({ todos, total: todos.length }, null, 2) }] };
52950
+ }
52951
+ case "add": {
52952
+ const todos = loadTodos();
52953
+ const newTodo = { id: `todo-${Date.now()}`, text: args?.text || "Untitled", completed: false, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
52954
+ todos.push(newTodo);
52955
+ saveTodos(todos);
52956
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, todo: newTodo }, null, 2) }] };
52957
+ }
52958
+ case "complete": {
52959
+ const todos = loadTodos();
52960
+ const todo = todos.find((t) => t.id === args?.id);
52961
+ if (!todo) return { content: [{ type: "text", text: JSON.stringify({ error: "Todo not found" }, null, 2) }] };
52962
+ todo.completed = true;
52963
+ todo.completedAt = (/* @__PURE__ */ new Date()).toISOString();
52964
+ saveTodos(todos);
52965
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, todo }, null, 2) }] };
52966
+ }
52967
+ case "delete": {
52968
+ let todos = loadTodos();
52969
+ const before = todos.length;
52970
+ todos = todos.filter((t) => t.id !== args?.id);
52971
+ saveTodos(todos);
52972
+ return { content: [{ type: "text", text: JSON.stringify({ success: todos.length < before, remaining: todos.length }, null, 2) }] };
52973
+ }
52974
+ default:
52975
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}` }, null, 2) }] };
52976
+ }
52977
+ }
52978
+ var LOCAL_TOOL_EXECUTORS = {
52979
+ bootspring_build: executeLocalBuild,
52980
+ bootspring_seed: executeLocalSeed,
52981
+ bootspring_todo: executeLocalTodo
52982
+ };
52348
52983
  var FALLBACK_TOOLS = [
52349
52984
  {
52350
52985
  name: "bootspring_agent",
@@ -52422,7 +53057,7 @@ var FALLBACK_TOOLS = [
52422
53057
  inputSchema: {
52423
53058
  type: "object",
52424
53059
  properties: {
52425
- action: { type: "string", enum: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance"] },
53060
+ action: { type: "string", enum: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance", "scan"] },
52426
53061
  reason: { type: "string" },
52427
53062
  autoDone: { type: "boolean" }
52428
53063
  },
@@ -52513,20 +53148,7 @@ function formatProxyError(error) {
52513
53148
  }
52514
53149
  return error?.message || "Unknown error from Bootspring API.";
52515
53150
  }
52516
- var LOCAL_TOOLS = /* @__PURE__ */ new Set(["bootspring_build", "bootspring_seed", "bootspring_todo"]);
52517
- function formatLocalToolGuidance(name, args) {
52518
- const action = args && typeof args.action === "string" ? args.action : "";
52519
- const base = name.replace(/^bootspring_/, "bootspring ");
52520
- const cmd = action ? `${base} ${action}` : base;
52521
- return [
52522
- `\`${name}\` is a local-state tool \u2014 it reads and writes files in your project (.bootspring/state, planning/, etc.) and therefore cannot be executed from the hosted MCP API.`,
52523
- ``,
52524
- `Run this in your project terminal:`,
52525
- ` ${cmd}`,
52526
- ``,
52527
- `Everything else in this conversation will still work \u2014 only local-state tools need to run from your shell.`
52528
- ].join("\n");
52529
- }
53151
+ var LOCAL_TOOLS = new Set(Object.keys(LOCAL_TOOL_EXECUTORS));
52530
53152
  async function resolveTools() {
52531
53153
  if (!auth2.isAuthenticated()) {
52532
53154
  return FALLBACK_TOOLS;
@@ -52560,9 +53182,14 @@ async function resolveResources() {
52560
53182
  }
52561
53183
  async function proxyToolCall(name, args) {
52562
53184
  if (LOCAL_TOOLS.has(name)) {
52563
- return {
52564
- content: [{ type: "text", text: formatLocalToolGuidance(name, args) }]
52565
- };
53185
+ try {
53186
+ return await LOCAL_TOOL_EXECUTORS[name](args || {});
53187
+ } catch (error) {
53188
+ return {
53189
+ content: [{ type: "text", text: `Error executing ${name}: ${error?.message || error}` }],
53190
+ isError: true
53191
+ };
53192
+ }
52566
53193
  }
52567
53194
  if (!auth2.isAuthenticated()) {
52568
53195
  return createAuthError("Authentication required. Run `bootspring auth login` first.");
@@ -52578,7 +53205,7 @@ async function proxyToolCall(name, args) {
52578
53205
  const response = await api2.callMcpTool(name, args || {});
52579
53206
  if (response && response.result && response.result.status === "local_execution_required") {
52580
53207
  return {
52581
- content: [{ type: "text", text: response.result.message || formatLocalToolGuidance(name, args) }]
53208
+ content: [{ type: "text", text: response.result.message || `${name} requires local execution. Run: bootspring ${name.replace("bootspring_", "")} ${args?.action || ""}`.trim() }]
52582
53209
  };
52583
53210
  }
52584
53211
  return {
@@ -52674,11 +53301,11 @@ module.exports = {
52674
53301
  FALLBACK_TOOLS,
52675
53302
  FALLBACK_RESOURCES,
52676
53303
  main,
52677
- // Exposed for tests — lock in the stale-install regression guard
53304
+ // Exposed for tests
52678
53305
  _diagnoseApiSurface: diagnoseApiSurface,
52679
53306
  _formatProxyError: formatProxyError,
52680
53307
  _LOCAL_TOOLS: LOCAL_TOOLS,
52681
- _formatLocalToolGuidance: formatLocalToolGuidance,
53308
+ _LOCAL_TOOL_EXECUTORS: LOCAL_TOOL_EXECUTORS,
52682
53309
  _proxyToolCall: proxyToolCall,
52683
53310
  _diagnoseFromApi: function diagnoseFromApi(apiShim, version) {
52684
53311
  const required = ["callMcpTool", "listMcpTools", "listMcpResources", "getMcpResource"];