capy-mcp 1.0.3 → 1.0.4

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 CHANGED
@@ -6,9 +6,10 @@ It does not generate the preview page itself. It gives an AI coding agent the re
6
6
 
7
7
  ## What it exposes
8
8
 
9
- The server currently registers two MCP tools:
9
+ The server currently registers three MCP tools:
10
10
 
11
11
  - `get_preview_brief`
12
+ - `update_preview`
12
13
  - `get_design_system`
13
14
 
14
15
  The tool accepts:
@@ -29,6 +30,8 @@ The tool returns structured content with:
29
30
 
30
31
  `get_design_system` writes a stable JSON artifact, by default at `.capy/design-system.json`, and returns a summary plus the artifact contents. This file is meant to be the machine-readable source of truth that later coding sessions can load before making UI changes.
31
32
 
33
+ `update_preview` writes `.capy/preview-state.json`, diffs the current repo against the prior snapshot, refreshes `.capy/design-system.json`, and returns an incremental `/preview` update brief. This works even if the repo is not using git.
34
+
32
35
  ## Install
33
36
 
34
37
  ```bash
@@ -62,8 +65,11 @@ Example stdio MCP config:
62
65
  import {
63
66
  buildDesignSystemArtifact,
64
67
  buildPreviewBrief,
68
+ buildPreviewStateSnapshot,
65
69
  detectFramework,
70
+ runPreviewUpdate,
66
71
  writeDesignSystemArtifact,
72
+ writePreviewStateSnapshot,
67
73
  } from "capy-mcp";
68
74
 
69
75
  const framework = await detectFramework(process.cwd());
@@ -72,6 +78,9 @@ const brief = await buildPreviewBrief(process.cwd(), {
72
78
  });
73
79
  const designSystem = await buildDesignSystemArtifact(process.cwd());
74
80
  await writeDesignSystemArtifact(process.cwd());
81
+ const previewState = await buildPreviewStateSnapshot(process.cwd());
82
+ await writePreviewStateSnapshot(process.cwd(), ".capy/preview-state.json", previewState);
83
+ const update = await runPreviewUpdate(process.cwd());
75
84
  ```
76
85
 
77
86
  ## Local development
@@ -15,25 +15,35 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
15
15
  });
16
16
  const cssVariables = await collectCssVariables(projectRoot, projectFacts.likelyStyleFiles);
17
17
  const components = await collectComponents(projectRoot, projectFacts);
18
+ const readFirst = buildReadFirst(previewBrief, components);
19
+ const bridges = await buildUsageBridges(projectRoot, projectFacts.likelyStyleFiles, components);
20
+ const gaps = buildGapNotes(projectFacts, components, cssVariables);
18
21
  const artifactPath = input.artifactPath ?? DEFAULT_ARTIFACT_PATH;
