nexusmem 0.3.3 → 0.4.0
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 +332 -287
- package/README.md +3 -0
- package/dist/cli/index.js +644 -167
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc17 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -147,7 +147,15 @@ var ConfigSchema = z.object({
|
|
|
147
147
|
maxCommits: z.number().int().positive().default(200),
|
|
148
148
|
maxFilesPerCommit: z.number().int().positive().default(20),
|
|
149
149
|
contextLines: z.number().int().nonnegative().default(3)
|
|
150
|
-
}).default({ enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 })
|
|
150
|
+
}).default({ enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 }),
|
|
151
|
+
/**
|
|
152
|
+
* JS/TS import-graph edges (`file_edges` table), derived from the
|
|
153
|
+
* working tree. On by default like git/shell/docs: no secrets risk,
|
|
154
|
+
* just structural relationships between files already tracked in git.
|
|
155
|
+
*/
|
|
156
|
+
structure: z.object({
|
|
157
|
+
enabled: z.boolean().default(true)
|
|
158
|
+
}).default({ enabled: true })
|
|
151
159
|
}).default({
|
|
152
160
|
git: { enabled: true, since: null, includeMerges: true },
|
|
153
161
|
shell: { enabled: true, tailLines: 300 },
|
|
@@ -160,7 +168,8 @@ var ConfigSchema = z.object({
|
|
|
160
168
|
maxPromptChars: 12e3
|
|
161
169
|
},
|
|
162
170
|
docs: { enabled: true, include: ["*.md"] },
|
|
163
|
-
diff: { enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 }
|
|
171
|
+
diff: { enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 },
|
|
172
|
+
structure: { enabled: true }
|
|
164
173
|
}),
|
|
165
174
|
limits: z.object({
|
|
166
175
|
maxFilesPerNode: z.number().int().positive().default(40),
|
|
@@ -549,20 +558,165 @@ async function hookStatus(target) {
|
|
|
549
558
|
return { installed: isHookInstalled(current) };
|
|
550
559
|
}
|
|
551
560
|
|
|
552
|
-
// src/
|
|
561
|
+
// src/hooks/install-git-precommit.ts
|
|
562
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
563
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
564
|
+
|
|
565
|
+
// src/hooks/git-pre-commit.ts
|
|
566
|
+
var MARK_START2 = "# >>> nexusmem precommit hook >>>";
|
|
567
|
+
var MARK_END2 = "# <<< nexusmem precommit hook <<<";
|
|
568
|
+
var SHEBANG = "#!/bin/sh";
|
|
569
|
+
function renderHookSnippet2() {
|
|
570
|
+
return [
|
|
571
|
+
MARK_START2,
|
|
572
|
+
"# Runs `nexusmem precheck` before each commit -- advisory only, never",
|
|
573
|
+
"# blocks a commit on its own (this hook does not pass --strict).",
|
|
574
|
+
"# Installed by: nexusmem hook git install",
|
|
575
|
+
"# Remove with: nexusmem hook git remove",
|
|
576
|
+
"if command -v nexusmem >/dev/null 2>&1; then",
|
|
577
|
+
" nexusmem precheck",
|
|
578
|
+
"fi",
|
|
579
|
+
MARK_END2,
|
|
580
|
+
""
|
|
581
|
+
].join("\n");
|
|
582
|
+
}
|
|
583
|
+
function isHookInstalled2(content) {
|
|
584
|
+
return content.includes(MARK_START2);
|
|
585
|
+
}
|
|
586
|
+
function isForeignHook(content) {
|
|
587
|
+
return content.trim().length > 0 && !isHookInstalled2(content);
|
|
588
|
+
}
|
|
589
|
+
function stripHookSnippet2(content) {
|
|
590
|
+
const startIdx = content.indexOf(MARK_START2);
|
|
591
|
+
const endIdx = content.indexOf(MARK_END2);
|
|
592
|
+
if (startIdx === -1 || endIdx === -1) return content;
|
|
593
|
+
const afterBlock = content.slice(endIdx + MARK_END2.length).replace(/^\r?\n/, "");
|
|
594
|
+
return content.slice(0, startIdx) + afterBlock;
|
|
595
|
+
}
|
|
596
|
+
function upsertHookSnippet2(content) {
|
|
597
|
+
const stripped = stripHookSnippet2(content).replace(/\s+$/, "");
|
|
598
|
+
const prefix = stripped.length > 0 ? `${stripped}
|
|
599
|
+
|
|
600
|
+
` : "";
|
|
601
|
+
return `${prefix}${renderHookSnippet2()}`;
|
|
602
|
+
}
|
|
603
|
+
function ensureShebang(content) {
|
|
604
|
+
if (content.startsWith("#!")) return content;
|
|
605
|
+
return content.length > 0 ? `${SHEBANG}
|
|
606
|
+
${content}` : `${SHEBANG}
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/hooks/install-git-precommit.ts
|
|
611
|
+
var ForeignGitHookError = class extends Error {
|
|
612
|
+
constructor(hookPath) {
|
|
613
|
+
super(
|
|
614
|
+
`${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.`
|
|
615
|
+
);
|
|
616
|
+
this.hookPath = hookPath;
|
|
617
|
+
this.name = "ForeignGitHookError";
|
|
618
|
+
}
|
|
619
|
+
hookPath;
|
|
620
|
+
};
|
|
621
|
+
function resolveGitHookTarget(repoRoot) {
|
|
622
|
+
return { hookPath: join4(repoRoot, ".git", "hooks", "pre-commit") };
|
|
623
|
+
}
|
|
624
|
+
async function readHook(path) {
|
|
625
|
+
try {
|
|
626
|
+
return await readFile3(path, "utf8");
|
|
627
|
+
} catch {
|
|
628
|
+
return "";
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function installGitHook(target, opts = {}) {
|
|
632
|
+
const current = await readHook(target.hookPath);
|
|
633
|
+
const alreadyInstalled = isHookInstalled2(current);
|
|
634
|
+
const foreign = isForeignHook(current);
|
|
635
|
+
if (foreign && !alreadyInstalled && !opts.force) {
|
|
636
|
+
throw new ForeignGitHookError(target.hookPath);
|
|
637
|
+
}
|
|
638
|
+
const next = upsertHookSnippet2(ensureShebang(current));
|
|
639
|
+
if (next === current) return { changed: false, alreadyInstalled, appendedToForeign: false };
|
|
640
|
+
await mkdir3(dirname2(target.hookPath), { recursive: true });
|
|
641
|
+
await writeFile3(target.hookPath, next, "utf8");
|
|
642
|
+
await chmod(target.hookPath, 493).catch(() => {
|
|
643
|
+
});
|
|
644
|
+
return { changed: true, alreadyInstalled, appendedToForeign: foreign && !alreadyInstalled };
|
|
645
|
+
}
|
|
646
|
+
async function removeGitHook(target) {
|
|
647
|
+
const current = await readHook(target.hookPath);
|
|
648
|
+
if (!isHookInstalled2(current)) return { changed: false };
|
|
649
|
+
const stripped = stripHookSnippet2(current).trim();
|
|
650
|
+
if (stripped === "" || stripped === SHEBANG) {
|
|
651
|
+
await unlink(target.hookPath).catch(() => {
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
await writeFile3(target.hookPath, `${stripped}
|
|
655
|
+
`, "utf8");
|
|
656
|
+
}
|
|
657
|
+
return { changed: true };
|
|
658
|
+
}
|
|
659
|
+
async function gitHookStatus(target) {
|
|
660
|
+
const current = await readHook(target.hookPath);
|
|
661
|
+
return { installed: isHookInstalled2(current), foreign: isForeignHook(current) };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/cli/commands/hook-git.ts
|
|
553
665
|
import pc from "picocolors";
|
|
666
|
+
async function runHookGitInstall(opts) {
|
|
667
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
668
|
+
const target = resolveGitHookTarget(repo.root);
|
|
669
|
+
const result = await installGitHook(target, { force: opts.force });
|
|
670
|
+
const lines = [
|
|
671
|
+
result.changed ? `${pc.green(result.alreadyInstalled ? "updated" : "installed")} git pre-commit hook` : `${pc.dim("already up to date")}`,
|
|
672
|
+
` hook ${target.hookPath}`
|
|
673
|
+
];
|
|
674
|
+
if (result.appendedToForeign) {
|
|
675
|
+
lines.push(` ${pc.yellow("appended after an existing pre-commit hook -- review")} ${target.hookPath}`);
|
|
676
|
+
}
|
|
677
|
+
lines.push(
|
|
678
|
+
"",
|
|
679
|
+
`Runs ${pc.bold("nexusmem precheck")} before each commit -- advisory only, never blocks a commit on its own.`,
|
|
680
|
+
`Run ${pc.bold("nexusmem hook git remove")} to undo this.`,
|
|
681
|
+
""
|
|
682
|
+
);
|
|
683
|
+
process.stdout.write(lines.join("\n"));
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
async function runHookGitRemove(opts) {
|
|
687
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
688
|
+
const target = resolveGitHookTarget(repo.root);
|
|
689
|
+
const result = await removeGitHook(target);
|
|
690
|
+
process.stdout.write(
|
|
691
|
+
result.changed ? `${pc.green("removed")} nexusmem's block from ${target.hookPath}
|
|
692
|
+
` : `${pc.dim("nothing to remove")} \u2014 no nexusmem block found in ${target.hookPath}
|
|
693
|
+
`
|
|
694
|
+
);
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
async function runHookGitStatus(opts) {
|
|
698
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
699
|
+
const target = resolveGitHookTarget(repo.root);
|
|
700
|
+
const result = await gitHookStatus(target);
|
|
701
|
+
const statusLabel = result.installed ? pc.green("installed") : result.foreign ? pc.yellow("a foreign hook exists (not nexusmem) -- install --force to append") : pc.yellow("not installed");
|
|
702
|
+
process.stdout.write([`${pc.dim("hook ")} ${target.hookPath}`, `${pc.dim("status")} ${statusLabel}`, ""].join("\n"));
|
|
703
|
+
return 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/cli/commands/hook.ts
|
|
707
|
+
import pc2 from "picocolors";
|
|
554
708
|
async function runHookInstall(opts) {
|
|
555
709
|
const target = await resolveHookTarget(opts.profile, opts.logPath);
|
|
556
710
|
const result = await installHook(target);
|
|
557
711
|
process.stdout.write(
|
|
558
712
|
[
|
|
559
|
-
result.changed ? `${
|
|
713
|
+
result.changed ? `${pc2.green(result.alreadyInstalled ? "updated" : "installed")} shell hook` : `${pc2.dim("already up to date")}`,
|
|
560
714
|
` profile ${target.profilePath}`,
|
|
561
715
|
` log ${target.logPath}`,
|
|
562
716
|
"",
|
|
563
717
|
`New commands in any PowerShell session using this profile will now log their timestamp, cwd and exit code.`,
|
|
564
718
|
`Open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect.`,
|
|
565
|
-
`Run ${
|
|
719
|
+
`Run ${pc2.bold("nexusmem hook remove")} to undo this.`,
|
|
566
720
|
""
|
|
567
721
|
].join("\n")
|
|
568
722
|
);
|
|
@@ -572,8 +726,8 @@ async function runHookRemove(opts) {
|
|
|
572
726
|
const target = await resolveHookTarget(opts.profile, opts.logPath);
|
|
573
727
|
const result = await removeHook(target);
|
|
574
728
|
process.stdout.write(
|
|
575
|
-
result.changed ? `${
|
|
576
|
-
` : `${
|
|
729
|
+
result.changed ? `${pc2.green("removed")} shell hook from ${target.profilePath}
|
|
730
|
+
` : `${pc2.dim("nothing to remove")} \u2014 no hook block found in ${target.profilePath}
|
|
577
731
|
`
|
|
578
732
|
);
|
|
579
733
|
return 0;
|
|
@@ -583,9 +737,9 @@ async function runHookStatus(opts) {
|
|
|
583
737
|
const result = await hookStatus(target);
|
|
584
738
|
process.stdout.write(
|
|
585
739
|
[
|
|
586
|
-
`${
|
|
587
|
-
`${
|
|
588
|
-
`${
|
|
740
|
+
`${pc2.dim("profile")} ${target.profilePath}`,
|
|
741
|
+
`${pc2.dim("log ")} ${target.logPath}`,
|
|
742
|
+
`${pc2.dim("status ")} ${result.installed ? pc2.green("installed") : pc2.yellow("not installed")}`,
|
|
589
743
|
""
|
|
590
744
|
].join("\n")
|
|
591
745
|
);
|
|
@@ -594,12 +748,12 @@ async function runHookStatus(opts) {
|
|
|
594
748
|
|
|
595
749
|
// src/cli/commands/init.ts
|
|
596
750
|
import { relative } from "path";
|
|
597
|
-
import
|
|
751
|
+
import pc3 from "picocolors";
|
|
598
752
|
|
|
599
753
|
// src/config/registry.ts
|
|
600
754
|
import { existsSync as existsSync2 } from "fs";
|
|
601
|
-
import { mkdir as
|
|
602
|
-
import { join as
|
|
755
|
+
import { mkdir as mkdir4, readFile as readFile4, rename, writeFile as writeFile4 } from "fs/promises";
|
|
756
|
+
import { join as join5 } from "path";
|
|
603
757
|
import { z as z2 } from "zod";
|
|
604
758
|
var ENTRY_SCHEMA = z2.object({
|
|
605
759
|
projectId: z2.string().min(1),
|
|
@@ -614,12 +768,12 @@ var REGISTRY_SCHEMA = z2.object({
|
|
|
614
768
|
projects: z2.array(ENTRY_SCHEMA).default([])
|
|
615
769
|
});
|
|
616
770
|
function registryPath() {
|
|
617
|
-
return
|
|
771
|
+
return join5(globalWorkspaceDir(), "projects.json");
|
|
618
772
|
}
|
|
619
773
|
async function readRegistry() {
|
|
620
774
|
let raw;
|
|
621
775
|
try {
|
|
622
|
-
raw = await
|
|
776
|
+
raw = await readFile4(registryPath(), "utf8");
|
|
623
777
|
} catch {
|
|
624
778
|
return [];
|
|
625
779
|
}
|
|
@@ -658,8 +812,8 @@ async function forgetProjects(projectIds) {
|
|
|
658
812
|
async function writeRegistry(projects) {
|
|
659
813
|
const path = registryPath();
|
|
660
814
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
661
|
-
await
|
|
662
|
-
await
|
|
815
|
+
await mkdir4(globalWorkspaceDir(), { recursive: true });
|
|
816
|
+
await writeFile4(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
|
|
663
817
|
`, "utf8");
|
|
664
818
|
await rename(tmp, path);
|
|
665
819
|
}
|
|
@@ -693,7 +847,7 @@ function makeProjectId({ root, originUrl }) {
|
|
|
693
847
|
// src/store/store.ts
|
|
694
848
|
import Database from "better-sqlite3";
|
|
695
849
|
import { mkdirSync } from "fs";
|
|
696
|
-
import { dirname as
|
|
850
|
+
import { dirname as dirname3 } from "path";
|
|
697
851
|
import * as sqliteVec from "sqlite-vec";
|
|
698
852
|
|
|
699
853
|
// src/store/fts.ts
|
|
@@ -820,10 +974,29 @@ CREATE TABLE node_links (
|
|
|
820
974
|
-- direction yet, so only the forward lookup gets an index.
|
|
821
975
|
CREATE INDEX idx_node_links_from ON node_links (from_node_id);
|
|
822
976
|
`;
|
|
977
|
+
var V4 = `
|
|
978
|
+
-- File-to-file structural relationships (currently: JS/TS import edges),
|
|
979
|
+
-- derived from the working tree rather than from any node's content. A
|
|
980
|
+
-- source file is not a node, so this cannot reuse node_links (both of its
|
|
981
|
+
-- columns FK to nodes.id) -- project_id has to be stored here explicitly
|
|
982
|
+
-- since there is no node to join through for it.
|
|
983
|
+
CREATE TABLE file_edges (
|
|
984
|
+
project_id TEXT NOT NULL,
|
|
985
|
+
from_path TEXT NOT NULL,
|
|
986
|
+
to_path TEXT NOT NULL,
|
|
987
|
+
kind TEXT NOT NULL,
|
|
988
|
+
PRIMARY KEY (project_id, from_path, to_path, kind)
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
-- "What imports this file" (e.g. blast-radius of a change) is the query a
|
|
992
|
+
-- future feature needs; the primary key already covers the forward direction.
|
|
993
|
+
CREATE INDEX idx_file_edges_to ON file_edges (project_id, to_path);
|
|
994
|
+
`;
|
|
823
995
|
var MIGRATIONS = [
|
|
824
996
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
825
997
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
826
|
-
{ version: 3, up: (db) => db.exec(V3) }
|
|
998
|
+
{ version: 3, up: (db) => db.exec(V3) },
|
|
999
|
+
{ version: 4, up: (db) => db.exec(V4) }
|
|
827
1000
|
];
|
|
828
1001
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
829
1002
|
function currentSchemaVersion(db) {
|
|
@@ -852,7 +1025,7 @@ var MemoryStore = class _MemoryStore {
|
|
|
852
1025
|
}
|
|
853
1026
|
db;
|
|
854
1027
|
static open(dbPath) {
|
|
855
|
-
mkdirSync(
|
|
1028
|
+
mkdirSync(dirname3(dbPath), { recursive: true });
|
|
856
1029
|
const db = new Database(dbPath);
|
|
857
1030
|
db.pragma("journal_mode = WAL");
|
|
858
1031
|
db.pragma("synchronous = NORMAL");
|
|
@@ -888,6 +1061,13 @@ var MemoryStore = class _MemoryStore {
|
|
|
888
1061
|
(r) => r.id
|
|
889
1062
|
);
|
|
890
1063
|
}
|
|
1064
|
+
/** Total nodes held under the given project identities. */
|
|
1065
|
+
countProjectNodes(projectIds) {
|
|
1066
|
+
if (projectIds.length === 0) return 0;
|
|
1067
|
+
const placeholders = projectIds.map(() => "?").join(", ");
|
|
1068
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS n FROM nodes WHERE project_id IN (${placeholders})`).get(...projectIds);
|
|
1069
|
+
return row.n;
|
|
1070
|
+
}
|
|
891
1071
|
/**
|
|
892
1072
|
* Write a batch of nodes in one transaction.
|
|
893
1073
|
*
|
|
@@ -1091,6 +1271,29 @@ var MemoryStore = class _MemoryStore {
|
|
|
1091
1271
|
return this.db.prepare(`DELETE FROM nodes WHERE ${scope}`).run(params).changes;
|
|
1092
1272
|
})();
|
|
1093
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Replace this project's entire `file_edges` snapshot in one transaction.
|
|
1276
|
+
*
|
|
1277
|
+
* Edges describe the current working tree, not history -- unlike
|
|
1278
|
+
* `pruneSourceNodes`'s incremental diff-against-a-scan, there is no cursor
|
|
1279
|
+
* to walk, so every `scan-structure`/sync run is a full rescan and this is
|
|
1280
|
+
* always a delete-then-insert of the whole set, never a partial update.
|
|
1281
|
+
*/
|
|
1282
|
+
replaceFileEdges(projectId, edges) {
|
|
1283
|
+
this.db.transaction(() => {
|
|
1284
|
+
this.db.prepare("DELETE FROM file_edges WHERE project_id = ?").run(projectId);
|
|
1285
|
+
const insert = this.db.prepare(
|
|
1286
|
+
"INSERT OR IGNORE INTO file_edges (project_id, from_path, to_path, kind) VALUES (?, ?, ?, ?)"
|
|
1287
|
+
);
|
|
1288
|
+
for (const edge of edges) insert.run(projectId, edge.fromPath, edge.toPath, edge.kind);
|
|
1289
|
+
})();
|
|
1290
|
+
}
|
|
1291
|
+
/** Edge count + distinct source-file count, for `nexusmem status`'s `structure` line. */
|
|
1292
|
+
fileEdgeStats(projectId) {
|
|
1293
|
+
const edges = this.db.prepare("SELECT COUNT(*) AS c FROM file_edges WHERE project_id = ?").get(projectId).c;
|
|
1294
|
+
const files = this.db.prepare("SELECT COUNT(DISTINCT from_path) AS c FROM file_edges WHERE project_id = ?").get(projectId).c;
|
|
1295
|
+
return { edges, files };
|
|
1296
|
+
}
|
|
1094
1297
|
/**
|
|
1095
1298
|
* Nodes for this project that have no embedding yet (new, or invalidated
|
|
1096
1299
|
* by a content change).
|
|
@@ -1214,9 +1417,9 @@ async function runInit(opts) {
|
|
|
1214
1417
|
if (already && !opts.force) {
|
|
1215
1418
|
const existing = await readConfig(ws);
|
|
1216
1419
|
process.stderr.write(
|
|
1217
|
-
`${
|
|
1218
|
-
project ${
|
|
1219
|
-
use ${
|
|
1420
|
+
`${pc3.yellow("already initialized")} ${relative(process.cwd(), ws.configPath) || ws.configPath}
|
|
1421
|
+
project ${pc3.cyan(existing.projectId)}
|
|
1422
|
+
use ${pc3.bold("--force")} to reset the config (the database is kept)
|
|
1220
1423
|
`
|
|
1221
1424
|
);
|
|
1222
1425
|
return 0;
|
|
@@ -1233,14 +1436,14 @@ async function runInit(opts) {
|
|
|
1233
1436
|
}
|
|
1234
1437
|
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
1235
1438
|
const lines = [
|
|
1236
|
-
`${
|
|
1237
|
-
` project ${
|
|
1439
|
+
`${pc3.green("initialized")} ${ws.dir}`,
|
|
1440
|
+
` project ${pc3.cyan(projectId)}`,
|
|
1238
1441
|
` repo ${repo.root}`,
|
|
1239
|
-
` branch ${repo.branch ??
|
|
1442
|
+
` branch ${repo.branch ?? pc3.yellow("(detached)")}`,
|
|
1240
1443
|
` schema v${LATEST_SCHEMA_VERSION}`
|
|
1241
1444
|
];
|
|
1242
1445
|
if (opts.enableConversation) {
|
|
1243
|
-
lines.push(` ${
|
|
1446
|
+
lines.push(` ${pc3.yellow("conversation source enabled")} -- transcripts will be redacted-but-indexed on sync`);
|
|
1244
1447
|
}
|
|
1245
1448
|
if (opts.hook) {
|
|
1246
1449
|
try {
|
|
@@ -1248,26 +1451,26 @@ async function runInit(opts) {
|
|
|
1248
1451
|
const result = await installHook(target);
|
|
1249
1452
|
lines.push(
|
|
1250
1453
|
"",
|
|
1251
|
-
`${
|
|
1454
|
+
`${pc3.green(result.changed ? "installed" : "already installed")} shell hook`,
|
|
1252
1455
|
` profile ${target.profilePath}`,
|
|
1253
1456
|
` log ${target.logPath}`,
|
|
1254
1457
|
` open a new PowerShell window (or run \`. $PROFILE\`) for it to take effect`
|
|
1255
1458
|
);
|
|
1256
1459
|
} catch (err) {
|
|
1257
1460
|
if (err instanceof ProfileNotFoundError) {
|
|
1258
|
-
lines.push("", `${
|
|
1461
|
+
lines.push("", `${pc3.yellow("hook not installed")} ${err.message}`);
|
|
1259
1462
|
} else {
|
|
1260
1463
|
throw err;
|
|
1261
1464
|
}
|
|
1262
1465
|
}
|
|
1263
1466
|
}
|
|
1264
|
-
lines.push("", `Next: ${
|
|
1467
|
+
lines.push("", `Next: ${pc3.bold("nexusmem sync")}`, "");
|
|
1265
1468
|
out(lines.join("\n"));
|
|
1266
1469
|
return 0;
|
|
1267
1470
|
}
|
|
1268
1471
|
|
|
1269
1472
|
// src/cli/commands/projects.ts
|
|
1270
|
-
import
|
|
1473
|
+
import pc4 from "picocolors";
|
|
1271
1474
|
async function runProjects(opts) {
|
|
1272
1475
|
const { entries, missing } = await readLiveRegistry();
|
|
1273
1476
|
const rows = entries.map((entry) => {
|
|
@@ -1286,7 +1489,7 @@ async function runProjects(opts) {
|
|
|
1286
1489
|
});
|
|
1287
1490
|
if (opts.prune) {
|
|
1288
1491
|
const removed = await forgetProjects(missing.map((entry) => entry.projectId));
|
|
1289
|
-
process.stderr.write(`${
|
|
1492
|
+
process.stderr.write(`${pc4.yellow("pruned")} ${removed} project(s) whose database is gone
|
|
1290
1493
|
`);
|
|
1291
1494
|
}
|
|
1292
1495
|
if (opts.json) {
|
|
@@ -1294,28 +1497,28 @@ async function runProjects(opts) {
|
|
|
1294
1497
|
`);
|
|
1295
1498
|
return 0;
|
|
1296
1499
|
}
|
|
1297
|
-
process.stderr.write(`${
|
|
1500
|
+
process.stderr.write(`${pc4.dim("registry")} ${registryPath()}
|
|
1298
1501
|
|
|
1299
1502
|
`);
|
|
1300
1503
|
if (rows.length === 0) {
|
|
1301
|
-
process.stderr.write(`${
|
|
1504
|
+
process.stderr.write(`${pc4.yellow("no projects registered")} -- run ${pc4.bold("nexusmem sync")} in a repository
|
|
1302
1505
|
`);
|
|
1303
1506
|
return 0;
|
|
1304
1507
|
}
|
|
1305
1508
|
for (const row of rows) {
|
|
1306
1509
|
const seen = new Date(row.lastSeenAt).toISOString().slice(0, 16).replace("T", " ");
|
|
1307
|
-
const count = row.nodes === null ?
|
|
1308
|
-
process.stdout.write(`${
|
|
1309
|
-
${
|
|
1510
|
+
const count = row.nodes === null ? pc4.yellow("unreadable") : `${row.nodes} node(s)`;
|
|
1511
|
+
process.stdout.write(`${pc4.cyan(row.projectId.slice(0, 8))} ${row.root}
|
|
1512
|
+
${pc4.dim(`${count}, last seen ${seen}`)}
|
|
1310
1513
|
`);
|
|
1311
1514
|
}
|
|
1312
1515
|
if (!opts.prune && missing.length > 0) {
|
|
1313
1516
|
process.stderr.write(
|
|
1314
1517
|
`
|
|
1315
|
-
${
|
|
1518
|
+
${pc4.yellow(`${missing.length} registered project(s) have no database on disk`)} ${pc4.dim("-- run with --prune to forget them")}
|
|
1316
1519
|
`
|
|
1317
1520
|
);
|
|
1318
|
-
for (const entry of missing) process.stderr.write(` ${
|
|
1521
|
+
for (const entry of missing) process.stderr.write(` ${pc4.dim(entry.root)}
|
|
1319
1522
|
`);
|
|
1320
1523
|
}
|
|
1321
1524
|
return 0;
|
|
@@ -1518,16 +1721,18 @@ function normalizeCommand(command) {
|
|
|
1518
1721
|
}
|
|
1519
1722
|
var MAX_TOKEN_DOC_FREQUENCY = 0.2;
|
|
1520
1723
|
var MIN_CORPUS_FOR_FREQUENCY_FILTER = 10;
|
|
1521
|
-
|
|
1724
|
+
var DEFAULT_BOILERPLATE_KINDS = ["conversation_turn", "session_summary"];
|
|
1725
|
+
function filterBoilerplateTokens(db, projectId, tokens, kinds = DEFAULT_BOILERPLATE_KINDS) {
|
|
1522
1726
|
if (tokens.length === 0) return tokens;
|
|
1523
|
-
const
|
|
1727
|
+
const kindsPlaceholder = kinds.map(() => "?").join(", ");
|
|
1728
|
+
const total = db.prepare(`SELECT COUNT(*) AS c FROM nodes WHERE project_id = ? AND kind IN (${kindsPlaceholder})`).get(projectId, ...kinds).c;
|
|
1524
1729
|
if (total < MIN_CORPUS_FOR_FREQUENCY_FILTER) return tokens;
|
|
1525
1730
|
const countMatching = db.prepare(
|
|
1526
1731
|
`SELECT COUNT(*) AS c FROM nodes_fts JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1527
|
-
WHERE nodes_fts MATCH ? AND n.project_id = ? AND n.kind IN (
|
|
1732
|
+
WHERE nodes_fts MATCH ? AND n.project_id = ? AND n.kind IN (${kindsPlaceholder})`
|
|
1528
1733
|
);
|
|
1529
1734
|
return tokens.filter((t) => {
|
|
1530
|
-
const matching = countMatching.get(`"${t}"*`, projectId).c;
|
|
1735
|
+
const matching = countMatching.get(`"${t}"*`, projectId, ...kinds).c;
|
|
1531
1736
|
return matching / total <= MAX_TOKEN_DOC_FREQUENCY;
|
|
1532
1737
|
});
|
|
1533
1738
|
}
|
|
@@ -1863,7 +2068,7 @@ var OllamaEmbeddingProvider = class {
|
|
|
1863
2068
|
};
|
|
1864
2069
|
|
|
1865
2070
|
// src/cli/commands/sync.ts
|
|
1866
|
-
import
|
|
2071
|
+
import pc5 from "picocolors";
|
|
1867
2072
|
|
|
1868
2073
|
// src/conversation/chunk.ts
|
|
1869
2074
|
var HEADING_LINE = /^#{1,6}\s+(.+)$/;
|
|
@@ -2851,25 +3056,25 @@ function collectShellHistory(entries, projectId, opts = {}) {
|
|
|
2851
3056
|
}
|
|
2852
3057
|
|
|
2853
3058
|
// src/conversation/claude-code-reader.ts
|
|
2854
|
-
import { readFile as
|
|
3059
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2855
3060
|
import { basename as basename2 } from "path";
|
|
2856
3061
|
|
|
2857
3062
|
// src/conversation/paths.ts
|
|
2858
3063
|
import { existsSync as existsSync3 } from "fs";
|
|
2859
3064
|
import { readdir } from "fs/promises";
|
|
2860
3065
|
import { homedir as homedir3 } from "os";
|
|
2861
|
-
import { join as
|
|
3066
|
+
import { join as join6 } from "path";
|
|
2862
3067
|
function claudeProjectSlug(repoRoot) {
|
|
2863
3068
|
return repoRoot.replace(/[\\/:]/g, "-");
|
|
2864
3069
|
}
|
|
2865
3070
|
function claudeProjectTranscriptDir(repoRoot) {
|
|
2866
|
-
return
|
|
3071
|
+
return join6(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
|
|
2867
3072
|
}
|
|
2868
3073
|
async function listTranscriptFiles(repoRoot) {
|
|
2869
3074
|
const dir = claudeProjectTranscriptDir(repoRoot);
|
|
2870
3075
|
if (!existsSync3(dir)) return [];
|
|
2871
3076
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
2872
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) =>
|
|
3077
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join6(dir, e.name));
|
|
2873
3078
|
}
|
|
2874
3079
|
|
|
2875
3080
|
// src/conversation/claude-code-reader.ts
|
|
@@ -2944,15 +3149,15 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
2944
3149
|
const files = await listTranscriptFiles(repoRoot);
|
|
2945
3150
|
const turns = [];
|
|
2946
3151
|
for (const file of files) {
|
|
2947
|
-
const raw = await
|
|
3152
|
+
const raw = await readFile5(file, "utf8");
|
|
2948
3153
|
turns.push(...parseClaudeCodeTranscript(raw, { sessionId: basename2(file, ".jsonl") }));
|
|
2949
3154
|
}
|
|
2950
3155
|
return turns;
|
|
2951
3156
|
}
|
|
2952
3157
|
|
|
2953
3158
|
// src/docs/read.ts
|
|
2954
|
-
import { readFile as
|
|
2955
|
-
import { join as
|
|
3159
|
+
import { readFile as readFile6, stat } from "fs/promises";
|
|
3160
|
+
import { join as join7 } from "path";
|
|
2956
3161
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
2957
3162
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
2958
3163
|
const pathspecs = opts.include ?? DEFAULT_PATHSPECS;
|
|
@@ -2965,11 +3170,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
2965
3170
|
const unreadable = [];
|
|
2966
3171
|
for (const relPath of paths) {
|
|
2967
3172
|
const path = relPath.replace(/\\/g, "/");
|
|
2968
|
-
const absPath =
|
|
3173
|
+
const absPath = join7(repoRoot, relPath);
|
|
2969
3174
|
let content;
|
|
2970
3175
|
let mtime;
|
|
2971
3176
|
try {
|
|
2972
|
-
[content, { mtime }] = await Promise.all([
|
|
3177
|
+
[content, { mtime }] = await Promise.all([readFile6(absPath, "utf8"), stat(absPath)]);
|
|
2973
3178
|
} catch {
|
|
2974
3179
|
unreadable.push(path);
|
|
2975
3180
|
continue;
|
|
@@ -2981,11 +3186,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
2981
3186
|
|
|
2982
3187
|
// src/shell/detect.ts
|
|
2983
3188
|
import { existsSync as existsSync4 } from "fs";
|
|
2984
|
-
import { readFile as
|
|
3189
|
+
import { readFile as readFile8, stat as stat2 } from "fs/promises";
|
|
2985
3190
|
|
|
2986
3191
|
// src/shell/hook-log.ts
|
|
2987
|
-
import { appendFile, mkdir as
|
|
2988
|
-
import { dirname as
|
|
3192
|
+
import { appendFile, mkdir as mkdir5, readFile as readFile7 } from "fs/promises";
|
|
3193
|
+
import { dirname as dirname4 } from "path";
|
|
2989
3194
|
function parseHookLogLine(line) {
|
|
2990
3195
|
const trimmed = line.trim();
|
|
2991
3196
|
if (!trimmed) return null;
|
|
@@ -3009,7 +3214,7 @@ function parseHookLogLine(line) {
|
|
|
3009
3214
|
async function readHookLog(path, fromLine) {
|
|
3010
3215
|
let raw;
|
|
3011
3216
|
try {
|
|
3012
|
-
raw = await
|
|
3217
|
+
raw = await readFile7(path, "utf8");
|
|
3013
3218
|
} catch {
|
|
3014
3219
|
return { entries: [], totalLines: fromLine };
|
|
3015
3220
|
}
|
|
@@ -3143,7 +3348,7 @@ function hookEntryToRaw(e) {
|
|
|
3143
3348
|
}
|
|
3144
3349
|
async function tryReadScrapeSource(path, parse, tailLines) {
|
|
3145
3350
|
if (!existsSync4(path)) return null;
|
|
3146
|
-
const [raw, stats] = await Promise.all([
|
|
3351
|
+
const [raw, stats] = await Promise.all([readFile8(path, "utf8"), stat2(path)]);
|
|
3147
3352
|
return parse(raw, stats.mtimeMs, { tailLines });
|
|
3148
3353
|
}
|
|
3149
3354
|
async function collectAvailableShellHistory(opts = {}) {
|
|
@@ -3264,6 +3469,101 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
|
|
|
3264
3469
|
})();
|
|
3265
3470
|
}
|
|
3266
3471
|
|
|
3472
|
+
// src/structure/collect.ts
|
|
3473
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3474
|
+
import { join as join8 } from "path";
|
|
3475
|
+
|
|
3476
|
+
// src/structure/extract.ts
|
|
3477
|
+
var IMPORT_PATTERNS = [
|
|
3478
|
+
// import ... from '...'; export ... from '...'
|
|
3479
|
+
/\b(?:import|export)\b[^'"\n]*?\bfrom\s*['"]([^'"]+)['"]/g,
|
|
3480
|
+
// import '...'; (side-effect only)
|
|
3481
|
+
/\bimport\s*['"]([^'"]+)['"]/g,
|
|
3482
|
+
// require('...')
|
|
3483
|
+
/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
3484
|
+
// dynamic import('...')
|
|
3485
|
+
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
3486
|
+
];
|
|
3487
|
+
function extractImportSpecifiers(source) {
|
|
3488
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3489
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
3490
|
+
pattern.lastIndex = 0;
|
|
3491
|
+
let match;
|
|
3492
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
3493
|
+
const specifier = match[1];
|
|
3494
|
+
if (specifier && (specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
3495
|
+
seen.add(specifier);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
return [...seen];
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// src/structure/resolve.ts
|
|
3503
|
+
import { posix } from "path";
|
|
3504
|
+
var REWRITE_EXTENSIONS = {
|
|
3505
|
+
".js": [".ts", ".tsx", ".js"],
|
|
3506
|
+
".jsx": [".tsx", ".jsx"],
|
|
3507
|
+
".mjs": [".mts", ".mjs"],
|
|
3508
|
+
".cjs": [".cts", ".cjs"]
|
|
3509
|
+
};
|
|
3510
|
+
var APPEND_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".d.ts", ".json"];
|
|
3511
|
+
var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
3512
|
+
function resolveSpecifier(fromPath, specifier, trackedPaths) {
|
|
3513
|
+
const fromDir = posix.dirname(fromPath);
|
|
3514
|
+
const joined = posix.normalize(posix.join(fromDir, specifier));
|
|
3515
|
+
if (trackedPaths.has(joined)) return joined;
|
|
3516
|
+
const ext = posix.extname(joined);
|
|
3517
|
+
const rewrites = REWRITE_EXTENSIONS[ext];
|
|
3518
|
+
if (rewrites) {
|
|
3519
|
+
const base = joined.slice(0, -ext.length);
|
|
3520
|
+
for (const replacement of rewrites) {
|
|
3521
|
+
const candidate = base + replacement;
|
|
3522
|
+
if (trackedPaths.has(candidate)) return candidate;
|
|
3523
|
+
}
|
|
3524
|
+
return null;
|
|
3525
|
+
}
|
|
3526
|
+
if (ext) return null;
|
|
3527
|
+
for (const suffix of APPEND_EXTENSIONS) {
|
|
3528
|
+
const candidate = joined + suffix;
|
|
3529
|
+
if (trackedPaths.has(candidate)) return candidate;
|
|
3530
|
+
}
|
|
3531
|
+
for (const indexFile of INDEX_FILES) {
|
|
3532
|
+
const candidate = posix.join(joined, indexFile);
|
|
3533
|
+
if (trackedPaths.has(candidate)) return candidate;
|
|
3534
|
+
}
|
|
3535
|
+
return null;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
// src/structure/collect.ts
|
|
3539
|
+
var TRACKED_PATHSPECS = ["*.ts", "*.tsx", "*.js", "*.jsx"];
|
|
3540
|
+
async function collectFileEdges(repoRoot) {
|
|
3541
|
+
const out = await git(repoRoot, ["ls-files", "--", ...TRACKED_PATHSPECS]);
|
|
3542
|
+
const paths = out.split("\n").map((line) => line.trim().replace(/\\/g, "/")).filter(Boolean);
|
|
3543
|
+
const trackedPaths = new Set(paths);
|
|
3544
|
+
const edges = [];
|
|
3545
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
3546
|
+
const unreadable = [];
|
|
3547
|
+
for (const path of paths) {
|
|
3548
|
+
let content;
|
|
3549
|
+
try {
|
|
3550
|
+
content = await readFile9(join8(repoRoot, path), "utf8");
|
|
3551
|
+
} catch {
|
|
3552
|
+
unreadable.push(path);
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3555
|
+
for (const specifier of extractImportSpecifiers(content)) {
|
|
3556
|
+
const target = resolveSpecifier(path, specifier, trackedPaths);
|
|
3557
|
+
if (!target || target === path) continue;
|
|
3558
|
+
const key = `${path}\0${target}`;
|
|
3559
|
+
if (seenEdges.has(key)) continue;
|
|
3560
|
+
seenEdges.add(key);
|
|
3561
|
+
edges.push({ fromPath: path, toPath: target, kind: "import" });
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
return { edges, filesScanned: paths.length, unreadable };
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3267
3567
|
// src/vector/sync.ts
|
|
3268
3568
|
var EMBEDDING_IDENTITY_KEY = "embedding.identity";
|
|
3269
3569
|
var DEFAULT_BATCH_SIZE = 32;
|
|
@@ -3358,25 +3658,25 @@ function addStats(into, from) {
|
|
|
3358
3658
|
async function syncGit(store, projectId, opts, repo, config, log) {
|
|
3359
3659
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
3360
3660
|
if (!repo.head) {
|
|
3361
|
-
log(`${
|
|
3661
|
+
log(`${pc5.yellow("git")} skipped -- repository has no commits yet`);
|
|
3362
3662
|
return { totals, seen: 0 };
|
|
3363
3663
|
}
|
|
3364
3664
|
if (!config.sources.git.enabled) {
|
|
3365
|
-
log(`${
|
|
3665
|
+
log(`${pc5.dim("git")} disabled in config`);
|
|
3366
3666
|
return { totals, seen: 0 };
|
|
3367
3667
|
}
|
|
3368
3668
|
let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GIT_SOURCE);
|
|
3369
3669
|
if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
|
|
3370
|
-
log(`${
|
|
3670
|
+
log(`${pc5.yellow("git cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a full walk`);
|
|
3371
3671
|
cursor = null;
|
|
3372
3672
|
}
|
|
3373
3673
|
if (cursor === repo.head) {
|
|
3374
|
-
log(`${
|
|
3674
|
+
log(`${pc5.green("git up to date")} at ${repo.head.slice(0, 7)}`);
|
|
3375
3675
|
store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
|
|
3376
3676
|
return { totals, seen: 0 };
|
|
3377
3677
|
}
|
|
3378
3678
|
log(
|
|
3379
|
-
`${
|
|
3679
|
+
`${pc5.dim("git syncing")} ${repo.branch ?? "HEAD"} ${cursor ? `${cursor.slice(0, 7)}..${repo.head.slice(0, 7)}` : "(full history)"}`
|
|
3380
3680
|
);
|
|
3381
3681
|
let batch = [];
|
|
3382
3682
|
let seen = 0;
|
|
@@ -3384,7 +3684,7 @@ async function syncGit(store, projectId, opts, repo, config, log) {
|
|
|
3384
3684
|
if (batch.length === 0) return;
|
|
3385
3685
|
addStats(totals, store.upsertNodes(batch));
|
|
3386
3686
|
batch = [];
|
|
3387
|
-
log(` ${
|
|
3687
|
+
log(` ${pc5.dim(`${seen} commits read, ${totals.inserted} new`)}`);
|
|
3388
3688
|
};
|
|
3389
3689
|
const nodes = collectGitCommits(repo.root, projectId, {
|
|
3390
3690
|
afterCommit: cursor,
|
|
@@ -3406,12 +3706,12 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
|
|
|
3406
3706
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
3407
3707
|
if (!repo.head) return { totals, seen: 0 };
|
|
3408
3708
|
if (!config.sources.diff.enabled) {
|
|
3409
|
-
log(`${
|
|
3709
|
+
log(`${pc5.dim("diff")} disabled in config`);
|
|
3410
3710
|
return { totals, seen: 0 };
|
|
3411
3711
|
}
|
|
3412
3712
|
let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, DIFF_SOURCE);
|
|
3413
3713
|
if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
|
|
3414
|
-
log(`${
|
|
3714
|
+
log(`${pc5.yellow("diff cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a bounded walk`);
|
|
3415
3715
|
cursor = null;
|
|
3416
3716
|
}
|
|
3417
3717
|
if (cursor === repo.head) {
|
|
@@ -3440,13 +3740,13 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
|
|
|
3440
3740
|
}
|
|
3441
3741
|
flush();
|
|
3442
3742
|
store.setSyncCursor(projectId, DIFF_SOURCE, repo.head);
|
|
3443
|
-
log(` ${
|
|
3743
|
+
log(` ${pc5.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
|
|
3444
3744
|
return { totals, seen };
|
|
3445
3745
|
}
|
|
3446
3746
|
async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
3447
3747
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
3448
3748
|
if (!config.sources.shell.enabled) {
|
|
3449
|
-
log(`${
|
|
3749
|
+
log(`${pc5.dim("shell")} disabled in config`);
|
|
3450
3750
|
return { totals, seen: 0 };
|
|
3451
3751
|
}
|
|
3452
3752
|
const results = await collectAvailableShellHistory({
|
|
@@ -3455,7 +3755,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
3455
3755
|
hookCursor: store.getSyncCursor(projectId, "shell:pwsh-hook")
|
|
3456
3756
|
});
|
|
3457
3757
|
if (results.length === 0) {
|
|
3458
|
-
log(`${
|
|
3758
|
+
log(`${pc5.dim("shell")} no history source found on this machine`);
|
|
3459
3759
|
return { totals, seen: 0 };
|
|
3460
3760
|
}
|
|
3461
3761
|
let seen = 0;
|
|
@@ -3467,7 +3767,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
3467
3767
|
addStats(totals, store.upsertNodes(nodes));
|
|
3468
3768
|
}
|
|
3469
3769
|
store.setSyncCursor(projectId, sourceKey, result.cursorAfter ?? `scanned:${result.entries.length}`);
|
|
3470
|
-
log(` ${
|
|
3770
|
+
log(` ${pc5.dim(`${sourceKey}: ${nodes.length} entr${nodes.length === 1 ? "y" : "ies"} read`)}`);
|
|
3471
3771
|
}
|
|
3472
3772
|
return { totals, seen };
|
|
3473
3773
|
}
|
|
@@ -3479,13 +3779,13 @@ function syncConversation(store, projectId, turns, config, log, forceEnabled) {
|
|
|
3479
3779
|
return { totals, seen: 0 };
|
|
3480
3780
|
}
|
|
3481
3781
|
if (turns.length === 0) {
|
|
3482
|
-
log(`${
|
|
3782
|
+
log(`${pc5.dim("conversation")} no transcripts found`);
|
|
3483
3783
|
return { totals, seen: 0 };
|
|
3484
3784
|
}
|
|
3485
3785
|
const nodes = collectConversationTurns(turns, projectId, { maxBodyChars: config.limits.maxBodyChars });
|
|
3486
3786
|
if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
|
|
3487
3787
|
store.setSyncCursor(projectId, CONVERSATION_SOURCE, `scanned:${nodes.length}`);
|
|
3488
|
-
log(` ${
|
|
3788
|
+
log(` ${pc5.dim(`${CONVERSATION_SOURCE}: ${nodes.length} of ${turns.length} exchange(s) kept`)}`);
|
|
3489
3789
|
return { totals, seen: nodes.length };
|
|
3490
3790
|
}
|
|
3491
3791
|
var SESSION_SOURCE = "session:claude-code";
|
|
@@ -3494,7 +3794,7 @@ async function syncSessions(store, projectId, turns, config, log) {
|
|
|
3494
3794
|
const settings = config.sources.session;
|
|
3495
3795
|
if (!settings.enabled) return { totals, seen: 0 };
|
|
3496
3796
|
if (turns.length === 0) {
|
|
3497
|
-
log(`${
|
|
3797
|
+
log(`${pc5.dim("session")} no transcripts found`);
|
|
3498
3798
|
return { totals, seen: 0 };
|
|
3499
3799
|
}
|
|
3500
3800
|
const result = await collectSessionSummaries(turns, projectId, new OllamaChatProvider({ model: settings.model }), {
|
|
@@ -3506,12 +3806,12 @@ async function syncSessions(store, projectId, turns, config, log) {
|
|
|
3506
3806
|
const meta = store.getNodeMeta(makeNodeId(projectId, "session_summary", sessionKey));
|
|
3507
3807
|
return typeof meta?.contentHash === "string" ? meta.contentHash : null;
|
|
3508
3808
|
},
|
|
3509
|
-
onProgress: (done, total) => log(` ${
|
|
3809
|
+
onProgress: (done, total) => log(` ${pc5.dim(`session: summarizing ${done}/${total}`)}`)
|
|
3510
3810
|
});
|
|
3511
3811
|
if (result.nodes.length > 0) addStats(totals, store.upsertNodes(result.nodes));
|
|
3512
3812
|
if (result.providerUnavailable) {
|
|
3513
3813
|
log(
|
|
3514
|
-
`${
|
|
3814
|
+
`${pc5.dim("session")} summarization model unavailable (is Ollama running with \`${settings.model}\` pulled?) -- skipped`
|
|
3515
3815
|
);
|
|
3516
3816
|
} else {
|
|
3517
3817
|
const parts = [`${result.nodes.length} summarized`];
|
|
@@ -3519,7 +3819,7 @@ async function syncSessions(store, projectId, turns, config, log) {
|
|
|
3519
3819
|
if (result.deferred > 0) parts.push(`${result.deferred} queued for the next sync`);
|
|
3520
3820
|
if (result.unsettled > 0) parts.push(`${result.unsettled} still active`);
|
|
3521
3821
|
if (result.failed > 0) parts.push(`${result.failed} failed`);
|
|
3522
|
-
log(` ${
|
|
3822
|
+
log(` ${pc5.dim(`${SESSION_SOURCE}: ${parts.join(", ")}`)}`);
|
|
3523
3823
|
}
|
|
3524
3824
|
store.setSyncCursor(projectId, SESSION_SOURCE, `scanned:${result.nodes.length}`);
|
|
3525
3825
|
return { totals, seen: result.nodes.length };
|
|
@@ -3528,7 +3828,7 @@ var DOCS_SOURCE = "docs";
|
|
|
3528
3828
|
async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
3529
3829
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
3530
3830
|
if (!config.sources.docs.enabled) {
|
|
3531
|
-
log(`${
|
|
3831
|
+
log(`${pc5.dim("docs")} disabled in config`);
|
|
3532
3832
|
return { totals, seen: 0 };
|
|
3533
3833
|
}
|
|
3534
3834
|
const { files, unreadable } = await readDocFiles(repoRoot, { include: config.sources.docs.include });
|
|
@@ -3542,14 +3842,25 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
3542
3842
|
);
|
|
3543
3843
|
store.setSyncCursor(projectId, DOCS_SOURCE, `scanned:${nodes.length}`);
|
|
3544
3844
|
if (files.length === 0 && unreadable.length === 0) {
|
|
3545
|
-
log(`${
|
|
3845
|
+
log(`${pc5.dim("docs")} no tracked .md files found`);
|
|
3546
3846
|
} else {
|
|
3547
|
-
const prunedPart = pruned > 0 ? `, ${
|
|
3847
|
+
const prunedPart = pruned > 0 ? `, ${pc5.yellow(`${pruned} stale removed`)}` : "";
|
|
3548
3848
|
const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
|
|
3549
|
-
log(` ${
|
|
3849
|
+
log(` ${pc5.dim(`${DOCS_SOURCE}: ${nodes.length} section(s) from ${files.length} file(s)`)}${prunedPart}${pc5.dim(skippedPart)}`);
|
|
3550
3850
|
}
|
|
3551
3851
|
return { totals, seen: nodes.length };
|
|
3552
3852
|
}
|
|
3853
|
+
async function syncStructure(store, projectId, repoRoot, config, log) {
|
|
3854
|
+
if (!config.sources.structure.enabled) {
|
|
3855
|
+
log(`${pc5.dim("structure")} disabled in config`);
|
|
3856
|
+
return { edges: 0, filesScanned: 0 };
|
|
3857
|
+
}
|
|
3858
|
+
const { edges, filesScanned, unreadable } = await collectFileEdges(repoRoot);
|
|
3859
|
+
store.replaceFileEdges(projectId, edges);
|
|
3860
|
+
const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (skipped)` : "";
|
|
3861
|
+
log(` ${pc5.dim(`structure: ${edges.length} edge(s) from ${filesScanned} file(s)`)}${pc5.dim(skippedPart)}`);
|
|
3862
|
+
return { edges: edges.length, filesScanned };
|
|
3863
|
+
}
|
|
3553
3864
|
var STALE_SHELL_SOURCES = ["shell:pwsh", "shell:bash", "shell:zsh"];
|
|
3554
3865
|
function collectPruneSources(opts) {
|
|
3555
3866
|
const sources = /* @__PURE__ */ new Set();
|
|
@@ -3564,15 +3875,15 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
3564
3875
|
const counts = sources.flatMap((source) => scopeIds.map((id) => ({ source, id, count: store.countSourceNodes(id, source) })));
|
|
3565
3876
|
const total = counts.reduce((sum, c) => sum + c.count, 0);
|
|
3566
3877
|
if (total === 0) {
|
|
3567
|
-
out(`${
|
|
3878
|
+
out(`${pc5.dim("prune-source")} no node(s) match ${sources.join(", ")} -- nothing to do
|
|
3568
3879
|
`);
|
|
3569
3880
|
return 0;
|
|
3570
3881
|
}
|
|
3571
|
-
const describe = (c) => ` ${
|
|
3882
|
+
const describe = (c) => ` ${pc5.dim(c.source)}${c.id !== projectId ? pc5.dim(` (prior identity ${c.id.slice(0, 8)})`) : ""}: ${c.count} node(s)`;
|
|
3572
3883
|
if (!yes) {
|
|
3573
3884
|
const lines = counts.filter((c) => c.count > 0).map(describe);
|
|
3574
3885
|
out(
|
|
3575
|
-
[`${
|
|
3886
|
+
[`${pc5.yellow("would remove")} ${total} node(s):`, ...lines, pc5.dim("re-run with --yes to actually delete these -- this cannot be undone"), ""].join(
|
|
3576
3887
|
"\n"
|
|
3577
3888
|
)
|
|
3578
3889
|
);
|
|
@@ -3581,7 +3892,7 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
3581
3892
|
let removed = 0;
|
|
3582
3893
|
for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
|
|
3583
3894
|
const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
|
|
3584
|
-
out(`${
|
|
3895
|
+
out(`${pc5.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
|
|
3585
3896
|
`);
|
|
3586
3897
|
return 0;
|
|
3587
3898
|
}
|
|
@@ -3598,7 +3909,7 @@ async function runSync(opts) {
|
|
|
3598
3909
|
store.upsertProject({ id: projectId, root: repo.root, originUrl: repo.originUrl });
|
|
3599
3910
|
if (opts.rebuild) {
|
|
3600
3911
|
const removed = store.clearProject(projectId);
|
|
3601
|
-
log(`${
|
|
3912
|
+
log(`${pc5.dim("rebuild")} dropped ${removed} existing node(s)`);
|
|
3602
3913
|
}
|
|
3603
3914
|
const staleProjectIds = store.listOtherProjectIds(projectId);
|
|
3604
3915
|
for (const staleId of staleProjectIds) {
|
|
@@ -3611,7 +3922,7 @@ async function runSync(opts) {
|
|
|
3611
3922
|
].filter((part) => part !== null);
|
|
3612
3923
|
if (parts.length > 0) {
|
|
3613
3924
|
log(
|
|
3614
|
-
`${
|
|
3925
|
+
`${pc5.yellow("reconciled")} previous project identity ${pc5.dim(staleId)} (remote URL likely changed): ${parts.join(", ")}`
|
|
3615
3926
|
);
|
|
3616
3927
|
}
|
|
3617
3928
|
}
|
|
@@ -3635,31 +3946,32 @@ async function runSync(opts) {
|
|
|
3635
3946
|
const conversation = syncConversation(store, projectId, turns, config, log, opts.conversationOverride);
|
|
3636
3947
|
const sessions = await syncSessions(store, projectId, turns, config, log);
|
|
3637
3948
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
3949
|
+
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
3638
3950
|
let embedLine = "";
|
|
3639
3951
|
if (!opts.noEmbed) {
|
|
3640
3952
|
let lastLogged = 0;
|
|
3641
3953
|
const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId, {
|
|
3642
3954
|
maxNodes: opts.embedLimit,
|
|
3643
|
-
onInvalidated: (count) => log(`${
|
|
3955
|
+
onInvalidated: (count) => log(`${pc5.yellow("vector")} embedding model changed -- dropped ${count} vector(s), re-embedding from scratch`),
|
|
3644
3956
|
onProgress: (attempted, total) => {
|
|
3645
3957
|
if (total < PROGRESS_THRESHOLD || attempted - lastLogged < PROGRESS_EVERY) return;
|
|
3646
3958
|
lastLogged = attempted;
|
|
3647
|
-
log(` ${
|
|
3959
|
+
log(` ${pc5.dim(`vector: ${attempted}/${total} embedded`)}`);
|
|
3648
3960
|
}
|
|
3649
3961
|
});
|
|
3650
3962
|
if (result.embedded > 0) {
|
|
3651
|
-
const skippedPart = result.skipped > 0 ?
|
|
3652
|
-
const remainingPart = result.remaining > 0 ?
|
|
3653
|
-
embedLine = ` ${
|
|
3963
|
+
const skippedPart = result.skipped > 0 ? pc5.dim(`, ${result.skipped} skipped`) : "";
|
|
3964
|
+
const remainingPart = result.remaining > 0 ? pc5.yellow(`, ${result.remaining} still pending`) : "";
|
|
3965
|
+
embedLine = ` ${pc5.dim(`vector: ${result.embedded} node(s) embedded`)}${skippedPart}${remainingPart}
|
|
3654
3966
|
`;
|
|
3655
3967
|
} else if (result.providerUnavailable) {
|
|
3656
|
-
log(`${
|
|
3968
|
+
log(`${pc5.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
3657
3969
|
}
|
|
3658
3970
|
}
|
|
3659
3971
|
let linkLine = "";
|
|
3660
3972
|
if (opts.linkFailures) {
|
|
3661
3973
|
const linkStats = correlateFailures(store, projectId);
|
|
3662
|
-
linkLine = ` ${
|
|
3974
|
+
linkLine = ` ${pc5.dim(`chains: ${linkStats.failuresExamined} failure(s) examined, ${linkStats.linkedByRetry} linked by retry, ${linkStats.linkedByDiscussion} by discussion`)}
|
|
3663
3975
|
`;
|
|
3664
3976
|
}
|
|
3665
3977
|
store.markSynced(projectId);
|
|
@@ -3676,11 +3988,12 @@ async function runSync(opts) {
|
|
|
3676
3988
|
const sessionPart = config.sources.session.enabled ? `, ${sessions.seen} session summar${sessions.seen === 1 ? "y" : "ies"}` : "";
|
|
3677
3989
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
3678
3990
|
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
3991
|
+
const structurePart = config.sources.structure.enabled ? `, ${structure.edges} import edge(s)` : "";
|
|
3679
3992
|
out(
|
|
3680
3993
|
[
|
|
3681
|
-
`${
|
|
3682
|
-
` ${
|
|
3683
|
-
` ${
|
|
3994
|
+
`${pc5.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
|
|
3995
|
+
` ${pc5.green(`+${totals.inserted} new`)} ${pc5.yellow(`~${totals.updated} updated`)} ${pc5.dim(`=${totals.unchanged} unchanged`)}`,
|
|
3996
|
+
` ${pc5.dim(`${stats.total} node(s) total across ${stats.distinctFiles} file path(s)`)}`,
|
|
3684
3997
|
""
|
|
3685
3998
|
].join("\n") + embedLine + linkLine
|
|
3686
3999
|
);
|
|
@@ -3875,8 +4188,116 @@ async function runMcpServer() {
|
|
|
3875
4188
|
await server.connect(transport);
|
|
3876
4189
|
}
|
|
3877
4190
|
|
|
4191
|
+
// src/cli/commands/precheck.ts
|
|
4192
|
+
import pc6 from "picocolors";
|
|
4193
|
+
|
|
4194
|
+
// src/correlate/precheck.ts
|
|
4195
|
+
var DEFAULT_RECENT_DAYS = 30;
|
|
4196
|
+
function tokensForFile(path) {
|
|
4197
|
+
const base = path.split("/").pop() ?? path;
|
|
4198
|
+
const stem = base.replace(/\.[a-zA-Z0-9]+$/, "");
|
|
4199
|
+
const words = stem.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
4200
|
+
return significantTokens(words.join(" "));
|
|
4201
|
+
}
|
|
4202
|
+
function assessFiles(store, projectId, paths, opts = {}) {
|
|
4203
|
+
const db = store.raw;
|
|
4204
|
+
const recentDays = opts.recentDays ?? DEFAULT_RECENT_DAYS;
|
|
4205
|
+
const cutoffEpoch = Date.now() - recentDays * 24 * 60 * 60 * 1e3;
|
|
4206
|
+
const findFailures = db.prepare(
|
|
4207
|
+
`SELECT n.id AS id, json_extract(n.meta, '$.command') AS command, n.ts AS ts
|
|
4208
|
+
FROM nodes_fts
|
|
4209
|
+
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
4210
|
+
WHERE nodes_fts MATCH ?
|
|
4211
|
+
AND n.project_id = ?
|
|
4212
|
+
AND n.kind = 'shell_command'
|
|
4213
|
+
AND json_extract(n.meta, '$.exitCode') IS NOT NULL
|
|
4214
|
+
AND json_extract(n.meta, '$.exitCode') != 0
|
|
4215
|
+
AND n.ts_epoch >= ?
|
|
4216
|
+
AND n.id NOT IN (SELECT from_node_id FROM node_links WHERE relation IN (?, ?))
|
|
4217
|
+
ORDER BY n.ts_epoch DESC`
|
|
4218
|
+
);
|
|
4219
|
+
const countCommits = db.prepare(
|
|
4220
|
+
`SELECT COUNT(DISTINCT nf.node_id) AS c
|
|
4221
|
+
FROM node_files nf
|
|
4222
|
+
JOIN nodes n ON n.id = nf.node_id
|
|
4223
|
+
WHERE n.project_id = ? AND n.kind = 'git_commit' AND nf.path = ? AND n.ts_epoch >= ?`
|
|
4224
|
+
);
|
|
4225
|
+
return paths.map((path) => {
|
|
4226
|
+
const tokens = filterBoilerplateTokens(db, projectId, tokensForFile(path), ["shell_command"]);
|
|
4227
|
+
const match = tokens.length > 0 ? tokens.map((t) => `"${t}"*`).join(" AND ") : null;
|
|
4228
|
+
const unresolvedFailures = match ? findFailures.all(match, projectId, cutoffEpoch, RESOLVED_BY_RETRY, RESOLVED_BY_DISCUSSION).filter((row) => Boolean(row.command)).map((row) => ({ id: row.id, command: row.command, ts: row.ts })) : [];
|
|
4229
|
+
const commitsRecent = countCommits.get(projectId, path, cutoffEpoch).c;
|
|
4230
|
+
return { path, unresolvedFailures, commitsRecent };
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
// src/cli/commands/precheck.ts
|
|
4235
|
+
var HIGH_CHURN_THRESHOLD = 4;
|
|
4236
|
+
async function stagedFiles(repoRoot) {
|
|
4237
|
+
const out = await git(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=ACM"]);
|
|
4238
|
+
return out.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
4239
|
+
}
|
|
4240
|
+
async function workingTreeFiles(repoRoot) {
|
|
4241
|
+
const out = await git(repoRoot, ["diff", "--name-only", "--diff-filter=ACM"]);
|
|
4242
|
+
return out.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
4243
|
+
}
|
|
4244
|
+
async function runPrecheck(opts) {
|
|
4245
|
+
const out = opts.out ?? ((chunk2) => void process.stderr.write(chunk2));
|
|
4246
|
+
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
4247
|
+
const targetFiles = opts.files ?? (opts.working ? await workingTreeFiles(repo.root) : await stagedFiles(repo.root));
|
|
4248
|
+
if (targetFiles.length === 0) {
|
|
4249
|
+
if (!opts.quiet) out(`${pc6.dim("precheck")} no files to check
|
|
4250
|
+
`);
|
|
4251
|
+
return 0;
|
|
4252
|
+
}
|
|
4253
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
4254
|
+
let risks;
|
|
4255
|
+
try {
|
|
4256
|
+
risks = assessFiles(store, projectId, targetFiles);
|
|
4257
|
+
} finally {
|
|
4258
|
+
store.close();
|
|
4259
|
+
}
|
|
4260
|
+
const flagged = risks.filter((r) => r.unresolvedFailures.length > 0 || r.commitsRecent >= HIGH_CHURN_THRESHOLD);
|
|
4261
|
+
if (flagged.length === 0) {
|
|
4262
|
+
if (!opts.quiet) out(`${pc6.green("precheck")} no warnings \u2014 looking good
|
|
4263
|
+
`);
|
|
4264
|
+
return 0;
|
|
4265
|
+
}
|
|
4266
|
+
out(`
|
|
4267
|
+
${pc6.bold("nexusmem precheck")}
|
|
4268
|
+
${pc6.dim("-".repeat(40))}
|
|
4269
|
+
|
|
4270
|
+
`);
|
|
4271
|
+
for (const risk of flagged) {
|
|
4272
|
+
out(` ${pc6.bold(risk.path)}
|
|
4273
|
+
`);
|
|
4274
|
+
if (risk.unresolvedFailures.length > 0) {
|
|
4275
|
+
out(` ${pc6.yellow("WARN")} What already failed here (${risk.unresolvedFailures.length} unresolved):
|
|
4276
|
+
`);
|
|
4277
|
+
for (const f of risk.unresolvedFailures.slice(0, 3)) {
|
|
4278
|
+
out(` ${pc6.dim(`${f.ts.slice(0, 10)} ${f.command.slice(0, 90)}`)}
|
|
4279
|
+
`);
|
|
4280
|
+
}
|
|
4281
|
+
if (risk.unresolvedFailures.length > 3) {
|
|
4282
|
+
out(` ${pc6.dim(`... and ${risk.unresolvedFailures.length - 3} more`)}
|
|
4283
|
+
`);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
if (risk.commitsRecent >= HIGH_CHURN_THRESHOLD) {
|
|
4287
|
+
out(` ${pc6.yellow("WARN")} high churn: ${risk.commitsRecent} commits touched this file recently
|
|
4288
|
+
`);
|
|
4289
|
+
}
|
|
4290
|
+
out("\n");
|
|
4291
|
+
}
|
|
4292
|
+
const failureCount = flagged.filter((r) => r.unresolvedFailures.length > 0).length;
|
|
4293
|
+
out(`${pc6.dim(`${flagged.length} file(s) flagged, ${failureCount} with unresolved failures.`)}
|
|
4294
|
+
`);
|
|
4295
|
+
if (opts.strict && failureCount > 0) return 1;
|
|
4296
|
+
return 0;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
3878
4299
|
// src/cli/commands/query.ts
|
|
3879
|
-
import
|
|
4300
|
+
import pc7 from "picocolors";
|
|
3880
4301
|
async function runQuery(opts) {
|
|
3881
4302
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
3882
4303
|
const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
|
|
@@ -3898,15 +4319,15 @@ async function runQuery(opts) {
|
|
|
3898
4319
|
const { bm25Count, vectorCount, hits, packed } = result;
|
|
3899
4320
|
if (opened && !opts.json) {
|
|
3900
4321
|
const searched = opened.sources.map((s) => s.label).join(", ");
|
|
3901
|
-
process.stderr.write(`${
|
|
4322
|
+
process.stderr.write(`${pc7.dim("scope ")} ${opened.sources.length} project(s): ${searched}
|
|
3902
4323
|
`);
|
|
3903
4324
|
for (const { entry } of opened.unreadable) {
|
|
3904
|
-
process.stderr.write(`${
|
|
4325
|
+
process.stderr.write(`${pc7.yellow("unreadable")} ${entry.root} -- skipped
|
|
3905
4326
|
`);
|
|
3906
4327
|
}
|
|
3907
4328
|
if (opened.missing.length > 0) {
|
|
3908
4329
|
process.stderr.write(
|
|
3909
|
-
`${
|
|
4330
|
+
`${pc7.dim("skipped")} ${opened.missing.length} registered project(s) whose database is not on disk ${pc7.dim("(nexusmem projects --prune to forget them)")}
|
|
3910
4331
|
`
|
|
3911
4332
|
);
|
|
3912
4333
|
}
|
|
@@ -3936,15 +4357,15 @@ async function runQuery(opts) {
|
|
|
3936
4357
|
return 0;
|
|
3937
4358
|
}
|
|
3938
4359
|
if (matched === 0) {
|
|
3939
|
-
process.stderr.write(`${
|
|
4360
|
+
process.stderr.write(`${pc7.yellow("no matches")} for "${opts.query}"
|
|
3940
4361
|
`);
|
|
3941
4362
|
return 0;
|
|
3942
4363
|
}
|
|
3943
4364
|
process.stderr.write(
|
|
3944
4365
|
[
|
|
3945
|
-
`${
|
|
3946
|
-
`${
|
|
3947
|
-
rawTokens > 0 ? `${
|
|
4366
|
+
`${pc7.dim("matched")} ${bm25Count} bm25${vectorCount > 0 ? ` + ${vectorCount} vector` : ""}, packed ${pc7.bold(String(packed.nodes.length))} into budget`,
|
|
4367
|
+
`${pc7.dim("tokens ")} ${packed.tokensUsed}/${packed.tokensBudget}` + (packed.droppedForBudget ? pc7.dim(` (${packed.droppedForBudget} dropped for budget)`) : "") + (packed.droppedForDiversity ? pc7.dim(` (${packed.droppedForDiversity} dropped for diversity)`) : ""),
|
|
4368
|
+
rawTokens > 0 ? `${pc7.dim("vs raw ")} ${rawTokens} tokens if these same matches were sent unpacked ${packerEfficiency >= 0 ? pc7.green(`(${(packerEfficiency * 100).toFixed(0)}% packer efficiency)`) : pc7.yellow(`(${(-packerEfficiency * 100).toFixed(0)}% larger -- overhead dominates on small result sets)`)}` : "",
|
|
3948
4369
|
""
|
|
3949
4370
|
].filter(Boolean).join("\n")
|
|
3950
4371
|
);
|
|
@@ -3958,10 +4379,10 @@ async function runQuery(opts) {
|
|
|
3958
4379
|
}
|
|
3959
4380
|
|
|
3960
4381
|
// src/cli/commands/scan-conversation.ts
|
|
3961
|
-
import
|
|
4382
|
+
import pc9 from "picocolors";
|
|
3962
4383
|
|
|
3963
4384
|
// src/cli/format.ts
|
|
3964
|
-
import
|
|
4385
|
+
import pc8 from "picocolors";
|
|
3965
4386
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
3966
4387
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
3967
4388
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
@@ -3973,9 +4394,9 @@ function signalBand(signal, bands) {
|
|
|
3973
4394
|
return "low";
|
|
3974
4395
|
}
|
|
3975
4396
|
var BAND_COLOR = {
|
|
3976
|
-
high:
|
|
3977
|
-
medium:
|
|
3978
|
-
low:
|
|
4397
|
+
high: pc8.green,
|
|
4398
|
+
medium: pc8.yellow,
|
|
4399
|
+
low: pc8.dim
|
|
3979
4400
|
};
|
|
3980
4401
|
function formatSignal(signal, bands) {
|
|
3981
4402
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -3988,9 +4409,9 @@ async function runScanConversation(opts) {
|
|
|
3988
4409
|
const files = await listTranscriptFiles(repo.root);
|
|
3989
4410
|
if (!opts.json) {
|
|
3990
4411
|
process.stderr.write(
|
|
3991
|
-
files.length ? `${
|
|
4412
|
+
files.length ? `${pc9.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
3992
4413
|
|
|
3993
|
-
` : `${
|
|
4414
|
+
` : `${pc9.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
3994
4415
|
`
|
|
3995
4416
|
);
|
|
3996
4417
|
}
|
|
@@ -4007,7 +4428,7 @@ async function runScanConversation(opts) {
|
|
|
4007
4428
|
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
4008
4429
|
process.stderr.write(
|
|
4009
4430
|
`
|
|
4010
|
-
${
|
|
4431
|
+
${pc9.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc9.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc9.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
|
|
4011
4432
|
);
|
|
4012
4433
|
return 0;
|
|
4013
4434
|
}
|
|
@@ -4016,20 +4437,20 @@ function formatNode(node) {
|
|
|
4016
4437
|
}
|
|
4017
4438
|
|
|
4018
4439
|
// src/cli/commands/scan-diff.ts
|
|
4019
|
-
import
|
|
4440
|
+
import pc11 from "picocolors";
|
|
4020
4441
|
|
|
4021
4442
|
// src/cli/commands/scan-git.ts
|
|
4022
|
-
import
|
|
4443
|
+
import pc10 from "picocolors";
|
|
4023
4444
|
async function runScanGit(opts) {
|
|
4024
4445
|
const repo = await readRepoInfo(opts.cwd);
|
|
4025
4446
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
4026
4447
|
if (!opts.json) {
|
|
4027
4448
|
process.stderr.write(
|
|
4028
4449
|
[
|
|
4029
|
-
`${
|
|
4030
|
-
`${
|
|
4031
|
-
`${
|
|
4032
|
-
`${
|
|
4450
|
+
`${pc10.dim("repo ")} ${repo.root}`,
|
|
4451
|
+
`${pc10.dim("branch ")} ${repo.branch ?? pc10.yellow("(detached)")}`,
|
|
4452
|
+
`${pc10.dim("origin ")} ${repo.originUrl ?? pc10.dim("(none)")}`,
|
|
4453
|
+
`${pc10.dim("project")} ${pc10.cyan(projectId)}`,
|
|
4033
4454
|
""
|
|
4034
4455
|
].join("\n")
|
|
4035
4456
|
);
|
|
@@ -4063,14 +4484,14 @@ function formatNode2(node) {
|
|
|
4063
4484
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
4064
4485
|
return [
|
|
4065
4486
|
formatSignal(node.signal, GIT_SIGNAL_BANDS),
|
|
4066
|
-
|
|
4067
|
-
|
|
4487
|
+
pc10.dim(date),
|
|
4488
|
+
pc10.magenta(sha),
|
|
4068
4489
|
node.title,
|
|
4069
|
-
|
|
4490
|
+
pc10.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
|
|
4070
4491
|
].join(" ");
|
|
4071
4492
|
}
|
|
4072
4493
|
function summarize2(nodes) {
|
|
4073
|
-
if (nodes.length === 0) return
|
|
4494
|
+
if (nodes.length === 0) return pc10.yellow("no commits matched");
|
|
4074
4495
|
const timestamps = nodes.map((n) => n.ts).sort();
|
|
4075
4496
|
const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
|
|
4076
4497
|
const totalTokens = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
@@ -4080,7 +4501,7 @@ function summarize2(nodes) {
|
|
|
4080
4501
|
}
|
|
4081
4502
|
const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
|
|
4082
4503
|
return [
|
|
4083
|
-
`${
|
|
4504
|
+
`${pc10.bold(String(nodes.length))} nodes ${pc10.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
|
|
4084
4505
|
` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
|
|
4085
4506
|
hottest.length ? ` hottest files:
|
|
4086
4507
|
${hottest.join("\n")}` : ""
|
|
@@ -4095,9 +4516,9 @@ async function runScanDiff(opts) {
|
|
|
4095
4516
|
if (!opts.json) {
|
|
4096
4517
|
process.stderr.write(
|
|
4097
4518
|
[
|
|
4098
|
-
`${
|
|
4099
|
-
`${
|
|
4100
|
-
`${
|
|
4519
|
+
`${pc11.dim("repo ")} ${repo.root}`,
|
|
4520
|
+
`${pc11.dim("branch ")} ${repo.branch ?? pc11.yellow("(detached)")}`,
|
|
4521
|
+
`${pc11.dim("project")} ${pc11.cyan(projectId)}`,
|
|
4101
4522
|
""
|
|
4102
4523
|
].join("\n")
|
|
4103
4524
|
);
|
|
@@ -4127,28 +4548,28 @@ function formatNode3(node) {
|
|
|
4127
4548
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
4128
4549
|
return [
|
|
4129
4550
|
formatSignal(node.signal, DIFF_SIGNAL_BANDS),
|
|
4130
|
-
|
|
4131
|
-
|
|
4551
|
+
pc11.dim(node.ts.slice(0, 10)),
|
|
4552
|
+
pc11.magenta(sha),
|
|
4132
4553
|
String(node.meta.path ?? ""),
|
|
4133
|
-
|
|
4554
|
+
pc11.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
|
|
4134
4555
|
].join(" ");
|
|
4135
4556
|
}
|
|
4136
4557
|
|
|
4137
4558
|
// src/cli/commands/scan-docs.ts
|
|
4138
|
-
import
|
|
4559
|
+
import pc12 from "picocolors";
|
|
4139
4560
|
async function runScanDocs(opts) {
|
|
4140
4561
|
const repo = await readRepoInfo(opts.cwd);
|
|
4141
4562
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
4142
4563
|
const { files, unreadable } = await readDocFiles(repo.root);
|
|
4143
4564
|
if (!opts.json) {
|
|
4144
4565
|
process.stderr.write(
|
|
4145
|
-
files.length ? `${
|
|
4566
|
+
files.length ? `${pc12.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
4146
4567
|
|
|
4147
|
-
` : `${
|
|
4568
|
+
` : `${pc12.yellow("no tracked .md files found")}
|
|
4148
4569
|
`
|
|
4149
4570
|
);
|
|
4150
4571
|
if (unreadable.length > 0) {
|
|
4151
|
-
process.stderr.write(`${
|
|
4572
|
+
process.stderr.write(`${pc12.yellow("unreadable")} ${unreadable.join(", ")}
|
|
4152
4573
|
|
|
4153
4574
|
`);
|
|
4154
4575
|
}
|
|
@@ -4164,7 +4585,7 @@ async function runScanDocs(opts) {
|
|
|
4164
4585
|
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
4165
4586
|
process.stderr.write(
|
|
4166
4587
|
`
|
|
4167
|
-
${
|
|
4588
|
+
${pc12.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc12.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
4168
4589
|
`
|
|
4169
4590
|
);
|
|
4170
4591
|
return 0;
|
|
@@ -4174,13 +4595,13 @@ function formatNode4(node) {
|
|
|
4174
4595
|
}
|
|
4175
4596
|
|
|
4176
4597
|
// src/cli/commands/scan-session.ts
|
|
4177
|
-
import
|
|
4598
|
+
import pc13 from "picocolors";
|
|
4178
4599
|
async function runScanSession(opts) {
|
|
4179
4600
|
const repo = await readRepoInfo(opts.cwd);
|
|
4180
4601
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
4181
4602
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
4182
4603
|
if (turns.length === 0) {
|
|
4183
|
-
process.stderr.write(`${
|
|
4604
|
+
process.stderr.write(`${pc13.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
4184
4605
|
`);
|
|
4185
4606
|
return 0;
|
|
4186
4607
|
}
|
|
@@ -4188,7 +4609,7 @@ async function runScanSession(opts) {
|
|
|
4188
4609
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
4189
4610
|
if (!opts.json) {
|
|
4190
4611
|
process.stderr.write(
|
|
4191
|
-
`${
|
|
4612
|
+
`${pc13.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
4192
4613
|
|
|
4193
4614
|
`
|
|
4194
4615
|
);
|
|
@@ -4214,7 +4635,7 @@ async function runScanSession(opts) {
|
|
|
4214
4635
|
}
|
|
4215
4636
|
for (const preview of previews) {
|
|
4216
4637
|
process.stdout.write(
|
|
4217
|
-
`${
|
|
4638
|
+
`${pc13.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
4218
4639
|
${preview.prompt}
|
|
4219
4640
|
|
|
4220
4641
|
`
|
|
@@ -4226,7 +4647,7 @@ ${preview.prompt}
|
|
|
4226
4647
|
settleMinutes: opts.settleMinutes,
|
|
4227
4648
|
maxSessions: opts.maxSessions,
|
|
4228
4649
|
onProgress: (done, total) => {
|
|
4229
|
-
if (!opts.json) process.stderr.write(` ${
|
|
4650
|
+
if (!opts.json) process.stderr.write(` ${pc13.dim(`summarizing ${done}/${total}`)}
|
|
4230
4651
|
`);
|
|
4231
4652
|
}
|
|
4232
4653
|
});
|
|
@@ -4236,21 +4657,21 @@ ${preview.prompt}
|
|
|
4236
4657
|
return 0;
|
|
4237
4658
|
}
|
|
4238
4659
|
for (const node of result.nodes) {
|
|
4239
|
-
process.stdout.write(`${
|
|
4240
|
-
${
|
|
4660
|
+
process.stdout.write(`${pc13.bold(node.title)}
|
|
4661
|
+
${pc13.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
4241
4662
|
${node.body}
|
|
4242
4663
|
|
|
4243
4664
|
`);
|
|
4244
4665
|
}
|
|
4245
4666
|
if (result.providerUnavailable) {
|
|
4246
4667
|
process.stderr.write(
|
|
4247
|
-
`${
|
|
4668
|
+
`${pc13.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
4248
4669
|
`
|
|
4249
4670
|
);
|
|
4250
4671
|
return 0;
|
|
4251
4672
|
}
|
|
4252
4673
|
process.stderr.write(
|
|
4253
|
-
`${
|
|
4674
|
+
`${pc13.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc13.yellow(`${result.failed} failed`)}` : "") + ` ${pc13.dim(`(model ${opts.model})`)}
|
|
4254
4675
|
`
|
|
4255
4676
|
);
|
|
4256
4677
|
return 0;
|
|
@@ -4258,16 +4679,16 @@ ${node.body}
|
|
|
4258
4679
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
4259
4680
|
|
|
4260
4681
|
// src/cli/commands/scan-shell.ts
|
|
4261
|
-
import
|
|
4682
|
+
import pc14 from "picocolors";
|
|
4262
4683
|
async function runScanShell(opts) {
|
|
4263
4684
|
const repo = await readRepoInfo(opts.cwd);
|
|
4264
4685
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
4265
4686
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
4266
4687
|
if (!opts.json) {
|
|
4267
4688
|
process.stderr.write(
|
|
4268
|
-
results.length ? `${
|
|
4689
|
+
results.length ? `${pc14.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
4269
4690
|
|
|
4270
|
-
` : `${
|
|
4691
|
+
` : `${pc14.yellow("no shell history source found on this machine")}
|
|
4271
4692
|
`
|
|
4272
4693
|
);
|
|
4273
4694
|
}
|
|
@@ -4276,7 +4697,7 @@ async function runScanShell(opts) {
|
|
|
4276
4697
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
4277
4698
|
allNodes.push(...nodes);
|
|
4278
4699
|
if (!opts.json) {
|
|
4279
|
-
process.stdout.write(`${
|
|
4700
|
+
process.stdout.write(`${pc14.bold(`shell:${result.name}`)} ${pc14.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
4280
4701
|
`);
|
|
4281
4702
|
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
4282
4703
|
`);
|
|
@@ -4289,20 +4710,47 @@ async function runScanShell(opts) {
|
|
|
4289
4710
|
return 0;
|
|
4290
4711
|
}
|
|
4291
4712
|
const approxTotal = allNodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
4292
|
-
process.stderr.write(`${
|
|
4713
|
+
process.stderr.write(`${pc14.bold(String(allNodes.length))} node(s) total ${pc14.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
4293
4714
|
`);
|
|
4294
4715
|
return 0;
|
|
4295
4716
|
}
|
|
4296
4717
|
function formatNode5(node) {
|
|
4297
|
-
const approx = node.meta.tsApprox ?
|
|
4718
|
+
const approx = node.meta.tsApprox ? pc14.dim("~") : " ";
|
|
4298
4719
|
const exit = node.meta.exitCode;
|
|
4299
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
4720
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc14.red(`exit ${exit}`) : "";
|
|
4300
4721
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
4301
4722
|
}
|
|
4302
4723
|
|
|
4724
|
+
// src/cli/commands/scan-structure.ts
|
|
4725
|
+
import pc15 from "picocolors";
|
|
4726
|
+
async function runScanStructure(opts) {
|
|
4727
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
4728
|
+
const { edges, filesScanned, unreadable } = await collectFileEdges(repo.root);
|
|
4729
|
+
if (opts.json) {
|
|
4730
|
+
process.stdout.write(`${JSON.stringify(edges, null, 2)}
|
|
4731
|
+
`);
|
|
4732
|
+
return 0;
|
|
4733
|
+
}
|
|
4734
|
+
if (unreadable.length > 0) {
|
|
4735
|
+
process.stderr.write(`${pc15.yellow("unreadable")} ${unreadable.join(", ")}
|
|
4736
|
+
|
|
4737
|
+
`);
|
|
4738
|
+
}
|
|
4739
|
+
for (const edge of edges) {
|
|
4740
|
+
process.stdout.write(`${edge.fromPath} ${pc15.dim("->")} ${edge.toPath}
|
|
4741
|
+
`);
|
|
4742
|
+
}
|
|
4743
|
+
process.stderr.write(
|
|
4744
|
+
`
|
|
4745
|
+
${pc15.bold(String(edges.length))} edge(s) from ${filesScanned} tracked .ts/.tsx/.js/.jsx file(s)
|
|
4746
|
+
`
|
|
4747
|
+
);
|
|
4748
|
+
return 0;
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4303
4751
|
// src/cli/commands/status.ts
|
|
4304
4752
|
import { statSync } from "fs";
|
|
4305
|
-
import
|
|
4753
|
+
import pc16 from "picocolors";
|
|
4306
4754
|
function humanBytes(bytes) {
|
|
4307
4755
|
if (bytes < 1024) return `${bytes} B`;
|
|
4308
4756
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -4316,6 +4764,7 @@ function fileSize(path) {
|
|
|
4316
4764
|
}
|
|
4317
4765
|
}
|
|
4318
4766
|
async function runStatus(opts) {
|
|
4767
|
+
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
4319
4768
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
4320
4769
|
const store = MemoryStore.open(ws.dbPath);
|
|
4321
4770
|
try {
|
|
@@ -4324,29 +4773,37 @@ async function runStatus(opts) {
|
|
|
4324
4773
|
const gitCursor = sources.find((s) => s.source === "git")?.cursor ?? null;
|
|
4325
4774
|
const schema = currentSchemaVersion(store.raw);
|
|
4326
4775
|
const chains = getChainStats(store, projectId);
|
|
4776
|
+
const otherProjectIds = store.listOtherProjectIds(projectId);
|
|
4777
|
+
const otherProjectNodes = store.countProjectNodes(otherProjectIds);
|
|
4778
|
+
const structure = store.fileEdgeStats(projectId);
|
|
4327
4779
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
4328
4780
|
const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
4329
|
-
|
|
4781
|
+
const staleProjectWarning = otherProjectIds.length ? `${pc16.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc16.bold(
|
|
4782
|
+
"nexusmem sync --prune-source <name>"
|
|
4783
|
+
)} to remove stale source data` : "";
|
|
4784
|
+
out(
|
|
4330
4785
|
[
|
|
4331
|
-
`${
|
|
4332
|
-
`${
|
|
4333
|
-
`${
|
|
4334
|
-
`${
|
|
4335
|
-
`${
|
|
4786
|
+
`${pc16.dim("repo ")} ${repo.root}`,
|
|
4787
|
+
`${pc16.dim("branch ")} ${repo.branch ?? pc16.yellow("(detached)")}`,
|
|
4788
|
+
`${pc16.dim("project ")} ${pc16.cyan(projectId)}`,
|
|
4789
|
+
`${pc16.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc16.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
4790
|
+
`${pc16.dim("database")} ${ws.dbPath} ${pc16.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
4791
|
+
staleProjectWarning,
|
|
4336
4792
|
"",
|
|
4337
|
-
`${
|
|
4793
|
+
`${pc16.bold(String(stats.total))} node(s)${stats.total ? ` ${pc16.dim(`${stats.oldest?.slice(0, 10)} .. ${stats.newest?.slice(0, 10)}`)}` : ""}`,
|
|
4338
4794
|
...kinds,
|
|
4339
|
-
stats.total ? ` ${
|
|
4795
|
+
stats.total ? ` ${pc16.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
|
|
4340
4796
|
"",
|
|
4341
|
-
sources.length ?
|
|
4797
|
+
sources.length ? pc16.dim("sources") : pc16.yellow("no sources synced yet"),
|
|
4342
4798
|
...sources.map((s) => {
|
|
4343
4799
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
4344
4800
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
4345
|
-
return ` ${s.source.padEnd(14)} ${
|
|
4801
|
+
return ` ${s.source.padEnd(14)} ${pc16.dim(`last run ${when}`)} ${pc16.dim(`cursor ${cursorLabel}`)}`;
|
|
4346
4802
|
}),
|
|
4347
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
4803
|
+
gitCursor && gitCursor !== repo.head ? `${pc16.yellow("git behind HEAD")} \u2014 run ${pc16.bold("nexusmem sync")}` : "",
|
|
4348
4804
|
"",
|
|
4349
|
-
chains.failuresTotal ? `${
|
|
4805
|
+
chains.failuresTotal ? `${pc16.dim("chains ")} ${pc16.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc16.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc16.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
4806
|
+
structure.edges ? `${pc16.dim("structure")} ${pc16.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : ""
|
|
4350
4807
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
4351
4808
|
);
|
|
4352
4809
|
return 0;
|
|
@@ -4361,7 +4818,7 @@ function isExpected(err) {
|
|
|
4361
4818
|
// the user fixes, not stack traces they debug.
|
|
4362
4819
|
err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
|
|
4363
4820
|
// (antivirus, a bad install). Actionable, and not our stack to print.
|
|
4364
|
-
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError;
|
|
4821
|
+
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError;
|
|
4365
4822
|
}
|
|
4366
4823
|
function guard(run) {
|
|
4367
4824
|
return async () => {
|
|
@@ -4369,7 +4826,7 @@ function guard(run) {
|
|
|
4369
4826
|
process.exitCode = await run();
|
|
4370
4827
|
} catch (err) {
|
|
4371
4828
|
if (isExpected(err)) {
|
|
4372
|
-
process.stderr.write(`${
|
|
4829
|
+
process.stderr.write(`${pc17.red("error")} ${err.message}
|
|
4373
4830
|
`);
|
|
4374
4831
|
process.exitCode = 1;
|
|
4375
4832
|
return;
|
|
@@ -4422,6 +4879,14 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
4422
4879
|
new Command("remove").description("Remove the hook block from your PowerShell profile").option("--profile <path>", "override the auto-detected $PROFILE path").action((options) => guard(() => runHookRemove({ profile: options.profile }))())
|
|
4423
4880
|
).addCommand(
|
|
4424
4881
|
new Command("status").description("Show whether the hook is installed").option("--profile <path>", "override the auto-detected $PROFILE path").action((options) => guard(() => runHookStatus({ profile: options.profile }))())
|
|
4882
|
+
).addCommand(
|
|
4883
|
+
new Command("git").description("Manage the opt-in git pre-commit hook that runs `nexusmem precheck` before each commit").addCommand(
|
|
4884
|
+
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 }))())
|
|
4885
|
+
).addCommand(
|
|
4886
|
+
new Command("remove").description("Remove nexusmem's block from .git/hooks/pre-commit").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runHookGitRemove({ cwd: options.cwd }))())
|
|
4887
|
+
).addCommand(
|
|
4888
|
+
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 }))())
|
|
4889
|
+
)
|
|
4425
4890
|
);
|
|
4426
4891
|
program.command("status").description("Show what is currently remembered for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runStatus({ cwd: options.cwd }))());
|
|
4427
4892
|
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("--json", "emit the packed result as JSON on stdout", false).action(
|
|
@@ -4483,10 +4948,22 @@ program.command("scan-session").description("Preview the session summaries a loc
|
|
|
4483
4948
|
)()
|
|
4484
4949
|
);
|
|
4485
4950
|
program.command("scan-docs").description("Preview the MemoryNodes tracked .md files would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action((options) => guard(() => runScanDocs({ cwd: options.cwd, minSignal: options.minSignal, json: options.json }))());
|
|
4951
|
+
program.command("precheck").description("Warn about staged files with unresolved past failures or high recent churn (advisory, exits 0 unless --strict)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--files <paths...>", "check exactly these repo-relative paths instead of what is staged").option("--working", "check the working tree (unstaged changes) instead of what is staged for commit", false).option("--strict", "exit 1 when any file has an unresolved failure", false).option("-q, --quiet", "only print output when there is something to warn about", false).action(
|
|
4952
|
+
(options) => guard(
|
|
4953
|
+
() => runPrecheck({
|
|
4954
|
+
cwd: options.cwd,
|
|
4955
|
+
files: options.files,
|
|
4956
|
+
working: options.working,
|
|
4957
|
+
strict: options.strict,
|
|
4958
|
+
quiet: options.quiet
|
|
4959
|
+
})
|
|
4960
|
+
)()
|
|
4961
|
+
);
|
|
4962
|
+
program.command("scan-structure").description("Preview the JS/TS import-graph edges a sync would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--json", "emit edges as JSON on stdout", false).action((options) => guard(() => runScanStructure({ cwd: options.cwd, json: options.json }))());
|
|
4486
4963
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
4487
4964
|
program.parseAsync(process.argv).catch((err) => {
|
|
4488
4965
|
const message = err instanceof Error ? err.message : String(err);
|
|
4489
|
-
process.stderr.write(`${
|
|
4966
|
+
process.stderr.write(`${pc17.red("error")} ${message}
|
|
4490
4967
|
`);
|
|
4491
4968
|
process.exitCode = 1;
|
|
4492
4969
|
});
|