nexting-cc-bridge 0.8.3
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 +252 -0
- package/dist/attach-manager.js +259 -0
- package/dist/bridge.js +931 -0
- package/dist/cli-args.js +14 -0
- package/dist/cli.js +742 -0
- package/dist/codex-prompts.js +148 -0
- package/dist/codex-thread-source.js +495 -0
- package/dist/codex-transcript.js +415 -0
- package/dist/dev-server.js +126 -0
- package/dist/discovery.js +111 -0
- package/dist/e2e/codec.js +119 -0
- package/dist/e2e/crypto.js +127 -0
- package/dist/e2e/key-store.js +48 -0
- package/dist/e2e/keychain-identity.js +29 -0
- package/dist/engine/adapter.js +5 -0
- package/dist/engine/claude-adapter.js +77 -0
- package/dist/engine/codex-adapter.js +593 -0
- package/dist/file-preview.js +292 -0
- package/dist/hub-protocol.js +28 -0
- package/dist/hub-server.js +106 -0
- package/dist/hub.js +84 -0
- package/dist/install-util.js +33 -0
- package/dist/local-shell.js +32 -0
- package/dist/mcp-config.js +230 -0
- package/dist/mcp-device-proxy.js +501 -0
- package/dist/media-hydrator.js +222 -0
- package/dist/message-counter.js +79 -0
- package/dist/phone-probe.js +55 -0
- package/dist/prompt-detector.js +213 -0
- package/dist/protocol.js +3 -0
- package/dist/pty-mirror.js +80 -0
- package/dist/pty-spawn.js +53 -0
- package/dist/scanner.js +422 -0
- package/dist/self-update.js +122 -0
- package/dist/session-map.js +15 -0
- package/dist/session-runner.js +131 -0
- package/dist/shell.js +104 -0
- package/dist/skills-scanner.js +167 -0
- package/dist/stdin-encode.js +32 -0
- package/dist/stream-translate.js +122 -0
- package/dist/terminal-render.js +29 -0
- package/dist/transcript-watcher.js +138 -0
- package/dist/transcript.js +346 -0
- package/dist/turn-probe.js +152 -0
- package/dist/types.js +2 -0
- package/dist/watch-manager.js +77 -0
- package/install-cc.sh +90 -0
- package/install-codex.sh +97 -0
- package/package.json +39 -0
- package/shim/claude +55 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
// Verified against codex 0.138.0 app-server protocol (generate-json-schema).
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { expandCodexPrompt } from "../codex-prompts.js";
|
|
6
|
+
/** Classify a parsed JSON-RPC "lite" object (no jsonrpc field). */
|
|
7
|
+
export function classifyRpc(o) {
|
|
8
|
+
if (!o || typeof o !== "object")
|
|
9
|
+
return "unknown";
|
|
10
|
+
const r = o;
|
|
11
|
+
if ("id" in r && ("result" in r || "error" in r))
|
|
12
|
+
return "response";
|
|
13
|
+
if ("id" in r && "method" in r)
|
|
14
|
+
return "server_request";
|
|
15
|
+
if ("method" in r && !("id" in r))
|
|
16
|
+
return "notification";
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Binary resolver
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Resolve the `codex` binary by absolute path (launchd PATH is minimal). */
|
|
23
|
+
export function resolveCodexBin() {
|
|
24
|
+
const home = os.homedir();
|
|
25
|
+
const candidates = [
|
|
26
|
+
process.env.NEXTING_CODEX_BIN,
|
|
27
|
+
path.join(home, ".npm-global/bin/codex"),
|
|
28
|
+
path.join(home, ".local/bin/codex"),
|
|
29
|
+
"/opt/homebrew/bin/codex",
|
|
30
|
+
"/usr/local/bin/codex",
|
|
31
|
+
].filter((p) => !!p);
|
|
32
|
+
for (const c of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(c))
|
|
35
|
+
return c;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* keep looking */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return "codex";
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Adapter factory
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/** TUI substrings the Codex CLI prints while a turn is running (best-effort —
|
|
47
|
+
* casing has varied across codex versions; the probe matches case-insensitively,
|
|
48
|
+
* so these collapse, but both observed spellings are kept for documentation). */
|
|
49
|
+
export const CODEX_RUNNING_MARKERS = ["esc to interrupt", "Esc to interrupt"];
|
|
50
|
+
/** Per-session stateful adapter for `codex app-server` JSON-RPC over stdio. */
|
|
51
|
+
export function createCodexAdapter() {
|
|
52
|
+
let threadId = null;
|
|
53
|
+
let resumeId;
|
|
54
|
+
let nextId = 0;
|
|
55
|
+
let nextQueueId = 0;
|
|
56
|
+
let eventSink = null;
|
|
57
|
+
// Fix 1: guard against re-firing handshake on any future response with userAgent
|
|
58
|
+
let handshakeDone = false;
|
|
59
|
+
// Fix 2: buffer user turns sent before threadId is known
|
|
60
|
+
let io = null;
|
|
61
|
+
const pendingTurns = [];
|
|
62
|
+
// True once a turn/start has been sent and until Codex completes that turn.
|
|
63
|
+
// This includes the short window before the app-server emits turn/started.
|
|
64
|
+
let turnBusy = false;
|
|
65
|
+
let activeTurnId = null;
|
|
66
|
+
const queuedFollowUps = [];
|
|
67
|
+
// Codex-owned "current activity" latch that drives the phone's state bar
|
|
68
|
+
// (`activity_status` → iOS `CodexSessionModel.activityState`). We emit a frame
|
|
69
|
+
// ONLY when the phase transitions, so a burst of same-phase events (e.g. many
|
|
70
|
+
// text deltas) never spams identical frames. iOS falls back to transcript-tail
|
|
71
|
+
// inference only when no activity_status frame has arrived.
|
|
72
|
+
let activityPhase = null;
|
|
73
|
+
function id() {
|
|
74
|
+
return nextId++;
|
|
75
|
+
}
|
|
76
|
+
function line(obj) {
|
|
77
|
+
return JSON.stringify(obj) + "\n";
|
|
78
|
+
}
|
|
79
|
+
function queueItem(text) {
|
|
80
|
+
nextQueueId += 1;
|
|
81
|
+
return {
|
|
82
|
+
id: `q${nextQueueId}`,
|
|
83
|
+
text,
|
|
84
|
+
createdAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function emitQueueStatus() {
|
|
88
|
+
eventSink?.({
|
|
89
|
+
type: "event",
|
|
90
|
+
kind: "queue_status",
|
|
91
|
+
payload: {
|
|
92
|
+
items: queuedFollowUps.map((item, index) => ({
|
|
93
|
+
id: item.id,
|
|
94
|
+
text: item.text,
|
|
95
|
+
createdAt: item.createdAt,
|
|
96
|
+
position: index,
|
|
97
|
+
})),
|
|
98
|
+
activeTurnId,
|
|
99
|
+
canSteer: activeTurnId != null,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Fix 2: shared helper so encodeUserTurn and the flush path produce identical lines
|
|
104
|
+
function buildTurnLine(content) {
|
|
105
|
+
return line({
|
|
106
|
+
id: id(),
|
|
107
|
+
method: "turn/start",
|
|
108
|
+
params: {
|
|
109
|
+
threadId,
|
|
110
|
+
input: [{ type: "text", text: content }],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function buildSteerLine(content) {
|
|
115
|
+
return line({
|
|
116
|
+
id: id(),
|
|
117
|
+
method: "turn/steer",
|
|
118
|
+
params: {
|
|
119
|
+
threadId,
|
|
120
|
+
expectedTurnId: activeTurnId,
|
|
121
|
+
input: [{ type: "text", text: content }],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function buildBusyTurnLine(content) {
|
|
126
|
+
turnBusy = true;
|
|
127
|
+
return buildTurnLine(content);
|
|
128
|
+
}
|
|
129
|
+
function writeBusyTurn(content) {
|
|
130
|
+
if (!io)
|
|
131
|
+
return;
|
|
132
|
+
io.write(buildBusyTurnLine(content));
|
|
133
|
+
}
|
|
134
|
+
function drainOneQueuedFollowUp() {
|
|
135
|
+
if (!io || threadId === null || queuedFollowUps.length === 0)
|
|
136
|
+
return false;
|
|
137
|
+
const next = queuedFollowUps.shift();
|
|
138
|
+
if (next == null)
|
|
139
|
+
return false;
|
|
140
|
+
emitQueueStatus();
|
|
141
|
+
writeBusyTurn(next.text);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function itemText(item) {
|
|
145
|
+
const content = item.content;
|
|
146
|
+
if (typeof content === "string")
|
|
147
|
+
return content;
|
|
148
|
+
if (!Array.isArray(content))
|
|
149
|
+
return "";
|
|
150
|
+
return content
|
|
151
|
+
.map((block) => {
|
|
152
|
+
if (typeof block === "string")
|
|
153
|
+
return block;
|
|
154
|
+
if (!block || typeof block !== "object")
|
|
155
|
+
return "";
|
|
156
|
+
const b = block;
|
|
157
|
+
return typeof b.text === "string" ? b.text : "";
|
|
158
|
+
})
|
|
159
|
+
.join("");
|
|
160
|
+
}
|
|
161
|
+
function isoFromMs(value) {
|
|
162
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
163
|
+
? new Date(value).toISOString()
|
|
164
|
+
: undefined;
|
|
165
|
+
}
|
|
166
|
+
function fileChangePayload(item) {
|
|
167
|
+
return {
|
|
168
|
+
id: item.id,
|
|
169
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
170
|
+
status: item.status,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Emit an `activity_status` frame iff the phase changed since the last one.
|
|
174
|
+
* `extra` carries phase-specific context (search `summary`, edit `count`). */
|
|
175
|
+
function activityFrame(phase, extra = {}) {
|
|
176
|
+
if (activityPhase === phase)
|
|
177
|
+
return [];
|
|
178
|
+
activityPhase = phase;
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
type: "event",
|
|
182
|
+
kind: "activity_status",
|
|
183
|
+
payload: { phase, active: phase !== "idle", ...extra },
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
/** Map a commandExecution's actions to an activity phase + summary. Codex's
|
|
188
|
+
* exploring commands (search/read/listFiles) drive the "searching" phase; a
|
|
189
|
+
* search action also surfaces its query as the bar summary. */
|
|
190
|
+
function commandActivity(item) {
|
|
191
|
+
const actions = Array.isArray(item.commandActions)
|
|
192
|
+
? item.commandActions
|
|
193
|
+
: [];
|
|
194
|
+
const search = actions.find((a) => a && a.type === "search");
|
|
195
|
+
const exploring = search != null ||
|
|
196
|
+
actions.some((a) => a && (a.type === "read" || a.type === "listFiles"));
|
|
197
|
+
if (!exploring)
|
|
198
|
+
return activityFrame("working");
|
|
199
|
+
const summary = typeof search?.query === "string" ? search.query : undefined;
|
|
200
|
+
return activityFrame("searching", summary ? { summary } : {});
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
runningMarkers: CODEX_RUNNING_MARKERS,
|
|
204
|
+
setEventSink(sink) {
|
|
205
|
+
eventSink = sink;
|
|
206
|
+
},
|
|
207
|
+
editQueuedTurn(itemId, text) {
|
|
208
|
+
const trimmed = text.trim();
|
|
209
|
+
if (!trimmed)
|
|
210
|
+
return false;
|
|
211
|
+
const item = queuedFollowUps.find((q) => q.id === itemId);
|
|
212
|
+
if (!item)
|
|
213
|
+
return false;
|
|
214
|
+
item.text = trimmed;
|
|
215
|
+
emitQueueStatus();
|
|
216
|
+
return true;
|
|
217
|
+
},
|
|
218
|
+
deleteQueuedTurn(itemId) {
|
|
219
|
+
const index = queuedFollowUps.findIndex((q) => q.id === itemId);
|
|
220
|
+
if (index < 0)
|
|
221
|
+
return false;
|
|
222
|
+
queuedFollowUps.splice(index, 1);
|
|
223
|
+
emitQueueStatus();
|
|
224
|
+
return true;
|
|
225
|
+
},
|
|
226
|
+
closeQueue() {
|
|
227
|
+
if (queuedFollowUps.length === 0)
|
|
228
|
+
return false;
|
|
229
|
+
queuedFollowUps.length = 0;
|
|
230
|
+
emitQueueStatus();
|
|
231
|
+
return true;
|
|
232
|
+
},
|
|
233
|
+
steerQueuedTurn(itemId) {
|
|
234
|
+
if (threadId === null || activeTurnId === null) {
|
|
235
|
+
emitQueueStatus();
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
const index = queuedFollowUps.findIndex((q) => q.id === itemId);
|
|
239
|
+
if (index < 0) {
|
|
240
|
+
emitQueueStatus();
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
const [item] = queuedFollowUps.splice(index, 1);
|
|
244
|
+
emitQueueStatus();
|
|
245
|
+
return buildSteerLine(item.text);
|
|
246
|
+
},
|
|
247
|
+
command(_resumeSessionId, _ctx) {
|
|
248
|
+
// codex app-server is always the same command; resuming is done via
|
|
249
|
+
// thread/resume inside the JSON-RPC handshake, not via CLI flags. The
|
|
250
|
+
// device-tool MCP server is configured out-of-band in ~/.codex/config.toml
|
|
251
|
+
// (written by mcp-config.ts before spawn), so mcpConfigPath is unused here.
|
|
252
|
+
return { bin: resolveCodexBin(), args: ["app-server"] };
|
|
253
|
+
},
|
|
254
|
+
start(ioArg, resumeSessionId) {
|
|
255
|
+
io = ioArg;
|
|
256
|
+
resumeId = resumeSessionId;
|
|
257
|
+
// If resuming, set threadId immediately so encodeUserTurn works even
|
|
258
|
+
// before thread/started arrives.
|
|
259
|
+
if (resumeSessionId) {
|
|
260
|
+
threadId = resumeSessionId;
|
|
261
|
+
}
|
|
262
|
+
// Send the initialize request — the handshake continues from translate()
|
|
263
|
+
// once the server responds, keeping ordering guarantees.
|
|
264
|
+
io.write(line({
|
|
265
|
+
id: id(),
|
|
266
|
+
method: "initialize",
|
|
267
|
+
params: {
|
|
268
|
+
clientInfo: {
|
|
269
|
+
name: "pinclaw-codex-bridge",
|
|
270
|
+
title: "Nexting",
|
|
271
|
+
version: "0.1.0",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
}));
|
|
275
|
+
},
|
|
276
|
+
translate(parsed, ioArg) {
|
|
277
|
+
const kind = classifyRpc(parsed);
|
|
278
|
+
const o = parsed;
|
|
279
|
+
// ---- terminal engine errors (surface, never drop) ----
|
|
280
|
+
// Codex reports request failures in two shapes that would otherwise be
|
|
281
|
+
// swallowed: an error RESPONSE ({id, error}) AND — the one that bit us — a
|
|
282
|
+
// TOP-LEVEL id-less {error} frame (e.g. "no rollout found for thread id …"
|
|
283
|
+
// when resuming a thread whose rollout was never written). classifyRpc tags
|
|
284
|
+
// the latter "unknown", so without this it is dropped at the function tail
|
|
285
|
+
// → the phone spins "working" forever with no feedback. Surface both as a
|
|
286
|
+
// result_error so the UI clears and shows a real message. (Retryable stream
|
|
287
|
+
// hiccups use the {method:"error",params.willRetry} NOTIFICATION path below;
|
|
288
|
+
// those carry no top-level `error`, so kind==="notification" is excluded.)
|
|
289
|
+
if (kind !== "notification") {
|
|
290
|
+
const errObj = o.error;
|
|
291
|
+
if (errObj && typeof errObj === "object") {
|
|
292
|
+
const msg = typeof errObj.message === "string"
|
|
293
|
+
? errObj.message
|
|
294
|
+
: "Codex 引擎返回错误";
|
|
295
|
+
const detail = typeof errObj.additionalDetails === "string"
|
|
296
|
+
? errObj.additionalDetails
|
|
297
|
+
: undefined;
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
type: "event",
|
|
301
|
+
kind: "result_error",
|
|
302
|
+
payload: { errors: [detail ? `${msg}: ${detail}` : msg] },
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ---- initialize RESPONSE ----
|
|
308
|
+
if (kind === "response") {
|
|
309
|
+
const result = o.result;
|
|
310
|
+
// Fix 1: only fire the handshake continuation once, even if a future
|
|
311
|
+
// response happens to carry a userAgent field.
|
|
312
|
+
if (result && typeof result.userAgent === "string" && !handshakeDone) {
|
|
313
|
+
handshakeDone = true;
|
|
314
|
+
// Handshake step 3: send "initialized" notification
|
|
315
|
+
ioArg.write(line({ method: "initialized" }));
|
|
316
|
+
// Handshake step 4: start or resume the thread
|
|
317
|
+
if (resumeId) {
|
|
318
|
+
ioArg.write(line({
|
|
319
|
+
id: id(),
|
|
320
|
+
method: "thread/resume",
|
|
321
|
+
params: {
|
|
322
|
+
threadId: resumeId,
|
|
323
|
+
approvalPolicy: "never",
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
ioArg.write(line({
|
|
329
|
+
id: id(),
|
|
330
|
+
method: "thread/start",
|
|
331
|
+
params: {
|
|
332
|
+
approvalPolicy: "never",
|
|
333
|
+
},
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
// Fix 3: clear resumeId so a defensive second start() can't reuse stale state
|
|
337
|
+
resumeId = undefined;
|
|
338
|
+
}
|
|
339
|
+
// All other responses (e.g. turn/start ACK) are ignored.
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
// ---- notifications ----
|
|
343
|
+
if (kind === "notification") {
|
|
344
|
+
const method = o.method;
|
|
345
|
+
const params = (o.params ?? {});
|
|
346
|
+
switch (method) {
|
|
347
|
+
case "thread/started": {
|
|
348
|
+
const thread = params.thread;
|
|
349
|
+
threadId = thread.id;
|
|
350
|
+
// Fix 2: flush the first turn that was enqueued before threadId
|
|
351
|
+
// arrived. Any extra phone sends become Codex follow-ups, not
|
|
352
|
+
// concurrent turn/start requests.
|
|
353
|
+
const firstPending = pendingTurns.shift();
|
|
354
|
+
if (firstPending != null)
|
|
355
|
+
writeBusyTurn(firstPending);
|
|
356
|
+
queuedFollowUps.push(...pendingTurns.map(queueItem));
|
|
357
|
+
pendingTurns.length = 0;
|
|
358
|
+
if (queuedFollowUps.length > 0)
|
|
359
|
+
emitQueueStatus();
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
type: "event",
|
|
363
|
+
kind: "system_init",
|
|
364
|
+
payload: {
|
|
365
|
+
session_id: thread.id,
|
|
366
|
+
cwd: thread.cwd,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
case "turn/started": {
|
|
372
|
+
turnBusy = true;
|
|
373
|
+
const turn = params.turn;
|
|
374
|
+
activeTurnId = typeof turn?.id === "string" ? turn.id : null;
|
|
375
|
+
emitQueueStatus();
|
|
376
|
+
return [
|
|
377
|
+
...activityFrame("working"),
|
|
378
|
+
{ type: "event", kind: "status", payload: { state: "running" } },
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
case "turn/completed": {
|
|
382
|
+
activeTurnId = null;
|
|
383
|
+
const drained = drainOneQueuedFollowUp();
|
|
384
|
+
if (!drained)
|
|
385
|
+
turnBusy = false;
|
|
386
|
+
emitQueueStatus();
|
|
387
|
+
// status idle first (legacy), then the activity bar clears.
|
|
388
|
+
return [
|
|
389
|
+
{ type: "event", kind: "status", payload: { state: "idle" } },
|
|
390
|
+
...activityFrame("idle"),
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
case "item/agentMessage/delta": {
|
|
394
|
+
const delta = params.delta;
|
|
395
|
+
return [
|
|
396
|
+
...activityFrame("writing"),
|
|
397
|
+
{
|
|
398
|
+
type: "event",
|
|
399
|
+
kind: "stream_text_delta",
|
|
400
|
+
payload: { index: 0, text: delta },
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
case "item/reasoning/textDelta":
|
|
405
|
+
case "item/reasoning/summaryTextDelta": {
|
|
406
|
+
const delta = params.delta;
|
|
407
|
+
return [
|
|
408
|
+
...activityFrame("thinking"),
|
|
409
|
+
{
|
|
410
|
+
type: "event",
|
|
411
|
+
kind: "stream_thinking_delta",
|
|
412
|
+
payload: { index: 0, text: delta },
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
case "item/completed": {
|
|
417
|
+
const item = params.item;
|
|
418
|
+
if (item.type === "userMessage") {
|
|
419
|
+
const text = itemText(item);
|
|
420
|
+
if (!text)
|
|
421
|
+
return [];
|
|
422
|
+
return [
|
|
423
|
+
{
|
|
424
|
+
type: "event",
|
|
425
|
+
kind: "user",
|
|
426
|
+
payload: {
|
|
427
|
+
uuid: item.id,
|
|
428
|
+
text,
|
|
429
|
+
timestamp: isoFromMs(params.completedAtMs),
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
if (item.type === "agentMessage") {
|
|
435
|
+
return [
|
|
436
|
+
{
|
|
437
|
+
type: "event",
|
|
438
|
+
kind: "assistant",
|
|
439
|
+
payload: {
|
|
440
|
+
uuid: item.id,
|
|
441
|
+
index: 0,
|
|
442
|
+
text: item.text,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
if (item.type === "commandExecution") {
|
|
448
|
+
const exitCode = item.exitCode;
|
|
449
|
+
return [
|
|
450
|
+
{
|
|
451
|
+
type: "event",
|
|
452
|
+
kind: "tool_result",
|
|
453
|
+
payload: {
|
|
454
|
+
tool_use_id: item.id,
|
|
455
|
+
content: item.aggregatedOutput,
|
|
456
|
+
is_error: exitCode !== 0 && exitCode != null,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
if (item.type === "fileChange") {
|
|
462
|
+
return [
|
|
463
|
+
{
|
|
464
|
+
type: "event",
|
|
465
|
+
kind: "file_change",
|
|
466
|
+
payload: fileChangePayload(item),
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
case "item/started": {
|
|
473
|
+
const item = params.item;
|
|
474
|
+
if (item.type === "commandExecution") {
|
|
475
|
+
const commandActions = Array.isArray(item.commandActions)
|
|
476
|
+
? item.commandActions
|
|
477
|
+
: [];
|
|
478
|
+
return [
|
|
479
|
+
...commandActivity(item),
|
|
480
|
+
{
|
|
481
|
+
type: "event",
|
|
482
|
+
kind: "tool_use",
|
|
483
|
+
payload: {
|
|
484
|
+
id: item.id,
|
|
485
|
+
name: "shell",
|
|
486
|
+
input: { command: item.command },
|
|
487
|
+
commandActions,
|
|
488
|
+
commandSource: item.source,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
if (item.type === "fileChange") {
|
|
494
|
+
const changes = Array.isArray(item.changes) ? item.changes : [];
|
|
495
|
+
return [
|
|
496
|
+
...activityFrame("editing", { count: changes.length }),
|
|
497
|
+
{
|
|
498
|
+
type: "event",
|
|
499
|
+
kind: "file_change",
|
|
500
|
+
payload: fileChangePayload(item),
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
case "item/fileChange/patchUpdated": {
|
|
507
|
+
const changes = Array.isArray(params.changes) ? params.changes : [];
|
|
508
|
+
return [
|
|
509
|
+
...activityFrame("editing", { count: changes.length }),
|
|
510
|
+
{
|
|
511
|
+
type: "event",
|
|
512
|
+
kind: "file_change",
|
|
513
|
+
payload: {
|
|
514
|
+
id: params.itemId,
|
|
515
|
+
changes,
|
|
516
|
+
status: "inProgress",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
case "error": {
|
|
522
|
+
const willRetry = params.willRetry;
|
|
523
|
+
if (willRetry)
|
|
524
|
+
return []; // engine will retry; don't disrupt the phone
|
|
525
|
+
const err = params.error;
|
|
526
|
+
const msg = err.message;
|
|
527
|
+
const detail = err.additionalDetails;
|
|
528
|
+
const full = detail ? `${msg}: ${detail}` : msg;
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
type: "event",
|
|
532
|
+
kind: "result_error",
|
|
533
|
+
payload: { errors: [full] },
|
|
534
|
+
},
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
// MCP server startup status: not surfaced to the phone, but logged to
|
|
538
|
+
// stderr so a failed pinclaw-device proxy launch is debuggable in the
|
|
539
|
+
// codex-bridge.err.log (stdout is the JSON-RPC channel — never log there).
|
|
540
|
+
case "mcpServer/startupStatus/updated": {
|
|
541
|
+
try {
|
|
542
|
+
process.stderr.write(`[codex-adapter] mcpServer/startupStatus: ${JSON.stringify(params)}\n`);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
/* logging must never break translate */
|
|
546
|
+
}
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
// Ignored notifications
|
|
550
|
+
case "remoteControl/status/changed":
|
|
551
|
+
case "thread/status/changed":
|
|
552
|
+
case "warning":
|
|
553
|
+
default:
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// server_request and unknown: ignore
|
|
558
|
+
return [];
|
|
559
|
+
},
|
|
560
|
+
encodeUserTurn(content) {
|
|
561
|
+
// Custom prompts ("/name args") are a TUI feature the app-server does
|
|
562
|
+
// NOT expand — substitute the prompt body here so phone sends behave
|
|
563
|
+
// like the Codex terminal. Non-matching text passes through untouched.
|
|
564
|
+
content = expandCodexPrompt(content);
|
|
565
|
+
// Fix 2: if threadId is not yet known, buffer the turn and return an
|
|
566
|
+
// empty string (a harmless noop write to stdin); it will be flushed
|
|
567
|
+
// in the thread/started handler.
|
|
568
|
+
if (threadId === null) {
|
|
569
|
+
pendingTurns.push(content);
|
|
570
|
+
return "";
|
|
571
|
+
}
|
|
572
|
+
// Codex App does not accept a second same-thread turn/start while a turn
|
|
573
|
+
// is active. Match the desktop app's end-of-turn follow-up queue instead
|
|
574
|
+
// of leaking Claude Code's immediate-send semantics into Codex.
|
|
575
|
+
if (turnBusy) {
|
|
576
|
+
queuedFollowUps.push(queueItem(content));
|
|
577
|
+
emitQueueStatus();
|
|
578
|
+
return "";
|
|
579
|
+
}
|
|
580
|
+
return buildBusyTurnLine(content);
|
|
581
|
+
},
|
|
582
|
+
encodeAnswer(_controlRequestId, _updatedInput) {
|
|
583
|
+
// v1 runs app-server with approvalPolicy:"never", so approval server_requests never arrive.
|
|
584
|
+
// If approvalPolicy ever changes, implement real tool-approval encoding here instead of this noop.
|
|
585
|
+
return line({ method: "noop" });
|
|
586
|
+
},
|
|
587
|
+
encodeDeny(_controlRequestId, _message) {
|
|
588
|
+
// v1 runs app-server with approvalPolicy:"never", so approval server_requests never arrive.
|
|
589
|
+
// If approvalPolicy ever changes, implement real tool-approval encoding here instead of this noop.
|
|
590
|
+
return line({ method: "noop" });
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|