@agent-plan/mcp 0.2.10 → 0.2.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAgyBA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA21BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { PlanStore, ExportService, withFeatureLock, needsMotivation } from "@agent-plan/core";
9
9
  import { serve } from "@agent-plan/server";
10
- import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, normalizeSlug } from "@agent-plan/core/naming";
10
+ import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug } from "@agent-plan/core/naming";
11
11
  const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
12
12
  const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
13
13
  function text(textValue, structuredContent) {
@@ -30,9 +30,67 @@ async function requireStore() {
30
30
  throw new Error(`No .planner/ found at ${st.root}. Use planner-init first.`);
31
31
  return st;
32
32
  }
33
- // In-process web server handle, managed by the planner-web tool. Lives as long
34
- // as this MCP stdio process (i.e. the host session). Null when not running.
33
+ // In-process web server handle, managed by the planner-web tool and planner-load.
34
+ // Lives as long as this MCP stdio process (i.e. the host session). Null when not running.
35
35
  let webHandle = null;
36
+ /** Start the web dashboard on LAN (0.0.0.0:0, OS-assigned port) if not already
37
+ * running. Returns the local/LAN URLs. No-op (empty url) when no .planner/ exists. */
38
+ async function ensureWebStarted() {
39
+ if (webHandle)
40
+ return { localUrl: webHandle.localUrl, lanUrl: webHandle.lanUrl, mode: webHandle.mode };
41
+ const root = planRoot();
42
+ const st = new PlanStore(root);
43
+ if (!(await st.exists()))
44
+ return { localUrl: "" };
45
+ try {
46
+ webHandle = await serve({ planRoot: root, host: "0.0.0.0", port: 0, quiet: true });
47
+ return { localUrl: webHandle.localUrl, lanUrl: webHandle.lanUrl, mode: webHandle.mode };
48
+ }
49
+ catch {
50
+ return { localUrl: "" };
51
+ }
52
+ }
53
+ /** Build a consolidated planner recap (project state + active work + pending
54
+ * handoff + web URL). Harness-agnostic equivalent of Pi's buildStartupResumeSummary. */
55
+ async function buildRecapText(st, web) {
56
+ const [plan, resume, handoff] = await Promise.all([st.loadAll(), st.loadResume(), st.loadHandoff()]);
57
+ const allTasks = plan.phases.flatMap((p) => p.tasks.map((t) => ({ phase: p, task: t })));
58
+ const totalF = plan.features.features.length;
59
+ const doneF = plan.features.features.filter((f) => f.status === "done").length;
60
+ const activeF = plan.features.features.filter((f) => f.status === "in-progress").length;
61
+ const totalP = plan.phases.length;
62
+ const doneP = plan.phases.filter((p) => p.status === "done").length;
63
+ const activeP = plan.phases.filter((p) => p.status === "in-progress" || p.status === "discovery").length;
64
+ const totalT = allTasks.length;
65
+ const doneT = allTasks.filter(({ task }) => task.status === "done").length;
66
+ const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
67
+ const inProgress = allTasks.filter(({ task }) => task.status === "in-progress");
68
+ const focus = inProgress[0];
69
+ const focusFeature = focus ? plan.features.features.find((f) => f.id === focus.phase.featureId) : undefined;
70
+ const lines = [];
71
+ lines.push("## Planner recap");
72
+ const name = plan.project.name || "(unnamed project)";
73
+ lines.push(`Project: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
74
+ lines.push(`Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active)`);
75
+ if (focus) {
76
+ lines.push(`Current focus: ${focusFeature?.name ?? "?"} / ${focus.phase.title} / ${focus.task.title} (in-progress)`);
77
+ }
78
+ else {
79
+ lines.push("Current focus: no active task — review the plan and pick the next concrete task");
80
+ }
81
+ if (resume?.nextSteps?.length)
82
+ lines.push(`Next step: ${resume.nextSteps[0]}`);
83
+ if (handoff) {
84
+ lines.push("", "## Pending handoff (read me first)", handoff.content || "(empty handoff)", "", "→ After processing the handoff above, call planner-handoff-clear to remove it.");
85
+ }
86
+ else if (activeT === 0) {
87
+ lines.push("", "No handoff pending and no task in-progress. Use planner-task-add / planner-task-start to begin work.");
88
+ }
89
+ if (web.localUrl) {
90
+ lines.push("", "## Web UI", `🌐 ${web.localUrl}${web.lanUrl ? " (LAN: " + web.lanUrl + ")" : ""}`);
91
+ }
92
+ return lines.join("\n");
93
+ }
36
94
  function findFeatureByRef(features, ref) {
37
95
  const normalized = ref.trim().toLowerCase();
38
96
  return features.find((feature) => feature.id.toLowerCase() === normalized)
@@ -508,7 +566,7 @@ server.registerTool("planner-task-add", {
508
566
  id: taskId,
509
567
  phaseId: found.id,
510
568
  number: found.tasks.length + 1,
511
- shortName: normalizeSlug(title).slice(0, 30),
569
+ shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
512
570
  title: title.trim(),
513
571
  status: "planned",
514
572
  description: description?.trim() ?? "",
@@ -644,12 +702,13 @@ server.registerTool("planner-task-start", {
644
702
  inputSchema: { task: z.string().min(1) },
645
703
  }, async ({ task: ref }) => {
646
704
  const st = await requireStore();
647
- // Hygiene Gate: block starting work if a pending handoff exists.
648
- if (existsSync(join(st.root, "HANDOFF.md"))) {
649
- return text(`🚨 HYGIENE VIOLATION: A pending handoff file exists at .planner/HANDOFF.md. ` +
650
- `You MUST read and delete it before you can officially start a task. ` +
651
- `This is a non-negotiable rule in AGENTS.md.`);
652
- }
705
+ // Hygiene notice (non-blocking): surface an existing handoff but never block
706
+ // task_start — the handoff is a captured-context artifact, not a lock (e.g. a
707
+ // small modification after writing a handoff should start without forcing
708
+ // deletion, which would lose context).
709
+ const handoffNotice = existsSync(join(st.root, "HANDOFF.md"))
710
+ ? "ℹ️ A handoff exists at .planner/HANDOFF.md — read it for context, then delete it with planner-handoff-delete when no longer needed. Proceeding with task start.\n"
711
+ : "";
653
712
  const found = findTaskByRef(await st.loadAllPhases(), ref);
654
713
  if (!found)
655
714
  return text(`Task not found: ${ref}`);
@@ -677,7 +736,7 @@ server.registerTool("planner-task-start", {
677
736
  return phase;
678
737
  });
679
738
  await st.syncTaskStatusRollup(found.phase.id);
680
- return writeAndSummarize(st, `✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
739
+ return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
681
740
  });
