@klhapp/skillmux 1.12.0 → 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 +8 -0
- package/README.md +18 -9
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +14 -4
- package/docs/concepts.md +2 -2
- package/docs/configuration.md +18 -3
- 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/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 -390
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -359
- 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/db-index.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { VaultSkill } from "./vault";
|
|
5
|
-
|
|
6
|
-
export interface SkillRow {
|
|
7
|
-
skill_id: string;
|
|
8
|
-
title: string;
|
|
9
|
-
description: string;
|
|
10
|
-
aliases: string;
|
|
11
|
-
content_sha256: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function openIndex(stateDir: string): Database {
|
|
15
|
-
mkdirSync(stateDir, { recursive: true });
|
|
16
|
-
const db = new Database(join(stateDir, "index.sqlite3"), { create: true });
|
|
17
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
18
|
-
db.run("PRAGMA busy_timeout = 2000");
|
|
19
|
-
db.run(`CREATE TABLE IF NOT EXISTS skills (
|
|
20
|
-
skill_id TEXT PRIMARY KEY,
|
|
21
|
-
title TEXT NOT NULL,
|
|
22
|
-
description TEXT NOT NULL,
|
|
23
|
-
aliases TEXT NOT NULL,
|
|
24
|
-
content_sha256 TEXT NOT NULL
|
|
25
|
-
)`);
|
|
26
|
-
db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
27
|
-
skill_id UNINDEXED, title, description, aliases,
|
|
28
|
-
tokenize = 'unicode61 remove_diacritics 2'
|
|
29
|
-
)`);
|
|
30
|
-
db.run(`CREATE TABLE IF NOT EXISTS vectors (
|
|
31
|
-
skill_id TEXT PRIMARY KEY,
|
|
32
|
-
content_sha256 TEXT NOT NULL,
|
|
33
|
-
embedding_fingerprint TEXT NOT NULL DEFAULT '',
|
|
34
|
-
dim INTEGER NOT NULL,
|
|
35
|
-
vec BLOB NOT NULL
|
|
36
|
-
)`);
|
|
37
|
-
const vectorColumns = db.query("PRAGMA table_info(vectors)").all() as { name: string }[];
|
|
38
|
-
if (!vectorColumns.some((column) => column.name === "embedding_fingerprint")) {
|
|
39
|
-
db.run("ALTER TABLE vectors ADD COLUMN embedding_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
40
|
-
}
|
|
41
|
-
// Audit rows live in audit.sqlite3; openAudit adopts any left here.
|
|
42
|
-
db.run(`CREATE TABLE IF NOT EXISTS index_meta (
|
|
43
|
-
key TEXT PRIMARY KEY,
|
|
44
|
-
value TEXT NOT NULL
|
|
45
|
-
)`);
|
|
46
|
-
return db;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function upsertSkill(db: Database, skill: VaultSkill): void {
|
|
50
|
-
const aliases = skill.aliases.join(" ");
|
|
51
|
-
db.transaction(() => {
|
|
52
|
-
db.run("DELETE FROM skills WHERE skill_id = ?", [skill.skill_id]);
|
|
53
|
-
db.run("DELETE FROM skills_fts WHERE skill_id = ?", [skill.skill_id]);
|
|
54
|
-
db.run(
|
|
55
|
-
"INSERT INTO skills (skill_id, title, description, aliases, content_sha256) VALUES (?, ?, ?, ?, ?)",
|
|
56
|
-
[skill.skill_id, skill.title, skill.description, aliases, skill.content_sha256],
|
|
57
|
-
);
|
|
58
|
-
db.run(
|
|
59
|
-
"INSERT INTO skills_fts (skill_id, title, description, aliases) VALUES (?, ?, ?, ?)",
|
|
60
|
-
[skill.skill_id, skill.title, skill.description, aliases],
|
|
61
|
-
);
|
|
62
|
-
})();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function toSkillRow(skill: VaultSkill): SkillRow {
|
|
66
|
-
return {
|
|
67
|
-
skill_id: skill.skill_id,
|
|
68
|
-
title: skill.title,
|
|
69
|
-
description: skill.description,
|
|
70
|
-
aliases: skill.aliases.join(" "),
|
|
71
|
-
content_sha256: skill.content_sha256,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Replace the whole lexical index with `rows`; drops vectors of removed skills. */
|
|
76
|
-
export function replaceSkills(db: Database, rows: SkillRow[]): void {
|
|
77
|
-
db.transaction(() => {
|
|
78
|
-
db.run("DELETE FROM skills");
|
|
79
|
-
db.run("DELETE FROM skills_fts");
|
|
80
|
-
for (const row of rows) {
|
|
81
|
-
db.run(
|
|
82
|
-
"INSERT INTO skills (skill_id, title, description, aliases, content_sha256) VALUES (?, ?, ?, ?, ?)",
|
|
83
|
-
[row.skill_id, row.title, row.description, row.aliases, row.content_sha256],
|
|
84
|
-
);
|
|
85
|
-
db.run(
|
|
86
|
-
"INSERT INTO skills_fts (skill_id, title, description, aliases) VALUES (?, ?, ?, ?)",
|
|
87
|
-
[row.skill_id, row.title, row.description, row.aliases],
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
db.run("DELETE FROM vectors WHERE skill_id NOT IN (SELECT skill_id FROM skills)");
|
|
91
|
-
})();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function ingestVault(db: Database, skills: VaultSkill[]): void {
|
|
95
|
-
replaceSkills(db, skills.map(toSkillRow));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function deleteSkill(db: Database, skillId: string): void {
|
|
99
|
-
db.transaction(() => {
|
|
100
|
-
db.run("DELETE FROM skills WHERE skill_id = ?", [skillId]);
|
|
101
|
-
db.run("DELETE FROM skills_fts WHERE skill_id = ?", [skillId]);
|
|
102
|
-
db.run("DELETE FROM vectors WHERE skill_id = ?", [skillId]);
|
|
103
|
-
})();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function skillCount(db: Database): number {
|
|
107
|
-
return (db.query("SELECT count(*) AS n FROM skills").get() as { n: number }).n;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function getSkillRow(db: Database, skillId: string): SkillRow | null {
|
|
111
|
-
return db.query("SELECT * FROM skills WHERE skill_id = ?").get(skillId) as SkillRow | null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Sanitize free text into an FTS5 OR-query; returns null when no usable terms
|
|
116
|
-
* remain. Terms keep any Unicode letters/digits (CJK included) so non-ASCII
|
|
117
|
-
* queries still get lexical recall — unicode61 tokenizes contiguous CJK runs
|
|
118
|
-
* as single tokens, so matching works at that granularity.
|
|
119
|
-
*/
|
|
120
|
-
export function toFtsQuery(text: string): string | null {
|
|
121
|
-
const terms = [
|
|
122
|
-
...new Set(
|
|
123
|
-
text
|
|
124
|
-
.toLowerCase()
|
|
125
|
-
.split(/[^\p{L}\p{N}]+/u)
|
|
126
|
-
.filter((t) => t.length >= 2),
|
|
127
|
-
),
|
|
128
|
-
];
|
|
129
|
-
if (terms.length === 0) return null;
|
|
130
|
-
return terms.map((t) => `"${t}"`).join(" OR ");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function ftsSearch(db: Database, text: string, k: number): SkillRow[] {
|
|
134
|
-
const query = toFtsQuery(text);
|
|
135
|
-
if (query === null) return [];
|
|
136
|
-
return db
|
|
137
|
-
.query(
|
|
138
|
-
`SELECT s.* FROM skills_fts f
|
|
139
|
-
JOIN skills s ON s.skill_id = f.skill_id
|
|
140
|
-
WHERE skills_fts MATCH ?
|
|
141
|
-
ORDER BY bm25(skills_fts) LIMIT ?`,
|
|
142
|
-
)
|
|
143
|
-
.all(query, k) as SkillRow[];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function findExactMatch(db: Database, query: string): SkillRow | null {
|
|
147
|
-
const cleanQuery = query.trim().toLowerCase();
|
|
148
|
-
return db
|
|
149
|
-
.query(
|
|
150
|
-
`SELECT * FROM skills
|
|
151
|
-
WHERE lower(skill_id) = ?
|
|
152
|
-
OR lower(title) = ?
|
|
153
|
-
OR ' ' || lower(aliases) || ' ' LIKE ?`,
|
|
154
|
-
)
|
|
155
|
-
.get(cleanQuery, cleanQuery, `% ${cleanQuery} %`) as SkillRow | null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function getIndexMeta(db: Database, key: string): string | null {
|
|
159
|
-
const row = db
|
|
160
|
-
.query("SELECT value FROM index_meta WHERE key = ?")
|
|
161
|
-
.get(key) as { value: string } | null;
|
|
162
|
-
return row ? row.value : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function setIndexMeta(db: Database, key: string, value: string): void {
|
|
166
|
-
db.run(
|
|
167
|
-
`INSERT INTO index_meta (key, value) VALUES (?, ?)
|
|
168
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
169
|
-
[key, value],
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function upsertVector(
|
|
174
|
-
db: Database,
|
|
175
|
-
skillId: string,
|
|
176
|
-
contentSha256: string,
|
|
177
|
-
embeddingFingerprint: string,
|
|
178
|
-
vec: Float32Array,
|
|
179
|
-
): void {
|
|
180
|
-
db.run(
|
|
181
|
-
`INSERT INTO vectors (skill_id, content_sha256, embedding_fingerprint, dim, vec) VALUES (?, ?, ?, ?, ?)
|
|
182
|
-
ON CONFLICT(skill_id) DO UPDATE SET
|
|
183
|
-
content_sha256 = excluded.content_sha256,
|
|
184
|
-
embedding_fingerprint = excluded.embedding_fingerprint,
|
|
185
|
-
dim = excluded.dim,
|
|
186
|
-
vec = excluded.vec`,
|
|
187
|
-
[skillId, contentSha256, embeddingFingerprint, vec.length, new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength)],
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Skills with no usable stored vector: none at all, content changed since
|
|
193
|
-
* embedding, or embedded at a different dimension than currently configured.
|
|
194
|
-
*/
|
|
195
|
-
export function skillsNeedingVectors(db: Database, dimension: number, embeddingFingerprint: string): SkillRow[] {
|
|
196
|
-
return db
|
|
197
|
-
.query(
|
|
198
|
-
`SELECT s.* FROM skills s
|
|
199
|
-
LEFT JOIN vectors v ON v.skill_id = s.skill_id
|
|
200
|
-
AND v.content_sha256 = s.content_sha256
|
|
201
|
-
AND v.dim = ?
|
|
202
|
-
AND v.embedding_fingerprint = ?
|
|
203
|
-
WHERE v.skill_id IS NULL`,
|
|
204
|
-
)
|
|
205
|
-
.all(dimension, embeddingFingerprint) as SkillRow[];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function cosine(a: Float32Array, b: Float32Array): number {
|
|
209
|
-
if (a.length !== b.length) return 0;
|
|
210
|
-
let dot = 0;
|
|
211
|
-
let normA = 0;
|
|
212
|
-
let normB = 0;
|
|
213
|
-
for (let i = 0; i < a.length; i++) {
|
|
214
|
-
dot += a[i]! * b[i]!;
|
|
215
|
-
normA += a[i]! * a[i]!;
|
|
216
|
-
normB += b[i]! * b[i]!;
|
|
217
|
-
}
|
|
218
|
-
if (normA === 0 || normB === 0) return 0;
|
|
219
|
-
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Brute-force cosine over every stored vector (vault is ~100 skills; no ANN). */
|
|
223
|
-
export function vectorTopK(db: Database, query: Float32Array, k: number): SkillRow[] {
|
|
224
|
-
const rows = db
|
|
225
|
-
.query(
|
|
226
|
-
`SELECT s.skill_id, s.title, s.description, s.aliases, s.content_sha256, v.vec
|
|
227
|
-
FROM vectors v JOIN skills s ON s.skill_id = v.skill_id`,
|
|
228
|
-
)
|
|
229
|
-
.all() as (SkillRow & { vec: Uint8Array })[];
|
|
230
|
-
return rows
|
|
231
|
-
.map(({ vec, ...row }) => ({
|
|
232
|
-
row,
|
|
233
|
-
score: cosine(query, new Float32Array(vec.buffer, vec.byteOffset, vec.byteLength / 4)),
|
|
234
|
-
}))
|
|
235
|
-
.sort((a, b) => b.score - a.score)
|
|
236
|
-
.slice(0, k)
|
|
237
|
-
.map((r) => r.row);
|
|
238
|
-
}
|
package/src/db.ts
DELETED
package/src/deployment.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import packageJson from "../package.json" with { type: "json" };
|
|
2
|
-
import { expandHome } from "./config";
|
|
3
|
-
import type { Config } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface DeploymentIdentity {
|
|
6
|
-
version: string;
|
|
7
|
-
runtime: "host" | "docker";
|
|
8
|
-
image_variant: "full" | "slim" | null;
|
|
9
|
-
vault_path: string;
|
|
10
|
-
state_dir: string;
|
|
11
|
-
inference_mode: Config["inference"]["mode"];
|
|
12
|
-
local_embedding_bundle: string | null;
|
|
13
|
-
remote_embedding_configured: boolean;
|
|
14
|
-
remote_reranker_configured: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function describeDeployment(
|
|
18
|
-
config: Config,
|
|
19
|
-
environment: Record<string, string | undefined> = process.env,
|
|
20
|
-
): DeploymentIdentity {
|
|
21
|
-
const runtime = environment.RUNNING_IN_DOCKER === "true" ? "docker" : "host";
|
|
22
|
-
const variant = environment.SKILLMUX_IMAGE_VARIANT;
|
|
23
|
-
const imageVariant = runtime === "docker" && (variant === "full" || variant === "slim")
|
|
24
|
-
? variant
|
|
25
|
-
: null;
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
version: packageJson.version,
|
|
29
|
-
runtime,
|
|
30
|
-
image_variant: imageVariant,
|
|
31
|
-
vault_path: expandHome(config.vault_path),
|
|
32
|
-
state_dir: expandHome(config.state_dir),
|
|
33
|
-
inference_mode: config.inference.mode,
|
|
34
|
-
local_embedding_bundle: config.inference.mode === "local" ? config.inference.bundle : null,
|
|
35
|
-
remote_embedding_configured: config.inference.mode === "remote",
|
|
36
|
-
remote_reranker_configured:
|
|
37
|
-
config.inference.mode === "remote" && !!config.inference.reranker,
|
|
38
|
-
};
|
|
39
|
-
}
|
package/src/doctor.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { createClients, RemoteInferenceError } from "./clients";
|
|
3
|
-
import { embeddingDimension, expandHome, isLoopbackBindHost } from "./config";
|
|
4
|
-
import { describeDeployment, type DeploymentIdentity } from "./deployment";
|
|
5
|
-
import { parseManifest, resolveManifestPath, validateManifest } from "./manifest";
|
|
6
|
-
import { readSkillmuxMarker } from "./sync";
|
|
7
|
-
import type { Config } from "./types";
|
|
8
|
-
import { findShadowedSkills } from "./vault";
|
|
9
|
-
|
|
10
|
-
export interface DoctorCheck {
|
|
11
|
-
name: string;
|
|
12
|
-
ok: boolean;
|
|
13
|
-
detail: string;
|
|
14
|
-
failure_kind?: "configuration" | "availability" | "protocol" | "unexpected";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DoctorReport {
|
|
18
|
-
mode: Config["inference"]["mode"];
|
|
19
|
-
capability: "hybrid" | "lexical-only" | "unavailable";
|
|
20
|
-
retrieval_capability: "lexical" | "hybrid" | "reranked";
|
|
21
|
-
version: DeploymentIdentity["version"];
|
|
22
|
-
runtime: DeploymentIdentity["runtime"];
|
|
23
|
-
image_variant: DeploymentIdentity["image_variant"];
|
|
24
|
-
vault_path: DeploymentIdentity["vault_path"];
|
|
25
|
-
state_dir: DeploymentIdentity["state_dir"];
|
|
26
|
-
inference_mode: DeploymentIdentity["inference_mode"];
|
|
27
|
-
local_embedding_bundle: DeploymentIdentity["local_embedding_bundle"];
|
|
28
|
-
remote_embedding_configured: DeploymentIdentity["remote_embedding_configured"];
|
|
29
|
-
remote_reranker_configured: DeploymentIdentity["remote_reranker_configured"];
|
|
30
|
-
checks: DoctorCheck[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export { describeDeployment };
|
|
34
|
-
|
|
35
|
-
export async function diagnose(
|
|
36
|
-
config: Config,
|
|
37
|
-
environment: Record<string, string | undefined> = process.env,
|
|
38
|
-
sources: Record<string, "default" | "toml" | "environment" | "admin"> = {},
|
|
39
|
-
): Promise<DoctorReport> {
|
|
40
|
-
const checks: DoctorCheck[] = [];
|
|
41
|
-
const envOverrides = config.config?.environment_overrides !== false;
|
|
42
|
-
checks.push({
|
|
43
|
-
name: "config_authority",
|
|
44
|
-
ok: true,
|
|
45
|
-
detail: envOverrides
|
|
46
|
-
? "environment overrides enabled"
|
|
47
|
-
: "TOML authoritative (environment overrides disabled)",
|
|
48
|
-
});
|
|
49
|
-
for (const [key, source] of Object.entries(sources)) {
|
|
50
|
-
checks.push({ name: `config_source:${key}`, ok: true, detail: source });
|
|
51
|
-
}
|
|
52
|
-
checks.push({ name: "vault", ok: existsSync(expandHome(config.vault_path)), detail: expandHome(config.vault_path) });
|
|
53
|
-
|
|
54
|
-
// SMX-91: `serve --transport http` itself refuses to start over this combination
|
|
55
|
-
// (assertSafeBindPosture in server.ts) unless SKILLMUX_ALLOW_INSECURE_BIND is set —
|
|
56
|
-
// surface it here too so it's visible without having to start the HTTP server first.
|
|
57
|
-
// An operator who has already set that env var has made an informed choice, so
|
|
58
|
-
// doctor treats it the same way the server does (ok, not a standing failure) —
|
|
59
|
-
// it doesn't re-litigate a decision the server itself already accepted.
|
|
60
|
-
if (config.server) {
|
|
61
|
-
const hostname = config.server.hostname ?? "127.0.0.1";
|
|
62
|
-
const insecureBindAcknowledged = environment.SKILLMUX_ALLOW_INSECURE_BIND === "true";
|
|
63
|
-
const bindIsSafe = isLoopbackBindHost(hostname) || config.server.auth_enabled || insecureBindAcknowledged;
|
|
64
|
-
checks.push({
|
|
65
|
-
name: "server_bind_posture",
|
|
66
|
-
ok: bindIsSafe,
|
|
67
|
-
detail: !bindIsSafe
|
|
68
|
-
? `${hostname} is reachable beyond this machine with auth_enabled=false — ` +
|
|
69
|
-
"MCP tools and /stats would be open to anyone who can reach this port; " +
|
|
70
|
-
"set server.auth_enabled=true, bind a loopback hostname, or set SKILLMUX_ALLOW_INSECURE_BIND=true"
|
|
71
|
-
: insecureBindAcknowledged && !isLoopbackBindHost(hostname) && !config.server.auth_enabled
|
|
72
|
-
? `${hostname}, auth_enabled=false, acknowledged via SKILLMUX_ALLOW_INSECURE_BIND`
|
|
73
|
-
: `${hostname}, auth_enabled=${config.server.auth_enabled}`,
|
|
74
|
-
failure_kind: bindIsSafe ? undefined : "configuration",
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const localPath of config.local_vault_paths) {
|
|
79
|
-
const expanded = expandHome(localPath);
|
|
80
|
-
checks.push({ name: `local_vault:${localPath}`, ok: existsSync(expanded), detail: expanded });
|
|
81
|
-
|
|
82
|
-
const strayManifest = resolveManifestPath(expanded);
|
|
83
|
-
if (strayManifest) {
|
|
84
|
-
checks.push({
|
|
85
|
-
name: `local_vault_manifest:${localPath}`,
|
|
86
|
-
ok: false,
|
|
87
|
-
detail: `stray manifest at ${strayManifest} — skillmux.toml only ever lives in vault_path, never in local_vault_paths`,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const marker = readSkillmuxMarker(expanded);
|
|
92
|
-
const currentVaultPath = expandHome(config.vault_path);
|
|
93
|
-
if (!marker || marker.role !== "local_vault") {
|
|
94
|
-
checks.push({
|
|
95
|
-
name: `local_vault_marker:${localPath}`,
|
|
96
|
-
ok: false,
|
|
97
|
-
detail: `no marker — run: skillmux local-vault init "${expanded}"`,
|
|
98
|
-
});
|
|
99
|
-
} else if (marker.vault_path !== currentVaultPath) {
|
|
100
|
-
checks.push({
|
|
101
|
-
name: `local_vault_marker:${localPath}`,
|
|
102
|
-
ok: false,
|
|
103
|
-
detail: `marker recorded vault_path ${marker.vault_path}, currently configured vault_path is ${currentVaultPath} — drift, re-run skillmux local-vault init`,
|
|
104
|
-
});
|
|
105
|
-
} else {
|
|
106
|
-
checks.push({ name: `local_vault_marker:${localPath}`, ok: true, detail: expanded });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const shadow of findShadowedSkills(expandHome(config.vault_path), config.local_vault_paths.map(expandHome))) {
|
|
111
|
-
checks.push({
|
|
112
|
-
name: `shadowed:${shadow.skill_id}`,
|
|
113
|
-
ok: true,
|
|
114
|
-
detail: `served from ${shadow.winner}; shadows ${shadow.shadowed.join(", ")}`,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const vaultPath = expandHome(config.vault_path);
|
|
119
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
120
|
-
if (!manifestPath) {
|
|
121
|
-
checks.push({ name: "manifest", ok: true, detail: "not yet initialized" });
|
|
122
|
-
} else {
|
|
123
|
-
try {
|
|
124
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
125
|
-
validateManifest(manifest, vaultPath, config.local_vault_paths.map(expandHome));
|
|
126
|
-
checks.push({ name: "manifest", ok: true, detail: manifestPath });
|
|
127
|
-
} catch (error) {
|
|
128
|
-
checks.push({
|
|
129
|
-
name: `manifest:${manifestPath}`,
|
|
130
|
-
ok: false,
|
|
131
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
mkdirSync(expandHome(config.state_dir), { recursive: true });
|
|
138
|
-
const probe = Bun.file(expandHome(`${config.state_dir}/.doctor`));
|
|
139
|
-
await probe.write("");
|
|
140
|
-
await probe.delete();
|
|
141
|
-
checks.push({ name: "state", ok: true, detail: expandHome(config.state_dir) });
|
|
142
|
-
} catch (error) {
|
|
143
|
-
checks.push({ name: "state", ok: false, detail: String(error) });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (config.inference.mode === "local") {
|
|
147
|
-
try {
|
|
148
|
-
mkdirSync(expandHome(config.inference.models_dir), { recursive: true });
|
|
149
|
-
checks.push({ name: "models", ok: true, detail: expandHome(config.inference.models_dir) });
|
|
150
|
-
} catch (error) {
|
|
151
|
-
checks.push({ name: "models", ok: false, detail: String(error) });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const inferenceFailure = (error: unknown): Pick<DoctorCheck, "detail" | "failure_kind"> =>
|
|
156
|
-
error instanceof RemoteInferenceError
|
|
157
|
-
? { detail: error.message, failure_kind: error.kind }
|
|
158
|
-
: { detail: "unexpected inference failure", failure_kind: "unexpected" };
|
|
159
|
-
|
|
160
|
-
const deployment = describeDeployment(config, environment);
|
|
161
|
-
const lexicalOnlySlim = deployment.image_variant === "slim" && config.inference.mode === "local";
|
|
162
|
-
if (lexicalOnlySlim) {
|
|
163
|
-
checks.push({
|
|
164
|
-
name: "retrieval",
|
|
165
|
-
ok: true,
|
|
166
|
-
detail: "lexical retrieval; Configure remote embeddings for hybrid retrieval",
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
const clients = createClients(config);
|
|
170
|
-
try {
|
|
171
|
-
const vectors = await clients.embed(["skill router diagnostic"]);
|
|
172
|
-
const actualDimension = vectors[0]?.length ?? 0;
|
|
173
|
-
checks.push({
|
|
174
|
-
name: "embedding",
|
|
175
|
-
ok: actualDimension === embeddingDimension(config),
|
|
176
|
-
detail: `dimension ${actualDimension}`,
|
|
177
|
-
});
|
|
178
|
-
} catch (error) {
|
|
179
|
-
checks.push({ name: "embedding", ok: false, ...inferenceFailure(error) });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (clients.rerank) {
|
|
183
|
-
try {
|
|
184
|
-
const scores = await clients.rerank("skill router diagnostic", [
|
|
185
|
-
{ skill_id: "doctor", text: "Routes a task to an appropriate skill." },
|
|
186
|
-
]);
|
|
187
|
-
checks.push({ name: "reranker", ok: scores.length === 1 && Number.isFinite(scores[0]), detail: "one finite score" });
|
|
188
|
-
} catch (error) {
|
|
189
|
-
checks.push({ name: "reranker", ok: false, ...inferenceFailure(error) });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const inferenceReady = checks.some((check) => check.name === "embedding" && check.ok);
|
|
195
|
-
const rerankerReady = checks.some((check) => check.name === "reranker" && check.ok);
|
|
196
|
-
const coreReady = checks.some((check) => check.name === "vault" && check.ok)
|
|
197
|
-
&& checks.some((check) => check.name === "state" && check.ok);
|
|
198
|
-
return {
|
|
199
|
-
...deployment,
|
|
200
|
-
mode: config.inference.mode,
|
|
201
|
-
capability: !coreReady ? "unavailable" : inferenceReady ? "hybrid" : "lexical-only",
|
|
202
|
-
retrieval_capability: rerankerReady ? "reranked" : inferenceReady ? "hybrid" : "lexical",
|
|
203
|
-
checks,
|
|
204
|
-
};
|
|
205
|
-
}
|