gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.49d972f
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/cli.js +15 -9
- package/dist/resource-loader.js +80 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +9 -4
- package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/dist/resources/extensions/gsd/auto-start.ts +25 -10
- package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/dist/resources/extensions/gsd/auto.ts +67 -22
- package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
- package/dist/resources/extensions/gsd/commands.ts +22 -28
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
- package/dist/resources/extensions/gsd/doctor.ts +2 -6
- package/dist/resources/extensions/gsd/export.ts +28 -2
- package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
- package/dist/resources/extensions/gsd/index.ts +2 -1
- package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
- package/dist/resources/extensions/gsd/metrics.ts +17 -31
- package/dist/resources/extensions/gsd/paths.ts +0 -8
- package/dist/resources/extensions/gsd/queue-order.ts +10 -11
- package/dist/resources/extensions/gsd/routing-history.ts +13 -17
- package/dist/resources/extensions/gsd/session-lock.ts +284 -0
- package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/dist/resources/extensions/gsd/types.ts +1 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
- package/dist/resources/extensions/remote-questions/notify.ts +1 -2
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/dist/resources/extensions/remote-questions/types.ts +3 -0
- package/dist/resources/extensions/shared/mod.ts +3 -0
- package/package.json +6 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +14 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +19 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +9 -4
- package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/src/resources/extensions/gsd/auto-start.ts +25 -10
- package/src/resources/extensions/gsd/auto-verification.ts +41 -7
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/src/resources/extensions/gsd/auto.ts +67 -22
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/src/resources/extensions/gsd/commands-logs.ts +536 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
- package/src/resources/extensions/gsd/commands.ts +22 -28
- package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/src/resources/extensions/gsd/doctor-types.ts +13 -0
- package/src/resources/extensions/gsd/doctor.ts +2 -6
- package/src/resources/extensions/gsd/export.ts +28 -2
- package/src/resources/extensions/gsd/gsd-db.ts +19 -0
- package/src/resources/extensions/gsd/index.ts +2 -1
- package/src/resources/extensions/gsd/json-persistence.ts +67 -0
- package/src/resources/extensions/gsd/metrics.ts +17 -31
- package/src/resources/extensions/gsd/paths.ts +0 -8
- package/src/resources/extensions/gsd/queue-order.ts +10 -11
- package/src/resources/extensions/gsd/routing-history.ts +13 -17
- package/src/resources/extensions/gsd/session-lock.ts +284 -0
- package/src/resources/extensions/gsd/session-status-io.ts +23 -41
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/src/resources/extensions/gsd/verification-gate.ts +13 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/src/resources/extensions/remote-questions/http-client.ts +76 -0
- package/src/resources/extensions/remote-questions/notify.ts +1 -2
- package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/src/resources/extensions/remote-questions/types.ts +3 -0
- package/src/resources/extensions/shared/mod.ts +3 -0
- package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/dist/resources/extensions/shared/progress-widget.ts +0 -282
- package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
- package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/src/resources/extensions/shared/progress-widget.ts +0 -282
- package/src/resources/extensions/shared/thinking-widget.ts +0 -107
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /gsd logs — Browse activity logs, debug logs, and metrics.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* /gsd logs — List recent activity + debug logs
|
|
6
|
+
* /gsd logs <N> — Show summary of activity log #N
|
|
7
|
+
* /gsd logs debug — List debug log files
|
|
8
|
+
* /gsd logs debug <N> — Show debug log summary #N
|
|
9
|
+
* /gsd logs tail [N] — Show last N activity log entries (default 5)
|
|
10
|
+
* /gsd logs clear — Remove old activity and debug logs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { gsdRoot } from "./paths.js";
|
|
17
|
+
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
18
|
+
|
|
19
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface LogEntry {
|
|
22
|
+
seq: number;
|
|
23
|
+
filename: string;
|
|
24
|
+
unitType: string;
|
|
25
|
+
unitId: string;
|
|
26
|
+
size: number;
|
|
27
|
+
mtime: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DebugLogEntry {
|
|
31
|
+
filename: string;
|
|
32
|
+
size: number;
|
|
33
|
+
mtime: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function activityDir(basePath: string): string {
|
|
39
|
+
return join(gsdRoot(basePath), "activity");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function debugDir(basePath: string): string {
|
|
43
|
+
return join(gsdRoot(basePath), "debug");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listActivityLogs(basePath: string): LogEntry[] {
|
|
47
|
+
const dir = activityDir(basePath);
|
|
48
|
+
if (!existsSync(dir)) return [];
|
|
49
|
+
|
|
50
|
+
const entries: LogEntry[] = [];
|
|
51
|
+
try {
|
|
52
|
+
for (const f of readdirSync(dir)) {
|
|
53
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
54
|
+
// Filename format: {seq}-{unitType}-{unitId}.jsonl
|
|
55
|
+
// unitType is lowercase-with-hyphens (e.g., "execute-task", "complete-slice")
|
|
56
|
+
// unitId starts with M followed by digits (e.g., "M001-S01-T01")
|
|
57
|
+
const match = f.match(/^(\d+)-([\w-]+?)-(M\d[\w-]*)\.jsonl$/);
|
|
58
|
+
if (!match) continue;
|
|
59
|
+
|
|
60
|
+
const filePath = join(dir, f);
|
|
61
|
+
let stat;
|
|
62
|
+
try { stat = statSync(filePath); } catch { continue; }
|
|
63
|
+
|
|
64
|
+
entries.push({
|
|
65
|
+
seq: parseInt(match[1], 10),
|
|
66
|
+
filename: f,
|
|
67
|
+
unitType: match[2],
|
|
68
|
+
unitId: match[3].replace(/-/g, "/"),
|
|
69
|
+
size: stat.size,
|
|
70
|
+
mtime: stat.mtime,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
} catch { /* dir not readable */ }
|
|
74
|
+
|
|
75
|
+
return entries.sort((a, b) => a.seq - b.seq);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listDebugLogs(basePath: string): DebugLogEntry[] {
|
|
79
|
+
const dir = debugDir(basePath);
|
|
80
|
+
if (!existsSync(dir)) return [];
|
|
81
|
+
|
|
82
|
+
const entries: DebugLogEntry[] = [];
|
|
83
|
+
try {
|
|
84
|
+
for (const f of readdirSync(dir)) {
|
|
85
|
+
if (!f.endsWith(".log")) continue;
|
|
86
|
+
const filePath = join(dir, f);
|
|
87
|
+
let stat;
|
|
88
|
+
try { stat = statSync(filePath); } catch { continue; }
|
|
89
|
+
entries.push({ filename: f, size: stat.size, mtime: stat.mtime });
|
|
90
|
+
}
|
|
91
|
+
} catch { /* dir not readable */ }
|
|
92
|
+
|
|
93
|
+
return entries.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatSize(bytes: number): string {
|
|
97
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
98
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
99
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatAge(date: Date): string {
|
|
103
|
+
const ms = Date.now() - date.getTime();
|
|
104
|
+
const mins = Math.floor(ms / 60_000);
|
|
105
|
+
if (mins < 1) return "just now";
|
|
106
|
+
if (mins < 60) return `${mins}m ago`;
|
|
107
|
+
const hrs = Math.floor(mins / 60);
|
|
108
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
109
|
+
const days = Math.floor(hrs / 24);
|
|
110
|
+
return `${days}d ago`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract a summary from an activity log JSONL file.
|
|
115
|
+
* Parses the entries to count tool calls, errors, and extract key events.
|
|
116
|
+
*/
|
|
117
|
+
function summarizeActivityLog(filePath: string): {
|
|
118
|
+
toolCalls: number;
|
|
119
|
+
errors: number;
|
|
120
|
+
filesWritten: string[];
|
|
121
|
+
commandsRun: Array<{ command: string; failed: boolean }>;
|
|
122
|
+
lastReasoning: string;
|
|
123
|
+
entryCount: number;
|
|
124
|
+
} {
|
|
125
|
+
const result = {
|
|
126
|
+
toolCalls: 0,
|
|
127
|
+
errors: 0,
|
|
128
|
+
filesWritten: new Set<string>(),
|
|
129
|
+
commandsRun: [] as Array<{ command: string; failed: boolean }>,
|
|
130
|
+
lastReasoning: "",
|
|
131
|
+
entryCount: 0,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
let raw: string;
|
|
135
|
+
try { raw = readFileSync(filePath, "utf-8"); } catch { return { ...result, filesWritten: [] }; }
|
|
136
|
+
|
|
137
|
+
const lines = raw.split("\n").filter(l => l.trim());
|
|
138
|
+
result.entryCount = lines.length;
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
let entry: Record<string, unknown>;
|
|
142
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
143
|
+
|
|
144
|
+
// Count tool calls
|
|
145
|
+
if (entry.type === "toolCall" || (entry.role === "assistant" && entry.content && Array.isArray(entry.content))) {
|
|
146
|
+
if (entry.type === "toolCall") {
|
|
147
|
+
result.toolCalls++;
|
|
148
|
+
const name = entry.name as string | undefined;
|
|
149
|
+
const args = entry.arguments as Record<string, unknown> | undefined;
|
|
150
|
+
|
|
151
|
+
if (name === "write" || name === "edit") {
|
|
152
|
+
const path = args?.file_path as string | undefined;
|
|
153
|
+
if (path) result.filesWritten.add(path);
|
|
154
|
+
}
|
|
155
|
+
if (name === "bash") {
|
|
156
|
+
const cmd = args?.command as string | undefined;
|
|
157
|
+
if (cmd) result.commandsRun.push({ command: cmd.slice(0, 80), failed: false });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Count errors
|
|
163
|
+
if (entry.role === "toolResult" && entry.isError) {
|
|
164
|
+
result.errors++;
|
|
165
|
+
// Mark last command as failed
|
|
166
|
+
if (result.commandsRun.length > 0) {
|
|
167
|
+
result.commandsRun[result.commandsRun.length - 1].failed = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Track assistant reasoning
|
|
172
|
+
if (entry.role === "assistant" && typeof entry.content === "string") {
|
|
173
|
+
result.lastReasoning = entry.content.slice(0, 200);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...result,
|
|
179
|
+
filesWritten: [...result.filesWritten],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract summary events from a debug log file.
|
|
185
|
+
*/
|
|
186
|
+
function summarizeDebugLog(filePath: string): {
|
|
187
|
+
events: number;
|
|
188
|
+
duration: string;
|
|
189
|
+
dispatches: number;
|
|
190
|
+
errors: Array<{ event: string; message: string }>;
|
|
191
|
+
} {
|
|
192
|
+
const result = {
|
|
193
|
+
events: 0,
|
|
194
|
+
duration: "unknown",
|
|
195
|
+
dispatches: 0,
|
|
196
|
+
errors: [] as Array<{ event: string; message: string }>,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let raw: string;
|
|
200
|
+
try { raw = readFileSync(filePath, "utf-8"); } catch { return result; }
|
|
201
|
+
|
|
202
|
+
const lines = raw.split("\n").filter(l => l.trim());
|
|
203
|
+
result.events = lines.length;
|
|
204
|
+
|
|
205
|
+
let firstTs = 0;
|
|
206
|
+
let lastTs = 0;
|
|
207
|
+
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
let entry: Record<string, unknown>;
|
|
210
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
211
|
+
|
|
212
|
+
const ts = entry.ts as string | undefined;
|
|
213
|
+
if (ts) {
|
|
214
|
+
const t = new Date(ts).getTime();
|
|
215
|
+
if (!firstTs) firstTs = t;
|
|
216
|
+
lastTs = t;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const event = entry.event as string | undefined;
|
|
220
|
+
if (!event) continue;
|
|
221
|
+
|
|
222
|
+
if (event === "debug-summary") {
|
|
223
|
+
result.dispatches = (entry.dispatches as number) ?? 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (event.includes("error") || event.includes("failed")) {
|
|
227
|
+
const msg = (entry.error as string) ?? (entry.message as string) ?? JSON.stringify(entry).slice(0, 100);
|
|
228
|
+
result.errors.push({ event, message: msg });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (firstTs && lastTs) {
|
|
233
|
+
const elapsed = lastTs - firstTs;
|
|
234
|
+
const mins = Math.floor(elapsed / 60_000);
|
|
235
|
+
if (mins < 1) result.duration = `${Math.floor(elapsed / 1000)}s`;
|
|
236
|
+
else if (mins < 60) result.duration = `${mins}m`;
|
|
237
|
+
else result.duration = `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Main Handler ───────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
export async function handleLogs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
246
|
+
const basePath = process.cwd();
|
|
247
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
248
|
+
const subCmd = parts[0] ?? "";
|
|
249
|
+
|
|
250
|
+
// /gsd logs clear
|
|
251
|
+
if (subCmd === "clear") {
|
|
252
|
+
await handleLogsClear(basePath, ctx);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// /gsd logs debug [N]
|
|
257
|
+
if (subCmd === "debug") {
|
|
258
|
+
const idx = parts[1] ? parseInt(parts[1], 10) : undefined;
|
|
259
|
+
await handleLogsDebug(basePath, ctx, idx);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// /gsd logs tail [N]
|
|
264
|
+
if (subCmd === "tail") {
|
|
265
|
+
const count = parts[1] ? parseInt(parts[1], 10) : 5;
|
|
266
|
+
await handleLogsTail(basePath, ctx, count);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// /gsd logs <N> — show specific activity log
|
|
271
|
+
if (subCmd && /^\d+$/.test(subCmd)) {
|
|
272
|
+
const seq = parseInt(subCmd, 10);
|
|
273
|
+
await handleLogsShow(basePath, ctx, seq);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// /gsd logs — list overview
|
|
278
|
+
await handleLogsList(basePath, ctx);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Subcommand Handlers ────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
284
|
+
const activities = listActivityLogs(basePath);
|
|
285
|
+
const debugLogs = listDebugLogs(basePath);
|
|
286
|
+
|
|
287
|
+
if (activities.length === 0 && debugLogs.length === 0) {
|
|
288
|
+
ctx.ui.notify(
|
|
289
|
+
"No logs found.\n\nActivity logs are created during auto-mode.\nDebug logs require GSD_DEBUG=1.",
|
|
290
|
+
"info",
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const lines: string[] = [];
|
|
296
|
+
|
|
297
|
+
if (activities.length > 0) {
|
|
298
|
+
lines.push("Activity Logs (.gsd/activity/):");
|
|
299
|
+
lines.push(" # Unit Type Unit ID Size Age");
|
|
300
|
+
lines.push(" " + "─".repeat(70));
|
|
301
|
+
|
|
302
|
+
// Show last 15 entries
|
|
303
|
+
const recent = activities.slice(-15);
|
|
304
|
+
for (const e of recent) {
|
|
305
|
+
const seq = String(e.seq).padStart(3, " ");
|
|
306
|
+
const type = e.unitType.padEnd(18, " ");
|
|
307
|
+
const id = e.unitId.padEnd(20, " ");
|
|
308
|
+
const size = formatSize(e.size).padStart(7, " ");
|
|
309
|
+
const age = formatAge(e.mtime);
|
|
310
|
+
lines.push(` ${seq} ${type} ${id} ${size} ${age}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (activities.length > 15) {
|
|
314
|
+
lines.push(` ... and ${activities.length - 15} older entries`);
|
|
315
|
+
}
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push(" View details: /gsd logs <#>");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (debugLogs.length > 0) {
|
|
321
|
+
lines.push("");
|
|
322
|
+
lines.push("Debug Logs (.gsd/debug/):");
|
|
323
|
+
for (let i = 0; i < debugLogs.length; i++) {
|
|
324
|
+
const d = debugLogs[i];
|
|
325
|
+
const size = formatSize(d.size).padStart(7, " ");
|
|
326
|
+
const age = formatAge(d.mtime);
|
|
327
|
+
lines.push(` ${i + 1}. ${d.filename} ${size} ${age}`);
|
|
328
|
+
}
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push(" View details: /gsd logs debug <#>");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Metrics summary
|
|
334
|
+
const metricsPath = join(gsdRoot(basePath), "metrics.json");
|
|
335
|
+
const isMetrics = (d: unknown): d is { units: Array<Record<string, unknown>> } =>
|
|
336
|
+
d !== null && typeof d === "object" && "units" in d! && Array.isArray((d as Record<string, unknown>).units);
|
|
337
|
+
const metrics = loadJsonFileOrNull(metricsPath, isMetrics);
|
|
338
|
+
if (metrics && metrics.units.length > 0) {
|
|
339
|
+
const units = metrics.units;
|
|
340
|
+
const totalCost = units.reduce((sum: number, u) => sum + ((u.cost as number) ?? 0), 0);
|
|
341
|
+
const totalTokens = units.reduce((sum: number, u) => {
|
|
342
|
+
const t = u.tokens as Record<string, number> | undefined;
|
|
343
|
+
return sum + (t?.total ?? 0);
|
|
344
|
+
}, 0);
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
lines.push("");
|
|
350
|
+
lines.push("Tip: Enable debug logging with GSD_DEBUG=1 before /gsd auto");
|
|
351
|
+
|
|
352
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function handleLogsShow(basePath: string, ctx: ExtensionCommandContext, seq: number): Promise<void> {
|
|
356
|
+
const activities = listActivityLogs(basePath);
|
|
357
|
+
const entry = activities.find(e => e.seq === seq);
|
|
358
|
+
|
|
359
|
+
if (!entry) {
|
|
360
|
+
ctx.ui.notify(`Activity log #${seq} not found. Run /gsd logs to see available logs.`, "warning");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const filePath = join(activityDir(basePath), entry.filename);
|
|
365
|
+
const summary = summarizeActivityLog(filePath);
|
|
366
|
+
|
|
367
|
+
const lines: string[] = [];
|
|
368
|
+
lines.push(`Activity Log #${entry.seq}: ${entry.unitType} — ${entry.unitId}`);
|
|
369
|
+
lines.push("─".repeat(60));
|
|
370
|
+
lines.push(`File: ${entry.filename}`);
|
|
371
|
+
lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
|
|
372
|
+
lines.push(`Entries: ${summary.entryCount} | Tool calls: ${summary.toolCalls} | Errors: ${summary.errors}`);
|
|
373
|
+
|
|
374
|
+
if (summary.filesWritten.length > 0) {
|
|
375
|
+
lines.push("");
|
|
376
|
+
lines.push("Files written/edited:");
|
|
377
|
+
for (const f of summary.filesWritten.slice(0, 10)) {
|
|
378
|
+
lines.push(` ${f}`);
|
|
379
|
+
}
|
|
380
|
+
if (summary.filesWritten.length > 10) {
|
|
381
|
+
lines.push(` ... and ${summary.filesWritten.length - 10} more`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (summary.commandsRun.length > 0) {
|
|
386
|
+
lines.push("");
|
|
387
|
+
lines.push("Commands run:");
|
|
388
|
+
for (const c of summary.commandsRun.slice(0, 10)) {
|
|
389
|
+
const status = c.failed ? " FAILED" : "";
|
|
390
|
+
lines.push(` ${c.command}${status}`);
|
|
391
|
+
}
|
|
392
|
+
if (summary.commandsRun.length > 10) {
|
|
393
|
+
lines.push(` ... and ${summary.commandsRun.length - 10} more`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (summary.errors > 0) {
|
|
398
|
+
lines.push("");
|
|
399
|
+
lines.push(`${summary.errors} error(s) encountered during this unit.`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (summary.lastReasoning) {
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("Last reasoning:");
|
|
405
|
+
lines.push(` "${summary.lastReasoning}${summary.lastReasoning.length >= 200 ? "..." : ""}"`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
lines.push("");
|
|
409
|
+
lines.push(`Full log: ${filePath}`);
|
|
410
|
+
|
|
411
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function handleLogsDebug(basePath: string, ctx: ExtensionCommandContext, idx?: number): Promise<void> {
|
|
415
|
+
const debugLogs = listDebugLogs(basePath);
|
|
416
|
+
|
|
417
|
+
if (debugLogs.length === 0) {
|
|
418
|
+
ctx.ui.notify(
|
|
419
|
+
"No debug logs found.\n\nEnable debug logging: GSD_DEBUG=1 gsd auto",
|
|
420
|
+
"info",
|
|
421
|
+
);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (idx === undefined) {
|
|
426
|
+
// List debug logs
|
|
427
|
+
const lines: string[] = ["Debug Logs (.gsd/debug/):", ""];
|
|
428
|
+
for (let i = 0; i < debugLogs.length; i++) {
|
|
429
|
+
const d = debugLogs[i];
|
|
430
|
+
lines.push(` ${i + 1}. ${d.filename} ${formatSize(d.size)} ${formatAge(d.mtime)}`);
|
|
431
|
+
}
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push("View details: /gsd logs debug <#>");
|
|
434
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Show specific debug log
|
|
439
|
+
if (idx < 1 || idx > debugLogs.length) {
|
|
440
|
+
ctx.ui.notify(`Debug log #${idx} not found. Available: 1-${debugLogs.length}`, "warning");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const entry = debugLogs[idx - 1];
|
|
445
|
+
const filePath = join(debugDir(basePath), entry.filename);
|
|
446
|
+
const summary = summarizeDebugLog(filePath);
|
|
447
|
+
|
|
448
|
+
const lines: string[] = [];
|
|
449
|
+
lines.push(`Debug Log: ${entry.filename}`);
|
|
450
|
+
lines.push("─".repeat(60));
|
|
451
|
+
lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
|
|
452
|
+
lines.push(`Events: ${summary.events} | Duration: ${summary.duration} | Dispatches: ${summary.dispatches}`);
|
|
453
|
+
|
|
454
|
+
if (summary.errors.length > 0) {
|
|
455
|
+
lines.push("");
|
|
456
|
+
lines.push("Errors/failures:");
|
|
457
|
+
for (const e of summary.errors.slice(0, 10)) {
|
|
458
|
+
lines.push(` [${e.event}] ${e.message}`);
|
|
459
|
+
}
|
|
460
|
+
if (summary.errors.length > 10) {
|
|
461
|
+
lines.push(` ... and ${summary.errors.length - 10} more`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
lines.push("");
|
|
466
|
+
lines.push(`Full log: ${filePath}`);
|
|
467
|
+
|
|
468
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function handleLogsTail(basePath: string, ctx: ExtensionCommandContext, count: number): Promise<void> {
|
|
472
|
+
const activities = listActivityLogs(basePath);
|
|
473
|
+
|
|
474
|
+
if (activities.length === 0) {
|
|
475
|
+
ctx.ui.notify("No activity logs found. Logs are created during auto-mode.", "info");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const recent = activities.slice(-Math.max(1, Math.min(count, 20)));
|
|
480
|
+
const lines: string[] = [`Last ${recent.length} activity log(s):`, ""];
|
|
481
|
+
|
|
482
|
+
for (const e of recent) {
|
|
483
|
+
const filePath = join(activityDir(basePath), e.filename);
|
|
484
|
+
const summary = summarizeActivityLog(filePath);
|
|
485
|
+
const status = summary.errors > 0 ? `${summary.errors} err` : "ok";
|
|
486
|
+
lines.push(` #${e.seq} ${e.unitType} ${e.unitId} — ${summary.toolCalls} tools, ${status}, ${formatAge(e.mtime)}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function handleLogsClear(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
493
|
+
let removedActivity = 0;
|
|
494
|
+
let removedDebug = 0;
|
|
495
|
+
|
|
496
|
+
// Clear activity logs older than 7 days, keep the 5 most recent
|
|
497
|
+
const activities = listActivityLogs(basePath);
|
|
498
|
+
const keepRecent = activities.slice(-5);
|
|
499
|
+
const keepSeqs = new Set(keepRecent.map(e => e.seq));
|
|
500
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
501
|
+
|
|
502
|
+
for (const e of activities) {
|
|
503
|
+
if (keepSeqs.has(e.seq)) continue;
|
|
504
|
+
if (e.mtime.getTime() < cutoff) {
|
|
505
|
+
try {
|
|
506
|
+
unlinkSync(join(activityDir(basePath), e.filename));
|
|
507
|
+
removedActivity++;
|
|
508
|
+
} catch { /* ignore */ }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Clear debug logs older than 3 days, keep latest 2
|
|
513
|
+
const debugLogs = listDebugLogs(basePath);
|
|
514
|
+
const keepDebug = debugLogs.slice(-2);
|
|
515
|
+
const keepDebugNames = new Set(keepDebug.map(d => d.filename));
|
|
516
|
+
const debugCutoff = Date.now() - 3 * 24 * 60 * 60 * 1000;
|
|
517
|
+
|
|
518
|
+
for (const d of debugLogs) {
|
|
519
|
+
if (keepDebugNames.has(d.filename)) continue;
|
|
520
|
+
if (d.mtime.getTime() < debugCutoff) {
|
|
521
|
+
try {
|
|
522
|
+
unlinkSync(join(debugDir(basePath), d.filename));
|
|
523
|
+
removedDebug++;
|
|
524
|
+
} catch { /* ignore */ }
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (removedActivity === 0 && removedDebug === 0) {
|
|
529
|
+
ctx.ui.notify("No old logs to clear.", "info");
|
|
530
|
+
} else {
|
|
531
|
+
ctx.ui.notify(
|
|
532
|
+
`Cleared ${removedActivity} activity log(s) and ${removedDebug} debug log(s).`,
|
|
533
|
+
"info",
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
@@ -22,6 +22,33 @@ import {
|
|
|
22
22
|
import { loadFile, saveFile, splitFrontmatter, parseFrontmatterMap } from "./files.js";
|
|
23
23
|
import { runClaudeImportFlow } from "./claude-import.js";
|
|
24
24
|
|
|
25
|
+
/** Extract body content after frontmatter closing delimiter, or null if none. */
|
|
26
|
+
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
27
|
+
const closingIdx = content.indexOf("\n---", content.indexOf("---"));
|
|
28
|
+
if (closingIdx === -1) return null;
|
|
29
|
+
const afterFrontmatter = content.slice(closingIdx + 4);
|
|
30
|
+
return afterFrontmatter.trim() ? afterFrontmatter : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Numeric validation helpers ──────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/** Parse a string as a non-negative integer, or return null on failure. */
|
|
36
|
+
function tryParseInteger(val: string): number | null {
|
|
37
|
+
return /^\d+$/.test(val) ? Number(val) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse a string as a finite number, or return null on failure. */
|
|
41
|
+
function tryParseNumber(val: string): number | null {
|
|
42
|
+
const n = Number(val);
|
|
43
|
+
return !isNaN(n) && isFinite(n) ? n : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parse a string as a number in the 0–100 range, or return null on failure. */
|
|
47
|
+
function tryParsePercentage(val: string): number | null {
|
|
48
|
+
const n = Number(val);
|
|
49
|
+
return !isNaN(n) && n >= 0 && n <= 100 ? n : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
export async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
26
53
|
const trimmed = args.trim();
|
|
27
54
|
|
|
@@ -98,12 +125,8 @@ export async function handleImportClaude(ctx: ExtensionCommandContext, scope: "g
|
|
|
98
125
|
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
99
126
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
100
127
|
if (existsSync(path)) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (closingIdx !== -1) {
|
|
104
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4);
|
|
105
|
-
if (afterFrontmatter.trim()) body = afterFrontmatter;
|
|
106
|
-
}
|
|
128
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
129
|
+
if (preserved) body = preserved;
|
|
107
130
|
}
|
|
108
131
|
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
109
132
|
};
|
|
@@ -124,14 +147,8 @@ export async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "glob
|
|
|
124
147
|
|
|
125
148
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
126
149
|
if (existsSync(path)) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (closingIdx !== -1) {
|
|
130
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4);
|
|
131
|
-
if (afterFrontmatter.trim()) {
|
|
132
|
-
body = afterFrontmatter;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
150
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
151
|
+
if (preserved) body = preserved;
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
const content = `---\n${frontmatter}---${body}`;
|
|
@@ -306,9 +323,10 @@ async function configureTimeouts(ctx: ExtensionCommandContext, prefs: Record<str
|
|
|
306
323
|
);
|
|
307
324
|
if (input !== null && input !== undefined) {
|
|
308
325
|
const val = input.trim();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
326
|
+
const parsed = tryParseInteger(val);
|
|
327
|
+
if (val && parsed !== null) {
|
|
328
|
+
autoSup[field.key] = parsed;
|
|
329
|
+
} else if (val) {
|
|
312
330
|
ctx.ui.notify(`Invalid value "${val}" for ${field.label} — must be a whole number. Keeping previous value.`, "warning");
|
|
313
331
|
} else if (!val && currentStr) {
|
|
314
332
|
delete autoSup[field.key];
|
|
@@ -467,9 +485,10 @@ async function configureBudget(ctx: ExtensionCommandContext, prefs: Record<strin
|
|
|
467
485
|
);
|
|
468
486
|
if (ceilingInput !== null && ceilingInput !== undefined) {
|
|
469
487
|
const val = ceilingInput.trim().replace(/^\$/, "");
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
488
|
+
const parsed = tryParseNumber(val);
|
|
489
|
+
if (val && parsed !== null) {
|
|
490
|
+
prefs.budget_ceiling = parsed;
|
|
491
|
+
} else if (val) {
|
|
473
492
|
ctx.ui.notify(`Invalid budget ceiling "${val}" — must be a number. Keeping previous value.`, "warning");
|
|
474
493
|
} else if (!val && ceilingStr) {
|
|
475
494
|
delete prefs.budget_ceiling;
|
|
@@ -493,14 +512,14 @@ async function configureBudget(ctx: ExtensionCommandContext, prefs: Record<strin
|
|
|
493
512
|
);
|
|
494
513
|
if (contextPauseInput !== null && contextPauseInput !== undefined) {
|
|
495
514
|
const val = contextPauseInput.trim().replace(/%$/, "");
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (
|
|
515
|
+
const parsed = tryParsePercentage(val);
|
|
516
|
+
if (val && parsed !== null) {
|
|
517
|
+
if (parsed === 0) {
|
|
499
518
|
delete prefs.context_pause_threshold;
|
|
500
519
|
} else {
|
|
501
|
-
prefs.context_pause_threshold =
|
|
520
|
+
prefs.context_pause_threshold = parsed;
|
|
502
521
|
}
|
|
503
|
-
} else if (val
|
|
522
|
+
} else if (val) {
|
|
504
523
|
ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning");
|
|
505
524
|
}
|
|
506
525
|
}
|
|
@@ -622,14 +641,8 @@ export async function handlePrefsWizard(
|
|
|
622
641
|
// Preserve existing body content (everything after closing ---)
|
|
623
642
|
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
624
643
|
if (existsSync(path)) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
if (closingIdx !== -1) {
|
|
628
|
-
const afterFrontmatter = existingContent.slice(closingIdx + 4); // skip past "\n---"
|
|
629
|
-
if (afterFrontmatter.trim()) {
|
|
630
|
-
body = afterFrontmatter;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
644
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
645
|
+
if (preserved) body = preserved;
|
|
633
646
|
}
|
|
634
647
|
|
|
635
648
|
const content = `---\n${frontmatter}---${body}`;
|