jeo-code 0.1.0 → 0.4.4

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 (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +804 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +562 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -0,0 +1,538 @@
1
+ /**
2
+ * web_search — provider-chain web search (gjc parity).
3
+ *
4
+ * Mirrors gjc's `web/search` design instead of hardcoding one backend:
5
+ *
6
+ * 1. An explicitly preferred provider (`JEO_SEARCH_PROVIDER`) that is
7
+ * available is primary.
8
+ * 2. Otherwise the ACTIVE MODEL's own native search provider is primary,
9
+ * but only when that provider's credentials are present (active-model
10
+ * gating — never credential scanning). Today that maps Anthropic models
11
+ * to Anthropic's server-side `web_search_20250305` tool.
12
+ * 3. DuckDuckGo (keyless HTML scraping, no API key, no OAuth) is ALWAYS
13
+ * appended as the terminal fallback, so a missing/failed primary still
14
+ * returns real results with zero configuration — results are always
15
+ * live provider data, never canned.
16
+ *
17
+ * Providers are tried in order; a runtime failure fails over to the next.
18
+ * The structured text output doubles as the source for the TUI's gjc-style
19
+ * Web Search card (Query / Answer / Sources / Metadata sections — see
20
+ * `webSearchCardLines` in tui/components/forge.ts).
21
+ */
22
+ import { createHash, randomBytes } from "node:crypto";
23
+ import { resolveCredential, type Credential } from "../auth";
24
+ import { jeoEnv } from "../util/env";
25
+ import type { ToolResult } from "./tools";
26
+
27
+ // ── Unified response shape ───────────────────────────────────────────────────
28
+
29
+ export interface WebSearchSource {
30
+ title: string;
31
+ url: string;
32
+ snippet?: string;
33
+ publishedDate?: string;
34
+ }
35
+
36
+ export interface WebSearchResponse {
37
+ /** Display label of the provider that actually produced this response. */
38
+ provider: string;
39
+ answer?: string;
40
+ sources: WebSearchSource[];
41
+ citations: { url: string; title?: string; citedText?: string }[];
42
+ searchQueries: string[];
43
+ usage?: { inputTokens?: number; outputTokens?: number; searchRequests?: number };
44
+ model?: string;
45
+ requestId?: string;
46
+ }
47
+
48
+ export interface SearchRequest {
49
+ query: string;
50
+ recency?: "day" | "week" | "month" | "year";
51
+ limit?: number;
52
+ maxTokens?: number;
53
+ }
54
+
55
+ export interface SearchProviderDef {
56
+ id: "anthropic" | "duckduckgo";
57
+ label: string;
58
+ /** Credential gate — keyless providers return true unconditionally. */
59
+ isAvailable(): Promise<boolean> | boolean;
60
+ /** Throws on failure; the chain runner fails over to the next provider. */
61
+ search(req: SearchRequest): Promise<WebSearchResponse>;
62
+ }
63
+
64
+ // ── Anthropic provider (server-side web_search tool) ─────────────────────────
65
+
66
+ const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
67
+ const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
68
+ const DEFAULT_SEARCH_MODEL = "claude-haiku-4-5";
69
+ const DEFAULT_MAX_TOKENS = 4096;
70
+ const HARD_TIMEOUT_MS = 90_000;
71
+ const SEARCH_SYSTEM_PROMPT =
72
+ "You are a research assistant with web search. Answer the query using web search, be concise, and ground every claim in the searched sources.";
73
+
74
+ // Claude Code OAuth request shape (mirrors src/ai/providers/anthropic.ts —
75
+ // Anthropic OAuth tokens are scoped to Claude Code-shaped requests).
76
+ const CLAUDE_CODE_VERSION = "2.1.63";
77
+ const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
78
+ const ANTHROPIC_OAUTH_BETA = [
79
+ "claude-code-20250219",
80
+ "oauth-2025-04-20",
81
+ "interleaved-thinking-2025-05-14",
82
+ ].join(",");
83
+
84
+ function anthropicSearchHeaders(credential: Credential): Record<string, string> {
85
+ if (credential.kind === "oauth") {
86
+ return {
87
+ "content-type": "application/json",
88
+ accept: "application/json",
89
+ authorization: `Bearer ${credential.token}`,
90
+ "anthropic-version": "2023-06-01",
91
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
92
+ "anthropic-dangerous-direct-browser-access": "true",
93
+ "user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
94
+ "x-app": "cli",
95
+ };
96
+ }
97
+ return {
98
+ "content-type": "application/json",
99
+ accept: "application/json",
100
+ "x-api-key": credential.kind === "api_key" ? credential.token : "",
101
+ "anthropic-version": "2023-06-01",
102
+ };
103
+ }
104
+
105
+ function billingHeaderBlock(systemPrompt: string): string {
106
+ const cch = createHash("sha256").update(JSON.stringify({ system: [systemPrompt] })).digest("hex").slice(0, 5);
107
+ const buildHash = randomBytes(2).toString("hex").slice(0, 3);
108
+ return `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION}.${buildHash}; cc_entrypoint=cli; cch=${cch};`;
109
+ }
110
+
111
+ function anthropicSystemBlocks(credential: Credential): { type: "text"; text: string }[] {
112
+ const blocks: { type: "text"; text: string }[] = [];
113
+ if (credential.kind === "oauth") {
114
+ blocks.push(
115
+ { type: "text", text: billingHeaderBlock(SEARCH_SYSTEM_PROMPT) },
116
+ { type: "text", text: CLAUDE_CODE_SYSTEM_INSTRUCTION },
117
+ );
118
+ }
119
+ blocks.push({ type: "text", text: SEARCH_SYSTEM_PROMPT });
120
+ return blocks;
121
+ }
122
+
123
+ async function resolveAnthropicSearchCredential(): Promise<Credential> {
124
+ const dedicated = jeoEnv("SEARCH_API_KEY");
125
+ if (dedicated) return { kind: "api_key", provider: "anthropic", token: dedicated };
126
+ return resolveCredential("anthropic");
127
+ }
128
+
129
+ /** Parse the Anthropic Messages response (content blocks) into the unified shape. */
130
+ export function parseAnthropicSearchResponse(response: any): WebSearchResponse {
131
+ const answerParts: string[] = [];
132
+ const searchQueries: string[] = [];
133
+ const sources: WebSearchSource[] = [];
134
+ const citations: WebSearchResponse["citations"] = [];
135
+
136
+ for (const block of response?.content ?? []) {
137
+ if (block?.type === "server_tool_use" && typeof block.input?.query === "string") {
138
+ searchQueries.push(block.input.query);
139
+ } else if (block?.type === "web_search_tool_result" && Array.isArray(block.content)) {
140
+ for (const result of block.content) {
141
+ if (result?.type === "web_search_result" && typeof result.url === "string") {
142
+ sources.push({
143
+ title: typeof result.title === "string" && result.title.trim() ? result.title : result.url,
144
+ url: result.url,
145
+ publishedDate: typeof result.page_age === "string" ? result.page_age : undefined,
146
+ });
147
+ }
148
+ }
149
+ } else if (block?.type === "text" && typeof block.text === "string") {
150
+ answerParts.push(block.text);
151
+ for (const c of block.citations ?? []) {
152
+ if (typeof c?.url === "string") {
153
+ citations.push({ url: c.url, title: c.title, citedText: c.cited_text });
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ return {
160
+ provider: "Anthropic",
161
+ answer: answerParts.join("\n\n") || undefined,
162
+ sources,
163
+ citations,
164
+ searchQueries,
165
+ usage: response?.usage
166
+ ? {
167
+ inputTokens: response.usage.input_tokens,
168
+ outputTokens: response.usage.output_tokens,
169
+ searchRequests: response.usage.server_tool_use?.web_search_requests,
170
+ }
171
+ : undefined,
172
+ model: typeof response?.model === "string" ? response.model : undefined,
173
+ requestId: typeof response?.id === "string" ? response.id : undefined,
174
+ };
175
+ }
176
+
177
+ async function searchAnthropic(req: SearchRequest): Promise<WebSearchResponse> {
178
+ const credential = await resolveAnthropicSearchCredential();
179
+ if (credential.kind !== "oauth" && credential.kind !== "api_key") {
180
+ throw new Error("Anthropic: no credentials (run 'jeo auth login anthropic' or set ANTHROPIC_API_KEY / JEO_SEARCH_API_KEY)");
181
+ }
182
+ const effectiveQuery = req.recency ? `${req.query} (results from the last ${req.recency})` : req.query;
183
+ const body = {
184
+ model: jeoEnv("SEARCH_MODEL") || DEFAULT_SEARCH_MODEL,
185
+ max_tokens: typeof req.maxTokens === "number" ? req.maxTokens : DEFAULT_MAX_TOKENS,
186
+ system: anthropicSystemBlocks(credential),
187
+ messages: [{ role: "user", content: effectiveQuery }],
188
+ tools: [{ type: WEB_SEARCH_TOOL_TYPE, name: "web_search" }],
189
+ };
190
+ const response = await fetch(ANTHROPIC_URL, {
191
+ method: "POST",
192
+ headers: anthropicSearchHeaders(credential),
193
+ body: JSON.stringify(body),
194
+ signal: AbortSignal.timeout(HARD_TIMEOUT_MS),
195
+ });
196
+ if (!response.ok) {
197
+ const detail = (await response.text().catch(() => "")).slice(0, 300);
198
+ throw new Error(`Anthropic API error (${response.status}): ${detail}`);
199
+ }
200
+ const parsed = parseAnthropicSearchResponse(await response.json().catch(() => null));
201
+ if (req.limit && req.limit > 0 && parsed.sources.length > req.limit) {
202
+ parsed.sources = parsed.sources.slice(0, req.limit);
203
+ }
204
+ return parsed;
205
+ }
206
+
207
+ // ── DuckDuckGo provider (keyless terminal fallback; gjc port) ────────────────
208
+
209
+ const DDG_HTML_ENDPOINT = "https://html.duckduckgo.com/html/";
210
+ const DDG_LITE_ENDPOINT = "https://lite.duckduckgo.com/lite/";
211
+ const DDG_DEFAULT_RESULTS = 10;
212
+ const DDG_MAX_RESULTS = 20;
213
+ const DDG_ATTEMPTS: Array<"html" | "lite"> = ["html", "lite", "html"];
214
+ const DDG_BACKOFF_MS = [0, 400, 800];
215
+ const DDG_USER_AGENTS = [
216
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
217
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
218
+ "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0",
219
+ ];
220
+ const DDG_RECENCY: Record<NonNullable<SearchRequest["recency"]>, string> = {
221
+ day: "d",
222
+ week: "w",
223
+ month: "m",
224
+ year: "y",
225
+ };
226
+ const DDG_FETCH_TIMEOUT_MS = 20_000;
227
+
228
+ function decodeEntities(input: string): string {
229
+ return input
230
+ .replace(/&amp;/g, "&")
231
+ .replace(/&lt;/g, "<")
232
+ .replace(/&gt;/g, ">")
233
+ .replace(/&quot;/g, '"')
234
+ .replace(/&#0*39;|&#x0*27;|&apos;/gi, "'")
235
+ .replace(/&#x0*2f;/gi, "/")
236
+ .replace(/&#(\d+);/g, (_, dec: string) => String.fromCodePoint(Number(dec)))
237
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16)))
238
+ .replace(/&nbsp;/g, " ");
239
+ }
240
+
241
+ function cleanText(fragment: string): string {
242
+ return decodeEntities(fragment.replace(/<[^>]+>/g, ""))
243
+ .replace(/\s+/g, " ")
244
+ .trim();
245
+ }
246
+
247
+ /** Resolve a DuckDuckGo result href to the real destination URL (drops ads/internal links). */
248
+ export function decodeResultUrl(href: string): string | null {
249
+ let h = decodeEntities(href.trim());
250
+ if (!h || h.startsWith("#")) return null;
251
+ if (h.startsWith("//")) h = `https:${h}`;
252
+ let parsed: URL;
253
+ try {
254
+ parsed = new URL(h, "https://duckduckgo.com");
255
+ } catch {
256
+ return null;
257
+ }
258
+ const uddg = parsed.searchParams.get("uddg");
259
+ if (uddg) {
260
+ try {
261
+ const target = new URL(uddg);
262
+ if (target.protocol !== "http:" && target.protocol !== "https:") return null;
263
+ if (target.hostname.endsWith("duckduckgo.com")) return null;
264
+ return target.toString();
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
270
+ if (parsed.hostname.endsWith("duckduckgo.com")) return null;
271
+ return parsed.toString();
272
+ }
273
+
274
+ /** Parse results from the `html.duckduckgo.com/html/` markup. */
275
+ export function parseHtmlResults(html: string): WebSearchSource[] {
276
+ const titleRe = /<a\b[^>]*class="[^"]*\bresult__a\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
277
+ const snippetRe = /<a\b[^>]*class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
278
+ const snippets: string[] = [];
279
+ for (const m of html.matchAll(snippetRe)) snippets.push(cleanText(m[1]!));
280
+ const results: WebSearchSource[] = [];
281
+ let idx = 0;
282
+ for (const m of html.matchAll(titleRe)) {
283
+ const url = decodeResultUrl(m[1]!);
284
+ const title = cleanText(m[2]!);
285
+ const snippet = snippets[idx];
286
+ idx++;
287
+ if (!url || !title) continue;
288
+ results.push({ title, url, snippet: snippet || undefined });
289
+ }
290
+ return results;
291
+ }
292
+
293
+ /** Parse results from the `lite.duckduckgo.com/lite/` markup. */
294
+ export function parseLiteResults(html: string): WebSearchSource[] {
295
+ const linkRe = /<a\b[^>]*class="[^"]*\bresult-link\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
296
+ const snippetRe = /<td\b[^>]*class="[^"]*\bresult-snippet\b[^"]*"[^>]*>([\s\S]*?)<\/td>/gi;
297
+ const snippets: string[] = [];
298
+ for (const m of html.matchAll(snippetRe)) snippets.push(cleanText(m[1]!));
299
+ const results: WebSearchSource[] = [];
300
+ let idx = 0;
301
+ for (const m of html.matchAll(linkRe)) {
302
+ const url = decodeResultUrl(m[1]!);
303
+ const title = cleanText(m[2]!);
304
+ const snippet = snippets[idx];
305
+ idx++;
306
+ if (!url || !title) continue;
307
+ results.push({ title, url, snippet: snippet || undefined });
308
+ }
309
+ return results;
310
+ }
311
+
312
+ function delay(ms: number): Promise<void> {
313
+ return new Promise(resolve => setTimeout(resolve, ms));
314
+ }
315
+
316
+ async function ddgFetchAndParse(endpoint: "html" | "lite", query: string, df: string | undefined, userAgent: string): Promise<WebSearchSource[]> {
317
+ const url = endpoint === "html" ? DDG_HTML_ENDPOINT : DDG_LITE_ENDPOINT;
318
+ const body = new URLSearchParams({ q: query });
319
+ if (df) body.set("df", df);
320
+ const response = await fetch(url, {
321
+ method: "POST",
322
+ headers: {
323
+ "User-Agent": userAgent,
324
+ Accept: "text/html,application/xhtml+xml",
325
+ "Content-Type": "application/x-www-form-urlencoded",
326
+ "Accept-Language": "en-US,en;q=0.9",
327
+ },
328
+ body,
329
+ signal: AbortSignal.timeout(DDG_FETCH_TIMEOUT_MS),
330
+ });
331
+ // DuckDuckGo signals soft blocks with 202 (which is still response.ok).
332
+ if (response.status === 202) throw new Error("duckduckgo: rate-limited (202)");
333
+ if (!response.ok) throw new Error(`DuckDuckGo error (${response.status})`);
334
+ const text = await response.text();
335
+ const parsed = endpoint === "html" ? parseHtmlResults(text) : parseLiteResults(text);
336
+ if (parsed.length === 0) throw new Error("duckduckgo: no parseable results (possible block)");
337
+ return parsed;
338
+ }
339
+
340
+ /** Keyless DuckDuckGo search with endpoint rotation + backoff (gjc port). It
341
+ * throws on total failure rather than returning an empty success — real
342
+ * results or an explicit error, never a fabricated response. */
343
+ async function searchDuckDuckGo(req: SearchRequest): Promise<WebSearchResponse> {
344
+ const numResults = Math.min(DDG_MAX_RESULTS, Math.max(1, req.limit ?? DDG_DEFAULT_RESULTS));
345
+ const df = req.recency ? DDG_RECENCY[req.recency] : undefined;
346
+ let lastError: unknown;
347
+ for (let attempt = 0; attempt < DDG_ATTEMPTS.length; attempt++) {
348
+ if (DDG_BACKOFF_MS[attempt]! > 0) await delay(DDG_BACKOFF_MS[attempt]!);
349
+ try {
350
+ const parsed = await ddgFetchAndParse(
351
+ DDG_ATTEMPTS[attempt]!,
352
+ req.query,
353
+ df,
354
+ DDG_USER_AGENTS[attempt % DDG_USER_AGENTS.length]!,
355
+ );
356
+ return {
357
+ provider: "DuckDuckGo",
358
+ sources: parsed.slice(0, numResults),
359
+ citations: [],
360
+ searchQueries: [],
361
+ };
362
+ } catch (error) {
363
+ lastError = error;
364
+ }
365
+ }
366
+ throw new Error(
367
+ `DuckDuckGo search failed after ${DDG_ATTEMPTS.length} attempts${lastError instanceof Error ? `: ${lastError.message}` : ""}`,
368
+ );
369
+ }
370
+
371
+ // ── Provider chain (gjc resolveProviderChain semantics) ──────────────────────
372
+
373
+ const PROVIDERS: Record<SearchProviderDef["id"], SearchProviderDef> = {
374
+ anthropic: {
375
+ id: "anthropic",
376
+ label: "Anthropic",
377
+ isAvailable: async () => {
378
+ if (jeoEnv("SEARCH_API_KEY")) return true;
379
+ const cred = await resolveCredential("anthropic").catch(() => null);
380
+ return cred?.kind === "oauth" || cred?.kind === "api_key";
381
+ },
382
+ search: searchAnthropic,
383
+ },
384
+ duckduckgo: {
385
+ id: "duckduckgo",
386
+ label: "DuckDuckGo",
387
+ isAvailable: () => true,
388
+ search: searchDuckDuckGo,
389
+ },
390
+ };
391
+
392
+ /** Active model provider → its native search provider (gjc MODEL_PROVIDER_TO_SEARCH).
393
+ * Providers without a jeo-native search implementation fall through to DuckDuckGo. */
394
+ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderDef["id"]> = {
395
+ anthropic: "anthropic",
396
+ };
397
+
398
+ let activeModelHint: string | undefined;
399
+
400
+ /** Called by the agent loop with the session's active model id, so the chain is
401
+ * active-model-gated like gjc (never credential-scanning across providers). */
402
+ export function setWebSearchActiveModel(model: string | undefined): void {
403
+ activeModelHint = model;
404
+ }
405
+
406
+ async function activeModelProviderName(): Promise<string | undefined> {
407
+ let model = activeModelHint;
408
+ if (!model) {
409
+ try {
410
+ const { readGlobalConfig } = await import("./state");
411
+ model = (await readGlobalConfig()).defaultModel;
412
+ } catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ if (!model) return undefined;
417
+ try {
418
+ const { resolveProvider } = await import("../ai/model-manager");
419
+ return resolveProvider(model);
420
+ } catch {
421
+ return undefined;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Resolve the ordered provider chain for a search request (gjc semantics):
427
+ * explicit preferred provider (when available) → active model's native search
428
+ * (when ITS credentials exist) → DuckDuckGo as the always-on terminal fallback.
429
+ */
430
+ export async function resolveSearchChain(opts: { preferred?: string; modelProvider?: string } = {}): Promise<SearchProviderDef[]> {
431
+ const chain: SearchProviderDef[] = [];
432
+ const preferred = (opts.preferred ?? jeoEnv("SEARCH_PROVIDER"))?.toLowerCase();
433
+
434
+ if (preferred && preferred !== "auto") {
435
+ const provider = PROVIDERS[preferred as SearchProviderDef["id"]];
436
+ if (provider && (await provider.isAvailable())) chain.push(provider);
437
+ } else {
438
+ const modelProvider = (opts.modelProvider ?? (await activeModelProviderName()))?.toLowerCase();
439
+ const nativeId = modelProvider ? MODEL_PROVIDER_TO_SEARCH[modelProvider] : undefined;
440
+ const provider = nativeId ? PROVIDERS[nativeId] : undefined;
441
+ if (provider && (await provider.isAvailable())) chain.push(provider);
442
+ }
443
+
444
+ if (!chain.some(p => p.id === "duckduckgo")) chain.push(PROVIDERS.duckduckgo);
445
+ return chain;
446
+ }
447
+
448
+ // ── Output formatting (LLM text + TUI card source) ───────────────────────────
449
+
450
+ function hostnameOf(url: string): string {
451
+ try {
452
+ return new URL(url).hostname.replace(/^www\./, "");
453
+ } catch {
454
+ return "";
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Format the unified response as structured text. This single string serves
460
+ * BOTH consumers: the LLM (sectioned answer + numbered sources/citations) and
461
+ * the TUI card renderer, which splits it back into the gjc-style
462
+ * Query / Answer / Sources / Metadata card sections.
463
+ */
464
+ export function formatWebSearchOutput(query: string, r: WebSearchResponse): string {
465
+ const parts: string[] = [`Query: ${query}`];
466
+
467
+ parts.push("", "## Answer", r.answer?.trim() || "No synthesized answer (see sources)");
468
+
469
+ parts.push("", `## Sources (${r.sources.length})`);
470
+ if (r.sources.length === 0) parts.push("No sources returned");
471
+ for (const [i, src] of r.sources.entries()) {
472
+ const domain = hostnameOf(src.url);
473
+ const meta = [domain ? `(${domain})` : "", src.publishedDate ?? ""].filter(Boolean).join(" · ");
474
+ parts.push(`[${i + 1}] ${src.title}${meta ? ` ${meta}` : ""}`, ` ${src.url}`);
475
+ if (src.snippet) {
476
+ const snippet = src.snippet.length > 240 ? `${src.snippet.slice(0, 239)}…` : src.snippet;
477
+ parts.push(` - ${snippet}`);
478
+ }
479
+ }
480
+
481
+ if (r.citations.length > 0) {
482
+ parts.push("", `## Citations (${r.citations.length})`);
483
+ for (const [i, c] of r.citations.entries()) {
484
+ parts.push(`[${i + 1}] ${c.title || c.url}`, ` ${c.url}`);
485
+ if (c.citedText) {
486
+ const cited = c.citedText.length > 240 ? `${c.citedText.slice(0, 239)}…` : c.citedText;
487
+ parts.push(` "${cited.replace(/\s+/g, " ").trim()}"`);
488
+ }
489
+ }
490
+ }
491
+
492
+ parts.push("", "## Metadata", `Provider: ${r.provider}`);
493
+ if (r.model) parts.push(`Model: ${r.model}`);
494
+ parts.push(`Sources: ${r.sources.length}`);
495
+ if (r.citations.length > 0) parts.push(`Citations: ${r.citations.length}`);
496
+ if (r.usage) {
497
+ const usage: string[] = [];
498
+ if (r.usage.inputTokens !== undefined) usage.push(`in ${r.usage.inputTokens}`);
499
+ if (r.usage.outputTokens !== undefined) usage.push(`out ${r.usage.outputTokens}`);
500
+ if (r.usage.searchRequests !== undefined) usage.push(`search ${r.usage.searchRequests}`);
501
+ if (usage.length > 0) parts.push(`Usage: ${usage.join(" · ")}`);
502
+ }
503
+ if (r.requestId) parts.push(`Request: ${r.requestId}`);
504
+ if (r.searchQueries.length > 0) parts.push(`Queries: ${r.searchQueries.slice(0, 3).join("; ")}`);
505
+
506
+ return parts.join("\n");
507
+ }
508
+
509
+ // ── Tool entrypoint ──────────────────────────────────────────────────────────
510
+
511
+ /** Execute one web search through the provider chain. Exported for the engine toolset. */
512
+ export async function webSearchTool(args: Record<string, any>, _cwd: string = process.cwd()): Promise<ToolResult> {
513
+ const query = typeof args.query === "string" ? args.query.trim() : "";
514
+ if (!query) return { success: false, output: "", error: "web_search requires a non-empty {query}." };
515
+
516
+ const req: SearchRequest = {
517
+ query,
518
+ recency: typeof args.recency === "string" && args.recency in DDG_RECENCY ? (args.recency as SearchRequest["recency"]) : undefined,
519
+ limit: typeof args.limit === "number" && args.limit > 0 ? args.limit : undefined,
520
+ maxTokens: typeof args.maxTokens === "number" ? args.maxTokens : undefined,
521
+ };
522
+
523
+ const chain = await resolveSearchChain();
524
+ const failures: string[] = [];
525
+ for (const provider of chain) {
526
+ try {
527
+ const response = await provider.search(req);
528
+ return { success: true, output: formatWebSearchOutput(query, response) };
529
+ } catch (err: any) {
530
+ failures.push(`${provider.label}: ${err?.message || String(err)}`);
531
+ }
532
+ }
533
+ return {
534
+ success: false,
535
+ output: "",
536
+ error: `web_search failed across all providers — ${failures.join("; ")}`,
537
+ };
538
+ }
@@ -0,0 +1,44 @@
1
+ <!-- Parent: ../AGENTS.md -->
2
+ <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
+
4
+ # ai
5
+
6
+ ## Purpose
7
+ Abstractions for LLM inference, provider management, tool formatting, and token counting. Decouples the core agent loop from specific API implementations.
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `registry.ts` | Central registry for available providers and model resolution |
13
+ | `types.ts` | Common interfaces for AI requests, responses, and streams |
14
+ | `format.ts` | Conversion between internal tool schemas and provider-specific formats |
15
+
16
+ ## Subdirectories
17
+ | Directory | Purpose |
18
+ |-----------|---------|
19
+ | `providers/` | Concrete implementations for Anthropic, OpenAI, Gemini, Ollama, etc. (see `providers/AGENTS.md`) |
20
+
21
+ ## For AI Agents
22
+
23
+ ### Working In This Directory
24
+ - When adding a new provider, implement the standard interface defined in `types.ts` and register it in `registry.ts`.
25
+ - Ensure streaming outputs are parsed reliably.
26
+ - Handle rate limits (429) gracefully, exposing standard retry hints to the caller.
27
+
28
+ ### Testing Requirements
29
+ - Unit tests for formatting and parsing.
30
+ - Integration tests (or skipped e2e tests) for actual provider connectivity.
31
+
32
+ ### Common Patterns
33
+ - Factory functions for instantiating provider clients.
34
+ - Normalization of diverse API error structures into standard exceptions.
35
+
36
+ ## Dependencies
37
+
38
+ ### Internal
39
+ - Consumed by `src/agent/loop.ts`.
40
+
41
+ ### External
42
+ - HTTP fetch (Bun.fetch).
43
+
44
+ <!-- MANUAL: -->
package/src/ai/index.ts CHANGED
@@ -8,4 +8,5 @@ export * from "./model-enrich";
8
8
  export { anthropicAdapter } from "./providers/anthropic";
9
9
  export { openaiAdapter } from "./providers/openai";
10
10
  export { geminiAdapter } from "./providers/gemini";
11
+ export { antigravityAdapter } from "./providers/antigravity";
11
12
  export { ollamaAdapter } from "./providers/ollama";
@@ -18,10 +18,11 @@ export interface ModelCatalogEntry {
18
18
  reasoning: boolean;
19
19
  recommended?: boolean;
20
20
  note?: string;
21
+ company?: string;
21
22
  }
22
23
 
23
24
  /** Canonical ids that should be surfaced as a provider's recommended default. */
24
- const RECOMMENDED = new Set(["claude-3-5-sonnet", "gpt-4o", "gemini-2.0-flash", "qwen2.5"]);
25
+ const RECOMMENDED = new Set(["claude-sonnet-4-5", "gpt-4o", "gemini-2.0-flash", "antigravity/gemini-3-pro-low", "qwen2.5"]);
25
26
 
26
27
  export function normalizeModelId(id: string): string {
27
28
  return (id ?? "").trim().toLowerCase();
@@ -36,6 +37,7 @@ function adapt(m: CatalogModel): ModelCatalogEntry {
36
37
  reasoning,
37
38
  recommended: RECOMMENDED.has(m.canonical),
38
39
  note: `${formatTokens(m.contextTokens)} ctx${reasoning ? ", reasoning" : ""}`,
40
+ company: m.company,
39
41
  };
40
42
  }
41
43