0agent 1.0.18 → 1.0.20
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/bin/0agent.js +44 -11
- package/bin/chat.js +427 -0
- package/package.json +6 -3
package/bin/0agent.js
CHANGED
|
@@ -45,7 +45,22 @@ if (cmd.startsWith('/')) {
|
|
|
45
45
|
|
|
46
46
|
// ─── Command dispatch ────────────────────────────────────────────────────
|
|
47
47
|
switch (cmd) {
|
|
48
|
-
case '':
|
|
48
|
+
case '': {
|
|
49
|
+
// No args: if configured → open chat TUI; if not → run init wizard
|
|
50
|
+
if (existsSync(CONFIG_PATH)) {
|
|
51
|
+
const { spawn: spawnC } = await import('node:child_process');
|
|
52
|
+
const chatSc = resolve(dirname(new URL(import.meta.url).pathname), 'chat.js');
|
|
53
|
+
if (existsSync(chatSc)) {
|
|
54
|
+
const p = spawnC(process.execPath, [chatSc], { stdio: 'inherit' });
|
|
55
|
+
await new Promise(r => p.on('close', r));
|
|
56
|
+
} else {
|
|
57
|
+
await runChat();
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
await runInit();
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
49
64
|
case 'init':
|
|
50
65
|
await runInit();
|
|
51
66
|
break;
|
|
@@ -66,9 +81,18 @@ switch (cmd) {
|
|
|
66
81
|
await runTask(args.slice(1));
|
|
67
82
|
break;
|
|
68
83
|
|
|
69
|
-
case 'chat':
|
|
70
|
-
|
|
84
|
+
case 'chat': {
|
|
85
|
+
// Launch the persistent Claude Code-style TUI
|
|
86
|
+
const { spawn: spawnChat } = await import('node:child_process');
|
|
87
|
+
const chatScript = resolve(dirname(new URL(import.meta.url).pathname), 'chat.js');
|
|
88
|
+
if (existsSync(chatScript)) {
|
|
89
|
+
const p = spawnChat(process.execPath, [chatScript], { stdio: 'inherit' });
|
|
90
|
+
await new Promise(r => p.on('close', r));
|
|
91
|
+
} else {
|
|
92
|
+
await runChat(); // fallback to old simple REPL
|
|
93
|
+
}
|
|
71
94
|
break;
|
|
95
|
+
}
|
|
72
96
|
|
|
73
97
|
case 'skill':
|
|
74
98
|
await runSkillCommand(args.slice(1));
|
|
@@ -112,20 +136,28 @@ switch (cmd) {
|
|
|
112
136
|
// Arrow-key select using enquirer (falls back to number input if not available)
|
|
113
137
|
async function arrowSelect(message, choices, initial = 0) {
|
|
114
138
|
try {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
139
|
+
const mod = await import('enquirer');
|
|
140
|
+
const Enquirer = mod.default ?? mod;
|
|
141
|
+
const prompt = new Enquirer.Select({
|
|
142
|
+
message,
|
|
143
|
+
choices: choices.map((c, i) => ({ name: c, value: String(i) })),
|
|
144
|
+
initial,
|
|
145
|
+
});
|
|
117
146
|
const answer = await prompt.run();
|
|
118
|
-
|
|
147
|
+
// answer is the name string — find its index
|
|
148
|
+
const idx = choices.indexOf(answer);
|
|
149
|
+
return idx >= 0 ? idx : initial;
|
|
119
150
|
} catch {
|
|
120
|
-
// Fallback: number-based selection
|
|
151
|
+
// Fallback: number-based selection if enquirer unavailable or non-TTY
|
|
121
152
|
return choose(message, choices, initial);
|
|
122
153
|
}
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
async function arrowInput(message, initial = '') {
|
|
126
157
|
try {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
158
|
+
const mod = await import('enquirer');
|
|
159
|
+
const Enquirer = mod.default ?? mod;
|
|
160
|
+
const prompt = new Enquirer.Input({ message, initial });
|
|
129
161
|
return await prompt.run();
|
|
130
162
|
} catch {
|
|
131
163
|
return ask(` ${message}: `);
|
|
@@ -134,8 +166,9 @@ async function arrowInput(message, initial = '') {
|
|
|
134
166
|
|
|
135
167
|
async function arrowPassword(message) {
|
|
136
168
|
try {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
169
|
+
const mod = await import('enquirer');
|
|
170
|
+
const Enquirer = mod.default ?? mod;
|
|
171
|
+
const prompt = new Enquirer.Password({ message });
|
|
139
172
|
return await prompt.run();
|
|
140
173
|
} catch {
|
|
141
174
|
return ask(` ${message}: `);
|
package/bin/chat.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 0agent Chat — persistent, streaming TUI.
|
|
4
|
+
*
|
|
5
|
+
* Stays open like Claude Code. Commands start with /.
|
|
6
|
+
* Responses stream word-by-word. Subagents visible inline.
|
|
7
|
+
* /model to switch. /key to add provider keys. Never forgets previous keys.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import YAML from 'yaml';
|
|
15
|
+
|
|
16
|
+
const AGENT_DIR = resolve(homedir(), '.0agent');
|
|
17
|
+
const CONFIG_PATH = resolve(AGENT_DIR, 'config.yaml');
|
|
18
|
+
const BASE_URL = process.env['ZEROAGENT_URL'] ?? 'http://localhost:4200';
|
|
19
|
+
|
|
20
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
21
|
+
const C = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
white: '\x1b[37m',
|
|
32
|
+
};
|
|
33
|
+
const fmt = (color, text) => `${color}${text}${C.reset}`;
|
|
34
|
+
const clearLine = () => process.stdout.write('\r\x1b[2K');
|
|
35
|
+
|
|
36
|
+
// ─── Config management ────────────────────────────────────────────────────────
|
|
37
|
+
function loadConfig() {
|
|
38
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
39
|
+
try { return YAML.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveConfig(cfg) {
|
|
43
|
+
writeFileSync(CONFIG_PATH, YAML.stringify(cfg), 'utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getCurrentProvider(cfg) {
|
|
47
|
+
const def = cfg?.llm_providers?.find(p => p.is_default) ?? cfg?.llm_providers?.[0];
|
|
48
|
+
return def ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── State ────────────────────────────────────────────────────────────────────
|
|
52
|
+
let cfg = loadConfig();
|
|
53
|
+
let sessionId = null; // current chat session
|
|
54
|
+
let streaming = false; // mid-token-stream
|
|
55
|
+
let ws = null; // WebSocket
|
|
56
|
+
let wsReady = false;
|
|
57
|
+
let pendingResolve = null;
|
|
58
|
+
let lineBuffer = ''; // accumulated line for stream
|
|
59
|
+
const history = []; // command history for arrow keys
|
|
60
|
+
|
|
61
|
+
// ─── Header ──────────────────────────────────────────────────────────────────
|
|
62
|
+
function printHeader() {
|
|
63
|
+
const provider = getCurrentProvider(cfg);
|
|
64
|
+
const modelStr = provider ? `${provider.provider}/${provider.model}` : 'no model';
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(fmt(C.bold, ' 0agent') + fmt(C.dim, ` — ${modelStr}`));
|
|
67
|
+
console.log(fmt(C.dim, ' Type a task, or /help for commands. Ctrl+C to exit.\n'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printInsights() {
|
|
71
|
+
fetch(`${BASE_URL}/api/insights?seen=false`)
|
|
72
|
+
.then(r => r.json())
|
|
73
|
+
.then(insights => {
|
|
74
|
+
if (!Array.isArray(insights) || insights.length === 0) return;
|
|
75
|
+
console.log(fmt(C.yellow, ` ${insights.length} insight${insights.length > 1 ? 's' : ''} since last session:`));
|
|
76
|
+
for (const ins of insights.slice(0, 2)) {
|
|
77
|
+
console.log(` ${fmt(C.dim, '›')} ${ins.summary}`);
|
|
78
|
+
if (ins.suggested_action) console.log(` ${fmt(C.cyan, '→')} ${fmt(C.dim, ins.suggested_action)}`);
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── WebSocket ────────────────────────────────────────────────────────────────
|
|
86
|
+
async function connectWS() {
|
|
87
|
+
try {
|
|
88
|
+
const { default: WS } = await import('ws').catch(() => ({ default: globalThis.WebSocket }));
|
|
89
|
+
ws = new WS(`ws://localhost:4200/ws`);
|
|
90
|
+
ws.on('open', () => {
|
|
91
|
+
wsReady = true;
|
|
92
|
+
ws.send(JSON.stringify({ type: 'subscribe', topics: ['sessions', 'graph', 'insights'] }));
|
|
93
|
+
});
|
|
94
|
+
ws.on('message', data => handleWsEvent(JSON.parse(data.toString())));
|
|
95
|
+
ws.on('close', () => { wsReady = false; setTimeout(connectWS, 2000); });
|
|
96
|
+
ws.on('error', () => { wsReady = false; });
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleWsEvent(event) {
|
|
101
|
+
if (!sessionId || event.session_id !== sessionId) return;
|
|
102
|
+
|
|
103
|
+
switch (event.type) {
|
|
104
|
+
case 'session.step': {
|
|
105
|
+
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
106
|
+
console.log(` ${fmt(C.dim, '›')} ${event.step}`);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'session.token': {
|
|
110
|
+
if (!streaming) { process.stdout.write('\n '); streaming = true; }
|
|
111
|
+
process.stdout.write(event.token);
|
|
112
|
+
lineBuffer += event.token;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'session.completed': {
|
|
116
|
+
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
117
|
+
const r = event.result ?? {};
|
|
118
|
+
if (r.files_written?.length) console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
|
|
119
|
+
if (r.tokens_used) process.stdout.write(fmt(C.dim, `\n ${r.tokens_used} tokens · ${r.model ?? ''}\n`));
|
|
120
|
+
|
|
121
|
+
// Confirm server if port mentioned
|
|
122
|
+
confirmServer(r, lineBuffer);
|
|
123
|
+
lineBuffer = '';
|
|
124
|
+
if (pendingResolve) { pendingResolve(); pendingResolve = null; }
|
|
125
|
+
rl.prompt();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'session.failed': {
|
|
129
|
+
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
130
|
+
console.log(`\n ${fmt(C.red, '✗')} ${event.error}\n`);
|
|
131
|
+
lineBuffer = '';
|
|
132
|
+
if (pendingResolve) { pendingResolve(); pendingResolve = null; }
|
|
133
|
+
rl.prompt();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'agent.insight': {
|
|
137
|
+
// Show insight inline, non-interruptive
|
|
138
|
+
const ins = event.insight ?? {};
|
|
139
|
+
process.stdout.write(`\n ${fmt(C.yellow, '◆')} ${ins.summary}\n`);
|
|
140
|
+
if (ins.suggested_action) process.stdout.write(` ${fmt(C.dim, `→ ${ins.suggested_action}`)}\n`);
|
|
141
|
+
if (!streaming) rl.prompt(true);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'graph.weight_updated': {
|
|
145
|
+
// Subtle dot — graph is learning
|
|
146
|
+
process.stdout.write(fmt(C.dim, '·'));
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function confirmServer(result, output) {
|
|
153
|
+
const allText = [...(result.commands_run ?? []), output].join(' ');
|
|
154
|
+
const portMatch = allText.match(/(?:localhost:|port\s*[=:]?\s*)(\d{4,5})/i);
|
|
155
|
+
if (!portMatch) return;
|
|
156
|
+
const port = parseInt(portMatch[1], 10);
|
|
157
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(2000) });
|
|
160
|
+
console.log(`\n ${fmt(C.green, '⬡')} Live at ${fmt(C.cyan, `http://localhost:${port}`)} (HTTP ${res.status})`);
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Task submission ──────────────────────────────────────────────────────────
|
|
165
|
+
async function runTask(input) {
|
|
166
|
+
let skillName, entityId;
|
|
167
|
+
let task = input;
|
|
168
|
+
|
|
169
|
+
// Parse inline flags: --skill /review, --entity sahil
|
|
170
|
+
const skillMatch = task.match(/--skill\s+([\w-]+)/);
|
|
171
|
+
if (skillMatch) { skillName = skillMatch[1]; task = task.replace(skillMatch[0], '').trim(); }
|
|
172
|
+
const entityMatch = task.match(/--entity\s+([\w-]+)/);
|
|
173
|
+
if (entityMatch) { entityId = entityMatch[1]; task = task.replace(entityMatch[0], '').trim(); }
|
|
174
|
+
|
|
175
|
+
// Slash-prefix → skill
|
|
176
|
+
if (task.startsWith('/') && !task.startsWith('/model') && !task.startsWith('/key')) {
|
|
177
|
+
const parts = task.split(/\s+/);
|
|
178
|
+
skillName = parts[0].slice(1);
|
|
179
|
+
task = parts.slice(1).join(' ') || `Run the /${skillName} skill`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const body = { task, ...(skillName && { skill: skillName }), ...(entityId && { entity_id: entityId }) };
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch(`${BASE_URL}/api/sessions`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify(body),
|
|
189
|
+
});
|
|
190
|
+
const s = await res.json();
|
|
191
|
+
sessionId = s.session_id ?? s.id;
|
|
192
|
+
return new Promise(resolve => { pendingResolve = resolve; });
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.log(` ${fmt(C.red, '✗')} ${e.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
199
|
+
async function handleCommand(input) {
|
|
200
|
+
const parts = input.trim().split(/\s+/);
|
|
201
|
+
const cmd = parts[0].toLowerCase();
|
|
202
|
+
|
|
203
|
+
switch (cmd) {
|
|
204
|
+
// /model — list or switch model
|
|
205
|
+
case '/model': {
|
|
206
|
+
if (!cfg) { console.log(fmt(C.red, ' No config found. Run: 0agent init')); break; }
|
|
207
|
+
const providers = cfg.llm_providers ?? [];
|
|
208
|
+
const current = getCurrentProvider(cfg);
|
|
209
|
+
|
|
210
|
+
if (parts.length === 1) {
|
|
211
|
+
// List current + available
|
|
212
|
+
console.log('\n Current model:');
|
|
213
|
+
console.log(` ${fmt(C.green, '●')} ${current?.provider}/${current?.model}\n`);
|
|
214
|
+
console.log(' All configured providers:');
|
|
215
|
+
for (const p of providers) {
|
|
216
|
+
const marker = p.is_default ? fmt(C.green, '●') : fmt(C.dim, '○');
|
|
217
|
+
console.log(` ${marker} ${p.provider}/${p.model}`);
|
|
218
|
+
}
|
|
219
|
+
console.log('\n ' + fmt(C.dim, 'Usage: /model anthropic claude-opus-4-6'));
|
|
220
|
+
console.log(' ' + fmt(C.dim, ' /model openai gpt-4o'));
|
|
221
|
+
console.log(' ' + fmt(C.dim, ' /model add anthropic sk-ant-...') + '\n');
|
|
222
|
+
} else if (parts[1] === 'add') {
|
|
223
|
+
// /model add <provider> <api-key>
|
|
224
|
+
const provider = parts[2];
|
|
225
|
+
const key = parts[3];
|
|
226
|
+
if (!provider || !key) { console.log(fmt(C.red, ' Usage: /model add <provider> <api-key>')); break; }
|
|
227
|
+
const defaultModels = { anthropic: 'claude-sonnet-4-6', openai: 'gpt-4o', xai: 'grok-3', gemini: 'gemini-2.0-flash', ollama: 'llama3.1' };
|
|
228
|
+
const existing = providers.findIndex(p => p.provider === provider);
|
|
229
|
+
if (existing >= 0) {
|
|
230
|
+
cfg.llm_providers[existing].api_key = key;
|
|
231
|
+
console.log(` ${fmt(C.green, '✓')} Updated ${provider} API key`);
|
|
232
|
+
} else {
|
|
233
|
+
cfg.llm_providers.push({ provider, model: defaultModels[provider] ?? provider, api_key: key, is_default: false });
|
|
234
|
+
console.log(` ${fmt(C.green, '✓')} Added ${provider}`);
|
|
235
|
+
}
|
|
236
|
+
saveConfig(cfg);
|
|
237
|
+
console.log(` ${fmt(C.dim, 'Restart daemon for changes to take effect: 0agent stop && 0agent start')}\n`);
|
|
238
|
+
} else {
|
|
239
|
+
// /model <provider> <model> OR /model <provider>
|
|
240
|
+
const provider = parts[1];
|
|
241
|
+
const model = parts[2];
|
|
242
|
+
// Set as default
|
|
243
|
+
for (const p of providers) p.is_default = false;
|
|
244
|
+
const match = providers.find(p => p.provider === provider);
|
|
245
|
+
if (match) {
|
|
246
|
+
match.is_default = true;
|
|
247
|
+
if (model) match.model = model;
|
|
248
|
+
saveConfig(cfg);
|
|
249
|
+
cfg = loadConfig();
|
|
250
|
+
console.log(` ${fmt(C.green, '✓')} Switched to ${fmt(C.cyan, `${provider}/${match.model}`)}`);
|
|
251
|
+
console.log(` ${fmt(C.dim, 'Restart daemon to apply: 0agent stop && 0agent start\n')}`);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(` ${fmt(C.red, '✗')} Provider "${provider}" not found. Add it first with:`);
|
|
254
|
+
console.log(` ${fmt(C.dim, `/model add ${provider} <api-key>`)}\n`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// /key — add or list API keys
|
|
261
|
+
case '/key': {
|
|
262
|
+
if (!cfg) { console.log(fmt(C.red, ' No config found. Run: 0agent init')); break; }
|
|
263
|
+
if (parts.length === 1) {
|
|
264
|
+
// List masked keys
|
|
265
|
+
console.log('\n Stored API keys:\n');
|
|
266
|
+
for (const p of cfg.llm_providers ?? []) {
|
|
267
|
+
const masked = p.api_key ? p.api_key.slice(0, 10) + '••••••' : fmt(C.dim, '(not set)');
|
|
268
|
+
console.log(` ${p.provider.padEnd(12)} ${masked}`);
|
|
269
|
+
}
|
|
270
|
+
console.log('\n ' + fmt(C.dim, 'Usage: /key <provider> <api-key>') + '\n');
|
|
271
|
+
} else {
|
|
272
|
+
const provider = parts[1];
|
|
273
|
+
const key = parts[2];
|
|
274
|
+
if (!key) { console.log(fmt(C.red, ` Usage: /key ${provider} <api-key>`)); break; }
|
|
275
|
+
const match = cfg.llm_providers?.find(p => p.provider === provider);
|
|
276
|
+
if (match) {
|
|
277
|
+
match.api_key = key;
|
|
278
|
+
saveConfig(cfg);
|
|
279
|
+
cfg = loadConfig();
|
|
280
|
+
console.log(` ${fmt(C.green, '✓')} ${provider} key updated (${key.slice(0, 8)}••••)\n`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(` ${fmt(C.yellow, '⚠')} "${provider}" not configured. Use /model add ${provider} ${key}\n`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// /status
|
|
289
|
+
case '/status': {
|
|
290
|
+
try {
|
|
291
|
+
const h = await fetch(`${BASE_URL}/api/health`).then(r => r.json());
|
|
292
|
+
console.log(`\n ${fmt(C.green, '✓')} Daemon running`);
|
|
293
|
+
console.log(` Graph: ${h.graph_nodes} nodes · ${h.graph_edges} edges`);
|
|
294
|
+
console.log(` Sessions: ${h.active_sessions} active`);
|
|
295
|
+
console.log(` Sandbox: ${h.sandbox_backend}\n`);
|
|
296
|
+
} catch {
|
|
297
|
+
console.log(` ${fmt(C.red, '✗')} Daemon not running. Run: 0agent start\n`);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// /skills
|
|
303
|
+
case '/skills': {
|
|
304
|
+
try {
|
|
305
|
+
const skills = await fetch(`${BASE_URL}/api/skills`).then(r => r.json());
|
|
306
|
+
const list = Array.isArray(skills) ? skills : skills.skills ?? [];
|
|
307
|
+
console.log('\n Available skills:\n');
|
|
308
|
+
for (const s of list.slice(0, 15)) {
|
|
309
|
+
console.log(` ${fmt(C.cyan, `/${s.name.padEnd(20)}`)} ${fmt(C.dim, s.description?.slice(0, 55) ?? '')}`);
|
|
310
|
+
}
|
|
311
|
+
if (list.length > 15) console.log(fmt(C.dim, ` ... and ${list.length - 15} more\n`));
|
|
312
|
+
else console.log();
|
|
313
|
+
} catch { console.log(fmt(C.dim, ' Daemon not running\n')); }
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// /graph
|
|
318
|
+
case '/graph': {
|
|
319
|
+
console.log(` ${fmt(C.cyan, 'Knowledge graph:')} ${fmt(C.dim, 'http://localhost:4200')}\n`);
|
|
320
|
+
try {
|
|
321
|
+
const { execSync } = await import('node:child_process');
|
|
322
|
+
execSync('open http://localhost:4200 2>/dev/null || xdg-open http://localhost:4200 2>/dev/null', { stdio: 'ignore' });
|
|
323
|
+
} catch {}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// /clear
|
|
328
|
+
case '/clear':
|
|
329
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
330
|
+
printHeader();
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
// /help
|
|
334
|
+
case '/help':
|
|
335
|
+
default: {
|
|
336
|
+
console.log('\n Commands:\n');
|
|
337
|
+
const cmds = [
|
|
338
|
+
['/model', 'Show or switch model (/model openai gpt-4o)'],
|
|
339
|
+
['/model add', 'Add provider key (/model add anthropic sk-ant-...)'],
|
|
340
|
+
['/key <provider>', 'Update stored API key'],
|
|
341
|
+
['/status', 'Daemon health + graph stats'],
|
|
342
|
+
['/skills', 'List available skills'],
|
|
343
|
+
['/graph', 'Open 3D knowledge graph in browser'],
|
|
344
|
+
['/clear', 'Clear screen'],
|
|
345
|
+
['/<skill>', 'Run a skill (/review, /build, /qa, /debug...)'],
|
|
346
|
+
['Ctrl+C', 'Exit'],
|
|
347
|
+
];
|
|
348
|
+
for (const [c, d] of cmds) {
|
|
349
|
+
console.log(` ${fmt(C.cyan, c.padEnd(20))} ${fmt(C.dim, d)}`);
|
|
350
|
+
}
|
|
351
|
+
console.log();
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Main REPL ────────────────────────────────────────────────────────────────
|
|
358
|
+
const rl = createInterface({
|
|
359
|
+
input: process.stdin,
|
|
360
|
+
output: process.stdout,
|
|
361
|
+
prompt: `\n ${fmt(C.cyan, '›')} `,
|
|
362
|
+
historySize: 100,
|
|
363
|
+
completer: (line) => {
|
|
364
|
+
const commands = ['/model', '/key', '/status', '/skills', '/graph', '/clear', '/help',
|
|
365
|
+
'/review', '/build', '/debug', '/qa', '/research', '/refactor', '/test-writer', '/retro'];
|
|
366
|
+
const hits = commands.filter(c => c.startsWith(line));
|
|
367
|
+
return [hits.length ? hits : commands, line];
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Restore history from conversations if possible
|
|
372
|
+
rl.on('history', () => {});
|
|
373
|
+
|
|
374
|
+
printHeader();
|
|
375
|
+
printInsights();
|
|
376
|
+
|
|
377
|
+
// Connect WebSocket for live events
|
|
378
|
+
connectWS();
|
|
379
|
+
|
|
380
|
+
// Check if daemon is running
|
|
381
|
+
fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(1500) })
|
|
382
|
+
.then(() => { rl.prompt(); })
|
|
383
|
+
.catch(async () => {
|
|
384
|
+
process.stdout.write(fmt(C.dim, ' Starting daemon'));
|
|
385
|
+
// Trigger auto-start via requireDaemon-equivalent
|
|
386
|
+
const { resolve: res, existsSync: ef } = await import('node:path').then(m => m);
|
|
387
|
+
const pkgRoot = res(new URL(import.meta.url).pathname, '..', '..');
|
|
388
|
+
const bundled = res(pkgRoot, 'dist', 'daemon.mjs');
|
|
389
|
+
if (ef(bundled) && ef(CONFIG_PATH)) {
|
|
390
|
+
const { spawn } = await import('node:child_process');
|
|
391
|
+
const child = spawn(process.execPath, [bundled], { detached: true, stdio: 'ignore', env: { ...process.env, ZEROAGENT_CONFIG: CONFIG_PATH } });
|
|
392
|
+
child.unref();
|
|
393
|
+
for (let i = 0; i < 20; i++) {
|
|
394
|
+
await new Promise(r => setTimeout(r, 500));
|
|
395
|
+
process.stdout.write(fmt(C.dim, '.'));
|
|
396
|
+
try { await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(500) }); process.stdout.write(fmt(C.green, ' ✓\n\n')); break; } catch {}
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
console.log(`\n ${fmt(C.dim, 'Run: 0agent start')}\n`);
|
|
400
|
+
}
|
|
401
|
+
rl.prompt();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
rl.on('line', async (input) => {
|
|
405
|
+
const line = input.trim();
|
|
406
|
+
if (!line) { rl.prompt(); return; }
|
|
407
|
+
|
|
408
|
+
if (line.startsWith('/') || ['/model','/key','/status','/skills','/graph','/clear','/help'].some(c => line.startsWith(c))) {
|
|
409
|
+
await handleCommand(line);
|
|
410
|
+
rl.prompt();
|
|
411
|
+
} else {
|
|
412
|
+
await runTask(line);
|
|
413
|
+
// prompt() is called from WS handler after session.completed
|
|
414
|
+
// but fall back if WS not connected
|
|
415
|
+
if (!wsReady) rl.prompt();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
rl.on('close', () => {
|
|
420
|
+
console.log(`\n ${fmt(C.dim, 'Goodbye.')}\n`);
|
|
421
|
+
process.exit(0);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
process.on('SIGINT', () => {
|
|
425
|
+
console.log(`\n ${fmt(C.dim, 'Ctrl+C — type /help for commands or Ctrl+C again to exit')}\n`);
|
|
426
|
+
rl.prompt();
|
|
427
|
+
});
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"0agent": "./bin/0agent.js"
|
|
9
|
+
"0agent": "./bin/0agent.js",
|
|
10
|
+
"0agent-chat": "./bin/chat.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"bin/",
|
|
13
14
|
"dist/",
|
|
14
15
|
"skills/",
|
|
15
|
-
"seeds/"
|
|
16
|
+
"seeds/",
|
|
17
|
+
"bin/chat.js"
|
|
16
18
|
],
|
|
17
19
|
"scripts": {
|
|
18
20
|
"build": "turbo run build",
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"@hono/node-server": "^1.13.0",
|
|
26
28
|
"better-sqlite3": "^11.6.0",
|
|
29
|
+
"enquirer": "^2.4.1",
|
|
27
30
|
"hono": "^4.6.0",
|
|
28
31
|
"ws": "^8.18.0",
|
|
29
32
|
"yaml": "^2.6.0",
|