@ccpocket/bridge 1.33.3 → 1.34.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.
@@ -27,6 +27,15 @@ export interface CodexSkillMetadata {
27
27
  defaultPrompt?: string;
28
28
  brandColor?: string;
29
29
  }
30
+ /** App / connector metadata returned by the Codex `app/list` RPC. */
31
+ export interface CodexAppMetadata {
32
+ id: string;
33
+ name: string;
34
+ description: string;
35
+ installUrl?: string;
36
+ isAccessible: boolean;
37
+ isEnabled: boolean;
38
+ }
30
39
  export interface CodexThreadSummary {
31
40
  id: string;
32
41
  preview: string;
@@ -64,10 +73,17 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
64
73
  private lastTokenUsage;
65
74
  /** Full skill metadata from the last `skills/list` response. */
66
75
  private _skills;
76
+ /** Full app metadata from the last `app/list` response. */
77
+ private _apps;
67
78
  /** Project path stored for re-fetching skills on `skills/changed`. */
68
79
  private _projectPath;
80
+ /** Prevent redundant completion fetch storms from repeated change notifications. */
81
+ private _completionFetchInFlight;
82
+ private _completionFetchQueued;
83
+ private _lastCompletionEntitiesSignature;
69
84
  /** Expose skill metadata so session/websocket can access it. */
70
85
  get skills(): CodexSkillMetadata[];
86
+ get apps(): CodexAppMetadata[];
71
87
  private rpcSeq;
72
88
  private pendingRpc;
73
89
  private stdoutBuffer;
@@ -135,6 +151,20 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
135
151
  name: string;
136
152
  path: string;
137
153
  }): void;
154
+ sendInputStructured(text: string, options?: {
155
+ images?: Array<{
156
+ base64: string;
157
+ mimeType: string;
158
+ }>;
159
+ skills?: Array<{
160
+ name: string;
161
+ path: string;
162
+ }>;
163
+ mentions?: Array<{
164
+ name: string;
165
+ path: string;
166
+ }>;
167
+ }): void;
138
168
  approve(toolUseId?: string, _updatedInput?: Record<string, unknown>): void;
139
169
  approveAlways(toolUseId?: string): void;
140
170
  reject(toolUseId?: string, _message?: string): void;
@@ -148,6 +178,17 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
148
178
  private emitToolResult;
149
179
  private resolvePendingApproval;
150
180
  private resolvePendingUserInput;
181
+ /**
182
+ * Approve a pending user-input request (McpElicitation fallback).
183
+ * Called when approve()/approveAlways() cannot find a pendingApproval —
184
+ * McpElicitation lives in pendingUserInputs but the app routes it through
185
+ * the permission (approve/reject) path.
186
+ */
187
+ private approveUserInput;
188
+ /**
189
+ * Reject a pending user-input request (McpElicitation fallback).
190
+ */
191
+ private rejectUserInput;
151
192
  /**
152
193
  * Plan approved → switch to Default mode and auto-start execution.
153
194
  */
