@piewf/cli 5.13.2 → 5.14.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/dist/src/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
- import { chmodSync, linkSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, linkSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, extname, join } from "node:path";
6
- import { pathToFileURL } from "node:url";
6
+ import { fileURLToPath } from "node:url";
7
7
  import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
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, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
11
+ import workflowExtension, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, sameFilesystemPath, 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
14
  import { isPersistedRun, listPersistedSessionIds, listRunIds } from "pi-extensible-workflows/persistence";
@@ -716,13 +716,8 @@ async function exportWorkflowCli(rawArgs, options) {
716
716
  writeLauncher(destination, workflowName, force);
717
717
  if (!output) {
718
718
  const binDir = join(homedir(), ".local", "bin");
719
- const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean).map((entry) => { try {
720
- return realpathSync(entry);
721
- }
722
- catch {
723
- return entry;
724
- } });
725
- if (!pathEntries.includes(binDir))
719
+ const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
720
+ if (!pathEntries.some((entry) => sameFilesystemPath(entry, binDir)))
726
721
  options.stderr(`Warning: ${binDir} is not in PATH\n`);
727
722
  }
728
723
  options.write(`Exported ${destination}\n`);
@@ -909,7 +904,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
909
904
  write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | share <run-id> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | run --script <path> [--name <workflow-name>] [--input <json>] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
910
905
  return 1;
911
906
  }
912
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
907
+ if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
913
908
  const controller = new AbortController();
914
909
  const onSignal = () => { controller.abort(); };
915
910
  process.once("SIGINT", onSignal);
@@ -1,10 +1,10 @@
1
- import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
- import { basename, dirname, extname, join, resolve } from "node:path";
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, dirname, extname, join } from "node:path";
3
3
  import { InMemoryCredentialStore, InMemoryModelsStore } 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, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
5
+ import { DEFAULT_SETTINGS, canonicalPath, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
6
6
  import { loadingRegistry } from "pi-extensible-workflows";
7
- import { selectResourcesByLayers, unmatchedResourcePatterns } from "pi-extensible-workflows";
7
+ import { selectResourcesByLayers, unmatchedResourcePatterns, mergeWorkflowExtensionSettings } from "pi-extensible-workflows";
8
8
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
9
9
  const AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE = "https://github.com/vekexasia/pi-extensible-workflows/issues/205";
10
10
  const AGENT_RESOURCE_SELECTOR_MIGRATION_MESSAGE = `\`disabledAgentResources\` is no longer supported by #205. Migrate to direct \`skills\`, \`extensions\`, and \`tools\` selectors: legacy patterns exclude resources and \`!pattern\` re-enables them, while new selectors include matches and \`!pattern\` excludes them. See ${AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE}`;
@@ -25,15 +25,6 @@ function usesLegacyRoleSelectors(path) {
25
25
  return false;
26
26
  }
27
27
  }
28
- function canonical(path) {
29
- const absolute = resolve(path);
30
- try {
31
- return realpathSync(absolute);
32
- }
33
- catch {
34
- return absolute;
35
- }
36
- }
37
28
  function isDynamicModelAlias(value, aliases) {
38
29
  const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
39
30
  const name = match?.[1];
@@ -72,7 +63,7 @@ function savedTrust(cwd, agentDir) {
72
63
  }
73
64
  if (!isObject(parsed))
74
65
  throw new Error("Pi trust.json must be an object");
75
- let current = canonical(cwd);
66
+ let current = canonicalPath(cwd);
76
67
  while (current !== dirname(current)) {
77
68
  const value = parsed[current];
78
69
  if (value === true || value === false)
@@ -249,7 +240,7 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
249
240
  return definition;
250
241
  }
251
242
  function matchResourcePolicy(policy, pi) {
252
- const extensions = [...new Set((pi.extensions ?? []).map(canonical))];
243
+ const extensions = [...new Set((pi.extensions ?? []).map(canonicalPath))];
253
244
  const skills = [...new Set(pi.skills ?? [])];
254
245
  const tools = [...new Set(pi.activeTools)];
255
246
  const layers = policy.selectorSources;
@@ -258,12 +249,12 @@ function matchResourcePolicy(policy, pi) {
258
249
  const selectedTools = selectResourcesByLayers([layers.global.tools, layers.project.tools], tools);
259
250
  return { ...policy, selectedSkills, selectedExtensions, selectedTools, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions), unmatchedTools: unmatchedResourcePatterns(policy.effective.tools ?? [], tools) };
260
251
  }
261
- async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, prompt, hooks, diagnostics) {
252
+ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, extensionSettings, prompt, hooks, diagnostics) {
262
253
  const setupDiagnostics = [];
263
254
  const signal = new AbortController().signal;
264
255
  const transport = { id: "doctor-local", createSession: async () => { throw new Error("Doctor inspection does not create transport sessions"); } };
265
256
  const run = { cwd, sessionId: "doctor", runId: "doctor", workflow: { name: "doctor" }, args: null, signal };
266
- const root = { cwd, model: { ...rootModel }, tools: new Set(activeTools), agentDefinitions: { [roleName]: definition }, agentDir, modelAliases: aliases, knownModels, availableModels, settingsPath, agentSetupHooks: hooks, agentResourcePolicy: () => structuredClone(basePolicy), runContext: run };
257
+ const root = { cwd, model: { ...rootModel }, tools: new Set(activeTools), agentDefinitions: { [roleName]: definition }, agentDir, extensionSettings, modelAliases: aliases, knownModels, availableModels, settingsPath, agentSetupHooks: hooks, agentResourcePolicy: () => structuredClone(basePolicy), runContext: run };
267
258
  const options = { label: roleName, workflowName: "doctor", role: roleName };
268
259
  let prepared;
269
260
  try {
@@ -313,13 +304,22 @@ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath,
313
304
  }
314
305
  }
