peaks-cli 1.0.12 → 1.0.13
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/bin/peaks.js +0 -0
- package/dist/src/cli/commands/config-commands.js +1 -17
- package/dist/src/cli/commands/core-artifact-commands.js +23 -0
- package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
- package/dist/src/cli/commands/mcp-commands.js +144 -0
- package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
- package/dist/src/cli/commands/openspec-commands.js +169 -0
- package/dist/src/cli/commands/project-commands.d.ts +3 -0
- package/dist/src/cli/commands/project-commands.js +37 -0
- package/dist/src/cli/commands/request-commands.d.ts +3 -0
- package/dist/src/cli/commands/request-commands.js +140 -0
- package/dist/src/cli/commands/understand-commands.d.ts +3 -0
- package/dist/src/cli/commands/understand-commands.js +78 -0
- package/dist/src/cli/commands/workflow-commands.js +56 -94
- package/dist/src/cli/program.js +10 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
- package/dist/src/services/artifacts/request-artifact-service.js +432 -0
- package/dist/src/services/codegraph/codegraph-service.js +26 -45
- package/dist/src/services/config/config-service.js +2 -22
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
- package/dist/src/services/doctor/doctor-service.d.ts +7 -0
- package/dist/src/services/doctor/doctor-service.js +139 -0
- package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
- package/dist/src/services/mcp/mcp-apply-service.js +112 -0
- package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
- package/dist/src/services/mcp/mcp-call-service.js +34 -0
- package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
- package/dist/src/services/mcp/mcp-client-service.js +49 -0
- package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
- package/dist/src/services/mcp/mcp-install-registry.js +38 -0
- package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
- package/dist/src/services/mcp/mcp-plan-service.js +109 -0
- package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
- package/dist/src/services/mcp/mcp-protocol.js +41 -0
- package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
- package/dist/src/services/mcp/mcp-scan-service.js +214 -0
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
- package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
- package/dist/src/services/mcp/mcp-types.d.ts +31 -0
- package/dist/src/services/mcp/mcp-types.js +1 -0
- package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
- package/dist/src/services/openspec/openspec-archive-service.js +28 -0
- package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
- package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
- package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
- package/dist/src/services/openspec/openspec-render-service.js +130 -0
- package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
- package/dist/src/services/openspec/openspec-scan-service.js +123 -0
- package/dist/src/services/openspec/openspec-types.d.ts +39 -0
- package/dist/src/services/openspec/openspec-types.js +1 -0
- package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
- package/dist/src/services/openspec/openspec-validate-service.js +77 -0
- package/dist/src/services/recommendations/capability-seed-items.js +2 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +1 -1
- package/dist/src/services/shadcn/shadcn-service.d.ts +4 -0
- package/dist/src/services/shadcn/shadcn-service.js +15 -30
- package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
- package/dist/src/services/skills/skill-runbook-service.js +60 -0
- package/dist/src/services/standards/project-standards-service.js +4 -9
- package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
- package/dist/src/services/understand/understand-scan-service.js +157 -0
- package/dist/src/services/understand/understand-types.d.ts +24 -0
- package/dist/src/services/understand/understand-types.js +1 -0
- package/dist/src/shared/json-schema-mini.d.ts +10 -0
- package/dist/src/shared/json-schema-mini.js +113 -0
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +9 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -8
- package/schemas/doctor-report.schema.json +34 -0
- package/schemas/mcp-apply-result.schema.json +46 -0
- package/schemas/mcp-install-plan.schema.json +71 -0
- package/schemas/mcp-install-spec.schema.json +29 -0
- package/schemas/mcp-server.schema.json +29 -0
- package/schemas/openspec-change-summary.schema.json +68 -0
- package/schemas/openspec-render-request.schema.json +61 -0
- package/schemas/openspec-validation-result.schema.json +36 -0
- package/skills/peaks-prd/SKILL.md +59 -8
- package/skills/peaks-prd/references/artifact-per-request.md +78 -0
- package/skills/peaks-prd/references/workflow.md +7 -5
- package/skills/peaks-qa/SKILL.md +74 -8
- package/skills/peaks-qa/references/artifact-contracts.md +2 -2
- package/skills/peaks-qa/references/artifact-per-request.md +83 -0
- package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
- package/skills/peaks-qa/references/regression-gates.md +2 -2
- package/skills/peaks-rd/SKILL.md +96 -9
- package/skills/peaks-rd/references/artifact-contracts.md +2 -2
- package/skills/peaks-rd/references/artifact-per-request.md +90 -0
- package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
- package/skills/peaks-rd/references/refactor-workflow.md +2 -2
- package/skills/peaks-sc/SKILL.md +44 -0
- package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
- package/skills/peaks-solo/SKILL.md +90 -9
- package/skills/peaks-solo/references/artifact-contracts.md +2 -2
- package/skills/peaks-solo/references/browser-workflow.md +114 -0
- package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
- package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
- package/skills/peaks-solo/references/refactor-mode.md +2 -2
- package/skills/peaks-solo/references/workflow.md +1 -1
- package/skills/peaks-txt/SKILL.md +42 -0
- package/skills/peaks-ui/SKILL.md +57 -33
- package/skills/peaks-ui/references/artifact-per-request.md +71 -0
- package/skills/peaks-ui/references/workflow.md +8 -11
- package/scripts/strip-internal-exports.mjs +0 -33
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type RequestArtifactRole, type RequestArtifactSummary } from '../artifacts/request-artifact-service.js';
|
|
2
|
+
import type { OpenSpecChangeSummary } from '../openspec/openspec-types.js';
|
|
3
|
+
import type { McpScanReport } from '../mcp/mcp-types.js';
|
|
4
|
+
import type { CapabilityItem } from '../recommendations/recommendation-types.js';
|
|
5
|
+
export type ProjectDashboardRequests = {
|
|
6
|
+
count: number;
|
|
7
|
+
byRole: Record<RequestArtifactRole, RequestArtifactSummary[]>;
|
|
8
|
+
byState: Record<string, number>;
|
|
9
|
+
};
|
|
10
|
+
export type ProjectDashboardOpenSpec = {
|
|
11
|
+
exists: boolean;
|
|
12
|
+
count: number;
|
|
13
|
+
changes: OpenSpecChangeSummary[];
|
|
14
|
+
};
|
|
15
|
+
export type ProjectDashboardUnderstand = {
|
|
16
|
+
exists: boolean;
|
|
17
|
+
graphExists: boolean;
|
|
18
|
+
graphPath: string;
|
|
19
|
+
parseError?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ProjectDashboardMcp = {
|
|
22
|
+
servers: McpScanReport['servers'];
|
|
23
|
+
scopes: McpScanReport['scopes'];
|
|
24
|
+
};
|
|
25
|
+
export type ProjectDashboardDoctor = {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
passed: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
};
|
|
30
|
+
export type ProjectDashboardRunbookHealth = {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
required: number;
|
|
33
|
+
healthy: number;
|
|
34
|
+
missingRunbook: string[];
|
|
35
|
+
applyNoteFailed: string[];
|
|
36
|
+
};
|
|
37
|
+
export type ProjectDashboardCapabilities = {
|
|
38
|
+
count: number;
|
|
39
|
+
mcpCount: number;
|
|
40
|
+
sample: Array<Pick<CapabilityItem, 'capabilityId' | 'name' | 'itemType' | 'category'>>;
|
|
41
|
+
};
|
|
42
|
+
export type ProjectDashboard = {
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
projectRoot: string;
|
|
45
|
+
requests: ProjectDashboardRequests;
|
|
46
|
+
openspec: ProjectDashboardOpenSpec;
|
|
47
|
+
understand: ProjectDashboardUnderstand;
|
|
48
|
+
mcp: ProjectDashboardMcp;
|
|
49
|
+
doctor: ProjectDashboardDoctor;
|
|
50
|
+
runbookHealth: ProjectDashboardRunbookHealth;
|
|
51
|
+
capabilities: ProjectDashboardCapabilities;
|
|
52
|
+
};
|
|
53
|
+
export type LoadProjectDashboardOptions = {
|
|
54
|
+
projectRoot: string;
|
|
55
|
+
sampleCapabilities?: number;
|
|
56
|
+
clock?: () => string;
|
|
57
|
+
doctorReport?: {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
passed: number;
|
|
60
|
+
failed: number;
|
|
61
|
+
};
|
|
62
|
+
runbookHealth?: ProjectDashboardRunbookHealth;
|
|
63
|
+
};
|
|
64
|
+
export declare function loadProjectDashboard(options: LoadProjectDashboardOptions): Promise<ProjectDashboard>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { listRequestArtifacts } from '../artifacts/request-artifact-service.js';
|
|
2
|
+
import { scanOpenSpec } from '../openspec/openspec-scan-service.js';
|
|
3
|
+
import { scanMcpServers } from '../mcp/mcp-scan-service.js';
|
|
4
|
+
import { scanUnderstandAnything } from '../understand/understand-scan-service.js';
|
|
5
|
+
import { seedCapabilityItems } from '../recommendations/capability-seed-items.js';
|
|
6
|
+
import { requiredSkillNames } from '../../shared/paths.js';
|
|
7
|
+
function defaultClock() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function groupRequestsByRole(items) {
|
|
11
|
+
const byRole = { prd: [], ui: [], rd: [], qa: [] };
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
byRole[item.role].push(item);
|
|
14
|
+
}
|
|
15
|
+
return byRole;
|
|
16
|
+
}
|
|
17
|
+
function countRequestsByState(items) {
|
|
18
|
+
const counts = {};
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
counts[item.state] = (counts[item.state] ?? 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
return counts;
|
|
23
|
+
}
|
|
24
|
+
async function loadDoctorAndRunbookHealth(doctorOverride, runbookOverride) {
|
|
25
|
+
if (doctorOverride !== undefined && runbookOverride !== undefined) {
|
|
26
|
+
return { doctor: doctorOverride, runbookHealth: runbookOverride };
|
|
27
|
+
}
|
|
28
|
+
if (doctorOverride !== undefined) {
|
|
29
|
+
return {
|
|
30
|
+
doctor: doctorOverride,
|
|
31
|
+
runbookHealth: { ok: true, required: 0, healthy: 0, missingRunbook: [], applyNoteFailed: [] }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const { runDoctor } = await import('../doctor/doctor-service.js');
|
|
35
|
+
const report = await runDoctor();
|
|
36
|
+
return {
|
|
37
|
+
doctor: { ok: report.summary.ok, passed: report.summary.passed, failed: report.summary.failed },
|
|
38
|
+
runbookHealth: runbookOverride ?? summarizeRunbookHealth(report.checks)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function summarizeRunbookHealth(checks) {
|
|
42
|
+
const missingRunbook = [];
|
|
43
|
+
const applyNoteFailed = [];
|
|
44
|
+
for (const check of checks) {
|
|
45
|
+
if (!check.ok && check.id.startsWith('skill-runbook:')) {
|
|
46
|
+
missingRunbook.push(check.id.slice('skill-runbook:'.length));
|
|
47
|
+
}
|
|
48
|
+
if (!check.ok && check.id.startsWith('skill-apply-note:')) {
|
|
49
|
+
applyNoteFailed.push(check.id.slice('skill-apply-note:'.length));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const required = requiredSkillNames.length;
|
|
53
|
+
const healthy = Math.max(0, required - missingRunbook.length - applyNoteFailed.length);
|
|
54
|
+
return {
|
|
55
|
+
ok: missingRunbook.length === 0 && applyNoteFailed.length === 0,
|
|
56
|
+
required,
|
|
57
|
+
healthy,
|
|
58
|
+
missingRunbook,
|
|
59
|
+
applyNoteFailed
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function buildCapabilitiesSummary(sampleSize) {
|
|
63
|
+
const items = seedCapabilityItems;
|
|
64
|
+
return {
|
|
65
|
+
count: items.length,
|
|
66
|
+
mcpCount: items.filter((item) => item.itemType === 'mcp').length,
|
|
67
|
+
sample: items.slice(0, sampleSize).map((item) => ({
|
|
68
|
+
capabilityId: item.capabilityId,
|
|
69
|
+
name: item.name,
|
|
70
|
+
itemType: item.itemType,
|
|
71
|
+
category: item.category
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function loadProjectDashboard(options) {
|
|
76
|
+
const clock = options.clock ?? defaultClock;
|
|
77
|
+
const sampleSize = options.sampleCapabilities ?? 8;
|
|
78
|
+
const [items, openspecReport, mcpReport, understandReport, doctorAndRunbook] = await Promise.all([
|
|
79
|
+
listRequestArtifacts({ projectRoot: options.projectRoot }),
|
|
80
|
+
scanOpenSpec({ openspecRoot: `${options.projectRoot}/openspec` }),
|
|
81
|
+
scanMcpServers({ projectRoot: options.projectRoot }),
|
|
82
|
+
scanUnderstandAnything({ projectRoot: options.projectRoot }),
|
|
83
|
+
loadDoctorAndRunbookHealth(options.doctorReport, options.runbookHealth)
|
|
84
|
+
]);
|
|
85
|
+
return {
|
|
86
|
+
generatedAt: clock(),
|
|
87
|
+
projectRoot: options.projectRoot,
|
|
88
|
+
requests: {
|
|
89
|
+
count: items.length,
|
|
90
|
+
byRole: groupRequestsByRole(items),
|
|
91
|
+
byState: countRequestsByState(items)
|
|
92
|
+
},
|
|
93
|
+
openspec: {
|
|
94
|
+
exists: openspecReport.exists,
|
|
95
|
+
count: openspecReport.changes.length,
|
|
96
|
+
changes: openspecReport.changes
|
|
97
|
+
},
|
|
98
|
+
understand: {
|
|
99
|
+
exists: understandReport.exists,
|
|
100
|
+
graphExists: understandReport.graph.exists,
|
|
101
|
+
graphPath: understandReport.graph.path,
|
|
102
|
+
...(understandReport.graph.parseError !== undefined ? { parseError: understandReport.graph.parseError } : {})
|
|
103
|
+
},
|
|
104
|
+
mcp: {
|
|
105
|
+
servers: mcpReport.servers,
|
|
106
|
+
scopes: mcpReport.scopes
|
|
107
|
+
},
|
|
108
|
+
doctor: doctorAndRunbook.doctor,
|
|
109
|
+
runbookHealth: doctorAndRunbook.runbookHealth,
|
|
110
|
+
capabilities: buildCapabilitiesSummary(sampleSize)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -11,8 +11,15 @@ export type DoctorReport = {
|
|
|
11
11
|
failed: number;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
export type CodegraphCapabilityProbe = {
|
|
15
|
+
packagePath: string;
|
|
16
|
+
version: string;
|
|
17
|
+
binaryPath: string;
|
|
18
|
+
binaryExists: boolean;
|
|
19
|
+
};
|
|
14
20
|
export type DoctorOptions = {
|
|
15
21
|
schemasBaseDir?: string;
|
|
16
22
|
skillsBaseDir?: string;
|
|
23
|
+
codegraphProbe?: () => CodegraphCapabilityProbe;
|
|
17
24
|
};
|
|
18
25
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
4
6
|
import { readText } from '../../shared/fs.js';
|
|
5
7
|
import { requiredSchemaFiles, requiredSkillNames, schemasDir } from '../../shared/paths.js';
|
|
6
8
|
import { getErrorMessage } from '../../shared/result.js';
|
|
7
9
|
import { loadSkillRegistry } from '../skills/skill-registry.js';
|
|
10
|
+
const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
|
|
11
|
+
function defaultCodegraphProbe() {
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const packagePath = require.resolve('@colbymchenry/codegraph/package.json');
|
|
14
|
+
const pkg = require(packagePath);
|
|
15
|
+
const binaryPath = resolvePath(dirname(packagePath), 'dist', 'bin', 'codegraph.js');
|
|
16
|
+
return {
|
|
17
|
+
packagePath,
|
|
18
|
+
version: pkg.version ?? 'unknown',
|
|
19
|
+
binaryPath,
|
|
20
|
+
binaryExists: existsSync(binaryPath)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
24
|
+
/peaks\s+memory\s+sync[^\n]*--apply/,
|
|
25
|
+
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
26
|
+
/peaks\s+artifacts\s+sync[^\n]*--apply/,
|
|
27
|
+
/peaks\s+openspec\s+archive[^\n]*--apply/,
|
|
28
|
+
/peaks\s+standards\s+(?:init|update)[^\n]*--apply/
|
|
29
|
+
];
|
|
30
|
+
const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
|
|
31
|
+
function extractRunbookSection(body) {
|
|
32
|
+
const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
|
|
33
|
+
return match === null ? null : (match[1] ?? null);
|
|
34
|
+
}
|
|
35
|
+
function findDestructiveApplyLines(section) {
|
|
36
|
+
const lines = section.split(/\r?\n/);
|
|
37
|
+
return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
|
|
38
|
+
}
|
|
8
39
|
export async function runDoctor(options = {}) {
|
|
9
40
|
const checks = [];
|
|
10
41
|
const registry = await loadSkillRegistry(options.skillsBaseDir);
|
|
@@ -35,6 +66,51 @@ export async function runDoctor(options = {}) {
|
|
|
35
66
|
message: `Skill ${failure.directory} has invalid metadata: ${failure.message}`
|
|
36
67
|
});
|
|
37
68
|
}
|
|
69
|
+
const requiredSkillNameSet = new Set(requiredSkillNames);
|
|
70
|
+
for (const skill of skills) {
|
|
71
|
+
if (!requiredSkillNameSet.has(skill.name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const body = await readText(skill.skillPath);
|
|
76
|
+
const hasRunbook = /## Default runbook\s/.test(body);
|
|
77
|
+
checks.push({
|
|
78
|
+
id: `skill-runbook:${skill.name}`,
|
|
79
|
+
ok: hasRunbook,
|
|
80
|
+
message: hasRunbook
|
|
81
|
+
? `Skill ${skill.name} declares a Default runbook`
|
|
82
|
+
: `Skill ${skill.name} is missing a ## Default runbook section`
|
|
83
|
+
});
|
|
84
|
+
const runbookSection = extractRunbookSection(body);
|
|
85
|
+
if (runbookSection !== null) {
|
|
86
|
+
const destructiveLines = findDestructiveApplyLines(runbookSection);
|
|
87
|
+
if (destructiveLines.length === 0) {
|
|
88
|
+
checks.push({
|
|
89
|
+
id: `skill-apply-note:${skill.name}`,
|
|
90
|
+
ok: true,
|
|
91
|
+
message: `Skill ${skill.name} runbook has no destructive --apply commands to gate`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const hasAuthorizationNote = AUTHORIZATION_KEYWORDS_PATTERN.test(runbookSection);
|
|
96
|
+
checks.push({
|
|
97
|
+
id: `skill-apply-note:${skill.name}`,
|
|
98
|
+
ok: hasAuthorizationNote,
|
|
99
|
+
message: hasAuthorizationNote
|
|
100
|
+
? `Skill ${skill.name} gates ${destructiveLines.length} destructive --apply command(s) with an authorization note`
|
|
101
|
+
: `Skill ${skill.name} has ${destructiveLines.length} destructive --apply command(s) without an authorization/dry-run note in the runbook section`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
checks.push({
|
|
108
|
+
id: `skill-runbook:${skill.name}`,
|
|
109
|
+
ok: false,
|
|
110
|
+
message: `Skill ${skill.name} runbook check failed: ${getErrorMessage(error)}`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
38
114
|
const schemaRoot = options.schemasBaseDir ?? schemasDir;
|
|
39
115
|
for (const schemaFile of requiredSchemaFiles) {
|
|
40
116
|
try {
|
|
@@ -56,6 +132,69 @@ export async function runDoctor(options = {}) {
|
|
|
56
132
|
ok: true,
|
|
57
133
|
message: hasUserConfig ? 'User config exists at ~/.peaks/config.json' : 'Optional user config not found at ~/.peaks/config.json'
|
|
58
134
|
});
|
|
135
|
+
const probe = options.codegraphProbe ?? defaultCodegraphProbe;
|
|
136
|
+
try {
|
|
137
|
+
const result = probe();
|
|
138
|
+
const versionOk = result.version === CODEGRAPH_EXPECTED_VERSION;
|
|
139
|
+
if (!versionOk) {
|
|
140
|
+
checks.push({
|
|
141
|
+
id: 'capability:codegraph',
|
|
142
|
+
ok: false,
|
|
143
|
+
message: `@colbymchenry/codegraph version mismatch: expected ${CODEGRAPH_EXPECTED_VERSION}, resolved ${result.version} at ${result.packagePath}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (!result.binaryExists) {
|
|
147
|
+
checks.push({
|
|
148
|
+
id: 'capability:codegraph',
|
|
149
|
+
ok: false,
|
|
150
|
+
message: `@colbymchenry/codegraph@${result.version} resolved at ${result.packagePath} but binary is missing at ${result.binaryPath}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
checks.push({
|
|
155
|
+
id: 'capability:codegraph',
|
|
156
|
+
ok: true,
|
|
157
|
+
message: `@colbymchenry/codegraph@${result.version} resolves with binary at ${result.binaryPath}`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
checks.push({
|
|
163
|
+
id: 'capability:codegraph',
|
|
164
|
+
ok: false,
|
|
165
|
+
message: `@colbymchenry/codegraph not resolvable: ${getErrorMessage(error)}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const schemaText = await readText(join(schemaRoot, 'doctor-report.schema.json'));
|
|
170
|
+
const schema = JSON.parse(schemaText);
|
|
171
|
+
const patternSource = schema.properties?.checks?.items?.properties?.id?.pattern;
|
|
172
|
+
if (typeof patternSource === 'string') {
|
|
173
|
+
const pattern = new RegExp(patternSource);
|
|
174
|
+
const mismatches = checks.filter((check) => !pattern.test(check.id)).map((check) => check.id);
|
|
175
|
+
checks.push({
|
|
176
|
+
id: 'doctor-self:check-id-pattern',
|
|
177
|
+
ok: mismatches.length === 0,
|
|
178
|
+
message: mismatches.length === 0
|
|
179
|
+
? 'All doctor check IDs match the doctor-report schema pattern'
|
|
180
|
+
: `Doctor check IDs missing from schema pattern: ${mismatches.join(', ')}`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
checks.push({
|
|
185
|
+
id: 'doctor-self:check-id-pattern',
|
|
186
|
+
ok: false,
|
|
187
|
+
message: 'doctor-report.schema.json does not declare a check.id pattern'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
checks.push({
|
|
193
|
+
id: 'doctor-self:check-id-pattern',
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `Failed to load doctor-report.schema.json for self-validation: ${getErrorMessage(error)}`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
59
198
|
const failed = checks.filter((check) => !check.ok).length;
|
|
60
199
|
return {
|
|
61
200
|
checks,
|
|
@@ -0,0 +1,31 @@
|
|
|
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>;
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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>;
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|