@@ -158,12 +199,8 @@ export declare class CodexProcess extends EventEmitter<CodexProcessEvents> {
158
199
  private handlePlanRejected;
159
200
  private bootstrap;
160
201
  private initializeRpcConnection;
161
- /**
162
- * Fetch skills from Codex app-server via `skills/list` RPC and emit them
163
- * as a `supported_commands` system message so the Flutter client can display
164
- * skill entries alongside built-in slash commands.
165
- */
166
- private fetchSkills;
202
+ private fetchCompletionEntities;
203
+ private _fetchCompletionEntitiesInternal;
167
204
  private runInputLoop;
168
205
  private handleStdoutChunk;
169
206
  private handleRpcEnvelope;
@@ -45,12 +45,21 @@ export class CodexProcess extends EventEmitter {
45
45
  lastTokenUsage = null;
46
46
  /** Full skill metadata from the last `skills/list` response. */
47
47
  _skills = [];
48
+ /** Full app metadata from the last `app/list` response. */
49
+ _apps = [];
48
50
  /** Project path stored for re-fetching skills on `skills/changed`. */
49
51
  _projectPath = null;
52
+ /** Prevent redundant completion fetch storms from repeated change notifications. */
53
+ _completionFetchInFlight = null;
54
+ _completionFetchQueued = false;
55
+ _lastCompletionEntitiesSignature = null;
50
56
  /** Expose skill metadata so session/websocket can access it. */
51
57
  get skills() {
52
58
  return this._skills;
53
59
  }
60
+ get apps() {
61
+ return this._apps;
62
+ }
54
63
  rpcSeq = 1;
55
64
  pendingRpc = new Map();
56
65
  stdoutBuffer = "";
@@ -273,13 +282,21 @@ export class CodexProcess extends EventEmitter {
273
282
  resolve({ text, images });
274
283
  }
275
284
  sendInputWithSkill(text, skill) {
285
+ this.sendInputStructured(text, { skills: [skill] });
286
+ }
287
+ sendInputStructured(text, options) {
276
288
  if (!this.inputResolve) {
277
- console.error("[codex-process] No pending input resolver for sendInputWithSkill");
289
+ console.error("[codex-process] No pending input resolver for sendInputStructured");
278
290
  return;
279
291
  }
280
292
  const resolve = this.inputResolve;
281
293
  this.inputResolve = null;
282
- resolve({ text, skill });
294
+ resolve({
295
+ text,
296
+ images: options?.images,
297
+ skills: options?.skills,
298
+ mentions: options?.mentions,
299
+ });
283
300
  }
284
301
  approve(toolUseId, _updatedInput) {
285
302
  // Check if this is a plan completion approval
@@ -290,6 +307,9 @@ export class CodexProcess extends EventEmitter {
290
307
  }
291
308
  const pending = this.resolvePendingApproval(toolUseId);
292
309
  if (!pending) {
310
+ // Fallback: McpElicitation lives in pendingUserInputs
311
+ if (this.approveUserInput(toolUseId, "Accept"))
312
+ return;
293
313
  console.log("[codex-process] approve() called but no pending permission requests");
294
314
  return;
295
315
  }
@@ -303,6 +323,9 @@ export class CodexProcess extends EventEmitter {
303
323
  approveAlways(toolUseId) {
304
324
  const pending = this.resolvePendingApproval(toolUseId);
305
325
  if (!pending) {
326
+ // Fallback: McpElicitation lives in pendingUserInputs
327
+ if (this.approveUserInput(toolUseId, "Accept"))
328
+ return;
306
329
  console.log("[codex-process] approveAlways() called but no pending permission requests");
307
330
  return;
308
331
  }
@@ -322,6 +345,9 @@ export class CodexProcess extends EventEmitter {
322
345
  }
323
346
  const pending = this.resolvePendingApproval(toolUseId);
324
347
  if (!pending) {
348
+ // Fallback: McpElicitation lives in pendingUserInputs
349
+ if (this.rejectUserInput(toolUseId, "Decline"))
350
+ return;
325
351
  console.log("[codex-process] reject() called but no pending permission requests");
326
352
  return;
327
353
  }
@@ -369,7 +395,7 @@ export class CodexProcess extends EventEmitter {
369
395
  return undefined;
370
396
  return {
371
397
  toolUseId: pendingAsk.toolUseId,
372
- toolName: "AskUserQuestion",
398
+ toolName: pendingAsk.toolName,
373
399
  input: { ...pendingAsk.input },
374
400
  };
375
401
  }
@@ -393,6 +419,39 @@ export class CodexProcess extends EventEmitter {
393
419
  const first = this.pendingUserInputs.values().next();
394
420
  return first.done ? undefined : first.value;
395
421
  }
422
+ /**
423
+ * Approve a pending user-input request (McpElicitation fallback).
424
+ * Called when approve()/approveAlways() cannot find a pendingApproval —
425
+ * McpElicitation lives in pendingUserInputs but the app routes it through
426
+ * the permission (approve/reject) path.
427
+ */
428
+ approveUserInput(toolUseId, result) {
429
+ const pending = this.resolvePendingUserInput(toolUseId);
430
+ if (!pending)
431
+ return false;
432
+ this.pendingUserInputs.delete(pending.toolUseId);
433
+ this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
434
+ this.emitToolResult(pending.toolUseId, "Approved");
435
+ if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
436
+ this.setStatus("running");
437
+ }
438
+ return true;
439
+ }
440
+ /**
441
+ * Reject a pending user-input request (McpElicitation fallback).
442
+ */
443
+ rejectUserInput(toolUseId, result) {
444
+ const pending = this.resolvePendingUserInput(toolUseId);
445
+ if (!pending)
446
+ return false;
447
+ this.pendingUserInputs.delete(pending.toolUseId);
448
+ this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
449
+ this.emitToolResult(pending.toolUseId, "Rejected");
450
+ if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
451
+ this.setStatus("running");
452
+ }
453
+ return true;
454
+ }
396
455
  // ---------------------------------------------------------------------------
397
456
  // Plan completion handlers (native collaboration_mode)
398
457
  // ---------------------------------------------------------------------------
@@ -526,9 +585,13 @@ export class CodexProcess extends EventEmitter {
526
585
  : {}),
527
586
  });
528
587
  this.setStatus("idle");
529
- // Fetch skills in background (non-blocking)
588
+ // Fetch skills/apps in background (non-blocking)
530
589
  this._projectPath = projectPath;
531
- void this.fetchSkills(projectPath);
590
+ setTimeout(() => {
591
+ if (!this.stopped) {
592
+ void this.fetchCompletionEntities(projectPath);
593
+ }
594
+ }, 25);
532
595
  await this.runInputLoop(options);
533
596
  }