19
- const sourceFiles = Array.from(new Set([
20
- ...projectFacts.likelyStyleFiles,
21
- ...components.map((component) => component.path),
22
- ])).sort();
23
22
  return {
24
- meta: {
23
+ forAgent: {
24
+ intent: input.mode === "update"
25
+ ? "Update /preview incrementally. Read the files below first, then touch only the sections affected by the changed files unless shared foundations changed."
26
+ : "Build or refine /preview. Read the files below first, then mirror the repo's existing UI language instead of inventing a new one.",
27
+ readFirst,
28
+ facts: buildAgentFacts(projectFacts, components, cssVariables),
29
+ bridges,
30
+ gaps,
31
+ updateHints: previewBrief.updateStrategy,
32
+ },
33
+ artifact: {
25
34
  generatedAt: new Date().toISOString(),
26
35
  mode: input.mode ?? "build",
27
36
  artifactPath,
37
+ changedFiles: input.changedFiles ?? [],
38
+ },
39
+ repo: {
28
40
  framework: framework.kind,
29
41
  routingStyle: framework.routingStyle,
30
42
  previewRoute: framework.previewRoute,
31
43
  previewEntryFile: framework.previewEntryFile,
32
44
  packageManager: framework.packageManager,
33
- changedFiles: input.changedFiles ?? [],
34
- sourceFiles,
35
45
  },
36
- directories: {
46
+ scan: {
37
47
  componentDirs: projectFacts.likelyComponentDirs,
38
48
  pageDirs: projectFacts.likelyPageDirs,
39
49
  styleFiles: projectFacts.likelyStyleFiles,
@@ -41,31 +51,23 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
41
51
  },
42
52
  tokens: {
43
53
  cssVariables,
44
- styleFiles: projectFacts.likelyStyleFiles,
54
+ themeSourceFiles: projectFacts.likelyStyleFiles,
55
+ },
56
+ components: {
57
+ count: components.length,
58
+ items: components,
45
59
  },
46
- components,
47
60
  preview: {
48
61
  route: projectFacts.previewRoute,
49
62
  entryFile: projectFacts.previewEntryFile,
50
63
  sections: previewBrief.deliverableSpec.sections,
51
64
  updateStrategy: previewBrief.updateStrategy,
52
65
  },
53
- guidance: {
54
- summary: buildSummary(projectFacts, framework, components, cssVariables),
55
- warnings: previewBrief.warnings,
56
- instructions: [
57
- "Read this file before making UI changes or updating /preview.",
58
- "Prefer the listed components, tokens, and preview sections over inventing new UI structure.",
59
- input.mode === "update"
60
- ? "When updating, inspect changedFiles first and only refresh affected sections unless foundations changed."
61
- : "If this artifact looks incomplete, inspect the listed sourceFiles before expanding it.",
62
- ],
63
- },
64
66
  };
65
67
  }
66
68
  export async function writeDesignSystemArtifact(projectRoot, input = {}) {
67
69
  const artifact = await buildDesignSystemArtifact(projectRoot, input);
68
- const artifactFile = join(projectRoot, artifact.meta.artifactPath);
70
+ const artifactFile = join(projectRoot, artifact.artifact.artifactPath);
69
71
  await writeText(artifactFile, `${JSON.stringify(artifact, null, 2)}\n`);
70
72
  return artifact;
71
73
  }
@@ -126,14 +128,6 @@ async function collectComponents(projectRoot, projectFacts) {
126
128
  }));
127
129
  return componentRecords;
128
130
  }
129
- function buildSummary(projectFacts, framework, components, cssVariables) {
130
- return [
131
- `Framework: ${framework.label}.`,
132
- `Preview target: ${projectFacts.previewEntryFile}.`,
133
- `Found ${components.length} component candidates and ${cssVariables.length} CSS variables.`,
134
- "Use this artifact as the canonical machine-readable UI map before editing the app or /preview.",
135
- ].join(" ");
136
- }
137
131
  function extractExports(contents) {
138
132
  const exports = new Set();
139
133
  const patterns = [
@@ -174,3 +168,96 @@ function classifyCssVariable(name) {
174
168
  function lineNumberAt(contents, index) {
175
169
  return contents.slice(0, index).split("\n").length;
176
170
  }
171
+ function buildReadFirst(previewBrief, components) {
172
+ const seen = new Set();
173
+ const readFirst = [];
174
+ const firstComponentByDir = new Map();
175
+ for (const component of components) {
176
+ const slashIndex = component.path.lastIndexOf("/");
177
+ if (slashIndex === -1)
178
+ continue;
179
+ const directory = component.path.slice(0, slashIndex);
180
+ if (!firstComponentByDir.has(directory)) {
181
+ firstComponentByDir.set(directory, component.path);
182
+ }
183
+ }
184
+ for (const step of previewBrief.inspectionPlan) {
185
+ for (const target of step.targets) {
186
+ const normalizedTarget = firstComponentByDir.get(target) ?? target;
187
+ if (seen.has(normalizedTarget))
188
+ continue;
189
+ seen.add(normalizedTarget);
190
+ readFirst.push({
191
+ path: normalizedTarget,
192
+ reason: step.reason,
193
+ });
194
+ if (readFirst.length >= 8) {
195
+ return readFirst;
196
+ }
197
+ }
198
+ }
199
+ return readFirst;
200
+ }
201
+ function buildAgentFacts(projectFacts, components, cssVariables) {
202
+ return [
203
+ `Use ${projectFacts.framework} conventions.`,
204
+ `Implement /preview at ${projectFacts.previewEntryFile}.`,
205
+ "Use repo-relative paths only.",
206
+ `Found ${components.length} component candidates and ${cssVariables.length} CSS variables.`,
207
+ ];
208
+ }
209
+ function buildGapNotes(projectFacts, components, cssVariables) {
210
+ const gaps = [];
211
+ if (components.length === 0) {
212
+ gaps.push("No components were detected. Grep src/components, src/ui, src/features, components, ui, or features before assuming the app has no reusable UI.");
213
+ }
214
+ if (cssVariables.length === 0 && projectFacts.likelyStyleFiles.length > 0) {
215
+ gaps.push(`No CSS variables were detected. Read ${projectFacts.likelyStyleFiles[0]} directly and look for literal classes, theme config, or inline color tokens.`);
216
+ }
217
+ if (projectFacts.likelyStyleFiles.length === 0) {
218
+ gaps.push("No global style files were detected. Inspect app shell files and component code directly for layout and color usage.");
219
+ }
220
+ return gaps;
221
+ }
222
+ async function buildUsageBridges(projectRoot, styleFiles, components) {
223
+ const bridges = [];
224
+ for (const relativePath of styleFiles) {
225
+ const fileContents = await readText(join(projectRoot, relativePath));
226
+ if (!fileContents)
227
+ continue;
228
+ if (fileContents.includes("hsl(var(--")) {
229
+ bridges.push(`Theme tokens are bridged through hsl(var(--...)) in ${relativePath}.`);
230
+ break;
231
+ }
232
+ }
233
+ const hexByFile = await collectHexLiterals(projectRoot, components);
234
+ for (const [path, hexValues] of hexByFile.slice(0, 3)) {
235
+ bridges.push(`Literal accents also appear in ${path}: ${hexValues.join(", ")}.`);
236
+ }
237
+ return bridges;
238
+ }
239
+ async function collectHexLiterals(projectRoot, components) {
240
+ const matches = [];
241
+ for (const component of components) {
242
+ const contents = await readText(join(projectRoot, component.path));
243
+ if (!contents)
244
+ continue;
245
+ const hexMatches = Array.from(contents.matchAll(/#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g))
246
+ .map((match) => normalizeHex(match[0]))
247
+ .filter((value, index, values) => values.indexOf(value) === index);
248
+ if (hexMatches.length > 0) {
249
+ matches.push([component.path, hexMatches.slice(0, 4)]);
250
+ }
251
+ }
252
+ return matches;
253
+ }
254
+ function normalizeHex(hex) {
255
+ const normalized = hex.replace("#", "").toUpperCase();
256
+ if (normalized.length === 3) {
257
+ return `#${normalized
258
+ .split("")
259
+ .map((char) => `${char}${char}`)
260
+ .join("")}`;
261
+ }
262
+ return `#${normalized}`;
263
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { buildPreviewBrief } from "./brief.js";
2
2
  export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
3
3
  export { detectFramework } from "./framework.js";
4
4
  export { createServer, main } from "./server.js";
5
- export type { ComponentRecord, CssVariableRecord, DesignSystemArtifact, DesignSystemBuildInput, DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, ProjectFacts, RoutingStyle, } from "./types.js";
5
+ export { buildPreviewStateSnapshot, runPreviewUpdate, writePreviewStateSnapshot } from "./update.js";
6
+ export type { ComponentRecord, CssVariableRecord, DesignSystemArtifact, DesignSystemBuildInput, DeliverableSpec, FrameworkInfo, FrameworkKind, InspectionStep, PreviewBrief, PreviewStateSnapshot, PreviewUpdateInput, PreviewUpdateResult, ProjectFacts, RoutingStyle, } from "./types.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { buildPreviewBrief } from "./brief.js";
2
2
  export { buildDesignSystemArtifact, writeDesignSystemArtifact } from "./design-system.js";
3
3
  export { detectFramework } from "./framework.js";
4
4
  export { createServer, main } from "./server.js";
5
+ export { buildPreviewStateSnapshot, runPreviewUpdate, writePreviewStateSnapshot } from "./update.js";
package/dist/server.js CHANGED
@@ -4,10 +4,93 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { buildPreviewBrief } from "./brief.js";
6
6
  import { writeDesignSystemArtifact } from "./design-system.js";
7
+ import { runPreviewUpdate } from "./update.js";
8
+ function buildPreviewBriefSchema() {
9
+ return {
10
+ project_facts: z.object({
11
+ framework: z.enum([
12
+ "next-app-router",
13
+ "next-pages-router",
14
+ "react-router",
15
+ "react-no-router",
16
+ "unknown",
17
+ ]),
18
+ routing_style: z.enum([
19
+ "app-router",
20
+ "pages-router",
21
+ "react-router",
22
+ "none",
23
+ "unknown",
24
+ ]),
25
+ preview_route: z.string(),
26
+ preview_entry_file: z.string(),
27
+ package_manager: z.enum(["npm", "pnpm", "yarn", "bun"]),
28
+ likely_component_dirs: z.array(z.string()),
29
+ likely_style_files: z.array(z.string()),
30
+ likely_page_dirs: z.array(z.string()),
31
+ likely_ui_dirs: z.array(z.string()),
32
+ }),
33
+ inspection_plan: z.array(z.object({
34
+ step: z.number(),
35
+ action: z.string(),
36
+ targets: z.array(z.string()),
37
+ reason: z.string(),
38
+ })),
39
+ constraints: z.array(z.string()),
40
+ deliverable_spec: z.object({
41
+ goal: z.string(),
42
+ layout: z.literal("bidirectional-scroll"),
43
+ allow_horizontal_rows: z.boolean(),
44
+ preview_route: z.string(),
45
+ preview_entry_file: z.string(),
46
+ sections: z.array(z.string()),
47
+ use_existing_components_first: z.boolean(),
48
+ interaction_features: z.array(z.string()),
49
+ }),
50
+ update_strategy: z.array(z.string()),
51
+ warnings: z.array(z.string()),
52
+ instructions: z.string(),
53
+ };
54
+ }
55
+ function toStructuredPreviewBrief(brief) {
56
+ return {
57
+ project_facts: {
58
+ framework: brief.projectFacts.framework,
59
+ routing_style: brief.projectFacts.routingStyle,
60
+ preview_route: brief.projectFacts.previewRoute,
61
+ preview_entry_file: brief.projectFacts.previewEntryFile,
62
+ package_manager: brief.projectFacts.packageManager,
63
+ likely_component_dirs: brief.projectFacts.likelyComponentDirs,
64
+ likely_style_files: brief.projectFacts.likelyStyleFiles,
65
+ likely_page_dirs: brief.projectFacts.likelyPageDirs,
66
+ likely_ui_dirs: brief.projectFacts.likelyUiDirs,
67
+ },
68
+ inspection_plan: brief.inspectionPlan.map((item) => ({
69
+ step: item.step,
70
+ action: item.action,
71
+ targets: item.targets,
72
+ reason: item.reason,
73
+ })),
74
+ constraints: brief.constraints,
75
+ deliverable_spec: {
76
+ goal: brief.deliverableSpec.goal,
77
+ layout: brief.deliverableSpec.layout,
78
+ allow_horizontal_rows: brief.deliverableSpec.allowHorizontalRows,
79
+ preview_route: brief.deliverableSpec.previewRoute,
80
+ preview_entry_file: brief.deliverableSpec.previewEntryFile,
81
+ sections: brief.deliverableSpec.sections,
82
+ use_existing_components_first: brief.deliverableSpec.useExistingComponentsFirst,
83
+ interaction_features: brief.deliverableSpec.interactionFeatures,
84
+ },
85
+ update_strategy: brief.updateStrategy,
86
+ warnings: brief.warnings,
87
+ instructions: brief.instructions,
88
+ };
89
+ }
7
90
  export function createServer(projectRoot = process.cwd()) {
8
91
  const server = new McpServer({
9
92
  name: "capy",
10
- version: "1.0.3",
93
+ version: "1.0.4",
11
94
  });
12
95
  server.registerTool("get_preview_brief", {
13
96
  title: "Get Preview Brief",
@@ -26,89 +109,69 @@ export function createServer(projectRoot = process.cwd()) {
26
109
  .optional()
27
110
  .describe("The user's request, so the returned instructions can stay aligned."),
28
111
  },
29
- outputSchema: {
30
- project_facts: z.object({
31
- framework: z.enum([
32
- "next-app-router",
33
- "next-pages-router",
34
- "react-router",
35
- "react-no-router",
36
- "unknown",
37
- ]),
38
- routing_style: z.enum([
39
- "app-router",
40
- "pages-router",
41
- "react-router",
42
- "none",
43
- "unknown",
44
- ]),
45
- preview_route: z.string(),
46
- preview_entry_file: z.string(),
47
- package_manager: z.enum(["npm", "pnpm", "yarn", "bun"]),
48
- likely_component_dirs: z.array(z.string()),
49
- likely_style_files: z.array(z.string()),
50
- likely_page_dirs: z.array(z.string()),
51
- likely_ui_dirs: z.array(z.string()),
52
- }),
53
- inspection_plan: z.array(z.object({
54
- step: z.number(),
55
- action: z.string(),
56
- targets: z.array(z.string()),
57
- reason: z.string(),
58
- })),
59
- constraints: z.array(z.string()),
60
- deliverable_spec: z.object({
61
- goal: z.string(),
62
- layout: z.literal("bidirectional-scroll"),
63
- allow_horizontal_rows: z.boolean(),
64
- preview_route: z.string(),
65
- preview_entry_file: z.string(),
66
- sections: z.array(z.string()),
67
- use_existing_components_first: z.boolean(),
68
- interaction_features: z.array(z.string()),
69
- }),
70
- update_strategy: z.array(z.string()),
71
- warnings: z.array(z.string()),
72
- instructions: z.string(),
73
- },
112
+ outputSchema: buildPreviewBriefSchema(),
74
113
  }, async ({ task = "build_preview", changedFiles, userGoal }) => {
75
114
  const brief = await buildPreviewBrief(projectRoot, {
76
115
  task,
77
116
  changedFiles,
78
117
  userGoal,
79
118
  });
119
+ const structuredContent = toStructuredPreviewBrief(brief);
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: JSON.stringify(structuredContent, null, 2),
125
+ },
126
+ ],
127
+ structuredContent,
128
+ };
129
+ });
130
+ server.registerTool("update_preview", {
131
+ title: "Update Preview",
132
+ description: "Diffs the current repo against Capy's last snapshot, refreshes the design-system artifact, and returns an incremental /preview update brief. Works even when the repo is not a git repository.",
133
+ inputSchema: {
134
+ snapshotPath: z
135
+ .string()
136
+ .default(".capy/preview-state.json")
137
+ .describe("Where Capy stores the previous file-hash snapshot relative to the repo root."),
138
+ designSystemPath: z
139
+ .string()
140
+ .default(".capy/design-system.json")
141
+ .describe("Where to refresh the machine-readable design-system artifact."),
142
+ changedFiles: z
143
+ .array(z.string())
144
+ .optional()
145
+ .describe("Optional manual override for changed files. If omitted, Capy diffs against its last snapshot."),
146
+ userGoal: z
147
+ .string()
148
+ .optional()
149
+ .describe("Optional user request to keep the update brief aligned with the task."),
150
+ },
151
+ outputSchema: {
152
+ snapshot_path: z.string(),
153
+ design_system_path: z.string(),
154
+ changed_files: z.array(z.string()),
155
+ baseline_created: z.boolean(),
156
+ used_manual_changed_files: z.boolean(),
157
+ warnings: z.array(z.string()),
158
+ preview_update_brief: z.object(buildPreviewBriefSchema()),
159
+ },
160
+ }, async ({ snapshotPath = ".capy/preview-state.json", designSystemPath = ".capy/design-system.json", changedFiles, userGoal, }) => {
161
+ const result = await runPreviewUpdate(projectRoot, {
162
+ snapshotPath,
163
+ designSystemPath,
164
+ changedFiles,
165
+ userGoal,
166
+ });
80
167
  const structuredContent = {
81
- project_facts: {
82
- framework: brief.projectFacts.framework,
83
- routing_style: brief.projectFacts.routingStyle,
84
- preview_route: brief.projectFacts.previewRoute,
85
- preview_entry_file: brief.projectFacts.previewEntryFile,
86
- package_manager: brief.projectFacts.packageManager,
87
- likely_component_dirs: brief.projectFacts.likelyComponentDirs,
88
- likely_style_files: brief.projectFacts.likelyStyleFiles,
89
- likely_page_dirs: brief.projectFacts.likelyPageDirs,
90
- likely_ui_dirs: brief.projectFacts.likelyUiDirs,
91
- },
92
- inspection_plan: brief.inspectionPlan.map((item) => ({
93
- step: item.step,
94
- action: item.action,
95
- targets: item.targets,
96
- reason: item.reason,
97
- })),
98
- constraints: brief.constraints,
99
- deliverable_spec: {
100
- goal: brief.deliverableSpec.goal,
101
- layout: brief.deliverableSpec.layout,
102
- allow_horizontal_rows: brief.deliverableSpec.allowHorizontalRows,
103
- preview_route: brief.deliverableSpec.previewRoute,
104
- preview_entry_file: brief.deliverableSpec.previewEntryFile,
105
- sections: brief.deliverableSpec.sections,
106
- use_existing_components_first: brief.deliverableSpec.useExistingComponentsFirst,
107
- interaction_features: brief.deliverableSpec.interactionFeatures,
108
- },
109
- update_strategy: brief.updateStrategy,
110
- warnings: brief.warnings,
111
- instructions: brief.instructions,
168
+ snapshot_path: result.snapshotPath,
169
+ design_system_path: result.designSystemPath,
170
+ changed_files: result.changedFiles,
171
+ baseline_created: result.baselineCreated,
172
+ used_manual_changed_files: result.usedManualChangedFiles,
173
+ warnings: result.warnings,
174
+ preview_update_brief: toStructuredPreviewBrief(result.previewBrief),
112
175
  };
113
176
  return {
114
177
  content: [
@@ -161,7 +224,7 @@ export function createServer(projectRoot = process.cwd()) {
161
224
  preview_entry_file: z.string(),
162
225
  component_count: z.number(),
163
226
  css_variable_count: z.number(),
164
- source_files: z.array(z.string()),
227
+ read_first: z.array(z.string()),
165
228
  warnings: z.array(z.string()),
166
229
  summary: z.string(),
167
230
  },
@@ -173,16 +236,16 @@ export function createServer(projectRoot = process.cwd()) {
173
236
  userGoal,
174
237
  });
175
238
  const structuredContent = {
176
- artifact_path: artifact.meta.artifactPath,
177
- framework: artifact.meta.framework,
178
- routing_style: artifact.meta.routingStyle,
179
- preview_route: artifact.meta.previewRoute,
180
- preview_entry_file: artifact.meta.previewEntryFile,
181
- component_count: artifact.components.length,
239
+ artifact_path: artifact.artifact.artifactPath,
240
+ framework: artifact.repo.framework,
241
+ routing_style: artifact.repo.routingStyle,
242
+ preview_route: artifact.repo.previewRoute,
243
+ preview_entry_file: artifact.repo.previewEntryFile,
244
+ component_count: artifact.components.count,
182
245
  css_variable_count: artifact.tokens.cssVariables.length,
183
- source_files: artifact.meta.sourceFiles,
184
- warnings: artifact.guidance.warnings,
185
- summary: artifact.guidance.summary,
246
+ read_first: artifact.forAgent.readFirst.map((item) => item.path),
247
+ warnings: artifact.forAgent.gaps,
248
+ summary: artifact.forAgent.intent,
186
249
  };
187
250
  return {
188
251
  content: [
package/dist/types.d.ts CHANGED
@@ -66,19 +66,31 @@ export interface ComponentRecord {
66
66
  kind: "primitive" | "component" | "feature" | "unknown";
67
67
  }
68
68
  export interface DesignSystemArtifact {
69
- meta: {
69
+ forAgent: {
70
+ intent: string;
71
+ readFirst: Array<{
72
+ path: string;
73
+ reason: string;
74
+ }>;
75
+ facts: string[];
76
+ bridges: string[];
77
+ gaps: string[];
78
+ updateHints: string[];
79
+ };
80
+ artifact: {
70
81
  generatedAt: string;
71
82
  mode: "build" | "update";
72
83
  artifactPath: string;
84
+ changedFiles: string[];
85
+ };
86
+ repo: {
73
87
  framework: FrameworkKind;
74
88
  routingStyle: RoutingStyle;
75
89
  previewRoute: string;
76
90
  previewEntryFile: string;
77
91
  packageManager: FrameworkInfo["packageManager"];
78
- changedFiles: string[];
79
- sourceFiles: string[];
80
92
  };
81
- directories: {
93
+ scan: {
82
94
  componentDirs: string[];
83
95
  pageDirs: string[];
84
96
  styleFiles: string[];
@@ -86,18 +98,36 @@ export interface DesignSystemArtifact {
86
98
  };
87
99
  tokens: {
88
100
  cssVariables: CssVariableRecord[];
89
- styleFiles: string[];
101
+ themeSourceFiles: string[];
102
+ };
103
+ components: {
104
+ count: number;
105
+ items: ComponentRecord[];
90
106
  };
91
- components: ComponentRecord[];
92
107
  preview: {
93
108
  route: string;
94
109
  entryFile: string;
95
110
  sections: string[];
96
111
  updateStrategy: string[];
97
112
  };
98
- guidance: {
99
- summary: string;
100
- warnings: string[];
101
- instructions: string[];
102
- };
113
+ }
114
+ export interface PreviewStateSnapshot {
115
+ generatedAt: string;
116
+ trackedFiles: Record<string, string>;
117
+ }
118
+ export interface PreviewUpdateInput {
119
+ snapshotPath?: string;
120
+ designSystemPath?: string;
121
+ changedFiles?: string[];
122
+ userGoal?: string;
123
+ }
124
+ export interface PreviewUpdateResult {
125
+ snapshotPath: string;
126
+ designSystemPath: string;
127
+ changedFiles: string[];
128
+ baselineCreated: boolean;
129
+ usedManualChangedFiles: boolean;
130
+ previewBrief: PreviewBrief;
131
+ designSystem: DesignSystemArtifact;
132
+ warnings: string[];
103
133
  }
@@ -0,0 +1,4 @@
1
+ import type { PreviewUpdateInput, PreviewUpdateResult, PreviewStateSnapshot } from "./types.js";
2
+ export declare function runPreviewUpdate(projectRoot: string, input?: PreviewUpdateInput): Promise<PreviewUpdateResult>;
3
+ export declare function buildPreviewStateSnapshot(projectRoot: string): Promise<PreviewStateSnapshot>;
4
+ export declare function writePreviewStateSnapshot(projectRoot: string, snapshotPath: string, snapshot: PreviewStateSnapshot): Promise<void>;
package/dist/update.js ADDED
@@ -0,0 +1,121 @@
1
+ import { createHash } from "crypto";
2
+ import { glob } from "glob";
3
+ import { join } from "path";
4
+ import { buildPreviewBrief } from "./brief.js";
5
+ import { writeDesignSystemArtifact } from "./design-system.js";
6
+ import { readText, toPosixPath, writeText } from "./files.js";
7
+ const DEFAULT_SNAPSHOT_PATH = ".capy/preview-state.json";
8
+ const DEFAULT_DESIGN_SYSTEM_PATH = ".capy/design-system.json";
9
+ export async function runPreviewUpdate(projectRoot, input = {}) {
10
+ const snapshotPath = input.snapshotPath ?? DEFAULT_SNAPSHOT_PATH;
11
+ const designSystemPath = input.designSystemPath ?? DEFAULT_DESIGN_SYSTEM_PATH;
12
+ const usedManualChangedFiles = Array.isArray(input.changedFiles);
13
+ const diffResult = usedManualChangedFiles
14
+ ? {
15
+ baselineCreated: false,
16
+ changedFiles: uniqueSorted(input.changedFiles ?? []),
17
+ snapshot: await buildPreviewStateSnapshot(projectRoot),
18
+ }
19
+ : await detectChangedFilesFromSnapshot(projectRoot, snapshotPath);
20
+ const previewBrief = await buildPreviewBrief(projectRoot, {
21
+ task: "update_preview",
22
+ changedFiles: diffResult.changedFiles,
23
+ userGoal: input.userGoal,
24
+ });
25
+ const designSystem = await writeDesignSystemArtifact(projectRoot, {
26
+ artifactPath: designSystemPath,
27
+ mode: "update",
28
+ changedFiles: diffResult.changedFiles,
29
+ userGoal: input.userGoal,
30
+ });
31
+ await writePreviewStateSnapshot(projectRoot, snapshotPath, diffResult.snapshot);
32
+ const warnings = [...previewBrief.warnings];
33
+ if (diffResult.baselineCreated) {
34
+ warnings.unshift("No prior preview-state snapshot was found. Capy created a baseline snapshot, so this run cannot infer incremental changes automatically.");
35
+ }
36
+ if (usedManualChangedFiles) {
37
+ warnings.unshift("Using caller-provided changedFiles instead of auto-detected snapshot diffs.");
38
+ }
39
+ return {
40
+ snapshotPath,
41
+ designSystemPath,
42
+ changedFiles: diffResult.changedFiles,
43
+ baselineCreated: diffResult.baselineCreated,
44
+ usedManualChangedFiles,
45
+ previewBrief,
46
+ designSystem,
47
+ warnings,
48
+ };
49
+ }
50
+ export async function buildPreviewStateSnapshot(projectRoot) {
51
+ const trackedFiles = await glob(["**/*.{ts,tsx,js,jsx,css,scss,sass,less,vue,svelte}"], {
52
+ cwd: projectRoot,
53
+ nodir: true,
54
+ ignore: [
55
+ "**/node_modules/**",
56
+ "**/.next/**",
57
+ "**/dist/**",
58
+ "**/build/**",
59
+ "**/coverage/**",
60
+ "**/.git/**",
61
+ "**/.capy/**",
62
+ ],
63
+ });
64
+ const fileHashes = {};
65
+ for (const file of trackedFiles.map((item) => toPosixPath(item)).sort()) {
66
+ const contents = await readText(join(projectRoot, file));
67
+ if (contents === null)
68
+ continue;
69
+ fileHashes[file] = createHash("sha1").update(contents).digest("hex");
70
+ }
71
+ return {
72
+ generatedAt: new Date().toISOString(),
73
+ trackedFiles: fileHashes,
74
+ };
75
+ }
76
+ export async function writePreviewStateSnapshot(projectRoot, snapshotPath, snapshot) {
77
+ await writeText(join(projectRoot, snapshotPath), `${JSON.stringify(snapshot, null, 2)}\n`);
78
+ }
79
+ async function detectChangedFilesFromSnapshot(projectRoot, snapshotPath) {
80
+ const snapshot = await buildPreviewStateSnapshot(projectRoot);
81
+ const previous = await readSnapshot(projectRoot, snapshotPath);
82
+ if (!previous) {
83
+ return {
84
+ baselineCreated: true,
85
+ changedFiles: [],
86
+ snapshot,
87
+ };
88
+ }
89
+ const changedFiles = new Set();
90
+ const previousFiles = previous.trackedFiles;
91
+ const currentFiles = snapshot.trackedFiles;
92
+ for (const [path, hash] of Object.entries(currentFiles)) {
93
+ if (previousFiles[path] !== hash) {
94
+ changedFiles.add(path);
95
+ }
96
+ }
97
+ for (const path of Object.keys(previousFiles)) {
98
+ if (!(path in currentFiles)) {
99
+ changedFiles.add(path);
100
+ }
101
+ }
102
+ return {
103
+ baselineCreated: false,
104
+ changedFiles: Array.from(changedFiles).sort(),
105
+ snapshot,
106
+ };
107
+ }
108
+ async function readSnapshot(projectRoot, snapshotPath) {
109
+ const raw = await readText(join(projectRoot, snapshotPath));
110
+ if (!raw)
111
+ return null;
112
+ try {
113
+ return JSON.parse(raw);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ function uniqueSorted(values) {
120
+ return Array.from(new Set(values)).sort();
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capy-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server that inspects a repo, returns a structured /preview brief, and writes a design-system JSON artifact for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",