u1s1-cli 0.16.6 → 0.18.0

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.
@@ -0,0 +1,333 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import vm from "node:vm";
5
+ import { dirname, resolve } from "node:path";
6
+ import { runPool, runSubagent, SUBAGENT_CONCURRENCY } from "../subagent.js";
7
+ // ---- WorkflowRunner:执行模型生成的编排脚本(路线 A:主进程 vm 上下文) ----
8
+ // 定位「防误不防恶」:vm 非硬安全边界,靠白名单注入 + 静态校验 + 脚本落盘可审查兜底。
9
+ /** 整个 run 的默认墙钟上限。 */
10
+ export const WORKFLOW_TIMEOUT_MS = 30 * 60_000;
11
+ /** 单个子任务的超时(与 spawn_subagent 工具一致)。 */
12
+ const PER_TASK_TIMEOUT_MS = 15 * 60_000;
13
+ /** 报告里最多保留多少行脚本 log 输出。 */
14
+ const MAX_LOG_LINES = 60;
15
+ /** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
16
+ export const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20_000_000;
17
+ /** JSONL 进度存档:一行一个已完成的子任务,断点续跑时按 key 跳过。 */
18
+ class ProgressStore {
19
+ path;
20
+ #cache = new Map();
21
+ constructor(path) {
22
+ this.path = path;
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ if (!existsSync(path))
25
+ return;
26
+ for (const line of readFileSync(path, "utf8").split("\n")) {
27
+ if (!line.trim())
28
+ continue;
29
+ try {
30
+ const e = JSON.parse(line);
31
+ if (typeof e.key === "string")
32
+ this.#cache.set(e.key, e);
33
+ }
34
+ catch {
35
+ // 尾部半行(进程被杀)直接忽略
36
+ }
37
+ }
38
+ }
39
+ get(key) {
40
+ return this.#cache.get(key);
41
+ }
42
+ append(entry) {
43
+ this.#cache.set(entry.key, entry);
44
+ appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
45
+ }
46
+ }
47
+ /**
48
+ * 断点续跑的任务标识:任务内容 + 模型的哈希。
49
+ * 前提是脚本确定性好 —— 相同任务串在续跑时视为同一任务(循环里重复相同任务串
50
+ * 的场景续跑粒度不精确,W1 接受此限制;i18n 这类每项任务内容唯一的场景完全够用)。
51
+ */
52
+ function taskKey(task, model) {
53
+ return createHash("sha1").update(`${task}|${model ?? ""}`).digest("hex").slice(0, 16);
54
+ }
55
+ /** 去掉注释和字符串字面量后再扫禁用模式,避免文档式注释误报。 */
56
+ function stripCommentsAndStrings(code) {
57
+ return code
58
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
59
+ .replace(/(^|[^:])\/\/[^\n]*/g, "$1 ")
60
+ .replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
61
+ .replace(/'(?:[^'\\\n]|\\.)*'/g, "''")
62
+ .replace(/`(?:[^`\\]|\\.)*`/g, "``");
63
+ }
64
+ /** 静态白名单校验:语法能编译 + 不碰沙箱之外的任何能力。 */
65
+ export function validateScript(code) {
66
+ const errors = [];
67
+ try {
68
+ // 与执行时同款包装,保证报错行号一致
69
+ new vm.Script(`(async () => {\n${code}\n})()`, { filename: "workflow.mjs" });
70
+ }
71
+ catch (e) {
72
+ errors.push(`语法错误:${e.message}`);
73
+ return errors;
74
+ }
75
+ const bare = stripCommentsAndStrings(code);
76
+ const forbidden = [
77
+ [/\brequire\s*\(/, "require()"],
78
+ [/\bimport\s*\(/, "动态 import()"],
79
+ [/^\s*import\s/m, "静态 import"],
80
+ [/\bprocess\b/, "process 对象"],
81
+ [/\bglobalThis\b/, "globalThis"],
82
+ [/\bfetch\s*\(/, "fetch()"],
83
+ [/\beval\s*\(/, "eval()"],
84
+ [/\bFunction\s*\(/, "new Function()"],
85
+ [/\.constructor\b/, ".constructor(原型链逃逸)"],
86
+ [/\bprototype\b/, "prototype(原型链逃逸)"],
87
+ [/\b__proto__\b/, "__proto__"],
88
+ [/\bWebAssembly\b/, "WebAssembly"],
89
+ [/node:/, "node: 内置模块"],
90
+ [/child_process/, "child_process"],
91
+ [/\bfs\b\s*\./, "fs 模块"],
92
+ ];
93
+ for (const [re, label] of forbidden) {
94
+ if (re.test(bare))
95
+ errors.push(`脚本使用了沙箱之外的能力:${label}`);
96
+ }
97
+ return errors;
98
+ }
99
+ /** 执行一个 workflow 脚本,返回给主对话的报告。不抛错——失败也进报告让模型自行修复。 */
100
+ export async function runWorkflow(opts) {
101
+ const started = Date.now();
102
+ const logs = [];
103
+ const stats = { spawns: 0, ok: 0, failed: 0, cached: 0 };
104
+ // 内部中止控制器:父会话 ESC 或整体超时都会触发,并传导到每个子 session
105
+ const internal = new AbortController();
106
+ const onParentAbort = () => internal.abort(opts.signal?.reason);
107
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
108
+ let timedOut = false;
109
+ const overallTimer = setTimeout(() => {
110
+ timedOut = true;
111
+ internal.abort(new Error(`整个工作流超时(${Math.round((opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS) / 60_000)} 分钟)`));
112
+ }, opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS);
113
+ const store = new ProgressStore(opts.progressPath);
114
+ const budgetTokens = opts.budgetTokens ?? WORKFLOW_DEFAULT_BUDGET_TOKENS;
115
+ let spentTokens = 0;
116
+ let running = 0;
117
+ const progress = { spawns: 0, ok: 0, failed: 0, cached: 0, running: 0, spentTokens: 0, budgetTokens };
118
+ const reportProgress = () => {
119
+ progress.spentTokens = spentTokens;
120
+ opts.onProgress?.({ ...progress });
121
+ };
122
+ /** git worktree 隔离:在 .u1s1/worktrees/ 下建临时工作树,返回其路径。 */
123
+ let worktreeSeq = 0;
124
+ function createTempWorktree() {
125
+ const dir = resolve(workflowsDir(), "../../.u1s1-worktrees", `wf-${Date.now()}-${++worktreeSeq}`);
126
+ execFileSync("git", ["worktree", "add", "--detach", dir], { stdio: "pipe" });
127
+ return dir;
128
+ }
129
+ function removeWorktree(dir) {
130
+ try {
131
+ execFileSync("git", ["worktree", "remove", "--force", dir], { stdio: "pipe" });
132
+ }
133
+ catch {
134
+ rmSync(dir, { recursive: true, force: true });
135
+ try {
136
+ execFileSync("git", ["worktree", "prune"], { stdio: "pipe" });
137
+ }
138
+ catch {
139
+ // 尽力清理即可
140
+ }
141
+ }
142
+ }
143
+ /** 注入沙箱的 subagent():字符串或 {task, model, worktree} 都行,返回最终输出文本。 */
144
+ async function subagentImpl(input) {
145
+ const o = typeof input === "string" ? { task: input } : (input ?? {});
146
+ const task = typeof o.task === "string" ? o.task.trim() : "";
147
+ if (!task)
148
+ throw new Error("subagent() 需要 task 字符串");
149
+ const model = typeof o.model === "string" && o.model ? o.model : undefined;
150
+ if (internal.signal.aborted)
151
+ throw new Error("工作流已中止,不再派发新子任务");
152
+ if (budgetTokens > 0 && spentTokens >= budgetTokens) {
153
+ throw new Error(`token 预算已用尽(已用 ${spentTokens.toLocaleString()} / 上限 ${budgetTokens.toLocaleString()}),不再派发新子任务`);
154
+ }
155
+ const key = taskKey(task, model);
156
+ if (opts.resume) {
157
+ const prev = store.get(key);
158
+ if (prev) {
159
+ stats.cached++;
160
+ progress.cached = stats.cached;
161
+ reportProgress();
162
+ if (prev.ok)
163
+ return prev.text;
164
+ throw new Error(prev.text);
165
+ }
166
+ }
167
+ stats.spawns++;
168
+ progress.spawns = stats.spawns;
169
+ running++;
170
+ progress.running = running;
171
+ reportProgress();
172
+ const t0 = Date.now();
173
+ let worktreeDir;
174
+ if (o.worktree === true)
175
+ worktreeDir = createTempWorktree();
176
+ try {
177
+ const out = await runSubagent({
178
+ task,
179
+ model,
180
+ parentModel: opts.parentModel,
181
+ timeoutMs: PER_TASK_TIMEOUT_MS,
182
+ signal: internal.signal,
183
+ cwd: worktreeDir,
184
+ noTools: o.noTools === true,
185
+ });
186
+ stats.ok++;
187
+ progress.ok = stats.ok;
188
+ spentTokens += out.usage.totalTokens;
189
+ store.append({ key, task: task.slice(0, 120), ok: true, text: out.text, ms: Date.now() - t0 });
190
+ return out.text;
191
+ }
192
+ catch (e) {
193
+ stats.failed++;
194
+ progress.failed = stats.failed;
195
+ const msg = e.message || "未知错误";
196
+ store.append({ key, task: task.slice(0, 120), ok: false, text: msg, ms: Date.now() - t0 });
197
+ throw new Error(msg);
198
+ }
199
+ finally {
200
+ if (worktreeDir)
201
+ removeWorktree(worktreeDir);
202
+ running--;
203
+ progress.running = running;
204
+ reportProgress();
205
+ }
206
+ }
207
+ /** 注入沙箱的 judge():廉价无工具模型调用,严格回答 PASS/FAIL,转成布尔。 */
208
+ async function judgeImpl(question) {
209
+ if (typeof question !== "string" || !question.trim())
210
+ throw new Error("judge() 需要问题字符串");
211
+ const answer = await subagentImpl({
212
+ task: `回答以下判定问题。只允许回答一个词:PASS 或 FAIL,不要任何其他内容。\n\n判定问题:${question.trim()}`,
213
+ noTools: true,
214
+ });
215
+ const pass = /\bPASS\b/i.test(answer) && !/\bFAIL\b/i.test(answer);
216
+ if (!pass && !/\bFAIL\b/i.test(answer))
217
+ logs.push(`⚠ judge 回答无法解析,按 FAIL 处理:${answer.slice(0, 120)}`);
218
+ return pass;
219
+ }
220
+ /** barrier 扇出:全部并发(封顶排队),单个失败该项为 null,不拖垮整批。 */
221
+ async function parallel(thunks) {
222
+ if (!Array.isArray(thunks))
223
+ throw new Error("parallel() 需要一个 thunk 数组");
224
+ const results = new Array(thunks.length);
225
+ await runPool(thunks, SUBAGENT_CONCURRENCY, async (thunk, i) => {
226
+ results[i] = await Promise.resolve()
227
+ .then(thunk)
228
+ .catch((e) => {
229
+ logs.push(`✗ 任务 ${i + 1} 失败:${e.message}`);
230
+ return null;
231
+ });
232
+ }, internal.signal);
233
+ return results;
234
+ }
235
+ /** 无屏障流水线:每个 item 独立穿过所有 stage,item 失败只废自己。 */
236
+ async function pipeline(items, stages) {
237
+ if (!Array.isArray(items))
238
+ throw new Error("pipeline() 第一个参数需要数组");
239
+ if (!Array.isArray(stages) || stages.length === 0)
240
+ throw new Error("pipeline() 第二个参数需要非空 stage 数组");
241
+ const results = new Array(items.length);
242
+ await runPool(items, SUBAGENT_CONCURRENCY, async (item, i) => {
243
+ let current = item;
244
+ for (let s = 0; s < stages.length; s++) {
245
+ try {
246
+ current = await stages[s](current);
247
+ }
248
+ catch (e) {
249
+ logs.push(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`);
250
+ current = null;
251
+ break;
252
+ }
253
+ }
254
+ results[i] = current;
255
+ }, internal.signal);
256
+ return results;
257
+ }
258
+ // 沙箱:只有编排原语 + 受限 log/timer,标准 JS 内建(Promise/JSON/Math…)天然可用
259
+ const fmtLogArg = (a) => typeof a === "string" ? a : (() => { try {
260
+ return JSON.stringify(a);
261
+ }
262
+ catch {
263
+ return String(a);
264
+ } })();
265
+ const pushLog = (...args) => {
266
+ if (logs.length < 500)
267
+ logs.push(args.map(fmtLogArg).join(" "));
268
+ };
269
+ const sandbox = {
270
+ subagent: subagentImpl,
271
+ parallel,
272
+ pipeline,
273
+ judge: judgeImpl,
274
+ log: pushLog,
275
+ console: { log: pushLog, info: pushLog, warn: pushLog, error: pushLog },
276
+ setTimeout: (fn, ms) => setTimeout(fn, Math.max(0, Math.min(Number(ms) || 0, 120_000))),
277
+ clearTimeout,
278
+ };
279
+ let scriptError;
280
+ let completionValue;
281
+ try {
282
+ // 包成 async IIFE:支持顶层 await 和 return;line 数与校验时一致
283
+ const script = new vm.Script(`(async () => {\n${opts.code}\n})()`, { filename: "workflow.mjs" });
284
+ const ctx = vm.createContext(sandbox);
285
+ completionValue = await Promise.resolve().then(() => script.runInContext(ctx));
286
+ }
287
+ catch (e) {
288
+ scriptError = e.stack?.split("\n").slice(0, 3).join("\n") || e.message;
289
+ }
290
+ finally {
291
+ clearTimeout(overallTimer);
292
+ opts.signal?.removeEventListener("abort", onParentAbort);
293
+ }
294
+ const seconds = Math.round((Date.now() - started) / 1000);
295
+ const aborted = internal.signal.aborted;
296
+ const budgetExhausted = budgetTokens > 0 && spentTokens >= budgetTokens;
297
+ const details = { ...stats, seconds, aborted, spentTokens, budgetExhausted };
298
+ const lines = [`工作流结束,用时 ${seconds}s · 子任务 ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · 续跑命中 ${stats.cached}` : ""} · 用 ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / 预算 ${budgetTokens.toLocaleString()}` : " (不限)"}${timedOut ? " · ⏱ 整体超时" : ""}${aborted && !timedOut ? " · 已中止" : ""}`, ""];
299
+ if (completionValue !== undefined) {
300
+ let shown;
301
+ try {
302
+ shown = JSON.stringify(completionValue, null, 2) ?? String(completionValue);
303
+ }
304
+ catch {
305
+ shown = String(completionValue);
306
+ }
307
+ lines.push("**返回值**", "", shown.slice(0, 3000), "");
308
+ }
309
+ if (scriptError)
310
+ lines.push("**脚本报错**", "", "```", scriptError, "```", "");
311
+ if (logs.length > 0) {
312
+ lines.push("**运行日志**", "", ...logs.slice(0, MAX_LOG_LINES).map((l) => `- ${l.slice(0, 300)}`));
313
+ if (logs.length > MAX_LOG_LINES)
314
+ lines.push(`- …(另有 ${logs.length - MAX_LOG_LINES} 条省略)`);
315
+ lines.push("");
316
+ }
317
+ lines.push(`进度存档:${opts.progressPath}(失败重跑可加 resume=true 续跑)`);
318
+ return { report: lines.join("\n").trim(), details };
319
+ }
320
+ /** 工作区目录:项目根 .u1s1/workflows/(脚本 + 进度存档都在这里)。 */
321
+ export function workflowsDir() {
322
+ const dir = resolve(process.cwd(), ".u1s1/workflows");
323
+ mkdirSync(dir, { recursive: true });
324
+ return dir;
325
+ }
326
+ /** 内联脚本落盘,返回脚本路径(进度存档按同名约定派生)。 */
327
+ export function saveWorkflowScript(code) {
328
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/T(\d{6}).*/, "-$1");
329
+ const path = resolve(workflowsDir(), `wf-${stamp}.mjs`);
330
+ mkdirSync(dirname(path), { recursive: true });
331
+ writeFileSync(path, code);
332
+ return path;
333
+ }
@@ -0,0 +1,13 @@
1
+ import { Type } from "typebox";
2
+ import type { ParentModelRef } from "../subagent.js";
3
+ /**
4
+ * run_workflow 工具:主 agent 把模型生成的编排脚本交给 WorkflowRunner 执行。
5
+ * 脚本在 vm 沙箱里跑,只能用注入的 subagent/parallel/pipeline 原语。
6
+ */
7
+ export declare function createRunWorkflowTool(getParentModel: () => ParentModelRef): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
8
+ script: Type.TOptional<Type.TString>;
9
+ script_path: Type.TOptional<Type.TString>;
10
+ resume: Type.TOptional<Type.TBoolean>;
11
+ timeout_minutes: Type.TOptional<Type.TNumber>;
12
+ budget_tokens: Type.TOptional<Type.TNumber>;
13
+ }>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
@@ -0,0 +1,119 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { Text } from "@earendil-works/pi-tui";
5
+ import { Type } from "typebox";
6
+ import { compactResultRender, truncate } from "../tools.js";
7
+ import { runWorkflow, saveWorkflowScript, validateScript, workflowsDir, WORKFLOW_DEFAULT_BUDGET_TOKENS, WORKFLOW_TIMEOUT_MS, } from "./runner.js";
8
+ /**
9
+ * run_workflow 工具:主 agent 把模型生成的编排脚本交给 WorkflowRunner 执行。
10
+ * 脚本在 vm 沙箱里跑,只能用注入的 subagent/parallel/pipeline 原语。
11
+ */
12
+ export function createRunWorkflowTool(getParentModel) {
13
+ return defineTool({
14
+ name: "run_workflow",
15
+ label: "工作流",
16
+ description: "Execute a JavaScript orchestration script that runs many sub-agents in structured patterns (fan-out/pipeline/loop). " +
17
+ "The script runs in a sandbox with ONLY these APIs: `subagent(task | {task, model})` returns the sub-agent's final output; " +
18
+ "`parallel([thunks])` fans out concurrently, a failed item becomes null (barrier); " +
19
+ "`pipeline(items, [stageFns])` streams each item through stages independently; plus `log(...)` and standard JS builtins. " +
20
+ "No require/import/process/fetch/fs access. Use for large parallelizable work (e.g. translate N locale files); " +
21
+ "don't use it for small tasks — write the script, pass it as `script`, then verify the report.",
22
+ promptSnippet: "Run a generated orchestration script that drives many sub-agents in parallel/pipeline patterns",
23
+ promptGuidelines: [
24
+ "Use run_workflow only when a task decomposes into many independent self-contained pieces; small jobs are cheaper done directly.",
25
+ "Every task string inside the script must be fully self-contained — sub-agents start with zero conversation context.",
26
+ "If the report shows failures, fix the script or call again with script_path + resume:true to continue from saved progress.",
27
+ ],
28
+ parameters: Type.Object({
29
+ script: Type.Optional(Type.String({ description: "Full JavaScript source of the orchestration script (top-level await allowed)." })),
30
+ script_path: Type.Optional(Type.String({
31
+ description: "Path to an existing workflow script under .u1s1/workflows/ — rerun it instead of passing `script`.",
32
+ })),
33
+ resume: Type.Optional(Type.Boolean({
34
+ description: "Skip sub-tasks already recorded as succeeded in the progress file (for crash/interrupt recovery).",
35
+ })),
36
+ timeout_minutes: Type.Optional(Type.Number({
37
+ description: `Wall-clock limit for the whole run in minutes (default ${WORKFLOW_TIMEOUT_MS / 60_000}, max 180).`,
38
+ minimum: 1,
39
+ maximum: 180,
40
+ })),
41
+ budget_tokens: Type.Optional(Type.Number({
42
+ description: `Total token ceiling for the whole run across all sub-agents (default ${WORKFLOW_DEFAULT_BUDGET_TOKENS}). Once exceeded, no new sub-agents are spawned; running ones finish. Use 0 for unlimited.`,
43
+ minimum: 0,
44
+ maximum: 1_000_000_000,
45
+ })),
46
+ }),
47
+ // 精简展示与 spawn_subagent 同款:收起一行摘要,ctrl+o 展开报告
48
+ renderShell: "self",
49
+ renderCall() {
50
+ return new Text("", 0, 0);
51
+ },
52
+ renderResult(result, options, theme, context) {
53
+ const d = result.details;
54
+ const summary = d && typeof d.ok === "number"
55
+ ? `🧭 工作流 · ✓${d.ok}${d.failed ? ` ✗${d.failed}` : ""}` +
56
+ (typeof d.seconds === "number" ? ` · ${d.seconds}s` : "") +
57
+ (d.aborted ? " · 已中止" : "")
58
+ : "🧭 工作流";
59
+ return compactResultRender(result, options, theme, context, summary);
60
+ },
61
+ async execute(_toolCallId, params, signal, onUpdate) {
62
+ let code = params.script?.trim();
63
+ let scriptPath;
64
+ if (code) {
65
+ const errors = validateScript(code);
66
+ if (errors.length > 0) {
67
+ throw new Error(`脚本未通过静态校验,请修复后重试:\n- ${errors.join("\n- ")}`);
68
+ }
69
+ scriptPath = saveWorkflowScript(code);
70
+ }
71
+ else if (params.script_path?.trim()) {
72
+ scriptPath = resolve(params.script_path.trim());
73
+ try {
74
+ code = readFileSync(scriptPath, "utf8");
75
+ }
76
+ catch {
77
+ throw new Error(`读不到脚本文件:${scriptPath}`);
78
+ }
79
+ }
80
+ else {
81
+ throw new Error("script 和 script_path 至少填一个");
82
+ }
83
+ const progressPath = scriptPath.replace(/\.mjs$/, ".progress.jsonl");
84
+ // 流式进度:onProgress 高频触发,节流到 ≥2s 一次才推给 TUI
85
+ let lastPush = 0;
86
+ const onProgress = onUpdate
87
+ ? (p) => {
88
+ const now = Date.now();
89
+ if (now - lastPush < 2_000)
90
+ return;
91
+ lastPush = now;
92
+ onUpdate({
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: `🧭 子任务 ✓${p.ok} ✗${p.failed}${p.cached ? ` 缓存${p.cached}` : ""} · 在跑 ${p.running} · ${(p.spentTokens / 1000).toFixed(0)}k${p.budgetTokens > 0 ? ` / ${(p.budgetTokens / 1000).toFixed(0)}k tokens` : ""}`,
97
+ },
98
+ ],
99
+ details: p,
100
+ });
101
+ }
102
+ : undefined;
103
+ const result = await runWorkflow({
104
+ code: code ?? "",
105
+ progressPath,
106
+ resume: params.resume === true,
107
+ parentModel: getParentModel(),
108
+ signal,
109
+ timeoutMs: Math.min(180, params.timeout_minutes ?? WORKFLOW_TIMEOUT_MS / 60_000) * 60_000,
110
+ budgetTokens: params.budget_tokens,
111
+ onProgress,
112
+ });
113
+ return {
114
+ content: [{ type: "text", text: truncate(result.report) }],
115
+ details: { ...result.details, scriptPath, workflowsDir: workflowsDir() },
116
+ };
117
+ },
118
+ });
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u1s1-cli",
3
- "version": "0.16.6",
3
+ "version": "0.18.0",
4
4
  "description": "u1s1 — 有一说一,最省心的 AI 编程搭子。终端里用中文说需求,AI 帮你读文件、改代码、跑命令。",
5
5
  "type": "module",
6
6
  "bin": {