@fenglimg/fabric-server 1.6.0 → 1.8.0-rc.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.
@@ -1,27 +1,25 @@
1
1
  import {
2
2
  AGENTS_MD_RESOURCE_URI,
3
- AgentsMetaFileMissingError,
4
- AgentsMetaInvalidError,
3
+ EVENT_LEDGER_PATH,
5
4
  LEDGER_PATH,
6
5
  LEGACY_LEDGER_PATH,
7
- appendLedgerEntry,
8
- approveHumanLock,
6
+ appendEventLedgerEvent,
9
7
  contextCache,
10
- ensureParentDirectory,
8
+ getEventLedgerPath,
11
9
  getLedgerPath,
12
10
  getLegacyLedgerPath,
13
11
  getRules,
12
+ invalidateRuleSyncCooldown,
13
+ isNodeError,
14
14
  readAgentsMeta,
15
- readHumanLock,
16
- readHumanLockEntry,
17
- readLedger,
18
- resolveLedgerPaths,
19
- runDoctorReport
20
- } from "./chunk-TZCE2K4D.js";
15
+ readEventLedger,
16
+ runDoctorReport,
17
+ sha256
18
+ } from "./chunk-E3BHIUIW.js";
21
19
 
22
20
  // src/http.ts
23
- import { randomUUID } from "crypto";
24
- import { appendFile, readFile as readFile2 } from "fs/promises";
21
+ import { randomUUID as randomUUID2 } from "crypto";
22
+ import { readFile as readFile3 } from "fs/promises";
25
23
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
26
24
  import {
27
25
  StreamableHTTPServerTransport
@@ -29,6 +27,7 @@ import {
29
27
  import chokidar2 from "chokidar";
30
28
 
31
29
  // src/api/_error.ts
30
+ import { FabricError } from "@fenglimg/fabric-shared/errors";
32
31
  function sendError(res, status, code, message, details) {
33
32
  const payload = {
34
33
  error: {
@@ -49,49 +48,15 @@ function sendUnknownError(res, error) {
49
48
  sendError(res, normalized.status, normalized.code, normalized.message, normalized.details);
50
49
  }
51
50
  function normalizeApiError(error) {
52
- if (error instanceof Error && "status" in error && "code" in error && typeof error.status === "number" && typeof error.code === "string") {
51
+ if (error instanceof FabricError) {
53
52
  return {
54
- status: error.status,
53
+ status: error.httpStatus,
55
54
  code: error.code,
56
- message: error.message
57
- };
58
- }
59
- if (error instanceof AgentsMetaFileMissingError) {
60
- return {
61
- status: 404,
62
- code: error.code,
63
- message: error.message
64
- };
65
- }
66
- if (error instanceof AgentsMetaInvalidError) {
67
- return {
68
- status: 500,
69
- code: error.code,
70
- message: error.message
55
+ message: error.message,
56
+ details: error.details
71
57
  };
72
58
  }
73
59
  if (error instanceof Error) {
74
- if (error.message.startsWith("Path escapes project root:")) {
75
- return {
76
- status: 403,
77
- code: "PATH_OUTSIDE_PROJECT_ROOT",
78
- message: error.message
79
- };
80
- }
81
- if (error.message.startsWith("Cannot find human lock entry:")) {
82
- return {
83
- status: 404,
84
- code: "HUMAN_LOCK_ENTRY_NOT_FOUND",
85
- message: error.message
86
- };
87
- }
88
- if (error.message.startsWith("Cannot find ledger entry:")) {
89
- return {
90
- status: 404,
91
- code: "LEDGER_ENTRY_NOT_FOUND",
92
- message: error.message
93
- };
94
- }
95
60
  return {
96
61
  status: 500,
97
62
  code: "INTERNAL_ERROR",
@@ -117,21 +82,186 @@ function registerDoctorApi(app, projectRoot) {
117
82
  }
118
83
 
119
84
  // src/api/events.ts
120
- import { createHash } from "crypto";
121
- import { open, readFile, stat } from "fs/promises";
85
+ import { open, readFile as readFile2, stat } from "fs/promises";
122
86
  import { join } from "path";
123
87
  import {
124
88
  agentsMetaSchema,
125
89
  fabricEventSchema,
126
90
  forensicReportSchema,
127
- humanLockFileSchema,
128
- ledgerEntrySchema
91
+ ledgerEntrySchema as ledgerEntrySchema2
129
92
  } from "@fenglimg/fabric-shared";
93
+ import { eventLedgerEventSchema } from "@fenglimg/fabric-shared";
130
94
  import chokidar from "chokidar";
95
+
96
+ // src/services/read-ledger.ts
97
+ import { randomUUID } from "crypto";
98
+ import { access, copyFile, readFile, rm } from "fs/promises";
99
+ import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
100
+ async function resolveLedgerPaths(projectRoot) {
101
+ const primaryPath = getLedgerPath(projectRoot);
102
+ const legacyPath = getLegacyLedgerPath(projectRoot);
103
+ const [primaryExists, legacyExists] = await Promise.all([
104
+ pathExists(primaryPath),
105
+ pathExists(legacyPath)
106
+ ]);
107
+ return {
108
+ primaryPath,
109
+ legacyPath,
110
+ readPath: primaryExists ? primaryPath : legacyPath,
111
+ usingLegacy: !primaryExists && legacyExists
112
+ };
113
+ }
114
+ async function readLedger(projectRoot, options = {}) {
115
+ const [legacyEntries, eventEntries] = await Promise.all([
116
+ readLegacyLedger(projectRoot),
117
+ readLedgerFromEventLedger(projectRoot)
118
+ ]);
119
+ const entries = mergeLedgerEntries(legacyEntries, eventEntries);
120
+ return entries.filter((entry) => options.source === void 0 || entry.source === options.source).filter((entry) => options.since === void 0 || entry.ts >= options.since);
121
+ }
122
+ async function readLegacyLedger(projectRoot) {
123
+ const { readPath } = await resolveLedgerPaths(projectRoot);
124
+ let raw;
125
+ try {
126
+ raw = await readFile(readPath, "utf8");
127
+ } catch (error) {
128
+ if (isNodeError(error) && error.code === "ENOENT") {
129
+ return [];
130
+ }
131
+ throw error;
132
+ }
133
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseLedgerLine(line, index)).filter((entry) => entry !== null);
134
+ }
135
+ async function appendLedgerEntry(projectRoot, entry) {
136
+ const nextEntry = createStoredLedgerEntry(entry);
137
+ for (const affectedPath of nextEntry.affected_paths) {
138
+ await appendEventLedgerEvent(projectRoot, {
139
+ event_type: "edit_intent_checked",
140
+ ts: nextEntry.ts,
141
+ path: affectedPath,
142
+ compliant: true,
143
+ intent: nextEntry.intent,
144
+ ledger_entry_id: nextEntry.id,
145
+ ledger_source: nextEntry.source,
146
+ commit_sha: nextEntry.source === "ai" ? nextEntry.commit_sha : void 0,
147
+ parent_sha: nextEntry.source === "human" ? nextEntry.parent_sha : void 0,
148
+ parent_ledger_entry_id: nextEntry.source === "human" ? nextEntry.parent_ledger_entry_id : void 0,
149
+ diff_stat: nextEntry.source === "human" ? nextEntry.diff_stat : void 0,
150
+ annotation: nextEntry.source === "human" ? nextEntry.annotation : void 0,
151
+ matched_rule_context_ts: null,
152
+ window_ms: 0
153
+ });
154
+ }
155
+ return nextEntry;
156
+ }
157
+ function createStoredLedgerEntry(entry) {
158
+ return ledgerEntrySchema.parse({
159
+ ...entry,
160
+ id: entry.id ?? `ledger:${randomUUID()}`
161
+ });
162
+ }
163
+ function parseLedgerLine(line, index) {
164
+ try {
165
+ const parsed = JSON.parse(line);
166
+ if (parsed.kind === "mcp-event") {
167
+ return null;
168
+ }
169
+ const result = ledgerEntrySchema.safeParse(parsed);
170
+ if (!result.success) {
171
+ return null;
172
+ }
173
+ return {
174
+ ...result.data,
175
+ id: result.data.id ?? createDerivedId(index, line)
176
+ };
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ async function readLedgerFromEventLedger(projectRoot) {
182
+ const { events } = await readEventLedger(projectRoot);
183
+ const grouped = /* @__PURE__ */ new Map();
184
+ for (const event of events) {
185
+ const entry = projectLedgerEvent(event);
186
+ if (entry === null) {
187
+ continue;
188
+ }
189
+ const existing = grouped.get(entry.id);
190
+ if (existing === void 0) {
191
+ grouped.set(entry.id, entry);
192
+ continue;
193
+ }
194
+ grouped.set(entry.id, {
195
+ ...existing,
196
+ ts: Math.min(existing.ts, entry.ts),
197
+ affected_paths: dedupeStrings([...existing.affected_paths, ...entry.affected_paths])
198
+ });
199
+ }
200
+ return Array.from(grouped.values());
201
+ }
202
+ function projectLedgerEvent(event) {
203
+ if (event.event_type !== "edit_intent_checked") {
204
+ return null;
205
+ }
206
+ const base = {
207
+ id: event.ledger_entry_id,
208
+ ts: event.ts,
209
+ intent: event.intent,
210
+ affected_paths: [event.path]
211
+ };
212
+ if (event.ledger_source === "human") {
213
+ return {
214
+ ...base,
215
+ source: "human",
216
+ parent_sha: event.parent_sha ?? event.ledger_entry_id,
217
+ parent_ledger_entry_id: event.parent_ledger_entry_id,
218
+ diff_stat: event.diff_stat ?? "event-ledger",
219
+ annotation: event.annotation
220
+ };
221
+ }
222
+ return {
223
+ ...base,
224
+ source: "ai",
225
+ commit_sha: event.commit_sha
226
+ };
227
+ }
228
+ function mergeLedgerEntries(legacyEntries, eventEntries) {
229
+ const byId = /* @__PURE__ */ new Map();
230
+ for (const entry of [...legacyEntries, ...eventEntries]) {
231
+ if (!byId.has(entry.id)) {
232
+ byId.set(entry.id, entry);
233
+ }
234
+ }
235
+ return Array.from(byId.values()).sort((left, right) => left.ts - right.ts);
236
+ }
237
+ function dedupeStrings(values) {
238
+ return Array.from(new Set(values));
239
+ }
240
+ function createDerivedId(index, line) {
241
+ return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
242
+ }
243
+ async function pathExists(path) {
244
+ try {
245
+ await access(path);
246
+ return true;
247
+ } catch (error) {
248
+ if (isNodeError(error) && error.code === "ENOENT") {
249
+ return false;
250
+ }
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ // src/api/events.ts
131
256
  var AGENTS_META_PATH = ".fabric/agents.meta.json";
132
- var HUMAN_LOCK_PATH = ".fabric/human-lock.json";
133
257
  var FORENSIC_PATH = ".fabric/forensic.json";
134
- var WATCHED_PATHS = [AGENTS_META_PATH, HUMAN_LOCK_PATH, FORENSIC_PATH, LEDGER_PATH, LEGACY_LEDGER_PATH];
258
+ var WATCHED_PATHS = [
259
+ AGENTS_META_PATH,
260
+ FORENSIC_PATH,
261
+ EVENT_LEDGER_PATH,
262
+ LEDGER_PATH,
263
+ LEGACY_LEDGER_PATH
264
+ ];
135
265
  var CONNECTION_LIMIT = 10;
136
266
  var HEARTBEAT_INTERVAL_MS = 3e4;
137
267
  var WATCH_DEBOUNCE_MS = 75;
@@ -173,7 +303,8 @@ function createEventsHandler(options) {
173
303
  activeLedgerPath: getLedgerPath(projectRoot),
174
304
  ledgerOffset: 0,
175
305
  ledgerRemainder: "",
176
- humanLockSnapshot: createEmptyHumanLockSnapshot(),
306
+ eventLedgerOffset: 0,
307
+ eventLedgerRemainder: "",
177
308
  nextEventId: 1,
178
309
  ringBuffer: new RingBuffer(RING_BUFFER_CAPACITY)
179
310
  };
@@ -252,7 +383,8 @@ async function ensureWatcher(state, projectRoot) {
252
383
  state.activeLedgerPath = ledgerState.path;
253
384
  state.ledgerOffset = ledgerState.size;
254
385
  state.ledgerRemainder = "";
255
- state.humanLockSnapshot = await readHumanLockSnapshot(projectRoot);
386
+ state.eventLedgerOffset = await readFileSize(getEventLedgerPath(projectRoot));
387
+ state.eventLedgerRemainder = "";
256
388
  const watcher = chokidar.watch([...WATCHED_PATHS], {
257
389
  cwd: projectRoot,
258
390
  ignoreInitial: true,
@@ -306,13 +438,13 @@ async function readEventsForFile(state, projectRoot, relativePath) {
306
438
  const event = await readMetaUpdatedEvent(projectRoot);
307
439
  return event === null ? [] : [event];
308
440
  }
309
- if (relativePath === HUMAN_LOCK_PATH) {
310
- return await readHumanLockEvents(state, projectRoot);
311
- }
312
441
  if (relativePath === FORENSIC_PATH) {
313
442
  const event = await readDriftDetectedEvent(projectRoot);
314
443
  return event === null ? [] : [event];
315
444
  }
445
+ if (relativePath === EVENT_LEDGER_PATH) {
446
+ return await readEventLedgerAppendedEvents(state, projectRoot);
447
+ }
316
448
  if (relativePath === LEDGER_PATH || relativePath === LEGACY_LEDGER_PATH) {
317
449
  return await readLedgerAppendedEvents(state, projectRoot);
318
450
  }
@@ -342,40 +474,6 @@ async function readDriftDetectedEvent(projectRoot) {
342
474
  payload: parsed
343
475
  };
344
476
  }
345
- async function readHumanLockEvents(state, projectRoot) {
346
- const previousSnapshot = state.humanLockSnapshot;
347
- const currentSnapshot = await readHumanLockSnapshot(projectRoot);
348
- state.humanLockSnapshot = currentSnapshot;
349
- const changedEntries = currentSnapshot.locked.filter((entry) => {
350
- const key = getHumanLockKey(entry);
351
- return previousSnapshot.hashByKey.get(key) !== entry.hash;
352
- });
353
- const approvedEntries = changedEntries.filter((entry) => {
354
- const key = getHumanLockKey(entry);
355
- return currentSnapshot.actualHashByKey.get(key) === entry.hash;
356
- });
357
- const driftChanged = !areSetsEqual(previousSnapshot.driftedKeys, currentSnapshot.driftedKeys);
358
- const events = [];
359
- if (approvedEntries.length > 0 || changedEntries.length > 0 && currentSnapshot.drifted.length === 0) {
360
- events.push({
361
- type: "lock:approved",
362
- payload: {
363
- locked: currentSnapshot.locked,
364
- approved: approvedEntries.length > 0 ? approvedEntries : changedEntries
365
- }
366
- });
367
- }
368
- if (currentSnapshot.drifted.length > 0 && (driftChanged || approvedEntries.length === 0)) {
369
- events.push({
370
- type: "lock:drift",
371
- payload: {
372
- locked: currentSnapshot.locked,
373
- drifted: currentSnapshot.drifted
374
- }
375
- });
376
- }
377
- return events;
378
- }
379
477
  async function readLedgerAppendedEvents(state, projectRoot) {
380
478
  const ledgerState = await resolveLedgerWatchState(projectRoot);
381
479
  const ledgerPath = ledgerState.path;
@@ -407,6 +505,31 @@ async function readLedgerAppendedEvents(state, projectRoot) {
407
505
  await handle.close();
408
506
  }
409
507
  }
508
+ async function readEventLedgerAppendedEvents(state, projectRoot) {
509
+ const eventLedgerPath = getEventLedgerPath(projectRoot);
510
+ const nextSize = await readFileSize(eventLedgerPath);
511
+ if (nextSize < state.eventLedgerOffset) {
512
+ state.eventLedgerOffset = 0;
513
+ state.eventLedgerRemainder = "";
514
+ }
515
+ if (nextSize === state.eventLedgerOffset) {
516
+ return [];
517
+ }
518
+ const startOffset = state.eventLedgerOffset;
519
+ state.eventLedgerOffset = nextSize;
520
+ const handle = await open(eventLedgerPath, "r");
521
+ try {
522
+ const length = nextSize - startOffset;
523
+ const buffer = Buffer.alloc(length);
524
+ await handle.read(buffer, 0, length, startOffset);
525
+ const chunk = `${state.eventLedgerRemainder}${buffer.toString("utf8")}`;
526
+ const lines = chunk.split(/\r?\n/);
527
+ state.eventLedgerRemainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
528
+ return lines.map((line) => line.trim()).filter((line) => line.length > 0).map(parseEventLedgerAppendedEvent).filter((event) => event !== null);
529
+ } finally {
530
+ await handle.close();
531
+ }
532
+ }
410
533
  async function resolveLedgerWatchState(projectRoot) {
411
534
  const paths = await resolveLedgerPaths(projectRoot);
412
535
  const path = paths.usingLegacy ? paths.legacyPath : paths.primaryPath;
@@ -419,7 +542,7 @@ function parseLedgerAppendedEvent(line) {
419
542
  if (parsed.kind === "mcp-event") {
420
543
  return null;
421
544
  }
422
- const validation = ledgerEntrySchema.safeParse(parsed);
545
+ const validation = ledgerEntrySchema2.safeParse(parsed);
423
546
  if (!validation.success) {
424
547
  return null;
425
548
  }
@@ -431,6 +554,26 @@ function parseLedgerAppendedEvent(line) {
431
554
  return null;
432
555
  }
433
556
  }
557
+ function parseEventLedgerAppendedEvent(line) {
558
+ try {
559
+ const parsed = eventLedgerEventSchema.safeParse(JSON.parse(line));
560
+ if (!parsed.success || parsed.data.event_type !== "edit_intent_checked") {
561
+ return null;
562
+ }
563
+ return {
564
+ type: "ledger:appended",
565
+ payload: {
566
+ id: parsed.data.ledger_entry_id,
567
+ ts: parsed.data.ts,
568
+ source: "ai",
569
+ intent: parsed.data.intent,
570
+ affected_paths: [parsed.data.path]
571
+ }
572
+ };
573
+ } catch {
574
+ return null;
575
+ }
576
+ }
434
577
  function broadcastEvent(state, event) {
435
578
  const payload = fabricEventSchema.parse(event);
436
579
  const eventId = state.nextEventId++;
@@ -460,68 +603,6 @@ data: ${data}
460
603
  }
461
604
  }
462
605
  }
463
- async function readHumanLockSnapshot(projectRoot) {
464
- const humanLockPath = join(projectRoot, HUMAN_LOCK_PATH);
465
- const raw = await readUtf8File(humanLockPath);
466
- if (raw === null) {
467
- return createEmptyHumanLockSnapshot();
468
- }
469
- const parsed = humanLockFileSchema.parse(JSON.parse(raw));
470
- const locked = parsed.locked ?? [];
471
- const actualHashByKey = await readActualHumanLockHashes(projectRoot, locked);
472
- const drifted = locked.filter((entry) => actualHashByKey.get(getHumanLockKey(entry)) !== entry.hash);
473
- return {
474
- locked,
475
- drifted,
476
- driftedKeys: new Set(drifted.map((entry) => getHumanLockKey(entry))),
477
- hashByKey: new Map(locked.map((entry) => [getHumanLockKey(entry), entry.hash])),
478
- actualHashByKey
479
- };
480
- }
481
- async function readActualHumanLockHashes(projectRoot, locked) {
482
- const uniqueFiles = Array.from(new Set(locked.map((entry) => entry.file)));
483
- const fileContents = await Promise.all(
484
- uniqueFiles.map(async (file) => {
485
- const raw = await readUtf8File(join(projectRoot, file));
486
- return [file, raw];
487
- })
488
- );
489
- const contentByFile = new Map(fileContents);
490
- return new Map(
491
- locked.map((entry) => {
492
- const content = contentByFile.get(entry.file);
493
- return [getHumanLockKey(entry), content == null ? "missing" : hashLockedContent(content, entry)];
494
- })
495
- );
496
- }
497
- function hashLockedContent(content, entry) {
498
- const lines = content.split(/\r?\n/);
499
- const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
500
- return `sha256:${createHash("sha256").update(slice).digest("hex")}`;
501
- }
502
- function getHumanLockKey(entry) {
503
- return `${entry.file}:${entry.start_line}:${entry.end_line}`;
504
- }
505
- function createEmptyHumanLockSnapshot() {
506
- return {
507
- locked: [],
508
- drifted: [],
509
- driftedKeys: /* @__PURE__ */ new Set(),
510
- hashByKey: /* @__PURE__ */ new Map(),
511
- actualHashByKey: /* @__PURE__ */ new Map()
512
- };
513
- }
514
- function areSetsEqual(left, right) {
515
- if (left.size !== right.size) {
516
- return false;
517
- }
518
- for (const value of left) {
519
- if (!right.has(value)) {
520
- return false;
521
- }
522
- }
523
- return true;
524
- }
525
606
  function readLastEventId(req) {
526
607
  const header = req.headers["last-event-id"];
527
608
  const headerValue = Array.isArray(header) ? header[0] : header;
@@ -542,9 +623,9 @@ function normalizePath(value) {
542
623
  }
543
624
  async function readUtf8File(path) {
544
625
  try {
545
- return await readFile(path, "utf8");
626
+ return await readFile2(path, "utf8");
546
627
  } catch (error) {
547
- if (isNodeError(error) && error.code === "ENOENT") {
628
+ if (isNodeError2(error) && error.code === "ENOENT") {
548
629
  return null;
549
630
  }
550
631
  throw error;
@@ -555,13 +636,13 @@ async function readFileSize(path) {
555
636
  const fileStat = await stat(path);
556
637
  return fileStat.size;
557
638
  } catch (error) {
558
- if (isNodeError(error) && error.code === "ENOENT") {
639
+ if (isNodeError2(error) && error.code === "ENOENT") {
559
640
  return 0;
560
641
  }
561
642
  throw error;
562
643
  }
563
644
  }
564
- function isNodeError(error) {
645
+ function isNodeError2(error) {
565
646
  return error instanceof Error;
566
647
  }
567
648
 
@@ -572,17 +653,26 @@ import { historyStateQuerySchema } from "@fenglimg/fabric-shared";
572
653
  import { execFile } from "child_process";
573
654
  import { promisify } from "util";
574
655
  import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
656
+ import { IOFabricError, RuleError } from "@fenglimg/fabric-shared/errors";
575
657
  var execFileAsync = promisify(execFile);
576
658
  var AGENTS_META_GIT_PATH = ".fabric/agents.meta.json";
577
- var HistoryReplayError = class extends Error {
578
- constructor(message, code, status) {
579
- super(message);
580
- this.code = code;
581
- this.status = status;
582
- this.name = "HistoryReplayError";
583
- }
584
- code;
585
- status;
659
+ var HistoryStateNotFoundError = class extends IOFabricError {
660
+ code = "HISTORY_STATE_NOT_FOUND";
661
+ httpStatus = 404;
662
+ constructor(message, opts) {
663
+ super(message, {
664
+ actionHint: opts?.actionHint ?? "Ensure the ledger exists and the requested timestamp or entry ID is within its range"
665
+ });
666
+ }
667
+ };
668
+ var LedgerEntryNotFoundError = class extends RuleError {
669
+ code = "LEDGER_ENTRY_NOT_FOUND";
670
+ httpStatus = 404;
671
+ constructor(message, opts) {
672
+ super(message, {
673
+ actionHint: opts?.actionHint ?? "Verify the ledger entry ID exists in the current ledger"
674
+ });
675
+ }
586
676
  };
587
677
  async function rehydrateAgentsMetaAt(projectRoot, target) {
588
678
  const ledger = await readLedger(projectRoot);
@@ -590,10 +680,8 @@ async function rehydrateAgentsMetaAt(projectRoot, target) {
590
680
  const replayedEntries = ledger.slice(0, selectedIndex + 1);
591
681
  const selectedEntry = replayedEntries.at(-1);
592
682
  if (selectedEntry === void 0) {
593
- throw new HistoryReplayError(
594
- "Cannot rehydrate history state because the ledger is empty.",
595
- "HISTORY_STATE_NOT_FOUND",
596
- 404
683
+ throw new HistoryStateNotFoundError(
684
+ "Cannot rehydrate history state because the ledger is empty."
597
685
  );
598
686
  }
599
687
  const commitCandidates = collectCommitCandidates(replayedEntries);
@@ -628,10 +716,8 @@ function resolveTargetIndex(ledger, target) {
628
716
  if ("ledgerEntryId" in target) {
629
717
  const index = ledger.findIndex((entry) => entry.id === target.ledgerEntryId);
630
718
  if (index === -1) {
631
- throw new HistoryReplayError(
632
- `Cannot find ledger entry: ${target.ledgerEntryId}`,
633
- "LEDGER_ENTRY_NOT_FOUND",
634
- 404
719
+ throw new LedgerEntryNotFoundError(
720
+ `Cannot find ledger entry: ${target.ledgerEntryId}`
635
721
  );
636
722
  }
637
723
  return index;
@@ -641,10 +727,8 @@ function resolveTargetIndex(ledger, target) {
641
727
  return index;
642
728
  }
643
729
  }
644
- throw new HistoryReplayError(
645
- `Cannot find ledger entry at or before timestamp: ${new Date(target.timestamp).toISOString()}`,
646
- "HISTORY_STATE_NOT_FOUND",
647
- 404
730
+ throw new HistoryStateNotFoundError(
731
+ `Cannot find ledger entry at or before timestamp: ${new Date(target.timestamp).toISOString()}`
648
732
  );
649
733
  }
650
734
  function collectCommitCandidates(entries) {
@@ -746,58 +830,6 @@ function registerHistoryApi(app, projectRoot) {
746
830
  });
747
831
  }
748
832
 
749
- // src/api/human-lock.ts
750
- import { humanLockApproveRequestSchema, humanLockFileParamsSchema } from "@fenglimg/fabric-shared";
751
- function registerHumanLockApi(app, projectRoot) {
752
- app.get("/api/human-lock", async (_req, res) => {
753
- try {
754
- await readAgentsMeta(projectRoot);
755
- res.json(await readHumanLock(projectRoot));
756
- } catch (error) {
757
- sendUnknownError(res, error);
758
- }
759
- });
760
- app.get(/^\/api\/human-lock\/(.+)$/, async (req, res) => {
761
- const rawFile = typeof req.params[0] === "string" ? decodeURIComponent(req.params[0]) : "";
762
- const validation = humanLockFileParamsSchema.safeParse({
763
- file: rawFile
764
- });
765
- if (!validation.success) {
766
- sendValidationError(res, "Invalid human-lock file path", validation.error.flatten());
767
- return;
768
- }
769
- try {
770
- await readAgentsMeta(projectRoot);
771
- const entry = await readHumanLockEntry(projectRoot, validation.data.file);
772
- if (entry === null) {
773
- sendError(
774
- res,
775
- 404,
776
- "HUMAN_LOCK_ENTRY_NOT_FOUND",
777
- `Cannot find human lock entry: ${validation.data.file}`
778
- );
779
- return;
780
- }
781
- res.json(entry);
782
- } catch (error) {
783
- sendUnknownError(res, error);
784
- }
785
- });
786
- app.post("/api/human-lock/approve", async (req, res) => {
787
- const validation = humanLockApproveRequestSchema.safeParse(req.body);
788
- if (!validation.success) {
789
- sendValidationError(res, "Invalid human-lock approval payload", validation.error.flatten());
790
- return;
791
- }
792
- try {
793
- await readAgentsMeta(projectRoot);
794
- res.json(await approveHumanLock(projectRoot, validation.data));
795
- } catch (error) {
796
- sendUnknownError(res, error);
797
- }
798
- });
799
- }
800
-
801
833
  // src/api/intent.ts
802
834
  import { annotateIntentRequestSchema } from "@fenglimg/fabric-shared";
803
835
 
@@ -806,7 +838,7 @@ async function annotateIntent(projectRoot, input) {
806
838
  const entries = await readLedger(projectRoot);
807
839
  const parentEntry = entries.find((entry2) => entry2.id === input.ledger_entry_id);
808
840
  if (parentEntry === void 0) {
809
- throw new Error(`Cannot find ledger entry: ${input.ledger_entry_id}`);
841
+ throw new LedgerEntryNotFoundError(`Cannot find ledger entry: ${input.ledger_entry_id}`);
810
842
  }
811
843
  const lastEntry = entries[entries.length - 1];
812
844
  if (lastEntry?.source === "human" && lastEntry.parent_ledger_entry_id === input.ledger_entry_id && lastEntry.annotation === input.annotation) {
@@ -1065,7 +1097,7 @@ function warnMissingDashboard(staticDir) {
1065
1097
  }
1066
1098
 
1067
1099
  // src/middleware/bearer-auth.ts
1068
- import { createHash as createHash2, timingSafeEqual } from "crypto";
1100
+ import { createHash, timingSafeEqual } from "crypto";
1069
1101
  function createBearerAuthMiddleware(token) {
1070
1102
  const expectedDigest = hashToken(token);
1071
1103
  return function bearerAuthMiddleware(req, res, next) {
@@ -1098,30 +1130,25 @@ function tokensMatch(token, expectedDigest) {
1098
1130
  return timingSafeEqual(hashToken(token), expectedDigest);
1099
1131
  }
1100
1132
  function hashToken(token) {
1101
- return createHash2("sha256").update(token, "utf8").digest();
1133
+ return createHash("sha256").update(token, "utf8").digest();
1102
1134
  }
1103
1135
 
1104
1136
  // src/http.ts
1105
1137
  var DEFAULT_HOST = "127.0.0.1";
1106
1138
  var NOTIFY_DEBOUNCE_MS = 200;
1107
1139
  var JsonlEventStore = class {
1108
- constructor(projectRoot, ledgerPath) {
1140
+ constructor(projectRoot) {
1109
1141
  this.projectRoot = projectRoot;
1110
- this.ledgerPath = ledgerPath;
1111
1142
  }
1112
1143
  projectRoot;
1113
- ledgerPath;
1114
1144
  async storeEvent(streamId, message) {
1115
- const eventId = randomUUID();
1116
- const entry = {
1117
- kind: "mcp-event",
1118
- eventId,
1119
- streamId,
1145
+ const eventId = randomUUID2();
1146
+ await appendEventLedgerEvent(this.projectRoot, {
1147
+ event_type: "mcp_event",
1148
+ mcp_event_id: eventId,
1149
+ stream_id: streamId,
1120
1150
  message
1121
- };
1122
- await ensureParentDirectory(this.ledgerPath);
1123
- await appendFile(this.ledgerPath, `${JSON.stringify(entry)}
1124
- `, "utf8");
1151
+ });
1125
1152
  return eventId;
1126
1153
  }
1127
1154
  async getStreamIdForEventId(eventId) {
@@ -1147,15 +1174,30 @@ var JsonlEventStore = class {
1147
1174
  return streamId;
1148
1175
  }
1149
1176
  async readEvents() {
1177
+ const { events: eventLedgerEvents } = await readEventLedger(this.projectRoot);
1178
+ const projectedEvents = eventLedgerEvents.flatMap((event) => {
1179
+ if (event.event_type !== "mcp_event") {
1180
+ return [];
1181
+ }
1182
+ return [{
1183
+ kind: "mcp-event",
1184
+ eventId: event.mcp_event_id,
1185
+ streamId: event.stream_id,
1186
+ message: event.message
1187
+ }];
1188
+ });
1189
+ if (projectedEvents.length > 0) {
1190
+ return projectedEvents;
1191
+ }
1150
1192
  let raw;
1151
1193
  try {
1152
- raw = await readFile2(this.ledgerPath, "utf8");
1194
+ raw = await readFile3(getLedgerPath(this.projectRoot), "utf8");
1153
1195
  } catch (error) {
1154
- if (isNodeError2(error) && error.code === "ENOENT") {
1196
+ if (isNodeError3(error) && error.code === "ENOENT") {
1155
1197
  try {
1156
- raw = await readFile2(getLegacyLedgerPath(this.projectRoot), "utf8");
1198
+ raw = await readFile3(getLegacyLedgerPath(this.projectRoot), "utf8");
1157
1199
  } catch (legacyError) {
1158
- if (isNodeError2(legacyError) && legacyError.code === "ENOENT") {
1200
+ if (isNodeError3(legacyError) && legacyError.code === "ENOENT") {
1159
1201
  return [];
1160
1202
  }
1161
1203
  throw legacyError;
@@ -1167,15 +1209,45 @@ var JsonlEventStore = class {
1167
1209
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => parseStoredMcpEvent(line)).filter((event) => event !== null);
1168
1210
  }
1169
1211
  };
1212
+ function handleCacheWatcherEvent(relativePath, projectRoot, sessions, timers) {
1213
+ const normalized = relativePath.replaceAll("\\", "/");
1214
+ if (normalized === ".fabric/agents.meta.json") {
1215
+ contextCache.invalidate("file_watch", projectRoot);
1216
+ clearTimeout(timers.getToolListTimer());
1217
+ timers.setToolListTimer(
1218
+ setTimeout(() => {
1219
+ notifyAllSessions(sessions, "tools/list_changed");
1220
+ }, NOTIFY_DEBOUNCE_MS)
1221
+ );
1222
+ return;
1223
+ }
1224
+ if (normalized === ".fabric/bootstrap/README.md") {
1225
+ contextCache.invalidate("file_watch", projectRoot);
1226
+ clearTimeout(timers.getAgentsMdTimer());
1227
+ timers.setAgentsMdTimer(
1228
+ setTimeout(() => {
1229
+ notifyAllSessions(sessions, "resource_updated", AGENTS_MD_RESOURCE_URI);
1230
+ }, NOTIFY_DEBOUNCE_MS)
1231
+ );
1232
+ return;
1233
+ }
1234
+ if (normalized.startsWith(".fabric/rules/") && normalized.endsWith(".md")) {
1235
+ contextCache.invalidate("file_watch", projectRoot);
1236
+ invalidateRuleSyncCooldown(projectRoot);
1237
+ }
1238
+ }
1170
1239
  function createFabricHttpApp(options) {
1171
1240
  const { projectRoot, host = DEFAULT_HOST, authToken, dashboardDistPath, dev } = options;
1172
1241
  const app = createMcpExpressApp({ host });
1173
- const ledgerPath = getLedgerPath(projectRoot);
1174
- const eventStore = new JsonlEventStore(projectRoot, ledgerPath);
1242
+ const eventStore = new JsonlEventStore(projectRoot);
1175
1243
  const sessions = /* @__PURE__ */ new Map();
1176
1244
  process.env.FABRIC_PROJECT_ROOT = projectRoot;
1177
1245
  const cacheWatcher = chokidar2.watch(
1178
- [".fabric/agents.meta.json", ".fabric/bootstrap/README.md"],
1246
+ [
1247
+ ".fabric/agents.meta.json",
1248
+ ".fabric/bootstrap/README.md",
1249
+ ".fabric/rules/**/*.md"
1250
+ ],
1179
1251
  {
1180
1252
  cwd: projectRoot,
1181
1253
  ignoreInitial: true,
@@ -1187,22 +1259,21 @@ function createFabricHttpApp(options) {
1187
1259
  );
1188
1260
  let agentsMdNotifyTimer;
1189
1261
  let toolListNotifyTimer;
1190
- cacheWatcher.on("change", (relativePath) => {
1191
- const normalized = relativePath.replaceAll("\\", "/");
1192
- if (normalized === ".fabric/agents.meta.json") {
1193
- contextCache.invalidate("file_watch", projectRoot);
1194
- clearTimeout(toolListNotifyTimer);
1195
- toolListNotifyTimer = setTimeout(() => {
1196
- notifyAllSessions(sessions, "tools/list_changed");
1197
- }, NOTIFY_DEBOUNCE_MS);
1198
- } else if (normalized === ".fabric/bootstrap/README.md") {
1199
- contextCache.invalidate("file_watch", projectRoot);
1200
- clearTimeout(agentsMdNotifyTimer);
1201
- agentsMdNotifyTimer = setTimeout(() => {
1202
- notifyAllSessions(sessions, "resource_updated", AGENTS_MD_RESOURCE_URI);
1203
- }, NOTIFY_DEBOUNCE_MS);
1204
- }
1205
- });
1262
+ const onCacheWatcherEvent = (relativePath) => {
1263
+ handleCacheWatcherEvent(relativePath, projectRoot, sessions, {
1264
+ getAgentsMdTimer: () => agentsMdNotifyTimer,
1265
+ getToolListTimer: () => toolListNotifyTimer,
1266
+ setAgentsMdTimer: (t) => {
1267
+ agentsMdNotifyTimer = t;
1268
+ },
1269
+ setToolListTimer: (t) => {
1270
+ toolListNotifyTimer = t;
1271
+ }
1272
+ });
1273
+ };
1274
+ cacheWatcher.on("change", onCacheWatcherEvent);
1275
+ cacheWatcher.on("add", onCacheWatcherEvent);
1276
+ cacheWatcher.on("unlink", onCacheWatcherEvent);
1206
1277
  let disposed = false;
1207
1278
  app.dispose = async () => {
1208
1279
  if (disposed) {
@@ -1226,7 +1297,6 @@ function createFabricHttpApp(options) {
1226
1297
  registerHistoryApi(app, projectRoot);
1227
1298
  registerScanApi(app, projectRoot);
1228
1299
  registerDoctorApi(app, projectRoot);
1229
- registerHumanLockApi(app, projectRoot);
1230
1300
  registerIntentApi(app, projectRoot);
1231
1301
  app.get("/events", createEventsHandler({ projectRoot }));
1232
1302
  app.all("/mcp", async (req, res) => {
@@ -1268,7 +1338,7 @@ async function createSession(eventStore, sessions) {
1268
1338
  const { createFabricServer } = await import("./index.js");
1269
1339
  const server = createFabricServer();
1270
1340
  const transport = new StreamableHTTPServerTransport({
1271
- sessionIdGenerator: randomUUID,
1341
+ sessionIdGenerator: randomUUID2,
1272
1342
  enableJsonResponse: true,
1273
1343
  eventStore,
1274
1344
  onsessioninitialized: async (sessionId) => {
@@ -1331,10 +1401,10 @@ function writeJsonRpcError(res, status, code, message) {
1331
1401
  id: null
1332
1402
  });
1333
1403
  }
1334
- function isNodeError2(error) {
1404
+ function isNodeError3(error) {
1335
1405
  return error instanceof Error;
1336
1406
  }
1337
1407
  export {
1338
1408
  createFabricHttpApp,
1339
- notifyAllSessions
1409
+ handleCacheWatcherEvent
1340
1410
  };