@massu/core 1.16.1 → 1.16.3
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/CHANGELOG.md +95 -0
- package/dist/cli.js +1090 -697
- package/dist/hooks/auto-learning-pipeline.js +401 -55
- package/dist/hooks/classify-failure.js +399 -53
- package/dist/hooks/cost-tracker.js +411 -65
- package/dist/hooks/fix-detector.js +410 -64
- package/dist/hooks/incident-pipeline.js +396 -50
- package/dist/hooks/intent-suggester.js +411 -65
- package/dist/hooks/memory-recall.js +415 -69
- package/dist/hooks/post-edit-context.js +413 -67
- package/dist/hooks/post-tool-use.js +439 -93
- package/dist/hooks/pre-compact.js +411 -65
- package/dist/hooks/pre-delete-check.js +415 -69
- package/dist/hooks/pre-tool-use-gate.js +415 -69
- package/dist/hooks/quality-event.js +411 -65
- package/dist/hooks/rule-enforcement-pipeline.js +397 -51
- package/dist/hooks/security-gate.js +411 -65
- package/dist/hooks/session-end.js +441 -95
- package/dist/hooks/session-start.js +533 -283
- package/dist/hooks/user-prompt.js +445 -99
- package/package.json +1 -1
|
@@ -372,9 +372,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
372
372
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
373
373
|
if (resolvedKey.apiKey) {
|
|
374
374
|
_config.cloud = {
|
|
375
|
-
enabled: true,
|
|
376
375
|
sync: { memory: true, analytics: true, audit: true },
|
|
377
376
|
..._config.cloud,
|
|
377
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
378
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
379
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
380
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
381
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
378
382
|
apiKey: resolvedKey.apiKey,
|
|
379
383
|
endpoint: resolvedEndpoint
|
|
380
384
|
};
|
|
@@ -594,9 +598,22 @@ var init_config = __esm({
|
|
|
594
598
|
})).default([])
|
|
595
599
|
}).optional();
|
|
596
600
|
CloudConfigSchema = z2.object({
|
|
597
|
-
|
|
601
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
602
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
603
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
604
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
605
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
606
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
607
|
+
enabled: z2.boolean().optional(),
|
|
598
608
|
apiKey: z2.string().optional(),
|
|
599
609
|
endpoint: z2.string().optional(),
|
|
610
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
611
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
612
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
613
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
614
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
615
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
616
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
600
617
|
sync: z2.object({
|
|
601
618
|
memory: z2.boolean().default(true),
|
|
602
619
|
analytics: z2.boolean().default(true),
|
|
@@ -813,6 +830,335 @@ var init_config = __esm({
|
|
|
813
830
|
}
|
|
814
831
|
});
|
|
815
832
|
|
|
833
|
+
// src/lib/fileLock.ts
|
|
834
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
835
|
+
import { dirname as dirname2 } from "path";
|
|
836
|
+
import * as lockfile from "proper-lockfile";
|
|
837
|
+
function readLockHolderPid(lockPath) {
|
|
838
|
+
try {
|
|
839
|
+
const raw = readFileSync3(`${lockPath}.pid`, "utf-8").trim();
|
|
840
|
+
const pid = Number.parseInt(raw, 10);
|
|
841
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
842
|
+
return pid;
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function busyWaitSync(ms) {
|
|
848
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`withFileLockSync requires SharedArrayBuffer + Atomics for its retry-loop wait. This Node runtime does not provide them \u2014 refusing to fall back to a CPU spinloop. If you hit this in a sandboxed serverless env, the fix is to perform the lock-protected operation in a host runtime that supports Atomics.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const sab = new SharedArrayBuffer(4);
|
|
854
|
+
const view = new Int32Array(sab);
|
|
855
|
+
Atomics.wait(view, 0, 0, ms);
|
|
856
|
+
}
|
|
857
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
858
|
+
mkdirSync2(dirname2(lockPath), { recursive: true });
|
|
859
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
860
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
861
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
862
|
+
const now = opts.now ?? Date.now;
|
|
863
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
864
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
865
|
+
let release = null;
|
|
866
|
+
const deadline = now() + blockMs;
|
|
867
|
+
for (; ; ) {
|
|
868
|
+
try {
|
|
869
|
+
release = lockfile.lockSync(lockPath, {
|
|
870
|
+
stale: staleMs,
|
|
871
|
+
retries: 0,
|
|
872
|
+
realpath: false
|
|
873
|
+
});
|
|
874
|
+
try {
|
|
875
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
} catch (err) {
|
|
880
|
+
const e = err;
|
|
881
|
+
const code = e.code;
|
|
882
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
883
|
+
throw err;
|
|
884
|
+
}
|
|
885
|
+
if (now() >= deadline) {
|
|
886
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
887
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
888
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
889
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
890
|
+
}
|
|
891
|
+
sleep(pollIntervalMs);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
return fn();
|
|
896
|
+
} finally {
|
|
897
|
+
try {
|
|
898
|
+
if (release) release();
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
var FileLockBusyError;
|
|
908
|
+
var init_fileLock = __esm({
|
|
909
|
+
"src/lib/fileLock.ts"() {
|
|
910
|
+
"use strict";
|
|
911
|
+
FileLockBusyError = class extends Error {
|
|
912
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
913
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
914
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
915
|
+
this.lockPath = lockPath;
|
|
916
|
+
this.holderPid = holderPid;
|
|
917
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
918
|
+
this.causeCode = causeCode;
|
|
919
|
+
this.name = "FileLockBusyError";
|
|
920
|
+
}
|
|
921
|
+
lockPath;
|
|
922
|
+
holderPid;
|
|
923
|
+
retryAfterSeconds;
|
|
924
|
+
causeCode;
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// src/lib/sqlite-loader.ts
|
|
930
|
+
import { createRequire } from "module";
|
|
931
|
+
import { spawnSync } from "child_process";
|
|
932
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
933
|
+
import { dirname as dirname3, join } from "path";
|
|
934
|
+
function isNativeAbiError(err) {
|
|
935
|
+
const code = err?.code;
|
|
936
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
937
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
938
|
+
return /NODE_MODULE_VERSION/.test(msg) || /was compiled against a different Node\.js version/.test(msg) || /ERR_DLOPEN_FAILED/.test(msg) || /\.node\b/.test(msg) && /dlopen|invalid ELF|mach-o|image not found|no such file|not a valid Win32/i.test(msg);
|
|
939
|
+
}
|
|
940
|
+
function loadBetterSqlite3(opts = {}) {
|
|
941
|
+
if (_testCtor) return _testCtor;
|
|
942
|
+
if (opts.fresh) bustNativeCache();
|
|
943
|
+
return req("better-sqlite3");
|
|
944
|
+
}
|
|
945
|
+
function bustNativeCache() {
|
|
946
|
+
try {
|
|
947
|
+
const cache = req.cache;
|
|
948
|
+
if (!cache) return;
|
|
949
|
+
for (const key of Object.keys(cache)) {
|
|
950
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
951
|
+
delete cache[key];
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function openDatabase(dbPath, opts = {}) {
|
|
958
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
959
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
960
|
+
const Ctor = loadBetterSqlite3();
|
|
961
|
+
try {
|
|
962
|
+
return new Ctor(dbPath, ctorOpts);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (!isNativeAbiError(err)) {
|
|
965
|
+
throw err;
|
|
966
|
+
}
|
|
967
|
+
if (!selfHeal) {
|
|
968
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
969
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
970
|
+
}
|
|
971
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
972
|
+
if (!result.healed && !result.contended) {
|
|
973
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
977
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
978
|
+
} catch (retryErr) {
|
|
979
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function attemptNativeHeal(err) {
|
|
984
|
+
const start = Date.now();
|
|
985
|
+
const abiTo = process.versions.modules;
|
|
986
|
+
const abiFrom = parseAbiFrom(err);
|
|
987
|
+
let pkgDir;
|
|
988
|
+
try {
|
|
989
|
+
pkgDir = dirname3(req.resolve("better-sqlite3/package.json"));
|
|
990
|
+
} catch {
|
|
991
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
992
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
993
|
+
}
|
|
994
|
+
if (!isWritable(pkgDir)) {
|
|
995
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
996
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
997
|
+
}
|
|
998
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
999
|
+
try {
|
|
1000
|
+
return withFileLockSync(
|
|
1001
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
1002
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
1003
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
1004
|
+
);
|
|
1005
|
+
} catch (lockErr) {
|
|
1006
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
1007
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
1008
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
1012
|
+
try {
|
|
1013
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
1014
|
+
new FreshCtor(":memory:").close();
|
|
1015
|
+
const res2 = {
|
|
1016
|
+
healed: true,
|
|
1017
|
+
abiFrom,
|
|
1018
|
+
abiTo,
|
|
1019
|
+
durationMs: Date.now() - start,
|
|
1020
|
+
detail: "already healed by a concurrent process"
|
|
1021
|
+
};
|
|
1022
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
1023
|
+
return res2;
|
|
1024
|
+
} catch (probeErr) {
|
|
1025
|
+
if (!isNativeAbiError(probeErr)) {
|
|
1026
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
1030
|
+
if (prebuildBin) {
|
|
1031
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
1032
|
+
cwd: pkgDir,
|
|
1033
|
+
encoding: "utf-8",
|
|
1034
|
+
timeout: 12e4
|
|
1035
|
+
});
|
|
1036
|
+
if (r.status === 0) {
|
|
1037
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1038
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1039
|
+
return res2;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const gypBin = resolveNodeGypBin();
|
|
1043
|
+
if (gypBin && hasCompiler()) {
|
|
1044
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
1045
|
+
cwd: pkgDir,
|
|
1046
|
+
encoding: "utf-8",
|
|
1047
|
+
timeout: 3e5
|
|
1048
|
+
});
|
|
1049
|
+
if (r.status === 0) {
|
|
1050
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
1051
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
1052
|
+
return res2;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const res = {
|
|
1056
|
+
healed: false,
|
|
1057
|
+
reason: "heal-failed",
|
|
1058
|
+
abiFrom,
|
|
1059
|
+
abiTo,
|
|
1060
|
+
durationMs: Date.now() - start,
|
|
1061
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
1062
|
+
};
|
|
1063
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
1064
|
+
return res;
|
|
1065
|
+
}
|
|
1066
|
+
function recordHealEvent(event) {
|
|
1067
|
+
try {
|
|
1068
|
+
const rec = {
|
|
1069
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1070
|
+
node: process.versions.node,
|
|
1071
|
+
platform: process.platform,
|
|
1072
|
+
arch: process.arch,
|
|
1073
|
+
...event
|
|
1074
|
+
};
|
|
1075
|
+
const dir = credentialsDir();
|
|
1076
|
+
mkdirSync3(dir, { recursive: true });
|
|
1077
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
1078
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
1079
|
+
try {
|
|
1080
|
+
chmodSync2(file, 384);
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function parseAbiFrom(err) {
|
|
1087
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
1088
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
1089
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
function detailOf(err) {
|
|
1093
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1094
|
+
return msg.slice(0, 300);
|
|
1095
|
+
}
|
|
1096
|
+
function isWritable(dir) {
|
|
1097
|
+
try {
|
|
1098
|
+
accessSync(dir, fsConstants.W_OK);
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function resolveLocalBin(pkgDir, name) {
|
|
1105
|
+
const candidates = [
|
|
1106
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
1107
|
+
// nested install
|
|
1108
|
+
join(pkgDir, "..", ".bin", name),
|
|
1109
|
+
// hoisted (node_modules/.bin)
|
|
1110
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
1111
|
+
// workspace hoist
|
|
1112
|
+
];
|
|
1113
|
+
for (const c of candidates) {
|
|
1114
|
+
if (existsSync3(c)) return c;
|
|
1115
|
+
}
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
function resolveNodeGypBin() {
|
|
1119
|
+
try {
|
|
1120
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
1121
|
+
} catch {
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function hasCompiler() {
|
|
1126
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
1127
|
+
try {
|
|
1128
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
1129
|
+
if (r.status === 0) return true;
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
1136
|
+
var init_sqlite_loader = __esm({
|
|
1137
|
+
"src/lib/sqlite-loader.ts"() {
|
|
1138
|
+
"use strict";
|
|
1139
|
+
init_credentials();
|
|
1140
|
+
init_fileLock();
|
|
1141
|
+
req = createRequire(import.meta.url);
|
|
1142
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
1143
|
+
MemoryEngineUnusableError = class extends Error {
|
|
1144
|
+
reason;
|
|
1145
|
+
remedy;
|
|
1146
|
+
detail;
|
|
1147
|
+
constructor(reason, detail) {
|
|
1148
|
+
super(
|
|
1149
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
1150
|
+
);
|
|
1151
|
+
this.name = "MemoryEngineUnusableError";
|
|
1152
|
+
this.reason = reason;
|
|
1153
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
1154
|
+
this.detail = detail;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
_testCtor = null;
|
|
1158
|
+
_testHeal = null;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
816
1162
|
// src/memory-vector.ts
|
|
817
1163
|
function float32ToBlob(vec) {
|
|
818
1164
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -833,27 +1179,26 @@ var init_memory_vector = __esm({
|
|
|
833
1179
|
});
|
|
834
1180
|
|
|
835
1181
|
// src/db-backup.ts
|
|
836
|
-
import
|
|
837
|
-
import {
|
|
838
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1182
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
|
|
1183
|
+
import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
|
|
839
1184
|
import { homedir as homedir3 } from "os";
|
|
840
1185
|
function dbBackupsRoot(home = homedir3()) {
|
|
841
1186
|
return resolve3(home, ".massu", "db-backups");
|
|
842
1187
|
}
|
|
843
1188
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
844
1189
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
845
|
-
return
|
|
1190
|
+
return join2(dbBackupsRoot(home), slug);
|
|
846
1191
|
}
|
|
847
1192
|
function backupStamp(nowMs) {
|
|
848
1193
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
849
1194
|
}
|
|
850
1195
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
851
1196
|
const dir = projectBackupDir(projectRoot, home);
|
|
852
|
-
if (!
|
|
1197
|
+
if (!existsSync4(dir)) return [];
|
|
853
1198
|
const out = [];
|
|
854
1199
|
for (const f of readdirSync(dir)) {
|
|
855
1200
|
if (!f.endsWith(".db")) continue;
|
|
856
|
-
const p =
|
|
1201
|
+
const p = join2(dir, f);
|
|
857
1202
|
const st = statSync(p);
|
|
858
1203
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
859
1204
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -866,13 +1211,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
866
1211
|
if (backups.length === 0) return false;
|
|
867
1212
|
const newest = backups[0];
|
|
868
1213
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
869
|
-
if (
|
|
1214
|
+
if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
870
1215
|
return true;
|
|
871
1216
|
}
|
|
872
1217
|
function integrityOk(dbPath) {
|
|
873
1218
|
let db = null;
|
|
874
1219
|
try {
|
|
875
|
-
db =
|
|
1220
|
+
db = openDatabase(dbPath, { readonly: true });
|
|
876
1221
|
const rows = db.pragma("integrity_check");
|
|
877
1222
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
878
1223
|
} catch {
|
|
@@ -885,16 +1230,16 @@ function integrityOk(dbPath) {
|
|
|
885
1230
|
}
|
|
886
1231
|
}
|
|
887
1232
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
888
|
-
if (!
|
|
1233
|
+
if (!existsSync4(dbPath)) {
|
|
889
1234
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
890
1235
|
}
|
|
891
1236
|
const dir = projectBackupDir(projectRoot, home);
|
|
892
|
-
|
|
1237
|
+
mkdirSync4(dir, { recursive: true });
|
|
893
1238
|
const name = basename(dbPath, ".db");
|
|
894
|
-
const dest =
|
|
1239
|
+
const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
895
1240
|
let src = null;
|
|
896
1241
|
try {
|
|
897
|
-
src =
|
|
1242
|
+
src = openDatabase(dbPath, { readonly: true });
|
|
898
1243
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
899
1244
|
} catch (err) {
|
|
900
1245
|
throw new DbBackupError(
|
|
@@ -933,7 +1278,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
933
1278
|
}
|
|
934
1279
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
935
1280
|
try {
|
|
936
|
-
if (!
|
|
1281
|
+
if (!existsSync4(dbPath)) return null;
|
|
937
1282
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
938
1283
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
939
1284
|
} catch (err) {
|
|
@@ -945,6 +1290,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
945
1290
|
var init_db_backup = __esm({
|
|
946
1291
|
"src/db-backup.ts"() {
|
|
947
1292
|
"use strict";
|
|
1293
|
+
init_sqlite_loader();
|
|
948
1294
|
DEFAULT_RETENTION = 5;
|
|
949
1295
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
950
1296
|
DbBackupError = class extends Error {
|
|
@@ -1057,9 +1403,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1057
1403
|
});
|
|
1058
1404
|
|
|
1059
1405
|
// src/memory-embedder-tokenizer.ts
|
|
1060
|
-
import { readFileSync as
|
|
1406
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1061
1407
|
function loadVocab(vocabPath) {
|
|
1062
|
-
const lines =
|
|
1408
|
+
const lines = readFileSync4(vocabPath, "utf-8").split("\n");
|
|
1063
1409
|
const vocab = /* @__PURE__ */ new Map();
|
|
1064
1410
|
for (let i = 0; i < lines.length; i++) {
|
|
1065
1411
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1179,9 +1525,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1179
1525
|
|
|
1180
1526
|
// src/memory-embedder.ts
|
|
1181
1527
|
import { fileURLToPath } from "url";
|
|
1182
|
-
import { dirname as
|
|
1183
|
-
import { existsSync as
|
|
1184
|
-
import { createRequire } from "module";
|
|
1528
|
+
import { dirname as dirname5, join as join3 } from "path";
|
|
1529
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1530
|
+
import { createRequire as createRequire2 } from "module";
|
|
1185
1531
|
function getActiveEmbedModel() {
|
|
1186
1532
|
return _activeModel;
|
|
1187
1533
|
}
|
|
@@ -1202,21 +1548,21 @@ function embeddingsDisabled() {
|
|
|
1202
1548
|
}
|
|
1203
1549
|
function resolveModelDir() {
|
|
1204
1550
|
try {
|
|
1205
|
-
let dir =
|
|
1551
|
+
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1206
1552
|
let root = null;
|
|
1207
1553
|
for (let i = 0; i < 8; i++) {
|
|
1208
|
-
if (
|
|
1554
|
+
if (existsSync5(join3(dir, "package.json"))) {
|
|
1209
1555
|
root = dir;
|
|
1210
1556
|
break;
|
|
1211
1557
|
}
|
|
1212
|
-
const parent =
|
|
1558
|
+
const parent = dirname5(dir);
|
|
1213
1559
|
if (parent === dir) break;
|
|
1214
1560
|
dir = parent;
|
|
1215
1561
|
}
|
|
1216
1562
|
if (!root) return null;
|
|
1217
1563
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1218
|
-
const candidate =
|
|
1219
|
-
if (
|
|
1564
|
+
const candidate = join3(root, rel);
|
|
1565
|
+
if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
|
|
1220
1566
|
}
|
|
1221
1567
|
return null;
|
|
1222
1568
|
} catch {
|
|
@@ -1225,9 +1571,9 @@ function resolveModelDir() {
|
|
|
1225
1571
|
}
|
|
1226
1572
|
function resolveWasmDir() {
|
|
1227
1573
|
try {
|
|
1228
|
-
const
|
|
1229
|
-
const main2 =
|
|
1230
|
-
return
|
|
1574
|
+
const req2 = createRequire2(import.meta.url);
|
|
1575
|
+
const main2 = req2.resolve(ORT_PKG);
|
|
1576
|
+
return dirname5(main2);
|
|
1231
1577
|
} catch {
|
|
1232
1578
|
return null;
|
|
1233
1579
|
}
|
|
@@ -1247,8 +1593,8 @@ async function getSession() {
|
|
|
1247
1593
|
ort.env.wasm.proxy = false;
|
|
1248
1594
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1249
1595
|
_ortTensor = ort.Tensor;
|
|
1250
|
-
const modelPath =
|
|
1251
|
-
const vocabPath =
|
|
1596
|
+
const modelPath = join3(modelDir, "model_quantized.onnx");
|
|
1597
|
+
const vocabPath = join3(modelDir, "vocab.txt");
|
|
1252
1598
|
_vocab = loadVocab(vocabPath);
|
|
1253
1599
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1254
1600
|
executionProviders: ["wasm"],
|
|
@@ -1585,9 +1931,8 @@ __export(memory_db_exports, {
|
|
|
1585
1931
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1586
1932
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1587
1933
|
});
|
|
1588
|
-
import
|
|
1589
|
-
import {
|
|
1590
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
1934
|
+
import { dirname as dirname6, basename as basename2 } from "path";
|
|
1935
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
1591
1936
|
function sanitizeFts5Query(raw) {
|
|
1592
1937
|
const trimmed = raw.trim();
|
|
1593
1938
|
if (!trimmed) return '""';
|
|
@@ -1603,12 +1948,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1603
1948
|
}
|
|
1604
1949
|
function getMemoryDb() {
|
|
1605
1950
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1606
|
-
const dir =
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
1951
|
+
const dir = dirname6(dbPath);
|
|
1952
|
+
if (!existsSync6(dir)) {
|
|
1953
|
+
mkdirSync5(dir, { recursive: true });
|
|
1609
1954
|
}
|
|
1610
|
-
const preExisting =
|
|
1611
|
-
const db =
|
|
1955
|
+
const preExisting = existsSync6(dbPath);
|
|
1956
|
+
const db = openDatabase(dbPath);
|
|
1612
1957
|
db.pragma("journal_mode = WAL");
|
|
1613
1958
|
db.pragma("foreign_keys = ON");
|
|
1614
1959
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -3314,6 +3659,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3314
3659
|
var init_memory_db = __esm({
|
|
3315
3660
|
"src/memory-db.ts"() {
|
|
3316
3661
|
"use strict";
|
|
3662
|
+
init_sqlite_loader();
|
|
3317
3663
|
init_config();
|
|
3318
3664
|
init_memory_vector();
|
|
3319
3665
|
init_db_backup();
|
|
@@ -3334,21 +3680,21 @@ var init_memory_db = __esm({
|
|
|
3334
3680
|
});
|
|
3335
3681
|
|
|
3336
3682
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3337
|
-
import { appendFileSync, mkdirSync as
|
|
3338
|
-
import { join as
|
|
3683
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
|
|
3684
|
+
import { join as join4, dirname as dirname7 } from "path";
|
|
3339
3685
|
function resolveFailureLogPath() {
|
|
3340
3686
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3341
3687
|
if (explicit) return explicit;
|
|
3342
3688
|
let dir = process.cwd();
|
|
3343
3689
|
for (let i = 0; i < 12; i++) {
|
|
3344
|
-
if (
|
|
3345
|
-
return
|
|
3690
|
+
if (existsSync7(join4(dir, ".massu")) || existsSync7(join4(dir, "massu.config.yaml"))) {
|
|
3691
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
3346
3692
|
}
|
|
3347
|
-
const parent =
|
|
3693
|
+
const parent = dirname7(dir);
|
|
3348
3694
|
if (parent === dir) break;
|
|
3349
3695
|
dir = parent;
|
|
3350
3696
|
}
|
|
3351
|
-
return
|
|
3697
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3352
3698
|
}
|
|
3353
3699
|
function recordHookFailure(hook, error, context) {
|
|
3354
3700
|
const record = {
|
|
@@ -3361,8 +3707,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
3361
3707
|
let wroteSomething = false;
|
|
3362
3708
|
try {
|
|
3363
3709
|
const path = resolveFailureLogPath();
|
|
3364
|
-
|
|
3365
|
-
|
|
3710
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
3711
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
3366
3712
|
wroteSomething = true;
|
|
3367
3713
|
} catch {
|
|
3368
3714
|
}
|
|
@@ -3407,9 +3753,9 @@ var init_hook_failure_signal = __esm({
|
|
|
3407
3753
|
// src/hooks/fix-detector.ts
|
|
3408
3754
|
init_config();
|
|
3409
3755
|
import { execFileSync } from "child_process";
|
|
3410
|
-
import { existsSync as
|
|
3756
|
+
import { existsSync as existsSync8, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync5 } from "fs";
|
|
3411
3757
|
import { tmpdir } from "os";
|
|
3412
|
-
import { join as
|
|
3758
|
+
import { join as join5 } from "path";
|
|
3413
3759
|
|
|
3414
3760
|
// src/hooks/lib/write-hook-message.ts
|
|
3415
3761
|
function writeHookMessage(message) {
|
|
@@ -3460,16 +3806,16 @@ var FIX_HEURISTICS = [
|
|
|
3460
3806
|
}
|
|
3461
3807
|
];
|
|
3462
3808
|
function getSessionFlagPath(sessionId) {
|
|
3463
|
-
const dir =
|
|
3464
|
-
if (!
|
|
3465
|
-
|
|
3809
|
+
const dir = join5(tmpdir(), "massu-auto-learning");
|
|
3810
|
+
if (!existsSync8(dir)) {
|
|
3811
|
+
mkdirSync7(dir, { recursive: true });
|
|
3466
3812
|
}
|
|
3467
|
-
return
|
|
3813
|
+
return join5(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3468
3814
|
}
|
|
3469
3815
|
function _stateDir() {
|
|
3470
|
-
const dir =
|
|
3471
|
-
if (!
|
|
3472
|
-
|
|
3816
|
+
const dir = join5(tmpdir(), "massu-fix-detector-state");
|
|
3817
|
+
if (!existsSync8(dir)) {
|
|
3818
|
+
mkdirSync7(dir, { recursive: true });
|
|
3473
3819
|
}
|
|
3474
3820
|
return dir;
|
|
3475
3821
|
}
|
|
@@ -3482,9 +3828,9 @@ function _cwdHash(cwd) {
|
|
|
3482
3828
|
}
|
|
3483
3829
|
function isGitWorkTreeFast(cwd) {
|
|
3484
3830
|
try {
|
|
3485
|
-
const cachePath =
|
|
3486
|
-
if (
|
|
3487
|
-
const cached = JSON.parse(
|
|
3831
|
+
const cachePath = join5(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
|
|
3832
|
+
if (existsSync8(cachePath)) {
|
|
3833
|
+
const cached = JSON.parse(readFileSync5(cachePath, "utf-8"));
|
|
3488
3834
|
const ageMs = Date.now() - cached.ts;
|
|
3489
3835
|
if (ageMs < 36e5) return cached.isWorkTree;
|
|
3490
3836
|
}
|
|
@@ -3496,7 +3842,7 @@ function isGitWorkTreeFast(cwd) {
|
|
|
3496
3842
|
isWorkTree = false;
|
|
3497
3843
|
}
|
|
3498
3844
|
try {
|
|
3499
|
-
|
|
3845
|
+
appendFileSync3(cachePath, "");
|
|
3500
3846
|
const fs = __require("fs");
|
|
3501
3847
|
fs.writeFileSync(cachePath, JSON.stringify({ isWorkTree, ts: Date.now() }));
|
|
3502
3848
|
} catch {
|
|
@@ -3507,11 +3853,11 @@ function isGitWorkTreeFast(cwd) {
|
|
|
3507
3853
|
}
|
|
3508
3854
|
}
|
|
3509
3855
|
function _disabledFlagPath(sessionId) {
|
|
3510
|
-
return
|
|
3856
|
+
return join5(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
|
|
3511
3857
|
}
|
|
3512
3858
|
function isSessionAutoDisabled(sessionId) {
|
|
3513
3859
|
try {
|
|
3514
|
-
return
|
|
3860
|
+
return existsSync8(_disabledFlagPath(sessionId));
|
|
3515
3861
|
} catch {
|
|
3516
3862
|
return false;
|
|
3517
3863
|
}
|
|
@@ -3529,7 +3875,7 @@ async function main() {
|
|
|
3529
3875
|
const input = await readStdin();
|
|
3530
3876
|
const hookInput = JSON.parse(input);
|
|
3531
3877
|
const filePath = hookInput.tool_input?.file_path;
|
|
3532
|
-
if (!filePath || !
|
|
3878
|
+
if (!filePath || !existsSync8(filePath)) {
|
|
3533
3879
|
process.exit(0);
|
|
3534
3880
|
return;
|
|
3535
3881
|
}
|
|
@@ -3589,8 +3935,8 @@ async function main() {
|
|
|
3589
3935
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3590
3936
|
};
|
|
3591
3937
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
3592
|
-
|
|
3593
|
-
const lines =
|
|
3938
|
+
appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
|
|
3939
|
+
const lines = readFileSync5(flagPath, "utf-8").split("\n").filter(Boolean);
|
|
3594
3940
|
if (lines.length === 1) {
|
|
3595
3941
|
writeHookMessage(
|
|
3596
3942
|
`[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
|