min-agent 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
package/dist/config.js CHANGED
@@ -1,33 +1,236 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, mkdirSync, existsSync, statSync } from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
4
  import readline from "readline";
5
- const CONFIG_DIR = path.join(os.homedir(), ".min-agent");
6
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
- const RULES_FILE = path.join(CONFIG_DIR, "rules.md");
5
+ import { atomicWriteFileSync } from "./tools/atomic-file.js";
6
+ import { getEffectiveSandboxPolicy, mergeSandboxConfig, parseSandboxConfig, sandboxLaunchSource, sandboxStatusLabel, } from "./sandbox.js";
7
+ const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".min-agent");
8
+ const CONFIG_FILE_NAME = "config.json";
9
+ const RULES_FILE_NAME = "rules.md";
8
10
  export function getConfigDir() {
9
- return CONFIG_DIR;
11
+ return process.env.MIN_AGENT_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
10
12
  }
11
13
  export function getRulesFile() {
12
- return RULES_FILE;
14
+ return path.join(getConfigDir(), RULES_FILE_NAME);
13
15
  }
14
- export function loadConfig() {
15
- if (!existsSync(CONFIG_FILE))
16
+ let cachedConfig = null;
17
+ const projectCache = new Map();
18
+ export function getProjectConfigDir() {
19
+ return path.join(process.cwd(), ".min-agent");
20
+ }
21
+ export function getProjectConfigFile() {
22
+ return path.join(getProjectConfigDir(), CONFIG_FILE_NAME);
23
+ }
24
+ export function loadProjectConfig() {
25
+ const file = getProjectConfigFile();
26
+ let st;
27
+ try {
28
+ st = statSync(file);
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ const cached = projectCache.get(file);
34
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size)
35
+ return cached.config;
36
+ let config;
37
+ try {
38
+ config = JSON.parse(readFileSync(file, "utf-8"));
39
+ }
40
+ catch {
41
+ console.warn(`\x1b[33m ⚠ Project config ${file} is not valid JSON; ignoring\x1b[0m`);
42
+ return {};
43
+ }
44
+ projectCache.set(file, { mtimeMs: st.mtimeMs, size: st.size, config });
45
+ return config;
46
+ }
47
+ export function saveProjectConfig(config) {
48
+ const dir = getProjectConfigDir();
49
+ mkdirSync(dir, { recursive: true });
50
+ // Keep unknown keys, drop empty skill arrays so the file stays minimal.
51
+ const { disabledSkills, enabledSkills, ...rest } = config;
52
+ atomicWriteFileSync(getProjectConfigFile(), JSON.stringify({
53
+ ...rest,
54
+ ...(disabledSkills?.length ? { disabledSkills } : {}),
55
+ ...(enabledSkills?.length ? { enabledSkills } : {}),
56
+ }, null, 2));
57
+ projectCache.delete(getProjectConfigFile());
58
+ }
59
+ export function loadConfigFile(configFile) {
60
+ let st;
61
+ try {
62
+ st = statSync(configFile);
63
+ }
64
+ catch {
16
65
  return {};
66
+ }
67
+ if (cachedConfig &&
68
+ cachedConfig.file === configFile &&
69
+ cachedConfig.mtimeMs === st.mtimeMs &&
70
+ cachedConfig.size === st.size) {
71
+ return cachedConfig.config;
72
+ }
73
+ let config;
17
74
  try {
18
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
75
+ config = JSON.parse(readFileSync(configFile, "utf-8"));
19
76
  }
20
77
  catch {
78
+ console.warn(`\x1b[33m ⚠ Config file ${configFile} is not valid JSON; using defaults\x1b[0m`);
21
79
  return {};
22
80
  }
81
+ cachedConfig = { file: configFile, mtimeMs: st.mtimeMs, size: st.size, config };
82
+ return config;
83
+ }
84
+ export function loadConfig() {
85
+ const raw = loadConfigFile(path.join(getConfigDir(), CONFIG_FILE_NAME));
86
+ const { config, migrated } = migrateLegacyConfig(raw);
87
+ if (migrated) {
88
+ try {
89
+ saveConfig(config);
90
+ }
91
+ catch { }
92
+ }
93
+ return config;
94
+ }
95
+ export function migrateLegacyConfig(config) {
96
+ const legacy = config.provider;
97
+ if (!legacy)
98
+ return { config, migrated: false };
99
+ const { provider, ...rest } = config;
100
+ return {
101
+ config: { ...rest, providers: [{ name: "default", ...provider }], activeProvider: "default" },
102
+ migrated: true,
103
+ };
104
+ }
105
+ export function mergeProjectOverGlobal(global, project) {
106
+ const activeProvider = project.activeProvider ?? global.activeProvider;
107
+ const sampling = project.sampling ? { ...global.sampling, ...project.sampling } : global.sampling;
108
+ const budget = project.budget ? { ...global.budget, ...project.budget } : global.budget;
109
+ const permission = parsePermissionMode(project.permission) ?? parsePermissionMode(global.permission);
110
+ const sandbox = mergeSandboxConfig(parseSandboxConfig(global.sandbox), parseSandboxConfig(project.sandbox));
111
+ const compaction = project.compaction ? { ...global.compaction, ...project.compaction } : global.compaction;
112
+ const agent = project.agent ? { ...global.agent, ...project.agent } : global.agent;
113
+ let providers = global.providers;
114
+ if (project.defaultModel && providers && providers.length > 0) {
115
+ const idx = activeProvider ? providers.findIndex((p) => p.name === activeProvider) : 0;
116
+ const target = idx >= 0 ? idx : 0;
117
+ providers = providers.map((p, i) => (i === target ? { ...p, defaultModel: project.defaultModel } : p));
118
+ }
119
+ return {
120
+ ...global,
121
+ activeProvider,
122
+ sampling,
123
+ budget,
124
+ permission,
125
+ ...(sandbox ? { sandbox } : {}),
126
+ compaction,
127
+ agent,
128
+ ...(providers ? { providers } : {}),
129
+ };
130
+ }
131
+ export function getEffectiveConfig() {
132
+ return mergeProjectOverGlobal(loadConfig(), loadProjectConfig());
133
+ }
134
+ export function getBudgetSnapshot() {
135
+ const global = loadConfig();
136
+ const project = loadProjectConfig();
137
+ const maxCostUSD = mergeProjectOverGlobal(global, project).budget?.maxCostUSD;
138
+ const source = project.budget?.maxCostUSD != null ? "project" : global.budget?.maxCostUSD != null ? "global" : null;
139
+ return { maxCostUSD, source };
140
+ }
141
+ export function parsePermissionMode(value) {
142
+ if (value === "ask" || value === "accept-edits" || value === "allow-all")
143
+ return value;
144
+ return undefined;
145
+ }
146
+ export function permissionModeLabel(mode) {
147
+ if (mode === "allow-all")
148
+ return "allow all";
149
+ if (mode === "accept-edits")
150
+ return "accept edits";
151
+ return "ask";
152
+ }
153
+ export function getPermissionSnapshot() {
154
+ const global = loadConfig();
155
+ const project = loadProjectConfig();
156
+ const permission = parsePermissionMode(project.permission) ?? parsePermissionMode(global.permission) ?? "ask";
157
+ const source = parsePermissionMode(project.permission) != null
158
+ ? "project"
159
+ : parsePermissionMode(global.permission) != null
160
+ ? "global"
161
+ : null;
162
+ return { permission, source };
163
+ }
164
+ export function setPermissionMode(permission, scope) {
165
+ if (scope === "project") {
166
+ const project = loadProjectConfig();
167
+ saveProjectConfig({ ...project, permission });
168
+ return;
169
+ }
170
+ saveConfig({ ...loadConfig(), permission });
171
+ }
172
+ export function setSandboxConfig(partial, scope) {
173
+ if (scope === "project") {
174
+ const project = loadProjectConfig();
175
+ const current = parseSandboxConfig(project.sandbox) ?? {};
176
+ saveProjectConfig({ ...project, sandbox: { ...current, ...partial } });
177
+ return;
178
+ }
179
+ const cfg = loadConfig();
180
+ const current = parseSandboxConfig(cfg.sandbox) ?? {};
181
+ saveConfig({ ...cfg, sandbox: { ...current, ...partial } });
182
+ }
183
+ export function getSandboxSnapshot() {
184
+ const policy = getEffectiveSandboxPolicy();
185
+ const launch = sandboxLaunchSource();
186
+ const project = parseSandboxConfig(loadProjectConfig().sandbox);
187
+ const global = parseSandboxConfig(loadConfig().sandbox);
188
+ const projectSet = Boolean(project?.mode || project?.network || project?.extraWriteRoots?.length || project?.extraReadRoots?.length);
189
+ const globalSet = Boolean(global?.mode || global?.network || global?.extraWriteRoots?.length || global?.extraReadRoots?.length);
190
+ const source = launch ?? (projectSet ? "project" : globalSet ? "global" : null);
191
+ return {
192
+ mode: policy.mode,
193
+ network: policy.network,
194
+ extraWriteRoots: policy.extraWriteRoots,
195
+ extraReadRoots: policy.extraReadRoots,
196
+ source,
197
+ label: sandboxStatusLabel(policy),
198
+ };
199
+ }
200
+ export function setBudgetMaxCost(maxCostUSD, scope) {
201
+ if (scope === "project") {
202
+ const project = loadProjectConfig();
203
+ saveProjectConfig({ ...project, budget: { ...project.budget, maxCostUSD } });
204
+ return;
205
+ }
206
+ const cfg = loadConfig();
207
+ saveConfig({ ...cfg, budget: { ...cfg.budget, maxCostUSD } });
208
+ }
209
+ export function getActiveProvider(config) {
210
+ if (!config.providers || config.providers.length === 0)
211
+ return undefined;
212
+ if (config.activeProvider) {
213
+ const found = config.providers.find((p) => p.name === config.activeProvider);
214
+ if (found)
215
+ return found;
216
+ }
217
+ return config.providers[0];
218
+ }
219
+ /** Appends /v1 to an Ollama base URL unless already present */
220
+ export function normalizeOllamaBaseURL(baseURL) {
221
+ const trimmed = baseURL.replace(/\/$/, "");
222
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
23
223
  }
24
224
  export function saveConfig(config) {
25
- mkdirSync(CONFIG_DIR, { recursive: true });
26
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
225
+ const dir = getConfigDir();
226
+ mkdirSync(dir, { recursive: true });
227
+ atomicWriteFileSync(path.join(dir, CONFIG_FILE_NAME), JSON.stringify(config, null, 2));
228
+ cachedConfig = null;
27
229
  }
28
230
  export function isConfigured() {
29
231
  const config = loadConfig();
30
- return !!(config.provider?.baseURL && config.provider?.apiKey);
232
+ const provider = getActiveProvider(config);
233
+ return !!(provider?.baseURL && provider?.apiKey);
31
234
  }
32
235
  async function fetchModelsFromURL(url, apiKey) {
33
236
  const noCacheURL = new URL(url);
@@ -56,20 +259,24 @@ function ask(rl, question, defaultValue) {
56
259
  });
57
260
  });
58
261
  }
59
- const MODELS_CACHE_FILE = path.join(CONFIG_DIR, "models-cache.json");
262
+ const MODELS_CACHE_FILE = "models-cache.json";
263
+ function modelsCachePath() {
264
+ return path.join(getConfigDir(), MODELS_CACHE_FILE);
265
+ }
60
266
  function loadModelsCache() {
61
- if (!existsSync(MODELS_CACHE_FILE))
267
+ const file = modelsCachePath();
268
+ if (!existsSync(file))
62
269
  return [];
63
270
  try {
64
- return JSON.parse(readFileSync(MODELS_CACHE_FILE, "utf-8"));
271
+ return JSON.parse(readFileSync(file, "utf-8"));
65
272
  }
66
273
  catch {
67
274
  return [];
68
275
  }
69
276
  }
70
277
  function saveModelsCache(models) {
71
- mkdirSync(CONFIG_DIR, { recursive: true });
72
- writeFileSync(MODELS_CACHE_FILE, JSON.stringify(models), "utf-8");
278
+ mkdirSync(getConfigDir(), { recursive: true });
279
+ atomicWriteFileSync(modelsCachePath(), JSON.stringify(models));
73
280
  }
74
281
  export async function fetchModels(baseURL, apiKey) {
75
282
  try {
@@ -101,26 +308,88 @@ export async function fetchModels(baseURL, apiKey) {
101
308
  }
102
309
  export async function runSetup() {
103
310
  const config = loadConfig();
104
- const existing = config.provider;
311
+ const providers = config.providers ?? [];
105
312
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
106
313
  console.log("\n🔧 min-agent 配置\n");
314
+ if (providers.length > 0) {
315
+ console.log("已配置的 Provider:");
316
+ providers.forEach((p, i) => {
317
+ const marker = p.name === config.activeProvider ? " ← 当前" : "";
318
+ console.log(` ${i + 1}. ${p.name ?? "(未命名)"} ${p.baseURL} (${p.defaultModel ?? "未设置模型"})${marker}`);
319
+ });
320
+ console.log();
321
+ console.log("选项:");
322
+ console.log(" 1. 添加新 Provider");
323
+ console.log(" 2. 切换当前 Provider");
324
+ console.log(" 3. 修改当前 Provider");
325
+ console.log(" 4. 完成");
326
+ const choice = await ask(rl, "选择 (1/2/3/4)", "4");
327
+ if (choice === "1") {
328
+ const provider = await collectProvider(rl, undefined);
329
+ provider.name =
330
+ (await ask(rl, "Provider 名称", `provider-${providers.length + 1}`)).trim() ||
331
+ `provider-${providers.length + 1}`;
332
+ providers.push(provider);
333
+ config.activeProvider = provider.name;
334
+ config.providers = providers;
335
+ saveConfig(config);
336
+ console.log(`\n✓ Provider "${provider.name}" 已添加并设为当前`);
337
+ }
338
+ else if (choice === "2") {
339
+ const sel = await ask(rl, "输入序号选择当前 Provider");
340
+ const idx = parseInt(sel, 10) - 1;
341
+ const target = providers[idx];
342
+ if (!target?.name) {
343
+ console.error("无效序号");
344
+ }
345
+ else {
346
+ config.activeProvider = target.name;
347
+ saveConfig(config);
348
+ console.log(`\n✓ 当前 Provider 已切换为: ${target.name}`);
349
+ }
350
+ }
351
+ else if (choice === "3") {
352
+ const name = getActiveProvider(config)?.name ?? providers[0]?.name;
353
+ if (!name) {
354
+ console.error("无可用 Provider");
355
+ rl.close();
356
+ return;
357
+ }
358
+ const idx = providers.findIndex((p) => p.name === name);
359
+ providers[idx] = { ...(await collectProvider(rl, providers[idx])), name };
360
+ config.providers = providers;
361
+ saveConfig(config);
362
+ console.log(`\n✓ Provider "${name}" 已更新`);
363
+ }
364
+ else {
365
+ console.log("\n配置未变更");
366
+ }
367
+ rl.close();
368
+ return;
369
+ }
370
+ // 首次配置
371
+ const provider = await collectProvider(rl, undefined);
372
+ provider.name = "default";
373
+ config.providers = [provider];
374
+ config.activeProvider = "default";
375
+ saveConfig(config);
376
+ rl.close();
377
+ console.log(`\n✓ 配置已保存到 ${path.join(getConfigDir(), CONFIG_FILE_NAME)}`);
378
+ }
379
+ async function collectProvider(rl, existing) {
107
380
  console.log("Provider 类型:");
108
381
  console.log(" 1. openai-compatible (默认,兼容 OpenAI API 的任意服务)");
109
382
  console.log(" 2. openai (OpenAI 官方)");
110
383
  console.log(" 3. ollama (本地 Ollama)");
111
384
  console.log();
112
385
  const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
113
- const providerType = typeChoice === "3" ? "ollama"
114
- : typeChoice === "2" ? "openai"
115
- : "openai-compatible";
386
+ const providerType = typeChoice === "3" ? "ollama" : typeChoice === "2" ? "openai" : "openai-compatible";
116
387
  let baseURL;
117
388
  let apiKey;
118
389
  if (providerType === "ollama") {
119
390
  baseURL = await ask(rl, "Ollama API URL", existing?.baseURL || "http://localhost:11434/v1");
120
- if (!baseURL.replace(/\/$/, "").endsWith("/v1")) {
121
- baseURL = `${baseURL.replace(/\/$/, "")}/v1`;
122
- }
123
- apiKey = "ollama"; // Ollama doesn't need a real key
391
+ baseURL = normalizeOllamaBaseURL(baseURL);
392
+ apiKey = "ollama";
124
393
  }
125
394
  else if (providerType === "openai") {
126
395
  baseURL = "https://api.openai.com/v1";
@@ -132,7 +401,6 @@ export async function runSetup() {
132
401
  }
133
402
  if (!baseURL || (!apiKey && providerType !== "ollama")) {
134
403
  console.error("URL 和 Key 不能为空");
135
- rl.close();
136
404
  process.exit(1);
137
405
  }
138
406
  console.log("\n正在获取模型列表...");
@@ -146,24 +414,16 @@ export async function runSetup() {
146
414
  });
147
415
  console.log();
148
416
  const choice = await ask(rl, "选择默认模型 (输入序号或模型名)", defaultModel);
149
- const idx = parseInt(choice) - 1;
150
- if (idx >= 0 && idx < models.length) {
417
+ const idx = parseInt(choice, 10) - 1;
418
+ if (idx >= 0 && idx < models.length)
151
419
  defaultModel = models[idx];
152
- }
153
- else if (choice) {
420
+ else if (choice)
154
421
  defaultModel = choice;
155
- }
156
422
  }
157
423
  else {
158
424
  console.log(" ⚠ 无法获取模型列表,请手动输入模型名");
159
425
  const hint = providerType === "ollama" ? "llama3" : providerType === "openai" ? "gpt-4o" : "";
160
426
  defaultModel = await ask(rl, "默认模型", defaultModel || hint);
161
427
  }
162
- rl.close();
163
- config.provider = { type: providerType, baseURL, apiKey, defaultModel };
164
- saveConfig(config);
165
- console.log(`\n✓ 配置已保存到 ${CONFIG_FILE}`);
166
- console.log(` Type: ${providerType}`);
167
- console.log(` URL: ${baseURL}`);
168
- console.log(` Model: ${defaultModel}\n`);
428
+ return { type: providerType, baseURL, apiKey, defaultModel };
169
429
  }
package/dist/confirm.js CHANGED
@@ -1,13 +1,42 @@
1
- import { loadConfig } from "./config.js";
1
+ import { AsyncLocalStorage } from "async_hooks";
2
+ import { getEffectiveConfig } from "./config.js";
3
+ import { normalizeQuestionOptions, normalizeQuestionPrompt } from "./question-format.js";
4
+ import readline from "readline";
2
5
  let autoApprove = false;
3
6
  export function setAutoApprove(value) {
4
7
  autoApprove = value;
5
8
  }
6
9
  export function isAutoApprove() {
10
+ const ctx = confirmAls.getStore();
11
+ if (ctx)
12
+ return ctx.autoApprove;
7
13
  if (autoApprove)
8
14
  return true;
9
- const config = loadConfig();
10
- return config.permission === "allow-all";
15
+ return getPermissionMode() === "allow-all";
16
+ }
17
+ let permissionOverride;
18
+ export function setPermissionOverride(mode) {
19
+ permissionOverride = mode;
20
+ }
21
+ export function getPermissionOverride() {
22
+ return permissionOverride;
23
+ }
24
+ export function getPermissionMode() {
25
+ if (permissionOverride)
26
+ return permissionOverride;
27
+ const config = getEffectiveConfig();
28
+ return config.permission === "allow-all" || config.permission === "accept-edits" || config.permission === "ask"
29
+ ? config.permission
30
+ : "ask";
31
+ }
32
+ export function isEditAutoApprove() {
33
+ if (isAutoApprove())
34
+ return true;
35
+ return getPermissionMode() === "accept-edits";
36
+ }
37
+ const confirmAls = new AsyncLocalStorage();
38
+ export function runWithConfirmContext(ctx, fn) {
39
+ return confirmAls.run(ctx, fn);
11
40
  }
12
41
  /** Optional readline interface to pause/resume during confirmation prompts. */
13
42
  let _rl = null;
@@ -19,16 +48,71 @@ let _tuiConfirm = null;
19
48
  export function setTuiConfirm(handler) {
20
49
  _tuiConfirm = handler;
21
50
  }
51
+ /** Optional TUI question handler — when set, askQuestion() delegates to the TUI. Returns null on cancel. */
52
+ let _tuiQuestion = null;
53
+ export function setTuiQuestion(handler) {
54
+ _tuiQuestion = handler;
55
+ }
56
+ /** Ask the user a free-form question. Returns the answer, or null if cancelled. */
57
+ export async function askQuestion(prompt, options) {
58
+ const safePrompt = normalizeQuestionPrompt(prompt);
59
+ const choices = normalizeQuestionOptions(options);
60
+ const safeOptions = choices.length > 0 ? choices : undefined;
61
+ const ctx = confirmAls.getStore();
62
+ if (ctx) {
63
+ if (ctx.autoApprove)
64
+ return null;
65
+ if (ctx.onQuestion)
66
+ return ctx.onQuestion(safePrompt, safeOptions);
67
+ return null;
68
+ }
69
+ if (_tuiQuestion)
70
+ return _tuiQuestion(safePrompt, safeOptions);
71
+ if (!process.stdin.isTTY) {
72
+ console.log(`\n\x1b[33m❓ ${safePrompt} (no interactive terminal — skipped)\x1b[0m`);
73
+ return null;
74
+ }
75
+ console.log();
76
+ console.log(`\x1b[33m❓ ${safePrompt}\x1b[0m`);
77
+ if (safeOptions) {
78
+ for (let i = 0; i < safeOptions.length; i++) {
79
+ console.log(`\x1b[90m ${i + 1}. ${safeOptions[i]}\x1b[0m`);
80
+ }
81
+ }
82
+ return new Promise((resolve) => {
83
+ const rl = readline.createInterface({
84
+ input: process.stdin,
85
+ output: process.stdout,
86
+ });
87
+ rl.question("\x1b[36m → \x1b[0m", (answer) => {
88
+ rl.close();
89
+ resolve(answer.trim() || "(no answer)");
90
+ });
91
+ });
92
+ }
22
93
  /**
23
94
  * Ask user for confirmation. Returns true if approved.
24
95
  * Display order: detail first, then [y/N] prompt at the bottom.
25
96
  */
26
97
  export async function confirm(message) {
98
+ const ctx = confirmAls.getStore();
99
+ if (ctx) {
100
+ if (ctx.autoApprove)
101
+ return true;
102
+ if (ctx.onConfirm)
103
+ return ctx.onConfirm(message);
104
+ return false;
105
+ }
27
106
  if (isAutoApprove())
28
107
  return true;
29
108
  // If TUI mode is active, delegate to the TUI overlay
30
109
  if (_tuiConfirm)
31
110
  return _tuiConfirm(message);
111
+ // Non-interactive stdin can never answer — reject instead of hanging
112
+ if (!process.stdin.isTTY) {
113
+ console.log(`\n\x1b[33m⚠ Rejected (no interactive terminal): ${message}\x1b[0m`);
114
+ return false;
115
+ }
32
116
  // Pause readline so it doesn't consume/echo the keystroke
33
117
  _rl?.pause();
34
118
  // Detail on top, prompt at the bottom
@@ -60,17 +144,29 @@ export async function confirm(message) {
60
144
  /** Check if a shell command is potentially dangerous */
61
145
  export function isDangerousCommand(command) {
62
146
  const dangerous = [
63
- /\brm\s+(-rf?|--recursive)\s/,
64
- /\brm\s+-[a-z]*f/,
147
+ /\brm\s+(-[a-z]*[rf][a-z]*|--recursive)\s/,
65
148
  /\bsudo\b/,
66
149
  /\bmkfs\b/,
67
150
  /\bdd\s+/,
68
151
  /\b(shutdown|reboot|halt|poweroff)\b/,
69
- /\bgit\s+(push|reset\s+--hard|clean\s+-[a-z]*f)/,
70
- /\bnpm\s+publish\b/,
71
- /\bdrop\s+(table|database)\b/i,
72
- /\btruncate\s+table\b/i,
152
+ /\bgit\s+(push|reset\s+--hard|clean\s+-[a-z]*f|rebase)/,
153
+ /\bnpm\s+(publish|unpublish)\b/,
154
+ /\b(drop|truncate)\s+(table|database)\b/i,
73
155
  /\bformat\b.*\b[a-z]:\b/i,
156
+ /\bfind\b[^|;&]*\s+-delete\b/,
157
+ /\|\s*(ba|z|fi)?sh\b/,
158
+ /\bchmod\s+(-[a-zA-Z]+\s+)?[0-7]{3,4}\b/,
159
+ /\bchown\s+-R\b/,
160
+ /\bkill(all)?\s+(-9|-[a-z]*9)/,
161
+ /\b(pkill|killall)\b/,
162
+ /\bgit\s+checkout\s+\./,
163
+ /\bgit\s+stash\b/,
164
+ /\b(docker|podman)\s+(rm|prune|system)\b/,
165
+ /\b(shred|wipefs)\b/,
166
+ /\b(cp|mv)\s+-[a-z]*f\b/,
167
+ /[^<>=!&]\s*>>?\s*[^<>=&]|^\s*>>?\s*[^<>=&]/,
168
+ /:\(\)\s*\{/,
169
+ />+\s*\/dev\/(sd[a-z]+|nvme\d|disk\d)/,
74
170
  ];
75
171
  return dangerous.some((re) => re.test(command));
76
172
  }