@bitovi/vybit 0.4.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/LICENSE +21 -0
- package/README.md +155 -0
- package/loader.mjs +11 -0
- package/overlay/dist/.gitkeep +0 -0
- package/overlay/dist/overlay.js +1547 -0
- package/package.json +57 -0
- package/panel/dist/assets/index-BUKLf5aN.css +1 -0
- package/panel/dist/assets/index-Cr2RD_Gn.js +549 -0
- package/panel/dist/index.html +25 -0
- package/server/app.ts +117 -0
- package/server/index.ts +57 -0
- package/server/mcp-tools.ts +356 -0
- package/server/queue.ts +281 -0
- package/server/tailwind-adapter.ts +17 -0
- package/server/tailwind-v3.ts +159 -0
- package/server/tailwind-v4.ts +160 -0
- package/server/tailwind.ts +50 -0
- package/server/tests/server.integration.test.ts +698 -0
- package/server/websocket.ts +130 -0
- package/shared/types.ts +304 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Tailwind Visual Editor</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&family=Inter:wght@400;600;700&family=Roboto+Mono:wght@400&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
html, body, #root { height: 100%; }
|
|
12
|
+
body {
|
|
13
|
+
background: #FFFFFF;
|
|
14
|
+
color: #334041;
|
|
15
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
16
|
+
font-size: 12px;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
<script type="module" crossorigin src="/panel/assets/index-Cr2RD_Gn.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/panel/assets/index-BUKLf5aN.css">
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="root"></div>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
package/server/app.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Express app factory
|
|
2
|
+
|
|
3
|
+
import express from "express";
|
|
4
|
+
import cors from "cors";
|
|
5
|
+
import { request as makeRequest } from "http";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
import { getByStatus, getQueueUpdate, clearAll } from "./queue.js";
|
|
9
|
+
import { resolveTailwindConfig, generateCssForClasses, getTailwindVersion } from "./tailwind.js";
|
|
10
|
+
import type { PatchStatus } from "../shared/types.js";
|
|
11
|
+
|
|
12
|
+
const VALID_STATUSES = new Set<string>(['staged', 'committed', 'implementing', 'implemented', 'error']);
|
|
13
|
+
|
|
14
|
+
export function createApp(packageRoot: string): express.Express {
|
|
15
|
+
const app = express();
|
|
16
|
+
app.use(cors());
|
|
17
|
+
|
|
18
|
+
app.get("/overlay.js", (_req, res) => {
|
|
19
|
+
const overlayPath = path.join(packageRoot, "overlay", "dist", "overlay.js");
|
|
20
|
+
res.sendFile(overlayPath, (err) => {
|
|
21
|
+
if (err) {
|
|
22
|
+
console.error("[http] Failed to serve overlay.js:", err);
|
|
23
|
+
if (!res.headersSent) res.status(404).end();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.get("/api/info", async (_req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const tailwindVersion = await getTailwindVersion();
|
|
31
|
+
res.json({ tailwindVersion });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error("[http] Failed to detect tailwind version:", err);
|
|
34
|
+
res.status(500).json({ error: "Failed to detect Tailwind version" });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.get("/tailwind-config", async (_req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const config = await resolveTailwindConfig();
|
|
41
|
+
res.json(config);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[http] Failed to resolve tailwind config:", err);
|
|
44
|
+
res.status(500).json({ error: "Failed to resolve Tailwind config" });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.post("/css", express.json(), async (req, res) => {
|
|
49
|
+
const { classes } = req.body as { classes?: unknown };
|
|
50
|
+
if (!Array.isArray(classes) || classes.some((c) => typeof c !== "string")) {
|
|
51
|
+
res.status(400).json({ error: "classes must be an array of strings" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const css = await generateCssForClasses(classes as string[]);
|
|
56
|
+
res.json({ css });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("[http] Failed to generate CSS:", err);
|
|
59
|
+
res.status(500).json({ error: "Failed to generate CSS" });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// --- Patch state REST endpoint ---
|
|
64
|
+
app.get("/patches", (_req, res) => {
|
|
65
|
+
const status = _req.query.status as string | undefined;
|
|
66
|
+
if (status) {
|
|
67
|
+
if (!VALID_STATUSES.has(status)) {
|
|
68
|
+
res.status(400).json({ error: `Invalid status. Must be one of: ${[...VALID_STATUSES].join(', ')}` });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
res.json(getByStatus(status as PatchStatus));
|
|
72
|
+
} else {
|
|
73
|
+
const update = getQueueUpdate();
|
|
74
|
+
res.json(update);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.delete("/patches", (_req, res) => {
|
|
79
|
+
clearAll();
|
|
80
|
+
res.json({ ok: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// --- Serve Panel app ---
|
|
84
|
+
if (process.env.PANEL_DEV) {
|
|
85
|
+
const panelDevPort = Number(process.env.PANEL_DEV_PORT) || 5174;
|
|
86
|
+
console.error(`[server] Panel dev mode: proxying /panel → http://localhost:${panelDevPort}`);
|
|
87
|
+
app.use("/panel", (req, res) => {
|
|
88
|
+
const proxyReq = makeRequest(
|
|
89
|
+
{
|
|
90
|
+
hostname: "localhost",
|
|
91
|
+
port: panelDevPort,
|
|
92
|
+
path: "/panel" + req.url,
|
|
93
|
+
method: req.method,
|
|
94
|
+
headers: { ...req.headers, host: `localhost:${panelDevPort}` },
|
|
95
|
+
},
|
|
96
|
+
(proxyRes) => {
|
|
97
|
+
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
98
|
+
proxyRes.pipe(res, { end: true });
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
proxyReq.on("error", () => {
|
|
102
|
+
if (!res.headersSent) res.status(502).send("Panel dev server not running on port " + panelDevPort);
|
|
103
|
+
});
|
|
104
|
+
req.pipe(proxyReq, { end: true });
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
const panelDist = path.join(packageRoot, "panel", "dist");
|
|
108
|
+
app.use("/panel", express.static(panelDist));
|
|
109
|
+
app.get("/panel/*", (_req, res) => {
|
|
110
|
+
res.sendFile(path.join(panelDist, "index.html"), (err) => {
|
|
111
|
+
if (err && !res.headersSent) res.status(404).end();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return app;
|
|
117
|
+
}
|
package/server/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// MCP Server entrypoint
|
|
4
|
+
|
|
5
|
+
import { createServer } from "http";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
|
|
12
|
+
import { getByStatus, getCounts, getNextCommitted, markCommitImplementing, markCommitImplemented, markImplementing, markImplemented, clearAll, onCommitted, getQueueUpdate } from "./queue.js";
|
|
13
|
+
import { createApp } from "./app.js";
|
|
14
|
+
import { setupWebSocket } from "./websocket.js";
|
|
15
|
+
import { registerMcpTools } from "./mcp-tools.js";
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const packageRoot = __dirname.includes(`${path.sep}dist${path.sep}`)
|
|
20
|
+
? path.resolve(__dirname, "..", "..")
|
|
21
|
+
: path.resolve(__dirname, "..");
|
|
22
|
+
|
|
23
|
+
const port = Number(process.env.PORT) || 3333;
|
|
24
|
+
|
|
25
|
+
// --- HTTP + WebSocket ---
|
|
26
|
+
const app = createApp(packageRoot);
|
|
27
|
+
const httpServer = createServer(app);
|
|
28
|
+
const { broadcastPatchUpdate } = setupWebSocket(httpServer);
|
|
29
|
+
|
|
30
|
+
httpServer.listen(port, () => {
|
|
31
|
+
console.error(`[server] HTTP + WS listening on http://localhost:${port}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// --- MCP Server (stdio) ---
|
|
35
|
+
const mcp = new McpServer(
|
|
36
|
+
{ name: "vybit", version: "0.1.0" },
|
|
37
|
+
{ capabilities: { tools: {} } },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
registerMcpTools(mcp, {
|
|
41
|
+
broadcastPatchUpdate,
|
|
42
|
+
getNextCommitted,
|
|
43
|
+
onCommitted,
|
|
44
|
+
markCommitImplementing,
|
|
45
|
+
markCommitImplemented,
|
|
46
|
+
markImplementing,
|
|
47
|
+
markImplemented,
|
|
48
|
+
getByStatus,
|
|
49
|
+
getCounts,
|
|
50
|
+
getQueueUpdate,
|
|
51
|
+
clearAll,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const transport = new StdioServerTransport();
|
|
55
|
+
await mcp.connect(transport);
|
|
56
|
+
console.error("[mcp] MCP server connected via stdio");
|
|
57
|
+
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// MCP tool registration
|
|
2
|
+
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import type { Patch, PatchStatus, Commit } from "../shared/types.js";
|
|
7
|
+
import type { PatchResult } from "./queue.js";
|
|
8
|
+
|
|
9
|
+
export interface McpToolDeps {
|
|
10
|
+
broadcastPatchUpdate: () => void;
|
|
11
|
+
getNextCommitted: () => Commit | null;
|
|
12
|
+
onCommitted: (listener: () => void) => () => void;
|
|
13
|
+
markCommitImplementing: (commitId: string) => void;
|
|
14
|
+
markCommitImplemented: (commitId: string, results: PatchResult[]) => void;
|
|
15
|
+
// Legacy per-patch methods (backward compat)
|
|
16
|
+
markImplementing: (ids: string[]) => number;
|
|
17
|
+
markImplemented: (ids: string[]) => number;
|
|
18
|
+
getByStatus: (status: PatchStatus) => Patch[];
|
|
19
|
+
getCounts: () => { staged: number; committed: number; implementing: number; implemented: number };
|
|
20
|
+
getQueueUpdate: () => any;
|
|
21
|
+
clearAll: () => { staged: number; committed: number; implementing: number; implemented: number };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KEEPALIVE_INTERVAL_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Prompt builders for implement_next_change
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function buildCommitInstructions(commit: Commit, remainingCount: number): string {
|
|
31
|
+
const classChanges = commit.patches.filter(p => p.kind === 'class-change');
|
|
32
|
+
const messages = commit.patches.filter(p => p.kind === 'message');
|
|
33
|
+
const designs = commit.patches.filter(p => p.kind === 'design');
|
|
34
|
+
const moreText = remainingCount > 0
|
|
35
|
+
? `${remainingCount} more commit${remainingCount === 1 ? '' : 's'} waiting in the queue after this one.`
|
|
36
|
+
: 'This is the last commit in the queue. After implementing it, call `implement_next_change` again to wait for future changes.';
|
|
37
|
+
|
|
38
|
+
let patchList = '';
|
|
39
|
+
let stepNum = 1;
|
|
40
|
+
for (const patch of commit.patches) {
|
|
41
|
+
if (patch.kind === 'class-change') {
|
|
42
|
+
const comp = patch.component?.name ?? 'unknown component';
|
|
43
|
+
const tag = patch.target?.tag ?? 'element';
|
|
44
|
+
const context = patch.context ?? '';
|
|
45
|
+
patchList += `### ${stepNum}. Class change \`${patch.id}\`
|
|
46
|
+
- **Component:** \`${comp}\`
|
|
47
|
+
- **Element:** \`<${tag}>\`
|
|
48
|
+
- **Class change:** \`${patch.originalClass}\` → \`${patch.newClass}\`
|
|
49
|
+
- **Property:** ${patch.property}
|
|
50
|
+
${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
|
|
51
|
+
`;
|
|
52
|
+
} else if (patch.kind === 'message') {
|
|
53
|
+
patchList += `### ${stepNum}. User message
|
|
54
|
+
> ${patch.message}
|
|
55
|
+
${patch.elementKey ? `\n_Scoped to: ${patch.elementKey}_\n` : ''}
|
|
56
|
+
`;
|
|
57
|
+
} else if (patch.kind === 'design') {
|
|
58
|
+
const comp = patch.component?.name ?? 'unknown component';
|
|
59
|
+
const tag = patch.target?.tag ?? 'element';
|
|
60
|
+
const context = patch.context ?? '';
|
|
61
|
+
patchList += `### ${stepNum}. Design sketch \`${patch.id}\`
|
|
62
|
+
- **Component:** \`${comp}\`
|
|
63
|
+
- **Element:** \`<${tag}>\`
|
|
64
|
+
- **Insert position:** ${patch.insertMode ?? 'after'} the element
|
|
65
|
+
- **Canvas size:** ${patch.canvasWidth ?? '?'}×${patch.canvasHeight ?? '?'}px
|
|
66
|
+
- The design image is included as a separate image content part below — refer to it for the visual intent.
|
|
67
|
+
${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
stepNum++;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build summary parts
|
|
74
|
+
const summaryParts: string[] = [];
|
|
75
|
+
if (classChanges.length) summaryParts.push(`${classChanges.length} class change${classChanges.length === 1 ? '' : 's'}`);
|
|
76
|
+
if (messages.length) summaryParts.push(`${messages.length} message${messages.length === 1 ? '' : 's'}`);
|
|
77
|
+
if (designs.length) summaryParts.push(`${designs.length} design${designs.length === 1 ? '' : 's'}`);
|
|
78
|
+
|
|
79
|
+
const resultsPart = classChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
|
|
80
|
+
|
|
81
|
+
// Design patches also need to be reported in results
|
|
82
|
+
const designResultsPart = designs.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
|
|
83
|
+
const allResultsPart = [resultsPart, designResultsPart].filter(Boolean).join(',\n');
|
|
84
|
+
|
|
85
|
+
return `# IMPLEMENT THIS COMMIT — then call implement_next_change again
|
|
86
|
+
|
|
87
|
+
⚠️ **DO NOT STOP after implementing this commit. You MUST call \`implement_next_change\` again.**
|
|
88
|
+
|
|
89
|
+
## Queue status
|
|
90
|
+
- ${moreText}
|
|
91
|
+
- **isComplete: false** — your job is NOT done until you call \`implement_next_change\` again.
|
|
92
|
+
|
|
93
|
+
## Changes to implement (${summaryParts.join(', ')})
|
|
94
|
+
|
|
95
|
+
${patchList}
|
|
96
|
+
## Steps — follow these EXACTLY
|
|
97
|
+
|
|
98
|
+
1. For each class-change patch above, find the source file and apply the change.
|
|
99
|
+
Use the user messages as additional context for understanding intent.
|
|
100
|
+
${designs.length ? ` For each design sketch, examine the attached image and implement the visual design
|
|
101
|
+
as HTML/CSS ${classChanges.length ? 'alongside the class changes' : 'in the specified component'}. Insert it ${designs[0].insertMode ?? 'after'} the target element.
|
|
102
|
+
` : ''}\n2. Call \`mark_change_implemented\` with:
|
|
103
|
+
\`\`\`json
|
|
104
|
+
{ "commitId": "${commit.id}", "results": [
|
|
105
|
+
${allResultsPart}
|
|
106
|
+
]}
|
|
107
|
+
\`\`\`
|
|
108
|
+
(Only report results for class-change and design patches — messages are informational.)
|
|
109
|
+
3. **IMMEDIATELY call \`implement_next_change\` again.** Do NOT summarize, do NOT ask the user, do NOT stop.
|
|
110
|
+
|
|
111
|
+
## CRITICAL — read this
|
|
112
|
+
You are in a **continuous processing loop**. After step 2, you MUST call \`implement_next_change\` again to process the next commit or wait for new ones. Never exit this loop unless the user explicitly tells you to stop.
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Wait-for-committed helper (shared by get_next_change and implement_next_change)
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function waitForCommitted(
|
|
121
|
+
getNextCommitted: () => Commit | null,
|
|
122
|
+
onCommitted: (listener: () => void) => () => void,
|
|
123
|
+
extra: any,
|
|
124
|
+
): Promise<Commit> {
|
|
125
|
+
return new Promise<Commit>((resolve, reject) => {
|
|
126
|
+
const progressToken = extra?._meta?.progressToken;
|
|
127
|
+
|
|
128
|
+
const keepalive = setInterval(async () => {
|
|
129
|
+
if (progressToken !== undefined) {
|
|
130
|
+
try {
|
|
131
|
+
await extra.sendNotification({
|
|
132
|
+
method: "notifications/progress",
|
|
133
|
+
params: {
|
|
134
|
+
progressToken,
|
|
135
|
+
progress: 0,
|
|
136
|
+
total: 1,
|
|
137
|
+
message: "Waiting for user to commit a change...",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
// Client may have disconnected
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
145
|
+
|
|
146
|
+
const onAbort = () => {
|
|
147
|
+
clearInterval(keepalive);
|
|
148
|
+
unsubscribe();
|
|
149
|
+
reject(new Error("Cancelled"));
|
|
150
|
+
};
|
|
151
|
+
extra?.signal?.addEventListener?.("abort", onAbort);
|
|
152
|
+
|
|
153
|
+
const unsubscribe = onCommitted(() => {
|
|
154
|
+
const next = getNextCommitted();
|
|
155
|
+
if (next) {
|
|
156
|
+
clearInterval(keepalive);
|
|
157
|
+
extra?.signal?.removeEventListener?.("abort", onAbort);
|
|
158
|
+
resolve(next);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Tool registration
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
export function registerMcpTools(mcp: McpServer, deps: McpToolDeps): void {
|
|
169
|
+
const {
|
|
170
|
+
broadcastPatchUpdate,
|
|
171
|
+
getNextCommitted,
|
|
172
|
+
onCommitted,
|
|
173
|
+
markCommitImplementing,
|
|
174
|
+
markCommitImplemented,
|
|
175
|
+
markImplementing,
|
|
176
|
+
markImplemented,
|
|
177
|
+
getByStatus,
|
|
178
|
+
getCounts,
|
|
179
|
+
getQueueUpdate,
|
|
180
|
+
clearAll,
|
|
181
|
+
} = deps;
|
|
182
|
+
|
|
183
|
+
// --- get_next_change ---
|
|
184
|
+
mcp.tool(
|
|
185
|
+
"get_next_change",
|
|
186
|
+
"Waits for and returns the next committed change (full commit with all patches). " +
|
|
187
|
+
"Transitions the commit to 'implementing' status. " +
|
|
188
|
+
"Returns only the raw commit data — no workflow instructions. " +
|
|
189
|
+
"Use implement_next_change instead if you want guided implementation with auto-looping.",
|
|
190
|
+
async (_extra) => {
|
|
191
|
+
const extra = _extra as any;
|
|
192
|
+
|
|
193
|
+
let commit = getNextCommitted();
|
|
194
|
+
if (!commit) {
|
|
195
|
+
commit = await waitForCommitted(getNextCommitted, onCommitted, extra);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
markCommitImplementing(commit.id);
|
|
199
|
+
broadcastPatchUpdate();
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text" as const,
|
|
205
|
+
text: JSON.stringify(commit, null, 2),
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// --- implement_next_change ---
|
|
213
|
+
mcp.tool(
|
|
214
|
+
"implement_next_change",
|
|
215
|
+
"CONTINUOUS LOOP: Waits for the next committed change (a commit with class-changes and context messages), " +
|
|
216
|
+
"returns implementation instructions, and REQUIRES the agent to apply all class-changes, mark them done, " +
|
|
217
|
+
"then call this tool AGAIN. Messages in the commit provide context for understanding intent. " +
|
|
218
|
+
"The agent MUST keep calling this tool in a loop until explicitly stopped by the user.",
|
|
219
|
+
async (_extra) => {
|
|
220
|
+
const extra = _extra as any;
|
|
221
|
+
|
|
222
|
+
let commit = getNextCommitted();
|
|
223
|
+
if (!commit) {
|
|
224
|
+
commit = await waitForCommitted(getNextCommitted, onCommitted, extra);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
markCommitImplementing(commit.id);
|
|
228
|
+
broadcastPatchUpdate();
|
|
229
|
+
|
|
230
|
+
// Count remaining committed commits (excluding this one, which is now 'implementing')
|
|
231
|
+
const queueState = getQueueUpdate();
|
|
232
|
+
const remaining = queueState.committedCount;
|
|
233
|
+
|
|
234
|
+
// Build content parts: JSON data, then any design images, then markdown instructions
|
|
235
|
+
const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [
|
|
236
|
+
{
|
|
237
|
+
type: "text" as const,
|
|
238
|
+
text: JSON.stringify({
|
|
239
|
+
isComplete: false,
|
|
240
|
+
nextAction: "implement all class-change patches in this commit, call mark_change_implemented, then call implement_next_change again",
|
|
241
|
+
remainingCommits: remaining,
|
|
242
|
+
commit,
|
|
243
|
+
}, null, 2),
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// Add design images as separate image content parts so the agent can see them
|
|
248
|
+
for (const patch of commit.patches) {
|
|
249
|
+
if (patch.kind === 'design' && patch.image) {
|
|
250
|
+
const match = patch.image.match(/^data:([^;]+);base64,(.+)$/);
|
|
251
|
+
if (match) {
|
|
252
|
+
content.push({
|
|
253
|
+
type: "image" as const,
|
|
254
|
+
data: match[2],
|
|
255
|
+
mimeType: match[1],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
content.push({
|
|
262
|
+
type: "text" as const,
|
|
263
|
+
text: buildCommitInstructions(commit, remaining),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return { content };
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// --- mark_change_implemented ---
|
|
271
|
+
mcp.tool(
|
|
272
|
+
"mark_change_implemented",
|
|
273
|
+
"Marks changes as implemented after the agent has applied them to source code. " +
|
|
274
|
+
"Accepts either commitId + per-patch results (new) or legacy ids array. " +
|
|
275
|
+
"After calling this, you MUST call implement_next_change again to continue processing.",
|
|
276
|
+
{
|
|
277
|
+
commitId: z.string().optional().describe("The commit ID (new commit-based flow)"),
|
|
278
|
+
results: z.array(z.object({
|
|
279
|
+
patchId: z.string().describe("ID of a class-change patch"),
|
|
280
|
+
success: z.boolean(),
|
|
281
|
+
error: z.string().optional(),
|
|
282
|
+
})).optional().describe("Per-patch results (class-change patches only — skip message patches)"),
|
|
283
|
+
ids: z.array(z.string()).optional().describe("Legacy: Patch IDs to mark as implemented"),
|
|
284
|
+
},
|
|
285
|
+
async ({ commitId, results, ids }) => {
|
|
286
|
+
let moved = 0;
|
|
287
|
+
|
|
288
|
+
if (commitId && results) {
|
|
289
|
+
// New commit-based flow
|
|
290
|
+
markCommitImplemented(commitId, results);
|
|
291
|
+
moved = results.filter(r => r.success).length;
|
|
292
|
+
} else if (ids) {
|
|
293
|
+
// Legacy per-patch flow
|
|
294
|
+
moved = markImplemented(ids);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
broadcastPatchUpdate();
|
|
298
|
+
|
|
299
|
+
const counts = getCounts();
|
|
300
|
+
const remaining = counts.committed + counts.implementing;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text" as const,
|
|
306
|
+
text: JSON.stringify({
|
|
307
|
+
moved,
|
|
308
|
+
isComplete: false,
|
|
309
|
+
nextAction: "call implement_next_change NOW to process the next change",
|
|
310
|
+
remainingInQueue: remaining,
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: "text" as const,
|
|
315
|
+
text: `✅ Marked ${moved} change(s) as implemented.\n\n` +
|
|
316
|
+
`⚠️ **YOUR NEXT STEP:** Call \`implement_next_change\` NOW.\n` +
|
|
317
|
+
`Do NOT stop. Do NOT summarize. Do NOT ask the user what to do.\n` +
|
|
318
|
+
`${remaining > 0 ? `There are ${remaining} more change(s) to process.` : 'No more changes right now — call implement_next_change to wait for the next one.'}`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// --- list_changes ---
|
|
326
|
+
mcp.tool(
|
|
327
|
+
"list_changes",
|
|
328
|
+
"Lists changes grouped by commit status. Optionally filter by a specific status.",
|
|
329
|
+
{ status: z.enum(["staged", "committed", "implementing", "implemented", "error"]).optional().describe("Filter by patch status") },
|
|
330
|
+
async ({ status }) => {
|
|
331
|
+
if (status) {
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text" as const, text: JSON.stringify(getByStatus(status), null, 2) }],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const queueState = getQueueUpdate();
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text" as const, text: JSON.stringify(queueState, null, 2) }],
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// --- discard_all_changes ---
|
|
344
|
+
mcp.tool(
|
|
345
|
+
"discard_all_changes",
|
|
346
|
+
"Discards all changes regardless of status",
|
|
347
|
+
async () => {
|
|
348
|
+
const counts = clearAll();
|
|
349
|
+
broadcastPatchUpdate();
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text" as const, text: JSON.stringify(counts) }],
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
}
|