@mrclrchtr/supi-debug 4.10.0 → 6.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,110 +1,27 @@
1
- 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";
10
- import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
4
  import { registerContextProvider } from "@mrclrchtr/supi-core/context";
12
5
  import {
13
6
  clearDebugEvents,
14
- configureDebugRegistry,
15
- DEBUG_REGISTRY_DEFAULTS,
16
- type DebugAgentAccess,
17
- type DebugEventQuery,
18
- type DebugEventView,
19
- getDebugEvents,
20
7
  getDebugSummary,
21
- isDebugLevel,
22
8
  subscribeDebugEvents,
23
9
  } from "@mrclrchtr/supi-core/debug";
24
10
  import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
25
- import { Type } from "typebox";
26
- import { formatDataLines } from "./format.ts";
11
+ import { registerDebugCommand } from "./command.ts";
12
+ import {
13
+ applyDebugConfig,
14
+ DEBUG_DEFAULTS,
15
+ DEBUG_SECTION,
16
+ normalizeMaxEvents,
17
+ syncLiveDebugRegistry,
18
+ } from "./config.ts";
27
19
  import { registerDebugMessageRenderer } from "./renderer.ts";
28
- import { DEBUG_EVENT_ENTRY_TYPE, readSessionDebugEvents } from "./session-events.ts";
20
+ import { DEBUG_EVENT_ENTRY_TYPE } from "./session-events.ts";
29
21
  import { maybeLogLoadStatus } from "./status-log.ts";
30
- import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
31
-
32
- const DEBUG_SECTION = "debug";
33
- const DEBUG_REPORT_TYPE = "supi-debug-report";
34
-
35
- interface DebugConfig extends Record<string, unknown> {
36
- enabled: boolean;
37
- agentAccess: DebugAgentAccess;
38
- maxEvents: number;
39
- }
40
-
41
- const DEBUG_DEFAULTS: DebugConfig = { ...DEBUG_REGISTRY_DEFAULTS };
22
+ import { registerDebugTool } from "./tool/debug/register.ts";
42
23
 
43
- type DebugToolParams = DebugEventQuery & { sessionFile?: string };
44
-
45
- function normalizeAgentAccess(value: string): DebugAgentAccess {
46
- return value === "off" || value === "raw" ? value : "sanitized";
47
- }
48
-
49
- function normalizeMaxEvents(value: string | number): number {
50
- const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
51
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEBUG_DEFAULTS.maxEvents;
52
- }
53
-
54
- function normalizeEnabled(value: unknown): boolean {
55
- if (typeof value === "boolean") {
56
- return value;
57
- }
58
-
59
- if (typeof value === "string") {
60
- const normalized = value.trim().toLowerCase();
61
- if (
62
- normalized === "true" ||
63
- normalized === "on" ||
64
- normalized === "1" ||
65
- normalized === "yes"
66
- ) {
67
- return true;
68
- }
69
- if (
70
- normalized === "false" ||
71
- normalized === "off" ||
72
- normalized === "0" ||
73
- normalized === "no" ||
74
- normalized === ""
75
- ) {
76
- return false;
77
- }
78
- return DEBUG_DEFAULTS.enabled;
79
- }
80
-
81
- if (value === 1) return true;
82
- if (value === 0) return false;
83
- return DEBUG_DEFAULTS.enabled;
84
- }
85
-
86
- function loadDebugConfig(cwd: string): DebugConfig {
87
- const config = loadSupiConfig(DEBUG_SECTION, cwd, DEBUG_DEFAULTS);
88
- return {
89
- enabled: normalizeEnabled(config.enabled),
90
- agentAccess: normalizeAgentAccess(String(config.agentAccess)),
91
- maxEvents: normalizeMaxEvents(config.maxEvents),
92
- };
93
- }
94
-
95
- function applyDebugConfig(cwd: string): DebugConfig {
96
- const config = loadDebugConfig(cwd);
97
- configureDebugRegistry(config);
98
- return config;
99
- }
100
-
101
- function syncLiveDebugRegistry(cwd: string): DebugConfig {
102
- const config = applyDebugConfig(cwd);
103
- if (!config.enabled) {
104
- clearDebugEvents();
105
- }
106
- return config;
107
- }
24
+ const baseDir = dirname(dirname(fileURLToPath(import.meta.url)));
108
25
 
109
26
  function registerDebugSettings(pi: ExtensionAPI): void {
110
27
  registerSettings(
@@ -143,88 +60,6 @@ function registerDebugSettings(pi: ExtensionAPI): void {
143
60
  );
144
61
  }
145
62
 
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
63
  function buildSummaryData(): Record<string, string | number> | null {
229
64
  const summary = getDebugSummary();
230
65
  if (!summary) return null;
@@ -239,60 +74,6 @@ function buildSummaryData(): Record<string, string | number> | null {
239
74
  return data;
240
75
  }
241
76
 
242
- async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
243
- if (!config.enabled && !params.sessionFile) {
244
- throw new Error(
245
- "SuPi debug event capture is disabled. Enable Debug in /supi-settings to retain events.",
246
- );
247
- }
248
-
249
- if (config.agentAccess === "off") {
250
- throw new Error("Agent access to SuPi debug events is disabled.");
251
- }
252
-
253
- const filters = {
254
- source: params.source,
255
- level: params.level,
256
- category: params.category,
257
- limit: params.limit,
258
- };
259
- const query: DebugEventQuery = {
260
- ...filters,
261
- includeRaw: params.includeRaw,
262
- allowRaw: config.agentAccess === "raw",
263
- };
264
- let events: DebugEventView[];
265
- let rawAccessDenied: boolean;
266
- let rawDataUnavailable = false;
267
- let persistedEventCount: number | undefined;
268
- if (params.sessionFile) {
269
- const persisted = await readSessionDebugEvents(params.sessionFile, filters);
270
- events = persisted.events;
271
- persistedEventCount = persisted.persistedEventCount;
272
- rawAccessDenied = Boolean(params.includeRaw);
273
- rawDataUnavailable = rawAccessDenied;
274
- } else {
275
- const result = getDebugEvents(query);
276
- events = result.events;
277
- rawAccessDenied = result.rawAccessDenied;
278
- }
279
- const output = truncateDebugOutput(
280
- formatEvents(events, rawAccessDenied, rawDataUnavailable, persistedEventCount).join("\n"),
281
- );
282
- return {
283
- 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
- },
293
- };
294
- }
295
-
296
77
  /** Register the shared SuPi debug command, settings, context summary, and agent tool. */
297
78
  export default function debugExtension(pi: ExtensionAPI) {
298
79
  applyDebugConfig(process.cwd());
@@ -315,86 +96,15 @@ export default function debugExtension(pi: ExtensionAPI) {
315
96
 
316
97
  pi.on("resources_discover", async (_event, ctx) => {
317
98
  maybeLogLoadStatus(pi, ctx.cwd, "resources_discover");
99
+ // Self-register the package prompt template so standalone installs and
100
+ // workspace-root loads expose the same `/supi-tooling-retro` surface.
101
+ return { promptPaths: [join(baseDir, "prompts")] };
318
102
  });
319
103
 
320
104
  pi.on("session_shutdown", () => {
321
105
  unsubscribeDebugEvents();
322
106
  });
323
107
 
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
- });
371
-
372
- pi.registerTool({
373
- name: "supi_debug",
374
- label: "SuPi Debug",
375
- description: toolDescription,
376
- promptSnippet,
377
- promptGuidelines,
378
- parameters: Type.Object({
379
- source: Type.Optional(Type.String({ description: "Filter by extension source, e.g. lsp" })),
380
- level: Type.Optional(
381
- StringEnum(["debug", "info", "warning", "error"], {
382
- description: "Filter by debug level",
383
- }),
384
- ),
385
- category: Type.Optional(Type.String({ description: "Filter by event category" })),
386
- limit: Type.Optional(Type.Number({ description: "Maximum number of events to return" })),
387
- sessionFile: Type.Optional(
388
- Type.String({ description: "PI session JSONL file containing persisted debug events" }),
389
- ),
390
- includeRaw: Type.Optional(
391
- Type.Boolean({ description: "Request raw event data when settings permit it" }),
392
- ),
393
- }),
394
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
395
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
396
- const config = applyDebugConfig(ctx.cwd);
397
- return buildToolResult(params as DebugToolParams, config);
398
- },
399
- });
108
+ registerDebugCommand(pi, applyDebugConfig, normalizeMaxEvents);
109
+ registerDebugTool(pi);
400
110
  }
@@ -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
+ }