nexusmem 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -1
- package/LICENSE +21 -21
- package/README.md +37 -2
- package/dist/cli/index.js +331 -13
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,72 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.3.3] — 2026-08-16
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`nexusmem status` now shows failure→fix chain counts** — `N/M failure(s) resolved (X retry, Y
|
|
17
|
+
discussion)`, plus a hint to run `sync --link-failures` when failures remain unresolved. The
|
|
18
|
+
chain feature is this project's most distinctive capability, but was previously invisible to
|
|
19
|
+
anyone who didn't already know to query `node_links` directly. Backed by the new
|
|
20
|
+
`getChainStats` in `src/correlate/failure-fix.ts`, which dedupes failures resolved by both
|
|
21
|
+
heuristics rather than double-counting them.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Discussion-bridge heuristic (`sync --link-failures`): corpus-relative boilerplate tokens no
|
|
26
|
+
longer produce false-positive failure→fix links.** Re-dogfooded at larger scale against a second
|
|
27
|
+
real project's history: a command made entirely of words that saturate a project's own corpus
|
|
28
|
+
(e.g. this repo's own name/verbs) could AND-match an unrelated turn that just happened to mention
|
|
29
|
+
the same words, and bm25 score alone could not separate that from a true positive (measured: the
|
|
30
|
+
false positive scored *stronger* than two real true positives). `filterBoilerplateTokens` in
|
|
31
|
+
`src/correlate/failure-fix.ts` drops any token that appears in over 20% of a project's own
|
|
32
|
+
`conversation_turn`/`session_summary` history before building the match query — measured against
|
|
33
|
+
real data, not guessed — and skips the discussion-match attempt entirely (rather than falling back
|
|
34
|
+
unfiltered) when every token turns out to be boilerplate, since a missed link is preferred over a
|
|
35
|
+
false one for this heuristic. Below 10 discussable nodes the filter is skipped, since frequency
|
|
36
|
+
isn't a meaningful signal yet on a young project.
|
|
37
|
+
|
|
38
|
+
## [0.3.2] — 2026-08-16
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **`mcpName` field in `package.json`**, required by the official MCP registry
|
|
43
|
+
(registry.modelcontextprotocol.io) to verify that whoever publishes `server.json` under
|
|
44
|
+
`io.github.yaminbkk/nexusmem` also controls the `nexusmem` npm package itself — the registry
|
|
45
|
+
rejects a publish attempt otherwise. No behavior change for CLI/MCP users; this is purely a
|
|
46
|
+
registry-ownership proof.
|
|
47
|
+
- **`list_recent_memory` MCP tool** — chronological listing of a repository's most recently
|
|
48
|
+
remembered nodes (git commits, diffs, shell commands, docs, conversation, session summaries),
|
|
49
|
+
newest first. Distinct from `search_memory`: no query, just "what has this project's memory
|
|
50
|
+
recorded lately" — built for the VS Code extension's sidebar view, which lists rather than
|
|
51
|
+
searches. Backed by `MemoryStore.listRecentNodes`, reusing the existing `idx_nodes_project_ts`
|
|
52
|
+
index.
|
|
53
|
+
- **`sync --prune-source <name>` and `sync --prune-stale-shell`** — drop one source's nodes without a
|
|
54
|
+
full `--rebuild`, which loses history that can't be re-read from disk (the shell tail window, older
|
|
55
|
+
conversation turns). `--prune-stale-shell` is a shortcut for the three dead pre-hook shell-scrape
|
|
56
|
+
sources (`shell:pwsh`, `shell:bash`, `shell:zsh`) at once. Dry-run by default — prints the matching
|
|
57
|
+
count and does nothing until `--yes` is also given, since this is an irreversible full wipe of the
|
|
58
|
+
named source(s), unlike `--rebuild`'s no-prompt full-project reset. Also sweeps any prior project
|
|
59
|
+
identity of this same repo (the id a renamed git remote leaves behind after
|
|
60
|
+
`fix(store): reconcile memory stranded by a changed git remote URL` migrates what it can) — a
|
|
61
|
+
live-id-only prune could not reach nodes reconciliation deliberately left in place. Exposed on both
|
|
62
|
+
the CLI and the MCP `sync_project` tool.
|
|
63
|
+
- **Discussion-heuristic failure→fix chains now surface**, tightened to an AND-joined significant-
|
|
64
|
+
token match instead of the original OR match. Re-verified against this repo's own real database:
|
|
65
|
+
5/5 discussion links correct (was ~half wrong when it shipped unsurfaced in 0.3.0). Chains now
|
|
66
|
+
follow across projects in `query --all-projects` too.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **`sync_project`'s summary no longer contains raw ANSI color codes on Windows.** `runInit`/`runSync`
|
|
71
|
+
format their output with picocolors for terminal display, and picocolors treats `platform ===
|
|
72
|
+
'win32'` as sufficient evidence of color support on its own, without checking `isTTY` — correct for
|
|
73
|
+
a real terminal, wrong for the MCP JSON-RPC channel, which is piped on every platform. Found live: a
|
|
74
|
+
real MCP client (the VS Code extension's Output channel) rendered the raw escape codes as literal
|
|
75
|
+
text instead of color. Stripped at the MCP boundary in `syncProject`, leaving the CLI's own terminal
|
|
76
|
+
output untouched.
|
|
77
|
+
|
|
12
78
|
## [0.3.1] — 2026-08-15
|
|
13
79
|
|
|
14
80
|
### Added
|
|
@@ -210,7 +276,10 @@ First public release.
|
|
|
210
276
|
there is no local-model summarization pass, and the conversation collector has never been audited
|
|
211
277
|
for the stale-node bug that was found and fixed in the docs collector.
|
|
212
278
|
|
|
213
|
-
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.3.
|
|
279
|
+
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.3.3...HEAD
|
|
280
|
+
[0.3.3]: https://github.com/yaminbkk/NexusMem/compare/v0.3.2...v0.3.3
|
|
281
|
+
[0.3.2]: https://github.com/yaminbkk/NexusMem/compare/v0.3.1...v0.3.2
|
|
282
|
+
[0.3.1]: https://github.com/yaminbkk/NexusMem/compare/v0.3.0...v0.3.1
|
|
214
283
|
[0.3.0]: https://github.com/yaminbkk/NexusMem/compare/v0.2.0...v0.3.0
|
|
215
284
|
[0.2.0]: https://github.com/yaminbkk/NexusMem/compare/v0.1.2...v0.2.0
|
|
216
285
|
[0.1.2]: https://github.com/yaminbkk/NexusMem/compare/v0.1.1...v0.1.2
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 NexusMem Contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NexusMem Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -87,6 +87,40 @@ It wraps your existing PowerShell prompt rather than replacing it, is idempotent
|
|
|
87
87
|
Exit codes are what make this worth installing. A failed command is a stronger signal than a
|
|
88
88
|
successful one, and without the hook there is no way to tell them apart.
|
|
89
89
|
|
|
90
|
+
## Failure → fix chains (opt-in)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
nexusmem sync --link-failures
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
After a normal sync, this walks every failed `shell_command` (non-zero exit code) and looks for
|
|
97
|
+
whatever later resolved it, using two independent heuristics: a later command in the same project
|
|
98
|
+
and working directory, exact same normalized text, that exited `0` within 24h (**same-command
|
|
99
|
+
retry**); and, separately, the best full-text match among nearby conversation turns or session
|
|
100
|
+
summaries, requiring every significant word of the failing command to appear, not just one
|
|
101
|
+
(**conversation bridge**). A failure can be linked by either, both, or neither.
|
|
102
|
+
|
|
103
|
+
Both links are surfaced in query results. The conversation-bridge heuristic originally matched on
|
|
104
|
+
any shared word, and dogfooding against this repo's own real history found it wrong on roughly half
|
|
105
|
+
its links — a shared word as generic as "npm" was enough to link an unrelated discussion. Requiring
|
|
106
|
+
every significant word fixed that: re-dogfooded against the same corpus, every resulting link (the
|
|
107
|
+
full set produced, not a sample) checked out correct on manual review of the full text, not just the
|
|
108
|
+
summary.
|
|
109
|
+
|
|
110
|
+
When a linked failure appears in a result set, its fix rides along immediately after it, inheriting
|
|
111
|
+
the failure's own relevance score rather than needing to match the query on its own merits. That is
|
|
112
|
+
the point: a query about why something failed shouldn't need to separately guess the words used in
|
|
113
|
+
whatever fixed it. This works across projects too — `query --all-projects` chains a failure to its
|
|
114
|
+
fix using whichever project's own database recorded the link, since links are always local to the
|
|
115
|
+
project they were found in.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ nexusmem query "why did npm whoami fail"
|
|
119
|
+
|
|
120
|
+
- 2026-08-12 shell: npm whoami (exit 1)
|
|
121
|
+
- 2026-08-12 shell: npm login (exit 0) -- linked as the fix
|
|
122
|
+
```
|
|
123
|
+
|
|
90
124
|
## How retrieval works
|
|
91
125
|
|
|
92
126
|
Every source normalizes to the same `MemoryNode` shape, so a commit, a shell command and a docs
|
|
@@ -293,8 +327,9 @@ is the intended way to tune scoring against a real repository before committing
|
|
|
293
327
|
`--json` to pipe them somewhere.
|
|
294
328
|
|
|
295
329
|
Every command takes `-C <path>` to target another repository. On `sync`, `--conversation` opts the
|
|
296
|
-
transcript source in for one run without persisting it, `--no-embed` skips the vector pass,
|
|
297
|
-
`--
|
|
330
|
+
transcript source in for one run without persisting it, `--no-embed` skips the vector pass,
|
|
331
|
+
`--link-failures` builds the failure → fix chains described above, and `--rebuild` drops the
|
|
332
|
+
project's nodes and re-ingests from scratch.
|
|
298
333
|
|
|
299
334
|
## Recall across projects
|
|
300
335
|
|
package/dist/cli/index.js
CHANGED
|
@@ -699,11 +699,15 @@ import * as sqliteVec from "sqlite-vec";
|
|
|
699
699
|
// src/store/fts.ts
|
|
700
700
|
var FTS_SYNTAX = /["'()*:^{}[\]-]/g;
|
|
701
701
|
var LOW_SIGNAL_TOKENS = /* @__PURE__ */ new Set(["id"]);
|
|
702
|
-
function
|
|
702
|
+
function significantTokens(input) {
|
|
703
703
|
const tokens = input.replace(FTS_SYNTAX, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 0);
|
|
704
|
-
if (tokens.length === 0) return
|
|
704
|
+
if (tokens.length === 0) return [];
|
|
705
705
|
const signal = tokens.filter((t) => !LOW_SIGNAL_TOKENS.has(t.toLowerCase()));
|
|
706
|
-
|
|
706
|
+
return signal.length > 0 ? signal : tokens;
|
|
707
|
+
}
|
|
708
|
+
function toMatchQuery(input) {
|
|
709
|
+
const kept = significantTokens(input);
|
|
710
|
+
if (kept.length === 0) return null;
|
|
707
711
|
return kept.map((t) => `"${t}"*`).join(" OR ");
|
|
708
712
|
}
|
|
709
713
|
|
|
@@ -798,9 +802,28 @@ CREATE VIRTUAL TABLE nodes_vec USING vec0 (
|
|
|
798
802
|
embedding float[${EMBEDDING_DIM}]
|
|
799
803
|
);
|
|
800
804
|
`;
|
|
805
|
+
var V3 = `
|
|
806
|
+
-- A relation between two existing nodes, not a new content node -- the
|
|
807
|
+
-- "failure -> fix" correlation is the relationship itself, and duplicating
|
|
808
|
+
-- either side's content into a third node would just be another
|
|
809
|
+
-- independently-ranked candidate instead of the link the feature needs.
|
|
810
|
+
-- One physical table, multiple relation kinds; 'resolved_by' is the first.
|
|
811
|
+
CREATE TABLE node_links (
|
|
812
|
+
from_node_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
813
|
+
to_node_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
814
|
+
relation TEXT NOT NULL,
|
|
815
|
+
created_at INTEGER NOT NULL,
|
|
816
|
+
PRIMARY KEY (from_node_id, to_node_id, relation)
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
-- Packing a failure node needs its resolutions; nothing needs the reverse
|
|
820
|
+
-- direction yet, so only the forward lookup gets an index.
|
|
821
|
+
CREATE INDEX idx_node_links_from ON node_links (from_node_id);
|
|
822
|
+
`;
|
|
801
823
|
var MIGRATIONS = [
|
|
802
824
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
803
|
-
{ version: 2, up: (db) => db.exec(V2) }
|
|
825
|
+
{ version: 2, up: (db) => db.exec(V2) },
|
|
826
|
+
{ version: 3, up: (db) => db.exec(V3) }
|
|
804
827
|
];
|
|
805
828
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
806
829
|
function currentSchemaVersion(db) {
|
|
@@ -972,6 +995,62 @@ var MemoryStore = class _MemoryStore {
|
|
|
972
995
|
this.db.prepare("DELETE FROM sync_state WHERE project_id = ?").run(projectId);
|
|
973
996
|
return info.changes;
|
|
974
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* Record a directed relationship between two existing nodes -- e.g. a
|
|
1000
|
+
* failed `shell_command` and whatever node later resolved it
|
|
1001
|
+
* (`relation = 'resolved_by'`). A relation, not a new content node: the
|
|
1002
|
+
* correlation *is* the relationship, and duplicating either side's content
|
|
1003
|
+
* into a third node would just be another independently-ranked candidate.
|
|
1004
|
+
*
|
|
1005
|
+
* Idempotent by design (`INSERT OR IGNORE` against the table's own primary
|
|
1006
|
+
* key) so re-running a correlation pass over already-linked nodes is a
|
|
1007
|
+
* no-op, not a duplicate-row error.
|
|
1008
|
+
*/
|
|
1009
|
+
linkNodes(fromNodeId, toNodeId, relation) {
|
|
1010
|
+
this.db.prepare("INSERT OR IGNORE INTO node_links (from_node_id, to_node_id, relation, created_at) VALUES (?, ?, ?, ?)").run(fromNodeId, toNodeId, relation, Date.now());
|
|
1011
|
+
}
|
|
1012
|
+
/** Ids linked from `fromNodeId` under one relation, most recently linked first. Empty if none exist. */
|
|
1013
|
+
getLinkedNodeIds(fromNodeId, relation) {
|
|
1014
|
+
return this.db.prepare("SELECT to_node_id FROM node_links WHERE from_node_id = ? AND relation = ? ORDER BY created_at DESC").all(fromNodeId, relation).map((row) => row.to_node_id);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Hydrate full content for a set of node ids, e.g. to pack a linked
|
|
1018
|
+
* resolution alongside the failure node that points at it. Order is not
|
|
1019
|
+
* guaranteed to match `ids`; ids with no matching row are silently omitted
|
|
1020
|
+
* rather than erroring. `node_links` has `ON DELETE CASCADE` on both
|
|
1021
|
+
* columns, so an individual node delete (e.g. `reconcile.ts` migrating a
|
|
1022
|
+
* node to a freshly-computed id) removes any link pointing at the old id
|
|
1023
|
+
* along with it -- correct as a safety default, though note that reconcile
|
|
1024
|
+
* does not currently re-create the link under the migrated node's new id;
|
|
1025
|
+
* that gap is not addressed here.
|
|
1026
|
+
*/
|
|
1027
|
+
getNodesByIds(ids) {
|
|
1028
|
+
if (ids.length === 0) return [];
|
|
1029
|
+
return this.db.prepare(
|
|
1030
|
+
`SELECT id, kind, project_id AS projectId, ts, title, body, signal
|
|
1031
|
+
FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
|
|
1032
|
+
).all(JSON.stringify(ids));
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The most recently-remembered nodes for a project, newest event first --
|
|
1036
|
+
* chronology, not relevance. No `body`: a listing (e.g. a sidebar) needs
|
|
1037
|
+
* the title and enough metadata to label each row, not the full text.
|
|
1038
|
+
* `idx_nodes_project_ts` already exists for exactly this access pattern.
|
|
1039
|
+
*/
|
|
1040
|
+
listRecentNodes(projectId, limit = 20) {
|
|
1041
|
+
return this.db.prepare(
|
|
1042
|
+
`SELECT id, kind, ts, source, title, signal
|
|
1043
|
+
FROM nodes
|
|
1044
|
+
WHERE project_id = ?
|
|
1045
|
+
ORDER BY ts_epoch DESC
|
|
1046
|
+
LIMIT ?`
|
|
1047
|
+
).all(projectId, limit);
|
|
1048
|
+
}
|
|
1049
|
+
/** How many nodes of one source exist for a project. Used to preview a `pruneSourceNodes` wipe before running it. */
|
|
1050
|
+
countSourceNodes(projectId, source) {
|
|
1051
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM nodes WHERE project_id = ? AND source = ?").get(projectId, source);
|
|
1052
|
+
return row.count;
|
|
1053
|
+
}
|
|
975
1054
|
/**
|
|
976
1055
|
* Delete the nodes of one source that its latest full scan did not produce.
|
|
977
1056
|
*
|
|
@@ -1429,6 +1508,105 @@ function renderContextBlock(query, result) {
|
|
|
1429
1508
|
return lines.join("\n");
|
|
1430
1509
|
}
|
|
1431
1510
|
|
|
1511
|
+
// src/correlate/failure-fix.ts
|
|
1512
|
+
var DEFAULT_RETRY_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1513
|
+
var DEFAULT_DISCUSSION_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1514
|
+
var RESOLVED_BY_RETRY = "resolved_by:retry";
|
|
1515
|
+
var RESOLVED_BY_DISCUSSION = "resolved_by:discussion";
|
|
1516
|
+
function normalizeCommand(command) {
|
|
1517
|
+
return command.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1518
|
+
}
|
|
1519
|
+
var MAX_TOKEN_DOC_FREQUENCY = 0.2;
|
|
1520
|
+
var MIN_CORPUS_FOR_FREQUENCY_FILTER = 10;
|
|
1521
|
+
function filterBoilerplateTokens(db, projectId, tokens) {
|
|
1522
|
+
if (tokens.length === 0) return tokens;
|
|
1523
|
+
const total = db.prepare(`SELECT COUNT(*) AS c FROM nodes WHERE project_id = ? AND kind IN ('conversation_turn', 'session_summary')`).get(projectId).c;
|
|
1524
|
+
if (total < MIN_CORPUS_FOR_FREQUENCY_FILTER) return tokens;
|
|
1525
|
+
const countMatching = db.prepare(
|
|
1526
|
+
`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 ('conversation_turn', 'session_summary')`
|
|
1528
|
+
);
|
|
1529
|
+
return tokens.filter((t) => {
|
|
1530
|
+
const matching = countMatching.get(`"${t}"*`, projectId).c;
|
|
1531
|
+
return matching / total <= MAX_TOKEN_DOC_FREQUENCY;
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function correlateFailures(store, projectId, opts = {}) {
|
|
1535
|
+
const retryWindowMs = opts.retryWindowMs ?? DEFAULT_RETRY_WINDOW_MS;
|
|
1536
|
+
const discussionWindowMs = opts.discussionWindowMs ?? DEFAULT_DISCUSSION_WINDOW_MS;
|
|
1537
|
+
const db = store.raw;
|
|
1538
|
+
const failures = db.prepare(
|
|
1539
|
+
`SELECT id, ts_epoch, json_extract(meta, '$.command') AS command, json_extract(meta, '$.cwd') AS cwd
|
|
1540
|
+
FROM nodes
|
|
1541
|
+
WHERE project_id = ? AND kind = 'shell_command'
|
|
1542
|
+
AND json_extract(meta, '$.exitCode') IS NOT NULL
|
|
1543
|
+
AND json_extract(meta, '$.exitCode') != 0`
|
|
1544
|
+
).all(projectId);
|
|
1545
|
+
const findRetry = db.prepare(
|
|
1546
|
+
`SELECT id FROM nodes
|
|
1547
|
+
WHERE project_id = ? AND kind = 'shell_command'
|
|
1548
|
+
AND json_extract(meta, '$.exitCode') = 0
|
|
1549
|
+
AND ts_epoch > ? AND ts_epoch <= ?
|
|
1550
|
+
AND lower(trim(json_extract(meta, '$.command'))) = ?
|
|
1551
|
+
AND (json_extract(meta, '$.cwd') IS ? OR json_extract(meta, '$.cwd') = ?)
|
|
1552
|
+
ORDER BY ts_epoch ASC LIMIT 1`
|
|
1553
|
+
);
|
|
1554
|
+
const findDiscussion = db.prepare(
|
|
1555
|
+
`SELECT n.id FROM nodes_fts
|
|
1556
|
+
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1557
|
+
WHERE nodes_fts MATCH ? AND n.project_id = ? AND n.kind IN ('conversation_turn', 'session_summary')
|
|
1558
|
+
AND n.ts_epoch > ? AND n.ts_epoch <= ?
|
|
1559
|
+
ORDER BY bm25(nodes_fts, 10.0, 1.0)
|
|
1560
|
+
LIMIT 1`
|
|
1561
|
+
);
|
|
1562
|
+
let linkedByRetry = 0;
|
|
1563
|
+
let linkedByDiscussion = 0;
|
|
1564
|
+
for (const failure of failures) {
|
|
1565
|
+
if (!failure.command) continue;
|
|
1566
|
+
const retry = findRetry.get(
|
|
1567
|
+
projectId,
|
|
1568
|
+
failure.ts_epoch,
|
|
1569
|
+
failure.ts_epoch + retryWindowMs,
|
|
1570
|
+
normalizeCommand(failure.command),
|
|
1571
|
+
failure.cwd,
|
|
1572
|
+
failure.cwd
|
|
1573
|
+
);
|
|
1574
|
+
if (retry) {
|
|
1575
|
+
store.linkNodes(failure.id, retry.id, RESOLVED_BY_RETRY);
|
|
1576
|
+
linkedByRetry += 1;
|
|
1577
|
+
}
|
|
1578
|
+
const tokens = filterBoilerplateTokens(db, projectId, significantTokens(failure.command));
|
|
1579
|
+
const match = tokens.length > 0 ? tokens.map((t) => `"${t}"*`).join(" AND ") : null;
|
|
1580
|
+
if (match) {
|
|
1581
|
+
const discussion = findDiscussion.get(match, projectId, failure.ts_epoch, failure.ts_epoch + discussionWindowMs);
|
|
1582
|
+
if (discussion) {
|
|
1583
|
+
store.linkNodes(failure.id, discussion.id, RESOLVED_BY_DISCUSSION);
|
|
1584
|
+
linkedByDiscussion += 1;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return { failuresExamined: failures.length, linkedByRetry, linkedByDiscussion };
|
|
1589
|
+
}
|
|
1590
|
+
function getChainStats(store, projectId) {
|
|
1591
|
+
const db = store.raw;
|
|
1592
|
+
const failuresTotal = db.prepare(
|
|
1593
|
+
`SELECT COUNT(*) AS c FROM nodes
|
|
1594
|
+
WHERE project_id = ? AND kind = 'shell_command'
|
|
1595
|
+
AND json_extract(meta, '$.exitCode') IS NOT NULL AND json_extract(meta, '$.exitCode') != 0`
|
|
1596
|
+
).get(projectId).c;
|
|
1597
|
+
const countDistinctLinked = (relations) => db.prepare(
|
|
1598
|
+
`SELECT COUNT(DISTINCT nl.from_node_id) AS c
|
|
1599
|
+
FROM node_links nl JOIN nodes n ON n.id = nl.from_node_id
|
|
1600
|
+
WHERE n.project_id = ? AND nl.relation IN (${relations.map(() => "?").join(", ")})`
|
|
1601
|
+
).get(projectId, ...relations).c;
|
|
1602
|
+
return {
|
|
1603
|
+
failuresTotal,
|
|
1604
|
+
resolvedByRetry: countDistinctLinked([RESOLVED_BY_RETRY]),
|
|
1605
|
+
resolvedByDiscussion: countDistinctLinked([RESOLVED_BY_DISCUSSION]),
|
|
1606
|
+
resolvedTotal: countDistinctLinked([RESOLVED_BY_RETRY, RESOLVED_BY_DISCUSSION])
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1432
1610
|
// src/retrieval/fuse.ts
|
|
1433
1611
|
var RRF_K = 60;
|
|
1434
1612
|
function reciprocalRankFusion(lists) {
|
|
@@ -1504,6 +1682,42 @@ function rankHits(hits, opts = {}) {
|
|
|
1504
1682
|
}
|
|
1505
1683
|
|
|
1506
1684
|
// src/retrieval/query-pipeline.ts
|
|
1685
|
+
var SURFACED_RELATIONS = [RESOLVED_BY_RETRY, RESOLVED_BY_DISCUSSION];
|
|
1686
|
+
function pullLinkedResolutions(resolveStore, ranked) {
|
|
1687
|
+
const present = new Set(ranked.map((hit) => hit.id));
|
|
1688
|
+
const withLinks = [];
|
|
1689
|
+
for (const hit of ranked) {
|
|
1690
|
+
withLinks.push(hit);
|
|
1691
|
+
if (hit.kind !== "shell_command") continue;
|
|
1692
|
+
const store = resolveStore(hit);
|
|
1693
|
+
if (!store) continue;
|
|
1694
|
+
for (const relation of SURFACED_RELATIONS) {
|
|
1695
|
+
for (const linkedId of store.getLinkedNodeIds(hit.id, relation)) {
|
|
1696
|
+
if (present.has(linkedId)) continue;
|
|
1697
|
+
const [resolution] = store.getNodesByIds([linkedId]);
|
|
1698
|
+
if (!resolution) continue;
|
|
1699
|
+
present.add(linkedId);
|
|
1700
|
+
withLinks.push({
|
|
1701
|
+
id: resolution.id,
|
|
1702
|
+
kind: resolution.kind,
|
|
1703
|
+
ts: resolution.ts,
|
|
1704
|
+
title: resolution.title,
|
|
1705
|
+
body: resolution.body,
|
|
1706
|
+
signal: resolution.signal,
|
|
1707
|
+
rank: 0,
|
|
1708
|
+
// no bm25/vector rank of its own -- never read again past this point
|
|
1709
|
+
relevance: hit.relevance,
|
|
1710
|
+
signalWeight: hit.signalWeight,
|
|
1711
|
+
recencyFactor: hit.recencyFactor,
|
|
1712
|
+
ageDays: hit.ageDays,
|
|
1713
|
+
score: hit.score,
|
|
1714
|
+
...hit.project ? { project: hit.project } : {}
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return withLinks;
|
|
1720
|
+
}
|
|
1507
1721
|
async function runCrossProjectQuery(sources, query, opts) {
|
|
1508
1722
|
const queryVector = opts.embeddingProvider ? await opts.embeddingProvider.embed(query) : null;
|
|
1509
1723
|
const lists = [];
|
|
@@ -1524,8 +1738,12 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
1524
1738
|
}
|
|
1525
1739
|
hits.push(...mergeSearchAndVectorHits(bm25Hits, vectorHits).map(label));
|
|
1526
1740
|
}
|
|
1741
|
+
const storeByLabel = new Map(sources.map((source) => [source.label, source.store]));
|
|
1527
1742
|
const relevanceScores = reciprocalRankFusion(lists);
|
|
1528
|
-
const ranked =
|
|
1743
|
+
const ranked = pullLinkedResolutions(
|
|
1744
|
+
(hit) => hit.project ? storeByLabel.get(hit.project) : void 0,
|
|
1745
|
+
rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
|
|
1746
|
+
);
|
|
1529
1747
|
const packed = packContext(ranked, opts.budget, { query });
|
|
1530
1748
|
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
1531
1749
|
}
|
|
@@ -1538,7 +1756,10 @@ async function runHybridQuery(store, projectId, query, opts) {
|
|
|
1538
1756
|
}
|
|
1539
1757
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
1540
1758
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
1541
|
-
const ranked =
|
|
1759
|
+
const ranked = pullLinkedResolutions(
|
|
1760
|
+
() => store,
|
|
1761
|
+
rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
|
|
1762
|
+
);
|
|
1542
1763
|
const packed = packContext(ranked, opts.budget, { query });
|
|
1543
1764
|
return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
|
|
1544
1765
|
}
|
|
@@ -3329,6 +3550,41 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
3329
3550
|
}
|
|
3330
3551
|
return { totals, seen: nodes.length };
|
|
3331
3552
|
}
|
|
3553
|
+
var STALE_SHELL_SOURCES = ["shell:pwsh", "shell:bash", "shell:zsh"];
|
|
3554
|
+
function collectPruneSources(opts) {
|
|
3555
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3556
|
+
if (opts.pruneStaleShell) {
|
|
3557
|
+
for (const source of STALE_SHELL_SOURCES) sources.add(source);
|
|
3558
|
+
}
|
|
3559
|
+
if (opts.pruneSource?.trim()) sources.add(opts.pruneSource.trim());
|
|
3560
|
+
return [...sources];
|
|
3561
|
+
}
|
|
3562
|
+
function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
3563
|
+
const scopeIds = [projectId, ...otherProjectIds];
|
|
3564
|
+
const counts = sources.flatMap((source) => scopeIds.map((id) => ({ source, id, count: store.countSourceNodes(id, source) })));
|
|
3565
|
+
const total = counts.reduce((sum, c) => sum + c.count, 0);
|
|
3566
|
+
if (total === 0) {
|
|
3567
|
+
out(`${pc4.dim("prune-source")} no node(s) match ${sources.join(", ")} -- nothing to do
|
|
3568
|
+
`);
|
|
3569
|
+
return 0;
|
|
3570
|
+
}
|
|
3571
|
+
const describe = (c) => ` ${pc4.dim(c.source)}${c.id !== projectId ? pc4.dim(` (prior identity ${c.id.slice(0, 8)})`) : ""}: ${c.count} node(s)`;
|
|
3572
|
+
if (!yes) {
|
|
3573
|
+
const lines = counts.filter((c) => c.count > 0).map(describe);
|
|
3574
|
+
out(
|
|
3575
|
+
[`${pc4.yellow("would remove")} ${total} node(s):`, ...lines, pc4.dim("re-run with --yes to actually delete these -- this cannot be undone"), ""].join(
|
|
3576
|
+
"\n"
|
|
3577
|
+
)
|
|
3578
|
+
);
|
|
3579
|
+
return 0;
|
|
3580
|
+
}
|
|
3581
|
+
let removed = 0;
|
|
3582
|
+
for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
|
|
3583
|
+
const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
|
|
3584
|
+
out(`${pc4.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
|
|
3585
|
+
`);
|
|
3586
|
+
return 0;
|
|
3587
|
+
}
|
|
3332
3588
|
async function runSync(opts) {
|
|
3333
3589
|
const { repo, ws, projectId, config } = await loadContext(opts.cwd);
|
|
3334
3590
|
const log = (line) => {
|
|
@@ -3367,6 +3623,10 @@ async function runSync(opts) {
|
|
|
3367
3623
|
if (config.projectId !== projectId) await writeConfig(ws, { ...config, projectId });
|
|
3368
3624
|
}
|
|
3369
3625
|
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
3626
|
+
const pruneSources = collectPruneSources(opts);
|
|
3627
|
+
if (pruneSources.length > 0) {
|
|
3628
|
+
return runPruneSources(store, projectId, staleProjectIds, pruneSources, opts.yes ?? false, out);
|
|
3629
|
+
}
|
|
3370
3630
|
const git2 = await syncGit(store, projectId, opts, repo, config, log);
|
|
3371
3631
|
const diffs = await syncDiffs(store, projectId, opts, repo, config, log);
|
|
3372
3632
|
const shell = await syncShell(store, projectId, opts, repo.root, config, log);
|
|
@@ -3396,6 +3656,12 @@ async function runSync(opts) {
|
|
|
3396
3656
|
log(`${pc4.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
3397
3657
|
}
|
|
3398
3658
|
}
|
|
3659
|
+
let linkLine = "";
|
|
3660
|
+
if (opts.linkFailures) {
|
|
3661
|
+
const linkStats = correlateFailures(store, projectId);
|
|
3662
|
+
linkLine = ` ${pc4.dim(`chains: ${linkStats.failuresExamined} failure(s) examined, ${linkStats.linkedByRetry} linked by retry, ${linkStats.linkedByDiscussion} by discussion`)}
|
|
3663
|
+
`;
|
|
3664
|
+
}
|
|
3399
3665
|
store.markSynced(projectId);
|
|
3400
3666
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
3401
3667
|
addStats(totals, git2.totals);
|
|
@@ -3416,7 +3682,7 @@ async function runSync(opts) {
|
|
|
3416
3682
|
` ${pc4.green(`+${totals.inserted} new`)} ${pc4.yellow(`~${totals.updated} updated`)} ${pc4.dim(`=${totals.unchanged} unchanged`)}`,
|
|
3417
3683
|
` ${pc4.dim(`${stats.total} node(s) total across ${stats.distinctFiles} file path(s)`)}`,
|
|
3418
3684
|
""
|
|
3419
|
-
].join("\n") + embedLine
|
|
3685
|
+
].join("\n") + embedLine + linkLine
|
|
3420
3686
|
);
|
|
3421
3687
|
return 0;
|
|
3422
3688
|
} finally {
|
|
@@ -3481,10 +3747,27 @@ async function syncProject(input) {
|
|
|
3481
3747
|
rebuild: false,
|
|
3482
3748
|
quiet: true,
|
|
3483
3749
|
noEmbed: input.noEmbed,
|
|
3750
|
+
pruneSource: input.pruneSource,
|
|
3751
|
+
pruneStaleShell: input.pruneStaleShell,
|
|
3752
|
+
yes: input.yes,
|
|
3484
3753
|
out
|
|
3485
3754
|
};
|
|
3486
3755
|
await runSync(opts);
|
|
3487
|
-
return { summary: chunks.join("").trim() };
|
|
3756
|
+
return { summary: stripAnsi(chunks.join("").trim()) };
|
|
3757
|
+
}
|
|
3758
|
+
function stripAnsi(text) {
|
|
3759
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3760
|
+
}
|
|
3761
|
+
async function listRecentMemory(input) {
|
|
3762
|
+
const repo = await readRepoInfo(input.projectRoot);
|
|
3763
|
+
const ws = resolveWorkspace(repo.root);
|
|
3764
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
3765
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
3766
|
+
try {
|
|
3767
|
+
return { items: store.listRecentNodes(projectId, input.limit) };
|
|
3768
|
+
} finally {
|
|
3769
|
+
store.close();
|
|
3770
|
+
}
|
|
3488
3771
|
}
|
|
3489
3772
|
async function getStatus(input) {
|
|
3490
3773
|
const repo = await readRepoInfo(input.projectRoot);
|
|
@@ -3536,13 +3819,16 @@ function createServer() {
|
|
|
3536
3819
|
"sync_project",
|
|
3537
3820
|
{
|
|
3538
3821
|
title: "Sync remembered history",
|
|
3539
|
-
description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database.",
|
|
3822
|
+
description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database. Pass pruneSource or pruneStaleShell instead to delete a dead source's nodes (e.g. the pre-hook shell scrape) rather than syncing -- dry-run unless yes is also true, since this is an irreversible full wipe of that source.",
|
|
3540
3823
|
inputSchema: {
|
|
3541
|
-
projectRoot: z3.string().describe("Absolute path to the repository root")
|
|
3824
|
+
projectRoot: z3.string().describe("Absolute path to the repository root"),
|
|
3825
|
+
pruneSource: z3.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
|
|
3826
|
+
pruneStaleShell: z3.boolean().optional().describe("Shortcut for pruneSource on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources"),
|
|
3827
|
+
yes: z3.boolean().optional().describe("Confirms the delete. Without it, pruneSource/pruneStaleShell only report the matching count.")
|
|
3542
3828
|
}
|
|
3543
3829
|
},
|
|
3544
|
-
async ({ projectRoot }) => {
|
|
3545
|
-
const result = await syncProject({ projectRoot });
|
|
3830
|
+
async ({ projectRoot, pruneSource, pruneStaleShell, yes }) => {
|
|
3831
|
+
const result = await syncProject({ projectRoot, pruneSource, pruneStaleShell, yes });
|
|
3546
3832
|
return { content: [{ type: "text", text: result.summary }] };
|
|
3547
3833
|
}
|
|
3548
3834
|
);
|
|
@@ -3563,6 +3849,24 @@ function createServer() {
|
|
|
3563
3849
|
};
|
|
3564
3850
|
}
|
|
3565
3851
|
);
|
|
3852
|
+
server.registerTool(
|
|
3853
|
+
"list_recent_memory",
|
|
3854
|
+
{
|
|
3855
|
+
title: "List recently remembered items",
|
|
3856
|
+
description: "List the most recently remembered items for a NexusMem-tracked repository -- git commits, code diffs, shell commands, tracked docs, and (if enabled) conversation transcripts and session summaries -- newest first. Chronological, not relevance-ranked: use search_memory instead for a specific question.",
|
|
3857
|
+
inputSchema: {
|
|
3858
|
+
projectRoot: z3.string().describe("Absolute path to the repository root"),
|
|
3859
|
+
limit: z3.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
|
|
3860
|
+
}
|
|
3861
|
+
},
|
|
3862
|
+
async ({ projectRoot, limit }) => {
|
|
3863
|
+
const result = await listRecentMemory({ projectRoot, limit });
|
|
3864
|
+
return {
|
|
3865
|
+
content: [{ type: "text", text: JSON.stringify(result.items, null, 2) }],
|
|
3866
|
+
structuredContent: { items: result.items }
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
);
|
|
3566
3870
|
return server;
|
|
3567
3871
|
}
|
|
3568
3872
|
async function runMcpServer() {
|
|
@@ -4019,6 +4323,7 @@ async function runStatus(opts) {
|
|
|
4019
4323
|
const sources = store.listSyncState(projectId);
|
|
4020
4324
|
const gitCursor = sources.find((s) => s.source === "git")?.cursor ?? null;
|
|
4021
4325
|
const schema = currentSchemaVersion(store.raw);
|
|
4326
|
+
const chains = getChainStats(store, projectId);
|
|
4022
4327
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
4023
4328
|
const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
4024
4329
|
process.stdout.write(
|
|
@@ -4040,7 +4345,8 @@ async function runStatus(opts) {
|
|
|
4040
4345
|
return ` ${s.source.padEnd(14)} ${pc13.dim(`last run ${when}`)} ${pc13.dim(`cursor ${cursorLabel}`)}`;
|
|
4041
4346
|
}),
|
|
4042
4347
|
gitCursor && gitCursor !== repo.head ? `${pc13.yellow("git behind HEAD")} \u2014 run ${pc13.bold("nexusmem sync")}` : "",
|
|
4043
|
-
""
|
|
4348
|
+
"",
|
|
4349
|
+
chains.failuresTotal ? `${pc13.dim("chains ")} ${pc13.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc13.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc13.bold("nexusmem sync --link-failures")} to link more` : ""}` : ""
|
|
4044
4350
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
4045
4351
|
);
|
|
4046
4352
|
return 0;
|
|
@@ -4083,6 +4389,14 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
4083
4389
|
"--embed-limit <count>",
|
|
4084
4390
|
"stop embedding after this many nodes (default: embed everything pending)",
|
|
4085
4391
|
(v) => Number.parseInt(v, 10)
|
|
4392
|
+
).option("--prune-source <name>", "delete every node from this exact source (e.g. shell:pwsh) instead of syncing -- dry-run unless --yes is also given").option(
|
|
4393
|
+
"--prune-stale-shell",
|
|
4394
|
+
"shortcut for --prune-source on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources -- dry-run unless --yes is also given",
|
|
4395
|
+
false
|
|
4396
|
+
).option("--yes", "confirm an irreversible --prune-source/--prune-stale-shell delete", false).option(
|
|
4397
|
+
"--link-failures",
|
|
4398
|
+
"opt-in (experimental): after ingest, link failed shell commands to whatever later resolved them",
|
|
4399
|
+
false
|
|
4086
4400
|
).option("-q, --quiet", "only print the final summary", false).action(
|
|
4087
4401
|
(options) => guard(
|
|
4088
4402
|
() => runSync({
|
|
@@ -4094,6 +4408,10 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
4094
4408
|
conversationOverride: options.conversation ? true : void 0,
|
|
4095
4409
|
noEmbed: !options.embed,
|
|
4096
4410
|
embedLimit: options.embedLimit,
|
|
4411
|
+
pruneSource: options.pruneSource,
|
|
4412
|
+
pruneStaleShell: options.pruneStaleShell,
|
|
4413
|
+
yes: options.yes,
|
|
4414
|
+
linkFailures: options.linkFailures,
|
|
4097
4415
|
quiet: options.quiet
|
|
4098
4416
|
})
|
|
4099
4417
|
)()
|