@finngi/pi-resources 0.0.0-stage → 0.1.0-alpha.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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -2
  3. package/node_modules/pi-resources/LICENSE +21 -0
  4. package/node_modules/pi-resources/README.md +101 -0
  5. package/node_modules/pi-resources/dist/extensions/resources.d.ts +3 -0
  6. package/node_modules/pi-resources/dist/extensions/resources.js +273 -0
  7. package/node_modules/pi-resources/dist/lib/package-picker.d.ts +3 -0
  8. package/node_modules/pi-resources/dist/lib/package-picker.js +61 -0
  9. package/node_modules/pi-resources/dist/lib/picker.d.ts +25 -0
  10. package/node_modules/pi-resources/dist/lib/picker.js +85 -0
  11. package/node_modules/pi-resources/dist/lib/resource-packages.d.ts +29 -0
  12. package/node_modules/pi-resources/dist/lib/resource-packages.js +187 -0
  13. package/node_modules/pi-resources/dist/lib/resource-selection.d.ts +91 -0
  14. package/node_modules/pi-resources/dist/lib/resource-selection.js +487 -0
  15. package/node_modules/pi-resources/dist/lib/saved-instruction-context.d.ts +4 -0
  16. package/node_modules/pi-resources/dist/lib/saved-instruction-context.js +24 -0
  17. package/node_modules/pi-resources/dist/lib/session-setup.d.ts +10 -0
  18. package/node_modules/pi-resources/dist/lib/session-setup.js +561 -0
  19. package/node_modules/pi-resources/node_modules/pi-contracts/LICENSE +21 -0
  20. package/node_modules/pi-resources/node_modules/pi-contracts/README.md +16 -0
  21. package/node_modules/pi-resources/node_modules/pi-contracts/dist/index.d.ts +268 -0
  22. package/node_modules/pi-resources/node_modules/pi-contracts/dist/index.js +9 -0
  23. package/node_modules/pi-resources/node_modules/pi-contracts/package.json +31 -0
  24. package/node_modules/pi-resources/node_modules/pi-resource-browser/LICENSE +21 -0
  25. package/node_modules/pi-resources/node_modules/pi-resource-browser/README.md +81 -0
  26. package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-browser.d.ts +130 -0
  27. package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-browser.js +974 -0
  28. package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-search.d.ts +14 -0
  29. package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-search.js +54 -0
  30. package/node_modules/pi-resources/node_modules/pi-resource-browser/package.json +38 -0
  31. package/node_modules/pi-resources/package.json +72 -0
  32. package/package.json +38 -4
