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/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
|
+
}
|