@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.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,404 @@
1
+ /**
2
+ * search_and_read tool — Brave LLM Context API.
3
+ *
4
+ * Single-call web search + page content extraction optimized for AI agents.
5
+ * Unlike search-the-web → fetch_page (two steps), this returns pre-extracted,
6
+ * relevance-scored page content in one API call.
7
+ *
8
+ * Best for: "I need to know about X" — when you want content, not just links.
9
+ * Use search-the-web when you want links/URLs to browse selectively.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+ import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@mariozechner/pi-coding-agent";
14
+ import { Text } from "@mariozechner/pi-tui";
15
+ import { Type } from "@sinclair/typebox";
16
+ import { StringEnum } from "@mariozechner/pi-ai";
17
+
18
+ import { LRUTTLCache } from "./cache";
19
+ import { fetchWithRetryTimed, HttpError, classifyError, type RateLimitInfo } from "./http";
20
+ import { normalizeQuery, extractDomain } from "./url-utils";
21
+ import { formatLLMContext, type LLMContextSnippet, type LLMContextSource } from "./format";
22
+
23
+ // =============================================================================
24
+ // Types
25
+ // =============================================================================
26
+
27
+ interface BraveLLMContextResponse {
28
+ grounding?: {
29
+ generic?: Array<{
30
+ url: string;
31
+ title: string;
32
+ snippets: string[];
33
+ }>;
34
+ poi?: {
35
+ name: string;
36
+ url: string;
37
+ title: string;
38
+ snippets: string[];
39
+ } | null;
40
+ map?: Array<{
41
+ name: string;
42
+ url: string;
43
+ title: string;
44
+ snippets: string[];
45
+ }>;
46
+ };
47
+ sources?: Record<string, {
48
+ title: string;
49
+ hostname: string;
50
+ age: string[] | null;
51
+ }>;
52
+ }
53
+
54
+ interface CachedLLMContext {
55
+ grounding: LLMContextSnippet[];
56
+ sources: Record<string, LLMContextSource>;
57
+ estimatedTokens: number;
58
+ }
59
+
60
+ interface LLMContextDetails {
61
+ query: string;
62
+ sourceCount: number;
63
+ snippetCount: number;
64
+ estimatedTokens: number;
65
+ cached: boolean;
66
+ latencyMs?: number;
67
+ rateLimit?: RateLimitInfo;
68
+ threshold?: string;
69
+ maxTokens?: number;
70
+ errorKind?: string;
71
+ error?: string;
72
+ retryAfterMs?: number;
73
+ }
74
+
75
+ // =============================================================================
76
+ // Cache
77
+ // =============================================================================
78
+
79
+ // LLM Context cache: max 50 entries, 10-minute TTL
80
+ const contextCache = new LRUTTLCache<CachedLLMContext>({ max: 50, ttlMs: 600_000 });
81
+ contextCache.startPurgeInterval(60_000);
82
+
83
+ // =============================================================================
84
+ // Helpers
85
+ // =============================================================================
86
+
87
+ function getBraveApiKey(): string {
88
+ return process.env.BRAVE_API_KEY || "";
89
+ }
90
+
91
+ function braveHeaders(): Record<string, string> {
92
+ return {
93
+ "Accept": "application/json",
94
+ "Accept-Encoding": "gzip",
95
+ "X-Subscription-Token": getBraveApiKey(),
96
+ };
97
+ }
98
+
99
+ /** Rough token estimate: ~4 chars per token for English text. */
100
+ function estimateTokens(text: string): number {
101
+ return Math.ceil(text.length / 4);
102
+ }
103
+
104
+ // =============================================================================
105
+ // Tool Registration
106
+ // =============================================================================
107
+
108
+ export function registerLLMContextTool(pi: ExtensionAPI) {
109
+ pi.registerTool({
110
+ name: "search_and_read",
111
+ label: "Search & Read",
112
+ description:
113
+ "Search the web AND read page content in a single call. Returns pre-extracted, " +
114
+ "relevance-scored text from multiple pages — no separate fetch_page needed. " +
115
+ "Powered by Brave's LLM Context API. Best when you need content, not just links. " +
116
+ "For selective URL browsing, use search-the-web + fetch_page instead.",
117
+ promptSnippet: "Search and read web page content in one step",
118
+ promptGuidelines: [
119
+ "Use search_and_read when you need actual page content about a topic — it searches and extracts in one call.",
120
+ "Prefer search_and_read over search-the-web + fetch_page when you just need to learn about something.",
121
+ "Use search-the-web when you need to browse specific URLs, control which pages to read, or want just links.",
122
+ "Start with the default maxTokens (8192). Use smaller values (2048-4096) for simple factual queries.",
123
+ "Use threshold='strict' for focused, high-relevance results. Use 'lenient' for broad coverage.",
124
+ ],
125
+ parameters: Type.Object({
126
+ query: Type.String({ description: "Search query — what you want to learn about" }),
127
+ maxTokens: Type.Optional(
128
+ Type.Number({
129
+ minimum: 1024,
130
+ maximum: 32768,
131
+ default: 8192,
132
+ description: "Approximate maximum tokens of content to return (default: 8192). Lower = faster + cheaper inference.",
133
+ })
134
+ ),
135
+ maxUrls: Type.Optional(
136
+ Type.Number({
137
+ minimum: 1,
138
+ maximum: 20,
139
+ default: 10,
140
+ description: "Maximum number of source URLs to include (default: 10).",
141
+ })
142
+ ),
143
+ threshold: Type.Optional(
144
+ StringEnum(["strict", "balanced", "lenient"] as const, {
145
+ description: "Relevance threshold. 'strict' = fewer but more relevant. 'balanced' (default). 'lenient' = broader coverage.",
146
+ })
147
+ ),
148
+ count: Type.Optional(
149
+ Type.Number({
150
+ minimum: 1,
151
+ maximum: 50,
152
+ default: 20,
153
+ description: "Maximum search results to consider (default: 20). More = broader but slower.",
154
+ })
155
+ ),
156
+ }),
157
+
158
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
159
+ if (signal?.aborted) {
160
+ return { content: [{ type: "text", text: "Search cancelled." }] };
161
+ }
162
+
163
+ const apiKey = getBraveApiKey();
164
+ if (!apiKey) {
165
+ return {
166
+ content: [{ type: "text", text: "Search unavailable: BRAVE_API_KEY is not set. Use secure_env_collect to set BRAVE_API_KEY." }],
167
+ isError: true,
168
+ details: { errorKind: "auth_error", error: "BRAVE_API_KEY not set" } satisfies Partial<LLMContextDetails>,
169
+ };
170
+ }
171
+
172
+ const maxTokens = params.maxTokens ?? 8192;
173
+ const maxUrls = params.maxUrls ?? 10;
174
+ const threshold = params.threshold ?? "balanced";
175
+ const count = params.count ?? 20;
176
+
177
+ // ------------------------------------------------------------------
178
+ // Cache lookup
179
+ // ------------------------------------------------------------------
180
+ const cacheKey = normalizeQuery(params.query) + `|t:${maxTokens}|u:${maxUrls}|th:${threshold}|c:${count}`;
181
+ const cached = contextCache.get(cacheKey);
182
+
183
+ if (cached) {
184
+ const output = formatLLMContext(params.query, cached.grounding, cached.sources, {
185
+ cached: true,
186
+ tokenCount: cached.estimatedTokens,
187
+ });
188
+
189
+ const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
190
+ let content = truncation.content;
191
+ if (truncation.truncated) {
192
+ const tempFile = await pi.writeTempFile(output, { prefix: "llm-context-" });
193
+ content += `\n\n[Truncated. Full content: ${tempFile}]`;
194
+ }
195
+
196
+ const totalSnippets = cached.grounding.reduce((sum, g) => sum + g.snippets.length, 0);
197
+ const details: LLMContextDetails = {
198
+ query: params.query,
199
+ sourceCount: cached.grounding.length,
200
+ snippetCount: totalSnippets,
201
+ estimatedTokens: cached.estimatedTokens,
202
+ cached: true,
203
+ threshold,
204
+ maxTokens,
205
+ };
206
+
207
+ return { content: [{ type: "text", text: content }], details };
208
+ }
209
+
210
+ onUpdate?.({ content: [{ type: "text", text: `Searching & reading about "${params.query}"...` }] });
211
+
212
+ try {
213
+ // ------------------------------------------------------------------
214
+ // Build LLM Context API request
215
+ // ------------------------------------------------------------------
216
+ const url = new URL("https://api.search.brave.com/res/v1/llm/context");
217
+ url.searchParams.append("q", params.query);
218
+ url.searchParams.append("count", String(count));
219
+ url.searchParams.append("maximum_number_of_tokens", String(maxTokens));
220
+ url.searchParams.append("maximum_number_of_urls", String(maxUrls));
221
+ url.searchParams.append("context_threshold_mode", threshold);
222
+
223
+ // Use a custom fetch flow to read error bodies from the Brave API
224
+ let timed;
225
+ try {
226
+ timed = await fetchWithRetryTimed(url.toString(), {
227
+ method: "GET",
228
+ headers: braveHeaders(),
229
+ signal,
230
+ }, 2);
231
+ } catch (fetchErr) {
232
+ // Try to extract Brave's structured error detail from the response body.
233
+ // This is especially useful for plan/subscription errors (OPTION_NOT_IN_PLAN).
234
+ let errorMessage: string | undefined;
235
+ let errorKindOverride: string | undefined;
236
+ if (fetchErr instanceof HttpError && fetchErr.response) {
237
+ try {
238
+ const body = await fetchErr.response.clone().json().catch(() => null);
239
+ if (body?.error?.detail) {
240
+ errorMessage = body.error.detail;
241
+ if (body.error.code === "OPTION_NOT_IN_PLAN") {
242
+ errorKindOverride = "plan_error";
243
+ errorMessage = `LLM Context API not available on your current Brave plan. ${body.error.detail} Upgrade at https://api-dashboard.search.brave.com/app/subscriptions — or use search-the-web + fetch_page as an alternative.`;
244
+ }
245
+ }
246
+ } catch { /* body already consumed or parse error — use generic message */ }
247
+ }
248
+ const classified = classifyError(fetchErr);
249
+ const message = errorMessage || classified.message;
250
+ return {
251
+ content: [{ type: "text", text: `search_and_read unavailable: ${message}` }],
252
+ details: {
253
+ errorKind: errorKindOverride || classified.kind,
254
+ error: message,
255
+ retryAfterMs: classified.retryAfterMs,
256
+ query: params.query,
257
+ } satisfies Partial<LLMContextDetails>,
258
+ isError: true,
259
+ };
260
+ }
261
+
262
+ const data: BraveLLMContextResponse = await timed.response.json();
263
+
264
+ // ------------------------------------------------------------------
265
+ // Normalize response
266
+ // ------------------------------------------------------------------
267
+ const grounding: LLMContextSnippet[] = [];
268
+
269
+ if (data.grounding?.generic) {
270
+ for (const item of data.grounding.generic) {
271
+ if (item.snippets && item.snippets.length > 0) {
272
+ grounding.push({
273
+ url: item.url,
274
+ title: item.title,
275
+ snippets: item.snippets,
276
+ });
277
+ }
278
+ }
279
+ }
280
+
281
+ // Include POI data if present
282
+ if (data.grounding?.poi && data.grounding.poi.snippets?.length) {
283
+ grounding.push({
284
+ url: data.grounding.poi.url,
285
+ title: data.grounding.poi.title || data.grounding.poi.name,
286
+ snippets: data.grounding.poi.snippets,
287
+ });
288
+ }
289
+
290
+ // Include map data if present
291
+ if (data.grounding?.map) {
292
+ for (const item of data.grounding.map) {
293
+ if (item.snippets?.length) {
294
+ grounding.push({
295
+ url: item.url,
296
+ title: item.title || item.name,
297
+ snippets: item.snippets,
298
+ });
299
+ }
300
+ }
301
+ }
302
+
303
+ const sources: Record<string, LLMContextSource> = {};
304
+ if (data.sources) {
305
+ for (const [sourceUrl, sourceInfo] of Object.entries(data.sources)) {
306
+ sources[sourceUrl] = {
307
+ title: sourceInfo.title,
308
+ hostname: sourceInfo.hostname,
309
+ age: sourceInfo.age,
310
+ };
311
+ }
312
+ }
313
+
314
+ // Estimate total token count from all snippets
315
+ const allText = grounding.map(g => g.snippets.join(" ")).join(" ");
316
+ const estimatedTokens = estimateTokens(allText);
317
+
318
+ // Cache the results
319
+ contextCache.set(cacheKey, { grounding, sources, estimatedTokens });
320
+
321
+ // ------------------------------------------------------------------
322
+ // Format output
323
+ // ------------------------------------------------------------------
324
+ const output = formatLLMContext(params.query, grounding, sources, {
325
+ tokenCount: estimatedTokens,
326
+ });
327
+
328
+ const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
329
+ let content = truncation.content;
330
+
331
+ if (truncation.truncated) {
332
+ const tempFile = await pi.writeTempFile(output, { prefix: "llm-context-" });
333
+ content += `\n\n[Truncated. Full content: ${tempFile}]`;
334
+ }
335
+
336
+ const totalSnippets = grounding.reduce((sum, g) => sum + g.snippets.length, 0);
337
+ const details: LLMContextDetails = {
338
+ query: params.query,
339
+ sourceCount: grounding.length,
340
+ snippetCount: totalSnippets,
341
+ estimatedTokens,
342
+ cached: false,
343
+ latencyMs: timed.latencyMs,
344
+ rateLimit: timed.rateLimit,
345
+ threshold,
346
+ maxTokens,
347
+ };
348
+
349
+ return { content: [{ type: "text", text: content }], details };
350
+ } catch (error) {
351
+ const classified = classifyError(error);
352
+ return {
353
+ content: [{ type: "text", text: `Search failed: ${classified.message}` }],
354
+ details: {
355
+ errorKind: classified.kind,
356
+ error: classified.message,
357
+ query: params.query,
358
+ } satisfies Partial<LLMContextDetails>,
359
+ isError: true,
360
+ };
361
+ }
362
+ },
363
+
364
+ renderCall(args, theme) {
365
+ let text = theme.fg("toolTitle", theme.bold("search_and_read "));
366
+ text += theme.fg("muted", `"${args.query}"`);
367
+
368
+ const meta: string[] = [];
369
+ if (args.maxTokens && args.maxTokens !== 8192) meta.push(`${(args.maxTokens / 1000).toFixed(0)}k tokens`);
370
+ if (args.threshold && args.threshold !== "balanced") meta.push(`threshold:${args.threshold}`);
371
+ if (args.maxUrls && args.maxUrls !== 10) meta.push(`${args.maxUrls} urls`);
372
+ if (meta.length > 0) {
373
+ text += " " + theme.fg("dim", `(${meta.join(", ")})`);
374
+ }
375
+
376
+ return new Text(text, 0, 0);
377
+ },
378
+
379
+ renderResult(result, { expanded }, theme) {
380
+ const details = result.details as LLMContextDetails | undefined;
381
+ if (details?.errorKind || details?.error) {
382
+ const kindTag = details.errorKind ? theme.fg("dim", ` [${details.errorKind}]`) : "";
383
+ return new Text(theme.fg("error", `✗ ${details.error ?? "Search failed"}`) + kindTag, 0, 0);
384
+ }
385
+
386
+ const cacheTag = details?.cached ? theme.fg("dim", " [cached]") : "";
387
+ const latencyTag = details?.latencyMs ? theme.fg("dim", ` ${details.latencyMs}ms`) : "";
388
+ const tokenTag = details?.estimatedTokens
389
+ ? theme.fg("dim", ` ~${(details.estimatedTokens / 1000).toFixed(1)}k tokens`)
390
+ : "";
391
+
392
+ let text = theme.fg("success",
393
+ `✓ ${details?.sourceCount ?? 0} sources, ${details?.snippetCount ?? 0} snippets for "${details?.query}"`) +
394
+ tokenTag + cacheTag + latencyTag;
395
+
396
+ if (expanded && result.content[0]?.type === "text") {
397
+ const preview = result.content[0].text.split("\n").slice(0, 10).join("\n");
398
+ text += "\n\n" + theme.fg("dim", preview);
399
+ }
400
+
401
+ return new Text(text, 0, 0);
402
+ },
403
+ });
404
+ }