@girardmedia/bootspring 2.5.8 → 2.5.10

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.
package/dist/cli/index.js CHANGED
@@ -3378,7 +3378,7 @@ var init_release = __esm({
3378
3378
  "../../packages/shared/src/release.ts"() {
3379
3379
  "use strict";
3380
3380
  init_cjs_shims();
3381
- BOOTSPRING_VERSION = "2.5.7";
3381
+ BOOTSPRING_VERSION = "2.5.10";
3382
3382
  BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
3383
3383
  }
3384
3384
  });
@@ -48530,19 +48530,62 @@ init_src();
48530
48530
  // src/middleware.ts
48531
48531
  init_cjs_shims();
48532
48532
  init_src2();
48533
+ function levenshtein(a, b) {
48534
+ const m = a.length, n = b.length;
48535
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
48536
+ for (let i2 = 0; i2 <= m; i2++) dp[i2][0] = i2;
48537
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
48538
+ for (let i2 = 1; i2 <= m; i2++)
48539
+ for (let j = 1; j <= n; j++)
48540
+ dp[i2][j] = Math.min(
48541
+ dp[i2 - 1][j] + 1,
48542
+ dp[i2][j - 1] + 1,
48543
+ dp[i2 - 1][j - 1] + (a[i2 - 1] !== b[j - 1] ? 1 : 0)
48544
+ );
48545
+ return dp[m][n];
48546
+ }
48547
+ function suggestSubcommand(typed, cmd) {
48548
+ const names = cmd.commands.flatMap((c) => [c.name(), ...c.aliases()]);
48549
+ let best = "";
48550
+ let bestDist = Infinity;
48551
+ for (const name of names) {
48552
+ const d = levenshtein(typed.toLowerCase(), name.toLowerCase());
48553
+ if (d < bestDist) {
48554
+ bestDist = d;
48555
+ best = name;
48556
+ }
48557
+ }
48558
+ const threshold = Math.ceil(Math.max(typed.length, best.length) * 0.4);
48559
+ return bestDist <= threshold ? best : null;
48560
+ }
48533
48561
  function applyHealingMiddleware(program3) {
48534
48562
  wrapCommand(program3);
48535
48563
  }
