@mnemom/mnemom 0.17.20-next.0 → 0.17.21
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/dist/commands/agent-invite.d.ts +3 -0
- package/dist/commands/agent-invite.js +1 -1
- package/dist/commands/agent-sessions.d.ts +15 -5
- package/dist/commands/agent-sessions.js +29 -5
- package/dist/commands/agent-setup.d.ts +1 -0
- package/dist/commands/agent-setup.js +13 -3
- package/dist/commands/agent.d.ts +13 -0
- package/dist/commands/agent.js +89 -3
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +7 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -2
- package/dist/lib/api.d.ts +41 -0
- package/dist/lib/auth.d.ts +3 -1
- package/dist/lib/auth.js +2 -2
- package/dist/lib/claude-plugin.d.ts +114 -0
- package/dist/lib/claude-plugin.js +148 -0
- package/dist/lib/config.d.ts +13 -2
- package/dist/lib/config.js +22 -6
- package/dist/lib/first-run/governed.js +7 -4
- package/dist/lib/oauth.d.ts +3 -1
- package/dist/lib/oauth.js +13 -1
- package/dist/lib/retired-env-guard.d.ts +7 -0
- package/dist/lib/retired-env-guard.js +46 -0
- package/dist/rc-proxy.mjs +83 -13
- package/dist/rc-state.mjs +257 -0
- package/package.json +5 -1
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/mnemom/.claude-plugin/plugin.json +47 -0
- package/plugins/mnemom/.mcp.json +12 -0
- package/plugins/mnemom/hooks/hooks.json +3 -0
- package/plugins/mnemom/hooks/register.tsx +1476 -0
- package/plugins/mnemom/skills/about-mnemom/SKILL.md +88 -0
- package/plugins/mnemom/types/index.d.ts +142 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// rc-state — where `mnemom agent --remote-control` keeps its per-session dirs, and the sweep that
|
|
2
|
+
// clears the ones a dead session left behind. Imported by rc-proxy.mjs; commands/agent.ts mirrors
|
|
3
|
+
// rcStateRoot() for the launcher's log dir (tsc emits no .mjs, so it cannot import this file).
|
|
4
|
+
//
|
|
5
|
+
// WHY NOT THE OS TEMP DIR
|
|
6
|
+
// The proxy's ephemeral CA must stay on disk for the whole session: Claude Code's
|
|
7
|
+
// NODE_EXTRA_CA_CERTS points at ca.pem, and every leaf rotation re-reads ca.pem and leaf.ext.
|
|
8
|
+
// OS temp dirs are cleaned behind a running process — macOS's dirhelper deletes $TMPDIR files
|
|
9
|
+
// not accessed for 3 days, and systemd-tmpfiles ages out /tmp on Linux — which broke every RC
|
|
10
|
+
// session older than ~3-4 days with UNABLE_TO_VERIFY_LEAF_SIGNATURE (2026-10-03). So the dirs
|
|
11
|
+
// live under the CLI's per-user state dir, ~/.mnemom (the convention auth.json, config.json and
|
|
12
|
+
// sessions.jsonl already follow on every platform), which nothing cleans. In exchange nothing
|
|
13
|
+
// cleans up after a crashed session either: each dir records its owner's pid in OWNER_FILE, and
|
|
14
|
+
// every proxy start sweeps the dirs whose owner is dead.
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
/** Every per-session dir under the root starts with one of these: the CA dir, the log dir. */
|
|
21
|
+
export const RC_DIR_PREFIXES = ["mnemom-code-rc-ca-", "mnemom-code-rc."];
|
|
22
|
+
/** The pid of the process that owns (and removes on exit) the dir it sits in. */
|
|
23
|
+
export const OWNER_FILE = "owner.pid";
|
|
24
|
+
|
|
25
|
+
/** ~/.mnemom/rc on every platform. */
|
|
26
|
+
export function rcStateRoot(home = os.homedir()) {
|
|
27
|
+
return path.join(home, ".mnemom", "rc");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create the root 0700 (and its parents) and return it. Throws when the home dir is not writable;
|
|
32
|
+
* the caller falls back to the OS temp dir, where the keep-alive in rc-proxy.mjs still applies.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureRcStateRoot(root = rcStateRoot()) {
|
|
35
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
36
|
+
fs.chmodSync(root, 0o700); // mkdir's mode is filtered through the umask, and skips an existing dir
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Signal 0 probes without signalling. EPERM means it exists but is someone else's: alive. */
|
|
41
|
+
export function pidAlive(pid) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return e.code === "EPERM";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Open flags. O_NOFOLLOW / O_DIRECTORY / O_NONBLOCK are POSIX-only; on Windows they are absent from
|
|
51
|
+
// fs.constants and count as 0 (no symlink race to close there: no POSIX modes, per-user profile).
|
|
52
|
+
const C = fs.constants;
|
|
53
|
+
const NOFOLLOW = C.O_NOFOLLOW || 0;
|
|
54
|
+
const DIRECTORY = C.O_DIRECTORY || 0;
|
|
55
|
+
const NONBLOCK = C.O_NONBLOCK || 0; // a FIFO planted under a known name must not block the open
|
|
56
|
+
|
|
57
|
+
/** This process's uid, or null where there is none (native Windows): no owner check there. */
|
|
58
|
+
export function currentUid(proc = process) {
|
|
59
|
+
return typeof proc.getuid === "function" ? proc.getuid() : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read a regular file without following a symlink at its last component (O_NOFOLLOW), checking
|
|
64
|
+
* the type on the OPEN fd. Returns null when it is missing, a symlink, or not a regular file.
|
|
65
|
+
*/
|
|
66
|
+
export function readNoFollow(file, { fsImpl = fs } = {}) {
|
|
67
|
+
let fd;
|
|
68
|
+
try {
|
|
69
|
+
fd = fsImpl.openSync(file, C.O_RDONLY | NOFOLLOW | NONBLOCK);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return fsImpl.fstatSync(fd).isFile() ? fsImpl.readFileSync(fd) : null;
|
|
75
|
+
} finally {
|
|
76
|
+
fsImpl.closeSync(fd);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set atime/mtime through an fd opened without following a symlink, so a planted link never moves
|
|
82
|
+
* its target's times. Returns false (and touches nothing) when the entry is missing or a symlink.
|
|
83
|
+
*/
|
|
84
|
+
export function touchNoFollow(file, now, { fsImpl = fs } = {}) {
|
|
85
|
+
let fd;
|
|
86
|
+
try {
|
|
87
|
+
fd = fsImpl.openSync(file, C.O_RDONLY | NOFOLLOW | NONBLOCK);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
fsImpl.futimesSync(fd, now, now);
|
|
93
|
+
return true;
|
|
94
|
+
} finally {
|
|
95
|
+
fsImpl.closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Write `bytes` to `dir/name` via a fresh O_EXCL|O_NOFOLLOW temp file, then rename it in. */
|
|
100
|
+
function writeAtomic(fsImpl, dir, name, bytes, mode) {
|
|
101
|
+
const tmp = path.join(
|
|
102
|
+
dir,
|
|
103
|
+
`.${name}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
104
|
+
);
|
|
105
|
+
// O_EXCL: the temp name must not exist, so nothing planted there (a symlink included) is followed.
|
|
106
|
+
const fd = fsImpl.openSync(tmp, C.O_WRONLY | C.O_CREAT | C.O_EXCL | NOFOLLOW, mode);
|
|
107
|
+
try {
|
|
108
|
+
let off = 0;
|
|
109
|
+
while (off < bytes.length) off += fsImpl.writeSync(fd, bytes, off, bytes.length - off);
|
|
110
|
+
fsImpl.fchmodSync(fd, mode); // the open's mode is filtered through the umask
|
|
111
|
+
} finally {
|
|
112
|
+
fsImpl.closeSync(fd);
|
|
113
|
+
}
|
|
114
|
+
// Write-then-rename, so a reader (openssl, a starting Claude Code) never sees half a file.
|
|
115
|
+
fsImpl.renameSync(tmp, path.join(dir, name));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const refuse = (dir) =>
|
|
119
|
+
new Error(`refusing to reuse ${dir}: not a private directory owned by this user`);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Keep `dir` and its long-lived `files` ([name, bytes, mode] triples) present and fresh, against an
|
|
123
|
+
* OS cleaner that deletes what it has not seen accessed. Creates `dir` 0700 if it is gone, restores
|
|
124
|
+
* any file that is missing or differs (byte-identical, same path), and refreshes the atime/mtime of
|
|
125
|
+
* the dir, everything in it, and `logfile` (if any). Returns the names it restored ("dir" first if
|
|
126
|
+
* it had to recreate the dir).
|
|
127
|
+
*
|
|
128
|
+
* The dir decides what CA Claude Code trusts, so nothing here acts on a path it checked earlier:
|
|
129
|
+
* * the dir is opened ONCE without following a symlink (O_NOFOLLOW|O_DIRECTORY) and checked on
|
|
130
|
+
* that fd: a real directory, owned by this uid, with no group/other bits. A symlink, a file or
|
|
131
|
+
* a foreign/non-private dir at `dir` is refused (throws), and nothing is read or written.
|
|
132
|
+
* * every file read and every atime refresh opens the entry without following a symlink and
|
|
133
|
+
* checks the open fd; every write goes to a fresh O_EXCL|O_NOFOLLOW temp file and is renamed in.
|
|
134
|
+
* * Node has no openat(), so the per-file opens still resolve `dir` by path. Before returning, the
|
|
135
|
+
* path is checked to still name the directory the held fd was validated on (dev + inode); if
|
|
136
|
+
* anything swapped it in between, this throws. Who could swap it: the parent is ~/.mnemom/rc
|
|
137
|
+
* (0700, ours) or, in the fallback, the OS temp dir, whose sticky bit stops another user from
|
|
138
|
+
* replacing a dir we own; the fd check covers the case where it was deleted and re-planted.
|
|
139
|
+
*/
|
|
140
|
+
export function keepPrivateDir(
|
|
141
|
+
dir,
|
|
142
|
+
files,
|
|
143
|
+
{
|
|
144
|
+
logfile = "",
|
|
145
|
+
now = new Date(),
|
|
146
|
+
fsImpl = fs,
|
|
147
|
+
uid = currentUid(),
|
|
148
|
+
platform = process.platform,
|
|
149
|
+
} = {},
|
|
150
|
+
) {
|
|
151
|
+
const restored = [];
|
|
152
|
+
let created = false;
|
|
153
|
+
try {
|
|
154
|
+
fsImpl.mkdirSync(path.dirname(dir), { recursive: true, mode: 0o700 });
|
|
155
|
+
fsImpl.mkdirSync(dir, { mode: 0o700 });
|
|
156
|
+
created = true;
|
|
157
|
+
restored.push("dir");
|
|
158
|
+
} catch (e) {
|
|
159
|
+
if (e.code !== "EEXIST") throw e;
|
|
160
|
+
}
|
|
161
|
+
let dfd;
|
|
162
|
+
try {
|
|
163
|
+
dfd = fsImpl.openSync(dir, C.O_RDONLY | DIRECTORY | NOFOLLOW);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// ELOOP: a symlink; ENOTDIR: a file (O_DIRECTORY). Either way not a dir we made.
|
|
166
|
+
if (e.code === "ELOOP" || e.code === "ENOTDIR") throw refuse(dir);
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const st = fsImpl.fstatSync(dfd);
|
|
171
|
+
if (!st.isDirectory() || (uid !== null && st.uid !== uid)) throw refuse(dir);
|
|
172
|
+
if (created)
|
|
173
|
+
fsImpl.fchmodSync(dfd, 0o700); // mkdir's mode is filtered through the umask
|
|
174
|
+
else if (platform !== "win32" && (st.mode & 0o077) !== 0) throw refuse(dir); // no POSIX modes there
|
|
175
|
+
for (const [name, bytes, mode] of files) {
|
|
176
|
+
const cur = readNoFollow(path.join(dir, name), { fsImpl });
|
|
177
|
+
if (cur && cur.equals(bytes)) continue;
|
|
178
|
+
writeAtomic(fsImpl, dir, name, bytes, mode);
|
|
179
|
+
restored.push(name);
|
|
180
|
+
}
|
|
181
|
+
for (const f of fsImpl.readdirSync(dir)) {
|
|
182
|
+
// false: removed under us (e.g. ca.key between sign and rm), or a symlink: nothing to keep alive.
|
|
183
|
+
touchNoFollow(path.join(dir, f), now, { fsImpl });
|
|
184
|
+
}
|
|
185
|
+
fsImpl.futimesSync(dfd, now, now);
|
|
186
|
+
const at = fsImpl.lstatSync(dir);
|
|
187
|
+
const held = fsImpl.fstatSync(dfd);
|
|
188
|
+
if (at.dev !== held.dev || at.ino !== held.ino)
|
|
189
|
+
throw new Error(`${dir} was replaced while it was being kept alive`);
|
|
190
|
+
} finally {
|
|
191
|
+
fsImpl.closeSync(dfd);
|
|
192
|
+
}
|
|
193
|
+
// The log may live in a temp dir (a user's override). false: not created yet, or gone — log()
|
|
194
|
+
// recreates it.
|
|
195
|
+
if (logfile) touchNoFollow(logfile, now, { fsImpl });
|
|
196
|
+
return restored;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ownerPid(dir, fsImpl) {
|
|
200
|
+
const b = readNoFollow(path.join(dir, OWNER_FILE), { fsImpl });
|
|
201
|
+
const n = b === null ? NaN : Number(b.toString("utf8").trim());
|
|
202
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The name a swept dir is moved to before removal. It carries an RC prefix on purpose: if the
|
|
206
|
+
* sweep dies between the move and the removal, the next sweep finds it (its owner is dead). */
|
|
207
|
+
export const SWEEP_PREFIX = "mnemom-code-rc.swept-";
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Remove the per-session dirs under `root` that no live process owns: those whose recorded owner
|
|
211
|
+
* is dead, and those with no readable owner that are older than `maxAgeMs` (a dir is only
|
|
212
|
+
* ownerless for the instant between its mkdtemp and its owner write, or after a crash in that
|
|
213
|
+
* instant). A dir whose owner is alive is never touched, however old. Returns the removed names.
|
|
214
|
+
*
|
|
215
|
+
* Race-safe removal: a candidate is first RENAMED to a fresh name under `root` (rename moves the
|
|
216
|
+
* entry itself and never follows a symlink), and only what was moved is checked (a real directory,
|
|
217
|
+
* via lstat) and removed. Nothing can be swapped in between the check and the removal, because the
|
|
218
|
+
* fresh name was never visible to anyone else. Whatever the move took that is not a directory is
|
|
219
|
+
* moved back. A dir that vanishes or changes mid-sweep (its owner's own cleanup) is skipped.
|
|
220
|
+
*/
|
|
221
|
+
export function sweepRcDirs(
|
|
222
|
+
root,
|
|
223
|
+
{ maxAgeMs, now = Date.now(), isAlive = pidAlive, fsImpl = fs } = {},
|
|
224
|
+
) {
|
|
225
|
+
const removed = [];
|
|
226
|
+
let names;
|
|
227
|
+
try {
|
|
228
|
+
names = fsImpl.readdirSync(root);
|
|
229
|
+
} catch {
|
|
230
|
+
return removed;
|
|
231
|
+
}
|
|
232
|
+
for (const name of names) {
|
|
233
|
+
if (!RC_DIR_PREFIXES.some((pre) => name.startsWith(pre))) continue;
|
|
234
|
+
const dir = path.join(root, name);
|
|
235
|
+
try {
|
|
236
|
+
const st = fsImpl.lstatSync(dir);
|
|
237
|
+
if (!st.isDirectory()) continue;
|
|
238
|
+
const pid = ownerPid(dir, fsImpl);
|
|
239
|
+
if (pid !== null) {
|
|
240
|
+
if (pid === process.pid || isAlive(pid)) continue;
|
|
241
|
+
} else if (!(now - st.mtimeMs > maxAgeMs)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const moved = path.join(root, SWEEP_PREFIX + crypto.randomBytes(6).toString("hex"));
|
|
245
|
+
fsImpl.renameSync(dir, moved);
|
|
246
|
+
if (!fsImpl.lstatSync(moved).isDirectory()) {
|
|
247
|
+
fsImpl.renameSync(moved, dir); // swapped for a non-dir after the check: put it back
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
fsImpl.rmSync(moved, { recursive: true, force: true });
|
|
251
|
+
removed.push(name);
|
|
252
|
+
} catch {
|
|
253
|
+
/* raced with its owner's own cleanup: nothing to do */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return removed;
|
|
257
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemom/mnemom",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.21",
|
|
4
4
|
"description": "Transparent AI agent tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
36
|
"!dist/__tests__",
|
|
37
|
+
"plugins",
|
|
38
|
+
"!plugins/**/*.test.tsx",
|
|
39
|
+
"!plugins/mnemom/README.md",
|
|
40
|
+
"!plugins/mnemom/tsconfig.json",
|
|
37
41
|
"README.md",
|
|
38
42
|
"LICENSE"
|
|
39
43
|
],
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mnemom",
|
|
3
|
+
"owner": { "name": "Mnemom", "email": "hello@mnemom.ai", "url": "https://www.mnemom.ai" },
|
|
4
|
+
"metadata": {
|
|
5
|
+
"description": "Mnemom plugins for Claude Code: governance, Trust Ratings and the Mnemom gateway.",
|
|
6
|
+
"version": "0.1.0"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "mnemom",
|
|
11
|
+
"source": "./mnemom",
|
|
12
|
+
"description": "Mnemom for Claude Code: the governed-agent band, /mnemom commands, the gateway guard and Mnemom attribution for a session launched by `mnemom agent`.",
|
|
13
|
+
"version": "0.1.0",
|
|
14
|
+
"author": { "name": "Mnemom", "email": "hello@mnemom.ai" },
|
|
15
|
+
"homepage": "https://docs.mnemom.ai",
|
|
16
|
+
"category": "productivity",
|
|
17
|
+
"keywords": ["mnemom", "governance", "trust", "gateway", "agent"]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mnemom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mnemom for Claude Code: the governed-agent band, the goal (sealed contract or inferred, with guardrails and the gateway's verdict), /mnemom commands, the gateway guard and Mnemom attribution for a session launched by `mnemom agent`.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Mnemom",
|
|
7
|
+
"email": "hello@mnemom.ai",
|
|
8
|
+
"url": "https://www.mnemom.ai"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://docs.mnemom.ai",
|
|
11
|
+
"repository": "https://github.com/mnemom/mnemom-platform",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"keywords": ["mnemom", "governance", "trust", "gateway", "agent"],
|
|
14
|
+
"types": "./types/index.d.ts",
|
|
15
|
+
"userConfig": {
|
|
16
|
+
"showBand": {
|
|
17
|
+
"title": "Show the Mnemom band",
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": true,
|
|
20
|
+
"description": "Draw the Mnemom band above the prompt (agent, cell, goal and its verdict, Trust Rating). Hide it for the session with /mnemom hide."
|
|
21
|
+
},
|
|
22
|
+
"requireGovernance": {
|
|
23
|
+
"title": "Require a governed session",
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"default": false,
|
|
26
|
+
"description": "Refuse every tool call while this session is NOT routed through the Mnemom gateway (i.e. it was not launched by `mnemom agent`). Off: warn only."
|
|
27
|
+
},
|
|
28
|
+
"guardReroute": {
|
|
29
|
+
"title": "Guard the gateway routing",
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"default": true,
|
|
32
|
+
"description": "In a governed session, deny Bash commands that point ANTHROPIC_BASE_URL away from the Mnemom gateway or unset it."
|
|
33
|
+
},
|
|
34
|
+
"commitTrailer": {
|
|
35
|
+
"title": "Mnemom commit trailers",
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": true,
|
|
38
|
+
"description": "Append Mnemom-Agent / Mnemom-Conversation trailers to the commit attribution in a governed session."
|
|
39
|
+
},
|
|
40
|
+
"spinnerMark": {
|
|
41
|
+
"title": "Mark the spinner and turn line",
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"default": true,
|
|
44
|
+
"description": "Mark the working spinner and the turn-duration line with the Mnemom sign in a governed session."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|