codexteer 0.14.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/LICENSE +21 -0
- package/README.md +519 -0
- package/assets/send-pururu.wav +0 -0
- package/bin/codexteer-wrapper.mjs +10 -0
- package/bin/codexteer.mjs +5 -0
- package/docs/distribution.md +186 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/node_modules/ws/lib/receiver.js +743 -0
- package/node_modules/ws/lib/sender.js +607 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +562 -0
- package/node_modules/ws/lib/websocket.js +1407 -0
- package/node_modules/ws/package.json +74 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/package.json +41 -0
- package/scripts/accessibility.applescript +43 -0
- package/scripts/inspect.applescript +72 -0
- package/scripts/send.applescript +45 -0
- package/src/app-server.mjs +117 -0
- package/src/bridge.mjs +79 -0
- package/src/checkpoint.mjs +128 -0
- package/src/cli.mjs +358 -0
- package/src/compatibility.mjs +115 -0
- package/src/desktop.mjs +207 -0
- package/src/directive.mjs +39 -0
- package/src/distribution.mjs +192 -0
- package/src/evidence.mjs +33 -0
- package/src/help.mjs +145 -0
- package/src/journal.mjs +120 -0
- package/src/launcher.mjs +145 -0
- package/src/monitor.mjs +123 -0
- package/src/observe.mjs +185 -0
- package/src/paged-read.mjs +228 -0
- package/src/prompt.mjs +51 -0
- package/src/resource.mjs +70 -0
- package/src/rpc.mjs +162 -0
- package/src/runner.mjs +32 -0
- package/src/runtime.mjs +130 -0
- package/src/settings.mjs +49 -0
- package/src/sound.mjs +16 -0
- package/src/store.mjs +78 -0
- package/src/subscription.mjs +118 -0
- package/src/supervise.mjs +57 -0
- package/src/thread-id.mjs +35 -0
- package/src/thread-store.mjs +72 -0
- package/src/version.mjs +19 -0
- package/src/wrapper.mjs +149 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { desktopDoctor, inspectDesktopUi, openDesktopThread, sendDesktopMessage } from "./desktop.mjs";
|
|
3
|
+
import { normalizeThreadId, threadDeepLink } from "./thread-id.mjs";
|
|
4
|
+
import { listLocalThreads } from "./thread-store.mjs";
|
|
5
|
+
import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
|
|
6
|
+
import { appServerDoctor, startDesktop } from "./launcher.mjs";
|
|
7
|
+
import { VERSION, helpData, renderHelp } from "./help.mjs";
|
|
8
|
+
import { observeThread, printObservation } from "./observe.mjs";
|
|
9
|
+
import { monitorCommand } from "./monitor.mjs";
|
|
10
|
+
import { createCheckpoint, listCheckpoints, getCheckpoint, checkpointSummary, verifyCheckpoint, runCheckpoint, attachArtifacts } from "./checkpoint.mjs";
|
|
11
|
+
import { acquireResource, resourceStatus, renewResource, releaseResource, listResources, runWithResource } from "./resource.mjs";
|
|
12
|
+
import { playSendSound } from "./sound.mjs";
|
|
13
|
+
import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_SEND_BACKEND = "app-server";
|
|
16
|
+
|
|
17
|
+
function success(command, data, json) {
|
|
18
|
+
if (json) console.log(JSON.stringify({ ok: true, command, data }));
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail(error, json) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
if (json) console.log(JSON.stringify({ ok: false, ...(error.watch ? { command: "watch", data: error.watch } : {}), error: {
|
|
25
|
+
message, code: error.code, delivery_status: error.delivery_status,
|
|
26
|
+
operation: error.operation, capability: error.capability,
|
|
27
|
+
sent: error.sent, thread_id: error.thread_id, rpc_code: error.rpc_code ?? error.rpcCode,
|
|
28
|
+
message_id: error.message_id, client_message_id: error.client_message_id, journal_update_required: error.journal_update_required,
|
|
29
|
+
} }));
|
|
30
|
+
else console.error(`codexteer: ${message}`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function takeOption(args, name, defaultValue) {
|
|
35
|
+
const index = args.indexOf(name);
|
|
36
|
+
if (index === -1 || (args.includes("--") && index > args.indexOf("--"))) return defaultValue;
|
|
37
|
+
const value = args[index + 1];
|
|
38
|
+
if (value == null || value.startsWith("--")) throw new Error(`${name} requires a value.`);
|
|
39
|
+
args.splice(index, 2);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function takeFlag(args, name) {
|
|
44
|
+
const index = args.indexOf(name);
|
|
45
|
+
if (index === -1 || (args.includes("--") && index > args.indexOf("--"))) return false;
|
|
46
|
+
args.splice(index, 1);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function takeOptions(args, name) {
|
|
51
|
+
const values = []; let value;
|
|
52
|
+
while ((value = takeOption(args, name, undefined)) !== undefined) values.push(value);
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readMessage(parts) {
|
|
57
|
+
if (parts[0] === "--") parts.shift();
|
|
58
|
+
if (parts.length === 1 && parts[0] === "-") return readFileSync(0, "utf8");
|
|
59
|
+
return parts.join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function backendOption(args, fallback) {
|
|
63
|
+
const backend = takeOption(args, "--backend", fallback);
|
|
64
|
+
if (backend != null && !["app-server", "ui"].includes(backend)) throw new Error("--backend must be app-server or ui.");
|
|
65
|
+
return backend;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printThreads(threads) {
|
|
69
|
+
if (threads.length === 0) {
|
|
70
|
+
console.log("No local Codex threads found.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const thread of threads) {
|
|
74
|
+
const location = thread.cwd ? ` ${thread.cwd}` : "";
|
|
75
|
+
console.log(`${thread.thread_id} ${thread.updated_at}${location}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function main(argv) {
|
|
80
|
+
const args = [...argv];
|
|
81
|
+
let json = takeFlag(args, "--json");
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const requiredNode = takeOption(args, "--require-node-version", undefined);
|
|
85
|
+
if (requiredNode !== undefined) {
|
|
86
|
+
if (!/^v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(requiredNode)) throw new Error("--require-node-version requires a Node version such as v25.1.0.");
|
|
87
|
+
if (requiredNode !== process.version) throw Object.assign(new Error(`This supervision command requires Node ${requiredNode}, but is running ${process.version}. Regenerate the supervisor prompt with the intended Node runtime.`), { code: "NODE_VERSION_MISMATCH" });
|
|
88
|
+
}
|
|
89
|
+
if (takeFlag(args, "--version")) {
|
|
90
|
+
if (json) success("version", { version: VERSION }, true);
|
|
91
|
+
else console.log(VERSION);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (args.length === 0 || takeFlag(args, "--help") || args[0] === "help") {
|
|
95
|
+
if (args[0] === "help") args.shift();
|
|
96
|
+
const candidate = args[0] ?? "overview";
|
|
97
|
+
const topic = candidate === "supervise" && args[1] === "prompt" ? "supervise prompt"
|
|
98
|
+
: /^(codex:\/\/|[0-9a-f]{8}-)/i.test(candidate) ? "send" : candidate;
|
|
99
|
+
const data = helpData(topic);
|
|
100
|
+
success("help", data, json);
|
|
101
|
+
if (!json) console.log(renderHelp(data));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let command = args.shift();
|
|
106
|
+
if (command === "supervise" && args[0] === "prompt") {
|
|
107
|
+
args.shift();
|
|
108
|
+
if (args.length !== 1) throw new Error("Expected: supervise prompt <THREAD>. See codexteer help supervise prompt.");
|
|
109
|
+
const data = await prepareSupervisorPrompt(args[0]);
|
|
110
|
+
success("supervise.prompt", data, json);
|
|
111
|
+
if (!json) console.log(data.prompt);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (command === "supervise") {
|
|
115
|
+
if (json) throw new Error("supervise inherits agent output and does not support --json. Use help supervise --json or supervise prompt <THREAD> --json.");
|
|
116
|
+
const separator = args.indexOf("--");
|
|
117
|
+
const ownArgs = separator < 0 ? args : args.slice(0, separator);
|
|
118
|
+
const agentArgs = separator < 0 ? [] : args.slice(separator + 1);
|
|
119
|
+
const agent = takeOption(ownArgs, "--agent", undefined);
|
|
120
|
+
if (ownArgs.length !== 1 || !agent) throw new Error("Expected: supervise <THREAD> --agent claude [-- <AGENT-ARGS...>].");
|
|
121
|
+
process.exitCode = await superviseAgent(ownArgs[0], { agent, agentArgs });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (command === "resource") {
|
|
125
|
+
const action = args.shift(); let data;
|
|
126
|
+
if (["acquire", "run"].includes(action)) {
|
|
127
|
+
const options = { owner: takeOption(args, "--owner", undefined), ttlMs: Number(takeOption(args, "--ttl-ms", "600000")), reason: takeOption(args, "--reason", ""), condition: takeOption(args, "--condition", ""), threadId: takeOption(args, "--thread", undefined) };
|
|
128
|
+
if (action === "run") {
|
|
129
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0")), includeOutput = takeFlag(args, "--include-output");
|
|
130
|
+
if (args[1] !== "--" || args.length < 3) throw new Error("Expected: resource run <NAME> --owner NAME -- <COMMAND> [ARGS...]");
|
|
131
|
+
data = await runWithResource(args[0], args.slice(2), { ...options, timeoutMs });
|
|
132
|
+
if (!includeOutput) delete data.output_tail;
|
|
133
|
+
if (!data.valid) process.exitCode = 1;
|
|
134
|
+
} else {
|
|
135
|
+
if (args.length !== 1) throw new Error("Expected: resource acquire <NAME> --owner NAME.");
|
|
136
|
+
data = await acquireResource(args[0], options);
|
|
137
|
+
}
|
|
138
|
+
} else if (["renew", "release"].includes(action)) {
|
|
139
|
+
const token = takeOption(args, "--token", undefined), ttlMs = action === "renew" ? Number(takeOption(args, "--ttl-ms", "600000")) : undefined;
|
|
140
|
+
if (args.length !== 1) throw new Error(`Expected: resource ${action} <NAME> --token TOKEN.`);
|
|
141
|
+
data = action === "renew" ? await renewResource(args[0], token, { ttlMs }) : await releaseResource(args[0], token);
|
|
142
|
+
} else if (action === "status" && args.length === 1) data = await resourceStatus(args[0]);
|
|
143
|
+
else if (action === "list" && args.length === 0) data = await listResources();
|
|
144
|
+
else throw new Error("See codexteer help resource.");
|
|
145
|
+
success(`resource.${action}`, data, json);
|
|
146
|
+
if (!json) console.log(JSON.stringify(data, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (command === "checkpoint") {
|
|
150
|
+
const action = args.shift(), includeOutput = takeFlag(args, "--include-output");
|
|
151
|
+
let data;
|
|
152
|
+
if (action === "capture") {
|
|
153
|
+
const paths = takeOptions(args, "--path"), excludes = takeOptions(args, "--exclude");
|
|
154
|
+
if (args.length !== 2) throw new Error("Expected: checkpoint capture <THREAD> <NAME> --path PATH ...");
|
|
155
|
+
data = await createCheckpoint(args[0], args[1], { paths, excludes });
|
|
156
|
+
} else if (action === "run") {
|
|
157
|
+
const timeoutMs = Number(takeOption(args, "--timeout-ms", "0"));
|
|
158
|
+
if (args[2] !== "--" || args.length < 4) throw new Error("Expected: checkpoint run <THREAD> <ID> -- <COMMAND> [ARGS...]");
|
|
159
|
+
data = await runCheckpoint(args[0], args[1], args.slice(3), { timeoutMs });
|
|
160
|
+
if (!includeOutput) delete data.output_tail;
|
|
161
|
+
if (!data.valid) process.exitCode = 1;
|
|
162
|
+
} else if (action === "attach") {
|
|
163
|
+
const refs = takeOptions(args, "--artifact");
|
|
164
|
+
if (args.length !== 2) throw new Error("Expected: checkpoint attach <THREAD> <ID> --artifact FILE ...");
|
|
165
|
+
data = await attachArtifacts(args[0], args[1], refs);
|
|
166
|
+
} else if (action === "list") {
|
|
167
|
+
if (args.length !== 1) throw new Error("Expected: checkpoint list <THREAD>.");
|
|
168
|
+
data = await listCheckpoints(args[0]);
|
|
169
|
+
} else if (["show", "verify"].includes(action)) {
|
|
170
|
+
if (args.length !== 2) throw new Error(`Expected: checkpoint ${action} <THREAD> <ID>.`);
|
|
171
|
+
data = action === "verify" ? await verifyCheckpoint(args[0], args[1]) : checkpointSummary(await getCheckpoint(args[0], args[1]), includeOutput);
|
|
172
|
+
if (action === "verify" && !data.valid) process.exitCode = 1;
|
|
173
|
+
} else throw new Error("See codexteer help checkpoint.");
|
|
174
|
+
success(`checkpoint.${action}`, data, json);
|
|
175
|
+
if (!json) console.log(JSON.stringify(data, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (command === "instructions") {
|
|
179
|
+
const action = args.shift();
|
|
180
|
+
if (action === "list") {
|
|
181
|
+
const all = takeFlag(args, "--all");
|
|
182
|
+
if (args.length !== 1) throw new Error("Expected: instructions list <THREAD> [--all].");
|
|
183
|
+
const data = await listInstructions(args[0], { all });
|
|
184
|
+
success("instructions.list", data, json);
|
|
185
|
+
if (!json) { for (const entry of data.instructions) console.log(`${entry.id} ${entry.instruction_status} ${entry.metadata?.source ?? "unspecified"}\n${entry.body}`); if (data.pending_changes.length) console.log(`配送未確認: ${data.pending_changes.map(e => e.id).join(", ")}`); }
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (action !== "retract") throw new Error("See codexteer help instructions.");
|
|
189
|
+
const reason = takeOption(args, "--reason", undefined), source = takeOption(args, "--source", undefined), basedOn = takeOption(args, "--based-on", undefined);
|
|
190
|
+
const dryRun = takeFlag(args, "--dry-run"), newTurn = takeFlag(args, "--new-turn");
|
|
191
|
+
if (args.length !== 2 || !reason?.trim()) throw new Error("Expected: instructions retract <THREAD> <MESSAGE-ID> --reason TEXT.");
|
|
192
|
+
const data = await sendTrackedMessage(args[0], reason, { retracts: args[1], source, basedOn, dryRun, newTurn });
|
|
193
|
+
success("instructions.retract", data, json);
|
|
194
|
+
if (!json) console.log(dryRun ? "撤回の送信プレビューです。" : `撤回を受け付けました。message_id: ${data.message_id}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (command === "history") {
|
|
198
|
+
const action = args.shift();
|
|
199
|
+
const includeText = takeFlag(args, "--include-text");
|
|
200
|
+
const pending = takeFlag(args, "--pending");
|
|
201
|
+
const report = action === "mark" ? { status: takeOption(args, "--status", undefined), note: takeOption(args, "--note", undefined), evidence: takeOptions(args, "--evidence"), by: takeOption(args, "--by", "local") } : {};
|
|
202
|
+
const [threadId, id] = args;
|
|
203
|
+
if (!threadId || args.length > 2 || !["list", "show", "check", "mark"].includes(action) || (["show", "mark"].includes(action) && !id) || (action === "list" && id)) throw new Error("See codexteer help history.");
|
|
204
|
+
const data = action === "list" ? await listMessages(threadId, { includeText, pending }) : action === "show" ? messageSummary(await getMessage(threadId, id), includeText) : action === "check" ? await reconcileMessages(threadId, id, { includeText }) : await markMessage(threadId, id, report);
|
|
205
|
+
success(`history.${action}`, data, json);
|
|
206
|
+
if (!json) for (const entry of Array.isArray(data) ? data : [data]) console.log(`${entry.id} ${entry.delivery_status} history:${entry.verification?.status ?? "unchecked"} response:${entry.response.status}${entry.body ? "\n" + entry.body : ""}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (["read", "status", "watch"].includes(command)) {
|
|
210
|
+
const fullHistory = takeFlag(args, "--full-history");
|
|
211
|
+
const options = command === "status" ? { fullHistory } : {
|
|
212
|
+
fullHistory,
|
|
213
|
+
since: takeOption(args, "--since", undefined),
|
|
214
|
+
limit: Number(takeOption(args, "--limit", "50")),
|
|
215
|
+
maxChars: Number(takeOption(args, "--max-chars", "2000")),
|
|
216
|
+
includeOutput: takeFlag(args, "--include-output"),
|
|
217
|
+
};
|
|
218
|
+
const stream = command === "watch" && takeFlag(args, "--stream");
|
|
219
|
+
if (stream) json = true;
|
|
220
|
+
if (command === "watch") {
|
|
221
|
+
const until = takeOption(args, "--until", undefined), timeout = takeOption(args, "--timeout-ms", undefined);
|
|
222
|
+
if (stream && (until !== undefined || timeout !== undefined)) throw new Error("--stream runs until cancelled; do not combine it with --until or --timeout-ms. See codexteer help monitor.");
|
|
223
|
+
Object.assign(options, { watch: true, until: until ?? "change", timeoutMs: Number(timeout ?? "30000"), pollMs: Number(takeOption(args, "--poll-ms", "1000")) });
|
|
224
|
+
}
|
|
225
|
+
if (args.length !== 1) throw new Error(`Expected: ${command} <THREAD>. See codexteer help ${command}.`);
|
|
226
|
+
if (stream) { await monitorCommand(args[0], options); return; }
|
|
227
|
+
const data = await observeThread(args[0], options);
|
|
228
|
+
if (command === "status") delete data.events;
|
|
229
|
+
success(command, data, json);
|
|
230
|
+
if (!json) printObservation(data, command === "status");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (command === "doctor") {
|
|
234
|
+
const backend = backendOption(args, "app-server");
|
|
235
|
+
const target = takeOption(args, "--thread", undefined);
|
|
236
|
+
if (target !== undefined && backend !== "app-server") throw new Error("doctor --thread requires --backend app-server.");
|
|
237
|
+
const threadId = target === undefined ? undefined : normalizeThreadId(target);
|
|
238
|
+
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
239
|
+
const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId });
|
|
240
|
+
report.default_send_backend = DEFAULT_SEND_BACKEND;
|
|
241
|
+
report.rollout_status = "enabled";
|
|
242
|
+
success("doctor", report, json);
|
|
243
|
+
if (!json) {
|
|
244
|
+
console.log(`${report.backend}: ${report.ready ? "ready" : "not ready"}`);
|
|
245
|
+
for (const [name, ok] of Object.entries(report.checks)) {
|
|
246
|
+
console.log(` ${ok ? "ok" : "missing"} ${name}`);
|
|
247
|
+
}
|
|
248
|
+
if (report.compatibility) {
|
|
249
|
+
console.log(` connection: ${report.connection.status}`);
|
|
250
|
+
console.log(` Desktop: ${report.desktop_version ?? "unknown"}, CLI: ${report.running_cli_version ?? "unknown"}, wrapper Node: ${report.running_wrapper_node_version ?? "unknown"}, local Node: ${report.cli_node_version}`);
|
|
251
|
+
console.log(` codexteer: ${report.codex_steer_compatibility.status} (local ${report.codex_steer_version}, running ${report.codex_steer_compatibility.runtime.version ?? "unknown"})`);
|
|
252
|
+
console.log(` runtime protocol: ${report.runtime_compatibility.protocol.status}`);
|
|
253
|
+
for (const [operation, contract] of Object.entries(report.runtime_compatibility.operations)) console.log(` ${operation}: ${contract.status}${contract.failure ? ` (${contract.failure})` : ""}`);
|
|
254
|
+
console.log(` observation compatibility: ${report.compatibility.status} (${report.compatibility.scope})`);
|
|
255
|
+
for (const [method, status] of Object.entries(report.compatibility.api_checks)) console.log(` ${method}: ${status}`);
|
|
256
|
+
if (target === undefined) console.log(" Verify a target: codexteer doctor --thread <THREAD> --json");
|
|
257
|
+
if (report.failure) console.log(` failure: ${report.failure.code}${report.failure.rpc_code === null ? "" : ` (RPC ${report.failure.rpc_code})`}`);
|
|
258
|
+
}
|
|
259
|
+
if (report.remediation) console.log(`\n${report.remediation}`);
|
|
260
|
+
}
|
|
261
|
+
if (!report.ready) process.exitCode = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === "desktop") {
|
|
266
|
+
if (args.shift() !== "start") throw new Error("Expected: codexteer desktop start");
|
|
267
|
+
const dryRun = takeFlag(args, "--dry-run");
|
|
268
|
+
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
269
|
+
const data = await startDesktop({ dryRun });
|
|
270
|
+
success("desktop.start", data, json);
|
|
271
|
+
if (!json) console.log(dryRun ? "Dry run: would launch Desktop with the shared App Server wrapper." : "Desktop shared App Server is ready.");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (command === "threads") {
|
|
276
|
+
if (args.shift() !== "list") throw new Error("Expected: codexteer threads list");
|
|
277
|
+
const limit = Number.parseInt(takeOption(args, "--limit", "20"), 10);
|
|
278
|
+
const desktopOnly = takeFlag(args, "--desktop-only");
|
|
279
|
+
if (args.length > 0) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
280
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
281
|
+
throw new Error("--limit must be an integer between 1 and 500.");
|
|
282
|
+
}
|
|
283
|
+
const threads = await listLocalThreads({ limit, desktopOnly });
|
|
284
|
+
success("threads.list", { threads, count: threads.length }, json);
|
|
285
|
+
if (!json) printThreads(threads);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (command === "thread") {
|
|
290
|
+
if (args.shift() !== "resolve") throw new Error("Expected: codexteer thread resolve <THREAD>");
|
|
291
|
+
if (args.length !== 1) throw new Error("thread resolve requires exactly one thread ID or URL.");
|
|
292
|
+
const threadId = normalizeThreadId(args[0]);
|
|
293
|
+
const data = { thread_id: threadId, deep_link: threadDeepLink(threadId) };
|
|
294
|
+
success("thread.resolve", data, json);
|
|
295
|
+
if (!json) console.log(threadId);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (command === "open") {
|
|
300
|
+
if (args.length !== 1) throw new Error("open requires exactly one thread ID or URL.");
|
|
301
|
+
const threadId = normalizeThreadId(args[0]);
|
|
302
|
+
const data = openDesktopThread(threadId);
|
|
303
|
+
success("open", data, json);
|
|
304
|
+
if (!json) console.log(`Opened ${data.deep_link}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (command === "debug-ui") {
|
|
309
|
+
const waitMs = Number.parseInt(takeOption(args, "--wait-ms", "1500"), 10);
|
|
310
|
+
if (args.length !== 1) throw new Error("debug-ui requires exactly one thread ID or URL.");
|
|
311
|
+
const threadId = normalizeThreadId(args[0]);
|
|
312
|
+
const data = inspectDesktopUi(threadId, { waitMs });
|
|
313
|
+
success("debug-ui", data, json);
|
|
314
|
+
if (!json) console.log(data.diagnostic);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (command !== "send") {
|
|
319
|
+
args.unshift(command);
|
|
320
|
+
command = "send";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const dryRun = takeFlag(args, "--dry-run");
|
|
324
|
+
const backend = backendOption(args, DEFAULT_SEND_BACKEND);
|
|
325
|
+
const explicitSound = takeFlag(args, "--sound");
|
|
326
|
+
const noSound = takeFlag(args, "--no-sound");
|
|
327
|
+
if (explicitSound && noSound) throw new Error("--sound and --no-sound cannot be combined.");
|
|
328
|
+
if (explicitSound && backend !== "app-server") throw new Error("--sound requires --backend app-server so receipt can be confirmed.");
|
|
329
|
+
const newTurn = takeFlag(args, "--new-turn");
|
|
330
|
+
const directive = { source: takeOption(args, "--source", undefined), kind: takeOption(args, "--kind", undefined), evidence: takeOptions(args, "--evidence"), basedOn: takeOption(args, "--based-on", undefined), supersedes: takeOption(args, "--supersedes", undefined), expiresAt: takeOption(args, "--expires-at", undefined), checkpoint: takeOption(args, "--checkpoint", undefined) };
|
|
331
|
+
if (backend === "ui" && (directive.source || directive.kind || directive.evidence.length || directive.basedOn || directive.supersedes || directive.expiresAt || directive.checkpoint)) throw new Error("Directive metadata requires --backend app-server.");
|
|
332
|
+
const keepFocus = takeFlag(args, "--keep-focus");
|
|
333
|
+
const waitInput = takeOption(args, "--wait-ms", undefined);
|
|
334
|
+
if (backend !== "ui" && (keepFocus || waitInput != null)) throw new Error("--keep-focus and --wait-ms require --backend ui.");
|
|
335
|
+
const waitMs = Number(waitInput ?? "1500");
|
|
336
|
+
const threadInput = args.shift();
|
|
337
|
+
if (!threadInput) throw new Error("send requires a thread ID or codex://threads URL.");
|
|
338
|
+
const message = readMessage(args);
|
|
339
|
+
if (message.trim() === "") throw new Error("send requires a non-empty message.");
|
|
340
|
+
const threadId = normalizeThreadId(threadInput);
|
|
341
|
+
const data = backend === "app-server"
|
|
342
|
+
? await sendTrackedMessage(threadId, message, { dryRun, newTurn, ...directive })
|
|
343
|
+
: sendDesktopMessage(threadId, message, { dryRun, waitMs, newTurn, keepFocus });
|
|
344
|
+
data.sound = noSound ? { played: false, reason: "disabled" } : playSendSound(data);
|
|
345
|
+
success("send", data, json);
|
|
346
|
+
if (!json) {
|
|
347
|
+
console.log(dryRun
|
|
348
|
+
? `Dry run: would ${newTurn ? "start a new turn in" : "steer"} ${threadId} (${data.message_characters} characters)`
|
|
349
|
+
: backend === "ui"
|
|
350
|
+
? `Submitted through Desktop UI for ${threadId}; delivery is unverified.`
|
|
351
|
+
: `App Server accepted input for ${threadId} (turn ${data.turn_id}).`);
|
|
352
|
+
if (data.message_id) console.log(`message_id: ${data.message_id}`);
|
|
353
|
+
if (!noSound && !dryRun && data.sent === true && !data.sound.played) console.error(`codexteer: Input was accepted, but the sound could not be played (${data.sound.reason}).`);
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
fail(error, json);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Versions here describe wire contracts, not package releases. Additive changes
|
|
2
|
+
// keep their contract version; incompatible changes get a new feature version.
|
|
3
|
+
export const RUNTIME_PROTOCOL = 1;
|
|
4
|
+
export const RUNTIME_CAPABILITIES = Object.freeze(Object.fromEntries(
|
|
5
|
+
["thread_read", "history_pagination", "turn_steer", "turn_start", "desktop_subscribe"].map(name => [name, Object.freeze([1])]),
|
|
6
|
+
));
|
|
7
|
+
|
|
8
|
+
const operations = {
|
|
9
|
+
read: ["thread_read"], watch: ["thread_read"], monitor: ["thread_read"], history_check: ["thread_read"],
|
|
10
|
+
send: ["thread_read", "turn_steer"], send_new_turn: ["thread_read", "turn_start", "desktop_subscribe"],
|
|
11
|
+
};
|
|
12
|
+
const readOperations = new Set(["read", "watch", "monitor", "history_check"]);
|
|
13
|
+
const methods = {
|
|
14
|
+
"thread/read": "thread_read", "thread/turns/list": "history_pagination", "thread/items/list": "history_pagination",
|
|
15
|
+
"turn/steer": "turn_steer", "turn/start": "turn_start",
|
|
16
|
+
};
|
|
17
|
+
const readMethods = new Set(["thread/read", "thread/turns/list", "thread/items/list", "thread/loaded/list"]);
|
|
18
|
+
const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
const wireVersion = value => Number.isSafeInteger(value) && value > 0;
|
|
20
|
+
|
|
21
|
+
function legacySource(state) {
|
|
22
|
+
// v0.13 advertised one aggregate v1 contract. Explicit capability declarations
|
|
23
|
+
// always take precedence, including empty/invalid ones; never fall back around them.
|
|
24
|
+
if (state?.codex_steer_capabilities !== undefined) return null;
|
|
25
|
+
if (state?.codex_steer_protocol === 1) return "legacy-v1";
|
|
26
|
+
// Pre-version wrapper from 83af51d, verified with this bundled CLI by the
|
|
27
|
+
// protocol fixture. Do not infer mutation support for arbitrary older servers.
|
|
28
|
+
if (state?.codex_steer_protocol === undefined && state?.codex_steer_package === undefined
|
|
29
|
+
&& state?.codex_steer_version === undefined && state?.schema === 1 && state?.cli_version === "0.153.4"
|
|
30
|
+
&& state?.node_path === "/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node") return "legacy-0.153.4";
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runtimeCompatibility(state, { verifiedMethods = [], unsupportedMethods = [] } = {}) {
|
|
35
|
+
const legacy = legacySource(state), declared = state?.codex_steer_protocol;
|
|
36
|
+
const protocol = {
|
|
37
|
+
status: wireVersion(declared) ? declared === RUNTIME_PROTOCOL ? "supported" : "unsupported" : legacy ? "supported" : "unverified",
|
|
38
|
+
client: RUNTIME_PROTOCOL, runtime: wireVersion(declared) ? declared : legacy ? 1 : null,
|
|
39
|
+
source: declared !== undefined ? wireVersion(declared) ? "declared" : "invalid" : legacy ?? "probe",
|
|
40
|
+
};
|
|
41
|
+
if (protocol.source === "probe") {
|
|
42
|
+
if (unsupportedMethods.includes("initialize")) {
|
|
43
|
+
protocol.status = "unsupported"; protocol.source = "probe-unsupported";
|
|
44
|
+
} else if (verifiedMethods.includes("initialize")) {
|
|
45
|
+
protocol.status = "supported"; protocol.runtime = 1; protocol.source = "probe-verified";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const declaredFeatures = state?.codex_steer_capabilities;
|
|
49
|
+
const features = Object.fromEntries(Object.entries(RUNTIME_CAPABILITIES).map(([name, supported]) => {
|
|
50
|
+
let versions = null, source = "probe", status = "unverified";
|
|
51
|
+
if (legacy) { versions = [1]; source = legacy; status = "supported"; }
|
|
52
|
+
else if (declaredFeatures !== undefined) {
|
|
53
|
+
source = "invalid";
|
|
54
|
+
if (record(declaredFeatures)) {
|
|
55
|
+
if (!Object.hasOwn(declaredFeatures, name)) { versions = []; source = "declared"; status = "unsupported"; }
|
|
56
|
+
else {
|
|
57
|
+
const value = declaredFeatures[name];
|
|
58
|
+
if (Array.isArray(value) && value.length <= 16 && value.every(wireVersion)) {
|
|
59
|
+
versions = [...new Set(value)]; source = "declared";
|
|
60
|
+
status = versions.some(version => supported.includes(version)) ? "supported" : "unsupported";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const probes = name === "thread_read" ? ["thread/read"] : name === "history_pagination" ? ["thread/turns/list", "thread/items/list"] : [];
|
|
66
|
+
if (source === "probe" && probes.length) {
|
|
67
|
+
// One missing required API disproves support even if another probe succeeded.
|
|
68
|
+
if (probes.some(method => unsupportedMethods.includes(method))) {
|
|
69
|
+
status = "unsupported"; source = "probe-unsupported";
|
|
70
|
+
} else if (probes.every(method => verifiedMethods.includes(method))) {
|
|
71
|
+
status = "supported"; versions = [1]; source = "probe-verified";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [name, { status, client: [...supported], runtime: versions, source }];
|
|
75
|
+
}));
|
|
76
|
+
const result = { protocol, features };
|
|
77
|
+
result.operations = Object.fromEntries(Object.entries(operations).map(([name, required]) => {
|
|
78
|
+
const entries = [protocol, ...required.map(feature => features[feature])];
|
|
79
|
+
return [name, { status: entries.some(x => x.status === "unsupported") ? "unsupported"
|
|
80
|
+
: entries.some(x => x.status !== "supported") ? "unverified" : "supported", requires: required }];
|
|
81
|
+
}));
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertContract(contract, { operation, capability, allowProbe }) {
|
|
86
|
+
if (contract.status === "supported" || (allowProbe && contract.source === "probe")) return;
|
|
87
|
+
const prefix = capability === "protocol" ? "RUNTIME_PROTOCOL" : "CAPABILITY";
|
|
88
|
+
const suffix = contract.status === "unsupported" ? "UNSUPPORTED" : "UNVERIFIED";
|
|
89
|
+
throw Object.assign(new Error(`${operation} requires a compatible ${capability} contract. Check codexteer doctor --json for supported operations; use a compatible CLI or update the wrapper after finishing current tasks.`), {
|
|
90
|
+
code: `${prefix}_${suffix}`, operation, capability,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function assertRuntimeOperation(state, operation) {
|
|
95
|
+
if (!Object.hasOwn(operations, operation)) throw new Error("Unknown runtime operation.");
|
|
96
|
+
const report = runtimeCompatibility(state), allowProbe = readOperations.has(operation);
|
|
97
|
+
assertContract(report.protocol, { operation, capability: "protocol", allowProbe });
|
|
98
|
+
for (const capability of operations[operation]) assertContract(report.features[capability], { operation, capability, allowProbe });
|
|
99
|
+
return report;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Guard optional read paths when they are actually used (pagination/freshness),
|
|
103
|
+
// rather than making a new API a requirement of every observation or send.
|
|
104
|
+
export function compatibleClient(client, state) {
|
|
105
|
+
const report = runtimeCompatibility(state);
|
|
106
|
+
return {
|
|
107
|
+
close: () => client.close(),
|
|
108
|
+
request(method, ...args) {
|
|
109
|
+
const allowProbe = readMethods.has(method), capability = methods[method];
|
|
110
|
+
assertContract(report.protocol, { operation: method, capability: "protocol", allowProbe });
|
|
111
|
+
if (capability) assertContract(report.features[capability], { operation: method, capability, allowProbe });
|
|
112
|
+
return client.request(method, ...args);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|