@aexol/spectral 0.9.58 → 0.9.60

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 (90) 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/extensions/kanban-bridge.d.ts.map +1 -1
  7. package/dist/extensions/kanban-bridge.js +21 -1
  8. package/dist/extensions/web/index.d.ts +10 -0
  9. package/dist/extensions/web/index.d.ts.map +1 -0
  10. package/dist/extensions/web/index.js +11 -0
  11. package/dist/extensions/web/tools/fetch.d.ts +3 -0
  12. package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
  13. package/dist/extensions/web/tools/fetch.js +80 -0
  14. package/dist/extensions/web/utils/fetcher.d.ts +34 -0
  15. package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
  16. package/dist/extensions/web/utils/fetcher.js +289 -0
  17. package/dist/extensions/web/utils/parser.d.ts +15 -0
  18. package/dist/extensions/web/utils/parser.d.ts.map +1 -0
  19. package/dist/extensions/web/utils/parser.js +205 -0
  20. package/dist/memory/branch.d.ts.map +1 -1
  21. package/dist/memory/branch.js +16 -8
  22. package/dist/memory/commands/status.d.ts.map +1 -1
  23. package/dist/memory/commands/status.js +3 -2
  24. package/dist/memory/config.d.ts +12 -0
  25. package/dist/memory/config.d.ts.map +1 -1
  26. package/dist/memory/config.js +21 -2
  27. package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
  28. package/dist/memory/hooks/compaction-trigger.js +19 -3
  29. package/dist/memory/index.d.ts.map +1 -1
  30. package/dist/memory/index.js +2 -0
  31. package/dist/memory/testing/random-text.js +1 -1
  32. package/dist/memory/tool-output-compressor.d.ts +14 -20
  33. package/dist/memory/tool-output-compressor.d.ts.map +1 -1
  34. package/dist/memory/tool-output-compressor.js +391 -137
  35. package/dist/memory/tools/compact-context.d.ts +5 -0
  36. package/dist/memory/tools/compact-context.d.ts.map +1 -0
  37. package/dist/memory/tools/compact-context.js +772 -0
  38. package/dist/relay/dispatcher.d.ts +1 -1
  39. package/dist/relay/dispatcher.d.ts.map +1 -1
  40. package/dist/relay/dispatcher.js +61 -1
  41. package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
  42. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  43. package/dist/sdk/coding-agent/core/agent-session.js +408 -210
  44. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
  45. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
  46. package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
  47. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
  48. package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
  49. package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
  50. package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
  51. package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
  52. package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
  53. package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
  54. package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
  55. package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
  56. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  57. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
  58. package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
  59. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  60. package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
  61. package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
  62. package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
  63. package/dist/sdk/coding-agent/index.d.ts +1 -1
  64. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  65. package/dist/server/agent-bridge.d.ts +12 -5
  66. package/dist/server/agent-bridge.d.ts.map +1 -1
  67. package/dist/server/agent-bridge.js +65 -17
  68. package/dist/server/handlers/paths-home.d.ts +11 -0
  69. package/dist/server/handlers/paths-home.d.ts.map +1 -0
  70. package/dist/server/handlers/paths-home.js +10 -0
  71. package/dist/server/handlers/paths-list.d.ts +24 -0
  72. package/dist/server/handlers/paths-list.d.ts.map +1 -0
  73. package/dist/server/handlers/paths-list.js +56 -0
  74. package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
  75. package/dist/server/handlers/paths-pick-directory.js +2 -1
  76. package/dist/server/handlers/sessions.d.ts +11 -1
  77. package/dist/server/handlers/sessions.d.ts.map +1 -1
  78. package/dist/server/handlers/sessions.js +22 -2
  79. package/dist/server/handlers/settings.d.ts +18 -1
  80. package/dist/server/handlers/settings.d.ts.map +1 -1
  81. package/dist/server/handlers/settings.js +63 -13
  82. package/dist/server/session-stream.d.ts +65 -37
  83. package/dist/server/session-stream.d.ts.map +1 -1
  84. package/dist/server/session-stream.js +525 -119
  85. package/dist/server/storage.d.ts +3 -0
  86. package/dist/server/storage.d.ts.map +1 -1
  87. package/dist/server/storage.js +25 -0
  88. package/dist/server/wire.d.ts +127 -2
  89. package/dist/server/wire.d.ts.map +1 -1
  90. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to `@aexol/spectral` are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Added
9
+
10
+ - Added built-in `web_fetch` tool for fetching public web pages as text,
11
+ Markdown, or raw HTML. Includes SSRF protection, redirect handling, timeout
12
+ and size limits, plus unit and integration tests.
13
+
8
14
  ## [0.9.40] - 2026-06-22
