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.
Files changed (161) hide show
  1. package/.codex/skills/speckit/SKILL.md +173 -0
  2. package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
  3. package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
  4. package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
  5. package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
  6. package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
  7. package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
  8. package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
  9. package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
  10. package/.codex/skills/speckit/references/acceptance.md +183 -0
  11. package/.codex/skills/speckit/references/analyze.md +186 -0
  12. package/.codex/skills/speckit/references/checklist.md +302 -0
  13. package/.codex/skills/speckit/references/clarify-auto.md +69 -0
  14. package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
  15. package/.codex/skills/speckit/references/clarify.md +189 -0
  16. package/.codex/skills/speckit/references/constitution.md +90 -0
  17. package/.codex/skills/speckit/references/group.md +89 -0
  18. package/.codex/skills/speckit/references/implement-task.md +115 -0
  19. package/.codex/skills/speckit/references/implement.md +129 -0
  20. package/.codex/skills/speckit/references/notes.md +82 -0
  21. package/.codex/skills/speckit/references/plan-deep.md +87 -0
  22. package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
  23. package/.codex/skills/speckit/references/plan-from-review.md +89 -0
  24. package/.codex/skills/speckit/references/plan.md +97 -0
  25. package/.codex/skills/speckit/references/review-plan.md +156 -0
  26. package/.codex/skills/speckit/references/specify.md +246 -0
  27. package/.codex/skills/speckit/references/tasks.md +155 -0
  28. package/.codex/skills/speckit/references/taskstoissues.md +33 -0
  29. package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
  30. package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
  31. package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
  32. package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
  33. package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
  34. package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
  35. package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
  36. package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
  37. package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
  38. package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
  39. package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
  40. package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
  41. package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
  42. package/.specify/memory/constitution.md +67 -0
  43. package/.specify/templates/agent-file-template.md +28 -0
  44. package/.specify/templates/checklist-template.md +49 -0
  45. package/.specify/templates/plan-template.md +126 -0
  46. package/.specify/templates/spec-template.md +135 -0
  47. package/.specify/templates/tasks-template.md +269 -0
  48. package/README.md +128 -0
  49. package/README.zh-CN.md +127 -0
  50. package/bun.lock +269 -0
  51. package/dist/cli/commands/codex/forkHome.js +88 -0
  52. package/dist/cli/commands/codex/send.js +55 -0
  53. package/dist/cli/commands/codex/sessionInfo.js +42 -0
  54. package/dist/cli/commands/codex/spawn.js +68 -0
  55. package/dist/cli/commands/find.js +26 -0
  56. package/dist/cli/commands/paneKill.js +33 -0
  57. package/dist/cli/commands/paneSpawn.js +40 -0
  58. package/dist/cli/commands/paneTitle.js +33 -0
  59. package/dist/cli/commands/read.js +34 -0
  60. package/dist/cli/commands/send.js +51 -0
  61. package/dist/cli/commands/snapshot.js +19 -0
  62. package/dist/cli/commands/ui/select.js +41 -0
  63. package/dist/cli/commands/windowKill.js +25 -0
  64. package/dist/cli/commands/windowLs.js +15 -0
  65. package/dist/cli/commands/windowNew.js +28 -0
  66. package/dist/cli/commands/windowRename.js +25 -0
  67. package/dist/cli/index.js +365 -0
  68. package/dist/cli/parse.js +39 -0
  69. package/dist/lib/codex/forkHome.js +101 -0
  70. package/dist/lib/codex/isCodexPane.js +55 -0
  71. package/dist/lib/codex/send.js +58 -0
  72. package/dist/lib/codex/sessionInfo.js +449 -0
  73. package/dist/lib/codex/spawn.js +246 -0
  74. package/dist/lib/contracts/types.js +2 -0
  75. package/dist/lib/fs/safeRm.js +32 -0
  76. package/dist/lib/io/readStdin.js +14 -0
  77. package/dist/lib/os/process.js +55 -0
  78. package/dist/lib/output/format.js +95 -0
  79. package/dist/lib/proc/lsof.js +42 -0
  80. package/dist/lib/proc/ps.js +60 -0
  81. package/dist/lib/targeting/errors.js +13 -0
  82. package/dist/lib/targeting/resolvePaneTarget.js +91 -0
  83. package/dist/lib/targeting/resolveWindowTarget.js +40 -0
  84. package/dist/lib/targeting/scope.js +58 -0
  85. package/dist/lib/tmux/capturePane.js +20 -0
  86. package/dist/lib/tmux/exec.js +66 -0
  87. package/dist/lib/tmux/paneOps.js +29 -0
  88. package/dist/lib/tmux/paste.js +23 -0
  89. package/dist/lib/tmux/sendKeys.js +47 -0
  90. package/dist/lib/tmux/session.js +29 -0
  91. package/dist/lib/tmux/snapshotPanes.js +46 -0
  92. package/dist/lib/tmux/snapshotWindows.js +24 -0
  93. package/dist/lib/tmux/windowOps.js +32 -0
  94. package/dist/lib/ui/popupSelect.js +432 -0
  95. package/dist/lib/ui/popupSupport.js +76 -0
  96. package/package.json +23 -0
  97. package/src/cli/commands/codex/forkHome.ts +141 -0
  98. package/src/cli/commands/codex/send.ts +83 -0
  99. package/src/cli/commands/codex/sessionInfo.ts +59 -0
  100. package/src/cli/commands/codex/spawn.ts +90 -0
  101. package/src/cli/commands/find.ts +40 -0
  102. package/src/cli/commands/paneKill.ts +49 -0
  103. package/src/cli/commands/paneSpawn.ts +53 -0
  104. package/src/cli/commands/paneTitle.ts +50 -0
  105. package/src/cli/commands/read.ts +48 -0
  106. package/src/cli/commands/send.ts +71 -0
  107. package/src/cli/commands/snapshot.ts +28 -0
  108. package/src/cli/commands/ui/select.ts +49 -0
  109. package/src/cli/commands/windowKill.ts +35 -0
  110. package/src/cli/commands/windowLs.ts +20 -0
  111. package/src/cli/commands/windowNew.ts +40 -0
  112. package/src/cli/commands/windowRename.ts +36 -0
  113. package/src/cli/index.ts +430 -0
  114. package/src/lib/codex/forkHome.ts +148 -0
  115. package/src/lib/codex/isCodexPane.ts +56 -0
  116. package/src/lib/codex/send.ts +84 -0
  117. package/src/lib/codex/sessionInfo.ts +521 -0
  118. package/src/lib/codex/spawn.ts +305 -0
  119. package/src/lib/contracts/types.ts +30 -0
  120. package/src/lib/fs/safeRm.ts +32 -0
  121. package/src/lib/io/readStdin.ts +11 -0
  122. package/src/lib/output/format.ts +105 -0
  123. package/src/lib/proc/lsof.ts +44 -0
  124. package/src/lib/proc/ps.ts +70 -0
  125. package/src/lib/targeting/errors.ts +25 -0
  126. package/src/lib/targeting/resolvePaneTarget.ts +106 -0
  127. package/src/lib/targeting/resolveWindowTarget.ts +45 -0
  128. package/src/lib/targeting/scope.ts +76 -0
  129. package/src/lib/tmux/capturePane.ts +21 -0
  130. package/src/lib/tmux/exec.ts +90 -0
  131. package/src/lib/tmux/paneOps.ts +35 -0
  132. package/src/lib/tmux/paste.ts +20 -0
  133. package/src/lib/tmux/sendKeys.ts +72 -0
  134. package/src/lib/tmux/session.ts +27 -0
  135. package/src/lib/tmux/snapshotPanes.ts +52 -0
  136. package/src/lib/tmux/snapshotWindows.ts +23 -0
  137. package/src/lib/tmux/windowOps.ts +43 -0
  138. package/src/lib/ui/popupSelect.ts +561 -0
  139. package/src/lib/ui/popupSupport.ts +84 -0
  140. package/tests/e2e/codexForkHome.test.ts +146 -0
  141. package/tests/e2e/codexSessionInfo.test.ts +112 -0
  142. package/tests/e2e/codexTuiSend.test.ts +68 -0
  143. package/tests/integration/codexSpawn.test.ts +113 -0
  144. package/tests/integration/paneOps.test.ts +60 -0
  145. package/tests/integration/sendRead.test.ts +52 -0
  146. package/tests/integration/snapshot.test.ts +39 -0
  147. package/tests/integration/tmuxHarness.ts +39 -0
  148. package/tests/integration/windowOps.test.ts +60 -0
  149. package/tests/unit/codexSend.test.ts +105 -0
  150. package/tests/unit/codexSessionInfo.test.ts +88 -0
  151. package/tests/unit/codexSpawn.test.ts +34 -0
  152. package/tests/unit/keys.test.ts +30 -0
  153. package/tests/unit/outputFormat.test.ts +52 -0
  154. package/tests/unit/popupSelect.test.ts +77 -0
  155. package/tests/unit/popupSupport.test.ts +109 -0
  156. package/tests/unit/resolvePaneTarget.test.ts +43 -0
  157. package/tests/unit/resolveWindowTarget.test.ts +36 -0
  158. package/tests/unit/safeRm.test.ts +41 -0
  159. package/tests/unit/scope.test.ts +57 -0
  160. package/tsconfig.json +14 -0
  161. package/vitest.config.ts +16 -0
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveCodexSessionInfo = resolveCodexSessionInfo;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const ps_1 = require("../proc/ps");
10
+ const lsof_1 = require("../proc/lsof");
11
+ const exec_1 = require("../tmux/exec");
12
+ const UUID_RE = /([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/g;
13
+ const SHELL_SNAPSHOT_RE = /(?<path>\/[^\s"' ]*\/shell_snapshots\/(?<uuid>[0-9a-fA-F-]{36})\.(?:sh|ps1))/;
14
+ const CODEX_COMMAND_PATTERNS = [
15
+ /\/@openai\/codex\/.*\/vendor\/.*\/codex\/codex\b/i,
16
+ /\/@openai\/codex\/bin\/codex\b/i,
17
+ /(^|[\s/])codex([\s]|$)/i
18
+ ];
19
+ function matchesCodexCommand(command) {
20
+ return CODEX_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
21
+ }
22
+ function parseSessionIdFromPath(value) {
23
+ const matches = value.match(UUID_RE);
24
+ if (!matches || matches.length === 0) {
25
+ throw new Error(`unable to parse session id from rollout path: ${value}`);
26
+ }
27
+ return matches[matches.length - 1];
28
+ }
29
+ function inferCodexHomeFromRolloutPath(rolloutPath) {
30
+ const parts = node_path_1.default.resolve(rolloutPath).split(node_path_1.default.sep);
31
+ const idx = parts.lastIndexOf("sessions");
32
+ if (idx <= 0) {
33
+ return undefined;
34
+ }
35
+ return parts.slice(0, idx).join(node_path_1.default.sep);
36
+ }
37
+ function inferCodexHomeFromSnapshotPath(snapshotPath) {
38
+ const resolved = node_path_1.default.resolve(snapshotPath);
39
+ const parent = node_path_1.default.dirname(resolved);
40
+ if (node_path_1.default.basename(parent) !== "shell_snapshots") {
41
+ return undefined;
42
+ }
43
+ return node_path_1.default.dirname(parent);
44
+ }
45
+ function tryParseShellSnapshot(cmdline) {
46
+ const match = SHELL_SNAPSHOT_RE.exec(cmdline);
47
+ if (!match || !match.groups) {
48
+ return null;
49
+ }
50
+ const snapshotPath = match.groups.path;
51
+ const sessionId = match.groups.uuid;
52
+ const codexHome = inferCodexHomeFromSnapshotPath(snapshotPath);
53
+ return { sessionId, snapshotPath, codexHome };
54
+ }
55
+ async function listRolloutFiles(codexHome, limit = 800) {
56
+ const sessionsDir = node_path_1.default.join(codexHome, "sessions");
57
+ const results = [];
58
+ const stack = [sessionsDir];
59
+ while (stack.length) {
60
+ const current = stack.pop();
61
+ if (!current) {
62
+ continue;
63
+ }
64
+ let entries;
65
+ try {
66
+ entries = await node_fs_1.promises.readdir(current, { withFileTypes: true });
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ for (const entry of entries) {
72
+ const full = node_path_1.default.join(current, entry.name);
73
+ if (entry.isDirectory()) {
74
+ stack.push(full);
75
+ continue;
76
+ }
77
+ if (!entry.isFile()) {
78
+ continue;
79
+ }
80
+ if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) {
81
+ continue;
82
+ }
83
+ try {
84
+ const stat = await node_fs_1.promises.stat(full);
85
+ results.push({ path: full, mtimeMs: stat.mtimeMs });
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ if (results.length >= limit) {
91
+ break;
92
+ }
93
+ }
94
+ if (results.length >= limit) {
95
+ break;
96
+ }
97
+ }
98
+ results.sort((a, b) => b.mtimeMs - a.mtimeMs);
99
+ return results.map((item) => item.path);
100
+ }
101
+ async function findRolloutBySessionId(codexHome, sessionId) {
102
+ const rollouts = await listRolloutFiles(codexHome, 800);
103
+ const candidates = rollouts.filter((p) => node_path_1.default.basename(p).includes(sessionId));
104
+ if (candidates.length === 0) {
105
+ return null;
106
+ }
107
+ return candidates[0];
108
+ }
109
+ async function readSessionMetaCwd(rolloutPath, maxLines = 30) {
110
+ try {
111
+ const handle = await node_fs_1.promises.open(rolloutPath, "r");
112
+ const stream = handle.createReadStream({ encoding: "utf8" });
113
+ let buffer = "";
114
+ let lineCount = 0;
115
+ for await (const chunk of stream) {
116
+ buffer += chunk;
117
+ let idx = buffer.indexOf("\n");
118
+ while (idx !== -1) {
119
+ const line = buffer.slice(0, idx).trim();
120
+ buffer = buffer.slice(idx + 1);
121
+ if (line) {
122
+ try {
123
+ const obj = JSON.parse(line);
124
+ if (obj?.type === "session_meta" && typeof obj?.payload === "object") {
125
+ const cwd = obj.payload?.cwd;
126
+ if (typeof cwd === "string" && cwd) {
127
+ await handle.close();
128
+ return cwd;
129
+ }
130
+ }
131
+ }
132
+ catch {
133
+ // ignore parse errors
134
+ }
135
+ lineCount += 1;
136
+ if (lineCount >= maxLines) {
137
+ await handle.close();
138
+ return null;
139
+ }
140
+ }
141
+ idx = buffer.indexOf("\n");
142
+ }
143
+ }
144
+ await handle.close();
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ return null;
150
+ }
151
+ function buildProcessMaps(processes) {
152
+ const parentByPid = new Map();
153
+ const commandByPid = new Map();
154
+ const ttyByPid = new Map();
155
+ for (const proc of processes) {
156
+ parentByPid.set(proc.pid, proc.ppid);
157
+ commandByPid.set(proc.pid, proc.command);
158
+ ttyByPid.set(proc.pid, proc.tty);
159
+ }
160
+ return { parentByPid, commandByPid, ttyByPid };
161
+ }
162
+ function isDescendant(pid, ancestorPid, parentByPid) {
163
+ let current = pid;
164
+ for (let i = 0; i < 400; i += 1) {
165
+ if (current === ancestorPid) {
166
+ return true;
167
+ }
168
+ current = parentByPid.get(current) ?? 0;
169
+ if (current <= 1) {
170
+ return false;
171
+ }
172
+ }
173
+ return false;
174
+ }
175
+ function filterRolloutPaths(paths) {
176
+ const out = [];
177
+ const seen = new Set();
178
+ for (const filePath of paths) {
179
+ if (!filePath.includes("rollout-") || !filePath.endsWith(".jsonl")) {
180
+ continue;
181
+ }
182
+ if (!filePath.includes(`${node_path_1.default.sep}sessions${node_path_1.default.sep}`)) {
183
+ continue;
184
+ }
185
+ if (seen.has(filePath)) {
186
+ continue;
187
+ }
188
+ seen.add(filePath);
189
+ out.push(filePath);
190
+ }
191
+ return out;
192
+ }
193
+ async function latestExistingPath(paths) {
194
+ const candidates = [];
195
+ for (const p of paths) {
196
+ try {
197
+ const stat = await node_fs_1.promises.stat(p);
198
+ candidates.push({ path: p, mtimeMs: stat.mtimeMs });
199
+ }
200
+ catch {
201
+ continue;
202
+ }
203
+ }
204
+ if (candidates.length === 0) {
205
+ throw new Error("rollout file not found");
206
+ }
207
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
208
+ return candidates[0].path;
209
+ }
210
+ async function getPanePid(paneId) {
211
+ try {
212
+ const result = await (0, exec_1.tmuxExec)(["display-message", "-p", "-t", paneId, "#{pane_pid}"]);
213
+ const trimmed = result.stdout.trim();
214
+ const pid = Number(trimmed);
215
+ return Number.isFinite(pid) ? pid : undefined;
216
+ }
217
+ catch {
218
+ return undefined;
219
+ }
220
+ }
221
+ async function detectShellSnapshot(processes, codexHome) {
222
+ const { parentByPid, commandByPid } = buildProcessMaps(processes);
223
+ let current = process.pid;
224
+ for (let i = 0; i < 80; i += 1) {
225
+ const cmdline = commandByPid.get(current) ?? "";
226
+ const hit = tryParseShellSnapshot(cmdline);
227
+ if (hit) {
228
+ const home = hit.codexHome ?? codexHome;
229
+ const rolloutPath = await findRolloutBySessionId(home, hit.sessionId);
230
+ if (rolloutPath) {
231
+ const inferredHome = inferCodexHomeFromRolloutPath(rolloutPath) ?? home;
232
+ return {
233
+ sessionId: hit.sessionId,
234
+ rolloutPath,
235
+ method: "shell-snapshot",
236
+ selfPid: process.pid,
237
+ codexHome: inferredHome
238
+ };
239
+ }
240
+ }
241
+ current = parentByPid.get(current) ?? 0;
242
+ if (current <= 1) {
243
+ break;
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+ async function pickRolloutForAnchorPid(anchorPid, processes, codexHome) {
249
+ const { parentByPid, commandByPid } = buildProcessMaps(processes);
250
+ const matchedPids = [];
251
+ let paths = [];
252
+ const snapshotHits = [];
253
+ for (const proc of processes) {
254
+ if (!matchesCodexCommand(proc.command)) {
255
+ continue;
256
+ }
257
+ if (!isDescendant(proc.pid, anchorPid, parentByPid)) {
258
+ continue;
259
+ }
260
+ matchedPids.push(proc.pid);
261
+ const snapshotHit = tryParseShellSnapshot(proc.command);
262
+ if (snapshotHit) {
263
+ snapshotHits.push(snapshotHit);
264
+ }
265
+ try {
266
+ const openFiles = await (0, lsof_1.listOpenFiles)(proc.pid);
267
+ paths = paths.concat(filterRolloutPaths(openFiles));
268
+ }
269
+ catch {
270
+ // ignore
271
+ }
272
+ }
273
+ const uniqMatched = Array.from(new Set(matchedPids));
274
+ if (paths.length > 0) {
275
+ const rolloutPath = await latestExistingPath(paths);
276
+ return { rolloutPath, matchedPids: uniqMatched, method: "anchor" };
277
+ }
278
+ for (const hit of snapshotHits) {
279
+ const home = hit.codexHome ?? codexHome;
280
+ const rolloutPath = await findRolloutBySessionId(home, hit.sessionId);
281
+ if (rolloutPath) {
282
+ return { rolloutPath, matchedPids: uniqMatched, method: "anchor-shell-snapshot" };
283
+ }
284
+ }
285
+ const rollouts = await listRolloutFiles(codexHome, 200);
286
+ const uniqRollouts = Array.from(new Set(rollouts));
287
+ if (uniqRollouts.length === 1) {
288
+ return { rolloutPath: uniqRollouts[0], matchedPids: uniqMatched, method: "sessions-singleton" };
289
+ }
290
+ if (uniqRollouts.length > 1) {
291
+ const lines = uniqRollouts
292
+ .slice(0, 12)
293
+ .map((p) => `- ${p}`)
294
+ .join("\n");
295
+ throw new Error(`no rollout paths found for anchor pid ${anchorPid} (found ${uniqRollouts.length} rollouts under codex home)\n${lines}`);
296
+ }
297
+ throw new Error(`no rollout paths found for anchor pid ${anchorPid}`);
298
+ }
299
+ async function pickRolloutAuto(processes, codexHome) {
300
+ const { parentByPid, commandByPid, ttyByPid } = buildProcessMaps(processes);
301
+ const selfPid = process.pid;
302
+ let current = selfPid;
303
+ for (let i = 0; i < 600; i += 1) {
304
+ current = parentByPid.get(current) ?? 0;
305
+ if (current <= 1) {
306
+ break;
307
+ }
308
+ const cmd = commandByPid.get(current) ?? "";
309
+ if (matchesCodexCommand(cmd)) {
310
+ try {
311
+ const picked = await pickRolloutForAnchorPid(current, processes, codexHome);
312
+ return { ...picked, method: "ancestor" };
313
+ }
314
+ catch {
315
+ continue;
316
+ }
317
+ }
318
+ }
319
+ const selfTty = ttyByPid.get(selfPid) ?? "?";
320
+ if (selfTty && selfTty !== "?") {
321
+ const rolloutsByPid = [];
322
+ for (const proc of processes) {
323
+ if (!matchesCodexCommand(proc.command)) {
324
+ continue;
325
+ }
326
+ if ((ttyByPid.get(proc.pid) ?? "?") !== selfTty) {
327
+ continue;
328
+ }
329
+ const openFiles = await (0, lsof_1.listOpenFiles)(proc.pid);
330
+ const filtered = filterRolloutPaths(openFiles);
331
+ if (filtered.length === 0) {
332
+ continue;
333
+ }
334
+ try {
335
+ rolloutsByPid.push({ pid: proc.pid, path: await latestExistingPath(filtered) });
336
+ }
337
+ catch {
338
+ continue;
339
+ }
340
+ }
341
+ const uniq = Array.from(new Set(rolloutsByPid.map((item) => item.path)));
342
+ if (uniq.length === 1) {
343
+ return { rolloutPath: uniq[0], matchedPids: [rolloutsByPid[0].pid], method: "tty" };
344
+ }
345
+ if (uniq.length > 1) {
346
+ const lines = rolloutsByPid
347
+ .slice(0, 12)
348
+ .map((item) => `- pid=${item.pid} rollout=${item.path}`)
349
+ .join("\n");
350
+ throw new Error(`multiple codex sessions on same tty; provide pane/pid.\n${lines}`);
351
+ }
352
+ }
353
+ const rolloutsByPid = [];
354
+ for (const proc of processes) {
355
+ if (!matchesCodexCommand(proc.command)) {
356
+ continue;
357
+ }
358
+ const openFiles = await (0, lsof_1.listOpenFiles)(proc.pid);
359
+ const filtered = filterRolloutPaths(openFiles);
360
+ if (filtered.length === 0) {
361
+ continue;
362
+ }
363
+ try {
364
+ rolloutsByPid.push({ pid: proc.pid, path: await latestExistingPath(filtered) });
365
+ }
366
+ catch {
367
+ continue;
368
+ }
369
+ }
370
+ const uniq = Array.from(new Set(rolloutsByPid.map((item) => item.path)));
371
+ if (uniq.length === 1) {
372
+ return { rolloutPath: uniq[0], matchedPids: [rolloutsByPid[0].pid], method: "global-singleton" };
373
+ }
374
+ if (uniq.length > 1) {
375
+ const lines = rolloutsByPid
376
+ .slice(0, 12)
377
+ .map((item) => `- pid=${item.pid} rollout=${item.path}`)
378
+ .join("\n");
379
+ throw new Error(`multiple codex sessions found; provide pane/pid.\n${lines}`);
380
+ }
381
+ throw new Error("no active codex sessions detected");
382
+ }
383
+ async function resolveCodexSessionInfo(options = {}) {
384
+ const codexHome = options.codexHome?.trim() ||
385
+ process.env.CODEX_HOME?.trim() ||
386
+ node_path_1.default.join(process.env.HOME || "", ".codex");
387
+ let anchorPid = options.panePid;
388
+ if (!anchorPid && options.paneId) {
389
+ anchorPid = await getPanePid(options.paneId);
390
+ }
391
+ let picked;
392
+ if (anchorPid) {
393
+ const processes = await (0, ps_1.listProcesses)();
394
+ picked = await pickRolloutForAnchorPid(anchorPid, processes, codexHome);
395
+ }
396
+ else {
397
+ const processes = await (0, ps_1.listProcesses)();
398
+ const snapshot = await detectShellSnapshot(processes, codexHome);
399
+ if (snapshot) {
400
+ return snapshot;
401
+ }
402
+ picked = await pickRolloutAuto(processes, codexHome);
403
+ }
404
+ const sessionId = parseSessionIdFromPath(picked.rolloutPath);
405
+ const inferredHome = inferCodexHomeFromRolloutPath(picked.rolloutPath) ?? codexHome;
406
+ if (options.cd) {
407
+ const cwd = await readSessionMetaCwd(picked.rolloutPath);
408
+ if (cwd && node_path_1.default.resolve(cwd) !== node_path_1.default.resolve(options.cd)) {
409
+ if (options.allowScanSessions) {
410
+ const rollouts = await listRolloutFiles(codexHome, 800);
411
+ const matches = [];
412
+ for (const rollout of rollouts) {
413
+ const metaCwd = await readSessionMetaCwd(rollout);
414
+ if (!metaCwd) {
415
+ continue;
416
+ }
417
+ if (node_path_1.default.resolve(metaCwd) !== node_path_1.default.resolve(options.cd)) {
418
+ continue;
419
+ }
420
+ matches.push({ sessionId: parseSessionIdFromPath(rollout), path: rollout });
421
+ }
422
+ if (matches.length === 1) {
423
+ return {
424
+ sessionId: matches[0].sessionId,
425
+ rolloutPath: matches[0].path,
426
+ method: "sessions-cwd",
427
+ codexHome: inferCodexHomeFromRolloutPath(matches[0].path) ?? codexHome
428
+ };
429
+ }
430
+ if (matches.length > 1) {
431
+ const lines = matches
432
+ .slice(0, 12)
433
+ .map((item) => `- ${item.sessionId} ${item.path}`)
434
+ .join("\n");
435
+ throw new Error(`multiple sessions matched cwd; provide explicit session id or rollout path.\n${lines}`);
436
+ }
437
+ }
438
+ throw new Error("detected rollout cwd does not match cd filter");
439
+ }
440
+ }
441
+ return {
442
+ sessionId,
443
+ rolloutPath: picked.rolloutPath,
444
+ method: picked.method,
445
+ matchedPids: picked.matchedPids,
446
+ selfPid: process.pid,
447
+ codexHome: inferredHome
448
+ };
449
+ }
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.codexSpawn = codexSpawn;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const exec_1 = require("../tmux/exec");
11
+ function parseNumber(value) {
12
+ const num = Number(value);
13
+ return Number.isFinite(num) ? num : 0;
14
+ }
15
+ async function getWindowId(paneId) {
16
+ const result = await (0, exec_1.tmuxExec)(["display-message", "-p", "-t", paneId, "#{window_id}"]);
17
+ const windowId = result.stdout.trim();
18
+ if (!windowId) {
19
+ throw new Error(`unable to resolve window_id from pane ${paneId}`);
20
+ }
21
+ return windowId;
22
+ }
23
+ async function getPaneCurrentPath(paneId) {
24
+ const result = await (0, exec_1.tmuxExec)([
25
+ "display-message",
26
+ "-p",
27
+ "-t",
28
+ paneId,
29
+ "#{pane_current_path}"
30
+ ]);
31
+ return result.stdout.trim() || process.cwd();
32
+ }
33
+ async function listPaneLayout(windowId) {
34
+ const result = await (0, exec_1.tmuxExec)([
35
+ "list-panes",
36
+ "-t",
37
+ windowId,
38
+ "-F",
39
+ "#{pane_id}|#{pane_left}|#{pane_top}|#{pane_current_path}"
40
+ ]);
41
+ const rows = [];
42
+ for (const line of result.stdout.split(/\r?\n/)) {
43
+ const trimmed = line.trim();
44
+ if (!trimmed) {
45
+ continue;
46
+ }
47
+ const [paneId, leftRaw, topRaw, panePath] = trimmed.split("|");
48
+ if (!paneId) {
49
+ continue;
50
+ }
51
+ rows.push({
52
+ paneId,
53
+ left: parseNumber(leftRaw),
54
+ top: parseNumber(topRaw),
55
+ path: (panePath ?? "").trim()
56
+ });
57
+ }
58
+ rows.sort((a, b) => (a.left !== b.left ? a.left - b.left : a.top - b.top));
59
+ return rows;
60
+ }
61
+ function buildColumns(rows) {
62
+ const columns = [];
63
+ const byLeft = new Map();
64
+ for (const row of rows) {
65
+ let col = byLeft.get(row.left);
66
+ if (!col) {
67
+ col = {
68
+ left: row.left,
69
+ count: 0,
70
+ topPaneId: row.paneId,
71
+ topPath: row.path,
72
+ topTop: row.top,
73
+ bottomPaneId: row.paneId,
74
+ bottomPath: row.path,
75
+ bottomTop: row.top
76
+ };
77
+ byLeft.set(row.left, col);
78
+ columns.push(col);
79
+ }
80
+ col.count += 1;
81
+ if (row.top < col.topTop) {
82
+ col.topTop = row.top;
83
+ col.topPaneId = row.paneId;
84
+ col.topPath = row.path;
85
+ }
86
+ if (row.top > col.bottomTop) {
87
+ col.bottomTop = row.top;
88
+ col.bottomPaneId = row.paneId;
89
+ col.bottomPath = row.path;
90
+ }
91
+ }
92
+ columns.sort((a, b) => a.left - b.left);
93
+ return columns;
94
+ }
95
+ function resolveReadyFile() {
96
+ const dir = (process.env.CODEX_PANE_READY_DIR || process.env.TMPDIR || node_os_1.default.tmpdir()).trim();
97
+ if (!dir) {
98
+ return null;
99
+ }
100
+ const safeDir = dir.replace(/\/+$/, "");
101
+ const name = `codex_pane_ready_${process.pid}_${Date.now()}_${Math.random()
102
+ .toString(16)
103
+ .slice(2)}`;
104
+ return node_path_1.default.join(safeDir, name);
105
+ }
106
+ async function waitForReadyFile(filePath) {
107
+ const maxTries = parseNumber(process.env.CODEX_PANE_READY_MAX_TRIES || "120");
108
+ const sleepMs = parseNumber(process.env.CODEX_PANE_READY_SLEEP_MS || "50");
109
+ for (let i = 0; i < Math.max(1, maxTries); i += 1) {
110
+ try {
111
+ await node_fs_1.promises.stat(filePath);
112
+ try {
113
+ await node_fs_1.promises.rm(filePath, { force: true });
114
+ }
115
+ catch {
116
+ // ignore
117
+ }
118
+ return;
119
+ }
120
+ catch {
121
+ // keep waiting
122
+ }
123
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, sleepMs)));
124
+ }
125
+ }
126
+ async function splitWindow(args, readyFile) {
127
+ if (readyFile) {
128
+ try {
129
+ const result = await (0, exec_1.tmuxExec)([
130
+ ...args,
131
+ "-e",
132
+ `CODEX_PANE_READY_FILE=${readyFile}`
133
+ ]);
134
+ return { paneId: result.stdout.trim(), readyUsed: true };
135
+ }
136
+ catch {
137
+ // fall through
138
+ }
139
+ }
140
+ const result = await (0, exec_1.tmuxExec)(args);
141
+ return { paneId: result.stdout.trim(), readyUsed: false };
142
+ }
143
+ async function codexSpawn(originPaneId, options = {}) {
144
+ const windowId = await getWindowId(originPaneId);
145
+ const layout = await listPaneLayout(windowId);
146
+ const totalPanes = layout.length;
147
+ const readyFile = resolveReadyFile();
148
+ if (options.forceSimpleSplit) {
149
+ const originPath = await getPaneCurrentPath(originPaneId);
150
+ const { paneId, readyUsed } = await splitWindow(["split-window", "-d", "-P", "-F", "#{pane_id}", "-t", originPaneId, "-c", originPath], readyFile);
151
+ if (!paneId) {
152
+ throw new Error("failed to create new pane");
153
+ }
154
+ if (readyUsed && readyFile) {
155
+ await waitForReadyFile(readyFile);
156
+ }
157
+ return {
158
+ createdPaneId: paneId,
159
+ meta: {
160
+ window_id: windowId,
161
+ action: "simple_split",
162
+ total_panes: totalPanes + 1,
163
+ source: "force-simple-split"
164
+ }
165
+ };
166
+ }
167
+ const columns = buildColumns(layout);
168
+ const colCount = columns.length;
169
+ if (colCount === 0) {
170
+ throw new Error(`no panes detected in window ${windowId}`);
171
+ }
172
+ if (colCount > 3) {
173
+ throw new Error("window has more than 3 columns; refusing to add pane");
174
+ }
175
+ if (columns[0].count !== 1) {
176
+ throw new Error(`leftmost column has ${columns[0].count} panes; expected 1`);
177
+ }
178
+ if (totalPanes >= 7) {
179
+ throw new Error("window already at max panes (1+3+3)");
180
+ }
181
+ if (colCount < 3) {
182
+ if (colCount === 2 && columns[1].count !== 1) {
183
+ throw new Error("right column already split; refusing to add a third column");
184
+ }
185
+ const target = columns[colCount - 1];
186
+ const { paneId, readyUsed } = await splitWindow([
187
+ "split-window",
188
+ "-h",
189
+ "-d",
190
+ "-P",
191
+ "-F",
192
+ "#{pane_id}",
193
+ "-t",
194
+ target.topPaneId,
195
+ "-c",
196
+ target.topPath
197
+ ], readyFile);
198
+ if (!paneId) {
199
+ throw new Error("failed to create new pane");
200
+ }
201
+ if (readyUsed && readyFile) {
202
+ await waitForReadyFile(readyFile);
203
+ }
204
+ return {
205
+ createdPaneId: paneId,
206
+ meta: {
207
+ window_id: windowId,
208
+ action: "add_column",
209
+ column: colCount + 1,
210
+ total_panes: totalPanes + 1
211
+ }
212
+ };
213
+ }
214
+ if (columns[1].count >= 3 && columns[2].count >= 3) {
215
+ throw new Error("columns 2 and 3 are full; refusing to add pane");
216
+ }
217
+ const targetColumnIndex = columns[1].count < 3 ? 1 : 2;
218
+ const targetColumn = columns[targetColumnIndex];
219
+ const { paneId, readyUsed } = await splitWindow([
220
+ "split-window",
221
+ "-v",
222
+ "-d",
223
+ "-P",
224
+ "-F",
225
+ "#{pane_id}",
226
+ "-t",
227
+ targetColumn.bottomPaneId,
228
+ "-c",
229
+ targetColumn.bottomPath
230
+ ], readyFile);
231
+ if (!paneId) {
232
+ throw new Error("failed to create new pane");
233
+ }
234
+ if (readyUsed && readyFile) {
235
+ await waitForReadyFile(readyFile);
236
+ }
237
+ return {
238
+ createdPaneId: paneId,
239
+ meta: {
240
+ window_id: windowId,
241
+ action: "add_row",
242
+ column: targetColumnIndex + 1,
243
+ total_panes: totalPanes + 1
244
+ }
245
+ };
246
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });