nexusmem 0.10.4 → 0.11.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 +768 -651
- package/LICENSE +21 -21
- package/README.md +605 -562
- package/dist/cli/agent-hook.js +450 -0
- package/dist/cli/agent-hook.js.map +1 -0
- package/dist/cli/index.js +1904 -291
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recorder.js +225 -0
- package/dist/cli/recorder.js.map +1 -0
- package/package.json +71 -70
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/shell/recorder.ts
|
|
4
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
|
|
7
|
+
// src/conversation/redact.ts
|
|
8
|
+
var REDACTION_MARK = "[redacted]";
|
|
9
|
+
var MARK = REDACTION_MARK;
|
|
10
|
+
var NOT_MARK = String.raw`(?!\[redacted\])`;
|
|
11
|
+
var keepPrefix = (groups) => `${groups[0]}${MARK}`;
|
|
12
|
+
var SECRET_KEYWORD = String.raw`(?:(?:pass(?:word|wd|phrase)|secret|credential|api[_-]?key|(?:access|private|secret|client|signing|encryption|master)[_-]?key)s?|token)`;
|
|
13
|
+
var SECRET_KEY = String.raw`(?:[A-Za-z0-9_.-]{0,100}?${SECRET_KEYWORD}|(?:-{1,2}|(?:[A-Za-z0-9]{1,40}[_.-]){1,8})pass)(?:[_.-][A-Za-z0-9]{1,40}){0,8}`;
|
|
14
|
+
var TYPE_WORD = String.raw`(?:string|number|boolean|bool|int|str|null|undefined|none|nil|true|false|any|unknown|object)(?=[\s;,)|\]}>]|$)`;
|
|
15
|
+
var SECRET_VALUE = String.raw`(?:"(?:\\.|[^"\\\r\n]){1,500}"|'(?:\\.|[^'\\\r\n]){1,500}'|\x60(?:\\.|[^\x60\\\r\n]){1,500}\x60|"(?!")[^\r\n]+|'(?!')[^\r\n]+|\x60(?!\x60)[^\r\n]+|[^\s'"\x60]+)`;
|
|
16
|
+
var SAME_COMMAND = String.raw`[^\n|;&]{0,500}?`;
|
|
17
|
+
var NOT_FLAG_OR_REDIRECT = String.raw`(?![-<>|&;])`;
|
|
18
|
+
var AUTH_SCHEME = String.raw`(?:bearer|basic|token|digest|negotiate|ntlm)`;
|
|
19
|
+
var CURL_USER = String.raw`\bcurl(?=[ \t])${SAME_COMMAND}[ \t](?:-u|--user)(?:[ \t]+|=)?`;
|
|
20
|
+
var RULES = [
|
|
21
|
+
{
|
|
22
|
+
name: "private-key-block",
|
|
23
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
24
|
+
highConfidence: true
|
|
25
|
+
},
|
|
26
|
+
{ name: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g, highConfidence: true },
|
|
27
|
+
{ name: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, highConfidence: true },
|
|
28
|
+
{ name: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, highConfidence: true },
|
|
29
|
+
{
|
|
30
|
+
name: "jwt",
|
|
31
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
32
|
+
highConfidence: true
|
|
33
|
+
},
|
|
34
|
+
// scheme://user:password@host -- the password may itself contain `@`, so the host starts after the last one.
|
|
35
|
+
{
|
|
36
|
+
name: "uri-credentials",
|
|
37
|
+
pattern: new RegExp(
|
|
38
|
+
String.raw`(\b[a-z][a-z0-9+.-]{1,30}://[^\s/?#'"@:]*:)${NOT_MARK}[^\s/?#'"]+(?=@[^\s/?#'"@]*(?:[/?#\s'"]|$))`,
|
|
39
|
+
"gi"
|
|
40
|
+
),
|
|
41
|
+
highConfidence: true,
|
|
42
|
+
render: keepPrefix
|
|
43
|
+
},
|
|
44
|
+
// An option whose name ends in `bearer` (`--oauth2-bearer x`) always takes a credential, whatever
|
|
45
|
+
// the token looks like. The prose-shaped rule below cannot cover it: it needs a digit, so an
|
|
46
|
+
// all-alphabetic token would survive.
|
|
47
|
+
{
|
|
48
|
+
name: "bearer-option-arg",
|
|
49
|
+
pattern: new RegExp(
|
|
50
|
+
String.raw`(?<=^|[\s'"(])(-{1,2}[A-Za-z0-9_.-]{0,40}bearer[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
51
|
+
"gi"
|
|
52
|
+
),
|
|
53
|
+
highConfidence: true,
|
|
54
|
+
render: keepPrefix
|
|
55
|
+
},
|
|
56
|
+
// A bearer token outside a header (`Bearer x`); it must contain a digit so "bearer authentication" is left alone.
|
|
57
|
+
{
|
|
58
|
+
name: "bearer-token",
|
|
59
|
+
pattern: new RegExp(String.raw`(\bbearer[ \t]+)${NOT_MARK}(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{16,}=*`, "gi"),
|
|
60
|
+
highConfidence: true,
|
|
61
|
+
render: keepPrefix
|
|
62
|
+
},
|
|
63
|
+
// Tool-anchored password arguments: `-p` means port or parents almost everywhere else. Case-sensitive (`-P` is mysql's port).
|
|
64
|
+
// mysql takes only the compact `-psecret` form; `-p secret` means "prompt, then use database secret".
|
|
65
|
+
{
|
|
66
|
+
name: "mysql-password-arg",
|
|
67
|
+
pattern: new RegExp(
|
|
68
|
+
String.raw`(\b(?:mysql|mysqldump|mysqladmin|mysqlimport|mysqlsh|mariadb|mariadb-dump)(?=[ \t])${SAME_COMMAND}[ \t]-p)${NOT_MARK}(?=\S)${SECRET_VALUE}`,
|
|
69
|
+
"g"
|
|
70
|
+
),
|
|
71
|
+
highConfidence: true,
|
|
72
|
+
render: keepPrefix
|
|
73
|
+
},
|
|
74
|
+
// Only sshpass's own options may precede -p, so a wrapped `ssh -p 22` is never read as the password.
|
|
75
|
+
{
|
|
76
|
+
name: "sshpass-password-arg",
|
|
77
|
+
pattern: new RegExp(String.raw`(\bsshpass(?:[ \t]+-[^p\s]\S*)*[ \t]+-p[ \t]*)${NOT_MARK}(?=\S)${SECRET_VALUE}`, "g"),
|
|
78
|
+
highConfidence: true,
|
|
79
|
+
render: keepPrefix
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "mongo-password-arg",
|
|
83
|
+
pattern: new RegExp(
|
|
84
|
+
String.raw`(\bmongo(?:sh|dump|restore|export|import|stat|top|files)?(?=[ \t])${SAME_COMMAND}[ \t]-p[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
85
|
+
"g"
|
|
86
|
+
),
|
|
87
|
+
highConfidence: true,
|
|
88
|
+
render: keepPrefix
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "redis-cli-password-arg",
|
|
92
|
+
pattern: new RegExp(
|
|
93
|
+
String.raw`(\bredis-cli(?=[ \t])${SAME_COMMAND}[ \t]-a[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
94
|
+
"g"
|
|
95
|
+
),
|
|
96
|
+
highConfidence: true,
|
|
97
|
+
render: keepPrefix
|
|
98
|
+
},
|
|
99
|
+
// Quoted credentials get their own branches: `curl -u "user:pass word"` holds a space, which
|
|
100
|
+
// the unquoted branch would stop at, leaving the rest of the password in the text. Like
|
|
101
|
+
// SECRET_VALUE, they step over escaped characters so `"alice:pa\"ss"` is not cut at `\"`. Inside
|
|
102
|
+
// quotes the user name may hold a space too (`"alice smith:pw"`); only the quote ends it.
|
|
103
|
+
{
|
|
104
|
+
name: "curl-user-password",
|
|
105
|
+
pattern: new RegExp(
|
|
106
|
+
String.raw`(${CURL_USER}"(?:\\.|[^:"\\\r\n])*:)${NOT_MARK}(?:\\.|[^"\\\r\n])*|(${CURL_USER}'(?:\\.|[^:'\\\r\n])*:)${NOT_MARK}(?:\\.|[^'\\\r\n])*|(${CURL_USER}[^\s:'"]*:)${NOT_MARK}[^\s'"]+`,
|
|
107
|
+
"g"
|
|
108
|
+
),
|
|
109
|
+
highConfidence: true,
|
|
110
|
+
render: (groups) => `${groups.find((g) => g.length > 0) ?? ""}${MARK}`
|
|
111
|
+
},
|
|
112
|
+
// Authorization: Bearer x / Basic x / token x. The scheme word is kept; it must not be mistaken for the value.
|
|
113
|
+
{
|
|
114
|
+
name: "authorization-header",
|
|
115
|
+
pattern: new RegExp(
|
|
116
|
+
String.raw`(\b(?:proxy-)?authorization["']?[ \t]*[:=][ \t]*["'\x60]?(?:${AUTH_SCHEME}[ \t]+)?)(?!${AUTH_SCHEME}[ \t])${NOT_MARK}[^\s'"\x60]+`,
|
|
117
|
+
"gi"
|
|
118
|
+
),
|
|
119
|
+
highConfidence: false,
|
|
120
|
+
render: keepPrefix
|
|
121
|
+
},
|
|
122
|
+
// `--password secret`: a secret-named option whose value is the next argument. The flag must start a
|
|
123
|
+
// command-line word, so prose like "the `id`-token heuristic" is not read as a `-token` option.
|
|
124
|
+
{
|
|
125
|
+
name: "secret-option-arg",
|
|
126
|
+
pattern: new RegExp(
|
|
127
|
+
String.raw`(?<=^|[\s'"(])(-{1,2}${SECRET_KEY}[ \t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,
|
|
128
|
+
"gi"
|
|
129
|
+
),
|
|
130
|
+
highConfidence: false,
|
|
131
|
+
render: keepPrefix
|
|
132
|
+
},
|
|
133
|
+
// key/token/secret/password = "value" or : value, in code, JSON, env-file, shell or prose form.
|
|
134
|
+
// The key start is an explicit non-identifier lookbehind, not `\b`: `_` is a word character, so
|
|
135
|
+
// `\b` never fired inside DB_PASSWORD and the value leaked. Found live via `scan-shell`.
|
|
136
|
+
{
|
|
137
|
+
name: "key-value-secret",
|
|
138
|
+
pattern: new RegExp(
|
|
139
|
+
String.raw`(?<![A-Za-z0-9_.-])(${SECRET_KEY})["']?[ \t]*[:=](?![=>])[ \t]*(?!${TYPE_WORD})${NOT_MARK}${SECRET_VALUE}`,
|
|
140
|
+
"gi"
|
|
141
|
+
),
|
|
142
|
+
highConfidence: false,
|
|
143
|
+
// Keep the key name so the redaction is legible ("apiKey: [redacted]").
|
|
144
|
+
render: (groups) => `${groups[0]}: ${MARK}`
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
function redact(text, profile = "all") {
|
|
148
|
+
let redactedCount = 0;
|
|
149
|
+
let out = text;
|
|
150
|
+
for (const rule of RULES) {
|
|
151
|
+
if (profile === "high-confidence" && !rule.highConfidence) continue;
|
|
152
|
+
out = out.replace(rule.pattern, (...args) => {
|
|
153
|
+
redactedCount += 1;
|
|
154
|
+
const end = args.findIndex((arg2, i) => i > 0 && typeof arg2 === "number");
|
|
155
|
+
const groups = args.slice(1, end).map((g) => typeof g === "string" ? g : "");
|
|
156
|
+
return rule.render ? rule.render(groups) : MARK;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { text: out, redactedCount };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/core/ids.ts
|
|
163
|
+
import { createHash } from "crypto";
|
|
164
|
+
function sha256Hex(input) {
|
|
165
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/shell/recorder.ts
|
|
169
|
+
var HOOK_SHELLS = /* @__PURE__ */ new Set(["pwsh-hook", "bash-hook", "zsh-hook"]);
|
|
170
|
+
var finiteOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
171
|
+
function toHookLogLine(rawEvent, shell) {
|
|
172
|
+
if (!HOOK_SHELLS.has(shell)) return null;
|
|
173
|
+
let event;
|
|
174
|
+
try {
|
|
175
|
+
event = JSON.parse(rawEvent.trim());
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (typeof event !== "object" || event === null) return null;
|
|
180
|
+
const e = event;
|
|
181
|
+
if (typeof e.ts !== "string" || typeof e.cwd !== "string" || typeof e.command !== "string" || e.command.trim() === "") return null;
|
|
182
|
+
return JSON.stringify({
|
|
183
|
+
ts: e.ts,
|
|
184
|
+
cwd: e.cwd,
|
|
185
|
+
exitCode: finiteOrNull(e.exitCode),
|
|
186
|
+
durationMs: finiteOrNull(e.durationMs),
|
|
187
|
+
command: redact(e.command).text,
|
|
188
|
+
shell,
|
|
189
|
+
// From the raw command, before redaction: the same hash hook node ids have always been derived from.
|
|
190
|
+
commandHash: sha256Hex(e.command).slice(0, 12)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function recordShellEvent(rawEvent, logPath, shell) {
|
|
194
|
+
const line = toHookLogLine(rawEvent, shell);
|
|
195
|
+
if (!line) return false;
|
|
196
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
197
|
+
await appendFile(logPath, `${line}
|
|
198
|
+
`, { encoding: "utf8", mode: 384 });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/cli/recorder.ts
|
|
203
|
+
var MAX_EVENT_BYTES = 1e6;
|
|
204
|
+
var drop = () => process.exit(1);
|
|
205
|
+
process.on("uncaughtException", drop);
|
|
206
|
+
process.on("unhandledRejection", drop);
|
|
207
|
+
function arg(name) {
|
|
208
|
+
const i = process.argv.indexOf(name);
|
|
209
|
+
return i === -1 ? void 0 : process.argv[i + 1];
|
|
210
|
+
}
|
|
211
|
+
async function main() {
|
|
212
|
+
const logPath = arg("--log");
|
|
213
|
+
const shell = arg("--shell");
|
|
214
|
+
const chunks = [];
|
|
215
|
+
let size = 0;
|
|
216
|
+
for await (const chunk of process.stdin) {
|
|
217
|
+
size += chunk.length;
|
|
218
|
+
if (size > MAX_EVENT_BYTES) drop();
|
|
219
|
+
chunks.push(chunk);
|
|
220
|
+
}
|
|
221
|
+
const ok = logPath && shell ? await recordShellEvent(Buffer.concat(chunks).toString("utf8"), logPath, shell) : false;
|
|
222
|
+
process.exit(ok ? 0 : 1);
|
|
223
|
+
}
|
|
224
|
+
main().catch(drop);
|
|
225
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shell/recorder.ts","../../src/conversation/redact.ts","../../src/core/ids.ts","../../src/cli/recorder.ts"],"sourcesContent":["import { appendFile, mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { redact } from '../conversation/redact.js';\nimport { sha256Hex } from '../core/ids.js';\n\n/**\n * The only writer of the shell hook log. A hook hands one raw command event\n * to this code over stdin; the raw command exists only in memory here --\n * hashed, then redacted -- and only the redacted line is ever persisted.\n */\n\nconst HOOK_SHELLS: ReadonlySet<string> = new Set(['pwsh-hook', 'bash-hook', 'zsh-hook']);\n\nconst finiteOrNull = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null);\n\n/** The redacted hook-log line for one raw event, or null for anything malformed (which is then dropped, never written raw). */\nexport function toHookLogLine(rawEvent: string, shell: string): string | null {\n if (!HOOK_SHELLS.has(shell)) return null;\n let event: unknown;\n try {\n // Windows PowerShell's redirected stdin can append its encoding's BOM when the pipe is closed;\n // String#trim strips U+FEFF (ECMAScript whitespace), JSON.parse alone does not.\n event = JSON.parse(rawEvent.trim());\n } catch {\n // Deliberately swallowed: a JSON.parse error message quotes the input, i.e. the raw command.\n return null;\n }\n if (typeof event !== 'object' || event === null) return null;\n const e = event as Record<string, unknown>;\n if (typeof e.ts !== 'string' || typeof e.cwd !== 'string' || typeof e.command !== 'string' || e.command.trim() === '') return null;\n\n return JSON.stringify({\n ts: e.ts,\n cwd: e.cwd,\n exitCode: finiteOrNull(e.exitCode),\n durationMs: finiteOrNull(e.durationMs),\n command: redact(e.command).text,\n shell,\n // From the raw command, before redaction: the same hash hook node ids have always been derived from.\n commandHash: sha256Hex(e.command).slice(0, 12),\n });\n}\n\n/** Appends the redacted line in one write -- no temp file, so nothing raw can be left behind by a crash. */\nexport async function recordShellEvent(rawEvent: string, logPath: string, shell: string): Promise<boolean> {\n const line = toHookLogLine(rawEvent, shell);\n if (!line) return false;\n await mkdir(dirname(logPath), { recursive: true });\n await appendFile(logPath, `${line}\\n`, { encoding: 'utf8', mode: 0o600 });\n return true;\n}\n","/**\n * Best-effort secret redaction for collected text before it is ever written\n * to the FTS index.\n *\n * This is a safety net, not a guarantee -- pattern-based redaction cannot\n * catch every shape a secret can take. It exists because conversation text\n * is the collector most likely to contain something sensitive (a pasted\n * credential, a key a user asked for help debugging), but a committed `.env`\n * or a hard-coded key makes the code-diff collector a real second candidate,\n * which is what the `high-confidence` profile below is for.\n */\n\ninterface Rule {\n name: string;\n pattern: RegExp;\n /**\n * The match is a secret by its own shape, with no reliance on the\n * surrounding text. Only these rules are safe to run over source code:\n * the shape rules match strings nothing else produces, while the\n * key/value rule matches ordinary code such as\n * `const apiKey = process.env.API_KEY` and would corrupt the very lines\n * a diff is indexed for.\n */\n highConfidence: boolean;\n /** Replacement built from the capture groups; without it the whole match becomes the marker. */\n render?: (groups: readonly string[]) => string;\n}\n\nexport const REDACTION_MARK = '[redacted]';\nconst MARK = REDACTION_MARK;\n// Every rule refuses to re-match its own marker, so redacting twice changes nothing.\nconst NOT_MARK = String.raw`(?!\\[redacted\\])`;\nconst keepPrefix = (groups: readonly string[]): string => `${groups[0]}${MARK}`;\n\n// The keyword may sit anywhere inside an identifier (DB_PASSWORD, OPENAI_API_KEY, dbPassword, --api-key).\n// `token` is singular only: plural keys (max_tokens, rawTokens) are LLM token counts, not credentials.\nconst SECRET_KEYWORD = String.raw`(?:(?:pass(?:word|wd|phrase)|secret|credential|api[_-]?key|(?:access|private|secret|client|signing|encryption|master)[_-]?key)s?|token)`;\n// Bare `pass` needs a prefix component or dashes (DB_PASS, --pass), so prose like \"first pass: x\" and `bypass` never match.\n// Bounded so a long identifier run full of keywords stays linear, not quadratic.\nconst SECRET_KEY = String.raw`(?:[A-Za-z0-9_.-]{0,100}?${SECRET_KEYWORD}|(?:-{1,2}|(?:[A-Za-z0-9]{1,40}[_.-]){1,8})pass)(?:[_.-][A-Za-z0-9]{1,40}){0,8}`;\n// Type annotations (`password: string`) are not values; everything else is hidden, however short.\nconst TYPE_WORD = String.raw`(?:string|number|boolean|bool|int|str|null|undefined|none|nil|true|false|any|unknown|object)(?=[\\s;,)|\\]}>]|$)`;\n// A quoted value runs to its *closing* quote: an escaped quote inside it (--password \"pa\\\"ss\")\n// must not end the match, or the tail after it survives redaction. Bounded so an ordinary value\n// stops at its own quote; one the bound cannot close (over 500 chars, or never closed) fails\n// closed and takes the rest of the line rather than none of it.\nconst SECRET_VALUE = String.raw`(?:\"(?:\\\\.|[^\"\\\\\\r\\n]){1,500}\"|'(?:\\\\.|[^'\\\\\\r\\n]){1,500}'|\\x60(?:\\\\.|[^\\x60\\\\\\r\\n]){1,500}\\x60|\"(?!\")[^\\r\\n]+|'(?!')[^\\r\\n]+|\\x60(?!\\x60)[^\\r\\n]+|[^\\s'\"\\x60]+)`;\n// The rest of one shell command: stops at a pipe, `;`, `&` or newline so a tool name never reaches into the next command.\nconst SAME_COMMAND = String.raw`[^\\n|;&]{0,500}?`;\n// A next argument that is a flag or a redirection is not a value.\nconst NOT_FLAG_OR_REDIRECT = String.raw`(?![-<>|&;])`;\nconst AUTH_SCHEME = String.raw`(?:bearer|basic|token|digest|negotiate|ntlm)`;\n/** Everything up to and including curl's credential flag, shared by that rule's quoted and bare branches. */\nconst CURL_USER = String.raw`\\bcurl(?=[ \\t])${SAME_COMMAND}[ \\t](?:-u|--user)(?:[ \\t]+|=)?`;\n\nconst RULES: Rule[] = [\n {\n name: 'private-key-block',\n pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,\n highConfidence: true,\n },\n { name: 'aws-access-key', pattern: /\\bAKIA[0-9A-Z]{16}\\b/g, highConfidence: true },\n { name: 'github-token', pattern: /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/g, highConfidence: true },\n { name: 'slack-token', pattern: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g, highConfidence: true },\n {\n name: 'jwt',\n pattern: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g,\n highConfidence: true,\n },\n // scheme://user:password@host -- the password may itself contain `@`, so the host starts after the last one.\n {\n name: 'uri-credentials',\n pattern: new RegExp(\n String.raw`(\\b[a-z][a-z0-9+.-]{1,30}://[^\\s/?#'\"@:]*:)${NOT_MARK}[^\\s/?#'\"]+(?=@[^\\s/?#'\"@]*(?:[/?#\\s'\"]|$))`,\n 'gi',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // An option whose name ends in `bearer` (`--oauth2-bearer x`) always takes a credential, whatever\n // the token looks like. The prose-shaped rule below cannot cover it: it needs a digit, so an\n // all-alphabetic token would survive.\n {\n name: 'bearer-option-arg',\n pattern: new RegExp(\n String.raw`(?<=^|[\\s'\"(])(-{1,2}[A-Za-z0-9_.-]{0,40}bearer[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // A bearer token outside a header (`Bearer x`); it must contain a digit so \"bearer authentication\" is left alone.\n {\n name: 'bearer-token',\n pattern: new RegExp(String.raw`(\\bbearer[ \\t]+)${NOT_MARK}(?=[A-Za-z0-9._~+/-]*\\d)[A-Za-z0-9._~+/-]{16,}=*`, 'gi'),\n highConfidence: true,\n render: keepPrefix,\n },\n // Tool-anchored password arguments: `-p` means port or parents almost everywhere else. Case-sensitive (`-P` is mysql's port).\n // mysql takes only the compact `-psecret` form; `-p secret` means \"prompt, then use database secret\".\n {\n name: 'mysql-password-arg',\n pattern: new RegExp(\n String.raw`(\\b(?:mysql|mysqldump|mysqladmin|mysqlimport|mysqlsh|mariadb|mariadb-dump)(?=[ \\t])${SAME_COMMAND}[ \\t]-p)${NOT_MARK}(?=\\S)${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // Only sshpass's own options may precede -p, so a wrapped `ssh -p 22` is never read as the password.\n {\n name: 'sshpass-password-arg',\n pattern: new RegExp(String.raw`(\\bsshpass(?:[ \\t]+-[^p\\s]\\S*)*[ \\t]+-p[ \\t]*)${NOT_MARK}(?=\\S)${SECRET_VALUE}`, 'g'),\n highConfidence: true,\n render: keepPrefix,\n },\n {\n name: 'mongo-password-arg',\n pattern: new RegExp(\n String.raw`(\\bmongo(?:sh|dump|restore|export|import|stat|top|files)?(?=[ \\t])${SAME_COMMAND}[ \\t]-p[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n {\n name: 'redis-cli-password-arg',\n pattern: new RegExp(\n String.raw`(\\bredis-cli(?=[ \\t])${SAME_COMMAND}[ \\t]-a[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'g',\n ),\n highConfidence: true,\n render: keepPrefix,\n },\n // Quoted credentials get their own branches: `curl -u \"user:pass word\"` holds a space, which\n // the unquoted branch would stop at, leaving the rest of the password in the text. Like\n // SECRET_VALUE, they step over escaped characters so `\"alice:pa\\\"ss\"` is not cut at `\\\"`. Inside\n // quotes the user name may hold a space too (`\"alice smith:pw\"`); only the quote ends it.\n {\n name: 'curl-user-password',\n pattern: new RegExp(\n String.raw`(${CURL_USER}\"(?:\\\\.|[^:\"\\\\\\r\\n])*:)${NOT_MARK}(?:\\\\.|[^\"\\\\\\r\\n])*|(${CURL_USER}'(?:\\\\.|[^:'\\\\\\r\\n])*:)${NOT_MARK}(?:\\\\.|[^'\\\\\\r\\n])*|(${CURL_USER}[^\\s:'\"]*:)${NOT_MARK}[^\\s'\"]+`,\n 'g',\n ),\n highConfidence: true,\n render: (groups) => `${groups.find((g) => g.length > 0) ?? ''}${MARK}`,\n },\n // Authorization: Bearer x / Basic x / token x. The scheme word is kept; it must not be mistaken for the value.\n {\n name: 'authorization-header',\n pattern: new RegExp(\n String.raw`(\\b(?:proxy-)?authorization[\"']?[ \\t]*[:=][ \\t]*[\"'\\x60]?(?:${AUTH_SCHEME}[ \\t]+)?)(?!${AUTH_SCHEME}[ \\t])${NOT_MARK}[^\\s'\"\\x60]+`,\n 'gi',\n ),\n highConfidence: false,\n render: keepPrefix,\n },\n // `--password secret`: a secret-named option whose value is the next argument. The flag must start a\n // command-line word, so prose like \"the `id`-token heuristic\" is not read as a `-token` option.\n {\n name: 'secret-option-arg',\n pattern: new RegExp(\n String.raw`(?<=^|[\\s'\"(])(-{1,2}${SECRET_KEY}[ \\t]+)${NOT_FLAG_OR_REDIRECT}${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: false,\n render: keepPrefix,\n },\n // key/token/secret/password = \"value\" or : value, in code, JSON, env-file, shell or prose form.\n // The key start is an explicit non-identifier lookbehind, not `\\b`: `_` is a word character, so\n // `\\b` never fired inside DB_PASSWORD and the value leaked. Found live via `scan-shell`.\n {\n name: 'key-value-secret',\n pattern: new RegExp(\n String.raw`(?<![A-Za-z0-9_.-])(${SECRET_KEY})[\"']?[ \\t]*[:=](?![=>])[ \\t]*(?!${TYPE_WORD})${NOT_MARK}${SECRET_VALUE}`,\n 'gi',\n ),\n highConfidence: false,\n // Keep the key name so the redaction is legible (\"apiKey: [redacted]\").\n render: (groups) => `${groups[0]}: ${MARK}`,\n },\n];\n\nexport interface RedactResult {\n text: string;\n redactedCount: number;\n}\n\n/**\n * `all` runs every rule -- the right trade for prose, where a false positive\n * costs a mangled sentence. `high-confidence` runs only the shape rules, for\n * text that *is* code and must survive redaction intact.\n */\nexport type RedactProfile = 'all' | 'high-confidence';\n\nexport function redact(text: string, profile: RedactProfile = 'all'): RedactResult {\n let redactedCount = 0;\n let out = text;\n\n for (const rule of RULES) {\n if (profile === 'high-confidence' && !rule.highConfidence) continue;\n out = out.replace(rule.pattern, (...args: unknown[]) => {\n redactedCount += 1;\n // `replace` passes (match, ...groups, offset, wholeString): the groups end at the first\n // number. Slicing by type keeps a match offset from ever being mistaken for a group.\n const end = args.findIndex((arg, i) => i > 0 && typeof arg === 'number');\n const groups = args.slice(1, end).map((g) => (typeof g === 'string' ? g : ''));\n return rule.render ? rule.render(groups) : MARK;\n });\n }\n\n return { text: out, redactedCount };\n}\n","import { createHash } from 'node:crypto';\nimport type { NodeKind } from './types.js';\n\n/** NUL cannot appear in any of our key components, so it is a safe joiner. */\nconst KEY_SEP = '\\u0000';\n\nexport function sha256Hex(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex');\n}\n\n/**\n * Content-addressed node id.\n *\n * `naturalKey` must be whatever uniquely identifies the event at its source\n * (a commit sha, a `timestamp:command` pair, ...). Re-running `sync` then\n * re-derives the exact same id, which makes ingestion idempotent without\n * relying on a cursor being correct.\n */\nexport function makeNodeId(projectId: string, kind: NodeKind, naturalKey: string): string {\n return sha256Hex([projectId, kind, naturalKey].join(KEY_SEP)).slice(0, 24);\n}\n","/**\n * Entry point the installed shell hooks run once per command:\n *\n * <hook> | node recorder.js --log <path> --shell <pwsh-hook|bash-hook|zsh-hook>\n *\n * Bundled separately from the CLI so it starts fast. Silent by design: it\n * never writes to stdout or stderr and creates no temp files, so no failure\n * path -- a bad event, an unwritable log, a crash -- can echo the command it\n * received anywhere. On any failure the event is dropped.\n */\nimport { recordShellEvent } from '../shell/recorder.js';\n\nconst MAX_EVENT_BYTES = 1_000_000;\n\nconst drop = (): never => process.exit(1);\nprocess.on('uncaughtException', drop);\nprocess.on('unhandledRejection', drop);\n\nfunction arg(name: string): string | undefined {\n const i = process.argv.indexOf(name);\n return i === -1 ? undefined : process.argv[i + 1];\n}\n\nasync function main(): Promise<void> {\n const logPath = arg('--log');\n const shell = arg('--shell');\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of process.stdin) {\n size += (chunk as Buffer).length;\n if (size > MAX_EVENT_BYTES) drop();\n chunks.push(chunk as Buffer);\n }\n const ok = logPath && shell ? await recordShellEvent(Buffer.concat(chunks).toString('utf8'), logPath, shell) : false;\n process.exit(ok ? 0 : 1);\n}\n\nmain().catch(drop);\n"],"mappings":"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","names":["arg"]}
|
package/package.json
CHANGED
|
@@ -1,70 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nexusmem",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local-first persistent memory for AI coding agents. Indexes git history, shell commands with exit codes, project docs and (opt-in) assistant transcripts into an embedded SQLite database, then serves back a ranked, token-budgeted context slice over MCP or the CLI.",
|
|
5
|
-
"mcpName": "io.github.yaminbkk/nexusmem",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/yaminbkk/NexusMem.git"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://github.com/yaminbkk/NexusMem#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/yaminbkk/NexusMem/issues"
|
|
14
|
-
},
|
|
15
|
-
"type": "module",
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=22"
|
|
18
|
-
},
|
|
19
|
-
"bin": {
|
|
20
|
-
"nexusmem": "dist/cli/index.js"
|
|
21
|
-
},
|
|
22
|
-
"files": [
|
|
23
|
-
"dist",
|
|
24
|
-
"README.md",
|
|
25
|
-
"CHANGELOG.md"
|
|
26
|
-
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"dev": "tsx src/cli/index.ts",
|
|
29
|
-
"build": "tsup",
|
|
30
|
-
"typecheck": "tsc --noEmit",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"test:watch": "vitest",
|
|
33
|
-
"smoke": "tsx scripts/smoke-package.ts",
|
|
34
|
-
"bench": "tsx scripts/benchmark.ts",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"@types/
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"esbuild@0.
|
|
69
|
-
|
|
70
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nexusmem",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Local-first persistent memory for AI coding agents. Indexes git history, shell commands with exit codes, project docs and (opt-in) assistant transcripts into an embedded SQLite database, then serves back a ranked, token-budgeted context slice over MCP or the CLI.",
|
|
5
|
+
"mcpName": "io.github.yaminbkk/nexusmem",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/yaminbkk/NexusMem.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/yaminbkk/NexusMem#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/yaminbkk/NexusMem/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"nexusmem": "dist/cli/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsx src/cli/index.ts",
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"smoke": "tsx scripts/smoke-package.ts",
|
|
34
|
+
"bench": "tsx scripts/benchmark.ts",
|
|
35
|
+
"bench:hook": "tsx scripts/bench-agent-hook.ts",
|
|
36
|
+
"eval": "tsx scripts/eval.ts",
|
|
37
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
|
+
"better-sqlite3": "^12.2.0",
|
|
42
|
+
"commander": "^14.0.0",
|
|
43
|
+
"picocolors": "^1.1.1",
|
|
44
|
+
"sqlite-vec": "^0.1.9",
|
|
45
|
+
"zod": "^4.0.5"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
50
|
+
"tsup": "^8.5.0",
|
|
51
|
+
"tsx": "^4.20.0",
|
|
52
|
+
"typescript": "^5.8.0",
|
|
53
|
+
"vitest": "^3.2.0"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"ai",
|
|
57
|
+
"shell-history",
|
|
58
|
+
"exit-codes",
|
|
59
|
+
"memory",
|
|
60
|
+
"context",
|
|
61
|
+
"mcp",
|
|
62
|
+
"local-first",
|
|
63
|
+
"sqlite",
|
|
64
|
+
"agent"
|
|
65
|
+
],
|
|
66
|
+
"allowScripts": {
|
|
67
|
+
"better-sqlite3@12.11.1": true,
|
|
68
|
+
"esbuild@0.27.7": true,
|
|
69
|
+
"esbuild@0.28.1": true
|
|
70
|
+
}
|
|
71
|
+
}
|