9
15
 
10
16
  ## [0.9.39] - 2026-06-22
package/README.md CHANGED
@@ -151,6 +151,49 @@ sends it anywhere. Note that `spectral serve` does **not** use this file — it
151
151
  routes inference through the backend proxy instead (see
152
152
  [How inference is routed](#how-inference-is-routed)).
153
153
 
154
+ ## Web Fetch tool
155
+
156
+ Spectral ships a built-in `web_fetch` tool (enabled by default) for fetching
157
+ public web pages and returning their content as plain text, Markdown, or raw
158
+ HTML. It is intended for quickly reading static pages, documentation, GitHub
159
+ repositories, JSON endpoints, and other public HTTP/HTTPS resources.
160
+
161
+ ### Parameters
162
+
163
+ | Parameter | Type | Default | Description |
164
+ | --------- | -------- | ---------- | --------------------------------------------------------------------------- |
165
+ | `url` | string | required | Public URL to fetch. Must begin with `http://` or `https://`. |
166
+ | `format` | string | `markdown` | Output format: `text`, `markdown`, or `html`. |
167
+ | `timeout` | number | `30` | Request timeout in seconds, clamped to the range `1`–`120`. |
168
+
169
+ ### When to use `web_fetch`
170
+
171
+ Use `web_fetch` when you only need to **read** publicly available content:
172
+
173
+ - Static documentation sites and README pages.
174
+ - GitHub file, blob, or raw URLs.
175
+ - Public JSON/XML APIs (e.g., Reddit JSON endpoints).
176
+ - Simple marketing pages or blogs that render their main content server-side.
177
+
178
+ `web_fetch` is fast, lightweight, and does not require a browser. It applies
179
+ SSRF protection by rejecting `file://`, `ftp://`, `localhost`, and private IP
180
+ addresses, and it limits responses to 5 MB.
181
+
182
+ ### When to use the Browser extension instead
183
+
184
+ Use the full **Browser Automation** extension (also enabled by default) when the
185
+ page requires a real browser:
186
+
187
+ - JavaScript-rendered SPAs where the markup is produced in the browser.
188
+ - Pages that need interaction (clicks, scrolling, form input) before the
189
+ relevant content appears.
190
+ - Sites that block simple HTTP clients or require cookies / session state.
191
+ - Screenshots, visual verification, or network-traffic inspection.
192
+
193
+ The Browser extension controls a real Chromium instance via Playwright and can
194
+ do everything `web_fetch` can, plus interaction, screenshots, and JavaScript
195
+ evaluation.
196
+
154
197
  ## Multiple machines
155
198
 
156
199
  You can run `spectral serve` on as many machines as you like under one team —
@@ -18,14 +18,19 @@
18
18
  *
19
19
  * Subagents use an in-process `agentLoop()` for:
20
20
  * - Clean context isolation (fresh messages array)
21
- * - Combined system prompt (main agent + subagent-specific instructions)
21
+ * - Combined system prompt (shared/base prompt + subagent-specific instructions)
22
22
  * - Tool filtering (only the tools specified in agent config)
23
23
  * - Model selection (session model preferred, agent-configured as fallback)
24
24
  * - Signal propagation (abort passthrough)
25
25
  * - Access to observational memory via the recall tool
26
26
  */
27
27
  import { type ExtensionAPI } from "../sdk/coding-agent/index.js";
28
+ import { type AgentConfig } from "./agents.js";
29
+ import type { ServerEvent } from "../server/wire.ts";
30
+ export declare function stripInjectedPrimaryAgentPrompt(systemPrompt: string | undefined): string | undefined;
31
+ export declare function buildSubagentSystemPrompt(baseSystemPrompt: string | undefined, agent: Pick<AgentConfig, "name" | "systemPrompt">): string;
28
32
  export default function subagentExtension(ext: ExtensionAPI, opts?: {
29
33
  disabledAgents?: string[];
34
+ emitSubagentEvent?: (event: ServerEvent) => void;
30
35
  }): void;
31
36
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAsftC,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA+UvG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAYH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,KAAK,WAAW,EAAmC,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsFrD,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpG;AAED,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,GAC/C,MAAM,CAKR;AAkdD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;CAAE,GACpF,IAAI,CAgZN"}
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Subagents use an in-process `agentLoop()` for:
20
20
  * - Clean context isolation (fresh messages array)
21
- * - Combined system prompt (main agent + subagent-specific instructions)
21
+ * - Combined system prompt (shared/base prompt + subagent-specific instructions)
22
22
  * - Tool filtering (only the tools specified in agent config)
23
23
  * - Model selection (session model preferred, agent-configured as fallback)
24
24
  * - Signal propagation (abort passthrough)
@@ -40,6 +40,8 @@ import { wrapToolDefinition } from "../sdk/coding-agent/core/tools/tool-definiti
40
40
  // ---------------------------------------------------------------------------
41
41
  const MAX_PARALLEL_TASKS = 8;
42
42
  const MAX_CONCURRENCY = 4;
43
+ const PRIMARY_AGENT_PROMPT_HEADER = "# Primary Agent:";
44
+ const PRIMARY_AGENT_PROMPT_DELIMITER = "\n\n---\n\n";
43
45
  // ---------------------------------------------------------------------------
44
46
  // Helpers — formatting
45
47
  // ---------------------------------------------------------------------------
@@ -73,6 +75,52 @@ function formatUsageStats(usage, model) {
73
75
  parts.push(model);
74
76
  return parts.join(" ");
75
77
  }
78
+ function isRecord(value) {
79
+ return value !== null && typeof value === "object" && !Array.isArray(value);
80
+ }
81
+ function textFromToolResultContent(value) {
82
+ if (!isRecord(value) || !Array.isArray(value.content))
83
+ return undefined;
84
+ const parts = [];
85
+ for (const part of value.content) {
86
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
87
+ parts.push(part.text);
88
+ }
89
+ }
90
+ const text = parts.join("").trim();
91
+ return text ? text : undefined;
92
+ }
93
+ function compactProgressMessage(value) {
94
+ const text = typeof value === "string" ? value.trim() : textFromToolResultContent(value);
95
+ if (text)
96
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
97
+ if (isRecord(value) && typeof value.message === "string" && value.message.trim()) {
98
+ const message = value.message.trim();
99
+ return message.length > 240 ? `${message.slice(0, 237)}...` : message;
100
+ }
101
+ if (isRecord(value) && typeof value.error === "string" && value.error.trim()) {
102
+ const error = value.error.trim();
103
+ return error.length > 240 ? `${error.slice(0, 237)}...` : error;
104
+ }
105
+ return value === undefined ? undefined : "Tool progress updated";
106
+ }
107
+ export function stripInjectedPrimaryAgentPrompt(systemPrompt) {
108
+ if (!systemPrompt)
109
+ return systemPrompt;
110
+ if (!systemPrompt.startsWith(PRIMARY_AGENT_PROMPT_HEADER))
111
+ return systemPrompt;
112
+ const delimiterIndex = systemPrompt.indexOf(PRIMARY_AGENT_PROMPT_DELIMITER);
113
+ if (delimiterIndex === -1)
114
+ return undefined;
115
+ const basePrompt = systemPrompt.slice(delimiterIndex + PRIMARY_AGENT_PROMPT_DELIMITER.length);
116
+ return basePrompt.trim().length > 0 ? basePrompt : undefined;
117
+ }
118
+ export function buildSubagentSystemPrompt(baseSystemPrompt, agent) {
119
+ const sharedSystemPrompt = stripInjectedPrimaryAgentPrompt(baseSystemPrompt);
120
+ return sharedSystemPrompt
121
+ ? `${sharedSystemPrompt}\n\n---\n\nSubagent: ${agent.name}\n${agent.systemPrompt}`
122
+ : agent.systemPrompt;
123
+ }
76
124
  // ---------------------------------------------------------------------------
77
125
  // Helpers — message extraction
78
126
  // ---------------------------------------------------------------------------
@@ -176,7 +224,7 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
176
224
  // ---------------------------------------------------------------------------
177
225
  // Core — run a single subagent in-process via agentLoop
178
226
  // ---------------------------------------------------------------------------
179
- async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, ext, ctx) {
227
+ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx) {
180
228
  const normalizedAgentName = agentName.replace(/^@/, "");
181
229
  const agent = agents.find((a) => a.name === normalizedAgentName);
182
230
  if (!agent) {
@@ -192,11 +240,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
192
240
  step,
193
241
  };
194
242
  }
