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/dist/db.js CHANGED
@@ -1,7 +1,7 @@
1
- // Thin typed facade over the experimental built-in node:sqlite.
2
- // Declaring our own minimal surface decouples us from @types/node version drift.
3
- // @ts-ignore - node:sqlite is experimental; type defs may be absent.
4
- import { DatabaseSync } from 'node:sqlite';
5
- export function openDb(path) {
6
- return new DatabaseSync(path);
7
- }
1
+ // Thin typed facade over the experimental built-in node:sqlite.
2
+ // Declaring our own minimal surface decouples us from @types/node version drift.
3
+ // @ts-ignore - node:sqlite is experimental; type defs may be absent.
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ export function openDb(path) {
6
+ return new DatabaseSync(path);
7
+ }
package/dist/debt.js CHANGED
@@ -1,42 +1,42 @@
1
- // Vibe Debt Delta (F-LOOP-9, ADR-0008): show the debt THIS change added — not the codebase's
2
- // hundreds of pre-existing warnings. Pure: parse a unified git diff into a small delta. Heuristic
3
- // by design (a hint, not a linter); reading the diff (git) lives in cli.ts.
4
- import { PLAIN } from './ui/theme.js';
5
- export function parseDiffDebt(diff) {
6
- let addedLines = 0, removedLines = 0, todos = 0;
7
- const addedDeps = [];
8
- let curFile = '';
9
- for (const line of (diff ?? '').split('\n')) {
10
- if (line.startsWith('+++ ')) {
11
- curFile = line.replace(/^\+\+\+ (b\/)?/, '').trim();
12
- continue;
13
- }
14
- if (line.startsWith('--- ') || line.startsWith('@@') || line.startsWith('diff '))
15
- continue;
16
- if (line.startsWith('+')) {
17
- addedLines++;
18
- if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line))
19
- todos++;
20
- if (/package\.json$/.test(curFile)) {
21
- const m = line.match(/^\+\s*"([^"]+)":\s*"[~^]?\d/); // "pkg": "^1.2.3" style additions
22
- if (m)
23
- addedDeps.push(m[1]);
24
- }
25
- }
26
- else if (line.startsWith('-')) {
27
- removedLines++;
28
- }
29
- }
30
- return { addedLines, removedLines, addedDeps, todos };
31
- }
32
- export function renderDebt(d, ui = PLAIN) {
33
- const lines = [ui.tone('Debt delta (this change only):', 'muted')];
34
- if (d.addedDeps.length)
35
- lines.push(ui.tone(` + ${d.addedDeps.length} runtime dependency${d.addedDeps.length > 1 ? '(ies)' : ''}: ${d.addedDeps.join(', ')}`, 'warning'));
36
- if (d.todos)
37
- lines.push(ui.tone(` + ${d.todos} TODO/FIXME`, 'warning'));
38
- lines.push(` ~ ${d.addedLines} added / ${d.removedLines} removed lines`);
39
- if (!d.addedDeps.length && !d.todos)
40
- lines.push(ui.tone(' (no new deps or TODOs)', 'success'));
41
- return lines.join('\n');
42
- }
1
+ // Vibe Debt Delta (F-LOOP-9, ADR-0008): show the debt THIS change added — not the codebase's
2
+ // hundreds of pre-existing warnings. Pure: parse a unified git diff into a small delta. Heuristic
3
+ // by design (a hint, not a linter); reading the diff (git) lives in cli.ts.
4
+ import { PLAIN } from './ui/theme.js';
5
+ export function parseDiffDebt(diff) {
6
+ let addedLines = 0, removedLines = 0, todos = 0;
7
+ const addedDeps = [];
8
+ let curFile = '';
9
+ for (const line of (diff ?? '').split('\n')) {
10
+ if (line.startsWith('+++ ')) {
11
+ curFile = line.replace(/^\+\+\+ (b\/)?/, '').trim();
12
+ continue;
13
+ }
14
+ if (line.startsWith('--- ') || line.startsWith('@@') || line.startsWith('diff '))
15
+ continue;
16
+ if (line.startsWith('+')) {
17
+ addedLines++;
18
+ if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line))
19
+ todos++;
20
+ if (/package\.json$/.test(curFile)) {
21
+ const m = line.match(/^\+\s*"([^"]+)":\s*"[~^]?\d/); // "pkg": "^1.2.3" style additions
22
+ if (m)
23
+ addedDeps.push(m[1]);
24
+ }
25
+ }
26
+ else if (line.startsWith('-')) {
27
+ removedLines++;
28
+ }
29
+ }
30
+ return { addedLines, removedLines, addedDeps, todos };
31
+ }
32
+ export function renderDebt(d, ui = PLAIN) {
33
+ const lines = [ui.tone('Debt delta (this change only):', 'muted')];
34
+ if (d.addedDeps.length)
35
+ lines.push(ui.tone(` + ${d.addedDeps.length} runtime dependency${d.addedDeps.length > 1 ? '(ies)' : ''}: ${d.addedDeps.join(', ')}`, 'warning'));
36
+ if (d.todos)
37
+ lines.push(ui.tone(` + ${d.todos} TODO/FIXME`, 'warning'));
38
+ lines.push(` ~ ${d.addedLines} added / ${d.removedLines} removed lines`);
39
+ if (!d.addedDeps.length && !d.todos)
40
+ lines.push(ui.tone(' (no new deps or TODOs)', 'success'));
41
+ return lines.join('\n');
42
+ }
package/dist/delegate.js CHANGED
@@ -1,64 +1,71 @@
1
- // Headless delegation (ADR-0007 B-2). The PRIMARY way framein pulls another role into a session:
2
- // drive each CLI's non-interactive print mode over child_process pipes — NO PTY. This sidesteps the
3
- // Windows ConPTY/node-pty risk and keeps zero runtime deps. Human-in-the-loop attach uses Node's
4
- // built-in `stdio:'inherit'` (interactiveCommand below) — a programmatic PTY (node-pty) is
5
- // deliberately not used (ADR-0009). These builders are pure; the spawn lives in cli.ts and is
6
- // live-verified against real claude/codex/gemini.
7
- import { DEFAULT_ROLE_PRIORITY } from './roles.js';
8
- /**
9
- * Non-interactive one-shot invocation for each agent (prompt via stdin, response on stdout).
10
- * `opts.trustFlags` (from trustPlan, F-TRUST) are FIXED per-agent permission-bypass flags appended
11
- * to argv — still no user input there, so it stays shell-safe under shell:true.
12
- */
13
- export function buildInvocation(agent, prompt, opts = {}) {
14
- const trust = opts.trustFlags ?? [];
15
- switch (agent) {
16
- case 'claude': return { command: 'claude', args: ['-p', ...trust], stdin: prompt };
17
- case 'codex': return { command: 'codex', args: ['exec', '--skip-git-repo-check', ...trust], stdin: prompt }; // exec refuses untrusted/non-git dirs otherwise
18
- // gemini -p takes the prompt as a value and APPENDS stdin; `--prompt=` (empty) + stdin keeps the
19
- // prompt off argv (shell-safe), `--skip-trust` is required for headless untrusted dirs. Verified.
20
- case 'gemini': return { command: 'gemini', args: ['--prompt=', '--skip-trust', ...trust], stdin: prompt };
21
- default: {
22
- const _exhaustive = agent;
23
- throw new Error(`unknown agent: ${String(_exhaustive)}`);
24
- }
25
- }
26
- }
27
- /** Which agent runs a role: the explicit assignment, else the role's default-priority head. */
28
- export function resolveAgent(roles, role) {
29
- return roles[role] ?? DEFAULT_ROLE_PRIORITY[role][0];
30
- }
31
- /** The fixed shell command (flags only, no user input) that runDelegated runs with shell:true. */
32
- export function invocationCommand(inv) {
33
- return [inv.command, ...inv.args].join(' ');
34
- }
35
- /**
36
- * The bare interactive command for an agent — launched with stdio:'inherit' so the human drives the
37
- * agent's own TUI directly, inside the already-synced frame (ADR-0007 B-2 interactive path, zero-dep).
38
- * A true programmatic PTY (read/inject/resize the agent's terminal) would need node-pty a native
39
- * runtime dependency that breaks framein's zero-dep invariant (ADR-0003)and framein observes via
40
- * the store/ledger, not by screen-scraping a TTY, so it is deliberately NOT used.
41
- */
42
- // `resume` re-enters the agent's MOST RECENT session in this cwd (handoff continuity, F-CAPSULE/ADR-0009):
43
- // claude `--continue`, codex `resume --last`, gemini `--resume`. framein decides re-entry from its own
44
- // ledger (a prior enter/return for this agent) it never scrapes the printed session id (ADR-0009).
45
- export function interactiveCommand(agent, resume = false, trustFlags = []) {
46
- // F-TRUST placement: codex `resume` is a SUBCOMMAND, so top-level bypass flags must come BEFORE it
47
- // (`codex --full-auto resume --last`); appending after the subcommand makes codex reject them. claude
48
- // `--continue` and gemini `--resume` are plain flags, so trust flags can follow.
49
- const t = trustFlags.length ? ` ${trustFlags.join(' ')}` : '';
50
- switch (agent) {
51
- case 'claude': return `claude${resume ? ' --continue' : ''}${t}`;
52
- case 'codex': return `codex${t}${resume ? ' resume --last' : ''}`;
53
- case 'gemini': return `gemini${resume ? ' --resume' : ''}${t}`;
54
- default: {
55
- const _exhaustive = agent;
56
- throw new Error(`unknown agent: ${String(_exhaustive)}`);
57
- }
58
- }
59
- }
60
- /** Human-readable preview for `--show`: the fixed command + a peek at the stdin prompt. */
61
- export function renderInvocation(inv) {
62
- const peek = inv.stdin.length > 60 ? inv.stdin.slice(0, 60) + '…' : inv.stdin;
63
- return `${invocationCommand(inv)} ⟵ stdin: ${JSON.stringify(peek)}`;
64
- }
1
+ // Headless delegation (ADR-0007 B-2). The PRIMARY way framein pulls another role into a session:
2
+ // drive each CLI's non-interactive print mode over child_process pipes — NO PTY. This sidesteps the
3
+ // Windows ConPTY/node-pty risk and keeps zero runtime deps. Human-in-the-loop attach uses Node's
4
+ // built-in `stdio:'inherit'` (interactiveCommand below) — a programmatic PTY (node-pty) is
5
+ // deliberately not used (ADR-0009). These builders are pure; the spawn lives in cli.ts and is
6
+ // live-verified against real claude/codex/gemini.
7
+ import { DEFAULT_ROLE_PRIORITY } from './roles.js';
8
+ export const HANDOFF_START_PROMPT = 'Framein handoff: run `framein capsule` first, restate the current task contract briefly, then continue from the local facts. Do not ask the user to re-explain context.';
9
+ /**
10
+ * Non-interactive one-shot invocation for each agent (prompt via stdin, response on stdout).
11
+ * `opts.trustFlags` (from trustPlan, F-TRUST) are FIXED per-agent permission-bypass flags appended
12
+ * to argv — still no user input there, so it stays shell-safe under shell:true.
13
+ */
14
+ export function buildInvocation(agent, prompt, opts = {}) {
15
+ const trust = opts.trustFlags ?? [];
16
+ switch (agent) {
17
+ case 'claude': return { command: 'claude', args: ['-p', ...trust], stdin: prompt };
18
+ case 'codex': return { command: 'codex', args: ['exec', '--skip-git-repo-check', ...trust], stdin: prompt }; // exec refuses untrusted/non-git dirs otherwise
19
+ // gemini -p takes the prompt as a value and APPENDS stdin; `--prompt=` (empty) + stdin keeps the
20
+ // prompt off argv (shell-safe), `--skip-trust` is required for headless untrusted dirs. Verified.
21
+ case 'gemini': return { command: 'gemini', args: ['--prompt=', '--skip-trust', ...trust], stdin: prompt };
22
+ default: {
23
+ const _exhaustive = agent;
24
+ throw new Error(`unknown agent: ${String(_exhaustive)}`);
25
+ }
26
+ }
27
+ }
28
+ /** Which agent runs a role: the explicit assignment, else the role's default-priority head. */
29
+ export function resolveAgent(roles, role) {
30
+ return roles[role] ?? DEFAULT_ROLE_PRIORITY[role][0];
31
+ }
32
+ /** The fixed shell command (flags only, no user input) that runDelegated runs with shell:true. */
33
+ export function invocationCommand(inv) {
34
+ return [inv.command, ...inv.args].join(' ');
35
+ }
36
+ /**
37
+ * The bare interactive command for an agent launched with stdio:'inherit' so the human drives the
38
+ * agent's own TUI directly, inside the already-synced frame (ADR-0007 B-2 interactive path, zero-dep).
39
+ * A true programmatic PTY (read/inject/resize the agent's terminal) would need node-ptya native
40
+ * runtime dependency that breaks framein's zero-dep invariant (ADR-0003) and framein observes via
41
+ * the store/ledger, not by screen-scraping a TTY, so it is deliberately NOT used.
42
+ */
43
+ // `resume` re-enters the agent's MOST RECENT session in this cwd (handoff continuity, F-CAPSULE/ADR-0009):
44
+ // claude `--continue`, codex `resume --last`, gemini `--resume`. framein decides re-entry from its own
45
+ // ledger (a prior enter/return for this agent) it never scrapes the printed session id (ADR-0009).
46
+ function shellQuote(arg) {
47
+ if (process.platform === 'win32')
48
+ return `"${arg.replace(/"/g, '\\"')}"`;
49
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
50
+ }
51
+ export function interactiveCommand(agent, resume = false, trustFlags = [], initialPrompt) {
52
+ // F-TRUST placement: codex `resume` is a SUBCOMMAND, so top-level bypass flags must come BEFORE it
53
+ // (`codex --full-auto resume --last`); appending after the subcommand makes codex reject them. claude
54
+ // `--continue` and gemini `--resume` are plain flags, so trust flags can follow.
55
+ const t = trustFlags.length ? ` ${trustFlags.join(' ')}` : '';
56
+ const p = initialPrompt ? ` ${shellQuote(initialPrompt)}` : '';
57
+ switch (agent) {
58
+ case 'claude': return `claude${resume ? ' --continue' : ''}${t}${p}`;
59
+ case 'codex': return `codex${t}${resume ? ' resume --last' : ''}${p}`;
60
+ case 'gemini': return `gemini${resume ? ' --resume' : ''}${t}${initialPrompt ? ` --prompt-interactive${p}` : ''}`;
61
+ default: {
62
+ const _exhaustive = agent;
63
+ throw new Error(`unknown agent: ${String(_exhaustive)}`);
64
+ }
65
+ }
66
+ }
67
+ /** Human-readable preview for `--show`: the fixed command + a peek at the stdin prompt. */
68
+ export function renderInvocation(inv) {
69
+ const peek = inv.stdin.length > 60 ? inv.stdin.slice(0, 60) + '…' : inv.stdin;
70
+ return `${invocationCommand(inv)} ⟵ stdin: ${JSON.stringify(peek)}`;
71
+ }
package/dist/detect.js CHANGED
@@ -1,118 +1,118 @@
1
- // Reuse-first (ADR-0002/0004): DETECT each agent's existing MCP servers and skills and
2
- // surface them; never proxy or reimplement them. Parsers are pure (fixture-testable);
3
- // the disk layer is best-effort and swallows missing/malformed files.
4
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
- import { homedir } from 'node:os';
6
- import { join } from 'node:path';
7
- // --- pure parsers (one per CLI's config format) ---
8
- /** Claude project `.mcp.json`: { "mcpServers": { name: { command, args } } } */
9
- export function parseClaudeMcpJson(text) {
10
- const servers = (JSON.parse(text)?.mcpServers ?? {});
11
- return Object.entries(servers).map(([name, def]) => ({ agent: 'claude', name, command: def?.command ?? '' }));
12
- }
13
- /** Gemini `settings.json`: { "mcpServers": { name: { command, args } } } */
14
- export function parseGeminiMcpJson(text) {
15
- const servers = (JSON.parse(text)?.mcpServers ?? {});
16
- return Object.entries(servers).map(([name, def]) => ({ agent: 'gemini', name, command: def?.command ?? '' }));
17
- }
18
- /**
19
- * Codex `~/.codex/config.toml`: top-level `[mcp_servers.<name>]` tables (sub-tables like
20
- * `[mcp_servers.<name>.env]` are NOT servers). Minimal line-based reader (zero-dep).
21
- */
22
- export function parseCodexMcpToml(text) {
23
- const out = [];
24
- let cur = null;
25
- for (const raw of text.split('\n')) {
26
- // strip a trailing ` # comment` (best effort: not quote-aware, but tolerates the common case)
27
- const line = raw.replace(/\s+#.*$/, '').trim();
28
- const sec = line.match(/^\[mcp_servers\.([^.\]]+)\]$/);
29
- if (sec) {
30
- cur = { agent: 'codex', name: sec[1], command: '' };
31
- out.push(cur);
32
- continue;
33
- }
34
- if (line.startsWith('[')) {
35
- cur = null;
36
- continue;
37
- } // any other section ends the current server
38
- if (cur) {
39
- const m = line.match(/^command\s*=\s*['"](.*?)['"]\s*$/);
40
- if (m)
41
- cur.command = m[1];
42
- }
43
- }
44
- return out;
45
- }
46
- /** Server names configured for more than one agent (surfaced, not auto-resolved). */
47
- export function findConflicts(servers) {
48
- const byName = new Map();
49
- for (const s of servers) {
50
- if (!byName.has(s.name))
51
- byName.set(s.name, new Set());
52
- byName.get(s.name).add(s.agent);
53
- }
54
- return [...byName.entries()].filter(([, agents]) => agents.size > 1).map(([name]) => name).sort();
55
- }
56
- /**
57
- * The config patches that would register framein's OWN MCP server into each CLI. Generated
58
- * for the user to apply after approval (§6.3) — framein does not write them automatically.
59
- */
60
- export function frameinMcpRegistration(command = 'framein', args = ['mcp', 'serve']) {
61
- const json = JSON.stringify({ mcpServers: { framein: { command, args } } }, null, 2);
62
- const codex = `[mcp_servers.framein]\ncommand = ${JSON.stringify(command)}\nargs = [${args.map((a) => JSON.stringify(a)).join(', ')}]`;
63
- return { claude: json, codex, gemini: json };
64
- }
65
- export const FRAMEIN_SKILLS = [
66
- { source: 'framein', name: 'adr-flow', description: 'record a decision as an ADR and re-sync all agents' },
67
- { source: 'framein', name: 'delegate', description: 'hand a task to another role; it reads the shared store' },
68
- { source: 'framein', name: 'cross-review', description: 'ask the reviewer role to audit the current change' },
69
- ];
70
- /** Parse the `name`/`description` from a SKILL.md YAML-ish frontmatter block. */
71
- export function parseSkillFrontmatter(md) {
72
- const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
73
- if (!fm)
74
- return {};
75
- const name = fm[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
76
- const description = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
77
- return { name, description };
78
- }
79
- // --- best-effort disk layer ---
80
- function tryParse(path, parse, into) {
81
- try {
82
- if (existsSync(path))
83
- into.push(...parse(readFileSync(path, 'utf8')));
84
- }
85
- catch { /* ignore malformed/missing */ }
86
- }
87
- export function detectMcpFromDisk(opts = {}) {
88
- const cwd = opts.cwd ?? process.cwd();
89
- const home = opts.home ?? homedir();
90
- const servers = [];
91
- tryParse(join(cwd, '.mcp.json'), parseClaudeMcpJson, servers);
92
- tryParse(join(home, '.codex', 'config.toml'), parseCodexMcpToml, servers);
93
- tryParse(join(home, '.gemini', 'settings.json'), parseGeminiMcpJson, servers);
94
- tryParse(join(cwd, '.gemini', 'settings.json'), parseGeminiMcpJson, servers);
95
- return servers;
96
- }
97
- export function detectSkillsFromDisk(opts = {}) {
98
- const cwd = opts.cwd ?? process.cwd();
99
- const home = opts.home ?? homedir();
100
- const out = [];
101
- for (const base of [join(cwd, '.claude', 'skills'), join(home, '.claude', 'skills')]) {
102
- try {
103
- if (!existsSync(base))
104
- continue;
105
- for (const entry of readdirSync(base, { withFileTypes: true })) {
106
- if (!entry.isDirectory())
107
- continue;
108
- const md = join(base, entry.name, 'SKILL.md');
109
- if (!existsSync(md))
110
- continue;
111
- const { name, description } = parseSkillFrontmatter(readFileSync(md, 'utf8'));
112
- out.push({ source: 'claude', name: name ?? entry.name, description: description ?? '' });
113
- }
114
- }
115
- catch { /* ignore */ }
116
- }
117
- return out;
118
- }
1
+ // Reuse-first (ADR-0002/0004): DETECT each agent's existing MCP servers and skills and
2
+ // surface them; never proxy or reimplement them. Parsers are pure (fixture-testable);
3
+ // the disk layer is best-effort and swallows missing/malformed files.
4
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ // --- pure parsers (one per CLI's config format) ---
8
+ /** Claude project `.mcp.json`: { "mcpServers": { name: { command, args } } } */
9
+ export function parseClaudeMcpJson(text) {
10
+ const servers = (JSON.parse(text)?.mcpServers ?? {});
11
+ return Object.entries(servers).map(([name, def]) => ({ agent: 'claude', name, command: def?.command ?? '' }));
12
+ }
13
+ /** Gemini `settings.json`: { "mcpServers": { name: { command, args } } } */
14
+ export function parseGeminiMcpJson(text) {
15
+ const servers = (JSON.parse(text)?.mcpServers ?? {});
16
+ return Object.entries(servers).map(([name, def]) => ({ agent: 'gemini', name, command: def?.command ?? '' }));
17
+ }
18
+ /**
19
+ * Codex `~/.codex/config.toml`: top-level `[mcp_servers.<name>]` tables (sub-tables like
20
+ * `[mcp_servers.<name>.env]` are NOT servers). Minimal line-based reader (zero-dep).
21
+ */
22
+ export function parseCodexMcpToml(text) {
23
+ const out = [];
24
+ let cur = null;
25
+ for (const raw of text.split('\n')) {
26
+ // strip a trailing ` # comment` (best effort: not quote-aware, but tolerates the common case)
27
+ const line = raw.replace(/\s+#.*$/, '').trim();
28
+ const sec = line.match(/^\[mcp_servers\.([^.\]]+)\]$/);
29
+ if (sec) {
30
+ cur = { agent: 'codex', name: sec[1], command: '' };
31
+ out.push(cur);
32
+ continue;
33
+ }
34
+ if (line.startsWith('[')) {
35
+ cur = null;
36
+ continue;
37
+ } // any other section ends the current server
38
+ if (cur) {
39
+ const m = line.match(/^command\s*=\s*['"](.*?)['"]\s*$/);
40
+ if (m)
41
+ cur.command = m[1];
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ /** Server names configured for more than one agent (surfaced, not auto-resolved). */
47
+ export function findConflicts(servers) {
48
+ const byName = new Map();
49
+ for (const s of servers) {
50
+ if (!byName.has(s.name))
51
+ byName.set(s.name, new Set());
52
+ byName.get(s.name).add(s.agent);
53
+ }
54
+ return [...byName.entries()].filter(([, agents]) => agents.size > 1).map(([name]) => name).sort();
55
+ }
56
+ /**
57
+ * The config patches that would register framein's OWN MCP server into each CLI. Generated
58
+ * for the user to apply after approval (§6.3) — framein does not write them automatically.
59
+ */
60
+ export function frameinMcpRegistration(command = 'framein', args = ['mcp', 'serve']) {
61
+ const json = JSON.stringify({ mcpServers: { framein: { command, args } } }, null, 2);
62
+ const codex = `[mcp_servers.framein]\ncommand = ${JSON.stringify(command)}\nargs = [${args.map((a) => JSON.stringify(a)).join(', ')}]`;
63
+ return { claude: json, codex, gemini: json };
64
+ }
65
+ export const FRAMEIN_SKILLS = [
66
+ { source: 'framein', name: 'adr-flow', description: 'record a decision as an ADR and re-sync all agents' },
67
+ { source: 'framein', name: 'delegate', description: 'hand a task to another role; it reads the shared store' },
68
+ { source: 'framein', name: 'cross-review', description: 'ask the reviewer role to audit the current change' },
69
+ ];
70
+ /** Parse the `name`/`description` from a SKILL.md YAML-ish frontmatter block. */
71
+ export function parseSkillFrontmatter(md) {
72
+ const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
73
+ if (!fm)
74
+ return {};
75
+ const name = fm[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
76
+ const description = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
77
+ return { name, description };
78
+ }
79
+ // --- best-effort disk layer ---
80
+ function tryParse(path, parse, into) {
81
+ try {
82
+ if (existsSync(path))
83
+ into.push(...parse(readFileSync(path, 'utf8')));
84
+ }
85
+ catch { /* ignore malformed/missing */ }
86
+ }
87
+ export function detectMcpFromDisk(opts = {}) {
88
+ const cwd = opts.cwd ?? process.cwd();
89
+ const home = opts.home ?? homedir();
90
+ const servers = [];
91
+ tryParse(join(cwd, '.mcp.json'), parseClaudeMcpJson, servers);
92
+ tryParse(join(home, '.codex', 'config.toml'), parseCodexMcpToml, servers);
93
+ tryParse(join(home, '.gemini', 'settings.json'), parseGeminiMcpJson, servers);
94
+ tryParse(join(cwd, '.gemini', 'settings.json'), parseGeminiMcpJson, servers);
95
+ return servers;
96
+ }
97
+ export function detectSkillsFromDisk(opts = {}) {
98
+ const cwd = opts.cwd ?? process.cwd();
99
+ const home = opts.home ?? homedir();
100
+ const out = [];
101
+ for (const base of [join(cwd, '.claude', 'skills'), join(home, '.claude', 'skills')]) {
102
+ try {
103
+ if (!existsSync(base))
104
+ continue;
105
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
106
+ if (!entry.isDirectory())
107
+ continue;
108
+ const md = join(base, entry.name, 'SKILL.md');
109
+ if (!existsSync(md))
110
+ continue;
111
+ const { name, description } = parseSkillFrontmatter(readFileSync(md, 'utf8'));
112
+ out.push({ source: 'claude', name: name ?? entry.name, description: description ?? '' });
113
+ }
114
+ }
115
+ catch { /* ignore */ }
116
+ }
117
+ return out;
118
+ }