@remnic/core 9.3.518 → 9.3.520
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/access-schema.d.ts +34 -34
- package/dist/index.d.ts +18 -1
- package/dist/index.js +513 -175
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +42 -42
- package/dist/shared-context/manager.d.ts +2 -2
- package/package.json +1 -1
- package/src/binary-lifecycle/backend.ts +162 -14
- package/src/binary-lifecycle/manifest.ts +24 -12
- package/src/binary-lifecycle/pipeline.test.ts +565 -1
- package/src/binary-lifecycle/pipeline.ts +296 -54
- package/src/binary-lifecycle/types.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -967,7 +967,6 @@ var DEFAULT_MAX_BINARY_SIZE_BYTES = 50 * 1024 * 1024;
|
|
|
967
967
|
var DEFAULT_GRACE_PERIOD_DAYS = 7;
|
|
968
968
|
|
|
969
969
|
// src/binary-lifecycle/backend.ts
|
|
970
|
-
import fs from "fs";
|
|
971
970
|
import fsp from "fs/promises";
|
|
972
971
|
import path from "path";
|
|
973
972
|
var FilesystemBackend = class {
|
|
@@ -980,40 +979,172 @@ var FilesystemBackend = class {
|
|
|
980
979
|
this.basePath = path.resolve(basePath);
|
|
981
980
|
}
|
|
982
981
|
resolveRemotePath(remotePath) {
|
|
983
|
-
|
|
984
|
-
throw new Error(`FilesystemBackend remotePath must be relative: ${JSON.stringify(remotePath)}`);
|
|
985
|
-
}
|
|
986
|
-
const resolved = path.resolve(this.basePath, remotePath);
|
|
982
|
+
const resolved = path.isAbsolute(remotePath) ? path.resolve(remotePath) : path.resolve(this.basePath, remotePath);
|
|
987
983
|
const relative = path.relative(this.basePath, resolved);
|
|
988
984
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
989
985
|
throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
|
|
990
986
|
}
|
|
991
987
|
return resolved;
|
|
992
988
|
}
|
|
993
|
-
|
|
989
|
+
isInsideBase(candidate, realBase) {
|
|
990
|
+
const relative = path.relative(realBase, candidate);
|
|
991
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
992
|
+
}
|
|
993
|
+
async realBasePathIfExists() {
|
|
994
|
+
try {
|
|
995
|
+
const stat = await fsp.lstat(this.basePath);
|
|
996
|
+
if (stat.isSymbolicLink()) {
|
|
997
|
+
throw new Error(`FilesystemBackend basePath must not be a symlink: ${this.basePath}`);
|
|
998
|
+
}
|
|
999
|
+
if (!stat.isDirectory()) {
|
|
1000
|
+
throw new Error(`FilesystemBackend basePath must be a directory: ${this.basePath}`);
|
|
1001
|
+
}
|
|
1002
|
+
return await fsp.realpath(this.basePath);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
if (err.code === "ENOENT") {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async ensureBaseDirectory() {
|
|
1011
|
+
await fsp.mkdir(this.basePath, { recursive: true });
|
|
1012
|
+
const realBase = await this.realBasePathIfExists();
|
|
1013
|
+
if (realBase === null) {
|
|
1014
|
+
throw new Error(`FilesystemBackend failed to create basePath: ${this.basePath}`);
|
|
1015
|
+
}
|
|
1016
|
+
return realBase;
|
|
1017
|
+
}
|
|
1018
|
+
async ensureSafeParentDirectory(dest) {
|
|
1019
|
+
const realBase = await this.ensureBaseDirectory();
|
|
1020
|
+
const destDir = path.dirname(dest);
|
|
1021
|
+
const relativeDir = path.relative(this.basePath, destDir);
|
|
1022
|
+
const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
|
|
1023
|
+
let current = this.basePath;
|
|
1024
|
+
for (const segment of segments) {
|
|
1025
|
+
if (segment === "." || segment === "") continue;
|
|
1026
|
+
current = path.join(current, segment);
|
|
1027
|
+
try {
|
|
1028
|
+
const stat = await fsp.lstat(current);
|
|
1029
|
+
if (stat.isSymbolicLink()) {
|
|
1030
|
+
throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (!stat.isDirectory()) {
|
|
1033
|
+
throw new Error(`FilesystemBackend remotePath parent is not a directory: ${current}`);
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
if (err.code !== "ENOENT") {
|
|
1037
|
+
throw err;
|
|
1038
|
+
}
|
|
1039
|
+
await fsp.mkdir(current);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const realParent = await fsp.realpath(destDir);
|
|
1043
|
+
if (!this.isInsideBase(realParent, realBase)) {
|
|
1044
|
+
throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${dest}`);
|
|
1045
|
+
}
|
|
1046
|
+
return realBase;
|
|
1047
|
+
}
|
|
1048
|
+
async resolveExistingRemotePath(remotePath) {
|
|
994
1049
|
const dest = this.resolveRemotePath(remotePath);
|
|
1050
|
+
const realBase = await this.realBasePathIfExists();
|
|
1051
|
+
if (realBase === null) {
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
995
1054
|
const destDir = path.dirname(dest);
|
|
996
|
-
|
|
997
|
-
|
|
1055
|
+
const relativeDir = path.relative(this.basePath, destDir);
|
|
1056
|
+
const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
|
|
1057
|
+
let current = this.basePath;
|
|
1058
|
+
for (const segment of segments) {
|
|
1059
|
+
if (segment === "." || segment === "") continue;
|
|
1060
|
+
current = path.join(current, segment);
|
|
1061
|
+
let stat;
|
|
1062
|
+
try {
|
|
1063
|
+
stat = await fsp.lstat(current);
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
if (err.code === "ENOENT") {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
throw err;
|
|
1069
|
+
}
|
|
1070
|
+
if (stat.isSymbolicLink()) {
|
|
1071
|
+
throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (!stat.isDirectory()) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const realParent = await fsp.realpath(destDir).catch((err) => {
|
|
1078
|
+
if (err.code === "ENOENT") return null;
|
|
1079
|
+
throw err;
|
|
1080
|
+
});
|
|
1081
|
+
if (realParent === null) {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
if (!this.isInsideBase(realParent, realBase)) {
|
|
1085
|
+
throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${JSON.stringify(remotePath)}`);
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
const stat = await fsp.lstat(dest);
|
|
1089
|
+
if (stat.isSymbolicLink()) {
|
|
1090
|
+
throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (!stat.isFile()) {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
if (err.code === "ENOENT") {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
throw err;
|
|
1100
|
+
}
|
|
1101
|
+
const realDest = await fsp.realpath(dest);
|
|
1102
|
+
if (!this.isInsideBase(realDest, realBase)) {
|
|
1103
|
+
throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
|
|
1104
|
+
}
|
|
998
1105
|
return dest;
|
|
999
1106
|
}
|
|
1000
|
-
async
|
|
1107
|
+
async upload(localPath, remotePath) {
|
|
1108
|
+
if (path.isAbsolute(remotePath)) {
|
|
1109
|
+
throw new Error(`FilesystemBackend upload remotePath must be relative: ${JSON.stringify(remotePath)}`);
|
|
1110
|
+
}
|
|
1001
1111
|
const dest = this.resolveRemotePath(remotePath);
|
|
1112
|
+
const realBase = await this.ensureSafeParentDirectory(dest);
|
|
1002
1113
|
try {
|
|
1003
|
-
await fsp.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1114
|
+
const stat = await fsp.lstat(dest);
|
|
1115
|
+
if (stat.isSymbolicLink()) {
|
|
1116
|
+
throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
|
|
1117
|
+
}
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
if (err.code !== "ENOENT") {
|
|
1120
|
+
throw err;
|
|
1121
|
+
}
|
|
1007
1122
|
}
|
|
1123
|
+
await fsp.copyFile(localPath, dest);
|
|
1124
|
+
const realDest = await fsp.realpath(dest);
|
|
1125
|
+
if (!this.isInsideBase(realDest, realBase)) {
|
|
1126
|
+
throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
|
|
1127
|
+
}
|
|
1128
|
+
return remotePath;
|
|
1129
|
+
}
|
|
1130
|
+
async exists(remotePath) {
|
|
1131
|
+
const dest = await this.resolveExistingRemotePath(remotePath);
|
|
1132
|
+
return dest !== null;
|
|
1008
1133
|
}
|
|
1009
1134
|
async delete(remotePath) {
|
|
1010
|
-
const dest = this.
|
|
1135
|
+
const dest = await this.resolveExistingRemotePath(remotePath);
|
|
1136
|
+
if (dest === null) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1011
1139
|
try {
|
|
1012
1140
|
await fsp.unlink(dest);
|
|
1013
1141
|
} catch (err) {
|
|
1014
1142
|
if (err.code !== "ENOENT") throw err;
|
|
1015
1143
|
}
|
|
1016
1144
|
}
|
|
1145
|
+
getRedirectTarget(remotePath) {
|
|
1146
|
+
return this.resolveRemotePath(remotePath);
|
|
1147
|
+
}
|
|
1017
1148
|
};
|
|
1018
1149
|
var NoneBackend = class {
|
|
1019
1150
|
type = "none";
|
|
@@ -1125,20 +1256,29 @@ function manifestPath(memoryDir) {
|
|
|
1125
1256
|
}
|
|
1126
1257
|
async function readManifest(memoryDir) {
|
|
1127
1258
|
const filePath = manifestPath(memoryDir);
|
|
1259
|
+
let raw;
|
|
1128
1260
|
try {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
return emptyManifest();
|
|
1133
|
-
}
|
|
1134
|
-
const obj = parsed;
|
|
1135
|
-
if (obj.version !== 1 || !Array.isArray(obj.assets)) {
|
|
1261
|
+
raw = await fsp3.readFile(filePath, "utf-8");
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
if (err.code === "ENOENT") {
|
|
1136
1264
|
return emptyManifest();
|
|
1137
1265
|
}
|
|
1138
|
-
|
|
1139
|
-
} catch {
|
|
1140
|
-
return emptyManifest();
|
|
1266
|
+
throw new Error(`Failed to read binary lifecycle manifest at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
1267
|
}
|
|
1268
|
+
let parsed;
|
|
1269
|
+
try {
|
|
1270
|
+
parsed = JSON.parse(raw);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
throw new Error(`Invalid binary lifecycle manifest JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1273
|
+
}
|
|
1274
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1275
|
+
throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected object`);
|
|
1276
|
+
}
|
|
1277
|
+
const obj = parsed;
|
|
1278
|
+
if (obj.version !== 1 || !Array.isArray(obj.assets)) {
|
|
1279
|
+
throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected version 1 with assets array`);
|
|
1280
|
+
}
|
|
1281
|
+
return parsed;
|
|
1142
1282
|
}
|
|
1143
1283
|
async function writeManifest(memoryDir, manifest) {
|
|
1144
1284
|
const dir = manifestDir(memoryDir);
|
|
@@ -1186,11 +1326,33 @@ function guessMimeType(ext) {
|
|
|
1186
1326
|
function escapeRegex(s) {
|
|
1187
1327
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1188
1328
|
}
|
|
1329
|
+
function resolveManifestAssetPath(memoryDir, originalPath) {
|
|
1330
|
+
if (originalPath.length === 0 || originalPath.includes("\0") || originalPath.includes("\\") || path4.isAbsolute(originalPath) || path4.win32.isAbsolute(originalPath)) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
const memoryRoot = path4.resolve(memoryDir);
|
|
1334
|
+
const fullPath = path4.resolve(memoryRoot, originalPath);
|
|
1335
|
+
const relative = path4.relative(memoryRoot, fullPath);
|
|
1336
|
+
if (relative === "" || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
return fullPath;
|
|
1340
|
+
}
|
|
1189
1341
|
function validateBinaryLifecycleConfig(config) {
|
|
1190
1342
|
if (typeof config.gracePeriodDays !== "number" || !Number.isFinite(config.gracePeriodDays) || !Number.isInteger(config.gracePeriodDays) || config.gracePeriodDays < 0) {
|
|
1191
1343
|
throw new Error("binary lifecycle gracePeriodDays must be a finite non-negative integer");
|
|
1192
1344
|
}
|
|
1193
1345
|
}
|
|
1346
|
+
function remotePathForAsset(backend, relPath) {
|
|
1347
|
+
const normalized = relPath.split(path4.sep).join("/");
|
|
1348
|
+
if (backend.type === "filesystem") {
|
|
1349
|
+
return `.binary-lifecycle/mirrors/${normalized}`;
|
|
1350
|
+
}
|
|
1351
|
+
return normalized;
|
|
1352
|
+
}
|
|
1353
|
+
function markdownTargetForAsset(asset) {
|
|
1354
|
+
return asset.redirectPath ?? asset.mirroredPath;
|
|
1355
|
+
}
|
|
1194
1356
|
async function stageMirror(memoryDir, newPaths, backend, assets, log2, dryRun) {
|
|
1195
1357
|
let mirrored = 0;
|
|
1196
1358
|
const errors = [];
|
|
@@ -1201,14 +1363,16 @@ async function stageMirror(memoryDir, newPaths, backend, assets, log2, dryRun) {
|
|
|
1201
1363
|
const contentHash = await hashFile2(fullPath);
|
|
1202
1364
|
const ext = path4.extname(relPath);
|
|
1203
1365
|
const mimeType = guessMimeType(ext);
|
|
1204
|
-
const remotePath = relPath;
|
|
1366
|
+
const remotePath = remotePathForAsset(backend, relPath);
|
|
1205
1367
|
let backendLocation = remotePath;
|
|
1206
1368
|
if (!dryRun) {
|
|
1207
1369
|
backendLocation = await backend.upload(fullPath, remotePath);
|
|
1208
1370
|
}
|
|
1371
|
+
const redirectPath = backend.getRedirectTarget?.(backendLocation);
|
|
1209
1372
|
const record = {
|
|
1210
1373
|
originalPath: relPath,
|
|
1211
1374
|
mirroredPath: backendLocation,
|
|
1375
|
+
...redirectPath ? { redirectPath } : {},
|
|
1212
1376
|
contentHash,
|
|
1213
1377
|
sizeBytes: stat.size,
|
|
1214
1378
|
mimeType,
|
|
@@ -1232,62 +1396,193 @@ async function stageMirror(memoryDir, newPaths, backend, assets, log2, dryRun) {
|
|
|
1232
1396
|
}
|
|
1233
1397
|
return { mirrored, errors };
|
|
1234
1398
|
}
|
|
1235
|
-
async function stageRedirect(memoryDir, assets, log2, dryRun) {
|
|
1399
|
+
async function stageRedirect(memoryDir, assets, log2, dryRun, readMarkdownFile, writeMarkdownFile) {
|
|
1236
1400
|
let redirected = 0;
|
|
1237
1401
|
const errors = [];
|
|
1238
|
-
const candidates = assets.filter((a) => a.status === "mirrored");
|
|
1402
|
+
const candidates = assets.filter((a) => a.status === "mirrored" || a.status === "error");
|
|
1239
1403
|
if (candidates.length === 0) return { redirected, errors };
|
|
1240
1404
|
const mdFiles = await findMarkdownFiles(memoryDir);
|
|
1241
1405
|
for (const asset of candidates) {
|
|
1242
|
-
|
|
1243
|
-
|
|
1406
|
+
const assetAbsolute = resolveManifestAssetPath(memoryDir, asset.originalPath);
|
|
1407
|
+
if (assetAbsolute === null) {
|
|
1408
|
+
const msg = `redirect blocked for ${asset.originalPath}: manifest path is outside memoryDir`;
|
|
1409
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1410
|
+
errors.push(msg);
|
|
1411
|
+
if (!dryRun) {
|
|
1412
|
+
asset.status = "error";
|
|
1413
|
+
}
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const updates = [];
|
|
1417
|
+
let scanFailCount = 0;
|
|
1244
1418
|
for (const mdPath of mdFiles) {
|
|
1245
1419
|
try {
|
|
1246
|
-
const content = await
|
|
1247
|
-
const
|
|
1248
|
-
const assetAbsolute = path4.join(memoryDir, asset.originalPath);
|
|
1249
|
-
const relativeToMd = path4.relative(mdDir, assetAbsolute);
|
|
1250
|
-
const relativeForward = relativeToMd.split(path4.sep).join("/");
|
|
1251
|
-
const escaped = escapeRegex(relativeForward);
|
|
1252
|
-
const pattern = new RegExp(
|
|
1253
|
-
`(!?\\[[^\\]]*\\]\\()(\\.\\/)?(${escaped})(\\))`,
|
|
1254
|
-
"g"
|
|
1255
|
-
);
|
|
1420
|
+
const content = await readMarkdownFile(mdPath);
|
|
1421
|
+
const pattern = markdownReferencePattern(asset, assetAbsolute, mdPath);
|
|
1256
1422
|
if (!pattern.test(content)) continue;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
});
|
|
1263
|
-
await fsp4.writeFile(mdPath, updated, "utf-8");
|
|
1264
|
-
}
|
|
1423
|
+
pattern.lastIndex = 0;
|
|
1424
|
+
const updated = content.replace(pattern, (_match, open, _target, close) => {
|
|
1425
|
+
return `${open}${markdownTargetForAsset(asset)}${close}`;
|
|
1426
|
+
});
|
|
1427
|
+
updates.push({ mdPath, content: updated });
|
|
1265
1428
|
} catch (err) {
|
|
1266
|
-
|
|
1429
|
+
scanFailCount++;
|
|
1267
1430
|
const msg = `redirect scan failed for ${mdPath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1268
1431
|
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1269
1432
|
errors.push(msg);
|
|
1270
1433
|
}
|
|
1271
1434
|
}
|
|
1272
|
-
if (
|
|
1435
|
+
if (scanFailCount > 0) {
|
|
1273
1436
|
if (!dryRun) {
|
|
1274
|
-
asset.status = "
|
|
1275
|
-
asset.redirectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1437
|
+
asset.status = "error";
|
|
1276
1438
|
}
|
|
1439
|
+
log2.warn(
|
|
1440
|
+
`[binary-lifecycle] redirect blocked for ${asset.originalPath}: ${scanFailCount} markdown scan failure(s)${dryRun ? "" : " \u2014 status set to error"}`
|
|
1441
|
+
);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (updates.length === 0) {
|
|
1445
|
+
if (asset.status === "error") {
|
|
1446
|
+
const verifyResult2 = await countRemainingLocalReferences(
|
|
1447
|
+
memoryDir,
|
|
1448
|
+
asset,
|
|
1449
|
+
assetAbsolute,
|
|
1450
|
+
mdFiles,
|
|
1451
|
+
readMarkdownFile
|
|
1452
|
+
);
|
|
1453
|
+
if (verifyResult2.errors.length > 0 || verifyResult2.remaining > 0) {
|
|
1454
|
+
if (!dryRun) {
|
|
1455
|
+
asset.status = "error";
|
|
1456
|
+
}
|
|
1457
|
+
for (const msg of verifyResult2.errors) {
|
|
1458
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1459
|
+
errors.push(msg);
|
|
1460
|
+
}
|
|
1461
|
+
if (verifyResult2.remaining > 0) {
|
|
1462
|
+
const msg = `redirect verification failed for ${asset.originalPath}: ${verifyResult2.remaining} local reference(s) remain`;
|
|
1463
|
+
log2.warn(`[binary-lifecycle] ${msg}`);
|
|
1464
|
+
errors.push(msg);
|
|
1465
|
+
}
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (asset.redirectedAt === void 0) {
|
|
1469
|
+
if (!dryRun) {
|
|
1470
|
+
asset.status = "mirrored";
|
|
1471
|
+
}
|
|
1472
|
+
log2.info(`[binary-lifecycle] preserved mirrored asset without redirected marker: ${asset.originalPath}${dryRun ? " [dry-run]" : ""}`);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (!Number.isFinite(new Date(asset.mirroredAt).getTime())) {
|
|
1476
|
+
const msg = `redirect blocked for ${asset.originalPath}: manifest mirroredAt is invalid`;
|
|
1477
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1478
|
+
errors.push(msg);
|
|
1479
|
+
if (!dryRun) {
|
|
1480
|
+
asset.status = "error";
|
|
1481
|
+
}
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
if (!dryRun) {
|
|
1485
|
+
asset.status = "redirected";
|
|
1486
|
+
asset.redirectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1487
|
+
}
|
|
1488
|
+
redirected++;
|
|
1489
|
+
log2.info(`[binary-lifecycle] redirected: ${asset.originalPath}${dryRun ? " [dry-run]" : ""}`);
|
|
1490
|
+
}
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (dryRun) {
|
|
1277
1494
|
redirected++;
|
|
1278
|
-
log2.info(`[binary-lifecycle] redirected: ${asset.originalPath}
|
|
1279
|
-
|
|
1495
|
+
log2.info(`[binary-lifecycle] redirected: ${asset.originalPath} [dry-run]`);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
let writeFailCount = 0;
|
|
1499
|
+
for (const update of updates) {
|
|
1500
|
+
try {
|
|
1501
|
+
await writeMarkdownFile(update.mdPath, update.content);
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
writeFailCount++;
|
|
1504
|
+
const msg = `redirect write failed for ${update.mdPath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1505
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1506
|
+
errors.push(msg);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (writeFailCount > 0) {
|
|
1280
1510
|
if (!dryRun) {
|
|
1281
1511
|
asset.status = "error";
|
|
1282
1512
|
}
|
|
1283
1513
|
log2.warn(
|
|
1284
|
-
`[binary-lifecycle] redirect
|
|
1514
|
+
`[binary-lifecycle] redirect write failure for ${asset.originalPath}: ${writeFailCount} write failure(s) \u2014 status set to error`
|
|
1285
1515
|
);
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
const redirectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1519
|
+
asset.redirectedAt = redirectedAt;
|
|
1520
|
+
const verifyResult = await countRemainingLocalReferences(
|
|
1521
|
+
memoryDir,
|
|
1522
|
+
asset,
|
|
1523
|
+
assetAbsolute,
|
|
1524
|
+
mdFiles,
|
|
1525
|
+
readMarkdownFile
|
|
1526
|
+
);
|
|
1527
|
+
if (verifyResult.errors.length > 0 || verifyResult.remaining > 0) {
|
|
1528
|
+
asset.status = "error";
|
|
1529
|
+
for (const msg of verifyResult.errors) {
|
|
1530
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1531
|
+
errors.push(msg);
|
|
1532
|
+
}
|
|
1533
|
+
if (verifyResult.remaining > 0) {
|
|
1534
|
+
const msg = `redirect verification failed for ${asset.originalPath}: ${verifyResult.remaining} local reference(s) remain`;
|
|
1535
|
+
log2.warn(`[binary-lifecycle] ${msg}`);
|
|
1536
|
+
errors.push(msg);
|
|
1537
|
+
}
|
|
1538
|
+
continue;
|
|
1286
1539
|
}
|
|
1540
|
+
asset.status = "redirected";
|
|
1541
|
+
asset.redirectedAt = redirectedAt;
|
|
1542
|
+
redirected++;
|
|
1543
|
+
log2.info(`[binary-lifecycle] redirected: ${asset.originalPath}`);
|
|
1287
1544
|
}
|
|
1288
1545
|
return { redirected, errors };
|
|
1289
1546
|
}
|
|
1290
|
-
async function
|
|
1547
|
+
async function countRemainingLocalReferences(memoryDir, asset, assetAbsolute, mdFiles, readMarkdownFile) {
|
|
1548
|
+
let remaining = 0;
|
|
1549
|
+
const errors = [];
|
|
1550
|
+
for (const mdPath of mdFiles) {
|
|
1551
|
+
try {
|
|
1552
|
+
const content = await readMarkdownFile(mdPath);
|
|
1553
|
+
const pattern = markdownReferencePattern(asset, assetAbsolute, mdPath);
|
|
1554
|
+
if (pattern.test(content)) {
|
|
1555
|
+
remaining++;
|
|
1556
|
+
}
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
errors.push(`redirect verification failed for ${mdPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return { remaining, errors };
|
|
1562
|
+
}
|
|
1563
|
+
function markdownReferencePattern(asset, assetAbsolute, mdPath) {
|
|
1564
|
+
const mdDir = path4.dirname(mdPath);
|
|
1565
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1566
|
+
const addCandidate = (candidate) => {
|
|
1567
|
+
const normalized = candidate.split(path4.sep).join("/");
|
|
1568
|
+
if (normalized.length === 0) return;
|
|
1569
|
+
candidates.add(normalized);
|
|
1570
|
+
const isParentTraversal = normalized === ".." || normalized.startsWith("../");
|
|
1571
|
+
if (!normalized.startsWith("./") && !normalized.startsWith("/") && !isParentTraversal) {
|
|
1572
|
+
candidates.add(`./${normalized}`);
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
addCandidate(path4.relative(mdDir, assetAbsolute));
|
|
1576
|
+
const originalPath = asset.originalPath.split(path4.sep).join("/");
|
|
1577
|
+
const originalAsFileRelative = path4.resolve(mdDir, ...originalPath.split("/"));
|
|
1578
|
+
if (path4.resolve(originalAsFileRelative) === path4.resolve(assetAbsolute)) {
|
|
1579
|
+
addCandidate(originalPath);
|
|
1580
|
+
}
|
|
1581
|
+
addCandidate(`/${originalPath}`);
|
|
1582
|
+
const alternatives = [...candidates].sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
1583
|
+
return new RegExp(`(!?\\[[^\\]]*\\]\\()(${alternatives})(\\))`, "g");
|
|
1584
|
+
}
|
|
1585
|
+
async function stageClean(memoryDir, assets, backend, gracePeriodDays, log2, dryRun, forceClean) {
|
|
1291
1586
|
let cleaned = 0;
|
|
1292
1587
|
const errors = [];
|
|
1293
1588
|
const now = Date.now();
|
|
@@ -1297,11 +1592,44 @@ async function stageClean(memoryDir, assets, gracePeriodDays, log2, dryRun, forc
|
|
|
1297
1592
|
);
|
|
1298
1593
|
for (const asset of candidates) {
|
|
1299
1594
|
const mirroredMs = new Date(asset.mirroredAt).getTime();
|
|
1595
|
+
if (!Number.isFinite(mirroredMs)) {
|
|
1596
|
+
const msg = `clean blocked for ${asset.originalPath}: manifest mirroredAt is invalid`;
|
|
1597
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1598
|
+
errors.push(msg);
|
|
1599
|
+
if (!dryRun) {
|
|
1600
|
+
asset.status = "error";
|
|
1601
|
+
}
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1300
1604
|
const ageMs = now - mirroredMs;
|
|
1301
1605
|
if (!forceClean && ageMs < graceMs) {
|
|
1302
1606
|
continue;
|
|
1303
1607
|
}
|
|
1304
|
-
const fullPath =
|
|
1608
|
+
const fullPath = resolveManifestAssetPath(memoryDir, asset.originalPath);
|
|
1609
|
+
if (fullPath === null) {
|
|
1610
|
+
const msg = `clean blocked for ${asset.originalPath}: manifest path is outside memoryDir`;
|
|
1611
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1612
|
+
errors.push(msg);
|
|
1613
|
+
if (!dryRun) {
|
|
1614
|
+
asset.status = "error";
|
|
1615
|
+
}
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
let remoteExists;
|
|
1619
|
+
try {
|
|
1620
|
+
remoteExists = await backend.exists(asset.mirroredPath);
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
const msg = `clean blocked for ${asset.originalPath}: failed to verify mirrored copy: ${err instanceof Error ? err.message : String(err)}`;
|
|
1623
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1624
|
+
errors.push(msg);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (!remoteExists) {
|
|
1628
|
+
const msg = `clean blocked for ${asset.originalPath}: mirrored copy is missing`;
|
|
1629
|
+
log2.error(`[binary-lifecycle] ${msg}`);
|
|
1630
|
+
errors.push(msg);
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1305
1633
|
try {
|
|
1306
1634
|
const currentHash = await hashFile2(fullPath);
|
|
1307
1635
|
if (currentHash !== asset.contentHash) {
|
|
@@ -1319,9 +1647,11 @@ async function stageClean(memoryDir, assets, gracePeriodDays, log2, dryRun, forc
|
|
|
1319
1647
|
log2.info(`[binary-lifecycle] cleaned: ${asset.originalPath}${dryRun ? " [dry-run]" : ""}`);
|
|
1320
1648
|
} catch (err) {
|
|
1321
1649
|
if (err.code === "ENOENT") {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1650
|
+
if (!dryRun) {
|
|
1651
|
+
asset.status = "cleaned";
|
|
1652
|
+
asset.cleanedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1653
|
+
cleaned++;
|
|
1654
|
+
}
|
|
1325
1655
|
} else {
|
|
1326
1656
|
const msg = `clean failed for ${asset.originalPath}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1327
1657
|
log2.error(`[binary-lifecycle] ${msg}`);
|
|
@@ -1378,10 +1708,18 @@ async function runBinaryLifecyclePipeline(memoryDir, config, backend, log2, opts
|
|
|
1378
1708
|
log2,
|
|
1379
1709
|
dryRun
|
|
1380
1710
|
);
|
|
1381
|
-
const redirectResult = await stageRedirect(
|
|
1711
|
+
const redirectResult = await stageRedirect(
|
|
1712
|
+
memoryDir,
|
|
1713
|
+
manifest.assets,
|
|
1714
|
+
log2,
|
|
1715
|
+
dryRun,
|
|
1716
|
+
opts?.readMarkdownFile ?? ((filePath) => fsp4.readFile(filePath, "utf-8")),
|
|
1717
|
+
opts?.writeMarkdownFile ?? ((filePath, content) => fsp4.writeFile(filePath, content, "utf-8"))
|
|
1718
|
+
);
|
|
1382
1719
|
const cleanResult = await stageClean(
|
|
1383
1720
|
memoryDir,
|
|
1384
1721
|
manifest.assets,
|
|
1722
|
+
backend,
|
|
1385
1723
|
config.gracePeriodDays,
|
|
1386
1724
|
log2,
|
|
1387
1725
|
dryRun,
|
|
@@ -1407,7 +1745,7 @@ async function runBinaryLifecyclePipeline(memoryDir, config, backend, log2, opts
|
|
|
1407
1745
|
}
|
|
1408
1746
|
|
|
1409
1747
|
// src/projection/index.ts
|
|
1410
|
-
import
|
|
1748
|
+
import fs from "fs";
|
|
1411
1749
|
import path5 from "path";
|
|
1412
1750
|
var VALID_PROJECTION_CATEGORIES = new Set(ALL_CATEGORY_KEYS);
|
|
1413
1751
|
async function generateContextTree(options) {
|
|
@@ -1426,14 +1764,14 @@ async function generateContextTree(options) {
|
|
|
1426
1764
|
const resolvedMemoryDir = path5.resolve(memoryDir);
|
|
1427
1765
|
const resolvedOutputDir = path5.resolve(outputDir);
|
|
1428
1766
|
const requestedCategories = validateProjectionCategories(filterCategories);
|
|
1429
|
-
const realMemoryDir =
|
|
1767
|
+
const realMemoryDir = fs.realpathSync(resolvedMemoryDir);
|
|
1430
1768
|
assertNotSymlink(resolvedOutputDir, "context tree outputDir");
|
|
1431
|
-
|
|
1432
|
-
const realOutputDir =
|
|
1769
|
+
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
1770
|
+
const realOutputDir = fs.realpathSync(resolvedOutputDir);
|
|
1433
1771
|
const allCategories = (requestedCategories ?? ALL_CATEGORY_KEYS).filter((c) => c !== "question");
|
|
1434
1772
|
for (const category of allCategories) {
|
|
1435
1773
|
const categoryDir = getCategoryDir(memoryDir, category);
|
|
1436
|
-
if (!
|
|
1774
|
+
if (!fs.existsSync(categoryDir)) continue;
|
|
1437
1775
|
assertSafeInputRoot(realMemoryDir, categoryDir, `${category} memory category`);
|
|
1438
1776
|
categoryCounts[category] = 0;
|
|
1439
1777
|
const files = walkR(categoryDir, realMemoryDir);
|
|
@@ -1443,7 +1781,7 @@ async function generateContextTree(options) {
|
|
|
1443
1781
|
nodesSkipped++;
|
|
1444
1782
|
continue;
|
|
1445
1783
|
}
|
|
1446
|
-
const content =
|
|
1784
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1447
1785
|
const fm = parseFrontmatter(content);
|
|
1448
1786
|
if (!fm) {
|
|
1449
1787
|
nodesSkipped++;
|
|
@@ -1463,7 +1801,7 @@ async function generateContextTree(options) {
|
|
|
1463
1801
|
}
|
|
1464
1802
|
if (includeEntities) {
|
|
1465
1803
|
const entitiesDir = path5.join(memoryDir, "entities");
|
|
1466
|
-
if (
|
|
1804
|
+
if (fs.existsSync(entitiesDir)) {
|
|
1467
1805
|
assertSafeInputRoot(realMemoryDir, entitiesDir, "entities root");
|
|
1468
1806
|
categoryCounts["entity"] = 0;
|
|
1469
1807
|
const entityFiles = walkR(entitiesDir, realMemoryDir);
|
|
@@ -1473,7 +1811,7 @@ async function generateContextTree(options) {
|
|
|
1473
1811
|
nodesSkipped++;
|
|
1474
1812
|
continue;
|
|
1475
1813
|
}
|
|
1476
|
-
const content =
|
|
1814
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1477
1815
|
const fileName = path5.basename(filePath, ".md");
|
|
1478
1816
|
const node = projectEntityNode(fileName, content);
|
|
1479
1817
|
const outputPath = resolveContainedOutputPath(realOutputDir, "entities", `${fileName}.md`);
|
|
@@ -1487,7 +1825,7 @@ async function generateContextTree(options) {
|
|
|
1487
1825
|
const shouldIncludeQuestions = includeQuestions && (requestedCategories === void 0 || requestedCategories.includes("question"));
|
|
1488
1826
|
if (shouldIncludeQuestions) {
|
|
1489
1827
|
const questionsDir = path5.join(memoryDir, "questions");
|
|
1490
|
-
if (
|
|
1828
|
+
if (fs.existsSync(questionsDir)) {
|
|
1491
1829
|
assertSafeInputRoot(realMemoryDir, questionsDir, "questions root");
|
|
1492
1830
|
categoryCounts["question"] = 0;
|
|
1493
1831
|
const qFiles = walkR(questionsDir, realMemoryDir);
|
|
@@ -1497,7 +1835,7 @@ async function generateContextTree(options) {
|
|
|
1497
1835
|
nodesSkipped++;
|
|
1498
1836
|
continue;
|
|
1499
1837
|
}
|
|
1500
|
-
const content =
|
|
1838
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1501
1839
|
const fm = parseFrontmatter(content);
|
|
1502
1840
|
if (!fm) {
|
|
1503
1841
|
nodesSkipped++;
|
|
@@ -1532,7 +1870,7 @@ function isPathInside(root, candidate) {
|
|
|
1532
1870
|
}
|
|
1533
1871
|
function assertNotSymlink(targetPath, label) {
|
|
1534
1872
|
try {
|
|
1535
|
-
if (
|
|
1873
|
+
if (fs.lstatSync(targetPath).isSymbolicLink()) {
|
|
1536
1874
|
throw new Error(`${label} must not be a symlink: ${targetPath}`);
|
|
1537
1875
|
}
|
|
1538
1876
|
} catch (err) {
|
|
@@ -1541,14 +1879,14 @@ function assertNotSymlink(targetPath, label) {
|
|
|
1541
1879
|
}
|
|
1542
1880
|
}
|
|
1543
1881
|
function assertSafeInputRoot(realMemoryDir, targetPath, label) {
|
|
1544
|
-
const stat =
|
|
1882
|
+
const stat = fs.lstatSync(targetPath);
|
|
1545
1883
|
if (stat.isSymbolicLink()) {
|
|
1546
1884
|
throw new Error(`${label} must not be a symlink: ${targetPath}`);
|
|
1547
1885
|
}
|
|
1548
1886
|
if (!stat.isDirectory()) {
|
|
1549
1887
|
throw new Error(`${label} must be a directory: ${targetPath}`);
|
|
1550
1888
|
}
|
|
1551
|
-
const realTarget =
|
|
1889
|
+
const realTarget = fs.realpathSync(targetPath);
|
|
1552
1890
|
if (!isPathInside(realMemoryDir, realTarget)) {
|
|
1553
1891
|
throw new Error(`${label} escapes memoryDir: ${targetPath}`);
|
|
1554
1892
|
}
|
|
@@ -1562,7 +1900,7 @@ function assertSafeOutputTarget(realOutputDir, outputPath) {
|
|
|
1562
1900
|
for (const segment of relative.split(path5.sep).filter(Boolean)) {
|
|
1563
1901
|
current = path5.join(current, segment);
|
|
1564
1902
|
try {
|
|
1565
|
-
const stat =
|
|
1903
|
+
const stat = fs.lstatSync(current);
|
|
1566
1904
|
if (stat.isSymbolicLink()) {
|
|
1567
1905
|
throw new Error(`context tree output path contains symlink: ${current}`);
|
|
1568
1906
|
}
|
|
@@ -1599,13 +1937,13 @@ function resolveContainedOutputPath(outputRoot, ...segments) {
|
|
|
1599
1937
|
}
|
|
1600
1938
|
function writeProjectedContent(realOutputDir, outputPath, generatedContent) {
|
|
1601
1939
|
assertSafeOutputTarget(realOutputDir, outputPath);
|
|
1602
|
-
|
|
1603
|
-
const realParent =
|
|
1940
|
+
fs.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
1941
|
+
const realParent = fs.realpathSync(path5.dirname(outputPath));
|
|
1604
1942
|
if (!isPathInside(realOutputDir, realParent)) {
|
|
1605
1943
|
throw new Error(`context tree output path escapes outputDir: ${outputPath}`);
|
|
1606
1944
|
}
|
|
1607
|
-
const existingContent =
|
|
1608
|
-
|
|
1945
|
+
const existingContent = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf8") : "";
|
|
1946
|
+
fs.writeFileSync(
|
|
1609
1947
|
outputPath,
|
|
1610
1948
|
preserveManualFencedBlocks(generatedContent, existingContent)
|
|
1611
1949
|
);
|
|
@@ -1638,19 +1976,19 @@ function extractManualFencedBlocks(content) {
|
|
|
1638
1976
|
function walkR(dir, realMemoryDir) {
|
|
1639
1977
|
const results = [];
|
|
1640
1978
|
function walk(directory) {
|
|
1641
|
-
for (const entry of
|
|
1979
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
1642
1980
|
const fullPath = path5.join(directory, entry.name);
|
|
1643
1981
|
if (entry.isSymbolicLink()) {
|
|
1644
1982
|
throw new Error(`context tree input path contains symlink: ${fullPath}`);
|
|
1645
1983
|
}
|
|
1646
1984
|
if (entry.isDirectory()) {
|
|
1647
|
-
const realDir =
|
|
1985
|
+
const realDir = fs.realpathSync(fullPath);
|
|
1648
1986
|
if (!isPathInside(realMemoryDir, realDir)) {
|
|
1649
1987
|
throw new Error(`context tree input path escapes memoryDir: ${fullPath}`);
|
|
1650
1988
|
}
|
|
1651
1989
|
walk(fullPath);
|
|
1652
1990
|
} else if (entry.name.endsWith(".md")) {
|
|
1653
|
-
const realFile =
|
|
1991
|
+
const realFile = fs.realpathSync(fullPath);
|
|
1654
1992
|
if (!isPathInside(realMemoryDir, realFile)) {
|
|
1655
1993
|
throw new Error(`context tree input path escapes memoryDir: ${fullPath}`);
|
|
1656
1994
|
}
|
|
@@ -1796,7 +2134,7 @@ function generateIndex(categoryCounts, outputDir) {
|
|
|
1796
2134
|
}
|
|
1797
2135
|
|
|
1798
2136
|
// src/onboarding/index.ts
|
|
1799
|
-
import
|
|
2137
|
+
import fs2 from "fs";
|
|
1800
2138
|
import path6 from "path";
|
|
1801
2139
|
var LANGUAGE_RULES = [
|
|
1802
2140
|
{
|
|
@@ -1902,7 +2240,7 @@ function onboard(options) {
|
|
|
1902
2240
|
const directory = path6.resolve(options.directory ?? process.cwd());
|
|
1903
2241
|
let rootStat;
|
|
1904
2242
|
try {
|
|
1905
|
-
rootStat =
|
|
2243
|
+
rootStat = fs2.statSync(directory);
|
|
1906
2244
|
} catch (err) {
|
|
1907
2245
|
throw new Error(`Cannot scan onboarding directory ${directory}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1908
2246
|
}
|
|
@@ -1932,7 +2270,7 @@ function walkDir(root, exclude, maxDepth) {
|
|
|
1932
2270
|
if (depth > maxDepth) return;
|
|
1933
2271
|
let entries;
|
|
1934
2272
|
try {
|
|
1935
|
-
entries =
|
|
2273
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1936
2274
|
} catch (err) {
|
|
1937
2275
|
if (depth === 0) {
|
|
1938
2276
|
throw new Error(`Cannot scan onboarding directory ${root}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2008,7 +2346,7 @@ function detectShape(files, root) {
|
|
|
2008
2346
|
);
|
|
2009
2347
|
const rootDirs = /* @__PURE__ */ new Set();
|
|
2010
2348
|
try {
|
|
2011
|
-
for (const entry of
|
|
2349
|
+
for (const entry of fs2.readdirSync(root, { withFileTypes: true })) {
|
|
2012
2350
|
if (entry.isDirectory()) rootDirs.add(entry.name);
|
|
2013
2351
|
}
|
|
2014
2352
|
} catch {
|
|
@@ -2089,7 +2427,7 @@ function discoverDocs(files, root) {
|
|
|
2089
2427
|
if (kind) {
|
|
2090
2428
|
let size = 0;
|
|
2091
2429
|
try {
|
|
2092
|
-
size =
|
|
2430
|
+
size = fs2.statSync(filePath).size;
|
|
2093
2431
|
} catch {
|
|
2094
2432
|
}
|
|
2095
2433
|
docs.push({
|
|
@@ -2131,14 +2469,14 @@ function buildPlan(languages, shape, docs, _root) {
|
|
|
2131
2469
|
}
|
|
2132
2470
|
function readJsonSafe(filePath) {
|
|
2133
2471
|
try {
|
|
2134
|
-
return JSON.parse(
|
|
2472
|
+
return JSON.parse(fs2.readFileSync(filePath, "utf8"));
|
|
2135
2473
|
} catch {
|
|
2136
2474
|
return null;
|
|
2137
2475
|
}
|
|
2138
2476
|
}
|
|
2139
2477
|
function readTomlWorkspace(filePath) {
|
|
2140
2478
|
try {
|
|
2141
|
-
const content =
|
|
2479
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
2142
2480
|
return content.includes("[workspace]");
|
|
2143
2481
|
} catch {
|
|
2144
2482
|
return false;
|
|
@@ -2146,7 +2484,7 @@ function readTomlWorkspace(filePath) {
|
|
|
2146
2484
|
}
|
|
2147
2485
|
|
|
2148
2486
|
// src/curation/index.ts
|
|
2149
|
-
import
|
|
2487
|
+
import fs3 from "fs";
|
|
2150
2488
|
import path7 from "path";
|
|
2151
2489
|
import crypto4 from "crypto";
|
|
2152
2490
|
async function curate(options) {
|
|
@@ -2232,12 +2570,12 @@ async function curate(options) {
|
|
|
2232
2570
|
};
|
|
2233
2571
|
}
|
|
2234
2572
|
function resolveTargets(targetPath) {
|
|
2235
|
-
const stat =
|
|
2573
|
+
const stat = fs3.statSync(targetPath);
|
|
2236
2574
|
if (stat.isFile()) return [targetPath];
|
|
2237
2575
|
const results = [];
|
|
2238
2576
|
const extensions = /* @__PURE__ */ new Set([".md", ".txt", ".mdx", ".rst"]);
|
|
2239
2577
|
function walk(dir) {
|
|
2240
|
-
for (const entry of
|
|
2578
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
2241
2579
|
const fullPath = path7.join(dir, entry.name);
|
|
2242
2580
|
if (entry.isDirectory()) {
|
|
2243
2581
|
if (entry.name !== "node_modules" && entry.name !== ".git") {
|
|
@@ -2253,7 +2591,7 @@ function resolveTargets(targetPath) {
|
|
|
2253
2591
|
}
|
|
2254
2592
|
function resolveProvenanceRoot(targetPath) {
|
|
2255
2593
|
const resolvedTarget = path7.resolve(targetPath);
|
|
2256
|
-
const stat =
|
|
2594
|
+
const stat = fs3.statSync(resolvedTarget);
|
|
2257
2595
|
return stat.isFile() ? path7.dirname(resolvedTarget) : resolvedTarget;
|
|
2258
2596
|
}
|
|
2259
2597
|
function extractStatements(content, filePath, projectRoot, source, sourceFileHash, categoryOverride, confidence, entityRef, tags) {
|
|
@@ -2348,11 +2686,11 @@ function findContradiction(stmt, existing) {
|
|
|
2348
2686
|
}
|
|
2349
2687
|
function loadExistingMemories(memoryDir) {
|
|
2350
2688
|
const result = /* @__PURE__ */ new Map();
|
|
2351
|
-
if (!
|
|
2689
|
+
if (!fs3.existsSync(memoryDir)) return result;
|
|
2352
2690
|
const dirs = ALL_CATEGORY_DIRS;
|
|
2353
2691
|
for (const dir of dirs) {
|
|
2354
2692
|
const fullDir = path7.join(memoryDir, dir);
|
|
2355
|
-
if (!
|
|
2693
|
+
if (!fs3.existsSync(fullDir)) continue;
|
|
2356
2694
|
walkFiles(fullDir, (filePath) => {
|
|
2357
2695
|
const content = readFileSafe(filePath);
|
|
2358
2696
|
if (!content) return;
|
|
@@ -2374,7 +2712,7 @@ function writeStatement(stmt, memoryDir) {
|
|
|
2374
2712
|
const dateDir = now.toISOString().split("T")[0];
|
|
2375
2713
|
const categoryDir = getCategoryDir(memoryDir, stmt.category);
|
|
2376
2714
|
const dir = path7.join(categoryDir, dateDir);
|
|
2377
|
-
|
|
2715
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2378
2716
|
const fileName = `${stmt.category}-${Date.now()}-${stmt.id.slice(0, 8)}.md`;
|
|
2379
2717
|
const filePath = path7.join(dir, fileName);
|
|
2380
2718
|
const frontmatter = [
|
|
@@ -2396,7 +2734,7 @@ function writeStatement(stmt, memoryDir) {
|
|
|
2396
2734
|
|
|
2397
2735
|
${stmt.content}
|
|
2398
2736
|
`;
|
|
2399
|
-
|
|
2737
|
+
fs3.writeFileSync(filePath, body);
|
|
2400
2738
|
return filePath;
|
|
2401
2739
|
}
|
|
2402
2740
|
function generateId() {
|
|
@@ -2413,7 +2751,7 @@ function tierFromConfidence(confidence) {
|
|
|
2413
2751
|
}
|
|
2414
2752
|
function readFileSafe(filePath) {
|
|
2415
2753
|
try {
|
|
2416
|
-
return
|
|
2754
|
+
return fs3.readFileSync(filePath, "utf8");
|
|
2417
2755
|
} catch {
|
|
2418
2756
|
return null;
|
|
2419
2757
|
}
|
|
@@ -2442,7 +2780,7 @@ function extractBody2(content) {
|
|
|
2442
2780
|
return match ? match[1].trim() : content.trim();
|
|
2443
2781
|
}
|
|
2444
2782
|
function walkFiles(dir, callback) {
|
|
2445
|
-
for (const entry of
|
|
2783
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
2446
2784
|
const fullPath = path7.join(dir, entry.name);
|
|
2447
2785
|
if (entry.isDirectory()) {
|
|
2448
2786
|
walkFiles(fullPath, callback);
|
|
@@ -2453,7 +2791,7 @@ function walkFiles(dir, callback) {
|
|
|
2453
2791
|
}
|
|
2454
2792
|
|
|
2455
2793
|
// src/dedup/index.ts
|
|
2456
|
-
import
|
|
2794
|
+
import fs4 from "fs";
|
|
2457
2795
|
import path8 from "path";
|
|
2458
2796
|
import crypto5 from "crypto";
|
|
2459
2797
|
var DEFAULT_DEDUP_THRESHOLD = 0.85;
|
|
@@ -2592,18 +2930,18 @@ function stripNegation(text) {
|
|
|
2592
2930
|
function loadMemories(memoryDir, categories, maxLoad = 1e4) {
|
|
2593
2931
|
const result = [];
|
|
2594
2932
|
const allCategories = categories ?? ALL_CATEGORY_DIRS;
|
|
2595
|
-
if (!
|
|
2596
|
-
const memoryRootReal =
|
|
2933
|
+
if (!fs4.existsSync(memoryDir)) return result;
|
|
2934
|
+
const memoryRootReal = fs4.realpathSync(memoryDir);
|
|
2597
2935
|
for (const category of allCategories) {
|
|
2598
2936
|
if (result.length >= maxLoad) break;
|
|
2599
2937
|
const dir = path8.join(memoryDir, category);
|
|
2600
|
-
if (!
|
|
2601
|
-
const categoryStat =
|
|
2938
|
+
if (!fs4.existsSync(dir)) continue;
|
|
2939
|
+
const categoryStat = fs4.lstatSync(dir);
|
|
2602
2940
|
if (categoryStat.isSymbolicLink()) {
|
|
2603
2941
|
throw new Error(`Refusing to scan symlinked memory category directory: ${dir}`);
|
|
2604
2942
|
}
|
|
2605
2943
|
if (!categoryStat.isDirectory()) continue;
|
|
2606
|
-
const categoryRootReal =
|
|
2944
|
+
const categoryRootReal = fs4.realpathSync(dir);
|
|
2607
2945
|
assertPathInsideRoot(memoryRootReal, categoryRootReal, dir);
|
|
2608
2946
|
walkMdFiles(dir, memoryRootReal, categoryRootReal, (filePath) => {
|
|
2609
2947
|
if (result.length >= maxLoad) return;
|
|
@@ -2627,14 +2965,14 @@ function hashContent2(content) {
|
|
|
2627
2965
|
}
|
|
2628
2966
|
function readFileSafe2(filePath, memoryRootReal, categoryRootReal) {
|
|
2629
2967
|
try {
|
|
2630
|
-
const fileStat =
|
|
2968
|
+
const fileStat = fs4.lstatSync(filePath);
|
|
2631
2969
|
if (fileStat.isSymbolicLink()) {
|
|
2632
2970
|
throw new Error(`Refusing to read symlinked memory file: ${filePath}`);
|
|
2633
2971
|
}
|
|
2634
|
-
const fileReal =
|
|
2972
|
+
const fileReal = fs4.realpathSync(filePath);
|
|
2635
2973
|
assertPathInsideRoot(memoryRootReal, fileReal, filePath);
|
|
2636
2974
|
assertPathInsideRoot(categoryRootReal, fileReal, filePath);
|
|
2637
|
-
return
|
|
2975
|
+
return fs4.readFileSync(filePath, "utf8");
|
|
2638
2976
|
} catch {
|
|
2639
2977
|
return null;
|
|
2640
2978
|
}
|
|
@@ -2664,13 +3002,13 @@ function assertPathInsideRoot(rootReal, targetReal, sourcePath) {
|
|
|
2664
3002
|
throw new Error(`Refusing to scan memory path outside root: ${sourcePath}`);
|
|
2665
3003
|
}
|
|
2666
3004
|
function walkMdFiles(dir, memoryRootReal, categoryRootReal, callback) {
|
|
2667
|
-
for (const entry of
|
|
3005
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
2668
3006
|
const fullPath = path8.join(dir, entry.name);
|
|
2669
|
-
const entryStat =
|
|
3007
|
+
const entryStat = fs4.lstatSync(fullPath);
|
|
2670
3008
|
if (entryStat.isSymbolicLink()) {
|
|
2671
3009
|
throw new Error(`Refusing to scan symlinked memory path: ${fullPath}`);
|
|
2672
3010
|
}
|
|
2673
|
-
const entryReal =
|
|
3011
|
+
const entryReal = fs4.realpathSync(fullPath);
|
|
2674
3012
|
assertPathInsideRoot(memoryRootReal, entryReal, fullPath);
|
|
2675
3013
|
assertPathInsideRoot(categoryRootReal, entryReal, fullPath);
|
|
2676
3014
|
if (entryStat.isDirectory()) {
|
|
@@ -2682,14 +3020,14 @@ function walkMdFiles(dir, memoryRootReal, categoryRootReal, callback) {
|
|
|
2682
3020
|
}
|
|
2683
3021
|
|
|
2684
3022
|
// src/review/index.ts
|
|
2685
|
-
import
|
|
3023
|
+
import fs5 from "fs";
|
|
2686
3024
|
import path9 from "path";
|
|
2687
3025
|
var DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
|
|
2688
3026
|
function realMemoryRoot(memoryDir) {
|
|
2689
3027
|
try {
|
|
2690
|
-
const stat =
|
|
3028
|
+
const stat = fs5.lstatSync(memoryDir);
|
|
2691
3029
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
2692
|
-
return
|
|
3030
|
+
return fs5.realpathSync(memoryDir);
|
|
2693
3031
|
} catch {
|
|
2694
3032
|
return null;
|
|
2695
3033
|
}
|
|
@@ -2700,21 +3038,21 @@ function isPathInside2(rootReal, candidateReal) {
|
|
|
2700
3038
|
}
|
|
2701
3039
|
function isSafeDirectory(rootReal, dir) {
|
|
2702
3040
|
try {
|
|
2703
|
-
const stat =
|
|
3041
|
+
const stat = fs5.lstatSync(dir);
|
|
2704
3042
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
2705
|
-
return isPathInside2(rootReal,
|
|
3043
|
+
return isPathInside2(rootReal, fs5.realpathSync(dir));
|
|
2706
3044
|
} catch {
|
|
2707
3045
|
return false;
|
|
2708
3046
|
}
|
|
2709
3047
|
}
|
|
2710
3048
|
function ensureSafeDirectory(rootReal, dir) {
|
|
2711
|
-
if (
|
|
3049
|
+
if (fs5.existsSync(dir)) {
|
|
2712
3050
|
if (!isSafeDirectory(rootReal, dir)) {
|
|
2713
3051
|
throw new Error(`Refusing to write through unsafe review path: ${dir}`);
|
|
2714
3052
|
}
|
|
2715
3053
|
return;
|
|
2716
3054
|
}
|
|
2717
|
-
|
|
3055
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2718
3056
|
if (!isSafeDirectory(rootReal, dir)) {
|
|
2719
3057
|
throw new Error(`Refusing to write through unsafe review path: ${dir}`);
|
|
2720
3058
|
}
|
|
@@ -2740,7 +3078,7 @@ function listReviewItems(options) {
|
|
|
2740
3078
|
items.push(item);
|
|
2741
3079
|
};
|
|
2742
3080
|
const suggestionsDir = path9.join(memoryDir, "suggestions");
|
|
2743
|
-
if (!isLimitReached() &&
|
|
3081
|
+
if (!isLimitReached() && fs5.existsSync(suggestionsDir) && isSafeDirectory(rootReal, suggestionsDir)) {
|
|
2744
3082
|
walkMd(rootReal, suggestionsDir, (filePath, content) => {
|
|
2745
3083
|
if (isLimitReached()) return true;
|
|
2746
3084
|
const fm = parseFrontmatter4(content);
|
|
@@ -2761,7 +3099,7 @@ function listReviewItems(options) {
|
|
|
2761
3099
|
});
|
|
2762
3100
|
}
|
|
2763
3101
|
const reviewDir = path9.join(memoryDir, "review");
|
|
2764
|
-
if (!isLimitReached() &&
|
|
3102
|
+
if (!isLimitReached() && fs5.existsSync(reviewDir) && isSafeDirectory(rootReal, reviewDir)) {
|
|
2765
3103
|
walkMd(rootReal, reviewDir, (filePath, content) => {
|
|
2766
3104
|
if (isLimitReached()) return true;
|
|
2767
3105
|
const fm = parseFrontmatter4(content);
|
|
@@ -2786,7 +3124,7 @@ function listReviewItems(options) {
|
|
|
2786
3124
|
for (const category of categories) {
|
|
2787
3125
|
if (isLimitReached()) break;
|
|
2788
3126
|
const dir = path9.join(memoryDir, category);
|
|
2789
|
-
if (!
|
|
3127
|
+
if (!fs5.existsSync(dir) || !isSafeDirectory(rootReal, dir)) continue;
|
|
2790
3128
|
walkMd(rootReal, dir, (filePath, content) => {
|
|
2791
3129
|
if (isLimitReached()) return true;
|
|
2792
3130
|
const fm = parseFrontmatter4(content);
|
|
@@ -2833,7 +3171,7 @@ function approveItem(memoryDir, itemId, options) {
|
|
|
2833
3171
|
if (!found) {
|
|
2834
3172
|
return { itemId, action: "approve", message: "Item not found" };
|
|
2835
3173
|
}
|
|
2836
|
-
const content =
|
|
3174
|
+
const content = fs5.readFileSync(found.filePath, "utf8");
|
|
2837
3175
|
const fm = parseFrontmatter4(content);
|
|
2838
3176
|
if (!fm) return { itemId, action: "approve", message: "Could not parse frontmatter" };
|
|
2839
3177
|
const updatedContent = updateFrontmatterFields(content, {
|
|
@@ -2842,7 +3180,7 @@ function approveItem(memoryDir, itemId, options) {
|
|
|
2842
3180
|
reviewDismissed: null
|
|
2843
3181
|
});
|
|
2844
3182
|
if (found.location === "category") {
|
|
2845
|
-
|
|
3183
|
+
fs5.writeFileSync(found.filePath, updatedContent, "utf8");
|
|
2846
3184
|
return {
|
|
2847
3185
|
itemId,
|
|
2848
3186
|
action: "approve",
|
|
@@ -2857,7 +3195,7 @@ function approveItem(memoryDir, itemId, options) {
|
|
|
2857
3195
|
const outputPath = path9.join(targetDir, dateDir, path9.basename(found.filePath));
|
|
2858
3196
|
ensureSafeDirectory(rootReal, path9.dirname(outputPath));
|
|
2859
3197
|
const promotedPath = writeFileWithoutClobber(outputPath, updatedContent, itemId);
|
|
2860
|
-
|
|
3198
|
+
fs5.unlinkSync(found.filePath);
|
|
2861
3199
|
return {
|
|
2862
3200
|
itemId,
|
|
2863
3201
|
action: "approve",
|
|
@@ -2871,11 +3209,11 @@ function dismissItem(memoryDir, itemId, options) {
|
|
|
2871
3209
|
return { itemId, action: "dismiss", message: "Item not found" };
|
|
2872
3210
|
}
|
|
2873
3211
|
if (found.location === "queue") {
|
|
2874
|
-
|
|
3212
|
+
fs5.unlinkSync(found.filePath);
|
|
2875
3213
|
return { itemId, action: "dismiss", message: "Dismissed and removed" };
|
|
2876
3214
|
}
|
|
2877
|
-
const content =
|
|
2878
|
-
|
|
3215
|
+
const content = fs5.readFileSync(found.filePath, "utf8");
|
|
3216
|
+
fs5.writeFileSync(
|
|
2879
3217
|
found.filePath,
|
|
2880
3218
|
updateFrontmatterFields(content, {
|
|
2881
3219
|
reviewDismissed: "true",
|
|
@@ -2895,12 +3233,12 @@ function flagItem(memoryDir, itemId, options) {
|
|
|
2895
3233
|
if (!found) {
|
|
2896
3234
|
return { itemId, action: "flag", message: "Item not found" };
|
|
2897
3235
|
}
|
|
2898
|
-
const content =
|
|
3236
|
+
const content = fs5.readFileSync(found.filePath, "utf8");
|
|
2899
3237
|
const fixed = updateFrontmatterFields(content, {
|
|
2900
3238
|
flagged: "true",
|
|
2901
3239
|
flaggedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2902
3240
|
});
|
|
2903
|
-
|
|
3241
|
+
fs5.writeFileSync(found.filePath, fixed);
|
|
2904
3242
|
return {
|
|
2905
3243
|
itemId,
|
|
2906
3244
|
action: "flag",
|
|
@@ -2913,13 +3251,13 @@ function findReviewFileById(memoryDir, id, options = {}) {
|
|
|
2913
3251
|
if (!rootReal) return null;
|
|
2914
3252
|
for (const loc of ["suggestions", "review"]) {
|
|
2915
3253
|
const dir = path9.join(memoryDir, loc);
|
|
2916
|
-
if (!
|
|
3254
|
+
if (!fs5.existsSync(dir) || !isSafeDirectory(rootReal, dir)) continue;
|
|
2917
3255
|
const found = findFileById(rootReal, dir, id);
|
|
2918
3256
|
if (found) return { filePath: found, location: "queue" };
|
|
2919
3257
|
}
|
|
2920
3258
|
for (const category of ALL_CATEGORY_DIRS) {
|
|
2921
3259
|
const dir = path9.join(memoryDir, category);
|
|
2922
|
-
if (!
|
|
3260
|
+
if (!fs5.existsSync(dir) || !isSafeDirectory(rootReal, dir)) continue;
|
|
2923
3261
|
const found = findFileById(rootReal, dir, id, (fm) => isLowConfidenceReviewCandidate(fm, options));
|
|
2924
3262
|
if (found) return { filePath: found, location: "category" };
|
|
2925
3263
|
}
|
|
@@ -2985,7 +3323,7 @@ function updateFrontmatterFields(content, fields) {
|
|
|
2985
3323
|
}
|
|
2986
3324
|
function readFileSafe3(filePath) {
|
|
2987
3325
|
try {
|
|
2988
|
-
return
|
|
3326
|
+
return fs5.readFileSync(filePath, "utf8");
|
|
2989
3327
|
} catch {
|
|
2990
3328
|
return null;
|
|
2991
3329
|
}
|
|
@@ -2999,7 +3337,7 @@ function writeFileWithoutClobber(basePath, content, discriminator) {
|
|
|
2999
3337
|
`${parsed.name}-${safeDiscriminator}${attempt === 1 ? "" : `-${attempt}`}${parsed.ext || ".md"}`
|
|
3000
3338
|
);
|
|
3001
3339
|
try {
|
|
3002
|
-
|
|
3340
|
+
fs5.writeFileSync(candidate, content, { encoding: "utf8", flag: "wx" });
|
|
3003
3341
|
return candidate;
|
|
3004
3342
|
} catch (error) {
|
|
3005
3343
|
if (error.code === "EEXIST") continue;
|
|
@@ -3049,7 +3387,7 @@ function extractBody4(content) {
|
|
|
3049
3387
|
}
|
|
3050
3388
|
function walkMd(rootReal, dir, callback) {
|
|
3051
3389
|
if (!isSafeDirectory(rootReal, dir)) return false;
|
|
3052
|
-
for (const entry of
|
|
3390
|
+
for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
|
|
3053
3391
|
const fullPath = path9.join(dir, entry.name);
|
|
3054
3392
|
if (entry.isSymbolicLink()) continue;
|
|
3055
3393
|
if (entry.isDirectory()) {
|
|
@@ -3064,7 +3402,7 @@ function walkMd(rootReal, dir, callback) {
|
|
|
3064
3402
|
function walkMdPaths(rootReal, dir) {
|
|
3065
3403
|
const results = [];
|
|
3066
3404
|
if (!isSafeDirectory(rootReal, dir)) return results;
|
|
3067
|
-
for (const entry of
|
|
3405
|
+
for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
|
|
3068
3406
|
const fullPath = path9.join(dir, entry.name);
|
|
3069
3407
|
if (entry.isSymbolicLink()) continue;
|
|
3070
3408
|
if (entry.isDirectory()) {
|
|
@@ -3077,7 +3415,7 @@ function walkMdPaths(rootReal, dir) {
|
|
|
3077
3415
|
}
|
|
3078
3416
|
|
|
3079
3417
|
// src/sync/index.ts
|
|
3080
|
-
import
|
|
3418
|
+
import fs6 from "fs";
|
|
3081
3419
|
import path10 from "path";
|
|
3082
3420
|
import crypto6 from "crypto";
|
|
3083
3421
|
var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".mdx", ".rst"]);
|
|
@@ -3116,8 +3454,8 @@ function syncChanges(options) {
|
|
|
3116
3454
|
for (const [relPath, hash] of Object.entries(currentFiles)) {
|
|
3117
3455
|
newState.fileHashes[relPath] = hash;
|
|
3118
3456
|
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3457
|
+
fs6.mkdirSync(path10.dirname(stateFilePath), { recursive: true });
|
|
3458
|
+
fs6.writeFileSync(stateFilePath, JSON.stringify(newState, null, 2));
|
|
3121
3459
|
}
|
|
3122
3460
|
return {
|
|
3123
3461
|
scanned: Object.keys(currentFiles).length,
|
|
@@ -3171,7 +3509,7 @@ function scanFiles(root, extensions, exclude) {
|
|
|
3171
3509
|
function walk(dir, isRoot = false) {
|
|
3172
3510
|
let entries;
|
|
3173
3511
|
try {
|
|
3174
|
-
entries =
|
|
3512
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
3175
3513
|
} catch (err) {
|
|
3176
3514
|
if (isRoot) {
|
|
3177
3515
|
throw new Error(
|
|
@@ -3190,7 +3528,7 @@ function scanFiles(root, extensions, exclude) {
|
|
|
3190
3528
|
if (!extensions.has(ext)) continue;
|
|
3191
3529
|
const relPath = path10.relative(root, fullPath);
|
|
3192
3530
|
try {
|
|
3193
|
-
const content =
|
|
3531
|
+
const content = fs6.readFileSync(fullPath, "utf8");
|
|
3194
3532
|
result[relPath] = hashContent3(content);
|
|
3195
3533
|
} catch {
|
|
3196
3534
|
}
|
|
@@ -3207,7 +3545,7 @@ function computeDiff(current, previous, sourceDir) {
|
|
|
3207
3545
|
if (!(relPath in previous)) {
|
|
3208
3546
|
let size = 0;
|
|
3209
3547
|
try {
|
|
3210
|
-
size =
|
|
3548
|
+
size = fs6.statSync(fullPath).size;
|
|
3211
3549
|
} catch {
|
|
3212
3550
|
}
|
|
3213
3551
|
changes.push({
|
|
@@ -3220,7 +3558,7 @@ function computeDiff(current, previous, sourceDir) {
|
|
|
3220
3558
|
} else if (previous[relPath] !== hash) {
|
|
3221
3559
|
let size = 0;
|
|
3222
3560
|
try {
|
|
3223
|
-
size =
|
|
3561
|
+
size = fs6.statSync(fullPath).size;
|
|
3224
3562
|
} catch {
|
|
3225
3563
|
}
|
|
3226
3564
|
changes.push({
|
|
@@ -3248,7 +3586,7 @@ function computeDiff(current, previous, sourceDir) {
|
|
|
3248
3586
|
}
|
|
3249
3587
|
function loadState(stateFilePath) {
|
|
3250
3588
|
try {
|
|
3251
|
-
const raw =
|
|
3589
|
+
const raw = fs6.readFileSync(stateFilePath, "utf8");
|
|
3252
3590
|
return JSON.parse(raw);
|
|
3253
3591
|
} catch {
|
|
3254
3592
|
return {
|
|
@@ -3263,7 +3601,7 @@ function hashContent3(content) {
|
|
|
3263
3601
|
}
|
|
3264
3602
|
|
|
3265
3603
|
// src/spaces/index.ts
|
|
3266
|
-
import
|
|
3604
|
+
import fs7 from "fs";
|
|
3267
3605
|
import path11 from "path";
|
|
3268
3606
|
import crypto7 from "crypto";
|
|
3269
3607
|
var MANIFEST_VERSION = 1;
|
|
@@ -3279,7 +3617,7 @@ function getManifestPath(baseDir) {
|
|
|
3279
3617
|
}
|
|
3280
3618
|
function loadManifest(baseDir, memoryDirOverride) {
|
|
3281
3619
|
const manifestPath2 = getManifestPath(baseDir);
|
|
3282
|
-
if (!
|
|
3620
|
+
if (!fs7.existsSync(manifestPath2)) {
|
|
3283
3621
|
const personalSpace = createPersonalSpace(baseDir, memoryDirOverride);
|
|
3284
3622
|
const manifest = {
|
|
3285
3623
|
activeSpaceId: personalSpace.id,
|
|
@@ -3289,19 +3627,19 @@ function loadManifest(baseDir, memoryDirOverride) {
|
|
|
3289
3627
|
saveManifest(manifest, baseDir);
|
|
3290
3628
|
return manifest;
|
|
3291
3629
|
}
|
|
3292
|
-
const raw = JSON.parse(
|
|
3630
|
+
const raw = JSON.parse(fs7.readFileSync(manifestPath2, "utf8"));
|
|
3293
3631
|
return raw;
|
|
3294
3632
|
}
|
|
3295
3633
|
function saveManifest(manifest, baseDir) {
|
|
3296
3634
|
const manifestPath2 = getManifestPath(baseDir);
|
|
3297
|
-
|
|
3298
|
-
|
|
3635
|
+
fs7.mkdirSync(path11.dirname(manifestPath2), { recursive: true });
|
|
3636
|
+
fs7.writeFileSync(manifestPath2, JSON.stringify(manifest, null, 2) + "\n");
|
|
3299
3637
|
}
|
|
3300
3638
|
function createPersonalSpace(baseDir, memoryDirOverride) {
|
|
3301
3639
|
const homeDir = baseDir ?? resolveHomeDir();
|
|
3302
3640
|
const standalonePath = path11.join(homeDir, ".engram", "memory");
|
|
3303
3641
|
const openclawPath = path11.join(homeDir, ".openclaw", "workspace", "memory", "local");
|
|
3304
|
-
const memoryDir = memoryDirOverride ?? readEnvVar("REMNIC_MEMORY_DIR") ?? readEnvVar("ENGRAM_MEMORY_DIR") ?? (
|
|
3642
|
+
const memoryDir = memoryDirOverride ?? readEnvVar("REMNIC_MEMORY_DIR") ?? readEnvVar("ENGRAM_MEMORY_DIR") ?? (fs7.existsSync(standalonePath) ? standalonePath : fs7.existsSync(openclawPath) ? openclawPath : standalonePath);
|
|
3305
3643
|
const normalizedMemoryDir = normalizeSpaceMemoryDir(memoryDir);
|
|
3306
3644
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3307
3645
|
return {
|
|
@@ -3353,7 +3691,7 @@ function createSpace(options) {
|
|
|
3353
3691
|
owner: readEnvVar("USER"),
|
|
3354
3692
|
parentSpaceId: options.parentSpaceId
|
|
3355
3693
|
};
|
|
3356
|
-
|
|
3694
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
3357
3695
|
manifest.spaces.push(space);
|
|
3358
3696
|
manifest.updatedAt = now;
|
|
3359
3697
|
saveManifest(manifest, options.baseDir);
|
|
@@ -3528,30 +3866,30 @@ function mergeSpaces(sourceSpaceId, targetSpaceId, options) {
|
|
|
3528
3866
|
}
|
|
3529
3867
|
function getAuditLog(baseDir) {
|
|
3530
3868
|
const auditPath = path11.join(getSpacesDir(baseDir), "audit.jsonl");
|
|
3531
|
-
if (!
|
|
3532
|
-
const lines =
|
|
3869
|
+
if (!fs7.existsSync(auditPath)) return [];
|
|
3870
|
+
const lines = fs7.readFileSync(auditPath, "utf8").trim().split("\n");
|
|
3533
3871
|
return lines.filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
3534
3872
|
}
|
|
3535
3873
|
function appendAudit(entry, baseDir) {
|
|
3536
3874
|
const auditPath = path11.join(getSpacesDir(baseDir), "audit.jsonl");
|
|
3537
|
-
|
|
3875
|
+
fs7.mkdirSync(path11.dirname(auditPath), { recursive: true });
|
|
3538
3876
|
const full = {
|
|
3539
3877
|
id: crypto7.randomUUID(),
|
|
3540
3878
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3541
3879
|
...entry
|
|
3542
3880
|
};
|
|
3543
|
-
|
|
3881
|
+
fs7.appendFileSync(auditPath, JSON.stringify(full) + "\n");
|
|
3544
3882
|
}
|
|
3545
3883
|
function copyMemories(sourceDir, targetDir, options) {
|
|
3546
3884
|
let merged = 0;
|
|
3547
3885
|
const conflicts = [];
|
|
3548
3886
|
let skipped = 0;
|
|
3549
|
-
if (!
|
|
3887
|
+
if (!fs7.existsSync(sourceDir)) {
|
|
3550
3888
|
return { merged: 0, conflicts: [], skipped: 0 };
|
|
3551
3889
|
}
|
|
3552
|
-
const sourceRoot =
|
|
3553
|
-
|
|
3554
|
-
const targetRoot =
|
|
3890
|
+
const sourceRoot = fs7.realpathSync(sourceDir);
|
|
3891
|
+
fs7.mkdirSync(targetDir, { recursive: true });
|
|
3892
|
+
const targetRoot = fs7.realpathSync(targetDir);
|
|
3555
3893
|
const sourceFiles = walkMd2(sourceRoot);
|
|
3556
3894
|
for (const sourcePath of sourceFiles) {
|
|
3557
3895
|
const sourceRealPath = safeRealpath(sourcePath);
|
|
@@ -3564,7 +3902,7 @@ function copyMemories(sourceDir, targetDir, options) {
|
|
|
3564
3902
|
skipped++;
|
|
3565
3903
|
continue;
|
|
3566
3904
|
}
|
|
3567
|
-
const content =
|
|
3905
|
+
const content = fs7.readFileSync(sourcePath, "utf8");
|
|
3568
3906
|
const relativePath = path11.relative(sourceRoot, sourceRealPath);
|
|
3569
3907
|
const targetPath = path11.resolve(targetRoot, relativePath);
|
|
3570
3908
|
if (!isPathInsideRoot(targetPath, targetRoot)) {
|
|
@@ -3579,7 +3917,7 @@ function copyMemories(sourceDir, targetDir, options) {
|
|
|
3579
3917
|
continue;
|
|
3580
3918
|
}
|
|
3581
3919
|
}
|
|
3582
|
-
if (
|
|
3920
|
+
if (fs7.existsSync(targetPath)) {
|
|
3583
3921
|
const targetStat = safeLstat(targetPath);
|
|
3584
3922
|
if (!targetStat?.isFile() || targetStat.isSymbolicLink()) {
|
|
3585
3923
|
skipped++;
|
|
@@ -3591,8 +3929,8 @@ function copyMemories(sourceDir, targetDir, options) {
|
|
|
3591
3929
|
continue;
|
|
3592
3930
|
}
|
|
3593
3931
|
}
|
|
3594
|
-
if (
|
|
3595
|
-
const targetContent =
|
|
3932
|
+
if (fs7.existsSync(targetPath) && !options?.force) {
|
|
3933
|
+
const targetContent = fs7.readFileSync(targetPath, "utf8");
|
|
3596
3934
|
const targetHash = hashContent4(targetContent);
|
|
3597
3935
|
if (sourceHash !== targetHash) {
|
|
3598
3936
|
conflicts.push({
|
|
@@ -3608,13 +3946,13 @@ function copyMemories(sourceDir, targetDir, options) {
|
|
|
3608
3946
|
skipped++;
|
|
3609
3947
|
continue;
|
|
3610
3948
|
}
|
|
3611
|
-
|
|
3949
|
+
fs7.mkdirSync(path11.dirname(targetPath), { recursive: true });
|
|
3612
3950
|
const targetParentRealPath = safeRealpath(path11.dirname(targetPath));
|
|
3613
3951
|
if (!targetParentRealPath || !isPathInsideRoot(targetParentRealPath, targetRoot)) {
|
|
3614
3952
|
skipped++;
|
|
3615
3953
|
continue;
|
|
3616
3954
|
}
|
|
3617
|
-
|
|
3955
|
+
fs7.writeFileSync(targetPath, content);
|
|
3618
3956
|
merged++;
|
|
3619
3957
|
}
|
|
3620
3958
|
return { merged, conflicts, skipped };
|
|
@@ -3625,7 +3963,7 @@ function hashContent4(content) {
|
|
|
3625
3963
|
function walkMd2(dir) {
|
|
3626
3964
|
const results = [];
|
|
3627
3965
|
function walk(d) {
|
|
3628
|
-
for (const entry of
|
|
3966
|
+
for (const entry of fs7.readdirSync(d, { withFileTypes: true })) {
|
|
3629
3967
|
const fullPath = path11.join(d, entry.name);
|
|
3630
3968
|
if (entry.isSymbolicLink()) {
|
|
3631
3969
|
continue;
|
|
@@ -3642,14 +3980,14 @@ function walkMd2(dir) {
|
|
|
3642
3980
|
}
|
|
3643
3981
|
function safeLstat(filePath) {
|
|
3644
3982
|
try {
|
|
3645
|
-
return
|
|
3983
|
+
return fs7.lstatSync(filePath);
|
|
3646
3984
|
} catch {
|
|
3647
3985
|
return null;
|
|
3648
3986
|
}
|
|
3649
3987
|
}
|
|
3650
3988
|
function safeRealpath(filePath) {
|
|
3651
3989
|
try {
|
|
3652
|
-
return
|
|
3990
|
+
return fs7.realpathSync(filePath);
|
|
3653
3991
|
} catch {
|
|
3654
3992
|
return null;
|
|
3655
3993
|
}
|
|
@@ -3775,7 +4113,7 @@ var REMNIC_RECALL_DECISION_RULES = `## When to Use Recall vs Direct Read
|
|
|
3775
4113
|
`;
|
|
3776
4114
|
|
|
3777
4115
|
// src/memory-extension/codex-publisher.ts
|
|
3778
|
-
import
|
|
4116
|
+
import fs8 from "fs";
|
|
3779
4117
|
import os from "os";
|
|
3780
4118
|
import path12 from "path";
|
|
3781
4119
|
var REMNIC_EXTENSION_DIR_NAME = "remnic";
|
|
@@ -3810,7 +4148,7 @@ var CodexMemoryExtensionPublisher = class {
|
|
|
3810
4148
|
async isHostAvailable() {
|
|
3811
4149
|
try {
|
|
3812
4150
|
const home = readEnvVar("CODEX_HOME")?.trim() || path12.join(resolveHomeDir(), ".codex");
|
|
3813
|
-
return
|
|
4151
|
+
return fs8.existsSync(home);
|
|
3814
4152
|
} catch {
|
|
3815
4153
|
return false;
|
|
3816
4154
|
}
|
|
@@ -3864,18 +4202,18 @@ When running inside the Codex phase-2 consolidation sandbox:
|
|
|
3864
4202
|
const filesWritten = [];
|
|
3865
4203
|
const skipped = [];
|
|
3866
4204
|
ctx.log.info(`Publishing Codex memory extension to ${extensionRoot}`);
|
|
3867
|
-
|
|
4205
|
+
fs8.mkdirSync(extensionRoot, { recursive: true });
|
|
3868
4206
|
const content = await this.renderInstructions(ctx);
|
|
3869
4207
|
const tmpPath = `${instructionsPath}.tmp-${process.pid}-${Date.now()}`;
|
|
3870
4208
|
try {
|
|
3871
|
-
|
|
3872
|
-
|
|
4209
|
+
fs8.writeFileSync(tmpPath, content, "utf-8");
|
|
4210
|
+
fs8.renameSync(tmpPath, instructionsPath);
|
|
3873
4211
|
filesWritten.push(instructionsPath);
|
|
3874
4212
|
ctx.log.info(`Wrote ${instructionsPath}`);
|
|
3875
4213
|
} catch (err) {
|
|
3876
4214
|
try {
|
|
3877
|
-
if (
|
|
3878
|
-
|
|
4215
|
+
if (fs8.existsSync(tmpPath)) {
|
|
4216
|
+
fs8.unlinkSync(tmpPath);
|
|
3879
4217
|
}
|
|
3880
4218
|
} catch {
|
|
3881
4219
|
}
|
|
@@ -3890,8 +4228,8 @@ When running inside the Codex phase-2 consolidation sandbox:
|
|
|
3890
4228
|
}
|
|
3891
4229
|
async unpublish() {
|
|
3892
4230
|
const extensionRoot = await this.resolveExtensionRoot();
|
|
3893
|
-
if (
|
|
3894
|
-
|
|
4231
|
+
if (fs8.existsSync(extensionRoot)) {
|
|
4232
|
+
fs8.rmSync(extensionRoot, { recursive: true, force: true });
|
|
3895
4233
|
}
|
|
3896
4234
|
}
|
|
3897
4235
|
};
|