0agent 1.0.16 → 1.0.18
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 +534 -81
- package/dist/daemon.mjs +492 -36
- package/package.json +1 -1
package/bin/0agent.js
CHANGED
|
@@ -94,26 +94,67 @@ switch (cmd) {
|
|
|
94
94
|
await runServe(args.slice(1));
|
|
95
95
|
break;
|
|
96
96
|
|
|
97
|
+
case 'watch':
|
|
98
|
+
await runWatch();
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'memory':
|
|
102
|
+
await runMemoryCommand(args.slice(1));
|
|
103
|
+
break;
|
|
104
|
+
|
|
97
105
|
default:
|
|
98
106
|
showHelp();
|
|
99
107
|
break;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
// ─── Init wizard
|
|
110
|
+
// ─── Init wizard — arrow key selection, GitHub memory built in ───────────────
|
|
111
|
+
|
|
112
|
+
// Arrow-key select using enquirer (falls back to number input if not available)
|
|
113
|
+
async function arrowSelect(message, choices, initial = 0) {
|
|
114
|
+
try {
|
|
115
|
+
const { Select } = await import('enquirer');
|
|
116
|
+
const prompt = new Select({ message, choices: choices.map((c, i) => ({ name: c, value: i })), initial });
|
|
117
|
+
const answer = await prompt.run();
|
|
118
|
+
return choices.indexOf(answer);
|
|
119
|
+
} catch {
|
|
120
|
+
// Fallback: number-based selection (no enquirer)
|
|
121
|
+
return choose(message, choices, initial);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function arrowInput(message, initial = '') {
|
|
126
|
+
try {
|
|
127
|
+
const { Input } = await import('enquirer');
|
|
128
|
+
const prompt = new Input({ message, initial });
|
|
129
|
+
return await prompt.run();
|
|
130
|
+
} catch {
|
|
131
|
+
return ask(` ${message}: `);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function arrowPassword(message) {
|
|
136
|
+
try {
|
|
137
|
+
const { Password } = await import('enquirer');
|
|
138
|
+
const prompt = new Password({ message });
|
|
139
|
+
return await prompt.run();
|
|
140
|
+
} catch {
|
|
141
|
+
return ask(` ${message}: `);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
103
144
|
|
|
104
145
|
async function runInit() {
|
|
105
|
-
console.log('\n
|
|
106
|
-
console.log(' │
|
|
107
|
-
console.log(' │ 0agent — An agent that learns.
|
|
108
|
-
console.log(' │
|
|
109
|
-
console.log(' │ v1.0
|
|
110
|
-
console.log(' └─────────────────────────────────────────┘\n');
|
|
111
|
-
|
|
112
|
-
// Check if already initialised
|
|
146
|
+
console.log('\n \x1b[1m┌─────────────────────────────────────────┐\x1b[0m');
|
|
147
|
+
console.log(' \x1b[1m│ │\x1b[0m');
|
|
148
|
+
console.log(' \x1b[1m│ 0agent — An agent that learns. │\x1b[0m');
|
|
149
|
+
console.log(' \x1b[1m│ │\x1b[0m');
|
|
150
|
+
console.log(' \x1b[1m│ v1.0 · Apache 2.0 │\x1b[0m');
|
|
151
|
+
console.log(' \x1b[1m└─────────────────────────────────────────┘\x1b[0m\n');
|
|
152
|
+
|
|
113
153
|
if (existsSync(CONFIG_PATH)) {
|
|
114
|
-
const answer = await ask('Config already exists. Reinitialise? [y/N] ');
|
|
154
|
+
const answer = await ask(' Config already exists. Reinitialise? [y/N] ');
|
|
115
155
|
if (answer.toLowerCase() !== 'y') {
|
|
116
|
-
console.log('\n
|
|
156
|
+
console.log('\n Running: 0agent start\n');
|
|
157
|
+
await startDaemon();
|
|
117
158
|
return;
|
|
118
159
|
}
|
|
119
160
|
}
|
|
@@ -123,96 +164,110 @@ async function runInit() {
|
|
|
123
164
|
mkdirSync(resolve(AGENT_DIR, 'skills', 'builtin'), { recursive: true });
|
|
124
165
|
mkdirSync(resolve(AGENT_DIR, 'skills', 'custom'), { recursive: true });
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
const
|
|
167
|
+
// ── Step 1: LLM Provider ────────────────────────────────────────────────
|
|
168
|
+
const providerIdx = await arrowSelect('LLM Provider', [
|
|
128
169
|
'Anthropic (Claude) ← recommended',
|
|
129
170
|
'OpenAI (GPT-4o)',
|
|
130
171
|
'xAI (Grok)',
|
|
131
172
|
'Google (Gemini)',
|
|
132
|
-
'Ollama (local
|
|
173
|
+
'Ollama (local — no API key)',
|
|
133
174
|
], 0);
|
|
134
|
-
const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][
|
|
175
|
+
const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][providerIdx];
|
|
135
176
|
|
|
136
|
-
// Model selection per provider
|
|
137
177
|
const MODELS = {
|
|
138
|
-
anthropic: [
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
],
|
|
143
|
-
openai: [
|
|
144
|
-
'gpt-4o ← recommended',
|
|
145
|
-
'gpt-4o-mini (faster, cheaper)',
|
|
146
|
-
'o3-mini (reasoning)',
|
|
147
|
-
],
|
|
148
|
-
xai: [
|
|
149
|
-
'grok-3 ← recommended',
|
|
150
|
-
'grok-3-mini',
|
|
151
|
-
],
|
|
152
|
-
gemini: [
|
|
153
|
-
'gemini-2.0-flash ← recommended',
|
|
154
|
-
'gemini-2.0-pro',
|
|
155
|
-
],
|
|
156
|
-
ollama: [
|
|
157
|
-
'llama3.1 ← recommended',
|
|
158
|
-
'mistral',
|
|
159
|
-
'codellama',
|
|
160
|
-
],
|
|
178
|
+
anthropic: ['claude-sonnet-4-6 ← recommended', 'claude-opus-4-6 (most capable)', 'claude-haiku-4-5 (fastest)'],
|
|
179
|
+
openai: ['gpt-4o ← recommended', 'gpt-4o-mini', 'o3-mini'],
|
|
180
|
+
xai: ['grok-3 ← recommended', 'grok-3-mini'],
|
|
181
|
+
gemini: ['gemini-2.0-flash ← recommended', 'gemini-2.0-pro'],
|
|
182
|
+
ollama: ['llama3.1 ← recommended', 'mistral', 'codellama'],
|
|
161
183
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (MODELS[providerKey]) {
|
|
165
|
-
console.log();
|
|
166
|
-
const modelIdx = await choose(' Which model?', MODELS[providerKey], 0);
|
|
167
|
-
model = MODELS[providerKey][modelIdx].split(/\s+/)[0];
|
|
168
|
-
}
|
|
184
|
+
const modelIdx = await arrowSelect('Which model?', MODELS[providerKey], 0);
|
|
185
|
+
const model = MODELS[providerKey][modelIdx].split(/\s+/)[0];
|
|
169
186
|
|
|
170
187
|
let apiKey = '';
|
|
171
188
|
if (providerKey !== 'ollama') {
|
|
172
|
-
apiKey = await
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
apiKey = await arrowPassword(`${providerKey} API key`);
|
|
190
|
+
apiKey = apiKey.trim();
|
|
191
|
+
if (!apiKey) {
|
|
192
|
+
console.log(' \x1b[33m⚠\x1b[0m No key — add it later in ~/.0agent/config.yaml');
|
|
175
193
|
} else {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (expectedPrefix && !apiKey.startsWith(expectedPrefix)) {
|
|
180
|
-
console.log(` ⚠️ Key doesn't look like a ${providerKey} key (expected: ${expectedPrefix}...)`);
|
|
194
|
+
const pfx = { anthropic: 'sk-ant-', openai: 'sk-', xai: 'xai-', gemini: 'AI' }[providerKey];
|
|
195
|
+
if (pfx && !apiKey.startsWith(pfx)) {
|
|
196
|
+
console.log(` \x1b[33m⚠\x1b[0m Unexpected key format (expected ${pfx}...)`);
|
|
181
197
|
} else {
|
|
182
|
-
console.log('
|
|
198
|
+
console.log(' \x1b[32m✓\x1b[0m Key format valid');
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
}
|
|
186
202
|
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'Skip (text-only mode)',
|
|
203
|
+
// ── Step 2: GitHub Memory ───────────────────────────────────────────────
|
|
204
|
+
const memChoice = await arrowSelect('Back up memory to GitHub?', [
|
|
205
|
+
'Yes — private repo, free, cross-device sync ← recommended',
|
|
206
|
+
'No — local only',
|
|
192
207
|
], 0);
|
|
193
|
-
const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embedding];
|
|
194
208
|
|
|
195
|
-
|
|
209
|
+
let ghToken = '', ghOwner = '', ghRepo = '0agent-memory';
|
|
210
|
+
if (memChoice === 0) {
|
|
211
|
+
// Try gh CLI first
|
|
212
|
+
try {
|
|
213
|
+
const { execSync: ex } = await import('node:child_process');
|
|
214
|
+
ghToken = ex('gh auth token 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
215
|
+
ghOwner = ex('gh api user --jq .login 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
216
|
+
if (ghToken && ghOwner) {
|
|
217
|
+
console.log(` \x1b[32m✓\x1b[0m gh CLI — authenticated as \x1b[1m${ghOwner}\x1b[0m`);
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
|
|
221
|
+
if (!ghToken) {
|
|
222
|
+
console.log('\n Create a GitHub token: \x1b[4mhttps://github.com/settings/tokens/new\x1b[0m');
|
|
223
|
+
console.log(' Required scope: \x1b[1mrepo\x1b[0m\n');
|
|
224
|
+
ghToken = await arrowPassword('GitHub token (ghp_...)');
|
|
225
|
+
ghToken = ghToken.trim();
|
|
226
|
+
if (ghToken) {
|
|
227
|
+
ghOwner = await verifyGitHubToken(ghToken) ?? '';
|
|
228
|
+
if (!ghOwner) {
|
|
229
|
+
console.log(' \x1b[31m✗\x1b[0m Invalid token — skipping GitHub memory');
|
|
230
|
+
ghToken = '';
|
|
231
|
+
} else {
|
|
232
|
+
console.log(` \x1b[32m✓\x1b[0m Authenticated as \x1b[1m${ghOwner}\x1b[0m`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (ghToken && ghOwner) {
|
|
238
|
+
process.stdout.write(` Creating private repo \x1b[1m${ghOwner}/0agent-memory\x1b[0m...`);
|
|
239
|
+
const ok = await createGitHubRepo(ghToken, '0agent-memory');
|
|
240
|
+
console.log(ok ? ' \x1b[32m✓\x1b[0m' : ' \x1b[33m(exists)\x1b[0m');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Step 3: Embedding ────────────────────────────────────────────────────
|
|
245
|
+
const embIdx = await arrowSelect('Embeddings (for semantic memory search)?', [
|
|
246
|
+
'Local via Ollama (nomic-embed-text) — free, private',
|
|
247
|
+
'OpenAI text-embedding-3-small — cloud',
|
|
248
|
+
'Skip — text-only mode',
|
|
249
|
+
], 0);
|
|
250
|
+
const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embIdx];
|
|
251
|
+
|
|
252
|
+
// ── Step 4: Sandbox ──────────────────────────────────────────────────────
|
|
196
253
|
const sandboxes = detectSandboxes();
|
|
197
|
-
console.log(` Detected: ${sandboxes.join(', ') || 'process (fallback)'}`);
|
|
198
254
|
const sandboxChoice = sandboxes[0] ?? 'process';
|
|
199
|
-
console.log(
|
|
255
|
+
console.log(`\n Sandbox: \x1b[32m${sandboxChoice}\x1b[0m detected`);
|
|
200
256
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
'software-engineering
|
|
204
|
-
'
|
|
257
|
+
// ── Step 5: Seed graph ───────────────────────────────────────────────────
|
|
258
|
+
const seedIdx = await arrowSelect('Starting knowledge?', [
|
|
259
|
+
'software-engineering — sprint workflow + 15 skills ← recommended',
|
|
260
|
+
'Start from scratch',
|
|
205
261
|
], 0);
|
|
206
|
-
const seedName =
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
console.log('\n
|
|
210
|
-
console.log(`
|
|
211
|
-
console.log(`
|
|
212
|
-
console.log(`
|
|
213
|
-
console.log(` Sandbox:
|
|
214
|
-
console.log(` Seed:
|
|
215
|
-
console.log();
|
|
262
|
+
const seedName = seedIdx === 0 ? 'software-engineering' : null;
|
|
263
|
+
|
|
264
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
265
|
+
console.log('\n \x1b[1mReady to launch\x1b[0m\n');
|
|
266
|
+
console.log(` LLM: \x1b[36m${providerKey}/${model}\x1b[0m`);
|
|
267
|
+
console.log(` API Key: ${apiKey ? '\x1b[32m✓ set\x1b[0m (' + apiKey.slice(0, 8) + '••••)' : '\x1b[33mnot set\x1b[0m'}`);
|
|
268
|
+
console.log(` Memory: ${ghToken ? `\x1b[32mgithub.com/${ghOwner}/0agent-memory\x1b[0m` : '\x1b[2mlocal only\x1b[0m'}`);
|
|
269
|
+
console.log(` Sandbox: \x1b[36m${sandboxChoice}\x1b[0m`);
|
|
270
|
+
console.log(` Seed: \x1b[36m${seedName ?? 'scratch'}\x1b[0m\n`);
|
|
216
271
|
|
|
217
272
|
// Write config
|
|
218
273
|
const dbPath = resolve(AGENT_DIR, 'graph.db');
|
|
@@ -248,6 +303,7 @@ graph:
|
|
|
248
303
|
hnsw_path: "${hnswPath}"
|
|
249
304
|
object_store_path: "${objPath}"
|
|
250
305
|
${seedName ? `\nseed: "${seedName}"` : ''}
|
|
306
|
+
${ghToken && ghOwner ? `\ngithub_memory:\n enabled: true\n token: "${ghToken}"\n owner: "${ghOwner}"\n repo: "${ghRepo}"` : ''}
|
|
251
307
|
`;
|
|
252
308
|
|
|
253
309
|
writeFileSync(CONFIG_PATH, config, 'utf8');
|
|
@@ -447,12 +503,12 @@ async function streamSession(sessionId) {
|
|
|
447
503
|
break;
|
|
448
504
|
case 'session.completed': {
|
|
449
505
|
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
450
|
-
// Show files written + commands run
|
|
451
506
|
const r = event.result ?? {};
|
|
452
507
|
if (r.files_written?.length) console.log(`\n \x1b[32m✓\x1b[0m Files: ${r.files_written.join(', ')}`);
|
|
453
508
|
if (r.commands_run?.length) console.log(` \x1b[32m✓\x1b[0m Commands run: ${r.commands_run.length}`);
|
|
454
509
|
if (r.tokens_used) console.log(` \x1b[2m${r.tokens_used} tokens · ${r.model}\x1b[0m`);
|
|
455
510
|
console.log('\n \x1b[32m✓ Done\x1b[0m\n');
|
|
511
|
+
await showResultPreview(r); // confirm server/file actually exists
|
|
456
512
|
ws.close();
|
|
457
513
|
resolve();
|
|
458
514
|
break;
|
|
@@ -496,6 +552,7 @@ async function pollSession(sessionId) {
|
|
|
496
552
|
console.log('\n ✓ Done\n');
|
|
497
553
|
const out = s.result?.output ?? s.result;
|
|
498
554
|
if (out && typeof out === 'string') console.log(` ${out}\n`);
|
|
555
|
+
await showResultPreview(s.result ?? {});
|
|
499
556
|
return;
|
|
500
557
|
}
|
|
501
558
|
if (s.status === 'failed') {
|
|
@@ -804,6 +861,362 @@ async function waitForTunnelUrl(proc, pattern, timeout) {
|
|
|
804
861
|
});
|
|
805
862
|
}
|
|
806
863
|
|
|
864
|
+
// ─── Memory sync (GitHub backend) ─────────────────────────────────────────
|
|
865
|
+
|
|
866
|
+
async function runMemoryCommand(memArgs) {
|
|
867
|
+
const sub = memArgs[0] ?? 'status';
|
|
868
|
+
|
|
869
|
+
switch (sub) {
|
|
870
|
+
// ── 0agent memory connect github ──────────────────────────────────────
|
|
871
|
+
case 'connect': {
|
|
872
|
+
const provider = memArgs[1] ?? 'github';
|
|
873
|
+
if (provider !== 'github') { console.log(' Only GitHub is supported: 0agent memory connect github'); break; }
|
|
874
|
+
|
|
875
|
+
console.log('\n \x1b[1m0agent Memory — GitHub Sync\x1b[0m\n');
|
|
876
|
+
console.log(' Your knowledge graph will be backed up to a private GitHub repository.');
|
|
877
|
+
console.log(' Free, versioned, cross-device. No server needed.\n');
|
|
878
|
+
|
|
879
|
+
// ── Authentication ──
|
|
880
|
+
let token = '';
|
|
881
|
+
|
|
882
|
+
// Try gh CLI first (already logged in for most devs)
|
|
883
|
+
try {
|
|
884
|
+
const { execSync: ex } = await import('node:child_process');
|
|
885
|
+
token = ex('gh auth token 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
886
|
+
if (token) {
|
|
887
|
+
const { execSync: ex2 } = await import('node:child_process');
|
|
888
|
+
const user = ex2('gh api user --jq .login 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
889
|
+
console.log(` \x1b[32m✓\x1b[0m Detected gh CLI — authenticated as \x1b[1m${user}\x1b[0m`);
|
|
890
|
+
}
|
|
891
|
+
} catch {}
|
|
892
|
+
|
|
893
|
+
// Fallback: GitHub Device Flow
|
|
894
|
+
if (!token) {
|
|
895
|
+
console.log(' \x1b[2mgh CLI not found — using GitHub token auth\x1b[0m\n');
|
|
896
|
+
console.log(' Create a token at: \x1b[4mhttps://github.com/settings/tokens/new\x1b[0m');
|
|
897
|
+
console.log(' Required scope: \x1b[1mrepo\x1b[0m\n');
|
|
898
|
+
token = await ask(' Paste token (starts with ghp_): ');
|
|
899
|
+
token = token.trim();
|
|
900
|
+
if (!token) { console.log(' No token provided.'); break; }
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Verify token
|
|
904
|
+
const owner = await verifyGitHubToken(token);
|
|
905
|
+
if (!owner) { console.log(' \x1b[31m✗\x1b[0m Invalid token or no access.'); break; }
|
|
906
|
+
console.log(` \x1b[32m✓\x1b[0m Authenticated as \x1b[1m${owner}\x1b[0m`);
|
|
907
|
+
|
|
908
|
+
// ── Create repo ──
|
|
909
|
+
const repoName = memArgs[2] ?? '0agent-memory';
|
|
910
|
+
process.stdout.write(` Creating private repo \x1b[1m${owner}/${repoName}\x1b[0m...`);
|
|
911
|
+
const created = await createGitHubRepo(token, repoName);
|
|
912
|
+
console.log(created ? ' \x1b[32m✓\x1b[0m' : ' \x1b[33m(already exists)\x1b[0m');
|
|
913
|
+
|
|
914
|
+
// ── Save to config ──
|
|
915
|
+
const YAML = await import('yaml');
|
|
916
|
+
const { readFileSync: rf, writeFileSync: wf, existsSync: ef } = await import('node:fs');
|
|
917
|
+
if (ef(CONFIG_PATH)) {
|
|
918
|
+
let cfg = rf(CONFIG_PATH, 'utf8');
|
|
919
|
+
// Remove old github_memory block if present
|
|
920
|
+
cfg = cfg.replace(/\ngithub_memory:[\s\S]*?(?=\n\w|\n$|$)/, '');
|
|
921
|
+
cfg += `\ngithub_memory:\n enabled: true\n token: "${token}"\n owner: "${owner}"\n repo: "${repoName}"\n`;
|
|
922
|
+
wf(CONFIG_PATH, cfg, 'utf8');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── Initial push ──
|
|
926
|
+
console.log('\n Performing initial sync...');
|
|
927
|
+
await requireDaemon();
|
|
928
|
+
const result = await daemonMemorySync('push');
|
|
929
|
+
if (result?.pushed) {
|
|
930
|
+
console.log(` \x1b[32m✓\x1b[0m Synced — ${result.nodes_synced} nodes, ${result.edges_synced} edges`);
|
|
931
|
+
console.log(`\n Memory repo: \x1b[4mhttps://github.com/${owner}/${repoName}\x1b[0m`);
|
|
932
|
+
console.log('\n From any machine, run:');
|
|
933
|
+
console.log(` \x1b[36m0agent memory connect github --repo ${owner}/${repoName}\x1b[0m\n`);
|
|
934
|
+
} else {
|
|
935
|
+
console.log(' \x1b[33m⚠\x1b[0m Initial sync skipped — run `0agent memory sync` after daemon starts.');
|
|
936
|
+
console.log(`\n Memory repo: \x1b[4mhttps://github.com/${owner}/${repoName}\x1b[0m\n`);
|
|
937
|
+
}
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ── 0agent memory sync ────────────────────────────────────────────────
|
|
942
|
+
case 'sync': {
|
|
943
|
+
await requireDaemon();
|
|
944
|
+
process.stdout.write(' Syncing memory to GitHub...');
|
|
945
|
+
const result = await daemonMemorySync('push');
|
|
946
|
+
if (result?.pushed) {
|
|
947
|
+
console.log(` \x1b[32m✓\x1b[0m ${result.nodes_synced} nodes, ${result.edges_synced} edges`);
|
|
948
|
+
} else {
|
|
949
|
+
console.log(` \x1b[31m✗\x1b[0m ${result?.error ?? 'No GitHub memory configured'}`);
|
|
950
|
+
if (!result?.error) console.log(' Run: 0agent memory connect github');
|
|
951
|
+
}
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ── 0agent memory pull ───────────────────────────────────────────────
|
|
956
|
+
case 'pull': {
|
|
957
|
+
await requireDaemon();
|
|
958
|
+
process.stdout.write(' Pulling memory from GitHub...');
|
|
959
|
+
const result = await daemonMemorySync('pull');
|
|
960
|
+
if (result?.pulled) {
|
|
961
|
+
console.log(` \x1b[32m✓\x1b[0m +${result.nodes_synced} nodes, +${result.edges_synced} edges merged`);
|
|
962
|
+
} else {
|
|
963
|
+
console.log(` \x1b[31m✗\x1b[0m ${result?.error ?? 'No GitHub memory configured'}`);
|
|
964
|
+
}
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ── 0agent memory status ──────────────────────────────────────────────
|
|
969
|
+
case 'status': {
|
|
970
|
+
const YAML = await import('yaml');
|
|
971
|
+
const { readFileSync: rf, existsSync: ef } = await import('node:fs');
|
|
972
|
+
if (!ef(CONFIG_PATH)) { console.log('\n Not initialised. Run: 0agent init\n'); break; }
|
|
973
|
+
|
|
974
|
+
const cfg = YAML.parse(rf(CONFIG_PATH, 'utf8'));
|
|
975
|
+
const ghMem = cfg.github_memory;
|
|
976
|
+
|
|
977
|
+
if (!ghMem?.enabled) {
|
|
978
|
+
console.log('\n Memory sync: \x1b[33mnot connected\x1b[0m');
|
|
979
|
+
console.log(' Run: 0agent memory connect github\n');
|
|
980
|
+
} else {
|
|
981
|
+
console.log(`\n Memory sync: \x1b[32m✓ connected\x1b[0m`);
|
|
982
|
+
console.log(` Repo: https://github.com/${ghMem.owner}/${ghMem.repo}`);
|
|
983
|
+
// Get last sync from daemon
|
|
984
|
+
try {
|
|
985
|
+
const res = await fetch(`${BASE_URL}/api/memory/status`).catch(() => null);
|
|
986
|
+
const data = res?.ok ? await res.json() : null;
|
|
987
|
+
if (data) {
|
|
988
|
+
console.log(` Last push: ${data.pushed_at ? new Date(data.pushed_at).toLocaleString() : 'never'}`);
|
|
989
|
+
console.log(` Last pull: ${data.pulled_at ? new Date(data.pulled_at).toLocaleString() : 'never'}`);
|
|
990
|
+
}
|
|
991
|
+
} catch {}
|
|
992
|
+
console.log();
|
|
993
|
+
}
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ── 0agent memory disconnect ──────────────────────────────────────────
|
|
998
|
+
case 'disconnect': {
|
|
999
|
+
const { readFileSync: rf, writeFileSync: wf, existsSync: ef } = await import('node:fs');
|
|
1000
|
+
if (ef(CONFIG_PATH)) {
|
|
1001
|
+
let cfg = rf(CONFIG_PATH, 'utf8');
|
|
1002
|
+
cfg = cfg.replace(/\ngithub_memory:[\s\S]*?(?=\n\w|\n$|$)/, '');
|
|
1003
|
+
wf(CONFIG_PATH, cfg, 'utf8');
|
|
1004
|
+
}
|
|
1005
|
+
console.log(' \x1b[32m✓\x1b[0m GitHub memory sync disabled. Local graph unchanged.');
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
default:
|
|
1010
|
+
console.log(' Usage: 0agent memory connect github | sync | pull | status | disconnect');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function verifyGitHubToken(token) {
|
|
1015
|
+
try {
|
|
1016
|
+
const res = await fetch('https://api.github.com/user', {
|
|
1017
|
+
headers: { Authorization: `Bearer ${token}`, 'User-Agent': '0agent/1.0' },
|
|
1018
|
+
signal: AbortSignal.timeout(8000),
|
|
1019
|
+
});
|
|
1020
|
+
if (!res.ok) return null;
|
|
1021
|
+
const user = await res.json();
|
|
1022
|
+
return user.login;
|
|
1023
|
+
} catch { return null; }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function createGitHubRepo(token, repoName) {
|
|
1027
|
+
const res = await fetch('https://api.github.com/user/repos', {
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': '0agent/1.0' },
|
|
1030
|
+
body: JSON.stringify({
|
|
1031
|
+
name: repoName,
|
|
1032
|
+
description: '0agent memory — knowledge graph backed up automatically',
|
|
1033
|
+
private: true,
|
|
1034
|
+
auto_init: true,
|
|
1035
|
+
}),
|
|
1036
|
+
signal: AbortSignal.timeout(10000),
|
|
1037
|
+
});
|
|
1038
|
+
return res.ok || res.status === 422; // 422 = already exists
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async function daemonMemorySync(direction) {
|
|
1042
|
+
try {
|
|
1043
|
+
const res = await fetch(`${BASE_URL}/api/memory/${direction}`, { method: 'POST' });
|
|
1044
|
+
if (!res.ok) return null;
|
|
1045
|
+
return await res.json();
|
|
1046
|
+
} catch { return null; }
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// ─── Result preview — confirms the agent's work actually ran ────────────────
|
|
1050
|
+
|
|
1051
|
+
async function showResultPreview(result) {
|
|
1052
|
+
if (!result) return;
|
|
1053
|
+
const files = result.files_written ?? [];
|
|
1054
|
+
const cmds = result.commands_run ?? [];
|
|
1055
|
+
const out = result.output ?? '';
|
|
1056
|
+
|
|
1057
|
+
// 1. Server check — if a port was mentioned, verify HTTP response
|
|
1058
|
+
const allText = [...cmds, out].join(' ');
|
|
1059
|
+
const portMatch = allText.match(/(?:localhost:|port\s*[=:]?\s*)(\d{4,5})/i);
|
|
1060
|
+
if (portMatch) {
|
|
1061
|
+
const port = parseInt(portMatch[1], 10);
|
|
1062
|
+
await sleep(1200); // give server a moment to bind
|
|
1063
|
+
try {
|
|
1064
|
+
const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(2500) });
|
|
1065
|
+
const body = await res.text();
|
|
1066
|
+
const preview = body.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
1067
|
+
console.log(` \x1b[32m⬡ Confirmed live:\x1b[0m http://localhost:${port} (HTTP ${res.status})`);
|
|
1068
|
+
if (preview) console.log(` \x1b[2m${preview}\x1b[0m`);
|
|
1069
|
+
} catch {
|
|
1070
|
+
// Server not up yet — non-fatal, ExecutionVerifier already handled this
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 2. File preview — show first few lines of the most significant created file
|
|
1075
|
+
if (files.length > 0) {
|
|
1076
|
+
const mainFile = files.find(f => /\.(html|jsx?|tsx?|py|rs|go|md|css|json)$/.test(f)) ?? files[0];
|
|
1077
|
+
try {
|
|
1078
|
+
const { readFileSync } = await import('node:fs');
|
|
1079
|
+
const { resolve: res } = await import('node:path');
|
|
1080
|
+
const fullPath = res(process.env['ZEROAGENT_CWD'] ?? process.cwd(), mainFile);
|
|
1081
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
1082
|
+
const lines = content.split('\n').slice(0, 6).join('\n');
|
|
1083
|
+
console.log(`\n \x1b[2m── ${mainFile} ─────────────────────────────────\x1b[0m`);
|
|
1084
|
+
console.log(` \x1b[2m${lines}\x1b[0m`);
|
|
1085
|
+
if (content.split('\n').length > 6) console.log(` \x1b[2m...\x1b[0m`);
|
|
1086
|
+
} catch {}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
console.log();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// ─── Watch mode — ambient intelligence ──────────────────────────────────────
|
|
1093
|
+
|
|
1094
|
+
async function runWatch() {
|
|
1095
|
+
// Ensure daemon is running (auto-starts if needed)
|
|
1096
|
+
await requireDaemon();
|
|
1097
|
+
|
|
1098
|
+
const { basename } = await import('node:path');
|
|
1099
|
+
const cwdName = basename(process.cwd());
|
|
1100
|
+
|
|
1101
|
+
// Header
|
|
1102
|
+
console.log(`\n \x1b[1m0agent\x1b[0m watching \x1b[36m${cwdName}\x1b[0m`);
|
|
1103
|
+
console.log(` ${'─'.repeat(42)}`);
|
|
1104
|
+
|
|
1105
|
+
// Show current graph state
|
|
1106
|
+
try {
|
|
1107
|
+
const h = await fetch(`${BASE_URL}/api/health`).then(r => r.json()).catch(() => null);
|
|
1108
|
+
if (h) {
|
|
1109
|
+
console.log(` Graph: ${h.graph_nodes ?? 0} nodes · ${h.graph_edges ?? 0} edges`);
|
|
1110
|
+
console.log(` Uptime: ${Math.round((h.uptime_ms ?? 0) / 60000)}m · Sandbox: ${h.sandbox_backend ?? '—'}`);
|
|
1111
|
+
}
|
|
1112
|
+
} catch {}
|
|
1113
|
+
|
|
1114
|
+
// Show any unseen insights immediately
|
|
1115
|
+
try {
|
|
1116
|
+
const insights = await fetch(`${BASE_URL}/api/insights?seen=false`).then(r => r.json()).catch(() => []);
|
|
1117
|
+
if (Array.isArray(insights) && insights.length > 0) {
|
|
1118
|
+
console.log(`\n \x1b[33m${insights.length} unseen insight${insights.length > 1 ? 's' : ''}:\x1b[0m`);
|
|
1119
|
+
for (const ins of insights.slice(0, 3)) {
|
|
1120
|
+
const icon = ins.type === 'test_failure' ? '\x1b[31m●\x1b[0m' : ins.type === 'git_anomaly' ? '\x1b[33m⚡\x1b[0m' : '\x1b[36m◆\x1b[0m';
|
|
1121
|
+
console.log(` ${icon} ${ins.summary}`);
|
|
1122
|
+
if (ins.suggested_action) console.log(` \x1b[2m→ ${ins.suggested_action}\x1b[0m`);
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log(`\n Watching for insights...`);
|
|
1126
|
+
}
|
|
1127
|
+
} catch {}
|
|
1128
|
+
|
|
1129
|
+
console.log(`\n \x1b[2mPress Enter to run suggested action · q to quit\x1b[0m\n`);
|
|
1130
|
+
|
|
1131
|
+
// Connect WebSocket for live events
|
|
1132
|
+
const WS = await importWS();
|
|
1133
|
+
let lastSuggestion = null;
|
|
1134
|
+
let ws;
|
|
1135
|
+
|
|
1136
|
+
const connect = () => {
|
|
1137
|
+
ws = new WS(`ws://localhost:4200/ws`);
|
|
1138
|
+
|
|
1139
|
+
ws.on('open', () => {
|
|
1140
|
+
ws.send(JSON.stringify({ type: 'subscribe', topics: ['sessions', 'graph', 'insights', 'stats'] }));
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
ws.on('message', (data) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const event = JSON.parse(data.toString());
|
|
1146
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
1147
|
+
|
|
1148
|
+
switch (event.type) {
|
|
1149
|
+
case 'agent.insight': {
|
|
1150
|
+
const ins = event.insight ?? {};
|
|
1151
|
+
const icon = ins.type === 'test_failure' ? '\x1b[31m● test\x1b[0m'
|
|
1152
|
+
: ins.type === 'git_anomaly' ? '\x1b[33m⚡ git\x1b[0m'
|
|
1153
|
+
: '\x1b[36m◆ insight\x1b[0m';
|
|
1154
|
+
console.log(` [${ts}] ${icon} ${ins.summary}`);
|
|
1155
|
+
if (ins.suggested_action) {
|
|
1156
|
+
console.log(` \x1b[36m→ ${ins.suggested_action}\x1b[0m`);
|
|
1157
|
+
lastSuggestion = ins.suggested_action;
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
case 'session.completed':
|
|
1162
|
+
console.log(` [${ts}] \x1b[32m✓\x1b[0m Session completed`);
|
|
1163
|
+
break;
|
|
1164
|
+
case 'session.failed':
|
|
1165
|
+
console.log(` [${ts}] \x1b[31m✗\x1b[0m Session failed: ${event.error}`);
|
|
1166
|
+
break;
|
|
1167
|
+
case 'graph.weight_updated':
|
|
1168
|
+
// Subtle learning indicator — one dot per weight change
|
|
1169
|
+
process.stdout.write('\x1b[2m·\x1b[0m');
|
|
1170
|
+
break;
|
|
1171
|
+
case 'team.synced':
|
|
1172
|
+
console.log(` [${ts}] \x1b[35m⬡\x1b[0m Team synced (↑${event.deltas_pushed ?? 0} ↓${event.deltas_pulled ?? 0})`);
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
} catch {}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
ws.on('error', () => {});
|
|
1179
|
+
ws.on('close', () => {
|
|
1180
|
+
setTimeout(connect, 3000); // reconnect on daemon restart
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
connect();
|
|
1185
|
+
|
|
1186
|
+
// Keyboard handling — Enter = act, q = quit
|
|
1187
|
+
if (process.stdin.isTTY) {
|
|
1188
|
+
process.stdin.setRawMode(true);
|
|
1189
|
+
process.stdin.resume();
|
|
1190
|
+
process.stdin.setEncoding('utf8');
|
|
1191
|
+
process.stdin.on('data', async (key) => {
|
|
1192
|
+
if (key === '\u0003' || key === 'q') { // Ctrl+C or q
|
|
1193
|
+
process.stdout.write('\n');
|
|
1194
|
+
ws?.close();
|
|
1195
|
+
process.stdin.setRawMode(false);
|
|
1196
|
+
process.exit(0);
|
|
1197
|
+
}
|
|
1198
|
+
if (key === '\r' && lastSuggestion) {
|
|
1199
|
+
// Extract executable part from suggestion
|
|
1200
|
+
const cmd = lastSuggestion.match(/(?:0agent\s+)?(\/?[\w-]+(?:\s+"[^"]*")?)/);
|
|
1201
|
+
if (cmd) {
|
|
1202
|
+
process.stdout.write('\n');
|
|
1203
|
+
const parts = cmd[1].trim().split(/\s+/);
|
|
1204
|
+
if (parts[0].startsWith('/')) {
|
|
1205
|
+
await runSkill(parts[0].slice(1), parts.slice(1));
|
|
1206
|
+
} else if (parts[0] === 'run' || !['start','stop','init','chat'].includes(parts[0])) {
|
|
1207
|
+
await runTask(parts[0] === 'run' ? parts.slice(1) : parts);
|
|
1208
|
+
}
|
|
1209
|
+
lastSuggestion = null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
} else {
|
|
1214
|
+
// Non-interactive: just watch, no keyboard
|
|
1215
|
+
process.on('SIGINT', () => { ws?.close(); process.exit(0); });
|
|
1216
|
+
await new Promise(() => {}); // run forever
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
807
1220
|
function showHelp() {
|
|
808
1221
|
console.log(`
|
|
809
1222
|
0agent — An agent that learns.
|
|
@@ -838,6 +1251,10 @@ function showHelp() {
|
|
|
838
1251
|
0agent /build --task next
|
|
839
1252
|
0agent /qa --url https://staging.myapp.com
|
|
840
1253
|
0agent serve --tunnel # then share the URL + 0agent team join <CODE>
|
|
1254
|
+
0agent watch # ambient mode — live insights, press Enter to act
|
|
1255
|
+
|
|
1256
|
+
Auto-start:
|
|
1257
|
+
The daemon auto-starts on first 0agent run. No need for 0agent start.
|
|
841
1258
|
`);
|
|
842
1259
|
}
|
|
843
1260
|
|
|
@@ -853,10 +1270,46 @@ async function isDaemonRunning() {
|
|
|
853
1270
|
}
|
|
854
1271
|
|
|
855
1272
|
async function requireDaemon() {
|
|
856
|
-
if (
|
|
857
|
-
|
|
1273
|
+
if (await isDaemonRunning()) return;
|
|
1274
|
+
|
|
1275
|
+
// Auto-start if config exists — no manual `0agent start` needed
|
|
1276
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
1277
|
+
console.log('\n Not initialised. Run: 0agent init\n');
|
|
858
1278
|
process.exit(1);
|
|
859
1279
|
}
|
|
1280
|
+
|
|
1281
|
+
process.stdout.write(' Starting daemon');
|
|
1282
|
+
await _startDaemonBackground();
|
|
1283
|
+
|
|
1284
|
+
for (let i = 0; i < 24; i++) {
|
|
1285
|
+
await sleep(500);
|
|
1286
|
+
process.stdout.write('.');
|
|
1287
|
+
if (await isDaemonRunning()) {
|
|
1288
|
+
process.stdout.write(' ✓\n\n');
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
process.stdout.write(' ✗\n');
|
|
1293
|
+
console.log(' Daemon failed to start. Check: 0agent logs\n');
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Internal: spawn daemon process without printing the full startup banner
|
|
1298
|
+
async function _startDaemonBackground() {
|
|
1299
|
+
const { resolve: res, dirname: dn, existsSync: ex } = await import('node:path').then(m => m);
|
|
1300
|
+
const pkgRoot = res(dn(new URL(import.meta.url).pathname), '..');
|
|
1301
|
+
const bundled = res(pkgRoot, 'dist', 'daemon.mjs');
|
|
1302
|
+
const devPath = res(pkgRoot, 'packages', 'daemon', 'dist', 'start.js');
|
|
1303
|
+
const script = ex(bundled) ? bundled : devPath;
|
|
1304
|
+
if (!ex(script)) return;
|
|
1305
|
+
|
|
1306
|
+
mkdirSync(resolve(AGENT_DIR, 'logs'), { recursive: true });
|
|
1307
|
+
const child = spawn(process.execPath, [script], {
|
|
1308
|
+
detached: true,
|
|
1309
|
+
stdio: 'ignore',
|
|
1310
|
+
env: { ...process.env, ZEROAGENT_CONFIG: CONFIG_PATH },
|
|
1311
|
+
});
|
|
1312
|
+
child.unref();
|
|
860
1313
|
}
|
|
861
1314
|
|
|
862
1315
|
async function importWS() {
|