@piewf/cli 5.1.1 → 5.3.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.
@@ -2,13 +2,50 @@ import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, re
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
+ function isJsonObject(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function readJsonObject(path) {
9
+ try {
10
+ const value = JSON.parse(readFileSync(path, "utf8"));
11
+ return isJsonObject(value) ? value : undefined;
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ function readPackageMetadata(path) {
18
+ const value = readJsonObject(path);
19
+ if (!value)
20
+ return undefined;
21
+ const metadata = {};
22
+ if (typeof value.name === "string")
23
+ metadata.name = value.name;
24
+ if (typeof value.version === "string")
25
+ metadata.version = value.version;
26
+ if (typeof value.bin === "string")
27
+ metadata.bin = value.bin;
28
+ else if (isJsonObject(value.bin)) {
29
+ const bin = {};
30
+ let valid = true;
31
+ for (const [name, entry] of Object.entries(value.bin)) {
32
+ if (typeof entry !== "string") {
33
+ valid = false;
34
+ break;
35
+ }
36
+ bin[name] = entry;
37
+ }
38
+ if (valid)
39
+ metadata.bin = bin;
40
+ }
41
+ return metadata;
42
+ }
5
43
  function packageJson() {
6
44
  const directory = dirname(fileURLToPath(import.meta.url));
7
45
  for (const path of [join(directory, "../package.json"), join(directory, "../../package.json")]) {
8
- try {
9
- return JSON.parse(readFileSync(path, "utf8"));
10
- }
11
- catch { /* Try the source and built layouts. */ }
46
+ const metadata = readPackageMetadata(path);
47
+ if (metadata)
48
+ return metadata;
12
49
  }
13
50
  return {};
14
51
  }
@@ -300,7 +337,6 @@ function workflowModule(workflow, functionSource, withRoles, aliasTargets, exten
300
337
  " registerWorkflowExtension({",
301
338
  ` version: ${JSON.stringify("1.0.0")},`,
302
339
  ` headline: ${JSON.stringify("Portable workflow bundle")},`,
303
- ` description: ${JSON.stringify(workflow.description)},`,
304
340
  ...(withRoles ? [` roleDirectories: [new URL("./roles", import.meta.url)],`] : []),
305
341
  ...(aliases.length ? [` modelAliases: { ${aliases.map(([name, target]) => `${JSON.stringify(name)}: { resolve: () => ${JSON.stringify(target)} }`).join(", ")} },`] : []),
306
342
  " functions: {",
@@ -354,12 +390,9 @@ function copyResources(root, resources) {
354
390
  throw new Error(`Bundle resource may contain credentials and cannot be selected: ${source}`);
355
391
  let name = basename(source);
356
392
  if (kind === "dependencies") {
357
- try {
358
- const packageName = JSON.parse(readFileSync(join(source, "package.json"), "utf8")).name;
359
- if (typeof packageName === "string" && packageName.trim())
360
- name = packageName;
361
- }
362
- catch { /* Files can be dependency entry points. */ }
393
+ const packageName = readPackageMetadata(join(source, "package.json"))?.name;
394
+ if (typeof packageName === "string" && packageName.trim())
395
+ name = packageName;
363
396
  }
364
397
  if (!name || name === "." || name === ".." || name.includes("\\") || name.startsWith("/"))
365
398
  throw new Error(`Invalid bundle resource name: ${name}`);
package/dist/src/cli.js CHANGED
@@ -8,17 +8,27 @@ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionF
8
8
  import { Value } from "typebox/value";
9
9
  import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
10
10
  import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
11
- import workflowExtension, { formatWorkflowProgress, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
11
+ import workflowExtension, { formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
12
12
  import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
13
13
  import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
14
+ import { isPersistedRun } from "pi-extensible-workflows/persistence";
14
15
  function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
15
16
  function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
16
- function clone(value) { return structuredClone(value); }
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; }
17
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"; }
18
28
  function scalarType(schema) {
19
29
  if (!object(schema) || typeof schema.type !== "string")
20
30
  return undefined;
21
- return ["string", "integer", "number", "boolean"].includes(schema.type) ? schema.type : undefined;
31
+ return isCliScalar(schema.type) ? schema.type : undefined;
22
32
  }
23
33
  function schemaPlan(schema) {
24
34
  if (!object(schema) || schema.type !== "object")
@@ -40,8 +50,14 @@ function schemaPlan(schema) {
40
50
  return { fields, ...(requiredScalars.length === 1 ? { positional: requiredScalars[0] } : {}) };
41
51
  }
42
52
  function scalarLabel(type) { return type === "integer" ? "integer" : type; }
43
- function scalarFieldType(field) { return field.type === "array" ? field.itemType : field.type; }
44
- function fieldLabel(field) { return field.type === "array" ? `${field.option} <${scalarLabel(field.itemType)}>` : `${field.option}${field.type === "boolean" ? "" : ` <${scalarLabel(field.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)}>`}`; }
45
61
  function fieldDescription(field) {
46
62
  const description = typeof field.schema.description === "string" ? field.schema.description.trim() : "";
47
63
  const required = field.required ? "required" : "optional";
@@ -109,15 +125,19 @@ export function parseWorkflowCliArgs(schema, rawArgs) {
109
125
  let endOptions = false;
110
126
  const assign = (field, raw) => {
111
127
  if (field.type === "array") {
112
- const values = Array.isArray(result[field.name]) ? result[field.name] : [];
113
- values.push(coerce(raw, field.itemType, field.schema.items));
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));
114
134
  result[field.name] = values;
115
135
  }
116
136
  else
117
137
  result[field.name] = coerce(raw, field.type, field.schema);
118
138
  };
119
139
  for (let index = 0; index < rawArgs.length; index += 1) {
120
- const token = rawArgs[index];
140
+ const token = requiredArg(rawArgs, index);
121
141
  if (token === "--") {
122
142
  endOptions = true;
123
143
  continue;
@@ -147,7 +167,7 @@ export function parseWorkflowCliArgs(schema, rawArgs) {
147
167
  if (equals >= 0)
148
168
  assign(field, token.slice(equals + 1));
149
169
  else if (rawArgs[index + 1] === "true" || rawArgs[index + 1] === "false")
150
- assign(field, rawArgs[++index]);
170
+ assign(field, requiredArg(rawArgs, ++index));
151
171
  else
152
172
  result[field.name] = true;
153
173
  }
@@ -222,7 +242,7 @@ export function parseDoctorArgs(rawArgs) {
222
242
  let role;
223
243
  let prompt;
224
244
  for (let index = 0; index < rawArgs.length; index += 1) {
225
- const token = rawArgs[index];
245
+ const token = requiredArg(rawArgs, index);
226
246
  const equals = token.indexOf("=");
227
247
  const option = equals >= 0 ? token.slice(0, equals) : token;
228
248
  if (option === "--role" || option === "--prompt") {
@@ -258,7 +278,7 @@ export function parseDoctorCleanupArgs(rawArgs) {
258
278
  let yes = false;
259
279
  let seenDays = false;
260
280
  for (let index = 0; index < rawArgs.length; index += 1) {
261
- const token = rawArgs[index];
281
+ const token = requiredArg(rawArgs, index);
262
282
  if (token === "--yes") {
263
283
  yes = true;
264
284
  continue;
@@ -302,6 +322,8 @@ function stripTrustOptions(rawArgs) {
302
322
  }
303
323
  return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
304
324
  }
325
+ function isHeadlessWorkflowResult(value) { return object(value) && Array.isArray(value.content) && value.content.every((entry) => object(entry) && typeof entry.type === "string" && typeof entry.text === "string"); }
326
+ function isHeadlessWorkflowTool(value) { return object(value) && value.name === "workflow" && typeof value.execute === "function"; }
305
327
  async function createWorkflowRuntime(options, shutdownHandlers = []) {
306
328
  const cwd = options.cwd ?? process.cwd();
307
329
  const agentDir = options.agentDir ?? getAgentDir();
@@ -324,6 +346,8 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
324
346
  for (const handler of extension.handlers.get("project_trust") ?? []) {
325
347
  try {
326
348
  const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
349
+ if (!object(result))
350
+ continue;
327
351
  if (result.trusted === "undecided")
328
352
  continue;
329
353
  if (result.trusted !== "yes" && result.trusted !== "no")
@@ -363,7 +387,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
363
387
  events: { emit() { } },
364
388
  };
365
389
  workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
366
- const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
390
+ const workflowTool = tools.find(isHeadlessWorkflowTool);
367
391
  if (!workflowTool)
368
392
  throw new Error("The workflow runtime could not be initialized");
369
393
  return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
@@ -409,7 +433,7 @@ else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}
409
433
  linkSync(tempPath, destination);
410
434
  }
411
435
  catch (error) {
412
- if (error.code === "EEXIST")
436
+ if (isNodeError(error, "EEXIST"))
413
437
  throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
414
438
  throw error;
415
439
  }
@@ -502,13 +526,15 @@ async function invokeWorkflow(fn, args, runtime, options, context) {
502
526
  return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
503
527
  const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
504
528
  try {
505
- const result = await runtime.workflowTool.execute(randomUUID(), { name: fn.name, script: `return await ${fn.name}(args);`, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && object(update.details.run))
529
+ const result = await runtime.workflowTool.execute(randomUUID(), { name: fn.name, script: `return await ${fn.name}(args);`, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && isPersistedRun(update.details.run))
506
530
  progress.update(update.details.run); }, context);
531
+ if (!isHeadlessWorkflowResult(result))
532
+ throw new Error("Workflow returned an invalid result");
507
533
  const details = object(result.details) ? result.details : {};
508
534
  const runId = typeof details.runId === "string" ? details.runId : undefined;
509
535
  if (runId)
510
536
  announceRunId(runId);
511
- if (has(details, "value"))
537
+ if (has(details, "value") && jsonValue(details.value))
512
538
  return { value: details.value, ...(runId ? { runId } : {}) };
513
539
  const first = result.content[0];
514
540
  if (!first || first.type !== "text")
@@ -557,7 +583,7 @@ async function runWorkflowCli(rawArgs, options) {
557
583
  options.write(workflowUsage());
558
584
  return args.length ? 0 : 1;
559
585
  }
560
- const name = args[0];
586
+ const name = requiredArg(args, 0);
561
587
  return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime, context) => {
562
588
  const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
563
589
  const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
@@ -582,13 +608,13 @@ async function exportWorkflowCli(rawArgs, options) {
582
608
  options.write(exportUsage());
583
609
  return args.length ? 0 : 1;
584
610
  }
585
- const workflowName = args[0];
611
+ const workflowName = requiredArg(args, 0);
586
612
  return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
587
613
  let name;
588
614
  let output;
589
615
  let force = false;
590
616
  for (let index = 1; index < args.length; index += 1) {
591
- const arg = args[index];
617
+ const arg = requiredArg(args, index);
592
618
  if (arg === "--force") {
593
619
  force = true;
594
620
  continue;
@@ -640,7 +666,7 @@ async function bundleWorkflowCli(rawArgs, options) {
640
666
  options.write(bundleUsage());
641
667
  return args.length ? 0 : 1;
642
668
  }
643
- const workflowName = args[0];
669
+ const workflowName = requiredArg(args, 0);
644
670
  return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
645
671
  let name;
646
672
  let output;
@@ -648,7 +674,7 @@ async function bundleWorkflowCli(rawArgs, options) {
648
674
  const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
649
675
  const resources = { extensions: [], skills: [], static: [], dependencies: [] };
650
676
  for (let index = 1; index < args.length; index += 1) {
651
- const arg = args[index];
677
+ const arg = requiredArg(args, index);
652
678
  if (arg === "--force") {
653
679
  force = true;
654
680
  continue;
@@ -657,7 +683,7 @@ async function bundleWorkflowCli(rawArgs, options) {
657
683
  const option = equals >= 0 ? arg.slice(0, equals) : arg;
658
684
  const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
659
685
  const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
660
- if (option === "--name" || option === "--output" || Object.prototype.hasOwnProperty.call(requirementOptions, option) || Object.prototype.hasOwnProperty.call(resourceOptions, option)) {
686
+ if (option === "--name" || option === "--output" || typedOptionKey(requirementOptions, option) || typedOptionKey(resourceOptions, option)) {
661
687
  const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
662
688
  if (!value)
663
689
  throw new Error(`Missing value for ${option}`);
@@ -665,9 +691,9 @@ async function bundleWorkflowCli(rawArgs, options) {
665
691
  name = value;
666
692
  else if (option === "--output")
667
693
  output = value;
668
- else if (Object.prototype.hasOwnProperty.call(requirementOptions, option))
694
+ else if (typedOptionKey(requirementOptions, option))
669
695
  requirements[requirementOptions[option]].push(value);
670
- else
696
+ else if (typedOptionKey(resourceOptions, option))
671
697
  resources[resourceOptions[option]].push(value);
672
698
  continue;
673
699
  }
@@ -758,10 +784,11 @@ export async function runCli(args, options = {}, write = (text) => { process.std
758
784
  }
759
785
  if (args[0] === "transcript" && args.length === 2) {
760
786
  try {
787
+ const transcript = requiredArg(args, 1);
761
788
  if (options.transcript)
762
- await options.transcript(args[1]);
789
+ await options.transcript(transcript);
763
790
  else
764
- write(`${transcriptFileLines(args[1]).join("\n")}\n`);
791
+ write(`${transcriptFileLines(transcript).join("\n")}\n`);
765
792
  return 0;
766
793
  }
767
794
  catch (error) {
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { validateBudget } from "pi-extensible-workflows";
6
6
  import { RUN_STATES } from "pi-extensible-workflows";
7
- import { jsonValue, validateModelAliases } from "pi-extensible-workflows";
7
+ import { isNodeError, jsonValue, validateModelAliases } from "pi-extensible-workflows";
8
8
  import { validateSchema } from "pi-extensible-workflows";
9
9
  import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, RunStore } from "pi-extensible-workflows/persistence";
10
10
  const TERMINAL_STATES = new Set(["completed", "failed", "stopped"]);
@@ -18,6 +18,13 @@ const SCHEDULER_STATES = new Set(["queued", "running", "waiting_for_child", "pau
18
18
  const BUDGET_DIMENSIONS = new Set(["tokens", "costUsd", "durationMs", "agentLaunches"]);
19
19
  const BUDGET_EVENT_TYPES = new Set(["soft_crossed", "hard_overrun", "hard_exhausted", "adjustment_requested", "adjustment_approved", "adjustment_rejected"]);
20
20
  function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
21
+ function isList(value) { return Array.isArray(value); }
22
+ function isThinking(value) { return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].some((candidate) => candidate === value); }
23
+ function isRunState(value) { return RUN_STATES.some((candidate) => candidate === value); }
24
+ function isAgentState(value) { return typeof value === "string" && AGENT_STATES.has(value); }
25
+ function isSchedulerState(value) { return typeof value === "string" && SCHEDULER_STATES.has(value); }
26
+ function isToolCallState(value) { return ["running", "completed", "failed"].some((candidate) => candidate === value); }
27
+ function isActivityKind(value) { return ["reasoning", "tool", "text"].some((candidate) => candidate === value); }
21
28
  function textError(error) { return error instanceof Error ? error.message : String(error); }
22
29
  function positiveDays(value) { if (!Number.isSafeInteger(value) || value < 1 || !Number.isFinite(value * DAY_MS))
23
30
  throw new Error("older-than-days must be a positive integer"); return value; }
@@ -26,17 +33,17 @@ function sameNames(left, right) { return left.length === right.length && left.ev
26
33
  async function jsonFile(path) { return JSON.parse(await readFile(path, "utf8")); }
27
34
  async function requiredFile(path) { const info = await lstat(path); if (!info.isFile())
28
35
  throw new Error(`Required artifact is not a regular file: ${path}`); }
29
- function stringList(value, label, nonEmpty = false) { if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || (nonEmpty && !item)))
36
+ function stringList(value, label, nonEmpty = false) { if (!isList(value) || value.some((item) => typeof item !== "string" || (nonEmpty && !item)))
30
37
  throw new Error(`${label} is invalid`); }
31
38
  function optionalString(value, label) { if (value !== undefined && typeof value !== "string")
32
39
  throw new Error(`${label} is invalid`); }
33
- function nonNegativeInteger(value, label) { if (!Number.isSafeInteger(value) || value < 0)
40
+ function nonNegativeInteger(value, label) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
34
41
  throw new Error(`${label} is invalid`); }
35
- function positiveInteger(value, label) { if (!Number.isSafeInteger(value) || value < 1)
42
+ function positiveInteger(value, label) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
36
43
  throw new Error(`${label} is invalid`); }
37
44
  function finiteNumber(value, label) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
38
45
  throw new Error(`${label} is invalid`); }
39
- function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking)))
46
+ function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !isThinking(value.thinking)))
40
47
  throw new Error(`${label} is invalid`); }
41
48
  function accounting(value, label) { if (!object(value))
42
49
  throw new Error(`${label} is invalid`); for (const key of ["input", "output", "cacheRead", "cacheWrite", "cost"])
@@ -47,7 +54,7 @@ function resourceExclusions(value, label) { if (value === undefined)
47
54
  stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
48
55
  stringList(value.extensions, `${label}.extensions`); }
49
56
  function agentDefinition(value, label) { if (!object(value))
50
- throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking))
57
+ throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
51
58
  throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined)
52
59
  stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
53
60
  function optionalRole(value, label) {
@@ -63,7 +70,7 @@ function optionalRole(value, label) {
63
70
  for (const key of ["model", "description"])
64
71
  if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string")
65
72
  throw new Error(`${label}.${key} is invalid`);
66
- if (value.thinking !== undefined && value.thinking !== null && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking))
73
+ if (value.thinking !== undefined && value.thinking !== null && !isThinking(value.thinking))
67
74
  throw new Error(`${label}.thinking is invalid`);
68
75
  if (value.tools !== undefined && value.tools !== null)
69
76
  stringList(value.tools, `${label}.tools`);
@@ -75,22 +82,20 @@ function optionalRole(value, label) {
75
82
  resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
76
83
  }
77
84
  function validateScheduledOptions(value, label) { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd)
78
- throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking))
85
+ throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
79
86
  throw new Error(`${label}.thinking is invalid`); optionalRole(value.role, `${label}.role`); if (value.schema !== undefined)
80
87
  validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined)
81
88
  nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null)
82
89
  positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions)))
83
90
  throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) {
84
- if (!object(value.agentIdentity) || !Array.isArray(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string")
91
+ if (!object(value.agentIdentity) || !isList(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string")
85
92
  throw new Error(`${label}.agentIdentity is invalid`);
86
93
  positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`);
87
94
  optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`);
88
95
  optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`);
89
96
  } }
90
- function validateSessionReference(value, label) {
91
- if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator)))
92
- throw new Error(`${label} is invalid`);
93
- }
97
+ function validateSessionReference(value, label) { if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator)))
98
+ throw new Error(`${label} is invalid`); }
94
99
  function validateAgentSetup(value, label) {
95
100
  if (!object(value))
96
101
  throw new Error(`${label} is invalid`);
@@ -102,7 +107,7 @@ function validateAgentSetup(value, label) {
102
107
  resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
103
108
  }
104
109
  function validateAgent(value, label) {
105
- if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || typeof value.state !== "string" || !AGENT_STATES.has(value.state))
110
+ if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || !isAgentState(value.state))
106
111
  throw new Error(`${label} is invalid`);
107
112
  optionalString(value.systemPrompt, `${label}.systemPrompt`);
108
113
  optionalString(value.prompt, `${label}.prompt`);
@@ -118,7 +123,7 @@ function validateAgent(value, label) {
118
123
  stringList(value.tools, `${label}.tools`);
119
124
  nonNegativeInteger(value.attempts, `${label}.attempts`);
120
125
  if (value.attemptDetails !== undefined) {
121
- if (!Array.isArray(value.attemptDetails))
126
+ if (!isList(value.attemptDetails))
122
127
  throw new Error(`${label}.attemptDetails is invalid`);
123
128
  for (const [index, attempt] of value.attemptDetails.entries()) {
124
129
  const at = `${label}.attemptDetails[${String(index)}]`;
@@ -126,8 +131,9 @@ function validateAgent(value, label) {
126
131
  throw new Error(`${at} is invalid`);
127
132
  validateAgentSetup(attempt.setup, `${at}.setup`);
128
133
  if (attempt.session !== undefined) {
129
- validateSessionReference(attempt.session, `${at}.session`);
130
- if (attempt.session.transport !== attempt.transport)
134
+ const session = attempt.session;
135
+ validateSessionReference(session, `${at}.session`);
136
+ if (session.transport !== attempt.transport)
131
137
  throw new Error(`${at}.session transport does not match attempt transport`);
132
138
  }
133
139
  accounting(attempt.accounting, `${at}.accounting`);
@@ -138,14 +144,14 @@ function validateAgent(value, label) {
138
144
  if (value.accounting !== undefined)
139
145
  accounting(value.accounting, `${label}.accounting`);
140
146
  if (value.toolCalls !== undefined) {
141
- if (!Array.isArray(value.toolCalls))
147
+ if (!isList(value.toolCalls))
142
148
  throw new Error(`${label}.toolCalls is invalid`);
143
149
  for (const [index, call] of value.toolCalls.entries())
144
- if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !["running", "completed", "failed"].includes(call.state))
150
+ if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !isToolCallState(call.state))
145
151
  throw new Error(`${label}.toolCalls[${String(index)}] is invalid`);
146
152
  }
147
153
  if (value.activity !== undefined) {
148
- if (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind) || typeof value.activity.text !== "string")
154
+ if (!object(value.activity) || !isActivityKind(value.activity.kind) || typeof value.activity.text !== "string")
149
155
  throw new Error(`${label}.activity is invalid`);
150
156
  }
151
157
  if (value.lastEventAt !== undefined)
@@ -155,20 +161,18 @@ function validateUsage(value, label) { if (!object(value))
155
161
  throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"])
156
162
  finiteNumber(value[key], `${label}.${key}`); }
157
163
  function validateBudgetEvents(value) { if (value === undefined)
158
- return; if (!Array.isArray(value))
164
+ return; if (!isList(value))
159
165
  throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) {
160
166
  const label = `budgetEvents[${String(index)}]`;
161
- if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !Array.isArray(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined)
167
+ if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !isList(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined)
162
168
  throw new Error(`${label} is invalid`);
163
169
  validateUsage(event.usage, `${label}.usage`);
164
170
  validateBudget(event.limits);
165
171
  } }
166
- function validateRunRecord(run) {
167
- const value = run;
168
- if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions"))
172
+ function validateRunRecord(value) {
173
+ if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !isRunState(value.state) || !isList(value.agents) || !isList(value.agentSessions) || Object.hasOwn(value, "nativeSessions"))
169
174
  throw new Error("Persisted run state is invalid");
170
- const agents = value.agents;
171
- agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
175
+ const agents = value.agents.map((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); return agent; });
172
176
  const agentIds = new Set();
173
177
  for (const agent of agents) {
174
178
  const id = agent.id;
@@ -178,36 +182,36 @@ function validateRunRecord(run) {
178
182
  }
179
183
  for (const agent of agents) {
180
184
  const parentId = agent.parentId;
181
- if (parentId !== undefined && (typeof parentId !== "string" || !agentIds.has(parentId)))
185
+ if (parentId !== undefined && !agentIds.has(parentId))
182
186
  throw new Error("Persisted agent has a missing parent");
183
187
  const seen = new Set();
184
- let parent = typeof parentId === "string" ? parentId : undefined;
188
+ let parent = parentId;
185
189
  while (parent) {
186
190
  if (seen.has(parent))
187
191
  throw new Error("Persisted agent parent cycle");
188
192
  seen.add(parent);
189
193
  const parentAgent = agents.find((candidate) => candidate.id === parent);
190
- parent = typeof parentAgent?.parentId === "string" ? parentAgent.parentId : undefined;
194
+ parent = parentAgent?.parentId;
191
195
  }
192
196
  }
193
- const sessions = value.agentSessions;
194
- for (const [index, session] of sessions.entries())
197
+ for (const [index, session] of value.agentSessions.entries())
195
198
  validateSessionReference(session, `agentSessions[${String(index)}]`);
196
199
  optionalString(value.parentRunId, "Persisted parent run");
197
200
  optionalString(value.failedAt, "Persisted failed path");
198
- if (value.retry !== undefined) {
199
- if (!object(value.retry) || typeof value.retry.sourceRunId !== "string" || !value.retry.sourceRunId || typeof value.retry.lineageRootRunId !== "string" || !value.retry.lineageRootRunId)
201
+ const retry = value.retry;
202
+ if (retry !== undefined) {
203
+ if (!object(retry) || typeof retry.sourceRunId !== "string" || !retry.sourceRunId || typeof retry.lineageRootRunId !== "string" || !retry.lineageRootRunId)
200
204
  throw new Error("Persisted retry provenance is invalid");
201
- const sourceRunId = value.retry.sourceRunId;
202
- stringList(value.retry.completedPaths, "Persisted retry completed paths");
203
- stringList(value.retry.incompletePaths, "Persisted retry incomplete paths");
204
- stringList(value.retry.namedWorktrees, "Persisted retry named worktrees");
205
+ const sourceRunId = retry.sourceRunId;
206
+ stringList(retry.completedPaths, "Persisted retry completed paths");
207
+ stringList(retry.incompletePaths, "Persisted retry incomplete paths");
208
+ stringList(retry.namedWorktrees, "Persisted retry named worktrees");
205
209
  if (value.parentRunId !== sourceRunId)
206
210
  throw new Error("Persisted retry parent does not match its source");
207
211
  }
208
212
  optionalString(value.phase, "Persisted phase");
209
213
  if (value.phaseHistory !== undefined) {
210
- if (!Array.isArray(value.phaseHistory))
214
+ if (!isList(value.phaseHistory))
211
215
  throw new Error("Persisted phase history is invalid");
212
216
  for (const phase of value.phaseHistory) {
213
217
  if (!object(phase) || typeof phase.phase !== "string" || !phase.phase)
@@ -215,6 +219,18 @@ function validateRunRecord(run) {
215
219
  nonNegativeInteger(phase.afterAgent, "Persisted phase history afterAgent");
216
220
  }
217
221
  }
222
+ if (value.phaseHistoryIndex !== undefined)
223
+ nonNegativeInteger(value.phaseHistoryIndex, "Persisted phase history index");
224
+ if (value.activeShellsByPhase !== undefined) {
225
+ if (!isList(value.activeShellsByPhase))
226
+ throw new Error("Persisted phase shell activity is invalid");
227
+ for (const activity of value.activeShellsByPhase) {
228
+ if (!object(activity) || !Number.isSafeInteger(activity.phaseIndex) || Number(activity.phaseIndex) < -1)
229
+ throw new Error("Persisted phase shell activity is invalid");
230
+ positiveInteger(activity.active, "Persisted phase shell activity count");
231
+ finiteNumber(activity.startedAt, "Persisted phase shell activity start");
232
+ }
233
+ }
218
234
  if (value.error !== undefined && (!object(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string"))
219
235
  throw new Error("Persisted run error is invalid");
220
236
  validateBudget(value.budget);
@@ -224,7 +240,7 @@ function validateRunRecord(run) {
224
240
  validateUsage(value.usage, "Persisted usage");
225
241
  validateBudgetEvents(value.budgetEvents);
226
242
  if (value.events !== undefined) {
227
- if (!Array.isArray(value.events))
243
+ if (!isList(value.events))
228
244
  throw new Error("Persisted run events are invalid");
229
245
  for (const event of value.events)
230
246
  if (!object(event) || typeof event.type !== "string" || typeof event.message !== "string")
@@ -232,7 +248,7 @@ function validateRunRecord(run) {
232
248
  }
233
249
  }
234
250
  function validateSnapshot(snapshot) {
235
- if (!object(snapshot) || typeof snapshot.script !== "string" || !snapshot.script || !jsonValue(snapshot.args) || !object(snapshot.metadata) || typeof snapshot.metadata.name !== "string" || !snapshot.metadata.name || (snapshot.metadata.description !== undefined && typeof snapshot.metadata.description !== "string") || !object(snapshot.settings) || !Number.isSafeInteger(snapshot.settings.concurrency) || Number(snapshot.settings.concurrency) < 1 || Number(snapshot.settings.concurrency) > 16 || !Array.isArray(snapshot.models) || snapshot.models.some((modelName) => typeof modelName !== "string") || !Array.isArray(snapshot.tools) || snapshot.tools.some((tool) => typeof tool !== "string") || !Array.isArray(snapshot.agentTypes) || snapshot.agentTypes.some((agentType) => typeof agentType !== "string") || !Array.isArray(snapshot.schemas))
251
+ if (!object(snapshot) || typeof snapshot.script !== "string" || !snapshot.script || !jsonValue(snapshot.args) || !object(snapshot.metadata) || typeof snapshot.metadata.name !== "string" || !snapshot.metadata.name || (snapshot.metadata.description !== undefined && typeof snapshot.metadata.description !== "string") || !object(snapshot.settings) || !Number.isSafeInteger(snapshot.settings.concurrency) || Number(snapshot.settings.concurrency) < 1 || Number(snapshot.settings.concurrency) > 16 || snapshot.settings.backgroundWidget !== undefined && typeof snapshot.settings.backgroundWidget !== "boolean" || !isList(snapshot.models) || snapshot.models.some((modelName) => typeof modelName !== "string") || !isList(snapshot.tools) || snapshot.tools.some((tool) => typeof tool !== "string") || !isList(snapshot.agentTypes) || snapshot.agentTypes.some((agentType) => typeof agentType !== "string") || !isList(snapshot.schemas))
236
252
  throw new Error("Persisted launch snapshot is invalid");
237
253
  if (snapshot.identityVersion !== undefined)
238
254
  positiveInteger(snapshot.identityVersion, "Persisted snapshot identity version");
@@ -278,7 +294,7 @@ async function validateSystemPrompts(store) {
278
294
  await store.systemPrompts();
279
295
  return;
280
296
  }
281
- if (!object(value) || value.version !== 1 || !Array.isArray(value.entries))
297
+ if (!object(value) || value.version !== 1 || !isList(value.entries))
282
298
  throw new Error("Persisted system prompts are invalid");
283
299
  for (const [index, entry] of value.entries.entries()) {
284
300
  const label = `system-prompts.entries[${String(index)}]`;
@@ -300,6 +316,13 @@ async function validateRunDirectory(store) {
300
316
  throw new Error(`Run artifact is not a regular file: ${join(store.directory, entry.name)}`);
301
317
  }
302
318
  }
319
+ function validateOwnershipRecord(value, label, ownershipIds) {
320
+ if (!object(value) || typeof value.id !== "string" || !value.id || ownershipIds.has(value.id) || typeof value.label !== "string" || !value.label || !isSchedulerState(value.state))
321
+ throw new Error(`${label} is invalid`);
322
+ optionalString(value.parentId, `${label}.parentId`);
323
+ optionalString(value.prompt, `${label}.prompt`);
324
+ validateScheduledOptions(value.options, `${label}.options`);
325
+ }
303
326
  async function validateRunArtifacts(store, workflowScript, state) {
304
327
  await validateRunDirectory(store);
305
328
  for (const name of REQUIRED_RUN_FILES)
@@ -307,7 +330,7 @@ async function validateRunArtifacts(store, workflowScript, state) {
307
330
  if (await readFile(join(store.directory, "workflow.js"), "utf8") !== workflowScript)
308
331
  throw new Error("Persisted workflow source does not match its launch snapshot");
309
332
  await validateSystemPrompts(store);
310
- const result = await jsonFile(join(store.directory, "result.json")).catch((error) => { if (error.code === "ENOENT")
333
+ const result = await jsonFile(join(store.directory, "result.json")).catch((error) => { if (isNodeError(error, "ENOENT"))
311
334
  return undefined; throw error; });
312
335
  if (result === undefined && state === "completed")
313
336
  throw new Error("Completed run result is missing");
@@ -315,21 +338,17 @@ async function validateRunArtifacts(store, workflowScript, state) {
315
338
  throw new Error("Persisted workflow result is invalid");
316
339
  validateJournal(await jsonFile(join(store.directory, "journal.json")));
317
340
  const rawOwnership = await jsonFile(join(store.directory, "ownership.json"));
318
- if (!Array.isArray(rawOwnership))
341
+ if (!isList(rawOwnership))
319
342
  throw new Error("Persisted ownership records are invalid");
320
- const ownership = rawOwnership;
321
343
  const ownershipIds = new Set();
322
- for (const [index, record] of ownership.entries()) {
344
+ const ownership = rawOwnership.map((record, index) => {
323
345
  const label = `ownership[${String(index)}]`;
324
- if (!object(record) || typeof record.id !== "string" || !record.id || ownershipIds.has(record.id) || typeof record.label !== "string" || !record.label || typeof record.state !== "string" || !SCHEDULER_STATES.has(record.state))
325
- throw new Error(`${label} is invalid`);
346
+ validateOwnershipRecord(record, label, ownershipIds);
326
347
  ownershipIds.add(record.id);
327
- optionalString(record.parentId, `${label}.parentId`);
328
- optionalString(record.prompt, `${label}.prompt`);
329
- validateScheduledOptions(record.options, `${label}.options`);
330
- }
348
+ return record;
349
+ });
331
350
  for (const record of ownership)
332
- if (object(record) && record.parentId !== undefined && (typeof record.parentId !== "string" || !ownershipIds.has(record.parentId)))
351
+ if (record.parentId !== undefined && !ownershipIds.has(record.parentId))
333
352
  throw new Error("Persisted ownership parent is missing");
334
353
  await store.validateDeletionWorktrees();
335
354
  const borrowed = await store.borrowedWorktrees();
@@ -467,7 +486,7 @@ function planSession(scan, cutoffMs) {
467
486
  }
468
487
  return { candidates: scan.runs.filter(({ runId }) => oldTerminal.has(runId) && !protectedRuns.has(runId)), skipped };
469
488
  }
470
- function deletionOrder(scan, candidates) {
489
+ function deletionOrder(_scan, candidates) {
471
490
  const candidateIds = new Set(candidates.map(({ runId }) => runId));
472
491
  const remaining = new Set(candidateIds);
473
492
  const ordered = [];
@@ -487,7 +506,7 @@ async function storedSessionIds(cwd, home) {
487
506
  entries = await sessionEntries(path);
488
507
  }
489
508
  catch (error) {
490
- if (error.code === "ENOENT")
509
+ if (isNodeError(error, "ENOENT"))
491
510
  return [];
492
511
  throw error;
493
512
  }
@@ -1,8 +1,8 @@
1
- import { readFileSync, readdirSync, realpathSync } from "node:fs";
1
+ import { readFileSync, readdirSync, realpathSync, statSync } 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, createLocalPiSession, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
5
+ import { DEFAULT_SETTINGS, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
6
6
  import { loadingRegistry } from "pi-extensible-workflows";
7
7
  import { disabledResources, unmatchedResourcePatterns } from "pi-extensible-workflows";
8
8
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
@@ -20,16 +20,23 @@ function isDynamicModelAlias(value, aliases) {
20
20
  const name = match?.[1];
21
21
  return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
22
22
  }
23
+ function isCredential(value) {
24
+ if (!isObject(value))
25
+ return false;
26
+ if (value.type === "api_key")
27
+ return (value.key === undefined || typeof value.key === "string") && (value.env === undefined || isObject(value.env) && Object.values(value.env).every((entry) => typeof entry === "string"));
28
+ return value.type === "oauth" && typeof value.refresh === "string" && typeof value.access === "string" && typeof value.expires === "number";
29
+ }
23
30
  async function readCredentials(agentDir) {
24
31
  const credentials = new InMemoryCredentialStore();
25
32
  try {
26
33
  const parsed = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"));
27
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
34
+ if (!isObject(parsed))
28
35
  throw new Error("Pi auth.json must be an object");
29
- await Promise.all(Object.entries(parsed).map(([provider, credential]) => credentials.modify(provider, async () => credential)));
36
+ await Promise.all(Object.entries(parsed).flatMap(([provider, credential]) => isCredential(credential) ? [credentials.modify(provider, async () => credential)] : []));
30
37
  }
31
38
  catch (error) {
32
- if (error.code !== "ENOENT")
39
+ if (!isNodeError(error, "ENOENT"))
33
40
  throw error;
34
41
  }
35
42
  return credentials;
@@ -40,11 +47,11 @@ function savedTrust(cwd, agentDir) {
40
47
  parsed = JSON.parse(readFileSync(join(agentDir, "trust.json"), "utf8"));
41
48
  }
42
49
  catch (error) {
43
- if (error.code === "ENOENT")
50
+ if (isNodeError(error, "ENOENT"))
44
51
  return undefined;
45
52
  throw error;
46
53
  }
47
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
54
+ if (!isObject(parsed))
48
55
  throw new Error("Pi trust.json must be an object");
49
56
  let current = canonical(cwd);
50
57
  while (current !== dirname(current)) {
@@ -106,12 +113,28 @@ async function discoverPi(cwd, agentDir) {
106
113
  process.env.PI_OFFLINE = previousOffline;
107
114
  }
108
115
  }
116
+ function isRoleFile(dir, entry) {
117
+ if (extname(entry.name) !== ".md")
118
+ return false;
119
+ if (entry.isFile())
120
+ return true;
121
+ if (!entry.isSymbolicLink())
122
+ return false;
123
+ try {
124
+ return statSync(join(dir, entry.name)).isFile();
125
+ }
126
+ catch (error) {
127
+ if (isNodeError(error, "ENOENT"))
128
+ return false;
129
+ throw error;
130
+ }
131
+ }
109
132
  function roleFiles(dir) {
110
133
  try {
111
- return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && extname(entry.name) === ".md").map((entry) => join(dir, entry.name)).sort();
134
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => isRoleFile(dir, entry)).map((entry) => join(dir, entry.name)).sort();
112
135
  }
113
136
  catch (error) {
114
- if (error.code === "ENOENT")
137
+ if (isNodeError(error, "ENOENT"))
115
138
  return [];
116
139
  throw error;
117
140
  }
@@ -128,7 +151,7 @@ function scanExtensionRoleFiles(registrations) {
128
151
  for (const registration of registrations) {
129
152
  try {
130
153
  const entries = readdirSync(registration.path, { withFileTypes: true });
131
- const roleFiles = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md");
154
+ const roleFiles = entries.filter((entry) => isRoleFile(registration.path, entry));
132
155
  if (!roleFiles.length)
133
156
  empty.push(registration);
134
157
  for (const entry of roleFiles)
@@ -161,7 +184,8 @@ function validateModel(value, known, available, source, diagnostics, aliases, dy
161
184
  diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
162
185
  }
163
186
  catch (error) {
164
- diagnostics.push(diagnostic("error", "MODEL_INVALID", error.message, source, error.message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
187
+ const message = errorText(error);
188
+ diagnostics.push(diagnostic("error", "MODEL_INVALID", message, source, message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
165
189
  }
166
190
  }
167
191
  function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
@@ -244,7 +268,7 @@ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath,
244
268
  return undefined;
245
269
  }
246
270
  finally {
247
- session.dispose();
271
+ await session.dispose();
248
272
  }
249
273
  }
250
274
  function resourcePolicySource(settingsSource) { return settingsSource; }
@@ -258,7 +282,7 @@ export async function doctor(options = {}) {
258
282
  settings = loadSettings(settingsPath);
259
283
  }
260
284
  catch (error) {
261
- diagnostics.push(diagnostic("error", "SETTINGS_INVALID", error.message, settingsPath, "Fix or remove the invalid workflow settings file."));
285
+ diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), settingsPath, "Fix or remove the invalid workflow settings file."));
262
286
  }
263
287
  let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
264
288
  const projectSettingsPath = workflowProjectSettingsPath(cwd);
@@ -267,7 +291,7 @@ export async function doctor(options = {}) {
267
291
  pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
268
292
  }
269
293
  catch (error) {
270
- 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."));
294
+ diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${errorText(error)}`, undefined, "Open and trust the project in Pi, fix extension errors, then rerun doctor."));
271
295
  pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
272
296
  }
273
297
  if (options.activeTools)
@@ -311,7 +335,7 @@ export async function doctor(options = {}) {
311
335
  const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
312
336
  const modelAliases = [
313
337
  ...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
314
- ...registeredModelAliases.map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription })),
338
+ ...registeredModelAliases.map(({ name, version, headline }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline })),
315
339
  ].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
316
340
  const roles = [];
317
341
  const definitions = new Map();
@@ -14,27 +14,39 @@ function text(content) {
14
14
  const parts = content;
15
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
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
+ }
17
33
  function transcriptPartLines(part) {
18
- if (typeof part !== "object" || part === null || !("type" in part))
34
+ if (!isTranscriptPart(part))
19
35
  return [];
20
- const value = part;
21
- if (value.type === "text" && typeof value.text === "string")
22
- return value.text.split("\n");
23
- if (value.type === "thinking" && typeof value.thinking === "string")
24
- return ["Thinking:", ...value.thinking.split("\n")];
25
- if (value.type === "toolCall" && typeof value.name === "string")
26
- return [`Tool call: ${value.name}`, JSON.stringify(value.arguments, null, 2)];
27
- if (value.type === "image")
28
- return ["[image]"];
29
- return [];
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]"];
30
43
  }
31
44
  function transcriptMessageLines(message) {
32
- if (typeof message !== "object" || message === null)
45
+ if (!isTranscriptMessage(message))
33
46
  return ["(invalid message)"];
34
- const value = message;
35
- const role = typeof value.role === "string" ? value.role : "message";
36
- const label = role === "toolResult" && typeof value.toolName === "string" ? `${role}: ${value.toolName}` : role === "custom" && typeof value.customType === "string" ? `${role}: ${value.customType}` : role;
37
- const content = Array.isArray(value.content) ? value.content.flatMap(transcriptPartLines) : typeof value.content === "string" ? value.content.split("\n") : [];
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") : [];
38
50
  return [`[${label}]`, ...(content.length ? content : ["(empty)"])];
39
51
  }
40
52
  export function transcriptLines(entries) {
@@ -58,8 +70,11 @@ function mergedModels(groups) {
58
70
  return [...totals].map(([model, cost]) => ({ model, cost })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
59
71
  }
60
72
  const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
73
+ function isThinkingLevel(value) {
74
+ return typeof value === "string" && thinkingLevels.some((level) => level === value);
75
+ }
61
76
  function parsedThinking(value) {
62
- return typeof value === "string" && thinkingLevels.includes(value) ? value : undefined;
77
+ return isThinkingLevel(value) ? value : undefined;
63
78
  }
64
79
  function modelName(provider, model) {
65
80
  return typeof provider === "string" && provider && typeof model === "string" && model ? `${provider}/${model}` : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.1.1",
3
+ "version": "5.3.0",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  "peerDependencies": {
30
30
  "@earendil-works/pi-ai": "*",
31
31
  "@earendil-works/pi-coding-agent": "*",
32
+ "@earendil-works/pi-tui": "*",
32
33
  "typebox": "*"
33
34
  },
34
35
  "devDependencies": {
@@ -45,7 +46,7 @@
45
46
  },
46
47
  "license": "MIT",
47
48
  "dependencies": {
48
- "pi-extensible-workflows": "5.1.1"
49
+ "pi-extensible-workflows": "5.3.0"
49
50
  },
50
51
  "publishConfig": {
51
52
  "access": "public"