nexusmem 0.10.1 → 0.10.2
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 +12 -0
- package/dist/cli/index.js +78 -4
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,18 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
|
|
|
11
11
|
|
|
12
12
|
No unreleased changes yet.
|
|
13
13
|
|
|
14
|
+
## [0.10.2] — 2026-08-30
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- `shell_command` nodes never ran through the same secret-redaction pass `conversation_turn` and
|
|
19
|
+
`code_diff` nodes already get: a command like `export API_KEY=...` landed verbatim in `title`/`body`,
|
|
20
|
+
which is exactly what the FTS index and vector embeddings are built from — a secret typed at a
|
|
21
|
+
prompt could resurface later through `search_memory`/`nexusmem query`. Fixed by running `redact()`
|
|
22
|
+
over the command before it reaches those fields. `meta.command` is kept raw on purpose: project-id
|
|
23
|
+
reconciliation and failure/fix correlation both hash or exact-match against the real command text,
|
|
24
|
+
and redacting that copy too would have silently orphaned nodes on a project-id migration.
|
|
25
|
+
|
|
14
26
|
## [0.10.1] — 2026-08-30
|
|
15
27
|
|
|
16
28
|
### Added
|
package/dist/cli/index.js
CHANGED
|
@@ -3989,8 +3989,8 @@ function scoreShellCommand(entry) {
|
|
|
3989
3989
|
else if (cmd.length <= 3) score -= 0.05;
|
|
3990
3990
|
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3991
3991
|
}
|
|
3992
|
-
function renderBody2(entry, maxChars) {
|
|
3993
|
-
const parts = [`$ ${
|
|
3992
|
+
function renderBody2(entry, command, maxChars) {
|
|
3993
|
+
const parts = [`$ ${command}`];
|
|
3994
3994
|
const metaLine = [];
|
|
3995
3995
|
if (entry.cwd) metaLine.push(`cwd: ${entry.cwd}`);
|
|
3996
3996
|
if (entry.exitCode !== null) metaLine.push(`exit: ${entry.exitCode}`);
|
|
@@ -4000,7 +4000,8 @@ function renderBody2(entry, maxChars) {
|
|
|
4000
4000
|
}
|
|
4001
4001
|
function toMemoryNode3(entry, projectId, opts = {}) {
|
|
4002
4002
|
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS5;
|
|
4003
|
-
const
|
|
4003
|
+
const { text: redactedCommand } = redact(entry.command);
|
|
4004
|
+
const titleLine = redactedCommand.split(/\r?\n/)[0] ?? redactedCommand;
|
|
4004
4005
|
return {
|
|
4005
4006
|
id: makeNodeId(projectId, "shell_command", entry.naturalKey),
|
|
4006
4007
|
kind: "shell_command",
|
|
@@ -4008,7 +4009,7 @@ function toMemoryNode3(entry, projectId, opts = {}) {
|
|
|
4008
4009
|
ts: entry.ts,
|
|
4009
4010
|
source: `shell:${entry.shell}`,
|
|
4010
4011
|
title: truncate(titleLine, MAX_TITLE_CHARS7),
|
|
4011
|
-
body: renderBody2(entry, maxBody),
|
|
4012
|
+
body: renderBody2(entry, redactedCommand, maxBody),
|
|
4012
4013
|
files: [],
|
|
4013
4014
|
signal: scoreShellCommand(entry),
|
|
4014
4015
|
provenance: "observed",
|
|
@@ -5735,6 +5736,44 @@ async function getStatus(input) {
|
|
|
5735
5736
|
store.close();
|
|
5736
5737
|
}
|
|
5737
5738
|
}
|
|
5739
|
+
async function listStaleSuggestions(input) {
|
|
5740
|
+
const repo = await readRepoInfo(input.projectRoot);
|
|
5741
|
+
const ws = resolveWorkspace(repo.root);
|
|
5742
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5743
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5744
|
+
try {
|
|
5745
|
+
return { suggestions: store.listContradictionSuggestions(projectId, { limit: input.limit }) };
|
|
5746
|
+
} finally {
|
|
5747
|
+
store.close();
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
async function resolveStaleSuggestion(input) {
|
|
5751
|
+
if (input.action === "dismiss") {
|
|
5752
|
+
const repo = await readRepoInfo(input.projectRoot);
|
|
5753
|
+
const ws = resolveWorkspace(repo.root);
|
|
5754
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5755
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5756
|
+
try {
|
|
5757
|
+
const dismissed = store.dismissContradictionSuggestion(projectId, input.candidateId);
|
|
5758
|
+
return {
|
|
5759
|
+
summary: dismissed > 0 ? `dismissed the contradiction suggestion for ${input.candidateId}` : `no open contradiction suggestion for ${input.candidateId}`
|
|
5760
|
+
};
|
|
5761
|
+
} finally {
|
|
5762
|
+
store.close();
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
if (!input.againstId) {
|
|
5766
|
+
throw new Error("againstId is required to accept a stale suggestion");
|
|
5767
|
+
}
|
|
5768
|
+
const chunks = [];
|
|
5769
|
+
await runMarkStale({
|
|
5770
|
+
cwd: input.projectRoot,
|
|
5771
|
+
nodeId: input.candidateId,
|
|
5772
|
+
supersedesId: input.againstId,
|
|
5773
|
+
out: (chunk2) => chunks.push(chunk2)
|
|
5774
|
+
});
|
|
5775
|
+
return { summary: stripAnsi(chunks.join("").trim()) };
|
|
5776
|
+
}
|
|
5738
5777
|
|
|
5739
5778
|
// src/mcp/server.ts
|
|
5740
5779
|
function createServer() {
|
|
@@ -5824,6 +5863,41 @@ function createServer() {
|
|
|
5824
5863
|
};
|
|
5825
5864
|
}
|
|
5826
5865
|
);
|
|
5866
|
+
server.registerTool(
|
|
5867
|
+
"list_stale_suggestions",
|
|
5868
|
+
{
|
|
5869
|
+
title: "List open contradiction suggestions",
|
|
5870
|
+
description: 'List open contradiction verdicts for a NexusMem-tracked repository -- candidates flagged by "nexusmem stale --check-contradictions" (or automatically during sync) as likely superseded by a newer, similar node. Nothing has been written yet; use resolve_stale_suggestion to act on one.',
|
|
5871
|
+
inputSchema: {
|
|
5872
|
+
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5873
|
+
limit: z4.number().int().positive().optional().describe("Max suggestions to return, most recently judged first. Default 50.")
|
|
5874
|
+
}
|
|
5875
|
+
},
|
|
5876
|
+
async ({ projectRoot, limit }) => {
|
|
5877
|
+
const result = await listStaleSuggestions({ projectRoot, limit });
|
|
5878
|
+
return {
|
|
5879
|
+
content: [{ type: "text", text: JSON.stringify(result.suggestions, null, 2) }],
|
|
5880
|
+
structuredContent: { suggestions: result.suggestions }
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
);
|
|
5884
|
+
server.registerTool(
|
|
5885
|
+
"resolve_stale_suggestion",
|
|
5886
|
+
{
|
|
5887
|
+
title: "Accept or dismiss a contradiction suggestion",
|
|
5888
|
+
description: '"accept" writes a supersede link from candidateId to againstId (the same effect as "nexusmem mark-stale") -- the ranker down-weights candidateId from then on but never deletes it. "dismiss" silences the suggestion without changing ranking, so it stops resurfacing on future stale/sync runs.',
|
|
5889
|
+
inputSchema: {
|
|
5890
|
+
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5891
|
+
candidateId: z4.string().describe("Id of the stale/superseded node"),
|
|
5892
|
+
action: z4.enum(["accept", "dismiss"]).describe("Whether to write the supersede link or silence the suggestion"),
|
|
5893
|
+
againstId: z4.string().optional().describe('Id of the superseding node. Required for "accept", ignored for "dismiss".')
|
|
5894
|
+
}
|
|
5895
|
+
},
|
|
5896
|
+
async ({ projectRoot, candidateId, action, againstId }) => {
|
|
5897
|
+
const result = await resolveStaleSuggestion({ projectRoot, candidateId, action, againstId });
|
|
5898
|
+
return { content: [{ type: "text", text: result.summary }] };
|
|
5899
|
+
}
|
|
5900
|
+
);
|
|
5827
5901
|
return server;
|
|
5828
5902
|
}
|
|
5829
5903
|
async function runMcpServer() {
|