@ccpocket/bridge 0.1.0 → 0.2.0

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.
@@ -42,36 +42,42 @@ export async function printStartupInfo(port, _host, apiKey) {
42
42
  list.push(addr.ip);
43
43
  grouped.set(addr.label, list);
44
44
  }
45
+ const hideIp = !!process.env.BRIDGE_HIDE_IP;
45
46
  for (const [label, ips] of grouped) {
46
47
  for (const ip of ips) {
47
48
  const padded = `${label}:`.padEnd(12);
48
- lines.push(`[bridge] ${padded} ws://${ip}:${port}`);
49
+ const display = hideIp ? "xxx.xxx.xxx.xxx" : ip;
50
+ lines.push(`[bridge] ${padded} ws://${display}:${port}`);
49
51
  }
50
52
  }
51
53
  // Use first LAN address, fallback to first address
52
54
  const primaryAddr = addresses.find((a) => a.label === "LAN")?.ip ?? addresses[0].ip;
53
55
  const deepLink = buildConnectionUrl(primaryAddr, port, apiKey);
54
56
  lines.push("");
55
- lines.push(`[bridge] Deep Link: ${deepLink}`);
56
- lines.push("");
57
- lines.push("[bridge] Scan QR code with ccpocket app:");
57
+ lines.push(`[bridge] Deep Link: ${hideIp ? "(hidden)" : deepLink}`);
58
+ if (!hideIp) {
59
+ lines.push("");
60
+ lines.push("[bridge] Scan QR code with ccpocket app:");
61
+ }
58
62
  // Print all non-QR lines
59
63
  console.log(lines.join("\n"));