@@ -0,0 +1,85 @@
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
+ import { writeResourceDefaults, writeResourceMode, } from "./resource-selection.js";
3
+ /** One scope implementation for independent resources and package availability. */
4
+ export function resourcePickerControls(options) {
5
+ const { ctx, setup, entries, members } = options;
6
+ const aggregate = (values, mixed) => values.length > 0 && values.every((value) => value === values[0]) ? values[0] : mixed;
7
+ const refresh = () => {
8
+ for (const entry of entries) {
9
+ const states = members(entry).map((member) => setup.state(member.kind, member.id, member.nativeDefault));
10
+ entry.selected = states.length > 0 && states.every((state) => state.selected);
11
+ if (states.length > 1)
12
+ entry.badge =
13
+ states.some((state) => state.selected) && !entry.selected ? "partial" : undefined;
14
+ entry.sessionState = aggregate(states.map((state) => state.sessionState), "mixed");
15
+ entry.projectState = aggregate(states.map((state) => state.projectState), "mixed");
16
+ entry.globalState = aggregate(states.map((state) => state.globalState), "mixed");
17
+ entry.projectMode = aggregate(states.map((state) => state.projectMode), "mixed");
18
+ entry.globalMode = aggregate(states.map((state) => state.globalMode), "mixed");
19
+ entry.effectiveSource = aggregate(states.map((state) => state.effectiveSource), undefined);
20
+ entry.mode = aggregate(states.map((state) => state.mode), undefined);
21
+ entry.modeSource = aggregate(states.map((state) => state.modeSource), undefined);
22
+ }
23
+ };
24
+ const eligible = (entry, scope) => {
25
+ if (entry.defaultable === false || (scope === "global" && entry.globalDefaultable === false))
26
+ throw Error("This resource cannot be saved at that scope");
27
+ if (scope === "project" && !ctx.isProjectTrusted())
28
+ throw Error("Saving defaults requires a trusted project");
29
+ return members(entry).filter((member) => scope !== "global" || member.globalDefaultable !== false);
30
+ };
31
+ const saved = async (scope, rows, state) => {
32
+ if (scope === "project" && !ctx.isProjectTrusted())
33
+ throw Error("Saving defaults requires a trusted project");
34
+ const selections = {};
35
+ const targets = rows.flatMap((entry) => eligible(entry, scope));
36
+ // Bulk snapshots save On/Off; session types change only through the saved-scope menu.
37
+ for (const member of targets) {
38
+ const selection = (selections[member.kind] ??= { knownIds: [], values: {} });
39
+ selection.knownIds.push(member.id);
40
+ selection.values[member.id] =
41
+ state === undefined
42
+ ? setup.state(member.kind, member.id, member.nativeDefault).selected
43
+ : state === "inherit"
44
+ ? undefined
45
+ : state === "on";
46
+ }
47
+ await writeResourceDefaults({ scope, selections, cwd: ctx.cwd, agentDir: getAgentDir() });
48
+ setup.refresh(ctx);
49
+ refresh();
50
+ };
51
+ const setSession = (entry, state) => {
52
+ const byKind = new Map();
53
+ for (const member of members(entry))
54
+ byKind.set(member.kind, { ...byKind.get(member.kind), [member.id]: state });
55
+ for (const [kind, states] of byKind)
56
+ setup.setStates(kind, states, ctx);
57
+ refresh();
58
+ };
59
+ refresh();
60
+ return {
61
+ refresh,
62
+ toggle: async (entry, selected) => {
63
+ setSession(entry, selected ? "on" : "off");
64
+ },
65
+ setSession: async (entry, state) => {
66
+ setSession(entry, state);
67
+ },
68
+ setDefault: (entry, state, scope) => saved(scope, [entry], state),
69
+ setMode: async (entry, mode, scope) => {
70
+ for (const member of eligible(entry, scope))
71
+ await writeResourceMode({
72
+ scope,
73
+ kind: member.kind,
74
+ id: member.id,
75
+ mode,
76
+ cwd: ctx.cwd,
77
+ agentDir: getAgentDir(),
78
+ });
79
+ setup.refresh(ctx);
80
+ refresh();
81
+ },
82
+ saveDefaults: (rows) => saved("project", rows),
83
+ saveGlobalDefaults: (rows) => saved("global", rows),
84
+ };
85
+ }
@@ -0,0 +1,29 @@
1
+ import { type SettingsManager, type SourceInfo } from "@earendil-works/pi-coding-agent";
2
+ export interface ResourcePackage {
3
+ name: string;
4
+ root: string;
5
+ globalDefaultable?: boolean;
6
+ /** Enabled native personal skills, before project/CLI sources are added. */
7
+ personalSkills?: string[];
8
+ description: string;
9
+ keywords: string[];
10
+ skills: string[];
11
+ agents: string[];
12
+ instructions: Array<{
13
+ catalogue: string;
14
+ path: string;
15
+ content: string;
16
+ }>;
17
+ }
18
+ export declare function withinResourceRoot(root: string, file: string): boolean;
19
+ /** Read package data through a bounded regular file; never execute package entrypoints. */
20
+ export declare function readResourceFile(root: string, file: string): string;
21
+ /** Only the two resource types missing from native Pi need a descriptor reader. */
22
+ export declare function readResourcePackage(directory: string, skills?: string[]): ResourcePackage;
23
+ /** Native Pi owns sources, identity, trust and skill filtering; discovery never installs packages. */
24
+ export declare function discoverResourcePackages(settings: SettingsManager, cwd: string, agentDir: string, loaded?: SourceInfo[], temporarySources?: string[]): Promise<{
25
+ packs: ResourcePackage[];
26
+ problems: string[];
27
+ }>;
28
+ /** Pack metadata augments the existing resource browser search, never the model prompt. */
29
+ export declare function resourcePackageKeywords(pack: ResourcePackage): string;
@@ -0,0 +1,187 @@
1
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readdirSync, readSync, realpathSync, statSync, } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { DefaultPackageManager, parseArgs, } from "@earendil-works/pi-coding-agent";
4
+ export function withinResourceRoot(root, file) {
5
+ const rel = relative(root, file);
6
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
7
+ }
8
+ /** Read package data through a bounded regular file; never execute package entrypoints. */
9
+ export function readResourceFile(root, file) {
10
+ const canonical = realpathSync(file);
11
+ if (!withinResourceRoot(root, canonical))
12
+ throw Error("resource path escapes pack root");
13
+ let fd;
14
+ try {
15
+ fd = openSync(canonical, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
16
+ const info = fstatSync(fd);
17
+ if (!info.isFile() || info.nlink !== 1 || info.size > 1024 * 1024)
18
+ throw Error("resource must be a bounded regular file without hard links");
19
+ const bytes = Buffer.allocUnsafe(1024 * 1024 + 1);
20
+ let length = 0;
21
+ while (length < bytes.length) {
22
+ const read = readSync(fd, bytes, length, bytes.length - length, length);
23
+ if (!read)
24
+ break;
25
+ length += read;
26
+ }
27
+ if (length > 1024 * 1024)
28
+ throw Error("resource exceeds the discovery byte limit");
29
+ const body = bytes.subarray(0, length).toString("utf8");
30
+ const after = lstatSync(canonical);
31
+ if (realpathSync(file) !== canonical ||
32
+ after.dev !== info.dev ||
33
+ after.ino !== info.ino ||
34
+ after.nlink !== 1)
35
+ throw Error("resource changed during discovery");
36
+ return body;
37
+ }
38
+ finally {
39
+ if (fd !== undefined)
40
+ closeSync(fd);
41
+ }
42
+ }
43
+ function declaredDirectories(root, value) {
44
+ if (value === undefined)
45
+ return [];
46
+ if (!Array.isArray(value) || value.some((path) => typeof path !== "string" || isAbsolute(path)))
47
+ throw Error("pi.agents and pi.instructions must be relative directory arrays");
48
+ return [
49
+ ...new Set(value.map((path) => {
50
+ const canonical = realpathSync(resolve(root, path));
51
+ if (!withinResourceRoot(root, canonical) || !statSync(canonical).isDirectory())
52
+ throw Error("resource directory escapes package root or is not a directory");
53
+ return canonical;
54
+ })),
55
+ ];
56
+ }
57
+ /** Only the two resource types missing from native Pi need a descriptor reader. */
58
+ export function readResourcePackage(directory, skills = []) {
59
+ const root = realpathSync(directory);
60
+ const manifest = JSON.parse(readResourceFile(root, join(root, "package.json")));
61
+ if (!manifest || typeof manifest.name !== "string" || !manifest.name.trim())
62
+ throw Error("package.json requires a package name");
63
+ if (manifest.description !== undefined && typeof manifest.description !== "string")
64
+ throw Error("package description must be a string");
65
+ if (manifest.keywords !== undefined &&
66
+ (!Array.isArray(manifest.keywords) ||
67
+ manifest.keywords.some((word) => typeof word !== "string")))
68
+ throw Error("package keywords must be strings");
69
+ const agents = declaredDirectories(root, manifest.pi?.agents);
70
+ const catalogues = declaredDirectories(root, manifest.pi?.instructions);
71
+ const instructions = catalogues.flatMap((catalogue) => readdirSync(catalogue)
72
+ .filter((name) => name.endsWith(".md"))
73
+ .sort()
74
+ .map((name) => ({
75
+ catalogue,
76
+ path: join(catalogue, name),
77
+ content: readResourceFile(root, join(catalogue, name)),
78
+ })));
79
+ return {
80
+ name: manifest.name,
81
+ root,
82
+ description: manifest.description ?? "",
83
+ keywords: manifest.keywords ?? [],
84
+ skills,
85
+ agents,
86
+ instructions,
87
+ };
88
+ }
89
+ /** Native Pi owns sources, identity, trust and skill filtering; discovery never installs packages. */
90
+ export async function discoverResourcePackages(settings, cwd, agentDir, loaded, temporarySources = parseArgs(process.argv.slice(2)).extensions ?? []) {
91
+ const manager = new DefaultPackageManager({ settingsManager: settings, cwd, agentDir });
92
+ const skipped = new Set();
93
+ const paths = await manager.resolve(async (source) => {
94
+ skipped.add(source);
95
+ return "skip";
96
+ });
97
+ const roots = new Map();
98
+ const personalRoots = new Set();
99
+ const personalSkills = new Map();
100
+ const temporaryRoots = new Set();
101
+ const problems = [];
102
+ // CLI packages have already passed Pi's resolver. Look up their roots without installing again.
103
+ for (const source of temporarySources) {
104
+ if (source.startsWith("builtin:"))
105
+ continue;
106
+ try {
107
+ // Pi 1.2 implements this scope, but its exported declaration omits it.
108
+ // @ts-expect-error Native getInstalledPath supports temporary package locations.
109
+ const directory = manager.getInstalledPath(source, "temporary");
110
+ if (!directory || !statSync(directory).isDirectory())
111
+ continue;
112
+ const root = realpathSync(directory);
113
+ temporaryRoots.add(root);
114
+ roots.set(root, (loaded ?? [])
115
+ .filter((skill) => {
116
+ try {
117
+ return withinResourceRoot(root, realpathSync(skill.path));
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ })
123
+ .map((skill) => skill.path));
124
+ }
125
+ catch {
126
+ problems.push(`${source}: temporary package resources could not be resolved`);
127
+ }
128
+ }
129
+ for (const [kind, resources] of Object.entries(paths))
130
+ for (const resource of resources) {
131
+ if (!resource.metadata.packageRoot)
132
+ continue;
133
+ const root = realpathSync(resource.metadata.packageRoot);
134
+ const skills = roots.get(root) ?? [];
135
+ if (resource.metadata.scope === "user") {
136
+ personalRoots.add(root);
137
+ if (kind === "skills" && resource.enabled)
138
+ personalSkills.set(root, [...(personalSkills.get(root) ?? []), resource.path]);
139
+ }
140
+ if (kind === "skills" &&
141
+ resource.enabled &&
142
+ (!loaded || loaded.some((source) => source.path === resource.path)))
143
+ skills.push(resource.path);
144
+ roots.set(root, skills);
145
+ }
146
+ // Native resolution has no output record for a package containing only custom resource types.
147
+ for (const entry of manager.listConfiguredPackages().reverse()) {
148
+ // Pi supplies the installed root for local, Git and npm sources; never resolve caches ourselves.
149
+ if (!entry.installedPath || skipped.has(entry.source))
150
+ continue;
151
+ const root = realpathSync(entry.installedPath);
152
+ if (entry.scope === "user")
153
+ personalRoots.add(root);
154
+ if (statSync(root).isDirectory() && !roots.has(root))
155
+ roots.set(root, []);
156
+ }
157
+ const packs = [], names = new Set(), temporaryNames = new Set();
158
+ for (const [root, skills] of roots) {
159
+ if (!existsSync(join(root, "package.json")))
160
+ continue;
161
+ try {
162
+ const pack = readResourcePackage(root, [...new Set(skills)]);
163
+ pack.globalDefaultable = personalRoots.has(root);
164
+ pack.personalSkills = personalSkills.get(root) ?? [];
165
+ // Availability gates only skills, agents and instructions; extension code always loads.
166
+ if (!pack.skills.length && !pack.agents.length && !pack.instructions.length)
167
+ continue;
168
+ // Explicit CLI content takes precedence over another configured copy of that package.
169
+ if (names.has(pack.name) && !temporaryRoots.has(root) && temporaryNames.has(pack.name))
170
+ continue;
171
+ if (names.has(pack.name))
172
+ throw Error("duplicate package name");
173
+ names.add(pack.name);
174
+ if (temporaryRoots.has(root))
175
+ temporaryNames.add(pack.name);
176
+ packs.push(pack);
177
+ }
178
+ catch {
179
+ problems.push(`${root}: package resources could not be read`);
180
+ }
181
+ }
182
+ return { packs, problems };
183
+ }
184
+ /** Pack metadata augments the existing resource browser search, never the model prompt. */
185
+ export function resourcePackageKeywords(pack) {
186
+ return [pack.name, pack.description, ...pack.keywords].join(" ");
187
+ }
@@ -0,0 +1,91 @@
1
+ import type { SettingsManager } from "@earendil-works/pi-coding-agent";
2
+ import { type ResourcePackage } from "./resource-packages.ts";
3
+ export type ResourceKind = "packages" | "skills" | "agents" | "instructions";
4
+ export type ResourceDefaultState = "inherit" | "on" | "off";
5
+ export type ResourceSelectionValue = Record<string, boolean> | undefined;
6
+ export type ResourceMode = "any" | "interactive";
7
+ export type ResourceModeState = "inherit" | ResourceMode;
8
+ export type ResourceModes = Record<string, ResourceMode> | undefined;
9
+ export declare function readResourceModes(settings: SettingsManager, scope: "project" | "global", kind: ResourceKind): ResourceModes;
10
+ export declare function validateResourceModes(value: unknown): ResourceModes;
11
+ export declare function resourceModeState(value: ResourceModes, id: string): ResourceModeState;
12
+ export declare function resolveResourceMode(global: ResourceModes, project: ResourceModes, id: string): {
13
+ mode: ResourceMode;
14
+ source: "project" | "global" | "native";
15
+ };
16
+ export declare function resourceApplies(settings: SettingsManager, kind: ResourceKind, id: string, interactive: boolean, packName?: string): boolean;
17
+ export declare function packageApplies(settings: SettingsManager, name: string, interactive: boolean): boolean;
18
+ /** Whether a package's resources load here, or why they do not. */
19
+ export type PackageAvailability = "available" | "off" | "interactive";
20
+ export declare function packageAvailability(settings: SettingsManager, name: string, interactive: boolean): PackageAvailability;
21
+ export interface SkillChoice {
22
+ name: string;
23
+ description: string;
24
+ path: string;
25
+ toggleable: boolean;
26
+ defaultSelected: boolean;
27
+ manualOnly?: boolean;
28
+ packName?: string;
29
+ packKeywords?: string;
30
+ }
31
+ export interface GlobalSelectionSnapshot {
32
+ present: boolean;
33
+ values?: Record<string, boolean>;
34
+ }
35
+ export declare function readResourceSelection(settings: SettingsManager, scope: "project" | "global", kind: ResourceKind): ResourceSelectionValue;
36
+ export declare function validateResourceSelection(value: unknown, kind: ResourceKind): ResourceSelectionValue;
37
+ export declare function resourceSelectionState(value: ResourceSelectionValue, id: string): ResourceDefaultState;
38
+ export declare function resolveResourceSelection(global: ResourceSelectionValue, project: ResourceSelectionValue, id: string, nativeDefault: boolean, session?: ResourceSelectionValue): {
39
+ globalState: ResourceDefaultState;
40
+ projectState: ResourceDefaultState;
41
+ effective: boolean;
42
+ source: "session" | "project" | "global" | "native";
43
+ };
44
+ export interface ResourceDefaultWrite {
45
+ scope: "project" | "global";
46
+ kind: ResourceKind;
47
+ id: string;
48
+ state: ResourceDefaultState;
49
+ knownIds: string[];
50
+ cwd?: string;
51
+ agentDir?: string;
52
+ }
53
+ export interface ResourceDefaultsWrite {
54
+ scope: "project" | "global";
55
+ selections: Partial<Record<ResourceKind, {
56
+ values: Record<string, boolean | undefined>;
57
+ knownIds: string[];
58
+ /** Bulk save: discard saved IDs that are no longer known in this category. */
59
+ snapshot?: boolean;
60
+ }>>;
61
+ cwd?: string;
62
+ agentDir?: string;
63
+ }
64
+ export declare function writeResourceDefault(options: ResourceDefaultWrite): Promise<void>;
65
+ export declare function writeResourceDefaults(options: ResourceDefaultsWrite): Promise<void>;
66
+ export declare function writeResourceMode(options: {
67
+ scope: "project" | "global";
68
+ kind: ResourceKind;
69
+ id: string;
70
+ mode: ResourceModeState;
71
+ cwd?: string;
72
+ agentDir?: string;
73
+ }): Promise<void>;
74
+ export declare function configuredSkillChoices(cwd: string, agentDir: string, loaded: Array<{
75
+ name: string;
76
+ path: string;
77
+ }>, packs?: ResourcePackage[]): SkillChoice[];
78
+ export declare function selectedSkills(settings: SettingsManager, choices: SkillChoice[], ignoreInvalidProjectSelection?: boolean, globalSnapshot?: GlobalSelectionSnapshot, interactive?: boolean): SkillChoice[];
79
+ export declare function filterPromptSkills<T extends {
80
+ name: string;
81
+ }>(skills: T[], selected: SkillChoice[]): T[];
82
+ export declare function selectedAgents<T extends {
83
+ name: string;
84
+ catalogueOnly?: boolean;
85
+ packName?: string;
86
+ }>(settings: SettingsManager, agents: T[], globalSnapshot?: GlobalSelectionSnapshot, interactive?: boolean): {
87
+ agents: T[];
88
+ configured: boolean;
89
+ error?: string;
90
+ };
91
+ export declare function assertSafeProjectSettingsLocation(cwd: string): Promise<void>;