@aexol/spectral 0.9.55 → 0.9.59

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 (95) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +43 -0
  3. package/dist/agent/index.d.ts +6 -1
  4. package/dist/agent/index.d.ts.map +1 -1
  5. package/dist/agent/index.js +133 -14
  6. package/dist/commands/login.d.ts +1 -0
  7. package/dist/commands/login.d.ts.map +1 -1
  8. package/dist/commands/login.js +12 -0
  9. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  10. package/dist/extensions/aexol-mcp.js +42 -52
  11. package/dist/extensions/kanban-bridge.d.ts.map +1 -1
  12. package/dist/extensions/kanban-bridge.js +126 -27
  13. package/dist/extensions/web/index.d.ts +10 -0
  14. package/dist/extensions/web/index.d.ts.map +1 -0
  15. package/dist/extensions/web/index.js +11 -0
  16. package/dist/extensions/web/tools/fetch.d.ts +3 -0
  17. package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
  18. package/dist/extensions/web/tools/fetch.js +80 -0
  19. package/dist/extensions/web/utils/fetcher.d.ts +34 -0
  20. package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
  21. package/dist/extensions/web/utils/fetcher.js +289 -0
  22. package/dist/extensions/web/utils/parser.d.ts +15 -0
  23. package/dist/extensions/web/utils/parser.d.ts.map +1 -0
  24. package/dist/extensions/web/utils/parser.js +205 -0
  25. package/dist/memory/branch.d.ts.map +1 -1
  26. package/dist/memory/branch.js +16 -8
  27. package/dist/memory/commands/status.d.ts.map +1 -1
  28. package/dist/memory/commands/status.js +3 -2
  29. package/dist/memory/config.d.ts +12 -0
  30. package/dist/memory/config.d.ts.map +1 -1
  31. package/dist/memory/config.js +21 -2
  32. package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
  33. package/dist/memory/hooks/compaction-trigger.js +19 -3
  34. package/dist/memory/index.d.ts.map +1 -1
  35. package/dist/memory/index.js +2 -0
  36. package/dist/memory/tool-output-compressor.d.ts +14 -20
  37. package/dist/memory/tool-output-compressor.d.ts.map +1 -1
  38. package/dist/memory/tool-output-compressor.js +391 -137
  39. package/dist/memory/tools/compact-context.d.ts +5 -0
  40. package/dist/memory/tools/compact-context.d.ts.map +1 -0
  41. package/dist/memory/tools/compact-context.js +772 -0
  42. package/dist/relay/dispatcher.d.ts +1 -1
  43. package/dist/relay/dispatcher.d.ts.map +1 -1
  44. package/dist/relay/dispatcher.js +73 -1
  45. package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
  46. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  47. package/dist/sdk/coding-agent/core/agent-session.js +408 -210
  48. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
  49. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
  50. package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
  51. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
  52. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
  53. package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
  54. package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
  55. package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
  56. package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
  57. package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
  58. package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
  59. package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
  60. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  61. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
  62. package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
  63. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  64. package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
  65. package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
  66. package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
  67. package/dist/sdk/coding-agent/index.d.ts +1 -1
  68. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  69. package/dist/server/agent-bridge.d.ts +12 -5
  70. package/dist/server/agent-bridge.d.ts.map +1 -1
  71. package/dist/server/agent-bridge.js +65 -17
  72. package/dist/server/handlers/paths-home.d.ts +11 -0
  73. package/dist/server/handlers/paths-home.d.ts.map +1 -0
  74. package/dist/server/handlers/paths-home.js +10 -0
  75. package/dist/server/handlers/paths-list.d.ts +24 -0
  76. package/dist/server/handlers/paths-list.d.ts.map +1 -0
  77. package/dist/server/handlers/paths-list.js +56 -0
  78. package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
  79. package/dist/server/handlers/paths-pick-directory.js +2 -1
  80. package/dist/server/handlers/projects.js +4 -4
  81. package/dist/server/handlers/sessions.d.ts +11 -1
  82. package/dist/server/handlers/sessions.d.ts.map +1 -1
  83. package/dist/server/handlers/sessions.js +22 -2
  84. package/dist/server/handlers/settings.d.ts +18 -1
  85. package/dist/server/handlers/settings.d.ts.map +1 -1
  86. package/dist/server/handlers/settings.js +63 -13
  87. package/dist/server/session-stream.d.ts +65 -37
  88. package/dist/server/session-stream.d.ts.map +1 -1
  89. package/dist/server/session-stream.js +525 -119
  90. package/dist/server/storage.d.ts +6 -0
  91. package/dist/server/storage.d.ts.map +1 -1
  92. package/dist/server/storage.js +72 -9
  93. package/dist/server/wire.d.ts +129 -2
  94. package/dist/server/wire.d.ts.map +1 -1
  95. package/package.json +2 -1
