@aexol/spectral 0.7.8 → 0.8.2

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 (197) 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/kanban-bridge.js +668 -0
  6. package/dist/extensions/spectral-vision-fallback.js +84 -46
  7. package/dist/mcp/agent-dir.js +1 -1
  8. package/dist/mcp/config.js +3 -3
  9. package/dist/mcp/init.js +1 -9
  10. package/dist/mcp/sampling-handler.js +1 -1
  11. package/dist/mcp/server-manager.js +5 -1
  12. package/dist/memory/commands/status.js +1 -1
  13. package/dist/memory/compaction.js +2 -2
  14. package/dist/memory/config.js +3 -3
  15. package/dist/memory/debug-log.js +1 -1
  16. package/dist/memory/index.js +2 -0
  17. package/dist/memory/observer.js +2 -2
  18. package/dist/memory/tokens.js +1 -1
  19. package/dist/memory/tools/read-project-observations.js +2 -2
  20. package/dist/memory/tools/recall-observation.js +2 -2
  21. package/dist/memory/tools/write-project-observation.js +60 -0
  22. package/dist/relay/auto-research.js +57 -23
  23. package/dist/relay/dispatcher.js +28 -2
  24. package/dist/relay/models-fetch.js +2 -2
  25. package/dist/{pi → sdk}/ai/env-api-keys.js +9 -49
  26. package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +1 -1
  27. package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +1 -1
  28. package/dist/{pi → sdk}/coding-agent/config.js +11 -78
  29. package/dist/{pi → sdk}/coding-agent/core/agent-session.js +2 -0
  30. package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
  31. package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +2 -35
  32. package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +1 -2
  33. package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
  34. package/dist/sdk/coding-agent/core/model-resolver-utils.js +8 -0
  35. package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +1 -1
  36. package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
  37. package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +1 -1
  38. package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
  39. package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
  40. package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +1 -170
  41. package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +3 -1
  42. package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
  43. package/dist/sdk/coding-agent/core/theme.js +202 -0
  44. package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +17 -18
  45. package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +7 -8
  46. package/dist/{pi → sdk}/coding-agent/core/tools/find.js +9 -13
  47. package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +10 -14
  48. package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +9 -10
  49. package/dist/{pi → sdk}/coding-agent/core/tools/read.js +15 -25
  50. package/dist/{pi/coding-agent/modes/interactive/components/diff.js → sdk/coding-agent/core/tools/render-diff.js} +18 -31
  51. package/dist/{pi → sdk}/coding-agent/core/tools/write.js +10 -11
  52. package/dist/{pi → sdk}/coding-agent/index.js +7 -5
  53. package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
  54. package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -1
  55. package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +2 -2
  56. package/dist/{pi → sdk}/coding-agent/utils/photon.js +2 -10
  57. package/dist/sdk/coding-agent/utils/pi-user-agent.js +3 -0
  58. package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
  59. package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
  60. package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
  61. package/dist/server/{pi-bridge.js → agent-bridge.js} +114 -97
  62. package/dist/server/handlers/sessions.js +21 -0
  63. package/dist/server/session-stream.js +5 -5
  64. package/package.json +6 -3
  65. package/dist/pi/coding-agent/bun/cli.js +0 -7
  66. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +0 -31
  67. package/dist/pi/coding-agent/cli/args.js +0 -340
  68. package/dist/pi/coding-agent/cli/file-processor.js +0 -82
  69. package/dist/pi/coding-agent/cli/initial-message.js +0 -21
  70. package/dist/pi/coding-agent/core/footer-data-provider.js +0 -309
  71. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +0 -35
  72. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +0 -26
  73. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +0 -3
  74. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +0 -1022
  75. package/dist/pi/coding-agent/utils/pi-user-agent.js +0 -4
  76. /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
  77. /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
  78. /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
  79. /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
  80. /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
  81. /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
  82. /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
  83. /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
  84. /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
  85. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
  86. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
  87. /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
  88. /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
  89. /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
  90. /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
  91. /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
  92. /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
  93. /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
  94. /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
  95. /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
  96. /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
  97. /package/dist/{pi → sdk}/agent-core/index.js +0 -0
  98. /package/dist/{pi → sdk}/agent-core/node.js +0 -0
  99. /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
  100. /package/dist/{pi → sdk}/agent-core/types.js +0 -0
  101. /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
  102. /package/dist/{pi → sdk}/ai/cli.js +0 -0
  103. /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
  104. /package/dist/{pi → sdk}/ai/image-models.js +0 -0
  105. /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
  106. /package/dist/{pi → sdk}/ai/images.js +0 -0
  107. /package/dist/{pi → sdk}/ai/index.js +0 -0
  108. /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
  109. /package/dist/{pi → sdk}/ai/models.js +0 -0
  110. /package/dist/{pi → sdk}/ai/oauth.js +0 -0
  111. /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
  112. /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
  113. /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
  114. /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
  115. /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
  116. /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
  117. /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
  118. /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
  119. /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
  120. /package/dist/{pi → sdk}/ai/stream.js +0 -0
  121. /package/dist/{pi → sdk}/ai/types.js +0 -0
  122. /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
  123. /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
  124. /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
  125. /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
  126. /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
  127. /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
  128. /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
  129. /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
  130. /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
  131. /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
  132. /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
  133. /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
  134. /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
  135. /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
  136. /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
  137. /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
  138. /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
  139. /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
  140. /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
  141. /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
  142. /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
  143. /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
  144. /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
  145. /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
  146. /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
  147. /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
  148. /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
  149. /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
  150. /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
  151. /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
  152. /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
  153. /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
  154. /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
  155. /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
  156. /package/dist/{pi → sdk}/coding-agent/core/keybindings.js +0 -0
  157. /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
  158. /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
  159. /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
  160. /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
  161. /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
  162. /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
  163. /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
  164. /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
  165. /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
  166. /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
  167. /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
  168. /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
  169. /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
  170. /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
  171. /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
  172. /package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +0 -0
  173. /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
  174. /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
  175. /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
  176. /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
  177. /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
  178. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
  179. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
  180. /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
  181. /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
  182. /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
  183. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
  184. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
  185. /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
  186. /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
  187. /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
  188. /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
  189. /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
  190. /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
  191. /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
  192. /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
  193. /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
  194. /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
  195. /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
  196. /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
  197. /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,14 @@
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 kanbanBridgeExtension from "../extensions/kanban-bridge.js";
59
+ import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
58
60
  import subagentExt from "../agent/index.js";
