draco-cli 0.0.0-stage → 3.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anuj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Draco CLI
2
+
3
+ ![Node.js 18+](https://img.shields.io/badge/Node.js-18%2B-green)
4
+ ![License: MIT](https://img.shields.io/badge/License-MIT-green)
5
+ ![Author: Anuj Mhatre](https://img.shields.io/badge/Author-Anuj%20Mhatre-orange)
6
+
7
+ ## Why We Built This
8
+
9
+ Developers need AI coding assistance directly in their terminal without switching contexts. Draco CLI provides a zero-login, terminal-first AI coding agent powered by the OpenCode Zen API.
10
+
11
+ ## What It Does
12
+
13
+ - Interactive REPL for AI-assisted coding in the terminal
14
+ - One-shot mode for quick questions: `draco "explain this error"`
15
+ - Auto-apply file edits with configurable approval levels
16
+ - Session persistence and resume with `--continue`
17
+ - MCP server integration for extended tool capabilities
18
+ - Multiple model support via `--model` flag
19
+
20
+ ## How It Was Built
21
+
22
+ - **Runtime:** Node.js 18+ with CommonJS modules
23
+ - **API:** OpenAI-compatible (OpenCode Zen)
24
+ - **Interface:** readline-based terminal UI with color support
25
+ - **Architecture:** Modular (CLI args, session management, providers, tools)
26
+ - **MCP:** Model Context Protocol for tool integrations
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Install globally
32
+ npm install -g draco-cli
33
+
34
+ # Or run directly
35
+ node bin/draco.js
36
+
37
+ # One-shot mode
38
+ draco "write a Python hello world"
39
+
40
+ # With specific model
41
+ draco --model=gpt-4 "refactor this function"
42
+ ```
43
+
44
+ **Environment Variables:**
45
+ ```bash
46
+ export OPENCODE_ZEN_KEY="your-api-key"
47
+ export DRACO_MODEL="nemotron-3-ultra-free"
48
+ export DRACO_APPROVALS="suggest" # suggest | auto-edit | full-auto
49
+ ```
50
+
51
+ ## Project Structure
52
+
53
+ ```
54
+ draco-cli/
55
+ ├── bin/
56
+ │ └── draco.js # Entry point
57
+ ├── src/
58
+ │ ├── index.js # Main orchestrator
59
+ │ ├── config.js # Configuration management
60
+ │ ├── cli/ # Args parsing, REPL, line queue
61
+ │ ├── core/ # Session management
62
+ │ ├── providers/ # AI provider integrations
63
+ │ ├── tools/ # File/shell tools
64
+ │ ├── integrations/ # MCP client
65
+ │ └── ui/ # Terminal UI utilities
66
+ ├── package.json # Dependencies
67
+ └── ARCHITECTURE.md # Detailed architecture docs
68
+ ```
69
+
70
+ ## Test Results
71
+
72
+ Manual CLI testing. No automated test suite (interactive terminal project).
73
+
74
+ ## License
75
+
76
+ MIT License - see [LICENSE](LICENSE) for details.
package/bin/draco.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { main } = require('../src/index.js');
5
+
6
+ main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
- {
2
- "name": "draco-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "draco-cli",
3
+ "version": "3.0.3",
4
+ "description": "Draco — a zero-login, terminal-first AI coding agent for the OpenCode Zen API.",
5
+ "license": "MIT",
6
+ "author": "Anuj",
7
+ "bin": {
8
+ "draco": "./bin/draco.js"
9
+ },
10
+ "main": "src/index.js",
11
+ "engines": {
12
+ "node": ">=18.0.0"
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "coding-agent",
18
+ "opencode",
19
+ "zen",
20
+ "terminal",
21
+ "agent"
22
+ ],
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "scripts": {
30
+ "start": "node bin/draco.js"
31
+ }
32
+ }
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ function parseArgs(argv) {
4
+ const flags = { model: undefined, yolo: false, version: false, help: false };
5
+ for (let i = 0; i < argv.length; i++) {
6
+ const a = argv[i];
7
+ if (a === '--help' || a === '-h') flags.help = true;
8
+ else if (a === '--version' || a === '-v') flags.version = true;
9
+ else if (a === '--yolo') flags.yolo = true;
10
+ else if (a === '--continue' || a === '-c') flags.continueSession = true;
11
+ else if (a === '--suggest') flags.approvals = 'suggest';
12
+ else if (a === '--auto-edit') flags.approvals = 'auto-edit';
13
+ else if (a === '--full-auto') flags.approvals = 'full-auto';
14
+ else if (a.startsWith('--model=')) flags.model = a.slice('--model='.length);
15
+ else if (a === '--model') {
16
+ if (i + 1 >= argv.length) throw new Error('--model requires a value.');
17
+ flags.model = argv[++i];
18
+ } else if (a.startsWith('-') && a !== '-') {
19
+ throw new Error(`Unknown option: ${a}`);
20
+ } else {
21
+ flags.promptArgs = flags.promptArgs || [];
22
+ flags.promptArgs.push(a);
23
+ }
24
+ }
25
+ return flags;
26
+ }
27
+
28
+ module.exports = { parseArgs };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const { SlashCommand } = require('./slash-commands');
4
+
5
+ class CommandRegistry {
6
+ constructor() {
7
+ this.commands = new Map();
8
+ this.aliases = new Map();
9
+ this.groups = new Map();
10
+ }
11
+
12
+ register(command) {
13
+ if (!(command instanceof SlashCommand)) {
14
+ throw new Error('Command must be an instance of SlashCommand');
15
+ }
16
+
17
+ this.commands.set(command.name, command);
18
+
19
+ for (const alias of command.aliases) {
20
+ this.aliases.set(alias, command.name);
21
+ }
22
+
23
+ const group = command.group || 'General';
24
+ if (!this.groups.has(group)) {
25
+ this.groups.set(group, []);
26
+ }
27
+ this.groups.get(group).push(command.name);
28
+
29
+ return this;
30
+ }
31
+
32
+ unregister(name) {
33
+ const command = this.commands.get(name) || this.getCommand(this.aliases.get(name));
34
+ if (!command) return false;
35
+
36
+ this.commands.delete(command.name);
37
+ for (const alias of command.aliases) {
38
+ this.aliases.delete(alias);
39
+ }
40
+
41
+ const group = this.groups.get(command.group);
42
+ if (group) {
43
+ const idx = group.indexOf(command.name);
44
+ if (idx !== -1) group.splice(idx, 1);
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ getCommand(name) {
51
+ if (!name) return null;
52
+ const resolvedName = this.aliases.get(name) || name;
53
+ return this.commands.get(resolvedName) || null;
54
+ }
55
+
56
+ async execute(name, args, context) {
57
+ const command = this.getCommand(name);
58
+ if (!command) {
59
+ return { success: false, error: `Unknown command: ${name}` };
60
+ }
61
+
62
+ try {
63
+ if (command.middleware && command.middleware.length > 0) {
64
+ for (const middleware of command.middleware) {
65
+ const result = await middleware(args, context);
66
+ if (result === false) {
67
+ return { success: false, error: 'Middleware rejected command' };
68
+ }
69
+ }
70
+ }
71
+
72
+ const result = await command.execute(args, context);
73
+ return { success: true, result };
74
+ } catch (error) {
75
+ return { success: false, error: error.message };
76
+ }
77
+ }
78
+
79
+ listCommands() {
80
+ const commands = [];
81
+ for (const [name, command] of this.commands) {
82
+ commands.push({
83
+ name,
84
+ description: command.description,
85
+ group: command.group,
86
+ aliases: command.aliases,
87
+ usage: command.usage,
88
+ });
89
+ }
90
+ return commands;
91
+ }
92
+
93
+ listByGroup() {
94
+ const grouped = {};
95
+ for (const [group, commandNames] of this.groups) {
96
+ grouped[group] = commandNames.map((name) => {
97
+ const cmd = this.commands.get(name);
98
+ return {
99
+ name,
100
+ description: cmd.description,
101
+ usage: cmd.usage,
102
+ };
103
+ });
104
+ }
105
+ return grouped;
106
+ }
107
+
108
+ findCommands(query) {
109
+ const results = [];
110
+ const lowerQuery = query.toLowerCase();
111
+
112
+ for (const [name, command] of this.commands) {
113
+ if (
114
+ name.toLowerCase().includes(lowerQuery) ||
115
+ command.description.toLowerCase().includes(lowerQuery)
116
+ ) {
117
+ results.push({
118
+ name,
119
+ description: command.description,
120
+ group: command.group,
121
+ usage: command.usage,
122
+ });
123
+ }
124
+ }
125
+
126
+ return results;
127
+ }
128
+ }
129
+
130
+ module.exports = { CommandRegistry };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ // One permanent 'line' listener feeding a queue + waiter list; nextLine() serves
4
+ // both the main prompt and nested confirmations in arrival order so no piped or
5
+ // pasted input line is ever dropped.
6
+ function createLineQueue(rl) {
7
+ const queue = [];
8
+ const waiters = [];
9
+ const state = { closed: false };
10
+
11
+ rl.on('line', (line) => {
12
+ const w = waiters.shift();
13
+ if (w) w.resolve(line);
14
+ else queue.push(line);
15
+ });
16
+
17
+ rl.on('close', () => {
18
+ state.closed = true;
19
+ while (waiters.length) waiters.shift().resolve(null);
20
+ });
21
+
22
+ function nextLine() {
23
+ if (queue.length > 0) return Promise.resolve(queue.shift());
24
+ if (state.closed) return Promise.resolve(null);
25
+ return new Promise((resolve) => waiters.push({ resolve }));
26
+ }
27
+
28
+ function isClosed() {
29
+ return state.closed;
30
+ }
31
+
32
+ return { nextLine, isClosed };
33
+ }
34
+
35
+ module.exports = { createLineQueue };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const ui = require('../ui/terminal.js');
4
+
5
+ async function runOneShot({ prompt, agent }) {
6
+ try {
7
+ process.stdout.write(`${ui.c.green}➜${ui.c.reset} ${prompt}\n`);
8
+ ui.divider();
9
+ await agent.runTurn(prompt);
10
+ return 0;
11
+ } catch (e) {
12
+ ui.error(e && e.message ? e.message : String(e));
13
+ return 1;
14
+ }
15
+ }
16
+
17
+ module.exports = { runOneShot };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const config = require('../config.js');
5
+ const ui = require('../ui/terminal.js');
6
+ const { handle, EXIT } = require('./slash-commands.js');
7
+
8
+ async function runRepl({ cfg, rl, lineQueue, agent, mcp }) {
9
+ const nextLine = lineQueue.nextLine;
10
+ const rt = { cfg, agent, mcp };
11
+ const basename = path.basename(cfg.workspaceRoot);
12
+
13
+ try {
14
+ while (true) {
15
+ if (!lineQueue.isClosed()) {
16
+ rl.setPrompt(ui.promptText(basename));
17
+ rl.prompt();
18
+ }
19
+ const line = await nextLine();
20
+ if (line === null) break;
21
+ const trimmed = line.trim();
22
+ if (!trimmed) continue;
23
+ ui.divider();
24
+ let result;
25
+ try {
26
+ result = await handle(trimmed, rt);
27
+ } catch (e) {
28
+ ui.error(e && e.message ? e.message : String(e));
29
+ continue;
30
+ }
31
+ if (result === EXIT) break;
32
+ if (result === false) {
33
+ await agent.runTurn(trimmed).catch((e) => {
34
+ ui.error(e && e.message ? e.message : String(e));
35
+ });
36
+ }
37
+ }
38
+ } finally {
39
+ ui.info('Goodbye.');
40
+ try {
41
+ rl.close();
42
+ } catch {}
43
+ }
44
+ }
45
+
46
+ module.exports = { runRepl };
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const api = require('../providers/openai-compatible.js');
6
+ const { listSessions } = require('../core/sessions-store.js');
7
+ const { APPROVAL_MODES } = require('../core/approvals.js');
8
+ const config = require('../config.js');
9
+ const ui = require('../ui/terminal.js');
10
+
11
+ const EXIT = Symbol('exit');
12
+
13
+ const HELP_LINES = [
14
+ 'Commands:',
15
+ ' /help Show this help',
16
+ ' /models List models from the live registry (cached list on failure)',
17
+ ' /model [id] Show or switch the active model (persists)',
18
+ ' /approvals [mode] suggest | auto-edit | full-auto (persists)',
19
+ ' /yolo [on|off] Shortcut for full-auto vs suggest',
20
+ ' /sessions List saved conversation sessions',
21
+ ' /resume [n] Resume a saved session in place',
22
+ ' /compact Summarize the conversation to free context',
23
+ ' /usage Session usage stats',
24
+ ' /retry Resend your last message',
25
+ ' /config Show effective configuration',
26
+ ' /clear Reset the conversation (keeps the system prompt)',
27
+ ' /init Generate an AGENTS.md project-instructions file',
28
+ ' /version Print version',
29
+ ' /quit, /exit Leave Draco',
30
+ ];
31
+
32
+ function helpText(cfg) {
33
+ const lines = HELP_LINES.slice();
34
+ if (!cfg.searchApiKey) {
35
+ lines.push(
36
+ '',
37
+ `${ui.c.gray}Web search uses DuckDuckGo (no key needed). Set DRACO_SEARCH_API_KEY to use Brave.${ui.c.reset}`
38
+ );
39
+ }
40
+ return lines.join('\n');
41
+ }
42
+
43
+ function describeSession(entry, i) {
44
+ const d = entry.data;
45
+ const firstUser = (d.messages.find((m) => m.role === 'user' && typeof m.content === 'string') || {}).content;
46
+ let preview = firstUser ? String(firstUser).replace(/\s+/g, ' ').slice(0, 48) : '(no prompt yet)';
47
+ return `${String(i + 1).padStart(2)} ${d.cwd} ${ui.c.gray}${(d.updatedAt || '').slice(0, 16).replace('T', ' ')}${ui.c.reset} ${d.model}\n ${ui.c.dim}${preview}${ui.c.reset}`;
48
+ }
49
+
50
+ async function handle(line, rt) {
51
+ if (!line.startsWith('/')) return false;
52
+ const [cmdRaw, ...rest] = line.trim().split(/\s+/);
53
+ const cmd = cmdRaw.toLowerCase();
54
+ const arg = rest.join(' ').trim();
55
+ const cfg = rt.cfg;
56
+
57
+ switch (cmd) {
58
+ case '/help':
59
+ process.stdout.write(helpText(cfg) + '\n');
60
+ return true;
61
+
62
+ case '/version':
63
+ process.stdout.write(`Draco CLI v${cfg.version}\n`);
64
+ return true;
65
+
66
+ case '/models': {
67
+ ui.info('Fetching model registry…');
68
+ try {
69
+ const ids = await api.listModels(cfg);
70
+ for (const id of ids) {
71
+ const marker = id === rt.agent.activeModel ? `${ui.c.green}*${ui.c.reset} ` : ' ';
72
+ process.stdout.write(`${marker}${id}\n`);
73
+ }
74
+ process.stdout.write(
75
+ `${ui.c.gray}(active marked with *; some accounts use an "opencode/" prefix — switch with /model opencode/<id>)${ui.c.reset}\n`
76
+ );
77
+ } catch (e) {
78
+ ui.error(
79
+ `Could not fetch live registry (${e.message || e}). Showing cached known free models:`
80
+ );
81
+ for (const id of config.KNOWN_FREE_MODELS) {
82
+ const marker = id === rt.agent.activeModel ? `${ui.c.green}*${ui.c.reset} ` : ' ';
83
+ process.stdout.write(`${marker}${id}\n`);
84
+ }
85
+ }
86
+ return true;
87
+ }
88
+
89
+ case '/model': {
90
+ if (!arg) {
91
+ process.stdout.write(`Active model: ${rt.agent.activeModel}\n`);
92
+ return true;
93
+ }
94
+ rt.agent.setModel(arg);
95
+ try {
96
+ config.persistConfig({ model: arg });
97
+ ui.success(`Model set to ${arg} (persisted to ${cfg.configFile}).`);
98
+ } catch (e) {
99
+ ui.error(`Model switched to ${arg} for this session, but persisting failed: ${e.message}`);
100
+ }
101
+ return true;
102
+ }
103
+
104
+ case '/yolo': {
105
+ const next = arg === 'on' ? 'full-auto' : arg === 'off' ? 'suggest' : (cfg.approvals === 'full-auto' ? 'suggest' : 'full-auto');
106
+ cfg.approvals = next;
107
+ cfg.yolo = next === 'full-auto';
108
+ try {
109
+ config.persistConfig({ approvals: next });
110
+ } catch {}
111
+ if (next === 'full-auto') ui.warn('FULL-AUTO — shell commands run without confirmation. Use with care.');
112
+ else ui.success(`Approval mode: ${next}.`);
113
+ return true;
114
+ }
115
+
116
+ case '/approvals': {
117
+ const modes = APPROVAL_MODES;
118
+ if (!arg) {
119
+ process.stdout.write(
120
+ [
121
+ `Approval mode: ${cfg.approvals}`,
122
+ '',
123
+ `${ui.c.bold}suggest${ui.c.reset} ask before file writes AND shell commands`,
124
+ `${ui.c.bold}auto-edit${ui.c.reset} apply file edits automatically, still ask before shell`,
125
+ `${ui.c.bold}full-auto${ui.c.reset} everything runs without asking`,
126
+ ].join('\n') + '\n'
127
+ );
128
+ return true;
129
+ }
130
+ const mode = arg.toLowerCase();
131
+ if (!modes.includes(mode)) {
132
+ ui.error(`Unknown mode "${arg}". Use one of: ${modes.join(', ')}.`);
133
+ return true;
134
+ }
135
+ cfg.approvals = mode;
136
+ cfg.yolo = mode === 'full-auto';
137
+ try {
138
+ config.persistConfig({ approvals: mode });
139
+ ui.success(`Approval mode: ${mode} (persisted).`);
140
+ } catch {
141
+ ui.success(`Approval mode: ${mode} (this session).`);
142
+ }
143
+ return true;
144
+ }
145
+
146
+ case '/sessions': {
147
+ const sessions = listSessions();
148
+ if (!sessions.length) {
149
+ ui.info('No saved sessions yet. Conversations auto-save as you chat.');
150
+ return true;
151
+ }
152
+ process.stdout.write(sessions.map((s, i) => describeSession(s, i)).join('\n') + '\n');
153
+ return true;
154
+ }
155
+
156
+ case '/resume': {
157
+ const sessions = listSessions();
158
+ if (!sessions.length) {
159
+ ui.info('No saved sessions to resume.');
160
+ return true;
161
+ }
162
+ const idx = parseInt(arg, 10);
163
+ if (!Number.isInteger(idx) || idx < 1 || idx > sessions.length) {
164
+ process.stdout.write(sessions.map((s, i) => describeSession(s, i)).join('\n') + '\n');
165
+ ui.info('Run /resume <number> to restore one.');
166
+ return true;
167
+ }
168
+ const chosen = sessions[idx - 1];
169
+ if (rt.agent.importSession(chosen.data)) {
170
+ ui.success(`Resumed session from ${chosen.data.cwd} (${chosen.data.model}).`);
171
+ } else {
172
+ ui.error('That session could not be restored.');
173
+ }
174
+ return true;
175
+ }
176
+
177
+ case '/compact': {
178
+ await rt.agent.compact();
179
+ return true;
180
+ }
181
+
182
+ case '/usage': {
183
+ const u = rt.agent.getUsage();
184
+ const lines = [
185
+ `messages in context: ${u.messages}`,
186
+ `turns this session: ${u.turns}`,
187
+ `response characters: ${u.outputChars}`,
188
+ `approx output tokens: ~${Math.round(u.outputChars / 4)}`,
189
+ ];
190
+ process.stdout.write(lines.join('\n') + '\n');
191
+ return true;
192
+ }
193
+
194
+ case '/init': {
195
+ const target = path.join(cfg.workspaceRoot, 'AGENTS.md');
196
+ if (fs.existsSync(target)) {
197
+ ui.warn('AGENTS.md already exists here — edit it directly.');
198
+ return true;
199
+ }
200
+ const template = [
201
+ `# AGENTS.md`,
202
+ '',
203
+ `Instructions for Draco (and other coding agents) working in this repository.`,
204
+ '',
205
+ '## Project overview',
206
+ '- What this project does:',
207
+ '',
208
+ '## Build & test commands',
209
+ '- Install:',
210
+ '- Build:',
211
+ '- Test:',
212
+ '',
213
+ '## Conventions',
214
+ '- Language/framework:',
215
+ '- Style rules:',
216
+ '- Files or directories never to modify:',
217
+ '',
218
+ ].join('\n');
219
+ try {
220
+ fs.writeFileSync(target, template);
221
+ ui.success(`Created ${target}. Fill it in — Draco reads it automatically every session.`);
222
+ } catch (e) {
223
+ ui.error(`Could not write AGENTS.md: ${e.message}`);
224
+ }
225
+ return true;
226
+ }
227
+
228
+ case '/config': {
229
+ const k = cfg.apiKey;
230
+ const masked = k
231
+ ? (k.length > 10 ? `${k.slice(0, 4)}…${k.slice(-4)}` : '•••')
232
+ : 'not set';
233
+ const mcpCount = rt.mcp ? rt.mcp.listTools().length : 0;
234
+ const lines = [
235
+ `version: ${cfg.version}`,
236
+ `endpoint: ${cfg.apiBase}`,
237
+ `model: ${rt.agent.activeModel}`,
238
+ `approvals: ${cfg.approvals}`,
239
+ `fallbackChain: ${cfg.fallbackChain.join(' -> ')}`,
240
+ `search: ${cfg.searchApiKey ? `${cfg.searchProvider} (key set)` : 'duckduckgo (keyless)'}`,
241
+ `mcp tools: ${mcpCount}`,
242
+ `workspaceRoot: ${cfg.workspaceRoot}`,
243
+ `config file: ${cfg.configFile}`,
244
+ `api key: ${masked} (${cfg.keySource || 'unknown source'})`,
245
+ ];
246
+ process.stdout.write(lines.map((l) => `${ui.c.gray}${l}${ui.c.reset}`).join('\n') + '\n');
247
+ return true;
248
+ }
249
+
250
+ case '/clear':
251
+ rt.agent.reset();
252
+ rt.agent.clearSavedSession();
253
+ ui.success('Conversation cleared.');
254
+ return true;
255
+
256
+ case '/retry':
257
+ await rt.agent.retry();
258
+ return true;
259
+
260
+ case '/quit':
261
+ case '/exit':
262
+ return EXIT;
263
+
264
+ default:
265
+ ui.error(`Unknown command "${cmdRaw}". Type /help for commands.`);
266
+ return true;
267
+ }
268
+ }
269
+
270
+ module.exports = { handle, EXIT };
271
+