682
741
  server.registerTool("planner-task-complete", {
683
742
  description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
@@ -798,8 +857,15 @@ server.registerTool("planner-web", {
798
857
  return text("planner-web not running. Use planner-web with action=start to start the dashboard.");
799
858
  });
800
859
  server.registerTool("planner-load", {
801
- description: "MCP no-op equivalent of Pi /planner load. MCP server is already loaded when tools are available.",
802
- }, async () => text("Planner MCP server is loaded."));
860
+ description: "Load/refresh the planner on explicit user request (NOT automatic): starts the web dashboard on LAN and returns a consolidated recap (project state, active task, pending handoff, web URL). This is the MCP equivalent of Pi /planner load. Call it ONLY when the user runs /planner load or /planner recap (or asks to load the planner). Present the recap to the user in that reply, including the web URL on a final prominent line. If a pending handoff is included, read it, summarize it to the user, then call planner-handoff-clear. Do NOT start the planner/web or show the web URL unless the user explicitly asks (load/recap/web status).",
861
+ }, async () => {
862
+ const st = store();
863
+ if (!(await st.exists()))
864
+ return text("No .planner/ found at " + planRoot() + ". Run planner-init first.");
865
+ const web = await ensureWebStarted();
866
+ const recap = await buildRecapText(st, web);
867
+ return text(recap);
868
+ });
803
869
  server.registerTool("planner-disable", {
804
870
  description: "MCP no-op equivalent of Pi /planner disable. Stop the MCP process from the host to disable it.",
805
871
  }, async () => text("To disable planner MCP, remove/disable this MCP server from the host config or stop the process."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.10",
3
+ "version": "0.2.16",
4
4
  "private": false,
5
5
  "description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
6
6
  "license": "MIT",
@@ -41,8 +41,8 @@
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.17.0",
43
43
  "zod": "^3.24.0",
44
- "@agent-plan/core": "0.2.10",
45
- "@agent-plan/server": "0.2.10"
44
+ "@agent-plan/core": "0.2.16",
45
+ "@agent-plan/server": "0.2.16"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.json",