nexusmem 0.10.3 → 0.10.4
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 +22 -3
- package/README.md +7 -0
- package/dist/cli/index.js +600 -160
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
5
|
import pc22 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
@@ -334,7 +334,7 @@ function toSpawnError(err, cwd, args) {
|
|
|
334
334
|
}
|
|
335
335
|
var BASE_ARGS = ["-c", "core.quotePath=false", "-c", "core.pager=", "--no-pager"];
|
|
336
336
|
var RETRY_DELAYS_MS = [50, 150, 400];
|
|
337
|
-
var realSleep = (ms) => new Promise((
|
|
337
|
+
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
338
338
|
async function* gitStream(cwd, args, opts = {}) {
|
|
339
339
|
const sleep = opts.sleep ?? realSleep;
|
|
340
340
|
for (let attempt = 0; ; attempt += 1) {
|
|
@@ -361,9 +361,9 @@ async function* runGitOnce(cwd, args, opts) {
|
|
|
361
361
|
child.stderr.on("data", (chunk2) => {
|
|
362
362
|
if (stderr.length < 64 * 1024) stderr += chunk2;
|
|
363
363
|
});
|
|
364
|
-
const exited = new Promise((
|
|
364
|
+
const exited = new Promise((resolve3, reject) => {
|
|
365
365
|
child.once("error", (err) => reject(toSpawnError(err, cwd, fullArgs)));
|
|
366
|
-
child.once("close", (code2, signal2) =>
|
|
366
|
+
child.once("close", (code2, signal2) => resolve3({ code: code2 ?? 0, signal: signal2 ?? null }));
|
|
367
367
|
});
|
|
368
368
|
exited.catch(() => {
|
|
369
369
|
});
|
|
@@ -507,16 +507,128 @@ async function resolvePowerShellProfilePath(exe = "powershell") {
|
|
|
507
507
|
return null;
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
|
+
function resolveBashProfilePath() {
|
|
511
|
+
return join3(homedir2(), ".bashrc");
|
|
512
|
+
}
|
|
513
|
+
function resolveZshProfilePath() {
|
|
514
|
+
return join3(homedir2(), ".zshrc");
|
|
515
|
+
}
|
|
510
516
|
|
|
511
|
-
// src/hooks/
|
|
517
|
+
// src/hooks/bash.ts
|
|
512
518
|
var MARK_START = "# >>> nexusmem shell hook >>>";
|
|
513
519
|
var MARK_END = "# <<< nexusmem shell hook <<<";
|
|
514
|
-
function
|
|
515
|
-
return `'${s.replace(/'/g,
|
|
520
|
+
function toBashLiteral(s) {
|
|
521
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
516
522
|
}
|
|
517
523
|
function renderHookSnippet(logPath) {
|
|
518
524
|
return [
|
|
519
525
|
MARK_START,
|
|
526
|
+
`__nxm_log_path=${toBashLiteral(logPath)}`,
|
|
527
|
+
'__nxm_cmd_start=""',
|
|
528
|
+
'__nxm_last_cmd=""',
|
|
529
|
+
'__nxm_debug_trap_owned=""',
|
|
530
|
+
"",
|
|
531
|
+
"__nxm_json_escape() {",
|
|
532
|
+
" local s=$1",
|
|
533
|
+
" s=${s//\\\\/\\\\\\\\}",
|
|
534
|
+
' s=${s//\\"/\\\\\\"}',
|
|
535
|
+
" s=${s//$'\\n'/\\\\n}",
|
|
536
|
+
" s=${s//$'\\t'/\\\\t}",
|
|
537
|
+
` printf '%s' "$s"`,
|
|
538
|
+
"}",
|
|
539
|
+
"",
|
|
540
|
+
"# Sets $__nxm_ms rather than echoing, to avoid a subshell fork on the bash 5+ fast path.",
|
|
541
|
+
"__nxm_now_ms() {",
|
|
542
|
+
' if [ -n "${EPOCHREALTIME:-}" ]; then',
|
|
543
|
+
" local es=${EPOCHREALTIME/./}",
|
|
544
|
+
" __nxm_ms=$(( es / 1000 ))",
|
|
545
|
+
" else",
|
|
546
|
+
" __nxm_ms=$(( $(date -u +%s) * 1000 ))",
|
|
547
|
+
" fi",
|
|
548
|
+
"}",
|
|
549
|
+
"",
|
|
550
|
+
// A DEBUG trap fires per simple command, so a pipeline sets __nxm_cmd_start
|
|
551
|
+
// once (first stage) and precmd clears it after logging.
|
|
552
|
+
"__nxm_preexec() {",
|
|
553
|
+
' case "$BASH_COMMAND" in __nxm_*) return ;; esac',
|
|
554
|
+
' if [ -z "$__nxm_cmd_start" ]; then',
|
|
555
|
+
" __nxm_now_ms; __nxm_cmd_start=$__nxm_ms",
|
|
556
|
+
" __nxm_last_cmd=$BASH_COMMAND",
|
|
557
|
+
" fi",
|
|
558
|
+
"}",
|
|
559
|
+
"",
|
|
560
|
+
"__nxm_precmd() {",
|
|
561
|
+
// Must be first: nothing above may run a command that would overwrite $?.
|
|
562
|
+
" local __nxm_exit=$?",
|
|
563
|
+
' [ -z "$__nxm_debug_trap_owned" ] && return',
|
|
564
|
+
' [ -z "$__nxm_last_cmd" ] && return',
|
|
565
|
+
"",
|
|
566
|
+
" local __nxm_dur=null",
|
|
567
|
+
' if [ -n "$__nxm_cmd_start" ]; then',
|
|
568
|
+
" __nxm_now_ms",
|
|
569
|
+
" __nxm_dur=$(( __nxm_ms - __nxm_cmd_start ))",
|
|
570
|
+
" fi",
|
|
571
|
+
"",
|
|
572
|
+
" local __nxm_dir",
|
|
573
|
+
' __nxm_dir=$(dirname -- "$__nxm_log_path" 2>/dev/null)',
|
|
574
|
+
' [ -d "$__nxm_dir" ] || mkdir -p "$__nxm_dir" 2>/dev/null',
|
|
575
|
+
` printf '{"ts":"%s","cwd":"%s","exitCode":%s,"durationMs":%s,"command":"%s","shell":"bash-hook"}\\n' \\`,
|
|
576
|
+
' "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" "$(__nxm_json_escape "$PWD")" "$__nxm_exit" "$__nxm_dur" \\',
|
|
577
|
+
' "$(__nxm_json_escape "$__nxm_last_cmd")" >> "$__nxm_log_path" 2>/dev/null',
|
|
578
|
+
"",
|
|
579
|
+
' __nxm_cmd_start=""',
|
|
580
|
+
' __nxm_last_cmd=""',
|
|
581
|
+
"}",
|
|
582
|
+
"",
|
|
583
|
+
"# Runs first in the chain (before any pre-existing PROMPT_COMMAND) so $? is still the real last-command status above.",
|
|
584
|
+
'case ";$PROMPT_COMMAND;" in',
|
|
585
|
+
' *";__nxm_precmd;"*) ;;',
|
|
586
|
+
' *) PROMPT_COMMAND="__nxm_precmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}" ;;',
|
|
587
|
+
"esac",
|
|
588
|
+
"",
|
|
589
|
+
// Installed last, after every other statement in this block has already
|
|
590
|
+
// run once -- otherwise sourcing this file trips our own trap on our own
|
|
591
|
+
// remaining setup statements (confirmed live: the case/esac above got
|
|
592
|
+
// logged as if it were a real command, the one time this was ordered
|
|
593
|
+
// before the trap install).
|
|
594
|
+
"# Only takes over an unset DEBUG trap -- never overwrites one another tool",
|
|
595
|
+
"# (direnv, a debugger, ...) already owns. If something else already holds",
|
|
596
|
+
"# it, precmd above logs nothing rather than log with no command text.",
|
|
597
|
+
'if [ -z "$(trap -p DEBUG)" ]; then',
|
|
598
|
+
" trap '__nxm_preexec' DEBUG",
|
|
599
|
+
" __nxm_debug_trap_owned=1",
|
|
600
|
+
"fi",
|
|
601
|
+
MARK_END,
|
|
602
|
+
""
|
|
603
|
+
].join("\n");
|
|
604
|
+
}
|
|
605
|
+
function isHookInstalled(profileContent) {
|
|
606
|
+
return profileContent.includes(MARK_START);
|
|
607
|
+
}
|
|
608
|
+
function stripHookSnippet(profileContent) {
|
|
609
|
+
const startIdx = profileContent.indexOf(MARK_START);
|
|
610
|
+
const endIdx = profileContent.indexOf(MARK_END);
|
|
611
|
+
if (startIdx === -1 || endIdx === -1) return profileContent;
|
|
612
|
+
const afterBlock = profileContent.slice(endIdx + MARK_END.length).replace(/^\r?\n/, "");
|
|
613
|
+
return profileContent.slice(0, startIdx) + afterBlock;
|
|
614
|
+
}
|
|
615
|
+
function upsertHookSnippet(profileContent, logPath) {
|
|
616
|
+
const stripped = stripHookSnippet(profileContent).replace(/\s+$/, "");
|
|
617
|
+
const prefix = stripped.length > 0 ? `${stripped}
|
|
618
|
+
|
|
619
|
+
` : "";
|
|
620
|
+
return `${prefix}${renderHookSnippet(logPath)}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/hooks/powershell.ts
|
|
624
|
+
var MARK_START2 = "# >>> nexusmem shell hook >>>";
|
|
625
|
+
var MARK_END2 = "# <<< nexusmem shell hook <<<";
|
|
626
|
+
function toPowerShellLiteral(s) {
|
|
627
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
628
|
+
}
|
|
629
|
+
function renderHookSnippet2(logPath) {
|
|
630
|
+
return [
|
|
631
|
+
MARK_START2,
|
|
520
632
|
"if (Test-Path Function:\\prompt) { $function:__ssd_original_prompt = $function:prompt }",
|
|
521
633
|
"$global:__ssd_last_history_id = -1",
|
|
522
634
|
`$global:__ssd_log_path = ${toPowerShellLiteral(logPath)}`,
|
|
@@ -544,39 +656,153 @@ function renderHookSnippet(logPath) {
|
|
|
544
656
|
" if (Test-Path Function:\\__ssd_original_prompt) { & $function:__ssd_original_prompt }",
|
|
545
657
|
` else { "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " }`,
|
|
546
658
|
"}",
|
|
547
|
-
|
|
659
|
+
MARK_END2,
|
|
548
660
|
""
|
|
549
661
|
].join("\n");
|
|
550
662
|
}
|
|
551
|
-
function
|
|
552
|
-
return profileContent.includes(
|
|
663
|
+
function isHookInstalled2(profileContent) {
|
|
664
|
+
return profileContent.includes(MARK_START2);
|
|
553
665
|
}
|
|
554
|
-
function
|
|
555
|
-
const startIdx = profileContent.indexOf(
|
|
556
|
-
const endIdx = profileContent.indexOf(
|
|
666
|
+
function stripHookSnippet2(profileContent) {
|
|
667
|
+
const startIdx = profileContent.indexOf(MARK_START2);
|
|
668
|
+
const endIdx = profileContent.indexOf(MARK_END2);
|
|
557
669
|
if (startIdx === -1 || endIdx === -1) return profileContent;
|
|
558
|
-
const afterBlock = profileContent.slice(endIdx +
|
|
670
|
+
const afterBlock = profileContent.slice(endIdx + MARK_END2.length).replace(/^\r?\n/, "");
|
|
559
671
|
return profileContent.slice(0, startIdx) + afterBlock;
|
|
560
672
|
}
|
|
561
|
-
function
|
|
562
|
-
const stripped =
|
|
673
|
+
function upsertHookSnippet2(profileContent, logPath) {
|
|
674
|
+
const stripped = stripHookSnippet2(profileContent).replace(/\s+$/, "");
|
|
563
675
|
const prefix = stripped.length > 0 ? `${stripped}
|
|
564
676
|
|
|
565
677
|
` : "";
|
|
566
|
-
return `${prefix}${
|
|
678
|
+
return `${prefix}${renderHookSnippet2(logPath)}`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/hooks/zsh.ts
|
|
682
|
+
var MARK_START3 = "# >>> nexusmem shell hook >>>";
|
|
683
|
+
var MARK_END3 = "# <<< nexusmem shell hook <<<";
|
|
684
|
+
function toZshLiteral(s) {
|
|
685
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
686
|
+
}
|
|
687
|
+
function renderHookSnippet3(logPath) {
|
|
688
|
+
return [
|
|
689
|
+
MARK_START3,
|
|
690
|
+
`__nxm_log_path=${toZshLiteral(logPath)}`,
|
|
691
|
+
'__nxm_cmd_start=""',
|
|
692
|
+
'__nxm_last_cmd=""',
|
|
693
|
+
"zmodload zsh/datetime 2>/dev/null",
|
|
694
|
+
"",
|
|
695
|
+
"__nxm_json_escape() {",
|
|
696
|
+
" local s=$1",
|
|
697
|
+
" s=${s//\\\\/\\\\\\\\}",
|
|
698
|
+
' s=${s//\\"/\\\\\\"}',
|
|
699
|
+
" s=${s//$'\\n'/\\\\n}",
|
|
700
|
+
" s=${s//$'\\t'/\\\\t}",
|
|
701
|
+
` printf '%s' "$s"`,
|
|
702
|
+
"}",
|
|
703
|
+
"",
|
|
704
|
+
"__nxm_preexec() {",
|
|
705
|
+
" __nxm_last_cmd=$1",
|
|
706
|
+
' if [ -n "${EPOCHREALTIME:-}" ]; then',
|
|
707
|
+
" __nxm_cmd_start=$(printf '%.0f' $(( EPOCHREALTIME * 1000 )))",
|
|
708
|
+
" else",
|
|
709
|
+
" __nxm_cmd_start=$(( $(date -u +%s) * 1000 ))",
|
|
710
|
+
" fi",
|
|
711
|
+
"}",
|
|
712
|
+
"",
|
|
713
|
+
"__nxm_precmd() {",
|
|
714
|
+
// Must be first: nothing above may run a command that would overwrite $?.
|
|
715
|
+
" local __nxm_exit=$?",
|
|
716
|
+
' [ -z "$__nxm_last_cmd" ] && return',
|
|
717
|
+
"",
|
|
718
|
+
" local __nxm_dur=null",
|
|
719
|
+
' if [ -n "$__nxm_cmd_start" ]; then',
|
|
720
|
+
" local __nxm_now",
|
|
721
|
+
' if [ -n "${EPOCHREALTIME:-}" ]; then',
|
|
722
|
+
" __nxm_now=$(printf '%.0f' $(( EPOCHREALTIME * 1000 )))",
|
|
723
|
+
" else",
|
|
724
|
+
" __nxm_now=$(( $(date -u +%s) * 1000 ))",
|
|
725
|
+
" fi",
|
|
726
|
+
" __nxm_dur=$(( __nxm_now - __nxm_cmd_start ))",
|
|
727
|
+
" fi",
|
|
728
|
+
"",
|
|
729
|
+
" local __nxm_dir",
|
|
730
|
+
' __nxm_dir=$(dirname -- "$__nxm_log_path" 2>/dev/null)',
|
|
731
|
+
' [ -d "$__nxm_dir" ] || mkdir -p "$__nxm_dir" 2>/dev/null',
|
|
732
|
+
` printf '{"ts":"%s","cwd":"%s","exitCode":%s,"durationMs":%s,"command":"%s","shell":"zsh-hook"}\\n' \\`,
|
|
733
|
+
' "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" "$(__nxm_json_escape "$PWD")" "$__nxm_exit" "$__nxm_dur" \\',
|
|
734
|
+
' "$(__nxm_json_escape "$__nxm_last_cmd")" >> "$__nxm_log_path" 2>/dev/null',
|
|
735
|
+
"",
|
|
736
|
+
' __nxm_cmd_start=""',
|
|
737
|
+
' __nxm_last_cmd=""',
|
|
738
|
+
"}",
|
|
739
|
+
"",
|
|
740
|
+
// Membership check (not just append) so re-sourcing this file doesn't register twice.
|
|
741
|
+
"typeset -ga preexec_functions precmd_functions",
|
|
742
|
+
'(( ${preexec_functions[(Ie)__nxm_preexec]} )) || preexec_functions=(__nxm_preexec "${preexec_functions[@]}")',
|
|
743
|
+
'(( ${precmd_functions[(Ie)__nxm_precmd]} )) || precmd_functions=(__nxm_precmd "${precmd_functions[@]}")',
|
|
744
|
+
MARK_END3,
|
|
745
|
+
""
|
|
746
|
+
].join("\n");
|
|
747
|
+
}
|
|
748
|
+
function isHookInstalled3(profileContent) {
|
|
749
|
+
return profileContent.includes(MARK_START3);
|
|
750
|
+
}
|
|
751
|
+
function stripHookSnippet3(profileContent) {
|
|
752
|
+
const startIdx = profileContent.indexOf(MARK_START3);
|
|
753
|
+
const endIdx = profileContent.indexOf(MARK_END3);
|
|
754
|
+
if (startIdx === -1 || endIdx === -1) return profileContent;
|
|
755
|
+
const afterBlock = profileContent.slice(endIdx + MARK_END3.length).replace(/^\r?\n/, "");
|
|
756
|
+
return profileContent.slice(0, startIdx) + afterBlock;
|
|
757
|
+
}
|
|
758
|
+
function upsertHookSnippet3(profileContent, logPath) {
|
|
759
|
+
const stripped = stripHookSnippet3(profileContent).replace(/\s+$/, "");
|
|
760
|
+
const prefix = stripped.length > 0 ? `${stripped}
|
|
761
|
+
|
|
762
|
+
` : "";
|
|
763
|
+
return `${prefix}${renderHookSnippet3(logPath)}`;
|
|
567
764
|
}
|
|
568
765
|
|
|
569
766
|
// src/hooks/install.ts
|
|
767
|
+
var SHELL_MODULES = {
|
|
768
|
+
pwsh: {
|
|
769
|
+
isHookInstalled: isHookInstalled2,
|
|
770
|
+
stripHookSnippet: stripHookSnippet2,
|
|
771
|
+
upsertHookSnippet: upsertHookSnippet2,
|
|
772
|
+
resolveProfilePath: async (override) => override ?? await resolvePowerShellProfilePath()
|
|
773
|
+
},
|
|
774
|
+
bash: {
|
|
775
|
+
isHookInstalled,
|
|
776
|
+
stripHookSnippet,
|
|
777
|
+
upsertHookSnippet,
|
|
778
|
+
resolveProfilePath: async (override) => override ?? resolveBashProfilePath()
|
|
779
|
+
},
|
|
780
|
+
zsh: {
|
|
781
|
+
isHookInstalled: isHookInstalled3,
|
|
782
|
+
stripHookSnippet: stripHookSnippet3,
|
|
783
|
+
upsertHookSnippet: upsertHookSnippet3,
|
|
784
|
+
resolveProfilePath: async (override) => override ?? resolveZshProfilePath()
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
function detectShellKind() {
|
|
788
|
+
if (process.platform === "win32") return "pwsh";
|
|
789
|
+
return (process.env.SHELL ?? "").includes("zsh") ? "zsh" : "bash";
|
|
790
|
+
}
|
|
570
791
|
var ProfileNotFoundError = class extends Error {
|
|
571
|
-
constructor() {
|
|
572
|
-
super(
|
|
792
|
+
constructor(shell) {
|
|
793
|
+
super(
|
|
794
|
+
shell === "pwsh" ? "Could not resolve a PowerShell profile path (tried `powershell -Command $PROFILE`). Pass --profile explicitly." : `Could not resolve a ${shell} profile path. Pass --profile explicitly.`
|
|
795
|
+
);
|
|
796
|
+
this.shell = shell;
|
|
573
797
|
this.name = "ProfileNotFoundError";
|
|
574
798
|
}
|
|
799
|
+
shell;
|
|
575
800
|
};
|
|
576
|
-
async function resolveHookTarget(profileOverride, logPathOverride) {
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
801
|
+
async function resolveHookTarget(shellOverride, profileOverride, logPathOverride) {
|
|
802
|
+
const shell = shellOverride ?? detectShellKind();
|
|
803
|
+
const profilePath = await SHELL_MODULES[shell].resolveProfilePath(profileOverride);
|
|
804
|
+
if (!profilePath) throw new ProfileNotFoundError(shell);
|
|
805
|
+
return { shell, profilePath, logPath: logPathOverride ?? hookLogPath() };
|
|
580
806
|
}
|
|
581
807
|
async function readProfile(path) {
|
|
582
808
|
try {
|
|
@@ -586,49 +812,52 @@ async function readProfile(path) {
|
|
|
586
812
|
}
|
|
587
813
|
}
|
|
588
814
|
async function installHook(target) {
|
|
815
|
+
const mod = SHELL_MODULES[target.shell];
|
|
589
816
|
const current = await readProfile(target.profilePath);
|
|
590
|
-
const alreadyInstalled = isHookInstalled(current);
|
|
591
|
-
const next = upsertHookSnippet(current, target.logPath);
|
|
817
|
+
const alreadyInstalled = mod.isHookInstalled(current);
|
|
818
|
+
const next = mod.upsertHookSnippet(current, target.logPath);
|
|
592
819
|
if (next === current) return { changed: false, alreadyInstalled };
|
|
593
820
|
await mkdir2(dirname(target.profilePath), { recursive: true });
|
|
594
821
|
await writeFile2(target.profilePath, next, "utf8");
|
|
595
822
|
return { changed: true, alreadyInstalled };
|
|
596
823
|
}
|
|
597
824
|
async function removeHook(target) {
|
|
825
|
+
const mod = SHELL_MODULES[target.shell];
|
|
598
826
|
const current = await readProfile(target.profilePath);
|
|
599
|
-
if (!isHookInstalled(current)) return { changed: false };
|
|
600
|
-
await writeFile2(target.profilePath, stripHookSnippet(current), "utf8");
|
|
827
|
+
if (!mod.isHookInstalled(current)) return { changed: false };
|
|
828
|
+
await writeFile2(target.profilePath, mod.stripHookSnippet(current), "utf8");
|
|
601
829
|
return { changed: true };
|
|
602
830
|
}
|
|
603
831
|
async function hookStatus(target) {
|
|
832
|
+
const mod = SHELL_MODULES[target.shell];
|
|
604
833
|
const current = await readProfile(target.profilePath);
|
|
605
|
-
return { installed: isHookInstalled(current) };
|
|
834
|
+
return { installed: mod.isHookInstalled(current) };
|
|
606
835
|
}
|
|
607
836
|
|
|
608
837
|
// src/hooks/install-git-precommit.ts
|
|
609
|
-
import { join as
|
|
838
|
+
import { join as join5 } from "path";
|
|
610
839
|
|
|
611
840
|
// src/hooks/git-hook-snippet.ts
|
|
612
841
|
var SHEBANG = "#!/bin/sh";
|
|
613
|
-
function
|
|
842
|
+
function isHookInstalled4(content, markers) {
|
|
614
843
|
return content.includes(markers.markStart);
|
|
615
844
|
}
|
|
616
845
|
function isForeignHook(content, markers) {
|
|
617
|
-
return content.trim().length > 0 && !
|
|
846
|
+
return content.trim().length > 0 && !isHookInstalled4(content, markers);
|
|
618
847
|
}
|
|
619
|
-
function
|
|
848
|
+
function stripHookSnippet4(content, markers) {
|
|
620
849
|
const startIdx = content.indexOf(markers.markStart);
|
|
621
850
|
const endIdx = content.indexOf(markers.markEnd);
|
|
622
851
|
if (startIdx === -1 || endIdx === -1) return content;
|
|
623
852
|
const afterBlock = content.slice(endIdx + markers.markEnd.length).replace(/^\r?\n/, "");
|
|
624
853
|
return content.slice(0, startIdx) + afterBlock;
|
|
625
854
|
}
|
|
626
|
-
function
|
|
627
|
-
const stripped =
|
|
855
|
+
function upsertHookSnippet4(content, markers, renderHookSnippet6) {
|
|
856
|
+
const stripped = stripHookSnippet4(content, markers).replace(/\s+$/, "");
|
|
628
857
|
const prefix = stripped.length > 0 ? `${stripped}
|
|
629
858
|
|
|
630
859
|
` : "";
|
|
631
|
-
return `${prefix}${
|
|
860
|
+
return `${prefix}${renderHookSnippet6()}`;
|
|
632
861
|
}
|
|
633
862
|
function ensureShebang(content) {
|
|
634
863
|
if (content.startsWith("#!")) return content;
|
|
@@ -638,12 +867,12 @@ ${content}` : `${SHEBANG}
|
|
|
638
867
|
}
|
|
639
868
|
|
|
640
869
|
// src/hooks/git-pre-commit.ts
|
|
641
|
-
var
|
|
642
|
-
var
|
|
643
|
-
var MARKERS = { markStart:
|
|
644
|
-
function
|
|
870
|
+
var MARK_START4 = "# >>> nexusmem precommit hook >>>";
|
|
871
|
+
var MARK_END4 = "# <<< nexusmem precommit hook <<<";
|
|
872
|
+
var MARKERS = { markStart: MARK_START4, markEnd: MARK_END4 };
|
|
873
|
+
function renderHookSnippet4() {
|
|
645
874
|
return [
|
|
646
|
-
|
|
875
|
+
MARK_START4,
|
|
647
876
|
"# Runs `nexusmem precheck` before each commit -- advisory only, never",
|
|
648
877
|
"# blocks a commit on its own (this hook does not pass --strict).",
|
|
649
878
|
"# Installed by: nexusmem hook git install",
|
|
@@ -651,24 +880,34 @@ function renderHookSnippet2() {
|
|
|
651
880
|
"if command -v nexusmem >/dev/null 2>&1; then",
|
|
652
881
|
" nexusmem precheck",
|
|
653
882
|
"fi",
|
|
654
|
-
|
|
883
|
+
MARK_END4,
|
|
655
884
|
""
|
|
656
885
|
].join("\n");
|
|
657
886
|
}
|
|
658
|
-
function
|
|
659
|
-
return
|
|
887
|
+
function isHookInstalled5(content) {
|
|
888
|
+
return isHookInstalled4(content, MARKERS);
|
|
660
889
|
}
|
|
661
890
|
function isForeignHook2(content) {
|
|
662
891
|
return isForeignHook(content, MARKERS);
|
|
663
892
|
}
|
|
664
|
-
function
|
|
665
|
-
return
|
|
893
|
+
function stripHookSnippet5(content) {
|
|
894
|
+
return stripHookSnippet4(content, MARKERS);
|
|
666
895
|
}
|
|
667
|
-
function
|
|
668
|
-
return
|
|
896
|
+
function upsertHookSnippet5(content) {
|
|
897
|
+
return upsertHookSnippet4(content, MARKERS, renderHookSnippet4);
|
|
669
898
|
}
|
|
670
899
|
var ensureShebang2 = ensureShebang;
|
|
671
900
|
|
|
901
|
+
// src/hooks/git-hooks-dir.ts
|
|
902
|
+
import { isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
903
|
+
async function resolveHooksDir(repoRoot) {
|
|
904
|
+
const raw = await gitOrNull(repoRoot, ["config", "--path", "core.hooksPath"]);
|
|
905
|
+
const hooksPathConfig = raw?.trim() || null;
|
|
906
|
+
if (!hooksPathConfig) return { dir: join4(repoRoot, ".git", "hooks"), hooksPathConfig: null };
|
|
907
|
+
const dir = isAbsolute(hooksPathConfig) ? hooksPathConfig : resolve2(repoRoot, hooksPathConfig);
|
|
908
|
+
return { dir, hooksPathConfig };
|
|
909
|
+
}
|
|
910
|
+
|
|
672
911
|
// src/hooks/git-hook-install.ts
|
|
673
912
|
import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
674
913
|
import { dirname as dirname2 } from "path";
|
|
@@ -724,16 +963,17 @@ var ForeignGitHookError = class extends Error {
|
|
|
724
963
|
hookPath;
|
|
725
964
|
};
|
|
726
965
|
var KIND = {
|
|
727
|
-
isHookInstalled:
|
|
966
|
+
isHookInstalled: isHookInstalled5,
|
|
728
967
|
isForeignHook: isForeignHook2,
|
|
729
|
-
stripHookSnippet:
|
|
730
|
-
upsertHookSnippet:
|
|
968
|
+
stripHookSnippet: stripHookSnippet5,
|
|
969
|
+
upsertHookSnippet: upsertHookSnippet5,
|
|
731
970
|
ensureShebang: ensureShebang2,
|
|
732
971
|
SHEBANG,
|
|
733
972
|
createForeignError: (hookPath) => new ForeignGitHookError(hookPath)
|
|
734
973
|
};
|
|
735
|
-
function resolveGitHookTarget(repoRoot) {
|
|
736
|
-
|
|
974
|
+
async function resolveGitHookTarget(repoRoot) {
|
|
975
|
+
const { dir, hooksPathConfig } = await resolveHooksDir(repoRoot);
|
|
976
|
+
return { hookPath: join5(dir, "pre-commit"), hooksPathConfig };
|
|
737
977
|
}
|
|
738
978
|
async function installGitHook(target, opts = {}) {
|
|
739
979
|
return installGitHookGeneric(target, KIND, opts);
|
|
@@ -746,41 +986,67 @@ async function gitHookStatus(target) {
|
|
|
746
986
|
}
|
|
747
987
|
|
|
748
988
|
// src/hooks/install-git-postcommit.ts
|
|
749
|
-
import { join as
|
|
989
|
+
import { join as join6 } from "path";
|
|
750
990
|
|
|
751
991
|
// src/hooks/git-post-commit.ts
|
|
752
|
-
var
|
|
753
|
-
var
|
|
754
|
-
var MARKERS2 = { markStart:
|
|
992
|
+
var MARK_START5 = "# >>> nexusmem postcommit hook >>>";
|
|
993
|
+
var MARK_END5 = "# <<< nexusmem postcommit hook <<<";
|
|
994
|
+
var MARKERS2 = { markStart: MARK_START5, markEnd: MARK_END5 };
|
|
755
995
|
var LOG_PATH = ".nexusmem/post-commit-sync.log";
|
|
756
996
|
var LOG_TRUNCATE_THRESHOLD = 2e3;
|
|
757
|
-
|
|
997
|
+
var STATE_PATH = ".nexusmem/post-commit-sync-state.json";
|
|
998
|
+
function renderHookSnippet5() {
|
|
758
999
|
return [
|
|
759
|
-
|
|
1000
|
+
MARK_START5,
|
|
760
1001
|
"# Runs a full `nexusmem sync` (including embedding) in the background after",
|
|
761
1002
|
"# each commit -- detached, so this never makes `git commit` itself wait.",
|
|
762
1003
|
`# Output goes to ${LOG_PATH} (reset once it passes ${LOG_TRUNCATE_THRESHOLD} lines), not the terminal.`,
|
|
1004
|
+
`# Last-run outcome is also written to ${STATE_PATH} (\`nexusmem status\` reads it).`,
|
|
763
1005
|
"# Installed by: nexusmem hook git-post install",
|
|
764
1006
|
"# Remove with: nexusmem hook git-post remove",
|
|
765
1007
|
"if command -v nexusmem >/dev/null 2>&1; then",
|
|
766
1008
|
" mkdir -p .nexusmem",
|
|
767
|
-
|
|
1009
|
+
" nohup sh -c '",
|
|
1010
|
+
` [ "$(wc -l <${LOG_PATH} 2>/dev/null || echo 0)" -gt ${LOG_TRUNCATE_THRESHOLD} ] && : >${LOG_PATH}`,
|
|
1011
|
+
` nexusmem sync --quiet --auto >>${LOG_PATH} 2>&1`,
|
|
1012
|
+
" ec=$?",
|
|
1013
|
+
' ok=$([ "$ec" -eq 0 ] && echo true || echo false)',
|
|
1014
|
+
// Atomic rename, not truncate-in-place like the log above: this file is
|
|
1015
|
+
// wholesale-replaced each run (no concurrent-appender fd to protect), so
|
|
1016
|
+
// a rename avoids a reader ever seeing a half-written, unparseable JSON file.
|
|
1017
|
+
// Double-quoted, not single: this whole block is already inside the
|
|
1018
|
+
// outer nohup sh -c '...' single-quoted argument below, and single
|
|
1019
|
+
// quotes cannot nest -- one here would close that argument early.
|
|
1020
|
+
` printf "{\\"ts\\":\\"%s\\",\\"ok\\":%s,\\"exitCode\\":%s}\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$ok" "$ec" >${STATE_PATH}.tmp && mv ${STATE_PATH}.tmp ${STATE_PATH}`,
|
|
1021
|
+
" ' >/dev/null 2>&1 &",
|
|
768
1022
|
"fi",
|
|
769
|
-
|
|
1023
|
+
MARK_END5,
|
|
770
1024
|
""
|
|
771
1025
|
].join("\n");
|
|
772
1026
|
}
|
|
773
|
-
function
|
|
774
|
-
|
|
1027
|
+
function parsePostCommitSyncState(raw) {
|
|
1028
|
+
let obj;
|
|
1029
|
+
try {
|
|
1030
|
+
obj = JSON.parse(raw);
|
|
1031
|
+
} catch {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof obj !== "object" || obj === null) return null;
|
|
1035
|
+
const o = obj;
|
|
1036
|
+
if (typeof o.ts !== "string" || typeof o.ok !== "boolean" || typeof o.exitCode !== "number") return null;
|
|
1037
|
+
return { ts: o.ts, ok: o.ok, exitCode: o.exitCode };
|
|
1038
|
+
}
|
|
1039
|
+
function isHookInstalled6(content) {
|
|
1040
|
+
return isHookInstalled4(content, MARKERS2);
|
|
775
1041
|
}
|
|
776
1042
|
function isForeignHook3(content) {
|
|
777
1043
|
return isForeignHook(content, MARKERS2);
|
|
778
1044
|
}
|
|
779
|
-
function
|
|
780
|
-
return
|
|
1045
|
+
function stripHookSnippet6(content) {
|
|
1046
|
+
return stripHookSnippet4(content, MARKERS2);
|
|
781
1047
|
}
|
|
782
|
-
function
|
|
783
|
-
return
|
|
1048
|
+
function upsertHookSnippet6(content) {
|
|
1049
|
+
return upsertHookSnippet4(content, MARKERS2, renderHookSnippet5);
|
|
784
1050
|
}
|
|
785
1051
|
var ensureShebang3 = ensureShebang;
|
|
786
1052
|
|
|
@@ -796,16 +1062,17 @@ var ForeignPostCommitHookError = class extends Error {
|
|
|
796
1062
|
hookPath;
|
|
797
1063
|
};
|
|
798
1064
|
var KIND2 = {
|
|
799
|
-
isHookInstalled:
|
|
1065
|
+
isHookInstalled: isHookInstalled6,
|
|
800
1066
|
isForeignHook: isForeignHook3,
|
|
801
|
-
stripHookSnippet:
|
|
802
|
-
upsertHookSnippet:
|
|
1067
|
+
stripHookSnippet: stripHookSnippet6,
|
|
1068
|
+
upsertHookSnippet: upsertHookSnippet6,
|
|
803
1069
|
ensureShebang: ensureShebang3,
|
|
804
1070
|
SHEBANG,
|
|
805
1071
|
createForeignError: (hookPath) => new ForeignPostCommitHookError(hookPath)
|
|
806
1072
|
};
|
|
807
|
-
function resolvePostCommitHookTarget(repoRoot) {
|
|
808
|
-
|
|
1073
|
+
async function resolvePostCommitHookTarget(repoRoot) {
|
|
1074
|
+
const { dir, hooksPathConfig } = await resolveHooksDir(repoRoot);
|
|
1075
|
+
return { hookPath: join6(dir, "post-commit"), hooksPathConfig };
|
|
809
1076
|
}
|
|
810
1077
|
async function installPostCommitGitHook(target, opts = {}) {
|
|
811
1078
|
return installGitHookGeneric(target, KIND2, opts);
|
|
@@ -1146,6 +1413,17 @@ var V12 = `
|
|
|
1146
1413
|
ALTER TABLE nodes ADD COLUMN retrieved_count INTEGER NOT NULL DEFAULT 0;
|
|
1147
1414
|
ALTER TABLE nodes ADD COLUMN last_retrieved_at INTEGER;
|
|
1148
1415
|
`;
|
|
1416
|
+
var V13 = `
|
|
1417
|
+
ALTER TABLE nodes ADD COLUMN capture_mode TEXT NOT NULL DEFAULT 'unknown';
|
|
1418
|
+
ALTER TABLE nodes ADD COLUMN source_ts TEXT;
|
|
1419
|
+
|
|
1420
|
+
UPDATE nodes
|
|
1421
|
+
SET source_ts = CASE
|
|
1422
|
+
WHEN kind = 'doc_section' THEN NULL
|
|
1423
|
+
WHEN kind = 'shell_command' AND json_extract(meta, '$.tsApprox') = 1 THEN NULL
|
|
1424
|
+
ELSE ts
|
|
1425
|
+
END;
|
|
1426
|
+
`;
|
|
1149
1427
|
var MIGRATIONS = [
|
|
1150
1428
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
1151
1429
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -1158,7 +1436,8 @@ var MIGRATIONS = [
|
|
|
1158
1436
|
{ version: 9, up: (db) => db.exec(V9) },
|
|
1159
1437
|
{ version: 10, up: (db) => db.exec(V10) },
|
|
1160
1438
|
{ version: 11, up: (db) => db.exec(V11) },
|
|
1161
|
-
{ version: 12, up: (db) => db.exec(V12) }
|
|
1439
|
+
{ version: 12, up: (db) => db.exec(V12) },
|
|
1440
|
+
{ version: 13, up: (db) => db.exec(V13) }
|
|
1162
1441
|
];
|
|
1163
1442
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
1164
1443
|
function currentSchemaVersion(db) {
|
|
@@ -1268,15 +1547,16 @@ function epochOf(ts) {
|
|
|
1268
1547
|
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
1269
1548
|
}
|
|
1270
1549
|
function upsertNodes(db, nodes) {
|
|
1271
|
-
const exists = db.prepare("SELECT body, signal, title FROM nodes WHERE id = ?");
|
|
1550
|
+
const exists = db.prepare("SELECT body, signal, title, capture_mode AS captureMode, source_ts AS sourceTs FROM nodes WHERE id = ?");
|
|
1551
|
+
const projectCreatedAt = db.prepare("SELECT created_at AS createdAt FROM projects WHERE id = ?");
|
|
1272
1552
|
const dropStaleEmbedding = db.prepare("DELETE FROM nodes_vec WHERE rowid = (SELECT rowid FROM nodes WHERE id = ?)");
|
|
1273
1553
|
const insertNode = db.prepare(
|
|
1274
|
-
`INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, provenance, supersedes, created_at)
|
|
1275
|
-
VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @provenance, @supersedes, @now)
|
|
1554
|
+
`INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, provenance, supersedes, created_at, capture_mode, source_ts)
|
|
1555
|
+
VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @provenance, @supersedes, @now, @captureMode, @sourceTs)
|
|
1276
1556
|
ON CONFLICT(id) DO UPDATE SET
|
|
1277
1557
|
ts = excluded.ts, ts_epoch = excluded.ts_epoch, source = excluded.source,
|
|
1278
1558
|
title = excluded.title, body = excluded.body, signal = excluded.signal, meta = excluded.meta,
|
|
1279
|
-
provenance = excluded.provenance`
|
|
1559
|
+
provenance = excluded.provenance, capture_mode = excluded.capture_mode, source_ts = excluded.source_ts`
|
|
1280
1560
|
);
|
|
1281
1561
|
const clearFiles = db.prepare("DELETE FROM node_files WHERE node_id = ?");
|
|
1282
1562
|
const insertFile = db.prepare(
|
|
@@ -1288,6 +1568,7 @@ function upsertNodes(db, nodes) {
|
|
|
1288
1568
|
);
|
|
1289
1569
|
const stats2 = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
|
|
1290
1570
|
const denyEntriesByProject = /* @__PURE__ */ new Map();
|
|
1571
|
+
const createdAtByProject = /* @__PURE__ */ new Map();
|
|
1291
1572
|
const run = db.transaction((batch) => {
|
|
1292
1573
|
const now = Date.now();
|
|
1293
1574
|
for (const node of batch) {
|
|
@@ -1300,14 +1581,28 @@ function upsertNodes(db, nodes) {
|
|
|
1300
1581
|
stats2.denied += 1;
|
|
1301
1582
|
continue;
|
|
1302
1583
|
}
|
|
1584
|
+
let initializedAt = createdAtByProject.get(node.projectId);
|
|
1585
|
+
if (initializedAt === void 0) {
|
|
1586
|
+
initializedAt = projectCreatedAt.get(node.projectId)?.createdAt ?? null;
|
|
1587
|
+
createdAtByProject.set(node.projectId, initializedAt);
|
|
1588
|
+
}
|
|
1589
|
+
const sourceTs = node.sourceTs === void 0 ? node.ts : node.sourceTs;
|
|
1590
|
+
const sourceEpoch = sourceTs === null ? Number.NaN : Date.parse(sourceTs);
|
|
1591
|
+
let captureMode = "unknown";
|
|
1592
|
+
if (sourceTs !== null && !Number.isNaN(sourceEpoch)) {
|
|
1593
|
+
if (node.captureMode === "backfilled") captureMode = "backfilled";
|
|
1594
|
+
else if (node.captureMode === "unknown") captureMode = "unknown";
|
|
1595
|
+
else if (initializedAt !== null && sourceEpoch < initializedAt) captureMode = "backfilled";
|
|
1596
|
+
else if (initializedAt !== null && sourceEpoch <= now) captureMode = "observed";
|
|
1597
|
+
}
|
|
1303
1598
|
const prior = exists.get(node.id);
|
|
1304
1599
|
if (prior) {
|
|
1305
|
-
if (prior.body === node.body && prior.signal === node.signal && prior.title === node.title) {
|
|
1600
|
+
if (prior.body === node.body && prior.signal === node.signal && prior.title === node.title && prior.captureMode === captureMode && prior.sourceTs === sourceTs) {
|
|
1306
1601
|
stats2.unchanged += 1;
|
|
1307
1602
|
continue;
|
|
1308
1603
|
}
|
|
1309
1604
|
stats2.updated += 1;
|
|
1310
|
-
dropStaleEmbedding.run(node.id);
|
|
1605
|
+
if (prior.body !== node.body || prior.title !== node.title) dropStaleEmbedding.run(node.id);
|
|
1311
1606
|
} else {
|
|
1312
1607
|
stats2.inserted += 1;
|
|
1313
1608
|
}
|
|
@@ -1323,6 +1618,8 @@ function upsertNodes(db, nodes) {
|
|
|
1323
1618
|
signal: node.signal,
|
|
1324
1619
|
meta: JSON.stringify(node.meta),
|
|
1325
1620
|
provenance: node.provenance ?? defaultProvenanceForKind(node.kind),
|
|
1621
|
+
captureMode,
|
|
1622
|
+
sourceTs,
|
|
1326
1623
|
supersedes: node.supersedes ?? null,
|
|
1327
1624
|
now
|
|
1328
1625
|
});
|
|
@@ -1360,13 +1657,14 @@ function clearProject(db, projectId) {
|
|
|
1360
1657
|
function getNodesByIds(db, ids) {
|
|
1361
1658
|
if (ids.length === 0) return [];
|
|
1362
1659
|
return db.prepare(
|
|
1363
|
-
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance,
|
|
1660
|
+
`SELECT id, kind, project_id AS projectId, ts, source_ts AS sourceTs, title, body, signal, provenance,
|
|
1661
|
+
capture_mode AS captureMode, trust_state AS trustState
|
|
1364
1662
|
FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
|
|
1365
1663
|
).all(JSON.stringify(ids));
|
|
1366
1664
|
}
|
|
1367
1665
|
function listRecentNodes(db, projectId, limit = 20) {
|
|
1368
1666
|
return db.prepare(
|
|
1369
|
-
`SELECT id, kind, ts, source, title, signal, provenance
|
|
1667
|
+
`SELECT id, kind, ts, source_ts AS sourceTs, source, title, signal, provenance, capture_mode AS captureMode
|
|
1370
1668
|
FROM nodes
|
|
1371
1669
|
WHERE project_id = ?
|
|
1372
1670
|
ORDER BY ts_epoch DESC
|
|
@@ -1645,7 +1943,8 @@ function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
|
1645
1943
|
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1646
1944
|
const k = asOfEpoch === null ? limit : Math.max(limit * 8, 50);
|
|
1647
1945
|
return db.prepare(
|
|
1648
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
|
|
1946
|
+
`SELECT n.id, n.kind, n.ts, n.source_ts AS sourceTs, n.title, n.body, n.signal, n.provenance,
|
|
1947
|
+
n.capture_mode AS captureMode, n.trust_state AS trustState, v.distance AS distance
|
|
1649
1948
|
FROM nodes_vec v
|
|
1650
1949
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1651
1950
|
WHERE v.embedding MATCH ? AND k = ? AND v.project_id = ?
|
|
@@ -1676,7 +1975,8 @@ function search(db, projectId, query, limit = 20, opts = {}) {
|
|
|
1676
1975
|
if (!match) return [];
|
|
1677
1976
|
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1678
1977
|
const rows = db.prepare(
|
|
1679
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
|
|
1978
|
+
`SELECT n.id, n.kind, n.ts, n.source_ts AS sourceTs, n.title, n.body, n.signal, n.provenance,
|
|
1979
|
+
n.capture_mode AS captureMode, n.trust_state AS trustState,
|
|
1680
1980
|
bm25(nodes_fts, 10.0, 1.0) AS rank
|
|
1681
1981
|
FROM nodes_fts
|
|
1682
1982
|
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
@@ -2180,14 +2480,18 @@ async function runForget(opts) {
|
|
|
2180
2480
|
|
|
2181
2481
|
// src/cli/commands/hook-git.ts
|
|
2182
2482
|
import pc2 from "picocolors";
|
|
2483
|
+
function hooksPathNote(hooksPathConfig) {
|
|
2484
|
+
return ` ${pc2.dim(`core.hooksPath=${hooksPathConfig}`)}`;
|
|
2485
|
+
}
|
|
2183
2486
|
async function runHookGitInstall(opts) {
|
|
2184
2487
|
const repo = await readRepoInfo(opts.cwd);
|
|
2185
|
-
const target = resolveGitHookTarget(repo.root);
|
|
2488
|
+
const target = await resolveGitHookTarget(repo.root);
|
|
2186
2489
|
const result = await installGitHook(target, { force: opts.force });
|
|
2187
2490
|
const lines = [
|
|
2188
2491
|
result.changed ? `${pc2.green(result.alreadyInstalled ? "updated" : "installed")} git pre-commit hook` : `${pc2.dim("already up to date")}`,
|
|
2189
2492
|
` hook ${target.hookPath}`
|
|
2190
2493
|
];
|
|
2494
|
+
if (target.hooksPathConfig) lines.push(hooksPathNote(target.hooksPathConfig));
|
|
2191
2495
|
if (result.appendedToForeign) {
|
|
2192
2496
|
lines.push(` ${pc2.yellow("appended after an existing pre-commit hook -- review")} ${target.hookPath}`);
|
|
2193
2497
|
}
|
|
@@ -2202,7 +2506,7 @@ async function runHookGitInstall(opts) {
|
|
|
2202
2506
|
}
|
|
2203
2507
|
async function runHookGitRemove(opts) {
|
|
2204
2508
|
const repo = await readRepoInfo(opts.cwd);
|
|
2205
|
-
const target = resolveGitHookTarget(repo.root);
|
|
2509
|
+
const target = await resolveGitHookTarget(repo.root);
|
|
2206
2510
|
const result = await removeGitHook(target);
|
|
2207
2511
|
process.stdout.write(
|
|
2208
2512
|
result.changed ? `${pc2.green("removed")} nexusmem's block from ${target.hookPath}
|
|
@@ -2213,20 +2517,28 @@ async function runHookGitRemove(opts) {
|
|
|
2213
2517
|
}
|
|
2214
2518
|
async function runHookGitStatus(opts) {
|
|
2215
2519
|
const repo = await readRepoInfo(opts.cwd);
|
|
2216
|
-
const target = resolveGitHookTarget(repo.root);
|
|
2520
|
+
const target = await resolveGitHookTarget(repo.root);
|
|
2217
2521
|
const result = await gitHookStatus(target);
|
|
2218
2522
|
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(
|
|
2523
|
+
process.stdout.write(
|
|
2524
|
+
[
|
|
2525
|
+
`${pc2.dim("hook ")} ${target.hookPath}`,
|
|
2526
|
+
`${pc2.dim("status")} ${statusLabel}`,
|
|
2527
|
+
target.hooksPathConfig ? hooksPathNote(target.hooksPathConfig) : "",
|
|
2528
|
+
""
|
|
2529
|
+
].filter((line) => line !== "").concat("").join("\n")
|
|
2530
|
+
);
|
|
2220
2531
|
return 0;
|
|
2221
2532
|
}
|
|
2222
2533
|
async function runHookGitPostInstall(opts) {
|
|
2223
2534
|
const repo = await readRepoInfo(opts.cwd);
|
|
2224
|
-
const target = resolvePostCommitHookTarget(repo.root);
|
|
2535
|
+
const target = await resolvePostCommitHookTarget(repo.root);
|
|
2225
2536
|
const result = await installPostCommitGitHook(target, { force: opts.force });
|
|
2226
2537
|
const lines = [
|
|
2227
2538
|
result.changed ? `${pc2.green(result.alreadyInstalled ? "updated" : "installed")} git post-commit hook` : `${pc2.dim("already up to date")}`,
|
|
2228
2539
|
` hook ${target.hookPath}`
|
|
2229
2540
|
];
|
|
2541
|
+
if (target.hooksPathConfig) lines.push(hooksPathNote(target.hooksPathConfig));
|
|
2230
2542
|
if (result.appendedToForeign) {
|
|
2231
2543
|
lines.push(` ${pc2.yellow("appended after an existing post-commit hook -- review")} ${target.hookPath}`);
|
|
2232
2544
|
}
|
|
@@ -2242,7 +2554,7 @@ async function runHookGitPostInstall(opts) {
|
|
|
2242
2554
|
}
|
|
2243
2555
|
async function runHookGitPostRemove(opts) {
|
|
2244
2556
|
const repo = await readRepoInfo(opts.cwd);
|
|
2245
|
-
const target = resolvePostCommitHookTarget(repo.root);
|
|
2557
|
+
const target = await resolvePostCommitHookTarget(repo.root);
|
|
2246
2558
|
const result = await removePostCommitGitHook(target);
|
|
2247
2559
|
process.stdout.write(
|
|
2248
2560
|
result.changed ? `${pc2.green("removed")} nexusmem's block from ${target.hookPath}
|
|
@@ -2253,26 +2565,43 @@ async function runHookGitPostRemove(opts) {
|
|
|
2253
2565
|
}
|
|
2254
2566
|
async function runHookGitPostStatus(opts) {
|
|
2255
2567
|
const repo = await readRepoInfo(opts.cwd);
|
|
2256
|
-
const target = resolvePostCommitHookTarget(repo.root);
|
|
2568
|
+
const target = await resolvePostCommitHookTarget(repo.root);
|
|
2257
2569
|
const result = await postCommitGitHookStatus(target);
|
|
2258
2570
|
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");
|
|
2259
|
-
process.stdout.write(
|
|
2571
|
+
process.stdout.write(
|
|
2572
|
+
[
|
|
2573
|
+
`${pc2.dim("hook ")} ${target.hookPath}`,
|
|
2574
|
+
`${pc2.dim("status")} ${statusLabel}`,
|
|
2575
|
+
target.hooksPathConfig ? hooksPathNote(target.hooksPathConfig) : "",
|
|
2576
|
+
""
|
|
2577
|
+
].filter((line) => line !== "").concat("").join("\n")
|
|
2578
|
+
);
|
|
2260
2579
|
return 0;
|
|
2261
2580
|
}
|
|
2262
2581
|
|
|
2263
2582
|
// src/cli/commands/hook.ts
|
|
2264
2583
|
import pc3 from "picocolors";
|
|
2584
|
+
function shellCaveats(shell) {
|
|
2585
|
+
if (shell !== "bash") return [];
|
|
2586
|
+
return [
|
|
2587
|
+
`Note: on macOS, Terminal.app opens login shells, which read ~/.bash_profile, not ~/.bashrc --`,
|
|
2588
|
+
`if commands aren't being logged there, source this file from your .bash_profile.`,
|
|
2589
|
+
`Note: if another tool already owns this shell's DEBUG trap, commands won't be logged at all`,
|
|
2590
|
+
`(rather than logged with the wrong exit code) -- check \`nexusmem hook status\` after installing.`
|
|
2591
|
+
];
|
|
2592
|
+
}
|
|
2265
2593
|
async function runHookInstall(opts) {
|
|
2266
|
-
const target = await resolveHookTarget(opts.profile, opts.logPath);
|
|
2594
|
+
const target = await resolveHookTarget(opts.shell, opts.profile, opts.logPath);
|
|
2267
2595
|
const result = await installHook(target);
|
|
2268
2596
|
process.stdout.write(
|
|
2269
2597
|
[
|
|
2270
|
-
result.changed ? `${pc3.green(result.alreadyInstalled ? "updated" : "installed")} shell hook` : `${pc3.dim("already up to date")}`,
|
|
2598
|
+
result.changed ? `${pc3.green(result.alreadyInstalled ? "updated" : "installed")} shell hook (${target.shell})` : `${pc3.dim("already up to date")}`,
|
|
2271
2599
|
` profile ${target.profilePath}`,
|
|
2272
2600
|
` log ${target.logPath}`,
|
|
2273
2601
|
"",
|
|
2274
|
-
`New commands in any
|
|
2275
|
-
`Open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect.`,
|
|
2602
|
+
`New commands in any ${target.shell} session using this profile will now log their timestamp, cwd and exit code.`,
|
|
2603
|
+
target.shell === "pwsh" ? `Open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect.` : `Open a new shell (or run \`. ${target.profilePath}\`) for it to take effect.`,
|
|
2604
|
+
...shellCaveats(target.shell),
|
|
2276
2605
|
`Run ${pc3.bold("nexusmem hook remove")} to undo this.`,
|
|
2277
2606
|
""
|
|
2278
2607
|
].join("\n")
|
|
@@ -2280,7 +2609,7 @@ async function runHookInstall(opts) {
|
|
|
2280
2609
|
return 0;
|
|
2281
2610
|
}
|
|
2282
2611
|
async function runHookRemove(opts) {
|
|
2283
|
-
const target = await resolveHookTarget(opts.profile, opts.logPath);
|
|
2612
|
+
const target = await resolveHookTarget(opts.shell, opts.profile, opts.logPath);
|
|
2284
2613
|
const result = await removeHook(target);
|
|
2285
2614
|
process.stdout.write(
|
|
2286
2615
|
result.changed ? `${pc3.green("removed")} shell hook from ${target.profilePath}
|
|
@@ -2290,10 +2619,11 @@ async function runHookRemove(opts) {
|
|
|
2290
2619
|
return 0;
|
|
2291
2620
|
}
|
|
2292
2621
|
async function runHookStatus(opts) {
|
|
2293
|
-
const target = await resolveHookTarget(opts.profile, opts.logPath);
|
|
2622
|
+
const target = await resolveHookTarget(opts.shell, opts.profile, opts.logPath);
|
|
2294
2623
|
const result = await hookStatus(target);
|
|
2295
2624
|
process.stdout.write(
|
|
2296
2625
|
[
|
|
2626
|
+
`${pc3.dim("shell ")} ${target.shell}`,
|
|
2297
2627
|
`${pc3.dim("profile")} ${target.profilePath}`,
|
|
2298
2628
|
`${pc3.dim("log ")} ${target.logPath}`,
|
|
2299
2629
|
`${pc3.dim("status ")} ${result.installed ? pc3.green("installed") : pc3.yellow("not installed")}`,
|
|
@@ -2310,7 +2640,7 @@ import pc4 from "picocolors";
|
|
|
2310
2640
|
// src/config/registry.ts
|
|
2311
2641
|
import { existsSync as existsSync2 } from "fs";
|
|
2312
2642
|
import { mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile5 } from "fs/promises";
|
|
2313
|
-
import { join as
|
|
2643
|
+
import { join as join7 } from "path";
|
|
2314
2644
|
import { z as z3 } from "zod";
|
|
2315
2645
|
var ENTRY_SCHEMA = z3.object({
|
|
2316
2646
|
projectId: z3.string().min(1),
|
|
@@ -2325,7 +2655,7 @@ var REGISTRY_SCHEMA = z3.object({
|
|
|
2325
2655
|
projects: z3.array(ENTRY_SCHEMA).default([])
|
|
2326
2656
|
});
|
|
2327
2657
|
function registryPath() {
|
|
2328
|
-
return
|
|
2658
|
+
return join7(globalWorkspaceDir(), "projects.json");
|
|
2329
2659
|
}
|
|
2330
2660
|
async function readRegistry() {
|
|
2331
2661
|
let raw;
|
|
@@ -2419,10 +2749,10 @@ async function runInit(opts) {
|
|
|
2419
2749
|
const result = await installHook(target);
|
|
2420
2750
|
lines.push(
|
|
2421
2751
|
"",
|
|
2422
|
-
`${pc4.green(result.changed ? "installed" : "already installed")} shell hook`,
|
|
2752
|
+
`${pc4.green(result.changed ? "installed" : "already installed")} shell hook (${target.shell})`,
|
|
2423
2753
|
` profile ${target.profilePath}`,
|
|
2424
2754
|
` log ${target.logPath}`,
|
|
2425
|
-
` open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect`
|
|
2755
|
+
target.shell === "pwsh" ? ` open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect` : ` open a new shell (or run \`. ${target.profilePath}\`) for it to take effect`
|
|
2426
2756
|
);
|
|
2427
2757
|
} catch (err) {
|
|
2428
2758
|
if (err instanceof ProfileNotFoundError) {
|
|
@@ -2550,6 +2880,13 @@ function approxTokens(text) {
|
|
|
2550
2880
|
// src/retrieval/pack.ts
|
|
2551
2881
|
var DEFAULT_SUMMARY_CHARS = 320;
|
|
2552
2882
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
2883
|
+
function renderedLabels(provenance, captureMode, trustState, project) {
|
|
2884
|
+
const labels = [`[${provenance}]`];
|
|
2885
|
+
if (captureMode !== "unknown") labels.push(`[capture:${captureMode}]`);
|
|
2886
|
+
if (trustState !== "candidate") labels.push(`[${trustState}]`);
|
|
2887
|
+
if (project) labels.push(`[${project}]`);
|
|
2888
|
+
return labels.join(" ");
|
|
2889
|
+
}
|
|
2553
2890
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
2554
2891
|
var MAX_PER_FAMILY = 2;
|
|
2555
2892
|
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section", "code_diff"]);
|
|
@@ -2680,7 +3017,8 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
2680
3017
|
continue;
|
|
2681
3018
|
}
|
|
2682
3019
|
const summary = summarize(hit, summaryChars, query);
|
|
2683
|
-
const
|
|
3020
|
+
const captureMode = hit.captureMode ?? "unknown";
|
|
3021
|
+
const tokens = approxTokens(hit.title) + approxTokens(summary) + approxTokens(renderedLabels(hit.provenance, captureMode, hit.trustState, hit.project)) + NODE_OVERHEAD_TOKENS;
|
|
2684
3022
|
if (tokensUsed + tokens > tokensBudget) {
|
|
2685
3023
|
droppedForBudget += 1;
|
|
2686
3024
|
continue;
|
|
@@ -2689,12 +3027,14 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
2689
3027
|
id: hit.id,
|
|
2690
3028
|
kind: hit.kind,
|
|
2691
3029
|
ts: hit.ts,
|
|
3030
|
+
sourceTs: hit.sourceTs === void 0 ? hit.ts : hit.sourceTs,
|
|
2692
3031
|
title: hit.title,
|
|
2693
3032
|
signal: hit.signal,
|
|
2694
3033
|
score: hit.score,
|
|
2695
3034
|
summary,
|
|
2696
3035
|
tokens,
|
|
2697
3036
|
provenance: hit.provenance,
|
|
3037
|
+
captureMode,
|
|
2698
3038
|
trustState: hit.trustState,
|
|
2699
3039
|
...hit.project ? { project: hit.project } : {}
|
|
2700
3040
|
});
|
|
@@ -2707,10 +3047,9 @@ function renderContextBlock(query, result) {
|
|
|
2707
3047
|
if (result.nodes.length === 0) return `No remembered context matched "${query}".`;
|
|
2708
3048
|
const lines = [`Relevant history for: ${query}`, ""];
|
|
2709
3049
|
for (const node of result.nodes) {
|
|
2710
|
-
const
|
|
2711
|
-
const
|
|
2712
|
-
|
|
2713
|
-
lines.push(`- ${node.ts.slice(0, 10)} ${provenance}${trust}${project}${node.title}`);
|
|
3050
|
+
const labels = renderedLabels(node.provenance, node.captureMode, node.trustState, node.project);
|
|
3051
|
+
const date = node.sourceTs === null ? "date unknown" : node.sourceTs.slice(0, 10);
|
|
3052
|
+
lines.push(`- ${date} ${labels} ${node.title}`);
|
|
2714
3053
|
if (node.summary && node.summary !== node.title) {
|
|
2715
3054
|
if (node.kind === "code_diff") {
|
|
2716
3055
|
for (const line of node.summary.split("\n")) lines.push(` ${line}`);
|
|
@@ -2755,13 +3094,17 @@ function correlateFailures(store, projectId, opts = {}) {
|
|
|
2755
3094
|
`SELECT id, ts_epoch, json_extract(meta, '$.command') AS command, json_extract(meta, '$.cwd') AS cwd
|
|
2756
3095
|
FROM nodes
|
|
2757
3096
|
WHERE project_id = ? AND kind = 'shell_command'
|
|
3097
|
+
AND source_ts IS NOT NULL
|
|
2758
3098
|
AND json_extract(meta, '$.exitCode') IS NOT NULL
|
|
2759
|
-
AND json_extract(meta, '$.exitCode') != 0
|
|
3099
|
+
AND json_extract(meta, '$.exitCode') != 0
|
|
3100
|
+
AND json_extract(meta, '$.cwd') IS NOT NULL`
|
|
2760
3101
|
).all(projectId);
|
|
2761
3102
|
const findRetry = db.prepare(
|
|
2762
3103
|
`SELECT id FROM nodes
|
|
2763
3104
|
WHERE project_id = ? AND kind = 'shell_command'
|
|
3105
|
+
AND source_ts IS NOT NULL
|
|
2764
3106
|
AND json_extract(meta, '$.exitCode') = 0
|
|
3107
|
+
AND json_extract(meta, '$.cwd') IS NOT NULL
|
|
2765
3108
|
AND ts_epoch > ? AND ts_epoch <= ?
|
|
2766
3109
|
AND lower(trim(json_extract(meta, '$.command'))) = ?
|
|
2767
3110
|
AND (json_extract(meta, '$.cwd') IS ? OR json_extract(meta, '$.cwd') = ?)
|
|
@@ -2844,10 +3187,12 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
|
|
|
2844
3187
|
id: hit.id,
|
|
2845
3188
|
kind: hit.kind,
|
|
2846
3189
|
ts: hit.ts,
|
|
3190
|
+
sourceTs: hit.sourceTs === void 0 ? hit.ts : hit.sourceTs,
|
|
2847
3191
|
title: hit.title,
|
|
2848
3192
|
body: hit.body,
|
|
2849
3193
|
signal: hit.signal,
|
|
2850
3194
|
provenance: hit.provenance,
|
|
3195
|
+
captureMode: hit.captureMode ?? "unknown",
|
|
2851
3196
|
trustState: hit.trustState,
|
|
2852
3197
|
rank: 0
|
|
2853
3198
|
});
|
|
@@ -2943,6 +3288,8 @@ function pullLinkedResolutions(resolveStore, ranked) {
|
|
|
2943
3288
|
body: resolution.body,
|
|
2944
3289
|
signal: resolution.signal,
|
|
2945
3290
|
provenance: resolution.provenance,
|
|
3291
|
+
captureMode: resolution.captureMode ?? "unknown",
|
|
3292
|
+
sourceTs: resolution.sourceTs,
|
|
2946
3293
|
trustState: resolution.trustState,
|
|
2947
3294
|
rank: 0,
|
|
2948
3295
|
// no bm25/vector rank of its own -- never read again past this point
|
|
@@ -3825,6 +4172,7 @@ function toMemoryNodes3(file, projectId, opts = {}) {
|
|
|
3825
4172
|
kind: "doc_section",
|
|
3826
4173
|
projectId,
|
|
3827
4174
|
ts: file.ts,
|
|
4175
|
+
sourceTs: null,
|
|
3828
4176
|
source: "docs",
|
|
3829
4177
|
title: sectionTitle(file.path, chunk2.heading, index, chunks.length),
|
|
3830
4178
|
body: truncate(chunk2.text, maxBody),
|
|
@@ -4155,7 +4503,8 @@ function toMemoryNode3(entry, projectId, opts = {}) {
|
|
|
4155
4503
|
id: makeNodeId(projectId, "shell_command", entry.naturalKey),
|
|
4156
4504
|
kind: "shell_command",
|
|
4157
4505
|
projectId,
|
|
4158
|
-
ts: entry.ts,
|
|
4506
|
+
ts: entry.ts ?? opts.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4507
|
+
sourceTs: entry.ts,
|
|
4159
4508
|
source: `shell:${entry.shell}`,
|
|
4160
4509
|
title: truncate(titleLine, MAX_TITLE_CHARS7),
|
|
4161
4510
|
body: renderBody2(entry, redactedCommand, maxBody),
|
|
@@ -4168,12 +4517,23 @@ function toMemoryNode3(entry, projectId, opts = {}) {
|
|
|
4168
4517
|
exitCode: entry.exitCode,
|
|
4169
4518
|
durationMs: entry.durationMs,
|
|
4170
4519
|
tsApprox: entry.tsApprox,
|
|
4520
|
+
sourceTimestamp: entry.ts,
|
|
4171
4521
|
shell: entry.shell
|
|
4172
4522
|
}
|
|
4173
4523
|
};
|
|
4174
4524
|
}
|
|
4175
4525
|
function collectShellHistory(entries, projectId, opts = {}) {
|
|
4176
|
-
|
|
4526
|
+
const recordedAt = opts.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4527
|
+
const parsedBaseEpoch = Date.parse(recordedAt);
|
|
4528
|
+
const baseEpoch = Number.isNaN(parsedBaseEpoch) ? Date.now() : parsedBaseEpoch;
|
|
4529
|
+
return entries.map(
|
|
4530
|
+
(entry, index) => toMemoryNode3(entry, projectId, {
|
|
4531
|
+
...opts,
|
|
4532
|
+
// This is record-time ordering only. The source timestamp stays null,
|
|
4533
|
+
// while increasing milliseconds preserve the history file's order.
|
|
4534
|
+
recordedAt: new Date(baseEpoch + index).toISOString()
|
|
4535
|
+
})
|
|
4536
|
+
);
|
|
4177
4537
|
}
|
|
4178
4538
|
|
|
4179
4539
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4184,18 +4544,18 @@ import { basename as basename2 } from "path";
|
|
|
4184
4544
|
import { existsSync as existsSync3 } from "fs";
|
|
4185
4545
|
import { readdir } from "fs/promises";
|
|
4186
4546
|
import { homedir as homedir3 } from "os";
|
|
4187
|
-
import { join as
|
|
4547
|
+
import { join as join8 } from "path";
|
|
4188
4548
|
function claudeProjectSlug(repoRoot) {
|
|
4189
4549
|
return repoRoot.replace(/[\\/:]/g, "-");
|
|
4190
4550
|
}
|
|
4191
4551
|
function claudeProjectTranscriptDir(repoRoot) {
|
|
4192
|
-
return
|
|
4552
|
+
return join8(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
|
|
4193
4553
|
}
|
|
4194
4554
|
async function listTranscriptFiles(repoRoot) {
|
|
4195
4555
|
const dir = claudeProjectTranscriptDir(repoRoot);
|
|
4196
4556
|
if (!existsSync3(dir)) return [];
|
|
4197
4557
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
4198
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) =>
|
|
4558
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join8(dir, e.name));
|
|
4199
4559
|
}
|
|
4200
4560
|
|
|
4201
4561
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4278,7 +4638,7 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
4278
4638
|
|
|
4279
4639
|
// src/docs/read.ts
|
|
4280
4640
|
import { readFile as readFile7, stat } from "fs/promises";
|
|
4281
|
-
import { join as
|
|
4641
|
+
import { join as join9 } from "path";
|
|
4282
4642
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
4283
4643
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
4284
4644
|
const pathspecs = opts.include ?? DEFAULT_PATHSPECS;
|
|
@@ -4291,7 +4651,7 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
4291
4651
|
const unreadable = [];
|
|
4292
4652
|
for (const relPath of paths) {
|
|
4293
4653
|
const path = relPath.replace(/\\/g, "/");
|
|
4294
|
-
const absPath =
|
|
4654
|
+
const absPath = join9(repoRoot, relPath);
|
|
4295
4655
|
let content;
|
|
4296
4656
|
let mtime;
|
|
4297
4657
|
try {
|
|
@@ -4509,6 +4869,7 @@ import { readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
|
4509
4869
|
// src/shell/hook-log.ts
|
|
4510
4870
|
import { appendFile, mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
|
|
4511
4871
|
import { dirname as dirname4 } from "path";
|
|
4872
|
+
var HOOK_SHELL_KINDS = /* @__PURE__ */ new Set(["pwsh-hook", "bash-hook", "zsh-hook"]);
|
|
4512
4873
|
function parseHookLogLine(line) {
|
|
4513
4874
|
const trimmed = line.trim();
|
|
4514
4875
|
if (!trimmed) return null;
|
|
@@ -4526,7 +4887,8 @@ function parseHookLogLine(line) {
|
|
|
4526
4887
|
cwd: o.cwd,
|
|
4527
4888
|
exitCode: typeof o.exitCode === "number" ? o.exitCode : null,
|
|
4528
4889
|
durationMs: typeof o.durationMs === "number" ? o.durationMs : null,
|
|
4529
|
-
command: o.command
|
|
4890
|
+
command: o.command,
|
|
4891
|
+
shell: typeof o.shell === "string" && HOOK_SHELL_KINDS.has(o.shell) ? o.shell : void 0
|
|
4530
4892
|
};
|
|
4531
4893
|
}
|
|
4532
4894
|
async function readHookLog(path, fromLine) {
|
|
@@ -4534,17 +4896,20 @@ async function readHookLog(path, fromLine) {
|
|
|
4534
4896
|
try {
|
|
4535
4897
|
raw = await readFile8(path, "utf8");
|
|
4536
4898
|
} catch {
|
|
4537
|
-
return { entries: [], totalLines: fromLine };
|
|
4899
|
+
return { entries: [], totalLines: fromLine, shellsSeen: /* @__PURE__ */ new Set() };
|
|
4538
4900
|
}
|
|
4539
4901
|
const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
|
|
4540
|
-
const
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4902
|
+
const allParsed = lines.map(parseHookLogLine);
|
|
4903
|
+
const shellsSeen = /* @__PURE__ */ new Set();
|
|
4904
|
+
for (const e of allParsed) if (e) shellsSeen.add(e.shell ?? "pwsh-hook");
|
|
4905
|
+
const sliceStart = fromLine > 0 && fromLine <= lines.length ? fromLine : 0;
|
|
4906
|
+
const entries = allParsed.slice(sliceStart).filter((e) => e !== null);
|
|
4907
|
+
return { entries, totalLines: lines.length, shellsSeen };
|
|
4543
4908
|
}
|
|
4544
4909
|
|
|
4545
4910
|
// src/shell/parse-bash.ts
|
|
4546
4911
|
var EPOCH_COMMENT = /^#(\d{9,10})$/;
|
|
4547
|
-
function parseBashHistory(raw,
|
|
4912
|
+
function parseBashHistory(raw, _mtimeMs, opts = {}) {
|
|
4548
4913
|
const lines = raw.split(/\r?\n/);
|
|
4549
4914
|
const prelim = [];
|
|
4550
4915
|
let pendingEpoch = null;
|
|
@@ -4561,12 +4926,11 @@ function parseBashHistory(raw, mtimeMs, opts = {}) {
|
|
|
4561
4926
|
const tail = opts.tailLines ? prelim.slice(-opts.tailLines) : prelim;
|
|
4562
4927
|
const startIndex = prelim.length - tail.length;
|
|
4563
4928
|
return tail.map((p, i) => {
|
|
4564
|
-
const fromEnd = tail.length - 1 - i;
|
|
4565
4929
|
const approx = p.ts === null;
|
|
4566
4930
|
return {
|
|
4567
4931
|
naturalKey: `bash:${startIndex + i}:${sha256Hex(p.command).slice(0, 12)}`,
|
|
4568
4932
|
command: p.command,
|
|
4569
|
-
ts: p.ts
|
|
4933
|
+
ts: p.ts,
|
|
4570
4934
|
tsApprox: approx,
|
|
4571
4935
|
exitCode: null,
|
|
4572
4936
|
cwd: null,
|
|
@@ -4577,16 +4941,15 @@ function parseBashHistory(raw, mtimeMs, opts = {}) {
|
|
|
4577
4941
|
}
|
|
4578
4942
|
|
|
4579
4943
|
// src/shell/parse-psreadline.ts
|
|
4580
|
-
function parsePsReadLineHistory(raw,
|
|
4944
|
+
function parsePsReadLineHistory(raw, _mtimeMs, opts = {}) {
|
|
4581
4945
|
const allLines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
4582
4946
|
const tail = opts.tailLines ? allLines.slice(-opts.tailLines) : allLines;
|
|
4583
4947
|
const startIndex = allLines.length - tail.length;
|
|
4584
4948
|
return tail.map((command, i) => {
|
|
4585
|
-
const fromEnd = tail.length - 1 - i;
|
|
4586
4949
|
return {
|
|
4587
4950
|
naturalKey: `pwsh:${startIndex + i}:${sha256Hex(command).slice(0, 12)}`,
|
|
4588
4951
|
command,
|
|
4589
|
-
ts:
|
|
4952
|
+
ts: null,
|
|
4590
4953
|
tsApprox: true,
|
|
4591
4954
|
exitCode: null,
|
|
4592
4955
|
cwd: null,
|
|
@@ -4598,7 +4961,7 @@ function parsePsReadLineHistory(raw, mtimeMs, opts = {}) {
|
|
|
4598
4961
|
|
|
4599
4962
|
// src/shell/parse-zsh.ts
|
|
4600
4963
|
var EXTENDED_PREFIX = /^: (\d+):(\d+);(.*)$/;
|
|
4601
|
-
function parseZshHistory(raw,
|
|
4964
|
+
function parseZshHistory(raw, _mtimeMs, opts = {}) {
|
|
4602
4965
|
const rawLines = raw.split(/\r?\n/);
|
|
4603
4966
|
const prelim = [];
|
|
4604
4967
|
let i = 0;
|
|
@@ -4630,12 +4993,11 @@ ${rawLines[i]}`;
|
|
|
4630
4993
|
const tail = opts.tailLines ? prelim.slice(-opts.tailLines) : prelim;
|
|
4631
4994
|
const startIndex = prelim.length - tail.length;
|
|
4632
4995
|
return tail.map((p, idx) => {
|
|
4633
|
-
const fromEnd = tail.length - 1 - idx;
|
|
4634
4996
|
const approx = p.ts === null;
|
|
4635
4997
|
return {
|
|
4636
4998
|
naturalKey: `zsh:${startIndex + idx}:${sha256Hex(p.command).slice(0, 12)}`,
|
|
4637
4999
|
command: p.command,
|
|
4638
|
-
ts: p.ts
|
|
5000
|
+
ts: p.ts,
|
|
4639
5001
|
tsApprox: approx,
|
|
4640
5002
|
exitCode: null,
|
|
4641
5003
|
cwd: null,
|
|
@@ -4653,15 +5015,16 @@ function isUnderRoot(cwd, root) {
|
|
|
4653
5015
|
return c === r || c.startsWith(`${r}/`);
|
|
4654
5016
|
}
|
|
4655
5017
|
function hookEntryToRaw(e) {
|
|
5018
|
+
const shell = e.shell ?? "pwsh-hook";
|
|
4656
5019
|
return {
|
|
4657
|
-
naturalKey:
|
|
5020
|
+
naturalKey: `${shell}:${e.ts}:${sha256Hex(e.command).slice(0, 12)}`,
|
|
4658
5021
|
command: e.command,
|
|
4659
5022
|
ts: e.ts,
|
|
4660
5023
|
tsApprox: false,
|
|
4661
5024
|
exitCode: e.exitCode,
|
|
4662
5025
|
cwd: e.cwd,
|
|
4663
5026
|
durationMs: e.durationMs,
|
|
4664
|
-
shell
|
|
5027
|
+
shell
|
|
4665
5028
|
};
|
|
4666
5029
|
}
|
|
4667
5030
|
async function tryReadScrapeSource(path, parse, tailLines) {
|
|
@@ -4675,21 +5038,29 @@ async function collectAvailableShellHistory(opts = {}) {
|
|
|
4675
5038
|
const preferHook = opts.preferHook ?? true;
|
|
4676
5039
|
const hookPath = hookLogPath();
|
|
4677
5040
|
const hookExists = existsSync4(hookPath);
|
|
5041
|
+
let shellsSeen = /* @__PURE__ */ new Set();
|
|
4678
5042
|
if (hookExists) {
|
|
4679
5043
|
const fromLine = Number(opts.hookCursor ?? "0") || 0;
|
|
4680
|
-
const { entries, totalLines } = await readHookLog(hookPath, fromLine);
|
|
5044
|
+
const { entries, totalLines, shellsSeen: seen } = await readHookLog(hookPath, fromLine);
|
|
5045
|
+
shellsSeen = seen;
|
|
4681
5046
|
const scoped = opts.repoRoot ? entries.filter((e) => isUnderRoot(e.cwd, opts.repoRoot)) : entries;
|
|
4682
5047
|
results.push({ name: "pwsh-hook", entries: scoped.map(hookEntryToRaw), cursorAfter: String(totalLines) });
|
|
4683
5048
|
}
|
|
4684
|
-
const skipPwshScrape = preferHook &&
|
|
5049
|
+
const skipPwshScrape = preferHook && shellsSeen.has("pwsh-hook");
|
|
4685
5050
|
if (!skipPwshScrape && process.platform === "win32") {
|
|
4686
5051
|
const entries = await tryReadScrapeSource(psReadLineHistoryPath(), parsePsReadLineHistory, tailLines);
|
|
4687
5052
|
if (entries) results.push({ name: "pwsh", entries });
|
|
4688
5053
|
}
|
|
4689
|
-
const
|
|
4690
|
-
if (
|
|
4691
|
-
|
|
4692
|
-
|
|
5054
|
+
const skipBashScrape = preferHook && shellsSeen.has("bash-hook");
|
|
5055
|
+
if (!skipBashScrape) {
|
|
5056
|
+
const bashEntries = await tryReadScrapeSource(bashHistoryPath(), parseBashHistory, tailLines);
|
|
5057
|
+
if (bashEntries) results.push({ name: "bash", entries: bashEntries });
|
|
5058
|
+
}
|
|
5059
|
+
const skipZshScrape = preferHook && shellsSeen.has("zsh-hook");
|
|
5060
|
+
if (!skipZshScrape) {
|
|
5061
|
+
const zshEntries = await tryReadScrapeSource(zshHistoryPath(), parseZshHistory, tailLines);
|
|
5062
|
+
if (zshEntries) results.push({ name: "zsh", entries: zshEntries });
|
|
5063
|
+
}
|
|
4693
5064
|
return results;
|
|
4694
5065
|
}
|
|
4695
5066
|
|
|
@@ -4698,8 +5069,8 @@ function recomputeByNaturalKey(db, oldProjectId, newProjectId, kind, source, com
|
|
|
4698
5069
|
const rows = source ? db.prepare("SELECT * FROM nodes WHERE project_id = ? AND kind = ? AND source = ?").all(oldProjectId, kind, source) : db.prepare("SELECT * FROM nodes WHERE project_id = ? AND kind = ?").all(oldProjectId, kind);
|
|
4699
5070
|
const nodeExists = db.prepare("SELECT 1 FROM nodes WHERE id = ?");
|
|
4700
5071
|
const insertNode = db.prepare(
|
|
4701
|
-
`INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, created_at)
|
|
4702
|
-
VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @createdAt)`
|
|
5072
|
+
`INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, created_at, capture_mode, source_ts)
|
|
5073
|
+
VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @createdAt, @captureMode, @sourceTs)`
|
|
4703
5074
|
);
|
|
4704
5075
|
const readFiles = db.prepare("SELECT path, previous_path, insertions, deletions, is_binary FROM node_files WHERE node_id = ?");
|
|
4705
5076
|
const insertFile = db.prepare(
|
|
@@ -4746,7 +5117,9 @@ function recomputeByNaturalKey(db, oldProjectId, newProjectId, kind, source, com
|
|
|
4746
5117
|
body: row.body,
|
|
4747
5118
|
signal: row.signal,
|
|
4748
5119
|
meta: row.meta,
|
|
4749
|
-
createdAt: row.created_at
|
|
5120
|
+
createdAt: row.created_at,
|
|
5121
|
+
captureMode: row.capture_mode,
|
|
5122
|
+
sourceTs: row.source_ts
|
|
4750
5123
|
});
|
|
4751
5124
|
for (const file of readFiles.all(row.id)) {
|
|
4752
5125
|
insertFile.run({
|
|
@@ -4777,15 +5150,27 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
|
|
|
4777
5150
|
(_row, meta) => typeof meta.sessionKey === "string" ? meta.sessionKey : null,
|
|
4778
5151
|
denyEntries
|
|
4779
5152
|
);
|
|
4780
|
-
const
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
5153
|
+
const HOOK_SHELL_SOURCES = [
|
|
5154
|
+
{ source: "shell:pwsh-hook", prefix: "pwsh-hook" },
|
|
5155
|
+
{ source: "shell:bash-hook", prefix: "bash-hook" },
|
|
5156
|
+
{ source: "shell:zsh-hook", prefix: "zsh-hook" }
|
|
5157
|
+
];
|
|
5158
|
+
const hookShell = { migrated: 0, deduped: 0, skipped: 0, denied: 0 };
|
|
5159
|
+
for (const { source, prefix } of HOOK_SHELL_SOURCES) {
|
|
5160
|
+
const r = recomputeByNaturalKey(
|
|
5161
|
+
db,
|
|
5162
|
+
oldProjectId,
|
|
5163
|
+
newProjectId,
|
|
5164
|
+
"shell_command",
|
|
5165
|
+
source,
|
|
5166
|
+
(row, meta) => typeof meta.command === "string" ? `${prefix}:${row.ts}:${sha256Hex(meta.command).slice(0, 12)}` : null,
|
|
5167
|
+
denyEntries
|
|
5168
|
+
);
|
|
5169
|
+
hookShell.migrated += r.migrated;
|
|
5170
|
+
hookShell.deduped += r.deduped;
|
|
5171
|
+
hookShell.skipped += r.skipped;
|
|
5172
|
+
hookShell.denied += r.denied;
|
|
5173
|
+
}
|
|
4789
5174
|
let deniedConversationTurns = 0;
|
|
4790
5175
|
if (denyEntries.length > 0) {
|
|
4791
5176
|
const conversationTurns = db.prepare(`SELECT id, title, body, meta FROM nodes WHERE project_id = ? AND kind = 'conversation_turn'`).all(oldProjectId);
|
|
@@ -4821,7 +5206,7 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
|
|
|
4821
5206
|
|
|
4822
5207
|
// src/structure/collect.ts
|
|
4823
5208
|
import { readFile as readFile10 } from "fs/promises";
|
|
4824
|
-
import { join as
|
|
5209
|
+
import { join as join10 } from "path";
|
|
4825
5210
|
|
|
4826
5211
|
// src/structure/extract.ts
|
|
4827
5212
|
var IMPORT_PATTERNS = [
|
|
@@ -5260,7 +5645,7 @@ async function collectFileEdges(repoRoot) {
|
|
|
5260
5645
|
const trackedPaths = new Set(paths);
|
|
5261
5646
|
let goModulePath = null;
|
|
5262
5647
|
try {
|
|
5263
|
-
goModulePath = parseGoModulePath(await readFile10(
|
|
5648
|
+
goModulePath = parseGoModulePath(await readFile10(join10(repoRoot, "go.mod"), "utf8"));
|
|
5264
5649
|
} catch {
|
|
5265
5650
|
goModulePath = null;
|
|
5266
5651
|
}
|
|
@@ -5270,7 +5655,7 @@ async function collectFileEdges(repoRoot) {
|
|
|
5270
5655
|
for (const path of paths) {
|
|
5271
5656
|
let content;
|
|
5272
5657
|
try {
|
|
5273
|
-
content = await readFile10(
|
|
5658
|
+
content = await readFile10(join10(repoRoot, path), "utf8");
|
|
5274
5659
|
} catch {
|
|
5275
5660
|
unreadable.push(path);
|
|
5276
5661
|
continue;
|
|
@@ -5361,9 +5746,9 @@ ${node.body}`)
|
|
|
5361
5746
|
|
|
5362
5747
|
// src/cli/sync-lock.ts
|
|
5363
5748
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
5364
|
-
import { join as
|
|
5749
|
+
import { join as join11 } from "path";
|
|
5365
5750
|
function lockPath(wsDir) {
|
|
5366
|
-
return
|
|
5751
|
+
return join11(wsDir, "sync.lock");
|
|
5367
5752
|
}
|
|
5368
5753
|
function readOwner(path) {
|
|
5369
5754
|
try {
|
|
@@ -6734,10 +7119,11 @@ async function runScanShell(opts) {
|
|
|
6734
7119
|
return 0;
|
|
6735
7120
|
}
|
|
6736
7121
|
function formatNode6(node) {
|
|
6737
|
-
const
|
|
7122
|
+
const sourceTimestamp = typeof node.meta.sourceTimestamp === "string" ? node.meta.sourceTimestamp : null;
|
|
7123
|
+
const timestamp = sourceTimestamp ? sourceTimestamp.slice(0, 16).replace("T", " ") : pc18.dim("unknown");
|
|
6738
7124
|
const exit = node.meta.exitCode;
|
|
6739
7125
|
const exitLabel = typeof exit === "number" && exit !== 0 ? pc18.red(`exit ${exit}`) : "";
|
|
6740
|
-
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS),
|
|
7126
|
+
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), timestamp, node.title, exitLabel].filter(Boolean).join(" ");
|
|
6741
7127
|
}
|
|
6742
7128
|
|
|
6743
7129
|
// src/cli/commands/scan-structure.ts
|
|
@@ -6827,8 +7213,9 @@ async function runStale(opts) {
|
|
|
6827
7213
|
}
|
|
6828
7214
|
|
|
6829
7215
|
// src/cli/commands/status.ts
|
|
6830
|
-
import { basename as basename4 } from "path";
|
|
7216
|
+
import { basename as basename4, join as join12 } from "path";
|
|
6831
7217
|
import { statSync } from "fs";
|
|
7218
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6832
7219
|
import pc21 from "picocolors";
|
|
6833
7220
|
function daySpan(oldest, newest) {
|
|
6834
7221
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
@@ -6868,6 +7255,32 @@ function fileSize(path) {
|
|
|
6868
7255
|
return 0;
|
|
6869
7256
|
}
|
|
6870
7257
|
}
|
|
7258
|
+
function relativeTime(fromMs, nowMs) {
|
|
7259
|
+
const diffSec = Math.max(0, Math.round((nowMs - fromMs) / 1e3));
|
|
7260
|
+
if (diffSec < 60) return `${diffSec} second${diffSec === 1 ? "" : "s"} ago`;
|
|
7261
|
+
const diffMin = Math.round(diffSec / 60);
|
|
7262
|
+
if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`;
|
|
7263
|
+
const diffHour = Math.round(diffMin / 60);
|
|
7264
|
+
if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`;
|
|
7265
|
+
const diffDay = Math.round(diffHour / 24);
|
|
7266
|
+
return `${diffDay} day${diffDay === 1 ? "" : "s"} ago`;
|
|
7267
|
+
}
|
|
7268
|
+
async function getShellHookStatus(injected) {
|
|
7269
|
+
try {
|
|
7270
|
+
const target = injected ?? await resolveHookTarget();
|
|
7271
|
+
return { shell: target.shell, installed: (await hookStatus(target)).installed };
|
|
7272
|
+
} catch {
|
|
7273
|
+
return null;
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
async function readLastAutoSync(repoRoot) {
|
|
7277
|
+
try {
|
|
7278
|
+
const raw = await readFile11(join12(repoRoot, STATE_PATH), "utf8");
|
|
7279
|
+
return parsePostCommitSyncState(raw);
|
|
7280
|
+
} catch {
|
|
7281
|
+
return null;
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
6871
7284
|
async function runStatus(opts) {
|
|
6872
7285
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
6873
7286
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
@@ -6887,6 +7300,27 @@ async function runStatus(opts) {
|
|
|
6887
7300
|
const structure = store.fileEdgeStats(projectId);
|
|
6888
7301
|
const staleCount = store.countStaleCandidates(projectId);
|
|
6889
7302
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
7303
|
+
const [shellHook, preCommitTarget, postCommitTarget, lastAutoSync] = await Promise.all([
|
|
7304
|
+
getShellHookStatus(opts.shellHookTarget),
|
|
7305
|
+
resolveGitHookTarget(repo.root),
|
|
7306
|
+
resolvePostCommitHookTarget(repo.root),
|
|
7307
|
+
readLastAutoSync(repo.root)
|
|
7308
|
+
]);
|
|
7309
|
+
const [preCommitStatus, postCommitStatus] = await Promise.all([
|
|
7310
|
+
gitHookStatus(preCommitTarget),
|
|
7311
|
+
postCommitGitHookStatus(postCommitTarget)
|
|
7312
|
+
]);
|
|
7313
|
+
const now = (opts.now ?? Date.now)();
|
|
7314
|
+
const hooksPathNote2 = (target) => target.hooksPathConfig ? pc21.dim(` (core.hooksPath=${target.hooksPathConfig})`) : "";
|
|
7315
|
+
const installedLabel = (installed) => installed ? pc21.green("installed") : pc21.yellow("not installed");
|
|
7316
|
+
const lastAutoSyncLabel = lastAutoSync ? `${lastAutoSync.ok ? pc21.green("ok") : pc21.red(`FAILED (exit ${lastAutoSync.exitCode})`)} ${pc21.dim(relativeTime(Date.parse(lastAutoSync.ts), now))}` : postCommitStatus.installed ? pc21.dim("never (or hook predates this field -- reinstall with `nexusmem hook git-post install`)") : pc21.dim("n/a -- hook not installed");
|
|
7317
|
+
const hooksLines = [
|
|
7318
|
+
pc21.dim("hooks"),
|
|
7319
|
+
` ${pc21.dim(`shell (${shellHook?.shell ?? "?"})`.padEnd(23))} ${shellHook ? installedLabel(shellHook.installed) : pc21.dim("unknown -- could not detect this machine\u2019s shell profile")}`,
|
|
7320
|
+
` ${pc21.dim("git pre-commit".padEnd(23))} ${installedLabel(preCommitStatus.installed)}${hooksPathNote2(preCommitTarget)}`,
|
|
7321
|
+
` ${pc21.dim("git post-commit".padEnd(23))} ${installedLabel(postCommitStatus.installed)}${hooksPathNote2(postCommitTarget)}`,
|
|
7322
|
+
` ${pc21.dim("last auto-sync".padEnd(23))} ${lastAutoSyncLabel}`
|
|
7323
|
+
];
|
|
6890
7324
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6891
7325
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
6892
7326
|
const staleProjectWarning = otherProjectIds.length ? `${pc21.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc21.bold(
|
|
@@ -6916,7 +7350,9 @@ async function runStatus(opts) {
|
|
|
6916
7350
|
chains.failuresTotal ? `${pc21.dim("chains ")} ${pc21.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc21.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc21.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
6917
7351
|
structure.edges ? `${pc21.dim("structure")} ${pc21.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6918
7352
|
staleCount ? `${pc21.dim("aging ")} ${pc21.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc21.bold("nexusmem stale")}` : "",
|
|
6919
|
-
flaggedCount ? `${pc21.dim("flagged ")} ${pc21.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc21.bold("nexusmem stale")} for detail` : ""
|
|
7353
|
+
flaggedCount ? `${pc21.dim("flagged ")} ${pc21.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc21.bold("nexusmem stale")} for detail` : "",
|
|
7354
|
+
"",
|
|
7355
|
+
...hooksLines
|
|
6920
7356
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6921
7357
|
);
|
|
6922
7358
|
return 0;
|
|
@@ -6992,12 +7428,16 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6992
7428
|
})
|
|
6993
7429
|
)()
|
|
6994
7430
|
);
|
|
6995
|
-
|
|
6996
|
-
|
|
7431
|
+
var SHELL_CHOICES = ["pwsh", "bash", "zsh"];
|
|
7432
|
+
function shellOption() {
|
|
7433
|
+
return new Option("--shell <kind>", "pwsh, bash, or zsh -- default: auto-detected from platform/$SHELL").choices(SHELL_CHOICES);
|
|
7434
|
+
}
|
|
7435
|
+
program.command("hook").description("Manage the opt-in shell hook that logs cwd + exit code + timestamp").addCommand(
|
|
7436
|
+
new Command("install").description("Install (or update) the hook in your shell profile").addOption(shellOption()).option("--profile <path>", "override the auto-detected profile path").action((options) => guard(() => runHookInstall({ shell: options.shell, profile: options.profile }))())
|
|
6997
7437
|
).addCommand(
|
|
6998
|
-
new Command("remove").description("Remove the hook block from your
|
|
7438
|
+
new Command("remove").description("Remove the hook block from your shell profile").addOption(shellOption()).option("--profile <path>", "override the auto-detected profile path").action((options) => guard(() => runHookRemove({ shell: options.shell, profile: options.profile }))())
|
|
6999
7439
|
).addCommand(
|
|
7000
|
-
new Command("status").description("Show whether the hook is installed").option("--profile <path>", "override the auto-detected
|
|
7440
|
+
new Command("status").description("Show whether the hook is installed").addOption(shellOption()).option("--profile <path>", "override the auto-detected profile path").action((options) => guard(() => runHookStatus({ shell: options.shell, profile: options.profile }))())
|
|
7001
7441
|
).addCommand(
|
|
7002
7442
|
new Command("git").description("Manage the opt-in git pre-commit hook that runs `nexusmem precheck` before each commit").addCommand(
|
|
7003
7443
|
new Command("install").description("Install (or update) the hook in .git/hooks/pre-commit").option("-C, --cwd <path>", "repository path", process.cwd()).option("--force", "append after an existing foreign pre-commit hook instead of refusing", false).action((options) => guard(() => runHookGitInstall({ cwd: options.cwd, force: options.force }))())
|