@aexol/spectral 0.7.7 → 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 (221) hide show
  1. package/dist/agent/agents.js +4 -4
  2. package/dist/agent/index.js +24 -148
  3. package/dist/cli.js +25 -220
  4. package/dist/commands/serve.js +1 -1
  5. package/dist/extensions/spectral-vision-fallback.js +225 -0
  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 +6 -6
  11. package/dist/memory/commands/view.js +16 -14
  12. package/dist/memory/compaction.js +33 -5
  13. package/dist/memory/config.js +3 -3
  14. package/dist/memory/debug-log.js +1 -1
  15. package/dist/memory/observer.js +2 -2
  16. package/dist/memory/prompts.js +5 -5
  17. package/dist/memory/tokens.js +1 -1
  18. package/dist/memory/tools/read-project-observations.js +2 -2
  19. package/dist/memory/tools/recall-observation.js +4 -4
  20. package/dist/relay/auto-research.js +23 -23
  21. package/dist/relay/dispatcher.js +28 -2
  22. package/dist/relay/models-fetch.js +15 -3
  23. package/dist/{pi → sdk}/coding-agent/cli/args.js +4 -4
  24. package/dist/{pi → sdk}/coding-agent/config.js +9 -20
  25. package/dist/{pi → sdk}/coding-agent/core/agent-session.js +5 -17
  26. package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
  27. package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +0 -6
  28. package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +7 -1
  29. package/dist/{pi → sdk}/coding-agent/core/keybindings.js +129 -2
  30. package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
  31. package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
  32. package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
  33. package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
  34. package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +20 -0
  35. package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
  36. package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +17 -63
  37. package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +4 -141
  38. package/dist/{pi → sdk}/coding-agent/core/tools/find.js +0 -11
  39. package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +0 -11
  40. package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +0 -11
  41. package/dist/{pi → sdk}/coding-agent/core/tools/read.js +0 -12
  42. package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +1 -14
  43. package/dist/{pi → sdk}/coding-agent/core/tools/write.js +2 -97
  44. package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
  45. package/dist/{pi → sdk}/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
  46. package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +26 -0
  47. package/dist/{pi → sdk}/coding-agent/modes/interactive/theme/theme.js +1 -2
  48. package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
  49. package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
  50. package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
  51. package/dist/server/{pi-bridge.js → agent-bridge.js} +158 -89
  52. package/dist/server/handlers/sessions.js +21 -0
  53. package/dist/server/session-stream.js +12 -6
  54. package/package.json +6 -3
  55. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
  56. package/dist/pi/coding-agent/core/export-html/index.js +0 -225
  57. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
  58. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +0 -32
  59. package/dist/pi/tui/autocomplete.js +0 -631
  60. package/dist/pi/tui/components/box.js +0 -103
  61. package/dist/pi/tui/components/cancellable-loader.js +0 -34
  62. package/dist/pi/tui/components/editor.js +0 -1915
  63. package/dist/pi/tui/components/image.js +0 -88
  64. package/dist/pi/tui/components/input.js +0 -425
  65. package/dist/pi/tui/components/loader.js +0 -68
  66. package/dist/pi/tui/components/markdown.js +0 -633
  67. package/dist/pi/tui/components/select-list.js +0 -158
  68. package/dist/pi/tui/components/settings-list.js +0 -184
  69. package/dist/pi/tui/components/spacer.js +0 -22
  70. package/dist/pi/tui/components/text.js +0 -88
  71. package/dist/pi/tui/components/truncated-text.js +0 -50
  72. package/dist/pi/tui/editor-component.js +0 -1
  73. package/dist/pi/tui/fuzzy.js +0 -109
  74. package/dist/pi/tui/index.js +0 -31
  75. package/dist/pi/tui/keybindings.js +0 -173
  76. package/dist/pi/tui/keys.js +0 -1172
  77. package/dist/pi/tui/kill-ring.js +0 -43
  78. package/dist/pi/tui/stdin-buffer.js +0 -360
  79. package/dist/pi/tui/terminal-image.js +0 -335
  80. package/dist/pi/tui/terminal.js +0 -324
  81. package/dist/pi/tui/tui.js +0 -1076
  82. package/dist/pi/tui/undo-stack.js +0 -24
  83. package/dist/pi/tui/utils.js +0 -1016
  84. /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
  85. /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
  86. /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
  87. /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
  88. /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
  89. /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
  90. /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
  91. /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
  92. /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
  93. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
  94. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
  95. /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
  96. /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
  97. /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
  98. /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
  99. /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
  100. /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
  101. /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
  102. /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
  103. /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
  104. /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
  105. /package/dist/{pi → sdk}/agent-core/index.js +0 -0
  106. /package/dist/{pi → sdk}/agent-core/node.js +0 -0
  107. /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
  108. /package/dist/{pi → sdk}/agent-core/types.js +0 -0
  109. /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
  110. /package/dist/{pi → sdk}/ai/cli.js +0 -0
  111. /package/dist/{pi → sdk}/ai/env-api-keys.js +0 -0
  112. /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
  113. /package/dist/{pi → sdk}/ai/image-models.js +0 -0
  114. /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
  115. /package/dist/{pi → sdk}/ai/images.js +0 -0
  116. /package/dist/{pi → sdk}/ai/index.js +0 -0
  117. /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
  118. /package/dist/{pi → sdk}/ai/models.js +0 -0
  119. /package/dist/{pi → sdk}/ai/oauth.js +0 -0
  120. /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
  121. /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
  122. /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
  123. /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
  124. /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
  125. /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
  126. /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
  127. /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
  128. /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
  129. /package/dist/{pi → sdk}/ai/stream.js +0 -0
  130. /package/dist/{pi → sdk}/ai/types.js +0 -0
  131. /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
  132. /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
  133. /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
  134. /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
  135. /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
  136. /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
  137. /package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +0 -0
  138. /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
  139. /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
  140. /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
  141. /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
  142. /package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +0 -0
  143. /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
  144. /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
  145. /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
  146. /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
  147. /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
  148. /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
  149. /package/dist/{pi → sdk}/coding-agent/bun/cli.js +0 -0
  150. /package/dist/{pi → sdk}/coding-agent/bun/restore-sandbox-env.js +0 -0
  151. /package/dist/{pi → sdk}/coding-agent/cli/file-processor.js +0 -0
  152. /package/dist/{pi → sdk}/coding-agent/cli/initial-message.js +0 -0
  153. /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
  154. /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
  155. /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
  156. /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
  157. /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
  158. /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
  159. /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
  160. /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
  161. /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
  162. /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
  163. /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
  164. /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
  165. /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
  166. /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
  167. /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
  168. /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
  169. /package/dist/{pi → sdk}/coding-agent/core/footer-data-provider.js +0 -0
  170. /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
  171. /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
  172. /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
  173. /package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +0 -0
  174. /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
  175. /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
  176. /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
  177. /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
  178. /package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +0 -0
  179. /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
  180. /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
  181. /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
  182. /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
  183. /package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +0 -0
  184. /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
  185. /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
  186. /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
  187. /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
  188. /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
  189. /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
  190. /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
  191. /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
  192. /package/dist/{pi → sdk}/coding-agent/index.js +0 -0
  193. /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
  194. /package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -0
  195. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/diff.js +0 -0
  196. /package/dist/{pi → sdk}/coding-agent/modes/interactive/interactive-mode.js +0 -0
  197. /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
  198. /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
  199. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
  200. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +0 -0
  201. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
  202. /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
  203. /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
  204. /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
  205. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
  206. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
  207. /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
  208. /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
  209. /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
  210. /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
  211. /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
  212. /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
  213. /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
  214. /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
  215. /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
  216. /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
  217. /package/dist/{pi → sdk}/coding-agent/utils/photon.js +0 -0
  218. /package/dist/{pi → sdk}/coding-agent/utils/pi-user-agent.js +0 -0
  219. /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
  220. /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
  221. /package/dist/{pi → sdk}/coding-agent/utils/syntax-highlight.js +0 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-research handler — sends an auto-research task through the existing
