@fenglimg/fabric-server 2.0.0-rc.28 → 2.0.0-rc.29
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-EEVSGTHG.js → chunk-CTQ4UMO4.js} +255 -165
- package/dist/{http-3IIPL3HQ.js → http-TAI5X7U5.js} +26 -2
- package/dist/index.d.ts +29 -9
- package/dist/index.js +65 -89
- package/package.json +2 -2
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
readEventLedger,
|
|
15
15
|
runDoctorReport,
|
|
16
16
|
sha256
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CTQ4UMO4.js";
|
|
18
18
|
|
|
19
19
|
// src/http.ts
|
|
20
20
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -972,6 +972,16 @@ function createBearerAuthMiddleware(token) {
|
|
|
972
972
|
next();
|
|
973
973
|
};
|
|
974
974
|
}
|
|
975
|
+
function createLoopbackDenyMiddleware() {
|
|
976
|
+
return function loopbackDenyMiddleware(_req, res, _next) {
|
|
977
|
+
sendError(
|
|
978
|
+
res,
|
|
979
|
+
401,
|
|
980
|
+
"UNAUTHORIZED",
|
|
981
|
+
"FABRIC_AUTH_TOKEN is not set. Either export FABRIC_AUTH_TOKEN=<secret> before running `fab serve`, or pass `--allow-loopback-no-auth` to explicitly opt in to unauthenticated loopback access (security risk)."
|
|
982
|
+
);
|
|
983
|
+
};
|
|
984
|
+
}
|
|
975
985
|
function readAuthorizationHeader(value) {
|
|
976
986
|
if (typeof value === "string" && value.length > 0) {
|
|
977
987
|
return value;
|
|
@@ -998,6 +1008,10 @@ function hashToken(token) {
|
|
|
998
1008
|
// src/http.ts
|
|
999
1009
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1000
1010
|
var NOTIFY_DEBOUNCE_MS = 200;
|
|
1011
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
1012
|
+
function isLoopbackHost(host) {
|
|
1013
|
+
return LOOPBACK_HOSTS.has(host);
|
|
1014
|
+
}
|
|
1001
1015
|
var JsonlEventStore = class {
|
|
1002
1016
|
constructor(projectRoot) {
|
|
1003
1017
|
this.projectRoot = projectRoot;
|
|
@@ -1089,7 +1103,12 @@ function handleCacheWatcherEvent(relativePath, projectRoot, sessions, timers) {
|
|
|
1089
1103
|
}
|
|
1090
1104
|
}
|
|
1091
1105
|
function createFabricHttpApp(options) {
|
|
1092
|
-
const { projectRoot, host = DEFAULT_HOST, authToken } = options;
|
|
1106
|
+
const { projectRoot, host = DEFAULT_HOST, authToken, allowLoopbackNoAuth = false } = options;
|
|
1107
|
+
if (allowLoopbackNoAuth && authToken === void 0 && !isLoopbackHost(host)) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`createFabricHttpApp: allowLoopbackNoAuth=true requires a loopback host (127.0.0.1 / localhost / ::1); got ${JSON.stringify(host)}. Either bind to loopback or set FABRIC_AUTH_TOKEN.`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1093
1112
|
const app = createMcpExpressApp({ host });
|
|
1094
1113
|
const eventStore = new JsonlEventStore(projectRoot);
|
|
1095
1114
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -1142,6 +1161,11 @@ function createFabricHttpApp(options) {
|
|
|
1142
1161
|
app.use("/api", bearerAuth);
|
|
1143
1162
|
app.use("/events", bearerAuth);
|
|
1144
1163
|
app.use("/mcp", bearerAuth);
|
|
1164
|
+
} else if (!allowLoopbackNoAuth) {
|
|
1165
|
+
const denyAll = createLoopbackDenyMiddleware();
|
|
1166
|
+
app.use("/api", denyAll);
|
|
1167
|
+
app.use("/events", denyAll);
|
|
1168
|
+
app.use("/mcp", denyAll);
|
|
1145
1169
|
}
|
|
1146
1170
|
registerKnowledgeApi(app, projectRoot);
|
|
1147
1171
|
registerKnowledgeContextApi(app, projectRoot);
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ type DoctorIssue = {
|
|
|
40
40
|
path?: string;
|
|
41
41
|
actionHint?: string;
|
|
42
42
|
};
|
|
43
|
+
type DoctorPayloadLimits = {
|
|
44
|
+
warn_bytes: number;
|
|
45
|
+
hard_bytes: number;
|
|
46
|
+
source: "default" | "config";
|
|
47
|
+
};
|
|
43
48
|
type DoctorSummary = {
|
|
44
49
|
target: string;
|
|
45
50
|
framework: {
|
|
@@ -60,6 +65,7 @@ type DoctorSummary = {
|
|
|
60
65
|
warningCount: number;
|
|
61
66
|
infoCount: number;
|
|
62
67
|
targetFiles: Record<string, boolean>;
|
|
68
|
+
payload_limits: DoctorPayloadLimits;
|
|
63
69
|
};
|
|
64
70
|
type DoctorReport = {
|
|
65
71
|
status: DoctorStatus;
|
|
@@ -156,7 +162,7 @@ type ArchiveHistoryReport = {
|
|
|
156
162
|
declare function runDoctorArchiveHistory(projectRoot: string, options: {
|
|
157
163
|
since: number;
|
|
158
164
|
}): Promise<ArchiveHistoryReport>;
|
|
159
|
-
type EnrichDescriptionsMode = "auto" | "interactive";
|
|
165
|
+
type EnrichDescriptionsMode = "auto" | "preview" | "readonly" | "interactive";
|
|
160
166
|
type EnrichDescriptionsCandidate = {
|
|
161
167
|
path: string;
|
|
162
168
|
missing: Array<"intent_clues" | "tech_stack" | "impact" | "must_read_if">;
|
|
@@ -194,13 +200,13 @@ type WriteKnowledgeMetaOptions = {
|
|
|
194
200
|
* guideline+process = deferred to rc.25 LLM-judge). Cited ids absent from
|
|
195
201
|
* this map fall into the `cite_id_unresolved` bucket.
|
|
196
202
|
*
|
|
197
|
-
* **
|
|
198
|
-
* are the
|
|
199
|
-
* | "
|
|
200
|
-
*
|
|
201
|
-
* `@fenglimg/fabric-shared`.
|
|
202
|
-
*
|
|
203
|
-
* (TASK-08 doctor)
|
|
203
|
+
* **Plural knowledge_type contract (rc.29 BUG-C1 unification):** the returned
|
|
204
|
+
* map values are the PLURAL `KnowledgeType` enum (`"models" | "decisions" |
|
|
205
|
+
* "guidelines" | "pitfalls" | "processes"`) — matching disk frontmatter,
|
|
206
|
+
* filesystem layout, MCP I/O surface, and the canonical `KnowledgeTypeSchema`
|
|
207
|
+
* exported from `@fenglimg/fabric-shared`. Legacy singular frontmatter is
|
|
208
|
+
* normalized at parse time (see `SINGULAR_TO_PLURAL` in `parseFrontmatter`);
|
|
209
|
+
* downstream callers (TASK-08 doctor) match against the plural enum.
|
|
204
210
|
*
|
|
205
211
|
* Both team (KT-*) and personal (KP-*) entries are included — they live in
|
|
206
212
|
* the same `meta.nodes` map.
|
|
@@ -382,6 +388,19 @@ interface KnowledgeSyncOptions {
|
|
|
382
388
|
mode?: "incremental" | "full";
|
|
383
389
|
/** When true, invalid frontmatter throws RuleValidationError (default: false — collect as warning). */
|
|
384
390
|
throwOnInvalidFrontmatter?: boolean;
|
|
391
|
+
/**
|
|
392
|
+
* v2.0.0-rc.29 TASK-005 (BUG-G1): when true, `ensureKnowledgeFresh`
|
|
393
|
+
* synchronously follows a drift detection with a `reconcileKnowledge`
|
|
394
|
+
* call to materialize the auto-heal (rewrite agents.meta.json + emit a
|
|
395
|
+
* paired `knowledge_meta_auto_healed` event). Default false preserves
|
|
396
|
+
* the rc.28 hot-path semantics where drift detection never blocks the
|
|
397
|
+
* MCP read on a meta rebuild. Opt-in is intended for callers that can
|
|
398
|
+
* tolerate ~tens-of-ms extra latency in exchange for the invariant
|
|
399
|
+
* "every knowledge_drift_detected has a paired heal event in the same
|
|
400
|
+
* tail window." Audit (BUG-G1) found 5/72 drifts healed on this repo
|
|
401
|
+
* (~7%) because the hot path emitted detect-only events.
|
|
402
|
+
*/
|
|
403
|
+
autoHealOnDrift?: boolean;
|
|
385
404
|
}
|
|
386
405
|
interface StructuredWarning {
|
|
387
406
|
code: string;
|
|
@@ -432,7 +451,7 @@ interface ReconcileKnowledgeOptions {
|
|
|
432
451
|
* plan_context call's auto-heal, which leaves the entry undiscoverable in
|
|
433
452
|
* the description_index window between approve and the next hint call.
|
|
434
453
|
*/
|
|
435
|
-
trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "post-approve" | "post-modify";
|
|
454
|
+
trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "auto-heal-after-drift" | "post-approve" | "post-modify";
|
|
436
455
|
}
|
|
437
456
|
/**
|
|
438
457
|
* Full scan + rewrites agents.meta.json with ground-truth disk state + emits
|
|
@@ -501,6 +520,7 @@ declare function startHttpServer(options: {
|
|
|
501
520
|
projectRoot: string;
|
|
502
521
|
host?: string;
|
|
503
522
|
authToken?: string;
|
|
523
|
+
allowLoopbackNoAuth?: boolean;
|
|
504
524
|
}): Promise<Server>;
|
|
505
525
|
|
|
506
526
|
export { AGENTS_MD_RESOURCE_URI, type AcquireOptions, type ArchiveHistoryEntry, type ArchiveHistoryReport, type CiteCoverageReport, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, type LockState, type PlanContextInput, type PlanContextResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, ServeLockHeldError, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, acquireLock, appendEventLedgerEvent, buildKnowledgeMeta, checkLockOrThrow, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, enrichDescriptions, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, isSameKnowledgeTestIndex, loadKbIdTypeMap, planContext, readLockState, readSelectionToken, reconcileKnowledge, releaseLock, reviewKnowledge, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorFix, runDoctorReport, stableStringify, startHttpServer, startStdioServer, writeKnowledgeMeta };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
loadKbIdTypeMap,
|
|
27
27
|
normalizeKnowledgePath,
|
|
28
28
|
readLockState,
|
|
29
|
+
readPayloadLimits,
|
|
30
|
+
readSelectionTokenTtlMs,
|
|
29
31
|
reconcileKnowledge,
|
|
30
32
|
releaseLock,
|
|
31
33
|
resolveProjectRoot,
|
|
@@ -37,12 +39,12 @@ import {
|
|
|
37
39
|
sha256,
|
|
38
40
|
stableStringify,
|
|
39
41
|
writeKnowledgeMeta
|
|
40
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-CTQ4UMO4.js";
|
|
41
43
|
|
|
42
44
|
// src/index.ts
|
|
43
|
-
import { existsSync as
|
|
45
|
+
import { existsSync as existsSync3 } from "fs";
|
|
44
46
|
import { readFile as readFile5 } from "fs/promises";
|
|
45
|
-
import { join as
|
|
47
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
46
48
|
import { fileURLToPath } from "url";
|
|
47
49
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
48
50
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -155,29 +157,11 @@ import {
|
|
|
155
157
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
156
158
|
import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
157
159
|
|
|
158
|
-
// src/config-loader.ts
|
|
159
|
-
import { existsSync, readFileSync } from "fs";
|
|
160
|
-
import { join } from "path";
|
|
161
|
-
function readFabricConfig(projectRoot) {
|
|
162
|
-
const configPath = join(projectRoot, "fabric.config.json");
|
|
163
|
-
if (!existsSync(configPath)) {
|
|
164
|
-
return {};
|
|
165
|
-
}
|
|
166
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
167
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
168
|
-
throw new Error(`Expected object in ${configPath}`);
|
|
169
|
-
}
|
|
170
|
-
return parsed;
|
|
171
|
-
}
|
|
172
|
-
function readPayloadLimits(projectRoot) {
|
|
173
|
-
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
160
|
// src/services/extract-knowledge.ts
|
|
177
|
-
import { existsSync
|
|
161
|
+
import { existsSync } from "fs";
|
|
178
162
|
import { readFile } from "fs/promises";
|
|
179
163
|
import { homedir } from "os";
|
|
180
|
-
import { join
|
|
164
|
+
import { join, relative } from "path";
|
|
181
165
|
import {
|
|
182
166
|
PROPOSED_REASON_DESCRIPTIONS
|
|
183
167
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
@@ -185,9 +169,9 @@ var TEAM_PENDING_REL = ".fabric/knowledge/pending";
|
|
|
185
169
|
var SLUG_MAX_LENGTH = 40;
|
|
186
170
|
function pendingBase(layer, projectRoot) {
|
|
187
171
|
if (layer === "personal") {
|
|
188
|
-
return
|
|
172
|
+
return join(resolvePersonalRoot(), ".fabric", "knowledge", "pending");
|
|
189
173
|
}
|
|
190
|
-
return
|
|
174
|
+
return join(projectRoot, TEAM_PENDING_REL);
|
|
191
175
|
}
|
|
192
176
|
function resolvePersonalRoot() {
|
|
193
177
|
return process.env.FABRIC_HOME ?? homedir();
|
|
@@ -243,10 +227,10 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
243
227
|
});
|
|
244
228
|
}
|
|
245
229
|
const baseDir = pendingBase(layer, projectRoot);
|
|
246
|
-
const absolutePath =
|
|
230
|
+
const absolutePath = join(baseDir, input.type, `${sanitizedSlug}.md`);
|
|
247
231
|
const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
248
232
|
await ensureParentDirectory(absolutePath);
|
|
249
|
-
if (
|
|
233
|
+
if (existsSync(absolutePath)) {
|
|
250
234
|
const existing = await readFile(absolutePath, "utf8");
|
|
251
235
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
252
236
|
if (existingKey === idempotencyKey) {
|
|
@@ -584,7 +568,7 @@ import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-sh
|
|
|
584
568
|
// src/services/plan-context.ts
|
|
585
569
|
import { minimatch } from "minimatch";
|
|
586
570
|
import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
587
|
-
var
|
|
571
|
+
var SELECTION_TOKEN_TTL_DEFAULT_MS = 5 * 60 * 1e3;
|
|
588
572
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
589
573
|
function assertPathInSandbox(rawPath) {
|
|
590
574
|
if (rawPath === "**" || rawPath === "*") return;
|
|
@@ -644,7 +628,8 @@ async function planContext(projectRoot, input) {
|
|
|
644
628
|
});
|
|
645
629
|
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
646
630
|
const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
|
|
647
|
-
const
|
|
631
|
+
const ttlMs = readSelectionTokenTtlMs(projectRoot) ?? SELECTION_TOKEN_TTL_DEFAULT_MS;
|
|
632
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds, Date.now(), ttlMs);
|
|
648
633
|
const result = {
|
|
649
634
|
revision_hash: meta.revision,
|
|
650
635
|
stale,
|
|
@@ -694,7 +679,7 @@ function readSelectionToken(token, now = Date.now()) {
|
|
|
694
679
|
}
|
|
695
680
|
return state2;
|
|
696
681
|
}
|
|
697
|
-
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
682
|
+
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now(), ttlMs = SELECTION_TOKEN_TTL_DEFAULT_MS) {
|
|
698
683
|
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
699
684
|
selectionTokenCache.set(token, {
|
|
700
685
|
token,
|
|
@@ -703,7 +688,7 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
703
688
|
required_stable_ids: requiredStableIds,
|
|
704
689
|
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
705
690
|
created_at: now,
|
|
706
|
-
expires_at: now +
|
|
691
|
+
expires_at: now + ttlMs
|
|
707
692
|
});
|
|
708
693
|
return token;
|
|
709
694
|
}
|
|
@@ -916,10 +901,10 @@ import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-sh
|
|
|
916
901
|
|
|
917
902
|
// src/services/review.ts
|
|
918
903
|
import { execFileSync } from "child_process";
|
|
919
|
-
import { existsSync as
|
|
904
|
+
import { existsSync as existsSync2 } from "fs";
|
|
920
905
|
import { readFile as readFile3, readdir, unlink } from "fs/promises";
|
|
921
906
|
import { homedir as homedir2 } from "os";
|
|
922
|
-
import { basename, join as
|
|
907
|
+
import { basename, join as join2, relative as relative2, resolve } from "path";
|
|
923
908
|
|
|
924
909
|
// src/services/knowledge-id-allocator.ts
|
|
925
910
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -1002,9 +987,9 @@ function isNodeError(err) {
|
|
|
1002
987
|
var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
|
|
1003
988
|
function pendingBaseAbs(layer, projectRoot) {
|
|
1004
989
|
if (layer === "personal") {
|
|
1005
|
-
return
|
|
990
|
+
return join2(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
|
|
1006
991
|
}
|
|
1007
|
-
return
|
|
992
|
+
return join2(projectRoot, PENDING_BASE_TEAM_REL);
|
|
1008
993
|
}
|
|
1009
994
|
var PLURAL_TYPES = [
|
|
1010
995
|
"decisions",
|
|
@@ -1013,13 +998,6 @@ var PLURAL_TYPES = [
|
|
|
1013
998
|
"models",
|
|
1014
999
|
"processes"
|
|
1015
1000
|
];
|
|
1016
|
-
var PLURAL_TO_SINGULAR = {
|
|
1017
|
-
decisions: "decision",
|
|
1018
|
-
pitfalls: "pitfall",
|
|
1019
|
-
guidelines: "guideline",
|
|
1020
|
-
models: "model",
|
|
1021
|
-
processes: "process"
|
|
1022
|
-
};
|
|
1023
1001
|
async function reviewKnowledge(projectRoot, input) {
|
|
1024
1002
|
switch (input.action) {
|
|
1025
1003
|
case "list":
|
|
@@ -1114,8 +1092,8 @@ async function listPending(projectRoot, filters) {
|
|
|
1114
1092
|
];
|
|
1115
1093
|
for (const source of sources) {
|
|
1116
1094
|
for (const type of typesToScan) {
|
|
1117
|
-
const dir =
|
|
1118
|
-
if (!
|
|
1095
|
+
const dir = join2(source.root, type);
|
|
1096
|
+
if (!existsSync2(dir)) {
|
|
1119
1097
|
continue;
|
|
1120
1098
|
}
|
|
1121
1099
|
let entries;
|
|
@@ -1126,7 +1104,7 @@ async function listPending(projectRoot, filters) {
|
|
|
1126
1104
|
}
|
|
1127
1105
|
for (const name of entries) {
|
|
1128
1106
|
if (!name.endsWith(".md")) continue;
|
|
1129
|
-
const absolutePath =
|
|
1107
|
+
const absolutePath = join2(dir, name);
|
|
1130
1108
|
let content;
|
|
1131
1109
|
try {
|
|
1132
1110
|
content = await readFile3(absolutePath, "utf8");
|
|
@@ -1182,7 +1160,7 @@ async function listPending(projectRoot, filters) {
|
|
|
1182
1160
|
}
|
|
1183
1161
|
async function approveAll(projectRoot, pendingPaths) {
|
|
1184
1162
|
const allocator = new KnowledgeIdAllocator(
|
|
1185
|
-
|
|
1163
|
+
join2(projectRoot, ".fabric", "agents.meta.json")
|
|
1186
1164
|
);
|
|
1187
1165
|
const approved = [];
|
|
1188
1166
|
for (const pendingPath of pendingPaths) {
|
|
@@ -1233,12 +1211,11 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
1233
1211
|
throw new Error(`pending file missing or invalid 'type' frontmatter: ${pendingPath}`);
|
|
1234
1212
|
}
|
|
1235
1213
|
const layer = fm.layer ?? "team";
|
|
1236
|
-
const
|
|
1237
|
-
const stableId = await allocator.allocate(layer, singularType);
|
|
1214
|
+
const stableId = await allocator.allocate(layer, pluralType);
|
|
1238
1215
|
allocatedId = stableId;
|
|
1239
1216
|
const newFilename = `${stableId}--${slug}.md`;
|
|
1240
|
-
const layerRoot = layer === "personal" ?
|
|
1241
|
-
targetAbs =
|
|
1217
|
+
const layerRoot = layer === "personal" ? join2(resolvePersonalRoot2(), ".fabric") : join2(projectRoot, ".fabric");
|
|
1218
|
+
targetAbs = join2(layerRoot, "knowledge", pluralType, newFilename);
|
|
1242
1219
|
await ensureParentDirectory(targetAbs);
|
|
1243
1220
|
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
1244
1221
|
await atomicWriteText(targetAbs, rewritten);
|
|
@@ -1250,12 +1227,12 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
1250
1227
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1251
1228
|
});
|
|
1252
1229
|
} catch {
|
|
1253
|
-
if (
|
|
1230
|
+
if (existsSync2(sourceAbs)) {
|
|
1254
1231
|
await unlink(sourceAbs);
|
|
1255
1232
|
}
|
|
1256
1233
|
}
|
|
1257
1234
|
} else {
|
|
1258
|
-
if (
|
|
1235
|
+
if (existsSync2(sourceAbs)) {
|
|
1259
1236
|
await unlink(sourceAbs);
|
|
1260
1237
|
}
|
|
1261
1238
|
}
|
|
@@ -1271,7 +1248,7 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
1271
1248
|
}
|
|
1272
1249
|
return { pending_path: pendingPath, stable_id: stableId };
|
|
1273
1250
|
} catch (err) {
|
|
1274
|
-
if (writtenTarget && targetAbs !== void 0 &&
|
|
1251
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync2(targetAbs)) {
|
|
1275
1252
|
try {
|
|
1276
1253
|
await unlink(targetAbs);
|
|
1277
1254
|
} catch {
|
|
@@ -1292,7 +1269,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
1292
1269
|
for (const pendingPath of pendingPaths) {
|
|
1293
1270
|
try {
|
|
1294
1271
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1295
|
-
if (
|
|
1272
|
+
if (existsSync2(sandboxed.abs)) {
|
|
1296
1273
|
const content = await readFile3(sandboxed.abs, "utf8");
|
|
1297
1274
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
1298
1275
|
if (merged !== content) {
|
|
@@ -1335,7 +1312,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
1335
1312
|
} catch {
|
|
1336
1313
|
return null;
|
|
1337
1314
|
}
|
|
1338
|
-
if (
|
|
1315
|
+
if (existsSync2(sandboxed.abs)) {
|
|
1339
1316
|
return {
|
|
1340
1317
|
absPath: sandboxed.abs,
|
|
1341
1318
|
isInProjectTree: sandboxed.isInProjectTree,
|
|
@@ -1375,12 +1352,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
1375
1352
|
const fromScope = fm.relevance_scope ?? "broad";
|
|
1376
1353
|
const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
|
|
1377
1354
|
const allocator = new KnowledgeIdAllocator(
|
|
1378
|
-
|
|
1355
|
+
join2(projectRoot, ".fabric", "agents.meta.json")
|
|
1379
1356
|
);
|
|
1380
|
-
const
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1383
|
-
const toAbs = join3(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
|
|
1357
|
+
const newStableId = await allocator.allocate(toLayer, pluralType);
|
|
1358
|
+
const toRoot = toLayer === "personal" ? join2(resolvePersonalRoot2(), ".fabric") : join2(projectRoot, ".fabric");
|
|
1359
|
+
const toAbs = join2(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
|
|
1384
1360
|
await ensureParentDirectory(toAbs);
|
|
1385
1361
|
await emitEventBestEffort2(projectRoot, {
|
|
1386
1362
|
event_type: "knowledge_promote_started",
|
|
@@ -1404,11 +1380,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
1404
1380
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1405
1381
|
});
|
|
1406
1382
|
} catch {
|
|
1407
|
-
if (
|
|
1383
|
+
if (existsSync2(target.absPath)) {
|
|
1408
1384
|
await unlink(target.absPath);
|
|
1409
1385
|
}
|
|
1410
1386
|
}
|
|
1411
|
-
} else if (
|
|
1387
|
+
} else if (existsSync2(target.absPath)) {
|
|
1412
1388
|
await unlink(target.absPath);
|
|
1413
1389
|
}
|
|
1414
1390
|
await emitEventBestEffort2(projectRoot, {
|
|
@@ -1447,14 +1423,14 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1447
1423
|
const sources = [
|
|
1448
1424
|
{ root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
|
|
1449
1425
|
{ root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
|
|
1450
|
-
{ root:
|
|
1451
|
-
{ root:
|
|
1426
|
+
{ root: join2(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
|
|
1427
|
+
{ root: join2(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
|
|
1452
1428
|
];
|
|
1453
1429
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
1454
1430
|
for (const source of sources) {
|
|
1455
1431
|
for (const type of typesToScan) {
|
|
1456
|
-
const dir =
|
|
1457
|
-
if (!
|
|
1432
|
+
const dir = join2(source.root, type);
|
|
1433
|
+
if (!existsSync2(dir)) continue;
|
|
1458
1434
|
let entries;
|
|
1459
1435
|
try {
|
|
1460
1436
|
entries = await readdir(dir);
|
|
@@ -1463,7 +1439,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1463
1439
|
}
|
|
1464
1440
|
for (const name of entries) {
|
|
1465
1441
|
if (!name.endsWith(".md")) continue;
|
|
1466
|
-
const absolutePath =
|
|
1442
|
+
const absolutePath = join2(dir, name);
|
|
1467
1443
|
let content;
|
|
1468
1444
|
try {
|
|
1469
1445
|
content = await readFile3(absolutePath, "utf8");
|
|
@@ -1505,15 +1481,14 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1505
1481
|
if (!matches) continue;
|
|
1506
1482
|
const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1507
1483
|
items.push({
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
...source.isPersonal ? { pending_path_absolute: absolutePath } : {},
|
|
1484
|
+
area: source.isPending ? "pending" : "canonical",
|
|
1485
|
+
path: reportedPath,
|
|
1486
|
+
...source.isPersonal ? { path_absolute: absolutePath } : {},
|
|
1512
1487
|
type,
|
|
1513
1488
|
layer,
|
|
1514
1489
|
maturity,
|
|
1515
|
-
// Only pending entries carry an origin tag (
|
|
1516
|
-
//
|
|
1490
|
+
// Only pending entries carry an origin tag (canonical hits live
|
|
1491
|
+
// outside the dual-pending-root convention).
|
|
1517
1492
|
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
1518
1493
|
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1519
1494
|
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
@@ -1521,10 +1496,11 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1521
1496
|
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
1522
1497
|
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
1523
1498
|
// v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
|
|
1524
|
-
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
|
|
1499
|
+
...filters?.include_body === true ? { body: bodyForSearch } : {},
|
|
1500
|
+
// Canonical hits always have an id; pending hits typically don't
|
|
1501
|
+
// yet — surface the frontmatter id when present so consumers can
|
|
1502
|
+
// dedupe across runs.
|
|
1503
|
+
...fm.id !== void 0 ? { stable_id: fm.id } : {}
|
|
1528
1504
|
});
|
|
1529
1505
|
}
|
|
1530
1506
|
}
|
|
@@ -1536,7 +1512,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
1536
1512
|
for (const pendingPath of pendingPaths) {
|
|
1537
1513
|
try {
|
|
1538
1514
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1539
|
-
if (
|
|
1515
|
+
if (existsSync2(sandboxed.abs)) {
|
|
1540
1516
|
const content = await readFile3(sandboxed.abs, "utf8");
|
|
1541
1517
|
const patch = {
|
|
1542
1518
|
status: "deferred",
|
|
@@ -1797,7 +1773,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
1797
1773
|
// src/services/knowledge-sections.ts
|
|
1798
1774
|
import { readFile as readFile4 } from "fs/promises";
|
|
1799
1775
|
import { homedir as homedir3 } from "os";
|
|
1800
|
-
import { join as
|
|
1776
|
+
import { join as join3 } from "path";
|
|
1801
1777
|
var PRIORITY_ORDER = {
|
|
1802
1778
|
high: 0,
|
|
1803
1779
|
medium: 1,
|
|
@@ -1957,9 +1933,9 @@ function outputLevelOrder(level) {
|
|
|
1957
1933
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
1958
1934
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
1959
1935
|
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
1960
|
-
return
|
|
1936
|
+
return join3(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
1961
1937
|
}
|
|
1962
|
-
return
|
|
1938
|
+
return join3(projectRoot, contentRef);
|
|
1963
1939
|
}
|
|
1964
1940
|
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
1965
1941
|
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
@@ -2028,15 +2004,15 @@ function formatError(error) {
|
|
|
2028
2004
|
}
|
|
2029
2005
|
function formatPreexistingRootMessage(projectRoot) {
|
|
2030
2006
|
const preexisting = [];
|
|
2031
|
-
if (
|
|
2032
|
-
if (
|
|
2007
|
+
if (existsSync3(join4(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
2008
|
+
if (existsSync3(join4(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
2033
2009
|
if (preexisting.length === 0) return null;
|
|
2034
2010
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
2035
2011
|
}
|
|
2036
2012
|
function createFabricServer(tracker) {
|
|
2037
2013
|
const server = new McpServer({
|
|
2038
2014
|
name: "fabric-knowledge-server",
|
|
2039
|
-
version: "2.0.0-rc.
|
|
2015
|
+
version: "2.0.0-rc.29"
|
|
2040
2016
|
});
|
|
2041
2017
|
registerPlanContext(server, tracker);
|
|
2042
2018
|
registerKnowledgeSections(server, tracker);
|
|
@@ -2051,9 +2027,9 @@ function createFabricServer(tracker) {
|
|
|
2051
2027
|
},
|
|
2052
2028
|
async (_uri) => {
|
|
2053
2029
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
2054
|
-
const path =
|
|
2030
|
+
const path = join4(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
2055
2031
|
let text = "";
|
|
2056
|
-
if (
|
|
2032
|
+
if (existsSync3(path)) {
|
|
2057
2033
|
text = await readFile5(path, "utf8");
|
|
2058
2034
|
}
|
|
2059
2035
|
return {
|
|
@@ -2144,9 +2120,9 @@ function createShutdownHandler(deps) {
|
|
|
2144
2120
|
};
|
|
2145
2121
|
}
|
|
2146
2122
|
async function startHttpServer(options) {
|
|
2147
|
-
const { createFabricHttpApp } = await import("./http-
|
|
2148
|
-
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
2149
|
-
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
2123
|
+
const { createFabricHttpApp } = await import("./http-TAI5X7U5.js");
|
|
2124
|
+
const { port, projectRoot, host = "127.0.0.1", authToken, allowLoopbackNoAuth } = options;
|
|
2125
|
+
const app = createFabricHttpApp({ projectRoot, host, authToken, allowLoopbackNoAuth });
|
|
2150
2126
|
return await new Promise((resolveServer, rejectServer) => {
|
|
2151
2127
|
const server = app.listen(port, host);
|
|
2152
2128
|
server.once("close", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-server",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"minimatch": "^10.0.1",
|
|
15
15
|
"zod": "^3.25.0",
|
|
16
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
16
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.29"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/express": "^5.0.6",
|