59
61
  import designerExtension from "../designer/index.js";
60
62
  import observationalMemory from "../memory/index.js";
@@ -133,7 +135,7 @@ function extractTextFromContent(content) {
133
135
  */
134
136
  function resolveMcpAdapterEntry() {
135
137
  const __dirname = dirname(fileURLToPath(import.meta.url));
136
- // pi-bridge.ts compiles to dist/server/pi-bridge.js;
138
+ // agent-bridge.ts compiles to dist/server/agent-bridge.js;
137
139
  // the bundled MCP adapter sits next door at dist/mcp/index.js.
138
140
  const bundledIndex = resolve(__dirname, "..", "mcp", "index.js");
139
141
  if (existsSync(bundledIndex))
@@ -195,6 +197,27 @@ function bareModelId(modelId) {
195
197
  const idx = modelId.lastIndexOf("/");
196
198
  return idx === -1 ? modelId : modelId.slice(idx + 1);
197
199
  }
200
+ /** Get model cost from backend credit rates, falling back to hardcoded pricing table. */
201
+ function getModelCost(m) {
202
+ // Prefer backend credit rates when configured
203
+ const hasCredits = m.creditInputPer1M != null ||
204
+ m.creditOutputPer1M != null ||
205
+ m.creditCacheReadPer1M != null ||
206
+ m.creditCacheWritePer1M != null;
207
+ if (hasCredits) {
208
+ return {
209
+ input: m.creditInputPer1M ?? 0,
210
+ output: m.creditOutputPer1M ?? 0,
211
+ cacheRead: m.creditCacheReadPer1M ?? 0,
212
+ cacheWrite: m.creditCacheWritePer1M ?? 0,
213
+ };
214
+ }
215
+ // Fall back to hardcoded pricing table when credits aren't configured
216
+ const pricing = lookupPricing(m.modelId);
217
+ return pricing
218
+ ? { input: pricing.input, output: pricing.output, cacheRead: pricing.cacheRead, cacheWrite: pricing.cacheWrite }
219
+ : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
220
+ }
198
221
  /** Look up pricing for a modelId. Returns null when unknown. */
199
222
  function lookupPricing(modelId) {
200
223
  const bare = bareModelId(modelId);
@@ -210,23 +233,6 @@ function lookupPricing(modelId) {
210
233
  }
211
234
  return null;
212
235
  }
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
236
  function inferSyntheticOpenAICompat(model) {
231
237
  const bare = bareModelId(model.modelId);
232
238
  const isDeepSeek = model.provider === "deepseek" ||
@@ -372,7 +378,7 @@ function toRestoredMemorySnapshot(snapshot) {
372
378
  details: normalizeSnapshotDetails(snapshot.details),
373
379
  };
374
380
  }
375
- export class PiBridge {
381
+ export class AgentBridge {
376
382
  session;
377
383
  sessionManager;
378
384
  unsubscribe;
@@ -412,8 +418,8 @@ export class PiBridge {
412
418
  */
413
419
  async start() {
414
420
  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); }];
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); }];
417
423
  // Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
