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,197 @@
1
+ /**
2
+ * ============================================================
3
+ * src/cli/print.ts —— CLI 非交互模式(-p / --print)
4
+ * ------------------------------------------------------------
5
+ * 不启动 Ink TUI,直接执行单次问答,流式输出到 stdout。
6
+ * 适合脚本集成、CI/CD、快速单次查询。
7
+ *
8
+ * v2 改进(对齐 kakadeai CLI 工程实践):
9
+ * 1. SIGINT 先保存 context 再退出(防丢失)
10
+ * 2. handleEvent 返回退出码,不直接 process.exit(单一职责)
11
+ * 3. EPIPE 保护:管道断开时静默退出不崩溃
12
+ * 4. stdin peek 超时:防止非 TTY 无管道时永久挂起
13
+ *
14
+ * 使用方式:
15
+ * minimal-agent -p "提示"
16
+ * echo "提示" | minimal-agent -p
17
+ * minimal-agent -p --verbose "提示"
18
+ * ============================================================
19
+ */
20
+ import { saveContext } from '../context/persistContext.js';
21
+ import { runWithPlugins } from '../plugins/pluginRunner.js';
22
+ /** stderr 显示工具结果时的截断阈值;超过即末尾加 "..." */
23
+ const TOOL_OUTPUT_PREVIEW_MAX = 200;
24
+ /** stdin 读取超时(ms),防止非 TTY + 无管道时永久挂起 */
25
+ const STDIN_TIMEOUT_MS = 3000;
26
+ function handleEPIPE(stream) {
27
+ return (err) => {
28
+ if (err.code === 'EPIPE')
29
+ stream.destroy();
30
+ };
31
+ }
32
+ export function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
33
+ if (content.length <= max)
34
+ return content;
35
+ return content.slice(0, max) + '...';
36
+ }
37
+ export function extractPromptArgs(args) {
38
+ // 布尔标志:直接过滤
39
+ const FLAG_BOOLEAN = new Set([
40
+ '-p',
41
+ '--print',
42
+ '--verbose',
43
+ '-v',
44
+ '-h',
45
+ '--help',
46
+ '-V',
47
+ '--version',
48
+ ]);
49
+ // 带值标志:本身 + 后面紧跟的值都要跳过
50
+ const FLAG_WITH_VALUE = new Set(['-d', '--cwd']);
51
+ const result = [];
52
+ for (let i = 0; i < args.length; i++) {
53
+ const a = args[i];
54
+ if (FLAG_BOOLEAN.has(a))
55
+ continue;
56
+ if (FLAG_WITH_VALUE.has(a)) {
57
+ i++; // 跳过 flag 后面的值
58
+ continue;
59
+ }
60
+ result.push(a);
61
+ }
62
+ return result;
63
+ }
64
+ /**
65
+ * CLI 非交互模式:读取 prompt,执行 runQuery,流式输出到 stdout,最后落盘 history。
66
+ */
67
+ export async function runPrintMode(provider, args, initialHistory, options) {
68
+ // EPIPE 保护:管道断开时不崩溃(如 `bun -p "..." | head -1`)
69
+ process.stdout.on('error', handleEPIPE(process.stdout));
70
+ process.stderr.on('error', handleEPIPE(process.stderr));
71
+ const promptArgs = extractPromptArgs(args);
72
+ let prompt;
73
+ if (promptArgs.length > 0) {
74
+ prompt = promptArgs.join(' ');
75
+ }
76
+ else if (!process.stdin.isTTY) {
77
+ prompt = await readFromStdin();
78
+ }
79
+ else {
80
+ console.error('错误: 请提供提示文本或使用管道输入');
81
+ process.exit(1);
82
+ }
83
+ if (!prompt.trim()) {
84
+ console.error('错误: 提示不能为空');
85
+ process.exit(1);
86
+ }
87
+ const abortController = new AbortController();
88
+ let interrupted = false;
89
+ process.on('SIGINT', () => {
90
+ if (interrupted)
91
+ process.exit(130);
92
+ interrupted = true;
93
+ abortController.abort();
94
+ console.error('\n已中断');
95
+ void saveContext(initialHistory).finally(() => process.exit(130));
96
+ });
97
+ const history = initialHistory;
98
+ const output = { buffer: '' };
99
+ try {
100
+ for await (const event of runWithPlugins(prompt, {
101
+ provider,
102
+ history,
103
+ signal: abortController.signal,
104
+ })) {
105
+ // event 是 LoopEvent | PluginEvent;handleEvent 识别 LoopEvent + workflow_warning,其它走 default 静默忽略
106
+ const result = handleEvent(event, output, options.verbose);
107
+ if (result.exitCode !== undefined) {
108
+ await saveContext(history);
109
+ process.exit(result.exitCode);
110
+ }
111
+ }
112
+ }
113
+ catch (e) {
114
+ if (e.name === 'AbortError') {
115
+ await saveContext(history);
116
+ process.exit(130);
117
+ }
118
+ console.error(`\n未捕获异常: ${e.message}`);
119
+ await saveContext(history);
120
+ process.exit(1);
121
+ }
122
+ await saveContext(history);
123
+ }
124
+ /**
125
+ * 处理 runQuery yield 出来的 LoopEvent。
126
+ * 纯事件分发函数,返回 { exitCode? } 由调用方决定是否退出。
127
+ */
128
+ export function handleEvent(event, output, verbose) {
129
+ switch (event.type) {
130
+ case 'text':
131
+ if (verbose)
132
+ process.stdout.write(event.delta);
133
+ output.buffer += event.delta;
134
+ return {};
135
+ case 'assistant_message':
136
+ return {};
137
+ case 'tool_start':
138
+ if (verbose) {
139
+ process.stderr.write(`\n🔧 工具: ${event.toolName}(${event.argsPreview})\n`);
140
+ }
141
+ return {};
142
+ case 'tool_end':
143
+ if (verbose) {
144
+ process.stderr.write(`${event.ok ? '✅' : '❌'} ${event.toolName}: ${truncateForDisplay(event.content)}\n`);
145
+ }
146
+ return {};
147
+ case 'compact_start':
148
+ if (verbose)
149
+ process.stderr.write('\n📝 正在压缩上下文...\n');
150
+ return {};
151
+ case 'compact_done':
152
+ if (verbose) {
153
+ process.stderr.write(`📝 压缩完成: ${event.before} → ${event.after} tokens\n`);
154
+ }
155
+ return {};
156
+ case 'workflow_warning':
157
+ // workflow 引擎警告(如多余位置参数、--input 覆盖位置参数等)始终走 stderr,
158
+ // 不受 verbose 控制 —— 这是用户必须看到的信号,避免静默丢弃语义被忽略。
159
+ process.stderr.write(`${event.message}\n`);
160
+ return {};
161
+ case 'turn_done':
162
+ if (!verbose)
163
+ console.log(output.buffer);
164
+ return {};
165
+ case 'error':
166
+ console.error(`\n错误: ${event.error}`);
167
+ return { exitCode: 1 };
168
+ default:
169
+ return {};
170
+ }
171
+ }
172
+ function readFromStdin() {
173
+ return new Promise((resolve) => {
174
+ let data = '';
175
+ let settled = false;
176
+ const timer = setTimeout(() => {
177
+ if (!settled) {
178
+ settled = true;
179
+ resolve('');
180
+ }
181
+ }, STDIN_TIMEOUT_MS);
182
+ process.stdin.setEncoding('utf8');
183
+ function onData(chunk) {
184
+ data += chunk;
185
+ }
186
+ function onEnd() {
187
+ if (!settled) {
188
+ clearTimeout(timer);
189
+ settled = true;
190
+ resolve(data.trim());
191
+ }
192
+ }
193
+ process.stdin.on('data', onData);
194
+ process.stdin.on('end', onEnd);
195
+ });
196
+ }
197
+ export { readFromStdin };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ============================================================
3
+ * src/config/configFile.ts —— 模式 B 向导写的 provider 配置
4
+ * ------------------------------------------------------------
5
+ * 与 src/config.ts 的关系:
6
+ * - config.ts 负责"读取 process.env 拼装 Provider"(模式 A)
7
+ * - 本文件负责"读写 ~/.minimal-agent/config.json"(模式 B)
8
+ * - 上层(loadProviderLayered)先看 env、再看本文件
9
+ *
10
+ * 与 src/context/persistContext.ts 的关系:
11
+ * - 复用同一个目录 ~/.minimal-agent/
12
+ * - 同样"解析失败返回 null、写失败静默"模式
13
+ *
14
+ * 为什么用 JSON 而非 .env 格式:
15
+ * - 解析无歧义(dotenv 各家行为略有差异)
16
+ * - 显式区分"向导写的" vs "用户写的 .env",避免互相覆盖
17
+ * - JSON 还可以放结构化扩展(如 savedAt 用于排查)
18
+ * ============================================================
19
+ */
20
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
21
+ import { homedir } from 'node:os';
22
+ import { dirname, join } from 'node:path';
23
+ /** 配置文件路径(带环境变量覆盖,主要给测试用) */
24
+ export function getConfigFilePath() {
25
+ return (process.env.MINIMAL_AGENT_CONFIG_FILE ??
26
+ join(homedir(), '.minimal-agent', 'config.json'));
27
+ }
28
+ /**
29
+ * 读取向导保存的配置。
30
+ * - 文件不存在 / JSON 损坏 / 必填字段缺失 → 返回 null
31
+ * - 成功 → 返回 SavedConfig
32
+ */
33
+ export async function readSavedConfig() {
34
+ const file = getConfigFilePath();
35
+ try {
36
+ const raw = await readFile(file, 'utf8');
37
+ const data = JSON.parse(raw);
38
+ if (typeof data.baseURL !== 'string' ||
39
+ typeof data.apiKey !== 'string' ||
40
+ typeof data.model !== 'string' ||
41
+ data.baseURL.length === 0 ||
42
+ data.apiKey.length === 0 ||
43
+ data.model.length === 0) {
44
+ return null;
45
+ }
46
+ return {
47
+ baseURL: data.baseURL,
48
+ apiKey: data.apiKey,
49
+ model: data.model,
50
+ provider: typeof data.provider === 'string' ? data.provider : undefined,
51
+ contextWindow: typeof data.contextWindow === 'number' && data.contextWindow > 0
52
+ ? data.contextWindow
53
+ : undefined,
54
+ tavilyApiKey: typeof data.tavilyApiKey === 'string' && data.tavilyApiKey.length > 0
55
+ ? data.tavilyApiKey
56
+ : undefined,
57
+ savedAt: typeof data.savedAt === 'number' ? data.savedAt : 0,
58
+ };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * 写入向导收集到的配置。
66
+ * unix 上 chmod 600(仅本人可读写),Windows 上 chmod 是 no-op,靠 NTFS ACL。
67
+ */
68
+ export async function saveConfig(cfg) {
69
+ const file = getConfigFilePath();
70
+ await mkdir(dirname(file), { recursive: true });
71
+ const data = { ...cfg, savedAt: Date.now() };
72
+ await writeFile(file, JSON.stringify(data, null, 2), 'utf8');
73
+ try {
74
+ await chmod(file, 0o600);
75
+ }
76
+ catch {
77
+ }
78
+ }
package/src/config.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ============================================================
3
+ * config.ts —— 全局配置加载(从 .env 读取)
4
+ * ------------------------------------------------------------
5
+ * 只从环境变量读取,集中管理所有配置。
6
+ *
7
+ * 必需的环境变量:
8
+ * MINIMAL_AGENT_BASE_URL - API 地址,如 https://api.minimax.chat/v1
9
+ * MINIMAL_AGENT_API_KEY - API key
10
+ * MINIMAL_AGENT_MODEL - 模型名,如 MiniMax-M2.7
11
+ *
12
+ * 可选的环境变量:
13
+ * MINIMAL_AGENT_PROVIDER - 友好名称,默认 "env"
14
+ * MINIMAL_AGENT_CONTEXT_WINDOW - 上下文窗口,默认 128000
15
+ * ============================================================
16
+ */
17
+ import { readSavedConfig } from './config/configFile.js';
18
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
19
+ /**
20
+ * 加载 Provider 配置(从环境变量)。
21
+ * 缺少必需变量时抛出明确错误。
22
+ */
23
+ export async function loadProvider() {
24
+ const baseURL = process.env.MINIMAL_AGENT_BASE_URL;
25
+ const apiKey = process.env.MINIMAL_AGENT_API_KEY;
26
+ const model = process.env.MINIMAL_AGENT_MODEL;
27
+ if (!baseURL || !apiKey || !model) {
28
+ const missing = [];
29
+ if (!baseURL)
30
+ missing.push('MINIMAL_AGENT_BASE_URL');
31
+ if (!apiKey)
32
+ missing.push('MINIMAL_AGENT_API_KEY');
33
+ if (!model)
34
+ missing.push('MINIMAL_AGENT_MODEL');
35
+ throw new Error(`缺少必需的环境变量:${missing.join(', ')}\n\n` +
36
+ `请在 .env 中配置:\n` +
37
+ ` MINIMAL_AGENT_BASE_URL=https://api.example.com/v1\n` +
38
+ ` MINIMAL_AGENT_API_KEY=your-api-key\n` +
39
+ ` MINIMAL_AGENT_MODEL=your-model\n\n` +
40
+ `参考 .env.example`);
41
+ }
42
+ const contextWindowRaw = process.env.MINIMAL_AGENT_CONTEXT_WINDOW;
43
+ let contextWindow = DEFAULT_CONTEXT_WINDOW;
44
+ if (contextWindowRaw) {
45
+ const n = parseInt(contextWindowRaw, 10);
46
+ if (!Number.isNaN(n) && n > 0) {
47
+ contextWindow = n;
48
+ }
49
+ }
50
+ return {
51
+ name: process.env.MINIMAL_AGENT_PROVIDER ?? 'env',
52
+ baseURL,
53
+ apiKey,
54
+ model,
55
+ contextWindow,
56
+ };
57
+ }
58
+ /**
59
+ * 层级加载:env 优先(模式 A),不全则回退到 ~/.minimal-agent/config.json(模式 B)。
60
+ * 都没有时返回 null —— 调用方决定是触发向导还是直接退出。
61
+ *
62
+ * 与 loadProvider() 的区别:
63
+ * - loadProvider() 只读 env,全缺则抛错(给 -p 模式用,明确告知"先做配置")
64
+ * - 本函数静默 fallback 到 JSON,全缺则返回 null(给 TUI 模式用,可触发向导)
65
+ *
66
+ * env 字段优先级是逐项的:只要 env 里有 BASE_URL 就用 env 的 BASE_URL,
67
+ * 其它字段从 JSON 补;这样用户可以临时用 env 覆盖单个字段做调试。
68
+ */
69
+ export async function loadProviderLayered() {
70
+ const envBaseURL = process.env.MINIMAL_AGENT_BASE_URL;
71
+ const envApiKey = process.env.MINIMAL_AGENT_API_KEY;
72
+ const envModel = process.env.MINIMAL_AGENT_MODEL;
73
+ const envName = process.env.MINIMAL_AGENT_PROVIDER;
74
+ const envContextRaw = process.env.MINIMAL_AGENT_CONTEXT_WINDOW;
75
+ let saved = null;
76
+ if (!envBaseURL || !envApiKey || !envModel) {
77
+ saved = await readSavedConfig();
78
+ }
79
+ const baseURL = envBaseURL ?? saved?.baseURL;
80
+ const apiKey = envApiKey ?? saved?.apiKey;
81
+ const model = envModel ?? saved?.model;
82
+ if (!baseURL || !apiKey || !model)
83
+ return null;
84
+ let contextWindow = DEFAULT_CONTEXT_WINDOW;
85
+ if (envContextRaw) {
86
+ const n = parseInt(envContextRaw, 10);
87
+ if (!Number.isNaN(n) && n > 0)
88
+ contextWindow = n;
89
+ }
90
+ else if (saved?.contextWindow) {
91
+ contextWindow = saved.contextWindow;
92
+ }
93
+ return {
94
+ name: envName ?? saved?.provider ?? 'env',
95
+ baseURL,
96
+ apiKey,
97
+ model,
98
+ contextWindow,
99
+ };
100
+ }
101
+ /**
102
+ * 把 saved config 里的工具 key 注入 process.env(env 已设则不覆盖)。
103
+ *
104
+ * 解决 npm 全局安装场景:`node dist/main.js` 不会自动加载 .env,
105
+ * Tavily 之类的工具 key 只能从 ~/.minimal-agent/config.json 取出再写回 env,
106
+ * websearch.ts 才能在 call() 里读到。
107
+ *
108
+ * 应在 main() 最早期调用(initWorkingDir 之后即可),保证 ALL_TOOLS
109
+ * 任何 call() 触发前 env 已就位。
110
+ */
111
+ export async function applyToolKeysToEnv() {
112
+ const saved = await readSavedConfig();
113
+ if (!saved)
114
+ return;
115
+ if (!process.env.TAVILY_API_KEY && saved.tavilyApiKey) {
116
+ process.env.TAVILY_API_KEY = saved.tavilyApiKey;
117
+ }
118
+ }