nexusmem 0.10.2 → 0.10.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 +20 -0
- package/README.md +555 -550
- package/dist/cli/index.js +282 -64
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -606,13 +606,41 @@ async function hookStatus(target) {
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
// src/hooks/install-git-precommit.ts
|
|
609
|
-
import {
|
|
610
|
-
|
|
609
|
+
import { join as join4 } from "path";
|
|
610
|
+
|
|
611
|
+
// src/hooks/git-hook-snippet.ts
|
|
612
|
+
var SHEBANG = "#!/bin/sh";
|
|
613
|
+
function isHookInstalled2(content, markers) {
|
|
614
|
+
return content.includes(markers.markStart);
|
|
615
|
+
}
|
|
616
|
+
function isForeignHook(content, markers) {
|
|
617
|
+
return content.trim().length > 0 && !isHookInstalled2(content, markers);
|
|
618
|
+
}
|
|
619
|
+
function stripHookSnippet2(content, markers) {
|
|
620
|
+
const startIdx = content.indexOf(markers.markStart);
|
|
621
|
+
const endIdx = content.indexOf(markers.markEnd);
|
|
622
|
+
if (startIdx === -1 || endIdx === -1) return content;
|
|
623
|
+
const afterBlock = content.slice(endIdx + markers.markEnd.length).replace(/^\r?\n/, "");
|
|
624
|
+
return content.slice(0, startIdx) + afterBlock;
|
|
625
|
+
}
|
|
626
|
+
function upsertHookSnippet2(content, markers, renderHookSnippet4) {
|
|
627
|
+
const stripped = stripHookSnippet2(content, markers).replace(/\s+$/, "");
|
|
628
|
+
const prefix = stripped.length > 0 ? `${stripped}
|
|
629
|
+
|
|
630
|
+
` : "";
|
|
631
|
+
return `${prefix}${renderHookSnippet4()}`;
|
|
632
|
+
}
|
|
633
|
+
function ensureShebang(content) {
|
|
634
|
+
if (content.startsWith("#!")) return content;
|
|
635
|
+
return content.length > 0 ? `${SHEBANG}
|
|
636
|
+
${content}` : `${SHEBANG}
|
|
637
|
+
`;
|
|
638
|
+
}
|
|
611
639
|
|
|
612
640
|
// src/hooks/git-pre-commit.ts
|
|
613
641
|
var MARK_START2 = "# >>> nexusmem precommit hook >>>";
|
|
614
642
|
var MARK_END2 = "# <<< nexusmem precommit hook <<<";
|
|
615
|
-
var
|
|
643
|
+
var MARKERS = { markStart: MARK_START2, markEnd: MARK_END2 };
|
|
616
644
|
function renderHookSnippet2() {
|
|
617
645
|
return [
|
|
618
646
|
MARK_START2,
|
|
@@ -627,47 +655,23 @@ function renderHookSnippet2() {
|
|
|
627
655
|
""
|
|
628
656
|
].join("\n");
|
|
629
657
|
}
|
|
630
|
-
function
|
|
631
|
-
return content
|
|
632
|
-
}
|
|
633
|
-
function isForeignHook(content) {
|
|
634
|
-
return content.trim().length > 0 && !isHookInstalled2(content);
|
|
658
|
+
function isHookInstalled3(content) {
|
|
659
|
+
return isHookInstalled2(content, MARKERS);
|
|
635
660
|
}
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
const endIdx = content.indexOf(MARK_END2);
|
|
639
|
-
if (startIdx === -1 || endIdx === -1) return content;
|
|
640
|
-
const afterBlock = content.slice(endIdx + MARK_END2.length).replace(/^\r?\n/, "");
|
|
641
|
-
return content.slice(0, startIdx) + afterBlock;
|
|
661
|
+
function isForeignHook2(content) {
|
|
662
|
+
return isForeignHook(content, MARKERS);
|
|
642
663
|
}
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
const prefix = stripped.length > 0 ? `${stripped}
|
|
646
|
-
|
|
647
|
-
` : "";
|
|
648
|
-
return `${prefix}${renderHookSnippet2()}`;
|
|
664
|
+
function stripHookSnippet3(content) {
|
|
665
|
+
return stripHookSnippet2(content, MARKERS);
|
|
649
666
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
return content.length > 0 ? `${SHEBANG}
|
|
653
|
-
${content}` : `${SHEBANG}
|
|
654
|
-
`;
|
|
667
|
+
function upsertHookSnippet3(content) {
|
|
668
|
+
return upsertHookSnippet2(content, MARKERS, renderHookSnippet2);
|
|
655
669
|
}
|
|
670
|
+
var ensureShebang2 = ensureShebang;
|
|
656
671
|
|
|
657
|
-
// src/hooks/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
super(
|
|
661
|
-
`${hookPath} already has a pre-commit hook NexusMem did not install. Pass --force to append nexusmem's check to the end of it, or integrate manually.`
|
|
662
|
-
);
|
|
663
|
-
this.hookPath = hookPath;
|
|
664
|
-
this.name = "ForeignGitHookError";
|
|
665
|
-
}
|
|
666
|
-
hookPath;
|
|
667
|
-
};
|
|
668
|
-
function resolveGitHookTarget(repoRoot) {
|
|
669
|
-
return { hookPath: join4(repoRoot, ".git", "hooks", "pre-commit") };
|
|
670
|
-
}
|
|
672
|
+
// src/hooks/git-hook-install.ts
|
|
673
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
674
|
+
import { dirname as dirname2 } from "path";
|
|
671
675
|
async function readHook(path) {
|
|
672
676
|
try {
|
|
673
677
|
return await readFile3(path, "utf8");
|
|
@@ -675,14 +679,14 @@ async function readHook(path) {
|
|
|
675
679
|
return "";
|
|
676
680
|
}
|
|
677
681
|
}
|
|
678
|
-
async function
|
|
682
|
+
async function installGitHookGeneric(target, kind, opts = {}) {
|
|
679
683
|
const current = await readHook(target.hookPath);
|
|
680
|
-
const alreadyInstalled =
|
|
681
|
-
const foreign = isForeignHook(current);
|
|
684
|
+
const alreadyInstalled = kind.isHookInstalled(current);
|
|
685
|
+
const foreign = kind.isForeignHook(current);
|
|
682
686
|
if (foreign && !alreadyInstalled && !opts.force) {
|
|
683
|
-
throw
|
|
687
|
+
throw kind.createForeignError(target.hookPath);
|
|
684
688
|
}
|
|
685
|
-
const next =
|
|
689
|
+
const next = kind.upsertHookSnippet(kind.ensureShebang(current));
|
|
686
690
|
if (next === current) return { changed: false, alreadyInstalled, appendedToForeign: false };
|
|
687
691
|
await mkdir3(dirname2(target.hookPath), { recursive: true });
|
|
688
692
|
await writeFile3(target.hookPath, next, "utf8");
|
|
@@ -690,11 +694,11 @@ async function installGitHook(target, opts = {}) {
|
|
|
690
694
|
});
|
|
691
695
|
return { changed: true, alreadyInstalled, appendedToForeign: foreign && !alreadyInstalled };
|
|
692
696
|
}
|
|
693
|
-
async function
|
|
697
|
+
async function removeGitHookGeneric(target, kind) {
|
|
694
698
|
const current = await readHook(target.hookPath);
|
|
695
|
-
if (!
|
|
696
|
-
const stripped =
|
|
697
|
-
if (stripped === "" || stripped === SHEBANG) {
|
|
699
|
+
if (!kind.isHookInstalled(current)) return { changed: false };
|
|
700
|
+
const stripped = kind.stripHookSnippet(current).trim();
|
|
701
|
+
if (stripped === "" || stripped === kind.SHEBANG) {
|
|
698
702
|
await unlink(target.hookPath).catch(() => {
|
|
699
703
|
});
|
|
700
704
|
} else {
|
|
@@ -703,9 +707,114 @@ async function removeGitHook(target) {
|
|
|
703
707
|
}
|
|
704
708
|
return { changed: true };
|
|
705
709
|
}
|
|
706
|
-
async function
|
|
710
|
+
async function gitHookStatusGeneric(target, kind) {
|
|
707
711
|
const current = await readHook(target.hookPath);
|
|
708
|
-
return { installed:
|
|
712
|
+
return { installed: kind.isHookInstalled(current), foreign: kind.isForeignHook(current) };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/hooks/install-git-precommit.ts
|
|
716
|
+
var ForeignGitHookError = class extends Error {
|
|
717
|
+
constructor(hookPath) {
|
|
718
|
+
super(
|
|
719
|
+
`${hookPath} already has a pre-commit hook NexusMem did not install. Pass --force to append nexusmem's check to the end of it, or integrate manually.`
|
|
720
|
+
);
|
|
721
|
+
this.hookPath = hookPath;
|
|
722
|
+
this.name = "ForeignGitHookError";
|
|
723
|
+
}
|
|
724
|
+
hookPath;
|
|
725
|
+
};
|
|
726
|
+
var KIND = {
|
|
727
|
+
isHookInstalled: isHookInstalled3,
|
|
728
|
+
isForeignHook: isForeignHook2,
|
|
729
|
+
stripHookSnippet: stripHookSnippet3,
|
|
730
|
+
upsertHookSnippet: upsertHookSnippet3,
|
|
731
|
+
ensureShebang: ensureShebang2,
|
|
732
|
+
SHEBANG,
|
|
733
|
+
createForeignError: (hookPath) => new ForeignGitHookError(hookPath)
|
|
734
|
+
};
|
|
735
|
+
function resolveGitHookTarget(repoRoot) {
|
|
736
|
+
return { hookPath: join4(repoRoot, ".git", "hooks", "pre-commit") };
|
|
737
|
+
}
|
|
738
|
+
async function installGitHook(target, opts = {}) {
|
|
739
|
+
return installGitHookGeneric(target, KIND, opts);
|
|
740
|
+
}
|
|
741
|
+
async function removeGitHook(target) {
|
|
742
|
+
return removeGitHookGeneric(target, KIND);
|
|
743
|
+
}
|
|
744
|
+
async function gitHookStatus(target) {
|
|
745
|
+
return gitHookStatusGeneric(target, KIND);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/hooks/install-git-postcommit.ts
|
|
749
|
+
import { join as join5 } from "path";
|
|
750
|
+
|
|
751
|
+
// src/hooks/git-post-commit.ts
|
|
752
|
+
var MARK_START3 = "# >>> nexusmem postcommit hook >>>";
|
|
753
|
+
var MARK_END3 = "# <<< nexusmem postcommit hook <<<";
|
|
754
|
+
var MARKERS2 = { markStart: MARK_START3, markEnd: MARK_END3 };
|
|
755
|
+
var LOG_PATH = ".nexusmem/post-commit-sync.log";
|
|
756
|
+
var LOG_TRUNCATE_THRESHOLD = 2e3;
|
|
757
|
+
function renderHookSnippet3() {
|
|
758
|
+
return [
|
|
759
|
+
MARK_START3,
|
|
760
|
+
"# Runs a full `nexusmem sync` (including embedding) in the background after",
|
|
761
|
+
"# each commit -- detached, so this never makes `git commit` itself wait.",
|
|
762
|
+
`# Output goes to ${LOG_PATH} (reset once it passes ${LOG_TRUNCATE_THRESHOLD} lines), not the terminal.`,
|
|
763
|
+
"# Installed by: nexusmem hook git-post install",
|
|
764
|
+
"# Remove with: nexusmem hook git-post remove",
|
|
765
|
+
"if command -v nexusmem >/dev/null 2>&1; then",
|
|
766
|
+
" mkdir -p .nexusmem",
|
|
767
|
+
` nohup sh -c '[ "$(wc -l <${LOG_PATH} 2>/dev/null || echo 0)" -gt ${LOG_TRUNCATE_THRESHOLD} ] && : >${LOG_PATH}; nexusmem sync --quiet --auto >>${LOG_PATH} 2>&1' >/dev/null 2>&1 &`,
|
|
768
|
+
"fi",
|
|
769
|
+
MARK_END3,
|
|
770
|
+
""
|
|
771
|
+
].join("\n");
|
|
772
|
+
}
|
|
773
|
+
function isHookInstalled4(content) {
|
|
774
|
+
return isHookInstalled2(content, MARKERS2);
|
|
775
|
+
}
|
|
776
|
+
function isForeignHook3(content) {
|
|
777
|
+
return isForeignHook(content, MARKERS2);
|
|
778
|
+
}
|
|
779
|
+
function stripHookSnippet4(content) {
|
|
780
|
+
return stripHookSnippet2(content, MARKERS2);
|
|
781
|
+
}
|
|
782
|
+
function upsertHookSnippet4(content) {
|
|
783
|
+
return upsertHookSnippet2(content, MARKERS2, renderHookSnippet3);
|
|
784
|
+
}
|
|
785
|
+
var ensureShebang3 = ensureShebang;
|
|
786
|
+
|
|
787
|
+
// src/hooks/install-git-postcommit.ts
|
|
788
|
+
var ForeignPostCommitHookError = class extends Error {
|
|
789
|
+
constructor(hookPath) {
|
|
790
|
+
super(
|
|
791
|
+
`${hookPath} already has a post-commit hook NexusMem did not install. Pass --force to append nexusmem's sync to the end of it, or integrate manually.`
|
|
792
|
+
);
|
|
793
|
+
this.hookPath = hookPath;
|
|
794
|
+
this.name = "ForeignPostCommitHookError";
|
|
795
|
+
}
|
|
796
|
+
hookPath;
|
|
797
|
+
};
|
|
798
|
+
var KIND2 = {
|
|
799
|
+
isHookInstalled: isHookInstalled4,
|
|
800
|
+
isForeignHook: isForeignHook3,
|
|
801
|
+
stripHookSnippet: stripHookSnippet4,
|
|
802
|
+
upsertHookSnippet: upsertHookSnippet4,
|
|
803
|
+
ensureShebang: ensureShebang3,
|
|
804
|
+
SHEBANG,
|
|
805
|
+
createForeignError: (hookPath) => new ForeignPostCommitHookError(hookPath)
|
|
806
|
+
};
|
|
807
|
+
function resolvePostCommitHookTarget(repoRoot) {
|
|
808
|
+
return { hookPath: join5(repoRoot, ".git", "hooks", "post-commit") };
|
|
809
|
+
}
|
|
810
|
+
async function installPostCommitGitHook(target, opts = {}) {
|
|
811
|
+
return installGitHookGeneric(target, KIND2, opts);
|
|
812
|
+
}
|
|
813
|
+
async function removePostCommitGitHook(target) {
|
|
814
|
+
return removeGitHookGeneric(target, KIND2);
|
|
815
|
+
}
|
|
816
|
+
async function postCommitGitHookStatus(target) {
|
|
817
|
+
return gitHookStatusGeneric(target, KIND2);
|
|
709
818
|
}
|
|
710
819
|
|
|
711
820
|
// src/store/deny-list.ts
|
|
@@ -2106,7 +2215,47 @@ async function runHookGitStatus(opts) {
|
|
|
2106
2215
|
const repo = await readRepoInfo(opts.cwd);
|
|
2107
2216
|
const target = resolveGitHookTarget(repo.root);
|
|
2108
2217
|
const result = await gitHookStatus(target);
|
|
2109
|
-
const statusLabel = result.installed ? pc2.green("installed") : result.foreign ? pc2.yellow("a foreign hook exists (not nexusmem) -- install --force to append") : pc2.yellow("not installed");
|
|
2218
|
+
const statusLabel = result.installed ? pc2.green("installed") : result.foreign ? pc2.yellow("a foreign hook exists (not nexusmem) -- nexusmem hook git install --force to append") : pc2.yellow("not installed");
|
|
2219
|
+
process.stdout.write([`${pc2.dim("hook ")} ${target.hookPath}`, `${pc2.dim("status")} ${statusLabel}`, ""].join("\n"));
|
|
2220
|
+
return 0;
|
|
2221
|
+
}
|
|
2222
|
+
async function runHookGitPostInstall(opts) {
|
|
2223
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
2224
|
+
const target = resolvePostCommitHookTarget(repo.root);
|
|
2225
|
+
const result = await installPostCommitGitHook(target, { force: opts.force });
|
|
2226
|
+
const lines = [
|
|
2227
|
+
result.changed ? `${pc2.green(result.alreadyInstalled ? "updated" : "installed")} git post-commit hook` : `${pc2.dim("already up to date")}`,
|
|
2228
|
+
` hook ${target.hookPath}`
|
|
2229
|
+
];
|
|
2230
|
+
if (result.appendedToForeign) {
|
|
2231
|
+
lines.push(` ${pc2.yellow("appended after an existing post-commit hook -- review")} ${target.hookPath}`);
|
|
2232
|
+
}
|
|
2233
|
+
lines.push(
|
|
2234
|
+
"",
|
|
2235
|
+
`Runs a full ${pc2.bold("nexusmem sync")} (including embedding) in the background after each commit --`,
|
|
2236
|
+
`detached, so it never makes ${pc2.bold("git commit")} itself wait. Output goes to .nexusmem/post-commit-sync.log.`,
|
|
2237
|
+
`Run ${pc2.bold("nexusmem hook git-post remove")} to undo this.`,
|
|
2238
|
+
""
|
|
2239
|
+
);
|
|
2240
|
+
process.stdout.write(lines.join("\n"));
|
|
2241
|
+
return 0;
|
|
2242
|
+
}
|
|
2243
|
+
async function runHookGitPostRemove(opts) {
|
|
2244
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
2245
|
+
const target = resolvePostCommitHookTarget(repo.root);
|
|
2246
|
+
const result = await removePostCommitGitHook(target);
|
|
2247
|
+
process.stdout.write(
|
|
2248
|
+
result.changed ? `${pc2.green("removed")} nexusmem's block from ${target.hookPath}
|
|
2249
|
+
` : `${pc2.dim("nothing to remove")} \u2014 no nexusmem block found in ${target.hookPath}
|
|
2250
|
+
`
|
|
2251
|
+
);
|
|
2252
|
+
return 0;
|
|
2253
|
+
}
|
|
2254
|
+
async function runHookGitPostStatus(opts) {
|
|
2255
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
2256
|
+
const target = resolvePostCommitHookTarget(repo.root);
|
|
2257
|
+
const result = await postCommitGitHookStatus(target);
|
|
2258
|
+
const statusLabel = result.installed ? pc2.green("installed") : result.foreign ? pc2.yellow("a foreign hook exists (not nexusmem) -- nexusmem hook git-post install --force to append") : pc2.yellow("not installed");
|
|
2110
2259
|
process.stdout.write([`${pc2.dim("hook ")} ${target.hookPath}`, `${pc2.dim("status")} ${statusLabel}`, ""].join("\n"));
|
|
2111
2260
|
return 0;
|
|
2112
2261
|
}
|
|
@@ -2161,7 +2310,7 @@ import pc4 from "picocolors";
|
|
|
2161
2310
|
// src/config/registry.ts
|
|
2162
2311
|
import { existsSync as existsSync2 } from "fs";
|
|
2163
2312
|
import { mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile5 } from "fs/promises";
|
|
2164
|
-
import { join as
|
|
2313
|
+
import { join as join6 } from "path";
|
|
2165
2314
|
import { z as z3 } from "zod";
|
|
2166
2315
|
var ENTRY_SCHEMA = z3.object({
|
|
2167
2316
|
projectId: z3.string().min(1),
|
|
@@ -2176,7 +2325,7 @@ var REGISTRY_SCHEMA = z3.object({
|
|
|
2176
2325
|
projects: z3.array(ENTRY_SCHEMA).default([])
|
|
2177
2326
|
});
|
|
2178
2327
|
function registryPath() {
|
|
2179
|
-
return
|
|
2328
|
+
return join6(globalWorkspaceDir(), "projects.json");
|
|
2180
2329
|
}
|
|
2181
2330
|
async function readRegistry() {
|
|
2182
2331
|
let raw;
|
|
@@ -4035,18 +4184,18 @@ import { basename as basename2 } from "path";
|
|
|
4035
4184
|
import { existsSync as existsSync3 } from "fs";
|
|
4036
4185
|
import { readdir } from "fs/promises";
|
|
4037
4186
|
import { homedir as homedir3 } from "os";
|
|
4038
|
-
import { join as
|
|
4187
|
+
import { join as join7 } from "path";
|
|
4039
4188
|
function claudeProjectSlug(repoRoot) {
|
|
4040
4189
|
return repoRoot.replace(/[\\/:]/g, "-");
|
|
4041
4190
|
}
|
|
4042
4191
|
function claudeProjectTranscriptDir(repoRoot) {
|
|
4043
|
-
return
|
|
4192
|
+
return join7(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
|
|
4044
4193
|
}
|
|
4045
4194
|
async function listTranscriptFiles(repoRoot) {
|
|
4046
4195
|
const dir = claudeProjectTranscriptDir(repoRoot);
|
|
4047
4196
|
if (!existsSync3(dir)) return [];
|
|
4048
4197
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
4049
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) =>
|
|
4198
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join7(dir, e.name));
|
|
4050
4199
|
}
|
|
4051
4200
|
|
|
4052
4201
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4129,7 +4278,7 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
4129
4278
|
|
|
4130
4279
|
// src/docs/read.ts
|
|
4131
4280
|
import { readFile as readFile7, stat } from "fs/promises";
|
|
4132
|
-
import { join as
|
|
4281
|
+
import { join as join8 } from "path";
|
|
4133
4282
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
4134
4283
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
4135
4284
|
const pathspecs = opts.include ?? DEFAULT_PATHSPECS;
|
|
@@ -4142,7 +4291,7 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
4142
4291
|
const unreadable = [];
|
|
4143
4292
|
for (const relPath of paths) {
|
|
4144
4293
|
const path = relPath.replace(/\\/g, "/");
|
|
4145
|
-
const absPath =
|
|
4294
|
+
const absPath = join8(repoRoot, relPath);
|
|
4146
4295
|
let content;
|
|
4147
4296
|
let mtime;
|
|
4148
4297
|
try {
|
|
@@ -4672,7 +4821,7 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
|
|
|
4672
4821
|
|
|
4673
4822
|
// src/structure/collect.ts
|
|
4674
4823
|
import { readFile as readFile10 } from "fs/promises";
|
|
4675
|
-
import { join as
|
|
4824
|
+
import { join as join9 } from "path";
|
|
4676
4825
|
|
|
4677
4826
|
// src/structure/extract.ts
|
|
4678
4827
|
var IMPORT_PATTERNS = [
|
|
@@ -5111,7 +5260,7 @@ async function collectFileEdges(repoRoot) {
|
|
|
5111
5260
|
const trackedPaths = new Set(paths);
|
|
5112
5261
|
let goModulePath = null;
|
|
5113
5262
|
try {
|
|
5114
|
-
goModulePath = parseGoModulePath(await readFile10(
|
|
5263
|
+
goModulePath = parseGoModulePath(await readFile10(join9(repoRoot, "go.mod"), "utf8"));
|
|
5115
5264
|
} catch {
|
|
5116
5265
|
goModulePath = null;
|
|
5117
5266
|
}
|
|
@@ -5121,7 +5270,7 @@ async function collectFileEdges(repoRoot) {
|
|
|
5121
5270
|
for (const path of paths) {
|
|
5122
5271
|
let content;
|
|
5123
5272
|
try {
|
|
5124
|
-
content = await readFile10(
|
|
5273
|
+
content = await readFile10(join9(repoRoot, path), "utf8");
|
|
5125
5274
|
} catch {
|
|
5126
5275
|
unreadable.push(path);
|
|
5127
5276
|
continue;
|
|
@@ -5210,6 +5359,52 @@ ${node.body}`)
|
|
|
5210
5359
|
};
|
|
5211
5360
|
}
|
|
5212
5361
|
|
|
5362
|
+
// src/cli/sync-lock.ts
|
|
5363
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
5364
|
+
import { join as join10 } from "path";
|
|
5365
|
+
function lockPath(wsDir) {
|
|
5366
|
+
return join10(wsDir, "sync.lock");
|
|
5367
|
+
}
|
|
5368
|
+
function readOwner(path) {
|
|
5369
|
+
try {
|
|
5370
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
5371
|
+
const pid = parsed?.pid;
|
|
5372
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? parsed : null;
|
|
5373
|
+
} catch {
|
|
5374
|
+
return null;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
function isPidAlive(pid) {
|
|
5378
|
+
try {
|
|
5379
|
+
process.kill(pid, 0);
|
|
5380
|
+
return true;
|
|
5381
|
+
} catch (err) {
|
|
5382
|
+
return err.code !== "ESRCH";
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
function acquireSyncLock(wsDir) {
|
|
5386
|
+
mkdirSync2(wsDir, { recursive: true });
|
|
5387
|
+
const path = lockPath(wsDir);
|
|
5388
|
+
for (; ; ) {
|
|
5389
|
+
try {
|
|
5390
|
+
writeFileSync(path, `${JSON.stringify({ pid: process.pid })}
|
|
5391
|
+
`, { flag: "wx" });
|
|
5392
|
+
return { release: () => tryUnlink(path) };
|
|
5393
|
+
} catch (err) {
|
|
5394
|
+
if (err.code !== "EEXIST") throw err;
|
|
5395
|
+
}
|
|
5396
|
+
const owner = readOwner(path);
|
|
5397
|
+
if (owner && isPidAlive(owner.pid)) return null;
|
|
5398
|
+
tryUnlink(path);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
function tryUnlink(path) {
|
|
5402
|
+
try {
|
|
5403
|
+
unlinkSync(path);
|
|
5404
|
+
} catch {
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5213
5408
|
// src/cli/commands/sync.ts
|
|
5214
5409
|
var BATCH_SIZE = 500;
|
|
5215
5410
|
var PROGRESS_THRESHOLD = 200;
|
|
@@ -5526,6 +5721,15 @@ async function runSync(opts) {
|
|
|
5526
5721
|
`);
|
|
5527
5722
|
};
|
|
5528
5723
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5724
|
+
let lock = null;
|
|
5725
|
+
if (opts.auto) {
|
|
5726
|
+
lock = acquireSyncLock(ws.dir);
|
|
5727
|
+
if (!lock) {
|
|
5728
|
+
out(`${pc7.dim("auto-sync")} another sync is already running for this project -- skipping
|
|
5729
|
+
`);
|
|
5730
|
+
return 0;
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5529
5733
|
const store = MemoryStore.open(ws.dbPath);
|
|
5530
5734
|
const started = Date.now();
|
|
5531
5735
|
try {
|
|
@@ -5635,6 +5839,7 @@ async function runSync(opts) {
|
|
|
5635
5839
|
return 0;
|
|
5636
5840
|
} finally {
|
|
5637
5841
|
store.close();
|
|
5842
|
+
lock?.release();
|
|
5638
5843
|
}
|
|
5639
5844
|
}
|
|
5640
5845
|
|
|
@@ -6726,7 +6931,7 @@ function isExpected(err) {
|
|
|
6726
6931
|
// the user fixes, not stack traces they debug.
|
|
6727
6932
|
err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
|
|
6728
6933
|
// (antivirus, a bad install). Actionable, and not our stack to print.
|
|
6729
|
-
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError || err instanceof QueryError || err instanceof ReviewError;
|
|
6934
|
+
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof ForeignPostCommitHookError || err instanceof DenyListError || err instanceof MarkStaleError || err instanceof QueryError || err instanceof ReviewError;
|
|
6730
6935
|
}
|
|
6731
6936
|
function guard(run) {
|
|
6732
6937
|
return async () => {
|
|
@@ -6762,7 +6967,11 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6762
6967
|
"--link-failures",
|
|
6763
6968
|
"opt-in (experimental): after ingest, link failed shell commands to whatever later resolved them",
|
|
6764
6969
|
false
|
|
6765
|
-
).option("-q, --quiet", "only print the final summary", false).
|
|
6970
|
+
).option("-q, --quiet", "only print the final summary", false).option(
|
|
6971
|
+
"--auto",
|
|
6972
|
+
"used by the post-commit hook: skip (instead of running) if another --auto sync already holds this project's lock -- a manually-run sync never checks it",
|
|
6973
|
+
false
|
|
6974
|
+
).action(
|
|
6766
6975
|
(options) => guard(
|
|
6767
6976
|
() => runSync({
|
|
6768
6977
|
cwd: options.cwd,
|
|
@@ -6778,7 +6987,8 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6778
6987
|
pruneStaleShell: options.pruneStaleShell,
|
|
6779
6988
|
yes: options.yes,
|
|
6780
6989
|
linkFailures: options.linkFailures,
|
|
6781
|
-
quiet: options.quiet
|
|
6990
|
+
quiet: options.quiet,
|
|
6991
|
+
auto: options.auto
|
|
6782
6992
|
})
|
|
6783
6993
|
)()
|
|
6784
6994
|
);
|
|
@@ -6796,6 +7006,14 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
6796
7006
|
).addCommand(
|
|
6797
7007
|
new Command("status").description("Show whether the git pre-commit hook is installed").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runHookGitStatus({ cwd: options.cwd }))())
|
|
6798
7008
|
)
|
|
7009
|
+
).addCommand(
|
|
7010
|
+
new Command("git-post").description("Manage the opt-in git post-commit hook that runs a full `nexusmem sync` (with embedding) after each commit").addCommand(
|
|
7011
|
+
new Command("install").description("Install (or update) the hook in .git/hooks/post-commit").option("-C, --cwd <path>", "repository path", process.cwd()).option("--force", "append after an existing foreign post-commit hook instead of refusing", false).action((options) => guard(() => runHookGitPostInstall({ cwd: options.cwd, force: options.force }))())
|
|
7012
|
+
).addCommand(
|
|
7013
|
+
new Command("remove").description("Remove nexusmem's block from .git/hooks/post-commit").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runHookGitPostRemove({ cwd: options.cwd }))())
|
|
7014
|
+
).addCommand(
|
|
7015
|
+
new Command("status").description("Show whether the git post-commit hook is installed").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runHookGitPostStatus({ cwd: options.cwd }))())
|
|
7016
|
+
)
|
|
6799
7017
|
);
|
|
6800
7018
|
program.command("status").description("Show what is currently remembered for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).option("--share", "print a plain-text summary formatted for sharing, e.g. on X or Reddit").action((options) => guard(() => runStatus({ cwd: options.cwd, share: options.share }))());
|
|
6801
7019
|
program.command("query").description("Search remembered history and print a token-budgeted context block").argument("<text>", "free-text query").option("-C, --cwd <path>", "repository path", process.cwd()).option("-b, --budget <tokens>", "max tokens in the packed context", (v) => Number.parseInt(v, 10), 2e3).option("-n, --candidates <count>", "how many search hits to rank before packing", (v) => Number.parseInt(v, 10), 30).option("--half-life <days>", "days for a node's recency weight to halve", (v) => Number.parseFloat(v)).option("--no-vector", "BM25 only -- skip embedding the query and vector search").option("-a, --all-projects", "search every registered repository, not just this one", false).option("--as-of <date>", 'bi-temporal read: only nodes recorded at or before this date -- "what did the store hold then", not "what happened then"').option("--json", "emit the packed result as JSON on stdout", false).action(
|