@magic-spells/constellation 0.1.1 → 0.2.1
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/README.md +35 -3
- package/dist/cli/index.js +75 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/{mcp → core}/git.d.ts +19 -0
- package/dist/{mcp → core}/git.js +59 -2
- package/dist/core/git.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/repos.d.ts +40 -0
- package/dist/core/repos.js +93 -0
- package/dist/core/repos.js.map +1 -0
- package/dist/core/sync.d.ts +28 -0
- package/dist/core/sync.js +85 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/types.d.ts +12 -0
- package/dist/mcp/server.js +313 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/serve/server.js +28 -4
- package/dist/serve/server.js.map +1 -1
- package/docs/001-file-format.md +25 -0
- package/docs/002-mcp.md +30 -0
- package/package.json +1 -1
- package/schemas/plan.json +27 -0
- package/skill/SKILL.md +65 -0
- package/skill/methodology.md +17 -0
- package/skill/types/plan.md +19 -0
- package/viewer/dist/assets/{arc-z-d28NWT.js → arc-CMMRgIq9.js} +1 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-Cbb-syEI.js +1 -0
- package/viewer/dist/assets/{architectureDiagram-3BPJPVTR-CjoeulX0.js → architectureDiagram-3BPJPVTR-CB5p_qlN.js} +1 -1
- package/viewer/dist/assets/{blockDiagram-GPEHLZMM-BGlWRy-x.js → blockDiagram-GPEHLZMM-DpnMj_e6.js} +1 -1
- package/viewer/dist/assets/{c4Diagram-AAUBKEIU-B09gKF2I.js → c4Diagram-AAUBKEIU-f91ngVN8.js} +1 -1
- package/viewer/dist/assets/channel-C2bkEQHQ.js +1 -0
- package/viewer/dist/assets/{chunk-2J33WTMH-Cfc4xM5m.js → chunk-2J33WTMH-y7Pz5tMO.js} +1 -1
- package/viewer/dist/assets/{chunk-3OPIFGDE-CfJIsbrs.js → chunk-3OPIFGDE-Ir_ghIUQ.js} +1 -1
- package/viewer/dist/assets/{chunk-4BX2VUAB-B1J5W5bz.js → chunk-4BX2VUAB-BTPyM1ux.js} +1 -1
- package/viewer/dist/assets/{chunk-55IACEB6-Bjg14U7h.js → chunk-55IACEB6-CtNd8e4Y.js} +1 -1
- package/viewer/dist/assets/{chunk-5ZQYHXKU-BCvNFwZ5.js → chunk-5ZQYHXKU-Dd3yz721.js} +1 -1
- package/viewer/dist/assets/{chunk-727SXJPM-XfiLvG1c.js → chunk-727SXJPM-CAM8mGbv.js} +1 -1
- package/viewer/dist/assets/{chunk-AQP2D5EJ-BS-Yzcyy.js → chunk-AQP2D5EJ-C3nW9Uve.js} +1 -1
- package/viewer/dist/assets/{chunk-BSJP7CBP-DdbVG_ma.js → chunk-BSJP7CBP-r6MRPvAH.js} +1 -1
- package/viewer/dist/assets/{chunk-CSCIHK7Q-C_v3KOxf.js → chunk-CSCIHK7Q-Cn-ZTxsS.js} +1 -1
- package/viewer/dist/assets/{chunk-FMBD7UC4-Djdrd8lQ.js → chunk-FMBD7UC4-5axXeZJa.js} +1 -1
- package/viewer/dist/assets/{chunk-KSCS5N6A-CLg-rwxg.js → chunk-KSCS5N6A-BwmNx0l0.js} +1 -1
- package/viewer/dist/assets/{chunk-L5ZTLDWV-DCOC9Gbq.js → chunk-L5ZTLDWV-C2YNkUjB.js} +1 -1
- package/viewer/dist/assets/{chunk-LZXEDZCA-D_cw-C3l.js → chunk-LZXEDZCA-CznuDgQD.js} +2 -2
- package/viewer/dist/assets/{chunk-ND2GUHAM-7krqLOpO.js → chunk-ND2GUHAM-CokDnghi.js} +1 -1
- package/viewer/dist/assets/{chunk-NZK2D7GU-BHqkidnq.js → chunk-NZK2D7GU-aaU0v4QE.js} +1 -1
- package/viewer/dist/assets/{chunk-O5CBEL6O-D953Jfjo.js → chunk-O5CBEL6O-ByfKhkZf.js} +1 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-DJRjGeIo.js +1 -0
- package/viewer/dist/assets/chunk-WU5MYG2G-C63BVvr_.js +1 -0
- package/viewer/dist/assets/{chunk-XPW4576I-tr-y3uUk.js → chunk-XPW4576I-Amo3awtq.js} +1 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-z_bO1V-j.js +1 -0
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-z_bO1V-j.js +1 -0
- package/viewer/dist/assets/{cose-bilkent-S5V4N54A-RHqSF1Fo.js → cose-bilkent-S5V4N54A-CO8yESCW.js} +1 -1
- package/viewer/dist/assets/{dagre-BM42HDAG-CaRlC_3n.js → dagre-BM42HDAG-BnmNVNI2.js} +1 -1
- package/viewer/dist/assets/{diagram-2AECGRRQ-zBc7NQxu.js → diagram-2AECGRRQ-lcGXcbei.js} +1 -1
- package/viewer/dist/assets/{diagram-5GNKFQAL-CdFUltct.js → diagram-5GNKFQAL-Cj3-lPUt.js} +1 -1
- package/viewer/dist/assets/{diagram-KO2AKTUF-DKBMUmIp.js → diagram-KO2AKTUF-CtXdRI4M.js} +1 -1
- package/viewer/dist/assets/{diagram-LMA3HP47-aj0REjiQ.js → diagram-LMA3HP47-CIyLAzX7.js} +1 -1
- package/viewer/dist/assets/{diagram-OG6HWLK6-Bqtmu-Nc.js → diagram-OG6HWLK6-BwF4bQCv.js} +1 -1
- package/viewer/dist/assets/{dist-CkpbBMm1.js → dist-DzXuQd4N.js} +1 -1
- package/viewer/dist/assets/{erDiagram-TEJ5UH35-_EpsTRRO.js → erDiagram-TEJ5UH35-DjwFw9yq.js} +1 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-BlKFmSzA.js +1 -0
- package/viewer/dist/assets/{flowDiagram-I6XJVG4X-ChGqvvgX.js → flowDiagram-I6XJVG4X-CzXbf7vQ.js} +1 -1
- package/viewer/dist/assets/{ganttDiagram-6RSMTGT7-BjcMfREp.js → ganttDiagram-6RSMTGT7-BYcIhfmm.js} +1 -1
- package/viewer/dist/assets/{gitGraph-WXDBUCRP-DfwN1rQD.js → gitGraph-WXDBUCRP-H8XeJ9QB.js} +1 -1
- package/viewer/dist/assets/{gitGraphDiagram-PVQCEYII-ClI7X9vN.js → gitGraphDiagram-PVQCEYII-DtAPXKFK.js} +1 -1
- package/viewer/dist/assets/index-Bqs39Itl.css +2 -0
- package/viewer/dist/assets/index-DLPCG_Or.js +98 -0
- package/viewer/dist/assets/{info-J43DQDTF-CofQIoJ2.js → info-J43DQDTF-BQScorZ0.js} +1 -1
- package/viewer/dist/assets/{infoDiagram-5YYISTIA-D2BcKzo_.js → infoDiagram-5YYISTIA-Udl1omvH.js} +1 -1
- package/viewer/dist/assets/{ishikawaDiagram-YF4QCWOH-CXw1XdbZ.js → ishikawaDiagram-YF4QCWOH-VnfzT_hk.js} +1 -1
- package/viewer/dist/assets/{journeyDiagram-JHISSGLW-3Jhqf-KB.js → journeyDiagram-JHISSGLW-Btn4EVoD.js} +1 -1
- package/viewer/dist/assets/{kanban-definition-UN3LZRKU-ClSj7gDE.js → kanban-definition-UN3LZRKU-flOzWhPe.js} +1 -1
- package/viewer/dist/assets/{line-DeameHHf.js → line-DJJ9BLuY.js} +1 -1
- package/viewer/dist/assets/{linear-BDDnY--V.js → linear-C2SUy5ns.js} +1 -1
- package/viewer/dist/assets/{mermaid-parser.core-Cah87FZ1.js → mermaid-parser.core-xhSeYiZo.js} +2 -2
- package/viewer/dist/assets/{mermaid.core-lOcajBDU.js → mermaid.core-DmeSIitY.js} +3 -3
- package/viewer/dist/assets/{mindmap-definition-RKZ34NQL-CebNj7Vy.js → mindmap-definition-RKZ34NQL-CCJcMKx1.js} +1 -1
- package/viewer/dist/assets/{packet-YPE3B663-BlKTjmdv.js → packet-YPE3B663-DS8RscAk.js} +1 -1
- package/viewer/dist/assets/{pie-LRSECV5Y-CHjlSKV3.js → pie-LRSECV5Y-I2ZQhrNJ.js} +1 -1
- package/viewer/dist/assets/{pieDiagram-4H26LBE5-zWcP8Zx3.js → pieDiagram-4H26LBE5-BS5HMs4j.js} +1 -1
- package/viewer/dist/assets/{quadrantDiagram-W4KKPZXB-BSBO8usz.js → quadrantDiagram-W4KKPZXB-W4H8cw2_.js} +1 -1
- package/viewer/dist/assets/{radar-GUYGQ44K-BDmaTdGw.js → radar-GUYGQ44K-kf6QvSne.js} +1 -1
- package/viewer/dist/assets/{requirementDiagram-4Y6WPE33-C1wh6GaA.js → requirementDiagram-4Y6WPE33-DSbuDQGQ.js} +1 -1
- package/viewer/dist/assets/{sankeyDiagram-5OEKKPKP-L3mqPVcH.js → sankeyDiagram-5OEKKPKP-CH8Qr2vL.js} +1 -1
- package/viewer/dist/assets/{sequenceDiagram-3UESZ5HK-7vbfvJiz.js → sequenceDiagram-3UESZ5HK-EugVcdad.js} +1 -1
- package/viewer/dist/assets/{src-CslsSLVV.js → src-CM_9kdrU.js} +1 -1
- package/viewer/dist/assets/{stateDiagram-AJRCARHV-CfEgJmUy.js → stateDiagram-AJRCARHV-DiFl94Qy.js} +1 -1
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-BlJV8F13.js +1 -0
- package/viewer/dist/assets/{timeline-definition-PNZ67QCA-DMhMwUyE.js → timeline-definition-PNZ67QCA-B0KW9GUN.js} +1 -1
- package/viewer/dist/assets/{treeView-BLDUP644-aGCNEij1.js → treeView-BLDUP644-CAEY4mxq.js} +1 -1
- package/viewer/dist/assets/{treemap-LRROVOQU-DTgPn5R6.js → treemap-LRROVOQU-VVaEaTlU.js} +1 -1
- package/viewer/dist/assets/{vennDiagram-CIIHVFJN-BZygSp3k.js → vennDiagram-CIIHVFJN-BSLGNfJd.js} +1 -1
- package/viewer/dist/assets/{wardley-L42UT6IY-BSxi2k9r.js → wardley-L42UT6IY-C-5cXbAU.js} +1 -1
- package/viewer/dist/assets/{wardleyDiagram-YWT4CUSO-ZBjbz3Mj.js → wardleyDiagram-YWT4CUSO-COk4TVnQ.js} +1 -1
- package/viewer/dist/assets/{xychartDiagram-2RQKCTM6-VFJI2tv3.js → xychartDiagram-2RQKCTM6-CClGJLJp.js} +1 -1
- package/viewer/dist/index.html +2 -2
- package/dist/mcp/git.js.map +0 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-QBvJmJV9.js +0 -1
- package/viewer/dist/assets/channel-COWeTobO.js +0 -1
- package/viewer/dist/assets/chunk-QZHKN3VN-D7xg9F2p.js +0 -1
- package/viewer/dist/assets/chunk-WU5MYG2G-CWG30QUB.js +0 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-BsU5cU-N.js +0 -1
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-BsU5cU-N.js +0 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-BEUuQhV3.js +0 -1
- package/viewer/dist/assets/index-BRID9hB2.js +0 -98
- package/viewer/dist/assets/index-CI5JmpBT.css +0 -2
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-Dj6oCjNw.js +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -4,14 +4,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { isHandleShaped, isKnownHandle, typeForHandle } from '../core/handles.js';
|
|
7
|
+
import { isHandleShaped, isKnownHandle, TYPE_FOLDERS, typeForHandle, } from '../core/handles.js';
|
|
8
8
|
import { loadPlan } from '../core/indexer.js';
|
|
9
9
|
import { lintPlan } from '../core/lint.js';
|
|
10
10
|
import { resolvePlanDir } from '../core/resolve.js';
|
|
11
11
|
import { TYPE_NAMES } from '../core/types.js';
|
|
12
|
-
import { diffPlan, planDirty, planLog, writeSyncPoint } from '
|
|
12
|
+
import { diffPlan, planDirty, planLog, writeSyncPoint } from '../core/git.js';
|
|
13
13
|
import { searchCards } from './search.js';
|
|
14
14
|
import { applyCardPatch, createCardFile, deepMerge, relPathForHandle, reservedFieldKeys, updateCardFile, } from '../core/writer.js';
|
|
15
|
+
import { connectedRepoToFm, listConnectedRepos, readConnectedRepos, removeConnectedRepoEntry, resolveConnectedRepo, upsertConnectedRepo, } from '../core/repos.js';
|
|
15
16
|
const INSTRUCTIONS = `# Constellation MCP
|
|
16
17
|
|
|
17
18
|
The project's architecture plan lives as markdown files in a constellation/ folder.
|
|
@@ -27,6 +28,11 @@ Writes are validated: every write tool lints and returns issues for the file it
|
|
|
27
28
|
update_card patch.fields deep-merges (arrays replace, null deletes); body replaces.
|
|
28
29
|
Body-only updates never reformat frontmatter.
|
|
29
30
|
|
|
31
|
+
describe_type is the type reference, served by this server: call it with no args for the
|
|
32
|
+
catalog of all 17 card types, or with a type (e.g. describe_type PAGE) for that type's
|
|
33
|
+
frontmatter schema + a golden example. Consult it before authoring a type you haven't used
|
|
34
|
+
this session — you don't need the authoring skill loaded to get the fields right.
|
|
35
|
+
|
|
30
36
|
Change tracking is git: diff_plan reports per-card changes since the sync marker (or HEAD).
|
|
31
37
|
Never stamp dirty flags into cards.
|
|
32
38
|
|
|
@@ -47,6 +53,22 @@ macro view. ALWAYS verify the sub-agents' work yourself after they have all fini
|
|
|
47
53
|
each change against its cards and run the build/tests; never trust their reports alone — then
|
|
48
54
|
set the sync point once.
|
|
49
55
|
|
|
56
|
+
Plan-first when changing code: when asked to build a feature or change behavior in an area
|
|
57
|
+
this plan covers, do NOT edit code first. The plan you make leads with Constellation — read
|
|
58
|
+
the affected neighborhood (get_card / traverse / search, connected: "full"), then add or
|
|
59
|
+
update the cards so they describe the desired END STATE (work that isn't built yet is
|
|
60
|
+
status: planned), wiring every connection between the affected cards. Show that set of card
|
|
61
|
+
changes as the proposal; on approval, bring the CODE up to match via the sync loop above.
|
|
62
|
+
FINISH by reconciling — re-read the touched cards against the code, run check_integrity so
|
|
63
|
+
no affected card is left an orphan and every connection is set, bump status (planned →
|
|
64
|
+
building → built → verified), commit, and set_sync_point. In plan mode the write tools are
|
|
65
|
+
unavailable by design (the read tools — get_card, list_cards, search, traverse,
|
|
66
|
+
describe_type, check_integrity, diff_plan, plan_log — are marked read-only and stay
|
|
67
|
+
available), so spend plan mode READING: pull in as much of the relevant plan as you can
|
|
68
|
+
(traverse from the entry points, connected: "full") to build a strong model of the project
|
|
69
|
+
fast, fold the intended card edits into the plan you present, and write them to
|
|
70
|
+
Constellation first, before any code, once the user approves.
|
|
71
|
+
|
|
50
72
|
For migrations or large scaffolds, use create_cards and add_connections (batched, one
|
|
51
73
|
lint pass) instead of many single calls — connections between cards in the same batch
|
|
52
74
|
resolve without transient "does not resolve" errors. A card is created even when issues
|
|
@@ -74,7 +96,7 @@ and confirm it with the user (it's plan.md's name: and the viewer's title; chang
|
|
|
74
96
|
anytime via update_card on PLAN-PROJECT); then follow the DATA (DB → DATATYPE →
|
|
75
97
|
API → PAGE, with FLOW/STATE for paths and lifecycles) and the USER (ROLE + auth FLOW
|
|
76
98
|
first, then PAGE/COMPONENT and key journeys) and the EDGES (EXTERNAL/JOB/EVENT); then zoom
|
|
77
|
-
into central or complex areas. Read before you ask — ask the user only for intent,
|
|
99
|
+
into central or complex areas. For a non-trivial plan, after the macro pass act as the ORCHESTRATOR here too: partition the build into independent neighborhoods (the DATA, the USER, the EDGES) and fan out a sub-agent per neighborhood in parallel — but assign each card to exactly one agent (two agents calling update_card on the same card race, later write wins) and have them return card specs you write via batched create_cards/add_connections, then verify each agent's work and lint once; use one agent for a small plan. Read before you ask — ask the user only for intent,
|
|
78
100
|
priorities, and history the code can't reveal. Then find gaps IN THE PLAN: step back and
|
|
79
101
|
hunt blind spots the user may not have considered — missing unhappy paths and lifecycle
|
|
80
102
|
states, auth/permission gaps, and cross-cutting concerns plans forget (security, privacy,
|
|
@@ -84,6 +106,21 @@ short, prioritized list of recommendations and ask about the judgment calls. For
|
|
|
84
106
|
method use the bootstrap_plan or audit_plan prompt. Status is planned → building → built →
|
|
85
107
|
verified; verify only against real code.
|
|
86
108
|
|
|
109
|
+
Connected repos (multi-repo work): a project can declare sibling repos in PLAN-PROJECT
|
|
110
|
+
connected_repos (name + on-disk path + description) — list_connected_repos shows them with
|
|
111
|
+
reachability, add_connected_repo links one (reciprocate:true also writes the reverse link
|
|
112
|
+
into the other repo, with the user's OK). These are REPO-level links only: cards never connect
|
|
113
|
+
across repos, and each repo's plan stays self-contained and lints alone. To work on a connected
|
|
114
|
+
repo, pass repo: "<name>" to any read or write tool (get_card, search, traverse, update_card,
|
|
115
|
+
…) and it reads/writes THAT repo's plan; omit repo for the current one. To answer a question
|
|
116
|
+
about a connected repo, first read its plan with repo:; if you need the real code (or its plan
|
|
117
|
+
can't answer), spawn a sub-agent scoped to that repo's path to investigate and report back —
|
|
118
|
+
and if its plan had the gap, fill it. For one change spanning repos: examine each repo's area,
|
|
119
|
+
write the per-repo card updates with repo: set on EVERY write (never omit it cross-repo, or the
|
|
120
|
+
write lands in the wrong place), then fan out a per-repo implementer sub-agent (each runs in
|
|
121
|
+
plain single-repo mode, blind to the others) and reconcile + set_sync_point per repo. Inside a
|
|
122
|
+
single repo, repo is unnecessary and everything behaves exactly as before.
|
|
123
|
+
|
|
87
124
|
To let the user browse the plan visually, start_viewer launches a local web server that
|
|
88
125
|
renders the plan as an editable site and returns its URL (it scans forward from port 4747
|
|
89
126
|
for a free port, so always read the actual port from the response). ALWAYS post that URL
|
|
@@ -92,7 +129,38 @@ The viewer runs until stop_viewer or until this server process exits.`;
|
|
|
92
129
|
// The full plan-from-code playbook lives in one file (skill/methodology.md), shared by the
|
|
93
130
|
// skill and the MCP prompts so the two can't drift. Resolve it relative to this module:
|
|
94
131
|
// from dist/mcp/server.js (or src/mcp/server.ts) '../..' is the package/repo root.
|
|
95
|
-
|
|
132
|
+
// Package root: from dist/mcp/server.js (or src/mcp/server.ts) '../..' is the
|
|
133
|
+
// package/repo root. The skill folder, type docs, and JSON schemas all ship here.
|
|
134
|
+
const PKG_ROOT = fileURLToPath(new URL('../..', import.meta.url));
|
|
135
|
+
const METHODOLOGY_PATH = path.join(PKG_ROOT, 'skill', 'methodology.md');
|
|
136
|
+
const SCHEMAS_DIR = path.join(PKG_ROOT, 'schemas');
|
|
137
|
+
const TYPE_DOCS_DIR = path.join(PKG_ROOT, 'skill', 'types');
|
|
138
|
+
/** Parsed JSON Schema for a type's frontmatter, or null if it can't be read. */
|
|
139
|
+
async function readTypeSchema(folder) {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(await readFile(path.join(SCHEMAS_DIR, `${folder}.json`), 'utf8'));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** The authoring reference (field table + golden example) for a type, or null. */
|
|
148
|
+
async function readTypeDoc(folder) {
|
|
149
|
+
try {
|
|
150
|
+
return await readFile(path.join(TYPE_DOCS_DIR, `${folder}.md`), 'utf8');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** First sentence of a schema's top-level description — its one-line purpose. */
|
|
157
|
+
function schemaPurpose(schema) {
|
|
158
|
+
const desc = schema && typeof schema === 'object' && 'description' in schema
|
|
159
|
+
? String(schema.description ?? '')
|
|
160
|
+
: '';
|
|
161
|
+
const match = desc.match(/^.*?\.(?:\s|$)/);
|
|
162
|
+
return (match ? match[0] : desc).trim();
|
|
163
|
+
}
|
|
96
164
|
let methodologyCache = null;
|
|
97
165
|
async function methodologyText() {
|
|
98
166
|
if (methodologyCache === null) {
|
|
@@ -120,6 +188,25 @@ function fail(code, message) {
|
|
|
120
188
|
isError: true,
|
|
121
189
|
};
|
|
122
190
|
}
|
|
191
|
+
async function openUrl(url) {
|
|
192
|
+
try {
|
|
193
|
+
const { spawn } = await import('node:child_process');
|
|
194
|
+
const child = process.platform === 'darwin'
|
|
195
|
+
? spawn('open', [url], { stdio: 'ignore', detached: true })
|
|
196
|
+
: process.platform === 'win32'
|
|
197
|
+
? spawn('cmd', ['/c', 'start', '', url], {
|
|
198
|
+
stdio: 'ignore',
|
|
199
|
+
detached: true,
|
|
200
|
+
windowsHide: true,
|
|
201
|
+
})
|
|
202
|
+
: spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
203
|
+
child.on('error', () => { });
|
|
204
|
+
child.unref();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Opening the browser is best-effort; callers still receive the URL.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
123
210
|
function summary(card) {
|
|
124
211
|
return {
|
|
125
212
|
handle: card.handle,
|
|
@@ -147,8 +234,12 @@ function issuesForFile(issues, relPath) {
|
|
|
147
234
|
const detailSchema = z.enum(['none', 'summary', 'full']);
|
|
148
235
|
const typeSchema = z.enum(TYPE_NAMES);
|
|
149
236
|
const statusSchema = z.enum(['planned', 'building', 'built', 'verified']);
|
|
237
|
+
const repoSchema = z
|
|
238
|
+
.string()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe('Target a connected repo by its connected_repos name (or a path). Omit to use the current repo.');
|
|
150
241
|
export function buildServer(options = {}) {
|
|
151
|
-
const server = new McpServer({ name: 'constellation', version: '0.
|
|
242
|
+
const server = new McpServer({ name: 'constellation', version: '0.2.1' }, { instructions: INSTRUCTIONS });
|
|
152
243
|
server.registerPrompt('bootstrap_plan', {
|
|
153
244
|
title: 'Bootstrap a plan from the codebase',
|
|
154
245
|
description: 'Analyze this repository macro→micro and build (or extend) the Constellation plan: follow the data and the user/auth journeys, capture flows, diagrams, and state machines, ask the user what the code cannot answer, and flag gaps.',
|
|
@@ -182,18 +273,48 @@ export function buildServer(options = {}) {
|
|
|
182
273
|
}
|
|
183
274
|
// A single web viewer owned by this server process; null until start_viewer runs.
|
|
184
275
|
let viewer = null;
|
|
185
|
-
|
|
276
|
+
const noPlanFound = () => fail('NO_PLAN_FOUND', `No constellation/ folder found by walking up from ${process.cwd()}. This MCP ` +
|
|
277
|
+
`server uses its own working directory — if that isn't your repo, set "cwd" to ` +
|
|
278
|
+
'the repo root in your MCP client config. Otherwise call init_plan (optionally ' +
|
|
279
|
+
'with { path } pointing at the repo root), or run `constellation init`.');
|
|
280
|
+
/**
|
|
281
|
+
* Resolve which plan a call targets. With no `repo`, the home plan (walk up
|
|
282
|
+
* from cwd); with `repo`, a connected repo selected by its connected_repos
|
|
283
|
+
* name or by a path. Returns the resolved root or a ready-to-return error.
|
|
284
|
+
*/
|
|
285
|
+
async function resolveTarget(repo) {
|
|
286
|
+
const home = await planRoot();
|
|
287
|
+
if (!repo) {
|
|
288
|
+
return home ? { root: home } : { error: noPlanFound() };
|
|
289
|
+
}
|
|
290
|
+
if (home) {
|
|
291
|
+
const resolved = await resolveConnectedRepo(home, repo);
|
|
292
|
+
if (resolved)
|
|
293
|
+
return { root: resolved.root };
|
|
294
|
+
const names = (await readConnectedRepos(home)).map((r) => r.name);
|
|
295
|
+
return {
|
|
296
|
+
error: fail('UNKNOWN_REPO', `Connected repo "${repo}" not found. Pass a connected_repos name ` +
|
|
297
|
+
`(${names.length ? names.join(', ') : 'none declared — add one with add_connected_repo'}) ` +
|
|
298
|
+
'or a path to a repo that has a constellation/ plan.'),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// No home plan: a name can't be looked up, but a path can still resolve.
|
|
302
|
+
const byPath = await resolvePlanDir(path.isAbsolute(repo) ? repo : path.resolve(process.cwd(), repo));
|
|
303
|
+
if (byPath)
|
|
304
|
+
return { root: byPath };
|
|
305
|
+
return {
|
|
306
|
+
error: fail('UNKNOWN_REPO', `No plan found for repo "${repo}". With no plan in the current directory, pass a ` +
|
|
307
|
+
'path to a repo that has a constellation/ plan.'),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** Wrap a handler with plan resolution and error reporting; `repo` selects a connected repo. */
|
|
186
311
|
function withPlan(handler) {
|
|
187
312
|
return async (args) => {
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
191
|
-
`server uses its own working directory — if that isn't your repo, set "cwd" to ` +
|
|
192
|
-
'the repo root in your MCP client config. Otherwise call init_plan (optionally ' +
|
|
193
|
-
'with { path } pointing at the repo root), or run `constellation init`.');
|
|
194
|
-
}
|
|
313
|
+
const target = await resolveTarget(args?.repo);
|
|
314
|
+
if ('error' in target)
|
|
315
|
+
return target.error;
|
|
195
316
|
try {
|
|
196
|
-
return await handler(root, args);
|
|
317
|
+
return await handler(target.root, args);
|
|
197
318
|
}
|
|
198
319
|
catch (err) {
|
|
199
320
|
return fail('INTERNAL', err instanceof Error ? err.message : String(err));
|
|
@@ -227,10 +348,12 @@ export function buildServer(options = {}) {
|
|
|
227
348
|
}
|
|
228
349
|
});
|
|
229
350
|
server.registerTool('get_card', {
|
|
351
|
+
annotations: { readOnlyHint: true },
|
|
230
352
|
description: 'Fetch one card by handle, optionally with all connected cards hydrated. connected: "full" returns the complete frontmatter and body of every connected card — use it when about to work on an area.',
|
|
231
353
|
inputSchema: {
|
|
232
354
|
handle: z.string(),
|
|
233
355
|
connected: detailSchema.optional().describe('default: summary'),
|
|
356
|
+
repo: repoSchema,
|
|
234
357
|
},
|
|
235
358
|
}, withPlan(async (root, { handle, connected }) => {
|
|
236
359
|
const index = await loadPlan(root);
|
|
@@ -243,6 +366,7 @@ export function buildServer(options = {}) {
|
|
|
243
366
|
});
|
|
244
367
|
}));
|
|
245
368
|
server.registerTool('list_cards', {
|
|
369
|
+
annotations: { readOnlyHint: true },
|
|
246
370
|
description: 'Catalog of cards filtered by type, kind, status, and/or connectedness. connected:false returns orphans (cards with zero connections). Returns summaries (handle, type, kind, name, status).',
|
|
247
371
|
inputSchema: {
|
|
248
372
|
types: z.array(typeSchema).optional(),
|
|
@@ -253,6 +377,7 @@ export function buildServer(options = {}) {
|
|
|
253
377
|
.optional()
|
|
254
378
|
.describe('false = orphans only; true = connected only'),
|
|
255
379
|
limit: z.number().int().min(1).max(500).optional(),
|
|
380
|
+
repo: repoSchema,
|
|
256
381
|
},
|
|
257
382
|
}, withPlan(async (root, { types, kind, status, connected, limit }) => {
|
|
258
383
|
const index = await loadPlan(root);
|
|
@@ -270,12 +395,14 @@ export function buildServer(options = {}) {
|
|
|
270
395
|
});
|
|
271
396
|
}));
|
|
272
397
|
server.registerTool('search', {
|
|
398
|
+
annotations: { readOnlyHint: true },
|
|
273
399
|
description: 'Scored full-text search over handles, names, kinds, and bodies. Set connected: "full" to hydrate each match with the complete content of its connected cards — fuzzy query to working context in one call.',
|
|
274
400
|
inputSchema: {
|
|
275
401
|
q: z.string(),
|
|
276
402
|
types: z.array(typeSchema).optional(),
|
|
277
403
|
limit: z.number().int().min(1).max(100).optional(),
|
|
278
404
|
connected: detailSchema.optional().describe('default: none'),
|
|
405
|
+
repo: repoSchema,
|
|
279
406
|
},
|
|
280
407
|
}, withPlan(async (root, { q, types, limit, connected }) => {
|
|
281
408
|
const index = await loadPlan(root);
|
|
@@ -290,12 +417,14 @@ export function buildServer(options = {}) {
|
|
|
290
417
|
});
|
|
291
418
|
}));
|
|
292
419
|
server.registerTool('traverse', {
|
|
420
|
+
annotations: { readOnlyHint: true },
|
|
293
421
|
description: 'Breadth-first walk of the connection graph from one or more starting handles. Seed it with diff_plan output for impact analysis. detail: "full" includes frontmatter and body of every reached card.',
|
|
294
422
|
inputSchema: {
|
|
295
423
|
start: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
296
424
|
depth: z.number().int().min(0).max(5).optional().describe('default: 2'),
|
|
297
425
|
types: z.array(typeSchema).optional(),
|
|
298
426
|
detail: z.enum(['summary', 'full']).optional().describe('default: summary'),
|
|
427
|
+
repo: repoSchema,
|
|
299
428
|
},
|
|
300
429
|
}, withPlan(async (root, { start, depth, types, detail }) => {
|
|
301
430
|
const index = await loadPlan(root);
|
|
@@ -339,8 +468,41 @@ export function buildServer(options = {}) {
|
|
|
339
468
|
const connections = index.connections.filter((c) => distance.has(c.a) && distance.has(c.b));
|
|
340
469
|
return ok({ cards, connections, not_found: missing });
|
|
341
470
|
}));
|
|
471
|
+
server.registerTool('describe_type', {
|
|
472
|
+
annotations: { readOnlyHint: true },
|
|
473
|
+
description: 'The card-type reference, served straight from this package. Call with no args for the catalog — all 17 types with their prefix, folder, and one-line purpose. Call with a type for everything needed to author one: the frontmatter JSON Schema (fields, which are required, descriptions) plus the golden example and authoring guidance. Use it before writing a card of a type you have not authored this session — it is the contract create_card/create_cards/update_card validate against (W002/W003), so you do not need the authoring skill loaded to get the fields right.',
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: typeSchema.optional().describe('omit for the full catalog'),
|
|
476
|
+
},
|
|
477
|
+
}, async ({ type }) => {
|
|
478
|
+
if (!type) {
|
|
479
|
+
const types = await Promise.all(TYPE_NAMES.map(async (t) => {
|
|
480
|
+
const folder = TYPE_FOLDERS[t];
|
|
481
|
+
return {
|
|
482
|
+
type: t,
|
|
483
|
+
prefix: `${t}-`,
|
|
484
|
+
folder,
|
|
485
|
+
purpose: schemaPurpose(await readTypeSchema(folder)),
|
|
486
|
+
};
|
|
487
|
+
}));
|
|
488
|
+
return ok({ types });
|
|
489
|
+
}
|
|
490
|
+
const folder = TYPE_FOLDERS[type];
|
|
491
|
+
const [schema, reference] = await Promise.all([
|
|
492
|
+
readTypeSchema(folder),
|
|
493
|
+
readTypeDoc(folder),
|
|
494
|
+
]);
|
|
495
|
+
return ok({
|
|
496
|
+
type,
|
|
497
|
+
prefix: `${type}-`,
|
|
498
|
+
folder,
|
|
499
|
+
reserved: ['name', 'kind', 'status', 'connections'],
|
|
500
|
+
schema,
|
|
501
|
+
reference,
|
|
502
|
+
});
|
|
503
|
+
});
|
|
342
504
|
server.registerTool('create_card', {
|
|
343
|
-
description: 'Create a new card. The handle determines type and file location. fields = type-specific frontmatter
|
|
505
|
+
description: 'Create a new card. The handle determines type and file location. fields = type-specific frontmatter — call describe_type(type) first for its field schema and a golden example; body = markdown. The card IS created even when issues are returned — issues are the current lint state, not a failure. Set validate:false to skip linting during bulk import (then run check_integrity once at the end). For many cards at once, prefer create_cards.',
|
|
344
506
|
inputSchema: {
|
|
345
507
|
handle: z.string(),
|
|
346
508
|
name: z.string().optional(),
|
|
@@ -353,6 +515,7 @@ export function buildServer(options = {}) {
|
|
|
353
515
|
.boolean()
|
|
354
516
|
.optional()
|
|
355
517
|
.describe('default true; false skips lint and returns no issues'),
|
|
518
|
+
repo: repoSchema,
|
|
356
519
|
},
|
|
357
520
|
}, withPlan(async (root, args) => {
|
|
358
521
|
const handle = args.handle.toUpperCase();
|
|
@@ -411,6 +574,7 @@ export function buildServer(options = {}) {
|
|
|
411
574
|
}))
|
|
412
575
|
.min(1)
|
|
413
576
|
.max(500),
|
|
577
|
+
repo: repoSchema,
|
|
414
578
|
},
|
|
415
579
|
}, withPlan(async (root, { cards }) => {
|
|
416
580
|
const index = await loadPlan(root);
|
|
@@ -479,6 +643,7 @@ export function buildServer(options = {}) {
|
|
|
479
643
|
.array(z.object({ from: z.string(), to: z.string() }))
|
|
480
644
|
.min(1)
|
|
481
645
|
.max(1000),
|
|
646
|
+
repo: repoSchema,
|
|
482
647
|
},
|
|
483
648
|
}, withPlan(async (root, { connections }) => {
|
|
484
649
|
const index = await loadPlan(root);
|
|
@@ -509,7 +674,7 @@ export function buildServer(options = {}) {
|
|
|
509
674
|
for (const [src, targets] of additions) {
|
|
510
675
|
const card = index.cards.get(src);
|
|
511
676
|
const existingList = Array.isArray(card.frontmatter.connections)
|
|
512
|
-
? card.frontmatter.connections
|
|
677
|
+
? card.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
513
678
|
: [];
|
|
514
679
|
const merged = [...new Set([...existingList, ...targets])];
|
|
515
680
|
const frontmatter = applyCardPatch(card.frontmatter, { connections: merged });
|
|
@@ -534,6 +699,7 @@ export function buildServer(options = {}) {
|
|
|
534
699
|
})
|
|
535
700
|
.optional(),
|
|
536
701
|
body: z.string().optional(),
|
|
702
|
+
repo: repoSchema,
|
|
537
703
|
},
|
|
538
704
|
}, withPlan(async (root, { handle, patch, body }) => {
|
|
539
705
|
const index = await loadPlan(root);
|
|
@@ -560,7 +726,7 @@ export function buildServer(options = {}) {
|
|
|
560
726
|
}));
|
|
561
727
|
server.registerTool('delete_card', {
|
|
562
728
|
description: 'Delete a card file. Returns the handles that referenced it (their references are now dangling) plus resulting lint issues.',
|
|
563
|
-
inputSchema: { handle: z.string() },
|
|
729
|
+
inputSchema: { handle: z.string(), repo: repoSchema },
|
|
564
730
|
}, withPlan(async (root, { handle }) => {
|
|
565
731
|
const index = await loadPlan(root);
|
|
566
732
|
const card = index.cards.get(handle.toUpperCase());
|
|
@@ -580,7 +746,7 @@ export function buildServer(options = {}) {
|
|
|
580
746
|
}));
|
|
581
747
|
server.registerTool('add_connection', {
|
|
582
748
|
description: 'Connect two cards by appending `to` to `from`’s connections list. No-op if they are already connected through any source.',
|
|
583
|
-
inputSchema: { from: z.string(), to: z.string() },
|
|
749
|
+
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
584
750
|
}, withPlan(async (root, args) => {
|
|
585
751
|
const index = await loadPlan(root);
|
|
586
752
|
const from = index.cards.get(args.from.toUpperCase());
|
|
@@ -607,7 +773,7 @@ export function buildServer(options = {}) {
|
|
|
607
773
|
}));
|
|
608
774
|
server.registerTool('remove_connection', {
|
|
609
775
|
description: 'Remove a connection by deleting it from either card’s connections list. Reports if the cards remain connected through other sources (frontmatter fields, body links, mermaid) that must be edited manually.',
|
|
610
|
-
inputSchema: { a: z.string(), b: z.string() },
|
|
776
|
+
inputSchema: { a: z.string(), b: z.string(), repo: repoSchema },
|
|
611
777
|
}, withPlan(async (root, args) => {
|
|
612
778
|
const index = await loadPlan(root);
|
|
613
779
|
const cardA = index.cards.get(args.a.toUpperCase());
|
|
@@ -659,9 +825,130 @@ export function buildServer(options = {}) {
|
|
|
659
825
|
issues: lint.issues.filter((i) => touched.has(i.file)),
|
|
660
826
|
});
|
|
661
827
|
}));
|
|
828
|
+
server.registerTool('list_connected_repos', {
|
|
829
|
+
annotations: { readOnlyHint: true },
|
|
830
|
+
description: 'List the sibling repos declared on PLAN-PROJECT connected_repos, each with its path, description, and whether it is reachable on this machine. Use a name as the `repo` selector on other tools to read or write that repo. Repo-level links only — not card connections.',
|
|
831
|
+
inputSchema: { repo: repoSchema },
|
|
832
|
+
}, withPlan(async (root) => {
|
|
833
|
+
const repos = await listConnectedRepos(root);
|
|
834
|
+
return ok({
|
|
835
|
+
connected_repos: repos.map((r) => ({
|
|
836
|
+
name: r.name,
|
|
837
|
+
path: r.path,
|
|
838
|
+
description: r.description ?? null,
|
|
839
|
+
reachable: r.reachable,
|
|
840
|
+
})),
|
|
841
|
+
});
|
|
842
|
+
}));
|
|
843
|
+
server.registerTool('add_connected_repo', {
|
|
844
|
+
description: 'Declare a sibling repo on PLAN-PROJECT connected_repos (a repo-level link, not a card connection). name is the lowercase `repo` selector; path is relative to this repo root (e.g. ../pyramid-server). reciprocate:true also writes the reverse link into the target repo — only do this with the user’s OK, since it edits the other repo. Upserts by name.',
|
|
845
|
+
inputSchema: {
|
|
846
|
+
name: z
|
|
847
|
+
.string()
|
|
848
|
+
.describe('lowercase id used as the `repo` selector (e.g. pyramid-server)'),
|
|
849
|
+
path: z
|
|
850
|
+
.string()
|
|
851
|
+
.describe('path to the connected repo root, relative to this repo (e.g. ../pyramid-server)'),
|
|
852
|
+
description: z.string().optional(),
|
|
853
|
+
reciprocate: z
|
|
854
|
+
.boolean()
|
|
855
|
+
.optional()
|
|
856
|
+
.describe('also add the reverse link into the target repo (writes there). default false'),
|
|
857
|
+
reverse_description: z
|
|
858
|
+
.string()
|
|
859
|
+
.optional()
|
|
860
|
+
.describe('description for the reverse link when reciprocate is set; defaults to none'),
|
|
861
|
+
repo: repoSchema,
|
|
862
|
+
},
|
|
863
|
+
}, withPlan(async (root, args) => {
|
|
864
|
+
const { name, path: repoPath, description, reciprocate, reverse_description } = args;
|
|
865
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
866
|
+
return fail('INVALID_NAME', `"${name}" is not a valid repo name (lowercase letters, digits, and hyphens).`);
|
|
867
|
+
}
|
|
868
|
+
const index = await loadPlan(root);
|
|
869
|
+
const planCard = index.cards.get('PLAN-PROJECT');
|
|
870
|
+
if (!planCard) {
|
|
871
|
+
return fail('NO_PLAN_PROJECT', 'No plan.md (PLAN-PROJECT) at the plan root to record connected_repos on.');
|
|
872
|
+
}
|
|
873
|
+
const entry = { name, path: repoPath, description };
|
|
874
|
+
const next = upsertConnectedRepo(await readConnectedRepos(root), entry);
|
|
875
|
+
const frontmatter = applyCardPatch(planCard.frontmatter, {
|
|
876
|
+
fields: { connected_repos: next.map(connectedRepoToFm) },
|
|
877
|
+
});
|
|
878
|
+
await updateCardFile(planCard.filePath, { frontmatter });
|
|
879
|
+
let reciprocated;
|
|
880
|
+
if (reciprocate) {
|
|
881
|
+
const target = await resolveConnectedRepo(root, repoPath);
|
|
882
|
+
if (!target) {
|
|
883
|
+
reciprocated = { ok: false, reason: 'target repo not reachable (no plan found at path)' };
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
const targetPlan = (await loadPlan(target.root)).cards.get('PLAN-PROJECT');
|
|
887
|
+
if (!targetPlan) {
|
|
888
|
+
reciprocated = { ok: false, reason: 'target repo has no plan.md (PLAN-PROJECT)' };
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
const homeRepoRoot = path.dirname(root);
|
|
892
|
+
const reverseName = path
|
|
893
|
+
.basename(homeRepoRoot)
|
|
894
|
+
.toLowerCase()
|
|
895
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
896
|
+
.replace(/^-+|-+$/g, '') || 'home';
|
|
897
|
+
const reverseEntry = {
|
|
898
|
+
name: reverseName,
|
|
899
|
+
path: path.relative(target.repoRoot, homeRepoRoot) || '.',
|
|
900
|
+
description: reverse_description,
|
|
901
|
+
};
|
|
902
|
+
const targetNext = upsertConnectedRepo(await readConnectedRepos(target.root), reverseEntry);
|
|
903
|
+
await updateCardFile(targetPlan.filePath, {
|
|
904
|
+
frontmatter: applyCardPatch(targetPlan.frontmatter, {
|
|
905
|
+
fields: { connected_repos: targetNext.map(connectedRepoToFm) },
|
|
906
|
+
}),
|
|
907
|
+
});
|
|
908
|
+
reciprocated = {
|
|
909
|
+
ok: true,
|
|
910
|
+
repo_root: target.repoRoot,
|
|
911
|
+
entry: connectedRepoToFm(reverseEntry),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const lint = await lintPlan(root);
|
|
917
|
+
return ok({
|
|
918
|
+
connected_repos: next.map(connectedRepoToFm),
|
|
919
|
+
reciprocated,
|
|
920
|
+
issues: issuesForFile(lint.issues, planCard.relPath),
|
|
921
|
+
});
|
|
922
|
+
}));
|
|
923
|
+
server.registerTool('remove_connected_repo', {
|
|
924
|
+
description: 'Remove a sibling repo from PLAN-PROJECT connected_repos by name. Does not touch the other repo.',
|
|
925
|
+
inputSchema: { name: z.string(), repo: repoSchema },
|
|
926
|
+
}, withPlan(async (root, { name }) => {
|
|
927
|
+
const planCard = (await loadPlan(root)).cards.get('PLAN-PROJECT');
|
|
928
|
+
if (!planCard) {
|
|
929
|
+
return fail('NO_PLAN_PROJECT', 'No plan.md (PLAN-PROJECT) at the plan root.');
|
|
930
|
+
}
|
|
931
|
+
const existing = await readConnectedRepos(root);
|
|
932
|
+
if (!existing.some((r) => r.name === name)) {
|
|
933
|
+
return ok({ removed: false, connected_repos: existing.map(connectedRepoToFm) });
|
|
934
|
+
}
|
|
935
|
+
const next = removeConnectedRepoEntry(existing, name);
|
|
936
|
+
await updateCardFile(planCard.filePath, {
|
|
937
|
+
frontmatter: applyCardPatch(planCard.frontmatter, {
|
|
938
|
+
fields: { connected_repos: next.length > 0 ? next.map(connectedRepoToFm) : null },
|
|
939
|
+
}),
|
|
940
|
+
});
|
|
941
|
+
const lint = await lintPlan(root);
|
|
942
|
+
return ok({
|
|
943
|
+
removed: true,
|
|
944
|
+
connected_repos: next.map(connectedRepoToFm),
|
|
945
|
+
issues: issuesForFile(lint.issues, planCard.relPath),
|
|
946
|
+
});
|
|
947
|
+
}));
|
|
662
948
|
server.registerTool('check_integrity', {
|
|
949
|
+
annotations: { readOnlyHint: true },
|
|
663
950
|
description: 'Lint the whole plan: broken handles, dangling references, wrong folders, schema violations, plus orphans (cards with zero connections). Errors break the graph; warnings and orphans are quality signals.',
|
|
664
|
-
inputSchema: {},
|
|
951
|
+
inputSchema: { repo: repoSchema },
|
|
665
952
|
}, withPlan(async (root) => {
|
|
666
953
|
const lint = await lintPlan(root);
|
|
667
954
|
const orphans = [...lint.index.cards.keys()]
|
|
@@ -676,19 +963,23 @@ export function buildServer(options = {}) {
|
|
|
676
963
|
});
|
|
677
964
|
}));
|
|
678
965
|
server.registerTool('diff_plan', {
|
|
966
|
+
annotations: { readOnlyHint: true },
|
|
679
967
|
description: 'Per-card plan changes from git. base defaults to the sync marker (constellation/.sync.json) or HEAD; head defaults to the working tree. Returns added/modified/removed cards with changed frontmatter keys. Feed the handles to traverse for blast radius.',
|
|
680
968
|
inputSchema: {
|
|
681
969
|
base: z.string().optional(),
|
|
682
970
|
head: z.string().optional(),
|
|
971
|
+
repo: repoSchema,
|
|
683
972
|
},
|
|
684
973
|
}, withPlan(async (root, { base, head }) => {
|
|
685
974
|
return ok(await diffPlan(root, base, head));
|
|
686
975
|
}));
|
|
687
976
|
server.registerTool('plan_log', {
|
|
977
|
+
annotations: { readOnlyHint: true },
|
|
688
978
|
description: 'Git history of one card: the commits that touched its file.',
|
|
689
979
|
inputSchema: {
|
|
690
980
|
handle: z.string(),
|
|
691
981
|
limit: z.number().int().min(1).max(100).optional(),
|
|
982
|
+
repo: repoSchema,
|
|
692
983
|
},
|
|
693
984
|
}, withPlan(async (root, { handle, limit }) => {
|
|
694
985
|
const index = await loadPlan(root);
|
|
@@ -701,7 +992,7 @@ export function buildServer(options = {}) {
|
|
|
701
992
|
}));
|
|
702
993
|
server.registerTool('set_sync_point', {
|
|
703
994
|
description: 'Record that code has been reconciled with the plan as of a commit (default HEAD). diff_plan uses this marker as its default base. Commit the plan first: if constellation/ has uncommitted changes, the marker points at a commit that lacks them and the response includes a warning.',
|
|
704
|
-
inputSchema: { sha: z.string().optional() },
|
|
995
|
+
inputSchema: { sha: z.string().optional(), repo: repoSchema },
|
|
705
996
|
}, withPlan(async (root, { sha }) => {
|
|
706
997
|
const point = await writeSyncPoint(root, sha);
|
|
707
998
|
const dirty = await planDirty(root);
|
|
@@ -763,13 +1054,7 @@ export function buildServer(options = {}) {
|
|
|
763
1054
|
const url = `http://localhost:${running.port}/`;
|
|
764
1055
|
viewer = { server: running, planRoot: root, url };
|
|
765
1056
|
if (open) {
|
|
766
|
-
|
|
767
|
-
const cmd = process.platform === 'darwin'
|
|
768
|
-
? 'open'
|
|
769
|
-
: process.platform === 'win32'
|
|
770
|
-
? 'start'
|
|
771
|
-
: 'xdg-open';
|
|
772
|
-
spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
|
|
1057
|
+
await openUrl(url);
|
|
773
1058
|
}
|
|
774
1059
|
return ok({ url, port: running.port, plan_root: root, editable: !(readonly ?? false) });
|
|
775
1060
|
}));
|