@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.0
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/CHANGELOG.md +79 -0
- package/docs/theme.md +38 -5
- package/examples/sdk/11-sessions.ts +2 -2
- package/package.json +7 -4
- package/src/cli/file-processor.ts +51 -2
- package/src/cli/plugin-cli.ts +25 -19
- package/src/cli/update-cli.ts +4 -3
- package/src/core/agent-session.ts +31 -4
- package/src/core/compaction/branch-summarization.ts +4 -32
- package/src/core/compaction/compaction.ts +6 -84
- package/src/core/compaction/utils.ts +2 -3
- package/src/core/custom-tools/types.ts +2 -0
- package/src/core/export-html/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +0 -1
- package/src/core/hooks/types.ts +2 -2
- package/src/core/plugins/doctor.ts +9 -1
- package/src/core/sdk.ts +2 -1
- package/src/core/session-manager.ts +518 -40
- package/src/core/settings-manager.ts +174 -0
- package/src/core/system-prompt.ts +9 -14
- package/src/core/title-generator.ts +2 -8
- package/src/core/tools/ask.ts +19 -37
- package/src/core/tools/bash.ts +2 -37
- package/src/core/tools/edit.ts +2 -9
- package/src/core/tools/exa/render.ts +52 -48
- package/src/core/tools/find.ts +10 -8
- package/src/core/tools/grep.ts +45 -17
- package/src/core/tools/ls.ts +22 -2
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +3 -0
- package/src/core/tools/lsp/index.ts +107 -55
- package/src/core/tools/lsp/render.ts +192 -79
- package/src/core/tools/lsp/types.ts +27 -0
- package/src/core/tools/lsp/utils.ts +62 -22
- package/src/core/tools/notebook.ts +9 -1
- package/src/core/tools/output.ts +37 -14
- package/src/core/tools/read.ts +349 -34
- package/src/core/tools/renderers.ts +290 -89
- package/src/core/tools/review.ts +12 -5
- package/src/core/tools/task/agents.ts +5 -5
- package/src/core/tools/task/commands.ts +3 -3
- package/src/core/tools/task/executor.ts +33 -1
- package/src/core/tools/task/index.ts +93 -6
- package/src/core/tools/task/render.ts +147 -66
- package/src/core/tools/task/types.ts +14 -9
- package/src/core/tools/web-fetch.ts +242 -103
- package/src/core/tools/web-search/index.ts +64 -20
- package/src/core/tools/web-search/providers/exa.ts +68 -172
- package/src/core/tools/web-search/render.ts +264 -74
- package/src/core/tools/write.ts +2 -8
- package/src/main.ts +10 -6
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +9 -4
- package/src/modes/interactive/components/bash-execution.ts +6 -3
- package/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
- package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
- package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
- package/src/modes/interactive/components/hook-message.ts +2 -2
- package/src/modes/interactive/components/hook-selector.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +22 -9
- package/src/modes/interactive/components/oauth-selector.ts +20 -4
- package/src/modes/interactive/components/plugin-settings.ts +4 -2
- package/src/modes/interactive/components/session-selector.ts +9 -6
- package/src/modes/interactive/components/settings-defs.ts +285 -1
- package/src/modes/interactive/components/settings-selector.ts +176 -3
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +170 -223
- package/src/modes/interactive/components/tool-execution.ts +446 -211
- package/src/modes/interactive/components/tree-selector.ts +17 -6
- package/src/modes/interactive/components/ttsr-notification.ts +4 -4
- package/src/modes/interactive/components/welcome.ts +27 -19
- package/src/modes/interactive/interactive-mode.ts +98 -13
- package/src/modes/interactive/theme/dark.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +3 -2
- package/src/modes/interactive/theme/theme-schema.json +120 -4
- package/src/modes/interactive/theme/theme.ts +1228 -14
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/init.md +30 -0
- package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/commands/init.md +0 -20
- /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import { parse as parseHtml } from "node-html-parser";
|
|
8
|
+
import webFetchDescription from "../../prompts/tools/web-fetch.md" with { type: "text" };
|
|
8
9
|
import { logger } from "../logger";
|
|
9
10
|
|
|
10
11
|
// =============================================================================
|
|
@@ -1492,10 +1493,25 @@ async function handleNpm(url: string, timeout: number): Promise<RenderResult | n
|
|
|
1492
1493
|
|
|
1493
1494
|
// Fetch from npm registry - use /latest endpoint for smaller response
|
|
1494
1495
|
const latestUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
|
1495
|
-
const
|
|
1496
|
+
const downloadsUrl = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
|
|
1497
|
+
|
|
1498
|
+
// Fetch package info and download stats in parallel
|
|
1499
|
+
const [result, downloadsResult] = await Promise.all([
|
|
1500
|
+
loadPage(latestUrl, { timeout }),
|
|
1501
|
+
loadPage(downloadsUrl, { timeout: Math.min(timeout, 5) }),
|
|
1502
|
+
]);
|
|
1496
1503
|
|
|
1497
1504
|
if (!result.ok) return null;
|
|
1498
1505
|
|
|
1506
|
+
// Parse download stats
|
|
1507
|
+
let weeklyDownloads: number | null = null;
|
|
1508
|
+
if (downloadsResult.ok) {
|
|
1509
|
+
try {
|
|
1510
|
+
const dlData = JSON.parse(downloadsResult.content) as { downloads?: number };
|
|
1511
|
+
weeklyDownloads = dlData.downloads ?? null;
|
|
1512
|
+
} catch {}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1499
1515
|
let pkg: {
|
|
1500
1516
|
name: string;
|
|
1501
1517
|
version: string;
|
|
@@ -1520,7 +1536,17 @@ async function handleNpm(url: string, timeout: number): Promise<RenderResult | n
|
|
|
1520
1536
|
|
|
1521
1537
|
md += `**Latest:** ${pkg.version || "unknown"}`;
|
|
1522
1538
|
if (pkg.license) md += ` · **License:** ${typeof pkg.license === "string" ? pkg.license : pkg.license}`;
|
|
1523
|
-
md += "\n
|
|
1539
|
+
md += "\n";
|
|
1540
|
+
if (weeklyDownloads !== null) {
|
|
1541
|
+
const formatted =
|
|
1542
|
+
weeklyDownloads >= 1_000_000
|
|
1543
|
+
? `${(weeklyDownloads / 1_000_000).toFixed(1)}M`
|
|
1544
|
+
: weeklyDownloads >= 1_000
|
|
1545
|
+
? `${(weeklyDownloads / 1_000).toFixed(1)}K`
|
|
1546
|
+
: String(weeklyDownloads);
|
|
1547
|
+
md += `**Weekly Downloads:** ${formatted}\n`;
|
|
1548
|
+
}
|
|
1549
|
+
md += "\n";
|
|
1524
1550
|
|
|
1525
1551
|
if (pkg.homepage) md += `**Homepage:** ${pkg.homepage}\n`;
|
|
1526
1552
|
const repoUrl = typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
|
|
@@ -1555,6 +1581,118 @@ async function handleNpm(url: string, timeout: number): Promise<RenderResult | n
|
|
|
1555
1581
|
return null;
|
|
1556
1582
|
}
|
|
1557
1583
|
|
|
1584
|
+
// =============================================================================
|
|
1585
|
+
// Crates.io Special Handling
|
|
1586
|
+
// =============================================================================
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Handle crates.io URLs via API
|
|
1590
|
+
*/
|
|
1591
|
+
async function handleCratesIo(url: string, timeout: number): Promise<RenderResult | null> {
|
|
1592
|
+
try {
|
|
1593
|
+
const parsed = new URL(url);
|
|
1594
|
+
if (parsed.hostname !== "crates.io" && parsed.hostname !== "www.crates.io") return null;
|
|
1595
|
+
|
|
1596
|
+
// Extract crate name from /crates/name or /crates/name/version
|
|
1597
|
+
const match = parsed.pathname.match(/^\/crates\/([^/]+)/);
|
|
1598
|
+
if (!match) return null;
|
|
1599
|
+
|
|
1600
|
+
const crateName = decodeURIComponent(match[1]);
|
|
1601
|
+
const fetchedAt = new Date().toISOString();
|
|
1602
|
+
|
|
1603
|
+
// Fetch from crates.io API
|
|
1604
|
+
const apiUrl = `https://crates.io/api/v1/crates/${crateName}`;
|
|
1605
|
+
const result = await loadPage(apiUrl, {
|
|
1606
|
+
timeout,
|
|
1607
|
+
headers: { "User-Agent": "omp-web-fetch/1.0 (https://github.com/anthropics)" },
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
if (!result.ok) return null;
|
|
1611
|
+
|
|
1612
|
+
let data: {
|
|
1613
|
+
crate: {
|
|
1614
|
+
name: string;
|
|
1615
|
+
description: string | null;
|
|
1616
|
+
downloads: number;
|
|
1617
|
+
recent_downloads: number;
|
|
1618
|
+
max_version: string;
|
|
1619
|
+
repository: string | null;
|
|
1620
|
+
homepage: string | null;
|
|
1621
|
+
documentation: string | null;
|
|
1622
|
+
categories: string[];
|
|
1623
|
+
keywords: string[];
|
|
1624
|
+
created_at: string;
|
|
1625
|
+
updated_at: string;
|
|
1626
|
+
};
|
|
1627
|
+
versions: Array<{
|
|
1628
|
+
num: string;
|
|
1629
|
+
downloads: number;
|
|
1630
|
+
created_at: string;
|
|
1631
|
+
license: string | null;
|
|
1632
|
+
rust_version: string | null;
|
|
1633
|
+
}>;
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
try {
|
|
1637
|
+
data = JSON.parse(result.content);
|
|
1638
|
+
} catch {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const crate = data.crate;
|
|
1643
|
+
const latestVersion = data.versions?.[0];
|
|
1644
|
+
|
|
1645
|
+
// Format download counts
|
|
1646
|
+
const formatDownloads = (n: number): string =>
|
|
1647
|
+
n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1_000 ? `${(n / 1_000).toFixed(1)}K` : String(n);
|
|
1648
|
+
|
|
1649
|
+
let md = `# ${crate.name}\n\n`;
|
|
1650
|
+
if (crate.description) md += `${crate.description}\n\n`;
|
|
1651
|
+
|
|
1652
|
+
md += `**Latest:** ${crate.max_version}`;
|
|
1653
|
+
if (latestVersion?.license) md += ` · **License:** ${latestVersion.license}`;
|
|
1654
|
+
if (latestVersion?.rust_version) md += ` · **MSRV:** ${latestVersion.rust_version}`;
|
|
1655
|
+
md += "\n";
|
|
1656
|
+
md += `**Downloads:** ${formatDownloads(crate.downloads)} total · ${formatDownloads(crate.recent_downloads)} recent\n\n`;
|
|
1657
|
+
|
|
1658
|
+
if (crate.repository) md += `**Repository:** ${crate.repository}\n`;
|
|
1659
|
+
if (crate.homepage && crate.homepage !== crate.repository) md += `**Homepage:** ${crate.homepage}\n`;
|
|
1660
|
+
if (crate.documentation) md += `**Docs:** ${crate.documentation}\n`;
|
|
1661
|
+
if (crate.keywords?.length) md += `**Keywords:** ${crate.keywords.join(", ")}\n`;
|
|
1662
|
+
if (crate.categories?.length) md += `**Categories:** ${crate.categories.join(", ")}\n`;
|
|
1663
|
+
|
|
1664
|
+
// Show recent versions
|
|
1665
|
+
if (data.versions?.length > 0) {
|
|
1666
|
+
md += `\n## Recent Versions\n\n`;
|
|
1667
|
+
for (const ver of data.versions.slice(0, 5)) {
|
|
1668
|
+
const date = ver.created_at.split("T")[0];
|
|
1669
|
+
md += `- **${ver.num}** (${date}) - ${formatDownloads(ver.downloads)} downloads\n`;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Try to fetch README from docs.rs or repository
|
|
1674
|
+
const docsRsUrl = `https://docs.rs/crate/${crateName}/${crate.max_version}/source/README.md`;
|
|
1675
|
+
const readmeResult = await loadPage(docsRsUrl, { timeout: Math.min(timeout, 5) });
|
|
1676
|
+
if (readmeResult.ok && readmeResult.content.length > 100 && !looksLikeHtml(readmeResult.content)) {
|
|
1677
|
+
md += `\n---\n\n## README\n\n${readmeResult.content}\n`;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
const output = finalizeOutput(md);
|
|
1681
|
+
return {
|
|
1682
|
+
url,
|
|
1683
|
+
finalUrl: url,
|
|
1684
|
+
contentType: "text/markdown",
|
|
1685
|
+
method: "crates.io",
|
|
1686
|
+
content: output.content,
|
|
1687
|
+
fetchedAt,
|
|
1688
|
+
truncated: output.truncated,
|
|
1689
|
+
notes: ["Fetched via crates.io API"],
|
|
1690
|
+
};
|
|
1691
|
+
} catch {}
|
|
1692
|
+
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1558
1696
|
// =============================================================================
|
|
1559
1697
|
// arXiv Special Handling
|
|
1560
1698
|
// =============================================================================
|
|
@@ -1803,6 +1941,7 @@ async function handleSpecialUrls(url: string, timeout: number): Promise<RenderRe
|
|
|
1803
1941
|
(await handleWikipedia(url, timeout)) ||
|
|
1804
1942
|
(await handleReddit(url, timeout)) ||
|
|
1805
1943
|
(await handleNpm(url, timeout)) ||
|
|
1944
|
+
(await handleCratesIo(url, timeout)) ||
|
|
1806
1945
|
(await handleArxiv(url, timeout)) ||
|
|
1807
1946
|
(await handleIacr(url, timeout))
|
|
1808
1947
|
);
|
|
@@ -2131,30 +2270,7 @@ export function createWebFetchTool(_cwd: string): AgentTool<typeof webFetchSchem
|
|
|
2131
2270
|
return {
|
|
2132
2271
|
name: "web_fetch",
|
|
2133
2272
|
label: "web_fetch",
|
|
2134
|
-
description:
|
|
2135
|
-
- Takes a URL and a prompt as input
|
|
2136
|
-
- Fetches the URL content, converts HTML to markdown
|
|
2137
|
-
- Processes the content with the prompt using a small, fast model
|
|
2138
|
-
- Returns the model's response about the content
|
|
2139
|
-
- Use this tool when you need to retrieve and analyze web content
|
|
2140
|
-
|
|
2141
|
-
Features:
|
|
2142
|
-
- Site-specific handlers for GitHub (issues, PRs, repos, gists), Stack Overflow, Wikipedia, Reddit, NPM, arXiv, IACR, and Twitter/X
|
|
2143
|
-
- Automatic detection and use of LLM-friendly endpoints (llms.txt, .md suffixes)
|
|
2144
|
-
- Binary file conversion (PDF, DOCX, etc.) via markitdown if available
|
|
2145
|
-
- HTML to text rendering via lynx if available
|
|
2146
|
-
- RSS/Atom feed parsing
|
|
2147
|
-
- JSON pretty-printing
|
|
2148
|
-
|
|
2149
|
-
Usage notes:
|
|
2150
|
-
- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.
|
|
2151
|
-
- The URL must be a fully-formed valid URL
|
|
2152
|
-
- HTTP URLs will be automatically upgraded to HTTPS
|
|
2153
|
-
- The prompt should describe what information you want to extract from the page
|
|
2154
|
-
- This tool is read-only and does not modify any files
|
|
2155
|
-
- Results may be summarized if the content is very large
|
|
2156
|
-
- Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
|
|
2157
|
-
- When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.`,
|
|
2273
|
+
description: webFetchDescription,
|
|
2158
2274
|
parameters: webFetchSchema,
|
|
2159
2275
|
execute: async (
|
|
2160
2276
|
_toolCallId: string,
|
|
@@ -2205,19 +2321,14 @@ export const webFetchTool = createWebFetchTool(process.cwd());
|
|
|
2205
2321
|
|
|
2206
2322
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2207
2323
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
2208
|
-
import type
|
|
2324
|
+
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
2209
2325
|
import type { CustomTool, CustomToolContext, RenderResultOptions } from "../custom-tools/types";
|
|
2210
2326
|
|
|
2211
|
-
// Tree formatting constants
|
|
2212
|
-
const TREE_MID = "├─";
|
|
2213
|
-
const TREE_END = "└─";
|
|
2214
|
-
const TREE_PIPE = "│";
|
|
2215
|
-
const TREE_HOOK = "⎿";
|
|
2216
|
-
|
|
2217
2327
|
/** Truncate text to max length with ellipsis */
|
|
2218
|
-
function truncate(text: string, maxLen: number): string {
|
|
2328
|
+
function truncate(text: string, maxLen: number, ellipsis: string): string {
|
|
2219
2329
|
if (text.length <= maxLen) return text;
|
|
2220
|
-
|
|
2330
|
+
const sliceLen = Math.max(0, maxLen - ellipsis.length);
|
|
2331
|
+
return `${text.slice(0, sliceLen)}${ellipsis}`;
|
|
2221
2332
|
}
|
|
2222
2333
|
|
|
2223
2334
|
/** Extract domain from URL */
|
|
@@ -2231,16 +2342,25 @@ function getDomain(url: string): string {
|
|
|
2231
2342
|
}
|
|
2232
2343
|
|
|
2233
2344
|
/** Get first N lines of text as preview */
|
|
2234
|
-
function getPreviewLines(text: string, maxLines: number, maxLineLen: number): string[] {
|
|
2345
|
+
function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis: string): string[] {
|
|
2235
2346
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
2236
|
-
return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen));
|
|
2347
|
+
return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen, ellipsis));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/** Count non-empty lines */
|
|
2351
|
+
function countNonEmptyLines(text: string): number {
|
|
2352
|
+
return text.split("\n").filter((l) => l.trim()).length;
|
|
2237
2353
|
}
|
|
2238
2354
|
|
|
2239
2355
|
/** Render web fetch call (URL preview) */
|
|
2240
|
-
export function renderWebFetchCall(
|
|
2356
|
+
export function renderWebFetchCall(
|
|
2357
|
+
args: { url: string; timeout?: number; raw?: boolean },
|
|
2358
|
+
uiTheme: Theme = theme,
|
|
2359
|
+
): Component {
|
|
2241
2360
|
const domain = getDomain(args.url);
|
|
2242
|
-
const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50);
|
|
2243
|
-
const
|
|
2361
|
+
const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, uiTheme.format.ellipsis);
|
|
2362
|
+
const icon = uiTheme.styledSymbol("status.pending", "muted");
|
|
2363
|
+
const text = `${icon} ${uiTheme.fg("toolTitle", "Web Fetch")} ${uiTheme.fg("accent", domain)}${uiTheme.fg("dim", path)}`;
|
|
2244
2364
|
return new Text(text, 0, 0);
|
|
2245
2365
|
}
|
|
2246
2366
|
|
|
@@ -2248,26 +2368,23 @@ export function renderWebFetchCall(args: { url: string; timeout?: number; raw?:
|
|
|
2248
2368
|
export function renderWebFetchResult(
|
|
2249
2369
|
result: { content: Array<{ type: string; text?: string }>; details?: WebFetchToolDetails },
|
|
2250
2370
|
options: RenderResultOptions,
|
|
2251
|
-
|
|
2371
|
+
uiTheme: Theme = theme,
|
|
2252
2372
|
): Component {
|
|
2253
2373
|
const { expanded } = options;
|
|
2254
2374
|
const details = result.details;
|
|
2255
2375
|
|
|
2256
2376
|
if (!details) {
|
|
2257
|
-
return new Text(
|
|
2377
|
+
return new Text(uiTheme.fg("error", "No response data"), 0, 0);
|
|
2258
2378
|
}
|
|
2259
2379
|
|
|
2260
2380
|
const domain = getDomain(details.finalUrl);
|
|
2261
2381
|
const hasRedirect = details.url !== details.finalUrl;
|
|
2262
2382
|
const hasNotes = details.notes.length > 0;
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
const expandHint = expanded ? "" :
|
|
2267
|
-
let text = `${
|
|
2268
|
-
"dim",
|
|
2269
|
-
details.method,
|
|
2270
|
-
)}${expandHint}`;
|
|
2383
|
+
const statusIcon = details.truncated
|
|
2384
|
+
? uiTheme.styledSymbol("status.warning", "warning")
|
|
2385
|
+
: uiTheme.styledSymbol("status.success", "success");
|
|
2386
|
+
const expandHint = expanded ? "" : uiTheme.fg("dim", " (Ctrl+O to expand)");
|
|
2387
|
+
let text = `${statusIcon} ${uiTheme.fg("toolTitle", "Web Fetch")} ${uiTheme.fg("accent", `(${domain})`)}${uiTheme.sep.dot}${uiTheme.fg("dim", details.method)}${expandHint}`;
|
|
2271
2388
|
|
|
2272
2389
|
// Get content text
|
|
2273
2390
|
const contentText = result.content[0]?.text ?? "";
|
|
@@ -2275,68 +2392,90 @@ export function renderWebFetchResult(
|
|
|
2275
2392
|
const contentBody = contentText.includes("---\n\n")
|
|
2276
2393
|
? contentText.split("---\n\n").slice(1).join("---\n\n")
|
|
2277
2394
|
: contentText;
|
|
2395
|
+
const lineCount = countNonEmptyLines(contentBody);
|
|
2396
|
+
const charCount = contentBody.trim().length;
|
|
2278
2397
|
|
|
2279
2398
|
if (!expanded) {
|
|
2280
|
-
// Collapsed view:
|
|
2399
|
+
// Collapsed view: metadata + preview
|
|
2400
|
+
const metaLines: string[] = [
|
|
2401
|
+
`${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
|
|
2402
|
+
`${uiTheme.fg("muted", "Method:")} ${details.method}`,
|
|
2403
|
+
];
|
|
2281
2404
|
if (hasRedirect) {
|
|
2282
|
-
|
|
2405
|
+
metaLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
|
|
2283
2406
|
}
|
|
2284
2407
|
if (details.truncated) {
|
|
2285
|
-
|
|
2408
|
+
metaLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
|
|
2409
|
+
}
|
|
2410
|
+
if (hasNotes) {
|
|
2411
|
+
metaLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
|
|
2286
2412
|
}
|
|
2287
2413
|
|
|
2288
|
-
const previewLines = getPreviewLines(contentBody, 3, 100);
|
|
2289
|
-
|
|
2290
|
-
|
|
2414
|
+
const previewLines = getPreviewLines(contentBody, 3, 100, uiTheme.format.ellipsis);
|
|
2415
|
+
const detailLines: string[] = [...metaLines];
|
|
2416
|
+
|
|
2417
|
+
if (previewLines.length === 0) {
|
|
2418
|
+
detailLines.push(uiTheme.fg("dim", "(no content)"));
|
|
2419
|
+
} else {
|
|
2420
|
+
for (const line of previewLines) {
|
|
2421
|
+
detailLines.push(uiTheme.fg("dim", line));
|
|
2422
|
+
}
|
|
2291
2423
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2424
|
+
|
|
2425
|
+
const remaining = Math.max(0, lineCount - previewLines.length);
|
|
2426
|
+
if (remaining > 0) {
|
|
2427
|
+
detailLines.push(uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines`));
|
|
2295
2428
|
} else {
|
|
2296
|
-
|
|
2429
|
+
const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
2430
|
+
detailLines.push(uiTheme.fg("muted", `${lineLabel}${uiTheme.sep.dot}${charCount} chars`));
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
for (let i = 0; i < detailLines.length; i++) {
|
|
2434
|
+
const isLast = i === detailLines.length - 1;
|
|
2435
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.vertical;
|
|
2436
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${detailLines[i]}`;
|
|
2297
2437
|
}
|
|
2298
2438
|
} else {
|
|
2299
|
-
// Expanded view:
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2439
|
+
// Expanded view: structured metadata + bounded content preview
|
|
2440
|
+
const metaLines: string[] = [
|
|
2441
|
+
`${uiTheme.fg("muted", "Content-Type:")} ${details.contentType || "unknown"}`,
|
|
2442
|
+
`${uiTheme.fg("muted", "Method:")} ${details.method}`,
|
|
2443
|
+
];
|
|
2444
|
+
if (hasRedirect) {
|
|
2445
|
+
metaLines.push(`${uiTheme.fg("muted", "Final URL:")} ${uiTheme.fg("mdLinkUrl", details.finalUrl)}`);
|
|
2446
|
+
}
|
|
2447
|
+
const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
2448
|
+
metaLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
|
|
2449
|
+
metaLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
|
|
2450
|
+
if (details.truncated) {
|
|
2451
|
+
metaLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
|
|
2452
|
+
}
|
|
2453
|
+
if (hasNotes) {
|
|
2454
|
+
metaLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.branch)} ${uiTheme.fg("accent", "Metadata")}`;
|
|
2458
|
+
for (let i = 0; i < metaLines.length; i++) {
|
|
2459
|
+
const isLast = i === metaLines.length - 1;
|
|
2460
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
2461
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg("dim", branch)} ${metaLines[i]}`;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("accent", "Content Preview")}`;
|
|
2465
|
+
const previewLines = getPreviewLines(contentBody, 12, 120, uiTheme.format.ellipsis);
|
|
2466
|
+
const remaining = Math.max(0, lineCount - previewLines.length);
|
|
2467
|
+
const contentPrefix = uiTheme.fg("dim", " ");
|
|
2468
|
+
|
|
2469
|
+
if (previewLines.length === 0) {
|
|
2470
|
+
text += `\n ${contentPrefix} ${uiTheme.fg("dim", "(no content)")}`;
|
|
2471
|
+
} else {
|
|
2472
|
+
for (const line of previewLines) {
|
|
2473
|
+
text += `\n ${contentPrefix} ${uiTheme.fg("dim", line)}`;
|
|
2330
2474
|
}
|
|
2331
2475
|
}
|
|
2332
2476
|
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const contentLines = contentBody.split("\n");
|
|
2336
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
2337
|
-
const line = contentLines[i];
|
|
2338
|
-
const isLast = i === contentLines.length - 1;
|
|
2339
|
-
text += `\n ${isLast ? " " : theme.fg("dim", " ")} ${line}`;
|
|
2477
|
+
if (remaining > 0) {
|
|
2478
|
+
text += `\n ${contentPrefix} ${uiTheme.fg("muted", `${uiTheme.format.ellipsis} ${remaining} more lines`)}`;
|
|
2340
2479
|
}
|
|
2341
2480
|
}
|
|
2342
2481
|
|
|
@@ -2349,7 +2488,7 @@ type WebFetchParams = { url: string; timeout?: number; raw?: boolean };
|
|
|
2349
2488
|
export const webFetchCustomTool: CustomTool<typeof webFetchSchema, WebFetchToolDetails> = {
|
|
2350
2489
|
name: "web_fetch",
|
|
2351
2490
|
label: "Web Fetch",
|
|
2352
|
-
description:
|
|
2491
|
+
description: webFetchDescription,
|
|
2353
2492
|
parameters: webFetchSchema,
|
|
2354
2493
|
|
|
2355
2494
|
async execute(
|
|
@@ -2362,11 +2501,11 @@ export const webFetchCustomTool: CustomTool<typeof webFetchSchema, WebFetchToolD
|
|
|
2362
2501
|
return webFetchTool.execute(toolCallId, params);
|
|
2363
2502
|
},
|
|
2364
2503
|
|
|
2365
|
-
renderCall(args: WebFetchParams,
|
|
2366
|
-
return renderWebFetchCall(args,
|
|
2504
|
+
renderCall(args: WebFetchParams, uiTheme: Theme) {
|
|
2505
|
+
return renderWebFetchCall(args, uiTheme);
|
|
2367
2506
|
},
|
|
2368
2507
|
|
|
2369
|
-
renderResult(result, options: RenderResultOptions,
|
|
2370
|
-
return renderWebFetchResult(result, options,
|
|
2508
|
+
renderResult(result, options: RenderResultOptions, uiTheme: Theme) {
|
|
2509
|
+
return renderWebFetchResult(result, options, uiTheme);
|
|
2371
2510
|
},
|
|
2372
2511
|
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
18
|
+
import webSearchDescription from "../../../prompts/tools/web-search.md" with { type: "text" };
|
|
18
19
|
import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../custom-tools/types";
|
|
19
20
|
import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../exa/mcp-client";
|
|
20
21
|
import { renderExaCall, renderExaResult } from "../exa/render";
|
|
@@ -107,31 +108,85 @@ async function detectProvider(): Promise<WebSearchProvider> {
|
|
|
107
108
|
return "anthropic";
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/** Truncate text for tool output */
|
|
112
|
+
function truncateText(text: string, maxLen: number): string {
|
|
113
|
+
if (text.length <= maxLen) return text;
|
|
114
|
+
return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatCount(label: string, count: number): string {
|
|
118
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
/** Format response for LLM consumption */
|
|
111
122
|
function formatForLLM(response: WebSearchResponse): string {
|
|
112
123
|
const parts: string[] = [];
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
parts.push(response.answer);
|
|
117
|
-
}
|
|
125
|
+
parts.push("## Answer");
|
|
126
|
+
parts.push(response.answer ? response.answer : "No answer text returned.");
|
|
118
127
|
|
|
119
|
-
// Add sources
|
|
120
128
|
if (response.sources.length > 0) {
|
|
121
129
|
parts.push("\n## Sources");
|
|
130
|
+
parts.push(formatCount("source", response.sources.length));
|
|
122
131
|
for (const [i, src] of response.sources.entries()) {
|
|
123
132
|
const age = formatAge(src.ageSeconds) || src.publishedDate;
|
|
124
133
|
const agePart = age ? ` (${age})` : "";
|
|
125
134
|
parts.push(`[${i + 1}] ${src.title}${agePart}\n ${src.url}`);
|
|
135
|
+
if (src.snippet) {
|
|
136
|
+
parts.push(` ${truncateText(src.snippet, 240)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
parts.push("\n## Sources");
|
|
141
|
+
parts.push("0 sources");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (response.citations && response.citations.length > 0) {
|
|
145
|
+
parts.push("\n## Citations");
|
|
146
|
+
parts.push(formatCount("citation", response.citations.length));
|
|
147
|
+
for (const [i, citation] of response.citations.entries()) {
|
|
148
|
+
const title = citation.title || citation.url;
|
|
149
|
+
parts.push(`[${i + 1}] ${title}\n ${citation.url}`);
|
|
150
|
+
if (citation.citedText) {
|
|
151
|
+
parts.push(` ${truncateText(citation.citedText, 240)}`);
|
|
152
|
+
}
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
|
|
129
|
-
// Add related questions (Perplexity)
|
|
130
156
|
if (response.relatedQuestions && response.relatedQuestions.length > 0) {
|
|
131
|
-
parts.push("\n## Related
|
|
157
|
+
parts.push("\n## Related");
|
|
158
|
+
parts.push(formatCount("question", response.relatedQuestions.length));
|
|
132
159
|
for (const q of response.relatedQuestions) {
|
|
133
160
|
parts.push(`- ${q}`);
|
|
134
161
|
}
|
|
162
|
+
} else {
|
|
163
|
+
parts.push("\n## Related");
|
|
164
|
+
parts.push("0 questions");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
parts.push("\n## Meta");
|
|
168
|
+
parts.push(`Provider: ${response.provider}`);
|
|
169
|
+
if (response.model) {
|
|
170
|
+
parts.push(`Model: ${response.model}`);
|
|
171
|
+
}
|
|
172
|
+
if (response.usage) {
|
|
173
|
+
const usageParts: string[] = [];
|
|
174
|
+
if (response.usage.inputTokens !== undefined) usageParts.push(`in ${response.usage.inputTokens}`);
|
|
175
|
+
if (response.usage.outputTokens !== undefined) usageParts.push(`out ${response.usage.outputTokens}`);
|
|
176
|
+
if (response.usage.totalTokens !== undefined) usageParts.push(`total ${response.usage.totalTokens}`);
|
|
177
|
+
if (response.usage.searchRequests !== undefined) usageParts.push(`search ${response.usage.searchRequests}`);
|
|
178
|
+
if (usageParts.length > 0) {
|
|
179
|
+
parts.push(`Usage: ${usageParts.join(" | ")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (response.requestId) {
|
|
183
|
+
parts.push(`Request: ${truncateText(response.requestId, 64)}`);
|
|
184
|
+
}
|
|
185
|
+
if (response.searchQueries && response.searchQueries.length > 0) {
|
|
186
|
+
parts.push(`Search queries: ${response.searchQueries.length}`);
|
|
187
|
+
for (const query of response.searchQueries.slice(0, 3)) {
|
|
188
|
+
parts.push(`- ${truncateText(query, 120)}`);
|
|
189
|
+
}
|
|
135
190
|
}
|
|
136
191
|
|
|
137
192
|
return parts.join("\n");
|
|
@@ -186,22 +241,11 @@ async function executeWebSearch(
|
|
|
186
241
|
}
|
|
187
242
|
}
|
|
188
243
|
|
|
189
|
-
const WEB_SEARCH_DESCRIPTION = `Allows OMP to search the web and use the results to inform responses
|
|
190
|
-
- Provides up-to-date information for current events and recent data
|
|
191
|
-
- Returns search result information formatted as search result blocks, including links as markdown hyperlinks
|
|
192
|
-
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
|
193
|
-
- Searches are performed automatically within a single API call
|
|
194
|
-
|
|
195
|
-
Common: system_prompt (guides response style)
|
|
196
|
-
Anthropic-specific: max_tokens
|
|
197
|
-
Perplexity-specific: model (sonar/sonar-pro), search_recency_filter, search_domain_filter, search_context_size, return_related_questions
|
|
198
|
-
Exa-specific: num_results`;
|
|
199
|
-
|
|
200
244
|
/** Web search tool as AgentTool (for allTools export) */
|
|
201
245
|
export const webSearchTool: AgentTool<typeof webSearchSchema> = {
|
|
202
246
|
name: "web_search",
|
|
203
247
|
label: "Web Search",
|
|
204
|
-
description:
|
|
248
|
+
description: webSearchDescription,
|
|
205
249
|
parameters: webSearchSchema,
|
|
206
250
|
execute: async (toolCallId, params) => {
|
|
207
251
|
return executeWebSearch(toolCallId, params as WebSearchParams);
|
|
@@ -212,7 +256,7 @@ export const webSearchTool: AgentTool<typeof webSearchSchema> = {
|
|
|
212
256
|
export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRenderDetails> = {
|
|
213
257
|
name: "web_search",
|
|
214
258
|
label: "Web Search",
|
|
215
|
-
description:
|
|
259
|
+
description: webSearchDescription,
|
|
216
260
|
parameters: webSearchSchema,
|
|
217
261
|
|
|
218
262
|
async execute(
|