@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.
@@ -1,11 +1,8 @@
1
- import { ensureCompatibilityImports, getMcpDiscoverySummary, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
2
- import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
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 { hasStoredTokens } from "./mcp-auth.js";
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 buildSharedConfigNoticeLines(configOverridePath) {
142
- const discovery = getMcpDiscoverySummary(configOverridePath);
143
- const onboardingState = loadOnboardingState();
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 { configChanged: false };
138
+ return;
160
139
  const discovery = getMcpDiscoverySummary(configOverridePath);
161
140
  const onboardingState = loadOnboardingState();
162
- const { createMcpSetupPanel } = await import("./mcp-setup-panel.js");
163
- let configChanged = false;
164
- const callbacks = {
165
- previewImports: (imports) => previewCompatibilityImports(imports, configOverridePath),
166
- previewStarterProject: () => previewStarterProjectConfig(),
167
- previewRepoPrompt: () => {
168
- const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
169
- if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName)
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 { showStatus, showTools, reconnectServers, authenticateServer, openMcpPanel, openMcpSetup } from "./commands.js";
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
- const result = await openMcpSetup(state, pi, ctx, earlyConfigPath, "setup");
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
- if (ctx.hasUI) {
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.theme.fg("accent", `MCP: ${connectedCount}/${total} servers`));
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 { REFLECTOR_MAX_PASSES, coverageTagCounts, migrateLegacyReflections, observationPoolTokens, renderSummary, runPruner, runReflector, } from "../compaction.js";
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, onEvent: (event) => { progress.onEvent(event); updateWidget(); }, maxTurns: turnLimits.reflectorMaxTurnsPerPass }, workingReflections, workingObservations, (pass, max) => { progress.setPhase("reflector", pass, max); updateWidget(); });
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, onEvent: (event) => { progress.onEvent(event); updateWidget(); }, maxTurns: turnLimits.prunerMaxTurnsPerPass }, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens, (pass, max) => { progress.setPhase("pruner", pass, max); updateWidget(); });
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)) {
@@ -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
- return "extension";
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: type ?? "info",
1068
- system: detectMemorySystem(message),
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.1",
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",