0agent 1.0.8 → 1.0.9

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 +193 -0
  2. package/package.json +1 -1
package/bin/0agent.js CHANGED
@@ -86,6 +86,14 @@ switch (cmd) {
86
86
  showLogs(args.slice(1));
87
87
  break;
88
88
 
89
+ case 'team':
90
+ await runTeamCommand(args.slice(1));
91
+ break;
92
+
93
+ case 'serve':
94
+ await runServe(args.slice(1));
95
+ break;
96
+
89
97
  default:
90
98
  showHelp();
91
99
  break;
@@ -614,6 +622,183 @@ function showLogs(logArgs) {
614
622
 
615
623
  // ─── Help ─────────────────────────────────────────────────────────────────
616
624
 
625
+ // ─── Team commands ────────────────────────────────────────────────────────────
626
+
627
+ async function runTeamCommand(teamArgs) {
628
+ const sub = teamArgs[0];
629
+ const SYNC_URL = process.env['ZEROAGENT_SYNC'] ?? 'http://localhost:4201';
630
+
631
+ switch (sub) {
632
+ case 'create': {
633
+ const name = teamArgs.slice(1).join(' ');
634
+ if (!name) { console.log(' Usage: 0agent team create "<name>"'); break; }
635
+ const res = await fetch(`${SYNC_URL}/api/teams`, {
636
+ method: 'POST',
637
+ headers: { 'Content-Type': 'application/json' },
638
+ body: JSON.stringify({
639
+ name,
640
+ creator_entity_id: crypto.randomUUID(),
641
+ creator_name: process.env['USER'] ?? 'User',
642
+ }),
643
+ }).catch(() => null);
644
+ if (!res?.ok) { console.log(` Sync server not running. Start it with: 0agent serve`); break; }
645
+ const team = await res.json();
646
+ console.log(`\n ✓ Team created: ${team.name}`);
647
+ console.log(` Invite code: \x1b[1m${team.invite_code}\x1b[0m`);
648
+ console.log(`\n Share with teammates:`);
649
+ console.log(` 0agent team join ${team.invite_code} --server ${SYNC_URL}\n`);
650
+ break;
651
+ }
652
+
653
+ case 'join': {
654
+ const code = teamArgs[1]?.toUpperCase();
655
+ const serverIdx = teamArgs.indexOf('--server');
656
+ const serverUrl = serverIdx >= 0 ? teamArgs[serverIdx + 1] : SYNC_URL;
657
+ if (!code) { console.log(' Usage: 0agent team join <CODE> [--server <url>]'); break; }
658
+ const res = await fetch(`${serverUrl}/api/teams/by-code/${code}`).catch(() => null);
659
+ if (!res?.ok) { console.log(` Invalid code or sync server unreachable: ${serverUrl}`); break; }
660
+ const team = await res.json();
661
+ const joinRes = await fetch(`${serverUrl}/api/teams/${team.id}/join`, {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify({
665
+ entity_node_id: crypto.randomUUID(),
666
+ name: process.env['USER'] ?? 'User',
667
+ }),
668
+ });
669
+ if (!joinRes.ok) { console.log(' Failed to join team.'); break; }
670
+ console.log(`\n ✓ Joined: ${team.name}`);
671
+ console.log(` Members: ${team.members?.length ?? '?'}`);
672
+ console.log(` Sync server: ${serverUrl}\n`);
673
+ break;
674
+ }
675
+
676
+ case 'list': {
677
+ // Show teams from local teams.yaml
678
+ const { readFileSync, existsSync } = await import('node:fs');
679
+ const { resolve } = await import('node:path');
680
+ const { homedir } = await import('node:os');
681
+ const teamsPath = resolve(homedir(), '.0agent', 'teams.yaml');
682
+ if (!existsSync(teamsPath)) { console.log('\n No teams joined yet. Use: 0agent team join <CODE>\n'); break; }
683
+ const YAML = await import('yaml');
684
+ const config = YAML.parse(readFileSync(teamsPath, 'utf8'));
685
+ console.log('\n Your teams:\n');
686
+ for (const m of (config.memberships ?? [])) {
687
+ const ago = m.last_synced_at ? `synced ${Math.round((Date.now() - m.last_synced_at) / 60000)}m ago` : 'never synced';
688
+ console.log(` ${m.team_name.padEnd(24)} ${m.invite_code} ${ago}`);
689
+ console.log(` ${' '.repeat(24)} ${m.server_url}`);
690
+ }
691
+ console.log();
692
+ break;
693
+ }
694
+
695
+ default:
696
+ console.log(' Usage: 0agent team create "<name>" | join <CODE> [--server <url>] | list');
697
+ }
698
+ }
699
+
700
+ // ─── Serve command (sync server + optional tunnel) ────────────────────────────
701
+
702
+ async function runServe(serveArgs) {
703
+ const hasTunnel = serveArgs.includes('--tunnel');
704
+ const port = parseInt(serveArgs.find(a => a.match(/^\d+$/)) ?? '4201', 10);
705
+
706
+ console.log(`\n Starting 0agent sync server on port ${port}...\n`);
707
+
708
+ // Find sync server entry point
709
+ const { resolve, dirname } = await import('node:path');
710
+ const { existsSync } = await import('node:fs');
711
+ const { spawn } = await import('node:child_process');
712
+ const { networkInterfaces } = await import('node:os');
713
+
714
+ const pkgRoot = resolve(dirname(new URL(import.meta.url).pathname), '..');
715
+ const serverScript = resolve(pkgRoot, 'packages', 'sync-server', 'src', 'index.ts');
716
+
717
+ if (!existsSync(serverScript)) {
718
+ console.log(' Sync server not found in package. Install with: npm install -g 0agent');
719
+ return;
720
+ }
721
+
722
+ // Start sync server
723
+ const proc = spawn(process.execPath, ['--experimental-specifier-resolution=node', serverScript], {
724
+ env: { ...process.env, SYNC_PORT: String(port), SYNC_HOST: '0.0.0.0' },
725
+ stdio: 'inherit',
726
+ detached: false,
727
+ });
728
+
729
+ // Get LAN IP
730
+ const nets = networkInterfaces();
731
+ let lanIp = '127.0.0.1';
732
+ for (const iface of Object.values(nets)) {
733
+ if (!iface) continue;
734
+ for (const net of iface) {
735
+ if (net.family === 'IPv4' && !net.internal) { lanIp = net.address; break; }
736
+ }
737
+ }
738
+
739
+ await sleep(1500);
740
+
741
+ const localUrl = `http://localhost:${port}`;
742
+ const lanUrl = `http://${lanIp}:${port}`;
743
+
744
+ console.log(`\n ✓ Sync server running`);
745
+ console.log(` Local: ${localUrl}`);
746
+ console.log(` LAN: ${lanUrl} ← share with teammates on same WiFi`);
747
+
748
+ if (hasTunnel) {
749
+ console.log('\n Opening public tunnel...');
750
+ let tunnelUrl = null;
751
+
752
+ // Try cloudflared
753
+ try {
754
+ const { execSync: es } = await import('node:child_process');
755
+ es('which cloudflared', { stdio: 'ignore' });
756
+ const cf = spawn('cloudflared', ['tunnel', '--url', localUrl], { stdio: ['ignore', 'pipe', 'pipe'] });
757
+ cf.unref();
758
+ tunnelUrl = await waitForTunnelUrl(cf, /https:\/\/[a-z0-9\-]+\.trycloudflare\.com/i, 12000);
759
+ } catch {}
760
+
761
+ // Try ngrok
762
+ if (!tunnelUrl) {
763
+ try {
764
+ const { execSync: es } = await import('node:child_process');
765
+ es('which ngrok', { stdio: 'ignore' });
766
+ const ng = spawn('ngrok', ['http', String(port), '--log=stdout'], { stdio: ['ignore', 'pipe', 'pipe'] });
767
+ ng.unref();
768
+ tunnelUrl = await waitForTunnelUrl(ng, /https:\/\/[a-z0-9\-]+\.ngrok/i, 8000);
769
+ } catch {}
770
+ }
771
+
772
+ if (tunnelUrl) {
773
+ console.log(` Public: \x1b[1m${tunnelUrl}\x1b[0m ← share with anyone`);
774
+ const code = Math.random().toString(36).slice(2,5).toUpperCase() + '-' + Math.floor(1000+Math.random()*9000);
775
+ console.log(`\n Share this with teammates:`);
776
+ console.log(` 0agent team join <CODE> --server ${tunnelUrl}\n`);
777
+ } else {
778
+ console.log(' No tunnel tool found. Install cloudflared: brew install cloudflared');
779
+ console.log(' Using LAN only.');
780
+ }
781
+ }
782
+
783
+ console.log('\n Press Ctrl+C to stop.\n');
784
+ proc.on('close', () => process.exit(0));
785
+ }
786
+
787
+ async function waitForTunnelUrl(proc, pattern, timeout) {
788
+ return new Promise(resolve => {
789
+ const chunks = [];
790
+ const onData = d => {
791
+ const s = d.toString(); chunks.push(s);
792
+ const match = chunks.join('').match(pattern);
793
+ if (match) { cleanup(); resolve(match[0]); }
794
+ };
795
+ proc.stdout?.on('data', onData);
796
+ proc.stderr?.on('data', onData);
797
+ const timer = setTimeout(() => { cleanup(); resolve(null); }, timeout);
798
+ const cleanup = () => { clearTimeout(timer); proc.stdout?.removeListener('data', onData); proc.stderr?.removeListener('data', onData); };
799
+ });
800
+ }
801
+
617
802
  function showHelp() {
618
803
  console.log(`
619
804
  0agent — An agent that learns.
@@ -632,6 +817,13 @@ function showHelp() {
632
817
  0agent improve Self-improvement analysis
633
818
  0agent logs Tail daemon logs
634
819
 
820
+ Team collaboration:
821
+ 0agent serve Start sync server (LAN)
822
+ 0agent serve --tunnel Start sync server + public tunnel
823
+ 0agent team create "<name>" Create a team, get invite code
824
+ 0agent team join <CODE> Join a team by invite code
825
+ 0agent team list List your teams
826
+
635
827
  Dashboard:
636
828
  http://localhost:4200 Web UI (after starting daemon)
637
829
 
@@ -640,6 +832,7 @@ function showHelp() {
640
832
  0agent /research "Acme Corp funding"
641
833
  0agent /build --task next
642
834
  0agent /qa --url https://staging.myapp.com
835
+ 0agent serve --tunnel # then share the URL + 0agent team join <CODE>
643
836
  `);
644
837
  }
645
838
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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",