@aexol/spectral 0.9.234 → 0.9.240
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/serve.js
CHANGED
|
@@ -49,6 +49,8 @@ import { getLiveMcpState } from "../mcp/state-getter.js";
|
|
|
49
49
|
import { preflightSqlite, SessionStore } from "../server/storage.js";
|
|
50
50
|
import { DevProcessRegistry } from "../server/dev-process-registry.js";
|
|
51
51
|
import { sweepOffloads } from "../server/offload-gc.js";
|
|
52
|
+
import { SERVE_READY_MARKER, clearServeState, getLiveServePid, ownsServeState, writeServePid, writeServeReady, } from "../utils/serve-state.js";
|
|
53
|
+
import { applyAugmentedPath } from "../utils/augment-path.js";
|
|
52
54
|
/** Default backend base URL when neither env nor override is set. */
|
|
53
55
|
const DEFAULT_BACKEND_URL = "https://api.aexol.ai";
|
|
54
56
|
import { getVersion } from "../version.js";
|
|
@@ -127,6 +129,7 @@ export async function runServe(opts = {}) {
|
|
|
127
129
|
const installSignalHandlers = opts.installSignalHandlers ?? true;
|
|
128
130
|
const silent = opts.silent ?? false;
|
|
129
131
|
const dbPath = opts.dbPath ?? join(getConfigDir(), "sessions.db");
|
|
132
|
+
const serveState = opts.serveState ?? true;
|
|
130
133
|
// Pre-flight: native sqlite. Fail fast with a friendly hint BEFORE we
|
|
131
134
|
// start opening sockets — keeps the error close to the cause.
|
|
132
135
|
const preflight = preflightSqlite(dbPath);
|
|
@@ -134,6 +137,29 @@ export async function runServe(opts = {}) {
|
|
|
134
137
|
process.stderr.write(`✗ ${preflight.error}\n`);
|
|
135
138
|
process.exit(1);
|
|
136
139
|
}
|
|
140
|
+
// Single-instance bookkeeping. Claim the pid file before any long-running
|
|
141
|
+
// work so `spectral desktop status|stop` and a second `spectral desktop`
|
|
142
|
+
// can find this daemon. Readers prune a stale file with a liveness check,
|
|
143
|
+
// so a SIGKILLed run never wedges the next start.
|
|
144
|
+
if (serveState) {
|
|
145
|
+
// Warn (never refuse) when another live daemon already holds the pid file.
|
|
146
|
+
// Two relays from one machine are legal — the second simply takes over
|
|
147
|
+
// ownership, which would otherwise silently make `desktop stop` target the
|
|
148
|
+
// wrong process.
|
|
149
|
+
//
|
|
150
|
+
// `spectral desktop` claims the pid file with its OWN pid before spawning
|
|
151
|
+
// us (that is what makes the single-instance check atomic), and passes that
|
|
152
|
+
// pid in `SPECTRAL_SERVE_PID_CLAIM`. Seeing it is the expected hand-off,
|
|
153
|
+
// not a competing daemon — ditto our direct parent.
|
|
154
|
+
const previous = getLiveServePid();
|
|
155
|
+
const launchClaim = Number.parseInt(process.env.SPECTRAL_SERVE_PID_CLAIM ?? "", 10);
|
|
156
|
+
const expectedHolder = previous === process.ppid || (Number.isInteger(launchClaim) && previous === launchClaim);
|
|
157
|
+
if (previous !== null && previous !== process.pid && !expectedHolder && !silent) {
|
|
158
|
+
process.stderr.write(`warning: another spectral serve appears to be running (pid ${previous}); ` +
|
|
159
|
+
`this process will take over ~/.spectral/serve.pid\n`);
|
|
160
|
+
}
|
|
161
|
+
writeServePid();
|
|
162
|
+
}
|
|
137
163
|
// Pre-flight: sweep stale large-result offload files (fire-and-forget).
|
|
138
164
|
const store = new SessionStore(dbPath);
|
|
139
165
|
const devProcessRegistry = new DevProcessRegistry(getAgentDir());
|
|
@@ -278,6 +304,30 @@ export async function runServe(opts = {}) {
|
|
|
278
304
|
// resume mid-turn.
|
|
279
305
|
const subscribers = new Map();
|
|
280
306
|
let relay;
|
|
307
|
+
/**
|
|
308
|
+
* Announce readiness to whoever launched us. The Electron app used to guess
|
|
309
|
+
* ("is the child still alive after 3 s?"), which raced the relay handshake:
|
|
310
|
+
* the web app loaded, subscribed to the relay before it existed, and showed
|
|
311
|
+
* `machine_disconnected` on every start. Instead, the FIRST time the socket
|
|
312
|
+
* is genuinely open we (a) drop `serve-ready.json` {pid, ts, version} and
|
|
313
|
+
* (b) print `SPECTRAL_SERVE_READY` on stdout. A launcher that redirected our
|
|
314
|
+
* stdio to a log file still has the file; a launcher watching stdout doesn't
|
|
315
|
+
* need the file. Later reconnects refresh the ready file's timestamp but do
|
|
316
|
+
* not re-print the marker — it is a startup signal, not a liveness ping.
|
|
317
|
+
*/
|
|
318
|
+
let readyAnnounced = false;
|
|
319
|
+
const announceServeReady = () => {
|
|
320
|
+
// Only the pid-file holder may publish readiness. A second daemon that took
|
|
321
|
+
// over, or one that lost the file to a restart, must not overwrite the
|
|
322
|
+
// ready signal of the process that owns it — `desktop`/`status` read that
|
|
323
|
+
// file as the answer to "is MY agent up".
|
|
324
|
+
if (ownsServeState())
|
|
325
|
+
writeServeReady(version);
|
|
326
|
+
if (readyAnnounced)
|
|
327
|
+
return;
|
|
328
|
+
readyAnnounced = true;
|
|
329
|
+
process.stdout.write(`${SERVE_READY_MARKER}\n`);
|
|
330
|
+
};
|
|
281
331
|
/** Wire event handlers on a (possibly new) RelayClient instance. */
|
|
282
332
|
function wireRelay(r) {
|
|
283
333
|
// Wire the meta publisher now that we have both the relay socket and
|
|
@@ -464,6 +514,12 @@ export async function runServe(opts = {}) {
|
|
|
464
514
|
process.stdout.write(`Reconnect attempt ${attempt} in ${delayMs}ms\n`);
|
|
465
515
|
});
|
|
466
516
|
}
|
|
517
|
+
// Readiness: fires on every successful socket open, including after the
|
|
518
|
+
// auth-failed re-registration below swaps in a fresh RelayClient.
|
|
519
|
+
r.on("open", () => {
|
|
520
|
+
if (serveState)
|
|
521
|
+
announceServeReady();
|
|
522
|
+
});
|
|
467
523
|
// The `error` listener must be registered regardless of `silent`:
|
|
468
524
|
// without it, a transport hiccup in silent mode would surface as an
|
|
469
525
|
// unhandled 'error' event and kill the whole serve process. (RelayClient
|
|
@@ -597,10 +653,23 @@ export async function runServe(opts = {}) {
|
|
|
597
653
|
catch {
|
|
598
654
|
// browser extension may not be loaded or already closed
|
|
599
655
|
}
|
|
656
|
+
// Only clear what we still own: a daemon that lost (or never took) the pid
|
|
657
|
+
// file must not delete the state of the daemon that holds it.
|
|
658
|
+
if (serveState && ownsServeState())
|
|
659
|
+
clearServeState();
|
|
600
660
|
if (!silent)
|
|
601
661
|
process.stdout.write("Spectral relay stopped\n");
|
|
602
662
|
};
|
|
603
663
|
if (installSignalHandlers) {
|
|
664
|
+
// Bookkeeping cleanup on EVERY exit route. The graceful-shutdown path calls
|
|
665
|
+
// `process.exit()` itself, so `close()` never runs on SIGTERM/SIGINT — and
|
|
666
|
+
// `spectral desktop stop` must not find a stale pid file after a clean
|
|
667
|
+
// shutdown. SIGKILL / crashes still leave a file behind, which readers prune
|
|
668
|
+
// via the liveness check in `getLiveServePid()`.
|
|
669
|
+
process.on("exit", () => {
|
|
670
|
+
if (serveState && ownsServeState())
|
|
671
|
+
clearServeState();
|
|
672
|
+
});
|
|
604
673
|
// Top-level error nets. A daemon must NOT die from a transient
|
|
605
674
|
// unhandled rejection (e.g. a fetch promise that lost its `.catch`
|
|
606
675
|
// when the relay flapped) or a stray throw from a third-party
|
|
@@ -727,11 +796,19 @@ async function handleConfigPatch(frame, deps) {
|
|
|
727
796
|
}
|
|
728
797
|
/**
|
|
729
798
|
* CLI entry point used by `cli.ts`. Surfaces the deprecation warning for
|
|
730
|
-
* `--port`/`SPECTRAL_PORT` once, then defers
|
|
799
|
+
* `--port`/`SPECTRAL_PORT` once, augments PATH for child processes, then defers
|
|
800
|
+
* to `runServe`.
|
|
731
801
|
*/
|
|
732
802
|
export async function runServeCli(rawArgs) {
|
|
733
803
|
const parsed = parseServeArgs(rawArgs);
|
|
734
804
|
const machineName = parsed.machineName ?? process.env.SPECTRAL_MACHINE_NAME;
|
|
805
|
+
// A GUI-launched daemon (LaunchAgent, `.app` helper, `desktop` wrapper)
|
|
806
|
+
// inherits a minimal PATH, which breaks every `npx`-based MCP server and any
|
|
807
|
+
// shell tool that expects node/git. Augment the daemon's own PATH once here so
|
|
808
|
+
// all descendants — in-process AgentBridge shells, dev-process registry, MCP
|
|
809
|
+
// stdio transports — inherit a usable one. Deliberately in the CLI driver and
|
|
810
|
+
// not `runServe()`, so in-process tests never mutate the runner's env.
|
|
811
|
+
applyAugmentedPath();
|
|
735
812
|
if (parsed.deprecatedPort || process.env.SPECTRAL_PORT) {
|
|
736
813
|
process.stderr.write(`warning: --port / SPECTRAL_PORT are no longer used. ` +
|
|
737
814
|
`spectral serve now connects outbound to the Aexol relay; there is no local port to bind.\n`);
|
|
@@ -2,16 +2,96 @@
|
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
/** Shape of the per-platform latest release manifest. */
|
|
30
|
+
export interface ReleaseManifest {
|
|
31
|
+
version: string;
|
|
32
|
+
platform: string;
|
|
33
|
+
url: string;
|
|
34
|
+
sha256: string;
|
|
35
|
+
/**
|
|
36
|
+
* Additive fields: manifests published before the release-zip scheme do not
|
|
37
|
+
* have them, so every reader must treat them as optional. When both are
|
|
38
|
+
* present, the release zip (binary + `<platform>-data/` companion assets) can
|
|
39
|
+
* be verified and installed instead of the raw binary alone.
|
|
40
|
+
*/
|
|
41
|
+
zipUrl?: string;
|
|
42
|
+
zipSha256?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Spawn a helper process (`unzip`, `tar`) and resolve with its exit code.
|
|
46
|
+
* Rejects when the executable cannot be started at all (ENOENT), which is how
|
|
47
|
+
* callers tell "tool missing" apart from "tool failed".
|
|
48
|
+
*/
|
|
49
|
+
export type RunCommand = (cmd: string, args: string[]) => Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Everything I/O-shaped in the update flow, injectable for tests: the flow
|
|
52
|
+
* (verify → unpack → swap → stamp) is then covered without network access, real
|
|
53
|
+
* archives, or a real standalone binary.
|
|
54
|
+
*/
|
|
55
|
+
export interface UpdateDeps {
|
|
56
|
+
out?: (t: string) => void;
|
|
57
|
+
err?: (t: string) => void;
|
|
58
|
+
fetchManifest?: (platform: string) => Promise<ReleaseManifest>;
|
|
59
|
+
download?: (url: string, destPath: string) => Promise<void>;
|
|
60
|
+
run?: RunCommand;
|
|
61
|
+
/** Executable to update (defaults to `process.execPath`). */
|
|
62
|
+
execPath?: string;
|
|
63
|
+
/** Current version (defaults to `getVersion()`). */
|
|
64
|
+
version?: string;
|
|
65
|
+
/** Standalone detection (defaults to `isStandaloneBinary()`). */
|
|
66
|
+
standalone?: boolean;
|
|
67
|
+
/** Release platform slug (defaults to `detectPlatform()`). */
|
|
68
|
+
platform?: string | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Replace the companion assets next to the binary with the ones from the zip.
|
|
72
|
+
*
|
|
73
|
+
* Semantics are those of scripts/install.sh: the `<platform>-data/` directory is
|
|
74
|
+
* placed next to the binary and *then* its contents are copied alongside it,
|
|
75
|
+
* with the previous `data/`, `mcp/` and `app-bridge.bundle.js` removed first —
|
|
76
|
+
* replace, not merge, so files deleted between releases do not linger.
|
|
77
|
+
*
|
|
78
|
+
* SAFETY: `installDir` is `dirname(process.execPath)` — the directory that
|
|
79
|
+
* *holds* the running binary. Only the explicitly named companion paths inside
|
|
80
|
+
* it are ever removed, and only for a real absolute path, so a bad `execPath`
|
|
81
|
+
* can never wipe a directory that contains the binary.
|
|
10
82
|
*/
|
|
83
|
+
export declare function swapCompanionAssets(params: {
|
|
84
|
+
stageDir: string;
|
|
85
|
+
installDir: string;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
dataDirName: string | null;
|
|
88
|
+
replaced: string[];
|
|
89
|
+
}>;
|
|
11
90
|
/**
|
|
12
91
|
* Run the `spectral update` subcommand.
|
|
13
92
|
*
|
|
14
93
|
* @param args remaining CLI args after the `update` subcommand
|
|
94
|
+
* @param deps injectable I/O for tests (network, spawns, exec path, version)
|
|
15
95
|
*/
|
|
16
|
-
export declare function runUpdate(args: string[]): Promise<void>;
|
|
96
|
+
export declare function runUpdate(args: string[], deps?: UpdateDeps): Promise<void>;
|
|
17
97
|
//# sourceMappingURL=update.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/commands/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA2BH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE1E;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1B,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/D,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAwMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwC9D;AA2FD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA4OpF"}
|