0agent 1.0.41 → 1.0.43
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/README.md +226 -89
- package/bin/0agent.js +46 -20
- package/bin/chat.js +172 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,141 +2,216 @@
|
|
|
2
2
|
|
|
3
3
|
**A persistent, learning AI agent that runs on your machine.**
|
|
4
4
|
|
|
5
|
+
> Runs a local daemon. Learns from every task. Remembers everything. Gets better over time.
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npx 0agent@latest
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/0agent)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://nodejs.org)
|
|
10
14
|
|
|
11
15
|
---
|
|
12
16
|
|
|
13
|
-
## What
|
|
17
|
+
## What is this?
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
# Sprint workflow
|
|
17
|
-
0agent /office-hours "I want to build a Slack bot"
|
|
18
|
-
0agent /plan-ceo-review
|
|
19
|
-
0agent /plan-eng-review
|
|
20
|
-
0agent /build
|
|
21
|
-
0agent /review
|
|
22
|
-
0agent /qa --url https://staging.myapp.com
|
|
23
|
-
0agent /ship
|
|
24
|
-
0agent /retro
|
|
25
|
-
|
|
26
|
-
# One-off tasks
|
|
27
|
-
0agent /research "Acme Corp Series B funding"
|
|
28
|
-
0agent /debug "TypeError at auth.ts:47"
|
|
29
|
-
0agent /test-writer src/payments/
|
|
30
|
-
0agent /refactor src/api/routes.ts
|
|
31
|
-
|
|
32
|
-
# Plain language
|
|
33
|
-
0agent run "fix the auth bug Marcus reported"
|
|
34
|
-
0agent run "research Acme Corp and draft a follow-up email to Sarah"
|
|
35
|
-
|
|
36
|
-
# Entity-scoped (learns who you are)
|
|
37
|
-
0agent run "pull auth metrics" --entity sarah_chen
|
|
38
|
-
```
|
|
19
|
+
0agent is a CLI agent that runs as a background daemon on your machine. It executes real tasks — shell commands, file operations, web search, browser automation — using your API key, and learns from every outcome via a weighted knowledge graph.
|
|
39
20
|
|
|
40
|
-
|
|
21
|
+
Unlike chat-based AI tools, 0agent:
|
|
41
22
|
|
|
42
|
-
|
|
23
|
+
- **Persists** — runs in the background, remembers past sessions
|
|
24
|
+
- **Learns** — every task outcome updates edge weights in a graph; plan selection improves over time
|
|
25
|
+
- **Executes** — actually runs commands, writes files, searches the web, opens browsers
|
|
26
|
+
- **Syncs** — optionally backs up the knowledge graph to a private GitHub repo
|
|
43
27
|
|
|
44
|
-
|
|
28
|
+
---
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
- Positive outcomes push them toward 1.0
|
|
48
|
-
- Negative outcomes push them toward 0.0
|
|
49
|
-
- After 100 traces, plan selection is noticeably better
|
|
30
|
+
## Quick start
|
|
50
31
|
|
|
51
|
-
|
|
32
|
+
```bash
|
|
33
|
+
npx 0agent@latest
|
|
34
|
+
```
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
The wizard asks for:
|
|
37
|
+
1. LLM provider + API key (Anthropic, OpenAI, xAI, Gemini, or local Ollama)
|
|
38
|
+
2. GitHub repo for memory backup (optional, uses `gh` CLI if installed)
|
|
39
|
+
3. Workspace folder (where the agent creates files — default: `~/0agent-workspace`)
|
|
40
|
+
4. Embedding provider (for semantic memory search)
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
- **API key** for Anthropic, OpenAI, or a local Ollama instance
|
|
57
|
-
- **Docker** (optional but recommended — enables sandboxed subagents)
|
|
42
|
+
After setup, the chat TUI opens automatically. No manual steps.
|
|
58
43
|
|
|
59
44
|
---
|
|
60
45
|
|
|
61
|
-
##
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Interactive chat
|
|
62
49
|
|
|
63
50
|
```bash
|
|
64
|
-
#
|
|
51
|
+
0agent # open chat (starts daemon if needed)
|
|
65
52
|
npx 0agent@latest
|
|
53
|
+
```
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
```
|
|
56
|
+
0agent — anthropic/claude-sonnet-4-6
|
|
57
|
+
Type a task, or /help for commands.
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
› make a website for my coffee shop and deploy it locally
|
|
60
|
+
› build a REST API in Go with auth
|
|
61
|
+
› research my competitor's pricing and draft a response strategy
|
|
73
62
|
```
|
|
74
63
|
|
|
75
|
-
|
|
64
|
+
Type while the agent works — messages queue automatically and run one after another.
|
|
76
65
|
|
|
77
|
-
|
|
66
|
+
### Slash skills
|
|
78
67
|
|
|
79
68
|
```bash
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
69
|
+
# Software engineering
|
|
70
|
+
0agent /review # code review current branch
|
|
71
|
+
0agent /build # run build, fix errors
|
|
72
|
+
0agent /qa # generate and run tests
|
|
73
|
+
0agent /debug # debug a failing test or error
|
|
74
|
+
0agent /refactor # refactor a file or module
|
|
75
|
+
0agent /test-writer # write unit tests
|
|
76
|
+
0agent /doc # generate documentation
|
|
77
|
+
|
|
78
|
+
# Planning & strategy
|
|
79
|
+
0agent /office-hours "I want to build a payments feature"
|
|
80
|
+
0agent /plan-eng-review
|
|
81
|
+
0agent /plan-ceo-review
|
|
82
|
+
0agent /retro # weekly retrospective
|
|
83
|
+
0agent /ship # pre-release checklist
|
|
87
84
|
|
|
88
|
-
#
|
|
89
|
-
|
|
85
|
+
# Research
|
|
86
|
+
0agent /research "Acme Corp Series B"
|
|
87
|
+
0agent /security-audit
|
|
88
|
+
```
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
node bin/0agent.js status
|
|
90
|
+
### Scheduled tasks
|
|
93
91
|
|
|
94
|
-
# Open dashboard
|
|
95
|
-
open http://localhost:4200
|
|
96
92
|
```
|
|
93
|
+
› /schedule add "run /retro" every Friday at 5pm
|
|
94
|
+
› /schedule add "check the build" every day at 9am
|
|
95
|
+
› /schedule list
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Commands
|
|
99
|
+
|
|
100
|
+
| Command | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `/model` | Show or switch model |
|
|
103
|
+
| `/model add anthropic sk-ant-...` | Add a provider API key |
|
|
104
|
+
| `/key anthropic sk-ant-...` | Update a stored key |
|
|
105
|
+
| `/status` | Daemon health + graph stats |
|
|
106
|
+
| `/skills` | List available skills |
|
|
107
|
+
| `/schedule` | Manage scheduled jobs |
|
|
108
|
+
| `/update` | Update to latest version |
|
|
109
|
+
| `/graph` | Open 3D knowledge graph |
|
|
110
|
+
| `/clear` | Clear screen |
|
|
111
|
+
| `Ctrl+C` | Cancel current task |
|
|
97
112
|
|
|
98
113
|
---
|
|
99
114
|
|
|
100
|
-
##
|
|
115
|
+
## How it learns
|
|
116
|
+
|
|
117
|
+
Every task updates a weighted knowledge graph stored in `~/.0agent/graph.db`.
|
|
101
118
|
|
|
102
119
|
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
Edge weights: 0.0 ──── 0.5 ──── 1.0
|
|
121
|
+
bad neutral good
|
|
122
|
+
|
|
123
|
+
After each task:
|
|
124
|
+
success → weight += 0.1 × learning_rate
|
|
125
|
+
failure → weight -= 0.1 × learning_rate
|
|
126
|
+
decay → weight → 0.5 over time (forgetting)
|
|
107
127
|
```
|
|
108
128
|
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
- **Self-improvement** — weekly analysis of skill gaps, workflow optimization, prompt refinement.
|
|
129
|
+
After ~50 interactions, plan selection measurably improves. The graph also stores:
|
|
130
|
+
- Discovered facts: URLs, ports, file paths, API endpoints (via `memory_write` tool)
|
|
131
|
+
- Conversation history (last 8 exchanges injected as context)
|
|
132
|
+
- Identity + personality per entity
|
|
114
133
|
|
|
115
134
|
---
|
|
116
135
|
|
|
117
|
-
##
|
|
136
|
+
## Memory sync
|
|
118
137
|
|
|
119
|
-
0agent can
|
|
138
|
+
0agent can back up its knowledge graph to a private GitHub repo:
|
|
120
139
|
|
|
121
|
-
```
|
|
122
|
-
#
|
|
123
|
-
|
|
140
|
+
```bash
|
|
141
|
+
# Set up during init, or add manually to ~/.0agent/config.yaml:
|
|
142
|
+
github_memory:
|
|
124
143
|
enabled: true
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
token: ghp_...
|
|
145
|
+
owner: your-username
|
|
146
|
+
repo: 0agent-memory
|
|
128
147
|
```
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
149
|
+
- **Pulls** on daemon start
|
|
150
|
+
- **Pushes** every 30 minutes if there are changes
|
|
151
|
+
- **Final push** on daemon shutdown
|
|
152
|
+
- The same repo doubles as a GitHub Codespace template for browser sessions
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## What can the agent actually do?
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
| Capability | How |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Run shell commands | `shell_exec` — bash, any CLI tool |
|
|
161
|
+
| Read / write files | `file_op` — read, write, list, mkdir |
|
|
162
|
+
| Search the web | `web_search` — DuckDuckGo, no API key needed |
|
|
163
|
+
| Scrape pages | `scrape_url` — full page text, tables, links |
|
|
164
|
+
| Open browser | `browser_open` — system Chrome or default OS browser |
|
|
165
|
+
| Remember facts | `memory_write` — persists to knowledge graph |
|
|
166
|
+
| Schedule tasks | Natural language cron via `/schedule` |
|
|
167
|
+
| Self-heal | Detects runtime errors, proposes + applies patches |
|
|
136
168
|
|
|
137
169
|
---
|
|
138
170
|
|
|
139
|
-
##
|
|
171
|
+
## Architecture
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
npx 0agent@latest
|
|
175
|
+
│
|
|
176
|
+
▼
|
|
177
|
+
┌─────────────────────────────────────────────────────────┐
|
|
178
|
+
│ CLI (bin/0agent.js + bin/chat.js) │
|
|
179
|
+
│ • Init wizard • Chat TUI • Slash commands │
|
|
180
|
+
└───────────────────────┬─────────────────────────────────┘
|
|
181
|
+
│ HTTP + WebSocket
|
|
182
|
+
▼
|
|
183
|
+
┌─────────────────────────────────────────────────────────┐
|
|
184
|
+
│ Daemon (dist/daemon.mjs) — port 4200 │
|
|
185
|
+
│ │
|
|
186
|
+
│ SessionManager ── AgentExecutor ── LLMExecutor │
|
|
187
|
+
│ │ │ │ │
|
|
188
|
+
│ │ CapabilityRegistry │ │
|
|
189
|
+
│ │ • shell_exec │ │
|
|
190
|
+
│ │ • file_op │ │
|
|
191
|
+
│ │ • web_search │ │
|
|
192
|
+
│ │ • scrape_url │ │
|
|
193
|
+
│ │ • browser_open │ │
|
|
194
|
+
│ │ • memory_write │ │
|
|
195
|
+
│ │ │ │
|
|
196
|
+
│ KnowledgeGraph ◄────── outcome feedback ┘ │
|
|
197
|
+
│ (SQLite + HNSW) │
|
|
198
|
+
│ │ │
|
|
199
|
+
│ GitHubMemorySync ── SchedulerManager ── SelfHealLoop │
|
|
200
|
+
└─────────────────────────────────────────────────────────┘
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Key packages:**
|
|
204
|
+
|
|
205
|
+
| Package | Description |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `packages/core` | Knowledge graph, inference engine, storage adapters |
|
|
208
|
+
| `packages/daemon` | HTTP server, session manager, agent executor, capabilities |
|
|
209
|
+
| `bin/chat.js` | Claude Code-style TUI with message queue, WS events, spinner |
|
|
210
|
+
| `bin/0agent.js` | CLI entry point, init wizard, daemon lifecycle |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Configuration
|
|
140
215
|
|
|
141
216
|
`~/.0agent/config.yaml` — created by `0agent init`, edit anytime:
|
|
142
217
|
|
|
@@ -144,21 +219,83 @@ The company graph sees `[from member] Sarah used /build` — not the raw convers
|
|
|
144
219
|
llm_providers:
|
|
145
220
|
- provider: anthropic
|
|
146
221
|
model: claude-sonnet-4-6
|
|
147
|
-
api_key: sk-ant-...
|
|
222
|
+
api_key: sk-ant-... # never committed to git
|
|
148
223
|
is_default: true
|
|
149
224
|
|
|
225
|
+
workspace:
|
|
226
|
+
path: /Users/you/0agent-workspace # agent creates files here
|
|
227
|
+
|
|
150
228
|
sandbox:
|
|
151
229
|
backend: docker # docker | podman | process | firecracker
|
|
152
230
|
|
|
153
|
-
|
|
231
|
+
github_memory:
|
|
154
232
|
enabled: true
|
|
233
|
+
token: ghp_...
|
|
234
|
+
owner: your-username
|
|
235
|
+
repo: 0agent-memory
|
|
236
|
+
|
|
237
|
+
embedding:
|
|
238
|
+
provider: nomic-ollama # nomic-ollama | openai | none
|
|
239
|
+
model: nomic-embed-text
|
|
240
|
+
dimensions: 768
|
|
241
|
+
```
|
|
155
242
|
|
|
156
|
-
|
|
157
|
-
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Local development
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
git clone https://github.com/cadetmaze/0agentv1
|
|
249
|
+
cd 0agentv1
|
|
250
|
+
pnpm install
|
|
251
|
+
pnpm build
|
|
252
|
+
|
|
253
|
+
# Run init wizard
|
|
254
|
+
node bin/0agent.js init
|
|
255
|
+
|
|
256
|
+
# Or start daemon directly
|
|
257
|
+
node bin/0agent.js start
|
|
258
|
+
node bin/chat.js
|
|
259
|
+
|
|
260
|
+
# Bundle daemon into single file
|
|
261
|
+
node scripts/bundle.mjs
|
|
262
|
+
|
|
263
|
+
# Check status
|
|
264
|
+
node bin/0agent.js status
|
|
265
|
+
open http://localhost:4200 # 3D knowledge graph dashboard
|
|
158
266
|
```
|
|
159
267
|
|
|
268
|
+
**Requirements:**
|
|
269
|
+
- Node.js ≥ 20
|
|
270
|
+
- pnpm (`npm install -g pnpm`)
|
|
271
|
+
- API key for Anthropic, OpenAI, xAI, Gemini, or a local Ollama instance
|
|
272
|
+
- Docker (optional — enables sandboxed execution)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Roadmap
|
|
277
|
+
|
|
278
|
+
- [ ] Telegram bot interface
|
|
279
|
+
- [ ] MCP server support (connect to external tools)
|
|
280
|
+
- [ ] Team collaboration (shared graph, sync via GitHub)
|
|
281
|
+
- [ ] Mobile companion app
|
|
282
|
+
- [ ] Plugin SDK for custom capabilities
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Contributing
|
|
287
|
+
|
|
288
|
+
Issues and PRs welcome. This is early-stage software — things break, APIs change.
|
|
289
|
+
|
|
290
|
+
1. Fork the repo
|
|
291
|
+
2. `pnpm install && pnpm build`
|
|
292
|
+
3. Make changes to `packages/daemon/src/` or `bin/`
|
|
293
|
+
4. `node scripts/bundle.mjs` to rebuild the bundle
|
|
294
|
+
5. Test with `node bin/0agent.js init`
|
|
295
|
+
6. Submit a PR
|
|
296
|
+
|
|
160
297
|
---
|
|
161
298
|
|
|
162
299
|
## License
|
|
163
300
|
|
|
164
|
-
Apache 2.0
|
|
301
|
+
[Apache 2.0](LICENSE) — use it, fork it, build on it.
|
package/bin/0agent.js
CHANGED
|
@@ -369,11 +369,20 @@ ${ghToken && ghOwner ? `\ngithub_memory:\n enabled: true\n token: "${ghToken}"
|
|
|
369
369
|
console.log(' ✓ Built-in skills installed');
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
//
|
|
373
|
-
|
|
372
|
+
// Force-kill any running daemon so new config is loaded fresh
|
|
373
|
+
await forceStopDaemon();
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
// Start fresh daemon with new config
|
|
376
|
+
console.log('\n Starting...');
|
|
376
377
|
await startDaemon();
|
|
378
|
+
|
|
379
|
+
// Open chat TUI immediately — no manual steps needed
|
|
380
|
+
const chatSc = resolve(dirname(new URL(import.meta.url).pathname), 'chat.js');
|
|
381
|
+
if (existsSync(chatSc)) {
|
|
382
|
+
const { spawn: sp } = await import('node:child_process');
|
|
383
|
+
const p = sp(process.execPath, [chatSc], { stdio: 'inherit' });
|
|
384
|
+
await new Promise(r => p.on('close', r));
|
|
385
|
+
}
|
|
377
386
|
}
|
|
378
387
|
|
|
379
388
|
function detectSandboxes() {
|
|
@@ -391,9 +400,17 @@ function detectSandboxes() {
|
|
|
391
400
|
// ─── Daemon lifecycle ─────────────────────────────────────────────────────
|
|
392
401
|
|
|
393
402
|
async function startDaemon() {
|
|
403
|
+
// If still running (e.g. stop was called just before), wait a bit
|
|
394
404
|
if (await isDaemonRunning()) {
|
|
395
|
-
|
|
396
|
-
|
|
405
|
+
// Give it up to 3s to die before giving up
|
|
406
|
+
for (let i = 0; i < 6; i++) {
|
|
407
|
+
await sleep(500);
|
|
408
|
+
if (!await isDaemonRunning()) break;
|
|
409
|
+
}
|
|
410
|
+
if (await isDaemonRunning()) {
|
|
411
|
+
console.log(' Daemon already running on port 4200. Run `0agent status`.');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
397
414
|
}
|
|
398
415
|
|
|
399
416
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -423,33 +440,42 @@ async function startDaemon() {
|
|
|
423
440
|
child.unref();
|
|
424
441
|
|
|
425
442
|
// Wait for daemon to be ready (poll /api/health)
|
|
426
|
-
process.stdout.write(' Starting');
|
|
427
443
|
for (let i = 0; i < 30; i++) {
|
|
428
444
|
await sleep(500);
|
|
429
|
-
process.stdout.write('.');
|
|
430
445
|
if (await isDaemonRunning()) {
|
|
431
|
-
|
|
432
|
-
console.log(` Daemon running on http://localhost:4200`);
|
|
433
|
-
console.log(` Dashboard: http://localhost:4200`);
|
|
434
|
-
console.log(` Run: 0agent run "your task"\n`);
|
|
446
|
+
process.stdout.write(` \x1b[32m✓\x1b[0m Daemon ready\n`);
|
|
435
447
|
return;
|
|
436
448
|
}
|
|
437
449
|
}
|
|
438
|
-
console.log('
|
|
450
|
+
console.log(' Daemon did not start in time. Check: 0agent logs');
|
|
439
451
|
}
|
|
440
452
|
|
|
441
453
|
function stopDaemon() {
|
|
442
|
-
|
|
454
|
+
// Try PID file first
|
|
455
|
+
if (existsSync(PID_PATH)) {
|
|
456
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf8').trim(), 10);
|
|
457
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
458
|
+
} else {
|
|
443
459
|
console.log(' No daemon PID file found. Is it running?');
|
|
444
|
-
return;
|
|
445
460
|
}
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
461
|
+
// Also kill by process name as a fallback
|
|
462
|
+
try { execSync('pkill -f "daemon.mjs" 2>/dev/null; true', { stdio: 'ignore' }); } catch {}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Forcefully kill any running daemon regardless of PID file state.
|
|
466
|
+
// Used after init to ensure fresh config is picked up.
|
|
467
|
+
async function forceStopDaemon() {
|
|
468
|
+
// Kill by PID file if present
|
|
469
|
+
if (existsSync(PID_PATH)) {
|
|
470
|
+
const pid = parseInt(readFileSync(PID_PATH, 'utf8').trim(), 10);
|
|
471
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
452
472
|
}
|
|
473
|
+
// Kill by process name (catches daemons started by chat.js or other means)
|
|
474
|
+
try { execSync('pkill -f "daemon.mjs" 2>/dev/null; true', { stdio: 'ignore' }); } catch {}
|
|
475
|
+
// Kill by port 4200 (last resort)
|
|
476
|
+
try { execSync('lsof -ti:4200 | xargs kill -9 2>/dev/null; true', { stdio: 'ignore' }); } catch {}
|
|
477
|
+
// Wait for port to be free
|
|
478
|
+
await sleep(1200);
|
|
453
479
|
}
|
|
454
480
|
|
|
455
481
|
async function showStatus() {
|
package/bin/chat.js
CHANGED
|
@@ -7,12 +7,41 @@
|
|
|
7
7
|
* /model to switch. /key to add provider keys. Never forgets previous keys.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createInterface } from 'node:readline';
|
|
10
|
+
import { createInterface, emitKeypressEvents, moveCursor, clearLine } from 'node:readline';
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import YAML from 'yaml';
|
|
15
15
|
|
|
16
|
+
// ─── Slash command registry (used for live menu + tab completion) ─────────────
|
|
17
|
+
const SLASH_COMMANDS = [
|
|
18
|
+
// Skills
|
|
19
|
+
{ cmd: '/review', desc: 'Code review — bugs, style, security issues' },
|
|
20
|
+
{ cmd: '/build', desc: 'Build project and fix compilation errors' },
|
|
21
|
+
{ cmd: '/qa', desc: 'Generate and run tests' },
|
|
22
|
+
{ cmd: '/debug', desc: 'Debug a failing test or runtime error' },
|
|
23
|
+
{ cmd: '/refactor', desc: 'Refactor a file or module' },
|
|
24
|
+
{ cmd: '/test-writer', desc: 'Write unit tests for your code' },
|
|
25
|
+
{ cmd: '/doc', desc: 'Generate documentation' },
|
|
26
|
+
{ cmd: '/research', desc: 'Research a topic, person, or company' },
|
|
27
|
+
{ cmd: '/retro', desc: 'Weekly retrospective — what went well / badly' },
|
|
28
|
+
{ cmd: '/ship', desc: 'Pre-release checklist — ready to deploy?' },
|
|
29
|
+
{ cmd: '/office-hours', desc: 'Plan a new feature or project from scratch' },
|
|
30
|
+
{ cmd: '/plan-eng-review',desc:'Engineering planning review' },
|
|
31
|
+
{ cmd: '/security-audit',desc: 'Security audit — find vulnerabilities' },
|
|
32
|
+
{ cmd: '/design-review', desc: 'Design review — architecture and patterns' },
|
|
33
|
+
// Built-ins
|
|
34
|
+
{ cmd: '/model', desc: 'Show or switch the LLM model' },
|
|
35
|
+
{ cmd: '/key', desc: 'Update a stored API key' },
|
|
36
|
+
{ cmd: '/status', desc: 'Daemon health, graph stats, active sessions' },
|
|
37
|
+
{ cmd: '/skills', desc: 'List all available skills' },
|
|
38
|
+
{ cmd: '/schedule', desc: 'Manage scheduled / recurring tasks' },
|
|
39
|
+
{ cmd: '/update', desc: 'Update 0agent to the latest version' },
|
|
40
|
+
{ cmd: '/graph', desc: 'Open 3D knowledge graph in browser' },
|
|
41
|
+
{ cmd: '/clear', desc: 'Clear the screen' },
|
|
42
|
+
{ cmd: '/help', desc: 'Show this help' },
|
|
43
|
+
];
|
|
44
|
+
|
|
16
45
|
const AGENT_DIR = resolve(homedir(), '.0agent');
|
|
17
46
|
const CONFIG_PATH = resolve(AGENT_DIR, 'config.yaml');
|
|
18
47
|
const BASE_URL = process.env['ZEROAGENT_URL'] ?? 'http://localhost:4200';
|
|
@@ -183,7 +212,7 @@ function printHeader() {
|
|
|
183
212
|
const modelStr = provider ? `${provider.provider}/${provider.model}` : 'no model';
|
|
184
213
|
console.log();
|
|
185
214
|
console.log(fmt(C.bold, ' 0agent') + fmt(C.dim, ` — ${modelStr}`));
|
|
186
|
-
console.log(fmt(C.dim, ' Type a task
|
|
215
|
+
console.log(fmt(C.dim, ' Type a task or /command — press Tab to browse, / to see all.\n'));
|
|
187
216
|
}
|
|
188
217
|
|
|
189
218
|
function printInsights() {
|
|
@@ -328,6 +357,14 @@ function handleWsEvent(event) {
|
|
|
328
357
|
if (r.files_written?.length) console.log(`\n ${fmt(C.green, '✓')} ${r.files_written.join(', ')}`);
|
|
329
358
|
if (r.tokens_used) process.stdout.write(fmt(C.dim, `\n ${r.tokens_used} tokens · ${r.model ?? ''}\n`));
|
|
330
359
|
|
|
360
|
+
// Contextual next-step suggestions
|
|
361
|
+
const suggestions = _suggestNext(lineBuffer, r);
|
|
362
|
+
if (suggestions.length > 0 && messageQueue.length === 0) {
|
|
363
|
+
process.stdout.write(
|
|
364
|
+
` ${fmt(C.dim, '→')} ${suggestions.map(s => fmt(C.cyan, s)).join(fmt(C.dim, ' · '))}\n`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
331
368
|
// Confirm server if port mentioned
|
|
332
369
|
confirmServer(r, lineBuffer);
|
|
333
370
|
lineBuffer = '';
|
|
@@ -381,6 +418,26 @@ function handleWsEvent(event) {
|
|
|
381
418
|
}
|
|
382
419
|
}
|
|
383
420
|
|
|
421
|
+
function _suggestNext(output, result) {
|
|
422
|
+
const text = (output + ' ' + (result?.output ?? '') + ' ' + JSON.stringify(result?.files_written ?? [])).toLowerCase();
|
|
423
|
+
if (text.includes('written') || text.includes('creat') || text.includes('built') || (result?.files_written?.length > 0)) {
|
|
424
|
+
return ['/review', '/qa'];
|
|
425
|
+
}
|
|
426
|
+
if (text.includes('test') || text.includes('spec') || text.includes('pass')) {
|
|
427
|
+
return ['/ship'];
|
|
428
|
+
}
|
|
429
|
+
if (text.includes('research') || text.includes('found') || text.includes('results')) {
|
|
430
|
+
return ['/office-hours', '/doc'];
|
|
431
|
+
}
|
|
432
|
+
if (text.includes('bug') || text.includes('fix') || text.includes('error') || text.includes('debug')) {
|
|
433
|
+
return ['/qa', '/review'];
|
|
434
|
+
}
|
|
435
|
+
if (text.includes('deploy') || text.includes('server') || text.includes('running')) {
|
|
436
|
+
return ['/qa', '/ship'];
|
|
437
|
+
}
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
|
|
384
441
|
async function confirmServer(result, output) {
|
|
385
442
|
const allText = [...(result.commands_run ?? []), output].join(' ');
|
|
386
443
|
const portMatch = allText.match(/(?:localhost:|port\s*[=:]?\s*)(\d{4,5})/i);
|
|
@@ -751,23 +808,116 @@ async function handleCommand(input) {
|
|
|
751
808
|
}
|
|
752
809
|
}
|
|
753
810
|
|
|
811
|
+
// ─── Live slash-command menu ──────────────────────────────────────────────────
|
|
812
|
+
// Drawn below the prompt as the user types. Uses moveCursor to avoid cursor
|
|
813
|
+
// save/restore conflicts with readline.
|
|
814
|
+
let _menuLines = 0; // how many lines the current menu occupies below the cursor
|
|
815
|
+
|
|
816
|
+
function _drawMenu(filter) {
|
|
817
|
+
if (pendingResolve) { _clearMenu(); return; } // don't show while session running
|
|
818
|
+
|
|
819
|
+
const items = filter === null ? [] :
|
|
820
|
+
SLASH_COMMANDS.filter(c =>
|
|
821
|
+
!filter || c.cmd.slice(1).toLowerCase().startsWith(filter.toLowerCase())
|
|
822
|
+
).slice(0, 10);
|
|
823
|
+
|
|
824
|
+
// If nothing changed (same count), skip redraw to avoid flicker
|
|
825
|
+
if (items.length === 0) { _clearMenu(); return; }
|
|
826
|
+
|
|
827
|
+
const needed = items.length + 1; // +1 for blank line
|
|
828
|
+
|
|
829
|
+
// Move down past existing menu lines (or 0), then clear downward
|
|
830
|
+
const existingLines = _menuLines;
|
|
831
|
+
if (existingLines > 0) {
|
|
832
|
+
moveCursor(process.stdout, 0, existingLines);
|
|
833
|
+
for (let i = 0; i < existingLines; i++) {
|
|
834
|
+
clearLine(process.stdout, 0);
|
|
835
|
+
if (i < existingLines - 1) moveCursor(process.stdout, 0, -1);
|
|
836
|
+
}
|
|
837
|
+
moveCursor(process.stdout, 0, -(existingLines - 1));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Print blank separator + menu items, tracking column 0
|
|
841
|
+
process.stdout.write('\n');
|
|
842
|
+
for (const m of items) {
|
|
843
|
+
process.stdout.write(
|
|
844
|
+
` ${fmt(C.cyan, m.cmd.padEnd(20))} ${fmt(C.dim, m.desc)}\x1b[K\n`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Move back up to the prompt line and restore cursor after the typed text
|
|
849
|
+
moveCursor(process.stdout, 0, -(needed));
|
|
850
|
+
// Jump to end of current line (readline already put cursor there)
|
|
851
|
+
moveCursor(process.stdout, 999, 0);
|
|
852
|
+
|
|
853
|
+
_menuLines = needed;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function _clearMenu() {
|
|
857
|
+
if (_menuLines === 0) return;
|
|
858
|
+
const n = _menuLines;
|
|
859
|
+
_menuLines = 0;
|
|
860
|
+
moveCursor(process.stdout, 0, n);
|
|
861
|
+
for (let i = 0; i < n; i++) {
|
|
862
|
+
clearLine(process.stdout, 0);
|
|
863
|
+
moveCursor(process.stdout, 0, -1);
|
|
864
|
+
}
|
|
865
|
+
moveCursor(process.stdout, 0, 1); // back to prompt line
|
|
866
|
+
}
|
|
867
|
+
|
|
754
868
|
// ─── Main REPL ────────────────────────────────────────────────────────────────
|
|
755
869
|
const rl = createInterface({
|
|
756
870
|
input: process.stdin,
|
|
757
871
|
output: process.stdout,
|
|
758
872
|
prompt: `\n ${fmt(C.cyan, '›')} `,
|
|
759
873
|
historySize: 100,
|
|
760
|
-
completer: (line) => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
874
|
+
completer: (line, callback) => {
|
|
875
|
+
if (!line.startsWith('/')) return callback(null, [[], line]);
|
|
876
|
+
|
|
877
|
+
const filter = line.slice(1).toLowerCase();
|
|
878
|
+
const matches = SLASH_COMMANDS.filter(c =>
|
|
879
|
+
!filter || c.cmd.slice(1).startsWith(filter)
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
if (matches.length === 0) return callback(null, [[], line]);
|
|
883
|
+
|
|
884
|
+
// Single match — let readline silently auto-complete
|
|
885
|
+
if (matches.length === 1) {
|
|
886
|
+
_clearMenu();
|
|
887
|
+
return callback(null, [[matches[0].cmd + ' '], line]);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Multiple matches — print formatted menu, suppress readline's plain list
|
|
891
|
+
_clearMenu();
|
|
892
|
+
process.stdout.write('\n\n');
|
|
893
|
+
for (const m of matches.slice(0, 12)) {
|
|
894
|
+
process.stdout.write(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}\n`);
|
|
895
|
+
}
|
|
896
|
+
if (matches.length > 12) {
|
|
897
|
+
process.stdout.write(` ${fmt(C.dim, `…and ${matches.length - 12} more`)}\n`);
|
|
898
|
+
}
|
|
899
|
+
process.stdout.write('\n');
|
|
900
|
+
setImmediate(() => rl.prompt(true));
|
|
901
|
+
return callback(null, [[], line]);
|
|
766
902
|
},
|
|
767
903
|
});
|
|
768
904
|
|
|
769
|
-
//
|
|
770
|
-
|
|
905
|
+
// Live menu on keypress — draws below the prompt as user types
|
|
906
|
+
emitKeypressEvents(process.stdin, rl);
|
|
907
|
+
process.stdin.on('keypress', (_char, key) => {
|
|
908
|
+
if (key?.name === 'return' || key?.name === 'enter') {
|
|
909
|
+
_clearMenu(); // clear before readline processes the line
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
setImmediate(() => {
|
|
913
|
+
const line = rl.line ?? '';
|
|
914
|
+
if (line.startsWith('/') && !pendingResolve) {
|
|
915
|
+
_drawMenu(line.slice(1));
|
|
916
|
+
} else {
|
|
917
|
+
_clearMenu();
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
});
|
|
771
921
|
|
|
772
922
|
printHeader();
|
|
773
923
|
printInsights();
|
|
@@ -1075,9 +1225,21 @@ async function drainQueue() {
|
|
|
1075
1225
|
}
|
|
1076
1226
|
|
|
1077
1227
|
rl.on('line', async (input) => {
|
|
1228
|
+
_clearMenu(); // always clear menu when a line is submitted
|
|
1078
1229
|
const line = input.trim();
|
|
1079
1230
|
if (!line) { rl.prompt(); return; }
|
|
1080
1231
|
|
|
1232
|
+
// Bare `/` → show full command palette
|
|
1233
|
+
if (line === '/') {
|
|
1234
|
+
console.log('');
|
|
1235
|
+
for (const m of SLASH_COMMANDS) {
|
|
1236
|
+
console.log(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}`);
|
|
1237
|
+
}
|
|
1238
|
+
console.log('');
|
|
1239
|
+
rl.prompt();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1081
1243
|
// If a session is already running, queue the message.
|
|
1082
1244
|
// pauseFor() stops the spinner briefly so the user can see the confirmation,
|
|
1083
1245
|
// then resumes — prevents spinner from overwriting their typed text.
|