315
306
  function resourcePolicySource(settingsSource) { return settingsSource; }
307
+ function validateDoctorExtensionSettings(registry, value, source, cwd, projectTrusted, settingsPath, diagnostics, role) {
308
+ try {
309
+ registry.validateExtensionSettings(value, { source, cwd, projectTrusted, settingsPath, ...(role === undefined ? {} : { role }) });
310
+ }
311
+ catch (error) {
312
+ diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), `${settingsPath}.extensionSettings`, "Fix the extension-owned settings reported in this error."));
313
+ }
314
+ }
316
315
  export async function doctor(options = {}) {
317
- const cwd = canonical(options.cwd ?? process.cwd());
318
- const agentDir = canonical(options.agentDir ?? getAgentDir());
319
- const settingsPath = canonical(options.settingsPath ?? workflowSettingsPath(agentDir));
316
+ const cwd = canonicalPath(options.cwd ?? process.cwd());
317
+ const agentDir = canonicalPath(options.agentDir ?? getAgentDir());
318
+ const settingsPath = canonicalPath(options.settingsPath ?? workflowSettingsPath(agentDir));
320
319
  const projectSettingsPath = workflowProjectSettingsPath(cwd);
321
320
  const legacyGlobalSettings = usesLegacySettings(settingsPath);
322
321
  const diagnostics = [];
322
+ const registry = options.registry ?? loadingRegistry();
323
323
  let settings = DEFAULT_SETTINGS;
324
324
  try {
325
325
  settings = loadSettings(settingsPath);
@@ -351,6 +351,11 @@ export async function doctor(options = {}) {
351
351
  const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
352
352
  settings = resolved.effective;
353
353
  settingsSources = resolved.sources;
354
+ if (resolved.global.extensionSettings !== undefined)
355
+ validateDoctorExtensionSettings(registry, resolved.global.extensionSettings, "global", cwd, pi.trust.trusted, resolved.globalSettingsPath, diagnostics);
356
+ if (pi.trust.trusted && resolved.project.extensionSettings !== undefined)
357
+ validateDoctorExtensionSettings(registry, resolved.project.extensionSettings, "project", cwd, true, resolved.projectSettingsPath, diagnostics);
358
+ validateDoctorExtensionSettings(registry, resolved.effective.extensionSettings, "effective", cwd, pi.trust.trusted, settingsSources.extensionSettings ?? settingsPath, diagnostics);
354
359
  }
355
360
  catch (error) {
356
361
  const message = errorText(error);
@@ -384,7 +389,6 @@ export async function doctor(options = {}) {
384
389
  const knownModels = new Set(pi.knownModels);
385
390
  const availableModels = new Set(pi.availableModels);
386
391
  const aliases = settings.modelAliases ?? {};
387
- const registry = options.registry ?? loadingRegistry();
388
392
  const registeredModelAliases = registry.modelAliases();
389
393
  const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
390
394
  const modelAliases = [
@@ -430,6 +434,8 @@ export async function doctor(options = {}) {
430
434
  const overriddenBy = starterOverriddenBy.get(file.path);
431
435
  roles.push({ name: file.name, path: file.path, scope: "extension", active: overriddenBy === undefined, extension: file.extension, ...(starterPath ? { overrides: starterPath } : {}), ...(overriddenBy ? { overriddenBy } : {}) });
432
436
  const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
437
+ if (definition)
438
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, file.path, diagnostics, file.name);
433
439
  if (duplicateExtensionNames.has(file.name))
434
440
  continue;
435
441
  if (extensionPaths.get(file.name) !== file.path)
@@ -452,6 +458,8 @@ export async function doctor(options = {}) {
452
458
  }
453
459
  }
454
460
  const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
461
+ if (definition)
462
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
455
463
  if (definition)
456
464
  definitions.set(name, definition);
457
465
  else
@@ -481,6 +489,8 @@ export async function doctor(options = {}) {
481
489
  }
482
490
  }
483
491
  const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
492
+ if (definition)
493
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
484
494
  if (definition)
485
495
  definitions.set(name, definition);
486
496
  else
@@ -513,7 +523,7 @@ export async function doctor(options = {}) {
513
523
  const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
514
524
  roleAliases = { ...aliases, ...dynamic };
515
525
  }
516
- roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
526
+ roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
517
527
  if (roleInspection)
518
528
  diagnostics.push(...roleInspection.setup.diagnostics);
519
529
  }
@@ -530,7 +540,7 @@ export async function doctor(options = {}) {
530
540
  const severityOrder = { error: 0, warning: 1 };
531
541
  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));
532
542
  roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
533
- return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(canonical))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
543
+ return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(canonicalPath))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
534
544
  }
