0agent 1.0.10 → 1.0.12

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/dist/daemon.mjs +838 -367
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/daemon/src/ZeroAgentDaemon.ts
2
- import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
3
- import { resolve as resolve4 } from "node:path";
2
+ import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
3
+ import { resolve as resolve5 } from "node:path";
4
4
  import { homedir as homedir3 } from "node:os";
5
5
 
6
6
  // packages/core/src/graph/GraphNode.ts
@@ -1686,365 +1686,481 @@ var EntityScopedContextLoader = class {
1686
1686
  };
1687
1687
 
1688
1688
  // packages/daemon/src/AgentExecutor.ts
1689
- import { spawn } from "node:child_process";
1690
- import { writeFileSync, readFileSync as readFileSync2, readdirSync, mkdirSync, existsSync as existsSync2 } from "node:fs";
1691
- import { resolve as resolve2, dirname, relative } from "node:path";
1689
+ import { spawn as spawn2 } from "node:child_process";
1690
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
1691
+ import { resolve as resolve3, dirname as dirname2, relative } from "node:path";
1692
1692
 
1693
- // packages/daemon/src/LLMExecutor.ts
1694
- var AGENT_TOOLS = [
1695
- {
1696
- name: "shell_exec",
1697
- description: "Execute a shell command in the working directory. Use for running servers (with & for background), installing packages, running tests, git operations, etc. Returns stdout + stderr.",
1693
+ // packages/daemon/src/capabilities/WebSearchCapability.ts
1694
+ import { execSync, spawnSync } from "node:child_process";
1695
+ var WebSearchCapability = class {
1696
+ name = "web_search";
1697
+ description = "Search the web. Returns titles, URLs, and snippets. No API key needed.";
1698
+ toolDefinition = {
1699
+ name: "web_search",
1700
+ description: "Search the web and return titles, URLs, and snippets. No API key needed. Use first to find pages, then scrape_url for full content.",
1698
1701
  input_schema: {
1699
1702
  type: "object",
1700
1703
  properties: {
1701
- command: { type: "string", description: "Shell command to execute" },
1702
- timeout_ms: { type: "number", description: "Timeout in milliseconds (default 30000)" }
1704
+ query: { type: "string", description: "Search query" },
1705
+ num_results: { type: "number", description: "Number of results (default 5, max 10)" }
1703
1706
  },
1704
- required: ["command"]
1707
+ required: ["query"]
1705
1708
  }
1706
- },
1707
- {
1708
- name: "write_file",
1709
- description: "Write content to a file. Creates parent directories if needed. Use for creating HTML, CSS, JS, config files, etc.",
1710
- input_schema: {
1711
- type: "object",
1712
- properties: {
1713
- path: { type: "string", description: "File path relative to working directory" },
1714
- content: { type: "string", description: "Full file content to write" }
1709
+ };
1710
+ async execute(input, _cwd) {
1711
+ const query = String(input.query ?? "");
1712
+ const n = Math.min(10, Number(input.num_results ?? 5));
1713
+ const start = Date.now();
1714
+ try {
1715
+ const output = await this.ddgHtml(query, n);
1716
+ if (output && output.length > 50) {
1717
+ return { success: true, output, duration_ms: Date.now() - start };
1718
+ }
1719
+ } catch {
1720
+ }
1721
+ try {
1722
+ const output = await this.browserSearch(query, n);
1723
+ return { success: true, output, fallback_used: "browser", duration_ms: Date.now() - start };
1724
+ } catch (err) {
1725
+ return {
1726
+ success: false,
1727
+ output: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
1728
+ duration_ms: Date.now() - start
1729
+ };
1730
+ }
1731
+ }
1732
+ async ddgHtml(query, n) {
1733
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
1734
+ const res = await fetch(url, {
1735
+ headers: {
1736
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0 Safari/537.36",
1737
+ "Accept": "text/html",
1738
+ "Accept-Language": "en-US,en;q=0.9"
1715
1739
  },
1716
- required: ["path", "content"]
1740
+ signal: AbortSignal.timeout(1e4)
1741
+ });
1742
+ const html = await res.text();
1743
+ const titleRe = /<a[^>]+class="result__a"[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
1744
+ const snippetRe = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
1745
+ const titles = [];
1746
+ const snippets = [];
1747
+ let m;
1748
+ while ((m = titleRe.exec(html)) !== null && titles.length < n) {
1749
+ let href = m[1];
1750
+ const title = m[2].replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").trim();
1751
+ const uddg = href.match(/[?&]uddg=([^&]+)/);
1752
+ if (uddg) href = decodeURIComponent(uddg[1]);
1753
+ if (href.startsWith("http") && title) titles.push({ url: href, title });
1754
+ }
1755
+ while ((m = snippetRe.exec(html)) !== null && snippets.length < n) {
1756
+ snippets.push(m[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
1757
+ }
1758
+ if (titles.length === 0) throw new Error("No results parsed from DDG");
1759
+ return titles.map(
1760
+ (t, i) => `${i + 1}. ${t.title}
1761
+ URL: ${t.url}${snippets[i] ? `
1762
+ ${snippets[i]}` : ""}`
1763
+ ).join("\n\n");
1764
+ }
1765
+ async browserSearch(query, n) {
1766
+ const searchUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
1767
+ try {
1768
+ const result = spawnSync("node", ["-e", `
1769
+ const { chromium } = require('playwright');
1770
+ (async () => {
1771
+ const b = await chromium.launch({ headless: true });
1772
+ const p = await b.newPage();
1773
+ await p.goto('${searchUrl}', { timeout: 15000 });
1774
+ await p.waitForSelector('[data-result]', { timeout: 8000 }).catch(() => {});
1775
+ const results = await p.$$eval('[data-result]', els =>
1776
+ els.slice(0, ${n}).map(el => ({
1777
+ title: el.querySelector('h2')?.innerText ?? '',
1778
+ url: el.querySelector('a')?.href ?? '',
1779
+ snippet: el.querySelector('[data-result="snippet"]')?.innerText ?? ''
1780
+ }))
1781
+ );
1782
+ await b.close();
1783
+ console.log(JSON.stringify(results));
1784
+ })();
1785
+ `], { timeout: 25e3, encoding: "utf8" });
1786
+ if (result.status === 0 && result.stdout) {
1787
+ const results = JSON.parse(result.stdout);
1788
+ return results.map(
1789
+ (r, i) => `${i + 1}. ${r.title}
1790
+ URL: ${r.url}${r.snippet ? `
1791
+ ${r.snippet}` : ""}`
1792
+ ).join("\n\n");
1793
+ }
1794
+ } catch {
1795
+ }
1796
+ const chrome = this.findChrome();
1797
+ if (chrome) {
1798
+ const result = spawnSync(chrome, [
1799
+ "--headless",
1800
+ "--no-sandbox",
1801
+ "--disable-gpu",
1802
+ `--dump-dom`,
1803
+ searchUrl
1804
+ ], { timeout: 15e3, encoding: "utf8" });
1805
+ if (result.stdout) {
1806
+ const html = result.stdout;
1807
+ const titles = [...html.matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>/g)].map((m) => m[1].replace(/<[^>]+>/g, "").trim()).filter((t) => t.length > 5).slice(0, n);
1808
+ if (titles.length > 0) return titles.map((t, i) => `${i + 1}. ${t}`).join("\n");
1809
+ }
1810
+ }
1811
+ throw new Error("No browser available for fallback search");
1812
+ }
1813
+ findChrome() {
1814
+ const candidates = [
1815
+ "google-chrome",
1816
+ "google-chrome-stable",
1817
+ "chromium-browser",
1818
+ "chromium",
1819
+ "/usr/bin/google-chrome",
1820
+ "/usr/bin/chromium-browser",
1821
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
1822
+ ];
1823
+ for (const c of candidates) {
1824
+ try {
1825
+ execSync(`which "${c}" 2>/dev/null || test -f "${c}"`, { stdio: "pipe" });
1826
+ return c;
1827
+ } catch {
1828
+ }
1717
1829
  }
1718
- },
1719
- {
1720
- name: "read_file",
1721
- description: "Read a file's contents.",
1830
+ return null;
1831
+ }
1832
+ };
1833
+
1834
+ // packages/daemon/src/capabilities/BrowserCapability.ts
1835
+ import { spawnSync as spawnSync2, execSync as execSync2 } from "node:child_process";
1836
+ var BrowserCapability = class {
1837
+ name = "browser_open";
1838
+ description = "Open a URL in a real browser. Returns page content, can take screenshots. Use when scrape_url fails on JS-heavy pages.";
1839
+ toolDefinition = {
1840
+ name: "browser_open",
1841
+ description: "Open a URL in a real headless browser. Handles JavaScript-rendered pages, SPAs, login flows. Use when scrape_url fails.",
1722
1842
  input_schema: {
1723
1843
  type: "object",
1724
1844
  properties: {
1725
- path: { type: "string", description: "File path relative to working directory" }
1845
+ url: { type: "string", description: "URL to open" },
1846
+ action: { type: "string", description: 'What to do: "read" (default), "screenshot", "click <selector>", "fill <selector> <value>"' },
1847
+ wait_for: { type: "string", description: "CSS selector to wait for before extracting content" },
1848
+ extract: { type: "string", description: "CSS selector to extract specific element text" }
1726
1849
  },
1727
- required: ["path"]
1850
+ required: ["url"]
1728
1851
  }
1729
- },
1730
- {
1731
- name: "list_dir",
1732
- description: "List files and directories.",
1733
- input_schema: {
1734
- type: "object",
1735
- properties: {
1736
- path: { type: "string", description: 'Directory path relative to working directory (default: ".")' }
1852
+ };
1853
+ async execute(input, _cwd) {
1854
+ const url = String(input.url ?? "");
1855
+ const action = String(input.action ?? "read");
1856
+ const waitFor = input.wait_for ? String(input.wait_for) : null;
1857
+ const extract = input.extract ? String(input.extract) : null;
1858
+ const start = Date.now();
1859
+ if (!url.startsWith("http")) {
1860
+ return { success: false, output: "URL must start with http:// or https://", duration_ms: 0 };
1861
+ }
1862
+ try {
1863
+ const output = await this.playwrightFetch(url, action, waitFor, extract);
1864
+ return { success: true, output, duration_ms: Date.now() - start };
1865
+ } catch {
1866
+ }
1867
+ try {
1868
+ const output = await this.chromeFetch(url);
1869
+ return { success: true, output, fallback_used: "system-chrome", duration_ms: Date.now() - start };
1870
+ } catch {
1871
+ }
1872
+ try {
1873
+ const res = await fetch(url, {
1874
+ headers: {
1875
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0 Safari/537.36",
1876
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1877
+ "Accept-Language": "en-US,en;q=0.9",
1878
+ "Accept-Encoding": "gzip, deflate, br"
1879
+ },
1880
+ signal: AbortSignal.timeout(15e3)
1881
+ });
1882
+ const text = await res.text();
1883
+ const plain = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 5e3);
1884
+ return { success: true, output: plain, fallback_used: "fetch", duration_ms: Date.now() - start };
1885
+ } catch (err) {
1886
+ return {
1887
+ success: false,
1888
+ output: `Browser unavailable. Install Playwright: npx playwright install chromium`,
1889
+ error: err instanceof Error ? err.message : String(err),
1890
+ duration_ms: Date.now() - start
1891
+ };
1892
+ }
1893
+ }
1894
+ async playwrightFetch(url, action, waitFor, extract) {
1895
+ const waitLine = waitFor ? `await p.waitForSelector('${waitFor}', { timeout: 8000 }).catch(() => {});` : "";
1896
+ const extractLine = extract ? `const text = await p.$eval('${extract}', el => el.innerText).catch(() => page_text);` : `const text = page_text;`;
1897
+ const script = `
1898
+ const { chromium } = require('playwright');
1899
+ (async () => {
1900
+ const b = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
1901
+ const p = await b.newPage();
1902
+ await p.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36');
1903
+ await p.goto('${url}', { waitUntil: 'domcontentloaded', timeout: 20000 });
1904
+ ${waitLine}
1905
+ const page_text = await p.evaluate(() => document.body.innerText ?? '');
1906
+ ${extractLine}
1907
+ console.log(text.slice(0, 6000));
1908
+ await b.close();
1909
+ })().catch(e => { console.error(e.message); process.exit(1); });
1910
+ `;
1911
+ const result = spawnSync2("node", ["-e", script], { timeout: 3e4, encoding: "utf8" });
1912
+ if (result.status !== 0) throw new Error(result.stderr || "Playwright failed");
1913
+ return result.stdout.trim();
1914
+ }
1915
+ async chromeFetch(url) {
1916
+ const candidates = [
1917
+ "google-chrome",
1918
+ "chromium-browser",
1919
+ "chromium",
1920
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
1921
+ ];
1922
+ for (const chrome of candidates) {
1923
+ try {
1924
+ execSync2(`which "${chrome}" 2>/dev/null || test -f "${chrome}"`, { stdio: "pipe" });
1925
+ const result = spawnSync2(chrome, ["--headless", "--no-sandbox", "--disable-gpu", "--dump-dom", url], {
1926
+ timeout: 15e3,
1927
+ encoding: "utf8"
1928
+ });
1929
+ if (result.status === 0 && result.stdout) {
1930
+ return result.stdout.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 5e3);
1931
+ }
1932
+ } catch {
1737
1933
  }
1738
1934
  }
1739
- },
1740
- {
1935
+ throw new Error("No system Chrome found");
1936
+ }
1937
+ };
1938
+
1939
+ // packages/daemon/src/capabilities/ScraperCapability.ts
1940
+ import { spawnSync as spawnSync3 } from "node:child_process";
1941
+ var ScraperCapability = class {
1942
+ name = "scrape_url";
1943
+ description = "Scrape a URL. Tries fast HTTP fetch, then Scrapling, then browser if needed.";
1944
+ browser = new BrowserCapability();
1945
+ toolDefinition = {
1741
1946
  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.",
1947
+ description: "Scrape a URL and return clean content. Handles JS-rendered pages. Fallback chain: HTTP \u2192 Scrapling \u2192 Browser.",
1743
1948
  input_schema: {
1744
1949
  type: "object",
1745
1950
  properties: {
1746
1951
  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)" }
1952
+ mode: { type: "string", description: '"text" (default), "links", "tables", "markdown"' },
1953
+ selector: { type: "string", description: "Optional CSS selector to target element" }
1750
1954
  },
1751
1955
  required: ["url"]
1752
1956
  }
1957
+ };
1958
+ async execute(input, cwd) {
1959
+ const url = String(input.url ?? "");
1960
+ const mode = String(input.mode ?? "text");
1961
+ const selector = input.selector ? String(input.selector) : null;
1962
+ const start = Date.now();
1963
+ if (!url.startsWith("http")) {
1964
+ return { success: false, output: "URL must start with http:// or https://", duration_ms: 0 };
1965
+ }
1966
+ try {
1967
+ const output = await this.plainFetch(url, mode, selector);
1968
+ if (output && output.length > 100) {
1969
+ return { success: true, output, duration_ms: Date.now() - start };
1970
+ }
1971
+ } catch {
1972
+ }
1973
+ try {
1974
+ const output = await this.scraplingFetch(url, mode);
1975
+ if (output && output.length > 100) {
1976
+ return { success: true, output, fallback_used: "scrapling", duration_ms: Date.now() - start };
1977
+ }
1978
+ } catch {
1979
+ }
1980
+ const browserResult = await this.browser.execute({ url, extract: selector ?? void 0 }, cwd);
1981
+ return { ...browserResult, fallback_used: "browser", duration_ms: Date.now() - start };
1753
1982
  }
1754
- ];
1755
- var LLMExecutor = class {
1756
- constructor(config) {
1757
- this.config = config;
1758
- }
1759
- get isConfigured() {
1760
- if (this.config.provider === "ollama") return true;
1761
- return !!this.config.api_key?.trim();
1762
- }
1763
- // ─── Single completion (no tools, no streaming) ──────────────────────────
1764
- async complete(messages, system) {
1765
- const res = await this.completeWithTools(messages, [], system, void 0);
1766
- return { content: res.content, tokens_used: res.tokens_used, model: res.model };
1983
+ async plainFetch(url, mode, selector) {
1984
+ const res = await fetch(url, {
1985
+ headers: { "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/120.0" },
1986
+ signal: AbortSignal.timeout(12e3)
1987
+ });
1988
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1989
+ const html = await res.text();
1990
+ if (mode === "links") {
1991
+ const links = [...html.matchAll(/href="(https?:\/\/[^"]+)"/g)].map((m) => m[1]).filter((v, i, a) => a.indexOf(v) === i).slice(0, 30);
1992
+ return links.join("\n");
1993
+ }
1994
+ if (mode === "tables") {
1995
+ const tables = [...html.matchAll(/<table[\s\S]*?<\/table>/gi)].map(
1996
+ (m) => m[0].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
1997
+ ).slice(0, 5);
1998
+ return tables.join("\n---\n");
1999
+ }
2000
+ const text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
2001
+ return text.slice(0, 6e3) + (text.length > 6e3 ? "\n\u2026[truncated]" : "");
2002
+ }
2003
+ async scraplingFetch(url, mode) {
2004
+ const extractLine = mode === "links" ? `result = [a.attrib.get('href','') for a in page.find_all('a') if a.attrib.get('href','').startswith('http')]` : `result = page.get_all_text()`;
2005
+ const script = `
2006
+ import sys
2007
+ try:
2008
+ from scrapling import Fetcher
2009
+ except ImportError:
2010
+ import subprocess
2011
+ subprocess.run([sys.executable,'-m','pip','install','scrapling','-q'],check=True)
2012
+ from scrapling import Fetcher
2013
+ f = Fetcher(auto_match=False)
2014
+ page = f.get('${url}', timeout=20)
2015
+ ${extractLine}
2016
+ if isinstance(result, list):
2017
+ print('\\n'.join(str(r) for r in result[:30]))
2018
+ else:
2019
+ t = str(result).strip()
2020
+ print(t[:5000] + ('...' if len(t)>5000 else ''))
2021
+ `.trim();
2022
+ const result = spawnSync3("python3", ["-c", script], { timeout: 35e3, encoding: "utf8" });
2023
+ if (result.status !== 0) throw new Error(result.stderr || "Scrapling failed");
2024
+ return result.stdout.trim();
1767
2025
  }
1768
- // ─── Tool-calling completion with optional streaming ─────────────────────
1769
- async completeWithTools(messages, tools, system, onToken) {
1770
- switch (this.config.provider) {
1771
- case "anthropic":
1772
- return this.anthropic(messages, tools, system, onToken);
1773
- case "openai":
1774
- return this.openai(messages, tools, system, onToken);
1775
- case "xai":
1776
- return this.openai(messages, tools, system, onToken, "https://api.x.ai/v1");
1777
- case "gemini":
1778
- return this.openai(messages, tools, system, onToken, "https://generativelanguage.googleapis.com/v1beta/openai");
1779
- case "ollama":
1780
- return this.ollama(messages, system, onToken);
1781
- default:
1782
- return this.openai(messages, tools, system, onToken);
2026
+ };
2027
+
2028
+ // packages/daemon/src/capabilities/ShellCapability.ts
2029
+ import { spawn } from "node:child_process";
2030
+ var ShellCapability = class {
2031
+ name = "shell_exec";
2032
+ description = "Execute shell commands in the working directory.";
2033
+ toolDefinition = {
2034
+ name: "shell_exec",
2035
+ description: "Execute a shell command. Use & for background processes. Returns stdout+stderr.",
2036
+ input_schema: {
2037
+ type: "object",
2038
+ properties: {
2039
+ command: { type: "string", description: "Shell command to execute" },
2040
+ timeout_ms: { type: "number", description: "Timeout (default 30000ms)" }
2041
+ },
2042
+ required: ["command"]
1783
2043
  }
2044
+ };
2045
+ async execute(input, cwd) {
2046
+ const command = String(input.command ?? "");
2047
+ const timeout = Number(input.timeout_ms ?? 3e4);
2048
+ const start = Date.now();
2049
+ return new Promise((resolve_) => {
2050
+ const chunks = [];
2051
+ const proc = spawn("bash", ["-c", command], {
2052
+ cwd,
2053
+ env: { ...process.env, TERM: "dumb" },
2054
+ timeout
2055
+ });
2056
+ proc.stdout.on("data", (d) => chunks.push(d.toString()));
2057
+ proc.stderr.on("data", (d) => chunks.push(d.toString()));
2058
+ proc.on("close", (code) => {
2059
+ const output = chunks.join("").trim();
2060
+ resolve_({
2061
+ success: code === 0,
2062
+ output: output || (code === 0 ? "(no output)" : `exit ${code}`),
2063
+ duration_ms: Date.now() - start,
2064
+ ...code !== 0 && { error: `exit code ${code}` }
2065
+ });
2066
+ });
2067
+ proc.on("error", (err) => {
2068
+ resolve_({ success: false, output: err.message, error: err.message, duration_ms: Date.now() - start });
2069
+ });
2070
+ });
1784
2071
  }
1785
- // ─── Anthropic ───────────────────────────────────────────────────────────
1786
- async anthropic(messages, tools, system, onToken) {
1787
- const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
1788
- const filtered = messages.filter((m) => m.role !== "system");
1789
- const anthropicMsgs = filtered.map((m) => {
1790
- if (m.role === "tool") {
2072
+ };
2073
+
2074
+ // packages/daemon/src/capabilities/FileCapability.ts
2075
+ import { readFileSync as readFileSync2, writeFileSync, readdirSync, mkdirSync, existsSync as existsSync2 } from "node:fs";
2076
+ import { resolve as resolve2, dirname } from "node:path";
2077
+ var FileCapability = class {
2078
+ name = "file_op";
2079
+ description = "Read, write, or list files. Scoped to working directory.";
2080
+ toolDefinition = {
2081
+ name: "file_op",
2082
+ description: "Read, write, or list files in the working directory.",
2083
+ input_schema: {
2084
+ type: "object",
2085
+ properties: {
2086
+ op: { type: "string", description: '"read", "write", or "list"' },
2087
+ path: { type: "string", description: "File or directory path (relative to cwd)" },
2088
+ content: { type: "string", description: "Content for write operation" }
2089
+ },
2090
+ required: ["op", "path"]
2091
+ }
2092
+ };
2093
+ async execute(input, cwd) {
2094
+ const op = String(input.op ?? "read");
2095
+ const rel = String(input.path ?? ".");
2096
+ const safe = resolve2(cwd, rel);
2097
+ const start = Date.now();
2098
+ if (!safe.startsWith(cwd)) {
2099
+ return { success: false, output: "Path outside working directory", duration_ms: 0 };
2100
+ }
2101
+ try {
2102
+ if (op === "read") {
2103
+ if (!existsSync2(safe)) return { success: false, output: `Not found: ${rel}`, duration_ms: Date.now() - start };
2104
+ const content = readFileSync2(safe, "utf8");
1791
2105
  return {
1792
- role: "user",
1793
- content: [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content }]
2106
+ success: true,
2107
+ output: content.length > 8e3 ? content.slice(0, 8e3) + "\n\u2026[truncated]" : content,
2108
+ duration_ms: Date.now() - start
1794
2109
  };
1795
2110
  }
1796
- if (m.role === "assistant" && m.tool_calls?.length) {
1797
- return {
1798
- role: "assistant",
1799
- content: [
1800
- ...m.content ? [{ type: "text", text: m.content }] : [],
1801
- ...m.tool_calls.map((tc) => ({
1802
- type: "tool_use",
1803
- id: tc.id,
1804
- name: tc.name,
1805
- input: tc.input
1806
- }))
1807
- ]
1808
- };
2111
+ if (op === "write") {
2112
+ mkdirSync(dirname(safe), { recursive: true });
2113
+ writeFileSync(safe, String(input.content ?? ""), "utf8");
2114
+ return { success: true, output: `Written: ${rel} (${String(input.content ?? "").length} bytes)`, duration_ms: Date.now() - start };
1809
2115
  }
1810
- return { role: m.role, content: m.content };
1811
- });
1812
- const body = {
1813
- model: this.config.model,
1814
- max_tokens: 8192,
1815
- messages: anthropicMsgs,
1816
- stream: true
1817
- };
1818
- if (sysContent) body.system = sysContent;
1819
- if (tools.length > 0) {
1820
- body.tools = tools.map((t) => ({
1821
- name: t.name,
1822
- description: t.description,
1823
- input_schema: t.input_schema
1824
- }));
1825
- }
1826
- const res = await fetch("https://api.anthropic.com/v1/messages", {
1827
- method: "POST",
1828
- headers: {
1829
- "Content-Type": "application/json",
1830
- "x-api-key": this.config.api_key,
1831
- "anthropic-version": "2023-06-01"
1832
- },
1833
- body: JSON.stringify(body)
1834
- });
1835
- if (!res.ok) {
1836
- const err = await res.text();
1837
- throw new Error(`Anthropic ${res.status}: ${err}`);
1838
- }
1839
- let textContent = "";
1840
- let stopReason = "end_turn";
1841
- let inputTokens = 0;
1842
- let outputTokens = 0;
1843
- let modelName = this.config.model;
1844
- const toolCalls = [];
1845
- const toolInputBuffers = {};
1846
- let currentToolId = "";
1847
- const reader = res.body.getReader();
1848
- const decoder = new TextDecoder();
1849
- let buf = "";
1850
- while (true) {
1851
- const { done, value } = await reader.read();
1852
- if (done) break;
1853
- buf += decoder.decode(value, { stream: true });
1854
- const lines = buf.split("\n");
1855
- buf = lines.pop() ?? "";
1856
- for (const line of lines) {
1857
- if (!line.startsWith("data: ")) continue;
1858
- const data = line.slice(6).trim();
1859
- if (data === "[DONE]" || data === "") continue;
1860
- let evt;
1861
- try {
1862
- evt = JSON.parse(data);
1863
- } catch {
1864
- continue;
1865
- }
1866
- const type = evt.type;
1867
- if (type === "message_start") {
1868
- const usage = evt.message?.usage;
1869
- inputTokens = usage?.input_tokens ?? 0;
1870
- modelName = evt.message?.model ?? modelName;
1871
- } else if (type === "content_block_start") {
1872
- const block = evt.content_block;
1873
- if (block?.type === "tool_use") {
1874
- currentToolId = block.id;
1875
- toolInputBuffers[currentToolId] = "";
1876
- toolCalls.push({ id: currentToolId, name: block.name, input: {} });
1877
- }
1878
- } else if (type === "content_block_delta") {
1879
- const delta = evt.delta;
1880
- if (delta?.type === "text_delta") {
1881
- const token = delta.text ?? "";
1882
- textContent += token;
1883
- if (onToken && token) onToken(token);
1884
- } else if (delta?.type === "input_json_delta") {
1885
- toolInputBuffers[currentToolId] = (toolInputBuffers[currentToolId] ?? "") + (delta.partial_json ?? "");
1886
- }
1887
- } else if (type === "content_block_stop") {
1888
- if (currentToolId && toolInputBuffers[currentToolId]) {
1889
- const tc = toolCalls.find((t) => t.id === currentToolId);
1890
- if (tc) {
1891
- try {
1892
- tc.input = JSON.parse(toolInputBuffers[currentToolId]);
1893
- } catch {
1894
- }
1895
- }
1896
- }
1897
- } else if (type === "message_delta") {
1898
- const usage = evt.usage;
1899
- outputTokens = usage?.output_tokens ?? 0;
1900
- const stop = evt.delta?.stop_reason;
1901
- if (stop === "tool_use") stopReason = "tool_use";
1902
- else if (stop === "end_turn") stopReason = "end_turn";
1903
- else if (stop === "max_tokens") stopReason = "max_tokens";
1904
- }
2116
+ if (op === "list") {
2117
+ if (!existsSync2(safe)) return { success: false, output: `Not found: ${rel}`, duration_ms: Date.now() - start };
2118
+ const entries = readdirSync(safe, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => `${e.isDirectory() ? "d" : "f"} ${e.name}`).join("\n");
2119
+ return { success: true, output: entries || "(empty)", duration_ms: Date.now() - start };
1905
2120
  }
2121
+ return { success: false, output: `Unknown op: ${op}`, duration_ms: Date.now() - start };
2122
+ } catch (err) {
2123
+ return { success: false, output: `Error: ${err instanceof Error ? err.message : String(err)}`, duration_ms: Date.now() - start };
1906
2124
  }
1907
- return {
1908
- content: textContent,
1909
- tool_calls: toolCalls.length > 0 ? toolCalls : null,
1910
- stop_reason: stopReason,
1911
- tokens_used: inputTokens + outputTokens,
1912
- model: modelName
1913
- };
1914
2125
  }
1915
- // ─── OpenAI (also xAI, Gemini) ───────────────────────────────────────────
1916
- async openai(messages, tools, system, onToken, baseUrl = "https://api.openai.com/v1") {
1917
- const allMessages = [];
1918
- const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
1919
- if (sysContent) allMessages.push({ role: "system", content: sysContent });
1920
- for (const m of messages.filter((m2) => m2.role !== "system")) {
1921
- if (m.role === "tool") {
1922
- allMessages.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content });
1923
- } else if (m.role === "assistant" && m.tool_calls?.length) {
1924
- allMessages.push({
1925
- role: "assistant",
1926
- content: m.content || null,
1927
- tool_calls: m.tool_calls.map((tc) => ({
1928
- id: tc.id,
1929
- type: "function",
1930
- function: { name: tc.name, arguments: JSON.stringify(tc.input) }
1931
- }))
1932
- });
1933
- } else {
1934
- allMessages.push({ role: m.role, content: m.content });
1935
- }
1936
- }
1937
- const body = {
1938
- model: this.config.model,
1939
- messages: allMessages,
1940
- max_tokens: 8192,
1941
- stream: true,
1942
- stream_options: { include_usage: true }
1943
- };
1944
- if (tools.length > 0) {
1945
- body.tools = tools.map((t) => ({
1946
- type: "function",
1947
- function: { name: t.name, description: t.description, parameters: t.input_schema }
1948
- }));
1949
- }
1950
- const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
1951
- method: "POST",
1952
- headers: {
1953
- "Content-Type": "application/json",
1954
- "Authorization": `Bearer ${this.config.api_key}`
1955
- },
1956
- body: JSON.stringify(body)
1957
- });
1958
- if (!res.ok) {
1959
- const err = await res.text();
1960
- throw new Error(`OpenAI ${res.status}: ${err}`);
2126
+ };
2127
+
2128
+ // packages/daemon/src/capabilities/CapabilityRegistry.ts
2129
+ var CapabilityRegistry = class {
2130
+ capabilities = /* @__PURE__ */ new Map();
2131
+ constructor() {
2132
+ this.register(new WebSearchCapability());
2133
+ this.register(new BrowserCapability());
2134
+ this.register(new ScraperCapability());
2135
+ this.register(new ShellCapability());
2136
+ this.register(new FileCapability());
2137
+ }
2138
+ register(cap) {
2139
+ this.capabilities.set(cap.name, cap);
2140
+ }
2141
+ get(name) {
2142
+ return this.capabilities.get(name);
2143
+ }
2144
+ getToolDefinitions() {
2145
+ return [...this.capabilities.values()].map((c) => c.toolDefinition);
2146
+ }
2147
+ async execute(toolName, input, cwd) {
2148
+ const cap = this.capabilities.get(toolName);
2149
+ if (!cap) {
2150
+ return { success: false, output: `Unknown capability: ${toolName}`, duration_ms: 0 };
1961
2151
  }
1962
- let textContent = "";
1963
- let tokensUsed = 0;
1964
- let modelName = this.config.model;
1965
- let stopReason = "end_turn";
1966
- const toolCallMap = {};
1967
- const reader = res.body.getReader();
1968
- const decoder = new TextDecoder();
1969
- let buf = "";
1970
- while (true) {
1971
- const { done, value } = await reader.read();
1972
- if (done) break;
1973
- buf += decoder.decode(value, { stream: true });
1974
- const lines = buf.split("\n");
1975
- buf = lines.pop() ?? "";
1976
- for (const line of lines) {
1977
- if (!line.startsWith("data: ")) continue;
1978
- const data = line.slice(6).trim();
1979
- if (data === "[DONE]") continue;
1980
- let evt;
1981
- try {
1982
- evt = JSON.parse(data);
1983
- } catch {
1984
- continue;
1985
- }
1986
- modelName = evt.model ?? modelName;
1987
- const usage = evt.usage;
1988
- if (usage?.total_tokens) tokensUsed = usage.total_tokens;
1989
- const choices = evt.choices;
1990
- if (!choices?.length) continue;
1991
- const delta = choices[0].delta;
1992
- if (!delta) continue;
1993
- const finish = choices[0].finish_reason;
1994
- if (finish === "tool_calls") stopReason = "tool_use";
1995
- else if (finish === "stop") stopReason = "end_turn";
1996
- const token = delta.content;
1997
- if (token) {
1998
- textContent += token;
1999
- if (onToken) onToken(token);
2000
- }
2001
- const toolCallDeltas = delta.tool_calls;
2002
- if (toolCallDeltas) {
2003
- for (const tc of toolCallDeltas) {
2004
- const idx = tc.index;
2005
- if (!toolCallMap[idx]) {
2006
- toolCallMap[idx] = { id: "", name: "", args: "" };
2007
- }
2008
- const fn = tc.function;
2009
- if (tc.id) toolCallMap[idx].id = tc.id;
2010
- if (fn?.name) toolCallMap[idx].name = fn.name;
2011
- if (fn?.arguments) toolCallMap[idx].args += fn.arguments;
2012
- }
2013
- }
2014
- }
2152
+ try {
2153
+ return await cap.execute(input, cwd);
2154
+ } catch (err) {
2155
+ return {
2156
+ success: false,
2157
+ output: `Capability error: ${err instanceof Error ? err.message : String(err)}`,
2158
+ duration_ms: 0
2159
+ };
2015
2160
  }
2016
- const toolCalls = Object.values(toolCallMap).filter((tc) => tc.id && tc.name).map((tc) => {
2017
- let input = {};
2018
- try {
2019
- input = JSON.parse(tc.args);
2020
- } catch {
2021
- }
2022
- return { id: tc.id, name: tc.name, input };
2023
- });
2024
- return {
2025
- content: textContent,
2026
- tool_calls: toolCalls.length > 0 ? toolCalls : null,
2027
- stop_reason: stopReason,
2028
- tokens_used: tokensUsed,
2029
- model: modelName
2030
- };
2031
2161
  }
2032
- // ─── Ollama (no streaming for simplicity) ────────────────────────────────
2033
- async ollama(messages, system, onToken) {
2034
- const baseUrl = this.config.base_url ?? "http://localhost:11434";
2035
- const allMessages = [];
2036
- const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
2037
- if (sysContent) allMessages.push({ role: "system", content: sysContent });
2038
- allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
2039
- const res = await fetch(`${baseUrl}/api/chat`, {
2040
- method: "POST",
2041
- headers: { "Content-Type": "application/json" },
2042
- body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
2043
- });
2044
- if (!res.ok) throw new Error(`Ollama error ${res.status}`);
2045
- const data = await res.json();
2046
- if (onToken) onToken(data.message.content);
2047
- return { content: data.message.content, tool_calls: null, stop_reason: "end_turn", tokens_used: data.eval_count ?? 0, model: this.config.model };
2162
+ list() {
2163
+ return [...this.capabilities.values()].map((c) => ({ name: c.name, description: c.description }));
2048
2164
  }
2049
2165
  };
2050
2166
 
@@ -2058,10 +2174,12 @@ var AgentExecutor = class {
2058
2174
  this.cwd = config.cwd;
2059
2175
  this.maxIterations = config.max_iterations ?? 20;
2060
2176
  this.maxCommandMs = config.max_command_ms ?? 3e4;
2177
+ this.registry = new CapabilityRegistry();
2061
2178
  }
2062
2179
  cwd;
2063
2180
  maxIterations;
2064
2181
  maxCommandMs;
2182
+ registry;
2065
2183
  async execute(task, systemContext) {
2066
2184
  const filesWritten = [];
2067
2185
  const commandsRun = [];
@@ -2078,7 +2196,7 @@ var AgentExecutor = class {
2078
2196
  try {
2079
2197
  response = await this.llm.completeWithTools(
2080
2198
  messages,
2081
- AGENT_TOOLS,
2199
+ this.registry.getToolDefinitions(),
2082
2200
  systemPrompt,
2083
2201
  // Only stream tokens on the final (non-tool) turn
2084
2202
  (token) => {
@@ -2108,8 +2226,12 @@ var AgentExecutor = class {
2108
2226
  this.onStep(`\u25B6 ${tc.name}(${this.summariseInput(tc.name, tc.input)})`);
2109
2227
  let result;
2110
2228
  try {
2111
- result = await this.executeTool(tc.name, tc.input);
2112
- if (tc.name === "write_file" && tc.input.path) {
2229
+ const capResult = await this.registry.execute(tc.name, tc.input, this.cwd);
2230
+ result = capResult.output;
2231
+ if (capResult.fallback_used) {
2232
+ this.onStep(` (used fallback: ${capResult.fallback_used})`);
2233
+ }
2234
+ if (tc.name === "file_op" && tc.input.op === "write" && tc.input.path) {
2113
2235
  filesWritten.push(String(tc.input.path));
2114
2236
  }
2115
2237
  if (tc.name === "shell_exec" && tc.input.command) {
@@ -2149,6 +2271,11 @@ var AgentExecutor = class {
2149
2271
  return this.readFile(String(input.path ?? ""));
2150
2272
  case "list_dir":
2151
2273
  return this.listDir(input.path ? String(input.path) : void 0);
2274
+ case "web_search":
2275
+ return this.webSearch(
2276
+ String(input.query ?? ""),
2277
+ Math.min(10, Number(input.num_results ?? 5))
2278
+ );
2152
2279
  case "scrape_url":
2153
2280
  return this.scrapeUrl(
2154
2281
  String(input.url ?? ""),
@@ -2161,9 +2288,9 @@ var AgentExecutor = class {
2161
2288
  }
2162
2289
  }
2163
2290
  shellExec(command, timeoutMs) {
2164
- return new Promise((resolve6) => {
2291
+ return new Promise((resolve7) => {
2165
2292
  const chunks = [];
2166
- const proc = spawn("bash", ["-c", command], {
2293
+ const proc = spawn2("bash", ["-c", command], {
2167
2294
  cwd: this.cwd,
2168
2295
  env: { ...process.env, TERM: "dumb" },
2169
2296
  timeout: timeoutMs
@@ -2172,29 +2299,74 @@ var AgentExecutor = class {
2172
2299
  proc.stderr.on("data", (d) => chunks.push(d.toString()));
2173
2300
  proc.on("close", (code) => {
2174
2301
  const output = chunks.join("").trim();
2175
- resolve6(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
2302
+ resolve7(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
2176
2303
  });
2177
2304
  proc.on("error", (err) => {
2178
- resolve6(`Error: ${err.message}`);
2305
+ resolve7(`Error: ${err.message}`);
2179
2306
  });
2180
2307
  });
2181
2308
  }
2182
2309
  writeFile(filePath, content) {
2183
2310
  const safe = this.safePath(filePath);
2184
2311
  if (!safe) return "Error: path outside working directory";
2185
- mkdirSync(dirname(safe), { recursive: true });
2186
- writeFileSync(safe, content, "utf8");
2312
+ mkdirSync2(dirname2(safe), { recursive: true });
2313
+ writeFileSync2(safe, content, "utf8");
2187
2314
  const rel = relative(this.cwd, safe);
2188
2315
  return `Written: ${rel} (${content.length} bytes)`;
2189
2316
  }
2190
2317
  readFile(filePath) {
2191
2318
  const safe = this.safePath(filePath);
2192
2319
  if (!safe) return "Error: path outside working directory";
2193
- if (!existsSync2(safe)) return `File not found: ${filePath}`;
2194
- const content = readFileSync2(safe, "utf8");
2320
+ if (!existsSync3(safe)) return `File not found: ${filePath}`;
2321
+ const content = readFileSync3(safe, "utf8");
2195
2322
  return content.length > 8e3 ? content.slice(0, 8e3) + `
2196
2323
  \u2026[truncated, ${content.length} total bytes]` : content;
2197
2324
  }
2325
+ async webSearch(query, numResults) {
2326
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
2327
+ let html = "";
2328
+ try {
2329
+ const res = await fetch(url, {
2330
+ headers: {
2331
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
2332
+ "Accept": "text/html,application/xhtml+xml",
2333
+ "Accept-Language": "en-US,en;q=0.9"
2334
+ },
2335
+ signal: AbortSignal.timeout(12e3)
2336
+ });
2337
+ html = await res.text();
2338
+ } catch (err) {
2339
+ return `Search request failed: ${err instanceof Error ? err.message : String(err)}`;
2340
+ }
2341
+ const results = [];
2342
+ const titleRe = /<a[^>]+class="result__a"[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
2343
+ const snippetRe = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
2344
+ const titles = [];
2345
+ const snippets = [];
2346
+ let m;
2347
+ while ((m = titleRe.exec(html)) !== null) {
2348
+ let href = m[1];
2349
+ const title = m[2].replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
2350
+ const uddg = href.match(/[?&]uddg=([^&]+)/);
2351
+ if (uddg) href = decodeURIComponent(uddg[1]);
2352
+ if (href.startsWith("http") && title && titles.length < numResults) {
2353
+ titles.push({ url: href, title });
2354
+ }
2355
+ }
2356
+ while ((m = snippetRe.exec(html)) !== null && snippets.length < numResults) {
2357
+ snippets.push(m[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
2358
+ }
2359
+ if (titles.length === 0) {
2360
+ const plainText = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 1500);
2361
+ return `No results parsed. Raw content:
2362
+ ${plainText}`;
2363
+ }
2364
+ return titles.map(
2365
+ (t, i) => `${i + 1}. ${t.title}
2366
+ URL: ${t.url}${snippets[i] ? `
2367
+ ${snippets[i]}` : ""}`
2368
+ ).join("\n\n");
2369
+ }
2198
2370
  async scrapeUrl(url, mode, selector, waitMs) {
2199
2371
  if (!url.startsWith("http")) return "Error: URL must start with http:// or https://";
2200
2372
  const selectorLine = selector ? `element = page.find('${selector}')
@@ -2237,9 +2409,9 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2237
2409
  listDir(dirPath) {
2238
2410
  const safe = this.safePath(dirPath ?? ".");
2239
2411
  if (!safe) return "Error: path outside working directory";
2240
- if (!existsSync2(safe)) return `Directory not found: ${dirPath}`;
2412
+ if (!existsSync3(safe)) return `Directory not found: ${dirPath}`;
2241
2413
  try {
2242
- const entries = readdirSync(safe, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => `${e.isDirectory() ? "d" : "f"} ${e.name}`).join("\n");
2414
+ const entries = readdirSync2(safe, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => `${e.isDirectory() ? "d" : "f"} ${e.name}`).join("\n");
2243
2415
  return entries || "(empty directory)";
2244
2416
  } catch (e) {
2245
2417
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
@@ -2247,7 +2419,7 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2247
2419
  }
2248
2420
  // ─── Helpers ───────────────────────────────────────────────────────────────
2249
2421
  safePath(p) {
2250
- const resolved = resolve2(this.cwd, p);
2422
+ const resolved = resolve3(this.cwd, p);
2251
2423
  return resolved.startsWith(this.cwd) ? resolved : null;
2252
2424
  }
2253
2425
  buildSystemPrompt(extra) {
@@ -2261,6 +2433,7 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2261
2433
  `- For npm/node projects: check package.json first with read_file or list_dir`,
2262
2434
  `- After write_file, verify with read_file if needed`,
2263
2435
  `- After shell_exec, check output for errors and retry if needed`,
2436
+ `- For research tasks: use web_search first, then scrape_url for full page content`,
2264
2437
  `- Use relative paths from the working directory`,
2265
2438
  `- Be concise in your final response: state what was done and where to find it`
2266
2439
  ];
@@ -2272,6 +2445,7 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
2272
2445
  if (toolName === "write_file") return `"${input.path}"`;
2273
2446
  if (toolName === "read_file") return `"${input.path}"`;
2274
2447
  if (toolName === "list_dir") return `"${input.path ?? "."}"`;
2448
+ if (toolName === "web_search") return `"${String(input.query ?? "").slice(0, 60)}"`;
2275
2449
  if (toolName === "scrape_url") return `"${String(input.url ?? "").slice(0, 60)}" mode=${input.mode ?? "text"}`;
2276
2450
  return JSON.stringify(input).slice(0, 60);
2277
2451
  }
@@ -2730,7 +2904,7 @@ var BackgroundWorkers = class {
2730
2904
  };
2731
2905
 
2732
2906
  // packages/daemon/src/SkillRegistry.ts
2733
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, existsSync as existsSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "node:fs";
2907
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3, existsSync as existsSync4, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "node:fs";
2734
2908
  import { join } from "node:path";
2735
2909
  import { homedir as homedir2 } from "node:os";
2736
2910
  import YAML2 from "yaml";
@@ -2753,11 +2927,11 @@ var SkillRegistry = class {
2753
2927
  this.loadFromDir(this.customDir, false);
2754
2928
  }
2755
2929
  loadFromDir(dir, isBuiltin) {
2756
- if (!existsSync3(dir)) return;
2757
- const files = readdirSync2(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
2930
+ if (!existsSync4(dir)) return;
2931
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
2758
2932
  for (const file of files) {
2759
2933
  try {
2760
- const raw = readFileSync3(join(dir, file), "utf8");
2934
+ const raw = readFileSync4(join(dir, file), "utf8");
2761
2935
  const skill = YAML2.parse(raw);
2762
2936
  if (skill.name) {
2763
2937
  this.skills.set(skill.name, skill);
@@ -2792,9 +2966,9 @@ var SkillRegistry = class {
2792
2966
  if (this.builtinNames.has(name)) {
2793
2967
  throw new Error(`Cannot override built-in skill: ${name}`);
2794
2968
  }
2795
- mkdirSync2(this.customDir, { recursive: true });
2969
+ mkdirSync3(this.customDir, { recursive: true });
2796
2970
  const filePath = join(this.customDir, `${name}.yaml`);
2797
- writeFileSync2(filePath, yamlContent, "utf8");
2971
+ writeFileSync3(filePath, yamlContent, "utf8");
2798
2972
  const skill = YAML2.parse(yamlContent);
2799
2973
  this.skills.set(name, skill);
2800
2974
  return skill;
@@ -2807,7 +2981,7 @@ var SkillRegistry = class {
2807
2981
  throw new Error(`Cannot delete built-in skill: ${name}`);
2808
2982
  }
2809
2983
  const filePath = join(this.customDir, `${name}.yaml`);
2810
- if (existsSync3(filePath)) {
2984
+ if (existsSync4(filePath)) {
2811
2985
  unlinkSync(filePath);
2812
2986
  }
2813
2987
  this.skills.delete(name);
@@ -2820,8 +2994,8 @@ var SkillRegistry = class {
2820
2994
  // packages/daemon/src/HTTPServer.ts
2821
2995
  import { Hono as Hono8 } from "hono";
2822
2996
  import { serve } from "@hono/node-server";
2823
- import { readFileSync as readFileSync4 } from "node:fs";
2824
- import { resolve as resolve3, dirname as dirname2 } from "node:path";
2997
+ import { readFileSync as readFileSync5 } from "node:fs";
2998
+ import { resolve as resolve4, dirname as dirname3 } from "node:path";
2825
2999
  import { fileURLToPath } from "node:url";
2826
3000
 
2827
3001
  // packages/daemon/src/routes/health.ts
@@ -3076,15 +3250,15 @@ function skillRoutes(deps) {
3076
3250
  // packages/daemon/src/HTTPServer.ts
3077
3251
  function findGraphHtml() {
3078
3252
  const candidates = [
3079
- resolve3(dirname2(fileURLToPath(import.meta.url)), "graph.html"),
3253
+ resolve4(dirname3(fileURLToPath(import.meta.url)), "graph.html"),
3080
3254
  // dev (src/)
3081
- resolve3(dirname2(fileURLToPath(import.meta.url)), "..", "graph.html"),
3255
+ resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "graph.html"),
3082
3256
  // bundled (dist/../)
3083
- resolve3(dirname2(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
3257
+ resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
3084
3258
  ];
3085
3259
  for (const p of candidates) {
3086
3260
  try {
3087
- readFileSync4(p);
3261
+ readFileSync5(p);
3088
3262
  return p;
3089
3263
  } catch {
3090
3264
  }
@@ -3108,7 +3282,7 @@ var HTTPServer = class {
3108
3282
  this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
3109
3283
  const serveGraph = (c) => {
3110
3284
  try {
3111
- const html = readFileSync4(GRAPH_HTML_PATH, "utf8");
3285
+ const html = readFileSync5(GRAPH_HTML_PATH, "utf8");
3112
3286
  return c.html(html);
3113
3287
  } catch {
3114
3288
  return c.html("<p>Graph UI not found. Run: pnpm build</p>");
@@ -3118,7 +3292,7 @@ var HTTPServer = class {
3118
3292
  this.app.get("/graph", serveGraph);
3119
3293
  }
3120
3294
  start() {
3121
- return new Promise((resolve6) => {
3295
+ return new Promise((resolve7) => {
3122
3296
  this.server = serve(
3123
3297
  {
3124
3298
  fetch: this.app.fetch,
@@ -3126,20 +3300,20 @@ var HTTPServer = class {
3126
3300
  hostname: this.deps.host
3127
3301
  },
3128
3302
  () => {
3129
- resolve6();
3303
+ resolve7();
3130
3304
  }
3131
3305
  );
3132
3306
  });
3133
3307
  }
3134
3308
  stop() {
3135
- return new Promise((resolve6, reject) => {
3309
+ return new Promise((resolve7, reject) => {
3136
3310
  if (!this.server) {
3137
- resolve6();
3311
+ resolve7();
3138
3312
  return;
3139
3313
  }
3140
3314
  this.server.close((err) => {
3141
3315
  if (err) reject(err);
3142
- else resolve6();
3316
+ else resolve7();
3143
3317
  });
3144
3318
  });
3145
3319
  }
@@ -3148,6 +3322,303 @@ var HTTPServer = class {
3148
3322
  }
3149
3323
  };
3150
3324
 
3325
+ // packages/daemon/src/LLMExecutor.ts
3326
+ var LLMExecutor = class {
3327
+ constructor(config) {
3328
+ this.config = config;
3329
+ }
3330
+ get isConfigured() {
3331
+ if (this.config.provider === "ollama") return true;
3332
+ return !!this.config.api_key?.trim();
3333
+ }
3334
+ // ─── Single completion (no tools, no streaming) ──────────────────────────
3335
+ async complete(messages, system) {
3336
+ const res = await this.completeWithTools(messages, [], system, void 0);
3337
+ return { content: res.content, tokens_used: res.tokens_used, model: res.model };
3338
+ }
3339
+ // ─── Tool-calling completion with optional streaming ─────────────────────
3340
+ async completeWithTools(messages, tools, system, onToken) {
3341
+ switch (this.config.provider) {
3342
+ case "anthropic":
3343
+ return this.anthropic(messages, tools, system, onToken);
3344
+ case "openai":
3345
+ return this.openai(messages, tools, system, onToken);
3346
+ case "xai":
3347
+ return this.openai(messages, tools, system, onToken, "https://api.x.ai/v1");
3348
+ case "gemini":
3349
+ return this.openai(messages, tools, system, onToken, "https://generativelanguage.googleapis.com/v1beta/openai");
3350
+ case "ollama":
3351
+ return this.ollama(messages, system, onToken);
3352
+ default:
3353
+ return this.openai(messages, tools, system, onToken);
3354
+ }
3355
+ }
3356
+ // ─── Anthropic ───────────────────────────────────────────────────────────
3357
+ async anthropic(messages, tools, system, onToken) {
3358
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
3359
+ const filtered = messages.filter((m) => m.role !== "system");
3360
+ const anthropicMsgs = filtered.map((m) => {
3361
+ if (m.role === "tool") {
3362
+ return {
3363
+ role: "user",
3364
+ content: [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content }]
3365
+ };
3366
+ }
3367
+ if (m.role === "assistant" && m.tool_calls?.length) {
3368
+ return {
3369
+ role: "assistant",
3370
+ content: [
3371
+ ...m.content ? [{ type: "text", text: m.content }] : [],
3372
+ ...m.tool_calls.map((tc) => ({
3373
+ type: "tool_use",
3374
+ id: tc.id,
3375
+ name: tc.name,
3376
+ input: tc.input
3377
+ }))
3378
+ ]
3379
+ };
3380
+ }
3381
+ return { role: m.role, content: m.content };
3382
+ });
3383
+ const body = {
3384
+ model: this.config.model,
3385
+ max_tokens: 8192,
3386
+ messages: anthropicMsgs,
3387
+ stream: true
3388
+ };
3389
+ if (sysContent) body.system = sysContent;
3390
+ if (tools.length > 0) {
3391
+ body.tools = tools.map((t) => ({
3392
+ name: t.name,
3393
+ description: t.description,
3394
+ input_schema: t.input_schema
3395
+ }));
3396
+ }
3397
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
3398
+ method: "POST",
3399
+ headers: {
3400
+ "Content-Type": "application/json",
3401
+ "x-api-key": this.config.api_key,
3402
+ "anthropic-version": "2023-06-01"
3403
+ },
3404
+ body: JSON.stringify(body)
3405
+ });
3406
+ if (!res.ok) {
3407
+ const err = await res.text();
3408
+ throw new Error(`Anthropic ${res.status}: ${err}`);
3409
+ }
3410
+ let textContent = "";
3411
+ let stopReason = "end_turn";
3412
+ let inputTokens = 0;
3413
+ let outputTokens = 0;
3414
+ let modelName = this.config.model;
3415
+ const toolCalls = [];
3416
+ const toolInputBuffers = {};
3417
+ let currentToolId = "";
3418
+ const reader = res.body.getReader();
3419
+ const decoder = new TextDecoder();
3420
+ let buf = "";
3421
+ while (true) {
3422
+ const { done, value } = await reader.read();
3423
+ if (done) break;
3424
+ buf += decoder.decode(value, { stream: true });
3425
+ const lines = buf.split("\n");
3426
+ buf = lines.pop() ?? "";
3427
+ for (const line of lines) {
3428
+ if (!line.startsWith("data: ")) continue;
3429
+ const data = line.slice(6).trim();
3430
+ if (data === "[DONE]" || data === "") continue;
3431
+ let evt;
3432
+ try {
3433
+ evt = JSON.parse(data);
3434
+ } catch {
3435
+ continue;
3436
+ }
3437
+ const type = evt.type;
3438
+ if (type === "message_start") {
3439
+ const usage = evt.message?.usage;
3440
+ inputTokens = usage?.input_tokens ?? 0;
3441
+ modelName = evt.message?.model ?? modelName;
3442
+ } else if (type === "content_block_start") {
3443
+ const block = evt.content_block;
3444
+ if (block?.type === "tool_use") {
3445
+ currentToolId = block.id;
3446
+ toolInputBuffers[currentToolId] = "";
3447
+ toolCalls.push({ id: currentToolId, name: block.name, input: {} });
3448
+ }
3449
+ } else if (type === "content_block_delta") {
3450
+ const delta = evt.delta;
3451
+ if (delta?.type === "text_delta") {
3452
+ const token = delta.text ?? "";
3453
+ textContent += token;
3454
+ if (onToken && token) onToken(token);
3455
+ } else if (delta?.type === "input_json_delta") {
3456
+ toolInputBuffers[currentToolId] = (toolInputBuffers[currentToolId] ?? "") + (delta.partial_json ?? "");
3457
+ }
3458
+ } else if (type === "content_block_stop") {
3459
+ if (currentToolId && toolInputBuffers[currentToolId]) {
3460
+ const tc = toolCalls.find((t) => t.id === currentToolId);
3461
+ if (tc) {
3462
+ try {
3463
+ tc.input = JSON.parse(toolInputBuffers[currentToolId]);
3464
+ } catch {
3465
+ }
3466
+ }
3467
+ }
3468
+ } else if (type === "message_delta") {
3469
+ const usage = evt.usage;
3470
+ outputTokens = usage?.output_tokens ?? 0;
3471
+ const stop = evt.delta?.stop_reason;
3472
+ if (stop === "tool_use") stopReason = "tool_use";
3473
+ else if (stop === "end_turn") stopReason = "end_turn";
3474
+ else if (stop === "max_tokens") stopReason = "max_tokens";
3475
+ }
3476
+ }
3477
+ }
3478
+ return {
3479
+ content: textContent,
3480
+ tool_calls: toolCalls.length > 0 ? toolCalls : null,
3481
+ stop_reason: stopReason,
3482
+ tokens_used: inputTokens + outputTokens,
3483
+ model: modelName
3484
+ };
3485
+ }
3486
+ // ─── OpenAI (also xAI, Gemini) ───────────────────────────────────────────
3487
+ async openai(messages, tools, system, onToken, baseUrl = "https://api.openai.com/v1") {
3488
+ const allMessages = [];
3489
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
3490
+ if (sysContent) allMessages.push({ role: "system", content: sysContent });
3491
+ for (const m of messages.filter((m2) => m2.role !== "system")) {
3492
+ if (m.role === "tool") {
3493
+ allMessages.push({ role: "tool", tool_call_id: m.tool_call_id, content: m.content });
3494
+ } else if (m.role === "assistant" && m.tool_calls?.length) {
3495
+ allMessages.push({
3496
+ role: "assistant",
3497
+ content: m.content || null,
3498
+ tool_calls: m.tool_calls.map((tc) => ({
3499
+ id: tc.id,
3500
+ type: "function",
3501
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) }
3502
+ }))
3503
+ });
3504
+ } else {
3505
+ allMessages.push({ role: m.role, content: m.content });
3506
+ }
3507
+ }
3508
+ const body = {
3509
+ model: this.config.model,
3510
+ messages: allMessages,
3511
+ max_tokens: 8192,
3512
+ stream: true,
3513
+ stream_options: { include_usage: true }
3514
+ };
3515
+ if (tools.length > 0) {
3516
+ body.tools = tools.map((t) => ({
3517
+ type: "function",
3518
+ function: { name: t.name, description: t.description, parameters: t.input_schema }
3519
+ }));
3520
+ }
3521
+ const res = await fetch(`${this.config.base_url ?? baseUrl}/chat/completions`, {
3522
+ method: "POST",
3523
+ headers: {
3524
+ "Content-Type": "application/json",
3525
+ "Authorization": `Bearer ${this.config.api_key}`
3526
+ },
3527
+ body: JSON.stringify(body)
3528
+ });
3529
+ if (!res.ok) {
3530
+ const err = await res.text();
3531
+ throw new Error(`OpenAI ${res.status}: ${err}`);
3532
+ }
3533
+ let textContent = "";
3534
+ let tokensUsed = 0;
3535
+ let modelName = this.config.model;
3536
+ let stopReason = "end_turn";
3537
+ const toolCallMap = {};
3538
+ const reader = res.body.getReader();
3539
+ const decoder = new TextDecoder();
3540
+ let buf = "";
3541
+ while (true) {
3542
+ const { done, value } = await reader.read();
3543
+ if (done) break;
3544
+ buf += decoder.decode(value, { stream: true });
3545
+ const lines = buf.split("\n");
3546
+ buf = lines.pop() ?? "";
3547
+ for (const line of lines) {
3548
+ if (!line.startsWith("data: ")) continue;
3549
+ const data = line.slice(6).trim();
3550
+ if (data === "[DONE]") continue;
3551
+ let evt;
3552
+ try {
3553
+ evt = JSON.parse(data);
3554
+ } catch {
3555
+ continue;
3556
+ }
3557
+ modelName = evt.model ?? modelName;
3558
+ const usage = evt.usage;
3559
+ if (usage?.total_tokens) tokensUsed = usage.total_tokens;
3560
+ const choices = evt.choices;
3561
+ if (!choices?.length) continue;
3562
+ const delta = choices[0].delta;
3563
+ if (!delta) continue;
3564
+ const finish = choices[0].finish_reason;
3565
+ if (finish === "tool_calls") stopReason = "tool_use";
3566
+ else if (finish === "stop") stopReason = "end_turn";
3567
+ const token = delta.content;
3568
+ if (token) {
3569
+ textContent += token;
3570
+ if (onToken) onToken(token);
3571
+ }
3572
+ const toolCallDeltas = delta.tool_calls;
3573
+ if (toolCallDeltas) {
3574
+ for (const tc of toolCallDeltas) {
3575
+ const idx = tc.index;
3576
+ if (!toolCallMap[idx]) {
3577
+ toolCallMap[idx] = { id: "", name: "", args: "" };
3578
+ }
3579
+ const fn = tc.function;
3580
+ if (tc.id) toolCallMap[idx].id = tc.id;
3581
+ if (fn?.name) toolCallMap[idx].name = fn.name;
3582
+ if (fn?.arguments) toolCallMap[idx].args += fn.arguments;
3583
+ }
3584
+ }
3585
+ }
3586
+ }
3587
+ const toolCalls = Object.values(toolCallMap).filter((tc) => tc.id && tc.name).map((tc) => {
3588
+ let input = {};
3589
+ try {
3590
+ input = JSON.parse(tc.args);
3591
+ } catch {
3592
+ }
3593
+ return { id: tc.id, name: tc.name, input };
3594
+ });
3595
+ return {
3596
+ content: textContent,
3597
+ tool_calls: toolCalls.length > 0 ? toolCalls : null,
3598
+ stop_reason: stopReason,
3599
+ tokens_used: tokensUsed,
3600
+ model: modelName
3601
+ };
3602
+ }
3603
+ // ─── Ollama (no streaming for simplicity) ────────────────────────────────
3604
+ async ollama(messages, system, onToken) {
3605
+ const baseUrl = this.config.base_url ?? "http://localhost:11434";
3606
+ const allMessages = [];
3607
+ const sysContent = system ?? messages.find((m) => m.role === "system")?.content;
3608
+ if (sysContent) allMessages.push({ role: "system", content: sysContent });
3609
+ allMessages.push(...messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
3610
+ const res = await fetch(`${baseUrl}/api/chat`, {
3611
+ method: "POST",
3612
+ headers: { "Content-Type": "application/json" },
3613
+ body: JSON.stringify({ model: this.config.model, messages: allMessages, stream: false })
3614
+ });
3615
+ if (!res.ok) throw new Error(`Ollama error ${res.status}`);
3616
+ const data = await res.json();
3617
+ if (onToken) onToken(data.message.content);
3618
+ return { content: data.message.content, tool_calls: null, stop_reason: "end_turn", tokens_used: data.eval_count ?? 0, model: this.config.model };
3619
+ }
3620
+ };
3621
+
3151
3622
  // packages/daemon/src/ZeroAgentDaemon.ts
3152
3623
  var ZeroAgentDaemon = class {
3153
3624
  config = null;
@@ -3163,13 +3634,13 @@ var ZeroAgentDaemon = class {
3163
3634
  startedAt = 0;
3164
3635
  pidFilePath;
3165
3636
  constructor() {
3166
- this.pidFilePath = resolve4(homedir3(), ".0agent", "daemon.pid");
3637
+ this.pidFilePath = resolve5(homedir3(), ".0agent", "daemon.pid");
3167
3638
  }
3168
3639
  async start(opts) {
3169
3640
  this.config = await loadConfig(opts?.config_path);
3170
- const dotDir = resolve4(homedir3(), ".0agent");
3171
- if (!existsSync4(dotDir)) {
3172
- mkdirSync3(dotDir, { recursive: true });
3641
+ const dotDir = resolve5(homedir3(), ".0agent");
3642
+ if (!existsSync5(dotDir)) {
3643
+ mkdirSync4(dotDir, { recursive: true });
3173
3644
  }
3174
3645
  this.adapter = new SQLiteAdapter({ db_path: this.config.graph.db_path });
3175
3646
  this.graph = new KnowledgeGraph(this.adapter);
@@ -3220,7 +3691,7 @@ var ZeroAgentDaemon = class {
3220
3691
  getStatus: () => this.getStatus()
3221
3692
  });
3222
3693
  await this.httpServer.start();
3223
- writeFileSync3(this.pidFilePath, String(process.pid), "utf8");
3694
+ writeFileSync4(this.pidFilePath, String(process.pid), "utf8");
3224
3695
  console.log(
3225
3696
  `[0agent] Daemon started on ${this.config.server.host}:${this.config.server.port} (PID: ${process.pid})`
3226
3697
  );
@@ -3254,7 +3725,7 @@ var ZeroAgentDaemon = class {
3254
3725
  this.graph = null;
3255
3726
  }
3256
3727
  this.adapter = null;
3257
- if (existsSync4(this.pidFilePath)) {
3728
+ if (existsSync5(this.pidFilePath)) {
3258
3729
  try {
3259
3730
  unlinkSync2(this.pidFilePath);
3260
3731
  } catch {
@@ -3284,11 +3755,11 @@ var ZeroAgentDaemon = class {
3284
3755
  };
3285
3756
 
3286
3757
  // packages/daemon/src/start.ts
3287
- import { resolve as resolve5 } from "node:path";
3758
+ import { resolve as resolve6 } from "node:path";
3288
3759
  import { homedir as homedir4 } from "node:os";
3289
- import { existsSync as existsSync5 } from "node:fs";
3290
- var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve5(homedir4(), ".0agent", "config.yaml");
3291
- if (!existsSync5(CONFIG_PATH)) {
3760
+ import { existsSync as existsSync6 } from "node:fs";
3761
+ var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve6(homedir4(), ".0agent", "config.yaml");
3762
+ if (!existsSync6(CONFIG_PATH)) {
3292
3763
  console.error(`
3293
3764
  0agent is not initialised.
3294
3765