48536
48564
  function wrapCommand(cmd) {
48565
+ if (!cmd._actionHandler && cmd.commands.length > 0 && cmd.parent) {
48566
+ cmd.action(() => {
48567
+ cmd.outputHelp();
48568
+ });
48569
+ }
48537
48570
  const listeners = cmd._actionHandler;
48538
48571
  if (listeners) {
48539
48572
  const originalAction = listeners;
48540
48573
  const hasSubcommands = cmd.commands.length > 0;
48541
48574
  cmd._actionHandler = async (...args) => {
48542
- if (hasSubcommands && args.length > 0 && typeof args[0] === "string") {
48543
- console.error(`error: unknown command '${args[0]}'. See 'bootspring ${cmd.name()} --help'.`);
48544
- process.exitCode = 1;
48545
- return;
48575
+ if (hasSubcommands && cmd.args?.length > 0) {
48576
+ const unknownArg = cmd.args[0];
48577
+ if (typeof unknownArg === "string") {
48578
+ if (unknownArg === "help") {
48579
+ cmd.outputHelp();
48580
+ return;
48581
+ }
48582
+ const suggestion = suggestSubcommand(unknownArg, cmd);
48583
+ const hint = suggestion ? `
48584
+ (Did you mean ${suggestion}?)` : "";
48585
+ console.error(`error: unknown command '${unknownArg}'${hint}`);
48586
+ process.exitCode = 1;
48587
+ return;
48588
+ }
48546
48589
  }
48547
48590
  const commandName = cmd.name();
48548
48591
  presence_exports.sendHeartbeat({ activity: `bootspring ${commandName}` });
@@ -49553,6 +49596,10 @@ ${COLORS.cyan}${COLORS.bold}MCP Connectors${COLORS.reset}
49553
49596
  }
49554
49597
  });
49555
49598
  mcpCmd.action(async () => {
49599
+ if (process.stdin.isTTY) {
49600
+ mcpCmd.outputHelp();
49601
+ return;
49602
+ }
49556
49603
  console.error("Starting MCP Proxy Server...");
49557
49604
  try {
49558
49605
  const mcpServer = await Promise.resolve().then(() => (init_src3(), src_exports3));
@@ -470,7 +470,7 @@ interface InstallContext {
470
470
  scriptPath: string;
471
471
  }
472
472
  declare const PACKAGE_NAME = "@girardmedia/bootspring";
473
- declare const CURRENT_VERSION = "2.5.7";
473
+ declare const CURRENT_VERSION = "2.5.10";
474
474
  declare const DEFAULT_INTERVAL_MS: number;
475
475
  declare const STATE_PATH: string;
476
476
  declare function compareVersions(a: string, b: string): number;
package/dist/core.js CHANGED
@@ -372,7 +372,7 @@ var init_release = __esm({
372
372
  "../../packages/shared/src/release.ts"() {
373
373
  "use strict";
374
374
  init_cjs_shims();
375
- BOOTSPRING_VERSION = "2.5.7";
375
+ BOOTSPRING_VERSION = "2.5.10";
376
376
  BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
377
377
  }
378
378
  });
@@ -21577,7 +21577,7 @@ ${COLORS2.dim}Run "bootspring mcp" for server options${COLORS2.reset}
21577
21577
  console.log(`${COLORS2.dim}Run "bootspring mcp" for setup instructions.${COLORS2.reset}
21578
21578
  `);
21579
21579
  }
21580
- var BOOTSPRING_VERSION2 = "2.5.7";
21580
+ var BOOTSPRING_VERSION2 = "2.5.10";
21581
21581
  var BOOTSPRING_PACKAGE_NAME2 = "@girardmedia/bootspring";
21582
21582
  var REDACTED2 = "[REDACTED]";
21583
21583
  var SENSITIVE_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|refresh[_-]?token|authorization|x[_-]?api[_-]?key|project[_-]?id)$/i;
@@ -21625,7 +21625,7 @@ var require_package = __commonJS({
21625
21625
  "../../../package.json"(exports2, module2) {
21626
21626
  module2.exports = {
21627
21627
  name: "bootspring-workspace",
21628
- version: "2.5.8",
21628
+ version: "2.5.10",
21629
21629
  private: true,
21630
21630
  description: "Workspace tooling for the Bootspring monorepo",
21631
21631
  keywords: [
@@ -31370,7 +31370,7 @@ var init_release = __esm({
31370
31370
  "../../packages/shared/src/release.ts"() {
31371
31371
  "use strict";
31372
31372
  init_cjs_shims();
31373
- BOOTSPRING_VERSION = "2.5.7";
31373
+ BOOTSPRING_VERSION = "2.5.10";
31374
31374
  BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
31375
31375
  }
31376
31376
  });
@@ -52219,7 +52219,7 @@ var require_package = __commonJS({
52219
52219
  "../../../package.json"(exports2, module2) {
52220
52220
  module2.exports = {
52221
52221
  name: "bootspring-workspace",
52222
- version: "2.5.8",
52222
+ version: "2.5.10",
52223
52223
  private: true,
52224
52224
  description: "Workspace tooling for the Bootspring monorepo",
52225
52225
  keywords: [
@@ -52345,6 +52345,527 @@ var core = require_dist2();
52345
52345
  var api2 = core.api;
52346
52346
  var auth2 = core.auth;
52347
52347
  var VERSION = require_package().version;
52348
+ function getProjectRoot() {
52349
+ return process.cwd();
52350
+ }
52351
+ function loadBuildState() {
52352
+ const fs3 = require("fs");
52353
+ const path3 = require("path");
52354
+ const stateFile = path3.join(getProjectRoot(), "planning", "BUILD_STATE.json");
52355
+ if (!fs3.existsSync(stateFile)) return null;
52356
+ try {
52357
+ return JSON.parse(fs3.readFileSync(stateFile, "utf-8"));
52358
+ } catch {
52359
+ return null;
52360
+ }
52361
+ }
52362
+ function saveBuildState(state) {
52363
+ const fs3 = require("fs");
52364
+ const path3 = require("path");
52365
+ const planDir = path3.join(getProjectRoot(), "planning");
52366
+ if (!fs3.existsSync(planDir)) fs3.mkdirSync(planDir, { recursive: true });
52367
+ state.metadata = state.metadata || {};
52368
+ state.metadata.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52369
+ if (state.loopSession) state.loopSession.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
52370
+ fs3.writeFileSync(path3.join(planDir, "BUILD_STATE.json"), JSON.stringify(state, null, 2));
52371
+ }
52372
+ function buildGetStats(state) {
52373
+ if (!state) return null;
52374
+ const q = state.implementationQueue || [];
52375
+ const completed = q.filter((t) => t.status === "completed").length;
52376
+ const pending = q.filter((t) => t.status === "pending").length;
52377
+ const inProgress = q.filter((t) => t.status === "in_progress").length;
52378
+ const blocked = q.filter((t) => t.status === "blocked").length;
52379
+ const skipped = q.filter((t) => t.status === "skipped").length;
52380
+ const total = q.length;
52381
+ const percent = total > 0 ? Math.round(completed / total * 100) : 0;
52382
+ return { total, completed, pending, inProgress, blocked, skipped, percent };
52383
+ }
52384
+ function buildGetNextTask(state) {
52385
+ if (!state) return null;
52386
+ const q = state.implementationQueue || [];
52387
+ const ip = q.find((t) => t.status === "in_progress");
52388
+ if (ip) return ip;
52389
+ for (const task of q.filter((t) => t.status === "pending")) {
52390
+ if (!task.dependencies || task.dependencies.length === 0) return task;
52391
+ const allDone = task.dependencies.every((depId) => {
52392
+ const dep = q.find((t) => t.id === depId);
52393
+ return dep && dep.status === "completed";
52394
+ });
52395
+ if (allDone) return task;
52396
+ }
52397
+ return null;
52398
+ }
52399
+ function buildUpdateTaskStatus(state, taskId, status) {
52400
+ const task = (state.implementationQueue || []).find((t) => t.id === taskId);
52401
+ if (!task) return false;
52402
+ task.status = status;
52403
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
52404
+ if (status === "completed") task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
52405
+ if (status === "completed" || status === "skipped") {
52406
+ state.loopSession = state.loopSession || {};
52407
+ state.loopSession.currentIteration = (state.loopSession.currentIteration || 0) + 1;
52408
+ }
52409
+ return true;
52410
+ }
52411
+ function formatBuildTask(task) {
52412
+ if (!task) return null;
52413
+ return {
52414
+ id: task.id,
52415
+ title: task.title,
52416
+ description: task.description,
52417
+ phase: task.phase,
52418
+ source: task.source,
52419
+ sourceSection: task.sourceSection,
52420
+ acceptanceCriteria: task.acceptanceCriteria || []
52421
+ };
52422
+ }
52423
+ async function executeLocalBuild(args) {
52424
+ const action = args?.action || "status";
52425
+ const state = loadBuildState();
52426
+ switch (action) {
52427
+ case "status": {
52428
+ if (!state) {
52429
+ 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) }] };
52430
+ }
52431
+ const stats = buildGetStats(state);
52432
+ const currentTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52433
+ return { content: [{ type: "text", text: JSON.stringify({
52434
+ project: state.projectName,
52435
+ phase: state.currentPhase,
52436
+ status: state.status,
52437
+ progress: { completed: stats.completed, pending: stats.pending, inProgress: stats.inProgress, total: stats.total, percent: stats.percent },
52438
+ currentTask: currentTask ? { id: currentTask.id, title: currentTask.title } : null,
52439
+ nextAction: currentTask ? "Complete the current task, then use action=done" : "Use action=next to get the next task"
52440
+ }, null, 2) }] };
52441
+ }
52442
+ case "next": {
52443
+ if (!state) {
52444
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found", hint: "Use action=init to initialize" }, null, 2) }] };
52445
+ }
52446
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52447
+ if (inProgress) {
52448
+ return { content: [{ type: "text", text: JSON.stringify({
52449
+ message: "Task already in progress",
52450
+ state: "in_progress",
52451
+ task: formatBuildTask(inProgress),
52452
+ hint: "Complete this task with action=done, or use action=skip to skip it"
52453
+ }, null, 2) }] };
52454
+ }
52455
+ const nextTask = buildGetNextTask(state);
52456
+ if (!nextTask) {
52457
+ const stats2 = buildGetStats(state);
52458
+ return { content: [{ type: "text", text: JSON.stringify({ message: "All tasks complete!", progress: { completed: stats2.completed, total: stats2.total }, celebration: "Build finished!" }, null, 2) }] };
52459
+ }
52460
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52461
+ saveBuildState(state);
52462
+ const stats = buildGetStats(state);
52463
+ return { content: [{ type: "text", text: JSON.stringify({
52464
+ task: formatBuildTask(nextTask),
52465
+ state: "task_ready",
52466
+ file: "planning/TODO.md",
52467
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent },
52468
+ instructions: [
52469
+ `Find ${nextTask.id} in planning/TODO.md for full details`,
52470
+ "Implement the task in the codebase",
52471
+ "Ensure acceptance criteria are met",
52472
+ "Run quality checks (lint, test)",
52473
+ "Commit changes with descriptive message",
52474
+ "Use action=done when complete"
52475
+ ]
52476
+ }, null, 2) }] };
52477
+ }
52478
+ case "current": {
52479
+ if (!state) {
52480
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52481
+ }
52482
+ const currentTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52483
+ if (!currentTask) {
52484
+ return { content: [{ type: "text", text: JSON.stringify({ message: "No task currently in progress", hint: "Use action=next to get the next task" }, null, 2) }] };
52485
+ }
52486
+ return { content: [{ type: "text", text: JSON.stringify({
52487
+ task: formatBuildTask(currentTask),
52488
+ instructions: ["Implement this task in the codebase", "Run quality checks (lint, test)", "Commit your changes", "Use action=done to mark complete"]
52489
+ }, null, 2) }] };
52490
+ }
52491
+ case "done": {
52492
+ if (!state) {
52493
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52494
+ }
52495
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52496
+ if (!inProgress) {
52497
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No task currently in progress", hint: "Use action=next to get a task first" }, null, 2) }] };
52498
+ }
52499
+ buildUpdateTaskStatus(state, inProgress.id, "completed");
52500
+ const nextTask = buildGetNextTask(state);
52501
+ if (nextTask) {
52502
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52503
+ }
52504
+ saveBuildState(state);
52505
+ const stats = buildGetStats(state);
52506
+ return { content: [{ type: "text", text: JSON.stringify({
52507
+ completed: { id: inProgress.id, title: inProgress.title },
52508
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent },
52509
+ nextTask: nextTask ? { ...formatBuildTask(nextTask), file: "planning/TODO.md" } : null,
52510
+ allComplete: !nextTask,
52511
+ message: nextTask ? `Complete! Next: implement ${nextTask.id} \u2014 ${nextTask.title}` : "Build complete! All tasks finished."
52512
+ }, null, 2) }] };
52513
+ }
52514
+ case "skip": {
52515
+ if (!state) {
52516
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52517
+ }
52518
+ const inProgress = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52519
+ if (!inProgress) {
52520
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No task to skip", hint: "Use action=next to get a task first" }, null, 2) }] };
52521
+ }
52522
+ buildUpdateTaskStatus(state, inProgress.id, "skipped");
52523
+ const nextTask = buildGetNextTask(state);
52524
+ if (nextTask) {
52525
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52526
+ }
52527
+ saveBuildState(state);
52528
+ return { content: [{ type: "text", text: JSON.stringify({
52529
+ skipped: { id: inProgress.id, title: inProgress.title, reason: args?.reason || "Skipped" },
52530
+ nextTask: nextTask ? formatBuildTask(nextTask) : null,
52531
+ message: nextTask ? `Skipped. Next: ${nextTask.title}` : "No more tasks available"
52532
+ }, null, 2) }] };
52533
+ }
52534
+ case "list": {
52535
+ if (!state) {
52536
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found" }, null, 2) }] };
52537
+ }
52538
+ const tasks = (state.implementationQueue || []).map((t) => ({ id: t.id, title: t.title, status: t.status, phase: t.phase }));
52539
+ return { content: [{ type: "text", text: JSON.stringify({ project: state.projectName, tasks }, null, 2) }] };
52540
+ }
52541
+ case "advance": {
52542
+ if (!state) {
52543
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No build state found", hint: "Use action=init or action=sync first" }, null, 2) }] };
52544
+ }
52545
+ const activeTask = (state.implementationQueue || []).find((t) => t.status === "in_progress");
52546
+ if (activeTask) {
52547
+ if (args?.autoDone) {
52548
+ try {
52549
+ const { execSync } = require("child_process");
52550
+ const gitStatus = execSync("git status --porcelain", { cwd: getProjectRoot(), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
52551
+ if (gitStatus.trim().length === 0) {
52552
+ buildUpdateTaskStatus(state, activeTask.id, "completed");
52553
+ const nextTask2 = buildGetNextTask(state);
52554
+ if (nextTask2) buildUpdateTaskStatus(state, nextTask2.id, "in_progress");
52555
+ saveBuildState(state);
52556
+ const stats = buildGetStats(state);
52557
+ return { content: [{ type: "text", text: JSON.stringify({
52558
+ state: nextTask2 ? "advanced" : "all_complete",
52559
+ autoCompleted: true,
52560
+ completed: { id: activeTask.id, title: activeTask.title },
52561
+ nextTask: nextTask2 ? formatBuildTask(nextTask2) : null,
52562
+ progress: { completed: stats.completed, total: stats.total, percent: stats.percent }
52563
+ }, null, 2) }] };
52564
+ }
52565
+ } catch {
52566
+ }
52567
+ }
52568
+ return { content: [{ type: "text", text: JSON.stringify({
52569
+ state: "in_progress",
52570
+ task: formatBuildTask(activeTask),
52571
+ hint: "Finish implementation and use action=done, or call action=advance with autoDone=true after commit"
52572
+ }, null, 2) }] };
52573
+ }
52574
+ const nextTask = buildGetNextTask(state);
52575
+ if (!nextTask) {
52576
+ const stats = buildGetStats(state);
52577
+ 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) }] };
52578
+ }
52579
+ buildUpdateTaskStatus(state, nextTask.id, "in_progress");
52580
+ saveBuildState(state);
52581
+ return { content: [{ type: "text", text: JSON.stringify({
52582
+ state: "task_ready",
52583
+ task: formatBuildTask(nextTask),
52584
+ file: "planning/TODO.md",
52585
+ hint: "Implement task, then call action=advance (or action=done) to continue loop"
52586
+ }, null, 2) }] };
52587
+ }
52588
+ case "sync": {
52589
+ const fs3 = require("fs");
52590
+ const pathMod = require("path");
52591
+ const projectRoot = getProjectRoot();
52592
+ const todoPath = pathMod.join(projectRoot, "planning", "TODO.md");
52593
+ if (!fs3.existsSync(todoPath)) {
52594
+ return { content: [{ type: "text", text: JSON.stringify({ error: "planning/TODO.md not found", hint: "Create planning/TODO.md with tasks first" }, null, 2) }] };
52595
+ }
52596
+ const todoContent = fs3.readFileSync(todoPath, "utf-8");
52597
+ const taskRegex = /^-\s*\[([ xX])\]\s*(?:\*\*)?([a-zA-Z]+-\d+)(?:\*\*)?[:\s]+(.+?)$/gm;
52598
+ const tasks = [];
52599
+ let match;
52600
+ while ((match = taskRegex.exec(todoContent)) !== null) {
52601
+ const checked = match[1].toLowerCase() === "x";
52602
+ tasks.push({
52603
+ id: match[2],
52604
+ title: match[3].trim().replace(/\*\*/g, ""),
52605
+ status: checked ? "completed" : "pending",
52606
+ phase: "mvp",
52607
+ source: "TODO.md"
52608
+ });
52609
+ }
52610
+ if (tasks.length === 0) {
52611
+ const simpleLine = /(?:bs|task)-\d+[:\s]+(.+)/gi;
52612
+ let lineMatch;
52613
+ while ((lineMatch = simpleLine.exec(todoContent)) !== null) {
52614
+ tasks.push({
52615
+ id: lineMatch[0].split(/[:\s]/)[0],
52616
+ title: lineMatch[1]?.trim() || lineMatch[0],
52617
+ status: "pending",
52618
+ phase: "mvp",
52619
+ source: "TODO.md"
52620
+ });
52621
+ }
52622
+ }
52623
+ const currentState = state || {
52624
+ version: "1.0.0",
52625
+ projectName: "Project",
52626
+ status: "pending",
52627
+ currentPhase: "mvp",
52628
+ phases: {},
52629
+ implementationQueue: [],
52630
+ mvpCriteria: { features: [], completionPercentage: 0, allCriteriaMet: false },
52631
+ loopSession: { sessionId: `build-${Date.now()}`, currentIteration: 0, maxIterations: 500, startedAt: (/* @__PURE__ */ new Date()).toISOString(), lastUpdated: null, pausedAt: null },
52632
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), seedSource: "TODO.md", preseedDocs: [] }
52633
+ };
52634
+ if (args?.replace) {
52635
+ currentState.implementationQueue = tasks;
52636
+ } else {
52637
+ const existingIds = new Set((currentState.implementationQueue || []).map((t) => t.id));
52638
+ for (const task of tasks) {
52639
+ if (!existingIds.has(task.id)) {
52640
+ currentState.implementationQueue = currentState.implementationQueue || [];
52641
+ currentState.implementationQueue.push(task);
52642
+ }
52643
+ }
52644
+ }
52645
+ saveBuildState(currentState);
52646
+ const stats = buildGetStats(currentState);
52647
+ return { content: [{ type: "text", text: JSON.stringify({
52648
+ success: true,
52649
+ source: "TODO.md",
52650
+ tasksFound: tasks.length,
52651
+ mode: args?.replace ? "replace" : "merge",
52652
+ progress: { total: stats.total, completed: stats.completed, pending: stats.pending },
52653
+ nextStep: "Use action=next to get the first task"
52654
+ }, null, 2) }] };
52655
+ }
52656
+ case "init": {
52657
+ const fs3 = require("fs");
52658
+ const pathMod = require("path");
52659
+ const projectRoot = getProjectRoot();
52660
+ const todoPath = pathMod.join(projectRoot, "planning", "TODO.md");
52661
+ if (fs3.existsSync(todoPath)) {
52662
+ return executeLocalBuild({ ...args, action: "sync", replace: true });
52663
+ }
52664
+ const newState = {
52665
+ version: "1.0.0",
52666
+ projectName: pathMod.basename(projectRoot),
52667
+ status: "pending",
52668
+ currentPhase: null,
52669
+ phases: { foundation: { status: "pending", tasks: [] }, mvp: { status: "pending", tasks: [] }, launch: { status: "pending", tasks: [] } },
52670
+ implementationQueue: [],
52671
+ mvpCriteria: { features: [], completionPercentage: 0, allCriteriaMet: false },
52672
+ loopSession: { sessionId: `build-${Date.now()}`, currentIteration: 0, maxIterations: 500, startedAt: (/* @__PURE__ */ new Date()).toISOString(), lastUpdated: null, pausedAt: null },
52673
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), seedSource: null, preseedDocs: [] }
52674
+ };
52675
+ saveBuildState(newState);
52676
+ return { content: [{ type: "text", text: JSON.stringify({
52677
+ success: true,
52678
+ message: "Build state initialized",
52679
+ project: newState.projectName,
52680
+ hint: "Create planning/TODO.md with tasks, then use action=sync to load them"
52681
+ }, null, 2) }] };
52682
+ }
52683
+ default:
52684
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["next", "current", "done", "skip", "status", "list", "init", "sync", "advance"] }, null, 2) }] };
52685
+ }
52686
+ }
52687
+ async function executeLocalSeed(args) {
52688
+ const fs3 = require("fs");
52689
+ const pathMod = require("path");
52690
+ const action = args?.action || "status";
52691
+ const projectRoot = getProjectRoot();
52692
+ const CONTEXT_DIR = ".bootspring/context";
52693
+ const contextDir = pathMod.join(projectRoot, CONTEXT_DIR);
52694
+ const CONTEXT_DOCS = {
52695
+ vision: { name: "VISION.md", title: "Vision", description: "Problem, solution, core values", required: true },
52696
+ audience: { name: "AUDIENCE.md", title: "Audience", description: "Target audience, personas, ICP", required: true },
52697
+ market: { name: "MARKET.md", title: "Market", description: "TAM/SAM/SOM, market opportunity", required: false },
52698
+ competitors: { name: "COMPETITORS.md", title: "Competitors", description: "Competitive landscape, differentiation", required: false },
52699
+ "business-model": { name: "BUSINESS_MODEL.md", title: "Business Model", description: "Revenue, pricing, unit economics", required: true },
52700
+ prd: { name: "PRD.md", title: "PRD", description: "Product requirements, user stories", required: true },
52701
+ "technical-spec": { name: "TECHNICAL_SPEC.md", title: "Technical Spec", description: "Architecture, tech stack, data model", required: false },
52702
+ roadmap: { name: "ROADMAP.md", title: "Roadmap", description: "Phases, milestones, timeline", required: false }
52703
+ };
52704
+ const PRESETS = {
52705
+ essential: ["vision", "audience", "business-model", "prd"],
52706
+ startup: ["vision", "audience", "market", "competitors", "business-model", "prd", "roadmap"],
52707
+ full: Object.keys(CONTEXT_DOCS),
52708
+ technical: ["vision", "prd", "technical-spec", "roadmap"],
52709
+ investor: ["vision", "audience", "market", "competitors", "business-model", "roadmap"]
52710
+ };
52711
+ switch (action) {
52712
+ case "setup":
52713
+ case "init": {
52714
+ const preset = args?.preset || "startup";
52715
+ if (!fs3.existsSync(contextDir)) fs3.mkdirSync(contextDir, { recursive: true });
52716
+ let projectName = pathMod.basename(projectRoot);
52717
+ try {
52718
+ const pkg = JSON.parse(fs3.readFileSync(pathMod.join(projectRoot, "package.json"), "utf-8"));
52719
+ projectName = pkg.name || projectName;
52720
+ } catch {
52721
+ }
52722
+ const docs = PRESETS[preset] || PRESETS.startup;
52723
+ let generated = 0;
52724
+ for (const docType of docs) {
52725
+ const meta = CONTEXT_DOCS[docType];
52726
+ if (!meta) continue;
52727
+ const filePath = pathMod.join(contextDir, meta.name);
52728
+ if (!fs3.existsSync(filePath)) {
52729
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
52730
+ fs3.writeFileSync(filePath, `# ${projectName} \u2014 ${meta.title}
52731
+
52732
+ **Generated:** ${date}
52733
+ **Status:** Draft
52734
+
52735
+ ## ${meta.title}
52736
+
52737
+ *${meta.description}*
52738
+
52739
+ ---
52740
+
52741
+ *Edit this file with your project details.*
52742
+ `);
52743
+ generated++;
52744
+ }
52745
+ }
52746
+ return { content: [{ type: "text", text: JSON.stringify({
52747
+ success: true,
52748
+ action: "init",
52749
+ preset,
52750
+ generated,
52751
+ contextDir: CONTEXT_DIR,
52752
+ docs: docs.map((d) => CONTEXT_DOCS[d]?.name).filter(Boolean),
52753
+ nextSteps: ["Edit the .md files in .bootspring/context/", 'Run bootspring_seed with action: "generate" or run bootspring seed merge']
52754
+ }, null, 2) }] };
52755
+ }
52756
+ case "status": {
52757
+ const seedPath = pathMod.join(projectRoot, "planning", "SEED.md");
52758
+ const hasSeed = fs3.existsSync(seedPath);
52759
+ let docs = [];
52760
+ if (fs3.existsSync(contextDir)) {
52761
+ docs = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md");
52762
+ }
52763
+ return { content: [{ type: "text", text: JSON.stringify({
52764
+ contextDocs: docs,
52765
+ total: docs.length,
52766
+ contextDir: CONTEXT_DIR,
52767
+ seedMd: hasSeed,
52768
+ seedPath: hasSeed ? "planning/SEED.md" : null,
52769
+ nextStep: docs.length === 0 ? 'Run bootspring_seed with action: "init"' : !hasSeed ? "Edit docs, then merge" : "Ready to build"
52770
+ }, null, 2) }] };
52771
+ }
52772
+ case "generate": {
52773
+ if (!fs3.existsSync(contextDir)) {
52774
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No context docs found", hint: 'Run bootspring_seed with action: "init" first' }, null, 2) }] };
52775
+ }
52776
+ const validDocs = Object.values(CONTEXT_DOCS).map((d) => d.name);
52777
+ const files = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md").sort((a, b) => {
52778
+ const ai = validDocs.indexOf(a);
52779
+ const bi = validDocs.indexOf(b);
52780
+ return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
52781
+ });
52782
+ if (files.length === 0) {
52783
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No .md files in context folder" }, null, 2) }] };
52784
+ }
52785
+ const sections = files.map((f) => fs3.readFileSync(pathMod.join(contextDir, f), "utf-8"));
52786
+ const planDir = pathMod.join(projectRoot, "planning");
52787
+ if (!fs3.existsSync(planDir)) fs3.mkdirSync(planDir, { recursive: true });
52788
+ fs3.writeFileSync(pathMod.join(planDir, "SEED.md"), sections.join("\n\n---\n\n"));
52789
+ return { content: [{ type: "text", text: JSON.stringify({
52790
+ success: true,
52791
+ action: "generate",
52792
+ merged: files.length,
52793
+ output: "planning/SEED.md",
52794
+ files,
52795
+ nextStep: 'Create planning/TODO.md with tasks, then use bootspring_build action: "sync"'
52796
+ }, null, 2) }] };
52797
+ }
52798
+ case "export": {
52799
+ if (!fs3.existsSync(contextDir)) {
52800
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No context docs found" }, null, 2) }] };
52801
+ }
52802
+ const docs = {};
52803
+ const files = fs3.readdirSync(contextDir).filter((f) => f.endsWith(".md") && f !== "README.md");
52804
+ for (const file of files) {
52805
+ docs[file.replace(".md", "")] = fs3.readFileSync(pathMod.join(contextDir, file), "utf-8");
52806
+ }
52807
+ return { content: [{ type: "text", text: JSON.stringify({ context: docs, exportedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) }] };
52808
+ }
52809
+ default:
52810
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}`, validActions: ["setup", "init", "generate", "status", "export"] }, null, 2) }] };
52811
+ }
52812
+ }
52813
+ async function executeLocalTodo(args) {
52814
+ const fs3 = require("fs");
52815
+ const pathMod = require("path");
52816
+ const action = args?.action || "list";
52817
+ const projectRoot = getProjectRoot();
52818
+ const todoFile = pathMod.join(projectRoot, ".bootspring", "todos.json");
52819
+ function loadTodos() {
52820
+ if (!fs3.existsSync(todoFile)) return [];
52821
+ try {
52822
+ return JSON.parse(fs3.readFileSync(todoFile, "utf-8"));
52823
+ } catch {
52824
+ return [];
52825
+ }
52826
+ }
52827
+ function saveTodos(todos) {
52828
+ const dir = pathMod.dirname(todoFile);
52829
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
52830
+ fs3.writeFileSync(todoFile, JSON.stringify(todos, null, 2));
52831
+ }
52832
+ switch (action) {
52833
+ case "list": {
52834
+ const todos = loadTodos();
52835
+ return { content: [{ type: "text", text: JSON.stringify({ todos, total: todos.length }, null, 2) }] };
52836
+ }
52837
+ case "add": {
52838
+ const todos = loadTodos();
52839
+ const newTodo = { id: `todo-${Date.now()}`, text: args?.text || "Untitled", completed: false, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
52840
+ todos.push(newTodo);
52841
+ saveTodos(todos);
52842
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, todo: newTodo }, null, 2) }] };
52843
+ }
52844
+ case "complete": {
52845
+ const todos = loadTodos();
52846
+ const todo = todos.find((t) => t.id === args?.id);
52847
+ if (!todo) return { content: [{ type: "text", text: JSON.stringify({ error: "Todo not found" }, null, 2) }] };
52848
+ todo.completed = true;
52849
+ todo.completedAt = (/* @__PURE__ */ new Date()).toISOString();
52850
+ saveTodos(todos);
52851
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, todo }, null, 2) }] };
52852
+ }
52853
+ case "delete": {
52854
+ let todos = loadTodos();
52855
+ const before = todos.length;
52856
+ todos = todos.filter((t) => t.id !== args?.id);
52857
+ saveTodos(todos);
52858
+ return { content: [{ type: "text", text: JSON.stringify({ success: todos.length < before, remaining: todos.length }, null, 2) }] };
52859
+ }
52860
+ default:
52861
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${action}` }, null, 2) }] };
52862
+ }
52863
+ }
52864
+ var LOCAL_TOOL_EXECUTORS = {
52865
+ bootspring_build: executeLocalBuild,
52866
+ bootspring_seed: executeLocalSeed,
52867
+ bootspring_todo: executeLocalTodo
52868
+ };
52348
52869
  var FALLBACK_TOOLS = [
52349
52870
  {
52350
52871
  name: "bootspring_agent",
@@ -52513,20 +53034,7 @@ function formatProxyError(error) {
52513
53034
  }
52514
53035
  return error?.message || "Unknown error from Bootspring API.";
52515
53036
  }
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
- }
53037
+ var LOCAL_TOOLS = new Set(Object.keys(LOCAL_TOOL_EXECUTORS));
52530
53038
  async function resolveTools() {
52531
53039
  if (!auth2.isAuthenticated()) {
52532
53040
  return FALLBACK_TOOLS;
@@ -52560,9 +53068,14 @@ async function resolveResources() {
52560
53068
  }
52561
53069
  async function proxyToolCall(name, args) {
52562
53070
  if (LOCAL_TOOLS.has(name)) {
52563
- return {
52564
- content: [{ type: "text", text: formatLocalToolGuidance(name, args) }]
52565
- };
53071
+ try {
53072
+ return await LOCAL_TOOL_EXECUTORS[name](args || {});
53073
+ } catch (error) {
53074
+ return {
53075
+ content: [{ type: "text", text: `Error executing ${name}: ${error?.message || error}` }],
53076
+ isError: true
53077
+ };
53078
+ }
52566
53079
  }
52567
53080
  if (!auth2.isAuthenticated()) {
52568
53081
  return createAuthError("Authentication required. Run `bootspring auth login` first.");
@@ -52578,7 +53091,7 @@ async function proxyToolCall(name, args) {
52578
53091
  const response = await api2.callMcpTool(name, args || {});
52579
53092
  if (response && response.result && response.result.status === "local_execution_required") {
52580
53093
  return {
52581
- content: [{ type: "text", text: response.result.message || formatLocalToolGuidance(name, args) }]
53094
+ content: [{ type: "text", text: response.result.message || `${name} requires local execution. Run: bootspring ${name.replace("bootspring_", "")} ${args?.action || ""}`.trim() }]
52582
53095
  };
52583
53096
  }
52584
53097
  return {
@@ -52674,11 +53187,11 @@ module.exports = {
52674
53187
  FALLBACK_TOOLS,
52675
53188
  FALLBACK_RESOURCES,
52676
53189
  main,
52677
- // Exposed for tests — lock in the stale-install regression guard
53190
+ // Exposed for tests
52678
53191
  _diagnoseApiSurface: diagnoseApiSurface,
52679
53192
  _formatProxyError: formatProxyError,
52680
53193
  _LOCAL_TOOLS: LOCAL_TOOLS,
52681
- _formatLocalToolGuidance: formatLocalToolGuidance,
53194
+ _LOCAL_TOOL_EXECUTORS: LOCAL_TOOL_EXECUTORS,
52682
53195
  _proxyToolCall: proxyToolCall,
52683
53196
  _diagnoseFromApi: function diagnoseFromApi(apiShim, version) {
52684
53197
  const required = ["callMcpTool", "listMcpTools", "listMcpResources", "getMcpResource"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.5.8",
3
+ "version": "2.5.10",
4
4
  "description": "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
5
5
  "keywords": [
6
6
  "ai",