@piewf/cli 5.17.1 → 5.17.2

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 CHANGED
@@ -26,7 +26,7 @@ Roles come from the bundled starter roles, `~/.pi/agent/pi-extensible-workflows/
26
26
  ## piewf
27
27
 
28
28
  ```sh
29
- piewf doctor [--role <role>] [--prompt <text>] [--json]
29
+ piewf doctor [role|role-file] [--role <role>] [--prompt <text>] [--json]
30
30
  piewf doctor cleanup [--older-than-days <days>] [--yes]
31
31
  piewf inspect [session-id] [--json|--summary] [--failed]
32
32
  piewf transcript <session-file>
package/dist/src/cli.js CHANGED
@@ -803,7 +803,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
803
803
  const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
804
804
  if (args[0] === "doctor" && args[1] !== "cleanup") {
805
805
  if (args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
806
- write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json]\n");
806
+ write("Usage: piewf doctor [role|role-file] [--role <role>] [--prompt <text>] [--json]\n");
807
807
  return 0;
808
808
  }
809
809
  try {
@@ -902,7 +902,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
902
902
  return 1;
903
903
  }
904
904
  }
905
- write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | share <run-id> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | run --script <path> [--name <workflow-name>] [--input <json>] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
905
+ write("Usage: piewf doctor [role|role-file] [--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");
906
906
  return 1;
907
907
  }
908
908
  if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
@@ -1,5 +1,5 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { basename, dirname, extname, join } from "node:path";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, dirname, extname, join, resolve } 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
5
  import { DEFAULT_SETTINGS, canonicalPath, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
@@ -207,10 +207,9 @@ function validateModel(value, known, available, source, diagnostics, aliases, dy
207
207
  diagnostics.push(diagnostic("error", "MODEL_INVALID", message, source, message.includes("thinking") ? THINKING_HINT : "Use provider/model:thinking."));
208
208
  }
209
209
  }
210
- function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
211
- let definition;
210
+ function parseRole(path, diagnostics, source) {
212
211
  try {
213
- definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
212
+ return parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
214
213
  }
215
214
  catch (error) {
216
215
  if (usesLegacyRoleSelectors(path)) {
@@ -221,6 +220,8 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
221
220
  diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
222
221
  return undefined;
223
222
  }
223
+ }
224
+ function inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath) {
224
225
  const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(path, definition.tools);
225
226
  if (toolSelectorDiagnostic)
226
227
  diagnostics.push(toolSelectorDiagnostic);
@@ -236,8 +237,13 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
236
237
  for (const selector of definition.tools ?? []) {
237
238
  const tool = selector.startsWith("!") ? selector.slice(1) : selector;
238
239
  if (!selector.startsWith("!") && !resourcePatternHasMagic(selector) && !activeTools.has(tool))
239
- diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under Pi active tools or enable its Pi extension."));
240
+ diagnostics.push(diagnostic("warning", "ROLE_TOOL_INACTIVE", `Tool is not in Pi's headless active tool list: ${tool}`, path, "Doctor cannot see tools that extensions add when a session starts; otherwise use a tool listed under Pi active tools or enable its Pi extension."));
240
241
  }
242
+ }
243
+ function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
244
+ const definition = parseRole(path, diagnostics, source);
245
+ if (definition)
246
+ inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
241
247
  return definition;
242
248
  }
243
249
  function matchResourcePolicy(policy, pi) {
@@ -435,15 +441,13 @@ export async function doctor(options = {}) {
435
441
  const starterPath = starterOverrides.get(file.path);
436
442
  const overriddenBy = starterOverriddenBy.get(file.path);
437
443
  roles.push({ name: file.name, path: file.path, scope: "extension", active: overriddenBy === undefined, extension: file.extension, ...(starterPath ? { overrides: starterPath } : {}), ...(overriddenBy ? { overriddenBy } : {}) });
438
- const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
439
- if (definition)
440
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, file.path, diagnostics, file.name);
444
+ const definition = parseRole(file.path, diagnostics, { directory: file.directory, extension: file.extension });
441
445
  if (duplicateExtensionNames.has(file.name))
442
446
  continue;
443
447
  if (extensionPaths.get(file.name) !== file.path)
444
448
  continue;
445
449
  if (definition)
446
- definitions.set(file.name, definition);
450
+ definitions.set(file.name, { path: file.path, definition });
447
451
  }
448
452
  const globalPaths = new Map();
