agent-skillboard 0.2.18 → 0.3.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/CHANGELOG.md +85 -0
- package/README.md +161 -255
- package/bin/postinstall.mjs +2 -1
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +41 -108
- package/docs/capabilities.md +17 -104
- package/docs/install.md +70 -473
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +133 -362
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +68 -279
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +49 -269
- package/examples/v2-multi-source.config.yaml +35 -0
- package/examples/v2-policy-error.config.yaml +6 -0
- package/package.json +1 -1
- package/src/advisor/actions.mjs +102 -6
- package/src/advisor/application-commands.mjs +10 -9
- package/src/advisor/apply-action.mjs +74 -1
- package/src/advisor/guidance.mjs +24 -16
- package/src/advisor/schema.mjs +17 -6
- package/src/advisor/skills.mjs +18 -5
- package/src/advisor.mjs +27 -9
- package/src/agent-integration-cli.mjs +96 -13
- package/src/agent-integration-content.mjs +21 -11
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-integration-home.mjs +14 -1
- package/src/agent-inventory-platforms.mjs +21 -8
- package/src/agent-inventory.mjs +44 -16
- package/src/agent-root-registry.mjs +127 -0
- package/src/agent-skill-import.mjs +2 -2
- package/src/agent-skill-roots.mjs +70 -13
- package/src/audit-paths.mjs +42 -0
- package/src/brief-cli.mjs +3 -2
- package/src/brief-renderer.mjs +1 -0
- package/src/cli.mjs +521 -235
- package/src/compatibility.mjs +24 -0
- package/src/control/can-use-guard.mjs +21 -1
- package/src/control/config-write.mjs +32 -2
- package/src/control/skill-crud.mjs +5 -0
- package/src/control/v2-guard.mjs +175 -0
- package/src/control/v2-skill-crud.mjs +32 -0
- package/src/control/v2-skill-forget.mjs +38 -0
- package/src/control/variant-status.mjs +47 -1
- package/src/control.mjs +55 -0
- package/src/doctor.mjs +71 -5
- package/src/domain/v2-policy.mjs +111 -0
- package/src/hook-plan.mjs +33 -3
- package/src/impact.mjs +52 -29
- package/src/index.mjs +25 -1
- package/src/init.mjs +50 -34
- package/src/install-health.mjs +177 -0
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +168 -19
- package/src/lifecycle-cli.mjs +40 -12
- package/src/lifecycle-content.mjs +52 -67
- package/src/migration/v1-to-v2.mjs +212 -0
- package/src/migration/v2-files.mjs +211 -0
- package/src/migration/v2-journal.mjs +169 -0
- package/src/migration/v2-projection.mjs +108 -0
- package/src/migration/v2-transaction.mjs +205 -0
- package/src/policy.mjs +3 -0
- package/src/reconcile.mjs +139 -111
- package/src/report.mjs +168 -148
- package/src/review.mjs +2 -0
- package/src/route-advisory.mjs +47 -2
- package/src/route-selection.mjs +38 -2
- package/src/route.mjs +62 -2
- package/src/shared-skill-reconcile.mjs +97 -0
- package/src/shared-skill.mjs +325 -0
- package/src/source-digest.mjs +42 -0
- package/src/source-profiles.mjs +171 -144
- package/src/source-verification.mjs +32 -48
- package/src/uninstall.mjs +22 -0
- package/src/user-state-paths.mjs +19 -0
- package/src/user-uninstall.mjs +161 -0
- package/src/workspace.mjs +119 -79
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { activeAgentSkillRootCandidates } from "./agent-skill-roots.mjs";
|
|
3
3
|
|
|
4
|
-
export async function defaultScanRoots(home, env) {
|
|
4
|
+
export async function defaultScanRoots(home, env, options = {}) {
|
|
5
5
|
const codexHome = env.CODEX_HOME ?? join(home, ".codex");
|
|
6
6
|
const agentRoots = await Promise.all([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
activeAgentSkillRootCandidates("codex", home, env, options),
|
|
8
|
+
activeAgentSkillRootCandidates("claude", home, env, options),
|
|
9
|
+
activeAgentSkillRootCandidates("opencode", home, env, options),
|
|
10
|
+
activeAgentSkillRootCandidates("hermes", home, env, options)
|
|
10
11
|
]);
|
|
11
12
|
return [
|
|
12
|
-
join(codexHome, "skills", ".system"),
|
|
13
|
-
join(codexHome, "plugins", "cache"),
|
|
14
|
-
|
|
13
|
+
{ path: join(codexHome, "skills", ".system") },
|
|
14
|
+
{ path: join(codexHome, "plugins", "cache") },
|
|
15
|
+
{ path: join(home, ".agents", "shared-skills") },
|
|
16
|
+
...agentRoots.flatMap((roots, index) => roots.map((root) => ({
|
|
17
|
+
path: root.skillRoot,
|
|
18
|
+
agent: ["codex", "claude", "opencode", "hermes"][index]
|
|
19
|
+
})))
|
|
15
20
|
];
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
export function sharedUserUnit(path, home) {
|
|
24
|
+
return userSkillUnit("shared.user-skills", path, home);
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export function systemCodexUnit(path, home) {
|
|
19
28
|
return {
|
|
20
29
|
id: "codex.system-skills",
|
|
@@ -38,6 +47,10 @@ export function userClaudeUnit(path, home) {
|
|
|
38
47
|
return userSkillUnit("claude.user-skills", path, home);
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
export function userOpenCodeUnit(path, home) {
|
|
51
|
+
return userSkillUnit("opencode.user-skills", path, home);
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function userHermesUnit(path, home) {
|
|
42
55
|
return userSkillUnit("hermes.user-skills", path, home);
|
|
43
56
|
}
|
package/src/agent-inventory.mjs
CHANGED
|
@@ -9,14 +9,25 @@ import {
|
|
|
9
9
|
hermesProfileUnit,
|
|
10
10
|
isHermesProfileSkillsPath,
|
|
11
11
|
safeSegment,
|
|
12
|
+
sharedUserUnit,
|
|
12
13
|
systemCodexUnit,
|
|
13
14
|
userClaudeUnit,
|
|
14
15
|
userCodexUnit,
|
|
15
|
-
userHermesUnit
|
|
16
|
+
userHermesUnit,
|
|
17
|
+
userOpenCodeUnit
|
|
16
18
|
} from "./agent-inventory-platforms.mjs";
|
|
17
19
|
import { readString, requireRecord } from "./config-helpers.mjs";
|
|
18
20
|
|
|
19
21
|
export const agentInventoryDetectors = Object.freeze([
|
|
22
|
+
{
|
|
23
|
+
id: "shared-user-skills",
|
|
24
|
+
matches(path) {
|
|
25
|
+
return path.endsWith("/.agents/shared-skills") || path.endsWith("\\.agents\\shared-skills");
|
|
26
|
+
},
|
|
27
|
+
async discover(path, home) {
|
|
28
|
+
return await discoverSkillDirectory(path, sharedUserUnit(path, home), { excludeSystem: true });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
20
31
|
{
|
|
21
32
|
id: "codex-plugin-cache",
|
|
22
33
|
matches(path) {
|
|
@@ -56,6 +67,16 @@ export const agentInventoryDetectors = Object.freeze([
|
|
|
56
67
|
return await discoverSkillDirectory(path, userClaudeUnit(path, home), { excludeSystem: true });
|
|
57
68
|
}
|
|
58
69
|
},
|
|
70
|
+
{
|
|
71
|
+
id: "opencode-user-skills",
|
|
72
|
+
matches(path) {
|
|
73
|
+
const normalized = path.replace(/\\/g, "/");
|
|
74
|
+
return normalized.endsWith("/.config/opencode/skills") || normalized.endsWith("/.opencode/skills");
|
|
75
|
+
},
|
|
76
|
+
async discover(path, home) {
|
|
77
|
+
return await discoverSkillDirectory(path, userOpenCodeUnit(path, home), { excludeSystem: true });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
59
80
|
{
|
|
60
81
|
id: "hermes-user-skills",
|
|
61
82
|
matches(path) {
|
|
@@ -89,16 +110,16 @@ export async function discoverAgentSkillInventory(options = {}) {
|
|
|
89
110
|
const env = options.env ?? process.env;
|
|
90
111
|
const home = options.home ?? env.HOME ?? env.USERPROFILE ?? homedir();
|
|
91
112
|
const detectors = options.detectors ?? agentInventoryDetectors;
|
|
92
|
-
const roots =
|
|
93
|
-
...(await defaultScanRoots(home, env)),
|
|
94
|
-
...readCsv(env.SKILLBOARD_INIT_SCAN_ROOTS),
|
|
95
|
-
...(options.roots ?? [])
|
|
113
|
+
const roots = uniqueRootEntries([
|
|
114
|
+
...(await defaultScanRoots(home, env, { registeredRoots: options.registeredRoots })),
|
|
115
|
+
...readCsv(env.SKILLBOARD_INIT_SCAN_ROOTS).map((path) => ({ path })),
|
|
116
|
+
...(options.roots ?? []).map((path) => ({ path }))
|
|
96
117
|
], home);
|
|
97
118
|
const groups = [];
|
|
98
119
|
const warnings = [];
|
|
99
120
|
|
|
100
121
|
for (const root of roots) {
|
|
101
|
-
const discovered = await discoverRoot(root, home, detectors);
|
|
122
|
+
const discovered = await discoverRoot(root.path, home, detectors, root.agent);
|
|
102
123
|
groups.push(...discovered.groups);
|
|
103
124
|
warnings.push(...discovered.warnings);
|
|
104
125
|
}
|
|
@@ -272,13 +293,13 @@ function ensureLocalWorkflow(workflowsMap, target, skills, document) {
|
|
|
272
293
|
return true;
|
|
273
294
|
}
|
|
274
295
|
|
|
275
|
-
async function discoverRoot(root, home, detectors) {
|
|
296
|
+
async function discoverRoot(root, home, detectors, agent) {
|
|
276
297
|
const path = resolvePath(root, home);
|
|
277
298
|
if (!(await exists(path))) {
|
|
278
299
|
return { groups: [], warnings: [] };
|
|
279
300
|
}
|
|
280
301
|
const warnings = [];
|
|
281
|
-
const
|
|
302
|
+
const matched = detectors.find((candidate) => {
|
|
282
303
|
try {
|
|
283
304
|
return candidate.matches(path);
|
|
284
305
|
} catch (error) {
|
|
@@ -286,6 +307,7 @@ async function discoverRoot(root, home, detectors) {
|
|
|
286
307
|
return false;
|
|
287
308
|
}
|
|
288
309
|
});
|
|
310
|
+
const detector = agent === undefined ? matched : forcedAgentDetector(agent, matched, detectors);
|
|
289
311
|
if (detector === undefined) {
|
|
290
312
|
return { groups: [], warnings };
|
|
291
313
|
}
|
|
@@ -298,6 +320,12 @@ async function discoverRoot(root, home, detectors) {
|
|
|
298
320
|
}
|
|
299
321
|
}
|
|
300
322
|
|
|
323
|
+
function forcedAgentDetector(agent, matched, detectors) {
|
|
324
|
+
if (matched?.id === "hermes-profile-skills" && agent === "hermes") return matched;
|
|
325
|
+
const id = `${agent}-user-skills`;
|
|
326
|
+
return detectors.find((candidate) => candidate.id === id) ?? matched;
|
|
327
|
+
}
|
|
328
|
+
|
|
301
329
|
async function discoverSkillDirectory(root, unit, options) {
|
|
302
330
|
const files = await findSkillFiles(root, root, options);
|
|
303
331
|
return files.length === 0 ? [] : [{ unit, root, files }];
|
|
@@ -416,6 +444,7 @@ async function inventoryFromGroups(groups, home, initialWarnings = []) {
|
|
|
416
444
|
const skill = {
|
|
417
445
|
id,
|
|
418
446
|
path: sourceAlias.path,
|
|
447
|
+
skillFile: resolve(file),
|
|
419
448
|
status: "quarantined",
|
|
420
449
|
invocation: "blocked",
|
|
421
450
|
exposure: "exported",
|
|
@@ -876,21 +905,20 @@ function uniqueStrings(values) {
|
|
|
876
905
|
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
|
877
906
|
}
|
|
878
907
|
|
|
879
|
-
function
|
|
880
|
-
const
|
|
881
|
-
const result = [];
|
|
908
|
+
function uniqueRootEntries(values, home) {
|
|
909
|
+
const byPath = new Map();
|
|
882
910
|
for (const value of values) {
|
|
883
|
-
const trimmed = value.trim();
|
|
911
|
+
const trimmed = value.path.trim();
|
|
884
912
|
if (trimmed.length === 0) {
|
|
885
913
|
continue;
|
|
886
914
|
}
|
|
887
915
|
const key = resolve(trimmed.startsWith("~/") ? join(home, trimmed.slice(2)) : trimmed);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
916
|
+
const existing = byPath.get(key);
|
|
917
|
+
if (existing === undefined || existing.agent === undefined && value.agent !== undefined) {
|
|
918
|
+
byPath.set(key, { path: trimmed, ...(value.agent === undefined ? {} : { agent: value.agent }) });
|
|
891
919
|
}
|
|
892
920
|
}
|
|
893
|
-
return
|
|
921
|
+
return [...byPath.values()];
|
|
894
922
|
}
|
|
895
923
|
|
|
896
924
|
function errorMessage(error) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { atomicWrite } from "./migration/v2-files.mjs";
|
|
4
|
+
|
|
5
|
+
const FORMAT_VERSION = 1;
|
|
6
|
+
const SUPPORTED_AGENTS = new Set(["codex", "claude", "opencode", "hermes"]);
|
|
7
|
+
|
|
8
|
+
export function agentRootRegistryPath(home) {
|
|
9
|
+
return join(resolve(home), ".skillboard", "agent-roots.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function loadRegisteredAgentRoots(home) {
|
|
13
|
+
const root = resolve(home);
|
|
14
|
+
const path = agentRootRegistryPath(root);
|
|
15
|
+
const stats = await lstat(path).catch(missingOnly);
|
|
16
|
+
if (stats === undefined) return [];
|
|
17
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
18
|
+
throw new Error("Agent root registry must be a regular file, not a symbolic link.");
|
|
19
|
+
}
|
|
20
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
21
|
+
if (value?.format_version !== FORMAT_VERSION || !Array.isArray(value.roots)) {
|
|
22
|
+
throw new Error("Invalid agent root registry format.");
|
|
23
|
+
}
|
|
24
|
+
const entries = normalizeEntries(value.roots, root);
|
|
25
|
+
for (const entry of entries) await assertSafeRootPath(root, entry.path);
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function proposedAgentRoot(home, agent, path, cwd = process.cwd()) {
|
|
30
|
+
assertAgent(agent);
|
|
31
|
+
const root = resolve(home);
|
|
32
|
+
const target = resolve(cwd, path);
|
|
33
|
+
const rel = relative(root, target);
|
|
34
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
35
|
+
throw new Error("Registered skill root must remain inside the invoking user's home.");
|
|
36
|
+
}
|
|
37
|
+
await assertSafeRootPath(root, target);
|
|
38
|
+
return { agent, path: target };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function writeRegisteredAgentRoots(home, entries) {
|
|
42
|
+
const root = resolve(home);
|
|
43
|
+
const normalized = normalizeEntries(entries.map((entry) => ({
|
|
44
|
+
agent: entry.agent,
|
|
45
|
+
path: portablePath(root, entry.path)
|
|
46
|
+
})), root);
|
|
47
|
+
const value = {
|
|
48
|
+
format_version: FORMAT_VERSION,
|
|
49
|
+
roots: normalized.map((entry) => ({ agent: entry.agent, path: portablePath(root, entry.path) }))
|
|
50
|
+
};
|
|
51
|
+
await atomicWrite(agentRootRegistryPath(root), Buffer.from(`${JSON.stringify(value, null, 2)}\n`));
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mergeRegisteredAgentRoots(entries, proposed) {
|
|
56
|
+
const byKey = new Map();
|
|
57
|
+
const agentByPath = new Map();
|
|
58
|
+
for (const entry of proposed === undefined ? entries : [...entries, proposed]) {
|
|
59
|
+
assertAgent(entry.agent);
|
|
60
|
+
const path = resolve(entry.path);
|
|
61
|
+
const registeredAgent = agentByPath.get(path);
|
|
62
|
+
if (registeredAgent !== undefined && registeredAgent !== entry.agent) {
|
|
63
|
+
throw new Error(`Custom skill root is already registered for agent ${registeredAgent}: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
agentByPath.set(path, entry.agent);
|
|
66
|
+
byKey.set(`${entry.agent}\0${path}`, { agent: entry.agent, path });
|
|
67
|
+
}
|
|
68
|
+
return [...byKey.values()].sort(compareEntries);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeEntries(entries, home) {
|
|
72
|
+
if (!entries.every((entry) => entry !== null && typeof entry === "object")) {
|
|
73
|
+
throw new Error("Invalid agent root registry entry.");
|
|
74
|
+
}
|
|
75
|
+
return mergeRegisteredAgentRoots(entries.map((entry) => {
|
|
76
|
+
assertAgent(entry.agent);
|
|
77
|
+
if (typeof entry.path !== "string" || entry.path.trim() === "") {
|
|
78
|
+
throw new Error("Registered skill root path must be a non-empty string.");
|
|
79
|
+
}
|
|
80
|
+
const path = isAbsolute(entry.path) ? resolve(entry.path) : resolve(home, entry.path);
|
|
81
|
+
const rel = relative(home, path);
|
|
82
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
83
|
+
throw new Error("Registered skill root must remain inside the invoking user's home.");
|
|
84
|
+
}
|
|
85
|
+
return { agent: entry.agent, path };
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function portablePath(home, path) {
|
|
90
|
+
return relative(home, resolve(path)).split("\\").join("/");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertAgent(agent) {
|
|
94
|
+
if (!SUPPORTED_AGENTS.has(agent)) {
|
|
95
|
+
throw new Error(`Unsupported setup agent: ${String(agent)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compareEntries(left, right) {
|
|
100
|
+
return `${left.agent}\0${left.path}`.localeCompare(`${right.agent}\0${right.path}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function assertSafeRootPath(home, target) {
|
|
104
|
+
const components = [];
|
|
105
|
+
let current = resolve(target);
|
|
106
|
+
while (current !== home) {
|
|
107
|
+
components.push(current);
|
|
108
|
+
const parent = resolve(current, "..");
|
|
109
|
+
if (parent === current) break;
|
|
110
|
+
current = parent;
|
|
111
|
+
}
|
|
112
|
+
for (const component of components.reverse()) {
|
|
113
|
+
const stats = await lstat(component).catch(missingOnly);
|
|
114
|
+
if (stats === undefined) continue;
|
|
115
|
+
if (stats.isSymbolicLink()) {
|
|
116
|
+
throw new Error("Registered skill root must not traverse a symbolic link.");
|
|
117
|
+
}
|
|
118
|
+
if (!stats.isDirectory()) {
|
|
119
|
+
throw new Error("Registered skill root components must be directories.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function missingOnly(error) {
|
|
125
|
+
if (error?.code === "ENOENT") return undefined;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
@@ -157,7 +157,7 @@ async function findSourceSkill(options) {
|
|
|
157
157
|
throw new Error(`Source skill not found: ${options.skill} under ${root}`);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
async function findSourceSkillInRoots(options) {
|
|
160
|
+
export async function findSourceSkillInRoots(options) {
|
|
161
161
|
const misses = [];
|
|
162
162
|
for (const root of options.roots) {
|
|
163
163
|
try {
|
|
@@ -200,7 +200,7 @@ function parseSkillFrontmatter(text) {
|
|
|
200
200
|
return parsed !== null && typeof parsed === "object" ? parsed : {};
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function analyzeAgentCompatibility(content, options) {
|
|
203
|
+
export function analyzeAgentCompatibility(content, options) {
|
|
204
204
|
const reasons = [];
|
|
205
205
|
for (const [agent, markers] of Object.entries(AGENT_MARKERS)) {
|
|
206
206
|
if (agent === options.targetAgent) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { access, readdir } from "node:fs/promises";
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { loadRegisteredAgentRoots } from "./agent-root-registry.mjs";
|
|
4
5
|
|
|
5
6
|
const SUPPORTED_AGENTS = Object.freeze(["codex", "claude", "opencode", "hermes"]);
|
|
6
7
|
|
|
@@ -9,11 +10,12 @@ export function supportedAgentNames() {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export async function detectedAgentSkillRoots(agent, home = homedir(), env = process.env, options = {}) {
|
|
12
|
-
const candidates = await
|
|
13
|
+
const candidates = await activeAgentSkillRootCandidates(agent, home, env, options);
|
|
13
14
|
const detected = [];
|
|
14
15
|
for (const candidate of candidates) {
|
|
15
16
|
if (
|
|
16
17
|
candidate.explicit
|
|
18
|
+
|| candidate.registered
|
|
17
19
|
|| await exists(candidate.skillRoot)
|
|
18
20
|
|| candidate.detectWhenExists !== undefined && await exists(candidate.detectWhenExists)
|
|
19
21
|
) {
|
|
@@ -27,7 +29,7 @@ export async function detectedAgentSkillRoots(agent, home = homedir(), env = pro
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export async function setupAgentSkillTargets(agent, home = homedir(), env = process.env, options = {}) {
|
|
30
|
-
const roots = await detectedAgentSkillRoots(agent, home, env,
|
|
32
|
+
const roots = await detectedAgentSkillRoots(agent, home, env, options);
|
|
31
33
|
return roots.map((root) => ({
|
|
32
34
|
agent,
|
|
33
35
|
home: resolve(home),
|
|
@@ -42,38 +44,61 @@ export async function preferredAgentSkillRoot(agent, home = homedir(), env = pro
|
|
|
42
44
|
return root.skillRoot;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
export async function
|
|
47
|
+
export async function activeAgentSkillRootCandidates(agent, home = homedir(), env = process.env, options = {}) {
|
|
48
|
+
const candidates = await agentSkillRootCandidates(agent, home, env, options);
|
|
49
|
+
const hasRegistered = candidates.some((entry) => entry.registered);
|
|
50
|
+
const hasExplicit = candidates.some((entry) => entry.explicit);
|
|
51
|
+
if (!hasRegistered && !hasExplicit) return candidates;
|
|
52
|
+
const active = await Promise.all(candidates.map(async (entry) => ({
|
|
53
|
+
entry,
|
|
54
|
+
keep: entry.registered
|
|
55
|
+
|| entry.explicit
|
|
56
|
+
|| entry.profile
|
|
57
|
+
|| entry.sharedCodex
|
|
58
|
+
|| await hasAgentOwnedSkillContent(entry.skillRoot)
|
|
59
|
+
})));
|
|
60
|
+
return active.filter(({ keep }) => keep).map(({ entry }) => entry);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function agentSkillRootCandidates(agent, home = homedir(), env = process.env, options = {}) {
|
|
46
64
|
const normalized = normalizeAgent(agent);
|
|
47
65
|
const xdgConfig = env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
66
|
+
const registered = (options.registeredRoots ?? await loadRegisteredAgentRoots(home))
|
|
67
|
+
.filter((entry) => entry.agent === normalized)
|
|
68
|
+
.map((entry) => candidate(entry.path, "registered custom skill root", false, false, undefined, { registered: true }));
|
|
48
69
|
if (normalized === "codex") {
|
|
49
70
|
const agentsHome = join(home, ".agents");
|
|
50
71
|
return uniqueCandidates([
|
|
72
|
+
...registered,
|
|
51
73
|
env.CODEX_HOME === undefined ? null : candidate(join(env.CODEX_HOME, "skills"), "CODEX_HOME/skills", true, false),
|
|
52
74
|
env.AGENTS_HOME === undefined ? null : candidate(join(env.AGENTS_HOME, "skills"), "AGENTS_HOME/skills", true, false),
|
|
53
|
-
candidate(join(agentsHome, "skills"), "~/.agents/skills", false, false, agentsHome),
|
|
54
|
-
candidate(join(home, ".codex", "skills"), "~/.codex/skills", false, true)
|
|
75
|
+
candidate(join(agentsHome, "skills"), "~/.agents/skills", false, false, agentsHome, { sharedCodex: true }),
|
|
76
|
+
candidate(join(home, ".codex", "skills"), "~/.codex/skills", false, true, join(home, ".codex"))
|
|
55
77
|
]);
|
|
56
78
|
}
|
|
57
79
|
if (normalized === "claude") {
|
|
58
80
|
return uniqueCandidates([
|
|
81
|
+
...registered,
|
|
59
82
|
env.CLAUDE_HOME === undefined ? null : candidate(join(env.CLAUDE_HOME, "skills"), "CLAUDE_HOME/skills", true, false),
|
|
60
|
-
candidate(join(home, ".claude", "skills"), "~/.claude/skills", false, true),
|
|
83
|
+
candidate(join(home, ".claude", "skills"), "~/.claude/skills", false, true, join(home, ".claude")),
|
|
61
84
|
candidate(join(xdgConfig, "claude", "skills"), "XDG claude skills", false, false),
|
|
62
85
|
candidate(join(home, ".config", "claude", "skills"), "~/.config/claude/skills", false, false)
|
|
63
86
|
]);
|
|
64
87
|
}
|
|
65
88
|
if (normalized === "opencode") {
|
|
66
89
|
return uniqueCandidates([
|
|
90
|
+
...registered,
|
|
67
91
|
env.OPENCODE_HOME === undefined ? null : candidate(join(env.OPENCODE_HOME, "skills"), "OPENCODE_HOME/skills", true, false),
|
|
68
|
-
candidate(join(xdgConfig, "opencode", "skills"), "XDG opencode skills", false, xdgConfig !== join(home, ".config")),
|
|
69
|
-
candidate(join(home, ".config", "opencode", "skills"), "~/.config/opencode/skills", false, true),
|
|
92
|
+
candidate(join(xdgConfig, "opencode", "skills"), "XDG opencode skills", false, xdgConfig !== join(home, ".config"), join(xdgConfig, "opencode")),
|
|
93
|
+
candidate(join(home, ".config", "opencode", "skills"), "~/.config/opencode/skills", false, true, join(home, ".config", "opencode")),
|
|
70
94
|
candidate(join(home, ".opencode", "skills"), "~/.opencode/skills", false, false)
|
|
71
95
|
]);
|
|
72
96
|
}
|
|
73
97
|
const hermesHome = env.HERMES_HOME ?? join(home, ".hermes");
|
|
74
98
|
return uniqueCandidates([
|
|
99
|
+
...registered,
|
|
75
100
|
env.HERMES_HOME === undefined ? null : candidate(join(env.HERMES_HOME, "skills"), "HERMES_HOME/skills", true, false),
|
|
76
|
-
candidate(join(home, ".hermes", "skills"), "~/.hermes/skills", false, true),
|
|
101
|
+
candidate(join(home, ".hermes", "skills"), "~/.hermes/skills", false, true, join(home, ".hermes")),
|
|
77
102
|
...(await hermesProfileCandidates(hermesHome))
|
|
78
103
|
]);
|
|
79
104
|
}
|
|
@@ -85,8 +110,8 @@ function normalizeAgent(agent) {
|
|
|
85
110
|
return agent;
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
function candidate(skillRoot, source, explicit, fallback, detectWhenExists) {
|
|
89
|
-
return { skillRoot, source, explicit, fallback, detectWhenExists };
|
|
113
|
+
function candidate(skillRoot, source, explicit, fallback, detectWhenExists, metadata = {}) {
|
|
114
|
+
return { skillRoot, source, explicit, fallback, detectWhenExists, ...metadata };
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
function fallbackCandidate(candidates) {
|
|
@@ -112,9 +137,41 @@ async function hermesProfileCandidates(hermesHome) {
|
|
|
112
137
|
const entries = await readdir(profilesRoot, { withFileTypes: true }).catch(() => []);
|
|
113
138
|
return entries
|
|
114
139
|
.filter((entry) => entry.isDirectory())
|
|
115
|
-
.map((entry) => candidate(
|
|
140
|
+
.map((entry) => candidate(
|
|
141
|
+
join(profilesRoot, entry.name, "skills"),
|
|
142
|
+
`Hermes profile ${entry.name}`,
|
|
143
|
+
false,
|
|
144
|
+
false,
|
|
145
|
+
join(profilesRoot, entry.name),
|
|
146
|
+
{ profile: true }
|
|
147
|
+
));
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
async function exists(path) {
|
|
119
151
|
return access(path).then(() => true, () => false);
|
|
120
152
|
}
|
|
153
|
+
|
|
154
|
+
async function hasAgentOwnedSkillContent(skillRoot) {
|
|
155
|
+
const entries = await readdir(skillRoot, { withFileTypes: true }).catch((error) => {
|
|
156
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return [];
|
|
157
|
+
throw error;
|
|
158
|
+
});
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (entry.name === "skillboard" || entry.name === ".system") continue;
|
|
161
|
+
if (!entry.isDirectory()) return true;
|
|
162
|
+
const metadata = await readFile(join(skillRoot, entry.name, ".skillboard-share.json"), "utf8")
|
|
163
|
+
.then((text) => {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(text);
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}, (error) => {
|
|
170
|
+
if (error?.code === "ENOENT") return null;
|
|
171
|
+
throw error;
|
|
172
|
+
});
|
|
173
|
+
if (metadata?.version === 1 && metadata.managed_by === "skillboard") continue;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function expandPortablePath(value, paths) {
|
|
5
|
+
if (value === "${PROJECT}") return paths.rootDir;
|
|
6
|
+
if (value.startsWith("${PROJECT}/")) return resolve(paths.rootDir, value.slice("${PROJECT}/".length));
|
|
7
|
+
if (value === "${HOME}") return homedir();
|
|
8
|
+
if (value.startsWith("${HOME}/")) return resolve(homedir(), value.slice("${HOME}/".length));
|
|
9
|
+
if (value === "<external>" || value.startsWith("<external>/")) return null;
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function portableObservation(value, options) {
|
|
14
|
+
if (value.startsWith("${PROJECT}") || value.startsWith("${HOME}") || value.startsWith("<external>")) return value;
|
|
15
|
+
if (value.startsWith("~/")) return `\${HOME}/${value.slice(2)}`;
|
|
16
|
+
return isAbsolute(value) ? auditPath(value, options) : value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function auditPath(path, options) {
|
|
20
|
+
const project = relative(options.rootDir, path);
|
|
21
|
+
if (!project.startsWith("..") && !isAbsolute(project)) {
|
|
22
|
+
return project === "" ? "${PROJECT}" : `\${PROJECT}/${project.replace(/\\/g, "/")}`;
|
|
23
|
+
}
|
|
24
|
+
const home = relative(homedir(), path);
|
|
25
|
+
if (!home.startsWith("..") && !isAbsolute(home)) {
|
|
26
|
+
return home === "" ? "${HOME}" : `\${HOME}/${home.replace(/\\/g, "/")}`;
|
|
27
|
+
}
|
|
28
|
+
return `<external>/${basename(path)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function redactPathError(error, options) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return replaceText(
|
|
34
|
+
replaceText(replaceText(message, options.rootDir, "${PROJECT}"), options.configDir, "${PROJECT}"),
|
|
35
|
+
homedir(),
|
|
36
|
+
"${HOME}"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function replaceText(value, search, replacement) {
|
|
41
|
+
return value.split(search).join(replacement);
|
|
42
|
+
}
|
package/src/brief-cli.mjs
CHANGED
|
@@ -5,11 +5,12 @@ import { renderSkillBrief } from "./brief-renderer.mjs";
|
|
|
5
5
|
export async function runBriefCommand(options, stdout, paths) {
|
|
6
6
|
const json = options.get("json") === "true";
|
|
7
7
|
const result = await buildSkillBrief({
|
|
8
|
-
root: briefRoot(options),
|
|
8
|
+
root: briefRoot(options) ?? dirname(paths.configPath),
|
|
9
9
|
configPath: paths.configPath,
|
|
10
10
|
skillsRoot: paths.skillsRoot,
|
|
11
11
|
workflow: options.get("workflow"),
|
|
12
12
|
intent: options.get("intent"),
|
|
13
|
+
agent: options.get("agent"),
|
|
13
14
|
includeActions: options.get("include-actions") === "true" || !json
|
|
14
15
|
});
|
|
15
16
|
writeBriefOutput(stdout, result, options);
|
|
@@ -22,7 +23,7 @@ function briefRoot(options) {
|
|
|
22
23
|
return dir;
|
|
23
24
|
}
|
|
24
25
|
const config = options.get("config");
|
|
25
|
-
return config !== undefined && isAbsolute(config) ? dirname(config) :
|
|
26
|
+
return config !== undefined && isAbsolute(config) ? dirname(config) : undefined;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function briefExitCode(result) {
|
package/src/brief-renderer.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export function renderSkillBrief(brief, options = {}) {
|
|
|
47
47
|
`Workflow: ${workflowSummary(brief.workflow)}`,
|
|
48
48
|
""
|
|
49
49
|
];
|
|
50
|
+
if (brief.compatibility?.notice !== undefined) lines.push(brief.compatibility.notice, "");
|
|
50
51
|
if (brief.error !== undefined) {
|
|
51
52
|
lines.push(`Error: ${safeText(brief.error.message)}`, "");
|
|
52
53
|
}
|