0agent 1.0.8 → 1.0.10

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 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/dist/daemon.mjs CHANGED
@@ -1736,6 +1736,20 @@ var AGENT_TOOLS = [
1736
1736
  path: { type: "string", description: 'Directory path relative to working directory (default: ".")' }
1737
1737
  }
1738
1738
  }
1739
+ },
1740
+ {
1741
+ name: "scrape_url",
1742
+ description: "Scrape a URL and return clean structured content. Handles JavaScript-rendered pages, auto-adapts to page structure, returns text/links/metadata. Better than shell curl for web pages.",
1743
+ input_schema: {
1744
+ type: "object",
1745
+ properties: {
1746
+ url: { type: "string", description: "URL to scrape" },
1747
+ mode: { type: "string", description: 'What to extract: "text" (default), "links", "tables", "full", "markdown"' },
1748
+ selector: { type: "string", description: "Optional CSS selector to target specific element" },
1749
+ wait_ms: { type: "number", description: "Wait N ms after page load (for JS-heavy pages, default 0)" }
1750
+ },
1751
+ required: ["url"]
1752
+ }
1739
1753
  }
1740
1754
  ];
1741
1755
  var LLMExecutor = class {
@@ -2135,6 +2149,13 @@ var AgentExecutor = class {
2135
2149
  return this.readFile(String(input.path ?? ""));
2136
2150
  case "list_dir":
2137
2151
  return this.listDir(input.path ? String(input.path) : void 0);
2152
+ case "scrape_url":
2153
+ return this.scrapeUrl(
2154
+ String(input.url ?? ""),
2155
+ String(input.mode ?? "text"),
2156
+ input.selector ? String(input.selector) : void 0,
2157
+ Number(input.wait_ms ?? 0)
2158
+ );
2138
2159
  default:
2139
2160
  return `Unknown tool: ${name}`;
2140
2161
  }
@@ -2174,6 +2195,45 @@ var AgentExecutor = class {
2174
2195
  return content.length > 8e3 ? content.slice(0, 8e3) + `
2175
2196
  \u2026[truncated, ${content.length} total bytes]` : content;
2176
2197
  }
2198
+ async scrapeUrl(url, mode, selector, waitMs) {
2199
+ if (!url.startsWith("http")) return "Error: URL must start with http:// or https://";
2200
+ const selectorLine = selector ? `element = page.find('${selector}')
2201
+ content = element.text if element else page.get_all_text()` : `content = page.get_all_text()`;
2202
+ const modeLine = mode === "links" ? `result = [a.attrib.get('href','') for a in page.find_all('a') if a.attrib.get('href','').startswith('http')]` : mode === "tables" ? `result = [str(t) for t in page.find_all('table')]` : mode === "markdown" ? `result = page.get_all_text()` : `result = page.get_all_text()`;
2203
+ const script = [
2204
+ `import sys`,
2205
+ `try:`,
2206
+ ` from scrapling import Fetcher`,
2207
+ `except ImportError:`,
2208
+ ` import subprocess, sys`,
2209
+ ` subprocess.run([sys.executable, '-m', 'pip', 'install', 'scrapling', '-q'], check=True)`,
2210
+ ` from scrapling import Fetcher`,
2211
+ `try:`,
2212
+ ` fetcher = Fetcher(auto_match=False)`,
2213
+ ` page = fetcher.get('${url}', timeout=20)`,
2214
+ ` ${modeLine}`,
2215
+ ` if isinstance(result, list):`,
2216
+ ` print('\\n'.join(str(r) for r in result[:50]))`,
2217
+ ` else:`,
2218
+ ` text = str(result).strip()`,
2219
+ ` print(text[:6000] + ('...[truncated]' if len(text)>6000 else ''))`,
2220
+ `except Exception as e:`,
2221
+ ` # Fallback to simple fetch if scrapling fails`,
2222
+ ` import urllib.request`,
2223
+ ` try:`,
2224
+ ` req = urllib.request.Request('${url}', headers={'User-Agent': 'Mozilla/5.0'})`,
2225
+ ` with urllib.request.urlopen(req, timeout=15) as resp:`,
2226
+ ` body = resp.read().decode('utf-8', errors='ignore')`,
2227
+ ` # Strip tags simply`,
2228
+ ` import re`,
2229
+ ` text = re.sub(r'<[^>]+>', ' ', body)`,
2230
+ ` text = re.sub(r'\\s+', ' ', text).strip()`,
2231
+ ` print(text[:5000])`,
2232
+ ` except Exception as e2:`,
2233
+ ` print(f'Scrape failed: {e} / {e2}', file=sys.stderr)`
2234
+ ].join("\n");
2235
+ return this.shellExec(`python3 -c "${script.replace(/"/g, '\\"').replace(/\n/g, ";")}"`, 3e4);
2236
+ }
2177
2237
  listDir(dirPath) {
2178
2238
  const safe = this.safePath(dirPath ?? ".");
2179
2239
  if (!safe) return "Error: path outside working directory";
@@ -2212,6 +2272,7 @@ var AgentExecutor = class {
2212
2272
  if (toolName === "write_file") return `"${input.path}"`;
2213
2273
  if (toolName === "read_file") return `"${input.path}"`;
2214
2274
  if (toolName === "list_dir") return `"${input.path ?? "."}"`;
2275
+ if (toolName === "scrape_url") return `"${String(input.url ?? "").slice(0, 60)}" mode=${input.mode ?? "text"}`;
2215
2276
  return JSON.stringify(input).slice(0, 60);
2216
2277
  }
2217
2278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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",