@ccpocket/bridge 1.71.0 → 1.72.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/dist/codex-process.d.ts +5 -4
- package/dist/codex-process.js +187 -34
- package/dist/codex-process.js.map +1 -1
- package/dist/directory-listing.d.ts +2 -2
- package/dist/directory-listing.js +4 -3
- package/dist/directory-listing.js.map +1 -1
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +6 -1
- package/dist/parser.js.map +1 -1
- package/dist/push-relay.d.ts +2 -0
- package/dist/push-relay.js +1 -0
- package/dist/push-relay.js.map +1 -1
- package/dist/sdk-process.d.ts +4 -4
- package/dist/sdk-process.js +8 -4
- package/dist/sdk-process.js.map +1 -1
- package/dist/websocket.d.ts +10 -1
- package/dist/websocket.js +137 -41
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { lstat, readFile, readlink, stat, unlink } from "node:fs/promises";
|
|
@@ -96,6 +96,7 @@ const OPT_IN_SERVER_MESSAGES = new Set([
|
|
|
96
96
|
"goal_state",
|
|
97
97
|
"guardian_approval",
|
|
98
98
|
"prompt_history_status",
|
|
99
|
+
"push_registration_result",
|
|
99
100
|
]);
|
|
100
101
|
function isRecord(value) {
|
|
101
102
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -485,6 +486,9 @@ export class BridgeWebSocketServer {
|
|
|
485
486
|
/** FCM token → push notification locale */
|
|
486
487
|
tokenLocales = new Map();
|
|
487
488
|
tokenPrivacyMode = new Map();
|
|
489
|
+
pushTokenGeneration = new Map();
|
|
490
|
+
pushTokenOperations = new Map();
|
|
491
|
+
nextPushTokenGeneration = 0;
|
|
488
492
|
failSetPermissionMode = envFlagEnabled("BRIDGE_FAIL_SET_PERMISSION_MODE");
|
|
489
493
|
failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
|
|
490
494
|
fileListMaxEntries;
|
|
@@ -579,6 +583,14 @@ export class BridgeWebSocketServer {
|
|
|
579
583
|
errorCode: "path_not_allowed",
|
|
580
584
|
};
|
|
581
585
|
}
|
|
586
|
+
sendToolActionError(ws, message, error) {
|
|
587
|
+
this.send(ws, {
|
|
588
|
+
type: "error",
|
|
589
|
+
message: error,
|
|
590
|
+
sessionId: message.sessionId,
|
|
591
|
+
toolUseId: message.toolUseId ?? message.id,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
582
594
|
normalizeAdditionalWritableRoots(roots, projectPath) {
|
|
583
595
|
if (!roots || roots.length === 0)
|
|
584
596
|
return {};
|
|
@@ -2243,33 +2255,64 @@ export class BridgeWebSocketServer {
|
|
|
2243
2255
|
case "push_register": {
|
|
2244
2256
|
const locale = normalizePushLocale(msg.locale);
|
|
2245
2257
|
const privacyMode = msg.privacyMode === true;
|
|
2258
|
+
const supportsRegistrationResult = this.clientSupportedServerMessages
|
|
2259
|
+
.get(ws)
|
|
2260
|
+
?.has("push_registration_result") ?? false;
|
|
2246
2261
|
console.log(`[ws] push_register received (platform: ${msg.platform}, locale: ${locale}, privacy: ${privacyMode}, configured: ${this.pushRelay.isConfigured})`);
|
|
2262
|
+
const generation = this.beginPushTokenOperation(msg.token);
|
|
2247
2263
|
if (!this.pushRelay.isConfigured) {
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2264
|
+
const error = "Push relay is not configured on bridge";
|
|
2265
|
+
this.send(ws, supportsRegistrationResult
|
|
2266
|
+
? {
|
|
2267
|
+
type: "push_registration_result",
|
|
2268
|
+
token: msg.token,
|
|
2269
|
+
requestId: msg.requestId ?? "",
|
|
2270
|
+
success: false,
|
|
2271
|
+
error,
|
|
2272
|
+
}
|
|
2273
|
+
: { type: "error", message: error });
|
|
2252
2274
|
return;
|
|
2253
2275
|
}
|
|
2254
|
-
this.
|
|
2255
|
-
|
|
2256
|
-
this.pushRelay
|
|
2257
|
-
.registerToken(msg.token, msg.platform, locale)
|
|
2276
|
+
const operation = this.enqueuePushTokenOperation(msg.token, () => this.pushRelay.registerToken(msg.token, msg.platform, locale));
|
|
2277
|
+
operation
|
|
2258
2278
|
.then(() => {
|
|
2279
|
+
if (!this.isCurrentPushTokenOperation(msg.token, generation))
|
|
2280
|
+
return;
|
|
2259
2281
|
console.log("[ws] push_register: token registered successfully");
|
|
2282
|
+
if (supportsRegistrationResult) {
|
|
2283
|
+
this.send(ws, {
|
|
2284
|
+
type: "push_registration_result",
|
|
2285
|
+
token: msg.token,
|
|
2286
|
+
requestId: msg.requestId ?? "",
|
|
2287
|
+
success: true,
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
// Enable delivery only after the acknowledgement has been queued
|
|
2291
|
+
// on the WebSocket, keeping the app on local fallback until then.
|
|
2292
|
+
this.tokenLocales.set(msg.token, locale);
|
|
2293
|
+
this.tokenPrivacyMode.set(msg.token, privacyMode);
|
|
2260
2294
|
})
|
|
2261
2295
|
.catch((err) => {
|
|
2296
|
+
if (!this.isCurrentPushTokenOperation(msg.token, generation))
|
|
2297
|
+
return;
|
|
2262
2298
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2263
2299
|
console.error(`[ws] push_register failed: ${detail}`);
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2300
|
+
const error = `Failed to register push token: ${detail}`;
|
|
2301
|
+
this.send(ws, supportsRegistrationResult
|
|
2302
|
+
? {
|
|
2303
|
+
type: "push_registration_result",
|
|
2304
|
+
token: msg.token,
|
|
2305
|
+
requestId: msg.requestId ?? "",
|
|
2306
|
+
success: false,
|
|
2307
|
+
error,
|
|
2308
|
+
}
|
|
2309
|
+
: { type: "error", message: error });
|
|
2268
2310
|
});
|
|
2269
2311
|
break;
|
|
2270
2312
|
}
|
|
2271
2313
|
case "push_unregister": {
|
|
2272
2314
|
console.log("[ws] push_unregister received");
|
|
2315
|
+
const generation = this.beginPushTokenOperation(msg.token);
|
|
2273
2316
|
if (!this.pushRelay.isConfigured) {
|
|
2274
2317
|
this.send(ws, {
|
|
2275
2318
|
type: "error",
|
|
@@ -2277,14 +2320,15 @@ export class BridgeWebSocketServer {
|
|
|
2277
2320
|
});
|
|
2278
2321
|
return;
|
|
2279
2322
|
}
|
|
2280
|
-
this.
|
|
2281
|
-
this.tokenPrivacyMode.delete(msg.token);
|
|
2282
|
-
this.pushRelay
|
|
2283
|
-
.unregisterToken(msg.token)
|
|
2323
|
+
this.enqueuePushTokenOperation(msg.token, () => this.pushRelay.unregisterToken(msg.token))
|
|
2284
2324
|
.then(() => {
|
|
2325
|
+
if (!this.isCurrentPushTokenOperation(msg.token, generation))
|
|
2326
|
+
return;
|
|
2285
2327
|
console.log("[ws] push_unregister: token unregistered successfully");
|
|
2286
2328
|
})
|
|
2287
2329
|
.catch((err) => {
|
|
2330
|
+
if (!this.isCurrentPushTokenOperation(msg.token, generation))
|
|
2331
|
+
return;
|
|
2288
2332
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2289
2333
|
console.error(`[ws] push_unregister failed: ${detail}`);
|
|
2290
2334
|
this.send(ws, {
|
|
@@ -2964,11 +3008,14 @@ export class BridgeWebSocketServer {
|
|
|
2964
3008
|
case "approve": {
|
|
2965
3009
|
const session = this.resolveSession(msg.sessionId);
|
|
2966
3010
|
if (!session) {
|
|
2967
|
-
this.
|
|
3011
|
+
this.sendToolActionError(ws, msg, "No active session.");
|
|
2968
3012
|
return;
|
|
2969
3013
|
}
|
|
2970
3014
|
if (session.provider === "codex") {
|
|
2971
|
-
session.process.approve(msg.id);
|
|
3015
|
+
const handled = session.process.approve(msg.id);
|
|
3016
|
+
if (handled === false) {
|
|
3017
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3018
|
+
}
|
|
2972
3019
|
break;
|
|
2973
3020
|
}
|
|
2974
3021
|
const sdkProc = session.process;
|
|
@@ -3016,70 +3063,85 @@ export class BridgeWebSocketServer {
|
|
|
3016
3063
|
this.broadcastSessionList();
|
|
3017
3064
|
}
|
|
3018
3065
|
else {
|
|
3019
|
-
sdkProc.approve(msg.id);
|
|
3066
|
+
const handled = sdkProc.approve(msg.id);
|
|
3067
|
+
if (handled === false) {
|
|
3068
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3069
|
+
}
|
|
3020
3070
|
}
|
|
3021
3071
|
break;
|
|
3022
3072
|
}
|
|
3023
3073
|
case "approve_always": {
|
|
3024
3074
|
const session = this.resolveSession(msg.sessionId);
|
|
3025
3075
|
if (!session) {
|
|
3026
|
-
this.
|
|
3076
|
+
this.sendToolActionError(ws, msg, "No active session.");
|
|
3027
3077
|
return;
|
|
3028
3078
|
}
|
|
3029
3079
|
if (session.provider === "codex") {
|
|
3030
|
-
session.process.approveAlways(msg.id);
|
|
3080
|
+
const handled = session.process.approveAlways(msg.id);
|
|
3081
|
+
if (handled === false) {
|
|
3082
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3083
|
+
}
|
|
3031
3084
|
break;
|
|
3032
3085
|
}
|
|
3033
|
-
session.process.approveAlways(msg.id);
|
|
3086
|
+
const handled = session.process.approveAlways(msg.id);
|
|
3087
|
+
if (handled === false) {
|
|
3088
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3089
|
+
}
|
|
3034
3090
|
break;
|
|
3035
3091
|
}
|
|
3036
3092
|
case "reject": {
|
|
3037
3093
|
const session = this.resolveSession(msg.sessionId);
|
|
3038
3094
|
if (!session) {
|
|
3039
|
-
this.
|
|
3095
|
+
this.sendToolActionError(ws, msg, "No active session.");
|
|
3040
3096
|
return;
|
|
3041
3097
|
}
|
|
3042
3098
|
if (session.provider === "codex") {
|
|
3043
|
-
session.process.reject(msg.id, msg.message);
|
|
3099
|
+
const handled = session.process.reject(msg.id, msg.message);
|
|
3100
|
+
if (handled === false) {
|
|
3101
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3102
|
+
}
|
|
3044
3103
|
break;
|
|
3045
3104
|
}
|
|
3046
|
-
session.process.reject(msg.id, msg.message);
|
|
3105
|
+
const handled = session.process.reject(msg.id, msg.message);
|
|
3106
|
+
if (handled === false) {
|
|
3107
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3108
|
+
}
|
|
3047
3109
|
break;
|
|
3048
3110
|
}
|
|
3049
3111
|
case "answer": {
|
|
3050
3112
|
const session = this.resolveSession(msg.sessionId);
|
|
3051
3113
|
if (!session) {
|
|
3052
|
-
this.
|
|
3114
|
+
this.sendToolActionError(ws, msg, "No active session.");
|
|
3053
3115
|
return;
|
|
3054
3116
|
}
|
|
3055
3117
|
if (session.provider === "codex") {
|
|
3056
|
-
session.process.answer(msg.toolUseId, msg.result);
|
|
3118
|
+
const handled = session.process.answer(msg.toolUseId, msg.result);
|
|
3119
|
+
if (handled === false) {
|
|
3120
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3121
|
+
}
|
|
3057
3122
|
break;
|
|
3058
3123
|
}
|
|
3059
|
-
session.process.answer(msg.toolUseId, msg.result);
|
|
3124
|
+
const handled = session.process.answer(msg.toolUseId, msg.result);
|
|
3125
|
+
if (handled === false) {
|
|
3126
|
+
this.sendToolActionError(ws, msg, "No matching pending tool action.");
|
|
3127
|
+
}
|
|
3060
3128
|
break;
|
|
3061
3129
|
}
|
|
3062
3130
|
case "install_tool_suggestion": {
|
|
3063
3131
|
const session = this.resolveSession(msg.sessionId);
|
|
3064
3132
|
if (!session) {
|
|
3065
|
-
this.
|
|
3133
|
+
this.sendToolActionError(ws, msg, "No active session.");
|
|
3066
3134
|
return;
|
|
3067
3135
|
}
|
|
3068
3136
|
if (session.provider !== "codex") {
|
|
3069
|
-
this.
|
|
3070
|
-
type: "error",
|
|
3071
|
-
message: "Tool suggestions are only supported for Codex sessions.",
|
|
3072
|
-
});
|
|
3137
|
+
this.sendToolActionError(ws, msg, "Tool suggestions are only supported for Codex sessions.");
|
|
3073
3138
|
return;
|
|
3074
3139
|
}
|
|
3075
3140
|
try {
|
|
3076
3141
|
await session.process.installToolSuggestion(msg.toolUseId);
|
|
3077
3142
|
}
|
|
3078
3143
|
catch (err) {
|
|
3079
|
-
this.
|
|
3080
|
-
type: "error",
|
|
3081
|
-
message: err instanceof Error ? err.message : String(err),
|
|
3082
|
-
});
|
|
3144
|
+
this.sendToolActionError(ws, msg, err instanceof Error ? err.message : String(err));
|
|
3083
3145
|
}
|
|
3084
3146
|
break;
|
|
3085
3147
|
}
|
|
@@ -3882,7 +3944,7 @@ export class BridgeWebSocketServer {
|
|
|
3882
3944
|
}
|
|
3883
3945
|
case "list_directory": {
|
|
3884
3946
|
try {
|
|
3885
|
-
const listing = await listAllowedDirectories(msg.path, this.allowedDirs, this.platform);
|
|
3947
|
+
const listing = await listAllowedDirectories(msg.path, this.allowedDirs, this.platform, msg.includeHidden ?? false);
|
|
3886
3948
|
this.send(ws, {
|
|
3887
3949
|
type: "directory_listing",
|
|
3888
3950
|
path: listing.path,
|
|
@@ -6015,10 +6077,40 @@ export class BridgeWebSocketServer {
|
|
|
6015
6077
|
const parts = session.projectPath.replace(/\/+$/, "").split("/");
|
|
6016
6078
|
return parts[parts.length - 1] || "";
|
|
6017
6079
|
}
|
|
6018
|
-
|
|
6080
|
+
beginPushTokenOperation(token) {
|
|
6081
|
+
const generation = ++this.nextPushTokenGeneration;
|
|
6082
|
+
this.pushTokenGeneration.set(token, generation);
|
|
6083
|
+
// Local delivery is disabled synchronously while the relay operation is
|
|
6084
|
+
// pending. The app stays on local fallback until a successful ACK.
|
|
6085
|
+
this.tokenLocales.delete(token);
|
|
6086
|
+
this.tokenPrivacyMode.delete(token);
|
|
6087
|
+
return generation;
|
|
6088
|
+
}
|
|
6089
|
+
enqueuePushTokenOperation(token, operation) {
|
|
6090
|
+
const previous = this.pushTokenOperations.get(token);
|
|
6091
|
+
const pending = (previous?.catch(() => { }) ?? Promise.resolve()).then(operation);
|
|
6092
|
+
this.pushTokenOperations.set(token, pending);
|
|
6093
|
+
const cleanup = () => {
|
|
6094
|
+
if (this.pushTokenOperations.get(token) === pending) {
|
|
6095
|
+
this.pushTokenOperations.delete(token);
|
|
6096
|
+
}
|
|
6097
|
+
};
|
|
6098
|
+
void pending.then(cleanup, cleanup);
|
|
6099
|
+
return pending;
|
|
6100
|
+
}
|
|
6101
|
+
isCurrentPushTokenOperation(token, generation) {
|
|
6102
|
+
return this.pushTokenGeneration.get(token) === generation;
|
|
6103
|
+
}
|
|
6104
|
+
/** Get unique locales from tokens acknowledged by the push relay. */
|
|
6019
6105
|
getRegisteredLocales() {
|
|
6020
6106
|
const locales = new Set(this.tokenLocales.values());
|
|
6021
|
-
return
|
|
6107
|
+
return [...locales];
|
|
6108
|
+
}
|
|
6109
|
+
/** Hashes of relay tokens that are currently safe for remote delivery. */
|
|
6110
|
+
getActivePushTokenHashes(locale) {
|
|
6111
|
+
return [...this.tokenLocales]
|
|
6112
|
+
.filter(([, tokenLocale]) => tokenLocale === locale)
|
|
6113
|
+
.map(([token]) => createHash("sha256").update(token).digest("hex"));
|
|
6022
6114
|
}
|
|
6023
6115
|
/** Whether any registered token has privacy mode enabled (conservative: privacy wins). */
|
|
6024
6116
|
isPrivacyMode() {
|
|
@@ -6040,6 +6132,8 @@ export class BridgeWebSocketServer {
|
|
|
6040
6132
|
maybeSendPushNotification(sessionId, msg) {
|
|
6041
6133
|
if (!this.pushRelay.isConfigured)
|
|
6042
6134
|
return;
|
|
6135
|
+
if (this.tokenLocales.size === 0)
|
|
6136
|
+
return;
|
|
6043
6137
|
const privacy = this.isPrivacyMode();
|
|
6044
6138
|
const label = privacy ? "" : this.sessionLabel(sessionId);
|
|
6045
6139
|
if (msg.type === "permission_request") {
|
|
@@ -6104,6 +6198,7 @@ export class BridgeWebSocketServer {
|
|
|
6104
6198
|
title,
|
|
6105
6199
|
body,
|
|
6106
6200
|
locale,
|
|
6201
|
+
tokenHashes: this.getActivePushTokenHashes(locale),
|
|
6107
6202
|
data,
|
|
6108
6203
|
})
|
|
6109
6204
|
.catch((err) => {
|
|
@@ -6177,6 +6272,7 @@ export class BridgeWebSocketServer {
|
|
|
6177
6272
|
title,
|
|
6178
6273
|
body,
|
|
6179
6274
|
locale,
|
|
6275
|
+
tokenHashes: this.getActivePushTokenHashes(locale),
|
|
6180
6276
|
data,
|
|
6181
6277
|
})
|
|
6182
6278
|
.catch((err) => {
|