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
@@ -13,29 +13,23 @@
13
13
  * P1 可扩展按工具特定字段(如 Read 的 fileSize)。
14
14
  * ============================================================
15
15
  */
16
-
17
- import { executeTool, getToolByName } from '../../../../src/plugin-sdk.ts';
18
- import { interpolateDeep } from '../expressions.ts';
19
- import type { RunContext, StepDef, StepResult, VarStack } from '../types.ts';
20
-
21
- export async function execToolStep(
22
- step: StepDef,
23
- vars: VarStack,
24
- ctx: RunContext,
25
- ): Promise<StepResult> {
26
- if (!step.tool) throw new Error(`step ${step.id}: 缺少 tool 字段`);
27
- const tool = getToolByName(step.tool);
28
- if (!tool) throw new Error(`step ${step.id}: 未知 tool "${step.tool}"`);
29
-
30
- const args = interpolateDeep(step.args ?? {}, vars);
31
- const argsJson = JSON.stringify(args);
32
- const result = await executeTool(tool.name, argsJson, ctx.signal);
33
- if (!result.ok) {
34
- throw new Error(result.error);
35
- }
36
- const content = result.content;
37
- return {
38
- raw: { content, result: content, args },
39
- preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
40
- };
16
+ import { executeTool, getToolByName } from '../../../../src/plugin-sdk.js';
17
+ import { interpolateDeep } from '../expressions.js';
18
+ export async function execToolStep(step, vars, ctx) {
19
+ if (!step.tool)
20
+ throw new Error(`step ${step.id}: 缺少 tool 字段`);
21
+ const tool = getToolByName(step.tool);
22
+ if (!tool)
23
+ throw new Error(`step ${step.id}: 未知 tool "${step.tool}"`);
24
+ const args = interpolateDeep(step.args ?? {}, vars);
25
+ const argsJson = JSON.stringify(args);
26
+ const result = await executeTool(tool.name, argsJson, ctx.signal);
27
+ if (!result.ok) {
28
+ throw new Error(result.error);
29
+ }
30
+ const content = result.content;
31
+ return {
32
+ raw: { content, result: content, args },
33
+ preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
34
+ };
41
35
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ============================================================
3
+ * src/workflows/types.ts —— workflow runner 核心类型
4
+ * ------------------------------------------------------------
5
+ * ralph-loop 让 LLM 自规划;workflow 反过来 —— 外层确定性 step 列表
6
+ * 驱动执行,LLM 只在 llm: / skill: 节点出场。本文件定义 YAML 解析
7
+ * 后的内存形态(WorkflowDef / StepDef),以及运行时上下文(VarStack /
8
+ * RunContext)和 UI 事件(WorkflowEvent)。
9
+ *
10
+ * 与 src/types.ts 关系:WorkflowEvent 是插件**私有**事件,结构上满足
11
+ * framework 的 PluginEvent 开放契约(type: string + 任意 payload),
12
+ * 经 plugin.ts -> pluginRunner 原样透传给 UI;UI 不识别即静默忽略。
13
+ * src/types.ts 的 LoopEvent union **不**包含 workflow_*,框架零插件耦合。
14
+ * ============================================================
15
+ */
16
+ // ---------------- 2. 变量栈(loop scope 用) ----------------
17
+ /**
18
+ * 简单的多帧变量栈:
19
+ * - 最外层(frame 0)放 inputs + 全局变量
20
+ * - 进入 loop 时 push() 新帧,存当前迭代的 as / as_idx
21
+ * - 退出 loop 时 pop()
22
+ *
23
+ * get(key) 从栈顶往下找第一个匹配的 frame,类似 JS 的 lexical scope。
24
+ */
25
+ export class VarStack {
26
+ frames = [{}];
27
+ set(key, val) {
28
+ this.frames[this.frames.length - 1][key] = val;
29
+ }
30
+ /** 写到栈底(root frame),用于 inputs / 全局常量 */
31
+ setGlobal(key, val) {
32
+ this.frames[0][key] = val;
33
+ }
34
+ get(key) {
35
+ for (let i = this.frames.length - 1; i >= 0; i--) {
36
+ if (key in this.frames[i])
37
+ return this.frames[i][key];
38
+ }
39
+ return undefined;
40
+ }
41
+ has(key) {
42
+ for (let i = this.frames.length - 1; i >= 0; i--) {
43
+ if (key in this.frames[i])
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ push() {
49
+ this.frames.push({});
50
+ }
51
+ pop() {
52
+ if (this.frames.length > 1)
53
+ this.frames.pop();
54
+ }
55
+ /** 合并所有帧为一个普通对象(外层覆盖内层),用于 vars 持久化与事件 */
56
+ snapshot() {
57
+ return Object.assign({}, ...this.frames);
58
+ }
59
+ }
@@ -18,48 +18,29 @@
18
18
  * 因为执行流程是确定性的,没有 LLM 自规划过程。
19
19
  * ============================================================
20
20
  */
21
-
22
21
  import { mkdir, writeFile, appendFile, rm } from 'node:fs/promises';
23
22
  import { join } from 'node:path';
24
-
25
23
  const STATE_DIR = '.minimal-agent-workflow';
26
-
27
24
  export class WorkflowState {
28
- readonly dir: string;
29
- constructor(workingDir: string) {
30
- this.dir = join(workingDir, STATE_DIR);
31
- }
32
-
33
- async init(name: string, inputs: Record<string, unknown>): Promise<void> {
34
- await rm(this.dir, { recursive: true, force: true });
35
- await mkdir(this.dir, { recursive: true });
36
- await writeFile(
37
- join(this.dir, 'current.json'),
38
- JSON.stringify({ name, startedAt: new Date().toISOString() }, null, 2),
39
- 'utf8',
40
- );
41
- await writeFile(
42
- join(this.dir, 'inputs.json'),
43
- JSON.stringify(inputs, null, 2),
44
- 'utf8',
45
- );
46
- await writeFile(join(this.dir, 'progress.md'), `# ${name}\n\n`, 'utf8');
47
- }
48
-
49
- async appendProgress(line: string): Promise<void> {
50
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
51
- await appendFile(join(this.dir, 'progress.md'), `- ${ts} ${line}\n`, 'utf8').catch(() => {});
52
- }
53
-
54
- async writeVars(snapshot: Record<string, unknown>): Promise<void> {
55
- await writeFile(
56
- join(this.dir, 'vars.json'),
57
- JSON.stringify(snapshot, null, 2),
58
- 'utf8',
59
- ).catch(() => {});
60
- }
61
-
62
- async cleanup(): Promise<void> {
63
- await rm(this.dir, { recursive: true, force: true }).catch(() => {});
64
- }
25
+ dir;
26
+ constructor(workingDir) {
27
+ this.dir = join(workingDir, STATE_DIR);
28
+ }
29
+ async init(name, inputs) {
30
+ await rm(this.dir, { recursive: true, force: true });
31
+ await mkdir(this.dir, { recursive: true });
32
+ await writeFile(join(this.dir, 'current.json'), JSON.stringify({ name, startedAt: new Date().toISOString() }, null, 2), 'utf8');
33
+ await writeFile(join(this.dir, 'inputs.json'), JSON.stringify(inputs, null, 2), 'utf8');
34
+ await writeFile(join(this.dir, 'progress.md'), `# ${name}\n\n`, 'utf8');
35
+ }
36
+ async appendProgress(line) {
37
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
38
+ await appendFile(join(this.dir, 'progress.md'), `- ${ts} ${line}\n`, 'utf8').catch(() => { });
39
+ }
40
+ async writeVars(snapshot) {
41
+ await writeFile(join(this.dir, 'vars.json'), JSON.stringify(snapshot, null, 2), 'utf8').catch(() => { });
42
+ }
43
+ async cleanup() {
44
+ await rm(this.dir, { recursive: true, force: true }).catch(() => { });
45
+ }
65
46
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ============================================================
3
+ * src/bootstrap/cwdArg.ts —— `-d` / `--cwd` 参数解析
4
+ * ------------------------------------------------------------
5
+ * 抽出来是为了让 main.tsx 之外能 import 这个纯函数做单测,
6
+ * 而不会触发 main.tsx 顶层的 main().catch(...) 副作用。
7
+ *
8
+ * 使用约定:在 initWorkingDir() 之前调一次。匹配到则建目录 + chdir。
9
+ * ============================================================
10
+ */
11
+ /**
12
+ * 扫 argv 找 `-d <dir>` / `--cwd <dir>`;只取第一个匹配。
13
+ * 找不到返回 null。
14
+ */
15
+ export function extractCwdArg(argv) {
16
+ for (let i = 0; i < argv.length; i++) {
17
+ if (argv[i] === '-d' || argv[i] === '--cwd') {
18
+ return argv[i + 1] ?? null;
19
+ }
20
+ }
21
+ return null;
22
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ============================================================
3
+ * src/bootstrap/workingDir.ts —— 工作目录管理
4
+ * ------------------------------------------------------------
5
+ * 启动时一次性捕获 process.cwd(),后续所有"操作目录"统一通过
6
+ * getWorkingDir() 读取,杜绝多处 process.cwd() 不一致的隐患。
7
+ *
8
+ * 设计选择:
9
+ * - 模块级 state:单进程单 cwd,不需要 AsyncLocalStorage
10
+ * - 锁定时机:main() 最开头,在加载 config / provider / history 之前
11
+ * - 环境变量覆盖 MINIMAL_AGENT_CWD:测试 / docker / 显式 cd 场景
12
+ * ============================================================
13
+ */
14
+ import { resolve } from 'node:path';
15
+ let _workingDir = null;
16
+ /** 首次调用锁定 cwd(解析 MINIMAL_AGENT_CWD),重复调用幂等。 */
17
+ export function initWorkingDir() {
18
+ if (_workingDir !== null)
19
+ return _workingDir;
20
+ const override = process.env.MINIMAL_AGENT_CWD;
21
+ _workingDir = resolve(override ?? process.cwd());
22
+ return _workingDir;
23
+ }
24
+ /** 读取已锁定的工作目录;尚未 init 时退到 lazy init(主要给测试)。 */
25
+ export function getWorkingDir() {
26
+ return _workingDir ?? initWorkingDir();
27
+ }
28
+ /** 仅供测试:重置 module-level state */
29
+ export function _resetWorkingDir() {
30
+ _workingDir = null;
31
+ }
@@ -0,0 +1,272 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================
4
+ * src/cli/configWizard.tsx —— 首次启动配置向导(Ink TUI)
5
+ * ------------------------------------------------------------
6
+ * 当 loadProviderLayered() 返回 null(env 和 ~/.minimal-agent/config.json 都没有
7
+ * 完整 provider 配置)时,由 Root.tsx 渲染本组件。
8
+ *
9
+ * 流程:
10
+ * Step 1: 选 provider 预设(minimax / openai / deepseek / kimi / custom)
11
+ * Step 2: 输入 baseURL(预设有默认值,用户可改)
12
+ * Step 3: 输入 apiKey(不回显,用 * 替代)
13
+ * Step 4: 输入 model(预设有候选)
14
+ * Step 5: 连接测试(POST /chat/completions max_tokens=5)
15
+ * - 通过 → saveConfig + onSuccess(provider) → Root 切到 App
16
+ * - 失败 → 显示错误 + 回到 Step 1(保留已输入的值方便修改)
17
+ *
18
+ * 设计取舍:
19
+ * - 不引第三方 prompt 库(@inquirer/prompts 等),用 Ink + useInput 手写,
20
+ * 保持"零新增依赖"承诺。
21
+ * - 不做 spinner 动画,loading 时显示一行文字够用。
22
+ * - 不做"返回上一步",要修改请按 ESC 退出向导重跑(简化键位逻辑)。
23
+ * —— 用户中途退出 = 不写文件 = 下次启动继续向导,符合需求 "直到成功为止"。
24
+ * ============================================================
25
+ */
26
+ import { useCallback, useMemo, useState } from 'react';
27
+ import { Box, Text, useApp, useInput } from 'ink';
28
+ import { saveConfig } from '../config/configFile.js';
29
+ // 兜底值:custom 或表外 model 都退到这里。
30
+ // 与 src/config.ts 的同名常量含义对齐("既没 env、saved 也没填 contextWindow" 的最终兜底)。
31
+ const FALLBACK_CONTEXT_WINDOW = 128_000;
32
+ const PRESETS = [
33
+ {
34
+ name: 'minimax',
35
+ label: 'MiniMax (海螺)',
36
+ baseURL: 'https://api.minimax.chat/v1',
37
+ models: ['MiniMax-M2.7', 'MiniMax-M1', 'abab6.5s-chat'],
38
+ contextWindow: 204_800,
39
+ },
40
+ {
41
+ name: 'deepseek',
42
+ label: 'DeepSeek',
43
+ baseURL: 'https://api.deepseek.com/v1',
44
+ models: ['deepseek-chat', 'deepseek-reasoner'],
45
+ contextWindow: 128_000,
46
+ },
47
+ {
48
+ name: 'openai',
49
+ label: 'OpenAI',
50
+ baseURL: 'https://api.openai.com/v1',
51
+ models: ['gpt-4o', 'gpt-4o-mini'],
52
+ contextWindow: 128_000,
53
+ },
54
+ {
55
+ name: 'moonshot',
56
+ label: 'Moonshot (Kimi)',
57
+ baseURL: 'https://api.moonshot.cn/v1',
58
+ models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'],
59
+ contextWindow: 128_000,
60
+ modelContextWindows: {
61
+ 'moonshot-v1-8k': 8_000,
62
+ 'moonshot-v1-32k': 32_000,
63
+ 'moonshot-v1-128k': 128_000,
64
+ },
65
+ },
66
+ {
67
+ name: 'custom',
68
+ label: '自定义(手动填 baseURL)',
69
+ baseURL: '',
70
+ models: [],
71
+ contextWindow: FALLBACK_CONTEXT_WINDOW,
72
+ },
73
+ ];
74
+ function resolveContextWindow(preset, model) {
75
+ return (preset.modelContextWindows?.[model] ??
76
+ preset.contextWindow ??
77
+ FALLBACK_CONTEXT_WINDOW);
78
+ }
79
+ export function ConfigWizard({ onSuccess }) {
80
+ const { exit } = useApp();
81
+ const [step, setStep] = useState('preset');
82
+ const [presetIndex, setPresetIndex] = useState(0);
83
+ const [baseURL, setBaseURL] = useState('');
84
+ const [apiKey, setApiKey] = useState('');
85
+ const [model, setModel] = useState('');
86
+ const [modelIndex, setModelIndex] = useState(0);
87
+ const [draft, setDraft] = useState('');
88
+ const [errorMsg, setErrorMsg] = useState(null);
89
+ const preset = PRESETS[presetIndex];
90
+ const enterStep = useCallback((next) => {
91
+ setErrorMsg(null);
92
+ if (next === 'baseURL')
93
+ setDraft(baseURL || preset.baseURL);
94
+ if (next === 'apiKey')
95
+ setDraft('');
96
+ if (next === 'model') {
97
+ if (preset.models.length > 0) {
98
+ setDraft(model || preset.models[0]);
99
+ setModelIndex(0);
100
+ }
101
+ else {
102
+ setDraft(model);
103
+ }
104
+ }
105
+ setStep(next);
106
+ }, [baseURL, model, preset]);
107
+ // handleTest 接受 finalModel 作为参数:
108
+ // Step 4 里调用方 `setModel(...) + handleTest()` 时,setModel 是异步的,handleTest
109
+ // 的 closure 还引用着旧的 model state;直接传 finalModel 才能避免"首次必跑两轮"
110
+ // —— 第一次发请求时 model 还是空串,API 报错,回到 Step 1 再走一遍才能成功。
111
+ //
112
+ // 测试通过后 **不立即** saveConfig + onSuccess,而是转去 Step 5(tavily)
113
+ // 让用户决定是否一并填 Tavily key,最后在 finalize() 一次性落盘。
114
+ const handleTest = useCallback(async (finalModel) => {
115
+ setStep('testing');
116
+ setErrorMsg(null);
117
+ try {
118
+ const url = `${baseURL.replace(/\/$/, '')}/chat/completions`;
119
+ const resp = await fetch(url, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'Content-Type': 'application/json',
123
+ Authorization: `Bearer ${apiKey}`,
124
+ },
125
+ body: JSON.stringify({
126
+ model: finalModel,
127
+ messages: [{ role: 'user', content: 'ping' }],
128
+ max_tokens: 5,
129
+ stream: false,
130
+ }),
131
+ });
132
+ if (!resp.ok) {
133
+ const body = await resp.text().catch(() => '');
134
+ throw new Error(`HTTP ${resp.status}: ${body.slice(0, 200) || resp.statusText}`);
135
+ }
136
+ const data = (await resp.json());
137
+ if (!Array.isArray(data.choices)) {
138
+ throw new Error('响应中没有 choices 字段,可能不是 OpenAI 兼容协议');
139
+ }
140
+ setDraft('');
141
+ setStep('tavily');
142
+ }
143
+ catch (e) {
144
+ setErrorMsg(e.message || '连接失败');
145
+ setStep('error');
146
+ }
147
+ }, [apiKey, baseURL]);
148
+ // 落盘 + 切到 App。tavily key 为空表示用户跳过 WebSearch 配置。
149
+ const finalize = useCallback(async (finalModel, tavilyApiKey) => {
150
+ const contextWindow = resolveContextWindow(preset, finalModel);
151
+ await saveConfig({
152
+ baseURL,
153
+ apiKey,
154
+ model: finalModel,
155
+ provider: preset.name,
156
+ contextWindow,
157
+ tavilyApiKey,
158
+ });
159
+ // 立刻注入当前进程 env,让本会话内的 WebSearch 不必重启就能用
160
+ if (tavilyApiKey) {
161
+ process.env.TAVILY_API_KEY = tavilyApiKey;
162
+ }
163
+ onSuccess({
164
+ name: preset.name,
165
+ baseURL,
166
+ apiKey,
167
+ model: finalModel,
168
+ contextWindow,
169
+ });
170
+ }, [apiKey, baseURL, onSuccess, preset]);
171
+ useInput((input, key) => {
172
+ if (key.escape || (key.ctrl && input === 'c')) {
173
+ exit();
174
+ return;
175
+ }
176
+ if (step === 'preset') {
177
+ if (key.upArrow) {
178
+ setPresetIndex((i) => (i - 1 + PRESETS.length) % PRESETS.length);
179
+ }
180
+ else if (key.downArrow) {
181
+ setPresetIndex((i) => (i + 1) % PRESETS.length);
182
+ }
183
+ else if (key.return) {
184
+ enterStep('baseURL');
185
+ }
186
+ return;
187
+ }
188
+ if (step === 'baseURL') {
189
+ if (key.return) {
190
+ if (draft.trim().length === 0)
191
+ return;
192
+ setBaseURL(draft.trim());
193
+ enterStep('apiKey');
194
+ }
195
+ else if (key.backspace || key.delete) {
196
+ setDraft((s) => s.slice(0, -1));
197
+ }
198
+ else if (input && !key.meta && !key.ctrl) {
199
+ setDraft((s) => s + input);
200
+ }
201
+ return;
202
+ }
203
+ if (step === 'apiKey') {
204
+ if (key.return) {
205
+ if (draft.length === 0)
206
+ return;
207
+ setApiKey(draft);
208
+ enterStep('model');
209
+ }
210
+ else if (key.backspace || key.delete) {
211
+ setDraft((s) => s.slice(0, -1));
212
+ }
213
+ else if (input && !key.meta && !key.ctrl) {
214
+ setDraft((s) => s + input);
215
+ }
216
+ return;
217
+ }
218
+ if (step === 'model') {
219
+ if (preset.models.length > 0) {
220
+ if (key.upArrow) {
221
+ const ni = (modelIndex - 1 + preset.models.length) % preset.models.length;
222
+ setModelIndex(ni);
223
+ setDraft(preset.models[ni]);
224
+ }
225
+ else if (key.downArrow) {
226
+ const ni = (modelIndex + 1) % preset.models.length;
227
+ setModelIndex(ni);
228
+ setDraft(preset.models[ni]);
229
+ }
230
+ else if (key.tab) {
231
+ setDraft((s) => s + '');
232
+ }
233
+ }
234
+ if (key.return) {
235
+ const finalModel = draft.trim();
236
+ if (finalModel.length === 0)
237
+ return;
238
+ setModel(finalModel);
239
+ void handleTest(finalModel);
240
+ }
241
+ else if (key.backspace || key.delete) {
242
+ setDraft((s) => s.slice(0, -1));
243
+ }
244
+ else if (input && !key.meta && !key.ctrl && !key.upArrow && !key.downArrow) {
245
+ setDraft((s) => s + input);
246
+ }
247
+ return;
248
+ }
249
+ if (step === 'tavily') {
250
+ if (key.return) {
251
+ // 空 draft 表示用户跳过 WebSearch 配置(合法路径,不报错)
252
+ const tavilyApiKey = draft.trim().length > 0 ? draft.trim() : undefined;
253
+ void finalize(model, tavilyApiKey);
254
+ }
255
+ else if (key.backspace || key.delete) {
256
+ setDraft((s) => s.slice(0, -1));
257
+ }
258
+ else if (input && !key.meta && !key.ctrl) {
259
+ setDraft((s) => s + input);
260
+ }
261
+ return;
262
+ }
263
+ if (step === 'error') {
264
+ if (key.return) {
265
+ enterStep('preset');
266
+ }
267
+ return;
268
+ }
269
+ });
270
+ const maskedApiKey = useMemo(() => '*'.repeat(Math.min(draft.length, 32)), [draft]);
271
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: "minimal-agent \u00B7 \u9996\u6B21\u914D\u7F6E\u5411\u5BFC" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: "\u6309 ESC \u6216 Ctrl+C \u9000\u51FA\uFF08\u4E0D\u5199\u914D\u7F6E\uFF0C\u4E0B\u6B21\u542F\u52A8\u7EE7\u7EED\u5411\u5BFC\uFF09" }) }), step === 'preset' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Step 1/5 \u00B7 \u9009\u62E9 provider \u9884\u8BBE\uFF08\u2191\u2193 \u79FB\u52A8\uFF0CEnter \u786E\u8BA4\uFF09" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: PRESETS.map((p, i) => (_jsxs(Text, { color: i === presetIndex ? 'cyan' : undefined, children: [i === presetIndex ? '> ' : ' ', p.label, p.baseURL ? ` (${p.baseURL})` : ''] }, p.name))) })] })), step === 'baseURL' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Step 2/5 \u00B7 \u8F93\u5165 API base URL\uFF08\u9ED8\u8BA4\u4E3A\u9884\u8BBE\u503C\uFF0C\u53EF\u6539\uFF09" }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: draft || ' ' }) }), _jsx(Text, { color: "gray", children: "Enter \u786E\u8BA4 \u00B7 Backspace \u5220\u9664" })] })), step === 'apiKey' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Step 3/5 \u00B7 \u8F93\u5165 API key\uFF08\u8F93\u5165\u5185\u5BB9\u4E0D\u4F1A\u663E\u793A\uFF09" }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: maskedApiKey || ' ' }) }), _jsxs(Text, { color: "gray", children: ["Enter \u786E\u8BA4 \u00B7 Backspace \u5220\u9664\uFF08\u5DF2\u8F93\u5165 ", draft.length, " \u5B57\u7B26\uFF09"] })] })), step === 'model' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Step 4/5 \u00B7 \u8F93\u5165\u6216\u9009\u62E9 model", preset.models.length > 0 ? '(↑↓ 切换预设,或直接编辑)' : ''] }), preset.models.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: preset.models.map((m, i) => (_jsxs(Text, { color: i === modelIndex ? 'cyan' : 'gray', children: [i === modelIndex ? '> ' : ' ', m] }, m))) })), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: draft || ' ' }) }), _jsx(Text, { color: "gray", children: "Enter \u63D0\u4EA4\u5E76\u6D4B\u8BD5\u8FDE\u63A5" })] })), step === 'testing' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["\u23F3 \u6B63\u5728\u6D4B\u8BD5\u8FDE\u63A5 ", baseURL, " ..."] }), _jsxs(Text, { color: "gray", children: ["model = ", model] })] })), step === 'tavily' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2713 LLM \u8FDE\u63A5\u6D4B\u8BD5\u901A\u8FC7" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Step 5/5 \u00B7 \u53EF\u9009 - Tavily API key\uFF08\u7528\u4E8E WebSearch \u5DE5\u5177\uFF09" }) }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: _jsx(Text, { children: maskedApiKey || ' ' }) }), _jsx(Text, { color: "gray", children: "\u76F4\u63A5 Enter \u8DF3\u8FC7\uFF08\u4E4B\u540E\u53EF\u5728\u5BF9\u8BDD\u91CC\u8F93\u5165 /config \u6DFB\u52A0\uFF0C\u6216\u8BBE env TAVILY_API_KEY\uFF09" }), _jsxs(Text, { color: "gray", children: ["\u514D\u8D39 key \u7533\u8BF7\uFF1Ahttps://tavily.com/ \u00B7 \u5DF2\u8F93\u5165 ", draft.length, " \u5B57\u7B26"] })] })), step === 'error' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u2717 \u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: errorMsg ?? '未知错误' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u5DF2\u8F93\u5165\uFF1A" }) }), _jsxs(Text, { color: "gray", children: [" provider = ", preset.name] }), _jsxs(Text, { color: "gray", children: [" baseURL = ", baseURL] }), _jsxs(Text, { color: "gray", children: [" model = ", model] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "\u6309 Enter \u56DE\u5230 Step 1 \u91CD\u8BD5 \u00B7 ESC \u9000\u51FA" }) })] }))] }));
272
+ }