@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/eval.ts
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
backfillEmbeddings,
|
|
6
|
-
getRuntime,
|
|
7
|
-
retrieveAndRerank,
|
|
8
|
-
} from "./router-core";
|
|
9
|
-
|
|
10
|
-
export interface EvalCase {
|
|
11
|
-
query: string;
|
|
12
|
-
split?: string;
|
|
13
|
-
relevant_skill_ids: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function parseEvalCases(raw: unknown): EvalCase[] {
|
|
17
|
-
if (!Array.isArray(raw)) throw new Error("Eval cases file must contain a JSON array");
|
|
18
|
-
const result: EvalCase[] = [];
|
|
19
|
-
for (let i = 0; i < raw.length; i++) {
|
|
20
|
-
const item = raw[i];
|
|
21
|
-
if (typeof item !== "object" || item === null) {
|
|
22
|
-
throw new Error(`Eval case at index ${i} must be an object`);
|
|
23
|
-
}
|
|
24
|
-
if ("expected" in item) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Legacy 'expected' field at case ${i} is no longer supported in eval datasets; use 'relevant_skill_ids' instead.`,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
if ("expected_outcome" in item) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Legacy 'expected_outcome' field at case ${i} is no longer supported in eval datasets; remove it and provide 'relevant_skill_ids' instead.`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
if (typeof (item as any).query !== "string" || (item as any).query.trim().length === 0) {
|
|
35
|
-
throw new Error(`Eval case at index ${i} has invalid 'query': must be a non-empty string`);
|
|
36
|
-
}
|
|
37
|
-
if (
|
|
38
|
-
!Array.isArray((item as any).relevant_skill_ids) ||
|
|
39
|
-
(item as any).relevant_skill_ids.some((id: unknown) => typeof id !== "string")
|
|
40
|
-
) {
|
|
41
|
-
throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': must be an array of strings`);
|
|
42
|
-
}
|
|
43
|
-
const relevantSkillIds = (item as any).relevant_skill_ids as string[];
|
|
44
|
-
if (relevantSkillIds.some((id) => id.trim().length === 0)) {
|
|
45
|
-
throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': IDs must be non-empty strings`);
|
|
46
|
-
}
|
|
47
|
-
if (new Set(relevantSkillIds).size !== relevantSkillIds.length) {
|
|
48
|
-
throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': duplicate IDs are not allowed`);
|
|
49
|
-
}
|
|
50
|
-
if ("split" in item && (item as any).split !== undefined && typeof (item as any).split !== "string") {
|
|
51
|
-
throw new Error(`Eval case at index ${i} has invalid 'split': must be a string if present`);
|
|
52
|
-
}
|
|
53
|
-
result.push({
|
|
54
|
-
query: (item as any).query,
|
|
55
|
-
...((item as any).split !== undefined ? { split: (item as any).split } : {}),
|
|
56
|
-
relevant_skill_ids: relevantSkillIds,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** AC17: dedup key for promoted cases. Collapses whitespace and case differences that are not meaningfully distinct queries. */
|
|
63
|
-
export function normalizeQuery(query: string): string {
|
|
64
|
-
return query.trim().replace(/\s+/g, " ").toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ObservedFetch {
|
|
68
|
-
query: string;
|
|
69
|
-
skill_id: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** AC17: groups fetched skill ids by normalized query into observed-split eval cases. */
|
|
73
|
-
export function buildPromotedCases(fetches: ObservedFetch[]): EvalCase[] {
|
|
74
|
-
const byQuery = new Map<string, { query: string; skillIds: string[]; seen: Set<string> }>();
|
|
75
|
-
for (const fetch of fetches) {
|
|
76
|
-
const key = normalizeQuery(fetch.query);
|
|
77
|
-
if (!key) continue;
|
|
78
|
-
let entry = byQuery.get(key);
|
|
79
|
-
if (!entry) {
|
|
80
|
-
entry = { query: fetch.query, skillIds: [], seen: new Set() };
|
|
81
|
-
byQuery.set(key, entry);
|
|
82
|
-
}
|
|
83
|
-
if (!entry.seen.has(fetch.skill_id)) {
|
|
84
|
-
entry.seen.add(fetch.skill_id);
|
|
85
|
-
entry.skillIds.push(fetch.skill_id);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return [...byQuery.values()].map((entry) => ({
|
|
89
|
-
query: entry.query,
|
|
90
|
-
split: "observed",
|
|
91
|
-
relevant_skill_ids: entry.skillIds,
|
|
92
|
-
}));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** AC18: never rewrites a case whose normalized query already exists in the target file. */
|
|
96
|
-
export function excludeExistingCases(
|
|
97
|
-
cases: EvalCase[],
|
|
98
|
-
existing: EvalCase[],
|
|
99
|
-
): { cases: EvalCase[]; skipped: number } {
|
|
100
|
-
const existingKeys = new Set(existing.map((c) => normalizeQuery(c.query)));
|
|
101
|
-
const kept: EvalCase[] = [];
|
|
102
|
-
let skipped = 0;
|
|
103
|
-
for (const c of cases) {
|
|
104
|
-
if (existingKeys.has(normalizeQuery(c.query))) {
|
|
105
|
-
skipped++;
|
|
106
|
-
} else {
|
|
107
|
-
kept.push(c);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return { cases: kept, skipped };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* AC17: joins fetches to the resolve that produced them for promotion. Only
|
|
115
|
-
* correlated fetches (a known resolve_audit_id) carry a query to promote;
|
|
116
|
-
* uncorrelated fetches have no resolve to join against and are excluded.
|
|
117
|
-
*/
|
|
118
|
-
export function queryPromotableFetches(db: Database, sinceIso: string): ObservedFetch[] {
|
|
119
|
-
return db
|
|
120
|
-
.query(
|
|
121
|
-
`SELECT audit.query AS query, fetch.skill_id AS skill_id
|
|
122
|
-
FROM fetch
|
|
123
|
-
JOIN audit ON fetch.resolve_audit_id = audit.id
|
|
124
|
-
WHERE fetch.ts >= ?
|
|
125
|
-
ORDER BY fetch.ts ASC`,
|
|
126
|
-
)
|
|
127
|
-
.all(sinceIso) as ObservedFetch[];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface EvalMetrics {
|
|
131
|
-
recall_at_5: number;
|
|
132
|
-
recall_at_10: number;
|
|
133
|
-
mrr: number;
|
|
134
|
-
ndcg_at_10: number;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export interface CandidateEvalDetail {
|
|
138
|
-
skill_id: string;
|
|
139
|
-
lexical_rank: number | null;
|
|
140
|
-
fused_rank: number | null;
|
|
141
|
-
reranked_rank?: number | null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export interface EvalCaseResult {
|
|
145
|
-
query: string;
|
|
146
|
-
relevant_skill_ids: string[];
|
|
147
|
-
retrieval: string;
|
|
148
|
-
degraded_from?: string | null;
|
|
149
|
-
degradation_reason?: string | null;
|
|
150
|
-
latency_ms: number;
|
|
151
|
-
recall_settings: {
|
|
152
|
-
k_lexical: number;
|
|
153
|
-
k_vector: number;
|
|
154
|
-
k_rerank: number;
|
|
155
|
-
};
|
|
156
|
-
candidates: CandidateEvalDetail[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface EvalReport {
|
|
160
|
-
queries: number;
|
|
161
|
-
judged_queries: number;
|
|
162
|
-
unjudged_queries: number;
|
|
163
|
-
lexical: EvalMetrics;
|
|
164
|
-
hybrid: EvalMetrics;
|
|
165
|
-
cases?: EvalCaseResult[];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function computeRankingMetrics(rankings: string[][], cases: EvalCase[]): EvalMetrics {
|
|
169
|
-
const judgedIndices: number[] = [];
|
|
170
|
-
for (let i = 0; i < cases.length; i++) {
|
|
171
|
-
if (cases[i]!.relevant_skill_ids && cases[i]!.relevant_skill_ids.length > 0) {
|
|
172
|
-
judgedIndices.push(i);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (judgedIndices.length === 0) {
|
|
177
|
-
return {
|
|
178
|
-
recall_at_5: 0,
|
|
179
|
-
recall_at_10: 0,
|
|
180
|
-
mrr: 0,
|
|
181
|
-
ndcg_at_10: 0,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
let totalRecall5 = 0;
|
|
186
|
-
let totalRecall10 = 0;
|
|
187
|
-
let totalMRR = 0;
|
|
188
|
-
let totalNDCG10 = 0;
|
|
189
|
-
|
|
190
|
-
for (const idx of judgedIndices) {
|
|
191
|
-
const c = cases[idx]!;
|
|
192
|
-
const ranking = rankings[idx] ?? [];
|
|
193
|
-
const relevantSet = new Set(c.relevant_skill_ids);
|
|
194
|
-
const numRelevant = c.relevant_skill_ids.length;
|
|
195
|
-
|
|
196
|
-
// Recall@5: fraction of relevant skill IDs present in the first 5 results
|
|
197
|
-
const top5 = ranking.slice(0, 5);
|
|
198
|
-
const count5 = top5.filter((id) => relevantSet.has(id)).length;
|
|
199
|
-
totalRecall5 += count5 / numRelevant;
|
|
200
|
-
|
|
201
|
-
// Recall@10: fraction of relevant skill IDs present in the first 10 results
|
|
202
|
-
const top10 = ranking.slice(0, 10);
|
|
203
|
-
const count10 = top10.filter((id) => relevantSet.has(id)).length;
|
|
204
|
-
totalRecall10 += count10 / numRelevant;
|
|
205
|
-
|
|
206
|
-
// MRR: reciprocal rank of the first relevant result (1-based), 0 if missing
|
|
207
|
-
const firstRelevantRank = ranking.findIndex((id) => relevantSet.has(id));
|
|
208
|
-
if (firstRelevantRank >= 0) {
|
|
209
|
-
totalMRR += 1 / (firstRelevantRank + 1);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// nDCG@10: binary relevance, DCG gain 1/log2(rank+1), IDCG over min(numRelevant, 10)
|
|
213
|
-
let dcg10 = 0;
|
|
214
|
-
for (let r = 0; r < Math.min(10, ranking.length); r++) {
|
|
215
|
-
if (relevantSet.has(ranking[r]!)) {
|
|
216
|
-
dcg10 += 1 / Math.log2(r + 2);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
let idcg10 = 0;
|
|
220
|
-
const idealCount = Math.min(numRelevant, 10);
|
|
221
|
-
for (let r = 0; r < idealCount; r++) {
|
|
222
|
-
idcg10 += 1 / Math.log2(r + 2);
|
|
223
|
-
}
|
|
224
|
-
if (idcg10 > 0) {
|
|
225
|
-
totalNDCG10 += dcg10 / idcg10;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const judgedCount = judgedIndices.length;
|
|
230
|
-
return {
|
|
231
|
-
recall_at_5: totalRecall5 / judgedCount,
|
|
232
|
-
recall_at_10: totalRecall10 / judgedCount,
|
|
233
|
-
mrr: totalMRR / judgedCount,
|
|
234
|
-
ndcg_at_10: totalNDCG10 / judgedCount,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function loadEvalCases(path = join(import.meta.dir, "..", "eval", "queries.json")): EvalCase[] {
|
|
239
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
240
|
-
return parseEvalCases(raw);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export async function evalVault(cases = loadEvalCases()): Promise<EvalReport> {
|
|
244
|
-
const { config } = await getRuntime();
|
|
245
|
-
await backfillEmbeddings();
|
|
246
|
-
|
|
247
|
-
const lexicalRankings: string[][] = [];
|
|
248
|
-
const hybridRankings: string[][] = [];
|
|
249
|
-
const caseResults: EvalCaseResult[] = [];
|
|
250
|
-
const kRerank = config.recall.k_rerank ?? Math.min(10, config.recall.k_lexical + config.recall.k_vector);
|
|
251
|
-
|
|
252
|
-
for (const evalCase of cases) {
|
|
253
|
-
const start = performance.now();
|
|
254
|
-
const retrievalResult = await retrieveAndRerank({ query: evalCase.query });
|
|
255
|
-
|
|
256
|
-
// lexical metrics use lexical rank
|
|
257
|
-
lexicalRankings.push(
|
|
258
|
-
retrievalResult.trace
|
|
259
|
-
.filter((candidate) => candidate.lexical_rank !== null)
|
|
260
|
-
.sort((a, b) => a.lexical_rank! - b.lexical_rank!)
|
|
261
|
-
.map((candidate) => candidate.skill_id),
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// retrieveAndRerank returns candidates in the exact order delivered to the
|
|
265
|
-
// caller: reranked when available, otherwise fused or lexical fallback.
|
|
266
|
-
hybridRankings.push(
|
|
267
|
-
retrievalResult.candidates.map((candidate) => candidate.skill_id),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const latency_ms = Math.round(performance.now() - start);
|
|
271
|
-
const candidateDetails: CandidateEvalDetail[] = retrievalResult.trace;
|
|
272
|
-
|
|
273
|
-
caseResults.push({
|
|
274
|
-
query: evalCase.query,
|
|
275
|
-
relevant_skill_ids: evalCase.relevant_skill_ids,
|
|
276
|
-
retrieval: retrievalResult.retrieval,
|
|
277
|
-
degraded_from: retrievalResult.degraded_from ?? null,
|
|
278
|
-
degradation_reason: retrievalResult.degradation_reason ?? null,
|
|
279
|
-
latency_ms,
|
|
280
|
-
recall_settings: {
|
|
281
|
-
k_lexical: config.recall.k_lexical,
|
|
282
|
-
k_vector: config.recall.k_vector,
|
|
283
|
-
k_rerank: kRerank,
|
|
284
|
-
},
|
|
285
|
-
candidates: candidateDetails,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const judged_queries = cases.filter(
|
|
290
|
-
(c) => c.relevant_skill_ids && c.relevant_skill_ids.length > 0,
|
|
291
|
-
).length;
|
|
292
|
-
const unjudged_queries = cases.length - judged_queries;
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
queries: cases.length,
|
|
296
|
-
judged_queries,
|
|
297
|
-
unjudged_queries,
|
|
298
|
-
lexical: computeRankingMetrics(lexicalRankings, cases),
|
|
299
|
-
hybrid: computeRankingMetrics(hybridRankings, cases),
|
|
300
|
-
cases: caseResults,
|
|
301
|
-
};
|
|
302
|
-
}
|
package/src/global-flags.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export type GlobalFlag =
|
|
2
|
-
| "--json"
|
|
3
|
-
| "--allow-insecure"
|
|
4
|
-
| "--verbose"
|
|
5
|
-
| "--dry-run";
|
|
6
|
-
|
|
7
|
-
export type GlobalFlagWithValue = "--context" | "--server";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Checks if an argument is a global flag that takes a value (e.g. `--context`, `--server`).
|
|
11
|
-
* If an `allowed` list is provided, only flags in that subset are matched.
|
|
12
|
-
*/
|
|
13
|
-
export function isGlobalFlagWithValue(
|
|
14
|
-
option: string | undefined,
|
|
15
|
-
allowed?: readonly GlobalFlagWithValue[] | GlobalFlagWithValue,
|
|
16
|
-
...rest: GlobalFlagWithValue[]
|
|
17
|
-
): boolean {
|
|
18
|
-
if (!option) return false;
|
|
19
|
-
if (allowed !== undefined) {
|
|
20
|
-
const list = Array.isArray(allowed) ? allowed : [allowed, ...rest];
|
|
21
|
-
return list.includes(option as GlobalFlagWithValue);
|
|
22
|
-
}
|
|
23
|
-
return option === "--context" || option === "--server";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Checks if an argument is a global flag that takes no value (e.g. `--json`, `--allow-insecure`, `--verbose`, `--dry-run`).
|
|
28
|
-
* If an `allowed` list is provided, only flags in that subset are matched.
|
|
29
|
-
*/
|
|
30
|
-
export function isGlobalFlag(
|
|
31
|
-
option: string | undefined,
|
|
32
|
-
allowed?: readonly GlobalFlag[] | GlobalFlag,
|
|
33
|
-
...rest: GlobalFlag[]
|
|
34
|
-
): boolean {
|
|
35
|
-
if (!option) return false;
|
|
36
|
-
if (allowed !== undefined) {
|
|
37
|
-
const list = Array.isArray(allowed) ? allowed : [allowed, ...rest];
|
|
38
|
-
return list.includes(option as GlobalFlag);
|
|
39
|
-
}
|
|
40
|
-
return (
|
|
41
|
-
option === "--json" ||
|
|
42
|
-
option === "--allow-insecure" ||
|
|
43
|
-
option === "--verbose" ||
|
|
44
|
-
option === "--dry-run"
|
|
45
|
-
);
|
|
46
|
-
}
|
package/src/init-agents.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export const SUPPORTED_AGENT_IDS = [
|
|
6
|
-
"claude-code",
|
|
7
|
-
"codex",
|
|
8
|
-
"opencode",
|
|
9
|
-
"github-copilot",
|
|
10
|
-
"windsurf",
|
|
11
|
-
"antigravity",
|
|
12
|
-
"goose",
|
|
13
|
-
"hermes",
|
|
14
|
-
] as const;
|
|
15
|
-
|
|
16
|
-
export type AgentId = (typeof SUPPORTED_AGENT_IDS)[number];
|
|
17
|
-
export type DeliveryMode = "managed-pins" | "full-vault";
|
|
18
|
-
|
|
19
|
-
export interface DetectedAgent {
|
|
20
|
-
agent: AgentId;
|
|
21
|
-
evidence: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type McpRegistrationScope = "user" | "project";
|
|
25
|
-
|
|
26
|
-
export interface McpRegistrationCommand {
|
|
27
|
-
command: string;
|
|
28
|
-
/** Returns undefined when this agent's CLI has no way to register at the requested scope. */
|
|
29
|
-
buildArgs: (scope: McpRegistrationScope) => string[] | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface AgentPathOptions {
|
|
33
|
-
home: string;
|
|
34
|
-
codexHome: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface AgentInstructionOptions extends AgentPathOptions {
|
|
38
|
-
claudeConfigDir: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The single source of truth for what each agent supports. Every other
|
|
43
|
-
* module (detection, skill-surface planning, readiness reporting, MCP
|
|
44
|
-
* registration, instruction files, shell completions, CLI help text) reads
|
|
45
|
-
* from this record instead of keeping its own copy — see git history for
|
|
46
|
-
* what it looked like before consolidation (agent support was duplicated
|
|
47
|
-
* across 6+ files and could silently drift, e.g. shell completions offering
|
|
48
|
-
* goose/hermes for `project init` when they can't actually be attached).
|
|
49
|
-
*/
|
|
50
|
-
interface AgentDefinition {
|
|
51
|
-
id: AgentId;
|
|
52
|
-
surfaceId?: "agent-skills" | "claude-code" | "codex" | "antigravity";
|
|
53
|
-
deliveryMode: DeliveryMode;
|
|
54
|
-
/** Filesystem evidence used by guided-mode agent detection. */
|
|
55
|
-
detectionPath?: (options: AgentPathOptions) => string;
|
|
56
|
-
/** Bespoke readiness message for full-vault agents with no managed-pins surface. */
|
|
57
|
-
manualSkillSurfaceMessage?: string;
|
|
58
|
-
instructions?: {
|
|
59
|
-
global?: (options: AgentInstructionOptions) => string;
|
|
60
|
-
project?: (projectRoot: string) => string;
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Only set for agents whose own CLI's exact registration command was
|
|
64
|
-
* verified against its own --help, not guessed from docs. Every other
|
|
65
|
-
* agent falls back to printing the registration snippet (see
|
|
66
|
-
* printLastMile in init.ts) rather than a guessed command.
|
|
67
|
-
*/
|
|
68
|
-
mcpRegistration?: McpRegistrationCommand;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface PlannedAgentSurface {
|
|
72
|
-
id: string;
|
|
73
|
-
targetName: string;
|
|
74
|
-
path: string;
|
|
75
|
-
deliveryMode: "managed-pins";
|
|
76
|
-
agents: AgentId[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface AgentSurfacePlan {
|
|
80
|
-
agents: AgentDefinition[];
|
|
81
|
-
surfaces: PlannedAgentSurface[];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
type ReadinessStatus = "ready" | "planned" | "manual" | "not-applicable";
|
|
85
|
-
|
|
86
|
-
export interface ReadinessAxis {
|
|
87
|
-
status: ReadinessStatus;
|
|
88
|
-
detail: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface AgentReadiness {
|
|
92
|
-
agent: AgentId;
|
|
93
|
-
skillSurface: ReadinessAxis;
|
|
94
|
-
mcpRegistration: ReadinessAxis;
|
|
95
|
-
instructionSetup: ReadinessAxis;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface ResolvedBuiltInTarget {
|
|
99
|
-
targetName: string;
|
|
100
|
-
path: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Target names whose install directory is deterministic — --dir is optional for these. */
|
|
104
|
-
export const BUILT_IN_TARGET_NAMES = new Set(["agent-skills", "claude-code", "codex"]);
|
|
105
|
-
|
|
106
|
-
const AGENTS: Record<AgentId, AgentDefinition> = {
|
|
107
|
-
"claude-code": {
|
|
108
|
-
id: "claude-code",
|
|
109
|
-
surfaceId: "claude-code",
|
|
110
|
-
deliveryMode: "managed-pins",
|
|
111
|
-
detectionPath: ({ home }) => join(home, ".claude"),
|
|
112
|
-
instructions: {
|
|
113
|
-
global: ({ claudeConfigDir }) => join(claudeConfigDir, "CLAUDE.md"),
|
|
114
|
-
project: (projectRoot) => join(projectRoot, "CLAUDE.md"),
|
|
115
|
-
},
|
|
116
|
-
mcpRegistration: {
|
|
117
|
-
command: "claude",
|
|
118
|
-
// claude mcp add --scope: local (default, unshared), project (writes a
|
|
119
|
-
// committed .mcp.json), or user (global). "project" is the only shared
|
|
120
|
-
// option, so that's what a project-scoped registration means here.
|
|
121
|
-
buildArgs: (scope) => [
|
|
122
|
-
"mcp",
|
|
123
|
-
"add",
|
|
124
|
-
"-s",
|
|
125
|
-
scope === "project" ? "project" : "user",
|
|
126
|
-
"skillmux",
|
|
127
|
-
"--",
|
|
128
|
-
"skillmux",
|
|
129
|
-
"serve",
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
codex: {
|
|
134
|
-
id: "codex",
|
|
135
|
-
surfaceId: "codex",
|
|
136
|
-
deliveryMode: "managed-pins",
|
|
137
|
-
detectionPath: ({ codexHome }) => codexHome,
|
|
138
|
-
instructions: {
|
|
139
|
-
global: ({ codexHome }) => join(codexHome, "AGENTS.md"),
|
|
140
|
-
},
|
|
141
|
-
mcpRegistration: {
|
|
142
|
-
command: "codex",
|
|
143
|
-
// codex mcp add has no --scope flag at all — it always writes to the
|
|
144
|
-
// global ~/.codex/config.toml, so project scope is not representable.
|
|
145
|
-
buildArgs: (scope) =>
|
|
146
|
-
scope === "project"
|
|
147
|
-
? undefined
|
|
148
|
-
: ["mcp", "add", "skillmux", "--", "skillmux", "serve"],
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
opencode: {
|
|
152
|
-
id: "opencode",
|
|
153
|
-
surfaceId: "agent-skills",
|
|
154
|
-
deliveryMode: "managed-pins",
|
|
155
|
-
detectionPath: ({ home }) => join(home, ".config", "opencode"),
|
|
156
|
-
instructions: {
|
|
157
|
-
global: ({ home }) => join(home, ".config", "opencode", "AGENTS.md"),
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
"github-copilot": {
|
|
161
|
-
id: "github-copilot",
|
|
162
|
-
surfaceId: "agent-skills",
|
|
163
|
-
deliveryMode: "managed-pins",
|
|
164
|
-
detectionPath: ({ home }) => join(home, ".config", "github-copilot"),
|
|
165
|
-
},
|
|
166
|
-
windsurf: {
|
|
167
|
-
id: "windsurf",
|
|
168
|
-
surfaceId: "agent-skills",
|
|
169
|
-
deliveryMode: "managed-pins",
|
|
170
|
-
detectionPath: ({ home }) => join(home, ".codeium", "windsurf"),
|
|
171
|
-
},
|
|
172
|
-
antigravity: {
|
|
173
|
-
id: "antigravity",
|
|
174
|
-
surfaceId: "antigravity",
|
|
175
|
-
deliveryMode: "managed-pins",
|
|
176
|
-
instructions: {
|
|
177
|
-
global: ({ home }) => join(home, ".gemini", "GEMINI.md"),
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
goose: {
|
|
181
|
-
id: "goose",
|
|
182
|
-
deliveryMode: "full-vault",
|
|
183
|
-
detectionPath: ({ home }) => join(home, ".config", "goose"),
|
|
184
|
-
manualSkillSurfaceMessage: "configure the full vault in Goose",
|
|
185
|
-
instructions: {
|
|
186
|
-
global: ({ home }) => join(home, ".config", "goose", ".goosehints"),
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
hermes: {
|
|
190
|
-
id: "hermes",
|
|
191
|
-
deliveryMode: "full-vault",
|
|
192
|
-
detectionPath: ({ home }) => join(home, ".hermes"),
|
|
193
|
-
manualSkillSurfaceMessage: "configure the full vault in Hermes external_dirs",
|
|
194
|
-
instructions: {
|
|
195
|
-
global: ({ home }) => join(home, ".hermes.md"),
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export function getAgentDefinition(agent: AgentId): AgentDefinition {
|
|
201
|
-
return AGENTS[agent];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Agents that can be attached to a project group (managed-pins delivery, not full-vault). */
|
|
205
|
-
export const MANAGED_PINS_AGENT_IDS = SUPPORTED_AGENT_IDS.filter(
|
|
206
|
-
(id) => AGENTS[id].deliveryMode === "managed-pins",
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
export function detectInstalledAgents(
|
|
210
|
-
options: {
|
|
211
|
-
home?: string;
|
|
212
|
-
codexHome?: string;
|
|
213
|
-
exists?: (path: string) => boolean;
|
|
214
|
-
} = {},
|
|
215
|
-
): DetectedAgent[] {
|
|
216
|
-
const home = options.home ?? homedir();
|
|
217
|
-
const codexHome = options.codexHome ?? join(home, ".codex");
|
|
218
|
-
const exists = options.exists ?? existsSync;
|
|
219
|
-
return SUPPORTED_AGENT_IDS.flatMap((agent) => {
|
|
220
|
-
const evidence = AGENTS[agent].detectionPath?.({ home, codexHome });
|
|
221
|
-
return evidence && exists(evidence) ? [{ agent, evidence }] : [];
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function surfacePath(
|
|
226
|
-
surfaceId: NonNullable<AgentDefinition["surfaceId"]>,
|
|
227
|
-
options: { home: string; codexHome?: string },
|
|
228
|
-
): string {
|
|
229
|
-
if (surfaceId === "agent-skills") return join(options.home, ".agents", "skills");
|
|
230
|
-
if (surfaceId === "claude-code") return join(options.home, ".claude", "skills");
|
|
231
|
-
if (surfaceId === "codex") return join(options.codexHome ?? join(options.home, ".codex"), "skills");
|
|
232
|
-
return join(options.home, ".gemini", "config", "skills");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function resolveBuiltInTarget(
|
|
236
|
-
name: string,
|
|
237
|
-
options: { home?: string; codexHome?: string; customPath?: string } = {},
|
|
238
|
-
): ResolvedBuiltInTarget {
|
|
239
|
-
const home = options.home ?? homedir();
|
|
240
|
-
if (name === "custom") {
|
|
241
|
-
if (!options.customPath) throw new Error("--target custom requires --path <dir>");
|
|
242
|
-
return { targetName: name, path: options.customPath };
|
|
243
|
-
}
|
|
244
|
-
if (name === "agent-skills") {
|
|
245
|
-
return { targetName: name, path: surfacePath("agent-skills", { home }) };
|
|
246
|
-
}
|
|
247
|
-
if (name === "claude-code") {
|
|
248
|
-
return { targetName: name, path: surfacePath("claude-code", { home }) };
|
|
249
|
-
}
|
|
250
|
-
if (name === "codex") {
|
|
251
|
-
return {
|
|
252
|
-
targetName: name,
|
|
253
|
-
path: surfacePath("codex", { home, codexHome: options.codexHome }),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
throw new Error(
|
|
257
|
-
`unknown --target "${name}"; supported targets: agent-skills, claude-code, codex, custom`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function planAgentSurfaces(
|
|
262
|
-
requestedAgents: readonly string[],
|
|
263
|
-
options: { home?: string; codexHome?: string } = {},
|
|
264
|
-
): AgentSurfacePlan {
|
|
265
|
-
const agents = [...new Set(requestedAgents)].map((id) => {
|
|
266
|
-
if (!SUPPORTED_AGENT_IDS.includes(id as AgentId)) {
|
|
267
|
-
throw new Error(
|
|
268
|
-
`unsupported agent "${id}"; supported agents: ${SUPPORTED_AGENT_IDS.join(", ")}`,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
return AGENTS[id as AgentId];
|
|
272
|
-
});
|
|
273
|
-
const home = options.home ?? homedir();
|
|
274
|
-
const surfaces = new Map<string, PlannedAgentSurface>();
|
|
275
|
-
|
|
276
|
-
for (const agent of agents) {
|
|
277
|
-
if (!agent.surfaceId) continue;
|
|
278
|
-
const path = surfacePath(agent.surfaceId, { home, codexHome: options.codexHome });
|
|
279
|
-
const existing = surfaces.get(path);
|
|
280
|
-
if (existing) {
|
|
281
|
-
if (!existing.agents.includes(agent.id)) existing.agents.push(agent.id);
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
surfaces.set(path, {
|
|
285
|
-
id: agent.surfaceId,
|
|
286
|
-
targetName: agent.surfaceId,
|
|
287
|
-
path,
|
|
288
|
-
deliveryMode: "managed-pins",
|
|
289
|
-
agents: [agent.id],
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return { agents, surfaces: [...surfaces.values()] };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function assessAgentReadiness(
|
|
297
|
-
plan: AgentSurfacePlan,
|
|
298
|
-
instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {},
|
|
299
|
-
): AgentReadiness[] {
|
|
300
|
-
return plan.agents.map((agent) => {
|
|
301
|
-
const surface = plan.surfaces.find((candidate) => candidate.agents.includes(agent.id));
|
|
302
|
-
let skillSurface: ReadinessAxis;
|
|
303
|
-
if (surface) {
|
|
304
|
-
skillSurface = { status: "planned", detail: surface.path };
|
|
305
|
-
} else if (agent.manualSkillSurfaceMessage) {
|
|
306
|
-
skillSurface = { status: "manual", detail: agent.manualSkillSurfaceMessage };
|
|
307
|
-
} else {
|
|
308
|
-
skillSurface = {
|
|
309
|
-
status: "not-applicable",
|
|
310
|
-
detail: "skills resolve through Skillmux MCP",
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const mcpRegistration: ReadinessAxis = {
|
|
315
|
-
status: "not-applicable",
|
|
316
|
-
detail: "native skill loading",
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
agent: agent.id,
|
|
321
|
-
skillSurface,
|
|
322
|
-
mcpRegistration,
|
|
323
|
-
instructionSetup: instructionReadiness[agent.id] ?? {
|
|
324
|
-
status: "manual",
|
|
325
|
-
detail: "instruction adapter not applied",
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
|
-
}
|