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