@aexol/spectral 0.7.8 → 0.8.0

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 (190) hide show
  1. package/dist/agent/agents.js +4 -4
  2. package/dist/agent/index.js +8 -8
  3. package/dist/cli.js +1 -1
  4. package/dist/commands/serve.js +1 -1
  5. package/dist/extensions/spectral-vision-fallback.js +81 -44
  6. package/dist/mcp/agent-dir.js +1 -1
  7. package/dist/mcp/config.js +3 -3
  8. package/dist/mcp/sampling-handler.js +1 -1
  9. package/dist/mcp/server-manager.js +5 -1
  10. package/dist/memory/commands/status.js +1 -1
  11. package/dist/memory/compaction.js +2 -2
  12. package/dist/memory/config.js +3 -3
  13. package/dist/memory/debug-log.js +1 -1
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/tokens.js +1 -1
  16. package/dist/memory/tools/read-project-observations.js +2 -2
  17. package/dist/memory/tools/recall-observation.js +2 -2
  18. package/dist/relay/auto-research.js +23 -23
  19. package/dist/relay/dispatcher.js +28 -2
  20. package/dist/relay/models-fetch.js +2 -2
  21. package/dist/{pi → sdk}/coding-agent/cli/args.js +4 -4
  22. package/dist/{pi → sdk}/coding-agent/config.js +9 -9
  23. package/dist/{pi → sdk}/coding-agent/core/agent-session.js +2 -0
  24. package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
  25. package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
  26. package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
  27. package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
  28. package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
  29. package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
  30. package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
  31. package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
  32. package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
  33. package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
  34. package/dist/server/{pi-bridge.js → agent-bridge.js} +113 -97
  35. package/dist/server/handlers/sessions.js +21 -0
  36. package/dist/server/session-stream.js +5 -5
  37. package/package.json +6 -3
  38. /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
  39. /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
  40. /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
  41. /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
  42. /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
  43. /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
  44. /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
  45. /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
  46. /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
  47. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
  48. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
  49. /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
  50. /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
  51. /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
  52. /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
  53. /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
  54. /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
  55. /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
  56. /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
  57. /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
  58. /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
  59. /package/dist/{pi → sdk}/agent-core/index.js +0 -0
  60. /package/dist/{pi → sdk}/agent-core/node.js +0 -0
  61. /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
  62. /package/dist/{pi → sdk}/agent-core/types.js +0 -0
  63. /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
  64. /package/dist/{pi → sdk}/ai/cli.js +0 -0
  65. /package/dist/{pi → sdk}/ai/env-api-keys.js +0 -0
  66. /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
  67. /package/dist/{pi → sdk}/ai/image-models.js +0 -0
  68. /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
  69. /package/dist/{pi → sdk}/ai/images.js +0 -0
  70. /package/dist/{pi → sdk}/ai/index.js +0 -0
  71. /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
  72. /package/dist/{pi → sdk}/ai/models.js +0 -0
  73. /package/dist/{pi → sdk}/ai/oauth.js +0 -0
  74. /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
  75. /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
  76. /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
  77. /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
  78. /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
  79. /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
  80. /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
  81. /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
  82. /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
  83. /package/dist/{pi → sdk}/ai/stream.js +0 -0
  84. /package/dist/{pi → sdk}/ai/types.js +0 -0
  85. /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
  86. /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
  87. /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
  88. /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
  89. /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
  90. /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
  91. /package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +0 -0
  92. /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
  93. /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
  94. /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
  95. /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
  96. /package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +0 -0
  97. /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
  98. /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
  99. /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
  100. /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
  101. /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
  102. /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
  103. /package/dist/{pi → sdk}/coding-agent/bun/cli.js +0 -0
  104. /package/dist/{pi → sdk}/coding-agent/bun/restore-sandbox-env.js +0 -0
  105. /package/dist/{pi → sdk}/coding-agent/cli/file-processor.js +0 -0
  106. /package/dist/{pi → sdk}/coding-agent/cli/initial-message.js +0 -0
  107. /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
  108. /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
  109. /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
  110. /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
  111. /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
  112. /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
  113. /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
  114. /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
  115. /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
  116. /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
  117. /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
  118. /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
  119. /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
  120. /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
  121. /package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +0 -0
  122. /package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +0 -0
  123. /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
  124. /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
  125. /package/dist/{pi → sdk}/coding-agent/core/footer-data-provider.js +0 -0
  126. /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
  127. /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
  128. /package/dist/{pi → sdk}/coding-agent/core/keybindings.js +0 -0
  129. /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
  130. /package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +0 -0
  131. /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
  132. /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
  133. /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
  134. /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
  135. /package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +0 -0
  136. /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
  137. /package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +0 -0
  138. /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
  139. /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
  140. /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
  141. /package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +0 -0
  142. /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
  143. /package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +0 -0
  144. /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
  145. /package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +0 -0
  146. /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
  147. /package/dist/{pi → sdk}/coding-agent/core/tools/find.js +0 -0
  148. /package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +0 -0
  149. /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
  150. /package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +0 -0
  151. /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
  152. /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
  153. /package/dist/{pi → sdk}/coding-agent/core/tools/read.js +0 -0
  154. /package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +0 -0
  155. /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
  156. /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
  157. /package/dist/{pi → sdk}/coding-agent/core/tools/write.js +0 -0
  158. /package/dist/{pi → sdk}/coding-agent/index.js +0 -0
  159. /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
  160. /package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -0
  161. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/diff.js +0 -0
  162. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/keybinding-hints.js +0 -0
  163. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/visual-truncate.js +0 -0
  164. /package/dist/{pi → sdk}/coding-agent/modes/interactive/interactive-mode.js +0 -0
  165. /package/dist/{pi → sdk}/coding-agent/modes/interactive/theme/theme.js +0 -0
  166. /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
  167. /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
  168. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
  169. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +0 -0
  170. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
  171. /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
  172. /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
  173. /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
  174. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
  175. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
  176. /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
  177. /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
  178. /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
  179. /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
  180. /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
  181. /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
  182. /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
  183. /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
  184. /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
  185. /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
  186. /package/dist/{pi → sdk}/coding-agent/utils/photon.js +0 -0
  187. /package/dist/{pi → sdk}/coding-agent/utils/pi-user-agent.js +0 -0
  188. /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
  189. /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
  190. /package/dist/{pi → sdk}/coding-agent/utils/syntax-highlight.js +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Per-connection pi SDK lifecycle.
