0agent 1.0.16 → 1.0.17

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 (2) hide show
  1. package/bin/0agent.js +219 -3
  2. package/package.json +1 -1
package/bin/0agent.js CHANGED
@@ -94,6 +94,10 @@ switch (cmd) {
94
94
  await runServe(args.slice(1));
95
95
  break;
96
96
 
97
+ case 'watch':
98
+ await runWatch();
99
+ break;
100
+
97
101
  default:
98
102
  showHelp();
99
103
  break;
@@ -447,12 +451,12 @@ async function streamSession(sessionId) {
447
451
  break;
448
452
  case 'session.completed': {
449
453
  if (streaming) { process.stdout.write('\n'); streaming = false; }
450
- // Show files written + commands run
451
454
  const r = event.result ?? {};
452
455
  if (r.files_written?.length) console.log(`\n \x1b[32m✓\x1b[0m Files: ${r.files_written.join(', ')}`);
453
456
  if (r.commands_run?.length) console.log(` \x1b[32m✓\x1b[0m Commands run: ${r.commands_run.length}`);
454
457
  if (r.tokens_used) console.log(` \x1b[2m${r.tokens_used} tokens · ${r.model}\x1b[0m`);
455
458
  console.log('\n \x1b[32m✓ Done\x1b[0m\n');
459
+ await showResultPreview(r); // confirm server/file actually exists
456
460
  ws.close();
457
461
  resolve();
458
462
  break;
@@ -496,6 +500,7 @@ async function pollSession(sessionId) {
496
500
  console.log('\n ✓ Done\n');
497
501
  const out = s.result?.output ?? s.result;
498
502
  if (out && typeof out === 'string') console.log(` ${out}\n`);
503
+ await showResultPreview(s.result ?? {});
499
504
  return;
500
505
  }
501
506
  if (s.status === 'failed') {
@@ -804,6 +809,177 @@ async function waitForTunnelUrl(proc, pattern, timeout) {
804
809
  });
805
810
  }
806
811
 
812
+ // ─── Result preview — confirms the agent's work actually ran ────────────────
813
+
814
+ async function showResultPreview(result) {
815
+ if (!result) return;
816
+ const files = result.files_written ?? [];
817
+ const cmds = result.commands_run ?? [];
818
+ const out = result.output ?? '';
819
+
820
+ // 1. Server check — if a port was mentioned, verify HTTP response
821
+ const allText = [...cmds, out].join(' ');
822
+ const portMatch = allText.match(/(?:localhost:|port\s*[=:]?\s*)(\d{4,5})/i);
823
+ if (portMatch) {
824
+ const port = parseInt(portMatch[1], 10);
825
+ await sleep(1200); // give server a moment to bind
826
+ try {
827
+ const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(2500) });
828
+ const body = await res.text();
829
+ const preview = body.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120);
830
+ console.log(` \x1b[32m⬡ Confirmed live:\x1b[0m http://localhost:${port} (HTTP ${res.status})`);
831
+ if (preview) console.log(` \x1b[2m${preview}\x1b[0m`);
832
+ } catch {
833
+ // Server not up yet — non-fatal, ExecutionVerifier already handled this
834
+ }
835
+ }
836
+
837
+ // 2. File preview — show first few lines of the most significant created file
838
+ if (files.length > 0) {
839
+ const mainFile = files.find(f => /\.(html|jsx?|tsx?|py|rs|go|md|css|json)$/.test(f)) ?? files[0];
840
+ try {
841
+ const { readFileSync } = await import('node:fs');
842
+ const { resolve: res } = await import('node:path');
843
+ const fullPath = res(process.env['ZEROAGENT_CWD'] ?? process.cwd(), mainFile);
844
+ const content = readFileSync(fullPath, 'utf8');
845
+ const lines = content.split('\n').slice(0, 6).join('\n');
846
+ console.log(`\n \x1b[2m── ${mainFile} ─────────────────────────────────\x1b[0m`);
847
+ console.log(` \x1b[2m${lines}\x1b[0m`);
848
+ if (content.split('\n').length > 6) console.log(` \x1b[2m...\x1b[0m`);
849
+ } catch {}
850
+ }
851
+
852
+ console.log();
853
+ }
854
+
855
+ // ─── Watch mode — ambient intelligence ──────────────────────────────────────
856
+
857
+ async function runWatch() {
858
+ // Ensure daemon is running (auto-starts if needed)
859
+ await requireDaemon();
860
+
861
+ const { basename } = await import('node:path');
862
+ const cwdName = basename(process.cwd());
863
+
864
+ // Header
865
+ console.log(`\n \x1b[1m0agent\x1b[0m watching \x1b[36m${cwdName}\x1b[0m`);
866
+ console.log(` ${'─'.repeat(42)}`);
867
+
868
+ // Show current graph state
869
+ try {
870
+ const h = await fetch(`${BASE_URL}/api/health`).then(r => r.json()).catch(() => null);
871
+ if (h) {
872
+ console.log(` Graph: ${h.graph_nodes ?? 0} nodes · ${h.graph_edges ?? 0} edges`);
873
+ console.log(` Uptime: ${Math.round((h.uptime_ms ?? 0) / 60000)}m · Sandbox: ${h.sandbox_backend ?? '—'}`);
874
+ }
875
+ } catch {}
876
+
877
+ // Show any unseen insights immediately
878
+ try {
879
+ const insights = await fetch(`${BASE_URL}/api/insights?seen=false`).then(r => r.json()).catch(() => []);
880
+ if (Array.isArray(insights) && insights.length > 0) {
881
+ console.log(`\n \x1b[33m${insights.length} unseen insight${insights.length > 1 ? 's' : ''}:\x1b[0m`);
882
+ for (const ins of insights.slice(0, 3)) {
883
+ const icon = ins.type === 'test_failure' ? '\x1b[31m●\x1b[0m' : ins.type === 'git_anomaly' ? '\x1b[33m⚡\x1b[0m' : '\x1b[36m◆\x1b[0m';
884
+ console.log(` ${icon} ${ins.summary}`);
885
+ if (ins.suggested_action) console.log(` \x1b[2m→ ${ins.suggested_action}\x1b[0m`);
886
+ }
887
+ } else {
888
+ console.log(`\n Watching for insights...`);
889
+ }
890
+ } catch {}
891
+
892
+ console.log(`\n \x1b[2mPress Enter to run suggested action · q to quit\x1b[0m\n`);
893
+
894
+ // Connect WebSocket for live events
895
+ const WS = await importWS();
896
+ let lastSuggestion = null;
897
+ let ws;
898
+
899
+ const connect = () => {
900
+ ws = new WS(`ws://localhost:4200/ws`);
901
+
902
+ ws.on('open', () => {
903
+ ws.send(JSON.stringify({ type: 'subscribe', topics: ['sessions', 'graph', 'insights', 'stats'] }));
904
+ });
905
+
906
+ ws.on('message', (data) => {
907
+ try {
908
+ const event = JSON.parse(data.toString());
909
+ const ts = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
910
+
911
+ switch (event.type) {
912
+ case 'agent.insight': {
913
+ const ins = event.insight ?? {};
914
+ const icon = ins.type === 'test_failure' ? '\x1b[31m● test\x1b[0m'
915
+ : ins.type === 'git_anomaly' ? '\x1b[33m⚡ git\x1b[0m'
916
+ : '\x1b[36m◆ insight\x1b[0m';
917
+ console.log(` [${ts}] ${icon} ${ins.summary}`);
918
+ if (ins.suggested_action) {
919
+ console.log(` \x1b[36m→ ${ins.suggested_action}\x1b[0m`);
920
+ lastSuggestion = ins.suggested_action;
921
+ }
922
+ break;
923
+ }
924
+ case 'session.completed':
925
+ console.log(` [${ts}] \x1b[32m✓\x1b[0m Session completed`);
926
+ break;
927
+ case 'session.failed':
928
+ console.log(` [${ts}] \x1b[31m✗\x1b[0m Session failed: ${event.error}`);
929
+ break;
930
+ case 'graph.weight_updated':
931
+ // Subtle learning indicator — one dot per weight change
932
+ process.stdout.write('\x1b[2m·\x1b[0m');
933
+ break;
934
+ case 'team.synced':
935
+ console.log(` [${ts}] \x1b[35m⬡\x1b[0m Team synced (↑${event.deltas_pushed ?? 0} ↓${event.deltas_pulled ?? 0})`);
936
+ break;
937
+ }
938
+ } catch {}
939
+ });
940
+
941
+ ws.on('error', () => {});
942
+ ws.on('close', () => {
943
+ setTimeout(connect, 3000); // reconnect on daemon restart
944
+ });
945
+ };
946
+
947
+ connect();
948
+
949
+ // Keyboard handling — Enter = act, q = quit
950
+ if (process.stdin.isTTY) {
951
+ process.stdin.setRawMode(true);
952
+ process.stdin.resume();
953
+ process.stdin.setEncoding('utf8');
954
+ process.stdin.on('data', async (key) => {
955
+ if (key === '\u0003' || key === 'q') { // Ctrl+C or q
956
+ process.stdout.write('\n');
957
+ ws?.close();
958
+ process.stdin.setRawMode(false);
959
+ process.exit(0);
960
+ }
961
+ if (key === '\r' && lastSuggestion) {
962
+ // Extract executable part from suggestion
963
+ const cmd = lastSuggestion.match(/(?:0agent\s+)?(\/?[\w-]+(?:\s+"[^"]*")?)/);
964
+ if (cmd) {
965
+ process.stdout.write('\n');
966
+ const parts = cmd[1].trim().split(/\s+/);
967
+ if (parts[0].startsWith('/')) {
968
+ await runSkill(parts[0].slice(1), parts.slice(1));
969
+ } else if (parts[0] === 'run' || !['start','stop','init','chat'].includes(parts[0])) {
970
+ await runTask(parts[0] === 'run' ? parts.slice(1) : parts);
971
+ }
972
+ lastSuggestion = null;
973
+ }
974
+ }
975
+ });
976
+ } else {
977
+ // Non-interactive: just watch, no keyboard
978
+ process.on('SIGINT', () => { ws?.close(); process.exit(0); });
979
+ await new Promise(() => {}); // run forever
980
+ }
981
+ }
982
+
807
983
  function showHelp() {
808
984
  console.log(`
809
985
  0agent — An agent that learns.
@@ -838,6 +1014,10 @@ function showHelp() {
838
1014
  0agent /build --task next
839
1015
  0agent /qa --url https://staging.myapp.com
840
1016
  0agent serve --tunnel # then share the URL + 0agent team join <CODE>
1017
+ 0agent watch # ambient mode — live insights, press Enter to act
1018
+
1019
+ Auto-start:
1020
+ The daemon auto-starts on first 0agent run. No need for 0agent start.
841
1021
  `);
842
1022
  }
843
1023
 
@@ -853,10 +1033,46 @@ async function isDaemonRunning() {
853
1033
  }
854
1034
 
855
1035
  async function requireDaemon() {
856
- if (!(await isDaemonRunning())) {
857
- console.log('\n Daemon is not running. Start it with: 0agent start\n');
1036
+ if (await isDaemonRunning()) return;
1037
+
1038
+ // Auto-start if config exists — no manual `0agent start` needed
1039
+ if (!existsSync(CONFIG_PATH)) {
1040
+ console.log('\n Not initialised. Run: 0agent init\n');
858
1041
  process.exit(1);
859
1042
  }
1043
+
1044
+ process.stdout.write(' Starting daemon');
1045
+ await _startDaemonBackground();
1046
+
1047
+ for (let i = 0; i < 24; i++) {
1048
+ await sleep(500);
1049
+ process.stdout.write('.');
1050
+ if (await isDaemonRunning()) {
1051
+ process.stdout.write(' ✓\n\n');
1052
+ return;
1053
+ }
1054
+ }
1055
+ process.stdout.write(' ✗\n');
1056
+ console.log(' Daemon failed to start. Check: 0agent logs\n');
1057
+ process.exit(1);
1058
+ }
1059
+
1060
+ // Internal: spawn daemon process without printing the full startup banner
1061
+ async function _startDaemonBackground() {
1062
+ const { resolve: res, dirname: dn, existsSync: ex } = await import('node:path').then(m => m);
1063
+ const pkgRoot = res(dn(new URL(import.meta.url).pathname), '..');
1064
+ const bundled = res(pkgRoot, 'dist', 'daemon.mjs');
1065
+ const devPath = res(pkgRoot, 'packages', 'daemon', 'dist', 'start.js');
1066
+ const script = ex(bundled) ? bundled : devPath;
1067
+ if (!ex(script)) return;
1068
+
1069
+ mkdirSync(resolve(AGENT_DIR, 'logs'), { recursive: true });
1070
+ const child = spawn(process.execPath, [script], {
1071
+ detached: true,
1072
+ stdio: 'ignore',
1073
+ env: { ...process.env, ZEROAGENT_CONFIG: CONFIG_PATH },
1074
+ });
1075
+ child.unref();
860
1076
  }
861
1077
 
862
1078
  async function importWS() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
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",