449
453
  const globalRoleDirs = workflowRoleDirectories(agentDir);
@@ -459,11 +463,9 @@ export async function doctor(options = {}) {
459
463
  extension.overriddenBy = path;
460
464
  }
461
465
  }
462
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
463
- if (definition)
464
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
466
+ const definition = parseRole(path, diagnostics);
465
467
  if (definition)
466
- definitions.set(name, definition);
468
+ definitions.set(name, { path, definition });
467
469
  else
468
470
  definitions.delete(name);
469
471
  }
@@ -490,24 +492,49 @@ export async function doctor(options = {}) {
490
492
  extension.overriddenBy = path;
491
493
  }
492
494
  }
493
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
495
+ const definition = parseRole(path, diagnostics);
494
496
  if (definition)
495
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
496
- if (definition)
497
- definitions.set(name, definition);
497
+ definitions.set(name, { path, definition });
498
498
  else
499
499
  definitions.delete(name);
500
500
  }
501
+ for (const [name, { path, definition }] of definitions) {
502
+ inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
503
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
504
+ }
501
505
  const rolePaths = new Set(roles.map(({ path }) => path));
502
506
  if (diagnostics.some(({ code, source }) => source !== undefined && rolePaths.has(source) && (code === "ROLE_FRONTMATTER" || code === "AGENT_RESOURCE_SELECTOR_MIGRATION")))
503
507
  diagnostics.push(diagnostic("error", "ROLE_LOAD_BLOCKED", "Workflow role loading is blocked because the runtime rejects the complete role set when any active role file is invalid.", undefined, "Fix the reported role file before launching workflows."));
504
508
  let roleInspection;
505
509
  if (options.role !== undefined) {
506
- const activeRole = roles.find(({ name, active }) => name === options.role && active);
507
- const definition = activeRole ? definitions.get(options.role) : undefined;
508
- if (!activeRole || !definition)
509
- diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
510
+ let target;
511
+ //NOTE: installed role names drop ".md", so a target ending in ".md" is a role file path.
512
+ if (options.role.endsWith(".md")) {
513
+ const path = resolve(cwd, options.role);
514
+ if (!existsSync(path) || !statSync(path).isFile())
515
+ diagnostics.push(diagnostic("error", "ROLE_FILE_NOT_FOUND", `Role file not found: ${path}`, path));
516
+ else {
517
+ const name = basename(path, ".md");
518
+ const found = [];
519
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, found, aliases, dynamicAliases, settingsPath);
520
+ if (definition)
521
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, found, name);
522
+ // Discovery already reported an installed file; add only what is new.
523
+ const known = new Set(diagnostics.map((item) => JSON.stringify(item)));
524
+ diagnostics.push(...found.filter((item) => !known.has(JSON.stringify(item))));
525
+ if (definition)
526
+ target = { name, path, definition };
527
+ }
528
+ }
510
529
  else {
530
+ const effective = definitions.get(options.role);
531
+ if (!effective)
532
+ diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
533
+ else
534
+ target = { name: options.role, ...effective };
535
+ }
536
+ if (target) {
537
+ const { name, path, definition } = target;
511
538
  const rootReference = pi.model ? `${pi.model.provider}/${pi.model.model}` : pi.availableModels[0] ?? pi.knownModels[0];
512
539
  if (!rootReference)
513
540
  diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", "Cannot inspect a role because Pi has no registered model"));
@@ -525,12 +552,12 @@ export async function doctor(options = {}) {
525
552
  const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
526
553
  roleAliases = { ...aliases, ...dynamic };
527
554
  }
528
- roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
555
+ roleInspection = await inspectRoleSession(cwd, agentDir, name, definition, path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
529
556
  if (roleInspection)
530
557
  diagnostics.push(...roleInspection.setup.diagnostics);
531
558
  }
532
559
  catch (error) {
533
- diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", errorText(error), activeRole.path));
560
+ diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", errorText(error), path));
534
561
  }
535
562
  }
536
563
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.17.1",
3
+ "version": "5.17.2",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -45,7 +45,7 @@
45
45
  "@earendil-works/pi-coding-agent": "0.85.0",
46
46
  "@earendil-works/pi-server": "0.85.0",
47
47
  "@earendil-works/pi-tui": "0.85.0",
48
- "pi-extensible-workflows": "5.17.1",
48
+ "pi-extensible-workflows": "5.17.2",
49
49
  "typebox": "1.3.7"
50
50
  },
51
51
  "publishConfig": {