@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/types.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
export interface RecallConfig {
|
|
2
|
-
k_lexical: number;
|
|
3
|
-
k_vector: number;
|
|
4
|
-
k_rerank?: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface OutputConfig {
|
|
8
|
-
top_k: number;
|
|
9
|
-
max_top_k: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Thresholds {
|
|
13
|
-
/** @deprecated Use output.top_k instead */
|
|
14
|
-
candidate_limit?: number;
|
|
15
|
-
match_score?: number;
|
|
16
|
-
match_margin?: number;
|
|
17
|
-
candidate_floor?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type ONNXDevice =
|
|
21
|
-
| "cpu"
|
|
22
|
-
| "auto"
|
|
23
|
-
| "gpu"
|
|
24
|
-
| "wasm"
|
|
25
|
-
| "webgpu"
|
|
26
|
-
| "cuda"
|
|
27
|
-
| "dml"
|
|
28
|
-
| "coreml"
|
|
29
|
-
| "webnn"
|
|
30
|
-
| "webnn-npu"
|
|
31
|
-
| "webnn-gpu"
|
|
32
|
-
| "webnn-cpu";
|
|
33
|
-
|
|
34
|
-
export type ONNXDtype =
|
|
35
|
-
| "q8"
|
|
36
|
-
| "auto"
|
|
37
|
-
| "fp32"
|
|
38
|
-
| "fp16"
|
|
39
|
-
| "int8"
|
|
40
|
-
| "uint8"
|
|
41
|
-
| "q4"
|
|
42
|
-
| "bnb4"
|
|
43
|
-
| "q4f16"
|
|
44
|
-
| "q2"
|
|
45
|
-
| "q2f16"
|
|
46
|
-
| "q1"
|
|
47
|
-
| "q1f16";
|
|
48
|
-
|
|
49
|
-
export interface ModelConfig {
|
|
50
|
-
model: string;
|
|
51
|
-
device?: ONNXDevice;
|
|
52
|
-
dtype?: ONNXDtype;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface LocalInferenceConfig {
|
|
56
|
-
mode: "local";
|
|
57
|
-
bundle: string;
|
|
58
|
-
models_dir: string;
|
|
59
|
-
embedding: ModelConfig & { dimension: number };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface RemoteEmbeddingConfig {
|
|
63
|
-
provider: "openai";
|
|
64
|
-
endpoint: string;
|
|
65
|
-
model: string;
|
|
66
|
-
dimension: number;
|
|
67
|
-
api_key_env?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface RemoteRerankerConfig {
|
|
71
|
-
adapter: "jina-v1" | "bifrost-v1";
|
|
72
|
-
endpoint: string;
|
|
73
|
-
model: string;
|
|
74
|
-
api_key_env?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface RemoteInferenceConfig {
|
|
78
|
-
mode: "remote";
|
|
79
|
-
timeout_ms: number;
|
|
80
|
-
embedding: RemoteEmbeddingConfig;
|
|
81
|
-
reranker?: RemoteRerankerConfig;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type InferenceConfig = LocalInferenceConfig | RemoteInferenceConfig;
|
|
85
|
-
|
|
86
|
-
export interface RateLimitConfig {
|
|
87
|
-
enabled: boolean;
|
|
88
|
-
requests_per_minute: number;
|
|
89
|
-
trust_proxy?: boolean;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface AdminConfig {
|
|
93
|
-
enabled: boolean;
|
|
94
|
-
token_env: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface ServerConfig {
|
|
98
|
-
auth_enabled: boolean;
|
|
99
|
-
auth_token_env: string;
|
|
100
|
-
allowed_origins: string[];
|
|
101
|
-
hostname?: string;
|
|
102
|
-
rate_limit?: RateLimitConfig;
|
|
103
|
-
admin?: AdminConfig;
|
|
104
|
-
max_body_bytes?: number;
|
|
105
|
-
max_concurrent_requests?: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ConfigPolicy {
|
|
109
|
-
environment_overrides?: boolean;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface EgressConfig {
|
|
113
|
-
allowed_hosts?: string[];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface AuditConfig {
|
|
117
|
-
/** Age in days beyond which audit rows are pruned. 0 disables pruning. */
|
|
118
|
-
retention_days: number;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface Config {
|
|
122
|
-
config?: ConfigPolicy;
|
|
123
|
-
vault_path: string;
|
|
124
|
-
local_vault_paths: string[];
|
|
125
|
-
state_dir: string;
|
|
126
|
-
recall: RecallConfig;
|
|
127
|
-
output: OutputConfig;
|
|
128
|
-
inference: InferenceConfig;
|
|
129
|
-
server?: ServerConfig;
|
|
130
|
-
audit?: AuditConfig;
|
|
131
|
-
egress?: EgressConfig;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface RankedCandidate {
|
|
135
|
-
rank: number;
|
|
136
|
-
skill_id: string;
|
|
137
|
-
description: string;
|
|
138
|
-
score: number | null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export type RetrievalCapability = "exact" | "reranked" | "hybrid" | "lexical";
|
|
142
|
-
|
|
143
|
-
export type DegradationReason =
|
|
144
|
-
| "embedding_timeout"
|
|
145
|
-
| "embedding_unavailable"
|
|
146
|
-
| "embedding_protocol_error"
|
|
147
|
-
| "reranker_timeout"
|
|
148
|
-
| "reranker_unavailable"
|
|
149
|
-
| "reranker_protocol_error";
|
|
150
|
-
|
|
151
|
-
export interface ResolveResult {
|
|
152
|
-
request_id: string;
|
|
153
|
-
retrieval: RetrievalCapability;
|
|
154
|
-
degraded_from?: "reranked" | "hybrid";
|
|
155
|
-
degradation_reason?: DegradationReason;
|
|
156
|
-
candidates: RankedCandidate[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface ResolveSkillInput {
|
|
160
|
-
query: string;
|
|
161
|
-
top_k?: number;
|
|
162
|
-
/** Test/ops escape hatch: use lexical retrieval only. Not exposed on the MCP wire. */
|
|
163
|
-
forceLexical?: boolean;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export interface FetchSkillInput {
|
|
167
|
-
skill_id: string;
|
|
168
|
-
request_id?: string;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface FetchSkillResult {
|
|
172
|
-
skill_id: string;
|
|
173
|
-
title: string;
|
|
174
|
-
content_sha256: string;
|
|
175
|
-
body: string;
|
|
176
|
-
files: string[];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export interface AuditCandidate {
|
|
180
|
-
skill_id: string;
|
|
181
|
-
score: number | null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export interface AuditRow {
|
|
185
|
-
id: number;
|
|
186
|
-
ts: string;
|
|
187
|
-
/** Null for rows written before request_id existed (AC4). */
|
|
188
|
-
request_id: string | null;
|
|
189
|
-
query: string;
|
|
190
|
-
retrieval: RetrievalCapability;
|
|
191
|
-
degraded_from?: "reranked" | "hybrid" | null;
|
|
192
|
-
degradation_reason?: DegradationReason | null;
|
|
193
|
-
candidates: AuditCandidate[];
|
|
194
|
-
latency_ms: number;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export interface FetchAuditRow {
|
|
198
|
-
id: number;
|
|
199
|
-
ts: string;
|
|
200
|
-
skill_id: string;
|
|
201
|
-
/** Exactly as supplied by the caller, including an unknown value. Null when the caller sent none. */
|
|
202
|
-
request_id: string | null;
|
|
203
|
-
/** Null when the fetch is uncorrelated: no request_id, an unknown/malformed one, or a pruned resolve row. */
|
|
204
|
-
resolve_audit_id: number | null;
|
|
205
|
-
/** Rank of skill_id in the correlated resolve's shortlist. Null when uncorrelated or absent from the shortlist. */
|
|
206
|
-
rank_at_resolve: number | null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export interface Clients {
|
|
210
|
-
embed(texts: string[]): Promise<Float32Array[]>;
|
|
211
|
-
rerank?: (query: string, docs: { skill_id: string; text: string }[]) => Promise<number[]>;
|
|
212
|
-
}
|
package/src/vault.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
3
|
-
|
|
4
|
-
export const SKILL_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,127}$/;
|
|
5
|
-
|
|
6
|
-
/** Provenance sidecar filename (see provenance.ts). Kept here, not re-imported from
|
|
7
|
-
* provenance.ts, so listSupportingFiles can exclude it without a circular import. */
|
|
8
|
-
export const SKILLMUX_ORIGIN_FILENAME = ".skillmux-origin";
|
|
9
|
-
|
|
10
|
-
export interface VaultSkill {
|
|
11
|
-
skill_id: string;
|
|
12
|
-
title: string;
|
|
13
|
-
description: string;
|
|
14
|
-
aliases: string[];
|
|
15
|
-
body: string;
|
|
16
|
-
content_sha256: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function sha256Hex(data: string | Uint8Array): string {
|
|
20
|
-
return new Bun.CryptoHasher("sha256").update(data).digest("hex");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface Frontmatter {
|
|
24
|
-
name?: unknown;
|
|
25
|
-
description?: unknown;
|
|
26
|
-
aliases?: unknown;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function parseSkillMd(skillId: string, raw: string): VaultSkill {
|
|
30
|
-
let fm: Frontmatter = {};
|
|
31
|
-
if (raw.startsWith("---\n")) {
|
|
32
|
-
const end = raw.indexOf("\n---", 4);
|
|
33
|
-
if (end === -1) throw new Error(`unterminated frontmatter in ${skillId}/SKILL.md`);
|
|
34
|
-
fm = (Bun.YAML.parse(raw.slice(4, end)) ?? {}) as Frontmatter;
|
|
35
|
-
}
|
|
36
|
-
const aliases = Array.isArray(fm.aliases) ? fm.aliases.map(String) : [];
|
|
37
|
-
return {
|
|
38
|
-
skill_id: skillId,
|
|
39
|
-
title: typeof fm.name === "string" && fm.name.length > 0 ? fm.name : skillId,
|
|
40
|
-
description: typeof fm.description === "string" ? fm.description : "",
|
|
41
|
-
aliases,
|
|
42
|
-
body: raw,
|
|
43
|
-
content_sha256: sha256Hex(raw),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Strict decode: invalid UTF-8 throws instead of silently mangling content. */
|
|
48
|
-
export function decodeUtf8Strict(bytes: Uint8Array): string {
|
|
49
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** SKILL.md itself is read directly (not via listSupportingFiles, which excludes
|
|
53
|
-
* it by name) — so its own symlink-ness must be checked here. Without this, a
|
|
54
|
-
* symlinked SKILL.md reachable via a shared git-backed vault or a hand-edit would
|
|
55
|
-
* have its target's content indexed and delivered to agents as the skill body.
|
|
56
|
-
*
|
|
57
|
-
* The skill directory itself must be checked too, separately from SKILL.md's own
|
|
58
|
-
* leaf check: `lstat` only refuses to follow the *final* path component, so a
|
|
59
|
-
* symlinked skill directory containing a real (non-symlink) SKILL.md file at its
|
|
60
|
-
* target silently passes the leaf check while still resolving straight through
|
|
61
|
-
* to arbitrary host content. */
|
|
62
|
-
export async function readSkill(vaultPath: string, skillId: string): Promise<VaultSkill> {
|
|
63
|
-
const skillDir = join(vaultPath, skillId);
|
|
64
|
-
if (lstatSync(skillDir).isSymbolicLink()) {
|
|
65
|
-
throw new Error(`refusing to read ${skillId}: the skill directory is a symlink`);
|
|
66
|
-
}
|
|
67
|
-
const path = join(skillDir, "SKILL.md");
|
|
68
|
-
if (lstatSync(path).isSymbolicLink()) {
|
|
69
|
-
throw new Error(`refusing to read ${skillId}/SKILL.md: it is a symlink`);
|
|
70
|
-
}
|
|
71
|
-
const bytes = await Bun.file(path).bytes();
|
|
72
|
-
return parseSkillMd(skillId, decodeUtf8Strict(bytes));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function scanVault(
|
|
76
|
-
vaultPath: string,
|
|
77
|
-
onInvalid?: (skillId: string, error: unknown) => void,
|
|
78
|
-
): Promise<VaultSkill[]> {
|
|
79
|
-
const skills: VaultSkill[] = [];
|
|
80
|
-
for (const entry of readdirSync(vaultPath, { withFileTypes: true })) {
|
|
81
|
-
if (!entry.isDirectory() || !SKILL_ID_PATTERN.test(entry.name)) continue;
|
|
82
|
-
try {
|
|
83
|
-
skills.push(await readSkill(vaultPath, entry.name));
|
|
84
|
-
} catch (error) {
|
|
85
|
-
onInvalid?.(entry.name, error);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return skills;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Local overlays checked first (in order), canonical vault_path is the fallback. */
|
|
92
|
-
export function vaultResolutionOrder(vaultPath: string, localVaultPaths: string[]): string[] {
|
|
93
|
-
return [...localVaultPaths, vaultPath];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Which configured root actually backs skillId, per local-overrides-first precedence. */
|
|
97
|
-
export function resolveSkillRoot(skillId: string, vaultPath: string, localVaultPaths: string[]): string | null {
|
|
98
|
-
for (const root of vaultResolutionOrder(vaultPath, localVaultPaths)) {
|
|
99
|
-
if (existsSync(join(root, skillId, "SKILL.md"))) return root;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Scans every configured root and merges by skill_id — first-seen (per resolution order) wins. */
|
|
105
|
-
export async function scanVaults(
|
|
106
|
-
vaultPath: string,
|
|
107
|
-
localVaultPaths: string[],
|
|
108
|
-
onInvalid?: (skillId: string, error: unknown) => void,
|
|
109
|
-
): Promise<VaultSkill[]> {
|
|
110
|
-
const seen = new Set<string>();
|
|
111
|
-
const merged: VaultSkill[] = [];
|
|
112
|
-
for (const root of vaultResolutionOrder(vaultPath, localVaultPaths)) {
|
|
113
|
-
if (!existsSync(root)) continue;
|
|
114
|
-
for (const skill of await scanVault(root, onInvalid)) {
|
|
115
|
-
if (seen.has(skill.skill_id)) continue;
|
|
116
|
-
seen.add(skill.skill_id);
|
|
117
|
-
merged.push(skill);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return merged;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface ShadowedSkill {
|
|
124
|
-
skill_id: string;
|
|
125
|
-
winner: string;
|
|
126
|
-
shadowed: string[];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** skill_ids present in more than one configured root — every root after the winner is silently shadowed. */
|
|
130
|
-
export function findShadowedSkills(vaultPath: string, localVaultPaths: string[]): ShadowedSkill[] {
|
|
131
|
-
const rootsBySkillId = new Map<string, string[]>();
|
|
132
|
-
for (const root of vaultResolutionOrder(vaultPath, localVaultPaths)) {
|
|
133
|
-
if (!existsSync(root)) continue;
|
|
134
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
135
|
-
if (!entry.isDirectory() || !SKILL_ID_PATTERN.test(entry.name)) continue;
|
|
136
|
-
if (!existsSync(join(root, entry.name, "SKILL.md"))) continue;
|
|
137
|
-
const roots = rootsBySkillId.get(entry.name) ?? [];
|
|
138
|
-
roots.push(root);
|
|
139
|
-
rootsBySkillId.set(entry.name, roots);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const shadowed: ShadowedSkill[] = [];
|
|
143
|
-
for (const [skillId, roots] of rootsBySkillId) {
|
|
144
|
-
if (roots.length > 1) shadowed.push({ skill_id: skillId, winner: roots[0]!, shadowed: roots.slice(1) });
|
|
145
|
-
}
|
|
146
|
-
return shadowed.sort((a, b) => a.skill_id.localeCompare(b.skill_id));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Relative paths of everything under the skill dir except SKILL.md and the
|
|
150
|
-
* provenance sidecar, sorted. Symlinks are excluded rather than followed —
|
|
151
|
-
* install/sync already refuse a skill containing one, but a symlink can still
|
|
152
|
-
* reach the vault directly (a shared git-backed vault pulled in, or a hand-edit),
|
|
153
|
-
* and this function otherwise feeds skill content straight into content
|
|
154
|
-
* scanning (scan.ts) and drift hashing (provenance.ts). `skillId` must be a
|
|
155
|
-
* single path segment — not the stricter SKILL_ID_PATTERN, since scanPath's
|
|
156
|
-
* ad-hoc single-directory mode legitimately passes an arbitrary, not-yet-
|
|
157
|
-
* normalized directory name — so `..`/`.`/a path separator is rejected to
|
|
158
|
-
* keep the walk inside `vaultPath` without breaking that mode. */
|
|
159
|
-
export function listSupportingFiles(vaultPath: string, skillId: string): string[] {
|
|
160
|
-
if (skillId === "" || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\")) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
const root = join(vaultPath, skillId);
|
|
164
|
-
// A symlinked skill directory must not be walked into: readdirSync follows a
|
|
165
|
-
// symlinked path argument (unlike the entry.isSymbolicLink() check below, which
|
|
166
|
-
// only applies to entries *found by* the walk), so without this a symlinked
|
|
167
|
-
// `root` would return the target directory's file listing straight through.
|
|
168
|
-
if (!existsSync(root) || lstatSync(root).isSymbolicLink()) return [];
|
|
169
|
-
const files: string[] = [];
|
|
170
|
-
const walk = (dir: string) => {
|
|
171
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
172
|
-
if (entry.isSymbolicLink()) continue;
|
|
173
|
-
const abs = join(dir, entry.name);
|
|
174
|
-
if (entry.isDirectory()) walk(abs);
|
|
175
|
-
else if (statSync(abs).isFile()) {
|
|
176
|
-
const rel = relative(root, abs);
|
|
177
|
-
if (rel !== "SKILL.md" && rel !== SKILLMUX_ORIGIN_FILENAME) files.push(rel);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
walk(root);
|
|
182
|
-
return files.sort();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function getVaultMaxMtime(vaultPath: string): number {
|
|
186
|
-
try {
|
|
187
|
-
let maxMtime = statSync(vaultPath).mtimeMs;
|
|
188
|
-
const entries = readdirSync(vaultPath, { withFileTypes: true });
|
|
189
|
-
for (const entry of entries) {
|
|
190
|
-
if (entry.isDirectory() && SKILL_ID_PATTERN.test(entry.name)) {
|
|
191
|
-
try {
|
|
192
|
-
const folderPath = join(vaultPath, entry.name);
|
|
193
|
-
const folderMtime = statSync(folderPath).mtimeMs;
|
|
194
|
-
maxMtime = Math.max(maxMtime, folderMtime);
|
|
195
|
-
|
|
196
|
-
const fileMtime = statSync(join(folderPath, "SKILL.md")).mtimeMs;
|
|
197
|
-
maxMtime = Math.max(maxMtime, fileMtime);
|
|
198
|
-
} catch {
|
|
199
|
-
// Ignore deleted files
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return maxMtime;
|
|
204
|
-
} catch {
|
|
205
|
-
return 0;
|
|
206
|
-
}
|
|
207
|
-
}
|