3
- * PiBridge (backend proxy) instead of spawning a separate pi process.
3
+ * AgentBridge (backend proxy) instead of spawning a separate pi process.
4
4
  *
5
5
  * This ensures auto-research uses the same model and API keys as the active
6
6
  * session — no separate subprocess, no missing API key errors.
@@ -47,12 +47,12 @@ function makeRelaySubscriber(sessionId, relay) {
47
47
  };
48
48
  }
49
49
  /**
50
- * Scan the project's .pi/extensions/auto-research/ directory for generated
50
+ * Scan the project's .spectral/extensions/auto-research/ directory for generated
51
51
  * extension directories. Each subdirectory with .ts files counts as one
52
52
  * extension.
53
53
  */
54
54
  function scanGeneratedExtensions(projectPath) {
55
- const arDir = path.join(projectPath, ".pi", "extensions", "auto-research");
55
+ const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
56
56
  const extensions = [];
57
57
  try {
58
58
  if (!fs.existsSync(arDir))
@@ -105,7 +105,7 @@ function scanGeneratedExtensions(projectPath) {
105
105
  }
106
106
  extensions.push({
107
107
  name: entry.name,
108
- path: `.pi/extensions/auto-research/${entry.name}`,
108
+ path: `.spectral/extensions/auto-research/${entry.name}`,
109
109
  description,
110
110
  usesLLM,
111
111
  fileCount,
@@ -137,7 +137,7 @@ function hasAgentsMdUpdate(projectPath) {
137
137
  */
138
138
  function readManifest(projectPath) {
139
139
  try {
140
- const mPath = path.join(projectPath, ".pi", "extensions", "auto-research", "manifest.json");
140
+ const mPath = path.join(projectPath, ".spectral", "extensions", "auto-research", "manifest.json");
141
141
  if (!fs.existsSync(mPath))
142
142
  return null;
143
143
  const raw = fs.readFileSync(mPath, "utf-8");
@@ -164,7 +164,7 @@ function gatherPreRunContext(projectPath) {
164
164
  const isIncremental = manifest !== null;
165
165
  const existingExtensions = [];
166
166
  try {
167
- const arDir = path.join(projectPath, ".pi", "extensions", "auto-research");
167
+ const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
168
168
  if (fs.existsSync(arDir)) {
169
169
  for (const entry of fs.readdirSync(arDir, { withFileTypes: true })) {
170
170
  if (entry.isDirectory() && entry.name !== "node_modules") {
@@ -209,7 +209,7 @@ function buildIncrementalSection(ctx) {
209
209
  if (ctx.existingExtensions.length > 0) {
210
210
  lines.push("### Existing extensions to review:");
211
211
  for (const name of ctx.existingExtensions) {
212
- lines.push(` - \`.pi/extensions/auto-research/${name}/\``);
212
+ lines.push(` - \`.spectral/extensions/auto-research/${name}/\``);
213
213
  }
214
214
  lines.push("");
215
215
  }
@@ -224,7 +224,7 @@ function buildIncrementalSection(ctx) {
224
224
  lines.push(" was removed from the project, delete the extension directory entirely.", "");
225
225
  lines.push("4. **Update AGENTS.md** — The AUTO-RESEARCH section should reflect the CURRENT");
226
226
  lines.push(" set of extensions (remove stale entries, add new ones).", "");
227
- lines.push("5. **Save manifest.json** — Write/update .pi/extensions/auto-research/manifest.json");
227
+ lines.push("5. **Save manifest.json** — Write/update .spectral/extensions/auto-research/manifest.json");
228
228
  lines.push(` with: lastRun (ISO), lastCommit (git HEAD), runCount (${ctx.manifest ? ctx.manifest.runCount + 1 : 1}),`);
229
229
  lines.push(" and extensions array with name/path/category for each generated extension.", "");
230
230
  return lines.join("\n");
@@ -234,7 +234,7 @@ function buildIncrementalSection(ctx) {
234
234
  */
235
235
  function writeManifest(projectPath, extensions) {
236
236
  try {
237
- const arDir = path.join(projectPath, ".pi", "extensions", "auto-research");
237
+ const arDir = path.join(projectPath, ".spectral", "extensions", "auto-research");
238
238
  if (!fs.existsSync(arDir))
239
239
  fs.mkdirSync(arDir, { recursive: true });
240
240
  let currentCommit = "unknown";
@@ -257,7 +257,7 @@ function writeManifest(projectPath, extensions) {
257
257
  }
258
258
  /**
259
259
  * Build the auto-research task prompt. This is sent as a user message
260
- * through the existing PiBridge, so the agent uses the session's model
260
+ * through the existing AgentBridge, so the agent uses the session's model
261
261
  * and backend proxy.
262
262
  *
263
263
  * @param projectPath Absolute path to the project root
@@ -297,8 +297,8 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
297
297
  "}",
298
298
  "```",
299
299
  "",
300
- "Extensions live in: `.pi/extensions/` (project-local) or `~/.pi/agent/extensions/` (user-global).",
301
- "Auto-research generates extensions into `.pi/extensions/auto-research/<name>/`.",
300
+ "Extensions live in: `.spectral/extensions/` (project-local) or `~/.spectral/agent/extensions/` (user-global).",
301
+ "Auto-research generates extensions into `.spectral/extensions/auto-research/<name>/`.",
302
302
  "",
303
303
  "### Tools (pi.registerTool)",
304
304
  "",
@@ -400,10 +400,10 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
400
400
  "",
401
401
  "### Extension File Structure",
402
402
  "",
403
- "Each extension is a directory under `.pi/extensions/auto-research/`:",
403
+ "Each extension is a directory under `.spectral/extensions/auto-research/`:",
404
404
  "",
405
405
  "```",
406
- ".pi/extensions/auto-research/",
406
+ ".spectral/extensions/auto-research/",
407
407
  " <extension-name>/",
408
408
  " index.ts # Entry point — default export activate(pi)",
409
409
  " utils.ts # [optional] Helper functions",
@@ -440,7 +440,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
440
440
  "",
441
441
  "1. **Context Collection** — Explore the project structure:",
442
442
  " - Read package.json, tsconfig.json, deno.json (if present)",
443
- " - Check existing extensions under .pi/extensions/",
443
+ " - Check existing extensions under .spectral/extensions/",
444
444
  " - Review key source files to understand architecture",
445
445
  " - Check git log for recent changes and patterns",
446
446
  "",
@@ -454,9 +454,9 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
454
454
  " - Use `pi.registerTool()` for simple tools with TypeBox validation",
455
455
  " - Use `pi.registerCommand()` for custom slash commands",
456
456
  " - For LLM-powered extensions, use `ctx.modelRegistry` to call models",
457
- " - Create files under `.pi/extensions/auto-research/<name>/`",
457
+ " - Create files under `.spectral/extensions/auto-research/<name>/`",
458
458
  " - Each extension needs an `index.ts` that registers its tools/commands",
459
- " - Read `.pi/agents/auto-research-templates.md` for proven extension templates",
459
+ " - Read `.spectral/agents/auto-research-templates.md` for proven extension templates",
460
460
  "",
461
461
  "4. **Validation** — Verify generated extensions:",
462
462
  " - Ensure all imports resolve to available packages (see reference above)",
@@ -478,7 +478,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
478
478
  "",
479
479
  "### Important Rules",
480
480
  "",
481
- "- Write each extension as TypeScript files under `.pi/extensions/auto-research/<name>/`",
481
+ "- Write each extension as TypeScript files under `.spectral/extensions/auto-research/<name>/`",
482
482
  "- Every extension directory MUST have an `index.ts` entry point",
483
483
  "- Use proper TypeScript with type annotations and error handling",
484
484
  "- Extensions must handle errors gracefully (never crash the agent)",
@@ -491,7 +491,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
491
491
  "agent sessions automatically know about the new capabilities.",
492
492
  "",
493
493
  "**How AGENTS.md works:**",
494
- "- Pi loads AGENTS.md at startup from the project root and parent directories",
494
+ "- Spectral loads AGENTS.md at startup from the project root and parent directories",
495
495
  "- All found AGENTS.md files are concatenated and injected into the system prompt",
496
496
  "- This means documented extensions get discovered by the agent automatically",
497
497
  "",
@@ -502,7 +502,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
502
502
  "## Auto-Generated Extensions",
503
503
  "",
504
504
  "These extensions were generated by auto-research. They are available",
505
- "in every session. Pi loads them automatically from `.pi/extensions/`.",
505
+ "in every session. Spectral loads them automatically from `.spectral/extensions/`.",
506
506
  "",
507
507
  "### `<extension-name>`",
508
508
  "",
@@ -539,7 +539,7 @@ function buildAutoResearchTask(projectPath, projectName, preRunContext = null) {
539
539
  // ---------------------------------------------------------------------------
540
540
  const AR_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes — generous for LLM-based analysis
541
541
  /**
542
- * Execute auto-research for a project through the existing PiBridge.
542
+ * Execute auto-research for a project through the existing AgentBridge.
543
543
  *
544
544
  * Caller (dispatcher) is fire-and-forget — this function is `void` and
545
545
  * all errors are surfaced as `auto_research_error` events on the wire
@@ -547,7 +547,7 @@ const AR_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes — generous for LLM-based a
547
547
  *
548
548
  * This replaces the old subprocess-spawning approach. Instead of launching
549
549
  * a separate pi process (which lacks backend proxy credentials), we send
550
- * the auto-research task through the session's existing PiBridge. The agent
550
+ * the auto-research task through the session's existing AgentBridge. The agent
551
551
  * uses the session's model, backend proxy, and all available tools.
552
552
  */
553
553
  export function handleAutoResearch(input, deps) {
@@ -700,7 +700,7 @@ export function handleAutoResearch(input, deps) {
700
700
  // then timeout fires and calls finalize() again — but finalize() is
701
701
  // gated by watcherFired, so it's a no-op. The only issue is we don't
702
702
  // clearTimeout on watcher fire — but that's fine, the timer is harmless.
703
- // --- Send the prompt through the existing PiBridge (backend proxy) ---
703
+ // --- Send the prompt through the existing AgentBridge (backend proxy) ---
704
704
  manager.prompt(sessionId, taskContent, storedModelId).catch((err) => {
705
705
  if (watcherFired)
706
706
  return; // already handled by watcher
@@ -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
@@ -28,11 +28,11 @@ const cache = new Map();
28
28
  export function clearAllowedModelsCache() {
29
29
  cache.clear();
30
30
  }
31
- const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages } }`;
31
+ const QUERY = `query AvailableBaseModels { availableBaseModels { name provider userModelId agentEnabled creditInputPer1M creditOutputPer1M creditCachedInputPer1M creditCacheReadPer1M creditCacheWritePer1M contextWindow supportsImages supportsReasoning isDefault isVisionDefault } }`;
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) {
@@ -104,6 +104,15 @@ export async function fetchAllowedModels(opts) {
104
104
  const supportsImages = typeof row?.supportsImages === "boolean"
105
105
  ? row.supportsImages
106
106
  : null;
107
+ const supportsReasoning = typeof row?.supportsReasoning === "boolean"
108
+ ? row.supportsReasoning
109
+ : null;
110
+ const isDefault = typeof row?.isDefault === "boolean"
111
+ ? row.isDefault
112
+ : null;
113
+ const isVisionDefault = typeof row?.isVisionDefault === "boolean"
114
+ ? row.isVisionDefault
115
+ : null;
107
116
  const model = {
108
117
  modelId: name,
109
118
  displayName: name,
@@ -115,6 +124,9 @@ export async function fetchAllowedModels(opts) {
115
124
  creditCacheWritePer1M: asOptionalNumber(row?.creditCacheWritePer1M),
116
125
  contextWindow,
117
126
  supportsImages,
127
+ supportsReasoning,
128
+ isDefault,
129
+ isVisionDefault,
118
130
  };
119
131
  if (typeof row?.userModelId === "string") {
120
132
  model.userModelId = row.userModelId;
@@ -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
@@ -297,17 +297,6 @@ export function getThemesDir() {
297
297
  /**
298
298
  * Get path to HTML export template directory (shipped with package)
299
299
  * - For Bun binary: export-html/ next to executable
300
- * - For Node.js (dist/): dist/core/export-html/
301
- * - For tsx (src/): src/core/export-html/
302
- */
303
- export function getExportTemplateDir() {
304
- if (isBunBinary) {
305
- return join(getPackageDir(), "export-html");
306
- }
307
- const packageDir = getPackageDir();
308
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
309
- return join(packageDir, srcOrDist, "core", "export-html");
310
- }
311
300
  /** Get path to package.json */
312
301
  export function getPackageJsonPath() {
313
302
  return join(getPackageDir(), "package.json");
@@ -347,28 +336,28 @@ export function getBundledInteractiveAssetPath(name) {
347
336
  return join(getInteractiveAssetsDir(), name);
348
337
  }
349
338
  const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
350
- const piConfigName = pkg.piConfig?.name;
339
+ const spectralConfigName = pkg.spectralConfig?.name;
351
340
  export const PACKAGE_NAME = pkg.name || "index.ts";
352
- export const APP_NAME = piConfigName || "pi";
353
- export const APP_TITLE = piConfigName ? APP_NAME : "π";
354
- 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";
355
344
  export const VERSION = pkg.version || "0.0.0";
356
- // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
345
+ // e.g., SPECTRAL_CODING_AGENT_DIR
357
346
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
358
347
  export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
359
348
  export function expandTildePath(path) {
360
349
  return normalizePath(path);
361
350
  }
362
- const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
351
+ const DEFAULT_SHARE_VIEWER_URL = "https://spectral.dev/session/";
363
352
  /** Get the share viewer URL for a gist ID */
364
353
  export function getShareViewerUrl(gistId) {
365
- 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;
366
355
  return `${baseUrl}#${gistId}`;
367
356
  }
368
357
  // =============================================================================
369
- // User Config Paths (~/.pi/agent/*)
358
+ // User Config Paths (~/.spectral/agent/*)
370
359
  // =============================================================================
371
- /** Get the agent config directory (e.g., ~/.pi/agent/) */
360
+ /** Get the agent config directory (e.g., ~/.spectral/agent/) */
372
361
  export function getAgentDir() {
373
362
  const envDir = process.env[ENV_AGENT_DIR];
374
363
  if (envDir) {
@@ -15,7 +15,6 @@
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
17
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "../../ai/index.js";
18
- import { theme } from "../modes/interactive/theme/theme.js";
19
18
  import { stripFrontmatter } from "../utils/frontmatter.js";
20
19
  import { resolvePath } from "../utils/paths.js";
21
20
  import { sleep } from "../utils/sleep.js";
@@ -23,8 +22,6 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
23
22
  import { executeBashWithOperations } from "./bash-executor.js";
24
23
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
25
24
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
26
- import { exportSessionToHtml } from "./export-html/index.js";
27
- import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
28
25
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
29
26
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
30
27
  import { expandPromptTemplate } from "./prompt-templates.js";
@@ -468,6 +465,8 @@ export class AgentSession {
468
465
  */
469
466
  dispose() {
470
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();
471
470
  this._disconnectFromAgent();
472
471
  this._eventListeners = [];
473
472
  cleanupSessionResources(this.sessionId);
@@ -1889,7 +1888,7 @@ export class AgentSession {
1889
1888
  extensionsResult.runtime.flagValues.set(name, value);
1890
1889
  }
1891
1890
  }
1892
- this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry);
1891
+ this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry, this.settingsManager);
1893
1892
  if (this._extensionRunnerRef) {
1894
1893
  this._extensionRunnerRef.current = this._extensionRunner;
1895
1894
  }
@@ -2398,19 +2397,8 @@ export class AgentSession {
2398
2397
  * @param outputPath Optional output path (defaults to session directory)
2399
2398
  * @returns Path to exported file
2400
2399
  */
2401
- async exportToHtml(outputPath) {
2402
- const themeName = this.settingsManager.getTheme();
2403
- // Create tool renderer if we have an extension runner (for custom tool HTML rendering)
2404
- const toolRenderer = createToolHtmlRenderer({
2405
- getToolDefinition: (name) => this.getToolDefinition(name),
2406
- theme,
2407
- cwd: this.sessionManager.getCwd(),
2408
- });
2409
- return await exportSessionToHtml(this.sessionManager, this.state, {
2410
- outputPath,
2411
- themeName,
2412
- toolRenderer,
2413
- });
2400
+ async exportToHtml(_outputPath) {
2401
+ throw new Error("HTML export has been removed. Use spectral serve instead.");
2414
2402
  }
2415
2403
  /**
2416
2404
  * Export the current session branch to a JSONL file.
@@ -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);
@@ -9,7 +9,6 @@ import { fileURLToPath } from "node:url";
9
9
  import * as _bundledPiAgentCore from "../../../agent-core/index.js";
10
10
  import * as _bundledPiAi from "../../../ai/index.js";
11
11
  import * as _bundledPiAiOauth from "../../../ai/oauth.js";
12
- import * as _bundledPiTui from "../../../tui/index.js";
13
12
  import { createJiti } from "@mariozechner/jiti";
14
13
  // Static imports of packages that extensions may use.
15
14
  // These MUST be static so Bun bundles them into the compiled binary.
@@ -34,12 +33,10 @@ const VIRTUAL_MODULES = {
34
33
  "@sinclair/typebox/compile": _bundledTypeboxCompile,
35
34
  "@sinclair/typebox/value": _bundledTypeboxValue,
36
35
  "../../../agent-core/index.ts": _bundledPiAgentCore,
37
- "../../../tui/index.ts": _bundledPiTui,
38
36
  "../../../ai/index.ts": _bundledPiAi,
39
37
  "../../../ai/oauth.ts": _bundledPiAiOauth,
40
38
  "../../index.ts": _bundledPiCodingAgent,
41
39
  "@mariozechner/pi-agent": _bundledPiAgentCore,
42
- "@mariozechner/pi-tui": _bundledPiTui,
43
40
  "@mariozechner/pi-ai": _bundledPiAi,
44
41
  "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
45
42
  "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
@@ -68,18 +65,15 @@ function getAliases() {
68
65
  };
69
66
  const piCodingAgentEntry = packageIndex;
70
67
  const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "../../../agent-core/index.ts");
71
- const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "../../../tui/index.ts");
72
68
  const piAiEntry = resolveWorkspaceOrImport("ai/dist/index.js", "../../../ai/index.ts");
73
69
  const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "../../../ai/oauth.ts");
74
70
  _aliases = {
75
71
  "../../index.ts": piCodingAgentEntry,
76
72
  "../../../agent-core/index.ts": piAgentCoreEntry,
77
- "../../../tui/index.ts": piTuiEntry,
78
73
  "../../../ai/index.ts": piAiEntry,
79
74
  "../../../ai/oauth.ts": piAiOauthEntry,
80
75
  "@mariozechner/pi-coding-agent": piCodingAgentEntry,
81
76
  "@mariozechner/pi-agent": piAgentCoreEntry,
82
- "@mariozechner/pi-tui": piTuiEntry,
83
77
  "@mariozechner/pi-ai": piAiEntry,
84
78
  "@mariozechner/pi-ai/oauth": piAiOauthEntry,
85
79
  typebox: typeboxEntry,