195
- // Build combined system prompt: main agent's + subagent's
196
- const mainSystemPrompt = ctx.getSystemPrompt();
197
- const combinedPrompt = mainSystemPrompt
198
- ? `${mainSystemPrompt}\n\n---\n\nSubagent: ${agent.name}\n${agent.systemPrompt}`
199
- : agent.systemPrompt;
243
+ // Build combined system prompt without leaking an injected primary-agent
244
+ // block into the isolated subagent context.
245
+ const combinedPrompt = buildSubagentSystemPrompt(ctx.getSystemPrompt(), agent);
200
246
  // Apply system prompt mutation if enabled in settings (opt-in)
201
247
  let effectivePrompt = combinedPrompt;
202
248
  if (ctx.settingsManager) {
@@ -259,11 +305,19 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
259
305
  };
260
306
  // Accumulated streaming text from text_delta events (reset per assistant turn).
261
307
  let streamingText = "";
308
+ // Accumulated thinking deltas for the live subagent sheet. This stays outside
309
+ // final `SingleResult` so the public subagent tool_result contract is unchanged.
310
+ let thinkingText = "";
262
311
  const emitUpdate = () => {
263
312
  if (onUpdate) {
313
+ const liveResult = {
314
+ ...currentResult,
315
+ ...(streamingText ? { streamingText } : {}),
316
+ ...(thinkingText ? { thinkingText } : {}),
317
+ };
264
318
  onUpdate({
265
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || "(running...)" }],
266
- details: makeDetails([currentResult]),
319
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || (thinkingText ? "(thinking...)" : "(running...)") }],
320
+ details: makeDetails([liveResult]),
267
321
  });