@@ -43,11 +43,32 @@ async function graphqlRequest(opts, query, variables) {
43
43
  catch { /* ignore */ }
44
44
  throw new GraphQLError(`HTTP ${res.status} from GraphQL${detail ? `: ${detail}` : ""}`, res.status);
45
45
  }
46
- const json = (await res.json());
46
+ const rawBody = await res.text();
47
+ let json;
48
+ try {
49
+ json = JSON.parse(rawBody);
50
+ }
51
+ catch {
52
+ throw new GraphQLError(`GraphQL returned non-JSON response (${res.status}): ${rawBody.slice(0, 200)}`);
53
+ }
47
54
  if (json.errors && json.errors.length > 0) {
48
- throw new GraphQLError(json.errors.map((e) => e.message).join("; "));
55
+ const errDetail = json.errors.map((e) => {
56
+ let s = e.message;
57
+ if (e.extensions?.code)
58
+ s += ` [code: ${e.extensions.code}]`;
59
+ return s;
60
+ }).join("; ");
61
+ // Detect auth-related errors — the backend returns 200 with auth errors in the body
62
+ const isAuthError = json.errors.some((e) => e.extensions?.code === "UNAUTHENTICATED" ||
63
+ (e.extensions?.originalError &&
64
+ typeof e.extensions.originalError === "object" &&
65
+ e.extensions.originalError.message === "Authentication required"));
66
+ process.stderr.write(`[kanban-bridge] GraphQL errors from ${opts.backendUrl}/graphql (project ${opts.projectId}): ${errDetail}\n`);
67
+ process.stderr.write(`[kanban-bridge] Full response: ${rawBody.slice(0, 500)}\n`);
68
+ throw new GraphQLError(errDetail, isAuthError ? 401 : undefined);
49
69
  }
50
70
  if (!json.data) {
71
+ process.stderr.write(`[kanban-bridge] GraphQL missing data from ${opts.backendUrl}/graphql (project ${opts.projectId}): ${rawBody.slice(0, 500)}\n`);
51
72
  throw new GraphQLError("GraphQL response missing data");
52
73
  }
53
74
  return json.data;
