@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.
@@ -0,0 +1,912 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmodSync, linkSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, extname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
8
+ import { Value } from "typebox/value";
9
+ import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
10
+ import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
11
+ import workflowExtension, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, sameFilesystemPath, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "@marcoscale98/pi-extensible-workflows";
12
+ import { CLI_PACKAGE_NAME, portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
13
+ import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
14
+ import { isPersistedRun, listPersistedSessionIds, listRunIds } from "@marcoscale98/pi-extensible-workflows/persistence";
15
+ import { shareTrajectoryRun } from "@marcoscale98/pi-extensible-workflows/trajectory";
16
+ function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
17
+ function requiredArg(args, index) {
18
+ const value = args[index];
19
+ if (value === undefined)
20
+ throw new Error("Missing argument");
21
+ return value;
22
+ }
23
+ function typedOptionKey(options, key) { return Object.prototype.hasOwnProperty.call(options, key); }
24
+ function clone(value) { const cloned = structuredClone(value); if (!jsonValue(cloned))
25
+ throw new Error("Invalid JSON passed to --input"); return cloned; }
26
+ function kebabCase(value) { return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase(); }
27
+ function isCliScalar(value) { return value === "string" || value === "integer" || value === "number" || value === "boolean"; }
28
+ function scalarType(schema) {
29
+ if (!object(schema) || typeof schema.type !== "string")
30
+ return undefined;
31
+ return isCliScalar(schema.type) ? schema.type : undefined;
32
+ }
33
+ function schemaPlan(schema) {
34
+ if (!object(schema) || schema.type !== "object")
35
+ return { fields: [] };
36
+ const properties = object(schema.properties) ? schema.properties : {};
37
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
38
+ const fields = [];
39
+ for (const [name, property] of Object.entries(properties)) {
40
+ if (!object(property))
41
+ continue;
42
+ const directType = scalarType(property);
43
+ const itemType = object(property.items) ? scalarType(property.items) : undefined;
44
+ const type = directType ?? itemType ? directType ?? "array" : undefined;
45
+ if (!type)
46
+ continue;
47
+ fields.push({ name, option: `--${kebabCase(name)}`, schema: property, type, ...(itemType ? { itemType } : {}), required: required.has(name) });
48
+ }
49
+ const requiredScalars = fields.filter((field) => field.required && field.type !== "array");
50
+ return { fields, ...(requiredScalars.length === 1 ? { positional: requiredScalars[0] } : {}) };
51
+ }
52
+ function scalarLabel(type) { return type === "integer" ? "integer" : type; }
53
+ function scalarFieldType(field) {
54
+ if (field.type !== "array")
55
+ return field.type;
56
+ if (!isCliScalar(field.itemType))
57
+ throw new Error("Invalid array field");
58
+ return field.itemType;
59
+ }
60
+ function fieldLabel(field) { return field.type === "array" ? `${field.option} <${scalarLabel(scalarFieldType(field))}>` : `${field.option}${field.type === "boolean" ? "" : ` <${scalarLabel(field.type)}>`}`; }
61
+ function fieldDescription(field) {
62
+ const description = typeof field.schema.description === "string" ? field.schema.description.trim() : "";
63
+ const required = field.required ? "required" : "optional";
64
+ const defaultValue = has(field.schema, "default") ? ` default=${JSON.stringify(field.schema.default)}` : "";
65
+ const enumSchema = field.type === "array" && object(field.schema.items) ? field.schema.items : field.schema;
66
+ const enumValue = Array.isArray(enumSchema.enum) ? ` enum=${enumSchema.enum.map((value) => JSON.stringify(value)).join(",")}` : "";
67
+ return [description, required, defaultValue, enumValue].filter(Boolean).join("; ");
68
+ }
69
+ export function formatWorkflowCliHelp(fn, command = "piewf") {
70
+ const plan = schemaPlan(fn.input);
71
+ const lines = [`Usage: ${command} run ${fn.name}${plan.positional ? ` <${plan.positional.name}>` : ""} [options]`, "", fn.description];
72
+ if (plan.positional) {
73
+ lines.push("", "Arguments:", ` <${plan.positional.name}> ${scalarLabel(scalarFieldType(plan.positional))}; ${fieldDescription(plan.positional)}`);
74
+ }
75
+ lines.push("", "Options:");
76
+ for (const field of plan.fields) {
77
+ const label = field === plan.positional ? `${field.option} <${scalarLabel(scalarFieldType(field))}>` : fieldLabel(field);
78
+ lines.push(` ${label.padEnd(24)}${fieldDescription(field)}`);
79
+ }
80
+ lines.push(" --input <json>".padEnd(28) + "JSON input escape hatch for complex schemas", ...launcherHelpLines(), " -h, --help".padEnd(28) + "Show this help");
81
+ return `${lines.join("\n")}\n`;
82
+ }
83
+ function enumAllows(schema, value) {
84
+ return !Array.isArray(schema.enum) || schema.enum.some((candidate) => JSON.stringify(candidate) === JSON.stringify(value));
85
+ }
86
+ function coerce(raw, type, schema) {
87
+ let value;
88
+ if (type === "string")
89
+ value = raw;
90
+ else if (type === "integer") {
91
+ if (!/^-?(?:0|[1-9]\d*)$/.test(raw))
92
+ throw new Error(`Invalid integer: ${raw}`);
93
+ value = Number(raw);
94
+ if (!Number.isSafeInteger(value))
95
+ throw new Error(`Invalid integer: ${raw}`);
96
+ }
97
+ else if (type === "number") {
98
+ value = Number(raw);
99
+ if (!raw.trim() || !Number.isFinite(value))
100
+ throw new Error(`Invalid number: ${raw}`);
101
+ }
102
+ else {
103
+ if (raw !== "true" && raw !== "false")
104
+ throw new Error(`Invalid boolean: ${raw}`);
105
+ value = raw === "true";
106
+ }
107
+ if (!enumAllows(schema, value))
108
+ throw new Error(`Invalid value for enum: ${raw}`);
109
+ return value;
110
+ }
111
+ function parseJsonInput(value) {
112
+ try {
113
+ return clone(JSON.parse(value));
114
+ }
115
+ catch {
116
+ throw new Error("Invalid JSON passed to --input");
117
+ }
118
+ }
119
+ export function parseWorkflowCliArgs(schema, rawArgs) {
120
+ const plan = schemaPlan(schema);
121
+ const fields = new Map(plan.fields.map((field) => [field.option, field]));
122
+ const result = {};
123
+ let input;
124
+ let positionalUsed = false;
125
+ let endOptions = false;
126
+ const assign = (field, raw) => {
127
+ if (field.type === "array") {
128
+ const current = result[field.name];
129
+ const values = Array.isArray(current) ? current : [];
130
+ const itemSchema = field.schema.items;
131
+ if (!object(itemSchema))
132
+ throw new Error("Invalid array field");
133
+ values.push(coerce(raw, scalarFieldType(field), itemSchema));
134
+ result[field.name] = values;
135
+ }
136
+ else
137
+ result[field.name] = coerce(raw, field.type, field.schema);
138
+ };
139
+ for (let index = 0; index < rawArgs.length; index += 1) {
140
+ const token = requiredArg(rawArgs, index);
141
+ if (token === "--") {
142
+ endOptions = true;
143
+ continue;
144
+ }
145
+ if (!endOptions && (token === "--input" || token.startsWith("--input="))) {
146
+ if (input !== undefined)
147
+ throw new Error("--input may only be provided once");
148
+ const raw = token.startsWith("--input=") ? token.slice("--input=".length) : rawArgs[++index];
149
+ if (raw === undefined)
150
+ throw new Error("Missing value for --input");
151
+ input = parseJsonInput(raw);
152
+ continue;
153
+ }
154
+ if (!endOptions && token.startsWith("--")) {
155
+ const equals = token.indexOf("=");
156
+ const option = equals >= 0 ? token.slice(0, equals) : token;
157
+ const negated = equals < 0 && option.startsWith("--no-");
158
+ const field = fields.get(negated ? `--${option.slice("--no-".length)}` : option);
159
+ if (!field)
160
+ throw new Error(`Unknown option: ${option}`);
161
+ if (negated) {
162
+ if (field.type !== "boolean")
163
+ throw new Error(`Invalid boolean option: ${option}`);
164
+ result[field.name] = false;
165
+ }
166
+ else if (field.type === "boolean") {
167
+ if (equals >= 0)
168
+ assign(field, token.slice(equals + 1));
169
+ else if (rawArgs[index + 1] === "true" || rawArgs[index + 1] === "false")
170
+ assign(field, requiredArg(rawArgs, ++index));
171
+ else
172
+ result[field.name] = true;
173
+ }
174
+ else {
175
+ const raw = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
176
+ if (raw === undefined || raw.startsWith("--"))
177
+ throw new Error(`Missing value for ${option}`);
178
+ assign(field, raw);
179
+ }
180
+ continue;
181
+ }
182
+ const positional = plan.positional;
183
+ const numericNegative = positional && (positional.type === "integer" || positional.type === "number") && /^-\d/.test(token);
184
+ if (!endOptions && token.startsWith("-") && !numericNegative)
185
+ throw new Error(`Unknown option: ${token}`);
186
+ if (!positional || positionalUsed)
187
+ throw new Error(`Unexpected argument: ${token}`);
188
+ assign(positional, token);
189
+ positionalUsed = true;
190
+ }
191
+ if (input !== undefined) {
192
+ if (Object.keys(result).length || positionalUsed)
193
+ throw new Error("--input cannot be combined with CLI arguments");
194
+ if (!object(input))
195
+ throw new Error("Workflow input must be a JSON object");
196
+ for (const field of plan.fields)
197
+ if (!has(input, field.name) && has(field.schema, "default"))
198
+ input[field.name] = clone(field.schema.default);
199
+ return input;
200
+ }
201
+ for (const field of plan.fields)
202
+ if (!has(result, field.name) && has(field.schema, "default"))
203
+ result[field.name] = clone(field.schema.default);
204
+ for (const field of plan.fields)
205
+ if (field.required && !has(result, field.name))
206
+ throw new Error(`Missing required argument: ${field.name}`);
207
+ return result;
208
+ }
209
+ function launcherHelpLines() {
210
+ return [
211
+ " --approve".padEnd(28) + "Trust project resources for this launch",
212
+ " --no-approve".padEnd(28) + "Do not trust project resources for this launch",
213
+ " --".padEnd(28) + "End launcher option parsing; pass later tokens to workflow input",
214
+ ];
215
+ }
216
+ function workflowUsage() { return [`Usage: piewf run <workflow-name> [workflow arguments] | run --script <path> [--name <workflow-name>] [--input <json>] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
217
+ function scriptWorkflowUsage() { return [`Usage: piewf run --script <path> [--name <workflow-name>] [--input <json>]`, "", "Options:", ...launcherHelpLines().slice(0, 2), " -h, --help".padEnd(28) + "Show this help"].join("\n") + "\n"; }
218
+ function scriptWorkflowName(scriptPath) {
219
+ const filename = basename(scriptPath);
220
+ const extension = extname(filename);
221
+ return extension ? filename.slice(0, -extension.length) : filename;
222
+ }
223
+ export function parseScriptWorkflowCliArgs(rawArgs) {
224
+ let scriptPath;
225
+ let name;
226
+ let input;
227
+ for (let index = 0; index < rawArgs.length; index += 1) {
228
+ const token = requiredArg(rawArgs, index);
229
+ if (token === "--help" || token === "-h")
230
+ return { help: true };
231
+ const equals = token.indexOf("=");
232
+ const option = equals >= 0 ? token.slice(0, equals) : token;
233
+ if (option === "--script" || option === "--name" || option === "--input") {
234
+ const value = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
235
+ if (!value?.trim())
236
+ throw new Error(`Missing value for ${option}`);
237
+ if (option === "--script") {
238
+ if (scriptPath !== undefined)
239
+ throw new Error("--script may only be provided once");
240
+ scriptPath = value;
241
+ }
242
+ else if (option === "--name") {
243
+ if (name !== undefined)
244
+ throw new Error("--name may only be provided once");
245
+ name = value;
246
+ }
247
+ else {
248
+ if (input !== undefined)
249
+ throw new Error("--input may only be provided once");
250
+ input = parseJsonInput(value);
251
+ }
252
+ continue;
253
+ }
254
+ throw new Error(`Unknown option: ${token}`);
255
+ }
256
+ if (scriptPath === undefined)
257
+ throw new Error("Missing required option: --script");
258
+ const workflowName = name === undefined ? scriptWorkflowName(scriptPath) : name.trim();
259
+ if (!workflowName)
260
+ throw new Error("Workflow name must be non-empty");
261
+ return { help: false, scriptPath, name: workflowName, args: input ?? null };
262
+ }
263
+ function exportUsage() { return [`Usage: piewf export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
264
+ function bundleUsage() { return [`Usage: piewf bundle <workflow-name> [--name <command>] [--output <directory>] [--force]`, "", "The bundle contains a launcher, manifest, workflow payload, and external-runtime setup instructions.", "Repeat --role, --alias, --tool, --command, or --environment to declare recipient requirements.", "Use --extension, --skill, --resource, and --dependency to copy selected payload resources."].join("\n") + "\n"; }
265
+ function parseInspectArgs(rawArgs) {
266
+ let sessionId;
267
+ let mode = "tui";
268
+ let failedOnly = false;
269
+ for (const arg of rawArgs) {
270
+ if (arg === "--json" || arg === "--summary") {
271
+ const next = arg === "--json" ? "json" : "summary";
272
+ if (mode !== "tui" && mode !== next)
273
+ throw new Error("inspect accepts only one output mode");
274
+ mode = next;
275
+ }
276
+ else if (arg === "--failed")
277
+ failedOnly = true;
278
+ else if (arg.startsWith("--"))
279
+ throw new Error(`Unknown inspect option: ${arg}`);
280
+ else if (sessionId !== undefined)
281
+ throw new Error(`Unexpected argument: ${arg}`);
282
+ else
283
+ sessionId = arg;
284
+ }
285
+ return { ...(sessionId ? { sessionId } : {}), mode: failedOnly && mode === "tui" ? "summary" : mode, failedOnly };
286
+ }
287
+ export function parseDoctorArgs(rawArgs) {
288
+ let role;
289
+ let prompt;
290
+ let json = false;
291
+ for (let index = 0; index < rawArgs.length; index += 1) {
292
+ const token = requiredArg(rawArgs, index);
293
+ const equals = token.indexOf("=");
294
+ const option = equals >= 0 ? token.slice(0, equals) : token;
295
+ if (option === "--json" && equals < 0) {
296
+ json = true;
297
+ continue;
298
+ }
299
+ if (option === "--role" || option === "--prompt") {
300
+ const value = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
301
+ if (!value)
302
+ throw new Error(`Missing value for ${option}`);
303
+ if (option === "--role") {
304
+ if (role !== undefined)
305
+ throw new Error("--role may only be provided once");
306
+ role = value;
307
+ }
308
+ else {
309
+ if (prompt !== undefined)
310
+ throw new Error("--prompt may only be provided once");
311
+ prompt = value;
312
+ }
313
+ continue;
314
+ }
315
+ if (token === "--help" || token === "-h")
316
+ throw new Error("help");
317
+ if (token.startsWith("--"))
318
+ throw new Error(`Unknown doctor option: ${token}`);
319
+ if (role !== undefined)
320
+ throw new Error(`Unexpected argument: ${token}`);
321
+ role = token;
322
+ }
323
+ if (prompt !== undefined && role === undefined)
324
+ throw new Error("--prompt requires --role");
325
+ return { ...(role === undefined ? {} : { role }), ...(prompt === undefined ? {} : { prompt }), ...(json ? { json: true } : {}) };
326
+ }
327
+ export function parseDoctorCleanupArgs(rawArgs) {
328
+ let olderThanDays = 90;
329
+ let yes = false;
330
+ let seenDays = false;
331
+ for (let index = 0; index < rawArgs.length; index += 1) {
332
+ const token = requiredArg(rawArgs, index);
333
+ if (token === "--yes") {
334
+ yes = true;
335
+ continue;
336
+ }
337
+ const inline = token.startsWith("--older-than-days=") ? token.slice("--older-than-days=".length) : undefined;
338
+ if (token === "--older-than-days" || inline !== undefined) {
339
+ if (seenDays)
340
+ throw new Error("--older-than-days may only be provided once");
341
+ const raw = inline ?? rawArgs[++index];
342
+ if (raw === undefined || !/^[1-9]\d*$/.test(raw))
343
+ throw new Error("older-than-days must be a positive integer");
344
+ const parsed = Number(raw);
345
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
346
+ throw new Error("older-than-days must be a positive integer");
347
+ olderThanDays = parsed;
348
+ seenDays = true;
349
+ continue;
350
+ }
351
+ throw new Error(`Unknown cleanup option: ${token}`);
352
+ }
353
+ return { olderThanDays, yes };
354
+ }
355
+ function stripTrustOptions(rawArgs) {
356
+ const args = [];
357
+ let trustOverride;
358
+ let endOptions = false;
359
+ for (const arg of rawArgs) {
360
+ if (arg === "--") {
361
+ endOptions = true;
362
+ args.push(arg);
363
+ continue;
364
+ }
365
+ if (!endOptions && (arg === "--approve" || arg === "--no-approve")) {
366
+ const next = arg === "--approve";
367
+ if (trustOverride !== undefined && trustOverride !== next)
368
+ throw new Error("--approve and --no-approve cannot be combined");
369
+ trustOverride = next;
370
+ }
371
+ else
372
+ args.push(arg);
373
+ }
374
+ return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
375
+ }
376
+ function isHeadlessWorkflowResult(value) { return object(value) && Array.isArray(value.content) && value.content.every((entry) => object(entry) && typeof entry.type === "string" && typeof entry.text === "string"); }
377
+ function isHeadlessWorkflowTool(value) { return object(value) && value.name === "workflow" && typeof value.execute === "function"; }
378
+ async function createWorkflowRuntime(options, shutdownHandlers = []) {
379
+ const cwd = options.cwd ?? process.cwd();
380
+ const agentDir = options.agentDir ?? getAgentDir();
381
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
382
+ const requiredTrust = hasTrustRequiringProjectResources(cwd);
383
+ const trustStore = new ProjectTrustStore(agentDir);
384
+ const defaultProjectTrust = settingsManager.getDefaultProjectTrust();
385
+ const resolveProjectTrust = async ({ extensionsResult }) => {
386
+ if (options.trustOverride !== undefined)
387
+ return options.trustOverride;
388
+ if (!requiredTrust)
389
+ return true;
390
+ const projectTrustContext = {
391
+ cwd,
392
+ mode: "print",
393
+ hasUI: false,
394
+ ui: { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => { } },
395
+ };
396
+ for (const extension of extensionsResult.extensions) {
397
+ for (const handler of extension.handlers.get("project_trust") ?? []) {
398
+ try {
399
+ const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
400
+ if (!object(result))
401
+ continue;
402
+ if (result.trusted === "undecided")
403
+ continue;
404
+ if (result.trusted !== "yes" && result.trusted !== "no")
405
+ continue;
406
+ const trusted = result.trusted === "yes";
407
+ if (result.remember === true)
408
+ trustStore.set(cwd, trusted);
409
+ return trusted;
410
+ }
411
+ catch { /* Project trust extensions are best effort, as in Pi. */ }
412
+ }
413
+ }
414
+ const savedTrust = trustStore.get(cwd);
415
+ if (savedTrust !== null)
416
+ return savedTrust;
417
+ return defaultProjectTrust === "always";
418
+ };
419
+ const services = await createAgentSessionServices({
420
+ cwd,
421
+ agentDir,
422
+ settingsManager,
423
+ resourceLoaderOptions: { ...(options.skillPaths?.length ? { additionalSkillPaths: [...options.skillPaths] } : {}) },
424
+ resourceLoaderReloadOptions: { resolveProjectTrust },
425
+ });
426
+ const extensions = services.resourceLoader.getExtensions();
427
+ const tools = [];
428
+ const activeTools = [...new Set(["read", "bash", "edit", "write"].concat(extensions.extensions.flatMap((extension) => [...extension.tools.keys()]), ["workflow"]))];
429
+ const headlessPi = {
430
+ registerTool(tool) { tools.push(tool); },
431
+ registerCommand() { },
432
+ getThinkingLevel: () => services.settingsManager.getDefaultThinkingLevel() ?? "medium",
433
+ getActiveTools: () => activeTools,
434
+ on(name, handler) { if (name === "session_shutdown" && typeof handler === "function")
435
+ shutdownHandlers.push(handler); },
436
+ appendEntry() { },
437
+ sendMessage() { },
438
+ events: { emit() { } },
439
+ };
440
+ workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
441
+ const workflowTool = tools.find(isHeadlessWorkflowTool);
442
+ if (!workflowTool)
443
+ throw new Error("The workflow runtime could not be initialized");
444
+ return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
445
+ }
446
+ function availableModelInfo(services, available = false) {
447
+ const models = available ? services.modelRuntime.getAvailableSnapshot() : services.modelRuntime.getModels();
448
+ return models.map(({ provider, id }) => ({ provider, id }));
449
+ }
450
+ async function selectedModel(services) {
451
+ const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), noTools: "all" });
452
+ try {
453
+ const model = session.model;
454
+ return model ? { provider: model.provider, id: model.id } : undefined;
455
+ }
456
+ finally {
457
+ session.dispose();
458
+ }
459
+ }
460
+ function commandName(value) { return value.trim() && !value.includes("/") && !value.includes("\\") ? value.trim() : ""; }
461
+ function writeLauncher(destination, workflowName, force) {
462
+ const parent = dirname(destination);
463
+ mkdirSync(parent, { recursive: true });
464
+ const tempDir = mkdtempSync(join(parent, ".pi-extensible-workflows-"));
465
+ const tempPath = join(tempDir, "launcher");
466
+ try {
467
+ const source = `#!/usr/bin/env node
468
+ import { spawnSync } from "node:child_process";
469
+ import { homedir } from "node:os";
470
+ import { join } from "node:path";
471
+ import { pathToFileURL } from "node:url";
472
+ let cli;
473
+ try { cli = await import(pathToFileURL(join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "npm", "node_modules", ...${JSON.stringify(CLI_PACKAGE_NAME.split("/"))}, "dist", "src", "cli.js")).href); } catch {}
474
+ if (!cli) try { cli = await import(import.meta.resolve(${JSON.stringify(CLI_PACKAGE_NAME)})); } catch {}
475
+ if (cli) process.exitCode = await cli.runCli(["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)]);
476
+ else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)], { stdio: "inherit" }); if (result.error) { console.error("Could not resolve the fork workflow CLI; install it or put piewf on PATH."); process.exitCode = 1; } else process.exitCode = result.status ?? 1; }
477
+ `;
478
+ writeFileSync(tempPath, source, { mode: 0o755 });
479
+ chmodSync(tempPath, 0o755);
480
+ if (force)
481
+ renameSync(tempPath, destination);
482
+ else {
483
+ try {
484
+ linkSync(tempPath, destination);
485
+ }
486
+ catch (error) {
487
+ if (isNodeError(error, "EEXIST"))
488
+ throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
489
+ throw error;
490
+ }
491
+ }
492
+ }
493
+ finally {
494
+ rmSync(tempDir, { recursive: true, force: true });
495
+ }
496
+ }
497
+ function terminalProgressStyles(enabled) {
498
+ const style = (code) => enabled ? (text) => `\x1b[${String(code)}m${text}\x1b[0m` : (text) => text;
499
+ return { accent: style(36), success: style(32), error: style(31), warning: style(33), muted: style(90), dim: style(2), bold: style(1) };
500
+ }
501
+ class CliProgress {
502
+ stderr;
503
+ onRunId;
504
+ #lastStable = "";
505
+ #lines = 0;
506
+ #frame = 0;
507
+ #run;
508
+ #runId;
509
+ #runtimeStartedAt = 0;
510
+ #runtimeBaseMs = 0;
511
+ #timer;
512
+ #interactive;
513
+ #styles;
514
+ constructor(stderr, tty, onRunId) {
515
+ this.stderr = stderr;
516
+ this.onRunId = onRunId;
517
+ this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
518
+ this.#styles = terminalProgressStyles(this.#interactive);
519
+ }
520
+ update(run) {
521
+ if (this.#runId !== run.id) {
522
+ this.#runId = run.id;
523
+ this.onRunId(run.id);
524
+ this.#runtimeStartedAt = Date.now();
525
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
526
+ }
527
+ else if (this.#run && this.#run.state !== "running" && run.state === "running") {
528
+ this.#runtimeStartedAt = Date.now();
529
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
530
+ }
531
+ this.#run = run;
532
+ if (!this.#interactive) {
533
+ this.#timer ??= setInterval(() => { this.render(); }, 1000);
534
+ this.#timer.unref();
535
+ this.render();
536
+ return;
537
+ }
538
+ this.#timer ??= setInterval(() => { this.render(); }, 80);
539
+ this.#timer.unref();
540
+ this.render();
541
+ }
542
+ render() {
543
+ if (!this.#run)
544
+ return;
545
+ const run = this.#run.state !== "running" ? this.#run : { ...this.#run, usage: { ...(this.#run.usage ?? { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 }), durationMs: Math.max(this.#run.usage?.durationMs ?? 0, this.#runtimeBaseMs + Date.now() - this.#runtimeStartedAt) } };
546
+ if (!this.#interactive) {
547
+ const stable = formatWorkflowProgress(run, "◇", this.#styles);
548
+ if (stable !== this.#lastStable) {
549
+ this.#lastStable = stable;
550
+ this.stderr(`${stable}\n`);
551
+ }
552
+ return;
553
+ }
554
+ const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
555
+ const width = process.stderr.columns || 80;
556
+ const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
557
+ this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
558
+ this.#lines = text.split("\n").length;
559
+ }
560
+ finish() {
561
+ if (this.#timer) {
562
+ clearInterval(this.#timer);
563
+ this.#timer = undefined;
564
+ }
565
+ if (this.#interactive && this.#lines) {
566
+ this.stderr(`\x1b[${String(this.#lines)}A\x1b[0J\x1b[?25h`);
567
+ this.#lines = 0;
568
+ }
569
+ this.#run = undefined;
570
+ }
571
+ }
572
+ async function invokeWorkflow(launch, runtime, options, context) {
573
+ if (launch.fn && (!object(launch.args) || !Value.Check(launch.fn.input, launch.args)))
574
+ throw new Error(`Invalid input for ${launch.fn.name}`);
575
+ if (!launch.fn && launch.scriptPath === undefined)
576
+ throw new Error("Workflow launch has no source");
577
+ let announcedRunId;
578
+ const announceRunId = (runId) => { if (announcedRunId === runId)
579
+ return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
580
+ const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
581
+ try {
582
+ const params = launch.scriptPath === undefined
583
+ ? { name: launch.name, script: `return await ${launch.fn?.name ?? launch.name}(args);`, args: launch.args, foreground: true }
584
+ : { name: launch.name, scriptPath: launch.scriptPath, args: launch.args, foreground: true };
585
+ const result = await runtime.workflowTool.execute(randomUUID(), params, options.signal, (update) => { if (object(update) && object(update.details) && isPersistedRun(update.details.run))
586
+ progress.update(update.details.run); }, context);
587
+ if (!isHeadlessWorkflowResult(result))
588
+ throw new Error("Workflow returned an invalid result");
589
+ const details = object(result.details) ? result.details : {};
590
+ const runId = typeof details.runId === "string" ? details.runId : undefined;
591
+ if (runId)
592
+ announceRunId(runId);
593
+ if (has(details, "value") && jsonValue(details.value))
594
+ return { value: details.value, ...(runId ? { runId } : {}) };
595
+ const first = result.content[0];
596
+ if (!first || first.type !== "text")
597
+ throw new Error("Workflow returned no result");
598
+ try {
599
+ return { value: parseJsonInput(first.text), ...(runId ? { runId } : {}) };
600
+ }
601
+ catch {
602
+ throw new Error("Workflow returned invalid JSON");
603
+ }
604
+ }
605
+ finally {
606
+ progress.finish();
607
+ }
608
+ }
609
+ async function createWorkflowContext(runtime, options) {
610
+ const model = await selectedModel(runtime.services);
611
+ const sessionManager = SessionManager.inMemory();
612
+ const modelRegistry = { getAll: () => availableModelInfo(runtime.services), getAvailable: () => availableModelInfo(runtime.services, true) };
613
+ return { cwd: options.cwd ?? process.cwd(), mode: "print", hasUI: false, ...(model ? { model } : {}), modelRegistry, sessionManager, isProjectTrusted: () => runtime.services.settingsManager.isProjectTrusted(), ui: { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => { }, onTerminalInput: () => () => { }, setStatus: () => { }, setWorkingMessage: () => { }, setWorkingVisible: () => { }, setWorkingIndicator: () => { }, setHiddenThinkingLabel: () => { }, setWidget: () => { }, setFooter: () => { }, setHeader: () => { }, setTitle: () => { }, custom: async () => undefined, pasteToEditor: () => { }, setEditorText: () => { }, getEditorText: () => "", editor: async () => undefined, addAutocompleteProvider: () => { } }, headless: true };
614
+ }
615
+ async function shutdownWorkflowRuntime(handlers, context) {
616
+ for (const handler of handlers) {
617
+ try {
618
+ await handler({ type: "session_shutdown", reason: "quit" }, context);
619
+ }
620
+ catch { /* Shutdown is best effort. */ }
621
+ }
622
+ }
623
+ async function withWorkflowRuntime(options, action) {
624
+ const shutdownHandlers = [];
625
+ let context = { cwd: options.cwd ?? process.cwd(), mode: "print", hasUI: false, headless: true };
626
+ try {
627
+ const runtime = await createWorkflowRuntime(options, shutdownHandlers);
628
+ context = await createWorkflowContext(runtime, options);
629
+ return await action(runtime, context);
630
+ }
631
+ finally {
632
+ await shutdownWorkflowRuntime(shutdownHandlers, context);
633
+ }
634
+ }
635
+ async function runWorkflowCli(rawArgs, options) {
636
+ const parsed = stripTrustOptions(rawArgs);
637
+ const args = parsed.args;
638
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
639
+ options.write(workflowUsage());
640
+ return args.length ? 0 : 1;
641
+ }
642
+ const runtimeOptions = { ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) };
643
+ const scriptMode = args[0] === "--script" || args[0]?.startsWith("--script=") || args[0]?.startsWith("--") && args.some((arg) => arg === "--script" || arg.startsWith("--script="));
644
+ if (scriptMode) {
645
+ const script = parseScriptWorkflowCliArgs(args);
646
+ if (script.help) {
647
+ options.write(scriptWorkflowUsage());
648
+ return 0;
649
+ }
650
+ return withWorkflowRuntime(runtimeOptions, async (runtime, context) => {
651
+ const result = await invokeWorkflow({ name: script.name, scriptPath: script.scriptPath, args: script.args }, runtime, options, context);
652
+ options.write(`${JSON.stringify(result.value)}\n`);
653
+ return 0;
654
+ });
655
+ }
656
+ const name = requiredArg(args, 0);
657
+ return withWorkflowRuntime(runtimeOptions, async (runtime, context) => {
658
+ const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
659
+ const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
660
+ if (!fn)
661
+ throw new Error(`Unknown workflow function: ${name}`);
662
+ if (help) {
663
+ options.write(formatWorkflowCliHelp(fn));
664
+ return 0;
665
+ }
666
+ const input = parseWorkflowCliArgs(fn.input, args.slice(1));
667
+ const result = await invokeWorkflow({ name: fn.name, fn, args: input }, runtime, options, context);
668
+ options.write(`${JSON.stringify(result.value)}\n`);
669
+ return 0;
670
+ });
671
+ }
672
+ async function exportWorkflowCli(rawArgs, options) {
673
+ const parsed = stripTrustOptions(rawArgs);
674
+ const args = parsed.args;
675
+ if (args.includes("--bundle"))
676
+ return bundleWorkflowCli(args.filter((arg) => arg !== "--bundle"), { ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) });
677
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
678
+ options.write(exportUsage());
679
+ return args.length ? 0 : 1;
680
+ }
681
+ const workflowName = requiredArg(args, 0);
682
+ return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
683
+ let name;
684
+ let output;
685
+ let force = false;
686
+ for (let index = 1; index < args.length; index += 1) {
687
+ const arg = requiredArg(args, index);
688
+ if (arg === "--force") {
689
+ force = true;
690
+ continue;
691
+ }
692
+ const equals = arg.indexOf("=");
693
+ const option = equals >= 0 ? arg.slice(0, equals) : arg;
694
+ if (option === "--name" || option === "--output") {
695
+ const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
696
+ if (!value)
697
+ throw new Error(`Missing value for ${option}`);
698
+ if (option === "--name")
699
+ name = value;
700
+ else
701
+ output = value;
702
+ continue;
703
+ }
704
+ if (arg === "--help" || arg === "-h") {
705
+ options.write(exportUsage());
706
+ return 0;
707
+ }
708
+ throw new Error(`Unknown option: ${arg}`);
709
+ }
710
+ if (!runtime.catalog.functions.some((candidate) => candidate.name === workflowName))
711
+ throw new Error(`Unknown workflow function: ${workflowName}`);
712
+ const command = commandName(name ?? kebabCase(workflowName));
713
+ if (!command)
714
+ throw new Error("Command name must be a non-empty name without path separators");
715
+ const destination = output ? output : join(homedir(), ".local", "bin", command);
716
+ writeLauncher(destination, workflowName, force);
717
+ if (!output) {
718
+ const binDir = join(homedir(), ".local", "bin");
719
+ const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
720
+ if (!pathEntries.some((entry) => sameFilesystemPath(entry, binDir)))
721
+ options.stderr(`Warning: ${binDir} is not in PATH\n`);
722
+ }
723
+ options.write(`Exported ${destination}\n`);
724
+ return 0;
725
+ });
726
+ }
727
+ async function bundleWorkflowCli(rawArgs, options) {
728
+ const parsed = stripTrustOptions(rawArgs);
729
+ const args = parsed.args;
730
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
731
+ options.write(bundleUsage());
732
+ return args.length ? 0 : 1;
733
+ }
734
+ const workflowName = requiredArg(args, 0);
735
+ return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
736
+ let name;
737
+ let output;
738
+ let force = false;
739
+ const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
740
+ const resources = { extensions: [], skills: [], static: [], dependencies: [] };
741
+ for (let index = 1; index < args.length; index += 1) {
742
+ const arg = requiredArg(args, index);
743
+ if (arg === "--force") {
744
+ force = true;
745
+ continue;
746
+ }
747
+ const equals = arg.indexOf("=");
748
+ const option = equals >= 0 ? arg.slice(0, equals) : arg;
749
+ const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
750
+ const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
751
+ if (option === "--name" || option === "--output" || typedOptionKey(requirementOptions, option) || typedOptionKey(resourceOptions, option)) {
752
+ const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
753
+ if (!value)
754
+ throw new Error(`Missing value for ${option}`);
755
+ if (option === "--name")
756
+ name = value;
757
+ else if (option === "--output")
758
+ output = value;
759
+ else if (typedOptionKey(requirementOptions, option))
760
+ requirements[requirementOptions[option]].push(value);
761
+ else if (typedOptionKey(resourceOptions, option))
762
+ resources[resourceOptions[option]].push(value);
763
+ continue;
764
+ }
765
+ if (arg === "--help" || arg === "-h") {
766
+ options.write(bundleUsage());
767
+ return 0;
768
+ }
769
+ throw new Error(`Unknown option: ${arg}`);
770
+ }
771
+ const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
772
+ if (!fn)
773
+ throw new Error(`Unknown workflow function: ${workflowName}`);
774
+ const source = registeredWorkflowFunctionSources()[workflowName];
775
+ if (!source)
776
+ throw new Error(`Workflow ${workflowName} is not exportable; add \`source: import.meta.url\` to extension ${fn.headline}`);
777
+ const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
778
+ const roles = Object.fromEntries(requirements.roles.map((role) => {
779
+ if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\"))
780
+ throw new Error(`Invalid role name for bundle: ${role}`);
781
+ const definition = definitions[role];
782
+ if (!definition)
783
+ throw new Error(`Unknown role for bundle: ${role}`);
784
+ return [role, definition];
785
+ }));
786
+ const command = commandName(name ?? kebabCase(workflowName));
787
+ if (!command)
788
+ throw new Error("Command name must be a non-empty name without path separators");
789
+ const destination = output ?? join(homedir(), ".local", "share", "pi-extensible-workflows", "bundles", command);
790
+ const aliasTargets = Object.fromEntries(requirements.aliases.flatMap((name) => {
791
+ const target = runtime.catalog.modelAliases?.[name];
792
+ return typeof target === "string" ? [[name, target]] : [];
793
+ }));
794
+ const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
795
+ await writePortableWorkflowBundle({ destination, command, workflow: fn, source: { module: source.module, export: source.export }, dependencies: source.dependencies, requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
796
+ options.write(`Bundled ${workflowName} at ${destination}\n`);
797
+ options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
798
+ return 0;
799
+ });
800
+ }
801
+ export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
802
+ const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
803
+ if (args[0] === "doctor" && args[1] !== "cleanup") {
804
+ if (args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
805
+ write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json]\n");
806
+ return 0;
807
+ }
808
+ try {
809
+ const { json, ...parsed } = parseDoctorArgs(args.slice(1));
810
+ const report = await doctor({ ...options, ...parsed });
811
+ write(json ? `${JSON.stringify(report)}\n` : formatDoctorReport(report));
812
+ return doctorExitCode(report);
813
+ }
814
+ catch (error) {
815
+ stderr(`Error: ${errorText(error)}\n`);
816
+ return 1;
817
+ }
818
+ }
819
+ if (args[0] === "doctor" && args[1] === "cleanup") {
820
+ if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
821
+ write("Usage: piewf doctor cleanup [--older-than-days <days>] [--yes]\n");
822
+ return 0;
823
+ }
824
+ try {
825
+ const parsed = parseDoctorCleanupArgs(args.slice(2));
826
+ const cleanupOptions = { ...parsed, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}) };
827
+ const report = await doctorCleanup(cleanupOptions);
828
+ write(formatDoctorCleanupReport(report));
829
+ return doctorCleanupExitCode(report);
830
+ }
831
+ catch (error) {
832
+ stderr(`Error: ${errorText(error)}\n`);
833
+ return 1;
834
+ }
835
+ }
836
+ if (args[0] === "inspect") {
837
+ try {
838
+ const parsed = parseInspectArgs(args.slice(1));
839
+ if (options.inspect)
840
+ await options.inspect(parsed.sessionId, parsed.mode, parsed.failedOnly);
841
+ else
842
+ await runSessionInspector(parsed.sessionId, parsed.mode, options.cwd ?? process.cwd(), undefined, write, parsed.failedOnly);
843
+ return 0;
844
+ }
845
+ catch (error) {
846
+ write(`Error: ${errorText(error)}\n`);
847
+ return 1;
848
+ }
849
+ }
850
+ if (args[0] === "transcript" && args.length === 2) {
851
+ try {
852
+ const transcript = requiredArg(args, 1);
853
+ if (options.transcript)
854
+ await options.transcript(transcript);
855
+ else
856
+ write(`${transcriptFileLines(transcript).join("\n")}\n`);
857
+ return 0;
858
+ }
859
+ catch (error) {
860
+ write(`Error: ${errorText(error)}\n`);
861
+ return 1;
862
+ }
863
+ }
864
+ if (args[0] === "share") {
865
+ if (args.length !== 2 || args[1] === "--help" || args[1] === "-h") {
866
+ write("Usage: piewf share <run-id>\n\nExports the run as a static Trajectory report and uploads it as a secret GitHub gist via the gh CLI.\nSecret gists are unlisted, not private: anyone with the link can read the full report.\n");
867
+ return args.some((arg) => arg === "--help" || arg === "-h") ? 0 : 1;
868
+ }
869
+ try {
870
+ const runId = requiredArg(args, 1);
871
+ const cwd = options.cwd ?? process.cwd();
872
+ let sessionId;
873
+ for (const candidate of await listPersistedSessionIds(cwd)) {
874
+ if ((await listRunIds(cwd, candidate, homedir(), false)).includes(runId)) {
875
+ sessionId = candidate;
876
+ break;
877
+ }
878
+ }
879
+ if (!sessionId) {
880
+ stderr(`Error: workflow run ${runId} was not found under ${cwd}\n`);
881
+ return 1;
882
+ }
883
+ const result = await shareTrajectoryRun({ cwd, sessionId, runId });
884
+ write(`Share URL: ${result.shareUrl}\nGist: ${result.gistUrl}\nSecret gist: anyone with the link can read the full report.\n`);
885
+ return 0;
886
+ }
887
+ catch (error) {
888
+ stderr(`Error: ${errorText(error)}\n`);
889
+ return 1;
890
+ }
891
+ }
892
+ if (args[0] === "bundle" || args[0] === "run" || args[0] === "export") {
893
+ try {
894
+ const workflowOptions = { write, stderr, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), ...(options.agentDir !== undefined ? { agentDir: options.agentDir } : {}), ...(options.signal ? { signal: options.signal } : {}), ...(options.trustOverride !== undefined ? { trustOverride: options.trustOverride } : {}), ...(options.isTTY !== undefined ? { isTTY: options.isTTY } : {}), ...(options.skillPaths?.length ? { skillPaths: [...options.skillPaths] } : {}) };
895
+ if (args[0] === "bundle")
896
+ return await bundleWorkflowCli(args.slice(1), workflowOptions);
897
+ return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
898
+ }
899
+ catch (error) {
900
+ stderr(`Error: ${errorText(error)}\n`);
901
+ return 1;
902
+ }
903
+ }
904
+ write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | share <run-id> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | run --script <path> [--name <workflow-name>] [--input <json>] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
905
+ return 1;
906
+ }
907
+ if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
908
+ const controller = new AbortController();
909
+ const onSignal = () => { controller.abort(); };
910
+ process.once("SIGINT", onSignal);
911
+ process.exitCode = await runCli(process.argv.slice(2), { signal: controller.signal });
912
+ }