@fenglimg/fabric-server 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-U3IQH5H6.js → chunk-DQ7RCYKB.js} +180 -14
- package/dist/{http-FL3MB4L2.js → http-YPXWM5QS.js} +127 -15
- package/dist/index.d.ts +7 -1
- package/dist/index.js +148 -75
- package/dist/static/assets/index-Btq99IfR.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
- package/dist/static/assets/index-BiK8yn_c.js +0 -5
|
@@ -1,13 +1,97 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var AGENTS_MD_RESOURCE_URI = "fabric://agents-md";
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,93 @@ 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
|
-
|
|
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 effectiveStart = cursor !== void 0 && cursor.offset > fileSize ? 0 : startOffset;
|
|
269
|
+
let newEntries = [];
|
|
270
|
+
if (fileSize > effectiveStart) {
|
|
271
|
+
const length = fileSize - effectiveStart;
|
|
272
|
+
let chunk;
|
|
273
|
+
try {
|
|
274
|
+
const handle = await open(auditPath, "r");
|
|
275
|
+
try {
|
|
276
|
+
const buffer = Buffer.alloc(length);
|
|
277
|
+
await handle.read(buffer, 0, length, effectiveStart);
|
|
278
|
+
chunk = `${priorRemainder}${buffer.toString("utf8")}`;
|
|
279
|
+
} finally {
|
|
280
|
+
await handle.close();
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
contextCache.resetAuditCursor(projectRoot);
|
|
284
|
+
return readAuditLogFull(projectRoot);
|
|
285
|
+
}
|
|
286
|
+
const lines = chunk.split(/\r?\n/);
|
|
287
|
+
const remainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
|
|
288
|
+
contextCache.setAuditCursor(projectRoot, { offset: fileSize, remainder });
|
|
289
|
+
newEntries = parseAuditLogText(lines.join("\n"));
|
|
290
|
+
} else {
|
|
291
|
+
contextCache.setAuditCursor(projectRoot, {
|
|
292
|
+
offset: fileSize,
|
|
293
|
+
remainder: cursor?.remainder ?? ""
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (effectiveStart === 0 && cursor !== void 0 && cursor.offset > fileSize) {
|
|
297
|
+
return newEntries.filter((e) => ts - e.ts <= windowMs);
|
|
298
|
+
}
|
|
299
|
+
return newEntries.filter((e) => ts - e.ts <= windowMs && e.ts <= ts);
|
|
300
|
+
}
|
|
301
|
+
function parseAuditLogText(raw) {
|
|
139
302
|
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map(parseAuditLogLine).filter((entry) => entry !== null);
|
|
140
303
|
}
|
|
141
304
|
function findPrecedingGetRulesEvent(entries, path, ts, windowMs) {
|
|
@@ -174,6 +337,7 @@ async function appendAuditLogEntries(projectRoot, entries) {
|
|
|
174
337
|
await mkdir(auditDir, { recursive: true });
|
|
175
338
|
await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
176
339
|
`, "utf8");
|
|
340
|
+
contextCache.resetAuditCursor(projectRoot);
|
|
177
341
|
}
|
|
178
342
|
function parseAuditLogLine(line) {
|
|
179
343
|
try {
|
|
@@ -583,7 +747,7 @@ async function readSavedForensic(projectRoot) {
|
|
|
583
747
|
}
|
|
584
748
|
async function inspectMetaRevision(projectRoot) {
|
|
585
749
|
try {
|
|
586
|
-
const meta = readAgentsMeta(projectRoot);
|
|
750
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
587
751
|
const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
|
|
588
752
|
const missingFiles = [];
|
|
589
753
|
let driftCount = 0;
|
|
@@ -594,7 +758,7 @@ async function inspectMetaRevision(projectRoot) {
|
|
|
594
758
|
driftCount += 1;
|
|
595
759
|
return "missing";
|
|
596
760
|
}
|
|
597
|
-
const actualHash = sha2562(
|
|
761
|
+
const actualHash = sha2562(readFileSync(absolutePath, "utf8"));
|
|
598
762
|
if (actualHash !== node.hash) {
|
|
599
763
|
driftCount += 1;
|
|
600
764
|
}
|
|
@@ -694,7 +858,7 @@ function findLatestGetRulesTs(entries, path, ts) {
|
|
|
694
858
|
function readDoctorAuditMode(projectRoot) {
|
|
695
859
|
const configPath = join5(projectRoot, "fabric.config.json");
|
|
696
860
|
try {
|
|
697
|
-
const parsed = JSON.parse(
|
|
861
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
698
862
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
699
863
|
return "off";
|
|
700
864
|
}
|
|
@@ -830,6 +994,8 @@ function isMissingFileError(error) {
|
|
|
830
994
|
}
|
|
831
995
|
|
|
832
996
|
export {
|
|
997
|
+
AGENTS_MD_RESOURCE_URI,
|
|
998
|
+
contextCache,
|
|
833
999
|
AgentsMetaFileMissingError,
|
|
834
1000
|
AgentsMetaInvalidError,
|
|
835
1001
|
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-
|
|
16
|
+
} from "./chunk-DQ7RCYKB.js";
|
|
15
17
|
|
|
16
18
|
// src/http.ts
|
|
17
|
-
import { randomUUID
|
|
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
|
|
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
|
|
419
|
+
const eventId = state.nextEventId++;
|
|
420
|
+
const data = JSON.stringify(payload);
|
|
421
|
+
const frame = `id: ${eventId}
|
|
372
422
|
event: ${payload.type}
|
|
373
|
-
data: ${
|
|
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
|
}
|
|
@@ -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 =
|
|
1138
|
+
const eventId = randomUUID();
|
|
1072
1139
|
const entry = {
|
|
1073
1140
|
kind: "mcp-event",
|
|
1074
1141
|
eventId,
|
|
@@ -1121,11 +1188,41 @@ 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", "AGENTS.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 === "AGENTS.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
|
+
});
|
|
1124
1220
|
app.disable("x-powered-by");
|
|
1125
1221
|
if (authToken !== void 0) {
|
|
1126
1222
|
const bearerAuth = createBearerAuthMiddleware(authToken);
|
|
1127
1223
|
app.use("/api", bearerAuth);
|
|
1128
1224
|
app.use("/events", bearerAuth);
|
|
1225
|
+
app.use("/mcp", bearerAuth);
|
|
1129
1226
|
}
|
|
1130
1227
|
registerRulesApi(app, projectRoot);
|
|
1131
1228
|
registerLedgerApi(app, projectRoot);
|
|
@@ -1156,11 +1253,25 @@ function createFabricHttpApp(options) {
|
|
|
1156
1253
|
registerDashboardStatic(app, { dashboardDistPath, dev });
|
|
1157
1254
|
return app;
|
|
1158
1255
|
}
|
|
1256
|
+
function notifyAllSessions(sessions, kind, uri) {
|
|
1257
|
+
for (const { server } of sessions.values()) {
|
|
1258
|
+
try {
|
|
1259
|
+
if (kind === "tools/list_changed") {
|
|
1260
|
+
server.sendToolListChanged();
|
|
1261
|
+
} else if (kind === "resources/list_changed") {
|
|
1262
|
+
server.sendResourceListChanged();
|
|
1263
|
+
} else if (kind === "resource_updated" && uri !== void 0) {
|
|
1264
|
+
void server.server.sendResourceUpdated({ uri });
|
|
1265
|
+
}
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1159
1270
|
async function createSession(eventStore, sessions) {
|
|
1160
1271
|
const { createFabricServer } = await import("./index.js");
|
|
1161
1272
|
const server = createFabricServer();
|
|
1162
1273
|
const transport = new StreamableHTTPServerTransport({
|
|
1163
|
-
sessionIdGenerator:
|
|
1274
|
+
sessionIdGenerator: randomUUID,
|
|
1164
1275
|
enableJsonResponse: true,
|
|
1165
1276
|
eventStore,
|
|
1166
1277
|
onsessioninitialized: async (sessionId) => {
|
|
@@ -1227,5 +1338,6 @@ function isNodeError2(error) {
|
|
|
1227
1338
|
return error instanceof Error;
|
|
1228
1339
|
}
|
|
1229
1340
|
export {
|
|
1230
|
-
createFabricHttpApp
|
|
1341
|
+
createFabricHttpApp,
|
|
1342
|
+
notifyAllSessions
|
|
1231
1343
|
};
|
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 AGENTS.md (L0 rules) file. */
|
|
68
|
+
declare const AGENTS_MD_RESOURCE_URI = "fabric://agents-md";
|
|
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 };
|