@@ -164,14 +185,42 @@ async function deleteKanbanTask(opts, taskId) {
164
185
  // ---------------------------------------------------------------------------
165
186
  // Config resolution
166
187
  // ---------------------------------------------------------------------------
167
- import { readConfig } from "../config.js";
188
+ import { getApiUrl, getConfigFile, readConfig } from "../config.js";
168
189
  import { readStudioBinding } from "../studio-binding.js";
169
- import { getConfigFile } from "../config.js";
170
- import { ensureAuthenticated } from "../auth-helper.js";
190
+ import { ensureAuthenticated, runOAuthFlow } from "../auth-helper.js";
191
+ import { stat } from "node:fs/promises";
192
+ import { join } from "node:path";
171
193
  // Track whether we've already attempted reauth in this session to avoid loops
172
194
  let _reauthAttempted = false;
173
- let _studioBinding = null;
174
- async function resolveKanbanConfig(explicitProjectId) {
195
+ const _bindingCache = new Map();
196
+ async function refreshBindingForCwd(cwd) {
197
+ const bindingFile = join(cwd, ".aexol", "aexol.jsonc");
198
+ const cached = _bindingCache.get(cwd);
199
+ let mtimeMs;
200
+ try {
201
+ mtimeMs = (await stat(bindingFile)).mtimeMs;
202
+ }
203
+ catch (err) {
204
+ const code = err.code;
205
+ if (code === "ENOENT" || code === "ENOTDIR") {
206
+ _bindingCache.set(cwd, { binding: null, mtimeMs: null });
207
+ return null;
208
+ }
209
+ return cached?.binding ?? null;
210
+ }
211
+ if (cached && mtimeMs === cached.mtimeMs)
212
+ return cached.binding;
213
+ try {
214
+ const binding = await readStudioBinding(cwd);
215
+ _bindingCache.set(cwd, { binding, mtimeMs });
216
+ return binding;
217
+ }
218
+ catch {
219
+ _bindingCache.set(cwd, { binding: cached?.binding ?? null, mtimeMs: null });
220
+ return cached?.binding ?? null;
221
+ }
222
+ }
223
+ async function resolveKanbanConfig(explicitProjectId, cwd) {
175
224
  const cfg = await readConfig();
176
225
  if (!cfg) {
177
226
  if (!_reauthAttempted) {
@@ -179,42 +228,50 @@ async function resolveKanbanConfig(explicitProjectId) {
179
228
  process.stderr.write(`[kanban-bridge] No config found at ${getConfigFile()}. Attempting OAuth login...\n`);
180
229
  const result = await ensureAuthenticated();
181
230
  if (result.ok && result.config) {
182
- return resolveKanbanConfig(explicitProjectId); // Retry with fresh config
231
+ return resolveKanbanConfig(explicitProjectId, cwd); // Retry with fresh config
183
232
  }
184
233
  }
185
234
  process.stderr.write(`[kanban-bridge] No config found at ${getConfigFile()}. Run \`spectral login\`.\n`);
186
235
  return null;
187
236
  }
188
- const token = cfg.teamApiKey || cfg.userJwt || "";
237
+ // If we only have a team API key, proactively get a user JWT via OAuth.
238
+ // The team API key passes MCP health checks but fails GraphQL kanban auth.
239
+ if (cfg.teamApiKey && !cfg.userJwt && !_reauthAttempted) {
240
+ _reauthAttempted = true;
241
+ process.stderr.write(`[kanban-bridge] Team API key found but no user JWT. Kanban requires user auth — obtaining via OAuth...\n`);
242
+ const oauthResult = await runOAuthFlow();
243
+ if (oauthResult.ok) {
244
+ return resolveKanbanConfig(explicitProjectId, cwd); // Retry with fresh config (now has userJwt)
245
+ }
246
+ process.stderr.write(`[kanban-bridge] OAuth failed: ${oauthResult.reason}. Kanban calls will likely fail.\n`);
247
+ }
248
+ const token = cfg.userJwt || cfg.teamApiKey || "";
189
249
  if (!token) {
190
250
  if (!_reauthAttempted) {
191
251
  _reauthAttempted = true;
192
252
  process.stderr.write(`[kanban-bridge] No auth token in config. Attempting OAuth login...\n`);
193
253
  const result = await ensureAuthenticated();
194
254
  if (result.ok && result.config) {
195
- return resolveKanbanConfig(explicitProjectId);
255
+ return resolveKanbanConfig(explicitProjectId, cwd);
196
256
  }
197
257
  }
198
258
  process.stderr.write(`[kanban-bridge] No auth token in config. Run \`spectral login\`.\n`);
199
259
  return null;
200
260
  }
201
261
  // Resolve backend URL — the config stores the MCP URL, we need the base
202
- const backendUrl = cfg.apiUrl.replace(/\/mcp$/, "");
262
+ const backendUrl = process.env.SPECTRAL_BACKEND_URL ||
263
+ getApiUrl(cfg.apiUrl).replace(/\/mcp\/?$/, "");
203
264
  // Resolve projectId: explicit param > studio binding
204
265
  let projectId = explicitProjectId || null;
205
266
  if (!projectId) {
206
- try {
207
- const binding = _studioBinding ?? await readStudioBinding();
208
- if (binding?.projectId) {
209
- projectId = binding.projectId;
210
- }
211
- }
212
- catch {
213
- // ignore
267
+ const lookupCwd = cwd ?? process.cwd();
268
+ const binding = await refreshBindingForCwd(lookupCwd);
269
+ if (binding?.projectId) {
270
+ projectId = binding.projectId;
214
271
  }
215
272
  }
216
273
  if (!projectId) {
217
- process.stderr.write(`[kanban-bridge] No projectId available. Bind a project via \`spectral bind <project-id>\` or pass projectId explicitly.\n`);
274
+ process.stderr.write(`[kanban-bridge] No projectId available for cwd ${cwd ?? process.cwd()}. Bind a project via \`spectral bind <project-id>\` or pass projectId explicitly.\n`);
218
275
  return null;
219
276
  }
220
277
  return { token, backendUrl, projectId };
@@ -290,7 +347,12 @@ const kanbanListTool = {
290
347
  };
291
348
  }
292
349
  catch (err) {
350
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
351
+ throw err; // Propagate to withReauthRetry for OAuth handling
352
+ }
293
353
  const msg = err instanceof Error ? err.message : String(err);
354
+ process.stderr.write(`[kanban-bridge] kanban_list failed for project ${cfg.projectId}: ${msg}\n` +
355
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
294
356
  return {
295
357
  content: [{ type: "text", text: `Error listing tasks: ${msg}` }],
296
358
  details: { isError: true },
@@ -347,7 +409,12 @@ const kanbanGetTool = {
347
409
  };
348
410
  }
349
411
  catch (err) {
412
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
413
+ throw err; // Propagate to withReauthRetry for OAuth handling
414
+ }
350
415
  const msg = err instanceof Error ? err.message : String(err);
416
+ process.stderr.write(`[kanban-bridge] kanban_get failed for project ${cfg.projectId}: ${msg}\n` +
417
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
351
418
  return { content: [{ type: "text", text: `Error: ${msg}` }], details: { isError: true } };
352
419
  }
353
420
  },
@@ -394,7 +461,12 @@ const kanbanCreateTool = {
394
461
  };
395
462
  }
396
463
  catch (err) {
464
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
465
+ throw err; // Propagate to withReauthRetry for OAuth handling
466
+ }
397
467
  const msg = err instanceof Error ? err.message : String(err);
468
+ process.stderr.write(`[kanban-bridge] kanban_create failed for project ${cfg.projectId}: ${msg}\n` +
469
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
398
470
  return { content: [{ type: "text", text: `Error creating task: ${msg}` }], details: { isError: true } };
399
471
  }
400
472
  },
@@ -444,7 +516,12 @@ const kanbanUpdateTool = {
444
516
  };
445
517
  }
446
518
  catch (err) {
519
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
520
+ throw err; // Propagate to withReauthRetry for OAuth handling
521
+ }
447
522
  const msg = err instanceof Error ? err.message : String(err);
523
+ process.stderr.write(`[kanban-bridge] kanban_update failed for project ${cfg.projectId}: ${msg}\n` +
524
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
448
525
  return { content: [{ type: "text", text: `Error updating task: ${msg}` }], details: { isError: true } };
449
526
  }
450
527
  },
