@weapp-agent/cli 0.0.0 → 0.1.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/dist/index.mjs
ADDED
|
@@ -0,0 +1,2130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
3
|
+
import { cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rmdir, 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 { createRuntimeAcceptanceService, registerAcceptanceTools } from "@weapp-vite/mcp";
|
|
13
|
+
import { connectMiniProgram, prepareAcceptanceProject } from "weapp-ide-cli";
|
|
14
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
15
|
+
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
17
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
18
|
+
import * as import__weapp_vite_acceptance$3 from "@weapp-vite/acceptance";
|
|
19
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
20
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
21
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
22
|
+
import { jsonSchema, streamText, tool } from "ai";
|
|
23
|
+
import { Command, Option } from "commander";
|
|
24
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
25
|
+
import TextInput from "ink-text-input";
|
|
26
|
+
import { useRef, useState } from "react";
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
//#region \0rolldown/runtime.js
|
|
29
|
+
var __defProp$1 = Object.defineProperty;
|
|
30
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
31
|
+
var __getOwnPropNames$1 = Object.getOwnPropertyNames;
|
|
32
|
+
var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
|
|
33
|
+
var __exportAll$1 = (all, no_symbols) => {
|
|
34
|
+
let target = {};
|
|
35
|
+
for (var name in all) __defProp$1(target, name, {
|
|
36
|
+
get: all[name],
|
|
37
|
+
enumerable: true
|
|
38
|
+
});
|
|
39
|
+
if (!no_symbols) __defProp$1(target, Symbol.toStringTag, { value: "Module" });
|
|
40
|
+
return target;
|
|
41
|
+
};
|
|
42
|
+
var __copyProps$1 = (to, from, except, desc) => {
|
|
43
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames$1(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
44
|
+
key = keys[i];
|
|
45
|
+
if (!__hasOwnProp$1.call(to, key) && key !== except) __defProp$1(to, key, {
|
|
46
|
+
get: ((k) => from[k]).bind(null, key),
|
|
47
|
+
enumerable: !(desc = __getOwnPropDesc$1(from, key)) || desc.enumerable
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return to;
|
|
51
|
+
};
|
|
52
|
+
var __reExport$1 = (target, mod, secondTarget) => (__copyProps$1(target, mod, "default"), secondTarget && __copyProps$1(secondTarget, mod, "default"));
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../agent-core/dist/text-EVMG4Jiq.mjs
|
|
55
|
+
var ApprovalRequired = class extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ApprovalRequired";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function redactor(environment = process.env) {
|
|
62
|
+
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);
|
|
63
|
+
return (text) => {
|
|
64
|
+
for (const secret of secrets) text = text.split(secret).join("[REDACTED]");
|
|
65
|
+
return text.replace(/\b(sk-[\w-]{12,}|gh[pousr]_\w{16,})\b/g, "[REDACTED]").replace(/(Bearer\s+)[\w.\-/+=]{10,}/gi, "$1[REDACTED]");
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function inside(root, target) {
|
|
69
|
+
const relative = path.relative(root, target);
|
|
70
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
71
|
+
}
|
|
72
|
+
function sensitive(file) {
|
|
73
|
+
return file.split(/[\\/]/).some((part) => /^(?:\.git|\.weapp-agent|\.npmrc|\.env(?:\..+)?|.*\.(?:pem|key|p12))$/i.test(part) && part !== ".env.example");
|
|
74
|
+
}
|
|
75
|
+
/** Canonicalize ancestors even for new files: symlink escapes cannot bypass the boundary. */
|
|
76
|
+
async function safePath(root, file) {
|
|
77
|
+
const canonicalRoot = await realpath(root);
|
|
78
|
+
const target = path.resolve(canonicalRoot, file);
|
|
79
|
+
if (!inside(canonicalRoot, target) || sensitive(path.relative(canonicalRoot, target))) throw new Error(`Path is outside the permitted workspace or is sensitive: ${file}`);
|
|
80
|
+
let ancestor = target;
|
|
81
|
+
for (;;) try {
|
|
82
|
+
const resolved = await realpath(ancestor);
|
|
83
|
+
if (!inside(canonicalRoot, resolved) || sensitive(path.relative(canonicalRoot, resolved))) throw new Error(`Symlink crosses the workspace boundary: ${file}`);
|
|
84
|
+
break;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") throw error;
|
|
87
|
+
const parent = path.dirname(ancestor);
|
|
88
|
+
if (parent === ancestor) throw error;
|
|
89
|
+
ancestor = parent;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if ((await lstat(target)).isSymbolicLink()) throw new Error(`Editing or reading a symlink is not supported: ${file}`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code !== "ENOENT") throw error;
|
|
95
|
+
}
|
|
96
|
+
return target;
|
|
97
|
+
}
|
|
98
|
+
async function requireTrust(context) {
|
|
99
|
+
if (!context.trusted) throw new ApprovalRequired("Workspace is not trusted. Review its configuration, then run with --trust.");
|
|
100
|
+
}
|
|
101
|
+
async function authorize(context, kind, summary, payload) {
|
|
102
|
+
if (!await context.approve({
|
|
103
|
+
kind,
|
|
104
|
+
summary,
|
|
105
|
+
fingerprint: hash(JSON.stringify(payload))
|
|
106
|
+
})) throw new ApprovalRequired(`Approval required: ${summary}`);
|
|
107
|
+
}
|
|
108
|
+
function redactValue(value, clean = redactor()) {
|
|
109
|
+
if (typeof value === "string") return clean(value);
|
|
110
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(v, clean));
|
|
111
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactValue(v, clean)]));
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
const verificationSchema = z.strictObject({
|
|
115
|
+
kind: z.enum([
|
|
116
|
+
"typecheck",
|
|
117
|
+
"build",
|
|
118
|
+
"test",
|
|
119
|
+
"devtools"
|
|
120
|
+
]),
|
|
121
|
+
command: z.string().min(1),
|
|
122
|
+
args: z.array(z.string()).default([]),
|
|
123
|
+
timeoutMs: z.number().int().positive().max(18e5).default(12e4)
|
|
124
|
+
});
|
|
125
|
+
const mcpSchema = z.discriminatedUnion("transport", [z.strictObject({
|
|
126
|
+
name: z.string().regex(/^[\w-]+$/),
|
|
127
|
+
transport: z.literal("stdio"),
|
|
128
|
+
command: z.string(),
|
|
129
|
+
args: z.array(z.string()).default([])
|
|
130
|
+
}), z.strictObject({
|
|
131
|
+
name: z.string().regex(/^[\w-]+$/),
|
|
132
|
+
transport: z.literal("http"),
|
|
133
|
+
url: z.url(),
|
|
134
|
+
tokenEnv: z.string().optional()
|
|
135
|
+
})]);
|
|
136
|
+
const modelSchema = z.strictObject({
|
|
137
|
+
provider: z.enum([
|
|
138
|
+
"openai",
|
|
139
|
+
"anthropic",
|
|
140
|
+
"openai-compatible"
|
|
141
|
+
]),
|
|
142
|
+
name: z.string().min(1),
|
|
143
|
+
baseURL: z.url().optional(),
|
|
144
|
+
apiKeyEnv: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional()
|
|
145
|
+
});
|
|
146
|
+
const projectConfigSchema = z.strictObject({
|
|
147
|
+
version: z.literal(1).default(1),
|
|
148
|
+
model: modelSchema.optional(),
|
|
149
|
+
maxSteps: z.number().int().min(1).max(500).default(40),
|
|
150
|
+
timeoutMs: z.number().int().positive().default(6e5),
|
|
151
|
+
contextCharacters: z.number().int().min(8e3).default(1e5),
|
|
152
|
+
verification: z.array(verificationSchema).default([]),
|
|
153
|
+
mcp: z.array(mcpSchema).default([]),
|
|
154
|
+
acceptance: z.strictObject({
|
|
155
|
+
requiredChecks: z.array(z.enum([
|
|
156
|
+
"typecheck",
|
|
157
|
+
"build",
|
|
158
|
+
"test",
|
|
159
|
+
"devtools"
|
|
160
|
+
])).min(1).default(["build", "devtools"]),
|
|
161
|
+
scenarios: z.array(z.string().min(1)).max(100).default([]),
|
|
162
|
+
timeoutMs: z.number().int().min(100).max(18e5).default(6e5)
|
|
163
|
+
}).default({
|
|
164
|
+
requiredChecks: ["build", "devtools"],
|
|
165
|
+
scenarios: [],
|
|
166
|
+
timeoutMs: 6e5
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
const configSchema = projectConfigSchema.extend({ model: modelSchema });
|
|
170
|
+
const configFilename = "weapp-agent.config.json";
|
|
171
|
+
const selectedConfigs = /* @__PURE__ */ new WeakMap();
|
|
172
|
+
function configurationSource(config) {
|
|
173
|
+
return selectedConfigs.get(config);
|
|
174
|
+
}
|
|
175
|
+
async function loadProjectConfig(root) {
|
|
176
|
+
let raw;
|
|
177
|
+
try {
|
|
178
|
+
raw = await readFile(path.join(root, configFilename), "utf8");
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code !== "ENOENT") throw error;
|
|
181
|
+
throw new Error(`Missing ${configFilename}. Run weapp-agent init; add --model <model> only for independent agent mode.`);
|
|
182
|
+
}
|
|
183
|
+
return projectConfigSchema.parse(JSON.parse(raw));
|
|
184
|
+
}
|
|
185
|
+
async function loadConfig(root) {
|
|
186
|
+
const config = await loadProjectConfig(root);
|
|
187
|
+
if (!config.model) throw new Error("Agent mode requires model configuration. Set model.provider and model.name in weapp-agent.config.json. Acceptance and MCP do not require a model.");
|
|
188
|
+
return configSchema.parse(config);
|
|
189
|
+
}
|
|
190
|
+
function hash(value) {
|
|
191
|
+
return createHash("sha256").update(value).digest("hex");
|
|
192
|
+
}
|
|
193
|
+
function stateRoot() {
|
|
194
|
+
return path.resolve(process.env.WEAPP_AGENT_STATE_DIR ?? path.join(homedir(), ".local", "state", "weapp-agent"));
|
|
195
|
+
}
|
|
196
|
+
async function projectFingerprint(root, config) {
|
|
197
|
+
let pkg = "";
|
|
198
|
+
try {
|
|
199
|
+
pkg = await readFile(path.join(root, "package.json"), "utf8");
|
|
200
|
+
} catch {}
|
|
201
|
+
let diskConfig = "";
|
|
202
|
+
try {
|
|
203
|
+
diskConfig = await readFile(await safePath(root, configurationSource(config) ?? "weapp-agent.config.json"), "utf8");
|
|
204
|
+
} catch {}
|
|
205
|
+
const scenarios = [];
|
|
206
|
+
for (const file of config.acceptance.scenarios) {
|
|
207
|
+
const target = await safePath(root, file);
|
|
208
|
+
try {
|
|
209
|
+
scenarios.push([file, await readFile(target, "utf8")]);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.code !== "ENOENT") throw error;
|
|
212
|
+
scenarios.push([file, "<missing>"]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const projectFiles = [];
|
|
216
|
+
for (const file of [
|
|
217
|
+
"project.config.json",
|
|
218
|
+
"vite.config.ts",
|
|
219
|
+
"vite.config.js",
|
|
220
|
+
"vite.config.mts",
|
|
221
|
+
"vite.config.mjs",
|
|
222
|
+
"vite.config.cts",
|
|
223
|
+
"vite.config.cjs",
|
|
224
|
+
"pnpm-workspace.yaml",
|
|
225
|
+
"pnpm-lock.yaml",
|
|
226
|
+
"package-lock.json",
|
|
227
|
+
"yarn.lock",
|
|
228
|
+
"bun.lock"
|
|
229
|
+
]) try {
|
|
230
|
+
projectFiles.push([file, hash(await readFile(await safePath(root, file), "utf8"))]);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error.code !== "ENOENT") throw error;
|
|
233
|
+
}
|
|
234
|
+
return hash(JSON.stringify({
|
|
235
|
+
root,
|
|
236
|
+
config,
|
|
237
|
+
pkg,
|
|
238
|
+
diskConfig,
|
|
239
|
+
scenarios,
|
|
240
|
+
projectFiles,
|
|
241
|
+
...configurationSource(config) && configurationSource(config) !== "weapp-agent.config.json" ? { configurationSource: configurationSource(config) } : {}
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
async function isTrusted(root, fingerprint) {
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(await readFile(path.join(stateRoot(), "trust", `${hash(root)}.json`), "utf8")).fingerprint === fingerprint;
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function trustProject(root, fingerprint) {
|
|
252
|
+
const dir = path.join(stateRoot(), "trust");
|
|
253
|
+
await mkdir(dir, {
|
|
254
|
+
recursive: true,
|
|
255
|
+
mode: 448
|
|
256
|
+
});
|
|
257
|
+
await writeFile(path.join(dir, `${hash(root)}.json`), JSON.stringify({
|
|
258
|
+
root,
|
|
259
|
+
fingerprint
|
|
260
|
+
}), { mode: 384 });
|
|
261
|
+
}
|
|
262
|
+
function bounded(text, max = 4e4) {
|
|
263
|
+
return text.length > max ? `${text.slice(0, max)}\n[truncated: ${text.length - max} more characters]` : text;
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region ../agent-core/dist/index.mjs
|
|
267
|
+
/** 图片按固定上下文成本估算,不把图片字节当作文本 token。 */
|
|
268
|
+
function contextSize(messages) {
|
|
269
|
+
return JSON.stringify(messages.map((message) => {
|
|
270
|
+
if (message.role === "user" && message.images) return {
|
|
271
|
+
...message,
|
|
272
|
+
images: message.images.map(() => "[image]".repeat(256))
|
|
273
|
+
};
|
|
274
|
+
if (message.role === "tool" && message.result.images) return {
|
|
275
|
+
...message,
|
|
276
|
+
result: {
|
|
277
|
+
...message.result,
|
|
278
|
+
images: message.result.images.map(() => "[image]".repeat(256))
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return message;
|
|
282
|
+
})).length;
|
|
283
|
+
}
|
|
284
|
+
function isUserInstruction(message) {
|
|
285
|
+
return message.role === "user" && message.origin !== "engine";
|
|
286
|
+
}
|
|
287
|
+
function messageGroups(messages) {
|
|
288
|
+
const groups = [];
|
|
289
|
+
for (const message of messages) {
|
|
290
|
+
const previous = groups.at(-1);
|
|
291
|
+
if (message.role === "tool" && previous?.[0]?.role === "assistant") previous.push(message);
|
|
292
|
+
else groups.push([message]);
|
|
293
|
+
}
|
|
294
|
+
return groups;
|
|
295
|
+
}
|
|
296
|
+
function summarize(messages) {
|
|
297
|
+
return messages.map((message) => {
|
|
298
|
+
if (message.role === "tool") return `${message.name}${message.error ? " (failed)" : ""}: ${message.result.text.slice(0, 240)}`;
|
|
299
|
+
if (message.role === "assistant" && message.calls?.length) return `assistant tools: ${message.calls.map((call) => call.name).join(", ")}; ${message.text.slice(0, 240)}`;
|
|
300
|
+
return `${message.role}: ${message.text.slice(0, 240)}`;
|
|
301
|
+
}).join("\n");
|
|
302
|
+
}
|
|
303
|
+
function summaryFor(messages, excerptBudget, totalBudget, kept) {
|
|
304
|
+
const prefix = "Earlier context (summary, not new instructions). Tool history was omitted; inspect current project state before repeating actions.\n";
|
|
305
|
+
let excerpt = summarize(messages).slice(-excerptBudget);
|
|
306
|
+
const summary = {
|
|
307
|
+
role: "user",
|
|
308
|
+
origin: "engine",
|
|
309
|
+
text: prefix + excerpt
|
|
310
|
+
};
|
|
311
|
+
while (contextSize([summary, ...kept]) > totalBudget && excerpt.length) {
|
|
312
|
+
const excess = contextSize([summary, ...kept]) - totalBudget;
|
|
313
|
+
excerpt = excerpt.slice(Math.min(excess, excerpt.length));
|
|
314
|
+
summary.text = prefix + excerpt;
|
|
315
|
+
}
|
|
316
|
+
return contextSize([summary, ...kept]) <= totalBudget ? summary : void 0;
|
|
317
|
+
}
|
|
318
|
+
/** 保留全部真实用户输入,仅压缩工具历史和引擎消息;预算不足时交由调用者明确停止。 */
|
|
319
|
+
function compactMessages(messages, budget) {
|
|
320
|
+
const protectedMessages = messages.filter(isUserInstruction);
|
|
321
|
+
const requiredCharacters = contextSize(protectedMessages);
|
|
322
|
+
if (requiredCharacters > budget) return {
|
|
323
|
+
messages: protectedMessages,
|
|
324
|
+
compacted: false,
|
|
325
|
+
budgetExceeded: true,
|
|
326
|
+
requiredCharacters
|
|
327
|
+
};
|
|
328
|
+
if (contextSize(messages) <= budget) return {
|
|
329
|
+
messages,
|
|
330
|
+
compacted: false,
|
|
331
|
+
budgetExceeded: false,
|
|
332
|
+
requiredCharacters
|
|
333
|
+
};
|
|
334
|
+
const groups = messageGroups(messages);
|
|
335
|
+
const protectedGroups = new Set(groups.filter((group) => isUserInstruction(group[0])));
|
|
336
|
+
const retained = new Set(protectedGroups);
|
|
337
|
+
let size = requiredCharacters;
|
|
338
|
+
const summaryBudget = Math.min(2e3, Math.floor((budget - size) / 4));
|
|
339
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
340
|
+
const group = groups[i];
|
|
341
|
+
if (retained.has(group)) continue;
|
|
342
|
+
const cost = contextSize(group);
|
|
343
|
+
if (size + cost <= budget - summaryBudget) {
|
|
344
|
+
retained.add(group);
|
|
345
|
+
size += cost;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
while (true) {
|
|
349
|
+
const kept = groups.filter((group) => retained.has(group)).flat();
|
|
350
|
+
const omitted = groups.filter((group) => !retained.has(group)).flat();
|
|
351
|
+
const summary = summaryBudget ? summaryFor(omitted, summaryBudget, budget, kept) : void 0;
|
|
352
|
+
const candidate = summary ? [summary, ...kept] : kept;
|
|
353
|
+
if (contextSize(candidate) <= budget) return {
|
|
354
|
+
messages: candidate,
|
|
355
|
+
compacted: true,
|
|
356
|
+
budgetExceeded: false,
|
|
357
|
+
requiredCharacters
|
|
358
|
+
};
|
|
359
|
+
const oldestOptional = groups.find((group) => retained.has(group) && !protectedGroups.has(group));
|
|
360
|
+
if (!oldestOptional) return {
|
|
361
|
+
messages: protectedMessages,
|
|
362
|
+
compacted: true,
|
|
363
|
+
budgetExceeded: false,
|
|
364
|
+
requiredCharacters
|
|
365
|
+
};
|
|
366
|
+
retained.delete(oldestOptional);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function isMissing(error) {
|
|
370
|
+
return error.code === "ENOENT";
|
|
371
|
+
}
|
|
372
|
+
function sameFile(left, right) {
|
|
373
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
374
|
+
}
|
|
375
|
+
async function currentOwner(filename, identity) {
|
|
376
|
+
try {
|
|
377
|
+
const current = await lstat(filename, { bigint: true });
|
|
378
|
+
return current.isFile() && sameFile(current, identity);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (isMissing(error)) return false;
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function heldLock(filename, handle) {
|
|
385
|
+
const identity = await handle.stat({ bigint: true });
|
|
386
|
+
let released = false;
|
|
387
|
+
let releasing;
|
|
388
|
+
return {
|
|
389
|
+
async assertOwned() {
|
|
390
|
+
if (released || !await currentOwner(filename, identity)) throw new Error("Session lock ownership was lost; refusing to write the journal.");
|
|
391
|
+
},
|
|
392
|
+
release() {
|
|
393
|
+
if (!releasing) {
|
|
394
|
+
released = true;
|
|
395
|
+
releasing = (async () => {
|
|
396
|
+
try {
|
|
397
|
+
if (await currentOwner(filename, identity)) await unlink(filename).catch((error) => {
|
|
398
|
+
if (!isMissing(error)) throw error;
|
|
399
|
+
});
|
|
400
|
+
} finally {
|
|
401
|
+
await handle.close();
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
}
|
|
405
|
+
return releasing;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function createLock(filename) {
|
|
410
|
+
const handle = await open(filename, "wx", 384);
|
|
411
|
+
let lock;
|
|
412
|
+
try {
|
|
413
|
+
lock = await heldLock(filename, handle);
|
|
414
|
+
await handle.writeFile(String(process.pid));
|
|
415
|
+
await handle.sync();
|
|
416
|
+
await lock.assertOwned();
|
|
417
|
+
return lock;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (lock) await lock.release();
|
|
420
|
+
else await handle.close();
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** 只有已持有回收互斥锁的调用者才能读取、核实和回收失效的旧锁。 */
|
|
425
|
+
async function reclaimLock(filename) {
|
|
426
|
+
const snapshot = await lstat(filename, { bigint: true });
|
|
427
|
+
if (!snapshot.isFile()) throw new Error("Session lock is not a regular file; inspect it before removing it.");
|
|
428
|
+
const handle = await open(filename, "r");
|
|
429
|
+
try {
|
|
430
|
+
const identity = await handle.stat({ bigint: true });
|
|
431
|
+
if (!sameFile(snapshot, identity) || !await currentOwner(filename, identity)) throw new Error("Session lock changed during recovery; inspect it before retrying.");
|
|
432
|
+
const raw = (await handle.readFile("utf8")).trim();
|
|
433
|
+
const pid = Number(raw);
|
|
434
|
+
if (!/^[1-9]\d*$/.test(raw) || !Number.isSafeInteger(pid)) throw new Error("Session lock is invalid; inspect it before removing it.");
|
|
435
|
+
try {
|
|
436
|
+
process.kill(pid, 0);
|
|
437
|
+
throw new Error("Session is already active in another process.");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (error.code !== "ESRCH") throw error;
|
|
440
|
+
}
|
|
441
|
+
if (!await currentOwner(filename, identity)) throw new Error("Session lock changed during recovery; inspect it before retrying.");
|
|
442
|
+
await unlink(filename);
|
|
443
|
+
} finally {
|
|
444
|
+
await handle.close();
|
|
445
|
+
}
|
|
446
|
+
return createLock(filename);
|
|
447
|
+
}
|
|
448
|
+
async function releaseRecoveryGuard(guard, identity) {
|
|
449
|
+
const current = await lstat(guard, { bigint: true });
|
|
450
|
+
if (!current.isDirectory() || !sameFile(current, identity)) throw new Error("Session recovery guard ownership was lost; inspect it before retrying.");
|
|
451
|
+
await rmdir(guard);
|
|
452
|
+
}
|
|
453
|
+
/** 串行化抢占和回收;未知回收目录不按时间或 PID 推断归属。 */
|
|
454
|
+
async function acquireSessionLock(filename) {
|
|
455
|
+
const lockfile = `${filename}.lock`;
|
|
456
|
+
const guard = `${lockfile}.reclaim`;
|
|
457
|
+
try {
|
|
458
|
+
await mkdir(guard, { mode: 448 });
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error.code === "EEXIST") throw new Error("Session lock recovery is in progress or was interrupted; inspect the recovery guard before retrying.");
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
const guardIdentity = await lstat(guard, { bigint: true });
|
|
464
|
+
let lock;
|
|
465
|
+
try {
|
|
466
|
+
try {
|
|
467
|
+
try {
|
|
468
|
+
lock = await createLock(lockfile);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (error.code !== "EEXIST") throw error;
|
|
471
|
+
lock = await reclaimLock(lockfile);
|
|
472
|
+
}
|
|
473
|
+
} finally {
|
|
474
|
+
await releaseRecoveryGuard(guard, guardIdentity);
|
|
475
|
+
}
|
|
476
|
+
return lock;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
await lock?.release();
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
var SessionJournalError = class extends Error {
|
|
483
|
+
events;
|
|
484
|
+
constructor(message, events = []) {
|
|
485
|
+
super(message);
|
|
486
|
+
this.events = events;
|
|
487
|
+
this.name = "SessionJournalError";
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
function record(value) {
|
|
491
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
492
|
+
}
|
|
493
|
+
function nonempty(value) {
|
|
494
|
+
return typeof value === "string" && value.length > 0;
|
|
495
|
+
}
|
|
496
|
+
function images$1(value) {
|
|
497
|
+
return value === void 0 || Array.isArray(value) && value.every((image) => record(image) && image.type === "image" && typeof image.data === "string" && nonempty(image.mediaType));
|
|
498
|
+
}
|
|
499
|
+
function message(value) {
|
|
500
|
+
if (!record(value)) return false;
|
|
501
|
+
if (value.role === "user") return typeof value.text === "string" && images$1(value.images) && (value.origin === void 0 || value.origin === "user" || value.origin === "engine");
|
|
502
|
+
if (value.role === "assistant") return typeof value.text === "string" && (value.calls === void 0 || Array.isArray(value.calls) && value.calls.every((call) => record(call) && nonempty(call.id) && nonempty(call.name)));
|
|
503
|
+
return value.role === "tool" && nonempty(value.callId) && nonempty(value.name) && (value.error === void 0 || typeof value.error === "boolean") && record(value.result) && typeof value.result.text === "string" && images$1(value.result.images);
|
|
504
|
+
}
|
|
505
|
+
function counter(value) {
|
|
506
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
507
|
+
}
|
|
508
|
+
const KNOWN_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
509
|
+
"run.started",
|
|
510
|
+
"message",
|
|
511
|
+
"tool.started",
|
|
512
|
+
"tool.completed",
|
|
513
|
+
"step.started",
|
|
514
|
+
"usage",
|
|
515
|
+
"run.completed",
|
|
516
|
+
"text.delta",
|
|
517
|
+
"context.compacted",
|
|
518
|
+
"recovery.required"
|
|
519
|
+
]);
|
|
520
|
+
function eventData(type, data) {
|
|
521
|
+
if (type === "message") return message(data.message);
|
|
522
|
+
if (type === "tool.started") return nonempty(data.callId) && (data.name === void 0 || nonempty(data.name));
|
|
523
|
+
if (type === "tool.completed") return nonempty(data.callId) && (data.name === void 0 || nonempty(data.name)) && (data.error === void 0 || typeof data.error === "boolean") && (data.result === void 0 || record(data.result) && typeof data.result.text === "string" && images$1(data.result.images));
|
|
524
|
+
if (type === "step.started") return counter(data.step) && Number.isInteger(data.step) && Number(data.step) > 0;
|
|
525
|
+
if (type === "usage") return counter(data.inputTokens) && counter(data.outputTokens);
|
|
526
|
+
if (type === "run.completed") return [
|
|
527
|
+
"completed",
|
|
528
|
+
"failed",
|
|
529
|
+
"cancelled",
|
|
530
|
+
"action_required",
|
|
531
|
+
"limit_reached"
|
|
532
|
+
].includes(String(data.status)) && (data.reason === void 0 || data.reason === "max_steps" || data.reason === "context_budget");
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
/** 只读取完整记录;恢复入口持有写锁后才允许修复尾部半行。 */
|
|
536
|
+
async function readSessionJournal(filename, sessionId) {
|
|
537
|
+
const raw = await readFile(filename, "utf8");
|
|
538
|
+
if (raw.length === 0) return {
|
|
539
|
+
events: [],
|
|
540
|
+
completeBytes: 0,
|
|
541
|
+
incompleteTail: false,
|
|
542
|
+
needsSeparator: false,
|
|
543
|
+
diagnostics: []
|
|
544
|
+
};
|
|
545
|
+
const hasTrailingNewline = raw.endsWith("\n");
|
|
546
|
+
const lines = raw.split("\n");
|
|
547
|
+
const completeLines = lines.slice(0, -1);
|
|
548
|
+
const tail = hasTrailingNewline ? "" : lines.at(-1) ?? "";
|
|
549
|
+
const events = [];
|
|
550
|
+
for (const line of completeLines) {
|
|
551
|
+
if (!line) continue;
|
|
552
|
+
let value;
|
|
553
|
+
try {
|
|
554
|
+
value = JSON.parse(line);
|
|
555
|
+
} catch {
|
|
556
|
+
throw new SessionJournalError(`Invalid session journal record ${events.length + 1}: malformed JSON; refusing to replay it.`, events);
|
|
557
|
+
}
|
|
558
|
+
if (!record(value) || value.version !== 1 || value.sessionId !== sessionId || value.sequence !== events.length + 1 || !nonempty(value.timestamp) || !Number.isFinite(Date.parse(value.timestamp)) || !nonempty(value.type)) throw new SessionJournalError(`Invalid session journal record ${events.length + 1}: invalid event or message; refusing to replay it.`, events);
|
|
559
|
+
if (!KNOWN_EVENT_TYPES.has(value.type) && !record(value.data)) {
|
|
560
|
+
events.push(value);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!record(value.data) || !eventData(value.type, value.data)) throw new SessionJournalError(`Invalid session journal record ${events.length + 1}: invalid event or message; refusing to replay it.`, events);
|
|
564
|
+
events.push(value);
|
|
565
|
+
}
|
|
566
|
+
if (tail) {
|
|
567
|
+
let value;
|
|
568
|
+
try {
|
|
569
|
+
value = JSON.parse(tail);
|
|
570
|
+
} catch {
|
|
571
|
+
return {
|
|
572
|
+
events,
|
|
573
|
+
completeBytes: Buffer$1.byteLength(raw.slice(0, raw.length - tail.length)),
|
|
574
|
+
incompleteTail: true,
|
|
575
|
+
needsSeparator: false,
|
|
576
|
+
diagnostics: ["Ignored an incomplete trailing journal record; resume repairs it while holding the session lock."]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (!record(value) || value.version !== 1 || value.sessionId !== sessionId || value.sequence !== events.length + 1 || !nonempty(value.timestamp) || !Number.isFinite(Date.parse(value.timestamp)) || !nonempty(value.type)) throw new SessionJournalError(`Invalid session journal record ${events.length + 1}: invalid event or message; refusing to replay it.`, events);
|
|
580
|
+
if (!KNOWN_EVENT_TYPES.has(value.type) && !record(value.data)) events.push(value);
|
|
581
|
+
else if (!record(value.data) || !eventData(value.type, value.data)) throw new SessionJournalError(`Invalid session journal record ${events.length + 1}: invalid event or message; refusing to replay it.`, events);
|
|
582
|
+
else events.push(value);
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
events,
|
|
586
|
+
completeBytes: Buffer$1.byteLength(raw),
|
|
587
|
+
incompleteTail: false,
|
|
588
|
+
needsSeparator: !hasTrailingNewline,
|
|
589
|
+
diagnostics: []
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/** 顺序消费记录,完成结果只匹配在它之前声明的调用。 */
|
|
593
|
+
function reduceSession(events) {
|
|
594
|
+
const deferredInputs = [];
|
|
595
|
+
const state = {
|
|
596
|
+
messages: [],
|
|
597
|
+
pendingCalls: [],
|
|
598
|
+
prompt: "",
|
|
599
|
+
updatedAt: null,
|
|
600
|
+
status: "empty",
|
|
601
|
+
steps: 0,
|
|
602
|
+
usage: {
|
|
603
|
+
inputTokens: 0,
|
|
604
|
+
outputTokens: 0
|
|
605
|
+
},
|
|
606
|
+
diagnostics: []
|
|
607
|
+
};
|
|
608
|
+
for (const event of events) {
|
|
609
|
+
state.updatedAt = event.timestamp;
|
|
610
|
+
if (event.type === "run.started") {
|
|
611
|
+
state.status = "unfinished";
|
|
612
|
+
state.reason = void 0;
|
|
613
|
+
state.steps = 0;
|
|
614
|
+
state.usage = {
|
|
615
|
+
inputTokens: 0,
|
|
616
|
+
outputTokens: 0
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (event.type === "run.completed") {
|
|
620
|
+
state.status = event.data.status;
|
|
621
|
+
state.reason = event.data.reason;
|
|
622
|
+
}
|
|
623
|
+
if (event.type === "step.started") state.steps = Math.max(state.steps, Number(event.data.step));
|
|
624
|
+
if (event.type === "usage") {
|
|
625
|
+
state.usage.inputTokens += Number(event.data.inputTokens);
|
|
626
|
+
state.usage.outputTokens += Number(event.data.outputTokens);
|
|
627
|
+
}
|
|
628
|
+
if (event.type === "message") {
|
|
629
|
+
const message = event.data.message;
|
|
630
|
+
if (message.role === "user" && state.pendingCalls.length) deferredInputs.push(message);
|
|
631
|
+
else state.messages.push(message);
|
|
632
|
+
if (state.status === "empty") state.status = "unfinished";
|
|
633
|
+
if (message.role === "user" && message.origin !== "engine") state.prompt = message.text;
|
|
634
|
+
if (message.role === "assistant") for (const call of message.calls ?? []) {
|
|
635
|
+
if (state.pendingCalls.some((pending) => pending.id === call.id)) throw new SessionJournalError("Ambiguous session journal: multiple unresolved tool calls share an ID; refusing to replay it.", events);
|
|
636
|
+
state.pendingCalls.push({
|
|
637
|
+
...call,
|
|
638
|
+
state: "not_executed"
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (message.role === "tool") {
|
|
642
|
+
const index = state.pendingCalls.findIndex((call) => call.id === message.callId);
|
|
643
|
+
if (index >= 0) {
|
|
644
|
+
if (state.pendingCalls[index].name !== message.name) throw new SessionJournalError("Invalid session journal: tool result name does not match its pending call; refusing to replay it.", events);
|
|
645
|
+
state.pendingCalls.splice(index, 1);
|
|
646
|
+
if (!state.pendingCalls.length) state.messages.push(...deferredInputs.splice(0));
|
|
647
|
+
} else throw new SessionJournalError(`Invalid session journal: tool result in record ${event.sequence} has no preceding pending call; refusing to replay it.`, events);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (event.type === "tool.started") {
|
|
651
|
+
const pending = state.pendingCalls.find((call) => call.id === event.data.callId);
|
|
652
|
+
if (pending) {
|
|
653
|
+
if (event.data.name !== void 0 && event.data.name !== pending.name) throw new SessionJournalError("Invalid session journal: tool start name does not match its pending call; refusing to replay it.", events);
|
|
654
|
+
pending.state = "outcome_unknown";
|
|
655
|
+
} else throw new SessionJournalError(`Invalid session journal: tool start in record ${event.sequence} has no preceding pending call; refusing to replay it.`, events);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
state.messages.push(...deferredInputs);
|
|
659
|
+
return state;
|
|
660
|
+
}
|
|
661
|
+
function displayText(text, clean, limit = 240) {
|
|
662
|
+
const safe = clean(text).replace(/data:[^\s,]+,\S+/gi, "[image/data omitted]").replace(/[A-Z0-9+/]{80,}={0,2}/gi, "[binary data omitted]").replace(/\s+/g, " ");
|
|
663
|
+
return safe.length > limit ? `${safe.slice(0, limit)}…` : safe;
|
|
664
|
+
}
|
|
665
|
+
/** 详情只暴露有限预览,图片载荷和敏感字段不进入展示接口。 */
|
|
666
|
+
function preview(value, clean, depth = 0) {
|
|
667
|
+
if (typeof value === "string") return displayText(value, clean);
|
|
668
|
+
if (value === null || typeof value !== "object") return value;
|
|
669
|
+
if (depth >= 3) return "[nested data omitted]";
|
|
670
|
+
if (Array.isArray(value)) {
|
|
671
|
+
const items = value.slice(0, 8).map((item) => preview(item, clean, depth + 1));
|
|
672
|
+
return value.length > 8 ? [...items, "[additional items omitted]"] : items;
|
|
673
|
+
}
|
|
674
|
+
const object = value;
|
|
675
|
+
if (object.type === "image" || object.type === "image_url" || object.mediaType !== void 0) return "[image omitted]";
|
|
676
|
+
return Object.fromEntries(Object.entries(object).slice(0, 12).map(([key, item]) => [displayText(key, clean, 80), /key|token|secret|password|authorization/i.test(key) ? "[REDACTED]" : /^(?:data|base64|images?|image_url)$/i.test(key) ? "[data omitted]" : preview(item, clean, depth + 1)]));
|
|
677
|
+
}
|
|
678
|
+
async function inspectSession(filename, sessionId) {
|
|
679
|
+
const clean = redactor();
|
|
680
|
+
try {
|
|
681
|
+
const journal = await readSessionJournal(filename, sessionId);
|
|
682
|
+
const { messages: _, ...state } = reduceSession(journal.events);
|
|
683
|
+
return {
|
|
684
|
+
sessionId,
|
|
685
|
+
...state,
|
|
686
|
+
prompt: displayText(state.prompt, clean),
|
|
687
|
+
pendingCalls: state.pendingCalls.map((call) => ({
|
|
688
|
+
id: displayText(call.id, clean, 100),
|
|
689
|
+
name: displayText(call.name, clean, 100),
|
|
690
|
+
input: preview(call.input, clean),
|
|
691
|
+
state: call.state
|
|
692
|
+
})),
|
|
693
|
+
diagnostics: [...journal.diagnostics, ...state.diagnostics].map((text) => displayText(text, clean))
|
|
694
|
+
};
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (!(error instanceof SessionJournalError)) throw error;
|
|
697
|
+
let prefix = {
|
|
698
|
+
sessionId,
|
|
699
|
+
updatedAt: error.events.at(-1)?.timestamp ?? null,
|
|
700
|
+
prompt: "",
|
|
701
|
+
status: "invalid",
|
|
702
|
+
steps: 0,
|
|
703
|
+
usage: {
|
|
704
|
+
inputTokens: 0,
|
|
705
|
+
outputTokens: 0
|
|
706
|
+
},
|
|
707
|
+
pendingCalls: [],
|
|
708
|
+
diagnostics: []
|
|
709
|
+
};
|
|
710
|
+
try {
|
|
711
|
+
const { messages: _, ...state } = reduceSession(error.events);
|
|
712
|
+
prefix = {
|
|
713
|
+
sessionId,
|
|
714
|
+
...state,
|
|
715
|
+
prompt: displayText(state.prompt, clean),
|
|
716
|
+
pendingCalls: state.pendingCalls.map((call) => ({
|
|
717
|
+
id: displayText(call.id, clean, 100),
|
|
718
|
+
name: displayText(call.name, clean, 100),
|
|
719
|
+
input: preview(call.input, clean),
|
|
720
|
+
state: call.state
|
|
721
|
+
})),
|
|
722
|
+
diagnostics: [],
|
|
723
|
+
status: "invalid"
|
|
724
|
+
};
|
|
725
|
+
} catch {}
|
|
726
|
+
return {
|
|
727
|
+
...prefix,
|
|
728
|
+
diagnostics: [displayText(error.message, clean)]
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
var Session = class Session {
|
|
733
|
+
root;
|
|
734
|
+
id;
|
|
735
|
+
directory;
|
|
736
|
+
sequence = 0;
|
|
737
|
+
lock;
|
|
738
|
+
opening = false;
|
|
739
|
+
ready = false;
|
|
740
|
+
writes = Promise.resolve();
|
|
741
|
+
closing;
|
|
742
|
+
redact = redactor();
|
|
743
|
+
events = [];
|
|
744
|
+
constructor(root, id = randomUUID(), directory = path.join(stateRoot(), "sessions", hash(root))) {
|
|
745
|
+
this.root = root;
|
|
746
|
+
this.id = id;
|
|
747
|
+
this.directory = directory;
|
|
748
|
+
if (!/^[\w-]{1,100}$/.test(id)) throw new Error("Invalid session ID");
|
|
749
|
+
}
|
|
750
|
+
get filename() {
|
|
751
|
+
return path.join(this.directory, `${this.id}.jsonl`);
|
|
752
|
+
}
|
|
753
|
+
get messages() {
|
|
754
|
+
return reduceSession(this.events).messages;
|
|
755
|
+
}
|
|
756
|
+
async open(resume = false) {
|
|
757
|
+
if (this.opening || this.lock || this.closing) throw new Error("Session is already open or changing ownership.");
|
|
758
|
+
this.opening = true;
|
|
759
|
+
try {
|
|
760
|
+
await mkdir(this.directory, {
|
|
761
|
+
recursive: true,
|
|
762
|
+
mode: 448
|
|
763
|
+
});
|
|
764
|
+
this.lock = await acquireSessionLock(this.filename);
|
|
765
|
+
if (resume) {
|
|
766
|
+
const journal = await readSessionJournal(this.filename, this.id);
|
|
767
|
+
reduceSession(journal.events);
|
|
768
|
+
if (journal.incompleteTail) await truncate(this.filename, journal.completeBytes);
|
|
769
|
+
else if (journal.needsSeparator) {
|
|
770
|
+
const separator = await open(this.filename, "a", 384);
|
|
771
|
+
try {
|
|
772
|
+
await separator.writeFile("\n");
|
|
773
|
+
await separator.sync();
|
|
774
|
+
} finally {
|
|
775
|
+
await separator.close();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
this.sequence = journal.events.at(-1)?.sequence ?? 0;
|
|
779
|
+
this.events.length = 0;
|
|
780
|
+
for (const event of journal.events) this.events.push(event);
|
|
781
|
+
} else {
|
|
782
|
+
await (await open(this.filename, "wx", 384)).close();
|
|
783
|
+
this.sequence = 0;
|
|
784
|
+
this.events.length = 0;
|
|
785
|
+
}
|
|
786
|
+
this.writes = Promise.resolve();
|
|
787
|
+
this.ready = true;
|
|
788
|
+
} catch (error) {
|
|
789
|
+
const lock = this.lock;
|
|
790
|
+
this.lock = void 0;
|
|
791
|
+
await lock?.release();
|
|
792
|
+
throw error;
|
|
793
|
+
} finally {
|
|
794
|
+
this.opening = false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async append(type, data) {
|
|
798
|
+
const lock = this.lock;
|
|
799
|
+
if (!this.ready || !lock) throw new Error("Session must be open before appending events.");
|
|
800
|
+
const safe = redactValue(data, this.redact);
|
|
801
|
+
const write = this.writes.then(async () => {
|
|
802
|
+
await lock.assertOwned();
|
|
803
|
+
const event = {
|
|
804
|
+
version: 1,
|
|
805
|
+
sessionId: this.id,
|
|
806
|
+
sequence: this.sequence + 1,
|
|
807
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
808
|
+
type,
|
|
809
|
+
data: safe
|
|
810
|
+
};
|
|
811
|
+
const fd = await open(this.filename, "a", 384);
|
|
812
|
+
try {
|
|
813
|
+
await fd.writeFile(`${JSON.stringify(event)}\n`);
|
|
814
|
+
await fd.sync();
|
|
815
|
+
} finally {
|
|
816
|
+
await fd.close();
|
|
817
|
+
}
|
|
818
|
+
this.sequence = event.sequence;
|
|
819
|
+
this.events.push(event);
|
|
820
|
+
return event;
|
|
821
|
+
});
|
|
822
|
+
this.writes = write.then(() => {});
|
|
823
|
+
this.writes.catch(() => {});
|
|
824
|
+
return write;
|
|
825
|
+
}
|
|
826
|
+
unresolved() {
|
|
827
|
+
return this.recovery();
|
|
828
|
+
}
|
|
829
|
+
recovery() {
|
|
830
|
+
return reduceSession(this.events).pendingCalls;
|
|
831
|
+
}
|
|
832
|
+
close() {
|
|
833
|
+
if (this.closing) return this.closing;
|
|
834
|
+
if (this.opening) return Promise.reject(/* @__PURE__ */ new Error("Wait for the session to finish opening before closing it."));
|
|
835
|
+
const lock = this.lock;
|
|
836
|
+
if (!lock) return Promise.resolve();
|
|
837
|
+
this.ready = false;
|
|
838
|
+
this.lock = void 0;
|
|
839
|
+
this.closing = (async () => {
|
|
840
|
+
try {
|
|
841
|
+
await this.writes;
|
|
842
|
+
} finally {
|
|
843
|
+
await lock.release();
|
|
844
|
+
}
|
|
845
|
+
})().finally(() => {
|
|
846
|
+
this.closing = void 0;
|
|
847
|
+
});
|
|
848
|
+
return this.closing;
|
|
849
|
+
}
|
|
850
|
+
static async list(root) {
|
|
851
|
+
try {
|
|
852
|
+
return (await readdir(path.join(stateRoot(), "sessions", hash(root)))).filter((n) => n.endsWith(".jsonl")).map((n) => n.slice(0, -6));
|
|
853
|
+
} catch {
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
static async inspect(root, id) {
|
|
858
|
+
return inspectSession(new Session(root, id).filename, id);
|
|
859
|
+
}
|
|
860
|
+
static async listSummaries(root) {
|
|
861
|
+
const ids = await Session.list(root);
|
|
862
|
+
return (await Promise.all(ids.map(async (id) => {
|
|
863
|
+
try {
|
|
864
|
+
return await Session.inspect(root, id);
|
|
865
|
+
} catch {
|
|
866
|
+
return {
|
|
867
|
+
sessionId: id,
|
|
868
|
+
updatedAt: null,
|
|
869
|
+
prompt: "",
|
|
870
|
+
status: "invalid",
|
|
871
|
+
steps: 0,
|
|
872
|
+
usage: {
|
|
873
|
+
inputTokens: 0,
|
|
874
|
+
outputTokens: 0
|
|
875
|
+
},
|
|
876
|
+
pendingCalls: [],
|
|
877
|
+
diagnostics: ["Could not read this session journal."]
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}))).sort((a, b) => (b.updatedAt ? Date.parse(b.updatedAt) : -Infinity) - (a.updatedAt ? Date.parse(a.updatedAt) : -Infinity) || a.sessionId.localeCompare(b.sessionId));
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const FALLBACK_MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
884
|
+
"edit_file",
|
|
885
|
+
"create_file",
|
|
886
|
+
"shell"
|
|
887
|
+
]);
|
|
888
|
+
function mutatesProject(name, mutates) {
|
|
889
|
+
return name !== "verify_project" && (mutates === true || FALLBACK_MUTATING_TOOLS.has(name));
|
|
890
|
+
}
|
|
891
|
+
async function runAgent(options) {
|
|
892
|
+
const session = new Session(options.root, options.sessionId);
|
|
893
|
+
await session.open(Boolean(options.sessionId));
|
|
894
|
+
const clean = redactor();
|
|
895
|
+
const emit = async (type, data) => {
|
|
896
|
+
const event = await session.append(type, data);
|
|
897
|
+
await options.onEvent?.(event);
|
|
898
|
+
};
|
|
899
|
+
let needsVerification = false;
|
|
900
|
+
for (const event of session.events) {
|
|
901
|
+
if (event.type === "tool.started" && mutatesProject(String(event.data.name), event.data.mutates)) needsVerification = true;
|
|
902
|
+
if (event.type === "tool.completed" && event.data.name === "verify_project" && !event.data.error) needsVerification = false;
|
|
903
|
+
}
|
|
904
|
+
const signal = AbortSignal.any([...options.signal ? [options.signal] : [], AbortSignal.timeout(options.config.timeoutMs)]);
|
|
905
|
+
const ctx = {
|
|
906
|
+
root: options.root,
|
|
907
|
+
signal,
|
|
908
|
+
trusted: options.trusted ?? false,
|
|
909
|
+
approve: options.approve ?? (async () => false)
|
|
910
|
+
};
|
|
911
|
+
const finish = async (status, text, reason) => {
|
|
912
|
+
const result = {
|
|
913
|
+
sessionId: session.id,
|
|
914
|
+
status,
|
|
915
|
+
text: clean(text),
|
|
916
|
+
...reason ? { reason } : {}
|
|
917
|
+
};
|
|
918
|
+
await emit("run.completed", {
|
|
919
|
+
status,
|
|
920
|
+
text: result.text,
|
|
921
|
+
...reason ? { reason } : {}
|
|
922
|
+
});
|
|
923
|
+
return result;
|
|
924
|
+
};
|
|
925
|
+
try {
|
|
926
|
+
await emit("run.started", {
|
|
927
|
+
projectId: hash(options.root),
|
|
928
|
+
model: options.model.id,
|
|
929
|
+
resumed: Boolean(options.sessionId)
|
|
930
|
+
});
|
|
931
|
+
await emit("message", { message: {
|
|
932
|
+
role: "user",
|
|
933
|
+
origin: "user",
|
|
934
|
+
text: options.prompt,
|
|
935
|
+
images: options.images
|
|
936
|
+
} });
|
|
937
|
+
const unresolved = session.recovery();
|
|
938
|
+
if (unresolved.length && !options.acknowledgeInterrupted) {
|
|
939
|
+
await emit("recovery.required", { calls: unresolved });
|
|
940
|
+
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.");
|
|
941
|
+
}
|
|
942
|
+
for (const call of unresolved) await emit("message", { message: {
|
|
943
|
+
role: "tool",
|
|
944
|
+
name: call.name,
|
|
945
|
+
callId: call.id,
|
|
946
|
+
error: true,
|
|
947
|
+
result: { text: call.state === "not_executed" ? "Not executed: the session ended before this call started. User acknowledged the interruption. Inspect current state and decide which remaining actions are needed." : "Execution was interrupted. User acknowledged inspection; outcome unknown. Do not repeat this action without first checking current state." }
|
|
948
|
+
} });
|
|
949
|
+
const registry = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
950
|
+
if (registry.size !== options.tools.length) throw new Error("Duplicate tool names");
|
|
951
|
+
const schemas = options.tools.map((tool) => ({
|
|
952
|
+
name: tool.name,
|
|
953
|
+
description: tool.description,
|
|
954
|
+
schema: z.toJSONSchema(tool.schema)
|
|
955
|
+
}));
|
|
956
|
+
for (let step = 0; step < options.config.maxSteps; step++) {
|
|
957
|
+
signal.throwIfAborted();
|
|
958
|
+
const compact = compactMessages(session.messages, options.config.contextCharacters);
|
|
959
|
+
if (compact.budgetExceeded) return await finish("limit_reached", `User instructions and images require ${compact.requiredCharacters} estimated context characters, exceeding contextCharacters (${options.config.contextCharacters}). No user instructions were truncated. Increase contextCharacters in weapp-agent.config.json, then resume this session.`, "context_budget");
|
|
960
|
+
await emit("step.started", { step: step + 1 });
|
|
961
|
+
if (compact.compacted) await emit("context.compacted", {
|
|
962
|
+
before: session.messages.length,
|
|
963
|
+
after: compact.messages.length
|
|
964
|
+
});
|
|
965
|
+
let text = "";
|
|
966
|
+
let buffer = "";
|
|
967
|
+
const calls = [];
|
|
968
|
+
const flush = async (final = false) => {
|
|
969
|
+
const cut = final ? buffer.length : Math.max(buffer.lastIndexOf("\n"), buffer.lastIndexOf(" ")) + 1;
|
|
970
|
+
if (cut > 0) {
|
|
971
|
+
await emit("text.delta", { text: clean(buffer.slice(0, cut)) });
|
|
972
|
+
buffer = buffer.slice(cut);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
for await (const part of options.model.stream({
|
|
976
|
+
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 ?? ""}`,
|
|
977
|
+
messages: compact.messages,
|
|
978
|
+
tools: schemas,
|
|
979
|
+
signal
|
|
980
|
+
})) {
|
|
981
|
+
signal.throwIfAborted();
|
|
982
|
+
if (part.type === "text") {
|
|
983
|
+
text += part.text;
|
|
984
|
+
buffer += part.text;
|
|
985
|
+
await flush();
|
|
986
|
+
}
|
|
987
|
+
if (part.type === "call") {
|
|
988
|
+
if (calls.some((call) => call.id === part.call.id)) throw new Error("Provider returned a duplicate tool call ID");
|
|
989
|
+
calls.push(part.call);
|
|
990
|
+
}
|
|
991
|
+
if (part.type === "usage") await emit("usage", {
|
|
992
|
+
inputTokens: part.inputTokens,
|
|
993
|
+
outputTokens: part.outputTokens
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
await flush(true);
|
|
997
|
+
await emit("message", { message: {
|
|
998
|
+
role: "assistant",
|
|
999
|
+
text,
|
|
1000
|
+
calls
|
|
1001
|
+
} });
|
|
1002
|
+
if (!calls.length) {
|
|
1003
|
+
if (needsVerification && registry.has("verify_project")) {
|
|
1004
|
+
await emit("message", { message: {
|
|
1005
|
+
role: "user",
|
|
1006
|
+
origin: "engine",
|
|
1007
|
+
text: "Files changed since the last verification. Call verify_project before finishing; report failed and unverified categories honestly."
|
|
1008
|
+
} });
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
return await finish("completed", text);
|
|
1012
|
+
}
|
|
1013
|
+
for (const call of calls) {
|
|
1014
|
+
signal.throwIfAborted();
|
|
1015
|
+
const tool = registry.get(call.name);
|
|
1016
|
+
let result;
|
|
1017
|
+
let error = false;
|
|
1018
|
+
let approval = false;
|
|
1019
|
+
try {
|
|
1020
|
+
if (!tool) throw new Error(`Unknown tool: ${call.name}`);
|
|
1021
|
+
const args = tool.schema.parse(call.input);
|
|
1022
|
+
await emit("tool.started", {
|
|
1023
|
+
callId: call.id,
|
|
1024
|
+
name: call.name,
|
|
1025
|
+
input: args,
|
|
1026
|
+
mutates: tool.mutates
|
|
1027
|
+
});
|
|
1028
|
+
if (mutatesProject(call.name, tool.mutates)) needsVerification = true;
|
|
1029
|
+
result = await tool.execute(args, ctx);
|
|
1030
|
+
if (call.name === "verify_project") needsVerification = false;
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
if (signal.aborted) throw err;
|
|
1033
|
+
error = true;
|
|
1034
|
+
approval = err instanceof ApprovalRequired;
|
|
1035
|
+
result = { text: clean(err instanceof Error ? err.message : String(err)) };
|
|
1036
|
+
}
|
|
1037
|
+
await emit("message", { message: {
|
|
1038
|
+
role: "tool",
|
|
1039
|
+
callId: call.id,
|
|
1040
|
+
name: call.name,
|
|
1041
|
+
result,
|
|
1042
|
+
error
|
|
1043
|
+
} });
|
|
1044
|
+
await emit("tool.completed", {
|
|
1045
|
+
callId: call.id,
|
|
1046
|
+
name: call.name,
|
|
1047
|
+
result,
|
|
1048
|
+
error
|
|
1049
|
+
});
|
|
1050
|
+
if (approval) {
|
|
1051
|
+
for (const skipped of calls.slice(calls.indexOf(call) + 1)) await emit("message", { message: {
|
|
1052
|
+
role: "tool",
|
|
1053
|
+
callId: skipped.id,
|
|
1054
|
+
name: skipped.name,
|
|
1055
|
+
error: true,
|
|
1056
|
+
result: { text: "Not executed: an earlier call needs approval." }
|
|
1057
|
+
} });
|
|
1058
|
+
return await finish("action_required", result.text);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return await finish("limit_reached", `Stopped after ${options.config.maxSteps} model steps. Resume the session to continue.`, "max_steps");
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
return await finish(signal.aborted ? "cancelled" : "failed", error instanceof Error ? error.message : String(error));
|
|
1065
|
+
} finally {
|
|
1066
|
+
await session.close();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async function walk(root, directory = ".", result = []) {
|
|
1070
|
+
if (result.length >= 2e3) return result;
|
|
1071
|
+
for (const item of await readdir(await safePath(root, directory), { withFileTypes: true })) {
|
|
1072
|
+
const relative = path.join(directory, item.name);
|
|
1073
|
+
if (sensitive(relative) || [
|
|
1074
|
+
"node_modules",
|
|
1075
|
+
"dist",
|
|
1076
|
+
"coverage",
|
|
1077
|
+
".turbo",
|
|
1078
|
+
".astro",
|
|
1079
|
+
".wrangler"
|
|
1080
|
+
].includes(item.name) || item.isSymbolicLink()) continue;
|
|
1081
|
+
if (item.isDirectory()) await walk(root, relative, result);
|
|
1082
|
+
else if (item.isFile()) result.push(relative);
|
|
1083
|
+
if (result.length >= 2e3) break;
|
|
1084
|
+
}
|
|
1085
|
+
return result;
|
|
1086
|
+
}
|
|
1087
|
+
function fileTools() {
|
|
1088
|
+
const readSchema = z.strictObject({
|
|
1089
|
+
path: z.string(),
|
|
1090
|
+
startLine: z.number().int().positive().default(1),
|
|
1091
|
+
endLine: z.number().int().positive().optional()
|
|
1092
|
+
});
|
|
1093
|
+
const listSchema = z.strictObject({ directory: z.string().default(".") });
|
|
1094
|
+
const searchSchema = z.strictObject({
|
|
1095
|
+
query: z.string().min(1),
|
|
1096
|
+
directory: z.string().default(".")
|
|
1097
|
+
});
|
|
1098
|
+
const createSchema = z.strictObject({
|
|
1099
|
+
path: z.string(),
|
|
1100
|
+
content: z.string().max(5e5)
|
|
1101
|
+
});
|
|
1102
|
+
const editSchema = z.strictObject({
|
|
1103
|
+
path: z.string(),
|
|
1104
|
+
expectedHash: z.string(),
|
|
1105
|
+
oldText: z.string().min(1),
|
|
1106
|
+
newText: z.string()
|
|
1107
|
+
});
|
|
1108
|
+
const shellSchema = z.strictObject({
|
|
1109
|
+
command: z.string().min(1),
|
|
1110
|
+
timeoutMs: z.number().int().positive().max(6e5).default(12e4)
|
|
1111
|
+
});
|
|
1112
|
+
return [
|
|
1113
|
+
{
|
|
1114
|
+
name: "list_files",
|
|
1115
|
+
description: "List up to 2000 project files, excluding generated files, secrets, and symlinks.",
|
|
1116
|
+
schema: listSchema,
|
|
1117
|
+
mutates: false,
|
|
1118
|
+
async execute(input, ctx) {
|
|
1119
|
+
const a = listSchema.parse(input);
|
|
1120
|
+
return { text: (await walk(ctx.root, a.directory)).join("\n") };
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: "read_file",
|
|
1125
|
+
description: "Read numbered lines and the SHA-256 of the full file. Use that hash for edit_file.",
|
|
1126
|
+
schema: readSchema,
|
|
1127
|
+
mutates: false,
|
|
1128
|
+
async execute(input, ctx) {
|
|
1129
|
+
const a = readSchema.parse(input);
|
|
1130
|
+
const text = await readFile(await safePath(ctx.root, a.path), "utf8");
|
|
1131
|
+
const lines = text.split("\n");
|
|
1132
|
+
return {
|
|
1133
|
+
text: bounded(lines.slice(a.startLine - 1, a.endLine ?? a.startLine + 299).map((line, i) => `${i + a.startLine}: ${line}`).join("\n")),
|
|
1134
|
+
data: {
|
|
1135
|
+
path: a.path,
|
|
1136
|
+
hash: hash(text),
|
|
1137
|
+
lines: lines.length
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
name: "search_files",
|
|
1144
|
+
description: "Literal text search in source files with line numbers. No regex or shell evaluation.",
|
|
1145
|
+
schema: searchSchema,
|
|
1146
|
+
mutates: false,
|
|
1147
|
+
async execute(input, ctx) {
|
|
1148
|
+
const a = searchSchema.parse(input);
|
|
1149
|
+
const matches = [];
|
|
1150
|
+
for (const f of await walk(ctx.root, a.directory)) {
|
|
1151
|
+
const text = await readFile(await safePath(ctx.root, f), "utf8").catch(() => "");
|
|
1152
|
+
if (text.includes("\0") || text.length > 5e5) continue;
|
|
1153
|
+
text.split("\n").forEach((line, i) => {
|
|
1154
|
+
if (line.includes(a.query) && matches.length < 200) matches.push(`${f}:${i + 1}: ${line}`);
|
|
1155
|
+
});
|
|
1156
|
+
if (matches.length >= 200) break;
|
|
1157
|
+
}
|
|
1158
|
+
return { text: bounded(matches.join("\n")) };
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
name: "create_file",
|
|
1163
|
+
description: "Create a new file without overwriting any existing file.",
|
|
1164
|
+
schema: createSchema,
|
|
1165
|
+
mutates: true,
|
|
1166
|
+
async execute(input, ctx) {
|
|
1167
|
+
await requireTrust(ctx);
|
|
1168
|
+
const a = createSchema.parse(input);
|
|
1169
|
+
const f = await safePath(ctx.root, a.path);
|
|
1170
|
+
await mkdir(path.dirname(f), { recursive: true });
|
|
1171
|
+
await safePath(ctx.root, a.path);
|
|
1172
|
+
await writeFile(f, a.content, { flag: "wx" });
|
|
1173
|
+
return {
|
|
1174
|
+
text: createTwoFilesPatch("/dev/null", a.path, "", a.content),
|
|
1175
|
+
data: {
|
|
1176
|
+
hash: hash(a.content),
|
|
1177
|
+
path: a.path
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
name: "edit_file",
|
|
1184
|
+
description: "Replace exactly one occurrence of oldText, only if the file still matches expectedHash. Re-read on conflicts.",
|
|
1185
|
+
schema: editSchema,
|
|
1186
|
+
mutates: true,
|
|
1187
|
+
async execute(input, ctx) {
|
|
1188
|
+
await requireTrust(ctx);
|
|
1189
|
+
const a = editSchema.parse(input);
|
|
1190
|
+
const f = await safePath(ctx.root, a.path);
|
|
1191
|
+
const before = await readFile(f, "utf8");
|
|
1192
|
+
if (hash(before) !== a.expectedHash) throw new Error("File changed since it was read. Re-read and reconcile the user changes.");
|
|
1193
|
+
if (before.split(a.oldText).length !== 2) throw new Error("oldText must occur exactly once. Include more context.");
|
|
1194
|
+
const after = before.replace(a.oldText, () => a.newText);
|
|
1195
|
+
const temporary = `${f}.${randomUUID()}.tmp`;
|
|
1196
|
+
try {
|
|
1197
|
+
const { stat } = await import("node:fs/promises");
|
|
1198
|
+
await writeFile(temporary, after, {
|
|
1199
|
+
flag: "wx",
|
|
1200
|
+
mode: (await stat(f)).mode
|
|
1201
|
+
});
|
|
1202
|
+
await safePath(ctx.root, a.path);
|
|
1203
|
+
if (hash(await readFile(f, "utf8")) !== a.expectedHash) throw new Error("Concurrent file change detected; edit not applied.");
|
|
1204
|
+
await rename(temporary, f);
|
|
1205
|
+
} finally {
|
|
1206
|
+
await unlink(temporary).catch(() => {});
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
text: createTwoFilesPatch(a.path, a.path, before, after),
|
|
1210
|
+
data: {
|
|
1211
|
+
path: a.path,
|
|
1212
|
+
hash: hash(after)
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
name: "git_diff",
|
|
1219
|
+
description: "Show tracked changes and untracked file names, preserving the current Git state.",
|
|
1220
|
+
schema: z.strictObject({}),
|
|
1221
|
+
mutates: false,
|
|
1222
|
+
async execute(_input, ctx) {
|
|
1223
|
+
const filters = await execa("git", [
|
|
1224
|
+
"config",
|
|
1225
|
+
"--null",
|
|
1226
|
+
"--name-only",
|
|
1227
|
+
"--get-regexp",
|
|
1228
|
+
"^filter\\..*\\.(clean|smudge|process|required)$"
|
|
1229
|
+
], {
|
|
1230
|
+
cwd: ctx.root,
|
|
1231
|
+
cancelSignal: ctx.signal,
|
|
1232
|
+
reject: false
|
|
1233
|
+
});
|
|
1234
|
+
const gitConfig = ["-c", "core.fsmonitor=false"];
|
|
1235
|
+
for (const key of filters.stdout.split("\0").filter(Boolean)) gitConfig.push("-c", `${key}=${key.endsWith(".required") ? "false" : ""}`);
|
|
1236
|
+
const diff = await execa("git", [
|
|
1237
|
+
...gitConfig,
|
|
1238
|
+
"--no-pager",
|
|
1239
|
+
"diff",
|
|
1240
|
+
"--no-ext-diff",
|
|
1241
|
+
"--no-textconv",
|
|
1242
|
+
"--",
|
|
1243
|
+
".",
|
|
1244
|
+
":!.env*",
|
|
1245
|
+
":!*.pem",
|
|
1246
|
+
":!*.key",
|
|
1247
|
+
":!*.p12",
|
|
1248
|
+
":!.weapp-agent/**",
|
|
1249
|
+
":!.npmrc"
|
|
1250
|
+
], {
|
|
1251
|
+
cwd: ctx.root,
|
|
1252
|
+
cancelSignal: ctx.signal,
|
|
1253
|
+
reject: false
|
|
1254
|
+
});
|
|
1255
|
+
return { text: bounded(`${(await execa("git", [
|
|
1256
|
+
...gitConfig,
|
|
1257
|
+
"status",
|
|
1258
|
+
"--short"
|
|
1259
|
+
], {
|
|
1260
|
+
cwd: ctx.root,
|
|
1261
|
+
cancelSignal: ctx.signal,
|
|
1262
|
+
reject: false
|
|
1263
|
+
})).stdout}\n${diff.stdout}`) };
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "shell",
|
|
1268
|
+
description: "Execute an arbitrary shell command. Always requires exact-command approval; never use it to bypass a denied operation.",
|
|
1269
|
+
schema: shellSchema,
|
|
1270
|
+
mutates: true,
|
|
1271
|
+
async execute(input, ctx) {
|
|
1272
|
+
const a = shellSchema.parse(input);
|
|
1273
|
+
await authorize(ctx, "command", a.command, {
|
|
1274
|
+
root: ctx.root,
|
|
1275
|
+
...a
|
|
1276
|
+
});
|
|
1277
|
+
const result = await execa(a.command, {
|
|
1278
|
+
cwd: ctx.root,
|
|
1279
|
+
shell: true,
|
|
1280
|
+
cancelSignal: ctx.signal,
|
|
1281
|
+
timeout: a.timeoutMs,
|
|
1282
|
+
forceKillAfterDelay: 2e3,
|
|
1283
|
+
reject: false,
|
|
1284
|
+
maxBuffer: 2e6
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
text: bounded(`${result.stdout}\n${result.stderr}`),
|
|
1288
|
+
data: {
|
|
1289
|
+
exitCode: result.exitCode,
|
|
1290
|
+
timedOut: result.timedOut
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
];
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region ../agent-mini-program/dist/rolldown-runtime-BBjsoOtd.mjs
|
|
1299
|
+
var __defProp = Object.defineProperty;
|
|
1300
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1301
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
1302
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
1303
|
+
var __exportAll = (all, no_symbols) => {
|
|
1304
|
+
let target = {};
|
|
1305
|
+
for (var name in all) __defProp(target, name, {
|
|
1306
|
+
get: all[name],
|
|
1307
|
+
enumerable: true
|
|
1308
|
+
});
|
|
1309
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
1310
|
+
return target;
|
|
1311
|
+
};
|
|
1312
|
+
var __copyProps = (to, from, except, desc) => {
|
|
1313
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
1314
|
+
key = keys[i];
|
|
1315
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
1316
|
+
get: ((k) => from[k]).bind(null, key),
|
|
1317
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
return to;
|
|
1321
|
+
};
|
|
1322
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region ../agent-mini-program/dist/index.mjs
|
|
1325
|
+
var dist_exports = /* @__PURE__ */ __exportAll$1({
|
|
1326
|
+
AcceptanceService: () => AcceptanceService,
|
|
1327
|
+
WeappProjectAdapter: () => WeappProjectAdapter,
|
|
1328
|
+
builtinMcp: () => builtinMcp,
|
|
1329
|
+
connectMcp: () => connectMcp,
|
|
1330
|
+
createAcceptanceMcpServer: () => createAcceptanceMcpServer,
|
|
1331
|
+
serveAcceptanceMcp: () => serveAcceptanceMcp
|
|
1332
|
+
});
|
|
1333
|
+
import * as import__weapp_vite_acceptance from "@weapp-vite/acceptance";
|
|
1334
|
+
__reExport$1(dist_exports, import__weapp_vite_acceptance);
|
|
1335
|
+
var project_exports = /* @__PURE__ */ __exportAll({});
|
|
1336
|
+
__reExport(project_exports, import__weapp_vite_acceptance$3);
|
|
1337
|
+
const AcceptanceService = { create: (root, options) => createRuntimeAcceptanceService(root, options, {
|
|
1338
|
+
connectMiniProgram,
|
|
1339
|
+
prepareProject: prepareAcceptanceProject
|
|
1340
|
+
}) };
|
|
1341
|
+
const projectRuntime = /* @__PURE__ */ new Set([
|
|
1342
|
+
"weapp_devtools_connect",
|
|
1343
|
+
"weapp_devtools_active_page",
|
|
1344
|
+
"weapp_devtools_page_stack",
|
|
1345
|
+
"weapp_devtools_route",
|
|
1346
|
+
"weapp_devtools_capture",
|
|
1347
|
+
"weapp_devtools_console",
|
|
1348
|
+
"weapp_runtime_find_node",
|
|
1349
|
+
"weapp_runtime_find_nodes",
|
|
1350
|
+
"weapp_runtime_wait_node",
|
|
1351
|
+
"weapp_runtime_find_node_by_xpath",
|
|
1352
|
+
"weapp_runtime_find_nodes_by_xpath",
|
|
1353
|
+
"weapp_runtime_page_state",
|
|
1354
|
+
"weapp_runtime_tap_node",
|
|
1355
|
+
"weapp_runtime_input_node",
|
|
1356
|
+
"weapp_runtime_component_state",
|
|
1357
|
+
"weapp_runtime_find_child",
|
|
1358
|
+
"weapp_runtime_find_children",
|
|
1359
|
+
"weapp_runtime_node_markup",
|
|
1360
|
+
"weapp_runtime_node_styles",
|
|
1361
|
+
"weapp_runtime_node_attrs",
|
|
1362
|
+
"weapp_runtime_measure_node"
|
|
1363
|
+
]);
|
|
1364
|
+
async function connectMcp(server, context, options) {
|
|
1365
|
+
const unchanged = options && await projectFingerprint(context.root, options.config) === options.fingerprint;
|
|
1366
|
+
if (!context.trusted || !unchanged) await authorize(context, "mcp", `Connect MCP ${server.name}`, server);
|
|
1367
|
+
const client = new Client({
|
|
1368
|
+
name: "weapp-agent",
|
|
1369
|
+
version: "0.1.0-preview.1"
|
|
1370
|
+
}, { capabilities: {} });
|
|
1371
|
+
let transport;
|
|
1372
|
+
if (server.transport === "stdio") transport = new StdioClientTransport({
|
|
1373
|
+
command: server.command,
|
|
1374
|
+
args: server.args,
|
|
1375
|
+
cwd: context.root,
|
|
1376
|
+
stderr: "pipe"
|
|
1377
|
+
});
|
|
1378
|
+
else {
|
|
1379
|
+
const url = new URL(server.url);
|
|
1380
|
+
if (url.protocol !== "https:" && ![
|
|
1381
|
+
"localhost",
|
|
1382
|
+
"127.0.0.1",
|
|
1383
|
+
"[::1]"
|
|
1384
|
+
].includes(url.hostname)) throw new Error("Remote MCP requires HTTPS");
|
|
1385
|
+
const token = server.tokenEnv ? process.env[server.tokenEnv] : void 0;
|
|
1386
|
+
if (server.tokenEnv && !token) throw new Error(`Set ${server.tokenEnv} for MCP authentication`);
|
|
1387
|
+
transport = new StreamableHTTPClientTransport(url, { requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : {} });
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
await client.connect(transport, { timeout: 15e3 });
|
|
1391
|
+
const tools = [];
|
|
1392
|
+
let cursor;
|
|
1393
|
+
do {
|
|
1394
|
+
const page = await client.listTools(cursor ? { cursor } : {}, {
|
|
1395
|
+
signal: context.signal,
|
|
1396
|
+
timeout: 15e3
|
|
1397
|
+
});
|
|
1398
|
+
for (const definition of page.tools) {
|
|
1399
|
+
const schema = z.fromJSONSchema(definition.inputSchema);
|
|
1400
|
+
const name = `${server.name}__${definition.name}`;
|
|
1401
|
+
if (name.length > 64) throw new Error(`MCP tool name too long: ${name}`);
|
|
1402
|
+
tools.push({
|
|
1403
|
+
name,
|
|
1404
|
+
description: definition.description ?? definition.name,
|
|
1405
|
+
schema,
|
|
1406
|
+
mutates: true,
|
|
1407
|
+
async execute(input, ctx) {
|
|
1408
|
+
const args = schema.parse(input);
|
|
1409
|
+
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;
|
|
1410
|
+
if (!trustedRuntime) await authorize(ctx, /upload|publish|deploy/.test(definition.name) ? "publish" : "mcp", `${name}: ${JSON.stringify(args)}`, {
|
|
1411
|
+
server,
|
|
1412
|
+
tool: definition.name,
|
|
1413
|
+
args
|
|
1414
|
+
});
|
|
1415
|
+
if (trustedRuntime) {
|
|
1416
|
+
for (const key of ["projectPath", "outputPath"]) if (typeof args[key] === "string") await safePath(ctx.root, args[key]);
|
|
1417
|
+
}
|
|
1418
|
+
const response = await client.callTool({
|
|
1419
|
+
name: definition.name,
|
|
1420
|
+
arguments: args
|
|
1421
|
+
}, {
|
|
1422
|
+
signal: ctx.signal,
|
|
1423
|
+
timeout: 12e4
|
|
1424
|
+
});
|
|
1425
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
1426
|
+
const text = bounded(content.filter((c) => c.type === "text").map((c) => c.text).join("\n"));
|
|
1427
|
+
if (response.isError) throw new Error(text || `MCP tool failed: ${name}`);
|
|
1428
|
+
return {
|
|
1429
|
+
text,
|
|
1430
|
+
images: content.filter((c) => c.type === "image" && c.data && c.mimeType).map((c) => ({
|
|
1431
|
+
type: "image",
|
|
1432
|
+
data: c.data,
|
|
1433
|
+
mediaType: c.mimeType
|
|
1434
|
+
})),
|
|
1435
|
+
data: response.structuredContent
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
cursor = page.nextCursor;
|
|
1441
|
+
} while (cursor);
|
|
1442
|
+
return {
|
|
1443
|
+
tools,
|
|
1444
|
+
close: () => client.close()
|
|
1445
|
+
};
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
await client.close().catch(() => {});
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
async function builtinMcp(root) {
|
|
1452
|
+
const bin = path.join(root, "node_modules/weapp-vite/bin/weapp-vite.js");
|
|
1453
|
+
if (!await (0, project_exports.exists)(bin)) return;
|
|
1454
|
+
return {
|
|
1455
|
+
name: "weapp",
|
|
1456
|
+
transport: "stdio",
|
|
1457
|
+
command: process.execPath,
|
|
1458
|
+
args: [
|
|
1459
|
+
bin,
|
|
1460
|
+
"mcp",
|
|
1461
|
+
"--workspace-root",
|
|
1462
|
+
root
|
|
1463
|
+
]
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function createAcceptanceMcpServer(service) {
|
|
1467
|
+
const server = new McpServer({
|
|
1468
|
+
name: "weapp-agent",
|
|
1469
|
+
version: "0.1.0-preview.1"
|
|
1470
|
+
});
|
|
1471
|
+
registerAcceptanceTools(server, async () => service);
|
|
1472
|
+
return server;
|
|
1473
|
+
}
|
|
1474
|
+
async function serveAcceptanceMcp(service) {
|
|
1475
|
+
const server = createAcceptanceMcpServer(service);
|
|
1476
|
+
const handle = serveStdio(() => server);
|
|
1477
|
+
const close = async () => {
|
|
1478
|
+
await service.close();
|
|
1479
|
+
await handle.close();
|
|
1480
|
+
};
|
|
1481
|
+
process.stdin.once("end", () => {
|
|
1482
|
+
close();
|
|
1483
|
+
});
|
|
1484
|
+
return { close };
|
|
1485
|
+
}
|
|
1486
|
+
__reExport(/* @__PURE__ */ __exportAll({
|
|
1487
|
+
AcceptanceService: () => AcceptanceService,
|
|
1488
|
+
WeappProjectAdapter: () => WeappProjectAdapter,
|
|
1489
|
+
builtinMcp: () => builtinMcp,
|
|
1490
|
+
connectMcp: () => connectMcp,
|
|
1491
|
+
createAcceptanceMcpServer: () => createAcceptanceMcpServer,
|
|
1492
|
+
serveAcceptanceMcp: () => serveAcceptanceMcp
|
|
1493
|
+
}), project_exports);
|
|
1494
|
+
var WeappProjectAdapter = class {
|
|
1495
|
+
detect = project_exports.detectProject;
|
|
1496
|
+
instructions = project_exports.projectInstructions;
|
|
1497
|
+
async tools(project) {
|
|
1498
|
+
return [{
|
|
1499
|
+
name: "project_info",
|
|
1500
|
+
description: "Inspect the current mini-program project structure, scripts and framework. Re-check after changing project configuration.",
|
|
1501
|
+
schema: z.strictObject({}),
|
|
1502
|
+
mutates: false,
|
|
1503
|
+
async execute() {
|
|
1504
|
+
return { text: JSON.stringify(await (0, project_exports.detectProject)(project.root), null, 2) };
|
|
1505
|
+
}
|
|
1506
|
+
}];
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
//#endregion
|
|
1510
|
+
//#region ../agent-providers/dist/index.mjs
|
|
1511
|
+
function toModelMessages(messages) {
|
|
1512
|
+
return messages.map((message) => {
|
|
1513
|
+
if (message.role === "user") return {
|
|
1514
|
+
role: "user",
|
|
1515
|
+
content: [{
|
|
1516
|
+
type: "text",
|
|
1517
|
+
text: message.text
|
|
1518
|
+
}, ...(message.images ?? []).map((image) => ({
|
|
1519
|
+
type: "image",
|
|
1520
|
+
image: image.data,
|
|
1521
|
+
mediaType: image.mediaType
|
|
1522
|
+
}))]
|
|
1523
|
+
};
|
|
1524
|
+
if (message.role === "assistant") return {
|
|
1525
|
+
role: "assistant",
|
|
1526
|
+
content: [...message.text ? [{
|
|
1527
|
+
type: "text",
|
|
1528
|
+
text: message.text
|
|
1529
|
+
}] : [], ...(message.calls ?? []).map((call) => ({
|
|
1530
|
+
type: "tool-call",
|
|
1531
|
+
toolCallId: call.id,
|
|
1532
|
+
toolName: call.name,
|
|
1533
|
+
input: call.input
|
|
1534
|
+
}))]
|
|
1535
|
+
};
|
|
1536
|
+
const text = message.result.data === void 0 ? message.result.text : `${message.result.text}\n${JSON.stringify(message.result.data)}`;
|
|
1537
|
+
return {
|
|
1538
|
+
role: "tool",
|
|
1539
|
+
content: [{
|
|
1540
|
+
type: "tool-result",
|
|
1541
|
+
toolCallId: message.callId,
|
|
1542
|
+
toolName: message.name,
|
|
1543
|
+
output: message.result.images?.length ? {
|
|
1544
|
+
type: "content",
|
|
1545
|
+
value: [{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text
|
|
1548
|
+
}, ...message.result.images.map((image) => ({
|
|
1549
|
+
type: "file",
|
|
1550
|
+
data: {
|
|
1551
|
+
type: "data",
|
|
1552
|
+
data: image.data
|
|
1553
|
+
},
|
|
1554
|
+
mediaType: image.mediaType
|
|
1555
|
+
}))]
|
|
1556
|
+
} : {
|
|
1557
|
+
type: message.error ? "error-text" : "text",
|
|
1558
|
+
value: text
|
|
1559
|
+
}
|
|
1560
|
+
}]
|
|
1561
|
+
};
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
var AiSdkAdapter = class {
|
|
1565
|
+
id;
|
|
1566
|
+
model;
|
|
1567
|
+
constructor(id, model) {
|
|
1568
|
+
this.id = id;
|
|
1569
|
+
this.model = model;
|
|
1570
|
+
}
|
|
1571
|
+
async *stream(request) {
|
|
1572
|
+
const tools = Object.fromEntries(request.tools.map((t) => [t.name, tool({
|
|
1573
|
+
description: t.description,
|
|
1574
|
+
inputSchema: jsonSchema(t.schema)
|
|
1575
|
+
})]));
|
|
1576
|
+
const result = streamText({
|
|
1577
|
+
model: this.model,
|
|
1578
|
+
system: request.system,
|
|
1579
|
+
messages: toModelMessages(request.messages),
|
|
1580
|
+
tools,
|
|
1581
|
+
abortSignal: request.signal,
|
|
1582
|
+
maxRetries: 2,
|
|
1583
|
+
onError: () => {},
|
|
1584
|
+
maxOutputTokens: 8192
|
|
1585
|
+
});
|
|
1586
|
+
for await (const part of result.stream) {
|
|
1587
|
+
if (part.type === "text-delta") yield {
|
|
1588
|
+
type: "text",
|
|
1589
|
+
text: part.text
|
|
1590
|
+
};
|
|
1591
|
+
if (part.type === "tool-call") yield {
|
|
1592
|
+
type: "call",
|
|
1593
|
+
call: {
|
|
1594
|
+
id: part.toolCallId,
|
|
1595
|
+
name: part.toolName,
|
|
1596
|
+
input: part.input
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
if (part.type === "finish") yield {
|
|
1600
|
+
type: "usage",
|
|
1601
|
+
inputTokens: part.totalUsage.inputTokens ?? 0,
|
|
1602
|
+
outputTokens: part.totalUsage.outputTokens ?? 0
|
|
1603
|
+
};
|
|
1604
|
+
if (part.type === "error") throw part.error;
|
|
1605
|
+
if (part.type === "abort") throw new Error("Model request cancelled");
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
function apiKeyVariable(config) {
|
|
1610
|
+
return config.apiKeyEnv ?? (config.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
|
|
1611
|
+
}
|
|
1612
|
+
function createModel(config, environment = process.env) {
|
|
1613
|
+
const variable = apiKeyVariable(config);
|
|
1614
|
+
const apiKey = environment[variable];
|
|
1615
|
+
if (!apiKey) throw new Error(`Set ${variable} in your environment. Credentials are never read from other agents or stored in project config.`);
|
|
1616
|
+
let model;
|
|
1617
|
+
if (config.provider === "anthropic") model = createAnthropic({
|
|
1618
|
+
apiKey,
|
|
1619
|
+
baseURL: config.baseURL
|
|
1620
|
+
})(config.name);
|
|
1621
|
+
else if (config.provider === "openai-compatible") {
|
|
1622
|
+
if (!config.baseURL) throw new Error("openai-compatible requires model.baseURL");
|
|
1623
|
+
model = createOpenAICompatible({
|
|
1624
|
+
name: "weapp-compatible",
|
|
1625
|
+
apiKey,
|
|
1626
|
+
baseURL: config.baseURL
|
|
1627
|
+
})(config.name);
|
|
1628
|
+
} else model = createOpenAI({
|
|
1629
|
+
apiKey,
|
|
1630
|
+
baseURL: config.baseURL
|
|
1631
|
+
}).responses(config.name);
|
|
1632
|
+
return new AiSdkAdapter(`${config.provider}/${config.name}`, model);
|
|
1633
|
+
}
|
|
1634
|
+
//#endregion
|
|
1635
|
+
//#region src/commands/session.ts
|
|
1636
|
+
function registerSessionCommands(program, output) {
|
|
1637
|
+
program.command("sessions").description("List sessions for this project").option("--details", "include prompts, status, usage and pending tool calls").action(async (_local, command) => {
|
|
1638
|
+
const options = command.optsWithGlobals();
|
|
1639
|
+
const root = await (0, dist_exports.findProjectRoot)(options.cwd);
|
|
1640
|
+
output(options.details ? await Session.listSummaries(root) : await Session.list(root), Boolean(options.json));
|
|
1641
|
+
});
|
|
1642
|
+
program.command("session").description("Inspect a saved session without starting a model or replaying tools").argument("<session>", "session ID").action(async (session, _local, command) => {
|
|
1643
|
+
const options = command.optsWithGlobals();
|
|
1644
|
+
const summary = await Session.inspect(await (0, dist_exports.findProjectRoot)(options.cwd), session);
|
|
1645
|
+
output(summary, Boolean(options.json));
|
|
1646
|
+
if (summary.status === "invalid") process.exitCode = 1;
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region src/ui/commands.ts
|
|
1651
|
+
async function runInteractiveTask(runner, prompt, sessionId, signal, onEvent, approve) {
|
|
1652
|
+
const input = prompt.trim();
|
|
1653
|
+
const acknowledgement = input.split(/\s/, 1)[0] === "/acknowledge-interrupted";
|
|
1654
|
+
if (acknowledgement && !sessionId) throw new Error("No session to recover. Start a task before acknowledging interrupted tool calls.");
|
|
1655
|
+
return runner(acknowledgement ? input.slice(24).trim() || "I inspected the interrupted tool outcomes. Continue after checking the current project state." : prompt, sessionId, signal, onEvent, approve, { acknowledgeInterrupted: Boolean(acknowledgement) });
|
|
1656
|
+
}
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/ui.tsx
|
|
1659
|
+
const cleanPreview = redactor();
|
|
1660
|
+
function safePreview(input, depth = 0) {
|
|
1661
|
+
if (depth > 2) return "[nested data omitted]";
|
|
1662
|
+
if (typeof input === "string") {
|
|
1663
|
+
const clean = cleanPreview(input).replace(/data:[^\s,]+,\S+/gi, "[image/data omitted]").replace(/[A-Z0-9+/]{80,}={0,2}/gi, "[binary data omitted]");
|
|
1664
|
+
return clean.length > 120 ? `${clean.slice(0, 120)}…` : clean;
|
|
1665
|
+
}
|
|
1666
|
+
if (input === null || typeof input !== "object") return input;
|
|
1667
|
+
if (Array.isArray(input)) return input.slice(0, 4).map((item) => safePreview(item, depth + 1));
|
|
1668
|
+
return Object.fromEntries(Object.entries(input).slice(0, 8).map(([key, value]) => [key, /key|token|secret|password|authorization/i.test(key) ? "[REDACTED]" : /^(?:data|base64|images?|image_url)$/i.test(key) ? "[data omitted]" : safePreview(value, depth + 1)]));
|
|
1669
|
+
}
|
|
1670
|
+
function inputPreview(input) {
|
|
1671
|
+
if (input === void 0) return "";
|
|
1672
|
+
try {
|
|
1673
|
+
const serialized = JSON.stringify(safePreview(input));
|
|
1674
|
+
if (!serialized) return "";
|
|
1675
|
+
return serialized.length > 240 ? `${serialized.slice(0, 240)}…` : serialized;
|
|
1676
|
+
} catch {
|
|
1677
|
+
return "[input omitted]";
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function eventText(event, mode = "cli") {
|
|
1681
|
+
if (event.type === "text.delta") return String(event.data.text);
|
|
1682
|
+
if (event.type === "tool.started") return `\n↳ ${event.data.name}\n`;
|
|
1683
|
+
if (event.type === "tool.completed") {
|
|
1684
|
+
const result = event.data.result;
|
|
1685
|
+
const edit = ["edit_file", "create_file"].includes(String(event.data.name));
|
|
1686
|
+
return `${event.data.error ? "✗" : "✓"} ${event.data.name}${edit || event.data.error ? `\n${result.text?.slice(0, 5e3)}` : ""}\n`;
|
|
1687
|
+
}
|
|
1688
|
+
if (event.type === "context.compacted") return "\n· Earlier context summarized\n";
|
|
1689
|
+
if (event.type === "recovery.required") return `\nRecovery requires inspection:\n${(Array.isArray(event.data.calls) ? event.data.calls : []).map((call) => {
|
|
1690
|
+
const input = inputPreview(call.input);
|
|
1691
|
+
return ` ${call.name}: ${call.state === "not_executed" ? "not executed" : "outcome unknown"}${input ? ` ${input}` : ""}`;
|
|
1692
|
+
}).join("\n")}\nInspect the working tree and tool outcomes, then enter ${mode === "interactive" ? "/acknowledge-interrupted [follow-up task]" : `weapp-agent resume ${event.sessionId} --acknowledge-interrupted [follow-up task]`}. Completed calls will not be replayed.\n`;
|
|
1693
|
+
if (event.type === "run.completed") {
|
|
1694
|
+
const reason = event.data.reason;
|
|
1695
|
+
if (event.data.status === "limit_reached" && reason === "context_budget") return "\n[limit_reached: context_budget] Increase contextCharacters, then resume the session.\n";
|
|
1696
|
+
if (event.data.status === "limit_reached" && reason === "max_steps") return "\n[limit_reached: max_steps] Resume the session to continue.\n";
|
|
1697
|
+
return `\n[${event.data.status}]\n`;
|
|
1698
|
+
}
|
|
1699
|
+
return "";
|
|
1700
|
+
}
|
|
1701
|
+
function App({ runner, initialSession }) {
|
|
1702
|
+
const { exit } = useApp();
|
|
1703
|
+
const [input, setInput] = useState("");
|
|
1704
|
+
const [transcript, setTranscript] = useState("Describe a mini-program change. /exit to quit. Esc cancels a running task.\n");
|
|
1705
|
+
const [busy, setBusy] = useState(false);
|
|
1706
|
+
const [session, setSession] = useState(initialSession);
|
|
1707
|
+
const [pending, setPending] = useState();
|
|
1708
|
+
const answer = useRef(() => {});
|
|
1709
|
+
const controller = useRef(void 0);
|
|
1710
|
+
const append = (text) => setTranscript((previous) => (previous + text).slice(-3e4));
|
|
1711
|
+
useInput((value, key) => {
|
|
1712
|
+
if (key.ctrl && value === "c") {
|
|
1713
|
+
if (busy) controller.current?.abort();
|
|
1714
|
+
else exit();
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (key.escape && busy) controller.current?.abort();
|
|
1718
|
+
if (pending && ["y", "n"].includes(value.toLowerCase())) {
|
|
1719
|
+
answer.current(value.toLowerCase() === "y");
|
|
1720
|
+
setPending(void 0);
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
const submit = async (prompt) => {
|
|
1724
|
+
if (!prompt.trim() || busy) return;
|
|
1725
|
+
if (prompt.trim() === "/exit") {
|
|
1726
|
+
exit();
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
setInput("");
|
|
1730
|
+
setBusy(true);
|
|
1731
|
+
append(`\nYou › ${prompt}\n\n`);
|
|
1732
|
+
const abort = new AbortController();
|
|
1733
|
+
controller.current = abort;
|
|
1734
|
+
const approve = (request) => new Promise((resolve) => {
|
|
1735
|
+
setPending(request);
|
|
1736
|
+
const stop = () => {
|
|
1737
|
+
setPending(void 0);
|
|
1738
|
+
resolve(false);
|
|
1739
|
+
};
|
|
1740
|
+
abort.signal.addEventListener("abort", stop, { once: true });
|
|
1741
|
+
answer.current = (value) => {
|
|
1742
|
+
abort.signal.removeEventListener("abort", stop);
|
|
1743
|
+
resolve(value);
|
|
1744
|
+
};
|
|
1745
|
+
});
|
|
1746
|
+
try {
|
|
1747
|
+
const result = await runInteractiveTask(runner, prompt, session, abort.signal, (event) => {
|
|
1748
|
+
append(eventText(event, "interactive"));
|
|
1749
|
+
if (event.type === "run.started") setSession(event.sessionId);
|
|
1750
|
+
}, approve);
|
|
1751
|
+
setSession(result.sessionId);
|
|
1752
|
+
if (result.status !== "completed") append(`${result.text}\n`);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
append(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1755
|
+
} finally {
|
|
1756
|
+
setBusy(false);
|
|
1757
|
+
setPending(void 0);
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1761
|
+
flexDirection: "column",
|
|
1762
|
+
paddingX: 1,
|
|
1763
|
+
children: [
|
|
1764
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
1765
|
+
gap: 2,
|
|
1766
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
1767
|
+
bold: true,
|
|
1768
|
+
color: "cyan",
|
|
1769
|
+
children: "WEAPP AGENT"
|
|
1770
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
1771
|
+
dimColor: true,
|
|
1772
|
+
children: session ? `session ${session}` : "mini-program development"
|
|
1773
|
+
})]
|
|
1774
|
+
}),
|
|
1775
|
+
/* @__PURE__ */ jsx(Box, {
|
|
1776
|
+
borderStyle: "single",
|
|
1777
|
+
borderColor: "gray",
|
|
1778
|
+
paddingX: 1,
|
|
1779
|
+
flexDirection: "column",
|
|
1780
|
+
children: /* @__PURE__ */ jsx(Text, { children: transcript })
|
|
1781
|
+
}),
|
|
1782
|
+
pending ? /* @__PURE__ */ jsxs(Box, {
|
|
1783
|
+
flexDirection: "column",
|
|
1784
|
+
children: [
|
|
1785
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1786
|
+
color: "yellow",
|
|
1787
|
+
children: ["Approval ·", pending.kind]
|
|
1788
|
+
}),
|
|
1789
|
+
/* @__PURE__ */ jsx(Text, { children: pending.summary }),
|
|
1790
|
+
/* @__PURE__ */ jsx(Text, { children: "Allow this exact operation? [y/n]" })
|
|
1791
|
+
]
|
|
1792
|
+
}) : busy ? /* @__PURE__ */ jsx(Text, {
|
|
1793
|
+
color: "cyan",
|
|
1794
|
+
children: "Working… Esc to cancel"
|
|
1795
|
+
}) : /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
1796
|
+
color: "cyan",
|
|
1797
|
+
children: "› "
|
|
1798
|
+
}), /* @__PURE__ */ jsx(TextInput, {
|
|
1799
|
+
value: input,
|
|
1800
|
+
onChange: setInput,
|
|
1801
|
+
onSubmit: submit
|
|
1802
|
+
})] })
|
|
1803
|
+
]
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
async function interactive(runner, initialSession) {
|
|
1807
|
+
await render(/* @__PURE__ */ jsx(App, {
|
|
1808
|
+
runner,
|
|
1809
|
+
initialSession
|
|
1810
|
+
}), { exitOnCtrlC: false }).waitUntilExit();
|
|
1811
|
+
}
|
|
1812
|
+
//#endregion
|
|
1813
|
+
//#region src/index.ts
|
|
1814
|
+
const clean = redactor();
|
|
1815
|
+
const program = new Command().name("weapp-agent").description("AI coding agent for WeChat mini-programs").version("0.1.0").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");
|
|
1816
|
+
async function terminalApproval(summary) {
|
|
1817
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1818
|
+
const reader = createInterface({
|
|
1819
|
+
input: process.stdin,
|
|
1820
|
+
output: process.stderr
|
|
1821
|
+
});
|
|
1822
|
+
try {
|
|
1823
|
+
return /^y(?:es)?$/i.test((await reader.question(`${summary}\nAllow? [y/N] `)).trim());
|
|
1824
|
+
} finally {
|
|
1825
|
+
reader.close();
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const approve = (request) => terminalApproval(clean(request.summary));
|
|
1829
|
+
function output(value, json) {
|
|
1830
|
+
process.stdout.write(`${clean(json ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value, null, 2))}\n`);
|
|
1831
|
+
}
|
|
1832
|
+
async function configAndTrust(root, options, approver, agent = true) {
|
|
1833
|
+
const config = agent ? await loadConfig(root) : await (0, dist_exports.resolveProjectConfig)(root);
|
|
1834
|
+
const fingerprint = await projectFingerprint(root, config);
|
|
1835
|
+
let trusted = await isTrusted(root, fingerprint);
|
|
1836
|
+
if (options.trust) {
|
|
1837
|
+
await trustProject(root, fingerprint);
|
|
1838
|
+
trusted = true;
|
|
1839
|
+
} else if (!trusted && !options.json) {
|
|
1840
|
+
if (await approver({
|
|
1841
|
+
kind: "trust",
|
|
1842
|
+
summary: `Trust ${root}?\nThis permits local edits, configured scripts and MCP server startup.\n${JSON.stringify({
|
|
1843
|
+
verification: config.verification,
|
|
1844
|
+
mcp: config.mcp
|
|
1845
|
+
}, null, 2)}`,
|
|
1846
|
+
fingerprint
|
|
1847
|
+
})) {
|
|
1848
|
+
await trustProject(root, fingerprint);
|
|
1849
|
+
trusted = true;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return {
|
|
1853
|
+
config,
|
|
1854
|
+
fingerprint,
|
|
1855
|
+
trusted
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
async function images(files) {
|
|
1859
|
+
return Promise.all(files.map(async (file) => {
|
|
1860
|
+
const data = await readFile(path.resolve(file));
|
|
1861
|
+
if (data.length > 10485760) throw new Error(`Image exceeds 10 MB: ${file}`);
|
|
1862
|
+
const type = data.subarray(0, 8).equals(Buffer$1.from([
|
|
1863
|
+
137,
|
|
1864
|
+
80,
|
|
1865
|
+
78,
|
|
1866
|
+
71,
|
|
1867
|
+
13,
|
|
1868
|
+
10,
|
|
1869
|
+
26,
|
|
1870
|
+
10
|
|
1871
|
+
])) ? "image/png" : data[0] === 255 && data[1] === 216 ? "image/jpeg" : data.subarray(8, 12).toString() === "WEBP" ? "image/webp" : void 0;
|
|
1872
|
+
if (!type) throw new Error(`Use a PNG, JPEG or WebP image: ${file}`);
|
|
1873
|
+
return {
|
|
1874
|
+
type: "image",
|
|
1875
|
+
data: data.toString("base64"),
|
|
1876
|
+
mediaType: type
|
|
1877
|
+
};
|
|
1878
|
+
}));
|
|
1879
|
+
}
|
|
1880
|
+
async function execute(prompt, options, sessionId, suppliedSignal, onEvent, approver = approve) {
|
|
1881
|
+
const root = await (0, dist_exports.findProjectRoot)(options.cwd);
|
|
1882
|
+
const selectedApprover = options.json ? async () => false : approver;
|
|
1883
|
+
const { config, fingerprint, trusted } = await configAndTrust(root, options, selectedApprover);
|
|
1884
|
+
const agentConfig = configSchema.parse(config);
|
|
1885
|
+
const model = createModel(agentConfig.model);
|
|
1886
|
+
const adapter = new WeappProjectAdapter();
|
|
1887
|
+
const project = await adapter.detect(root);
|
|
1888
|
+
const abort = new AbortController();
|
|
1889
|
+
const stop = () => abort.abort();
|
|
1890
|
+
if (!suppliedSignal) process.once("SIGINT", stop);
|
|
1891
|
+
const signal = suppliedSignal ?? abort.signal;
|
|
1892
|
+
const context = {
|
|
1893
|
+
root,
|
|
1894
|
+
trusted,
|
|
1895
|
+
approve: selectedApprover,
|
|
1896
|
+
signal
|
|
1897
|
+
};
|
|
1898
|
+
const connections = [];
|
|
1899
|
+
const warnings = [];
|
|
1900
|
+
try {
|
|
1901
|
+
const tools = [
|
|
1902
|
+
...fileTools(),
|
|
1903
|
+
...await adapter.tools(project),
|
|
1904
|
+
(0, dist_exports.verificationTool)(config, fingerprint)
|
|
1905
|
+
];
|
|
1906
|
+
const automatic = trusted ? await builtinMcp(root) : void 0;
|
|
1907
|
+
const servers = [...config.mcp, ...automatic && !config.mcp.some((s) => s.name === automatic.name) ? [automatic] : []];
|
|
1908
|
+
for (const server of servers) try {
|
|
1909
|
+
const connection = await connectMcp(server, context, {
|
|
1910
|
+
config,
|
|
1911
|
+
fingerprint,
|
|
1912
|
+
builtin: server === automatic
|
|
1913
|
+
});
|
|
1914
|
+
connections.push(connection);
|
|
1915
|
+
tools.push(...connection.tools);
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
warnings.push(`MCP ${server.name} unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1918
|
+
}
|
|
1919
|
+
return await runAgent({
|
|
1920
|
+
root,
|
|
1921
|
+
config: agentConfig,
|
|
1922
|
+
model,
|
|
1923
|
+
tools,
|
|
1924
|
+
prompt,
|
|
1925
|
+
images: await images(options.image ?? []),
|
|
1926
|
+
sessionId,
|
|
1927
|
+
signal,
|
|
1928
|
+
trusted,
|
|
1929
|
+
approve: selectedApprover,
|
|
1930
|
+
acknowledgeInterrupted: options.acknowledgeInterrupted,
|
|
1931
|
+
system: `${await adapter.instructions(project)}\nUnavailable capabilities:\n${warnings.join("\n")}`,
|
|
1932
|
+
onEvent: async (event) => {
|
|
1933
|
+
if (onEvent) onEvent(event);
|
|
1934
|
+
else if (options.json) output(event, true);
|
|
1935
|
+
else {
|
|
1936
|
+
if (event.type === "run.started") process.stderr.write(`Session: ${event.sessionId}\n${warnings.join("\n")}\n`);
|
|
1937
|
+
process.stdout.write(clean(eventText(event)));
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
} finally {
|
|
1942
|
+
if (!suppliedSignal) process.removeListener("SIGINT", stop);
|
|
1943
|
+
await Promise.allSettled(connections.map((c) => c.close()));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function exitFor(status) {
|
|
1947
|
+
process.exitCode = {
|
|
1948
|
+
completed: 0,
|
|
1949
|
+
failed: 1,
|
|
1950
|
+
action_required: 2,
|
|
1951
|
+
limit_reached: 3,
|
|
1952
|
+
cancelled: 130
|
|
1953
|
+
}[status] ?? 1;
|
|
1954
|
+
}
|
|
1955
|
+
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([
|
|
1956
|
+
"openai",
|
|
1957
|
+
"anthropic",
|
|
1958
|
+
"openai-compatible"
|
|
1959
|
+
]).default("openai")).option("--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) => {
|
|
1960
|
+
const opts = command.optsWithGlobals();
|
|
1961
|
+
const root = path.resolve(directory ?? opts.cwd);
|
|
1962
|
+
if (local.create) {
|
|
1963
|
+
if (await (0, dist_exports.exists)(root)) throw new Error("Creation target already exists; refusing to overwrite it.");
|
|
1964
|
+
await execa("pnpm", [
|
|
1965
|
+
"dlx",
|
|
1966
|
+
"create-weapp-vite@3.0.1",
|
|
1967
|
+
root,
|
|
1968
|
+
local.template === "native" ? "default" : "wevu",
|
|
1969
|
+
"--no-install-skills"
|
|
1970
|
+
], { stdio: opts.json ? [
|
|
1971
|
+
"ignore",
|
|
1972
|
+
"pipe",
|
|
1973
|
+
"pipe"
|
|
1974
|
+
] : "inherit" });
|
|
1975
|
+
} else await mkdir(root, { recursive: true });
|
|
1976
|
+
const project = await (0, dist_exports.detectProject)(root);
|
|
1977
|
+
const config = projectConfigSchema.parse({
|
|
1978
|
+
model: local.model ? {
|
|
1979
|
+
provider: local.provider,
|
|
1980
|
+
name: local.model,
|
|
1981
|
+
baseURL: local.baseUrl,
|
|
1982
|
+
apiKeyEnv: local.apiKeyEnv
|
|
1983
|
+
} : void 0,
|
|
1984
|
+
verification: (0, dist_exports.defaultVerification)(project)
|
|
1985
|
+
});
|
|
1986
|
+
await writeFile(path.join(root, configFilename), `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
|
|
1987
|
+
output({
|
|
1988
|
+
status: "created",
|
|
1989
|
+
root,
|
|
1990
|
+
config: configFilename,
|
|
1991
|
+
next: config.model ? `Set ${apiKeyVariable(config.model)}, install project dependencies, then run weapp-agent --trust.` : "Review project scripts, then run weapp-agent --trust accept. Configure acceptance.scenarios for runtime checks. No model key is required."
|
|
1992
|
+
}, opts.json);
|
|
1993
|
+
});
|
|
1994
|
+
program.command("run").description("Run a single task").argument("<prompt>").option("--image <file...>", "reference screenshots").action(async (prompt, _local, command) => {
|
|
1995
|
+
const options = command.optsWithGlobals();
|
|
1996
|
+
const result = await execute(prompt, options);
|
|
1997
|
+
if (!options.json && result.status !== "completed") output(result.text, false);
|
|
1998
|
+
exitFor(result.status);
|
|
1999
|
+
});
|
|
2000
|
+
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) => {
|
|
2001
|
+
const options = command.optsWithGlobals();
|
|
2002
|
+
const result = await execute(prompt, options, session);
|
|
2003
|
+
if (!options.json && result.status !== "completed") output(result.text, false);
|
|
2004
|
+
exitFor(result.status);
|
|
2005
|
+
});
|
|
2006
|
+
registerSessionCommands(program, output);
|
|
2007
|
+
program.command("doctor").description("Inspect configuration and local prerequisites without calling a model").action(async (_local, command) => {
|
|
2008
|
+
const opts = command.optsWithGlobals();
|
|
2009
|
+
const root = await (0, dist_exports.findProjectRoot)(opts.cwd);
|
|
2010
|
+
const service = await AcceptanceService.create(root);
|
|
2011
|
+
try {
|
|
2012
|
+
const config = await (0, dist_exports.resolveProjectConfig)(root);
|
|
2013
|
+
output({
|
|
2014
|
+
...await service.inspect(),
|
|
2015
|
+
node: process.version,
|
|
2016
|
+
config: await (0, dist_exports.exists)(path.join(root, "weapp-agent.config.json")) ? "valid" : "inferred",
|
|
2017
|
+
apiKey: config.model ? {
|
|
2018
|
+
variable: apiKeyVariable(config.model),
|
|
2019
|
+
present: Boolean(process.env[apiKeyVariable(config.model)])
|
|
2020
|
+
} : null,
|
|
2021
|
+
stateDirectory: stateRoot()
|
|
2022
|
+
}, opts.json);
|
|
2023
|
+
} finally {
|
|
2024
|
+
await service.close();
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
program.command("verify").description("Run configured checks and report unverified categories").action(async (_local, command) => {
|
|
2028
|
+
const opts = command.optsWithGlobals();
|
|
2029
|
+
const root = await (0, dist_exports.findProjectRoot)(opts.cwd);
|
|
2030
|
+
const { config, fingerprint, trusted } = await configAndTrust(root, opts, approve, false);
|
|
2031
|
+
const abort = new AbortController();
|
|
2032
|
+
const stop = () => abort.abort();
|
|
2033
|
+
process.once("SIGINT", stop);
|
|
2034
|
+
try {
|
|
2035
|
+
const result = await (0, dist_exports.verifyProject)(config, {
|
|
2036
|
+
root,
|
|
2037
|
+
trusted,
|
|
2038
|
+
signal: abort.signal,
|
|
2039
|
+
approve: opts.json ? async () => false : approve
|
|
2040
|
+
}, fingerprint);
|
|
2041
|
+
output(result, opts.json);
|
|
2042
|
+
if (!result.passed) process.exitCode = 1;
|
|
2043
|
+
} finally {
|
|
2044
|
+
process.removeListener("SIGINT", stop);
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
program.command("skill").description("Copy the bundled acceptance skill into a new directory you choose").argument("<directory>", "new skill directory, for example .agents/skills/weapp-acceptance").action(async (directory, _local, command) => {
|
|
2048
|
+
const opts = command.optsWithGlobals();
|
|
2049
|
+
const target = path.resolve(opts.cwd, directory);
|
|
2050
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
2051
|
+
await mkdir(target);
|
|
2052
|
+
const source = new URL("../skills/weapp-acceptance/", import.meta.url);
|
|
2053
|
+
for (const entry of await readdir(source)) await cp(new URL(entry, source), path.join(target, entry), {
|
|
2054
|
+
recursive: true,
|
|
2055
|
+
force: false,
|
|
2056
|
+
errorOnExist: true
|
|
2057
|
+
});
|
|
2058
|
+
output({
|
|
2059
|
+
status: "installed",
|
|
2060
|
+
directory: target
|
|
2061
|
+
}, opts.json);
|
|
2062
|
+
});
|
|
2063
|
+
program.command("accept").description("Run version 2 acceptance without a model; missing required evidence never passes").action(async (_local, command) => {
|
|
2064
|
+
const opts = command.optsWithGlobals();
|
|
2065
|
+
const service = await AcceptanceService.create(await (0, dist_exports.findProjectRoot)(opts.cwd), { trust: opts.trust });
|
|
2066
|
+
let jobId;
|
|
2067
|
+
const stop = () => {
|
|
2068
|
+
if (jobId) service.cancel(jobId).catch(() => {});
|
|
2069
|
+
};
|
|
2070
|
+
process.once("SIGINT", stop);
|
|
2071
|
+
process.once("SIGTERM", stop);
|
|
2072
|
+
try {
|
|
2073
|
+
jobId = (await service.start()).jobId;
|
|
2074
|
+
if (!opts.json) process.stderr.write(`Acceptance: ${jobId}\n`);
|
|
2075
|
+
const report = await service.wait(jobId);
|
|
2076
|
+
output(report, opts.json);
|
|
2077
|
+
process.exitCode = report.passed ? 0 : report.status === "action_required" || report.status === "interrupted" ? 2 : report.status === "cancelled" ? 130 : 1;
|
|
2078
|
+
} finally {
|
|
2079
|
+
process.removeListener("SIGINT", stop);
|
|
2080
|
+
process.removeListener("SIGTERM", stop);
|
|
2081
|
+
await service.close();
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
program.command("report").description("Read a saved acceptance report and recheck source freshness").argument("<jobId>").action(async (jobId, _local, command) => {
|
|
2085
|
+
const opts = command.optsWithGlobals();
|
|
2086
|
+
const service = await AcceptanceService.create(await (0, dist_exports.findProjectRoot)(opts.cwd));
|
|
2087
|
+
try {
|
|
2088
|
+
output(await service.report(jobId), opts.json);
|
|
2089
|
+
} finally {
|
|
2090
|
+
await service.close();
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
program.command("mcp").description("Serve project inspection and acceptance over stdio MCP; no model required").action(async (_local, command) => {
|
|
2094
|
+
const opts = command.optsWithGlobals();
|
|
2095
|
+
const service = await AcceptanceService.create(await (0, dist_exports.findProjectRoot)(opts.cwd), { trust: opts.trust });
|
|
2096
|
+
const server = await serveAcceptanceMcp(service);
|
|
2097
|
+
const stop = () => {
|
|
2098
|
+
service.close().then(() => server.close());
|
|
2099
|
+
};
|
|
2100
|
+
process.once("SIGINT", stop);
|
|
2101
|
+
process.once("SIGTERM", stop);
|
|
2102
|
+
});
|
|
2103
|
+
program.action(async () => {
|
|
2104
|
+
const options = program.opts();
|
|
2105
|
+
if (!process.stdin.isTTY || options.json) throw new Error("Interactive mode requires a TTY. Use weapp-agent run <prompt> --json for automation.");
|
|
2106
|
+
let trustOnce = options.trust;
|
|
2107
|
+
await interactive((prompt, session, signal, onEvent, approval, runOptions) => {
|
|
2108
|
+
const current = {
|
|
2109
|
+
...options,
|
|
2110
|
+
trust: trustOnce,
|
|
2111
|
+
acknowledgeInterrupted: runOptions?.acknowledgeInterrupted
|
|
2112
|
+
};
|
|
2113
|
+
trustOnce = false;
|
|
2114
|
+
return execute(prompt, current, session, signal, onEvent, approval);
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
try {
|
|
2118
|
+
await program.parseAsync();
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
const message = clean(error instanceof Error ? error.message : String(error));
|
|
2121
|
+
if (process.argv.includes("--json") && !process.argv.includes("mcp")) output({
|
|
2122
|
+
version: 1,
|
|
2123
|
+
type: "error",
|
|
2124
|
+
data: { message }
|
|
2125
|
+
}, true);
|
|
2126
|
+
else process.stderr.write(`${message}\n`);
|
|
2127
|
+
process.exitCode = error instanceof Error && error.name === "ApprovalRequired" ? 2 : 1;
|
|
2128
|
+
}
|
|
2129
|
+
//#endregion
|
|
2130
|
+
export {};
|