@aexol/spectral 0.8.5 → 0.8.6

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 (158) hide show
  1. package/dist/agent/index.d.ts +1 -1
  2. package/dist/agent/index.d.ts.map +1 -1
  3. package/dist/agent/index.js +11 -11
  4. package/dist/cli.js +1 -1
  5. package/dist/commands/serve.d.ts +3 -3
  6. package/dist/commands/serve.d.ts.map +1 -1
  7. package/dist/commands/serve.js +5 -2
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/designer/index.d.ts +3 -3
  11. package/dist/designer/index.d.ts.map +1 -1
  12. package/dist/designer/index.js +9 -9
  13. package/dist/extensions/aexol-mcp.d.ts +6 -6
  14. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  15. package/dist/extensions/aexol-mcp.js +12 -12
  16. package/dist/extensions/kanban-bridge.d.ts +2 -2
  17. package/dist/extensions/kanban-bridge.d.ts.map +1 -1
  18. package/dist/extensions/kanban-bridge.js +3 -3
  19. package/dist/extensions/openrouter-attribution.d.ts +1 -1
  20. package/dist/extensions/openrouter-attribution.d.ts.map +1 -1
  21. package/dist/extensions/openrouter-attribution.js +2 -2
  22. package/dist/extensions/spectral-vision-fallback.d.ts +1 -1
  23. package/dist/extensions/spectral-vision-fallback.d.ts.map +1 -1
  24. package/dist/extensions/spectral-vision-fallback.js +3 -3
  25. package/dist/index.d.ts +4 -4
  26. package/dist/index.js +4 -4
  27. package/dist/mcp/commands.d.ts +1 -1
  28. package/dist/mcp/commands.d.ts.map +1 -1
  29. package/dist/mcp/commands.js +1 -1
  30. package/dist/mcp/config.d.ts +5 -5
  31. package/dist/mcp/config.d.ts.map +1 -1
  32. package/dist/mcp/config.js +15 -15
  33. package/dist/mcp/host-html-template.js +3 -3
  34. package/dist/mcp/index.d.ts +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +15 -13
  37. package/dist/mcp/init.d.ts +1 -1
  38. package/dist/mcp/init.d.ts.map +1 -1
  39. package/dist/mcp/init.js +4 -4
  40. package/dist/mcp/mcp-oauth-provider.js +1 -1
  41. package/dist/mcp/proxy-modes.d.ts +1 -1
  42. package/dist/mcp/proxy-modes.d.ts.map +1 -1
  43. package/dist/mcp/proxy-modes.js +2 -2
  44. package/dist/mcp/server-manager.js +2 -2
  45. package/dist/mcp/state-getter.d.ts +14 -0
  46. package/dist/mcp/state-getter.d.ts.map +1 -0
  47. package/dist/mcp/state-getter.js +21 -0
  48. package/dist/mcp/tool-registrar.js +1 -1
  49. package/dist/mcp/ui-server.js +1 -1
  50. package/dist/mcp/ui-stream-types.d.ts +11 -11
  51. package/dist/mcp/ui-stream-types.d.ts.map +1 -1
  52. package/dist/mcp/ui-stream-types.js +5 -5
  53. package/dist/mcp/utils.d.ts +2 -2
  54. package/dist/mcp/utils.d.ts.map +1 -1
  55. package/dist/mcp/utils.js +10 -10
  56. package/dist/mcp-client.d.ts +1 -1
  57. package/dist/mcp-client.js +1 -1
  58. package/dist/memory/commands/status.d.ts +1 -1
  59. package/dist/memory/commands/status.d.ts.map +1 -1
  60. package/dist/memory/commands/status.js +2 -2
  61. package/dist/memory/commands/view.d.ts +1 -1
  62. package/dist/memory/commands/view.d.ts.map +1 -1
  63. package/dist/memory/commands/view.js +2 -2
  64. package/dist/memory/hooks/compaction-hook.d.ts +1 -1
  65. package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
  66. package/dist/memory/hooks/compaction-hook.js +5 -5
  67. package/dist/memory/hooks/compaction-trigger.d.ts +1 -1
  68. package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
  69. package/dist/memory/hooks/compaction-trigger.js +3 -3
  70. package/dist/memory/hooks/observer-trigger.d.ts +1 -1
  71. package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
  72. package/dist/memory/hooks/observer-trigger.js +4 -4
  73. package/dist/memory/index.d.ts +1 -1
  74. package/dist/memory/index.d.ts.map +1 -1
  75. package/dist/memory/index.js +9 -9
  76. package/dist/memory/tools/read-project-observations.d.ts +1 -1
  77. package/dist/memory/tools/read-project-observations.d.ts.map +1 -1
  78. package/dist/memory/tools/read-project-observations.js +2 -2
  79. package/dist/memory/tools/recall-observation.d.ts +1 -1
  80. package/dist/memory/tools/recall-observation.d.ts.map +1 -1
  81. package/dist/memory/tools/recall-observation.js +2 -2
  82. package/dist/memory/tools/write-project-observation.d.ts +1 -1
  83. package/dist/memory/tools/write-project-observation.d.ts.map +1 -1
  84. package/dist/memory/tools/write-project-observation.js +2 -2
  85. package/dist/preflight.d.ts +1 -1
  86. package/dist/preflight.js +1 -1
  87. package/dist/relay/auto-research.d.ts +2 -2
  88. package/dist/relay/auto-research.js +34 -34
  89. package/dist/relay/dispatcher.d.ts +15 -6
  90. package/dist/relay/dispatcher.d.ts.map +1 -1
  91. package/dist/relay/dispatcher.js +33 -6
  92. package/dist/sdk/ai/types.d.ts +1 -1
  93. package/dist/sdk/ai/utils/oauth/openai-codex.d.ts +1 -1
  94. package/dist/sdk/ai/utils/oauth/openai-codex.js +2 -2
  95. package/dist/sdk/coding-agent/core/agent-session.d.ts +2 -2
  96. package/dist/sdk/coding-agent/core/agent-session.js +3 -3
  97. package/dist/sdk/coding-agent/core/auth-storage.d.ts +2 -2
  98. package/dist/sdk/coding-agent/core/auth-storage.js +2 -2
  99. package/dist/sdk/coding-agent/core/bash-executor.js +1 -1
  100. package/dist/sdk/coding-agent/core/compaction/branch-summarization.js +1 -1
  101. package/dist/sdk/coding-agent/core/compaction/compaction.js +1 -1
  102. package/dist/sdk/coding-agent/core/extensions/loader.d.ts.map +1 -1
  103. package/dist/sdk/coding-agent/core/extensions/loader.js +18 -22
  104. package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
  105. package/dist/sdk/coding-agent/core/extensions/runner.js +1 -1
  106. package/dist/sdk/coding-agent/core/extensions/types.d.ts +9 -9
  107. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  108. package/dist/sdk/coding-agent/core/package-manager.d.ts +1 -1
  109. package/dist/sdk/coding-agent/core/package-manager.d.ts.map +1 -1
  110. package/dist/sdk/coding-agent/core/package-manager.js +14 -14
  111. package/dist/sdk/coding-agent/core/sdk.d.ts +1 -1
  112. package/dist/sdk/coding-agent/core/sdk.js +2 -2
  113. package/dist/sdk/coding-agent/core/session-manager.d.ts +2 -2
  114. package/dist/sdk/coding-agent/core/session-manager.d.ts.map +1 -1
  115. package/dist/sdk/coding-agent/core/system-prompt.js +7 -7
  116. package/dist/sdk/coding-agent/core/tools/bash.d.ts +2 -2
  117. package/dist/sdk/coding-agent/core/tools/bash.js +3 -3
  118. package/dist/sdk/coding-agent/core/tools/output-accumulator.js +1 -1
  119. package/dist/sdk/coding-agent/migrations.d.ts +1 -1
  120. package/dist/sdk/coding-agent/migrations.js +4 -4
  121. package/dist/sdk/coding-agent/modes/print-mode.d.ts +2 -2
  122. package/dist/sdk/coding-agent/modes/print-mode.js +2 -2
  123. package/dist/sdk/coding-agent/utils/clipboard-image.js +1 -1
  124. package/dist/sdk/coding-agent/utils/spectral-user-agent.d.ts +2 -0
  125. package/dist/sdk/coding-agent/utils/spectral-user-agent.d.ts.map +1 -0
  126. package/dist/sdk/coding-agent/utils/spectral-user-agent.js +3 -0
  127. package/dist/sdk/coding-agent/utils/version-check.d.ts +5 -5
  128. package/dist/sdk/coding-agent/utils/version-check.d.ts.map +1 -1
  129. package/dist/sdk/coding-agent/utils/version-check.js +7 -7
  130. package/dist/sdk/coding-agent/utils/windows-self-update.js +1 -1
  131. package/dist/server/agent-bridge.d.ts +33 -33
  132. package/dist/server/agent-bridge.d.ts.map +1 -1
  133. package/dist/server/agent-bridge.js +58 -58
  134. package/dist/server/handlers/mcp-status.d.ts +21 -0
  135. package/dist/server/handlers/mcp-status.d.ts.map +1 -0
  136. package/dist/server/handlers/mcp-status.js +52 -0
  137. package/dist/server/handlers/sessions.d.ts +1 -1
  138. package/dist/server/handlers/sessions.js +1 -1
  139. package/dist/server/handlers/settings.d.ts +30 -0
  140. package/dist/server/handlers/settings.d.ts.map +1 -0
  141. package/dist/server/handlers/settings.js +123 -0
  142. package/dist/server/paths.d.ts +2 -2
  143. package/dist/server/paths.js +2 -2
  144. package/dist/server/session-stream.d.ts +25 -25
  145. package/dist/server/session-stream.d.ts.map +1 -1
  146. package/dist/server/session-stream.js +43 -34
  147. package/dist/server/shutdown.d.ts +3 -3
  148. package/dist/server/shutdown.d.ts.map +1 -1
  149. package/dist/server/shutdown.js +3 -3
  150. package/dist/server/storage.d.ts +4 -4
  151. package/dist/server/storage.js +6 -6
  152. package/dist/server/wire.d.ts +8 -8
  153. package/dist/server/wire.d.ts.map +1 -1
  154. package/dist/server/wire.js +1 -1
  155. package/package.json +1 -1
  156. package/dist/sdk/coding-agent/utils/pi-user-agent.d.ts +0 -2
  157. package/dist/sdk/coding-agent/utils/pi-user-agent.d.ts.map +0 -1
  158. package/dist/sdk/coding-agent/utils/pi-user-agent.js +0 -3
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Per-connection pi SDK lifecycle.
2
+ * Per-connection spectral SDK lifecycle.
3
3
  *