@@ -495,7 +572,12 @@ const kanbanMoveTool = {
495
572
  };
496
573
  }
497
574
  catch (err) {
575
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
576
+ throw err; // Propagate to withReauthRetry for OAuth handling
577
+ }
498
578
  const msg = err instanceof Error ? err.message : String(err);
579
+ process.stderr.write(`[kanban-bridge] kanban_move failed for project ${cfg.projectId}: ${msg}\n` +
580
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
499
581
  return { content: [{ type: "text", text: `Error moving task: ${msg}` }], details: { isError: true } };
500
582
  }
501
583
  },
@@ -616,7 +698,12 @@ const kanbanNextTool = {
616
698
  return { content: [{ type: "text", text: lines.join("\n") }], details: {} };
617
699
  }
618
700
  catch (err) {
701
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
702
+ throw err; // Propagate to withReauthRetry for OAuth handling
703
+ }
619
704
  const msg = err instanceof Error ? err.message : String(err);
705
+ process.stderr.write(`[kanban-bridge] kanban_next failed for project ${cfg.projectId}: ${msg}\n` +
706
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
620
707
  return { content: [{ type: "text", text: `Error: ${msg}` }], details: { isError: true } };
621
708
  }
622
709
  },
@@ -650,7 +737,12 @@ const kanbanDeleteTool = {
650
737
  };
651
738
  }
652
739
  catch (err) {
740
+ if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
741
+ throw err; // Propagate to withReauthRetry for OAuth handling
742
+ }
653
743
  const msg = err instanceof Error ? err.message : String(err);
744
+ process.stderr.write(`[kanban-bridge] kanban_delete failed for project ${cfg.projectId}: ${msg}\n` +
745
+ `${err instanceof Error && err.stack ? err.stack + '\n' : ''}`);
654
746
  return { content: [{ type: "text", text: `Error deleting task: ${msg}` }], details: { isError: true } };
655
747
  }
656
748
  },
