@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
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 —
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -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 (
|
|
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;
|
|
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"}
|
package/dist/agent/index.js
CHANGED
|
@@ -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 (
|
|
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
|
|
196
|
-
|
|
197
|
-
const combinedPrompt =
|
|
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([
|
|
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
|
|
293
|
-
//
|
|
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";
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 2b. OAuth: open browser for Aexol Studio authorization.
|
|
10
10
|
* 3. Verify reachability + auth by calling tools/list against the URL.
|
|
11
11
|
* 4. On success: persist to ~/.spectral/config.json (mode 0600).
|
|
12
|
+
* 5. Chain OAuth to also obtain a user JWT for Kanban/Studio features.
|
|
12
13
|
*
|
|
13
14
|
* Exits non-zero on any failure so shell scripts can detect it.
|
|
14
15
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqCzF;AAED,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAqF9C"}
|
package/dist/commands/login.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 2b. OAuth: open browser for Aexol Studio authorization.
|
|
10
10
|
* 3. Verify reachability + auth by calling tools/list against the URL.
|
|
11
11
|
* 4. On success: persist to ~/.spectral/config.json (mode 0600).
|
|
12
|
+
* 5. Chain OAuth to also obtain a user JWT for Kanban/Studio features.
|
|
12
13
|
*
|
|
13
14
|
* Exits non-zero on any failure so shell scripts can detect it.
|
|
14
15
|
*
|
|
@@ -20,6 +21,7 @@ import { password, select } from "@inquirer/prompts";
|
|
|
20
21
|
import pc from "picocolors";
|
|
21
22
|
import { TEAM_API_KEY_PREFIX, getApiUrl, getConfigFile, validateTeamApiKey, writeConfig, } from "../config.js";
|
|
22
23
|
import { AexolMcpClient, AexolMcpError } from "../mcp-client.js";
|
|
24
|
+
import { runOAuthFlow } from "../auth-helper.js";
|
|
23
25
|
/**
|
|
24
26
|
* Pure login logic. Throws `Error` with one of these stable message prefixes:
|
|
25
27
|
* - "Invalid API key prefix" — local validation failed
|
|
@@ -126,4 +128,14 @@ export async function runLogin() {
|
|
|
126
128
|
}
|
|
127
129
|
process.stdout.write(pc.green(`✓ Authenticated. Found ${result.toolCount} tools.\n`));
|
|
128
130
|
process.stdout.write(pc.dim(`Saved credentials to ${getConfigFile()}\n`));
|
|
131
|
+
// Chain OAuth for Kanban/Studio features (non-fatal — MCP tools work without it)
|
|
132
|
+
process.stdout.write(pc.dim("\nAuthorizing for Aexol Studio features (Kanban)...\n"));
|
|
133
|
+
const oauthResult = await runOAuthFlow(apiUrl);
|
|
134
|
+
if (oauthResult.ok) {
|
|
135
|
+
process.stdout.write(pc.green("✓ CLI authorized for Studio/Kanban.\n"));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
process.stderr.write(pc.yellow(`⚠ OAuth authorization skipped (${oauthResult.reason}). Kanban/Studio features may not be available.\n`));
|
|
139
|
+
process.stderr.write(pc.dim(" Run `spectral login` and choose OAuth to set it up later.\n"));
|
|
140
|
+
}
|
|
129
141
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aexol-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/aexol-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"aexol-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/aexol-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,8BAA8B,CAAC;AAiKnG,wBAA8B,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAsShF"}
|
|
@@ -80,22 +80,16 @@ function toLabel(name) {
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
// Studio binding – in-memory cache, refreshed on startup & by local tools
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let bindingMtimeMs = null;
|
|
83
|
+
/** Per-cwd binding cache — eliminates races when multiple sessions share a process. */
|
|
84
|
+
const bindingCache = new Map();
|
|
86
85
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* new conversation. The AgentBridge's reloadProjectBindings() refreshes model
|
|
91
|
-
* proxy routing, but the aexol-mcp extension's currentBinding is only read at
|
|
92
|
-
* session_start otherwise. mtime caching keeps the common case (no change) at
|
|
93
|
-
* a single cheap stat() per remote_* call.
|
|
86
|
+
* Per-cwd binding reader. Reads `.aexol/aexol.jsonc` under the given
|
|
87
|
+
* working directory with mtime caching. The cache key is the cwd itself,
|
|
88
|
+
* so concurrent sessions serving different projects never interfere.
|
|
94
89
|
*/
|
|
95
|
-
async function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const bindingFile = join(sessionCwd, ".aexol", "aexol.jsonc");
|
|
90
|
+
async function refreshBindingForCwd(cwd) {
|
|
91
|
+
const bindingFile = join(cwd, ".aexol", "aexol.jsonc");
|
|
92
|
+
const cached = bindingCache.get(cwd);
|
|
99
93
|
let mtimeMs;
|
|
100
94
|
try {
|
|
101
95
|
mtimeMs = (await stat(bindingFile)).mtimeMs;
|
|
@@ -103,24 +97,23 @@ async function refreshBindingFromDisk() {
|
|
|
103
97
|
catch (err) {
|
|
104
98
|
const code = err.code;
|
|
105
99
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
bindingMtimeMs = null;
|
|
109
|
-
}
|
|
110
|
-
return;
|
|
100
|
+
bindingCache.set(cwd, { binding: null, mtimeMs: null });
|
|
101
|
+
return null;
|
|
111
102
|
}
|
|
112
103
|
// Non-fatal: keep the last known binding.
|
|
113
|
-
return;
|
|
104
|
+
return cached?.binding ?? null;
|
|
114
105
|
}
|
|
115
|
-
if (mtimeMs ===
|
|
116
|
-
return;
|
|
106
|
+
if (cached && mtimeMs === cached.mtimeMs)
|
|
107
|
+
return cached.binding;
|
|
117
108
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
const binding = await readStudioBinding(cwd);
|
|
110
|
+
bindingCache.set(cwd, { binding, mtimeMs });
|
|
111
|
+
return binding;
|
|
120
112
|
}
|
|
121
113
|
catch {
|
|
122
114
|
// Parse error etc. — keep last known binding, force re-read next time.
|
|
123
|
-
|
|
115
|
+
bindingCache.set(cwd, { binding: cached?.binding ?? null, mtimeMs: null });
|
|
116
|
+
return cached?.binding ?? null;
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
/**
|
|
@@ -141,15 +134,15 @@ const MODEL_INJECTING_TOOLS = new Set([
|
|
|
141
134
|
]);
|
|
142
135
|
const SPECTRAL_PROXY_USER_MODEL = "spectral-proxy-user-model";
|
|
143
136
|
const BUILT_IN_PREFIX = "built-in/";
|
|
144
|
-
function augmentParams(toolName, params, ctx) {
|
|
137
|
+
function augmentParams(toolName, params, ctx, binding) {
|
|
145
138
|
if (!toolName.startsWith("remote_"))
|
|
146
139
|
return { params };
|
|
147
140
|
const augmented = { ...params };
|
|
148
|
-
if (
|
|
149
|
-
augmented.teamId =
|
|
141
|
+
if (binding?.teamId && !("teamId" in params)) {
|
|
142
|
+
augmented.teamId = binding.teamId;
|
|
150
143
|
}
|
|
151
|
-
if (
|
|
152
|
-
augmented.projectId =
|
|
144
|
+
if (binding?.projectId && !("projectId" in params)) {
|
|
145
|
+
augmented.projectId = binding.projectId;
|
|
153
146
|
}
|
|
154
147
|
if (MODEL_INJECTING_TOOLS.has(toolName)) {
|
|
155
148
|
const model = ctx.model;
|
|
@@ -185,13 +178,12 @@ export default async function aexolMcpExtension(ext) {
|
|
|
185
178
|
const client = new AexolMcpClient(apiUrl, token);
|
|
186
179
|
// ---- Load local Studio binding on session start --------------------------
|
|
187
180
|
ext.on("session_start", async (_event, ctx) => {
|
|
188
|
-
sessionCwd = ctx.cwd;
|
|
189
181
|
try {
|
|
190
|
-
await
|
|
191
|
-
if (
|
|
192
|
-
const displayName =
|
|
193
|
-
process.stderr.write(`[aexol-mcp] Studio binding found: ${displayName} (${
|
|
194
|
-
(
|
|
182
|
+
const binding = await refreshBindingForCwd(ctx.cwd);
|
|
183
|
+
if (binding) {
|
|
184
|
+
const displayName = binding.name ?? binding.projectId;
|
|
185
|
+
process.stderr.write(`[aexol-mcp] Studio binding found: ${displayName} (${binding.projectId})` +
|
|
186
|
+
(binding.teamId ? `, team ${binding.teamId}` : "") +
|
|
195
187
|
`\n`);
|
|
196
188
|
}
|
|
197
189
|
}
|
|
@@ -253,8 +245,8 @@ export default async function aexolMcpExtension(ext) {
|
|
|
253
245
|
description: tool.description ?? "",
|
|
254
246
|
parameters,
|
|
255
247
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
256
|
-
await
|
|
257
|
-
const augmented = augmentParams(tool.name, params, ctx);
|
|
248
|
+
const binding = await refreshBindingForCwd(ctx.cwd);
|
|
249
|
+
const augmented = augmentParams(tool.name, params, ctx, binding);
|
|
258
250
|
if ("error" in augmented) {
|
|
259
251
|
return {
|
|
260
252
|
content: [{ type: "text", text: augmented.error }],
|
|
@@ -335,10 +327,8 @@ export default async function aexolMcpExtension(ext) {
|
|
|
335
327
|
};
|
|
336
328
|
try {
|
|
337
329
|
await writeStudioBinding(binding, ctx.cwd);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
currentBinding = binding;
|
|
341
|
-
bindingMtimeMs = null;
|
|
330
|
+
// Invalidate the per-cwd cache so the next refreshBindingForCwd re-reads.
|
|
331
|
+
bindingCache.set(ctx.cwd, { binding, mtimeMs: null });
|
|
342
332
|
const displayName = binding.name ?? binding.projectId;
|
|
343
333
|
return {
|
|
344
334
|
content: [{
|
|
@@ -377,20 +367,19 @@ export default async function aexolMcpExtension(ext) {
|
|
|
377
367
|
properties: {},
|
|
378
368
|
},
|
|
379
369
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
380
|
-
|
|
370
|
+
const binding = await refreshBindingForCwd(ctx.cwd);
|
|
371
|
+
if (!binding) {
|
|
381
372
|
return {
|
|
382
373
|
content: [{ type: "text", text: "No Studio binding found in this directory — nothing to unbind." }],
|
|
383
374
|
details: { isError: false },
|
|
384
375
|
};
|
|
385
376
|
}
|
|
386
|
-
const displayName =
|
|
387
|
-
const projectId =
|
|
377
|
+
const displayName = binding.name ?? binding.projectId;
|
|
378
|
+
const projectId = binding.projectId;
|
|
388
379
|
try {
|
|
389
380
|
const deleted = await deleteStudioBinding(ctx.cwd);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
currentBinding = null;
|
|
393
|
-
bindingMtimeMs = null;
|
|
381
|
+
// Invalidate the per-cwd cache.
|
|
382
|
+
bindingCache.set(ctx.cwd, { binding: null, mtimeMs: null });
|
|
394
383
|
if (deleted) {
|
|
395
384
|
return {
|
|
396
385
|
content: [{
|
|
@@ -430,8 +419,9 @@ export default async function aexolMcpExtension(ext) {
|
|
|
430
419
|
type: "object",
|
|
431
420
|
properties: {},
|
|
432
421
|
},
|
|
433
|
-
async execute() {
|
|
434
|
-
|
|
422
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
423
|
+
const binding = await refreshBindingForCwd(ctx.cwd);
|
|
424
|
+
if (!binding) {
|
|
435
425
|
return {
|
|
436
426
|
content: [{
|
|
437
427
|
type: "text",
|
|
@@ -444,7 +434,7 @@ export default async function aexolMcpExtension(ext) {
|
|
|
444
434
|
return {
|
|
445
435
|
content: [{
|
|
446
436
|
type: "text",
|
|
447
|
-
text: JSON.stringify(
|
|
437
|
+
text: JSON.stringify(binding, null, 2),
|
|
448
438
|
}],
|
|
449
439
|
details: { isError: false },
|
|
450
440
|
};
|
|
@@ -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;
|
|
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"}
|