418
424
  // node_modules. The static `import` was causing tsc to type-check
419
425
  // pi-mcp-adapter's source and fail the build on its type errors.
@@ -428,25 +434,25 @@ export class PiBridge {
428
434
  }
429
435
  }
430
436
  catch {
431
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
437
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
432
438
  }
433
439
  }
434
440
  else {
435
- console.info("[PiBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
441
+ console.info("[AgentBridge] pi-mcp-adapter not found; standard MCP servers disabled.");
436
442
  }
437
443
  // ResourceLoader with extensions wired in via factories.
438
444
  // Each factory's signature `(pi: ExtensionAPI) => Promise<void>` matches
439
445
  // the ExtensionFactory type exactly, so we can pass them directly.
440
446
  //
441
- // Skill discovery: pi's defaults scan ~/.pi/agent/skills/ (user),
442
- // .pi/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
447
+ // Skill discovery: pi's defaults scan ~/.spectral/agent/skills/ (user),
448
+ // .spectral/skills/ (project via CONFIG_DIR_NAME), and .agents/skills/
443
449
  // (ancestor-walked). We additionally walk ancestors for
444
450
  // .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
445
451
  // work out of the box.
446
452
  const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
447
453
  const resourceLoader = new DefaultResourceLoader({
448
454
  cwd: this.opts.cwd,
449
- agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.pi/agent`,
455
+ agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
450
456
  extensionFactories,
451
457
  noExtensions: false,
452
458
  noSkills: false,
@@ -552,7 +558,7 @@ export class PiBridge {
552
558
  }
553
559
  // system messages are informational only; skip for LLM context
554
560
  }
555
- console.info(`[PiBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
561
+ console.info(`[AgentBridge] Rehydrated ${this.opts.history.length} history message(s) into session context`);
556
562
  }
557
563
  if (this.opts.memorySnapshot) {
558
564
  const restoredSnapshot = toRestoredMemorySnapshot(this.opts.memorySnapshot);
@@ -568,10 +574,10 @@ export class PiBridge {
568
574
  coveredSourceCount: restoredSnapshot.coveredSourceCount,
569
575
  });
570
576
  }
571
- console.info(`[PiBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
577
+ console.info(`[AgentBridge] Restored observational memory snapshot covering ${restoredSnapshot.coveredSourceCount} source entr${restoredSnapshot.coveredSourceCount === 1 ? "y" : "ies"}`);
572
578
  }
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
579
+ // Build a model registry that does NOT touch ~/.spectral/agent/auth.json or
580
+ // ~/.spectral/agent/models.json — the backend is now the only source of
575
581
  // provider credentials and the only allowed inference target. We then
576
582
  // register synthetic providers (`spectral-proxy-anthropic` /
577
583
  // `spectral-proxy-openai`) whose `baseUrl` points at the backend's
@@ -584,39 +590,29 @@ export class PiBridge {
584
590
  // disk-based auth: this is the single path for `spectral serve`.
585
591
  const authStorage = AuthStorage.inMemory();
586
592
  this.modelRegistry = ModelRegistry.inMemory(authStorage);
587
- const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
588
593
  let allowedModels;
589
594
  try {
590
- allowedModels = await fetchModels({
591
- backendUrl: this.opts.backendUrl,
592
- machineJwt: this.opts.machineJwt,
593
- });
595
+ allowedModels = await this.refreshAllowedModels();
594
596
  }
595
597
  catch (err) {
596
598
  const e = err instanceof Error ? err : new Error(String(err));
597
599
  throw new Error(`Failed to fetch allowed models from backend; check SPECTRAL_BACKEND_URL ` +
598
600
  `and machine JWT. Underlying error: ${e.message}`);
599
601
  }
600
- this.allowedModels = allowedModels;
601
- this.registerSyntheticProviders(allowedModels);
602
602
  // Build an in-memory SettingsManager seeded with admin-configured
603
603
  // defaults from the backend. findInitialModel() will pick up the
604
604
  // isDefault model; the vision extension can query isVisionDefault.
605
605
  const settingsOverrides = {};
606
606
  const defaultModel = allowedModels.find((m) => m.isDefault);
607
607
  if (defaultModel) {
608
- const proxyProvider = defaultModel.provider === "anthropic"
609
- ? SPECTRAL_PROXY_ANTHROPIC
610
- : SPECTRAL_PROXY_OPENAI;
608
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultModel);
611
609
  settingsOverrides.defaultProvider = proxyProvider;
612
610
  settingsOverrides.defaultModel = defaultModel.modelId;
613
611
  console.info(`✓ Default model from backend: ${proxyProvider}/${defaultModel.modelId}`);
614
612
  }
615
613
  const defaultVisionModel = allowedModels.find((m) => m.isVisionDefault);
616
614
  if (defaultVisionModel) {
617
- const proxyProvider = defaultVisionModel.provider === "anthropic"
618
- ? SPECTRAL_PROXY_ANTHROPIC
619
- : SPECTRAL_PROXY_OPENAI;
615
+ const proxyProvider = this.proxyProviderForAllowedModel(defaultVisionModel);
620
616
  settingsOverrides.defaultVisionProvider = proxyProvider;
621
617
  settingsOverrides.defaultVisionModel = defaultVisionModel.modelId;
622
618
  console.info(`✓ Default vision model from backend: ${proxyProvider}/${defaultVisionModel.modelId}`);
@@ -649,15 +645,38 @@ export class PiBridge {
649
645
  // registration.
650
646
  try {
651
647
  await this.session.bindExtensions({ uiContext });
652
- console.info("[PiBridge] session_start emitted; extensions initialized.");
648
+ console.info("[AgentBridge] session_start emitted; extensions initialized.");
653
649
  }
654
650
  catch (err) {
655
651
  const msg = err instanceof Error ? err.message : String(err);
656
- console.warn(`[PiBridge] session_start failed (extension init error): ${msg}`);
652
+ console.warn(`[AgentBridge] session_start failed (extension init error): ${msg}`);
657
653
  }
658
654
  // Subscribe BEFORE any prompt fires.
659
655
  this.unsubscribe = this.session.subscribe((ev) => this.handleEvent(ev));
660
656
  }
657
+ proxyProviderForAllowedModel(model) {
658
+ if (model.provider === "anthropic") {
659
+ return SPECTRAL_PROXY_ANTHROPIC;
660
+ }
661
+ if (model.provider === "built-in") {
662
+ return SPECTRAL_PROXY_USER_MODEL;
663
+ }
664
+ return SPECTRAL_PROXY_OPENAI;
665
+ }
666
+ async refreshAllowedModels(opts) {
667
+ if (!this.modelRegistry) {
668
+ throw new Error("AgentBridge model registry unavailable");
669
+ }
670
+ const fetchModels = this.opts.fetchAllowedModels ?? defaultFetchAllowedModels;
671
+ const allowedModels = await fetchModels({
672
+ backendUrl: this.opts.backendUrl,
673
+ machineJwt: this.opts.machineJwt,
674
+ bypassCache: opts?.bypassCache,
675
+ });
676
+ this.allowedModels = allowedModels;
677
+ this.registerSyntheticProviders(allowedModels);
678
+ return allowedModels;
679
+ }
661
680
  /**
662
681
  * Register one synthetic provider per upstream API shape. Anthropic models
663
682
  * go to `${backendUrl}/v1/messages` (Messages API); everything else (OpenAI,
@@ -684,7 +703,6 @@ export class PiBridge {
684
703
  authHeader: true,
685
704
  api: "anthropic-messages",
686
705
  models: anthropicModels.map((m) => {
687
- const pricing = lookupPricing(m.modelId);
688
706
  return {
689
707
  id: m.modelId,
690
708
  name: m.displayName,
@@ -697,12 +715,9 @@ export class PiBridge {
697
715
  // at our synthetic proxy provider so auth resolves to the machine JWT.
698
716
  provider: SPECTRAL_PROXY_ANTHROPIC,
699
717
  baseUrl,
700
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
718
+ reasoning: m.supportsReasoning ?? false,
701
719
  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 },
720
+ cost: getModelCost(m),
706
721
  contextWindow: m.contextWindow ?? 0,
707
722
  maxTokens: 0,
708
723
  };
@@ -716,7 +731,6 @@ export class PiBridge {
716
731
  authHeader: true,
717
732
  api: "openai-completions",
718
733
  models: openaiCompatModels.map((m) => {
719
- const pricing = lookupPricing(m.modelId);
720
734
  return {
721
735
  id: m.modelId,
722
736
  name: m.displayName,
@@ -727,12 +741,9 @@ export class PiBridge {
727
741
  // breaking auth lookup against our synthetic proxy provider.
728
742
  provider: SPECTRAL_PROXY_OPENAI,
729
743
  baseUrl,
730
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
744
+ reasoning: m.supportsReasoning ?? false,
731
745
  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 },
746
+ cost: getModelCost(m),
736
747
  contextWindow: m.contextWindow ?? 0,
737
748
  maxTokens: 0,
738
749
  compat: inferSyntheticOpenAICompat(m),
@@ -752,18 +763,15 @@ export class PiBridge {
752
763
  authHeader: true,
753
764
  api: "openai-completions",
754
765
  models: userModelEntries.map((m) => {
755
- const pricing = lookupPricing(m.modelId);
756
766
  return {
757
767
  id: m.modelId,
758
768
  name: m.displayName,
759
769
  api: "openai-completions",
760
770
  provider: SPECTRAL_PROXY_USER_MODEL,
761
771
  baseUrl,
762
- reasoning: m.supportsReasoning ?? supportsReasoning(m.modelId),
772
+ reasoning: m.supportsReasoning ?? false,
763
773
  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 },
774
+ cost: getModelCost(m),
767
775
  contextWindow: m.contextWindow ?? 0,
768
776
  maxTokens: 0,
769
777
  compat: inferSyntheticOpenAICompat(m),
@@ -844,7 +852,7 @@ export class PiBridge {
844
852
  if (!modelId)
845
853
  return true; // nothing to apply — pi keeps its current model
846
854
  if (!this.session)
847
- throw new Error("PiBridge.start() not called");
855
+ throw new Error("AgentBridge.start() not called");
848
856
  if (this.lastAppliedModelId === modelId)
849
857
  return true; // idempotent: same model already in effect
850
858
  if (!this.modelRegistry) {
@@ -857,13 +865,26 @@ export class PiBridge {
857
865
  });
858
866
  return false;
859
867
  }
860
- const model = this.modelRegistry
868
+ let model = this.modelRegistry
861
869
  .getAvailable()
862
870
  .find((m) => m.id === modelId);
871
+ let refreshError;
872
+ if (!model) {
873
+ try {
874
+ await this.refreshAllowedModels({ bypassCache: true });
875
+ model = this.modelRegistry
876
+ .getAvailable()
877
+ .find((m) => m.id === modelId);
878
+ }
879
+ catch (err) {
880
+ refreshError = err instanceof Error ? err : new Error(String(err));
881
+ this.opts.onError?.(refreshError);
882
+ }
883
+ }
863
884
  if (!model) {
864
885
  this.opts.emit({
865
886
  type: "error",
866
- message: `Unknown modelId "${modelId}" — not found in pi model registry`,
887
+ message: `Unknown modelId "${modelId}" — not found in pi model registry${refreshError ? ` after refresh: ${refreshError.message}` : ""}`,
867
888
  });
868
889
  return false;
869
890
  }
@@ -965,16 +986,14 @@ export class PiBridge {
965
986
  */
966
987
  async prompt(text, images) {
967
988
  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;
989
+ throw new Error("AgentBridge.start() not called");
990
+ // Always pass images through to the session.
991
+ // The spectral-vision-fallback extension intercepts images via the
992
+ // `context` event and replaces them with text descriptions using a
993
+ // vision-capable model BEFORE they reach the LLM. This means images
994
+ // work regardless of whether the main model supports them.
976
995
  try {
977
- if (images && images.length > 0 && modelSupportsImages) {
996
+ if (images && images.length > 0) {
978
997
  const imageContents = images.map((img) => ({
979
998
  type: "image",
980
999
  data: img.data,
@@ -982,20 +1001,6 @@ export class PiBridge {
982
1001
  }));
983
1002
  await this.session.prompt(text, { images: imageContents });
984
1003
  }
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
1004
  else {
1000
1005
  await this.session.prompt(text);
1001
1006
  }
@@ -1017,7 +1022,7 @@ export class PiBridge {
1017
1022
  */
1018
1023
  async compact(customInstructions) {
1019
1024
  if (!this.session)
1020
- throw new Error("PiBridge.start() not called");
1025
+ throw new Error("AgentBridge.start() not called");
1021
1026
  this.memoryPhase = "compacting";
1022
1027
  try {
1023
1028
  await this.session.compact(customInstructions);
@@ -1106,13 +1111,13 @@ export class PiBridge {
1106
1111
  e.type === "tool_call" ||
1107
1112
  e.type === "tool_result");
1108
1113
  if (!finalContent && !hasMeaningfulEvent) {
1109
- console.debug("[pi-bridge] skipping empty intermediate message");
1114
+ console.debug("[agent-bridge] skipping empty intermediate message");
1110
1115
  try {
1111
1116
  this.opts.onAssistantMessageSkipped?.(messageId);
1112
1117
  }
1113
1118
  catch (err) {
1114
1119
  const e = err instanceof Error ? err : new Error(String(err));
1115
- console.error(`[pi-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1120
+ console.error(`[agent-bridge] onAssistantMessageSkipped failed: ${e.message}`);
1116
1121
  }
1117
1122
  return;
1118
1123
  }
@@ -1440,6 +1445,15 @@ function detectMemorySystem(message) {
1440
1445
  // Keep generic observational-memory messages visible in the landing badge.
1441
1446
  return "memory_observer";
1442
1447
  }
1448
+ /**
1449
+ * Detect subsystem from notification messages by known prefix.
1450
+ * Returns undefined for generic extension messages without a matching prefix.
1451
+ */
1452
+ function detectNotifSystem(message) {
1453
+ if (message.startsWith("[spectral-vision]"))
1454
+ return "vision";
1455
+ return detectMemorySystem(message);
1456
+ }
1443
1457
  /**
1444
1458
  * Create a minimal ExtensionUIContext that forwards `notify()` calls as
1445
1459
  * `agent_notification` wire events. All other UI methods are no-ops —
@@ -1450,15 +1464,18 @@ function detectMemorySystem(message) {
1450
1464
  function createHeadlessUIContext(emit) {
1451
1465
  // Defer to a Proxy so we don't need to stub every method.
1452
1466
  // `notify` is the only method called by extensions in serve mode
1453
- // (observational memory, MCP status bar updates).
1467
+ // (observational memory, MCP status bar updates, spectral-vision).
1454
1468
  const handler = {
1455
1469
  get(_target, prop) {
1456
1470
  if (prop === "notify") {
1457
1471
  return (message, type) => {
1458
1472
  const level = type ?? "info";
1459
- const system = detectMemorySystem(message);
1473
+ const system = detectNotifSystem(message);
1460
1474
  if (system?.startsWith("memory_")) {
1461
- console.info(`[PiBridge][memory][${level}] ${message}`);
1475
+ console.info(`[AgentBridge][memory][${level}] ${message}`);
1476
+ }
1477
+ else if (system === "vision") {
1478
+ console.info(`[AgentBridge][vision][${level}] ${message}`);
1462
1479
  }
1463
1480
  emit({
1464
1481
  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.2",
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
  },
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import { APP_NAME } from "../config.js";
3
- process.title = APP_NAME;
4
- process.emitWarning = (() => { });
5
- import { restoreSandboxEnv } from "./restore-sandbox-env.js";
6
- restoreSandboxEnv();
7
- await import("../cli.js");
@@ -1,31 +0,0 @@
1
- /**
2
- * Workaround for https://github.com/oven-sh/bun/issues/27802
3
- *
4
- * Bun compiled binaries have an empty `process.env` when running inside
5
- * sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover
6
- * the environment from `/proc/self/environ`.
7
- */
8
- import { readFileSync } from "node:fs";
9
- /**
10
- * Restore environment variables from `/proc/self/environ` when running
11
- * inside a sandbox where Bun's `process.env` is empty.
12
- */
13
- export function restoreSandboxEnv() {
14
- if (!process.versions?.bun)
15
- return;
16
- // If process.env already has entries, nothing to fix.
17
- if (Object.keys(process.env).length > 0)
18
- return;
19
- try {
20
- const data = readFileSync("/proc/self/environ", "utf-8");
21
- for (const entry of data.split("\0")) {
22
- const idx = entry.indexOf("=");
23
- if (idx > 0) {
24
- process.env[entry.slice(0, idx)] = entry.slice(idx + 1);
25
- }
26
- }
27
- }
28
- catch {
29
- // /proc/self/environ may not be readable; ignore.
30
- }
31
- }