@@ -671,15 +763,17 @@ function withReauthRetry(toolName, execute) {
671
763
  if (err instanceof GraphQLError && (err.status === 401 || err.status === 403)) {
672
764
  if (_reauthAttempted) {
673
765
  return {
674
- content: [{ type: "text", text: `Authentication failed (HTTP ${err.status}). Please run \`spectral login\` to re-authenticate.` }],
766
+ content: [{ type: "text", text: `Error: Authentication failed (HTTP ${err.status}). Please run \`spectral login\` to re-authenticate.` }],
675
767
  details: { isError: true },
676
768
  };
677
769
  }
678
770
  _reauthAttempted = true;
679
771
  process.stderr.write(`[kanban-bridge] Auth error (${err.status}) on ${toolName}. Attempting OAuth re-login...\n`);
680
- const result = await ensureAuthenticated();
681
- if (result.ok && result.config) {
772
+ const oauthResult = await runOAuthFlow();
773
+ if (oauthResult.ok) {
682
774
  process.stderr.write(`[kanban-bridge] Re-authenticated — retrying ${toolName}...\n`);
775
+ // Reset reauth flag so resolveKanbanConfig can use the fresh token
776
+ _reauthAttempted = false;
683
777
  try {
684
778
  return await execute(toolCallId, params, signal, onUpdate, ctx);
685
779
  }
@@ -692,11 +786,17 @@ function withReauthRetry(toolName, execute) {
692
786
  }
693
787
  }
694
788
  return {
695
- content: [{ type: "text", text: `Re-authentication failed: ${result.reason ?? "unknown"}. Please run \`spectral login\` manually.` }],
789
+ content: [{ type: "text", text: `Error: Re-authentication failed: ${oauthResult.reason ?? "unknown"}. Please run \`spectral login\` manually.` }],
696
790
  details: { isError: true },
697
791
  };
698
792
  }
699
- throw err;
793
+ const msg = err instanceof Error ? err.message : String(err);
794
+ process.stderr.write(`[kanban-bridge] ${toolName} unexpected failure: ${msg}\n` +
795
+ `${err instanceof Error && err.stack ? err.stack + "\n" : ""}`);
796
+ return {
797
+ content: [{ type: "text", text: `Kanban ${toolName} failed: ${msg}` }],
798
+ details: { isError: true },
799
+ };
700
800
  }
701
801
  };
702
802
  }
@@ -727,8 +827,7 @@ export default async function kanbanBridgeExtension(ext) {
727
827
  }
728
828
  // Verify config is resolvable on startup (non-fatal — tools work lazily too)