534
597
  catch (err) {
@@ -560,58 +623,101 @@ export class CodexProcess extends EventEmitter {
560
623
  });
561
624
  this.notify("initialized", {});
562
625
  }
563
- /**
564
- * Fetch skills from Codex app-server via `skills/list` RPC and emit them
565
- * as a `supported_commands` system message so the Flutter client can display
566
- * skill entries alongside built-in slash commands.
567
- */
568
- async fetchSkills(projectPath) {
626
+ async fetchCompletionEntities(projectPath) {
627
+ if (this._completionFetchInFlight) {
628
+ this._completionFetchQueued = true;
629
+ return this._completionFetchInFlight;
630
+ }
631
+ this._completionFetchInFlight = this._fetchCompletionEntitiesInternal(projectPath);
632
+ try {
633
+ await this._completionFetchInFlight;
634
+ }
635
+ finally {
636
+ this._completionFetchInFlight = null;
637
+ if (this._completionFetchQueued && !this.stopped && this._projectPath) {
638
+ this._completionFetchQueued = false;
639
+ void this.fetchCompletionEntities(this._projectPath);
640
+ }
641
+ }
642
+ }
643
+ async _fetchCompletionEntitiesInternal(projectPath) {
569
644
  const TIMEOUT_MS = 10_000;
570
645
  try {
571
- const result = (await Promise.race([
646
+ const skillsResult = (await Promise.race([
572
647
  this.request("skills/list", { cwds: [projectPath] }),
573
648
  new Promise((resolve) => setTimeout(() => resolve(null), TIMEOUT_MS)),
574
649
  ]));
575
- if (this.stopped || !result?.data)
576
- return;
650
+ const appsResult = (await Promise.race([
651
+ this.request("app/list", {
652
+ cursor: null,
653
+ limit: 100,
654
+ threadId: this._threadId ?? undefined,
655
+ forceRefetch: false,
656
+ }),
657
+ new Promise((resolve) => setTimeout(() => resolve(null), TIMEOUT_MS)),
658
+ ]));
577
659
  const skills = [];
578
- const slashCommands = [];
579
660
  const skillMetadata = [];
580
- for (const entry of result.data) {
581
- for (const skill of entry.skills) {
582
- if (skill.enabled) {
583
- skills.push(skill.name);
584
- slashCommands.push(skill.name);
585
- skillMetadata.push({
586
- name: skill.name,
587
- path: skill.path,
588
- description: skill.description,
589
- shortDescription: skill.shortDescription ??
590
- skill.interface?.shortDescription ??
591
- undefined,
592
- enabled: skill.enabled,
593
- scope: skill.scope,
594
- displayName: skill.interface?.displayName ?? undefined,
595
- defaultPrompt: skill.interface?.defaultPrompt ?? undefined,
596
- brandColor: skill.interface?.brandColor ?? undefined,
597
- });
661
+ if (skillsResult?.data) {
662
+ for (const entry of skillsResult.data) {
663
+ for (const skill of entry.skills) {
664
+ if (skill.enabled) {
665
+ skills.push(skill.name);
666
+ skillMetadata.push({
667
+ name: skill.name,
668
+ path: skill.path,
669
+ description: skill.description,
670
+ shortDescription: skill.shortDescription ??
671
+ skill.interface?.shortDescription ??
672
+ undefined,
673
+ enabled: skill.enabled,
674
+ scope: skill.scope,
675
+ displayName: skill.interface?.displayName ?? undefined,
676
+ defaultPrompt: skill.interface?.defaultPrompt ?? undefined,
677
+ brandColor: skill.interface?.brandColor ?? undefined,
678
+ });
679
+ }
598
680
  }
599
681
  }
600
682
  }
601
683
  this._skills = skillMetadata;
602
- if (slashCommands.length > 0) {
603
- console.log(`[codex-process] skills/list returned ${slashCommands.length} skills`);
684
+ const appMetadata = (appsResult?.data ?? [])
685
+ .filter((app) => (app.isAccessible ?? true) && (app.isEnabled ?? true))
686
+ .map((app) => ({
687
+ id: app.id,
688
+ name: app.name,
689
+ description: app.description,
690
+ installUrl: app.installUrl ?? undefined,
691
+ isAccessible: app.isAccessible ?? true,
692
+ isEnabled: app.isEnabled ?? true,
693
+ }));
694
+ this._apps = appMetadata;
695
+ if (this.stopped)
696
+ return;
697
+ const signature = JSON.stringify({
698
+ skills,
699
+ skillMetadata,
700
+ apps: appMetadata.map((app) => app.id),
701
+ appMetadata,
702
+ });
703
+ if (signature === this._lastCompletionEntitiesSignature) {
704
+ return;
705
+ }
706
+ this._lastCompletionEntitiesSignature = signature;
707
+ if (skills.length > 0 || appMetadata.length > 0) {
708
+ console.log(`[codex-process] completion entities loaded: ${skills.length} skills, ${appMetadata.length} apps`);
604
709
  this.emitMessage({
605
710
  type: "system",
606
711
  subtype: "supported_commands",
607
- slashCommands,
608
712
  skills,
609
713
  skillMetadata,
714
+ apps: appMetadata.map((app) => app.id),
715
+ appMetadata,
610
716
  });
611
717
  }
612
718
  }
613
719
  catch (err) {
614
- console.log(`[codex-process] skills/list failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
720
+ console.log(`[codex-process] completion entity fetch failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
615
721
  }
616
722
  }
617
723
  async runInputLoop(options) {
@@ -997,9 +1103,15 @@ export class CodexProcess extends EventEmitter {
997
1103
  break;
998
1104
  }
999
1105
  case "skills/changed": {
1000
- // Re-fetch skills when Codex notifies us of changes
1106
+ // Re-fetch skills/apps when Codex notifies us of changes
1001
1107
  if (this._projectPath) {
1002
- void this.fetchSkills(this._projectPath);
1108
+ void this.fetchCompletionEntities(this._projectPath);
1109
+ }
1110
+ break;
1111
+ }
1112
+ case "app/list/updated": {
1113
+ if (this._projectPath) {
1114
+ void this.fetchCompletionEntities(this._projectPath);
1003
1115
  }
1004
1116
  break;
1005
1117
  }
@@ -1367,12 +1479,19 @@ export class CodexProcess extends EventEmitter {
1367
1479
  async toRpcInput(pendingInput) {
1368
1480
  const input = [];
1369
1481
  const tempPaths = [];
1370
- // Prepend SkillUserInput if a skill reference is attached
1371
- if (pendingInput.skill) {
1482
+ // Prepend structured input items before the free-form text body.
1483
+ for (const skill of pendingInput.skills ?? []) {
1372
1484
  input.push({
1373
1485
  type: "skill",
1374
- name: pendingInput.skill.name,
1375
- path: pendingInput.skill.path,
1486
+ name: skill.name,
1487
+ path: skill.path,
1488
+ });
1489
+ }
1490
+ for (const mention of pendingInput.mentions ?? []) {
1491
+ input.push({
1492
+ type: "mention",
1493
+ name: mention.name,
1494
+ path: mention.path,
1376
1495
  });
1377
1496
  }
1378
1497
  input.push({ type: "text", text: pendingInput.text });