@papillon6814/copi 0.1.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.
@@ -0,0 +1,584 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ function inferSearchScopes(options) {
5
+ const homeDir = path.resolve(options.homeDir);
6
+ const agentDir = path.resolve(options.agentDir);
7
+ const projectDir = path.resolve(options.projectDir);
8
+ if (homeDir === projectDir && agentDir === projectDir)
9
+ return { home: false, project: true };
10
+ if (homeDir === projectDir)
11
+ return { home: true, project: false };
12
+ return { home: true, project: true };
13
+ }
14
+ const excludedNames = new Set([".git", ".cache", ".npm", "auth", "node_modules", "cache", "caches", "history", "sessions"]);
15
+ const contextFiles = ["AGENTS.md", "AGENTS.MD", "AGENTS.override.md", "CLAUDE.md", "CLAUDE.MD"];
16
+ function isWithin(root, target) {
17
+ const relative = path.relative(root, target);
18
+ return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
19
+ }
20
+ function shouldExclude(relativePath) {
21
+ return relativePath.split(path.sep).some((part) => {
22
+ const lower = part.toLowerCase();
23
+ return excludedNames.has(lower) || lower === "auth.json" || lower === "mcp-auth.json" || lower === "oauth.json" || lower.startsWith(".env");
24
+ });
25
+ }
26
+ function displayRelative(root, target) {
27
+ return path.relative(root, target) || path.basename(target);
28
+ }
29
+ function parseJson(value) {
30
+ try {
31
+ return record(JSON.parse(value));
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function record(value) {
38
+ if (typeof value !== "object" || value === null || Array.isArray(value))
39
+ return undefined;
40
+ return value;
41
+ }
42
+ function strings(value) {
43
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
44
+ }
45
+ function packageSelections(value) {
46
+ if (!Array.isArray(value))
47
+ return [];
48
+ const selections = [];
49
+ for (const entry of value) {
50
+ if (typeof entry === "string") {
51
+ selections.push({ source: entry });
52
+ continue;
53
+ }
54
+ const item = record(entry);
55
+ if (item && typeof item.source === "string") {
56
+ selections.push({
57
+ source: item.source,
58
+ ...(Array.isArray(item.skills) ? { skills: strings(item.skills) } : {}),
59
+ ...(Array.isArray(item.agents) ? { agents: strings(item.agents) } : {}),
60
+ });
61
+ }
62
+ }
63
+ return selections;
64
+ }
65
+ function globExpression(pattern) {
66
+ let expression = "^";
67
+ for (let index = 0; index < pattern.length; index += 1) {
68
+ const character = pattern[index];
69
+ if (character === "*" && pattern[index + 1] === "*") {
70
+ expression += ".*";
71
+ index += 1;
72
+ }
73
+ else if (character === "*") {
74
+ expression += "[^/]*";
75
+ }
76
+ else if (character === "?") {
77
+ expression += "[^/]";
78
+ }
79
+ else {
80
+ expression += character?.replace(/[|\\{}()[\]^$+?.]/g, "\\$&") ?? "";
81
+ }
82
+ }
83
+ return new RegExp(`${expression}$`);
84
+ }
85
+ function packagePaths(declared, filters) {
86
+ if (!filters)
87
+ return declared;
88
+ let selected = [];
89
+ const positive = filters.filter((filter) => !filter.startsWith("!") && !filter.startsWith("-") && !filter.startsWith("+"));
90
+ if (positive.length === 0)
91
+ selected = [...declared];
92
+ for (const filter of positive) {
93
+ const pattern = globExpression(filter.replace(/^\+/, ""));
94
+ selected.push(...declared.filter((entry) => pattern.test(entry)));
95
+ }
96
+ for (const filter of filters.filter((entry) => entry.startsWith("!") || entry.startsWith("-"))) {
97
+ const pattern = globExpression(filter.slice(1));
98
+ selected = selected.filter((entry) => !pattern.test(entry));
99
+ }
100
+ for (const filter of filters.filter((entry) => entry.startsWith("+"))) {
101
+ const selectedPath = filter.slice(1);
102
+ if (declared.includes(selectedPath))
103
+ selected.push(selectedPath);
104
+ }
105
+ return [...new Set(selected)];
106
+ }
107
+ function addWarning(warnings, code, target, message) {
108
+ if (!warnings.some((warning) => warning.code === code && warning.path === target))
109
+ warnings.push({ code, path: target, message });
110
+ }
111
+ async function canonicalDirectory(directory) {
112
+ try {
113
+ const target = await realpath(directory);
114
+ return (await lstat(target)).isDirectory() ? target : undefined;
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ export async function discoverResources(options) {
121
+ const scopes = options.scopes ?? inferSearchScopes(options);
122
+ if (!scopes.home && !scopes.project)
123
+ return { candidates: [], warnings: [], excludedPaths: [] };
124
+ const warnings = [];
125
+ const excludedPaths = new Set();
126
+ const candidates = [];
127
+ const candidateKeys = new Set();
128
+ const home = scopes.home ? await canonicalDirectory(options.homeDir) : undefined;
129
+ const agent = scopes.home ? await canonicalDirectory(options.agentDir) : undefined;
130
+ const project = scopes.project ? await canonicalDirectory(options.projectDir) : undefined;
131
+ const packageAgentDir = agent;
132
+ if (scopes.home && !home)
133
+ addWarning(warnings, "unavailable-root", options.homeDir, "The explicitly selected home directory is unavailable.");
134
+ if (scopes.home && !agent)
135
+ addWarning(warnings, "unavailable-root", options.agentDir, "The explicitly selected Pi agent directory is unavailable.");
136
+ if (scopes.project && !project)
137
+ addWarning(warnings, "unavailable-root", options.projectDir, "The explicitly selected project directory is unavailable.");
138
+ const roots = [];
139
+ if (home)
140
+ roots.push({ path: home, scope: "shared", source: "shared-agent-skills", destinationRoot: ".pi/skills", destinationKind: "project" });
141
+ if (agent)
142
+ roots.push({ path: agent, scope: "user", source: "pi-agent-directory", destinationRoot: ".pi/skills", destinationKind: "global-only" });
143
+ if (project)
144
+ roots.push({ path: project, scope: "project", source: "project", destinationRoot: ".pi/skills", destinationKind: "project" });
145
+ const addCandidate = async (kind, root, sourcePath, name, scope, source, destination) => {
146
+ const canonicalSource = await realpath(sourcePath).catch(() => undefined);
147
+ const canonicalRoot = await realpath(root).catch(() => undefined);
148
+ if (!canonicalSource || !canonicalRoot || !isWithin(canonicalRoot, canonicalSource)) {
149
+ addWarning(warnings, "resource-outside-root", sourcePath, "A configured resource resolves outside its allowed search root and was not included.");
150
+ return;
151
+ }
152
+ const stat = await lstat(canonicalSource).catch(() => undefined);
153
+ if (!stat || (!stat.isDirectory() && !stat.isFile()) || stat.isSymbolicLink())
154
+ return;
155
+ const relative = displayRelative(canonicalRoot, canonicalSource);
156
+ if (shouldExclude(relative)) {
157
+ excludedPaths.add(canonicalSource);
158
+ return;
159
+ }
160
+ const key = `${kind}\0${canonicalSource}`;
161
+ if (candidateKeys.has(key))
162
+ return;
163
+ candidateKeys.add(key);
164
+ candidates.push({
165
+ id: createHash("sha256").update(key).digest("hex").slice(0, 24),
166
+ kind,
167
+ source,
168
+ scope,
169
+ sourceRoot: canonicalRoot,
170
+ sourcePath: canonicalSource,
171
+ name,
172
+ destination,
173
+ });
174
+ };
175
+ const walk = async (root, startPath, onFile) => {
176
+ const canonicalRoot = await canonicalDirectory(root);
177
+ if (!canonicalRoot)
178
+ return;
179
+ const visit = async (directory) => {
180
+ let entries;
181
+ try {
182
+ entries = await readdir(directory, { withFileTypes: true });
183
+ }
184
+ catch (error) {
185
+ const code = error.code;
186
+ addWarning(warnings, code === "EACCES" ? "permission-denied" : "scan-error", directory, code === "EACCES" ? "Permission denied while searching this directory." : "Unable to search this directory.");
187
+ return;
188
+ }
189
+ for (const entry of entries) {
190
+ const child = path.join(directory, entry.name);
191
+ const relative = path.relative(canonicalRoot, child);
192
+ if (shouldExclude(relative)) {
193
+ excludedPaths.add(child);
194
+ if (relative.split(path.sep).some((part) => part.toLowerCase() === "auth")) {
195
+ addWarning(warnings, "auth-directory-excluded", child, "An auth directory was excluded; copied resources may be missing authentication-specific code.");
196
+ }
197
+ continue;
198
+ }
199
+ if (entry.isSymbolicLink()) {
200
+ const target = await realpath(child).catch(() => undefined);
201
+ addWarning(warnings, target && isWithin(canonicalRoot, target) ? "symlink-skipped" : "external-symlink", child, "Symbolic links are not followed during resource discovery.");
202
+ continue;
203
+ }
204
+ if (entry.isDirectory())
205
+ await visit(child);
206
+ else if (entry.isFile())
207
+ await onFile(child);
208
+ }
209
+ };
210
+ const canonicalStart = await realpath(startPath).catch(() => undefined);
211
+ if (!canonicalStart || !isWithin(canonicalRoot, canonicalStart)) {
212
+ addWarning(warnings, "resource-outside-root", startPath, "A configured resource root resolves outside its allowed search root.");
213
+ return;
214
+ }
215
+ const startRelative = path.relative(canonicalRoot, canonicalStart);
216
+ if (shouldExclude(startRelative)) {
217
+ excludedPaths.add(canonicalStart);
218
+ if (startRelative.split(path.sep).some((part) => part.toLowerCase() === "auth")) {
219
+ addWarning(warnings, "auth-directory-excluded", canonicalStart, "An auth directory was excluded; copied resources may be missing authentication-specific code.");
220
+ }
221
+ return;
222
+ }
223
+ const stat = await lstat(canonicalStart).catch(() => undefined);
224
+ if (!stat)
225
+ return;
226
+ if (stat.isFile())
227
+ await onFile(canonicalStart);
228
+ else if (stat.isDirectory())
229
+ await visit(canonicalStart);
230
+ };
231
+ const addSkillRoot = async (root, startPath, scope, source, destinationRoot) => {
232
+ await walk(root, startPath, async (file) => {
233
+ if (path.basename(file) !== "SKILL.md")
234
+ return;
235
+ const folder = path.dirname(file);
236
+ await addCandidate("skill", root, folder, path.basename(folder), scope, source, { kind: "project", path: path.posix.join(destinationRoot, path.basename(folder)) });
237
+ });
238
+ };
239
+ const addAgentRoot = async (root, startPath, scope, source) => {
240
+ await walk(root, startPath, async (file) => {
241
+ const base = path.basename(file);
242
+ const relativeParts = path.relative(root, file).split(path.sep);
243
+ if (base.toLowerCase() === "skill.md" || relativeParts.some((part) => part.toLowerCase() === "skills") || !base.toLowerCase().endsWith(".md") || base.endsWith(".chain.md"))
244
+ return;
245
+ await addCandidate("subagent", root, file, base, scope, source, { kind: "project", path: path.posix.join(".pi/agents", base) });
246
+ });
247
+ };
248
+ const addSettingCandidate = async (file, root, scope, kind, destination) => {
249
+ await addCandidate(kind, root, file, path.basename(file), scope, scope === "project" ? "project-pi-config" : "pi-agent-directory", destination);
250
+ };
251
+ const scanConfiguredResource = async (settingDir, allowedRoots, value, scope, kind, source) => {
252
+ const selected = value.replace(/^\+/, "");
253
+ if (selected.startsWith("!") || selected.startsWith("-"))
254
+ return;
255
+ const resolved = path.resolve(settingDir, selected.replace(/^~/, options.homeDir));
256
+ const allowed = allowedRoots.find((allowedRoot) => isWithin(allowedRoot, resolved));
257
+ if (!allowed) {
258
+ addWarning(warnings, "resource-outside-root", resolved, "A configured resource path is outside the authorized home or project tree.");
259
+ return;
260
+ }
261
+ const destinationRoot = kind === "skill" ? ".pi/skills" : ".pi/agents";
262
+ if (/[*?]/.test(selected)) {
263
+ const pattern = globExpression(path.relative(allowed, resolved).split(path.sep).join("/"));
264
+ await walk(allowed, allowed, async (file) => {
265
+ const relativeFile = path.relative(allowed, file).split(path.sep).join("/");
266
+ const base = path.basename(file);
267
+ if (kind === "skill" && base === "SKILL.md") {
268
+ const resourcePath = path.relative(allowed, path.dirname(file)).split(path.sep).join("/");
269
+ if (!pattern.test(resourcePath) && !pattern.test(relativeFile))
270
+ return;
271
+ await addCandidate("skill", allowed, path.dirname(file), path.basename(path.dirname(file)), scope, source, { kind: "project", path: path.posix.join(destinationRoot, path.basename(path.dirname(file))) });
272
+ }
273
+ else if (kind === "subagent" && base.toLowerCase().endsWith(".md") && base !== "SKILL.md" && !base.endsWith(".chain.md") && pattern.test(relativeFile)) {
274
+ await addCandidate("subagent", allowed, file, base, scope, source, { kind: "project", path: path.posix.join(".pi/agents", base) });
275
+ }
276
+ });
277
+ return;
278
+ }
279
+ if (kind === "skill") {
280
+ await addSkillRoot(allowed, resolved, scope, source, destinationRoot);
281
+ }
282
+ else {
283
+ await addAgentRoot(allowed, resolved, scope, source);
284
+ }
285
+ };
286
+ const isExcludedPackagePath = (target, authorizedRoots) => {
287
+ return authorizedRoots.some((root) => isWithin(root, target) && path.relative(root, target).split(path.sep).some((part) => {
288
+ return part.toLowerCase() !== "node_modules" && shouldExclude(part);
289
+ }));
290
+ };
291
+ const resolveAuthorizedPackageRoot = async (requestedPath, authorizedRoots) => {
292
+ const packageRoot = await canonicalDirectory(requestedPath);
293
+ if (!packageRoot)
294
+ return undefined;
295
+ if (!authorizedRoots.some((allowedRoot) => isWithin(allowedRoot, packageRoot))) {
296
+ addWarning(warnings, "resource-outside-root", requestedPath, "A declared Pi package resolves outside the authorized home or project tree.");
297
+ return undefined;
298
+ }
299
+ if (isExcludedPackagePath(requestedPath, authorizedRoots) || isExcludedPackagePath(packageRoot, authorizedRoots)) {
300
+ excludedPaths.add(packageRoot);
301
+ addWarning(warnings, "excluded-package", requestedPath, "A declared package is inside an excluded authentication, environment, history, or cache path.");
302
+ return undefined;
303
+ }
304
+ return packageRoot;
305
+ };
306
+ const packageRootFor = async (selection, settingsDir, allowedRoots, packageAgentDir) => {
307
+ let selected = selection.source;
308
+ if (selected.startsWith("npm:")) {
309
+ const specifier = selected.slice(4);
310
+ const packageName = specifier.startsWith("@")
311
+ ? `${specifier.split("/")[0]}/${specifier.split("/")[1]?.split("@")[0] ?? ""}`
312
+ : specifier.split("@")[0];
313
+ if (!packageName)
314
+ return undefined;
315
+ return await resolveAuthorizedPackageRoot(path.join(packageAgentDir, "npm", "node_modules", ...packageName.split("/")), allowedRoots);
316
+ }
317
+ if (selected.startsWith("git:")) {
318
+ selected = selected.slice(4).replace(/^https?:\/\//, "");
319
+ selected = selected.replace(/\.git$/, "");
320
+ const at = selected.lastIndexOf("@");
321
+ if (at > selected.lastIndexOf("/"))
322
+ selected = selected.slice(0, at);
323
+ return await resolveAuthorizedPackageRoot(path.join(packageAgentDir, "git", ...selected.split("/")), allowedRoots);
324
+ }
325
+ if (selected.startsWith("file:"))
326
+ selected = selected.slice(5);
327
+ const resolved = path.resolve(settingsDir, selected);
328
+ if (!allowedRoots.some((allowedRoot) => isWithin(allowedRoot, resolved))) {
329
+ addWarning(warnings, "resource-outside-root", resolved, "A declared local package is outside the authorized home or project tree.");
330
+ return undefined;
331
+ }
332
+ return await resolveAuthorizedPackageRoot(resolved, allowedRoots);
333
+ };
334
+ const manifestResources = async (packageRoot, selection, scope, source, authorizedRoots) => {
335
+ const canonicalPackageRoot = await canonicalDirectory(packageRoot);
336
+ if (!canonicalPackageRoot || !authorizedRoots.some((allowedRoot) => isWithin(allowedRoot, canonicalPackageRoot))) {
337
+ addWarning(warnings, "resource-outside-root", packageRoot, "The declared Pi package is outside the authorized home or project tree.");
338
+ return;
339
+ }
340
+ const manifestPath = path.join(canonicalPackageRoot, "package.json");
341
+ const canonicalManifest = await realpath(manifestPath).catch(() => undefined);
342
+ if (!canonicalManifest || !authorizedRoots.some((allowedRoot) => isWithin(allowedRoot, canonicalManifest))) {
343
+ addWarning(warnings, "resource-outside-root", manifestPath, "The declared package manifest resolves outside the authorized home or project tree.");
344
+ return;
345
+ }
346
+ if (isExcludedPackagePath(canonicalPackageRoot, authorizedRoots) || isExcludedPackagePath(canonicalManifest, authorizedRoots)) {
347
+ excludedPaths.add(canonicalPackageRoot);
348
+ addWarning(warnings, "excluded-package", manifestPath, "An excluded package or manifest was not inspected.");
349
+ return;
350
+ }
351
+ let manifest;
352
+ try {
353
+ manifest = record(parseJson(await readFile(canonicalManifest, "utf8")));
354
+ }
355
+ catch (error) {
356
+ const code = error.code;
357
+ addWarning(warnings, code === "ENOENT" ? "missing-package-manifest" : "invalid-package-manifest", manifestPath, "The declared package manifest could not be read.");
358
+ return;
359
+ }
360
+ if (!manifest) {
361
+ addWarning(warnings, "invalid-package-manifest", manifestPath, "The declared package manifest is not a JSON object.");
362
+ return;
363
+ }
364
+ const pi = record(manifest.pi);
365
+ const legacyPi = record(manifest["pi-subagents"]);
366
+ const declaredSkills = strings(pi?.skills);
367
+ const declaredAgents = [
368
+ ...strings(record(pi?.subagents)?.agents),
369
+ ...strings(legacyPi?.agents),
370
+ ];
371
+ const skills = packagePaths(declaredSkills, selection.skills);
372
+ const agents = packagePaths(declaredAgents, selection.agents);
373
+ for (const entry of skills) {
374
+ await scanConfiguredResource(canonicalPackageRoot, [canonicalPackageRoot], entry, scope, "skill", source);
375
+ }
376
+ for (const entry of agents) {
377
+ await scanConfiguredResource(canonicalPackageRoot, [canonicalPackageRoot], entry, scope, "subagent", source);
378
+ }
379
+ };
380
+ const scanPackageSelections = async (value, settingsPath, allowedRoots, packageAgentDir) => {
381
+ for (const selection of packageSelections(value)) {
382
+ const packageAuthorizedRoots = selection.source.startsWith("npm:") || selection.source.startsWith("git:")
383
+ ? [...new Set([...allowedRoots, packageAgentDir])]
384
+ : allowedRoots;
385
+ const root = await packageRootFor(selection, path.dirname(settingsPath), packageAuthorizedRoots, packageAgentDir);
386
+ if (!root) {
387
+ addWarning(warnings, "package-unavailable", selection.source, "The declared Pi package is unavailable in the explicitly selected search roots.");
388
+ continue;
389
+ }
390
+ const source = `package:${selection.source}`;
391
+ await manifestResources(root, selection, "package", source, packageAuthorizedRoots);
392
+ }
393
+ };
394
+ if (home) {
395
+ const globalShared = path.join(home, ".agents", "skills");
396
+ await addSkillRoot(home, globalShared, "shared", "shared-agent-skills", ".pi/skills");
397
+ }
398
+ if (agent) {
399
+ for (const [name, kind, destination] of [
400
+ ["settings.json", "pi-settings", { kind: "project", path: ".pi/settings.json" }],
401
+ ["mcp.json", "mcp-config", { kind: "project", path: ".pi/mcp.json" }],
402
+ ["models.json", "model-config", { kind: "global-only" }],
403
+ ["keybindings.json", "keybindings", { kind: "global-only" }],
404
+ ]) {
405
+ const file = path.join(agent, name);
406
+ await addSettingCandidate(file, agent, "user", kind, destination);
407
+ }
408
+ await addSkillRoot(agent, path.join(agent, "skills"), "user", "pi-user-skills", ".pi/skills");
409
+ await addAgentRoot(agent, path.join(agent, "agents"), "user", "pi-user-agents");
410
+ for (const name of contextFiles) {
411
+ await addCandidate("instruction", agent, path.join(agent, name), name, "user", "pi-user-instructions", { kind: "project", path: name });
412
+ }
413
+ for (const name of ["SYSTEM.md", "APPEND_SYSTEM.md"]) {
414
+ await addCandidate("instruction", agent, path.join(agent, name), name, "user", "pi-user-instructions", { kind: "project", path: path.posix.join(".pi", name) });
415
+ }
416
+ }
417
+ if (project) {
418
+ const piDir = path.join(project, ".pi");
419
+ for (const [name, kind, destination] of [
420
+ ["settings.json", "pi-settings", { kind: "project", path: ".pi/settings.json" }],
421
+ ["mcp.json", "mcp-config", { kind: "project", path: ".pi/mcp.json" }],
422
+ ]) {
423
+ await addSettingCandidate(path.join(piDir, name), project, "project", kind, destination);
424
+ }
425
+ for (const [name, kind, destination] of [
426
+ ["models.json", "model-config", { kind: "global-only" }],
427
+ ["keybindings.json", "keybindings", { kind: "global-only" }],
428
+ ]) {
429
+ await addSettingCandidate(path.join(piDir, name), project, "project", kind, destination);
430
+ }
431
+ await addSkillRoot(project, path.join(piDir, "skills"), "project", "project-pi-skills", ".pi/skills");
432
+ await addSkillRoot(project, path.join(project, ".agents", "skills"), "project", "project-agent-skills", ".pi/skills");
433
+ await addAgentRoot(project, path.join(piDir, "agents"), "project", "project-pi-agents");
434
+ await addAgentRoot(project, path.join(project, ".agents"), "project", "project-agent-definitions");
435
+ for (const name of contextFiles) {
436
+ await addCandidate("instruction", project, path.join(project, name), name, "project", "project-instructions", { kind: "project", path: name });
437
+ }
438
+ for (const name of ["SYSTEM.md", "APPEND_SYSTEM.md"]) {
439
+ await addCandidate("instruction", project, path.join(piDir, name), name, "project", "project-instructions", { kind: "project", path: path.posix.join(".pi", name) });
440
+ }
441
+ const packageManifest = path.join(project, "package.json");
442
+ if (await lstat(packageManifest).then((stat) => stat.isFile()).catch(() => false)) {
443
+ await manifestResources(project, { source: path.basename(project) }, "project", `package:${path.basename(project)}`, [project]);
444
+ }
445
+ }
446
+ const scanSettings = async (settingsPath, scope, authorized, packageAgentDir) => {
447
+ const settingsStat = await lstat(settingsPath).catch((error) => {
448
+ if (error.code !== "ENOENT")
449
+ addWarning(warnings, error.code === "EACCES" ? "permission-denied" : "settings-unavailable", settingsPath, "Unable to access Pi settings.");
450
+ return undefined;
451
+ });
452
+ if (!settingsStat)
453
+ return;
454
+ const canonicalSettingsPath = await realpath(settingsPath).catch(() => undefined);
455
+ if (!canonicalSettingsPath || !authorized.some((allowedRoot) => isWithin(allowedRoot, canonicalSettingsPath))) {
456
+ addWarning(warnings, "resource-outside-root", settingsPath, "Settings resolve outside the authorized home or project tree and were not read.");
457
+ return;
458
+ }
459
+ let settings;
460
+ try {
461
+ const parsed = parseJson(await readFile(canonicalSettingsPath, "utf8"));
462
+ if (!parsed) {
463
+ addWarning(warnings, "invalid-settings", settingsPath, "Settings are not valid readable JSON; configured resources were not expanded.");
464
+ return;
465
+ }
466
+ settings = parsed;
467
+ }
468
+ catch (error) {
469
+ const code = error.code;
470
+ if (code !== "ENOENT")
471
+ addWarning(warnings, code === "EACCES" ? "permission-denied" : "invalid-settings", settingsPath, "Settings are not valid readable JSON; configured resources were not expanded.");
472
+ return;
473
+ }
474
+ const settingDir = path.dirname(settingsPath);
475
+ for (const entry of strings(settings.skills))
476
+ await scanConfiguredResource(settingDir, authorized, entry, scope, "skill", "settings-resource");
477
+ const subagents = record(settings.subagents);
478
+ for (const entry of strings(subagents?.agentScanDirs))
479
+ await scanConfiguredResource(settingDir, authorized, entry, scope, "subagent", "settings-agent-scan-root");
480
+ await scanPackageSelections(settings.packages, settingsPath, authorized, packageAgentDir);
481
+ };
482
+ const homeProjects = new Set();
483
+ const homeProfiles = new Set();
484
+ if (home) {
485
+ await walk(home, home, async (file) => {
486
+ const relativeParts = path.relative(home, file).split(path.sep);
487
+ const base = path.basename(file);
488
+ const piIndex = relativeParts.indexOf(".pi");
489
+ const profileName = piIndex >= 0 ? relativeParts[piIndex + 1] : undefined;
490
+ if (piIndex >= 0 && profileName && (profileName === "agent" || profileName.startsWith("agent-") || profileName.startsWith("agent_"))) {
491
+ const profileRoot = path.join(home, ...relativeParts.slice(0, piIndex + 2));
492
+ const profilePath = relativeParts.slice(piIndex + 2);
493
+ homeProfiles.add(profileRoot);
494
+ if (profilePath.length === 1) {
495
+ const config = new Map([
496
+ ["settings.json", ["pi-settings", { kind: "project", path: ".pi/settings.json" }]],
497
+ ["mcp.json", ["mcp-config", { kind: "project", path: ".pi/mcp.json" }]],
498
+ ["models.json", ["model-config", { kind: "global-only" }]],
499
+ ["keybindings.json", ["keybindings", { kind: "global-only" }]],
500
+ ]);
501
+ const resource = config.get(base);
502
+ if (resource)
503
+ await addSettingCandidate(file, home, "user", resource[0], resource[1]);
504
+ if (contextFiles.includes(base))
505
+ await addCandidate("instruction", home, file, base, "user", "pi-user-instructions", { kind: "project", path: base });
506
+ if (base === "SYSTEM.md" || base === "APPEND_SYSTEM.md")
507
+ await addCandidate("instruction", home, file, base, "user", "pi-user-instructions", { kind: "project", path: path.posix.join(".pi", base) });
508
+ }
509
+ else if (profilePath[0] === "skills" && base === "SKILL.md") {
510
+ const skillPath = path.dirname(file);
511
+ await addCandidate("skill", home, skillPath, path.basename(skillPath), "user", "pi-user-skills", { kind: "project", path: path.posix.join(".pi/skills", path.basename(skillPath)) });
512
+ }
513
+ else if (profilePath[0] === "agents" && base.toLowerCase().endsWith(".md") && !base.endsWith(".chain.md")) {
514
+ await addCandidate("subagent", home, file, base, "user", "pi-user-agents", { kind: "project", path: path.posix.join(".pi/agents", base) });
515
+ }
516
+ return;
517
+ }
518
+ if (piIndex >= 0) {
519
+ const projectRoot = path.join(home, ...relativeParts.slice(0, piIndex));
520
+ homeProjects.add(projectRoot);
521
+ const piPath = relativeParts.slice(piIndex + 1);
522
+ if (piPath[0] === "skills" && base === "SKILL.md") {
523
+ const skillPath = path.dirname(file);
524
+ await addCandidate("skill", home, skillPath, path.basename(skillPath), "project", "project-pi-skills", { kind: "project", path: path.posix.join(".pi/skills", path.basename(skillPath)) });
525
+ }
526
+ else if (piPath[0] === "agents" && base.toLowerCase().endsWith(".md") && !base.endsWith(".chain.md")) {
527
+ await addCandidate("subagent", home, file, base, "project", "project-pi-agents", { kind: "project", path: path.posix.join(".pi/agents", base) });
528
+ }
529
+ else if (piPath.length === 1) {
530
+ const config = new Map([
531
+ ["settings.json", ["pi-settings", { kind: "project", path: ".pi/settings.json" }]],
532
+ ["mcp.json", ["mcp-config", { kind: "project", path: ".pi/mcp.json" }]],
533
+ ["models.json", ["model-config", { kind: "global-only" }]],
534
+ ["keybindings.json", ["keybindings", { kind: "global-only" }]],
535
+ ]);
536
+ const resource = config.get(base);
537
+ if (resource)
538
+ await addSettingCandidate(file, home, "project", resource[0], resource[1]);
539
+ if (base === "SYSTEM.md" || base === "APPEND_SYSTEM.md")
540
+ await addCandidate("instruction", home, file, base, "project", "project-instructions", { kind: "project", path: path.posix.join(".pi", base) });
541
+ }
542
+ }
543
+ const agentsIndex = relativeParts.indexOf(".agents");
544
+ if (agentsIndex >= 0) {
545
+ const agentsPath = relativeParts.slice(agentsIndex + 1);
546
+ const projectRoot = path.join(home, ...relativeParts.slice(0, agentsIndex));
547
+ if (projectRoot !== home)
548
+ homeProjects.add(projectRoot);
549
+ if (agentsPath[0] === "skills" && base === "SKILL.md") {
550
+ const skillPath = path.dirname(file);
551
+ const scope = agentsIndex === 0 ? "shared" : "project";
552
+ await addCandidate("skill", home, skillPath, path.basename(skillPath), scope, scope === "shared" ? "shared-agent-skills" : "project-agent-skills", { kind: "project", path: path.posix.join(".pi/skills", path.basename(skillPath)) });
553
+ }
554
+ else if (base.toLowerCase().endsWith(".md") && base !== "SKILL.md" && !base.endsWith(".chain.md") && !agentsPath.includes("skills")) {
555
+ await addCandidate("subagent", home, file, base, "project", "project-agent-definitions", { kind: "project", path: path.posix.join(".pi/agents", base) });
556
+ }
557
+ }
558
+ if (contextFiles.includes(base)) {
559
+ const projectRoot = path.dirname(file);
560
+ if (projectRoot !== home && !isWithin(path.join(home, ".pi"), projectRoot))
561
+ homeProjects.add(projectRoot);
562
+ await addCandidate("instruction", home, file, base, "project", "project-instructions", { kind: "project", path: base });
563
+ }
564
+ });
565
+ for (const profileRoot of homeProfiles) {
566
+ await scanSettings(path.join(profileRoot, "settings.json"), "user", [profileRoot, home], profileRoot);
567
+ }
568
+ for (const projectRoot of homeProjects) {
569
+ await scanSettings(path.join(projectRoot, ".pi", "settings.json"), "project", [projectRoot], packageAgentDir ?? projectRoot);
570
+ const manifestPath = path.join(projectRoot, "package.json");
571
+ if (await lstat(manifestPath).then((stat) => stat.isFile()).catch(() => false)) {
572
+ await manifestResources(projectRoot, { source: path.basename(projectRoot) }, "project", `package:${path.basename(projectRoot)}`, [projectRoot]);
573
+ }
574
+ }
575
+ }
576
+ if (agent) {
577
+ await scanSettings(path.join(agent, "settings.json"), "user", [agent, ...(home ? [home] : [])], agent);
578
+ }
579
+ if (project) {
580
+ await scanSettings(path.join(project, ".pi", "settings.json"), "project", [project], packageAgentDir ?? project);
581
+ }
582
+ candidates.sort((left, right) => left.name.localeCompare(right.name) || left.sourcePath.localeCompare(right.sourcePath));
583
+ return { candidates, warnings, excludedPaths: [...excludedPaths].sort() };
584
+ }
@@ -0,0 +1,4 @@
1
+ import type { Catalog, Group } from "./types.ts";
2
+ export declare function createGroup(catalog: Catalog, name: string): Group;
3
+ export declare function renameGroup(catalog: Catalog, groupId: string, name: string): void;
4
+ export declare function setGroupItems(catalog: Catalog, groupId: string, itemIds: string[]): void;
@@ -0,0 +1,30 @@
1
+ import { randomUUID } from "node:crypto";
2
+ function findGroup(catalog, groupId) {
3
+ const group = catalog.groups.find((item) => item.id === groupId);
4
+ if (!group)
5
+ throw new Error(`Unknown group: ${groupId}`);
6
+ return group;
7
+ }
8
+ function normalizedName(name) {
9
+ if (name.trim().length === 0)
10
+ throw new Error("Group name must not be empty.");
11
+ return name.trim();
12
+ }
13
+ export function createGroup(catalog, name) {
14
+ const group = { id: randomUUID(), name: normalizedName(name), itemIds: [] };
15
+ catalog.groups.push(group);
16
+ return group;
17
+ }
18
+ export function renameGroup(catalog, groupId, name) {
19
+ const nextName = normalizedName(name);
20
+ findGroup(catalog, groupId).name = nextName;
21
+ }
22
+ export function setGroupItems(catalog, groupId, itemIds) {
23
+ const group = findGroup(catalog, groupId);
24
+ if (!Array.isArray(itemIds) || new Set(itemIds).size !== itemIds.length)
25
+ throw new Error("Group membership must contain unique item identifiers.");
26
+ const knownItems = new Set(catalog.items.map((item) => item.id));
27
+ if (itemIds.some((id) => !knownItems.has(id)))
28
+ throw new Error("Group membership references an unknown saved item.");
29
+ group.itemIds = [...itemIds];
30
+ }
@@ -0,0 +1,5 @@
1
+ import { type PreparedResource, type PreparedInspection } from "./catalog.ts";
2
+ import type { InspectionResult, ResourceCandidate } from "./types.ts";
3
+ export declare function inspectPreparedResource(prepared: PreparedResource): Promise<PreparedInspection>;
4
+ export declare function inspectResource(candidate: ResourceCandidate): Promise<InspectionResult>;
5
+ export declare function maskSensitiveText(value: string): string;