729
829
  ext.on("session_start", async (_event, ctx) => {
730
- _studioBinding = await readStudioBinding(ctx.cwd);
731
- const cfg = await resolveKanbanConfig();
830
+ const cfg = await resolveKanbanConfig(undefined, ctx.cwd);
732
831
  if (cfg) {
733
832
  process.stderr.write(`[kanban-bridge] Registered ${registered} Kanban tool(s). Connected to project ${cfg.projectId}.\n`);
734
833
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Web Fetch native extension for Spectral.
3
+ *
4
+ * Registers a single tool, `web_fetch`, which fetches a public URL and returns
5
+ * its content as text, markdown, or raw HTML. The model provides URLs itself;
6
+ * there is no search capability.
7
+ */
8
+ import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
9
+ export default function webExtension(ext: ExtensionAPI): Promise<void>;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpE,wBAA8B,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E"}
@@ -0,0 +1,11 @@
1
+ import { registerWebFetchTool } from "./tools/fetch.js";
2
+ export default async function webExtension(ext) {
3
+ try {
4
+ registerWebFetchTool(ext);
5
+ process.stderr.write("[web] Registered web_fetch tool.\n");
6
+ }
7
+ catch (err) {
8
+ const msg = err instanceof Error ? err.message : String(err);
9
+ process.stderr.write(`[web] Failed to register web_fetch tool: ${msg}\n`);
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
2
+ export declare function registerWebFetchTool(ext: ExtensionAPI): void;
3
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AA+BxG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA2D5D"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * web_fetch tool definition.
3
+ */
4
+ import { Type } from "typebox";
5
+ import { truncateTail } from "../../../sdk/coding-agent/core/tools/truncate.js";
6
+ import { fetchWebPage, WebFetchError } from "../utils/fetcher.js";
7
+ import { convertContent } from "../utils/parser.js";
8
+ const MAX_OUTPUT_LINES = 4000;
9
+ const MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
10
+ const webFetchSchema = Type.Object({
11
+ url: Type.String({
12
+ description: "The URL to fetch. Must begin with http:// or https://.",
13
+ pattern: "^https?://",
14
+ }),
15
+ format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("markdown"), Type.Literal("html"), Type.Literal("json")], {
16
+ default: "markdown",
17
+ description: "Output format: text, markdown (default), html, or json.",
18
+ })),
19
+ timeout: Type.Optional(Type.Number({
20
+ default: 30,
21
+ minimum: 1,
22
+ maximum: 120,
23
+ description: "Request timeout in seconds (default 30, maximum 120).",
24
+ })),
25
+ });
26
+ export function registerWebFetchTool(ext) {
27
+ const tool = {
28
+ name: "web_fetch",
29
+ label: "Web Fetch",
30
+ description: "Fetch a public URL and return its content as readable text, markdown, raw HTML, or pretty JSON. " +
31
+ "Useful for reading documentation, GitHub pages, Reddit JSON endpoints, and other web content. " +
32
+ "Supports HTTP/HTTPS only and rejects binary content.",
33
+ promptSnippet: "`web_fetch { url, format?, timeout? }` — fetch a web page as markdown/text/HTML/JSON",
34
+ parameters: webFetchSchema,
35
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
36
+ const { url, format = "markdown", timeout = 30 } = params;
37
+ try {
38
+ const fetchResult = await fetchWebPage(url, {
39
+ timeoutSeconds: timeout,
40
+ signal,
41
+ });
42
+ const converted = await convertContent(fetchResult.content, format);
43
+ const truncation = truncateTail(converted, {
44
+ maxLines: MAX_OUTPUT_LINES,
45
+ maxBytes: MAX_OUTPUT_BYTES,
46
+ });
47
+ const output = truncation.content;
48
+ const truncationNotice = truncation.truncated
49
+ ? `\n\n[Output truncated: exceeded ${truncation.truncatedBy === "bytes" ? `${MAX_OUTPUT_BYTES} bytes` : `${MAX_OUTPUT_LINES} lines`}. Original size: ${truncation.totalBytes} bytes, ${truncation.totalLines} lines.]`
50
+ : "";
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes${truncationNotice}\n\n${output}`,
56
+ },
57
+ ],
58
+ details: {
59
+ isError: false,
60
+ url: fetchResult.finalUrl,
61
+ contentType: fetchResult.contentType,
62
+ format,
63
+ output,
64
+ truncated: truncation.truncated,
65
+ truncatedBy: truncation.truncatedBy,
66
+ },
67
+ };
68
+ }
69
+ catch (err) {
70
+ const reason = err instanceof WebFetchError || err instanceof Error ? err.message : String(err);
71
+ const message = `Unable to fetch ${url}: ${reason}`;
72
+ return {
73
+ content: [{ type: "text", text: `❌ ${message}` }],
74
+ details: { isError: true, message },
75
+ };
76
+ }
77
+ },
78
+ };
79
+ ext.registerTool(tool);
80
+ }
@@ -0,0 +1,34 @@
1
+ export declare class WebFetchError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface WebFetchOptions {
5
+ timeoutSeconds?: number;
6
+ signal?: AbortSignal;
7
+ userAgent?: string;
8
+ /**
9
+ * Bypass SSRF private-host/IP blocking. Used only by tests that spin up a
10
+ * local mock HTTP server; never exposed through the `web_fetch` tool schema.
11
+ */
12
+ allowPrivateHosts?: boolean;
13
+ }
14
+ export interface WebFetchResult {
15
+ url: string;
16
+ finalUrl: string;
17
+ contentType: string;
18
+ content: string;
19
+ contentLengthBytes: number;
20
+ }
21
+ /**
22
+ * Validate a URL with SSRF protection.
23
+ *
24
+ * Throws WebFetchError for invalid URLs, non-http(s) protocols, blocked
25
+ * hostnames, or hostnames that resolve to private IP addresses.
26
+ */
27
+ export declare function validateUrl(urlStr: string, allowPrivateHosts?: boolean): Promise<URL>;
28
+ /** Validate that the Content-Type is readable text, not binary media. */
29
+ export declare function validateContentType(contentType: string): void;
30
+ /**
31
+ * Fetch a public URL and return its decoded text content.
32
+ */
33
+ export declare function fetchWebPage(url: string, opts?: WebFetchOptions): Promise<WebFetchResult>;
34
+ //# sourceMappingURL=fetcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/utils/fetcher.ts"],"names":[],"mappings":"AASA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAuDD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,iBAAiB,UAAQ,GACxB,OAAO,CAAC,GAAG,CAAC,CAoDd;AAcD,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CA+C7D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,cAAc,CAAC,CAoIzB"}