minimal-agent 0.2.0 → 0.3.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/README.md +54 -72
- package/package.json +18 -13
- package/plugins/ralph-wiggum/plugin.js +205 -0
- package/plugins/ralph-wiggum/src/goalState.js +260 -0
- package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
- package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
- package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
- package/plugins/workflow-runner/commands/workflow.md +13 -3
- package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
- package/plugins/workflow-runner/src/expressions.js +369 -0
- package/plugins/workflow-runner/src/index.js +216 -0
- package/plugins/workflow-runner/src/loader.js +183 -0
- package/plugins/workflow-runner/src/runner.js +290 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
- package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
- package/plugins/workflow-runner/src/types.js +59 -0
- package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
- package/src/bootstrap/cwdArg.js +22 -0
- package/src/bootstrap/workingDir.js +31 -0
- package/src/cli/configWizard.js +272 -0
- package/src/cli/print.js +197 -0
- package/src/config/configFile.js +78 -0
- package/src/config.js +118 -0
- package/src/context/compact.js +357 -0
- package/src/context/microCompactLite.js +151 -0
- package/src/context/persistContext.js +109 -0
- package/src/context/reactiveCompact.js +121 -0
- package/src/context/sessionPath.js +58 -0
- package/src/context/snipCompact.js +112 -0
- package/src/context/tokenCounter.js +66 -0
- package/src/llm/client.js +182 -0
- package/src/loop.js +230 -0
- package/src/main.js +116 -0
- package/src/plugin-sdk.js +24 -0
- package/src/plugins/commandRouter.js +169 -0
- package/src/plugins/hookEngine.js +258 -0
- package/src/plugins/pluginApi.js +23 -0
- package/src/plugins/pluginLoader.js +71 -0
- package/src/plugins/pluginRunner.js +65 -0
- package/src/plugins/transcript.js +171 -0
- package/src/prompts/projectInstructions.js +48 -0
- package/src/prompts/skillList.js +126 -0
- package/src/prompts/system.js +155 -0
- package/src/session/runTurn.js +41 -0
- package/src/session/sessionState.js +19 -0
- package/src/tools/bash/bash.js +352 -0
- package/src/tools/bash/semantics.js +85 -0
- package/src/tools/bash/warnings.js +98 -0
- package/src/tools/edit/edit.js +253 -0
- package/src/tools/edit/multi-edit.js +155 -0
- package/src/tools/glob/glob.js +97 -0
- package/src/tools/grep/grep.js +185 -0
- package/src/tools/grep/rgPath.js +173 -0
- package/src/tools/index.js +94 -0
- package/src/tools/read/read.js +209 -0
- package/src/tools/shared/fileState.js +61 -0
- package/src/tools/shared/fileUtils.js +281 -0
- package/src/tools/shared/schemas.js +16 -0
- package/src/tools/types.js +21 -0
- package/src/tools/webbrowser/browser.js +55 -0
- package/src/tools/webbrowser/webbrowser.js +194 -0
- package/src/tools/webfetch/preapproved.js +267 -0
- package/src/tools/webfetch/webfetch.js +317 -0
- package/src/tools/websearch/websearch.js +161 -0
- package/src/tools/write/write.js +125 -0
- package/src/types/turndown.d.ts +23 -0
- package/src/types.js +16 -0
- package/src/ui/App.js +37 -0
- package/src/ui/InputBox.js +240 -0
- package/src/ui/MessageList.js +28 -0
- package/src/ui/Root.js +70 -0
- package/src/ui/StatusLine.js +41 -0
- package/src/ui/ToolStatus.js +11 -0
- package/src/ui/hooks/useChat.js +234 -0
- package/src/ui/hooks/usePasteHandler.js +137 -0
- package/src/ui/hooks/useTextBuffer.js +55 -0
- package/src/ui/hooks/useTokenUsage.js +30 -0
- package/src/ui/textBuffer.js +217 -0
- package/src/utils/packageRoot.js +37 -0
- package/src/utils/resourcePaths.js +49 -0
- package/src/utils/zodToJson.js +29 -0
- package/dist/main.js +0 -5315
- package/plugins/ralph-wiggum/plugin.ts +0 -275
- package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
- package/plugins/ralph-wiggum/src/goalState.ts +0 -310
- package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
- package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
- package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
- package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
- package/plugins/workflow-runner/src/expressions.ts +0 -371
- package/plugins/workflow-runner/src/index.ts +0 -194
- package/plugins/workflow-runner/src/loader.ts +0 -193
- package/plugins/workflow-runner/src/runner.ts +0 -313
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
- package/plugins/workflow-runner/src/types.ts +0 -183
- package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
- package/plugins/workflow-runner/test/e2e.test.ts +0 -268
- package/plugins/workflow-runner/test/expressions.test.ts +0 -140
- package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
- package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
- package/plugins/workflow-runner/test/graceful.test.ts +0 -139
- package/plugins/workflow-runner/test/loader.test.ts +0 -216
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
- package/plugins/workflow-runner/test/runner.test.ts +0 -511
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================
|
|
3
|
-
* src/workflows/expressions.ts —— workflow 表达式引擎(mini)
|
|
4
|
-
* ------------------------------------------------------------
|
|
5
|
-
* WHY 不直接 eval / Function?
|
|
6
|
-
* Workflow 来源不可信(用户编辑器生成 / 别人贡献的 yaml),允许任意
|
|
7
|
-
* JS 执行 = 任意远程代码执行。所以我们手写一个**只支持必要语法**的
|
|
8
|
-
* 递归下降解析器:常量、变量引用、点路径、字符串/数字字面量、数组、
|
|
9
|
-
* 比较、布尔运算、白名单内置函数。
|
|
10
|
-
*
|
|
11
|
-
* 支持的语法:
|
|
12
|
-
* expr := or
|
|
13
|
-
* or := and ( '||' and )*
|
|
14
|
-
* and := not ( '&&' not )*
|
|
15
|
-
* not := '!' not | comparison
|
|
16
|
-
* comparison := primary ( ('==' | '!=' | '>=' | '<=' | '>' | '<') primary )?
|
|
17
|
-
* primary := number | string | array | call | path | '(' expr ')'
|
|
18
|
-
* array := '[' expr (',' expr)* ']'
|
|
19
|
-
* call := IDENT '(' expr (',' expr)* ')' // 白名单
|
|
20
|
-
* path := IDENT ('.' IDENT)* // 变量 + 点路径
|
|
21
|
-
*
|
|
22
|
-
* 白名单函数:fileExists / length / lower / upper
|
|
23
|
-
*
|
|
24
|
-
* 插值:${EXPR} —— 把 EXPR 求值后 toString 替换;对象/数组用 JSON.stringify。
|
|
25
|
-
* interpolateDeep 递归处理对象/数组/字符串。
|
|
26
|
-
* ============================================================
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { existsSync } from 'node:fs';
|
|
30
|
-
import { join, isAbsolute } from 'node:path';
|
|
31
|
-
|
|
32
|
-
import { getWorkingDir } from '../../../src/plugin-sdk.ts';
|
|
33
|
-
import type { VarStack } from './types.ts';
|
|
34
|
-
|
|
35
|
-
// ---------------- Tokenizer ----------------
|
|
36
|
-
|
|
37
|
-
type Token =
|
|
38
|
-
| { kind: 'num'; value: number }
|
|
39
|
-
| { kind: 'str'; value: string }
|
|
40
|
-
| { kind: 'ident'; value: string }
|
|
41
|
-
| { kind: 'punct'; value: string };
|
|
42
|
-
|
|
43
|
-
const PUNCT2 = new Set(['==', '!=', '>=', '<=', '&&', '||']);
|
|
44
|
-
const PUNCT1 = new Set(['(', ')', '[', ']', ',', '.', '!', '>', '<']);
|
|
45
|
-
|
|
46
|
-
function tokenize(src: string): Token[] {
|
|
47
|
-
const out: Token[] = [];
|
|
48
|
-
let i = 0;
|
|
49
|
-
while (i < src.length) {
|
|
50
|
-
const c = src[i];
|
|
51
|
-
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
52
|
-
i++;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
// 字符串
|
|
56
|
-
if (c === '"' || c === "'") {
|
|
57
|
-
const quote = c;
|
|
58
|
-
let s = '';
|
|
59
|
-
i++;
|
|
60
|
-
while (i < src.length && src[i] !== quote) {
|
|
61
|
-
if (src[i] === '\\' && i + 1 < src.length) {
|
|
62
|
-
const nx = src[i + 1];
|
|
63
|
-
s += nx === 'n' ? '\n' : nx === 't' ? '\t' : nx;
|
|
64
|
-
i += 2;
|
|
65
|
-
} else {
|
|
66
|
-
s += src[i++];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (src[i] !== quote) throw new Error(`未闭合的字符串:${src}`);
|
|
70
|
-
i++;
|
|
71
|
-
out.push({ kind: 'str', value: s });
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
// 数字
|
|
75
|
-
if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9')) {
|
|
76
|
-
let j = i;
|
|
77
|
-
if (src[j] === '-') j++;
|
|
78
|
-
while (j < src.length && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.')) j++;
|
|
79
|
-
out.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
|
|
80
|
-
i = j;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
// 双字符标点
|
|
84
|
-
const two = src.slice(i, i + 2);
|
|
85
|
-
if (PUNCT2.has(two)) {
|
|
86
|
-
out.push({ kind: 'punct', value: two });
|
|
87
|
-
i += 2;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// 单字符标点
|
|
91
|
-
if (PUNCT1.has(c)) {
|
|
92
|
-
out.push({ kind: 'punct', value: c });
|
|
93
|
-
i++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
// 标识符
|
|
97
|
-
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_') {
|
|
98
|
-
let j = i;
|
|
99
|
-
while (
|
|
100
|
-
j < src.length &&
|
|
101
|
-
((src[j] >= 'a' && src[j] <= 'z') ||
|
|
102
|
-
(src[j] >= 'A' && src[j] <= 'Z') ||
|
|
103
|
-
(src[j] >= '0' && src[j] <= '9') ||
|
|
104
|
-
src[j] === '_')
|
|
105
|
-
) {
|
|
106
|
-
j++;
|
|
107
|
-
}
|
|
108
|
-
out.push({ kind: 'ident', value: src.slice(i, j) });
|
|
109
|
-
i = j;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
throw new Error(`非法字符 '${c}' 在表达式 "${src}"`);
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------- Parser → AST → eval(一次性递归下降,零 AST 对象) ----------------
|
|
118
|
-
|
|
119
|
-
interface Cursor {
|
|
120
|
-
toks: Token[];
|
|
121
|
-
pos: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function peek(c: Cursor): Token | null {
|
|
125
|
-
return c.pos < c.toks.length ? c.toks[c.pos] : null;
|
|
126
|
-
}
|
|
127
|
-
function eat(c: Cursor): Token {
|
|
128
|
-
return c.toks[c.pos++];
|
|
129
|
-
}
|
|
130
|
-
function isPunct(c: Cursor, v: string): boolean {
|
|
131
|
-
const t = peek(c);
|
|
132
|
-
return !!t && t.kind === 'punct' && t.value === v;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const BUILTINS: Record<string, (args: unknown[]) => unknown> = {
|
|
136
|
-
fileExists(args) {
|
|
137
|
-
const p = String(args[0] ?? '');
|
|
138
|
-
if (!p) return false;
|
|
139
|
-
const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
|
|
140
|
-
return existsSync(abs);
|
|
141
|
-
},
|
|
142
|
-
length(args) {
|
|
143
|
-
const v = args[0];
|
|
144
|
-
if (typeof v === 'string' || Array.isArray(v)) return v.length;
|
|
145
|
-
if (v && typeof v === 'object') return Object.keys(v).length;
|
|
146
|
-
return 0;
|
|
147
|
-
},
|
|
148
|
-
lower(args) {
|
|
149
|
-
return String(args[0] ?? '').toLowerCase();
|
|
150
|
-
},
|
|
151
|
-
upper(args) {
|
|
152
|
-
return String(args[0] ?? '').toUpperCase();
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
function parseOr(c: Cursor, vars: VarStack): unknown {
|
|
157
|
-
let left = parseAnd(c, vars);
|
|
158
|
-
while (isPunct(c, '||')) {
|
|
159
|
-
eat(c);
|
|
160
|
-
const right = parseAnd(c, vars);
|
|
161
|
-
left = truthy(left) || truthy(right);
|
|
162
|
-
}
|
|
163
|
-
return left;
|
|
164
|
-
}
|
|
165
|
-
function parseAnd(c: Cursor, vars: VarStack): unknown {
|
|
166
|
-
let left = parseNot(c, vars);
|
|
167
|
-
while (isPunct(c, '&&')) {
|
|
168
|
-
eat(c);
|
|
169
|
-
const right = parseNot(c, vars);
|
|
170
|
-
left = truthy(left) && truthy(right);
|
|
171
|
-
}
|
|
172
|
-
return left;
|
|
173
|
-
}
|
|
174
|
-
function parseNot(c: Cursor, vars: VarStack): unknown {
|
|
175
|
-
if (isPunct(c, '!')) {
|
|
176
|
-
eat(c);
|
|
177
|
-
return !truthy(parseNot(c, vars));
|
|
178
|
-
}
|
|
179
|
-
return parseComparison(c, vars);
|
|
180
|
-
}
|
|
181
|
-
function parseComparison(c: Cursor, vars: VarStack): unknown {
|
|
182
|
-
const left = parsePrimary(c, vars);
|
|
183
|
-
const t = peek(c);
|
|
184
|
-
if (t && t.kind === 'punct' && ['==', '!=', '>=', '<=', '>', '<'].includes(t.value)) {
|
|
185
|
-
eat(c);
|
|
186
|
-
const right = parsePrimary(c, vars);
|
|
187
|
-
switch (t.value) {
|
|
188
|
-
case '==':
|
|
189
|
-
// eslint-disable-next-line eqeqeq
|
|
190
|
-
return left == right;
|
|
191
|
-
case '!=':
|
|
192
|
-
// eslint-disable-next-line eqeqeq
|
|
193
|
-
return left != right;
|
|
194
|
-
case '>':
|
|
195
|
-
return (left as number) > (right as number);
|
|
196
|
-
case '<':
|
|
197
|
-
return (left as number) < (right as number);
|
|
198
|
-
case '>=':
|
|
199
|
-
return (left as number) >= (right as number);
|
|
200
|
-
case '<=':
|
|
201
|
-
return (left as number) <= (right as number);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return left;
|
|
205
|
-
}
|
|
206
|
-
function parsePrimary(c: Cursor, vars: VarStack): unknown {
|
|
207
|
-
const t = peek(c);
|
|
208
|
-
if (!t) throw new Error('表达式意外结束');
|
|
209
|
-
|
|
210
|
-
if (t.kind === 'punct' && t.value === '(') {
|
|
211
|
-
eat(c);
|
|
212
|
-
const v = parseOr(c, vars);
|
|
213
|
-
if (!isPunct(c, ')')) throw new Error("缺少 ')'");
|
|
214
|
-
eat(c);
|
|
215
|
-
return v;
|
|
216
|
-
}
|
|
217
|
-
if (t.kind === 'punct' && t.value === '[') {
|
|
218
|
-
eat(c);
|
|
219
|
-
const arr: unknown[] = [];
|
|
220
|
-
if (!isPunct(c, ']')) {
|
|
221
|
-
arr.push(parseOr(c, vars));
|
|
222
|
-
while (isPunct(c, ',')) {
|
|
223
|
-
eat(c);
|
|
224
|
-
arr.push(parseOr(c, vars));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (!isPunct(c, ']')) throw new Error("缺少 ']'");
|
|
228
|
-
eat(c);
|
|
229
|
-
return arr;
|
|
230
|
-
}
|
|
231
|
-
if (t.kind === 'num') {
|
|
232
|
-
eat(c);
|
|
233
|
-
return t.value;
|
|
234
|
-
}
|
|
235
|
-
if (t.kind === 'str') {
|
|
236
|
-
eat(c);
|
|
237
|
-
return t.value;
|
|
238
|
-
}
|
|
239
|
-
if (t.kind === 'ident') {
|
|
240
|
-
eat(c);
|
|
241
|
-
// 函数调用?
|
|
242
|
-
if (isPunct(c, '(')) {
|
|
243
|
-
eat(c);
|
|
244
|
-
const argv: unknown[] = [];
|
|
245
|
-
if (!isPunct(c, ')')) {
|
|
246
|
-
argv.push(parseOr(c, vars));
|
|
247
|
-
while (isPunct(c, ',')) {
|
|
248
|
-
eat(c);
|
|
249
|
-
argv.push(parseOr(c, vars));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (!isPunct(c, ')')) throw new Error("缺少 ')'");
|
|
253
|
-
eat(c);
|
|
254
|
-
const fn = BUILTINS[t.value];
|
|
255
|
-
if (!fn) throw new Error(`未知函数 "${t.value}"。白名单:${Object.keys(BUILTINS).join(', ')}`);
|
|
256
|
-
return fn(argv);
|
|
257
|
-
}
|
|
258
|
-
// 字面常量
|
|
259
|
-
if (t.value === 'true') return true;
|
|
260
|
-
if (t.value === 'false') return false;
|
|
261
|
-
if (t.value === 'null') return null;
|
|
262
|
-
// 变量 + 点路径
|
|
263
|
-
let cur: unknown = vars.get(t.value);
|
|
264
|
-
while (isPunct(c, '.')) {
|
|
265
|
-
eat(c);
|
|
266
|
-
const next = eat(c);
|
|
267
|
-
if (next.kind !== 'ident') throw new Error('点号后必须是标识符');
|
|
268
|
-
cur = cur != null && typeof cur === 'object' ? (cur as Record<string, unknown>)[next.value] : undefined;
|
|
269
|
-
}
|
|
270
|
-
return cur;
|
|
271
|
-
}
|
|
272
|
-
throw new Error(`无法解析 token: ${JSON.stringify(t)}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function truthy(v: unknown): boolean {
|
|
276
|
-
if (v === null || v === undefined) return false;
|
|
277
|
-
if (typeof v === 'boolean') return v;
|
|
278
|
-
if (typeof v === 'number') return v !== 0 && !Number.isNaN(v);
|
|
279
|
-
if (typeof v === 'string') return v.length > 0;
|
|
280
|
-
if (Array.isArray(v)) return v.length > 0;
|
|
281
|
-
if (typeof v === 'object') return Object.keys(v).length > 0;
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ---------------- 对外 API ----------------
|
|
286
|
-
|
|
287
|
-
/** 计算表达式(裸的,不带 ${} 包裹)。失败抛错。 */
|
|
288
|
-
export function evalExpr(expr: string, vars: VarStack): unknown {
|
|
289
|
-
const toks = tokenize(expr);
|
|
290
|
-
if (toks.length === 0) return undefined;
|
|
291
|
-
const cursor: Cursor = { toks, pos: 0 };
|
|
292
|
-
const v = parseOr(cursor, vars);
|
|
293
|
-
if (cursor.pos !== toks.length) {
|
|
294
|
-
throw new Error(`表达式多余 token: "${expr}"`);
|
|
295
|
-
}
|
|
296
|
-
return v;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* 字符串插值:把 "${EXPR}" 替换成 EXPR 求值结果。
|
|
301
|
-
* 多个 ${} 串联也支持。括号配对识别 —— "${ ${nested} }" 会被当成一个外层
|
|
302
|
-
* EXPR(内部 ${nested} 文本原样,但表达式里不允许 ${} 语法,所以会报错)。
|
|
303
|
-
*
|
|
304
|
-
* 转义:`\$` → 字面 $,跳过插值。
|
|
305
|
-
*/
|
|
306
|
-
export function interpolate(template: string, vars: VarStack): string {
|
|
307
|
-
let out = '';
|
|
308
|
-
let i = 0;
|
|
309
|
-
while (i < template.length) {
|
|
310
|
-
const c = template[i];
|
|
311
|
-
if (c === '\\' && template[i + 1] === '$') {
|
|
312
|
-
out += '$';
|
|
313
|
-
i += 2;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (c === '$' && template[i + 1] === '{') {
|
|
317
|
-
// 找匹配的 '}'
|
|
318
|
-
let depth = 1;
|
|
319
|
-
let j = i + 2;
|
|
320
|
-
while (j < template.length && depth > 0) {
|
|
321
|
-
if (template[j] === '{') depth++;
|
|
322
|
-
else if (template[j] === '}') depth--;
|
|
323
|
-
if (depth > 0) j++;
|
|
324
|
-
}
|
|
325
|
-
if (depth !== 0) {
|
|
326
|
-
// 不匹配;原样输出
|
|
327
|
-
out += template.slice(i);
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
const expr = template.slice(i + 2, j);
|
|
331
|
-
const v = evalExpr(expr, vars);
|
|
332
|
-
out += stringify(v);
|
|
333
|
-
i = j + 1;
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
out += c;
|
|
337
|
-
i++;
|
|
338
|
-
}
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function stringify(v: unknown): string {
|
|
343
|
-
if (v === null || v === undefined) return '';
|
|
344
|
-
if (typeof v === 'string') return v;
|
|
345
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
346
|
-
try {
|
|
347
|
-
return JSON.stringify(v);
|
|
348
|
-
} catch {
|
|
349
|
-
return String(v);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** 递归对对象/数组/字符串做插值;其它原样返回。 */
|
|
354
|
-
export function interpolateDeep<T>(value: T, vars: VarStack): T {
|
|
355
|
-
if (typeof value === 'string') {
|
|
356
|
-
return interpolate(value, vars) as unknown as T;
|
|
357
|
-
}
|
|
358
|
-
if (Array.isArray(value)) {
|
|
359
|
-
return value.map((v) => interpolateDeep(v, vars)) as unknown as T;
|
|
360
|
-
}
|
|
361
|
-
if (value && typeof value === 'object') {
|
|
362
|
-
const out: Record<string, unknown> = {};
|
|
363
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
364
|
-
// 注意:key 也插值(loop 里 `hook_${idx}` 这种),但只对字符串 key 有意义
|
|
365
|
-
const newKey = interpolate(k, vars);
|
|
366
|
-
out[newKey] = interpolateDeep(v, vars);
|
|
367
|
-
}
|
|
368
|
-
return out as unknown as T;
|
|
369
|
-
}
|
|
370
|
-
return value;
|
|
371
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================
|
|
3
|
-
* src/workflows/index.ts —— /workflow 与 /workflows 命令的胶水层
|
|
4
|
-
* ------------------------------------------------------------
|
|
5
|
-
* 作为 pluginRunner 与 runner 之间的桥:
|
|
6
|
-
* - runWorkflowFromCommand:解析 /workflow <name> --input k=v args 调度
|
|
7
|
-
* - runWorkflowsList:处理 /workflows 列表展示(yield "text" 事件让 UI 显示)
|
|
8
|
-
*
|
|
9
|
-
* R1 红线:本文件只在 pluginRunner 命中 workflow 模式时才被 import。
|
|
10
|
-
* 不污染默认 T-A-O-R 路径。
|
|
11
|
-
* ============================================================
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { findWorkflowByName, listWorkflows } from './loader.ts';
|
|
15
|
-
import { runWorkflow } from './runner.ts';
|
|
16
|
-
import type { InputDef, RunContext, WorkflowEvent } from './types.ts';
|
|
17
|
-
import type { LoopEvent, Message, Provider } from '../../../src/plugin-sdk.ts';
|
|
18
|
-
|
|
19
|
-
export interface RunWorkflowFromCommandOptions {
|
|
20
|
-
provider: Provider;
|
|
21
|
-
history: Message[];
|
|
22
|
-
signal?: AbortSignal;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 把 "/workflow <name> --input k=v --input k2=v2" 拆成 name + inputs。
|
|
27
|
-
* 简单解析器(不依赖第三方):
|
|
28
|
-
* - 第一个非 flag token = name
|
|
29
|
-
* - --input k=v 重复
|
|
30
|
-
* - 不识别其他 flag(P0 不报错,仅 console.warn)
|
|
31
|
-
*/
|
|
32
|
-
export function parseWorkflowArgs(rawArgs: string): {
|
|
33
|
-
name: string | null;
|
|
34
|
-
inputs: Record<string, string>;
|
|
35
|
-
} {
|
|
36
|
-
const tokens = tokenizeArgs(rawArgs);
|
|
37
|
-
const inputs: Record<string, string> = {};
|
|
38
|
-
let name: string | null = null;
|
|
39
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
40
|
-
const t = tokens[i];
|
|
41
|
-
if (t === '--input') {
|
|
42
|
-
const kv = tokens[i + 1];
|
|
43
|
-
if (!kv) break;
|
|
44
|
-
const eq = kv.indexOf('=');
|
|
45
|
-
if (eq > 0) {
|
|
46
|
-
inputs[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
47
|
-
}
|
|
48
|
-
i++;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (t.startsWith('--')) {
|
|
52
|
-
// 未知 flag:跳过其后一个 token(保守地认为它带值)
|
|
53
|
-
i++;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (name === null) {
|
|
57
|
-
name = t;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return { name, inputs };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 简单 token 化:双引号包裹的整体当一个 token;其它按空白切。 */
|
|
64
|
-
function tokenizeArgs(s: string): string[] {
|
|
65
|
-
const out: string[] = [];
|
|
66
|
-
let i = 0;
|
|
67
|
-
while (i < s.length) {
|
|
68
|
-
while (i < s.length && /\s/.test(s[i])) i++;
|
|
69
|
-
if (i >= s.length) break;
|
|
70
|
-
if (s[i] === '"' || s[i] === "'") {
|
|
71
|
-
const q = s[i];
|
|
72
|
-
i++;
|
|
73
|
-
let acc = '';
|
|
74
|
-
while (i < s.length && s[i] !== q) {
|
|
75
|
-
acc += s[i++];
|
|
76
|
-
}
|
|
77
|
-
if (i < s.length) i++;
|
|
78
|
-
out.push(acc);
|
|
79
|
-
} else {
|
|
80
|
-
let acc = '';
|
|
81
|
-
while (i < s.length && !/\s/.test(s[i])) {
|
|
82
|
-
acc += s[i++];
|
|
83
|
-
}
|
|
84
|
-
out.push(acc);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 把字符串形态的 input 值按 InputDef.type 转成合适的 JS 类型 + 校验。
|
|
92
|
-
* 校验失败抛 Error,pluginRunner 会把它转成 error event。
|
|
93
|
-
*/
|
|
94
|
-
function coerceInputs(
|
|
95
|
-
raw: Record<string, string>,
|
|
96
|
-
defs: InputDef[] | undefined,
|
|
97
|
-
): Record<string, unknown> {
|
|
98
|
-
const out: Record<string, unknown> = {};
|
|
99
|
-
if (!defs) return { ...raw };
|
|
100
|
-
|
|
101
|
-
for (const def of defs) {
|
|
102
|
-
const rawVal = raw[def.name];
|
|
103
|
-
if (rawVal === undefined) {
|
|
104
|
-
if (def.required) {
|
|
105
|
-
throw new Error(`缺少必填 input: ${def.name}`);
|
|
106
|
-
}
|
|
107
|
-
if (def.default !== undefined) {
|
|
108
|
-
out[def.name] = def.default;
|
|
109
|
-
}
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (def.type === 'number') {
|
|
113
|
-
const n = parseFloat(rawVal);
|
|
114
|
-
if (Number.isNaN(n)) throw new Error(`input ${def.name} 必须是数字,收到 "${rawVal}"`);
|
|
115
|
-
out[def.name] = n;
|
|
116
|
-
} else if (def.type === 'enum') {
|
|
117
|
-
if (!def.values?.includes(rawVal)) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`input ${def.name} 必须是 [${def.values?.join(', ')}] 之一,收到 "${rawVal}"`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
out[def.name] = rawVal;
|
|
123
|
-
} else {
|
|
124
|
-
out[def.name] = rawVal;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// 未声明的 input 也透传(让 yaml 里可以引用 ad-hoc 变量)
|
|
128
|
-
for (const k of Object.keys(raw)) {
|
|
129
|
-
if (!(k in out)) out[k] = raw[k];
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** /workflow <name> [--input k=v] —— 主入口,由 pluginRunner 调用。 */
|
|
135
|
-
export async function* runWorkflowFromCommand(
|
|
136
|
-
rawArgs: string,
|
|
137
|
-
opts: RunWorkflowFromCommandOptions,
|
|
138
|
-
): AsyncGenerator<WorkflowEvent | LoopEvent, void, void> {
|
|
139
|
-
const { name, inputs: rawInputs } = parseWorkflowArgs(rawArgs);
|
|
140
|
-
if (!name) {
|
|
141
|
-
yield {
|
|
142
|
-
type: 'error',
|
|
143
|
-
error:
|
|
144
|
-
'/workflow 用法: /workflow <name> [--input key=val ...]\n输入 /workflows 查看可用工作流。',
|
|
145
|
-
};
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const def = await findWorkflowByName(name);
|
|
149
|
-
if (!def) {
|
|
150
|
-
yield {
|
|
151
|
-
type: 'error',
|
|
152
|
-
error: `未找到 workflow "${name}"。输入 /workflows 查看可用列表。`,
|
|
153
|
-
};
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let coerced: Record<string, unknown>;
|
|
158
|
-
try {
|
|
159
|
-
coerced = coerceInputs(rawInputs, def.inputs);
|
|
160
|
-
} catch (e) {
|
|
161
|
-
yield { type: 'error', error: (e as Error).message };
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ctx: RunContext = {
|
|
166
|
-
provider: opts.provider,
|
|
167
|
-
signal: opts.signal,
|
|
168
|
-
history: opts.history,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
yield* runWorkflow(def, coerced, ctx);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** /workflows —— 列出可用 workflow。yield 一条 text 事件让 UI 显示。 */
|
|
175
|
-
export async function* runWorkflowsList(): AsyncGenerator<LoopEvent, void, void> {
|
|
176
|
-
const list = await listWorkflows();
|
|
177
|
-
if (list.length === 0) {
|
|
178
|
-
yield {
|
|
179
|
-
type: 'text',
|
|
180
|
-
delta:
|
|
181
|
-
'当前没有可用工作流。在 cwd/workflows/ 下放 *.yaml 即可(参考 workflows/schema.json 或 docs/workflow-design.md)。\n',
|
|
182
|
-
};
|
|
183
|
-
yield { type: 'turn_done' };
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const lines = ['可用工作流:'];
|
|
187
|
-
for (const w of list) {
|
|
188
|
-
lines.push(` /workflow ${w.name} — ${w.description.split('\n')[0].trim()}`);
|
|
189
|
-
}
|
|
190
|
-
lines.push('');
|
|
191
|
-
lines.push('运行示例: /workflow <name> --input key=val');
|
|
192
|
-
yield { type: 'text', delta: lines.join('\n') + '\n' };
|
|
193
|
-
yield { type: 'turn_done' };
|
|
194
|
-
}
|