@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.
- package/CHANGELOG.md +6 -0
- package/README.md +43 -0
- package/dist/agent/index.d.ts +6 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +133 -14
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +12 -0
- package/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +42 -52
- package/dist/extensions/kanban-bridge.d.ts.map +1 -1
- package/dist/extensions/kanban-bridge.js +126 -27
- package/dist/extensions/web/index.d.ts +10 -0
- package/dist/extensions/web/index.d.ts.map +1 -0
- package/dist/extensions/web/index.js +11 -0
- package/dist/extensions/web/tools/fetch.d.ts +3 -0
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/web/tools/fetch.js +80 -0
- package/dist/extensions/web/utils/fetcher.d.ts +34 -0
- package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/web/utils/fetcher.js +289 -0
- package/dist/extensions/web/utils/parser.d.ts +15 -0
- package/dist/extensions/web/utils/parser.d.ts.map +1 -0
- package/dist/extensions/web/utils/parser.js +205 -0
- package/dist/memory/branch.d.ts.map +1 -1
- package/dist/memory/branch.js +16 -8
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +3 -2
- package/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +21 -2
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +19 -3
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/tool-output-compressor.d.ts +14 -20
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +391 -137
- package/dist/memory/tools/compact-context.d.ts +5 -0
- package/dist/memory/tools/compact-context.d.ts.map +1 -0
- package/dist/memory/tools/compact-context.js +772 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +73 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +408 -210
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/server/agent-bridge.d.ts +12 -5
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +65 -17
- package/dist/server/handlers/paths-home.d.ts +11 -0
- package/dist/server/handlers/paths-home.d.ts.map +1 -0
- package/dist/server/handlers/paths-home.js +10 -0
- package/dist/server/handlers/paths-list.d.ts +24 -0
- package/dist/server/handlers/paths-list.d.ts.map +1 -0
- package/dist/server/handlers/paths-list.js +56 -0
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +2 -1
- package/dist/server/handlers/projects.js +4 -4
- package/dist/server/handlers/sessions.d.ts +11 -1
- package/dist/server/handlers/sessions.d.ts.map +1 -1
- package/dist/server/handlers/sessions.js +22 -2
- package/dist/server/handlers/settings.d.ts +18 -1
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +63 -13
- package/dist/server/session-stream.d.ts +65 -37
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +525 -119
- package/dist/server/storage.d.ts +6 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +72 -9
- package/dist/server/wire.d.ts +129 -2
- package/dist/server/wire.d.ts.map +1 -1
- 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
|
|
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
|
-
|
|
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 {
|
|
170
|
-
import {
|
|
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
|
-
|
|
174
|
-
async function
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
681
|
-
if (
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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"}
|