@klhapp/skillmux 1.11.2 → 1.13.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/CHANGELOG.md +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/skill-management.md +1 -1
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/init-instructions.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
lstatSync,
|
|
3
|
-
mkdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
renameSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
statSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
11
|
-
import { DISCOVERY_PARAGRAPH } from "./init";
|
|
12
|
-
import { getAgentDefinition, type AgentId } from "./init-agents";
|
|
13
|
-
|
|
14
|
-
export const INSTRUCTION_BLOCK_START = "<!-- skillmux:discovery:start -->";
|
|
15
|
-
export const INSTRUCTION_BLOCK_END = "<!-- skillmux:discovery:end -->";
|
|
16
|
-
|
|
17
|
-
const MANAGED_BLOCK = [
|
|
18
|
-
INSTRUCTION_BLOCK_START,
|
|
19
|
-
DISCOVERY_PARAGRAPH,
|
|
20
|
-
INSTRUCTION_BLOCK_END,
|
|
21
|
-
].join("\n");
|
|
22
|
-
|
|
23
|
-
export interface InstructionChange {
|
|
24
|
-
path: string;
|
|
25
|
-
agents: AgentId[];
|
|
26
|
-
status: "create" | "update" | "unchanged";
|
|
27
|
-
before: string | null;
|
|
28
|
-
after: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface InstructionPlan {
|
|
32
|
-
changes: InstructionChange[];
|
|
33
|
-
manual: Array<{ agent: AgentId; reason: string }>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface InstructionPlanOptions {
|
|
37
|
-
home?: string;
|
|
38
|
-
codexHome?: string;
|
|
39
|
-
claudeConfigDir?: string;
|
|
40
|
-
readFile?: (path: string) => string | null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function instructionPath(
|
|
44
|
-
agent: AgentId,
|
|
45
|
-
options: Required<Pick<InstructionPlanOptions, "home" | "codexHome" | "claudeConfigDir">>,
|
|
46
|
-
): string | undefined {
|
|
47
|
-
return getAgentDefinition(agent).instructions?.global?.(options);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function readInstructionFile(path: string): string | null {
|
|
51
|
-
try {
|
|
52
|
-
const stat = lstatSync(path);
|
|
53
|
-
if (stat.isSymbolicLink()) {
|
|
54
|
-
throw new Error(`instruction file is a symbolic link and will not be modified: ${path}`);
|
|
55
|
-
}
|
|
56
|
-
if (!stat.isFile()) throw new Error(`instruction path is not a file: ${path}`);
|
|
57
|
-
return readFileSync(path, "utf8");
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function withManagedBlock(existing: string | null, path: string): string {
|
|
65
|
-
if (existing === null || existing.length === 0) return `${MANAGED_BLOCK}\n`;
|
|
66
|
-
|
|
67
|
-
const starts = existing.split(INSTRUCTION_BLOCK_START).length - 1;
|
|
68
|
-
const ends = existing.split(INSTRUCTION_BLOCK_END).length - 1;
|
|
69
|
-
if (starts === 0 && ends === 0) return `${existing.trimEnd()}\n\n${MANAGED_BLOCK}\n`;
|
|
70
|
-
if (starts !== 1 || ends !== 1) {
|
|
71
|
-
throw new Error(`instruction file has a malformed skillmux managed block: ${path}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const start = existing.indexOf(INSTRUCTION_BLOCK_START);
|
|
75
|
-
const end = existing.indexOf(INSTRUCTION_BLOCK_END, start);
|
|
76
|
-
if (end < start) throw new Error(`instruction file has a malformed skillmux managed block: ${path}`);
|
|
77
|
-
return `${existing.slice(0, start)}${MANAGED_BLOCK}${existing.slice(end + INSTRUCTION_BLOCK_END.length)}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function planInstructionSetupWithResolver(
|
|
81
|
-
requestedAgents: readonly AgentId[],
|
|
82
|
-
resolvePath: (agent: AgentId) => string | undefined,
|
|
83
|
-
readFile: (path: string) => string | null,
|
|
84
|
-
): InstructionPlan {
|
|
85
|
-
const changesByPath = new Map<string, InstructionChange>();
|
|
86
|
-
const manual: InstructionPlan["manual"] = [];
|
|
87
|
-
|
|
88
|
-
for (const agent of [...new Set(requestedAgents)]) {
|
|
89
|
-
const path = resolvePath(agent);
|
|
90
|
-
if (!path) {
|
|
91
|
-
manual.push({ agent, reason: "no safe durable user instruction file is known" });
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const existingChange = changesByPath.get(path);
|
|
95
|
-
if (existingChange) {
|
|
96
|
-
existingChange.agents.push(agent);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const before = readFile(path);
|
|
101
|
-
const after = withManagedBlock(before, path);
|
|
102
|
-
changesByPath.set(path, {
|
|
103
|
-
path,
|
|
104
|
-
agents: [agent],
|
|
105
|
-
status: before === null ? "create" : before === after ? "unchanged" : "update",
|
|
106
|
-
before,
|
|
107
|
-
after,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { changes: [...changesByPath.values()], manual };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function planInstructionSetup(
|
|
115
|
-
requestedAgents: readonly AgentId[],
|
|
116
|
-
options: InstructionPlanOptions = {},
|
|
117
|
-
): InstructionPlan {
|
|
118
|
-
const home = options.home ?? process.env.HOME ?? "";
|
|
119
|
-
const resolvedOptions = {
|
|
120
|
-
home,
|
|
121
|
-
codexHome: options.codexHome ?? process.env.CODEX_HOME ?? join(home, ".codex"),
|
|
122
|
-
claudeConfigDir: options.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"),
|
|
123
|
-
};
|
|
124
|
-
return planInstructionSetupWithResolver(
|
|
125
|
-
requestedAgents,
|
|
126
|
-
(agent) => instructionPath(agent, resolvedOptions),
|
|
127
|
-
options.readFile ?? readInstructionFile,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function projectInstructionPath(agent: AgentId, projectRoot: string): string | undefined {
|
|
132
|
-
return getAgentDefinition(agent).instructions?.project?.(projectRoot);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function planProjectInstructionSetup(
|
|
136
|
-
requestedAgents: readonly AgentId[],
|
|
137
|
-
projectRoot: string,
|
|
138
|
-
options: Pick<InstructionPlanOptions, "readFile"> = {},
|
|
139
|
-
): InstructionPlan {
|
|
140
|
-
return planInstructionSetupWithResolver(
|
|
141
|
-
requestedAgents,
|
|
142
|
-
(agent) => projectInstructionPath(agent, projectRoot),
|
|
143
|
-
options.readFile ?? readInstructionFile,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function atomicWrite(path: string, content: string, mode: number): void {
|
|
148
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
149
|
-
const temporaryPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
150
|
-
try {
|
|
151
|
-
writeFileSync(temporaryPath, content, { encoding: "utf8", mode });
|
|
152
|
-
renameSync(temporaryPath, path);
|
|
153
|
-
} finally {
|
|
154
|
-
rmSync(temporaryPath, { force: true });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function applyInstructionPlan(plan: InstructionPlan): void {
|
|
159
|
-
for (const change of plan.changes) {
|
|
160
|
-
if (change.status === "unchanged") continue;
|
|
161
|
-
if (readInstructionFile(change.path) !== change.before) {
|
|
162
|
-
throw new Error(`instruction file changed after planning: ${change.path}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const applied: InstructionChange[] = [];
|
|
167
|
-
try {
|
|
168
|
-
for (const change of plan.changes) {
|
|
169
|
-
if (change.status === "unchanged") continue;
|
|
170
|
-
const mode = change.before === null ? 0o600 : statSync(change.path).mode;
|
|
171
|
-
atomicWrite(change.path, change.after, mode);
|
|
172
|
-
applied.push(change);
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
rollbackInstructionChanges(applied);
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function rollbackInstructionChanges(changes: InstructionChange[]): void {
|
|
181
|
-
for (const change of [...changes].reverse()) {
|
|
182
|
-
if (change.before === null) {
|
|
183
|
-
rmSync(change.path, { force: true });
|
|
184
|
-
} else {
|
|
185
|
-
atomicWrite(change.path, change.before, statSync(change.path).mode);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function rollbackInstructionPlan(plan: InstructionPlan): void {
|
|
191
|
-
rollbackInstructionChanges(plan.changes.filter((change) => change.status !== "unchanged"));
|
|
192
|
-
}
|
package/src/init.ts
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
lstatSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
readlinkSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
realpathSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
rmdirSync,
|
|
11
|
-
symlinkSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
} from "node:fs";
|
|
15
|
-
import { hostname } from "node:os";
|
|
16
|
-
import { basename, dirname, join } from "node:path";
|
|
17
|
-
import {
|
|
18
|
-
parseManifest,
|
|
19
|
-
resolveManifestPath,
|
|
20
|
-
serializeManifest,
|
|
21
|
-
type Manifest,
|
|
22
|
-
CORE_SKILL_LIMIT,
|
|
23
|
-
MANIFEST_FILENAME,
|
|
24
|
-
} from "./manifest";
|
|
25
|
-
import { BUILT_IN_TARGET_NAMES } from "./init-agents";
|
|
26
|
-
import {
|
|
27
|
-
adoptTarget,
|
|
28
|
-
preflightAdoptTarget,
|
|
29
|
-
readSkillmuxMarker,
|
|
30
|
-
SKILLMUX_MARKER_FILENAME,
|
|
31
|
-
} from "./sync";
|
|
32
|
-
import { SKILL_ID_PATTERN } from "./vault";
|
|
33
|
-
|
|
34
|
-
export const DEFAULT_SURFACE_CANDIDATES = ["~/.claude/skills", "~/.agents/skills"];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Test/ops escape hatch: comma-separated absolute paths overriding
|
|
38
|
-
* DEFAULT_SURFACE_CANDIDATES. Not part of config.toml — "others as
|
|
39
|
-
* configured" (spec.md) is deliberately left as an implementation-time
|
|
40
|
-
* choice, same as the proposal heuristic. Exists primarily so tests never
|
|
41
|
-
* probe the real $HOME's ~/.claude/skills or ~/.agents/skills.
|
|
42
|
-
*/
|
|
43
|
-
export function surfaceCandidates(): string[] {
|
|
44
|
-
const override = process.env.SKILLMUX_INIT_SURFACES;
|
|
45
|
-
return override ? override.split(",").filter((p) => p.length > 0) : DEFAULT_SURFACE_CANDIDATES;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface SurfaceCandidate {
|
|
49
|
-
path: string;
|
|
50
|
-
canonicalPath?: string;
|
|
51
|
-
exists: boolean;
|
|
52
|
-
isSymlink: boolean;
|
|
53
|
-
skillCount: number;
|
|
54
|
-
alreadyMarked: boolean;
|
|
55
|
-
state: "missing" | "directory" | "broken-symlink" | "external-symlink" | "full-vault" | "unsupported";
|
|
56
|
-
deliveryMode: "managed-pins" | "full-vault" | "external";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function countSkillDirs(dir: string): number {
|
|
60
|
-
let count = 0;
|
|
61
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
62
|
-
if (entry.isDirectory() && SKILL_ID_PATTERN.test(entry.name) && existsSync(join(dir, entry.name, "SKILL.md"))) {
|
|
63
|
-
count++;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return count;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function detectSurfaces(candidatePaths: string[], vaultPath?: string): SurfaceCandidate[] {
|
|
70
|
-
const canonicalVaultPath = vaultPath ? realpathSync(vaultPath) : undefined;
|
|
71
|
-
|
|
72
|
-
return candidatePaths.map((path): SurfaceCandidate => {
|
|
73
|
-
let stat;
|
|
74
|
-
try {
|
|
75
|
-
stat = lstatSync(path);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
78
|
-
return {
|
|
79
|
-
path,
|
|
80
|
-
exists: false,
|
|
81
|
-
isSymlink: false,
|
|
82
|
-
skillCount: 0,
|
|
83
|
-
alreadyMarked: false,
|
|
84
|
-
state: "missing",
|
|
85
|
-
deliveryMode: "managed-pins",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (stat.isSymbolicLink()) {
|
|
90
|
-
let canonicalPath: string;
|
|
91
|
-
try {
|
|
92
|
-
canonicalPath = realpathSync(path);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
95
|
-
return {
|
|
96
|
-
path,
|
|
97
|
-
exists: false,
|
|
98
|
-
isSymlink: true,
|
|
99
|
-
skillCount: 0,
|
|
100
|
-
alreadyMarked: false,
|
|
101
|
-
state: "broken-symlink",
|
|
102
|
-
deliveryMode: "external",
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const isFullVault = canonicalVaultPath !== undefined && canonicalPath === canonicalVaultPath;
|
|
107
|
-
return {
|
|
108
|
-
path,
|
|
109
|
-
canonicalPath,
|
|
110
|
-
exists: true,
|
|
111
|
-
isSymlink: true,
|
|
112
|
-
skillCount: 0,
|
|
113
|
-
alreadyMarked: false,
|
|
114
|
-
state: isFullVault ? "full-vault" : "external-symlink",
|
|
115
|
-
deliveryMode: isFullVault ? "full-vault" : "external",
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const canonicalPath = realpathSync(path);
|
|
120
|
-
const isFullVault = canonicalVaultPath !== undefined && canonicalPath === canonicalVaultPath;
|
|
121
|
-
if (isFullVault) {
|
|
122
|
-
return {
|
|
123
|
-
path,
|
|
124
|
-
canonicalPath,
|
|
125
|
-
exists: true,
|
|
126
|
-
isSymlink: false,
|
|
127
|
-
skillCount: 0,
|
|
128
|
-
alreadyMarked: false,
|
|
129
|
-
state: "full-vault",
|
|
130
|
-
deliveryMode: "full-vault",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!stat.isDirectory()) {
|
|
135
|
-
return {
|
|
136
|
-
path,
|
|
137
|
-
canonicalPath,
|
|
138
|
-
exists: true,
|
|
139
|
-
isSymlink: false,
|
|
140
|
-
skillCount: 0,
|
|
141
|
-
alreadyMarked: false,
|
|
142
|
-
state: "unsupported",
|
|
143
|
-
deliveryMode: "external",
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
path,
|
|
149
|
-
canonicalPath,
|
|
150
|
-
exists: true,
|
|
151
|
-
isSymlink: false,
|
|
152
|
-
skillCount: countSkillDirs(path),
|
|
153
|
-
alreadyMarked: readSkillmuxMarker(path) !== null,
|
|
154
|
-
state: "directory",
|
|
155
|
-
deliveryMode: "managed-pins",
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Conservative default: no slash-command/workflow-router detection heuristic
|
|
162
|
-
* at TDD time (spec.md, "skr init" AC) — evidence-only, nothing proposed
|
|
163
|
-
* until a concrete heuristic is agreed.
|
|
164
|
-
*/
|
|
165
|
-
export function proposeManifest(_candidates: SurfaceCandidate[]): Pick<Manifest, "core" | "project"> {
|
|
166
|
-
return { core: { skills: [] }, project: {} };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** e.g. ~/.claude/skills -> "claude"; ~/.agents/skills -> "agents". */
|
|
170
|
-
export function deriveTargetName(path: string): string {
|
|
171
|
-
return basename(dirname(path)).replace(/^\./, "").toLowerCase();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface ConfirmedTarget {
|
|
175
|
-
name: string;
|
|
176
|
-
dir: string;
|
|
177
|
-
migrateFullVault?: boolean;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface InitTransactionParticipant {
|
|
181
|
-
apply: () => void;
|
|
182
|
-
rollback: () => void;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function preflightManagedTargets(vaultPath: string, targets: ConfirmedTarget[]): void {
|
|
186
|
-
const canonicalVaultPath = realpathSync(vaultPath);
|
|
187
|
-
|
|
188
|
-
for (const target of targets) {
|
|
189
|
-
let stat;
|
|
190
|
-
try {
|
|
191
|
-
stat = lstatSync(target.dir);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (stat.isSymbolicLink()) {
|
|
198
|
-
const canonicalTargetPath = realpathSync(target.dir);
|
|
199
|
-
if (target.migrateFullVault && canonicalTargetPath === canonicalVaultPath) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
throw new Error(
|
|
203
|
-
`target "${target.name}" (${target.dir}) is a symbolic link; classify or migrate it before managed-pins adoption`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (!stat.isDirectory()) {
|
|
207
|
-
throw new Error(`target "${target.name}" (${target.dir}) is not a directory`);
|
|
208
|
-
}
|
|
209
|
-
if (realpathSync(target.dir) === canonicalVaultPath) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`target "${target.name}" (${target.dir}) is the full-vault surface; it cannot be adopted as managed-pins`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
preflightAdoptTarget(target.dir, target.name, vaultPath);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Writes skillmux.toml with the conservative-default core/project and the
|
|
220
|
-
* confirmed targets, then adopts each confirmed dir in place (creating it
|
|
221
|
-
* first if it doesn't exist yet). Unconfirmed candidates are simply never
|
|
222
|
-
* passed in — this function never discovers paths on its own.
|
|
223
|
-
*/
|
|
224
|
-
export function planInitManifest(
|
|
225
|
-
vaultPath: string,
|
|
226
|
-
confirmedTargets: ConfirmedTarget[],
|
|
227
|
-
coreSkillIds: string[] = [],
|
|
228
|
-
): Manifest {
|
|
229
|
-
preflightManagedTargets(vaultPath, confirmedTargets);
|
|
230
|
-
|
|
231
|
-
const existingManifestPath = resolveManifestPath(vaultPath);
|
|
232
|
-
const existingManifest = existingManifestPath
|
|
233
|
-
? parseManifest(readFileSync(existingManifestPath, "utf-8"))
|
|
234
|
-
: { ...proposeManifest([]), targets: {} };
|
|
235
|
-
const manifest: Manifest = {
|
|
236
|
-
...existingManifest,
|
|
237
|
-
core: {
|
|
238
|
-
...existingManifest.core,
|
|
239
|
-
skills: [...new Set([...existingManifest.core.skills, ...coreSkillIds])],
|
|
240
|
-
},
|
|
241
|
-
targets: {
|
|
242
|
-
...existingManifest.targets,
|
|
243
|
-
...Object.fromEntries(
|
|
244
|
-
confirmedTargets.map((target) => {
|
|
245
|
-
const existingTarget = existingManifest.targets[target.name];
|
|
246
|
-
return [
|
|
247
|
-
target.name,
|
|
248
|
-
BUILT_IN_TARGET_NAMES.has(target.name)
|
|
249
|
-
? existingTarget
|
|
250
|
-
? { ...existingTarget, dir: undefined }
|
|
251
|
-
: { host: hostname(), project_groups: [] }
|
|
252
|
-
: existingTarget
|
|
253
|
-
? { ...existingTarget, dir: target.dir }
|
|
254
|
-
: { dir: target.dir, host: hostname(), project_groups: [] },
|
|
255
|
-
];
|
|
256
|
-
}),
|
|
257
|
-
),
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
if (manifest.core.skills.length > CORE_SKILL_LIMIT) {
|
|
261
|
-
throw new Error(
|
|
262
|
-
`[core] has ${manifest.core.skills.length} skills, exceeding the limit of ${CORE_SKILL_LIMIT}`,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
for (const skillId of coreSkillIds) {
|
|
266
|
-
if (!SKILL_ID_PATTERN.test(skillId) || !existsSync(join(vaultPath, skillId, "SKILL.md"))) {
|
|
267
|
-
throw new Error(`[core] skill "${skillId}" does not exist in the vault`);
|
|
268
|
-
}
|
|
269
|
-
for (const [groupName, group] of Object.entries(existingManifest.project ?? {})) {
|
|
270
|
-
if (group.skills.includes(skillId)) {
|
|
271
|
-
throw new Error(`skill "${skillId}" appears in both [core] and [project.${groupName}]`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return manifest;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function applyInit(
|
|
279
|
-
vaultPath: string,
|
|
280
|
-
confirmedTargets: ConfirmedTarget[],
|
|
281
|
-
participant?: InitTransactionParticipant,
|
|
282
|
-
coreSkillIds: string[] = [],
|
|
283
|
-
): Manifest {
|
|
284
|
-
const manifest = planInitManifest(vaultPath, confirmedTargets, coreSkillIds);
|
|
285
|
-
|
|
286
|
-
const manifestPath = join(vaultPath, MANIFEST_FILENAME);
|
|
287
|
-
const serializedManifest = serializeManifest(manifest);
|
|
288
|
-
const shouldWriteManifest =
|
|
289
|
-
!existsSync(manifestPath) || readFileSync(manifestPath, "utf-8") !== serializedManifest;
|
|
290
|
-
const createdDirs: string[] = [];
|
|
291
|
-
const adoptedDirs: string[] = [];
|
|
292
|
-
const migratedFullVaultDirs: Array<{ dir: string; linkTarget: string }> = [];
|
|
293
|
-
let participantApplied = false;
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
for (const target of confirmedTargets) {
|
|
297
|
-
let targetStat;
|
|
298
|
-
try {
|
|
299
|
-
targetStat = lstatSync(target.dir);
|
|
300
|
-
} catch (error) {
|
|
301
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
302
|
-
}
|
|
303
|
-
if (targetStat?.isSymbolicLink()) {
|
|
304
|
-
if (!target.migrateFullVault || realpathSync(target.dir) !== realpathSync(vaultPath)) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
`target "${target.name}" (${target.dir}) changed to an unsafe symbolic link after preflight`,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const linkTarget = readlinkSync(target.dir);
|
|
310
|
-
unlinkSync(target.dir);
|
|
311
|
-
mkdirSync(target.dir, { recursive: true });
|
|
312
|
-
migratedFullVaultDirs.push({ dir: target.dir, linkTarget });
|
|
313
|
-
} else if (targetStat && !targetStat.isDirectory()) {
|
|
314
|
-
throw new Error(`target "${target.name}" (${target.dir}) changed to a non-directory after preflight`);
|
|
315
|
-
}
|
|
316
|
-
if (!existsSync(target.dir)) {
|
|
317
|
-
mkdirSync(target.dir, { recursive: true });
|
|
318
|
-
createdDirs.push(target.dir);
|
|
319
|
-
}
|
|
320
|
-
if (adoptTarget(target.dir, target.name, vaultPath).adopted) {
|
|
321
|
-
adoptedDirs.push(target.dir);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (participant) {
|
|
326
|
-
participant.apply();
|
|
327
|
-
participantApplied = true;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (shouldWriteManifest) {
|
|
331
|
-
const temporaryManifestPath = join(
|
|
332
|
-
vaultPath,
|
|
333
|
-
`.${MANIFEST_FILENAME}.${process.pid}-${Date.now()}.tmp`,
|
|
334
|
-
);
|
|
335
|
-
try {
|
|
336
|
-
writeFileSync(temporaryManifestPath, serializedManifest);
|
|
337
|
-
renameSync(temporaryManifestPath, manifestPath);
|
|
338
|
-
} catch (error) {
|
|
339
|
-
if (existsSync(temporaryManifestPath)) unlinkSync(temporaryManifestPath);
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} catch (error) {
|
|
344
|
-
try {
|
|
345
|
-
if (participantApplied) participant?.rollback();
|
|
346
|
-
} finally {
|
|
347
|
-
for (const dir of adoptedDirs.reverse()) {
|
|
348
|
-
const markerPath = join(dir, SKILLMUX_MARKER_FILENAME);
|
|
349
|
-
if (existsSync(markerPath)) unlinkSync(markerPath);
|
|
350
|
-
}
|
|
351
|
-
for (const migration of migratedFullVaultDirs.reverse()) {
|
|
352
|
-
if (existsSync(migration.dir)) rmdirSync(migration.dir);
|
|
353
|
-
symlinkSync(migration.linkTarget, migration.dir);
|
|
354
|
-
}
|
|
355
|
-
for (const dir of createdDirs.reverse()) {
|
|
356
|
-
if (existsSync(dir)) rmdirSync(dir);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return manifest;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export const DISCOVERY_PARAGRAPH =
|
|
366
|
-
"Skills: only a curated core is loaded statically. Before improvising a " +
|
|
367
|
-
"multi-step workflow, or when a task smells like a domain you have no loaded " +
|
|
368
|
-
"skill for (career/resume, trading, SEO, i18n, design, one-off tooling), " +
|
|
369
|
-
"call `resolve_skill` with a one-line task description. `resolve_skill` " +
|
|
370
|
-
"returns a ranked shortlist; inspect candidates, fetch one or more " +
|
|
371
|
-
"relevant skills with `fetch_skill`, or ignore all and proceed normally " +
|
|
372
|
-
"when none are useful.";
|
|
373
|
-
|
|
374
|
-
export const MCP_REGISTRATION_SNIPPET = JSON.stringify(
|
|
375
|
-
{ mcpServers: { "skillmux": { command: "skillmux", args: ["serve"] } } },
|
|
376
|
-
null,
|
|
377
|
-
2,
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
/** §3.4 step 4: "print the last mile" — MCP registration command + discovery paragraph. */
|
|
381
|
-
export function printLastMile(): string {
|
|
382
|
-
return [
|
|
383
|
-
"Register with your MCP client:",
|
|
384
|
-
MCP_REGISTRATION_SNIPPET,
|
|
385
|
-
"",
|
|
386
|
-
"Add this paragraph to your shared agent instructions (~80 tokens, the entire T3 discovery mechanism):",
|
|
387
|
-
DISCOVERY_PARAGRAPH,
|
|
388
|
-
].join("\n");
|
|
389
|
-
}
|