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.
Files changed (3) hide show
  1. package/bin/0agent.js +534 -81
  2. package/dist/daemon.mjs +492 -36
  3. 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.0 · Apache 2.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 Run `0agent start` to start the daemon.\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
- console.log(' Step 1 of 5: LLM Provider\n');
127
- const provider = await choose(' Which LLM provider?', [
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, free)',
173
+ 'Ollama (local — no API key)',
133
174
  ], 0);
134
- const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][provider];
175
+ const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][providerIdx];
135
176
 
136
- // Model selection per provider
137
177
  const MODELS = {
138
- anthropic: [
139
- 'claude-sonnet-4-6 ← recommended (fast + smart)',
140
- 'claude-opus-4-6 (most capable, slower)',
141
- 'claude-haiku-4-5 (fastest, cheapest)',
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
- let model = '';
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 ask(`\n API Key: `);
173
- if (!apiKey.trim()) {
174
- console.log(' ⚠️ No API key provided. You can set it later in ~/.0agent/config.yaml');
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
- // Validate key format
177
- const keyPrefixes = { anthropic: 'sk-ant-', openai: 'sk-', xai: 'xai-', gemini: 'AI' };
178
- const expectedPrefix = keyPrefixes[providerKey];
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(' API key format looks valid');
198
+ console.log(' \x1b[32m✓\x1b[0m Key format valid');
183
199
  }
184
200
  }
185
201
  }
186
202
 
187
- console.log('\n Step 2 of 5: Embedding model\n');
188
- const embedding = await choose(' Embedding backend?', [
189
- 'Local via Ollama (nomic-embed-text)free, private',
190
- 'OpenAI text-embedding-3-small (cloud)',
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 syncrecommended',
206
+ 'No local only',
192
207
  ], 0);
193
- const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embedding];
194
208
 
195
- console.log('\n Step 3 of 5: Sandbox backend\n');
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(` Using: ${sandboxChoice}`);
255
+ console.log(`\n Sandbox: \x1b[32m${sandboxChoice}\x1b[0m detected`);
200
256
 
201
- console.log('\n Step 4 of 5: Seed graph\n');
202
- const seed = await choose(' Start with a seed graph?', [
203
- 'software-engineering (skills + sprint workflow) ← recommended',
204
- 'scratch (empty graph)',
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 = seed === 0 ? 'software-engineering' : null;
207
-
208
- // Step 5: confirm
209
- console.log('\n Step 5 of 5: Ready\n');
210
- console.log(` Provider: ${providerKey}`);
211
- console.log(` Model: ${model}`);
212
- console.log(` API Key: ${apiKey ? apiKey.slice(0, 8) + '••••••••' : '(not set)'}`);
213
- console.log(` Sandbox: ${sandboxChoice}`);
214
- console.log(` Seed: ${seedName ?? 'scratch'}`);
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 (!(await isDaemonRunning())) {
857
- console.log('\n Daemon is not running. Start it with: 0agent start\n');
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() {