@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.
Files changed (68) hide show
  1. package/README.md +47 -0
  2. package/dist/agent/index.d.ts +32 -5
  3. package/dist/agent/index.d.ts.map +1 -1
  4. package/dist/agent/index.js +120 -22
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +18 -4
  8. package/dist/commands/desktop.d.ts +167 -0
  9. package/dist/commands/desktop.d.ts.map +1 -0
  10. package/dist/commands/desktop.js +540 -0
  11. package/dist/commands/serve.d.ts +9 -1
  12. package/dist/commands/serve.d.ts.map +1 -1
  13. package/dist/commands/serve.js +78 -1
  14. package/dist/commands/update.d.ts +85 -5
  15. package/dist/commands/update.d.ts.map +1 -1
  16. package/dist/commands/update.js +288 -58
  17. package/dist/extensions/desktop-control/macos.d.ts.map +1 -1
  18. package/dist/extensions/desktop-control/macos.js +2 -2
  19. package/dist/mcp/server-manager.d.ts.map +1 -1
  20. package/dist/mcp/server-manager.js +6 -0
  21. package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -1
  22. package/dist/memory/tools/receive-agent-observations.js +12 -2
  23. package/dist/relay/dispatcher.d.ts.map +1 -1
  24. package/dist/relay/dispatcher.js +30 -2
  25. package/dist/sdk/coding-agent/core/extensions/types.d.ts +8 -0
  26. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  27. package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
  28. package/dist/sdk/coding-agent/core/system-prompt.js +5 -0
  29. package/dist/server/agent-bridge/tps-metrics.d.ts +34 -0
  30. package/dist/server/agent-bridge/tps-metrics.d.ts.map +1 -1
  31. package/dist/server/agent-bridge/tps-metrics.js +22 -0
  32. package/dist/server/agent-bridge.d.ts +9 -0
  33. package/dist/server/agent-bridge.d.ts.map +1 -1
  34. package/dist/server/agent-bridge.js +83 -3
  35. package/dist/server/handlers/paths-list.d.ts +1 -1
  36. package/dist/server/handlers/paths-list.js +1 -1
  37. package/dist/server/inter-agent-broker.d.ts +12 -0
  38. package/dist/server/inter-agent-broker.d.ts.map +1 -1
  39. package/dist/server/inter-agent-broker.js +1 -0
  40. package/dist/server/preset-chain.d.ts +151 -0
  41. package/dist/server/preset-chain.d.ts.map +1 -0
  42. package/dist/server/preset-chain.js +250 -0
  43. package/dist/server/session-stream.d.ts +55 -1
  44. package/dist/server/session-stream.d.ts.map +1 -1
  45. package/dist/server/session-stream.js +203 -24
  46. package/dist/server/storage/rows.d.ts +2 -0
  47. package/dist/server/storage/rows.d.ts.map +1 -1
  48. package/dist/server/storage/schema.d.ts +2 -2
  49. package/dist/server/storage/schema.d.ts.map +1 -1
  50. package/dist/server/storage/schema.js +2 -1
  51. package/dist/server/storage.d.ts +7 -0
  52. package/dist/server/storage.d.ts.map +1 -1
  53. package/dist/server/storage.js +142 -9
  54. package/dist/server/wire.d.ts +71 -0
  55. package/dist/server/wire.d.ts.map +1 -1
  56. package/dist/utils/augment-path.d.ts +51 -0
  57. package/dist/utils/augment-path.d.ts.map +1 -0
  58. package/dist/utils/augment-path.js +234 -0
  59. package/dist/utils/open-browser.d.ts +55 -0
  60. package/dist/utils/open-browser.d.ts.map +1 -0
  61. package/dist/utils/open-browser.js +163 -0
  62. package/dist/utils/serve-state.d.ts +264 -0
  63. package/dist/utils/serve-state.d.ts.map +1 -0
  64. package/dist/utils/serve-state.js +563 -0
  65. package/dist/utils/standalone.d.ts +14 -0
  66. package/dist/utils/standalone.d.ts.map +1 -0
  67. package/dist/utils/standalone.js +23 -0
  68. package/package.json +1 -3
@@ -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 to `runServe`.
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 binary, verifies its SHA256 checksum, and atomically
6
- * replaces the running binary on disk.
5
+ * downloads the new artifacts, verifies their SHA256 digests, and installs them
6
+ * over the current installation.
7
7
  *
8
- * Non-standalone install modes (Electron desktop app, npm global) are detected
9
- * and the user is pointed at the correct update path instead.
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;;;;;;;;;GASG;AA2NH;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8H7D"}
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"}