skillwiki 0.9.63 → 0.10.1
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-C5OLZRRM.js +357 -0
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-R6BKJWVC.js +890 -0
- package/dist/chunk-SCGC7YNM.js +213 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
- package/dist/cli.js +1780 -697
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-PW4OOOMV.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 +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- 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 +24 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +24 -0
- package/skills/wiki-crystallize/SKILL.md +3 -0
|
@@ -3,334 +3,51 @@ 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
|
-
|
|
46
|
-
|
|
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
|
+
listReviewRequiredOps,
|
|
26
|
+
loadFleetManifestAndHost,
|
|
27
|
+
parseDotenvFile,
|
|
28
|
+
parseDotenvText,
|
|
29
|
+
profileKey,
|
|
30
|
+
resolveVaultSyncPullHelper,
|
|
31
|
+
satelliteGateFromFleetLoad,
|
|
32
|
+
snapshotterAliasForLocalHost,
|
|
33
|
+
writeDotenv
|
|
34
|
+
} from "./chunk-R6BKJWVC.js";
|
|
35
|
+
import {
|
|
36
|
+
CompoundSchema,
|
|
37
|
+
ExitCode,
|
|
38
|
+
MetaSchema,
|
|
39
|
+
RawSourceSchema,
|
|
40
|
+
TypedKnowledgeSchema,
|
|
41
|
+
WorkItemSchema,
|
|
42
|
+
detectSchema,
|
|
43
|
+
err,
|
|
44
|
+
getErrorMessage,
|
|
45
|
+
ok
|
|
46
|
+
} from "./chunk-C5OLZRRM.js";
|
|
330
47
|
|
|
331
48
|
// src/commands/log-append.ts
|
|
332
|
-
import { readFile
|
|
333
|
-
import { join as
|
|
49
|
+
import { readFile, stat } from "fs/promises";
|
|
50
|
+
import { join as join3 } from "path";
|
|
334
51
|
|
|
335
52
|
// src/utils/last-op.ts
|
|
336
53
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
@@ -374,58 +91,12 @@ function clearLastOp(vault) {
|
|
|
374
91
|
}
|
|
375
92
|
}
|
|
376
93
|
|
|
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
94
|
// src/utils/log-lock.ts
|
|
424
|
-
import { randomBytes
|
|
95
|
+
import { randomBytes } from "crypto";
|
|
425
96
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
426
|
-
import { join as
|
|
97
|
+
import { join as join2 } from "path";
|
|
427
98
|
function logLockPath(vault) {
|
|
428
|
-
return
|
|
99
|
+
return join2(vault, ".skillwiki", "log-append.lock");
|
|
429
100
|
}
|
|
430
101
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
431
102
|
function readLogLock(path) {
|
|
@@ -441,10 +112,10 @@ async function acquireLogLock(vault, opts = {}) {
|
|
|
441
112
|
const staleMs = opts.staleMs ?? 1e4;
|
|
442
113
|
const reclaimStale = opts.reclaimStale ?? true;
|
|
443
114
|
const path = logLockPath(vault);
|
|
444
|
-
const dir =
|
|
115
|
+
const dir = join2(vault, ".skillwiki");
|
|
445
116
|
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
446
117
|
const deadline = Date.now() + retryMs;
|
|
447
|
-
const ownerToken =
|
|
118
|
+
const ownerToken = randomBytes(16).toString("hex");
|
|
448
119
|
const acquired = (/* @__PURE__ */ new Date()).toISOString();
|
|
449
120
|
const content = JSON.stringify({ pid: process.pid, owner_token: ownerToken, acquired }) + "\n";
|
|
450
121
|
for (; ; ) {
|
|
@@ -487,146 +158,6 @@ function releaseLogLock(handle) {
|
|
|
487
158
|
}
|
|
488
159
|
}
|
|
489
160
|
|
|
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
161
|
// src/commands/log-append.ts
|
|
631
162
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
632
163
|
function operationMarker(operationId) {
|
|
@@ -638,7 +169,7 @@ function operationMarker(operationId) {
|
|
|
638
169
|
async function appendWhileLocked(logPath, content, marker) {
|
|
639
170
|
let logText;
|
|
640
171
|
try {
|
|
641
|
-
logText = await
|
|
172
|
+
logText = await readFile(logPath, "utf8");
|
|
642
173
|
} catch {
|
|
643
174
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
644
175
|
}
|
|
@@ -677,7 +208,7 @@ ${appendedContent}
|
|
|
677
208
|
}
|
|
678
209
|
async function runLogAppend(input) {
|
|
679
210
|
try {
|
|
680
|
-
await stat(
|
|
211
|
+
await stat(join3(input.vault, "SCHEMA.md"));
|
|
681
212
|
} catch {
|
|
682
213
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
683
214
|
}
|
|
@@ -706,7 +237,7 @@ async function runLogAppend(input) {
|
|
|
706
237
|
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
707
238
|
}
|
|
708
239
|
const lockHandle = acquired.data;
|
|
709
|
-
const logPath =
|
|
240
|
+
const logPath = join3(input.vault, "log.md");
|
|
710
241
|
let outcome;
|
|
711
242
|
let released;
|
|
712
243
|
try {
|
|
@@ -751,8 +282,8 @@ async function runLogAppend(input) {
|
|
|
751
282
|
|
|
752
283
|
// src/utils/sync-lock.ts
|
|
753
284
|
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
|
|
285
|
+
import { join as join4 } from "path";
|
|
286
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
756
287
|
function getEnvSessionId() {
|
|
757
288
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
758
289
|
if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
|
|
@@ -765,7 +296,7 @@ function getSessionId() {
|
|
|
765
296
|
}
|
|
766
297
|
function getCwdHash(cwd) {
|
|
767
298
|
const path = cwd || process.cwd();
|
|
768
|
-
const hash =
|
|
299
|
+
const hash = createHash("sha256").update(path).digest("hex");
|
|
769
300
|
return hash.slice(0, 8);
|
|
770
301
|
}
|
|
771
302
|
function getCliSessionId(cwd) {
|
|
@@ -774,7 +305,7 @@ function getCliSessionId(cwd) {
|
|
|
774
305
|
return `cli-${getCwdHash(cwd)}`;
|
|
775
306
|
}
|
|
776
307
|
function lockPath(vault) {
|
|
777
|
-
return
|
|
308
|
+
return join4(vault, ".skillwiki", "sync.lock");
|
|
778
309
|
}
|
|
779
310
|
function readLock(vault) {
|
|
780
311
|
const path = lockPath(vault);
|
|
@@ -793,7 +324,7 @@ function isStale(lock, now) {
|
|
|
793
324
|
}
|
|
794
325
|
function acquireLock(vault, opts = {}) {
|
|
795
326
|
const path = lockPath(vault);
|
|
796
|
-
const dir =
|
|
327
|
+
const dir = join4(vault, ".skillwiki");
|
|
797
328
|
if (!existsSync3(dir)) {
|
|
798
329
|
mkdirSync3(dir, { recursive: true });
|
|
799
330
|
}
|
|
@@ -864,7 +395,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
864
395
|
}
|
|
865
396
|
}
|
|
866
397
|
function acquireOwnedSyncLock(vault, opts) {
|
|
867
|
-
const ownerToken =
|
|
398
|
+
const ownerToken = randomBytes2(16).toString("hex");
|
|
868
399
|
const sessionId = `publish-${process.pid}-${ownerToken.slice(0, 12)}`;
|
|
869
400
|
const now = /* @__PURE__ */ new Date();
|
|
870
401
|
const lock = {
|
|
@@ -878,7 +409,7 @@ function acquireOwnedSyncLock(vault, opts) {
|
|
|
878
409
|
};
|
|
879
410
|
const path = lockPath(vault);
|
|
880
411
|
try {
|
|
881
|
-
mkdirSync3(
|
|
412
|
+
mkdirSync3(join4(vault, ".skillwiki"), { recursive: true });
|
|
882
413
|
} catch (error) {
|
|
883
414
|
return err("WRITE_FAILED", { path, message: String(error) });
|
|
884
415
|
}
|
|
@@ -909,40 +440,13 @@ function releaseOwnedSyncLock(handle) {
|
|
|
909
440
|
}
|
|
910
441
|
|
|
911
442
|
// 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
|
-
}
|
|
443
|
+
import { createHash as createHash2 } from "crypto";
|
|
444
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
445
|
+
import { resolve, relative, sep } from "path";
|
|
942
446
|
|
|
943
447
|
// src/utils/index-entry.ts
|
|
944
|
-
import { readFile as
|
|
945
|
-
import { join as
|
|
448
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
449
|
+
import { join as join5 } from "path";
|
|
946
450
|
var TYPE_SECTION = {
|
|
947
451
|
entity: "Entities",
|
|
948
452
|
concept: "Concepts",
|
|
@@ -986,128 +490,31 @@ function renderIndexUpsert(text, input) {
|
|
|
986
490
|
});
|
|
987
491
|
}
|
|
988
492
|
async function upsertIndexEntry(input) {
|
|
989
|
-
const path =
|
|
990
|
-
let
|
|
493
|
+
const path = join5(input.vault, "index.md");
|
|
494
|
+
let before = "";
|
|
991
495
|
try {
|
|
992
|
-
|
|
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;
|
|
1024
|
-
try {
|
|
1025
|
-
vaultReal = realpathSync(vault);
|
|
496
|
+
before = await readFile2(path, "utf8");
|
|
1026
497
|
} catch {
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
const absolutePath2 = resolve(vaultReal, target);
|
|
1030
|
-
const parent = dirname2(absolutePath2);
|
|
1031
|
-
let parentReal;
|
|
1032
|
-
try {
|
|
1033
|
-
parentReal = realpathSync(parent);
|
|
1034
|
-
} catch {
|
|
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" });
|
|
498
|
+
before = "";
|
|
1040
499
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
500
|
+
const projection = await renderRootIndex({ vault: input.vault, currentText: before });
|
|
501
|
+
if (projection.ok) {
|
|
502
|
+
if (projection.data.text === before) return ok({ changed: false });
|
|
503
|
+
const written2 = await writeRootIndexProjection(input.vault, projection.data);
|
|
504
|
+
if (!written2.ok) return written2;
|
|
505
|
+
return ok({ changed: written2.data.changed });
|
|
1043
506
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
return ok({ absolutePath: absolutePath2, existingRealPath });
|
|
1064
|
-
}
|
|
1065
|
-
function invalidFrontmatter(target, issues) {
|
|
1066
|
-
return err("INVALID_FRONTMATTER", {
|
|
1067
|
-
target,
|
|
1068
|
-
errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
|
|
507
|
+
const rendered = renderIndexUpsert(before, input);
|
|
508
|
+
if (!rendered.ok) return rendered;
|
|
509
|
+
if (!rendered.data.changed) return ok({ changed: false });
|
|
510
|
+
const written = await writeRootIndexProjection(input.vault, {
|
|
511
|
+
text: rendered.data.text,
|
|
512
|
+
entries: [],
|
|
513
|
+
duplicates_removed: 0,
|
|
514
|
+
ghosts_removed: []
|
|
1069
515
|
});
|
|
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, []);
|
|
516
|
+
if (!written.ok) return written;
|
|
517
|
+
return ok({ changed: written.data.changed });
|
|
1111
518
|
}
|
|
1112
519
|
|
|
1113
520
|
// src/commands/validate.ts
|
|
@@ -1121,7 +528,7 @@ var SCHEMAS = {
|
|
|
1121
528
|
async function runValidate(input) {
|
|
1122
529
|
let text;
|
|
1123
530
|
try {
|
|
1124
|
-
text = await
|
|
531
|
+
text = await readFile3(input.file, "utf8");
|
|
1125
532
|
} catch {
|
|
1126
533
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
1127
534
|
}
|
|
@@ -1172,13 +579,13 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
1172
579
|
let logUpdated = false;
|
|
1173
580
|
let applyHint = "";
|
|
1174
581
|
if (input.apply && input.vault) {
|
|
1175
|
-
const absFile =
|
|
1176
|
-
const absVault =
|
|
1177
|
-
const relPath =
|
|
582
|
+
const absFile = resolve(input.file);
|
|
583
|
+
const absVault = resolve(input.vault);
|
|
584
|
+
const relPath = relative(absVault, absFile).split(sep).join("/");
|
|
1178
585
|
if (relPath.startsWith("..")) {
|
|
1179
586
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
|
|
1180
587
|
}
|
|
1181
|
-
const operationId =
|
|
588
|
+
const operationId = createHash2("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
|
|
1182
589
|
if (det.schema === "typed-knowledge" || det.schema === "meta") {
|
|
1183
590
|
const prepared = prepareTypedPage(text, relPath);
|
|
1184
591
|
if (!prepared.ok) {
|
|
@@ -1262,109 +669,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
|
1262
669
|
|
|
1263
670
|
// src/commands/graph.ts
|
|
1264
671
|
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
|
-
}
|
|
672
|
+
import { dirname } from "path";
|
|
1368
673
|
|
|
1369
674
|
// src/parsers/wikilinks.ts
|
|
1370
675
|
var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
@@ -1518,7 +823,7 @@ async function runGraphBuild(input) {
|
|
|
1518
823
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
1519
824
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
1520
825
|
try {
|
|
1521
|
-
await mkdir(
|
|
826
|
+
await mkdir(dirname(input.out), { recursive: true });
|
|
1522
827
|
await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
|
|
1523
828
|
} catch (e) {
|
|
1524
829
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
@@ -1561,102 +866,8 @@ function computeAdamicAdar(adj) {
|
|
|
1561
866
|
return out;
|
|
1562
867
|
}
|
|
1563
868
|
|
|
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
869
|
// src/utils/wiki-path.ts
|
|
1659
|
-
import { join as
|
|
870
|
+
import { join as join6 } from "path";
|
|
1660
871
|
async function resolveInitTimePath(input) {
|
|
1661
872
|
const chain = [];
|
|
1662
873
|
if (input.flag !== void 0 && input.flag.length > 0) {
|
|
@@ -1669,27 +880,27 @@ async function resolveInitTimePath(input) {
|
|
|
1669
880
|
return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
|
|
1670
881
|
}
|
|
1671
882
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1672
|
-
const sw = await parseDotenvFile(
|
|
883
|
+
const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1673
884
|
if (sw.WIKI_PATH !== void 0) {
|
|
1674
885
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
|
|
1675
886
|
return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
|
|
1676
887
|
}
|
|
1677
888
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
1678
|
-
const hermes = await parseDotenvFile(
|
|
889
|
+
const hermes = await parseDotenvFile(join6(input.home, ".hermes", ".env"));
|
|
1679
890
|
if (hermes.WIKI_PATH !== void 0) {
|
|
1680
891
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
|
|
1681
892
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
1682
893
|
}
|
|
1683
894
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
1684
895
|
if (input.cwd) {
|
|
1685
|
-
const projCfg = await parseDotenvFile(
|
|
896
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1686
897
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1687
898
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1688
899
|
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
1689
900
|
}
|
|
1690
901
|
}
|
|
1691
902
|
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
1692
|
-
const fallback =
|
|
903
|
+
const fallback = join6(input.home, "wiki");
|
|
1693
904
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
1694
905
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
1695
906
|
}
|
|
@@ -1700,7 +911,7 @@ async function resolveRuntimePath(input) {
|
|
|
1700
911
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
1701
912
|
}
|
|
1702
913
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
1703
|
-
const swGlobal = await parseDotenvFile(
|
|
914
|
+
const swGlobal = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
|
|
1704
915
|
const wikiName = input.wiki;
|
|
1705
916
|
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
1706
917
|
if (wikiName.toLowerCase() === "default") {
|
|
@@ -1744,7 +955,7 @@ async function resolveRuntimePath(input) {
|
|
|
1744
955
|
}
|
|
1745
956
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
1746
957
|
if (input.cwd) {
|
|
1747
|
-
const projCfg = await parseDotenvFile(
|
|
958
|
+
const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
|
|
1748
959
|
if (projCfg.WIKI_PATH !== void 0) {
|
|
1749
960
|
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
1750
961
|
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
@@ -1864,8 +1075,8 @@ function simulateRemoval(adj, removed) {
|
|
|
1864
1075
|
}
|
|
1865
1076
|
|
|
1866
1077
|
// src/commands/audit.ts
|
|
1867
|
-
import { readFile as
|
|
1868
|
-
import { dirname as
|
|
1078
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1079
|
+
import { dirname as dirname2, resolve as resolve2, join as join8 } from "path";
|
|
1869
1080
|
|
|
1870
1081
|
// src/parsers/citations.ts
|
|
1871
1082
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
@@ -1975,9 +1186,9 @@ function hasWikilinkCitations(body) {
|
|
|
1975
1186
|
}
|
|
1976
1187
|
|
|
1977
1188
|
// src/utils/raw-source.ts
|
|
1978
|
-
import { existsSync as
|
|
1979
|
-
import { stat as
|
|
1980
|
-
import { join as
|
|
1189
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1190
|
+
import { stat as stat2 } from "fs/promises";
|
|
1191
|
+
import { join as join7 } from "path";
|
|
1981
1192
|
function normalizeRawSourceTarget(entry) {
|
|
1982
1193
|
let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
1983
1194
|
target = target.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
@@ -1987,21 +1198,21 @@ function normalizeRawSourceTarget(entry) {
|
|
|
1987
1198
|
function rawSourceTargetCandidates(vault, target) {
|
|
1988
1199
|
const normalized = normalizeRawSourceTarget(target);
|
|
1989
1200
|
if (!normalized) return [];
|
|
1990
|
-
const candidates = [
|
|
1991
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
1201
|
+
const candidates = [join7(vault, normalized)];
|
|
1202
|
+
if (!normalized.endsWith(".md")) candidates.push(join7(vault, `${normalized}.md`));
|
|
1992
1203
|
if (normalized.startsWith("raw/")) {
|
|
1993
|
-
candidates.push(
|
|
1994
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
1204
|
+
candidates.push(join7(vault, "_archive", normalized));
|
|
1205
|
+
if (!normalized.endsWith(".md")) candidates.push(join7(vault, "_archive", `${normalized}.md`));
|
|
1995
1206
|
}
|
|
1996
1207
|
return [...new Set(candidates)];
|
|
1997
1208
|
}
|
|
1998
1209
|
function rawSourceTargetExistsSync(vault, target) {
|
|
1999
|
-
return rawSourceTargetCandidates(vault, target).some((candidate) =>
|
|
1210
|
+
return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync4(candidate));
|
|
2000
1211
|
}
|
|
2001
1212
|
async function rawSourceTargetExists(vault, target) {
|
|
2002
1213
|
for (const candidate of rawSourceTargetCandidates(vault, target)) {
|
|
2003
1214
|
try {
|
|
2004
|
-
await
|
|
1215
|
+
await stat2(candidate);
|
|
2005
1216
|
return true;
|
|
2006
1217
|
} catch {
|
|
2007
1218
|
}
|
|
@@ -2013,7 +1224,7 @@ async function rawSourceTargetExists(vault, target) {
|
|
|
2013
1224
|
async function runAudit(input) {
|
|
2014
1225
|
let text;
|
|
2015
1226
|
try {
|
|
2016
|
-
text = await
|
|
1227
|
+
text = await readFile4(input.file, "utf8");
|
|
2017
1228
|
} catch {
|
|
2018
1229
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
2019
1230
|
}
|
|
@@ -2021,7 +1232,7 @@ async function runAudit(input) {
|
|
|
2021
1232
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
2022
1233
|
const split = splitFrontmatter(text);
|
|
2023
1234
|
const body = split.ok ? split.data.body : text;
|
|
2024
|
-
const vault = await findVaultRoot(
|
|
1235
|
+
const vault = await findVaultRoot(dirname2(resolve2(input.file)));
|
|
2025
1236
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
2026
1237
|
const markers = extractCitationMarkers(body);
|
|
2027
1238
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -2066,11 +1277,11 @@ async function findVaultRoot(start) {
|
|
|
2066
1277
|
let cur = start;
|
|
2067
1278
|
for (let i = 0; i < 20; i++) {
|
|
2068
1279
|
try {
|
|
2069
|
-
await
|
|
1280
|
+
await stat3(join8(cur, "SCHEMA.md"));
|
|
2070
1281
|
return cur;
|
|
2071
1282
|
} catch {
|
|
2072
1283
|
}
|
|
2073
|
-
const parent =
|
|
1284
|
+
const parent = dirname2(cur);
|
|
2074
1285
|
if (parent === cur) return null;
|
|
2075
1286
|
cur = parent;
|
|
2076
1287
|
}
|
|
@@ -2158,11 +1369,11 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
|
|
|
2158
1369
|
}
|
|
2159
1370
|
|
|
2160
1371
|
// src/commands/tag-audit.ts
|
|
2161
|
-
import { readFile as
|
|
2162
|
-
import { join as
|
|
1372
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1373
|
+
import { join as join9 } from "path";
|
|
2163
1374
|
|
|
2164
1375
|
// src/parsers/taxonomy.ts
|
|
2165
|
-
import
|
|
1376
|
+
import yaml from "js-yaml";
|
|
2166
1377
|
var TAG_SLUG_RE = /^[a-z0-9][a-z0-9_./-]*$/;
|
|
2167
1378
|
function taxonomyItemIndent(yamlText) {
|
|
2168
1379
|
const lines = yamlText.split(/\r?\n/);
|
|
@@ -2184,12 +1395,12 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
2184
1395
|
const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
|
|
2185
1396
|
const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
|
|
2186
1397
|
const sectionText = schemaText.slice(afterHeading, sectionEnd);
|
|
2187
|
-
const
|
|
2188
|
-
if (!
|
|
1398
|
+
const open = /^```yaml[ \t]*\r?$/m.exec(sectionText);
|
|
1399
|
+
if (!open || open.index === void 0) {
|
|
2189
1400
|
return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
|
|
2190
1401
|
}
|
|
2191
|
-
const openStart = afterHeading +
|
|
2192
|
-
const yamlStart = openStart +
|
|
1402
|
+
const openStart = afterHeading + open.index;
|
|
1403
|
+
const yamlStart = openStart + open[0].length + 1;
|
|
2193
1404
|
const afterOpen = schemaText.slice(yamlStart, sectionEnd);
|
|
2194
1405
|
const close = /^```[ \t]*\r?$/m.exec(afterOpen);
|
|
2195
1406
|
if (!close || close.index === void 0) {
|
|
@@ -2201,7 +1412,7 @@ function parseTaxonomyDocument(schemaText) {
|
|
|
2201
1412
|
const yamlText = schemaText.slice(yamlStart, yamlEnd);
|
|
2202
1413
|
let parsed;
|
|
2203
1414
|
try {
|
|
2204
|
-
parsed =
|
|
1415
|
+
parsed = yaml.load(yamlText, { schema: yaml.JSON_SCHEMA });
|
|
2205
1416
|
} catch (error) {
|
|
2206
1417
|
return err("INVALID_FRONTMATTER", { message: getErrorMessage(error) });
|
|
2207
1418
|
}
|
|
@@ -2221,8 +1432,8 @@ function extractTaxonomy(schemaText) {
|
|
|
2221
1432
|
return ok(parsed.data.tags);
|
|
2222
1433
|
}
|
|
2223
1434
|
function renderTag(tag) {
|
|
2224
|
-
const roundTrip =
|
|
2225
|
-
`, { schema:
|
|
1435
|
+
const roundTrip = yaml.load(`value: ${tag}
|
|
1436
|
+
`, { schema: yaml.JSON_SCHEMA });
|
|
2226
1437
|
return typeof roundTrip.value === "string" && roundTrip.value === tag ? tag : JSON.stringify(tag);
|
|
2227
1438
|
}
|
|
2228
1439
|
function taxonomyCommentForPage(page, date, reason) {
|
|
@@ -2276,13 +1487,43 @@ function reconcileTaxonomyDocument(schemaText, input) {
|
|
|
2276
1487
|
changed: true
|
|
2277
1488
|
});
|
|
2278
1489
|
}
|
|
1490
|
+
function taxonomyEnvelope(text, doc) {
|
|
1491
|
+
return text.slice(0, doc.yamlStart) + "<taxonomy-yaml>" + text.slice(doc.closingFenceStart);
|
|
1492
|
+
}
|
|
1493
|
+
function mergeTaxonomyConflict(baseText, oursText, theirsText) {
|
|
1494
|
+
const base = parseTaxonomyDocument(baseText);
|
|
1495
|
+
const ours = parseTaxonomyDocument(oursText);
|
|
1496
|
+
const theirs = parseTaxonomyDocument(theirsText);
|
|
1497
|
+
if (!base.ok) return base;
|
|
1498
|
+
if (!ours.ok) return ours;
|
|
1499
|
+
if (!theirs.ok) return theirs;
|
|
1500
|
+
const envelope = taxonomyEnvelope(baseText, base.data);
|
|
1501
|
+
if (taxonomyEnvelope(oursText, ours.data) !== envelope || taxonomyEnvelope(theirsText, theirs.data) !== envelope) {
|
|
1502
|
+
return err("SCHEME_REJECTED", { reason: "non-taxonomy-change" });
|
|
1503
|
+
}
|
|
1504
|
+
const baseTags = new Set(base.data.tags);
|
|
1505
|
+
const addedFromOurs = ours.data.tags.filter((tag) => !baseTags.has(tag)).sort();
|
|
1506
|
+
const addedFromTheirs = theirs.data.tags.filter((tag) => !baseTags.has(tag)).sort();
|
|
1507
|
+
const tags = [...base.data.tags, .../* @__PURE__ */ new Set([...addedFromOurs, ...addedFromTheirs])];
|
|
1508
|
+
const rendered = reconcileTaxonomyDocument(baseText, {
|
|
1509
|
+
tags,
|
|
1510
|
+
comment: "# -- reconciled: taxonomy-only three-stage merge --"
|
|
1511
|
+
});
|
|
1512
|
+
if (!rendered.ok) return rendered;
|
|
1513
|
+
return ok({
|
|
1514
|
+
text: rendered.data.text,
|
|
1515
|
+
tags,
|
|
1516
|
+
added_from_ours: addedFromOurs,
|
|
1517
|
+
added_from_theirs: addedFromTheirs
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
2279
1520
|
|
|
2280
1521
|
// src/commands/tag-audit.ts
|
|
2281
1522
|
async function runTagAudit(input) {
|
|
2282
1523
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
2283
1524
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
2284
1525
|
const scan = scanResult.data;
|
|
2285
|
-
const schemaText = await
|
|
1526
|
+
const schemaText = await readFile5(join9(input.vault, "SCHEMA.md"), "utf8");
|
|
2286
1527
|
const tax = extractTaxonomy(schemaText);
|
|
2287
1528
|
if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
|
|
2288
1529
|
const allowed = new Set(tax.data);
|
|
@@ -2299,69 +1540,328 @@ async function runTagAudit(input) {
|
|
|
2299
1540
|
pageViolations.push({ page: p.relPath, tag: t });
|
|
2300
1541
|
}
|
|
2301
1542
|
}
|
|
2302
|
-
return pageViolations;
|
|
1543
|
+
return pageViolations;
|
|
1544
|
+
});
|
|
1545
|
+
for (const result of perPage) {
|
|
1546
|
+
if (!Array.isArray(result)) {
|
|
1547
|
+
return { exitCode: ExitCode.INVALID_FRONTMATTER, result };
|
|
1548
|
+
}
|
|
1549
|
+
violations.push(...result);
|
|
1550
|
+
}
|
|
1551
|
+
if (violations.length > 0) {
|
|
1552
|
+
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") }) };
|
|
1553
|
+
}
|
|
1554
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/commands/index-check.ts
|
|
1558
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1559
|
+
import { join as join10 } from "path";
|
|
1560
|
+
function normalizeIndexTarget(raw) {
|
|
1561
|
+
return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
|
|
1562
|
+
}
|
|
1563
|
+
async function runIndexCheck(input) {
|
|
1564
|
+
const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
1565
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1566
|
+
let indexText = "";
|
|
1567
|
+
try {
|
|
1568
|
+
indexText = await readFile6(join10(input.vault, "index.md"), "utf8");
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
const indexTargets = /* @__PURE__ */ new Set();
|
|
1572
|
+
const indexBare = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1574
|
+
const target = normalizeIndexTarget(s);
|
|
1575
|
+
indexTargets.add(target);
|
|
1576
|
+
const bare = target.split("/").pop().toLowerCase();
|
|
1577
|
+
const list = indexBare.get(bare) ?? [];
|
|
1578
|
+
list.push(target);
|
|
1579
|
+
indexBare.set(bare, list);
|
|
1580
|
+
}
|
|
1581
|
+
const required = /* @__PURE__ */ new Map();
|
|
1582
|
+
const known = /* @__PURE__ */ new Set();
|
|
1583
|
+
for (const p of scan.data.typedKnowledge) {
|
|
1584
|
+
const target = p.relPath.replace(/\.md$/, "");
|
|
1585
|
+
required.set(target, p.relPath);
|
|
1586
|
+
known.add(target);
|
|
1587
|
+
}
|
|
1588
|
+
for (const p of scan.data.compound) {
|
|
1589
|
+
known.add(p.relPath.replace(/\.md$/, ""));
|
|
1590
|
+
}
|
|
1591
|
+
const missing_from_index = [];
|
|
1592
|
+
for (const [target, relPath] of required.entries()) {
|
|
1593
|
+
if (indexTargets.has(target)) continue;
|
|
1594
|
+
const bare = target.split("/").pop().toLowerCase();
|
|
1595
|
+
const bareHits = indexBare.get(bare) ?? [];
|
|
1596
|
+
const basenameOnly = bareHits.filter((t) => !t.includes("/"));
|
|
1597
|
+
const sameNameRequired = [...required.keys()].filter(
|
|
1598
|
+
(t) => t.split("/").pop().toLowerCase() === bare
|
|
1599
|
+
);
|
|
1600
|
+
if (basenameOnly.length === 1 && sameNameRequired.length === 1) continue;
|
|
1601
|
+
missing_from_index.push(relPath);
|
|
1602
|
+
}
|
|
1603
|
+
const ghost_entries = [];
|
|
1604
|
+
for (const target of indexTargets) {
|
|
1605
|
+
if (known.has(target)) continue;
|
|
1606
|
+
if (!target.includes("/")) {
|
|
1607
|
+
const bare = target.toLowerCase();
|
|
1608
|
+
const matches = [...known].filter((k) => k.split("/").pop().toLowerCase() === bare);
|
|
1609
|
+
if (matches.length === 0) ghost_entries.push(target);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
ghost_entries.push(target);
|
|
1613
|
+
}
|
|
1614
|
+
const hintLines = [];
|
|
1615
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1616
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1617
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1618
|
+
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1619
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1620
|
+
}
|
|
1621
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/commands/project-index.ts
|
|
1625
|
+
import { readdir, readFile as readFile7, mkdir as mkdir2 } from "fs/promises";
|
|
1626
|
+
import { join as join11, dirname as dirname3, basename } from "path";
|
|
1627
|
+
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
1628
|
+
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
1629
|
+
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
1630
|
+
const found = [];
|
|
1631
|
+
let entries;
|
|
1632
|
+
try {
|
|
1633
|
+
entries = await readdir(rootAbs, { withFileTypes: true });
|
|
1634
|
+
} catch {
|
|
1635
|
+
return found;
|
|
1636
|
+
}
|
|
1637
|
+
for (const entry of entries) {
|
|
1638
|
+
const abs = join11(rootAbs, entry.name);
|
|
1639
|
+
const rel = `${rootRel}/${entry.name}`;
|
|
1640
|
+
if (entry.isDirectory()) {
|
|
1641
|
+
found.push(...await scanMarkdownTree(abs, rel));
|
|
1642
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1643
|
+
found.push(rel);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return found;
|
|
1647
|
+
}
|
|
1648
|
+
function projectLocalType(slug, page, data) {
|
|
1649
|
+
if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
|
|
1650
|
+
if (page.startsWith(`projects/${slug}/work/`)) {
|
|
1651
|
+
if (typeof data.kind === "string") return data.kind;
|
|
1652
|
+
const name = basename(page, ".md");
|
|
1653
|
+
if (name === "spec" || name === "plan" || name === "retro") return name;
|
|
1654
|
+
return "work";
|
|
1655
|
+
}
|
|
1656
|
+
if (page.startsWith(`projects/${slug}/architecture/`)) {
|
|
1657
|
+
return typeof data.type === "string" ? data.type : "architecture";
|
|
1658
|
+
}
|
|
1659
|
+
if (page.startsWith(`projects/${slug}/history/`)) {
|
|
1660
|
+
if (typeof data.kind === "string") return data.kind;
|
|
1661
|
+
if (typeof data.type === "string") return data.type;
|
|
1662
|
+
return "history";
|
|
1663
|
+
}
|
|
1664
|
+
return typeof data.type === "string" ? data.type : "project";
|
|
1665
|
+
}
|
|
1666
|
+
async function renderProjectIndex(vault, slug, opts = {}) {
|
|
1667
|
+
const projectDir = join11(vault, "projects", slug);
|
|
1668
|
+
try {
|
|
1669
|
+
await readdir(projectDir);
|
|
1670
|
+
} catch {
|
|
1671
|
+
return err("PROJECT_NOT_FOUND", { slug, path: projectDir });
|
|
1672
|
+
}
|
|
1673
|
+
const wikilinkPattern = `[[${slug}]]`;
|
|
1674
|
+
const entries = [];
|
|
1675
|
+
const compoundDir = join11(vault, "projects", slug, "compound");
|
|
1676
|
+
try {
|
|
1677
|
+
const compoundFiles = await readdir(compoundDir, { withFileTypes: true });
|
|
1678
|
+
for (const entry of compoundFiles) {
|
|
1679
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1680
|
+
const filePath = join11(compoundDir, entry.name);
|
|
1681
|
+
let text;
|
|
1682
|
+
try {
|
|
1683
|
+
text = await readFile7(filePath, "utf8");
|
|
1684
|
+
} catch {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const fm = extractFrontmatter(text);
|
|
1688
|
+
if (!fm.ok) continue;
|
|
1689
|
+
entries.push({
|
|
1690
|
+
page: `projects/${slug}/compound/${entry.name}`,
|
|
1691
|
+
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
1692
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
} catch {
|
|
1696
|
+
}
|
|
1697
|
+
for (const dir of LAYER2_DIRS) {
|
|
1698
|
+
let files;
|
|
1699
|
+
try {
|
|
1700
|
+
files = await readdir(join11(vault, dir), { withFileTypes: true });
|
|
1701
|
+
} catch {
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
for (const entry of files) {
|
|
1705
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1706
|
+
const filePath = join11(vault, dir, entry.name);
|
|
1707
|
+
let text;
|
|
1708
|
+
try {
|
|
1709
|
+
text = await readFile7(filePath, "utf8");
|
|
1710
|
+
} catch {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
const fm = extractFrontmatter(text);
|
|
1714
|
+
if (!fm.ok) continue;
|
|
1715
|
+
const pp = fm.data.provenance_projects;
|
|
1716
|
+
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
1717
|
+
entries.push({
|
|
1718
|
+
page: `${dir}/${entry.name}`,
|
|
1719
|
+
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
1720
|
+
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
1725
|
+
const rootAbs = join11(projectDir, dir);
|
|
1726
|
+
const rootRel = `projects/${slug}/${dir}`;
|
|
1727
|
+
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
1728
|
+
for (const page of pages) {
|
|
1729
|
+
const filePath = join11(vault, page);
|
|
1730
|
+
let text;
|
|
1731
|
+
try {
|
|
1732
|
+
text = await readFile7(filePath, "utf8");
|
|
1733
|
+
} catch {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
const fm = extractFrontmatter(text);
|
|
1737
|
+
if (!fm.ok) continue;
|
|
1738
|
+
entries.push({
|
|
1739
|
+
page,
|
|
1740
|
+
type: projectLocalType(slug, page, fm.data),
|
|
1741
|
+
title: typeof fm.data.title === "string" ? fm.data.title : basename(page, ".md")
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const typeOrder = {
|
|
1746
|
+
entity: 0,
|
|
1747
|
+
concept: 1,
|
|
1748
|
+
comparison: 2,
|
|
1749
|
+
query: 3,
|
|
1750
|
+
summary: 4,
|
|
1751
|
+
meta: 5,
|
|
1752
|
+
requirement: 6,
|
|
1753
|
+
spec: 7,
|
|
1754
|
+
plan: 8,
|
|
1755
|
+
retro: 9,
|
|
1756
|
+
architecture: 10,
|
|
1757
|
+
pattern: 11,
|
|
1758
|
+
gotcha: 12,
|
|
1759
|
+
lesson: 13,
|
|
1760
|
+
antipattern: 14,
|
|
1761
|
+
compound: 15,
|
|
1762
|
+
work: 16,
|
|
1763
|
+
history: 17
|
|
1764
|
+
};
|
|
1765
|
+
entries.sort((a, b) => {
|
|
1766
|
+
const ta = typeOrder[a.type] ?? 99;
|
|
1767
|
+
const tb = typeOrder[b.type] ?? 99;
|
|
1768
|
+
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
2303
1769
|
});
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
1770
|
+
const today = opts.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1771
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1772
|
+
for (const e of entries) {
|
|
1773
|
+
const group = e.type;
|
|
1774
|
+
if (!grouped.has(group)) grouped.set(group, []);
|
|
1775
|
+
grouped.get(group).push(e);
|
|
1776
|
+
}
|
|
1777
|
+
let body = `# Knowledge Index: ${slug}
|
|
1778
|
+
|
|
1779
|
+
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
1780
|
+
|
|
1781
|
+
`;
|
|
1782
|
+
for (const [type, items] of grouped) {
|
|
1783
|
+
body += `## ${type}
|
|
1784
|
+
|
|
1785
|
+
`;
|
|
1786
|
+
for (const item of items) {
|
|
1787
|
+
const pageRef = item.page.replace(/\.md$/, "");
|
|
1788
|
+
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
1789
|
+
`;
|
|
2307
1790
|
}
|
|
2308
|
-
|
|
1791
|
+
body += "\n";
|
|
2309
1792
|
}
|
|
2310
|
-
if (
|
|
2311
|
-
|
|
1793
|
+
if (entries.length === 0) {
|
|
1794
|
+
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
1795
|
+
`;
|
|
2312
1796
|
}
|
|
2313
|
-
return
|
|
1797
|
+
return ok({
|
|
1798
|
+
text: body,
|
|
1799
|
+
entries,
|
|
1800
|
+
index_path: `projects/${slug}/knowledge.md`
|
|
1801
|
+
});
|
|
2314
1802
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
1803
|
+
async function runProjectIndex(input) {
|
|
1804
|
+
const slug = input.slug;
|
|
1805
|
+
const projectDir = join11(input.vault, "projects", slug);
|
|
1806
|
+
const rendered = await renderProjectIndex(input.vault, slug);
|
|
1807
|
+
if (!rendered.ok) {
|
|
1808
|
+
return {
|
|
1809
|
+
exitCode: rendered.error === "PROJECT_NOT_FOUND" ? ExitCode.PROJECT_NOT_FOUND : ExitCode.WRITE_FAILED,
|
|
1810
|
+
result: rendered
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
const indexPath = join11(projectDir, "knowledge.md");
|
|
1814
|
+
const entries = rendered.data.entries;
|
|
1815
|
+
let existing = false;
|
|
1816
|
+
let stale = false;
|
|
2323
1817
|
try {
|
|
2324
|
-
|
|
1818
|
+
const existingText = await readFile7(indexPath, "utf8");
|
|
1819
|
+
existing = true;
|
|
1820
|
+
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
1821
|
+
const existingPages = new Set(existingEntries.map((l) => {
|
|
1822
|
+
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
1823
|
+
return m ? m[1] : "";
|
|
1824
|
+
}));
|
|
1825
|
+
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
1826
|
+
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
2325
1827
|
} catch {
|
|
2326
1828
|
}
|
|
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") }) };
|
|
1829
|
+
if (input.apply) {
|
|
1830
|
+
try {
|
|
1831
|
+
await mkdir2(dirname3(indexPath), { recursive: true });
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
return {
|
|
1834
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1835
|
+
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
const written = await atomicWriteText(indexPath, rendered.data.text);
|
|
1839
|
+
if (!written.ok) {
|
|
1840
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
1841
|
+
}
|
|
2358
1842
|
}
|
|
2359
|
-
|
|
1843
|
+
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
1844
|
+
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
1845
|
+
return {
|
|
1846
|
+
exitCode: ExitCode.OK,
|
|
1847
|
+
result: ok({
|
|
1848
|
+
slug,
|
|
1849
|
+
entries,
|
|
1850
|
+
existing,
|
|
1851
|
+
stale,
|
|
1852
|
+
index_path: rendered.data.index_path,
|
|
1853
|
+
humanHint: `project: ${slug}
|
|
1854
|
+
entries: ${entries.length}${staleHint}
|
|
1855
|
+
${action}
|
|
1856
|
+
|
|
1857
|
+
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
1858
|
+
})
|
|
1859
|
+
};
|
|
2360
1860
|
}
|
|
2361
1861
|
|
|
2362
1862
|
// src/commands/stale.ts
|
|
2363
|
-
import { readdir as readdir2, rename
|
|
2364
|
-
import { join as
|
|
1863
|
+
import { readdir as readdir2, rename, mkdir as mkdir3, readFile as readFile8 } from "fs/promises";
|
|
1864
|
+
import { join as join12 } from "path";
|
|
2365
1865
|
|
|
2366
1866
|
// src/parsers/expiry-annotations.ts
|
|
2367
1867
|
var HEADING_RE = /^#{1,6}\s+(.+)$/;
|
|
@@ -2398,8 +1898,8 @@ function parseExpiryAnnotations(content, pagePath) {
|
|
|
2398
1898
|
}
|
|
2399
1899
|
|
|
2400
1900
|
// src/commands/stale.ts
|
|
2401
|
-
function daysSince(
|
|
2402
|
-
return Math.floor((Date.now() - Date.parse(
|
|
1901
|
+
function daysSince(isoDate) {
|
|
1902
|
+
return Math.floor((Date.now() - Date.parse(isoDate)) / 864e5);
|
|
2403
1903
|
}
|
|
2404
1904
|
async function runStale(input) {
|
|
2405
1905
|
const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
|
|
@@ -2410,7 +1910,7 @@ async function runStale(input) {
|
|
|
2410
1910
|
const archived = [];
|
|
2411
1911
|
const workDirs = /* @__PURE__ */ new Map();
|
|
2412
1912
|
const workDirsBySlug = /* @__PURE__ */ new Map();
|
|
2413
|
-
const projectsDir =
|
|
1913
|
+
const projectsDir = join12(input.vault, "projects");
|
|
2414
1914
|
let projectSlugs = [];
|
|
2415
1915
|
try {
|
|
2416
1916
|
projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
@@ -2423,7 +1923,7 @@ async function runStale(input) {
|
|
|
2423
1923
|
projectSlugs = [input.project];
|
|
2424
1924
|
}
|
|
2425
1925
|
for (const slug of projectSlugs) {
|
|
2426
|
-
const workPath =
|
|
1926
|
+
const workPath = join12(projectsDir, slug, "work");
|
|
2427
1927
|
let entries;
|
|
2428
1928
|
try {
|
|
2429
1929
|
entries = await readdir2(workPath, { withFileTypes: true });
|
|
@@ -2434,7 +1934,7 @@ async function runStale(input) {
|
|
|
2434
1934
|
for (const e of entries) {
|
|
2435
1935
|
if (!e.isDirectory()) continue;
|
|
2436
1936
|
const relDir = `projects/${slug}/work/${e.name}`;
|
|
2437
|
-
const absDir =
|
|
1937
|
+
const absDir = join12(workPath, e.name);
|
|
2438
1938
|
let status = "";
|
|
2439
1939
|
let files;
|
|
2440
1940
|
try {
|
|
@@ -2447,7 +1947,7 @@ async function runStale(input) {
|
|
|
2447
1947
|
for (const f of files) {
|
|
2448
1948
|
if (!f.endsWith(".md")) continue;
|
|
2449
1949
|
try {
|
|
2450
|
-
const fm = extractFrontmatter(await
|
|
1950
|
+
const fm = extractFrontmatter(await readFile8(join12(absDir, f), "utf8"));
|
|
2451
1951
|
if (fm.ok && typeof fm.data.status === "string") {
|
|
2452
1952
|
status = fm.data.status;
|
|
2453
1953
|
break;
|
|
@@ -2478,9 +1978,9 @@ async function runStale(input) {
|
|
|
2478
1978
|
if (input.project && !project.includes(input.project)) return null;
|
|
2479
1979
|
let inferred = false;
|
|
2480
1980
|
if (input.forceScan && !kind) {
|
|
2481
|
-
const
|
|
2482
|
-
if (!LOOP_CYCLE_PATTERN.test(
|
|
2483
|
-
const m =
|
|
1981
|
+
const basename3 = t.relPath.split("/").pop();
|
|
1982
|
+
if (!LOOP_CYCLE_PATTERN.test(basename3)) {
|
|
1983
|
+
const m = basename3.match(KIND_FROM_FILENAME);
|
|
2484
1984
|
if (m) {
|
|
2485
1985
|
kind = m[1];
|
|
2486
1986
|
inferred = true;
|
|
@@ -2491,9 +1991,9 @@ async function runStale(input) {
|
|
|
2491
1991
|
const bodyStart = content.indexOf("---", 4);
|
|
2492
1992
|
if (bodyStart > 0) {
|
|
2493
1993
|
const body = content.slice(bodyStart);
|
|
2494
|
-
const
|
|
2495
|
-
if (
|
|
2496
|
-
const candidate =
|
|
1994
|
+
const wikilink = body.match(/\[\[([a-z0-9-]+)\]\]/);
|
|
1995
|
+
if (wikilink) {
|
|
1996
|
+
const candidate = wikilink[1];
|
|
2497
1997
|
if (workDirsBySlug.has(candidate)) {
|
|
2498
1998
|
project = `[[${candidate}]]`;
|
|
2499
1999
|
inferred = true;
|
|
@@ -2543,9 +2043,9 @@ async function runStale(input) {
|
|
|
2543
2043
|
}
|
|
2544
2044
|
}
|
|
2545
2045
|
await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
|
|
2546
|
-
const specPath =
|
|
2046
|
+
const specPath = join12(input.vault, relDir, "spec.md");
|
|
2547
2047
|
try {
|
|
2548
|
-
const specContent = await
|
|
2048
|
+
const specContent = await readFile8(specPath, "utf8");
|
|
2549
2049
|
const specFm = extractFrontmatter(specContent);
|
|
2550
2050
|
if (specFm.ok && typeof specFm.data.source === "string") {
|
|
2551
2051
|
const sourcePath = specFm.data.source;
|
|
@@ -2574,7 +2074,7 @@ async function runStale(input) {
|
|
|
2574
2074
|
if (daysSince(dateStr) < input.days) continue;
|
|
2575
2075
|
let files;
|
|
2576
2076
|
try {
|
|
2577
|
-
files = await readdir2(
|
|
2077
|
+
files = await readdir2(join12(input.vault, relDir));
|
|
2578
2078
|
} catch {
|
|
2579
2079
|
continue;
|
|
2580
2080
|
}
|
|
@@ -2644,7 +2144,7 @@ async function runStale(input) {
|
|
|
2644
2144
|
staleSections.push(...staleSectionResults.flat());
|
|
2645
2145
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2646
2146
|
if (input.archive) {
|
|
2647
|
-
const archiveDir =
|
|
2147
|
+
const archiveDir = join12(input.vault, "_archive", today);
|
|
2648
2148
|
await mkdir3(archiveDir, { recursive: true });
|
|
2649
2149
|
const citedRawPaths = /* @__PURE__ */ new Set();
|
|
2650
2150
|
for (const page of scan.typedKnowledge) {
|
|
@@ -2660,9 +2160,9 @@ async function runStale(input) {
|
|
|
2660
2160
|
}
|
|
2661
2161
|
for (const t of staleTranscripts) {
|
|
2662
2162
|
if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
|
|
2663
|
-
const dest =
|
|
2163
|
+
const dest = join12(archiveDir, t.path.split("/").pop());
|
|
2664
2164
|
try {
|
|
2665
|
-
await
|
|
2165
|
+
await rename(join12(input.vault, t.path), dest);
|
|
2666
2166
|
archived.push(t.path);
|
|
2667
2167
|
} catch {
|
|
2668
2168
|
}
|
|
@@ -2672,18 +2172,18 @@ async function runStale(input) {
|
|
|
2672
2172
|
if (parts.length >= 4 && parts[0] === "projects") {
|
|
2673
2173
|
const slug = parts[1];
|
|
2674
2174
|
const itemName = parts[3];
|
|
2675
|
-
const histDir =
|
|
2175
|
+
const histDir = join12(input.vault, "projects", slug, "history", "archived-work");
|
|
2676
2176
|
await mkdir3(histDir, { recursive: true });
|
|
2677
|
-
const dest =
|
|
2177
|
+
const dest = join12(histDir, itemName);
|
|
2678
2178
|
try {
|
|
2679
|
-
await
|
|
2179
|
+
await rename(join12(input.vault, w.path), dest);
|
|
2680
2180
|
archived.push(w.path);
|
|
2681
2181
|
} catch {
|
|
2682
2182
|
}
|
|
2683
2183
|
} else {
|
|
2684
|
-
const dest =
|
|
2184
|
+
const dest = join12(archiveDir, w.path.replace(/\//g, "_"));
|
|
2685
2185
|
try {
|
|
2686
|
-
await
|
|
2186
|
+
await rename(join12(input.vault, w.path), dest);
|
|
2687
2187
|
archived.push(w.path);
|
|
2688
2188
|
} catch {
|
|
2689
2189
|
}
|
|
@@ -2738,19 +2238,19 @@ async function runPagesize(input) {
|
|
|
2738
2238
|
}
|
|
2739
2239
|
|
|
2740
2240
|
// src/commands/log-rotate.ts
|
|
2741
|
-
import { readFile as
|
|
2742
|
-
import { join as
|
|
2241
|
+
import { readFile as readFile9, rename as rename2, writeFile as writeFile2, stat as stat4 } from "fs/promises";
|
|
2242
|
+
import { join as join13 } from "path";
|
|
2743
2243
|
var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
2744
2244
|
async function runLogRotate(input) {
|
|
2745
2245
|
try {
|
|
2746
|
-
await
|
|
2246
|
+
await stat4(join13(input.vault, "SCHEMA.md"));
|
|
2747
2247
|
} catch {
|
|
2748
2248
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
2749
2249
|
}
|
|
2750
|
-
const logPath =
|
|
2250
|
+
const logPath = join13(input.vault, "log.md");
|
|
2751
2251
|
let logText;
|
|
2752
2252
|
try {
|
|
2753
|
-
logText = await
|
|
2253
|
+
logText = await readFile9(logPath, "utf8");
|
|
2754
2254
|
} catch {
|
|
2755
2255
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2756
2256
|
}
|
|
@@ -2767,9 +2267,9 @@ async function runLogRotate(input) {
|
|
|
2767
2267
|
}
|
|
2768
2268
|
const newestYear = matches[matches.length - 1][1];
|
|
2769
2269
|
const rotatedName = `log-${newestYear}.md`;
|
|
2770
|
-
const rotatedPath =
|
|
2270
|
+
const rotatedPath = join13(input.vault, rotatedName);
|
|
2771
2271
|
try {
|
|
2772
|
-
await
|
|
2272
|
+
await rename2(logPath, rotatedPath);
|
|
2773
2273
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2774
2274
|
const fresh = `# Vault Log
|
|
2775
2275
|
|
|
@@ -2779,7 +2279,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
2779
2279
|
|
|
2780
2280
|
- Previous log moved to ${rotatedName}
|
|
2781
2281
|
`;
|
|
2782
|
-
await
|
|
2282
|
+
await writeFile2(logPath, fresh, "utf8");
|
|
2783
2283
|
} catch (e) {
|
|
2784
2284
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
2785
2285
|
}
|
|
@@ -2812,13 +2312,13 @@ async function runTopicMapCheck(input) {
|
|
|
2812
2312
|
}
|
|
2813
2313
|
|
|
2814
2314
|
// src/commands/index-link-format.ts
|
|
2815
|
-
import { readFile as
|
|
2816
|
-
import { join as
|
|
2315
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2316
|
+
import { join as join14 } from "path";
|
|
2817
2317
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
2818
2318
|
async function runIndexLinkFormat(input) {
|
|
2819
2319
|
let text = "";
|
|
2820
2320
|
try {
|
|
2821
|
-
text = await
|
|
2321
|
+
text = await readFile10(join14(input.vault, "index.md"), "utf8");
|
|
2822
2322
|
} catch {
|
|
2823
2323
|
}
|
|
2824
2324
|
const markdown_links = [];
|
|
@@ -2831,9 +2331,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
2831
2331
|
}
|
|
2832
2332
|
|
|
2833
2333
|
// src/commands/dedup.ts
|
|
2834
|
-
import { createHash as
|
|
2835
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
2836
|
-
import { dirname as
|
|
2334
|
+
import { createHash as createHash3 } from "crypto";
|
|
2335
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
2336
|
+
import { dirname as dirname4, join as join15, resolve as resolve3 } from "path";
|
|
2837
2337
|
|
|
2838
2338
|
// src/utils/rclone.ts
|
|
2839
2339
|
import { execFile } from "child_process";
|
|
@@ -2925,7 +2425,7 @@ async function runDedup(input) {
|
|
|
2925
2425
|
const manifest = safeEntries.length > 0 ? {
|
|
2926
2426
|
version: 1,
|
|
2927
2427
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2928
|
-
vault:
|
|
2428
|
+
vault: resolve3(input.vault),
|
|
2929
2429
|
entries: safeEntries
|
|
2930
2430
|
} : void 0;
|
|
2931
2431
|
const remote = await planAndMaybePruneRemote(input, safeEntries);
|
|
@@ -2937,7 +2437,7 @@ async function runDedup(input) {
|
|
|
2937
2437
|
}
|
|
2938
2438
|
if (input.manifestOut && manifest) {
|
|
2939
2439
|
try {
|
|
2940
|
-
mkdirSync4(
|
|
2440
|
+
mkdirSync4(dirname4(input.manifestOut), { recursive: true });
|
|
2941
2441
|
writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
|
|
2942
2442
|
`, "utf-8");
|
|
2943
2443
|
} catch (e) {
|
|
@@ -2952,7 +2452,7 @@ async function runDedup(input) {
|
|
|
2952
2452
|
}
|
|
2953
2453
|
}
|
|
2954
2454
|
for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
|
|
2955
|
-
const text =
|
|
2455
|
+
const text = readFileSync4(join15(input.vault, page.relPath), "utf-8");
|
|
2956
2456
|
let updated = text;
|
|
2957
2457
|
let changed = false;
|
|
2958
2458
|
for (const [oldPath, newPath] of replacements) {
|
|
@@ -2970,12 +2470,12 @@ async function runDedup(input) {
|
|
|
2970
2470
|
}
|
|
2971
2471
|
}
|
|
2972
2472
|
if (changed) {
|
|
2973
|
-
writeFileSync4(
|
|
2473
|
+
writeFileSync4(join15(input.vault, page.relPath), updated);
|
|
2974
2474
|
rewired.push(page.relPath);
|
|
2975
2475
|
}
|
|
2976
2476
|
}
|
|
2977
2477
|
for (const oldPath of replacements.keys()) {
|
|
2978
|
-
const fullPath =
|
|
2478
|
+
const fullPath = join15(input.vault, oldPath);
|
|
2979
2479
|
try {
|
|
2980
2480
|
unlinkSync4(fullPath);
|
|
2981
2481
|
removed.push(oldPath);
|
|
@@ -3069,14 +2569,14 @@ function buildSafeEntries(vault, duplicates, unsafe) {
|
|
|
3069
2569
|
return entries;
|
|
3070
2570
|
}
|
|
3071
2571
|
function hashRawBody(vault, relPath) {
|
|
3072
|
-
const text =
|
|
2572
|
+
const text = readFileSync4(join15(vault, relPath), "utf-8");
|
|
3073
2573
|
const split = splitFrontmatter(text);
|
|
3074
2574
|
const body = split.ok ? split.data.body : text;
|
|
3075
|
-
return
|
|
2575
|
+
return createHash3("sha256").update(body).digest("hex");
|
|
3076
2576
|
}
|
|
3077
2577
|
function readManifest(path) {
|
|
3078
2578
|
try {
|
|
3079
|
-
const parsed = JSON.parse(
|
|
2579
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
3080
2580
|
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
3081
2581
|
return err("INVALID_FRONTMATTER", { message: "dedup manifest must have version 1 and entries[]" });
|
|
3082
2582
|
}
|
|
@@ -3092,7 +2592,7 @@ async function planAndMaybePruneRemote(input, entries) {
|
|
|
3092
2592
|
}
|
|
3093
2593
|
|
|
3094
2594
|
// src/utils/safe-write.ts
|
|
3095
|
-
import { readFile as
|
|
2595
|
+
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
3096
2596
|
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
3097
2597
|
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
3098
2598
|
function bodyBytes(text) {
|
|
@@ -3102,7 +2602,7 @@ function bodyBytes(text) {
|
|
|
3102
2602
|
}
|
|
3103
2603
|
async function readIfExists(absPath) {
|
|
3104
2604
|
try {
|
|
3105
|
-
return await
|
|
2605
|
+
return await readFile11(absPath, "utf8");
|
|
3106
2606
|
} catch (e) {
|
|
3107
2607
|
if (e.code === "ENOENT") return null;
|
|
3108
2608
|
throw e;
|
|
@@ -3225,10 +2725,10 @@ ${newBody}`;
|
|
|
3225
2725
|
}
|
|
3226
2726
|
|
|
3227
2727
|
// src/commands/lint.ts
|
|
3228
|
-
import { existsSync as
|
|
3229
|
-
import { readFile as
|
|
3230
|
-
import { createHash as
|
|
3231
|
-
import { join as
|
|
2728
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2729
|
+
import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
|
|
2730
|
+
import { createHash as createHash5 } from "crypto";
|
|
2731
|
+
import { join as join17, relative as relative2, sep as sep2 } from "path";
|
|
3232
2732
|
|
|
3233
2733
|
// src/commands/sparse-community.ts
|
|
3234
2734
|
async function runSparseCommunity(input) {
|
|
@@ -3244,7 +2744,7 @@ async function runSparseCommunity(input) {
|
|
|
3244
2744
|
}
|
|
3245
2745
|
|
|
3246
2746
|
// src/commands/raw-body-dedup.ts
|
|
3247
|
-
import { createHash as
|
|
2747
|
+
import { createHash as createHash4 } from "crypto";
|
|
3248
2748
|
async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
3249
2749
|
const scanResult = scan ? ok(scan) : await scanVault(vault);
|
|
3250
2750
|
if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
|
|
@@ -3253,7 +2753,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
3253
2753
|
const text = await readPageCached(raw, pageTextCache);
|
|
3254
2754
|
const split = splitFrontmatter(text);
|
|
3255
2755
|
if (!split.ok) return null;
|
|
3256
|
-
const bodyHash =
|
|
2756
|
+
const bodyHash = createHash4("sha256").update(split.data.body).digest("hex");
|
|
3257
2757
|
const fm = extractFrontmatter(text);
|
|
3258
2758
|
let fmSha256 = null;
|
|
3259
2759
|
if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
|
|
@@ -3285,9 +2785,9 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
|
|
|
3285
2785
|
}
|
|
3286
2786
|
|
|
3287
2787
|
// src/commands/path-too-long.ts
|
|
3288
|
-
import { existsSync as
|
|
3289
|
-
import { mkdir as mkdir4, readFile as
|
|
3290
|
-
import { dirname as
|
|
2788
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2789
|
+
import { mkdir as mkdir4, readFile as readFile12, rename as rename3, unlink } from "fs/promises";
|
|
2790
|
+
import { dirname as dirname5, join as join16, posix, resolve as resolve4 } from "path";
|
|
3291
2791
|
var MAX_PATH_LENGTH = 240;
|
|
3292
2792
|
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
3293
2793
|
async function runPathTooLong(input) {
|
|
@@ -3320,10 +2820,10 @@ async function fixPathTooLong(input) {
|
|
|
3320
2820
|
}
|
|
3321
2821
|
try {
|
|
3322
2822
|
if (target.mode === "dedupe") {
|
|
3323
|
-
await
|
|
2823
|
+
await unlink(join16(input.vault, violation.relPath));
|
|
3324
2824
|
} else {
|
|
3325
|
-
await mkdir4(
|
|
3326
|
-
await
|
|
2825
|
+
await mkdir4(dirname5(join16(input.vault, target.relPath)), { recursive: true });
|
|
2826
|
+
await rename3(join16(input.vault, violation.relPath), join16(input.vault, target.relPath));
|
|
3327
2827
|
}
|
|
3328
2828
|
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
3329
2829
|
} catch {
|
|
@@ -3337,7 +2837,7 @@ async function fixPathTooLong(input) {
|
|
|
3337
2837
|
for (const page of afterScan.data.allMarkdown) {
|
|
3338
2838
|
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
3339
2839
|
try {
|
|
3340
|
-
const original = await
|
|
2840
|
+
const original = await readFile12(page.absPath, "utf8");
|
|
3341
2841
|
let updated = original;
|
|
3342
2842
|
for (const fix of fixed) {
|
|
3343
2843
|
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
@@ -3374,7 +2874,7 @@ function findPathTooLongViolations(pages, maxLength) {
|
|
|
3374
2874
|
}
|
|
3375
2875
|
function maxFixPathLength(vault) {
|
|
3376
2876
|
if (process.platform !== "win32") return MAX_PATH_LENGTH;
|
|
3377
|
-
const root =
|
|
2877
|
+
const root = resolve4(vault);
|
|
3378
2878
|
const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
|
|
3379
2879
|
const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
|
|
3380
2880
|
return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
|
|
@@ -3400,9 +2900,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
3400
2900
|
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
3401
2901
|
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
3402
2902
|
if (candidate === original || candidate.length > maxLength) continue;
|
|
3403
|
-
const candidatePath =
|
|
3404
|
-
if (!
|
|
3405
|
-
if (await hasSameContent(
|
|
2903
|
+
const candidatePath = join16(vault, candidate);
|
|
2904
|
+
if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
2905
|
+
if (await hasSameContent(join16(vault, original), candidatePath)) {
|
|
3406
2906
|
return { relPath: candidate, mode: "dedupe" };
|
|
3407
2907
|
}
|
|
3408
2908
|
}
|
|
@@ -3411,8 +2911,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
|
3411
2911
|
function candidateRelPaths(preferred, maxLength) {
|
|
3412
2912
|
const candidates = [preferred];
|
|
3413
2913
|
if (preferred.length > maxLength) return candidates;
|
|
3414
|
-
const dir =
|
|
3415
|
-
const filename =
|
|
2914
|
+
const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
|
|
2915
|
+
const filename = posix.basename(preferred);
|
|
3416
2916
|
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
3417
2917
|
const base = ext ? filename.slice(0, -3) : filename;
|
|
3418
2918
|
const dirPrefix = dir ? `${dir}/` : "";
|
|
@@ -3426,7 +2926,7 @@ function candidateRelPaths(preferred, maxLength) {
|
|
|
3426
2926
|
}
|
|
3427
2927
|
async function hasSameContent(a, b) {
|
|
3428
2928
|
try {
|
|
3429
|
-
const [left, right] = await Promise.all([
|
|
2929
|
+
const [left, right] = await Promise.all([readFile12(a), readFile12(b)]);
|
|
3430
2930
|
return left.equals(right);
|
|
3431
2931
|
} catch {
|
|
3432
2932
|
return false;
|
|
@@ -3439,8 +2939,8 @@ function shouldRewriteReferences(relPath) {
|
|
|
3439
2939
|
}
|
|
3440
2940
|
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
3441
2941
|
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
3442
|
-
const oldStem =
|
|
3443
|
-
const newStem =
|
|
2942
|
+
const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
|
|
2943
|
+
const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
|
|
3444
2944
|
if (oldStem !== newStem) {
|
|
3445
2945
|
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3446
2946
|
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
@@ -3534,6 +3034,10 @@ function buildCliSurface() {
|
|
|
3534
3034
|
syncCmd.command("status").option("--wiki <name>").option("--include-stashes").option("--include-remote-health").option("--check-snapshotter");
|
|
3535
3035
|
syncCmd.command("push").option("--wiki <name>");
|
|
3536
3036
|
syncCmd.command("pull").option("--wiki <name>");
|
|
3037
|
+
syncCmd.command("resolve-derived").option("--operation-id <id>").option("--wiki <name>");
|
|
3038
|
+
const syncJournalCmd = syncCmd.command("journal");
|
|
3039
|
+
syncJournalCmd.command("list").option("--wiki <name>");
|
|
3040
|
+
syncJournalCmd.command("clear-stale").option("--dry-run").option("--wiki <name>");
|
|
3537
3041
|
syncCmd.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
|
|
3538
3042
|
syncCmd.command("unlock").option("--force").option("--wiki <name>");
|
|
3539
3043
|
syncCmd.command("peers").option("--wiki <name>");
|
|
@@ -3553,16 +3057,16 @@ function buildCliSurface() {
|
|
|
3553
3057
|
fleetCmd.command("health").option("--file <path>").option("--host-id <id>").option("--json");
|
|
3554
3058
|
const surface = /* @__PURE__ */ new Map();
|
|
3555
3059
|
const rootFlags = new Set(program.options.map((o) => o.long ?? o.short).filter((f) => f != null));
|
|
3556
|
-
function
|
|
3060
|
+
function walk(cmd, prefix, parentFlags) {
|
|
3557
3061
|
const key = prefix ? `${prefix}.${cmd.name()}` : cmd.name();
|
|
3558
3062
|
const flags = /* @__PURE__ */ new Set([...parentFlags, ...cmd.options.map((o) => o.long ?? o.short).filter((f) => f != null)]);
|
|
3559
3063
|
surface.set(key, flags);
|
|
3560
3064
|
for (const sub of cmd.commands) {
|
|
3561
|
-
|
|
3065
|
+
walk(sub, key, flags);
|
|
3562
3066
|
}
|
|
3563
3067
|
}
|
|
3564
3068
|
for (const cmd of program.commands) {
|
|
3565
|
-
|
|
3069
|
+
walk(cmd, "", rootFlags);
|
|
3566
3070
|
}
|
|
3567
3071
|
return surface;
|
|
3568
3072
|
}
|
|
@@ -3838,7 +3342,7 @@ function recomputeRawSha256IfPresent(content) {
|
|
|
3838
3342
|
const split = splitFrontmatter(content);
|
|
3839
3343
|
if (!split.ok) return content;
|
|
3840
3344
|
if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
|
|
3841
|
-
const sha256 =
|
|
3345
|
+
const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
|
|
3842
3346
|
const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
|
|
3843
3347
|
return `---
|
|
3844
3348
|
${rawFrontmatter}
|
|
@@ -3886,24 +3390,24 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
|
3886
3390
|
const entries = await readdir3(absDir, { withFileTypes: true });
|
|
3887
3391
|
const pages = [];
|
|
3888
3392
|
for (const entry of entries) {
|
|
3889
|
-
const absPath =
|
|
3393
|
+
const absPath = join17(absDir, entry.name);
|
|
3890
3394
|
if (entry.isDirectory()) {
|
|
3891
3395
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3892
3396
|
pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
|
|
3893
3397
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3894
|
-
pages.push({ absPath, relPath:
|
|
3398
|
+
pages.push({ absPath, relPath: relative2(vaultRoot, absPath).split(sep2).join("/") });
|
|
3895
3399
|
}
|
|
3896
3400
|
}
|
|
3897
3401
|
return pages;
|
|
3898
3402
|
}
|
|
3899
3403
|
async function collectCliRefsPages(vault) {
|
|
3900
|
-
if (!
|
|
3404
|
+
if (!existsSync6(join17(vault, "SCHEMA.md"))) {
|
|
3901
3405
|
return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
|
|
3902
3406
|
}
|
|
3903
3407
|
const pages = [];
|
|
3904
3408
|
for (const dir of CLI_REFS_TYPED_DIRS) {
|
|
3905
|
-
const absDir =
|
|
3906
|
-
if (!
|
|
3409
|
+
const absDir = join17(vault, dir);
|
|
3410
|
+
if (!existsSync6(absDir)) continue;
|
|
3907
3411
|
pages.push(...await walkMarkdownFiles(absDir, vault));
|
|
3908
3412
|
}
|
|
3909
3413
|
return ok(pages);
|
|
@@ -4024,7 +3528,7 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
|
|
|
4024
3528
|
for (const relPath of fileSourceUrlFrontmatterFlags) {
|
|
4025
3529
|
try {
|
|
4026
3530
|
const absPath = `${input.vault}/${relPath}`;
|
|
4027
|
-
const raw = await
|
|
3531
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4028
3532
|
const parts = raw.split("---", 3);
|
|
4029
3533
|
if (parts.length < 3) {
|
|
4030
3534
|
unresolved.push(relPath);
|
|
@@ -4417,8 +3921,8 @@ async function runLint(input) {
|
|
|
4417
3921
|
const readKnowledgeContent = (slug) => {
|
|
4418
3922
|
const existing = knowledgeContentCache.get(slug);
|
|
4419
3923
|
if (existing) return existing;
|
|
4420
|
-
const knowledgePath =
|
|
4421
|
-
const pending =
|
|
3924
|
+
const knowledgePath = join17(lintVault, "projects", slug, "knowledge.md");
|
|
3925
|
+
const pending = existsSync6(knowledgePath) ? readFile13(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
|
|
4422
3926
|
knowledgeContentCache.set(slug, pending);
|
|
4423
3927
|
return pending;
|
|
4424
3928
|
};
|
|
@@ -4524,7 +4028,7 @@ async function runLint(input) {
|
|
|
4524
4028
|
for (const relPath of legacyPages) {
|
|
4525
4029
|
try {
|
|
4526
4030
|
const absPath = `${input.vault}/${relPath}`;
|
|
4527
|
-
const raw = await
|
|
4031
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4528
4032
|
const split = splitFrontmatter(raw);
|
|
4529
4033
|
if (!split.ok) {
|
|
4530
4034
|
unresolved.push(relPath);
|
|
@@ -4623,7 +4127,7 @@ ${newBody}`;
|
|
|
4623
4127
|
for (const relPath of noOverview) {
|
|
4624
4128
|
try {
|
|
4625
4129
|
const absPath = `${input.vault}/${relPath}`;
|
|
4626
|
-
const raw = await
|
|
4130
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4627
4131
|
const split = splitFrontmatter(raw);
|
|
4628
4132
|
if (!split.ok) {
|
|
4629
4133
|
unresolved.push(relPath);
|
|
@@ -4664,7 +4168,7 @@ ${trimmedBody}`;
|
|
|
4664
4168
|
for (const relPath of missingTldrFlags) {
|
|
4665
4169
|
try {
|
|
4666
4170
|
const absPath = `${input.vault}/${relPath}`;
|
|
4667
|
-
const raw = await
|
|
4171
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4668
4172
|
const split = splitFrontmatter(raw);
|
|
4669
4173
|
if (!split.ok) {
|
|
4670
4174
|
unresolved.push(relPath);
|
|
@@ -4714,7 +4218,7 @@ ${lines.join("\n")}`;
|
|
|
4714
4218
|
for (const relPath of wikilinkCitationFlags) {
|
|
4715
4219
|
try {
|
|
4716
4220
|
const absPath = `${input.vault}/${relPath}`;
|
|
4717
|
-
const raw = await
|
|
4221
|
+
const raw = await readFile13(absPath, "utf8");
|
|
4718
4222
|
const split = splitFrontmatter(raw);
|
|
4719
4223
|
if (!split.ok) {
|
|
4720
4224
|
unresolved.push(relPath);
|
|
@@ -5055,14 +4559,14 @@ async function runSyncLintDelta(input) {
|
|
|
5055
4559
|
}
|
|
5056
4560
|
|
|
5057
4561
|
// src/commands/config.ts
|
|
5058
|
-
import { readFile as
|
|
5059
|
-
import { existsSync as
|
|
5060
|
-
import { join as
|
|
4562
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4563
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4564
|
+
import { join as join18 } from "path";
|
|
5061
4565
|
function validateKey(key) {
|
|
5062
4566
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
5063
4567
|
}
|
|
5064
4568
|
function configPath(home) {
|
|
5065
|
-
return
|
|
4569
|
+
return join18(home, ".skillwiki", ".env");
|
|
5066
4570
|
}
|
|
5067
4571
|
async function runConfigGet(input) {
|
|
5068
4572
|
if (!validateKey(input.key)) {
|
|
@@ -5080,7 +4584,7 @@ async function runConfigSet(input) {
|
|
|
5080
4584
|
try {
|
|
5081
4585
|
let originalContent;
|
|
5082
4586
|
try {
|
|
5083
|
-
originalContent = await
|
|
4587
|
+
originalContent = await readFile14(filePath, "utf8");
|
|
5084
4588
|
} catch {
|
|
5085
4589
|
}
|
|
5086
4590
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -5097,501 +4601,39 @@ async function runConfigList(input) {
|
|
|
5097
4601
|
let profiles;
|
|
5098
4602
|
if (input.profiles) {
|
|
5099
4603
|
const defaultProfile = map["WIKI_DEFAULT"];
|
|
5100
|
-
profiles = [];
|
|
5101
|
-
for (const key of Object.keys(map)) {
|
|
5102
|
-
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
5103
|
-
if (m && key !== "WIKI_PATH") {
|
|
5104
|
-
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
5105
|
-
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
5106
|
-
}
|
|
5107
|
-
}
|
|
5108
|
-
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
5109
|
-
}
|
|
5110
|
-
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");
|
|
5111
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
5112
|
-
}
|
|
5113
|
-
async function runConfigPath(input) {
|
|
5114
|
-
const filePath = configPath(input.home);
|
|
5115
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync8(filePath), humanHint: filePath }) };
|
|
5116
|
-
}
|
|
5117
|
-
|
|
5118
|
-
// src/commands/fleet.ts
|
|
5119
|
-
import { readFile as readFile17 } from "fs/promises";
|
|
5120
|
-
import { hostname as nodeHostname, userInfo } from "os";
|
|
5121
|
-
import { join as join20 } from "path";
|
|
5122
|
-
import yaml3 from "js-yaml";
|
|
5123
|
-
var FLEET_REL_PATH = join20("projects", "llm-wiki", "architecture", "fleet.yaml");
|
|
5124
|
-
async function runFleetValidate(input) {
|
|
5125
|
-
const loaded = await loadFleetManifest(input.file);
|
|
5126
|
-
if (!loaded.ok) {
|
|
5127
|
-
if (loaded.error === "FILE_NOT_FOUND") {
|
|
5128
|
-
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
5129
|
-
}
|
|
5130
|
-
const errors = fleetLoadErrors(loaded);
|
|
5131
|
-
return invalidFleet(errors);
|
|
5132
|
-
}
|
|
5133
|
-
const warnings = fleetWarnings(loaded.manifest);
|
|
5134
|
-
const snapshotter = findSnapshotter(loaded.manifest);
|
|
5135
|
-
return {
|
|
5136
|
-
exitCode: ExitCode.OK,
|
|
5137
|
-
result: ok({
|
|
5138
|
-
valid: true,
|
|
5139
|
-
errors: [],
|
|
5140
|
-
warnings,
|
|
5141
|
-
host_count: Object.keys(loaded.manifest.hosts).length,
|
|
5142
|
-
snapshotter,
|
|
5143
|
-
humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
|
|
5144
|
-
})
|
|
5145
|
-
};
|
|
5146
|
-
}
|
|
5147
|
-
async function runFleetContext(input) {
|
|
5148
|
-
const env = input.env ?? process.env;
|
|
5149
|
-
const home = input.home ?? env.HOME ?? "";
|
|
5150
|
-
const cwd = input.cwd ?? process.cwd();
|
|
5151
|
-
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
5152
|
-
const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
|
|
5153
|
-
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
5154
|
-
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
5155
|
-
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5156
|
-
const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
|
|
5157
|
-
if (!loaded.ok) {
|
|
5158
|
-
const warnings = ["fleet manifest unavailable or invalid"];
|
|
5159
|
-
const markdown2 = formatUnknownContext({
|
|
5160
|
-
generatedAt,
|
|
5161
|
-
osHostname,
|
|
5162
|
-
user,
|
|
5163
|
-
cwd,
|
|
5164
|
-
vault,
|
|
5165
|
-
reason: warnings[0],
|
|
5166
|
-
trace: [],
|
|
5167
|
-
warnings
|
|
5168
|
-
});
|
|
5169
|
-
return {
|
|
5170
|
-
exitCode: ExitCode.OK,
|
|
5171
|
-
result: ok({
|
|
5172
|
-
manifest_loaded: false,
|
|
5173
|
-
generated_at: generatedAt,
|
|
5174
|
-
identity_status: "unknown",
|
|
5175
|
-
resolver_trace: [],
|
|
5176
|
-
warnings,
|
|
5177
|
-
markdown: markdown2,
|
|
5178
|
-
humanHint: markdown2
|
|
5179
|
-
})
|
|
5180
|
-
};
|
|
5181
|
-
}
|
|
5182
|
-
const resolved = await resolveFleetHostId({
|
|
5183
|
-
manifest: loaded.manifest,
|
|
5184
|
-
hostId: input.hostId,
|
|
5185
|
-
env,
|
|
5186
|
-
home,
|
|
5187
|
-
osHostname
|
|
5188
|
-
});
|
|
5189
|
-
if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
|
|
5190
|
-
const source = resolved.source ?? "unknown";
|
|
5191
|
-
const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
|
|
5192
|
-
const markdown2 = formatInvalidContext({
|
|
5193
|
-
generatedAt,
|
|
5194
|
-
hostId: resolved.hostId,
|
|
5195
|
-
source,
|
|
5196
|
-
osHostname,
|
|
5197
|
-
user,
|
|
5198
|
-
cwd,
|
|
5199
|
-
vault,
|
|
5200
|
-
trace: resolved.trace,
|
|
5201
|
-
warnings
|
|
5202
|
-
});
|
|
5203
|
-
return {
|
|
5204
|
-
exitCode: ExitCode.OK,
|
|
5205
|
-
result: ok({
|
|
5206
|
-
manifest_loaded: true,
|
|
5207
|
-
host_id: resolved.hostId,
|
|
5208
|
-
source: resolved.source,
|
|
5209
|
-
generated_at: generatedAt,
|
|
5210
|
-
identity_status: "invalid",
|
|
5211
|
-
resolver_trace: resolved.trace,
|
|
5212
|
-
warnings,
|
|
5213
|
-
markdown: markdown2,
|
|
5214
|
-
humanHint: markdown2
|
|
5215
|
-
})
|
|
5216
|
-
};
|
|
5217
|
-
}
|
|
5218
|
-
if (!resolved.hostId) {
|
|
5219
|
-
const warnings = ["host identity is unresolved"];
|
|
5220
|
-
const markdown2 = formatUnknownContext({
|
|
5221
|
-
generatedAt,
|
|
5222
|
-
osHostname,
|
|
5223
|
-
user,
|
|
5224
|
-
cwd,
|
|
5225
|
-
vault,
|
|
5226
|
-
reason: warnings[0],
|
|
5227
|
-
trace: resolved.trace,
|
|
5228
|
-
warnings
|
|
5229
|
-
});
|
|
5230
|
-
return {
|
|
5231
|
-
exitCode: ExitCode.OK,
|
|
5232
|
-
result: ok({
|
|
5233
|
-
manifest_loaded: true,
|
|
5234
|
-
generated_at: generatedAt,
|
|
5235
|
-
identity_status: "unknown",
|
|
5236
|
-
resolver_trace: resolved.trace,
|
|
5237
|
-
warnings,
|
|
5238
|
-
markdown: markdown2,
|
|
5239
|
-
humanHint: markdown2
|
|
5240
|
-
})
|
|
5241
|
-
};
|
|
5242
|
-
}
|
|
5243
|
-
const markdown = formatKnownContext({
|
|
5244
|
-
manifest: loaded.manifest,
|
|
5245
|
-
hostId: resolved.hostId,
|
|
5246
|
-
source: resolved.source,
|
|
5247
|
-
generatedAt,
|
|
5248
|
-
osHostname,
|
|
5249
|
-
user,
|
|
5250
|
-
cwd,
|
|
5251
|
-
vault,
|
|
5252
|
-
trace: resolved.trace
|
|
5253
|
-
});
|
|
5254
|
-
return {
|
|
5255
|
-
exitCode: ExitCode.OK,
|
|
5256
|
-
result: ok({
|
|
5257
|
-
manifest_loaded: true,
|
|
5258
|
-
host_id: resolved.hostId,
|
|
5259
|
-
source: resolved.source,
|
|
5260
|
-
generated_at: generatedAt,
|
|
5261
|
-
identity_status: "known",
|
|
5262
|
-
resolver_trace: resolved.trace,
|
|
5263
|
-
warnings: [],
|
|
5264
|
-
markdown,
|
|
5265
|
-
humanHint: markdown
|
|
5266
|
-
})
|
|
5267
|
-
};
|
|
5268
|
-
}
|
|
5269
|
-
function fleetContextEnv(input) {
|
|
5270
|
-
const env = input.env ?? process.env;
|
|
5271
|
-
const home = input.home ?? env.HOME ?? "";
|
|
5272
|
-
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
5273
|
-
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
5274
|
-
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
5275
|
-
return { env, home, osHostname, vault, file };
|
|
5276
|
-
}
|
|
5277
|
-
async function loadFleetManifestAndHost(input) {
|
|
5278
|
-
const { env, home, osHostname, file } = fleetContextEnv(input);
|
|
5279
|
-
if (!file) return null;
|
|
5280
|
-
const loaded = await loadFleetManifest(file);
|
|
5281
|
-
if (!loaded.ok) return null;
|
|
5282
|
-
const resolved = await resolveFleetHostId({
|
|
5283
|
-
manifest: loaded.manifest,
|
|
5284
|
-
hostId: input.hostId,
|
|
5285
|
-
env,
|
|
5286
|
-
home,
|
|
5287
|
-
osHostname
|
|
5288
|
-
});
|
|
5289
|
-
if (!resolved.hostId) {
|
|
5290
|
-
return {
|
|
5291
|
-
manifest: loaded.manifest,
|
|
5292
|
-
hostId: void 0,
|
|
5293
|
-
source: resolved.source,
|
|
5294
|
-
warnings: ["host identity is unresolved"],
|
|
5295
|
-
identityStatus: "unknown"
|
|
5296
|
-
};
|
|
5297
|
-
}
|
|
5298
|
-
if (!loaded.manifest.hosts[resolved.hostId]) {
|
|
5299
|
-
const source = resolved.source ?? "unknown";
|
|
5300
|
-
return {
|
|
5301
|
-
manifest: loaded.manifest,
|
|
5302
|
-
hostId: resolved.hostId,
|
|
5303
|
-
source: resolved.source,
|
|
5304
|
-
warnings: [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`],
|
|
5305
|
-
identityStatus: "invalid"
|
|
5306
|
-
};
|
|
5307
|
-
}
|
|
5308
|
-
return {
|
|
5309
|
-
manifest: loaded.manifest,
|
|
5310
|
-
hostId: resolved.hostId,
|
|
5311
|
-
source: resolved.source,
|
|
5312
|
-
warnings: [],
|
|
5313
|
-
identityStatus: "known"
|
|
5314
|
-
};
|
|
5315
|
-
}
|
|
5316
|
-
function snapshotterAliasForLocalHost(fleetLoad) {
|
|
5317
|
-
if (!fleetLoad?.manifest || !fleetLoad.hostId) return void 0;
|
|
5318
|
-
const snapshotterId = Object.entries(fleetLoad.manifest.hosts).find(([, h]) => h.role === "snapshotter")?.[0];
|
|
5319
|
-
if (!snapshotterId) return void 0;
|
|
5320
|
-
const profile = fleetLoad.manifest.hosts[snapshotterId]?.access?.from?.[fleetLoad.hostId];
|
|
5321
|
-
if (!profile || profile.status !== "configured" && profile.status !== "local") return void 0;
|
|
5322
|
-
const aliases = profile.ssh_aliases ?? [];
|
|
5323
|
-
return aliases.length > 0 ? aliases[0] : void 0;
|
|
5324
|
-
}
|
|
5325
|
-
function satelliteGateFromFleetLoad(load) {
|
|
5326
|
-
if (!load?.hostId) return { satelliteExpected: false };
|
|
5327
|
-
const host = load.manifest.hosts[load.hostId];
|
|
5328
|
-
if (!host) return { satelliteExpected: false };
|
|
5329
|
-
return { satelliteExpected: host.maintenance?.skillwiki_satellite?.enabled === true };
|
|
5330
|
-
}
|
|
5331
|
-
async function loadFleetManifest(file) {
|
|
5332
|
-
let text;
|
|
5333
|
-
try {
|
|
5334
|
-
text = await readFile17(file, "utf8");
|
|
5335
|
-
} catch {
|
|
5336
|
-
return { ok: false, error: "FILE_NOT_FOUND" };
|
|
5337
|
-
}
|
|
5338
|
-
let parsed;
|
|
5339
|
-
try {
|
|
5340
|
-
parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
|
|
5341
|
-
} catch (error) {
|
|
5342
|
-
return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
|
|
5343
|
-
}
|
|
5344
|
-
const result = FleetManifestSchema.safeParse(parsed);
|
|
5345
|
-
if (!result.success) {
|
|
5346
|
-
return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
|
|
5347
|
-
}
|
|
5348
|
-
return { ok: true, manifest: result.data };
|
|
5349
|
-
}
|
|
5350
|
-
function invalidFleet(errors) {
|
|
5351
|
-
return {
|
|
5352
|
-
exitCode: ExitCode.FLEET_MANIFEST_INVALID,
|
|
5353
|
-
result: ok({
|
|
5354
|
-
valid: false,
|
|
5355
|
-
errors,
|
|
5356
|
-
warnings: [],
|
|
5357
|
-
host_count: 0,
|
|
5358
|
-
humanHint: `INVALID fleet manifest
|
|
5359
|
-
${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
|
|
5360
|
-
})
|
|
5361
|
-
};
|
|
5362
|
-
}
|
|
5363
|
-
function fleetLoadErrors(loaded) {
|
|
5364
|
-
if (loaded.error === "INVALID_YAML") {
|
|
5365
|
-
return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
|
|
5366
|
-
}
|
|
5367
|
-
if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
|
|
5368
|
-
return loaded.detail.map((issue) => {
|
|
5369
|
-
const zodIssue = issue;
|
|
5370
|
-
return {
|
|
5371
|
-
path: (zodIssue.path ?? []).join("."),
|
|
5372
|
-
message: zodIssue.message ?? "invalid value"
|
|
5373
|
-
};
|
|
5374
|
-
});
|
|
5375
|
-
}
|
|
5376
|
-
return [{ path: "", message: loaded.error }];
|
|
5377
|
-
}
|
|
5378
|
-
function fleetWarnings(manifest) {
|
|
5379
|
-
const warnings = [];
|
|
5380
|
-
for (const [id, host] of Object.entries(manifest.hosts)) {
|
|
5381
|
-
if (host.role === "snapshotter" && host.protected !== true) {
|
|
5382
|
-
warnings.push(`snapshotter host '${id}' is not protected=true`);
|
|
5383
|
-
}
|
|
5384
|
-
}
|
|
5385
|
-
return warnings;
|
|
5386
|
-
}
|
|
5387
|
-
function findSnapshotter(manifest) {
|
|
5388
|
-
return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
|
|
5389
|
-
}
|
|
5390
|
-
async function resolveFleetHostId(input) {
|
|
5391
|
-
const trace = [];
|
|
5392
|
-
if (input.hostId) {
|
|
5393
|
-
trace.push({ source: "--host-id", status: "matched", value: input.hostId });
|
|
5394
|
-
return { hostId: input.hostId, source: "host-id", trace };
|
|
5395
|
-
}
|
|
5396
|
-
trace.push({ source: "--host-id", status: "unset" });
|
|
5397
|
-
if (input.env.SKILLWIKI_HOST_ID) {
|
|
5398
|
-
trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
|
|
5399
|
-
return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
|
|
5400
|
-
}
|
|
5401
|
-
trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
|
|
5402
|
-
if (input.env.AGENT_HOST_ID) {
|
|
5403
|
-
trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
|
|
5404
|
-
return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
|
|
5405
|
-
}
|
|
5406
|
-
trace.push({ source: "AGENT_HOST_ID", status: "unset" });
|
|
5407
|
-
if (input.home) {
|
|
5408
|
-
const dotenv = await parseDotenvFile(join20(input.home, ".skillwiki", ".env"));
|
|
5409
|
-
if (dotenv.SKILLWIKI_HOST_ID) {
|
|
5410
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
|
|
5411
|
-
return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
|
|
5412
|
-
}
|
|
5413
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
|
|
5414
|
-
} else {
|
|
5415
|
-
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
|
|
5416
|
-
}
|
|
5417
|
-
if (input.env.VS_HOSTNAME) {
|
|
5418
|
-
trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
|
|
5419
|
-
return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
|
|
5420
|
-
}
|
|
5421
|
-
trace.push({ source: "VS_HOSTNAME", status: "unset" });
|
|
5422
|
-
const hostname = input.osHostname.trim();
|
|
5423
|
-
if (hostname) {
|
|
5424
|
-
if (input.manifest.hosts[hostname]) {
|
|
5425
|
-
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
5426
|
-
return { hostId: hostname, source: "hostname", trace };
|
|
5427
|
-
}
|
|
5428
|
-
const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
|
|
5429
|
-
if (byHostname) {
|
|
5430
|
-
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
5431
|
-
return { hostId: byHostname[0], source: "hostname", trace };
|
|
5432
|
-
}
|
|
5433
|
-
trace.push({ source: "hostname", status: "unmatched", value: hostname });
|
|
5434
|
-
} else {
|
|
5435
|
-
trace.push({ source: "hostname", status: "unset" });
|
|
5436
|
-
}
|
|
5437
|
-
return { trace };
|
|
5438
|
-
}
|
|
5439
|
-
function formatKnownContext(input) {
|
|
5440
|
-
const host = input.manifest.hosts[input.hostId];
|
|
5441
|
-
const protectedValue = host.protected === true ? "true" : "false";
|
|
5442
|
-
const writesTo = host.writes_to.join(", ");
|
|
5443
|
-
const selfAliases = collectSelfAliases(input.manifest, input.hostId);
|
|
5444
|
-
const outbound = collectOutboundAccess(input.manifest, input.hostId);
|
|
5445
|
-
const maintenanceLines = formatMaintenanceLines(host);
|
|
5446
|
-
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.`;
|
|
5447
|
-
return [
|
|
5448
|
-
"## Runtime Host Context",
|
|
5449
|
-
"",
|
|
5450
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5451
|
-
`- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
|
|
5452
|
-
"- Identity status: `known`",
|
|
5453
|
-
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
5454
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5455
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5456
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5457
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5458
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5459
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5460
|
-
`- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
|
|
5461
|
-
...maintenanceLines,
|
|
5462
|
-
`- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
|
|
5463
|
-
`- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
|
|
5464
|
-
`- Guidance: ${guidance}`
|
|
5465
|
-
].join("\n");
|
|
5466
|
-
}
|
|
5467
|
-
function formatUnknownContext(input) {
|
|
5468
|
-
return [
|
|
5469
|
-
"## Runtime Host Context",
|
|
5470
|
-
"",
|
|
5471
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5472
|
-
"- Current machine: unknown",
|
|
5473
|
-
"- Identity status: `unknown`",
|
|
5474
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5475
|
-
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
5476
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5477
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5478
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5479
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5480
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5481
|
-
"- Fleet role: unknown",
|
|
5482
|
-
"- Self SSH aliases known in fleet: unknown",
|
|
5483
|
-
"- Declared outbound SSH from this source: unknown",
|
|
5484
|
-
`- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
|
|
5485
|
-
].join("\n");
|
|
5486
|
-
}
|
|
5487
|
-
function formatInvalidContext(input) {
|
|
5488
|
-
return [
|
|
5489
|
-
"## Runtime Host Context",
|
|
5490
|
-
"",
|
|
5491
|
-
`- Context generated: \`${input.generatedAt}\``,
|
|
5492
|
-
"- Current machine: unknown",
|
|
5493
|
-
"- Identity status: `invalid`",
|
|
5494
|
-
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
5495
|
-
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
5496
|
-
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
5497
|
-
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
5498
|
-
`- User: ${formatMaybe(input.user)}`,
|
|
5499
|
-
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
5500
|
-
`- Vault: ${formatMaybe(input.vault)}`,
|
|
5501
|
-
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
5502
|
-
"- Fleet role: unknown",
|
|
5503
|
-
"- Self SSH aliases known in fleet: unknown",
|
|
5504
|
-
"- Declared outbound SSH from this source: unknown",
|
|
5505
|
-
`- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
|
|
5506
|
-
].join("\n");
|
|
5507
|
-
}
|
|
5508
|
-
function collectSelfAliases(manifest, hostId2) {
|
|
5509
|
-
const aliases = [];
|
|
5510
|
-
const host = manifest.hosts[hostId2];
|
|
5511
|
-
const access = host?.access?.from ?? {};
|
|
5512
|
-
for (const profile of Object.values(access)) {
|
|
5513
|
-
for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
|
|
5514
|
-
}
|
|
5515
|
-
return [...new Set(aliases)];
|
|
5516
|
-
}
|
|
5517
|
-
function collectOutboundAccess(manifest, sourceHostId) {
|
|
5518
|
-
const hosts = [];
|
|
5519
|
-
for (const [targetId, target] of Object.entries(manifest.hosts)) {
|
|
5520
|
-
if (targetId === sourceHostId) continue;
|
|
5521
|
-
const profile = target.access?.from?.[sourceHostId];
|
|
5522
|
-
if (profile && (profile.status === "configured" || profile.status === "local")) {
|
|
5523
|
-
hosts.push({
|
|
5524
|
-
hostId: targetId,
|
|
5525
|
-
sshAliases: [...new Set(profile.ssh_aliases ?? [])],
|
|
5526
|
-
users: [...new Set(profile.users ?? [])]
|
|
5527
|
-
});
|
|
5528
|
-
}
|
|
5529
|
-
}
|
|
5530
|
-
return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
|
|
5531
|
-
}
|
|
5532
|
-
function formatMaintenanceLines(host) {
|
|
5533
|
-
const satellite = host.maintenance?.skillwiki_satellite;
|
|
5534
|
-
if (!satellite?.enabled) return [];
|
|
5535
|
-
return [
|
|
5536
|
-
`- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
|
|
5537
|
-
`- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
|
|
5538
|
-
];
|
|
5539
|
-
}
|
|
5540
|
-
function formatOutboundAccess(values) {
|
|
5541
|
-
if (values.length === 0) return "none";
|
|
5542
|
-
return values.map((value) => {
|
|
5543
|
-
const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
|
|
5544
|
-
const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
|
|
5545
|
-
return `\`${value.hostId}\`${aliasPart}${usersPart}`;
|
|
5546
|
-
}).join("; ");
|
|
5547
|
-
}
|
|
5548
|
-
function formatResolution(source, hostId2) {
|
|
5549
|
-
return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId2}\`` : `unknown -> \`${hostId2}\``;
|
|
5550
|
-
}
|
|
5551
|
-
function formatTrace(values) {
|
|
5552
|
-
if (values.length === 0) return "not available";
|
|
5553
|
-
return values.map((value) => {
|
|
5554
|
-
const source = `\`${value.source}\``;
|
|
5555
|
-
if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
|
|
5556
|
-
if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
|
|
5557
|
-
return `${source} ${value.status}`;
|
|
5558
|
-
}).join("; ");
|
|
5559
|
-
}
|
|
5560
|
-
function formatWarnings(values) {
|
|
5561
|
-
return values.length > 0 ? values.join("; ") : "none";
|
|
5562
|
-
}
|
|
5563
|
-
function formatList(values) {
|
|
5564
|
-
return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
|
|
5565
|
-
}
|
|
5566
|
-
function formatMaybe(value) {
|
|
5567
|
-
return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
|
|
5568
|
-
}
|
|
5569
|
-
function safeEnvValue(value) {
|
|
5570
|
-
return value && value.trim().length > 0 ? value : void 0;
|
|
5571
|
-
}
|
|
5572
|
-
function safeUserName() {
|
|
5573
|
-
try {
|
|
5574
|
-
return userInfo().username;
|
|
5575
|
-
} catch {
|
|
5576
|
-
return "";
|
|
4604
|
+
profiles = [];
|
|
4605
|
+
for (const key of Object.keys(map)) {
|
|
4606
|
+
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
4607
|
+
if (m && key !== "WIKI_PATH") {
|
|
4608
|
+
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
4609
|
+
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
5577
4613
|
}
|
|
4614
|
+
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");
|
|
4615
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
4616
|
+
}
|
|
4617
|
+
async function runConfigPath(input) {
|
|
4618
|
+
const filePath = configPath(input.home);
|
|
4619
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync7(filePath), humanHint: filePath }) };
|
|
5578
4620
|
}
|
|
5579
4621
|
|
|
5580
4622
|
// src/commands/doctor.ts
|
|
5581
|
-
import { existsSync as
|
|
5582
|
-
import { join as
|
|
4623
|
+
import { existsSync as existsSync13, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
|
|
4624
|
+
import { join as join24, resolve as resolve5 } from "path";
|
|
5583
4625
|
import { execSync as execSync2 } from "child_process";
|
|
5584
4626
|
import { platform as platform2 } from "os";
|
|
5585
4627
|
|
|
5586
4628
|
// src/utils/plugin-registry.ts
|
|
5587
|
-
import { existsSync as
|
|
5588
|
-
import { join as
|
|
5589
|
-
var REGISTRY_PATH =
|
|
5590
|
-
var CODEX_CONFIG_PATH =
|
|
4629
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
4630
|
+
import { join as join19 } from "path";
|
|
4631
|
+
var REGISTRY_PATH = join19(".claude", "plugins", "installed_plugins.json");
|
|
4632
|
+
var CODEX_CONFIG_PATH = join19(".codex", "config.toml");
|
|
5591
4633
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
5592
4634
|
function readInstalledPlugins(home) {
|
|
5593
4635
|
try {
|
|
5594
|
-
const raw =
|
|
4636
|
+
const raw = readFileSync5(join19(home, REGISTRY_PATH), "utf8");
|
|
5595
4637
|
return JSON.parse(raw);
|
|
5596
4638
|
} catch {
|
|
5597
4639
|
return null;
|
|
@@ -5627,8 +4669,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
|
|
|
5627
4669
|
function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
5628
4670
|
const config = readCodexPluginConfig(home, key, marketplace);
|
|
5629
4671
|
if (!config?.enabled) return null;
|
|
5630
|
-
const cacheRoot =
|
|
5631
|
-
if (!
|
|
4672
|
+
const cacheRoot = join19(home, ".codex", "plugins", "cache", marketplace, pluginName);
|
|
4673
|
+
if (!existsSync8(cacheRoot)) return null;
|
|
5632
4674
|
let versions;
|
|
5633
4675
|
try {
|
|
5634
4676
|
versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
@@ -5643,7 +4685,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
|
5643
4685
|
key,
|
|
5644
4686
|
pluginName,
|
|
5645
4687
|
marketplace,
|
|
5646
|
-
installPath:
|
|
4688
|
+
installPath: join19(cacheRoot, version),
|
|
5647
4689
|
version,
|
|
5648
4690
|
sourceType: config.sourceType,
|
|
5649
4691
|
source: config.source
|
|
@@ -5660,7 +4702,7 @@ function parsePluginKey(key) {
|
|
|
5660
4702
|
function readCodexPluginConfig(home, key, marketplace) {
|
|
5661
4703
|
let raw;
|
|
5662
4704
|
try {
|
|
5663
|
-
raw =
|
|
4705
|
+
raw = readFileSync5(join19(home, CODEX_CONFIG_PATH), "utf8");
|
|
5664
4706
|
} catch {
|
|
5665
4707
|
return null;
|
|
5666
4708
|
}
|
|
@@ -5702,8 +4744,8 @@ function parseTomlScalar(rawValue) {
|
|
|
5702
4744
|
}
|
|
5703
4745
|
|
|
5704
4746
|
// src/utils/conflict-markers.ts
|
|
5705
|
-
import { existsSync as
|
|
5706
|
-
import { join as
|
|
4747
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
4748
|
+
import { join as join20 } from "path";
|
|
5707
4749
|
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
5708
4750
|
const findings = [];
|
|
5709
4751
|
const lines = text.split(/\r?\n/);
|
|
@@ -5754,21 +4796,21 @@ function walkMarkdownFiles2(root, dir, rel, out) {
|
|
|
5754
4796
|
for (const entry of entries) {
|
|
5755
4797
|
if (entry.isDirectory()) {
|
|
5756
4798
|
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
5757
|
-
walkMarkdownFiles2(root,
|
|
4799
|
+
walkMarkdownFiles2(root, join20(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
5758
4800
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5759
4801
|
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
5760
4802
|
}
|
|
5761
4803
|
}
|
|
5762
4804
|
}
|
|
5763
4805
|
function scanVaultConflictMarkers(vaultRoot) {
|
|
5764
|
-
if (!
|
|
4806
|
+
if (!existsSync9(vaultRoot)) return [];
|
|
5765
4807
|
const relPaths = [];
|
|
5766
4808
|
walkMarkdownFiles2(vaultRoot, vaultRoot, "", relPaths);
|
|
5767
4809
|
const all = [];
|
|
5768
4810
|
for (const rel of relPaths) {
|
|
5769
4811
|
let text;
|
|
5770
4812
|
try {
|
|
5771
|
-
text =
|
|
4813
|
+
text = readFileSync6(join20(vaultRoot, rel), "utf8");
|
|
5772
4814
|
} catch {
|
|
5773
4815
|
continue;
|
|
5774
4816
|
}
|
|
@@ -5778,8 +4820,8 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5778
4820
|
}
|
|
5779
4821
|
|
|
5780
4822
|
// src/utils/remote-health.ts
|
|
5781
|
-
import { existsSync as
|
|
5782
|
-
import { join as
|
|
4823
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
4824
|
+
import { join as join21 } from "path";
|
|
5783
4825
|
import { execFileSync } from "child_process";
|
|
5784
4826
|
var REMOTE_PROBE_TIMEOUT_MS = 3e3;
|
|
5785
4827
|
var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
@@ -5790,7 +4832,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
|
5790
4832
|
}).trim();
|
|
5791
4833
|
function readWikiS3RemoteConfigured(home) {
|
|
5792
4834
|
try {
|
|
5793
|
-
const content =
|
|
4835
|
+
const content = readFileSync7(join21(home, ".skillwiki", ".env"), "utf8");
|
|
5794
4836
|
for (const line of content.split(/\r?\n/)) {
|
|
5795
4837
|
const trimmed = line.trim();
|
|
5796
4838
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -5815,7 +4857,7 @@ function resolveWikiS3Remote(input) {
|
|
|
5815
4857
|
return readWikiS3RemoteConfigured(input.home);
|
|
5816
4858
|
}
|
|
5817
4859
|
function probeGithubReachability(vaultPath, exec = defaultExec) {
|
|
5818
|
-
if (!
|
|
4860
|
+
if (!existsSync10(join21(vaultPath, ".git"))) return "unknown";
|
|
5819
4861
|
try {
|
|
5820
4862
|
exec("git", ["remote", "get-url", "origin"], vaultPath);
|
|
5821
4863
|
} catch {
|
|
@@ -5838,8 +4880,8 @@ function probeS3Reachability(remote, exec = defaultExec) {
|
|
|
5838
4880
|
return "unreachable";
|
|
5839
4881
|
}
|
|
5840
4882
|
}
|
|
5841
|
-
function probeSnapshotterSsh(
|
|
5842
|
-
if (!
|
|
4883
|
+
function probeSnapshotterSsh(sshAlias, exec = defaultExec) {
|
|
4884
|
+
if (!sshAlias) return "unknown";
|
|
5843
4885
|
try {
|
|
5844
4886
|
exec("ssh", [
|
|
5845
4887
|
"-o",
|
|
@@ -5848,7 +4890,7 @@ function probeSnapshotterSsh(sshAlias2, exec = defaultExec) {
|
|
|
5848
4890
|
"ConnectTimeout=3",
|
|
5849
4891
|
"-o",
|
|
5850
4892
|
"StrictHostKeyChecking=accept-new",
|
|
5851
|
-
|
|
4893
|
+
sshAlias,
|
|
5852
4894
|
"true"
|
|
5853
4895
|
]);
|
|
5854
4896
|
return "ok";
|
|
@@ -5880,11 +4922,11 @@ function probeRemoteHealth(input) {
|
|
|
5880
4922
|
}
|
|
5881
4923
|
|
|
5882
4924
|
// src/utils/satellite-run-health.ts
|
|
5883
|
-
import { existsSync as
|
|
5884
|
-
import { join as
|
|
4925
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
4926
|
+
import { join as join22 } from "path";
|
|
5885
4927
|
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
5886
4928
|
function satelliteLatestRunPath(vault) {
|
|
5887
|
-
return
|
|
4929
|
+
return join22(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
5888
4930
|
}
|
|
5889
4931
|
function isFailedRunStatus(status) {
|
|
5890
4932
|
return status === "fail" || status === "failure";
|
|
@@ -5906,9 +4948,9 @@ function readSatelliteLatestRunFromText(text) {
|
|
|
5906
4948
|
}
|
|
5907
4949
|
function readSatelliteLatestRun(vault) {
|
|
5908
4950
|
const latestPath = satelliteLatestRunPath(vault);
|
|
5909
|
-
if (!
|
|
4951
|
+
if (!existsSync11(latestPath)) return null;
|
|
5910
4952
|
try {
|
|
5911
|
-
return parseLatestRunFile(
|
|
4953
|
+
return parseLatestRunFile(readFileSync8(latestPath, "utf8"));
|
|
5912
4954
|
} catch {
|
|
5913
4955
|
return null;
|
|
5914
4956
|
}
|
|
@@ -5937,8 +4979,8 @@ function evaluateSatelliteRunHealth(vault, now) {
|
|
|
5937
4979
|
// src/utils/s3-mount-health.ts
|
|
5938
4980
|
import { execSync } from "child_process";
|
|
5939
4981
|
import { platform } from "os";
|
|
5940
|
-
import { readFileSync as
|
|
5941
|
-
import { join as
|
|
4982
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile15 } from "fs";
|
|
4983
|
+
import { join as join23 } from "path";
|
|
5942
4984
|
var OS = platform();
|
|
5943
4985
|
function findRcloneMountPid() {
|
|
5944
4986
|
try {
|
|
@@ -6022,7 +5064,7 @@ function extractRcloneFs(args) {
|
|
|
6022
5064
|
function getRcloneArgs(pid) {
|
|
6023
5065
|
try {
|
|
6024
5066
|
if (OS === "linux") {
|
|
6025
|
-
const raw =
|
|
5067
|
+
const raw = readFileSync9(`/proc/${pid}/cmdline`);
|
|
6026
5068
|
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
6027
5069
|
} else {
|
|
6028
5070
|
const out = execSync(`ps -o args= -p ${pid}`, {
|
|
@@ -6065,7 +5107,7 @@ function queryRcloneRC(rcAddr, fs) {
|
|
|
6065
5107
|
function detectFuseMount(vaultPath) {
|
|
6066
5108
|
try {
|
|
6067
5109
|
if (OS === "linux") {
|
|
6068
|
-
const mounts =
|
|
5110
|
+
const mounts = readFileSync9("/proc/mounts", "utf8");
|
|
6069
5111
|
let best = null;
|
|
6070
5112
|
for (const line of mounts.split("\n")) {
|
|
6071
5113
|
const parts = line.split(" ");
|
|
@@ -6096,7 +5138,7 @@ function detectFuseMount(vaultPath) {
|
|
|
6096
5138
|
return null;
|
|
6097
5139
|
}
|
|
6098
5140
|
function writeTest(dir) {
|
|
6099
|
-
const testFile =
|
|
5141
|
+
const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
6100
5142
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
6101
5143
|
const start = Date.now();
|
|
6102
5144
|
try {
|
|
@@ -6107,7 +5149,7 @@ function writeTest(dir) {
|
|
|
6107
5149
|
const writeMs = Date.now() - start;
|
|
6108
5150
|
const readStart = Date.now();
|
|
6109
5151
|
try {
|
|
6110
|
-
const back =
|
|
5152
|
+
const back = readFile15(testFile, "utf8");
|
|
6111
5153
|
const readMs = Date.now() - readStart;
|
|
6112
5154
|
if (back !== payload) {
|
|
6113
5155
|
try {
|
|
@@ -6181,14 +5223,14 @@ function checkNodeVersion() {
|
|
|
6181
5223
|
function detectCliChannels(argv, home) {
|
|
6182
5224
|
const channels = [];
|
|
6183
5225
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
6184
|
-
const devPath =
|
|
5226
|
+
const devPath = resolve5(argv[1]);
|
|
6185
5227
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
6186
5228
|
}
|
|
6187
5229
|
try {
|
|
6188
5230
|
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
6189
5231
|
if (whichOut) {
|
|
6190
5232
|
const isDev = isDevSymlink(whichOut);
|
|
6191
|
-
if (!channels.some((c) => c.path ===
|
|
5233
|
+
if (!channels.some((c) => c.path === resolve5(whichOut))) {
|
|
6192
5234
|
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
6193
5235
|
}
|
|
6194
5236
|
}
|
|
@@ -6196,22 +5238,22 @@ function detectCliChannels(argv, home) {
|
|
|
6196
5238
|
}
|
|
6197
5239
|
const plugin = findPlugin(home);
|
|
6198
5240
|
if (plugin) {
|
|
6199
|
-
const pluginBin =
|
|
6200
|
-
if (
|
|
5241
|
+
const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
|
|
5242
|
+
if (existsSync13(pluginBin)) {
|
|
6201
5243
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
6202
5244
|
}
|
|
6203
5245
|
}
|
|
6204
|
-
const installBin =
|
|
6205
|
-
if (
|
|
5246
|
+
const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
|
|
5247
|
+
if (existsSync13(installBin)) {
|
|
6206
5248
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
6207
5249
|
}
|
|
6208
5250
|
return channels;
|
|
6209
5251
|
}
|
|
6210
5252
|
function isDevSymlink(binPath) {
|
|
6211
5253
|
try {
|
|
6212
|
-
const st =
|
|
5254
|
+
const st = lstatSync(binPath);
|
|
6213
5255
|
if (st.isSymbolicLink()) {
|
|
6214
|
-
const target =
|
|
5256
|
+
const target = resolve5(binPath, "..", readlinkSync(binPath));
|
|
6215
5257
|
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
6216
5258
|
}
|
|
6217
5259
|
} catch {
|
|
@@ -6263,7 +5305,7 @@ function isDevSourceRun(argv) {
|
|
|
6263
5305
|
}
|
|
6264
5306
|
async function checkConfigFile(home) {
|
|
6265
5307
|
const cfgPath = configPath(home);
|
|
6266
|
-
if (!
|
|
5308
|
+
if (!existsSync13(cfgPath)) {
|
|
6267
5309
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
6268
5310
|
}
|
|
6269
5311
|
try {
|
|
@@ -6278,7 +5320,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
6278
5320
|
if (resolvedPath === void 0) {
|
|
6279
5321
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6280
5322
|
}
|
|
6281
|
-
if (
|
|
5323
|
+
if (existsSync13(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
|
|
6282
5324
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
6283
5325
|
}
|
|
6284
5326
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -6287,13 +5329,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
6287
5329
|
if (resolvedPath === void 0) {
|
|
6288
5330
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6289
5331
|
}
|
|
6290
|
-
if (!
|
|
5332
|
+
if (!existsSync13(resolvedPath)) {
|
|
6291
5333
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
6292
5334
|
}
|
|
6293
5335
|
const missing = [];
|
|
6294
|
-
if (!
|
|
5336
|
+
if (!existsSync13(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
6295
5337
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
6296
|
-
if (!
|
|
5338
|
+
if (!existsSync13(join24(resolvedPath, dir))) missing.push(dir + "/");
|
|
6297
5339
|
}
|
|
6298
5340
|
if (missing.length === 0) {
|
|
6299
5341
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -6301,8 +5343,8 @@ function checkVaultStructure(resolvedPath) {
|
|
|
6301
5343
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
6302
5344
|
}
|
|
6303
5345
|
function checkSkillsInstalled(home, cwd) {
|
|
6304
|
-
const srcDir = cwd ?
|
|
6305
|
-
if (srcDir &&
|
|
5346
|
+
const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
|
|
5347
|
+
if (srcDir && existsSync13(srcDir)) {
|
|
6306
5348
|
const found = findInstalledSkillMd(srcDir);
|
|
6307
5349
|
if (found.length > 0) {
|
|
6308
5350
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
@@ -6315,8 +5357,8 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
6315
5357
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
6316
5358
|
}
|
|
6317
5359
|
}
|
|
6318
|
-
const skillsDir =
|
|
6319
|
-
if (
|
|
5360
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
5361
|
+
if (existsSync13(skillsDir)) {
|
|
6320
5362
|
const found = findInstalledSkillMd(skillsDir);
|
|
6321
5363
|
if (found.length > 0) {
|
|
6322
5364
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -6326,10 +5368,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
6326
5368
|
}
|
|
6327
5369
|
function checkDuplicateSkills(home) {
|
|
6328
5370
|
const plugin = findPlugin(home);
|
|
6329
|
-
const skillsDir =
|
|
5371
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
6330
5372
|
const agentSkillDirs = [
|
|
6331
|
-
{ label: "~/.codex/skills/", path:
|
|
6332
|
-
{ label: "~/.agents/skills/", path:
|
|
5373
|
+
{ label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
|
|
5374
|
+
{ label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
|
|
6333
5375
|
];
|
|
6334
5376
|
if (!plugin) {
|
|
6335
5377
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -6432,8 +5474,8 @@ async function checkProfiles(home) {
|
|
|
6432
5474
|
}
|
|
6433
5475
|
async function checkProjectLocalOverride(cwd) {
|
|
6434
5476
|
const dir = cwd ?? process.cwd();
|
|
6435
|
-
const envPath =
|
|
6436
|
-
if (
|
|
5477
|
+
const envPath = join24(dir, ".skillwiki", ".env");
|
|
5478
|
+
if (existsSync13(envPath)) {
|
|
6437
5479
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
6438
5480
|
}
|
|
6439
5481
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -6442,7 +5484,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
6442
5484
|
if (resolvedPath === void 0) {
|
|
6443
5485
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6444
5486
|
}
|
|
6445
|
-
if (!
|
|
5487
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6446
5488
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
6447
5489
|
}
|
|
6448
5490
|
try {
|
|
@@ -6465,9 +5507,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
6465
5507
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6466
5508
|
}
|
|
6467
5509
|
const missing = [];
|
|
6468
|
-
if (!
|
|
6469
|
-
if (!
|
|
6470
|
-
if (!
|
|
5510
|
+
if (!existsSync13(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
5511
|
+
if (!existsSync13(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
5512
|
+
if (!existsSync13(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
6471
5513
|
if (missing.length === 0) {
|
|
6472
5514
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
6473
5515
|
}
|
|
@@ -6477,12 +5519,12 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
6477
5519
|
if (resolvedPath === void 0) {
|
|
6478
5520
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6479
5521
|
}
|
|
6480
|
-
const rawDir =
|
|
6481
|
-
if (!
|
|
5522
|
+
const rawDir = join24(resolvedPath, "raw");
|
|
5523
|
+
if (!existsSync13(rawDir)) {
|
|
6482
5524
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
6483
5525
|
}
|
|
6484
5526
|
const found = [];
|
|
6485
|
-
(function
|
|
5527
|
+
(function walk(dir, rel) {
|
|
6486
5528
|
let entries;
|
|
6487
5529
|
try {
|
|
6488
5530
|
entries = readdirSync3(dir, { withFileTypes: true });
|
|
@@ -6493,7 +5535,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
6493
5535
|
if (entry.name === ".DS_Store") {
|
|
6494
5536
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
6495
5537
|
} else if (entry.isDirectory()) {
|
|
6496
|
-
|
|
5538
|
+
walk(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
6497
5539
|
}
|
|
6498
5540
|
}
|
|
6499
5541
|
})(rawDir, "");
|
|
@@ -6524,7 +5566,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
6524
5566
|
if (resolvedPath === void 0) {
|
|
6525
5567
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6526
5568
|
}
|
|
6527
|
-
if (!
|
|
5569
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6528
5570
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
6529
5571
|
}
|
|
6530
5572
|
let timestamp;
|
|
@@ -6572,7 +5614,7 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
6572
5614
|
if (resolvedPath === void 0) {
|
|
6573
5615
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
6574
5616
|
}
|
|
6575
|
-
if (!
|
|
5617
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6576
5618
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
6577
5619
|
}
|
|
6578
5620
|
try {
|
|
@@ -6640,7 +5682,7 @@ function remoteMainHash(resolvedPath) {
|
|
|
6640
5682
|
}
|
|
6641
5683
|
function checkStaleRemoteMain(resolvedPath) {
|
|
6642
5684
|
if (resolvedPath === void 0) return void 0;
|
|
6643
|
-
if (!
|
|
5685
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) return void 0;
|
|
6644
5686
|
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
6645
5687
|
if (!localOrigin) return void 0;
|
|
6646
5688
|
const remoteMain = remoteMainHash(resolvedPath);
|
|
@@ -6656,7 +5698,7 @@ function checkVaultLocalGit(resolvedPath) {
|
|
|
6656
5698
|
if (resolvedPath === void 0) {
|
|
6657
5699
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6658
5700
|
}
|
|
6659
|
-
if (!
|
|
5701
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6660
5702
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
6661
5703
|
}
|
|
6662
5704
|
try {
|
|
@@ -6675,7 +5717,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
6675
5717
|
if (resolvedPath === void 0) {
|
|
6676
5718
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
6677
5719
|
}
|
|
6678
|
-
if (!
|
|
5720
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6679
5721
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
6680
5722
|
}
|
|
6681
5723
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -6719,7 +5761,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
6719
5761
|
if (resolvedPath === void 0) {
|
|
6720
5762
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
6721
5763
|
}
|
|
6722
|
-
if (!
|
|
5764
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6723
5765
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
6724
5766
|
}
|
|
6725
5767
|
try {
|
|
@@ -6746,7 +5788,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
6746
5788
|
if (resolvedPath === void 0) {
|
|
6747
5789
|
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
6748
5790
|
}
|
|
6749
|
-
if (!
|
|
5791
|
+
if (!existsSync13(join24(resolvedPath, ".git"))) {
|
|
6750
5792
|
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
6751
5793
|
}
|
|
6752
5794
|
if (!hasOriginMain(resolvedPath)) {
|
|
@@ -6774,7 +5816,7 @@ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
|
|
|
6774
5816
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
|
|
6775
5817
|
}
|
|
6776
5818
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
6777
|
-
if (!
|
|
5819
|
+
if (!existsSync13(latestPath)) {
|
|
6778
5820
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
|
|
6779
5821
|
}
|
|
6780
5822
|
try {
|
|
@@ -6862,11 +5904,11 @@ async function checkFleetIdentity(input) {
|
|
|
6862
5904
|
}
|
|
6863
5905
|
function pullLogPaths(home) {
|
|
6864
5906
|
const paths = platform2() === "darwin" ? [
|
|
6865
|
-
|
|
6866
|
-
|
|
5907
|
+
join24(home, "Library", "Logs", "wiki-pull.log"),
|
|
5908
|
+
join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6867
5909
|
] : [
|
|
6868
|
-
|
|
6869
|
-
|
|
5910
|
+
join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
5911
|
+
join24(home, "Library", "Logs", "wiki-pull.log")
|
|
6870
5912
|
];
|
|
6871
5913
|
return [...new Set(paths)];
|
|
6872
5914
|
}
|
|
@@ -6878,12 +5920,12 @@ function isRecentLogLine(line, nowMs) {
|
|
|
6878
5920
|
return nowMs - ts <= 24 * 60 * 60 * 1e3;
|
|
6879
5921
|
}
|
|
6880
5922
|
function checkVaultGitPullFailures(home) {
|
|
6881
|
-
const path = pullLogPaths(home).find((p) =>
|
|
5923
|
+
const path = pullLogPaths(home).find((p) => existsSync13(p));
|
|
6882
5924
|
if (!path) {
|
|
6883
5925
|
return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
|
|
6884
5926
|
}
|
|
6885
5927
|
try {
|
|
6886
|
-
const lines =
|
|
5928
|
+
const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
6887
5929
|
const now = Date.now();
|
|
6888
5930
|
const failures = lines.filter(
|
|
6889
5931
|
(line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
|
|
@@ -6906,8 +5948,8 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
6906
5948
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
6907
5949
|
}
|
|
6908
5950
|
const mountPoint = fuse.mountPoint;
|
|
6909
|
-
const conceptsDir =
|
|
6910
|
-
if (!
|
|
5951
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
5952
|
+
if (!existsSync13(conceptsDir)) {
|
|
6911
5953
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
6912
5954
|
}
|
|
6913
5955
|
const start = Date.now();
|
|
@@ -7089,8 +6131,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
7089
6131
|
if (!fuse) {
|
|
7090
6132
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
7091
6133
|
}
|
|
7092
|
-
const conceptsDir =
|
|
7093
|
-
if (!
|
|
6134
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
6135
|
+
if (!existsSync13(conceptsDir)) {
|
|
7094
6136
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
7095
6137
|
}
|
|
7096
6138
|
const result = writeTest(conceptsDir);
|
|
@@ -7174,9 +6216,45 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
7174
6216
|
`${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
|
|
7175
6217
|
);
|
|
7176
6218
|
}
|
|
6219
|
+
function checkVaultSyncPullHelper(home, env) {
|
|
6220
|
+
const path = resolveVaultSyncPullHelper({
|
|
6221
|
+
vault: "",
|
|
6222
|
+
home,
|
|
6223
|
+
env
|
|
6224
|
+
});
|
|
6225
|
+
if (path) {
|
|
6226
|
+
return check("pass", "vault_sync_pull_helper", "Vault-sync pull helper", `Resolved: ${path}`);
|
|
6227
|
+
}
|
|
6228
|
+
return check(
|
|
6229
|
+
"error",
|
|
6230
|
+
"vault_sync_pull_helper",
|
|
6231
|
+
"Vault-sync pull helper",
|
|
6232
|
+
"Not found \u2014 install skillwiki@0.10.1+, redeploy vault-sync, or set SKILLWIKI_VAULT_SYNC_PULL_HELPER"
|
|
6233
|
+
);
|
|
6234
|
+
}
|
|
6235
|
+
function checkVaultSyncReviewRequiredJournals(vaultPath) {
|
|
6236
|
+
if (!vaultPath || !existsSync13(join24(vaultPath, ".git"))) {
|
|
6237
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "No git vault \u2014 check skipped");
|
|
6238
|
+
}
|
|
6239
|
+
try {
|
|
6240
|
+
const ops = listReviewRequiredOps(vaultPath);
|
|
6241
|
+
if (ops.length === 0) {
|
|
6242
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "None");
|
|
6243
|
+
}
|
|
6244
|
+
const sample = ops[0]?.opId ?? "?";
|
|
6245
|
+
return check(
|
|
6246
|
+
"warn",
|
|
6247
|
+
"vault_sync_review_required_journals",
|
|
6248
|
+
"Review-required journals",
|
|
6249
|
+
`${ops.length} handoff(s); oldest/sample: ${sample} \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`
|
|
6250
|
+
);
|
|
6251
|
+
} catch {
|
|
6252
|
+
return check("pass", "vault_sync_review_required_journals", "Review-required journals", "Could not read journals \u2014 check skipped");
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
7177
6255
|
function readVaultSyncConfig(home) {
|
|
7178
6256
|
try {
|
|
7179
|
-
const content =
|
|
6257
|
+
const content = readFileSync10(join24(home, ".skillwiki", ".env"), "utf8");
|
|
7180
6258
|
let installed = false;
|
|
7181
6259
|
let role;
|
|
7182
6260
|
let serviceScope;
|
|
@@ -7205,7 +6283,7 @@ function readVaultSyncConfig(home) {
|
|
|
7205
6283
|
}
|
|
7206
6284
|
function readKeyFromEnvFile(path, keys) {
|
|
7207
6285
|
try {
|
|
7208
|
-
const content =
|
|
6286
|
+
const content = readFileSync10(path, "utf8");
|
|
7209
6287
|
for (const line of content.split(/\r?\n/)) {
|
|
7210
6288
|
const trimmed = line.trim();
|
|
7211
6289
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
@@ -7227,7 +6305,7 @@ function resolveSnapshotGitWorktree(config) {
|
|
|
7227
6305
|
if (fromProfile) return fromProfile;
|
|
7228
6306
|
}
|
|
7229
6307
|
const defaultPath = "/root/wiki-git";
|
|
7230
|
-
return
|
|
6308
|
+
return existsSync13(defaultPath) ? defaultPath : void 0;
|
|
7231
6309
|
}
|
|
7232
6310
|
function vaultSyncChecks(input) {
|
|
7233
6311
|
const os = input.os ?? platform2();
|
|
@@ -7244,16 +6322,16 @@ function vaultSyncChecks(input) {
|
|
|
7244
6322
|
];
|
|
7245
6323
|
}
|
|
7246
6324
|
const isMac = os === "darwin";
|
|
7247
|
-
const logDir = input.logDir ?? (isMac ?
|
|
7248
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
7249
|
-
const filterPath = input.filterPath ??
|
|
7250
|
-
const packagedSnapshotPath =
|
|
6325
|
+
const logDir = input.logDir ?? (isMac ? join24(home, "Library", "Logs") : join24(home, ".local", "state", "vault-sync", "log"));
|
|
6326
|
+
const shareDir = input.shareDir ?? (isMac ? join24(home, "Library", "Application Support", "vault-sync", "bin") : join24(home, ".local", "share", "vault-sync", "bin"));
|
|
6327
|
+
const filterPath = input.filterPath ?? join24(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
6328
|
+
const packagedSnapshotPath = join24(shareDir, "wiki-snapshot.sh");
|
|
7251
6329
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
7252
|
-
const snapshotPath = input.snapshotScriptPath ?? (
|
|
6330
|
+
const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
7253
6331
|
function snapshotLastStatusCheck() {
|
|
7254
|
-
const snapshotLog =
|
|
6332
|
+
const snapshotLog = join24(logDir, "wiki-snapshot.log");
|
|
7255
6333
|
try {
|
|
7256
|
-
const logContent =
|
|
6334
|
+
const logContent = readFileSync10(snapshotLog, "utf8");
|
|
7257
6335
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7258
6336
|
if (lines.length === 0) {
|
|
7259
6337
|
return check(
|
|
@@ -7298,14 +6376,14 @@ function vaultSyncChecks(input) {
|
|
|
7298
6376
|
}
|
|
7299
6377
|
}
|
|
7300
6378
|
if (input.vaultSyncRole === "snapshotter") {
|
|
7301
|
-
const c12 =
|
|
6379
|
+
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}`);
|
|
7302
6380
|
const serviceScope = input.vaultSyncServiceScope ?? "user";
|
|
7303
|
-
const userTimerPath =
|
|
6381
|
+
const userTimerPath = join24(home, ".config", "systemd", "user", "wiki-snapshot.timer");
|
|
7304
6382
|
const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
|
|
7305
6383
|
let c22;
|
|
7306
|
-
if (serviceScope === "user" &&
|
|
6384
|
+
if (serviceScope === "user" && existsSync13(userTimerPath)) {
|
|
7307
6385
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
|
|
7308
|
-
} else if (serviceScope === "system" &&
|
|
6386
|
+
} else if (serviceScope === "system" && existsSync13(systemTimerPath)) {
|
|
7309
6387
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
|
|
7310
6388
|
} else if (os !== "linux") {
|
|
7311
6389
|
c22 = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "Snapshotter scheduler is Linux-only and no wiki-snapshot.timer file was found");
|
|
@@ -7337,7 +6415,7 @@ function vaultSyncChecks(input) {
|
|
|
7337
6415
|
);
|
|
7338
6416
|
let c52;
|
|
7339
6417
|
try {
|
|
7340
|
-
if (!
|
|
6418
|
+
if (!existsSync13(snapshotPath)) {
|
|
7341
6419
|
c52 = check(
|
|
7342
6420
|
"error",
|
|
7343
6421
|
"vault_sync_snapshot_guard",
|
|
@@ -7345,7 +6423,7 @@ function vaultSyncChecks(input) {
|
|
|
7345
6423
|
`Snapshot script not found at ${snapshotPath}`
|
|
7346
6424
|
);
|
|
7347
6425
|
} else {
|
|
7348
|
-
const content =
|
|
6426
|
+
const content = readFileSync10(snapshotPath, "utf8");
|
|
7349
6427
|
if (!content.includes("--max-delete")) {
|
|
7350
6428
|
c52 = check(
|
|
7351
6429
|
"error",
|
|
@@ -7372,8 +6450,8 @@ function vaultSyncChecks(input) {
|
|
|
7372
6450
|
}
|
|
7373
6451
|
return [c12, c22, c32, cFetch2, c42, c52];
|
|
7374
6452
|
}
|
|
7375
|
-
const pushScriptPath =
|
|
7376
|
-
const c1 =
|
|
6453
|
+
const pushScriptPath = join24(shareDir, "wiki-push.sh");
|
|
6454
|
+
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`);
|
|
7377
6455
|
let c2;
|
|
7378
6456
|
try {
|
|
7379
6457
|
if (isMac) {
|
|
@@ -7424,10 +6502,10 @@ function vaultSyncChecks(input) {
|
|
|
7424
6502
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
7425
6503
|
);
|
|
7426
6504
|
}
|
|
7427
|
-
const logFile =
|
|
6505
|
+
const logFile = join24(logDir, "wiki-push.log");
|
|
7428
6506
|
let c3;
|
|
7429
6507
|
try {
|
|
7430
|
-
const logContent =
|
|
6508
|
+
const logContent = readFileSync10(logFile, "utf8");
|
|
7431
6509
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7432
6510
|
if (lines.length === 0) {
|
|
7433
6511
|
c3 = check(
|
|
@@ -7483,7 +6561,7 @@ function vaultSyncChecks(input) {
|
|
|
7483
6561
|
}
|
|
7484
6562
|
}
|
|
7485
6563
|
} catch {
|
|
7486
|
-
c3 =
|
|
6564
|
+
c3 = existsSync13(logDir) ? check(
|
|
7487
6565
|
"warn",
|
|
7488
6566
|
"vault_sync_last_push_age",
|
|
7489
6567
|
"Vault sync last push recency",
|
|
@@ -7495,10 +6573,10 @@ function vaultSyncChecks(input) {
|
|
|
7495
6573
|
`Log directory not found at ${logDir}`
|
|
7496
6574
|
);
|
|
7497
6575
|
}
|
|
7498
|
-
const fetchLogFile =
|
|
6576
|
+
const fetchLogFile = join24(logDir, "wiki-fetch.log");
|
|
7499
6577
|
let cFetch;
|
|
7500
6578
|
try {
|
|
7501
|
-
const logContent =
|
|
6579
|
+
const logContent = readFileSync10(fetchLogFile, "utf8");
|
|
7502
6580
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
7503
6581
|
if (lines.length === 0) {
|
|
7504
6582
|
cFetch = check(
|
|
@@ -7542,7 +6620,7 @@ function vaultSyncChecks(input) {
|
|
|
7542
6620
|
}
|
|
7543
6621
|
let c4;
|
|
7544
6622
|
try {
|
|
7545
|
-
if (!
|
|
6623
|
+
if (!existsSync13(filterPath)) {
|
|
7546
6624
|
c4 = check(
|
|
7547
6625
|
"error",
|
|
7548
6626
|
"vault_sync_filter_present",
|
|
@@ -7550,7 +6628,7 @@ function vaultSyncChecks(input) {
|
|
|
7550
6628
|
`Filter file not found at ${filterPath}`
|
|
7551
6629
|
);
|
|
7552
6630
|
} else {
|
|
7553
|
-
const content =
|
|
6631
|
+
const content = readFileSync10(filterPath, "utf8");
|
|
7554
6632
|
const requiredExcludes = [
|
|
7555
6633
|
"remotely-save/data.json",
|
|
7556
6634
|
".skillwiki/sync.lock",
|
|
@@ -7593,7 +6671,7 @@ function vaultSyncChecks(input) {
|
|
|
7593
6671
|
);
|
|
7594
6672
|
} else {
|
|
7595
6673
|
try {
|
|
7596
|
-
if (!
|
|
6674
|
+
if (!existsSync13(snapshotPath)) {
|
|
7597
6675
|
c5 = check(
|
|
7598
6676
|
"error",
|
|
7599
6677
|
"vault_sync_snapshot_guard",
|
|
@@ -7601,7 +6679,7 @@ function vaultSyncChecks(input) {
|
|
|
7601
6679
|
`Snapshot script not found at ${snapshotPath}`
|
|
7602
6680
|
);
|
|
7603
6681
|
} else {
|
|
7604
|
-
const content =
|
|
6682
|
+
const content = readFileSync10(snapshotPath, "utf8");
|
|
7605
6683
|
if (!content.includes("--max-delete")) {
|
|
7606
6684
|
c5 = check(
|
|
7607
6685
|
"error",
|
|
@@ -7639,15 +6717,15 @@ function findSkillMd(dir) {
|
|
|
7639
6717
|
}
|
|
7640
6718
|
for (const entry of entries) {
|
|
7641
6719
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
7642
|
-
results.push(
|
|
6720
|
+
results.push(join24(dir, entry.name));
|
|
7643
6721
|
} else if (entry.isDirectory()) {
|
|
7644
|
-
results.push(...findSkillMd(
|
|
6722
|
+
results.push(...findSkillMd(join24(dir, entry.name)));
|
|
7645
6723
|
}
|
|
7646
6724
|
}
|
|
7647
6725
|
return results;
|
|
7648
6726
|
}
|
|
7649
6727
|
function findInstalledSkillMd(dir) {
|
|
7650
|
-
const directSkills = findSkillNames(dir).map((name) =>
|
|
6728
|
+
const directSkills = findSkillNames(dir).map((name) => join24(dir, name, "SKILL.md"));
|
|
7651
6729
|
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
7652
6730
|
}
|
|
7653
6731
|
function findSkillNames(dir) {
|
|
@@ -7659,7 +6737,7 @@ function findSkillNames(dir) {
|
|
|
7659
6737
|
return results;
|
|
7660
6738
|
}
|
|
7661
6739
|
for (const entry of entries) {
|
|
7662
|
-
if (entry.isDirectory() &&
|
|
6740
|
+
if (entry.isDirectory() && existsSync13(join24(dir, entry.name, "SKILL.md"))) {
|
|
7663
6741
|
results.push(entry.name);
|
|
7664
6742
|
}
|
|
7665
6743
|
}
|
|
@@ -7703,7 +6781,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
7703
6781
|
}
|
|
7704
6782
|
let logLines = 0;
|
|
7705
6783
|
try {
|
|
7706
|
-
logLines =
|
|
6784
|
+
logLines = readFileSync10(join24(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
7707
6785
|
} catch {
|
|
7708
6786
|
}
|
|
7709
6787
|
return [
|
|
@@ -7784,6 +6862,8 @@ async function runDoctor(input) {
|
|
|
7784
6862
|
vaultSyncServiceScope: vsConfig.serviceScope,
|
|
7785
6863
|
snapshotScriptPath: vsConfig.snapshotScript
|
|
7786
6864
|
}));
|
|
6865
|
+
checks.push(checkVaultSyncPullHelper(input.home, input.env ?? process.env));
|
|
6866
|
+
checks.push(checkVaultSyncReviewRequiredJournals(resolvedPath));
|
|
7787
6867
|
const satelliteGate = satelliteGateFromFleetLoad(fleetLoad);
|
|
7788
6868
|
checks.push(checkSatelliteLastRun(resolvedPath, satelliteGate.satelliteExpected));
|
|
7789
6869
|
checks.push(checkSatelliteTimer(satelliteGate.satelliteExpected));
|
|
@@ -7811,7 +6891,7 @@ async function runDoctor(input) {
|
|
|
7811
6891
|
}
|
|
7812
6892
|
|
|
7813
6893
|
// src/utils/package-info.ts
|
|
7814
|
-
import { readFileSync as
|
|
6894
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
7815
6895
|
function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
7816
6896
|
return [
|
|
7817
6897
|
new URL("../package.json", baseUrl),
|
|
@@ -7821,7 +6901,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
|
7821
6901
|
function readCliPackageJson(baseUrl = import.meta.url) {
|
|
7822
6902
|
for (const url of packageJsonCandidateUrls(baseUrl)) {
|
|
7823
6903
|
try {
|
|
7824
|
-
const pkg = JSON.parse(
|
|
6904
|
+
const pkg = JSON.parse(readFileSync11(url, "utf8"));
|
|
7825
6905
|
if (typeof pkg.version === "string") {
|
|
7826
6906
|
return { ...pkg, version: pkg.version };
|
|
7827
6907
|
}
|
|
@@ -7831,214 +6911,11 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
7831
6911
|
throw new Error(`Could not locate skillwiki package.json from ${baseUrl}`);
|
|
7832
6912
|
}
|
|
7833
6913
|
|
|
7834
|
-
// src/commands/project-index.ts
|
|
7835
|
-
import { readdir as readdir4, readFile as readFile19, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
7836
|
-
import { join as join27, dirname as dirname8, basename as basename2 } from "path";
|
|
7837
|
-
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
7838
|
-
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
7839
|
-
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
7840
|
-
const found = [];
|
|
7841
|
-
let entries;
|
|
7842
|
-
try {
|
|
7843
|
-
entries = await readdir4(rootAbs, { withFileTypes: true });
|
|
7844
|
-
} catch {
|
|
7845
|
-
return found;
|
|
7846
|
-
}
|
|
7847
|
-
for (const entry of entries) {
|
|
7848
|
-
const abs = join27(rootAbs, entry.name);
|
|
7849
|
-
const rel = `${rootRel}/${entry.name}`;
|
|
7850
|
-
if (entry.isDirectory()) {
|
|
7851
|
-
found.push(...await scanMarkdownTree(abs, rel));
|
|
7852
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7853
|
-
found.push(rel);
|
|
7854
|
-
}
|
|
7855
|
-
}
|
|
7856
|
-
return found;
|
|
7857
|
-
}
|
|
7858
|
-
function projectLocalType(slug, page, data) {
|
|
7859
|
-
if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
|
|
7860
|
-
if (page.startsWith(`projects/${slug}/work/`)) {
|
|
7861
|
-
if (typeof data.kind === "string") return data.kind;
|
|
7862
|
-
const name = basename2(page, ".md");
|
|
7863
|
-
if (name === "spec" || name === "plan" || name === "retro") return name;
|
|
7864
|
-
return "work";
|
|
7865
|
-
}
|
|
7866
|
-
if (page.startsWith(`projects/${slug}/architecture/`)) {
|
|
7867
|
-
return typeof data.type === "string" ? data.type : "architecture";
|
|
7868
|
-
}
|
|
7869
|
-
if (page.startsWith(`projects/${slug}/history/`)) {
|
|
7870
|
-
if (typeof data.kind === "string") return data.kind;
|
|
7871
|
-
if (typeof data.type === "string") return data.type;
|
|
7872
|
-
return "history";
|
|
7873
|
-
}
|
|
7874
|
-
return typeof data.type === "string" ? data.type : "project";
|
|
7875
|
-
}
|
|
7876
|
-
async function runProjectIndex(input) {
|
|
7877
|
-
const slug = input.slug;
|
|
7878
|
-
const projectDir = join27(input.vault, "projects", slug);
|
|
7879
|
-
try {
|
|
7880
|
-
await readdir4(projectDir);
|
|
7881
|
-
} catch {
|
|
7882
|
-
return {
|
|
7883
|
-
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
7884
|
-
result: err("PROJECT_NOT_FOUND", { slug, path: projectDir })
|
|
7885
|
-
};
|
|
7886
|
-
}
|
|
7887
|
-
const wikilinkPattern = `[[${slug}]]`;
|
|
7888
|
-
const entries = [];
|
|
7889
|
-
const compoundDir = join27(input.vault, "projects", slug, "compound");
|
|
7890
|
-
try {
|
|
7891
|
-
const compoundFiles = await readdir4(compoundDir, { withFileTypes: true });
|
|
7892
|
-
for (const entry of compoundFiles) {
|
|
7893
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7894
|
-
const filePath = join27(compoundDir, entry.name);
|
|
7895
|
-
let text;
|
|
7896
|
-
try {
|
|
7897
|
-
text = await readFile19(filePath, "utf8");
|
|
7898
|
-
} catch {
|
|
7899
|
-
continue;
|
|
7900
|
-
}
|
|
7901
|
-
const fm = extractFrontmatter(text);
|
|
7902
|
-
if (!fm.ok) continue;
|
|
7903
|
-
entries.push({
|
|
7904
|
-
page: `projects/${slug}/compound/${entry.name}`,
|
|
7905
|
-
type: typeof fm.data.type === "string" ? fm.data.type : "compound",
|
|
7906
|
-
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
7907
|
-
});
|
|
7908
|
-
}
|
|
7909
|
-
} catch {
|
|
7910
|
-
}
|
|
7911
|
-
for (const dir of LAYER2_DIRS) {
|
|
7912
|
-
let files;
|
|
7913
|
-
try {
|
|
7914
|
-
files = await readdir4(join27(input.vault, dir), { withFileTypes: true });
|
|
7915
|
-
} catch {
|
|
7916
|
-
continue;
|
|
7917
|
-
}
|
|
7918
|
-
for (const entry of files) {
|
|
7919
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7920
|
-
const filePath = join27(input.vault, dir, entry.name);
|
|
7921
|
-
let text;
|
|
7922
|
-
try {
|
|
7923
|
-
text = await readFile19(filePath, "utf8");
|
|
7924
|
-
} catch {
|
|
7925
|
-
continue;
|
|
7926
|
-
}
|
|
7927
|
-
const fm = extractFrontmatter(text);
|
|
7928
|
-
if (!fm.ok) continue;
|
|
7929
|
-
const pp = fm.data.provenance_projects;
|
|
7930
|
-
if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
|
|
7931
|
-
entries.push({
|
|
7932
|
-
page: `${dir}/${entry.name}`,
|
|
7933
|
-
type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
|
|
7934
|
-
title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
|
|
7935
|
-
});
|
|
7936
|
-
}
|
|
7937
|
-
}
|
|
7938
|
-
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
7939
|
-
const rootAbs = join27(projectDir, dir);
|
|
7940
|
-
const rootRel = `projects/${slug}/${dir}`;
|
|
7941
|
-
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
7942
|
-
for (const page of pages) {
|
|
7943
|
-
const filePath = join27(input.vault, page);
|
|
7944
|
-
let text;
|
|
7945
|
-
try {
|
|
7946
|
-
text = await readFile19(filePath, "utf8");
|
|
7947
|
-
} catch {
|
|
7948
|
-
continue;
|
|
7949
|
-
}
|
|
7950
|
-
const fm = extractFrontmatter(text);
|
|
7951
|
-
if (!fm.ok) continue;
|
|
7952
|
-
entries.push({
|
|
7953
|
-
page,
|
|
7954
|
-
type: projectLocalType(slug, page, fm.data),
|
|
7955
|
-
title: typeof fm.data.title === "string" ? fm.data.title : basename2(page, ".md")
|
|
7956
|
-
});
|
|
7957
|
-
}
|
|
7958
|
-
}
|
|
7959
|
-
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 };
|
|
7960
|
-
entries.sort((a, b) => {
|
|
7961
|
-
const ta = typeOrder[a.type] ?? 99;
|
|
7962
|
-
const tb = typeOrder[b.type] ?? 99;
|
|
7963
|
-
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
7964
|
-
});
|
|
7965
|
-
const indexPath = join27(projectDir, "knowledge.md");
|
|
7966
|
-
let existing = false;
|
|
7967
|
-
let stale = false;
|
|
7968
|
-
try {
|
|
7969
|
-
const existingText = await readFile19(indexPath, "utf8");
|
|
7970
|
-
existing = true;
|
|
7971
|
-
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
7972
|
-
const existingPages = new Set(existingEntries.map((l) => {
|
|
7973
|
-
const m = l.match(/\[\[([^\]]+)\]\]/);
|
|
7974
|
-
return m ? m[1] : "";
|
|
7975
|
-
}));
|
|
7976
|
-
const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
|
|
7977
|
-
stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
|
|
7978
|
-
} catch {
|
|
7979
|
-
}
|
|
7980
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7981
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
7982
|
-
for (const e of entries) {
|
|
7983
|
-
const group = e.type;
|
|
7984
|
-
if (!grouped.has(group)) grouped.set(group, []);
|
|
7985
|
-
grouped.get(group).push(e);
|
|
7986
|
-
}
|
|
7987
|
-
let body = `# Knowledge Index: ${slug}
|
|
7988
|
-
|
|
7989
|
-
Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
7990
|
-
|
|
7991
|
-
`;
|
|
7992
|
-
for (const [type, items] of grouped) {
|
|
7993
|
-
body += `## ${type}
|
|
7994
|
-
|
|
7995
|
-
`;
|
|
7996
|
-
for (const item of items) {
|
|
7997
|
-
const pageRef = item.page.replace(/\.md$/, "");
|
|
7998
|
-
body += `- [[${pageRef}]] \u2014 ${item.title}
|
|
7999
|
-
`;
|
|
8000
|
-
}
|
|
8001
|
-
body += "\n";
|
|
8002
|
-
}
|
|
8003
|
-
if (entries.length === 0) {
|
|
8004
|
-
body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
|
|
8005
|
-
`;
|
|
8006
|
-
}
|
|
8007
|
-
if (input.apply) {
|
|
8008
|
-
try {
|
|
8009
|
-
await mkdir5(dirname8(indexPath), { recursive: true });
|
|
8010
|
-
await writeFile5(indexPath, body, "utf8");
|
|
8011
|
-
} catch (e) {
|
|
8012
|
-
return {
|
|
8013
|
-
exitCode: ExitCode.WRITE_FAILED,
|
|
8014
|
-
result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
|
|
8015
|
-
};
|
|
8016
|
-
}
|
|
8017
|
-
}
|
|
8018
|
-
const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
|
|
8019
|
-
const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
|
|
8020
|
-
return {
|
|
8021
|
-
exitCode: ExitCode.OK,
|
|
8022
|
-
result: ok({
|
|
8023
|
-
slug,
|
|
8024
|
-
entries,
|
|
8025
|
-
existing,
|
|
8026
|
-
stale,
|
|
8027
|
-
index_path: `projects/${slug}/knowledge.md`,
|
|
8028
|
-
humanHint: `project: ${slug}
|
|
8029
|
-
entries: ${entries.length}${staleHint}
|
|
8030
|
-
${action}
|
|
8031
|
-
|
|
8032
|
-
${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
|
|
8033
|
-
})
|
|
8034
|
-
};
|
|
8035
|
-
}
|
|
8036
|
-
|
|
8037
6914
|
// src/commands/observe.ts
|
|
8038
|
-
import { mkdir as
|
|
8039
|
-
import { existsSync as
|
|
8040
|
-
import { join as
|
|
8041
|
-
import { createHash as
|
|
6915
|
+
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
6916
|
+
import { existsSync as existsSync14, statSync as statSync3 } from "fs";
|
|
6917
|
+
import { join as join25 } from "path";
|
|
6918
|
+
import { createHash as createHash6 } from "crypto";
|
|
8042
6919
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
8043
6920
|
function slugify(text) {
|
|
8044
6921
|
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -8060,15 +6937,15 @@ async function runObserve(input) {
|
|
|
8060
6937
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
8061
6938
|
};
|
|
8062
6939
|
}
|
|
8063
|
-
if (!
|
|
6940
|
+
if (!existsSync14(input.vault) || !statSync3(input.vault).isDirectory()) {
|
|
8064
6941
|
return {
|
|
8065
6942
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
8066
6943
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
8067
6944
|
};
|
|
8068
6945
|
}
|
|
8069
|
-
const transcriptsDir =
|
|
6946
|
+
const transcriptsDir = join25(input.vault, "raw", "transcripts");
|
|
8070
6947
|
try {
|
|
8071
|
-
await
|
|
6948
|
+
await mkdir5(transcriptsDir, { recursive: true });
|
|
8072
6949
|
} catch {
|
|
8073
6950
|
return {
|
|
8074
6951
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
@@ -8078,11 +6955,11 @@ async function runObserve(input) {
|
|
|
8078
6955
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8079
6956
|
const slug = slugify(input.text);
|
|
8080
6957
|
const fileName = `${today}-observation-${slug}.md`;
|
|
8081
|
-
const filePath =
|
|
6958
|
+
const filePath = join25(transcriptsDir, fileName);
|
|
8082
6959
|
const body = `
|
|
8083
6960
|
${input.text.trim()}
|
|
8084
6961
|
`;
|
|
8085
|
-
const sha256 =
|
|
6962
|
+
const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
8086
6963
|
const frontmatterLines = [
|
|
8087
6964
|
"---",
|
|
8088
6965
|
"source_url:",
|
|
@@ -8096,7 +6973,7 @@ ${input.text.trim()}
|
|
|
8096
6973
|
frontmatterLines.push("---");
|
|
8097
6974
|
const content = frontmatterLines.join("\n") + body;
|
|
8098
6975
|
try {
|
|
8099
|
-
await
|
|
6976
|
+
await writeFile4(filePath, content, "utf8");
|
|
8100
6977
|
} catch (e) {
|
|
8101
6978
|
return {
|
|
8102
6979
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -8118,9 +6995,9 @@ ${input.text.trim()}
|
|
|
8118
6995
|
}
|
|
8119
6996
|
|
|
8120
6997
|
// src/commands/memory.ts
|
|
8121
|
-
import { createHash as
|
|
8122
|
-
import { mkdir as
|
|
8123
|
-
import { basename as
|
|
6998
|
+
import { createHash as createHash7 } from "crypto";
|
|
6999
|
+
import { mkdir as mkdir6, readFile as readFile16, readdir as readdir4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
|
|
7000
|
+
import { basename as basename2, extname, join as join26, relative as relative3, sep as sep3 } from "path";
|
|
8124
7001
|
async function runMemoryTopics(input) {
|
|
8125
7002
|
const scan = await scanVault(input.vault);
|
|
8126
7003
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -8184,9 +7061,9 @@ async function runMemoryIndex(input) {
|
|
|
8184
7061
|
}
|
|
8185
7062
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8186
7063
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
8187
|
-
const absCachePath =
|
|
8188
|
-
await
|
|
8189
|
-
await
|
|
7064
|
+
const absCachePath = join26(input.vault, relCachePath);
|
|
7065
|
+
await mkdir6(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
7066
|
+
await writeFile5(absCachePath, `${JSON.stringify({
|
|
8190
7067
|
generated_at: generatedAt,
|
|
8191
7068
|
project: input.project,
|
|
8192
7069
|
topics: state.topics,
|
|
@@ -8377,7 +7254,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
8377
7254
|
}
|
|
8378
7255
|
async function checkMemoryIndex(vault, project, current) {
|
|
8379
7256
|
const relCachePath = memoryCacheRelPath(project);
|
|
8380
|
-
const cacheText = await readIfExists2(
|
|
7257
|
+
const cacheText = await readIfExists2(join26(vault, relCachePath));
|
|
8381
7258
|
if (!cacheText) {
|
|
8382
7259
|
return {
|
|
8383
7260
|
ok: true,
|
|
@@ -8494,7 +7371,7 @@ async function readMemoryPage(page, project, warnings) {
|
|
|
8494
7371
|
title,
|
|
8495
7372
|
summary: summarize(body),
|
|
8496
7373
|
updated,
|
|
8497
|
-
hash:
|
|
7374
|
+
hash: createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
8498
7375
|
topics,
|
|
8499
7376
|
project,
|
|
8500
7377
|
...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
|
|
@@ -8770,23 +7647,23 @@ function renderMemoryIndexStatusHint(status) {
|
|
|
8770
7647
|
}
|
|
8771
7648
|
async function readIfExists2(path) {
|
|
8772
7649
|
try {
|
|
8773
|
-
return await
|
|
7650
|
+
return await readFile16(path, "utf8");
|
|
8774
7651
|
} catch {
|
|
8775
7652
|
return "";
|
|
8776
7653
|
}
|
|
8777
7654
|
}
|
|
8778
7655
|
async function collectImportFiles(source) {
|
|
8779
|
-
const st = await
|
|
7656
|
+
const st = await stat5(source);
|
|
8780
7657
|
if (st.isFile()) return isImportCandidate(source) ? [source] : [];
|
|
8781
7658
|
const files = [];
|
|
8782
7659
|
await walkImportFiles(source, files);
|
|
8783
7660
|
return files.sort((a, b) => a.localeCompare(b));
|
|
8784
7661
|
}
|
|
8785
7662
|
async function walkImportFiles(dir, out) {
|
|
8786
|
-
const entries = await
|
|
7663
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
8787
7664
|
for (const entry of entries) {
|
|
8788
7665
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
8789
|
-
const path =
|
|
7666
|
+
const path = join26(dir, entry.name);
|
|
8790
7667
|
if (entry.isDirectory()) {
|
|
8791
7668
|
await walkImportFiles(path, out);
|
|
8792
7669
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -8799,9 +7676,9 @@ function isImportCandidate(path) {
|
|
|
8799
7676
|
return ext === ".md" || ext === ".txt";
|
|
8800
7677
|
}
|
|
8801
7678
|
async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
8802
|
-
const st = await
|
|
7679
|
+
const st = await stat5(file);
|
|
8803
7680
|
const sourceKind = classifyImportSource(file);
|
|
8804
|
-
const hash =
|
|
7681
|
+
const hash = createHash7("sha256").update(await readFile16(file)).digest("hex");
|
|
8805
7682
|
const baseEntry = {
|
|
8806
7683
|
source_path: file,
|
|
8807
7684
|
source_kind: sourceKind,
|
|
@@ -8824,7 +7701,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8824
7701
|
reason: "policy_source_not_imported"
|
|
8825
7702
|
};
|
|
8826
7703
|
}
|
|
8827
|
-
const text = await
|
|
7704
|
+
const text = await readFile16(file, "utf8");
|
|
8828
7705
|
const extracted = extractImportText(text, sourceKind);
|
|
8829
7706
|
if (!extracted) {
|
|
8830
7707
|
return {
|
|
@@ -8835,8 +7712,8 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8835
7712
|
}
|
|
8836
7713
|
const redacted = redactSensitiveContent(extracted, { file });
|
|
8837
7714
|
const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
|
|
8838
|
-
const sourceSlug = slugify2(
|
|
8839
|
-
const relSource =
|
|
7715
|
+
const sourceSlug = slugify2(basename2(file, extname(file)));
|
|
7716
|
+
const relSource = relative3(sourceRoot, file).split(sep3).join("/");
|
|
8840
7717
|
const entry = {
|
|
8841
7718
|
...baseEntry,
|
|
8842
7719
|
status: "ready",
|
|
@@ -8853,9 +7730,9 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
8853
7730
|
const content = hiddenString(entry, "__content");
|
|
8854
7731
|
const project = hiddenString(entry, "__project");
|
|
8855
7732
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
8856
|
-
const absPath =
|
|
8857
|
-
await
|
|
8858
|
-
await
|
|
7733
|
+
const absPath = join26(vault, relPath);
|
|
7734
|
+
await mkdir6(join26(vault, "raw", "transcripts"), { recursive: true });
|
|
7735
|
+
await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
8859
7736
|
const validation = await runValidate({ file: absPath });
|
|
8860
7737
|
return {
|
|
8861
7738
|
relPath,
|
|
@@ -8870,7 +7747,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
8870
7747
|
const stem = proposed.slice(0, -ext.length);
|
|
8871
7748
|
let candidate = proposed;
|
|
8872
7749
|
let i = 2;
|
|
8873
|
-
while (await readIfExists2(
|
|
7750
|
+
while (await readIfExists2(join26(vault, candidate))) {
|
|
8874
7751
|
candidate = `${stem}-${i}${ext}`;
|
|
8875
7752
|
i++;
|
|
8876
7753
|
}
|
|
@@ -8894,7 +7771,7 @@ function renderImportCapture(entry, content, project, today) {
|
|
|
8894
7771
|
`source_paths: ["${entry.source_path.replaceAll('"', '\\"')}"]`,
|
|
8895
7772
|
"---",
|
|
8896
7773
|
"",
|
|
8897
|
-
`# Imported Memory: ${
|
|
7774
|
+
`# Imported Memory: ${basename2(entry.source_path, extname(entry.source_path))}`,
|
|
8898
7775
|
"",
|
|
8899
7776
|
`Source kind: ${entry.source_kind}`,
|
|
8900
7777
|
"",
|
|
@@ -8908,8 +7785,8 @@ function hiddenString(entry, key) {
|
|
|
8908
7785
|
return entry[key] ?? "";
|
|
8909
7786
|
}
|
|
8910
7787
|
function classifyImportSource(file) {
|
|
8911
|
-
const rel = file.split(
|
|
8912
|
-
const name =
|
|
7788
|
+
const rel = file.split(sep3).join("/");
|
|
7789
|
+
const name = basename2(file);
|
|
8913
7790
|
if (rel.includes("/.codex/memories/")) return "codex-memory";
|
|
8914
7791
|
if (rel.includes("/.codex/rules/")) return "codex-rule";
|
|
8915
7792
|
if (rel.includes("/.claude/") && name === "napkin.md") return "napkin";
|
|
@@ -9042,10 +7919,10 @@ function memoryCacheRelPath(project) {
|
|
|
9042
7919
|
}
|
|
9043
7920
|
async function readMemoryCache(vault, project) {
|
|
9044
7921
|
if (project) {
|
|
9045
|
-
const projectCache = await readIfExists2(
|
|
7922
|
+
const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
|
|
9046
7923
|
if (projectCache) return projectCache;
|
|
9047
7924
|
}
|
|
9048
|
-
return readIfExists2(
|
|
7925
|
+
return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
|
|
9049
7926
|
}
|
|
9050
7927
|
function dedupePages(pages) {
|
|
9051
7928
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -9131,8 +8008,8 @@ function slugify2(value) {
|
|
|
9131
8008
|
}
|
|
9132
8009
|
|
|
9133
8010
|
// src/commands/query.ts
|
|
9134
|
-
import { readFile as
|
|
9135
|
-
import { join as
|
|
8011
|
+
import { readFile as readFile17, stat as stat6 } from "fs/promises";
|
|
8012
|
+
import { join as join27 } from "path";
|
|
9136
8013
|
var W_KEYWORD = 2;
|
|
9137
8014
|
var W_SOURCE_OVERLAP = 4;
|
|
9138
8015
|
var W_WIKILINK = 3;
|
|
@@ -9185,11 +8062,11 @@ async function runQuery(input) {
|
|
|
9185
8062
|
);
|
|
9186
8063
|
const results = pages.map((page) => {
|
|
9187
8064
|
const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
|
|
9188
|
-
const
|
|
8065
|
+
const wikilink = scoreWikilink(page.relPath, seedPaths, graph);
|
|
9189
8066
|
const aa = scoreAdamicAdar(page.relPath, seedPaths, graph);
|
|
9190
8067
|
const typeAffinity = scoreTypeAffinity(page.type, queryTerms);
|
|
9191
8068
|
const isSeed = page.keywordScore > 0;
|
|
9192
|
-
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP +
|
|
8069
|
+
const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink * W_WIKILINK + aa * W_ADAMIC_ADAR;
|
|
9193
8070
|
const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
|
|
9194
8071
|
return {
|
|
9195
8072
|
path: page.relPath,
|
|
@@ -9253,10 +8130,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
9253
8130
|
return score;
|
|
9254
8131
|
}
|
|
9255
8132
|
async function loadOrBuildGraph(vault) {
|
|
9256
|
-
const graphPath =
|
|
8133
|
+
const graphPath = join27(vault, ".skillwiki", "graph.json");
|
|
9257
8134
|
let needsBuild = false;
|
|
9258
8135
|
try {
|
|
9259
|
-
const fileStat = await
|
|
8136
|
+
const fileStat = await stat6(graphPath);
|
|
9260
8137
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
9261
8138
|
if (ageHours > 24) needsBuild = true;
|
|
9262
8139
|
} catch {
|
|
@@ -9267,7 +8144,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
9267
8144
|
if (buildResult.exitCode !== 0) return null;
|
|
9268
8145
|
}
|
|
9269
8146
|
try {
|
|
9270
|
-
const raw = await
|
|
8147
|
+
const raw = await readFile17(graphPath, "utf8");
|
|
9271
8148
|
return JSON.parse(raw);
|
|
9272
8149
|
} catch {
|
|
9273
8150
|
return null;
|
|
@@ -9279,30 +8156,30 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
9279
8156
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9280
8157
|
|
|
9281
8158
|
// src/mcp/tools.ts
|
|
9282
|
-
import { z
|
|
8159
|
+
import { z } from "zod";
|
|
9283
8160
|
|
|
9284
8161
|
// src/mcp/vault-resolve.ts
|
|
9285
|
-
import { join as
|
|
8162
|
+
import { join as join28, resolve as resolve7 } from "path";
|
|
9286
8163
|
|
|
9287
8164
|
// src/mcp/allowlist.ts
|
|
9288
|
-
import { resolve as
|
|
9289
|
-
import { realpathSync
|
|
8165
|
+
import { resolve as resolve6, sep as sep4 } from "path";
|
|
8166
|
+
import { realpathSync } from "fs";
|
|
9290
8167
|
function parseVaultAllowlist(envValue) {
|
|
9291
8168
|
if (envValue === void 0 || envValue.trim() === "") return null;
|
|
9292
|
-
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) =>
|
|
8169
|
+
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve6(p));
|
|
9293
8170
|
}
|
|
9294
8171
|
function vaultAllowedByList(vaultPath, allowlist) {
|
|
9295
8172
|
if (!allowlist || allowlist.length === 0) return true;
|
|
9296
8173
|
let canonical = vaultPath;
|
|
9297
8174
|
try {
|
|
9298
|
-
canonical =
|
|
8175
|
+
canonical = realpathSync(vaultPath);
|
|
9299
8176
|
} catch {
|
|
9300
|
-
canonical =
|
|
8177
|
+
canonical = resolve6(vaultPath);
|
|
9301
8178
|
}
|
|
9302
|
-
const resolved =
|
|
8179
|
+
const resolved = resolve6(canonical);
|
|
9303
8180
|
return allowlist.some((root) => {
|
|
9304
|
-
const r =
|
|
9305
|
-
return resolved === r || resolved.startsWith(r +
|
|
8181
|
+
const r = resolve6(root);
|
|
8182
|
+
return resolved === r || resolved.startsWith(r + sep4);
|
|
9306
8183
|
});
|
|
9307
8184
|
}
|
|
9308
8185
|
function getVaultAllowlistFromEnv() {
|
|
@@ -9315,7 +8192,7 @@ async function resolveMcpVault(input) {
|
|
|
9315
8192
|
let vaultPath;
|
|
9316
8193
|
let source = "resolved";
|
|
9317
8194
|
if (input.vault !== void 0 && input.vault.length > 0) {
|
|
9318
|
-
vaultPath =
|
|
8195
|
+
vaultPath = resolve7(input.vault);
|
|
9319
8196
|
source = "flag";
|
|
9320
8197
|
} else {
|
|
9321
8198
|
const r = await resolveRuntimePath({
|
|
@@ -9327,7 +8204,7 @@ async function resolveMcpVault(input) {
|
|
|
9327
8204
|
cwd: input.cwd ?? process.cwd()
|
|
9328
8205
|
});
|
|
9329
8206
|
if (!r.ok) return r;
|
|
9330
|
-
vaultPath =
|
|
8207
|
+
vaultPath = resolve7(r.data.path);
|
|
9331
8208
|
source = r.data.source;
|
|
9332
8209
|
}
|
|
9333
8210
|
const scan = await scanVault(vaultPath);
|
|
@@ -9344,7 +8221,7 @@ async function resolveMcpVault(input) {
|
|
|
9344
8221
|
return ok({ vault: vaultPath, source });
|
|
9345
8222
|
}
|
|
9346
8223
|
function defaultGraphOut(vault) {
|
|
9347
|
-
return
|
|
8224
|
+
return join28(vault, ".skillwiki", "graph.json");
|
|
9348
8225
|
}
|
|
9349
8226
|
|
|
9350
8227
|
// src/mcp/result-format.ts
|
|
@@ -9361,7 +8238,7 @@ function formatToolResult(payload) {
|
|
|
9361
8238
|
// src/mcp/audit-log.ts
|
|
9362
8239
|
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
9363
8240
|
import { homedir } from "os";
|
|
9364
|
-
import { join as
|
|
8241
|
+
import { join as join29 } from "path";
|
|
9365
8242
|
function auditEnabled() {
|
|
9366
8243
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
9367
8244
|
if (v === "0" || v === "false") return false;
|
|
@@ -9373,7 +8250,7 @@ function auditSink() {
|
|
|
9373
8250
|
function auditFilePath() {
|
|
9374
8251
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
9375
8252
|
if (custom && custom.length > 0) return custom;
|
|
9376
|
-
return
|
|
8253
|
+
return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
9377
8254
|
}
|
|
9378
8255
|
function auditMcpToolCall(entry) {
|
|
9379
8256
|
if (!auditEnabled()) return;
|
|
@@ -9383,7 +8260,7 @@ function auditMcpToolCall(entry) {
|
|
|
9383
8260
|
return;
|
|
9384
8261
|
}
|
|
9385
8262
|
const path = auditFilePath();
|
|
9386
|
-
mkdirSync5(
|
|
8263
|
+
mkdirSync5(join29(path, ".."), { recursive: true });
|
|
9387
8264
|
appendFileSync(path, line, "utf8");
|
|
9388
8265
|
}
|
|
9389
8266
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -9412,18 +8289,18 @@ async function runMcpToolHandler(tool, input, fn) {
|
|
|
9412
8289
|
|
|
9413
8290
|
// src/mcp/tools.ts
|
|
9414
8291
|
var vaultFields = {
|
|
9415
|
-
vault:
|
|
9416
|
-
wiki:
|
|
8292
|
+
vault: z.string().optional().describe("Vault root directory; omitted = resolve WIKI_PATH / default ~/wiki"),
|
|
8293
|
+
wiki: z.string().optional().describe("Wiki profile name for vault resolution")
|
|
9417
8294
|
};
|
|
9418
8295
|
function registerMcpTools(server) {
|
|
9419
8296
|
server.registerTool(
|
|
9420
8297
|
"skillwiki.query",
|
|
9421
8298
|
{
|
|
9422
8299
|
description: "Ranked vault query over typed knowledge (read-only). Returns Result envelope JSON.",
|
|
9423
|
-
inputSchema:
|
|
8300
|
+
inputSchema: z.object({
|
|
9424
8301
|
...vaultFields,
|
|
9425
|
-
text:
|
|
9426
|
-
limit:
|
|
8302
|
+
text: z.string().min(1).describe("Query text"),
|
|
8303
|
+
limit: z.number().int().positive().optional().describe("Max results (default 10)")
|
|
9427
8304
|
})
|
|
9428
8305
|
},
|
|
9429
8306
|
async ({ vault, wiki, text, limit }) => runMcpToolHandler("skillwiki.query", { vault, wiki }, async () => {
|
|
@@ -9437,13 +8314,13 @@ function registerMcpTools(server) {
|
|
|
9437
8314
|
"skillwiki.lint_summary",
|
|
9438
8315
|
{
|
|
9439
8316
|
description: "Vault lint bucket summary (read-only, no --fix). Returns Result envelope JSON.",
|
|
9440
|
-
inputSchema:
|
|
8317
|
+
inputSchema: z.object({
|
|
9441
8318
|
...vaultFields,
|
|
9442
|
-
only:
|
|
9443
|
-
examplesLimit:
|
|
9444
|
-
days:
|
|
9445
|
-
lines:
|
|
9446
|
-
logThreshold:
|
|
8319
|
+
only: z.string().optional().describe("Run a single lint bucket"),
|
|
8320
|
+
examplesLimit: z.number().int().nonnegative().optional().describe("Examples per bucket in summary (default 3)"),
|
|
8321
|
+
days: z.number().int().positive().optional().describe("Stale threshold days (default 90)"),
|
|
8322
|
+
lines: z.number().int().positive().optional().describe("Pagesize threshold lines (default 200)"),
|
|
8323
|
+
logThreshold: z.number().int().positive().optional().describe("Log rotation threshold (default 500)")
|
|
9447
8324
|
})
|
|
9448
8325
|
},
|
|
9449
8326
|
async (args) => runMcpToolHandler("skillwiki.lint_summary", { vault: args.vault, wiki: args.wiki }, async () => {
|
|
@@ -9467,7 +8344,7 @@ function registerMcpTools(server) {
|
|
|
9467
8344
|
"skillwiki.doctor",
|
|
9468
8345
|
{
|
|
9469
8346
|
description: "Diagnose skillwiki setup, vault path, sync, and plugin channels (read-only).",
|
|
9470
|
-
inputSchema:
|
|
8347
|
+
inputSchema: z.object({
|
|
9471
8348
|
...vaultFields
|
|
9472
8349
|
})
|
|
9473
8350
|
},
|
|
@@ -9488,9 +8365,9 @@ function registerMcpTools(server) {
|
|
|
9488
8365
|
"skillwiki.graph_build",
|
|
9489
8366
|
{
|
|
9490
8367
|
description: "Build wikilink graph JSON under .skillwiki/graph.json (writes graph file only).",
|
|
9491
|
-
inputSchema:
|
|
8368
|
+
inputSchema: z.object({
|
|
9492
8369
|
...vaultFields,
|
|
9493
|
-
out:
|
|
8370
|
+
out: z.string().optional().describe("Output path (default <vault>/.skillwiki/graph.json)")
|
|
9494
8371
|
})
|
|
9495
8372
|
},
|
|
9496
8373
|
async ({ vault, wiki, out }) => runMcpToolHandler("skillwiki.graph_build", { vault, wiki }, async () => {
|
|
@@ -9505,9 +8382,9 @@ function registerMcpTools(server) {
|
|
|
9505
8382
|
"skillwiki.project_index",
|
|
9506
8383
|
{
|
|
9507
8384
|
description: "List project index entries for a slug (read-only, apply=false).",
|
|
9508
|
-
inputSchema:
|
|
8385
|
+
inputSchema: z.object({
|
|
9509
8386
|
...vaultFields,
|
|
9510
|
-
slug:
|
|
8387
|
+
slug: z.string().min(1).describe("Project slug under projects/{slug}/")
|
|
9511
8388
|
})
|
|
9512
8389
|
},
|
|
9513
8390
|
async ({ vault, wiki, slug }) => runMcpToolHandler("skillwiki.project_index", { vault, wiki }, async () => {
|
|
@@ -9521,10 +8398,10 @@ function registerMcpTools(server) {
|
|
|
9521
8398
|
"skillwiki.stale",
|
|
9522
8399
|
{
|
|
9523
8400
|
description: "List stale pages, transcripts, and incomplete work items (read-only).",
|
|
9524
|
-
inputSchema:
|
|
8401
|
+
inputSchema: z.object({
|
|
9525
8402
|
...vaultFields,
|
|
9526
|
-
days:
|
|
9527
|
-
project:
|
|
8403
|
+
days: z.number().int().positive().optional().describe("Stale age threshold (default 90)"),
|
|
8404
|
+
project: z.string().optional().describe("Scope to one project slug")
|
|
9528
8405
|
})
|
|
9529
8406
|
},
|
|
9530
8407
|
async ({ vault, wiki, days, project }) => runMcpToolHandler("skillwiki.stale", { vault, wiki }, async () => {
|
|
@@ -9543,8 +8420,8 @@ function registerMcpTools(server) {
|
|
|
9543
8420
|
"skillwiki.config_get",
|
|
9544
8421
|
{
|
|
9545
8422
|
description: "Read a single skillwiki config key from ~/.skillwiki/.env (read-only).",
|
|
9546
|
-
inputSchema:
|
|
9547
|
-
key:
|
|
8423
|
+
inputSchema: z.object({
|
|
8424
|
+
key: z.string().min(1).describe("Config key (e.g. WIKI_PATH or profile key)")
|
|
9548
8425
|
})
|
|
9549
8426
|
},
|
|
9550
8427
|
async ({ key }) => runMcpToolHandler("skillwiki.config_get", {}, async () => {
|
|
@@ -9555,7 +8432,7 @@ function registerMcpTools(server) {
|
|
|
9555
8432
|
}
|
|
9556
8433
|
|
|
9557
8434
|
// src/mcp/mutating-tools.ts
|
|
9558
|
-
import { z as
|
|
8435
|
+
import { z as z2 } from "zod";
|
|
9559
8436
|
|
|
9560
8437
|
// src/mcp/mutation-gate.ts
|
|
9561
8438
|
function mcpMutationsEnabled() {
|
|
@@ -9566,20 +8443,20 @@ var MCP_MUTATION_DISABLED_MESSAGE = "Mutating MCP tools are disabled. Set SKILLW
|
|
|
9566
8443
|
|
|
9567
8444
|
// src/mcp/mutating-tools.ts
|
|
9568
8445
|
var vaultFields2 = {
|
|
9569
|
-
vault:
|
|
9570
|
-
wiki:
|
|
8446
|
+
vault: z2.string().optional(),
|
|
8447
|
+
wiki: z2.string().optional()
|
|
9571
8448
|
};
|
|
9572
8449
|
function registerMcpMutatingTools(server) {
|
|
9573
8450
|
server.registerTool(
|
|
9574
8451
|
"skillwiki.observe",
|
|
9575
8452
|
{
|
|
9576
8453
|
description: "Create a new raw/transcripts capture file (mutating). Requires SKILLWIKI_MCP_ALLOW_MUTATIONS=true.",
|
|
9577
|
-
inputSchema:
|
|
8454
|
+
inputSchema: z2.object({
|
|
9578
8455
|
...vaultFields2,
|
|
9579
|
-
text:
|
|
9580
|
-
kind:
|
|
9581
|
-
project:
|
|
9582
|
-
confirm_mutation:
|
|
8456
|
+
text: z2.string().min(1).describe("Capture body text"),
|
|
8457
|
+
kind: z2.enum(["note", "bug", "task", "idea", "session-log"]).optional(),
|
|
8458
|
+
project: z2.string().optional().describe("Project slug for frontmatter"),
|
|
8459
|
+
confirm_mutation: z2.literal(true).describe("Must be true to acknowledge vault write")
|
|
9583
8460
|
})
|
|
9584
8461
|
},
|
|
9585
8462
|
async (args) => runMcpToolHandler("skillwiki.observe", { vault: args.vault, wiki: args.wiki }, async () => {
|
|
@@ -9603,8 +8480,8 @@ function registerMcpMutatingTools(server) {
|
|
|
9603
8480
|
}
|
|
9604
8481
|
|
|
9605
8482
|
// src/mcp/resources.ts
|
|
9606
|
-
import { readFile as
|
|
9607
|
-
import { join as
|
|
8483
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
8484
|
+
import { join as join31 } from "path";
|
|
9608
8485
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9609
8486
|
|
|
9610
8487
|
// src/mcp/lint-bucket.ts
|
|
@@ -9732,9 +8609,9 @@ async function fetchQueryPreview(input) {
|
|
|
9732
8609
|
}
|
|
9733
8610
|
|
|
9734
8611
|
// src/mcp/graph-html.ts
|
|
9735
|
-
import { readFile as
|
|
9736
|
-
import { join as
|
|
9737
|
-
import { existsSync as
|
|
8612
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
8613
|
+
import { join as join30 } from "path";
|
|
8614
|
+
import { existsSync as existsSync15 } from "fs";
|
|
9738
8615
|
var TYPE_COLORS = {
|
|
9739
8616
|
entities: "#e74c3c",
|
|
9740
8617
|
concepts: "#27ae60",
|
|
@@ -9802,9 +8679,9 @@ ${nodeSvg}
|
|
|
9802
8679
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
9803
8680
|
}
|
|
9804
8681
|
async function fetchGraphHtmlReport(input) {
|
|
9805
|
-
const graphPath = input.graphPath ??
|
|
8682
|
+
const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
|
|
9806
8683
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
9807
|
-
if (!
|
|
8684
|
+
if (!existsSync15(graphPath)) {
|
|
9808
8685
|
return {
|
|
9809
8686
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
9810
8687
|
result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
|
|
@@ -9812,7 +8689,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
9812
8689
|
}
|
|
9813
8690
|
let raw;
|
|
9814
8691
|
try {
|
|
9815
|
-
raw = await
|
|
8692
|
+
raw = await readFile18(graphPath, "utf8");
|
|
9816
8693
|
} catch (e) {
|
|
9817
8694
|
return {
|
|
9818
8695
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -9876,7 +8753,7 @@ async function fetchStaleSummary(input) {
|
|
|
9876
8753
|
|
|
9877
8754
|
// src/mcp/resources.ts
|
|
9878
8755
|
async function readVaultFile(vault, rel) {
|
|
9879
|
-
return
|
|
8756
|
+
return readFile19(join31(vault, rel), "utf8");
|
|
9880
8757
|
}
|
|
9881
8758
|
async function tailLines(text, lines) {
|
|
9882
8759
|
const parts = text.split(/\r?\n/);
|
|
@@ -9962,9 +8839,9 @@ function registerMcpResources(server) {
|
|
|
9962
8839
|
if (!v.ok) {
|
|
9963
8840
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
9964
8841
|
}
|
|
9965
|
-
const path =
|
|
8842
|
+
const path = join31(v.data.vault, ".skillwiki", "graph.json");
|
|
9966
8843
|
try {
|
|
9967
|
-
const raw = await
|
|
8844
|
+
const raw = await readFile19(path, "utf8");
|
|
9968
8845
|
const graph = JSON.parse(raw);
|
|
9969
8846
|
const adjacency = graph.adjacency ?? {};
|
|
9970
8847
|
const nodes = Object.keys(adjacency);
|
|
@@ -10138,14 +9015,14 @@ function registerMcpResources(server) {
|
|
|
10138
9015
|
}
|
|
10139
9016
|
|
|
10140
9017
|
// src/mcp/prompts.ts
|
|
10141
|
-
import { z as
|
|
9018
|
+
import { z as z3 } from "zod";
|
|
10142
9019
|
function registerMcpPrompts(server) {
|
|
10143
9020
|
server.registerPrompt(
|
|
10144
9021
|
"skillwiki-research-query",
|
|
10145
9022
|
{
|
|
10146
9023
|
description: "Structured vault research using skillwiki.query and typed pages",
|
|
10147
9024
|
argsSchema: {
|
|
10148
|
-
topic:
|
|
9025
|
+
topic: z3.string().describe("Research topic or question")
|
|
10149
9026
|
}
|
|
10150
9027
|
},
|
|
10151
9028
|
({ topic }) => ({
|
|
@@ -10176,8 +9053,8 @@ function registerMcpPrompts(server) {
|
|
|
10176
9053
|
{
|
|
10177
9054
|
description: "Plan a project work item using vault project workspace conventions",
|
|
10178
9055
|
argsSchema: {
|
|
10179
|
-
slug:
|
|
10180
|
-
idea:
|
|
9056
|
+
slug: z3.string().describe("Project slug"),
|
|
9057
|
+
idea: z3.string().describe("Work item idea or bug/feature summary")
|
|
10181
9058
|
}
|
|
10182
9059
|
},
|
|
10183
9060
|
({ slug, idea }) => ({
|
|
@@ -10205,7 +9082,7 @@ function registerMcpPrompts(server) {
|
|
|
10205
9082
|
{
|
|
10206
9083
|
description: "Review vault health via lint summary, doctor, and stale tools",
|
|
10207
9084
|
argsSchema: {
|
|
10208
|
-
vault:
|
|
9085
|
+
vault: z3.string().optional().describe("Optional vault path")
|
|
10209
9086
|
}
|
|
10210
9087
|
},
|
|
10211
9088
|
({ vault }) => ({
|
|
@@ -10231,7 +9108,7 @@ function registerMcpPrompts(server) {
|
|
|
10231
9108
|
{
|
|
10232
9109
|
description: "Audit citation health using query + lint buckets",
|
|
10233
9110
|
argsSchema: {
|
|
10234
|
-
focus:
|
|
9111
|
+
focus: z3.string().optional().describe("Page or topic focus")
|
|
10235
9112
|
}
|
|
10236
9113
|
},
|
|
10237
9114
|
({ focus }) => ({
|
|
@@ -10274,20 +9151,9 @@ async function runSkillwikiMcpStdio() {
|
|
|
10274
9151
|
}
|
|
10275
9152
|
|
|
10276
9153
|
export {
|
|
10277
|
-
ExitCode,
|
|
10278
|
-
ok,
|
|
10279
|
-
err,
|
|
10280
|
-
RawSourceSchema,
|
|
10281
|
-
MetaSchema,
|
|
10282
|
-
isBlockedHost,
|
|
10283
|
-
splitFrontmatter,
|
|
10284
|
-
extractFrontmatter,
|
|
10285
|
-
scanSensitiveContent,
|
|
10286
|
-
redactSensitiveContent,
|
|
10287
9154
|
readLastOp,
|
|
10288
9155
|
appendLastOp,
|
|
10289
9156
|
clearLastOp,
|
|
10290
|
-
atomicWriteText,
|
|
10291
9157
|
runLogAppend,
|
|
10292
9158
|
renderIndexUpsert,
|
|
10293
9159
|
upsertIndexEntry,
|
|
@@ -10298,16 +9164,8 @@ export {
|
|
|
10298
9164
|
releaseLock,
|
|
10299
9165
|
acquireOwnedSyncLock,
|
|
10300
9166
|
releaseOwnedSyncLock,
|
|
10301
|
-
assertTargetInsideVault,
|
|
10302
|
-
prepareTypedPage,
|
|
10303
9167
|
runValidate,
|
|
10304
|
-
scanVault,
|
|
10305
|
-
readPage,
|
|
10306
9168
|
runGraphBuild,
|
|
10307
|
-
profileKey,
|
|
10308
|
-
parseDotenvText,
|
|
10309
|
-
parseDotenvFile,
|
|
10310
|
-
writeDotenv,
|
|
10311
9169
|
resolveInitTimePath,
|
|
10312
9170
|
resolveRuntimePath,
|
|
10313
9171
|
runOrphans,
|
|
@@ -10317,9 +9175,12 @@ export {
|
|
|
10317
9175
|
extractTaxonomy,
|
|
10318
9176
|
taxonomyCommentForPage,
|
|
10319
9177
|
reconcileTaxonomyDocument,
|
|
9178
|
+
mergeTaxonomyConflict,
|
|
10320
9179
|
runLinks,
|
|
10321
9180
|
runTagAudit,
|
|
10322
9181
|
runIndexCheck,
|
|
9182
|
+
renderProjectIndex,
|
|
9183
|
+
runProjectIndex,
|
|
10323
9184
|
runStale,
|
|
10324
9185
|
runPagesize,
|
|
10325
9186
|
runLogRotate,
|
|
@@ -10343,13 +9204,6 @@ export {
|
|
|
10343
9204
|
runConfigPath,
|
|
10344
9205
|
buildDegradedReasons,
|
|
10345
9206
|
probeRemoteHealth,
|
|
10346
|
-
FLEET_REL_PATH,
|
|
10347
|
-
runFleetValidate,
|
|
10348
|
-
runFleetContext,
|
|
10349
|
-
loadFleetManifestAndHost,
|
|
10350
|
-
snapshotterAliasForLocalHost,
|
|
10351
|
-
loadFleetManifest,
|
|
10352
|
-
resolveFleetHostId,
|
|
10353
9207
|
SATELLITE_STALE_MS,
|
|
10354
9208
|
satelliteLatestRunPath,
|
|
10355
9209
|
isFailedRunStatus,
|
|
@@ -10357,7 +9211,6 @@ export {
|
|
|
10357
9211
|
evaluateSatelliteRunHealth,
|
|
10358
9212
|
runDoctor,
|
|
10359
9213
|
readCliPackageJson,
|
|
10360
|
-
runProjectIndex,
|
|
10361
9214
|
runObserve,
|
|
10362
9215
|
runMemoryTopics,
|
|
10363
9216
|
runMemoryIndex,
|