peaks-cli 1.3.4 → 1.3.6

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.
Files changed (69) hide show
  1. package/dist/src/cli/commands/hook-handle.d.ts +2 -2
  2. package/dist/src/cli/commands/hook-handle.js +5 -10
  3. package/dist/src/cli/commands/hooks-commands.js +44 -29
  4. package/dist/src/cli/commands/project-commands.js +7 -1
  5. package/dist/src/cli/commands/workspace-commands.js +1 -2
  6. package/dist/src/cli/program.js +3 -4
  7. package/dist/src/services/dashboard/project-dashboard-service.d.ts +0 -7
  8. package/dist/src/services/dashboard/project-dashboard-service.js +1 -8
  9. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
  10. package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
  11. package/dist/src/services/ide/adapters/claude-code-adapter.js +0 -3
  12. package/dist/src/services/ide/adapters/trae-adapter.js +2 -17
  13. package/dist/src/services/ide/ide-types.d.ts +1 -18
  14. package/dist/src/services/progress/progress-service.d.ts +23 -103
  15. package/dist/src/services/progress/progress-service.js +24 -137
  16. package/dist/src/services/scan/file-size-scan.d.ts +4 -0
  17. package/dist/src/services/scan/file-size-scan.js +32 -3
  18. package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
  19. package/dist/src/services/skills/hooks-settings-service.js +153 -28
  20. package/dist/src/shared/incrementing-number.d.ts +0 -8
  21. package/dist/src/shared/incrementing-number.js +11 -1
  22. package/dist/src/shared/version.d.ts +1 -1
  23. package/dist/src/shared/version.js +1 -1
  24. package/package.json +1 -1
  25. package/skills/peaks-prd/SKILL.md +16 -16
  26. package/skills/peaks-prd/references/workflow.md +4 -4
  27. package/skills/peaks-qa/SKILL.md +25 -32
  28. package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
  29. package/skills/peaks-qa/references/regression-gates.md +1 -1
  30. package/skills/peaks-rd/SKILL.md +8 -21
  31. package/skills/peaks-rd/references/{openspec-mcp-cli.md → openspec-cli.md} +11 -14
  32. package/skills/peaks-solo/SKILL.md +1 -1
  33. package/skills/peaks-solo/references/a2a-artifact-mapping.md +1 -1
  34. package/skills/peaks-solo/references/browser-workflow.md +49 -38
  35. package/skills/peaks-solo/references/external-skill-invocation.md +9 -7
  36. package/skills/peaks-solo/references/{openspec-mcp-workflow.md → openspec-workflow.md} +5 -20
  37. package/skills/peaks-solo/references/runbook.md +21 -21
  38. package/skills/peaks-solo/references/sub-agent-dispatch.md +16 -35
  39. package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
  40. package/skills/peaks-ui/SKILL.md +22 -24
  41. package/skills/peaks-ui/references/workflow.md +2 -2
  42. package/dist/src/cli/commands/mcp-commands.d.ts +0 -3
  43. package/dist/src/cli/commands/mcp-commands.js +0 -144
  44. package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
  45. package/dist/src/cli/commands/progress-close-kill.js +0 -152
  46. package/dist/src/cli/commands/progress-commands.d.ts +0 -3
  47. package/dist/src/cli/commands/progress-commands.js +0 -379
  48. package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
  49. package/dist/src/cli/commands/progress-start-spawn.js +0 -140
  50. package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
  51. package/dist/src/cli/commands/progress-watch-render.js +0 -308
  52. package/dist/src/services/mcp/mcp-apply-service.d.ts +0 -31
  53. package/dist/src/services/mcp/mcp-apply-service.js +0 -112
  54. package/dist/src/services/mcp/mcp-call-service.d.ts +0 -17
  55. package/dist/src/services/mcp/mcp-call-service.js +0 -34
  56. package/dist/src/services/mcp/mcp-client-service.d.ts +0 -14
  57. package/dist/src/services/mcp/mcp-client-service.js +0 -49
  58. package/dist/src/services/mcp/mcp-install-registry.d.ts +0 -11
  59. package/dist/src/services/mcp/mcp-install-registry.js +0 -38
  60. package/dist/src/services/mcp/mcp-plan-service.d.ts +0 -29
  61. package/dist/src/services/mcp/mcp-plan-service.js +0 -109
  62. package/dist/src/services/mcp/mcp-protocol.d.ts +0 -24
  63. package/dist/src/services/mcp/mcp-protocol.js +0 -41
  64. package/dist/src/services/mcp/mcp-scan-service.d.ts +0 -8
  65. package/dist/src/services/mcp/mcp-scan-service.js +0 -214
  66. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +0 -10
  67. package/dist/src/services/mcp/mcp-stdio-transport.js +0 -50
  68. package/dist/src/services/mcp/mcp-types.d.ts +0 -31
  69. package/dist/src/services/mcp/mcp-types.js +0 -1
