@llblab/pi-actors 0.19.11 → 0.20.1
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/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/lib/actor-inspector-tui.d.ts +55 -0
- package/dist/lib/actor-inspector-tui.js +559 -0
- package/dist/lib/actor-messages.d.ts +25 -0
- package/dist/lib/actor-messages.js +122 -0
- package/dist/lib/actor-recipe-context.d.ts +14 -0
- package/dist/lib/actor-recipe-context.js +79 -0
- package/dist/lib/actor-rooms.d.ts +81 -0
- package/dist/lib/actor-rooms.js +468 -0
- package/dist/lib/async-runs.d.ts +101 -0
- package/dist/lib/async-runs.js +612 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +592 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +226 -0
- package/dist/lib/execution.d.ts +63 -0
- package/dist/lib/execution.js +450 -0
- package/dist/lib/file-state.d.ts +6 -0
- package/dist/lib/file-state.js +25 -0
- package/dist/lib/identity.d.ts +9 -0
- package/dist/lib/identity.js +27 -0
- package/dist/lib/observability.d.ts +86 -0
- package/dist/lib/observability.js +534 -0
- package/dist/lib/output.d.ts +25 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/paths.d.ts +11 -0
- package/dist/lib/paths.js +33 -0
- package/dist/lib/prompts.d.ts +23 -0
- package/dist/lib/prompts.js +50 -0
- package/dist/lib/recipe-discovery.d.ts +50 -0
- package/dist/lib/recipe-discovery.js +317 -0
- package/dist/lib/recipe-migration.d.ts +21 -0
- package/dist/lib/recipe-migration.js +90 -0
- package/dist/lib/recipe-references.d.ts +67 -0
- package/dist/lib/recipe-references.js +542 -0
- package/dist/lib/recipe-usage.d.ts +6 -0
- package/dist/lib/recipe-usage.js +57 -0
- package/dist/lib/registry.d.ts +47 -0
- package/dist/lib/registry.js +222 -0
- package/dist/lib/runtime.d.ts +36 -0
- package/dist/lib/runtime.js +126 -0
- package/dist/lib/schema.d.ts +48 -0
- package/dist/lib/schema.js +355 -0
- package/dist/lib/temp.d.ts +10 -0
- package/dist/lib/temp.js +90 -0
- package/dist/lib/tools.d.ts +39 -0
- package/dist/lib/tools.js +982 -0
- package/lib/async-runs.ts +20 -4
- package/lib/paths.ts +5 -1
- package/package.json +5 -2
- package/scripts/async-runner.mjs +8 -12
- package/scripts/validate-recipe.mjs +9 -13
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt and schema copy helpers
|
|
3
|
+
* Zones: prompts, onboarding, tool schema copy
|
|
4
|
+
* Owns LLM-facing descriptions, prompt snippets, guidelines, and parameter descriptions
|
|
5
|
+
*/
|
|
6
|
+
export declare const REGISTER_TOOL_DESCRIPTION: string;
|
|
7
|
+
export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
|
|
8
|
+
export declare const REGISTER_TOOL_GUIDELINES: string[];
|
|
9
|
+
export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.\n- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.\n- For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.";
|
|
10
|
+
export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
|
|
11
|
+
readonly name: "Tool name in snake_case (e.g., 'transcribe')";
|
|
12
|
+
readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
|
|
13
|
+
readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
|
|
14
|
+
readonly state_dir: "Optional async run state directory for a co-located template recipe.";
|
|
15
|
+
readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
|
|
16
|
+
readonly templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.";
|
|
17
|
+
readonly templateNull: "Delete the tool when template is null.";
|
|
18
|
+
readonly args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast";
|
|
19
|
+
readonly update: "Set to true to overwrite an existing tool registration.";
|
|
20
|
+
readonly values: "Optional default runtime placeholder values for a co-located template recipe.";
|
|
21
|
+
};
|
|
22
|
+
export declare function formatRegisteredToolPromptSnippet(template: unknown): string;
|
|
23
|
+
export declare function formatRecipeToolPromptSnippet(recipe: string, asyncRecipe: boolean): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt and schema copy helpers
|
|
3
|
+
* Zones: prompts, onboarding, tool schema copy
|
|
4
|
+
* Owns LLM-facing descriptions, prompt snippets, guidelines, and parameter descriptions
|
|
5
|
+
*/
|
|
6
|
+
export const REGISTER_TOOL_DESCRIPTION = "Register a persistent custom tool from a command template, template recipe path, or co-located template recipe. " +
|
|
7
|
+
"Definitions are stored as recipe files under ~/.pi/agent/recipes across reloads. " +
|
|
8
|
+
"Use update=true to overwrite an existing tool, template=null/empty to delete.";
|
|
9
|
+
export const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
|
|
10
|
+
export const REGISTER_TOOL_GUIDELINES = [
|
|
11
|
+
"Use register_tool to wrap trusted local commands, scripts, programs, libraries, or template recipes as persistent pi tools.",
|
|
12
|
+
"After register_tool succeeds, the new tool is immediately callable and remains available after reload.",
|
|
13
|
+
'Set template=null or template="" in register_tool to delete a persisted tool.',
|
|
14
|
+
"Set update=true in register_tool to overwrite an existing tool registration.",
|
|
15
|
+
];
|
|
16
|
+
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
17
|
+
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
18
|
+
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
19
|
+
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.
|
|
20
|
+
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
21
|
+
- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
|
|
22
|
+
- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
|
|
23
|
+
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
24
|
+
- Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
|
|
25
|
+
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
|
|
26
|
+
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
|
|
27
|
+
- Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
|
|
28
|
+
- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.
|
|
29
|
+
- For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
|
|
30
|
+
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
31
|
+
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
32
|
+
description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
33
|
+
async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
34
|
+
state_dir: "Optional async run state directory for a co-located template recipe.",
|
|
35
|
+
template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.",
|
|
36
|
+
templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
|
|
37
|
+
templateNull: "Delete the tool when template is null.",
|
|
38
|
+
args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast",
|
|
39
|
+
update: "Set to true to overwrite an existing tool registration.",
|
|
40
|
+
values: "Optional default runtime placeholder values for a co-located template recipe.",
|
|
41
|
+
};
|
|
42
|
+
export function formatRegisteredToolPromptSnippet(template) {
|
|
43
|
+
const rendered = typeof template === "string" ? template : JSON.stringify(template);
|
|
44
|
+
return `Execute command template: ${rendered}`;
|
|
45
|
+
}
|
|
46
|
+
export function formatRecipeToolPromptSnippet(recipe, asyncRecipe) {
|
|
47
|
+
return asyncRecipe
|
|
48
|
+
? `Start async template recipe: ${recipe}`
|
|
49
|
+
: `Execute template recipe: ${recipe}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-discovered recipe registry helpers
|
|
3
|
+
* Zones: recipe discovery, tool exposure, migration diagnostics
|
|
4
|
+
* Owns filename identity discovery across prioritized recipe roots
|
|
5
|
+
*/
|
|
6
|
+
import type { RegisteredTool } from "./config.ts";
|
|
7
|
+
import type { TemplateRecipeConfig } from "./recipe-references.ts";
|
|
8
|
+
export interface DiscoveredRecipe {
|
|
9
|
+
id: string;
|
|
10
|
+
path: string;
|
|
11
|
+
root: string;
|
|
12
|
+
priority: number;
|
|
13
|
+
config?: TemplateRecipeConfig;
|
|
14
|
+
active: boolean;
|
|
15
|
+
shadowed: boolean;
|
|
16
|
+
invalid: boolean;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
tool: boolean;
|
|
19
|
+
mutableUsage: boolean;
|
|
20
|
+
diagnostics: string[];
|
|
21
|
+
shadows: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface RecipeIntegrityManifestEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
path: string;
|
|
26
|
+
root: string;
|
|
27
|
+
sha256: string;
|
|
28
|
+
size: number;
|
|
29
|
+
tool: boolean;
|
|
30
|
+
active: boolean;
|
|
31
|
+
invalid: boolean;
|
|
32
|
+
disabled: boolean;
|
|
33
|
+
shadowed: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface RecipeDiscoveryResult {
|
|
36
|
+
active: Map<string, DiscoveredRecipe>;
|
|
37
|
+
entries: DiscoveredRecipe[];
|
|
38
|
+
diagnostics: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface RecipeDiscoverySource {
|
|
41
|
+
root?: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
defaultTool?: boolean;
|
|
44
|
+
mutableUsage?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function discoverRecipeSources(sources: RecipeDiscoverySource[]): RecipeDiscoveryResult;
|
|
47
|
+
export declare function discoverRecipes(roots: string[]): RecipeDiscoveryResult;
|
|
48
|
+
export declare function createRecipeIntegrityManifest(result: RecipeDiscoveryResult): RecipeIntegrityManifestEntry[];
|
|
49
|
+
export declare function summarizeDiscovery(result: RecipeDiscoveryResult): Record<string, unknown>;
|
|
50
|
+
export declare function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | undefined;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-discovered recipe registry helpers
|
|
3
|
+
* Zones: recipe discovery, tool exposure, migration diagnostics
|
|
4
|
+
* Owns filename identity discovery across prioritized recipe roots
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import * as CommandTemplates from "./command-templates.js";
|
|
10
|
+
import * as RecipeReferences from "./recipe-references.js";
|
|
11
|
+
import * as Schema from "./schema.js";
|
|
12
|
+
function assertToolSafeRepeatConfig(config, argTypes, defaults) {
|
|
13
|
+
if (typeof config === "string" || config === undefined || config === null)
|
|
14
|
+
return;
|
|
15
|
+
if (Array.isArray(config)) {
|
|
16
|
+
for (const step of config)
|
|
17
|
+
assertToolSafeRepeatConfig(step, argTypes, defaults);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (typeof config !== "object")
|
|
21
|
+
return;
|
|
22
|
+
const node = config;
|
|
23
|
+
if (typeof node.repeat === "string") {
|
|
24
|
+
const trimmed = node.repeat.trim();
|
|
25
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
26
|
+
const match = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
|
|
27
|
+
if (!match || (argTypes[match[1]]?.kind !== "array" && !Array.isArray(defaults[match[1]])))
|
|
28
|
+
throw new Error("Command template repeat must be a positive integer or {array.length} with an array argument/default.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
assertToolSafeRepeatConfig(node.template, argTypes, defaults);
|
|
32
|
+
assertToolSafeRepeatConfig(node.recover, argTypes, defaults);
|
|
33
|
+
}
|
|
34
|
+
function listRecipeFiles(root) {
|
|
35
|
+
if (!existsSync(root))
|
|
36
|
+
return [];
|
|
37
|
+
return readdirSync(root, { withFileTypes: true })
|
|
38
|
+
.filter((entry) => entry.isFile() &&
|
|
39
|
+
entry.name.endsWith(".json") &&
|
|
40
|
+
entry.name !== "legacy-tool-registry-migration-report.json")
|
|
41
|
+
.map((entry) => join(root, entry.name))
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
function getRecipeConfigDiagnostics(file, config) {
|
|
45
|
+
if (!config)
|
|
46
|
+
return [`Invalid recipe: ${file}`];
|
|
47
|
+
return CommandTemplates.getCommandTemplateWarnings(typeof config.template === "string" ? config.template : { template: config.template }).map((warning) => `Recipe ${file}: ${warning}`);
|
|
48
|
+
}
|
|
49
|
+
function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutableUsage = false) {
|
|
50
|
+
const id = RecipeReferences.getRecipeIdFromPath(file);
|
|
51
|
+
try {
|
|
52
|
+
const config = RecipeReferences.readResolvedRecipeConfig(file);
|
|
53
|
+
const invalid = !config;
|
|
54
|
+
const disabled = config?.disabled === true;
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
path: file,
|
|
58
|
+
root,
|
|
59
|
+
priority,
|
|
60
|
+
config,
|
|
61
|
+
active: false,
|
|
62
|
+
shadowed: false,
|
|
63
|
+
invalid,
|
|
64
|
+
disabled,
|
|
65
|
+
tool: defaultTool && !disabled && !invalid,
|
|
66
|
+
mutableUsage,
|
|
67
|
+
diagnostics: getRecipeConfigDiagnostics(file, config),
|
|
68
|
+
shadows: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
path: file,
|
|
75
|
+
root,
|
|
76
|
+
priority,
|
|
77
|
+
active: false,
|
|
78
|
+
shadowed: false,
|
|
79
|
+
invalid: true,
|
|
80
|
+
disabled: false,
|
|
81
|
+
tool: false,
|
|
82
|
+
mutableUsage,
|
|
83
|
+
diagnostics: [
|
|
84
|
+
`Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
85
|
+
],
|
|
86
|
+
shadows: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function filesForSource(source) {
|
|
91
|
+
const defaultTool = source.defaultTool === true;
|
|
92
|
+
const mutableUsage = source.mutableUsage === true;
|
|
93
|
+
if (source.file)
|
|
94
|
+
return [{ root: source.root ?? source.file, file: source.file, defaultTool, mutableUsage }];
|
|
95
|
+
return source.root
|
|
96
|
+
? listRecipeFiles(source.root).map((file) => ({
|
|
97
|
+
root: source.root,
|
|
98
|
+
file,
|
|
99
|
+
defaultTool,
|
|
100
|
+
mutableUsage,
|
|
101
|
+
}))
|
|
102
|
+
: [];
|
|
103
|
+
}
|
|
104
|
+
function getRecipeRootDiagnostics(sources) {
|
|
105
|
+
const diagnostics = [];
|
|
106
|
+
const roots = new Set(sources
|
|
107
|
+
.map((source) => source.root)
|
|
108
|
+
.filter((root) => typeof root === "string"));
|
|
109
|
+
for (const root of roots) {
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(root))
|
|
112
|
+
continue;
|
|
113
|
+
const stat = statSync(root);
|
|
114
|
+
if ((stat.mode & 0o002) !== 0) {
|
|
115
|
+
diagnostics.push(`Recipe root is world-writable; review permissions: ${root}`);
|
|
116
|
+
}
|
|
117
|
+
if ((stat.mode & 0o020) !== 0) {
|
|
118
|
+
diagnostics.push(`Recipe root is group-writable; review ownership and permissions: ${root}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
diagnostics.push(`Failed to inspect recipe root ${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return diagnostics;
|
|
126
|
+
}
|
|
127
|
+
export function discoverRecipeSources(sources) {
|
|
128
|
+
const entries = sources.flatMap((source, priority) => filesForSource(source).map(({ root, file, defaultTool, mutableUsage }) => readDiscoveredRecipe(root, file, priority, defaultTool, mutableUsage)));
|
|
129
|
+
const byId = new Map();
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const bucket = byId.get(entry.id) ?? [];
|
|
132
|
+
bucket.push(entry);
|
|
133
|
+
byId.set(entry.id, bucket);
|
|
134
|
+
}
|
|
135
|
+
const active = new Map();
|
|
136
|
+
const diagnostics = getRecipeRootDiagnostics(sources);
|
|
137
|
+
for (const [id, bucket] of byId) {
|
|
138
|
+
bucket.sort((a, b) => a.priority - b.priority || a.path.localeCompare(b.path));
|
|
139
|
+
const winner = bucket[0];
|
|
140
|
+
winner.active = true;
|
|
141
|
+
winner.shadows = bucket.slice(1).map((entry) => entry.path);
|
|
142
|
+
active.set(id, winner);
|
|
143
|
+
for (const shadow of bucket.slice(1))
|
|
144
|
+
shadow.shadowed = true;
|
|
145
|
+
if (winner.invalid)
|
|
146
|
+
diagnostics.push(`Recipe ${id} is invalid and blocks lower-priority recipes`);
|
|
147
|
+
if (winner.disabled)
|
|
148
|
+
diagnostics.push(`Recipe ${id} is disabled and blocks lower-priority recipes`);
|
|
149
|
+
if (winner.shadows.length > 0)
|
|
150
|
+
diagnostics.push(`Recipe ${id} shadows ${winner.shadows.length} lower-priority recipe(s)`);
|
|
151
|
+
diagnostics.push(...winner.diagnostics);
|
|
152
|
+
}
|
|
153
|
+
return { active, entries, diagnostics };
|
|
154
|
+
}
|
|
155
|
+
export function discoverRecipes(roots) {
|
|
156
|
+
return discoverRecipeSources(roots.map((root) => ({ root })));
|
|
157
|
+
}
|
|
158
|
+
function recipeUsage(config) {
|
|
159
|
+
const usage = config?.usage;
|
|
160
|
+
return usage && typeof usage === "object" && !Array.isArray(usage)
|
|
161
|
+
? usage
|
|
162
|
+
: undefined;
|
|
163
|
+
}
|
|
164
|
+
function cleanupRecommendation(entry) {
|
|
165
|
+
if (entry.invalid) {
|
|
166
|
+
return {
|
|
167
|
+
id: entry.id,
|
|
168
|
+
path: entry.path,
|
|
169
|
+
reason: "invalid recipe blocks lower-priority entries with the same id",
|
|
170
|
+
actions: ["fix", "delete", "archive"],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (entry.shadowed) {
|
|
174
|
+
return {
|
|
175
|
+
id: entry.id,
|
|
176
|
+
path: entry.path,
|
|
177
|
+
reason: "shadowed by a higher-priority recipe",
|
|
178
|
+
actions: ["merge", "delete", "archive"],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (entry.disabled) {
|
|
182
|
+
return {
|
|
183
|
+
id: entry.id,
|
|
184
|
+
path: entry.path,
|
|
185
|
+
reason: "disabled recipe is retained but not exposed as a tool",
|
|
186
|
+
actions: ["keep disabled", "delete", "archive"],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const usage = recipeUsage(entry.config);
|
|
190
|
+
const calls = Number(usage?.calls ?? 0);
|
|
191
|
+
if (entry.mutableUsage && entry.tool && calls === 0) {
|
|
192
|
+
return {
|
|
193
|
+
id: entry.id,
|
|
194
|
+
path: entry.path,
|
|
195
|
+
reason: "active user tool has no recorded launches",
|
|
196
|
+
actions: ["keep as tool", "move out of tool root", "delete", "archive"],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (entry.mutableUsage && !entry.tool) {
|
|
200
|
+
return {
|
|
201
|
+
id: entry.id,
|
|
202
|
+
path: entry.path,
|
|
203
|
+
reason: "user recipe is a component, not an active tool",
|
|
204
|
+
actions: ["keep component", "move into tool root", "merge", "delete", "archive"],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (entry.shadows.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
id: entry.id,
|
|
210
|
+
path: entry.path,
|
|
211
|
+
reason: `overrides ${entry.shadows.length} lower-priority recipe(s)`,
|
|
212
|
+
actions: ["keep override", "merge", "delete", "archive"],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
export function createRecipeIntegrityManifest(result) {
|
|
218
|
+
return result.entries
|
|
219
|
+
.map((entry) => {
|
|
220
|
+
const bytes = readFileSync(entry.path);
|
|
221
|
+
return {
|
|
222
|
+
active: entry.active,
|
|
223
|
+
disabled: entry.disabled,
|
|
224
|
+
id: entry.id,
|
|
225
|
+
invalid: entry.invalid,
|
|
226
|
+
path: entry.path,
|
|
227
|
+
root: entry.root,
|
|
228
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
229
|
+
shadowed: entry.shadowed,
|
|
230
|
+
size: bytes.byteLength,
|
|
231
|
+
tool: entry.tool,
|
|
232
|
+
};
|
|
233
|
+
})
|
|
234
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path));
|
|
235
|
+
}
|
|
236
|
+
function recommendationForEntry(entry, activePath) {
|
|
237
|
+
const recommendation = cleanupRecommendation(entry);
|
|
238
|
+
if (!recommendation)
|
|
239
|
+
return undefined;
|
|
240
|
+
if (entry.shadowed && activePath) {
|
|
241
|
+
return {
|
|
242
|
+
...recommendation,
|
|
243
|
+
reason: `shadowed by ${activePath}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return recommendation;
|
|
247
|
+
}
|
|
248
|
+
export function summarizeDiscovery(result) {
|
|
249
|
+
const recommendations = result.entries
|
|
250
|
+
.map((entry) => recommendationForEntry(entry, result.active.get(entry.id)?.path))
|
|
251
|
+
.filter((entry) => Boolean(entry))
|
|
252
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)) || String(a.path).localeCompare(String(b.path)));
|
|
253
|
+
return {
|
|
254
|
+
active: [...result.active.values()].map((entry) => ({
|
|
255
|
+
id: entry.id,
|
|
256
|
+
path: entry.path,
|
|
257
|
+
description: entry.config?.description,
|
|
258
|
+
tool: entry.tool,
|
|
259
|
+
disabled: entry.disabled,
|
|
260
|
+
invalid: entry.invalid,
|
|
261
|
+
shadows: entry.shadows,
|
|
262
|
+
...(recipeUsage(entry.config) ? { usage: recipeUsage(entry.config) } : {}),
|
|
263
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
264
|
+
shadowed: result.entries
|
|
265
|
+
.filter((entry) => entry.shadowed)
|
|
266
|
+
.map((entry) => ({ id: entry.id, path: entry.path, shadowedBy: result.active.get(entry.id)?.path }))
|
|
267
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
|
|
268
|
+
invalid: result.entries
|
|
269
|
+
.filter((entry) => entry.invalid)
|
|
270
|
+
.map((entry) => ({ id: entry.id, path: entry.path, diagnostics: entry.diagnostics }))
|
|
271
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
272
|
+
disabled: result.entries
|
|
273
|
+
.filter((entry) => entry.disabled)
|
|
274
|
+
.map((entry) => ({ id: entry.id, path: entry.path }))
|
|
275
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
276
|
+
recommendations,
|
|
277
|
+
diagnostics: result.diagnostics,
|
|
278
|
+
integrity_manifest: createRecipeIntegrityManifest(result),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function toRegisteredTool(entry) {
|
|
282
|
+
if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
|
|
283
|
+
return undefined;
|
|
284
|
+
const cfg = entry.config;
|
|
285
|
+
const template = entry.path;
|
|
286
|
+
const description = cfg.description ?? `Execute template recipe: ${entry.id}`;
|
|
287
|
+
const argTemplate = cfg.template;
|
|
288
|
+
const argTemplateConfig = typeof argTemplate === "object" && !Array.isArray(argTemplate)
|
|
289
|
+
? {
|
|
290
|
+
...argTemplate,
|
|
291
|
+
...(cfg.args !== undefined ? { args: cfg.args } : {}),
|
|
292
|
+
defaults: { ...(argTemplate.defaults ?? {}), ...(cfg.defaults ?? {}) },
|
|
293
|
+
}
|
|
294
|
+
: { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
|
|
295
|
+
const explicitArgTypes = Object.fromEntries((cfg.args ?? []).map((arg) => {
|
|
296
|
+
const parsed = Schema.parseToolArgToken(String(arg));
|
|
297
|
+
return [parsed.arg, parsed.type];
|
|
298
|
+
}));
|
|
299
|
+
assertToolSafeRepeatConfig(argTemplateConfig, explicitArgTypes, cfg.defaults ?? {});
|
|
300
|
+
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
301
|
+
return {
|
|
302
|
+
name: entry.id,
|
|
303
|
+
description,
|
|
304
|
+
template,
|
|
305
|
+
recipe: cfg,
|
|
306
|
+
args: Schema.getToolArgNames(argTemplateConfig),
|
|
307
|
+
defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [key, String(value)])),
|
|
308
|
+
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
309
|
+
...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
|
|
310
|
+
...(cfg.args ? { storedArgs: cfg.args } : {}),
|
|
311
|
+
...(cfg.defaults
|
|
312
|
+
? {
|
|
313
|
+
storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [key, String(value)])),
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy tool-registry to recipe-file migration helpers
|
|
3
|
+
* Zones: registry migration, recipe persistence, compatibility diagnostics
|
|
4
|
+
* Owns one-way migration from legacy tool registry entries into user recipe files
|
|
5
|
+
*/
|
|
6
|
+
export interface LegacyRegistryMigrationResult {
|
|
7
|
+
migrated: string[];
|
|
8
|
+
skipped: string[];
|
|
9
|
+
conflicts: string[];
|
|
10
|
+
invalid: string[];
|
|
11
|
+
archive?: string;
|
|
12
|
+
report?: string;
|
|
13
|
+
warnings: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface LegacyRegistryMigrationOptions {
|
|
16
|
+
configPath: string;
|
|
17
|
+
recipeRoot: string;
|
|
18
|
+
reservedToolNames: Set<string>;
|
|
19
|
+
archive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function migrateLegacyToolRegistry(options: LegacyRegistryMigrationOptions): LegacyRegistryMigrationResult;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy tool-registry to recipe-file migration helpers
|
|
3
|
+
* Zones: registry migration, recipe persistence, compatibility diagnostics
|
|
4
|
+
* Owns one-way migration from legacy tool registry entries into user recipe files
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import * as Config from "./config.js";
|
|
9
|
+
import { writeJsonAtomic } from "./file-state.js";
|
|
10
|
+
import * as RecipeReferences from "./recipe-references.js";
|
|
11
|
+
function recipePathForTool(recipeRoot, name) {
|
|
12
|
+
return join(recipeRoot, `${name}.json`);
|
|
13
|
+
}
|
|
14
|
+
function toRecipeConfig(tool) {
|
|
15
|
+
return {
|
|
16
|
+
description: tool.description,
|
|
17
|
+
...(tool.recipe?.async !== undefined ? { async: tool.recipe.async } : {}),
|
|
18
|
+
...(tool.recipe?.state_dir ? { state_dir: tool.recipe.state_dir } : {}),
|
|
19
|
+
...(tool.storedArgs ? { args: tool.storedArgs } : {}),
|
|
20
|
+
...(tool.storedDefaults ? { defaults: tool.storedDefaults } : {}),
|
|
21
|
+
template: tool.template,
|
|
22
|
+
...(tool.recipe?.values ? { values: tool.recipe.values } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function nextArchivePath(configPath) {
|
|
26
|
+
const base = `${configPath}.migrated`;
|
|
27
|
+
if (!existsSync(base))
|
|
28
|
+
return base;
|
|
29
|
+
for (let index = 1; index < 1000; index += 1) {
|
|
30
|
+
const path = `${base}.${index}`;
|
|
31
|
+
if (!existsSync(path))
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
return `${base}.${Date.now()}`;
|
|
35
|
+
}
|
|
36
|
+
export function migrateLegacyToolRegistry(options) {
|
|
37
|
+
const { configPath, recipeRoot, reservedToolNames } = options;
|
|
38
|
+
const result = {
|
|
39
|
+
migrated: [],
|
|
40
|
+
skipped: [],
|
|
41
|
+
conflicts: [],
|
|
42
|
+
invalid: [],
|
|
43
|
+
warnings: [],
|
|
44
|
+
};
|
|
45
|
+
if (!existsSync(configPath))
|
|
46
|
+
return result;
|
|
47
|
+
const loaded = Config.loadToolConfig(configPath, reservedToolNames);
|
|
48
|
+
result.warnings.push(...loaded.warnings);
|
|
49
|
+
mkdirSync(recipeRoot, { recursive: true });
|
|
50
|
+
for (const [name, tool] of loaded.tools) {
|
|
51
|
+
const path = recipePathForTool(recipeRoot, name);
|
|
52
|
+
if (existsSync(path)) {
|
|
53
|
+
result.conflicts.push(name);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const recipe = toRecipeConfig(tool);
|
|
57
|
+
try {
|
|
58
|
+
writeJsonAtomic(path, recipe);
|
|
59
|
+
const parsed = RecipeReferences.readResolvedRecipeConfig(path);
|
|
60
|
+
if (!parsed) {
|
|
61
|
+
result.invalid.push(name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
result.migrated.push(name);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
result.invalid.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (loaded.tools.size === 0)
|
|
71
|
+
result.skipped.push(basename(configPath));
|
|
72
|
+
const reportPath = join(recipeRoot, "legacy-tool-registry-migration-report.json");
|
|
73
|
+
writeJsonAtomic(reportPath, {
|
|
74
|
+
source: configPath,
|
|
75
|
+
migrated: result.migrated,
|
|
76
|
+
skipped: result.skipped,
|
|
77
|
+
conflicts: result.conflicts,
|
|
78
|
+
invalid: result.invalid,
|
|
79
|
+
warnings: result.warnings,
|
|
80
|
+
});
|
|
81
|
+
result.report = reportPath;
|
|
82
|
+
if (options.archive !== false &&
|
|
83
|
+
result.conflicts.length === 0 &&
|
|
84
|
+
result.invalid.length === 0) {
|
|
85
|
+
const archive = nextArchivePath(configPath);
|
|
86
|
+
renameSync(configPath, archive);
|
|
87
|
+
result.archive = archive;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template recipe reference helpers
|
|
3
|
+
* Zones: registry config, async runs, path resolution
|
|
4
|
+
* Owns detection, loading, and recipe-layer expansion for template recipe files
|
|
5
|
+
*/
|
|
6
|
+
import type { CommandTemplateValue } from "./command-templates.ts";
|
|
7
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
8
|
+
export interface TemplateRecipeImportBinding {
|
|
9
|
+
from?: string;
|
|
10
|
+
defaults?: Record<string, unknown>;
|
|
11
|
+
values?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
|
|
14
|
+
export interface TemplateRecipeMailbox {
|
|
15
|
+
accepts?: string[];
|
|
16
|
+
emits?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface TemplateRecipeDefinition {
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
imports?: Record<string, TemplateRecipeImport>;
|
|
23
|
+
template: CommandTemplateValue;
|
|
24
|
+
args?: string[];
|
|
25
|
+
defaults?: Record<string, unknown>;
|
|
26
|
+
parallel?: boolean;
|
|
27
|
+
label?: string;
|
|
28
|
+
when?: boolean | string;
|
|
29
|
+
timeout?: number | string;
|
|
30
|
+
delay?: number | string;
|
|
31
|
+
output?: string;
|
|
32
|
+
artifacts?: Record<string, string>;
|
|
33
|
+
mailbox?: TemplateRecipeMailbox;
|
|
34
|
+
retire_when?: "children_terminal";
|
|
35
|
+
retry?: number | string;
|
|
36
|
+
failure?: CommandTemplates.CommandTemplateFailureScope;
|
|
37
|
+
recover?: CommandTemplateValue;
|
|
38
|
+
repeat?: number;
|
|
39
|
+
values?: Record<string, unknown>;
|
|
40
|
+
usage?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
|
|
43
|
+
async?: boolean;
|
|
44
|
+
state_dir?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TemplateRecipeContextRecord {
|
|
47
|
+
alias?: string;
|
|
48
|
+
depth: number;
|
|
49
|
+
file: string;
|
|
50
|
+
import_path: string[];
|
|
51
|
+
name: string;
|
|
52
|
+
recipe: Record<string, unknown>;
|
|
53
|
+
role: "entry" | "import";
|
|
54
|
+
}
|
|
55
|
+
export interface ReadResolvedRecipeConfigOptions {
|
|
56
|
+
includeActorRecipeContext?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
|
|
59
|
+
export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
|
|
60
|
+
export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
|
|
61
|
+
export declare function getRecipeIdFromPath(file: string): string;
|
|
62
|
+
export declare function readResolvedRecipeConfig(file: string, stack?: string[], options?: ReadResolvedRecipeConfigOptions): TemplateRecipeConfig | undefined;
|
|
63
|
+
export declare function buildRecipeContextRecords(file: string): TemplateRecipeContextRecord[];
|
|
64
|
+
export declare function getRecipeTemplate(value: unknown): CommandTemplateValue | undefined;
|
|
65
|
+
export declare function isRecipeReference(value: unknown): boolean;
|
|
66
|
+
export declare function isAsyncRecipeReference(value: unknown): boolean;
|
|
67
|
+
export declare function isRecipeTool(template: unknown, recipe: TemplateRecipeConfig | undefined): boolean;
|