268
322
  }
269
323
  };
@@ -288,14 +342,19 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
288
342
  hadError = true;
289
343
  break;
290
344
  }
345
+ onInternalEvent?.(event, parentToolCallId);
291
346
  if (event.type === "message_update" && event.message?.role === "assistant") {
292
- // Forward streaming text deltas so the UI can show the subagent's
293
- // response as it's generated, not just the final result.
347
+ // Forward streaming text/thinking deltas so the UI can show the
348
+ // subagent process as it's generated, not just the final result.
294
349
  const inner = event.assistantMessageEvent;
295
350
  if (inner?.type === "text_delta" && inner.delta) {
296
351
  streamingText += inner.delta;
297
352
  emitUpdate();
298
353
  }
354
+ else if (inner?.type === "thinking_delta" && inner.delta) {
355
+ thinkingText += inner.delta;
356
+ emitUpdate();
357
+ }
299
358
  continue;
300
359
  }
301
360
  if (event.type === "message_end" && event.message) {
@@ -451,6 +510,66 @@ export default function subagentExtension(ext, opts) {
451
510
  projectAgentsDir: discovery.projectAgentsDir,
452
511
  results,
453
512
  });
513
+ // Forward internal subagent tool lifecycle events to the bridge so the
514
+ // frontend sheet can show a live activity log. text_delta streaming
515
+ // still flows through the existing onUpdate/subagent_progress path.
516
+ const emitSubagentEvent = opts?.emitSubagentEvent;
517
+ const subagentStepStartedAt = new Map();
518
+ const subagentStepArgs = new Map();
519
+ const emitSubagentToolProgress = emitSubagentEvent
520
+ ? (event, parentToolCallId) => {
521
+ if (event.type === "tool_execution_start") {
522
+ const startedAt = Date.now();
523
+ subagentStepStartedAt.set(event.toolCallId, startedAt);
524
+ subagentStepArgs.set(event.toolCallId, event.args);
525
+ emitSubagentEvent({
526
+ type: "subagent_tool_progress",
527
+ toolCallId: parentToolCallId,
528
+ stepId: event.toolCallId,
529
+ name: event.toolName,
530
+ args: event.args,
531
+ status: "running",
532
+ startedAt,
533
+ });
534
+ }
535
+ else if (event.type === "tool_execution_update") {
536
+ const startedAt = subagentStepStartedAt.get(event.toolCallId) ?? Date.now();
537
+ subagentStepStartedAt.set(event.toolCallId, startedAt);
538
+ subagentStepArgs.set(event.toolCallId, event.args);
539
+ emitSubagentEvent({
540
+ type: "subagent_tool_progress",
541
+ toolCallId: parentToolCallId,
542
+ stepId: event.toolCallId,
543
+ name: event.toolName,
544
+ args: event.args,
545
+ status: "running",
546
+ partialResult: event.partialResult,
547
+ message: compactProgressMessage(event.partialResult),
548
+ startedAt,
549
+ });
550
+ }
551
+ else if (event.type === "tool_execution_end") {
552
+ const startedAt = subagentStepStartedAt.get(event.toolCallId) ?? Date.now();
553
+ const finishedAt = Date.now();
554
+ const args = subagentStepArgs.get(event.toolCallId);
555
+ subagentStepStartedAt.delete(event.toolCallId);
556
+ subagentStepArgs.delete(event.toolCallId);
557
+ emitSubagentEvent({
558
+ type: "subagent_tool_progress",
559
+ toolCallId: parentToolCallId,
560
+ stepId: event.toolCallId,
561
+ name: event.toolName,
562
+ args,
563
+ status: event.isError ? "error" : "success",
564
+ result: event.result,
565
+ isError: event.isError,
566
+ message: event.isError ? compactProgressMessage(event.result) : undefined,
567
+ startedAt,
568
+ finishedAt,
569
+ });
570
+ }
571
+ }
572
+ : undefined;
454
573
  if (modeCount !== 1) {
455
574
  const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
456
575
  return {
@@ -513,7 +632,7 @@ export default function subagentExtension(ext, opts) {
513
632
  }
514
633
  }
515
634
  : undefined;
516
- const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), ext, ctx);
635
+ const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), _toolCallId, emitSubagentToolProgress, ext, ctx);
517
636
  results.push(result);