3
3
  *
4
- * One `PiBridge` instance per active WebSocket connection. Wraps:
4
+ * One `AgentBridge` instance per active WebSocket connection. Wraps:
5
5
  * - `createAgentSession` (in-memory session manager — we own persistence in
6
6
  * SQLite; pi doesn't need to write its own JSONL files).
7
7
  * - `subscribe` listener that translates pi `AgentSessionEvent`s into our
@@ -41,7 +41,7 @@
41
41
  * History rehydration:
42
42
  * On first attach to a previously-created session (e.g. after a server
43
43
  * restart), the SessionStreamManager passes the full SQLite transcript
44
- * to the PiBridge via `PiBridgeOptions.history`. Before
44
+ * to the AgentBridge via `AgentBridgeOptions.history`. Before
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
@@ -49,12 +49,13 @@
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
51
  import { createJiti } from "@mariozechner/jiti";
52
- import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "../pi/coding-agent/index.js";
52
+ import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "../sdk/coding-agent/index.js";
53
53
  import { randomUUID } from "node:crypto";
54
54
  import { existsSync, statSync } from "node:fs";
55
55
  import { dirname, join, resolve } from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
57
  import aexolMcpExtension from "../extensions/aexol-mcp.js";
58
+ import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
58
59
  import subagentExt from "../agent/index.js";
59
60
  import designerExtension from "../designer/index.js";
60
61
  import observationalMemory from "../memory/index.js";
@@ -133,7 +134,7 @@ function extractTextFromContent(content) {
133
134
  */
134
135
  function resolveMcpAdapterEntry() {
135
136
  const __dirname = dirname(fileURLToPath(import.meta.url));
136
- // pi-bridge.ts compiles to dist/server/pi-bridge.js;
137
+ // agent-bridge.ts compiles to dist/server/agent-bridge.js;
137
138
  // the bundled MCP adapter sits next door at dist/mcp/index.js.
138
139
  const bundledIndex = resolve(__dirname, "..", "mcp", "index.js");
139
140
  if (existsSync(bundledIndex))
@@ -195,6 +196,27 @@ function bareModelId(modelId) {
195
196
  const idx = modelId.lastIndexOf("/");
196
197
  return idx === -1 ? modelId : modelId.slice(idx + 1);
197
198
  }
199
+ /** Get model cost from backend credit rates, falling back to hardcoded pricing table. */
200
+ function getModelCost(m) {
201
+ // Prefer backend credit rates when configured
202
+ const hasCredits = m.creditInputPer1M != null ||
203
+ m.creditOutputPer1M != null ||
204
+ m.creditCacheReadPer1M != null ||
205
+ m.creditCacheWritePer1M != null;
206
+ if (hasCredits) {
207
+ return {
208
+ input: m.creditInputPer1M ?? 0,
209
+ output: m.creditOutputPer1M ?? 0,
210
+ cacheRead: m.creditCacheReadPer1M ?? 0,
211
+ cacheWrite: m.creditCacheWritePer1M ?? 0,
212
+ };
213
+ }
214
+ // Fall back to hardcoded pricing table when credits aren't configured
215
+ const pricing = lookupPricing(m.modelId);
216
+ return pricing
217
+ ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
218
+ : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
219
+ }
198
220
  /** Look up pricing for a modelId. Returns null when unknown. */
199
221
  function lookupPricing(modelId) {
200
222
  const bare = bareModelId(modelId);
@@ -210,23 +232,6 @@ function lookupPricing(modelId) {
210
232
  }
211
233
  return null;
212
234
  }
213
- /**
214
- * Model prefixes known to support reasoning/thinking.
215
- * Mirrors pi-ai's supportsXhigh() + additional models.
216
- */
217
- const REASONING_SUPPORT_PREFIXES = [
218
- "gpt-5.2", "gpt-5.3", "gpt-5.4", "gpt-5.5",
219
- "claude-opus-4",
220
- "o3", "o4",
221
- "deepseek-r1",
222
- "deepseek-v4",
223
- "gemini-2.5",
224
- ];
225
- /** Check if a modelId prefix indicates reasoning/thinking support. */
226
- function supportsReasoning(modelId) {
227
- const bare = bareModelId(modelId);
228
- return REASONING_SUPPORT_PREFIXES.some((p) => modelId.startsWith(p) || bare.startsWith(p));
229
- }
230
235
  function inferSyntheticOpenAICompat(model) {
231
236
  const bare = bareModelId(model.modelId);
232
237
  const isDeepSeek = model.provider === "deepseek" ||
@@ -372,7 +377,7 @@ function toRestoredMemorySnapshot(snapshot) {
372
377
  details: normalizeSnapshotDetails(snapshot.details),
373
378
  };
374
379
  }
375
- export class PiBridge {
380
+ export class AgentBridge {
376
381
  session;
377
382
  sessionManager;
378
383
  unsubscribe;
@@ -412,8 +417,8 @@ export class PiBridge {
412
417
  */
413
418
  async start() {
414
419
  if (this.disposed)
415
- throw new Error("PiBridge already disposed");
416
- const extensionFactories = [aexolMcpExtension, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
420
+ throw new Error("AgentBridge already disposed");
421
+ const extensionFactories = [aexolMcpExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
417
422
  // Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
418
423
  // node_modules. The static `import` was causing tsc to type-check
419
424
  // pi-mcp-adapter's source and fail the build on its type errors.
@@ -428,25 +433,25 @@ export class PiBridge {
428
433
  }
429
434
  }
430
435
  catch {
431
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
436
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
432
437
  }
433
438
  }
434
439
  else {
435
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
440
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
436
441
  }
437
442
  // ResourceLoader with extensions wired in via factories.
438
443
  // Each factory's signature `(pi: ExtensionAPI) => Promise<void>` matches
439
444
  // the ExtensionFactory type exactly, so we can pass them directly.
440
445
  //
441
- // Skill discovery: pi's defaults scan ~/.pi/agent/skills/ (user),
442
- // .pi/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
446
+ // Skill discovery: pi's defaults scan ~/.spectral/agent/skills/ (user),
447
+ // .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
443
448
  // (ancestor-walked). We additionally walk ancestors for
444
449
  // .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
445
450
  // work out of the box.
446
451
  const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
447
452
  const resourceLoader = new DefaultResourceLoader({
448
453
  cwd: this.opts.cwd,
449
- agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.pi/agent`,
454
+ agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
450
455
  extensionFactories,
451
456
  noExtensions: false,
452
457
  noSkills: false,
@@ -552,7 +557,7 @@ export class PiBridge {
552
557
  }
553
558
  // system messages are informational only; skip for LLM context
554
559
  }
555
- console.info(`[PiBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
560
+ console.info(`[AgentBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
556
561
  }
557
562
  if (this.opts.memorySnapshot) {
558
563
  const restoredSnapshot = toRestoredMemorySnapshot(this.opts.memorySnapshot);
@@ -568,10 +573,10 @@ export class PiBridge {
568
573
  coveredSourceCount: restoredSnapshot.coveredSourceCount,
569
574
  });
570
575
  }
571
- console.info(`[PiBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
576
+ console.info(`[AgentBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
572
577
  }
573
- // Build a model registry that does NOT touch ~/.pi/agent/auth.json or
574
- // ~/.pi/agent/models.json — the backend is now the only source of
578
+ // Build a model registry that does NOT touch ~/.spectral/agent/auth.json or
579
+ // ~/.spectral/agent/models.json — the backend is now the only source of
575
580
  // provider credentials and the only allowed inference target. We then
576
581
  // register synthetic providers (`spectral-proxy-anthropic` /
577
582
  // `spectral-proxy-openai`) whose `baseUrl` points at the backend's
@@ -584,39 +589,29 @@ export class PiBridge {
584
589
  // disk-based auth: this is the single path for `spectral serve`.
585
590
  const authStorage = AuthStorage.inMemory();
586
591
  this.modelRegistry = ModelRegistry.inMemory(authStorage);
587
- const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
588
592
  let allowedModels;
589
593
  try {
590
- allowedModels = await fetchModels({
591
- backendUrl: this.opts.backendUrl,
592
- machineJwt: this.opts.machineJwt,
593
- });
594
+ allowedModels = await this.refreshAllowedModels();
594
595
  }
595
596
  catch (err) {
596
597
  const e = err instanceof Error ? err : new Error(String(err));
597
598
  throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
598
599
  `and machine JWT. Underlying error: ${e.message}`);
599
600
  }
600
- this.allowedModels = allowedModels;
601
- this.registerSyntheticProviders(allowedModels);
602
601
  // Build an in-memory SettingsManager seeded with admin-configured
603
602
  // defaults from the backend. findInitialModel() will pick up the
604
603
  // isDefault model; the vision extension can query isVisionDefault.
605
604
  const settingsOverrides = {};
606
605
  const defaultModel = allowedModels.find((m) => m.isDefault);
607
606
  if (defaultModel) {
608
- const proxyProvider = defaultModel.provider === "anthropic"
609
- ? SPECTRAL_PROXY_ANTHROPIC
610
- : SPECTRAL_PROXY_OPENAI;
607
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultModel);
611
608
  settingsOverrides.defaultProvider = proxyProvider;
612
609
  settingsOverrides.defaultModel = defaultModel.modelId;
613
610
  console.info(`✓ Default model from backend: ${proxyProvider}/${defaultModel.modelId}`);
614
611
  }
615
612
  const defaultVisionModel = allowedModels.find((m) => m.isVisionDefault);
616
613
  if (defaultVisionModel) {
617
- const proxyProvider = defaultVisionModel.provider === "anthropic"
618
- ? SPECTRAL_PROXY_ANTHROPIC
619
- : SPECTRAL_PROXY_OPENAI;
614
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultVisionModel);
620
615
  settingsOverrides.defaultVisionProvider = proxyProvider;
621
616
  settingsOverrides.defaultVisionModel = defaultVisionModel.modelId;
622
617
  console.info(`✓ Default vision model from backend: ${proxyProvider}/${defaultVisionModel.modelId}`);
@@ -649,15 +644,38 @@ export class PiBridge {
649
644
  // registration.
650
645
  try {
651
646
  await this.session.bindExtensions({ uiContext });
652
- console.info("[PiBridge] session_start emitted; extensions initialized.");
647
+ console.info("[AgentBridge] session_start emitted; extensions initialized.");
653
648
  }
654
649
  catch (err) {
655
650
  const msg = err instanceof Error ? err.message : String(err);
656
- console.warn(`[PiBridge] session_start failed (extension init error): ${msg}`);
651
+ console.warn(`[AgentBridge] session_start failed (extension init error): ${msg}`);
657
652
  }
658
653
  // Subscribe BEFORE any prompt fires.
659
654
  this.unsubscribe = this.session.subscribe((ev) => this.handleEvent(ev));
660
655
  }
656
+ proxyProviderForAllowedModel(model) {
657
+ if (model.provider === "anthropic") {
658
+ return SPECTRAL_PROXY_ANTHROPIC;
659
+ }
660
+ if (model.provider === "built-in") {
661
+ return SPECTRAL_PROXY_USER_MODEL;
662
+ }
663
+ return SPECTRAL_PROXY_OPENAI;
664
+ }
665
+ async refreshAllowedModels(opts) {
666
+ if (!this.modelRegistry) {
667
+ throw new Error("AgentBridge model registry unavailable");
668
+ }
669
+ const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
670
+ const allowedModels = await fetchModels({
671
+ backendUrl: this.opts.backendUrl,
672
+ machineJwt: this.opts.machineJwt,
673
+ bypassCache: opts?.bypassCache,
674
+ });
675
+ this.allowedModels = allowedModels;
676
+ this.registerSyntheticProviders(allowedModels);
677
+ return allowedModels;
678
+ }
661
679
  /**
662
680
  * Register one synthetic provider per upstream API shape. Anthropic models
663
681
  * go to `${backendUrl}/v1/messages` (Messages API); everything else (OpenAI,
@@ -684,7 +702,6 @@ export class PiBridge {
684
702
  authHeader: true,
685
703
  api: "anthropic-messages",
686
704
  models: anthropicModels.map((m) => {
687
- const pricing = lookupPricing(m.modelId);
688
705
  return {
689
706
  id: m.modelId,
690
707
  name: m.displayName,
@@ -697,12 +714,9 @@ export class PiBridge {
697
714
  // at our synthetic proxy provider so auth resolves to the machine JWT.
698
715
  provider: SPECTRAL_PROXY_ANTHROPIC,
699
716
  baseUrl,
700
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
717
+ reasoning: m.supportsReasoning ?? false,
701
718
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
702
- // Real pricing so pi can compute accurate token costs.
703
- cost: pricing
704
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
705
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
719
+ cost: getModelCost(m),
706
720
  contextWindow: m.contextWindow ?? 0,
707
721
  maxTokens: 0,
708
722
  };
@@ -716,7 +730,6 @@ export class PiBridge {
716
730
  authHeader: true,
717
731
  api: "openai-completions",
718
732
  models: openaiCompatModels.map((m) => {
719
- const pricing = lookupPricing(m.modelId);
720
733
  return {
721
734
  id: m.modelId,
722
735
  name: m.displayName,
@@ -727,12 +740,9 @@ export class PiBridge {
727
740
  // breaking auth lookup against our synthetic proxy provider.
728
741
  provider: SPECTRAL_PROXY_OPENAI,
729
742
  baseUrl,
730
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
743
+ reasoning: m.supportsReasoning ?? false,
731
744
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
732
- // Real pricing so pi can compute accurate token costs.
733
- cost: pricing
734
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
735
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
745
+ cost: getModelCost(m),
736
746
  contextWindow: m.contextWindow ?? 0,
737
747
  maxTokens: 0,
738
748
  compat: inferSyntheticOpenAICompat(m),
@@ -752,18 +762,15 @@ export class PiBridge {
752
762
  authHeader: true,
753
763
  api: "openai-completions",
754
764
  models: userModelEntries.map((m) => {
755
- const pricing = lookupPricing(m.modelId);
756
765
  return {
757
766
  id: m.modelId,
758
767
  name: m.displayName,
759
768
  api: "openai-completions",
760
769
  provider: SPECTRAL_PROXY_USER_MODEL,
761
770
  baseUrl,
762
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
771
+ reasoning: m.supportsReasoning ?? false,
763
772
  input: m.supportsImages !== false ? ["text", "image"] : ["text"],
764
- cost: pricing
765
- ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
766
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
773
+ cost: getModelCost(m),
767
774
  contextWindow: m.contextWindow ?? 0,
768
775
  maxTokens: 0,
769
776
  compat: inferSyntheticOpenAICompat(m),
@@ -844,7 +851,7 @@ export class PiBridge {
844
851
  if (!modelId)
845
852
  return true; // nothing to apply — pi keeps its current model
846
853
  if (!this.session)
847
- throw new Error("PiBridge.start() not called");
854
+ throw new Error("AgentBridge.start() not called");
848
855
  if (this.lastAppliedModelId === modelId)
849
856
  return true; // idempotent: same model already in effect
850
857
  if (!this.modelRegistry) {
@@ -857,13 +864,26 @@ export class PiBridge {
857
864
  });
858
865
  return false;
859
866
  }
860
- const model = this.modelRegistry
867
+ let model = this.modelRegistry
861
868
  .getAvailable()
862
869
  .find((m) => m.id === modelId);
870
+ let refreshError;
871
+ if (!model) {
872
+ try {
873
+ await this.refreshAllowedModels({ bypassCache: true });
874
+ model = this.modelRegistry
875
+ .getAvailable()
876
+ .find((m) => m.id === modelId);
877
+ }
878
+ catch (err) {
879
+ refreshError = err instanceof Error ? err : new Error(String(err));
880
+ this.opts.onError?.(refreshError);
881
+ }
882
+ }
863
883
  if (!model) {
864
884
  this.opts.emit({
865
885
  type: "error",
866
- message: `Unknown modelId "${modelId}" — not found in pi model registry`,
886
+ message: `Unknown modelId "${modelId}" — not found in pi model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
867
887
  });
868
888
  return false;
869
889
  }
@@ -965,16 +985,14 @@ export class PiBridge {
965
985
  */
966
986
  async prompt(text, images) {
967
987
  if (!this.session)
968
- throw new Error("PiBridge.start() not called");
969
- // Check whether the currently active model supports image input.
970
- // When `supportsImages` is null/undefined (unknown), we are conservative
971
- // and convert images to text rather than risking a 400 error.
972
- const currentModel = this.lastAppliedModelId
973
- ? this.allowedModels?.find((m) => m.modelId === this.lastAppliedModelId)
974
- : undefined;
975
- const modelSupportsImages = currentModel?.supportsImages === true;
988
+ throw new Error("AgentBridge.start() not called");
989
+ // Always pass images through to the session.
990
+ // The spectral-vision-fallback extension intercepts images via the
991
+ // `context` event and replaces them with text descriptions using a
992
+ // vision-capable model BEFORE they reach the LLM. This means images
993
+ // work regardless of whether the main model supports them.
976
994
  try {
977
- if (images && images.length > 0 && modelSupportsImages) {
995
+ if (images && images.length > 0) {
978
996
  const imageContents = images.map((img) => ({
979
997
  type: "image",
980
998
  data: img.data,
@@ -982,20 +1000,6 @@ export class PiBridge {
982
1000
  }));
983
1001
  await this.session.prompt(text, { images: imageContents });
984
1002
  }
985
- else if (images && images.length > 0 && !modelSupportsImages) {
986
- // Model doesn't support images — convert them to text descriptions
987
- // so the conversation can continue instead of hanging.
988
- const imageDescriptions = images
989
- .map((img, i) => `[Image ${i + 1}: ${img.mimeType}, ${img.data.length.toLocaleString()} bytes base64]`)
990
- .join("\n");
991
- const augmentedText = `${text}\n\n---\nThe following image(s) were attached but the current model does not support image input:\n${imageDescriptions}\n(Describe what you see or ask the user to switch to a model that supports images.)`;
992
- this.opts.emit({
993
- type: "agent_notification",
994
- message: `The current model does not support image input. ${images.length} image(s) were converted to text descriptions.`,
995
- level: "warning",
996
- });
997
- await this.session.prompt(augmentedText);
998
- }
999
1003
  else {
1000
1004
  await this.session.prompt(text);
1001
1005
  }
@@ -1017,7 +1021,7 @@ export class PiBridge {
1017
1021
  */
1018
1022
  async compact(customInstructions) {
1019
1023
  if (!this.session)
1020
- throw new Error("PiBridge.start() not called");
1024
+ throw new Error("AgentBridge.start() not called");
1021
1025
  this.memoryPhase = "compacting";
1022
1026
  try {
1023
1027
  await this.session.compact(customInstructions);
@@ -1106,13 +1110,13 @@ export class PiBridge {
1106
1110
  e.type === "tool_call" ||
1107
1111
  e.type === "tool_result");
1108
1112
  if (!finalContent && !hasMeaningfulEvent) {
1109
- console.debug("[pi-bridge] skipping empty intermediate message");
1113
+ console.debug("[agent-bridge] skipping empty intermediate message");
1110
1114
  try {
1111
1115
  this.opts.onAssistantMessageSkipped?.(messageId);
1112
1116
  }
1113
1117
  catch (err) {
1114
1118
  const e = err instanceof Error ? err : new Error(String(err));
1115
- console.error(`[pi-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1119
+ console.error(`[agent-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1116
1120
  }
1117
1121
  return;
1118
1122
  }
@@ -1440,6 +1444,15 @@ function detectMemorySystem(message) {
1440
1444
  // Keep generic observational-memory messages visible in the landing badge.
1441
1445
  return "memory_observer";
1442
1446
  }
1447
+ /**
1448
+ * Detect subsystem from notification messages by known prefix.
1449
+ * Returns undefined for generic extension messages without a matching prefix.
1450
+ */
1451
+ function detectNotifSystem(message) {
1452
+ if (message.startsWith("[spectral-vision]"))
1453
+ return "vision";
1454
+ return detectMemorySystem(message);
1455
+ }
1443
1456
  /**
1444
1457
  * Create a minimal ExtensionUIContext that forwards `notify()` calls as
1445
1458
  * `agent_notification` wire events. All other UI methods are no-ops —
@@ -1450,15 +1463,18 @@ function detectMemorySystem(message) {
1450
1463
  function createHeadlessUIContext(emit) {
1451
1464
  // Defer to a Proxy so we don't need to stub every method.
1452
1465
  // `notify` is the only method called by extensions in serve mode
1453
- // (observational memory, MCP status bar updates).
1466
+ // (observational memory, MCP status bar updates, spectral-vision).
1454
1467
  const handler = {
1455
1468
  get(_target, prop) {
1456
1469
  if (prop === "notify") {
1457
1470
  return (message, type) => {
1458
1471
  const level = type ?? "info";
1459
- const system = detectMemorySystem(message);
1472
+ const system = detectNotifSystem(message);
1460
1473
  if (system?.startsWith("memory_")) {
1461
- console.info(`[PiBridge][memory][${level}] ${message}`);
1474
+ console.info(`[AgentBridge][memory][${level}] ${message}`);
1475
+ }
1476
+ else if (system === "vision") {
1477
+ console.info(`[AgentBridge][vision][${level}] ${message}`);
1462
1478
  }
1463
1479
  emit({
1464
1480
  type: "agent_notification",
@@ -65,6 +65,27 @@ export async function handleCompactSession(store, manager, id) {
65
65
  }
66
66
  return { ok: true };
67
67
  }
68
+ /**
69
+ * Remember & delete: compact the session (which persists observations as
70
+ * project memory via the compaction hook), then delete the session.
71
+ *
72
+ * This gives the user a way to keep a session's reflections as durable
73
+ * cross-session memory even for short sessions that never hit the compaction
74
+ * threshold naturally.
75
+ */
76
+ export async function handleRememberAndDeleteSession(store, manager, id) {
77
+ const detail = store.getSession(id);
78
+ if (!detail)
79
+ throw new NotFoundError("Session not found");
80
+ try {
81
+ await manager.compactSession(id);
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ throw new BadRequestError(message);
86
+ }
87
+ return { ok: true };
88
+ }
68
89
  /**
69
90
  * Fork a session: create a new session copying all messages from the
70
91
  * source, with the `fork_compact_source_id` flag set so the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session-scoped streaming layer for `spectral serve`.
3
3
  *
4
- * Background: prior to this module each WebSocket owned its own `PiBridge`
4
+ * Background: prior to this module each WebSocket owned its own `AgentBridge`
5
5
  * instance and the routes layer enforced single-writer-wins (4001 eviction)
6
6
  * to keep that bridge unique per session. That model lost data on browser
7
7
  * refresh — the WS close torn down the pi process mid-stream, and a re-open
@@ -37,14 +37,14 @@
37
37
  * for now — streams accumulate for the lifetime of the server process.
38
38
  */
39
39
  import { randomUUID } from "node:crypto";
40
- import { PiBridge } from "./pi-bridge.js";
40
+ import { AgentBridge } from "./agent-bridge.js";
41
41
  import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
42
42
  import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
43
43
  import { loadConfig } from "../memory/config.js";
44
44
  import { setProjectObsStore } from "../memory/project-observations-store.js";
45
45
  import { estimateStringTokens } from "../memory/tokens.js";
46
46
  import { reflectionContent, reflectionId } from "../memory/types.js";
47
- const DEFAULT_BRIDGE_FACTORY = (args) => new PiBridge(args);
47
+ const DEFAULT_BRIDGE_FACTORY = (args) => new AgentBridge(args);
48
48
  /** Safety limit for autonomous loop iterations per session. */
49
49
  const MAX_LOOP_ITERATIONS = 100;
50
50
  /**
@@ -646,7 +646,7 @@ export class SessionStreamManager {
646
646
  assistantText: "",
647
647
  };
648
648
  // 4. Fire pi. `prompt` resolves on agent_end; errors are handled inside
649
- // PiBridge (it emits `error` for us). We don't await — broadcast is
649
+ // AgentBridge (it emits `error` for us). We don't await — broadcast is
650
650
  // driven by the bridge's emit callback.
651
651
  void stream.bridge.prompt(content, images);
652
652
  }
@@ -1127,7 +1127,7 @@ export class SessionStreamManager {
1127
1127
  }
1128
1128
  // Broadcast first, then maybe close out the turn. agent_end clears the
1129
1129
  // buffer because by that point the assistant message is already in
1130
- // SQLite (PiBridge calls onAssistantMessageComplete on message_end,
1130
+ // SQLite (AgentBridge calls onAssistantMessageComplete on message_end,
1131
1131
  // which fires before agent_end).
1132
1132
  //
1133
1133
  // Track context window state from token_usage events — the bridge emits
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.7.8",
4
- "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
3
+ "version": "0.8.0",
4
+ "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "bin": {
@@ -30,7 +30,6 @@
30
30
  "coding-agent",
31
31
  "ai",
32
32
  "cli",
33
- "pi",
34
33
  "claude",
35
34
  "openai",
36
35
  "agent",
@@ -46,6 +45,10 @@
46
45
  "url": "https://gitlab.aexol.com/aexol/spectral/-/issues"
47
46
  },
48
47
  "license": "MIT",
48
+ "spectralConfig": {
49
+ "name": "spectral",
50
+ "configDir": ".spectral"
51
+ },
49
52
  "publishConfig": {
50
53
  "access": "public"
51
54
  },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes