@mrclrchtr/supi-debug 4.5.0 → 4.6.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/README.md +8 -6
- package/node_modules/@mrclrchtr/supi-core/README.md +1 -3
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +49 -12
- package/package.json +4 -4
- package/src/debug.ts +85 -26
- package/src/session-events.ts +93 -0
- package/src/tool/guidance.ts +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-debug">
|
|
3
|
-
<
|
|
4
|
-
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-debug/assets/logo.png" alt="SuPi" width="50%">
|
|
5
|
-
</picture>
|
|
3
|
+
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-debug/assets/social-preview.png" alt="SuPi Debug" width="100%">
|
|
6
4
|
</a>
|
|
7
5
|
</div>
|
|
8
6
|
|
|
@@ -37,6 +35,7 @@ It also registers a **Debug** provider section for `/supi-context`.
|
|
|
37
35
|
## Event behavior
|
|
38
36
|
|
|
39
37
|
- events are session-local
|
|
38
|
+
- sanitized events are also persisted in the session JSONL for later inspection
|
|
40
39
|
- the event buffer is cleared on `session_start`
|
|
41
40
|
- if debug capture is disabled, no events are retained
|
|
42
41
|
- agent-facing access is blocked, sanitized, or raw depending on settings
|
|
@@ -86,9 +85,9 @@ Both `/supi-debug` and `supi_debug` support the same basic filters:
|
|
|
86
85
|
- `category`
|
|
87
86
|
- `limit`
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
For historical sessions, pass `sessionFile` to `supi_debug`, or
|
|
89
|
+
`sessionFile=<path>` to `/supi-debug`. Historical sessions never retain raw data.
|
|
90
|
+
The tool also accepts `includeRaw` for live-session data when settings allow it.
|
|
92
91
|
|
|
93
92
|
## Settings
|
|
94
93
|
|
|
@@ -100,6 +99,9 @@ Available settings:
|
|
|
100
99
|
- `agentAccess` — `off`, `sanitized`, or `raw`
|
|
101
100
|
- `maxEvents` — maximum retained events in memory
|
|
102
101
|
|
|
102
|
+
Historical inspection works for events captured after this version is loaded. For example, an
|
|
103
|
+
agent can call `supi_debug` with `sessionFile` set to a PI session JSONL path.
|
|
104
|
+
|
|
103
105
|
Defaults come from the shared debug registry:
|
|
104
106
|
|
|
105
107
|
```json
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
|
|
3
|
-
<
|
|
4
|
-
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/logo.png" alt="SuPi" width="50%">
|
|
5
|
-
</picture>
|
|
3
|
+
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/social-preview.png" alt="SuPi Core" width="100%">
|
|
6
4
|
</a>
|
|
7
5
|
</div>
|
|
8
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.6.0",
|
|
4
|
+
"description": "Shared settings, configuration, reporting, and session infrastructure",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -64,6 +64,9 @@ export interface DebugEventQueryResult {
|
|
|
64
64
|
rawAccessDenied: boolean;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Receives a sanitized event whenever the registry records one. */
|
|
68
|
+
export type DebugEventListener = (event: DebugEventView) => void;
|
|
69
|
+
|
|
67
70
|
export interface DebugSummary {
|
|
68
71
|
total: number;
|
|
69
72
|
byLevel: Partial<Record<DebugLevel, number>>;
|
|
@@ -73,6 +76,7 @@ export interface DebugSummary {
|
|
|
73
76
|
interface DebugRegistryState {
|
|
74
77
|
config: DebugRegistryConfig;
|
|
75
78
|
events: DebugEvent[];
|
|
79
|
+
listeners: Set<DebugEventListener>;
|
|
76
80
|
nextId: number;
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -97,10 +101,14 @@ function getState(): DebugRegistryState {
|
|
|
97
101
|
state = {
|
|
98
102
|
config: cloneConfig(DEBUG_REGISTRY_DEFAULTS),
|
|
99
103
|
events: [],
|
|
104
|
+
listeners: new Set(),
|
|
100
105
|
nextId: 1,
|
|
101
106
|
};
|
|
102
107
|
(globalThis as Record<symbol, unknown>)[REGISTRY_KEY] = state;
|
|
103
108
|
}
|
|
109
|
+
// Keep the shared registry compatible with extension reloads that reuse an
|
|
110
|
+
// instance created before listeners existed.
|
|
111
|
+
if (!state.listeners) state.listeners = new Set();
|
|
104
112
|
return state;
|
|
105
113
|
}
|
|
106
114
|
|
|
@@ -119,7 +127,16 @@ function trimToMaxEvents(state: DebugRegistryState): void {
|
|
|
119
127
|
state.events.splice(0, state.events.length - maxEvents);
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
/** Return whether a debug level is recognized by the registry. */
|
|
131
|
+
export function isDebugLevel(value: unknown): value is DebugLevel {
|
|
132
|
+
return value === "debug" || value === "info" || value === "warning" || value === "error";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Match a debug event against the supported source, level, and category filters. */
|
|
136
|
+
export function matchesDebugEventQuery(
|
|
137
|
+
event: Pick<DebugEventView, "source" | "level" | "category">,
|
|
138
|
+
query: Pick<DebugEventQuery, "source" | "level" | "category">,
|
|
139
|
+
): boolean {
|
|
123
140
|
if (query.source && event.source !== query.source) return false;
|
|
124
141
|
if (query.level && event.level !== query.level) return false;
|
|
125
142
|
if (query.category && event.category !== query.category) return false;
|
|
@@ -168,6 +185,26 @@ export function redactDebugData<T>(value: T): T {
|
|
|
168
185
|
return redactValue(value, 8) as T;
|
|
169
186
|
}
|
|
170
187
|
|
|
188
|
+
function toSanitizedView(event: DebugEvent): DebugEventView {
|
|
189
|
+
return {
|
|
190
|
+
id: event.id,
|
|
191
|
+
timestamp: event.timestamp,
|
|
192
|
+
source: event.source,
|
|
193
|
+
level: event.level,
|
|
194
|
+
category: event.category,
|
|
195
|
+
message: event.message,
|
|
196
|
+
cwd: event.cwd,
|
|
197
|
+
data: event.data,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Subscribe to sanitized events. Listeners are isolated so diagnostics cannot disrupt producers. */
|
|
202
|
+
export function subscribeDebugEvents(listener: DebugEventListener): () => void {
|
|
203
|
+
const state = getState();
|
|
204
|
+
state.listeners.add(listener);
|
|
205
|
+
return () => state.listeners.delete(listener);
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
/** Record a session-local debug event if debugging is enabled. */
|
|
172
209
|
export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
173
210
|
const state = getState();
|
|
@@ -183,6 +220,14 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
|
183
220
|
};
|
|
184
221
|
state.events.push(event);
|
|
185
222
|
trimToMaxEvents(state);
|
|
223
|
+
const view = toSanitizedView(event);
|
|
224
|
+
for (const listener of state.listeners) {
|
|
225
|
+
try {
|
|
226
|
+
listener(view);
|
|
227
|
+
} catch {
|
|
228
|
+
// Debug-event consumers must not alter producer behavior.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
186
231
|
return { ...event };
|
|
187
232
|
}
|
|
188
233
|
|
|
@@ -196,21 +241,12 @@ export function getDebugEvents(query: DebugEventQuery = {}): DebugEventQueryResu
|
|
|
196
241
|
const limit = query.limit && query.limit > 0 ? Math.floor(query.limit) : state.config.maxEvents;
|
|
197
242
|
|
|
198
243
|
const events = state.events
|
|
199
|
-
.filter((event) =>
|
|
244
|
+
.filter((event) => matchesDebugEventQuery(event, query))
|
|
200
245
|
.slice()
|
|
201
246
|
.reverse()
|
|
202
247
|
.slice(0, limit)
|
|
203
248
|
.map((event): DebugEventView => {
|
|
204
|
-
const view
|
|
205
|
-
id: event.id,
|
|
206
|
-
timestamp: event.timestamp,
|
|
207
|
-
source: event.source,
|
|
208
|
-
level: event.level,
|
|
209
|
-
category: event.category,
|
|
210
|
-
message: event.message,
|
|
211
|
-
cwd: event.cwd,
|
|
212
|
-
data: event.data,
|
|
213
|
-
};
|
|
249
|
+
const view = toSanitizedView(event);
|
|
214
250
|
if (allowRaw && event.rawData !== undefined) {
|
|
215
251
|
view.rawData = event.rawData;
|
|
216
252
|
}
|
|
@@ -246,5 +282,6 @@ export function resetDebugRegistry(): void {
|
|
|
246
282
|
const state = getState();
|
|
247
283
|
state.config = cloneConfig(DEBUG_REGISTRY_DEFAULTS);
|
|
248
284
|
state.events = [];
|
|
285
|
+
state.listeners.clear();
|
|
249
286
|
state.nextId = 1;
|
|
250
287
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-debug",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.6.0",
|
|
4
|
+
"description": "Capture and inspect SuPi debug events",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mrclrchtr/supi-core": "4.
|
|
34
|
+
"@mrclrchtr/supi-core": "4.6.0"
|
|
35
35
|
},
|
|
36
36
|
"bundledDependencies": [
|
|
37
37
|
"@mrclrchtr/supi-core"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"extensions": [
|
|
61
61
|
"./src/extension.ts"
|
|
62
62
|
],
|
|
63
|
-
"image": "https://raw.githubusercontent.com/mrclrchtr/supi/main/
|
|
63
|
+
"image": "https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-debug/assets/social-preview.png"
|
|
64
64
|
},
|
|
65
65
|
"main": "src/api.ts",
|
|
66
66
|
"exports": {
|
package/src/debug.ts
CHANGED
|
@@ -16,14 +16,16 @@ import {
|
|
|
16
16
|
type DebugAgentAccess,
|
|
17
17
|
type DebugEventQuery,
|
|
18
18
|
type DebugEventView,
|
|
19
|
-
type DebugLevel,
|
|
20
19
|
getDebugEvents,
|
|
21
20
|
getDebugSummary,
|
|
21
|
+
isDebugLevel,
|
|
22
|
+
subscribeDebugEvents,
|
|
22
23
|
} from "@mrclrchtr/supi-core/debug";
|
|
23
24
|
import { registerDeclarativeSettings } from "@mrclrchtr/supi-core/settings";
|
|
24
25
|
import { Type } from "typebox";
|
|
25
26
|
import { formatDataLines } from "./format.ts";
|
|
26
27
|
import { registerDebugMessageRenderer } from "./renderer.ts";
|
|
28
|
+
import { DEBUG_EVENT_ENTRY_TYPE, readSessionDebugEvents } from "./session-events.ts";
|
|
27
29
|
import { maybeLogLoadStatus } from "./status-log.ts";
|
|
28
30
|
import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
|
|
29
31
|
|
|
@@ -38,7 +40,7 @@ interface DebugConfig extends Record<string, unknown> {
|
|
|
38
40
|
|
|
39
41
|
const DEBUG_DEFAULTS: DebugConfig = { ...DEBUG_REGISTRY_DEFAULTS };
|
|
40
42
|
|
|
41
|
-
type DebugToolParams = DebugEventQuery;
|
|
43
|
+
type DebugToolParams = DebugEventQuery & { sessionFile?: string };
|
|
42
44
|
|
|
43
45
|
function normalizeAgentAccess(value: string): DebugAgentAccess {
|
|
44
46
|
return value === "off" || value === "raw" ? value : "sanitized";
|
|
@@ -138,8 +140,8 @@ function registerDebugSettings(pi: ExtensionAPI): void {
|
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
function parseCommandArgs(args: string):
|
|
142
|
-
const query:
|
|
143
|
+
function parseCommandArgs(args: string): DebugToolParams {
|
|
144
|
+
const query: DebugToolParams = {};
|
|
143
145
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
144
146
|
for (const part of parts) {
|
|
145
147
|
const [key, value] = part.split("=", 2);
|
|
@@ -148,14 +150,11 @@ function parseCommandArgs(args: string): DebugEventQuery {
|
|
|
148
150
|
if (key === "category") query.category = value;
|
|
149
151
|
if (key === "level" && isDebugLevel(value)) query.level = value;
|
|
150
152
|
if (key === "limit") query.limit = normalizeMaxEvents(value);
|
|
153
|
+
if (key === "sessionFile") query.sessionFile = value;
|
|
151
154
|
}
|
|
152
155
|
return query;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
function isDebugLevel(value: string): value is DebugLevel {
|
|
156
|
-
return value === "debug" || value === "info" || value === "warning" || value === "error";
|
|
157
|
-
}
|
|
158
|
-
|
|
159
158
|
function pushFormattedData(lines: string[], label: string, value: unknown): void {
|
|
160
159
|
const dataLines = formatDataLines(value);
|
|
161
160
|
if (dataLines.length === 0) return;
|
|
@@ -169,9 +168,18 @@ function pushFormattedData(lines: string[], label: string, value: unknown): void
|
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
function formatEvents(
|
|
171
|
+
function formatEvents(
|
|
172
|
+
events: DebugEventView[],
|
|
173
|
+
rawAccessDenied: boolean,
|
|
174
|
+
rawDataUnavailable = false,
|
|
175
|
+
persistedEventCount?: number,
|
|
176
|
+
): string[] {
|
|
173
177
|
if (events.length === 0) {
|
|
174
|
-
return
|
|
178
|
+
return persistedEventCount === 0
|
|
179
|
+
? [
|
|
180
|
+
"This session has no persisted debug events; sessions recorded before persistence cannot be backfilled.",
|
|
181
|
+
]
|
|
182
|
+
: ["No matching debug events available."];
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
const lines: string[] = [];
|
|
@@ -183,7 +191,10 @@ function formatEvents(events: DebugEventView[], rawAccessDenied: boolean): strin
|
|
|
183
191
|
pushFormattedData(lines, "data", event.data);
|
|
184
192
|
pushFormattedData(lines, "rawData", event.rawData);
|
|
185
193
|
}
|
|
186
|
-
if (
|
|
194
|
+
if (rawDataUnavailable) {
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("Raw debug data is not persisted for historical sessions.");
|
|
197
|
+
} else if (rawAccessDenied) {
|
|
187
198
|
lines.push("");
|
|
188
199
|
lines.push("Raw debug data was requested but is not enabled in SuPi Debug settings.");
|
|
189
200
|
}
|
|
@@ -225,40 +236,55 @@ function buildSummaryData(): Record<string, string | number> | null {
|
|
|
225
236
|
return data;
|
|
226
237
|
}
|
|
227
238
|
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function buildToolResult(params: DebugToolParams, config: DebugConfig) {
|
|
233
|
-
if (!config.enabled) {
|
|
239
|
+
async function buildToolResult(params: DebugToolParams, config: DebugConfig) {
|
|
240
|
+
if (!config.enabled && !params.sessionFile) {
|
|
234
241
|
throw new Error(
|
|
235
242
|
"SuPi debug event capture is disabled. Enable Debug in /supi-settings to retain events.",
|
|
236
243
|
);
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
if (
|
|
246
|
+
if (config.agentAccess === "off") {
|
|
240
247
|
throw new Error("Agent access to SuPi debug events is disabled.");
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
const
|
|
250
|
+
const filters = {
|
|
244
251
|
source: params.source,
|
|
245
252
|
level: params.level,
|
|
246
253
|
category: params.category,
|
|
247
254
|
limit: params.limit,
|
|
255
|
+
};
|
|
256
|
+
const query: DebugEventQuery = {
|
|
257
|
+
...filters,
|
|
248
258
|
includeRaw: params.includeRaw,
|
|
249
259
|
allowRaw: config.agentAccess === "raw",
|
|
250
260
|
};
|
|
251
|
-
|
|
261
|
+
let events: DebugEventView[];
|
|
262
|
+
let rawAccessDenied: boolean;
|
|
263
|
+
let rawDataUnavailable = false;
|
|
264
|
+
let persistedEventCount: number | undefined;
|
|
265
|
+
if (params.sessionFile) {
|
|
266
|
+
const persisted = await readSessionDebugEvents(params.sessionFile, filters);
|
|
267
|
+
events = persisted.events;
|
|
268
|
+
persistedEventCount = persisted.persistedEventCount;
|
|
269
|
+
rawAccessDenied = Boolean(params.includeRaw);
|
|
270
|
+
rawDataUnavailable = rawAccessDenied;
|
|
271
|
+
} else {
|
|
272
|
+
const result = getDebugEvents(query);
|
|
273
|
+
events = result.events;
|
|
274
|
+
rawAccessDenied = result.rawAccessDenied;
|
|
275
|
+
}
|
|
252
276
|
const output = truncateDebugOutput(
|
|
253
|
-
formatEvents(
|
|
277
|
+
formatEvents(events, rawAccessDenied, rawDataUnavailable, persistedEventCount).join("\n"),
|
|
254
278
|
);
|
|
255
279
|
return {
|
|
256
280
|
content: [{ type: "text" as const, text: output.text }],
|
|
257
281
|
details: {
|
|
258
|
-
enabled:
|
|
282
|
+
enabled: config.enabled,
|
|
259
283
|
agentAccess: config.agentAccess,
|
|
260
|
-
|
|
261
|
-
|
|
284
|
+
sessionFile: params.sessionFile,
|
|
285
|
+
rawAccessDenied,
|
|
286
|
+
rawDataUnavailable,
|
|
287
|
+
events,
|
|
262
288
|
truncation: output.truncation,
|
|
263
289
|
},
|
|
264
290
|
};
|
|
@@ -269,6 +295,9 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
269
295
|
applyDebugConfig(process.cwd());
|
|
270
296
|
registerDebugSettings(pi);
|
|
271
297
|
registerDebugMessageRenderer(pi);
|
|
298
|
+
const unsubscribeDebugEvents = subscribeDebugEvents((event) => {
|
|
299
|
+
pi.appendEntry(DEBUG_EVENT_ENTRY_TYPE, event);
|
|
300
|
+
});
|
|
272
301
|
|
|
273
302
|
registerContextProvider({
|
|
274
303
|
id: "debug",
|
|
@@ -287,11 +316,16 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
287
316
|
maybeLogLoadStatus(pi, ctx.cwd, "resources_discover");
|
|
288
317
|
});
|
|
289
318
|
|
|
319
|
+
pi.on("session_shutdown", () => {
|
|
320
|
+
unsubscribeDebugEvents();
|
|
321
|
+
});
|
|
322
|
+
|
|
290
323
|
pi.registerCommand("supi-debug", {
|
|
291
324
|
description: "Show recent SuPi debug events",
|
|
292
325
|
handler: async (args, ctx) => {
|
|
293
326
|
const config = applyDebugConfig(ctx.cwd);
|
|
294
|
-
|
|
327
|
+
const query = parseCommandArgs(args);
|
|
328
|
+
if (!config.enabled && !query.sessionFile) {
|
|
295
329
|
pi.sendMessage({
|
|
296
330
|
customType: DEBUG_REPORT_TYPE,
|
|
297
331
|
content: "SuPi debug event capture is disabled. Enable Debug in /supi-settings.",
|
|
@@ -300,7 +334,29 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
300
334
|
return;
|
|
301
335
|
}
|
|
302
336
|
|
|
303
|
-
|
|
337
|
+
if (query.sessionFile) {
|
|
338
|
+
const persisted = await readSessionDebugEvents(query.sessionFile, {
|
|
339
|
+
source: query.source,
|
|
340
|
+
level: query.level,
|
|
341
|
+
category: query.category,
|
|
342
|
+
limit: query.limit,
|
|
343
|
+
});
|
|
344
|
+
const output = truncateDebugOutput(
|
|
345
|
+
formatEvents(persisted.events, false, false, persisted.persistedEventCount).join("\n"),
|
|
346
|
+
);
|
|
347
|
+
pi.sendMessage({
|
|
348
|
+
customType: DEBUG_REPORT_TYPE,
|
|
349
|
+
content: output.text,
|
|
350
|
+
display: true,
|
|
351
|
+
details: {
|
|
352
|
+
sessionFile: query.sessionFile,
|
|
353
|
+
events: persisted.events,
|
|
354
|
+
truncation: output.truncation,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
304
360
|
const { events, rawAccessDenied } = getDebugEvents(query);
|
|
305
361
|
const output = truncateDebugOutput(formatEvents(events, rawAccessDenied).join("\n"));
|
|
306
362
|
pi.sendMessage({
|
|
@@ -327,6 +383,9 @@ export default function debugExtension(pi: ExtensionAPI) {
|
|
|
327
383
|
),
|
|
328
384
|
category: Type.Optional(Type.String({ description: "Filter by event category" })),
|
|
329
385
|
limit: Type.Optional(Type.Number({ description: "Maximum number of events to return" })),
|
|
386
|
+
sessionFile: Type.Optional(
|
|
387
|
+
Type.String({ description: "PI session JSONL file containing persisted debug events" }),
|
|
388
|
+
),
|
|
330
389
|
includeRaw: Type.Optional(
|
|
331
390
|
Type.Boolean({ description: "Request raw event data when settings permit it" }),
|
|
332
391
|
),
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import {
|
|
4
|
+
type DebugEventQuery,
|
|
5
|
+
type DebugEventView,
|
|
6
|
+
isDebugLevel,
|
|
7
|
+
matchesDebugEventQuery,
|
|
8
|
+
redactDebugData,
|
|
9
|
+
} from "@mrclrchtr/supi-core/debug";
|
|
10
|
+
|
|
11
|
+
/** Custom session-entry type used for sanitized debug-event persistence. */
|
|
12
|
+
export const DEBUG_EVENT_ENTRY_TYPE = "supi-debug-event";
|
|
13
|
+
|
|
14
|
+
type PersistedDebugEventQuery = Pick<DebugEventQuery, "source" | "level" | "category" | "limit">;
|
|
15
|
+
|
|
16
|
+
/** Sanitized events and total persisted entries found in one PI session file. */
|
|
17
|
+
export interface SessionDebugEvents {
|
|
18
|
+
events: DebugEventView[];
|
|
19
|
+
persistedEventCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parsePersistedEvent(data: unknown): DebugEventView | undefined {
|
|
23
|
+
if (typeof data !== "object" || data === null) return undefined;
|
|
24
|
+
const event = data as Record<string, unknown>;
|
|
25
|
+
if (
|
|
26
|
+
typeof event.id !== "number" ||
|
|
27
|
+
!Number.isFinite(event.id) ||
|
|
28
|
+
typeof event.timestamp !== "number" ||
|
|
29
|
+
!Number.isFinite(event.timestamp) ||
|
|
30
|
+
typeof event.source !== "string" ||
|
|
31
|
+
!isDebugLevel(event.level) ||
|
|
32
|
+
typeof event.category !== "string" ||
|
|
33
|
+
typeof event.message !== "string" ||
|
|
34
|
+
(event.cwd !== undefined && typeof event.cwd !== "string")
|
|
35
|
+
) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: event.id,
|
|
41
|
+
timestamp: event.timestamp,
|
|
42
|
+
source: event.source,
|
|
43
|
+
level: event.level,
|
|
44
|
+
category: event.category,
|
|
45
|
+
message: event.message,
|
|
46
|
+
cwd: event.cwd,
|
|
47
|
+
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseDebugEntry(line: string): unknown {
|
|
52
|
+
if (!line.includes('"customType"')) return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(line);
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read sanitized debug events persisted by SuPi Debug from a PI session file. */
|
|
61
|
+
export async function readSessionDebugEvents(
|
|
62
|
+
sessionFile: string,
|
|
63
|
+
query: PersistedDebugEventQuery = {},
|
|
64
|
+
): Promise<SessionDebugEvents> {
|
|
65
|
+
const events: DebugEventView[] = [];
|
|
66
|
+
let persistedEventCount = 0;
|
|
67
|
+
const lines = createInterface({
|
|
68
|
+
input: createReadStream(sessionFile, { encoding: "utf8" }),
|
|
69
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
for await (const line of lines) {
|
|
73
|
+
const entry = parseDebugEntry(line);
|
|
74
|
+
if (
|
|
75
|
+
typeof entry !== "object" ||
|
|
76
|
+
entry === null ||
|
|
77
|
+
(entry as Record<string, unknown>).type !== "custom" ||
|
|
78
|
+
(entry as Record<string, unknown>).customType !== DEBUG_EVENT_ENTRY_TYPE
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
persistedEventCount++;
|
|
84
|
+
const event = parsePersistedEvent((entry as Record<string, unknown>).data);
|
|
85
|
+
if (event && matchesDebugEventQuery(event, query)) events.push(event);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const limit = query.limit && query.limit > 0 ? Math.floor(query.limit) : Number.POSITIVE_INFINITY;
|
|
89
|
+
return {
|
|
90
|
+
events: events.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit),
|
|
91
|
+
persistedEventCount,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/tool/guidance.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
export const toolDescription = `Fetch recent
|
|
5
|
+
export const toolDescription = `Fetch recent SuPi debug events, or sanitized persisted events from a PI session JSONL via sessionFile, with optional filters. Raw data is available only for the live session when allowed. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first).`;
|
|
6
6
|
|
|
7
|
-
export const promptSnippet = "supi_debug — fetch
|
|
7
|
+
export const promptSnippet = "supi_debug — fetch live or persisted SuPi debug events";
|
|
8
8
|
|
|
9
9
|
export const promptGuidelines = [
|
|
10
|
-
"Use supi_debug for SuPi failures, fallback reasons, or
|
|
10
|
+
"Use supi_debug for SuPi failures, fallback reasons, or session debug events; pass sessionFile to inspect a prior session and request raw data only when explicitly asked and settings allow it.",
|
|
11
11
|
];
|