yzcode-cli 1.0.1 → 1.0.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.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Shared transport-layer helpers for bridge message handling.
3
+ *
4
+ * Extracted from replBridge.ts so both the env-based core (initBridgeCore)
5
+ * and the env-less core (initEnvLessBridgeCore) can use the same ingress
6
+ * parsing, control-request handling, and echo-dedup machinery.
7
+ *
8
+ * Everything here is pure — no closure over bridge-specific state. All
9
+ * collaborators (transport, sessionId, UUID sets, callbacks) are passed
10
+ * as params.
11
+ */
12
+
13
+ import { randomUUID } from 'crypto'
14
+ import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
15
+ import type {
16
+ SDKControlRequest,
17
+ SDKControlResponse,
18
+ } from '../entrypoints/sdk/controlTypes.js'
19
+ import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
20
+ import { logEvent } from '../services/analytics/index.js'
21
+ import { EMPTY_USAGE } from '../services/api/emptyUsage.js'
22
+ import type { Message } from '../types/message.js'
23
+ import { normalizeControlMessageKeys } from '../utils/controlMessageCompat.js'
24
+ import { logForDebugging } from '../utils/debug.js'
25
+ import { stripDisplayTagsAllowEmpty } from '../utils/displayTags.js'
26
+ import { errorMessage } from '../utils/errors.js'
27
+ import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
28
+ import { jsonParse } from '../utils/slowOperations.js'
29
+ import type { ReplBridgeTransport } from './replBridgeTransport.js'
30
+
31
+ // ─── Type guards ─────────────────────────────────────────────────────────────
32
+
33
+ /** Type predicate for parsed WebSocket messages. SDKMessage is a
34
+ * discriminated union on `type` — validating the discriminant is
35
+ * sufficient for the predicate; callers narrow further via the union. */
36
+ export function isSDKMessage(value: unknown): value is SDKMessage {
37
+ return (
38
+ value !== null &&
39
+ typeof value === 'object' &&
40
+ 'type' in value &&
41
+ typeof value.type === 'string'
42
+ )
43
+ }
44
+
45
+ /** Type predicate for control_response messages from the server. */
46
+ export function isSDKControlResponse(
47
+ value: unknown,
48
+ ): value is SDKControlResponse {
49
+ return (
50
+ value !== null &&
51
+ typeof value === 'object' &&
52
+ 'type' in value &&
53
+ value.type === 'control_response' &&
54
+ 'response' in value
55
+ )
56
+ }
57
+
58
+ /** Type predicate for control_request messages from the server. */
59
+ export function isSDKControlRequest(
60
+ value: unknown,
61
+ ): value is SDKControlRequest {
62
+ return (
63
+ value !== null &&
64
+ typeof value === 'object' &&
65
+ 'type' in value &&
66
+ value.type === 'control_request' &&
67
+ 'request_id' in value &&
68
+ 'request' in value
69
+ )
70
+ }
71
+
72
+ /**
73
+ * True for message types that should be forwarded to the bridge transport.
74
+ * The server only wants user/assistant turns and slash-command system events;
75
+ * everything else (tool_result, progress, etc.) is internal REPL chatter.
76
+ */
77
+ export function isEligibleBridgeMessage(m: Message): boolean {
78
+ // Virtual messages (REPL inner calls) are display-only — bridge/SDK
79
+ // consumers see the REPL tool_use/result which summarizes the work.
80
+ if ((m.type === 'user' || m.type === 'assistant') && m.isVirtual) {
81
+ return false
82
+ }
83
+ return (
84
+ m.type === 'user' ||
85
+ m.type === 'assistant' ||
86
+ (m.type === 'system' && m.subtype === 'local_command')
87
+ )
88
+ }
89
+
90
+ /**
91
+ * Extract title-worthy text from a Message for onUserMessage. Returns
92
+ * undefined for messages that shouldn't title the session: non-user, meta
93
+ * (nudges), tool results, compact summaries, non-human origins (task
94
+ * notifications, channel messages), or pure display-tag content
95
+ * (<ide_opened_file>, <session-start-hook>, etc.).
96
+ *
97
+ * Synthetic interrupts ([Request interrupted by user]) are NOT filtered here —
98
+ * isSyntheticMessage lives in messages.ts (heavy import, pulls command
99
+ * registry). The initialMessages path in initReplBridge checks it; the
100
+ * writeMessages path reaching an interrupt as the *first* message is
101
+ * implausible (an interrupt implies a prior prompt already flowed through).
102
+ */
103
+ export function extractTitleText(m: Message): string | undefined {
104
+ if (m.type !== 'user' || m.isMeta || m.toolUseResult || m.isCompactSummary)
105
+ return undefined
106
+ if (m.origin && m.origin.kind !== 'human') return undefined
107
+ const content = m.message.content
108
+ let raw: string | undefined
109
+ if (typeof content === 'string') {
110
+ raw = content
111
+ } else {
112
+ for (const block of content) {
113
+ if (block.type === 'text') {
114
+ raw = block.text
115
+ break
116
+ }
117
+ }
118
+ }
119
+ if (!raw) return undefined
120
+ const clean = stripDisplayTagsAllowEmpty(raw)
121
+ return clean || undefined
122
+ }
123
+
124
+ // ─── Ingress routing ─────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Parse an ingress WebSocket message and route it to the appropriate handler.
128
+ * Ignores messages whose UUID is in recentPostedUUIDs (echoes of what we sent)
129
+ * or in recentInboundUUIDs (re-deliveries we've already forwarded — e.g.
130
+ * server replayed history after a transport swap lost the seq-num cursor).
131
+ */
132
+ export function handleIngressMessage(
133
+ data: string,
134
+ recentPostedUUIDs: BoundedUUIDSet,
135
+ recentInboundUUIDs: BoundedUUIDSet,
136
+ onInboundMessage: ((msg: SDKMessage) => void | Promise<void>) | undefined,
137
+ onPermissionResponse?: ((response: SDKControlResponse) => void) | undefined,
138
+ onControlRequest?: ((request: SDKControlRequest) => void) | undefined,
139
+ ): void {
140
+ try {
141
+ const parsed: unknown = normalizeControlMessageKeys(jsonParse(data))
142
+
143
+ // control_response is not an SDKMessage — check before the type guard
144
+ if (isSDKControlResponse(parsed)) {
145
+ logForDebugging('[bridge:repl] Ingress message type=control_response')
146
+ onPermissionResponse?.(parsed)
147
+ return
148
+ }
149
+
150
+ // control_request from the server (initialize, set_model, can_use_tool).
151
+ // Must respond promptly or the server kills the WS (~10-14s timeout).
152
+ if (isSDKControlRequest(parsed)) {
153
+ logForDebugging(
154
+ `[bridge:repl] Inbound control_request subtype=${parsed.request.subtype}`,
155
+ )
156
+ onControlRequest?.(parsed)
157
+ return
158
+ }
159
+
160
+ if (!isSDKMessage(parsed)) return
161
+
162
+ // Check for UUID to detect echoes of our own messages
163
+ const uuid =
164
+ 'uuid' in parsed && typeof parsed.uuid === 'string'
165
+ ? parsed.uuid
166
+ : undefined
167
+
168
+ if (uuid && recentPostedUUIDs.has(uuid)) {
169
+ logForDebugging(
170
+ `[bridge:repl] Ignoring echo: type=${parsed.type} uuid=${uuid}`,
171
+ )
172
+ return
173
+ }
174
+
175
+ // Defensive dedup: drop inbound prompts we've already forwarded. The
176
+ // SSE seq-num carryover (lastTransportSequenceNum) is the primary fix
177
+ // for history-replay; this catches edge cases where that negotiation
178
+ // fails (server ignores from_sequence_num, transport died before
179
+ // receiving any frames, etc).
180
+ if (uuid && recentInboundUUIDs.has(uuid)) {
181
+ logForDebugging(
182
+ `[bridge:repl] Ignoring re-delivered inbound: type=${parsed.type} uuid=${uuid}`,
183
+ )
184
+ return
185
+ }
186
+
187
+ logForDebugging(
188
+ `[bridge:repl] Ingress message type=${parsed.type}${uuid ? ` uuid=${uuid}` : ''}`,
189
+ )
190
+
191
+ if (parsed.type === 'user') {
192
+ if (uuid) recentInboundUUIDs.add(uuid)
193
+ logEvent('tengu_bridge_message_received', {
194
+ is_repl: true,
195
+ })
196
+ // Fire-and-forget — handler may be async (attachment resolution).
197
+ void onInboundMessage?.(parsed)
198
+ } else {
199
+ logForDebugging(
200
+ `[bridge:repl] Ignoring non-user inbound message: type=${parsed.type}`,
201
+ )
202
+ }
203
+ } catch (err) {
204
+ logForDebugging(
205
+ `[bridge:repl] Failed to parse ingress message: ${errorMessage(err)}`,
206
+ )
207
+ }
208
+ }
209
+
210
+ // ─── Server-initiated control requests ───────────────────────────────────────
211
+
212
+ export type ServerControlRequestHandlers = {
213
+ transport: ReplBridgeTransport | null
214
+ sessionId: string
215
+ /**
216
+ * When true, all mutable requests (interrupt, set_model, set_permission_mode,
217
+ * set_max_thinking_tokens) reply with an error instead of false-success.
218
+ * initialize still replies success — the server kills the connection otherwise.
219
+ * Used by the outbound-only bridge mode and the SDK's /bridge subpath so claude.ai sees a
220
+ * proper error instead of "action succeeded but nothing happened locally".
221
+ */
222
+ outboundOnly?: boolean
223
+ onInterrupt?: () => void
224
+ onSetModel?: (model: string | undefined) => void
225
+ onSetMaxThinkingTokens?: (maxTokens: number | null) => void
226
+ onSetPermissionMode?: (
227
+ mode: PermissionMode,
228
+ ) => { ok: true } | { ok: false; error: string }
229
+ }
230
+
231
+ const OUTBOUND_ONLY_ERROR =
232
+ 'This session is outbound-only. Enable Remote Control locally to allow inbound control.'
233
+
234
+ /**
235
+ * Respond to inbound control_request messages from the server. The server
236
+ * sends these for session lifecycle events (initialize, set_model) and
237
+ * for turn-level coordination (interrupt, set_max_thinking_tokens). If we
238
+ * don't respond, the server hangs and kills the WS after ~10-14s.
239
+ *
240
+ * Previously a closure inside initBridgeCore's onWorkReceived; now takes
241
+ * collaborators as params so both cores can use it.
242
+ */
243
+ export function handleServerControlRequest(
244
+ request: SDKControlRequest,
245
+ handlers: ServerControlRequestHandlers,
246
+ ): void {
247
+ const {
248
+ transport,
249
+ sessionId,
250
+ outboundOnly,
251
+ onInterrupt,
252
+ onSetModel,
253
+ onSetMaxThinkingTokens,
254
+ onSetPermissionMode,
255
+ } = handlers
256
+ if (!transport) {
257
+ logForDebugging(
258
+ '[bridge:repl] Cannot respond to control_request: transport not configured',
259
+ )
260
+ return
261
+ }
262
+
263
+ let response: SDKControlResponse
264
+
265
+ // Outbound-only: reply error for mutable requests so claude.ai doesn't show
266
+ // false success. initialize must still succeed (server kills the connection
267
+ // if it doesn't — see comment above).
268
+ if (outboundOnly && request.request.subtype !== 'initialize') {
269
+ response = {
270
+ type: 'control_response',
271
+ response: {
272
+ subtype: 'error',
273
+ request_id: request.request_id,
274
+ error: OUTBOUND_ONLY_ERROR,
275
+ },
276
+ }
277
+ const event = { ...response, session_id: sessionId }
278
+ void transport.write(event)
279
+ logForDebugging(
280
+ `[bridge:repl] Rejected ${request.request.subtype} (outbound-only) request_id=${request.request_id}`,
281
+ )
282
+ return
283
+ }
284
+
285
+ switch (request.request.subtype) {
286
+ case 'initialize':
287
+ // Respond with minimal capabilities — the REPL handles
288
+ // commands, models, and account info itself.
289
+ response = {
290
+ type: 'control_response',
291
+ response: {
292
+ subtype: 'success',
293
+ request_id: request.request_id,
294
+ response: {
295
+ commands: [],
296
+ output_style: 'normal',
297
+ available_output_styles: ['normal'],
298
+ models: [],
299
+ account: {},
300
+ pid: process.pid,
301
+ },
302
+ },
303
+ }
304
+ break
305
+
306
+ case 'set_model':
307
+ onSetModel?.(request.request.model)
308
+ response = {
309
+ type: 'control_response',
310
+ response: {
311
+ subtype: 'success',
312
+ request_id: request.request_id,
313
+ },
314
+ }
315
+ break
316
+
317
+ case 'set_max_thinking_tokens':
318
+ onSetMaxThinkingTokens?.(request.request.max_thinking_tokens)
319
+ response = {
320
+ type: 'control_response',
321
+ response: {
322
+ subtype: 'success',
323
+ request_id: request.request_id,
324
+ },
325
+ }
326
+ break
327
+
328
+ case 'set_permission_mode': {
329
+ // The callback returns a policy verdict so we can send an error
330
+ // control_response without importing isAutoModeGateEnabled /
331
+ // isBypassPermissionsModeDisabled here (bootstrap-isolation). If no
332
+ // callback is registered (daemon context, which doesn't wire this —
333
+ // see daemonBridge.ts), return an error verdict rather than a silent
334
+ // false-success: the mode is never actually applied in that context,
335
+ // so success would lie to the client.
336
+ const verdict = onSetPermissionMode?.(request.request.mode) ?? {
337
+ ok: false,
338
+ error:
339
+ 'set_permission_mode is not supported in this context (onSetPermissionMode callback not registered)',
340
+ }
341
+ if (verdict.ok) {
342
+ response = {
343
+ type: 'control_response',
344
+ response: {
345
+ subtype: 'success',
346
+ request_id: request.request_id,
347
+ },
348
+ }
349
+ } else {
350
+ response = {
351
+ type: 'control_response',
352
+ response: {
353
+ subtype: 'error',
354
+ request_id: request.request_id,
355
+ error: verdict.error,
356
+ },
357
+ }
358
+ }
359
+ break
360
+ }
361
+
362
+ case 'interrupt':
363
+ onInterrupt?.()
364
+ response = {
365
+ type: 'control_response',
366
+ response: {
367
+ subtype: 'success',
368
+ request_id: request.request_id,
369
+ },
370
+ }
371
+ break
372
+
373
+ default:
374
+ // Unknown subtype — respond with error so the server doesn't
375
+ // hang waiting for a reply that never comes.
376
+ response = {
377
+ type: 'control_response',
378
+ response: {
379
+ subtype: 'error',
380
+ request_id: request.request_id,
381
+ error: `REPL bridge does not handle control_request subtype: ${request.request.subtype}`,
382
+ },
383
+ }
384
+ }
385
+
386
+ const event = { ...response, session_id: sessionId }
387
+ void transport.write(event)
388
+ logForDebugging(
389
+ `[bridge:repl] Sent control_response for ${request.request.subtype} request_id=${request.request_id} result=${response.response.subtype}`,
390
+ )
391
+ }
392
+
393
+ // ─── Result message (for session archival on teardown) ───────────────────────
394
+
395
+ /**
396
+ * Build a minimal `SDKResultSuccess` message for session archival.
397
+ * The server needs this event before a WS close to trigger archival.
398
+ */
399
+ export function makeResultMessage(sessionId: string): SDKResultSuccess {
400
+ return {
401
+ type: 'result',
402
+ subtype: 'success',
403
+ duration_ms: 0,
404
+ duration_api_ms: 0,
405
+ is_error: false,
406
+ num_turns: 0,
407
+ result: '',
408
+ stop_reason: null,
409
+ total_cost_usd: 0,
410
+ usage: { ...EMPTY_USAGE },
411
+ modelUsage: {},
412
+ permission_denials: [],
413
+ session_id: sessionId,
414
+ uuid: randomUUID(),
415
+ }
416
+ }
417
+
418
+ // ─── BoundedUUIDSet (echo-dedup ring buffer) ─────────────────────────────────
419
+
420
+ /**
421
+ * FIFO-bounded set backed by a circular buffer. Evicts the oldest entry
422
+ * when capacity is reached, keeping memory usage constant at O(capacity).
423
+ *
424
+ * Messages are added in chronological order, so evicted entries are always
425
+ * the oldest. The caller relies on external ordering (the hook's
426
+ * lastWrittenIndexRef) as the primary dedup — this set is a secondary
427
+ * safety net for echo filtering and race-condition dedup.
428
+ */
429
+ export class BoundedUUIDSet {
430
+ private readonly capacity: number
431
+ private readonly ring: (string | undefined)[]
432
+ private readonly set = new Set<string>()
433
+ private writeIdx = 0
434
+
435
+ constructor(capacity: number) {
436
+ this.capacity = capacity
437
+ this.ring = new Array<string | undefined>(capacity)
438
+ }
439
+
440
+ add(uuid: string): void {
441
+ if (this.set.has(uuid)) return
442
+ // Evict the entry at the current write position (if occupied)
443
+ const evicted = this.ring[this.writeIdx]
444
+ if (evicted !== undefined) {
445
+ this.set.delete(evicted)
446
+ }
447
+ this.ring[this.writeIdx] = uuid
448
+ this.set.add(uuid)
449
+ this.writeIdx = (this.writeIdx + 1) % this.capacity
450
+ }
451
+
452
+ has(uuid: string): boolean {
453
+ return this.set.has(uuid)
454
+ }
455
+
456
+ clear(): void {
457
+ this.set.clear()
458
+ this.ring.fill(undefined)
459
+ this.writeIdx = 0
460
+ }
461
+ }
@@ -0,0 +1,43 @@
1
+ import type { PermissionUpdate } from '../utils/permissions/PermissionUpdateSchema.js'
2
+
3
+ type BridgePermissionResponse = {
4
+ behavior: 'allow' | 'deny'
5
+ updatedInput?: Record<string, unknown>
6
+ updatedPermissions?: PermissionUpdate[]
7
+ message?: string
8
+ }
9
+
10
+ type BridgePermissionCallbacks = {
11
+ sendRequest(
12
+ requestId: string,
13
+ toolName: string,
14
+ input: Record<string, unknown>,
15
+ toolUseId: string,
16
+ description: string,
17
+ permissionSuggestions?: PermissionUpdate[],
18
+ blockedPath?: string,
19
+ ): void
20
+ sendResponse(requestId: string, response: BridgePermissionResponse): void
21
+ /** Cancel a pending control_request so the web app can dismiss its prompt. */
22
+ cancelRequest(requestId: string): void
23
+ onResponse(
24
+ requestId: string,
25
+ handler: (response: BridgePermissionResponse) => void,
26
+ ): () => void // returns unsubscribe
27
+ }
28
+
29
+ /** Type predicate for validating a parsed control_response payload
30
+ * as a BridgePermissionResponse. Checks the required `behavior`
31
+ * discriminant rather than using an unsafe `as` cast. */
32
+ function isBridgePermissionResponse(
33
+ value: unknown,
34
+ ): value is BridgePermissionResponse {
35
+ if (!value || typeof value !== 'object') return false
36
+ return (
37
+ 'behavior' in value &&
38
+ (value.behavior === 'allow' || value.behavior === 'deny')
39
+ )
40
+ }
41
+
42
+ export { isBridgePermissionResponse }
43
+ export type { BridgePermissionCallbacks, BridgePermissionResponse }