@@ -1,308 +0,0 @@
1
- /**
2
- * In-place progress renderer for `peaks progress watch`.
3
- *
4
- * Goals (in order of importance):
5
- * 1. **In-place overwrite.** Every tick the dynamic rows
6
- * (status line + progress bar) get rewritten, NOT
7
- * appended. We bypass `io.stdout` (which is
8
- * `console.log` and adds a trailing `\n` per call) and
9
- * write directly to `process.stdout`. The cursor-up
10
- * and erase-line escapes are the same ones terminal-kit
11
- * emits; we do not need a terminal-kit dependency for
12
- * them.
13
- * 2. **Clear PEAKS-CLI branding.** A static 3-line header
14
- * with the brand bar, the project root, and the
15
- * progress-file path is painted once. The user always
16
- * sees what they are looking at, even after the
17
- * watch loop has overwritten the dynamic rows a
18
- * thousand times.
19
- * 3. **Graceful degrade.** When stdout is not a TTY
20
- * (CI / pipe / `--json`), we fall back to a single
21
- * static snapshot per tick (no cursor moves, no
22
- * SGR colour) and a single newline. This keeps the
23
- * tool scriptable without dropping into a wall of
24
- * escape codes.
25
- *
26
- * Token-cost note: this module is rendered to the user's
27
- * terminal, never into LLM context. The watch side has
28
- * zero token cost.
29
- */
30
- import chalk from 'chalk';
31
- // ─────────────────────────────────────────────────────────────────────
32
- // Raw byte writes. We do NOT go through `io.stdout` because the
33
- // default `io.stdout` is `console.log` and appends a trailing
34
- // newline, which would defeat in-place overwrite.
35
- // ─────────────────────────────────────────────────────────────────────
36
- function rawWrite(text) {
37
- process.stdout.write(text);
38
- }
39
- /**
40
- * Number of lines that the dynamic dashboard occupies. We
41
- * rewrite exactly this many rows per tick via cursor-up +
42
- * erase-line, so the static header above and the
43
- * `press Ctrl-C` footer below stay put.
44
- */
45
- const DYNAMIC_LINES = 2;
46
- /** ANSI: cursor up N rows. */
47
- const CURSOR_UP_N = (n) => `\x1b[${n}A`;
48
- /** ANSI: erase the current row from cursor to end-of-line. */
49
- const ERASE_LINE = '\x1b[2K';
50
- /** ANSI: reset all SGR attributes. */
51
- const RESET = '\x1b[0m';
52
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
53
- const PHASE_LABEL = {
54
- starting: 'starting',
55
- running: 'running',
56
- verifying: 'verifying',
57
- completing: 'completing',
58
- finished: 'finished',
59
- failed: 'failed',
60
- idle: 'idle'
61
- };
62
- const PHASE_COLOR = {
63
- starting: chalk.cyan,
64
- running: chalk.cyan,
65
- verifying: chalk.cyan,
66
- completing: chalk.cyan,
67
- finished: chalk.green,
68
- failed: chalk.red,
69
- idle: chalk.gray
70
- };
71
- function pickSpinnerFrame(tick) {
72
- return SPINNER_FRAMES[Math.abs(tick) % SPINNER_FRAMES.length];
73
- }
74
- function formatElapsed(ms) {
75
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
76
- const hours = Math.floor(totalSeconds / 3600);
77
- const minutes = Math.floor((totalSeconds % 3600) / 60);
78
- const seconds = totalSeconds % 60;
79
- if (hours > 0) {
80
- return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
81
- }
82
- return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
83
- }
84
- /**
85
- * `elapsedMs` based fake progress, 0..1, capped at 1 after
86
- * 10 minutes. Visual cue that the watch is alive; NOT a real
87
- * percent-complete.
88
- */
89
- function computeFakeProgress(data) {
90
- if (data === null)
91
- return 0;
92
- const startedAtMs = new Date(data.current.startedAt).getTime();
93
- if (Number.isNaN(startedAtMs))
94
- return 0;
95
- const elapsedMs = Math.max(0, Date.now() - startedAtMs);
96
- const upperBoundMs = 10 * 60 * 1000;
97
- return Math.min(1, elapsedMs / upperBoundMs);
98
- }
99
- // ─────────────────────────────────────────────────────────────────────
100
- // ASCII art for the PEAKS-CLI brand bar. Kept in code (not a
101
- // dependency) so the user sees the brand even when terminal-kit
102
- // is unavailable.
103
- // ─────────────────────────────────────────────────────────────────────
104
- /**
105
- * Three-row ASCII wordmark for PEAKS-CLI. Each letter is 5
106
- * columns wide; P-E-A-K-S take 5 letters (25 cols), the
107
- * dash takes 9 cols (with surrounding space), C-L-I take 3
108
- * letters (15 cols). Total ≈ 49 cols, fits in 80-col
109
- * terminals without wrapping.
110
- *
111
- * The block-character density (full blocks on the
112
- * prominent rows) makes the brand visually dominant
113
- * against the gray "sub-agent progress watch" tagline
114
- * that sits below — so the user always knows whose
115
- * dashboard they are looking at, even when the title
116
- * bar is hidden.
117
- */
118
- const PEAKS_CLI_ASCII = [
119
- '█████ █████ █████ ██ ██ ██████ █████ ██ ███',
120
- '█ █ █ █ █ █ ██ ██ ██ ██ ██ ██ ',
121
- '█████ █████ █████ ██████ ██ ████ █████ ███'
122
- ];
123
- /**
124
- * Render the static header: the three-line big PEAKS-CLI
125
- * wordmark, a separator, a small "sub-agent progress watch"
126
- * tagline, and the project / file path. Painted ONCE at the
127
- * top of the watch, then never touched again.
128
- *
129
- * Visual hierarchy (per user feedback):
130
- * 1. PEAKS-CLI — 3-line big block art, bold cyan
131
- * 2. separator
132
- * 3. sub-agent progress watch — 1 line, smaller, gray
133
- * 4. project / path — 1 line each, gray
134
- */
135
- function renderHeader(projectRoot, progressFilePath, isTty) {
136
- if (!isTty) {
137
- // Non-TTY: emit a single header line, no colour. The
138
- // dashboard still emits 2 dynamic lines per tick, so
139
- // consumers that need to parse the output can rely on
140
- // a known line count.
141
- return [
142
- `PEAKS-CLI · sub-agent progress watch · project=${projectRoot}`,
143
- `path: ${progressFilePath}`
144
- ].join('\n') + '\n';
145
- }
146
- // PEAKS-CLI block-art rows in bold cyan (the "big" brand).
147
- const brandLines = PEAKS_CLI_ASCII.map((row) => chalk.bold.cyan(` ${row}`));
148
- // The tagline below the brand — small, gray, no big
149
- // chrome. The user sees the brand first, the
150
- // descriptor second.
151
- const tagline = chalk.gray(' sub-agent progress watch');
152
- const projLine = chalk.gray(` project: ${projectRoot}`);
153
- const pathLine = chalk.gray(` path: ${progressFilePath}`);
154
- const separator = chalk.gray(' ' + '─'.repeat(60));
155
- return [
156
- brandLines[0] ?? '',
157
- brandLines[1] ?? '',
158
- brandLines[2] ?? '',
159
- separator,
160
- tagline,
161
- projLine,
162
- pathLine,
163
- ''
164
- ].join('\n');
165
- }
166
- /**
167
- * Render the 2-line dynamic dashboard. Status row on top
168
- * (spinner + phase + elapsed + step), progress bar on the
169
- * bottom. When `data` is null (no progress file yet), we
170
- * still show a live spinner + a "waiting…" message so the
171
- * user knows the watch is alive.
172
- */
173
- function renderDynamicRows(data, tick, width, isTty) {
174
- const progressFraction = computeFakeProgress(data);
175
- if (data === null) {
176
- return {
177
- status: isTty
178
- ? ` ${chalk.gray(pickSpinnerFrame(tick))} ${chalk.gray('idle')} ${chalk.gray('(no progress file yet — sub-agent has not started)')}`
179
- : `idle (no progress file yet)`,
180
- bar: isTty ? renderBar(0, width, isTty) : ''
181
- };
182
- }
183
- const startedAtMs = new Date(data.current.startedAt).getTime();
184
- const elapsedMs = Number.isNaN(startedAtMs) ? 0 : Math.max(0, Date.now() - startedAtMs);
185
- const phase = PHASE_LABEL[data.current.phase];
186
- const step = data.current.step.length > 60 ? data.current.step.slice(0, 57) + '...' : data.current.step;
187
- const verdict = data.current.verdict ? ` verdict=${data.current.verdict}` : '';
188
- const role = data.role ? ` role=${data.role}` : '';
189
- const spinner = pickSpinnerFrame(tick);
190
- if (!isTty) {
191
- return {
192
- status: `${spinner} ${phase} ${formatElapsed(elapsedMs)} ${step}${role}${verdict}`,
193
- bar: ''
194
- };
195
- }
196
- const colorize = PHASE_COLOR[data.current.phase] ?? chalk.cyan;
197
- const spinnerColor = phase === 'failed' ? chalk.red
198
- : phase === 'finished' ? chalk.green
199
- : chalk.cyan;
200
- const statusLine = ` ${spinnerColor(spinner)} ${colorize(phase.padEnd(11))} ${chalk.yellow(formatElapsed(elapsedMs))} ${step}${chalk.gray(role)}${verdict ? chalk.gray(verdict) : ''}`;
201
- return {
202
- status: statusLine,
203
- bar: renderBar(progressFraction, width, isTty)
204
- };
205
- }
206
- function renderBar(fraction, width, isTty) {
207
- if (!isTty)
208
- return '';
209
- // 8ths-of-cell block characters: ░ (empty) U+2591, full blocks
210
- // U+2588..U+258F for the partial last cell.
211
- const barCells = Math.max(10, Math.min(50, Math.floor(width * 0.4)));
212
- const filled = Math.round(barCells * Math.max(0, Math.min(1, fraction)) * 8);
213
- const fullBlocks = Math.floor(filled / 8);
214
- const fracBlock = filled % 8;
215
- const emptyCells = barCells * 8 - filled;
216
- const fullStr = '█'.repeat(fullBlocks);
217
- const fracStr = fracBlock > 0 ? String.fromCharCode(0x2588 + (8 - fracBlock)) : '';
218
- const emptyStr = '░'.repeat(Math.floor(emptyCells / 8));
219
- const percent = String(Math.round(fraction * 100)).padStart(3, ' ');
220
- return ` ${chalk.green(fullStr + fracStr + emptyStr)} ${chalk.gray(`${percent}%`)}`;
221
- }
222
- export class WatchRenderer {
223
- projectRoot;
224
- progressFilePath;
225
- width;
226
- isTty;
227
- hasRenderedDynamic = false;
228
- constructor(options) {
229
- this.projectRoot = options.projectRoot;
230
- this.progressFilePath = options.progressFilePath;
231
- this.width = (process.stdout.columns ?? 120) - 1;
232
- this.isTty = process.stdout.isTTY === true;
233
- }
234
- /**
235
- * Paint the static header once at the top of the watch
236
- * (the PEAKS-CLI wordmark, separator, project, path). Then
237
- * paint the dynamic rows for the first tick. From here on
238
- * the dynamic rows are the only thing we touch.
239
- */
240
- start() {
241
- rawWrite(renderHeader(this.projectRoot, this.progressFilePath, this.isTty));
242
- this.paintDynamicOnce(null, 0);
243
- }
244
- /**
245
- * Repaint the 2 dynamic rows in place. First call moves
246
- * the cursor up N rows from the bottom of the previously
247
- * painted block; subsequent calls do the same.
248
- */
249
- tick(data, tickCount) {
250
- if (this.hasRenderedDynamic) {
251
- // Move cursor up to the top of the previously-painted
252
- // dynamic block.
253
- rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
254
- }
255
- this.paintDynamicOnce(data, tickCount);
256
- }
257
- paintDynamicOnce(data, tick) {
258
- const { status, bar } = renderDynamicRows(data, tick, this.width, this.isTty);
259
- // Erase-then-rewrite the status row.
260
- rawWrite(ERASE_LINE + status + '\n');
261
- // Erase-then-rewrite the bar row. (In non-TTY mode the
262
- // bar is empty; we still emit a newline so the line
263
- // count stays consistent.)
264
- rawWrite(ERASE_LINE + bar + '\n');
265
- this.hasRenderedDynamic = true;
266
- }
267
- /**
268
- * Paint a final 2-line verdict + a farewell line below the
269
- * dashboard, then return. The cursor stays at the bottom
270
- * of the farewell so the user's shell prompt lands on the
271
- * next row.
272
- */
273
- finalize(data) {
274
- // Rewrite the dynamic block one last time so the user can
275
- // read the verdict.
276
- if (this.hasRenderedDynamic) {
277
- rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
278
- }
279
- this.paintDynamicOnce(data, Number.MAX_SAFE_INTEGER);
280
- // Then emit the farewell BELOW the dashboard, in green.
281
- const verdictSuffix = data.current.verdict !== undefined ? ` (verdict=${data.current.verdict})` : '';
282
- const farewell = this.isTty
283
- ? chalk.green(`✔ peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`)
284
- : `peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`;
285
- rawWrite(ERASE_LINE + farewell + '\n');
286
- }
287
- /**
288
- * Force a non-ANSI snapshot of the current state, used by
289
- * the `--once` mode and for fallback when stdout is not a
290
- * TTY. Does NOT touch the cursor state — safe to call from
291
- * any context.
292
- */
293
- static snapshot(data) {
294
- return renderDynamicRows(data, 0, 80, false);
295
- }
296
- }
297
- /**
298
- * Strip ANSI escapes from a string. Used for visible-length
299
- * accounting; not for re-painting.
300
- */
301
- export function stripAnsi(input) {
302
- // eslint-disable-next-line no-control-regex
303
- return input.replace(/\x1b\[[0-9;]*m/g, '');
304
- }
305
- /** Reset terminal SGR — used on early-return error paths. */
306
- export function resetTerminal() {
307
- rawWrite(RESET);
308
- }
@@ -1,31 +0,0 @@
1
- import { type PlanMcpInstallOptions, type McpInstallEnvCheck } from './mcp-plan-service.js';
2
- export type McpApplyAction = 'add' | 'update' | 'claimed' | 'noop';
3
- export type McpApplyBackupInfo = {
4
- path: string | null;
5
- skipped: boolean;
6
- };
7
- export type McpApplyResult = {
8
- capabilityId: string;
9
- action: McpApplyAction;
10
- backup: McpApplyBackupInfo;
11
- written: {
12
- settingsPath: string;
13
- managedMarkerPath: string;
14
- };
15
- envCheck: McpInstallEnvCheck;
16
- };
17
- export type McpApplyOptions = PlanMcpInstallOptions & {
18
- claim?: boolean;
19
- backupRoot?: string;
20
- clock?: () => string;
21
- };
22
- export type McpRollbackOptions = {
23
- backupPath: string;
24
- globalSettingsPath?: string;
25
- };
26
- export type McpRollbackResult = {
27
- restoredFrom: string;
28
- restoredTo: string;
29
- };
30
- export declare function applyMcpInstall(capabilityId: string, options?: McpApplyOptions): Promise<McpApplyResult>;
31
- export declare function rollbackMcpInstall(options: McpRollbackOptions): Promise<McpRollbackResult>;
@@ -1,112 +0,0 @@
1
- import { homedir } from 'node:os';
2
- import { dirname, join } from 'node:path';
3
- import { mkdir, writeFile } from 'node:fs/promises';
4
- import { pathExists, readText } from '../../shared/fs.js';
5
- import { planMcpInstall } from './mcp-plan-service.js';
6
- function defaultGlobalSettingsPath() {
7
- return join(homedir(), '.claude', 'settings.json');
8
- }
9
- function defaultManagedMarkerPath() {
10
- return join(homedir(), '.peaks', 'mcp-managed.json');
11
- }
12
- function defaultBackupRoot() {
13
- return join(homedir(), '.peaks-artifacts', 'mcp-backups');
14
- }
15
- function defaultClock() {
16
- return new Date().toISOString().replace(/[:.]/g, '-');
17
- }
18
- async function readJsonFile(path) {
19
- if (!(await pathExists(path))) {
20
- return {};
21
- }
22
- const raw = await readText(path);
23
- if (raw.length === 0) {
24
- return {};
25
- }
26
- const parsed = JSON.parse(raw);
27
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
28
- return {};
29
- }
30
- return parsed;
31
- }
32
- async function writeJsonFile(path, data) {
33
- await mkdir(dirname(path), { recursive: true });
34
- await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
35
- }
36
- function buildServerConfig(spec) {
37
- const env = {};
38
- for (const key of spec.envKeys) {
39
- env[key] = `\${${key}}`;
40
- }
41
- return { command: spec.command, args: [...spec.args], env };
42
- }
43
- async function createBackup(settingsPath, backupRoot, timestamp) {
44
- if (!(await pathExists(settingsPath))) {
45
- return null;
46
- }
47
- const backupPath = join(backupRoot, timestamp, 'settings.json');
48
- await mkdir(dirname(backupPath), { recursive: true });
49
- await writeFile(backupPath, await readText(settingsPath), 'utf8');
50
- return backupPath;
51
- }
52
- async function updateManagedMarker(markerPath, name) {
53
- const marker = await readJsonFile(markerPath);
54
- const existing = Array.isArray(marker.servers)
55
- ? marker.servers.filter((entry) => typeof entry === 'string')
56
- : [];
57
- if (!existing.includes(name)) {
58
- existing.push(name);
59
- }
60
- await writeJsonFile(markerPath, { ...marker, servers: existing });
61
- }
62
- export async function applyMcpInstall(capabilityId, options = {}) {
63
- const plan = await planMcpInstall(capabilityId, options);
64
- if (plan.action === 'unknown-capability' || plan.spec === null) {
65
- throw new Error(`No MCP install spec registered for capability ${capabilityId} (unknown-capability)`);
66
- }
67
- if (plan.envCheck.missing.length > 0) {
68
- throw new Error(`Refusing to apply: missing required env vars: ${plan.envCheck.missing.join(', ')}`);
69
- }
70
- if (plan.action === 'conflict' && options.claim !== true) {
71
- throw new Error(`Refusing to apply: server ${plan.spec.name} exists but is not peaks-managed (conflict). Re-run with --claim to take ownership.`);
72
- }
73
- const settingsPath = options.globalSettingsPath ?? defaultGlobalSettingsPath();
74
- const markerPath = options.managedMarkerPath ?? defaultManagedMarkerPath();
75
- const backupRoot = options.backupRoot ?? defaultBackupRoot();
76
- const clock = options.clock ?? defaultClock;
77
- if (plan.action === 'noop') {
78
- return {
79
- capabilityId,
80
- action: 'noop',
81
- backup: { path: null, skipped: true },
82
- written: { settingsPath, managedMarkerPath: markerPath },
83
- envCheck: plan.envCheck
84
- };
85
- }
86
- const backupPath = await createBackup(settingsPath, backupRoot, clock());
87
- const settings = await readJsonFile(settingsPath);
88
- const existingServers = settings.mcpServers !== null && typeof settings.mcpServers === 'object' && !Array.isArray(settings.mcpServers)
89
- ? settings.mcpServers
90
- : {};
91
- const nextServers = { ...existingServers, [plan.spec.name]: buildServerConfig(plan.spec) };
92
- await writeJsonFile(settingsPath, { ...settings, mcpServers: nextServers });
93
- await updateManagedMarker(markerPath, plan.spec.name);
94
- const action = plan.action === 'conflict' ? 'claimed' : plan.action;
95
- return {
96
- capabilityId,
97
- action,
98
- backup: { path: backupPath, skipped: false },
99
- written: { settingsPath, managedMarkerPath: markerPath },
100
- envCheck: plan.envCheck
101
- };
102
- }
103
- export async function rollbackMcpInstall(options) {
104
- if (!(await pathExists(options.backupPath))) {
105
- throw new Error(`Refusing to rollback: backup file not found at ${options.backupPath}`);
106
- }
107
- const target = options.globalSettingsPath ?? defaultGlobalSettingsPath();
108
- const content = await readText(options.backupPath);
109
- await mkdir(dirname(target), { recursive: true });
110
- await writeFile(target, content, 'utf8');
111
- return { restoredFrom: options.backupPath, restoredTo: target };
112
- }
@@ -1,17 +0,0 @@
1
- import { type McpInstallSpec } from './mcp-install-registry.js';
2
- import { type McpClientTransport } from './mcp-client-service.js';
3
- export type McpCallTransportFactory = (spec: McpInstallSpec, env: Record<string, string | undefined>) => McpClientTransport;
4
- export type McpCallOptions = {
5
- capabilityId: string;
6
- toolName: string;
7
- transportFactory: McpCallTransportFactory;
8
- args?: Record<string, unknown>;
9
- env?: Record<string, string | undefined>;
10
- timeoutMs?: number;
11
- };
12
- export type McpCallResult = {
13
- capabilityId: string;
14
- toolName: string;
15
- result: unknown;
16
- };
17
- export declare function callMcpTool(options: McpCallOptions): Promise<McpCallResult>;
@@ -1,34 +0,0 @@
1
- import { findMcpInstallSpec } from './mcp-install-registry.js';
2
- import { createMcpClient } from './mcp-client-service.js';
3
- function checkRequiredEnv(spec, env) {
4
- return spec.envKeys.filter((key) => {
5
- const value = env[key];
6
- return value === undefined || value.length === 0;
7
- });
8
- }
9
- export async function callMcpTool(options) {
10
- const spec = findMcpInstallSpec(options.capabilityId);
11
- if (spec === null) {
12
- throw new Error(`No MCP install spec registered for capability ${options.capabilityId}`);
13
- }
14
- const env = options.env ?? process.env;
15
- const missing = checkRequiredEnv(spec, env);
16
- if (missing.length > 0) {
17
- throw new Error(`Refusing to call ${spec.name}: missing required env vars: ${missing.join(', ')}`);
18
- }
19
- const transport = options.transportFactory(spec, env);
20
- const clientOptions = options.timeoutMs !== undefined
21
- ? { transport, timeoutMs: options.timeoutMs }
22
- : { transport };
23
- const client = createMcpClient(clientOptions);
24
- try {
25
- const result = await client.request('tools/call', {
26
- name: options.toolName,
27
- arguments: options.args ?? {}
28
- });
29
- return { capabilityId: options.capabilityId, toolName: options.toolName, result };
30
- }
31
- finally {
32
- await client.close();
33
- }
34
- }
@@ -1,14 +0,0 @@
1
- export type McpClientTransport = {
2
- send: (line: string) => Promise<void>;
3
- onLine: (handler: (line: string) => void) => void;
4
- close: () => Promise<void>;
5
- };
6
- export type McpClientOptions = {
7
- transport: McpClientTransport;
8
- timeoutMs?: number;
9
- };
10
- export type McpClientHandle = {
11
- request: (method: string, params?: unknown) => Promise<unknown>;
12
- close: () => Promise<void>;
13
- };
14
- export declare function createMcpClient(options: McpClientOptions): McpClientHandle;
@@ -1,49 +0,0 @@
1
- import { buildRequest, parseMessages, serializeMessage } from './mcp-protocol.js';
2
- const DEFAULT_TIMEOUT_MS = 30000;
3
- export function createMcpClient(options) {
4
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5
- let nextId = 1;
6
- let buffer = '';
7
- const pending = new Map();
8
- function deliver(message) {
9
- const entry = pending.get(message.id);
10
- if (entry === undefined) {
11
- return;
12
- }
13
- pending.delete(message.id);
14
- clearTimeout(entry.timer);
15
- if (message.error !== undefined) {
16
- entry.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
17
- return;
18
- }
19
- entry.resolve(message.result);
20
- }
21
- options.transport.onLine((line) => {
22
- buffer += line;
23
- const { messages, remainder } = parseMessages(buffer);
24
- buffer = remainder;
25
- for (const message of messages) {
26
- deliver(message);
27
- }
28
- });
29
- async function request(method, params) {
30
- const id = nextId++;
31
- const message = serializeMessage(buildRequest(id, method, params));
32
- return new Promise((resolve, reject) => {
33
- const timer = setTimeout(() => {
34
- pending.delete(id);
35
- reject(new Error(`MCP request ${method} timed out after ${timeoutMs}ms`));
36
- }, timeoutMs);
37
- pending.set(id, { resolve, reject, timer });
38
- options.transport.send(message).catch((error) => {
39
- clearTimeout(timer);
40
- pending.delete(id);
41
- reject(error instanceof Error ? error : new Error(String(error)));
42
- });
43
- });
44
- }
45
- async function close() {
46
- await options.transport.close();
47
- }
48
- return { request, close };
49
- }
@@ -1,11 +0,0 @@
1
- import type { McpServerScope } from './mcp-types.js';
2
- export type McpInstallSpec = {
3
- capabilityId: string;
4
- name: string;
5
- scope: McpServerScope;
6
- command: string;
7
- args: string[];
8
- envKeys: string[];
9
- };
10
- export declare const seedMcpInstalls: ReadonlyArray<McpInstallSpec>;
11
- export declare function findMcpInstallSpec(capabilityId: string): McpInstallSpec | null;
@@ -1,38 +0,0 @@
1
- export const seedMcpInstalls = [
2
- {
3
- capabilityId: 'context7.docs-lookup',
4
- name: 'context7',
5
- scope: 'global',
6
- command: 'npx',
7
- args: ['-y', '@upstash/context7-mcp@latest'],
8
- envKeys: ['CONTEXT7_API_KEY']
9
- },
10
- {
11
- capabilityId: 'playwright-mcp.browser-validation',
12
- name: 'playwright',
13
- scope: 'global',
14
- command: 'npx',
15
- args: ['-y', '@playwright/mcp@latest'],
16
- envKeys: []
17
- },
18
- {
19
- capabilityId: 'chrome-devtools-mcp.browser-debug',
20
- name: 'chrome-devtools',
21
- scope: 'global',
22
- command: 'npx',
23
- args: ['-y', 'chrome-devtools-mcp@latest'],
24
- envKeys: []
25
- },
26
- {
27
- capabilityId: 'figma-context-mcp.design-context',
28
- name: 'figma',
29
- scope: 'global',
30
- command: 'npx',
31
- args: ['-y', 'figma-developer-mcp@latest', '--stdio'],
32
- envKeys: ['FIGMA_API_KEY']
33
- }
34
- ];
35
- export function findMcpInstallSpec(capabilityId) {
36
- const match = seedMcpInstalls.find((spec) => spec.capabilityId === capabilityId);
37
- return match ?? null;
38
- }
@@ -1,29 +0,0 @@
1
- import { type McpScanOptions } from './mcp-scan-service.js';
2
- import { type McpInstallSpec } from './mcp-install-registry.js';
3
- import type { McpServerConfig } from './mcp-types.js';
4
- export type McpInstallAction = 'add' | 'update' | 'noop' | 'conflict' | 'unknown-capability';
5
- export type McpInstallEnvCheck = {
6
- missing: string[];
7
- };
8
- export type McpInstallDiffEntry<T> = {
9
- before: T;
10
- after: T;
11
- };
12
- export type McpInstallDiff = {
13
- command?: McpInstallDiffEntry<string>;
14
- args?: McpInstallDiffEntry<string[]>;
15
- envKeys?: McpInstallDiffEntry<string[]>;
16
- };
17
- export type McpInstallPlan = {
18
- capabilityId: string;
19
- action: McpInstallAction;
20
- spec: McpInstallSpec | null;
21
- current: McpServerConfig | null;
22
- envCheck: McpInstallEnvCheck;
23
- diff: McpInstallDiff | null;
24
- nextActions: string[];
25
- };
26
- export type PlanMcpInstallOptions = McpScanOptions & {
27
- env?: Record<string, string | undefined>;
28
- };
29
- export declare function planMcpInstall(capabilityId: string, options?: PlanMcpInstallOptions): Promise<McpInstallPlan>;