@mnemom/mnemom 0.17.32-next.1 → 0.17.32-next.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/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.js +167 -67
- package/dist/durable/adopt-api.d.ts +37 -0
- package/dist/durable/adopt-api.js +106 -0
- package/dist/durable/adopt-live.d.ts +17 -0
- package/dist/durable/adopt-live.js +152 -0
- package/dist/durable/adopt.d.ts +305 -0
- package/dist/durable/adopt.js +679 -0
- package/dist/durable/cwd-rule.d.ts +65 -0
- package/dist/durable/cwd-rule.js +111 -0
- package/dist/durable/discover.d.ts +68 -0
- package/dist/durable/discover.js +123 -0
- package/dist/durable/gate.d.ts +8 -0
- package/dist/durable/gate.js +13 -0
- package/dist/durable/launch-spec.d.ts +137 -0
- package/dist/durable/launch-spec.js +277 -0
- package/dist/durable/proc.d.ts +86 -0
- package/dist/durable/proc.js +279 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +52 -1
- package/dist/lib/agent.d.ts +7 -0
- package/dist/lib/agent.js +26 -0
- package/dist/lib/api.d.ts +32 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +13 -0
- package/dist/lib/record-org.d.ts +27 -0
- package/dist/lib/record-org.js +44 -0
- package/dist/lib/session-key.d.ts +68 -0
- package/dist/lib/session-key.js +101 -0
- package/dist/lib/statusline.d.ts +13 -0
- package/dist/lib/statusline.js +27 -3
- package/dist/lib/subscription-login.d.ts +1 -1
- package/dist/lib/subscription-login.js +4 -1
- package/dist/vendor/durable-contract/index.js +5663 -0
- package/package.json +5 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The C4 cwd rule (S1/C4 registered worktree root; C14-R04; BACKBONE C4 "Lock-time cwd check",
|
|
3
|
+
* amendment 32). A cwd is accepted when its realpath equals `git rev-parse --show-toplevel` (run
|
|
4
|
+
* with hooks and fsmonitor off and every GIT_* variable cleared): the primary checkout or a linked
|
|
5
|
+
* worktree. Otherwise it is `durable_cwd_not_root {cwd_kind}`:
|
|
6
|
+
* subdirectory inside a work tree, not its top level
|
|
7
|
+
* not_git not inside a git work tree (or gone)
|
|
8
|
+
* symlink_transcript a running session whose cwd is reached through a symlink (the path
|
|
9
|
+
* differs from its realpath) and whose transcript sits under the project
|
|
10
|
+
* dir slugged from the symlink path, not the realpath
|
|
11
|
+
* A running session whose transcript is under neither slug is unadoptable (no_transcript): the
|
|
12
|
+
* slug rule cannot place it (e.g. a long path Claude Code truncates and hashes), so it is never
|
|
13
|
+
* guessed to be a symlink.
|
|
14
|
+
* The fix lines are the S1 catalogue entries under the fix-line rule (renderFixLine). A line whose
|
|
15
|
+
* path parameter is unknown (not_git outside any checkout) is not printed: every printed fix line
|
|
16
|
+
* is a working command (C11-R27), never a placeholder.
|
|
17
|
+
*
|
|
18
|
+
* The decision is pure over injected facts; liveCwdDeps is the thin driver.
|
|
19
|
+
*/
|
|
20
|
+
import { type CwdKind, type LauncherKey } from "../vendor/durable-contract/index.js";
|
|
21
|
+
export interface CwdDeps {
|
|
22
|
+
/** realpath(p), or null when it does not resolve. */
|
|
23
|
+
realpath(p: string): string | null;
|
|
24
|
+
/** `git rev-parse --show-toplevel` in `dir` (hooks, fsmonitor and GIT_* off), or null. */
|
|
25
|
+
gitToplevel(dir: string): string | null;
|
|
26
|
+
/** The project-dir names under `${CLAUDE_CONFIG_DIR:-~/.claude}/projects` holding `<ccSessionId>.jsonl`. */
|
|
27
|
+
transcriptDirs(ccSessionId: string): string[];
|
|
28
|
+
}
|
|
29
|
+
export type CwdVerdict = {
|
|
30
|
+
ok: true;
|
|
31
|
+
root: string;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
cwd_kind: CwdKind;
|
|
35
|
+
/** The fix-line parameter for this kind: toplevel or realpath; not_git knows no checkout. */
|
|
36
|
+
params: Record<string, string>;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
cwd_kind: null;
|
|
40
|
+
unadoptable: "no_transcript";
|
|
41
|
+
};
|
|
42
|
+
/** Claude Code's project-dir name for a path: every non-alphanumeric character becomes '-'. */
|
|
43
|
+
export declare function projectSlug(path: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Classify a cwd. `ccSessionId` is given for a running session (lock of a running session,
|
|
46
|
+
* adopt): its transcript must sit under the realpath's project dir; a transcript reached only
|
|
47
|
+
* through the symlink slug is symlink_transcript, and none at all is unadoptable (a seat could
|
|
48
|
+
* not --resume it).
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyCwd(cwd: string, deps: CwdDeps, ccSessionId?: string | null): CwdVerdict;
|
|
51
|
+
/**
|
|
52
|
+
* The S1 fix line for a cwd refusal (fix-line rule: the done clause when a non-terminal row holds
|
|
53
|
+
* the name, the config suffix when the launcher key is not `always`), or null when a path the
|
|
54
|
+
* entry needs is unknown.
|
|
55
|
+
*/
|
|
56
|
+
export declare function cwdFixLine(verdict: Extract<CwdVerdict, {
|
|
57
|
+
cwd_kind: CwdKind;
|
|
58
|
+
}>, ctx: {
|
|
59
|
+
name: string;
|
|
60
|
+
launcherKey: LauncherKey;
|
|
61
|
+
nameHeld: boolean;
|
|
62
|
+
}): string | null;
|
|
63
|
+
/** git with no hooks, no fsmonitor and no GIT_* from the caller's environment. */
|
|
64
|
+
export declare function gitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
65
|
+
export declare function liveCwdDeps(env?: NodeJS.ProcessEnv): CwdDeps;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The C4 cwd rule (S1/C4 registered worktree root; C14-R04; BACKBONE C4 "Lock-time cwd check",
|
|
3
|
+
* amendment 32). A cwd is accepted when its realpath equals `git rev-parse --show-toplevel` (run
|
|
4
|
+
* with hooks and fsmonitor off and every GIT_* variable cleared): the primary checkout or a linked
|
|
5
|
+
* worktree. Otherwise it is `durable_cwd_not_root {cwd_kind}`:
|
|
6
|
+
* subdirectory inside a work tree, not its top level
|
|
7
|
+
* not_git not inside a git work tree (or gone)
|
|
8
|
+
* symlink_transcript a running session whose cwd is reached through a symlink (the path
|
|
9
|
+
* differs from its realpath) and whose transcript sits under the project
|
|
10
|
+
* dir slugged from the symlink path, not the realpath
|
|
11
|
+
* A running session whose transcript is under neither slug is unadoptable (no_transcript): the
|
|
12
|
+
* slug rule cannot place it (e.g. a long path Claude Code truncates and hashes), so it is never
|
|
13
|
+
* guessed to be a symlink.
|
|
14
|
+
* The fix lines are the S1 catalogue entries under the fix-line rule (renderFixLine). A line whose
|
|
15
|
+
* path parameter is unknown (not_git outside any checkout) is not printed: every printed fix line
|
|
16
|
+
* is a working command (C11-R27), never a placeholder.
|
|
17
|
+
*
|
|
18
|
+
* The decision is pure over injected facts; liveCwdDeps is the thin driver.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { readdirSync, realpathSync, existsSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { placeholdersOf, renderFixLine, } from "../vendor/durable-contract/index.js";
|
|
25
|
+
/** Claude Code's project-dir name for a path: every non-alphanumeric character becomes '-'. */
|
|
26
|
+
export function projectSlug(path) {
|
|
27
|
+
return path.replace(/[^A-Za-z0-9]/g, "-");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Classify a cwd. `ccSessionId` is given for a running session (lock of a running session,
|
|
31
|
+
* adopt): its transcript must sit under the realpath's project dir; a transcript reached only
|
|
32
|
+
* through the symlink slug is symlink_transcript, and none at all is unadoptable (a seat could
|
|
33
|
+
* not --resume it).
|
|
34
|
+
*/
|
|
35
|
+
export function classifyCwd(cwd, deps, ccSessionId = null) {
|
|
36
|
+
const real = deps.realpath(cwd);
|
|
37
|
+
if (real === null)
|
|
38
|
+
return { ok: false, cwd_kind: "not_git", params: {} };
|
|
39
|
+
const top = deps.gitToplevel(real);
|
|
40
|
+
if (top === null)
|
|
41
|
+
return { ok: false, cwd_kind: "not_git", params: {} };
|
|
42
|
+
const realTop = deps.realpath(top) ?? top;
|
|
43
|
+
if (realTop !== real)
|
|
44
|
+
return { ok: false, cwd_kind: "subdirectory", params: { toplevel: realTop } };
|
|
45
|
+
if (ccSessionId === null)
|
|
46
|
+
return { ok: true, root: real };
|
|
47
|
+
const dirs = deps.transcriptDirs(ccSessionId);
|
|
48
|
+
if (dirs.includes(projectSlug(real)))
|
|
49
|
+
return { ok: true, root: real };
|
|
50
|
+
if (cwd !== real && dirs.includes(projectSlug(cwd)))
|
|
51
|
+
return { ok: false, cwd_kind: "symlink_transcript", params: { realpath: real } };
|
|
52
|
+
return { ok: false, cwd_kind: null, unadoptable: "no_transcript" };
|
|
53
|
+
}
|
|
54
|
+
const CWD_FIX = {
|
|
55
|
+
subdirectory: "cwd_subdirectory",
|
|
56
|
+
not_git: "cwd_not_git",
|
|
57
|
+
symlink_transcript: "cwd_symlink_transcript",
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The S1 fix line for a cwd refusal (fix-line rule: the done clause when a non-terminal row holds
|
|
61
|
+
* the name, the config suffix when the launcher key is not `always`), or null when a path the
|
|
62
|
+
* entry needs is unknown.
|
|
63
|
+
*/
|
|
64
|
+
export function cwdFixLine(verdict, ctx) {
|
|
65
|
+
const key = CWD_FIX[verdict.cwd_kind];
|
|
66
|
+
const params = { ...verdict.params, name: ctx.name };
|
|
67
|
+
if (placeholdersOf(key).some((p) => params[p] === undefined))
|
|
68
|
+
return null;
|
|
69
|
+
return renderFixLine(key, { launcherKey: ctx.launcherKey, nameHeld: ctx.nameHeld, params });
|
|
70
|
+
}
|
|
71
|
+
/** git with no hooks, no fsmonitor and no GIT_* from the caller's environment. */
|
|
72
|
+
export function gitEnv(env) {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [k, v] of Object.entries(env))
|
|
75
|
+
if (!k.startsWith("GIT_"))
|
|
76
|
+
out[k] = v;
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
export function liveCwdDeps(env = process.env) {
|
|
80
|
+
const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
|
|
81
|
+
return {
|
|
82
|
+
realpath(p) {
|
|
83
|
+
try {
|
|
84
|
+
return realpathSync(p);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
gitToplevel(dir) {
|
|
91
|
+
const r = spawnSync("git", [
|
|
92
|
+
"-c",
|
|
93
|
+
"core.hooksPath=/dev/null",
|
|
94
|
+
"-c",
|
|
95
|
+
"core.fsmonitor=false",
|
|
96
|
+
"rev-parse",
|
|
97
|
+
"--show-toplevel",
|
|
98
|
+
], { cwd: dir, env: gitEnv(env), encoding: "utf8", timeout: 10_000 });
|
|
99
|
+
const out = typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
100
|
+
return r.status === 0 && out !== "" ? out : null;
|
|
101
|
+
},
|
|
102
|
+
transcriptDirs(ccSessionId) {
|
|
103
|
+
try {
|
|
104
|
+
return readdirSync(projects).filter((d) => existsSync(join(projects, d, `${ccSessionId}.jsonl`)));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step-2 discovery of BACKBONE C1/C02-R26: the live tracked claude processes on this machine,
|
|
3
|
+
* one per conversation, each with its exact argv (KERN_PROCARGS2 / /proc, never ps), environment
|
|
4
|
+
* and cwd.
|
|
5
|
+
*
|
|
6
|
+
* A tracked claude is the process `mnemom agent` exec'd: its environment carries the session name
|
|
7
|
+
* (MNEMOM_AGENT_SESSION_NAME) and the conversation id (the `x-mnemom-conversation-id` line of
|
|
8
|
+
* ANTHROPIC_CUSTOM_HEADERS on the terminal shape, MNEMOM_AGENT_RC_CONVERSATION_ID on the RC shape).
|
|
9
|
+
* Its descendants (hooks, MCP servers, tools) inherit that environment, so the claude is the
|
|
10
|
+
* topmost process of the conversation: the one whose parent does not carry the same conversation
|
|
11
|
+
* id. The parent launcher's own argv is read too: its tokens after `--` pin the passthrough split.
|
|
12
|
+
*
|
|
13
|
+
* Pure over an injected snapshot (`ProcSnapshot`); `liveSnapshot` is the thin driver.
|
|
14
|
+
*/
|
|
15
|
+
import { type ProcArgs, type ProcDeps, type ProcRow } from "./proc.js";
|
|
16
|
+
export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
17
|
+
export declare const RC_CONVERSATION_ENV = "MNEMOM_AGENT_RC_CONVERSATION_ID";
|
|
18
|
+
/** The agent slug the original runs under (its x-mnemom-agent header), or null. */
|
|
19
|
+
export declare function agentSlugOf(env: Readonly<Record<string, string>>): string | null;
|
|
20
|
+
export interface LiveClaude {
|
|
21
|
+
pid: number;
|
|
22
|
+
ppid: number;
|
|
23
|
+
conversationId: string;
|
|
24
|
+
sessionName: string;
|
|
25
|
+
/** argv[0..] exactly as exec'd. */
|
|
26
|
+
argv: string[];
|
|
27
|
+
env: Record<string, string>;
|
|
28
|
+
/** The process cwd (always a realpath), or null when unreadable. */
|
|
29
|
+
cwd: string | null;
|
|
30
|
+
/** The parent launcher's tokens after its own `--`, or null when it is not readable. */
|
|
31
|
+
passthroughHint: string[] | null;
|
|
32
|
+
/** RC shape (the conversation id came from the RC proxy variable). */
|
|
33
|
+
remoteControl: boolean;
|
|
34
|
+
/** The process start time when it was discovered (readProcStart), or null when unreadable. */
|
|
35
|
+
start: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ProcSnapshot {
|
|
38
|
+
table: ProcRow[];
|
|
39
|
+
args: Map<number, ProcArgs>;
|
|
40
|
+
cwd(pid: number): string | null;
|
|
41
|
+
start(pid: number): string | null;
|
|
42
|
+
}
|
|
43
|
+
/** `KEY=value` entries as a record (the first `=` splits). */
|
|
44
|
+
export declare function envRecord(entries: readonly string[]): Record<string, string>;
|
|
45
|
+
/** The conversation a process belongs to, from its environment (terminal header first). */
|
|
46
|
+
export declare function conversationOf(env: Readonly<Record<string, string>>): {
|
|
47
|
+
id: string;
|
|
48
|
+
rc: boolean;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* The tokens after the first `--` of `mnemom agent`'s argv; [] when the argv was read and has
|
|
52
|
+
* none (the launcher takes no flag passthrough without `--`, so the split is still exact); null
|
|
53
|
+
* when the parent's argv could not be read or is not a `mnemom agent` launch (the split is then by
|
|
54
|
+
* shape, see splitLauncherArgv).
|
|
55
|
+
*/
|
|
56
|
+
export declare function launcherPassthrough(argv: readonly string[] | undefined): string[] | null;
|
|
57
|
+
/** A durable seat's claude (C2 sets MNEMOM_SEAT_* in its env): never an original to adopt. */
|
|
58
|
+
export declare function isSeatEnv(env: Readonly<Record<string, string>>): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The process is the claude binary itself (argv[0] names it), not a hook, MCP server or tool that
|
|
61
|
+
* inherited the session's environment: such a child outlives a dead claude (reparented to init) and
|
|
62
|
+
* must never read as the live session.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isClaudeArgv0(argv0: string | undefined): boolean;
|
|
65
|
+
/** The live tracked claudes in a snapshot, one per conversation (the lowest pid on a tie). */
|
|
66
|
+
export declare function findLiveClaudes(snap: ProcSnapshot, selfPid: number): LiveClaude[];
|
|
67
|
+
/** A snapshot of this user's processes (argv and environment read exactly). */
|
|
68
|
+
export declare function liveSnapshot(deps?: ProcDeps, uid?: number): ProcSnapshot;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step-2 discovery of BACKBONE C1/C02-R26: the live tracked claude processes on this machine,
|
|
3
|
+
* one per conversation, each with its exact argv (KERN_PROCARGS2 / /proc, never ps), environment
|
|
4
|
+
* and cwd.
|
|
5
|
+
*
|
|
6
|
+
* A tracked claude is the process `mnemom agent` exec'd: its environment carries the session name
|
|
7
|
+
* (MNEMOM_AGENT_SESSION_NAME) and the conversation id (the `x-mnemom-conversation-id` line of
|
|
8
|
+
* ANTHROPIC_CUSTOM_HEADERS on the terminal shape, MNEMOM_AGENT_RC_CONVERSATION_ID on the RC shape).
|
|
9
|
+
* Its descendants (hooks, MCP servers, tools) inherit that environment, so the claude is the
|
|
10
|
+
* topmost process of the conversation: the one whose parent does not carry the same conversation
|
|
11
|
+
* id. The parent launcher's own argv is read too: its tokens after `--` pin the passthrough split.
|
|
12
|
+
*
|
|
13
|
+
* Pure over an injected snapshot (`ProcSnapshot`); `liveSnapshot` is the thin driver.
|
|
14
|
+
*/
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
import { liveProcDeps, processTable, readManyProcArgs, readProcCwd, readProcStart, } from "./proc.js";
|
|
17
|
+
export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
18
|
+
export const RC_CONVERSATION_ENV = "MNEMOM_AGENT_RC_CONVERSATION_ID";
|
|
19
|
+
const CONVERSATION_HEADER_RE = /^x-mnemom-conversation-id:\s*(\S+)\s*$/im;
|
|
20
|
+
const AGENT_HEADER_RE = /^x-mnemom-agent:\s*(\S+)\s*$/im;
|
|
21
|
+
/** The agent slug the original runs under (its x-mnemom-agent header), or null. */
|
|
22
|
+
export function agentSlugOf(env) {
|
|
23
|
+
const m = AGENT_HEADER_RE.exec(env.ANTHROPIC_CUSTOM_HEADERS ?? "");
|
|
24
|
+
return m ? m[1] : null;
|
|
25
|
+
}
|
|
26
|
+
/** `KEY=value` entries as a record (the first `=` splits). */
|
|
27
|
+
export function envRecord(entries) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const i = e.indexOf("=");
|
|
31
|
+
if (i > 0)
|
|
32
|
+
out[e.slice(0, i)] = e.slice(i + 1);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** The conversation a process belongs to, from its environment (terminal header first). */
|
|
37
|
+
export function conversationOf(env) {
|
|
38
|
+
const m = CONVERSATION_HEADER_RE.exec(env.ANTHROPIC_CUSTOM_HEADERS ?? "");
|
|
39
|
+
if (m)
|
|
40
|
+
return { id: m[1], rc: false };
|
|
41
|
+
const rc = env[RC_CONVERSATION_ENV];
|
|
42
|
+
return rc ? { id: rc, rc: true } : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The tokens after the first `--` of `mnemom agent`'s argv; [] when the argv was read and has
|
|
46
|
+
* none (the launcher takes no flag passthrough without `--`, so the split is still exact); null
|
|
47
|
+
* when the parent's argv could not be read or is not a `mnemom agent` launch (the split is then by
|
|
48
|
+
* shape, see splitLauncherArgv).
|
|
49
|
+
*/
|
|
50
|
+
export function launcherPassthrough(argv) {
|
|
51
|
+
if (!argv)
|
|
52
|
+
return null;
|
|
53
|
+
const i = argv.indexOf("--");
|
|
54
|
+
// A parent that is not `… agent …` (a wrapper, a shell) says nothing about the split.
|
|
55
|
+
if (!(i < 0 ? argv : argv.slice(0, i)).includes("agent"))
|
|
56
|
+
return null;
|
|
57
|
+
return i < 0 ? [] : argv.slice(i + 1);
|
|
58
|
+
}
|
|
59
|
+
/** A durable seat's claude (C2 sets MNEMOM_SEAT_* in its env): never an original to adopt. */
|
|
60
|
+
export function isSeatEnv(env) {
|
|
61
|
+
return Object.keys(env).some((k) => k.startsWith("MNEMOM_SEAT_"));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The process is the claude binary itself (argv[0] names it), not a hook, MCP server or tool that
|
|
65
|
+
* inherited the session's environment: such a child outlives a dead claude (reparented to init) and
|
|
66
|
+
* must never read as the live session.
|
|
67
|
+
*/
|
|
68
|
+
export function isClaudeArgv0(argv0) {
|
|
69
|
+
return argv0 !== undefined && basename(argv0) === "claude";
|
|
70
|
+
}
|
|
71
|
+
/** The live tracked claudes in a snapshot, one per conversation (the lowest pid on a tie). */
|
|
72
|
+
export function findLiveClaudes(snap, selfPid) {
|
|
73
|
+
const rows = new Map(snap.table.map((r) => [r.pid, r]));
|
|
74
|
+
const convOf = (pid) => {
|
|
75
|
+
const a = snap.args.get(pid);
|
|
76
|
+
return a ? conversationOf(envRecord(a.env)) : null;
|
|
77
|
+
};
|
|
78
|
+
const byConv = new Map();
|
|
79
|
+
for (const row of [...snap.table].sort((a, b) => a.pid - b.pid)) {
|
|
80
|
+
if (row.pid === selfPid)
|
|
81
|
+
continue;
|
|
82
|
+
const a = snap.args.get(row.pid);
|
|
83
|
+
if (!a)
|
|
84
|
+
continue;
|
|
85
|
+
const env = envRecord(a.env);
|
|
86
|
+
const conv = conversationOf(env);
|
|
87
|
+
const name = env[SESSION_NAME_ENV];
|
|
88
|
+
if (!conv || !name || isSeatEnv(env))
|
|
89
|
+
continue;
|
|
90
|
+
if (!isClaudeArgv0(a.argv[0]))
|
|
91
|
+
continue; // a hook, MCP server or tool, never the session
|
|
92
|
+
if (convOf(row.ppid)?.id === conv.id)
|
|
93
|
+
continue; // a descendant of the claude
|
|
94
|
+
if (byConv.has(conv.id))
|
|
95
|
+
continue;
|
|
96
|
+
const parent = rows.has(row.ppid) ? snap.args.get(row.ppid) : undefined;
|
|
97
|
+
byConv.set(conv.id, {
|
|
98
|
+
pid: row.pid,
|
|
99
|
+
ppid: row.ppid,
|
|
100
|
+
conversationId: conv.id,
|
|
101
|
+
sessionName: name,
|
|
102
|
+
argv: a.argv,
|
|
103
|
+
env,
|
|
104
|
+
cwd: snap.cwd(row.pid),
|
|
105
|
+
passthroughHint: launcherPassthrough(parent?.argv),
|
|
106
|
+
remoteControl: conv.rc,
|
|
107
|
+
start: snap.start(row.pid),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return [...byConv.values()];
|
|
111
|
+
}
|
|
112
|
+
/** A snapshot of this user's processes (argv and environment read exactly). */
|
|
113
|
+
export function liveSnapshot(deps = liveProcDeps, uid = process.getuid?.() ?? -1) {
|
|
114
|
+
const table = processTable(deps);
|
|
115
|
+
const mine = table.filter((r) => r.uid === uid).map((r) => r.pid);
|
|
116
|
+
const args = readManyProcArgs(mine, deps);
|
|
117
|
+
return {
|
|
118
|
+
table,
|
|
119
|
+
args,
|
|
120
|
+
cwd: (pid) => readProcCwd(pid, deps),
|
|
121
|
+
start: (pid) => readProcStart(pid, deps),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The off-state answer of a durable verb the CLI registers hidden (C14-R22: with the flag off
|
|
3
|
+
* `sessions adopt` does not exist; BACKBONE Flags/CLI: every durable verb word, `--help` too,
|
|
4
|
+
* exits 2 with gatedMessage(…, 'durable') and no side effects). Both hold at once: commander's own
|
|
5
|
+
* unknown-command line, then the gate pointer. The caller prints the lines on stderr and exits 2.
|
|
6
|
+
*/
|
|
7
|
+
import { type ExperimentalId } from "../lib/experimental.js";
|
|
8
|
+
export declare function durableVerbOffLines(word: string, what: string, enabled: readonly ExperimentalId[]): string[] | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The off-state answer of a durable verb the CLI registers hidden (C14-R22: with the flag off
|
|
3
|
+
* `sessions adopt` does not exist; BACKBONE Flags/CLI: every durable verb word, `--help` too,
|
|
4
|
+
* exits 2 with gatedMessage(…, 'durable') and no side effects). Both hold at once: commander's own
|
|
5
|
+
* unknown-command line, then the gate pointer. The caller prints the lines on stderr and exits 2.
|
|
6
|
+
*/
|
|
7
|
+
import { gateCheck } from "../lib/experimental.js";
|
|
8
|
+
export function durableVerbOffLines(word, what, enabled) {
|
|
9
|
+
const pointer = gateCheck(what, "durable", enabled);
|
|
10
|
+
return pointer === null
|
|
11
|
+
? null
|
|
12
|
+
: [`error: unknown command '${word}' (durable sessions are off)`, pointer];
|
|
13
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one launch-spec builder, buildDurableLaunchSpec (BACKBONE C4; C14-R01..R05, R12, R13; S1
|
|
3
|
+
* spec.ts). Every path that commits a spec (the lock verb, /mnemom lock, the launcher's
|
|
4
|
+
* durable=always path, the API lock route's validation and `sessions adopt`) builds it here.
|
|
5
|
+
*
|
|
6
|
+
* Pure: no I/O. The callers read the argv (the running source reads the original claude's argv
|
|
7
|
+
* exactly, the idle source passes the launcher's resolved continue argv) and resolve the cwd.
|
|
8
|
+
*
|
|
9
|
+
* Choices (each from S1 or the C14 spec, none invented here):
|
|
10
|
+
* - Only --model <v>, --effort <v> and --trim-mcp reach a committed spec (S1 SEAT_USER_FLAGS).
|
|
11
|
+
* - Names follow ruling 15 (S1 droppedArgvNames): left to right, flags verbatim, deduplicated,
|
|
12
|
+
* then one `positional arguments (<n>)` entry; tokens after claude's own `--` are positionals.
|
|
13
|
+
* - An allowlisted flag whose value the closed S1 schema cannot hold (MODEL_RE, EFFORT_VALUES:
|
|
14
|
+
* newlines, shell metacharacters, unknown effort) is dropped and named instead (C14-R03, R05,
|
|
15
|
+
* R23); the last valid occurrence of a repeated flag wins, as claude's parser does.
|
|
16
|
+
* - The yolo-default bypass flag the launcher injects counts as dropped and is named (S1
|
|
17
|
+
* spec.ts freeze; C14-R12), and a dropped --remote-control (explicit or auto-upgraded) is
|
|
18
|
+
* always named (M1 is terminal-only).
|
|
19
|
+
* - The spec holds no goal, prompt, positional, remote-control field, plugin path or hash: the
|
|
20
|
+
* plugin is the token `bundled-mnemom` (C14-R05, R26).
|
|
21
|
+
*/
|
|
22
|
+
import { EFFORT_VALUES, type LaunchSpec, type LauncherKey } from "../vendor/durable-contract/index.js";
|
|
23
|
+
/** The bypass flag the launcher's yolo default injects (cli/src/commands/agent.ts buildCliArgs). */
|
|
24
|
+
export declare const YOLO_FLAG = "--dangerously-skip-permissions";
|
|
25
|
+
/** The launcher option for Remote Control; never committed in M1, always named when dropped. */
|
|
26
|
+
export declare const REMOTE_CONTROL_FLAG = "--remote-control";
|
|
27
|
+
/** The user flags a spec can carry, after the filter. */
|
|
28
|
+
export interface KeptFlags {
|
|
29
|
+
model?: string;
|
|
30
|
+
effort?: (typeof EFFORT_VALUES)[number];
|
|
31
|
+
trim_mcp: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The C14-R01..R03 filter over a claude-level argv (after argv[0]): the flags a seat keeps, and
|
|
35
|
+
* the allowlisted flags dropped because their value is missing or not schema-valid. Every other
|
|
36
|
+
* name comes from droppedArgvNames (S1), so the two never disagree.
|
|
37
|
+
*/
|
|
38
|
+
export declare function filterSeatArgv(argv: readonly string[]): {
|
|
39
|
+
kept: KeptFlags;
|
|
40
|
+
invalid: string[];
|
|
41
|
+
};
|
|
42
|
+
/** The dropped-flag names (ruling 15) plus the invalid-value names, positional entry kept last. */
|
|
43
|
+
export declare function droppedNames(argv: readonly string[]): string[];
|
|
44
|
+
/** What the one builder reads (S1 BUILDER_INPUT_TABLE). */
|
|
45
|
+
export interface BuilderInput {
|
|
46
|
+
/** `running`: the original claude's argv read exactly; `idle`: the launcher's continue argv. */
|
|
47
|
+
source: "running" | "idle";
|
|
48
|
+
/**
|
|
49
|
+
* The argv the spec's flags come from, claude-level, without the daemon-only flags: the
|
|
50
|
+
* launcher's own --model/--effort/--trim-mcp plus the user's passthrough
|
|
51
|
+
* (semanticArgvOf splits an original claude argv into these).
|
|
52
|
+
*/
|
|
53
|
+
argv: readonly string[];
|
|
54
|
+
/** The user's tokens whose dropped flags are named (the passthrough). Defaults to `argv`. */
|
|
55
|
+
userArgv?: readonly string[];
|
|
56
|
+
/** The launcher injected the yolo-default bypass flag (named as dropped, S1 freeze). */
|
|
57
|
+
yoloInjected?: boolean;
|
|
58
|
+
/** The launch was (or would have auto-upgraded to) Remote Control: named, never committed. */
|
|
59
|
+
remoteControl?: boolean;
|
|
60
|
+
/** The registered worktree root (realpath, checked by the C4 cwd rule before this call). */
|
|
61
|
+
cwd: string;
|
|
62
|
+
}
|
|
63
|
+
export interface BuiltLaunchSpec {
|
|
64
|
+
spec: LaunchSpec;
|
|
65
|
+
/** The 'flags dropped:' list (ruling 15), unchanged into the Note, lock and non-TTY lines. */
|
|
66
|
+
dropped: string[];
|
|
67
|
+
/** The canonical spec hash (S1 specHash): what the charter commits. */
|
|
68
|
+
hash: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* buildDurableLaunchSpec: the committed, goal-elided spec and the names of what it dropped.
|
|
72
|
+
* Throws a RangeError when the result would not validate against the closed S1 schema (a cwd
|
|
73
|
+
* that is not absolute, or holds a newline): a caller bug, never a user input path.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildDurableLaunchSpec(input: BuilderInput): Promise<BuiltLaunchSpec>;
|
|
76
|
+
/** The seat's user-flag argv for a spec (the daemon adds its own flags around it), C14-R01. */
|
|
77
|
+
export declare function specUserArgv(spec: LaunchSpec): string[];
|
|
78
|
+
/** The canonical multiset of a spec's user flags, for comparing with an argv (S1 canonicalArgvPairs). */
|
|
79
|
+
export declare function specUserPairs(spec: LaunchSpec): string[];
|
|
80
|
+
/** The original claude argv split into what the launcher injected and the user's passthrough. */
|
|
81
|
+
export interface LauncherArgvSplit {
|
|
82
|
+
/** --model/--effort/--trim-mcp as the launcher rendered them (the spec's launcher half). */
|
|
83
|
+
launcherArgv: string[];
|
|
84
|
+
passthrough: string[];
|
|
85
|
+
yoloInjected: boolean;
|
|
86
|
+
/** The --session-id the launcher pinned, if any. */
|
|
87
|
+
sessionId: string | null;
|
|
88
|
+
/**
|
|
89
|
+
* The split is known exactly: the parent launcher's passthrough pinned it, or the parent was
|
|
90
|
+
* read and had no `--` (so the user passed no flags). Otherwise it was read by shape.
|
|
91
|
+
*/
|
|
92
|
+
exact: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* When not exact: the launcher-shaped flags read by shape that a user could also have passed
|
|
95
|
+
* (--session-id, --plugin-dir, the trailing --mcp-config). They are named as dropped, so a
|
|
96
|
+
* user's own flag is never silently taken as launcher-injected (C14-R02, R12, R26). --model,
|
|
97
|
+
* --effort and the yolo flag are never listed: they are kept or named whoever passed them.
|
|
98
|
+
*/
|
|
99
|
+
ambiguous: string[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Split an original claude argv (after argv[0]) the way `mnemom agent` built it (buildCliArgs):
|
|
103
|
+
* [--model v] [--effort v] [yolo] [--session-id id] [--plugin-dir dir] <passthrough>
|
|
104
|
+
* [--strict-mcp-config] [--mcp-config <v>]. `passthroughHint` (the tokens after the parent
|
|
105
|
+
* launcher's `--`, read from its own argv) pins the split exactly when it occurs where the
|
|
106
|
+
* layout allows; an empty hint (the parent was read and had no `--`) makes the shape read exact;
|
|
107
|
+
* without a hint (null or absent) the prefix and suffix are read by shape and reported ambiguous.
|
|
108
|
+
*/
|
|
109
|
+
export declare function splitLauncherArgv(argv: readonly string[], passthroughHint?: readonly string[] | null): LauncherArgvSplit;
|
|
110
|
+
/** The running-source builder input for an original claude's exact argv and its checked cwd. */
|
|
111
|
+
export declare function runningSourceInput(originalArgv: readonly string[], cwd: string, opts?: {
|
|
112
|
+
passthroughHint?: readonly string[] | null;
|
|
113
|
+
remoteControl?: boolean;
|
|
114
|
+
}): BuilderInput;
|
|
115
|
+
/** The re-lock comparison of a built spec with the committed one, by the S1 canonical hash. */
|
|
116
|
+
export type SpecCompare = {
|
|
117
|
+
changed: false;
|
|
118
|
+
} | {
|
|
119
|
+
changed: true;
|
|
120
|
+
status: 422;
|
|
121
|
+
code: "durable_unsupported";
|
|
122
|
+
feature: "launch_spec_change";
|
|
123
|
+
};
|
|
124
|
+
export declare function compareWithCommitted(built: LaunchSpec, committed: LaunchSpec | string): Promise<SpecCompare>;
|
|
125
|
+
/**
|
|
126
|
+
* The fix lines printed with a launch_spec_change refusal: the S1 `launch_spec_change` entry
|
|
127
|
+
* under the fix-line rule (the locked row holds the name, so the done clause applies), plus the
|
|
128
|
+
* sole-unlock entry only when this is the agent's sole locked session (C14-R11).
|
|
129
|
+
*/
|
|
130
|
+
export declare function launchSpecChangeFixLines(ctx: {
|
|
131
|
+
name: string;
|
|
132
|
+
flags: string;
|
|
133
|
+
launcherKey: LauncherKey;
|
|
134
|
+
soleLockedSession: boolean;
|
|
135
|
+
}): [string, ...string[]];
|
|
136
|
+
/** The adopt Note for an adopted row (C14-R12, R21): S1 grammar, unchanged builder names. */
|
|
137
|
+
export declare function adoptNote(dropped: readonly string[]): string;
|