@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.
- package/CHANGELOG.md +5 -0
- package/dist/agent/agents.js +1 -1
- package/dist/agent/index.js +199 -184
- package/dist/commands/serve.js +0 -3
- package/dist/designer/data/systems/renault/DESIGN.md +1 -1
- package/dist/designer/philosophies.js +668 -0
- package/dist/mcp/sampling-handler.js +1 -1
- package/dist/memory/commands/status.js +1 -1
- package/dist/memory/compaction.js +2 -2
- package/dist/memory/config.js +1 -1
- package/dist/memory/debug-log.js +1 -1
- package/dist/memory/hooks/compaction-hook.js +29 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/observer.js +2 -2
- package/dist/memory/project-observations-store.js +14 -0
- package/dist/memory/tokens.js +1 -1
- package/dist/memory/tools/read-project-observations.js +82 -0
- package/dist/memory/tools/recall-observation.js +2 -2
- package/dist/pi/agent-core/agent-loop.js +501 -0
- package/dist/pi/agent-core/agent.js +401 -0
- package/dist/pi/agent-core/harness/agent-harness.js +899 -0
- package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
- package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
- package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
- package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
- package/dist/pi/agent-core/harness/messages.js +101 -0
- package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
- package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
- package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
- package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
- package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
- package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
- package/dist/pi/agent-core/harness/session/session.js +196 -0
- package/dist/pi/agent-core/harness/session/uuid.js +49 -0
- package/dist/pi/agent-core/harness/skills.js +310 -0
- package/dist/pi/agent-core/harness/system-prompt.js +29 -0
- package/dist/pi/agent-core/harness/types.js +93 -0
- package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
- package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
- package/dist/pi/agent-core/index.js +24 -0
- package/dist/pi/agent-core/node.js +2 -0
- package/dist/pi/agent-core/proxy.js +277 -0
- package/dist/pi/agent-core/types.js +1 -0
- package/dist/pi/ai/api-registry.js +43 -0
- package/dist/pi/ai/cli.js +120 -0
- package/dist/pi/ai/env-api-keys.js +169 -0
- package/dist/pi/ai/image-models.generated.js +441 -0
- package/dist/pi/ai/image-models.js +22 -0
- package/dist/pi/ai/images-api-registry.js +21 -0
- package/dist/pi/ai/images.js +13 -0
- package/dist/pi/ai/index.js +18 -0
- package/dist/pi/ai/models.generated.js +16220 -0
- package/dist/pi/ai/models.js +70 -0
- package/dist/pi/ai/oauth.js +1 -0
- package/dist/pi/ai/providers/anthropic.js +945 -0
- package/dist/pi/ai/providers/faux.js +367 -0
- package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
- package/dist/pi/ai/providers/openai-completions.js +945 -0
- package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
- package/dist/pi/ai/providers/register-builtins.js +97 -0
- package/dist/pi/ai/providers/simple-options.js +40 -0
- package/dist/pi/ai/providers/transform-messages.js +183 -0
- package/dist/pi/ai/session-resources.js +21 -0
- package/dist/pi/ai/stream.js +26 -0
- package/dist/pi/ai/types.js +1 -0
- package/dist/pi/ai/utils/diagnostics.js +24 -0
- package/dist/pi/ai/utils/event-stream.js +80 -0
- package/dist/pi/ai/utils/hash.js +13 -0
- package/dist/pi/ai/utils/headers.js +7 -0
- package/dist/pi/ai/utils/json-parse.js +112 -0
- package/dist/pi/ai/utils/node-http-proxy.js +96 -0
- package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
- package/dist/pi/ai/utils/oauth/device-code.js +54 -0
- package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
- package/dist/pi/ai/utils/oauth/index.js +121 -0
- package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
- package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
- package/dist/pi/ai/utils/oauth/pkce.js +30 -0
- package/dist/pi/ai/utils/oauth/types.js +1 -0
- package/dist/pi/ai/utils/overflow.js +150 -0
- package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
- package/dist/pi/ai/utils/typebox-helpers.js +20 -0
- package/dist/pi/ai/utils/validation.js +280 -0
- package/dist/pi/coding-agent/bun/cli.js +7 -0
- package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
- package/dist/pi/coding-agent/cli/args.js +340 -0
- package/dist/pi/coding-agent/cli/file-processor.js +82 -0
- package/dist/pi/coding-agent/cli/initial-message.js +21 -0
- package/dist/pi/coding-agent/cli.js +17 -0
- package/dist/pi/coding-agent/config.js +414 -0
- package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
- package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
- package/dist/pi/coding-agent/core/agent-session.js +2498 -0
- package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
- package/dist/pi/coding-agent/core/auth-storage.js +441 -0
- package/dist/pi/coding-agent/core/bash-executor.js +110 -0
- package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
- package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
- package/dist/pi/coding-agent/core/compaction/index.js +6 -0
- package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
- package/dist/pi/coding-agent/core/defaults.js +1 -0
- package/dist/pi/coding-agent/core/diagnostics.js +1 -0
- package/dist/pi/coding-agent/core/event-bus.js +24 -0
- package/dist/pi/coding-agent/core/exec.js +74 -0
- package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
- package/dist/pi/coding-agent/core/export-html/index.js +225 -0
- package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
- package/dist/pi/coding-agent/core/extensions/index.js +8 -0
- package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
- package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
- package/dist/pi/coding-agent/core/extensions/types.js +44 -0
- package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
- package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
- package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
- package/dist/pi/coding-agent/core/index.js +11 -0
- package/dist/pi/coding-agent/core/keybindings.js +294 -0
- package/dist/pi/coding-agent/core/messages.js +122 -0
- package/dist/pi/coding-agent/core/model-registry.js +728 -0
- package/dist/pi/coding-agent/core/model-resolver.js +494 -0
- package/dist/pi/coding-agent/core/output-guard.js +58 -0
- package/dist/pi/coding-agent/core/package-manager.js +2020 -0
- package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
- package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
- package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
- package/dist/pi/coding-agent/core/resource-loader.js +733 -0
- package/dist/pi/coding-agent/core/sdk.js +282 -0
- package/dist/pi/coding-agent/core/session-cwd.js +37 -0
- package/dist/pi/coding-agent/core/session-manager.js +1146 -0
- package/dist/pi/coding-agent/core/settings-manager.js +794 -0
- package/dist/pi/coding-agent/core/skills.js +386 -0
- package/dist/pi/coding-agent/core/slash-commands.js +24 -0
- package/dist/pi/coding-agent/core/source-info.js +18 -0
- package/dist/pi/coding-agent/core/system-prompt.js +122 -0
- package/dist/pi/coding-agent/core/telemetry.js +8 -0
- package/dist/pi/coding-agent/core/timings.js +30 -0
- package/dist/pi/coding-agent/core/tools/bash.js +341 -0
- package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
- package/dist/pi/coding-agent/core/tools/edit.js +324 -0
- package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
- package/dist/pi/coding-agent/core/tools/find.js +297 -0
- package/dist/pi/coding-agent/core/tools/grep.js +303 -0
- package/dist/pi/coding-agent/core/tools/index.js +111 -0
- package/dist/pi/coding-agent/core/tools/ls.js +168 -0
- package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
- package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
- package/dist/pi/coding-agent/core/tools/read.js +288 -0
- package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
- package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
- package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
- package/dist/pi/coding-agent/core/tools/write.js +212 -0
- package/dist/pi/coding-agent/index.js +41 -0
- package/dist/pi/coding-agent/main.js +5 -0
- package/dist/pi/coding-agent/migrations.js +280 -0
- package/dist/pi/coding-agent/modes/index.js +7 -0
- package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
- package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
- package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
- package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
- package/dist/pi/coding-agent/modes/print-mode.js +130 -0
- package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
- package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
- package/dist/pi/coding-agent/utils/ansi.js +51 -0
- package/dist/pi/coding-agent/utils/changelog.js +86 -0
- package/dist/pi/coding-agent/utils/child-process.js +87 -0
- package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
- package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
- package/dist/pi/coding-agent/utils/clipboard.js +116 -0
- package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
- package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
- package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
- package/dist/pi/coding-agent/utils/git.js +162 -0
- package/dist/pi/coding-agent/utils/html.js +39 -0
- package/dist/pi/coding-agent/utils/image-convert.js +38 -0
- package/dist/pi/coding-agent/utils/image-resize.js +136 -0
- package/dist/pi/coding-agent/utils/mime.js +68 -0
- package/dist/pi/coding-agent/utils/paths.js +91 -0
- package/dist/pi/coding-agent/utils/photon.js +120 -0
- package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
- package/dist/pi/coding-agent/utils/shell.js +194 -0
- package/dist/pi/coding-agent/utils/sleep.js +16 -0
- package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
- package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
- package/dist/pi/coding-agent/utils/version-check.js +81 -0
- package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
- package/dist/pi/tui/autocomplete.js +631 -0
- package/dist/pi/tui/components/box.js +103 -0
- package/dist/pi/tui/components/cancellable-loader.js +34 -0
- package/dist/pi/tui/components/editor.js +1915 -0
- package/dist/pi/tui/components/image.js +88 -0
- package/dist/pi/tui/components/input.js +425 -0
- package/dist/pi/tui/components/loader.js +68 -0
- package/dist/pi/tui/components/markdown.js +633 -0
- package/dist/pi/tui/components/select-list.js +158 -0
- package/dist/pi/tui/components/settings-list.js +184 -0
- package/dist/pi/tui/components/spacer.js +22 -0
- package/dist/pi/tui/components/text.js +88 -0
- package/dist/pi/tui/components/truncated-text.js +50 -0
- package/dist/pi/tui/editor-component.js +1 -0
- package/dist/pi/tui/fuzzy.js +109 -0
- package/dist/pi/tui/index.js +31 -0
- package/dist/pi/tui/keybindings.js +173 -0
- package/dist/pi/tui/keys.js +1172 -0
- package/dist/pi/tui/kill-ring.js +43 -0
- package/dist/pi/tui/stdin-buffer.js +360 -0
- package/dist/pi/tui/terminal-image.js +335 -0
- package/dist/pi/tui/terminal.js +324 -0
- package/dist/pi/tui/tui.js +1076 -0
- package/dist/pi/tui/undo-stack.js +24 -0
- package/dist/pi/tui/utils.js +1016 -0
- package/dist/server/pi-bridge.js +1 -1
- package/dist/server/session-stream.js +8 -111
- package/dist/server/storage.js +62 -1
- package/dist/server/title-generator.js +14 -153
- package/package.json +24 -6
package/dist/server/pi-bridge.js
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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) {
|
package/dist/server/storage.js
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
* ("New conversation" / null / empty).
|
|
2
|
+
* Session title utilities.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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",
|