aiwcli 0.13.8 → 0.15.1
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/README.md +11 -1
- package/dist/commands/launch.d.ts +8 -0
- package/dist/commands/launch.js +96 -5
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +42 -0
- package/dist/templates/_shared/.claude/skills/codex/prompt.md +30 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/headless.ts +33 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/index.ts +6 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +145 -0
- package/dist/templates/_shared/lib-ts/agent-exec/base-agent.ts +229 -0
- package/dist/templates/_shared/lib-ts/agent-exec/execution-backend.ts +50 -0
- package/dist/templates/_shared/lib-ts/agent-exec/index.ts +6 -0
- package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
- package/dist/templates/_shared/lib-ts/base/cli-args.ts +287 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +53 -47
- package/dist/templates/_shared/lib-ts/base/models.ts +16 -0
- package/dist/templates/_shared/lib-ts/base/preflight.ts +98 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +381 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +8 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +35 -11
- package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
- package/dist/templates/_shared/lib-ts/types.ts +17 -0
- package/dist/templates/_shared/scripts/status_line.ts +93 -47
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +71 -0
- package/dist/templates/_shared/skills/prompt-codex/scripts/launch-codex.ts +387 -0
- package/dist/templates/_shared/skills/prompt-codex/scripts/watch-codex.ts +257 -0
- package/dist/templates/cc-native/.claude/settings.json +121 -1
- package/dist/templates/cc-native/_cc-native/CLAUDE.md +73 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/CLAUDE.md +70 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +120 -43
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +66 -12
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/preflight.ts +14 -80
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/agent.ts +19 -7
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/base/base-agent.ts +4 -215
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/index.ts +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/claude-agent.ts +9 -39
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/codex-agent.ts +19 -22
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/gemini-agent.ts +2 -1
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +65 -36
- package/oclif.manifest.json +21 -3
- package/package.json +1 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Launch Codex in a tmux pane and inject a prompt into its REPL.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] plan
|
|
7
|
+
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] --file <path>
|
|
8
|
+
* bun launch-codex.ts [--model fast|standard|smart|<model-id>] [--sandbox read-only|workspace-write|danger-full-access] [--no-yolo] [--capture] [--context <id>] <inline text...>
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getTmuxAvailability,
|
|
16
|
+
launchDriverInTmuxOrFallback,
|
|
17
|
+
} from "../../../lib-ts/base/tmux-driver.js";
|
|
18
|
+
import { getProjectRoot } from "../../../lib-ts/base/constants.js";
|
|
19
|
+
import { resolveCodexModel, codexReplSpec, buildCliInvocation, isCodexSandbox, type CodexSandbox } from "../../../lib-ts/base/cli-args.js";
|
|
20
|
+
import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
|
|
21
|
+
import { logDebug, logWarn } from "../../../lib-ts/base/logger.js";
|
|
22
|
+
import { displayPath } from "../../../lib-ts/base/utils.js";
|
|
23
|
+
import { getContextBySessionId, getContext } from "../../../lib-ts/context/context-store.js";
|
|
24
|
+
import { buildExternalAgentContext } from "../../../lib-ts/context/context-formatter.js";
|
|
25
|
+
import { findLatestPlan } from "../../../lib-ts/context/plan-manager.js";
|
|
26
|
+
import type { ContextState } from "../../../lib-ts/types.js";
|
|
27
|
+
|
|
28
|
+
/** Codex-specific model abbreviations. Checked before tier resolution. */
|
|
29
|
+
const CODEX_ALIASES: Record<string, string> = {
|
|
30
|
+
spark: CODEX_MODELS.spark,
|
|
31
|
+
codex: CODEX_MODELS.codex,
|
|
32
|
+
gpt: CODEX_MODELS.gpt,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SESSION_DISCOVERY_TIMEOUT_MS = 12000;
|
|
36
|
+
const SESSION_DISCOVERY_POLL_MS = 250;
|
|
37
|
+
const SESSION_MTIME_WINDOW_MS = 120000;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function eprint(...args: unknown[]): void {
|
|
44
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sleep(ms: number): Promise<void> {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectSessionJsonlFiles(rootDir: string): string[] {
|
|
52
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
53
|
+
const stack: string[] = [rootDir];
|
|
54
|
+
const files: string[] = [];
|
|
55
|
+
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const dir = stack.pop();
|
|
58
|
+
if (!dir) continue;
|
|
59
|
+
let entries: fs.Dirent[] = [];
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fullPath = path.join(dir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
stack.push(fullPath);
|
|
70
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
71
|
+
files.push(fullPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function samePath(a: string, b: string): boolean {
|
|
80
|
+
const left = path.resolve(a);
|
|
81
|
+
const right = path.resolve(b);
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
return left === right;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readSessionMeta(sessionFile: string): { sessionId: string; cwd: string } | null {
|
|
89
|
+
try {
|
|
90
|
+
const raw = fs.readFileSync(sessionFile, "utf-8");
|
|
91
|
+
const firstLine = raw.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
92
|
+
if (!firstLine) return null;
|
|
93
|
+
const parsed = JSON.parse(firstLine);
|
|
94
|
+
if (parsed?.type !== "session_meta") return null;
|
|
95
|
+
const sessionId = parsed?.payload?.id;
|
|
96
|
+
const cwd = parsed?.payload?.cwd;
|
|
97
|
+
if (typeof sessionId !== "string" || typeof cwd !== "string") return null;
|
|
98
|
+
return { sessionId, cwd };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findLatestSessionCandidate(
|
|
105
|
+
projectRoot: string,
|
|
106
|
+
launchStartedAtMs: number,
|
|
107
|
+
): { sessionId: string; sessionFile: string } | null {
|
|
108
|
+
const sessionsRoot = path.join(os.homedir(), ".codex", "sessions");
|
|
109
|
+
const files = collectSessionJsonlFiles(sessionsRoot);
|
|
110
|
+
if (files.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const candidates: Array<{ sessionId: string; sessionFile: string; mtimeMs: number }> = [];
|
|
113
|
+
for (const sessionFile of files) {
|
|
114
|
+
let mtimeMs = 0;
|
|
115
|
+
try {
|
|
116
|
+
mtimeMs = fs.statSync(sessionFile).mtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (mtimeMs < launchStartedAtMs - SESSION_MTIME_WINDOW_MS) continue;
|
|
121
|
+
|
|
122
|
+
const meta = readSessionMeta(sessionFile);
|
|
123
|
+
if (!meta) continue;
|
|
124
|
+
if (!samePath(meta.cwd, projectRoot)) continue;
|
|
125
|
+
|
|
126
|
+
candidates.push({ sessionId: meta.sessionId, sessionFile, mtimeMs });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (candidates.length === 0) return null;
|
|
130
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
131
|
+
const best = candidates[0];
|
|
132
|
+
return best ? { sessionId: best.sessionId, sessionFile: best.sessionFile } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function waitForCaptureSession(
|
|
136
|
+
projectRoot: string,
|
|
137
|
+
launchStartedAtMs: number,
|
|
138
|
+
): Promise<{ sessionId: string; sessionFile: string } | null> {
|
|
139
|
+
const deadline = Date.now() + SESSION_DISCOVERY_TIMEOUT_MS;
|
|
140
|
+
while (Date.now() < deadline) {
|
|
141
|
+
const candidate = findLatestSessionCandidate(projectRoot, launchStartedAtMs);
|
|
142
|
+
if (candidate) return candidate;
|
|
143
|
+
await sleep(SESSION_DISCOVERY_POLL_MS);
|
|
144
|
+
}
|
|
145
|
+
return findLatestSessionCandidate(projectRoot, launchStartedAtMs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Fallback plan discovery: scan all context plan dirs by mtime. */
|
|
149
|
+
function findLatestPlanByMtime(projectRoot: string): string | null {
|
|
150
|
+
const contextsDir = path.join(projectRoot, "_output", "contexts");
|
|
151
|
+
if (!fs.existsSync(contextsDir)) return null;
|
|
152
|
+
|
|
153
|
+
let best: { path: string; mtime: number } | null = null;
|
|
154
|
+
|
|
155
|
+
for (const ctxEntry of fs.readdirSync(contextsDir)) {
|
|
156
|
+
if (ctxEntry.startsWith("_")) continue;
|
|
157
|
+
const plansDir = path.join(contextsDir, ctxEntry, "plans");
|
|
158
|
+
if (!fs.existsSync(plansDir)) continue;
|
|
159
|
+
|
|
160
|
+
for (const file of fs.readdirSync(plansDir)) {
|
|
161
|
+
if (!file.endsWith(".md")) continue;
|
|
162
|
+
const fullPath = path.join(plansDir, file);
|
|
163
|
+
try {
|
|
164
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
165
|
+
if (!best || mtime > best.mtime) {
|
|
166
|
+
best = { path: fullPath, mtime };
|
|
167
|
+
}
|
|
168
|
+
} catch { /* skip unreadable */ }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return best?.path ?? null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Arg parsing
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const rawArgs = process.argv.slice(2);
|
|
180
|
+
|
|
181
|
+
if (rawArgs.length === 0) {
|
|
182
|
+
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extract --model, --sandbox, and --context flags before mode dispatch
|
|
187
|
+
let modelFlag: string | undefined;
|
|
188
|
+
let sandboxFlag: CodexSandbox | undefined;
|
|
189
|
+
let contextFlag: string | undefined;
|
|
190
|
+
let yolo = true;
|
|
191
|
+
let capture = false;
|
|
192
|
+
const args: string[] = [];
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
195
|
+
if (rawArgs[i] === "--model" && i + 1 < rawArgs.length) {
|
|
196
|
+
modelFlag = rawArgs[++i];
|
|
197
|
+
} else if (rawArgs[i] === "--sandbox" && i + 1 < rawArgs.length) {
|
|
198
|
+
const val = rawArgs[++i];
|
|
199
|
+
if (!isCodexSandbox(val)) {
|
|
200
|
+
eprint(`Error: Invalid sandbox mode "${val}". Valid: read-only, workspace-write, danger-full-access`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
sandboxFlag = val;
|
|
204
|
+
} else if (rawArgs[i] === "--context" && i + 1 < rawArgs.length) {
|
|
205
|
+
contextFlag = rawArgs[++i];
|
|
206
|
+
} else if (rawArgs[i] === "--yolo") {
|
|
207
|
+
yolo = true;
|
|
208
|
+
} else if (rawArgs[i] === "--no-yolo") {
|
|
209
|
+
yolo = false;
|
|
210
|
+
} else if (rawArgs[i] === "--capture") {
|
|
211
|
+
capture = true;
|
|
212
|
+
} else {
|
|
213
|
+
args.push(rawArgs[i]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (args.length === 0) {
|
|
218
|
+
eprint("Usage: launch-codex.ts [--model <model>] [--sandbox <mode>] [--no-yolo] [--capture] [--context <id>] plan | --file <path> | <text...>");
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Resolve model: alias first, then tier/pass-through via shared resolver
|
|
223
|
+
let resolvedModel: string | undefined;
|
|
224
|
+
if (modelFlag) {
|
|
225
|
+
const lower = modelFlag.toLowerCase();
|
|
226
|
+
resolvedModel = lower in CODEX_ALIASES
|
|
227
|
+
? CODEX_ALIASES[lower]
|
|
228
|
+
: resolveCodexModel(modelFlag);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let promptPath: string | null = null;
|
|
232
|
+
let tempFile: string | null = null;
|
|
233
|
+
|
|
234
|
+
const projectRoot = getProjectRoot(process.cwd());
|
|
235
|
+
|
|
236
|
+
// Context lookup — available for all modes (orientation header + plan discovery)
|
|
237
|
+
// --context flag preferred (passed by skill caller); CLAUDE_SESSION_ID as fallback (hooks only)
|
|
238
|
+
let ctx: ContextState | null = null;
|
|
239
|
+
if (contextFlag) {
|
|
240
|
+
ctx = getContext(contextFlag, projectRoot) ?? null;
|
|
241
|
+
} else {
|
|
242
|
+
const sessionId = process.env.CLAUDE_SESSION_ID;
|
|
243
|
+
if (sessionId) {
|
|
244
|
+
ctx = getContextBySessionId(sessionId, projectRoot) ?? null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (args[0] === "plan") {
|
|
249
|
+
// Plan discovery: context system first, mtime fallback second
|
|
250
|
+
let planPath: string | null = null;
|
|
251
|
+
|
|
252
|
+
if (ctx) {
|
|
253
|
+
planPath = findLatestPlan(ctx.id, projectRoot);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!planPath) {
|
|
257
|
+
planPath = findLatestPlanByMtime(projectRoot);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!planPath) {
|
|
261
|
+
eprint("Error: No plan found. Create a plan first (use plan mode), then run this command.");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
promptPath = planPath;
|
|
266
|
+
console.log(`Found plan: ${displayPath(planPath)}`);
|
|
267
|
+
|
|
268
|
+
} else if (args[0] === "--file") {
|
|
269
|
+
if (!args[1]) {
|
|
270
|
+
eprint("Error: --file requires a path argument.");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const filePath = path.resolve(args[1]);
|
|
274
|
+
if (!fs.existsSync(filePath)) {
|
|
275
|
+
eprint(`Error: File not found: ${filePath}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
promptPath = filePath;
|
|
279
|
+
|
|
280
|
+
} else {
|
|
281
|
+
// Inline text: join args, write to temp file
|
|
282
|
+
const text = args.join(" ");
|
|
283
|
+
tempFile = path.join(os.tmpdir(), `codex-prompt-${Date.now()}.md`);
|
|
284
|
+
fs.writeFileSync(tempFile, text, "utf-8");
|
|
285
|
+
promptPath = tempFile;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Prepend context orientation if available — graceful degradation on failure
|
|
289
|
+
if (ctx && promptPath) {
|
|
290
|
+
try {
|
|
291
|
+
const orientation = buildExternalAgentContext(ctx, projectRoot);
|
|
292
|
+
const original = fs.readFileSync(promptPath, "utf-8");
|
|
293
|
+
const combined = `${orientation}\n\n---\n\n${original}`;
|
|
294
|
+
const contextPromptPath = path.join(os.tmpdir(), `codex-ctx-prompt-${Date.now()}.md`);
|
|
295
|
+
fs.writeFileSync(contextPromptPath, combined, "utf-8");
|
|
296
|
+
if (tempFile) {
|
|
297
|
+
try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
|
|
298
|
+
}
|
|
299
|
+
promptPath = contextPromptPath;
|
|
300
|
+
tempFile = contextPromptPath;
|
|
301
|
+
} catch {
|
|
302
|
+
logWarn("codex-skill", `Context orientation prepend failed for ${ctx.id}, continuing without header`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Pre-flight: tmux required
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
const tmux = getTmuxAvailability();
|
|
311
|
+
if (!tmux.available) {
|
|
312
|
+
eprint(`Error: tmux is required for Codex REPL mode. ${tmux.reason ?? ""}`);
|
|
313
|
+
if (tempFile) try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Launch Codex REPL in tmux pane
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
// Build args via centralized CLI builder
|
|
322
|
+
const codexArgs = buildCliInvocation(codexReplSpec(resolvedModel, sandboxFlag, yolo)).args;
|
|
323
|
+
if (yolo) console.log("Mode: YOLO (bypass approvals and sandbox)");
|
|
324
|
+
if (sandboxFlag) console.log(`Sandbox: ${sandboxFlag}`);
|
|
325
|
+
if (resolvedModel) console.log(`Model: ${resolvedModel}${modelFlag !== resolvedModel ? ` (from "${modelFlag}")` : ""}`);
|
|
326
|
+
|
|
327
|
+
logDebug("codex-skill", `Launching: model=${resolvedModel ?? "default"}, sandbox=${sandboxFlag ?? "default"}, yolo=${yolo}, source=${args[0]}, bytes=${promptPath ? fs.statSync(promptPath).size : 0}`);
|
|
328
|
+
const launchStartedAtMs = Date.now();
|
|
329
|
+
|
|
330
|
+
const result = await launchDriverInTmuxOrFallback({
|
|
331
|
+
toolName: "codex",
|
|
332
|
+
mode: "repl",
|
|
333
|
+
args: codexArgs,
|
|
334
|
+
promptPath,
|
|
335
|
+
sendPromptInRepl: true,
|
|
336
|
+
allowExecFallback: false,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Cleanup temp file after injection
|
|
340
|
+
if (tempFile) {
|
|
341
|
+
try { fs.unlinkSync(tempFile); } catch { /* ignore */ }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!result.launched) {
|
|
345
|
+
logWarn("codex-skill", `Launch failed: ${result.reason}`);
|
|
346
|
+
eprint(`Error: Failed to launch Codex. ${result.reason ?? ""}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Log injection diagnostics
|
|
351
|
+
const diag = result.sendDiagnostics;
|
|
352
|
+
if (diag) {
|
|
353
|
+
if (diag.success) {
|
|
354
|
+
logDebug("codex-skill", `Injection OK: promptWait=${diag.promptWaitMs}ms, retrySent=${diag.retrySent}`);
|
|
355
|
+
} else {
|
|
356
|
+
logWarn("codex-skill", `Injection failed at ${diag.failedAt}: wait=${diag.promptWaitMs}ms, stderr=${diag.tmuxStderr ?? "none"}, paneTail=${diag.paneTailOnTimeout ?? "none"}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (result.paneId) {
|
|
361
|
+
console.log(`Codex launched in tmux pane: ${result.paneId}`);
|
|
362
|
+
} else {
|
|
363
|
+
console.log("Codex launched in tmux pane.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (capture && result.paneId) {
|
|
367
|
+
try {
|
|
368
|
+
const sessionInfo = await waitForCaptureSession(projectRoot, launchStartedAtMs);
|
|
369
|
+
if (sessionInfo) {
|
|
370
|
+
console.log(`CODEX_CAPTURE_PANE=${result.paneId}`);
|
|
371
|
+
console.log(`CODEX_CAPTURE_SESSION_ID=${sessionInfo.sessionId}`);
|
|
372
|
+
console.log(`CODEX_CAPTURE_SESSION_FILE=${sessionInfo.sessionFile}`);
|
|
373
|
+
} else {
|
|
374
|
+
logWarn(
|
|
375
|
+
"codex-skill",
|
|
376
|
+
`Capture session discovery failed for pane ${result.paneId} in ${SESSION_DISCOVERY_TIMEOUT_MS}ms`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
logWarn("codex-skill", `Capture session discovery threw for ${result.paneId}: ${String(error)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (result.reason) {
|
|
385
|
+
// Partial success (e.g., launched but prompt injection failed)
|
|
386
|
+
eprint(`Warning: ${result.reason}`);
|
|
387
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { inference } from "../../../lib-ts/base/inference.js";
|
|
8
|
+
import { logDebug, logWarn } from "../../../lib-ts/base/logger.js";
|
|
9
|
+
import { CODEX_MODELS } from "../../../lib-ts/base/models.js";
|
|
10
|
+
import { execFileAsync } from "../../../lib-ts/base/subprocess-utils.js";
|
|
11
|
+
import { getTmuxAvailability } from "../../../lib-ts/base/tmux-driver.js";
|
|
12
|
+
|
|
13
|
+
const POLL_INTERVAL_MS = 2000;
|
|
14
|
+
const POLL_TIMEOUT_MS = 3000;
|
|
15
|
+
const SUMMARY_TIMEOUT_SEC = 8;
|
|
16
|
+
const RESUME_TIMEOUT_MS = 45000;
|
|
17
|
+
const MAX_TRANSCRIPT_LINES = 220;
|
|
18
|
+
const MAX_LINE_LENGTH = 500;
|
|
19
|
+
const SUMMARY_UNAVAILABLE_MESSAGE = "Codex session completed. Summary unavailable.";
|
|
20
|
+
|
|
21
|
+
const TRANSCRIPT_SUMMARY_PROMPT = `Summarize this Codex session transcript excerpt.
|
|
22
|
+
Return 3-5 concise bullet points.
|
|
23
|
+
Focus on:
|
|
24
|
+
- what was accomplished
|
|
25
|
+
- files changed
|
|
26
|
+
- errors or blockers
|
|
27
|
+
Do not ask follow-up questions.
|
|
28
|
+
Do not request additional input.
|
|
29
|
+
If information is partial, provide best-effort summary from available text.`;
|
|
30
|
+
|
|
31
|
+
const RESUME_SUMMARY_PROMPT = `Summarize the previous Codex session in 3-5 concise bullet points.
|
|
32
|
+
Focus on:
|
|
33
|
+
- what was accomplished
|
|
34
|
+
- files changed
|
|
35
|
+
- errors or blockers
|
|
36
|
+
Do not ask follow-up questions.
|
|
37
|
+
Do not request additional input.
|
|
38
|
+
If the prior session was brief, still provide a best-effort summary.`;
|
|
39
|
+
|
|
40
|
+
function sleep(ms: number): Promise<void> {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeCleanup(filePath: string): void {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
47
|
+
} catch {
|
|
48
|
+
// Best-effort cleanup only.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readTextIfExists(filePath: string): string {
|
|
53
|
+
try {
|
|
54
|
+
if (!filePath || !fs.existsSync(filePath)) return "";
|
|
55
|
+
return fs.readFileSync(filePath, "utf-8").trim();
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeText(text: string): string {
|
|
62
|
+
return text
|
|
63
|
+
.replace(/\r/g, "")
|
|
64
|
+
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "")
|
|
65
|
+
.replace(/\s+/g, " ")
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getMessageContentText(content: unknown): string {
|
|
70
|
+
if (!Array.isArray(content)) return "";
|
|
71
|
+
return content
|
|
72
|
+
.map((entry: any) => {
|
|
73
|
+
if (!entry || typeof entry !== "object") return "";
|
|
74
|
+
if (typeof entry.text !== "string") return "";
|
|
75
|
+
return entry.text;
|
|
76
|
+
})
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectTranscriptLines(sessionFile: string): string[] {
|
|
81
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) return [];
|
|
82
|
+
|
|
83
|
+
const out: string[] = [];
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const raw = fs.readFileSync(sessionFile, "utf-8");
|
|
88
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
let parsed: any;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(line);
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let role = "";
|
|
99
|
+
let text = "";
|
|
100
|
+
|
|
101
|
+
if (parsed?.type === "response_item" && parsed?.payload?.type === "message") {
|
|
102
|
+
role = parsed?.payload?.role === "assistant" ? "assistant" : (parsed?.payload?.role === "user" ? "user" : "");
|
|
103
|
+
text = getMessageContentText(parsed?.payload?.content);
|
|
104
|
+
} else if (parsed?.type === "item.completed" && parsed?.item?.type === "agent_message" && typeof parsed?.item?.text === "string") {
|
|
105
|
+
role = "assistant";
|
|
106
|
+
text = parsed.item.text;
|
|
107
|
+
} else if (parsed?.type === "event_msg" && parsed?.payload?.type === "agent_message" && typeof parsed?.payload?.message === "string") {
|
|
108
|
+
role = "assistant";
|
|
109
|
+
text = parsed.payload.message;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalized = normalizeText(text);
|
|
113
|
+
if (!role || !normalized) continue;
|
|
114
|
+
|
|
115
|
+
const truncated = normalized.length > MAX_LINE_LENGTH ? `${normalized.slice(0, MAX_LINE_LENGTH)}...` : normalized;
|
|
116
|
+
const tagged = `${role}: ${truncated}`;
|
|
117
|
+
if (seen.has(tagged)) continue;
|
|
118
|
+
|
|
119
|
+
seen.add(tagged);
|
|
120
|
+
out.push(tagged);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logWarn("codex-capture", `Session transcript parse failed: ${String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return out.slice(-MAX_TRANSCRIPT_LINES);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function looksLikeBadSummary(output: string): boolean {
|
|
130
|
+
const normalized = output.toLowerCase();
|
|
131
|
+
return (
|
|
132
|
+
normalized.includes("don't see") ||
|
|
133
|
+
normalized.includes("no output") ||
|
|
134
|
+
normalized.includes("could you provide") ||
|
|
135
|
+
normalized.includes("paste")
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function waitForPaneClose(paneId: string): Promise<void> {
|
|
140
|
+
const tmux = getTmuxAvailability();
|
|
141
|
+
if (!tmux.available || !tmux.tmuxPath) {
|
|
142
|
+
logWarn("codex-capture", `tmux unavailable while watching pane ${paneId}: ${tmux.reason ?? "unknown reason"}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
while (true) {
|
|
147
|
+
const result = await execFileAsync(tmux.tmuxPath, ["list-panes", "-a", "-F", "#{pane_id}"], {
|
|
148
|
+
timeout: POLL_TIMEOUT_MS,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
logDebug("codex-capture", `list-panes failed; assuming pane closed (${result.stderr.trim() || "no stderr"})`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const activePaneIds = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
157
|
+
if (!activePaneIds.includes(paneId)) return;
|
|
158
|
+
|
|
159
|
+
await sleep(POLL_INTERVAL_MS);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function summarizeViaSessionFileSpark(sessionFile: string): string | null {
|
|
164
|
+
const transcriptLines = collectTranscriptLines(sessionFile);
|
|
165
|
+
if (transcriptLines.length === 0) return null;
|
|
166
|
+
|
|
167
|
+
const transcript = transcriptLines.join("\n");
|
|
168
|
+
const result = inference(
|
|
169
|
+
TRANSCRIPT_SUMMARY_PROMPT,
|
|
170
|
+
`Session transcript excerpt:\n\n${transcript}`,
|
|
171
|
+
"fast",
|
|
172
|
+
SUMMARY_TIMEOUT_SEC,
|
|
173
|
+
{ model: CODEX_MODELS.spark },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (result.success && result.output && result.output.trim() && !looksLikeBadSummary(result.output)) {
|
|
177
|
+
return result.output.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logWarn(
|
|
181
|
+
"codex-capture",
|
|
182
|
+
`Session-file Spark summary failed: ${result.error ?? "empty or low-signal output"}`,
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function summarizeViaResume(sessionId: string): Promise<string | null> {
|
|
188
|
+
const outputFile = path.join(os.tmpdir(), `codex-resume-summary-${Date.now()}-${process.pid}.txt`);
|
|
189
|
+
|
|
190
|
+
const result = await execFileAsync(
|
|
191
|
+
"codex",
|
|
192
|
+
[
|
|
193
|
+
"exec",
|
|
194
|
+
"resume",
|
|
195
|
+
sessionId,
|
|
196
|
+
RESUME_SUMMARY_PROMPT,
|
|
197
|
+
"--json",
|
|
198
|
+
"--model",
|
|
199
|
+
CODEX_MODELS.spark,
|
|
200
|
+
"--output-last-message",
|
|
201
|
+
outputFile,
|
|
202
|
+
],
|
|
203
|
+
{ timeout: RESUME_TIMEOUT_MS },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const summary = readTextIfExists(outputFile);
|
|
207
|
+
safeCleanup(outputFile);
|
|
208
|
+
|
|
209
|
+
if (summary && !looksLikeBadSummary(summary)) return summary;
|
|
210
|
+
logWarn("codex-capture", `codex exec resume failed for ${sessionId}: exit=${result.exitCode}, stderr=${result.stderr.trim() || "none"}`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function summarizeFromSessionFileFallback(sessionFile: string): string | null {
|
|
215
|
+
const lines = collectTranscriptLines(sessionFile).slice(-12);
|
|
216
|
+
if (lines.length === 0) return null;
|
|
217
|
+
return `Codex session completed. Transcript fallback:\n- ${lines.join("\n- ")}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function main(): Promise<void> {
|
|
221
|
+
const [paneId, sessionId, sessionFile] = process.argv.slice(2);
|
|
222
|
+
|
|
223
|
+
if (!paneId) {
|
|
224
|
+
console.log(SUMMARY_UNAVAILABLE_MESSAGE);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await waitForPaneClose(paneId);
|
|
229
|
+
|
|
230
|
+
const transcriptSummary = summarizeViaSessionFileSpark(sessionFile ?? "");
|
|
231
|
+
if (transcriptSummary) {
|
|
232
|
+
console.log(transcriptSummary);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (sessionId) {
|
|
237
|
+
const resumeSummary = await summarizeViaResume(sessionId);
|
|
238
|
+
if (resumeSummary) {
|
|
239
|
+
console.log(resumeSummary);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const fallback = summarizeFromSessionFileFallback(sessionFile ?? "");
|
|
245
|
+
if (fallback) {
|
|
246
|
+
console.log(fallback);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(SUMMARY_UNAVAILABLE_MESSAGE);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch((error) => {
|
|
254
|
+
logWarn("codex-capture", `watch-codex failed: ${String(error)}`);
|
|
255
|
+
console.log(SUMMARY_UNAVAILABLE_MESSAGE);
|
|
256
|
+
process.exit(0);
|
|
257
|
+
});
|