zoe-agent 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 (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,613 @@
1
+ /**
2
+ * Zoe CLI — Setup Wizard
3
+ *
4
+ * Interactive setup wizard for configuring API keys and providers.
5
+ * Extracted from index.ts for separation of concerns.
6
+ */
7
+ import inquirer from 'inquirer';
8
+ import chalk from 'chalk';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { createProvider } from '../../providers/factory.js';
13
+ import { MODEL_CATALOG, CUSTOM_MODEL_VALUE, DEFAULT_MODELS } from '../../models-catalog.js';
14
+ import { resolveProviderConfigFromApp } from '../../core/provider-resolver.js';
15
+ import { getConfigPaths, loadJsonConfig, maskSecret, saveConfig, } from './config-loader.js';
16
+ import { isNonInteractive } from './docker-utils.js';
17
+ // ── Constants ──────────────────────────────────────────────────────────
18
+ const ALL_PROVIDER_TYPES = ['openai-compatible', 'openai', 'anthropic', 'glm'];
19
+ const ADD_PROVIDER_VALUE = '__add_provider__';
20
+ // ── Setup Wizard ───────────────────────────────────────────────────────
21
+ /**
22
+ * Run the interactive setup wizard.
23
+ * @param options.project - If true, save to project-level config instead of global.
24
+ */
25
+ export async function runSetup(options = {}) {
26
+ // Guard: setup wizard requires interactive TTY
27
+ if (isNonInteractive()) {
28
+ console.log(chalk.yellow('Setup wizard requires an interactive terminal.'));
29
+ console.log(chalk.dim('Set API keys via environment variables instead:'));
30
+ console.log(chalk.dim(' OPENAI_API_KEY, ANTHROPIC_API_KEY, GLM_API_KEY'));
31
+ console.log(chalk.dim(' LLM_PROVIDER (openai-compatible|openai|anthropic|glm)'));
32
+ console.log(chalk.dim('Or mount a config file at ~/.zoe/setting.json'));
33
+ process.exit(1);
34
+ }
35
+ const isProject = options.project;
36
+ const { global: GLOBAL_CONFIG_FILE, local: LOCAL_CONFIG_FILE, globalDir: GLOBAL_CONFIG_DIR } = getConfigPaths();
37
+ const targetFile = isProject ? LOCAL_CONFIG_FILE : GLOBAL_CONFIG_FILE;
38
+ const targetDir = isProject ? path.join(process.cwd(), '.zoe') : GLOBAL_CONFIG_DIR;
39
+ console.log(chalk.bold.cyan("Zoe Agent Setup Wizard \n"));
40
+ console.log(chalk.dim(`Config will be saved to: ${targetFile}`));
41
+ const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
42
+ const localConfig = loadJsonConfig(LOCAL_CONFIG_FILE);
43
+ const currentConfig = isProject
44
+ ? { ...globalConfig, ...localConfig }
45
+ : { ...localConfig, ...globalConfig };
46
+ const anyExisting = currentConfig.models;
47
+ // Step 1: Select providers to configure
48
+ const { providers } = await inquirer.prompt([
49
+ {
50
+ type: 'checkbox',
51
+ name: 'providers',
52
+ message: 'Which providers do you want to configure?',
53
+ choices: [
54
+ { name: `OpenAI API Compatible${anyExisting?.['openai-compatible'] ? ' (configured)' : ''}`, value: 'openai-compatible', checked: !!anyExisting?.['openai-compatible'] },
55
+ { name: `OpenAI Official${anyExisting?.openai ? ' (configured)' : ''}`, value: 'openai', checked: !!anyExisting?.openai },
56
+ { name: `Anthropic Official${anyExisting?.anthropic ? ' (configured)' : ''}`, value: 'anthropic', checked: !!anyExisting?.anthropic },
57
+ { name: `GLM Code Plan${anyExisting?.glm ? ' (configured)' : ''}`, value: 'glm', checked: !!anyExisting?.glm },
58
+ ],
59
+ validate: (input) => input.length > 0 ? true : 'Select at least one provider.'
60
+ }
61
+ ]);
62
+ // Step 2: Per-provider configuration
63
+ const modelsConfig = {};
64
+ for (const p of providers) {
65
+ const ex = anyExisting?.[p];
66
+ if (p === 'openai-compatible') {
67
+ const answers = await inquirer.prompt([
68
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `OpenAI-Compatible API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'OpenAI-Compatible API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
69
+ { type: 'input', name: 'baseUrl', message: 'API Base URL:', default: ex?.baseUrl || currentConfig.baseUrl || 'https://api.openai.com/v1' },
70
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || currentConfig.model || DEFAULT_MODELS['openai-compatible'] }
71
+ ]);
72
+ modelsConfig['openai-compatible'] = { apiKey: answers.apiKey || ex?.apiKey || '', baseUrl: answers.baseUrl, model: answers.model };
73
+ }
74
+ else if (p === 'openai') {
75
+ const answers = await inquirer.prompt([
76
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `OpenAI API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'OpenAI API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
77
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || DEFAULT_MODELS.openai }
78
+ ]);
79
+ modelsConfig.openai = { apiKey: answers.apiKey || ex?.apiKey || '', model: answers.model };
80
+ }
81
+ else if (p === 'anthropic') {
82
+ const answers = await inquirer.prompt([
83
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `Anthropic API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'Anthropic API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
84
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || DEFAULT_MODELS.anthropic }
85
+ ]);
86
+ modelsConfig.anthropic = { apiKey: answers.apiKey || ex?.apiKey || '', model: answers.model };
87
+ }
88
+ else if (p === 'glm') {
89
+ const keyAnswer = await inquirer.prompt([{ type: 'password', name: 'apiKey', message: ex?.apiKey ? `GLM API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'GLM API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' }]);
90
+ const modelAnswer = await inquirer.prompt([{ type: 'list', name: 'model', message: 'Select Model:', choices: ['haiku', 'sonnet', 'opus'], default: ex?.model || DEFAULT_MODELS.glm }]);
91
+ modelsConfig.glm = { apiKey: keyAnswer.apiKey || ex?.apiKey || '', model: modelAnswer.model };
92
+ }
93
+ }
94
+ // Preserve or remove unselected providers
95
+ const previouslyConfigured = anyExisting ? Object.keys(anyExisting) : [];
96
+ const unselectedProviders = previouslyConfigured.filter((p) => !providers.includes(p));
97
+ if (unselectedProviders.length > 0) {
98
+ const { removeUnselected } = await inquirer.prompt([
99
+ {
100
+ type: 'confirm',
101
+ name: 'removeUnselected',
102
+ message: `The following providers are configured but were not selected: ${unselectedProviders.join(', ')}. Remove their configuration?`,
103
+ default: false,
104
+ },
105
+ ]);
106
+ if (!removeUnselected) {
107
+ for (const p of unselectedProviders) {
108
+ const existingEntry = anyExisting?.[p];
109
+ if (existingEntry) {
110
+ modelsConfig[p] = existingEntry;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ // Step 3: Default provider
116
+ const { defaultProvider } = await inquirer.prompt([
117
+ {
118
+ type: 'list',
119
+ name: 'defaultProvider',
120
+ message: 'Which provider should be active by default?',
121
+ choices: Object.keys(modelsConfig).map((p) => ({ name: p, value: p })),
122
+ default: currentConfig.provider || providers[0]
123
+ }
124
+ ]);
125
+ // Step 4: Optional extras
126
+ const { configureImage } = await inquirer.prompt([
127
+ {
128
+ type: 'confirm',
129
+ name: 'configureImage',
130
+ message: 'Do you want to configure a separate Image Generation Service (DALL-E)?',
131
+ default: !!currentConfig.imageApiKey
132
+ }
133
+ ]);
134
+ const { configureEmail } = await inquirer.prompt([
135
+ {
136
+ type: 'confirm',
137
+ name: 'configureEmail',
138
+ message: 'Do you want to configure the Email Tool (SMTP)?',
139
+ default: !!currentConfig.smtpHost
140
+ }
141
+ ]);
142
+ const { configureSearch } = await inquirer.prompt([
143
+ {
144
+ type: 'confirm',
145
+ name: 'configureSearch',
146
+ message: 'Do you want to configure Web Search (Tavily)?',
147
+ default: !!currentConfig.tavilyApiKey
148
+ }
149
+ ]);
150
+ const { configureNotify } = await inquirer.prompt([
151
+ {
152
+ type: 'confirm',
153
+ name: 'configureNotify',
154
+ message: 'Do you want to configure Group Bots (Feishu/DingTalk/WeCom)?',
155
+ default: !!(currentConfig.feishuWebhook || currentConfig.dingtalkWebhook || currentConfig.wecomWebhook)
156
+ }
157
+ ]);
158
+ let imageConfig = {};
159
+ if (configureImage) {
160
+ const imageAnswers = await inquirer.prompt([
161
+ {
162
+ type: 'password',
163
+ name: 'imageApiKey',
164
+ message: currentConfig.imageApiKey
165
+ ? `Enter Image Service API Key (Leave empty to keep ${maskSecret(currentConfig.imageApiKey)}, or leave empty to use main API key):`
166
+ : 'Enter Image Service API Key (Leave empty to use main API key):',
167
+ mask: '*'
168
+ },
169
+ {
170
+ type: 'input',
171
+ name: 'imageBaseUrl',
172
+ message: 'Enter Image Service Base URL:',
173
+ default: currentConfig.imageBaseUrl || currentConfig.baseUrl || 'https://api.openai.com/v1'
174
+ },
175
+ {
176
+ type: 'input',
177
+ name: 'imageModel',
178
+ message: 'Default Image Model:',
179
+ default: currentConfig.imageModel || 'dall-e-3'
180
+ }
181
+ ]);
182
+ imageConfig = {
183
+ imageApiKey: imageAnswers.imageApiKey || currentConfig.imageApiKey,
184
+ imageBaseUrl: imageAnswers.imageBaseUrl,
185
+ imageModel: imageAnswers.imageModel
186
+ };
187
+ }
188
+ let emailConfig = {};
189
+ if (configureEmail) {
190
+ const emailAnswers = await inquirer.prompt([
191
+ {
192
+ type: 'input',
193
+ name: 'smtpHost',
194
+ message: 'SMTP Host:',
195
+ default: currentConfig.smtpHost
196
+ },
197
+ {
198
+ type: 'input',
199
+ name: 'smtpPort',
200
+ message: 'SMTP Port:',
201
+ default: currentConfig.smtpPort || '587'
202
+ },
203
+ {
204
+ type: 'input',
205
+ name: 'smtpUser',
206
+ message: 'SMTP Username:',
207
+ default: currentConfig.smtpUser
208
+ },
209
+ {
210
+ type: 'password',
211
+ name: 'smtpPass',
212
+ message: currentConfig.smtpPass
213
+ ? `SMTP Password (Leave empty to keep ${maskSecret(currentConfig.smtpPass)}):`
214
+ : 'SMTP Password:',
215
+ mask: '*',
216
+ validate: (input) => { return true; }
217
+ },
218
+ {
219
+ type: 'input',
220
+ name: 'smtpFrom',
221
+ message: 'Sender Email Address (From):',
222
+ default: currentConfig.smtpFrom || currentConfig.smtpUser
223
+ }
224
+ ]);
225
+ emailConfig = { ...emailAnswers, smtpPass: emailAnswers.smtpPass || currentConfig.smtpPass };
226
+ if (!emailConfig.smtpFrom && emailConfig.smtpUser) {
227
+ emailConfig.smtpFrom = emailConfig.smtpUser;
228
+ }
229
+ }
230
+ let searchConfig = {};
231
+ if (configureSearch) {
232
+ const searchAnswers = await inquirer.prompt([
233
+ {
234
+ type: 'password',
235
+ name: 'tavilyApiKey',
236
+ message: currentConfig.tavilyApiKey
237
+ ? `Tavily API Key (Leave empty to keep ${maskSecret(currentConfig.tavilyApiKey)}):`
238
+ : 'Tavily API Key (Free at tavily.com):',
239
+ mask: '*'
240
+ }
241
+ ]);
242
+ searchConfig = { tavilyApiKey: searchAnswers.tavilyApiKey || currentConfig.tavilyApiKey };
243
+ }
244
+ let notifyConfig = {};
245
+ if (configureNotify) {
246
+ const notifyAnswers = await inquirer.prompt([
247
+ {
248
+ type: 'password',
249
+ name: 'feishuWebhook',
250
+ message: currentConfig.feishuWebhook
251
+ ? `Feishu Webhook (Leave empty to keep ${maskSecret(currentConfig.feishuWebhook)}):`
252
+ : 'Feishu Webhook (Optional):',
253
+ mask: '*'
254
+ },
255
+ {
256
+ type: 'input',
257
+ name: 'feishuKeyword',
258
+ message: 'Feishu Security Keyword (Optional):',
259
+ default: currentConfig.feishuKeyword
260
+ },
261
+ {
262
+ type: 'password',
263
+ name: 'dingtalkWebhook',
264
+ message: currentConfig.dingtalkWebhook
265
+ ? `DingTalk Webhook (Leave empty to keep ${maskSecret(currentConfig.dingtalkWebhook)}):`
266
+ : 'DingTalk Webhook (Optional):',
267
+ mask: '*'
268
+ },
269
+ {
270
+ type: 'input',
271
+ name: 'dingtalkKeyword',
272
+ message: 'DingTalk Security Keyword (Optional):',
273
+ default: currentConfig.dingtalkKeyword
274
+ },
275
+ {
276
+ type: 'password',
277
+ name: 'wecomWebhook',
278
+ message: currentConfig.wecomWebhook
279
+ ? `WeCom Webhook (Leave empty to keep ${maskSecret(currentConfig.wecomWebhook)}):`
280
+ : 'WeCom Webhook (Optional):',
281
+ mask: '*'
282
+ },
283
+ {
284
+ type: 'input',
285
+ name: 'wecomKeyword',
286
+ message: 'WeCom Security Keyword (Optional):',
287
+ default: currentConfig.wecomKeyword
288
+ }
289
+ ]);
290
+ notifyConfig = {
291
+ feishuWebhook: notifyAnswers.feishuWebhook || currentConfig.feishuWebhook,
292
+ feishuKeyword: notifyAnswers.feishuKeyword || currentConfig.feishuKeyword,
293
+ dingtalkWebhook: notifyAnswers.dingtalkWebhook || currentConfig.dingtalkWebhook,
294
+ dingtalkKeyword: notifyAnswers.dingtalkKeyword || currentConfig.dingtalkKeyword,
295
+ wecomWebhook: notifyAnswers.wecomWebhook || currentConfig.wecomWebhook,
296
+ wecomKeyword: notifyAnswers.wecomKeyword || currentConfig.wecomKeyword
297
+ };
298
+ }
299
+ const newConfig = {
300
+ provider: defaultProvider,
301
+ models: modelsConfig,
302
+ ...imageConfig,
303
+ ...emailConfig,
304
+ ...searchConfig,
305
+ ...notifyConfig
306
+ };
307
+ try {
308
+ if (!fs.existsSync(targetDir)) {
309
+ fs.mkdirSync(targetDir, { recursive: true });
310
+ }
311
+ fs.writeFileSync(targetFile, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
312
+ console.log(chalk.green(`\nConfiguration saved to ${targetFile}`));
313
+ console.log(chalk.cyan("You can now run 'zoe' to start using the agent."));
314
+ // Create ~/zoe_documents workspace
315
+ const docsDir = path.join(os.homedir(), 'zoe_documents');
316
+ const subdirs = ['notes', 'templates', 'output', 'knowledge'];
317
+ if (!fs.existsSync(docsDir)) {
318
+ fs.mkdirSync(docsDir, { recursive: true });
319
+ for (const sub of subdirs) {
320
+ fs.mkdirSync(path.join(docsDir, sub), { recursive: true });
321
+ }
322
+ fs.writeFileSync(path.join(docsDir, 'README.md'), `# zoe_documents\n\nThis is your Zoe agent workspace. Files here are accessible across all projects.\n\n- \`notes/\` — Agent-created notes and session logs\n- \`templates/\` — Reusable templates you or the agent can reference\n- \`output/\` — Generated artifacts (reports, summaries)\n- \`knowledge/\` — Reference documents for the agent to use\n\nReference files in conversation with \`@zoe_documents/path/to/file\`\n`, 'utf-8');
323
+ console.log(chalk.green(`Created agent workspace at ${docsDir}`));
324
+ }
325
+ }
326
+ catch (error) {
327
+ console.error(chalk.red(`Failed to write config: ${error.message}`));
328
+ }
329
+ }
330
+ // ── Inline provider management (used by /models command) ───────────────
331
+ async function addProviderInline(config) {
332
+ const configured = Object.keys(config.models || {});
333
+ const available = ALL_PROVIDER_TYPES.filter(p => !configured.includes(p));
334
+ if (available.length === 0) {
335
+ console.log(chalk.yellow('All available providers are already configured.'));
336
+ return null;
337
+ }
338
+ const { provider } = await inquirer.prompt([
339
+ {
340
+ type: 'select',
341
+ name: 'provider',
342
+ message: 'Which provider to add?',
343
+ choices: available.map(p => ({ name: p, value: p })),
344
+ },
345
+ ]);
346
+ if (!config.models)
347
+ config.models = {};
348
+ if (provider === 'openai-compatible') {
349
+ const answers = await inquirer.prompt([
350
+ { type: 'password', name: 'apiKey', message: 'API Key:', mask: '*', validate: (input) => input ? true : 'API Key cannot be empty.' },
351
+ { type: 'input', name: 'baseUrl', message: 'API Base URL:', default: 'https://api.openai.com/v1' },
352
+ { type: 'input', name: 'model', message: 'Default Model:', default: DEFAULT_MODELS['openai-compatible'] },
353
+ ]);
354
+ config.models['openai-compatible'] = { apiKey: answers.apiKey, baseUrl: answers.baseUrl, model: answers.model };
355
+ }
356
+ else if (provider === 'openai') {
357
+ const answers = await inquirer.prompt([
358
+ { type: 'password', name: 'apiKey', message: 'OpenAI API Key:', mask: '*', validate: (input) => input ? true : 'API Key cannot be empty.' },
359
+ { type: 'input', name: 'model', message: 'Default Model:', default: DEFAULT_MODELS.openai },
360
+ ]);
361
+ config.models.openai = { apiKey: answers.apiKey, model: answers.model };
362
+ }
363
+ else if (provider === 'anthropic') {
364
+ const answers = await inquirer.prompt([
365
+ { type: 'password', name: 'apiKey', message: 'Anthropic API Key:', mask: '*', validate: (input) => input ? true : 'API Key cannot be empty.' },
366
+ { type: 'input', name: 'model', message: 'Default Model:', default: DEFAULT_MODELS.anthropic },
367
+ ]);
368
+ config.models.anthropic = { apiKey: answers.apiKey, model: answers.model };
369
+ }
370
+ else if (provider === 'glm') {
371
+ const keyAnswer = await inquirer.prompt([
372
+ { type: 'password', name: 'apiKey', message: 'GLM API Key:', mask: '*', validate: (input) => input ? true : 'API Key cannot be empty.' },
373
+ ]);
374
+ const modelAnswer = await inquirer.prompt([
375
+ { type: 'select', name: 'model', message: 'Select Model:', choices: ['haiku', 'sonnet', 'opus'], default: DEFAULT_MODELS.glm },
376
+ ]);
377
+ config.models.glm = { apiKey: keyAnswer.apiKey, model: modelAnswer.model };
378
+ }
379
+ saveConfig(config);
380
+ console.log(chalk.green(`Added ${provider} to your configuration.`));
381
+ return provider;
382
+ }
383
+ async function editProviderConfig(config, providerType) {
384
+ if (!config.models)
385
+ config.models = {};
386
+ const ex = config.models?.[providerType];
387
+ if (providerType === 'openai-compatible') {
388
+ const answers = await inquirer.prompt([
389
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
390
+ { type: 'input', name: 'baseUrl', message: 'API Base URL:', default: ex?.baseUrl || 'https://api.openai.com/v1' },
391
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || DEFAULT_MODELS['openai-compatible'] },
392
+ ]);
393
+ config.models['openai-compatible'] = { apiKey: answers.apiKey || ex?.apiKey || '', baseUrl: answers.baseUrl, model: answers.model };
394
+ }
395
+ else if (providerType === 'openai') {
396
+ const answers = await inquirer.prompt([
397
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `OpenAI API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'OpenAI API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
398
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || DEFAULT_MODELS.openai },
399
+ ]);
400
+ config.models.openai = { apiKey: answers.apiKey || ex?.apiKey || '', model: answers.model };
401
+ }
402
+ else if (providerType === 'anthropic') {
403
+ const answers = await inquirer.prompt([
404
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `Anthropic API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'Anthropic API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
405
+ { type: 'input', name: 'model', message: 'Default Model:', default: ex?.model || DEFAULT_MODELS.anthropic },
406
+ ]);
407
+ config.models.anthropic = { apiKey: answers.apiKey || ex?.apiKey || '', model: answers.model };
408
+ }
409
+ else if (providerType === 'glm') {
410
+ const keyAnswer = await inquirer.prompt([
411
+ { type: 'password', name: 'apiKey', message: ex?.apiKey ? `GLM API Key (Leave empty to keep ${maskSecret(ex.apiKey)}):` : 'GLM API Key:', mask: '*', validate: (input) => (input || ex?.apiKey) ? true : 'API Key cannot be empty.' },
412
+ ]);
413
+ const modelAnswer = await inquirer.prompt([
414
+ { type: 'list', name: 'model', message: 'Select Model:', choices: ['haiku', 'sonnet', 'opus'], default: ex?.model || DEFAULT_MODELS.glm },
415
+ ]);
416
+ config.models.glm = { apiKey: keyAnswer.apiKey || ex?.apiKey || '', model: modelAnswer.model };
417
+ }
418
+ saveConfig(config);
419
+ console.log(chalk.green(`Updated ${providerType} configuration.`));
420
+ }
421
+ async function removeProviderConfig(config, providerType) {
422
+ const { confirm } = await inquirer.prompt([
423
+ {
424
+ type: 'confirm',
425
+ name: 'confirm',
426
+ message: `Remove ${providerType} config? This cannot be undone.`,
427
+ default: false,
428
+ },
429
+ ]);
430
+ if (!confirm) {
431
+ console.log(chalk.dim('Removal cancelled.'));
432
+ return false;
433
+ }
434
+ if (config.models) {
435
+ delete config.models[providerType];
436
+ }
437
+ console.log(chalk.green(`Removed ${providerType} configuration.`));
438
+ return true;
439
+ }
440
+ // ── Models command handler ─────────────────────────────────────────────
441
+ /**
442
+ * Handle the /models interactive command.
443
+ * Allows switching, editing, adding, and removing providers at runtime.
444
+ */
445
+ export async function handleModelsCommand(agent, config, activeProvider) {
446
+ // Guard: /models interactive switching requires a TTY
447
+ if (isNonInteractive()) {
448
+ console.log(chalk.yellow('Interactive model switching is not available in non-interactive mode.'));
449
+ console.log(chalk.dim('Use --provider <name> or --model <model> flags, or set LLM_PROVIDER env var.'));
450
+ return activeProvider;
451
+ }
452
+ if (!config.models)
453
+ config.models = {};
454
+ try {
455
+ const configured = Object.keys(config.models).filter(k => config.models[k]?.apiKey);
456
+ if (configured.length === 0) {
457
+ console.log(chalk.yellow('No providers configured. Let\'s add one.'));
458
+ const added = await addProviderInline(config);
459
+ if (!added)
460
+ return activeProvider;
461
+ return handleModelsCommand(agent, config, added);
462
+ }
463
+ // Step 1: Select provider
464
+ const providerChoices = configured.map(p => ({
465
+ name: `${p}${p === activeProvider ? ' (active)' : ''}`,
466
+ value: p,
467
+ }));
468
+ providerChoices.push({ name: 'Add a new provider...', value: ADD_PROVIDER_VALUE });
469
+ const providerAnswer = await inquirer.prompt([
470
+ {
471
+ type: 'select',
472
+ name: 'selected',
473
+ message: 'Select a provider:',
474
+ choices: providerChoices,
475
+ default: activeProvider,
476
+ },
477
+ ]);
478
+ if (providerAnswer.selected === ADD_PROVIDER_VALUE) {
479
+ const added = await addProviderInline(config);
480
+ if (!added)
481
+ return handleModelsCommand(agent, config, activeProvider);
482
+ return handleModelsCommand(agent, config, added);
483
+ }
484
+ const selected = providerAnswer.selected;
485
+ // Step 2: Select action
486
+ const actionAnswer = await inquirer.prompt([
487
+ {
488
+ type: 'select',
489
+ name: 'action',
490
+ message: `Choose action for ${selected}:`,
491
+ choices: [
492
+ { name: 'Switch model', value: 'switch' },
493
+ { name: 'Edit config', value: 'edit' },
494
+ { name: 'Remove provider', value: 'remove' },
495
+ { name: '\u2190 Back', value: 'back' },
496
+ ],
497
+ },
498
+ ]);
499
+ if (actionAnswer.action === 'back') {
500
+ return handleModelsCommand(agent, config, activeProvider);
501
+ }
502
+ if (actionAnswer.action === 'edit') {
503
+ await editProviderConfig(config, selected);
504
+ // If editing the active provider, reload it
505
+ if (selected === activeProvider) {
506
+ const providerConfig = resolveProviderConfigFromApp(config, selected);
507
+ if (providerConfig) {
508
+ const newProvider = await createProvider(providerConfig);
509
+ agent.switchProvider(newProvider, providerConfig.model);
510
+ console.log(chalk.green(`Reloaded ${selected} with updated config.`));
511
+ }
512
+ }
513
+ return activeProvider;
514
+ }
515
+ if (actionAnswer.action === 'remove') {
516
+ const removed = await removeProviderConfig(config, selected);
517
+ if (removed) {
518
+ if (selected === activeProvider) {
519
+ // Fall back to first remaining configured provider
520
+ const remaining = Object.keys(config.models || {}).filter(k => config.models[k]?.apiKey);
521
+ if (remaining.length > 0) {
522
+ activeProvider = remaining[0];
523
+ const providerConfig = resolveProviderConfigFromApp(config, activeProvider);
524
+ if (providerConfig) {
525
+ const newProvider = await createProvider(providerConfig);
526
+ agent.switchProvider(newProvider, providerConfig.model);
527
+ console.log(chalk.green(`Switched active provider to ${activeProvider}.`));
528
+ }
529
+ // Update config.provider to reflect new active provider
530
+ config.provider = activeProvider;
531
+ }
532
+ else {
533
+ console.log(chalk.yellow('No providers remaining. Use /models to add one.'));
534
+ }
535
+ }
536
+ // Ensure config.provider doesn't reference a removed provider
537
+ if (config.provider && !config.models?.[config.provider]?.apiKey) {
538
+ const remaining = Object.keys(config.models || {}).filter(k => config.models[k]?.apiKey);
539
+ config.provider = remaining.length > 0 ? remaining[0] : undefined;
540
+ }
541
+ saveConfig(config);
542
+ return activeProvider;
543
+ }
544
+ return activeProvider;
545
+ }
546
+ // action === 'switch' — existing model selection flow
547
+ const catalog = MODEL_CATALOG[selected];
548
+ let model;
549
+ if (catalog.length > 0) {
550
+ const currentModel = config.models[selected]?.model || '';
551
+ const modelChoices = catalog.map(m => ({
552
+ name: m.name,
553
+ value: m.id,
554
+ }));
555
+ modelChoices.push({ name: 'Type custom model...', value: CUSTOM_MODEL_VALUE });
556
+ const modelAnswer = await inquirer.prompt([
557
+ {
558
+ type: 'select',
559
+ name: 'model',
560
+ message: `Select a model for ${selected}:`,
561
+ choices: modelChoices,
562
+ default: currentModel,
563
+ },
564
+ ]);
565
+ if (modelAnswer.model === CUSTOM_MODEL_VALUE) {
566
+ const customAnswer = await inquirer.prompt([
567
+ {
568
+ type: 'input',
569
+ name: 'model',
570
+ message: 'Enter model name:',
571
+ default: currentModel,
572
+ },
573
+ ]);
574
+ model = customAnswer.model;
575
+ }
576
+ else {
577
+ model = modelAnswer.model;
578
+ }
579
+ }
580
+ else {
581
+ const customAnswer = await inquirer.prompt([
582
+ {
583
+ type: 'input',
584
+ name: 'model',
585
+ message: 'Enter model name:',
586
+ default: config.models[selected]?.model || DEFAULT_MODELS['openai-compatible'],
587
+ },
588
+ ]);
589
+ model = customAnswer.model;
590
+ }
591
+ config.models[selected].model = model;
592
+ const providerConfig = resolveProviderConfigFromApp(config, selected);
593
+ if (providerConfig) {
594
+ const newProvider = await createProvider(providerConfig);
595
+ agent.switchProvider(newProvider, model);
596
+ console.log(chalk.green(`Switched to ${selected} (${model})`));
597
+ return selected;
598
+ }
599
+ else {
600
+ console.log(chalk.red(`Failed to resolve provider config for ${selected}`));
601
+ return activeProvider;
602
+ }
603
+ }
604
+ catch (err) {
605
+ if (err.message?.includes('User force closed') || err.message?.includes('Prompt was canceled')) {
606
+ console.log(chalk.dim('\nModel selection cancelled.'));
607
+ }
608
+ else {
609
+ console.error(chalk.red('Error in models command:'), err.message);
610
+ }
611
+ return activeProvider;
612
+ }
613
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Zoe CLI — System Prompts
3
+ *
4
+ * Two system prompts, selected by launch mode:
5
+ * - non-interactive (headless / piped / docker / --no-interactive):
6
+ * the Docker-native "worker unit" prompt — byte-identical to the
7
+ * historical CLI system prompt.
8
+ * - interactive (TTY + interactive flag): a general-purpose agent
9
+ * prompt tuned for a live terminal session (the TUI, or
10
+ * interactive readline).
11
+ *
12
+ * Mode detection reuses the CLI's existing signals — Commander's
13
+ * `options.interactive` (`--no-interactive`) and `isNonInteractive()`
14
+ * (TTY / docker / env). Core's `runAgentLoop` stays mode-agnostic: it
15
+ * only receives the selected prompt string.
16
+ */
17
+ export type LaunchMode = 'interactive' | 'non-interactive';
18
+ /**
19
+ * Shared, runtime-derived environment block embedded in every prompt.
20
+ * Leading and trailing newlines are intentional — callers interpolate it
21
+ * between section headers.
22
+ */
23
+ export declare function buildSystemInfoBlock(): string;
24
+ /**
25
+ * Non-interactive / Docker / headless prompt.
26
+ * Byte-identical to the historical CLI system prompt.
27
+ */
28
+ export declare function buildSystemPrompt(): string;
29
+ /**
30
+ * Interactive prompt for terminal sessions (TUI or interactive readline).
31
+ *
32
+ * Role, tool list, numbered process, and output format follow the
33
+ * interactive-agent conventions shared by tools like Command Code; the
34
+ * working principles mirror this project's own engineering standards
35
+ * (think before acting, surgical changes, simplicity, goal-driven).
36
+ */
37
+ export declare function buildInteractiveSystemPrompt(): string;
38
+ /**
39
+ * Resolve launch mode from the CLI's two existing interactive signals.
40
+ *
41
+ * A session is interactive only when the Commander interactive flag is on
42
+ * (i.e. not `--no-interactive`) AND the process is in an interactive
43
+ * context (TTY, not docker, no non-interactive env). This matches every
44
+ * documented launch path:
45
+ * - plain `zoe` in a TTY -> interactive
46
+ * - `zoe -n` / `--no-interactive` -> non-interactive
47
+ * - piped stdin -> non-interactive
48
+ * - `zoe --docker` -> non-interactive
49
+ */
50
+ export declare function resolveLaunchMode(options: {
51
+ interactive?: boolean;
52
+ }): LaunchMode;
53
+ /**
54
+ * Select the system prompt for a launch mode.
55
+ */
56
+ export declare function selectSystemPrompt(mode: LaunchMode): string;