@piewf/cli 5.14.1 → 5.15.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
@@ -8,7 +8,8 @@ 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, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, sameFilesystemPath, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
11
+ import { loadAgentDefinitions } from "pi-extensible-workflows/roles";
12
+ import workflowExtension, { errorText, formatWorkflowProgress, isNodeError, jsonValue, object, registeredWorkflowFunctionSources, sameFilesystemPath, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
12
13
  import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
13
14
  import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
14
15
  import { isPersistedRun, listPersistedSessionIds, listRunIds } from "pi-extensible-workflows/persistence";
@@ -2,7 +2,8 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
2
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, canonicalPath, 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, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
6
+ import { parseRoleMarkdown, workflowRoleDirectories } from "pi-extensible-workflows/roles";
6
7
  import { loadingRegistry } from "pi-extensible-workflows";
7
8
  import { selectResourcesByLayers, unmatchedResourcePatterns, mergeWorkflowExtensionSettings } from "pi-extensible-workflows";
8
9
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
@@ -397,6 +398,7 @@ export async function doctor(options = {}) {
397
398
  ].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
398
399
  const roles = [];
399
400
  const definitions = new Map();
401
+ // Keep this scan local because doctor reports every invalid and duplicate file; discoverRoles intentionally fails closed on the complete set.
400
402
  const extensionScan = scanExtensionRoleFiles(registeredWorkflowRoleDirectoryRegistrations());
401
403
  for (const { registration, error } of extensionScan.errors) {
402
404
  const message = errorText(error);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { type ResolvedRole } from "pi-extensible-workflows/roles";
3
+ export declare function piArguments(role: ResolvedRole, skillPaths: ReadonlyMap<string, string>, rest: readonly string[]): string[];
4
+ export declare function resolvePiArguments(name: string, rest: readonly string[], cwd?: string, agentDir?: string): Promise<string[]>;
5
+ export declare function runPiRole(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { constants } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { DefaultPackageManager, DefaultResourceLoader, ProjectTrustStore, SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent";
7
+ import { discoverRoles, loadRole, resolveRole } from "pi-extensible-workflows/roles";
8
+ import { CONTEXT_FILE_SCOPES, WorkflowError, errorText, resourcePatternHasMagic, sameFilesystemPath } from "pi-extensible-workflows";
9
+ // Pi builtin tools stand in for the workflow session boundary; extension tool names selected by the role pass through.
10
+ const PI_BUILTIN_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
11
+ function starterRoleDirectories() {
12
+ const core = dirname(fileURLToPath(import.meta.resolve("pi-extensible-workflows")));
13
+ return [{ path: resolve(core, "../starter/roles"), extension: { version: "0.0.0", headline: "Starter roles" }, builtin: true }];
14
+ }
15
+ function projectTrust(cwd, agentDir, args) {
16
+ const end = args.indexOf("--");
17
+ const flag = args.slice(0, end < 0 ? args.length : end).filter((arg) => ["--approve", "-a", "--no-approve", "-na"].includes(arg)).at(-1); // last wins, as in pi
18
+ if (flag)
19
+ return flag === "--approve" || flag === "-a";
20
+ return new ProjectTrustStore(agentDir).get(cwd) ?? false; // no saved decision: project roles, settings, and resources stay out
21
+ }
22
+ export function piArguments(role, skillPaths, rest) {
23
+ const argv = [];
24
+ if (role.model)
25
+ argv.push("--model", `${role.model.provider}/${role.model.model}${role.model.thinking ? `:${role.model.thinking}` : ""}`);
26
+ if (role.selectorLayers.tools.some((layer) => layer !== undefined))
27
+ argv.push("--tools", (role.tools ?? []).join(","));
28
+ argv.push("--no-skills");
29
+ for (const name of role.selectedSkills ?? [])
30
+ argv.push("--skill", skillPaths.get(name) ?? name);
31
+ argv.push("--no-extensions");
32
+ for (const path of role.selectedExtensions ?? [])
33
+ argv.push("--extension", path);
34
+ if (role.systemPrompt.text)
35
+ argv.push(role.systemPrompt.mode === "override" ? "--system-prompt" : "--append-system-prompt", role.systemPrompt.text);
36
+ if (role.contextFiles !== undefined && new Set(role.contextFiles).size < CONTEXT_FILE_SCOPES.length) {
37
+ if (role.contextFiles.length > 0)
38
+ throw new Error(`Role ${role.name ?? ""} selects a subset of context file scopes (${role.contextFiles.join(", ")}); pi-role only supports all scopes or none`);
39
+ argv.push("--no-context-files");
40
+ }
41
+ return [...argv, ...rest];
42
+ }
43
+ export async function resolvePiArguments(name, rest, cwd = process.cwd(), agentDir = getAgentDir()) {
44
+ const projectTrusted = projectTrust(cwd, agentDir, rest);
45
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
46
+ settingsManager.setProjectTrusted(projectTrusted);
47
+ const discovered = await new DefaultPackageManager({ cwd, agentDir, settingsManager }).resolve();
48
+ const visible = ({ enabled, metadata }) => enabled && (projectTrusted || metadata.scope !== "project");
49
+ const extensions = [...new Set(discovered.extensions.filter(visible).map(({ path }) => path))];
50
+ const loader = new DefaultResourceLoader({ cwd, agentDir, settingsManager, noExtensions: true, noSkills: true, additionalSkillPaths: [...new Set(discovered.skills.filter(visible).map(({ path }) => path))] });
51
+ await loader.reload();
52
+ const skillPaths = new Map(loader.getSkills().skills.map(({ name: skill, filePath }) => [skill, filePath]));
53
+ const discovery = { cwd, agentDir, projectTrusted, extensionRoleDirectories: starterRoleDirectories() };
54
+ const resolveWith = (definition) => {
55
+ const requestedTools = resolveRole(name, { ...discovery, definition }).selectorLayers.tools.flatMap((layer) => layer ?? []).filter((selector) => !selector.startsWith("!") && !resourcePatternHasMagic(selector));
56
+ return resolveRole(name, { ...discovery, definition, resources: { extensions, skills: [...skillPaths.keys()], tools: [...new Set([...PI_BUILTIN_TOOLS, ...requestedTools])] } });
57
+ };
58
+ const definition = loadRole(name, discovery);
59
+ let role;
60
+ try {
61
+ role = resolveWith(definition);
62
+ }
63
+ catch (error) {
64
+ // Starter roles use aliases the workflow extension resolves against the launching session's model; standalone, pi's default model plays that part.
65
+ if (!(error instanceof WorkflowError) || error.code !== "UNKNOWN_MODEL")
66
+ throw error;
67
+ process.stderr.write(`pi-role: ${errorText(error)}; starting pi with its default model\n`);
68
+ const withoutModel = { ...definition };
69
+ delete withoutModel.model;
70
+ role = resolveWith(withoutModel);
71
+ }
72
+ return piArguments(role, skillPaths, rest);
73
+ }
74
+ function usage(cwd, agentDir) {
75
+ const roles = discoverRoles({ cwd, agentDir, projectTrusted: projectTrust(cwd, agentDir, []), extensionRoleDirectories: starterRoleDirectories() });
76
+ const lines = Object.entries(roles).map(([name, { description }]) => ` ${name.padEnd(16)}${description ?? ""}`);
77
+ return `Usage: pi-role <role> [pi arguments...]\n\nRoles:\n${lines.join("\n")}\n\nProject roles need pi's saved trust decision or --approve.\n`;
78
+ }
79
+ export async function runPiRole(argv) {
80
+ const [name, ...rest] = argv;
81
+ if (!name || name === "--help" || name === "-h") {
82
+ process.stdout.write(usage(process.cwd(), getAgentDir()));
83
+ return name ? 0 : 1;
84
+ }
85
+ let args;
86
+ try {
87
+ args = await resolvePiArguments(name, rest);
88
+ }
89
+ catch (error) {
90
+ process.stderr.write(`pi-role: ${errorText(error)}\n`);
91
+ return 1;
92
+ }
93
+ return new Promise((done) => {
94
+ const child = spawn("pi", args, { stdio: "inherit" });
95
+ process.on("SIGINT", () => { }); // the child owns the terminal; it handles Ctrl+C and exits
96
+ child.on("error", (error) => { process.stderr.write(`pi-role: ${errorText(error)}\n`); done(1); });
97
+ child.on("exit", (code, signal) => { done(signal ? 128 + constants.signals[signal] : code ?? 1); });
98
+ });
99
+ }
100
+ if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
101
+ process.exitCode = await runPiRole(process.argv.slice(2));
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.14.1",
3
+ "version": "5.15.0",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -10,16 +10,18 @@
10
10
  },
11
11
  "type": "module",
12
12
  "exports": {
13
- ".": "./dist/src/cli.js"
13
+ ".": "./dist/src/cli.js",
14
+ "./pi-role": "./dist/src/pi-role.js"
14
15
  },
15
16
  "bin": {
16
- "piewf": "./dist/src/cli.js"
17
+ "piewf": "./dist/src/cli.js",
18
+ "pi-role": "./dist/src/pi-role.js"
17
19
  },
18
20
  "files": [
19
21
  "dist/src"
20
22
  ],
21
23
  "scripts": {
22
- "build": "npm --prefix ../.. run build --workspace=../core && rm -rf dist && tsc -p tsconfig.json && chmod +x dist/src/cli.js",
24
+ "build": "npm --prefix ../.. run build --workspace=../core && rm -rf dist && tsc -p tsconfig.json && chmod +x dist/src/cli.js dist/src/pi-role.js",
23
25
  "inspect": "node dist/src/cli.js inspect",
24
26
  "test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
25
27
  "test:run": "files=${TEST_FILES:-dist/test/*.test.js}; printf '%s\\n' $files | xargs -n1 -P20 sh -c 'tmp=$(mktemp -d); TMPDIR=\"$tmp\" node --test --test-concurrency=1 --test-timeout=60000 --test-force-exit --test-reporter=dot \"$1\"; status=$?; rm -rf \"$tmp\"; exit $status' sh",
@@ -43,7 +45,7 @@
43
45
  "@earendil-works/pi-coding-agent": "0.85.0",
44
46
  "@earendil-works/pi-server": "0.85.0",
45
47
  "@earendil-works/pi-tui": "0.85.0",
46
- "pi-extensible-workflows": "5.14.1",
48
+ "pi-extensible-workflows": "5.15.0",
47
49
  "typebox": "1.3.7"
48
50
  },
49
51
  "publishConfig": {