@fenglimg/fabric-server 1.2.0 → 1.3.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,13 +1,97 @@
1
+ // src/constants.ts
2
+ var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
3
+
4
+ // src/cache.ts
5
+ var ContextCache = class {
6
+ constructor(defaultTtlMs = 5e3) {
7
+ this.defaultTtlMs = defaultTtlMs;
8
+ }
9
+ defaultTtlMs;
10
+ // Slot 1: raw AgentsMeta keyed by projectRoot
11
+ metaSlot = /* @__PURE__ */ new Map();
12
+ // Slot 2: GetRulesContext keyed by projectRoot
13
+ contextSlot = /* @__PURE__ */ new Map();
14
+ // Slot 3: audit sliding-window cursor keyed by projectRoot
15
+ auditSlot = /* @__PURE__ */ new Map();
16
+ // ---------------------------------------------------------------------------
17
+ // Generic get / set / invalidate
18
+ // ---------------------------------------------------------------------------
19
+ get(slot, key) {
20
+ const store = this.slotStore(slot);
21
+ const entry = store.get(key);
22
+ if (entry === void 0) {
23
+ return void 0;
24
+ }
25
+ if (entry.expiresAt !== 0 && Date.now() > entry.expiresAt) {
26
+ store.delete(key);
27
+ return void 0;
28
+ }
29
+ return entry.value;
30
+ }
31
+ set(slot, key, value, ttlMs) {
32
+ const store = this.slotStore(slot);
33
+ const resolvedTtl = ttlMs ?? this.defaultTtlMs;
34
+ const expiresAt = resolvedTtl > 0 ? Date.now() + resolvedTtl : 0;
35
+ store.set(key, { value, expiresAt });
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Audit cursor (separate API — not TTL-based)
39
+ // ---------------------------------------------------------------------------
40
+ getAuditCursor(projectRoot) {
41
+ return this.auditSlot.get(projectRoot);
42
+ }
43
+ setAuditCursor(projectRoot, cursor) {
44
+ this.auditSlot.set(projectRoot, cursor);
45
+ }
46
+ resetAuditCursor(projectRoot) {
47
+ this.auditSlot.delete(projectRoot);
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Invalidation
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Invalidate cache slots based on what changed.
54
+ *
55
+ * @param reason "meta_write" — only the meta slot for this projectRoot
56
+ * "file_watch" — meta + context slots (AGENTS.md may have changed)
57
+ * @param projectRoot Optional; if omitted, clears ALL keys in affected slots.
58
+ */
59
+ invalidate(reason, projectRoot) {
60
+ if (reason === "meta_write") {
61
+ if (projectRoot !== void 0) {
62
+ this.metaSlot.delete(projectRoot);
63
+ } else {
64
+ this.metaSlot.clear();
65
+ }
66
+ return;
67
+ }
68
+ if (projectRoot !== void 0) {
69
+ this.metaSlot.delete(projectRoot);
70
+ this.contextSlot.delete(projectRoot);
71
+ } else {
72
+ this.metaSlot.clear();
73
+ this.contextSlot.clear();
74
+ }
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Helpers
78
+ // ---------------------------------------------------------------------------
79
+ slotStore(slot) {
80
+ return slot === "meta" ? this.metaSlot : this.contextSlot;
81
+ }
82
+ };
83
+ var contextCache = new ContextCache(5e3);
84
+
1
85
  // src/services/doctor.ts
2
86
  import { createHash as createHash2 } from "crypto";
3
- import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
87
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
4
88
  import { readFile as readFile4 } from "fs/promises";
5
89
  import { isAbsolute as isAbsolute2, join as join5, posix as posix2, resolve as resolve3 } from "path";
6
90
  import { forensicReportSchema } from "@fenglimg/fabric-shared";
7
91
  import { detectFramework } from "@fenglimg/fabric-shared/node";
8
92
 
9
93
  // src/meta-reader.ts
10
- import { readFileSync } from "fs";
94
+ import { readFile } from "fs/promises";
11
95
  import { join } from "path";
12
96
  import { agentsMetaSchema } from "@fenglimg/fabric-shared";
13
97
  import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
@@ -36,26 +120,33 @@ function getAgentsMetaPath(projectRoot) {
36
120
  function resolveProjectRoot() {
37
121
  return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
38
122
  }
39
- function readAgentsMeta(projectRoot) {
123
+ async function readAgentsMeta(projectRoot) {
124
+ const cached = contextCache.get("meta", projectRoot);
125
+ if (cached !== void 0) {
126
+ return cached;
127
+ }
40
128
  const metaPath = getAgentsMetaPath(projectRoot);
41
129
  let raw;
42
130
  try {
43
- raw = readFileSync(metaPath, "utf8");
131
+ raw = await readFile(metaPath, "utf8");
44
132
  } catch (error) {
45
133
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
46
134
  throw new AgentsMetaFileMissingError(metaPath);
47
135
  }
48
136
  throw error;
49
137
  }
138
+ let parsed;
50
139
  try {
51
- return agentsMetaSchema.parse(JSON.parse(raw));
140
+ parsed = agentsMetaSchema.parse(JSON.parse(raw));
52
141
  } catch (error) {
53
142
  throw new AgentsMetaInvalidError(metaPath, error);
54
143
  }
144
+ contextCache.set("meta", projectRoot, parsed);
145
+ return parsed;
55
146
  }
56
147
 
57
148
  // src/services/audit-log.ts
58
- import { appendFile, mkdir, readFile } from "fs/promises";
149
+ import { appendFile, mkdir, open, stat } from "fs/promises";
59
150
  import { isAbsolute, join as join2, posix, relative, resolve as resolve2 } from "path";
60
151
 
61
152
  // src/services/_shared.ts
@@ -103,7 +194,9 @@ async function appendGetRulesAuditEvent(projectRoot, input) {
103
194
  async function appendEditIntentAuditEvents(projectRoot, input) {
104
195
  const ts = input.ts ?? Date.now();
105
196
  const windowMs = input.window_ms ?? DEFAULT_AUDIT_WINDOW_MS;
106
- const getRulesEntries = (await readAuditLog(projectRoot)).filter(isGetRulesAuditEntry);
197
+ const getRulesEntries = (await readAuditLog(projectRoot, { windowMs, ts })).filter(
198
+ isGetRulesAuditEntry
199
+ );
107
200
  const entries = input.affected_paths.map((affectedPath) => {
108
201
  const path = normalizeAuditPath(projectRoot, affectedPath);
109
202
  const matchedGetRules = findPrecedingGetRulesEvent(getRulesEntries, path, ts, windowMs);
@@ -119,23 +212,100 @@ async function appendEditIntentAuditEvents(projectRoot, input) {
119
212
  window_ms: windowMs
120
213
  };
121
214
  });
215
+ const compliance = {
216
+ compliant: entries.length === 0 || entries.every((e) => e.compliant),
217
+ matched_get_rules_ts: entries.length > 0 && entries[0].matched_get_rules_ts !== null ? new Date(entries[0].matched_get_rules_ts).toISOString() : null,
218
+ window_ms: windowMs
219
+ };
122
220
  if (entries.length === 0) {
123
- return [];
221
+ return { entries, compliance };
124
222
  }
125
223
  await appendAuditLogEntries(projectRoot, entries);
126
- return entries;
224
+ return { entries, compliance };
127
225
  }
128
- async function readAuditLog(projectRoot) {
226
+ async function readAuditLog(projectRoot, opts) {
227
+ if (opts === void 0) {
228
+ return readAuditLogFull(projectRoot);
229
+ }
230
+ return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
231
+ }
232
+ async function readAuditLogFull(projectRoot) {
129
233
  const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
130
234
  let raw;
131
235
  try {
132
- raw = await readFile(auditPath, "utf8");
236
+ const fileStat = await stat(auditPath);
237
+ const handle = await open(auditPath, "r");
238
+ try {
239
+ const buffer = Buffer.alloc(fileStat.size);
240
+ await handle.read(buffer, 0, fileStat.size, 0);
241
+ raw = buffer.toString("utf8");
242
+ } finally {
243
+ await handle.close();
244
+ }
245
+ } catch (error) {
246
+ if (isNodeError(error) && error.code === "ENOENT") {
247
+ return [];
248
+ }
249
+ throw error;
250
+ }
251
+ return parseAuditLogText(raw);
252
+ }
253
+ async function readAuditLogWindowed(projectRoot, ts, windowMs) {
254
+ const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
255
+ let fileSize;
256
+ try {
257
+ const fileStat = await stat(auditPath);
258
+ fileSize = fileStat.size;
133
259
  } catch (error) {
134
260
  if (isNodeError(error) && error.code === "ENOENT") {
135
261
  return [];
136
262
  }
137
263
  throw error;
138
264
  }
265
+ const cursor = contextCache.getAuditCursor(projectRoot);
266
+ const startOffset = cursor !== void 0 && cursor.offset <= fileSize ? cursor.offset : 0;
267
+ const priorRemainder = startOffset > 0 && cursor !== void 0 ? cursor.remainder : "";
268
+ const priorWindowEntries = startOffset > 0 && cursor !== void 0 ? cursor.windowEntries : [];
269
+ const effectiveStart = cursor !== void 0 && cursor.offset > fileSize ? 0 : startOffset;
270
+ let newEntries = [];
271
+ if (fileSize > effectiveStart) {
272
+ const length = fileSize - effectiveStart;
273
+ let chunk;
274
+ try {
275
+ const handle = await open(auditPath, "r");
276
+ try {
277
+ const buffer = Buffer.alloc(length);
278
+ await handle.read(buffer, 0, length, effectiveStart);
279
+ chunk = `${priorRemainder}${buffer.toString("utf8")}`;
280
+ } finally {
281
+ await handle.close();
282
+ }
283
+ } catch (error) {
284
+ contextCache.resetAuditCursor(projectRoot);
285
+ return (await readAuditLogFull(projectRoot)).filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
286
+ }
287
+ const lines = chunk.split(/\r?\n/);
288
+ const remainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
289
+ const windowEntries = [...priorWindowEntries, ...parseAuditLogText(lines.join("\n"))].filter(
290
+ (entry) => ts - entry.ts <= windowMs && entry.ts <= ts
291
+ );
292
+ contextCache.setAuditCursor(projectRoot, { offset: fileSize, remainder, windowEntries });
293
+ newEntries = windowEntries;
294
+ } else {
295
+ const windowEntries = priorWindowEntries.filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
296
+ contextCache.setAuditCursor(projectRoot, {
297
+ offset: fileSize,
298
+ remainder: cursor?.remainder ?? "",
299
+ windowEntries
300
+ });
301
+ newEntries = windowEntries;
302
+ }
303
+ if (effectiveStart === 0 && cursor !== void 0 && cursor.offset > fileSize) {
304
+ return newEntries.filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
305
+ }
306
+ return newEntries;
307
+ }
308
+ function parseAuditLogText(raw) {
139
309
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map(parseAuditLogLine).filter((entry) => entry !== null);
140
310
  }
141
311
  function findPrecedingGetRulesEvent(entries, path, ts, windowMs) {
@@ -583,7 +753,7 @@ async function readSavedForensic(projectRoot) {
583
753
  }
584
754
  async function inspectMetaRevision(projectRoot) {
585
755
  try {
586
- const meta = readAgentsMeta(projectRoot);
756
+ const meta = await readAgentsMeta(projectRoot);
587
757
  const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
588
758
  const missingFiles = [];
589
759
  let driftCount = 0;
@@ -594,7 +764,7 @@ async function inspectMetaRevision(projectRoot) {
594
764
  driftCount += 1;
595
765
  return "missing";
596
766
  }
597
- const actualHash = sha2562(readFileSync2(absolutePath, "utf8"));
767
+ const actualHash = sha2562(readFileSync(absolutePath, "utf8"));
598
768
  if (actualHash !== node.hash) {
599
769
  driftCount += 1;
600
770
  }
@@ -694,7 +864,7 @@ function findLatestGetRulesTs(entries, path, ts) {
694
864
  function readDoctorAuditMode(projectRoot) {
695
865
  const configPath = join5(projectRoot, "fabric.config.json");
696
866
  try {
697
- const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
867
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
698
868
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
699
869
  return "off";
700
870
  }
@@ -830,6 +1000,8 @@ function isMissingFileError(error) {
830
1000
  }
831
1001
 
832
1002
  export {
1003
+ AGENTS_MD_RESOURCE_URI,
1004
+ contextCache,
833
1005
  AgentsMetaFileMissingError,
834
1006
  AgentsMetaInvalidError,
835
1007
  resolveProjectRoot,
@@ -1,9 +1,11 @@
1
1
  import {
2
+ AGENTS_MD_RESOURCE_URI,
2
3
  AgentsMetaFileMissingError,
3
4
  AgentsMetaInvalidError,
4
5
  appendLedgerEntry,
5
6
  assertPathWithinProjectRoot,
6
7
  atomicWriteText,
8
+ contextCache,
7
9
  hashHumanLockedContent,
8
10
  readAgentsMeta,
9
11
  readHumanLock,
@@ -11,16 +13,17 @@ import {
11
13
  readHumanLockEntry,
12
14
  readLedger,
13
15
  runDoctorReport
14
- } from "./chunk-U3IQH5H6.js";
16
+ } from "./chunk-GU7AMRM3.js";
15
17
 
16
18
  // src/http.ts
17
- import { randomUUID as randomUUID2 } from "crypto";
19
+ import { randomUUID } from "crypto";
18
20
  import { appendFile, readFile as readFile2 } from "fs/promises";
19
21
  import { join as join3 } from "path";
20
22
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
21
23
  import {
22
24
  StreamableHTTPServerTransport
23
25
  } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
26
+ import chokidar2 from "chokidar";
24
27
 
25
28
  // src/api/_error.ts
26
29
  function sendError(res, status, code, message, details) {
@@ -111,7 +114,7 @@ function registerDoctorApi(app, projectRoot) {
111
114
  }
112
115
 
113
116
  // src/api/events.ts
114
- import { createHash, randomUUID } from "crypto";
117
+ import { createHash } from "crypto";
115
118
  import { open, readFile, stat } from "fs/promises";
116
119
  import { join } from "path";
117
120
  import {
@@ -130,6 +133,36 @@ var WATCHED_PATHS = [AGENTS_META_PATH, HUMAN_LOCK_PATH, FORENSIC_PATH, LEDGER_PA
130
133
  var CONNECTION_LIMIT = 10;
131
134
  var HEARTBEAT_INTERVAL_MS = 3e4;
132
135
  var WATCH_DEBOUNCE_MS = 75;
136
+ var RING_BUFFER_CAPACITY = 50;
137
+ var RingBuffer = class {
138
+ constructor(capacity) {
139
+ this.capacity = capacity;
140
+ this.buf = new Array(capacity).fill(void 0);
141
+ }
142
+ capacity;
143
+ buf;
144
+ head = 0;
145
+ count = 0;
146
+ push(event) {
147
+ this.buf[this.head] = event;
148
+ this.head = (this.head + 1) % this.capacity;
149
+ if (this.count < this.capacity) {
150
+ this.count++;
151
+ }
152
+ }
153
+ replayFrom(afterId) {
154
+ const result = [];
155
+ const total = this.count;
156
+ const start = this.count < this.capacity ? 0 : this.head;
157
+ for (let i = 0; i < total; i++) {
158
+ const entry = this.buf[(start + i) % this.capacity];
159
+ if (entry !== void 0 && entry.id > afterId) {
160
+ result.push(entry);
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ };
133
166
  function createEventsHandler(options) {
134
167
  const { projectRoot } = options;
135
168
  const state = {
@@ -137,7 +170,9 @@ function createEventsHandler(options) {
137
170
  pendingTimers: /* @__PURE__ */ new Map(),
138
171
  ledgerOffset: 0,
139
172
  ledgerRemainder: "",
140
- humanLockSnapshot: createEmptyHumanLockSnapshot()
173
+ humanLockSnapshot: createEmptyHumanLockSnapshot(),
174
+ nextEventId: 1,
175
+ ringBuffer: new RingBuffer(RING_BUFFER_CAPACITY)
141
176
  };
142
177
  return async function handleEvents(req, res) {
143
178
  if (state.clients.size >= CONNECTION_LIMIT) {
@@ -161,6 +196,19 @@ function createEventsHandler(options) {
161
196
  res.setHeader("X-Accel-Buffering", "no");
162
197
  res.flushHeaders?.();
163
198
  res.write(": connected\n\n");
199
+ const lastEventId = readLastEventId(req);
200
+ if (lastEventId !== void 0) {
201
+ const missed = state.ringBuffer.replayFrom(lastEventId);
202
+ for (const entry of missed) {
203
+ if (!res.writableEnded) {
204
+ res.write(`id: ${entry.id}
205
+ event: ${entry.type}
206
+ data: ${entry.data}
207
+
208
+ `);
209
+ }
210
+ }
211
+ }
164
212
  state.clients.add(res);
165
213
  const heartbeat = setInterval(() => {
166
214
  if (!res.writableEnded) {
@@ -368,11 +416,14 @@ function parseLedgerAppendedEvent(line) {
368
416
  }
369
417
  function broadcastEvent(state, event) {
370
418
  const payload = fabricEventSchema.parse(event);
371
- const frame = `id: ${randomUUID()}
419
+ const eventId = state.nextEventId++;
420
+ const data = JSON.stringify(payload);
421
+ const frame = `id: ${eventId}
372
422
  event: ${payload.type}
373
- data: ${JSON.stringify(payload)}
423
+ data: ${data}
374
424
 
375
425
  `;
426
+ state.ringBuffer.push({ id: eventId, type: payload.type, data });
376
427
  const disconnectedClients = [];
377
428
  for (const client of state.clients) {
378
429
  try {
@@ -454,6 +505,21 @@ function areSetsEqual(left, right) {
454
505
  }
455
506
  return true;
456
507
  }
508
+ function readLastEventId(req) {
509
+ const header = req.headers["last-event-id"];
510
+ const headerValue = Array.isArray(header) ? header[0] : header;
511
+ const rawUrl = req.url ?? "";
512
+ const queryStart = rawUrl.indexOf("?");
513
+ const queryString = queryStart >= 0 ? rawUrl.slice(queryStart + 1) : "";
514
+ const params = new URLSearchParams(queryString);
515
+ const queryValue = params.get("lastEventId") ?? void 0;
516
+ const raw = headerValue ?? queryValue;
517
+ if (raw === void 0 || raw.length === 0) {
518
+ return void 0;
519
+ }
520
+ const parsed = Number.parseInt(raw, 10);
521
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
522
+ }
457
523
  function normalizePath(value) {
458
524
  return value.replaceAll("\\", "/");
459
525
  }
@@ -730,7 +796,7 @@ function createApproveLedgerEntry(input) {
730
796
  function registerHumanLockApi(app, projectRoot) {
731
797
  app.get("/api/human-lock", async (_req, res) => {
732
798
  try {
733
- readAgentsMeta(projectRoot);
799
+ await readAgentsMeta(projectRoot);
734
800
  res.json(await readHumanLock(projectRoot));
735
801
  } catch (error) {
736
802
  sendUnknownError(res, error);
@@ -746,7 +812,7 @@ function registerHumanLockApi(app, projectRoot) {
746
812
  return;
747
813
  }
748
814
  try {
749
- readAgentsMeta(projectRoot);
815
+ await readAgentsMeta(projectRoot);
750
816
  const entry = await readHumanLockEntry(projectRoot, validation.data.file);
751
817
  if (entry === null) {
752
818
  sendError(
@@ -769,7 +835,7 @@ function registerHumanLockApi(app, projectRoot) {
769
835
  return;
770
836
  }
771
837
  try {
772
- readAgentsMeta(projectRoot);
838
+ await readAgentsMeta(projectRoot);
773
839
  res.json(await approveHumanLock(projectRoot, validation.data));
774
840
  } catch (error) {
775
841
  sendUnknownError(res, error);
@@ -822,7 +888,7 @@ function registerIntentApi(app, projectRoot) {
822
888
  return;
823
889
  }
824
890
  try {
825
- readAgentsMeta(projectRoot);
891
+ await readAgentsMeta(projectRoot);
826
892
  const result = await annotateIntent(projectRoot, validation.data);
827
893
  res.status(result.created ? 201 : 200).json(result);
828
894
  } catch (error) {
@@ -844,7 +910,7 @@ function registerLedgerApi(app, projectRoot) {
844
910
  return;
845
911
  }
846
912
  try {
847
- readAgentsMeta(projectRoot);
913
+ await readAgentsMeta(projectRoot);
848
914
  res.json(await readLedger(projectRoot, validation.data));
849
915
  } catch (error) {
850
916
  sendUnknownError(res, error);
@@ -856,7 +922,7 @@ function registerLedgerApi(app, projectRoot) {
856
922
  function registerRulesApi(app, projectRoot) {
857
923
  app.get("/api/rules", async (_req, res) => {
858
924
  try {
859
- res.json(readAgentsMeta(projectRoot));
925
+ res.json(await readAgentsMeta(projectRoot));
860
926
  } catch (error) {
861
927
  sendUnknownError(res, error);
862
928
  }
@@ -893,7 +959,7 @@ function createScanReport(targetInput = process.cwd()) {
893
959
  const framework = detectFramework(target);
894
960
  const readmeQuality = getReadmeQuality(target);
895
961
  const hasContributing = existsSync(join2(target, "CONTRIBUTING.md"));
896
- const hasExistingFabric = existsSync(join2(target, "AGENTS.md")) || existsSync(join2(target, ".fabric"));
962
+ const hasExistingFabric = existsSync(join2(target, ".fabric", "bootstrap", "README.md")) || existsSync(join2(target, ".fabric"));
897
963
  const walkResult = walkFiles(target, DEFAULT_IGNORES);
898
964
  return {
899
965
  target,
@@ -970,18 +1036,18 @@ function toPosixPath(path) {
970
1036
  function buildRecommendations(input) {
971
1037
  const recommendations = [];
972
1038
  if (!input.hasExistingFabric) {
973
- recommendations.push("L0: Run fab init to scaffold AGENTS.md with TODO markers.");
1039
+ recommendations.push("L0: Run fab init to scaffold .fabric/bootstrap/README.md with TODO markers.");
974
1040
  }
975
1041
  if (input.readmeQuality === "stub") {
976
- recommendations.push("L0: Expand README.md before promoting project facts into AGENTS.md references.");
1042
+ recommendations.push("L0: Expand README.md before promoting project facts into Fabric references.");
977
1043
  }
978
1044
  if (!input.hasContributing) {
979
- recommendations.push("L0: Add CONTRIBUTING.md or leave an AGENTS.md TODO reference for contribution flow.");
1045
+ recommendations.push("L0: Add CONTRIBUTING.md or leave a bootstrap TODO reference for contribution flow.");
980
1046
  }
981
1047
  if (input.framework.kind === "unknown") {
982
1048
  recommendations.push("L1: Add tech-stack TODOs manually because no framework marker was detected.");
983
1049
  } else {
984
- recommendations.push(`L1: Review ${input.framework.kind} directories for future scoped AGENTS.md files.`);
1050
+ recommendations.push(`L1: Review ${input.framework.kind} directories for future scoped Fabric rule files.`);
985
1051
  }
986
1052
  return recommendations;
987
1053
  }
@@ -1062,13 +1128,14 @@ function hashToken(token) {
1062
1128
  // src/http.ts
1063
1129
  var DEFAULT_HOST = "127.0.0.1";
1064
1130
  var LEDGER_FILE = ".intent-ledger.jsonl";
1131
+ var NOTIFY_DEBOUNCE_MS = 200;
1065
1132
  var JsonlEventStore = class {
1066
1133
  constructor(ledgerPath) {
1067
1134
  this.ledgerPath = ledgerPath;
1068
1135
  }
1069
1136
  ledgerPath;
1070
1137
  async storeEvent(streamId, message) {
1071
- const eventId = randomUUID2();
1138
+ const eventId = randomUUID();
1072
1139
  const entry = {
1073
1140
  kind: "mcp-event",
1074
1141
  eventId,
@@ -1121,11 +1188,51 @@ function createFabricHttpApp(options) {
1121
1188
  const eventStore = new JsonlEventStore(ledgerPath);
1122
1189
  const sessions = /* @__PURE__ */ new Map();
1123
1190
  process.env.FABRIC_PROJECT_ROOT = projectRoot;
1191
+ const cacheWatcher = chokidar2.watch(
1192
+ [".fabric/agents.meta.json", ".fabric/bootstrap/README.md"],
1193
+ {
1194
+ cwd: projectRoot,
1195
+ ignoreInitial: true,
1196
+ awaitWriteFinish: {
1197
+ stabilityThreshold: 120,
1198
+ pollInterval: 20
1199
+ }
1200
+ }
1201
+ );
1202
+ let agentsMdNotifyTimer;
1203
+ let toolListNotifyTimer;
1204
+ cacheWatcher.on("change", (relativePath) => {
1205
+ const normalized = relativePath.replaceAll("\\", "/");
1206
+ if (normalized === ".fabric/agents.meta.json") {
1207
+ contextCache.invalidate("file_watch", projectRoot);
1208
+ clearTimeout(toolListNotifyTimer);
1209
+ toolListNotifyTimer = setTimeout(() => {
1210
+ notifyAllSessions(sessions, "tools/list_changed");
1211
+ }, NOTIFY_DEBOUNCE_MS);
1212
+ } else if (normalized === ".fabric/bootstrap/README.md") {
1213
+ contextCache.invalidate("file_watch", projectRoot);
1214
+ clearTimeout(agentsMdNotifyTimer);
1215
+ agentsMdNotifyTimer = setTimeout(() => {
1216
+ notifyAllSessions(sessions, "resource_updated", AGENTS_MD_RESOURCE_URI);
1217
+ }, NOTIFY_DEBOUNCE_MS);
1218
+ }
1219
+ });
1220
+ let disposed = false;
1221
+ app.dispose = async () => {
1222
+ if (disposed) {
1223
+ return;
1224
+ }
1225
+ disposed = true;
1226
+ clearTimeout(agentsMdNotifyTimer);
1227
+ clearTimeout(toolListNotifyTimer);
1228
+ await cacheWatcher.close();
1229
+ };
1124
1230
  app.disable("x-powered-by");
1125
1231
  if (authToken !== void 0) {
1126
1232
  const bearerAuth = createBearerAuthMiddleware(authToken);
1127
1233
  app.use("/api", bearerAuth);
1128
1234
  app.use("/events", bearerAuth);
1235
+ app.use("/mcp", bearerAuth);
1129
1236
  }
1130
1237
  registerRulesApi(app, projectRoot);
1131
1238
  registerLedgerApi(app, projectRoot);
@@ -1156,11 +1263,25 @@ function createFabricHttpApp(options) {
1156
1263
  registerDashboardStatic(app, { dashboardDistPath, dev });
1157
1264
  return app;
1158
1265
  }
1266
+ function notifyAllSessions(sessions, kind, uri) {
1267
+ for (const { server } of sessions.values()) {
1268
+ try {
1269
+ if (kind === "tools/list_changed") {
1270
+ server.sendToolListChanged();
1271
+ } else if (kind === "resources/list_changed") {
1272
+ server.sendResourceListChanged();
1273
+ } else if (kind === "resource_updated" && uri !== void 0) {
1274
+ void server.server.sendResourceUpdated({ uri });
1275
+ }
1276
+ } catch {
1277
+ }
1278
+ }
1279
+ }
1159
1280
  async function createSession(eventStore, sessions) {
1160
1281
  const { createFabricServer } = await import("./index.js");
1161
1282
  const server = createFabricServer();
1162
1283
  const transport = new StreamableHTTPServerTransport({
1163
- sessionIdGenerator: randomUUID2,
1284
+ sessionIdGenerator: randomUUID,
1164
1285
  enableJsonResponse: true,
1165
1286
  eventStore,
1166
1287
  onsessioninitialized: async (sessionId) => {
@@ -1227,5 +1348,6 @@ function isNodeError2(error) {
1227
1348
  return error instanceof Error;
1228
1349
  }
1229
1350
  export {
1230
- createFabricHttpApp
1351
+ createFabricHttpApp,
1352
+ notifyAllSessions
1231
1353
  };
package/dist/index.d.ts CHANGED
@@ -61,6 +61,12 @@ declare function runDoctorAuditReport(target: string, options?: {
61
61
  windowMs?: number;
62
62
  }): Promise<DoctorAuditReport>;
63
63
 
64
+ /**
65
+ * Shared constants used across the server package.
66
+ */
67
+ /** MCP resource URI for the project's bootstrap README (L0 rules) file. */
68
+ declare const AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
69
+
64
70
  declare function createFabricServer(): McpServer;
65
71
  declare function startStdioServer(): Promise<void>;
66
72
  declare function startHttpServer(options: {
@@ -72,4 +78,4 @@ declare function startHttpServer(options: {
72
78
  dev?: boolean;
73
79
  }): Promise<Server>;
74
80
 
75
- export { type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
81
+ export { AGENTS_MD_RESOURCE_URI, type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };