@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/README.md
CHANGED
|
@@ -53,6 +53,11 @@ Requires Node.js **20 or newer**.
|
|
|
53
53
|
can create projects, open sessions, and chat with the agent on that machine
|
|
54
54
|
from any tab.
|
|
55
55
|
|
|
56
|
+
Steps 2 and 3 are one command: `spectral desktop`. It starts `serve` as a
|
|
57
|
+
background daemon (survives closing the terminal), waits until the relay is
|
|
58
|
+
genuinely connected, then opens the UI in your default browser — see
|
|
59
|
+
[`spectral desktop`](#spectral-desktop).
|
|
60
|
+
|
|
56
61
|
You can also run Spectral as a plain local TUI without the relay — just invoke
|
|
57
62
|
`spectral` (no subcommand) and it acts as a normal pi terminal session with the
|
|
58
63
|
Aexol MCP extension auto-loaded.
|
|
@@ -65,6 +70,7 @@ Aexol MCP extension auto-loaded.
|
|
|
65
70
|
| `spectral login` | Interactive auth. Verifies the key against the MCP backend and stores it. |
|
|
66
71
|
| `spectral logout` | Removes `~/.spectral/config.json`. Idempotent. |
|
|
67
72
|
| `spectral serve` | Connect this machine to the Aexol relay. Stays up; survives reconnects. |
|
|
73
|
+
| `spectral desktop` | Start `serve` as a daemon, wait for it, open the UI. `status` / `stop` too. |
|
|
68
74
|
| `spectral --version` | Print version. |
|
|
69
75
|
| `spectral --help` | Print Spectral header, then pi's full help. |
|
|
70
76
|
|
|
@@ -74,6 +80,39 @@ Aexol MCP extension auto-loaded.
|
|
|
74
80
|
| ----------------------- | ----------------------------------------------------- |
|
|
75
81
|
| `--machine-name <name>` | Override the display name (default: `os.hostname()`). |
|
|
76
82
|
|
|
83
|
+
### `spectral desktop`
|
|
84
|
+
|
|
85
|
+
The headless replacement for the removed Electron app: same lifecycle, no
|
|
86
|
+
bundled browser, and the UI is your own browser at
|
|
87
|
+
`https://aexol.ai/agent` (override with `SPECTRAL_LANDING_URL`).
|
|
88
|
+
|
|
89
|
+
| Command | Description |
|
|
90
|
+
| ----------------------- | ---------------------------------------------------------------------- |
|
|
91
|
+
| `spectral desktop` | Start the agent, or reuse the one already running, then open the UI. |
|
|
92
|
+
| `spectral desktop status` | Report pid / version / readiness signal. Exit `3` when not running. |
|
|
93
|
+
| `spectral desktop stop` | SIGTERM, then SIGKILL after 10s. Removes the pid / ready files. |
|
|
94
|
+
|
|
95
|
+
Mechanics worth knowing:
|
|
96
|
+
|
|
97
|
+
- Single instance via `~/.spectral/serve.pid`, claimed atomically (`O_EXCL`)
|
|
98
|
+
*before* the daemon is spawned — two simultaneous `spectral desktop` runs
|
|
99
|
+
start exactly one daemon. A dead pid is detected and pruned, so a crashed
|
|
100
|
+
daemon never blocks the next start, and a pid file held by a process this
|
|
101
|
+
command did not launch is never overwritten or deleted.
|
|
102
|
+
- If `~/.spectral/config.json` has no Studio token, `desktop` runs the
|
|
103
|
+
interactive `spectral login` flow first (and refuses to prompt when there is
|
|
104
|
+
no terminal, telling you to run `spectral login` yourself).
|
|
105
|
+
- Readiness is a real signal, not a timeout: the daemon prints
|
|
106
|
+
`SPECTRAL_SERVE_READY` once the relay WebSocket opens and writes
|
|
107
|
+
`serve-ready.json`. Opening the UI only after that avoids the
|
|
108
|
+
`machine_disconnected` flash you get if the page subscribes before the socket
|
|
109
|
+
exists.
|
|
110
|
+
- `~/.spectral/serve.log` is created `0600` and rotated to `serve.log.old` at
|
|
111
|
+
start once it passes 10 MB (`SPECTRAL_DESKTOP_LOG_MAX_BYTES`).
|
|
112
|
+
- The daemon is spawned detached with an augmented `PATH` (`/etc/paths`,
|
|
113
|
+
`getconf PATH`, Homebrew/nvm/Node dirs), so MCP servers declared as
|
|
114
|
+
`npx -y <pkg>` work even when Spectral was launched from a GUI context.
|
|
115
|
+
|
|
77
116
|
Anything that isn't a Spectral subcommand is forwarded verbatim to pi, so any pi
|
|
78
117
|
flag you know works. Example: `spectral -p "summarize this repo"`.
|
|
79
118
|
|
|
@@ -139,10 +178,18 @@ behaves like a vanilla pi install.
|
|
|
139
178
|
| `~/.spectral/config.json` | Aexol MCP URL + team API key. Created by `spectral login`. Mode `0600`. |
|
|
140
179
|
| `~/.spectral/machine.json` | Machine identity + relay JWT. Created on first `spectral serve`. |
|
|
141
180
|
| `~/.spectral/sessions.db` | Local SQLite for projects, sessions, messages. |
|
|
181
|
+
| `~/.spectral/serve.pid` | Relay daemon pid (single-instance lock). Pruned when stale. |
|
|
182
|
+
| `~/.spectral/serve-ready.json` | `{pid, ts, version}` written when the relay socket opens. |
|
|
183
|
+
| `~/.spectral/serve.log` | Daemon stdout/stderr, written by `spectral desktop`. |
|
|
142
184
|
| `SPECTRAL_CONFIG_DIR` | Override the directory above. |
|
|
143
185
|
| `SPECTRAL_MCP_URL` | Override the MCP URL at login time. |
|
|
144
186
|
| `SPECTRAL_BACKEND_URL` | Override the backend HTTP base for `spectral serve`. |
|
|
145
187
|
| `SPECTRAL_RELAY_URL` | Override the derived relay WebSocket URL. |
|
|
188
|
+
| `SPECTRAL_LANDING_URL` | UI that `spectral desktop` opens (default `https://aexol.ai/agent`). |
|
|
189
|
+
| `SPECTRAL_SERVE_LOG_PATH` | Daemon log path for `spectral desktop`. |
|
|
190
|
+
| `SPECTRAL_DESKTOP_READY_TIMEOUT_MS` | Readiness wait before `desktop` gives up (default 15000). |
|
|
191
|
+
| `SPECTRAL_DESKTOP_STOP_TIMEOUT_MS` | SIGTERM grace before SIGKILL in `desktop stop` (default 10000). |
|
|
192
|
+
| `SPECTRAL_DESKTOP_LOG_MAX_BYTES` | Serve-log size that triggers rotation at start (default 10485760). |
|
|
146
193
|
|
|
147
194
|
Pi's own auth state for the local TUI path (Codex subscription) lives in
|
|
148
195
|
`~/.pi/agent/auth.json` on the same machine. Spectral never reads it and never
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -24,14 +24,16 @@
|
|
|
24
24
|
* - Signal propagation (abort passthrough)
|
|
25
25
|
* - Access to observational memory via the `recall` and
|
|
26
26
|
* `write_project_observation` tools, which run against a parent-session
|
|
27
|
-
* point-in-time snapshot (branch + session id)
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* point-in-time snapshot (branch + session id). `receive_agent_observations`
|
|
28
|
+
* also runs inside subagents but polls with a DISTINCT derived
|
|
29
|
+
* observation session id (never the bare parent id); `share_project_observation`,
|
|
30
|
+
* `compact_context`, and `subagent` remain excluded
|
|
30
31
|
*/
|
|
31
|
-
import type { AgentTool } from "../sdk/agent-core/index.js";
|
|
32
|
+
import type { AgentEvent, AgentTool } from "../sdk/agent-core/index.js";
|
|
32
33
|
import { type ExtensionAPI, type ExtensionContext } from "../sdk/coding-agent/index.js";
|
|
33
34
|
import { type AgentConfig } from "./agents.js";
|
|
34
35
|
import type { ServerEvent } from "../server/wire.ts";
|
|
36
|
+
import { type TpsUsageFields } from "../server/agent-bridge/tps-metrics.ts";
|
|
35
37
|
import type { ReadonlySessionManager } from "../sdk/coding-agent/core/session-manager.js";
|
|
36
38
|
/**
|
|
37
39
|
* Build the system prompt for a subagent session.
|
|
@@ -49,7 +51,32 @@ export declare function buildSubagentSystemPrompt(baseSystemPrompt: string | und
|
|
|
49
51
|
* SessionManager. Each getBranch call returns fresh clones of the snapshot.
|
|
50
52
|
*/
|
|
51
53
|
export declare function createSubagentSessionManagerSnapshot(sessionManager: ExtensionContext["sessionManager"]): Pick<ReadonlySessionManager, "getBranch" | "getSessionId">;
|
|
52
|
-
export declare function resolveSubagentTools(cwd: string, ext: ExtensionAPI, ctx: ExtensionContext): AgentTool[];
|
|
54
|
+
export declare function resolveSubagentTools(cwd: string, ext: ExtensionAPI, ctx: ExtensionContext, agentName?: string): AgentTool[];
|
|
55
|
+
/**
|
|
56
|
+
* Runs a child agent with an independent per-subagent timeout.
|
|
57
|
+
*
|
|
58
|
+
* Combines the parent's abort signal with a fresh timeout AbortController so
|
|
59
|
+
* that no single subagent can exhaust the parent prompt's full timeout budget.
|
|
60
|
+
* The budget covers the WHOLE child run including any resume retries (see
|
|
61
|
+
* `runSingleAgent`), so wall time stays bounded by SUBAGENT_TIMEOUT_MS.
|
|
62
|
+
* On timeout, returns a clear error result instead of throwing so the parent
|
|
63
|
+
* agent can continue (e.g. stop a chain, or surface the error as the tool
|
|
64
|
+
* result for parallel/single modes).
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* LIVE TPS snapshot emitted by the subagent runner after each LLM round that
|
|
68
|
+
* produced tokens (subagent_progress contract). Carried through
|
|
69
|
+
* `onInternalEvent` alongside real `AgentEvent`s; the subagent extension maps
|
|
70
|
+
* it to the `subagent_tps` wire event for the bridge, which folds it into the
|
|
71
|
+
* throttled `subagent_progress` frames keyed by the parent tool call id.
|
|
72
|
+
*/
|
|
73
|
+
export interface SubagentTpsSnapshotEvent {
|
|
74
|
+
type: "subagent_tps_snapshot";
|
|
75
|
+
/** Token-weighted TPS fields so far (omitted fields = no sound data yet). */
|
|
76
|
+
tps: TpsUsageFields;
|
|
77
|
+
}
|
|
78
|
+
/** Internal events the subagent runner forwards through `onInternalEvent`. */
|
|
79
|
+
export type SubagentInternalEvent = AgentEvent | SubagentTpsSnapshotEvent;
|
|
53
80
|
/** Test hook: replace the run-level retry sleep (e.g. with an instant/collecting stub). */
|
|
54
81
|
export declare function setSubagentRetrySleepForTesting(fn?: (ms: number, signal?: AbortSignal) => Promise<void>): void;
|
|
55
82
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAEX,UAAU,EAEV,SAAS,EAET,MAAM,4BAA4B,CAAC;AAqBpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAKrB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACN,KAAK,WAAW,EAKhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,uCAAuC,CAAC;AAS/C,OAAO,KAAK,EAAE,sBAAsB,EAAgB,MAAM,6CAA6C,CAAC;AAyGxG;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAYR;AA2ND;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,cAAc,CAAC,CAe5D;AAgCD,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,EACrB,SAAS,CAAC,EAAE,MAAM,GAChB,SAAS,EAAE,CAkDb;AAyBD;;;;;;;;;;GAUG;AACH;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,6EAA6E;IAC7E,GAAG,EAAE,cAAc,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,wBAAwB,CAAC;AAmF1E,2FAA2F;AAC3F,wBAAgB,+BAA+B,CAC9C,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GACtD,IAAI,CAEN;AAmBD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAC9D,MAAM,CAiBR;AAmmBD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,GACC,IAAI,CA+cN"}
|
package/dist/agent/index.js
CHANGED
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
* - Signal propagation (abort passthrough)
|
|
25
25
|
* - Access to observational memory via the `recall` and
|
|
26
26
|
* `write_project_observation` tools, which run against a parent-session
|
|
27
|
-
* point-in-time snapshot (branch + session id)
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* point-in-time snapshot (branch + session id). `receive_agent_observations`
|
|
28
|
+
* also runs inside subagents but polls with a DISTINCT derived
|
|
29
|
+
* observation session id (never the bare parent id); `share_project_observation`,
|
|
30
|
+
* `compact_context`, and `subagent` remain excluded
|
|
30
31
|
*/
|
|
31
32
|
import { agentLoop, agentLoopContinue, } from "../sdk/agent-core/index.js";
|
|
32
33
|
import { buildIsolatedRequestTimePrunedMessagesResult, COMPACT_CONTEXT_TOOL_NAME, } from "../memory/tools/compact-context.js";
|
|
@@ -37,6 +38,8 @@ import { sleep } from "../sdk/coding-agent/utils/sleep.js";
|
|
|
37
38
|
import { Type } from "typebox";
|
|
38
39
|
import { discoverAgents, agentMetadataBlock, } from "./agents.js";
|
|
39
40
|
import { loadAgentSettings } from "../server/handlers/agent-settings.js";
|
|
41
|
+
import { buildTpsUsageFields, computeTpsMetrics, } from "../server/agent-bridge/tps-metrics.js";
|
|
42
|
+
import { randomUUID } from "node:crypto";
|
|
40
43
|
// ---------------------------------------------------------------------------
|
|
41
44
|
// Built-in tool resolution (no spawn needed — tools are registered in-process)
|
|
42
45
|
// ---------------------------------------------------------------------------
|
|
@@ -294,14 +297,16 @@ export function createSubagentSessionManagerSnapshot(sessionManager) {
|
|
|
294
297
|
* - `compact_context`: depends on parent session/UI state (sessionManager,
|
|
295
298
|
* settingsManager, ui, compaction actions) that is unavailable inside the
|
|
296
299
|
* isolated `{ cwd, getSystemPrompt }` subagent context.
|
|
297
|
-
* - `
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
+
* - `share_project_observation`: requires the parent session identity to
|
|
301
|
+
* attribute shared observations; exposing it to subagents would let them
|
|
302
|
+
* misattribute messages and create share/receive feedback loops.
|
|
303
|
+
* - `receive_agent_observations` is NOT excluded: it runs with a derived
|
|
304
|
+
* `observationSessionId` (see `resolveSubagentTools`) so a subagent polls
|
|
305
|
+
* its own inbox instead of consuming the parent session's.
|
|
300
306
|
*/
|
|
301
307
|
const SUBAGENT_EXCLUDED_TOOL_NAMES = new Set([
|
|
302
308
|
"subagent",
|
|
303
309
|
COMPACT_CONTEXT_TOOL_NAME,
|
|
304
|
-
RECEIVE_AGENT_OBSERVATIONS_TOOL_NAME,
|
|
305
310
|
SHARE_PROJECT_OBSERVATION_TOOL_NAME,
|
|
306
311
|
]);
|
|
307
312
|
/**
|
|
@@ -311,8 +316,9 @@ const SUBAGENT_EXCLUDED_TOOL_NAMES = new Set([
|
|
|
311
316
|
const SUBAGENT_SESSION_SNAPSHOT_TOOL_NAMES = new Set([
|
|
312
317
|
RECALL_OBSERVATION_TOOL_NAME,
|
|
313
318
|
WRITE_PROJECT_OBSERVATION_TOOL_NAME,
|
|
319
|
+
RECEIVE_AGENT_OBSERVATIONS_TOOL_NAME,
|
|
314
320
|
]);
|
|
315
|
-
export function resolveSubagentTools(cwd, ext, ctx) {
|
|
321
|
+
export function resolveSubagentTools(cwd, ext, ctx, agentName) {
|
|
316
322
|
// Subagents receive every tool registered on the primary agent. Built-in
|
|
317
323
|
// tools are created fresh with the subagent's isolated cwd/jobs, and
|
|
318
324
|
// extension-registered tools are wrapped with a `SubagentToolContext`:
|
|
@@ -325,6 +331,16 @@ export function resolveSubagentTools(cwd, ext, ctx) {
|
|
|
325
331
|
// One shared snapshot per invocation so all snapshot-routed tools observe
|
|
326
332
|
// the same parent-session epoch (branch + session id).
|
|
327
333
|
const sharedSnapshot = createSubagentSessionManagerSnapshot(ctx.sessionManager);
|
|
334
|
+
// Distinct observation identity for inter-agent tools. Derived from the
|
|
335
|
+
// parent session id so the inbox stays project/session-scoped, but never
|
|
336
|
+
// equal to it: per-recipient delivery means a subagent polling with the
|
|
337
|
+
// bare parent id would consume (and mark delivered) the parent's
|
|
338
|
+
// broadcast observations. The suffix is per-invocation, so parallel and
|
|
339
|
+
// retried subagent runs each get their own inbox view.
|
|
340
|
+
const parentSessionId = ctx.sessionManager?.getSessionId?.();
|
|
341
|
+
const observationSessionId = parentSessionId
|
|
342
|
+
? `${parentSessionId}#sub:${(agentName ?? "agent").replace(/^@/, "")}:${randomUUID().slice(0, 8)}`
|
|
343
|
+
: undefined;
|
|
328
344
|
for (const toolInfo of ext.getAllTools()) {
|
|
329
345
|
const name = toolInfo.name;
|
|
330
346
|
if (SUBAGENT_EXCLUDED_TOOL_NAMES.has(name))
|
|
@@ -345,8 +361,8 @@ export function resolveSubagentTools(cwd, ext, ctx) {
|
|
|
345
361
|
if (!toolDef)
|
|
346
362
|
continue;
|
|
347
363
|
const isolatedContext = SUBAGENT_SESSION_SNAPSHOT_TOOL_NAMES.has(name)
|
|
348
|
-
? { cwd, sessionManager: sharedSnapshot, getSystemPrompt: ctx.getSystemPrompt }
|
|
349
|
-
: { cwd, getSystemPrompt: ctx.getSystemPrompt };
|
|
364
|
+
? { cwd, sessionManager: sharedSnapshot, getSystemPrompt: ctx.getSystemPrompt, observationSessionId }
|
|
365
|
+
: { cwd, getSystemPrompt: ctx.getSystemPrompt, observationSessionId };
|
|
350
366
|
tools.push(wrapToolDefinition(toolDef, () => isolatedContext));
|
|
351
367
|
}
|
|
352
368
|
return tools;
|
|
@@ -371,17 +387,6 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
|
|
|
371
387
|
await Promise.all(workers);
|
|
372
388
|
return results;
|
|
373
389
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Runs a child agent with an independent per-subagent timeout.
|
|
376
|
-
*
|
|
377
|
-
* Combines the parent's abort signal with a fresh timeout AbortController so
|
|
378
|
-
* that no single subagent can exhaust the parent prompt's full timeout budget.
|
|
379
|
-
* The budget covers the WHOLE child run including any resume retries (see
|
|
380
|
-
* `runSingleAgent`), so wall time stays bounded by SUBAGENT_TIMEOUT_MS.
|
|
381
|
-
* On timeout, returns a clear error result instead of throwing so the parent
|
|
382
|
-
* agent can continue (e.g. stop a chain, or surface the error as the tool
|
|
383
|
-
* result for parallel/single modes).
|
|
384
|
-
*/
|
|
385
390
|
async function runSingleAgentWithTimeout(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx, maxRetries) {
|
|
386
391
|
const timeoutMs = SUBAGENT_TIMEOUT_MS;
|
|
387
392
|
const timeoutSeconds = timeoutMs / 1000;
|
|
@@ -545,7 +550,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
545
550
|
// per-agent tool filtering). Built-ins (read, bash, apply_patch) plus
|
|
546
551
|
// observational-memory tools (recall, read-project-observations) are
|
|
547
552
|
// always available so subagents can actually do work.
|
|
548
|
-
const tools = resolveSubagentTools(effectiveCwd, ext, ctx);
|
|
553
|
+
const tools = resolveSubagentTools(effectiveCwd, ext, ctx, agentName);
|
|
549
554
|
const currentResult = {
|
|
550
555
|
agent: agentName,
|
|
551
556
|
agentSource: agent.source,
|
|
@@ -571,6 +576,18 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
571
576
|
// Responses API reasoning metadata that lets the UI classify accumulated
|
|
572
577
|
// subagent thinking as activity (mirrors the main-session thinking metadata).
|
|
573
578
|
let thinkingMetadata;
|
|
579
|
+
// Per-invocation TPS accumulation (subagent_end contract). Declared before
|
|
580
|
+
// the try block so both the settled and thrown return paths can attach it;
|
|
581
|
+
// survives the run-level resume retry so retried rounds count like their
|
|
582
|
+
// usage does.
|
|
583
|
+
const tpsAccumulator = {
|
|
584
|
+
durationMsSum: 0,
|
|
585
|
+
outputTokens: 0,
|
|
586
|
+
outputDurationMs: 0,
|
|
587
|
+
ttftMsSum: 0,
|
|
588
|
+
ttftRounds: 0,
|
|
589
|
+
lastRoundTps: null,
|
|
590
|
+
};
|
|
574
591
|
const emitUpdate = () => {
|
|
575
592
|
if (onUpdate) {
|
|
576
593
|
const liveResult = {
|
|
@@ -653,6 +670,11 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
653
670
|
const collectedMessages = [];
|
|
654
671
|
let hadError = false;
|
|
655
672
|
let segments = 0;
|
|
673
|
+
// Per-LLM-round timing stamps (TPS contract), mirroring the bridge's
|
|
674
|
+
// main-session `pending` stamps: message_start opens the round, the
|
|
675
|
+
// first text/thinking delta ends the TTFT window, message_end closes it.
|
|
676
|
+
let roundStartAt;
|
|
677
|
+
let roundFirstDeltaAt;
|
|
656
678
|
// One child run may span several segments: the first one starts the
|
|
657
679
|
// loop, any later one resumes it. Tool results already recorded in
|
|
658
680
|
// `collectedMessages` are part of the resumed context, so tools are
|
|
@@ -664,15 +686,25 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
664
686
|
break;
|
|
665
687
|
}
|
|
666
688
|
onInternalEvent?.(event, parentToolCallId);
|
|
689
|
+
if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
690
|
+
// New LLM round begins. Reset the delta stamp so a stale
|
|
691
|
+
// first-delta timestamp from a previous round cannot poison TTFT.
|
|
692
|
+
roundStartAt = Date.now();
|
|
693
|
+
roundFirstDeltaAt = undefined;
|
|
694
|
+
}
|
|
667
695
|
if (event.type === "message_update" && event.message?.role === "assistant") {
|
|
668
696
|
// Forward streaming text/thinking deltas so the UI can show the
|
|
669
697
|
// subagent process as it's generated, not just the final result.
|
|
670
698
|
const inner = event.assistantMessageEvent;
|
|
671
699
|
if (inner?.type === "text_delta" && inner.delta) {
|
|
700
|
+
if (roundFirstDeltaAt === undefined)
|
|
701
|
+
roundFirstDeltaAt = Date.now();
|
|
672
702
|
streamingText += inner.delta;
|
|
673
703
|
emitUpdate();
|
|
674
704
|
}
|
|
675
705
|
else if (inner?.type === "thinking_delta" && inner.delta) {
|
|
706
|
+
if (roundFirstDeltaAt === undefined)
|
|
707
|
+
roundFirstDeltaAt = Date.now();
|
|
676
708
|
thinkingText += inner.delta;
|
|
677
709
|
if (inner.metadata) {
|
|
678
710
|
thinkingMetadata = { ...(thinkingMetadata ?? {}), ...inner.metadata };
|
|
@@ -682,6 +714,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
682
714
|
continue;
|
|
683
715
|
}
|
|
684
716
|
if (event.type === "message_end" && event.message) {
|
|
717
|
+
// Stamp at handler entry so the round metrics below measure the
|
|
718
|
+
// whole stream (same source as the bridge's main-session path).
|
|
719
|
+
const roundEndAt = Date.now();
|
|
685
720
|
const msg = event.message;
|
|
686
721
|
collectedMessages.push(msg);
|
|
687
722
|
if (msg.role === "assistant") {
|
|
@@ -703,6 +738,36 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
703
738
|
currentResult.stopReason = msg.stopReason;
|
|
704
739
|
if (msg.errorMessage)
|
|
705
740
|
currentResult.errorMessage = msg.errorMessage;
|
|
741
|
+
// Per-round TPS metrics (mirrors the main-session token_usage
|
|
742
|
+
// stamping): message_start → first delta → message_end with this
|
|
743
|
+
// round's output tokens. Zero-token rounds contribute timing but
|
|
744
|
+
// are skipped in the TPS numerator/denominator.
|
|
745
|
+
const roundMetrics = computeTpsMetrics(roundStartAt, roundFirstDeltaAt, roundEndAt, usage?.output ?? 0);
|
|
746
|
+
if (roundMetrics.durationMs != null) {
|
|
747
|
+
tpsAccumulator.durationMsSum += roundMetrics.durationMs;
|
|
748
|
+
}
|
|
749
|
+
if (roundMetrics.tokensPerSecond != null &&
|
|
750
|
+
roundMetrics.durationMs != null) {
|
|
751
|
+
tpsAccumulator.outputTokens += usage?.output ?? 0;
|
|
752
|
+
tpsAccumulator.outputDurationMs += roundMetrics.durationMs;
|
|
753
|
+
tpsAccumulator.lastRoundTps = roundMetrics.tokensPerSecond;
|
|
754
|
+
}
|
|
755
|
+
if (roundMetrics.ttftMs != null) {
|
|
756
|
+
tpsAccumulator.ttftMsSum += roundMetrics.ttftMs;
|
|
757
|
+
tpsAccumulator.ttftRounds += 1;
|
|
758
|
+
}
|
|
759
|
+
// LIVE TPS snapshot (subagent_progress contract): after each LLM
|
|
760
|
+
// round that produced tokens, push the token-weighted
|
|
761
|
+
// accumulator snapshot through onInternalEvent so the bridge can
|
|
762
|
+
// fold it into the throttled subagent_progress frames keyed by
|
|
763
|
+
// the parent tool call id. Omitted until the first round with
|
|
764
|
+
// tokens; unsound fields are omitted by buildTpsUsageFields.
|
|
765
|
+
if (roundMetrics.tokensPerSecond != null) {
|
|
766
|
+
onInternalEvent?.({
|
|
767
|
+
type: "subagent_tps_snapshot",
|
|
768
|
+
tps: buildTpsUsageFields(tpsAccumulator),
|
|
769
|
+
}, parentToolCallId);
|
|
770
|
+
}
|
|
706
771
|
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
707
772
|
hadError = true;
|
|
708
773
|
}
|
|
@@ -730,6 +795,10 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
730
795
|
// shows the resumed segment's reasoning, not stale text.
|
|
731
796
|
thinkingText = "";
|
|
732
797
|
thinkingMetadata = undefined;
|
|
798
|
+
// Drop round stamps from the previous segment so a resumed segment's
|
|
799
|
+
// first assistant round measures from its own message_start onward.
|
|
800
|
+
roundStartAt = undefined;
|
|
801
|
+
roundFirstDeltaAt = undefined;
|
|
733
802
|
currentResult.stopReason = undefined;
|
|
734
803
|
currentResult.errorMessage = undefined;
|
|
735
804
|
currentResult.stderr = "";
|
|
@@ -810,6 +879,15 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
810
879
|
currentResult.errorMessage = `${currentResult.errorMessage} (failed after ${segments} attempts)`;
|
|
811
880
|
}
|
|
812
881
|
}
|
|
882
|
+
if (tpsAccumulator.durationMsSum > 0 || tpsAccumulator.ttftRounds > 0) {
|
|
883
|
+
currentResult.tps = {
|
|
884
|
+
...buildTpsUsageFields(tpsAccumulator),
|
|
885
|
+
outputTokens: tpsAccumulator.outputTokens,
|
|
886
|
+
outputDurationMs: tpsAccumulator.outputDurationMs,
|
|
887
|
+
ttftMsSum: tpsAccumulator.ttftMsSum,
|
|
888
|
+
ttftRounds: tpsAccumulator.ttftRounds,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
813
891
|
return currentResult;
|
|
814
892
|
}
|
|
815
893
|
catch (err) {
|
|
@@ -818,6 +896,15 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
818
896
|
currentResult.stopReason = "error";
|
|
819
897
|
currentResult.errorMessage = errorMsg;
|
|
820
898
|
currentResult.stderr = errorMsg;
|
|
899
|
+
if (tpsAccumulator.durationMsSum > 0 || tpsAccumulator.ttftRounds > 0) {
|
|
900
|
+
currentResult.tps = {
|
|
901
|
+
...buildTpsUsageFields(tpsAccumulator),
|
|
902
|
+
outputTokens: tpsAccumulator.outputTokens,
|
|
903
|
+
outputDurationMs: tpsAccumulator.outputDurationMs,
|
|
904
|
+
ttftMsSum: tpsAccumulator.ttftMsSum,
|
|
905
|
+
ttftRounds: tpsAccumulator.ttftRounds,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
821
908
|
return currentResult;
|
|
822
909
|
}
|
|
823
910
|
}
|
|
@@ -1000,6 +1087,17 @@ export default function subagentExtension(ext, opts) {
|
|
|
1000
1087
|
finishedAt,
|
|
1001
1088
|
});
|
|
1002
1089
|
}
|
|
1090
|
+
else if (event.type === "subagent_tps_snapshot") {
|
|
1091
|
+
// LIVE TPS snapshot from the runner (subagent_progress
|
|
1092
|
+
// contract): forward as the internal `subagent_tps` wire
|
|
1093
|
+
// event; the bridge folds it into the throttled
|
|
1094
|
+
// subagent_progress frames for this tool call.
|
|
1095
|
+
emitSubagentEvent({
|
|
1096
|
+
type: "subagent_tps",
|
|
1097
|
+
toolCallId: parentToolCallId,
|
|
1098
|
+
...event.tps,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1003
1101
|
}
|
|
1004
1102
|
: undefined;
|
|
1005
1103
|
if (modeCount !== 1) {
|
package/dist/cli.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* - `spectral login` → interactive auth flow (writes ~/.spectral/config.json)
|
|
9
9
|
* - `spectral logout` → deletes ~/.spectral/config.json
|
|
10
10
|
* - `spectral serve` → start the Aexol relay WebSocket server (headless)
|
|
11
|
+
* - `spectral desktop`→ start `serve` detached + open the Studio UI in the
|
|
12
|
+
* browser (the thin replacement for the old desktop app)
|
|
11
13
|
* - `spectral bind` → link directory to an Aexol Studio project
|
|
12
14
|
* - `spectral unbind` → remove Aexol Studio project binding
|
|
13
15
|
* - `spectral --version` / `--help` → branded short-circuits, no auth needed.
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG"}
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* - `spectral login` → interactive auth flow (writes ~/.spectral/config.json)
|
|
9
9
|
* - `spectral logout` → deletes ~/.spectral/config.json
|
|
10
10
|
* - `spectral serve` → start the Aexol relay WebSocket server (headless)
|
|
11
|
+
* - `spectral desktop`→ start `serve` detached + open the Studio UI in the
|
|
12
|
+
* browser (the thin replacement for the old desktop app)
|
|
11
13
|
* - `spectral bind` → link directory to an Aexol Studio project
|
|
12
14
|
* - `spectral unbind` → remove Aexol Studio project binding
|
|
13
15
|
* - `spectral --version` / `--help` → branded short-circuits, no auth needed.
|
|
@@ -16,8 +18,7 @@
|
|
|
16
18
|
import { getVersion } from "./version.js";
|
|
17
19
|
// ---- Read our own version ----------------------------------------------------
|
|
18
20
|
// Centralized in src/version.ts so the standalone Bun binary, the npm
|
|
19
|
-
// install
|
|
20
|
-
// silently falling back to "0.0.0".
|
|
21
|
+
// install all resolve the same real version instead of falling back to "0.0.0".
|
|
21
22
|
const VERSION = getVersion();
|
|
22
23
|
const TAGLINE = "Coding agent that never sleeps";
|
|
23
24
|
// ---- Branded helpers ---------------------------------------------------------
|
|
@@ -33,6 +34,7 @@ function printHeader() {
|
|
|
33
34
|
" spectral login Interactive authentication (team API key or OAuth)",
|
|
34
35
|
" spectral logout Remove stored Aexol credentials",
|
|
35
36
|
" spectral serve Connect this machine to the Aexol relay backend",
|
|
37
|
+
" spectral desktop Start the agent in the background and open the UI",
|
|
36
38
|
" spectral bind Link this directory to an Aexol Studio project",
|
|
37
39
|
" spectral unbind Remove the Aexol Studio project binding",
|
|
38
40
|
" spectral update Check for and install the latest Spectral version",
|
|
@@ -40,8 +42,9 @@ function printHeader() {
|
|
|
40
42
|
" spectral --version Print version and exit",
|
|
41
43
|
" spectral --help Print this help and exit",
|
|
42
44
|
"",
|
|
43
|
-
"TUI/interactive mode has been removed. Use `spectral
|
|
44
|
-
"
|
|
45
|
+
"TUI/interactive mode has been removed. Use `spectral desktop` to work in",
|
|
46
|
+
"the Aexol Studio web frontend, or `spectral serve` to only connect this",
|
|
47
|
+
"machine to the relay backend.",
|
|
45
48
|
"",
|
|
46
49
|
].join("\n"));
|
|
47
50
|
}
|
|
@@ -99,6 +102,17 @@ async function main() {
|
|
|
99
102
|
await runServeCli(args.slice(1));
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
105
|
+
if (first === "desktop") {
|
|
106
|
+
const { runDesktop } = await import("./commands/desktop.js");
|
|
107
|
+
// Thin wrapper: it spawns a detached `spectral serve`, waits for its
|
|
108
|
+
// readiness marker, opens the browser, then exits. The exit code tells the
|
|
109
|
+
// shell whether the agent came up.
|
|
110
|
+
// `exitCode` (not `process.exit`) so buffered stdout is flushed and any
|
|
111
|
+
// pending cleanup still runs — the wrapper has nothing left open, but a
|
|
112
|
+
// hard exit here would truncate the message on a slow terminal.
|
|
113
|
+
process.exitCode = await runDesktop(args.slice(1));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
102
116
|
if (first === "bind") {
|
|
103
117
|
const { runBind } = await import("./commands/bind.js");
|
|
104
118
|
await runBind(args.slice(1));
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spectral desktop` — the thin, headless replacement for the Electron shell.
|
|
3
|
+
*
|
|
4
|
+
* What the Electron app actually contributed was four things, all reproduced
|
|
5
|
+
* here without a bundled Chromium:
|
|
6
|
+
*
|
|
7
|
+
* 1. single-instance guarantee → `~/.spectral/serve.pid` + liveness check
|
|
8
|
+
* 1. single-instance guarantee → `~/.spectral/serve.pid`, claimed
|
|
9
|
+
* atomically (`O_EXCL`) before the spawn
|
|
10
|
+
* 2. start the local relay daemon → detached `spectral serve`, unref'd,
|
|
11
|
+
* stdout+stderr appended to serve.log
|
|
12
|
+
* 3. "wait until it is really up" → the `SPECTRAL_SERVE_READY` stdout marker
|
|
13
|
+
* and `serve-ready.json` (see
|
|
14
|
+
* src/utils/serve-state.ts), replacing the
|
|
15
|
+
* old "assume ready after 3 s" heuristic
|
|
16
|
+
* 4. show the UI → open the system browser at
|
|
17
|
+
* https://aexol.ai/agent
|
|
18
|
+
* (override: SPECTRAL_LANDING_URL)
|
|
19
|
+
*
|
|
20
|
+
* The PATH augmentation the app performed for its child process now happens in
|
|
21
|
+
* `augmentPath()` (src/utils/augment-path.ts) and is applied to the spawned
|
|
22
|
+
* daemon, so MCP servers launched via `npx` keep working.
|
|
23
|
+
*
|
|
24
|
+
* What is deliberately NOT reproduced (see the removal notes): the splash
|
|
25
|
+
* window, in-app auto-update (use `spectral update`), the native app menu, and
|
|
26
|
+
* the JWT-via-IPC login bridge — authentication goes through the existing
|
|
27
|
+
* `spectral login` browser flow, which already writes `userJwt` to
|
|
28
|
+
* `~/.spectral/config.json`.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 success / already running, 1 failure, 3 stopped-state for
|
|
31
|
+
* `status` when the daemon is not running.
|
|
32
|
+
*/
|
|
33
|
+
import { type SpectralConfig } from "../config.js";
|
|
34
|
+
import { type OpenBrowserResult } from "../utils/open-browser.js";
|
|
35
|
+
import { type ServeReadyResult, type ServeGenealogy } from "../utils/serve-state.js";
|
|
36
|
+
/** Where the Studio agent UI lives. */
|
|
37
|
+
export declare const DEFAULT_LANDING_URL = "https://aexol.ai/agent";
|
|
38
|
+
/** How long to wait for the daemon's readiness signal. */
|
|
39
|
+
export declare const DEFAULT_READY_TIMEOUT_MS = 15000;
|
|
40
|
+
/** Grace period between SIGTERM and SIGKILL in `desktop stop`. */
|
|
41
|
+
export declare const DEFAULT_STOP_TIMEOUT_MS = 10000;
|
|
42
|
+
/**
|
|
43
|
+
* Set in the daemon's env to the wrapper's own pid. The daemon sees the
|
|
44
|
+
* placeholder we left in `serve.pid` and must not read it as "another serve is
|
|
45
|
+
* running" — it is the claim of the process that just launched it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SERVE_PID_CLAIM_ENV = "SPECTRAL_SERVE_PID_CLAIM";
|
|
48
|
+
export declare function getLandingUrl(env?: NodeJS.ProcessEnv): string;
|
|
49
|
+
/** Render a grace window the way it is actually configured (10s, 250ms). */
|
|
50
|
+
export declare function formatGraceMs(ms: number): string;
|
|
51
|
+
export interface ServeSpawnPlan {
|
|
52
|
+
command: string;
|
|
53
|
+
args: string[];
|
|
54
|
+
cwd: string;
|
|
55
|
+
env: NodeJS.ProcessEnv;
|
|
56
|
+
logFile: string;
|
|
57
|
+
}
|
|
58
|
+
interface ResolveCommandInput {
|
|
59
|
+
execPath?: string;
|
|
60
|
+
execArgv?: readonly string[];
|
|
61
|
+
argv1?: string;
|
|
62
|
+
standalone?: boolean;
|
|
63
|
+
/** Entry points to try, in order (already-resolved absolute paths). */
|
|
64
|
+
candidates?: string[];
|
|
65
|
+
exists?: (path: string) => boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decide how to launch a detached `spectral serve`.
|
|
69
|
+
*
|
|
70
|
+
* Three launch shapes must all work:
|
|
71
|
+
* - standalone Bun binary → exec the binary itself with `serve`
|
|
72
|
+
* - npm install (`spectral` bin → dist/cli.js) → `node <cli.js> serve`
|
|
73
|
+
* - `npm run dev` (tsx src/cli.ts) → re-run the very entry the user invoked,
|
|
74
|
+
* with `process.execArgv` so the tsx loader is carried over
|
|
75
|
+
*
|
|
76
|
+
* @returns the spawn plan's command/args portion. Falls back to whatever
|
|
77
|
+
* `spectral` is on PATH when no entry point can be identified.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveServeCommand(input?: ResolveCommandInput): {
|
|
80
|
+
command: string;
|
|
81
|
+
args: string[];
|
|
82
|
+
};
|
|
83
|
+
/** Compose the full plan for spawning the daemon. */
|
|
84
|
+
export declare function planServeSpawn(env?: NodeJS.ProcessEnv, input?: ResolveCommandInput): ServeSpawnPlan;
|
|
85
|
+
/**
|
|
86
|
+
* Will this spawn re-exec into the daemon instead of becoming it directly?
|
|
87
|
+
*
|
|
88
|
+
* Matters for readiness: a standalone binary and `node dist/cli.js serve` are
|
|
89
|
+
* the daemon, so their exit is a hard failure we can report instantly. Under
|
|
90
|
+
* `npm run dev` (`tsx src/cli.ts`) the child is a loader wrapper that execs
|
|
91
|
+
* something else — its death tells us nothing, and only the ready signal or the
|
|
92
|
+
* timeout may decide.
|
|
93
|
+
*/
|
|
94
|
+
export declare function spawnExpectsReExec(plan: ServeSpawnPlan): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Launch the daemon detached from this terminal so it survives the wrapper
|
|
97
|
+
* exiting (that is the whole point: close the shell, the agent stays up).
|
|
98
|
+
* Returns the child pid, or null when the spawn itself failed.
|
|
99
|
+
*/
|
|
100
|
+
export declare function spawnDetachedServe(plan: ServeSpawnPlan): number | null;
|
|
101
|
+
export interface DesktopDeps {
|
|
102
|
+
env?: NodeJS.ProcessEnv;
|
|
103
|
+
spawnServe?: (plan: ServeSpawnPlan) => number | null;
|
|
104
|
+
waitForReady?: (opts: {
|
|
105
|
+
pid: number;
|
|
106
|
+
logFile: string;
|
|
107
|
+
logOffset: number;
|
|
108
|
+
/** Timestamp captured before the spawn, for ready-signal freshness. */
|
|
109
|
+
sinceMs: number;
|
|
110
|
+
/** True when the child re-execs, so its own exit proves nothing. */
|
|
111
|
+
expectReExec: boolean;
|
|
112
|
+
timeoutMs: number;
|
|
113
|
+
}) => Promise<ServeReadyResult>;
|
|
114
|
+
openUrl?: (url: string) => Promise<OpenBrowserResult>;
|
|
115
|
+
/** Interactive `spectral login`. */
|
|
116
|
+
runLogin?: () => Promise<void>;
|
|
117
|
+
readCfg?: () => Promise<SpectralConfig | null>;
|
|
118
|
+
signal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
119
|
+
alive?: (pid: number) => boolean;
|
|
120
|
+
sleep?: (ms: number) => Promise<void>;
|
|
121
|
+
/** Override the SIGTERM→SIGKILL grace window (tests, exotic supervisors). */
|
|
122
|
+
stopGraceMs?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Injectable genealogy verdict for the serve.pid hand-off confirmation. The
|
|
125
|
+
* real probe walks `ps -o ppid=` and can transiently fail under parallel
|
|
126
|
+
* test load, so tests pin the verdict to keep the
|
|
127
|
+
* `daemon`/`foreign`/`unknown` classification deterministic.
|
|
128
|
+
*/
|
|
129
|
+
genealogy?: (pid: number, ancestors: number[]) => ServeGenealogy;
|
|
130
|
+
planInput?: ResolveCommandInput;
|
|
131
|
+
out?: (text: string) => void;
|
|
132
|
+
err?: (text: string) => void;
|
|
133
|
+
isInteractive?: () => boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface TerminateAgentOptions {
|
|
136
|
+
/** Pid to stop — always the one from the pid file, never a stale handle. */
|
|
137
|
+
pid: number;
|
|
138
|
+
/** SIGTERM grace window before SIGKILL. */
|
|
139
|
+
graceMs: number;
|
|
140
|
+
alive: (pid: number) => boolean;
|
|
141
|
+
signal: (pid: number, signal: NodeJS.Signals) => void;
|
|
142
|
+
sleep: (ms: number) => Promise<void>;
|
|
143
|
+
out?: (text: string) => void;
|
|
144
|
+
/** Poll interval while waiting for the daemon to drain. */
|
|
145
|
+
pollMs?: number;
|
|
146
|
+
/** How long to wait after SIGKILL before giving up. */
|
|
147
|
+
killWaitMs?: number;
|
|
148
|
+
}
|
|
149
|
+
export type TerminateAgentOutcome = "stopped" | "killed" | "failed";
|
|
150
|
+
/**
|
|
151
|
+
* SIGTERM → wait out the drain → SIGKILL. Shared by `desktop stop` and by the
|
|
152
|
+
* start path's "it never came up, do not leave it half-started" branch — the
|
|
153
|
+
* old start path fired a bare SIGTERM and walked away, which left a detached
|
|
154
|
+
* daemon that ignored TERM (or won the race) running behind a failed command.
|
|
155
|
+
*
|
|
156
|
+
* The wait is bounded by a poll COUNT rather than a wall-clock deadline, so a
|
|
157
|
+
* caller with a synchronous `sleep` (tests) is both fast and deterministic.
|
|
158
|
+
*/
|
|
159
|
+
export declare function terminateAgentPid(opts: TerminateAgentOptions): Promise<TerminateAgentOutcome>;
|
|
160
|
+
/**
|
|
161
|
+
* `spectral desktop [start|status|stop]`
|
|
162
|
+
*
|
|
163
|
+
* @returns process exit code; `cli.ts` assigns it to `process.exitCode`.
|
|
164
|
+
*/
|
|
165
|
+
export declare function runDesktop(args?: string[], deps?: DesktopDeps): Promise<number>;
|
|
166
|
+
export {};
|
|
167
|
+
//# sourceMappingURL=desktop.d.ts.map
|