@sandagent/runner-cli 0.9.19 → 0.9.21

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/bundle.mjs +336 -7
  2. package/package.json +5 -5
package/dist/bundle.mjs CHANGED
@@ -1209,7 +1209,7 @@ function createOpenCodeRunner(options = {}) {
1209
1209
  import { appendFileSync as appendFileSync2, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "node:fs";
1210
1210
  import { join as join5 } from "node:path";
1211
1211
  import { getModel } from "@mariozechner/pi-ai";
1212
- import { AuthStorage, createAgentSession, createBashTool, ModelRegistry, SessionManager } from "@mariozechner/pi-coding-agent";
1212
+ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@mariozechner/pi-coding-agent";
1213
1213
 
1214
1214
  // ../../packages/runner-pi/dist/sandagent-resource-loader.js
1215
1215
  import { existsSync as existsSync3 } from "node:fs";
@@ -1299,13 +1299,311 @@ var SandagentResourceLoader = class {
1299
1299
  }
1300
1300
  };
1301
1301
 
1302
- // ../../packages/runner-pi/dist/pi-runner.js
1302
+ // ../../packages/runner-pi/dist/tool-overrides.js
1303
+ import { createBashTool, createReadTool } from "@mariozechner/pi-coding-agent";
1304
+
1305
+ // ../../packages/runner-pi/dist/web-tools.js
1306
+ var braveProvider = {
1307
+ id: "brave",
1308
+ label: "Brave Search",
1309
+ envKeys: ["BRAVE_API_KEY"],
1310
+ async search({ apiKey, query, count, country, freshness }) {
1311
+ const params = new URLSearchParams({
1312
+ q: query,
1313
+ count: String(Math.min(count, 20))
1314
+ });
1315
+ if (country)
1316
+ params.set("country", country);
1317
+ if (freshness)
1318
+ params.set("freshness", freshness);
1319
+ const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
1320
+ headers: {
1321
+ Accept: "application/json",
1322
+ "Accept-Encoding": "gzip",
1323
+ "X-Subscription-Token": apiKey
1324
+ }
1325
+ });
1326
+ if (!res.ok) {
1327
+ const body = await res.text().catch(() => "");
1328
+ throw new Error(`Brave API ${res.status}: ${res.statusText}
1329
+ ${body}`);
1330
+ }
1331
+ const data = await res.json();
1332
+ const results = [];
1333
+ if (data.web?.results) {
1334
+ for (const r of data.web.results) {
1335
+ if (results.length >= count)
1336
+ break;
1337
+ results.push({
1338
+ title: r.title ?? "",
1339
+ link: r.url ?? "",
1340
+ snippet: r.description ?? "",
1341
+ age: r.age ?? r.page_age ?? ""
1342
+ });
1343
+ }
1344
+ }
1345
+ return results;
1346
+ }
1347
+ };
1348
+ var tavilyProvider = {
1349
+ id: "tavily",
1350
+ label: "Tavily",
1351
+ envKeys: ["TAVILY_API_KEY"],
1352
+ async search({ apiKey, query, count }) {
1353
+ const res = await fetch("https://api.tavily.com/search", {
1354
+ method: "POST",
1355
+ headers: { "Content-Type": "application/json" },
1356
+ body: JSON.stringify({
1357
+ api_key: apiKey,
1358
+ query,
1359
+ max_results: Math.min(count, 10),
1360
+ include_answer: false
1361
+ })
1362
+ });
1363
+ if (!res.ok) {
1364
+ const body = await res.text().catch(() => "");
1365
+ throw new Error(`Tavily API ${res.status}: ${res.statusText}
1366
+ ${body}`);
1367
+ }
1368
+ const data = await res.json();
1369
+ const results = [];
1370
+ if (Array.isArray(data.results)) {
1371
+ for (const r of data.results) {
1372
+ results.push({
1373
+ title: r.title ?? "",
1374
+ link: r.url ?? "",
1375
+ snippet: r.content ?? ""
1376
+ });
1377
+ }
1378
+ }
1379
+ return results;
1380
+ }
1381
+ };
1382
+ var AUTO_DETECT_ORDER = [braveProvider, tavilyProvider];
1383
+ function getEnv(env, key) {
1384
+ const v = env[key] ?? process.env[key];
1385
+ return v && v.length > 0 ? v : void 0;
1386
+ }
1387
+ function resolveSearchProviders(env) {
1388
+ const available = [];
1389
+ for (const p of AUTO_DETECT_ORDER) {
1390
+ for (const key of p.envKeys) {
1391
+ const val = getEnv(env, key);
1392
+ if (val) {
1393
+ available.push({ provider: p, apiKey: val });
1394
+ break;
1395
+ }
1396
+ }
1397
+ }
1398
+ return available;
1399
+ }
1400
+ function resolveSearchProvider(env) {
1401
+ const all = resolveSearchProviders(env);
1402
+ return all.length > 0 ? all[0] : null;
1403
+ }
1404
+ function isRateLimitError(err) {
1405
+ if (!(err instanceof Error))
1406
+ return false;
1407
+ const msg = err.message;
1408
+ return msg.includes("429") || msg.includes("rate") || msg.includes("quota") || msg.includes("limit");
1409
+ }
1410
+ var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
1411
+ function htmlToText(html) {
1412
+ return html.replace(/<(script|style|noscript)[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr)>/gi, "\n").replace(/<(p|div|h[1-6]|li|tr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
1413
+ }
1414
+ async function fetchPageContent(url) {
1415
+ const controller = new AbortController();
1416
+ const timeout = setTimeout(() => controller.abort(), 15e3);
1417
+ try {
1418
+ const res = await fetch(url, {
1419
+ headers: {
1420
+ "User-Agent": BROWSER_UA,
1421
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1422
+ "Accept-Language": "en-US,en;q=0.9"
1423
+ },
1424
+ signal: controller.signal
1425
+ });
1426
+ if (!res.ok)
1427
+ return `(HTTP ${res.status}: ${res.statusText})`;
1428
+ const html = await res.text();
1429
+ const text = htmlToText(html);
1430
+ return text.length > 5e4 ? `${text.slice(0, 5e4)}
1431
+
1432
+ [Truncated]` : text;
1433
+ } catch (e) {
1434
+ const msg = e instanceof Error ? e.message : String(e);
1435
+ return `(Error fetching ${url}: ${msg})`;
1436
+ } finally {
1437
+ clearTimeout(timeout);
1438
+ }
1439
+ }
1440
+ function formatSearchResults(results, providerLabel) {
1441
+ if (results.length === 0)
1442
+ return "No results found.";
1443
+ const header = `[${providerLabel}] ${results.length} result(s)
1444
+ `;
1445
+ return header + results.map((r, i) => {
1446
+ const lines = [
1447
+ `--- Result ${i + 1} ---`,
1448
+ `Title: ${r.title}`,
1449
+ `Link: ${r.link}`
1450
+ ];
1451
+ if (r.age)
1452
+ lines.push(`Age: ${r.age}`);
1453
+ lines.push(`Snippet: ${r.snippet}`);
1454
+ if (r.content)
1455
+ lines.push(`Content:
1456
+ ${r.content}`);
1457
+ return lines.join("\n");
1458
+ }).join("\n\n");
1459
+ }
1460
+ var webSearchSchema = {
1461
+ type: "object",
1462
+ required: ["query"],
1463
+ properties: {
1464
+ query: {
1465
+ type: "string",
1466
+ description: "Search query string"
1467
+ },
1468
+ count: {
1469
+ type: "number",
1470
+ description: "Number of results to return (default: 5, max: 20)"
1471
+ },
1472
+ freshness: {
1473
+ type: "string",
1474
+ description: 'Filter by time: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year), or "YYYY-MM-DDtoYYYY-MM-DD"'
1475
+ },
1476
+ country: {
1477
+ type: "string",
1478
+ description: "Two-letter country code for results (default: US)"
1479
+ },
1480
+ fetch_content: {
1481
+ type: "boolean",
1482
+ description: "If true, also fetch and include page content for each result (slower)"
1483
+ }
1484
+ }
1485
+ };
1486
+ var webFetchSchema = {
1487
+ type: "object",
1488
+ required: ["url"],
1489
+ properties: {
1490
+ url: {
1491
+ type: "string",
1492
+ description: "URL to fetch and extract readable content from"
1493
+ }
1494
+ }
1495
+ };
1496
+ function buildWebSearchTool(env) {
1497
+ const providers = resolveSearchProviders(env);
1498
+ if (providers.length === 0) {
1499
+ throw new Error("web_search: no search provider available. Set BRAVE_API_KEY or TAVILY_API_KEY.");
1500
+ }
1501
+ return {
1502
+ name: "web_search",
1503
+ label: "web search",
1504
+ description: "Search the web for information. Returns titles, URLs, and snippets. Use for documentation lookups, fact-checking, current events, or any query requiring web results.",
1505
+ promptSnippet: "web_search(query, count?, freshness?, country?, fetch_content?) - search the web",
1506
+ promptGuidelines: [
1507
+ "Use web_search when you need current information, documentation, or facts not available locally.",
1508
+ "Set fetch_content=true only when you need the actual page text, not just snippets \u2014 it is slower.",
1509
+ "Prefer specific, focused queries over broad ones for better results."
1510
+ ],
1511
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1512
+ parameters: webSearchSchema,
1513
+ async execute(_toolCallId, params, _signal, _onUpdate) {
1514
+ const p = params;
1515
+ const query = p.query;
1516
+ const count = p.count ?? 5;
1517
+ const country = p.country ?? "US";
1518
+ const freshness = p.freshness;
1519
+ const shouldFetchContent = p.fetch_content ?? false;
1520
+ let lastError;
1521
+ for (const { provider, apiKey } of providers) {
1522
+ try {
1523
+ const results = await provider.search({
1524
+ apiKey,
1525
+ query,
1526
+ count,
1527
+ country,
1528
+ freshness
1529
+ });
1530
+ if (shouldFetchContent) {
1531
+ for (const r of results) {
1532
+ r.content = await fetchPageContent(r.link);
1533
+ }
1534
+ }
1535
+ return {
1536
+ content: [
1537
+ {
1538
+ type: "text",
1539
+ text: formatSearchResults(results, provider.label)
1540
+ }
1541
+ ],
1542
+ details: void 0
1543
+ };
1544
+ } catch (e) {
1545
+ lastError = e;
1546
+ if (isRateLimitError(e) && providers.length > 1) {
1547
+ console.error(`[sandagent:pi] ${provider.label} rate-limited, trying next provider...`);
1548
+ continue;
1549
+ }
1550
+ break;
1551
+ }
1552
+ }
1553
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
1554
+ return {
1555
+ content: [
1556
+ {
1557
+ type: "text",
1558
+ text: `Web search error: ${msg}`
1559
+ }
1560
+ ],
1561
+ details: void 0
1562
+ };
1563
+ }
1564
+ };
1565
+ }
1566
+ function buildWebFetchTool() {
1567
+ return {
1568
+ name: "web_fetch",
1569
+ label: "web fetch",
1570
+ description: "Fetch a web page and extract its readable text content. Use when you need the full content of a specific URL (article, docs page, etc.).",
1571
+ promptSnippet: "web_fetch(url) - fetch and extract content from a URL",
1572
+ promptGuidelines: [
1573
+ "Use web_fetch when you already have a URL and need its content.",
1574
+ "For finding URLs first, use web_search instead."
1575
+ ],
1576
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
1577
+ parameters: webFetchSchema,
1578
+ async execute(_toolCallId, params, _signal, _onUpdate) {
1579
+ const p = params;
1580
+ const url = p.url;
1581
+ try {
1582
+ const content = await fetchPageContent(url);
1583
+ return {
1584
+ content: [{ type: "text", text: content }],
1585
+ details: void 0
1586
+ };
1587
+ } catch (e) {
1588
+ const msg = e instanceof Error ? e.message : String(e);
1589
+ return {
1590
+ content: [
1591
+ { type: "text", text: `Error fetching URL: ${msg}` }
1592
+ ],
1593
+ details: void 0
1594
+ };
1595
+ }
1596
+ }
1597
+ };
1598
+ }
1599
+
1600
+ // ../../packages/runner-pi/dist/tool-overrides.js
1303
1601
  function redactSecrets(text, secrets) {
1304
1602
  if (Object.keys(secrets).length === 0)
1305
1603
  return text;
1306
1604
  let result = text;
1307
1605
  const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1308
- const values = Object.values(secrets).filter((v) => v.length >= 8).sort((a, b) => b.length - a.length);
1606
+ const values = Object.values(secrets).filter((v) => v.length >= 4).sort((a, b) => b.length - a.length);
1309
1607
  for (const v of values) {
1310
1608
  const ev = escapeRegex(v);
1311
1609
  result = result.replace(new RegExp(`^\\S+=.*${ev}.*$\\n?`, "gm"), "");
@@ -1315,6 +1613,11 @@ function redactSecrets(text, secrets) {
1315
1613
  result = result.replace(/\n{3,}/g, "\n\n");
1316
1614
  return result.trim();
1317
1615
  }
1616
+ function redactResultContent(result, secrets) {
1617
+ if (result?.content && Array.isArray(result.content)) {
1618
+ result.content = result.content.map((c) => c.type === "text" && typeof c.text === "string" ? { ...c, text: redactSecrets(c.text, secrets) } : c);
1619
+ }
1620
+ }
1318
1621
  function buildEnvInjectedBashTool(cwd, extraEnv) {
1319
1622
  const bashAgentTool = createBashTool(cwd, {
1320
1623
  spawnHook: (ctx) => ({
@@ -1330,13 +1633,39 @@ function buildEnvInjectedBashTool(cwd, extraEnv) {
1330
1633
  parameters: bashAgentTool.parameters,
1331
1634
  async execute(toolCallId, params, signal, onUpdate) {
1332
1635
  const result = await bashAgentTool.execute(toolCallId, params, signal, onUpdate);
1333
- if (result?.content && Array.isArray(result.content)) {
1334
- result.content = result.content.map((c) => c.type === "text" && typeof c.text === "string" ? { ...c, text: redactSecrets(c.text, extraEnv) } : c);
1335
- }
1636
+ redactResultContent(result, extraEnv);
1637
+ return result;
1638
+ }
1639
+ };
1640
+ }
1641
+ function buildSecretRedactingReadTool(cwd, secrets) {
1642
+ const readAgentTool = createReadTool(cwd);
1643
+ return {
1644
+ name: readAgentTool.name,
1645
+ label: readAgentTool.label ?? "read",
1646
+ description: readAgentTool.description,
1647
+ // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema from pi internals
1648
+ parameters: readAgentTool.parameters,
1649
+ async execute(toolCallId, params, signal, onUpdate) {
1650
+ const result = await readAgentTool.execute(toolCallId, params, signal, onUpdate);
1651
+ redactResultContent(result, secrets);
1336
1652
  return result;
1337
1653
  }
1338
1654
  };
1339
1655
  }
1656
+ function buildSecretAwareTools(cwd, secrets) {
1657
+ const tools = [
1658
+ buildEnvInjectedBashTool(cwd, secrets),
1659
+ buildSecretRedactingReadTool(cwd, secrets),
1660
+ buildWebFetchTool()
1661
+ ];
1662
+ if (resolveSearchProvider(secrets)) {
1663
+ tools.push(buildWebSearchTool(secrets));
1664
+ }
1665
+ return tools;
1666
+ }
1667
+
1668
+ // ../../packages/runner-pi/dist/pi-runner.js
1340
1669
  var LOG_PREFIX2 = "[sandagent:pi]";
1341
1670
  function parseModelSpec(model) {
1342
1671
  const trimmed = model.trim();
@@ -1511,7 +1840,7 @@ function createPiRunner(options = {}) {
1511
1840
  if (resourceLoader) {
1512
1841
  await resourceLoader.reload();
1513
1842
  }
1514
- const customTools = options.env && Object.keys(options.env).length > 0 ? [buildEnvInjectedBashTool(cwd, options.env)] : [];
1843
+ const customTools = options.env && Object.keys(options.env).length > 0 ? buildSecretAwareTools(cwd, options.env) : [];
1515
1844
  const { session } = await createAgentSession({
1516
1845
  cwd,
1517
1846
  model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandagent/runner-cli",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "description": "SandAgent Runner CLI - Like gemini-cli or claude-code, runs in your local terminal with AI SDK UI streaming",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,12 +53,12 @@
53
53
  "esbuild": "^0.27.2",
54
54
  "typescript": "^5.3.0",
55
55
  "vitest": "^1.6.1",
56
+ "@sandagent/runner-core": "0.1.1-beta.0",
56
57
  "@sandagent/runner-codex": "0.6.2",
57
- "@sandagent/runner-opencode": "0.6.2",
58
58
  "@sandagent/runner-gemini": "0.6.2",
59
- "@sandagent/runner-core": "0.1.1-beta.0",
60
- "@sandagent/runner-pi": "0.6.4-beta.0",
61
- "@sandagent/runner-claude": "0.6.2"
59
+ "@sandagent/runner-opencode": "0.6.2",
60
+ "@sandagent/runner-claude": "0.6.2",
61
+ "@sandagent/runner-pi": "0.6.4-beta.0"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "tsc && pnpm bundle",