tokenmaxxing 1.8.0 → 1.9.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.
Files changed (85) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +0 -40
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +1 -33
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -73
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -34
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +6 -127
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -48
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -40
  67. package/src/sdk.ts +0 -59
  68. package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
  69. package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
  70. package/agent-plugin/hooks/cursor-relay.json +0 -14
  71. package/agent-plugin/skills/relay-session/SKILL.md +0 -118
  72. package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
  73. package/src/cli/relay.ts +0 -323
  74. package/src/entries/relaypermission.ts +0 -105
  75. package/src/lib/relay/config.ts +0 -84
  76. package/src/lib/relay/decide.ts +0 -75
  77. package/src/lib/relay/gc.ts +0 -80
  78. package/src/lib/relay/install.ts +0 -143
  79. package/src/lib/relay/markers.ts +0 -148
  80. package/src/lib/relay/modes.ts +0 -82
  81. package/src/lib/relay/protocol.ts +0 -61
  82. package/src/lib/relay/registry.ts +0 -175
  83. package/src/lib/relay/tmux.ts +0 -109
  84. package/src/lib/relay/turn.ts +0 -137
  85. package/src/lib/relay/worker.ts +0 -141
@@ -1,105 +0,0 @@
1
- // PermissionRequest hook for relay workers. When TOKENMAXXING_RELAY_SESSION is
2
- // set, park on a pending marker until `relay decide` writes a decision, then
3
- // return allow/deny. Under bypassPermissions (or when pings are disabled),
4
- // auto-allow without surfacing. Never blocks non-relay sessions. Always exits 0
5
- // with a JSON decision body Claude understands.
6
-
7
- import { delay } from "es-toolkit";
8
- import { z } from "zod";
9
- import { loadRelayConfig } from "../lib/relay/config.ts";
10
- import {
11
- clearDecision,
12
- readDecision,
13
- writePendingRequest,
14
- } from "../lib/relay/markers.ts";
15
- import { permissionPingsEnabled } from "../lib/relay/modes.ts";
16
- import { readEntry, registryHas } from "../lib/relay/registry.ts";
17
- import { RELAY_SESSION_ENV } from "../lib/relay/worker.ts";
18
- import { log } from "../lib/log.ts";
19
-
20
- const StdinSchema = z.looseObject({
21
- tool_name: z.string().optional(),
22
- tool_input: z.unknown().optional(),
23
- request_id: z.string().optional(),
24
- permission_suggestions: z.unknown().optional(),
25
- });
26
-
27
- async function readStdin(): Promise<string> {
28
- const chunks: Uint8Array[] = [];
29
- for await (const c of Bun.stdin.stream()) chunks.push(c);
30
- return Buffer.concat(chunks).toString("utf8");
31
- }
32
-
33
- function allow(): string {
34
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } } });
35
- }
36
-
37
- function deny(): string {
38
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "deny" } } });
39
- }
40
-
41
- function fallbackAllow(): string {
42
- // Unknown Claude envelope shapes: fail open for non-relay; for relay we still
43
- // prefer an explicit decision. Default allow keeps the worker unblocked if
44
- // the host never answers within timeout only after we already tried.
45
- return allow();
46
- }
47
-
48
- export async function handleRelayPermission(input: { rawStdin: string }): Promise<string> {
49
- if (process.env.TOKENMAXXING_PROBE) return allow();
50
-
51
- const sessionId = process.env[RELAY_SESSION_ENV];
52
- if (sessionId == null || sessionId === "") return allow();
53
- if (!registryHas({ sessionId })) return allow();
54
-
55
- const entry = readEntry({ sessionId });
56
- if (entry != null && !permissionPingsEnabled({ mode: entry.permissionMode })) {
57
- return allow();
58
- }
59
-
60
- const parsed = StdinSchema.safeParse((() => {
61
- try {
62
- return JSON.parse(input.rawStdin);
63
- } catch {
64
- return {};
65
- }
66
- })());
67
- const requestId = parsed.success && parsed.data.request_id
68
- ? parsed.data.request_id
69
- : crypto.randomUUID();
70
- const toolName = parsed.success ? (parsed.data.tool_name ?? "tool") : "tool";
71
- const detail = parsed.success
72
- ? JSON.stringify(parsed.data.tool_input ?? parsed.data).slice(0, 500)
73
- : input.rawStdin.slice(0, 500);
74
-
75
- writePendingRequest({
76
- sessionId,
77
- requestId,
78
- summary: `Permission needed: ${toolName}`,
79
- detail,
80
- });
81
-
82
- const cfg = loadRelayConfig();
83
- const deadline = Date.now() + cfg.decideTimeoutMs;
84
- while (Date.now() < deadline) {
85
- const decision = readDecision({ sessionId, requestId });
86
- if (decision != null) {
87
- clearDecision({ sessionId, requestId });
88
- return decision.approve ? allow() : deny();
89
- }
90
- await delay(50);
91
- }
92
- log("relay.permission_timeout", { session: sessionId.slice(0, 8), requestId: requestId.slice(0, 8) });
93
- return deny();
94
- }
95
-
96
- export async function runRelayPermissionHook(): Promise<number> {
97
- try {
98
- const out = await handleRelayPermission({ rawStdin: await readStdin() });
99
- process.stdout.write(out);
100
- } catch (e) {
101
- log("relay.permission_error", { err: e instanceof Error ? e.message : String(e) });
102
- process.stdout.write(fallbackAllow());
103
- }
104
- return 0;
105
- }
@@ -1,84 +0,0 @@
1
- // $TOKENMAXXING_HOME/relay.json - sparse overrides merged with defaults.
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { z } from "zod";
5
- import { paths } from "../paths.ts";
6
- import { writeFileAtomic } from "../atomic.ts";
7
- import { ClaudePermissionModeSchema } from "./modes.ts";
8
-
9
- const WorkerSchema = z.enum(["claude", "codex"]);
10
- export type RelayWorker = z.infer<typeof WorkerSchema>;
11
-
12
- const HostsSchema = z.object({
13
- cursor: z.object({ model: z.string().min(1).optional() }).default({}),
14
- claude: z.object({ model: z.string().min(1).optional() }).default({}),
15
- }).default({ cursor: {}, claude: {} });
16
-
17
- export const RelayConfigFileSchema = z.object({
18
- defaultWorker: WorkerSchema.optional(),
19
- defaultPermissionMode: ClaudePermissionModeSchema.optional(),
20
- turnTimeoutMs: z.number().int().positive().optional(),
21
- decideTimeoutMs: z.number().int().positive().optional(),
22
- idleTtlMs: z.number().int().positive().optional(),
23
- sameSessionBusyMs: z.number().int().positive().optional(),
24
- sessionPrefix: z.string().min(1).optional(),
25
- hosts: HostsSchema.optional(),
26
- });
27
- export type RelayConfigFile = z.infer<typeof RelayConfigFileSchema>;
28
-
29
- export const RelayConfigSchema = z.object({
30
- defaultWorker: WorkerSchema,
31
- defaultPermissionMode: ClaudePermissionModeSchema,
32
- turnTimeoutMs: z.number().int().positive(),
33
- decideTimeoutMs: z.number().int().positive(),
34
- idleTtlMs: z.number().int().positive(),
35
- sameSessionBusyMs: z.number().int().positive(),
36
- sessionPrefix: z.string().min(1),
37
- hosts: z.object({
38
- cursor: z.object({ model: z.string().min(1).optional() }),
39
- claude: z.object({ model: z.string().min(1).optional() }),
40
- }),
41
- });
42
- export type RelayConfig = z.infer<typeof RelayConfigSchema>;
43
-
44
- export const DEFAULT_RELAY_CONFIG: RelayConfig = {
45
- defaultWorker: "claude",
46
- defaultPermissionMode: "auto",
47
- turnTimeoutMs: 30 * 60 * 1000,
48
- decideTimeoutMs: 30 * 60 * 1000,
49
- idleTtlMs: 60 * 60 * 1000,
50
- sameSessionBusyMs: 50,
51
- sessionPrefix: "xx-relay-",
52
- hosts: { cursor: {}, claude: {} },
53
- };
54
-
55
- export function loadRelayConfig(): RelayConfig {
56
- if (!existsSync(paths.relayJson)) return DEFAULT_RELAY_CONFIG;
57
- const raw = RelayConfigFileSchema.parse(JSON.parse(readFileSync(paths.relayJson, "utf8")));
58
- return RelayConfigSchema.parse({
59
- ...DEFAULT_RELAY_CONFIG,
60
- ...raw,
61
- hosts: {
62
- cursor: { ...DEFAULT_RELAY_CONFIG.hosts.cursor, ...raw.hosts?.cursor },
63
- claude: { ...DEFAULT_RELAY_CONFIG.hosts.claude, ...raw.hosts?.claude },
64
- },
65
- defaultPermissionMode: raw.defaultPermissionMode ?? DEFAULT_RELAY_CONFIG.defaultPermissionMode,
66
- });
67
- }
68
-
69
- export function writeRelayConfig(input: { file: RelayConfigFile }): void {
70
- const validated = RelayConfigFileSchema.parse(input.file);
71
- writeFileAtomic(paths.relayJson, JSON.stringify(validated, null, 2) + "\n");
72
- }
73
-
74
- export function mergeRelayConfigFile(input: { patch: RelayConfigFile }): RelayConfig {
75
- const existing = existsSync(paths.relayJson)
76
- ? RelayConfigFileSchema.parse(JSON.parse(readFileSync(paths.relayJson, "utf8")))
77
- : {};
78
- const next = RelayConfigFileSchema.parse({ ...existing, ...input.patch, hosts: {
79
- cursor: { ...existing.hosts?.cursor, ...input.patch.hosts?.cursor },
80
- claude: { ...existing.hosts?.claude, ...input.patch.hosts?.claude },
81
- } });
82
- writeRelayConfig({ file: next });
83
- return loadRelayConfig();
84
- }
@@ -1,75 +0,0 @@
1
- // relay decide: approve/deny a pending permission ping and resume the wait.
2
-
3
- import { delay } from "es-toolkit";
4
- import { loadRelayConfig } from "./config.ts";
5
- import {
6
- clearPendingRequest,
7
- listPendingRequests,
8
- readPendingRequest,
9
- writeDecision,
10
- } from "./markers.ts";
11
- import { runTurn, type TurnResult } from "./turn.ts";
12
- import { readEntry, touchEntry, withSessionLock } from "./registry.ts";
13
-
14
- export type DecideParams = {
15
- sessionId: string;
16
- requestId?: string;
17
- approve: boolean;
18
- /** After writing the decision, wait for turn-done or the next ping. */
19
- wait?: boolean;
20
- cwd?: string;
21
- timeoutMs?: number;
22
- now?: () => number;
23
- sleep?: (ms: number) => Promise<void>;
24
- };
25
-
26
- export async function runDecide(input: DecideParams): Promise<{
27
- decisionWritten: boolean;
28
- requestId: string;
29
- turn?: TurnResult;
30
- }> {
31
- const entry = readEntry({ sessionId: input.sessionId });
32
- if (entry == null) throw new Error(`relay session not found: ${input.sessionId}`);
33
-
34
- let requestId = input.requestId ?? entry.pendingRequestId;
35
- if (requestId == null) {
36
- const pending = listPendingRequests({ sessionId: input.sessionId });
37
- requestId = pending[0]?.requestId;
38
- }
39
- if (requestId == null) throw new Error(`no pending permission request for session ${input.sessionId}`);
40
-
41
- const pending = readPendingRequest({ sessionId: input.sessionId, requestId });
42
- if (pending == null) throw new Error(`pending request not found: ${requestId}`);
43
-
44
- writeDecision({
45
- sessionId: input.sessionId,
46
- requestId,
47
- approve: input.approve,
48
- now: (input.now ?? Date.now)(),
49
- });
50
- clearPendingRequest({ sessionId: input.sessionId, requestId });
51
- await withSessionLock({
52
- sessionId: input.sessionId,
53
- fn: () => touchEntry({
54
- sessionId: input.sessionId,
55
- state: "running",
56
- pendingRequestId: null,
57
- now: (input.now ?? Date.now)(),
58
- }),
59
- });
60
-
61
- if (input.wait === false) {
62
- return { decisionWritten: true, requestId };
63
- }
64
-
65
- const cfg = loadRelayConfig();
66
- const turn = await runTurn({
67
- sessionId: input.sessionId,
68
- cwd: input.cwd ?? entry.cwd,
69
- waitOnly: true,
70
- timeoutMs: input.timeoutMs ?? cfg.decideTimeoutMs,
71
- now: input.now,
72
- sleep: input.sleep ?? delay,
73
- });
74
- return { decisionWritten: true, requestId, turn };
75
- }
@@ -1,80 +0,0 @@
1
- // relay destroy / gc / status. Exact tmux session names only; never pattern-kill.
2
-
3
- import { rmSync } from "node:fs";
4
- import { loadRelayConfig } from "./config.ts";
5
- import { clearSessionMarkers } from "./markers.ts";
6
- import {
7
- deleteEntry,
8
- listEntries,
9
- readEntry,
10
- sessionLockPath,
11
- withSessionLock,
12
- type RelayRegistryEntry,
13
- } from "./registry.ts";
14
- import { getTmux } from "./tmux.ts";
15
-
16
- export async function destroySession(input: { sessionId: string }): Promise<boolean> {
17
- const entry = readEntry({ sessionId: input.sessionId });
18
- if (entry == null) {
19
- // Still try exact tmux name from config prefix in case registry was lost.
20
- return false;
21
- }
22
- await withSessionLock({
23
- sessionId: input.sessionId,
24
- fn: () => {
25
- getTmux().killSession({ name: entry.tmuxName });
26
- clearSessionMarkers({ sessionId: input.sessionId });
27
- deleteEntry({ sessionId: input.sessionId });
28
- rmSync(sessionLockPath({ sessionId: input.sessionId }), { force: true });
29
- },
30
- });
31
- return true;
32
- }
33
-
34
- export type GcResult = {
35
- reaped: string[];
36
- kept: string[];
37
- };
38
-
39
- /** Reap dead tmux sessions and idle sessions past idleTtlMs. */
40
- export async function gcSessions(input: { now?: number; idleTtlMs?: number } = {}): Promise<GcResult> {
41
- const cfg = loadRelayConfig();
42
- const now = input.now ?? Date.now();
43
- const idleTtlMs = input.idleTtlMs ?? cfg.idleTtlMs;
44
- const reaped: string[] = [];
45
- const kept: string[] = [];
46
- const tmux = getTmux();
47
-
48
- for (const entry of listEntries()) {
49
- const alive = tmux.hasSession({ name: entry.tmuxName });
50
- const idleTooLong = now - entry.lastActiveAt > idleTtlMs;
51
- if (!alive || idleTooLong || entry.state === "destroyed") {
52
- await destroySession({ sessionId: entry.sessionId });
53
- reaped.push(entry.sessionId);
54
- continue;
55
- }
56
- kept.push(entry.sessionId);
57
- }
58
- return { reaped, kept };
59
- }
60
-
61
- export function statusSessions(): RelayRegistryEntry[] {
62
- const tmux = getTmux();
63
- return listEntries().map((entry) => ({
64
- ...entry,
65
- // annotate liveness in a non-schema field via spread for printers; keep schema pure
66
- })).map((entry) => {
67
- void tmux.hasSession({ name: entry.tmuxName });
68
- return entry;
69
- });
70
- }
71
-
72
- export type StatusRow = RelayRegistryEntry & { tmuxAlive: boolean };
73
-
74
- export function statusRows(): StatusRow[] {
75
- const tmux = getTmux();
76
- return listEntries().map((entry) => ({
77
- ...entry,
78
- tmuxAlive: tmux.hasSession({ name: entry.tmuxName }),
79
- }));
80
- }
@@ -1,143 +0,0 @@
1
- // relay install: write thin host agent templates + optional hook snippets.
2
- // Merge only tokenmaxxing-owned keys.
3
-
4
- import { existsSync, mkdirSync, readFileSync, cpSync } from "node:fs";
5
- import { dirname, join } from "node:path";
6
- import { z } from "zod";
7
- import { writeFileAtomic } from "../atomic.ts";
8
- import { paths } from "../paths.ts";
9
- import { isOurHookCommand } from "../settings.ts";
10
-
11
- const pluginRoot = () => join(import.meta.dir, "../../../agent-plugin");
12
-
13
- function repoAgentsDir(): string {
14
- return join(pluginRoot(), "agents");
15
- }
16
-
17
- function repoSkillDir(): string {
18
- return join(pluginRoot(), "skills", "relay-session");
19
- }
20
-
21
- function repoHooksDir(): string {
22
- return join(pluginRoot(), "hooks");
23
- }
24
-
25
- export type InstallTarget = "cursor" | "claude" | "all";
26
-
27
- export type InstallResult = {
28
- agentsWritten: string[];
29
- skillWritten: boolean;
30
- hooksMerged: string[];
31
- };
32
-
33
- function copyAgent(input: { name: string; destDir: string }): string {
34
- mkdirSync(input.destDir, { recursive: true });
35
- const src = join(repoAgentsDir(), input.name);
36
- const dest = join(input.destDir, input.name);
37
- if (!existsSync(src)) throw new Error(`missing agent template: ${src}`);
38
- cpSync(src, dest);
39
- return dest;
40
- }
41
-
42
- function copySkill(input: { destDir: string }): boolean {
43
- const src = repoSkillDir();
44
- if (!existsSync(src)) return false;
45
- mkdirSync(input.destDir, { recursive: true });
46
- cpSync(src, input.destDir, { recursive: true });
47
- return true;
48
- }
49
-
50
- const CursorHooksSchema = z.looseObject({
51
- version: z.number().optional(),
52
- hooks: z.record(z.string(), z.unknown()).optional(),
53
- });
54
-
55
- const TOKENMAXXING_HOOK_KEY = "tokenmaxxingRelay";
56
-
57
- /** Merge Cursor hooks.json: only the tokenmaxxingRelay key is ours. */
58
- export function mergeCursorHooks(input: { hooksPath: string }): boolean {
59
- const snippetPath = join(repoHooksDir(), "cursor-relay.json");
60
- if (!existsSync(snippetPath)) return false;
61
- const snippet = CursorHooksSchema.parse(JSON.parse(readFileSync(snippetPath, "utf8")));
62
- let existing: z.infer<typeof CursorHooksSchema> = { version: 1, hooks: {} };
63
- if (existsSync(input.hooksPath)) {
64
- try {
65
- existing = CursorHooksSchema.parse(JSON.parse(readFileSync(input.hooksPath, "utf8")));
66
- } catch {
67
- existing = { version: 1, hooks: {} };
68
- }
69
- }
70
- existing.hooks ??= {};
71
- const ours = snippet.hooks?.[TOKENMAXXING_HOOK_KEY];
72
- if (ours === undefined) return false;
73
- existing.hooks[TOKENMAXXING_HOOK_KEY] = ours;
74
- mkdirSync(dirname(input.hooksPath), { recursive: true });
75
- writeFileAtomic(input.hooksPath, JSON.stringify(existing, null, 2) + "\n");
76
- return true;
77
- }
78
-
79
- const ClaudeSettingsLoose = z.looseObject({
80
- hooks: z.record(z.string(), z.array(z.looseObject({
81
- matcher: z.string().optional(),
82
- hooks: z.array(z.looseObject({ type: z.string(), command: z.string() })).default([]),
83
- }))).optional(),
84
- });
85
-
86
- const RELAY_PERM_SUB = "__relay-permission-hook";
87
-
88
- /** Append PermissionRequest hook for relay; never remove foreign hooks. */
89
- export function mergeClaudeRelayPermissionHook(input: { settingsPath?: string } = {}): boolean {
90
- const settingsPath = input.settingsPath ?? paths.claudeSettings;
91
- const bin = join(paths.binDir, "tokenmaxxing");
92
- const command = `${JSON.stringify(bin)} ${RELAY_PERM_SUB}`;
93
- let settings: z.infer<typeof ClaudeSettingsLoose> = {};
94
- if (existsSync(settingsPath)) {
95
- settings = ClaudeSettingsLoose.parse(JSON.parse(readFileSync(settingsPath, "utf8")));
96
- }
97
- settings.hooks ??= {};
98
- settings.hooks.PermissionRequest ??= [];
99
- const arr = settings.hooks.PermissionRequest;
100
- const present = arr.some((g) => g.hooks.some((h) => h.command === command || isOurHookCommand(h.command, RELAY_PERM_SUB)));
101
- if (!present) {
102
- arr.push({ hooks: [{ type: "command", command }] });
103
- }
104
- mkdirSync(dirname(settingsPath), { recursive: true });
105
- const mode = existsSync(settingsPath) ? 0o600 : 0o600;
106
- writeFileAtomic(settingsPath, JSON.stringify(settings, null, 2) + "\n", mode);
107
- return true;
108
- }
109
-
110
- export function installRelayHosts(input: {
111
- target: InstallTarget;
112
- cursorAgentsDir?: string;
113
- claudeAgentsDir?: string;
114
- cursorHooksPath?: string;
115
- mergeHooks?: boolean;
116
- }): InstallResult {
117
- const home = process.env.HOME ?? "";
118
- const cursorAgents = input.cursorAgentsDir ?? join(home, ".cursor", "agents");
119
- const claudeAgents = input.claudeAgentsDir ?? join(home, ".claude", "agents");
120
- const agentsWritten: string[] = [];
121
- const hooksMerged: string[] = [];
122
- let skillWritten = false;
123
-
124
- if (input.target === "cursor" || input.target === "all") {
125
- agentsWritten.push(copyAgent({ name: "tokenmaxxing-claude.md", destDir: cursorAgents }));
126
- agentsWritten.push(copyAgent({ name: "tokenmaxxing-codex.md", destDir: cursorAgents }));
127
- skillWritten = copySkill({ destDir: join(home, ".cursor", "skills", "relay-session") }) || skillWritten;
128
- if (input.mergeHooks !== false) {
129
- const hooksPath = input.cursorHooksPath ?? join(home, ".cursor", "hooks.json");
130
- if (mergeCursorHooks({ hooksPath })) hooksMerged.push(hooksPath);
131
- }
132
- }
133
- if (input.target === "claude" || input.target === "all") {
134
- agentsWritten.push(copyAgent({ name: "tokenmaxxing-claude.md", destDir: claudeAgents }));
135
- agentsWritten.push(copyAgent({ name: "tokenmaxxing-codex.md", destDir: claudeAgents }));
136
- if (input.mergeHooks !== false) {
137
- if (mergeClaudeRelayPermissionHook()) hooksMerged.push(paths.claudeSettings);
138
- }
139
- }
140
- return { agentsWritten, skillWritten, hooksMerged };
141
- }
142
-
143
- export { RELAY_PERM_SUB };
@@ -1,148 +0,0 @@
1
- // Additive turn-done and permission-needed markers under relayDir.
2
- // Never writes into respawn/.
3
-
4
- import { existsSync, mkdirSync, readFileSync, rmSync, readdirSync } from "node:fs";
5
- import { z } from "zod";
6
- import { writeFileAtomic } from "../atomic.ts";
7
- import {
8
- decisionPath,
9
- pendingRequestPath,
10
- relayDecisionsDir,
11
- relayPendingDir,
12
- relayTurnDoneDir,
13
- turnDonePath,
14
- } from "./registry.ts";
15
-
16
- export const TurnDoneMarkerSchema = z.object({
17
- sessionId: z.uuid(),
18
- ts: z.number().int().nonnegative(),
19
- source: z.enum(["claude-stop", "codex-stop", "test"]).default("claude-stop"),
20
- });
21
- export type TurnDoneMarker = z.infer<typeof TurnDoneMarkerSchema>;
22
-
23
- export const PendingRequestSchema = z.object({
24
- requestId: z.string().min(1),
25
- sessionId: z.uuid(),
26
- summary: z.string(),
27
- detail: z.string(),
28
- ts: z.number().int().nonnegative(),
29
- });
30
- export type PendingRequest = z.infer<typeof PendingRequestSchema>;
31
-
32
- export const DecisionSchema = z.object({
33
- requestId: z.string().min(1),
34
- sessionId: z.uuid(),
35
- approve: z.boolean(),
36
- ts: z.number().int().nonnegative(),
37
- });
38
- export type Decision = z.infer<typeof DecisionSchema>;
39
-
40
- /** Write a turn-done marker for a relay session. Additive; never touches respawn/. */
41
- export function writeTurnDoneMarker(input: {
42
- sessionId: string;
43
- source?: TurnDoneMarker["source"];
44
- now?: number;
45
- }): void {
46
- mkdirSync(relayTurnDoneDir(), { recursive: true });
47
- const payload = TurnDoneMarkerSchema.parse({
48
- sessionId: input.sessionId,
49
- ts: input.now ?? Date.now(),
50
- source: input.source ?? "claude-stop",
51
- });
52
- writeFileAtomic(turnDonePath({ sessionId: input.sessionId }), JSON.stringify(payload) + "\n");
53
- }
54
-
55
- export function readTurnDoneMarker(input: { sessionId: string }): TurnDoneMarker | null {
56
- const path = turnDonePath(input);
57
- if (!existsSync(path)) return null;
58
- return TurnDoneMarkerSchema.parse(JSON.parse(readFileSync(path, "utf8")));
59
- }
60
-
61
- export function clearTurnDoneMarker(input: { sessionId: string }): void {
62
- rmSync(turnDonePath(input), { force: true });
63
- }
64
-
65
- export function writePendingRequest(input: {
66
- sessionId: string;
67
- requestId: string;
68
- summary: string;
69
- detail: string;
70
- now?: number;
71
- }): PendingRequest {
72
- const payload = PendingRequestSchema.parse({
73
- requestId: input.requestId,
74
- sessionId: input.sessionId,
75
- summary: input.summary,
76
- detail: input.detail,
77
- ts: input.now ?? Date.now(),
78
- });
79
- writeFileAtomic(
80
- pendingRequestPath({ sessionId: input.sessionId, requestId: input.requestId }),
81
- JSON.stringify(payload, null, 2) + "\n",
82
- );
83
- return payload;
84
- }
85
-
86
- export function readPendingRequest(input: { sessionId: string; requestId: string }): PendingRequest | null {
87
- const path = pendingRequestPath(input);
88
- if (!existsSync(path)) return null;
89
- return PendingRequestSchema.parse(JSON.parse(readFileSync(path, "utf8")));
90
- }
91
-
92
- export function listPendingRequests(input: { sessionId: string }): PendingRequest[] {
93
- const dir = `${relayPendingDir()}/${input.sessionId}`;
94
- if (!existsSync(dir)) return [];
95
- const out: PendingRequest[] = [];
96
- for (const name of readdirSync(dir)) {
97
- if (!name.endsWith(".json")) continue;
98
- try {
99
- out.push(PendingRequestSchema.parse(JSON.parse(readFileSync(`${dir}/${name}`, "utf8"))));
100
- } catch {
101
- // skip
102
- }
103
- }
104
- return out.sort((a, b) => a.ts - b.ts);
105
- }
106
-
107
- export function clearPendingRequest(input: { sessionId: string; requestId: string }): void {
108
- rmSync(pendingRequestPath(input), { force: true });
109
- }
110
-
111
- export function writeDecision(input: {
112
- sessionId: string;
113
- requestId: string;
114
- approve: boolean;
115
- now?: number;
116
- }): Decision {
117
- mkdirSync(`${relayDecisionsDir()}/${input.sessionId}`, { recursive: true });
118
- const payload = DecisionSchema.parse({
119
- requestId: input.requestId,
120
- sessionId: input.sessionId,
121
- approve: input.approve,
122
- ts: input.now ?? Date.now(),
123
- });
124
- writeFileAtomic(
125
- decisionPath({ sessionId: input.sessionId, requestId: input.requestId }),
126
- JSON.stringify(payload, null, 2) + "\n",
127
- );
128
- return payload;
129
- }
130
-
131
- export function readDecision(input: { sessionId: string; requestId: string }): Decision | null {
132
- const path = decisionPath(input);
133
- if (!existsSync(path)) return null;
134
- return DecisionSchema.parse(JSON.parse(readFileSync(path, "utf8")));
135
- }
136
-
137
- export function clearDecision(input: { sessionId: string; requestId: string }): void {
138
- rmSync(decisionPath(input), { force: true });
139
- }
140
-
141
- /** Clear all marker artifacts for a session (destroy/gc). Does not touch respawn/. */
142
- export function clearSessionMarkers(input: { sessionId: string }): void {
143
- clearTurnDoneMarker(input);
144
- const pendingDir = `${relayPendingDir()}/${input.sessionId}`;
145
- const decisionsDir = `${relayDecisionsDir()}/${input.sessionId}`;
146
- rmSync(pendingDir, { recursive: true, force: true });
147
- rmSync(decisionsDir, { recursive: true, force: true });
148
- }