@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,55 +1,87 @@
1
1
  /**
2
2
  * Spectral Vision Extension
3
3
  *
4
- * Automatically describes images using a vision-capable model and replaces
5
- * raw image data with text descriptions. This saves context for the main agent
6
- * and allows non-vision models to "see" images.
4
+ * Automatically switches to a vision-capable model for image processing
5
+ * when images are attached. This allows non-vision models to "see" images
6
+ * and saves main agent context by using a potentially smaller/cheaper
7
+ * vision model for image understanding.
7
8
  *
8
9
  * Logic:
9
10
  * - ALWAYS intercepts images (even when main model supports vision),
10
- * replacing them with text descriptions to save main agent context.
11
- * - Uses the main model for vision descriptions if it supports images.
12
- * - Falls back to the admin-configured default vision model (isVisionDefault
13
- * flag from backend via SettingsManager).
14
- * - If no admin default is configured, falls back to the first available
11
+ * replacing them with text descriptions.
12
+ * - Prefers the admin-configured default vision model (isVisionDefault
13
+ * flag from backend via SettingsManager) for image descriptions.
14
+ * - Falls back to the main model for vision if no admin default is
15
+ * configured and the main model supports images.
16
+ * - If neither is available, falls back to the first available
15
17
  * vision-capable model by provider priority.
16
18
  *
17
19
  * Hooks into the `context` event to intercept ALL images before they reach
18
20
  * the LLM (covers user-attached images and tool-result images alike).
19
21
  */
20
- import { streamSimple } from "../pi/ai/index.js";
22
+ import { streamSimple } from "../sdk/ai/index.js";
21
23
  // ---------------------------------------------------------------------------
22
24
  // Helpers
23
25
  // ---------------------------------------------------------------------------
