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