@mrclrchtr/supi-debug 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/debug.ts CHANGED
@@ -1,12 +1,7 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
1
3
  import { StringEnum } from "@earendil-works/pi-ai";
2
- import {
3
- DEFAULT_MAX_BYTES,
4
- DEFAULT_MAX_LINES,
5
- type ExtensionAPI,
6
- formatSize,
7
- type TruncationResult,
8
- truncateHead,
9
- } from "@earendil-works/pi-coding-agent";
4
+ import type { AgentToolUpdateCallback, ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
5
  import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
11
6
  import { registerContextProvider } from "@mrclrchtr/supi-core/context";
12
7
  import {
@@ -18,20 +13,28 @@ import {
18
13
  type DebugEventView,
19
14
  getDebugEvents,
20
15
  getDebugSummary,
21
- isDebugLevel,
16
+ isDebugOperationId,
22
17
  subscribeDebugEvents,
23
18
  } from "@mrclrchtr/supi-core/debug";
19
+ import { resolveToolPath } from "@mrclrchtr/supi-core/path";
24
20
  import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
25
21
  import { Type } from "typebox";
26
- import { formatDataLines } from "./format.ts";
27
- import { registerDebugMessageRenderer } from "./renderer.ts";
22
+ import { registerDebugCommand } from "./command.ts";
23
+ import { formatDebugEvents, truncateDebugOutput } from "./output.ts";
24
+ import type { DebugToolParams } from "./query.ts";
25
+ import { createDebugRenderDetails } from "./render-details.ts";
26
+ import {
27
+ registerDebugMessageRenderer,
28
+ renderDebugToolCall,
29
+ renderDebugToolResult,
30
+ } from "./renderer.ts";
28
31
  import { DEBUG_EVENT_ENTRY_TYPE, readSessionDebugEvents } from "./session-events.ts";
29
32
  import { maybeLogLoadStatus } from "./status-log.ts";
30
33
  import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
31
34
 
32
- const DEBUG_SECTION = "debug";
33
- const DEBUG_REPORT_TYPE = "supi-debug-report";
35
+ const baseDir = dirname(dirname(fileURLToPath(import.meta.url)));
34
36
 
37
+ const DEBUG_SECTION = "debug";
35
38
  interface DebugConfig extends Record<string, unknown> {
36
39
  enabled: boolean;
37
40
  agentAccess: DebugAgentAccess;
@@ -40,8 +43,6 @@ interface DebugConfig extends Record<string, unknown> {
40
43
 
41
44
  const DEBUG_DEFAULTS: DebugConfig = { ...DEBUG_REGISTRY_DEFAULTS };
42
45
 
43
- type DebugToolParams = DebugEventQuery & { sessionFile?: string };
44
-
45
46
  function normalizeAgentAccess(value: string): DebugAgentAccess {
46
47
  return value === "off" || value === "raw" ? value : "sanitized";
47
48
  }
@@ -143,88 +144,6 @@ function registerDebugSettings(pi: ExtensionAPI): void {
143
144
  );
144
145
  }
145
146
 
146
- function parseCommandArgs(args: string): DebugToolParams {
147
- const query: DebugToolParams = {};
148
- const parts = args.trim().split(/\s+/).filter(Boolean);
149
- for (const part of parts) {
150
- const [key, value] = part.split("=", 2);
151
- if (!value) continue;
152
- if (key === "source") query.source = value;
153
- if (key === "category") query.category = value;
154
- if (key === "level" && isDebugLevel(value)) query.level = value;
155
- if (key === "limit") query.limit = normalizeMaxEvents(value);
156
- if (key === "sessionFile") query.sessionFile = value;
157
- }
158
- return query;
159
- }
160
-
161
- function pushFormattedData(lines: string[], label: string, value: unknown): void {
162
- const dataLines = formatDataLines(value);
163
- if (dataLines.length === 0) return;
164
- if (dataLines.length === 1) {
165
- lines.push(` ${label}: ${dataLines[0]}`);
166
- } else {
167
- lines.push(` ${label}:`);
168
- for (const dl of dataLines) {
169
- lines.push(` ${dl}`);
170
- }
171
- }
172
- }
173
-
174
- function formatEvents(
175
- events: DebugEventView[],
176
- rawAccessDenied: boolean,
177
- rawDataUnavailable = false,
178
- persistedEventCount?: number,
179
- ): string[] {
180
- if (events.length === 0) {
181
- return persistedEventCount === 0
182
- ? [
183
- "This session has no persisted debug events; sessions recorded before persistence cannot be backfilled.",
184
- ]
185
- : ["No matching debug events available."];
186
- }
187
-
188
- const lines: string[] = [];
189
- for (const event of events) {
190
- lines.push(
191
- `[${new Date(event.timestamp).toISOString()}] ${event.level.toUpperCase()} ${event.source}/${event.category}: ${event.message}`,
192
- );
193
- if (event.cwd) lines.push(` cwd: ${event.cwd}`);
194
- pushFormattedData(lines, "data", event.data);
195
- pushFormattedData(lines, "rawData", event.rawData);
196
- }
197
- if (rawDataUnavailable) {
198
- lines.push("");
199
- lines.push("Raw debug data is not persisted for historical sessions.");
200
- } else if (rawAccessDenied) {
201
- lines.push("");
202
- lines.push("Raw debug data was requested but is not enabled in SuPi Debug settings.");
203
- }
204
- return lines;
205
- }
206
-
207
- function appendTruncationNote(content: string, truncation: TruncationResult): string {
208
- if (!truncation.truncated) return content;
209
-
210
- const omittedLines = truncation.totalLines - truncation.outputLines;
211
- const omittedBytes = truncation.totalBytes - truncation.outputBytes;
212
- const separator = content.length > 0 ? "\n\n" : "";
213
- return `${content}${separator}[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ${omittedLines} lines (${formatSize(omittedBytes)}) omitted. Use filters or a smaller limit to narrow results.]`;
214
- }
215
-
216
- function truncateDebugOutput(content: string): { text: string; truncation?: TruncationResult } {
217
- const truncation = truncateHead(content, {
218
- maxLines: DEFAULT_MAX_LINES,
219
- maxBytes: DEFAULT_MAX_BYTES,
220
- });
221
-
222
- return {
223
- text: appendTruncationNote(truncation.content, truncation),
224
- truncation: truncation.truncated ? truncation : undefined,
225
- };
226
- }
227
-
228
147
  function buildSummaryData(): Record<string, string | number> | null {
229
148
  const summary = getDebugSummary();
230
149
  if (!summary) return null;
@@ -239,7 +158,36 @@ function buildSummaryData(): Record<string, string | number> | null {
239
158
  return data;
240
159
  }
241
160
 
242
- async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
161
+ interface DebugProgressDetails {
162
+ scannedLines: number;
163
+ persistedEventCount: number;
164
+ matchedEvents: number;
165
+ }
166
+
167
+ function reportDebugProgress(
168
+ onUpdate: AgentToolUpdateCallback<unknown> | undefined,
169
+ progress: DebugProgressDetails,
170
+ ): void {
171
+ onUpdate?.({
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `Reading persisted debug events: ${progress.matchedEvents} matching events found.`,
176
+ },
177
+ ],
178
+ details: progress,
179
+ });
180
+ }
181
+
182
+ interface DebugToolExecutionOptions {
183
+ config: DebugConfig;
184
+ cwd: string;
185
+ signal?: AbortSignal;
186
+ onUpdate?: AgentToolUpdateCallback<unknown>;
187
+ }
188
+
189
+ async function buildToolResult(params: DebugToolParams, options: DebugToolExecutionOptions) {
190
+ const { config, cwd, signal, onUpdate } = options;
243
191
  if (!config.enabled && !params.sessionFile) {
244
192
  throw new Error(
245
193
  "SuPi debug event capture is disabled. Enable Debug in /supi-settings to retain events.",
@@ -251,6 +199,7 @@ async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
251
199
  }
252
200
 
253
201
  const filters = {
202
+ operationId: params.operationId,
254
203
  source: params.source,
255
204
  level: params.level,
256
205
  category: params.category,
@@ -266,7 +215,14 @@ async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
266
215
  let rawDataUnavailable = false;
267
216
  let persistedEventCount: number | undefined;
268
217
  if (params.sessionFile) {
269
- const persisted = await readSessionDebugEvents(params.sessionFile, filters);
218
+ const sessionFile = resolveToolPath(cwd, params.sessionFile);
219
+ const persisted =
220
+ signal || onUpdate
221
+ ? await readSessionDebugEvents(sessionFile, filters, {
222
+ signal,
223
+ onProgress: (progress) => reportDebugProgress(onUpdate, progress),
224
+ })
225
+ : await readSessionDebugEvents(sessionFile, filters);
270
226
  events = persisted.events;
271
227
  persistedEventCount = persisted.persistedEventCount;
272
228
  rawAccessDenied = Boolean(params.includeRaw);
@@ -277,19 +233,28 @@ async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
277
233
  rawAccessDenied = result.rawAccessDenied;
278
234
  }
279
235
  const output = truncateDebugOutput(
280
- formatEvents(events, rawAccessDenied, rawDataUnavailable, persistedEventCount).join("\n"),
236
+ formatDebugEvents(events, rawAccessDenied, rawDataUnavailable, persistedEventCount).join("\n"),
281
237
  );
238
+ const details = createDebugRenderDetails(events, {
239
+ enabled: config.enabled,
240
+ agentAccess: config.agentAccess,
241
+ sessionFile: params.sessionFile,
242
+ rawAccessDenied,
243
+ rawDataUnavailable,
244
+ persistedEventCount,
245
+ eventCount: events.length,
246
+ emptyReason:
247
+ events.length === 0
248
+ ? persistedEventCount === 0
249
+ ? "no-persisted-events"
250
+ : "no-matches"
251
+ : undefined,
252
+ truncation: output.truncation,
253
+ });
254
+
282
255
  return {
283
256
  content: [{ type: "text" as const, text: output.text }],
284
- details: {
285
- enabled: config.enabled,
286
- agentAccess: config.agentAccess,
287
- sessionFile: params.sessionFile,
288
- rawAccessDenied,
289
- rawDataUnavailable,
290
- events,
291
- truncation: output.truncation,
292
- },
257
+ details,
293
258
  };
294
259
  }
295
260
 
@@ -315,59 +280,16 @@ export default function debugExtension(pi: ExtensionAPI) {
315
280
 
316
281
  pi.on("resources_discover", async (_event, ctx) => {
317
282
  maybeLogLoadStatus(pi, ctx.cwd, "resources_discover");
283
+ // Self-register the package prompt template so standalone installs and
284
+ // workspace-root loads expose the same `/supi-tooling-retro` surface.
285
+ return { promptPaths: [join(baseDir, "prompts")] };
318
286
  });
319
287
 
320
288
  pi.on("session_shutdown", () => {
321
289
  unsubscribeDebugEvents();
322
290
  });
323
291
 
324
- pi.registerCommand("supi-debug", {
325
- description: "Show recent SuPi debug events",
326
- handler: async (args, ctx) => {
327
- const config = applyDebugConfig(ctx.cwd);
328
- const query = parseCommandArgs(args);
329
- if (!config.enabled && !query.sessionFile) {
330
- pi.sendMessage({
331
- customType: DEBUG_REPORT_TYPE,
332
- content: "SuPi debug event capture is disabled. Enable Debug in /supi-settings.",
333
- display: true,
334
- });
335
- return;
336
- }
337
-
338
- if (query.sessionFile) {
339
- const persisted = await readSessionDebugEvents(query.sessionFile, {
340
- source: query.source,
341
- level: query.level,
342
- category: query.category,
343
- limit: query.limit,
344
- });
345
- const output = truncateDebugOutput(
346
- formatEvents(persisted.events, false, false, persisted.persistedEventCount).join("\n"),
347
- );
348
- pi.sendMessage({
349
- customType: DEBUG_REPORT_TYPE,
350
- content: output.text,
351
- display: true,
352
- details: {
353
- sessionFile: query.sessionFile,
354
- events: persisted.events,
355
- truncation: output.truncation,
356
- },
357
- });
358
- return;
359
- }
360
-
361
- const { events, rawAccessDenied } = getDebugEvents(query);
362
- const output = truncateDebugOutput(formatEvents(events, rawAccessDenied).join("\n"));
363
- pi.sendMessage({
364
- customType: DEBUG_REPORT_TYPE,
365
- content: output.text,
366
- display: true,
367
- details: { events, rawAccessDenied, truncation: output.truncation },
368
- });
369
- },
370
- });
292
+ registerDebugCommand(pi, applyDebugConfig, normalizeMaxEvents);
371
293
 
372
294
  pi.registerTool({
373
295
  name: "supi_debug",
@@ -376,6 +298,12 @@ export default function debugExtension(pi: ExtensionAPI) {
376
298
  promptSnippet,
377
299
  promptGuidelines,
378
300
  parameters: Type.Object({
301
+ operationId: Type.Optional(
302
+ Type.String({
303
+ description: "Filter by exact Debug Operation ID",
304
+ pattern: "^op-[A-Za-z0-9_-]{21}[AQgw]$",
305
+ }),
306
+ ),
379
307
  source: Type.Optional(Type.String({ description: "Filter by extension source, e.g. lsp" })),
380
308
  level: Type.Optional(
381
309
  StringEnum(["debug", "info", "warning", "error"], {
@@ -392,9 +320,15 @@ export default function debugExtension(pi: ExtensionAPI) {
392
320
  ),
393
321
  }),
394
322
  // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
395
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
323
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
324
+ const query = params as DebugToolParams;
325
+ if (query.operationId !== undefined && !isDebugOperationId(query.operationId)) {
326
+ throw new Error("Invalid Debug Operation ID");
327
+ }
396
328
  const config = applyDebugConfig(ctx.cwd);
397
- return buildToolResult(params as DebugToolParams, config);
329
+ return buildToolResult(query, { config, cwd: ctx.cwd, signal, onUpdate });
398
330
  },
331
+ renderCall: renderDebugToolCall,
332
+ renderResult: renderDebugToolResult,
399
333
  });
400
334
  }
package/src/output.ts ADDED
@@ -0,0 +1,101 @@
1
+ import {
2
+ DEFAULT_MAX_BYTES,
3
+ DEFAULT_MAX_LINES,
4
+ formatSize,
5
+ type TruncationResult,
6
+ truncateHead,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import type { DebugEventView } from "@mrclrchtr/supi-core/debug";
9
+ import { formatDataLines } from "./format.ts";
10
+
11
+ const TRUNCATION_RESERVE_LINES = 2;
12
+ const TRUNCATION_RESERVE_BYTES = 512;
13
+
14
+ function pushFormattedData(lines: string[], label: string, value: unknown): void {
15
+ const dataLines = formatDataLines(value);
16
+ if (dataLines.length === 0) return;
17
+ if (dataLines.length === 1) {
18
+ lines.push(` ${label}: ${dataLines[0]}`);
19
+ } else {
20
+ lines.push(` ${label}:`);
21
+ for (const line of dataLines) lines.push(` ${line}`);
22
+ }
23
+ }
24
+
25
+ function formatTimestamp(timestamp: number): string {
26
+ const date = new Date(timestamp);
27
+ return Number.isNaN(date.getTime()) ? String(timestamp) : date.toISOString();
28
+ }
29
+
30
+ /** Format debug events for model-facing or command output. */
31
+ export function formatDebugEvents(
32
+ events: readonly DebugEventView[],
33
+ rawAccessDenied: boolean,
34
+ rawDataUnavailable = false,
35
+ persistedEventCount?: number,
36
+ ): string[] {
37
+ if (events.length === 0) {
38
+ return persistedEventCount === 0
39
+ ? [
40
+ "This session has no persisted debug events; sessions recorded before persistence cannot be backfilled.",
41
+ ]
42
+ : ["No matching debug events available."];
43
+ }
44
+
45
+ const lines: string[] = [];
46
+ for (const event of events) {
47
+ lines.push(
48
+ `[${formatTimestamp(event.timestamp)}] ${event.level.toUpperCase()} ${event.source}/${event.category}: ${event.message}`,
49
+ );
50
+ if (event.operationId) lines.push(` operationId: ${event.operationId}`);
51
+ if (event.cwd) lines.push(` cwd: ${event.cwd}`);
52
+ pushFormattedData(lines, "data", event.data);
53
+ pushFormattedData(lines, "rawData", event.rawData);
54
+ }
55
+ if (rawDataUnavailable) {
56
+ lines.push("", "Raw debug data is not persisted for historical sessions.");
57
+ } else if (rawAccessDenied) {
58
+ lines.push("", "Raw debug data was requested but is not enabled in SuPi Debug settings.");
59
+ }
60
+ return lines;
61
+ }
62
+
63
+ function formatTruncationNote(truncation: TruncationResult): string {
64
+ const omittedLines = truncation.totalLines - truncation.outputLines;
65
+ const omittedBytes = truncation.totalBytes - truncation.outputBytes;
66
+ return `[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ${omittedLines} lines (${formatSize(omittedBytes)}) omitted. Use filters or a smaller limit to narrow results.]`;
67
+ }
68
+
69
+ function appendTruncationNote(content: string, truncation: TruncationResult): string {
70
+ const note = formatTruncationNote(truncation);
71
+ return content.length > 0 ? `${content}\n\n${note}` : note;
72
+ }
73
+
74
+ function reserveTruncationSpace(content: string): TruncationResult {
75
+ const initial = truncateHead(content, {
76
+ maxLines: DEFAULT_MAX_LINES,
77
+ maxBytes: DEFAULT_MAX_BYTES,
78
+ });
79
+ if (!initial.truncated) return initial;
80
+
81
+ return truncateHead(content, {
82
+ maxLines: DEFAULT_MAX_LINES - TRUNCATION_RESERVE_LINES,
83
+ maxBytes: DEFAULT_MAX_BYTES - TRUNCATION_RESERVE_BYTES,
84
+ });
85
+ }
86
+
87
+ /** Limit model-visible debug output to PI's standard tool-output bounds. */
88
+ export function truncateDebugOutput(content: string): {
89
+ text: string;
90
+ truncation?: TruncationResult;
91
+ } {
92
+ const truncation = reserveTruncationSpace(content);
93
+ const text = truncation.truncated
94
+ ? appendTruncationNote(truncation.content, truncation)
95
+ : truncation.content;
96
+
97
+ return {
98
+ text,
99
+ truncation: truncation.truncated ? truncation : undefined,
100
+ };
101
+ }
package/src/query.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { type DebugEventQuery, isDebugLevel, isDebugOperationId } from "@mrclrchtr/supi-core/debug";
2
+
3
+ /** Command and Tool query with optional persisted-session selection. */
4
+ export type DebugToolParams = DebugEventQuery & { sessionFile?: string };
5
+
6
+ /** Parse exact key-value filters for the user-facing Debug command. */
7
+ export function parseDebugCommandArgs(
8
+ args: string,
9
+ normalizeLimit: (value: string) => number,
10
+ ): DebugToolParams {
11
+ const query: DebugToolParams = {};
12
+ for (const part of args.trim().split(/\s+/).filter(Boolean)) {
13
+ const [key, value] = part.split("=", 2);
14
+ if (!value) continue;
15
+ applyDebugFilter(query, key, value, normalizeLimit);
16
+ }
17
+ return query;
18
+ }
19
+
20
+ function applyDebugFilter(
21
+ query: DebugToolParams,
22
+ key: string,
23
+ value: string,
24
+ normalizeLimit: (value: string) => number,
25
+ ): void {
26
+ switch (key) {
27
+ case "operationId":
28
+ if (isDebugOperationId(value)) query.operationId = value;
29
+ return;
30
+ case "source":
31
+ query.source = value;
32
+ return;
33
+ case "category":
34
+ query.category = value;
35
+ return;
36
+ case "level":
37
+ if (isDebugLevel(value)) query.level = value;
38
+ return;
39
+ case "limit":
40
+ query.limit = normalizeLimit(value);
41
+ return;
42
+ case "sessionFile":
43
+ query.sessionFile = value;
44
+ }
45
+ }