tmux-agent 0.1.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/.codex/skills/speckit/SKILL.md +173 -0
- package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
- package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
- package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
- package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
- package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
- package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
- package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
- package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
- package/.codex/skills/speckit/references/acceptance.md +183 -0
- package/.codex/skills/speckit/references/analyze.md +186 -0
- package/.codex/skills/speckit/references/checklist.md +302 -0
- package/.codex/skills/speckit/references/clarify-auto.md +69 -0
- package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
- package/.codex/skills/speckit/references/clarify.md +189 -0
- package/.codex/skills/speckit/references/constitution.md +90 -0
- package/.codex/skills/speckit/references/group.md +89 -0
- package/.codex/skills/speckit/references/implement-task.md +115 -0
- package/.codex/skills/speckit/references/implement.md +129 -0
- package/.codex/skills/speckit/references/notes.md +82 -0
- package/.codex/skills/speckit/references/plan-deep.md +87 -0
- package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
- package/.codex/skills/speckit/references/plan-from-review.md +89 -0
- package/.codex/skills/speckit/references/plan.md +97 -0
- package/.codex/skills/speckit/references/review-plan.md +156 -0
- package/.codex/skills/speckit/references/specify.md +246 -0
- package/.codex/skills/speckit/references/tasks.md +155 -0
- package/.codex/skills/speckit/references/taskstoissues.md +33 -0
- package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
- package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
- package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
- package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
- package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
- package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
- package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
- package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
- package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
- package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
- package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
- package/.specify/memory/constitution.md +67 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +49 -0
- package/.specify/templates/plan-template.md +126 -0
- package/.specify/templates/spec-template.md +135 -0
- package/.specify/templates/tasks-template.md +269 -0
- package/README.md +128 -0
- package/README.zh-CN.md +127 -0
- package/bun.lock +269 -0
- package/dist/cli/commands/codex/forkHome.js +88 -0
- package/dist/cli/commands/codex/send.js +55 -0
- package/dist/cli/commands/codex/sessionInfo.js +42 -0
- package/dist/cli/commands/codex/spawn.js +68 -0
- package/dist/cli/commands/find.js +26 -0
- package/dist/cli/commands/paneKill.js +33 -0
- package/dist/cli/commands/paneSpawn.js +40 -0
- package/dist/cli/commands/paneTitle.js +33 -0
- package/dist/cli/commands/read.js +34 -0
- package/dist/cli/commands/send.js +51 -0
- package/dist/cli/commands/snapshot.js +19 -0
- package/dist/cli/commands/ui/select.js +41 -0
- package/dist/cli/commands/windowKill.js +25 -0
- package/dist/cli/commands/windowLs.js +15 -0
- package/dist/cli/commands/windowNew.js +28 -0
- package/dist/cli/commands/windowRename.js +25 -0
- package/dist/cli/index.js +365 -0
- package/dist/cli/parse.js +39 -0
- package/dist/lib/codex/forkHome.js +101 -0
- package/dist/lib/codex/isCodexPane.js +55 -0
- package/dist/lib/codex/send.js +58 -0
- package/dist/lib/codex/sessionInfo.js +449 -0
- package/dist/lib/codex/spawn.js +246 -0
- package/dist/lib/contracts/types.js +2 -0
- package/dist/lib/fs/safeRm.js +32 -0
- package/dist/lib/io/readStdin.js +14 -0
- package/dist/lib/os/process.js +55 -0
- package/dist/lib/output/format.js +95 -0
- package/dist/lib/proc/lsof.js +42 -0
- package/dist/lib/proc/ps.js +60 -0
- package/dist/lib/targeting/errors.js +13 -0
- package/dist/lib/targeting/resolvePaneTarget.js +91 -0
- package/dist/lib/targeting/resolveWindowTarget.js +40 -0
- package/dist/lib/targeting/scope.js +58 -0
- package/dist/lib/tmux/capturePane.js +20 -0
- package/dist/lib/tmux/exec.js +66 -0
- package/dist/lib/tmux/paneOps.js +29 -0
- package/dist/lib/tmux/paste.js +23 -0
- package/dist/lib/tmux/sendKeys.js +47 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/tmux/snapshotPanes.js +46 -0
- package/dist/lib/tmux/snapshotWindows.js +24 -0
- package/dist/lib/tmux/windowOps.js +32 -0
- package/dist/lib/ui/popupSelect.js +432 -0
- package/dist/lib/ui/popupSupport.js +76 -0
- package/package.json +23 -0
- package/src/cli/commands/codex/forkHome.ts +141 -0
- package/src/cli/commands/codex/send.ts +83 -0
- package/src/cli/commands/codex/sessionInfo.ts +59 -0
- package/src/cli/commands/codex/spawn.ts +90 -0
- package/src/cli/commands/find.ts +40 -0
- package/src/cli/commands/paneKill.ts +49 -0
- package/src/cli/commands/paneSpawn.ts +53 -0
- package/src/cli/commands/paneTitle.ts +50 -0
- package/src/cli/commands/read.ts +48 -0
- package/src/cli/commands/send.ts +71 -0
- package/src/cli/commands/snapshot.ts +28 -0
- package/src/cli/commands/ui/select.ts +49 -0
- package/src/cli/commands/windowKill.ts +35 -0
- package/src/cli/commands/windowLs.ts +20 -0
- package/src/cli/commands/windowNew.ts +40 -0
- package/src/cli/commands/windowRename.ts +36 -0
- package/src/cli/index.ts +430 -0
- package/src/lib/codex/forkHome.ts +148 -0
- package/src/lib/codex/isCodexPane.ts +56 -0
- package/src/lib/codex/send.ts +84 -0
- package/src/lib/codex/sessionInfo.ts +521 -0
- package/src/lib/codex/spawn.ts +305 -0
- package/src/lib/contracts/types.ts +30 -0
- package/src/lib/fs/safeRm.ts +32 -0
- package/src/lib/io/readStdin.ts +11 -0
- package/src/lib/output/format.ts +105 -0
- package/src/lib/proc/lsof.ts +44 -0
- package/src/lib/proc/ps.ts +70 -0
- package/src/lib/targeting/errors.ts +25 -0
- package/src/lib/targeting/resolvePaneTarget.ts +106 -0
- package/src/lib/targeting/resolveWindowTarget.ts +45 -0
- package/src/lib/targeting/scope.ts +76 -0
- package/src/lib/tmux/capturePane.ts +21 -0
- package/src/lib/tmux/exec.ts +90 -0
- package/src/lib/tmux/paneOps.ts +35 -0
- package/src/lib/tmux/paste.ts +20 -0
- package/src/lib/tmux/sendKeys.ts +72 -0
- package/src/lib/tmux/session.ts +27 -0
- package/src/lib/tmux/snapshotPanes.ts +52 -0
- package/src/lib/tmux/snapshotWindows.ts +23 -0
- package/src/lib/tmux/windowOps.ts +43 -0
- package/src/lib/ui/popupSelect.ts +561 -0
- package/src/lib/ui/popupSupport.ts +84 -0
- package/tests/e2e/codexForkHome.test.ts +146 -0
- package/tests/e2e/codexSessionInfo.test.ts +112 -0
- package/tests/e2e/codexTuiSend.test.ts +68 -0
- package/tests/integration/codexSpawn.test.ts +113 -0
- package/tests/integration/paneOps.test.ts +60 -0
- package/tests/integration/sendRead.test.ts +52 -0
- package/tests/integration/snapshot.test.ts +39 -0
- package/tests/integration/tmuxHarness.ts +39 -0
- package/tests/integration/windowOps.test.ts +60 -0
- package/tests/unit/codexSend.test.ts +105 -0
- package/tests/unit/codexSessionInfo.test.ts +88 -0
- package/tests/unit/codexSpawn.test.ts +34 -0
- package/tests/unit/keys.test.ts +30 -0
- package/tests/unit/outputFormat.test.ts +52 -0
- package/tests/unit/popupSelect.test.ts +77 -0
- package/tests/unit/popupSupport.test.ts +109 -0
- package/tests/unit/resolvePaneTarget.test.ts +43 -0
- package/tests/unit/resolveWindowTarget.test.ts +36 -0
- package/tests/unit/safeRm.test.ts +41 -0
- package/tests/unit/scope.test.ts +57 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +16 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { ensureErrorPrefix, formatPaneTable, formatWindowTable } from "../lib/output/format";
|
|
4
|
+
import type { PaneInfo, WindowInfo } from "../lib/contracts/types";
|
|
5
|
+
import { TargetResolutionError } from "../lib/targeting/errors";
|
|
6
|
+
import { windowLs } from "./commands/windowLs";
|
|
7
|
+
import { snapshot } from "./commands/snapshot";
|
|
8
|
+
import { find } from "./commands/find";
|
|
9
|
+
import { send } from "./commands/send";
|
|
10
|
+
import { read } from "./commands/read";
|
|
11
|
+
import { windowNewCommand } from "./commands/windowNew";
|
|
12
|
+
import { windowRenameCommand } from "./commands/windowRename";
|
|
13
|
+
import { windowKillCommand } from "./commands/windowKill";
|
|
14
|
+
import { paneSpawnCommand } from "./commands/paneSpawn";
|
|
15
|
+
import { paneTitleCommand } from "./commands/paneTitle";
|
|
16
|
+
import { paneKillCommand } from "./commands/paneKill";
|
|
17
|
+
import { codexSendCommand } from "./commands/codex/send";
|
|
18
|
+
import { codexSessionInfoCommand } from "./commands/codex/sessionInfo";
|
|
19
|
+
import { codexForkHomeCleanupCommand, codexForkHomePrepareCommand } from "./commands/codex/forkHome";
|
|
20
|
+
import { codexSpawnCommand } from "./commands/codex/spawn";
|
|
21
|
+
import { uiSelectCommand } from "./commands/ui/select";
|
|
22
|
+
|
|
23
|
+
function writeLine(stream: NodeJS.WritableStream, text: string): void {
|
|
24
|
+
if (!text.endsWith("\n")) {
|
|
25
|
+
stream.write(`${text}\n`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
stream.write(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatError(error: unknown): string {
|
|
32
|
+
if (error instanceof TargetResolutionError) {
|
|
33
|
+
const base = ensureErrorPrefix(error.message);
|
|
34
|
+
if (error.kind === "ambiguous" && error.candidates?.length) {
|
|
35
|
+
if (error.targetKind === "pane") {
|
|
36
|
+
return `${base}\n${formatPaneTable(error.candidates as PaneInfo[])}`;
|
|
37
|
+
}
|
|
38
|
+
return `${base}\n${formatWindowTable(error.candidates as WindowInfo[])}`;
|
|
39
|
+
}
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
return ensureErrorPrefix(error.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return ensureErrorPrefix(String(error));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseOptionalBoolean(value: string | boolean | undefined): boolean {
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "boolean") {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
const normalized = value.trim().toLowerCase();
|
|
58
|
+
return normalized !== "false";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseNonNegativeNumber(optionName: string): (value: string) => number {
|
|
62
|
+
return (value: string) => {
|
|
63
|
+
const num = Number(value);
|
|
64
|
+
if (!Number.isFinite(num) || num < 0) {
|
|
65
|
+
throw new Error(`invalid --${optionName}`);
|
|
66
|
+
}
|
|
67
|
+
return num;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildProgram(): Command {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
program
|
|
74
|
+
.name("agent-tmux")
|
|
75
|
+
.description("LLM-friendly tmux control plane CLI for windows/panes.")
|
|
76
|
+
.exitOverride()
|
|
77
|
+
.configureOutput({
|
|
78
|
+
writeErr: () => undefined
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const window = program.command("window");
|
|
82
|
+
window.command("ls")
|
|
83
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
84
|
+
.option("--session <session>", "tmux session name")
|
|
85
|
+
.action(async (options: { json: boolean; session?: string }) => {
|
|
86
|
+
const output = await windowLs({ json: options.json, session: options.session });
|
|
87
|
+
if (output) {
|
|
88
|
+
writeLine(process.stdout, output);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
window.command("new")
|
|
93
|
+
.argument("<name>")
|
|
94
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
95
|
+
.option("--session <session>", "tmux session name")
|
|
96
|
+
.option("--command <command>", "initial command to run (or '-' to read from stdin)")
|
|
97
|
+
.action(async (name: string, options: { json: boolean; session?: string; command?: string }) => {
|
|
98
|
+
const output = await windowNewCommand(name, {
|
|
99
|
+
json: options.json,
|
|
100
|
+
session: options.session,
|
|
101
|
+
command: options.command
|
|
102
|
+
});
|
|
103
|
+
if (output) {
|
|
104
|
+
writeLine(process.stdout, output);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
window.command("rename")
|
|
109
|
+
.argument("<windowTarget>")
|
|
110
|
+
.argument("<name>")
|
|
111
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
112
|
+
.option("--session <session>", "tmux session name")
|
|
113
|
+
.action(async (target: string, name: string, options: { json: boolean; session?: string }) => {
|
|
114
|
+
const output = await windowRenameCommand(target, name, {
|
|
115
|
+
json: options.json,
|
|
116
|
+
session: options.session
|
|
117
|
+
});
|
|
118
|
+
if (output) {
|
|
119
|
+
writeLine(process.stdout, output);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
window.command("kill")
|
|
124
|
+
.argument("<windowTarget>")
|
|
125
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
126
|
+
.option("--session <session>", "tmux session name")
|
|
127
|
+
.action(async (target: string, options: { json: boolean; session?: string }) => {
|
|
128
|
+
const output = await windowKillCommand(target, {
|
|
129
|
+
json: options.json,
|
|
130
|
+
session: options.session
|
|
131
|
+
});
|
|
132
|
+
if (output) {
|
|
133
|
+
writeLine(process.stdout, output);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const pane = program.command("pane");
|
|
138
|
+
pane.command("spawn")
|
|
139
|
+
.argument("<command>")
|
|
140
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
141
|
+
.option("--session <session>", "tmux session name")
|
|
142
|
+
.option("--window <window>", "tmux window target")
|
|
143
|
+
.option("--title <title>", "pane title")
|
|
144
|
+
.action(async (command: string, options: { json: boolean; session?: string; window?: string; title?: string }) => {
|
|
145
|
+
const output = await paneSpawnCommand(command, {
|
|
146
|
+
json: options.json,
|
|
147
|
+
session: options.session,
|
|
148
|
+
window: options.window,
|
|
149
|
+
title: options.title
|
|
150
|
+
});
|
|
151
|
+
if (output) {
|
|
152
|
+
writeLine(process.stdout, output);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
pane.command("title")
|
|
157
|
+
.argument("<paneTarget>")
|
|
158
|
+
.argument("<title>")
|
|
159
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
160
|
+
.option("--session <session>", "tmux session name")
|
|
161
|
+
.option("--window <window>", "tmux window target")
|
|
162
|
+
.action(async (target: string, title: string, options: { json: boolean; session?: string; window?: string }) => {
|
|
163
|
+
const output = await paneTitleCommand(target, title, {
|
|
164
|
+
json: options.json,
|
|
165
|
+
session: options.session,
|
|
166
|
+
window: options.window
|
|
167
|
+
});
|
|
168
|
+
if (output) {
|
|
169
|
+
writeLine(process.stdout, output);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pane.command("kill")
|
|
174
|
+
.argument("<paneTarget>")
|
|
175
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
176
|
+
.option("--session <session>", "tmux session name")
|
|
177
|
+
.option("--window <window>", "tmux window target")
|
|
178
|
+
.action(async (target: string, options: { json: boolean; session?: string; window?: string }) => {
|
|
179
|
+
const output = await paneKillCommand(target, {
|
|
180
|
+
json: options.json,
|
|
181
|
+
session: options.session,
|
|
182
|
+
window: options.window
|
|
183
|
+
});
|
|
184
|
+
if (output) {
|
|
185
|
+
writeLine(process.stdout, output);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
program.command("snapshot")
|
|
190
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
191
|
+
.option("--session <session>", "tmux session name")
|
|
192
|
+
.option("--window <window>", "tmux window target")
|
|
193
|
+
.action(async (options: { json: boolean; session?: string; window?: string }) => {
|
|
194
|
+
const output = await snapshot({
|
|
195
|
+
json: options.json,
|
|
196
|
+
session: options.session,
|
|
197
|
+
window: options.window
|
|
198
|
+
});
|
|
199
|
+
if (output) {
|
|
200
|
+
writeLine(process.stdout, output);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
program.command("find")
|
|
205
|
+
.argument("<query>")
|
|
206
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
207
|
+
.option("--session <session>", "tmux session name")
|
|
208
|
+
.option("--window <window>", "tmux window target")
|
|
209
|
+
.action(async (query: string, options: { json: boolean; session?: string; window?: string }) => {
|
|
210
|
+
const output = await find(query, {
|
|
211
|
+
json: options.json,
|
|
212
|
+
session: options.session,
|
|
213
|
+
window: options.window
|
|
214
|
+
});
|
|
215
|
+
if (output) {
|
|
216
|
+
writeLine(process.stdout, output);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
program.command("send")
|
|
221
|
+
.argument("<paneTarget>")
|
|
222
|
+
.argument("<text>")
|
|
223
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
224
|
+
.option("--session <session>", "tmux session name")
|
|
225
|
+
.option("--window <window>", "tmux window target")
|
|
226
|
+
.option("--no-enter [boolean]", "do not submit Enter", parseOptionalBoolean, false)
|
|
227
|
+
.option("--enter-delay-ms <ms>", "delay before submitting Enter", parseNonNegativeNumber("enter-delay-ms"))
|
|
228
|
+
.action(async (
|
|
229
|
+
target: string,
|
|
230
|
+
text: string,
|
|
231
|
+
options: {
|
|
232
|
+
json: boolean;
|
|
233
|
+
session?: string;
|
|
234
|
+
window?: string;
|
|
235
|
+
noEnter: boolean;
|
|
236
|
+
enterDelayMs?: number;
|
|
237
|
+
}
|
|
238
|
+
) => {
|
|
239
|
+
const output = await send(target, text, {
|
|
240
|
+
json: options.json,
|
|
241
|
+
session: options.session,
|
|
242
|
+
window: options.window,
|
|
243
|
+
noEnter: options.noEnter,
|
|
244
|
+
enterDelayMs: options.enterDelayMs
|
|
245
|
+
});
|
|
246
|
+
if (output) {
|
|
247
|
+
writeLine(process.stdout, output);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
program.command("read")
|
|
252
|
+
.argument("<paneTarget>")
|
|
253
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
254
|
+
.option("--session <session>", "tmux session name")
|
|
255
|
+
.option("--window <window>", "tmux window target")
|
|
256
|
+
.option("--lines <n>", "lines to capture", parseNonNegativeNumber("lines"))
|
|
257
|
+
.action(async (
|
|
258
|
+
target: string,
|
|
259
|
+
options: { json: boolean; session?: string; window?: string; lines?: number }
|
|
260
|
+
) => {
|
|
261
|
+
const output = await read(target, {
|
|
262
|
+
json: options.json,
|
|
263
|
+
session: options.session,
|
|
264
|
+
window: options.window,
|
|
265
|
+
lines: options.lines
|
|
266
|
+
});
|
|
267
|
+
if (output) {
|
|
268
|
+
writeLine(process.stdout, output);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const codex = program.command("codex");
|
|
273
|
+
codex.command("send")
|
|
274
|
+
.argument("<paneTarget>")
|
|
275
|
+
.argument("<text>")
|
|
276
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
277
|
+
.option("--session <session>", "tmux session name")
|
|
278
|
+
.option("--window <window>", "tmux window target")
|
|
279
|
+
.option("--submit <key>", "submit key (default Enter; use 'none' to disable)")
|
|
280
|
+
.option("--submit-delay-ms <ms>", "delay before submitting", parseNonNegativeNumber("submit-delay-ms"))
|
|
281
|
+
.option("--post-delay-ms <ms>", "delay after submit", parseNonNegativeNumber("post-delay-ms"))
|
|
282
|
+
.option("--capture-tail <n>", "capture tail lines (prints captured tail when not --json)", parseNonNegativeNumber("capture-tail"))
|
|
283
|
+
.action(async (
|
|
284
|
+
target: string,
|
|
285
|
+
text: string,
|
|
286
|
+
options: {
|
|
287
|
+
json: boolean;
|
|
288
|
+
session?: string;
|
|
289
|
+
window?: string;
|
|
290
|
+
submit?: string;
|
|
291
|
+
submitDelayMs?: number;
|
|
292
|
+
postDelayMs?: number;
|
|
293
|
+
captureTail?: number;
|
|
294
|
+
}
|
|
295
|
+
) => {
|
|
296
|
+
const output = await codexSendCommand(target, text, {
|
|
297
|
+
json: options.json,
|
|
298
|
+
session: options.session,
|
|
299
|
+
window: options.window,
|
|
300
|
+
submit: options.submit,
|
|
301
|
+
submitDelayMs: options.submitDelayMs,
|
|
302
|
+
postDelayMs: options.postDelayMs,
|
|
303
|
+
captureTail: options.captureTail
|
|
304
|
+
});
|
|
305
|
+
if (output) {
|
|
306
|
+
writeLine(process.stdout, output);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
codex.command("session-info")
|
|
311
|
+
.argument("<paneTarget>")
|
|
312
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
313
|
+
.option("--session <session>", "tmux session name")
|
|
314
|
+
.option("--window <window>", "tmux window target")
|
|
315
|
+
.action(async (target: string, options: { json: boolean; session?: string; window?: string }) => {
|
|
316
|
+
const output = await codexSessionInfoCommand(target, {
|
|
317
|
+
json: options.json,
|
|
318
|
+
session: options.session,
|
|
319
|
+
window: options.window
|
|
320
|
+
});
|
|
321
|
+
if (output) {
|
|
322
|
+
writeLine(process.stdout, output);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const forkHome = codex.command("fork-home");
|
|
327
|
+
forkHome.command("prepare")
|
|
328
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
329
|
+
.option("--parent-rollout <path>", "parent rollout path")
|
|
330
|
+
.option("--fork-home <path>", "fork CODEX_HOME path")
|
|
331
|
+
.option("--parent-codex-home <path>", "parent CODEX_HOME override")
|
|
332
|
+
.option("--run-id <id>", "run id for naming")
|
|
333
|
+
.option("--copy-config [boolean]", "copy config.toml", parseOptionalBoolean)
|
|
334
|
+
.option("--spec <spec>", "read json spec from stdin (use '-')")
|
|
335
|
+
.action(async (options: {
|
|
336
|
+
json: boolean;
|
|
337
|
+
parentRollout?: string;
|
|
338
|
+
forkHome?: string;
|
|
339
|
+
parentCodexHome?: string;
|
|
340
|
+
runId?: string;
|
|
341
|
+
copyConfig?: boolean;
|
|
342
|
+
spec?: string;
|
|
343
|
+
}) => {
|
|
344
|
+
const output = await codexForkHomePrepareCommand({
|
|
345
|
+
json: options.json,
|
|
346
|
+
parentRollout: options.parentRollout,
|
|
347
|
+
forkHome: options.forkHome,
|
|
348
|
+
parentCodexHome: options.parentCodexHome,
|
|
349
|
+
runId: options.runId,
|
|
350
|
+
copyConfig: options.copyConfig,
|
|
351
|
+
spec: options.spec
|
|
352
|
+
});
|
|
353
|
+
if (output) {
|
|
354
|
+
writeLine(process.stdout, output);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
forkHome.command("cleanup")
|
|
359
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
360
|
+
.option("--path <path>", "path to remove")
|
|
361
|
+
.option("--allowed-root <root>", "allowed root guard")
|
|
362
|
+
.option("--spec <spec>", "read json spec from stdin (use '-')")
|
|
363
|
+
.action(async (options: { json: boolean; path?: string; allowedRoot?: string; spec?: string }) => {
|
|
364
|
+
const output = await codexForkHomeCleanupCommand({
|
|
365
|
+
json: options.json,
|
|
366
|
+
path: options.path,
|
|
367
|
+
allowedRoot: options.allowedRoot,
|
|
368
|
+
spec: options.spec
|
|
369
|
+
});
|
|
370
|
+
if (output) {
|
|
371
|
+
writeLine(process.stdout, output);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
codex.command("spawn")
|
|
376
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
377
|
+
.option("--origin <paneTarget>", "origin pane target")
|
|
378
|
+
.option("--session <session>", "tmux session name")
|
|
379
|
+
.option("--window <window>", "tmux window target")
|
|
380
|
+
.option("--force-simple-split [boolean]", "force fallback split", parseOptionalBoolean, false)
|
|
381
|
+
.action(async (options: {
|
|
382
|
+
json: boolean;
|
|
383
|
+
origin?: string;
|
|
384
|
+
session?: string;
|
|
385
|
+
window?: string;
|
|
386
|
+
forceSimpleSplit: boolean;
|
|
387
|
+
}) => {
|
|
388
|
+
const output = await codexSpawnCommand({
|
|
389
|
+
json: options.json,
|
|
390
|
+
origin: options.origin,
|
|
391
|
+
session: options.session,
|
|
392
|
+
window: options.window,
|
|
393
|
+
forceSimpleSplit: options.forceSimpleSplit
|
|
394
|
+
});
|
|
395
|
+
if (output) {
|
|
396
|
+
writeLine(process.stdout, output);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const ui = program.command("ui");
|
|
401
|
+
ui.command("select")
|
|
402
|
+
.requiredOption("--spec <spec>", "read json spec from stdin (use '-')")
|
|
403
|
+
.option("--json [boolean]", "output json", parseOptionalBoolean, false)
|
|
404
|
+
.action(async (options: { json: boolean; spec: string }) => {
|
|
405
|
+
const output = await uiSelectCommand({ json: options.json, spec: options.spec });
|
|
406
|
+
if (output) {
|
|
407
|
+
writeLine(process.stdout, output);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return program;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function run(): Promise<void> {
|
|
415
|
+
const argv = process.argv.slice(2);
|
|
416
|
+
if (argv.length === 0) {
|
|
417
|
+
throw new Error("missing command");
|
|
418
|
+
}
|
|
419
|
+
const program = buildProgram();
|
|
420
|
+
await program.parseAsync(process.argv);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
run().catch((error) => {
|
|
424
|
+
if (error instanceof CommanderError && error.exitCode === 0) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const message = formatError(error);
|
|
428
|
+
writeLine(process.stderr, message);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { safeRemove } from "../fs/safeRm";
|
|
4
|
+
|
|
5
|
+
export type ForkHomePrepareOptions = {
|
|
6
|
+
parentCodexHome?: string;
|
|
7
|
+
parentRolloutPath: string;
|
|
8
|
+
forkHome: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
copyConfig?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ForkHomePrepareResult = {
|
|
14
|
+
action: "prepare";
|
|
15
|
+
parent_rollout_path: string;
|
|
16
|
+
fork_home: string;
|
|
17
|
+
fork_rollout_path: string;
|
|
18
|
+
copied_config: boolean;
|
|
19
|
+
auth_strategy: "symlink" | "copy" | "missing" | "failed";
|
|
20
|
+
credentials_strategy: "symlink" | "copy" | "missing" | "failed";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ForkHomeCleanupResult = {
|
|
24
|
+
action: "cleanup";
|
|
25
|
+
removed: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolvePath(value: string): string {
|
|
29
|
+
return path.resolve(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function defaultCodexHome(): string {
|
|
33
|
+
const env = process.env.CODEX_HOME?.trim();
|
|
34
|
+
if (env) {
|
|
35
|
+
return resolvePath(env);
|
|
36
|
+
}
|
|
37
|
+
return resolvePath(path.join(process.env.HOME || "", ".codex"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function copyOrSymlink(
|
|
41
|
+
src: string,
|
|
42
|
+
dst: string
|
|
43
|
+
): Promise<"symlink" | "copy" | "missing" | "failed"> {
|
|
44
|
+
try {
|
|
45
|
+
await fs.stat(src);
|
|
46
|
+
} catch {
|
|
47
|
+
return "missing";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(dst);
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
await fs.symlink(src, dst);
|
|
58
|
+
return "symlink";
|
|
59
|
+
} catch {
|
|
60
|
+
try {
|
|
61
|
+
await fs.copyFile(src, dst);
|
|
62
|
+
return "copy";
|
|
63
|
+
} catch {
|
|
64
|
+
return "failed";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveForkRolloutPath(
|
|
70
|
+
parentCodexHome: string,
|
|
71
|
+
parentRolloutPath: string,
|
|
72
|
+
forkHome: string,
|
|
73
|
+
runId: string
|
|
74
|
+
): string {
|
|
75
|
+
try {
|
|
76
|
+
const relative = path.relative(parentCodexHome, parentRolloutPath);
|
|
77
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
78
|
+
return path.join(forkHome, relative);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
return path.join(forkHome, "sessions", `rollout-${runId}.jsonl`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function prepareForkHome(
|
|
87
|
+
options: ForkHomePrepareOptions
|
|
88
|
+
): Promise<ForkHomePrepareResult> {
|
|
89
|
+
const parentCodexHome = resolvePath(options.parentCodexHome || defaultCodexHome());
|
|
90
|
+
const parentRolloutPath = resolvePath(options.parentRolloutPath);
|
|
91
|
+
const forkHome = resolvePath(options.forkHome);
|
|
92
|
+
const runId = options.runId?.trim() || String(process.pid);
|
|
93
|
+
const copyConfig = options.copyConfig ?? true;
|
|
94
|
+
|
|
95
|
+
const stat = await fs.stat(parentRolloutPath);
|
|
96
|
+
if (!stat.isFile()) {
|
|
97
|
+
throw new Error(`parent_rollout_path is not a file: ${parentRolloutPath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await fs.mkdir(forkHome, { recursive: true });
|
|
101
|
+
|
|
102
|
+
let copiedConfig = false;
|
|
103
|
+
if (copyConfig) {
|
|
104
|
+
const configPath = path.join(parentCodexHome, "config.toml");
|
|
105
|
+
try {
|
|
106
|
+
await fs.copyFile(configPath, path.join(forkHome, "config.toml"));
|
|
107
|
+
copiedConfig = true;
|
|
108
|
+
} catch {
|
|
109
|
+
copiedConfig = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const authStrategy = await copyOrSymlink(
|
|
114
|
+
path.join(parentCodexHome, "auth.json"),
|
|
115
|
+
path.join(forkHome, "auth.json")
|
|
116
|
+
);
|
|
117
|
+
const credentialsStrategy = await copyOrSymlink(
|
|
118
|
+
path.join(parentCodexHome, ".credentials.json"),
|
|
119
|
+
path.join(forkHome, ".credentials.json")
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const forkRolloutPath = resolveForkRolloutPath(
|
|
123
|
+
parentCodexHome,
|
|
124
|
+
parentRolloutPath,
|
|
125
|
+
forkHome,
|
|
126
|
+
runId
|
|
127
|
+
);
|
|
128
|
+
await fs.mkdir(path.dirname(forkRolloutPath), { recursive: true });
|
|
129
|
+
await fs.copyFile(parentRolloutPath, forkRolloutPath);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
action: "prepare",
|
|
133
|
+
parent_rollout_path: parentRolloutPath,
|
|
134
|
+
fork_home: forkHome,
|
|
135
|
+
fork_rollout_path: forkRolloutPath,
|
|
136
|
+
copied_config: copiedConfig,
|
|
137
|
+
auth_strategy: authStrategy,
|
|
138
|
+
credentials_strategy: credentialsStrategy
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function cleanupForkHome(
|
|
143
|
+
pathToRemove: string,
|
|
144
|
+
allowedRoot: string
|
|
145
|
+
): Promise<ForkHomeCleanupResult> {
|
|
146
|
+
const removed = await safeRemove(pathToRemove, allowedRoot);
|
|
147
|
+
return { action: "cleanup", removed };
|
|
148
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PaneInfo } from "../contracts/types";
|
|
2
|
+
import { listProcesses, type ProcessEntry } from "../proc/ps";
|
|
3
|
+
|
|
4
|
+
const CODEX_PATTERN = /codex/i;
|
|
5
|
+
|
|
6
|
+
function matchesCodex(value: string): boolean {
|
|
7
|
+
return CODEX_PATTERN.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasCodexDescendant(processes: ProcessEntry[], rootPid: number): boolean {
|
|
11
|
+
const byParent = new Map<number, ProcessEntry[]>();
|
|
12
|
+
for (const process of processes) {
|
|
13
|
+
const list = byParent.get(process.ppid);
|
|
14
|
+
if (list) {
|
|
15
|
+
list.push(process);
|
|
16
|
+
} else {
|
|
17
|
+
byParent.set(process.ppid, [process]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const queue: number[] = [rootPid];
|
|
22
|
+
const visited = new Set<number>();
|
|
23
|
+
while (queue.length) {
|
|
24
|
+
const current = queue.shift();
|
|
25
|
+
if (current === undefined || visited.has(current)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
visited.add(current);
|
|
29
|
+
const children = byParent.get(current);
|
|
30
|
+
if (!children) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const child of children) {
|
|
34
|
+
if (matchesCodex(child.command)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
queue.push(child.pid);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function isCodexPane(pane: PaneInfo): Promise<boolean> {
|
|
44
|
+
if (matchesCodex(pane.command) || matchesCodex(pane.title)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (!pane.pid || !Number.isFinite(pane.pid)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const processes = await listProcesses();
|
|
52
|
+
return hasCodexDescendant(processes, pane.pid);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|