@magic-spells/constellation 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/cli/index.js +62 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/update-check.d.ts +6 -0
- package/dist/cli/update-check.js +70 -0
- package/dist/cli/update-check.js.map +1 -0
- 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/resolve.js +10 -1
- package/dist/core/resolve.js.map +1 -1
- package/dist/core/scaffold.d.ts +16 -3
- package/dist/core/scaffold.js +30 -7
- package/dist/core/scaffold.js.map +1 -1
- 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 +434 -38
- 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 +56 -7
- package/examples/constellation/plan.md +1 -1
- package/package.json +3 -1
- package/schemas/plan.json +27 -0
- package/skill/SKILL.md +131 -1
- package/skill/methodology.md +228 -0
- package/skill/types/plan.md +19 -0
- package/viewer/dist/assets/{arc-Kj6pF3JI.js → arc-CMMRgIq9.js} +1 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-Cbb-syEI.js +1 -0
- package/viewer/dist/assets/{architectureDiagram-3BPJPVTR-C5bZdErB.js → architectureDiagram-3BPJPVTR-CB5p_qlN.js} +1 -1
- package/viewer/dist/assets/{blockDiagram-GPEHLZMM-C1Q6l6fE.js → blockDiagram-GPEHLZMM-DpnMj_e6.js} +1 -1
- package/viewer/dist/assets/{c4Diagram-AAUBKEIU-BmM6Tmtq.js → c4Diagram-AAUBKEIU-f91ngVN8.js} +1 -1
- package/viewer/dist/assets/channel-C2bkEQHQ.js +1 -0
- package/viewer/dist/assets/{chunk-2J33WTMH-z09tLTpZ.js → chunk-2J33WTMH-y7Pz5tMO.js} +1 -1
- package/viewer/dist/assets/{chunk-3OPIFGDE-BynpXh1r.js → chunk-3OPIFGDE-Ir_ghIUQ.js} +1 -1
- package/viewer/dist/assets/{chunk-4BX2VUAB-CDOVuPyG.js → chunk-4BX2VUAB-BTPyM1ux.js} +1 -1
- package/viewer/dist/assets/{chunk-55IACEB6-nBwigOgn.js → chunk-55IACEB6-CtNd8e4Y.js} +1 -1
- package/viewer/dist/assets/{chunk-5ZQYHXKU-Bxe5xIy_.js → chunk-5ZQYHXKU-Dd3yz721.js} +1 -1
- package/viewer/dist/assets/{chunk-727SXJPM-DZmTgL68.js → chunk-727SXJPM-CAM8mGbv.js} +1 -1
- package/viewer/dist/assets/{chunk-AQP2D5EJ-B7wr_Owx.js → chunk-AQP2D5EJ-C3nW9Uve.js} +1 -1
- package/viewer/dist/assets/{chunk-BSJP7CBP-DbAKfVCK.js → chunk-BSJP7CBP-r6MRPvAH.js} +1 -1
- package/viewer/dist/assets/{chunk-CSCIHK7Q-C0rsBwqP.js → chunk-CSCIHK7Q-Cn-ZTxsS.js} +1 -1
- package/viewer/dist/assets/{chunk-FMBD7UC4-BAtzt0wv.js → chunk-FMBD7UC4-5axXeZJa.js} +1 -1
- package/viewer/dist/assets/{chunk-KSCS5N6A-CXXwf52I.js → chunk-KSCS5N6A-BwmNx0l0.js} +1 -1
- package/viewer/dist/assets/{chunk-L5ZTLDWV-DS4vRI1U.js → chunk-L5ZTLDWV-C2YNkUjB.js} +1 -1
- package/viewer/dist/assets/chunk-LZXEDZCA-CznuDgQD.js +2 -0
- package/viewer/dist/assets/{chunk-ND2GUHAM-CUSnPl8t.js → chunk-ND2GUHAM-CokDnghi.js} +1 -1
- package/viewer/dist/assets/{chunk-NZK2D7GU-Dh986nJk.js → chunk-NZK2D7GU-aaU0v4QE.js} +1 -1
- package/viewer/dist/assets/{chunk-O5CBEL6O-JAEZ_pS6.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-BwMZI0gv.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-NGC7gYHM.js → cose-bilkent-S5V4N54A-CO8yESCW.js} +1 -1
- package/viewer/dist/assets/{dagre-BM42HDAG-RD63uyvd.js → dagre-BM42HDAG-BnmNVNI2.js} +1 -1
- package/viewer/dist/assets/{diagram-2AECGRRQ-hwnqqCcb.js → diagram-2AECGRRQ-lcGXcbei.js} +1 -1
- package/viewer/dist/assets/{diagram-5GNKFQAL-q8EaoZSG.js → diagram-5GNKFQAL-Cj3-lPUt.js} +1 -1
- package/viewer/dist/assets/{diagram-KO2AKTUF-D4_5Qf-l.js → diagram-KO2AKTUF-CtXdRI4M.js} +1 -1
- package/viewer/dist/assets/{diagram-LMA3HP47-D8pwekFs.js → diagram-LMA3HP47-CIyLAzX7.js} +1 -1
- package/viewer/dist/assets/{diagram-OG6HWLK6-D9KinIWZ.js → diagram-OG6HWLK6-BwF4bQCv.js} +1 -1
- package/viewer/dist/assets/{dist-CFOOgrqc.js → dist-DzXuQd4N.js} +1 -1
- package/viewer/dist/assets/{erDiagram-TEJ5UH35-D0Wfq250.js → erDiagram-TEJ5UH35-DjwFw9yq.js} +1 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-BlKFmSzA.js +1 -0
- package/viewer/dist/assets/{flowDiagram-I6XJVG4X-Y2DY-Ze2.js → flowDiagram-I6XJVG4X-CzXbf7vQ.js} +1 -1
- package/viewer/dist/assets/{ganttDiagram-6RSMTGT7-BnqkeLVw.js → ganttDiagram-6RSMTGT7-BYcIhfmm.js} +1 -1
- package/viewer/dist/assets/{gitGraph-WXDBUCRP-Cft7usRT.js → gitGraph-WXDBUCRP-H8XeJ9QB.js} +1 -1
- package/viewer/dist/assets/{gitGraphDiagram-PVQCEYII-D-cYtraK.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-Djc8Bx3F.js → info-J43DQDTF-BQScorZ0.js} +1 -1
- package/viewer/dist/assets/{infoDiagram-5YYISTIA-D-ehtyyJ.js → infoDiagram-5YYISTIA-Udl1omvH.js} +1 -1
- package/viewer/dist/assets/{ishikawaDiagram-YF4QCWOH-Ct3f6bH-.js → ishikawaDiagram-YF4QCWOH-VnfzT_hk.js} +1 -1
- package/viewer/dist/assets/{journeyDiagram-JHISSGLW-DXlULEmi.js → journeyDiagram-JHISSGLW-Btn4EVoD.js} +1 -1
- package/viewer/dist/assets/{kanban-definition-UN3LZRKU-3vE9h-R7.js → kanban-definition-UN3LZRKU-flOzWhPe.js} +1 -1
- package/viewer/dist/assets/{line-B8MygbLB.js → line-DJJ9BLuY.js} +1 -1
- package/viewer/dist/assets/{linear-CfMuM0B3.js → linear-C2SUy5ns.js} +1 -1
- package/viewer/dist/assets/{mermaid-parser.core-DzlZTbbh.js → mermaid-parser.core-xhSeYiZo.js} +2 -2
- package/viewer/dist/assets/{mermaid.core-IM-sPiyq.js → mermaid.core-DmeSIitY.js} +3 -3
- package/viewer/dist/assets/{mindmap-definition-RKZ34NQL-CMnpAq1T.js → mindmap-definition-RKZ34NQL-CCJcMKx1.js} +1 -1
- package/viewer/dist/assets/{packet-YPE3B663-D44AzgHh.js → packet-YPE3B663-DS8RscAk.js} +1 -1
- package/viewer/dist/assets/{pie-LRSECV5Y-DL8AVJH_.js → pie-LRSECV5Y-I2ZQhrNJ.js} +1 -1
- package/viewer/dist/assets/{pieDiagram-4H26LBE5-FvKK5jd7.js → pieDiagram-4H26LBE5-BS5HMs4j.js} +1 -1
- package/viewer/dist/assets/{quadrantDiagram-W4KKPZXB-CmjSkU8c.js → quadrantDiagram-W4KKPZXB-W4H8cw2_.js} +1 -1
- package/viewer/dist/assets/{radar-GUYGQ44K-BvfZTVyH.js → radar-GUYGQ44K-kf6QvSne.js} +1 -1
- package/viewer/dist/assets/{requirementDiagram-4Y6WPE33-BOjca3VH.js → requirementDiagram-4Y6WPE33-DSbuDQGQ.js} +1 -1
- package/viewer/dist/assets/{sankeyDiagram-5OEKKPKP-ANcjfNix.js → sankeyDiagram-5OEKKPKP-CH8Qr2vL.js} +1 -1
- package/viewer/dist/assets/{sequenceDiagram-3UESZ5HK-BLQ9AL7I.js → sequenceDiagram-3UESZ5HK-EugVcdad.js} +1 -1
- package/viewer/dist/assets/src-CM_9kdrU.js +1 -0
- package/viewer/dist/assets/{stateDiagram-AJRCARHV-D6CriBS6.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-BNhWZ_DL.js → timeline-definition-PNZ67QCA-B0KW9GUN.js} +1 -1
- package/viewer/dist/assets/{treeView-BLDUP644-CY6Ph5Pu.js → treeView-BLDUP644-CAEY4mxq.js} +1 -1
- package/viewer/dist/assets/{treemap-LRROVOQU-DChSA_Qx.js → treemap-LRROVOQU-VVaEaTlU.js} +1 -1
- package/viewer/dist/assets/{vennDiagram-CIIHVFJN-C01WznAC.js → vennDiagram-CIIHVFJN-BSLGNfJd.js} +1 -1
- package/viewer/dist/assets/{wardley-L42UT6IY-BJ8uNoJu.js → wardley-L42UT6IY-C-5cXbAU.js} +1 -1
- package/viewer/dist/assets/{wardleyDiagram-YWT4CUSO-DwDEzlVm.js → wardleyDiagram-YWT4CUSO-COk4TVnQ.js} +1 -1
- package/viewer/dist/assets/{xychartDiagram-2RQKCTM6-BCvIDwU0.js → xychartDiagram-2RQKCTM6-CClGJLJp.js} +1 -1
- package/viewer/dist/index.html +3 -3
- package/dist/mcp/git.js.map +0 -1
- package/viewer/dist/assets/architecture-7EHR7CIX-CGfWeim3.js +0 -1
- package/viewer/dist/assets/channel-19IdUS_c.js +0 -1
- package/viewer/dist/assets/chunk-LZXEDZCA-CclT9MXr.js +0 -2
- package/viewer/dist/assets/chunk-QZHKN3VN-BKc_Kg2Z.js +0 -1
- package/viewer/dist/assets/chunk-WU5MYG2G-9ssTSMzt.js +0 -1
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-DXv85WFd.js +0 -1
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-DXv85WFd.js +0 -1
- package/viewer/dist/assets/eventmodeling-FCH6USID-D3KRSuC1.js +0 -1
- package/viewer/dist/assets/index-CDR-riG2.css +0 -2
- package/viewer/dist/assets/index-DRPsTWe2.js +0 -98
- package/viewer/dist/assets/src-CAMdANUp.js +0 -1
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-DcTp66RQ.js +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { rm } from 'node:fs/promises';
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
6
|
import { z } from 'zod';
|
|
5
|
-
import { isHandleShaped, isKnownHandle, typeForHandle } from '../core/handles.js';
|
|
7
|
+
import { isHandleShaped, isKnownHandle, TYPE_FOLDERS, typeForHandle, } from '../core/handles.js';
|
|
6
8
|
import { loadPlan } from '../core/indexer.js';
|
|
7
9
|
import { lintPlan } from '../core/lint.js';
|
|
8
10
|
import { resolvePlanDir } from '../core/resolve.js';
|
|
9
11
|
import { TYPE_NAMES } from '../core/types.js';
|
|
10
|
-
import { diffPlan, planDirty, planLog, writeSyncPoint } from '
|
|
12
|
+
import { diffPlan, planDirty, planLog, writeSyncPoint } from '../core/git.js';
|
|
11
13
|
import { searchCards } from './search.js';
|
|
12
14
|
import { applyCardPatch, createCardFile, deepMerge, relPathForHandle, reservedFieldKeys, updateCardFile, } from '../core/writer.js';
|
|
15
|
+
import { connectedRepoToFm, listConnectedRepos, readConnectedRepos, removeConnectedRepoEntry, resolveConnectedRepo, upsertConnectedRepo, } from '../core/repos.js';
|
|
13
16
|
const INSTRUCTIONS = `# Constellation MCP
|
|
14
17
|
|
|
15
18
|
The project's architecture plan lives as markdown files in a constellation/ folder.
|
|
@@ -25,10 +28,46 @@ Writes are validated: every write tool lints and returns issues for the file it
|
|
|
25
28
|
update_card patch.fields deep-merges (arrays replace, null deletes); body replaces.
|
|
26
29
|
Body-only updates never reformat frontmatter.
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
|
|
36
|
+
Change tracking is git: diff_plan reports per-card changes since the sync marker (or HEAD).
|
|
37
|
+
Never stamp dirty flags into cards.
|
|
38
|
+
|
|
39
|
+
"Sync the plan" / "sync the plan to the code" = bring the CODE up to match the plan (the
|
|
40
|
+
plan is the source of truth — behavior changes in the plan FIRST, then in code, never the
|
|
41
|
+
reverse). It is NOT merely stamping the marker. The loop: diff_plan (base = marker) for what
|
|
42
|
+
changed → traverse the changed handles (detail: "full") for blast radius → update the
|
|
43
|
+
application code to match those cards → run the build/tests and bump card status → commit,
|
|
44
|
+
then set_sync_point to advance the marker (commit the plan first — it warns if the plan is
|
|
45
|
+
uncommitted). When the diff is large and the affected areas don't share files, act as the
|
|
46
|
+
ORCHESTRATOR rather than editing it all yourself: partition the blast radius into
|
|
47
|
+
independent, non-overlapping neighborhoods (split on file boundaries so no two agents touch
|
|
48
|
+
the same file, AND assign each plan card to exactly one agent — two agents calling
|
|
49
|
+
update_card on the same card race, and the later write silently clobbers the earlier) and
|
|
50
|
+
fan out a sub-agent per neighborhood in parallel; use one agent when the change is small or
|
|
51
|
+
files overlap. Delegating keeps your context clean and lets you hold the
|
|
52
|
+
macro view. ALWAYS verify the sub-agents' work yourself after they have all finished — re-read
|
|
53
|
+
each change against its cards and run the build/tests; never trust their reports alone — then
|
|
54
|
+
set the sync point once.
|
|
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.
|
|
32
71
|
|
|
33
72
|
For migrations or large scaffolds, use create_cards and add_connections (batched, one
|
|
34
73
|
lint pass) instead of many single calls — connections between cards in the same batch
|
|
@@ -40,11 +79,106 @@ The plan folder is found by walking up from the working directory, BOUNDED by th
|
|
|
40
79
|
root (it never adopts another repo's plan). If no plan exists in this repo (tools return
|
|
41
80
|
NO_PLAN_FOUND), call init_plan once — create_card works immediately after.
|
|
42
81
|
|
|
82
|
+
When building or auditing a plan, act as a senior engineer and architect advising the user,
|
|
83
|
+
not a scribe taking dictation: don't assume they know everything — bring expertise, name
|
|
84
|
+
trade-offs and risks, propose what's missing, and explain the why so they can decide. Hold
|
|
85
|
+
the bar high and with integrity: do it right, be honest about built-vs-planned and
|
|
86
|
+
verified-vs-assumed, and surface uncertainty rather than papering over it. But don't
|
|
87
|
+
over-engineer — there's elegance in simplicity: calibrate to the project's scope, recommend
|
|
88
|
+
the smallest change that most improves the plan, and don't manufacture gaps to look thorough.
|
|
89
|
+
The aim is a plan the user would be proud to ship. Hold it to one bar above all: if every
|
|
90
|
+
line of code were deleted, the app could be rebuilt from the plan alone — aim for that
|
|
91
|
+
coverage (not volume), and whatever you couldn't rebuild is the gap.
|
|
92
|
+
|
|
93
|
+
Work macro→micro: orient (manifest, routes, folder
|
|
94
|
+
layout) and seed PLAN-PROJECT + a system DIAGRAM — propose a human-readable project name
|
|
95
|
+
and confirm it with the user (it's plan.md's name: and the viewer's title; change it
|
|
96
|
+
anytime via update_card on PLAN-PROJECT); then follow the DATA (DB → DATATYPE →
|
|
97
|
+
API → PAGE, with FLOW/STATE for paths and lifecycles) and the USER (ROLE + auth FLOW
|
|
98
|
+
first, then PAGE/COMPONENT and key journeys) and the EDGES (EXTERNAL/JOB/EVENT); then zoom
|
|
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,
|
|
100
|
+
priorities, and history the code can't reveal. Then find gaps IN THE PLAN: step back and
|
|
101
|
+
hunt blind spots the user may not have considered — missing unhappy paths and lifecycle
|
|
102
|
+
states, auth/permission gaps, and cross-cutting concerns plans forget (security, privacy,
|
|
103
|
+
observability, rate limits, pagination, migrations, testing). The mechanical checks
|
|
104
|
+
(check_integrity orphans, dangling refs, code-without-cards) are just hygiene. Give a
|
|
105
|
+
short, prioritized list of recommendations and ask about the judgment calls. For the full
|
|
106
|
+
method use the bootstrap_plan or audit_plan prompt. Status is planned → building → built →
|
|
107
|
+
verified; verify only against real code.
|
|
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
|
+
|
|
43
124
|
To let the user browse the plan visually, start_viewer launches a local web server that
|
|
44
125
|
renders the plan as an editable site and returns its URL (it scans forward from port 4747
|
|
45
126
|
for a free port, so always read the actual port from the response). ALWAYS post that URL
|
|
46
127
|
back to the user as a clickable link, e.g. http://localhost:4747/, and tell them the port.
|
|
47
128
|
The viewer runs until stop_viewer or until this server process exits.`;
|
|
129
|
+
// The full plan-from-code playbook lives in one file (skill/methodology.md), shared by the
|
|
130
|
+
// skill and the MCP prompts so the two can't drift. Resolve it relative to this module:
|
|
131
|
+
// from dist/mcp/server.js (or src/mcp/server.ts) '../..' is the package/repo root.
|
|
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
|
+
}
|
|
164
|
+
let methodologyCache = null;
|
|
165
|
+
async function methodologyText() {
|
|
166
|
+
if (methodologyCache === null) {
|
|
167
|
+
methodologyCache = await readFile(METHODOLOGY_PATH, 'utf8');
|
|
168
|
+
}
|
|
169
|
+
return methodologyCache;
|
|
170
|
+
}
|
|
171
|
+
/** A prompt body = a one-line mode intro followed by the shared methodology. */
|
|
172
|
+
async function planPromptBody(intro) {
|
|
173
|
+
try {
|
|
174
|
+
return `${intro}\n\n${await methodologyText()}`;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return (`${intro}\n\n(Could not read the methodology file; follow the macro→micro summary ` +
|
|
178
|
+
`in the server instructions: orient, follow the data, follow the user/auth, follow ` +
|
|
179
|
+
`the edges, zoom in, ask only what the code can't answer, find gaps, recommend.)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
48
182
|
function ok(data) {
|
|
49
183
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
50
184
|
}
|
|
@@ -54,6 +188,25 @@ function fail(code, message) {
|
|
|
54
188
|
isError: true,
|
|
55
189
|
};
|
|
56
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
|
+
}
|
|
57
210
|
function summary(card) {
|
|
58
211
|
return {
|
|
59
212
|
handle: card.handle,
|
|
@@ -81,25 +234,87 @@ function issuesForFile(issues, relPath) {
|
|
|
81
234
|
const detailSchema = z.enum(['none', 'summary', 'full']);
|
|
82
235
|
const typeSchema = z.enum(TYPE_NAMES);
|
|
83
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.');
|
|
84
241
|
export function buildServer(options = {}) {
|
|
85
|
-
const server = new McpServer({ name: 'constellation', version: '0.
|
|
242
|
+
const server = new McpServer({ name: 'constellation', version: '0.2.0' }, { instructions: INSTRUCTIONS });
|
|
243
|
+
server.registerPrompt('bootstrap_plan', {
|
|
244
|
+
title: 'Bootstrap a plan from the codebase',
|
|
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.',
|
|
246
|
+
}, async () => ({
|
|
247
|
+
messages: [
|
|
248
|
+
{
|
|
249
|
+
role: 'user',
|
|
250
|
+
content: {
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: await planPromptBody('Bootstrap (or extend) the Constellation plan for THIS repository by analyzing its code. If no plan exists, call init_plan first. Work the method below end to end, then report the gaps you found and a short, prioritized list of recommendations.'),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}));
|
|
257
|
+
server.registerPrompt('audit_plan', {
|
|
258
|
+
title: 'Audit the plan against the code',
|
|
259
|
+
description: 'Reconcile the existing plan with the codebase: find gaps, orphans, stale or missing cards, and dangling refs; verify card statuses against the real code; and make tasteful recommendations.',
|
|
260
|
+
}, async () => ({
|
|
261
|
+
messages: [
|
|
262
|
+
{
|
|
263
|
+
role: 'user',
|
|
264
|
+
content: {
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: await planPromptBody('Audit the existing Constellation plan for THIS repository against its code. Lean on Steps 7–8 (find gaps, recommend) and on verifying statuses against real code, but use the whole method as a checklist. Report what is missing, stale, or orphaned, then a short, prioritized list of recommendations.'),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
}));
|
|
86
271
|
async function planRoot() {
|
|
87
272
|
return options.planRoot ?? resolvePlanDir();
|
|
88
273
|
}
|
|
89
274
|
// A single web viewer owned by this server process; null until start_viewer runs.
|
|
90
275
|
let viewer = null;
|
|
91
|
-
|
|
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. */
|
|
92
311
|
function withPlan(handler) {
|
|
93
312
|
return async (args) => {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
-
`server uses its own working directory — if that isn't your repo, set "cwd" to ` +
|
|
98
|
-
'the repo root in your MCP client config. Otherwise call init_plan (optionally ' +
|
|
99
|
-
'with { path } pointing at the repo root), or run `constellation init`.');
|
|
100
|
-
}
|
|
313
|
+
const target = await resolveTarget(args?.repo);
|
|
314
|
+
if ('error' in target)
|
|
315
|
+
return target.error;
|
|
101
316
|
try {
|
|
102
|
-
return await handler(root, args);
|
|
317
|
+
return await handler(target.root, args);
|
|
103
318
|
}
|
|
104
319
|
catch (err) {
|
|
105
320
|
return fail('INTERNAL', err instanceof Error ? err.message : String(err));
|
|
@@ -107,20 +322,25 @@ export function buildServer(options = {}) {
|
|
|
107
322
|
};
|
|
108
323
|
}
|
|
109
324
|
server.registerTool('init_plan', {
|
|
110
|
-
description: 'Bootstrap a new plan: create a constellation/ folder with a starter plan.md. Use only when no plan exists yet (other tools return NO_PLAN_FOUND). After this, create_card works immediately.',
|
|
325
|
+
description: 'Bootstrap a new plan: create a constellation/ folder with a starter plan.md. Use only when no plan exists yet (other tools return NO_PLAN_FOUND). Pass name to set the project name (shown as the viewer title); if omitted it defaults to a title-cased folder name (pyramid-server → "Pyramid Server"). Propose a name, confirm it with the user, and change it anytime via update_card on PLAN-PROJECT. After this, create_card works immediately.',
|
|
111
326
|
inputSchema: {
|
|
112
327
|
path: z
|
|
113
328
|
.string()
|
|
114
329
|
.optional()
|
|
115
330
|
.describe('directory to create constellation/ in (default: cwd)'),
|
|
331
|
+
name: z
|
|
332
|
+
.string()
|
|
333
|
+
.optional()
|
|
334
|
+
.describe('project name for plan.md (default: a title-cased folder name)'),
|
|
116
335
|
},
|
|
117
|
-
}, async ({ path: target }) => {
|
|
336
|
+
}, async ({ path: target, name }) => {
|
|
118
337
|
try {
|
|
119
338
|
const { initPlan } = await import('../core/scaffold.js');
|
|
120
|
-
const created = await initPlan(target ?? process.cwd());
|
|
339
|
+
const { root: created, name: projectName } = await initPlan(target ?? process.cwd(), { name });
|
|
121
340
|
return ok({
|
|
122
341
|
created,
|
|
123
|
-
|
|
342
|
+
name: projectName,
|
|
343
|
+
next: `Plan created with project name "${projectName}" (the viewer title). Confirm the name with the user — change it anytime via update_card on PLAN-PROJECT (patch.name). Then add cards with create_card.`,
|
|
124
344
|
});
|
|
125
345
|
}
|
|
126
346
|
catch (err) {
|
|
@@ -128,10 +348,12 @@ export function buildServer(options = {}) {
|
|
|
128
348
|
}
|
|
129
349
|
});
|
|
130
350
|
server.registerTool('get_card', {
|
|
351
|
+
annotations: { readOnlyHint: true },
|
|
131
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.',
|
|
132
353
|
inputSchema: {
|
|
133
354
|
handle: z.string(),
|
|
134
355
|
connected: detailSchema.optional().describe('default: summary'),
|
|
356
|
+
repo: repoSchema,
|
|
135
357
|
},
|
|
136
358
|
}, withPlan(async (root, { handle, connected }) => {
|
|
137
359
|
const index = await loadPlan(root);
|
|
@@ -144,6 +366,7 @@ export function buildServer(options = {}) {
|
|
|
144
366
|
});
|
|
145
367
|
}));
|
|
146
368
|
server.registerTool('list_cards', {
|
|
369
|
+
annotations: { readOnlyHint: true },
|
|
147
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).',
|
|
148
371
|
inputSchema: {
|
|
149
372
|
types: z.array(typeSchema).optional(),
|
|
@@ -154,6 +377,7 @@ export function buildServer(options = {}) {
|
|
|
154
377
|
.optional()
|
|
155
378
|
.describe('false = orphans only; true = connected only'),
|
|
156
379
|
limit: z.number().int().min(1).max(500).optional(),
|
|
380
|
+
repo: repoSchema,
|
|
157
381
|
},
|
|
158
382
|
}, withPlan(async (root, { types, kind, status, connected, limit }) => {
|
|
159
383
|
const index = await loadPlan(root);
|
|
@@ -171,12 +395,14 @@ export function buildServer(options = {}) {
|
|
|
171
395
|
});
|
|
172
396
|
}));
|
|
173
397
|
server.registerTool('search', {
|
|
398
|
+
annotations: { readOnlyHint: true },
|
|
174
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.',
|
|
175
400
|
inputSchema: {
|
|
176
401
|
q: z.string(),
|
|
177
402
|
types: z.array(typeSchema).optional(),
|
|
178
403
|
limit: z.number().int().min(1).max(100).optional(),
|
|
179
404
|
connected: detailSchema.optional().describe('default: none'),
|
|
405
|
+
repo: repoSchema,
|
|
180
406
|
},
|
|
181
407
|
}, withPlan(async (root, { q, types, limit, connected }) => {
|
|
182
408
|
const index = await loadPlan(root);
|
|
@@ -191,12 +417,14 @@ export function buildServer(options = {}) {
|
|
|
191
417
|
});
|
|
192
418
|
}));
|
|
193
419
|
server.registerTool('traverse', {
|
|
420
|
+
annotations: { readOnlyHint: true },
|
|
194
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.',
|
|
195
422
|
inputSchema: {
|
|
196
423
|
start: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
197
424
|
depth: z.number().int().min(0).max(5).optional().describe('default: 2'),
|
|
198
425
|
types: z.array(typeSchema).optional(),
|
|
199
426
|
detail: z.enum(['summary', 'full']).optional().describe('default: summary'),
|
|
427
|
+
repo: repoSchema,
|
|
200
428
|
},
|
|
201
429
|
}, withPlan(async (root, { start, depth, types, detail }) => {
|
|
202
430
|
const index = await loadPlan(root);
|
|
@@ -240,8 +468,41 @@ export function buildServer(options = {}) {
|
|
|
240
468
|
const connections = index.connections.filter((c) => distance.has(c.a) && distance.has(c.b));
|
|
241
469
|
return ok({ cards, connections, not_found: missing });
|
|
242
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
|
+
});
|
|
243
504
|
server.registerTool('create_card', {
|
|
244
|
-
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.',
|
|
245
506
|
inputSchema: {
|
|
246
507
|
handle: z.string(),
|
|
247
508
|
name: z.string().optional(),
|
|
@@ -254,6 +515,7 @@ export function buildServer(options = {}) {
|
|
|
254
515
|
.boolean()
|
|
255
516
|
.optional()
|
|
256
517
|
.describe('default true; false skips lint and returns no issues'),
|
|
518
|
+
repo: repoSchema,
|
|
257
519
|
},
|
|
258
520
|
}, withPlan(async (root, args) => {
|
|
259
521
|
const handle = args.handle.toUpperCase();
|
|
@@ -312,6 +574,7 @@ export function buildServer(options = {}) {
|
|
|
312
574
|
}))
|
|
313
575
|
.min(1)
|
|
314
576
|
.max(500),
|
|
577
|
+
repo: repoSchema,
|
|
315
578
|
},
|
|
316
579
|
}, withPlan(async (root, { cards }) => {
|
|
317
580
|
const index = await loadPlan(root);
|
|
@@ -380,6 +643,7 @@ export function buildServer(options = {}) {
|
|
|
380
643
|
.array(z.object({ from: z.string(), to: z.string() }))
|
|
381
644
|
.min(1)
|
|
382
645
|
.max(1000),
|
|
646
|
+
repo: repoSchema,
|
|
383
647
|
},
|
|
384
648
|
}, withPlan(async (root, { connections }) => {
|
|
385
649
|
const index = await loadPlan(root);
|
|
@@ -406,18 +670,20 @@ export function buildServer(options = {}) {
|
|
|
406
670
|
additions.get(f).add(t);
|
|
407
671
|
}
|
|
408
672
|
let added = 0;
|
|
673
|
+
const touched = new Set();
|
|
409
674
|
for (const [src, targets] of additions) {
|
|
410
675
|
const card = index.cards.get(src);
|
|
411
676
|
const existingList = Array.isArray(card.frontmatter.connections)
|
|
412
|
-
? card.frontmatter.connections
|
|
677
|
+
? card.frontmatter.connections.filter((c) => typeof c === 'string')
|
|
413
678
|
: [];
|
|
414
679
|
const merged = [...new Set([...existingList, ...targets])];
|
|
415
680
|
const frontmatter = applyCardPatch(card.frontmatter, { connections: merged });
|
|
416
681
|
await updateCardFile(card.filePath, { frontmatter });
|
|
682
|
+
touched.add(card.relPath);
|
|
417
683
|
added += targets.size;
|
|
418
684
|
}
|
|
419
685
|
const lint = await lintPlan(root);
|
|
420
|
-
return ok({ added, failed,
|
|
686
|
+
return ok({ added, failed, issues: lint.issues.filter((i) => touched.has(i.file)) });
|
|
421
687
|
}));
|
|
422
688
|
server.registerTool('update_card', {
|
|
423
689
|
description: 'Update a card. patch.name/kind/status set or delete (null); patch.connections replaces the list; patch.fields deep-merges into type-specific frontmatter (arrays replace, null deletes). body replaces the whole body. Body-only updates never reformat frontmatter.',
|
|
@@ -433,6 +699,7 @@ export function buildServer(options = {}) {
|
|
|
433
699
|
})
|
|
434
700
|
.optional(),
|
|
435
701
|
body: z.string().optional(),
|
|
702
|
+
repo: repoSchema,
|
|
436
703
|
},
|
|
437
704
|
}, withPlan(async (root, { handle, patch, body }) => {
|
|
438
705
|
const index = await loadPlan(root);
|
|
@@ -459,7 +726,7 @@ export function buildServer(options = {}) {
|
|
|
459
726
|
}));
|
|
460
727
|
server.registerTool('delete_card', {
|
|
461
728
|
description: 'Delete a card file. Returns the handles that referenced it (their references are now dangling) plus resulting lint issues.',
|
|
462
|
-
inputSchema: { handle: z.string() },
|
|
729
|
+
inputSchema: { handle: z.string(), repo: repoSchema },
|
|
463
730
|
}, withPlan(async (root, { handle }) => {
|
|
464
731
|
const index = await loadPlan(root);
|
|
465
732
|
const card = index.cards.get(handle.toUpperCase());
|
|
@@ -479,7 +746,7 @@ export function buildServer(options = {}) {
|
|
|
479
746
|
}));
|
|
480
747
|
server.registerTool('add_connection', {
|
|
481
748
|
description: 'Connect two cards by appending `to` to `from`’s connections list. No-op if they are already connected through any source.',
|
|
482
|
-
inputSchema: { from: z.string(), to: z.string() },
|
|
749
|
+
inputSchema: { from: z.string(), to: z.string(), repo: repoSchema },
|
|
483
750
|
}, withPlan(async (root, args) => {
|
|
484
751
|
const index = await loadPlan(root);
|
|
485
752
|
const from = index.cards.get(args.from.toUpperCase());
|
|
@@ -497,11 +764,16 @@ export function buildServer(options = {}) {
|
|
|
497
764
|
connections: [...existing, to.handle],
|
|
498
765
|
});
|
|
499
766
|
await updateCardFile(from.filePath, { frontmatter });
|
|
500
|
-
|
|
767
|
+
const lint = await lintPlan(root);
|
|
768
|
+
return ok({
|
|
769
|
+
connected: [from.handle, to.handle],
|
|
770
|
+
declared_on: from.handle,
|
|
771
|
+
issues: issuesForFile(lint.issues, from.relPath),
|
|
772
|
+
});
|
|
501
773
|
}));
|
|
502
774
|
server.registerTool('remove_connection', {
|
|
503
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.',
|
|
504
|
-
inputSchema: { a: z.string(), b: z.string() },
|
|
776
|
+
inputSchema: { a: z.string(), b: z.string(), repo: repoSchema },
|
|
505
777
|
}, withPlan(async (root, args) => {
|
|
506
778
|
const index = await loadPlan(root);
|
|
507
779
|
const cardA = index.cards.get(args.a.toUpperCase());
|
|
@@ -526,7 +798,8 @@ export function buildServer(options = {}) {
|
|
|
526
798
|
removedFrom.push(card.handle);
|
|
527
799
|
}
|
|
528
800
|
}
|
|
529
|
-
const
|
|
801
|
+
const lint = await lintPlan(root);
|
|
802
|
+
const after = lint.index;
|
|
530
803
|
const stillConnected = after.connectedHandles.get(cardA.handle)?.has(cardB.handle) ?? false;
|
|
531
804
|
const remainingSources = [];
|
|
532
805
|
if (stillConnected) {
|
|
@@ -542,15 +815,140 @@ export function buildServer(options = {}) {
|
|
|
542
815
|
remainingSources.push(`mermaid block in ${card.handle}`);
|
|
543
816
|
}
|
|
544
817
|
}
|
|
818
|
+
const touched = new Set(removedFrom
|
|
819
|
+
.map((h) => after.cards.get(h)?.relPath)
|
|
820
|
+
.filter((p) => Boolean(p)));
|
|
545
821
|
return ok({
|
|
546
822
|
removed_from: removedFrom,
|
|
547
823
|
still_connected: stillConnected,
|
|
548
824
|
remaining_sources: remainingSources,
|
|
825
|
+
issues: lint.issues.filter((i) => touched.has(i.file)),
|
|
826
|
+
});
|
|
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),
|
|
549
946
|
});
|
|
550
947
|
}));
|
|
551
948
|
server.registerTool('check_integrity', {
|
|
949
|
+
annotations: { readOnlyHint: true },
|
|
552
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.',
|
|
553
|
-
inputSchema: {},
|
|
951
|
+
inputSchema: { repo: repoSchema },
|
|
554
952
|
}, withPlan(async (root) => {
|
|
555
953
|
const lint = await lintPlan(root);
|
|
556
954
|
const orphans = [...lint.index.cards.keys()]
|
|
@@ -565,19 +963,23 @@ export function buildServer(options = {}) {
|
|
|
565
963
|
});
|
|
566
964
|
}));
|
|
567
965
|
server.registerTool('diff_plan', {
|
|
966
|
+
annotations: { readOnlyHint: true },
|
|
568
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.',
|
|
569
968
|
inputSchema: {
|
|
570
969
|
base: z.string().optional(),
|
|
571
970
|
head: z.string().optional(),
|
|
971
|
+
repo: repoSchema,
|
|
572
972
|
},
|
|
573
973
|
}, withPlan(async (root, { base, head }) => {
|
|
574
974
|
return ok(await diffPlan(root, base, head));
|
|
575
975
|
}));
|
|
576
976
|
server.registerTool('plan_log', {
|
|
977
|
+
annotations: { readOnlyHint: true },
|
|
577
978
|
description: 'Git history of one card: the commits that touched its file.',
|
|
578
979
|
inputSchema: {
|
|
579
980
|
handle: z.string(),
|
|
580
981
|
limit: z.number().int().min(1).max(100).optional(),
|
|
982
|
+
repo: repoSchema,
|
|
581
983
|
},
|
|
582
984
|
}, withPlan(async (root, { handle, limit }) => {
|
|
583
985
|
const index = await loadPlan(root);
|
|
@@ -590,7 +992,7 @@ export function buildServer(options = {}) {
|
|
|
590
992
|
}));
|
|
591
993
|
server.registerTool('set_sync_point', {
|
|
592
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.',
|
|
593
|
-
inputSchema: { sha: z.string().optional() },
|
|
995
|
+
inputSchema: { sha: z.string().optional(), repo: repoSchema },
|
|
594
996
|
}, withPlan(async (root, { sha }) => {
|
|
595
997
|
const point = await writeSyncPoint(root, sha);
|
|
596
998
|
const dirty = await planDirty(root);
|
|
@@ -652,13 +1054,7 @@ export function buildServer(options = {}) {
|
|
|
652
1054
|
const url = `http://localhost:${running.port}/`;
|
|
653
1055
|
viewer = { server: running, planRoot: root, url };
|
|
654
1056
|
if (open) {
|
|
655
|
-
|
|
656
|
-
const cmd = process.platform === 'darwin'
|
|
657
|
-
? 'open'
|
|
658
|
-
: process.platform === 'win32'
|
|
659
|
-
? 'start'
|
|
660
|
-
: 'xdg-open';
|
|
661
|
-
spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
|
|
1057
|
+
await openUrl(url);
|
|
662
1058
|
}
|
|
663
1059
|
return ok({ url, port: running.port, plan_root: root, editable: !(readonly ?? false) });
|
|
664
1060
|
}));
|