60
- // Generate and print QR code
61
- try {
62
- const qrText = await QRCode.toString(deepLink, {
63
- type: "terminal",
64
- small: true,
65
- });
66
- // Indent QR code lines
67
- const indented = qrText
68
- .split("\n")
69
- .map((line) => ` ${line}`)
70
- .join("\n");
71
- console.log(indented);
72
- }
73
- catch {
74
- console.log("[bridge] (QR code generation failed)");
64
+ // Generate and print QR code (skip when hiding IP)
65
+ if (!hideIp) {
66
+ try {
67
+ const qrText = await QRCode.toString(deepLink, {
68
+ type: "terminal",
69
+ small: true,
70
+ });
71
+ // Indent QR code lines
72
+ const indented = qrText
73
+ .split("\n")
74
+ .map((line) => ` ${line}`)
75
+ .join("\n");
76
+ console.log(indented);
77
+ }
78
+ catch {
79
+ console.log("[bridge] (QR code generation failed)");
80
+ }
75
81
  }
76
82
  console.log("[bridge] ───────────────────────────────────────────────");
77
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"startup-info.js","sourceRoot":"","sources":["../src/startup-info.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"startup-info.js","sourceRoot":"","sources":["../src/startup-info.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,8 @@ export declare class BridgeWebSocketServer {
34
34
  private recentSessionsRequestId;
35
35
  private debugEvents;
36
36
  private notifiedPermissionToolUses;
37
+ /** FCM token → push notification locale */
38
+ private tokenLocales;
37
39
  constructor(options: BridgeServerOptions);
38
40
  close(): void;
39
41
  /** Return session count for /health endpoint. */
@@ -50,6 +52,8 @@ export declare class BridgeWebSocketServer {
50
52
  private broadcastSessionMessage;
51
53
  /** Extract a short project label from the full projectPath (last directory name). */
52
54
  private projectLabel;
55
+ /** Get unique locales from registered tokens. Falls back to ["en"] if none registered. */
56
+ private getRegisteredLocales;
53
57
  private maybeSendPushNotification;
54
58
  private broadcast;
55
59
  private send;
package/dist/websocket.js CHANGED
@@ -11,6 +11,7 @@ import { listWindows, takeScreenshot } from "./screenshot.js";
11
11
  import { DebugTraceStore } from "./debug-trace-store.js";
12
12
  import { RecordingStore } from "./recording-store.js";
13
13
  import { PushRelayClient } from "./push-relay.js";
14
+ import { normalizePushLocale, t } from "./push-i18n.js";
14
15
  import { fetchAllUsage } from "./usage.js";
15
16
  export class BridgeWebSocketServer {
16
17
  static MAX_DEBUG_EVENTS = 800;
@@ -29,6 +30,8 @@ export class BridgeWebSocketServer {
29
30
  recentSessionsRequestId = 0;
30
31
  debugEvents = new Map();
31
32
  notifiedPermissionToolUses = new Map();
33
+ /** FCM token → push notification locale */
34
+ tokenLocales = new Map();
32
35
  constructor(options) {
33
36
  const { server, apiKey, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup } = options;
34
37
  this.apiKey = apiKey ?? null;
@@ -204,35 +207,43 @@ export class BridgeWebSocketServer {
204
207
  }
205
208
  // Acknowledge receipt immediately so the client can mark the message as sent
206
209
  this.send(ws, { type: "input_ack", sessionId: session.id });
210
+ // Normalize images: support new `images` array and legacy single-image fields
211
+ let images = [];
212
+ if (msg.images && msg.images.length > 0) {
213
+ images = msg.images;
214
+ }
215
+ else if (msg.imageBase64 && msg.mimeType) {
216
+ // Legacy single-image fallback
217
+ images = [{ base64: msg.imageBase64, mimeType: msg.mimeType }];
218
+ }
207
219
  // Add user_input to in-memory history.
208
220
  // The SDK stream does NOT emit user messages, so session.history would
209
221
  // otherwise lack them. This ensures get_history responses include user
210
222
  // messages and replaceEntries on the client side preserves them.
211
223
  // We do NOT broadcast this back — Flutter already shows it via sendMessage().
212
- const hasImage = !!(msg.imageBase64 || msg.imageId);
213
224
  session.history.push({
214
225
  type: "user_input",
215
226
  text,
216
- ...(hasImage ? { imageCount: 1 } : {}),
227
+ ...(images.length > 0 ? { imageCount: images.length } : {}),
217
228
  });
218
- // Codex input path (text + optional image)
229
+ // Persist images to Gallery Store asynchronously (fire-and-forget)
230
+ if (images.length > 0 && this.galleryStore && session.projectPath) {
231
+ for (const img of images) {
232
+ this.galleryStore.addImageFromBase64(img.base64, img.mimeType, session.projectPath, msg.sessionId).catch((err) => {
233
+ console.warn(`[ws] Failed to persist image to gallery: ${err}`);
234
+ });
235
+ }
236
+ }
237
+ // Codex input path
219
238
  if (session.provider === "codex") {
220
239
  const codexProc = session.process;
221
- if (msg.imageBase64 && msg.mimeType) {
222
- codexProc.sendInputWithImage(text, {
223
- base64: msg.imageBase64,
224
- mimeType: msg.mimeType,
225
- });
226
- if (this.galleryStore && session.projectPath) {
227
- this.galleryStore.addImageFromBase64(msg.imageBase64, msg.mimeType, session.projectPath, msg.sessionId).catch((err) => {
228
- console.warn(`[ws] Failed to persist image to gallery: ${err}`);
229
- });
230
- }
240
+ if (images.length > 0) {
241
+ codexProc.sendInputWithImages(text, images);
231
242
  }
232
243
  else if (msg.imageId && this.galleryStore) {
233
244
  this.galleryStore.getImageAsBase64(msg.imageId).then((imageData) => {
234
245
  if (imageData) {
235
- codexProc.sendInputWithImage(text, imageData);
246
+ codexProc.sendInputWithImages(text, [imageData]);
236
247
  }
237
248
  else {
238
249
  console.warn(`[ws] Image not found: ${msg.imageId}`);
@@ -248,26 +259,17 @@ export class BridgeWebSocketServer {
248
259
  }
249
260
  break;
250
261
  }
251
- // Priority 1: Direct Base64 image (simplified flow)
262
+ // Claude Code input path
252
263
  const claudeProc = session.process;
253
- if (msg.imageBase64 && msg.mimeType) {
254
- console.log(`[ws] Sending message with inline Base64 image (${msg.mimeType})`);
255
- claudeProc.sendInputWithImage(text, {
256
- base64: msg.imageBase64,
257
- mimeType: msg.mimeType,
258
- });
259
- // Persist to Gallery Store asynchronously (fire-and-forget)
260
- if (this.galleryStore && session.projectPath) {
261
- this.galleryStore.addImageFromBase64(msg.imageBase64, msg.mimeType, session.projectPath, msg.sessionId).catch((err) => {
262
- console.warn(`[ws] Failed to persist image to gallery: ${err}`);
263
- });
264
- }
264
+ if (images.length > 0) {
265
+ console.log(`[ws] Sending message with ${images.length} inline Base64 image(s)`);
266
+ claudeProc.sendInputWithImages(text, images);
265
267
  }
266
- // Priority 2: Legacy imageId mode (backward compatibility)
268
+ // Legacy imageId mode (backward compatibility)
267
269
  else if (msg.imageId && this.galleryStore) {
268
270
  this.galleryStore.getImageAsBase64(msg.imageId).then((imageData) => {
269
271
  if (imageData) {
270
- claudeProc.sendInputWithImage(text, imageData);
272
+ claudeProc.sendInputWithImages(text, [imageData]);
271
273
  }
272
274
  else {
273
275
  console.warn(`[ws] Image not found: ${msg.imageId}`);
@@ -278,19 +280,21 @@ export class BridgeWebSocketServer {
278
280
  session.process.sendInput(text);
279
281
  });
280
282
  }
281
- // Priority 3: Text-only message
283
+ // Text-only message
282
284
  else {
283
285
  session.process.sendInput(text);
284
286
  }
285
287
  break;
286
288
  }
287
289
  case "push_register": {
288
- console.log(`[ws] push_register received (platform: ${msg.platform}, configured: ${this.pushRelay.isConfigured})`);
290
+ const locale = normalizePushLocale(msg.locale);
291
+ console.log(`[ws] push_register received (platform: ${msg.platform}, locale: ${locale}, configured: ${this.pushRelay.isConfigured})`);
289
292
  if (!this.pushRelay.isConfigured) {
290
293
  this.send(ws, { type: "error", message: "Push relay is not configured on bridge" });
291
294
  return;
292
295
  }
293
- this.pushRelay.registerToken(msg.token, msg.platform).then(() => {
296
+ this.tokenLocales.set(msg.token, locale);
297
+ this.pushRelay.registerToken(msg.token, msg.platform, locale).then(() => {
294
298
  console.log("[ws] push_register: token registered successfully");
295
299
  }).catch((err) => {
296
300
  const detail = err instanceof Error ? err.message : String(err);
@@ -305,6 +309,7 @@ export class BridgeWebSocketServer {
305
309
  this.send(ws, { type: "error", message: "Push relay is not configured on bridge" });
306
310
  return;
307
311
  }
312
+ this.tokenLocales.delete(msg.token);
308
313
  this.pushRelay.unregisterToken(msg.token).then(() => {
309
314
  console.log("[ws] push_unregister: token unregistered successfully");
310
315
  }).catch((err) => {
@@ -426,6 +431,7 @@ export class BridgeWebSocketServer {
426
431
  projectPath,
427
432
  ...(permissionMode ? { permissionMode } : {}),
428
433
  clearContext: true,
434
+ sourceSessionId: sessionId,
429
435
  });
430
436
  this.broadcastSessionList();
431
437
  }
@@ -1138,6 +1144,11 @@ export class BridgeWebSocketServer {
1138
1144
  const parts = session.projectPath.replace(/\/+$/, "").split("/");
1139
1145
  return parts[parts.length - 1] || "";
1140
1146
  }
1147
+ /** Get unique locales from registered tokens. Falls back to ["en"] if none registered. */
1148
+ getRegisteredLocales() {
1149
+ const locales = new Set(this.tokenLocales.values());
1150
+ return locales.size > 0 ? [...locales] : ["en"];
1151
+ }
1141
1152
  maybeSendPushNotification(sessionId, msg) {
1142
1153
  if (!this.pushRelay.isConfigured)
1143
1154
  return;
@@ -1149,38 +1160,54 @@ export class BridgeWebSocketServer {
1149
1160
  seen.add(msg.toolUseId);
1150
1161
  this.notifiedPermissionToolUses.set(sessionId, seen);
1151
1162
  const isAskUserQuestion = msg.toolName === "AskUserQuestion";
1163
+ const isExitPlanMode = msg.toolName === "ExitPlanMode";
1152
1164
  const eventType = isAskUserQuestion ? "ask_user_question" : "approval_required";
1153
- const title = project
1154
- ? (isAskUserQuestion ? `回答待ち - ${project}` : `承認待ち - ${project}`)
1155
- : (isAskUserQuestion ? "回答待ち" : "承認待ち");
1156
- let body;
1165
+ // Extract question text for AskUserQuestion
1166
+ let questionText;
1157
1167
  if (isAskUserQuestion) {
1158
- // Extract question text from input.questions[0].question
1159
1168
  const questions = msg.input?.questions;
1160
1169
  const firstQuestion = Array.isArray(questions) && questions.length > 0
1161
1170
  ? questions[0]?.question
1162
1171
  : undefined;
1163
- body = typeof firstQuestion === "string" && firstQuestion.length > 0
1164
- ? firstQuestion.slice(0, 120)
1165
- : "Claude が質問しています";
1172
+ if (typeof firstQuestion === "string" && firstQuestion.length > 0) {
1173
+ questionText = firstQuestion.slice(0, 120);
1174
+ }
1166
1175
  }
1167
- else {
1168
- body = `${msg.toolName} の実行を承認してください`;
1176
+ const data = {
1177
+ sessionId,
1178
+ provider: this.sessionManager.get(sessionId)?.provider ?? "claude",
1179
+ toolUseId: msg.toolUseId,
1180
+ toolName: msg.toolName,
1181
+ };
1182
+ for (const locale of this.getRegisteredLocales()) {
1183
+ let title;
1184
+ let body;
1185
+ if (isExitPlanMode) {
1186
+ const titleKey = "plan_ready_title";
1187
+ title = project ? `${t(locale, titleKey)} - ${project}` : t(locale, titleKey);
1188
+ body = t(locale, "plan_ready_body");
1189
+ }
1190
+ else if (isAskUserQuestion) {
1191
+ const titleKey = "ask_title";
1192
+ title = project ? `${t(locale, titleKey)} - ${project}` : t(locale, titleKey);
1193
+ body = questionText ?? t(locale, "ask_default_body");
1194
+ }
1195
+ else {
1196
+ const titleKey = "approval_title";
1197
+ title = project ? `${t(locale, titleKey)} - ${project}` : t(locale, titleKey);
1198
+ body = t(locale, "approval_body", { toolName: msg.toolName });
1199
+ }
1200
+ void this.pushRelay.notify({
1201
+ eventType,
1202
+ title,
1203
+ body,
1204
+ locale,
1205
+ data,
1206
+ }).catch((err) => {
1207
+ const detail = err instanceof Error ? err.message : String(err);
1208
+ console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
1209
+ });
1169
1210
  }
1170
- void this.pushRelay.notify({
1171
- eventType,
1172
- title,
1173
- body,
1174
- data: {
1175
- sessionId,
1176
- provider: this.sessionManager.get(sessionId)?.provider ?? "claude",
1177
- toolUseId: msg.toolUseId,
1178
- toolName: msg.toolName,
1179
- },
1180
- }).catch((err) => {
1181
- const detail = err instanceof Error ? err.message : String(err);
1182
- console.warn(`[ws] Failed to send push notification (${eventType}): ${detail}`);
1183
- });
1184
1211
  return;
1185
1212
  }
1186
1213
  if (msg.type !== "result")
@@ -1191,24 +1218,14 @@ export class BridgeWebSocketServer {
1191
1218
  return;
1192
1219
  const isSuccess = msg.subtype === "success";
1193
1220
  const eventType = isSuccess ? "session_completed" : "session_failed";
1194
- const title = project
1195
- ? (isSuccess ? `✅ ${project}` : `❌ ${project}`)
1196
- : (isSuccess ? "タスク完了" : "エラー発生");
1197
- let body;
1221
+ const pieces = [];
1198
1222
  if (isSuccess) {
1199
- const pieces = [];
1200
1223
  if (msg.duration != null)
1201
1224
  pieces.push(`${msg.duration.toFixed(1)}s`);
1202
1225
  if (msg.cost != null)
1203
1226
  pieces.push(`$${msg.cost.toFixed(4)}`);
1204
- const stats = pieces.length > 0 ? ` (${pieces.join(", ")})` : "";
1205
- body = msg.result
1206
- ? `${msg.result.slice(0, 120)}${stats}`
1207
- : `セッション完了${stats}`;
1208
- }
1209
- else {
1210
- body = msg.error ? msg.error.slice(0, 120) : "セッションが失敗しました";
1211
1227
  }
1228
+ const stats = pieces.length > 0 ? ` (${pieces.join(", ")})` : "";
1212
1229
  const data = {
1213
1230
  sessionId,
1214
1231
  provider: this.sessionManager.get(sessionId)?.provider ?? "claude",
@@ -1218,15 +1235,30 @@ export class BridgeWebSocketServer {
1218
1235
  data.stopReason = msg.stopReason;
1219
1236
  if (msg.sessionId)
1220
1237
  data.providerSessionId = msg.sessionId;
1221
- void this.pushRelay.notify({
1222
- eventType,
1223
- title,
1224
- body,
1225
- data,
1226
- }).catch((err) => {
1227
- const detail = err instanceof Error ? err.message : String(err);
1228
- console.warn(`[ws] Failed to send push notification (${eventType}): ${detail}`);
1229
- });
1238
+ for (const locale of this.getRegisteredLocales()) {
1239
+ const title = project
1240
+ ? (isSuccess ? `✅ ${project}` : `❌ ${project}`)
1241
+ : (isSuccess ? t(locale, "task_completed") : t(locale, "error_occurred"));
1242
+ let body;
1243
+ if (isSuccess) {
1244
+ body = msg.result
1245
+ ? `${msg.result.slice(0, 120)}${stats}`
1246
+ : `${t(locale, "session_completed")}${stats}`;
1247
+ }
1248
+ else {
1249
+ body = msg.error ? msg.error.slice(0, 120) : t(locale, "session_failed");
1250
+ }
1251
+ void this.pushRelay.notify({
1252
+ eventType,
1253
+ title,
1254
+ body,
1255
+ locale,
1256
+ data,
1257
+ }).catch((err) => {
1258
+ const detail = err instanceof Error ? err.message : String(err);
1259
+ console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
1260
+ });
1261
+ }
1230
1262
  }
1231
1263
  broadcast(msg) {
1232
1264
  const data = JSON.stringify(msg);