535
545
  function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
536
546
  export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
@@ -608,7 +618,7 @@ export function formatDoctorReport(report) {
608
618
  `- Agent dir: \`${report.agentDir}\``,
609
619
  `- Global workflow settings: \`${report.settingsPath}\``,
610
620
  `- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
611
- `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
621
+ `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, extensionSettings=\`${report.settingsSources.extensionSettings ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
612
622
  `- Limits: concurrency=${String(report.settings.concurrency)}`,
613
623
  "",
614
624
  "## Trust/resources",
@@ -173,7 +173,7 @@ async function loadRuns(cwd, sessionId, home) {
173
173
  }
174
174
  return runs;
175
175
  }
176
- async function agentReport(agent) {
176
+ function agentReport(agent) {
177
177
  const fallbackModel = `${agent.model.provider}/${agent.model.model}`;
178
178
  const fallbackThinking = agent.model.thinking;
179
179
  const attempts = [];
@@ -181,15 +181,10 @@ async function agentReport(agent) {
181
181
  const setup = attempt.setup;
182
182
  const path = attemptTranscriptPath(attempt);
183
183
  const log = path ? readTranscript(path) : undefined;
184
- if (log) {
185
- const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
186
- const cost = log.cost;
187
- attempts.push({ attempt: attempt.attempt, prompt: log.prompt ?? "(transcript unavailable)", model, ...(log.thinking !== undefined ? { thinking: log.thinking } : {}), cost, models: log.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
188
- continue;
189
- }
190
- const model = `${setup.model.provider}/${setup.model.model}`;
191
- const cost = attempt.accounting.cost;
192
- attempts.push({ attempt: attempt.attempt, prompt: "(transcript unavailable)", model, ...(setup.model.thinking !== undefined ? { thinking: setup.model.thinking } : {}), cost, models: [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
184
+ const model = log?.model ?? `${setup.model.provider}/${setup.model.model}`;
185
+ const thinking = log ? log.thinking : setup.model.thinking;
186
+ const cost = log?.cost ?? attempt.accounting.cost;
187
+ attempts.push({ attempt: attempt.attempt, prompt: log?.prompt ?? "(transcript unavailable)", model, ...(thinking !== undefined ? { thinking } : {}), cost, models: log?.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
193
188
  }
194
189
  if (!attempts.length) {
195
190
  const cost = agent.accounting?.cost ?? 0;
@@ -223,7 +218,7 @@ export async function loadSessionReport(path, home = homedir()) {
223
218
  const runId = resultRunId(result);
224
219
  const loaded = runId ? runs.get(runId) : undefined;
225
220
  const args = call.arguments;
226
- const agents = loaded ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
221
+ const agents = loaded ? loaded.run.agents.map(agentReport) : [];
227
222
  const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
228
223
  const name = loaded?.run.workflowName ?? (typeof args.name === "string" ? args.name : "workflow");
229
224
  const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.13.2",
3
+ "version": "5.14.0",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "@earendil-works/pi-coding-agent": "0.85.0",
44
44
  "@earendil-works/pi-server": "0.85.0",
45
45
  "@earendil-works/pi-tui": "0.85.0",
46
- "pi-extensible-workflows": "5.13.2",
46
+ "pi-extensible-workflows": "5.14.0",
47
47
  "typebox": "1.3.7"
48
48
  },
49
49
  "publishConfig": {