skillwiki 0.9.62 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5TBIMLTZ.js +343 -0
- package/dist/chunk-C5OLZRRM.js +357 -0
- package/dist/{chunk-2PENIQ3A.js → chunk-DR7KFHNH.js} +793 -1983
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-S5ABQCXQ.js +580 -0
- package/dist/cli.js +1820 -665
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-SILEUQEV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/.codex-plugin/plugin.json +3 -3
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +29 -2
- package/skills/skills/wiki-archive/SKILL.md +18 -4
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/skills/wiki-remove/SKILL.md +96 -0
- package/skills/using-skillwiki/SKILL.md +29 -2
- package/skills/wiki-archive/SKILL.md +18 -4
- package/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/wiki-remove/SKILL.md +96 -0
|
@@ -3,334 +3,49 @@ import {
|
|
|
3
3
|
latestFromCache,
|
|
4
4
|
semverGt
|
|
5
5
|
} from "./chunk-7I2TPIV5.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DEDUP_APPLIED: 36,
|
|
46
|
-
PROJECT_NOT_FOUND: 37,
|
|
47
|
-
SYMLINK_FAILED: 38,
|
|
48
|
-
COMPOUND_PROMOTED: 39,
|
|
49
|
-
SKILL_VERSION_MISMATCH: 40,
|
|
50
|
-
INGEST_VALIDATION_FAILED: 41,
|
|
51
|
-
SYNC_PUSH_FAILED: 42,
|
|
52
|
-
SYNC_PULL_FAILED: 43,
|
|
53
|
-
BACKUP_SYNC_FAILED: 44,
|
|
54
|
-
BACKUP_RESTORE_CONFLICTS: 45,
|
|
55
|
-
USAGE: 46,
|
|
56
|
-
BODY_TRUNCATION_GUARD: 47,
|
|
57
|
-
SYNC_LOCK_HELD: 48,
|
|
58
|
-
LOG_APPEND_LOCK_HELD: 49,
|
|
59
|
-
FLEET_MANIFEST_INVALID: 50,
|
|
60
|
-
SENSITIVE_CONTENT_DETECTED: 51,
|
|
61
|
-
FLEET_SATELLITE_HEALTH_FAILED: 52,
|
|
62
|
-
PROTECTED_SNAPSHOTTER_WRITE_BLOCKED: 53
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ../shared/src/json-output.ts
|
|
66
|
-
function ok(data) {
|
|
67
|
-
return { ok: true, data };
|
|
68
|
-
}
|
|
69
|
-
function err(error, detail) {
|
|
70
|
-
return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ../shared/src/schemas.ts
|
|
74
|
-
import { z } from "zod";
|
|
75
|
-
var isoDate = z.string().refine((s) => {
|
|
76
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
|
|
77
|
-
const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
|
|
78
|
-
return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
|
|
79
|
-
}, { message: "must be YYYY-MM-DD" });
|
|
80
|
-
var wikilink = z.string().regex(/^\[\[[^\[\]]+\]\]$/, 'must be "[[name]]"');
|
|
81
|
-
var TypedKnowledgeSchema = z.object({
|
|
82
|
-
title: z.string().min(1),
|
|
83
|
-
aliases: z.array(z.string()).optional(),
|
|
84
|
-
created: isoDate,
|
|
85
|
-
updated: isoDate,
|
|
86
|
-
type: z.enum(["entity", "concept", "comparison", "query"]),
|
|
87
|
-
tags: z.array(z.string()),
|
|
88
|
-
sources: z.array(z.string()).min(1),
|
|
89
|
-
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
90
|
-
contested: z.boolean().optional(),
|
|
91
|
-
contradictions: z.array(z.string()).optional(),
|
|
92
|
-
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
93
|
-
provenance_projects: z.array(wikilink).optional(),
|
|
94
|
-
work_items: z.array(wikilink).optional(),
|
|
95
|
-
stale_ttl: z.number().int().positive().optional()
|
|
96
|
-
}).superRefine((v, ctx) => {
|
|
97
|
-
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
98
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
102
|
-
var RawSourceSchema = z.object({
|
|
103
|
-
title: z.string().min(1).optional(),
|
|
104
|
-
source_url: z.string().nullable(),
|
|
105
|
-
created: isoDate.optional(),
|
|
106
|
-
ingested: isoDate,
|
|
107
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
108
|
-
sha256: sha256Hex.optional(),
|
|
109
|
-
project: wikilink.optional(),
|
|
110
|
-
work_item: wikilink.optional(),
|
|
111
|
-
kind: z.enum(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]).optional()
|
|
112
|
-
}).superRefine((v, ctx) => {
|
|
113
|
-
if (v.work_item !== void 0 && (v.project === void 0 || v.kind === void 0)) {
|
|
114
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "project and kind are required when work_item is set" });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
var WorkItemSchema = z.object({
|
|
118
|
-
title: z.string().min(1),
|
|
119
|
-
aliases: z.array(z.string()).optional(),
|
|
120
|
-
created: isoDate,
|
|
121
|
-
updated: isoDate,
|
|
122
|
-
started: isoDate,
|
|
123
|
-
completed: isoDate.optional(),
|
|
124
|
-
kind: z.enum(["feature", "issue", "refactor", "decision"]),
|
|
125
|
-
status: z.enum(["planned", "in-progress", "completed", "abandoned"]),
|
|
126
|
-
priority: z.enum(["high", "medium", "low"]),
|
|
127
|
-
project: wikilink,
|
|
128
|
-
owner: wikilink.optional(),
|
|
129
|
-
parent: wikilink.optional(),
|
|
130
|
-
related: z.array(wikilink).optional(),
|
|
131
|
-
sources: z.array(z.string()).optional()
|
|
132
|
-
}).superRefine((v, ctx) => {
|
|
133
|
-
if (v.status === "completed" && !v.completed) {
|
|
134
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["completed"], message: "required when status is completed" });
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
var CompoundSchema = z.object({
|
|
138
|
-
title: z.string().min(1),
|
|
139
|
-
aliases: z.array(z.string()).optional(),
|
|
140
|
-
created: isoDate,
|
|
141
|
-
updated: isoDate,
|
|
142
|
-
type: z.enum(["lesson", "pattern", "antipattern", "gotcha"]),
|
|
143
|
-
tags: z.array(z.string()),
|
|
144
|
-
confidence: z.enum(["high", "medium", "low"]),
|
|
145
|
-
contradicts: z.array(z.string()).optional(),
|
|
146
|
-
project: wikilink,
|
|
147
|
-
work_items: z.array(wikilink).min(1),
|
|
148
|
-
promoted_to: wikilink.optional(),
|
|
149
|
-
cssclasses: z.array(z.string()).optional()
|
|
150
|
-
});
|
|
151
|
-
var sessionPinPath = z.string().regex(
|
|
152
|
-
/^(entities|concepts|comparisons|queries|meta)\/.+\.md$/,
|
|
153
|
-
"must reference a typed-knowledge markdown page"
|
|
154
|
-
);
|
|
155
|
-
var SessionPinSchema = z.object({
|
|
156
|
-
title: z.string().min(1),
|
|
157
|
-
path: sessionPinPath,
|
|
158
|
-
scope: z.enum(["global", "project"]),
|
|
159
|
-
project: wikilink.optional(),
|
|
160
|
-
summary: z.string().min(1).optional(),
|
|
161
|
-
updated: isoDate.optional()
|
|
162
|
-
}).superRefine((v, ctx) => {
|
|
163
|
-
if (v.scope === "project" && v.project === void 0) {
|
|
164
|
-
ctx.addIssue({
|
|
165
|
-
code: z.ZodIssueCode.custom,
|
|
166
|
-
path: ["project"],
|
|
167
|
-
message: "project is required when scope is project"
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
var MetaSchema = z.object({
|
|
172
|
-
title: z.string().min(1),
|
|
173
|
-
aliases: z.array(z.string()).optional(),
|
|
174
|
-
created: isoDate,
|
|
175
|
-
updated: isoDate,
|
|
176
|
-
type: z.literal("meta"),
|
|
177
|
-
tags: z.array(z.string()),
|
|
178
|
-
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
179
|
-
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
180
|
-
provenance_projects: z.array(wikilink).optional(),
|
|
181
|
-
generated_by: z.string().min(1).optional(),
|
|
182
|
-
generated_at: z.string().datetime().optional(),
|
|
183
|
-
generated_kind: z.enum(["session-brief"]).optional(),
|
|
184
|
-
meta_kind: z.enum(["session-pins"]).optional(),
|
|
185
|
-
stale_ttl: z.number().int().positive().optional(),
|
|
186
|
-
pins: z.array(SessionPinSchema).optional()
|
|
187
|
-
}).superRefine((v, ctx) => {
|
|
188
|
-
const isGeneratedSessionBrief = v.generated_kind === "session-brief";
|
|
189
|
-
const isSessionPins = v.meta_kind === "session-pins";
|
|
190
|
-
if (isSessionPins && (!v.pins || v.pins.length === 0)) {
|
|
191
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pins"], message: "required when meta_kind is session-pins" });
|
|
192
|
-
}
|
|
193
|
-
if (!isGeneratedSessionBrief && !isSessionPins && (!v.provenance_projects || v.provenance_projects.length < 2)) {
|
|
194
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "meta pages must reference \u22652 projects" });
|
|
195
|
-
}
|
|
196
|
-
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
197
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
var hostId = z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "must be a lowercase host id");
|
|
201
|
-
var endpointName = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/, "must be a hostname-like token");
|
|
202
|
-
var ipAddress = z.string().min(1).regex(/^[0-9a-fA-F:.]+$/, "must be an IP address token");
|
|
203
|
-
var sshAlias = z.string().min(1).regex(/^[A-Za-z0-9_.@-]+$/, "must be an SSH alias token");
|
|
204
|
-
var sshUser = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/, "must be an SSH user token");
|
|
205
|
-
var absolutePath = z.string().min(1).regex(/^\//, "must be an absolute path");
|
|
206
|
-
var FleetAccessProfileSchema = z.object({
|
|
207
|
-
status: z.enum(["local", "configured", "planned", "absent", "unknown"]),
|
|
208
|
-
ssh_aliases: z.array(sshAlias).optional(),
|
|
209
|
-
users: z.array(sshUser).optional(),
|
|
210
|
-
transports: z.array(z.enum(["local", "public-ip", "tailscale", "private-lan"])).min(1)
|
|
211
|
-
}).strict();
|
|
212
|
-
var FleetHostIdentitySchema = z.object({
|
|
213
|
-
hostnames: z.array(endpointName).min(1),
|
|
214
|
-
public_addresses: z.array(ipAddress).optional(),
|
|
215
|
-
private_addresses: z.array(ipAddress).optional(),
|
|
216
|
-
tailscale: z.object({
|
|
217
|
-
node_names: z.array(endpointName).optional(),
|
|
218
|
-
magicdns_names: z.array(endpointName).optional(),
|
|
219
|
-
addresses: z.array(ipAddress).optional()
|
|
220
|
-
}).strict().optional()
|
|
221
|
-
}).strict();
|
|
222
|
-
var FleetSkillwikiSatelliteSchema = z.object({
|
|
223
|
-
enabled: z.boolean(),
|
|
224
|
-
user: sshUser,
|
|
225
|
-
vault_path: absolutePath,
|
|
226
|
-
repo_path: absolutePath,
|
|
227
|
-
ssh_alias: sshAlias,
|
|
228
|
-
scheduler: z.enum(["systemd"]),
|
|
229
|
-
timezone: z.string().min(1).optional(),
|
|
230
|
-
jobs: z.array(z.enum([
|
|
231
|
-
"self-update-check",
|
|
232
|
-
"vault-sync-preflight",
|
|
233
|
-
"agent-memory-trends-daily",
|
|
234
|
-
"session-brief-refresh",
|
|
235
|
-
"health-summary"
|
|
236
|
-
])).min(1),
|
|
237
|
-
cadence: z.object({
|
|
238
|
-
self_update_check: z.literal("every-4-hours").optional(),
|
|
239
|
-
daily_window: z.string().min(1).optional()
|
|
240
|
-
}).strict().optional()
|
|
241
|
-
}).strict();
|
|
242
|
-
var FleetHostSchema = z.object({
|
|
243
|
-
class: z.enum(["dev-macos", "dev-linux", "prod-linux", "unknown"]),
|
|
244
|
-
role: z.enum(["leaf", "snapshotter"]),
|
|
245
|
-
writes_to: z.array(z.enum(["s3", "github"])).min(1),
|
|
246
|
-
protected: z.boolean().optional(),
|
|
247
|
-
identity: FleetHostIdentitySchema,
|
|
248
|
-
access: z.object({
|
|
249
|
-
from: z.record(hostId, FleetAccessProfileSchema).optional()
|
|
250
|
-
}).strict().optional(),
|
|
251
|
-
maintenance: z.object({
|
|
252
|
-
skillwiki_satellite: FleetSkillwikiSatelliteSchema.optional()
|
|
253
|
-
}).strict().optional()
|
|
254
|
-
}).strict();
|
|
255
|
-
var FleetManifestSchema = z.object({
|
|
256
|
-
"$schema": z.string().url().optional(),
|
|
257
|
-
schema_version: z.literal(1),
|
|
258
|
-
vault_remote: z.string().min(1),
|
|
259
|
-
s3_remote: z.string().min(1).optional(),
|
|
260
|
-
hosts: z.record(hostId, FleetHostSchema)
|
|
261
|
-
}).strict().superRefine((v, ctx) => {
|
|
262
|
-
const entries = Object.entries(v.hosts);
|
|
263
|
-
if (entries.length === 0) {
|
|
264
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["hosts"], message: "must contain at least one host" });
|
|
265
|
-
}
|
|
266
|
-
const snapshotters = entries.filter(([, host]) => host.role === "snapshotter").map(([id]) => id);
|
|
267
|
-
if (snapshotters.length !== 1) {
|
|
268
|
-
ctx.addIssue({
|
|
269
|
-
code: z.ZodIssueCode.custom,
|
|
270
|
-
path: ["hosts"],
|
|
271
|
-
message: `must contain exactly one snapshotter host, found ${snapshotters.length}`
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
function detectSchema(fm) {
|
|
276
|
-
const COMPOUND_TYPES = /* @__PURE__ */ new Set(["lesson", "pattern", "antipattern", "gotcha"]);
|
|
277
|
-
if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
|
|
278
|
-
if (fm.type === "meta") return { schema: "meta" };
|
|
279
|
-
if ("type" in fm && "sources" in fm) return { schema: "typed-knowledge" };
|
|
280
|
-
if ("ingested" in fm && ("source_url" in fm || "sha256" in fm)) return { schema: "raw" };
|
|
281
|
-
const RAW_KINDS = /* @__PURE__ */ new Set(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]);
|
|
282
|
-
if ("ingested" in fm && typeof fm.kind === "string" && RAW_KINDS.has(fm.kind)) return { schema: "raw" };
|
|
283
|
-
if ("kind" in fm && "status" in fm) return { schema: "work-item" };
|
|
284
|
-
return { schema: null };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ../shared/src/blocked-hosts.ts
|
|
288
|
-
var METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
289
|
-
"metadata.google.internal",
|
|
290
|
-
"metadata"
|
|
291
|
-
]);
|
|
292
|
-
var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
|
|
293
|
-
function ipv4ToInt(ip) {
|
|
294
|
-
const parts = ip.split(".");
|
|
295
|
-
if (parts.length !== 4) return null;
|
|
296
|
-
let n = 0;
|
|
297
|
-
for (const p of parts) {
|
|
298
|
-
const v = Number(p);
|
|
299
|
-
if (!Number.isInteger(v) || v < 0 || v > 255) return null;
|
|
300
|
-
n = (n << 8) + v;
|
|
301
|
-
}
|
|
302
|
-
return n >>> 0;
|
|
303
|
-
}
|
|
304
|
-
function inRange(ip, baseStr, prefix) {
|
|
305
|
-
const ipN = ipv4ToInt(ip);
|
|
306
|
-
const baseN = ipv4ToInt(baseStr);
|
|
307
|
-
if (ipN === null || baseN === null) return false;
|
|
308
|
-
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
309
|
-
return (ipN & mask) === (baseN & mask);
|
|
310
|
-
}
|
|
311
|
-
function isBlockedHost(host) {
|
|
312
|
-
const lower = host.toLowerCase();
|
|
313
|
-
if (METADATA_HOSTS.has(lower)) return true;
|
|
314
|
-
if (METADATA_IPS.has(host)) return true;
|
|
315
|
-
if (lower === "::1") return true;
|
|
316
|
-
if (lower.startsWith("fe80:")) return true;
|
|
317
|
-
if (ipv4ToInt(host) === null) return false;
|
|
318
|
-
if (inRange(host, "10.0.0.0", 8)) return true;
|
|
319
|
-
if (inRange(host, "172.16.0.0", 12)) return true;
|
|
320
|
-
if (inRange(host, "192.168.0.0", 16)) return true;
|
|
321
|
-
if (inRange(host, "169.254.0.0", 16)) return true;
|
|
322
|
-
if (inRange(host, "127.0.0.0", 8)) return true;
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ../shared/src/error-message.ts
|
|
327
|
-
function getErrorMessage(e) {
|
|
328
|
-
return e instanceof Error ? e.message : String(e);
|
|
329
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
atomicWriteText,
|
|
8
|
+
extractFrontmatter,
|
|
9
|
+
mapWithConcurrency,
|
|
10
|
+
prepareTypedPage,
|
|
11
|
+
readPage,
|
|
12
|
+
readPageCached,
|
|
13
|
+
redactSensitiveContent,
|
|
14
|
+
renderRootIndex,
|
|
15
|
+
resolveReadOnlyVaultRoot,
|
|
16
|
+
scanSensitiveContent,
|
|
17
|
+
scanVault,
|
|
18
|
+
splitFrontmatter,
|
|
19
|
+
vaultIoConcurrency,
|
|
20
|
+
writeRootIndexProjection
|
|
21
|
+
} from "./chunk-IZABIE44.js";
|
|
22
|
+
import {
|
|
23
|
+
CONFIG_KEYS,
|
|
24
|
+
isValidWikiProfileKey,
|
|
25
|
+
loadFleetManifestAndHost,
|
|
26
|
+
parseDotenvFile,
|
|
27
|
+
parseDotenvText,
|
|
28
|
+
profileKey,
|
|
29
|
+
satelliteGateFromFleetLoad,
|
|
30
|
+
snapshotterAliasForLocalHost,
|
|
31
|
+
writeDotenv
|
|
32
|
+
} from "./chunk-S5ABQCXQ.js";
|
|
33
|
+
import {
|
|
34
|
+
CompoundSchema,
|
|
35
|
+
ExitCode,
|
|
36
|
+
MetaSchema,
|
|
37
|
+
RawSourceSchema,
|
|
38
|
+
TypedKnowledgeSchema,
|
|
39
|
+
WorkItemSchema,
|
|
40
|
+
detectSchema,
|
|
41
|
+
err,
|
|
42
|
+
getErrorMessage,
|
|
43
|
+
ok
|
|
44
|
+
} from "./chunk-C5OLZRRM.js";
|
|
330
45
|
|
|
331
46
|
// src/commands/log-append.ts
|
|
332
|
-
import { readFile
|
|
333
|
-
import { join as
|
|
47
|
+
import { readFile, stat } from "fs/promises";
|
|
48
|
+
import { join as join3 } from "path";
|
|
334
49
|
|
|
335
50
|
// src/utils/last-op.ts
|
|
336
51
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
@@ -374,58 +89,12 @@ function clearLastOp(vault) {
|
|
|
374
89
|
}
|
|
375
90
|
}
|
|
376
91
|
|
|
377
|
-
// src/utils/atomic-write.ts
|
|
378
|
-
import { randomBytes } from "crypto";
|
|
379
|
-
import { open, readFile, rename, unlink } from "fs/promises";
|
|
380
|
-
import { basename, dirname, join as join2 } from "path";
|
|
381
|
-
async function readExisting(path) {
|
|
382
|
-
try {
|
|
383
|
-
return await readFile(path, "utf8");
|
|
384
|
-
} catch (error) {
|
|
385
|
-
if (error.code === "ENOENT") return null;
|
|
386
|
-
throw error;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
async function atomicWriteText(path, text) {
|
|
390
|
-
let existing;
|
|
391
|
-
try {
|
|
392
|
-
existing = await readExisting(path);
|
|
393
|
-
} catch (error) {
|
|
394
|
-
return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
|
|
395
|
-
}
|
|
396
|
-
if (existing === text) return ok({ changed: false, existed: true });
|
|
397
|
-
const tmp = join2(
|
|
398
|
-
dirname(path),
|
|
399
|
-
`.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
|
|
400
|
-
);
|
|
401
|
-
try {
|
|
402
|
-
const handle = await open(tmp, "wx");
|
|
403
|
-
try {
|
|
404
|
-
await handle.writeFile(text, "utf8");
|
|
405
|
-
try {
|
|
406
|
-
await handle.sync();
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
409
|
-
} finally {
|
|
410
|
-
await handle.close();
|
|
411
|
-
}
|
|
412
|
-
await rename(tmp, path);
|
|
413
|
-
return ok({ changed: true, existed: existing !== null });
|
|
414
|
-
} catch (error) {
|
|
415
|
-
try {
|
|
416
|
-
await unlink(tmp);
|
|
417
|
-
} catch {
|
|
418
|
-
}
|
|
419
|
-
return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
92
|
// src/utils/log-lock.ts
|
|
424
|
-
import { randomBytes
|
|
93
|
+
import { randomBytes } from "crypto";
|
|
425
94
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
426
|
-
import { join as
|
|
95
|
+
import { join as join2 } from "path";
|
|
427
96
|
function logLockPath(vault) {
|
|
428
|
-
return
|
|
97
|
+
return join2(vault, ".skillwiki", "log-append.lock");
|
|
429
98
|
}
|
|
430
99
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
431
100
|
function readLogLock(path) {
|
|
@@ -441,10 +110,10 @@ async function acquireLogLock(vault, opts = {}) {
|
|
|
441
110
|
const staleMs = opts.staleMs ?? 1e4;
|
|
442
111
|
const reclaimStale = opts.reclaimStale ?? true;
|
|
443
112
|
const path = logLockPath(vault);
|
|
444
|
-
const dir =
|
|
113
|
+
const dir = join2(vault, ".skillwiki");
|
|
445
114
|
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
446
115
|
const deadline = Date.now() + retryMs;
|
|
447
|
-
const ownerToken =
|
|
116
|
+
const ownerToken = randomBytes(16).toString("hex");
|
|
448
117
|
const acquired = (/* @__PURE__ */ new Date()).toISOString();
|
|
449
118
|
const content = JSON.stringify({ pid: process.pid, owner_token: ownerToken, acquired }) + "\n";
|
|
450
119
|
for (; ; ) {
|
|
@@ -487,146 +156,6 @@ function releaseLogLock(handle) {
|
|
|
487
156
|
}
|
|
488
157
|
}
|
|
489
158
|
|
|
490
|
-
// src/utils/sensitive-content.ts
|
|
491
|
-
import { createHash } from "crypto";
|
|
492
|
-
var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
|
|
493
|
-
var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
|
|
494
|
-
var MATCHERS = [
|
|
495
|
-
{
|
|
496
|
-
kind: "private_key",
|
|
497
|
-
re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
kind: "authorization_header",
|
|
501
|
-
re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
|
|
502
|
-
valueGroup: 1
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
kind: "cookie",
|
|
506
|
-
re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
|
|
507
|
-
valueGroup: 1
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
kind: "jwt",
|
|
511
|
-
re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
|
|
512
|
-
valueGroup: 1
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
kind: "provider_key",
|
|
516
|
-
re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
|
|
517
|
-
valueGroup: 1
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
kind: "access_key",
|
|
521
|
-
re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
|
|
522
|
-
valueGroup: 1
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
kind: "access_key",
|
|
526
|
-
re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
|
|
527
|
-
valueGroup: 1
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
kind: "api_key",
|
|
531
|
-
re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
|
|
532
|
-
valueGroup: 1
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
kind: "password",
|
|
536
|
-
re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
|
|
537
|
-
valueGroup: 1
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
kind: "secret",
|
|
541
|
-
re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
|
|
542
|
-
valueGroup: 1
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
kind: "token",
|
|
546
|
-
re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
|
|
547
|
-
valueGroup: 1
|
|
548
|
-
}
|
|
549
|
-
];
|
|
550
|
-
function fingerprint(value) {
|
|
551
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
552
|
-
}
|
|
553
|
-
function lineFor(text, offset) {
|
|
554
|
-
return text.slice(0, offset).split(/\r?\n/).length;
|
|
555
|
-
}
|
|
556
|
-
function redactMarker(kind, value) {
|
|
557
|
-
return `[REDACTED:${kind}:${fingerprint(value)}]`;
|
|
558
|
-
}
|
|
559
|
-
function isSyntheticPlaceholder(value) {
|
|
560
|
-
return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
|
|
561
|
-
}
|
|
562
|
-
function collectMatches(text) {
|
|
563
|
-
const matches = [];
|
|
564
|
-
for (const matcher of MATCHERS) {
|
|
565
|
-
matcher.re.lastIndex = 0;
|
|
566
|
-
for (const m of text.matchAll(matcher.re)) {
|
|
567
|
-
const whole = m[0];
|
|
568
|
-
const start = m.index ?? 0;
|
|
569
|
-
if (REDACTED_RE.test(whole)) continue;
|
|
570
|
-
const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
|
|
571
|
-
if (isSyntheticPlaceholder(value)) continue;
|
|
572
|
-
const valueOffset = whole.lastIndexOf(value);
|
|
573
|
-
const valueStart = start + Math.max(0, valueOffset);
|
|
574
|
-
matches.push({
|
|
575
|
-
start,
|
|
576
|
-
end: start + whole.length,
|
|
577
|
-
valueStart,
|
|
578
|
-
valueEnd: valueStart + value.length,
|
|
579
|
-
kind: matcher.kind
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return matches.sort((a, b) => {
|
|
584
|
-
if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
|
|
585
|
-
return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
function collapseOverlaps(matches) {
|
|
589
|
-
const kept = [];
|
|
590
|
-
for (const match of matches) {
|
|
591
|
-
const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
|
|
592
|
-
if (!overlaps) kept.push(match);
|
|
593
|
-
}
|
|
594
|
-
return kept;
|
|
595
|
-
}
|
|
596
|
-
function scanSensitiveContent(text, opts = {}) {
|
|
597
|
-
return collapseOverlaps(collectMatches(text)).map((match) => {
|
|
598
|
-
const value = text.slice(match.valueStart, match.valueEnd);
|
|
599
|
-
const marker = redactMarker(match.kind, value);
|
|
600
|
-
const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
|
|
601
|
-
const preview = rawPreview.replace(value, marker);
|
|
602
|
-
return {
|
|
603
|
-
file: opts.file,
|
|
604
|
-
line: lineFor(text, match.valueStart),
|
|
605
|
-
kind: match.kind,
|
|
606
|
-
preview,
|
|
607
|
-
fingerprint: fingerprint(value)
|
|
608
|
-
};
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
function redactSensitiveContent(text, opts = {}) {
|
|
612
|
-
const matches = collapseOverlaps(collectMatches(text));
|
|
613
|
-
if (matches.length === 0) return { text, changed: false, findings: [] };
|
|
614
|
-
let out = "";
|
|
615
|
-
let cursor = 0;
|
|
616
|
-
for (const match of matches) {
|
|
617
|
-
const value = text.slice(match.valueStart, match.valueEnd);
|
|
618
|
-
out += text.slice(cursor, match.valueStart);
|
|
619
|
-
out += redactMarker(match.kind, value);
|
|
620
|
-
cursor = match.valueEnd;
|
|
621
|
-
}
|
|
622
|
-
out += text.slice(cursor);
|
|
623
|
-
return {
|
|
624
|
-
text: out,
|
|
625
|
-
changed: out !== text,
|
|
626
|
-
findings: scanSensitiveContent(text, opts)
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
159
|
// src/commands/log-append.ts
|
|
631
160
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
632
161
|
function operationMarker(operationId) {
|
|
@@ -638,7 +167,7 @@ function operationMarker(operationId) {
|
|
|
638
167
|
async function appendWhileLocked(logPath, content, marker) {
|
|
639
168
|
let logText;
|
|
640
169
|
try {
|
|
641
|
-
logText = await
|
|
170
|
+
logText = await readFile(logPath, "utf8");
|
|
642
171
|
} catch {
|
|
643
172
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
644
173
|
}
|
|
@@ -677,7 +206,7 @@ ${appendedContent}
|
|
|
677
206
|
}
|
|
678
207
|
async function runLogAppend(input) {
|
|
679
208
|
try {
|
|
680
|
-
await stat(
|
|
209
|
+
await stat(join3(input.vault, "SCHEMA.md"));
|
|
681
210
|
} catch {
|
|
682
211
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
683
212
|
}
|
|
@@ -706,7 +235,7 @@ async function runLogAppend(input) {
|
|
|
706
235
|
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
707
236
|
}
|
|
708
237
|
const lockHandle = acquired.data;
|
|
709
|
-
const logPath =
|
|
238
|
+
const logPath = join3(input.vault, "log.md");
|
|
710
239
|
let outcome;
|
|
711
240
|
let released;
|
|
712
241
|
try {
|
|
@@ -751,8 +280,8 @@ async function runLogAppend(input) {
|
|
|
751
280
|
|
|
752
281
|
// src/utils/sync-lock.ts
|
|
753
282
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
754
|
-
import { join as
|
|
755
|
-
import { createHash
|
|
283
|
+
import { join as join4 } from "path";
|
|
284
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
756
285
|
function getEnvSessionId() {
|
|
757
286
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
758
287
|
if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
|
|
@@ -765,7 +294,7 @@ function getSessionId() {
|
|
|
765
294
|
}
|
|
766
295
|
function getCwdHash(cwd) {
|
|
767
296
|
const path = cwd || process.cwd();
|
|
768
|
-
const hash =
|
|
297
|
+
const hash = createHash("sha256").update(path).digest("hex");
|
|
769
298
|
return hash.slice(0, 8);
|
|
770
299
|
}
|
|
771
300
|
function getCliSessionId(cwd) {
|
|
@@ -774,7 +303,7 @@ function getCliSessionId(cwd) {
|
|
|
774
303
|
return `cli-${getCwdHash(cwd)}`;
|
|
775
304
|
}
|
|
776
305
|
function lockPath(vault) {
|
|
777
|
-
return
|
|
306
|
+
return join4(vault, ".skillwiki", "sync.lock");
|
|
778
307
|
}
|
|
779
308
|
function readLock(vault) {
|
|
780
309
|
const path = lockPath(vault);
|
|
@@ -793,7 +322,7 @@ function isStale(lock, now) {
|
|
|
793
322
|
}
|
|
794
323
|
function acquireLock(vault, opts = {}) {
|
|
795
324
|
const path = lockPath(vault);
|
|
796
|
-
const dir =
|
|
325
|
+
const dir = join4(vault, ".skillwiki");
|
|
797
326
|
if (!existsSync3(dir)) {
|
|
798
327
|
mkdirSync3(dir, { recursive: true });
|
|
799
328
|
}
|
|
@@ -864,7 +393,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
864
393
|
}
|
|
865
394
|
}
|
|
866
395
|
function acquireOwnedSyncLock(vault, opts) {
|
|
867
|
-
const ownerToken =
|
|
396
|
+
const ownerToken = randomBytes2(16).toString("hex");
|
|
868
397
|
const sessionId = `publish-${process.pid}-${ownerToken.slice(0, 12)}`;
|
|
869
398
|
const now = /* @__PURE__ */ new Date();
|
|
870
399
|
const lock = {
|
|
@@ -878,7 +407,7 @@ function acquireOwnedSyncLock(vault, opts) {
|
|
|
878
407
|
};
|
|
879
408
|
const path = lockPath(vault);
|
|
880
409
|
try {
|
|
881
|
-
mkdirSync3(
|
|
410
|
+
mkdirSync3(join4(vault, ".skillwiki"), { recursive: true });
|
|
882
411
|
} catch (error) {
|
|
883
412
|
return err("WRITE_FAILED", { path, message: String(error) });
|
|
884
413
|
}
|
|
@@ -909,40 +438,13 @@ function releaseOwnedSyncLock(handle) {
|
|
|
909
438
|
}
|
|
910
439
|
|
|
911
440
|
// src/commands/validate.ts
|
|
912
|
-
import { createHash as
|
|
913
|
-
import { readFile as
|
|
914
|
-
import { resolve
|
|
915
|
-
|
|
916
|
-
// src/parsers/frontmatter.ts
|
|
917
|
-
import yaml from "js-yaml";
|
|
918
|
-
var FM_OPEN = /^---\r?\n/;
|
|
919
|
-
function splitFrontmatter(text) {
|
|
920
|
-
if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
|
|
921
|
-
const afterOpen = text.replace(FM_OPEN, "");
|
|
922
|
-
const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
|
|
923
|
-
if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
|
|
924
|
-
const rawFrontmatter = afterOpen.slice(0, closeIdx);
|
|
925
|
-
const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
|
|
926
|
-
const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
|
|
927
|
-
const body = text.slice(bodyStart);
|
|
928
|
-
return ok({ rawFrontmatter, body, bodyStart });
|
|
929
|
-
}
|
|
930
|
-
function extractFrontmatter(text) {
|
|
931
|
-
const split = splitFrontmatter(text);
|
|
932
|
-
if (!split.ok) return split;
|
|
933
|
-
if (!split.data.rawFrontmatter) return ok({});
|
|
934
|
-
try {
|
|
935
|
-
const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
|
|
936
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
|
|
937
|
-
return ok(parsed);
|
|
938
|
-
} catch (e) {
|
|
939
|
-
return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
|
|
940
|
-
}
|
|
941
|
-
}
|
|
441
|
+
import { createHash as createHash2 } from "crypto";
|
|
442
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
443
|
+
import { resolve, relative, sep } from "path";
|
|
942
444
|
|
|
943
445
|
// src/utils/index-entry.ts
|
|
944
|
-
import { readFile as
|
|
945
|
-
import { join as
|
|
446
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
447
|
+
import { join as join5 } from "path";
|
|
946
448
|
var TYPE_SECTION = {
|
|
947
449
|
entity: "Entities",
|
|
948
450
|
concept: "Concepts",
|
|
@@ -986,128 +488,31 @@ function renderIndexUpsert(text, input) {
|
|
|
986
488
|
});
|
|
987
489
|
}
|
|
988
490
|
async function upsertIndexEntry(input) {
|
|
989
|
-
const path =
|
|
990
|
-
let
|
|
991
|
-
try {
|
|
992
|
-
current = await readFile3(path, "utf8");
|
|
993
|
-
} catch (error) {
|
|
994
|
-
return err("FILE_NOT_FOUND", { path, message: String(error) });
|
|
995
|
-
}
|
|
996
|
-
const rendered = renderIndexUpsert(current, input);
|
|
997
|
-
if (!rendered.ok) return rendered;
|
|
998
|
-
if (!rendered.data.changed) return ok({ changed: false });
|
|
999
|
-
const written = await atomicWriteText(path, rendered.data.text);
|
|
1000
|
-
return written.ok ? ok({ changed: written.data.changed }) : written;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// src/utils/typed-page.ts
|
|
1004
|
-
import { lstatSync, realpathSync } from "fs";
|
|
1005
|
-
import { dirname as dirname2, posix, relative, resolve, sep } from "path";
|
|
1006
|
-
var TYPE_DIRECTORY = {
|
|
1007
|
-
entity: "entities",
|
|
1008
|
-
concept: "concepts",
|
|
1009
|
-
comparison: "comparisons",
|
|
1010
|
-
query: "queries",
|
|
1011
|
-
meta: "meta"
|
|
1012
|
-
};
|
|
1013
|
-
function validateTypedTarget(target) {
|
|
1014
|
-
const segments = target.split("/");
|
|
1015
|
-
if (target.length === 0 || posix.isAbsolute(target) || target.includes("\\") || posix.normalize(target) !== target || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !/^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9._/-]*\.md$/.test(target)) {
|
|
1016
|
-
return err("VAULT_PATH_INVALID", { target, message: "unsafe typed-page target" });
|
|
1017
|
-
}
|
|
1018
|
-
return ok(target);
|
|
1019
|
-
}
|
|
1020
|
-
function assertTargetInsideVault(vault, target) {
|
|
1021
|
-
const validated = validateTypedTarget(target);
|
|
1022
|
-
if (!validated.ok) return validated;
|
|
1023
|
-
let vaultReal;
|
|
491
|
+
const path = join5(input.vault, "index.md");
|
|
492
|
+
let before = "";
|
|
1024
493
|
try {
|
|
1025
|
-
|
|
494
|
+
before = await readFile2(path, "utf8");
|
|
1026
495
|
} catch {
|
|
1027
|
-
|
|
496
|
+
before = "";
|
|
1028
497
|
}
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
|
|
1036
|
-
}
|
|
1037
|
-
const parentRelative = relative(vaultReal, parentReal).split(sep).join("/");
|
|
1038
|
-
if (parentRelative === ".." || parentRelative.startsWith("../")) {
|
|
1039
|
-
return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
|
|
1040
|
-
}
|
|
1041
|
-
if (parentReal !== parent) {
|
|
1042
|
-
return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
|
|
1043
|
-
}
|
|
1044
|
-
let existingRealPath;
|
|
1045
|
-
try {
|
|
1046
|
-
const targetStat = lstatSync(absolutePath2);
|
|
1047
|
-
if (targetStat.isSymbolicLink()) {
|
|
1048
|
-
return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
|
|
1049
|
-
}
|
|
1050
|
-
if (!targetStat.isFile()) {
|
|
1051
|
-
return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
|
|
1052
|
-
}
|
|
1053
|
-
try {
|
|
1054
|
-
existingRealPath = realpathSync(absolutePath2);
|
|
1055
|
-
} catch {
|
|
1056
|
-
return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
|
|
1057
|
-
}
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
if (error.code !== "ENOENT") {
|
|
1060
|
-
return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
|
|
1061
|
-
}
|
|
498
|
+
const projection = await renderRootIndex({ vault: input.vault, currentText: before });
|
|
499
|
+
if (projection.ok) {
|
|
500
|
+
if (projection.data.text === before) return ok({ changed: false });
|
|
501
|
+
const written2 = await writeRootIndexProjection(input.vault, projection.data);
|
|
502
|
+
if (!written2.ok) return written2;
|
|
503
|
+
return ok({ changed: written2.data.changed });
|
|
1062
504
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
505
|
+
const rendered = renderIndexUpsert(before, input);
|
|
506
|
+
if (!rendered.ok) return rendered;
|
|
507
|
+
if (!rendered.data.changed) return ok({ changed: false });
|
|
508
|
+
const written = await writeRootIndexProjection(input.vault, {
|
|
509
|
+
text: rendered.data.text,
|
|
510
|
+
entries: [],
|
|
511
|
+
duplicates_removed: 0,
|
|
512
|
+
ghosts_removed: []
|
|
1069
513
|
});
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const safeTarget = validateTypedTarget(target);
|
|
1073
|
-
if (!safeTarget.ok) return safeTarget;
|
|
1074
|
-
const sensitive = scanSensitiveContent(content, { file: target });
|
|
1075
|
-
if (sensitive.length > 0) {
|
|
1076
|
-
return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
|
|
1077
|
-
}
|
|
1078
|
-
const frontmatter = extractFrontmatter(content);
|
|
1079
|
-
if (!frontmatter.ok) return frontmatter;
|
|
1080
|
-
const detected = detectSchema(frontmatter.data);
|
|
1081
|
-
if (detected.schema === "typed-knowledge") {
|
|
1082
|
-
const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
|
|
1083
|
-
if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
|
|
1084
|
-
const expectedDirectory = TYPE_DIRECTORY[parsed.data.type];
|
|
1085
|
-
if (!expectedDirectory || !target.startsWith(`${expectedDirectory}/`)) {
|
|
1086
|
-
return err("SCHEME_REJECTED", { target, type: parsed.data.type, message: "frontmatter type does not match target directory" });
|
|
1087
|
-
}
|
|
1088
|
-
return ok({
|
|
1089
|
-
target,
|
|
1090
|
-
title: parsed.data.title,
|
|
1091
|
-
type: parsed.data.type,
|
|
1092
|
-
tags: [...parsed.data.tags],
|
|
1093
|
-
content
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
if (detected.schema === "meta") {
|
|
1097
|
-
const parsed = MetaSchema.safeParse(frontmatter.data);
|
|
1098
|
-
if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
|
|
1099
|
-
if (!target.startsWith("meta/")) {
|
|
1100
|
-
return err("SCHEME_REJECTED", { target, type: "meta", message: "frontmatter type does not match target directory" });
|
|
1101
|
-
}
|
|
1102
|
-
return ok({
|
|
1103
|
-
target,
|
|
1104
|
-
title: parsed.data.title,
|
|
1105
|
-
type: "meta",
|
|
1106
|
-
tags: [...parsed.data.tags],
|
|
1107
|
-
content
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
return invalidFrontmatter(target, []);
|
|
514
|
+
if (!written.ok) return written;
|
|
515
|
+
return ok({ changed: written.data.changed });
|
|
1111
516
|
}
|
|
1112
517
|
|
|
1113
518
|
// src/commands/validate.ts
|
|
@@ -1121,7 +526,7 @@ var SCHEMAS = {
|
|
|
1121
526
|
async function runValidate(input) {
|
|
1122
527
|
let text;
|
|
1123
528
|
try {
|
|
1124
|
-
text = await
|
|
529
|
+
text = await readFile3(input.file, "utf8");
|
|
1125
530
|
} catch {
|
|
1126
531
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
1127
532
|
}
|
|
@@ -1172,13 +577,13 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
1172
577
|
let logUpdated = false;
|
|
1173
578
|
let applyHint = "";
|
|
1174
579
|
if (input.apply && input.vault) {
|
|
1175
|
-
const absFile =
|
|
1176
|
-
const absVault =
|
|
1177
|
-
const relPath =
|
|
580
|
+
const absFile = resolve(input.file);
|
|
581
|
+
const absVault = resolve(input.vault);
|
|
582
|
+
const relPath = relative(absVault, absFile).split(sep).join("/");
|
|
1178
583
|
if (relPath.startsWith("..")) {
|
|
1179
584
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
1180
585
|
}
|
|
1181
|
-
const operationId =
|
|
586
|
+
const operationId = createHash2("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
|
|
1182
587
|
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
1183
588
|
const prepared = prepareTypedPage(text, relPath);
|
|
1184
589
|
if (!prepared.ok) {
|
|
@@ -1262,109 +667,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
1262
667
|
|
|
1263
668
|
// src/commands/graph.ts
|
|
1264
669
|
import { writeFile, mkdir } from "fs/promises";
|
|
1265
|
-
import { dirname
|
|
1266
|
-
|
|
1267
|
-
// src/utils/vault.ts
|
|
1268
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1269
|
-
import { readFile as readFile5, readdir, stat as stat2 } from "fs/promises";
|
|
1270
|
-
import { join as join7, relative as relative3, sep as sep3 } from "path";
|
|
1271
|
-
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
1272
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1273
|
-
var DEFAULT_IO_CONCURRENCY = 1;
|
|
1274
|
-
function vaultIoConcurrency() {
|
|
1275
|
-
const raw = Number.parseInt(process.env.SKILLWIKI_VAULT_IO_CONCURRENCY ?? "", 10);
|
|
1276
|
-
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 64) : DEFAULT_IO_CONCURRENCY;
|
|
1277
|
-
}
|
|
1278
|
-
function decodeProcMountPath(value) {
|
|
1279
|
-
return value.replace(/\\040/g, " ");
|
|
1280
|
-
}
|
|
1281
|
-
function isRcloneFuseVaultFromMounts(root, mounts) {
|
|
1282
|
-
return mounts.split(/\r?\n/).some((line) => {
|
|
1283
|
-
const parts = line.split(" ");
|
|
1284
|
-
if (parts.length < 3) return false;
|
|
1285
|
-
const mountPoint = decodeProcMountPath(parts[1]);
|
|
1286
|
-
const fsType = parts[2];
|
|
1287
|
-
return fsType === "fuse.rclone" && (root === mountPoint || root.startsWith(`${mountPoint}/`));
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
function resolveReadOnlyVaultRootWithMounts(root, mounts) {
|
|
1291
|
-
if (/^(1|true|yes)$/i.test(process.env.SKILLWIKI_DISABLE_VAULT_READ_MIRROR ?? "")) {
|
|
1292
|
-
return { root, mirrored: false };
|
|
1293
|
-
}
|
|
1294
|
-
const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
|
|
1295
|
-
if (explicitMirror && existsSync4(join7(explicitMirror, "SCHEMA.md"))) {
|
|
1296
|
-
return { root: explicitMirror, mirrored: explicitMirror !== root };
|
|
1297
|
-
}
|
|
1298
|
-
const siblingMirror = `${root}-git`;
|
|
1299
|
-
if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync4(join7(siblingMirror, "SCHEMA.md"))) {
|
|
1300
|
-
return { root: siblingMirror, mirrored: true };
|
|
1301
|
-
}
|
|
1302
|
-
return { root, mirrored: false };
|
|
1303
|
-
}
|
|
1304
|
-
function resolveReadOnlyVaultRoot(root) {
|
|
1305
|
-
let mounts = "";
|
|
1306
|
-
try {
|
|
1307
|
-
mounts = readFileSync4("/proc/mounts", "utf8");
|
|
1308
|
-
} catch {
|
|
1309
|
-
}
|
|
1310
|
-
return resolveReadOnlyVaultRootWithMounts(root, mounts);
|
|
1311
|
-
}
|
|
1312
|
-
async function mapWithConcurrency(items, limit, mapper) {
|
|
1313
|
-
const out = new Array(items.length);
|
|
1314
|
-
let next = 0;
|
|
1315
|
-
const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
|
|
1316
|
-
for (; ; ) {
|
|
1317
|
-
const index = next++;
|
|
1318
|
-
if (index >= items.length) return;
|
|
1319
|
-
out[index] = await mapper(items[index], index);
|
|
1320
|
-
}
|
|
1321
|
-
});
|
|
1322
|
-
await Promise.all(workers);
|
|
1323
|
-
return out;
|
|
1324
|
-
}
|
|
1325
|
-
async function scanVault(root) {
|
|
1326
|
-
try {
|
|
1327
|
-
await stat2(join7(root, "SCHEMA.md"));
|
|
1328
|
-
} catch {
|
|
1329
|
-
return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
|
|
1330
|
-
}
|
|
1331
|
-
const all = await walk(root);
|
|
1332
|
-
const rels = all.map((p) => ({ absPath: p, relPath: relative3(root, p).split(sep3).join("/") }));
|
|
1333
|
-
return ok({
|
|
1334
|
-
root,
|
|
1335
|
-
allMarkdown: rels,
|
|
1336
|
-
typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
|
|
1337
|
-
raw: rels.filter((p) => p.relPath.startsWith("raw/")),
|
|
1338
|
-
workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
|
|
1339
|
-
compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
async function walk(dir) {
|
|
1343
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
1344
|
-
const out = [];
|
|
1345
|
-
const subdirs = [];
|
|
1346
|
-
for (const e of entries) {
|
|
1347
|
-
const p = join7(dir, e.name);
|
|
1348
|
-
if (e.isDirectory()) {
|
|
1349
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
1350
|
-
subdirs.push(p);
|
|
1351
|
-
} else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
1352
|
-
}
|
|
1353
|
-
const nested = await mapWithConcurrency(subdirs, Math.min(8, vaultIoConcurrency()), walk);
|
|
1354
|
-
for (const files of nested) out.push(...files);
|
|
1355
|
-
return out;
|
|
1356
|
-
}
|
|
1357
|
-
async function readPage(p) {
|
|
1358
|
-
return readFile5(p.absPath, "utf8");
|
|
1359
|
-
}
|
|
1360
|
-
async function readPageCached(p, cache) {
|
|
1361
|
-
if (!cache) return readPage(p);
|
|
1362
|
-
const existing = cache.get(p.absPath);
|
|
1363
|
-
if (existing) return existing;
|
|
1364
|
-
const pending = readPage(p);
|
|
1365
|
-
cache.set(p.absPath, pending);
|
|
1366
|
-
return pending;
|
|
1367
|
-
}
|
|
670
|
+
import { dirname } from "path";
|
|
1368
671
|
|
|
1369
672
|
// src/parsers/wikilinks.ts
|
|
1370
673
|
var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
@@ -1518,7 +821,7 @@ async function runGraphBuild(input) {
|
|
|
1518
821
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
1519
822
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
1520
823
|
try {
|
|
1521
|
-
await mkdir(
|
|
824
|
+
await mkdir(dirname(input.out), { recursive: true });
|
|
1522
825
|
await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
1523
826
|
} catch (e) {
|
|
1524
827
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
@@ -1561,102 +864,8 @@ function computeAdamicAdar(adj) {
|
|
|
1561
864
|
return out;
|
|
1562
865
|
}
|
|
1563
866
|
|
|
1564
|
-
// src/utils/dotenv.ts
|
|
1565
|
-
import { readFile as readFile6, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1566
|
-
import { dirname as dirname4 } from "path";
|
|
1567
|
-
var CONFIG_KEYS = [
|
|
1568
|
-
"WIKI_PATH",
|
|
1569
|
-
"WIKI_LANG",
|
|
1570
|
-
"SKILLWIKI_HOST_ID",
|
|
1571
|
-
"AUTO_COMMIT",
|
|
1572
|
-
"BACKUP_ENDPOINT",
|
|
1573
|
-
"BACKUP_BUCKET",
|
|
1574
|
-
"BACKUP_REGION",
|
|
1575
|
-
"BACKUP_ACCESS_KEY_ID",
|
|
1576
|
-
"BACKUP_SECRET_ACCESS_KEY"
|
|
1577
|
-
];
|
|
1578
|
-
var _whitelist = new Set(CONFIG_KEYS);
|
|
1579
|
-
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
1580
|
-
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
1581
|
-
var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
|
|
1582
|
-
function isValidWikiProfileKey(key) {
|
|
1583
|
-
if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
|
|
1584
|
-
return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
|
|
1585
|
-
}
|
|
1586
|
-
function profileKey(name, suffix) {
|
|
1587
|
-
return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
|
|
1588
|
-
}
|
|
1589
|
-
function parseDotenvText(text) {
|
|
1590
|
-
const out = {};
|
|
1591
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
1592
|
-
const line = rawLine.trim();
|
|
1593
|
-
if (line.length === 0 || line.startsWith("#")) continue;
|
|
1594
|
-
const eq = line.indexOf("=");
|
|
1595
|
-
if (eq <= 0) continue;
|
|
1596
|
-
const key = line.slice(0, eq).trim();
|
|
1597
|
-
const value = line.slice(eq + 1).trim();
|
|
1598
|
-
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
1599
|
-
if (value.length === 0) continue;
|
|
1600
|
-
out[key] = value;
|
|
1601
|
-
}
|
|
1602
|
-
return out;
|
|
1603
|
-
}
|
|
1604
|
-
async function parseDotenvFile(path) {
|
|
1605
|
-
let text;
|
|
1606
|
-
try {
|
|
1607
|
-
text = await readFile6(path, "utf8");
|
|
1608
|
-
} catch {
|
|
1609
|
-
return {};
|
|
1610
|
-
}
|
|
1611
|
-
return parseDotenvText(text);
|
|
1612
|
-
}
|
|
1613
|
-
async function writeDotenv(filePath, entries, originalContent) {
|
|
1614
|
-
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
1615
|
-
await mkdir2(dirname4(filePath), { recursive: true });
|
|
1616
|
-
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
1617
|
-
}
|
|
1618
|
-
function freshLines(entries) {
|
|
1619
|
-
const out = [];
|
|
1620
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
1621
|
-
if (value !== void 0) out.push(`${key}=${value}`);
|
|
1622
|
-
}
|
|
1623
|
-
return out;
|
|
1624
|
-
}
|
|
1625
|
-
function updateLines(originalContent, entries) {
|
|
1626
|
-
let rawLines = originalContent.split(/\r?\n/);
|
|
1627
|
-
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
1628
|
-
rawLines = rawLines.slice(0, -1);
|
|
1629
|
-
}
|
|
1630
|
-
const keysToWrite = new Set(Object.keys(entries));
|
|
1631
|
-
const out = [];
|
|
1632
|
-
for (const line of rawLines) {
|
|
1633
|
-
const trimmed = line.trim();
|
|
1634
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
1635
|
-
out.push(line);
|
|
1636
|
-
continue;
|
|
1637
|
-
}
|
|
1638
|
-
const eq = trimmed.indexOf("=");
|
|
1639
|
-
if (eq <= 0) {
|
|
1640
|
-
out.push(line);
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1643
|
-
const key = trimmed.slice(0, eq).trim();
|
|
1644
|
-
if (keysToWrite.has(key)) {
|
|
1645
|
-
out.push(`${key}=${entries[key]}`);
|
|
1646
|
-
keysToWrite.delete(key);
|
|
1647
|
-
} else {
|
|
1648
|
-
out.push(line);
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
for (const key of keysToWrite) {
|
|
1652
|
-
const value = entries[key];
|
|
1653
|
-
if (value !== void 0) out.push(`${key}=${value}`);
|
|
1654
|
-
}
|
|
1655
|
-
return out;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
867
|
// src/utils/wiki-path.ts
|
|
1659
|
-
import { join as
|
|
868
|
+
import { join as join6 } from "path";
|
|
1660
869
|
async function resolveInitTimePath(input) {
|
|
1661
870
|
const chain = [];
|
|
1662
871
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
@@ -1669,27 +878,27 @@ async function resolveInitTimePath(input) {
|
|
|
1669
878
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
1670
879
|
}
|
|
1671
880
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1672
|
-
const sw = await parseDotenvFile(
|
|
881
|
+
const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1673
882
|
if (sw.WIKI_PATH !== void 0) {
|
|
1674
883
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
1675
884
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
1676
885
|
}
|
|
1677
886
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
1678
|
-
const hermes = await parseDotenvFile(
|
|
887
|
+
const hermes = await parseDotenvFile(join6(input.home, ".hermes", ".env"));
|
|
1679
888
|
if (hermes.WIKI_PATH !== void 0) {
|
|
1680
889
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
1681
890
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
1682
891
|
}
|
|
1683
892
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
1684
893
|
if (input.cwd) {
|
|
1685
|
-
const projCfg = await parseDotenvFile(
|
|
894
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1686
895
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1687
896
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1688
897
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
1689
898
|
}
|
|
1690
899
|
}
|
|
1691
900
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
1692
|
-
const fallback =
|
|
901
|
+
const fallback = join6(input.home, "wiki");
|
|
1693
902
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
1694
903
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
1695
904
|
}
|
|
@@ -1700,7 +909,7 @@ async function resolveRuntimePath(input) {
|
|
|
1700
909
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
1701
910
|
}
|
|
1702
911
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
1703
|
-
const swGlobal = await parseDotenvFile(
|
|
912
|
+
const swGlobal = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1704
913
|
const wikiName = input.wiki;
|
|
1705
914
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
1706
915
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -1744,7 +953,7 @@ async function resolveRuntimePath(input) {
|
|
|
1744
953
|
}
|
|
1745
954
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1746
955
|
if (input.cwd) {
|
|
1747
|
-
const projCfg = await parseDotenvFile(
|
|
956
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1748
957
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1749
958
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1750
959
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -1864,8 +1073,8 @@ function simulateRemoval(adj, removed) {
|
|
|
1864
1073
|
}
|
|
1865
1074
|
|
|
1866
1075
|
// src/commands/audit.ts
|
|
1867
|
-
import { readFile as
|
|
1868
|
-
import { dirname as
|
|
1076
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1077
|
+
import { dirname as dirname2, resolve as resolve2, join as join8 } from "path";
|
|
1869
1078
|
|
|
1870
1079
|
// src/parsers/citations.ts
|
|
1871
1080
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
@@ -1975,9 +1184,9 @@ function hasWikilinkCitations(body) {
|
|
|
1975
1184
|
}
|
|
1976
1185
|
|
|
1977
1186
|
// src/utils/raw-source.ts
|
|
1978
|
-
import { existsSync as
|
|
1979
|
-
import { stat as
|
|
1980
|
-
import { join as
|
|
1187
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1188
|
+
import { stat as stat2 } from "fs/promises";
|
|
1189
|
+
import { join as join7 } from "path";
|
|
1981
1190
|
function normalizeRawSourceTarget(entry) {
|
|
1982
1191
|
let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
1983
1192
|
target = target.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
@@ -1987,21 +1196,21 @@ function normalizeRawSourceTarget(entry) {
|
|
|
1987
1196
|
function rawSourceTargetCandidates(vault, target) {
|
|
1988
1197
|
const normalized = normalizeRawSourceTarget(target);
|
|
1989
1198
|
if (!normalized) return [];
|
|
1990
|
-
const candidates = [
|
|
1991
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
1199
|
+
const candidates = [join7(vault, normalized)];
|
|
1200
|
+
if (!normalized.endsWith(".md")) candidates.push(join7(vault, `${normalized}.md`));
|
|
1992
1201
|
if (normalized.startsWith("raw/")) {
|
|
1993
|
-
candidates.push(
|
|
1994
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
1202
|
+
candidates.push(join7(vault, "_archive", normalized));
|
|
1203
|
+
if (!normalized.endsWith(".md")) candidates.push(join7(vault, "_archive", `${normalized}.md`));
|
|
1995
1204
|
}
|
|
1996
1205
|
return [...new Set(candidates)];
|
|
1997
1206
|
}
|
|
1998
1207
|
function rawSourceTargetExistsSync(vault, target) {
|
|
1999
|
-
return rawSourceTargetCandidates(vault, target).some((candidate) =>
|
|
1208
|
+
return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync4(candidate));
|
|
2000
1209
|
}
|
|
2001
1210
|
async function rawSourceTargetExists(vault, target) {
|
|
2002
1211
|
for (const candidate of rawSourceTargetCandidates(vault, target)) {
|
|
2003
1212
|
try {
|
|
2004
|
-
await
|
|
1213
|
+
await stat2(candidate);
|
|
2005
1214
|
return true;
|
|
2006
1215
|
} catch {
|
|
2007
1216
|
}
|
|
@@ -2013,7 +1222,7 @@ async function rawSourceTargetExists(vault, target) {
|
|
|
2013
1222
|
async function runAudit(input) {
|
|
2014
1223
|
let text;
|
|
2015
1224
|
try {
|
|
2016
|
-
text = await
|
|
1225
|
+
text = await readFile4(input.file, "utf8");
|
|
2017
1226
|
} catch {
|
|
2018
1227
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
2019
1228
|
}
|
|
@@ -2021,7 +1230,7 @@ async function runAudit(input) {
|
|
|
2021
1230
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
2022
1231
|
const split = splitFrontmatter(text);
|
|
2023
1232
|
const body = split.ok ? split.data.body : text;
|
|
2024
|
-
const vault = await findVaultRoot(
|
|
1233
|
+
const vault = await findVaultRoot(dirname2(resolve2(input.file)));
|
|
2025
1234
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
2026
1235
|
const markers = extractCitationMarkers(body);
|
|
2027
1236
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -2066,11 +1275,11 @@ async function findVaultRoot(start) {
|
|
|
2066
1275
|
let cur = start;
|
|
2067
1276
|
for (let i = 0; i < 20; i++) {
|
|
2068
1277
|
try {
|
|
2069
|
-
await
|
|
1278
|
+
await stat3(join8(cur, "SCHEMA.md"));
|
|
2070
1279
|
return cur;
|
|
2071
1280
|
} catch {
|
|
2072
1281
|
}
|
|
2073
|
-
const parent =
|
|
1282
|
+
const parent = dirname2(cur);
|
|
2074
1283
|
if (parent === cur) return null;
|
|
2075
1284
|
cur = parent;
|
|
2076
1285
|
}
|
|
@@ -2158,11 +1367,11 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
2158
1367
|
}
|
|
2159
1368
|
|
|
2160
1369
|
// src/commands/tag-audit.ts
|
|
2161
|
-
import { readFile as
|
|
2162
|
-
import { join as
|
|
1370
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1371
|
+
import { join as join9 } from "path";
|
|
2163
1372
|
|
|
2164
1373
|
// src/parsers/taxonomy.ts
|
|
2165
|
-
import
|
|
1374
|
+
import yaml from "js-yaml";
|
|
2166
1375
|
var TAG_SLUG_RE = /^[a-z0-9][a-z0-9_./-]*$/;
|
|
2167
1376
|
function taxonomyItemIndent(yamlText) {
|
|
2168
1377
|
const lines = yamlText.split(/\r?\n/);
|
|
@@ -2184,12 +1393,12 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
2184
1393
|
const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
|
|
2185
1394
|
const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
|
|
2186
1395
|
const sectionText = schemaText.slice(afterHeading, sectionEnd);
|
|
2187
|
-
const
|
|
2188
|
-
if (!
|
|
1396
|
+
const open = /^```yaml[ \t]*\r?$/m.exec(sectionText);
|
|
1397
|
+
if (!open || open.index === void 0) {
|
|
2189
1398
|
return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
|
|
2190
1399
|
}
|
|
2191
|
-
const openStart = afterHeading +
|
|
2192
|
-
const yamlStart = openStart +
|
|
1400
|
+
const openStart = afterHeading + open.index;
|
|
1401
|
+
const yamlStart = openStart + open[0].length + 1;
|
|
2193
1402
|
const afterOpen = schemaText.slice(yamlStart, sectionEnd);
|
|
2194
1403
|
const close = /^```[ \t]*\r?$/m.exec(afterOpen);
|
|
2195
1404
|
if (!close || close.index === void 0) {
|
|
@@ -2201,7 +1410,7 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
2201
1410
|
const yamlText = schemaText.slice(yamlStart, yamlEnd);
|
|
2202
1411
|
let parsed;
|
|
2203
1412
|
try {
|
|
2204
|
-
parsed =
|
|
1413
|
+
parsed = yaml.load(yamlText, { schema: yaml.JSON_SCHEMA });
|
|
2205
1414
|
} catch (error) {
|
|
2206
1415
|
return err("INVALID_FRONTMATTER", { message: getErrorMessage(error) });
|
|
2207
1416
|
}
|
|
@@ -2221,8 +1430,8 @@ function extractTaxonomy(schemaText) {
|
|
|
2221
1430
|
return ok(parsed.data.tags);
|
|
2222
1431
|
}
|
|
2223
1432
|
function renderTag(tag) {
|
|
2224
|
-
const roundTrip =
|
|
2225
|
-
`, { schema:
|
|
1433
|
+
const roundTrip = yaml.load(`value: ${tag}
|
|
1434
|
+
`, { schema: yaml.JSON_SCHEMA });
|
|
2226
1435
|
return typeof roundTrip.value === "string" && roundTrip.value === tag ? tag : JSON.stringify(tag);
|
|
2227
1436
|
}
|
|
2228
1437
|
function taxonomyCommentForPage(page, date, reason) {
|
|
@@ -2276,13 +1485,43 @@ function reconcileTaxonomyDocument(schemaText, input) {
|
|
|
2276
1485
|
changed: true
|
|
2277
1486
|
});
|
|
2278
1487
|
}
|
|
1488
|
+
function taxonomyEnvelope(text, doc) {
|
|
1489
|
+
return text.slice(0, doc.yamlStart) + "<taxonomy-yaml>" + text.slice(doc.closingFenceStart);
|
|
1490
|
+
}
|
|
1491
|
+
function mergeTaxonomyConflict(baseText, oursText, theirsText) {
|
|
1492
|
+
const base = parseTaxonomyDocument(baseText);
|
|
1493
|
+
const ours = parseTaxonomyDocument(oursText);
|
|
1494
|
+
const theirs = parseTaxonomyDocument(theirsText);
|
|
1495
|
+
if (!base.ok) return base;
|
|
1496
|
+
if (!ours.ok) return ours;
|
|
1497
|
+
if (!theirs.ok) return theirs;
|
|
1498
|
+
const envelope = taxonomyEnvelope(baseText, base.data);
|
|
1499
|
+
if (taxonomyEnvelope(oursText, ours.data) !== envelope || taxonomyEnvelope(theirsText, theirs.data) !== envelope) {
|
|
1500
|
+
return err("SCHEME_REJECTED", { reason: "non-taxonomy-change" });
|
|
1501
|
+
}
|
|
1502
|
+
const baseTags = new Set(base.data.tags);
|
|
1503
|
+
const addedFromOurs = ours.data.tags.filter((tag) => !baseTags.has(tag)).sort();
|
|
1504
|
+
const addedFromTheirs = theirs.data.tags.filter((tag) => !baseTags.has(tag)).sort();
|
|
1505
|
+
const tags = [...base.data.tags, .../* @__PURE__ */ new Set([...addedFromOurs, ...addedFromTheirs])];
|
|
1506
|
+
const rendered = reconcileTaxonomyDocument(baseText, {
|
|
1507
|
+
tags,
|
|
1508
|
+
comment: "# -- reconciled: taxonomy-only three-stage merge --"
|
|
1509
|
+
});
|
|
1510
|
+
if (!rendered.ok) return rendered;
|
|
1511
|
+
return ok({
|
|
1512
|
+
text: rendered.data.text,
|
|
1513
|
+
tags,
|
|
1514
|
+
added_from_ours: addedFromOurs,
|
|
1515
|
+
added_from_theirs: addedFromTheirs
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
2279
1518
|
|
|
2280
1519
|
// src/commands/tag-audit.ts
|
|
2281
1520
|
async function runTagAudit(input) {
|
|
2282
1521
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
2283
1522
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
2284
1523
|
const scan = scanResult.data;
|
|
2285
|
-
const schemaText = await
|
|
1524
|
+
const schemaText = await readFile5(join9(input.vault, "SCHEMA.md"), "utf8");
|
|
2286
1525
|
const tax = extractTaxonomy(schemaText);
|
|
2287
1526
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
2288
1527
|
const allowed = new Set(tax.data);
|
|
@@ -2299,69 +1538,328 @@ async function runTagAudit(input) {
|
|
|
2299
1538
|
pageViolations.push({ page: p.relPath, tag: t });
|
|
2300
1539
|
}
|
|
2301
1540
|
}
|
|
2302
|
-
return pageViolations;
|
|
1541
|
+
return pageViolations;
|
|
1542
|
+
});
|
|
1543
|
+
for (const result of perPage) {
|
|
1544
|
+
if (!Array.isArray(result)) {
|
|
1545
|
+
return { exitCode: ExitCode.INVALID_FRONTMATTER, result };
|
|
1546
|
+
}
|
|
1547
|
+
violations.push(...result);
|
|
1548
|
+
}
|
|
1549
|
+
if (violations.length > 0) {
|
|
1550
|
+
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
|
|
1551
|
+
}
|
|
1552
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/commands/index-check.ts
|
|
1556
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1557
|
+
import { join as join10 } from "path";
|
|
1558
|
+
function normalizeIndexTarget(raw) {
|
|
1559
|
+
return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
|
|
1560
|
+
}
|
|
1561
|
+
async function runIndexCheck(input) {
|
|
1562
|
+
const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
1563
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1564
|
+
let indexText = "";
|
|
1565
|
+
try {
|
|
1566
|
+
indexText = await readFile6(join10(input.vault, "index.md"), "utf8");
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
const indexTargets = /* @__PURE__ */ new Set();
|
|
1570
|
+
const indexBare = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1572
|
+
const target = normalizeIndexTarget(s);
|
|
1573
|
+
indexTargets.add(target);
|
|
1574
|
+
const bare = target.split("/").pop().toLowerCase();
|
|
1575
|
+
const list = indexBare.get(bare) ?? [];
|
|
1576
|
+
list.push(target);
|
|
1577
|
+
indexBare.set(bare, list);
|
|
1578
|
+
}
|
|
1579
|
+
const required = /* @__PURE__ */ new Map();
|
|
1580
|
+
const known = /* @__PURE__ */ new Set();
|
|
1581
|
+
for (const p of scan.data.typedKnowledge) {
|
|
1582
|
+
const target = p.relPath.replace(/\.md$/, "");
|
|
1583
|
+
required.set(target, p.relPath);
|
|
1584
|
+
known.add(target);
|
|
1585
|
+
}
|
|
1586
|
+
for (const p of scan.data.compound) {
|
|
1587
|
+
known.add(p.relPath.replace(/\.md$/, ""));
|
|
1588
|
+
}
|
|
1589
|
+
const missing_from_index = [];
|
|
1590
|
+
for (const [target, relPath] of required.entries()) {
|
|
1591
|
+
if (indexTargets.has(target)) continue;
|
|
1592
|
+
const bare = target.split("/").pop().toLowerCase();
|
|
1593
|
+
const bareHits = indexBare.get(bare) ?? [];
|
|
1594
|
+
const basenameOnly = bareHits.filter((t) => !t.includes("/"));
|
|
1595
|
+
const sameNameRequired = [...required.keys()].filter(
|
|
1596
|
+
(t) => t.split("/").pop().toLowerCase() === bare
|
|
1597
|
+
);
|
|
1598
|
+
if (basenameOnly.length === 1 && sameNameRequired.length === 1) continue;
|
|
1599
|
+
missing_from_index.push(relPath);
|
|
1600
|
+
}
|
|
1601
|
+
const ghost_entries = [];
|
|
1602
|
+
for (const target of indexTargets) {
|
|
1603
|
+
if (known.has(target)) continue;
|
|
1604
|
+
if (!target.includes("/")) {
|
|
1605
|
+
const bare = target.toLowerCase();
|
|
1606
|
+
const matches = [...known].filter((k) => k.split("/").pop().toLowerCase() === bare);
|
|
1607
|
+
if (matches.length === 0) ghost_entries.push(target);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
ghost_entries.push(target);
|
|
1611
|
+
}
|
|
1612
|
+
const hintLines = [];
|
|
1613
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1614
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1615
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1616
|
+
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1617
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1618
|
+
}
|
|
1619
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/commands/project-index.ts
|
|
1623
|
+
import { readdir, readFile as readFile7, mkdir as mkdir2 } from "fs/promises";
|
|
1624
|
+
import { join as join11, dirname as dirname3, basename } from "path";
|
|
1625
|
+
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
1626
|
+
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
1627
|
+
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
1628
|
+
const found = [];
|
|
1629
|
+
let entries;
|
|
1630
|
+
try {
|
|
1631
|
+
entries = await readdir(rootAbs, { withFileTypes: true });
|
|
1632
|
+
} catch {
|
|
1633
|
+
return found;
|
|
1634
|
+
}
|
|
1635
|
+
for (const entry of entries) {
|
|
1636
|
+
const abs = join11(rootAbs, entry.name);
|
|
1637
|
+
const rel = `${rootRel}/${entry.name}`;
|
|
1638
|
+
if (entry.isDirectory()) {
|
|
1639
|
+
found.push(...await scanMarkdownTree(abs, rel));
|
|
1640
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1641
|
+
found.push(rel);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return found;
|
|
1645
|
+
}
|
|
1646
|
+
function projectLocalType(slug, page, data) {
|
|
1647
|
+
if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
|
|
1648
|
+
if (page.startsWith(`projects/${slug}/work/`)) {
|
|
1649
|
+
if (typeof data.kind === "string") return data.kind;
|
|
1650
|
+
const name = basename(page, ".md");
|
|
1651
|
+
if (name === "spec" || name === "plan" || name === "retro") return name;
|
|
1652
|
+
return "work";
|
|
1653
|
+
}
|
|
1654
|
+
if (page.startsWith(`projects/${slug}/architecture/`)) {
|
|
1655
|
+
return typeof data.type === "string" ? data.type : "architecture";
|
|
1656
|
+
}
|
|
1657
|
+
if (page.startsWith(`projects/${slug}/history/`)) {
|
|
1658
|
+
if (typeof data.kind === "string") return data.kind;
|
|
1659
|
+
if (typeof data.type === "string") return data.type;
|
|
1660
|
+
return "history";
|
|
1661
|
+
}
|
|
1662
|
+
return typeof data.type === "string" ? data.type : "project";
|
|
1663
|
+
}
|
|
1664
|
+
async function renderProjectIndex(vault, slug, opts = {}) {
|
|
1665
|
+
const projectDir = join11(vault, "projects", slug);
|
|
1666
|
+
try {
|
|
1667
|
+
await readdir(projectDir);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return err("PROJECT_NOT_FOUND", { slug, path: projectDir });
|
|
1670
|
+
}
|
|
1671
|
+
const wikilinkPattern = `[[${slug}]]`;
|
|
1672
|
+
const entries = [];
|
|
1673
|
+
const compoundDir = join11(vault, "projects", slug, "compound");
|
|
1674
|
+
try {
|
|
1675
|
+
const compoundFiles = await readdir(compoundDir, { withFileTypes: true });
|
|
1676
|
+
for (const entry of compoundFiles) {
|
|
1677
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1678
|
+
const filePath = join11(compoundDir, entry.name);
|
|
1679
|
+
let text;
|
|
1680
|
+
try {
|
|
1681
|
+
text = await readFile7(filePath, "utf8");
|
|
1682
|
+
} catch {
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
const fm = extractFrontmatter(text);
|
|
1686
|
+
if (!fm.ok) continue;
|
|
1687
|
+
entries.push({
|
|
1688
|
+
page: `projects/${slug}/compound/${entry.name}`,
|
|
1689
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
1690
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
for (const dir of LAYER2_DIRS) {
|
|
1696
|
+
let files;
|
|
1697
|
+
try {
|
|
1698
|
+
files = await readdir(join11(vault, dir), { withFileTypes: true });
|
|
1699
|
+
} catch {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
for (const entry of files) {
|
|
1703
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1704
|
+
const filePath = join11(vault, dir, entry.name);
|
|
1705
|
+
let text;
|
|
1706
|
+
try {
|
|
1707
|
+
text = await readFile7(filePath, "utf8");
|
|
1708
|
+
} catch {
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const fm = extractFrontmatter(text);
|
|
1712
|
+
if (!fm.ok) continue;
|
|
1713
|
+
const pp = fm.data.provenance_projects;
|
|
1714
|
+
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
1715
|
+
entries.push({
|
|
1716
|
+
page: `${dir}/${entry.name}`,
|
|
1717
|
+
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
1718
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
1723
|
+
const rootAbs = join11(projectDir, dir);
|
|
1724
|
+
const rootRel = `projects/${slug}/${dir}`;
|
|
1725
|
+
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
1726
|
+
for (const page of pages) {
|
|
1727
|
+
const filePath = join11(vault, page);
|
|
1728
|
+
let text;
|
|
1729
|
+
try {
|
|
1730
|
+
text = await readFile7(filePath, "utf8");
|
|
1731
|
+
} catch {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
const fm = extractFrontmatter(text);
|
|
1735
|
+
if (!fm.ok) continue;
|
|
1736
|
+
entries.push({
|
|
1737
|
+
page,
|
|
1738
|
+
type: projectLocalType(slug, page, fm.data),
|
|
1739
|
+
title: typeof fm.data.title === "string" ? fm.data.title : basename(page, ".md")
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const typeOrder = {
|
|
1744
|
+
entity: 0,
|
|
1745
|
+
concept: 1,
|
|
1746
|
+
comparison: 2,
|
|
1747
|
+
query: 3,
|
|
1748
|
+
summary: 4,
|
|
1749
|
+
meta: 5,
|
|
1750
|
+
requirement: 6,
|
|
1751
|
+
spec: 7,
|
|
1752
|
+
plan: 8,
|
|
1753
|
+
retro: 9,
|
|
1754
|
+
architecture: 10,
|
|
1755
|
+
pattern: 11,
|
|
1756
|
+
gotcha: 12,
|
|
1757
|
+
lesson: 13,
|
|
1758
|
+
antipattern: 14,
|
|
1759
|
+
compound: 15,
|
|
1760
|
+
work: 16,
|
|
1761
|
+
history: 17
|
|
1762
|
+
};
|
|
1763
|
+
entries.sort((a, b) => {
|
|
1764
|
+
const ta = typeOrder[a.type] ?? 99;
|
|
1765
|
+
const tb = typeOrder[b.type] ?? 99;
|
|
1766
|
+
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
2303
1767
|
});
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
1768
|
+
const today = opts.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1769
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1770
|
+
for (const e of entries) {
|
|
1771
|
+
const group = e.type;
|
|
1772
|
+
if (!grouped.has(group)) grouped.set(group, []);
|
|
1773
|
+
grouped.get(group).push(e);
|
|
1774
|
+
}
|
|
1775
|
+
let body = `# Knowledge Index: ${slug}
|
|
1776
|
+
|
|
1777
|
+
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
1778
|
+
|
|
1779
|
+
`;
|
|
1780
|
+
for (const [type, items] of grouped) {
|
|
1781
|
+
body += `## ${type}
|
|
1782
|
+
|
|
1783
|
+
`;
|
|
1784
|
+
for (const item of items) {
|
|
1785
|
+
const pageRef = item.page.replace(/\.md$/, "");
|
|
1786
|
+
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
1787
|
+
`;
|
|
2307
1788
|
}
|
|
2308
|
-
|
|
1789
|
+
body += "\n";
|
|
2309
1790
|
}
|
|
2310
|
-
if (
|
|
2311
|
-
|
|
1791
|
+
if (entries.length === 0) {
|
|
1792
|
+
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
1793
|
+
`;
|
|
2312
1794
|
}
|
|
2313
|
-
return
|
|
1795
|
+
return ok({
|
|
1796
|
+
text: body,
|
|
1797
|
+
entries,
|
|
1798
|
+
index_path: `projects/${slug}/knowledge.md`
|
|
1799
|
+
});
|
|
2314
1800
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
1801
|
+
async function runProjectIndex(input) {
|
|
1802
|
+
const slug = input.slug;
|
|
1803
|
+
const projectDir = join11(input.vault, "projects", slug);
|
|
1804
|
+
const rendered = await renderProjectIndex(input.vault, slug);
|
|
1805
|
+
if (!rendered.ok) {
|
|
1806
|
+
return {
|
|
1807
|
+
exitCode: rendered.error === "PROJECT_NOT_FOUND" ? ExitCode.PROJECT_NOT_FOUND : ExitCode.WRITE_FAILED,
|
|
1808
|
+
result: rendered
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
const indexPath = join11(projectDir, "knowledge.md");
|
|
1812
|
+
const entries = rendered.data.entries;
|
|
1813
|
+
let existing = false;
|
|
1814
|
+
let stale = false;
|
|
2323
1815
|
try {
|
|
2324
|
-
|
|
1816
|
+
const existingText = await readFile7(indexPath, "utf8");
|
|
1817
|
+
existing = true;
|
|
1818
|
+
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
1819
|
+
const existingPages = new Set(existingEntries.map((l) => {
|
|
1820
|
+
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
1821
|
+
return m ? m[1] : "";
|
|
1822
|
+
}));
|
|
1823
|
+
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
1824
|
+
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
2325
1825
|
} catch {
|
|
2326
1826
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
2341
|
-
fileSlugs.set(slug, p.relPath);
|
|
2342
|
-
}
|
|
2343
|
-
const missing_from_index = [];
|
|
2344
|
-
for (const [slug, relPath] of requiredSlugs.entries()) {
|
|
2345
|
-
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
2346
|
-
}
|
|
2347
|
-
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
2348
|
-
const ghost_entries = [];
|
|
2349
|
-
for (const [lower, orig] of indexSlugsLower) {
|
|
2350
|
-
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
2351
|
-
}
|
|
2352
|
-
const hintLines = [];
|
|
2353
|
-
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
2354
|
-
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
2355
|
-
if (hintLines.length === 0) hintLines.push("index OK");
|
|
2356
|
-
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
2357
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1827
|
+
if (input.apply) {
|
|
1828
|
+
try {
|
|
1829
|
+
await mkdir2(dirname3(indexPath), { recursive: true });
|
|
1830
|
+
} catch (e) {
|
|
1831
|
+
return {
|
|
1832
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1833
|
+
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
const written = await atomicWriteText(indexPath, rendered.data.text);
|
|
1837
|
+
if (!written.ok) {
|
|
1838
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
1839
|
+
}
|
|
2358
1840
|
}
|
|
2359
|
-
|
|
1841
|
+
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
1842
|
+
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
1843
|
+
return {
|
|
1844
|
+
exitCode: ExitCode.OK,
|
|
1845
|
+
result: ok({
|
|
1846
|
+
slug,
|
|
1847
|
+
entries,
|
|
1848
|
+
existing,
|
|
1849
|
+
stale,
|
|
1850
|
+
index_path: rendered.data.index_path,
|
|
1851
|
+
humanHint: `project: ${slug}
|
|
1852
|
+
entries: ${entries.length}${staleHint}
|
|
1853
|
+
${action}
|
|
1854
|
+
|
|
1855
|
+
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
1856
|
+
})
|
|
1857
|
+
};
|
|
2360
1858
|
}
|
|
2361
1859
|
|
|
2362
1860
|
// src/commands/stale.ts
|
|
2363
|
-
import { readdir as readdir2, rename
|
|
2364
|
-
import { join as
|
|
1861
|
+
import { readdir as readdir2, rename, mkdir as mkdir3, readFile as readFile8 } from "fs/promises";
|
|
1862
|
+
import { join as join12 } from "path";
|
|
2365
1863
|
|
|
2366
1864
|
// src/parsers/expiry-annotations.ts
|
|
2367
1865
|
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
@@ -2398,8 +1896,8 @@ function parseExpiryAnnotations(content, pagePath) {
|
|
|
2398
1896
|
}
|
|
2399
1897
|
|
|
2400
1898
|
// src/commands/stale.ts
|
|
2401
|
-
function daysSince(
|
|
2402
|
-
return Math.floor((Date.now() - Date.parse(
|
|
1899
|
+
function daysSince(isoDate) {
|
|
1900
|
+
return Math.floor((Date.now() - Date.parse(isoDate)) / 864e5);
|
|
2403
1901
|
}
|
|
2404
1902
|
async function runStale(input) {
|
|
2405
1903
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
@@ -2410,7 +1908,7 @@ async function runStale(input) {
|
|
|
2410
1908
|
const archived = [];
|
|
2411
1909
|
const workDirs = /* @__PURE__ */ new Map();
|
|
2412
1910
|
const workDirsBySlug = /* @__PURE__ */ new Map();
|
|
2413
|
-
const projectsDir =
|
|
1911
|
+
const projectsDir = join12(input.vault, "projects");
|
|
2414
1912
|
let projectSlugs = [];
|
|
2415
1913
|
try {
|
|
2416
1914
|
projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
@@ -2423,7 +1921,7 @@ async function runStale(input) {
|
|
|
2423
1921
|
projectSlugs = [input.project];
|
|
2424
1922
|
}
|
|
2425
1923
|
for (const slug of projectSlugs) {
|
|
2426
|
-
const workPath =
|
|
1924
|
+
const workPath = join12(projectsDir, slug, "work");
|
|
2427
1925
|
let entries;
|
|
2428
1926
|
try {
|
|
2429
1927
|
entries = await readdir2(workPath, { withFileTypes: true });
|
|
@@ -2434,7 +1932,7 @@ async function runStale(input) {
|
|
|
2434
1932
|
for (const e of entries) {
|
|
2435
1933
|
if (!e.isDirectory()) continue;
|
|
2436
1934
|
const relDir = `projects/${slug}/work/${e.name}`;
|
|
2437
|
-
const absDir =
|
|
1935
|
+
const absDir = join12(workPath, e.name);
|
|
2438
1936
|
let status = "";
|
|
2439
1937
|
let files;
|
|
2440
1938
|
try {
|
|
@@ -2447,7 +1945,7 @@ async function runStale(input) {
|
|
|
2447
1945
|
for (const f of files) {
|
|
2448
1946
|
if (!f.endsWith(".md")) continue;
|
|
2449
1947
|
try {
|
|
2450
|
-
const fm = extractFrontmatter(await
|
|
1948
|
+
const fm = extractFrontmatter(await readFile8(join12(absDir, f), "utf8"));
|
|
2451
1949
|
if (fm.ok && typeof fm.data.status === "string") {
|
|
2452
1950
|
status = fm.data.status;
|
|
2453
1951
|
break;
|
|
@@ -2478,9 +1976,9 @@ async function runStale(input) {
|
|
|
2478
1976
|
if (input.project && !project.includes(input.project)) return null;
|
|
2479
1977
|
let inferred = false;
|
|
2480
1978
|
if (input.forceScan && !kind) {
|
|
2481
|
-
const
|
|
2482
|
-
if (!LOOP_CYCLE_PATTERN.test(
|
|
2483
|
-
const m =
|
|
1979
|
+
const basename3 = t.relPath.split("/").pop();
|
|
1980
|
+
if (!LOOP_CYCLE_PATTERN.test(basename3)) {
|
|
1981
|
+
const m = basename3.match(KIND_FROM_FILENAME);
|
|
2484
1982
|
if (m) {
|
|
2485
1983
|
kind = m[1];
|
|
2486
1984
|
inferred = true;
|
|
@@ -2491,9 +1989,9 @@ async function runStale(input) {
|
|
|
2491
1989
|
const bodyStart = content.indexOf("---", 4);
|
|
2492
1990
|
if (bodyStart > 0) {
|
|
2493
1991
|
const body = content.slice(bodyStart);
|
|
2494
|
-
const
|
|
2495
|
-
if (
|
|
2496
|
-
const candidate =
|
|
1992
|
+
const wikilink = body.match(/\[\[([a-z0-9-]+)\]\]/);
|
|
1993
|
+
if (wikilink) {
|
|
1994
|
+
const candidate = wikilink[1];
|
|
2497
1995
|
if (workDirsBySlug.has(candidate)) {
|
|
2498
1996
|
project = `[[${candidate}]]`;
|
|
2499
1997
|
inferred = true;
|
|
@@ -2543,9 +2041,9 @@ async function runStale(input) {
|
|
|
2543
2041
|
}
|
|
2544
2042
|
}
|
|
2545
2043
|
await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
|
|
2546
|
-
const specPath =
|
|
2044
|
+
const specPath = join12(input.vault, relDir, "spec.md");
|
|
2547
2045
|
try {
|
|
2548
|
-
const specContent = await
|
|
2046
|
+
const specContent = await readFile8(specPath, "utf8");
|
|
2549
2047
|
const specFm = extractFrontmatter(specContent);
|
|
2550
2048
|
if (specFm.ok && typeof specFm.data.source === "string") {
|
|
2551
2049
|
const sourcePath = specFm.data.source;
|
|
@@ -2574,7 +2072,7 @@ async function runStale(input) {
|
|
|
2574
2072
|
if (daysSince(dateStr) < input.days) continue;
|
|
2575
2073
|
let files;
|
|
2576
2074
|
try {
|
|
2577
|
-
files = await readdir2(
|
|
2075
|
+
files = await readdir2(join12(input.vault, relDir));
|
|
2578
2076
|
} catch {
|
|
2579
2077
|
continue;
|
|
2580
2078
|
}
|
|
@@ -2644,7 +2142,7 @@ async function runStale(input) {
|
|
|
2644
2142
|
staleSections.push(...staleSectionResults.flat());
|
|
2645
2143
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2646
2144
|
if (input.archive) {
|
|
2647
|
-
const archiveDir =
|
|
2145
|
+
const archiveDir = join12(input.vault, "_archive", today);
|
|
2648
2146
|
await mkdir3(archiveDir, { recursive: true });
|
|
2649
2147
|
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
2650
2148
|
for (const page of scan.typedKnowledge) {
|
|
@@ -2660,9 +2158,9 @@ async function runStale(input) {
|
|
|
2660
2158
|
}
|
|
2661
2159
|
for (const t of staleTranscripts) {
|
|
2662
2160
|
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
2663
|
-
const dest =
|
|
2161
|
+
const dest = join12(archiveDir, t.path.split("/").pop());
|
|
2664
2162
|
try {
|
|
2665
|
-
await
|
|
2163
|
+
await rename(join12(input.vault, t.path), dest);
|
|
2666
2164
|
archived.push(t.path);
|
|
2667
2165
|
} catch {
|
|
2668
2166
|
}
|
|
@@ -2672,18 +2170,18 @@ async function runStale(input) {
|
|
|
2672
2170
|
if (parts.length >= 4 && parts[0] === "projects") {
|
|
2673
2171
|
const slug = parts[1];
|
|
2674
2172
|
const itemName = parts[3];
|
|
2675
|
-
const histDir =
|
|
2173
|
+
const histDir = join12(input.vault, "projects", slug, "history", "archived-work");
|
|
2676
2174
|
await mkdir3(histDir, { recursive: true });
|
|
2677
|
-
const dest =
|
|
2175
|
+
const dest = join12(histDir, itemName);
|
|
2678
2176
|
try {
|
|
2679
|
-
await
|
|
2177
|
+
await rename(join12(input.vault, w.path), dest);
|
|
2680
2178
|
archived.push(w.path);
|
|
2681
2179
|
} catch {
|
|
2682
2180
|
}
|
|
2683
2181
|
} else {
|
|
2684
|
-
const dest =
|
|
2182
|
+
const dest = join12(archiveDir, w.path.replace(/\//g, "_"));
|
|
2685
2183
|
try {
|
|
2686
|
-
await
|
|
2184
|
+
await rename(join12(input.vault, w.path), dest);
|
|
2687
2185
|
archived.push(w.path);
|
|
2688
2186
|
} catch {
|
|
2689
2187
|
}
|
|
@@ -2738,19 +2236,19 @@ async function runPagesize(input) {
|
|
|
2738
2236
|
}
|
|
2739
2237
|
|
|
2740
2238
|
// src/commands/log-rotate.ts
|
|
2741
|
-
import { readFile as
|
|
2742
|
-
import { join as
|
|
2239
|
+
import { readFile as readFile9, rename as rename2, writeFile as writeFile2, stat as stat4 } from "fs/promises";
|
|
2240
|
+
import { join as join13 } from "path";
|
|
2743
2241
|
var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
2744
2242
|
async function runLogRotate(input) {
|
|
2745
2243
|
try {
|
|
2746
|
-
await
|
|
2244
|
+
await stat4(join13(input.vault, "SCHEMA.md"));
|
|
2747
2245
|
} catch {
|
|
2748
2246
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
2749
2247
|
}
|
|
2750
|
-
const logPath =
|
|
2248
|
+
const logPath = join13(input.vault, "log.md");
|
|
2751
2249
|
let logText;
|
|
2752
2250
|
try {
|
|
2753
|
-
logText = await
|
|
2251
|
+
logText = await readFile9(logPath, "utf8");
|
|
2754
2252
|
} catch {
|
|
2755
2253
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2756
2254
|
}
|
|
@@ -2767,9 +2265,9 @@ async function runLogRotate(input) {
|
|
|
2767
2265
|
}
|
|
2768
2266
|
const newestYear = matches[matches.length - 1][1];
|
|
2769
2267
|
const rotatedName = `log-${newestYear}.md`;
|
|
2770
|
-
const rotatedPath =
|
|
2268
|
+
const rotatedPath = join13(input.vault, rotatedName);
|
|
2771
2269
|
try {
|
|
2772
|
-
await
|
|
2270
|
+
await rename2(logPath, rotatedPath);
|
|
2773
2271
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2774
2272
|
const fresh = `# Vault Log
|
|
2775
2273
|
|
|
@@ -2779,7 +2277,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
2779
2277
|
|
|
2780
2278
|
- Previous log moved to ${rotatedName}
|
|
2781
2279
|
`;
|
|
2782
|
-
await
|
|
2280
|
+
await writeFile2(logPath, fresh, "utf8");
|
|
2783
2281
|
} catch (e) {
|
|
2784
2282
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
2785
2283
|
}
|
|
@@ -2812,13 +2310,13 @@ async function runTopicMapCheck(input) {
|
|
|
2812
2310
|
}
|
|
2813
2311
|
|
|
2814
2312
|
// src/commands/index-link-format.ts
|
|
2815
|
-
import { readFile as
|
|
2816
|
-
import { join as
|
|
2313
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2314
|
+
import { join as join14 } from "path";
|
|
2817
2315
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
2818
2316
|
async function runIndexLinkFormat(input) {
|
|
2819
2317
|
let text = "";
|
|
2820
2318
|
try {
|
|
2821
|
-
text = await
|
|
2319
|
+
text = await readFile10(join14(input.vault, "index.md"), "utf8");
|
|
2822
2320
|
} catch {
|
|
2823
2321
|
}
|
|
2824
2322
|
const markdown_links = [];
|
|
@@ -2831,9 +2329,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
2831
2329
|
}
|
|
2832
2330
|
|
|
2833
2331
|
// src/commands/dedup.ts
|
|
2834
|
-
import { createHash as
|
|
2835
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
2836
|
-
import { dirname as
|
|
2332
|
+
import { createHash as createHash3 } from "crypto";
|
|
2333
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
2334
|
+
import { dirname as dirname4, join as join15, resolve as resolve3 } from "path";
|
|
2837
2335
|
|
|
2838
2336
|
// src/utils/rclone.ts
|
|
2839
2337
|
import { execFile } from "child_process";
|
|
@@ -2925,7 +2423,7 @@ async function runDedup(input) {
|
|
|
2925
2423
|
const manifest = safeEntries.length > 0 ? {
|
|
2926
2424
|
version: 1,
|
|
2927
2425
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2928
|
-
vault:
|
|
2426
|
+
vault: resolve3(input.vault),
|
|
2929
2427
|
entries: safeEntries
|
|
2930
2428
|
} : void 0;
|
|
2931
2429
|
const remote = await planAndMaybePruneRemote(input, safeEntries);
|
|
@@ -2937,7 +2435,7 @@ async function runDedup(input) {
|
|
|
2937
2435
|
}
|
|
2938
2436
|
if (input.manifestOut && manifest) {
|
|
2939
2437
|
try {
|
|
2940
|
-
mkdirSync4(
|
|
2438
|
+
mkdirSync4(dirname4(input.manifestOut), { recursive: true });
|
|
2941
2439
|
writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
|
|
2942
2440
|
`, "utf-8");
|
|
2943
2441
|
} catch (e) {
|
|
@@ -2952,7 +2450,7 @@ async function runDedup(input) {
|
|
|
2952
2450
|
}
|
|
2953
2451
|
}
|
|
2954
2452
|
for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
|
|
2955
|
-
const text =
|
|
2453
|
+
const text = readFileSync4(join15(input.vault, page.relPath), "utf-8");
|
|
2956
2454
|
let updated = text;
|
|
2957
2455
|
let changed = false;
|
|
2958
2456
|
for (const [oldPath, newPath] of replacements) {
|
|
@@ -2970,12 +2468,12 @@ async function runDedup(input) {
|
|
|
2970
2468
|
}
|
|
2971
2469
|
}
|
|
2972
2470
|
if (changed) {
|
|
2973
|
-
writeFileSync4(
|
|
2471
|
+
writeFileSync4(join15(input.vault, page.relPath), updated);
|
|
2974
2472
|
rewired.push(page.relPath);
|
|
2975
2473
|
}
|
|
2976
2474
|
}
|
|
2977
2475
|
for (const oldPath of replacements.keys()) {
|
|
2978
|
-
const fullPath =
|
|
2476
|
+
const fullPath = join15(input.vault, oldPath);
|
|
2979
2477
|
try {
|
|
2980
2478
|
unlinkSync4(fullPath);
|
|
2981
2479
|
removed.push(oldPath);
|
|
@@ -3069,14 +2567,14 @@ function buildSafeEntries(vault, duplicates, unsafe) {
|
|
|
3069
2567
|
return entries;
|
|
3070
2568
|
}
|
|
3071
2569
|
function hashRawBody(vault, relPath) {
|
|
3072
|
-
const text =
|
|
2570
|
+
const text = readFileSync4(join15(vault, relPath), "utf-8");
|
|
3073
2571
|
const split = splitFrontmatter(text);
|
|
3074
2572
|
const body = split.ok ? split.data.body : text;
|
|
3075
|
-
return
|
|
2573
|
+
return createHash3("sha256").update(body).digest("hex");
|
|
3076
2574
|
}
|
|
3077
2575
|
function readManifest(path) {
|
|
3078
2576
|
try {
|
|
3079
|
-
const parsed = JSON.parse(
|
|
2577
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
3080
2578
|
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
3081
2579
|
return err("INVALID_FRONTMATTER", { message: "dedup manifest must have version 1 and entries[]" });
|
|
3082
2580
|
}
|
|
@@ -3092,7 +2590,7 @@ async function planAndMaybePruneRemote(input, entries) {
|
|
|
3092
2590
|
}
|
|
3093
2591
|
|
|
3094
2592
|
// src/utils/safe-write.ts
|
|
3095
|
-
import { readFile as
|
|
2593
|
+
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
3096
2594
|
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
3097
2595
|
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
3098
2596
|
function bodyBytes(text) {
|
|
@@ -3102,7 +2600,7 @@ function bodyBytes(text) {
|
|
|
3102
2600
|
}
|
|
3103
2601
|
async function readIfExists(absPath) {
|
|
3104
2602
|
try {
|
|
3105
|
-
return await
|
|
2603
|
+
return await readFile11(absPath, "utf8");
|
|
3106
2604
|
} catch (e) {
|
|
3107
2605
|
if (e.code === "ENOENT") return null;
|
|
3108
2606
|
throw e;
|
|
@@ -3225,10 +2723,10 @@ ${newBody}`;
|
|
|
3225
2723
|
}
|
|
3226
2724
|
|
|
3227
2725
|
// src/commands/lint.ts
|
|
3228
|
-
import { existsSync as
|
|
3229
|
-
import { readFile as
|
|
3230
|
-
import { createHash as
|
|
3231
|
-
import { join as
|
|
2726
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2727
|
+
import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
|
|
2728
|
+
import { createHash as createHash5 } from "crypto";
|
|
2729
|
+
import { join as join17, relative as relative2, sep as sep2 } from "path";
|
|
3232
2730
|
|
|
3233
2731
|
// src/commands/sparse-community.ts
|
|
3234
2732
|
async function runSparseCommunity(input) {
|
|
@@ -3244,7 +2742,7 @@ async function runSparseCommunity(input) {
|
|
|
3244
2742
|
}
|
|
3245
2743
|
|
|
3246
2744
|
// src/commands/raw-body-dedup.ts
|
|
3247
|
-
import { createHash as
|
|
2745
|
+
import { createHash as createHash4 } from "crypto";
|
|
3248
2746
|
async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
3249
2747
|
const scanResult = scan ? ok(scan) : await scanVault(vault);
|
|
3250
2748
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
@@ -3253,7 +2751,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
3253
2751
|
const text = await readPageCached(raw, pageTextCache);
|
|
3254
2752
|
const split = splitFrontmatter(text);
|
|
3255
2753
|
if (!split.ok) return null;
|
|
3256
|
-
const bodyHash =
|
|
2754
|
+
const bodyHash = createHash4("sha256").update(split.data.body).digest("hex");
|
|
3257
2755
|
const fm = extractFrontmatter(text);
|
|
3258
2756
|
let fmSha256 = null;
|
|
3259
2757
|
if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
|
|
@@ -3285,9 +2783,9 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
3285
2783
|
}
|
|
3286
2784
|
|
|
3287
2785
|
// src/commands/path-too-long.ts
|
|
3288
|
-
import { existsSync as
|
|
3289
|
-
import { mkdir as mkdir4, readFile as
|
|
3290
|
-
import { dirname as
|
|
2786
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2787
|
+
import { mkdir as mkdir4, readFile as readFile12, rename as rename3, unlink } from "fs/promises";
|
|
2788
|
+
import { dirname as dirname5, join as join16, posix, resolve as resolve4 } from "path";
|
|
3291
2789
|
var MAX_PATH_LENGTH = 240;
|
|
3292
2790
|
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
3293
2791
|
async function runPathTooLong(input) {
|
|
@@ -3320,10 +2818,10 @@ async function fixPathTooLong(input) {
|
|
|
3320
2818
|
}
|
|
3321
2819
|
try {
|
|
3322
2820
|
if (target.mode === "dedupe") {
|
|
3323
|
-
await
|
|
2821
|
+
await unlink(join16(input.vault, violation.relPath));
|
|
3324
2822
|
} else {
|
|
3325
|
-
await mkdir4(
|
|
3326
|
-
await
|
|
2823
|
+
await mkdir4(dirname5(join16(input.vault, target.relPath)), { recursive: true });
|
|
2824
|
+
await rename3(join16(input.vault, violation.relPath), join16(input.vault, target.relPath));
|
|
3327
2825
|
}
|
|
3328
2826
|
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
3329
2827
|
} catch {
|
|
@@ -3337,7 +2835,7 @@ async function fixPathTooLong(input) {
|
|
|
3337
2835
|
for (const page of afterScan.data.allMarkdown) {
|
|
3338
2836
|
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
3339
2837
|
try {
|
|
3340
|
-
const original = await
|
|
2838
|
+
const original = await readFile12(page.absPath, "utf8");
|
|
3341
2839
|
let updated = original;
|
|
3342
2840
|
for (const fix of fixed) {
|
|
3343
2841
|
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
@@ -3374,7 +2872,7 @@ function findPathTooLongViolations(pages, maxLength) {
|
|
|
3374
2872
|
}
|
|
3375
2873
|
function maxFixPathLength(vault) {
|
|
3376
2874
|
if (process.platform !== "win32") return MAX_PATH_LENGTH;
|
|
3377
|
-
const root =
|
|
2875
|
+
const root = resolve4(vault);
|
|
3378
2876
|
const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
|
|
3379
2877
|
const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
|
|
3380
2878
|
return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
|
|
@@ -3400,9 +2898,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
3400
2898
|
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
3401
2899
|
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
3402
2900
|
if (candidate === original || candidate.length > maxLength) continue;
|
|
3403
|
-
const candidatePath =
|
|
3404
|
-
if (!
|
|
3405
|
-
if (await hasSameContent(
|
|
2901
|
+
const candidatePath = join16(vault, candidate);
|
|
2902
|
+
if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
2903
|
+
if (await hasSameContent(join16(vault, original), candidatePath)) {
|
|
3406
2904
|
return { relPath: candidate, mode: "dedupe" };
|
|
3407
2905
|
}
|
|
3408
2906
|
}
|
|
@@ -3411,8 +2909,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
|
3411
2909
|
function candidateRelPaths(preferred, maxLength) {
|
|
3412
2910
|
const candidates = [preferred];
|
|
3413
2911
|
if (preferred.length > maxLength) return candidates;
|
|
3414
|
-
const dir =
|
|
3415
|
-
const filename =
|
|
2912
|
+
const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
|
|
2913
|
+
const filename = posix.basename(preferred);
|
|
3416
2914
|
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
3417
2915
|
const base = ext ? filename.slice(0, -3) : filename;
|
|
3418
2916
|
const dirPrefix = dir ? `${dir}/` : "";
|
|
@@ -3426,7 +2924,7 @@ function candidateRelPaths(preferred, maxLength) {
|
|
|
3426
2924
|
}
|
|
3427
2925
|
async function hasSameContent(a, b) {
|
|
3428
2926
|
try {
|
|
3429
|
-
const [left, right] = await Promise.all([
|
|
2927
|
+
const [left, right] = await Promise.all([readFile12(a), readFile12(b)]);
|
|
3430
2928
|
return left.equals(right);
|
|
3431
2929
|
} catch {
|
|
3432
2930
|
return false;
|
|
@@ -3439,8 +2937,8 @@ function shouldRewriteReferences(relPath) {
|
|
|
3439
2937
|
}
|
|
3440
2938
|
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
3441
2939
|
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
3442
|
-
const oldStem =
|
|
3443
|
-
const newStem =
|
|
2940
|
+
const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
|
|
2941
|
+
const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
|
|
3444
2942
|
if (oldStem !== newStem) {
|
|
3445
2943
|
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3446
2944
|
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
@@ -3492,6 +2990,7 @@ function buildCliSurface() {
|
|
|
3492
2990
|
program.command("doctor").option("--check-snapshotter");
|
|
3493
2991
|
program.command("status").option("--wiki <name>");
|
|
3494
2992
|
program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
|
|
2993
|
+
program.command("remove").option("--wiki <name>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--reason <text>");
|
|
3495
2994
|
program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
|
|
3496
2995
|
program.command("dedup").option("--apply").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
|
|
3497
2996
|
program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
|
|
@@ -3552,16 +3051,16 @@ function buildCliSurface() {
|
|
|
3552
3051
|
fleetCmd.command("health").option("--file <path>").option("--host-id <id>").option("--json");
|
|
3553
3052
|
const surface = /* @__PURE__ */ new Map();
|
|
3554
3053
|
const rootFlags = new Set(program.options.map((o) => o.long ?? o.short).filter((f) => f != null));
|
|
3555
|
-
function
|
|
3054
|
+
function walk(cmd, prefix, parentFlags) {
|
|
3556
3055
|
const key = prefix ? `${prefix}.${cmd.name()}` : cmd.name();
|
|
3557
3056
|
const flags = /* @__PURE__ */ new Set([...parentFlags, ...cmd.options.map((o) => o.long ?? o.short).filter((f) => f != null)]);
|
|
3558
3057
|
surface.set(key, flags);
|
|
3559
3058
|
for (const sub of cmd.commands) {
|
|
3560
|
-
|
|
3059
|
+
walk(sub, key, flags);
|
|
3561
3060
|
}
|
|
3562
3061
|
}
|
|
3563
3062
|
for (const cmd of program.commands) {
|
|
3564
|
-
|
|
3063
|
+
walk(cmd, "", rootFlags);
|
|
3565
3064
|
}
|
|
3566
3065
|
return surface;
|
|
3567
3066
|
}
|
|
@@ -3837,7 +3336,7 @@ function recomputeRawSha256IfPresent(content) {
|
|
|
3837
3336
|
const split = splitFrontmatter(content);
|
|
3838
3337
|
if (!split.ok) return content;
|
|
3839
3338
|
if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
|
|
3840
|
-
const sha256 =
|
|
3339
|
+
const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
|
|
3841
3340
|
const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
|
|
3842
3341
|
return `---
|
|
3843
3342
|
${rawFrontmatter}
|
|
@@ -3885,24 +3384,24 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
|
3885
3384
|
const entries = await readdir3(absDir, { withFileTypes: true });
|
|
3886
3385
|
const pages = [];
|
|
3887
3386
|
for (const entry of entries) {
|
|
3888
|
-
const absPath =
|
|
3387
|
+
const absPath = join17(absDir, entry.name);
|
|
3889
3388
|
if (entry.isDirectory()) {
|
|
3890
3389
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3891
3390
|
pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
|
|
3892
3391
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3893
|
-
pages.push({ absPath, relPath:
|
|
3392
|
+
pages.push({ absPath, relPath: relative2(vaultRoot, absPath).split(sep2).join("/") });
|
|
3894
3393
|
}
|
|
3895
3394
|
}
|
|
3896
3395
|
return pages;
|
|
3897
3396
|
}
|
|
3898
3397
|
async function collectCliRefsPages(vault) {
|
|
3899
|
-
if (!
|
|
3398
|
+
if (!existsSync6(join17(vault, "SCHEMA.md"))) {
|
|
3900
3399
|
return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
|
|
3901
3400
|
}
|
|
3902
3401
|
const pages = [];
|
|
3903
3402
|
for (const dir of CLI_REFS_TYPED_DIRS) {
|
|
3904
|
-
const absDir =
|
|
3905
|
-
if (!
|
|
3403
|
+
const absDir = join17(vault, dir);
|
|
3404
|
+
if (!existsSync6(absDir)) continue;
|
|
3906
3405
|
pages.push(...await walkMarkdownFiles(absDir, vault));
|
|
3907
3406
|
}
|
|
3908
3407
|
return ok(pages);
|
|
@@ -4023,7 +3522,7 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
|
|
|
4023
3522
|
for (const relPath of fileSourceUrlFrontmatterFlags) {
|
|
4024
3523
|
try {
|
|
4025
3524
|
const absPath = `${input.vault}/${relPath}`;
|
|
4026
|
-
const raw = await
|
|
3525
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4027
3526
|
const parts = raw.split("---", 3);
|
|
4028
3527
|
if (parts.length < 3) {
|
|
4029
3528
|
unresolved.push(relPath);
|
|
@@ -4416,8 +3915,8 @@ async function runLint(input) {
|
|
|
4416
3915
|
const readKnowledgeContent = (slug) => {
|
|
4417
3916
|
const existing = knowledgeContentCache.get(slug);
|
|
4418
3917
|
if (existing) return existing;
|
|
4419
|
-
const knowledgePath =
|
|
4420
|
-
const pending =
|
|
3918
|
+
const knowledgePath = join17(lintVault, "projects", slug, "knowledge.md");
|
|
3919
|
+
const pending = existsSync6(knowledgePath) ? readFile13(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
|
|
4421
3920
|
knowledgeContentCache.set(slug, pending);
|
|
4422
3921
|
return pending;
|
|
4423
3922
|
};
|
|
@@ -4523,7 +4022,7 @@ async function runLint(input) {
|
|
|
4523
4022
|
for (const relPath of legacyPages) {
|
|
4524
4023
|
try {
|
|
4525
4024
|
const absPath = `${input.vault}/${relPath}`;
|
|
4526
|
-
const raw = await
|
|
4025
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4527
4026
|
const split = splitFrontmatter(raw);
|
|
4528
4027
|
if (!split.ok) {
|
|
4529
4028
|
unresolved.push(relPath);
|
|
@@ -4622,7 +4121,7 @@ ${newBody}`;
|
|
|
4622
4121
|
for (const relPath of noOverview) {
|
|
4623
4122
|
try {
|
|
4624
4123
|
const absPath = `${input.vault}/${relPath}`;
|
|
4625
|
-
const raw = await
|
|
4124
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4626
4125
|
const split = splitFrontmatter(raw);
|
|
4627
4126
|
if (!split.ok) {
|
|
4628
4127
|
unresolved.push(relPath);
|
|
@@ -4663,7 +4162,7 @@ ${trimmedBody}`;
|
|
|
4663
4162
|
for (const relPath of missingTldrFlags) {
|
|
4664
4163
|
try {
|
|
4665
4164
|
const absPath = `${input.vault}/${relPath}`;
|
|
4666
|
-
const raw = await
|
|
4165
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4667
4166
|
const split = splitFrontmatter(raw);
|
|
4668
4167
|
if (!split.ok) {
|
|
4669
4168
|
unresolved.push(relPath);
|
|
@@ -4713,7 +4212,7 @@ ${lines.join("\n")}`;
|
|
|
4713
4212
|
for (const relPath of wikilinkCitationFlags) {
|
|
4714
4213
|
try {
|
|
4715
4214
|
const absPath = `${input.vault}/${relPath}`;
|
|
4716
|
-
const raw = await
|
|
4215
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4717
4216
|
const split = splitFrontmatter(raw);
|
|
4718
4217
|
if (!split.ok) {
|
|
4719
4218
|
unresolved.push(relPath);
|
|
@@ -5054,14 +4553,14 @@ async function runSyncLintDelta(input) {
|
|
|
5054
4553
|
}
|
|
5055
4554
|
|
|
5056
4555
|
// src/commands/config.ts
|
|
5057
|
-
import { readFile as
|
|
5058
|
-
import { existsSync as
|
|
5059
|
-
import { join as
|
|
4556
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4557
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4558
|
+
import { join as join18 } from "path";
|
|
5060
4559
|
function validateKey(key) {
|
|
5061
4560
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
5062
4561
|
}
|
|
5063
4562
|
function configPath(home) {
|
|
5064
|
-
return
|
|
4563
|
+
return join18(home, ".skillwiki", ".env");
|
|
5065
4564
|
}
|
|
5066
4565
|
async function runConfigGet(input) {
|
|
5067
4566
|
if (!validateKey(input.key)) {
|
|
@@ -5079,7 +4578,7 @@ async function runConfigSet(input) {
|
|
|
5079
4578
|
try {
|
|
5080
4579
|
let originalContent;
|
|
5081
4580
|
try {
|
|
5082
|
-
originalContent = await
|
|
4581
|
+
originalContent = await readFile14(filePath, "utf8");
|
|
5083
4582
|
} catch {
|
|
5084
4583
|
}
|
|
5085
4584
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -5092,505 +4591,43 @@ async function runConfigSet(input) {
|
|
|
5092
4591
|
}
|
|
5093
4592
|
async function runConfigList(input) {
|
|
5094
4593
|
const map = await parseDotenvFile(configPath(input.home));
|
|
5095
|
-
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
5096
|
-
let profiles;
|
|
5097
|
-
if (input.profiles) {
|
|
5098
|
-
const defaultProfile = map["WIKI_DEFAULT"];
|
|
5099
|
-
profiles = [];
|
|
5100
|
-
for (const key of Object.keys(map)) {
|
|
5101
|
-
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
5102
|
-
if (m && key !== "WIKI_PATH") {
|
|
5103
|
-
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
5104
|
-
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
5105
|
-
}
|
|
5106
|
-
}
|
|
5107
|
-
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
5108
|
-
}
|
|
5109
|
-
const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
|
|
5110
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
5111
|
-
}
|
|
5112
|
-
async function runConfigPath(input) {
|
|
5113
|
-
const filePath = configPath(input.home);
|
|
5114
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync8(filePath), humanHint: filePath }) };
|
|
5115
|
-
}
|
|
5116
|
-
|
|
5117
|
-
// src/commands/fleet.ts
|
|
5118
|
-
import { readFile as readFile17 } from "fs/promises";
|
|
5119
|
-
import { hostname as nodeHostname, userInfo } from "os";
|
|
5120
|
-
import { join as join20 } from "path";
|
|
5121
|
-
import yaml3 from "js-yaml";
|
|
5122
|
-
var FLEET_REL_PATH = join20("projects", "llm-wiki", "architecture", "fleet.yaml");
|
|
5123
|
-
async function runFleetValidate(input) {
|
|
5124
|
-
const loaded = await loadFleetManifest(input.file);
|
|
5125
|
-
if (!loaded.ok) {
|
|
5126
|
-
if (loaded.error === "FILE_NOT_FOUND") {
|
|
5127
|
-
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
5128
|
-
}
|
|
5129
|
-
const errors = fleetLoadErrors(loaded);
|
|
5130
|
-
return invalidFleet(errors);
|
|
5131
|
-
}
|
|
5132
|
-
const warnings = fleetWarnings(loaded.manifest);
|
|
5133
|
-
const snapshotter = findSnapshotter(loaded.manifest);
|
|
5134
|
-
return {
|
|
5135
|
-
exitCode: ExitCode.OK,
|
|
5136
|
-
result: ok({
|
|
5137
|
-
valid: true,
|
|
5138
|
-
errors: [],
|
|
5139
|
-
warnings,
|
|
5140
|
-
host_count: Object.keys(loaded.manifest.hosts).length,
|
|
5141
|
-
snapshotter,
|
|
5142
|
-
humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
|
|
5143
|
-
})
|
|
5144
|
-
};
|
|
5145
|
-
}
|
|
5146
|
-
async function runFleetContext(input) {
|
|
5147
|
-
const env = input.env ?? process.env;
|
|
5148
|
-
const home = input.home ?? env.HOME ?? "";
|
|
5149
|
-
const cwd = input.cwd ?? process.cwd();
|
|
5150
|
-
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
5151
|
-
const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
|
|
5152
|
-
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
5153
|
-
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
5154
|
-
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5155
|
-
const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
|
|
5156
|
-
if (!loaded.ok) {
|
|
5157
|
-
const warnings = ["fleet manifest unavailable or invalid"];
|
|
5158
|
-
const markdown2 = formatUnknownContext({
|
|
5159
|
-
generatedAt,
|
|
5160
|
-
osHostname,
|
|
5161
|
-
user,
|
|
5162
|
-
cwd,
|
|
5163
|
-
vault,
|
|
5164
|
-
reason: warnings[0],
|
|
5165
|
-
trace: [],
|
|
5166
|
-
warnings
|
|
5167
|
-
});
|
|
5168
|
-
return {
|
|
5169
|
-
exitCode: ExitCode.OK,
|
|
5170
|
-
result: ok({
|
|
5171
|
-
manifest_loaded: false,
|
|
5172
|
-
generated_at: generatedAt,
|
|
5173
|
-
identity_status: "unknown",
|
|
5174
|
-
resolver_trace: [],
|
|
5175
|
-
warnings,
|
|
5176
|
-
markdown: markdown2,
|
|
5177
|
-
humanHint: markdown2
|
|
5178
|
-
})
|
|
5179
|
-
};
|
|
5180
|
-
}
|
|
5181
|
-
const resolved = await resolveFleetHostId({
|
|
5182
|
-
manifest: loaded.manifest,
|
|
5183
|
-
hostId: input.hostId,
|
|
5184
|
-
env,
|
|
5185
|
-
home,
|
|
5186
|
-
osHostname
|
|
5187
|
-
});
|
|
5188
|
-
if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
|
|
5189
|
-
const source = resolved.source ?? "unknown";
|
|
5190
|
-
const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
|
|
5191
|
-
const markdown2 = formatInvalidContext({
|
|
5192
|
-
generatedAt,
|
|
5193
|
-
hostId: resolved.hostId,
|
|
5194
|
-
source,
|
|
5195
|
-
osHostname,
|
|
5196
|
-
user,
|
|
5197
|
-
cwd,
|
|
5198
|
-
vault,
|
|
5199
|
-
trace: resolved.trace,
|
|
5200
|
-
warnings
|
|
5201
|
-
});
|
|
5202
|
-
return {
|
|
5203
|
-
exitCode: ExitCode.OK,
|
|
5204
|
-
result: ok({
|
|
5205
|
-
manifest_loaded: true,
|
|
5206
|
-
host_id: resolved.hostId,
|
|
5207
|
-
source: resolved.source,
|
|
5208
|
-
generated_at: generatedAt,
|
|
5209
|
-
identity_status: "invalid",
|
|
5210
|
-
resolver_trace: resolved.trace,
|
|
5211
|
-
warnings,
|
|
5212
|
-
markdown: markdown2,
|
|
5213
|
-
humanHint: markdown2
|
|
5214
|
-
})
|
|
5215
|
-
};
|
|
5216
|
-
}
|
|
5217
|
-
if (!resolved.hostId) {
|
|
5218
|
-
const warnings = ["host identity is unresolved"];
|
|
5219
|
-
const markdown2 = formatUnknownContext({
|
|
5220
|
-
generatedAt,
|
|
5221
|
-
osHostname,
|
|
5222
|
-
user,
|
|
5223
|
-
cwd,
|
|
5224
|
-
vault,
|
|
5225
|
-
reason: warnings[0],
|
|
5226
|
-
trace: resolved.trace,
|
|
5227
|
-
warnings
|
|
5228
|
-
});
|
|
5229
|
-
return {
|
|
5230
|
-
exitCode: ExitCode.OK,
|
|
5231
|
-
result: ok({
|
|
5232
|
-
manifest_loaded: true,
|
|
5233
|
-
generated_at: generatedAt,
|
|
5234
|
-
identity_status: "unknown",
|
|
5235
|
-
resolver_trace: resolved.trace,
|
|
5236
|
-
warnings,
|
|
5237
|
-
markdown: markdown2,
|
|
5238
|
-
humanHint: markdown2
|
|
5239
|
-
})
|
|
5240
|
-
};
|
|
5241
|
-
}
|
|
5242
|
-
const markdown = formatKnownContext({
|
|
5243
|
-
manifest: loaded.manifest,
|
|
5244
|
-
hostId: resolved.hostId,
|
|
5245
|
-
source: resolved.source,
|
|
5246
|
-
generatedAt,
|
|
5247
|
-
osHostname,
|
|
5248
|
-
user,
|
|
5249
|
-
cwd,
|
|
5250
|
-
vault,
|
|
5251
|
-
trace: resolved.trace
|
|
5252
|
-
});
|
|
5253
|
-
return {
|
|
5254
|
-
exitCode: ExitCode.OK,
|
|
5255
|
-
result: ok({
|
|
5256
|
-
manifest_loaded: true,
|
|
5257
|
-
host_id: resolved.hostId,
|
|
5258
|
-
source: resolved.source,
|
|
5259
|
-
generated_at: generatedAt,
|
|
5260
|
-
identity_status: "known",
|
|
5261
|
-
resolver_trace: resolved.trace,
|
|
5262
|
-
warnings: [],
|
|
5263
|
-
markdown,
|
|
5264
|
-
humanHint: markdown
|
|
5265
|
-
})
|
|
5266
|
-
};
|
|
5267
|
-
}
|
|
5268
|
-
function fleetContextEnv(input) {
|
|
5269
|
-
const env = input.env ?? process.env;
|
|
5270
|
-
const home = input.home ?? env.HOME ?? "";
|
|
5271
|
-
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
5272
|
-
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
5273
|
-
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
5274
|
-
return { env, home, osHostname, vault, file };
|
|
5275
|
-
}
|
|
5276
|
-
async function loadFleetManifestAndHost(input) {
|
|
5277
|
-
const { env, home, osHostname, file } = fleetContextEnv(input);
|
|
5278
|
-
if (!file) return null;
|
|
5279
|
-
const loaded = await loadFleetManifest(file);
|
|
5280
|
-
if (!loaded.ok) return null;
|
|
5281
|
-
const resolved = await resolveFleetHostId({
|
|
5282
|
-
manifest: loaded.manifest,
|
|
5283
|
-
hostId: input.hostId,
|
|
5284
|
-
env,
|
|
5285
|
-
home,
|
|
5286
|
-
osHostname
|
|
5287
|
-
});
|
|
5288
|
-
if (!resolved.hostId) {
|
|
5289
|
-
return {
|
|
5290
|
-
manifest: loaded.manifest,
|
|
5291
|
-
hostId: void 0,
|
|
5292
|
-
source: resolved.source,
|
|
5293
|
-
warnings: ["host identity is unresolved"],
|
|
5294
|
-
identityStatus: "unknown"
|
|
5295
|
-
};
|
|
5296
|
-
}
|
|
5297
|
-
if (!loaded.manifest.hosts[resolved.hostId]) {
|
|
5298
|
-
const source = resolved.source ?? "unknown";
|
|
5299
|
-
return {
|
|
5300
|
-
manifest: loaded.manifest,
|
|
5301
|
-
hostId: resolved.hostId,
|
|
5302
|
-
source: resolved.source,
|
|
5303
|
-
warnings: [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`],
|
|
5304
|
-
identityStatus: "invalid"
|
|
5305
|
-
};
|
|
5306
|
-
}
|
|
5307
|
-
return {
|
|
5308
|
-
manifest: loaded.manifest,
|
|
5309
|
-
hostId: resolved.hostId,
|
|
5310
|
-
source: resolved.source,
|
|
5311
|
-
warnings: [],
|
|
5312
|
-
identityStatus: "known"
|
|
5313
|
-
};
|
|
5314
|
-
}
|
|
5315
|
-
function snapshotterAliasForLocalHost(fleetLoad) {
|
|
5316
|
-
if (!fleetLoad?.manifest || !fleetLoad.hostId) return void 0;
|
|
5317
|
-
const snapshotterId = Object.entries(fleetLoad.manifest.hosts).find(([, h]) => h.role === "snapshotter")?.[0];
|
|
5318
|
-
if (!snapshotterId) return void 0;
|
|
5319
|
-
const profile = fleetLoad.manifest.hosts[snapshotterId]?.access?.from?.[fleetLoad.hostId];
|
|
5320
|
-
if (!profile || profile.status !== "configured" && profile.status !== "local") return void 0;
|
|
5321
|
-
const aliases = profile.ssh_aliases ?? [];
|
|
5322
|
-
return aliases.length > 0 ? aliases[0] : void 0;
|
|
5323
|
-
}
|
|
5324
|
-
function satelliteGateFromFleetLoad(load) {
|
|
5325
|
-
if (!load?.hostId) return { satelliteExpected: false };
|
|
5326
|
-
const host = load.manifest.hosts[load.hostId];
|
|
5327
|
-
if (!host) return { satelliteExpected: false };
|
|
5328
|
-
return { satelliteExpected: host.maintenance?.skillwiki_satellite?.enabled === true };
|
|
5329
|
-
}
|
|
5330
|
-
async function loadFleetManifest(file) {
|
|
5331
|
-
let text;
|
|
5332
|
-
try {
|
|
5333
|
-
text = await readFile17(file, "utf8");
|
|
5334
|
-
} catch {
|
|
5335
|
-
return { ok: false, error: "FILE_NOT_FOUND" };
|
|
5336
|
-
}
|
|
5337
|
-
let parsed;
|
|
5338
|
-
try {
|
|
5339
|
-
parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
|
|
5340
|
-
} catch (error) {
|
|
5341
|
-
return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
|
|
5342
|
-
}
|
|
5343
|
-
const result = FleetManifestSchema.safeParse(parsed);
|
|
5344
|
-
if (!result.success) {
|
|
5345
|
-
return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
|
|
5346
|
-
}
|
|
5347
|
-
return { ok: true, manifest: result.data };
|
|
5348
|
-
}
|
|
5349
|
-
function invalidFleet(errors) {
|
|
5350
|
-
return {
|
|
5351
|
-
exitCode: ExitCode.FLEET_MANIFEST_INVALID,
|
|
5352
|
-
result: ok({
|
|
5353
|
-
valid: false,
|
|
5354
|
-
errors,
|
|
5355
|
-
warnings: [],
|
|
5356
|
-
host_count: 0,
|
|
5357
|
-
humanHint: `INVALID fleet manifest
|
|
5358
|
-
${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
|
|
5359
|
-
})
|
|
5360
|
-
};
|
|
5361
|
-
}
|
|
5362
|
-
function fleetLoadErrors(loaded) {
|
|
5363
|
-
if (loaded.error === "INVALID_YAML") {
|
|
5364
|
-
return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
|
|
5365
|
-
}
|
|
5366
|
-
if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
|
|
5367
|
-
return loaded.detail.map((issue) => {
|
|
5368
|
-
const zodIssue = issue;
|
|
5369
|
-
return {
|
|
5370
|
-
path: (zodIssue.path ?? []).join("."),
|
|
5371
|
-
message: zodIssue.message ?? "invalid value"
|
|
5372
|
-
};
|
|
5373
|
-
});
|
|
5374
|
-
}
|
|
5375
|
-
return [{ path: "", message: loaded.error }];
|
|
5376
|
-
}
|
|
5377
|
-
function fleetWarnings(manifest) {
|
|
5378
|
-
const warnings = [];
|
|
5379
|
-
for (const [id, host] of Object.entries(manifest.hosts)) {
|
|
5380
|
-
if (host.role === "snapshotter" && host.protected !== true) {
|
|
5381
|
-
warnings.push(`snapshotter host '${id}' is not protected=true`);
|
|
5382
|
-
}
|
|
5383
|
-
}
|
|
5384
|
-
return warnings;
|
|
5385
|
-
}
|
|
5386
|
-
function findSnapshotter(manifest) {
|
|
5387
|
-
return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
|
|
5388
|
-
}
|
|
5389
|
-
async function resolveFleetHostId(input) {
|
|
5390
|
-
const trace = [];
|
|
5391
|
-
if (input.hostId) {
|
|
5392
|
-
trace.push({ source: "--host-id", status: "matched", value: input.hostId });
|
|
5393
|
-
return { hostId: input.hostId, source: "host-id", trace };
|
|
5394
|
-
}
|
|
5395
|
-
trace.push({ source: "--host-id", status: "unset" });
|
|
5396
|
-
if (input.env.SKILLWIKI_HOST_ID) {
|
|
5397
|
-
trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
|
|
5398
|
-
return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
|
|
5399
|
-
}
|
|
5400
|
-
trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
|
|
5401
|
-
if (input.env.AGENT_HOST_ID) {
|
|
5402
|
-
trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
|
|
5403
|
-
return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
|
|
5404
|
-
}
|
|
5405
|
-
trace.push({ source: "AGENT_HOST_ID", status: "unset" });
|
|
5406
|
-
if (input.home) {
|
|
5407
|
-
const dotenv = await parseDotenvFile(join20(input.home, ".skillwiki", ".env"));
|
|
5408
|
-
if (dotenv.SKILLWIKI_HOST_ID) {
|
|
5409
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
|
|
5410
|
-
return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
|
|
5411
|
-
}
|
|
5412
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
|
|
5413
|
-
} else {
|
|
5414
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
|
|
5415
|
-
}
|
|
5416
|
-
if (input.env.VS_HOSTNAME) {
|
|
5417
|
-
trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
|
|
5418
|
-
return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
|
|
5419
|
-
}
|
|
5420
|
-
trace.push({ source: "VS_HOSTNAME", status: "unset" });
|
|
5421
|
-
const hostname = input.osHostname.trim();
|
|
5422
|
-
if (hostname) {
|
|
5423
|
-
if (input.manifest.hosts[hostname]) {
|
|
5424
|
-
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
5425
|
-
return { hostId: hostname, source: "hostname", trace };
|
|
5426
|
-
}
|
|
5427
|
-
const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
|
|
5428
|
-
if (byHostname) {
|
|
5429
|
-
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
5430
|
-
return { hostId: byHostname[0], source: "hostname", trace };
|
|
5431
|
-
}
|
|
5432
|
-
trace.push({ source: "hostname", status: "unmatched", value: hostname });
|
|
5433
|
-
} else {
|
|
5434
|
-
trace.push({ source: "hostname", status: "unset" });
|
|
5435
|
-
}
|
|
5436
|
-
return { trace };
|
|
5437
|
-
}
|
|
5438
|
-
function formatKnownContext(input) {
|
|
5439
|
-
const host = input.manifest.hosts[input.hostId];
|
|
5440
|
-
const protectedValue = host.protected === true ? "true" : "false";
|
|
5441
|
-
const writesTo = host.writes_to.join(", ");
|
|
5442
|
-
const selfAliases = collectSelfAliases(input.manifest, input.hostId);
|
|
5443
|
-
const outbound = collectOutboundAccess(input.manifest, input.hostId);
|
|
5444
|
-
const maintenanceLines = formatMaintenanceLines(host);
|
|
5445
|
-
const guidance = host.role === "snapshotter" && host.protected === true ? `this session is already on \`${input.hostId}\`; this is a protected snapshotter host. Live-vault authoring at the resolved \`skillwiki path\` is allowed here. Do not mutate snapshot worktrees or repo-local project workspaces from this session except explicitly approved snapshot maintenance. Keep release-validation workflows read-only when they are documented as such.` : input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
|
|
5446
|
-
return [
|
|
5447
|
-
"## Runtime Host Context",
|
|
5448
|
-
"",
|
|
5449
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5450
|
-
`- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
|
|
5451
|
-
"- Identity status: `known`",
|
|
5452
|
-
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
5453
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5454
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5455
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5456
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5457
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5458
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5459
|
-
`- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
|
|
5460
|
-
...maintenanceLines,
|
|
5461
|
-
`- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
|
|
5462
|
-
`- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
|
|
5463
|
-
`- Guidance: ${guidance}`
|
|
5464
|
-
].join("\n");
|
|
5465
|
-
}
|
|
5466
|
-
function formatUnknownContext(input) {
|
|
5467
|
-
return [
|
|
5468
|
-
"## Runtime Host Context",
|
|
5469
|
-
"",
|
|
5470
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5471
|
-
"- Current machine: unknown",
|
|
5472
|
-
"- Identity status: `unknown`",
|
|
5473
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5474
|
-
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
5475
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5476
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5477
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5478
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5479
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5480
|
-
"- Fleet role: unknown",
|
|
5481
|
-
"- Self SSH aliases known in fleet: unknown",
|
|
5482
|
-
"- Declared outbound SSH from this source: unknown",
|
|
5483
|
-
`- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
|
|
5484
|
-
].join("\n");
|
|
5485
|
-
}
|
|
5486
|
-
function formatInvalidContext(input) {
|
|
5487
|
-
return [
|
|
5488
|
-
"## Runtime Host Context",
|
|
5489
|
-
"",
|
|
5490
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5491
|
-
"- Current machine: unknown",
|
|
5492
|
-
"- Identity status: `invalid`",
|
|
5493
|
-
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
5494
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5495
|
-
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
5496
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5497
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5498
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5499
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5500
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5501
|
-
"- Fleet role: unknown",
|
|
5502
|
-
"- Self SSH aliases known in fleet: unknown",
|
|
5503
|
-
"- Declared outbound SSH from this source: unknown",
|
|
5504
|
-
`- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
|
|
5505
|
-
].join("\n");
|
|
5506
|
-
}
|
|
5507
|
-
function collectSelfAliases(manifest, hostId2) {
|
|
5508
|
-
const aliases = [];
|
|
5509
|
-
const host = manifest.hosts[hostId2];
|
|
5510
|
-
const access = host?.access?.from ?? {};
|
|
5511
|
-
for (const profile of Object.values(access)) {
|
|
5512
|
-
for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
|
|
5513
|
-
}
|
|
5514
|
-
return [...new Set(aliases)];
|
|
5515
|
-
}
|
|
5516
|
-
function collectOutboundAccess(manifest, sourceHostId) {
|
|
5517
|
-
const hosts = [];
|
|
5518
|
-
for (const [targetId, target] of Object.entries(manifest.hosts)) {
|
|
5519
|
-
if (targetId === sourceHostId) continue;
|
|
5520
|
-
const profile = target.access?.from?.[sourceHostId];
|
|
5521
|
-
if (profile && (profile.status === "configured" || profile.status === "local")) {
|
|
5522
|
-
hosts.push({
|
|
5523
|
-
hostId: targetId,
|
|
5524
|
-
sshAliases: [...new Set(profile.ssh_aliases ?? [])],
|
|
5525
|
-
users: [...new Set(profile.users ?? [])]
|
|
5526
|
-
});
|
|
4594
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
4595
|
+
let profiles;
|
|
4596
|
+
if (input.profiles) {
|
|
4597
|
+
const defaultProfile = map["WIKI_DEFAULT"];
|
|
4598
|
+
profiles = [];
|
|
4599
|
+
for (const key of Object.keys(map)) {
|
|
4600
|
+
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
4601
|
+
if (m && key !== "WIKI_PATH") {
|
|
4602
|
+
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
4603
|
+
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
4604
|
+
}
|
|
5527
4605
|
}
|
|
4606
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
5528
4607
|
}
|
|
5529
|
-
|
|
5530
|
-
}
|
|
5531
|
-
function formatMaintenanceLines(host) {
|
|
5532
|
-
const satellite = host.maintenance?.skillwiki_satellite;
|
|
5533
|
-
if (!satellite?.enabled) return [];
|
|
5534
|
-
return [
|
|
5535
|
-
`- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
|
|
5536
|
-
`- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
|
|
5537
|
-
];
|
|
5538
|
-
}
|
|
5539
|
-
function formatOutboundAccess(values) {
|
|
5540
|
-
if (values.length === 0) return "none";
|
|
5541
|
-
return values.map((value) => {
|
|
5542
|
-
const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
|
|
5543
|
-
const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
|
|
5544
|
-
return `\`${value.hostId}\`${aliasPart}${usersPart}`;
|
|
5545
|
-
}).join("; ");
|
|
5546
|
-
}
|
|
5547
|
-
function formatResolution(source, hostId2) {
|
|
5548
|
-
return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId2}\`` : `unknown -> \`${hostId2}\``;
|
|
5549
|
-
}
|
|
5550
|
-
function formatTrace(values) {
|
|
5551
|
-
if (values.length === 0) return "not available";
|
|
5552
|
-
return values.map((value) => {
|
|
5553
|
-
const source = `\`${value.source}\``;
|
|
5554
|
-
if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
|
|
5555
|
-
if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
|
|
5556
|
-
return `${source} ${value.status}`;
|
|
5557
|
-
}).join("; ");
|
|
5558
|
-
}
|
|
5559
|
-
function formatWarnings(values) {
|
|
5560
|
-
return values.length > 0 ? values.join("; ") : "none";
|
|
5561
|
-
}
|
|
5562
|
-
function formatList(values) {
|
|
5563
|
-
return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
|
|
5564
|
-
}
|
|
5565
|
-
function formatMaybe(value) {
|
|
5566
|
-
return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
|
|
5567
|
-
}
|
|
5568
|
-
function safeEnvValue(value) {
|
|
5569
|
-
return value && value.trim().length > 0 ? value : void 0;
|
|
4608
|
+
const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
|
|
4609
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
5570
4610
|
}
|
|
5571
|
-
function
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
} catch {
|
|
5575
|
-
return "";
|
|
5576
|
-
}
|
|
4611
|
+
async function runConfigPath(input) {
|
|
4612
|
+
const filePath = configPath(input.home);
|
|
4613
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync7(filePath), humanHint: filePath }) };
|
|
5577
4614
|
}
|
|
5578
4615
|
|
|
5579
4616
|
// src/commands/doctor.ts
|
|
5580
|
-
import { existsSync as
|
|
5581
|
-
import { join as
|
|
4617
|
+
import { existsSync as existsSync13, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
|
|
4618
|
+
import { join as join24, resolve as resolve5 } from "path";
|
|
5582
4619
|
import { execSync as execSync2 } from "child_process";
|
|
5583
4620
|
import { platform as platform2 } from "os";
|
|
5584
4621
|
|
|
5585
4622
|
// src/utils/plugin-registry.ts
|
|
5586
|
-
import { existsSync as
|
|
5587
|
-
import { join as
|
|
5588
|
-
var REGISTRY_PATH =
|
|
5589
|
-
var CODEX_CONFIG_PATH =
|
|
4623
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
4624
|
+
import { join as join19 } from "path";
|
|
4625
|
+
var REGISTRY_PATH = join19(".claude", "plugins", "installed_plugins.json");
|
|
4626
|
+
var CODEX_CONFIG_PATH = join19(".codex", "config.toml");
|
|
5590
4627
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
5591
4628
|
function readInstalledPlugins(home) {
|
|
5592
4629
|
try {
|
|
5593
|
-
const raw =
|
|
4630
|
+
const raw = readFileSync5(join19(home, REGISTRY_PATH), "utf8");
|
|
5594
4631
|
return JSON.parse(raw);
|
|
5595
4632
|
} catch {
|
|
5596
4633
|
return null;
|
|
@@ -5626,8 +4663,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
|
|
|
5626
4663
|
function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
5627
4664
|
const config = readCodexPluginConfig(home, key, marketplace);
|
|
5628
4665
|
if (!config?.enabled) return null;
|
|
5629
|
-
const cacheRoot =
|
|
5630
|
-
if (!
|
|
4666
|
+
const cacheRoot = join19(home, ".codex", "plugins", "cache", marketplace, pluginName);
|
|
4667
|
+
if (!existsSync8(cacheRoot)) return null;
|
|
5631
4668
|
let versions;
|
|
5632
4669
|
try {
|
|
5633
4670
|
versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
@@ -5642,7 +4679,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
|
5642
4679
|
key,
|
|
5643
4680
|
pluginName,
|
|
5644
4681
|
marketplace,
|
|
5645
|
-
installPath:
|
|
4682
|
+
installPath: join19(cacheRoot, version),
|
|
5646
4683
|
version,
|
|
5647
4684
|
sourceType: config.sourceType,
|
|
5648
4685
|
source: config.source
|
|
@@ -5659,7 +4696,7 @@ function parsePluginKey(key) {
|
|
|
5659
4696
|
function readCodexPluginConfig(home, key, marketplace) {
|
|
5660
4697
|
let raw;
|
|
5661
4698
|
try {
|
|
5662
|
-
raw =
|
|
4699
|
+
raw = readFileSync5(join19(home, CODEX_CONFIG_PATH), "utf8");
|
|
5663
4700
|
} catch {
|
|
5664
4701
|
return null;
|
|
5665
4702
|
}
|
|
@@ -5701,8 +4738,8 @@ function parseTomlScalar(rawValue) {
|
|
|
5701
4738
|
}
|
|
5702
4739
|
|
|
5703
4740
|
// src/utils/conflict-markers.ts
|
|
5704
|
-
import { existsSync as
|
|
5705
|
-
import { join as
|
|
4741
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
4742
|
+
import { join as join20 } from "path";
|
|
5706
4743
|
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
5707
4744
|
const findings = [];
|
|
5708
4745
|
const lines = text.split(/\r?\n/);
|
|
@@ -5753,21 +4790,21 @@ function walkMarkdownFiles2(root, dir, rel, out) {
|
|
|
5753
4790
|
for (const entry of entries) {
|
|
5754
4791
|
if (entry.isDirectory()) {
|
|
5755
4792
|
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
5756
|
-
walkMarkdownFiles2(root,
|
|
4793
|
+
walkMarkdownFiles2(root, join20(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
5757
4794
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5758
4795
|
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
5759
4796
|
}
|
|
5760
4797
|
}
|
|
5761
4798
|
}
|
|
5762
4799
|
function scanVaultConflictMarkers(vaultRoot) {
|
|
5763
|
-
if (!
|
|
4800
|
+
if (!existsSync9(vaultRoot)) return [];
|
|
5764
4801
|
const relPaths = [];
|
|
5765
4802
|
walkMarkdownFiles2(vaultRoot, vaultRoot, "", relPaths);
|
|
5766
4803
|
const all = [];
|
|
5767
4804
|
for (const rel of relPaths) {
|
|
5768
4805
|
let text;
|
|
5769
4806
|
try {
|
|
5770
|
-
text =
|
|
4807
|
+
text = readFileSync6(join20(vaultRoot, rel), "utf8");
|
|
5771
4808
|
} catch {
|
|
5772
4809
|
continue;
|
|
5773
4810
|
}
|
|
@@ -5777,8 +4814,8 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5777
4814
|
}
|
|
5778
4815
|
|
|
5779
4816
|
// src/utils/remote-health.ts
|
|
5780
|
-
import { existsSync as
|
|
5781
|
-
import { join as
|
|
4817
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
4818
|
+
import { join as join21 } from "path";
|
|
5782
4819
|
import { execFileSync } from "child_process";
|
|
5783
4820
|
var REMOTE_PROBE_TIMEOUT_MS = 3e3;
|
|
5784
4821
|
var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
@@ -5789,7 +4826,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
|
5789
4826
|
}).trim();
|
|
5790
4827
|
function readWikiS3RemoteConfigured(home) {
|
|
5791
4828
|
try {
|
|
5792
|
-
const content =
|
|
4829
|
+
const content = readFileSync7(join21(home, ".skillwiki", ".env"), "utf8");
|
|
5793
4830
|
for (const line of content.split(/\r?\n/)) {
|
|
5794
4831
|
const trimmed = line.trim();
|
|
5795
4832
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -5814,7 +4851,7 @@ function resolveWikiS3Remote(input) {
|
|
|
5814
4851
|
return readWikiS3RemoteConfigured(input.home);
|
|
5815
4852
|
}
|
|
5816
4853
|
function probeGithubReachability(vaultPath, exec = defaultExec) {
|
|
5817
|
-
if (!
|
|
4854
|
+
if (!existsSync10(join21(vaultPath, ".git"))) return "unknown";
|
|
5818
4855
|
try {
|
|
5819
4856
|
exec("git", ["remote", "get-url", "origin"], vaultPath);
|
|
5820
4857
|
} catch {
|
|
@@ -5837,8 +4874,8 @@ function probeS3Reachability(remote, exec = defaultExec) {
|
|
|
5837
4874
|
return "unreachable";
|
|
5838
4875
|
}
|
|
5839
4876
|
}
|
|
5840
|
-
function probeSnapshotterSsh(
|
|
5841
|
-
if (!
|
|
4877
|
+
function probeSnapshotterSsh(sshAlias, exec = defaultExec) {
|
|
4878
|
+
if (!sshAlias) return "unknown";
|
|
5842
4879
|
try {
|
|
5843
4880
|
exec("ssh", [
|
|
5844
4881
|
"-o",
|
|
@@ -5847,7 +4884,7 @@ function probeSnapshotterSsh(sshAlias2, exec = defaultExec) {
|
|
|
5847
4884
|
"ConnectTimeout=3",
|
|
5848
4885
|
"-o",
|
|
5849
4886
|
"StrictHostKeyChecking=accept-new",
|
|
5850
|
-
|
|
4887
|
+
sshAlias,
|
|
5851
4888
|
"true"
|
|
5852
4889
|
]);
|
|
5853
4890
|
return "ok";
|
|
@@ -5879,11 +4916,11 @@ function probeRemoteHealth(input) {
|
|
|
5879
4916
|
}
|
|
5880
4917
|
|
|
5881
4918
|
// src/utils/satellite-run-health.ts
|
|
5882
|
-
import { existsSync as
|
|
5883
|
-
import { join as
|
|
4919
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
4920
|
+
import { join as join22 } from "path";
|
|
5884
4921
|
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
5885
4922
|
function satelliteLatestRunPath(vault) {
|
|
5886
|
-
return
|
|
4923
|
+
return join22(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
5887
4924
|
}
|
|
5888
4925
|
function isFailedRunStatus(status) {
|
|
5889
4926
|
return status === "fail" || status === "failure";
|
|
@@ -5905,9 +4942,9 @@ function readSatelliteLatestRunFromText(text) {
|
|
|
5905
4942
|
}
|
|
5906
4943
|
function readSatelliteLatestRun(vault) {
|
|
5907
4944
|
const latestPath = satelliteLatestRunPath(vault);
|
|
5908
|
-
if (!
|
|
4945
|
+
if (!existsSync11(latestPath)) return null;
|
|
5909
4946
|
try {
|
|
5910
|
-
return parseLatestRunFile(
|
|
4947
|
+
return parseLatestRunFile(readFileSync8(latestPath, "utf8"));
|
|
5911
4948
|
} catch {
|
|
5912
4949
|
return null;
|
|
5913
4950
|
}
|
|
@@ -5936,8 +4973,8 @@ function evaluateSatelliteRunHealth(vault, now) {
|
|
|
5936
4973
|
// src/utils/s3-mount-health.ts
|
|
5937
4974
|
import { execSync } from "child_process";
|
|
5938
4975
|
import { platform } from "os";
|
|
5939
|
-
import { readFileSync as
|
|
5940
|
-
import { join as
|
|
4976
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile15 } from "fs";
|
|
4977
|
+
import { join as join23 } from "path";
|
|
5941
4978
|
var OS = platform();
|
|
5942
4979
|
function findRcloneMountPid() {
|
|
5943
4980
|
try {
|
|
@@ -6021,7 +5058,7 @@ function extractRcloneFs(args) {
|
|
|
6021
5058
|
function getRcloneArgs(pid) {
|
|
6022
5059
|
try {
|
|
6023
5060
|
if (OS === "linux") {
|
|
6024
|
-
const raw =
|
|
5061
|
+
const raw = readFileSync9(`/proc/${pid}/cmdline`);
|
|
6025
5062
|
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
6026
5063
|
} else {
|
|
6027
5064
|
const out = execSync(`ps -o args= -p ${pid}`, {
|
|
@@ -6064,7 +5101,7 @@ function queryRcloneRC(rcAddr, fs) {
|
|
|
6064
5101
|
function detectFuseMount(vaultPath) {
|
|
6065
5102
|
try {
|
|
6066
5103
|
if (OS === "linux") {
|
|
6067
|
-
const mounts =
|
|
5104
|
+
const mounts = readFileSync9("/proc/mounts", "utf8");
|
|
6068
5105
|
let best = null;
|
|
6069
5106
|
for (const line of mounts.split("\n")) {
|
|
6070
5107
|
const parts = line.split(" ");
|
|
@@ -6095,7 +5132,7 @@ function detectFuseMount(vaultPath) {
|
|
|
6095
5132
|
return null;
|
|
6096
5133
|
}
|
|
6097
5134
|
function writeTest(dir) {
|
|
6098
|
-
const testFile =
|
|
5135
|
+
const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
6099
5136
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
6100
5137
|
const start = Date.now();
|
|
6101
5138
|
try {
|
|
@@ -6106,7 +5143,7 @@ function writeTest(dir) {
|
|
|
6106
5143
|
const writeMs = Date.now() - start;
|
|
6107
5144
|
const readStart = Date.now();
|
|
6108
5145
|
try {
|
|
6109
|
-
const back =
|
|
5146
|
+
const back = readFile15(testFile, "utf8");
|
|
6110
5147
|
const readMs = Date.now() - readStart;
|
|
6111
5148
|
if (back !== payload) {
|
|
6112
5149
|
try {
|
|
@@ -6180,14 +5217,14 @@ function checkNodeVersion() {
|
|
|
6180
5217
|
function detectCliChannels(argv, home) {
|
|
6181
5218
|
const channels = [];
|
|
6182
5219
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
6183
|
-
const devPath =
|
|
5220
|
+
const devPath = resolve5(argv[1]);
|
|
6184
5221
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
6185
5222
|
}
|
|
6186
5223
|
try {
|
|
6187
5224
|
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
6188
5225
|
if (whichOut) {
|
|
6189
5226
|
const isDev = isDevSymlink(whichOut);
|
|
6190
|
-
if (!channels.some((c) => c.path ===
|
|
5227
|
+
if (!channels.some((c) => c.path === resolve5(whichOut))) {
|
|
6191
5228
|
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
6192
5229
|
}
|
|
6193
5230
|
}
|
|
@@ -6195,22 +5232,22 @@ function detectCliChannels(argv, home) {
|
|
|
6195
5232
|
}
|
|
6196
5233
|
const plugin = findPlugin(home);
|
|
6197
5234
|
if (plugin) {
|
|
6198
|
-
const pluginBin =
|
|
6199
|
-
if (
|
|
5235
|
+
const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
|
|
5236
|
+
if (existsSync13(pluginBin)) {
|
|
6200
5237
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
6201
5238
|
}
|
|
6202
5239
|
}
|
|
6203
|
-
const installBin =
|
|
6204
|
-
if (
|
|
5240
|
+
const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
|
|
5241
|
+
if (existsSync13(installBin)) {
|
|
6205
5242
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
6206
5243
|
}
|
|
6207
5244
|
return channels;
|
|
6208
5245
|
}
|
|
6209
5246
|
function isDevSymlink(binPath) {
|
|
6210
5247
|
try {
|
|
6211
|
-
const st =
|
|
5248
|
+
const st = lstatSync(binPath);
|
|
6212
5249
|
if (st.isSymbolicLink()) {
|
|
6213
|
-
const target =
|
|
5250
|
+
const target = resolve5(binPath, "..", readlinkSync(binPath));
|
|
6214
5251
|
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
6215
5252
|
}
|
|
6216
5253
|
} catch {
|
|
@@ -6262,7 +5299,7 @@ function isDevSourceRun(argv) {
|
|
|
6262
5299
|
}
|
|
6263
5300
|
async function checkConfigFile(home) {
|
|
6264
5301
|
const cfgPath = configPath(home);
|
|
6265
|
-
if (!
|
|
5302
|
+
if (!existsSync13(cfgPath)) {
|
|
6266
5303
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
6267
5304
|
}
|
|
6268
5305
|
try {
|
|
@@ -6277,7 +5314,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
6277
5314
|
if (resolvedPath === void 0) {
|
|
6278
5315
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6279
5316
|
}
|
|
6280
|
-
if (
|
|
5317
|
+
if (existsSync13(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
|
|
6281
5318
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
6282
5319
|
}
|
|
6283
5320
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -6286,13 +5323,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
6286
5323
|
if (resolvedPath === void 0) {
|
|
6287
5324
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6288
5325
|
}
|
|
6289
|
-
if (!
|
|
5326
|
+
if (!existsSync13(resolvedPath)) {
|
|
6290
5327
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
6291
5328
|
}
|
|
6292
5329
|
const missing = [];
|
|
6293
|
-
if (!
|
|
5330
|
+
if (!existsSync13(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
6294
5331
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
6295
|
-
if (!
|
|
5332
|
+
if (!existsSync13(join24(resolvedPath, dir))) missing.push(dir + "/");
|
|
6296
5333
|
}
|
|
6297
5334
|
if (missing.length === 0) {
|
|
6298
5335
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -6300,8 +5337,8 @@ function checkVaultStructure(resolvedPath) {
|
|
|
6300
5337
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
6301
5338
|
}
|
|
6302
5339
|
function checkSkillsInstalled(home, cwd) {
|
|
6303
|
-
const srcDir = cwd ?
|
|
6304
|
-
if (srcDir &&
|
|
5340
|
+
const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
|
|
5341
|
+
if (srcDir && existsSync13(srcDir)) {
|
|
6305
5342
|
const found = findInstalledSkillMd(srcDir);
|
|
6306
5343
|
if (found.length > 0) {
|
|
6307
5344
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
@@ -6314,8 +5351,8 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
6314
5351
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
6315
5352
|
}
|
|
6316
5353
|
}
|
|
6317
|
-
const skillsDir =
|
|
6318
|
-
if (
|
|
5354
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
5355
|
+
if (existsSync13(skillsDir)) {
|
|
6319
5356
|
const found = findInstalledSkillMd(skillsDir);
|
|
6320
5357
|
if (found.length > 0) {
|
|
6321
5358
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -6325,10 +5362,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
6325
5362
|
}
|
|
6326
5363
|
function checkDuplicateSkills(home) {
|
|
6327
5364
|
const plugin = findPlugin(home);
|
|
6328
|
-
const skillsDir =
|
|
5365
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
6329
5366
|
const agentSkillDirs = [
|
|
6330
|
-
{ label: "~/.codex/skills/", path:
|
|
6331
|
-
{ label: "~/.agents/skills/", path:
|
|
5367
|
+
{ label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
|
|
5368
|
+
{ label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
|
|
6332
5369
|
];
|
|
6333
5370
|
if (!plugin) {
|
|
6334
5371
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -6431,8 +5468,8 @@ async function checkProfiles(home) {
|
|
|
6431
5468
|
}
|
|
6432
5469
|
async function checkProjectLocalOverride(cwd) {
|
|
6433
5470
|
const dir = cwd ?? process.cwd();
|
|
6434
|
-
const envPath =
|
|
6435
|
-
if (
|
|
5471
|
+
const envPath = join24(dir, ".skillwiki", ".env");
|
|
5472
|
+
if (existsSync13(envPath)) {
|
|
6436
5473
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
6437
5474
|
}
|
|
6438
5475
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -6441,7 +5478,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
6441
5478
|
if (resolvedPath === void 0) {
|
|
6442
5479
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6443
5480
|
}
|
|
6444
|
-
if (!
|
|
5481
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6445
5482
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
6446
5483
|
}
|
|
6447
5484
|
try {
|
|
@@ -6464,9 +5501,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
6464
5501
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6465
5502
|
}
|
|
6466
5503
|
const missing = [];
|
|
6467
|
-
if (!
|
|
6468
|
-
if (!
|
|
6469
|
-
if (!
|
|
5504
|
+
if (!existsSync13(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
5505
|
+
if (!existsSync13(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
5506
|
+
if (!existsSync13(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
6470
5507
|
if (missing.length === 0) {
|
|
6471
5508
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
6472
5509
|
}
|
|
@@ -6476,12 +5513,12 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
6476
5513
|
if (resolvedPath === void 0) {
|
|
6477
5514
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6478
5515
|
}
|
|
6479
|
-
const rawDir =
|
|
6480
|
-
if (!
|
|
5516
|
+
const rawDir = join24(resolvedPath, "raw");
|
|
5517
|
+
if (!existsSync13(rawDir)) {
|
|
6481
5518
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
6482
5519
|
}
|
|
6483
5520
|
const found = [];
|
|
6484
|
-
(function
|
|
5521
|
+
(function walk(dir, rel) {
|
|
6485
5522
|
let entries;
|
|
6486
5523
|
try {
|
|
6487
5524
|
entries = readdirSync3(dir, { withFileTypes: true });
|
|
@@ -6492,7 +5529,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
6492
5529
|
if (entry.name === ".DS_Store") {
|
|
6493
5530
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
6494
5531
|
} else if (entry.isDirectory()) {
|
|
6495
|
-
|
|
5532
|
+
walk(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
6496
5533
|
}
|
|
6497
5534
|
}
|
|
6498
5535
|
})(rawDir, "");
|
|
@@ -6523,7 +5560,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
6523
5560
|
if (resolvedPath === void 0) {
|
|
6524
5561
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6525
5562
|
}
|
|
6526
|
-
if (!
|
|
5563
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6527
5564
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
6528
5565
|
}
|
|
6529
5566
|
let timestamp;
|
|
@@ -6571,7 +5608,7 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
6571
5608
|
if (resolvedPath === void 0) {
|
|
6572
5609
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
6573
5610
|
}
|
|
6574
|
-
if (!
|
|
5611
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6575
5612
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
6576
5613
|
}
|
|
6577
5614
|
try {
|
|
@@ -6639,7 +5676,7 @@ function remoteMainHash(resolvedPath) {
|
|
|
6639
5676
|
}
|
|
6640
5677
|
function checkStaleRemoteMain(resolvedPath) {
|
|
6641
5678
|
if (resolvedPath === void 0) return void 0;
|
|
6642
|
-
if (!
|
|
5679
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) return void 0;
|
|
6643
5680
|
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
6644
5681
|
if (!localOrigin) return void 0;
|
|
6645
5682
|
const remoteMain = remoteMainHash(resolvedPath);
|
|
@@ -6655,7 +5692,7 @@ function checkVaultLocalGit(resolvedPath) {
|
|
|
6655
5692
|
if (resolvedPath === void 0) {
|
|
6656
5693
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6657
5694
|
}
|
|
6658
|
-
if (!
|
|
5695
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6659
5696
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
6660
5697
|
}
|
|
6661
5698
|
try {
|
|
@@ -6674,7 +5711,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
6674
5711
|
if (resolvedPath === void 0) {
|
|
6675
5712
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
6676
5713
|
}
|
|
6677
|
-
if (!
|
|
5714
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6678
5715
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
6679
5716
|
}
|
|
6680
5717
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -6718,7 +5755,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
6718
5755
|
if (resolvedPath === void 0) {
|
|
6719
5756
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
6720
5757
|
}
|
|
6721
|
-
if (!
|
|
5758
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6722
5759
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
6723
5760
|
}
|
|
6724
5761
|
try {
|
|
@@ -6745,7 +5782,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
6745
5782
|
if (resolvedPath === void 0) {
|
|
6746
5783
|
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
6747
5784
|
}
|
|
6748
|
-
if (!
|
|
5785
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6749
5786
|
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
6750
5787
|
}
|
|
6751
5788
|
if (!hasOriginMain(resolvedPath)) {
|
|
@@ -6773,7 +5810,7 @@ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
|
|
|
6773
5810
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
|
|
6774
5811
|
}
|
|
6775
5812
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
6776
|
-
if (!
|
|
5813
|
+
if (!existsSync13(latestPath)) {
|
|
6777
5814
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
|
|
6778
5815
|
}
|
|
6779
5816
|
try {
|
|
@@ -6861,11 +5898,11 @@ async function checkFleetIdentity(input) {
|
|
|
6861
5898
|
}
|
|
6862
5899
|
function pullLogPaths(home) {
|
|
6863
5900
|
const paths = platform2() === "darwin" ? [
|
|
6864
|
-
|
|
6865
|
-
|
|
5901
|
+
join24(home, "Library", "Logs", "wiki-pull.log"),
|
|
5902
|
+
join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6866
5903
|
] : [
|
|
6867
|
-
|
|
6868
|
-
|
|
5904
|
+
join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
5905
|
+
join24(home, "Library", "Logs", "wiki-pull.log")
|
|
6869
5906
|
];
|
|
6870
5907
|
return [...new Set(paths)];
|
|
6871
5908
|
}
|
|
@@ -6877,12 +5914,12 @@ function isRecentLogLine(line, nowMs) {
|
|
|
6877
5914
|
return nowMs - ts <= 24 * 60 * 60 * 1e3;
|
|
6878
5915
|
}
|
|
6879
5916
|
function checkVaultGitPullFailures(home) {
|
|
6880
|
-
const path = pullLogPaths(home).find((p) =>
|
|
5917
|
+
const path = pullLogPaths(home).find((p) => existsSync13(p));
|
|
6881
5918
|
if (!path) {
|
|
6882
5919
|
return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
|
|
6883
5920
|
}
|
|
6884
5921
|
try {
|
|
6885
|
-
const lines =
|
|
5922
|
+
const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
6886
5923
|
const now = Date.now();
|
|
6887
5924
|
const failures = lines.filter(
|
|
6888
5925
|
(line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
|
|
@@ -6905,8 +5942,8 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
6905
5942
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
6906
5943
|
}
|
|
6907
5944
|
const mountPoint = fuse.mountPoint;
|
|
6908
|
-
const conceptsDir =
|
|
6909
|
-
if (!
|
|
5945
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
5946
|
+
if (!existsSync13(conceptsDir)) {
|
|
6910
5947
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
6911
5948
|
}
|
|
6912
5949
|
const start = Date.now();
|
|
@@ -7088,8 +6125,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
7088
6125
|
if (!fuse) {
|
|
7089
6126
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
7090
6127
|
}
|
|
7091
|
-
const conceptsDir =
|
|
7092
|
-
if (!
|
|
6128
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
6129
|
+
if (!existsSync13(conceptsDir)) {
|
|
7093
6130
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
7094
6131
|
}
|
|
7095
6132
|
const result = writeTest(conceptsDir);
|
|
@@ -7175,7 +6212,7 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
7175
6212
|
}
|
|
7176
6213
|
function readVaultSyncConfig(home) {
|
|
7177
6214
|
try {
|
|
7178
|
-
const content =
|
|
6215
|
+
const content = readFileSync10(join24(home, ".skillwiki", ".env"), "utf8");
|
|
7179
6216
|
let installed = false;
|
|
7180
6217
|
let role;
|
|
7181
6218
|
let serviceScope;
|
|
@@ -7204,7 +6241,7 @@ function readVaultSyncConfig(home) {
|
|
|
7204
6241
|
}
|
|
7205
6242
|
function readKeyFromEnvFile(path, keys) {
|
|
7206
6243
|
try {
|
|
7207
|
-
const content =
|
|
6244
|
+
const content = readFileSync10(path, "utf8");
|
|
7208
6245
|
for (const line of content.split(/\r?\n/)) {
|
|
7209
6246
|
const trimmed = line.trim();
|
|
7210
6247
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
@@ -7226,7 +6263,7 @@ function resolveSnapshotGitWorktree(config) {
|
|
|
7226
6263
|
if (fromProfile) return fromProfile;
|
|
7227
6264
|
}
|
|
7228
6265
|
const defaultPath = "/root/wiki-git";
|
|
7229
|
-
return
|
|
6266
|
+
return existsSync13(defaultPath) ? defaultPath : void 0;
|
|
7230
6267
|
}
|
|
7231
6268
|
function vaultSyncChecks(input) {
|
|
7232
6269
|
const os = input.os ?? platform2();
|
|
@@ -7243,16 +6280,16 @@ function vaultSyncChecks(input) {
|
|
|
7243
6280
|
];
|
|
7244
6281
|
}
|
|
7245
6282
|
const isMac = os === "darwin";
|
|
7246
|
-
const logDir = input.logDir ?? (isMac ?
|
|
7247
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
7248
|
-
const filterPath = input.filterPath ??
|
|
7249
|
-
const packagedSnapshotPath =
|
|
6283
|
+
const logDir = input.logDir ?? (isMac ? join24(home, "Library", "Logs") : join24(home, ".local", "state", "vault-sync", "log"));
|
|
6284
|
+
const shareDir = input.shareDir ?? (isMac ? join24(home, "Library", "Application Support", "vault-sync", "bin") : join24(home, ".local", "share", "vault-sync", "bin"));
|
|
6285
|
+
const filterPath = input.filterPath ?? join24(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
6286
|
+
const packagedSnapshotPath = join24(shareDir, "wiki-snapshot.sh");
|
|
7250
6287
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
7251
|
-
const snapshotPath = input.snapshotScriptPath ?? (
|
|
6288
|
+
const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
7252
6289
|
function snapshotLastStatusCheck() {
|
|
7253
|
-
const snapshotLog =
|
|
6290
|
+
const snapshotLog = join24(logDir, "wiki-snapshot.log");
|
|
7254
6291
|
try {
|
|
7255
|
-
const logContent =
|
|
6292
|
+
const logContent = readFileSync10(snapshotLog, "utf8");
|
|
7256
6293
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7257
6294
|
if (lines.length === 0) {
|
|
7258
6295
|
return check(
|
|
@@ -7297,14 +6334,14 @@ function vaultSyncChecks(input) {
|
|
|
7297
6334
|
}
|
|
7298
6335
|
}
|
|
7299
6336
|
if (input.vaultSyncRole === "snapshotter") {
|
|
7300
|
-
const c12 =
|
|
6337
|
+
const c12 = existsSync13(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
|
|
7301
6338
|
const serviceScope = input.vaultSyncServiceScope ?? "user";
|
|
7302
|
-
const userTimerPath =
|
|
6339
|
+
const userTimerPath = join24(home, ".config", "systemd", "user", "wiki-snapshot.timer");
|
|
7303
6340
|
const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
|
|
7304
6341
|
let c22;
|
|
7305
|
-
if (serviceScope === "user" &&
|
|
6342
|
+
if (serviceScope === "user" && existsSync13(userTimerPath)) {
|
|
7306
6343
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
|
|
7307
|
-
} else if (serviceScope === "system" &&
|
|
6344
|
+
} else if (serviceScope === "system" && existsSync13(systemTimerPath)) {
|
|
7308
6345
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
|
|
7309
6346
|
} else if (os !== "linux") {
|
|
7310
6347
|
c22 = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "Snapshotter scheduler is Linux-only and no wiki-snapshot.timer file was found");
|
|
@@ -7336,7 +6373,7 @@ function vaultSyncChecks(input) {
|
|
|
7336
6373
|
);
|
|
7337
6374
|
let c52;
|
|
7338
6375
|
try {
|
|
7339
|
-
if (!
|
|
6376
|
+
if (!existsSync13(snapshotPath)) {
|
|
7340
6377
|
c52 = check(
|
|
7341
6378
|
"error",
|
|
7342
6379
|
"vault_sync_snapshot_guard",
|
|
@@ -7344,7 +6381,7 @@ function vaultSyncChecks(input) {
|
|
|
7344
6381
|
`Snapshot script not found at ${snapshotPath}`
|
|
7345
6382
|
);
|
|
7346
6383
|
} else {
|
|
7347
|
-
const content =
|
|
6384
|
+
const content = readFileSync10(snapshotPath, "utf8");
|
|
7348
6385
|
if (!content.includes("--max-delete")) {
|
|
7349
6386
|
c52 = check(
|
|
7350
6387
|
"error",
|
|
@@ -7371,8 +6408,8 @@ function vaultSyncChecks(input) {
|
|
|
7371
6408
|
}
|
|
7372
6409
|
return [c12, c22, c32, cFetch2, c42, c52];
|
|
7373
6410
|
}
|
|
7374
|
-
const pushScriptPath =
|
|
7375
|
-
const c1 =
|
|
6411
|
+
const pushScriptPath = join24(shareDir, "wiki-push.sh");
|
|
6412
|
+
const c1 = existsSync13(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
|
|
7376
6413
|
let c2;
|
|
7377
6414
|
try {
|
|
7378
6415
|
if (isMac) {
|
|
@@ -7423,10 +6460,10 @@ function vaultSyncChecks(input) {
|
|
|
7423
6460
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
7424
6461
|
);
|
|
7425
6462
|
}
|
|
7426
|
-
const logFile =
|
|
6463
|
+
const logFile = join24(logDir, "wiki-push.log");
|
|
7427
6464
|
let c3;
|
|
7428
6465
|
try {
|
|
7429
|
-
const logContent =
|
|
6466
|
+
const logContent = readFileSync10(logFile, "utf8");
|
|
7430
6467
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7431
6468
|
if (lines.length === 0) {
|
|
7432
6469
|
c3 = check(
|
|
@@ -7482,7 +6519,7 @@ function vaultSyncChecks(input) {
|
|
|
7482
6519
|
}
|
|
7483
6520
|
}
|
|
7484
6521
|
} catch {
|
|
7485
|
-
c3 =
|
|
6522
|
+
c3 = existsSync13(logDir) ? check(
|
|
7486
6523
|
"warn",
|
|
7487
6524
|
"vault_sync_last_push_age",
|
|
7488
6525
|
"Vault sync last push recency",
|
|
@@ -7494,10 +6531,10 @@ function vaultSyncChecks(input) {
|
|
|
7494
6531
|
`Log directory not found at ${logDir}`
|
|
7495
6532
|
);
|
|
7496
6533
|
}
|
|
7497
|
-
const fetchLogFile =
|
|
6534
|
+
const fetchLogFile = join24(logDir, "wiki-fetch.log");
|
|
7498
6535
|
let cFetch;
|
|
7499
6536
|
try {
|
|
7500
|
-
const logContent =
|
|
6537
|
+
const logContent = readFileSync10(fetchLogFile, "utf8");
|
|
7501
6538
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7502
6539
|
if (lines.length === 0) {
|
|
7503
6540
|
cFetch = check(
|
|
@@ -7541,7 +6578,7 @@ function vaultSyncChecks(input) {
|
|
|
7541
6578
|
}
|
|
7542
6579
|
let c4;
|
|
7543
6580
|
try {
|
|
7544
|
-
if (!
|
|
6581
|
+
if (!existsSync13(filterPath)) {
|
|
7545
6582
|
c4 = check(
|
|
7546
6583
|
"error",
|
|
7547
6584
|
"vault_sync_filter_present",
|
|
@@ -7549,7 +6586,7 @@ function vaultSyncChecks(input) {
|
|
|
7549
6586
|
`Filter file not found at ${filterPath}`
|
|
7550
6587
|
);
|
|
7551
6588
|
} else {
|
|
7552
|
-
const content =
|
|
6589
|
+
const content = readFileSync10(filterPath, "utf8");
|
|
7553
6590
|
const requiredExcludes = [
|
|
7554
6591
|
"remotely-save/data.json",
|
|
7555
6592
|
".skillwiki/sync.lock",
|
|
@@ -7592,7 +6629,7 @@ function vaultSyncChecks(input) {
|
|
|
7592
6629
|
);
|
|
7593
6630
|
} else {
|
|
7594
6631
|
try {
|
|
7595
|
-
if (!
|
|
6632
|
+
if (!existsSync13(snapshotPath)) {
|
|
7596
6633
|
c5 = check(
|
|
7597
6634
|
"error",
|
|
7598
6635
|
"vault_sync_snapshot_guard",
|
|
@@ -7600,7 +6637,7 @@ function vaultSyncChecks(input) {
|
|
|
7600
6637
|
`Snapshot script not found at ${snapshotPath}`
|
|
7601
6638
|
);
|
|
7602
6639
|
} else {
|
|
7603
|
-
const content =
|
|
6640
|
+
const content = readFileSync10(snapshotPath, "utf8");
|
|
7604
6641
|
if (!content.includes("--max-delete")) {
|
|
7605
6642
|
c5 = check(
|
|
7606
6643
|
"error",
|
|
@@ -7638,15 +6675,15 @@ function findSkillMd(dir) {
|
|
|
7638
6675
|
}
|
|
7639
6676
|
for (const entry of entries) {
|
|
7640
6677
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
7641
|
-
results.push(
|
|
6678
|
+
results.push(join24(dir, entry.name));
|
|
7642
6679
|
} else if (entry.isDirectory()) {
|
|
7643
|
-
results.push(...findSkillMd(
|
|
6680
|
+
results.push(...findSkillMd(join24(dir, entry.name)));
|
|
7644
6681
|
}
|
|
7645
6682
|
}
|
|
7646
6683
|
return results;
|
|
7647
6684
|
}
|
|
7648
6685
|
function findInstalledSkillMd(dir) {
|
|
7649
|
-
const directSkills = findSkillNames(dir).map((name) =>
|
|
6686
|
+
const directSkills = findSkillNames(dir).map((name) => join24(dir, name, "SKILL.md"));
|
|
7650
6687
|
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
7651
6688
|
}
|
|
7652
6689
|
function findSkillNames(dir) {
|
|
@@ -7658,7 +6695,7 @@ function findSkillNames(dir) {
|
|
|
7658
6695
|
return results;
|
|
7659
6696
|
}
|
|
7660
6697
|
for (const entry of entries) {
|
|
7661
|
-
if (entry.isDirectory() &&
|
|
6698
|
+
if (entry.isDirectory() && existsSync13(join24(dir, entry.name, "SKILL.md"))) {
|
|
7662
6699
|
results.push(entry.name);
|
|
7663
6700
|
}
|
|
7664
6701
|
}
|
|
@@ -7702,7 +6739,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
7702
6739
|
}
|
|
7703
6740
|
let logLines = 0;
|
|
7704
6741
|
try {
|
|
7705
|
-
logLines =
|
|
6742
|
+
logLines = readFileSync10(join24(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
7706
6743
|
} catch {
|
|
7707
6744
|
}
|
|
7708
6745
|
return [
|
|
@@ -7810,7 +6847,7 @@ async function runDoctor(input) {
|
|
|
7810
6847
|
}
|
|
7811
6848
|
|
|
7812
6849
|
// src/utils/package-info.ts
|
|
7813
|
-
import { readFileSync as
|
|
6850
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
7814
6851
|
function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
7815
6852
|
return [
|
|
7816
6853
|
new URL("../package.json", baseUrl),
|
|
@@ -7820,7 +6857,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
|
7820
6857
|
function readCliPackageJson(baseUrl = import.meta.url) {
|
|
7821
6858
|
for (const url of packageJsonCandidateUrls(baseUrl)) {
|
|
7822
6859
|
try {
|
|
7823
|
-
const pkg = JSON.parse(
|
|
6860
|
+
const pkg = JSON.parse(readFileSync11(url, "utf8"));
|
|
7824
6861
|
if (typeof pkg.version === "string") {
|
|
7825
6862
|
return { ...pkg, version: pkg.version };
|
|
7826
6863
|
}
|
|
@@ -7830,214 +6867,11 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
7830
6867
|
throw new Error(`Could not locate skillwiki package.json from ${baseUrl}`);
|
|
7831
6868
|
}
|
|
7832
6869
|
|
|
7833
|
-
// src/commands/project-index.ts
|
|
7834
|
-
import { readdir as readdir4, readFile as readFile19, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
7835
|
-
import { join as join27, dirname as dirname8, basename as basename2 } from "path";
|
|
7836
|
-
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
7837
|
-
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
7838
|
-
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
7839
|
-
const found = [];
|
|
7840
|
-
let entries;
|
|
7841
|
-
try {
|
|
7842
|
-
entries = await readdir4(rootAbs, { withFileTypes: true });
|
|
7843
|
-
} catch {
|
|
7844
|
-
return found;
|
|
7845
|
-
}
|
|
7846
|
-
for (const entry of entries) {
|
|
7847
|
-
const abs = join27(rootAbs, entry.name);
|
|
7848
|
-
const rel = `${rootRel}/${entry.name}`;
|
|
7849
|
-
if (entry.isDirectory()) {
|
|
7850
|
-
found.push(...await scanMarkdownTree(abs, rel));
|
|
7851
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7852
|
-
found.push(rel);
|
|
7853
|
-
}
|
|
7854
|
-
}
|
|
7855
|
-
return found;
|
|
7856
|
-
}
|
|
7857
|
-
function projectLocalType(slug, page, data) {
|
|
7858
|
-
if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
|
|
7859
|
-
if (page.startsWith(`projects/${slug}/work/`)) {
|
|
7860
|
-
if (typeof data.kind === "string") return data.kind;
|
|
7861
|
-
const name = basename2(page, ".md");
|
|
7862
|
-
if (name === "spec" || name === "plan" || name === "retro") return name;
|
|
7863
|
-
return "work";
|
|
7864
|
-
}
|
|
7865
|
-
if (page.startsWith(`projects/${slug}/architecture/`)) {
|
|
7866
|
-
return typeof data.type === "string" ? data.type : "architecture";
|
|
7867
|
-
}
|
|
7868
|
-
if (page.startsWith(`projects/${slug}/history/`)) {
|
|
7869
|
-
if (typeof data.kind === "string") return data.kind;
|
|
7870
|
-
if (typeof data.type === "string") return data.type;
|
|
7871
|
-
return "history";
|
|
7872
|
-
}
|
|
7873
|
-
return typeof data.type === "string" ? data.type : "project";
|
|
7874
|
-
}
|
|
7875
|
-
async function runProjectIndex(input) {
|
|
7876
|
-
const slug = input.slug;
|
|
7877
|
-
const projectDir = join27(input.vault, "projects", slug);
|
|
7878
|
-
try {
|
|
7879
|
-
await readdir4(projectDir);
|
|
7880
|
-
} catch {
|
|
7881
|
-
return {
|
|
7882
|
-
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
7883
|
-
result: err("PROJECT_NOT_FOUND", { slug, path: projectDir })
|
|
7884
|
-
};
|
|
7885
|
-
}
|
|
7886
|
-
const wikilinkPattern = `[[${slug}]]`;
|
|
7887
|
-
const entries = [];
|
|
7888
|
-
const compoundDir = join27(input.vault, "projects", slug, "compound");
|
|
7889
|
-
try {
|
|
7890
|
-
const compoundFiles = await readdir4(compoundDir, { withFileTypes: true });
|
|
7891
|
-
for (const entry of compoundFiles) {
|
|
7892
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7893
|
-
const filePath = join27(compoundDir, entry.name);
|
|
7894
|
-
let text;
|
|
7895
|
-
try {
|
|
7896
|
-
text = await readFile19(filePath, "utf8");
|
|
7897
|
-
} catch {
|
|
7898
|
-
continue;
|
|
7899
|
-
}
|
|
7900
|
-
const fm = extractFrontmatter(text);
|
|
7901
|
-
if (!fm.ok) continue;
|
|
7902
|
-
entries.push({
|
|
7903
|
-
page: `projects/${slug}/compound/${entry.name}`,
|
|
7904
|
-
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
7905
|
-
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
7906
|
-
});
|
|
7907
|
-
}
|
|
7908
|
-
} catch {
|
|
7909
|
-
}
|
|
7910
|
-
for (const dir of LAYER2_DIRS) {
|
|
7911
|
-
let files;
|
|
7912
|
-
try {
|
|
7913
|
-
files = await readdir4(join27(input.vault, dir), { withFileTypes: true });
|
|
7914
|
-
} catch {
|
|
7915
|
-
continue;
|
|
7916
|
-
}
|
|
7917
|
-
for (const entry of files) {
|
|
7918
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7919
|
-
const filePath = join27(input.vault, dir, entry.name);
|
|
7920
|
-
let text;
|
|
7921
|
-
try {
|
|
7922
|
-
text = await readFile19(filePath, "utf8");
|
|
7923
|
-
} catch {
|
|
7924
|
-
continue;
|
|
7925
|
-
}
|
|
7926
|
-
const fm = extractFrontmatter(text);
|
|
7927
|
-
if (!fm.ok) continue;
|
|
7928
|
-
const pp = fm.data.provenance_projects;
|
|
7929
|
-
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
7930
|
-
entries.push({
|
|
7931
|
-
page: `${dir}/${entry.name}`,
|
|
7932
|
-
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
7933
|
-
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
7934
|
-
});
|
|
7935
|
-
}
|
|
7936
|
-
}
|
|
7937
|
-
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
7938
|
-
const rootAbs = join27(projectDir, dir);
|
|
7939
|
-
const rootRel = `projects/${slug}/${dir}`;
|
|
7940
|
-
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
7941
|
-
for (const page of pages) {
|
|
7942
|
-
const filePath = join27(input.vault, page);
|
|
7943
|
-
let text;
|
|
7944
|
-
try {
|
|
7945
|
-
text = await readFile19(filePath, "utf8");
|
|
7946
|
-
} catch {
|
|
7947
|
-
continue;
|
|
7948
|
-
}
|
|
7949
|
-
const fm = extractFrontmatter(text);
|
|
7950
|
-
if (!fm.ok) continue;
|
|
7951
|
-
entries.push({
|
|
7952
|
-
page,
|
|
7953
|
-
type: projectLocalType(slug, page, fm.data),
|
|
7954
|
-
title: typeof fm.data.title === "string" ? fm.data.title : basename2(page, ".md")
|
|
7955
|
-
});
|
|
7956
|
-
}
|
|
7957
|
-
}
|
|
7958
|
-
const typeOrder = { entity: 0, concept: 1, comparison: 2, query: 3, summary: 4, meta: 5, requirement: 6, spec: 7, plan: 8, retro: 9, architecture: 10, pattern: 11, gotcha: 12, lesson: 13, antipattern: 14, compound: 15, work: 16, history: 17 };
|
|
7959
|
-
entries.sort((a, b) => {
|
|
7960
|
-
const ta = typeOrder[a.type] ?? 99;
|
|
7961
|
-
const tb = typeOrder[b.type] ?? 99;
|
|
7962
|
-
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
7963
|
-
});
|
|
7964
|
-
const indexPath = join27(projectDir, "knowledge.md");
|
|
7965
|
-
let existing = false;
|
|
7966
|
-
let stale = false;
|
|
7967
|
-
try {
|
|
7968
|
-
const existingText = await readFile19(indexPath, "utf8");
|
|
7969
|
-
existing = true;
|
|
7970
|
-
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
7971
|
-
const existingPages = new Set(existingEntries.map((l) => {
|
|
7972
|
-
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
7973
|
-
return m ? m[1] : "";
|
|
7974
|
-
}));
|
|
7975
|
-
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
7976
|
-
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
7977
|
-
} catch {
|
|
7978
|
-
}
|
|
7979
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7980
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7981
|
-
for (const e of entries) {
|
|
7982
|
-
const group = e.type;
|
|
7983
|
-
if (!grouped.has(group)) grouped.set(group, []);
|
|
7984
|
-
grouped.get(group).push(e);
|
|
7985
|
-
}
|
|
7986
|
-
let body = `# Knowledge Index: ${slug}
|
|
7987
|
-
|
|
7988
|
-
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
7989
|
-
|
|
7990
|
-
`;
|
|
7991
|
-
for (const [type, items] of grouped) {
|
|
7992
|
-
body += `## ${type}
|
|
7993
|
-
|
|
7994
|
-
`;
|
|
7995
|
-
for (const item of items) {
|
|
7996
|
-
const pageRef = item.page.replace(/\.md$/, "");
|
|
7997
|
-
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
7998
|
-
`;
|
|
7999
|
-
}
|
|
8000
|
-
body += "\n";
|
|
8001
|
-
}
|
|
8002
|
-
if (entries.length === 0) {
|
|
8003
|
-
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
8004
|
-
`;
|
|
8005
|
-
}
|
|
8006
|
-
if (input.apply) {
|
|
8007
|
-
try {
|
|
8008
|
-
await mkdir5(dirname8(indexPath), { recursive: true });
|
|
8009
|
-
await writeFile5(indexPath, body, "utf8");
|
|
8010
|
-
} catch (e) {
|
|
8011
|
-
return {
|
|
8012
|
-
exitCode: ExitCode.WRITE_FAILED,
|
|
8013
|
-
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
8014
|
-
};
|
|
8015
|
-
}
|
|
8016
|
-
}
|
|
8017
|
-
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
8018
|
-
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
8019
|
-
return {
|
|
8020
|
-
exitCode: ExitCode.OK,
|
|
8021
|
-
result: ok({
|
|
8022
|
-
slug,
|
|
8023
|
-
entries,
|
|
8024
|
-
existing,
|
|
8025
|
-
stale,
|
|
8026
|
-
index_path: `projects/${slug}/knowledge.md`,
|
|
8027
|
-
humanHint: `project: ${slug}
|
|
8028
|
-
entries: ${entries.length}${staleHint}
|
|
8029
|
-
${action}
|
|
8030
|
-
|
|
8031
|
-
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
8032
|
-
})
|
|
8033
|
-
};
|
|
8034
|
-
}
|
|
8035
|
-
|
|
8036
6870
|
// src/commands/observe.ts
|
|
8037
|
-
import { mkdir as
|
|
8038
|
-
import { existsSync as
|
|
8039
|
-
import { join as
|
|
8040
|
-
import { createHash as
|
|
6871
|
+
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
6872
|
+
import { existsSync as existsSync14, statSync as statSync3 } from "fs";
|
|
6873
|
+
import { join as join25 } from "path";
|
|
6874
|
+
import { createHash as createHash6 } from "crypto";
|
|
8041
6875
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
8042
6876
|
function slugify(text) {
|
|
8043
6877
|
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -8059,15 +6893,15 @@ async function runObserve(input) {
|
|
|
8059
6893
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
8060
6894
|
};
|
|
8061
6895
|
}
|
|
8062
|
-
if (!
|
|
6896
|
+
if (!existsSync14(input.vault) || !statSync3(input.vault).isDirectory()) {
|
|
8063
6897
|
return {
|
|
8064
6898
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
8065
6899
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
8066
6900
|
};
|
|
8067
6901
|
}
|
|
8068
|
-
const transcriptsDir =
|
|
6902
|
+
const transcriptsDir = join25(input.vault, "raw", "transcripts");
|
|
8069
6903
|
try {
|
|
8070
|
-
await
|
|
6904
|
+
await mkdir5(transcriptsDir, { recursive: true });
|
|
8071
6905
|
} catch {
|
|
8072
6906
|
return {
|
|
8073
6907
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
@@ -8077,11 +6911,11 @@ async function runObserve(input) {
|
|
|
8077
6911
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8078
6912
|
const slug = slugify(input.text);
|
|
8079
6913
|
const fileName = `${today}-observation-${slug}.md`;
|
|
8080
|
-
const filePath =
|
|
6914
|
+
const filePath = join25(transcriptsDir, fileName);
|
|
8081
6915
|
const body = `
|
|
8082
6916
|
${input.text.trim()}
|
|
8083
6917
|
`;
|
|
8084
|
-
const sha256 =
|
|
6918
|
+
const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
8085
6919
|
const frontmatterLines = [
|
|
8086
6920
|
"---",
|
|
8087
6921
|
"source_url:",
|
|
@@ -8095,7 +6929,7 @@ ${input.text.trim()}
|
|
|
8095
6929
|
frontmatterLines.push("---");
|
|
8096
6930
|
const content = frontmatterLines.join("\n") + body;
|
|
8097
6931
|
try {
|
|
8098
|
-
await
|
|
6932
|
+
await writeFile4(filePath, content, "utf8");
|
|
8099
6933
|
} catch (e) {
|
|
8100
6934
|
return {
|
|
8101
6935
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -8117,9 +6951,9 @@ ${input.text.trim()}
|
|
|
8117
6951
|
}
|
|
8118
6952
|
|
|
8119
6953
|
// src/commands/memory.ts
|
|
8120
|
-
import { createHash as
|
|
8121
|
-
import { mkdir as
|
|
8122
|
-
import { basename as
|
|
6954
|
+
import { createHash as createHash7 } from "crypto";
|
|
6955
|
+
import { mkdir as mkdir6, readFile as readFile16, readdir as readdir4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
6956
|
+
import { basename as basename2, extname, join as join26, relative as relative3, sep as sep3 } from "path";
|
|
8123
6957
|
async function runMemoryTopics(input) {
|
|
8124
6958
|
const scan = await scanVault(input.vault);
|
|
8125
6959
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -8183,9 +7017,9 @@ async function runMemoryIndex(input) {
|
|
|
8183
7017
|
}
|
|
8184
7018
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8185
7019
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
8186
|
-
const absCachePath =
|
|
8187
|
-
await
|
|
8188
|
-
await
|
|
7020
|
+
const absCachePath = join26(input.vault, relCachePath);
|
|
7021
|
+
await mkdir6(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
7022
|
+
await writeFile5(absCachePath, `${JSON.stringify({
|
|
8189
7023
|
generated_at: generatedAt,
|
|
8190
7024
|
project: input.project,
|
|
8191
7025
|
topics: state.topics,
|
|
@@ -8376,7 +7210,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
8376
7210
|
}
|
|
8377
7211
|
async function checkMemoryIndex(vault, project, current) {
|
|
8378
7212
|
const relCachePath = memoryCacheRelPath(project);
|
|
8379
|
-
const cacheText = await readIfExists2(
|
|
7213
|
+
const cacheText = await readIfExists2(join26(vault, relCachePath));
|
|
8380
7214
|
if (!cacheText) {
|
|
8381
7215
|
return {
|
|
8382
7216
|
ok: true,
|
|
@@ -8493,7 +7327,7 @@ async function readMemoryPage(page, project, warnings) {
|
|
|
8493
7327
|
title,
|
|
8494
7328
|
summary: summarize(body),
|
|
8495
7329
|
updated,
|
|
8496
|
-
hash:
|
|
7330
|
+
hash: createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
8497
7331
|
topics,
|
|
8498
7332
|
project,
|
|
8499
7333
|
...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
|
|
@@ -8769,23 +7603,23 @@ function renderMemoryIndexStatusHint(status) {
|
|
|
8769
7603
|
}
|
|
8770
7604
|
async function readIfExists2(path) {
|
|
8771
7605
|
try {
|
|
8772
|
-
return await
|
|
7606
|
+
return await readFile16(path, "utf8");
|
|
8773
7607
|
} catch {
|
|
8774
7608
|
return "";
|
|
8775
7609
|
}
|
|
8776
7610
|
}
|
|
8777
7611
|
async function collectImportFiles(source) {
|
|
8778
|
-
const st = await
|
|
7612
|
+
const st = await stat5(source);
|
|
8779
7613
|
if (st.isFile()) return isImportCandidate(source) ? [source] : [];
|
|
8780
7614
|
const files = [];
|
|
8781
7615
|
await walkImportFiles(source, files);
|
|
8782
7616
|
return files.sort((a, b) => a.localeCompare(b));
|
|
8783
7617
|
}
|
|
8784
7618
|
async function walkImportFiles(dir, out) {
|
|
8785
|
-
const entries = await
|
|
7619
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
8786
7620
|
for (const entry of entries) {
|
|
8787
7621
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
8788
|
-
const path =
|
|
7622
|
+
const path = join26(dir, entry.name);
|
|
8789
7623
|
if (entry.isDirectory()) {
|
|
8790
7624
|
await walkImportFiles(path, out);
|
|
8791
7625
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -8798,9 +7632,9 @@ function isImportCandidate(path) {
|
|
|
8798
7632
|
return ext === ".md" || ext === ".txt";
|
|
8799
7633
|
}
|
|
8800
7634
|
async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
8801
|
-
const st = await
|
|
7635
|
+
const st = await stat5(file);
|
|
8802
7636
|
const sourceKind = classifyImportSource(file);
|
|
8803
|
-
const hash =
|
|
7637
|
+
const hash = createHash7("sha256").update(await readFile16(file)).digest("hex");
|
|
8804
7638
|
const baseEntry = {
|
|
8805
7639
|
source_path: file,
|
|
8806
7640
|
source_kind: sourceKind,
|
|
@@ -8823,7 +7657,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8823
7657
|
reason: "policy_source_not_imported"
|
|
8824
7658
|
};
|
|
8825
7659
|
}
|
|
8826
|
-
const text = await
|
|
7660
|
+
const text = await readFile16(file, "utf8");
|
|
8827
7661
|
const extracted = extractImportText(text, sourceKind);
|
|
8828
7662
|
if (!extracted) {
|
|
8829
7663
|
return {
|
|
@@ -8834,8 +7668,8 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8834
7668
|
}
|
|
8835
7669
|
const redacted = redactSensitiveContent(extracted, { file });
|
|
8836
7670
|
const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
|
|
8837
|
-
const sourceSlug = slugify2(
|
|
8838
|
-
const relSource =
|
|
7671
|
+
const sourceSlug = slugify2(basename2(file, extname(file)));
|
|
7672
|
+
const relSource = relative3(sourceRoot, file).split(sep3).join("/");
|
|
8839
7673
|
const entry = {
|
|
8840
7674
|
...baseEntry,
|
|
8841
7675
|
status: "ready",
|
|
@@ -8852,9 +7686,9 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
8852
7686
|
const content = hiddenString(entry, "__content");
|
|
8853
7687
|
const project = hiddenString(entry, "__project");
|
|
8854
7688
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
8855
|
-
const absPath =
|
|
8856
|
-
await
|
|
8857
|
-
await
|
|
7689
|
+
const absPath = join26(vault, relPath);
|
|
7690
|
+
await mkdir6(join26(vault, "raw", "transcripts"), { recursive: true });
|
|
7691
|
+
await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
8858
7692
|
const validation = await runValidate({ file: absPath });
|
|
8859
7693
|
return {
|
|
8860
7694
|
relPath,
|
|
@@ -8869,7 +7703,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
8869
7703
|
const stem = proposed.slice(0, -ext.length);
|
|
8870
7704
|
let candidate = proposed;
|
|
8871
7705
|
let i = 2;
|
|
8872
|
-
while (await readIfExists2(
|
|
7706
|
+
while (await readIfExists2(join26(vault, candidate))) {
|
|
8873
7707
|
candidate = `${stem}-${i}${ext}`;
|
|
8874
7708
|
i++;
|
|
8875
7709
|
}
|
|
@@ -8893,7 +7727,7 @@ function renderImportCapture(entry, content, project, today) {
|
|
|
8893
7727
|
`source_paths: ["${entry.source_path.replaceAll('"', '\\"')}"]`,
|
|
8894
7728
|
"---",
|
|
8895
7729
|
"",
|
|
8896
|
-
`# Imported Memory: ${
|
|
7730
|
+
`# Imported Memory: ${basename2(entry.source_path, extname(entry.source_path))}`,
|
|
8897
7731
|
"",
|
|
8898
7732
|
`Source kind: ${entry.source_kind}`,
|
|
8899
7733
|
"",
|
|
@@ -8907,8 +7741,8 @@ function hiddenString(entry, key) {
|
|
|
8907
7741
|
return entry[key] ?? "";
|
|
8908
7742
|
}
|
|
8909
7743
|
function classifyImportSource(file) {
|
|
8910
|
-
const rel = file.split(
|
|
8911
|
-
const name =
|
|
7744
|
+
const rel = file.split(sep3).join("/");
|
|
7745
|
+
const name = basename2(file);
|
|
8912
7746
|
if (rel.includes("/.codex/memories/")) return "codex-memory";
|
|
8913
7747
|
if (rel.includes("/.codex/rules/")) return "codex-rule";
|
|
8914
7748
|
if (rel.includes("/.claude/") && name === "napkin.md") return "napkin";
|
|
@@ -9041,10 +7875,10 @@ function memoryCacheRelPath(project) {
|
|
|
9041
7875
|
}
|
|
9042
7876
|
async function readMemoryCache(vault, project) {
|
|
9043
7877
|
if (project) {
|
|
9044
|
-
const projectCache = await readIfExists2(
|
|
7878
|
+
const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
|
|
9045
7879
|
if (projectCache) return projectCache;
|
|
9046
7880
|
}
|
|
9047
|
-
return readIfExists2(
|
|
7881
|
+
return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
|
|
9048
7882
|
}
|
|
9049
7883
|
function dedupePages(pages) {
|
|
9050
7884
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9130,8 +7964,8 @@ function slugify2(value) {
|
|
|
9130
7964
|
}
|
|
9131
7965
|
|
|
9132
7966
|
// src/commands/query.ts
|
|
9133
|
-
import { readFile as
|
|
9134
|
-
import { join as
|
|
7967
|
+
import { readFile as readFile17, stat as stat6 } from "fs/promises";
|
|
7968
|
+
import { join as join27 } from "path";
|
|
9135
7969
|
var W_KEYWORD = 2;
|
|
9136
7970
|
var W_SOURCE_OVERLAP = 4;
|
|
9137
7971
|
var W_WIKILINK = 3;
|
|
@@ -9184,11 +8018,11 @@ async function runQuery(input) {
|
|
|
9184
8018
|
);
|
|
9185
8019
|
const results = pages.map((page) => {
|
|
9186
8020
|
const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
|
|
9187
|
-
const
|
|
8021
|
+
const wikilink = scoreWikilink(page.relPath, seedPaths, graph);
|
|
9188
8022
|
const aa = scoreAdamicAdar(page.relPath, seedPaths, graph);
|
|
9189
8023
|
const typeAffinity = scoreTypeAffinity(page.type, queryTerms);
|
|
9190
8024
|
const isSeed = page.keywordScore > 0;
|
|
9191
|
-
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP +
|
|
8025
|
+
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink * W_WIKILINK + aa * W_ADAMIC_ADAR;
|
|
9192
8026
|
const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
|
|
9193
8027
|
return {
|
|
9194
8028
|
path: page.relPath,
|
|
@@ -9252,10 +8086,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
9252
8086
|
return score;
|
|
9253
8087
|
}
|
|
9254
8088
|
async function loadOrBuildGraph(vault) {
|
|
9255
|
-
const graphPath =
|
|
8089
|
+
const graphPath = join27(vault, ".skillwiki", "graph.json");
|
|
9256
8090
|
let needsBuild = false;
|
|
9257
8091
|
try {
|
|
9258
|
-
const fileStat = await
|
|
8092
|
+
const fileStat = await stat6(graphPath);
|
|
9259
8093
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
9260
8094
|
if (ageHours > 24) needsBuild = true;
|
|
9261
8095
|
} catch {
|
|
@@ -9266,7 +8100,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
9266
8100
|
if (buildResult.exitCode !== 0) return null;
|
|
9267
8101
|
}
|
|
9268
8102
|
try {
|
|
9269
|
-
const raw = await
|
|
8103
|
+
const raw = await readFile17(graphPath, "utf8");
|
|
9270
8104
|
return JSON.parse(raw);
|
|
9271
8105
|
} catch {
|
|
9272
8106
|
return null;
|
|
@@ -9278,30 +8112,30 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
9278
8112
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9279
8113
|
|
|
9280
8114
|
// src/mcp/tools.ts
|
|
9281
|
-
import { z
|
|
8115
|
+
import { z } from "zod";
|
|
9282
8116
|
|
|
9283
8117
|
// src/mcp/vault-resolve.ts
|
|
9284
|
-
import { join as
|
|
8118
|
+
import { join as join28, resolve as resolve7 } from "path";
|
|
9285
8119
|
|
|
9286
8120
|
// src/mcp/allowlist.ts
|
|
9287
|
-
import { resolve as
|
|
9288
|
-
import { realpathSync
|
|
8121
|
+
import { resolve as resolve6, sep as sep4 } from "path";
|
|
8122
|
+
import { realpathSync } from "fs";
|
|
9289
8123
|
function parseVaultAllowlist(envValue) {
|
|
9290
8124
|
if (envValue === void 0 || envValue.trim() === "") return null;
|
|
9291
|
-
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) =>
|
|
8125
|
+
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve6(p));
|
|
9292
8126
|
}
|
|
9293
8127
|
function vaultAllowedByList(vaultPath, allowlist) {
|
|
9294
8128
|
if (!allowlist || allowlist.length === 0) return true;
|
|
9295
8129
|
let canonical = vaultPath;
|
|
9296
8130
|
try {
|
|
9297
|
-
canonical =
|
|
8131
|
+
canonical = realpathSync(vaultPath);
|
|
9298
8132
|
} catch {
|
|
9299
|
-
canonical =
|
|
8133
|
+
canonical = resolve6(vaultPath);
|
|
9300
8134
|
}
|
|
9301
|
-
const resolved =
|
|
8135
|
+
const resolved = resolve6(canonical);
|
|
9302
8136
|
return allowlist.some((root) => {
|
|
9303
|
-
const r =
|
|
9304
|
-
return resolved === r || resolved.startsWith(r +
|
|
8137
|
+
const r = resolve6(root);
|
|
8138
|
+
return resolved === r || resolved.startsWith(r + sep4);
|
|
9305
8139
|
});
|
|
9306
8140
|
}
|
|
9307
8141
|
function getVaultAllowlistFromEnv() {
|
|
@@ -9314,7 +8148,7 @@ async function resolveMcpVault(input) {
|
|
|
9314
8148
|
let vaultPath;
|
|
9315
8149
|
let source = "resolved";
|
|
9316
8150
|
if (input.vault !== void 0 && input.vault.length > 0) {
|
|
9317
|
-
vaultPath =
|
|
8151
|
+
vaultPath = resolve7(input.vault);
|
|
9318
8152
|
source = "flag";
|
|
9319
8153
|
} else {
|
|
9320
8154
|
const r = await resolveRuntimePath({
|
|
@@ -9326,7 +8160,7 @@ async function resolveMcpVault(input) {
|
|
|
9326
8160
|
cwd: input.cwd ?? process.cwd()
|
|
9327
8161
|
});
|
|
9328
8162
|
if (!r.ok) return r;
|
|
9329
|
-
vaultPath =
|
|
8163
|
+
vaultPath = resolve7(r.data.path);
|
|
9330
8164
|
source = r.data.source;
|
|
9331
8165
|
}
|
|
9332
8166
|
const scan = await scanVault(vaultPath);
|
|
@@ -9343,7 +8177,7 @@ async function resolveMcpVault(input) {
|
|
|
9343
8177
|
return ok({ vault: vaultPath, source });
|
|
9344
8178
|
}
|
|
9345
8179
|
function defaultGraphOut(vault) {
|
|
9346
|
-
return
|
|
8180
|
+
return join28(vault, ".skillwiki", "graph.json");
|
|
9347
8181
|
}
|
|
9348
8182
|
|
|
9349
8183
|
// src/mcp/result-format.ts
|
|
@@ -9360,7 +8194,7 @@ function formatToolResult(payload) {
|
|
|
9360
8194
|
// src/mcp/audit-log.ts
|
|
9361
8195
|
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
9362
8196
|
import { homedir } from "os";
|
|
9363
|
-
import { join as
|
|
8197
|
+
import { join as join29 } from "path";
|
|
9364
8198
|
function auditEnabled() {
|
|
9365
8199
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
9366
8200
|
if (v === "0" || v === "false") return false;
|
|
@@ -9372,7 +8206,7 @@ function auditSink() {
|
|
|
9372
8206
|
function auditFilePath() {
|
|
9373
8207
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
9374
8208
|
if (custom && custom.length > 0) return custom;
|
|
9375
|
-
return
|
|
8209
|
+
return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
9376
8210
|
}
|
|
9377
8211
|
function auditMcpToolCall(entry) {
|
|
9378
8212
|
if (!auditEnabled()) return;
|
|
@@ -9382,7 +8216,7 @@ function auditMcpToolCall(entry) {
|
|
|
9382
8216
|
return;
|
|
9383
8217
|
}
|
|
9384
8218
|
const path = auditFilePath();
|
|
9385
|
-
mkdirSync5(
|
|
8219
|
+
mkdirSync5(join29(path, ".."), { recursive: true });
|
|
9386
8220
|
appendFileSync(path, line, "utf8");
|
|
9387
8221
|
}
|
|
9388
8222
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -9411,18 +8245,18 @@ async function runMcpToolHandler(tool, input, fn) {
|
|
|
9411
8245
|
|
|
9412
8246
|
// src/mcp/tools.ts
|
|
9413
8247
|
var vaultFields = {
|
|
9414
|
-
vault:
|
|
9415
|
-
wiki:
|
|
8248
|
+
vault: z.string().optional().describe("Vault root directory; omitted = resolve WIKI_PATH / default ~/wiki"),
|
|
8249
|
+
wiki: z.string().optional().describe("Wiki profile name for vault resolution")
|
|
9416
8250
|
};
|
|
9417
8251
|
function registerMcpTools(server) {
|
|
9418
8252
|
server.registerTool(
|
|
9419
8253
|
"skillwiki.query",
|
|
9420
8254
|
{
|
|
9421
8255
|
description: "Ranked vault query over typed knowledge (read-only). Returns Result envelope JSON.",
|
|
9422
|
-
inputSchema:
|
|
8256
|
+
inputSchema: z.object({
|
|
9423
8257
|
...vaultFields,
|
|
9424
|
-
text:
|
|
9425
|
-
limit:
|
|
8258
|
+
text: z.string().min(1).describe("Query text"),
|
|
8259
|
+
limit: z.number().int().positive().optional().describe("Max results (default 10)")
|
|
9426
8260
|
})
|
|
9427
8261
|
},
|
|
9428
8262
|
async ({ vault, wiki, text, limit }) => runMcpToolHandler("skillwiki.query", { vault, wiki }, async () => {
|
|
@@ -9436,13 +8270,13 @@ function registerMcpTools(server) {
|
|
|
9436
8270
|
"skillwiki.lint_summary",
|
|
9437
8271
|
{
|
|
9438
8272
|
description: "Vault lint bucket summary (read-only, no --fix). Returns Result envelope JSON.",
|
|
9439
|
-
inputSchema:
|
|
8273
|
+
inputSchema: z.object({
|
|
9440
8274
|
...vaultFields,
|
|
9441
|
-
only:
|
|
9442
|
-
examplesLimit:
|
|
9443
|
-
days:
|
|
9444
|
-
lines:
|
|
9445
|
-
logThreshold:
|
|
8275
|
+
only: z.string().optional().describe("Run a single lint bucket"),
|
|
8276
|
+
examplesLimit: z.number().int().nonnegative().optional().describe("Examples per bucket in summary (default 3)"),
|
|
8277
|
+
days: z.number().int().positive().optional().describe("Stale threshold days (default 90)"),
|
|
8278
|
+
lines: z.number().int().positive().optional().describe("Pagesize threshold lines (default 200)"),
|
|
8279
|
+
logThreshold: z.number().int().positive().optional().describe("Log rotation threshold (default 500)")
|
|
9446
8280
|
})
|
|
9447
8281
|
},
|
|
9448
8282
|
async (args) => runMcpToolHandler("skillwiki.lint_summary", { vault: args.vault, wiki: args.wiki }, async () => {
|
|
@@ -9466,7 +8300,7 @@ function registerMcpTools(server) {
|
|
|
9466
8300
|
"skillwiki.doctor",
|
|
9467
8301
|
{
|
|
9468
8302
|
description: "Diagnose skillwiki setup, vault path, sync, and plugin channels (read-only).",
|
|
9469
|
-
inputSchema:
|
|
8303
|
+
inputSchema: z.object({
|
|
9470
8304
|
...vaultFields
|
|
9471
8305
|
})
|
|
9472
8306
|
},
|
|
@@ -9487,9 +8321,9 @@ function registerMcpTools(server) {
|
|
|
9487
8321
|
"skillwiki.graph_build",
|
|
9488
8322
|
{
|
|
9489
8323
|
description: "Build wikilink graph JSON under .skillwiki/graph.json (writes graph file only).",
|
|
9490
|
-
inputSchema:
|
|
8324
|
+
inputSchema: z.object({
|
|
9491
8325
|
...vaultFields,
|
|
9492
|
-
out:
|
|
8326
|
+
out: z.string().optional().describe("Output path (default <vault>/.skillwiki/graph.json)")
|
|
9493
8327
|
})
|
|
9494
8328
|
},
|
|
9495
8329
|
async ({ vault, wiki, out }) => runMcpToolHandler("skillwiki.graph_build", { vault, wiki }, async () => {
|
|
@@ -9504,9 +8338,9 @@ function registerMcpTools(server) {
|
|
|
9504
8338
|
"skillwiki.project_index",
|
|
9505
8339
|
{
|
|
9506
8340
|
description: "List project index entries for a slug (read-only, apply=false).",
|
|
9507
|
-
inputSchema:
|
|
8341
|
+
inputSchema: z.object({
|
|
9508
8342
|
...vaultFields,
|
|
9509
|
-
slug:
|
|
8343
|
+
slug: z.string().min(1).describe("Project slug under projects/{slug}/")
|
|
9510
8344
|
})
|
|
9511
8345
|
},
|
|
9512
8346
|
async ({ vault, wiki, slug }) => runMcpToolHandler("skillwiki.project_index", { vault, wiki }, async () => {
|
|
@@ -9520,10 +8354,10 @@ function registerMcpTools(server) {
|
|
|
9520
8354
|
"skillwiki.stale",
|
|
9521
8355
|
{
|
|
9522
8356
|
description: "List stale pages, transcripts, and incomplete work items (read-only).",
|
|
9523
|
-
inputSchema:
|
|
8357
|
+
inputSchema: z.object({
|
|
9524
8358
|
...vaultFields,
|
|
9525
|
-
days:
|
|
9526
|
-
project:
|
|
8359
|
+
days: z.number().int().positive().optional().describe("Stale age threshold (default 90)"),
|
|
8360
|
+
project: z.string().optional().describe("Scope to one project slug")
|
|
9527
8361
|
})
|
|
9528
8362
|
},
|
|
9529
8363
|
async ({ vault, wiki, days, project }) => runMcpToolHandler("skillwiki.stale", { vault, wiki }, async () => {
|
|
@@ -9542,8 +8376,8 @@ function registerMcpTools(server) {
|
|
|
9542
8376
|
"skillwiki.config_get",
|
|
9543
8377
|
{
|
|
9544
8378
|
description: "Read a single skillwiki config key from ~/.skillwiki/.env (read-only).",
|
|
9545
|
-
inputSchema:
|
|
9546
|
-
key:
|
|
8379
|
+
inputSchema: z.object({
|
|
8380
|
+
key: z.string().min(1).describe("Config key (e.g. WIKI_PATH or profile key)")
|
|
9547
8381
|
})
|
|
9548
8382
|
},
|
|
9549
8383
|
async ({ key }) => runMcpToolHandler("skillwiki.config_get", {}, async () => {
|
|
@@ -9554,7 +8388,7 @@ function registerMcpTools(server) {
|
|
|
9554
8388
|
}
|
|
9555
8389
|
|
|
9556
8390
|
// src/mcp/mutating-tools.ts
|
|
9557
|
-
import { z as
|
|
8391
|
+
import { z as z2 } from "zod";
|
|
9558
8392
|
|
|
9559
8393
|
// src/mcp/mutation-gate.ts
|
|
9560
8394
|
function mcpMutationsEnabled() {
|
|
@@ -9565,20 +8399,20 @@ var MCP_MUTATION_DISABLED_MESSAGE = "Mutating MCP tools are disabled. Set SKILLW
|
|
|
9565
8399
|
|
|
9566
8400
|
// src/mcp/mutating-tools.ts
|
|
9567
8401
|
var vaultFields2 = {
|
|
9568
|
-
vault:
|
|
9569
|
-
wiki:
|
|
8402
|
+
vault: z2.string().optional(),
|
|
8403
|
+
wiki: z2.string().optional()
|
|
9570
8404
|
};
|
|
9571
8405
|
function registerMcpMutatingTools(server) {
|
|
9572
8406
|
server.registerTool(
|
|
9573
8407
|
"skillwiki.observe",
|
|
9574
8408
|
{
|
|
9575
8409
|
description: "Create a new raw/transcripts capture file (mutating). Requires SKILLWIKI_MCP_ALLOW_MUTATIONS=true.",
|
|
9576
|
-
inputSchema:
|
|
8410
|
+
inputSchema: z2.object({
|
|
9577
8411
|
...vaultFields2,
|
|
9578
|
-
text:
|
|
9579
|
-
kind:
|
|
9580
|
-
project:
|
|
9581
|
-
confirm_mutation:
|
|
8412
|
+
text: z2.string().min(1).describe("Capture body text"),
|
|
8413
|
+
kind: z2.enum(["note", "bug", "task", "idea", "session-log"]).optional(),
|
|
8414
|
+
project: z2.string().optional().describe("Project slug for frontmatter"),
|
|
8415
|
+
confirm_mutation: z2.literal(true).describe("Must be true to acknowledge vault write")
|
|
9582
8416
|
})
|
|
9583
8417
|
},
|
|
9584
8418
|
async (args) => runMcpToolHandler("skillwiki.observe", { vault: args.vault, wiki: args.wiki }, async () => {
|
|
@@ -9602,8 +8436,8 @@ function registerMcpMutatingTools(server) {
|
|
|
9602
8436
|
}
|
|
9603
8437
|
|
|
9604
8438
|
// src/mcp/resources.ts
|
|
9605
|
-
import { readFile as
|
|
9606
|
-
import { join as
|
|
8439
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
8440
|
+
import { join as join31 } from "path";
|
|
9607
8441
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9608
8442
|
|
|
9609
8443
|
// src/mcp/lint-bucket.ts
|
|
@@ -9731,9 +8565,9 @@ async function fetchQueryPreview(input) {
|
|
|
9731
8565
|
}
|
|
9732
8566
|
|
|
9733
8567
|
// src/mcp/graph-html.ts
|
|
9734
|
-
import { readFile as
|
|
9735
|
-
import { join as
|
|
9736
|
-
import { existsSync as
|
|
8568
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
8569
|
+
import { join as join30 } from "path";
|
|
8570
|
+
import { existsSync as existsSync15 } from "fs";
|
|
9737
8571
|
var TYPE_COLORS = {
|
|
9738
8572
|
entities: "#e74c3c",
|
|
9739
8573
|
concepts: "#27ae60",
|
|
@@ -9801,9 +8635,9 @@ ${nodeSvg}
|
|
|
9801
8635
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
9802
8636
|
}
|
|
9803
8637
|
async function fetchGraphHtmlReport(input) {
|
|
9804
|
-
const graphPath = input.graphPath ??
|
|
8638
|
+
const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
|
|
9805
8639
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
9806
|
-
if (!
|
|
8640
|
+
if (!existsSync15(graphPath)) {
|
|
9807
8641
|
return {
|
|
9808
8642
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
9809
8643
|
result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
|
|
@@ -9811,7 +8645,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
9811
8645
|
}
|
|
9812
8646
|
let raw;
|
|
9813
8647
|
try {
|
|
9814
|
-
raw = await
|
|
8648
|
+
raw = await readFile18(graphPath, "utf8");
|
|
9815
8649
|
} catch (e) {
|
|
9816
8650
|
return {
|
|
9817
8651
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -9875,7 +8709,7 @@ async function fetchStaleSummary(input) {
|
|
|
9875
8709
|
|
|
9876
8710
|
// src/mcp/resources.ts
|
|
9877
8711
|
async function readVaultFile(vault, rel) {
|
|
9878
|
-
return
|
|
8712
|
+
return readFile19(join31(vault, rel), "utf8");
|
|
9879
8713
|
}
|
|
9880
8714
|
async function tailLines(text, lines) {
|
|
9881
8715
|
const parts = text.split(/\r?\n/);
|
|
@@ -9961,9 +8795,9 @@ function registerMcpResources(server) {
|
|
|
9961
8795
|
if (!v.ok) {
|
|
9962
8796
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
9963
8797
|
}
|
|
9964
|
-
const path =
|
|
8798
|
+
const path = join31(v.data.vault, ".skillwiki", "graph.json");
|
|
9965
8799
|
try {
|
|
9966
|
-
const raw = await
|
|
8800
|
+
const raw = await readFile19(path, "utf8");
|
|
9967
8801
|
const graph = JSON.parse(raw);
|
|
9968
8802
|
const adjacency = graph.adjacency ?? {};
|
|
9969
8803
|
const nodes = Object.keys(adjacency);
|
|
@@ -10137,14 +8971,14 @@ function registerMcpResources(server) {
|
|
|
10137
8971
|
}
|
|
10138
8972
|
|
|
10139
8973
|
// src/mcp/prompts.ts
|
|
10140
|
-
import { z as
|
|
8974
|
+
import { z as z3 } from "zod";
|
|
10141
8975
|
function registerMcpPrompts(server) {
|
|
10142
8976
|
server.registerPrompt(
|
|
10143
8977
|
"skillwiki-research-query",
|
|
10144
8978
|
{
|
|
10145
8979
|
description: "Structured vault research using skillwiki.query and typed pages",
|
|
10146
8980
|
argsSchema: {
|
|
10147
|
-
topic:
|
|
8981
|
+
topic: z3.string().describe("Research topic or question")
|
|
10148
8982
|
}
|
|
10149
8983
|
},
|
|
10150
8984
|
({ topic }) => ({
|
|
@@ -10175,8 +9009,8 @@ function registerMcpPrompts(server) {
|
|
|
10175
9009
|
{
|
|
10176
9010
|
description: "Plan a project work item using vault project workspace conventions",
|
|
10177
9011
|
argsSchema: {
|
|
10178
|
-
slug:
|
|
10179
|
-
idea:
|
|
9012
|
+
slug: z3.string().describe("Project slug"),
|
|
9013
|
+
idea: z3.string().describe("Work item idea or bug/feature summary")
|
|
10180
9014
|
}
|
|
10181
9015
|
},
|
|
10182
9016
|
({ slug, idea }) => ({
|
|
@@ -10204,7 +9038,7 @@ function registerMcpPrompts(server) {
|
|
|
10204
9038
|
{
|
|
10205
9039
|
description: "Review vault health via lint summary, doctor, and stale tools",
|
|
10206
9040
|
argsSchema: {
|
|
10207
|
-
vault:
|
|
9041
|
+
vault: z3.string().optional().describe("Optional vault path")
|
|
10208
9042
|
}
|
|
10209
9043
|
},
|
|
10210
9044
|
({ vault }) => ({
|
|
@@ -10230,7 +9064,7 @@ function registerMcpPrompts(server) {
|
|
|
10230
9064
|
{
|
|
10231
9065
|
description: "Audit citation health using query + lint buckets",
|
|
10232
9066
|
argsSchema: {
|
|
10233
|
-
focus:
|
|
9067
|
+
focus: z3.string().optional().describe("Page or topic focus")
|
|
10234
9068
|
}
|
|
10235
9069
|
},
|
|
10236
9070
|
({ focus }) => ({
|
|
@@ -10273,20 +9107,9 @@ async function runSkillwikiMcpStdio() {
|
|
|
10273
9107
|
}
|
|
10274
9108
|
|
|
10275
9109
|
export {
|
|
10276
|
-
ExitCode,
|
|
10277
|
-
ok,
|
|
10278
|
-
err,
|
|
10279
|
-
RawSourceSchema,
|
|
10280
|
-
MetaSchema,
|
|
10281
|
-
isBlockedHost,
|
|
10282
|
-
splitFrontmatter,
|
|
10283
|
-
extractFrontmatter,
|
|
10284
|
-
scanSensitiveContent,
|
|
10285
|
-
redactSensitiveContent,
|
|
10286
9110
|
readLastOp,
|
|
10287
9111
|
appendLastOp,
|
|
10288
9112
|
clearLastOp,
|
|
10289
|
-
atomicWriteText,
|
|
10290
9113
|
runLogAppend,
|
|
10291
9114
|
renderIndexUpsert,
|
|
10292
9115
|
upsertIndexEntry,
|
|
@@ -10297,16 +9120,8 @@ export {
|
|
|
10297
9120
|
releaseLock,
|
|
10298
9121
|
acquireOwnedSyncLock,
|
|
10299
9122
|
releaseOwnedSyncLock,
|
|
10300
|
-
assertTargetInsideVault,
|
|
10301
|
-
prepareTypedPage,
|
|
10302
9123
|
runValidate,
|
|
10303
|
-
scanVault,
|
|
10304
|
-
readPage,
|
|
10305
9124
|
runGraphBuild,
|
|
10306
|
-
profileKey,
|
|
10307
|
-
parseDotenvText,
|
|
10308
|
-
parseDotenvFile,
|
|
10309
|
-
writeDotenv,
|
|
10310
9125
|
resolveInitTimePath,
|
|
10311
9126
|
resolveRuntimePath,
|
|
10312
9127
|
runOrphans,
|
|
@@ -10316,9 +9131,12 @@ export {
|
|
|
10316
9131
|
extractTaxonomy,
|
|
10317
9132
|
taxonomyCommentForPage,
|
|
10318
9133
|
reconcileTaxonomyDocument,
|
|
9134
|
+
mergeTaxonomyConflict,
|
|
10319
9135
|
runLinks,
|
|
10320
9136
|
runTagAudit,
|
|
10321
9137
|
runIndexCheck,
|
|
9138
|
+
renderProjectIndex,
|
|
9139
|
+
runProjectIndex,
|
|
10322
9140
|
runStale,
|
|
10323
9141
|
runPagesize,
|
|
10324
9142
|
runLogRotate,
|
|
@@ -10342,13 +9160,6 @@ export {
|
|
|
10342
9160
|
runConfigPath,
|
|
10343
9161
|
buildDegradedReasons,
|
|
10344
9162
|
probeRemoteHealth,
|
|
10345
|
-
FLEET_REL_PATH,
|
|
10346
|
-
runFleetValidate,
|
|
10347
|
-
runFleetContext,
|
|
10348
|
-
loadFleetManifestAndHost,
|
|
10349
|
-
snapshotterAliasForLocalHost,
|
|
10350
|
-
loadFleetManifest,
|
|
10351
|
-
resolveFleetHostId,
|
|
10352
9163
|
SATELLITE_STALE_MS,
|
|
10353
9164
|
satelliteLatestRunPath,
|
|
10354
9165
|
isFailedRunStatus,
|
|
@@ -10356,7 +9167,6 @@ export {
|
|
|
10356
9167
|
evaluateSatelliteRunHealth,
|
|
10357
9168
|
runDoctor,
|
|
10358
9169
|
readCliPackageJson,
|
|
10359
|
-
runProjectIndex,
|
|
10360
9170
|
runObserve,
|
|
10361
9171
|
runMemoryTopics,
|
|
10362
9172
|
runMemoryIndex,
|