@piewf/cli 3.4.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.
@@ -0,0 +1,445 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { emitKeypressEvents } from "node:readline";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin, stdout } from "node:process";
6
+ import { highlightCode, initTheme, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
7
+ import { formatBudgetStatus, inspectWorkflowScript } from "pi-extensible-workflows";
8
+ import { listPersistedSessionIds, listRunIds, RunStore } from "pi-extensible-workflows/persistence";
9
+ function text(content) {
10
+ if (typeof content === "string")
11
+ return content;
12
+ if (!Array.isArray(content))
13
+ return "";
14
+ const parts = content;
15
+ return parts.flatMap((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string" ? [part.text] : []).join("");
16
+ }
17
+ function transcriptPartLines(part) {
18
+ if (typeof part !== "object" || part === null || !("type" in part))
19
+ return [];
20
+ const value = part;
21
+ if (value.type === "text" && typeof value.text === "string")
22
+ return value.text.split("\n");
23
+ if (value.type === "thinking" && typeof value.thinking === "string")
24
+ return ["Thinking:", ...value.thinking.split("\n")];
25
+ if (value.type === "toolCall" && typeof value.name === "string")
26
+ return [`Tool call: ${value.name}`, JSON.stringify(value.arguments, null, 2)];
27
+ if (value.type === "image")
28
+ return ["[image]"];
29
+ return [];
30
+ }
31
+ function transcriptMessageLines(message) {
32
+ if (typeof message !== "object" || message === null)
33
+ return ["(invalid message)"];
34
+ const value = message;
35
+ const role = typeof value.role === "string" ? value.role : "message";
36
+ const label = role === "toolResult" && typeof value.toolName === "string" ? `${role}: ${value.toolName}` : role === "custom" && typeof value.customType === "string" ? `${role}: ${value.customType}` : role;
37
+ const content = Array.isArray(value.content) ? value.content.flatMap(transcriptPartLines) : typeof value.content === "string" ? value.content.split("\n") : [];
38
+ return [`[${label}]`, ...(content.length ? content : ["(empty)"])];
39
+ }
40
+ export function transcriptLines(entries) {
41
+ if (!entries.length)
42
+ return ["(no active transcript entries)"];
43
+ return entries.flatMap((entry, index) => {
44
+ const lines = entry.type === "message" ? transcriptMessageLines(entry.message) : entry.type === "model_change" ? [`[model] ${entry.provider}/${entry.modelId}`] : entry.type === "thinking_level_change" ? [`[thinking] ${entry.thinkingLevel}`] : entry.type === "compaction" ? ["[compaction]", ...entry.summary.split("\n")] : entry.type === "branch_summary" ? ["[branch summary]", ...entry.summary.split("\n")] : entry.type === "custom_message" ? [`[custom_message: ${entry.customType}]`, ...(typeof entry.content === "string" ? entry.content.split("\n") : entry.content.flatMap(transcriptPartLines))] : entry.type === "custom" ? [`[custom: ${entry.customType}]`] : entry.type === "label" ? [`[label] ${entry.label ?? ""}`] : [`[session info] ${entry.name ?? ""}`];
45
+ return index ? ["", ...lines] : lines;
46
+ });
47
+ }
48
+ export function transcriptFileLines(path) {
49
+ return transcriptLines(SessionManager.open(path).buildContextEntries());
50
+ }
51
+ function mergedModels(groups) {
52
+ const totals = new Map();
53
+ for (const group of groups)
54
+ for (const item of group)
55
+ totals.set(item.model, (totals.get(item.model) ?? 0) + item.cost);
56
+ return [...totals].map(([model, cost]) => ({ model, cost })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
57
+ }
58
+ const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
59
+ function parsedThinking(value) {
60
+ return typeof value === "string" && thinkingLevels.includes(value) ? value : undefined;
61
+ }
62
+ function modelName(provider, model) {
63
+ return typeof provider === "string" && provider && typeof model === "string" && model ? `${provider}/${model}` : undefined;
64
+ }
65
+ function transcript(manager) {
66
+ const models = new Map();
67
+ let cost = 0;
68
+ let prompt;
69
+ let model;
70
+ let thinking;
71
+ for (const entry of manager.getEntries()) {
72
+ if (entry.type === "model_change") {
73
+ model = modelName(entry.provider, entry.modelId);
74
+ if (!model)
75
+ throw new Error("Invalid model policy");
76
+ continue;
77
+ }
78
+ if (entry.type === "thinking_level_change") {
79
+ thinking = parsedThinking(entry.thinkingLevel);
80
+ if (thinking === undefined)
81
+ throw new Error("Invalid thinking policy");
82
+ continue;
83
+ }
84
+ if (entry.type !== "message")
85
+ continue;
86
+ const message = entry.message;
87
+ if (message.role === "user" && prompt === undefined) {
88
+ const full = text(message.content);
89
+ const marker = "\n\nTask:\n";
90
+ prompt = full.includes(marker) ? full.slice(full.indexOf(marker) + marker.length) : full;
91
+ }
92
+ if (message.role === "assistant") {
93
+ const actualModel = modelName(message.provider, message.model);
94
+ const messageCost = message.usage.cost.total;
95
+ if (!actualModel || typeof messageCost !== "number" || !Number.isFinite(messageCost))
96
+ throw new Error("Invalid assistant policy");
97
+ model = actualModel;
98
+ cost += messageCost;
99
+ models.set(actualModel, (models.get(actualModel) ?? 0) + messageCost);
100
+ }
101
+ }
102
+ return { ...(prompt !== undefined ? { prompt } : {}), cost, models: [...models].map(([model, modelCost]) => ({ model, cost: modelCost })), ...(model !== undefined ? { model } : {}), ...(thinking !== undefined ? { thinking } : {}) };
103
+ }
104
+ function readTranscript(path) {
105
+ try {
106
+ if (!existsSync(path) || !statSync(path).isFile() || statSync(path).size === 0)
107
+ return undefined;
108
+ const manager = SessionManager.open(path);
109
+ if (!manager.getHeader())
110
+ return undefined;
111
+ const summary = transcript(manager);
112
+ return summary.model === undefined ? undefined : summary;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ function attemptTranscriptPath(attempt) {
119
+ const session = attempt.session;
120
+ if (!session || session.transport !== "local" || typeof session.locator !== "object" || session.locator === null || Array.isArray(session.locator))
121
+ return undefined;
122
+ const path = session.locator.sessionFile;
123
+ return typeof path === "string" && path ? path : undefined;
124
+ }
125
+ function resultRunId(result) {
126
+ if (!result)
127
+ return undefined;
128
+ if (typeof result.details === "object" && result.details !== null && "runId" in result.details && typeof result.details.runId === "string")
129
+ return result.details.runId;
130
+ const raw = text(result.content);
131
+ try {
132
+ const parsed = JSON.parse(raw);
133
+ return typeof parsed === "object" && parsed !== null && "runId" in parsed && typeof parsed.runId === "string" ? parsed.runId : undefined;
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ function workflowEntries(manager) {
140
+ const calls = [];
141
+ const results = new Map();
142
+ for (const entry of manager.getEntries()) {
143
+ if (entry.type !== "message")
144
+ continue;
145
+ const message = entry.message;
146
+ if (message.role === "assistant")
147
+ for (const part of message.content) {
148
+ if (part.type === "toolCall" && part.name === "workflow")
149
+ calls.push({ id: part.id, arguments: part.arguments });
150
+ }
151
+ if (message.role === "toolResult" && message.toolName === "workflow")
152
+ results.set(message.toolCallId, { toolCallId: message.toolCallId, isError: message.isError, content: message.content, details: message.details });
153
+ }
154
+ return { calls, results };
155
+ }
156
+ async function loadRuns(cwd, sessionId, home) {
157
+ const runs = new Map();
158
+ for (const runId of await listRunIds(cwd, sessionId, home)) {
159
+ try {
160
+ runs.set(runId, await new RunStore(cwd, sessionId, runId, home).load());
161
+ }
162
+ catch { /* Ignore corrupt or concurrently removed runs. */ }
163
+ }
164
+ return runs;
165
+ }
166
+ async function agentReport(agent) {
167
+ const fallbackModel = `${agent.model.provider}/${agent.model.model}`;
168
+ const fallbackThinking = agent.model.thinking;
169
+ const attempts = [];
170
+ for (const attempt of agent.attemptDetails ?? []) {
171
+ const setup = attempt.setup;
172
+ const path = attemptTranscriptPath(attempt);
173
+ const log = path ? readTranscript(path) : undefined;
174
+ if (log) {
175
+ const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
176
+ const cost = log.cost;
177
+ attempts.push({ attempt: attempt.attempt, prompt: log.prompt ?? "(transcript unavailable)", model, ...(log.thinking !== undefined ? { thinking: log.thinking } : {}), cost, models: log.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
178
+ continue;
179
+ }
180
+ const model = `${setup.model.provider}/${setup.model.model}`;
181
+ const cost = attempt.accounting.cost;
182
+ attempts.push({ attempt: attempt.attempt, prompt: "(transcript unavailable)", model, ...(setup.model.thinking !== undefined ? { thinking: setup.model.thinking } : {}), cost, models: [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
183
+ }
184
+ if (!attempts.length) {
185
+ const cost = agent.accounting?.cost ?? 0;
186
+ attempts.push({ attempt: 1, prompt: "(transcript unavailable)", model: fallbackModel, ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}), cost, models: [{ model: fallbackModel, cost }] });
187
+ }
188
+ const latest = attempts[attempts.length - 1];
189
+ return { name: agent.name, ...(agent.label ? { label: agent.label } : {}), state: agent.state, ...(agent.role ? { role: agent.role } : {}), ...(agent.requestedModel ? { requestedModel: agent.requestedModel } : {}), model: latest?.model ?? fallbackModel, ...(latest?.thinking !== undefined ? { thinking: latest.thinking } : {}), cost: attempts.reduce((sum, attempt) => sum + attempt.cost, 0), attempts, ...(latest?.setup ? { setup: latest.setup } : {}) };
190
+ }
191
+ export function matchSession(query, sessions) {
192
+ const exact = sessions.filter(({ id }) => id === query);
193
+ if (exact[0])
194
+ return exact[0];
195
+ const partial = sessions.filter(({ id }) => id.startsWith(query));
196
+ if (partial.length === 1 && partial[0])
197
+ return partial[0];
198
+ if (!partial.length)
199
+ throw new Error(`Session not found: ${query}`);
200
+ throw new Error(`Session ID is ambiguous: ${query}`);
201
+ }
202
+ export async function loadSessionReport(path, home = homedir()) {
203
+ const manager = SessionManager.open(path);
204
+ const header = manager.getHeader();
205
+ if (!header)
206
+ throw new Error(`Invalid session file: ${path}`);
207
+ const parent = transcript(manager);
208
+ const { calls, results } = workflowEntries(manager);
209
+ const runs = await loadRuns(header.cwd, header.id, home);
210
+ const workflows = [];
211
+ for (const call of calls) {
212
+ const result = results.get(call.id);
213
+ const runId = resultRunId(result);
214
+ const loaded = runId ? runs.get(runId) : undefined;
215
+ const args = call.arguments;
216
+ const agents = loaded ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
217
+ const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
218
+ const name = loaded?.run.workflowName ?? (typeof args.workflow === "string" ? args.workflow : typeof args.name === "string" ? args.name : "workflow");
219
+ const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
220
+ const script = typeof args.script === "string" && args.script.trim() ? args.script : loaded?.snapshot.script;
221
+ let staticCalls = [];
222
+ let parseError;
223
+ if (script) {
224
+ try {
225
+ staticCalls = inspectWorkflowScript(script);
226
+ }
227
+ catch (error) {
228
+ parseError = error instanceof Error ? error.message : String(error);
229
+ }
230
+ }
231
+ workflows.push({
232
+ name,
233
+ ...(description ? { description } : {}),
234
+ status: loaded?.run.state ?? (result ? result.isError ? "failed" : "completed" : "pending"),
235
+ ...(runId ? { runId } : {}),
236
+ ...(script ? { script } : {}),
237
+ calls: staticCalls,
238
+ ...(parseError ? { parseError } : {}),
239
+ cost: agents.reduce((sum, agent) => sum + agent.cost, 0),
240
+ models,
241
+ agents,
242
+ ...(loaded?.run.budget ? { budget: loaded.run.budget } : {}),
243
+ ...(loaded?.run.budgetVersion !== undefined ? { budgetVersion: loaded.run.budgetVersion } : {}),
244
+ ...(loaded?.run.usage ? { usage: loaded.run.usage } : {}),
245
+ ...(loaded?.run.budgetEvents ? { budgetEvents: loaded.run.budgetEvents } : {}),
246
+ ...(loaded?.run.events?.length ? { events: loaded.run.events } : {})
247
+ });
248
+ }
249
+ const workflowCost = workflows.reduce((sum, workflow) => sum + workflow.cost, 0);
250
+ return {
251
+ id: header.id, cwd: header.cwd, path, cost: parent.cost, models: parent.models, workflows,
252
+ totalCost: parent.cost + workflowCost,
253
+ totalModels: mergedModels([parent.models, ...workflows.map(({ models }) => models)]),
254
+ };
255
+ }
256
+ const ansi = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m", red: "\x1b[31m", yellow: "\x1b[33m", inverse: "\x1b[7m" };
257
+ const style = (code, value) => `${code}${value}${ansi.reset}`;
258
+ const money = (cost) => `$${cost < 0.01 && cost > 0 ? cost.toFixed(4) : cost.toFixed(2)}`;
259
+ const modelSummary = (models) => models.length ? models.map(({ model, cost }) => `${model} ${money(cost)}`).join(" · ") : "(none)";
260
+ function wrapped(lines, width) {
261
+ return lines.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines : [""]);
262
+ }
263
+ function detailLines(workflow) {
264
+ const lines = [
265
+ style(ansi.bold + ansi.cyan, workflow.name),
266
+ `${workflow.status} · ${money(workflow.cost)}${workflow.runId ? ` · ${workflow.runId}` : ""}`,
267
+ workflow.description ?? "",
268
+ ...(workflow.events?.length ? ["", style(ansi.bold, "Run events"), ...workflow.events.map((event) => `${event.type}: ${event.message}`)] : []),
269
+ "",
270
+ ...(workflow.budget ? formatBudgetStatus({ budget: workflow.budget, ...(workflow.budgetVersion !== undefined ? { budgetVersion: workflow.budgetVersion } : {}), ...(workflow.usage ? { usage: workflow.usage } : {}), ...(workflow.budgetEvents ? { budgetEvents: workflow.budgetEvents } : {}) }).map((line) => `Budget ${line}`) : []),
271
+ style(ansi.bold, "Models"),
272
+ modelSummary(workflow.models),
273
+ "",
274
+ style(ansi.bold, "Static workflow calls"),
275
+ ...(workflow.parseError ? [style(ansi.red, `Parse error: ${workflow.parseError}`)] : workflow.calls.length ? workflow.calls.map((call, index) => {
276
+ const fields = [call.name ? `name=${JSON.stringify(call.name)}` : "", call.prompt ? `prompt=${JSON.stringify(call.prompt)}` : call.kind === "agent" || call.kind === "checkpoint" ? "prompt=<dynamic>" : "", call.label ? `label=${call.label}` : "", call.role ? `role=${call.role}` : "", call.model ? `model=${call.model}` : ""].filter(Boolean);
277
+ return `${String(index + 1)}. ${call.kind}${fields.length ? ` · ${fields.join(" · ")}` : ""}`;
278
+ }) : ["(none)"]),
279
+ "",
280
+ style(ansi.bold, "Agents and runtime prompts"),
281
+ ];
282
+ if (!workflow.agents.length)
283
+ lines.push("(no agent run was persisted)");
284
+ for (const agent of workflow.agents) {
285
+ lines.push("", style(agent.state === "completed" ? ansi.green : agent.state === "failed" ? ansi.red : ansi.yellow, `${agent.label ?? agent.name} [${agent.state}]`), `${agent.role ? `role=${agent.role} · ` : ""}${agent.requestedModel ? `requested=${agent.requestedModel} · ` : ""}${agent.model}${agent.thinking !== undefined ? `:${agent.thinking}` : ""} · ${money(agent.cost)}`);
286
+ for (const attempt of agent.attempts) {
287
+ lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}`, ...(attempt.setup ? [
288
+ `Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
289
+ `Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
290
+ ...(attempt.setup.disabledAgentResources ? [
291
+ `Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
292
+ `Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
293
+ `Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
294
+ `Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
295
+ `Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
296
+ `Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
297
+ ] : []),
298
+ ] : []));
299
+ }
300
+ }
301
+ return lines.filter((line, index) => line || index !== 2);
302
+ }
303
+ let themeReady = false;
304
+ function highlighted(script) {
305
+ if (!themeReady) {
306
+ initTheme(undefined, false);
307
+ themeReady = true;
308
+ }
309
+ return highlightCode(script, "javascript");
310
+ }
311
+ export function renderInspector(report, state, width = 80, height = 24, highlighter = highlighted) {
312
+ const usableWidth = Math.max(1, width);
313
+ const selected = report.workflows[state.selected];
314
+ if (state.view === "list") {
315
+ const header = wrapped([
316
+ style(ansi.bold + ansi.cyan, "Pi workflow session inspector"),
317
+ `${report.id} · ${report.cwd}`,
318
+ `Total ${money(report.totalCost)} · parent ${money(report.cost)}`,
319
+ modelSummary(report.totalModels),
320
+ "",
321
+ style(ansi.bold, `Workflows (${String(report.workflows.length)})`),
322
+ ], usableWidth);
323
+ const rows = report.workflows.length ? report.workflows.map((workflow, index) => `${index === state.selected ? style(ansi.inverse, ">") : " "} ${workflow.name} · ${workflow.status} · ${money(workflow.cost)} · ${String(workflow.agents.length)} agents`) : ["No workflow calls found."];
324
+ const footer = wrapped(["", style(ansi.dim, "↑↓ select · enter details · q quit")], usableWidth);
325
+ const room = Math.max(1, height - header.length - footer.length);
326
+ const start = Math.max(0, Math.min(state.selected - Math.floor(room / 2), rows.length - room));
327
+ return [...header, ...rows.slice(start, start + room).map((line) => wrapped([line], usableWidth)[0] ?? ""), ...footer].slice(0, height);
328
+ }
329
+ if (!selected)
330
+ return wrapped(["No workflow selected.", style(ansi.dim, "esc back · q quit")], usableWidth).slice(0, height);
331
+ const title = state.view === "script" ? `${selected.name} · script` : `${selected.name} · details`;
332
+ const body = state.view === "script" ? selected.script ? highlighter(selected.script) : ["Script unavailable."] : detailLines(selected);
333
+ const fitted = wrapped(body, usableWidth);
334
+ const header = wrapped([style(ansi.bold + ansi.cyan, title)], usableWidth);
335
+ const hint = state.view === "script" ? "↑↓/pgup/pgdn scroll · esc details · q quit" : "↑↓/pgup/pgdn scroll · s script · esc workflows · q quit";
336
+ const footer = wrapped([style(ansi.dim, hint)], usableWidth);
337
+ const room = Math.max(1, height - header.length - footer.length);
338
+ const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
339
+ return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
340
+ }
341
+ export async function loadPersistedSessionSummary(cwd, sessionId, home = homedir(), failedOnly = false) {
342
+ const runs = [];
343
+ for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
344
+ try {
345
+ const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
346
+ if (!failedOnly || summary.state === "failed")
347
+ runs.push(summary);
348
+ }
349
+ catch { /* Ignore corrupt or concurrently removed runs. */ }
350
+ }
351
+ return { schemaVersion: 1, cwd, sessionId, runs };
352
+ }
353
+ export async function loadPersistedSummaries(cwd, sessionId, home = homedir(), failedOnly = false) {
354
+ const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
355
+ const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
356
+ return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
357
+ }
358
+ export function formatPersistedRunSummary(summary, sessionId = summary.sessionId) {
359
+ const counts = summary.agents.reduce((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
360
+ const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
361
+ return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
362
+ }
363
+ function nextState(current, key, workflowCount) {
364
+ if (current.view === "list") {
365
+ if (key === "up")
366
+ return { ...current, selected: Math.max(0, current.selected - 1) };
367
+ if (key === "down")
368
+ return { ...current, selected: Math.min(Math.max(0, workflowCount - 1), current.selected + 1) };
369
+ if (key === "return" && workflowCount)
370
+ return { ...current, view: "detail", scroll: 0 };
371
+ return current;
372
+ }
373
+ if (key === "escape" || key === "left")
374
+ return { ...current, view: current.view === "script" ? "detail" : "list", scroll: 0 };
375
+ if ((key === "s" || key === "tab") && current.view === "detail")
376
+ return { ...current, view: "script", scroll: 0 };
377
+ const delta = key === "up" ? -1 : key === "down" ? 1 : key === "pageup" ? -10 : key === "pagedown" ? 10 : 0;
378
+ return delta ? { ...current, scroll: Math.max(0, current.scroll + delta) } : current;
379
+ }
380
+ export async function showSessionInspector(report) {
381
+ if (!stdin.isTTY || !stdout.isTTY)
382
+ throw new Error("The session inspector requires an interactive terminal.");
383
+ let state = { view: "list", selected: 0, scroll: 0 };
384
+ const render = () => { stdout.write(`\x1b[H\x1b[2J${renderInspector(report, state, stdout.columns || 80, stdout.rows || 24).join("\n")}`); };
385
+ await new Promise((resolve) => {
386
+ const wasRaw = stdin.isRaw;
387
+ const done = () => {
388
+ stdin.off("keypress", onKey);
389
+ stdout.off("resize", render);
390
+ stdin.setRawMode(wasRaw);
391
+ if (!wasRaw)
392
+ stdin.pause();
393
+ stdout.write("\x1b[?25h\x1b[?1049l");
394
+ resolve();
395
+ };
396
+ const onKey = (value, key) => {
397
+ if ((key.ctrl && key.name === "c") || value === "q") {
398
+ done();
399
+ return;
400
+ }
401
+ state = nextState(state, value === "s" ? "s" : key.name ?? value, report.workflows.length);
402
+ render();
403
+ };
404
+ emitKeypressEvents(stdin);
405
+ stdin.setRawMode(true);
406
+ stdin.resume();
407
+ stdin.on("keypress", onKey);
408
+ stdout.on("resize", render);
409
+ stdout.write("\x1b[?1049h\x1b[?25l");
410
+ render();
411
+ });
412
+ }
413
+ async function askSessionId() {
414
+ if (!stdin.isTTY || !stdout.isTTY)
415
+ throw new Error("Pass a session ID when stdin is not interactive.");
416
+ const prompt = createInterface({ input: stdin, output: stdout });
417
+ try {
418
+ return (await prompt.question("Session ID: ")).trim();
419
+ }
420
+ finally {
421
+ prompt.close();
422
+ }
423
+ }
424
+ export async function resolveSession(query, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR) {
425
+ return matchSession(query, await SessionManager.listAll(sessionDir));
426
+ }
427
+ export async function runSessionInspector(sessionId, mode = "tui", cwd = process.cwd(), home = homedir(), write = (text) => { stdout.write(text); }, failedOnly = false) {
428
+ if (mode !== "tui") {
429
+ const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
430
+ if (mode === "json") {
431
+ const value = sessionId?.trim() ? sessions[0] : undefined;
432
+ write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
433
+ return;
434
+ }
435
+ const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
436
+ const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
437
+ write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
438
+ return;
439
+ }
440
+ const query = sessionId?.trim() || await askSessionId();
441
+ if (!query)
442
+ throw new Error("Session ID is required.");
443
+ const session = await resolveSession(query);
444
+ await showSessionInspector(await loadSessionReport(session.path));
445
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@piewf/cli",
3
+ "version": "3.4.2",
4
+ "description": "CLI for pi-extensible-workflows",
5
+ "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vekexasia/pi-extensible-workflows.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": "./dist/src/cli.js"
14
+ },
15
+ "bin": {
16
+ "piewf": "./dist/src/cli.js"
17
+ },
18
+ "files": [
19
+ "dist/src"
20
+ ],
21
+ "scripts": {
22
+ "build": "npm --prefix ../.. run build --workspace=../core && rm -rf dist && tsc -p tsconfig.json && chmod +x dist/src/cli.js",
23
+ "inspect": "node dist/src/cli.js inspect",
24
+ "test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
25
+ "test:run": "files=${TEST_FILES:-dist/test/*.test.js}; printf '%s\\n' $files | xargs -n1 -P20 sh -c 'tmp=$(mktemp -d); TMPDIR=\"$tmp\" node --test --test-concurrency=1 --test-timeout=60000 --test-force-exit --test-reporter=dot \"$1\"; status=$?; rm -rf \"$tmp\"; exit $status' sh",
26
+ "lint": "eslint .",
27
+ "prepack": "npm run build"
28
+ },
29
+ "peerDependencies": {
30
+ "@earendil-works/pi-ai": "*",
31
+ "@earendil-works/pi-coding-agent": "*",
32
+ "typebox": "*"
33
+ },
34
+ "devDependencies": {
35
+ "@earendil-works/pi-ai": "0.80.9",
36
+ "@earendil-works/pi-coding-agent": "0.80.9",
37
+ "@eslint/js": "10.0.1",
38
+ "@types/node": "24.12.4",
39
+ "eslint": "10.0.3",
40
+ "typescript": "5.9.3",
41
+ "typescript-eslint": "8.63.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.19.0"
45
+ },
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "pi-extensible-workflows": "3.4.2"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }