@fenglimg/fabric-server 1.6.0 → 1.7.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-PTFSYO4Y.js +2083 -0
- package/dist/{http-DJCTLGF4.js → http-6LFZLHCN.js} +269 -198
- package/dist/index.d.ts +42 -60
- package/dist/index.js +125 -666
- package/dist/static/assets/index-DEc8gkD_.js +10 -0
- package/dist/static/assets/index-FoBU5Kta.css +1 -0
- package/dist/static/index.html +9 -7
- package/package.json +3 -3
- package/dist/chunk-TZCE2K4D.js +0 -1447
- package/dist/static/assets/index-B5hhHHl2.css +0 -1
- package/dist/static/assets/index-LJh6IezM.js +0 -14
|
@@ -2,26 +2,25 @@ import {
|
|
|
2
2
|
AGENTS_MD_RESOURCE_URI,
|
|
3
3
|
AgentsMetaFileMissingError,
|
|
4
4
|
AgentsMetaInvalidError,
|
|
5
|
+
EVENT_LEDGER_PATH,
|
|
5
6
|
LEDGER_PATH,
|
|
6
7
|
LEGACY_LEDGER_PATH,
|
|
7
|
-
|
|
8
|
-
approveHumanLock,
|
|
8
|
+
appendEventLedgerEvent,
|
|
9
9
|
contextCache,
|
|
10
|
-
|
|
10
|
+
getEventLedgerPath,
|
|
11
11
|
getLedgerPath,
|
|
12
12
|
getLegacyLedgerPath,
|
|
13
13
|
getRules,
|
|
14
|
+
isNodeError,
|
|
14
15
|
readAgentsMeta,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
runDoctorReport
|
|
20
|
-
} from "./chunk-TZCE2K4D.js";
|
|
16
|
+
readEventLedger,
|
|
17
|
+
runDoctorReport,
|
|
18
|
+
sha256
|
|
19
|
+
} from "./chunk-PTFSYO4Y.js";
|
|
21
20
|
|
|
22
21
|
// src/http.ts
|
|
23
|
-
import { randomUUID } from "crypto";
|
|
24
|
-
import {
|
|
22
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
23
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
25
24
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
26
25
|
import {
|
|
27
26
|
StreamableHTTPServerTransport
|
|
@@ -117,21 +116,186 @@ function registerDoctorApi(app, projectRoot) {
|
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
// src/api/events.ts
|
|
120
|
-
import {
|
|
121
|
-
import { open, readFile, stat } from "fs/promises";
|
|
119
|
+
import { open, readFile as readFile2, stat } from "fs/promises";
|
|
122
120
|
import { join } from "path";
|
|
123
121
|
import {
|
|
124
122
|
agentsMetaSchema,
|
|
125
123
|
fabricEventSchema,
|
|
126
124
|
forensicReportSchema,
|
|
127
|
-
|
|
128
|
-
ledgerEntrySchema
|
|
125
|
+
ledgerEntrySchema as ledgerEntrySchema2
|
|
129
126
|
} from "@fenglimg/fabric-shared";
|
|
127
|
+
import { eventLedgerEventSchema } from "@fenglimg/fabric-shared";
|
|
130
128
|
import chokidar from "chokidar";
|
|
129
|
+
|
|
130
|
+
// src/services/read-ledger.ts
|
|
131
|
+
import { randomUUID } from "crypto";
|
|
132
|
+
import { access, copyFile, readFile, rm } from "fs/promises";
|
|
133
|
+
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
134
|
+
async function resolveLedgerPaths(projectRoot) {
|
|
135
|
+
const primaryPath = getLedgerPath(projectRoot);
|
|
136
|
+
const legacyPath = getLegacyLedgerPath(projectRoot);
|
|
137
|
+
const [primaryExists, legacyExists] = await Promise.all([
|
|
138
|
+
pathExists(primaryPath),
|
|
139
|
+
pathExists(legacyPath)
|
|
140
|
+
]);
|
|
141
|
+
return {
|
|
142
|
+
primaryPath,
|
|
143
|
+
legacyPath,
|
|
144
|
+
readPath: primaryExists ? primaryPath : legacyPath,
|
|
145
|
+
usingLegacy: !primaryExists && legacyExists
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function readLedger(projectRoot, options = {}) {
|
|
149
|
+
const [legacyEntries, eventEntries] = await Promise.all([
|
|
150
|
+
readLegacyLedger(projectRoot),
|
|
151
|
+
readLedgerFromEventLedger(projectRoot)
|
|
152
|
+
]);
|
|
153
|
+
const entries = mergeLedgerEntries(legacyEntries, eventEntries);
|
|
154
|
+
return entries.filter((entry) => options.source === void 0 || entry.source === options.source).filter((entry) => options.since === void 0 || entry.ts >= options.since);
|
|
155
|
+
}
|
|
156
|
+
async function readLegacyLedger(projectRoot) {
|
|
157
|
+
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
158
|
+
let raw;
|
|
159
|
+
try {
|
|
160
|
+
raw = await readFile(readPath, "utf8");
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseLedgerLine(line, index)).filter((entry) => entry !== null);
|
|
168
|
+
}
|
|
169
|
+
async function appendLedgerEntry(projectRoot, entry) {
|
|
170
|
+
const nextEntry = createStoredLedgerEntry(entry);
|
|
171
|
+
for (const affectedPath of nextEntry.affected_paths) {
|
|
172
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
173
|
+
event_type: "edit_intent_checked",
|
|
174
|
+
ts: nextEntry.ts,
|
|
175
|
+
path: affectedPath,
|
|
176
|
+
compliant: true,
|
|
177
|
+
intent: nextEntry.intent,
|
|
178
|
+
ledger_entry_id: nextEntry.id,
|
|
179
|
+
ledger_source: nextEntry.source,
|
|
180
|
+
commit_sha: nextEntry.source === "ai" ? nextEntry.commit_sha : void 0,
|
|
181
|
+
parent_sha: nextEntry.source === "human" ? nextEntry.parent_sha : void 0,
|
|
182
|
+
parent_ledger_entry_id: nextEntry.source === "human" ? nextEntry.parent_ledger_entry_id : void 0,
|
|
183
|
+
diff_stat: nextEntry.source === "human" ? nextEntry.diff_stat : void 0,
|
|
184
|
+
annotation: nextEntry.source === "human" ? nextEntry.annotation : void 0,
|
|
185
|
+
matched_rule_context_ts: null,
|
|
186
|
+
window_ms: 0
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return nextEntry;
|
|
190
|
+
}
|
|
191
|
+
function createStoredLedgerEntry(entry) {
|
|
192
|
+
return ledgerEntrySchema.parse({
|
|
193
|
+
...entry,
|
|
194
|
+
id: entry.id ?? `ledger:${randomUUID()}`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function parseLedgerLine(line, index) {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(line);
|
|
200
|
+
if (parsed.kind === "mcp-event") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const result = ledgerEntrySchema.safeParse(parsed);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...result.data,
|
|
209
|
+
id: result.data.id ?? createDerivedId(index, line)
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function readLedgerFromEventLedger(projectRoot) {
|
|
216
|
+
const events = await readEventLedger(projectRoot);
|
|
217
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
218
|
+
for (const event of events) {
|
|
219
|
+
const entry = projectLedgerEvent(event);
|
|
220
|
+
if (entry === null) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const existing = grouped.get(entry.id);
|
|
224
|
+
if (existing === void 0) {
|
|
225
|
+
grouped.set(entry.id, entry);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
grouped.set(entry.id, {
|
|
229
|
+
...existing,
|
|
230
|
+
ts: Math.min(existing.ts, entry.ts),
|
|
231
|
+
affected_paths: dedupeStrings([...existing.affected_paths, ...entry.affected_paths])
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return Array.from(grouped.values());
|
|
235
|
+
}
|
|
236
|
+
function projectLedgerEvent(event) {
|
|
237
|
+
if (event.event_type !== "edit_intent_checked") {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const base = {
|
|
241
|
+
id: event.ledger_entry_id,
|
|
242
|
+
ts: event.ts,
|
|
243
|
+
intent: event.intent,
|
|
244
|
+
affected_paths: [event.path]
|
|
245
|
+
};
|
|
246
|
+
if (event.ledger_source === "human") {
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
source: "human",
|
|
250
|
+
parent_sha: event.parent_sha ?? event.ledger_entry_id,
|
|
251
|
+
parent_ledger_entry_id: event.parent_ledger_entry_id,
|
|
252
|
+
diff_stat: event.diff_stat ?? "event-ledger",
|
|
253
|
+
annotation: event.annotation
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...base,
|
|
258
|
+
source: "ai",
|
|
259
|
+
commit_sha: event.commit_sha
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function mergeLedgerEntries(legacyEntries, eventEntries) {
|
|
263
|
+
const byId = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const entry of [...legacyEntries, ...eventEntries]) {
|
|
265
|
+
if (!byId.has(entry.id)) {
|
|
266
|
+
byId.set(entry.id, entry);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return Array.from(byId.values()).sort((left, right) => left.ts - right.ts);
|
|
270
|
+
}
|
|
271
|
+
function dedupeStrings(values) {
|
|
272
|
+
return Array.from(new Set(values));
|
|
273
|
+
}
|
|
274
|
+
function createDerivedId(index, line) {
|
|
275
|
+
return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
|
|
276
|
+
}
|
|
277
|
+
async function pathExists(path) {
|
|
278
|
+
try {
|
|
279
|
+
await access(path);
|
|
280
|
+
return true;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/api/events.ts
|
|
131
290
|
var AGENTS_META_PATH = ".fabric/agents.meta.json";
|
|
132
|
-
var HUMAN_LOCK_PATH = ".fabric/human-lock.json";
|
|
133
291
|
var FORENSIC_PATH = ".fabric/forensic.json";
|
|
134
|
-
var WATCHED_PATHS = [
|
|
292
|
+
var WATCHED_PATHS = [
|
|
293
|
+
AGENTS_META_PATH,
|
|
294
|
+
FORENSIC_PATH,
|
|
295
|
+
EVENT_LEDGER_PATH,
|
|
296
|
+
LEDGER_PATH,
|
|
297
|
+
LEGACY_LEDGER_PATH
|
|
298
|
+
];
|
|
135
299
|
var CONNECTION_LIMIT = 10;
|
|
136
300
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
137
301
|
var WATCH_DEBOUNCE_MS = 75;
|
|
@@ -173,7 +337,8 @@ function createEventsHandler(options) {
|
|
|
173
337
|
activeLedgerPath: getLedgerPath(projectRoot),
|
|
174
338
|
ledgerOffset: 0,
|
|
175
339
|
ledgerRemainder: "",
|
|
176
|
-
|
|
340
|
+
eventLedgerOffset: 0,
|
|
341
|
+
eventLedgerRemainder: "",
|
|
177
342
|
nextEventId: 1,
|
|
178
343
|
ringBuffer: new RingBuffer(RING_BUFFER_CAPACITY)
|
|
179
344
|
};
|
|
@@ -252,7 +417,8 @@ async function ensureWatcher(state, projectRoot) {
|
|
|
252
417
|
state.activeLedgerPath = ledgerState.path;
|
|
253
418
|
state.ledgerOffset = ledgerState.size;
|
|
254
419
|
state.ledgerRemainder = "";
|
|
255
|
-
state.
|
|
420
|
+
state.eventLedgerOffset = await readFileSize(getEventLedgerPath(projectRoot));
|
|
421
|
+
state.eventLedgerRemainder = "";
|
|
256
422
|
const watcher = chokidar.watch([...WATCHED_PATHS], {
|
|
257
423
|
cwd: projectRoot,
|
|
258
424
|
ignoreInitial: true,
|
|
@@ -306,13 +472,13 @@ async function readEventsForFile(state, projectRoot, relativePath) {
|
|
|
306
472
|
const event = await readMetaUpdatedEvent(projectRoot);
|
|
307
473
|
return event === null ? [] : [event];
|
|
308
474
|
}
|
|
309
|
-
if (relativePath === HUMAN_LOCK_PATH) {
|
|
310
|
-
return await readHumanLockEvents(state, projectRoot);
|
|
311
|
-
}
|
|
312
475
|
if (relativePath === FORENSIC_PATH) {
|
|
313
476
|
const event = await readDriftDetectedEvent(projectRoot);
|
|
314
477
|
return event === null ? [] : [event];
|
|
315
478
|
}
|
|
479
|
+
if (relativePath === EVENT_LEDGER_PATH) {
|
|
480
|
+
return await readEventLedgerAppendedEvents(state, projectRoot);
|
|
481
|
+
}
|
|
316
482
|
if (relativePath === LEDGER_PATH || relativePath === LEGACY_LEDGER_PATH) {
|
|
317
483
|
return await readLedgerAppendedEvents(state, projectRoot);
|
|
318
484
|
}
|
|
@@ -342,40 +508,6 @@ async function readDriftDetectedEvent(projectRoot) {
|
|
|
342
508
|
payload: parsed
|
|
343
509
|
};
|
|
344
510
|
}
|
|
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
511
|
async function readLedgerAppendedEvents(state, projectRoot) {
|
|
380
512
|
const ledgerState = await resolveLedgerWatchState(projectRoot);
|
|
381
513
|
const ledgerPath = ledgerState.path;
|
|
@@ -407,6 +539,31 @@ async function readLedgerAppendedEvents(state, projectRoot) {
|
|
|
407
539
|
await handle.close();
|
|
408
540
|
}
|
|
409
541
|
}
|
|
542
|
+
async function readEventLedgerAppendedEvents(state, projectRoot) {
|
|
543
|
+
const eventLedgerPath = getEventLedgerPath(projectRoot);
|
|
544
|
+
const nextSize = await readFileSize(eventLedgerPath);
|
|
545
|
+
if (nextSize < state.eventLedgerOffset) {
|
|
546
|
+
state.eventLedgerOffset = 0;
|
|
547
|
+
state.eventLedgerRemainder = "";
|
|
548
|
+
}
|
|
549
|
+
if (nextSize === state.eventLedgerOffset) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
const startOffset = state.eventLedgerOffset;
|
|
553
|
+
state.eventLedgerOffset = nextSize;
|
|
554
|
+
const handle = await open(eventLedgerPath, "r");
|
|
555
|
+
try {
|
|
556
|
+
const length = nextSize - startOffset;
|
|
557
|
+
const buffer = Buffer.alloc(length);
|
|
558
|
+
await handle.read(buffer, 0, length, startOffset);
|
|
559
|
+
const chunk = `${state.eventLedgerRemainder}${buffer.toString("utf8")}`;
|
|
560
|
+
const lines = chunk.split(/\r?\n/);
|
|
561
|
+
state.eventLedgerRemainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
|
|
562
|
+
return lines.map((line) => line.trim()).filter((line) => line.length > 0).map(parseEventLedgerAppendedEvent).filter((event) => event !== null);
|
|
563
|
+
} finally {
|
|
564
|
+
await handle.close();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
410
567
|
async function resolveLedgerWatchState(projectRoot) {
|
|
411
568
|
const paths = await resolveLedgerPaths(projectRoot);
|
|
412
569
|
const path = paths.usingLegacy ? paths.legacyPath : paths.primaryPath;
|
|
@@ -419,7 +576,7 @@ function parseLedgerAppendedEvent(line) {
|
|
|
419
576
|
if (parsed.kind === "mcp-event") {
|
|
420
577
|
return null;
|
|
421
578
|
}
|
|
422
|
-
const validation =
|
|
579
|
+
const validation = ledgerEntrySchema2.safeParse(parsed);
|
|
423
580
|
if (!validation.success) {
|
|
424
581
|
return null;
|
|
425
582
|
}
|
|
@@ -431,6 +588,26 @@ function parseLedgerAppendedEvent(line) {
|
|
|
431
588
|
return null;
|
|
432
589
|
}
|
|
433
590
|
}
|
|
591
|
+
function parseEventLedgerAppendedEvent(line) {
|
|
592
|
+
try {
|
|
593
|
+
const parsed = eventLedgerEventSchema.safeParse(JSON.parse(line));
|
|
594
|
+
if (!parsed.success || parsed.data.event_type !== "edit_intent_checked") {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
type: "ledger:appended",
|
|
599
|
+
payload: {
|
|
600
|
+
id: parsed.data.ledger_entry_id,
|
|
601
|
+
ts: parsed.data.ts,
|
|
602
|
+
source: "ai",
|
|
603
|
+
intent: parsed.data.intent,
|
|
604
|
+
affected_paths: [parsed.data.path]
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
434
611
|
function broadcastEvent(state, event) {
|
|
435
612
|
const payload = fabricEventSchema.parse(event);
|
|
436
613
|
const eventId = state.nextEventId++;
|
|
@@ -460,68 +637,6 @@ data: ${data}
|
|
|
460
637
|
}
|
|
461
638
|
}
|
|
462
639
|
}
|
|
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
640
|
function readLastEventId(req) {
|
|
526
641
|
const header = req.headers["last-event-id"];
|
|
527
642
|
const headerValue = Array.isArray(header) ? header[0] : header;
|
|
@@ -542,9 +657,9 @@ function normalizePath(value) {
|
|
|
542
657
|
}
|
|
543
658
|
async function readUtf8File(path) {
|
|
544
659
|
try {
|
|
545
|
-
return await
|
|
660
|
+
return await readFile2(path, "utf8");
|
|
546
661
|
} catch (error) {
|
|
547
|
-
if (
|
|
662
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
548
663
|
return null;
|
|
549
664
|
}
|
|
550
665
|
throw error;
|
|
@@ -555,13 +670,13 @@ async function readFileSize(path) {
|
|
|
555
670
|
const fileStat = await stat(path);
|
|
556
671
|
return fileStat.size;
|
|
557
672
|
} catch (error) {
|
|
558
|
-
if (
|
|
673
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
559
674
|
return 0;
|
|
560
675
|
}
|
|
561
676
|
throw error;
|
|
562
677
|
}
|
|
563
678
|
}
|
|
564
|
-
function
|
|
679
|
+
function isNodeError2(error) {
|
|
565
680
|
return error instanceof Error;
|
|
566
681
|
}
|
|
567
682
|
|
|
@@ -746,58 +861,6 @@ function registerHistoryApi(app, projectRoot) {
|
|
|
746
861
|
});
|
|
747
862
|
}
|
|
748
863
|
|
|
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
864
|
// src/api/intent.ts
|
|
802
865
|
import { annotateIntentRequestSchema } from "@fenglimg/fabric-shared";
|
|
803
866
|
|
|
@@ -1065,7 +1128,7 @@ function warnMissingDashboard(staticDir) {
|
|
|
1065
1128
|
}
|
|
1066
1129
|
|
|
1067
1130
|
// src/middleware/bearer-auth.ts
|
|
1068
|
-
import { createHash
|
|
1131
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
1069
1132
|
function createBearerAuthMiddleware(token) {
|
|
1070
1133
|
const expectedDigest = hashToken(token);
|
|
1071
1134
|
return function bearerAuthMiddleware(req, res, next) {
|
|
@@ -1098,30 +1161,25 @@ function tokensMatch(token, expectedDigest) {
|
|
|
1098
1161
|
return timingSafeEqual(hashToken(token), expectedDigest);
|
|
1099
1162
|
}
|
|
1100
1163
|
function hashToken(token) {
|
|
1101
|
-
return
|
|
1164
|
+
return createHash("sha256").update(token, "utf8").digest();
|
|
1102
1165
|
}
|
|
1103
1166
|
|
|
1104
1167
|
// src/http.ts
|
|
1105
1168
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1106
1169
|
var NOTIFY_DEBOUNCE_MS = 200;
|
|
1107
1170
|
var JsonlEventStore = class {
|
|
1108
|
-
constructor(projectRoot
|
|
1171
|
+
constructor(projectRoot) {
|
|
1109
1172
|
this.projectRoot = projectRoot;
|
|
1110
|
-
this.ledgerPath = ledgerPath;
|
|
1111
1173
|
}
|
|
1112
1174
|
projectRoot;
|
|
1113
|
-
ledgerPath;
|
|
1114
1175
|
async storeEvent(streamId, message) {
|
|
1115
|
-
const eventId =
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
eventId,
|
|
1119
|
-
streamId,
|
|
1176
|
+
const eventId = randomUUID2();
|
|
1177
|
+
await appendEventLedgerEvent(this.projectRoot, {
|
|
1178
|
+
event_type: "mcp_event",
|
|
1179
|
+
mcp_event_id: eventId,
|
|
1180
|
+
stream_id: streamId,
|
|
1120
1181
|
message
|
|
1121
|
-
};
|
|
1122
|
-
await ensureParentDirectory(this.ledgerPath);
|
|
1123
|
-
await appendFile(this.ledgerPath, `${JSON.stringify(entry)}
|
|
1124
|
-
`, "utf8");
|
|
1182
|
+
});
|
|
1125
1183
|
return eventId;
|
|
1126
1184
|
}
|
|
1127
1185
|
async getStreamIdForEventId(eventId) {
|
|
@@ -1147,15 +1205,30 @@ var JsonlEventStore = class {
|
|
|
1147
1205
|
return streamId;
|
|
1148
1206
|
}
|
|
1149
1207
|
async readEvents() {
|
|
1208
|
+
const eventLedgerEvents = await readEventLedger(this.projectRoot);
|
|
1209
|
+
const projectedEvents = eventLedgerEvents.flatMap((event) => {
|
|
1210
|
+
if (event.event_type !== "mcp_event") {
|
|
1211
|
+
return [];
|
|
1212
|
+
}
|
|
1213
|
+
return [{
|
|
1214
|
+
kind: "mcp-event",
|
|
1215
|
+
eventId: event.mcp_event_id,
|
|
1216
|
+
streamId: event.stream_id,
|
|
1217
|
+
message: event.message
|
|
1218
|
+
}];
|
|
1219
|
+
});
|
|
1220
|
+
if (projectedEvents.length > 0) {
|
|
1221
|
+
return projectedEvents;
|
|
1222
|
+
}
|
|
1150
1223
|
let raw;
|
|
1151
1224
|
try {
|
|
1152
|
-
raw = await
|
|
1225
|
+
raw = await readFile3(getLedgerPath(this.projectRoot), "utf8");
|
|
1153
1226
|
} catch (error) {
|
|
1154
|
-
if (
|
|
1227
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
1155
1228
|
try {
|
|
1156
|
-
raw = await
|
|
1229
|
+
raw = await readFile3(getLegacyLedgerPath(this.projectRoot), "utf8");
|
|
1157
1230
|
} catch (legacyError) {
|
|
1158
|
-
if (
|
|
1231
|
+
if (isNodeError3(legacyError) && legacyError.code === "ENOENT") {
|
|
1159
1232
|
return [];
|
|
1160
1233
|
}
|
|
1161
1234
|
throw legacyError;
|
|
@@ -1170,8 +1243,7 @@ var JsonlEventStore = class {
|
|
|
1170
1243
|
function createFabricHttpApp(options) {
|
|
1171
1244
|
const { projectRoot, host = DEFAULT_HOST, authToken, dashboardDistPath, dev } = options;
|
|
1172
1245
|
const app = createMcpExpressApp({ host });
|
|
1173
|
-
const
|
|
1174
|
-
const eventStore = new JsonlEventStore(projectRoot, ledgerPath);
|
|
1246
|
+
const eventStore = new JsonlEventStore(projectRoot);
|
|
1175
1247
|
const sessions = /* @__PURE__ */ new Map();
|
|
1176
1248
|
process.env.FABRIC_PROJECT_ROOT = projectRoot;
|
|
1177
1249
|
const cacheWatcher = chokidar2.watch(
|
|
@@ -1226,7 +1298,6 @@ function createFabricHttpApp(options) {
|
|
|
1226
1298
|
registerHistoryApi(app, projectRoot);
|
|
1227
1299
|
registerScanApi(app, projectRoot);
|
|
1228
1300
|
registerDoctorApi(app, projectRoot);
|
|
1229
|
-
registerHumanLockApi(app, projectRoot);
|
|
1230
1301
|
registerIntentApi(app, projectRoot);
|
|
1231
1302
|
app.get("/events", createEventsHandler({ projectRoot }));
|
|
1232
1303
|
app.all("/mcp", async (req, res) => {
|
|
@@ -1268,7 +1339,7 @@ async function createSession(eventStore, sessions) {
|
|
|
1268
1339
|
const { createFabricServer } = await import("./index.js");
|
|
1269
1340
|
const server = createFabricServer();
|
|
1270
1341
|
const transport = new StreamableHTTPServerTransport({
|
|
1271
|
-
sessionIdGenerator:
|
|
1342
|
+
sessionIdGenerator: randomUUID2,
|
|
1272
1343
|
enableJsonResponse: true,
|
|
1273
1344
|
eventStore,
|
|
1274
1345
|
onsessioninitialized: async (sessionId) => {
|
|
@@ -1331,7 +1402,7 @@ function writeJsonRpcError(res, status, code, message) {
|
|
|
1331
1402
|
id: null
|
|
1332
1403
|
});
|
|
1333
1404
|
}
|
|
1334
|
-
function
|
|
1405
|
+
function isNodeError3(error) {
|
|
1335
1406
|
return error instanceof Error;
|
|
1336
1407
|
}
|
|
1337
1408
|
export {
|