pi-extensible-workflows 1.0.1 → 3.0.0
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/README.md +9 -2
- package/dist/src/agent-execution.d.ts +13 -14
- package/dist/src/agent-execution.js +110 -197
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +536 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -29
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/host.d.ts +62 -0
- package/dist/src/host.js +2696 -0
- package/dist/src/index.d.ts +11 -557
- package/dist/src/index.js +8 -3974
- package/dist/src/persistence.d.ts +32 -19
- package/dist/src/persistence.js +310 -70
- package/dist/src/registry.d.ts +31 -0
- package/dist/src/registry.js +169 -0
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +4 -1
- package/dist/src/types.d.ts +565 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +128 -0
- package/dist/src/validation.d.ts +24 -0
- package/dist/src/validation.js +740 -0
- package/dist/src/workflow-evals.js +2 -1
- package/package.json +4 -3
- package/skills/pi-extensible-workflows/SKILL.md +52 -67
- package/src/agent-execution.ts +84 -147
- package/src/budget.ts +75 -0
- package/src/cli.ts +427 -6
- package/src/doctor.ts +13 -32
- package/src/execution.ts +540 -0
- package/src/herdr.ts +73 -0
- package/src/host.ts +2265 -0
- package/src/index.ts +11 -3453
- package/src/persistence.ts +255 -47
- package/src/registry.ts +157 -0
- package/src/session-inspector.ts +5 -1
- package/src/types.ts +113 -0
- package/src/utils.ts +108 -0
- package/src/validation.ts +616 -0
- package/src/workflow-evals.ts +2 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,9 +1,512 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { chmodSync, linkSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Value } from "typebox/value";
|
|
4
9
|
import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
5
|
-
import {
|
|
10
|
+
import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog } from "./index.js";
|
|
11
|
+
import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
|
|
12
|
+
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
13
|
+
function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
14
|
+
function clone(value) { return structuredClone(value); }
|
|
15
|
+
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(); }
|
|
16
|
+
function scalarType(schema) {
|
|
17
|
+
if (!object(schema) || typeof schema.type !== "string")
|
|
18
|
+
return undefined;
|
|
19
|
+
return ["string", "integer", "number", "boolean"].includes(schema.type) ? schema.type : undefined;
|
|
20
|
+
}
|
|
21
|
+
function schemaPlan(schema) {
|
|
22
|
+
if (!object(schema) || schema.type !== "object")
|
|
23
|
+
return { fields: [] };
|
|
24
|
+
const properties = object(schema.properties) ? schema.properties : {};
|
|
25
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
|
|
26
|
+
const fields = [];
|
|
27
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
28
|
+
if (!object(property))
|
|
29
|
+
continue;
|
|
30
|
+
const directType = scalarType(property);
|
|
31
|
+
const itemType = object(property.items) ? scalarType(property.items) : undefined;
|
|
32
|
+
const type = directType ?? itemType ? directType ?? "array" : undefined;
|
|
33
|
+
if (!type)
|
|
34
|
+
continue;
|
|
35
|
+
fields.push({ name, option: `--${kebabCase(name)}`, schema: property, type, ...(itemType ? { itemType } : {}), required: required.has(name) });
|
|
36
|
+
}
|
|
37
|
+
const requiredScalars = fields.filter((field) => field.required && field.type !== "array");
|
|
38
|
+
return { fields, ...(requiredScalars.length === 1 ? { positional: requiredScalars[0] } : {}) };
|
|
39
|
+
}
|
|
40
|
+
function scalarLabel(type) { return type === "integer" ? "integer" : type; }
|
|
41
|
+
function scalarFieldType(field) { return field.type === "array" ? field.itemType : field.type; }
|
|
42
|
+
function fieldLabel(field) { return field.type === "array" ? `${field.option} <${scalarLabel(field.itemType)}>` : `${field.option}${field.type === "boolean" ? "" : ` <${scalarLabel(field.type)}>`}`; }
|
|
43
|
+
function fieldDescription(field) {
|
|
44
|
+
const description = typeof field.schema.description === "string" ? field.schema.description.trim() : "";
|
|
45
|
+
const required = field.required ? "required" : "optional";
|
|
46
|
+
const defaultValue = has(field.schema, "default") ? ` default=${JSON.stringify(field.schema.default)}` : "";
|
|
47
|
+
const enumSchema = field.type === "array" && object(field.schema.items) ? field.schema.items : field.schema;
|
|
48
|
+
const enumValue = Array.isArray(enumSchema.enum) ? ` enum=${enumSchema.enum.map((value) => JSON.stringify(value)).join(",")}` : "";
|
|
49
|
+
return [description, required, defaultValue, enumValue].filter(Boolean).join("; ");
|
|
50
|
+
}
|
|
51
|
+
export function formatWorkflowCliHelp(fn, command = "pi-extensible-workflows") {
|
|
52
|
+
const plan = schemaPlan(fn.input);
|
|
53
|
+
const lines = [`Usage: ${command} run ${fn.name}${plan.positional ? ` <${plan.positional.name}>` : ""} [options]`, "", fn.description];
|
|
54
|
+
if (plan.positional) {
|
|
55
|
+
lines.push("", "Arguments:", ` <${plan.positional.name}> ${scalarLabel(scalarFieldType(plan.positional))}; ${fieldDescription(plan.positional)}`);
|
|
56
|
+
}
|
|
57
|
+
lines.push("", "Options:");
|
|
58
|
+
for (const field of plan.fields) {
|
|
59
|
+
const label = field === plan.positional ? `${field.option} <${scalarLabel(scalarFieldType(field))}>` : fieldLabel(field);
|
|
60
|
+
lines.push(` ${label.padEnd(24)}${fieldDescription(field)}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(" --input <json>".padEnd(28) + "JSON input escape hatch for complex schemas", ...launcherHelpLines(), " -h, --help".padEnd(28) + "Show this help");
|
|
63
|
+
return `${lines.join("\n")}\n`;
|
|
64
|
+
}
|
|
65
|
+
function enumAllows(schema, value) {
|
|
66
|
+
return !Array.isArray(schema.enum) || schema.enum.some((candidate) => JSON.stringify(candidate) === JSON.stringify(value));
|
|
67
|
+
}
|
|
68
|
+
function coerce(raw, type, schema) {
|
|
69
|
+
let value;
|
|
70
|
+
if (type === "string")
|
|
71
|
+
value = raw;
|
|
72
|
+
else if (type === "integer") {
|
|
73
|
+
if (!/^-?(?:0|[1-9]\d*)$/.test(raw))
|
|
74
|
+
throw new Error(`Invalid integer: ${raw}`);
|
|
75
|
+
value = Number(raw);
|
|
76
|
+
if (!Number.isSafeInteger(value))
|
|
77
|
+
throw new Error(`Invalid integer: ${raw}`);
|
|
78
|
+
}
|
|
79
|
+
else if (type === "number") {
|
|
80
|
+
value = Number(raw);
|
|
81
|
+
if (!Number.isFinite(value))
|
|
82
|
+
throw new Error(`Invalid number: ${raw}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (raw !== "true" && raw !== "false")
|
|
86
|
+
throw new Error(`Invalid boolean: ${raw}`);
|
|
87
|
+
value = raw === "true";
|
|
88
|
+
}
|
|
89
|
+
if (!enumAllows(schema, value))
|
|
90
|
+
throw new Error(`Invalid value for enum: ${raw}`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function parseJsonInput(value) {
|
|
94
|
+
try {
|
|
95
|
+
return clone(JSON.parse(value));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error("Invalid JSON passed to --input");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function parseWorkflowCliArgs(schema, rawArgs) {
|
|
102
|
+
const plan = schemaPlan(schema);
|
|
103
|
+
const fields = new Map(plan.fields.map((field) => [field.option, field]));
|
|
104
|
+
const result = {};
|
|
105
|
+
let input;
|
|
106
|
+
let positionalUsed = false;
|
|
107
|
+
let endOptions = false;
|
|
108
|
+
const assign = (field, raw) => {
|
|
109
|
+
if (field.type === "array") {
|
|
110
|
+
const values = Array.isArray(result[field.name]) ? result[field.name] : [];
|
|
111
|
+
values.push(coerce(raw, field.itemType, field.schema.items));
|
|
112
|
+
result[field.name] = values;
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
result[field.name] = coerce(raw, field.type, field.schema);
|
|
116
|
+
};
|
|
117
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
118
|
+
const token = rawArgs[index];
|
|
119
|
+
if (token === "--") {
|
|
120
|
+
endOptions = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!endOptions && (token === "--input" || token.startsWith("--input="))) {
|
|
124
|
+
if (input !== undefined)
|
|
125
|
+
throw new Error("--input may only be provided once");
|
|
126
|
+
const raw = token.startsWith("--input=") ? token.slice("--input=".length) : rawArgs[++index];
|
|
127
|
+
if (raw === undefined)
|
|
128
|
+
throw new Error("Missing value for --input");
|
|
129
|
+
input = parseJsonInput(raw);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!endOptions && token.startsWith("--")) {
|
|
133
|
+
const equals = token.indexOf("=");
|
|
134
|
+
const option = equals >= 0 ? token.slice(0, equals) : token;
|
|
135
|
+
const negated = equals < 0 && option.startsWith("--no-");
|
|
136
|
+
const field = fields.get(negated ? `--${option.slice("--no-".length)}` : option);
|
|
137
|
+
if (!field)
|
|
138
|
+
throw new Error(`Unknown option: ${option}`);
|
|
139
|
+
if (negated) {
|
|
140
|
+
if (field.type !== "boolean")
|
|
141
|
+
throw new Error(`Invalid boolean option: ${option}`);
|
|
142
|
+
result[field.name] = false;
|
|
143
|
+
}
|
|
144
|
+
else if (field.type === "boolean") {
|
|
145
|
+
if (equals >= 0)
|
|
146
|
+
assign(field, token.slice(equals + 1));
|
|
147
|
+
else if (rawArgs[index + 1] === "true" || rawArgs[index + 1] === "false")
|
|
148
|
+
assign(field, rawArgs[++index]);
|
|
149
|
+
else
|
|
150
|
+
result[field.name] = true;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const raw = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
|
|
154
|
+
if (raw === undefined || raw.startsWith("--"))
|
|
155
|
+
throw new Error(`Missing value for ${option}`);
|
|
156
|
+
assign(field, raw);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const positional = plan.positional;
|
|
161
|
+
const numericNegative = positional && (positional.type === "integer" || positional.type === "number") && /^-\d/.test(token);
|
|
162
|
+
if (!endOptions && token.startsWith("-") && !numericNegative)
|
|
163
|
+
throw new Error(`Unknown option: ${token}`);
|
|
164
|
+
if (!positional || positionalUsed)
|
|
165
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
166
|
+
assign(positional, token);
|
|
167
|
+
positionalUsed = true;
|
|
168
|
+
}
|
|
169
|
+
if (input !== undefined) {
|
|
170
|
+
if (Object.keys(result).length || positionalUsed)
|
|
171
|
+
throw new Error("--input cannot be combined with CLI arguments");
|
|
172
|
+
if (!object(input))
|
|
173
|
+
throw new Error("Workflow input must be a JSON object");
|
|
174
|
+
for (const field of plan.fields)
|
|
175
|
+
if (!has(input, field.name) && has(field.schema, "default"))
|
|
176
|
+
input[field.name] = clone(field.schema.default);
|
|
177
|
+
return input;
|
|
178
|
+
}
|
|
179
|
+
for (const field of plan.fields)
|
|
180
|
+
if (!has(result, field.name) && has(field.schema, "default"))
|
|
181
|
+
result[field.name] = clone(field.schema.default);
|
|
182
|
+
for (const field of plan.fields)
|
|
183
|
+
if (field.required && !has(result, field.name))
|
|
184
|
+
throw new Error(`Missing required argument: ${field.name}`);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function launcherHelpLines() {
|
|
188
|
+
return [
|
|
189
|
+
" --approve".padEnd(28) + "Trust project resources for this launch",
|
|
190
|
+
" --no-approve".padEnd(28) + "Do not trust project resources for this launch",
|
|
191
|
+
" --".padEnd(28) + "End launcher option parsing; pass later tokens to workflow input",
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
function workflowUsage() { return [`Usage: pi-extensible-workflows run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
195
|
+
function exportUsage() { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
|
|
196
|
+
function stripTrustOptions(rawArgs) {
|
|
197
|
+
const args = [];
|
|
198
|
+
let trustOverride;
|
|
199
|
+
let endOptions = false;
|
|
200
|
+
for (const arg of rawArgs) {
|
|
201
|
+
if (arg === "--") {
|
|
202
|
+
endOptions = true;
|
|
203
|
+
args.push(arg);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!endOptions && (arg === "--approve" || arg === "--no-approve")) {
|
|
207
|
+
const next = arg === "--approve";
|
|
208
|
+
if (trustOverride !== undefined && trustOverride !== next)
|
|
209
|
+
throw new Error("--approve and --no-approve cannot be combined");
|
|
210
|
+
trustOverride = next;
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
args.push(arg);
|
|
214
|
+
}
|
|
215
|
+
return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
|
|
216
|
+
}
|
|
217
|
+
async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
218
|
+
const cwd = options.cwd ?? process.cwd();
|
|
219
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
220
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
221
|
+
const requiredTrust = hasTrustRequiringProjectResources(cwd);
|
|
222
|
+
const trustStore = new ProjectTrustStore(agentDir);
|
|
223
|
+
const defaultProjectTrust = settingsManager.getDefaultProjectTrust();
|
|
224
|
+
const resolveProjectTrust = async ({ extensionsResult }) => {
|
|
225
|
+
if (options.trustOverride !== undefined)
|
|
226
|
+
return options.trustOverride;
|
|
227
|
+
if (!requiredTrust)
|
|
228
|
+
return true;
|
|
229
|
+
const projectTrustContext = {
|
|
230
|
+
cwd,
|
|
231
|
+
mode: "print",
|
|
232
|
+
hasUI: false,
|
|
233
|
+
ui: { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: () => { } },
|
|
234
|
+
};
|
|
235
|
+
for (const extension of extensionsResult.extensions) {
|
|
236
|
+
for (const handler of extension.handlers.get("project_trust") ?? []) {
|
|
237
|
+
try {
|
|
238
|
+
const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
|
|
239
|
+
if (result.trusted === "undecided")
|
|
240
|
+
continue;
|
|
241
|
+
if (result.trusted !== "yes" && result.trusted !== "no")
|
|
242
|
+
continue;
|
|
243
|
+
const trusted = result.trusted === "yes";
|
|
244
|
+
if (result.remember === true)
|
|
245
|
+
trustStore.set(cwd, trusted);
|
|
246
|
+
return trusted;
|
|
247
|
+
}
|
|
248
|
+
catch { /* Project trust extensions are best effort, as in Pi. */ }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const savedTrust = trustStore.get(cwd);
|
|
252
|
+
if (savedTrust !== null)
|
|
253
|
+
return savedTrust;
|
|
254
|
+
return defaultProjectTrust === "always";
|
|
255
|
+
};
|
|
256
|
+
const services = await createAgentSessionServices({
|
|
257
|
+
cwd,
|
|
258
|
+
agentDir,
|
|
259
|
+
settingsManager,
|
|
260
|
+
resourceLoaderOptions: {},
|
|
261
|
+
resourceLoaderReloadOptions: { resolveProjectTrust },
|
|
262
|
+
});
|
|
263
|
+
const extensions = services.resourceLoader.getExtensions();
|
|
264
|
+
const tools = [];
|
|
265
|
+
const activeTools = [...new Set(["read", "bash", "edit", "write"].concat(extensions.extensions.flatMap((extension) => [...extension.tools.keys()]), ["workflow"]))];
|
|
266
|
+
const headlessPi = {
|
|
267
|
+
registerTool(tool) { tools.push(tool); },
|
|
268
|
+
registerCommand() { },
|
|
269
|
+
getThinkingLevel: () => services.settingsManager.getDefaultThinkingLevel() ?? "medium",
|
|
270
|
+
getActiveTools: () => activeTools,
|
|
271
|
+
on(name, handler) { if (name === "session_shutdown" && typeof handler === "function")
|
|
272
|
+
shutdownHandlers.push(handler); },
|
|
273
|
+
appendEntry() { },
|
|
274
|
+
sendMessage() { },
|
|
275
|
+
events: { emit() { } },
|
|
276
|
+
};
|
|
277
|
+
workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir);
|
|
278
|
+
const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
|
|
279
|
+
if (!workflowTool)
|
|
280
|
+
throw new Error("The workflow runtime could not be initialized");
|
|
281
|
+
return { catalog: workflowCatalog(), services, workflowTool, shutdownHandlers };
|
|
282
|
+
}
|
|
283
|
+
function availableModelInfo(services, available = false) {
|
|
284
|
+
const models = available ? services.modelRuntime.getAvailableSnapshot() : services.modelRuntime.getModels();
|
|
285
|
+
return models.map(({ provider, id }) => ({ provider, id }));
|
|
286
|
+
}
|
|
287
|
+
async function selectedModel(services) {
|
|
288
|
+
const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), noTools: "all" });
|
|
289
|
+
try {
|
|
290
|
+
const model = session.model;
|
|
291
|
+
return model ? { provider: model.provider, id: model.id } : undefined;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
session.dispose();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function shellQuote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }
|
|
298
|
+
function commandName(value) { return value.trim() && !value.includes("/") && !value.includes("\\") ? value.trim() : ""; }
|
|
299
|
+
function writeLauncher(destination, workflowName, force) {
|
|
300
|
+
const parent = dirname(destination);
|
|
301
|
+
mkdirSync(parent, { recursive: true });
|
|
302
|
+
const tempDir = mkdtempSync(join(parent, ".pi-extensible-workflows-"));
|
|
303
|
+
const tempPath = join(tempDir, "launcher");
|
|
304
|
+
try {
|
|
305
|
+
writeFileSync(tempPath, `#!/bin/sh\nexec node ${shellQuote(fileURLToPath(import.meta.url))} run ${shellQuote(workflowName)} "$@"\n`, { mode: 0o755 });
|
|
306
|
+
chmodSync(tempPath, 0o755);
|
|
307
|
+
if (force)
|
|
308
|
+
renameSync(tempPath, destination);
|
|
309
|
+
else {
|
|
310
|
+
try {
|
|
311
|
+
linkSync(tempPath, destination);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error.code === "EEXIST")
|
|
315
|
+
throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function terminalProgressStyles(enabled) {
|
|
325
|
+
const style = (code) => enabled ? (text) => `\x1b[${String(code)}m${text}\x1b[0m` : (text) => text;
|
|
326
|
+
return { accent: style(36), success: style(32), error: style(31), warning: style(33), muted: style(90), dim: style(2), bold: style(1) };
|
|
327
|
+
}
|
|
328
|
+
class CliProgress {
|
|
329
|
+
stderr;
|
|
330
|
+
#lastStable = "";
|
|
331
|
+
#lines = 0;
|
|
332
|
+
#frame = 0;
|
|
333
|
+
#run;
|
|
334
|
+
#timer;
|
|
335
|
+
#interactive;
|
|
336
|
+
#styles;
|
|
337
|
+
constructor(stderr, tty) {
|
|
338
|
+
this.stderr = stderr;
|
|
339
|
+
this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
340
|
+
this.#styles = terminalProgressStyles(this.#interactive);
|
|
341
|
+
}
|
|
342
|
+
update(run) {
|
|
343
|
+
const stable = formatWorkflowProgress(run, "◇", this.#styles);
|
|
344
|
+
if (!this.#interactive) {
|
|
345
|
+
if (stable !== this.#lastStable) {
|
|
346
|
+
this.#lastStable = stable;
|
|
347
|
+
this.stderr(`${stable}\n`);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.#run = run;
|
|
352
|
+
this.#timer ??= setInterval(() => { this.render(); }, 80);
|
|
353
|
+
this.#timer.unref();
|
|
354
|
+
this.render();
|
|
355
|
+
}
|
|
356
|
+
render() {
|
|
357
|
+
if (!this.#run)
|
|
358
|
+
return;
|
|
359
|
+
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
|
|
360
|
+
const width = process.stderr.columns || 80;
|
|
361
|
+
const text = truncateWorkflowProgress(formatWorkflowProgress(this.#run, spinner, this.#styles), width).join("\n");
|
|
362
|
+
this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
|
|
363
|
+
this.#lines = text.split("\n").length;
|
|
364
|
+
}
|
|
365
|
+
finish() {
|
|
366
|
+
if (this.#timer) {
|
|
367
|
+
clearInterval(this.#timer);
|
|
368
|
+
this.#timer = undefined;
|
|
369
|
+
}
|
|
370
|
+
if (this.#interactive && this.#lines) {
|
|
371
|
+
this.stderr(`\x1b[${String(this.#lines)}A\x1b[0J\x1b[?25h`);
|
|
372
|
+
this.#lines = 0;
|
|
373
|
+
}
|
|
374
|
+
this.#run = undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function invokeWorkflow(fn, args, runtime, options, context) {
|
|
378
|
+
if (!Value.Check(fn.input, args))
|
|
379
|
+
throw new Error(`Invalid input for ${fn.name}`);
|
|
380
|
+
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY);
|
|
381
|
+
try {
|
|
382
|
+
const result = await runtime.workflowTool.execute(randomUUID(), { workflow: fn.name, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && object(update.details.run))
|
|
383
|
+
progress.update(update.details.run); }, context);
|
|
384
|
+
const details = object(result.details) ? result.details : {};
|
|
385
|
+
if (has(details, "value"))
|
|
386
|
+
return details.value;
|
|
387
|
+
const first = result.content[0];
|
|
388
|
+
if (!first || first.type !== "text")
|
|
389
|
+
throw new Error("Workflow returned no result");
|
|
390
|
+
try {
|
|
391
|
+
return parseJsonInput(first.text);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw new Error("Workflow returned invalid JSON");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
progress.finish();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function createWorkflowContext(runtime, options) {
|
|
402
|
+
const model = await selectedModel(runtime.services);
|
|
403
|
+
const sessionManager = SessionManager.inMemory();
|
|
404
|
+
const modelRegistry = { getAll: () => availableModelInfo(runtime.services), getAvailable: () => availableModelInfo(runtime.services, true) };
|
|
405
|
+
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 };
|
|
406
|
+
}
|
|
407
|
+
async function shutdownWorkflowRuntime(handlers, context) {
|
|
408
|
+
for (const handler of handlers) {
|
|
409
|
+
try {
|
|
410
|
+
await handler({ type: "session_shutdown", reason: "quit" }, context);
|
|
411
|
+
}
|
|
412
|
+
catch { /* Shutdown is best effort. */ }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function withWorkflowRuntime(options, action) {
|
|
416
|
+
const shutdownHandlers = [];
|
|
417
|
+
let context = { cwd: options.cwd ?? process.cwd(), mode: "print", hasUI: false, headless: true };
|
|
418
|
+
try {
|
|
419
|
+
const runtime = await createWorkflowRuntime(options, shutdownHandlers);
|
|
420
|
+
context = await createWorkflowContext(runtime, options);
|
|
421
|
+
return await action(runtime, context);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
await shutdownWorkflowRuntime(shutdownHandlers, context);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function runWorkflowCli(rawArgs, options) {
|
|
428
|
+
const parsed = stripTrustOptions(rawArgs);
|
|
429
|
+
const args = parsed.args;
|
|
430
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
431
|
+
options.write(workflowUsage());
|
|
432
|
+
return args.length ? 0 : 1;
|
|
433
|
+
}
|
|
434
|
+
const name = args[0];
|
|
435
|
+
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime, context) => {
|
|
436
|
+
const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
|
|
437
|
+
const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
|
|
438
|
+
if (!fn)
|
|
439
|
+
throw new Error(`Unknown workflow function: ${name}`);
|
|
440
|
+
if (help) {
|
|
441
|
+
options.write(formatWorkflowCliHelp(fn));
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
const input = parseWorkflowCliArgs(fn.input, args.slice(1));
|
|
445
|
+
const value = await invokeWorkflow(fn, input, runtime, options, context);
|
|
446
|
+
options.write(`${JSON.stringify(value)}\n`);
|
|
447
|
+
return 0;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
async function exportWorkflowCli(rawArgs, options) {
|
|
451
|
+
const parsed = stripTrustOptions(rawArgs);
|
|
452
|
+
const args = parsed.args;
|
|
453
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h") {
|
|
454
|
+
options.write(exportUsage());
|
|
455
|
+
return args.length ? 0 : 1;
|
|
456
|
+
}
|
|
457
|
+
const workflowName = args[0];
|
|
458
|
+
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
459
|
+
let name;
|
|
460
|
+
let output;
|
|
461
|
+
let force = false;
|
|
462
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
463
|
+
const arg = args[index];
|
|
464
|
+
if (arg === "--force") {
|
|
465
|
+
force = true;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const equals = arg.indexOf("=");
|
|
469
|
+
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
470
|
+
if (option === "--name" || option === "--output") {
|
|
471
|
+
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
472
|
+
if (!value)
|
|
473
|
+
throw new Error(`Missing value for ${option}`);
|
|
474
|
+
if (option === "--name")
|
|
475
|
+
name = value;
|
|
476
|
+
else
|
|
477
|
+
output = value;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (arg === "--help" || arg === "-h") {
|
|
481
|
+
options.write(exportUsage());
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
485
|
+
}
|
|
486
|
+
if (!runtime.catalog.functions.some((candidate) => candidate.name === workflowName))
|
|
487
|
+
throw new Error(`Unknown workflow function: ${workflowName}`);
|
|
488
|
+
const command = commandName(name ?? kebabCase(workflowName));
|
|
489
|
+
if (!command)
|
|
490
|
+
throw new Error("Command name must be a non-empty name without path separators");
|
|
491
|
+
const destination = output ? output : join(homedir(), ".local", "bin", command);
|
|
492
|
+
writeLauncher(destination, workflowName, force);
|
|
493
|
+
if (!output) {
|
|
494
|
+
const binDir = join(homedir(), ".local", "bin");
|
|
495
|
+
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean).map((entry) => { try {
|
|
496
|
+
return realpathSync(entry);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return entry;
|
|
500
|
+
} });
|
|
501
|
+
if (!pathEntries.includes(binDir))
|
|
502
|
+
options.stderr(`Warning: ${binDir} is not in PATH\n`);
|
|
503
|
+
}
|
|
504
|
+
options.write(`Exported ${destination}\n`);
|
|
505
|
+
return 0;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
6
508
|
export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
|
|
509
|
+
const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
|
|
7
510
|
if (args[0] === "doctor" && args.length === 1) {
|
|
8
511
|
const report = await doctor(options);
|
|
9
512
|
write(formatDoctorReport(report));
|
|
@@ -19,8 +522,35 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
19
522
|
return 1;
|
|
20
523
|
}
|
|
21
524
|
}
|
|
22
|
-
|
|
525
|
+
if (args[0] === "transcript" && args.length === 2) {
|
|
526
|
+
try {
|
|
527
|
+
if (options.transcript)
|
|
528
|
+
await options.transcript(args[1]);
|
|
529
|
+
else
|
|
530
|
+
write(`${transcriptFileLines(args[1]).join("\n")}\n`);
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (args[0] === "run" || args[0] === "export") {
|
|
539
|
+
try {
|
|
540
|
+
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 } : {}) };
|
|
541
|
+
return args[0] === "run" ? await runWorkflowCli(args.slice(1), workflowOptions) : await exportWorkflowCli(args.slice(1), workflowOptions);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
write("Usage: pi-extensible-workflows doctor | inspect [session-id] | transcript <session-file> | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]\n");
|
|
23
549
|
return 1;
|
|
24
550
|
}
|
|
25
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href)
|
|
26
|
-
|
|
551
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
552
|
+
const controller = new AbortController();
|
|
553
|
+
const onSignal = () => { controller.abort(); };
|
|
554
|
+
process.once("SIGINT", onSignal);
|
|
555
|
+
process.exitCode = await runCli(process.argv.slice(2), { signal: controller.signal });
|
|
556
|
+
}
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentResourcePolicy, type
|
|
1
|
+
import { type AgentResourcePolicy, type WorkflowFunction, type WorkflowSettings } from "./index.js";
|
|
2
2
|
export type DoctorSeverity = "error" | "warning";
|
|
3
3
|
export interface DoctorDiagnostic {
|
|
4
4
|
severity: DoctorSeverity;
|
|
@@ -15,7 +15,7 @@ export interface DoctorRole {
|
|
|
15
15
|
overrides?: string;
|
|
16
16
|
overriddenBy?: string;
|
|
17
17
|
}
|
|
18
|
-
export interface
|
|
18
|
+
export interface DoctorFunction {
|
|
19
19
|
name: string;
|
|
20
20
|
description: string;
|
|
21
21
|
valid: boolean;
|
|
@@ -36,7 +36,7 @@ export interface DoctorPiState {
|
|
|
36
36
|
}[];
|
|
37
37
|
extensions?: readonly string[];
|
|
38
38
|
skills?: readonly string[];
|
|
39
|
-
|
|
39
|
+
functions: Readonly<Record<string, WorkflowFunction>>;
|
|
40
40
|
}
|
|
41
41
|
export interface DoctorReport {
|
|
42
42
|
cwd: string;
|
|
@@ -46,7 +46,7 @@ export interface DoctorReport {
|
|
|
46
46
|
trust: DoctorTrust;
|
|
47
47
|
activeTools: readonly string[];
|
|
48
48
|
roles: readonly DoctorRole[];
|
|
49
|
-
|
|
49
|
+
functions: readonly DoctorFunction[];
|
|
50
50
|
resourcePolicy: AgentResourcePolicy;
|
|
51
51
|
diagnostics: readonly DoctorDiagnostic[];
|
|
52
52
|
}
|
package/dist/src/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
4
4
|
import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveModelReference, parseRoleMarkdown,
|
|
5
|
+
import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveModelReference, parseRoleMarkdown, registeredWorkflowFunctions, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "./index.js";
|
|
6
6
|
const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
|
|
7
7
|
function canonical(path) {
|
|
8
8
|
const absolute = resolve(path);
|
|
@@ -88,7 +88,7 @@ async function discoverPi(cwd, agentDir) {
|
|
|
88
88
|
...extensions.errors.map(({ path, error }) => ({ path, message: error })),
|
|
89
89
|
...services.diagnostics.filter(({ type }) => type === "error").map(({ message }) => ({ message })),
|
|
90
90
|
],
|
|
91
|
-
|
|
91
|
+
functions: registeredWorkflowFunctions(),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
finally {
|
|
@@ -179,7 +179,7 @@ export async function doctor(options = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
catch (error) {
|
|
181
181
|
diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${error.message}`, undefined, "Open and trust the project in Pi, fix extension errors, then rerun doctor."));
|
|
182
|
-
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [],
|
|
182
|
+
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
|
|
183
183
|
}
|
|
184
184
|
if (options.activeTools)
|
|
185
185
|
pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
|
|
@@ -237,34 +237,14 @@ export async function doctor(options = {}) {
|
|
|
237
237
|
else
|
|
238
238
|
definitions.delete(name);
|
|
239
239
|
}
|
|
240
|
-
const
|
|
241
|
-
for (const [name,
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const checked = preflight(workflow.script, {
|
|
245
|
-
models: new Set(pi.knownModels),
|
|
246
|
-
tools: activeTools,
|
|
247
|
-
agentTypes: new Set(definitions.keys()),
|
|
248
|
-
modelAliases: aliases,
|
|
249
|
-
knownModels,
|
|
250
|
-
settingsPath,
|
|
251
|
-
skipModelAvailability: true,
|
|
252
|
-
}, [], { name, description: workflow.description });
|
|
253
|
-
for (const model of checked.referenced.models)
|
|
254
|
-
if (!knownModels.has(model) || !availableModels.has(model))
|
|
255
|
-
diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${model}`, name));
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
valid = false;
|
|
259
|
-
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
260
|
-
diagnostics.push(diagnostic("error", `WORKFLOW_${typed.code}`, typed.message, name, "Fix the registered workflow source or its referenced role/tool/model/extension."));
|
|
261
|
-
}
|
|
262
|
-
workflows.push({ name, description: workflow.description, valid });
|
|
240
|
+
const functions = [];
|
|
241
|
+
for (const [name, fn] of Object.entries(pi.functions).sort(([left], [right]) => left.localeCompare(right))) {
|
|
242
|
+
functions.push({ name, description: fn.description, valid: true });
|
|
263
243
|
}
|
|
264
244
|
const severityOrder = { error: 0, warning: 1 };
|
|
265
245
|
diagnostics.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || (left.source ?? "").localeCompare(right.source ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message));
|
|
266
246
|
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
267
|
-
return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles,
|
|
247
|
+
return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, resourcePolicy, diagnostics };
|
|
268
248
|
}
|
|
269
249
|
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
270
250
|
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
@@ -299,8 +279,8 @@ export function formatDoctorReport(report) {
|
|
|
299
279
|
"## Roles",
|
|
300
280
|
...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
|
|
301
281
|
"",
|
|
302
|
-
"## Reusable
|
|
303
|
-
...(report.
|
|
282
|
+
"## Reusable functions",
|
|
283
|
+
...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),
|
|
304
284
|
"",
|
|
305
285
|
"## Diagnostics",
|
|
306
286
|
...(report.diagnostics.length ? report.diagnostics.map((item) => `- [${item.severity}] ${item.code}${item.source ? ` \`${item.source}\`` : ""}: ${item.message}${item.hint ? ` Fix: ${item.hint}` : ""}`) : ["- [ok] No diagnostics"]),
|