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
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "tmux-agent",
3
+ "version": "0.1.0",
4
+ "description": "LLM-friendly tmux control plane CLI",
5
+ "packageManager": "bun@1.2.17",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "agent-tmux": "dist/cli/index.js"
9
+ },
10
+ "dependencies": {
11
+ "commander": "^12.1.0"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "typecheck": "tsc -p tsconfig.json --noEmit",
16
+ "test": "vitest run"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.11.30",
20
+ "typescript": "^5.5.4",
21
+ "vitest": "^1.6.0"
22
+ }
23
+ }
@@ -0,0 +1,141 @@
1
+ import { formatJson } from "../../../lib/output/format";
2
+ import { readStdin } from "../../../lib/io/readStdin";
3
+ import { cleanupForkHome, prepareForkHome } from "../../../lib/codex/forkHome";
4
+
5
+ type PrepareSpec = {
6
+ parent_codex_home?: string;
7
+ parent_rollout_path?: string;
8
+ fork_home?: string;
9
+ run_id?: string;
10
+ copy_config?: boolean;
11
+ };
12
+
13
+ type CleanupSpec = {
14
+ path?: string;
15
+ cleanup_path?: string;
16
+ allowed_root?: string;
17
+ };
18
+
19
+ function parseJsonObject(raw: string): Record<string, unknown> {
20
+ let parsed: unknown;
21
+ try {
22
+ parsed = JSON.parse(raw);
23
+ } catch (error) {
24
+ throw new Error("spec must be valid JSON");
25
+ }
26
+ if (!parsed || typeof parsed !== "object") {
27
+ throw new Error("spec must be a JSON object");
28
+ }
29
+ return parsed as Record<string, unknown>;
30
+ }
31
+
32
+ async function readSpec(specArg?: string): Promise<Record<string, unknown> | null> {
33
+ if (!specArg) {
34
+ return null;
35
+ }
36
+ if (specArg !== "-") {
37
+ throw new Error("spec must be '-' to read from stdin");
38
+ }
39
+ const raw = await readStdin();
40
+ return parseJsonObject(raw);
41
+ }
42
+
43
+ function pickString(
44
+ value: string | boolean | undefined,
45
+ fallback: unknown
46
+ ): string | undefined {
47
+ if (typeof value === "string" && value.trim()) {
48
+ return value.trim();
49
+ }
50
+ if (typeof fallback === "string" && fallback.trim()) {
51
+ return fallback.trim();
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function pickBoolean(
57
+ value: string | boolean | undefined,
58
+ fallback: unknown,
59
+ defaultValue: boolean
60
+ ): boolean {
61
+ if (typeof value === "boolean") {
62
+ return value;
63
+ }
64
+ if (typeof value === "string") {
65
+ return value.trim().toLowerCase() !== "false";
66
+ }
67
+ if (typeof fallback === "boolean") {
68
+ return fallback;
69
+ }
70
+ return defaultValue;
71
+ }
72
+
73
+ export type CodexForkHomePrepareOptions = {
74
+ json?: boolean;
75
+ parentRollout?: string;
76
+ forkHome?: string;
77
+ parentCodexHome?: string;
78
+ runId?: string;
79
+ copyConfig?: boolean;
80
+ spec?: string;
81
+ };
82
+
83
+ export async function codexForkHomePrepareCommand(
84
+ options: CodexForkHomePrepareOptions = {}
85
+ ): Promise<string> {
86
+ const spec = await readSpec(options.spec);
87
+ const specTyped = spec as PrepareSpec | null;
88
+
89
+ const parentRolloutPath = pickString(
90
+ options.parentRollout,
91
+ specTyped?.parent_rollout_path
92
+ );
93
+ const forkHome = pickString(options.forkHome, specTyped?.fork_home);
94
+ if (!parentRolloutPath || !forkHome) {
95
+ throw new Error("fork-home prepare requires --parent-rollout and --fork-home");
96
+ }
97
+
98
+ const result = await prepareForkHome({
99
+ parentCodexHome: pickString(options.parentCodexHome, specTyped?.parent_codex_home),
100
+ parentRolloutPath,
101
+ forkHome,
102
+ runId: pickString(options.runId, specTyped?.run_id),
103
+ copyConfig: pickBoolean(options.copyConfig, specTyped?.copy_config, true)
104
+ });
105
+
106
+ if (options.json) {
107
+ return formatJson(result);
108
+ }
109
+
110
+ return `${result.fork_home}\n${result.fork_rollout_path}`;
111
+ }
112
+
113
+ export type CodexForkHomeCleanupOptions = {
114
+ json?: boolean;
115
+ path?: string;
116
+ allowedRoot?: string;
117
+ spec?: string;
118
+ };
119
+
120
+ export async function codexForkHomeCleanupCommand(
121
+ options: CodexForkHomeCleanupOptions = {}
122
+ ): Promise<string> {
123
+ const spec = await readSpec(options.spec);
124
+ const specTyped = spec as CleanupSpec | null;
125
+
126
+ const cleanupPath = pickString(options.path, specTyped?.path ?? specTyped?.cleanup_path);
127
+ const allowedRoot = pickString(options.allowedRoot, specTyped?.allowed_root);
128
+ if (!cleanupPath || !allowedRoot) {
129
+ throw new Error("fork-home cleanup requires --path and --allowed-root");
130
+ }
131
+
132
+ const result = await cleanupForkHome(cleanupPath, allowedRoot);
133
+ if (!result.removed) {
134
+ throw new Error("cleanup did not remove path");
135
+ }
136
+
137
+ if (options.json) {
138
+ return formatJson(result);
139
+ }
140
+ return String(result.removed);
141
+ }
@@ -0,0 +1,83 @@
1
+ import { formatJson } from "../../../lib/output/format";
2
+ import { readStdin } from "../../../lib/io/readStdin";
3
+ import { resolveSessionScope, resolveWindowScope } from "../../../lib/targeting/scope";
4
+ import { resolvePaneTarget } from "../../../lib/targeting/resolvePaneTarget";
5
+ import { snapshotPanes } from "../../../lib/tmux/snapshotPanes";
6
+ import { codexSend } from "../../../lib/codex/send";
7
+
8
+ function isPaneIdTarget(target: string): boolean {
9
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
10
+ }
11
+
12
+ export type CodexSendOptions = {
13
+ json?: boolean;
14
+ session?: string;
15
+ window?: string;
16
+ submit?: string;
17
+ submitDelayMs?: number;
18
+ postDelayMs?: number;
19
+ captureTail?: number;
20
+ };
21
+
22
+ export async function codexSendCommand(
23
+ target: string,
24
+ textArg: string,
25
+ options: CodexSendOptions = {}
26
+ ): Promise<string> {
27
+ if (!target || !textArg) {
28
+ throw new Error("codex send requires <paneTarget> <text>");
29
+ }
30
+
31
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
32
+
33
+ let windowId: string | undefined;
34
+ if (!isPaneIdTarget(target)) {
35
+ const window = await resolveWindowScope(
36
+ scope.session,
37
+ options.window?.trim() || undefined
38
+ );
39
+ windowId = window.wid;
40
+ }
41
+
42
+ const panes = await snapshotPanes({ session: scope.session });
43
+ const resolved = resolvePaneTarget(panes, target, { windowId });
44
+
45
+ const textSource = textArg === "-" ? "stdin" : "arg";
46
+ const text = textArg === "-" ? await readStdin() : textArg;
47
+
48
+ const submitRaw = options.submit === undefined ? "Enter" : String(options.submit);
49
+ const submitKeyRaw = submitRaw.trim();
50
+ const submitKey =
51
+ submitKeyRaw.toLowerCase() === "none" || submitKeyRaw.toLowerCase() === "false"
52
+ ? ""
53
+ : submitKeyRaw;
54
+ const submitDelayMs =
55
+ (options.submitDelayMs !== undefined ? Math.max(0, options.submitDelayMs) : undefined) ??
56
+ (submitKey ? 250 : 0);
57
+ const postDelayMs = options.postDelayMs !== undefined ? Math.max(0, options.postDelayMs) : 0;
58
+ const captureTailLines =
59
+ options.captureTail !== undefined ? Math.max(0, Math.floor(options.captureTail)) : undefined;
60
+
61
+ const result = await codexSend(resolved.id, text, {
62
+ submitKey,
63
+ submitDelayMs,
64
+ postDelayMs,
65
+ captureTailLines
66
+ });
67
+
68
+ if (options.json) {
69
+ const { capture_tail, ...sent } = result;
70
+ return formatJson({
71
+ requested: { target, text_source: textSource },
72
+ resolved: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
73
+ sent,
74
+ ...(capture_tail ? { capture_tail } : {})
75
+ });
76
+ }
77
+
78
+ if (captureTailLines && captureTailLines > 0) {
79
+ return result.capture_tail ?? resolved.id;
80
+ }
81
+
82
+ return resolved.id;
83
+ }
@@ -0,0 +1,59 @@
1
+ import { formatJson } from "../../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../../lib/targeting/scope";
3
+ import { resolvePaneTarget } from "../../../lib/targeting/resolvePaneTarget";
4
+ import { snapshotPanes } from "../../../lib/tmux/snapshotPanes";
5
+ import { resolveCodexSessionInfo } from "../../../lib/codex/sessionInfo";
6
+
7
+ function isPaneIdTarget(target: string): boolean {
8
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
9
+ }
10
+
11
+ export type CodexSessionInfoOptions = {
12
+ json?: boolean;
13
+ session?: string;
14
+ window?: string;
15
+ };
16
+
17
+ export async function codexSessionInfoCommand(
18
+ target: string,
19
+ options: CodexSessionInfoOptions = {}
20
+ ): Promise<string> {
21
+ if (!target) {
22
+ throw new Error("codex session-info requires <paneTarget>");
23
+ }
24
+
25
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
26
+
27
+ let windowId: string | undefined;
28
+ if (!isPaneIdTarget(target)) {
29
+ const window = await resolveWindowScope(
30
+ scope.session,
31
+ options.window?.trim() || undefined
32
+ );
33
+ windowId = window.wid;
34
+ }
35
+
36
+ const panes = await snapshotPanes({ session: scope.session });
37
+ const resolved = resolvePaneTarget(panes, target, { windowId });
38
+ const resolvedPane = panes.find((pane) => pane.id === resolved.id);
39
+
40
+ const info = await resolveCodexSessionInfo({
41
+ paneId: resolved.id,
42
+ panePid: resolvedPane?.pid
43
+ });
44
+
45
+ if (options.json) {
46
+ return formatJson({
47
+ requested: { target },
48
+ resolved: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
49
+ session_id: info.sessionId,
50
+ rollout_path: info.rolloutPath,
51
+ method: info.method,
52
+ ...(info.matchedPids ? { matched_pids: info.matchedPids } : {}),
53
+ ...(info.selfPid ? { self_pid: info.selfPid } : {}),
54
+ ...(info.codexHome ? { codex_home: info.codexHome } : {})
55
+ });
56
+ }
57
+
58
+ return `${info.sessionId}\n${info.rolloutPath}`;
59
+ }
@@ -0,0 +1,90 @@
1
+ import { formatJson } from "../../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../../lib/targeting/scope";
3
+ import { resolvePaneTarget } from "../../../lib/targeting/resolvePaneTarget";
4
+ import { snapshotPanes } from "../../../lib/tmux/snapshotPanes";
5
+ import { codexSpawn } from "../../../lib/codex/spawn";
6
+ import { tmuxExec } from "../../../lib/tmux/exec";
7
+
8
+ function isPaneIdTarget(target: string): boolean {
9
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
10
+ }
11
+
12
+ async function readGlobalOriginOption(): Promise<string | null> {
13
+ try {
14
+ const result = await tmuxExec(["show", "-gqv", "@panel_origin_pane_id"]);
15
+ const value = result.stdout.trim();
16
+ if (!value || value.includes("#{") || !value.startsWith("%")) {
17
+ return null;
18
+ }
19
+ return value;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function resolveOriginCandidate(): string | null {
26
+ const envFirst = [process.env.CODEX_PANE_ID, process.env.ORIGIN_PANE_ID, process.env.TMUX_PANE];
27
+ for (const candidate of envFirst) {
28
+ const value = (candidate ?? "").trim();
29
+ if (!value || value.includes("#{")) {
30
+ continue;
31
+ }
32
+ return value;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export type CodexSpawnOptions = {
38
+ json?: boolean;
39
+ origin?: string;
40
+ session?: string;
41
+ window?: string;
42
+ forceSimpleSplit?: boolean;
43
+ };
44
+
45
+ export async function codexSpawnCommand(options: CodexSpawnOptions = {}): Promise<string> {
46
+ let originTarget = options.origin ? String(options.origin).trim() : "";
47
+ if (!originTarget) {
48
+ originTarget = resolveOriginCandidate() ?? "";
49
+ }
50
+ if (!originTarget) {
51
+ originTarget = (await readGlobalOriginOption()) ?? "";
52
+ }
53
+
54
+ if (!originTarget) {
55
+ throw new Error(
56
+ "codex spawn requires --origin <paneTarget> (or run inside tmux with TMUX_PANE)"
57
+ );
58
+ }
59
+
60
+ const panesAll = await snapshotPanes({});
61
+
62
+ let windowId: string | undefined;
63
+ if (!isPaneIdTarget(originTarget)) {
64
+ const scope = await resolveSessionScope(
65
+ options.session?.trim() || undefined
66
+ );
67
+ const window = await resolveWindowScope(
68
+ scope.session,
69
+ options.window?.trim() || undefined
70
+ );
71
+ windowId = window.wid;
72
+ }
73
+
74
+ const resolved = resolvePaneTarget(panesAll, originTarget, { windowId });
75
+
76
+ const created = await codexSpawn(resolved.id, {
77
+ forceSimpleSplit: Boolean(options.forceSimpleSplit)
78
+ });
79
+
80
+ if (options.json) {
81
+ return formatJson({
82
+ requested: { origin: originTarget },
83
+ origin: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
84
+ created: { id: created.createdPaneId },
85
+ meta: created.meta
86
+ });
87
+ }
88
+
89
+ return created.createdPaneId;
90
+ }
@@ -0,0 +1,40 @@
1
+ import { formatJson, formatPaneTable } from "../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../lib/targeting/scope";
3
+ import { snapshotPanes } from "../../lib/tmux/snapshotPanes";
4
+
5
+ export type FindOptions = {
6
+ json?: boolean;
7
+ session?: string;
8
+ window?: string;
9
+ };
10
+
11
+ export async function find(query: string, options: FindOptions = {}): Promise<string> {
12
+ if (!query || !query.trim()) {
13
+ throw new Error("missing find query");
14
+ }
15
+
16
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
17
+ const window = await resolveWindowScope(
18
+ scope.session,
19
+ options.window?.trim() || undefined
20
+ );
21
+
22
+ const panes = await snapshotPanes({ windowId: window.wid });
23
+ const needle = query.toLowerCase();
24
+ const matches = panes.filter(
25
+ (pane) =>
26
+ pane.command.toLowerCase().includes(needle) ||
27
+ pane.title.toLowerCase().includes(needle)
28
+ );
29
+
30
+ if (options.json) {
31
+ return formatJson({
32
+ query,
33
+ session: { name: scope.session },
34
+ window: { widx: window.widx, wid: window.wid, name: window.name },
35
+ panes: matches
36
+ });
37
+ }
38
+
39
+ return formatPaneTable(matches);
40
+ }
@@ -0,0 +1,49 @@
1
+ import { formatJson } from "../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../lib/targeting/scope";
3
+ import { resolvePaneTarget } from "../../lib/targeting/resolvePaneTarget";
4
+ import { snapshotPanes } from "../../lib/tmux/snapshotPanes";
5
+ import { paneKill } from "../../lib/tmux/paneOps";
6
+
7
+ function isPaneIdTarget(target: string): boolean {
8
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
9
+ }
10
+
11
+ export type PaneKillOptions = {
12
+ json?: boolean;
13
+ session?: string;
14
+ window?: string;
15
+ };
16
+
17
+ export async function paneKillCommand(
18
+ target: string,
19
+ options: PaneKillOptions = {}
20
+ ): Promise<string> {
21
+ if (!target) {
22
+ throw new Error("pane kill requires <paneTarget>");
23
+ }
24
+
25
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
26
+
27
+ let windowId: string | undefined;
28
+ if (!isPaneIdTarget(target)) {
29
+ const window = await resolveWindowScope(
30
+ scope.session,
31
+ options.window?.trim() || undefined
32
+ );
33
+ windowId = window.wid;
34
+ }
35
+
36
+ const panes = await snapshotPanes({ session: scope.session });
37
+ const resolved = resolvePaneTarget(panes, target, { windowId });
38
+ await paneKill(resolved.id);
39
+
40
+ if (options.json) {
41
+ return formatJson({
42
+ requested: { target },
43
+ resolved: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
44
+ killed: true
45
+ });
46
+ }
47
+
48
+ return resolved.id;
49
+ }
@@ -0,0 +1,53 @@
1
+ import { formatJson } from "../../lib/output/format";
2
+ import { readStdin } from "../../lib/io/readStdin";
3
+ import { resolveSessionScope, resolveWindowScope } from "../../lib/targeting/scope";
4
+ import { paneSpawn, paneTitle } from "../../lib/tmux/paneOps";
5
+ import { tmuxExec } from "../../lib/tmux/exec";
6
+ import { pasteText } from "../../lib/tmux/paste";
7
+
8
+ export type PaneSpawnOptions = {
9
+ json?: boolean;
10
+ session?: string;
11
+ window?: string;
12
+ title?: string;
13
+ };
14
+
15
+ export async function paneSpawnCommand(
16
+ commandArg: string,
17
+ options: PaneSpawnOptions = {}
18
+ ): Promise<string> {
19
+ if (!commandArg) {
20
+ throw new Error("pane spawn requires <command>");
21
+ }
22
+
23
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
24
+ const window = await resolveWindowScope(scope.session, options.window?.trim() || undefined);
25
+
26
+ let command = commandArg;
27
+ if (commandArg === "-") {
28
+ command = await readStdin();
29
+ }
30
+
31
+ const created = await paneSpawn(window.wid);
32
+ const title = options.title?.trim() || undefined;
33
+ if (command) {
34
+ await pasteText(created.id, command);
35
+ await tmuxExec(["send-keys", "-t", created.id, "Enter"]);
36
+ }
37
+ if (title) {
38
+ await paneTitle(created.id, title);
39
+ }
40
+
41
+ if (options.json) {
42
+ return formatJson({
43
+ created: {
44
+ id: created.id,
45
+ idx: created.idx,
46
+ command,
47
+ title: title ?? ""
48
+ }
49
+ });
50
+ }
51
+
52
+ return created.id;
53
+ }
@@ -0,0 +1,50 @@
1
+ import { formatJson } from "../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../lib/targeting/scope";
3
+ import { resolvePaneTarget } from "../../lib/targeting/resolvePaneTarget";
4
+ import { snapshotPanes } from "../../lib/tmux/snapshotPanes";
5
+ import { paneTitle } from "../../lib/tmux/paneOps";
6
+
7
+ function isPaneIdTarget(target: string): boolean {
8
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
9
+ }
10
+
11
+ export type PaneTitleOptions = {
12
+ json?: boolean;
13
+ session?: string;
14
+ window?: string;
15
+ };
16
+
17
+ export async function paneTitleCommand(
18
+ target: string,
19
+ title: string,
20
+ options: PaneTitleOptions = {}
21
+ ): Promise<string> {
22
+ if (!target || !title) {
23
+ throw new Error("pane title requires <paneTarget> <title>");
24
+ }
25
+
26
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
27
+
28
+ let windowId: string | undefined;
29
+ if (!isPaneIdTarget(target)) {
30
+ const window = await resolveWindowScope(
31
+ scope.session,
32
+ options.window?.trim() || undefined
33
+ );
34
+ windowId = window.wid;
35
+ }
36
+
37
+ const panes = await snapshotPanes({ session: scope.session });
38
+ const resolved = resolvePaneTarget(panes, target, { windowId });
39
+ await paneTitle(resolved.id, title);
40
+
41
+ if (options.json) {
42
+ return formatJson({
43
+ requested: { target, title },
44
+ resolved: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
45
+ updated: { title }
46
+ });
47
+ }
48
+
49
+ return resolved.id;
50
+ }
@@ -0,0 +1,48 @@
1
+ import { formatJson } from "../../lib/output/format";
2
+ import { resolveSessionScope, resolveWindowScope } from "../../lib/targeting/scope";
3
+ import { resolvePaneTarget } from "../../lib/targeting/resolvePaneTarget";
4
+ import { snapshotPanes } from "../../lib/tmux/snapshotPanes";
5
+ import { capturePane, splitLines } from "../../lib/tmux/capturePane";
6
+
7
+ function isPaneIdTarget(target: string): boolean {
8
+ return target.startsWith("%") || /^@[^%]+%[^%]+$/.test(target);
9
+ }
10
+
11
+ export type ReadOptions = {
12
+ json?: boolean;
13
+ session?: string;
14
+ window?: string;
15
+ lines?: number;
16
+ };
17
+
18
+ export async function read(target: string, options: ReadOptions = {}): Promise<string> {
19
+ if (!target) {
20
+ throw new Error("read requires <paneTarget>");
21
+ }
22
+
23
+ const lineCount = Math.max(1, Math.floor(options.lines ?? 20));
24
+ const scope = await resolveSessionScope(options.session?.trim() || undefined);
25
+
26
+ let windowId: string | undefined;
27
+ if (!isPaneIdTarget(target)) {
28
+ const window = await resolveWindowScope(
29
+ scope.session,
30
+ options.window?.trim() || undefined
31
+ );
32
+ windowId = window.wid;
33
+ }
34
+
35
+ const panes = await snapshotPanes({ session: scope.session });
36
+ const resolved = resolvePaneTarget(panes, target, { windowId });
37
+
38
+ const text = await capturePane(resolved.id, lineCount);
39
+ if (options.json) {
40
+ return formatJson({
41
+ requested: { target, lines: lineCount },
42
+ resolved: { id: resolved.id, ...(resolved.idx !== undefined ? { idx: resolved.idx } : {}) },
43
+ lines: splitLines(text)
44
+ });
45
+ }
46
+
47
+ return text;
48
+ }