@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
@@ -41,7 +41,7 @@
41
41
  import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
42
42
  import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
43
43
  import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
44
- import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
44
+ import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
45
45
  import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
46
46
  import { shutdownState } from "../server/shutdown.js";
47
47
  import { handleAutoResearch } from "./auto-research.js";
@@ -126,6 +126,14 @@ export function matchRoute(method, path) {
126
126
  return { route: "compact_session", id };
127
127
  return null;
128
128
  }
129
+ // /api/sessions/:id/remember-and-delete
130
+ const rememberDeleteMatch = /^\/api\/sessions\/([^/]+)\/remember-and-delete$/.exec(cleanPath);
131
+ if (rememberDeleteMatch) {
132
+ const id = decodeURIComponent(rememberDeleteMatch[1]);
133
+ if (method === "POST")
134
+ return { route: "remember_and_delete_session", id };
135
+ return null;
136
+ }
129
137
  // /api/sessions/:id/fork
130
138
  const forkMatch = /^\/api\/sessions\/([^/]+)\/fork$/.exec(cleanPath);
131
139
  if (forkMatch) {
@@ -341,6 +349,24 @@ async function dispatchRoute(match, body, deps) {
341
349
  }
342
350
  case "compact_session":
343
351
  return await handleCompactSession(store, manager, id);
352
+ case "remember_and_delete_session": {
353
+ // Compact first — the compaction hook's persistProjectObservations
354
+ // runs inside compactSession, writing reflections to cross-session
355
+ // durable memory. On failure (e.g. no API key), we let the error
356
+ // propagate so the session is NOT deleted.
357
+ await handleRememberAndDeleteSession(store, manager, id);
358
+ const detail = store.getSession(id);
359
+ manager.disposeSessionStream(id);
360
+ handleDeleteSession(store, id);
361
+ if (detail) {
362
+ safePublish(publishMetaEvent, logger, {
363
+ type: "session_deleted",
364
+ projectId: detail.projectId,
365
+ sessionId: id,
366
+ });
367
+ }
368
+ return { ok: true };
369
+ }
344
370
  case "fork_session": {
345
371
  const session = handleForkSession(store, id, asObject(body));
346
372
  safePublish(publishMetaEvent, logger, {
@@ -682,7 +708,7 @@ export function detachAllSubscribers(manager, subscribers) {
682
708
  }
683
709
  /**
684
710
  * Dispatch an `auto_research` frame. Sends the auto-research task through
685
- * the existing PiBridge (backend proxy) instead of spawning a separate pi
711
+ * the existing AgentBridge (backend proxy) instead of spawning a separate pi
686
712
  * subprocess. This ensures auto-research uses the same model and API keys
687
713
  * as the active session.
688
714
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Fetch the admin-managed list of allowed base models from the backend.
3
3
  *
4
- * Used by `PiBridge` at startup to register synthetic providers
4
+ * Used by `AgentBridge` at startup to register synthetic providers
5
5
  * (`spectral-proxy-anthropic` / `spectral-proxy-openai`) that route every
6
6
  * inference call through the backend's `/v1/messages` and
7
7
  * `/v1/chat/completions` endpoints. The backend authenticates the call
@@ -32,7 +32,7 @@ const QUERY = `query AvailableBaseModels { availableBaseModels { name provider u
32
32
  /**
33
33
  * Fetch the whitelist of allowed base models. Throws on any failure with a
34
34
  * message tailored for an operator running `spectral serve` — the caller
35
- * (PiBridge.start) lets the throw propagate so the WS subscriber sees a
35
+ * (AgentBridge.start) lets the throw propagate so the WS subscriber sees a
36
36
  * clear error event instead of a silent fall-through to "no models".
37
37
  */
38
38
  export async function fetchAllowedModels(opts) {
@@ -232,7 +232,7 @@ ${chalk.bold("Options:")}
232
232
  --export <file> Export session file to HTML and exit
233
233
  --list-models [search] List available models (with optional fuzzy search)
234
234
  --verbose Force verbose startup (overrides quietStartup setting)
235
- --offline Disable startup network operations (same as PI_OFFLINE=1)
235
+ --offline Disable startup network operations (same as SPECTRAL_OFFLINE=1)
236
236
  --help, -h Show this help
237
237
  --version, -v Show version number
238
238
 
@@ -324,9 +324,9 @@ ${chalk.bold("Environment Variables:")}
324
324
  ${ENV_AGENT_DIR.padEnd(32)} - Config directory (default: ~/${CONFIG_DIR_NAME}/agent)
325
325
  ${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)
326
326
  PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
327
- PI_OFFLINE - Disable startup network operations when set to 1/true/yes
328
- PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
329
- PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
327
+ SPECTRAL_OFFLINE - Disable startup network operations when set to 1/true/yes
328
+ SPECTRAL_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
329
+ SPECTRAL_SHARE_VIEWER_URL - Base URL for /share command (default: https://spectral.dev/session/)
330
330
 
331
331
  ${chalk.bold("Built-in Tool Names:")}
332
332
  read - Read file contents
@@ -336,28 +336,28 @@ export function getBundledInteractiveAssetPath(name) {
336
336
  return join(getInteractiveAssetsDir(), name);
337
337
  }
338
338
  const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
339
- const piConfigName = pkg.piConfig?.name;
339
+ const spectralConfigName = pkg.spectralConfig?.name;
340
340
  export const PACKAGE_NAME = pkg.name || "index.ts";
341
- export const APP_NAME = piConfigName || "pi";
342
- export const APP_TITLE = piConfigName ? APP_NAME : "π";
343
- export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
341
+ export const APP_NAME = spectralConfigName || "spectral";
342
+ export const APP_TITLE = spectralConfigName ? APP_NAME : "spectral";
343
+ export const CONFIG_DIR_NAME = pkg.spectralConfig?.configDir || ".spectral";
344
344
  export const VERSION = pkg.version || "0.0.0";
345
- // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
345
+ // e.g., SPECTRAL_CODING_AGENT_DIR
346
346
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
347
347
  export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
348
348
  export function expandTildePath(path) {
349
349
  return normalizePath(path);
350
350
  }
351
- const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
351
+ const DEFAULT_SHARE_VIEWER_URL = "https://spectral.dev/session/";
352
352
  /** Get the share viewer URL for a gist ID */
353
353
  export function getShareViewerUrl(gistId) {
354
- const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;
354
+ const baseUrl = process.env.SPECTRAL_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL;
355
355
  return `${baseUrl}#${gistId}`;
356
356
  }
357
357
  // =============================================================================
358
- // User Config Paths (~/.pi/agent/*)
358
+ // User Config Paths (~/.spectral/agent/*)
359
359
  // =============================================================================
360
- /** Get the agent config directory (e.g., ~/.pi/agent/) */
360
+ /** Get the agent config directory (e.g., ~/.spectral/agent/) */
361
361
  export function getAgentDir() {
362
362
  const envDir = process.env[ENV_AGENT_DIR];
363
363
  if (envDir) {
@@ -465,6 +465,8 @@ export class AgentSession {
465
465
  */
466
466
  dispose() {
467
467
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
468
+ this.abortRetry();
469
+ this.agent.abort();
468
470
  this._disconnectFromAgent();
469
471
  this._eventListeners = [];
470
472
  cleanupSessionResources(this.sessionId);
@@ -543,6 +543,157 @@ export function prepareCompaction(pathEntries, settings) {
543
543
  };
544
544
  }
545
545
  // ============================================================================
546
+ // Tool Call Deduplication
547
+ // ============================================================================
548
+ /**
549
+ * Tools that always return the same result for the same arguments.
550
+ * Same (name, args) from any point in the session = duplicate.
551
+ * Only the most recent call is kept.
552
+ */
553
+ const IDEMPOTENT_READ_TOOLS = new Set(["read"]);
554
+ /**
555
+ * Tools whose output may differ between calls with the same arguments.
556
+ * Deduplication requires comparing actual outputs to determine equivalence.
557
+ */
558
+ const OUTPUT_DEPENDENT_TOOLS = new Set(["bash"]);
559
+ /**
560
+ * Tools that mutate state. Never deduplicated — chronological ordering matters.
561
+ */
562
+ const MUTABLE_TOOLS = new Set(["edit", "write"]);
563
+ /**
564
+ * Build a stable string key from a Record's sorted keys.
565
+ * Ensures {a:1, b:2} and {b:2, a:1} produce the same key.
566
+ */
567
+ function stableArgs(args) {
568
+ const sortedKeys = Object.keys(args).sort();
569
+ const sorted = {};
570
+ for (const key of sortedKeys) {
571
+ sorted[key] = args[key];
572
+ }
573
+ return JSON.stringify(sorted);
574
+ }
575
+ /**
576
+ * Compute the deduplication key for a tool call.
577
+ * - Idempotent-read tools: keyed by (name, args) — same args always same result
578
+ * - Output-dependent tools: keyed by (name, args, output) — output comparison needed
579
+ */
580
+ function toolCallDedupKey(toolName, args, toolCallId, resultMap) {
581
+ const argsKey = stableArgs(args);
582
+ if (OUTPUT_DEPENDENT_TOOLS.has(toolName)) {
583
+ const output = resultMap.get(toolCallId) ?? "";
584
+ return `${toolName}:${argsKey}:${output}`;
585
+ }
586
+ return `${toolName}:${argsKey}`;
587
+ }
588
+ /**
589
+ * Check whether a tool should be excluded from deduplication.
590
+ * Mutating tools and unknown tools are never deduplicated (conservative).
591
+ */
592
+ function isMutableOrUnknownTool(toolName) {
593
+ if (MUTABLE_TOOLS.has(toolName))
594
+ return true;
595
+ if (IDEMPOTENT_READ_TOOLS.has(toolName))
596
+ return false;
597
+ if (OUTPUT_DEPENDENT_TOOLS.has(toolName))
598
+ return false;
599
+ // Unknown tools: conservative — never deduplicate
600
+ return true;
601
+ }
602
+ /**
603
+ * Deduplicate repeated tool calls in a message array by keeping only the
604
+ * most recent occurrence of each (tool, args) pair.
605
+ *
606
+ * Strategy:
607
+ * - **Idempotent-read tools** (read, grep, glob): same (name, args) → keep last only.
608
+ * These tools return the same content for the same arguments.
609
+ * - **Output-dependent tools** (bash): same (name, args, output) → keep last only.
610
+ * Two bash calls with same command but different output are NOT duplicates.
611
+ * - **Mutating tools** (edit, write): never deduplicated. Chronological ordering
612
+ * of mutations matters for correctness.
613
+ * - **Unknown tools** (extensions, MCP, custom): never deduplicated. Conservative
614
+ * by default — only tools in the known sets above participate.
615
+ *
616
+ * Deduplication removes both the ToolCall block from the assistant message
617
+ * and the corresponding ToolResultMessage from the array.
618
+ *
619
+ * Recalculated each time compaction runs — prompt cache is only impacted
620
+ * alongside compression, not on every turn.
621
+ */
622
+ export function deduplicateToolCalls(messages) {
623
+ if (messages.length === 0)
624
+ return messages;
625
+ // Phase 1: Build result lookup for output-dependent tools
626
+ const resultMap = new Map();
627
+ for (const msg of messages) {
628
+ if (msg.role === "toolResult" && Array.isArray(msg.content)) {
629
+ const text = msg.content
630
+ .filter((c) => c.type === "text")
631
+ .map((c) => c.text)
632
+ .join("\n");
633
+ resultMap.set(msg.toolCallId, text);
634
+ }
635
+ }
636
+ // Phase 2: Walk newest→oldest, collect keys. The first encounter
637
+ // (newest) wins; all earlier tool calls with the same key are duplicates.
638
+ const seen = new Map(); // key → toolCallId (keep newest)
639
+ const duplicateIds = new Set();
640
+ for (let i = messages.length - 1; i >= 0; i--) {
641
+ const msg = messages[i];
642
+ if (msg.role !== "assistant")
643
+ continue;
644
+ if (!("content" in msg) || !Array.isArray(msg.content))
645
+ continue;
646
+ // Iterate blocks newest-first: within a single assistant message,
647
+ // the rightmost tool call is the "most recent" one.
648
+ for (let j = msg.content.length - 1; j >= 0; j--) {
649
+ const block = msg.content[j];
650
+ if (typeof block !== "object" || block === null)
651
+ continue;
652
+ if (!("type" in block) || block.type !== "toolCall")
653
+ continue;
654
+ const toolBlock = block;
655
+ if (isMutableOrUnknownTool(toolBlock.name))
656
+ continue;
657
+ const key = toolCallDedupKey(toolBlock.name, toolBlock.arguments, toolBlock.id, resultMap);
658
+ if (seen.has(key)) {
659
+ duplicateIds.add(toolBlock.id);
660
+ }
661
+ else {
662
+ seen.set(key, toolBlock.id);
663
+ }
664
+ }
665
+ }
666
+ if (duplicateIds.size === 0)
667
+ return messages;
668
+ // Phase 3: Filter out duplicate tool results and strip duplicate
669
+ // ToolCall blocks from assistant messages.
670
+ const deduped = [];
671
+ let modified = false;
672
+ for (const msg of messages) {
673
+ if (msg.role === "toolResult" && duplicateIds.has(msg.toolCallId)) {
674
+ modified = true;
675
+ continue;
676
+ }
677
+ if (msg.role === "assistant" && "content" in msg && Array.isArray(msg.content)) {
678
+ const originalLength = msg.content.length;
679
+ const filteredContent = msg.content.filter((block) => {
680
+ if (typeof block !== "object" || block === null)
681
+ return true;
682
+ if (!("type" in block) || block.type !== "toolCall")
683
+ return true;
684
+ return !duplicateIds.has(block.id);
685
+ });
686
+ if (filteredContent.length < originalLength) {
687
+ modified = true;
688
+ deduped.push({ ...msg, content: filteredContent });
689
+ continue;
690
+ }
691
+ }
692
+ deduped.push(msg);
693
+ }
694
+ return modified ? deduped : messages;
695
+ }
696
+ // ============================================================================
546
697
  // Main compaction function
547
698
  // ============================================================================
548
699
  const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
@@ -568,22 +719,27 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
568
719
  */
569
720
  export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn) {
570
721
  const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
722
+ // Remove redundant tool calls before summarization.
723
+ // Deduplication is applied here (not in prepareCompaction) so that
724
+ // the session_before_compact extension hook receives raw messages.
725
+ const dedupedMessages = deduplicateToolCalls(messagesToSummarize);
726
+ const dedupedTurnPrefix = deduplicateToolCalls(turnPrefixMessages);
571
727
  // Generate summaries (can be parallel if both needed) and merge into one
572
728
  let summary;
573
- if (isSplitTurn && turnPrefixMessages.length > 0) {
729
+ if (isSplitTurn && dedupedTurnPrefix.length > 0) {
574
730
  // Generate both summaries in parallel
575
731
  const [historyResult, turnPrefixResult] = await Promise.all([
576
- messagesToSummarize.length > 0
577
- ? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn)
732
+ dedupedMessages.length > 0
733
+ ? generateSummary(dedupedMessages, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn)
578
734
  : Promise.resolve("No prior history."),
579
- generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
735
+ generateTurnPrefixSummary(dedupedTurnPrefix, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
580
736
  ]);
581
737
  // Merge into single summary
582
738
  summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
583
739
  }
584
740
  else {
585
741
  // Just generate history summary
586
- summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn);
742
+ summary = await generateSummary(dedupedMessages, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn);
587
743
  }
588
744
  // Compute file lists and append to summary
589
745
  const { readFiles, modifiedFiles } = computeFileLists(fileOps);
@@ -241,16 +241,23 @@ export class ModelRegistry {
241
241
  loadError = undefined;
242
242
  authStorage;
243
243
  modelsJsonPath;
244
- constructor(authStorage, modelsJsonPath) {
244
+ constructor(authStorage, modelsJsonPath, loadBuiltins) {
245
245
  this.authStorage = authStorage;
246
246
  this.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined;
247
- this.loadModels();
247
+ if (loadBuiltins) {
248
+ this.loadModels();
249
+ }
250
+ // When loadBuiltins is false, we start with an empty model list
251
+ // — the caller is responsible for populating it via registerProvider().
252
+ // This is used in relay/serve mode where all models come from the
253
+ // backend's synthetic proxy providers, and loading the 16K-line
254
+ // models.generated.ts is pure startup waste.
248
255
  }
249
256
  static create(authStorage, modelsJsonPath = join(getAgentDir(), "models.json")) {
250
- return new ModelRegistry(authStorage, modelsJsonPath);
257
+ return new ModelRegistry(authStorage, modelsJsonPath, true);
251
258
  }
252
259
  static inMemory(authStorage) {
253
- return new ModelRegistry(authStorage, undefined);
260
+ return new ModelRegistry(authStorage, undefined, false);
254
261
  }
255
262
  /**
256
263
  * Reload models from disk (built-in + custom from models.json).
@@ -33,7 +33,7 @@ const NETWORK_TIMEOUT_MS = 10000;
33
33
  const UPDATE_CHECK_CONCURRENCY = 4;
34
34
  const GIT_UPDATE_CONCURRENCY = 4;
35
35
  function isOfflineModeEnabled() {
36
- const value = process.env.PI_OFFLINE;
36
+ const value = process.env.SPECTRAL_OFFLINE;
37
37
  if (!value)
38
38
  return false;
39
39
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
@@ -1833,9 +1833,9 @@ export class DefaultPackageManager {
1833
1833
  this.addResource(target, path, metadata, enabled);
1834
1834
  }
1835
1835
  };
1836
- // Project extensions from .pi/
1836
+ // Project extensions from .spectral/
1837
1837
  addResources("extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadata, projectOverrides.extensions, projectBaseDir);
1838
- // Project skills from .pi/
1838
+ // Project skills from .spectral/
1839
1839
  addResources("skills", collectAutoSkillEntries(projectDirs.skills, "pi"), projectMetadata, projectOverrides.skills, projectBaseDir);
1840
1840
  // Project skills from .agents/ (each with its own baseDir)
1841
1841
  for (const agentsSkillsDir of projectAgentsSkillDirs) {
@@ -1848,9 +1848,9 @@ export class DefaultPackageManager {
1848
1848
  }
1849
1849
  addResources("prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadata, projectOverrides.prompts, projectBaseDir);
1850
1850
  addResources("themes", collectAutoThemeEntries(projectDirs.themes), projectMetadata, projectOverrides.themes, projectBaseDir);
1851
- // User extensions from ~/.pi/agent/
1851
+ // User extensions from ~/.spectral/agent/
1852
1852
  addResources("extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir);
1853
- // User skills from ~/.pi/agent/
1853
+ // User skills from ~/.spectral/agent/
1854
1854
  addResources("skills", collectAutoSkillEntries(userDirs.skills, "pi"), userMetadata, userOverrides.skills, globalBaseDir);
1855
1855
  // User skills from ~/.agents/ (with its own baseDir)
1856
1856
  const userAgentsBaseDir = dirname(userAgentsSkillsDir);
@@ -31,7 +31,7 @@ function getAttributionHeaders(model, settingsManager) {
31
31
  }
32
32
  if (model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai")) {
33
33
  return {
34
- "HTTP-Referer": "https://pi.dev",
34
+ "HTTP-Referer": "https://spectral.dev",
35
35
  "X-OpenRouter-Title": "pi",
36
36
  "X-OpenRouter-Categories": "cli-agent",
37
37
  };
@@ -208,7 +208,7 @@ export function buildSessionContext(entries, leafId, byId) {
208
208
  }
209
209
  /**
210
210
  * Compute the default session directory for a cwd.
211
- * Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
211
+ * Encodes cwd into a safe directory name under ~/.spectral/agent/sessions/.
212
212
  */
213
213
  export function getDefaultSessionDir(cwd, agentDir = getDefaultAgentDir()) {
214
214
  const resolvedCwd = resolvePath(cwd);
@@ -1002,7 +1002,7 @@ export class SessionManager {
1002
1002
  /**
1003
1003
  * Create a new session.
1004
1004
  * @param cwd Working directory (stored in session header)
1005
- * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
1005
+ * @param sessionDir Optional session directory. If omitted, uses default (~/.spectral/agent/sessions/<encoded-cwd>/).
1006
1006
  */
1007
1007
  static create(cwd, sessionDir) {
1008
1008
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
@@ -1027,7 +1027,7 @@ export class SessionManager {
1027
1027
  /**
1028
1028
  * Continue the most recent session, or create new if none.
1029
1029
  * @param cwd Working directory
1030
- * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
1030
+ * @param sessionDir Optional session directory. If omitted, uses default (~/.spectral/agent/sessions/<encoded-cwd>/).
1031
1031
  */
1032
1032
  static continueRecent(cwd, sessionDir) {
1033
1033
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
@@ -1089,7 +1089,7 @@ export class SessionManager {
1089
1089
  /**
1090
1090
  * List all sessions for a directory.
1091
1091
  * @param cwd Working directory (used to compute default session directory)
1092
- * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
1092
+ * @param sessionDir Optional session directory. If omitted, uses default (~/.spectral/agent/sessions/<encoded-cwd>/).
1093
1093
  * @param onProgress Optional callback for progress updates (loaded, total)
1094
1094
  */
1095
1095
  static async list(cwd, sessionDir, onProgress) {
@@ -3,6 +3,6 @@ function isTruthyEnvFlag(value) {
3
3
  return false;
4
4
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
5
5
  }
6
- export function isInstallTelemetryEnabled(settingsManager, telemetryEnv = process.env.PI_TELEMETRY) {
6
+ export function isInstallTelemetryEnabled(settingsManager, telemetryEnv = process.env.SPECTRAL_TELEMETRY) {
7
7
  return telemetryEnv !== undefined ? isTruthyEnvFlag(telemetryEnv) : settingsManager.getEnableInstallTelemetry();
8
8
  }
@@ -64,10 +64,10 @@ export function migrateAuthToAuthJson() {
64
64
  return providers;
65
65
  }
66
66
  /**
67
- * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories.
67
+ * Migrate sessions from ~/.spectral/agent/*.jsonl to proper session directories.
68
68
  *
69
- * Bug in v0.30.0: Sessions were saved to ~/.pi/agent/ instead of
70
- * ~/.pi/agent/sessions/<encoded-cwd>/. This migration moves them
69
+ * Bug in v0.30.0: Sessions were saved to ~/.spectral/agent/ instead of
70
+ * ~/.spectral/agent/sessions/<encoded-cwd>/. This migration moves them
71
71
  * to the correct location based on the cwd in their session header.
72
72
  *
73
73
  * See: https://github.com/earendil-works/pi-mono/issues/320
@@ -10,7 +10,7 @@ const TOOLS_DIR = getBinDir();
10
10
  const NETWORK_TIMEOUT_MS = 10_000;
11
11
  const DOWNLOAD_TIMEOUT_MS = 120_000;
12
12
  function isOfflineModeEnabled() {
13
- const value = process.env.PI_OFFLINE;
13
+ const value = process.env.SPECTRAL_OFFLINE;
14
14
  if (!value)
15
15
  return false;
16
16
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
@@ -1,5 +1,5 @@
1
1
  import { getPiUserAgent } from "./pi-user-agent.js";
2
- const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
2
+ const LATEST_VERSION_URL = "https://spectral.dev/api/latest-version";
3
3
  const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
4
4
  function parsePackageVersion(version) {
5
5
  const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
@@ -41,7 +41,7 @@ export function isNewerPackageVersion(candidateVersion, currentVersion) {
41
41
  return candidateVersion.trim() !== currentVersion.trim();
42
42
  }
43
43
  export async function getLatestPiRelease(currentVersion, options = {}) {
44
- if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
44
+ if (process.env.SPECTRAL_SKIP_VERSION_CHECK || process.env.SPECTRAL_OFFLINE)
45
45
  return undefined;
46
46
  const response = await fetch(LATEST_VERSION_URL, {
47
47
  headers: {
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
3
3
  import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path";
4
4
  import { getCwdRelativePath } from "./paths.js";
5
- const QUARANTINE_DIR_NAME = ".pi-native-quarantine";
5
+ const QUARANTINE_DIR_NAME = ".spectral-native-quarantine";
6
6
  function normalizePath(path) {
7
7
  return toNamespacedPath(resolve(path));
8
8
  }