@kata-sh/cli 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch_page tool — Extract clean markdown from any URL.
|
|
3
|
+
*
|
|
4
|
+
* v3 improvements:
|
|
5
|
+
* - offset parameter for continuation reading (like file read offsets)
|
|
6
|
+
* - selector parameter for Jina's X-Target-Selector (extract specific sections)
|
|
7
|
+
* - Jina failure diagnostics surfaced in details
|
|
8
|
+
* - Content-type awareness (JSON passthrough, PDF detection)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
14
|
+
import { Type } from "@sinclair/typebox";
|
|
15
|
+
|
|
16
|
+
import { LRUTTLCache } from "./cache";
|
|
17
|
+
import { fetchSimple, HttpError } from "./http";
|
|
18
|
+
import { extractDomain } from "./url-utils";
|
|
19
|
+
import { formatPageContent, type FormatPageOptions } from "./format";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Cache
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
interface CachedPage {
|
|
26
|
+
content: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
source: "jina" | "direct";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Page content cache: max 30 entries, 15-minute TTL
|
|
32
|
+
const pageCache = new LRUTTLCache<CachedPage>({ max: 30, ttlMs: 900_000 });
|
|
33
|
+
pageCache.startPurgeInterval(120_000);
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Jina Reader
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch page content via Jina Reader API.
|
|
41
|
+
* Returns content + metadata, or throws with a descriptive error.
|
|
42
|
+
*/
|
|
43
|
+
async function fetchViaJina(
|
|
44
|
+
url: string,
|
|
45
|
+
options: { signal?: AbortSignal; selector?: string } = {}
|
|
46
|
+
): Promise<{ content: string; title?: string }> {
|
|
47
|
+
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
48
|
+
|
|
49
|
+
const headers: Record<string, string> = {
|
|
50
|
+
"Accept": "text/plain",
|
|
51
|
+
"X-Return-Format": "markdown",
|
|
52
|
+
"X-No-Cache": "false",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Use Jina API key if available for higher rate limits
|
|
56
|
+
const jinaKey = process.env.JINA_API_KEY;
|
|
57
|
+
if (jinaKey) {
|
|
58
|
+
headers["Authorization"] = `Bearer ${jinaKey}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Target specific CSS selector on the page
|
|
62
|
+
if (options.selector) {
|
|
63
|
+
headers["X-Target-Selector"] = options.selector;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const response = await fetchSimple(jinaUrl, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
headers,
|
|
69
|
+
signal: options.signal,
|
|
70
|
+
timeoutMs: 20_000,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
|
|
75
|
+
// Jina returns markdown with a title line at the top
|
|
76
|
+
// Format: "Title: <title>\nURL Source: <url>\n\n<content>"
|
|
77
|
+
let title: string | undefined;
|
|
78
|
+
let content = text;
|
|
79
|
+
|
|
80
|
+
const titleMatch = text.match(/^Title:\s*(.+)\n/);
|
|
81
|
+
if (titleMatch) {
|
|
82
|
+
title = titleMatch[1].trim();
|
|
83
|
+
content = text.replace(/^Title:\s*.+\n/, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Strip the URL Source line
|
|
87
|
+
content = content.replace(/^URL Source:\s*.+\n\n?/, "");
|
|
88
|
+
|
|
89
|
+
// Strip Markdown images to save tokens
|
|
90
|
+
content = content.replace(/!\[([^\]]*)\]\([^)]+\)/g, "");
|
|
91
|
+
|
|
92
|
+
// Collapse excessive whitespace
|
|
93
|
+
content = content.replace(/\n{4,}/g, "\n\n\n");
|
|
94
|
+
|
|
95
|
+
return { content: content.trim(), title };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Basic fallback: fetch raw HTML and do crude text extraction.
|
|
100
|
+
*/
|
|
101
|
+
async function fetchDirectFallback(
|
|
102
|
+
url: string,
|
|
103
|
+
signal?: AbortSignal
|
|
104
|
+
): Promise<{ content: string; title?: string; contentType?: string }> {
|
|
105
|
+
const response = await fetchSimple(url, {
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
"Accept": "text/html,application/xhtml+xml,application/json,text/plain",
|
|
109
|
+
"User-Agent": "Mozilla/5.0 (compatible; pi-coding-agent/1.0)",
|
|
110
|
+
},
|
|
111
|
+
signal,
|
|
112
|
+
timeoutMs: 15_000,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const contentType = response.headers.get("content-type") || "";
|
|
116
|
+
|
|
117
|
+
// JSON passthrough — return formatted JSON directly
|
|
118
|
+
if (contentType.includes("application/json")) {
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(text);
|
|
122
|
+
return {
|
|
123
|
+
content: "```json\n" + JSON.stringify(parsed, null, 2) + "\n```",
|
|
124
|
+
title: undefined,
|
|
125
|
+
contentType: "application/json",
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return { content: text, title: undefined, contentType };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Plain text passthrough
|
|
133
|
+
if (contentType.includes("text/plain")) {
|
|
134
|
+
const text = await response.text();
|
|
135
|
+
return { content: text, title: undefined, contentType: "text/plain" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// PDF detection — can't extract, but tell the agent
|
|
139
|
+
if (contentType.includes("application/pdf")) {
|
|
140
|
+
return {
|
|
141
|
+
content: "[This URL is a PDF document. Content extraction is not supported for PDFs.]",
|
|
142
|
+
title: undefined,
|
|
143
|
+
contentType: "application/pdf",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const html = await response.text();
|
|
148
|
+
|
|
149
|
+
// Extract title
|
|
150
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
151
|
+
const title = titleMatch ? titleMatch[1].trim() : undefined;
|
|
152
|
+
|
|
153
|
+
// Strip tags, decode entities, collapse whitespace
|
|
154
|
+
let text = html
|
|
155
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
156
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
157
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, "")
|
|
158
|
+
.replace(/<header[\s\S]*?<\/header>/gi, "")
|
|
159
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, "")
|
|
160
|
+
.replace(/<\/?(p|div|br|h[1-6]|li|tr|blockquote|pre|section|article)[^>]*>/gi, "\n")
|
|
161
|
+
.replace(/<[^>]+>/g, " ")
|
|
162
|
+
.replace(/&/g, "&")
|
|
163
|
+
.replace(/</g, "<")
|
|
164
|
+
.replace(/>/g, ">")
|
|
165
|
+
.replace(/"/g, '"')
|
|
166
|
+
.replace(/'/g, "'")
|
|
167
|
+
.replace(/ /g, " ")
|
|
168
|
+
.replace(/[ \t]+/g, " ")
|
|
169
|
+
.replace(/\n[ \t]+/g, "\n")
|
|
170
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
171
|
+
.trim();
|
|
172
|
+
|
|
173
|
+
return { content: text, title, contentType };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Smart Truncation
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Truncate page content to a target character count, trying to break
|
|
182
|
+
* at paragraph boundaries rather than mid-sentence.
|
|
183
|
+
*/
|
|
184
|
+
function smartTruncate(
|
|
185
|
+
content: string,
|
|
186
|
+
maxChars: number,
|
|
187
|
+
offset: number = 0
|
|
188
|
+
): { content: string; truncated: boolean; hasMore: boolean; nextOffset?: number } {
|
|
189
|
+
// Apply offset first
|
|
190
|
+
const sliced = offset > 0 ? content.slice(offset) : content;
|
|
191
|
+
|
|
192
|
+
if (sliced.length <= maxChars) {
|
|
193
|
+
return { content: sliced, truncated: false, hasMore: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Find the last paragraph break before maxChars
|
|
197
|
+
const window = sliced.slice(0, maxChars);
|
|
198
|
+
const lastParagraph = window.lastIndexOf("\n\n");
|
|
199
|
+
const lastSentence = window.lastIndexOf(". ");
|
|
200
|
+
const lastNewline = window.lastIndexOf("\n");
|
|
201
|
+
|
|
202
|
+
// Prefer paragraph > sentence > newline > hard cut
|
|
203
|
+
let cutPoint = maxChars;
|
|
204
|
+
if (lastParagraph > maxChars * 0.6) {
|
|
205
|
+
cutPoint = lastParagraph;
|
|
206
|
+
} else if (lastSentence > maxChars * 0.6) {
|
|
207
|
+
cutPoint = lastSentence + 1;
|
|
208
|
+
} else if (lastNewline > maxChars * 0.6) {
|
|
209
|
+
cutPoint = lastNewline;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const nextOffset = offset + cutPoint;
|
|
213
|
+
const hasMore = nextOffset < content.length;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
content: sliced.slice(0, cutPoint).trim() + "\n\n[... content truncated]",
|
|
217
|
+
truncated: true,
|
|
218
|
+
hasMore,
|
|
219
|
+
nextOffset: hasMore ? nextOffset : undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// Single page fetch (shared between single and multi modes)
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
interface FetchPageResult {
|
|
228
|
+
content: string;
|
|
229
|
+
title?: string;
|
|
230
|
+
source: "jina" | "direct";
|
|
231
|
+
jinaError?: string;
|
|
232
|
+
contentType?: string;
|
|
233
|
+
originalChars: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function fetchOnePage(
|
|
237
|
+
url: string,
|
|
238
|
+
options: { signal?: AbortSignal; selector?: string }
|
|
239
|
+
): Promise<FetchPageResult> {
|
|
240
|
+
let pageContent: string;
|
|
241
|
+
let pageTitle: string | undefined;
|
|
242
|
+
let source: "jina" | "direct" = "jina";
|
|
243
|
+
let jinaError: string | undefined;
|
|
244
|
+
let contentType: string | undefined;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await fetchViaJina(url, options);
|
|
248
|
+
pageContent = result.content;
|
|
249
|
+
pageTitle = result.title;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// Capture Jina failure reason for diagnostics
|
|
252
|
+
jinaError = err instanceof HttpError
|
|
253
|
+
? `Jina HTTP ${err.statusCode}`
|
|
254
|
+
: (err as Error).message ?? String(err);
|
|
255
|
+
source = "direct";
|
|
256
|
+
|
|
257
|
+
const result = await fetchDirectFallback(url, options.signal);
|
|
258
|
+
pageContent = result.content;
|
|
259
|
+
pageTitle = result.title;
|
|
260
|
+
contentType = result.contentType;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
content: pageContent,
|
|
265
|
+
title: pageTitle,
|
|
266
|
+
source,
|
|
267
|
+
jinaError,
|
|
268
|
+
contentType,
|
|
269
|
+
originalChars: pageContent.length,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Details Interface
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
interface FetchPageDetails {
|
|
278
|
+
url: string;
|
|
279
|
+
title?: string;
|
|
280
|
+
charCount: number;
|
|
281
|
+
originalChars?: number;
|
|
282
|
+
truncated: boolean;
|
|
283
|
+
cached: boolean;
|
|
284
|
+
source?: "jina" | "direct";
|
|
285
|
+
jinaError?: string;
|
|
286
|
+
contentType?: string;
|
|
287
|
+
hasMore?: boolean;
|
|
288
|
+
nextOffset?: number;
|
|
289
|
+
offset?: number;
|
|
290
|
+
selector?: string;
|
|
291
|
+
error?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// Tool Registration
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
export function registerFetchPageTool(pi: ExtensionAPI) {
|
|
299
|
+
pi.registerTool({
|
|
300
|
+
name: "fetch_page",
|
|
301
|
+
label: "Fetch Page",
|
|
302
|
+
description:
|
|
303
|
+
"Fetch a web page and extract its content as clean markdown. " +
|
|
304
|
+
"Use this to read the full content of URLs found via search-the-web. " +
|
|
305
|
+
"Uses Jina Reader for high-quality markdown extraction. " +
|
|
306
|
+
"Control the amount of content returned with maxChars (default: 8000, max: 30000).",
|
|
307
|
+
promptSnippet: "Fetch and extract clean content from a web page URL as markdown",
|
|
308
|
+
promptGuidelines: [
|
|
309
|
+
"Use fetch_page to read the content of URLs found via search-the-web when you need more detail than snippets provide.",
|
|
310
|
+
"Start with the default maxChars (8000). Increase only if the first fetch lacks the detail you need.",
|
|
311
|
+
"For very long pages, use a smaller maxChars and increase if needed — this saves context tokens.",
|
|
312
|
+
"The extracted content is already clean markdown — no HTML tags, no navigation, no ads.",
|
|
313
|
+
],
|
|
314
|
+
parameters: Type.Object({
|
|
315
|
+
url: Type.String({ description: "URL to fetch and extract content from" }),
|
|
316
|
+
maxChars: Type.Optional(
|
|
317
|
+
Type.Number({
|
|
318
|
+
minimum: 1000,
|
|
319
|
+
maximum: 30000,
|
|
320
|
+
default: 8000,
|
|
321
|
+
description: "Maximum characters of content to return (default: 8000, max: 30000). Controls context token usage.",
|
|
322
|
+
})
|
|
323
|
+
),
|
|
324
|
+
offset: Type.Optional(
|
|
325
|
+
Type.Number({
|
|
326
|
+
minimum: 0,
|
|
327
|
+
description: "Character offset to start reading from (for continuation of truncated pages). Use the nextOffset value from a previous fetch_page result.",
|
|
328
|
+
})
|
|
329
|
+
),
|
|
330
|
+
selector: Type.Optional(
|
|
331
|
+
Type.String({
|
|
332
|
+
description: "CSS selector to extract only a specific section of the page (e.g., 'main', 'article', '.api-docs'). Reduces noise and token usage.",
|
|
333
|
+
})
|
|
334
|
+
),
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
338
|
+
if (signal?.aborted) {
|
|
339
|
+
return { content: [{ type: "text", text: "Fetch cancelled." }] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const maxChars = params.maxChars ?? 8000;
|
|
343
|
+
const offset = params.offset ?? 0;
|
|
344
|
+
const url = params.url.trim();
|
|
345
|
+
|
|
346
|
+
// Validate URL
|
|
347
|
+
try {
|
|
348
|
+
new URL(url);
|
|
349
|
+
} catch {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: `Invalid URL: ${url}` }],
|
|
352
|
+
isError: true,
|
|
353
|
+
details: { error: "Invalid URL", url } satisfies Partial<FetchPageDetails>,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ------------------------------------------------------------------
|
|
358
|
+
// Cache lookup (full content cached, offset/truncation applied after)
|
|
359
|
+
// ------------------------------------------------------------------
|
|
360
|
+
const cacheKey = params.selector ? `${url}|sel:${params.selector}` : url;
|
|
361
|
+
const cached = pageCache.get(cacheKey);
|
|
362
|
+
|
|
363
|
+
if (cached) {
|
|
364
|
+
const trunc = smartTruncate(cached.content, maxChars, offset);
|
|
365
|
+
const opts: FormatPageOptions = {
|
|
366
|
+
title: cached.title,
|
|
367
|
+
charCount: trunc.content.length,
|
|
368
|
+
truncated: trunc.truncated,
|
|
369
|
+
originalChars: trunc.truncated ? cached.content.length : undefined,
|
|
370
|
+
hasMore: trunc.hasMore,
|
|
371
|
+
nextOffset: trunc.nextOffset,
|
|
372
|
+
};
|
|
373
|
+
const output = formatPageContent(url, trunc.content, opts);
|
|
374
|
+
|
|
375
|
+
const finalTruncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
376
|
+
const details: FetchPageDetails = {
|
|
377
|
+
url,
|
|
378
|
+
title: cached.title,
|
|
379
|
+
charCount: trunc.content.length,
|
|
380
|
+
originalChars: cached.content.length,
|
|
381
|
+
truncated: trunc.truncated,
|
|
382
|
+
cached: true,
|
|
383
|
+
source: cached.source,
|
|
384
|
+
hasMore: trunc.hasMore,
|
|
385
|
+
nextOffset: trunc.nextOffset,
|
|
386
|
+
offset: offset || undefined,
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: finalTruncation.content }],
|
|
390
|
+
details,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const domain = extractDomain(url);
|
|
395
|
+
onUpdate?.({ content: [{ type: "text", text: `Fetching ${domain}...` }] });
|
|
396
|
+
|
|
397
|
+
// ------------------------------------------------------------------
|
|
398
|
+
// Fetch page content
|
|
399
|
+
// ------------------------------------------------------------------
|
|
400
|
+
let result: FetchPageResult;
|
|
401
|
+
try {
|
|
402
|
+
result = await fetchOnePage(url, { signal, selector: params.selector });
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const message = err instanceof HttpError
|
|
405
|
+
? `HTTP ${err.statusCode}`
|
|
406
|
+
: (err as Error).message ?? String(err);
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: "text", text: `Failed to fetch ${domain}: ${message}` }],
|
|
409
|
+
isError: true,
|
|
410
|
+
details: { error: message, url } satisfies Partial<FetchPageDetails>,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check for empty content
|
|
415
|
+
if (!result.content || result.content.length < 50) {
|
|
416
|
+
return {
|
|
417
|
+
content: [{ type: "text", text: `Page at ${domain} returned no extractable content.` }],
|
|
418
|
+
details: { url, charCount: 0, source: result.source, cached: false, truncated: false, jinaError: result.jinaError } satisfies FetchPageDetails,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Cache the full content
|
|
423
|
+
pageCache.set(cacheKey, { content: result.content, title: result.title, source: result.source });
|
|
424
|
+
|
|
425
|
+
// Smart truncate with offset
|
|
426
|
+
const trunc = smartTruncate(result.content, maxChars, offset);
|
|
427
|
+
|
|
428
|
+
const opts: FormatPageOptions = {
|
|
429
|
+
title: result.title,
|
|
430
|
+
charCount: trunc.content.length,
|
|
431
|
+
truncated: trunc.truncated,
|
|
432
|
+
originalChars: trunc.truncated ? result.originalChars : undefined,
|
|
433
|
+
hasMore: trunc.hasMore,
|
|
434
|
+
nextOffset: trunc.nextOffset,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const output = formatPageContent(url, trunc.content, opts);
|
|
438
|
+
|
|
439
|
+
const finalTruncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
440
|
+
let content = finalTruncation.content;
|
|
441
|
+
if (finalTruncation.truncated) {
|
|
442
|
+
const tempFile = await pi.writeTempFile(output, { prefix: "fetch-page-" });
|
|
443
|
+
content += `\n\n[Truncated to fit context. Full content: ${tempFile}]`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const details: FetchPageDetails = {
|
|
447
|
+
url,
|
|
448
|
+
title: result.title,
|
|
449
|
+
charCount: trunc.content.length,
|
|
450
|
+
originalChars: result.originalChars,
|
|
451
|
+
truncated: trunc.truncated,
|
|
452
|
+
cached: false,
|
|
453
|
+
source: result.source,
|
|
454
|
+
jinaError: result.jinaError,
|
|
455
|
+
contentType: result.contentType,
|
|
456
|
+
hasMore: trunc.hasMore,
|
|
457
|
+
nextOffset: trunc.nextOffset,
|
|
458
|
+
offset: offset || undefined,
|
|
459
|
+
selector: params.selector,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
content: [{ type: "text", text: content }],
|
|
464
|
+
details,
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
renderCall(args, theme) {
|
|
469
|
+
const domain = extractDomain(args.url);
|
|
470
|
+
let text = theme.fg("toolTitle", theme.bold("fetch_page "));
|
|
471
|
+
text += theme.fg("accent", domain);
|
|
472
|
+
|
|
473
|
+
const meta: string[] = [];
|
|
474
|
+
if (args.maxChars && args.maxChars !== 8000) meta.push(`max ${(args.maxChars / 1000).toFixed(0)}k`);
|
|
475
|
+
if (args.offset) meta.push(`offset:${args.offset}`);
|
|
476
|
+
if (args.selector) meta.push(`sel:"${args.selector}"`);
|
|
477
|
+
if (meta.length > 0) {
|
|
478
|
+
text += " " + theme.fg("dim", `(${meta.join(", ")})`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return new Text(text, 0, 0);
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
renderResult(result, { expanded }, theme) {
|
|
485
|
+
const details = result.details as FetchPageDetails | undefined;
|
|
486
|
+
if (details?.error) {
|
|
487
|
+
return new Text(theme.fg("error", `✗ ${details.error}`), 0, 0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const domain = extractDomain(details?.url || "");
|
|
491
|
+
const title = details?.title ? ` — ${details.title}` : "";
|
|
492
|
+
const chars = details?.charCount ? `${(details.charCount / 1000).toFixed(1)}k chars` : "";
|
|
493
|
+
const cacheTag = details?.cached ? theme.fg("dim", " [cached]") : "";
|
|
494
|
+
const sourceTag = details?.source === "direct" ? theme.fg("dim", " [direct]") : "";
|
|
495
|
+
const truncTag = details?.truncated && details?.originalChars
|
|
496
|
+
? theme.fg("dim", ` [${(details.originalChars / 1000).toFixed(0)}k total]`)
|
|
497
|
+
: "";
|
|
498
|
+
const moreTag = details?.hasMore && details?.nextOffset
|
|
499
|
+
? theme.fg("accent", ` [more→offset:${details.nextOffset}]`)
|
|
500
|
+
: "";
|
|
501
|
+
const jinaTag = details?.jinaError
|
|
502
|
+
? theme.fg("warning", ` [jina failed: ${details.jinaError}]`)
|
|
503
|
+
: "";
|
|
504
|
+
|
|
505
|
+
let text = theme.fg("success", `✓ ${domain}${title}`) + ` ${chars}` +
|
|
506
|
+
cacheTag + sourceTag + truncTag + moreTag + jinaTag;
|
|
507
|
+
|
|
508
|
+
if (expanded) {
|
|
509
|
+
const content = result.content[0];
|
|
510
|
+
if (content?.type === "text") {
|
|
511
|
+
const preview = content.text.split("\n").slice(0, 8).join("\n");
|
|
512
|
+
text += "\n\n" + theme.fg("dim", preview);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return new Text(text, 0, 0);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|