@fenglimg/fabric-server 2.0.0-rc.36 → 2.0.0-rc.38

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.
@@ -1,1286 +0,0 @@
1
- import {
2
- EVENT_LEDGER_PATH,
3
- LEDGER_PATH,
4
- LEGACY_LEDGER_PATH,
5
- appendEventLedgerEvent,
6
- contextCache,
7
- getEventLedgerPath,
8
- getKnowledge,
9
- getLedgerPath,
10
- getLegacyLedgerPath,
11
- invalidateKnowledgeSyncCooldown,
12
- isNodeError,
13
- readAgentsMeta,
14
- readEventLedger,
15
- runDoctorReport,
16
- sha256
17
- } from "./chunk-5XXH2VZZ.js";
18
-
19
- // src/http.ts
20
- import { randomUUID as randomUUID2 } from "crypto";
21
- import { readFile as readFile3 } from "fs/promises";
22
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
23
- import {
24
- StreamableHTTPServerTransport
25
- } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
26
- import chokidar2 from "chokidar";
27
-
28
- // src/api/_error.ts
29
- import { FabricError } from "@fenglimg/fabric-shared/errors";
30
- function sendError(res, status, code, message, details) {
31
- const payload = {
32
- error: {
33
- code,
34
- message
35
- }
36
- };
37
- if (details !== void 0) {
38
- payload.error.details = details;
39
- }
40
- res.status(status).json(payload);
41
- }
42
- function sendValidationError(res, message, details) {
43
- sendError(res, 400, "BAD_REQUEST", message, details);
44
- }
45
- function sendUnknownError(res, error) {
46
- const normalized = normalizeApiError(error);
47
- sendError(res, normalized.status, normalized.code, normalized.message, normalized.details);
48
- }
49
- function normalizeApiError(error) {
50
- if (error instanceof FabricError) {
51
- return {
52
- status: error.httpStatus,
53
- code: error.code,
54
- message: error.message,
55
- details: error.details
56
- };
57
- }
58
- if (error instanceof Error) {
59
- return {
60
- status: 500,
61
- code: "INTERNAL_ERROR",
62
- message: error.message
63
- };
64
- }
65
- return {
66
- status: 500,
67
- code: "INTERNAL_ERROR",
68
- message: `Unexpected error: ${String(error)}`
69
- };
70
- }
71
-
72
- // src/api/doctor.ts
73
- function registerDoctorApi(app, projectRoot) {
74
- app.get("/api/doctor", async (_req, res) => {
75
- try {
76
- res.json(await runDoctorReport(projectRoot));
77
- } catch (error) {
78
- sendUnknownError(res, error);
79
- }
80
- });
81
- }
82
-
83
- // src/api/events.ts
84
- import { open, readFile as readFile2, stat } from "fs/promises";
85
- import { join } from "path";
86
- import {
87
- agentsMetaSchema,
88
- fabricEventSchema,
89
- ledgerEntrySchema as ledgerEntrySchema2
90
- } from "@fenglimg/fabric-shared";
91
- import { eventLedgerEventSchema } from "@fenglimg/fabric-shared";
92
- import chokidar from "chokidar";
93
-
94
- // src/services/read-ledger.ts
95
- import { randomUUID } from "crypto";
96
- import { access, copyFile, readFile, rm } from "fs/promises";
97
- import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
98
- async function resolveLedgerPaths(projectRoot) {
99
- const primaryPath = getLedgerPath(projectRoot);
100
- const legacyPath = getLegacyLedgerPath(projectRoot);
101
- const [primaryExists, legacyExists] = await Promise.all([
102
- pathExists(primaryPath),
103
- pathExists(legacyPath)
104
- ]);
105
- return {
106
- primaryPath,
107
- legacyPath,
108
- readPath: primaryExists ? primaryPath : legacyPath,
109
- usingLegacy: !primaryExists && legacyExists
110
- };
111
- }
112
- async function readLedger(projectRoot, options = {}) {
113
- const [legacyEntries, eventEntries] = await Promise.all([
114
- readLegacyLedger(projectRoot),
115
- readLedgerFromEventLedger(projectRoot)
116
- ]);
117
- const entries = mergeLedgerEntries(legacyEntries, eventEntries);
118
- return entries.filter((entry) => options.source === void 0 || entry.source === options.source).filter((entry) => options.since === void 0 || entry.ts >= options.since);
119
- }
120
- async function readLegacyLedger(projectRoot) {
121
- const { readPath } = await resolveLedgerPaths(projectRoot);
122
- let raw;
123
- try {
124
- raw = await readFile(readPath, "utf8");
125
- } catch (error) {
126
- if (isNodeError(error) && error.code === "ENOENT") {
127
- return [];
128
- }
129
- throw error;
130
- }
131
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseLedgerLine(line, index)).filter((entry) => entry !== null);
132
- }
133
- function parseLedgerLine(line, index) {
134
- try {
135
- const parsed = JSON.parse(line);
136
- if (parsed.kind === "mcp-event") {
137
- return null;
138
- }
139
- const result = ledgerEntrySchema.safeParse(parsed);
140
- if (!result.success) {
141
- return null;
142
- }
143
- return {
144
- ...result.data,
145
- id: result.data.id ?? createDerivedId(index, line)
146
- };
147
- } catch {
148
- return null;
149
- }
150
- }
151
- async function readLedgerFromEventLedger(projectRoot) {
152
- const { events } = await readEventLedger(projectRoot);
153
- const grouped = /* @__PURE__ */ new Map();
154
- for (const event of events) {
155
- const entry = projectLedgerEvent(event);
156
- if (entry === null) {
157
- continue;
158
- }
159
- const existing = grouped.get(entry.id);
160
- if (existing === void 0) {
161
- grouped.set(entry.id, entry);
162
- continue;
163
- }
164
- grouped.set(entry.id, {
165
- ...existing,
166
- ts: Math.min(existing.ts, entry.ts),
167
- affected_paths: dedupeStrings([...existing.affected_paths, ...entry.affected_paths])
168
- });
169
- }
170
- return Array.from(grouped.values());
171
- }
172
- function projectLedgerEvent(event) {
173
- if (event.event_type !== "edit_intent_checked") {
174
- return null;
175
- }
176
- const base = {
177
- id: event.ledger_entry_id,
178
- ts: event.ts,
179
- intent: event.intent,
180
- affected_paths: [event.path]
181
- };
182
- if (event.ledger_source === "human") {
183
- return {
184
- ...base,
185
- source: "human",
186
- parent_sha: event.parent_sha ?? event.ledger_entry_id,
187
- parent_ledger_entry_id: event.parent_ledger_entry_id,
188
- diff_stat: event.diff_stat ?? "event-ledger",
189
- annotation: event.annotation
190
- };
191
- }
192
- return {
193
- ...base,
194
- source: "ai",
195
- commit_sha: event.commit_sha
196
- };
197
- }
198
- function mergeLedgerEntries(legacyEntries, eventEntries) {
199
- const byId = /* @__PURE__ */ new Map();
200
- for (const entry of [...legacyEntries, ...eventEntries]) {
201
- if (!byId.has(entry.id)) {
202
- byId.set(entry.id, entry);
203
- }
204
- }
205
- return Array.from(byId.values()).sort((left, right) => left.ts - right.ts);
206
- }
207
- function dedupeStrings(values) {
208
- return Array.from(new Set(values));
209
- }
210
- function createDerivedId(index, line) {
211
- return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
212
- }
213
- async function pathExists(path) {
214
- try {
215
- await access(path);
216
- return true;
217
- } catch (error) {
218
- if (isNodeError(error) && error.code === "ENOENT") {
219
- return false;
220
- }
221
- throw error;
222
- }
223
- }
224
-
225
- // src/api/events.ts
226
- var AGENTS_META_PATH = ".fabric/agents.meta.json";
227
- var WATCHED_PATHS = [
228
- AGENTS_META_PATH,
229
- EVENT_LEDGER_PATH,
230
- LEDGER_PATH,
231
- LEGACY_LEDGER_PATH
232
- ];
233
- var CONNECTION_LIMIT = 10;
234
- var HEARTBEAT_INTERVAL_MS = 3e4;
235
- var WATCH_DEBOUNCE_MS = 75;
236
- var RING_BUFFER_CAPACITY = 50;
237
- var RingBuffer = class {
238
- constructor(capacity) {
239
- this.capacity = capacity;
240
- this.buf = new Array(capacity).fill(void 0);
241
- }
242
- capacity;
243
- buf;
244
- head = 0;
245
- count = 0;
246
- push(event) {
247
- this.buf[this.head] = event;
248
- this.head = (this.head + 1) % this.capacity;
249
- if (this.count < this.capacity) {
250
- this.count++;
251
- }
252
- }
253
- replayFrom(afterId) {
254
- const result = [];
255
- const total = this.count;
256
- const start = this.count < this.capacity ? 0 : this.head;
257
- for (let i = 0; i < total; i++) {
258
- const entry = this.buf[(start + i) % this.capacity];
259
- if (entry !== void 0 && entry.id > afterId) {
260
- result.push(entry);
261
- }
262
- }
263
- return result;
264
- }
265
- };
266
- function createEventsHandler(options) {
267
- const { projectRoot } = options;
268
- const state = {
269
- clients: /* @__PURE__ */ new Set(),
270
- pendingTimers: /* @__PURE__ */ new Map(),
271
- activeLedgerPath: getLedgerPath(projectRoot),
272
- ledgerOffset: 0,
273
- ledgerRemainder: "",
274
- eventLedgerOffset: 0,
275
- eventLedgerRemainder: "",
276
- nextEventId: 1,
277
- ringBuffer: new RingBuffer(RING_BUFFER_CAPACITY)
278
- };
279
- return async function handleEvents(req, res) {
280
- if (state.clients.size >= CONNECTION_LIMIT) {
281
- res.statusCode = 503;
282
- res.setHeader("Content-Type", "application/json; charset=utf-8");
283
- res.end(
284
- JSON.stringify({
285
- error: {
286
- code: "SSE_CONNECTION_LIMIT",
287
- message: `Too many SSE clients connected. Limit: ${CONNECTION_LIMIT}.`
288
- }
289
- })
290
- );
291
- return;
292
- }
293
- await ensureWatcher(state, projectRoot);
294
- res.statusCode = 200;
295
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
296
- res.setHeader("Cache-Control", "no-cache, no-transform");
297
- res.setHeader("Connection", "keep-alive");
298
- res.setHeader("X-Accel-Buffering", "no");
299
- res.flushHeaders?.();
300
- res.write(": connected\n\n");
301
- const lastEventId = readLastEventId(req);
302
- if (lastEventId !== void 0) {
303
- const missed = state.ringBuffer.replayFrom(lastEventId);
304
- for (const entry of missed) {
305
- if (!res.writableEnded) {
306
- res.write(`id: ${entry.id}
307
- event: ${entry.type}
308
- data: ${entry.data}
309
-
310
- `);
311
- }
312
- }
313
- }
314
- state.clients.add(res);
315
- const heartbeat = setInterval(() => {
316
- if (!res.writableEnded) {
317
- res.write(": ping\n\n");
318
- }
319
- }, HEARTBEAT_INTERVAL_MS);
320
- let cleanedUp = false;
321
- const cleanup = async () => {
322
- if (cleanedUp) {
323
- return;
324
- }
325
- cleanedUp = true;
326
- clearInterval(heartbeat);
327
- state.clients.delete(res);
328
- if (state.clients.size === 0) {
329
- await stopWatcher(state);
330
- }
331
- };
332
- req.on("aborted", () => {
333
- void cleanup();
334
- });
335
- req.on("close", () => {
336
- void cleanup();
337
- });
338
- res.on("close", () => {
339
- void cleanup();
340
- });
341
- res.on("error", () => {
342
- void cleanup();
343
- });
344
- };
345
- }
346
- async function ensureWatcher(state, projectRoot) {
347
- if (state.watcher !== void 0) {
348
- return;
349
- }
350
- const ledgerState = await resolveLedgerWatchState(projectRoot);
351
- state.activeLedgerPath = ledgerState.path;
352
- state.ledgerOffset = ledgerState.size;
353
- state.ledgerRemainder = "";
354
- state.eventLedgerOffset = await readFileSize(getEventLedgerPath(projectRoot));
355
- state.eventLedgerRemainder = "";
356
- const watcher = chokidar.watch([...WATCHED_PATHS], {
357
- cwd: projectRoot,
358
- ignoreInitial: true,
359
- awaitWriteFinish: {
360
- stabilityThreshold: 120,
361
- pollInterval: 20
362
- }
363
- });
364
- watcher.on("add", (relativePath) => {
365
- scheduleFileChange(state, projectRoot, normalizePath(relativePath));
366
- });
367
- watcher.on("change", (relativePath) => {
368
- scheduleFileChange(state, projectRoot, normalizePath(relativePath));
369
- });
370
- state.watcher = watcher;
371
- }
372
- async function stopWatcher(state) {
373
- const watcher = state.watcher;
374
- if (watcher === void 0) {
375
- return;
376
- }
377
- state.watcher = void 0;
378
- for (const timer of state.pendingTimers.values()) {
379
- clearTimeout(timer);
380
- }
381
- state.pendingTimers.clear();
382
- await watcher.close();
383
- }
384
- function scheduleFileChange(state, projectRoot, relativePath) {
385
- if (!WATCHED_PATHS.includes(relativePath)) {
386
- return;
387
- }
388
- const existingTimer = state.pendingTimers.get(relativePath);
389
- if (existingTimer !== void 0) {
390
- clearTimeout(existingTimer);
391
- }
392
- const timer = setTimeout(() => {
393
- state.pendingTimers.delete(relativePath);
394
- void publishFileChange(state, projectRoot, relativePath);
395
- }, WATCH_DEBOUNCE_MS);
396
- state.pendingTimers.set(relativePath, timer);
397
- }
398
- async function publishFileChange(state, projectRoot, relativePath) {
399
- const events = await readEventsForFile(state, projectRoot, relativePath);
400
- for (const event of events) {
401
- broadcastEvent(state, event);
402
- }
403
- }
404
- async function readEventsForFile(state, projectRoot, relativePath) {
405
- if (relativePath === AGENTS_META_PATH) {
406
- const event = await readMetaUpdatedEvent(projectRoot);
407
- return event === null ? [] : [event];
408
- }
409
- if (relativePath === EVENT_LEDGER_PATH) {
410
- return await readEventLedgerAppendedEvents(state, projectRoot);
411
- }
412
- if (relativePath === LEDGER_PATH || relativePath === LEGACY_LEDGER_PATH) {
413
- return await readLedgerAppendedEvents(state, projectRoot);
414
- }
415
- return [];
416
- }
417
- async function readMetaUpdatedEvent(projectRoot) {
418
- const filePath = join(projectRoot, AGENTS_META_PATH);
419
- const raw = await readUtf8File(filePath);
420
- if (raw === null) {
421
- return null;
422
- }
423
- const parsed = agentsMetaSchema.parse(JSON.parse(raw));
424
- return {
425
- type: "meta:updated",
426
- payload: parsed
427
- };
428
- }
429
- async function readLedgerAppendedEvents(state, projectRoot) {
430
- const ledgerState = await resolveLedgerWatchState(projectRoot);
431
- const ledgerPath = ledgerState.path;
432
- const nextSize = ledgerState.size;
433
- if (ledgerPath !== state.activeLedgerPath) {
434
- state.activeLedgerPath = ledgerPath;
435
- state.ledgerOffset = 0;
436
- state.ledgerRemainder = "";
437
- }
438
- if (nextSize < state.ledgerOffset) {
439
- state.ledgerOffset = 0;
440
- state.ledgerRemainder = "";
441
- }
442
- if (nextSize === state.ledgerOffset) {
443
- return [];
444
- }
445
- const startOffset = state.ledgerOffset;
446
- state.ledgerOffset = nextSize;
447
- const handle = await open(ledgerPath, "r");
448
- try {
449
- const length = nextSize - startOffset;
450
- const buffer = Buffer.alloc(length);
451
- await handle.read(buffer, 0, length, startOffset);
452
- const chunk = `${state.ledgerRemainder}${buffer.toString("utf8")}`;
453
- const lines = chunk.split(/\r?\n/);
454
- state.ledgerRemainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
455
- return lines.map((line) => line.trim()).filter((line) => line.length > 0).map(parseLedgerAppendedEvent).filter((event) => event !== null);
456
- } finally {
457
- await handle.close();
458
- }
459
- }
460
- async function readEventLedgerAppendedEvents(state, projectRoot) {
461
- const eventLedgerPath = getEventLedgerPath(projectRoot);
462
- const nextSize = await readFileSize(eventLedgerPath);
463
- if (nextSize < state.eventLedgerOffset) {
464
- state.eventLedgerOffset = 0;
465
- state.eventLedgerRemainder = "";
466
- }
467
- if (nextSize === state.eventLedgerOffset) {
468
- return [];
469
- }
470
- const startOffset = state.eventLedgerOffset;
471
- state.eventLedgerOffset = nextSize;
472
- const handle = await open(eventLedgerPath, "r");
473
- try {
474
- const length = nextSize - startOffset;
475
- const buffer = Buffer.alloc(length);
476
- await handle.read(buffer, 0, length, startOffset);
477
- const chunk = `${state.eventLedgerRemainder}${buffer.toString("utf8")}`;
478
- const lines = chunk.split(/\r?\n/);
479
- state.eventLedgerRemainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
480
- return lines.map((line) => line.trim()).filter((line) => line.length > 0).map(parseEventLedgerAppendedEvent).filter((event) => event !== null);
481
- } finally {
482
- await handle.close();
483
- }
484
- }
485
- async function resolveLedgerWatchState(projectRoot) {
486
- const paths = await resolveLedgerPaths(projectRoot);
487
- const path = paths.usingLegacy ? paths.legacyPath : paths.primaryPath;
488
- const size = await readFileSize(path);
489
- return { path, size };
490
- }
491
- function parseLedgerAppendedEvent(line) {
492
- try {
493
- const parsed = JSON.parse(line);
494
- if (parsed.kind === "mcp-event") {
495
- return null;
496
- }
497
- const validation = ledgerEntrySchema2.safeParse(parsed);
498
- if (!validation.success) {
499
- return null;
500
- }
501
- return {
502
- type: "ledger:appended",
503
- payload: validation.data
504
- };
505
- } catch {
506
- return null;
507
- }
508
- }
509
- function parseEventLedgerAppendedEvent(line) {
510
- try {
511
- const parsed = eventLedgerEventSchema.safeParse(JSON.parse(line));
512
- if (!parsed.success || parsed.data.event_type !== "edit_intent_checked") {
513
- return null;
514
- }
515
- return {
516
- type: "ledger:appended",
517
- payload: {
518
- id: parsed.data.ledger_entry_id,
519
- ts: parsed.data.ts,
520
- source: "ai",
521
- intent: parsed.data.intent,
522
- affected_paths: [parsed.data.path]
523
- }
524
- };
525
- } catch {
526
- return null;
527
- }
528
- }
529
- function broadcastEvent(state, event) {
530
- const payload = fabricEventSchema.parse(event);
531
- const eventId = state.nextEventId++;
532
- const data = JSON.stringify(payload);
533
- const frame = `id: ${eventId}
534
- event: ${payload.type}
535
- data: ${data}
536
-
537
- `;
538
- state.ringBuffer.push({ id: eventId, type: payload.type, data });
539
- const disconnectedClients = [];
540
- for (const client of state.clients) {
541
- try {
542
- if (client.writableEnded) {
543
- disconnectedClients.push(client);
544
- continue;
545
- }
546
- client.write(frame);
547
- } catch {
548
- disconnectedClients.push(client);
549
- }
550
- }
551
- for (const client of disconnectedClients) {
552
- state.clients.delete(client);
553
- if (!client.writableEnded) {
554
- client.end();
555
- }
556
- }
557
- }
558
- function readLastEventId(req) {
559
- const header = req.headers["last-event-id"];
560
- const headerValue = Array.isArray(header) ? header[0] : header;
561
- const rawUrl = req.url ?? "";
562
- const queryStart = rawUrl.indexOf("?");
563
- const queryString = queryStart >= 0 ? rawUrl.slice(queryStart + 1) : "";
564
- const params = new URLSearchParams(queryString);
565
- const queryValue = params.get("lastEventId") ?? void 0;
566
- const raw = headerValue ?? queryValue;
567
- if (raw === void 0 || raw.length === 0) {
568
- return void 0;
569
- }
570
- const parsed = Number.parseInt(raw, 10);
571
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
572
- }
573
- function normalizePath(value) {
574
- return value.replaceAll("\\", "/");
575
- }
576
- async function readUtf8File(path) {
577
- try {
578
- return await readFile2(path, "utf8");
579
- } catch (error) {
580
- if (isNodeError2(error) && error.code === "ENOENT") {
581
- return null;
582
- }
583
- throw error;
584
- }
585
- }
586
- async function readFileSize(path) {
587
- try {
588
- const fileStat = await stat(path);
589
- return fileStat.size;
590
- } catch (error) {
591
- if (isNodeError2(error) && error.code === "ENOENT") {
592
- return 0;
593
- }
594
- throw error;
595
- }
596
- }
597
- function isNodeError2(error) {
598
- return error instanceof Error;
599
- }
600
-
601
- // src/api/history.ts
602
- import { historyStateQuerySchema } from "@fenglimg/fabric-shared";
603
-
604
- // src/services/rehydrate-state.ts
605
- import { execFile } from "child_process";
606
- import { promisify } from "util";
607
- import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
608
- import { IOFabricError, RuleError } from "@fenglimg/fabric-shared/errors";
609
- var execFileAsync = promisify(execFile);
610
- var AGENTS_META_GIT_PATH = ".fabric/agents.meta.json";
611
- var HistoryStateNotFoundError = class extends IOFabricError {
612
- code = "HISTORY_STATE_NOT_FOUND";
613
- httpStatus = 404;
614
- constructor(message, opts) {
615
- super(message, {
616
- actionHint: opts?.actionHint ?? "Ensure the ledger exists and the requested timestamp or entry ID is within its range"
617
- });
618
- }
619
- };
620
- var LedgerEntryNotFoundError = class extends RuleError {
621
- code = "LEDGER_ENTRY_NOT_FOUND";
622
- httpStatus = 404;
623
- constructor(message, opts) {
624
- super(message, {
625
- actionHint: opts?.actionHint ?? "Verify the ledger entry ID exists in the current ledger"
626
- });
627
- }
628
- };
629
- async function rehydrateAgentsMetaAt(projectRoot, target) {
630
- const ledger = await readLedger(projectRoot);
631
- const selectedIndex = resolveTargetIndex(ledger, target);
632
- const replayedEntries = ledger.slice(0, selectedIndex + 1);
633
- const selectedEntry = replayedEntries.at(-1);
634
- if (selectedEntry === void 0) {
635
- throw new HistoryStateNotFoundError(
636
- "Cannot rehydrate history state because the ledger is empty."
637
- );
638
- }
639
- const commitCandidates = collectCommitCandidates(replayedEntries);
640
- for (const commit of commitCandidates) {
641
- const meta = await tryReadAgentsMetaFromGit(projectRoot, commit);
642
- if (meta !== null) {
643
- return {
644
- meta,
645
- metadata: {
646
- at_ledger_id: selectedEntry.id,
647
- at_commit: commit,
648
- replayed_count: replayedEntries.length,
649
- mode: "git-show"
650
- },
651
- entries: replayedEntries
652
- };
653
- }
654
- }
655
- const fallbackMeta = buildLedgerFallbackMeta(replayedEntries);
656
- return {
657
- meta: fallbackMeta,
658
- metadata: {
659
- at_ledger_id: selectedEntry.id,
660
- at_commit: commitCandidates[0] ?? null,
661
- replayed_count: replayedEntries.length,
662
- mode: "ledger-fallback"
663
- },
664
- entries: replayedEntries
665
- };
666
- }
667
- function resolveTargetIndex(ledger, target) {
668
- if ("ledgerEntryId" in target) {
669
- const index = ledger.findIndex((entry) => entry.id === target.ledgerEntryId);
670
- if (index === -1) {
671
- throw new LedgerEntryNotFoundError(
672
- `Cannot find ledger entry: ${target.ledgerEntryId}`
673
- );
674
- }
675
- return index;
676
- }
677
- for (let index = ledger.length - 1; index >= 0; index -= 1) {
678
- if (ledger[index]?.ts <= target.timestamp) {
679
- return index;
680
- }
681
- }
682
- throw new HistoryStateNotFoundError(
683
- `Cannot find ledger entry at or before timestamp: ${new Date(target.timestamp).toISOString()}`
684
- );
685
- }
686
- function collectCommitCandidates(entries) {
687
- const commits = [];
688
- const seen = /* @__PURE__ */ new Set();
689
- for (let index = entries.length - 1; index >= 0; index -= 1) {
690
- const entry = entries[index];
691
- const commit = entry.source === "ai" ? entry.commit_sha : entry.parent_sha;
692
- if (typeof commit !== "string" || commit.length === 0 || commit === "root" || seen.has(commit)) {
693
- continue;
694
- }
695
- seen.add(commit);
696
- commits.push(commit);
697
- }
698
- return commits;
699
- }
700
- async function tryReadAgentsMetaFromGit(projectRoot, commit) {
701
- try {
702
- const { stdout } = await execFileAsync(
703
- "git",
704
- ["show", `${commit}:${AGENTS_META_GIT_PATH}`],
705
- {
706
- cwd: projectRoot,
707
- encoding: "utf8",
708
- maxBuffer: 1024 * 1024
709
- }
710
- );
711
- return agentsMetaSchema2.parse(JSON.parse(stdout));
712
- } catch (error) {
713
- if (isRecoverableGitError(error)) {
714
- return null;
715
- }
716
- throw error;
717
- }
718
- }
719
- function buildLedgerFallbackMeta(entries) {
720
- const nodes = entries.reduce((current, entry) => {
721
- const hashBase = entry.source === "ai" ? entry.commit_sha ?? entry.id : entry.parent_sha;
722
- for (const affectedPath of entry.affected_paths) {
723
- current[affectedPath] = {
724
- file: affectedPath,
725
- scope_glob: affectedPath,
726
- deps: [],
727
- priority: "medium",
728
- // v2.0.0-rc.30 TASK-004: dropped `layer: "L2"` — use `level` only;
729
- // AgentsMetaNode no longer carries `layer`.
730
- level: "L2",
731
- topology_type: "mirror",
732
- hash: `replayed:${hashBase ?? entry.id}`
733
- };
734
- }
735
- return current;
736
- }, {});
737
- const lastEntry = entries.at(-1);
738
- return {
739
- revision: lastEntry?.source === "ai" ? lastEntry.commit_sha ?? `replayed:${lastEntry.id ?? entries.length}` : `replayed:${lastEntry?.id ?? entries.length}`,
740
- nodes
741
- };
742
- }
743
- function isRecoverableGitError(error) {
744
- if (!(error instanceof Error)) {
745
- return false;
746
- }
747
- const nodeError = error;
748
- return nodeError.code === "ENOENT" || typeof nodeError.stderr === "string";
749
- }
750
-
751
- // src/api/history.ts
752
- function registerHistoryApi(app, projectRoot) {
753
- app.get("/api/history/state", async (req, res) => {
754
- const validation = historyStateQuerySchema.safeParse({
755
- ledger_id: req.query.ledger_id,
756
- ts: req.query.at ?? req.query.ts
757
- });
758
- if (!validation.success) {
759
- sendValidationError(res, "Invalid history replay query parameters", validation.error.flatten());
760
- return;
761
- }
762
- try {
763
- const result = "ledger_id" in validation.data && validation.data.ledger_id !== void 0 ? await rehydrateAgentsMetaAt(projectRoot, { ledgerEntryId: validation.data.ledger_id }) : await rehydrateAgentsMetaAt(projectRoot, { timestamp: validation.data.ts });
764
- res.json(result);
765
- } catch (error) {
766
- sendUnknownError(res, error);
767
- }
768
- });
769
- app.get("/api/replay", async (req, res) => {
770
- const validation = historyStateQuerySchema.safeParse({
771
- ledger_id: req.query.ledger_id,
772
- ts: req.query.at ?? req.query.ts
773
- });
774
- if (!validation.success) {
775
- sendValidationError(res, "Invalid history replay query parameters", validation.error.flatten());
776
- return;
777
- }
778
- try {
779
- const result = "ledger_id" in validation.data && validation.data.ledger_id !== void 0 ? await rehydrateAgentsMetaAt(projectRoot, { ledgerEntryId: validation.data.ledger_id }) : await rehydrateAgentsMetaAt(projectRoot, { timestamp: validation.data.ts });
780
- res.json(result);
781
- } catch (error) {
782
- sendUnknownError(res, error);
783
- }
784
- });
785
- }
786
-
787
- // src/api/ledger.ts
788
- import { ledgerQuerySchema } from "@fenglimg/fabric-shared";
789
- function registerLedgerApi(app, projectRoot) {
790
- app.get("/api/ledger", async (req, res) => {
791
- const validation = ledgerQuerySchema.safeParse({
792
- source: req.query.source,
793
- since: req.query.since
794
- });
795
- if (!validation.success) {
796
- sendValidationError(res, "Invalid ledger query parameters", validation.error.flatten());
797
- return;
798
- }
799
- try {
800
- await readAgentsMeta(projectRoot);
801
- res.json(await readLedger(projectRoot, validation.data));
802
- } catch (error) {
803
- sendUnknownError(res, error);
804
- }
805
- });
806
- }
807
-
808
- // src/api/knowledge.ts
809
- function registerKnowledgeApi(app, projectRoot) {
810
- app.get("/api/rules", async (_req, res) => {
811
- try {
812
- res.json(await readAgentsMeta(projectRoot));
813
- } catch (error) {
814
- sendUnknownError(res, error);
815
- }
816
- });
817
- }
818
-
819
- // src/api/knowledge-context.ts
820
- function registerKnowledgeContextApi(app, projectRoot) {
821
- app.get("/api/rules/context", async (req, res) => {
822
- const path = typeof req.query.path === "string" ? req.query.path.trim() : "";
823
- if (path.length === 0) {
824
- sendValidationError(res, "Missing required query parameter: path", {
825
- fieldErrors: {
826
- path: ["Expected a non-empty path query parameter."]
827
- }
828
- });
829
- return;
830
- }
831
- try {
832
- const result = await getKnowledge(projectRoot, { path });
833
- res.json(result.rules);
834
- } catch (error) {
835
- sendUnknownError(res, error);
836
- }
837
- });
838
- }
839
-
840
- // src/api/scan.ts
841
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
842
- import { isAbsolute, join as join2, relative, resolve, sep } from "path";
843
- import { detectFramework } from "@fenglimg/fabric-shared/node";
844
- var DEFAULT_IGNORES = [
845
- "**/*.meta",
846
- "library/**",
847
- "temp/**",
848
- "build/**",
849
- "settings/**",
850
- "profiles/**",
851
- "node_modules/**",
852
- "dist/**",
853
- ".git/**",
854
- ".fabric/**"
855
- ];
856
- function registerScanApi(app, projectRoot) {
857
- app.get("/api/scan", async (_req, res) => {
858
- try {
859
- res.json(await createScanReport(projectRoot));
860
- } catch (error) {
861
- sendUnknownError(res, error);
862
- }
863
- });
864
- }
865
- async function createScanReport(targetInput = process.cwd()) {
866
- const target = normalizeTarget(targetInput);
867
- const framework = detectFramework(target);
868
- const readmeQuality = getReadmeQuality(target);
869
- const hasContributing = existsSync(join2(target, "CONTRIBUTING.md"));
870
- const hasExistingFabric = existsSync(join2(target, ".fabric"));
871
- const walkResult = walkFiles(target, DEFAULT_IGNORES);
872
- return {
873
- target,
874
- framework,
875
- readmeQuality,
876
- hasContributing,
877
- fileCount: walkResult.fileCount,
878
- ignoredCount: walkResult.ignoredCount,
879
- hasExistingFabric,
880
- recommendations: buildRecommendations({
881
- framework,
882
- readmeQuality,
883
- hasContributing,
884
- hasExistingFabric
885
- })
886
- };
887
- }
888
- function normalizeTarget(targetInput) {
889
- return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
890
- }
891
- function getReadmeQuality(target) {
892
- const readmePath = join2(target, "README.md");
893
- if (!existsSync(readmePath)) {
894
- return "stub";
895
- }
896
- const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
897
- return wordCount >= 200 ? "ok" : "stub";
898
- }
899
- function walkFiles(root, ignorePatterns) {
900
- if (!existsSync(root) || !statSync(root).isDirectory()) {
901
- throw new Error(`Target must be an existing directory: ${root}`);
902
- }
903
- let fileCount = 0;
904
- let ignoredCount = 0;
905
- const stack = [root];
906
- while (stack.length > 0) {
907
- const current = stack.pop();
908
- if (current === void 0) {
909
- continue;
910
- }
911
- for (const entry of readdirSync(current, { withFileTypes: true })) {
912
- const absolutePath = join2(current, entry.name);
913
- const relativePath = toPosixPath(relative(root, absolutePath));
914
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
915
- ignoredCount += 1;
916
- continue;
917
- }
918
- if (entry.isDirectory()) {
919
- stack.push(absolutePath);
920
- } else if (entry.isFile()) {
921
- fileCount += 1;
922
- }
923
- }
924
- }
925
- return { fileCount, ignoredCount };
926
- }
927
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
928
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
929
- }
930
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
931
- const normalizedPattern = toPosixPath(pattern);
932
- if (normalizedPattern === "**/*.meta") {
933
- return relativePath.endsWith(".meta");
934
- }
935
- if (normalizedPattern.endsWith("/**")) {
936
- const directoryPrefix = normalizedPattern.slice(0, -3);
937
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
938
- }
939
- return relativePath === normalizedPattern;
940
- }
941
- function toPosixPath(path) {
942
- return path.split(sep).join("/");
943
- }
944
- function buildRecommendations(input) {
945
- const recommendations = [];
946
- if (!input.hasExistingFabric) {
947
- recommendations.push("L0: Run `fabric install` to scaffold the .fabric/ knowledge layout (decisions, pitfalls, guidelines, models, processes).");
948
- }
949
- if (input.readmeQuality === "stub") {
950
- recommendations.push("L0: Expand README.md before promoting project facts into Fabric knowledge entries.");
951
- }
952
- if (!input.hasContributing) {
953
- recommendations.push("L0: Add CONTRIBUTING.md or capture contribution-flow guidance under .fabric/knowledge/processes/.");
954
- }
955
- if (input.framework.kind === "unknown") {
956
- recommendations.push("L1: Add tech-stack TODOs manually because no framework marker was detected.");
957
- } else {
958
- recommendations.push(`L1: Review ${input.framework.kind} directories for future scoped Fabric rule files.`);
959
- }
960
- return recommendations;
961
- }
962
-
963
- // src/middleware/bearer-auth.ts
964
- import { createHash, timingSafeEqual } from "crypto";
965
- function createBearerAuthMiddleware(token) {
966
- const expectedDigest = hashToken(token);
967
- return function bearerAuthMiddleware(req, res, next) {
968
- const header = readAuthorizationHeader(req.headers.authorization);
969
- const providedToken = parseBearerToken(header);
970
- if (providedToken === void 0 || !tokensMatch(providedToken, expectedDigest)) {
971
- sendError(res, 401, "UNAUTHORIZED", "Bearer token required");
972
- return;
973
- }
974
- next();
975
- };
976
- }
977
- function createLoopbackDenyMiddleware() {
978
- return function loopbackDenyMiddleware(_req, res, _next) {
979
- sendError(
980
- res,
981
- 401,
982
- "UNAUTHORIZED",
983
- "FABRIC_AUTH_TOKEN is not set. Either export FABRIC_AUTH_TOKEN=<secret> before running `fabric serve`, or pass `--allow-loopback-no-auth` to explicitly opt in to unauthenticated loopback access (security risk)."
984
- );
985
- };
986
- }
987
- function readAuthorizationHeader(value) {
988
- if (typeof value === "string" && value.length > 0) {
989
- return value;
990
- }
991
- if (Array.isArray(value)) {
992
- return value.find((entry) => entry.length > 0);
993
- }
994
- return void 0;
995
- }
996
- function parseBearerToken(header) {
997
- if (header === void 0) {
998
- return void 0;
999
- }
1000
- const match = /^Bearer\s+(.+)$/i.exec(header);
1001
- return match?.[1];
1002
- }
1003
- function tokensMatch(token, expectedDigest) {
1004
- return timingSafeEqual(hashToken(token), expectedDigest);
1005
- }
1006
- function hashToken(token) {
1007
- return createHash("sha256").update(token, "utf8").digest();
1008
- }
1009
-
1010
- // src/http.ts
1011
- var DEFAULT_HOST = "127.0.0.1";
1012
- var NOTIFY_DEBOUNCE_MS = 200;
1013
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
1014
- function isLoopbackHost(host) {
1015
- return LOOPBACK_HOSTS.has(host);
1016
- }
1017
- var JsonlEventStore = class {
1018
- constructor(projectRoot) {
1019
- this.projectRoot = projectRoot;
1020
- }
1021
- projectRoot;
1022
- async storeEvent(streamId, message) {
1023
- const eventId = randomUUID2();
1024
- await appendEventLedgerEvent(this.projectRoot, {
1025
- event_type: "mcp_event",
1026
- mcp_event_id: eventId,
1027
- stream_id: streamId,
1028
- message
1029
- });
1030
- return eventId;
1031
- }
1032
- async getStreamIdForEventId(eventId) {
1033
- const events = await this.readEvents();
1034
- return events.find((event) => event.eventId === eventId)?.streamId;
1035
- }
1036
- async replayEventsAfter(lastEventId, { send }) {
1037
- const events = await this.readEvents();
1038
- const startIndex = events.findIndex((event) => event.eventId === lastEventId);
1039
- if (startIndex === -1) {
1040
- throw new Error(`Unknown event ID: ${lastEventId}`);
1041
- }
1042
- const streamId = events[startIndex]?.streamId;
1043
- if (streamId === void 0) {
1044
- throw new Error(`Missing stream for event ID: ${lastEventId}`);
1045
- }
1046
- for (const event of events.slice(startIndex + 1)) {
1047
- if (event.streamId !== streamId) {
1048
- continue;
1049
- }
1050
- await send(event.eventId, event.message);
1051
- }
1052
- return streamId;
1053
- }
1054
- async readEvents() {
1055
- const { events: eventLedgerEvents } = await readEventLedger(this.projectRoot);
1056
- const projectedEvents = eventLedgerEvents.flatMap((event) => {
1057
- if (event.event_type !== "mcp_event") {
1058
- return [];
1059
- }
1060
- return [{
1061
- kind: "mcp-event",
1062
- eventId: event.mcp_event_id,
1063
- streamId: event.stream_id,
1064
- message: event.message
1065
- }];
1066
- });
1067
- if (projectedEvents.length > 0) {
1068
- return projectedEvents;
1069
- }
1070
- let raw;
1071
- try {
1072
- raw = await readFile3(getLedgerPath(this.projectRoot), "utf8");
1073
- } catch (error) {
1074
- if (isNodeError3(error) && error.code === "ENOENT") {
1075
- try {
1076
- raw = await readFile3(getLegacyLedgerPath(this.projectRoot), "utf8");
1077
- } catch (legacyError) {
1078
- if (isNodeError3(legacyError) && legacyError.code === "ENOENT") {
1079
- return [];
1080
- }
1081
- throw legacyError;
1082
- }
1083
- } else {
1084
- throw error;
1085
- }
1086
- }
1087
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => parseStoredMcpEvent(line)).filter((event) => event !== null);
1088
- }
1089
- };
1090
- function handleCacheWatcherEvent(relativePath, projectRoot, sessions, timers) {
1091
- const normalized = relativePath.replaceAll("\\", "/");
1092
- if (normalized === ".fabric/agents.meta.json") {
1093
- contextCache.invalidate("file_watch", projectRoot);
1094
- clearTimeout(timers.getToolListTimer());
1095
- timers.setToolListTimer(
1096
- setTimeout(() => {
1097
- notifyAllSessions(sessions, "tools/list_changed");
1098
- }, NOTIFY_DEBOUNCE_MS)
1099
- );
1100
- return;
1101
- }
1102
- if (normalized.startsWith(".fabric/knowledge/") && normalized.endsWith(".md")) {
1103
- contextCache.invalidate("file_watch", projectRoot);
1104
- invalidateKnowledgeSyncCooldown(projectRoot);
1105
- }
1106
- }
1107
- function createFabricHttpApp(options) {
1108
- const { projectRoot, host = DEFAULT_HOST, authToken, allowLoopbackNoAuth = false } = options;
1109
- if (allowLoopbackNoAuth && authToken === void 0 && !isLoopbackHost(host)) {
1110
- throw new Error(
1111
- `createFabricHttpApp: allowLoopbackNoAuth=true requires a loopback host (127.0.0.1 / localhost / ::1); got ${JSON.stringify(host)}. Either bind to loopback or set FABRIC_AUTH_TOKEN.`
1112
- );
1113
- }
1114
- const app = createMcpExpressApp({ host });
1115
- const eventStore = new JsonlEventStore(projectRoot);
1116
- const sessions = /* @__PURE__ */ new Map();
1117
- process.env.FABRIC_PROJECT_ROOT = projectRoot;
1118
- const cacheWatcher = chokidar2.watch(
1119
- [
1120
- ".fabric/agents.meta.json",
1121
- ".fabric/knowledge/**/*.md",
1122
- ".fabric/knowledge/pending/**/*.md"
1123
- ],
1124
- {
1125
- cwd: projectRoot,
1126
- ignoreInitial: true,
1127
- awaitWriteFinish: {
1128
- stabilityThreshold: 120,
1129
- pollInterval: 20
1130
- }
1131
- }
1132
- );
1133
- let agentsMdNotifyTimer;
1134
- let toolListNotifyTimer;
1135
- const onCacheWatcherEvent = (relativePath) => {
1136
- handleCacheWatcherEvent(relativePath, projectRoot, sessions, {
1137
- getAgentsMdTimer: () => agentsMdNotifyTimer,
1138
- getToolListTimer: () => toolListNotifyTimer,
1139
- setAgentsMdTimer: (t) => {
1140
- agentsMdNotifyTimer = t;
1141
- },
1142
- setToolListTimer: (t) => {
1143
- toolListNotifyTimer = t;
1144
- }
1145
- });
1146
- };
1147
- cacheWatcher.on("change", onCacheWatcherEvent);
1148
- cacheWatcher.on("add", onCacheWatcherEvent);
1149
- cacheWatcher.on("unlink", onCacheWatcherEvent);
1150
- let disposed = false;
1151
- app.dispose = async () => {
1152
- if (disposed) {
1153
- return;
1154
- }
1155
- disposed = true;
1156
- clearTimeout(agentsMdNotifyTimer);
1157
- clearTimeout(toolListNotifyTimer);
1158
- await cacheWatcher.close();
1159
- };
1160
- app.disable("x-powered-by");
1161
- if (authToken !== void 0) {
1162
- const bearerAuth = createBearerAuthMiddleware(authToken);
1163
- app.use("/api", bearerAuth);
1164
- app.use("/events", bearerAuth);
1165
- app.use("/mcp", bearerAuth);
1166
- } else if (!allowLoopbackNoAuth) {
1167
- const denyAll = createLoopbackDenyMiddleware();
1168
- app.use("/api", denyAll);
1169
- app.use("/events", denyAll);
1170
- app.use("/mcp", denyAll);
1171
- }
1172
- registerKnowledgeApi(app, projectRoot);
1173
- registerKnowledgeContextApi(app, projectRoot);
1174
- registerLedgerApi(app, projectRoot);
1175
- registerHistoryApi(app, projectRoot);
1176
- registerScanApi(app, projectRoot);
1177
- registerDoctorApi(app, projectRoot);
1178
- app.get("/events", createEventsHandler({ projectRoot }));
1179
- app.all("/mcp", async (req, res) => {
1180
- const sessionId = readHeader(req.headers["mcp-session-id"]);
1181
- if (sessionId !== void 0) {
1182
- const session2 = sessions.get(sessionId);
1183
- if (session2 === void 0) {
1184
- writeJsonRpcError(res, 404, -32001, "Session not found");
1185
- return;
1186
- }
1187
- await session2.transport.handleRequest(req, res, req.body);
1188
- return;
1189
- }
1190
- if (!isInitializeRequest(req.body)) {
1191
- writeJsonRpcError(res, 400, -32e3, "Bad Request: Mcp-Session-Id header is required");
1192
- return;
1193
- }
1194
- const session = await createSession(eventStore, sessions);
1195
- await session.transport.handleRequest(req, res, req.body);
1196
- });
1197
- return app;
1198
- }
1199
- function notifyAllSessions(sessions, kind, uri) {
1200
- for (const { server } of sessions.values()) {
1201
- try {
1202
- if (kind === "tools/list_changed") {
1203
- server.sendToolListChanged();
1204
- } else if (kind === "resources/list_changed") {
1205
- server.sendResourceListChanged();
1206
- } else if (kind === "resource_updated" && uri !== void 0) {
1207
- void server.server.sendResourceUpdated({ uri });
1208
- }
1209
- } catch {
1210
- }
1211
- }
1212
- }
1213
- async function createSession(eventStore, sessions) {
1214
- const { createFabricServer } = await import("./index.js");
1215
- const server = createFabricServer();
1216
- const transport = new StreamableHTTPServerTransport({
1217
- sessionIdGenerator: randomUUID2,
1218
- enableJsonResponse: true,
1219
- eventStore,
1220
- onsessioninitialized: async (sessionId) => {
1221
- sessions.set(sessionId, { server, transport });
1222
- },
1223
- onsessionclosed: async (sessionId) => {
1224
- sessions.delete(sessionId);
1225
- }
1226
- });
1227
- transport.onclose = () => {
1228
- const sessionId = transport.sessionId;
1229
- if (sessionId !== void 0) {
1230
- sessions.delete(sessionId);
1231
- }
1232
- };
1233
- await server.connect(transport);
1234
- return { server, transport };
1235
- }
1236
- function isInitializeRequest(body) {
1237
- if (Array.isArray(body)) {
1238
- return body.some((entry) => isInitializeMessage(entry));
1239
- }
1240
- return isInitializeMessage(body);
1241
- }
1242
- function isInitializeMessage(value) {
1243
- return value !== null && typeof value === "object" && "jsonrpc" in value && "method" in value && value.jsonrpc === "2.0" && value.method === "initialize";
1244
- }
1245
- function parseStoredMcpEvent(line) {
1246
- try {
1247
- const parsed = JSON.parse(line);
1248
- if (parsed.kind !== "mcp-event" || typeof parsed.eventId !== "string" || typeof parsed.streamId !== "string" || parsed.message === void 0) {
1249
- return null;
1250
- }
1251
- return {
1252
- kind: "mcp-event",
1253
- eventId: parsed.eventId,
1254
- streamId: parsed.streamId,
1255
- message: parsed.message
1256
- };
1257
- } catch {
1258
- return null;
1259
- }
1260
- }
1261
- function readHeader(value) {
1262
- if (typeof value === "string" && value.length > 0) {
1263
- return value;
1264
- }
1265
- if (Array.isArray(value)) {
1266
- return value.find((entry) => entry.length > 0);
1267
- }
1268
- return void 0;
1269
- }
1270
- function writeJsonRpcError(res, status, code, message) {
1271
- res.status(status).json({
1272
- jsonrpc: "2.0",
1273
- error: {
1274
- code,
1275
- message
1276
- },
1277
- id: null
1278
- });
1279
- }
1280
- function isNodeError3(error) {
1281
- return error instanceof Error;
1282
- }
1283
- export {
1284
- createFabricHttpApp,
1285
- handleCacheWatcherEvent
1286
- };