draco-cli 0.0.0-stage → 3.0.2
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 +21 -0
- package/README.md +76 -3
- package/bin/draco.js +6 -0
- package/package.json +32 -6
- package/src/cli/args.js +28 -0
- package/src/cli/command-registry.js +130 -0
- package/src/cli/line-queue.js +35 -0
- package/src/cli/oneshot.js +17 -0
- package/src/cli/repl.js +46 -0
- package/src/cli/slash-commands.js +271 -0
- package/src/config.js +216 -0
- package/src/core/approvals.js +20 -0
- package/src/core/compaction.js +53 -0
- package/src/core/config-manager.js +174 -0
- package/src/core/fallback.js +11 -0
- package/src/core/images.js +40 -0
- package/src/core/logger.js +207 -0
- package/src/core/middleware.js +102 -0
- package/src/core/plugin-manager.js +149 -0
- package/src/core/prompt.js +45 -0
- package/src/core/session-history.js +161 -0
- package/src/core/session.js +307 -0
- package/src/core/sessions-store.js +56 -0
- package/src/index.js +255 -0
- package/src/integrations/mcp-manager.js +248 -0
- package/src/providers/openai-compatible.js +288 -0
- package/src/tools/fs-tools.js +79 -0
- package/src/tools/index.js +152 -0
- package/src/tools/sandbox.js +21 -0
- package/src/tools/shell-tool.js +43 -0
- package/src/tools/web-tools.js +322 -0
- package/src/ui/diff.js +53 -0
- package/src/ui/markdown.js +106 -0
- package/src/ui/terminal.js +114 -0
- package/src/ui/theme.js +167 -0
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
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Draco CLI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "draco-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "draco-cli",
|
|
3
|
+
"version": "3.0.2",
|
|
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
|
+
}
|
package/src/cli/args.js
ADDED
|
@@ -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 };
|
package/src/cli/repl.js
ADDED
|
@@ -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
|
+
|