@smartmemory/compose 0.2.58-beta → 0.3.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 +2 -2
- package/bin/compose.js +55 -54
- package/dist/assets/{App-s4CulNkO.js → App-DJ5xk_Wx.js} +218 -218
- package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
- package/dist/assets/{arc-kIU8DDth.js → arc-DX5jqmcO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
- package/dist/assets/channel-vsDnTvkh.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
- package/dist/assets/{index-CBWbmG7W.js → index-DZTJEk-y.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
- package/dist/assets/{linear-CiccpAAd.js → linear-C1paCqE7.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +0 -5
- package/lib/build-stream-schema.js +1 -1
- package/lib/build.js +1762 -2614
- package/lib/consumer-fanout.js +1317 -0
- package/lib/feature-validator.js +6 -4
- package/lib/flow-state.js +15 -14
- package/lib/gsd-budget.js +48 -10
- package/lib/gsd-prompt.js +3 -4
- package/lib/gsd-stuck.js +1 -1
- package/lib/gsd.js +303 -149
- package/lib/local-claude-connector.js +149 -0
- package/lib/new.js +162 -307
- package/lib/result-normalizer.js +233 -18
- package/lib/review-lenses.js +1 -1
- package/lib/step-prompt.js +41 -119
- package/lib/stratum-engine.js +297 -0
- package/lib/stratum-mcp-client.js +224 -213
- package/lib/vocabulary-compliance.js +268 -0
- package/lib/vocabulary-inject.js +1 -36
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +5 -17
- package/pipelines/build.profiles.json +11 -0
- package/pipelines/build.stratum.yaml +288 -467
- package/pipelines/gsd.stratum.yaml +73 -125
- package/pipelines/new.stratum.yaml +68 -149
- package/server/build-routes.js +4 -1
- package/server/design-routes.js +37 -21
- package/server/index.js +13 -21
- package/server/lifecycle-guard.js +1 -1
- package/server/pipeline-routes.js +113 -31
- package/server/stratum-client.js +33 -47
- package/server/stratum-sync.js +3 -4
- package/server/vision-server.js +1 -1
- package/dist/assets/channel-DiJkE9og.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
- package/lib/connector-factory-shim.js +0 -167
- package/server/agent-mcp.js +0 -10
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, statSync, accessSync, constants as fsConstants } from 'node:fs';
|
|
2
|
+
import { execFileSync as _execFileSyncDefault } from 'node:child_process';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// Development/monorepo fallback when @smartmemory/stratum is not installed.
|
|
11
|
+
export const LIVE_STRATUM_TS_MCP_BIN = resolve(
|
|
12
|
+
__dirname, '..', '..', 'stratum', 'ts', 'src', 'mcp', 'bin.mjs',
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// The flow/gate SEAM uses the TS `query`/`gate` CLI (not the MCP bin).
|
|
16
|
+
export const LIVE_STRATUM_TS_CLI_BIN = resolve(
|
|
17
|
+
__dirname, '..', '..', 'stratum', 'ts', 'src', 'cli', 'bin.mjs',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// D1: a valid-format run id (matches the engine's `/^[a-zA-Z0-9-]+$/`) that no
|
|
21
|
+
// store will ever hold, so `query flow <SENTINEL>` deterministically returns the
|
|
22
|
+
// stratum NOT_FOUND projection — a POSITIVE identity check that does not depend on
|
|
23
|
+
// store data (an empty `query flows` array from a fresh store is legitimate and
|
|
24
|
+
// indistinguishable from an adversarial `[]`).
|
|
25
|
+
const PROBE_SENTINEL_RUN_ID = 'stratum-probe-nonexistent-run-0';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* D1/C1: POSITIVELY identify a flow/gate binary as one that speaks the stratum
|
|
29
|
+
* query contract, before the adapter trusts it. Existence/executability alone is
|
|
30
|
+
* not enough (a bare `stratum` on $PATH can exist yet answer with "Unknown
|
|
31
|
+
* command" and exit 0), and a shape-agnostic "returns an array" check is spoofed
|
|
32
|
+
* by an adversarial stub emitting `[]` or `[{...}]` — AND a fresh store legitimately
|
|
33
|
+
* returns `[]`. So the probe queries a nonexistent-but-valid run id: a compatible
|
|
34
|
+
* binary echoes it back in the exact NOT_FOUND projection
|
|
35
|
+
* (`{error:{code:"NOT_FOUND", message:"Flow '<id>' not found"}}`). Returns
|
|
36
|
+
* { ok, reason? }; never throws.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} bin
|
|
39
|
+
* @param {{ execFileSync?: Function }} [deps] test seam
|
|
40
|
+
*/
|
|
41
|
+
export function probeStratumBin(bin, deps = {}) {
|
|
42
|
+
const execFileSync = deps.execFileSync ?? _execFileSyncDefault;
|
|
43
|
+
// Existence / executability. A configured path must be an executable regular
|
|
44
|
+
// file; a bare command is looked up on $PATH via the query call itself.
|
|
45
|
+
if (typeof bin === 'string' && bin.includes('/')) {
|
|
46
|
+
try {
|
|
47
|
+
if (!statSync(bin).isFile()) return { ok: false, reason: `${bin} is not a regular file` };
|
|
48
|
+
accessSync(bin, fsConstants.X_OK);
|
|
49
|
+
} catch {
|
|
50
|
+
return { ok: false, reason: `${bin} is not an executable file` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// The NOT_FOUND projection is written to STDOUT but the CLI exits NON-ZERO for
|
|
54
|
+
// it, so capture stdout whether the call returns (exit 0) or throws (exit != 0).
|
|
55
|
+
let out;
|
|
56
|
+
try {
|
|
57
|
+
out = execFileSync(bin, ['query', 'flow', PROBE_SENTINEL_RUN_ID], {
|
|
58
|
+
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// E3: a TIMEOUT or SIGNAL-terminated run must NEVER be treated as a legitimate
|
|
62
|
+
// non-zero exit — a hung binary can flush partial/garbage stdout that happens to
|
|
63
|
+
// parse. Detect termination BEFORE trusting any captured stdout.
|
|
64
|
+
if (err && (err.killed === true || err.signal != null || err.code === 'ETIMEDOUT')) {
|
|
65
|
+
return { ok: false, reason: `\`${bin} query flow\` timed out or was terminated (${err.signal ?? err.code ?? 'killed'})` };
|
|
66
|
+
}
|
|
67
|
+
const captured = err?.stdout;
|
|
68
|
+
if (typeof captured === 'string' && captured.length > 0) {
|
|
69
|
+
out = captured;
|
|
70
|
+
} else if (captured && typeof captured.toString === 'function' && captured.length) {
|
|
71
|
+
out = captured.toString('utf8');
|
|
72
|
+
} else {
|
|
73
|
+
return { ok: false, reason: `\`${bin} query flow\` failed to run: ${err?.message ?? err}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(out);
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: `${bin} does not speak the stratum query contract `
|
|
83
|
+
+ `(\`query flow\` returned non-JSON: ${String(out).trim().slice(0, 80)})`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// E2: EXACT structural + literal match — not a substring. A JSON argument-parrot
|
|
87
|
+
// that echoes the sentinel anywhere inside a NOT_FOUND envelope must NOT pass. The
|
|
88
|
+
// message must equal the exact projection format the TS CLI emits.
|
|
89
|
+
const code = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
90
|
+
? parsed.error?.code : undefined;
|
|
91
|
+
const message = parsed && typeof parsed === 'object' ? parsed.error?.message : undefined;
|
|
92
|
+
const expectedMessage = `Flow '${PROBE_SENTINEL_RUN_ID}' not found`;
|
|
93
|
+
if (code !== 'NOT_FOUND' || message !== expectedMessage) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: `${bin} does not speak the stratum query contract `
|
|
97
|
+
+ `(\`query flow <sentinel>\` did not return the exact NOT_FOUND projection; got: ${String(out).trim().slice(0, 120)})`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ok: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function configuredEngine(cwd) {
|
|
104
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return undefined;
|
|
105
|
+
const configPath = join(cwd, '.compose', 'compose.json');
|
|
106
|
+
if (!existsSync(configPath)) return undefined;
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(configPath, 'utf8'))?.capabilities?.stratumEngine;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const PYTHON_LEGACY_ERROR =
|
|
115
|
+
'The Python Stratum engine has been deleted. Use the python-legacy branch for the archived Python runtime.';
|
|
116
|
+
|
|
117
|
+
/** Resolve env -> project capability -> TS-only runtime. */
|
|
118
|
+
export function resolveStratumEngine(cwd = process.cwd()) {
|
|
119
|
+
let value = process.env.COMPOSE_STRATUM_ENGINE;
|
|
120
|
+
if (value === undefined || value === '') value = configuredEngine(cwd);
|
|
121
|
+
if (value === undefined || value === '') return 'ts';
|
|
122
|
+
if (value === 'python') throw new Error(PYTHON_LEGACY_ERROR);
|
|
123
|
+
if (value !== 'ts') throw new Error(`stratumEngine must be "ts", got ${JSON.stringify(value)}`);
|
|
124
|
+
return 'ts';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const BIN_CONFIG = {
|
|
128
|
+
mcp: {
|
|
129
|
+
env: 'COMPOSE_STRATUM_TS_MCP_BIN',
|
|
130
|
+
packageBin: 'stratum-mcp',
|
|
131
|
+
sibling: LIVE_STRATUM_TS_MCP_BIN,
|
|
132
|
+
},
|
|
133
|
+
cli: {
|
|
134
|
+
env: 'COMPOSE_STRATUM_TS_CLI_BIN',
|
|
135
|
+
packageBin: 'stratum',
|
|
136
|
+
sibling: LIVE_STRATUM_TS_CLI_BIN,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Resolve a bin from an installed @smartmemory/stratum package, if present. */
|
|
141
|
+
function resolveInstalledBin(packageBin, requireResolve) {
|
|
142
|
+
let packageJsonPath;
|
|
143
|
+
try {
|
|
144
|
+
// The dependency line is intentionally added only when Stratum is published.
|
|
145
|
+
packageJsonPath = requireResolve('@smartmemory/stratum/package.json');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error?.code === 'MODULE_NOT_FOUND' || error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
155
|
+
const relativeBin = typeof packageJson.bin === 'object'
|
|
156
|
+
? packageJson.bin?.[packageBin]
|
|
157
|
+
: undefined;
|
|
158
|
+
if (typeof relativeBin !== 'string') return undefined;
|
|
159
|
+
return resolve(dirname(packageJsonPath), relativeBin);
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve env -> installed dependency -> sibling checkout, checking every
|
|
167
|
+
* candidate before returning it.
|
|
168
|
+
*
|
|
169
|
+
* @param {'mcp'|'cli'} kind
|
|
170
|
+
* @param {string} [cwd]
|
|
171
|
+
* @param {{env?: object, requireResolve?: Function, siblingBins?: object}} [deps]
|
|
172
|
+
*/
|
|
173
|
+
export function resolveStratumBin(kind, cwd = process.cwd(), deps = {}) {
|
|
174
|
+
const config = BIN_CONFIG[kind];
|
|
175
|
+
if (!config) throw new Error(`Unknown Stratum bin kind: ${JSON.stringify(kind)}`);
|
|
176
|
+
|
|
177
|
+
const env = deps.env ?? process.env;
|
|
178
|
+
const envCandidate = env[config.env]
|
|
179
|
+
// Backwards compatibility for the pre-chain flow/gate override.
|
|
180
|
+
|| (kind === 'cli' ? env.COMPOSE_STRATUM_TS_BIN : undefined);
|
|
181
|
+
if (envCandidate && existsSync(envCandidate)) return envCandidate;
|
|
182
|
+
|
|
183
|
+
const installedCandidate = resolveInstalledBin(
|
|
184
|
+
config.packageBin,
|
|
185
|
+
deps.requireResolve ?? require.resolve.bind(require),
|
|
186
|
+
);
|
|
187
|
+
if (installedCandidate && existsSync(installedCandidate)) return installedCandidate;
|
|
188
|
+
|
|
189
|
+
const siblingCandidate = deps.siblingBins?.[kind] ?? config.sibling;
|
|
190
|
+
if (siblingCandidate && existsSync(siblingCandidate)) return siblingCandidate;
|
|
191
|
+
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unable to resolve the Stratum ${kind} binary for ${cwd}. `
|
|
194
|
+
+ 'Install @smartmemory/stratum or place a stratum checkout as a sibling of compose.',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Resolve the selected engine to StratumMcpClient.connect() options. */
|
|
199
|
+
export function resolveStratumMcpConnection(cwd, deps = {}) {
|
|
200
|
+
resolveStratumEngine(cwd);
|
|
201
|
+
return {
|
|
202
|
+
command: (deps.env ?? process.env).COMPOSE_STRATUM_TS_NODE || process.execPath,
|
|
203
|
+
args: [resolveStratumBin('mcp', cwd, deps)],
|
|
204
|
+
...(cwd ? { cwd } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readStratumMcpEntry(cwd) {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(readFileSync(join(cwd, '.mcp.json'), 'utf8'))?.mcpServers?.stratum;
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function failureReason(error) {
|
|
217
|
+
const stderr = error?.stderr;
|
|
218
|
+
if (typeof stderr === 'string' && stderr.trim()) return stderr.trim();
|
|
219
|
+
if (stderr && typeof stderr.toString === 'function' && stderr.length) {
|
|
220
|
+
return stderr.toString('utf8').trim();
|
|
221
|
+
}
|
|
222
|
+
return error?.message ?? String(error);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The `.mcp.json` `stratum` entry Compose OWNS for a workspace: whatever the
|
|
227
|
+
* runtime resolver spawns (installed dependency -> sibling checkout). This is
|
|
228
|
+
* exactly what `compose init` writes, so heal/check stay consistent with it.
|
|
229
|
+
*
|
|
230
|
+
* NB: deliberately NOT the npx-pinned form that `stratum mcp install`/`doctor`
|
|
231
|
+
* write for external consumers — that form 404s until @smartmemory/stratum is
|
|
232
|
+
* published, so delegating Compose's heal to `stratum doctor --fix` would
|
|
233
|
+
* rewrite a working direct-bin entry into a broken one. Compose resolves the
|
|
234
|
+
* engine itself and heals to the path it can prove exists.
|
|
235
|
+
*/
|
|
236
|
+
function desiredStratumEntry(cwd, deps = {}) {
|
|
237
|
+
const conn = resolveStratumMcpConnection(cwd, deps);
|
|
238
|
+
return { command: conn.command, args: conn.args };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function sameMcpEntry(a, b) {
|
|
242
|
+
if (!a || !b || a.command !== b.command) return false;
|
|
243
|
+
const aArgs = Array.isArray(a.args) ? a.args : [];
|
|
244
|
+
const bArgs = Array.isArray(b.args) ? b.args : [];
|
|
245
|
+
return aArgs.length === bArgs.length && aArgs.every((value, index) => value === bArgs[index]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Read-only wiring check for `compose doctor`. Reports stale when Compose's
|
|
250
|
+
* resolved entry differs from what is on disk (i.e. `compose update` would
|
|
251
|
+
* rewrite it). Never throws.
|
|
252
|
+
*/
|
|
253
|
+
export function checkStratumWiring(cwd, deps = {}) {
|
|
254
|
+
let desired;
|
|
255
|
+
try {
|
|
256
|
+
desired = desiredStratumEntry(cwd, deps);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { ok: false, skipped: failureReason(error) };
|
|
259
|
+
}
|
|
260
|
+
const current = readStratumMcpEntry(cwd);
|
|
261
|
+
if (sameMcpEntry(current, desired)) return { ok: true, stale: false };
|
|
262
|
+
return { ok: false, stale: true, current, desired };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Best-effort repair of a project's Stratum MCP wiring to Compose's resolved
|
|
267
|
+
* entry, preserving sibling MCP servers. Never throws.
|
|
268
|
+
*/
|
|
269
|
+
export function healStratumWiring(cwd, deps = {}) {
|
|
270
|
+
const before = readStratumMcpEntry(cwd);
|
|
271
|
+
let desired;
|
|
272
|
+
try {
|
|
273
|
+
desired = desiredStratumEntry(cwd, deps);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return { healed: false, before, after: before, skipped: failureReason(error) };
|
|
276
|
+
}
|
|
277
|
+
if (sameMcpEntry(before, desired)) return { healed: false, before, after: before };
|
|
278
|
+
|
|
279
|
+
const path = join(cwd, '.mcp.json');
|
|
280
|
+
let document = {};
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
283
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) document = parsed;
|
|
284
|
+
} catch {
|
|
285
|
+
document = {}; // missing or malformed → rebuild a minimal valid document
|
|
286
|
+
}
|
|
287
|
+
if (!document.mcpServers || typeof document.mcpServers !== 'object' || Array.isArray(document.mcpServers)) {
|
|
288
|
+
document.mcpServers = {};
|
|
289
|
+
}
|
|
290
|
+
document.mcpServers.stratum = desired;
|
|
291
|
+
try {
|
|
292
|
+
writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return { healed: false, before, after: before, skipped: failureReason(error) };
|
|
295
|
+
}
|
|
296
|
+
return { healed: true, before, after: desired };
|
|
297
|
+
}
|