framein 0.0.4 → 0.0.5
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 +262 -226
- package/dist/adr.js +17 -17
- package/dist/anomaly.js +39 -39
- package/dist/bin.js +27 -27
- package/dist/blast.js +51 -51
- package/dist/brief.js +21 -21
- package/dist/capsule.js +91 -64
- package/dist/challenge.js +195 -0
- package/dist/cli.js +2150 -2090
- package/dist/db.js +7 -7
- package/dist/debt.js +42 -42
- package/dist/delegate.js +71 -64
- package/dist/detect.js +118 -118
- package/dist/disagree.js +106 -85
- package/dist/evidence.js +72 -72
- package/dist/fileWriter.js +35 -35
- package/dist/ingest.js +38 -38
- package/dist/managedBlock.js +101 -101
- package/dist/mcpRegister.js +85 -85
- package/dist/mcpServer.js +138 -138
- package/dist/palette.js +63 -63
- package/dist/projector.js +65 -65
- package/dist/quota.js +30 -30
- package/dist/recipe.js +55 -55
- package/dist/rescue.js +38 -38
- package/dist/roles.js +61 -61
- package/dist/select.js +50 -50
- package/dist/shell.js +127 -127
- package/dist/stats.js +74 -74
- package/dist/store.js +277 -277
- package/dist/task.js +94 -94
- package/dist/trust.js +39 -39
- package/dist/types.js +3 -3
- package/dist/ui/banner.js +32 -32
- package/dist/ui/capabilities.js +35 -35
- package/dist/ui/theme.js +49 -49
- package/dist/wrappers.js +62 -63
- package/package.json +46 -45
package/dist/managedBlock.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
// Managed-block upsert: framein owns only the region between its markers.
|
|
2
|
-
// Everything a user writes outside the markers is preserved across re-projection.
|
|
3
|
-
//
|
|
4
|
-
// Robustness (see codex review): markers are matched as EXACT FULL LINES, malformed
|
|
5
|
-
// states (dangling / reversed / duplicate markers) are cleaned to a single canonical
|
|
6
|
-
// block without losing user text, and marker strings that appear inside the core data
|
|
7
|
-
// are defanged so they can never be mistaken for real markers.
|
|
8
|
-
export const MANAGED_BEGIN = '<!-- framein:begin — managed by `frame`; edits inside are overwritten. Source: .frame/store.db -->';
|
|
9
|
-
export const MANAGED_END = '<!-- framein:end -->';
|
|
10
|
-
function isMarker(line, marker) {
|
|
11
|
-
return line.trim() === marker;
|
|
12
|
-
}
|
|
13
|
-
/** Neutralize any core line that IS exactly a marker, so it cannot be parsed as one. */
|
|
14
|
-
function defangMarkerLines(core) {
|
|
15
|
-
return core.split('\n').map((ln) => {
|
|
16
|
-
const t = ln.trim();
|
|
17
|
-
return t === MANAGED_BEGIN || t === MANAGED_END ? ln.replace('<!--', '<!--') : ln;
|
|
18
|
-
}).join('\n');
|
|
19
|
-
}
|
|
20
|
-
/** Wrap the core block in the managed markers. Identical across all three files. */
|
|
21
|
-
export function wrapManaged(coreBlock) {
|
|
22
|
-
return `${MANAGED_BEGIN}\n${defangMarkerLines(coreBlock)}\n${MANAGED_END}`;
|
|
23
|
-
}
|
|
24
|
-
/** All well-formed [begin,end] line-index regions, paired greedily. */
|
|
25
|
-
function findRegions(lines) {
|
|
26
|
-
const regions = [];
|
|
27
|
-
let i = 0;
|
|
28
|
-
while (i < lines.length) {
|
|
29
|
-
if (isMarker(lines[i], MANAGED_BEGIN)) {
|
|
30
|
-
let j = i + 1;
|
|
31
|
-
while (j < lines.length && !isMarker(lines[j], MANAGED_END))
|
|
32
|
-
j++;
|
|
33
|
-
if (j < lines.length) {
|
|
34
|
-
regions.push([i, j]);
|
|
35
|
-
i = j + 1;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
i++;
|
|
40
|
-
}
|
|
41
|
-
return regions;
|
|
42
|
-
}
|
|
43
|
-
/** Count of lines that are exactly a begin or end marker. */
|
|
44
|
-
function markerLineCount(lines) {
|
|
45
|
-
return lines.filter((l) => isMarker(l, MANAGED_BEGIN) || isMarker(l, MANAGED_END)).length;
|
|
46
|
-
}
|
|
47
|
-
/** Remove every well-formed managed region and any stray marker lines; preserve the rest. */
|
|
48
|
-
export function stripManagedBlocks(content) {
|
|
49
|
-
const lines = content.split('\n');
|
|
50
|
-
const out = [];
|
|
51
|
-
for (let i = 0; i < lines.length; i++) {
|
|
52
|
-
if (isMarker(lines[i], MANAGED_BEGIN)) {
|
|
53
|
-
let j = i + 1;
|
|
54
|
-
while (j < lines.length && !isMarker(lines[j], MANAGED_END))
|
|
55
|
-
j++;
|
|
56
|
-
if (j < lines.length) {
|
|
57
|
-
i = j;
|
|
58
|
-
continue;
|
|
59
|
-
} // drop a full region
|
|
60
|
-
continue; // dangling begin: drop just this line
|
|
61
|
-
}
|
|
62
|
-
if (isMarker(lines[i], MANAGED_END))
|
|
63
|
-
continue; // stray end: drop just this line
|
|
64
|
-
out.push(lines[i]);
|
|
65
|
-
}
|
|
66
|
-
return out.join('\n');
|
|
67
|
-
}
|
|
68
|
-
/** The single well-formed managed region (markers inclusive), or null. */
|
|
69
|
-
export function extractManagedBlock(content) {
|
|
70
|
-
const lines = content.split('\n');
|
|
71
|
-
const regions = findRegions(lines);
|
|
72
|
-
if (regions.length === 0)
|
|
73
|
-
return null;
|
|
74
|
-
const [b, e] = regions[0];
|
|
75
|
-
return lines.slice(b, e + 1).join('\n');
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Insert or replace the managed block in `existing`, preserving everything outside it.
|
|
79
|
-
*
|
|
80
|
-
* - null/empty existing → fresh file: `# <title>` heading + the managed block.
|
|
81
|
-
* - exactly one clean region → replace it IN PLACE (outside bytes preserved exactly).
|
|
82
|
-
* - malformed (dangling/reversed/duplicate/stray markers) → clean all of them and append a
|
|
83
|
-
* single canonical block; subsequent runs see one clean region and are fully idempotent.
|
|
84
|
-
*
|
|
85
|
-
* `managed` must be the output of wrapManaged() (markers included).
|
|
86
|
-
*/
|
|
87
|
-
export function upsertManagedBlock(existing, title, managed) {
|
|
88
|
-
if (existing == null || existing.trim() === '') {
|
|
89
|
-
return `# ${title}\n\n${managed}\n`;
|
|
90
|
-
}
|
|
91
|
-
const lines = existing.split('\n');
|
|
92
|
-
const regions = findRegions(lines);
|
|
93
|
-
const healthySingle = regions.length === 1 && markerLineCount(lines) === 2;
|
|
94
|
-
if (healthySingle) {
|
|
95
|
-
const [b, e] = regions[0];
|
|
96
|
-
return [...lines.slice(0, b), ...managed.split('\n'), ...lines.slice(e + 1)].join('\n');
|
|
97
|
-
}
|
|
98
|
-
// malformed or empty-of-user-text: rebuild cleanly without losing content
|
|
99
|
-
const body = stripManagedBlocks(existing).replace(/\s*$/, '');
|
|
100
|
-
return `${body === '' ? `# ${title}` : body}\n\n${managed}\n`;
|
|
101
|
-
}
|
|
1
|
+
// Managed-block upsert: framein owns only the region between its markers.
|
|
2
|
+
// Everything a user writes outside the markers is preserved across re-projection.
|
|
3
|
+
//
|
|
4
|
+
// Robustness (see codex review): markers are matched as EXACT FULL LINES, malformed
|
|
5
|
+
// states (dangling / reversed / duplicate markers) are cleaned to a single canonical
|
|
6
|
+
// block without losing user text, and marker strings that appear inside the core data
|
|
7
|
+
// are defanged so they can never be mistaken for real markers.
|
|
8
|
+
export const MANAGED_BEGIN = '<!-- framein:begin — managed by `frame`; edits inside are overwritten. Source: .frame/store.db -->';
|
|
9
|
+
export const MANAGED_END = '<!-- framein:end -->';
|
|
10
|
+
function isMarker(line, marker) {
|
|
11
|
+
return line.trim() === marker;
|
|
12
|
+
}
|
|
13
|
+
/** Neutralize any core line that IS exactly a marker, so it cannot be parsed as one. */
|
|
14
|
+
function defangMarkerLines(core) {
|
|
15
|
+
return core.split('\n').map((ln) => {
|
|
16
|
+
const t = ln.trim();
|
|
17
|
+
return t === MANAGED_BEGIN || t === MANAGED_END ? ln.replace('<!--', '<!--') : ln;
|
|
18
|
+
}).join('\n');
|
|
19
|
+
}
|
|
20
|
+
/** Wrap the core block in the managed markers. Identical across all three files. */
|
|
21
|
+
export function wrapManaged(coreBlock) {
|
|
22
|
+
return `${MANAGED_BEGIN}\n${defangMarkerLines(coreBlock)}\n${MANAGED_END}`;
|
|
23
|
+
}
|
|
24
|
+
/** All well-formed [begin,end] line-index regions, paired greedily. */
|
|
25
|
+
function findRegions(lines) {
|
|
26
|
+
const regions = [];
|
|
27
|
+
let i = 0;
|
|
28
|
+
while (i < lines.length) {
|
|
29
|
+
if (isMarker(lines[i], MANAGED_BEGIN)) {
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j < lines.length && !isMarker(lines[j], MANAGED_END))
|
|
32
|
+
j++;
|
|
33
|
+
if (j < lines.length) {
|
|
34
|
+
regions.push([i, j]);
|
|
35
|
+
i = j + 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return regions;
|
|
42
|
+
}
|
|
43
|
+
/** Count of lines that are exactly a begin or end marker. */
|
|
44
|
+
function markerLineCount(lines) {
|
|
45
|
+
return lines.filter((l) => isMarker(l, MANAGED_BEGIN) || isMarker(l, MANAGED_END)).length;
|
|
46
|
+
}
|
|
47
|
+
/** Remove every well-formed managed region and any stray marker lines; preserve the rest. */
|
|
48
|
+
export function stripManagedBlocks(content) {
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
const out = [];
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
if (isMarker(lines[i], MANAGED_BEGIN)) {
|
|
53
|
+
let j = i + 1;
|
|
54
|
+
while (j < lines.length && !isMarker(lines[j], MANAGED_END))
|
|
55
|
+
j++;
|
|
56
|
+
if (j < lines.length) {
|
|
57
|
+
i = j;
|
|
58
|
+
continue;
|
|
59
|
+
} // drop a full region
|
|
60
|
+
continue; // dangling begin: drop just this line
|
|
61
|
+
}
|
|
62
|
+
if (isMarker(lines[i], MANAGED_END))
|
|
63
|
+
continue; // stray end: drop just this line
|
|
64
|
+
out.push(lines[i]);
|
|
65
|
+
}
|
|
66
|
+
return out.join('\n');
|
|
67
|
+
}
|
|
68
|
+
/** The single well-formed managed region (markers inclusive), or null. */
|
|
69
|
+
export function extractManagedBlock(content) {
|
|
70
|
+
const lines = content.split('\n');
|
|
71
|
+
const regions = findRegions(lines);
|
|
72
|
+
if (regions.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const [b, e] = regions[0];
|
|
75
|
+
return lines.slice(b, e + 1).join('\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Insert or replace the managed block in `existing`, preserving everything outside it.
|
|
79
|
+
*
|
|
80
|
+
* - null/empty existing → fresh file: `# <title>` heading + the managed block.
|
|
81
|
+
* - exactly one clean region → replace it IN PLACE (outside bytes preserved exactly).
|
|
82
|
+
* - malformed (dangling/reversed/duplicate/stray markers) → clean all of them and append a
|
|
83
|
+
* single canonical block; subsequent runs see one clean region and are fully idempotent.
|
|
84
|
+
*
|
|
85
|
+
* `managed` must be the output of wrapManaged() (markers included).
|
|
86
|
+
*/
|
|
87
|
+
export function upsertManagedBlock(existing, title, managed) {
|
|
88
|
+
if (existing == null || existing.trim() === '') {
|
|
89
|
+
return `# ${title}\n\n${managed}\n`;
|
|
90
|
+
}
|
|
91
|
+
const lines = existing.split('\n');
|
|
92
|
+
const regions = findRegions(lines);
|
|
93
|
+
const healthySingle = regions.length === 1 && markerLineCount(lines) === 2;
|
|
94
|
+
if (healthySingle) {
|
|
95
|
+
const [b, e] = regions[0];
|
|
96
|
+
return [...lines.slice(0, b), ...managed.split('\n'), ...lines.slice(e + 1)].join('\n');
|
|
97
|
+
}
|
|
98
|
+
// malformed or empty-of-user-text: rebuild cleanly without losing content
|
|
99
|
+
const body = stripManagedBlocks(existing).replace(/\s*$/, '');
|
|
100
|
+
return `${body === '' ? `# ${title}` : body}\n\n${managed}\n`;
|
|
101
|
+
}
|
package/dist/mcpRegister.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
// Apply step of the MCP registration flow (§6.3: detect -> propose -> APPLY approved -> verify).
|
|
2
|
-
// Pure, idempotent config-merge writers that ADD framein's own MCP server into each CLI's config
|
|
3
|
-
// without clobbering anything else (same safety contract as managedBlock, ADR-0007 B-1). The
|
|
4
|
-
// actual file write + live `claude mcp list` spawn live in cli.ts; only the parser below is pure.
|
|
5
|
-
// Zero-dep: JSON via JSON.parse/stringify, TOML via a line-based text merge (no TOML serializer).
|
|
6
|
-
/** framein registers itself as a subprocess MCP server each CLI launches (ADR-0007). */
|
|
7
|
-
export const FRAMEIN_ENTRY = { command: 'framein', args: ['mcp', 'serve'] };
|
|
8
|
-
/**
|
|
9
|
-
* Pick the spawn command the agent CLIs should use to launch framein's MCP server: the canonical
|
|
10
|
-
* `framein` bin when it's on PATH (installed product), else `node <abs cli.js>` (dev / not globally
|
|
11
|
-
* installed). The agent spawns this verbatim, so it MUST resolve to a real executable.
|
|
12
|
-
*/
|
|
13
|
-
export function resolveFrameinEntry(frameOnPath, cliPath) {
|
|
14
|
-
return frameOnPath ? { command: 'framein', args: ['mcp', 'serve'] } : { command: 'node', args: [cliPath, 'mcp', 'serve'] };
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Merge `mcpServers.<name>` into a JSON config (Claude `.mcp.json`, Gemini `settings.json`),
|
|
18
|
-
* preserving every other key and server. Idempotent. Reformats with 2-space indent.
|
|
19
|
-
*/
|
|
20
|
-
export function applyJsonMcp(existing, name = 'framein', entry = FRAMEIN_ENTRY) {
|
|
21
|
-
let root = {};
|
|
22
|
-
if (existing && existing.trim()) {
|
|
23
|
-
const parsed = JSON.parse(existing);
|
|
24
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
25
|
-
root = parsed;
|
|
26
|
-
}
|
|
27
|
-
const prev = root.mcpServers;
|
|
28
|
-
const servers = prev && typeof prev === 'object' && !Array.isArray(prev) ? prev : {};
|
|
29
|
-
servers[name] = { command: entry.command, args: entry.args };
|
|
30
|
-
root.mcpServers = servers;
|
|
31
|
-
return JSON.stringify(root, null, 2) + '\n';
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Merge a `[mcp_servers.<name>]` table into a Codex `config.toml` as text: replace the existing
|
|
35
|
-
* block (header line through the next table header / EOF) if present, else append. Idempotent.
|
|
36
|
-
* Preserves all other content; only the framein table is rewritten.
|
|
37
|
-
*/
|
|
38
|
-
export function applyCodexMcp(existing, name = 'framein', entry = FRAMEIN_ENTRY) {
|
|
39
|
-
const header = `[mcp_servers.${name}]`;
|
|
40
|
-
const block = [
|
|
41
|
-
header,
|
|
42
|
-
`command = ${JSON.stringify(entry.command)}`,
|
|
43
|
-
`args = [${entry.args.map((a) => JSON.stringify(a)).join(', ')}]`,
|
|
44
|
-
];
|
|
45
|
-
const text = existing ?? '';
|
|
46
|
-
const lines = text.split('\n');
|
|
47
|
-
const start = lines.findIndex((l) => l.trim() === header);
|
|
48
|
-
if (start === -1) {
|
|
49
|
-
const base = text.trim();
|
|
50
|
-
return (base ? base + '\n\n' : '') + block.join('\n') + '\n';
|
|
51
|
-
}
|
|
52
|
-
// Block ends at the next table header (any `[...]`) or EOF.
|
|
53
|
-
let end = lines.length;
|
|
54
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
55
|
-
if (/^\s*\[/.test(lines[i])) {
|
|
56
|
-
end = i;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const before = lines.slice(0, start);
|
|
61
|
-
const after = lines.slice(end);
|
|
62
|
-
const sep = after.length && after[0].trim() !== '' ? [''] : []; // keep a blank line before a following table
|
|
63
|
-
return [...before, ...block, ...sep, ...after].join('\n').replace(/\n*$/, '\n');
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Read the connection state of a server from `claude mcp list` output. `connected`/`failed` when
|
|
67
|
-
* a health marker (✓/✗) is shown, `registered` when listed without one, `absent` when not found.
|
|
68
|
-
* (The verify step; the spawn that produces `output` is the live B-layer piece.)
|
|
69
|
-
*/
|
|
70
|
-
export function parseClaudeMcpList(output, name = 'framein') {
|
|
71
|
-
for (const raw of output.split('\n')) {
|
|
72
|
-
const line = raw.trim();
|
|
73
|
-
const colon = line.indexOf(':');
|
|
74
|
-
if (colon === -1)
|
|
75
|
-
continue;
|
|
76
|
-
if (line.slice(0, colon).trim() !== name)
|
|
77
|
-
continue;
|
|
78
|
-
if (/✗|✘|fail/i.test(line))
|
|
79
|
-
return 'failed';
|
|
80
|
-
if (/✓|✔|connected/i.test(line))
|
|
81
|
-
return 'connected';
|
|
82
|
-
return 'registered';
|
|
83
|
-
}
|
|
84
|
-
return 'absent';
|
|
85
|
-
}
|
|
1
|
+
// Apply step of the MCP registration flow (§6.3: detect -> propose -> APPLY approved -> verify).
|
|
2
|
+
// Pure, idempotent config-merge writers that ADD framein's own MCP server into each CLI's config
|
|
3
|
+
// without clobbering anything else (same safety contract as managedBlock, ADR-0007 B-1). The
|
|
4
|
+
// actual file write + live `claude mcp list` spawn live in cli.ts; only the parser below is pure.
|
|
5
|
+
// Zero-dep: JSON via JSON.parse/stringify, TOML via a line-based text merge (no TOML serializer).
|
|
6
|
+
/** framein registers itself as a subprocess MCP server each CLI launches (ADR-0007). */
|
|
7
|
+
export const FRAMEIN_ENTRY = { command: 'framein', args: ['mcp', 'serve'] };
|
|
8
|
+
/**
|
|
9
|
+
* Pick the spawn command the agent CLIs should use to launch framein's MCP server: the canonical
|
|
10
|
+
* `framein` bin when it's on PATH (installed product), else `node <abs cli.js>` (dev / not globally
|
|
11
|
+
* installed). The agent spawns this verbatim, so it MUST resolve to a real executable.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveFrameinEntry(frameOnPath, cliPath) {
|
|
14
|
+
return frameOnPath ? { command: 'framein', args: ['mcp', 'serve'] } : { command: 'node', args: [cliPath, 'mcp', 'serve'] };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Merge `mcpServers.<name>` into a JSON config (Claude `.mcp.json`, Gemini `settings.json`),
|
|
18
|
+
* preserving every other key and server. Idempotent. Reformats with 2-space indent.
|
|
19
|
+
*/
|
|
20
|
+
export function applyJsonMcp(existing, name = 'framein', entry = FRAMEIN_ENTRY) {
|
|
21
|
+
let root = {};
|
|
22
|
+
if (existing && existing.trim()) {
|
|
23
|
+
const parsed = JSON.parse(existing);
|
|
24
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
25
|
+
root = parsed;
|
|
26
|
+
}
|
|
27
|
+
const prev = root.mcpServers;
|
|
28
|
+
const servers = prev && typeof prev === 'object' && !Array.isArray(prev) ? prev : {};
|
|
29
|
+
servers[name] = { command: entry.command, args: entry.args };
|
|
30
|
+
root.mcpServers = servers;
|
|
31
|
+
return JSON.stringify(root, null, 2) + '\n';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Merge a `[mcp_servers.<name>]` table into a Codex `config.toml` as text: replace the existing
|
|
35
|
+
* block (header line through the next table header / EOF) if present, else append. Idempotent.
|
|
36
|
+
* Preserves all other content; only the framein table is rewritten.
|
|
37
|
+
*/
|
|
38
|
+
export function applyCodexMcp(existing, name = 'framein', entry = FRAMEIN_ENTRY) {
|
|
39
|
+
const header = `[mcp_servers.${name}]`;
|
|
40
|
+
const block = [
|
|
41
|
+
header,
|
|
42
|
+
`command = ${JSON.stringify(entry.command)}`,
|
|
43
|
+
`args = [${entry.args.map((a) => JSON.stringify(a)).join(', ')}]`,
|
|
44
|
+
];
|
|
45
|
+
const text = existing ?? '';
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
48
|
+
if (start === -1) {
|
|
49
|
+
const base = text.trim();
|
|
50
|
+
return (base ? base + '\n\n' : '') + block.join('\n') + '\n';
|
|
51
|
+
}
|
|
52
|
+
// Block ends at the next table header (any `[...]`) or EOF.
|
|
53
|
+
let end = lines.length;
|
|
54
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
55
|
+
if (/^\s*\[/.test(lines[i])) {
|
|
56
|
+
end = i;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const before = lines.slice(0, start);
|
|
61
|
+
const after = lines.slice(end);
|
|
62
|
+
const sep = after.length && after[0].trim() !== '' ? [''] : []; // keep a blank line before a following table
|
|
63
|
+
return [...before, ...block, ...sep, ...after].join('\n').replace(/\n*$/, '\n');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read the connection state of a server from `claude mcp list` output. `connected`/`failed` when
|
|
67
|
+
* a health marker (✓/✗) is shown, `registered` when listed without one, `absent` when not found.
|
|
68
|
+
* (The verify step; the spawn that produces `output` is the live B-layer piece.)
|
|
69
|
+
*/
|
|
70
|
+
export function parseClaudeMcpList(output, name = 'framein') {
|
|
71
|
+
for (const raw of output.split('\n')) {
|
|
72
|
+
const line = raw.trim();
|
|
73
|
+
const colon = line.indexOf(':');
|
|
74
|
+
if (colon === -1)
|
|
75
|
+
continue;
|
|
76
|
+
if (line.slice(0, colon).trim() !== name)
|
|
77
|
+
continue;
|
|
78
|
+
if (/✗|✘|fail/i.test(line))
|
|
79
|
+
return 'failed';
|
|
80
|
+
if (/✓|✔|connected/i.test(line))
|
|
81
|
+
return 'connected';
|
|
82
|
+
return 'registered';
|
|
83
|
+
}
|
|
84
|
+
return 'absent';
|
|
85
|
+
}
|