@kenkaiiii/ggcoder 5.48.0 → 5.49.1

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 (125) hide show
  1. package/dist/app-sidecar-auth-broadcast.test.js +1 -1
  2. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -1
  3. package/dist/app-sidecar.js +253 -2
  4. package/dist/app-sidecar.js.map +1 -1
  5. package/dist/cli/shared.d.ts.map +1 -1
  6. package/dist/cli/shared.js +2 -0
  7. package/dist/cli/shared.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +6 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +1 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session-compaction.test.js +1 -0
  15. package/dist/core/agent-session-compaction.test.js.map +1 -1
  16. package/dist/core/agent-session.d.ts +2 -0
  17. package/dist/core/agent-session.d.ts.map +1 -1
  18. package/dist/core/agent-session.js +15 -8
  19. package/dist/core/agent-session.js.map +1 -1
  20. package/dist/core/auth-providers.d.ts.map +1 -1
  21. package/dist/core/auth-providers.js +21 -21
  22. package/dist/core/auth-providers.js.map +1 -1
  23. package/dist/core/context-limits.d.ts +35 -0
  24. package/dist/core/context-limits.d.ts.map +1 -0
  25. package/dist/core/context-limits.js +35 -0
  26. package/dist/core/context-limits.js.map +1 -0
  27. package/dist/core/context-limits.test.d.ts +2 -0
  28. package/dist/core/context-limits.test.d.ts.map +1 -0
  29. package/dist/core/context-limits.test.js +55 -0
  30. package/dist/core/context-limits.test.js.map +1 -0
  31. package/dist/core/local-endpoint-store.test.js +1 -9
  32. package/dist/core/local-endpoint-store.test.js.map +1 -1
  33. package/dist/core/mcp/deferred-catalog.d.ts +9 -0
  34. package/dist/core/mcp/deferred-catalog.d.ts.map +1 -1
  35. package/dist/core/mcp/deferred-catalog.js +15 -1
  36. package/dist/core/mcp/deferred-catalog.js.map +1 -1
  37. package/dist/core/process-manager-notifications.test.js +31 -13
  38. package/dist/core/process-manager-notifications.test.js.map +1 -1
  39. package/dist/core/process-manager-wake.test.js +8 -4
  40. package/dist/core/process-manager-wake.test.js.map +1 -1
  41. package/dist/core/process-manager.d.ts +11 -0
  42. package/dist/core/process-manager.d.ts.map +1 -1
  43. package/dist/core/process-manager.js +9 -2
  44. package/dist/core/process-manager.js.map +1 -1
  45. package/dist/core/sandbox.d.ts.map +1 -1
  46. package/dist/core/sandbox.js +66 -1
  47. package/dist/core/sandbox.js.map +1 -1
  48. package/dist/core/sandbox.test.js +64 -0
  49. package/dist/core/sandbox.test.js.map +1 -1
  50. package/dist/core/settings-manager.d.ts +9 -0
  51. package/dist/core/settings-manager.d.ts.map +1 -1
  52. package/dist/core/settings-manager.js +18 -0
  53. package/dist/core/settings-manager.js.map +1 -1
  54. package/dist/core/skills-routing.test.js +42 -0
  55. package/dist/core/skills-routing.test.js.map +1 -1
  56. package/dist/core/skills.d.ts +16 -1
  57. package/dist/core/skills.d.ts.map +1 -1
  58. package/dist/core/skills.js +37 -5
  59. package/dist/core/skills.js.map +1 -1
  60. package/dist/core/subagent-manager.d.ts +17 -0
  61. package/dist/core/subagent-manager.d.ts.map +1 -1
  62. package/dist/core/subagent-manager.js +92 -1
  63. package/dist/core/subagent-manager.js.map +1 -1
  64. package/dist/core/subagent-manager.test.js +161 -0
  65. package/dist/core/subagent-manager.test.js.map +1 -1
  66. package/dist/core/subagent-turn-record.d.ts +17 -0
  67. package/dist/core/subagent-turn-record.d.ts.map +1 -0
  68. package/dist/core/subagent-turn-record.js +82 -0
  69. package/dist/core/subagent-turn-record.js.map +1 -0
  70. package/dist/core/subagent-turn-record.test.d.ts +2 -0
  71. package/dist/core/subagent-turn-record.test.d.ts.map +1 -0
  72. package/dist/core/subagent-turn-record.test.js +70 -0
  73. package/dist/core/subagent-turn-record.test.js.map +1 -0
  74. package/dist/hf-pull.d.ts +66 -0
  75. package/dist/hf-pull.d.ts.map +1 -0
  76. package/dist/hf-pull.js +109 -0
  77. package/dist/hf-pull.js.map +1 -0
  78. package/dist/hf-pull.test.d.ts +2 -0
  79. package/dist/hf-pull.test.d.ts.map +1 -0
  80. package/dist/hf-pull.test.js +112 -0
  81. package/dist/hf-pull.test.js.map +1 -0
  82. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  83. package/dist/modes/subagent-worker-mode.js +66 -8
  84. package/dist/modes/subagent-worker-mode.js.map +1 -1
  85. package/dist/system-prompt.d.ts +7 -2
  86. package/dist/system-prompt.d.ts.map +1 -1
  87. package/dist/system-prompt.js +26 -9
  88. package/dist/system-prompt.js.map +1 -1
  89. package/dist/system-prompt.test.js +26 -0
  90. package/dist/system-prompt.test.js.map +1 -1
  91. package/dist/tools/index.d.ts +3 -0
  92. package/dist/tools/index.d.ts.map +1 -1
  93. package/dist/tools/index.js +1 -1
  94. package/dist/tools/index.js.map +1 -1
  95. package/dist/tools/path-utils.d.ts +28 -1
  96. package/dist/tools/path-utils.d.ts.map +1 -1
  97. package/dist/tools/path-utils.js +44 -2
  98. package/dist/tools/path-utils.js.map +1 -1
  99. package/dist/tools/path-utils.test.js +57 -1
  100. package/dist/tools/path-utils.test.js.map +1 -1
  101. package/dist/tools/skill.d.ts +3 -2
  102. package/dist/tools/skill.d.ts.map +1 -1
  103. package/dist/tools/skill.js +10 -7
  104. package/dist/tools/skill.js.map +1 -1
  105. package/dist/tools/tool-search.d.ts +2 -1
  106. package/dist/tools/tool-search.d.ts.map +1 -1
  107. package/dist/tools/tool-search.js +28 -3
  108. package/dist/tools/tool-search.js.map +1 -1
  109. package/dist/tools/tool-search.test.js +58 -0
  110. package/dist/tools/tool-search.test.js.map +1 -1
  111. package/dist/tools/web-fetch.d.ts +7 -0
  112. package/dist/tools/web-fetch.d.ts.map +1 -1
  113. package/dist/tools/web-fetch.js +284 -20
  114. package/dist/tools/web-fetch.js.map +1 -1
  115. package/dist/tools/web-fetch.test.js +137 -0
  116. package/dist/tools/web-fetch.test.js.map +1 -1
  117. package/dist/ui/App.d.ts.map +1 -1
  118. package/dist/ui/App.js.map +1 -1
  119. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  120. package/dist/ui/components/ModelSelector.js +1 -0
  121. package/dist/ui/components/ModelSelector.js.map +1 -1
  122. package/dist/ui/login.d.ts.map +1 -1
  123. package/dist/ui/login.js +7 -2
  124. package/dist/ui/login.js.map +1 -1
  125. package/package.json +4 -4
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { resolveToolSchema } from "@kenkaiiii/gg-ai";
3
+ import { CONTEXT_LIMITS } from "../core/context-limits.js";
2
4
  const ToolSearchParams = z.object({
3
5
  query: z
4
6
  .string()
@@ -15,7 +17,7 @@ const ToolSearchParams = z.object({
15
17
  * learns *now* whether the capability really exists, instead of promoting a
16
18
  * tool that will fail on first call. Returns undefined for already-live tools.
17
19
  */
18
- export function createToolSearchTool(catalog, onPromote, resolveCached) {
20
+ export function createToolSearchTool(catalog, onPromote, resolveCached, limits = CONTEXT_LIMITS) {
19
21
  return {
20
22
  name: "tool_search",
21
23
  description: "Load a tool that is listed as available on demand. Searches the catalog of " +
@@ -31,7 +33,26 @@ export function createToolSearchTool(catalog, onPromote, resolveCached) {
31
33
  ? "No tools matched and the catalog is empty — every catalog tool is already available."
32
34
  : `No tools matched "${query}". Still in the catalog: ${remaining.join(", ")}`;
33
35
  }
34
- const promoted = catalog.promote(matches.map((t) => t.name));
36
+ // Schema budget, fail closed: a tool whose serialized input schema exceeds
37
+ // the budget is NOT promoted — promoting it would bill those bytes on every
38
+ // subsequent request. The user can raise contextLimits.mcpToolSchemaBytes.
39
+ const oversized = [];
40
+ const promotable = [];
41
+ for (const tool of matches) {
42
+ const bytes = Buffer.byteLength(JSON.stringify(resolveToolSchema(tool)), "utf8");
43
+ if (bytes > limits.mcpToolSchemaBytes)
44
+ oversized.push({ name: tool.name, bytes });
45
+ else
46
+ promotable.push(tool);
47
+ }
48
+ if (promotable.length === 0) {
49
+ const details = oversized
50
+ .map((t) => `- ${t.name}: schema is ${Math.round(t.bytes / 1024)}KB (budget ${Math.round(limits.mcpToolSchemaBytes / 1024)}KB)`)
51
+ .join("\n");
52
+ return (`No tools promoted — every match exceeds the schema byte budget.\n${details}\n` +
53
+ `Raise the contextLimits.mcpToolSchemaBytes setting if this tool is trusted.`);
54
+ }
55
+ const promoted = catalog.promote(promotable.map((t) => t.name));
35
56
  onPromote(promoted);
36
57
  const unavailable = new Map();
37
58
  if (resolveCached) {
@@ -48,7 +69,11 @@ export function createToolSearchTool(catalog, onPromote, resolveCached) {
48
69
  }
49
70
  const lines = available.map((t) => `- ${t.name}: ${t.description.split("\n")[0].slice(0, 200)}`);
50
71
  const body = `${available.length} tool(s) now available:\n${lines.join("\n")}`;
51
- return failures.length > 0 ? `${body}\n\nUnreachable:\n${failures.join("\n")}` : body;
72
+ const refused = oversized.length > 0
73
+ ? `\n\nRefused (schema byte budget):\n${oversized.map((t) => `- ${t.name} (${Math.round(t.bytes / 1024)}KB schema)`).join("\n")}\nRaise the contextLimits.mcpToolSchemaBytes setting if a refused tool is trusted.`
74
+ : "";
75
+ const unreachable = failures.length > 0 ? `\n\nUnreachable:\n${failures.join("\n")}` : "";
76
+ return `${body}${refused}${unreachable}`;
52
77
  },
53
78
  };
54
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-search.js","sourceRoot":"","sources":["../../src/tools/tool-search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-search.js","sourceRoot":"","sources":["../../src/tools/tool-search.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { z } from "zod";
3
+ import { resolveContextLimits } from "../core/context-limits.js";
3
4
  import { DeferredToolCatalog } from "../core/mcp/deferred-catalog.js";
4
5
  import { createToolSearchTool } from "./tool-search.js";
5
6
  const CONTEXT = {};
@@ -65,5 +66,62 @@ describe("tool_search", () => {
65
66
  const tool = createToolSearchTool(catalog, () => { });
66
67
  expect(await search(tool, "design screenshots")).toContain("1 tool(s) now available");
67
68
  });
69
+ it("refuses promotion of a tool whose schema exceeds the byte budget", async () => {
70
+ const catalog = new DeferredToolCatalog();
71
+ const huge = z.object({
72
+ blob: z.string().describe("x".repeat(200 * 1024)), // 200KB serialized schema
73
+ });
74
+ catalog.add([
75
+ {
76
+ name: "mcp__big__blob",
77
+ description: "fetch design screenshots",
78
+ parameters: huge,
79
+ execute: async () => "ok",
80
+ },
81
+ stub("mcp__figma__get_screens", "fetch design screenshots"),
82
+ ]);
83
+ const promotedNames = [];
84
+ const tool = createToolSearchTool(catalog, (tools) => promotedNames.push(...tools.map((t) => t.name)));
85
+ const result = await search(tool, "design screenshots");
86
+ // The fit tool promoted; the oversized one was refused and named.
87
+ expect(result).toContain("1 tool(s) now available");
88
+ expect(result).toContain("mcp__figma__get_screens");
89
+ expect(result).toContain("Refused (schema byte budget)");
90
+ expect(result).toContain("mcp__big__blob");
91
+ expect(result).toContain("contextLimits.mcpToolSchemaBytes");
92
+ expect(promotedNames).toEqual(["mcp__figma__get_screens"]);
93
+ // Refused tool stays in the catalog, not the live toolset.
94
+ expect(catalog.names()).toContain("mcp__big__blob");
95
+ });
96
+ it("fails closed when EVERY match exceeds the schema budget", async () => {
97
+ const catalog = new DeferredToolCatalog();
98
+ catalog.add([
99
+ {
100
+ name: "mcp__big__blob",
101
+ description: "fetch design screenshots",
102
+ parameters: z.object({ blob: z.string().describe("x".repeat(200 * 1024)) }),
103
+ execute: async () => "ok",
104
+ },
105
+ ]);
106
+ const promotedNames = [];
107
+ const tool = createToolSearchTool(catalog, (tools) => promotedNames.push(...tools.map((t) => t.name)));
108
+ const result = await search(tool, "design screenshots");
109
+ expect(result).toContain("No tools promoted");
110
+ expect(result).toContain("mcp__big__blob");
111
+ expect(promotedNames).toEqual([]);
112
+ });
113
+ it("honors a raised schema budget from limits", async () => {
114
+ const catalog = new DeferredToolCatalog();
115
+ catalog.add([
116
+ {
117
+ name: "mcp__big__blob",
118
+ description: "fetch design screenshots",
119
+ parameters: z.object({ blob: z.string().describe("x".repeat(8 * 1024)) }), // ~8KB
120
+ execute: async () => "ok",
121
+ },
122
+ ]);
123
+ const tool = createToolSearchTool(catalog, () => { }, undefined, resolveContextLimits({ mcpToolSchemaBytes: 16 * 1024 }));
124
+ expect(await search(tool, "design screenshots")).toContain("1 tool(s) now available");
125
+ });
68
126
  });
69
127
  //# sourceMappingURL=tool-search.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-search.test.js","sourceRoot":"","sources":["../../src/tools/tool-search.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-search.test.js","sourceRoot":"","sources":["../../src/tools/tool-search.test.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,11 @@ import { type GetNetworkPolicy } from "../core/network-guard.js";
7
7
  */
8
8
  export declare function isBlockedUrl(urlString: string): boolean;
9
9
  export declare function htmlToCleanText(html: string): string;
10
+ /** A hyperlink shown in an outline render as `anchor text [number]`. */
11
+ export interface OutlineLink {
12
+ number: number;
13
+ url: string;
14
+ }
10
15
  type LlmsCandidateKind = "llms" | "llms-full" | "llms-ctx" | "page-md";
11
16
  interface LlmsCandidate {
12
17
  url: string;
@@ -20,11 +25,13 @@ export declare function createWebFetchTool(getNetworkPolicy?: GetNetworkPolicy):
20
25
  declare const parameters: z.ZodObject<{
21
26
  url: z.ZodOptional<z.ZodString>;
22
27
  urls: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ follow: z.ZodOptional<z.ZodNumber>;
23
29
  max_length: z.ZodOptional<z.ZodNumber>;
24
30
  format: z.ZodOptional<z.ZodEnum<{
25
31
  text: "text";
26
32
  html: "html";
27
33
  markdown: "markdown";
34
+ outline: "outline";
28
35
  }>>;
29
36
  prefer_llms_txt: z.ZodOptional<z.ZodBoolean>;
30
37
  }, z.core.$strip>;
@@ -1 +1 @@
1
- {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../src/tools/web-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,qBAAqB,CAAC;AAIlE,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CA0CvD;AAmGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAwBD,KAAK,iBAAiB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvE,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA8HD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAqB,GAC5B,OAAO,CAAC,UAAU,CAAC,CA2BrB;AAqLD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAuCnF;AA2ED,wBAAgB,kBAAkB,CAChC,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,SAAS,CAAC,OAAO,UAAU,CAAC,CAuC9B;AA0BD,QAAA,MAAM,UAAU;;;;;;;;;;iBAoBZ,CAAC"}
1
+ {"version":3,"file":"web-fetch.d.ts","sourceRoot":"","sources":["../../src/tools/web-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,qBAAqB,CAAC;AAIlE,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CA0CvD;AAmGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAcD,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AA6MD,KAAK,iBAAiB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvE,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkID,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAqB,GAC5B,OAAO,CAAC,UAAU,CAAC,CA2BrB;AAwLD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAuCnF;AA2HD,wBAAgB,kBAAkB,CAChC,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,SAAS,CAAC,OAAO,UAAU,CAAC,CA8E9B;AA0BD,QAAA,MAAM,UAAU;;;;;;;;;;;;iBAyCb,CAAC"}
@@ -142,6 +142,184 @@ export function htmlToCleanText(html) {
142
142
  .replace(/\n{3,}/g, "\n\n")
143
143
  .trim();
144
144
  }
145
+ // ── Outline rendering (compact numbered view) ────────────────
146
+ /** Hard cap on how many links one tool call will number. */
147
+ const MAX_OUTLINE_LINKS = 100;
148
+ /** ~500 tokens of body: the whole point of the outline format. */
149
+ const OUTLINE_DEFAULT_MAX_LENGTH = 2000;
150
+ /** Per-session render cache size (pages, not bytes). */
151
+ const MAX_CACHED_PAGES = 32;
152
+ const OUTLINE_INDEX_HEADER = "Links (fetch one with `follow`):";
153
+ const BLOCKED_URL_MESSAGE = "Error: URL blocked — requests to private/internal network addresses are not allowed.";
154
+ /**
155
+ * Allocates the small stable numbers that replace hyperlinks in an outline
156
+ * render. Scoped to a single tool call, deduped by absolute URL, and capped at
157
+ * {@link MAX_OUTLINE_LINKS} so a link-farm page cannot grow the index (or the
158
+ * session's follow map) without bound.
159
+ */
160
+ class LinkNumbers {
161
+ byUrl = new Map();
162
+ used = new Set();
163
+ ordered = [];
164
+ next = 1;
165
+ get size() {
166
+ return this.byUrl.size;
167
+ }
168
+ /** Adopt the numbers baked into a cached render so they stay resolvable. */
169
+ reserve(links) {
170
+ for (const link of links) {
171
+ if (this.used.has(link.number))
172
+ continue;
173
+ this.used.add(link.number);
174
+ this.byUrl.set(link.url, link.number);
175
+ this.ordered.push(link);
176
+ }
177
+ }
178
+ /** Number for `url`, or null once the per-call cap is reached. */
179
+ numberFor(url) {
180
+ const existing = this.byUrl.get(url);
181
+ if (existing !== undefined)
182
+ return existing;
183
+ if (this.byUrl.size >= MAX_OUTLINE_LINKS)
184
+ return null;
185
+ while (this.used.has(this.next))
186
+ this.next++;
187
+ const number = this.next++;
188
+ this.used.add(number);
189
+ this.byUrl.set(url, number);
190
+ this.ordered.push({ number, url });
191
+ return number;
192
+ }
193
+ all() {
194
+ return this.ordered;
195
+ }
196
+ }
197
+ /** Absolute http(s) form of `href` relative to `base`, or null if unusable. */
198
+ function absoluteHttpUrl(href, base) {
199
+ const trimmed = href.trim();
200
+ if (!trimmed || trimmed.startsWith("#"))
201
+ return null;
202
+ let resolved;
203
+ try {
204
+ resolved = new URL(trimmed, base);
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ if (resolved.protocol !== "http:" && resolved.protocol !== "https:")
210
+ return null;
211
+ // Fragments only move within a page we already have: drop them so anchors
212
+ // into the same document collapse onto one number, and spend no number at
213
+ // all on links back into the page being rendered.
214
+ resolved.hash = "";
215
+ let self;
216
+ try {
217
+ self = new URL(base);
218
+ }
219
+ catch {
220
+ return resolved.toString();
221
+ }
222
+ self.hash = "";
223
+ if (resolved.href === self.href)
224
+ return null;
225
+ return resolved.href;
226
+ }
227
+ const MARKDOWN_LINK = /(!?)\[([^\]]*)\]\(\s*<?([^)<>\s]*)>?(?:\s+"[^"]*")?\s*\)/g;
228
+ const HTML_ANCHOR = /<a\b[^>]*\bhref\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;
229
+ /** Rewrite markdown links to `text [n]`; drop images, which carry no text. */
230
+ function numberMarkdownLinks(markdown, base, numbers) {
231
+ return markdown.replace(MARKDOWN_LINK, (_match, image, text, href) => {
232
+ if (image)
233
+ return "";
234
+ const url = absoluteHttpUrl(href, base);
235
+ if (!url)
236
+ return text;
237
+ const number = numbers.numberFor(url);
238
+ return number === null ? text : `${text} [${number}]`;
239
+ });
240
+ }
241
+ /** Same rewrite for raw HTML, used when the markdown extractor is unavailable. */
242
+ function numberHtmlAnchors(html, base, numbers) {
243
+ return html.replace(HTML_ANCHOR, (_match, href, inner) => {
244
+ const url = absoluteHttpUrl(decodeHTMLEntities(href), base);
245
+ if (!url)
246
+ return inner;
247
+ const number = numbers.numberFor(url);
248
+ return number === null ? inner : `${inner} [${number}]`;
249
+ });
250
+ }
251
+ function compactBlankLines(text) {
252
+ return text
253
+ .split("\n")
254
+ .map((line) => line.replace(/[ \t]+$/, ""))
255
+ .join("\n")
256
+ .replace(/\n{3,}/g, "\n\n")
257
+ .trim();
258
+ }
259
+ /**
260
+ * Render a fetched document as main content with every hyperlink replaced by a
261
+ * number, followed by a compact number → absolute URL index. The body reuses
262
+ * the existing `max_length` budget; only links still visible in the (possibly
263
+ * truncated) body are indexed.
264
+ */
265
+ async function renderOutline(text, isHtml, finalUrl, opts) {
266
+ const numbers = opts.numbers ?? new LinkNumbers();
267
+ let heading = "";
268
+ let body;
269
+ if (!isHtml) {
270
+ body = numberMarkdownLinks(text, finalUrl, numbers);
271
+ }
272
+ else {
273
+ let extracted;
274
+ try {
275
+ extracted = await extractToMarkdown(text, finalUrl);
276
+ }
277
+ catch {
278
+ extracted = null;
279
+ }
280
+ if (extracted) {
281
+ heading = extracted.title ? `# ${extracted.title}\n\n` : "";
282
+ body = numberMarkdownLinks(extracted.markdown, finalUrl, numbers);
283
+ }
284
+ else {
285
+ body = htmlToCleanText(numberHtmlAnchors(removeBoilerplateElements(text), finalUrl, numbers));
286
+ }
287
+ }
288
+ const rendered = truncate(compactBlankLines(heading + body), opts.maxLength);
289
+ const shown = numbers.all().filter((link) => rendered.includes(`[${link.number}]`));
290
+ let output = rendered;
291
+ if (shown.length > 0) {
292
+ const index = shown.map((link) => `[${link.number}] ${link.url}`).join("\n");
293
+ output += `\n\n${OUTLINE_INDEX_HEADER}\n${index}`;
294
+ if (numbers.size >= MAX_OUTLINE_LINKS) {
295
+ output += `\n[link index truncated at ${MAX_OUTLINE_LINKS}; further links left as plain text]`;
296
+ }
297
+ }
298
+ opts.onRender?.(finalUrl, output, [...shown]);
299
+ return output;
300
+ }
301
+ function cacheKey(url, maxLength) {
302
+ return `${maxLength}\u0000${url}`;
303
+ }
304
+ function cacheGet(cache, key) {
305
+ const hit = cache.get(key);
306
+ if (!hit)
307
+ return undefined;
308
+ // Re-insert so Map iteration order stays least-recently-used first.
309
+ cache.delete(key);
310
+ cache.set(key, hit);
311
+ return hit;
312
+ }
313
+ function cacheSet(cache, key, entry) {
314
+ cache.delete(key);
315
+ cache.set(key, entry);
316
+ while (cache.size > MAX_CACHED_PAGES) {
317
+ const oldest = cache.keys().next();
318
+ if (oldest.done)
319
+ break;
320
+ cache.delete(oldest.value);
321
+ }
322
+ }
145
323
  // ── Fetch configuration ──────────────────────────────────────
146
324
  const MAX_REDIRECTS = 5;
147
325
  const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
@@ -165,7 +343,7 @@ const DEFAULT_LLMS_CANDIDATE_LIMIT = 6;
165
343
  * error string describing why the fetch could not complete.
166
344
  */
167
345
  function headersForFormat(format, honestUserAgent = false) {
168
- const accept = format === "html"
346
+ const accept = format === "html" || format === "outline"
169
347
  ? "text/html,application/xhtml+xml;q=0.9,*/*;q=0.5"
170
348
  : format === "markdown"
171
349
  ? "text/markdown,text/plain;q=0.9,text/html;q=0.8,*/*;q=0.5"
@@ -324,6 +502,9 @@ async function processHtmlOrText(response, text, opts) {
324
502
  /application\/octet-stream|binary\/octet-stream|text\/plain/i.test(response.contentType);
325
503
  const isHtml = response.contentType.includes("html") ||
326
504
  (genericContentType && /^(?:<!doctype\s+html|<html\b|<head\b|<body\b)/i.test(prefix));
505
+ if (opts.format === "outline") {
506
+ return await renderOutline(text, isHtml, response.finalUrl, opts);
507
+ }
327
508
  if (opts.format === "html") {
328
509
  return truncate(text, opts.maxLength);
329
510
  }
@@ -352,7 +533,7 @@ async function processHtmlOrText(response, text, opts) {
352
533
  */
353
534
  async function fetchAndProcess(url, opts, signal) {
354
535
  if (isBlockedUrl(url)) {
355
- return "Error: URL blocked — requests to private/internal network addresses are not allowed.";
536
+ return BLOCKED_URL_MESSAGE;
356
537
  }
357
538
  try {
358
539
  const result = await fetchOne(url, signal, opts.format, opts.getNetworkPolicy);
@@ -519,27 +700,98 @@ async function tryLlmsResource(url, opts, signal) {
519
700
  return probes.find((probe) => probe !== null) ?? null;
520
701
  }
521
702
  async function fetchWithPreferredDocs(url, opts, signal) {
522
- if (opts.format !== "html" && opts.preferLlmsTxt && !isBlockedUrl(url) && isDocish(url)) {
703
+ if (opts.format !== "html" &&
704
+ opts.format !== "outline" &&
705
+ opts.preferLlmsTxt &&
706
+ !isBlockedUrl(url) &&
707
+ isDocish(url)) {
523
708
  const llms = await tryLlmsResource(url, opts, signal);
524
709
  if (llms)
525
710
  return llms;
526
711
  }
527
712
  return await fetchAndProcess(url, opts, signal);
528
713
  }
714
+ /**
715
+ * Outline-mode wrapper around {@link fetchWithPreferredDocs} that serves
716
+ * repeat views of a page from the per-session cache. The URL is re-validated
717
+ * *before* the cache is consulted, so a cached render can never resurrect a
718
+ * host the current SSRF/allowlist policy forbids.
719
+ */
720
+ async function fetchPage(url, opts, signal, cache) {
721
+ if (opts.format !== "outline")
722
+ return await fetchWithPreferredDocs(url, opts, signal);
723
+ if (isBlockedUrl(url))
724
+ return BLOCKED_URL_MESSAGE;
725
+ const policyError = checkUrlPolicy(url, opts.getNetworkPolicy);
726
+ if (policyError)
727
+ return `Error: ${policyError}`;
728
+ const requestKey = cacheKey(url, opts.maxLength);
729
+ const hit = cacheGet(cache, requestKey);
730
+ if (hit) {
731
+ opts.numbers?.reserve(hit.links);
732
+ return hit.text;
733
+ }
734
+ let rendered;
735
+ const result = await fetchWithPreferredDocs(url, {
736
+ ...opts,
737
+ onRender: (finalUrl, text, links) => {
738
+ rendered = { text, links };
739
+ // Keyed by the post-redirect URL, plus the requested URL below.
740
+ cacheSet(cache, cacheKey(finalUrl, opts.maxLength), rendered);
741
+ },
742
+ }, signal);
743
+ if (rendered)
744
+ cacheSet(cache, requestKey, rendered);
745
+ return result;
746
+ }
529
747
  export function createWebFetchTool(getNetworkPolicy) {
748
+ // Per-session state, both bounded: the render cache evicts least-recently
749
+ // used pages past MAX_CACHED_PAGES, and the follow map is replaced (not
750
+ // grown) by each outline call, itself capped at MAX_OUTLINE_LINKS entries.
751
+ const pageCache = new Map();
752
+ let followTargets = new Map();
530
753
  return {
531
754
  name: "web_fetch",
532
755
  description: "Fetch and read web page content. Accepts a single `url` or a `urls` array (up to 10, " +
533
- "fetched concurrently). Returns clean Markdown by default (`format`: markdown|text|html) via " +
534
- "main-content extraction. Extracts text from PDFs, follows safe redirects automatically, and " +
535
- "prefers a site's curated /llms.txt for docs pages when available.",
756
+ "fetched concurrently). Returns clean Markdown by default (`format`: markdown|text|html|outline) " +
757
+ "via main-content extraction. Extracts text from PDFs, follows safe redirects automatically, and " +
758
+ "prefers a site's curated /llms.txt for docs pages when available.\n" +
759
+ '`format: "outline"` is the cheap mode: main content only, every hyperlink replaced by a ' +
760
+ "number (`anchor text [12]`) with a numbered URL index at the end, and a small default " +
761
+ "`max_length`. Use it when hunting for the right page; then pass `follow: 12` " +
762
+ "(instead of `url`) to fetch link 12 from the last outline. Repeat views of a page in the " +
763
+ "same session are served from cache. Outline mode skips the /llms.txt probe.",
536
764
  parameters,
537
765
  async execute(args, context) {
538
- const maxLength = args.max_length ?? 10000;
539
766
  const format = args.format ?? "markdown";
767
+ const maxLength = args.max_length ?? (format === "outline" ? OUTLINE_DEFAULT_MAX_LENGTH : 10000);
540
768
  const preferLlmsTxt = args.prefer_llms_txt !== false;
769
+ const numbers = format === "outline" ? new LinkNumbers() : undefined;
770
+ // A followed link is attacker-controlled page content: it resolves to a
771
+ // plain URL here and then travels the exact same validation path as a
772
+ // user-supplied one (isBlockedUrl + allowlist + per-redirect-hop checks).
773
+ let followUrl;
774
+ if (args.follow !== undefined) {
775
+ followUrl = followTargets.get(args.follow);
776
+ if (!followUrl) {
777
+ return followTargets.size === 0
778
+ ? 'Error: no numbered links available — fetch a page with format: "outline" first.'
779
+ : `Error: link [${args.follow}] is not in the last outline (known: ${[...followTargets.keys()].join(", ")}).`;
780
+ }
781
+ }
782
+ // Every outline render replaces the follow map, even when the page had no
783
+ // links: keeping the previous page's numbers would make `follow: 3` fetch
784
+ // from a page the user already moved past, while the error text and the
785
+ // tool description both promise "the last outline". Non-outline fetches
786
+ // leave the map alone, so a markdown read does not discard usable numbers.
787
+ const remember = (output) => {
788
+ if (numbers) {
789
+ followTargets = new Map(numbers.all().map((link) => [link.number, link.url]));
790
+ }
791
+ return output;
792
+ };
541
793
  // Multi-URL path: bounded-concurrency pool, per-URL budget, ordered output.
542
- if (args.urls && args.urls.length > 0) {
794
+ if (!followUrl && args.urls && args.urls.length > 0) {
543
795
  const urls = args.urls;
544
796
  const perUrlBudget = Math.max(PER_URL_MIN_BUDGET, Math.floor(maxLength / urls.length));
545
797
  const opts = {
@@ -547,16 +799,17 @@ export function createWebFetchTool(getNetworkPolicy) {
547
799
  format,
548
800
  preferLlmsTxt,
549
801
  getNetworkPolicy,
802
+ numbers,
550
803
  };
551
- const sections = await runPool(urls, MAX_CONCURRENCY, (u) => fetchWithPreferredDocs(u, opts, context.signal));
552
- return urls.map((u, i) => `## ${u}\n${sections[i]}`).join("\n\n");
804
+ const sections = await runPool(urls, MAX_CONCURRENCY, (u) => fetchPage(u, opts, context.signal, pageCache));
805
+ return remember(urls.map((u, i) => `## ${u}\n${sections[i]}`).join("\n\n"));
553
806
  }
554
- const url = args.url;
807
+ const url = followUrl ?? args.url;
555
808
  if (!url) {
556
- return "Error: provide either `url` or `urls`.";
809
+ return "Error: provide either `url`, `urls`, or `follow`.";
557
810
  }
558
- const opts = { maxLength, format, preferLlmsTxt, getNetworkPolicy };
559
- return await fetchWithPreferredDocs(url, opts, context.signal);
811
+ const opts = { maxLength, format, preferLlmsTxt, getNetworkPolicy, numbers };
812
+ return remember(await fetchPage(url, opts, context.signal, pageCache));
560
813
  },
561
814
  };
562
815
  }
@@ -585,17 +838,28 @@ const parameters = z
585
838
  .max(MAX_URLS)
586
839
  .optional()
587
840
  .describe(`Fetch multiple URLs concurrently (up to ${MAX_URLS}); returns a sectioned digest`),
588
- max_length: z.number().optional().describe("Maximum characters to return (default: 10000)"),
841
+ follow: z
842
+ .number()
843
+ .int()
844
+ .positive()
845
+ .optional()
846
+ .describe("Fetch link N from the most recent outline render, instead of `url`. " +
847
+ "Followed links are SSRF/allowlist-checked exactly like a supplied URL."),
848
+ max_length: z
849
+ .number()
850
+ .optional()
851
+ .describe(`Maximum characters to return (default: 10000; ${OUTLINE_DEFAULT_MAX_LENGTH} for outline)`),
589
852
  format: z
590
- .enum(["markdown", "text", "html"])
853
+ .enum(["markdown", "text", "html", "outline"])
591
854
  .optional()
592
- .describe("Output format: markdown (default, main-content extraction), text, or html"),
855
+ .describe("Output format: markdown (default, main-content extraction), text, html, or outline " +
856
+ `(compact main content with each link replaced by a number plus a numbered URL index, ` +
857
+ `capped at ${MAX_OUTLINE_LINKS} links — cheapest; follow links with \`follow\`)`),
593
858
  prefer_llms_txt: z
594
859
  .boolean()
595
860
  .optional()
596
861
  .describe("Prefer a site's curated /llms.txt for documentation pages (default: true)"),
597
862
  })
598
- .refine((v) => Boolean(v.url) !== Boolean(v.urls && v.urls.length > 0), {
599
- message: "Provide exactly one of `url` or `urls`.",
600
- });
863
+ .refine((v) => [Boolean(v.url), Boolean(v.urls && v.urls.length > 0), v.follow !== undefined].filter(Boolean)
864
+ .length === 1, { message: "Provide exactly one of `url`, `urls`, or `follow`." });
601
865
  //# sourceMappingURL=web-fetch.js.map