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.
- package/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- 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;
|