@fenglimg/fabric-server 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-U3IQH5H6.js +851 -0
- package/dist/{http-3BNWQWPP.js → http-FL3MB4L2.js} +45 -422
- package/dist/index.d.ts +61 -1
- package/dist/index.js +154 -38
- package/dist/static/assets/index-BiK8yn_c.js +5 -0
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
- package/dist/chunk-KZO24GUQ.js +0 -199
- package/dist/static/assets/index-D_EcxWYV.js +0 -5
|
@@ -9,398 +9,19 @@ import {
|
|
|
9
9
|
readHumanLock,
|
|
10
10
|
readHumanLockDocument,
|
|
11
11
|
readHumanLockEntry,
|
|
12
|
-
readLedger
|
|
13
|
-
|
|
12
|
+
readLedger,
|
|
13
|
+
runDoctorReport
|
|
14
|
+
} from "./chunk-U3IQH5H6.js";
|
|
14
15
|
|
|
15
16
|
// src/http.ts
|
|
16
17
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
17
|
-
import { appendFile, readFile as
|
|
18
|
-
import { join as
|
|
18
|
+
import { appendFile, readFile as readFile2 } from "fs/promises";
|
|
19
|
+
import { join as join3 } from "path";
|
|
19
20
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
20
21
|
import {
|
|
21
22
|
StreamableHTTPServerTransport
|
|
22
23
|
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
24
|
|
|
24
|
-
// src/services/doctor.ts
|
|
25
|
-
import { createHash } from "crypto";
|
|
26
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
27
|
-
import { readFile } from "fs/promises";
|
|
28
|
-
import { isAbsolute, join, posix, resolve } from "path";
|
|
29
|
-
import { detectFramework, forensicReportSchema } from "@fenglimg/fabric-shared";
|
|
30
|
-
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
31
|
-
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
32
|
-
".fabric",
|
|
33
|
-
".git",
|
|
34
|
-
".next",
|
|
35
|
-
".turbo",
|
|
36
|
-
"Library",
|
|
37
|
-
"Temp",
|
|
38
|
-
"build",
|
|
39
|
-
"coverage",
|
|
40
|
-
"dist",
|
|
41
|
-
"node_modules"
|
|
42
|
-
]);
|
|
43
|
-
var LEDGER_WARN_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
44
|
-
var LEDGER_ERROR_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
45
|
-
async function runDoctorReport(target) {
|
|
46
|
-
const projectRoot = normalizeTarget(target);
|
|
47
|
-
const framework = detectFramework(projectRoot);
|
|
48
|
-
const entryPoints = collectEntryPoints(projectRoot);
|
|
49
|
-
const [savedForensic, metaSnapshot, humanLockSnapshot, ledgerSnapshot] = await Promise.all([
|
|
50
|
-
readSavedForensic(projectRoot),
|
|
51
|
-
inspectMetaRevision(projectRoot),
|
|
52
|
-
inspectHumanLock(projectRoot),
|
|
53
|
-
inspectLedger(projectRoot)
|
|
54
|
-
]);
|
|
55
|
-
const checks = [
|
|
56
|
-
createForensicCheck(savedForensic, framework, entryPoints),
|
|
57
|
-
createFrameworkCheck(savedForensic, framework, entryPoints),
|
|
58
|
-
createMetaRevisionCheck(metaSnapshot),
|
|
59
|
-
createProtectedPathsCheck(humanLockSnapshot),
|
|
60
|
-
createLedgerCheck(ledgerSnapshot)
|
|
61
|
-
];
|
|
62
|
-
return {
|
|
63
|
-
status: reduceStatus(checks.map((check) => check.status)),
|
|
64
|
-
checks,
|
|
65
|
-
summary: {
|
|
66
|
-
target: projectRoot,
|
|
67
|
-
framework: {
|
|
68
|
-
kind: framework.kind,
|
|
69
|
-
version: framework.version,
|
|
70
|
-
subkind: framework.subkind
|
|
71
|
-
},
|
|
72
|
-
entryPoints,
|
|
73
|
-
driftCount: humanLockSnapshot.driftCount,
|
|
74
|
-
protectedPathCount: humanLockSnapshot.protectedPathCount,
|
|
75
|
-
protectedPathsIntact: humanLockSnapshot.present && humanLockSnapshot.driftCount === 0,
|
|
76
|
-
lastLedgerEntryTs: ledgerSnapshot.lastEntryTs,
|
|
77
|
-
lastLedgerEntryAgeMs: ledgerSnapshot.lastEntryAgeMs,
|
|
78
|
-
metaRevision: metaSnapshot.revision
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function createForensicCheck(forensic, framework, entryPoints) {
|
|
83
|
-
if (!forensic.present) {
|
|
84
|
-
return {
|
|
85
|
-
name: "Forensic snapshot",
|
|
86
|
-
status: "error",
|
|
87
|
-
message: `${forensic.reason} Live scan detects ${formatFramework(framework)} with ${entryPoints.length} entry point${entryPoints.length === 1 ? "" : "s"}.`
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
name: "Forensic snapshot",
|
|
92
|
-
status: "ok",
|
|
93
|
-
message: `Loaded .fabric/forensic.json for ${formatFramework(forensic.report.framework)} with ${forensic.report.entry_points.length} recorded entry point${forensic.report.entry_points.length === 1 ? "" : "s"}.`
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
function createFrameworkCheck(forensic, framework, entryPoints) {
|
|
97
|
-
if (framework.kind === "unknown") {
|
|
98
|
-
return {
|
|
99
|
-
name: "Framework fingerprint",
|
|
100
|
-
status: "warn",
|
|
101
|
-
message: "Unable to identify the project framework from current files."
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (!forensic.present) {
|
|
105
|
-
return {
|
|
106
|
-
name: "Framework fingerprint",
|
|
107
|
-
status: "warn",
|
|
108
|
-
message: `Live detection sees ${formatFramework(framework)} and ${entryPoints.length} entry point${entryPoints.length === 1 ? "" : "s"}, but no forensic baseline exists yet.`
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const matches = forensic.report.framework.kind === framework.kind && forensic.report.framework.version === framework.version && forensic.report.framework.subkind === framework.subkind;
|
|
112
|
-
if (!matches) {
|
|
113
|
-
return {
|
|
114
|
-
name: "Framework fingerprint",
|
|
115
|
-
status: "warn",
|
|
116
|
-
message: `Forensic baseline says ${formatFramework(forensic.report.framework)}; live scan says ${formatFramework(framework)}.`
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
name: "Framework fingerprint",
|
|
121
|
-
status: "ok",
|
|
122
|
-
message: `Framework baseline matches live scan: ${formatFramework(framework)} \xB7 ${entryPoints.length} current entry point${entryPoints.length === 1 ? "" : "s"}.`
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function createMetaRevisionCheck(snapshot) {
|
|
126
|
-
if (!snapshot.present) {
|
|
127
|
-
return {
|
|
128
|
-
name: "Meta revision",
|
|
129
|
-
status: "error",
|
|
130
|
-
message: snapshot.unexpectedError ?? "agents.meta.json is missing."
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
if (snapshot.driftCount > 0 || snapshot.missingFiles.length > 0) {
|
|
134
|
-
const parts = [
|
|
135
|
-
`${snapshot.driftCount} tracked AGENTS file drift`,
|
|
136
|
-
snapshot.missingFiles.length > 0 ? `${snapshot.missingFiles.length} missing tracked file` : null
|
|
137
|
-
].filter((part) => part !== null);
|
|
138
|
-
return {
|
|
139
|
-
name: "Meta revision",
|
|
140
|
-
status: "error",
|
|
141
|
-
message: `agents.meta.json revision ${snapshot.revision} is stale: ${parts.join(" \xB7 ")}.`
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
name: "Meta revision",
|
|
146
|
-
status: "ok",
|
|
147
|
-
message: `agents.meta.json revision ${snapshot.revision} matches ${snapshot.nodeCount} tracked AGENTS file${snapshot.nodeCount === 1 ? "" : "s"}.`
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function createProtectedPathsCheck(snapshot) {
|
|
151
|
-
if (!snapshot.present) {
|
|
152
|
-
return {
|
|
153
|
-
name: "Protected paths",
|
|
154
|
-
status: "warn",
|
|
155
|
-
message: snapshot.reason
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (snapshot.driftCount > 0) {
|
|
159
|
-
return {
|
|
160
|
-
name: "Protected paths",
|
|
161
|
-
status: "warn",
|
|
162
|
-
message: `${snapshot.driftCount} of ${snapshot.protectedPathCount} protected path${snapshot.protectedPathCount === 1 ? "" : "s"} drifted from approved hashes.`
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
name: "Protected paths",
|
|
167
|
-
status: "ok",
|
|
168
|
-
message: `${snapshot.protectedPathCount} protected path${snapshot.protectedPathCount === 1 ? "" : "s"} intact with zero hash drift.`
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function createLedgerCheck(snapshot) {
|
|
172
|
-
if (snapshot.lastEntryTs === null || snapshot.lastEntryAgeMs === null) {
|
|
173
|
-
return {
|
|
174
|
-
name: "Intent ledger",
|
|
175
|
-
status: "warn",
|
|
176
|
-
message: "No ledger entries recorded yet."
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (snapshot.lastEntryAgeMs >= LEDGER_ERROR_AFTER_MS) {
|
|
180
|
-
return {
|
|
181
|
-
name: "Intent ledger",
|
|
182
|
-
status: "error",
|
|
183
|
-
message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${new Date(snapshot.lastEntryTs).toISOString()}).`
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
if (snapshot.lastEntryAgeMs >= LEDGER_WARN_AFTER_MS) {
|
|
187
|
-
return {
|
|
188
|
-
name: "Intent ledger",
|
|
189
|
-
status: "warn",
|
|
190
|
-
message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${new Date(snapshot.lastEntryTs).toISOString()}).`
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
name: "Intent ledger",
|
|
195
|
-
status: "ok",
|
|
196
|
-
message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${snapshot.count} total entr${snapshot.count === 1 ? "y" : "ies"}).`
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
async function readSavedForensic(projectRoot) {
|
|
200
|
-
const forensicPath = join(projectRoot, ".fabric", "forensic.json");
|
|
201
|
-
try {
|
|
202
|
-
const raw = await readFile(forensicPath, "utf8");
|
|
203
|
-
const parsed = forensicReportSchema.safeParse(JSON.parse(raw));
|
|
204
|
-
if (!parsed.success) {
|
|
205
|
-
return {
|
|
206
|
-
present: false,
|
|
207
|
-
reason: "forensic.json is invalid."
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
present: true,
|
|
212
|
-
report: parsed.data
|
|
213
|
-
};
|
|
214
|
-
} catch (error) {
|
|
215
|
-
if (isMissingFileError(error)) {
|
|
216
|
-
return {
|
|
217
|
-
present: false,
|
|
218
|
-
reason: ".fabric/forensic.json is missing."
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
present: false,
|
|
223
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function inspectMetaRevision(projectRoot) {
|
|
228
|
-
try {
|
|
229
|
-
const meta = readAgentsMeta(projectRoot);
|
|
230
|
-
const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
|
|
231
|
-
const missingFiles = [];
|
|
232
|
-
let driftCount = 0;
|
|
233
|
-
const revisionSource = entries.map(([, node]) => {
|
|
234
|
-
const absolutePath = join(projectRoot, node.file);
|
|
235
|
-
if (!existsSync(absolutePath)) {
|
|
236
|
-
missingFiles.push(node.file);
|
|
237
|
-
driftCount += 1;
|
|
238
|
-
return "missing";
|
|
239
|
-
}
|
|
240
|
-
const actualHash = sha256(readFileSync(absolutePath, "utf8"));
|
|
241
|
-
if (actualHash !== node.hash) {
|
|
242
|
-
driftCount += 1;
|
|
243
|
-
}
|
|
244
|
-
return actualHash;
|
|
245
|
-
}).join("");
|
|
246
|
-
const revision = sha256(revisionSource);
|
|
247
|
-
return {
|
|
248
|
-
present: true,
|
|
249
|
-
revision: meta.revision,
|
|
250
|
-
nodeCount: entries.length,
|
|
251
|
-
driftCount: revision === meta.revision ? driftCount : Math.max(driftCount, 1),
|
|
252
|
-
missingFiles
|
|
253
|
-
};
|
|
254
|
-
} catch (error) {
|
|
255
|
-
return {
|
|
256
|
-
present: false,
|
|
257
|
-
revision: null,
|
|
258
|
-
nodeCount: 0,
|
|
259
|
-
driftCount: 0,
|
|
260
|
-
missingFiles: [],
|
|
261
|
-
unexpectedError: error instanceof Error ? error.message : String(error)
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
async function inspectHumanLock(projectRoot) {
|
|
266
|
-
try {
|
|
267
|
-
const entries = await readHumanLock(projectRoot);
|
|
268
|
-
return {
|
|
269
|
-
present: true,
|
|
270
|
-
driftCount: entries.filter((entry) => entry.drift).length,
|
|
271
|
-
protectedPathCount: entries.length
|
|
272
|
-
};
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (isMissingFileError(error)) {
|
|
275
|
-
return {
|
|
276
|
-
present: false,
|
|
277
|
-
driftCount: 0,
|
|
278
|
-
protectedPathCount: 0,
|
|
279
|
-
reason: ".fabric/human-lock.json is missing; no protected paths are being tracked."
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
present: false,
|
|
284
|
-
driftCount: 0,
|
|
285
|
-
protectedPathCount: 0,
|
|
286
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
async function inspectLedger(projectRoot) {
|
|
291
|
-
const entries = await readLedger(projectRoot);
|
|
292
|
-
const lastEntry = entries.reduce(
|
|
293
|
-
(latest, entry) => latest === null || entry.ts > latest ? entry.ts : latest,
|
|
294
|
-
null
|
|
295
|
-
);
|
|
296
|
-
return {
|
|
297
|
-
count: entries.length,
|
|
298
|
-
lastEntryTs: lastEntry,
|
|
299
|
-
lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0)
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function normalizeTarget(targetInput) {
|
|
303
|
-
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
304
|
-
}
|
|
305
|
-
function collectEntryPoints(root) {
|
|
306
|
-
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
307
|
-
return [];
|
|
308
|
-
}
|
|
309
|
-
const entries = [];
|
|
310
|
-
const stack = [root];
|
|
311
|
-
while (stack.length > 0) {
|
|
312
|
-
const current = stack.pop();
|
|
313
|
-
if (current === void 0) {
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
317
|
-
const absolutePath = join(current, entry.name);
|
|
318
|
-
const relativePath = posix.normalize(absolutePath.slice(root.length + 1).split("\\").join("/"));
|
|
319
|
-
if (relativePath.length === 0) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (entry.isDirectory()) {
|
|
323
|
-
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
stack.push(absolutePath);
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (!entry.isFile()) {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const reason = getEntryPointReason(relativePath);
|
|
333
|
-
if (reason !== null) {
|
|
334
|
-
entries.push({
|
|
335
|
-
path: relativePath,
|
|
336
|
-
reason
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
342
|
-
}
|
|
343
|
-
function getEntryPointReason(relativePath) {
|
|
344
|
-
const extension = relativePath.slice(relativePath.lastIndexOf("."));
|
|
345
|
-
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
const directory = posix.dirname(relativePath);
|
|
349
|
-
const fileName = posix.basename(relativePath);
|
|
350
|
-
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
351
|
-
if (directory === "assets/scripts" || directory === "scripts") {
|
|
352
|
-
return "top-level script";
|
|
353
|
-
}
|
|
354
|
-
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
355
|
-
return "application entry";
|
|
356
|
-
}
|
|
357
|
-
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
358
|
-
return "next app route";
|
|
359
|
-
}
|
|
360
|
-
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
361
|
-
return "next page route";
|
|
362
|
-
}
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
function reduceStatus(statuses) {
|
|
366
|
-
if (statuses.includes("error")) {
|
|
367
|
-
return "error";
|
|
368
|
-
}
|
|
369
|
-
if (statuses.includes("warn")) {
|
|
370
|
-
return "warn";
|
|
371
|
-
}
|
|
372
|
-
return "ok";
|
|
373
|
-
}
|
|
374
|
-
function formatFramework(framework) {
|
|
375
|
-
const pieces = [framework.kind, framework.version !== "unknown" ? framework.version : null, framework.subkind].filter((piece) => piece !== null && piece !== "unknown");
|
|
376
|
-
return pieces.length > 0 ? pieces.join(" \xB7 ") : "unknown";
|
|
377
|
-
}
|
|
378
|
-
function formatAge(ageMs) {
|
|
379
|
-
const seconds = Math.floor(ageMs / 1e3);
|
|
380
|
-
if (seconds < 60) {
|
|
381
|
-
return `${seconds}s`;
|
|
382
|
-
}
|
|
383
|
-
const minutes = Math.floor(seconds / 60);
|
|
384
|
-
if (minutes < 60) {
|
|
385
|
-
return `${minutes}m`;
|
|
386
|
-
}
|
|
387
|
-
const hours = Math.floor(minutes / 60);
|
|
388
|
-
if (hours < 48) {
|
|
389
|
-
return `${hours}h`;
|
|
390
|
-
}
|
|
391
|
-
const days = Math.floor(hours / 24);
|
|
392
|
-
if (days < 14) {
|
|
393
|
-
return `${days}d`;
|
|
394
|
-
}
|
|
395
|
-
return `${Math.floor(days / 7)}w`;
|
|
396
|
-
}
|
|
397
|
-
function sha256(content) {
|
|
398
|
-
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
399
|
-
}
|
|
400
|
-
function isMissingFileError(error) {
|
|
401
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
402
|
-
}
|
|
403
|
-
|
|
404
25
|
// src/api/_error.ts
|
|
405
26
|
function sendError(res, status, code, message, details) {
|
|
406
27
|
const payload = {
|
|
@@ -490,13 +111,13 @@ function registerDoctorApi(app, projectRoot) {
|
|
|
490
111
|
}
|
|
491
112
|
|
|
492
113
|
// src/api/events.ts
|
|
493
|
-
import { createHash
|
|
494
|
-
import { open, readFile
|
|
495
|
-
import { join
|
|
114
|
+
import { createHash, randomUUID } from "crypto";
|
|
115
|
+
import { open, readFile, stat } from "fs/promises";
|
|
116
|
+
import { join } from "path";
|
|
496
117
|
import {
|
|
497
118
|
agentsMetaSchema,
|
|
498
119
|
fabricEventSchema,
|
|
499
|
-
forensicReportSchema
|
|
120
|
+
forensicReportSchema,
|
|
500
121
|
humanLockFileSchema,
|
|
501
122
|
ledgerEntrySchema
|
|
502
123
|
} from "@fenglimg/fabric-shared";
|
|
@@ -576,7 +197,7 @@ async function ensureWatcher(state, projectRoot) {
|
|
|
576
197
|
if (state.watcher !== void 0) {
|
|
577
198
|
return;
|
|
578
199
|
}
|
|
579
|
-
state.ledgerOffset = await readFileSize(
|
|
200
|
+
state.ledgerOffset = await readFileSize(join(projectRoot, LEDGER_PATH));
|
|
580
201
|
state.ledgerRemainder = "";
|
|
581
202
|
state.humanLockSnapshot = await readHumanLockSnapshot(projectRoot);
|
|
582
203
|
const watcher = chokidar.watch([...WATCHED_PATHS], {
|
|
@@ -645,7 +266,7 @@ async function readEventsForFile(state, projectRoot, relativePath) {
|
|
|
645
266
|
return [];
|
|
646
267
|
}
|
|
647
268
|
async function readMetaUpdatedEvent(projectRoot) {
|
|
648
|
-
const filePath =
|
|
269
|
+
const filePath = join(projectRoot, AGENTS_META_PATH);
|
|
649
270
|
const raw = await readUtf8File(filePath);
|
|
650
271
|
if (raw === null) {
|
|
651
272
|
return null;
|
|
@@ -657,12 +278,12 @@ async function readMetaUpdatedEvent(projectRoot) {
|
|
|
657
278
|
};
|
|
658
279
|
}
|
|
659
280
|
async function readDriftDetectedEvent(projectRoot) {
|
|
660
|
-
const filePath =
|
|
281
|
+
const filePath = join(projectRoot, FORENSIC_PATH);
|
|
661
282
|
const raw = await readUtf8File(filePath);
|
|
662
283
|
if (raw === null) {
|
|
663
284
|
return null;
|
|
664
285
|
}
|
|
665
|
-
const parsed =
|
|
286
|
+
const parsed = forensicReportSchema.parse(JSON.parse(raw));
|
|
666
287
|
return {
|
|
667
288
|
type: "drift:detected",
|
|
668
289
|
payload: parsed
|
|
@@ -703,7 +324,7 @@ async function readHumanLockEvents(state, projectRoot) {
|
|
|
703
324
|
return events;
|
|
704
325
|
}
|
|
705
326
|
async function readLedgerAppendedEvents(state, projectRoot) {
|
|
706
|
-
const ledgerPath =
|
|
327
|
+
const ledgerPath = join(projectRoot, LEDGER_PATH);
|
|
707
328
|
const nextSize = await readFileSize(ledgerPath);
|
|
708
329
|
if (nextSize < state.ledgerOffset) {
|
|
709
330
|
state.ledgerOffset = 0;
|
|
@@ -772,7 +393,7 @@ data: ${JSON.stringify(payload)}
|
|
|
772
393
|
}
|
|
773
394
|
}
|
|
774
395
|
async function readHumanLockSnapshot(projectRoot) {
|
|
775
|
-
const humanLockPath =
|
|
396
|
+
const humanLockPath = join(projectRoot, HUMAN_LOCK_PATH);
|
|
776
397
|
const raw = await readUtf8File(humanLockPath);
|
|
777
398
|
if (raw === null) {
|
|
778
399
|
return createEmptyHumanLockSnapshot();
|
|
@@ -793,7 +414,7 @@ async function readActualHumanLockHashes(projectRoot, locked) {
|
|
|
793
414
|
const uniqueFiles = Array.from(new Set(locked.map((entry) => entry.file)));
|
|
794
415
|
const fileContents = await Promise.all(
|
|
795
416
|
uniqueFiles.map(async (file) => {
|
|
796
|
-
const raw = await readUtf8File(
|
|
417
|
+
const raw = await readUtf8File(join(projectRoot, file));
|
|
797
418
|
return [file, raw];
|
|
798
419
|
})
|
|
799
420
|
);
|
|
@@ -808,7 +429,7 @@ async function readActualHumanLockHashes(projectRoot, locked) {
|
|
|
808
429
|
function hashLockedContent(content, entry) {
|
|
809
430
|
const lines = content.split(/\r?\n/);
|
|
810
431
|
const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
|
|
811
|
-
return `sha256:${
|
|
432
|
+
return `sha256:${createHash("sha256").update(slice).digest("hex")}`;
|
|
812
433
|
}
|
|
813
434
|
function getHumanLockKey(entry) {
|
|
814
435
|
return `${entry.file}:${entry.start_line}:${entry.end_line}`;
|
|
@@ -838,7 +459,7 @@ function normalizePath(value) {
|
|
|
838
459
|
}
|
|
839
460
|
async function readUtf8File(path) {
|
|
840
461
|
try {
|
|
841
|
-
return await
|
|
462
|
+
return await readFile(path, "utf8");
|
|
842
463
|
} catch (error) {
|
|
843
464
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
844
465
|
return null;
|
|
@@ -985,6 +606,8 @@ function buildLedgerFallbackMeta(entries) {
|
|
|
985
606
|
scope_glob: affectedPath,
|
|
986
607
|
deps: [],
|
|
987
608
|
priority: "medium",
|
|
609
|
+
layer: "L2",
|
|
610
|
+
topology_type: "mirror",
|
|
988
611
|
hash: `replayed:${hashBase ?? entry.id}`
|
|
989
612
|
};
|
|
990
613
|
}
|
|
@@ -1241,9 +864,9 @@ function registerRulesApi(app, projectRoot) {
|
|
|
1241
864
|
}
|
|
1242
865
|
|
|
1243
866
|
// src/api/scan.ts
|
|
1244
|
-
import { existsSync
|
|
1245
|
-
import { isAbsolute
|
|
1246
|
-
import { detectFramework
|
|
867
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
868
|
+
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
869
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1247
870
|
var DEFAULT_IGNORES = [
|
|
1248
871
|
"**/*.meta",
|
|
1249
872
|
"library/**",
|
|
@@ -1266,11 +889,11 @@ function registerScanApi(app, projectRoot) {
|
|
|
1266
889
|
});
|
|
1267
890
|
}
|
|
1268
891
|
function createScanReport(targetInput = process.cwd()) {
|
|
1269
|
-
const target =
|
|
1270
|
-
const framework =
|
|
892
|
+
const target = normalizeTarget(targetInput);
|
|
893
|
+
const framework = detectFramework(target);
|
|
1271
894
|
const readmeQuality = getReadmeQuality(target);
|
|
1272
|
-
const hasContributing =
|
|
1273
|
-
const hasExistingFabric =
|
|
895
|
+
const hasContributing = existsSync(join2(target, "CONTRIBUTING.md"));
|
|
896
|
+
const hasExistingFabric = existsSync(join2(target, "AGENTS.md")) || existsSync(join2(target, ".fabric"));
|
|
1274
897
|
const walkResult = walkFiles(target, DEFAULT_IGNORES);
|
|
1275
898
|
return {
|
|
1276
899
|
target,
|
|
@@ -1288,19 +911,19 @@ function createScanReport(targetInput = process.cwd()) {
|
|
|
1288
911
|
})
|
|
1289
912
|
};
|
|
1290
913
|
}
|
|
1291
|
-
function
|
|
1292
|
-
return
|
|
914
|
+
function normalizeTarget(targetInput) {
|
|
915
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
1293
916
|
}
|
|
1294
917
|
function getReadmeQuality(target) {
|
|
1295
|
-
const readmePath =
|
|
1296
|
-
if (!
|
|
918
|
+
const readmePath = join2(target, "README.md");
|
|
919
|
+
if (!existsSync(readmePath)) {
|
|
1297
920
|
return "stub";
|
|
1298
921
|
}
|
|
1299
|
-
const wordCount =
|
|
922
|
+
const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
|
|
1300
923
|
return wordCount >= 200 ? "ok" : "stub";
|
|
1301
924
|
}
|
|
1302
925
|
function walkFiles(root, ignorePatterns) {
|
|
1303
|
-
if (!
|
|
926
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
1304
927
|
throw new Error(`Target must be an existing directory: ${root}`);
|
|
1305
928
|
}
|
|
1306
929
|
let fileCount = 0;
|
|
@@ -1311,8 +934,8 @@ function walkFiles(root, ignorePatterns) {
|
|
|
1311
934
|
if (current === void 0) {
|
|
1312
935
|
continue;
|
|
1313
936
|
}
|
|
1314
|
-
for (const entry of
|
|
1315
|
-
const absolutePath =
|
|
937
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
938
|
+
const absolutePath = join2(current, entry.name);
|
|
1316
939
|
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
1317
940
|
if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
|
|
1318
941
|
ignoredCount += 1;
|
|
@@ -1364,18 +987,18 @@ function buildRecommendations(input) {
|
|
|
1364
987
|
}
|
|
1365
988
|
|
|
1366
989
|
// src/api/static.ts
|
|
1367
|
-
import { existsSync as
|
|
1368
|
-
import { dirname, resolve as
|
|
990
|
+
import { existsSync as existsSync2 } from "fs";
|
|
991
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
1369
992
|
import { fileURLToPath } from "url";
|
|
1370
993
|
import express from "express";
|
|
1371
|
-
var DEFAULT_STATIC_DIR =
|
|
994
|
+
var DEFAULT_STATIC_DIR = resolve2(dirname(fileURLToPath(import.meta.url)), "static");
|
|
1372
995
|
function registerDashboardStatic(app, options = {}) {
|
|
1373
996
|
if (options.dev ?? process.env.NODE_ENV === "development") {
|
|
1374
997
|
return;
|
|
1375
998
|
}
|
|
1376
|
-
const staticDir =
|
|
1377
|
-
const indexPath =
|
|
1378
|
-
if (!
|
|
999
|
+
const staticDir = resolve2(options.dashboardDistPath ?? DEFAULT_STATIC_DIR);
|
|
1000
|
+
const indexPath = resolve2(staticDir, "index.html");
|
|
1001
|
+
if (!existsSync2(indexPath)) {
|
|
1379
1002
|
warnMissingDashboard(staticDir);
|
|
1380
1003
|
app.get("/", (_req, res) => {
|
|
1381
1004
|
res.status(404).json({
|
|
@@ -1400,7 +1023,7 @@ function warnMissingDashboard(staticDir) {
|
|
|
1400
1023
|
}
|
|
1401
1024
|
|
|
1402
1025
|
// src/middleware/bearer-auth.ts
|
|
1403
|
-
import { createHash as
|
|
1026
|
+
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
1404
1027
|
function createBearerAuthMiddleware(token) {
|
|
1405
1028
|
const expectedDigest = hashToken(token);
|
|
1406
1029
|
return function bearerAuthMiddleware(req, res, next) {
|
|
@@ -1433,7 +1056,7 @@ function tokensMatch(token, expectedDigest) {
|
|
|
1433
1056
|
return timingSafeEqual(hashToken(token), expectedDigest);
|
|
1434
1057
|
}
|
|
1435
1058
|
function hashToken(token) {
|
|
1436
|
-
return
|
|
1059
|
+
return createHash2("sha256").update(token, "utf8").digest();
|
|
1437
1060
|
}
|
|
1438
1061
|
|
|
1439
1062
|
// src/http.ts
|
|
@@ -1481,7 +1104,7 @@ var JsonlEventStore = class {
|
|
|
1481
1104
|
async readEvents() {
|
|
1482
1105
|
let raw;
|
|
1483
1106
|
try {
|
|
1484
|
-
raw = await
|
|
1107
|
+
raw = await readFile2(this.ledgerPath, "utf8");
|
|
1485
1108
|
} catch (error) {
|
|
1486
1109
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
1487
1110
|
return [];
|
|
@@ -1494,7 +1117,7 @@ var JsonlEventStore = class {
|
|
|
1494
1117
|
function createFabricHttpApp(options) {
|
|
1495
1118
|
const { projectRoot, host = DEFAULT_HOST, authToken, dashboardDistPath, dev } = options;
|
|
1496
1119
|
const app = createMcpExpressApp({ host });
|
|
1497
|
-
const ledgerPath =
|
|
1120
|
+
const ledgerPath = join3(projectRoot, LEDGER_FILE);
|
|
1498
1121
|
const eventStore = new JsonlEventStore(ledgerPath);
|
|
1499
1122
|
const sessions = /* @__PURE__ */ new Map();
|
|
1500
1123
|
process.env.FABRIC_PROJECT_ROOT = projectRoot;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
import { Server } from 'node:http';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { AuditMode } from '@fenglimg/fabric-shared';
|
|
4
|
+
|
|
5
|
+
type DoctorStatus = "ok" | "warn" | "error";
|
|
6
|
+
type DoctorCheck = {
|
|
7
|
+
name: string;
|
|
8
|
+
status: DoctorStatus;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
type DoctorSummary = {
|
|
12
|
+
target: string;
|
|
13
|
+
framework: {
|
|
14
|
+
kind: string;
|
|
15
|
+
version: string;
|
|
16
|
+
subkind: string;
|
|
17
|
+
};
|
|
18
|
+
entryPoints: Array<{
|
|
19
|
+
path: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}>;
|
|
22
|
+
driftCount: number;
|
|
23
|
+
protectedPathCount: number;
|
|
24
|
+
protectedPathsIntact: boolean;
|
|
25
|
+
lastLedgerEntryTs: number | null;
|
|
26
|
+
lastLedgerEntryAgeMs: number | null;
|
|
27
|
+
metaRevision: string | null;
|
|
28
|
+
audit: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
mode: AuditMode;
|
|
31
|
+
checkedPathCount: number;
|
|
32
|
+
violationCount: number;
|
|
33
|
+
windowMs: number;
|
|
34
|
+
} | null;
|
|
35
|
+
};
|
|
36
|
+
type DoctorReport = {
|
|
37
|
+
status: DoctorStatus;
|
|
38
|
+
checks: DoctorCheck[];
|
|
39
|
+
summary: DoctorSummary;
|
|
40
|
+
audit: DoctorAuditReport | null;
|
|
41
|
+
};
|
|
42
|
+
type DoctorAuditViolation = {
|
|
43
|
+
editTs: number;
|
|
44
|
+
entryId: string;
|
|
45
|
+
intent: string;
|
|
46
|
+
lastGetRulesTs: number | null;
|
|
47
|
+
path: string;
|
|
48
|
+
};
|
|
49
|
+
type DoctorAuditReport = {
|
|
50
|
+
mode: AuditMode;
|
|
51
|
+
skipped: boolean;
|
|
52
|
+
windowMs: number;
|
|
53
|
+
checkedPathCount: number;
|
|
54
|
+
violationCount: number;
|
|
55
|
+
violations: DoctorAuditViolation[];
|
|
56
|
+
};
|
|
57
|
+
declare function runDoctorReport(target: string): Promise<DoctorReport>;
|
|
58
|
+
declare function runDoctorAuditReport(target: string, options?: {
|
|
59
|
+
force?: boolean;
|
|
60
|
+
mode?: AuditMode;
|
|
61
|
+
windowMs?: number;
|
|
62
|
+
}): Promise<DoctorAuditReport>;
|
|
3
63
|
|
|
4
64
|
declare function createFabricServer(): McpServer;
|
|
5
65
|
declare function startStdioServer(): Promise<void>;
|
|
@@ -12,4 +72,4 @@ declare function startHttpServer(options: {
|
|
|
12
72
|
dev?: boolean;
|
|
13
73
|
}): Promise<Server>;
|
|
14
74
|
|
|
15
|
-
export { createFabricServer, startHttpServer, startStdioServer };
|
|
75
|
+
export { type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
|