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.
- package/dist/daemon.mjs +838 -367
- 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
|
|
3
|
-
import { resolve as
|
|
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
|
|
1691
|
-
import { resolve as
|
|
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/
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1704
|
+
query: { type: "string", description: "Search query" },
|
|
1705
|
+
num_results: { type: "number", description: "Number of results (default 5, max 10)" }
|
|
1703
1706
|
},
|
|
1704
|
-
required: ["
|
|
1707
|
+
required: ["query"]
|
|
1705
1708
|
}
|
|
1706
|
-
}
|
|
1707
|
-
{
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
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(/&/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
|
-
|
|
1721
|
-
|
|
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
|
-
|
|
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: ["
|
|
1850
|
+
required: ["url"]
|
|
1728
1851
|
}
|
|
1729
|
-
}
|
|
1730
|
-
{
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
|
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: '
|
|
1748
|
-
selector: { type: "string", description: "Optional CSS selector to target
|
|
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
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
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 (
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
|
|
2033
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
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((
|
|
2291
|
+
return new Promise((resolve7) => {
|
|
2165
2292
|
const chunks = [];
|
|
2166
|
-
const proc =
|
|
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
|
-
|
|
2302
|
+
resolve7(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
|
|
2176
2303
|
});
|
|
2177
2304
|
proc.on("error", (err) => {
|
|
2178
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
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 (!
|
|
2194
|
-
const content =
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/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 (!
|
|
2412
|
+
if (!existsSync3(safe)) return `Directory not found: ${dirPath}`;
|
|
2241
2413
|
try {
|
|
2242
|
-
const entries =
|
|
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 =
|
|
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
|
|
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 (!
|
|
2757
|
-
const files =
|
|
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 =
|
|
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
|
-
|
|
2969
|
+
mkdirSync3(this.customDir, { recursive: true });
|
|
2796
2970
|
const filePath = join(this.customDir, `${name}.yaml`);
|
|
2797
|
-
|
|
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 (
|
|
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
|
|
2824
|
-
import { resolve as
|
|
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
|
-
|
|
3253
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "graph.html"),
|
|
3080
3254
|
// dev (src/)
|
|
3081
|
-
|
|
3255
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "graph.html"),
|
|
3082
3256
|
// bundled (dist/../)
|
|
3083
|
-
|
|
3257
|
+
resolve4(dirname3(fileURLToPath(import.meta.url)), "..", "dist", "graph.html")
|
|
3084
3258
|
];
|
|
3085
3259
|
for (const p of candidates) {
|
|
3086
3260
|
try {
|
|
3087
|
-
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
3303
|
+
resolve7();
|
|
3130
3304
|
}
|
|
3131
3305
|
);
|
|
3132
3306
|
});
|
|
3133
3307
|
}
|
|
3134
3308
|
stop() {
|
|
3135
|
-
return new Promise((
|
|
3309
|
+
return new Promise((resolve7, reject) => {
|
|
3136
3310
|
if (!this.server) {
|
|
3137
|
-
|
|
3311
|
+
resolve7();
|
|
3138
3312
|
return;
|
|
3139
3313
|
}
|
|
3140
3314
|
this.server.close((err) => {
|
|
3141
3315
|
if (err) reject(err);
|
|
3142
|
-
else
|
|
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 =
|
|
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 =
|
|
3171
|
-
if (!
|
|
3172
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3758
|
+
import { resolve as resolve6 } from "node:path";
|
|
3288
3759
|
import { homedir as homedir4 } from "node:os";
|
|
3289
|
-
import { existsSync as
|
|
3290
|
-
var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ??
|
|
3291
|
-
if (!
|
|
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
|
|