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/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 { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "fs/promises";
610
- import { dirname as dirname2, join as join4 } from "path";
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 SHEBANG = "#!/bin/sh";
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 isHookInstalled2(content) {
631
- return content.includes(MARK_START2);
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 stripHookSnippet2(content) {
637
- const startIdx = content.indexOf(MARK_START2);
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 upsertHookSnippet2(content) {
644
- const stripped = stripHookSnippet2(content).replace(/\s+$/, "");
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 ensureShebang(content) {
651
- if (content.startsWith("#!")) return content;
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/install-git-precommit.ts
658
- var ForeignGitHookError = class extends Error {
659
- constructor(hookPath) {
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 installGitHook(target, opts = {}) {
682
+ async function installGitHookGeneric(target, kind, opts = {}) {
679
683
  const current = await readHook(target.hookPath);
680
- const alreadyInstalled = isHookInstalled2(current);
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 new ForeignGitHookError(target.hookPath);
687
+ throw kind.createForeignError(target.hookPath);
684
688
  }
685
- const next = upsertHookSnippet2(ensureShebang(current));
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 removeGitHook(target) {
697
+ async function removeGitHookGeneric(target, kind) {
694
698
  const current = await readHook(target.hookPath);
695
- if (!isHookInstalled2(current)) return { changed: false };
696
- const stripped = stripHookSnippet2(current).trim();
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 gitHookStatus(target) {
710
+ async function gitHookStatusGeneric(target, kind) {
707
711
  const current = await readHook(target.hookPath);
708
- return { installed: isHookInstalled2(current), foreign: isForeignHook(current) };
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 join5 } from "path";
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 join5(globalWorkspaceDir(), "projects.json");
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 join6 } from "path";
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 join6(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
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) => join6(dir, e.name));
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 join7 } from "path";
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 = join7(repoRoot, relPath);
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 join8 } from "path";
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(join8(repoRoot, "go.mod"), "utf8"));
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(join8(repoRoot, path), "utf8");
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).action(
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(