518
637
  const isError = result.exitCode !== 0 ||
519
638
  result.stopReason === "error" ||
@@ -591,7 +710,7 @@ export default function subagentExtension(ext, opts) {
591
710
  allResults[index] = partial.details.results[0];
592
711
  emitParallelUpdate();
593
712
  }
594
- }, makeDetails("parallel"), ext, ctx);
713
+ }, makeDetails("parallel"), _toolCallId, emitSubagentToolProgress, ext, ctx);
595
714
  allResults[index] = result;
596
715
  emitParallelUpdate();
597
716
  return result;
@@ -614,7 +733,7 @@ export default function subagentExtension(ext, opts) {
614
733
  }
615
734
  // ── Single mode ─────────────────────────────────────────────
616
735
  if (params.agent && params.task) {
617
- const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), ext, ctx);
736
+ const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), _toolCallId, emitSubagentToolProgress, ext, ctx);
618
737
  const isError = result.exitCode !== 0 ||
619
738
  result.stopReason === "error" ||
620
739
  result.stopReason === "aborted";
@@ -1 +1 @@
1
- {"version":3,"file":"kanban-bridge.d.ts","sourceRoot":"","sources":["../../src/extensions/kanban-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,8BAA8B,CAAC;AA09BnG,wBAA8B,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCpF"}
1
+ {"version":3,"file":"kanban-bridge.d.ts","sourceRoot":"","sources":["../../src/extensions/kanban-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,8BAA8B,CAAC;AAg/BnG,wBAA8B,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCpF"}
@@ -188,6 +188,8 @@ async function deleteKanbanTask(opts, taskId) {
188
188
  import { getApiUrl, getConfigFile, readConfig } from "../config.js";
189
189
  import { readStudioBinding } from "../studio-binding.js";
190
190
  import { ensureAuthenticated, runOAuthFlow } from "../auth-helper.js";
191
+ import { loadMachine } from "../relay/machine-store.js";
192
+ import { isJwtExpired } from "../relay/registration.js";
191
193
  import { stat } from "node:fs/promises";
192
194
  import { join } from "node:path";
193
195
  // Track whether we've already attempted reauth in this session to avoid loops
@@ -245,7 +247,25 @@ async function resolveKanbanConfig(explicitProjectId, cwd) {
245
247
  }
246
248
  process.stderr.write(`[kanban-bridge] OAuth failed: ${oauthResult.reason}. Kanban calls will likely fail.\n`);
247
249
  }
248
- const token = cfg.userJwt || cfg.teamApiKey || "";
250
+ // Prefer machine JWT (long-lived, no browser OAuth needed)
251
+ // Falls back to user JWT / team API key for non-serve contexts
252
+ let token = "";
253
+ let tokenSource = "";
254
+ const machine = await loadMachine();
255
+ if (machine?.machineJwt && !isJwtExpired(machine.machineJwt)) {
256
+ token = machine.machineJwt;
257
+ tokenSource = "machine-jwt";
258
+ process.stderr.write(`[kanban-bridge] Using machine JWT (machine: ${machine.machineName}, team: ${machine.teamId ?? "unknown"}).\n`);
259
+ }
260
+ else {
261
+ token = cfg.userJwt || cfg.teamApiKey || "";
262
+ if (cfg.userJwt) {
263
+ tokenSource = "user-jwt";
264
+ }
265
+ else if (cfg.teamApiKey) {
266
+ tokenSource = "team-api-key";
267
+ }
268
+ }
249
269
  if (!token) {
250
270
  if (!_reauthAttempted) {
251
271
  _reauthAttempted = true;
@@ -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"}