4
4
  * One `AgentBridge` instance per active WebSocket connection. Wraps:
5
5
  * - `createAgentSession` (in-memory session manager — we own persistence in
6
- * SQLite; pi doesn't need to write its own JSONL files).
7
- * - `subscribe` listener that translates pi `AgentSessionEvent`s into our
6
+ * SQLite; spectral doesn't need to write its own JSONL files).
7
+ * - `subscribe` listener that translates spectral `AgentSessionEvent`s into our
8
8
  * own `ServerEvent` wire format and pushes them through a caller-supplied
9
9
  * sink.
10
10
  * - `prompt(text)` to send user input.
11
11
  * - `dispose()` for clean teardown on WS close.
12
12
  *
13
- * Event mapping (pi → wire):
13
+ * Event mapping (agent → wire):
14
14
  * - `message_start` (assistant) → emit our own `message_start` with a
15
- * fresh UUID `messageId`. Pi's AssistantMessage has no stable id field, so
15
+ * fresh UUID `messageId`. spectral's AssistantMessage has no stable id field, so
16
16
  * we mint one per turn and use it for all subsequent deltas/tool events
17
17
  * until `message_end`.
18
18
  * - `message_update` w/ inner text_delta / thinking_delta → wire `text_delta`
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * Persistence shape:
31
31
  * `events_jsonl` is the newline-delimited JSON of the wire-format
32
- * `ServerEvent`s we emitted for this message — NOT raw pi
32
+ * `ServerEvent`s we emitted for this message — NOT raw spectral
33
33
  * `AgentSessionEvent`s. This guarantees the client's `parseWireEvents`
34
34
  * reducer can rehydrate the turn after a refresh using the exact same
35
35
  * reducer it uses for the live broadcast.
@@ -45,7 +45,7 @@
45
45
  * `createAgentSession` is called, each message is appended to the
46
46
  * in-memory SessionManager so the LLM sees the full conversation
47
47
  * context from the very first prompt. Multi-turn conversations within
48
- * a single pi session also work normally (the same AgentSession
48
+ * a single spectral session also work normally (the same AgentSession
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
51
  import { createJiti } from "@mariozechner/jiti";
@@ -64,12 +64,12 @@ import { Runtime } from "../memory/runtime.js";
64
64
  import { OBSERVATIONAL_MEMORY_CONTEXT_CUSTOM_TYPE, OBSERVATIONAL_MEMORY_SNAPSHOT_CUSTOM_TYPE, } from "../memory/types.js";
65
65
  import { fetchAllowedModels as defaultFetchAllowedModels, } from "../relay/models-fetch.js";
66
66
  /**
67
- * Synthetic provider names registered with pi's `ModelRegistry`. They route
67
+ * Synthetic provider names registered with spectral's `ModelRegistry`. They route
68
68
  * 100% of inference traffic through the backend (`${backendUrl}/v1`), which
69
69
  * authenticates with the machine JWT and forwards to the upstream provider
70
70
  * with centrally-managed API keys.
71
71
  *
72
- * Two providers (rather than one) because pi's `ProviderConfigInput.api`
72
+ * Two providers (rather than one) because spectral's `ProviderConfigInput.api`
73
73
  * picks the request shape per registration. Backend supports both, but a
74
74
  * single bag would force all models onto one shape; instead we group by
75
75
  * upstream provider type.
@@ -84,7 +84,7 @@ const SPECTRAL_PROXY_USER_MODEL = "spectral-proxy-user-model";
84
84
  *
85
85
  * Directories are returned in order from the deepest ancestor to `cwd` so
86
86
  * project-local skills override ancestor-level ones when there are name
87
- * collisions (pi's skill loader keeps the first found).
87
+ * collisions (spectral's skill loader keeps the first found).
88
88
  */
89
89
  function collectAncestorSkillDirs(cwd, relativePaths) {
90
90
  const found = [];
@@ -111,8 +111,8 @@ function collectAncestorSkillDirs(cwd, relativePaths) {
111
111
  /**
112
112
  * Concatenate text from an `AssistantMessage.content` array. Returns the
113
113
  * empty string when no text blocks are present (tool-only turns) or when
114
- * the input is missing/non-array (defensive — pi's `message_end` always
115
- * carries an array, but we don't want to crash on a future SDK change).
114
+ * the input is missing/non-array (defensive — spectral's `message_end` always
115
+ * carries an array, but we don't want to crash on a future sdk change).
116
116
  */
117
117
  function extractTextFromContent(content) {
118
118
  if (!Array.isArray(content))
@@ -129,7 +129,7 @@ function extractTextFromContent(content) {
129
129
  return out;
130
130
  }
131
131
  /**
132
- * Resolve the entry point of the pi-mcp-adapter extension.
132
+ * Resolve the entry point of the spectral-mcp-adapter extension.
133
133
  * Uses the bundled copy in dist/mcp/index.js (same directory as this file).
134
134
  * Returns the absolute path, or null if the bundled file is missing.
135
135
  */
@@ -145,8 +145,8 @@ function resolveMcpAdapterEntry() {
145
145
  /**
146
146
  * Token pricing per model (USD per 1M tokens). Matches provider list
147
147
  * prices as of May 2026. Used to compute token cost server-side when
148
- * pi's own cost field is unavailable (synthetic proxy models are
149
- * registered with zero cost to avoid pi-side billing).
148
+ * spectral's own cost field is unavailable (synthetic proxy models are
149
+ * registered with zero cost to avoid agent-side billing).
150
150
  *
151
151
  * Keys are matched as prefix substrings against modelId, so
152
152
  * `"claude-sonnet-4"` covers both `claude-sonnet-4-20250514` and any
@@ -386,7 +386,7 @@ export class AgentBridge {
386
386
  disposed = false;
387
387
  opts;
388
388
  /**
389
- * Pi's model registry. Built lazily in `start()` so we can resolve a
389
+ * spectral's model registry. Built lazily in `start()` so we can resolve a
390
390
  * `modelId` (envelope-supplied or SQLite-persisted) to a concrete `Model`
391
391
  * via `registry.getAll().find(m => m.id === modelId)` before invoking
392
392
  * `session.setModel()`. Phase 3 (Available Models whitelist).
@@ -400,9 +400,9 @@ export class AgentBridge {
400
400
  allowedModels;
401
401
  /**
402
402
  * Last `modelId` we successfully applied via `session.setModel()`, or
403
- * `undefined` if we never applied one (pi falls back to its own settings
403
+ * `undefined` if we never applied one (spectral falls back to its own settings
404
404
  * file in that case, matching pre-Phase-3 behaviour). Tracked so repeated
405
- * envelopes carrying the same modelId don't churn pi's internal state.
405
+ * envelopes carrying the same modelId don't churn spectral's internal state.
406
406
  */
407
407
  lastAppliedModelId;
408
408
  /** Current model's credit rates (from availableBaseModels), used for token_usage. */
@@ -413,38 +413,38 @@ export class AgentBridge {
413
413
  this.opts = opts;
414
414
  }
415
415
  /**
416
- * Create the pi session, wire up subscription, and return.
416
+ * Create the spectral session, wire up subscription, and return.
417
417
  * Throws on creation failure (caller should surface to client).
418
418
  */
419
419
  async start() {
420
420
  if (this.disposed)
421
421
  throw new Error("AgentBridge already disposed");
422
- const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
423
- // Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
422
+ const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (ext) => { spectralVisionExtension(ext); }, async (ext) => { subagentExt(ext); }, async (ext) => { designerExtension(ext); }, async (ext) => { observationalMemory(ext); }];
423
+ // Load spectral-mcp-adapter via jiti so tsc never crawls its .ts files in
424
424
  // node_modules. The static `import` was causing tsc to type-check
425
- // pi-mcp-adapter's source and fail the build on its type errors.
426
- // jiti is the same loader pi uses internally for all extensions.
425
+ // spectral-mcp-adapter's source and fail the build on its type errors.
426
+ // jiti is the same loader spectral uses internally for all extensions.
427
427
  const mcpAdapterPath = resolveMcpAdapterEntry();
428
428
  if (mcpAdapterPath) {
429
429
  try {
430
430
  const jiti = createJiti(import.meta.url, { moduleCache: false });
431
431
  const mcpAdapterFactory = await jiti.import(mcpAdapterPath, { default: true });
432
432
  if (typeof mcpAdapterFactory === "function") {
433
- extensionFactories.push(async (pi) => mcpAdapterFactory(pi));
433
+ extensionFactories.push(async (ext) => mcpAdapterFactory(ext));
434
434
  }
435
435
  }
436
436
  catch {
437
- console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
437
+ console.info("[AgentBridge] spectral-mcp-adapter not found; standard MCP servers disabled.");
438
438
  }
439
439
  }
440
440
  else {
441
- console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
441
+ console.info("[AgentBridge] spectral-mcp-adapter not found; standard MCP servers disabled.");
442
442
  }
443
443
  // ResourceLoader with extensions wired in via factories.
444
- // Each factory's signature `(pi: ExtensionAPI) => Promise<void>` matches
444
+ // Each factory's signature `(ext: ExtensionAPI) => Promise<void>` matches
445
445
  // the ExtensionFactory type exactly, so we can pass them directly.
446
446
  //
447
- // Skill discovery: pi's defaults scan ~/.spectral/agent/skills/ (user),
447
+ // Skill discovery: spectral's defaults scan ~/.spectral/agent/skills/ (user),
448
448
  // .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
449
449
  // (ancestor-walked). We additionally walk ancestors for
450
450
  // .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
@@ -581,7 +581,7 @@ export class AgentBridge {
581
581
  // provider credentials and the only allowed inference target. We then
582
582
  // register synthetic providers (`spectral-proxy-anthropic` /
583
583
  // `spectral-proxy-openai`) whose `baseUrl` points at the backend's
584
- // `/v1` proxy and whose `apiKey` is the machine JWT. Pi will then
584
+ // `/v1` proxy and whose `apiKey` is the machine JWT. spectral will then
585
585
  // POST `${baseUrl}/messages` (or `/chat/completions`) with
586
586
  // `Authorization: Bearer <machineJwt>` for every turn — the backend
587
587
  // verifies the JWT, looks up the requested `model` in the BaseModel
@@ -639,7 +639,7 @@ export class AgentBridge {
639
639
  }
640
640
  catch { /* best-effort */ }
641
641
  });
642
- // Emit session_start so extensions can initialize (e.g. pi-mcp-adapter
642
+ // Emit session_start so extensions can initialize (e.g. spectral-mcp-adapter
643
643
  // connects to MCP servers, loads configs from ~/.config/mcp/mcp.json etc.).
644
644
  // bindExtensions also fires resources_discover for dynamic skill/prompt
645
645
  // registration.
@@ -684,7 +684,7 @@ export class AgentBridge {
684
684
  * (OpenAI-compatible API). The backend supports both endpoints natively
685
685
  * (verified in F1).
686
686
  *
687
- * Pi will send `Authorization: Bearer ${apiKey}` (because `authHeader: true`)
687
+ * spectral will send `Authorization: Bearer ${apiKey}` (because `authHeader: true`)
688
688
  * which carries the machine JWT — the only credential the backend trusts.
689
689
  *
690
690
  * The `id` we register is the raw `modelId` (e.g. `claude-3-5-haiku-latest`),
@@ -707,7 +707,7 @@ export class AgentBridge {
707
707
  id: m.modelId,
708
708
  name: m.displayName,
709
709
  api: "anthropic-messages",
710
- // Pin provider/baseUrl explicitly so pi's ModelRegistry doesn't
710
+ // Pin provider/baseUrl explicitly so spectral's ModelRegistry doesn't
711
711
  // auto-derive `provider` from a slash-prefixed id (e.g. treating
712
712
  // `deepseek/deepseek-v4-pro` as provider `"deepseek"`), which would
713
713
  // make `hasConfiguredAuth(model)` look up the wrong provider key
@@ -735,7 +735,7 @@ export class AgentBridge {
735
735
  id: m.modelId,
736
736
  name: m.displayName,
737
737
  api: "openai-completions",
738
- // See anthropic batch above for rationale — without these, pi
738
+ // See anthropic batch above for rationale — without these, spectral
739
739
  // auto-derives `provider` from slash-prefixed ids like
740
740
  // `deepseek/deepseek-v4-pro` or `meta-llama/llama-3.3-70b-instruct`,
741
741
  // breaking auth lookup against our synthetic proxy provider.
@@ -781,7 +781,7 @@ export class AgentBridge {
781
781
  }
782
782
  }
783
783
  /**
784
- * Apply a sticky model selection to the underlying pi session, if it
784
+ * Apply a sticky model selection to the underlying spectral session, if it
785
785
  * differs from what was last applied. No-ops when:
786
786
  * - `modelId` is null/undefined (caller passed nothing to apply)
787
787
  * - the same modelId was already applied to this session
@@ -792,7 +792,7 @@ export class AgentBridge {
792
792
  * Returns true when the requested model is now in effect (either because
793
793
  * we just applied it or because it was already applied). Returns false
794
794
  * on resolution failure so the caller can skip `prompt()` and avoid
795
- * driving pi against the wrong model.
795
+ * driving spectral against the wrong model.
796
796
  *
797
797
  * Phase 3 (Available Models whitelist).
798
798
  */
@@ -809,7 +809,7 @@ export class AgentBridge {
809
809
  return this.allowedModels?.[0]?.modelId;
810
810
  }
811
811
  /**
812
- * Return current session context usage from pi's built-in estimator.
812
+ * Return current session context usage from spectral's built-in estimator.
813
813
  * Used after compaction and session start to push updated context-window
814
814
  * stats to the frontend without waiting for the next assistant turn.
815
815
  */
@@ -850,7 +850,7 @@ export class AgentBridge {
850
850
  }
851
851
  async setModel(modelId) {
852
852
  if (!modelId)
853
- return true; // nothing to apply — pi keeps its current model
853
+ return true; // nothing to apply — spectral keeps its current model
854
854
  if (!this.session)
855
855
  throw new Error("AgentBridge.start() not called");
856
856
  if (this.lastAppliedModelId === modelId)
@@ -858,7 +858,7 @@ export class AgentBridge {
858
858
  if (!this.modelRegistry) {
859
859
  // Defensive: start() always populates this; if it didn't we can't
860
860
  // resolve and must surface to the client rather than silently using
861
- // pi's default.
861
+ // spectral's default.
862
862
  this.opts.emit({
863
863
  type: "error",
864
864
  message: `Cannot apply modelId "${modelId}": model registry unavailable`,
@@ -884,7 +884,7 @@ export class AgentBridge {
884
884
  if (!model) {
885
885
  this.opts.emit({
886
886
  type: "error",
887
- message: `Unknown modelId "${modelId}" — not found in pi model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
887
+ message: `Unknown modelId "${modelId}" — not found in spectral model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
888
888
  });
889
889
  return false;
890
890
  }
@@ -914,18 +914,18 @@ export class AgentBridge {
914
914
  }
915
915
  }
916
916
  /**
917
- * Map a frontend reasoning-effort string to pi's ThinkingLevel.
917
+ * Map a frontend reasoning-effort string to spectral's ThinkingLevel.
918
918
  * Frontend sends: xhigh | high | medium | low | minimal | none | undefined
919
- * Pi expects: "high" | "medium" | "low" | "minimal" | "off"
919
+ * spectral expects: "high" | "medium" | "low" | "minimal" | "off"
920
920
  *
921
921
  * Mapping:
922
- * xhigh → high (pi doesn't have xhigh, default to max)
922
+ * xhigh → high (spectral doesn't have xhigh, default to max)
923
923
  * high → high
924
924
  * medium → medium
925
925
  * low → low
926
926
  * minimal → minimal
927
927
  * none → off
928
- * undefined → no-op (pi keeps whatever it has currently)
928
+ * undefined → no-op (spectral keeps whatever it has currently)
929
929
  */
930
930
  mapReasoningEffortToThinkingLevel(effort) {
931
931
  if (effort === undefined)
@@ -946,10 +946,10 @@ export class AgentBridge {
946
946
  }
947
947
  /**
948
948
  * Set the reasoning/thinking effort level for the next prompt.
949
- * Pass `undefined` to leave pi's current level unchanged.
949
+ * Pass `undefined` to leave spectral's current level unchanged.
950
950
  *
951
951
  * The caller (SessionStreamManager) is responsible for persisting the
952
- * value to SQLite; this method only applies it to pi's in-memory session.
952
+ * value to SQLite; this method only applies it to spectral's in-memory session.
953
953
  */
954
954
  setReasoningEffort(effort) {
955
955
  const level = this.mapReasoningEffortToThinkingLevel(effort);
@@ -973,13 +973,13 @@ export class AgentBridge {
973
973
  }
974
974
  }
975
975
  /**
976
- * Forward a user message to pi. Resolves when the full turn ends.
976
+ * Forward a user message to ext. Resolves when the full turn ends.
977
977
  * The caller is responsible for persisting the user message to SQLite
978
- * BEFORE invoking this — we don't do it here because pi's `prompt` may
978
+ * BEFORE invoking this — we don't do it here because spectral's `prompt` may
979
979
  * fail and we still want the user message recorded.
980
980
  *
981
981
  * When `images` is non-empty, each base64-encoded attachment is converted
982
- * to a pi `ImageContent` block and passed as `options.images` to
982
+ * to a spectral `ImageContent` block and passed as `options.images` to
983
983
  * `session.prompt()`. If the current model does not support image inputs,
984
984
  * images are instead converted to text placeholders so the conversation
985
985
  * can continue without errors.
@@ -1012,7 +1012,7 @@ export class AgentBridge {
1012
1012
  }
1013
1013
  }
1014
1014
  /**
1015
- * Manually compact the session context via pi's built-in compaction.
1015
+ * Manually compact the session context via spectral's built-in compaction.
1016
1016
  * Pi generates a summary of older conversation history, preserving the
1017
1017
  * most recent ~20K tokens verbatim. Compaction events are forwarded to
1018
1018
  * the wire through `handleEvent()`.
@@ -1136,8 +1136,8 @@ export class AgentBridge {
1136
1136
  }
1137
1137
  /**
1138
1138
  * Subscriber callback. Public so tests can drive event flow without
1139
- * spinning up a real pi session — production code never calls this
1140
- * directly; pi's `subscribe()` does, via the closure registered in
1139
+ * spinning up a real spectral session — production code never calls this
1140
+ * directly; spectral's `subscribe()` does, via the closure registered in
1141
1141
  * `start()`.
1142
1142
  */
1143
1143
  handleEvent(ev) {
@@ -1152,7 +1152,7 @@ export class AgentBridge {
1152
1152
  // Finalize the previous pending message (if any) before starting a
1153
1153
  // new one. This captures tool events that arrived after the previous
1154
1154
  // `message_end` but before this `message_start`. Deferred persistence
1155
- // is necessary because pi fires tool_execution_* events BETWEEN
1155
+ // is necessary because spectral fires tool_execution_* events BETWEEN
1156
1156
  // messages — after the previous `message_end` nulled the pending in
1157
1157
  // the old code, those tool events were lost from history.
1158
1158
  this.finalizePendingMessage();
@@ -1327,7 +1327,7 @@ export class AgentBridge {
1327
1327
  if (ev.message.role !== "assistant" || !this.pending)
1328
1328
  return;
1329
1329
  const { messageId, text } = this.pending;
1330
- // Prefer the assembled text from pi's final AssistantMessage
1330
+ // Prefer the assembled text from spectral's final AssistantMessage
1331
1331
  // (authoritative — providers like deepseek only populate this and
1332
1332
  // skip per-token `text_delta`s entirely). Fall back to the
1333
1333
  // accumulator for providers that stream deltas without re-asserting
@@ -1338,7 +1338,7 @@ export class AgentBridge {
1338
1338
  const endEvent = { type: "message_end", messageId };
1339
1339
  this.pending.wireEvents.push(endEvent);
1340
1340
  this.opts.emit(endEvent);
1341
- // Emit token usage for this assistant message. pi provides token
1341
+ // Emit token usage for this assistant message. spectral provides token
1342
1342
  // counts via ev.message.usage; credits are computed from the active
1343
1343
  // model's configured credit rates.
1344
1344
  //
@@ -1355,7 +1355,7 @@ export class AgentBridge {
1355
1355
  (usage?.cacheRead ?? 0) +
1356
1356
  (usage?.cacheWrite ?? 0);
1357
1357
  if (usage && totalTokens > 0) {
1358
- // Resolve current context window usage from pi's built-in
1358
+ // Resolve current context window usage from spectral's built-in
1359
1359
  // `getContextUsage()`, which handles post-compaction ambiguity
1360
1360
  // and estimates total tokens from the full session tree.
1361
1361
  const ctxUsage = this.session?.getContextUsage();
@@ -1378,7 +1378,7 @@ export class AgentBridge {
1378
1378
  this.opts.emit(usageEvent);
1379
1379
  }
1380
1380
  // Defer persistence: keep `this.pending` alive so tool events that
1381
- // arrive after `message_end` (pi fires tool_execution_* events
1381
+ // arrive after `message_end` (spectral fires tool_execution_* events
1382
1382
  // BETWEEN messages) are buffered into `pending.wireEvents`. We store
1383
1383
  // the final content and persist later — when the next `message_start`
1384
1384
  // signals a new step, or when `agent_end` closes the turn.
@@ -1415,7 +1415,7 @@ export class AgentBridge {
1415
1415
  return;
1416
1416
  }
1417
1417
  default:
1418
- // Other pi-internal events (turn_start, queue_update,
1418
+ // Other spectral-internal events (turn_start, queue_update,
1419
1419
  // auto_retry_*, tool_execution_update) are intentionally not on
1420
1420
  // the wire surface for MVP and are NOT persisted — the wire format
1421
1421
  // is the source of truth for replay.
@@ -1457,7 +1457,7 @@ function detectNotifSystem(message) {
1457
1457
  /**
1458
1458
  * Create a minimal ExtensionUIContext that forwards `notify()` calls as
1459
1459
  * `agent_notification` wire events. All other UI methods are no-ops —
1460
- * only extensions (not pi's TUI) call into the UI context in headless mode,
1460
+ * only extensions (not spectral's TUI) call into the UI context in headless mode,
1461
1461
  * and extensions that call methods other than `notify()` are expected to
1462
1462
  * guard with `ctx.hasUI` first.
1463
1463
  */
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Handler for `GET /api/mcp-status`.
3
+ *
4
+ * Returns a snapshot of the locally connected MCP servers managed by
5
+ * `spectral serve`. Pure handler pattern — no framework, no side effects.
6
+ * The caller (the relay dispatcher) injects `McpExtensionState`.
7
+ *
8
+ * This module intentionally imports ONLY `McpExtensionState` (a pure-type
9
+ * import from `state.ts`). It avoids `init.ts` / `config.ts` / etc. so the
10
+ * dispatcher never pulls in heavy transitive deps.
11
+ */
12
+ import type { McpExtensionState } from "../../mcp/state.js";
13
+ export interface McpServerStatus {
14
+ name: string;
15
+ status: "connected" | "disconnected" | "needs-auth" | "failed" | "cached";
16
+ toolCount: number;
17
+ /** Epoch-ms of the most recent connection failure, if the server is in a failed state. */
18
+ failedAt?: number;
19
+ }
20
+ export declare function handleListMcpStatus(state: McpExtensionState): McpServerStatus[];
21
+ //# sourceMappingURL=mcp-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-status.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/mcp-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,iBAAiB,GACvB,eAAe,EAAE,CA0BnB"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Handler for `GET /api/mcp-status`.
3
+ *
4
+ * Returns a snapshot of the locally connected MCP servers managed by
5
+ * `spectral serve`. Pure handler pattern — no framework, no side effects.
6
+ * The caller (the relay dispatcher) injects `McpExtensionState`.
7
+ *
8
+ * This module intentionally imports ONLY `McpExtensionState` (a pure-type
9
+ * import from `state.ts`). It avoids `init.ts` / `config.ts` / etc. so the
10
+ * dispatcher never pulls in heavy transitive deps.
11
+ */
12
+ /** Maximum age (ms) of a failure entry before we treat it as stale. */
13
+ const FAILURE_BACKOFF_MS = 60_000;
14
+ export function handleListMcpStatus(state) {
15
+ const result = [];
16
+ for (const name of Object.keys(state.config.mcpServers)) {
17
+ const connection = state.manager.getConnection(name);
18
+ const metadata = state.toolMetadata.get(name);
19
+ const toolCount = metadata?.length ?? 0;
20
+ const failedAgo = getFailureAgeSeconds(state, name);
21
+ let status = "disconnected";
22
+ let failedAt;
23
+ if (connection?.status === "connected") {
24
+ status = "connected";
25
+ }
26
+ else if (connection?.status === "needs-auth") {
27
+ status = "needs-auth";
28
+ }
29
+ else if (failedAgo !== null) {
30
+ status = "failed";
31
+ failedAt = state.failureTracker.get(name) ?? undefined;
32
+ }
33
+ else if (metadata !== undefined) {
34
+ status = "cached";
35
+ }
36
+ result.push({ name, status, toolCount, failedAt });
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * Mirrors `init.ts#getFailureAgeSeconds` without pulling in init.ts's
42
+ * heavy runtime deps (config, consent manager, UI resource handler, etc.).
43
+ */
44
+ function getFailureAgeSeconds(state, serverName) {
45
+ const failedAt = state.failureTracker.get(serverName);
46
+ if (!failedAt)
47
+ return null;
48
+ const ageMs = Date.now() - failedAt;
49
+ if (ageMs > FAILURE_BACKOFF_MS)
50
+ return null;
51
+ return Math.round(ageMs / 1000);
52
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Same shape as projects handlers — see that file's header for the contract.
5
5
  *
6
- * Note: session deletion does not tear down the in-flight pi stream itself.
6
+ * Note: session deletion does not tear down the in-flight spectral stream itself.
7
7
  * The Batch 3 dispatcher must call `manager.disposeSessionStream(id)` BEFORE
8
8
  * invoking `handleDeleteSession` so the FK cascade doesn't leave a zombie
9
9
  * bridge driving events at a row that no longer exists. This matches the
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Same shape as projects handlers — see that file's header for the contract.
5
5
  *
6
- * Note: session deletion does not tear down the in-flight pi stream itself.
6
+ * Note: session deletion does not tear down the in-flight spectral stream itself.
7
7
  * The Batch 3 dispatcher must call `manager.disposeSessionStream(id)` BEFORE
8
8
  * invoking `handleDeleteSession` so the FK cascade doesn't leave a zombie
9
9
  * bridge driving events at a row that no longer exists. This matches the
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
3
+ *
4
+ * Settings are stored in the machine's global `settings.json` under the
5
+ * `"observational-memory"` namespace (see `src/memory/config.ts`).
6
+ *
7
+ * GET /api/settings → returns full observational-memory config
8
+ * PUT /api/settings → updates a single key via `{ key, value }` body
9
+ *
10
+ * All paths are machine-level — no session context needed.
11
+ */
12
+ export interface PutSettingsInput {
13
+ key?: unknown;
14
+ value?: unknown;
15
+ }
16
+ export interface PutSettingsOutput {
17
+ ok: true;
18
+ key: string;
19
+ value: number;
20
+ }
21
+ export interface GetSettingsOutput {
22
+ observationThresholdTokens: number;
23
+ compactionThresholdTokens: number;
24
+ reflectionThresholdTokens: number;
25
+ passive: boolean;
26
+ debugLog: boolean;
27
+ }
28
+ export declare function handleGetSettings(cwd: string): GetSettingsOutput;
29
+ export declare function handlePutSettings(cwd: string, body: PutSettingsInput): PutSettingsOutput;
30
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAsFD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAShE;AAMD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,iBAAiB,CA0BnB"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
3
+ *
4
+ * Settings are stored in the machine's global `settings.json` under the
5
+ * `"observational-memory"` namespace (see `src/memory/config.ts`).
6
+ *
7
+ * GET /api/settings → returns full observational-memory config
8
+ * PUT /api/settings → updates a single key via `{ key, value }` body
9
+ *
10
+ * All paths are machine-level — no session context needed.
11
+ */
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import lockfile from "proper-lockfile";
15
+ import { getAgentDir } from "../../sdk/coding-agent/index.js";
16
+ import { loadConfig } from "../../memory/config.js";
17
+ import { BadRequestError } from "./errors.js";
18
+ /** Recognised setting keys (dot-separated path within the config object). */
19
+ const ALLOWED_KEYS = new Set([
20
+ "observationThresholdTokens",
21
+ "compactionThresholdTokens",
22
+ "reflectionThresholdTokens",
23
+ ]);
24
+ /** Sane bounds for token thresholds. */
25
+ const MIN_TOKENS = 1_000;
26
+ const MAX_TOKENS = 1_000_000;
27
+ // ---------------------------------------------------------------------------
28
+ // File helpers (inlined to avoid pulling in the full SettingsManager)
29
+ // ---------------------------------------------------------------------------
30
+ function acquireLockSyncWithRetry(path) {
31
+ const maxAttempts = 10;
32
+ const delayMs = 20;
33
+ let lastError;
34
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
35
+ try {
36
+ return lockfile.lockSync(path, { realpath: false });
37
+ }
38
+ catch (error) {
39
+ const code = typeof error === "object" && error !== null && "code" in error
40
+ ? String(error.code)
41
+ : undefined;
42
+ if (code !== "ELOCKED" || attempt === maxAttempts) {
43
+ throw error;
44
+ }
45
+ lastError = error;
46
+ const start = Date.now();
47
+ while (Date.now() - start < delayMs) {
48
+ // busy-wait; synchronous by design
49
+ }
50
+ }
51
+ }
52
+ throw lastError ?? new Error("Failed to acquire settings lock");
53
+ }
54
+ /**
55
+ * Atomically read-modify-write the global `settings.json` under the
56
+ * `"observational-memory"` namespace.
57
+ */
58
+ function withObservationalMemoryLock(fn) {
59
+ const path = join(getAgentDir(), "settings.json");
60
+ const dir = dirname(path);
61
+ let release;
62
+ try {
63
+ const fileExists = existsSync(path);
64
+ if (fileExists) {
65
+ release = acquireLockSyncWithRetry(path);
66
+ }
67
+ const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
68
+ const parsed = JSON.parse(raw);
69
+ const ns = (parsed["observational-memory"] ?? {});
70
+ const next = fn(ns);
71
+ if (next !== undefined) {
72
+ parsed["observational-memory"] = { ...ns, ...next };
73
+ if (!existsSync(dir)) {
74
+ mkdirSync(dir, { recursive: true });
75
+ }
76
+ if (!release) {
77
+ release = acquireLockSyncWithRetry(path);
78
+ }
79
+ writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
80
+ }
81
+ }
82
+ finally {
83
+ if (release) {
84
+ release();
85
+ }
86
+ }
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // GET handler
90
+ // ---------------------------------------------------------------------------
91
+ export function handleGetSettings(cwd) {
92
+ const config = loadConfig(cwd);
93
+ return {
94
+ observationThresholdTokens: config.observationThresholdTokens,
95
+ compactionThresholdTokens: config.compactionThresholdTokens,
96
+ reflectionThresholdTokens: config.reflectionThresholdTokens,
97
+ passive: config.passive,
98
+ debugLog: config.debugLog,
99
+ };
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // PUT handler
103
+ // ---------------------------------------------------------------------------
104
+ export function handlePutSettings(cwd, body) {
105
+ // Validate key
106
+ if (typeof body.key !== "string" || !ALLOWED_KEYS.has(body.key)) {
107
+ throw new BadRequestError(`settings key must be one of: ${[...ALLOWED_KEYS].join(", ")}`);
108
+ }
109
+ const key = body.key;
110
+ // Validate value
111
+ if (typeof body.value !== "number" || !Number.isInteger(body.value)) {
112
+ throw new BadRequestError("settings value must be an integer");
113
+ }
114
+ if (body.value < MIN_TOKENS || body.value > MAX_TOKENS) {
115
+ throw new BadRequestError(`token threshold must be between ${MIN_TOKENS.toLocaleString()} and ${MAX_TOKENS.toLocaleString()}`);
116
+ }
117
+ const value = body.value;
118
+ // Persist
119
+ withObservationalMemoryLock((current) => {
120
+ return { ...current, [key]: value };
121
+ });
122
+ return { ok: true, key, value };
123
+ }