@ccpocket/bridge 1.50.0 → 1.52.2
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/dist/auto-rename.d.ts +8 -2
- package/dist/auto-rename.js +16 -4
- package/dist/auto-rename.js.map +1 -1
- package/dist/codex-process.d.ts +1 -0
- package/dist/codex-process.js +9 -0
- package/dist/codex-process.js.map +1 -1
- package/dist/git-operations.d.ts +17 -0
- package/dist/git-operations.js +76 -0
- package/dist/git-operations.js.map +1 -1
- package/dist/parser.d.ts +25 -0
- package/dist/parser.js +9 -0
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +11 -1
- package/dist/session.js.map +1 -1
- package/dist/sessions-index.d.ts +1 -0
- package/dist/sessions-index.js +43 -5
- package/dist/sessions-index.js.map +1 -1
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +272 -5
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -7,13 +7,13 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { WebSocketServer, WebSocket } from "ws";
|
|
8
8
|
import { SessionManager, } from "./session.js";
|
|
9
9
|
import { SdkProcess } from "./sdk-process.js";
|
|
10
|
-
import { CodexProcess } from "./codex-process.js";
|
|
10
|
+
import { CodexProcess, } from "./codex-process.js";
|
|
11
11
|
import { parseClientMessage, } from "./parser.js";
|
|
12
|
-
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
12
|
+
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
13
13
|
import { ArchiveStore } from "./archive-store.js";
|
|
14
14
|
import { WorktreeStore } from "./worktree-store.js";
|
|
15
15
|
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
|
|
16
|
-
import { stageFiles, stageHunks, unstageFiles, unstageHunks, gitCommit, gitPush, listGitFiles, listBranches, createBranch, checkoutBranch, revertFiles, revertHunks, gitFetch, gitPull, gitRemoteStatus, } from "./git-operations.js";
|
|
16
|
+
import { stageFiles, stageHunks, unstageFiles, unstageHunks, gitCommit, gitPush, listGitFiles, listBranches, createBranch, checkoutBranch, revertFiles, revertHunks, gitFetch, gitPull, gitRemoteStatus, gitStatus, } from "./git-operations.js";
|
|
17
17
|
import { generateCommitMessage } from "./git-assist.js";
|
|
18
18
|
import { listWindows, takeScreenshot } from "./screenshot.js";
|
|
19
19
|
import { DebugTraceStore } from "./debug-trace-store.js";
|
|
@@ -38,10 +38,50 @@ const CODEX_MODELS = [
|
|
|
38
38
|
"gpt-5.3-codex",
|
|
39
39
|
"gpt-5.3-codex-spark",
|
|
40
40
|
];
|
|
41
|
+
const CODEX_USER_TURN_UUID_RE = /^codex:user-turn:(\d+)$/;
|
|
41
42
|
const OPT_IN_SERVER_MESSAGES = new Set([
|
|
42
43
|
"conversation_queue",
|
|
43
44
|
"prompt_history_status",
|
|
44
45
|
]);
|
|
46
|
+
function parseCodexUserTurnOrdinal(uuid) {
|
|
47
|
+
if (!uuid)
|
|
48
|
+
return null;
|
|
49
|
+
const match = uuid.match(CODEX_USER_TURN_UUID_RE);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
const ordinal = Number(match[1]);
|
|
53
|
+
return Number.isInteger(ordinal) && ordinal > 0 ? ordinal : null;
|
|
54
|
+
}
|
|
55
|
+
function countCodexUserTurnsInSession(session) {
|
|
56
|
+
let count = 0;
|
|
57
|
+
let maxOrdinal = 0;
|
|
58
|
+
const observe = (uuid) => {
|
|
59
|
+
count += 1;
|
|
60
|
+
const ordinal = parseCodexUserTurnOrdinal(uuid);
|
|
61
|
+
if (ordinal !== null) {
|
|
62
|
+
maxOrdinal = Math.max(maxOrdinal, ordinal);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (Array.isArray(session.pastMessages)) {
|
|
66
|
+
for (const message of session.pastMessages) {
|
|
67
|
+
if (!message || typeof message !== "object")
|
|
68
|
+
continue;
|
|
69
|
+
const item = message;
|
|
70
|
+
if (item.role === "user" && item.isMeta !== true) {
|
|
71
|
+
observe(typeof item.uuid === "string" ? item.uuid : undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const message of session.history) {
|
|
76
|
+
if (message.type === "user_input") {
|
|
77
|
+
observe(message.userMessageUuid);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Math.max(count, maxOrdinal);
|
|
81
|
+
}
|
|
82
|
+
function nextCodexUserTurnUuid(session) {
|
|
83
|
+
return codexUserTurnUuid(countCodexUserTurnsInSession(session) + 1);
|
|
84
|
+
}
|
|
45
85
|
// ---- Codex mode mapping helpers ----
|
|
46
86
|
/** Map unified PermissionMode to Codex approval_policy.
|
|
47
87
|
* Only "bypassPermissions" maps to "never"; all others use "on-request". */
|
|
@@ -394,6 +434,118 @@ export class BridgeWebSocketServer {
|
|
|
394
434
|
}
|
|
395
435
|
return msg;
|
|
396
436
|
}
|
|
437
|
+
async rewindCodexConversation(ws, sessionId, targetUuid, mode) {
|
|
438
|
+
if (mode !== "conversation") {
|
|
439
|
+
this.send(ws, {
|
|
440
|
+
type: "rewind_result",
|
|
441
|
+
success: false,
|
|
442
|
+
mode,
|
|
443
|
+
error: "Codex only supports conversation rewind",
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const session = this.sessionManager.get(sessionId);
|
|
448
|
+
if (!session) {
|
|
449
|
+
this.send(ws, {
|
|
450
|
+
type: "rewind_result",
|
|
451
|
+
success: false,
|
|
452
|
+
mode,
|
|
453
|
+
error: `Session ${sessionId} not found`,
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const codexProcess = session.process;
|
|
458
|
+
if (session.provider !== "codex" ||
|
|
459
|
+
typeof codexProcess.rollbackThread !== "function") {
|
|
460
|
+
this.send(ws, {
|
|
461
|
+
type: "rewind_result",
|
|
462
|
+
success: false,
|
|
463
|
+
mode,
|
|
464
|
+
error: "Session is not a Codex session",
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (session.status !== "idle" || (codexProcess.status ?? session.status) !== "idle") {
|
|
469
|
+
this.send(ws, {
|
|
470
|
+
type: "rewind_result",
|
|
471
|
+
success: false,
|
|
472
|
+
mode,
|
|
473
|
+
error: "Cannot rewind while Codex is running",
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (session.codexQueuedInput) {
|
|
478
|
+
this.send(ws, {
|
|
479
|
+
type: "rewind_result",
|
|
480
|
+
success: false,
|
|
481
|
+
mode,
|
|
482
|
+
error: "Cannot rewind while Codex has queued input",
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const targetOrdinal = parseCodexUserTurnOrdinal(targetUuid);
|
|
487
|
+
const totalUserTurns = countCodexUserTurnsInSession(session);
|
|
488
|
+
if (targetOrdinal === null || targetOrdinal > totalUserTurns) {
|
|
489
|
+
this.send(ws, {
|
|
490
|
+
type: "rewind_result",
|
|
491
|
+
success: false,
|
|
492
|
+
mode,
|
|
493
|
+
error: "Invalid Codex rewind target",
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const numTurns = totalUserTurns - targetOrdinal;
|
|
498
|
+
if (numTurns <= 0) {
|
|
499
|
+
this.send(ws, {
|
|
500
|
+
type: "rewind_result",
|
|
501
|
+
success: false,
|
|
502
|
+
mode,
|
|
503
|
+
error: "No newer turns to rewind",
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const threadId = codexProcess.sessionId ?? session.claudeSessionId;
|
|
508
|
+
if (!threadId) {
|
|
509
|
+
this.send(ws, {
|
|
510
|
+
type: "rewind_result",
|
|
511
|
+
success: false,
|
|
512
|
+
mode,
|
|
513
|
+
error: "No Codex thread ID available for rewind",
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const projectPath = session.projectPath;
|
|
518
|
+
const codexSettings = session.codexSettings;
|
|
519
|
+
const worktreeOpts = session.worktreePath
|
|
520
|
+
? {
|
|
521
|
+
existingWorktreePath: session.worktreePath,
|
|
522
|
+
worktreeBranch: session.worktreeBranch,
|
|
523
|
+
}
|
|
524
|
+
: undefined;
|
|
525
|
+
await codexProcess.rollbackThread(numTurns);
|
|
526
|
+
const pastMessages = await getCodexSessionHistory(threadId);
|
|
527
|
+
this.sessionManager.destroy(sessionId);
|
|
528
|
+
const newSessionId = this.sessionManager.create(projectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
529
|
+
...(codexSettings ?? {}),
|
|
530
|
+
threadId,
|
|
531
|
+
});
|
|
532
|
+
const newSession = this.sessionManager.get(newSessionId);
|
|
533
|
+
this.send(ws, {
|
|
534
|
+
type: "rewind_result",
|
|
535
|
+
success: true,
|
|
536
|
+
mode,
|
|
537
|
+
});
|
|
538
|
+
this.send(ws, this.buildSessionCreatedMessage({
|
|
539
|
+
sessionId: newSessionId,
|
|
540
|
+
provider: "codex",
|
|
541
|
+
projectPath,
|
|
542
|
+
session: newSession,
|
|
543
|
+
approvalsReviewer: codexSettings?.approvalsReviewer,
|
|
544
|
+
sandboxMode: codexSettings?.sandboxMode,
|
|
545
|
+
sourceSessionId: sessionId,
|
|
546
|
+
}));
|
|
547
|
+
this.sendSessionList(ws);
|
|
548
|
+
}
|
|
397
549
|
sendTip(ws, sessionId, tipCode, session) {
|
|
398
550
|
const tipMsg = {
|
|
399
551
|
type: "system",
|
|
@@ -810,6 +962,7 @@ export class BridgeWebSocketServer {
|
|
|
810
962
|
itemId: randomUUID(),
|
|
811
963
|
text,
|
|
812
964
|
createdAt: new Date().toISOString(),
|
|
965
|
+
userMessageUuid: nextCodexUserTurnUuid(session),
|
|
813
966
|
...(images.length > 0 ? { imageCount: images.length, images } : {}),
|
|
814
967
|
...(imageRefs ? { imageRefs } : {}),
|
|
815
968
|
...(codexSkills.length > 0 ? { skills: codexSkills } : {}),
|
|
@@ -846,6 +999,9 @@ export class BridgeWebSocketServer {
|
|
|
846
999
|
const userEntry = this.sessionManager.appendHistory(session.id, {
|
|
847
1000
|
type: "user_input",
|
|
848
1001
|
text,
|
|
1002
|
+
...(session.provider === "codex"
|
|
1003
|
+
? { userMessageUuid: nextCodexUserTurnUuid(session) }
|
|
1004
|
+
: {}),
|
|
849
1005
|
...(clientMessageId ? { clientMessageId } : {}),
|
|
850
1006
|
timestamp: new Date().toISOString(),
|
|
851
1007
|
...(images.length > 0 ? { imageCount: images.length } : {}),
|
|
@@ -2304,11 +2460,41 @@ export class BridgeWebSocketServer {
|
|
|
2304
2460
|
});
|
|
2305
2461
|
return;
|
|
2306
2462
|
}
|
|
2463
|
+
const resolvedFileStat = fileStat.isSymbolicLink()
|
|
2464
|
+
? await stat(absPath)
|
|
2465
|
+
: fileStat;
|
|
2466
|
+
const ext = extname(absPath).toLowerCase();
|
|
2467
|
+
if (BridgeWebSocketServer.FILE_PEEK_IMAGE_EXTENSIONS.has(ext)) {
|
|
2468
|
+
const mimeType = BridgeWebSocketServer.mimeTypeForExt(ext);
|
|
2469
|
+
if (resolvedFileStat.size > BridgeWebSocketServer.MAX_IMAGE_SIZE) {
|
|
2470
|
+
this.send(ws, {
|
|
2471
|
+
type: "file_content",
|
|
2472
|
+
filePath: msg.filePath,
|
|
2473
|
+
kind: "image",
|
|
2474
|
+
content: "",
|
|
2475
|
+
mimeType,
|
|
2476
|
+
sizeBytes: resolvedFileStat.size,
|
|
2477
|
+
error: "Image too large to preview. Maximum size is 5 MB.",
|
|
2478
|
+
});
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const buf = await readFile(absPath);
|
|
2482
|
+
this.send(ws, {
|
|
2483
|
+
type: "file_content",
|
|
2484
|
+
filePath: msg.filePath,
|
|
2485
|
+
kind: "image",
|
|
2486
|
+
content: "",
|
|
2487
|
+
base64: buf.toString("base64"),
|
|
2488
|
+
mimeType,
|
|
2489
|
+
sizeBytes: buf.length,
|
|
2490
|
+
});
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2307
2493
|
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
2308
2494
|
? msg.maxLines
|
|
2309
2495
|
: 5000;
|
|
2310
2496
|
const raw = await readFile(absPath, "utf-8");
|
|
2311
|
-
const
|
|
2497
|
+
const textExt = ext.replace(/^\./, "").toLowerCase();
|
|
2312
2498
|
const languageMap = {
|
|
2313
2499
|
ts: "typescript",
|
|
2314
2500
|
tsx: "typescript",
|
|
@@ -2343,7 +2529,7 @@ export class BridgeWebSocketServer {
|
|
|
2343
2529
|
makefile: "makefile",
|
|
2344
2530
|
gradle: "groovy",
|
|
2345
2531
|
};
|
|
2346
|
-
const language = languageMap[
|
|
2532
|
+
const language = languageMap[textExt] ?? (textExt || undefined);
|
|
2347
2533
|
const lines = raw.split("\n");
|
|
2348
2534
|
const truncated = lines.length > maxLines;
|
|
2349
2535
|
const content = truncated
|
|
@@ -2352,6 +2538,7 @@ export class BridgeWebSocketServer {
|
|
|
2352
2538
|
this.send(ws, {
|
|
2353
2539
|
type: "file_content",
|
|
2354
2540
|
filePath: msg.filePath,
|
|
2541
|
+
kind: "text",
|
|
2355
2542
|
content,
|
|
2356
2543
|
language,
|
|
2357
2544
|
totalLines: lines.length,
|
|
@@ -2877,6 +3064,65 @@ export class BridgeWebSocketServer {
|
|
|
2877
3064
|
}
|
|
2878
3065
|
break;
|
|
2879
3066
|
}
|
|
3067
|
+
case "git_status": {
|
|
3068
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
3069
|
+
this.send(ws, {
|
|
3070
|
+
type: "git_status_result",
|
|
3071
|
+
sessionId: msg.sessionId,
|
|
3072
|
+
projectPath: msg.projectPath,
|
|
3073
|
+
hasUncommittedChanges: false,
|
|
3074
|
+
stagedCount: 0,
|
|
3075
|
+
unstagedCount: 0,
|
|
3076
|
+
untrackedCount: 0,
|
|
3077
|
+
remoteStatusIncluded: false,
|
|
3078
|
+
hasRemoteChanges: false,
|
|
3079
|
+
commitsAhead: 0,
|
|
3080
|
+
commitsBehind: 0,
|
|
3081
|
+
hasUpstream: false,
|
|
3082
|
+
error: `Path not allowed: ${msg.projectPath}`,
|
|
3083
|
+
});
|
|
3084
|
+
break;
|
|
3085
|
+
}
|
|
3086
|
+
try {
|
|
3087
|
+
const result = gitStatus(msg.projectPath, {
|
|
3088
|
+
includeRemote: msg.includeRemote,
|
|
3089
|
+
});
|
|
3090
|
+
this.send(ws, {
|
|
3091
|
+
type: "git_status_result",
|
|
3092
|
+
sessionId: msg.sessionId,
|
|
3093
|
+
projectPath: msg.projectPath,
|
|
3094
|
+
hasUncommittedChanges: result.hasUncommittedChanges,
|
|
3095
|
+
stagedCount: result.stagedCount,
|
|
3096
|
+
unstagedCount: result.unstagedCount,
|
|
3097
|
+
untrackedCount: result.untrackedCount,
|
|
3098
|
+
remoteStatusIncluded: result.remoteStatusIncluded,
|
|
3099
|
+
hasRemoteChanges: result.hasRemoteChanges,
|
|
3100
|
+
commitsAhead: result.commitsAhead,
|
|
3101
|
+
commitsBehind: result.commitsBehind,
|
|
3102
|
+
hasUpstream: result.hasUpstream,
|
|
3103
|
+
branch: result.branch,
|
|
3104
|
+
remoteError: result.remoteError,
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
catch (err) {
|
|
3108
|
+
this.send(ws, {
|
|
3109
|
+
type: "git_status_result",
|
|
3110
|
+
sessionId: msg.sessionId,
|
|
3111
|
+
projectPath: msg.projectPath,
|
|
3112
|
+
hasUncommittedChanges: false,
|
|
3113
|
+
stagedCount: 0,
|
|
3114
|
+
unstagedCount: 0,
|
|
3115
|
+
untrackedCount: 0,
|
|
3116
|
+
remoteStatusIncluded: false,
|
|
3117
|
+
hasRemoteChanges: false,
|
|
3118
|
+
commitsAhead: 0,
|
|
3119
|
+
commitsBehind: 0,
|
|
3120
|
+
hasUpstream: false,
|
|
3121
|
+
error: String(err),
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
2880
3126
|
case "git_remote_status": {
|
|
2881
3127
|
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2882
3128
|
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
@@ -2913,6 +3159,14 @@ export class BridgeWebSocketServer {
|
|
|
2913
3159
|
});
|
|
2914
3160
|
return;
|
|
2915
3161
|
}
|
|
3162
|
+
if (session.provider === "codex") {
|
|
3163
|
+
this.send(ws, {
|
|
3164
|
+
type: "rewind_preview",
|
|
3165
|
+
canRewind: false,
|
|
3166
|
+
error: "Codex rewind does not restore files",
|
|
3167
|
+
});
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
2916
3170
|
this.sessionManager
|
|
2917
3171
|
.rewindFiles(msg.sessionId, msg.targetUuid, true)
|
|
2918
3172
|
.then((result) => {
|
|
@@ -2954,6 +3208,11 @@ export class BridgeWebSocketServer {
|
|
|
2954
3208
|
error: errMsg,
|
|
2955
3209
|
});
|
|
2956
3210
|
};
|
|
3211
|
+
if (session.provider === "codex") {
|
|
3212
|
+
this.rewindCodexConversation(ws, msg.sessionId, msg.targetUuid, msg.mode)
|
|
3213
|
+
.catch(handleError);
|
|
3214
|
+
break;
|
|
3215
|
+
}
|
|
2957
3216
|
if (msg.mode === "code") {
|
|
2958
3217
|
// Code-only rewind: rewind files without restarting the conversation
|
|
2959
3218
|
this.sessionManager
|
|
@@ -4013,6 +4272,14 @@ export class BridgeWebSocketServer {
|
|
|
4013
4272
|
".bmp",
|
|
4014
4273
|
".svg",
|
|
4015
4274
|
]);
|
|
4275
|
+
static FILE_PEEK_IMAGE_EXTENSIONS = new Set([
|
|
4276
|
+
".png",
|
|
4277
|
+
".jpg",
|
|
4278
|
+
".jpeg",
|
|
4279
|
+
".gif",
|
|
4280
|
+
".webp",
|
|
4281
|
+
".svg",
|
|
4282
|
+
]);
|
|
4016
4283
|
// Image diff thresholds (configurable via environment variables)
|
|
4017
4284
|
// - Auto-display: images ≤ threshold are sent inline as base64
|
|
4018
4285
|
// - Max size: images ≤ max are available for on-demand loading
|