weapp-agent 0.1.0-preview.1
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 +22 -0
- package/dist/index.mjs +1571 -0
- package/package.json +65 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, truncate, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { createTwoFilesPatch } from "diff";
|
|
11
|
+
import { execa } from "execa";
|
|
12
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
13
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
14
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
16
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
17
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
18
|
+
import { jsonSchema, streamText, tool } from "ai";
|
|
19
|
+
import { Command, Option } from "commander";
|
|
20
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
21
|
+
import TextInput from "ink-text-input";
|
|
22
|
+
import { useRef, useState } from "react";
|
|
23
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
24
|
+
//#region ../../packages/core/dist/index.mjs
|
|
25
|
+
const verificationSchema = z.object({
|
|
26
|
+
kind: z.enum([
|
|
27
|
+
"typecheck",
|
|
28
|
+
"build",
|
|
29
|
+
"test",
|
|
30
|
+
"devtools"
|
|
31
|
+
]),
|
|
32
|
+
command: z.string().min(1),
|
|
33
|
+
args: z.array(z.string()).default([]),
|
|
34
|
+
timeoutMs: z.number().int().positive().max(18e5).default(12e4)
|
|
35
|
+
}).strict();
|
|
36
|
+
const mcpSchema = z.discriminatedUnion("transport", [z.object({
|
|
37
|
+
name: z.string().regex(/^[\w-]+$/),
|
|
38
|
+
transport: z.literal("stdio"),
|
|
39
|
+
command: z.string(),
|
|
40
|
+
args: z.array(z.string()).default([])
|
|
41
|
+
}).strict(), z.object({
|
|
42
|
+
name: z.string().regex(/^[\w-]+$/),
|
|
43
|
+
transport: z.literal("http"),
|
|
44
|
+
url: z.string().url(),
|
|
45
|
+
tokenEnv: z.string().optional()
|
|
46
|
+
}).strict()]);
|
|
47
|
+
const configSchema = z.object({
|
|
48
|
+
version: z.literal(1).default(1),
|
|
49
|
+
model: z.object({
|
|
50
|
+
provider: z.enum([
|
|
51
|
+
"openai",
|
|
52
|
+
"anthropic",
|
|
53
|
+
"openai-compatible"
|
|
54
|
+
]),
|
|
55
|
+
name: z.string().min(1),
|
|
56
|
+
baseURL: z.string().url().optional(),
|
|
57
|
+
apiKeyEnv: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional()
|
|
58
|
+
}).strict(),
|
|
59
|
+
maxSteps: z.number().int().min(1).max(500).default(40),
|
|
60
|
+
timeoutMs: z.number().int().positive().default(6e5),
|
|
61
|
+
contextCharacters: z.number().int().min(8e3).default(1e5),
|
|
62
|
+
verification: z.array(verificationSchema).default([]),
|
|
63
|
+
mcp: z.array(mcpSchema).default([])
|
|
64
|
+
}).strict();
|
|
65
|
+
const configFilename = "weapp-agent.config.json";
|
|
66
|
+
async function loadConfig(root) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await readFile(path.join(root, configFilename), "utf8");
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error(`Missing ${configFilename}. Run weapp-agent init --provider openai --model <model>.`);
|
|
72
|
+
}
|
|
73
|
+
return configSchema.parse(JSON.parse(raw));
|
|
74
|
+
}
|
|
75
|
+
function hash(value) {
|
|
76
|
+
return createHash("sha256").update(value).digest("hex");
|
|
77
|
+
}
|
|
78
|
+
function stateRoot() {
|
|
79
|
+
return path.resolve(process.env.WEAPP_AGENT_STATE_DIR ?? path.join(homedir(), ".local", "state", "weapp-agent"));
|
|
80
|
+
}
|
|
81
|
+
async function projectFingerprint(root, config) {
|
|
82
|
+
let pkg = "";
|
|
83
|
+
try {
|
|
84
|
+
pkg = await readFile(path.join(root, "package.json"), "utf8");
|
|
85
|
+
} catch {}
|
|
86
|
+
let diskConfig = "";
|
|
87
|
+
try {
|
|
88
|
+
diskConfig = await readFile(path.join(root, configFilename), "utf8");
|
|
89
|
+
} catch {}
|
|
90
|
+
return hash(JSON.stringify({
|
|
91
|
+
root,
|
|
92
|
+
config,
|
|
93
|
+
pkg,
|
|
94
|
+
diskConfig
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
async function isTrusted(root, fingerprint) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(await readFile(path.join(stateRoot(), "trust", `${hash(root)}.json`), "utf8")).fingerprint === fingerprint;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function trustProject(root, fingerprint) {
|
|
105
|
+
const dir = path.join(stateRoot(), "trust");
|
|
106
|
+
await mkdir(dir, {
|
|
107
|
+
recursive: true,
|
|
108
|
+
mode: 448
|
|
109
|
+
});
|
|
110
|
+
await writeFile(path.join(dir, `${hash(root)}.json`), JSON.stringify({
|
|
111
|
+
root,
|
|
112
|
+
fingerprint
|
|
113
|
+
}), { mode: 384 });
|
|
114
|
+
}
|
|
115
|
+
var ApprovalRequired = class extends Error {
|
|
116
|
+
constructor(message) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "ApprovalRequired";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function redactor(environment = process.env) {
|
|
122
|
+
const secrets = Object.entries(environment).filter(([k, v]) => /KEY|TOKEN|SECRET|PASSWORD/i.test(k) && v && v.length >= 8).map(([, v]) => v).sort((a, b) => b.length - a.length);
|
|
123
|
+
return (text) => {
|
|
124
|
+
for (const secret of secrets) text = text.split(secret).join("[REDACTED]");
|
|
125
|
+
return text.replace(/\b(sk-[\w-]{12,}|gh[pousr]_\w{16,})\b/g, "[REDACTED]").replace(/(Bearer\s+)[\w.\-/+=]{10,}/gi, "$1[REDACTED]");
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function inside(root, target) {
|
|
129
|
+
const relative = path.relative(root, target);
|
|
130
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
131
|
+
}
|
|
132
|
+
function sensitive(file) {
|
|
133
|
+
return file.split(/[\\/]/).some((part) => /^(?:\.git|\.weapp-agent|\.npmrc|\.env(?:\..+)?|.*\.(?:pem|key|p12))$/i.test(part) && part !== ".env.example");
|
|
134
|
+
}
|
|
135
|
+
/** Canonicalize ancestors even for new files: symlink escapes cannot bypass the boundary. */
|
|
136
|
+
async function safePath(root, file) {
|
|
137
|
+
const canonicalRoot = await realpath(root);
|
|
138
|
+
const target = path.resolve(canonicalRoot, file);
|
|
139
|
+
if (!inside(canonicalRoot, target) || sensitive(path.relative(canonicalRoot, target))) throw new Error(`Path is outside the permitted workspace or is sensitive: ${file}`);
|
|
140
|
+
let ancestor = target;
|
|
141
|
+
for (;;) try {
|
|
142
|
+
const resolved = await realpath(ancestor);
|
|
143
|
+
if (!inside(canonicalRoot, resolved) || sensitive(path.relative(canonicalRoot, resolved))) throw new Error(`Symlink crosses the workspace boundary: ${file}`);
|
|
144
|
+
break;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code !== "ENOENT") throw error;
|
|
147
|
+
const parent = path.dirname(ancestor);
|
|
148
|
+
if (parent === ancestor) throw error;
|
|
149
|
+
ancestor = parent;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
if ((await lstat(target)).isSymbolicLink()) throw new Error(`Editing or reading a symlink is not supported: ${file}`);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code !== "ENOENT") throw error;
|
|
155
|
+
}
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
async function requireTrust(context) {
|
|
159
|
+
if (!context.trusted) throw new ApprovalRequired("Workspace is not trusted. Review its configuration, then run with --trust.");
|
|
160
|
+
}
|
|
161
|
+
async function authorize(context, kind, summary, payload) {
|
|
162
|
+
if (!await context.approve({
|
|
163
|
+
kind,
|
|
164
|
+
summary,
|
|
165
|
+
fingerprint: hash(JSON.stringify(payload))
|
|
166
|
+
})) throw new ApprovalRequired(`Approval required: ${summary}`);
|
|
167
|
+
}
|
|
168
|
+
function redactValue(value, clean = redactor()) {
|
|
169
|
+
if (typeof value === "string") return clean(value);
|
|
170
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(v, clean));
|
|
171
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactValue(v, clean)]));
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
var Session = class {
|
|
175
|
+
root;
|
|
176
|
+
id;
|
|
177
|
+
directory;
|
|
178
|
+
sequence = 0;
|
|
179
|
+
handle;
|
|
180
|
+
redact = redactor();
|
|
181
|
+
events = [];
|
|
182
|
+
messages = [];
|
|
183
|
+
constructor(root, id = randomUUID(), directory = path.join(stateRoot(), "sessions", hash(root))) {
|
|
184
|
+
this.root = root;
|
|
185
|
+
this.id = id;
|
|
186
|
+
this.directory = directory;
|
|
187
|
+
if (!/^[\w-]{1,100}$/.test(id)) throw new Error("Invalid session ID");
|
|
188
|
+
}
|
|
189
|
+
get filename() {
|
|
190
|
+
return path.join(this.directory, `${this.id}.jsonl`);
|
|
191
|
+
}
|
|
192
|
+
async open(resume = false) {
|
|
193
|
+
await mkdir(this.directory, {
|
|
194
|
+
recursive: true,
|
|
195
|
+
mode: 448
|
|
196
|
+
});
|
|
197
|
+
const lock = `${this.filename}.lock`;
|
|
198
|
+
try {
|
|
199
|
+
this.handle = await open(lock, "wx", 384);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error.code !== "EEXIST") throw error;
|
|
202
|
+
const pid = Number(await readFile(lock, "utf8"));
|
|
203
|
+
if (!Number.isInteger(pid) || pid <= 0) throw new Error("Session lock is invalid; inspect it before removing it.");
|
|
204
|
+
try {
|
|
205
|
+
process.kill(pid, 0);
|
|
206
|
+
throw new Error("Session is already active in another process.");
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (err.code !== "ESRCH") throw err;
|
|
209
|
+
}
|
|
210
|
+
await unlink(lock);
|
|
211
|
+
this.handle = await open(lock, "wx", 384);
|
|
212
|
+
}
|
|
213
|
+
await this.handle.writeFile(String(process.pid));
|
|
214
|
+
try {
|
|
215
|
+
if (resume) {
|
|
216
|
+
const raw = await readFile(this.filename, "utf8");
|
|
217
|
+
const end = raw.lastIndexOf("\n") + 1;
|
|
218
|
+
const complete = raw.slice(0, end);
|
|
219
|
+
if (end !== raw.length) await truncate(this.filename, Buffer.byteLength(complete));
|
|
220
|
+
for (const line of complete.split("\n").filter(Boolean)) {
|
|
221
|
+
const event = JSON.parse(line);
|
|
222
|
+
if (event.version !== 1 || event.sessionId !== this.id || event.sequence !== this.sequence + 1) throw new Error("Invalid session journal; refusing to replay it.");
|
|
223
|
+
this.sequence = event.sequence;
|
|
224
|
+
this.events.push(event);
|
|
225
|
+
if (event.type === "message") this.messages.push(event.data.message);
|
|
226
|
+
}
|
|
227
|
+
} else await (await open(this.filename, "wx", 384)).close();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
await this.close();
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async append(type, data) {
|
|
234
|
+
const safe = redactValue(data, this.redact);
|
|
235
|
+
const event = {
|
|
236
|
+
version: 1,
|
|
237
|
+
sessionId: this.id,
|
|
238
|
+
sequence: ++this.sequence,
|
|
239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
type,
|
|
241
|
+
data: safe
|
|
242
|
+
};
|
|
243
|
+
const fd = await open(this.filename, "a", 384);
|
|
244
|
+
try {
|
|
245
|
+
await fd.writeFile(`${JSON.stringify(event)}\n`);
|
|
246
|
+
await fd.sync();
|
|
247
|
+
} finally {
|
|
248
|
+
await fd.close();
|
|
249
|
+
}
|
|
250
|
+
this.events.push(event);
|
|
251
|
+
if (type === "message") this.messages.push(safe.message);
|
|
252
|
+
return event;
|
|
253
|
+
}
|
|
254
|
+
unresolved() {
|
|
255
|
+
const results = new Set(this.messages.filter((m) => m.role === "tool").map((m) => m.callId));
|
|
256
|
+
return this.messages.flatMap((m) => m.role === "assistant" ? (m.calls ?? []).filter((call) => !results.has(call.id)) : []);
|
|
257
|
+
}
|
|
258
|
+
async close() {
|
|
259
|
+
if (!this.handle) return;
|
|
260
|
+
await this.handle.close();
|
|
261
|
+
this.handle = void 0;
|
|
262
|
+
await unlink(`${this.filename}.lock`).catch(() => {});
|
|
263
|
+
}
|
|
264
|
+
static async list(root) {
|
|
265
|
+
try {
|
|
266
|
+
return (await readdir(path.join(stateRoot(), "sessions", hash(root)))).filter((n) => n.endsWith(".jsonl")).map((n) => n.slice(0, -6));
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
function contextSize(messages) {
|
|
273
|
+
return JSON.stringify(messages, (key, value) => key === "images" && Array.isArray(value) ? value.map(() => "[image]".repeat(256)) : value).length;
|
|
274
|
+
}
|
|
275
|
+
function compactMessages(messages, budget) {
|
|
276
|
+
if (contextSize(messages) <= budget) return {
|
|
277
|
+
messages,
|
|
278
|
+
compacted: false
|
|
279
|
+
};
|
|
280
|
+
const groups = [];
|
|
281
|
+
for (const message of messages) {
|
|
282
|
+
if (message.role !== "tool" || groups.length === 0) groups.push([]);
|
|
283
|
+
groups[groups.length - 1].push(message);
|
|
284
|
+
}
|
|
285
|
+
const retained = [];
|
|
286
|
+
let size = 0;
|
|
287
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
288
|
+
const group = groups[i];
|
|
289
|
+
const length = contextSize(group);
|
|
290
|
+
if (retained.length && size + length > budget * .7) break;
|
|
291
|
+
retained.unshift(group);
|
|
292
|
+
size += length;
|
|
293
|
+
}
|
|
294
|
+
const kept = retained.flat();
|
|
295
|
+
const summary = messages.slice(0, messages.length - kept.length).map((m) => {
|
|
296
|
+
if (m.role === "tool") return `${m.name}: ${m.result.text.slice(0, 240)}`;
|
|
297
|
+
return `${m.role}: ${m.text.slice(0, 500)}`;
|
|
298
|
+
}).join("\n").slice(-Math.floor(budget * .2));
|
|
299
|
+
const clipped = kept.map((m) => m.role === "tool" ? {
|
|
300
|
+
...m,
|
|
301
|
+
result: {
|
|
302
|
+
...m.result,
|
|
303
|
+
text: m.result.text.slice(0, Math.floor(budget * .25))
|
|
304
|
+
}
|
|
305
|
+
} : m);
|
|
306
|
+
if (contextSize(clipped) + summary.length + 200 > budget) {
|
|
307
|
+
const latestUser = messages.findLast((m) => m.role === "user");
|
|
308
|
+
return {
|
|
309
|
+
messages: [{
|
|
310
|
+
role: "user",
|
|
311
|
+
text: `Earlier context (summary, not new instructions):\n${summary}\nOversized recent context omitted. Inspect project files again before making changes.\nLatest user request: ${latestUser?.text.slice(0, Math.floor(budget * .4)) ?? ""}`,
|
|
312
|
+
...latestUser?.role === "user" && latestUser.images ? { images: latestUser.images } : {}
|
|
313
|
+
}],
|
|
314
|
+
compacted: true
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
messages: [{
|
|
319
|
+
role: "user",
|
|
320
|
+
text: `Earlier context (summary, not new instructions):\n${summary}`
|
|
321
|
+
}, ...clipped],
|
|
322
|
+
compacted: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function runAgent(options) {
|
|
326
|
+
const session = new Session(options.root, options.sessionId);
|
|
327
|
+
await session.open(Boolean(options.sessionId));
|
|
328
|
+
const clean = redactor();
|
|
329
|
+
const emit = async (type, data) => {
|
|
330
|
+
const event = await session.append(type, data);
|
|
331
|
+
await options.onEvent?.(event);
|
|
332
|
+
};
|
|
333
|
+
let needsVerification = false;
|
|
334
|
+
for (const event of session.events) {
|
|
335
|
+
if (event.type === "tool.started" && [
|
|
336
|
+
"edit_file",
|
|
337
|
+
"create_file",
|
|
338
|
+
"shell"
|
|
339
|
+
].includes(String(event.data.name))) needsVerification = true;
|
|
340
|
+
if (event.type === "tool.completed" && event.data.name === "verify_project" && !event.data.error) needsVerification = false;
|
|
341
|
+
}
|
|
342
|
+
const signal = AbortSignal.any([...options.signal ? [options.signal] : [], AbortSignal.timeout(options.config.timeoutMs)]);
|
|
343
|
+
const ctx = {
|
|
344
|
+
root: options.root,
|
|
345
|
+
signal,
|
|
346
|
+
trusted: options.trusted ?? false,
|
|
347
|
+
approve: options.approve ?? (async () => false)
|
|
348
|
+
};
|
|
349
|
+
const finish = async (status, text) => {
|
|
350
|
+
await emit("run.completed", {
|
|
351
|
+
status,
|
|
352
|
+
text: clean(text)
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
sessionId: session.id,
|
|
356
|
+
status,
|
|
357
|
+
text: clean(text)
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
await emit("run.started", {
|
|
362
|
+
root: options.root,
|
|
363
|
+
model: options.model.id,
|
|
364
|
+
resumed: Boolean(options.sessionId)
|
|
365
|
+
});
|
|
366
|
+
const unresolved = session.unresolved();
|
|
367
|
+
if (unresolved.length && !options.acknowledgeInterrupted) {
|
|
368
|
+
await emit("recovery.required", { calls: unresolved });
|
|
369
|
+
return await finish("action_required", "Interrupted tool calls require inspection. Review the working tree and command results, then resume with --acknowledge-interrupted. Calls will not be replayed.");
|
|
370
|
+
}
|
|
371
|
+
for (const call of unresolved) await emit("message", { message: {
|
|
372
|
+
role: "tool",
|
|
373
|
+
name: call.name,
|
|
374
|
+
callId: call.id,
|
|
375
|
+
error: true,
|
|
376
|
+
result: { text: "Execution was interrupted. User acknowledged inspection; outcome unknown. Do not repeat this action without first checking current state." }
|
|
377
|
+
} });
|
|
378
|
+
await emit("message", { message: {
|
|
379
|
+
role: "user",
|
|
380
|
+
text: options.prompt,
|
|
381
|
+
images: options.images
|
|
382
|
+
} });
|
|
383
|
+
const registry = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
384
|
+
if (registry.size !== options.tools.length) throw new Error("Duplicate tool names");
|
|
385
|
+
const schemas = options.tools.map((tool) => ({
|
|
386
|
+
name: tool.name,
|
|
387
|
+
description: tool.description,
|
|
388
|
+
schema: z.toJSONSchema(tool.schema)
|
|
389
|
+
}));
|
|
390
|
+
for (let step = 0; step < options.config.maxSteps; step++) {
|
|
391
|
+
signal.throwIfAborted();
|
|
392
|
+
await emit("step.started", { step: step + 1 });
|
|
393
|
+
const compact = compactMessages(session.messages, options.config.contextCharacters);
|
|
394
|
+
if (compact.compacted) await emit("context.compacted", {
|
|
395
|
+
before: session.messages.length,
|
|
396
|
+
after: compact.messages.length
|
|
397
|
+
});
|
|
398
|
+
let text = "";
|
|
399
|
+
let buffer = "";
|
|
400
|
+
const calls = [];
|
|
401
|
+
const flush = async (final = false) => {
|
|
402
|
+
const cut = final ? buffer.length : Math.max(buffer.lastIndexOf("\n"), buffer.lastIndexOf(" ")) + 1;
|
|
403
|
+
if (cut > 0) {
|
|
404
|
+
await emit("text.delta", { text: clean(buffer.slice(0, cut)) });
|
|
405
|
+
buffer = buffer.slice(cut);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
for await (const part of options.model.stream({
|
|
409
|
+
system: `You are Weapp Agent, an expert in WeChat mini-programs, weapp-vite and Wevu. Read project instructions before edits. Understand root causes. Use read_file hashes for edits and preserve user changes. After edits, run verify_project and inspect its actual results. If checks fail, fix the cause and rerun. Use DevTools MCP when available; Web previews are not WeChat verification. Report passed, failed and unverified checks separately. Never claim unexecuted checks passed. Respect tool denials; do not bypass them with another tool. Stop if blocked.\n${options.system ?? ""}`,
|
|
410
|
+
messages: compact.messages,
|
|
411
|
+
tools: schemas,
|
|
412
|
+
signal
|
|
413
|
+
})) {
|
|
414
|
+
signal.throwIfAborted();
|
|
415
|
+
if (part.type === "text") {
|
|
416
|
+
text += part.text;
|
|
417
|
+
buffer += part.text;
|
|
418
|
+
await flush();
|
|
419
|
+
}
|
|
420
|
+
if (part.type === "call") {
|
|
421
|
+
if (calls.some((call) => call.id === part.call.id)) throw new Error("Provider returned a duplicate tool call ID");
|
|
422
|
+
calls.push(part.call);
|
|
423
|
+
}
|
|
424
|
+
if (part.type === "usage") await emit("usage", {
|
|
425
|
+
inputTokens: part.inputTokens,
|
|
426
|
+
outputTokens: part.outputTokens
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
await flush(true);
|
|
430
|
+
await emit("message", { message: {
|
|
431
|
+
role: "assistant",
|
|
432
|
+
text,
|
|
433
|
+
calls
|
|
434
|
+
} });
|
|
435
|
+
if (!calls.length) {
|
|
436
|
+
if (needsVerification && registry.has("verify_project")) {
|
|
437
|
+
await emit("message", { message: {
|
|
438
|
+
role: "user",
|
|
439
|
+
text: "Files changed since the last verification. Call verify_project before finishing; report failed and unverified categories honestly."
|
|
440
|
+
} });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
return await finish("completed", text);
|
|
444
|
+
}
|
|
445
|
+
for (const call of calls) {
|
|
446
|
+
signal.throwIfAborted();
|
|
447
|
+
const tool = registry.get(call.name);
|
|
448
|
+
let result;
|
|
449
|
+
let error = false;
|
|
450
|
+
let approval = false;
|
|
451
|
+
try {
|
|
452
|
+
if (!tool) throw new Error(`Unknown tool: ${call.name}`);
|
|
453
|
+
const args = tool.schema.parse(call.input);
|
|
454
|
+
await emit("tool.started", {
|
|
455
|
+
callId: call.id,
|
|
456
|
+
name: call.name,
|
|
457
|
+
input: args,
|
|
458
|
+
mutates: tool.mutates
|
|
459
|
+
});
|
|
460
|
+
result = await tool.execute(args, ctx);
|
|
461
|
+
if ([
|
|
462
|
+
"edit_file",
|
|
463
|
+
"create_file",
|
|
464
|
+
"shell"
|
|
465
|
+
].includes(call.name)) needsVerification = true;
|
|
466
|
+
if (call.name === "verify_project") needsVerification = false;
|
|
467
|
+
} catch (err) {
|
|
468
|
+
if (signal.aborted) throw err;
|
|
469
|
+
error = true;
|
|
470
|
+
approval = err instanceof ApprovalRequired;
|
|
471
|
+
result = { text: clean(err instanceof Error ? err.message : String(err)) };
|
|
472
|
+
}
|
|
473
|
+
await emit("message", { message: {
|
|
474
|
+
role: "tool",
|
|
475
|
+
callId: call.id,
|
|
476
|
+
name: call.name,
|
|
477
|
+
result,
|
|
478
|
+
error
|
|
479
|
+
} });
|
|
480
|
+
await emit("tool.completed", {
|
|
481
|
+
callId: call.id,
|
|
482
|
+
name: call.name,
|
|
483
|
+
result,
|
|
484
|
+
error
|
|
485
|
+
});
|
|
486
|
+
if (approval) {
|
|
487
|
+
for (const skipped of calls.slice(calls.indexOf(call) + 1)) await emit("message", { message: {
|
|
488
|
+
role: "tool",
|
|
489
|
+
callId: skipped.id,
|
|
490
|
+
name: skipped.name,
|
|
491
|
+
error: true,
|
|
492
|
+
result: { text: "Not executed: an earlier call needs approval." }
|
|
493
|
+
} });
|
|
494
|
+
return await finish("action_required", result.text);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return await finish("limit_reached", `Stopped after ${options.config.maxSteps} model steps. Resume the session to continue.`);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return await finish(signal.aborted ? "cancelled" : "failed", error instanceof Error ? error.message : String(error));
|
|
501
|
+
} finally {
|
|
502
|
+
await session.close();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const limit = 4e4;
|
|
506
|
+
function bounded(text, max = limit) {
|
|
507
|
+
return text.length > max ? `${text.slice(0, max)}\n[truncated: ${text.length - max} more characters]` : text;
|
|
508
|
+
}
|
|
509
|
+
async function walk(root, directory = ".", result = []) {
|
|
510
|
+
if (result.length >= 2e3) return result;
|
|
511
|
+
for (const item of await readdir(await safePath(root, directory), { withFileTypes: true })) {
|
|
512
|
+
const relative = path.join(directory, item.name);
|
|
513
|
+
if (sensitive(relative) || [
|
|
514
|
+
"node_modules",
|
|
515
|
+
"dist",
|
|
516
|
+
"coverage",
|
|
517
|
+
".turbo",
|
|
518
|
+
".astro",
|
|
519
|
+
".wrangler"
|
|
520
|
+
].includes(item.name) || item.isSymbolicLink()) continue;
|
|
521
|
+
if (item.isDirectory()) await walk(root, relative, result);
|
|
522
|
+
else if (item.isFile()) result.push(relative);
|
|
523
|
+
if (result.length >= 2e3) break;
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
function fileTools() {
|
|
528
|
+
const readSchema = z.object({
|
|
529
|
+
path: z.string(),
|
|
530
|
+
startLine: z.number().int().positive().default(1),
|
|
531
|
+
endLine: z.number().int().positive().optional()
|
|
532
|
+
}).strict();
|
|
533
|
+
const listSchema = z.object({ directory: z.string().default(".") }).strict();
|
|
534
|
+
const searchSchema = z.object({
|
|
535
|
+
query: z.string().min(1),
|
|
536
|
+
directory: z.string().default(".")
|
|
537
|
+
}).strict();
|
|
538
|
+
const createSchema = z.object({
|
|
539
|
+
path: z.string(),
|
|
540
|
+
content: z.string().max(5e5)
|
|
541
|
+
}).strict();
|
|
542
|
+
const editSchema = z.object({
|
|
543
|
+
path: z.string(),
|
|
544
|
+
expectedHash: z.string(),
|
|
545
|
+
oldText: z.string().min(1),
|
|
546
|
+
newText: z.string()
|
|
547
|
+
}).strict();
|
|
548
|
+
const shellSchema = z.object({
|
|
549
|
+
command: z.string().min(1),
|
|
550
|
+
timeoutMs: z.number().int().positive().max(6e5).default(12e4)
|
|
551
|
+
}).strict();
|
|
552
|
+
return [
|
|
553
|
+
{
|
|
554
|
+
name: "list_files",
|
|
555
|
+
description: "List up to 2000 project files, excluding generated files, secrets, and symlinks.",
|
|
556
|
+
schema: listSchema,
|
|
557
|
+
mutates: false,
|
|
558
|
+
async execute(input, ctx) {
|
|
559
|
+
const a = listSchema.parse(input);
|
|
560
|
+
return { text: (await walk(ctx.root, a.directory)).join("\n") };
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "read_file",
|
|
565
|
+
description: "Read numbered lines and the SHA-256 of the full file. Use that hash for edit_file.",
|
|
566
|
+
schema: readSchema,
|
|
567
|
+
mutates: false,
|
|
568
|
+
async execute(input, ctx) {
|
|
569
|
+
const a = readSchema.parse(input);
|
|
570
|
+
const text = await readFile(await safePath(ctx.root, a.path), "utf8");
|
|
571
|
+
const lines = text.split("\n");
|
|
572
|
+
return {
|
|
573
|
+
text: bounded(lines.slice(a.startLine - 1, a.endLine ?? a.startLine + 299).map((line, i) => `${i + a.startLine}: ${line}`).join("\n")),
|
|
574
|
+
data: {
|
|
575
|
+
path: a.path,
|
|
576
|
+
hash: hash(text),
|
|
577
|
+
lines: lines.length
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "search_files",
|
|
584
|
+
description: "Literal text search in source files with line numbers. No regex or shell evaluation.",
|
|
585
|
+
schema: searchSchema,
|
|
586
|
+
mutates: false,
|
|
587
|
+
async execute(input, ctx) {
|
|
588
|
+
const a = searchSchema.parse(input);
|
|
589
|
+
const matches = [];
|
|
590
|
+
for (const f of await walk(ctx.root, a.directory)) {
|
|
591
|
+
const text = await readFile(await safePath(ctx.root, f), "utf8").catch(() => "");
|
|
592
|
+
if (text.includes("\0") || text.length > 5e5) continue;
|
|
593
|
+
text.split("\n").forEach((line, i) => {
|
|
594
|
+
if (line.includes(a.query) && matches.length < 200) matches.push(`${f}:${i + 1}: ${line}`);
|
|
595
|
+
});
|
|
596
|
+
if (matches.length >= 200) break;
|
|
597
|
+
}
|
|
598
|
+
return { text: bounded(matches.join("\n")) };
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: "create_file",
|
|
603
|
+
description: "Create a new file without overwriting any existing file.",
|
|
604
|
+
schema: createSchema,
|
|
605
|
+
mutates: true,
|
|
606
|
+
async execute(input, ctx) {
|
|
607
|
+
await requireTrust(ctx);
|
|
608
|
+
const a = createSchema.parse(input);
|
|
609
|
+
const f = await safePath(ctx.root, a.path);
|
|
610
|
+
await mkdir(path.dirname(f), { recursive: true });
|
|
611
|
+
await safePath(ctx.root, a.path);
|
|
612
|
+
await writeFile(f, a.content, { flag: "wx" });
|
|
613
|
+
return {
|
|
614
|
+
text: createTwoFilesPatch("/dev/null", a.path, "", a.content),
|
|
615
|
+
data: {
|
|
616
|
+
hash: hash(a.content),
|
|
617
|
+
path: a.path
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: "edit_file",
|
|
624
|
+
description: "Replace exactly one occurrence of oldText, only if the file still matches expectedHash. Re-read on conflicts.",
|
|
625
|
+
schema: editSchema,
|
|
626
|
+
mutates: true,
|
|
627
|
+
async execute(input, ctx) {
|
|
628
|
+
await requireTrust(ctx);
|
|
629
|
+
const a = editSchema.parse(input);
|
|
630
|
+
const f = await safePath(ctx.root, a.path);
|
|
631
|
+
const before = await readFile(f, "utf8");
|
|
632
|
+
if (hash(before) !== a.expectedHash) throw new Error("File changed since it was read. Re-read and reconcile the user changes.");
|
|
633
|
+
if (before.split(a.oldText).length !== 2) throw new Error("oldText must occur exactly once. Include more context.");
|
|
634
|
+
const after = before.replace(a.oldText, () => a.newText);
|
|
635
|
+
const temporary = `${f}.${randomUUID()}.tmp`;
|
|
636
|
+
try {
|
|
637
|
+
const { stat } = await import("node:fs/promises");
|
|
638
|
+
await writeFile(temporary, after, {
|
|
639
|
+
flag: "wx",
|
|
640
|
+
mode: (await stat(f)).mode
|
|
641
|
+
});
|
|
642
|
+
await safePath(ctx.root, a.path);
|
|
643
|
+
if (hash(await readFile(f, "utf8")) !== a.expectedHash) throw new Error("Concurrent file change detected; edit not applied.");
|
|
644
|
+
await rename(temporary, f);
|
|
645
|
+
} finally {
|
|
646
|
+
await unlink(temporary).catch(() => {});
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
text: createTwoFilesPatch(a.path, a.path, before, after),
|
|
650
|
+
data: {
|
|
651
|
+
path: a.path,
|
|
652
|
+
hash: hash(after)
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: "git_diff",
|
|
659
|
+
description: "Show tracked changes and untracked file names, preserving the current Git state.",
|
|
660
|
+
schema: z.object({}).strict(),
|
|
661
|
+
mutates: false,
|
|
662
|
+
async execute(_input, ctx) {
|
|
663
|
+
const filters = await execa("git", [
|
|
664
|
+
"config",
|
|
665
|
+
"--null",
|
|
666
|
+
"--name-only",
|
|
667
|
+
"--get-regexp",
|
|
668
|
+
"^filter\\..*\\.(clean|smudge|process|required)$"
|
|
669
|
+
], {
|
|
670
|
+
cwd: ctx.root,
|
|
671
|
+
cancelSignal: ctx.signal,
|
|
672
|
+
reject: false
|
|
673
|
+
});
|
|
674
|
+
const gitConfig = ["-c", "core.fsmonitor=false"];
|
|
675
|
+
for (const key of filters.stdout.split("\0").filter(Boolean)) gitConfig.push("-c", `${key}=${key.endsWith(".required") ? "false" : ""}`);
|
|
676
|
+
const diff = await execa("git", [
|
|
677
|
+
...gitConfig,
|
|
678
|
+
"--no-pager",
|
|
679
|
+
"diff",
|
|
680
|
+
"--no-ext-diff",
|
|
681
|
+
"--no-textconv",
|
|
682
|
+
"--",
|
|
683
|
+
".",
|
|
684
|
+
":!.env*",
|
|
685
|
+
":!*.pem",
|
|
686
|
+
":!*.key",
|
|
687
|
+
":!*.p12",
|
|
688
|
+
":!.weapp-agent/**",
|
|
689
|
+
":!.npmrc"
|
|
690
|
+
], {
|
|
691
|
+
cwd: ctx.root,
|
|
692
|
+
cancelSignal: ctx.signal,
|
|
693
|
+
reject: false
|
|
694
|
+
});
|
|
695
|
+
return { text: bounded(`${(await execa("git", [
|
|
696
|
+
...gitConfig,
|
|
697
|
+
"status",
|
|
698
|
+
"--short"
|
|
699
|
+
], {
|
|
700
|
+
cwd: ctx.root,
|
|
701
|
+
cancelSignal: ctx.signal,
|
|
702
|
+
reject: false
|
|
703
|
+
})).stdout}\n${diff.stdout}`) };
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "shell",
|
|
708
|
+
description: "Execute an arbitrary shell command. Always requires exact-command approval; never use it to bypass a denied operation.",
|
|
709
|
+
schema: shellSchema,
|
|
710
|
+
mutates: true,
|
|
711
|
+
async execute(input, ctx) {
|
|
712
|
+
const a = shellSchema.parse(input);
|
|
713
|
+
await authorize(ctx, "command", a.command, {
|
|
714
|
+
root: ctx.root,
|
|
715
|
+
...a
|
|
716
|
+
});
|
|
717
|
+
const result = await execa(a.command, {
|
|
718
|
+
cwd: ctx.root,
|
|
719
|
+
shell: true,
|
|
720
|
+
cancelSignal: ctx.signal,
|
|
721
|
+
timeout: a.timeoutMs,
|
|
722
|
+
forceKillAfterDelay: 2e3,
|
|
723
|
+
reject: false,
|
|
724
|
+
maxBuffer: 2e6
|
|
725
|
+
});
|
|
726
|
+
return {
|
|
727
|
+
text: bounded(`${result.stdout}\n${result.stderr}`),
|
|
728
|
+
data: {
|
|
729
|
+
exitCode: result.exitCode,
|
|
730
|
+
timedOut: result.timedOut
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
];
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region ../../packages/mini-program/dist/index.mjs
|
|
739
|
+
async function json(file) {
|
|
740
|
+
try {
|
|
741
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
742
|
+
} catch {
|
|
743
|
+
return {};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function findProjectRoot(cwd) {
|
|
747
|
+
const start = await realpath(cwd);
|
|
748
|
+
let dir = start;
|
|
749
|
+
for (;;) {
|
|
750
|
+
const pkg = await json(path.join(dir, "package.json"));
|
|
751
|
+
if (await exists(path.join(dir, "weapp-agent.config.json")) || pkg.dependencies?.["weapp-vite"] || pkg.devDependencies?.["weapp-vite"] || await exists(path.join(dir, "project.config.json"))) return dir;
|
|
752
|
+
const parent = path.dirname(dir);
|
|
753
|
+
if (parent === dir) return start;
|
|
754
|
+
dir = parent;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function exists(file) {
|
|
758
|
+
try {
|
|
759
|
+
await access(file);
|
|
760
|
+
return true;
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function detectProject(root) {
|
|
766
|
+
root = await realpath(root);
|
|
767
|
+
const pkg = await json(path.join(root, "package.json"));
|
|
768
|
+
const config = await json(path.join(root, "project.config.json"));
|
|
769
|
+
const dependencies = {
|
|
770
|
+
...pkg.dependencies,
|
|
771
|
+
...pkg.devDependencies
|
|
772
|
+
};
|
|
773
|
+
let vite = "";
|
|
774
|
+
for (const file of [
|
|
775
|
+
"vite.config.ts",
|
|
776
|
+
"vite.config.js",
|
|
777
|
+
"vite.config.mts"
|
|
778
|
+
]) if (await exists(path.join(root, file))) vite = await readFile(path.join(root, file), "utf8");
|
|
779
|
+
const sourceRoot = vite.match(/\bsrcRoot\s*:\s*['"]([^'"]+)['"]/)?.[1] ?? (await exists(path.join(root, "src")) ? "src" : ".");
|
|
780
|
+
const outputRoot = vite.match(/\boutDir\s*:\s*['"]([^'"]+)['"]/)?.[1] ?? config.miniprogramRoot ?? (dependencies["weapp-vite"] ? "dist" : ".");
|
|
781
|
+
await safePath(root, sourceRoot);
|
|
782
|
+
await safePath(root, outputRoot);
|
|
783
|
+
const app = await json(path.join(root, sourceRoot, "app.json"));
|
|
784
|
+
const sources = [];
|
|
785
|
+
const scan = async (directory, depth = 0) => {
|
|
786
|
+
if (depth > 8 || sources.length > 2e3) return;
|
|
787
|
+
const entries = await readdir(await safePath(root, directory), { withFileTypes: true }).catch(() => []);
|
|
788
|
+
for (const entry of entries) {
|
|
789
|
+
if (entry.isSymbolicLink() || entry.name.startsWith(".") || ["node_modules", "dist"].includes(entry.name)) continue;
|
|
790
|
+
const relative = path.join(directory, entry.name);
|
|
791
|
+
if (entry.isDirectory()) await scan(relative, depth + 1);
|
|
792
|
+
else if (/\.(?:vue|wxml)$/.test(entry.name)) sources.push(path.relative(sourceRoot, relative).split(path.sep).join("/"));
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
await scan(sourceRoot);
|
|
796
|
+
const hasVue = sources.some((file) => file.endsWith(".vue"));
|
|
797
|
+
const hasNative = sources.some((file) => file.endsWith(".wxml"));
|
|
798
|
+
const warnings = [];
|
|
799
|
+
if (vite && !/\bsrcRoot\s*:\s*['"]/.test(vite) && !await exists(path.join(root, sourceRoot, "app.json"))) warnings.push("Dynamic Vite configuration: source/output paths are inferred. Check project_info before editing.");
|
|
800
|
+
return {
|
|
801
|
+
root,
|
|
802
|
+
kind: hasVue && Boolean(dependencies.wevu) ? "wevu" : hasNative || dependencies["weapp-vite"] || config.appid || app.pages ? "native" : "unknown",
|
|
803
|
+
packageManager: String(pkg.packageManager ?? "").startsWith("yarn") ? "yarn" : String(pkg.packageManager ?? "").startsWith("bun") ? "bun" : await exists(path.join(root, "pnpm-lock.yaml")) || await exists(path.join(root, "pnpm-workspace.yaml")) || String(pkg.packageManager ?? "").startsWith("pnpm") ? "pnpm" : "npm",
|
|
804
|
+
sourceRoot,
|
|
805
|
+
outputRoot,
|
|
806
|
+
pages: Array.isArray(app.pages) ? app.pages : sources.filter((file) => file.startsWith("pages/")).map((file) => file.replace(/\.(vue|wxml)$/, "")),
|
|
807
|
+
subPackages: (app.subPackages ?? app.subpackages ?? []).map((p) => p.root),
|
|
808
|
+
scripts: pkg.scripts ?? {},
|
|
809
|
+
weappViteVersion: dependencies["weapp-vite"],
|
|
810
|
+
warnings
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function defaultVerification(project) {
|
|
814
|
+
return [
|
|
815
|
+
"typecheck",
|
|
816
|
+
"build",
|
|
817
|
+
"test"
|
|
818
|
+
].filter((kind) => project.scripts[kind]).map((kind) => ({
|
|
819
|
+
kind,
|
|
820
|
+
command: project.packageManager,
|
|
821
|
+
args: ["run", kind],
|
|
822
|
+
timeoutMs: 12e4
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
async function projectInstructions(project) {
|
|
826
|
+
const snippets = [];
|
|
827
|
+
const add = async (file) => {
|
|
828
|
+
try {
|
|
829
|
+
const target = await safePath(project.root, file);
|
|
830
|
+
snippets.push(`Instructions scoped to ${path.dirname(file)} (${file}):\n${bounded(await readFile(target, "utf8"), 12e3)}`);
|
|
831
|
+
} catch {}
|
|
832
|
+
};
|
|
833
|
+
await add("AGENTS.md");
|
|
834
|
+
await add("AGENTS.local.md");
|
|
835
|
+
const visit = async (directory, depth) => {
|
|
836
|
+
if (depth > 6 || snippets.join("").length > 32e3) return;
|
|
837
|
+
const entries = await readdir(await safePath(project.root, directory), { withFileTypes: true }).catch(() => []);
|
|
838
|
+
if (directory !== ".") {
|
|
839
|
+
if (entries.some((e) => e.name === "AGENTS.md")) await add(path.join(directory, "AGENTS.md"));
|
|
840
|
+
if (entries.some((e) => e.name === "AGENTS.local.md")) await add(path.join(directory, "AGENTS.local.md"));
|
|
841
|
+
}
|
|
842
|
+
for (const e of entries) if (e.isDirectory() && !e.isSymbolicLink() && !e.name.startsWith(".") && ![
|
|
843
|
+
"node_modules",
|
|
844
|
+
"dist",
|
|
845
|
+
"coverage"
|
|
846
|
+
].includes(e.name)) await visit(path.join(directory, e.name), depth + 1);
|
|
847
|
+
};
|
|
848
|
+
await visit(".", 0);
|
|
849
|
+
const docs = path.join(project.root, "node_modules/weapp-vite/dist/docs");
|
|
850
|
+
const names = await readdir(docs).catch(() => []);
|
|
851
|
+
for (const name of names.filter((n) => /^(?:README|ai|cli|getting-started|configuration).*\.md$/i.test(n)).slice(0, 4)) snippets.push(`Installed weapp-vite documentation (${name}):\n${bounded(await readFile(path.join(docs, name), "utf8"), 6e3)}`);
|
|
852
|
+
return `Project facts:\n${JSON.stringify(project)}\n${snippets.join("\n\n")}`;
|
|
853
|
+
}
|
|
854
|
+
const projectRuntime = /* @__PURE__ */ new Set([
|
|
855
|
+
"weapp_devtools_connect",
|
|
856
|
+
"weapp_devtools_active_page",
|
|
857
|
+
"weapp_devtools_page_stack",
|
|
858
|
+
"weapp_devtools_route",
|
|
859
|
+
"weapp_devtools_capture",
|
|
860
|
+
"weapp_devtools_console",
|
|
861
|
+
"weapp_runtime_find_node",
|
|
862
|
+
"weapp_runtime_find_nodes",
|
|
863
|
+
"weapp_runtime_wait_node",
|
|
864
|
+
"weapp_runtime_find_node_by_xpath",
|
|
865
|
+
"weapp_runtime_find_nodes_by_xpath",
|
|
866
|
+
"weapp_runtime_page_state",
|
|
867
|
+
"weapp_runtime_tap_node",
|
|
868
|
+
"weapp_runtime_input_node",
|
|
869
|
+
"weapp_runtime_component_state",
|
|
870
|
+
"weapp_runtime_find_child",
|
|
871
|
+
"weapp_runtime_find_children",
|
|
872
|
+
"weapp_runtime_node_markup",
|
|
873
|
+
"weapp_runtime_node_styles",
|
|
874
|
+
"weapp_runtime_node_attrs",
|
|
875
|
+
"weapp_runtime_measure_node"
|
|
876
|
+
]);
|
|
877
|
+
async function connectMcp(server, context, options) {
|
|
878
|
+
const unchanged = options && await projectFingerprint(context.root, options.config) === options.fingerprint;
|
|
879
|
+
if (!context.trusted || !unchanged) await authorize(context, "mcp", `Connect MCP ${server.name}`, server);
|
|
880
|
+
const client = new Client({
|
|
881
|
+
name: "weapp-agent",
|
|
882
|
+
version: "0.1.0-preview.1"
|
|
883
|
+
}, { capabilities: {} });
|
|
884
|
+
let transport;
|
|
885
|
+
if (server.transport === "stdio") transport = new StdioClientTransport({
|
|
886
|
+
command: server.command,
|
|
887
|
+
args: server.args,
|
|
888
|
+
cwd: context.root,
|
|
889
|
+
stderr: "pipe"
|
|
890
|
+
});
|
|
891
|
+
else {
|
|
892
|
+
const url = new URL(server.url);
|
|
893
|
+
if (url.protocol !== "https:" && ![
|
|
894
|
+
"localhost",
|
|
895
|
+
"127.0.0.1",
|
|
896
|
+
"[::1]"
|
|
897
|
+
].includes(url.hostname)) throw new Error("Remote MCP requires HTTPS");
|
|
898
|
+
const token = server.tokenEnv ? process.env[server.tokenEnv] : void 0;
|
|
899
|
+
if (server.tokenEnv && !token) throw new Error(`Set ${server.tokenEnv} for MCP authentication`);
|
|
900
|
+
transport = new StreamableHTTPClientTransport(url, { requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : {} });
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
await client.connect(transport, { timeout: 15e3 });
|
|
904
|
+
const tools = [];
|
|
905
|
+
let cursor;
|
|
906
|
+
do {
|
|
907
|
+
const page = await client.listTools(cursor ? { cursor } : {}, {
|
|
908
|
+
signal: context.signal,
|
|
909
|
+
timeout: 15e3
|
|
910
|
+
});
|
|
911
|
+
for (const definition of page.tools) {
|
|
912
|
+
const schema = z.fromJSONSchema(definition.inputSchema);
|
|
913
|
+
const name = `${server.name}__${definition.name}`;
|
|
914
|
+
if (name.length > 64) throw new Error(`MCP tool name too long: ${name}`);
|
|
915
|
+
tools.push({
|
|
916
|
+
name,
|
|
917
|
+
description: definition.description ?? definition.name,
|
|
918
|
+
schema,
|
|
919
|
+
mutates: true,
|
|
920
|
+
async execute(input, ctx) {
|
|
921
|
+
const args = schema.parse(input);
|
|
922
|
+
const trustedRuntime = options?.builtin && ctx.trusted && projectRuntime.has(definition.name) && args.port === void 0 && args.sessionId === void 0 && args.preferOpenedSession !== true && await projectFingerprint(ctx.root, options.config) === options.fingerprint;
|
|
923
|
+
if (!trustedRuntime) await authorize(ctx, /upload|publish|deploy/.test(definition.name) ? "publish" : "mcp", `${name}: ${JSON.stringify(args)}`, {
|
|
924
|
+
server,
|
|
925
|
+
tool: definition.name,
|
|
926
|
+
args
|
|
927
|
+
});
|
|
928
|
+
if (trustedRuntime) {
|
|
929
|
+
for (const key of ["projectPath", "outputPath"]) if (typeof args[key] === "string") await safePath(ctx.root, args[key]);
|
|
930
|
+
}
|
|
931
|
+
const response = await client.callTool({
|
|
932
|
+
name: definition.name,
|
|
933
|
+
arguments: args
|
|
934
|
+
}, void 0, {
|
|
935
|
+
signal: ctx.signal,
|
|
936
|
+
timeout: 12e4
|
|
937
|
+
});
|
|
938
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
939
|
+
const text = bounded(content.filter((c) => c.type === "text").map((c) => c.text).join("\n"));
|
|
940
|
+
if (response.isError) throw new Error(text || `MCP tool failed: ${name}`);
|
|
941
|
+
return {
|
|
942
|
+
text,
|
|
943
|
+
images: content.filter((c) => c.type === "image" && c.data && c.mimeType).map((c) => ({
|
|
944
|
+
type: "image",
|
|
945
|
+
data: c.data,
|
|
946
|
+
mediaType: c.mimeType
|
|
947
|
+
})),
|
|
948
|
+
data: response.structuredContent
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
cursor = page.nextCursor;
|
|
954
|
+
} while (cursor);
|
|
955
|
+
return {
|
|
956
|
+
tools,
|
|
957
|
+
close: () => client.close()
|
|
958
|
+
};
|
|
959
|
+
} catch (error) {
|
|
960
|
+
await client.close().catch(() => {});
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function verifyProject(config, context, originalFingerprint) {
|
|
965
|
+
await requireTrust(context);
|
|
966
|
+
const checks = [];
|
|
967
|
+
for (const command of config.verification) {
|
|
968
|
+
context.signal.throwIfAborted();
|
|
969
|
+
const current = await projectFingerprint(context.root, config);
|
|
970
|
+
if (current !== originalFingerprint || /publish|upload|deploy/i.test([command.command, ...command.args].join(" "))) await authorize(context, "command", `Verify: ${command.command} ${command.args.join(" ")}`, {
|
|
971
|
+
command,
|
|
972
|
+
fingerprint: current
|
|
973
|
+
});
|
|
974
|
+
const result = await execa(command.command, command.args, {
|
|
975
|
+
cwd: context.root,
|
|
976
|
+
cancelSignal: context.signal,
|
|
977
|
+
timeout: command.timeoutMs,
|
|
978
|
+
forceKillAfterDelay: 2e3,
|
|
979
|
+
reject: false,
|
|
980
|
+
maxBuffer: 2e6
|
|
981
|
+
});
|
|
982
|
+
checks.push({
|
|
983
|
+
kind: command.kind,
|
|
984
|
+
status: result.exitCode === 0 ? "passed" : "failed",
|
|
985
|
+
command: `${command.command} ${command.args.join(" ")}`,
|
|
986
|
+
exitCode: result.exitCode,
|
|
987
|
+
output: bounded(`${result.stdout}\n${result.stderr}`)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
for (const kind of [
|
|
991
|
+
"typecheck",
|
|
992
|
+
"build",
|
|
993
|
+
"test",
|
|
994
|
+
"devtools"
|
|
995
|
+
]) if (!checks.some((c) => c.kind === kind)) checks.push({
|
|
996
|
+
kind,
|
|
997
|
+
status: "unverified",
|
|
998
|
+
output: kind === "devtools" ? "No real DevTools verification command configured. Web preview is not WeChat runtime evidence." : "No verification command configured."
|
|
999
|
+
});
|
|
1000
|
+
return {
|
|
1001
|
+
passed: checks.some((c) => c.status === "passed") && !checks.some((c) => c.status === "failed"),
|
|
1002
|
+
checks
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function verificationTool(config, fingerprint) {
|
|
1006
|
+
return {
|
|
1007
|
+
name: "verify_project",
|
|
1008
|
+
description: "Run configured typecheck/build/test/DevTools commands and return separate passed, failed and unverified results. Fix failures, then rerun. Missing checks are never considered passed.",
|
|
1009
|
+
schema: z.object({}).strict(),
|
|
1010
|
+
mutates: true,
|
|
1011
|
+
async execute(_input, context) {
|
|
1012
|
+
const report = await verifyProject(config, context, fingerprint);
|
|
1013
|
+
return {
|
|
1014
|
+
text: JSON.stringify(report, null, 2),
|
|
1015
|
+
data: report
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
var WeappProjectAdapter = class {
|
|
1021
|
+
detect = detectProject;
|
|
1022
|
+
instructions = projectInstructions;
|
|
1023
|
+
async tools(project) {
|
|
1024
|
+
return [{
|
|
1025
|
+
name: "project_info",
|
|
1026
|
+
description: "Inspect the current mini-program project structure, scripts and framework. Re-check after changing project configuration.",
|
|
1027
|
+
schema: z.object({}).strict(),
|
|
1028
|
+
mutates: false,
|
|
1029
|
+
async execute() {
|
|
1030
|
+
return { text: JSON.stringify(await detectProject(project.root), null, 2) };
|
|
1031
|
+
}
|
|
1032
|
+
}];
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
async function builtinMcp(root) {
|
|
1036
|
+
const bin = path.join(root, "node_modules/weapp-vite/bin/weapp-vite.js");
|
|
1037
|
+
if (!await exists(bin)) return;
|
|
1038
|
+
return {
|
|
1039
|
+
name: "weapp",
|
|
1040
|
+
transport: "stdio",
|
|
1041
|
+
command: process.execPath,
|
|
1042
|
+
args: [
|
|
1043
|
+
bin,
|
|
1044
|
+
"mcp",
|
|
1045
|
+
"--workspace-root",
|
|
1046
|
+
root
|
|
1047
|
+
]
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region ../../packages/providers/dist/index.mjs
|
|
1052
|
+
function toModelMessages(messages) {
|
|
1053
|
+
return messages.map((message) => {
|
|
1054
|
+
if (message.role === "user") return {
|
|
1055
|
+
role: "user",
|
|
1056
|
+
content: [{
|
|
1057
|
+
type: "text",
|
|
1058
|
+
text: message.text
|
|
1059
|
+
}, ...(message.images ?? []).map((image) => ({
|
|
1060
|
+
type: "image",
|
|
1061
|
+
image: image.data,
|
|
1062
|
+
mediaType: image.mediaType
|
|
1063
|
+
}))]
|
|
1064
|
+
};
|
|
1065
|
+
if (message.role === "assistant") return {
|
|
1066
|
+
role: "assistant",
|
|
1067
|
+
content: [...message.text ? [{
|
|
1068
|
+
type: "text",
|
|
1069
|
+
text: message.text
|
|
1070
|
+
}] : [], ...(message.calls ?? []).map((call) => ({
|
|
1071
|
+
type: "tool-call",
|
|
1072
|
+
toolCallId: call.id,
|
|
1073
|
+
toolName: call.name,
|
|
1074
|
+
input: call.input
|
|
1075
|
+
}))]
|
|
1076
|
+
};
|
|
1077
|
+
const text = message.result.data === void 0 ? message.result.text : `${message.result.text}\n${JSON.stringify(message.result.data)}`;
|
|
1078
|
+
return {
|
|
1079
|
+
role: "tool",
|
|
1080
|
+
content: [{
|
|
1081
|
+
type: "tool-result",
|
|
1082
|
+
toolCallId: message.callId,
|
|
1083
|
+
toolName: message.name,
|
|
1084
|
+
output: message.result.images?.length ? {
|
|
1085
|
+
type: "content",
|
|
1086
|
+
value: [{
|
|
1087
|
+
type: "text",
|
|
1088
|
+
text
|
|
1089
|
+
}, ...message.result.images.map((image) => ({
|
|
1090
|
+
type: "file",
|
|
1091
|
+
data: {
|
|
1092
|
+
type: "data",
|
|
1093
|
+
data: image.data
|
|
1094
|
+
},
|
|
1095
|
+
mediaType: image.mediaType
|
|
1096
|
+
}))]
|
|
1097
|
+
} : {
|
|
1098
|
+
type: message.error ? "error-text" : "text",
|
|
1099
|
+
value: text
|
|
1100
|
+
}
|
|
1101
|
+
}]
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
var AiSdkAdapter = class {
|
|
1106
|
+
id;
|
|
1107
|
+
model;
|
|
1108
|
+
constructor(id, model) {
|
|
1109
|
+
this.id = id;
|
|
1110
|
+
this.model = model;
|
|
1111
|
+
}
|
|
1112
|
+
async *stream(request) {
|
|
1113
|
+
const tools = Object.fromEntries(request.tools.map((t) => [t.name, tool({
|
|
1114
|
+
description: t.description,
|
|
1115
|
+
inputSchema: jsonSchema(t.schema)
|
|
1116
|
+
})]));
|
|
1117
|
+
const result = streamText({
|
|
1118
|
+
model: this.model,
|
|
1119
|
+
system: request.system,
|
|
1120
|
+
messages: toModelMessages(request.messages),
|
|
1121
|
+
tools,
|
|
1122
|
+
abortSignal: request.signal,
|
|
1123
|
+
maxRetries: 2,
|
|
1124
|
+
onError: () => {},
|
|
1125
|
+
maxOutputTokens: 8192
|
|
1126
|
+
});
|
|
1127
|
+
for await (const part of result.fullStream) {
|
|
1128
|
+
if (part.type === "text-delta") yield {
|
|
1129
|
+
type: "text",
|
|
1130
|
+
text: part.text
|
|
1131
|
+
};
|
|
1132
|
+
if (part.type === "tool-call") yield {
|
|
1133
|
+
type: "call",
|
|
1134
|
+
call: {
|
|
1135
|
+
id: part.toolCallId,
|
|
1136
|
+
name: part.toolName,
|
|
1137
|
+
input: part.input
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
if (part.type === "finish") yield {
|
|
1141
|
+
type: "usage",
|
|
1142
|
+
inputTokens: part.totalUsage.inputTokens ?? 0,
|
|
1143
|
+
outputTokens: part.totalUsage.outputTokens ?? 0
|
|
1144
|
+
};
|
|
1145
|
+
if (part.type === "error") throw part.error;
|
|
1146
|
+
if (part.type === "abort") throw new Error("Model request cancelled");
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function apiKeyVariable(config) {
|
|
1151
|
+
return config.apiKeyEnv ?? (config.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
|
|
1152
|
+
}
|
|
1153
|
+
function createModel(config, environment = process.env) {
|
|
1154
|
+
const variable = apiKeyVariable(config);
|
|
1155
|
+
const apiKey = environment[variable];
|
|
1156
|
+
if (!apiKey) throw new Error(`Set ${variable} in your environment. Credentials are never read from other agents or stored in project config.`);
|
|
1157
|
+
let model;
|
|
1158
|
+
if (config.provider === "anthropic") model = createAnthropic({
|
|
1159
|
+
apiKey,
|
|
1160
|
+
baseURL: config.baseURL
|
|
1161
|
+
})(config.name);
|
|
1162
|
+
else if (config.provider === "openai-compatible") {
|
|
1163
|
+
if (!config.baseURL) throw new Error("openai-compatible requires model.baseURL");
|
|
1164
|
+
model = createOpenAICompatible({
|
|
1165
|
+
name: "weapp-compatible",
|
|
1166
|
+
apiKey,
|
|
1167
|
+
baseURL: config.baseURL
|
|
1168
|
+
})(config.name);
|
|
1169
|
+
} else model = createOpenAI({
|
|
1170
|
+
apiKey,
|
|
1171
|
+
baseURL: config.baseURL
|
|
1172
|
+
}).responses(config.name);
|
|
1173
|
+
return new AiSdkAdapter(`${config.provider}/${config.name}`, model);
|
|
1174
|
+
}
|
|
1175
|
+
//#endregion
|
|
1176
|
+
//#region src/ui.tsx
|
|
1177
|
+
function eventText(event) {
|
|
1178
|
+
if (event.type === "text.delta") return String(event.data.text);
|
|
1179
|
+
if (event.type === "tool.started") return `\n↳ ${event.data.name}\n`;
|
|
1180
|
+
if (event.type === "tool.completed") {
|
|
1181
|
+
const result = event.data.result;
|
|
1182
|
+
const edit = ["edit_file", "create_file"].includes(String(event.data.name));
|
|
1183
|
+
return `${event.data.error ? "✗" : "✓"} ${event.data.name}${edit || event.data.error ? `\n${result.text?.slice(0, 5e3)}` : ""}\n`;
|
|
1184
|
+
}
|
|
1185
|
+
if (event.type === "context.compacted") return "\n· Earlier context summarized\n";
|
|
1186
|
+
if (event.type === "run.completed") return `\n[${event.data.status}]\n`;
|
|
1187
|
+
return "";
|
|
1188
|
+
}
|
|
1189
|
+
function App({ runner, initialSession }) {
|
|
1190
|
+
const { exit } = useApp();
|
|
1191
|
+
const [input, setInput] = useState("");
|
|
1192
|
+
const [transcript, setTranscript] = useState("Describe a mini-program change. /exit to quit. Esc cancels a running task.\n");
|
|
1193
|
+
const [busy, setBusy] = useState(false);
|
|
1194
|
+
const [session, setSession] = useState(initialSession);
|
|
1195
|
+
const [pending, setPending] = useState();
|
|
1196
|
+
const answer = useRef(() => {});
|
|
1197
|
+
const controller = useRef(void 0);
|
|
1198
|
+
const append = (text) => setTranscript((previous) => (previous + text).slice(-3e4));
|
|
1199
|
+
useInput((value, key) => {
|
|
1200
|
+
if (key.ctrl && value === "c") {
|
|
1201
|
+
if (busy) controller.current?.abort();
|
|
1202
|
+
else exit();
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (key.escape && busy) controller.current?.abort();
|
|
1206
|
+
if (pending && ["y", "n"].includes(value.toLowerCase())) {
|
|
1207
|
+
answer.current(value.toLowerCase() === "y");
|
|
1208
|
+
setPending(void 0);
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
const submit = async (prompt) => {
|
|
1212
|
+
if (!prompt.trim() || busy) return;
|
|
1213
|
+
if (prompt.trim() === "/exit") {
|
|
1214
|
+
exit();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
setInput("");
|
|
1218
|
+
setBusy(true);
|
|
1219
|
+
append(`\nYou › ${prompt}\n\n`);
|
|
1220
|
+
const abort = new AbortController();
|
|
1221
|
+
controller.current = abort;
|
|
1222
|
+
const approve = (request) => new Promise((resolve) => {
|
|
1223
|
+
setPending(request);
|
|
1224
|
+
const stop = () => {
|
|
1225
|
+
setPending(void 0);
|
|
1226
|
+
resolve(false);
|
|
1227
|
+
};
|
|
1228
|
+
abort.signal.addEventListener("abort", stop, { once: true });
|
|
1229
|
+
answer.current = (value) => {
|
|
1230
|
+
abort.signal.removeEventListener("abort", stop);
|
|
1231
|
+
resolve(value);
|
|
1232
|
+
};
|
|
1233
|
+
});
|
|
1234
|
+
try {
|
|
1235
|
+
const result = await runner(prompt, session, abort.signal, (event) => {
|
|
1236
|
+
append(eventText(event));
|
|
1237
|
+
if (event.type === "run.started") setSession(event.sessionId);
|
|
1238
|
+
}, approve);
|
|
1239
|
+
setSession(result.sessionId);
|
|
1240
|
+
if (result.status !== "completed") append(`${result.text}\n`);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
append(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1243
|
+
} finally {
|
|
1244
|
+
setBusy(false);
|
|
1245
|
+
setPending(void 0);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1249
|
+
flexDirection: "column",
|
|
1250
|
+
paddingX: 1,
|
|
1251
|
+
children: [
|
|
1252
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
1253
|
+
gap: 2,
|
|
1254
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
1255
|
+
bold: true,
|
|
1256
|
+
color: "cyan",
|
|
1257
|
+
children: "WEAPP AGENT"
|
|
1258
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
1259
|
+
dimColor: true,
|
|
1260
|
+
children: session ? `session ${session}` : "mini-program development"
|
|
1261
|
+
})]
|
|
1262
|
+
}),
|
|
1263
|
+
/* @__PURE__ */ jsx(Box, {
|
|
1264
|
+
borderStyle: "single",
|
|
1265
|
+
borderColor: "gray",
|
|
1266
|
+
paddingX: 1,
|
|
1267
|
+
flexDirection: "column",
|
|
1268
|
+
children: /* @__PURE__ */ jsx(Text, { children: transcript })
|
|
1269
|
+
}),
|
|
1270
|
+
pending ? /* @__PURE__ */ jsxs(Box, {
|
|
1271
|
+
flexDirection: "column",
|
|
1272
|
+
children: [
|
|
1273
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1274
|
+
color: "yellow",
|
|
1275
|
+
children: ["Approval ·", pending.kind]
|
|
1276
|
+
}),
|
|
1277
|
+
/* @__PURE__ */ jsx(Text, { children: pending.summary }),
|
|
1278
|
+
/* @__PURE__ */ jsx(Text, { children: "Allow this exact operation? [y/n]" })
|
|
1279
|
+
]
|
|
1280
|
+
}) : busy ? /* @__PURE__ */ jsx(Text, {
|
|
1281
|
+
color: "cyan",
|
|
1282
|
+
children: "Working… Esc to cancel"
|
|
1283
|
+
}) : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
1284
|
+
color: "cyan",
|
|
1285
|
+
children: "› "
|
|
1286
|
+
}), /* @__PURE__ */ jsx(TextInput, {
|
|
1287
|
+
value: input,
|
|
1288
|
+
onChange: setInput,
|
|
1289
|
+
onSubmit: submit
|
|
1290
|
+
})] })
|
|
1291
|
+
]
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async function interactive(runner, initialSession) {
|
|
1295
|
+
await render(/* @__PURE__ */ jsx(App, {
|
|
1296
|
+
runner,
|
|
1297
|
+
initialSession
|
|
1298
|
+
}), { exitOnCtrlC: false }).waitUntilExit();
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/index.ts
|
|
1302
|
+
const clean = redactor();
|
|
1303
|
+
const program = new Command().name("weapp-agent").description("AI coding agent for WeChat mini-programs").version("0.1.0-preview.1").option("-C, --cwd <directory>", "project directory", process.cwd()).option("--trust", "trust this exact project configuration and scripts").option("--json", "emit machine-readable JSON; disable interactive approvals");
|
|
1304
|
+
async function terminalApproval(summary) {
|
|
1305
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1306
|
+
const reader = createInterface({
|
|
1307
|
+
input: process.stdin,
|
|
1308
|
+
output: process.stderr
|
|
1309
|
+
});
|
|
1310
|
+
try {
|
|
1311
|
+
return /^y(?:es)?$/i.test((await reader.question(`${summary}\nAllow? [y/N] `)).trim());
|
|
1312
|
+
} finally {
|
|
1313
|
+
reader.close();
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const approve = (request) => terminalApproval(clean(request.summary));
|
|
1317
|
+
function output(value, json) {
|
|
1318
|
+
process.stdout.write(`${clean(json ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value, null, 2))}\n`);
|
|
1319
|
+
}
|
|
1320
|
+
async function configAndTrust(root, options, approver) {
|
|
1321
|
+
const config = await loadConfig(root);
|
|
1322
|
+
const fingerprint = await projectFingerprint(root, config);
|
|
1323
|
+
let trusted = await isTrusted(root, fingerprint);
|
|
1324
|
+
if (options.trust) {
|
|
1325
|
+
await trustProject(root, fingerprint);
|
|
1326
|
+
trusted = true;
|
|
1327
|
+
} else if (!trusted && !options.json) {
|
|
1328
|
+
if (await approver({
|
|
1329
|
+
kind: "trust",
|
|
1330
|
+
summary: `Trust ${root}?\nThis permits local edits, configured scripts and MCP server startup.\n${JSON.stringify({
|
|
1331
|
+
verification: config.verification,
|
|
1332
|
+
mcp: config.mcp
|
|
1333
|
+
}, null, 2)}`,
|
|
1334
|
+
fingerprint
|
|
1335
|
+
})) {
|
|
1336
|
+
await trustProject(root, fingerprint);
|
|
1337
|
+
trusted = true;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
config,
|
|
1342
|
+
fingerprint,
|
|
1343
|
+
trusted
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
async function images(files) {
|
|
1347
|
+
return Promise.all(files.map(async (file) => {
|
|
1348
|
+
const data = await readFile(path.resolve(file));
|
|
1349
|
+
if (data.length > 10485760) throw new Error(`Image exceeds 10 MB: ${file}`);
|
|
1350
|
+
const type = data.subarray(0, 8).equals(Buffer.from([
|
|
1351
|
+
137,
|
|
1352
|
+
80,
|
|
1353
|
+
78,
|
|
1354
|
+
71,
|
|
1355
|
+
13,
|
|
1356
|
+
10,
|
|
1357
|
+
26,
|
|
1358
|
+
10
|
|
1359
|
+
])) ? "image/png" : data[0] === 255 && data[1] === 216 ? "image/jpeg" : data.subarray(8, 12).toString() === "WEBP" ? "image/webp" : void 0;
|
|
1360
|
+
if (!type) throw new Error(`Use a PNG, JPEG or WebP image: ${file}`);
|
|
1361
|
+
return {
|
|
1362
|
+
type: "image",
|
|
1363
|
+
data: data.toString("base64"),
|
|
1364
|
+
mediaType: type
|
|
1365
|
+
};
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
async function execute(prompt, options, sessionId, suppliedSignal, onEvent, approver = approve) {
|
|
1369
|
+
const root = await findProjectRoot(options.cwd);
|
|
1370
|
+
const selectedApprover = options.json ? async () => false : approver;
|
|
1371
|
+
const { config, fingerprint, trusted } = await configAndTrust(root, options, selectedApprover);
|
|
1372
|
+
const model = createModel(config.model);
|
|
1373
|
+
const adapter = new WeappProjectAdapter();
|
|
1374
|
+
const project = await adapter.detect(root);
|
|
1375
|
+
const abort = new AbortController();
|
|
1376
|
+
const stop = () => abort.abort();
|
|
1377
|
+
if (!suppliedSignal) process.once("SIGINT", stop);
|
|
1378
|
+
const signal = suppliedSignal ?? abort.signal;
|
|
1379
|
+
const context = {
|
|
1380
|
+
root,
|
|
1381
|
+
trusted,
|
|
1382
|
+
approve: selectedApprover,
|
|
1383
|
+
signal
|
|
1384
|
+
};
|
|
1385
|
+
const connections = [];
|
|
1386
|
+
const warnings = [];
|
|
1387
|
+
try {
|
|
1388
|
+
const tools = [
|
|
1389
|
+
...fileTools(),
|
|
1390
|
+
...await adapter.tools(project),
|
|
1391
|
+
verificationTool(config, fingerprint)
|
|
1392
|
+
];
|
|
1393
|
+
const automatic = trusted ? await builtinMcp(root) : void 0;
|
|
1394
|
+
const servers = [...config.mcp, ...automatic && !config.mcp.some((s) => s.name === automatic.name) ? [automatic] : []];
|
|
1395
|
+
for (const server of servers) try {
|
|
1396
|
+
const connection = await connectMcp(server, context, {
|
|
1397
|
+
config,
|
|
1398
|
+
fingerprint,
|
|
1399
|
+
builtin: server === automatic
|
|
1400
|
+
});
|
|
1401
|
+
connections.push(connection);
|
|
1402
|
+
tools.push(...connection.tools);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
warnings.push(`MCP ${server.name} unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1405
|
+
}
|
|
1406
|
+
return await runAgent({
|
|
1407
|
+
root,
|
|
1408
|
+
config,
|
|
1409
|
+
model,
|
|
1410
|
+
tools,
|
|
1411
|
+
prompt,
|
|
1412
|
+
images: await images(options.image ?? []),
|
|
1413
|
+
sessionId,
|
|
1414
|
+
signal,
|
|
1415
|
+
trusted,
|
|
1416
|
+
approve: selectedApprover,
|
|
1417
|
+
acknowledgeInterrupted: options.acknowledgeInterrupted,
|
|
1418
|
+
system: `${await adapter.instructions(project)}\nUnavailable capabilities:\n${warnings.join("\n")}`,
|
|
1419
|
+
onEvent: async (event) => {
|
|
1420
|
+
if (onEvent) onEvent(event);
|
|
1421
|
+
else if (options.json) output(event, true);
|
|
1422
|
+
else {
|
|
1423
|
+
if (event.type === "run.started") process.stderr.write(`Session: ${event.sessionId}\n${warnings.join("\n")}\n`);
|
|
1424
|
+
process.stdout.write(clean(eventText(event)));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
} finally {
|
|
1429
|
+
if (!suppliedSignal) process.removeListener("SIGINT", stop);
|
|
1430
|
+
await Promise.allSettled(connections.map((c) => c.close()));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function exitFor(status) {
|
|
1434
|
+
process.exitCode = {
|
|
1435
|
+
completed: 0,
|
|
1436
|
+
failed: 1,
|
|
1437
|
+
action_required: 2,
|
|
1438
|
+
limit_reached: 3,
|
|
1439
|
+
cancelled: 130
|
|
1440
|
+
}[status] ?? 1;
|
|
1441
|
+
}
|
|
1442
|
+
program.command("init").description("Configure an existing project, or scaffold one with --create").argument("[directory]", "target directory").option("--create", "create a new weapp-vite project; target must not exist").addOption(new Option("--template <template>", "new project template").choices(["wevu", "native"]).default("wevu")).addOption(new Option("--provider <provider>", "model provider").choices([
|
|
1443
|
+
"openai",
|
|
1444
|
+
"anthropic",
|
|
1445
|
+
"openai-compatible"
|
|
1446
|
+
]).default("openai")).requiredOption("--model <model>", "model identifier; no hard-coded model default").option("--base-url <url>", "custom provider endpoint").option("--api-key-env <name>", "environment variable containing the API key").action(async (directory, local, command) => {
|
|
1447
|
+
const opts = command.optsWithGlobals();
|
|
1448
|
+
const root = path.resolve(directory ?? opts.cwd);
|
|
1449
|
+
if (local.create) {
|
|
1450
|
+
if (await exists(root)) throw new Error("Creation target already exists; refusing to overwrite it.");
|
|
1451
|
+
await execa("pnpm", [
|
|
1452
|
+
"dlx",
|
|
1453
|
+
"create-weapp-vite@3.0.1",
|
|
1454
|
+
root,
|
|
1455
|
+
local.template === "native" ? "default" : "wevu",
|
|
1456
|
+
"--no-install-skills"
|
|
1457
|
+
], { stdio: opts.json ? [
|
|
1458
|
+
"ignore",
|
|
1459
|
+
"pipe",
|
|
1460
|
+
"pipe"
|
|
1461
|
+
] : "inherit" });
|
|
1462
|
+
} else await mkdir(root, { recursive: true });
|
|
1463
|
+
const project = await detectProject(root);
|
|
1464
|
+
const config = configSchema.parse({
|
|
1465
|
+
model: {
|
|
1466
|
+
provider: local.provider,
|
|
1467
|
+
name: local.model,
|
|
1468
|
+
baseURL: local.baseUrl,
|
|
1469
|
+
apiKeyEnv: local.apiKeyEnv
|
|
1470
|
+
},
|
|
1471
|
+
verification: defaultVerification(project)
|
|
1472
|
+
});
|
|
1473
|
+
await writeFile(path.join(root, configFilename), `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
|
|
1474
|
+
output({
|
|
1475
|
+
status: "created",
|
|
1476
|
+
root,
|
|
1477
|
+
config: configFilename,
|
|
1478
|
+
next: `Set ${apiKeyVariable(config.model)}, install project dependencies, then run weapp-agent --trust.`
|
|
1479
|
+
}, opts.json);
|
|
1480
|
+
});
|
|
1481
|
+
program.command("run").description("Run a single task").argument("<prompt>").option("--image <file...>", "reference screenshots").action(async (prompt, _local, command) => {
|
|
1482
|
+
const options = command.optsWithGlobals();
|
|
1483
|
+
const result = await execute(prompt, options);
|
|
1484
|
+
if (!options.json && result.status !== "completed") output(result.text, false);
|
|
1485
|
+
exitFor(result.status);
|
|
1486
|
+
});
|
|
1487
|
+
program.command("resume").description("Resume a saved session without replaying completed tools").argument("<session>").argument("[prompt]", "follow-up task", "Continue the task after inspecting the current project state.").option("--acknowledge-interrupted", "confirm inspection of interrupted tool outcomes; do not replay them").option("--image <file...>").action(async (session, prompt, _local, command) => {
|
|
1488
|
+
const options = command.optsWithGlobals();
|
|
1489
|
+
const result = await execute(prompt, options, session);
|
|
1490
|
+
if (!options.json && result.status !== "completed") output(result.text, false);
|
|
1491
|
+
exitFor(result.status);
|
|
1492
|
+
});
|
|
1493
|
+
program.command("sessions").description("List sessions for this project").action(async (_local, command) => {
|
|
1494
|
+
const opts = command.optsWithGlobals();
|
|
1495
|
+
output(await Session.list(await findProjectRoot(opts.cwd)), opts.json);
|
|
1496
|
+
});
|
|
1497
|
+
program.command("doctor").description("Inspect configuration and local prerequisites without calling a model").action(async (_local, command) => {
|
|
1498
|
+
const opts = command.optsWithGlobals();
|
|
1499
|
+
const root = await findProjectRoot(opts.cwd);
|
|
1500
|
+
const project = await detectProject(root);
|
|
1501
|
+
let config;
|
|
1502
|
+
let configError;
|
|
1503
|
+
try {
|
|
1504
|
+
config = await loadConfig(root);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
configError = String(error);
|
|
1507
|
+
}
|
|
1508
|
+
const key = config ? apiKeyVariable(config.model) : void 0;
|
|
1509
|
+
output({
|
|
1510
|
+
root,
|
|
1511
|
+
node: process.version,
|
|
1512
|
+
project,
|
|
1513
|
+
config: config ? "valid" : configError,
|
|
1514
|
+
apiKey: key ? {
|
|
1515
|
+
variable: key,
|
|
1516
|
+
present: Boolean(process.env[key])
|
|
1517
|
+
} : null,
|
|
1518
|
+
localMcp: Boolean(await builtinMcp(root)),
|
|
1519
|
+
stateDirectory: stateRoot(),
|
|
1520
|
+
devtools: "unverified: use a real DevTools verification command",
|
|
1521
|
+
trusted: config ? await isTrusted(root, await projectFingerprint(root, config)) : false
|
|
1522
|
+
}, opts.json);
|
|
1523
|
+
if (!config) process.exitCode = 1;
|
|
1524
|
+
});
|
|
1525
|
+
program.command("verify").description("Run configured checks and report unverified categories").action(async (_local, command) => {
|
|
1526
|
+
const opts = command.optsWithGlobals();
|
|
1527
|
+
const root = await findProjectRoot(opts.cwd);
|
|
1528
|
+
const { config, fingerprint, trusted } = await configAndTrust(root, opts, approve);
|
|
1529
|
+
const abort = new AbortController();
|
|
1530
|
+
const stop = () => abort.abort();
|
|
1531
|
+
process.once("SIGINT", stop);
|
|
1532
|
+
try {
|
|
1533
|
+
const result = await verifyProject(config, {
|
|
1534
|
+
root,
|
|
1535
|
+
trusted,
|
|
1536
|
+
signal: abort.signal,
|
|
1537
|
+
approve: opts.json ? async () => false : approve
|
|
1538
|
+
}, fingerprint);
|
|
1539
|
+
output(result, opts.json);
|
|
1540
|
+
if (!result.passed) process.exitCode = 1;
|
|
1541
|
+
} finally {
|
|
1542
|
+
process.removeListener("SIGINT", stop);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
program.action(async () => {
|
|
1546
|
+
const options = program.opts();
|
|
1547
|
+
if (!process.stdin.isTTY || options.json) throw new Error("Interactive mode requires a TTY. Use weapp-agent run <prompt> --json for automation.");
|
|
1548
|
+
let trustOnce = options.trust;
|
|
1549
|
+
await interactive((prompt, session, signal, onEvent, approval) => {
|
|
1550
|
+
const current = {
|
|
1551
|
+
...options,
|
|
1552
|
+
trust: trustOnce
|
|
1553
|
+
};
|
|
1554
|
+
trustOnce = false;
|
|
1555
|
+
return execute(prompt, current, session, signal, onEvent, approval);
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1558
|
+
try {
|
|
1559
|
+
await program.parseAsync();
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
const message = clean(error instanceof Error ? error.message : String(error));
|
|
1562
|
+
if (process.argv.includes("--json")) output({
|
|
1563
|
+
version: 1,
|
|
1564
|
+
type: "error",
|
|
1565
|
+
data: { message }
|
|
1566
|
+
}, true);
|
|
1567
|
+
else process.stderr.write(`${message}\n`);
|
|
1568
|
+
process.exitCode = error instanceof Error && error.name === "ApprovalRequired" ? 2 : 1;
|
|
1569
|
+
}
|
|
1570
|
+
//#endregion
|
|
1571
|
+
export {};
|