pi-gsd 2.0.1 → 2.0.2
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/pi-gsd-hooks.js +1532 -0
- package/package.json +3 -5
- package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
- package/src/cli.ts +0 -644
- package/src/commands/base.ts +0 -67
- package/src/commands/commit.ts +0 -22
- package/src/commands/config.ts +0 -71
- package/src/commands/frontmatter.ts +0 -51
- package/src/commands/index.ts +0 -76
- package/src/commands/init.ts +0 -43
- package/src/commands/milestone.ts +0 -37
- package/src/commands/phase.ts +0 -92
- package/src/commands/progress.ts +0 -71
- package/src/commands/roadmap.ts +0 -40
- package/src/commands/scaffold.ts +0 -19
- package/src/commands/state.ts +0 -102
- package/src/commands/template.ts +0 -52
- package/src/commands/verify.ts +0 -70
- package/src/commands/workstream.ts +0 -98
- package/src/commands/wxp.ts +0 -65
- package/src/lib/commands.ts +0 -1040
- package/src/lib/config.ts +0 -385
- package/src/lib/core.ts +0 -1167
- package/src/lib/frontmatter.ts +0 -462
- package/src/lib/init.ts +0 -517
- package/src/lib/milestone.ts +0 -290
- package/src/lib/model-profiles.ts +0 -272
- package/src/lib/phase.ts +0 -1012
- package/src/lib/profile-output.ts +0 -237
- package/src/lib/profile-pipeline.ts +0 -556
- package/src/lib/roadmap.ts +0 -378
- package/src/lib/schemas.ts +0 -290
- package/src/lib/security.ts +0 -176
- package/src/lib/state.ts +0 -1175
- package/src/lib/template.ts +0 -246
- package/src/lib/uat.ts +0 -289
- package/src/lib/verify.ts +0 -879
- package/src/lib/workstream.ts +0 -524
- package/src/output.ts +0 -45
- package/src/schemas/pi-gsd-settings.schema.json +0 -80
- package/src/schemas/wxp.xsd +0 -619
- package/src/schemas/wxp.zod.ts +0 -318
- package/src/wxp/__tests__/arguments.test.ts +0 -86
- package/src/wxp/__tests__/conditions.test.ts +0 -106
- package/src/wxp/__tests__/executor.test.ts +0 -95
- package/src/wxp/__tests__/helpers.ts +0 -26
- package/src/wxp/__tests__/integration.test.ts +0 -166
- package/src/wxp/__tests__/new-features.test.ts +0 -222
- package/src/wxp/__tests__/parser.test.ts +0 -159
- package/src/wxp/__tests__/paste.test.ts +0 -66
- package/src/wxp/__tests__/schema.test.ts +0 -120
- package/src/wxp/__tests__/security.test.ts +0 -87
- package/src/wxp/__tests__/shell.test.ts +0 -85
- package/src/wxp/__tests__/string-ops.test.ts +0 -25
- package/src/wxp/__tests__/variables.test.ts +0 -65
- package/src/wxp/arguments.ts +0 -89
- package/src/wxp/conditions.ts +0 -78
- package/src/wxp/executor.ts +0 -191
- package/src/wxp/index.ts +0 -191
- package/src/wxp/parser.ts +0 -198
- package/src/wxp/paste.ts +0 -51
- package/src/wxp/security.ts +0 -102
- package/src/wxp/shell.ts +0 -81
- package/src/wxp/string-ops.ts +0 -44
- package/src/wxp/variables.ts +0 -109
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* profile-pipeline.ts - Profile rendering pipeline (session scanning, message extraction).
|
|
3
|
-
*
|
|
4
|
-
* Supports two session storage formats:
|
|
5
|
-
*
|
|
6
|
-
* ## Claude / agent sessions
|
|
7
|
-
* Path: ~/.claude/projects/<encoded-project>/<session>.jsonl
|
|
8
|
-
* (also checks ~/.agent/projects/ for the legacy agent harness)
|
|
9
|
-
* Format: Each line is a raw AgentMessage JSON object with a top-level `role` field.
|
|
10
|
-
* User messages have role "human"; assistant messages have role "assistant".
|
|
11
|
-
*
|
|
12
|
-
* ## Pi sessions
|
|
13
|
-
* Path: ~/.pi/agent/sessions/--<path-with-slashes-as-dashes>--/<timestamp>_<uuid>.jsonl
|
|
14
|
-
* The directory name encodes the project cwd: "/" is replaced with "-" and the
|
|
15
|
-
* whole path is wrapped in "--" delimiters. E.g. "/home/user/my-project" becomes
|
|
16
|
-
* "--home-user-my-project--".
|
|
17
|
-
* Format: Each line is a JSONL entry (SessionEntry) with a "type" discriminant:
|
|
18
|
-
* - Header (first line only): {"type":"session","version":3,"id":"<uuid>","timestamp":"...","cwd":"/path"}
|
|
19
|
-
* - Message entry: {"type":"message","id":"<8hex>","parentId":"<8hex>|null","timestamp":"...",
|
|
20
|
-
* "message":{"role":"user"|"assistant"|"toolResult",...}}
|
|
21
|
-
* The message.role for human turns is "user" (not "human" as in Claude sessions).
|
|
22
|
-
* message.content is a string (user) or array of content blocks (assistant/toolResult).
|
|
23
|
-
* - Other entries (model_change, session_info, compaction, etc.) are ignored for profiling.
|
|
24
|
-
*
|
|
25
|
-
* Auto-detection: cmdScanSessions detects both harness types. When --harness pi is given,
|
|
26
|
-
* pi sessions are listed first and marked as priority. Existing Claude session reading
|
|
27
|
-
* is fully preserved.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { gsdError, output } from "./core.js";
|
|
31
|
-
|
|
32
|
-
interface ProfileSampleOptions {
|
|
33
|
-
limit?: number;
|
|
34
|
-
maxPerProject?: number | null;
|
|
35
|
-
maxChars?: number;
|
|
36
|
-
/** "pi" | "claude" | "agent" - when set, prioritise that harness */
|
|
37
|
-
harness?: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ExtractMessagesOptions {
|
|
41
|
-
sessionId?: string | null;
|
|
42
|
-
limit?: number | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ScanSessionsOptions {
|
|
46
|
-
verbose?: boolean;
|
|
47
|
-
json?: boolean;
|
|
48
|
-
/** "pi" | "claude" | "agent" - when set, prioritise that harness */
|
|
49
|
-
harness?: string | null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
-
const fs = require("fs") as typeof import("fs");
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
-
const path = require("path") as typeof import("path");
|
|
56
|
-
|
|
57
|
-
// ─── Path helpers ─────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Returns the Claude/agent session base path.
|
|
61
|
-
* Checks ~/.agent/projects first (legacy agent harness), falls back to ~/.claude/projects.
|
|
62
|
-
*/
|
|
63
|
-
function getClaudeSessionsBasePath(overridePath?: string | null): string {
|
|
64
|
-
if (overridePath) return overridePath;
|
|
65
|
-
const home = process.env["HOME"] ?? "";
|
|
66
|
-
const agentProjects = path.join(home, ".agent", "projects");
|
|
67
|
-
if (fs.existsSync(agentProjects)) return agentProjects;
|
|
68
|
-
return path.join(home, ".claude", "projects");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Returns the pi session base path: ~/.pi/agent/sessions/
|
|
73
|
-
*
|
|
74
|
-
* Pi stores sessions in per-project subdirectories named by encoding the project's
|
|
75
|
-
* cwd: every "/" is replaced with "-" and the result is wrapped in "--" delimiters.
|
|
76
|
-
* Example: "/home/user/my-proj" → "--home-user-my-proj--"
|
|
77
|
-
*/
|
|
78
|
-
function getPiSessionsBasePath(): string {
|
|
79
|
-
const home = process.env["HOME"] ?? "";
|
|
80
|
-
return path.join(home, ".pi", "agent", "sessions");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Extracts the human-readable project path from a pi session directory name.
|
|
85
|
-
* "--home-user-my-proj--" → "/home/user/my-proj"
|
|
86
|
-
*/
|
|
87
|
-
function decodePiProjectDir(dirName: string): string {
|
|
88
|
-
// Strip surrounding "--" delimiters, then replace "-" back to "/"
|
|
89
|
-
// Note: this is lossy (hyphens in path become slashes) but sufficient for display.
|
|
90
|
-
if (dirName.startsWith("--") && dirName.endsWith("--")) {
|
|
91
|
-
return "/" + dirName.slice(2, -2).replace(/-/g, "/");
|
|
92
|
-
}
|
|
93
|
-
return dirName;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Pi JSONL helpers ─────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* A pi session entry. Only the fields relevant for profiling are typed here.
|
|
100
|
-
* All other entry types (model_change, compaction, etc.) are represented as unknown.
|
|
101
|
-
*/
|
|
102
|
-
interface PiSessionEntry {
|
|
103
|
-
type: string;
|
|
104
|
-
id?: string;
|
|
105
|
-
parentId?: string | null;
|
|
106
|
-
timestamp?: string;
|
|
107
|
-
// For type === "session" (header)
|
|
108
|
-
version?: number;
|
|
109
|
-
cwd?: string;
|
|
110
|
-
// For type === "message"
|
|
111
|
-
message?: {
|
|
112
|
-
role: "user" | "assistant" | "toolResult" | string;
|
|
113
|
-
content?: unknown; // string | ContentBlock[]
|
|
114
|
-
timestamp?: number;
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Returns true if the first parseable line of a JSONL file looks like a pi session header.
|
|
120
|
-
* Pi session files begin with: {"type":"session","version":...}
|
|
121
|
-
*/
|
|
122
|
-
function isPiSessionFile(filePath: string): boolean {
|
|
123
|
-
try {
|
|
124
|
-
const firstLine = fs
|
|
125
|
-
.readFileSync(filePath, "utf-8")
|
|
126
|
-
.split("\n")
|
|
127
|
-
.find((l) => l.trim().length > 0);
|
|
128
|
-
if (!firstLine) return false;
|
|
129
|
-
const parsed = JSON.parse(firstLine) as Record<string, unknown>;
|
|
130
|
-
return parsed["type"] === "session" && "version" in parsed;
|
|
131
|
-
} catch {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Extracts the text content from a pi message's content field.
|
|
138
|
-
* Content may be a plain string or an array of content blocks ({type:"text", text:"..."}).
|
|
139
|
-
*/
|
|
140
|
-
function extractPiMessageText(content: unknown): string {
|
|
141
|
-
if (typeof content === "string") return content;
|
|
142
|
-
if (Array.isArray(content)) {
|
|
143
|
-
return content
|
|
144
|
-
.filter(
|
|
145
|
-
(b) =>
|
|
146
|
-
b !== null &&
|
|
147
|
-
typeof b === "object" &&
|
|
148
|
-
(b as Record<string, unknown>)["type"] === "text",
|
|
149
|
-
)
|
|
150
|
-
.map((b) => String((b as Record<string, unknown>)["text"] ?? ""))
|
|
151
|
-
.join(" ");
|
|
152
|
-
}
|
|
153
|
-
return "";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Reads all user-turn messages from a pi JSONL session file.
|
|
158
|
-
* Returns raw entry objects so callers can inspect role/content themselves.
|
|
159
|
-
*/
|
|
160
|
-
function readPiSessionMessages(filePath: string): PiSessionEntry[] {
|
|
161
|
-
try {
|
|
162
|
-
const lines = fs
|
|
163
|
-
.readFileSync(filePath, "utf-8")
|
|
164
|
-
.split("\n")
|
|
165
|
-
.filter(Boolean);
|
|
166
|
-
const messages: PiSessionEntry[] = [];
|
|
167
|
-
for (const line of lines) {
|
|
168
|
-
try {
|
|
169
|
-
const entry = JSON.parse(line) as PiSessionEntry;
|
|
170
|
-
// Only keep message entries (skip header, model_change, etc.)
|
|
171
|
-
if (entry.type === "message" && entry.message) {
|
|
172
|
-
messages.push(entry);
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
/* skip malformed lines */
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return messages;
|
|
179
|
-
} catch {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ─── Exported commands ────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
export async function cmdScanSessions(
|
|
187
|
-
overridePath: string | null | undefined,
|
|
188
|
-
options: ScanSessionsOptions,
|
|
189
|
-
raw: boolean,
|
|
190
|
-
): Promise<void> {
|
|
191
|
-
const harness = options.harness ?? null;
|
|
192
|
-
const isPiHarness = harness === "pi";
|
|
193
|
-
|
|
194
|
-
// Collect pi sessions
|
|
195
|
-
const piBase = getPiSessionsBasePath();
|
|
196
|
-
const piAvailable = fs.existsSync(piBase);
|
|
197
|
-
const piProjects: Array<{
|
|
198
|
-
name: string;
|
|
199
|
-
sessions: number;
|
|
200
|
-
path: string;
|
|
201
|
-
source: "pi";
|
|
202
|
-
cwd: string;
|
|
203
|
-
}> = [];
|
|
204
|
-
|
|
205
|
-
if (piAvailable) {
|
|
206
|
-
try {
|
|
207
|
-
const entries = fs
|
|
208
|
-
.readdirSync(piBase, { withFileTypes: true })
|
|
209
|
-
.filter((e) => e.isDirectory());
|
|
210
|
-
for (const entry of entries) {
|
|
211
|
-
const projectDir = path.join(piBase, entry.name);
|
|
212
|
-
const sessionFiles = fs
|
|
213
|
-
.readdirSync(projectDir)
|
|
214
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
215
|
-
piProjects.push({
|
|
216
|
-
name: entry.name,
|
|
217
|
-
sessions: sessionFiles.length,
|
|
218
|
-
path: projectDir,
|
|
219
|
-
source: "pi",
|
|
220
|
-
cwd: decodePiProjectDir(entry.name),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
} catch {
|
|
224
|
-
/* ok - partial failures are non-fatal */
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Collect Claude/agent sessions (skip when explicit pi harness + no override path)
|
|
229
|
-
const claudeBase = getClaudeSessionsBasePath(
|
|
230
|
-
isPiHarness && !overridePath ? null : overridePath,
|
|
231
|
-
);
|
|
232
|
-
const claudeAvailable =
|
|
233
|
-
!isPiHarness || overridePath ? fs.existsSync(claudeBase) : false;
|
|
234
|
-
const claudeProjects: Array<{
|
|
235
|
-
name: string;
|
|
236
|
-
sessions: number;
|
|
237
|
-
path: string;
|
|
238
|
-
source: "claude";
|
|
239
|
-
}> = [];
|
|
240
|
-
|
|
241
|
-
if (claudeAvailable && (!isPiHarness || overridePath)) {
|
|
242
|
-
try {
|
|
243
|
-
const entries = fs
|
|
244
|
-
.readdirSync(claudeBase, { withFileTypes: true })
|
|
245
|
-
.filter((e) => e.isDirectory());
|
|
246
|
-
for (const entry of entries) {
|
|
247
|
-
const projectDir = path.join(claudeBase, entry.name);
|
|
248
|
-
const sessionFiles = fs
|
|
249
|
-
.readdirSync(projectDir)
|
|
250
|
-
.filter((f) => f.endsWith(".jsonl") || f.endsWith(".json"));
|
|
251
|
-
claudeProjects.push({
|
|
252
|
-
name: entry.name,
|
|
253
|
-
sessions: sessionFiles.length,
|
|
254
|
-
path: projectDir,
|
|
255
|
-
source: "claude",
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
/* ok */
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Pi sessions first when pi harness is prioritised
|
|
264
|
-
const projects = isPiHarness
|
|
265
|
-
? [...piProjects, ...claudeProjects]
|
|
266
|
-
: [...claudeProjects, ...piProjects];
|
|
267
|
-
|
|
268
|
-
if (projects.length === 0) {
|
|
269
|
-
const searched: string[] = [];
|
|
270
|
-
if (piAvailable) searched.push(piBase);
|
|
271
|
-
else searched.push(piBase + " (not found)");
|
|
272
|
-
if (!isPiHarness)
|
|
273
|
-
searched.push(claudeAvailable ? claudeBase : claudeBase + " (not found)");
|
|
274
|
-
output(
|
|
275
|
-
{
|
|
276
|
-
available: false,
|
|
277
|
-
reason: `No sessions found. Searched: ${searched.join(", ")}`,
|
|
278
|
-
projects: [],
|
|
279
|
-
count: 0,
|
|
280
|
-
},
|
|
281
|
-
raw,
|
|
282
|
-
);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
output(
|
|
287
|
-
{
|
|
288
|
-
available: true,
|
|
289
|
-
pi_base: piAvailable ? piBase : null,
|
|
290
|
-
claude_base: claudeAvailable ? claudeBase : null,
|
|
291
|
-
projects,
|
|
292
|
-
count: projects.length,
|
|
293
|
-
},
|
|
294
|
-
raw,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export async function cmdExtractMessages(
|
|
299
|
-
projectArg: string,
|
|
300
|
-
options: ExtractMessagesOptions,
|
|
301
|
-
raw: boolean,
|
|
302
|
-
overridePath?: string | null,
|
|
303
|
-
): Promise<void> {
|
|
304
|
-
// Try pi sessions first (project dir may be a decoded cwd or a raw --path-- dir name)
|
|
305
|
-
const piBase = getPiSessionsBasePath();
|
|
306
|
-
let resolvedDir: string | null = null;
|
|
307
|
-
let sessionFormat: "pi" | "claude" = "claude";
|
|
308
|
-
|
|
309
|
-
if (fs.existsSync(piBase)) {
|
|
310
|
-
// Direct match on encoded dir name (e.g. "--home-user-proj--")
|
|
311
|
-
const direct = path.join(piBase, projectArg);
|
|
312
|
-
if (fs.existsSync(direct)) {
|
|
313
|
-
resolvedDir = direct;
|
|
314
|
-
sessionFormat = "pi";
|
|
315
|
-
} else {
|
|
316
|
-
// Try fuzzy match: find a pi project dir whose decoded cwd ends with projectArg
|
|
317
|
-
try {
|
|
318
|
-
const dirs = fs
|
|
319
|
-
.readdirSync(piBase, { withFileTypes: true })
|
|
320
|
-
.filter((e) => e.isDirectory());
|
|
321
|
-
for (const d of dirs) {
|
|
322
|
-
const decoded = decodePiProjectDir(d.name);
|
|
323
|
-
if (
|
|
324
|
-
decoded.endsWith("/" + projectArg) ||
|
|
325
|
-
decoded === projectArg ||
|
|
326
|
-
d.name === projectArg
|
|
327
|
-
) {
|
|
328
|
-
resolvedDir = path.join(piBase, d.name);
|
|
329
|
-
sessionFormat = "pi";
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
} catch {
|
|
334
|
-
/* ok */
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Fall back to Claude/agent path
|
|
340
|
-
if (!resolvedDir) {
|
|
341
|
-
const claudeBase = getClaudeSessionsBasePath(overridePath);
|
|
342
|
-
const claudeDir = path.join(claudeBase, projectArg);
|
|
343
|
-
if (fs.existsSync(claudeDir)) {
|
|
344
|
-
resolvedDir = claudeDir;
|
|
345
|
-
sessionFormat = "claude";
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!resolvedDir) {
|
|
350
|
-
output(
|
|
351
|
-
{ error: `Project not found: ${projectArg}`, available_projects: [] },
|
|
352
|
-
raw,
|
|
353
|
-
);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const messages: unknown[] = [];
|
|
358
|
-
const sessionFiles = fs
|
|
359
|
-
.readdirSync(resolvedDir)
|
|
360
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
361
|
-
const limit = options.limit ?? null;
|
|
362
|
-
|
|
363
|
-
for (const file of sessionFiles) {
|
|
364
|
-
if (options.sessionId && !file.includes(options.sessionId)) continue;
|
|
365
|
-
const filePath = path.join(resolvedDir, file);
|
|
366
|
-
|
|
367
|
-
if (sessionFormat === "pi" || isPiSessionFile(filePath)) {
|
|
368
|
-
// Pi format: entries wrapped in {type:"message", message:{...}}
|
|
369
|
-
try {
|
|
370
|
-
const lines = fs
|
|
371
|
-
.readFileSync(filePath, "utf-8")
|
|
372
|
-
.split("\n")
|
|
373
|
-
.filter(Boolean);
|
|
374
|
-
for (const line of lines) {
|
|
375
|
-
try {
|
|
376
|
-
const entry = JSON.parse(line) as PiSessionEntry;
|
|
377
|
-
if (entry.type === "message" && entry.message) {
|
|
378
|
-
messages.push(entry.message);
|
|
379
|
-
if (limit && messages.length >= limit) break;
|
|
380
|
-
}
|
|
381
|
-
} catch {
|
|
382
|
-
/* skip malformed */
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
} catch {
|
|
386
|
-
/* ok */
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
// Claude format: each line is a raw message object
|
|
390
|
-
try {
|
|
391
|
-
const lines = fs
|
|
392
|
-
.readFileSync(filePath, "utf-8")
|
|
393
|
-
.split("\n")
|
|
394
|
-
.filter(Boolean);
|
|
395
|
-
for (const line of lines) {
|
|
396
|
-
try {
|
|
397
|
-
const msg = JSON.parse(line);
|
|
398
|
-
messages.push(msg);
|
|
399
|
-
if (limit && messages.length >= limit) break;
|
|
400
|
-
} catch {
|
|
401
|
-
/* skip malformed */
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
} catch {
|
|
405
|
-
/* ok */
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (limit && messages.length >= limit) break;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
output(
|
|
412
|
-
{
|
|
413
|
-
project: projectArg,
|
|
414
|
-
source: sessionFormat,
|
|
415
|
-
messages,
|
|
416
|
-
count: messages.length,
|
|
417
|
-
},
|
|
418
|
-
raw,
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export async function cmdProfileSample(
|
|
423
|
-
overridePath: string | null | undefined,
|
|
424
|
-
options: ProfileSampleOptions,
|
|
425
|
-
raw: boolean,
|
|
426
|
-
): Promise<void> {
|
|
427
|
-
const harness = options.harness ?? null;
|
|
428
|
-
const isPiHarness = harness === "pi";
|
|
429
|
-
const limit = options.limit ?? 150;
|
|
430
|
-
const maxChars = options.maxChars ?? 500;
|
|
431
|
-
|
|
432
|
-
const samples: unknown[] = [];
|
|
433
|
-
|
|
434
|
-
// ── Pi sessions ──────────────────────────────────────────────────────────
|
|
435
|
-
const piBase = getPiSessionsBasePath();
|
|
436
|
-
if (fs.existsSync(piBase)) {
|
|
437
|
-
try {
|
|
438
|
-
const projects = fs
|
|
439
|
-
.readdirSync(piBase, { withFileTypes: true })
|
|
440
|
-
.filter((e) => e.isDirectory());
|
|
441
|
-
|
|
442
|
-
outer_pi: for (const project of projects) {
|
|
443
|
-
const projectDir = path.join(piBase, project.name);
|
|
444
|
-
const sessionFiles = fs
|
|
445
|
-
.readdirSync(projectDir)
|
|
446
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
447
|
-
let perProject = 0;
|
|
448
|
-
for (const file of sessionFiles) {
|
|
449
|
-
const entries = readPiSessionMessages(path.join(projectDir, file));
|
|
450
|
-
for (const entry of entries) {
|
|
451
|
-
if (entry.message?.role === "user") {
|
|
452
|
-
const text = extractPiMessageText(entry.message.content).slice(
|
|
453
|
-
0,
|
|
454
|
-
maxChars,
|
|
455
|
-
);
|
|
456
|
-
if (text.length > 20) {
|
|
457
|
-
samples.push({
|
|
458
|
-
project: decodePiProjectDir(project.name),
|
|
459
|
-
text,
|
|
460
|
-
source: "pi",
|
|
461
|
-
});
|
|
462
|
-
perProject++;
|
|
463
|
-
if (
|
|
464
|
-
options.maxPerProject &&
|
|
465
|
-
perProject >= options.maxPerProject
|
|
466
|
-
)
|
|
467
|
-
break;
|
|
468
|
-
if (samples.length >= limit) break outer_pi;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
} catch {
|
|
475
|
-
/* ok */
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// ── Claude / agent sessions ───────────────────────────────────────────────
|
|
480
|
-
// Skip Claude scan when --harness pi and enough samples collected, but always
|
|
481
|
-
// include Claude if an explicit --path override is given.
|
|
482
|
-
const skipClaude = isPiHarness && !overridePath && samples.length >= limit;
|
|
483
|
-
if (!skipClaude) {
|
|
484
|
-
const claudeBase = getClaudeSessionsBasePath(overridePath);
|
|
485
|
-
if (fs.existsSync(claudeBase)) {
|
|
486
|
-
try {
|
|
487
|
-
const projects = fs
|
|
488
|
-
.readdirSync(claudeBase, { withFileTypes: true })
|
|
489
|
-
.filter((e) => e.isDirectory());
|
|
490
|
-
|
|
491
|
-
outer_claude: for (const project of projects) {
|
|
492
|
-
const projectDir = path.join(claudeBase, project.name);
|
|
493
|
-
const sessionFiles = fs
|
|
494
|
-
.readdirSync(projectDir)
|
|
495
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
496
|
-
let perProject = 0;
|
|
497
|
-
for (const file of sessionFiles) {
|
|
498
|
-
try {
|
|
499
|
-
const lines = fs
|
|
500
|
-
.readFileSync(path.join(projectDir, file), "utf-8")
|
|
501
|
-
.split("\n")
|
|
502
|
-
.filter(Boolean);
|
|
503
|
-
for (const line of lines) {
|
|
504
|
-
try {
|
|
505
|
-
const msg = JSON.parse(line);
|
|
506
|
-
// Claude sessions: role "human"; some formats use type "human"
|
|
507
|
-
if (msg.role === "human" || msg.type === "human") {
|
|
508
|
-
const text = (msg.content || msg.message || "").slice(
|
|
509
|
-
0,
|
|
510
|
-
maxChars,
|
|
511
|
-
);
|
|
512
|
-
if (text.length > 20) {
|
|
513
|
-
samples.push({
|
|
514
|
-
project: project.name,
|
|
515
|
-
text,
|
|
516
|
-
source: "claude",
|
|
517
|
-
});
|
|
518
|
-
perProject++;
|
|
519
|
-
if (
|
|
520
|
-
options.maxPerProject &&
|
|
521
|
-
perProject >= options.maxPerProject
|
|
522
|
-
)
|
|
523
|
-
break;
|
|
524
|
-
if (samples.length >= limit) break outer_claude;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
} catch {
|
|
528
|
-
/* ok */
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
} catch {
|
|
532
|
-
/* ok */
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
} catch {
|
|
537
|
-
/* ok */
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (samples.length === 0) {
|
|
543
|
-
output(
|
|
544
|
-
{
|
|
545
|
-
available: false,
|
|
546
|
-
reason: "No user messages found in any session",
|
|
547
|
-
samples: [],
|
|
548
|
-
count: 0,
|
|
549
|
-
},
|
|
550
|
-
raw,
|
|
551
|
-
);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
output({ available: true, samples, count: samples.length }, raw);
|
|
556
|
-
}
|