@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.
- package/dist/{chunk-U3IQH5H6.js → chunk-GU7AMRM3.js} +186 -14
- package/dist/{http-EQBDM4C7.js → http-6V75VA23.js} +142 -20
- package/dist/index.d.ts +7 -1
- package/dist/index.js +163 -76
- package/dist/static/assets/index-DvqI1Lwz.js +10 -0
- package/dist/static/index.html +13 -13
- package/package.json +3 -3
- package/dist/static/assets/index-_hQ_P7Zz.js +0 -5
|
@@ -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
|
|
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,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
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
16
|
+
} from "./chunk-GU7AMRM3.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
|
}
|
|
@@ -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, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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 };
|