@aexol/spectral 0.7.1 → 0.7.3

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 (217) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/server/pi-bridge.js +1 -1
  214. package/dist/server/session-stream.js +8 -111
  215. package/dist/server/storage.js +62 -1
  216. package/dist/server/title-generator.js +14 -153
  217. package/package.json +24 -6
@@ -49,7 +49,7 @@
49
49
  * instance is reused across `prompt()` calls).
50
50
  */
51
51
  import { createJiti } from "@mariozechner/jiti";
52
- import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
52
+ import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, } from "../pi/coding-agent/index.js";
53
53
  import { randomUUID } from "node:crypto";
54
54
  import { existsSync, statSync } from "node:fs";
55
55
  import { dirname, join, resolve } from "node:path";
@@ -41,9 +41,9 @@ import { PiBridge } from "./pi-bridge.js";
41
41
  import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
42
42
  import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
43
43
  import { loadConfig } from "../memory/config.js";
44
+ import { setProjectObsStore } from "../memory/project-observations-store.js";
44
45
  import { estimateStringTokens } from "../memory/tokens.js";
45
46
  import { reflectionContent, reflectionId } from "../memory/types.js";
46
- import { generateSessionTitle, isDefaultTitle, } from "./title-generator.js";
47
47
  const DEFAULT_BRIDGE_FACTORY = (args) => new PiBridge(args);
48
48
  /** Safety limit for autonomous loop iterations per session. */
49
49
  const MAX_LOOP_ITERATIONS = 100;
@@ -233,17 +233,7 @@ export class SessionStreamManager {
233
233
  machineJwt;
234
234
  bridgeFactory;
235
235
  agentDir;
236
- titleLlmCall;
237
- disableAutoTitle;
238
- publishMetaEvent;
239
236
  streams = new Map();
240
- /**
241
- * Sessions for which we've already attempted (or queued) auto-title
242
- * generation in this server process. Per-process is intentional: a server
243
- * restart resets the set, but `isDefaultTitle()` still gates the work so a
244
- * since-renamed session is never overwritten.
245
- */
246
- titleGenerationAttempted = new Set();
247
237
  disposed = false;
248
238
  constructor(opts) {
249
239
  this.store = opts.store;
@@ -252,9 +242,13 @@ export class SessionStreamManager {
252
242
  this.machineJwt = opts.machineJwt;
253
243
  this.bridgeFactory = opts.bridgeFactory ?? DEFAULT_BRIDGE_FACTORY;
254
244
  this.agentDir = opts.agentDir;
255
- this.titleLlmCall = opts.titleLlmCall;
256
- this.disableAutoTitle = opts.disableAutoTitle === true;
257
- this.publishMetaEvent = opts.publishMetaEvent;
245
+ // Wire the project observations store singleton so the memory extension's
246
+ // read_project_observations tool can query cross-session observations.
247
+ setProjectObsStore({
248
+ insertProjectObservations: (pid, sid, obs, ts) => this.store.insertProjectObservations(pid, sid, obs, ts),
249
+ searchProjectObservations: (pid, q, limit) => this.store.searchProjectObservations(pid, q, limit),
250
+ getProjectByCwd: (cwd) => this.store.getProjectByCwd(cwd),
251
+ });
258
252
  }
259
253
  /**
260
254
  * Attach a subscriber to a session. Lazily creates the underlying pi
@@ -1192,11 +1186,6 @@ export class SessionStreamManager {
1192
1186
  stream.lastFlushedEventCount = 0;
1193
1187
  const finishedTurn = stream.currentTurn;
1194
1188
  stream.currentTurn = null;
1195
- // Fire-and-forget auto-title generation. Runs only once per session
1196
- // per server lifetime, only when the session is still wearing its
1197
- // default title, and never blocks the user's stream (the user's
1198
- // turn is already complete by the time this runs).
1199
- this.maybeGenerateTitle(stream, finishedTurn);
1200
1189
  // Autonomous iterative loop (Ralph Wiggum pattern).
1201
1190
  // When loopActive is set, check for completion marker, then re-send
1202
1191
  // the ORIGINAL prompt so the agent sees its prior changes and
@@ -1300,98 +1289,6 @@ export class SessionStreamManager {
1300
1289
  console.error(`[spectral] error: batch-persist flush failed: ${err instanceof Error ? err.message : String(err)}`);
1301
1290
  }
1302
1291
  }
1303
- /**
1304
- * Auto-title the session if it's still wearing the default title and we
1305
- * haven't already attempted generation in this process. Fire-and-forget
1306
- * (errors are caught, logged, and swallowed) — the user's stream finished
1307
- * before this runs, so blocking would only delay the broadcast.
1308
- */
1309
- maybeGenerateTitle(stream, finishedTurn) {
1310
- if (this.disableAutoTitle)
1311
- return;
1312
- if (this.titleGenerationAttempted.has(stream.sessionId))
1313
- return;
1314
- // Check the persisted title now (manual rename takes precedence).
1315
- const detail = this.store.getSession(stream.sessionId);
1316
- if (!detail || !isDefaultTitle(detail.title))
1317
- return;
1318
- // Find the first user message + the assistant text from the just-finished
1319
- // turn. We deliberately read user content from SQLite (authoritative)
1320
- // and assistant content from the in-memory turn buffer (cheap, and
1321
- // matches what the user just saw).
1322
- const firstUser = detail.messages.find((m) => m.role === "user");
1323
- if (!firstUser || !firstUser.content.trim())
1324
- return;
1325
- let assistantText = finishedTurn?.assistantText ?? "";
1326
- if (!assistantText) {
1327
- // Fallback: pull the most recent assistant message from SQLite. This
1328
- // can happen if `agent_end` fires for a turn whose buffer was cleared
1329
- // by an intervening error event, or when this code path is reached
1330
- // via a synthetic test event.
1331
- const lastAssistant = [...detail.messages]
1332
- .reverse()
1333
- .find((m) => m.role === "assistant");
1334
- assistantText = lastAssistant?.content ?? "";
1335
- }
1336
- // Mark BEFORE awaiting so a second `agent_end` arriving while we're
1337
- // generating doesn't double-fire. Even if generation throws, we leave
1338
- // the entry in place — one-shot semantics, no retries.
1339
- this.titleGenerationAttempted.add(stream.sessionId);
1340
- void this.runTitleGeneration(stream, firstUser.content, assistantText);
1341
- }
1342
- async runTitleGeneration(stream, firstUserMessage, firstAssistantMessage) {
1343
- try {
1344
- const title = await generateSessionTitle(firstUserMessage, firstAssistantMessage, {
1345
- cwd: stream.cwd,
1346
- agentDir: this.agentDir,
1347
- llmCall: this.titleLlmCall,
1348
- });
1349
- if (this.disposed)
1350
- return;
1351
- if (!title)
1352
- return;
1353
- // Re-check the title hasn't been changed underneath us while we were
1354
- // waiting on the LLM (e.g. user manually renamed mid-generation).
1355
- const current = this.store.getSession(stream.sessionId);
1356
- if (!current || !isDefaultTitle(current.title))
1357
- return;
1358
- const updated = this.store.renameSession(stream.sessionId, title);
1359
- if (!updated)
1360
- return;
1361
- // Broadcast to every subscriber of this session so all open tabs
1362
- // update their sidebar in real time. The wire event is independent
1363
- // of the in-flight turn lifecycle, so it's safe to fire post-agent_end.
1364
- this.broadcast(stream, {
1365
- type: "session_renamed",
1366
- sessionId: stream.sessionId,
1367
- title: updated.title,
1368
- });
1369
- // Cross-tab fan-out hint: tabs that don't have THIS session open
1370
- // (and so don't have a per-session ws subscription) still want to
1371
- // refresh their sidebar to show the new title. Best-effort; a
1372
- // failed publish never undoes the rename.
1373
- if (this.publishMetaEvent) {
1374
- try {
1375
- this.publishMetaEvent({
1376
- type: "session_renamed",
1377
- projectId: updated.projectId,
1378
- sessionId: stream.sessionId,
1379
- });
1380
- }
1381
- catch (err) {
1382
- const msg = err instanceof Error ? err.message : String(err);
1383
- console.warn(`[spectral] warn: meta publish for auto-title failed: ${msg}`);
1384
- }
1385
- }
1386
- }
1387
- catch (err) {
1388
- // Defensive: generateSessionTitle already swallows LLM errors. This
1389
- // catches any unexpected throw from rename/broadcast so the manager
1390
- // is never destabilized by a background title task.
1391
- const msg = err instanceof Error ? err.message : String(err);
1392
- console.warn(`[spectral] warn: auto-title pipeline failed: ${msg}`);
1393
- }
1394
- }
1395
1292
  broadcast(stream, event) {
1396
1293
  const dead = [];
1397
1294
  for (const sub of stream.subscribers) {
@@ -78,6 +78,17 @@ CREATE TABLE IF NOT EXISTS session_memory_snapshots (
78
78
  covered_source_count INTEGER NOT NULL,
79
79
  updated_at INTEGER NOT NULL
80
80
  );
81
+
82
+ CREATE TABLE IF NOT EXISTS project_observations (
83
+ id TEXT PRIMARY KEY,
84
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
85
+ session_id TEXT NOT NULL,
86
+ content TEXT NOT NULL,
87
+ relevance TEXT NOT NULL CHECK (relevance IN ('low','medium','high','critical')),
88
+ created_at INTEGER NOT NULL
89
+ );
90
+
91
+ CREATE INDEX IF NOT EXISTS idx_project_obs_project ON project_observations(project_id, created_at DESC);
81
92
  `;
82
93
  /**
83
94
  * Synchronous binding-file reader for a project at the given filesystem path.
@@ -108,7 +119,7 @@ function applyBindingFields(project) {
108
119
  };
109
120
  }
110
121
  /** Tables we own — used by the migration drop step. */
111
- const KNOWN_TABLES = ["session_memory_snapshots", "messages", "sessions", "projects"];
122
+ const KNOWN_TABLES = ["project_observations", "session_memory_snapshots", "messages", "sessions", "projects"];
112
123
  /** Best-effort parse of the `images_json` column into `ImageAttachment[]`. */
113
124
  function parseImagesJson(raw) {
114
125
  if (!raw || raw === "")
@@ -169,6 +180,10 @@ export class SessionStore {
169
180
  // Fork & Compact: flag + per-session source tracking.
170
181
  stmtSetForkCompactSource;
171
182
  stmtGetForkCompactSource;
183
+ // Project observations: cross-session durable memory.
184
+ stmtInsertProjectObs;
185
+ stmtSearchProjectObs;
186
+ stmtGetProjectByCwd;
172
187
  constructor(path) {
173
188
  this.path = path;
174
189
  // Make sure the parent directory exists. mkdirSync with recursive is a
@@ -307,6 +322,14 @@ export class SessionStore {
307
322
  this.stmtSetSessionModel = this.db.prepare(`UPDATE sessions SET model_id = ? WHERE id = ?`);
308
323
  this.stmtSetForkCompactSource = this.db.prepare(`UPDATE sessions SET fork_compact_source_id = ? WHERE id = ?`);
309
324
  this.stmtGetForkCompactSource = this.db.prepare(`SELECT fork_compact_source_id FROM sessions WHERE id = ?`);
325
+ this.stmtInsertProjectObs = this.db.prepare(`INSERT OR REPLACE INTO project_observations (id, project_id, session_id, content, relevance, created_at)
326
+ VALUES (?, ?, ?, ?, ?, ?)`);
327
+ this.stmtSearchProjectObs = this.db.prepare(`SELECT id, project_id, session_id, content, relevance, created_at
328
+ FROM project_observations
329
+ WHERE project_id = ? AND content LIKE ?
330
+ ORDER BY created_at DESC
331
+ LIMIT 20`);
332
+ this.stmtGetProjectByCwd = this.db.prepare(`SELECT id FROM projects WHERE path = ? LIMIT 1`);
310
333
  }
311
334
  /** Smoke check: returns the names of the tables in the DB. */
312
335
  listTables() {
@@ -653,6 +676,44 @@ export class SessionStore {
653
676
  clearForkCompactSource(sessionId) {
654
677
  this.stmtSetForkCompactSource.run(null, sessionId);
655
678
  }
679
+ // ----------------------------------------------------------------------
680
+ // Project observations (cross-session durable memory)
681
+ // ----------------------------------------------------------------------
682
+ /**
683
+ * Insert multiple project observations in a single transaction.
684
+ * Uses INSERT OR REPLACE so re-running after the same compaction is idempotent.
685
+ */
686
+ insertProjectObservations(projectId, sessionId, observations, createdAt) {
687
+ if (observations.length === 0)
688
+ return;
689
+ const tx = this.db.transaction(() => {
690
+ for (const obs of observations) {
691
+ this.stmtInsertProjectObs.run(obs.id, projectId, sessionId, obs.content, obs.relevance, createdAt);
692
+ }
693
+ });
694
+ tx();
695
+ }
696
+ /**
697
+ * Search project observations by substring match. Returns up to 20 most recent.
698
+ */
699
+ searchProjectObservations(projectId, query, limit = 20) {
700
+ const pattern = `%${query}%`;
701
+ const rows = this.stmtSearchProjectObs.all(projectId, pattern);
702
+ return rows.slice(0, limit).map((r) => ({
703
+ content: r.content,
704
+ relevance: r.relevance,
705
+ createdAt: r.created_at,
706
+ sessionId: r.session_id,
707
+ }));
708
+ }
709
+ /**
710
+ * Look up a project by its absolute filesystem path.
711
+ * Returns null when no project has been registered for the given path.
712
+ */
713
+ getProjectByCwd(cwd) {
714
+ const row = this.stmtGetProjectByCwd.get(cwd);
715
+ return row?.id ?? null;
716
+ }
656
717
  close() {
657
718
  if (this.closed)
658
719
  return;
@@ -1,34 +1,13 @@
1
1
  /**
2
- * Auto-titler for sessions whose title is still the default
3
- * ("New conversation" / null / empty).
2
+ * Session title utilities.
4
3
  *
5
- * Strategy:
6
- * - After the first assistant turn completes (agent_end), the session-stream
7
- * manager fires `generateAndPersistTitle` once per session per server
8
- * lifetime. We track attempted ids in `SessionStreamManager` so a second
9
- * message in the same session never retriggers.
10
- * - Title generation itself is a one-shot LLM call. To keep the prod path
11
- * decoupled from tests, the call is injectable via the `llmCall`
12
- * dependency. The default implementation spawns a one-off pi
13
- * `AgentSession` with `noTools: "all"`, sends the prompt, and accumulates
14
- * `text_delta` events until `agent_end`. This reuses every bit of pi's
15
- * auth + model selection without introducing a new SDK code path.
16
- * - All failures are swallowed: log a warning and leave the title alone.
17
- * The caller's stream is never affected — title gen runs fire-and-forget.
18
- *
19
- * Sanitization rules (applied after the LLM responds):
20
- * - Trim whitespace
21
- * - Strip surrounding quotes (`"…"`, `'…'`, `“…”`)
22
- * - Take only the first non-empty line
23
- * - Drop trailing punctuation (`.`, `,`, `!`, `?`, `;`, `:`)
24
- * - Truncate to 60 characters
25
- *
26
- * Edge cases:
27
- * - Empty assistant message (e.g. only tool calls) → fall back to using the
28
- * user message alone for the prompt; if that's also empty, skip.
29
- * - LLM returns empty string after sanitization → return null, skip rename.
4
+ * Pure helpers for title sanitization and default-title detection.
5
+ * Title generation now happens via the backend GraphQL mutation
6
+ * `generateSessionTitle`, called from the landing frontend after the
7
+ * first assistant turn completes. The CLI no longer has its own LLM
8
+ * pipeline for titles — the landing→backend→CLI REST PATCH flow
9
+ * handles both the rename and the cross-tab meta publish.
30
10
  */
31
- import { createAgentSession, SessionManager, } from "@mariozechner/pi-coding-agent";
32
11
  const DEFAULT_TITLES = new Set(["New conversation", "", "Untitled"]);
33
12
  export const SESSION_TITLE_DEFAULT = "New conversation";
34
13
  /** True if `title` looks like the auto-generated default and is fair game. */
@@ -56,8 +35,8 @@ export function sanitizeTitle(raw) {
56
35
  const quotePairs = [
57
36
  ['"', '"'],
58
37
  ["'", "'"],
59
- ["\u201C", "\u201D"], //
60
- ["\u2018", "\u2019"], //
38
+ ["\u201C", "\u201D"], // " "
39
+ ["\u2018", "\u2019"], // ' '
61
40
  ["`", "`"],
62
41
  ];
63
42
  for (const [open, close] of quotePairs) {
@@ -68,129 +47,11 @@ export function sanitizeTitle(raw) {
68
47
  }
69
48
  // Drop trailing punctuation.
70
49
  out = out.replace(/[.,!?;:]+$/u, "").trim();
71
- // Truncate.
72
- if (out.length > 60)
73
- out = out.slice(0, 60).trim();
74
- if (!out)
75
- return null;
76
- return out;
77
- }
78
- /**
79
- * Build the prompt sent to the LLM. Truncates the assistant message body so
80
- * a long answer doesn't blow out the context window of a tiny title model.
81
- */
82
- export function buildTitlePrompt(firstUserMessage, firstAssistantMessage) {
83
- const userText = firstUserMessage.trim();
84
- const assistantText = firstAssistantMessage.trim().slice(0, 500);
85
- const lines = [
86
- "Generate a short title (4-6 words, no quotes, no trailing punctuation) for this conversation.",
87
- "Respond with ONLY the title — no preamble, no explanation, no quotes.",
88
- "",
89
- `User: ${userText}`,
90
- ];
91
- if (assistantText)
92
- lines.push("", `Assistant: ${assistantText}`);
93
- lines.push("", "Title:");
94
- return lines.join("\n");
95
- }
96
- /**
97
- * Generate a sanitized title for a conversation, or `null` if generation
98
- * failed / produced nothing usable.
99
- */
100
- export async function generateSessionTitle(firstUserMessage, firstAssistantMessage, opts) {
101
- const trimmedUser = firstUserMessage.trim();
102
- if (!trimmedUser) {
103
- // Nothing to summarize — pi was driven by an empty user turn (shouldn't
104
- // happen via the wire path, but be defensive).
105
- return null;
106
- }
107
- const prompt = buildTitlePrompt(trimmedUser, firstAssistantMessage);
108
- const llmCall = opts.llmCall ?? defaultLlmCall(opts);
109
- try {
110
- const raw = await llmCall(prompt);
111
- return sanitizeTitle(raw);
112
- }
113
- catch (err) {
114
- const msg = err instanceof Error ? err.message : String(err);
115
- console.warn(`[spectral] warn: title generation failed: ${msg}`);
50
+ if (!out || out.length > 80) {
51
+ if (out.length > 80) {
52
+ return out.slice(0, 80);
53
+ }
116
54
  return null;
117
55
  }
118
- }
119
- /**
120
- * Default LLM call: spawn a one-off pi `AgentSession` with no tools, send the
121
- * prompt, and accumulate `text_delta`s until `agent_end`. Disposes the session
122
- * on completion (or failure).
123
- *
124
- * This deliberately reuses pi's existing model/auth resolution so the user
125
- * doesn't have to configure an extra API key just for title generation. The
126
- * downside is a slightly heavier code path than calling an LLM SDK directly;
127
- * the upside is zero new auth surface.
128
- */
129
- function defaultLlmCall(opts) {
130
- return async (prompt) => {
131
- const { session } = await createAgentSession({
132
- cwd: opts.cwd,
133
- agentDir: opts.agentDir,
134
- sessionManager: SessionManager.inMemory(opts.cwd),
135
- noTools: "all",
136
- });
137
- let text = "";
138
- let resolveDone;
139
- let rejectDone;
140
- const done = new Promise((resolve, reject) => {
141
- resolveDone = resolve;
142
- rejectDone = reject;
143
- });
144
- const unsubscribe = session.subscribe((ev) => {
145
- if (ev.type === "message_update") {
146
- const inner = ev.assistantMessageEvent;
147
- if (inner.type === "text_delta")
148
- text += inner.delta;
149
- }
150
- else if (ev.type === "agent_end") {
151
- resolveDone();
152
- }
153
- });
154
- try {
155
- // `prompt` resolves when the turn ends; `agent_end` should already
156
- // have fired by then but we double-await for safety.
157
- await session.prompt(prompt);
158
- // In the event prompt() resolves before agent_end (defensive), wait a
159
- // microtask to let the listener flush.
160
- await Promise.race([
161
- done,
162
- new Promise((r) => setTimeout(r, 0)).then(() => undefined),
163
- ]);
164
- }
165
- catch (err) {
166
- const e = err instanceof Error ? err : new Error(String(err));
167
- try {
168
- unsubscribe();
169
- }
170
- catch {
171
- /* ignore */
172
- }
173
- try {
174
- session.dispose();
175
- }
176
- catch {
177
- /* ignore */
178
- }
179
- rejectDone(e);
180
- throw e;
181
- }
182
- try {
183
- unsubscribe();
184
- }
185
- catch {
186
- /* ignore */
187
- }
188
- try {
189
- session.dispose();
190
- }
191
- catch {
192
- /* ignore */
193
- }
194
- return text;
195
- };
56
+ return out;
196
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -50,24 +50,42 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
+ "@anthropic-ai/sdk": "^0.75.0",
53
54
  "@inquirer/prompts": "^7.2.0",
54
55
  "@mariozechner/jiti": "2.6.5",
55
- "@mariozechner/pi-coding-agent": "0.70.2",
56
- "better-sqlite3": "^12.9.0",
57
- "@mariozechner/pi-agent-core": "0.70.2",
58
- "@mariozechner/pi-ai": "0.70.2",
59
- "@modelcontextprotocol/sdk": "^1.25.1",
60
56
  "@modelcontextprotocol/ext-apps": "^1.2.2",
57
+ "@modelcontextprotocol/sdk": "^1.25.1",
58
+ "@silvia-odwyer/photon-node": "^0.3.3",
59
+ "better-sqlite3": "^12.9.0",
60
+ "chalk": "^5.6.2",
61
+ "cross-spawn": "^7.0.6",
62
+ "diff": "^8.0.0",
63
+ "get-east-asian-width": "^1.3.0",
64
+ "glob": "^11.0.0",
65
+ "highlight.js": "^11.11.0",
66
+ "hosted-git-info": "^8.0.0",
67
+ "ignore": "^7.0.0",
68
+ "marked": "^15.0.0",
69
+ "minimatch": "^10.0.0",
61
70
  "open": "^10.2.0",
71
+ "openai": "^5.23.0",
72
+ "partial-json": "^0.1.7",
62
73
  "picocolors": "^1.1.1",
74
+ "proper-lockfile": "^4.1.2",
75
+ "proxy-agent": "^6.5.0",
63
76
  "typebox": "^1.1.24",
77
+ "undici": "^7.0.0",
64
78
  "ws": "^8.20.0",
79
+ "yaml": "^2.7.0",
65
80
  "zod": "^3.25.0 || ^4.0.0"
66
81
  },
67
82
  "devDependencies": {
68
83
  "@aexol/relay-protocol": "file:../packages/relay-protocol",
69
84
  "@types/better-sqlite3": "^7.6.13",
85
+ "@types/cross-spawn": "^6.0.6",
86
+ "@types/hosted-git-info": "^3.0.5",
70
87
  "@types/node": "^20.11.0",
88
+ "@types/proper-lockfile": "^4.1.4",
71
89
  "@types/ws": "^8.18.1",
72
90
  "tsx": "^4.7.0",
73
91
  "typescript": "^5.4.0",