@aexol/spectral 0.4.1 → 0.4.4
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/dist/mcp/commands.js +15 -128
- package/dist/mcp/index.js +4 -18
- package/dist/mcp/init.js +12 -1
- package/dist/memory/hooks/compaction-hook.js +3 -31
- package/dist/memory/tools/recall-observation.js +0 -155
- package/dist/server/pi-bridge.js +10 -4
- package/package.json +1 -2
- package/dist/mcp/mcp-panel.js +0 -646
- package/dist/mcp/mcp-setup-panel.js +0 -485
- package/dist/memory/progress.js +0 -134
package/dist/mcp/commands.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadMetadataCache } from "./metadata-cache.js";
|
|
1
|
+
import { getMcpDiscoverySummary, } from "./config.js";
|
|
2
|
+
import { updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
|
|
4
3
|
import { buildToolMetadata } from "./tool-metadata.js";
|
|
5
4
|
import { supportsOAuth, authenticate } from "./mcp-auth-flow.js";
|
|
6
|
-
import {
|
|
7
|
-
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.js";
|
|
8
|
-
import { openPath } from "./utils.js";
|
|
5
|
+
import { loadOnboardingState, markSharedConfigHintShown } from "./onboarding-state.js";
|
|
9
6
|
export async function showStatus(state, ctx) {
|
|
10
7
|
if (!ctx.hasUI)
|
|
11
8
|
return;
|
|
@@ -120,7 +117,6 @@ export async function authenticateServer(serverName, config, ctx) {
|
|
|
120
117
|
return;
|
|
121
118
|
}
|
|
122
119
|
try {
|
|
123
|
-
ctx.ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
|
|
124
120
|
const status = await authenticate(serverName, definition.url, definition);
|
|
125
121
|
if (status === "authenticated") {
|
|
126
122
|
ctx.ui.notify(`OAuth authentication successful for "${serverName}"!\n` +
|
|
@@ -134,130 +130,21 @@ export async function authenticateServer(serverName, config, ctx) {
|
|
|
134
130
|
const message = error instanceof Error ? error.message : String(error);
|
|
135
131
|
ctx.ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
|
|
136
132
|
}
|
|
137
|
-
finally {
|
|
138
|
-
ctx.ui.setStatus("mcp-auth", undefined);
|
|
139
|
-
}
|
|
140
133
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
|
|
145
|
-
return { lines: [], fingerprint: null };
|
|
146
|
-
}
|
|
147
|
-
const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
|
|
148
|
-
const sourceList = sharedSources.map((source) => source.path).join(", ");
|
|
149
|
-
return {
|
|
150
|
-
lines: [
|
|
151
|
-
`Using standard MCP config from ${sourceList}.`,
|
|
152
|
-
"Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.",
|
|
153
|
-
],
|
|
154
|
-
fingerprint: discovery.fingerprint,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
export async function openMcpSetup(_state, pi, ctx, configOverridePath, mode = "setup") {
|
|
134
|
+
export async function showSetupStatus(state, pi, ctx, configOverridePath) {
|
|
135
|
+
// Show MCP status first, then check for shared-config discovery hints
|
|
136
|
+
await showStatus(state, ctx);
|
|
158
137
|
if (!ctx.hasUI)
|
|
159
|
-
return
|
|
138
|
+
return;
|
|
160
139
|
const discovery = getMcpDiscoverySummary(configOverridePath);
|
|
161
140
|
const onboardingState = loadOnboardingState();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return null;
|
|
171
|
-
return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
172
|
-
},
|
|
173
|
-
adoptImports: async (imports) => {
|
|
174
|
-
const result = ensureCompatibilityImports(imports, configOverridePath);
|
|
175
|
-
if (result.added.length > 0)
|
|
176
|
-
configChanged = true;
|
|
177
|
-
return result;
|
|
178
|
-
},
|
|
179
|
-
scaffoldProjectConfig: async () => {
|
|
180
|
-
const path = writeStarterProjectConfig();
|
|
181
|
-
configChanged = true;
|
|
182
|
-
return { path };
|
|
183
|
-
},
|
|
184
|
-
addRepoPrompt: async () => {
|
|
185
|
-
const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
|
|
186
|
-
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
|
|
187
|
-
throw new Error("RepoPrompt is not available to add from this setup screen.");
|
|
188
|
-
}
|
|
189
|
-
const path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
190
|
-
configChanged = true;
|
|
191
|
-
return { path, serverName: repoPrompt.serverName };
|
|
192
|
-
},
|
|
193
|
-
openPath: async (targetPath) => {
|
|
194
|
-
await openPath(pi, targetPath);
|
|
195
|
-
},
|
|
196
|
-
markSetupCompleted: () => {
|
|
197
|
-
persistSetupCompleted(discovery.fingerprint);
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
return new Promise((resolve) => {
|
|
201
|
-
ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
202
|
-
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
|
|
203
|
-
done();
|
|
204
|
-
resolve({ configChanged });
|
|
205
|
-
});
|
|
206
|
-
}, { overlay: true, overlayOptions: { anchor: "center", width: 92 } });
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
export async function openMcpPanel(state, pi, ctx, configOverridePath) {
|
|
210
|
-
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
211
|
-
return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
|
|
212
|
-
}
|
|
213
|
-
const config = state.config;
|
|
214
|
-
const cache = loadMetadataCache();
|
|
215
|
-
const provenanceMap = getServerProvenance(pi.getFlag("mcp-config") ?? configOverridePath);
|
|
216
|
-
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(pi.getFlag("mcp-config") ?? configOverridePath);
|
|
217
|
-
const callbacks = {
|
|
218
|
-
reconnect: async (serverName) => {
|
|
219
|
-
return lazyConnect(state, serverName);
|
|
220
|
-
},
|
|
221
|
-
getConnectionStatus: (serverName) => {
|
|
222
|
-
const definition = config.mcpServers[serverName];
|
|
223
|
-
const connection = state.manager.getConnection(serverName);
|
|
224
|
-
if (connection?.status === "needs-auth") {
|
|
225
|
-
return "needs-auth";
|
|
226
|
-
}
|
|
227
|
-
if (definition?.auth === "oauth"
|
|
228
|
-
&& definition.oauth !== false
|
|
229
|
-
&& definition.oauth?.grantType !== "client_credentials"
|
|
230
|
-
&& !hasStoredTokens(serverName)) {
|
|
231
|
-
return "needs-auth";
|
|
232
|
-
}
|
|
233
|
-
if (connection?.status === "connected")
|
|
234
|
-
return "connected";
|
|
235
|
-
if (getFailureAgeSeconds(state, serverName) !== null)
|
|
236
|
-
return "failed";
|
|
237
|
-
return "idle";
|
|
238
|
-
},
|
|
239
|
-
refreshCacheAfterReconnect: (serverName) => {
|
|
240
|
-
const freshCache = loadMetadataCache();
|
|
241
|
-
return freshCache?.servers?.[serverName] ?? null;
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
const { createMcpPanel } = await import("./mcp-panel.js");
|
|
245
|
-
let configChanged = false;
|
|
246
|
-
await new Promise((resolve) => {
|
|
247
|
-
ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
248
|
-
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result) => {
|
|
249
|
-
if (!result.cancelled && result.changes.size > 0) {
|
|
250
|
-
writeDirectToolsConfig(result.changes, provenanceMap, config);
|
|
251
|
-
configChanged = true;
|
|
252
|
-
ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
|
|
253
|
-
}
|
|
254
|
-
done();
|
|
255
|
-
resolve();
|
|
256
|
-
}, { noticeLines });
|
|
257
|
-
}, { overlay: true, overlayOptions: { anchor: "center", width: 82 } });
|
|
258
|
-
});
|
|
259
|
-
if (noticeLines.length > 0 && fingerprint) {
|
|
260
|
-
markSharedConfigHintShown(fingerprint);
|
|
141
|
+
if (discovery.hasSharedServers && !onboardingState.sharedConfigHintShown) {
|
|
142
|
+
const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
|
|
143
|
+
const sourceList = sharedSources.map((source) => source.path).join(", ");
|
|
144
|
+
ctx.ui.notify(`Using standard MCP config from ${sourceList}.\n` +
|
|
145
|
+
"Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.", "info");
|
|
146
|
+
if (discovery.fingerprint) {
|
|
147
|
+
markSharedConfigHintShown(discovery.fingerprint);
|
|
148
|
+
}
|
|
261
149
|
}
|
|
262
|
-
return { configChanged };
|
|
263
150
|
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
2
|
+
import { showTools, reconnectServers, authenticateServer, showSetupStatus } from "./commands.js";
|
|
3
3
|
import { loadMcpConfig } from "./config.js";
|
|
4
4
|
import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.js";
|
|
5
5
|
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
|
|
@@ -157,27 +157,13 @@ export default function mcpAdapter(pi) {
|
|
|
157
157
|
case "tools":
|
|
158
158
|
await showTools(state, ctx);
|
|
159
159
|
break;
|
|
160
|
-
case "setup":
|
|
161
|
-
|
|
162
|
-
if (result?.configChanged) {
|
|
163
|
-
await ctx.reload();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
160
|
+
case "setup":
|
|
161
|
+
await showSetupStatus(state, pi, ctx, earlyConfigPath);
|
|
166
162
|
break;
|
|
167
|
-
}
|
|
168
163
|
case "status":
|
|
169
164
|
case "":
|
|
170
165
|
default:
|
|
171
|
-
|
|
172
|
-
const result = await openMcpPanel(state, pi, ctx, earlyConfigPath);
|
|
173
|
-
if (result?.configChanged) {
|
|
174
|
-
await ctx.reload();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
await showStatus(state, ctx);
|
|
180
|
-
}
|
|
166
|
+
await showSetupStatus(state, pi, ctx, earlyConfigPath);
|
|
181
167
|
break;
|
|
182
168
|
}
|
|
183
169
|
},
|
package/dist/mcp/init.js
CHANGED
|
@@ -208,6 +208,17 @@ export function flushMetadataCache(state) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
function safeFg(ui, color, text) {
|
|
212
|
+
try {
|
|
213
|
+
const styled = ui?.theme?.fg?.(color, text);
|
|
214
|
+
if (styled)
|
|
215
|
+
return styled;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// fall through to plain text
|
|
219
|
+
}
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
211
222
|
export function updateStatusBar(state) {
|
|
212
223
|
const ui = state.ui;
|
|
213
224
|
if (!ui)
|
|
@@ -218,7 +229,7 @@ export function updateStatusBar(state) {
|
|
|
218
229
|
return;
|
|
219
230
|
}
|
|
220
231
|
const connectedCount = state.manager.getAllConnections().size;
|
|
221
|
-
ui.setStatus("mcp", ui
|
|
232
|
+
ui.setStatus("mcp", safeFg(ui, "accent", `MCP: ${connectedCount}/${total} servers`));
|
|
222
233
|
}
|
|
223
234
|
export function getFailureAgeSeconds(state, serverName) {
|
|
224
235
|
const failedAt = state.failureTracker.get(serverName);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
2
1
|
import { debugLog, withDebugLogContext } from "../debug-log.js";
|
|
3
2
|
import { resolveTurnLimits } from "../config.js";
|
|
4
3
|
import { collectObservationsByCoverage, findLastCompactionIndex, gapRawEntries, getMemoryState, } from "../branch.js";
|
|
5
|
-
import {
|
|
4
|
+
import { coverageTagCounts, migrateLegacyReflections, observationPoolTokens, renderSummary, runPruner, runReflector, } from "../compaction.js";
|
|
6
5
|
import { observationsToPromptLines, runObserver } from "../observer.js";
|
|
7
|
-
import { CompactionProgressTracker } from "../progress.js";
|
|
8
6
|
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
|
|
9
7
|
import { estimateStringTokens } from "../tokens.js";
|
|
10
8
|
import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine, } from "../types.js";
|
|
@@ -29,9 +27,6 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
29
27
|
return { cancel: true };
|
|
30
28
|
}
|
|
31
29
|
runtime.compactHookInFlight = true;
|
|
32
|
-
const progress = new CompactionProgressTracker();
|
|
33
|
-
const WIDGET_NAME = "om_compact_progress";
|
|
34
|
-
let clearWidget = () => { };
|
|
35
30
|
try {
|
|
36
31
|
runtime.ensureConfig(ctx.cwd);
|
|
37
32
|
const runId = `compaction-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
@@ -60,21 +55,6 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
60
55
|
return { cancel: true };
|
|
61
56
|
}
|
|
62
57
|
runtime.resolveFailureNotified = false;
|
|
63
|
-
const updateWidget = () => {
|
|
64
|
-
if (!hasUI || !ui)
|
|
65
|
-
return;
|
|
66
|
-
if (!progress.getPhase()) {
|
|
67
|
-
ui.setWidget(WIDGET_NAME, undefined);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
ui.setWidget(WIDGET_NAME, (_tui, theme) => {
|
|
71
|
-
return new Text(progress.formatWidget(theme), 0, 0);
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
clearWidget = () => {
|
|
75
|
-
if (hasUI && ui)
|
|
76
|
-
ui.setWidget(WIDGET_NAME, undefined);
|
|
77
|
-
};
|
|
78
58
|
let entries = branchEntries;
|
|
79
59
|
if (runtime.observerPromise) {
|
|
80
60
|
try {
|
|
@@ -112,8 +92,6 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
112
92
|
});
|
|
113
93
|
if (hasUI)
|
|
114
94
|
ui?.notify(`Observational memory: sync catch-up observer running on ~${gapTokenEstimate.toLocaleString()}-token gap`, "info");
|
|
115
|
-
progress.setPhase("observer", 1, 1);
|
|
116
|
-
updateWidget();
|
|
117
95
|
runtime.observerInFlight = true;
|
|
118
96
|
const gapCall = runObserver({
|
|
119
97
|
model: resolved.model,
|
|
@@ -243,11 +221,8 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
243
221
|
});
|
|
244
222
|
if (hasUI)
|
|
245
223
|
ui?.notify("Observational memory: running reflector + pruner...", "info");
|
|
246
|
-
progress.setPhase("reflector", 1, REFLECTOR_MAX_PASSES);
|
|
247
|
-
progress.setStartingCounts(workingReflections.length, workingObservations.length);
|
|
248
|
-
updateWidget();
|
|
249
224
|
const coverageBefore = coverageTagCounts(workingReflections, workingObservations);
|
|
250
|
-
const reflectorResult = await runReflector({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal,
|
|
225
|
+
const reflectorResult = await runReflector({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.reflectorMaxTurnsPerPass }, workingReflections, workingObservations);
|
|
251
226
|
finalReflections = reflectorResult.reflections;
|
|
252
227
|
const coverageAfter = coverageTagCounts(finalReflections, workingObservations);
|
|
253
228
|
debugLog("compaction.reflector.result", {
|
|
@@ -257,7 +232,7 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
257
232
|
beforeReflections: workingReflections.length,
|
|
258
233
|
afterReflections: finalReflections.length,
|
|
259
234
|
});
|
|
260
|
-
const prunerResult = await runPruner({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal,
|
|
235
|
+
const prunerResult = await runPruner({ model: resolved.model, apiKey: resolved.apiKey, headers: resolved.headers, signal, maxTurns: turnLimits.prunerMaxTurnsPerPass }, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens);
|
|
261
236
|
finalObservations = prunerResult.observations;
|
|
262
237
|
debugLog("compaction.pruner.result", {
|
|
263
238
|
stopReason: prunerResult.stopReason,
|
|
@@ -267,7 +242,6 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
267
242
|
beforeObservations: workingObservations.length,
|
|
268
243
|
afterObservations: finalObservations.length,
|
|
269
244
|
});
|
|
270
|
-
updateWidget();
|
|
271
245
|
if (hasUI) {
|
|
272
246
|
ui?.notify(`Observational memory: diagnostics — ${formatReflectorStats(reflectorResult.stats)}; coverage ${formatCoverageCounts(coverageBefore)} → ${formatCoverageCounts(coverageAfter)}; ${formatPrunerStats(prunerResult)}`, "info");
|
|
273
247
|
}
|
|
@@ -312,8 +286,6 @@ export function registerCompactionHook(pi, runtime) {
|
|
|
312
286
|
}
|
|
313
287
|
finally {
|
|
314
288
|
runtime.compactHookInFlight = false;
|
|
315
|
-
progress.clear();
|
|
316
|
-
clearWidget();
|
|
317
289
|
}
|
|
318
290
|
});
|
|
319
291
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Type } from "@mariozechner/pi-ai";
|
|
2
2
|
import { defineTool } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
4
3
|
import { recallMemorySources, } from "../branch.js";
|
|
5
4
|
import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
|
|
6
5
|
import { estimateEntryTokens } from "../tokens.js";
|
|
@@ -313,154 +312,6 @@ function renderFoundResult(result) {
|
|
|
313
312
|
const text = isObservationOnly(details) ? renderObservationOnlyTextFromResult(result) : renderMemoryText(result);
|
|
314
313
|
return textResult(text, details);
|
|
315
314
|
}
|
|
316
|
-
function plural(n, singular, pluralForm = `${singular}s`) {
|
|
317
|
-
return `${n.toLocaleString()} ${n === 1 ? singular : pluralForm}`;
|
|
318
|
-
}
|
|
319
|
-
function sourceEntriesFromDetails(details) {
|
|
320
|
-
if (!isObservationOnly(details))
|
|
321
|
-
return details.sourceEntries;
|
|
322
|
-
return details.matches.flatMap((match) => match.sourceEntries ?? []);
|
|
323
|
-
}
|
|
324
|
-
function tokenSummary(tokens) {
|
|
325
|
-
return `~${tokens.toLocaleString()} ${tokens === 1 ? "token" : "tokens"}`;
|
|
326
|
-
}
|
|
327
|
-
function isFailureStatus(status) {
|
|
328
|
-
return status === "invalid_id" || status === "not_found";
|
|
329
|
-
}
|
|
330
|
-
function observationCountForHeader(details) {
|
|
331
|
-
return isObservationOnly(details) ? details.matches.length : details.observations.length;
|
|
332
|
-
}
|
|
333
|
-
export function formatRecallHeaderForTui(details) {
|
|
334
|
-
if (isFailureStatus(details.status))
|
|
335
|
-
return "× failure";
|
|
336
|
-
const parts = ["✓ success"];
|
|
337
|
-
if (details.reflections.length > 0)
|
|
338
|
-
parts.push(plural(details.reflections.length, "reflection"));
|
|
339
|
-
const observations = observationCountForHeader(details);
|
|
340
|
-
if (observations > 0)
|
|
341
|
-
parts.push(plural(observations, "observation"));
|
|
342
|
-
const sources = sourceEntriesFromDetails(details);
|
|
343
|
-
if (sources.length > 0)
|
|
344
|
-
parts.push(plural(sources.length, "source"));
|
|
345
|
-
const tokens = sources.reduce((sum, source) => sum + source.tokens, 0);
|
|
346
|
-
if (tokens > 0)
|
|
347
|
-
parts.push(tokenSummary(tokens));
|
|
348
|
-
return parts.join(" · ");
|
|
349
|
-
}
|
|
350
|
-
const TUI_TYPE_WIDTH = 15;
|
|
351
|
-
const TUI_META_WIDTH = 31;
|
|
352
|
-
function alignedRow(type, meta, text) {
|
|
353
|
-
return `${type.padEnd(TUI_TYPE_WIDTH)} ${meta.padEnd(TUI_META_WIDTH)} ${text}`.trimEnd();
|
|
354
|
-
}
|
|
355
|
-
function sourceTag(source) {
|
|
356
|
-
const origin = source.origin.trim().toLowerCase();
|
|
357
|
-
if (origin === "user")
|
|
358
|
-
return "user";
|
|
359
|
-
if (origin === "assistant")
|
|
360
|
-
return "assistant";
|
|
361
|
-
if (origin.startsWith("tool result"))
|
|
362
|
-
return "tool";
|
|
363
|
-
if (origin.startsWith("custom message"))
|
|
364
|
-
return "custom";
|
|
365
|
-
if (origin.startsWith("branch summary"))
|
|
366
|
-
return "summary";
|
|
367
|
-
return origin.split(/[^a-z0-9]+/).find(Boolean) ?? "entry";
|
|
368
|
-
}
|
|
369
|
-
function sourceMetadataLine(source) {
|
|
370
|
-
return alignedRow("✓ source", `${source.timestamp} [${sourceTag(source)}]`, tokenSummary(source.tokens));
|
|
371
|
-
}
|
|
372
|
-
function observationLine(observation) {
|
|
373
|
-
return alignedRow("✓ observation", `${observation.timestamp} [${observation.relevance}]`, observation.content);
|
|
374
|
-
}
|
|
375
|
-
function reflectionLine(reflection) {
|
|
376
|
-
return alignedRow("✓ reflection", "", reflection.content);
|
|
377
|
-
}
|
|
378
|
-
function noteLine(kind, text) {
|
|
379
|
-
return alignedRow("• note", `[${kind}]`, text);
|
|
380
|
-
}
|
|
381
|
-
function indentContent(content) {
|
|
382
|
-
return content
|
|
383
|
-
.split("\n")
|
|
384
|
-
.map((line) => ` ${line}`)
|
|
385
|
-
.join("\n");
|
|
386
|
-
}
|
|
387
|
-
function unavailableEvidenceMessage(details) {
|
|
388
|
-
if (details.unavailableReflectionProvenance.length > 0 && details.observations.length === 0) {
|
|
389
|
-
return "migrated legacy reflection has no supporting observations";
|
|
390
|
-
}
|
|
391
|
-
return "no source entries are available for this memory id";
|
|
392
|
-
}
|
|
393
|
-
function pushSourceLines(lines, sources, expanded) {
|
|
394
|
-
for (const source of sources) {
|
|
395
|
-
lines.push(sourceMetadataLine(source));
|
|
396
|
-
if (expanded && source.content) {
|
|
397
|
-
lines.push(indentContent(source.content));
|
|
398
|
-
lines.push("");
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
function memoryRows(details) {
|
|
403
|
-
if (isObservationOnly(details))
|
|
404
|
-
return details.matches.map((match) => observationLine(match.observation));
|
|
405
|
-
return [
|
|
406
|
-
...details.reflections.map((reflection) => reflectionLine(reflection)),
|
|
407
|
-
...details.observations.map((observation) => observationLine(observation.observation)),
|
|
408
|
-
];
|
|
409
|
-
}
|
|
410
|
-
function noteRows(details, sources) {
|
|
411
|
-
const notes = [];
|
|
412
|
-
if (details.status === "invalid_id") {
|
|
413
|
-
notes.push(noteLine("invalid id", `memory ids must be 12 lowercase hex characters; received ${details.memoryId}`));
|
|
414
|
-
return notes;
|
|
415
|
-
}
|
|
416
|
-
if (details.status === "not_found") {
|
|
417
|
-
notes.push(noteLine("not found", `no observation or reflection with id ${details.memoryId} was found on the current branch`));
|
|
418
|
-
return notes;
|
|
419
|
-
}
|
|
420
|
-
if (details.collision)
|
|
421
|
-
notes.push(noteLine("id collision", `multiple memory items share ${details.memoryId}`));
|
|
422
|
-
if (sources.length === 0 && (details.reflections.length > 0 || details.observations.length > 0 || details.matches.length > 0)) {
|
|
423
|
-
notes.push(noteLine("unavailable evidence", unavailableEvidenceMessage(details)));
|
|
424
|
-
}
|
|
425
|
-
return notes;
|
|
426
|
-
}
|
|
427
|
-
export function formatRecallResultForTui(result, expanded) {
|
|
428
|
-
const details = result.details;
|
|
429
|
-
if (!details) {
|
|
430
|
-
const text = result.content
|
|
431
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
432
|
-
.map((part) => part.text)
|
|
433
|
-
.join("\n");
|
|
434
|
-
return text || "recall";
|
|
435
|
-
}
|
|
436
|
-
const sources = sourceEntriesFromDetails(details);
|
|
437
|
-
const lines = [];
|
|
438
|
-
const rows = memoryRows(details);
|
|
439
|
-
const notes = noteRows(details, sources);
|
|
440
|
-
lines.push(...rows);
|
|
441
|
-
if (rows.length > 0 && notes.length > 0)
|
|
442
|
-
lines.push("");
|
|
443
|
-
lines.push(...notes);
|
|
444
|
-
if ((rows.length > 0 || notes.length > 0) && sources.length > 0)
|
|
445
|
-
lines.push("");
|
|
446
|
-
pushSourceLines(lines, sources, expanded);
|
|
447
|
-
if (!expanded && sources.some((source) => source.content)) {
|
|
448
|
-
lines.push("", "(Ctrl+O to expand)");
|
|
449
|
-
}
|
|
450
|
-
return lines.join("\n").trimEnd();
|
|
451
|
-
}
|
|
452
|
-
export function formatRecallCallForTui(id) {
|
|
453
|
-
return `recall ${id ?? "..."}`;
|
|
454
|
-
}
|
|
455
|
-
export function formatRecallRenderedResultForTui(result, expanded) {
|
|
456
|
-
const body = formatRecallResultForTui(result, expanded);
|
|
457
|
-
const header = result.details ? formatRecallHeaderForTui(result.details) : undefined;
|
|
458
|
-
if (header && body)
|
|
459
|
-
return `\n${header}\n\n${body}`;
|
|
460
|
-
if (header)
|
|
461
|
-
return `\n${header}`;
|
|
462
|
-
return body ? `\n${body}` : "";
|
|
463
|
-
}
|
|
464
315
|
export const recallObservationTool = defineTool({
|
|
465
316
|
name: RECALL_OBSERVATION_TOOL_NAME,
|
|
466
317
|
label: "Recall memory evidence",
|
|
@@ -482,12 +333,6 @@ export const recallObservationTool = defineTool({
|
|
|
482
333
|
"Must be a specific id; this tool does not search by topic.",
|
|
483
334
|
}),
|
|
484
335
|
}),
|
|
485
|
-
renderCall(args) {
|
|
486
|
-
return new Text(formatRecallCallForTui(args.id), 0, 0);
|
|
487
|
-
},
|
|
488
|
-
renderResult(result, options) {
|
|
489
|
-
return new Text(formatRecallRenderedResultForTui(result, options.expanded), 0, 0);
|
|
490
|
-
},
|
|
491
336
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
492
337
|
const memoryId = params.id;
|
|
493
338
|
if (!MEMORY_ID_PATTERN.test(memoryId)) {
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -1040,11 +1040,12 @@ function detectMemorySystem(message) {
|
|
|
1040
1040
|
return "memory_observer";
|
|
1041
1041
|
if (lower.includes("compaction") || lower.includes("compact"))
|
|
1042
1042
|
return "memory_compaction";
|
|
1043
|
-
if (lower.includes("reflection"))
|
|
1043
|
+
if (lower.includes("reflection") || lower.includes("reflect"))
|
|
1044
1044
|
return "memory_reflection";
|
|
1045
1045
|
if (lower.includes("pruner") || lower.includes("prune"))
|
|
1046
1046
|
return "memory_pruner";
|
|
1047
|
-
|
|
1047
|
+
// Keep generic observational-memory messages visible in the landing badge.
|
|
1048
|
+
return "memory_observer";
|
|
1048
1049
|
}
|
|
1049
1050
|
/**
|
|
1050
1051
|
* Create a minimal ExtensionUIContext that forwards `notify()` calls as
|
|
@@ -1061,11 +1062,16 @@ function createHeadlessUIContext(emit) {
|
|
|
1061
1062
|
get(_target, prop) {
|
|
1062
1063
|
if (prop === "notify") {
|
|
1063
1064
|
return (message, type) => {
|
|
1065
|
+
const level = type ?? "info";
|
|
1066
|
+
const system = detectMemorySystem(message);
|
|
1067
|
+
if (system?.startsWith("memory_")) {
|
|
1068
|
+
console.info(`[PiBridge][memory][${level}] ${message}`);
|
|
1069
|
+
}
|
|
1064
1070
|
emit({
|
|
1065
1071
|
type: "agent_notification",
|
|
1066
1072
|
message,
|
|
1067
|
-
level
|
|
1068
|
-
system
|
|
1073
|
+
level,
|
|
1074
|
+
system,
|
|
1069
1075
|
});
|
|
1070
1076
|
};
|
|
1071
1077
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexol/spectral",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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,
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"better-sqlite3": "^12.9.0",
|
|
57
57
|
"@mariozechner/pi-agent-core": "0.70.2",
|
|
58
58
|
"@mariozechner/pi-ai": "0.70.2",
|
|
59
|
-
"@mariozechner/pi-tui": "0.70.2",
|
|
60
59
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
61
60
|
"@modelcontextprotocol/ext-apps": "^1.2.2",
|
|
62
61
|
"open": "^10.2.0",
|