skillspub 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.
package/dist/core.js ADDED
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ function lexists(p) {
5
+ try {
6
+ fs.lstatSync(p);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export function migrateLegacyConfig(configDir, legacyDir) {
14
+ if (configDir === legacyDir || !fs.existsSync(legacyDir))
15
+ return;
16
+ fs.mkdirSync(configDir, { recursive: true });
17
+ for (const entry of fs.readdirSync(legacyDir)) {
18
+ const destination = path.join(configDir, entry);
19
+ if (!lexists(destination))
20
+ fs.cpSync(path.join(legacyDir, entry), destination, { recursive: true });
21
+ }
22
+ }
23
+ export function defaultHome(options = {}) {
24
+ const configDir = process.env.SKILLSPUB_CONFIG_DIR ??
25
+ path.join(os.homedir(), '.config', 'skillspub');
26
+ // Migration-only: legacy data is copied into canonical config, never used directly.
27
+ if (options.migrate !== false &&
28
+ (process.env.SKM_CONFIG_DIR || !process.env.SKILLSPUB_CONFIG_DIR))
29
+ migrateLegacyConfig(configDir, process.env.SKM_CONFIG_DIR ?? path.join(os.homedir(), '.config', 'skm'));
30
+ return { configDir };
31
+ }
@@ -0,0 +1,307 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadTargets, normalizeSlotName, scanGlobalInventory, scanProjectInventory, readStateFile, } from "./inventory.js";
4
+ import { inspectHarnesses } from "./harnesses/registry.js";
5
+ export class ExplainError extends Error {
6
+ code;
7
+ details;
8
+ constructor(code, message, details = {}) {
9
+ super(message);
10
+ this.code = code;
11
+ this.details = details;
12
+ }
13
+ }
14
+ function resolveResource(report, selector) {
15
+ const matches = selector.startsWith('skill:')
16
+ ? report.resources.filter(({ realPath }) => realPath === selector.slice('skill:'.length))
17
+ : report.resources.filter(({ name }) => name === selector);
18
+ if (matches.length === 0)
19
+ throw new ExplainError('installed_resource_not_found', `installed Skill resource not found: ${selector}`, { selector });
20
+ if (matches.length > 1) {
21
+ const variants = matches.map(({ id, name, realPath }) => ({
22
+ name,
23
+ resourceId: id,
24
+ realPath,
25
+ selector: `skill:${realPath}`,
26
+ }));
27
+ throw new ExplainError('ambiguous_selector', `skill name "${selector}" is ambiguous; use an explicit skill:<realPath> selector`, { name: selector, matches: variants });
28
+ }
29
+ return matches[0];
30
+ }
31
+ function canonicalPath(value) {
32
+ try {
33
+ return fs.realpathSync(value);
34
+ }
35
+ catch {
36
+ return path.resolve(value);
37
+ }
38
+ }
39
+ function directRelationship(root, resource) {
40
+ const entry = path.join(root, normalizeSlotName(resource.name));
41
+ try {
42
+ const stat = fs.lstatSync(entry);
43
+ const realPath = fs.realpathSync(entry);
44
+ if (!fs.existsSync(path.join(realPath, 'SKILL.md')))
45
+ return { relationships: [] };
46
+ return { relationships: [{
47
+ resourceId: realPath,
48
+ realPath,
49
+ slot: normalizeSlotName(resource.name),
50
+ activation: 'on',
51
+ form: stat.isSymbolicLink() ? 'link' : 'local',
52
+ path: entry,
53
+ selected: realPath === resource.realPath,
54
+ }] };
55
+ }
56
+ catch (error) {
57
+ if (error.code === 'ENOENT')
58
+ return { relationships: [] };
59
+ return { relationships: [], error: error.message };
60
+ }
61
+ }
62
+ function explainRoots(report, resource, harness) {
63
+ return harness.roots.map((root) => {
64
+ const target = report.targets.find((candidate) => candidate.key === root.targetKey && candidate.scope === root.scope &&
65
+ canonicalPath(candidate.discoveryRoot) === canonicalPath(root.discoveryRoot));
66
+ const direct = target ? undefined : directRelationship(root.discoveryRoot, resource);
67
+ const relationships = target
68
+ ? report.relationships
69
+ .filter(({ targetId, slot }) => targetId === target.id && slot === normalizeSlotName(resource.name))
70
+ .map((relationship) => ({
71
+ resourceId: relationship.resourceId,
72
+ realPath: relationship.realPath,
73
+ slot: relationship.slot,
74
+ activation: relationship.activation,
75
+ form: relationship.form,
76
+ path: relationship.path,
77
+ selected: relationship.resourceId === resource.id,
78
+ }))
79
+ : direct?.relationships ?? [];
80
+ return {
81
+ id: target?.id ?? `external:${harness.key}:${root.targetKey}:${root.scope}:${root.discoveryRoot}`,
82
+ targetKey: root.targetKey,
83
+ scope: root.scope,
84
+ kind: root.kind,
85
+ path: root.discoveryRoot,
86
+ consumption: direct?.error ? 'unknown' : root.consumption,
87
+ reason: direct?.error ? `${root.reason} Inspection failed: ${direct.error}` : root.reason,
88
+ relationships,
89
+ };
90
+ });
91
+ }
92
+ function stateFileForTarget(home, report, targetId) {
93
+ const target = report.targets.find(({ id }) => id === targetId);
94
+ if (!target || target.scope === 'global')
95
+ return path.join(home.configDir, 'state.json');
96
+ if (target.scope === 'project')
97
+ return report.stateFile;
98
+ return path.join(target.sourceDirectory ?? '', '.skillspub', 'state.json');
99
+ }
100
+ function presetClaimStatus(home, report, targetId, slot) {
101
+ const claims = readStateFile(stateFileForTarget(home, report, targetId)).claims;
102
+ if (claims === undefined)
103
+ return 'unclaimed';
104
+ if (!claims || typeof claims !== 'object' || Array.isArray(claims))
105
+ return 'unknown';
106
+ const value = claims[`${targetId}\0${slot}`];
107
+ if (value === undefined)
108
+ return 'unclaimed';
109
+ if (!Array.isArray(value) || value.some((claim) => typeof claim !== 'string'))
110
+ return 'unknown';
111
+ return value.length > 0 ? 'claimed' : 'unclaimed';
112
+ }
113
+ function planVisibility(home, report, resource, harness, explanation, wanted) {
114
+ const blockers = [];
115
+ if (!harness.detected)
116
+ blockers.push({ code: 'harness_not_detected', message: `${harness.name} is not detected.` });
117
+ if (harness.support !== 'managed')
118
+ blockers.push({ code: 'support_incomplete', message: `${harness.name} support is not managed.` });
119
+ if (explanation.effectiveVisibility === 'unknown')
120
+ blockers.push({ code: 'visibility_unknown', message: 'Effective visibility is unknown; no safe plan can be produced.' });
121
+ if (explanation.conflicts.length > 0)
122
+ blockers.push({ code: 'unresolved_variant', message: 'Resolve same-name Variants before changing visibility.' });
123
+ if (blockers.length > 0)
124
+ return { executable: false, steps: [], blockers };
125
+ if (wanted === 'visible') {
126
+ if (explanation.effectiveVisibility === 'visible')
127
+ return { executable: true, steps: [], blockers: [] };
128
+ const target = report.targets.find((candidate) => candidate.key === harness.key && candidate.writable &&
129
+ (report.scope === 'global' ? candidate.scope === 'global' : candidate.scope === 'project'));
130
+ if (!target)
131
+ return {
132
+ executable: false,
133
+ steps: [],
134
+ blockers: [{ code: 'no_writable_target', message: `No writable ${harness.name} Target exists in this scope.` }],
135
+ };
136
+ const slotName = normalizeSlotName(resource.name);
137
+ const relationships = report.relationships.filter(({ targetId, slot }) => targetId === target.id && slot === slotName);
138
+ const selected = relationships.find(({ resourceId }) => resourceId === resource.id);
139
+ if (!selected && relationships.length > 0)
140
+ return {
141
+ executable: false,
142
+ steps: [],
143
+ blockers: [{ code: 'target_slot_occupied', message: `${target.id}/${resource.name} is occupied by another Variant.` }],
144
+ };
145
+ const form = harness.link.supported ? 'link' : harness.mirror?.supported ? 'mirror' : undefined;
146
+ if (!selected && !form)
147
+ return {
148
+ executable: false,
149
+ steps: [],
150
+ blockers: [{ code: 'unsupported_resource_form', message: `${harness.name} cannot safely create this Relationship.` }],
151
+ };
152
+ let operation = 'activate';
153
+ if (!selected)
154
+ operation = form === 'mirror' ? 'create-mirror' : 'create-link';
155
+ const from = selected?.activation ?? 'missing';
156
+ const step = {
157
+ operation,
158
+ targetId: target.id,
159
+ targetKey: target.key,
160
+ slot: slotName,
161
+ from,
162
+ to: 'on',
163
+ preconditions: [
164
+ { code: 'resource_identity', message: `Resource must remain ${resource.realPath}.` },
165
+ { code: 'target_slot_state', message: `${target.id}/${slotName} must remain ${from}.` },
166
+ ],
167
+ };
168
+ const stepForm = selected?.form ?? form;
169
+ if (stepForm)
170
+ step.form = stepForm;
171
+ if (selected)
172
+ step.path = selected.path;
173
+ return { executable: true, steps: [step], blockers: [] };
174
+ }
175
+ if (explanation.effectiveVisibility === 'not-visible')
176
+ return { executable: true, steps: [], blockers: [] };
177
+ const steps = [];
178
+ for (const root of explanation.roots.filter(({ consumption }) => consumption === 'consumed')) {
179
+ for (const relationship of root.relationships.filter(({ selected, activation }) => selected && activation === 'on')) {
180
+ if (root.kind !== 'harness') {
181
+ blockers.push({
182
+ code: 'cross_harness_side_effect',
183
+ message: `Hiding ${root.path}/${relationship.slot} would affect other consumers of this ${root.kind} root.`,
184
+ });
185
+ continue;
186
+ }
187
+ const target = report.targets.find(({ id }) => id === root.id);
188
+ if (!target?.writable) {
189
+ blockers.push({
190
+ code: 'read_only_contributing_root',
191
+ message: `${root.path} is inherited and read-only in this scope.`,
192
+ });
193
+ continue;
194
+ }
195
+ const claimStatus = presetClaimStatus(home, report, root.id, relationship.slot);
196
+ if (claimStatus === 'unknown') {
197
+ blockers.push({
198
+ code: 'unknown_preset_claims',
199
+ message: `Preset claims for ${root.id}/${relationship.slot} cannot be confirmed.`,
200
+ });
201
+ continue;
202
+ }
203
+ if (claimStatus === 'claimed') {
204
+ blockers.push({
205
+ code: 'active_preset_claim',
206
+ message: `An active Preset claims ${root.id}/${relationship.slot} ON.`,
207
+ });
208
+ continue;
209
+ }
210
+ steps.push({
211
+ operation: 'deactivate',
212
+ targetId: root.id,
213
+ targetKey: root.targetKey,
214
+ slot: relationship.slot,
215
+ from: 'on',
216
+ to: 'off',
217
+ form: relationship.form,
218
+ path: relationship.path,
219
+ preconditions: [
220
+ { code: 'resource_identity', message: `Resource must remain ${resource.realPath}.` },
221
+ { code: 'target_slot_state', message: `${root.id}/${relationship.slot} must remain on.` },
222
+ { code: 'preset_claims_absent', message: `${root.id}/${relationship.slot} must remain free of Preset claims.` },
223
+ ],
224
+ });
225
+ }
226
+ }
227
+ return { executable: blockers.length === 0, steps: blockers.length === 0 ? steps : [], blockers };
228
+ }
229
+ function explainHarness(home, report, resource, harness, wanted) {
230
+ const roots = explainRoots(report, resource, harness);
231
+ const consumed = roots.filter(({ consumption }) => consumption === 'consumed');
232
+ const unknownRoots = roots.filter(({ consumption }) => consumption === 'unknown');
233
+ const conflicts = consumed.flatMap((root) => root.relationships
234
+ .filter((relationship) => relationship.slot === normalizeSlotName(resource.name) &&
235
+ relationship.resourceId !== resource.id && relationship.activation === 'on')
236
+ .map((relationship) => ({
237
+ code: 'same_name_variant',
238
+ message: `${relationship.realPath} competes in consumed root ${root.path}.`,
239
+ resourceId: relationship.resourceId ?? relationship.realPath ?? relationship.path,
240
+ realPath: relationship.realPath ?? relationship.path,
241
+ })));
242
+ const selectedRoots = consumed.filter((root) => root.relationships.some((relationship) => relationship.selected && relationship.activation === 'on'));
243
+ let effectiveVisibility;
244
+ if (!harness.detected || harness.support !== 'managed')
245
+ effectiveVisibility = 'unknown';
246
+ else if (conflicts.length > 0)
247
+ effectiveVisibility = 'conflicted';
248
+ else if (unknownRoots.length > 0)
249
+ effectiveVisibility = 'unknown';
250
+ else
251
+ effectiveVisibility = selectedRoots.length > 0 ? 'visible' : 'not-visible';
252
+ const reasons = [];
253
+ if (!harness.detected)
254
+ reasons.push({ code: 'harness_not_detected', message: `${harness.name} was not detected locally.` });
255
+ if (harness.support !== 'managed')
256
+ reasons.push({ code: 'support_incomplete', message: `${harness.name} support is ${harness.support}.` });
257
+ for (const root of selectedRoots)
258
+ reasons.push({ code: 'relationship_consumed', message: `Selected resource is ON in ${root.path}.` });
259
+ for (const root of unknownRoots)
260
+ reasons.push({ code: 'root_unknown', message: `Consumption cannot be confirmed for ${root.path}.` });
261
+ if (effectiveVisibility === 'not-visible')
262
+ reasons.push({ code: 'no_consumed_relationship', message: 'No consumed root has an ON Relationship to the selected resource.' });
263
+ const explanation = {
264
+ key: harness.key,
265
+ name: harness.name,
266
+ detected: harness.detected,
267
+ support: harness.support,
268
+ evidence: harness.evidence,
269
+ sharedConsumption: harness.sharedConsumption,
270
+ isolation: harness.isolation,
271
+ effectiveVisibility,
272
+ roots,
273
+ reasons,
274
+ warnings: harness.detected ? [{
275
+ code: 'local_version_unknown',
276
+ message: `Local ${harness.name} version was not confirmed; discovery semantics use the Adapter's verified evidence.`,
277
+ }] : [],
278
+ conflicts,
279
+ };
280
+ if (wanted)
281
+ explanation.plan = planVisibility(home, report, resource, harness, explanation, wanted);
282
+ return explanation;
283
+ }
284
+ export function explainVisibilityFromInventory(home, report, selector, options = {}, inspected = inspectHarnesses(home, report.targets, report.projectPath)) {
285
+ const resource = resolveResource(report, selector);
286
+ let harnesses = [...inspected.detected, ...inspected.available];
287
+ if (options.harness) {
288
+ harnesses = harnesses.filter(({ key }) => key === options.harness);
289
+ if (harnesses.length === 0)
290
+ throw new ExplainError('unknown_harness', `unknown Harness: ${options.harness}`, {
291
+ harness: options.harness,
292
+ });
293
+ }
294
+ return {
295
+ resource: { id: resource.id, name: resource.name, realPath: resource.realPath },
296
+ scope: { kind: report.scope, ...(report.projectPath ? { projectPath: report.projectPath } : {}) },
297
+ ...(options.want ? { wanted: options.want } : {}),
298
+ harnesses: harnesses.map((harness) => explainHarness(home, report, resource, harness, options.want)),
299
+ };
300
+ }
301
+ export function explainVisibility(home, selector, options = {}) {
302
+ const targets = loadTargets(home);
303
+ const report = options.projectPath
304
+ ? scanProjectInventory(home, options.projectPath, targets, { persist: false })
305
+ : scanGlobalInventory(home, targets, { persist: false });
306
+ return explainVisibilityFromInventory(home, report, selector, options);
307
+ }
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { resolveHarnessTarget } from "./target.js";
5
+ const EVIDENCE = [
6
+ {
7
+ url: 'https://docs.anthropic.com/en/docs/claude-code/skills',
8
+ verifiedVersion: 'docs-2026-08-12',
9
+ detail: 'Claude Code discovers personal and project skills in .claude/skills and supports symlinks.',
10
+ },
11
+ {
12
+ url: 'https://docs.anthropic.com/en/docs/claude-code/settings',
13
+ verifiedVersion: 'docs-2026-08-12',
14
+ detail: 'Claude Code settings do not add the Shared Agent Skills directory.',
15
+ },
16
+ ];
17
+ export const claudeAdapter = {
18
+ key: 'claude',
19
+ name: 'Claude Code',
20
+ targetDefinition() {
21
+ const root = path.join(os.homedir(), '.claude');
22
+ return {
23
+ key: 'claude',
24
+ kind: 'harness',
25
+ discoveryRoot: path.join(root, 'skills'),
26
+ parkingRoot: path.join(root, '.skillspub-off', 'skills'),
27
+ projectPath: '.claude/skills',
28
+ relationship: { support: 'managed', link: 'supported' },
29
+ };
30
+ },
31
+ inspect(_home, targets, projectPath) {
32
+ const claudeTarget = resolveHarnessTarget(targets, 'claude', () => claudeAdapter.targetDefinition());
33
+ const sharedTarget = targets.find(({ key }) => key === 'shared');
34
+ const projectRoot = projectPath ? path.resolve(projectPath) : undefined;
35
+ const detected = fs.existsSync(claudeTarget.discoveryRoot) ||
36
+ fs.existsSync(path.dirname(claudeTarget.discoveryRoot)) ||
37
+ Boolean(projectRoot && fs.existsSync(path.join(projectRoot, '.claude')));
38
+ return {
39
+ key: 'claude',
40
+ name: 'Claude Code',
41
+ detected,
42
+ support: 'managed',
43
+ evidence: EVIDENCE,
44
+ targets: [
45
+ { scope: 'global', discoveryRoot: claudeTarget.discoveryRoot },
46
+ ...(projectRoot ? [{
47
+ scope: 'project',
48
+ discoveryRoot: path.join(projectRoot, claudeTarget.projectPath),
49
+ }] : []),
50
+ ],
51
+ roots: [
52
+ {
53
+ kind: 'harness',
54
+ targetKey: 'claude',
55
+ scope: 'global',
56
+ discoveryRoot: claudeTarget.discoveryRoot,
57
+ consumption: 'consumed',
58
+ reason: 'Claude Code discovers its Global Skill Target.',
59
+ },
60
+ ...(projectRoot ? [{
61
+ kind: 'harness',
62
+ targetKey: 'claude',
63
+ scope: 'project',
64
+ discoveryRoot: path.join(projectRoot, claudeTarget.projectPath),
65
+ consumption: 'consumed',
66
+ reason: 'Claude Code discovers the exact Project Skill Target.',
67
+ }] : []),
68
+ ...(sharedTarget ? [{
69
+ kind: 'shared',
70
+ targetKey: 'shared',
71
+ scope: 'global',
72
+ discoveryRoot: sharedTarget.discoveryRoot,
73
+ consumption: 'excluded',
74
+ reason: 'Claude Code does not consume the Shared Agent Skills root.',
75
+ }, ...(projectRoot ? [{
76
+ kind: 'shared',
77
+ targetKey: 'shared',
78
+ scope: 'project',
79
+ discoveryRoot: path.join(projectRoot, sharedTarget.projectPath),
80
+ consumption: 'excluded',
81
+ reason: 'Claude Code does not consume the Project Shared Agent Skills root.',
82
+ }] : [])] : []),
83
+ ],
84
+ sharedConsumption: {
85
+ status: 'not-consumed',
86
+ detail: 'Claude Code does not discover the Shared Agent Skills directory.',
87
+ },
88
+ isolation: {
89
+ status: 'not-required',
90
+ detail: 'No setup or Harness configuration write is required for independent visibility.',
91
+ },
92
+ link: { supported: true },
93
+ };
94
+ },
95
+ };