@aexol/spectral 0.9.234 → 0.9.241
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/README.md +47 -0
- package/dist/agent/index.d.ts +32 -5
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +120 -22
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -4
- package/dist/commands/desktop.d.ts +167 -0
- package/dist/commands/desktop.d.ts.map +1 -0
- package/dist/commands/desktop.js +540 -0
- package/dist/commands/serve.d.ts +9 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +78 -1
- package/dist/commands/update.d.ts +85 -5
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +288 -58
- package/dist/extensions/desktop-control/macos.d.ts.map +1 -1
- package/dist/extensions/desktop-control/macos.js +2 -2
- package/dist/mcp/server-manager.d.ts.map +1 -1
- package/dist/mcp/server-manager.js +6 -0
- package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -1
- package/dist/memory/tools/receive-agent-observations.js +12 -2
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +30 -2
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +8 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +5 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts +34 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts.map +1 -1
- package/dist/server/agent-bridge/tps-metrics.js +22 -0
- package/dist/server/agent-bridge.d.ts +9 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +83 -3
- package/dist/server/handlers/paths-list.d.ts +1 -1
- package/dist/server/handlers/paths-list.js +1 -1
- package/dist/server/inter-agent-broker.d.ts +12 -0
- package/dist/server/inter-agent-broker.d.ts.map +1 -1
- package/dist/server/inter-agent-broker.js +1 -0
- package/dist/server/preset-chain.d.ts +151 -0
- package/dist/server/preset-chain.d.ts.map +1 -0
- package/dist/server/preset-chain.js +250 -0
- package/dist/server/session-stream.d.ts +55 -1
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +203 -24
- package/dist/server/storage/rows.d.ts +2 -0
- package/dist/server/storage/rows.d.ts.map +1 -1
- package/dist/server/storage/schema.d.ts +2 -2
- package/dist/server/storage/schema.d.ts.map +1 -1
- package/dist/server/storage/schema.js +2 -1
- package/dist/server/storage.d.ts +7 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +142 -9
- package/dist/server/wire.d.ts +71 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/dist/utils/augment-path.d.ts +51 -0
- package/dist/utils/augment-path.d.ts.map +1 -0
- package/dist/utils/augment-path.js +234 -0
- package/dist/utils/open-browser.d.ts +55 -0
- package/dist/utils/open-browser.d.ts.map +1 -0
- package/dist/utils/open-browser.js +163 -0
- package/dist/utils/serve-state.d.ts +264 -0
- package/dist/utils/serve-state.d.ts.map +1 -0
- package/dist/utils/serve-state.js +563 -0
- package/dist/utils/standalone.d.ts +14 -0
- package/dist/utils/standalone.d.ts.map +1 -0
- package/dist/utils/standalone.js +23 -0
- package/package.json +1 -3
package/dist/commands/update.js
CHANGED
|
@@ -2,24 +2,58 @@
|
|
|
2
2
|
* `spectral update` — standalone binary self-update.
|
|
3
3
|
*
|
|
4
4
|
* Fetches the latest release manifest from api.aexol.ai, compares versions,
|
|
5
|
-
* downloads the new
|
|
6
|
-
*
|
|
5
|
+
* downloads the new artifacts, verifies their SHA256 digests, and installs them
|
|
6
|
+
* over the current installation.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Two manifest shapes are supported:
|
|
9
|
+
*
|
|
10
|
+
* - binary only (every manifest ever published): `url` + `sha256` pin the
|
|
11
|
+
* single-file binary. Only the binary is replaced — the companion assets an
|
|
12
|
+
* `install.sh` user has next to it (`data/systems/`, `mcp/`,
|
|
13
|
+
* `app-bridge.bundle.js`) keep the previous version, which is called out
|
|
14
|
+
* with a warning telling the user to re-run `curl | sh`.
|
|
15
|
+
* - release zip (ADDITIVE `zipUrl` + `zipSha256`, emitted by
|
|
16
|
+
* cli-node/scripts/generate-cli-manifest.sh once scripts/package-standalone.sh
|
|
17
|
+
* produced the archive): the zip carries the binary *and* the
|
|
18
|
+
* `<platform>-data/` companion directory. Both are installed with the same
|
|
19
|
+
* replace-don't-merge semantics as scripts/install.sh, so files deleted
|
|
20
|
+
* between releases do not linger.
|
|
21
|
+
*
|
|
22
|
+
* Nothing is installed unverified: the zip digest is checked before unpacking,
|
|
23
|
+
* the inner binary digest before swapping, and any mismatch aborts without
|
|
24
|
+
* touching the current installation.
|
|
25
|
+
*
|
|
26
|
+
* Non-standalone install modes (npm global install) are detected and the user
|
|
27
|
+
* is pointed at the correct update path instead.
|
|
10
28
|
*/
|
|
29
|
+
import { spawn } from "node:child_process";
|
|
11
30
|
import { createHash } from "node:crypto";
|
|
12
|
-
import { chmod, mkdtemp, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
31
|
+
import { chmod, cp, mkdir, mkdtemp, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
13
32
|
import { tmpdir } from "node:os";
|
|
14
33
|
import { basename, dirname, join } from "node:path";
|
|
15
34
|
import pc from "picocolors";
|
|
16
35
|
import { getVersion } from "../version.js";
|
|
36
|
+
import { isStandaloneBinary } from "../utils/standalone.js";
|
|
17
37
|
/** Base URL for the Aexol release manifest / binary downloads. */
|
|
18
38
|
const RELEASE_BASE = "https://api.aexol.ai/api/releases/download";
|
|
39
|
+
/** Stable entry point of the curl|sh installer (quoted in warnings). */
|
|
40
|
+
const INSTALLER_URL = "https://api.aexol.ai/api/releases/download/latest/install.sh";
|
|
41
|
+
/** Default `run`: a real spawn, stdio detached from the CLI's own output. */
|
|
42
|
+
const defaultRun = (cmd, args) => new Promise((resolveRun, rejectRun) => {
|
|
43
|
+
const child = spawn(cmd, args, { stdio: "ignore" });
|
|
44
|
+
child.on("error", rejectRun);
|
|
45
|
+
child.on("close", (code) => resolveRun(code ?? -1));
|
|
46
|
+
});
|
|
47
|
+
/** Companion directories install.sh places next to the binary. */
|
|
48
|
+
const COMPANION_DIRS = ["data", "mcp"];
|
|
49
|
+
/** Companion files install.sh places next to the binary. */
|
|
50
|
+
const COMPANION_FILES = ["app-bridge.bundle.js"];
|
|
51
|
+
/** Version stamp read by scripts/install.sh to skip a redundant reinstall. */
|
|
52
|
+
const VERSION_STAMP = ".spectral-version";
|
|
19
53
|
/**
|
|
20
54
|
* Resolve the current CLI version via the centralized getVersion() helper
|
|
21
|
-
* (src/version.ts), which works for npm install
|
|
22
|
-
*
|
|
55
|
+
* (src/version.ts), which works for npm install and standalone Bun binaries —
|
|
56
|
+
* both of which previously fell back to "0.0.0".
|
|
23
57
|
*/
|
|
24
58
|
function currentVersion() {
|
|
25
59
|
return getVersion();
|
|
@@ -59,28 +93,6 @@ function detectPlatform() {
|
|
|
59
93
|
return "win-x64";
|
|
60
94
|
return null;
|
|
61
95
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Whether the process is running as a Bun `--compile` standalone binary.
|
|
64
|
-
*
|
|
65
|
-
* In a compiled binary, `process.execPath` points at the binary itself (e.g.
|
|
66
|
-
* `spectral`) rather than a `node`/`bun` runtime, and Bun's embedded-files
|
|
67
|
-
* surface may be present.
|
|
68
|
-
*/
|
|
69
|
-
function isStandaloneBinary() {
|
|
70
|
-
// Bun-compiled binary with embedded files is a strong positive signal.
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
-
const Bun = globalThis.Bun;
|
|
73
|
-
if (Bun && typeof Bun === "object" && "embeddedFiles" in Bun) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
const execName = basename(process.execPath).replace(/\.exe$/i, "");
|
|
77
|
-
const isRuntime = execName === "node" || execName === "bun";
|
|
78
|
-
return !isRuntime;
|
|
79
|
-
}
|
|
80
|
-
/** True when invoked inside the Electron desktop app. */
|
|
81
|
-
function isElectronBundled() {
|
|
82
|
-
return Boolean(process.env.SPECTRAL_DESKTOP_VERSION);
|
|
83
|
-
}
|
|
84
96
|
/**
|
|
85
97
|
* Compute the SHA256 hex digest of a file's contents.
|
|
86
98
|
*/
|
|
@@ -133,6 +145,139 @@ async function fetchManifest(platform) {
|
|
|
133
145
|
const data = (await res.json());
|
|
134
146
|
return data;
|
|
135
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Unpack `zipPath` into a fresh sub-directory of `tempDir` using the first
|
|
150
|
+
* archive tool that is both present and successful:
|
|
151
|
+
*
|
|
152
|
+
* 1. `unzip` — the normal case (macOS, most Linux distros, Git Bash);
|
|
153
|
+
* 2. `tar -xf`— works where tar is bsdtar (macOS, Windows' built-in tar) and
|
|
154
|
+
* merely exits non-zero where tar is GNU tar (which has no zip support).
|
|
155
|
+
*
|
|
156
|
+
* `node:zlib` is deliberately not used: it decodes gzip, not the zip container.
|
|
157
|
+
*/
|
|
158
|
+
async function unpackReleaseZip(params) {
|
|
159
|
+
const { zipPath, tempDir, run } = params;
|
|
160
|
+
const destDir = join(tempDir, "unpacked");
|
|
161
|
+
await mkdir(destDir, { recursive: true });
|
|
162
|
+
const attempts = [
|
|
163
|
+
{ cmd: "unzip", args: ["-q", "-o", zipPath, "-d", destDir] },
|
|
164
|
+
{ cmd: "tar", args: ["-xf", zipPath, "-C", destDir] },
|
|
165
|
+
];
|
|
166
|
+
const notes = [];
|
|
167
|
+
for (const attempt of attempts) {
|
|
168
|
+
try {
|
|
169
|
+
const code = await run(attempt.cmd, attempt.args);
|
|
170
|
+
if (code === 0)
|
|
171
|
+
return { ok: true, destDir, notes: [...notes, `${attempt.cmd} ok`] };
|
|
172
|
+
notes.push(`${attempt.cmd} exited with code ${code}`);
|
|
173
|
+
}
|
|
174
|
+
catch (runErr) {
|
|
175
|
+
const msg = runErr instanceof Error ? runErr.message : String(runErr);
|
|
176
|
+
notes.push(`${attempt.cmd} unavailable (${msg})`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { ok: false, notes };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Locate the `<platform>-data/` companion directory at the root of an unpacked
|
|
183
|
+
* release zip (the layout defined by scripts/package-standalone.sh).
|
|
184
|
+
*/
|
|
185
|
+
async function findDataDir(stageDir) {
|
|
186
|
+
const entries = await readdir(stageDir, { withFileTypes: true });
|
|
187
|
+
const hit = entries.find((e) => e.isDirectory() && e.name.endsWith("-data"));
|
|
188
|
+
return hit ? join(stageDir, hit.name) : null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pick the binary out of an unpacked release zip. The archive ships it as
|
|
192
|
+
* `spectral` / `spectral.exe` (scripts/package-standalone.sh renames it), while
|
|
193
|
+
* a user may have renamed their own install — so the live name is tried first,
|
|
194
|
+
* then the canonical ones.
|
|
195
|
+
*/
|
|
196
|
+
async function findStagedBinary(stageDir, liveName) {
|
|
197
|
+
const candidates = [liveName, "spectral", "spectral.exe"];
|
|
198
|
+
for (const name of candidates) {
|
|
199
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const candidate = join(stageDir, name);
|
|
203
|
+
try {
|
|
204
|
+
const st = await stat(candidate);
|
|
205
|
+
if (st.isFile())
|
|
206
|
+
return candidate;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// not there — try the next candidate name
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Replace the companion assets next to the binary with the ones from the zip.
|
|
216
|
+
*
|
|
217
|
+
* Semantics are those of scripts/install.sh: the `<platform>-data/` directory is
|
|
218
|
+
* placed next to the binary and *then* its contents are copied alongside it,
|
|
219
|
+
* with the previous `data/`, `mcp/` and `app-bridge.bundle.js` removed first —
|
|
220
|
+
* replace, not merge, so files deleted between releases do not linger.
|
|
221
|
+
*
|
|
222
|
+
* SAFETY: `installDir` is `dirname(process.execPath)` — the directory that
|
|
223
|
+
* *holds* the running binary. Only the explicitly named companion paths inside
|
|
224
|
+
* it are ever removed, and only for a real absolute path, so a bad `execPath`
|
|
225
|
+
* can never wipe a directory that contains the binary.
|
|
226
|
+
*/
|
|
227
|
+
export async function swapCompanionAssets(params) {
|
|
228
|
+
const { stageDir, installDir } = params;
|
|
229
|
+
if (!installDir || !installDir.startsWith("/") || installDir === "/") {
|
|
230
|
+
return { dataDirName: null, replaced: [] };
|
|
231
|
+
}
|
|
232
|
+
const srcDataDir = await findDataDir(stageDir);
|
|
233
|
+
if (!srcDataDir)
|
|
234
|
+
return { dataDirName: null, replaced: [] };
|
|
235
|
+
const dataDirName = basename(srcDataDir);
|
|
236
|
+
if (!dataDirName || dataDirName === "." || dataDirName === "..") {
|
|
237
|
+
return { dataDirName: null, replaced: [] };
|
|
238
|
+
}
|
|
239
|
+
// Drop the previous version's data directory, then stage the new one next to
|
|
240
|
+
// the binary. `cp` rather than `rename`: the temp dir may live on a different
|
|
241
|
+
// filesystem than ~/.spectral/bin, which would make a rename fail with EXDEV.
|
|
242
|
+
const destDataDir = join(installDir, dataDirName);
|
|
243
|
+
await rm(destDataDir, { recursive: true, force: true });
|
|
244
|
+
await cp(srcDataDir, destDataDir, { recursive: true, force: true });
|
|
245
|
+
// Remove the stale companion assets *before* copying the new ones.
|
|
246
|
+
for (const name of COMPANION_DIRS) {
|
|
247
|
+
await rm(join(installDir, name), { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
for (const name of COMPANION_FILES) {
|
|
250
|
+
await rm(join(installDir, name), { force: true });
|
|
251
|
+
}
|
|
252
|
+
// Copy the new companion contents next to the binary: the compiled binary
|
|
253
|
+
// resolves data/systems and app-bridge.bundle.js relative to execPath.
|
|
254
|
+
const replaced = [];
|
|
255
|
+
for (const entry of await readdir(destDataDir, { withFileTypes: true })) {
|
|
256
|
+
await cp(join(destDataDir, entry.name), join(installDir, entry.name), {
|
|
257
|
+
recursive: true,
|
|
258
|
+
force: true,
|
|
259
|
+
});
|
|
260
|
+
replaced.push(entry.name);
|
|
261
|
+
}
|
|
262
|
+
return { dataDirName, replaced };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Record the installed version so install.sh can skip a redundant reinstall.
|
|
266
|
+
* Only written when the companion assets moved together with the binary.
|
|
267
|
+
*/
|
|
268
|
+
async function writeVersionStamp(installDir, version) {
|
|
269
|
+
await writeFile(join(installDir, VERSION_STAMP), `${version}\n`);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Drop the version stamp: only the binary was updated, so the installation as a
|
|
273
|
+
* whole no longer matches the stamped version — re-running `curl | sh` must
|
|
274
|
+
* then refresh the companion assets instead of reporting "already installed".
|
|
275
|
+
*/
|
|
276
|
+
async function clearVersionStamp(installDir) {
|
|
277
|
+
if (!installDir || installDir === "/")
|
|
278
|
+
return;
|
|
279
|
+
await rm(join(installDir, VERSION_STAMP), { force: true });
|
|
280
|
+
}
|
|
136
281
|
/**
|
|
137
282
|
* Atomically replace the running binary at `currentPath` with `newBinaryPath`.
|
|
138
283
|
*
|
|
@@ -140,7 +285,7 @@ async function fetchManifest(platform) {
|
|
|
140
285
|
* place, and makes it executable. On Windows the running executable cannot be
|
|
141
286
|
* renamed while live, so we print an actionable message instead.
|
|
142
287
|
*/
|
|
143
|
-
async function atomicReplace(currentPath, newBinaryPath) {
|
|
288
|
+
async function atomicReplace(currentPath, newBinaryPath, out = (t) => process.stdout.write(t)) {
|
|
144
289
|
const isWindows = process.platform === "win32";
|
|
145
290
|
const dir = dirname(currentPath);
|
|
146
291
|
if (isWindows) {
|
|
@@ -149,7 +294,7 @@ async function atomicReplace(currentPath, newBinaryPath) {
|
|
|
149
294
|
// user to restart. A launcher / wrapper script would handle the swap.
|
|
150
295
|
const staged = join(dir, basename(currentPath) + ".new");
|
|
151
296
|
await rename(newBinaryPath, staged);
|
|
152
|
-
|
|
297
|
+
out(pc.yellow(`New binary staged at ${staged}\n` +
|
|
153
298
|
`Close all spectral processes, then replace the .exe manually,\n` +
|
|
154
299
|
`or run the provided update helper.\n`));
|
|
155
300
|
return;
|
|
@@ -199,76 +344,161 @@ async function atomicReplace(currentPath, newBinaryPath) {
|
|
|
199
344
|
* Run the `spectral update` subcommand.
|
|
200
345
|
*
|
|
201
346
|
* @param args remaining CLI args after the `update` subcommand
|
|
347
|
+
* @param deps injectable I/O for tests (network, spawns, exec path, version)
|
|
202
348
|
*/
|
|
203
|
-
export async function runUpdate(args) {
|
|
349
|
+
export async function runUpdate(args, deps = {}) {
|
|
204
350
|
const checkOnly = args.includes("--check") || args.includes("-c");
|
|
351
|
+
const out = deps.out ?? ((t) => process.stdout.write(t));
|
|
352
|
+
const err = deps.err ?? ((t) => process.stderr.write(t));
|
|
353
|
+
const getManifest = deps.fetchManifest ?? fetchManifest;
|
|
354
|
+
const download = deps.download ?? downloadToFile;
|
|
355
|
+
const run = deps.run ?? defaultRun;
|
|
356
|
+
const standalone = deps.standalone ?? isStandaloneBinary();
|
|
357
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
205
358
|
// --- Installation mode detection --------------------------------------
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
"Check for Updates to update.\n"));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!isStandaloneBinary()) {
|
|
212
|
-
process.stdout.write(pc.cyan("Run `npm i -g @aexol/spectral@latest` to update.\n"));
|
|
359
|
+
if (!standalone) {
|
|
360
|
+
out(pc.cyan("Run `npm i -g @aexol/spectral@latest` to update.\n"));
|
|
213
361
|
return;
|
|
214
362
|
}
|
|
215
363
|
// --- Standalone self-update flow --------------------------------------
|
|
216
|
-
const platform = detectPlatform();
|
|
364
|
+
const platform = deps.platform !== undefined ? deps.platform : detectPlatform();
|
|
217
365
|
if (!platform) {
|
|
218
|
-
|
|
366
|
+
err(pc.red(`Update is not supported on this platform (${process.platform}/${process.arch}).\n`));
|
|
219
367
|
return;
|
|
220
368
|
}
|
|
221
|
-
const cur = currentVersion();
|
|
369
|
+
const cur = deps.version ?? currentVersion();
|
|
222
370
|
let manifest;
|
|
223
371
|
try {
|
|
224
|
-
manifest = await
|
|
372
|
+
manifest = await getManifest(platform);
|
|
225
373
|
}
|
|
226
374
|
catch {
|
|
227
|
-
|
|
375
|
+
out(pc.yellow("Could not check for updates. Make sure you're connected to the internet.\n"));
|
|
228
376
|
return;
|
|
229
377
|
}
|
|
230
378
|
const latest = manifest.version.replace(/^v/i, "");
|
|
231
379
|
const comparison = compareSemver(cur, latest);
|
|
232
380
|
if (comparison >= 0) {
|
|
233
|
-
|
|
381
|
+
out(pc.green(`Spectral is up to date (v${cur}).\n`));
|
|
234
382
|
return;
|
|
235
383
|
}
|
|
236
384
|
if (checkOnly) {
|
|
237
|
-
|
|
385
|
+
out(pc.cyan(`Update available: v${cur} → v${latest}. Run \`spectral update\` to install.\n`));
|
|
238
386
|
return;
|
|
239
387
|
}
|
|
240
388
|
// --- Download + verify + replace -------------------------------------
|
|
241
389
|
if (!manifest.url) {
|
|
242
|
-
|
|
390
|
+
err(pc.red("Update manifest is missing a download URL. Aborting.\n"));
|
|
243
391
|
return;
|
|
244
392
|
}
|
|
245
393
|
let tempDir = null;
|
|
246
394
|
try {
|
|
247
395
|
tempDir = await mkdtemp(join(tmpdir(), "spectral-update-"));
|
|
396
|
+
const currentPath = execPath;
|
|
397
|
+
const installDir = dirname(currentPath);
|
|
398
|
+
// The manifest only pins a zip when the archive was actually published.
|
|
399
|
+
const zipUrl = manifest.zipUrl?.trim() ?? "";
|
|
400
|
+
const zipSha = manifest.zipSha256?.trim() ?? "";
|
|
401
|
+
if (!!zipUrl !== !!zipSha) {
|
|
402
|
+
out(pc.yellow("Release manifest pins only part of the archive (zipUrl/zipSha256) —\n" +
|
|
403
|
+
"installing the verified raw binary instead.\n"));
|
|
404
|
+
}
|
|
405
|
+
// ----- preferred path: release zip (binary + companion assets) -------
|
|
406
|
+
if (zipUrl && zipSha) {
|
|
407
|
+
const zipPath = join(tempDir, "release.zip");
|
|
408
|
+
out(pc.dim(`Downloading v${latest} (release zip)…\n`));
|
|
409
|
+
await download(zipUrl, zipPath);
|
|
410
|
+
await stat(zipPath);
|
|
411
|
+
out(pc.dim("Verifying archive checksum…\n"));
|
|
412
|
+
const actualZip = await sha256OfFile(zipPath);
|
|
413
|
+
if (actualZip.toLowerCase() !== zipSha.toLowerCase()) {
|
|
414
|
+
err(pc.red(`Archive checksum mismatch. Expected ${zipSha.toLowerCase()}, got ${actualZip}.\n` +
|
|
415
|
+
"The downloaded zip may be corrupted or tampered with. Nothing was installed.\n"));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const unpacked = await unpackReleaseZip({ zipPath, tempDir, run });
|
|
419
|
+
if (unpacked.ok && unpacked.destDir) {
|
|
420
|
+
const stageDir = unpacked.destDir;
|
|
421
|
+
const stagedSource = await findStagedBinary(stageDir, basename(currentPath));
|
|
422
|
+
if (!stagedSource) {
|
|
423
|
+
err(pc.red(`Release zip does not contain the ${basename(currentPath)} binary at its root. ` +
|
|
424
|
+
"Nothing was installed.\n"));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const newBinary = join(tempDir, basename(currentPath) || "spectral-new");
|
|
428
|
+
await cp(stagedSource, newBinary, { force: true });
|
|
429
|
+
// The zip digest already pins the archive; re-checking the binary inside
|
|
430
|
+
// also guards against a mismatched archive (same as install.sh).
|
|
431
|
+
if (manifest.sha256) {
|
|
432
|
+
out(pc.dim("Verifying binary checksum…\n"));
|
|
433
|
+
const actualBin = await sha256OfFile(newBinary);
|
|
434
|
+
if (actualBin.toLowerCase() !== manifest.sha256.toLowerCase()) {
|
|
435
|
+
err(pc.red(`Binary checksum mismatch. Expected ${manifest.sha256.toLowerCase()}, got ${actualBin}.\n` +
|
|
436
|
+
"Nothing was installed.\n"));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
out(pc.dim("Installing…\n"));
|
|
441
|
+
if (process.platform === "win32") {
|
|
442
|
+
// A running .exe cannot be swapped from within itself, so the companion
|
|
443
|
+
// assets are left untouched too — refreshing them while the old binary
|
|
444
|
+
// is still in place would split the pair.
|
|
445
|
+
await atomicReplace(currentPath, newBinary, out);
|
|
446
|
+
out(pc.yellow("Companion assets were NOT updated. Re-run the installer to refresh them:\n" +
|
|
447
|
+
` curl -fsSL ${INSTALLER_URL} | sh\n`));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// Binary first: `atomicReplace` is the step most likely to fail (it needs
|
|
451
|
+
// write access to the install directory), and if it does nothing else has
|
|
452
|
+
// been touched. The companion assets only move once the new binary is in
|
|
453
|
+
// place, so a failed update never leaves new assets next to an old binary.
|
|
454
|
+
await atomicReplace(currentPath, newBinary, out);
|
|
455
|
+
const { dataDirName, replaced } = await swapCompanionAssets({ stageDir, installDir });
|
|
456
|
+
await writeVersionStamp(installDir, latest);
|
|
457
|
+
out(pc.green(`Spectral updated to v${latest}. Restart spectral to use the new version.\n`));
|
|
458
|
+
if (replaced.length) {
|
|
459
|
+
out(pc.dim(`Companion assets refreshed: ${[dataDirName, ...replaced].join(", ")}.\n`));
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
out(pc.yellow("WARNING: the release zip contained no *-data directory —\n" +
|
|
463
|
+
"companion assets were left as they were.\n"));
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// No usable archive tool on this machine: fall through to the raw binary.
|
|
468
|
+
out(pc.yellow(`Could not unpack the release zip (${unpacked.notes.join("; ")}) —\n` +
|
|
469
|
+
"falling back to replacing the binary only.\n"));
|
|
470
|
+
}
|
|
471
|
+
// ----- fallback path: raw binary only --------------------------------
|
|
248
472
|
const downloadedPath = join(tempDir, "spectral-new");
|
|
249
|
-
|
|
250
|
-
await
|
|
473
|
+
out(pc.dim(`Downloading v${latest}…\n`));
|
|
474
|
+
await download(manifest.url, downloadedPath);
|
|
251
475
|
// Verify the download is complete and readable.
|
|
252
476
|
await stat(downloadedPath);
|
|
253
477
|
// SHA256 verification.
|
|
254
478
|
if (manifest.sha256) {
|
|
255
|
-
|
|
479
|
+
out(pc.dim("Verifying checksum…\n"));
|
|
256
480
|
const actual = await sha256OfFile(downloadedPath);
|
|
257
481
|
const expected = manifest.sha256.toLowerCase();
|
|
258
482
|
if (actual.toLowerCase() !== expected) {
|
|
259
|
-
|
|
483
|
+
err(pc.red(`Checksum mismatch. Expected ${expected}, got ${actual}.\n` +
|
|
260
484
|
"The downloaded file may be corrupted or tampered with. Aborting.\n"));
|
|
261
485
|
return;
|
|
262
486
|
}
|
|
263
487
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
488
|
+
out(pc.dim("Installing…\n"));
|
|
489
|
+
await atomicReplace(currentPath, downloadedPath, out);
|
|
490
|
+
// The stamp means "this version is installed *with* its companion assets".
|
|
491
|
+
// Only the binary moved, so drop it — a stale stamp would make install.sh
|
|
492
|
+
// report "already installed" and never refresh data/ + mcp/.
|
|
493
|
+
await clearVersionStamp(installDir);
|
|
494
|
+
out(pc.green(`Spectral updated to v${latest}. Restart spectral to use the new version.\n`));
|
|
495
|
+
out(pc.yellow("WARNING: only the binary was replaced — companion assets (design systems,\n" +
|
|
496
|
+
"MCP app bridge) may be out of date. Refresh them by re-running:\n" +
|
|
497
|
+
` curl -fsSL ${INSTALLER_URL} | sh\n`));
|
|
268
498
|
}
|
|
269
|
-
catch (
|
|
270
|
-
const msg =
|
|
271
|
-
|
|
499
|
+
catch (failure) {
|
|
500
|
+
const msg = failure instanceof Error ? failure.message : String(failure);
|
|
501
|
+
err(pc.red(`Update failed: ${msg}\n`));
|
|
272
502
|
}
|
|
273
503
|
finally {
|
|
274
504
|
if (tempDir) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"macos.d.ts","sourceRoot":"","sources":["../../../src/extensions/desktop-control/macos.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAmB,MAAM,YAAY,CAAC;AA+CjF,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,QAAQ,CAAA;CAAE,CAAC,GAAG,MAAM,CA2CtI;AAED,uGAAuG;AACvG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAW/E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAUlE;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAanF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjE;AAUD,8EAA8E;AAC9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9D;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB3E;AAuED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiB5D;AAMD,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"macos.d.ts","sourceRoot":"","sources":["../../../src/extensions/desktop-control/macos.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAmB,MAAM,YAAY,CAAC;AA+CjF,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,QAAQ,CAAA;CAAE,CAAC,GAAG,MAAM,CA2CtI;AAED,uGAAuG;AACvG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAW/E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAUlE;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAanF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjE;AAUD,8EAA8E;AAC9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9D;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB3E;AAuED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiB5D;AAMD,eAAO,MAAM,sBAAsB,QAIuC,CAAC;AAE3E;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,MAAM,CAMvD;AAoBD,eAAO,MAAM,YAAY,EAAE,cA8E1B,CAAC"}
|
|
@@ -268,7 +268,7 @@ export function accessibilityError(rawStderr) {
|
|
|
268
268
|
if (s.includes("assistive access") || s.includes("accessib")) {
|
|
269
269
|
return ("macOS blocked desktop control: Accessibility permission is missing.\n" +
|
|
270
270
|
"Fix: System Settings > Privacy & Security > Accessibility -> add (and enable) the app that runs " +
|
|
271
|
-
"spectral (your terminal, or the
|
|
271
|
+
"spectral (your terminal, or the spectral desktop wrapper). Then retry. " +
|
|
272
272
|
"You can verify from the terminal with: osascript -e 'tell application \"System Events\" to get name of first process whose frontmost is true'");
|
|
273
273
|
}
|
|
274
274
|
if (s.includes("not allowed to send apple events") || s.includes("-1743") || s.includes("automat")) {
|
|
@@ -283,7 +283,7 @@ export function accessibilityError(rawStderr) {
|
|
|
283
283
|
export const SCREEN_RECORDING_ERROR = "macOS blocked the safety screenshots: Screen Recording permission is missing.\n" +
|
|
284
284
|
"Without it the BEFORE/AFTER captures silently show only the desktop wallpaper, so the action was refused before mutating anything.\n" +
|
|
285
285
|
"Fix: System Settings > Privacy & Security > Screen Recording -> add (and enable) the app that runs " +
|
|
286
|
-
"spectral (your terminal, or the
|
|
286
|
+
"spectral (your terminal, or the spectral desktop wrapper). Then retry.";
|
|
287
287
|
/**
|
|
288
288
|
* One-shot JXA probe: CGPreflightScreenCaptureAccess() — true when Screen
|
|
289
289
|
* Recording TCC is granted. The function is missing from CoreGraphics'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/server-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,mCAAmC,EACnC,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB,OAAO,EAA2B,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"server-manager.d.ts","sourceRoot":"","sources":["../../src/mcp/server-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,mCAAmC,EACnC,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB,OAAO,EAA2B,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAI3F,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,YAAY,CAAC;CAC/C;AAED,KAAK,gBAAgB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,mCAAmC,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;AAElH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,eAAe,CAAgD;IACvE,OAAO,CAAC,iBAAiB,CAAuC;IAChE,OAAO,CAAC,cAAc,CAAmC;IAEzD,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI;IAI3D,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAyBtE,gBAAgB;IAmF9B,OAAO,CAAC,YAAY;YAWN,mBAAmB;YAyEnB,aAAa;YAab,iBAAiB;IAkB/B,OAAO,CAAC,iCAAiC;IAQzC,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAI/E,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAI3C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAgBpE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAelC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAIzD,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAIlD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOzB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOrC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;CAMjD"}
|
|
@@ -10,6 +10,7 @@ import { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
|
10
10
|
import { supportsOAuth } from "./mcp-auth-flow.js";
|
|
11
11
|
import { registerSamplingHandler } from "./sampling-handler.js";
|
|
12
12
|
import { interpolateEnvRecord, resolveBearerToken, resolveConfigPath } from "./utils.js";
|
|
13
|
+
import { augmentPath, getPathEnvKey } from "../utils/augment-path.js";
|
|
13
14
|
export class McpServerManager {
|
|
14
15
|
connections = new Map();
|
|
15
16
|
connectPromises = new Map();
|
|
@@ -293,6 +294,11 @@ function resolveEnv(env) {
|
|
|
293
294
|
resolved[key] = value;
|
|
294
295
|
}
|
|
295
296
|
}
|
|
297
|
+
// MCP stdio servers are spawned by name (`npx -y pkg`, `node server.js`), so
|
|
298
|
+
// they live or die by PATH. When spectral itself was launched from a GUI
|
|
299
|
+
// context that PATH has no node/npm in it — augment it for the child only,
|
|
300
|
+
// leaving our own process env untouched.
|
|
301
|
+
resolved[getPathEnvKey(resolved)] = augmentPath(resolved[getPathEnvKey(resolved)]);
|
|
296
302
|
if (!env)
|
|
297
303
|
return resolved;
|
|
298
304
|
const overrides = interpolateEnvRecord(env);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receive-agent-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/receive-agent-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIhF,eAAO,MAAM,oCAAoC,+BAA+B,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,GAAG,YAAY,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,4BAA4B;;;
|
|
1
|
+
{"version":3,"file":"receive-agent-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/receive-agent-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIhF,eAAO,MAAM,oCAAoC,+BAA+B,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,GAAG,YAAY,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,4BAA4B;;;iFAkFvC,CAAC;AAEH,wBAAgB,oCAAoC,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE5E"}
|
|
@@ -8,7 +8,8 @@ export const receiveAgentObservationsTool = defineTool({
|
|
|
8
8
|
label: "Receive observations from other agents",
|
|
9
9
|
description: "Poll for observations shared by other active sessions on this machine working in the " +
|
|
10
10
|
"same directory (same-machine, same-cwd; no cross-machine or team relay). " +
|
|
11
|
-
"Call this before starting work or when you suspect another agent has shared context."
|
|
11
|
+
"Call this before starting work or when you suspect another agent has shared context. " +
|
|
12
|
+
"Delivery is per-recipient: each session polls its own inbox, and you never see your own messages.",
|
|
12
13
|
promptSnippet: "Use receive_agent_observations() to pull shared observations from other active sessions on this machine.",
|
|
13
14
|
promptGuidelines: [
|
|
14
15
|
"Messages are same-machine + same-cwd only; there is no cross-machine or team fan-out.",
|
|
@@ -29,7 +30,13 @@ export const receiveAgentObservationsTool = defineTool({
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
const projectId = store.getProjectByCwd(ctx.cwd);
|
|
32
|
-
|
|
33
|
+
// Subagents receive a DISTINCT derived observation session id on the
|
|
34
|
+
// SubagentToolContext so they poll their own inbox; primary sessions
|
|
35
|
+
// fall back to the session manager's id. Never the bare parent id for
|
|
36
|
+
// subagents — per-recipient delivery would let a subagent consume the
|
|
37
|
+
// parent's broadcast observations.
|
|
38
|
+
const subCtx = ctx;
|
|
39
|
+
const sessionId = subCtx.observationSessionId ?? subCtx.sessionManager?.getSessionId?.();
|
|
33
40
|
if (!projectId || !sessionId) {
|
|
34
41
|
return {
|
|
35
42
|
content: [{ type: "text", text: "No project/session context." }],
|
|
@@ -40,6 +47,9 @@ export const receiveAgentObservationsTool = defineTool({
|
|
|
40
47
|
projectId,
|
|
41
48
|
channel: "memory",
|
|
42
49
|
recipientSessionId: sessionId,
|
|
50
|
+
// Self-filtering: never surface the requesting session's own
|
|
51
|
+
// observations back to it (live subscribe already does this).
|
|
52
|
+
excludeSenderSessionId: sessionId,
|
|
43
53
|
since: params.since,
|
|
44
54
|
limit: params.limit ?? 20,
|
|
45
55
|
markDelivered: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAEV,oBAAoB,EACpB,UAAU,EACX,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,YAAY,EACZ,SAAS,EACT,6BAA6B,EAC9B,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAM3D;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,6BAA6B,EAAE,CAAC;AAMlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,2DAA2D;IAC3D,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;;;;OASG;IACH,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,yEAAyE;IACzE,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAClD;AA0HD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -291,7 +291,11 @@ export async function handleClientMessage(frame, deps) {
|
|
|
291
291
|
// Set autonomous iterative loop state before firing the prompt.
|
|
292
292
|
// loop:true → start/renew loop with the current content as original prompt;
|
|
293
293
|
// loop:false → stop any active loop.
|
|
294
|
-
|
|
294
|
+
// Arm loop state BEFORE any await: for non-reference prompts this is
|
|
295
|
+
// synchronous, preserving the client_message/subscribe interleave below.
|
|
296
|
+
// For "@preset:<name>" prompts this returns a promise that we await just
|
|
297
|
+
// before firing the turn, so the loop replays the EXPANDED prompt.
|
|
298
|
+
const armed = manager.setLoopActive(sessionId, isLoop, content, loopMaxIterations, loopGoal);
|
|
295
299
|
// 2. Attach (idempotent). On first attach we capture the replay payload
|
|
296
300
|
// and synthesize a `session_ready` ws_event so the browser sees the
|
|
297
301
|
// same first frame it would have on a direct WS connection.
|
|
@@ -350,6 +354,30 @@ export async function handleClientMessage(frame, deps) {
|
|
|
350
354
|
});
|
|
351
355
|
}
|
|
352
356
|
}
|
|
357
|
+
// "@preset:<name>" loop prompts: resolve the preset before the turn fires
|
|
358
|
+
// (undefined = already armed synchronously — no yield, no reordering).
|
|
359
|
+
// The arm path resolves to the EXPANDED prompt; `null` means it failed or
|
|
360
|
+
// was aborted (loop state changed mid-resolution) — fall back to the raw
|
|
361
|
+
// content in those cases. The promise is caught, not just awaited: serve
|
|
362
|
+
// calls this handler fire-and-forget, so a rejection (e.g. a sync throw
|
|
363
|
+
// from teamIdForSession inside the manager) would otherwise surface as an
|
|
364
|
+
// unhandled rejection instead of a visible error event.
|
|
365
|
+
let expandedLoopPrompt = null;
|
|
366
|
+
if (armed instanceof Promise) {
|
|
367
|
+
try {
|
|
368
|
+
expandedLoopPrompt = await armed;
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
372
|
+
logger.error?.(`[dispatcher] loop arming failed for ${sessionId}:`, err);
|
|
373
|
+
relay.send({
|
|
374
|
+
kind: "ws_event",
|
|
375
|
+
sessionId,
|
|
376
|
+
event: { type: "error", message: humanizeProviderError(msg) },
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const promptContent = expandedLoopPrompt ?? content;
|
|
353
381
|
// 3. Fire the prompt. `prompt()` resolves on `agent_end`; errors are
|
|
354
382
|
// converted to `error` events by the manager itself, so we just log.
|
|
355
383
|
// `modelId` (Phase 3 — Available Models whitelist) is forwarded as
|
|
@@ -359,7 +387,7 @@ export async function handleClientMessage(frame, deps) {
|
|
|
359
387
|
//
|
|
360
388
|
// When `loop: true`, loop state is set before prompting; the normal
|
|
361
389
|
// model is used — loop replay is handled by session-stream on agent_end.
|
|
362
|
-
manager.prompt(sessionId,
|
|
390
|
+
manager.prompt(sessionId, promptContent, modelId, validImages, reasoningEffort).catch((err) => {
|
|
363
391
|
logger.error?.(`[dispatcher] manager.prompt failed for ${sessionId}:`, err);
|
|
364
392
|
});
|
|
365
393
|
// Arm the interval scheduler after the first (immediate) launch.
|
|
@@ -108,6 +108,14 @@ export interface SubagentToolContext {
|
|
|
108
108
|
cwd: string;
|
|
109
109
|
getSystemPrompt(): string;
|
|
110
110
|
sessionManager?: Pick<ReadonlySessionManager, "getBranch" | "getSessionId">;
|
|
111
|
+
/**
|
|
112
|
+
* Session identity for inter-agent observation tools (polling the shared
|
|
113
|
+
* inter-agent inbox). Subagents receive a DISTINCT derived id
|
|
114
|
+
* (`${parentSessionId}#sub:${agentName}:${suffix}`) so they never consume
|
|
115
|
+
* or misattribute the parent session's inbox. Absent in primary sessions —
|
|
116
|
+
* tools fall back to `sessionManager.getSessionId()`.
|
|
117
|
+
*/
|
|
118
|
+
observationSessionId?: string;
|
|
111
119
|
}
|
|
112
120
|
/**
|
|
113
121
|
* Extended context for command handlers.
|