24
- /** Find the best available vision-capable model from the registry */
25
- function findVisionModel(ctx) {
26
+ /**
27
+ * Check if the SettingsManager has an admin-configured default vision model
28
+ * with working auth. Returns the model if found and auth-ready, or undefined.
29
+ */
30
+ function getAdminVisionModel(ctx) {
31
+ const settings = ctx.settingsManager;
32
+ if (!settings)
33
+ return undefined;
34
+ const defaultVisionProvider = settings.getDefaultVisionProvider();
35
+ const defaultVisionModel = settings.getDefaultVisionModel();
36
+ if (!defaultVisionProvider || !defaultVisionModel)
37
+ return undefined;
26
38
  const allModels = ctx.modelRegistry.getAll();
27
- const availableModels = allModels.filter((m) => ctx.modelRegistry.hasConfiguredAuth(m));
28
- // Filter to models that support images
29
- const visionModels = availableModels.filter((m) => m.input.includes("image"));
30
- if (visionModels.length === 0)
39
+ const match = allModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
40
+ if (!match || !match.input.includes("image"))
31
41
  return undefined;
32
- // 1. Try admin-configured default vision model from settings
33
- const settings = ctx.settingsManager;
34
- if (settings) {
35
- const defaultVisionProvider = settings.getDefaultVisionProvider();
36
- const defaultVisionModel = settings.getDefaultVisionModel();
37
- if (defaultVisionProvider && defaultVisionModel) {
38
- const match = visionModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
39
- if (match) {
40
- process.stderr.write(`[spectral-vision] Using admin-configured default: ${match.provider}/${match.id}\n`);
41
- return match;
42
- }
43
- }
42
+ if (!ctx.modelRegistry.hasConfiguredAuth(match))
43
+ return undefined;
44
+ return match;
45
+ }
46
+ /**
47
+ * Resolve the best vision model to use for image descriptions.
48
+ *
49
+ * Priority:
50
+ * 1. Admin-configured default vision model (isVisionDefault from backend)
51
+ * 2. Current main model if it supports images
52
+ * 3. Best available vision model by provider priority
53
+ * 4. First available vision model
54
+ */
55
+ function resolveVisionModel(ctx) {
56
+ // 1. Admin-configured default vision model (always preferred)
57
+ const adminModel = getAdminVisionModel(ctx);
58
+ if (adminModel) {
59
+ process.stderr.write(`[spectral-vision] Using admin-configured default: ${adminModel.provider}/${adminModel.id}\n`);
60
+ return adminModel;
44
61
  }
45
- // 2. Fall back to provider priority
62
+ // 2. Current main model if it supports images
63
+ const currentModel = ctx.model;
64
+ if (currentModel?.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(currentModel)) {
65
+ process.stderr.write(`[spectral-vision] Using main model for vision: ${currentModel.provider}/${currentModel.id}\n`);
66
+ return currentModel;
67
+ }
68
+ // 3. Fall back to best available vision model by provider priority
69
+ const allModels = ctx.modelRegistry.getAll();
70
+ const visionModels = allModels.filter((m) => m.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(m));
71
+ if (visionModels.length === 0)
72
+ return undefined;
46
73
  const providerPriority = ["anthropic", "openai", "google", "openrouter"];
47
74
  for (const provider of providerPriority) {
48
75
  const match = visionModels.find((m) => m.provider === provider);
49
- if (match)
76
+ if (match) {
77
+ process.stderr.write(`[spectral-vision] Using fallback vision model: ${match.provider}/${match.id}\n`);
50
78
  return match;
79
+ }
51
80
  }
52
- return visionModels[0];
81
+ // 4. First available vision model
82
+ const fallback = visionModels[0];
83
+ process.stderr.write(`[spectral-vision] Using first available vision model: ${fallback.provider}/${fallback.id}\n`);
84
+ return fallback;
53
85
  }
54
86
  /** Check if any content block in an array is an image */
55
87
  function hasImageContent(content) {
@@ -111,6 +143,13 @@ async function describeImages(visionModel, content, contextText, ctx) {
111
143
  const textNote = description.trim()
112
144
  ? `[Image description from ${visionModel.provider}/${visionModel.id} (${imageCount} image(s)):\n${description}\n]`
113
145
  : `[${imageCount} image(s) — vision model returned empty description]`;
146
+ // Notify the frontend that the vision extension analyzed the image(s)
147
+ try {
148
+ ctx.ui.notify(`[spectral-vision] Analyzed ${imageCount} image(s) with ${visionModel.provider}/${visionModel.id}`, "info");
149
+ }
150
+ catch {
151
+ // UI context may not be available in headless modes
152
+ }
114
153
  return [
115
154
  ...textPrefixBlocks,
116
155
  { type: "text", text: textNote },
@@ -119,6 +158,13 @@ async function describeImages(visionModel, content, contextText, ctx) {
119
158
  catch (err) {
120
159
  const msg = err instanceof Error ? err.message : String(err);
121
160
  process.stderr.write(`[spectral-vision] Vision call failed: ${msg}\n`);
161
+ // Notify frontend about the failure
162
+ try {
163
+ ctx.ui.notify(`[spectral-vision] Failed to analyze ${imageCount} image(s): ${msg}`, "warning");
164
+ }
165
+ catch {
166
+ // UI context may not be available in headless modes
167
+ }
122
168
  return [
123
169
  ...textPrefixBlocks,
124
170
  {
@@ -134,7 +180,7 @@ async function describeImages(visionModel, content, contextText, ctx) {
134
180
  export default function spectralVisionExtension(pi) {
135
181
  let visionModel;
136
182
  pi.on("session_start", (_event, ctx) => {
137
- visionModel = findVisionModel(ctx);
183
+ visionModel = resolveVisionModel(ctx);
138
184
  if (visionModel) {
139
185
  process.stderr.write(`[spectral-vision] Ready — using ${visionModel.provider}/${visionModel.id} for image descriptions.\n`);
140
186
  }
@@ -151,27 +197,19 @@ export default function spectralVisionExtension(pi) {
151
197
  }
152
198
  if (totalImages === 0)
153
199
  return;
154
- // Resolve vision model
155
- const currentModel = ctx.model;
156
- // If main model supports images, use it for vision (saves auth setup)
157
- if (currentModel?.input.includes("image")) {
158
- visionModel = currentModel;
159
- }
160
- else {
161
- // Refresh vision model
162
- if (!visionModel ||
163
- !ctx.modelRegistry.hasConfiguredAuth(visionModel)) {
164
- visionModel = findVisionModel(ctx);
165
- }
166
- }
200
+ // Re-resolve vision model each time images are encountered.
201
+ // This ensures automatic switching to the admin-configured default
202
+ // vision model even when the main model supports vision.
203
+ visionModel = resolveVisionModel(ctx);
167
204
  if (!visionModel) {
168
205
  process.stderr.write("[spectral-vision] No vision model available, images will be omitted\n");
169
206
  return;
170
207
  }
171
208
  process.stderr.write(`[spectral-vision] Describing ${totalImages} image(s) with ${visionModel.provider}/${visionModel.id}...\n`);
172
- // Process each message
209
+ // Process each message — intercept both user messages and tool results
210
+ // that contain images (e.g. read tool returning a screenshot).
173
211
  const processed = await Promise.all(messages.map(async (msg) => {
174
- if (msg.role !== "user" || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
212
+ if ((msg.role !== "user" && msg.role !== "toolResult") || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
175
213
  return msg;
176
214
  }
177
215
  // Extract context from preceding text blocks
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  export function getAgentDir() {
4
4
  const configured = process.env.PI_CODING_AGENT_DIR?.trim();
5
5
  if (!configured) {
6
- return join(homedir(), ".pi", "agent");
6
+ return join(homedir(), ".spectral", "agent");
7
7
  }
8
8
  if (configured === "~") {
9
9
  return homedir();
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from "node:path";
5
5
  import { getAgentPath } from "./agent-dir.js";
6
6
  const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
7
7
  const PROJECT_CONFIG_NAME = ".mcp.json";
8
- const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
8
+ const PROJECT_MCP_CONFIG_NAME = ".spectral/mcp.json";
9
9
  const REPOPROMPT_BINARY_CANDIDATES = [
10
10
  join(homedir(), "RepoPrompt", "repoprompt_cli"),
11
11
  "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
@@ -32,7 +32,7 @@ export function getProjectConfigPath(cwd = process.cwd()) {
32
32
  return resolve(cwd, PROJECT_CONFIG_NAME);
33
33
  }
34
34
  export function getProjectPiConfigPath(cwd = process.cwd()) {
35
- return resolve(cwd, PROJECT_PI_CONFIG_NAME);
35
+ return resolve(cwd, PROJECT_MCP_CONFIG_NAME);
36
36
  }
37
37
  export function getConfigDiscoveryPaths(overridePath) {
38
38
  return getConfigSources(overridePath).map((source) => ({
@@ -368,7 +368,7 @@ function findProjectRoot(cwd = process.cwd()) {
368
368
  if (existsSync(join(current, ".git"))
369
369
  || existsSync(join(current, "package.json"))
370
370
  || existsSync(join(current, PROJECT_CONFIG_NAME))
371
- || existsSync(join(current, ".pi"))) {
371
+ || existsSync(join(current, ".spectral"))) {
372
372
  return current;
373
373
  }
374
374
  const parent = dirname(current);
package/dist/mcp/init.js CHANGED
@@ -208,15 +208,7 @@ export function flushMetadataCache(state) {
208
208
  }
209
209
  }
210
210
  }
211
- function safeFg(ui, color, text) {
212
- try {
213
- const styled = ui?.theme?.fg?.(color, text);
214
- if (styled)
215
- return styled;
216
- }
217
- catch {
218
- // fall through to plain text
219
- }
211
+ function safeFg(_ui, _color, text) {
220
212
  return text;
221
213
  }
222
214
  export function updateStatusBar(state) {
@@ -1,4 +1,4 @@
1
- import { complete } from "../pi/ai/index.js";
1
+ import { complete } from "../sdk/ai/index.js";
2
2
  import { truncateAtWord } from "./utils.js";
3
3
  import { CreateMessageRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  export function registerSamplingHandler(client, options) {
@@ -96,6 +96,8 @@ export class McpServerManager {
96
96
  }
97
97
  // Check for UnauthorizedError — server requires OAuth.
98
98
  // This can fire from createHttpTransport probe or client.connect.
99
+ // transport may be undefined if createHttpTransport threw during the probe
100
+ // before returning a transport; close() handles this safely.
99
101
  if (error instanceof UnauthorizedError && supportsOAuth(definition)) {
100
102
  return {
101
103
  client,
@@ -239,7 +241,9 @@ export class McpServerManager {
239
241
  connection.status = "closed";
240
242
  this.connections.delete(name);
241
243
  await connection.client.close().catch(() => { });
242
- await connection.transport.close().catch(() => { });
244
+ if (connection.transport) {
245
+ await connection.transport.close().catch(() => { });
246
+ }
243
247
  }
244
248
  async closeAll() {
245
249
  const names = [...this.connections.keys()];
@@ -1,4 +1,4 @@
1
- import { SettingsManager } from "../../pi/coding-agent/index.js";
1
+ import { SettingsManager } from "../../sdk/coding-agent/index.js";
2
2
  import { getMemoryState, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../branch.js";
3
3
  import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
4
4
  import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
@@ -1,5 +1,5 @@
1
- import { agentLoop } from "../pi/agent-core/index.js";
2
- import { Type } from "../pi/ai/index.js";
1
+ import { agentLoop } from "../sdk/agent-core/index.js";
2
+ import { Type } from "../sdk/ai/index.js";
3
3
  import { debugLog, isDebugLogEnabled } from "./debug-log.js";
4
4
  import { hashId } from "./ids.js";
5
5
  import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "../pi/coding-agent/index.js";
3
+ import { getAgentDir } from "../sdk/coding-agent/index.js";
4
4
  export const DEFAULTS = {
5
5
  observationThresholdTokens: 1_000,
6
6
  compactionThresholdTokens: 50_000,
7
- reflectionThresholdTokens: 30_000,
7
+ reflectionThresholdTokens: 10_000,
8
8
  passive: false,
9
9
  debugLog: false,
10
10
  observerMaxChunkTokens: 30_000,
@@ -72,7 +72,7 @@ function readNamespacedConfig(path) {
72
72
  }
73
73
  export function loadConfig(cwd, env = process.env) {
74
74
  const globalPath = join(getAgentDir(), "settings.json");
75
- const projectPath = join(cwd, ".pi", "settings.json");
75
+ const projectPath = join(cwd, ".spectral", "settings.json");
76
76
  return {
77
77
  ...DEFAULTS,
78
78
  ...readNamespacedConfig(globalPath),
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, appendFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { getAgentDir } from "../pi/coding-agent/index.js";
4
+ import { getAgentDir } from "../sdk/coding-agent/index.js";
5
5
  export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024;
6
6
  export const DEBUG_LOG_RELATIVE_PATH = join("observational-memory", "debug.ndjson");
7
7
  const storage = new AsyncLocalStorage();
@@ -6,6 +6,7 @@ import { registerObserverTrigger } from "./hooks/observer-trigger.js";
6
6
  import { Runtime } from "./runtime.js";
7
7
  import { registerRecallTool } from "./tools/recall-observation.js";
8
8
  import { registerReadProjectObservationsTool } from "./tools/read-project-observations.js";
9
+ import { registerWriteProjectObservationTool } from "./tools/write-project-observation.js";
9
10
  export default function observationalMemory(pi) {
10
11
  const runtime = new Runtime();
11
12
  // Log extension load so we can confirm it's running in serve mode.
@@ -17,4 +18,5 @@ export default function observationalMemory(pi) {
17
18
  registerViewCommand(pi, runtime);
18
19
  registerRecallTool(pi);
19
20
  registerReadProjectObservationsTool(pi);
21
+ registerWriteProjectObservationTool(pi);
20
22
  }
@@ -1,5 +1,5 @@
1
- import { agentLoop } from "../pi/agent-core/index.js";
2
- import { Type } from "../pi/ai/index.js";
1
+ import { agentLoop } from "../sdk/agent-core/index.js";
2
+ import { Type } from "../sdk/ai/index.js";
3
3
  import { hashId } from "./ids.js";
4
4
  import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
5
5
  import { OBSERVER_SYSTEM } from "./prompts.js";
@@ -1,4 +1,4 @@
1
- import { estimateTokens as estimateMessageTokens } from "../pi/coding-agent/index.js";
1
+ import { estimateTokens as estimateMessageTokens } from "../sdk/coding-agent/index.js";
2
2
  export function estimateStringTokens(text) {
3
3
  return Math.ceil(text.length / 4);
4
4
  }
@@ -1,5 +1,5 @@
1
- import { Type } from "../../pi/ai/index.js";
2
- import { defineTool } from "../../pi/coding-agent/index.js";
1
+ import { Type } from "../../sdk/ai/index.js";
2
+ import { defineTool } from "../../sdk/coding-agent/index.js";
3
3
  import { getProjectObsStore } from "../project-observations-store.js";
4
4
  export const READ_PROJECT_OBSERVATIONS_TOOL_NAME = "read_project_observations";
5
5
  export const readProjectObservationsTool = defineTool({
@@ -1,5 +1,5 @@
1
- import { Type } from "../../pi/ai/index.js";
2
- import { defineTool } from "../../pi/coding-agent/index.js";
1
+ import { Type } from "../../sdk/ai/index.js";
2
+ import { defineTool } from "../../sdk/coding-agent/index.js";
3
3
  import { recallMemorySources, } from "../branch.js";
4
4
  import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
5
5
  import { estimateEntryTokens } from "../tokens.js";
@@ -0,0 +1,60 @@
1
+ import { Type } from "../../sdk/ai/index.js";
2
+ import { defineTool } from "../../sdk/coding-agent/index.js";
3
+ import { getProjectObsStore } from "../project-observations-store.js";
4
+ import { hashId } from "../ids.js";
5
+ export const WRITE_PROJECT_OBSERVATION_TOOL_NAME = "write_project_observation";
6
+ export const writeProjectObservationTool = defineTool({
7
+ name: WRITE_PROJECT_OBSERVATION_TOOL_NAME,
8
+ label: "Write project observation",
9
+ description: "Write a durable, cross-session observation about this project. " +
10
+ "Use this to persist discovered conventions, architectural rules, " +
11
+ "gotchas, and directory purposes. Observations are deduplicated by " +
12
+ "content hash — writing the same fact twice is harmless (idempotent). " +
13
+ "Future sessions can discover these via read_project_observations.",
14
+ promptSnippet: "Use write_project_observation(content, relevance) to persist project-level knowledge.",
15
+ promptGuidelines: [
16
+ "Use for STABLE facts: conventions, architecture decisions, gotchas, file roles.",
17
+ "Use relevance='critical' for NEVER-EDIT rules (auto-generated files, invariant constraints).",
18
+ "Use relevance='high' for important conventions the agent must follow.",
19
+ "Use relevance='medium' for useful patterns and directory purposes.",
20
+ "Content must be a single, self-contained sentence — no markdown, no lists.",
21
+ "Deduplication is automatic — same content = same ID → INSERT OR REPLACE.",
22
+ ],
23
+ parameters: Type.Object({
24
+ content: Type.String({
25
+ minLength: 1,
26
+ description: "A single self-contained sentence describing the fact. No markdown, no lists.",
27
+ }),
28
+ relevance: Type.Union([
29
+ Type.Literal("low"),
30
+ Type.Literal("medium"),
31
+ Type.Literal("high"),
32
+ Type.Literal("critical"),
33
+ ], { description: "Importance: critical > high > medium > low" }),
34
+ }),
35
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
36
+ const store = getProjectObsStore();
37
+ if (!store) {
38
+ return {
39
+ content: [{ type: "text", text: "Project observations store is not available." }],
40
+ details: { status: "store_unavailable" },
41
+ };
42
+ }
43
+ const projectId = store.getProjectByCwd(ctx.cwd);
44
+ if (!projectId) {
45
+ return {
46
+ content: [{ type: "text", text: `No project found for current working directory: ${ctx.cwd}` }],
47
+ details: { status: "no_project" },
48
+ };
49
+ }
50
+ const id = hashId(params.content);
51
+ store.insertProjectObservations(projectId, "auto-research", [{ id, content: params.content, relevance: params.relevance }], Date.now());
52
+ return {
53
+ content: [{ type: "text", text: `Observation [${id}] recorded (relevance: ${params.relevance}).` }],
54
+ details: { id, status: "ok" },
55
+ };
56
+ },
57
+ });
58
+ export function registerWriteProjectObservationTool(pi) {
59
+ pi.registerTool(writeProjectObservationTool);
60
+ }