@oneworks/cli 0.1.0-alpha.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/channel.js +7 -0
  3. package/cli.js +5 -0
  4. package/mem.js +7 -0
  5. package/package.json +59 -0
  6. package/postinstall.js +75 -0
  7. package/src/AGENTS.md +169 -0
  8. package/src/channel-cli.ts +19 -0
  9. package/src/cli-argv.ts +27 -0
  10. package/src/cli.ts +63 -0
  11. package/src/commands/@core/adapter-option.ts +85 -0
  12. package/src/commands/@core/extra-options.ts +12 -0
  13. package/src/commands/@core/plugin-install.ts +1 -0
  14. package/src/commands/@core/plugin-source.ts +1 -0
  15. package/src/commands/accounts.ts +204 -0
  16. package/src/commands/adapter/prepare-selection.ts +181 -0
  17. package/src/commands/adapter/prepare.ts +104 -0
  18. package/src/commands/adapter.ts +48 -0
  19. package/src/commands/agent/actions.ts +176 -0
  20. package/src/commands/agent/runtime-store-commands.ts +56 -0
  21. package/src/commands/agent/runtime-store-events.ts +23 -0
  22. package/src/commands/agent/runtime-store-session.ts +170 -0
  23. package/src/commands/agent/runtime-store-shared.ts +139 -0
  24. package/src/commands/agent/runtime-store.ts +4 -0
  25. package/src/commands/agent.ts +81 -0
  26. package/src/commands/benchmark.ts +198 -0
  27. package/src/commands/channel.ts +594 -0
  28. package/src/commands/clear.ts +140 -0
  29. package/src/commands/config/actions.ts +196 -0
  30. package/src/commands/config/display-state.ts +108 -0
  31. package/src/commands/config/index.ts +135 -0
  32. package/src/commands/config/interactive.ts +121 -0
  33. package/src/commands/config/read-state.ts +56 -0
  34. package/src/commands/config/section-state.ts +109 -0
  35. package/src/commands/config/shared.ts +195 -0
  36. package/src/commands/kill.ts +41 -0
  37. package/src/commands/list.ts +224 -0
  38. package/src/commands/memory/context.ts +76 -0
  39. package/src/commands/memory/entries.ts +131 -0
  40. package/src/commands/memory/shared.ts +89 -0
  41. package/src/commands/memory/store.ts +69 -0
  42. package/src/commands/memory/target.ts +54 -0
  43. package/src/commands/memory.ts +97 -0
  44. package/src/commands/plugin.ts +62 -0
  45. package/src/commands/report-targets.ts +149 -0
  46. package/src/commands/report.ts +232 -0
  47. package/src/commands/run/adapter-cli-version.ts +65 -0
  48. package/src/commands/run/command.ts +982 -0
  49. package/src/commands/run/input-bridge.ts +108 -0
  50. package/src/commands/run/input-control.ts +112 -0
  51. package/src/commands/run/input-decision.ts +88 -0
  52. package/src/commands/run/options.ts +104 -0
  53. package/src/commands/run/output.ts +179 -0
  54. package/src/commands/run/permission-decision.ts +19 -0
  55. package/src/commands/run/permission-recovery.ts +194 -0
  56. package/src/commands/run/permission-state.ts +177 -0
  57. package/src/commands/run/print-idle-timeout.ts +47 -0
  58. package/src/commands/run/protocol-envelope.ts +111 -0
  59. package/src/commands/run/protocol-stdio.ts +71 -0
  60. package/src/commands/run/protocol.ts +391 -0
  61. package/src/commands/run/runtime-command-bridge.ts +190 -0
  62. package/src/commands/run/runtime-event-sink.ts +560 -0
  63. package/src/commands/run/session-exit-controller.ts +45 -0
  64. package/src/commands/run/types.ts +65 -0
  65. package/src/commands/run.ts +62 -0
  66. package/src/commands/session-control.ts +133 -0
  67. package/src/commands/skills/add-command.ts +88 -0
  68. package/src/commands/skills/install-command.ts +105 -0
  69. package/src/commands/skills/install.ts +216 -0
  70. package/src/commands/skills/progress.ts +126 -0
  71. package/src/commands/skills/publish-command.ts +85 -0
  72. package/src/commands/skills/register.ts +17 -0
  73. package/src/commands/skills/remove-command.ts +102 -0
  74. package/src/commands/skills/shared.ts +117 -0
  75. package/src/commands/skills/sync.ts +571 -0
  76. package/src/commands/skills/types.ts +33 -0
  77. package/src/commands/skills.ts +1 -0
  78. package/src/commands/stop.ts +41 -0
  79. package/src/config.ts +1 -0
  80. package/src/default-skill-plugin.ts +29 -0
  81. package/src/env.ts +1 -0
  82. package/src/hooks/plugins/index.ts +66 -0
  83. package/src/mem-cli.ts +19 -0
  84. package/src/session-cache.ts +250 -0
  85. package/src/session-permission-cache.ts +40 -0
  86. package/src/utils.ts +25 -0
  87. package/src/workspace.ts +12 -0
@@ -0,0 +1,391 @@
1
+ /* eslint-disable max-lines -- protocol command dispatch handles every runtime envelope type in one place */
2
+ import { spawn } from 'node:child_process'
3
+ import process from 'node:process'
4
+
5
+ import type { RuntimeSessionCommandEnvelope, RuntimeSessionResultEnvelope } from '@oneworks/runtime-protocol'
6
+ import { mergeProcessEnvWithProjectEnv } from '@oneworks/utils'
7
+
8
+ import {
9
+ appendRuntimeCommand,
10
+ createRuntimeSession,
11
+ readRuntimeEvents,
12
+ readRuntimeStatus
13
+ } from '../agent/runtime-store'
14
+ import {
15
+ fallbackProtocolCommand,
16
+ parseRuntimeProtocolCommand,
17
+ requiredString,
18
+ toErrorResult,
19
+ toSuccessResult
20
+ } from './protocol-envelope'
21
+
22
+ export interface ExecuteRuntimeProtocolCommandOptions {
23
+ cwd: string
24
+ env?: NodeJS.ProcessEnv
25
+ now?: () => number
26
+ }
27
+
28
+ const trimOptional = (value: string | undefined) => {
29
+ const normalized = value?.trim()
30
+ return normalized == null || normalized === '' ? undefined : normalized
31
+ }
32
+
33
+ const optionalString = (value: unknown) =>
34
+ typeof value === 'string' && value.trim() !== ''
35
+ ? value.trim()
36
+ : undefined
37
+
38
+ const RUNTIME_EFFORTS = new Set(['low', 'medium', 'high', 'max'])
39
+ const RUNTIME_PERMISSION_MODES = new Set(['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'])
40
+ const RUNTIME_RESUME_CONSUMER_STATUSES = new Set([
41
+ 'completed',
42
+ 'failed',
43
+ 'crashed',
44
+ 'stopped',
45
+ 'cancelled',
46
+ 'killed'
47
+ ])
48
+
49
+ const optionalEffort = (value: unknown): RuntimeSessionCommandEnvelope['effort'] => {
50
+ const normalized = optionalString(value)
51
+ return normalized != null && RUNTIME_EFFORTS.has(normalized)
52
+ ? normalized as RuntimeSessionCommandEnvelope['effort']
53
+ : undefined
54
+ }
55
+
56
+ const optionalPermissionMode = (value: unknown): RuntimeSessionCommandEnvelope['permissionMode'] => {
57
+ const normalized = optionalString(value)
58
+ return normalized != null && RUNTIME_PERMISSION_MODES.has(normalized)
59
+ ? normalized as RuntimeSessionCommandEnvelope['permissionMode']
60
+ : undefined
61
+ }
62
+
63
+ const envDefault = (env: NodeJS.ProcessEnv | undefined, key: string) => trimOptional(env?.[key])
64
+
65
+ const pushOption = (args: string[], flag: string, value: string | undefined) => {
66
+ if (value != null) {
67
+ args.push(flag, value)
68
+ }
69
+ }
70
+
71
+ const CODEX_PARENT_RUNTIME_ENV_KEYS = [
72
+ 'CODEX_SANDBOX',
73
+ 'CODEX_SANDBOX_NETWORK_DISABLED',
74
+ 'CODEX_THREAD_ID',
75
+ 'CODEX_SHELL',
76
+ '__ONEWORKS_CODEX_HOOK_RUNTIME__',
77
+ '__ONEWORKS_CODEX_TASK_SESSION_ID__',
78
+ '__ONEWORKS_CODEX_HOOKS_ACTIVE__',
79
+ '__ONEWORKS_HOOK_BRIDGE_ADAPTER__'
80
+ ] as const
81
+
82
+ const shouldDeferRuntimeConsumerToServer = (
83
+ command: RuntimeSessionCommandEnvelope,
84
+ env: NodeJS.ProcessEnv | undefined
85
+ ) =>
86
+ (
87
+ trimOptional(command.hostSessionId) != null ||
88
+ trimOptional(command.roomId) != null ||
89
+ trimOptional(env?.__ONEWORKS_AGENT_ROOM_HOST_SESSION_ID__) != null
90
+ ) &&
91
+ (
92
+ trimOptional(env?.__ONEWORKS_PROJECT_BASE_DIR__) != null ||
93
+ trimOptional(env?.__ONEWORKS_RUNTIME_PROTOCOL_CONSUMER__) != null
94
+ ) &&
95
+ env?.ONEWORKS_RUNTIME_PROTOCOL_FORCE_LOCAL_CONSUMER !== '1' &&
96
+ env?.ONEWORKS_RUNTIME_PROTOCOL_FORCE_LOCAL_CONSUMER !== 'true'
97
+
98
+ const sanitizeRuntimeConsumerEnv = (env: NodeJS.ProcessEnv) => {
99
+ const nextEnv = { ...env }
100
+ for (const key of CODEX_PARENT_RUNTIME_ENV_KEYS) {
101
+ delete nextEnv[key]
102
+ }
103
+ return nextEnv
104
+ }
105
+
106
+ export const shouldStartRuntimeConsumer = (command: RuntimeSessionCommandEnvelope, env?: NodeJS.ProcessEnv) =>
107
+ env != null &&
108
+ command.background !== false &&
109
+ env?.ONEWORKS_RUNTIME_PROTOCOL_DISABLE_CONSUMER !== '1' &&
110
+ env?.ONEWORKS_RUNTIME_PROTOCOL_DISABLE_CONSUMER !== 'true' &&
111
+ !shouldDeferRuntimeConsumerToServer(command, env)
112
+
113
+ export const shouldStartRuntimeResumeConsumer = (params: {
114
+ command: RuntimeSessionCommandEnvelope
115
+ env?: NodeJS.ProcessEnv
116
+ status?: string
117
+ }) =>
118
+ params.env != null &&
119
+ params.command.background !== false &&
120
+ params.env?.ONEWORKS_RUNTIME_PROTOCOL_DISABLE_CONSUMER !== '1' &&
121
+ params.env?.ONEWORKS_RUNTIME_PROTOCOL_DISABLE_CONSUMER !== 'true' &&
122
+ params.status != null &&
123
+ RUNTIME_RESUME_CONSUMER_STATUSES.has(params.status)
124
+
125
+ const startRuntimeConsumer = (params: {
126
+ adapter?: string
127
+ command: RuntimeSessionCommandEnvelope
128
+ cwd: string
129
+ effort?: RuntimeSessionCommandEnvelope['effort']
130
+ entity: string
131
+ env?: NodeJS.ProcessEnv
132
+ message: string
133
+ model?: string
134
+ permissionMode?: RuntimeSessionCommandEnvelope['permissionMode']
135
+ sessionId: string
136
+ }) => {
137
+ const cliEntrypoint = process.argv[1]
138
+ if (cliEntrypoint == null || cliEntrypoint.trim() === '') {
139
+ throw new Error('Unable to resolve One Works CLI entrypoint for runtime consumer.')
140
+ }
141
+
142
+ const args = [
143
+ cliEntrypoint,
144
+ '__run',
145
+ '--print',
146
+ '--output-format',
147
+ 'stream-json',
148
+ '--session-id',
149
+ params.sessionId,
150
+ '--entity',
151
+ params.entity
152
+ ]
153
+ pushOption(args, '--adapter', params.adapter)
154
+ pushOption(args, '--model', params.model)
155
+ pushOption(args, '--effort', params.effort)
156
+ pushOption(args, '--permission-mode', params.permissionMode)
157
+ args.push(params.message)
158
+
159
+ const child = spawn(process.execPath, args, {
160
+ cwd: params.cwd,
161
+ detached: true,
162
+ env: sanitizeRuntimeConsumerEnv(mergeProcessEnvWithProjectEnv({
163
+ ...(params.env ?? {}),
164
+ __ONEWORKS_RUNTIME_PROTOCOL_CONSUMER__: '1',
165
+ __ONEWORKS_RUNTIME_PROTOCOL_SESSION_ID__: params.sessionId
166
+ }, { workspaceFolder: params.cwd })),
167
+ stdio: 'ignore'
168
+ })
169
+ child.unref()
170
+ return child.pid
171
+ }
172
+
173
+ const startRuntimeResumeConsumer = (params: {
174
+ cwd: string
175
+ effort?: RuntimeSessionCommandEnvelope['effort']
176
+ env?: NodeJS.ProcessEnv
177
+ model?: string
178
+ permissionMode?: RuntimeSessionCommandEnvelope['permissionMode']
179
+ sessionId: string
180
+ }) => {
181
+ const cliEntrypoint = process.argv[1]
182
+ if (cliEntrypoint == null || cliEntrypoint.trim() === '') {
183
+ throw new Error('Unable to resolve One Works CLI entrypoint for runtime resume consumer.')
184
+ }
185
+
186
+ const args = [
187
+ cliEntrypoint,
188
+ '__run',
189
+ '--print',
190
+ '--output-format',
191
+ 'stream-json',
192
+ '--resume',
193
+ params.sessionId
194
+ ]
195
+ pushOption(args, '--model', params.model)
196
+ pushOption(args, '--effort', params.effort)
197
+ pushOption(args, '--permission-mode', params.permissionMode)
198
+
199
+ const child = spawn(process.execPath, args, {
200
+ cwd: params.cwd,
201
+ detached: true,
202
+ env: sanitizeRuntimeConsumerEnv(mergeProcessEnvWithProjectEnv({
203
+ ...(params.env ?? {}),
204
+ __ONEWORKS_RUNTIME_PROTOCOL_CONSUMER__: '1',
205
+ __ONEWORKS_RUNTIME_PROTOCOL_SESSION_ID__: params.sessionId
206
+ }, { workspaceFolder: params.cwd })),
207
+ stdio: 'ignore'
208
+ })
209
+ child.unref()
210
+ return child.pid
211
+ }
212
+
213
+ const appendFollowUpRuntimeCommand = async (
214
+ command: RuntimeSessionCommandEnvelope,
215
+ options: ExecuteRuntimeProtocolCommandOptions,
216
+ type: 'resume' | 'send_message'
217
+ ) => {
218
+ const sessionId = requiredString(command, 'sessionId')
219
+ const status = await readRuntimeStatus(options.cwd, sessionId, options.env)
220
+ const result = await appendRuntimeCommand({
221
+ cwd: options.cwd,
222
+ commandId: command.commandId,
223
+ env: options.env,
224
+ memberKey: command.memberKey,
225
+ message: command.message ?? command.content,
226
+ now: options.now,
227
+ priority: command.priority,
228
+ roomId: command.roomId,
229
+ runId: command.runId,
230
+ sessionId,
231
+ source: command.source,
232
+ type
233
+ })
234
+ const consumerPid = shouldStartRuntimeResumeConsumer({
235
+ command,
236
+ env: options.env,
237
+ status: status.status
238
+ })
239
+ ? startRuntimeResumeConsumer({
240
+ cwd: options.cwd,
241
+ effort: optionalEffort(command.effort),
242
+ env: options.env,
243
+ model: optionalString(command.model),
244
+ permissionMode: optionalPermissionMode(command.permissionMode),
245
+ sessionId
246
+ })
247
+ : undefined
248
+
249
+ return {
250
+ ...result,
251
+ ...(consumerPid != null ? { consumerPid } : {})
252
+ }
253
+ }
254
+
255
+ export const executeRuntimeProtocolCommand = async (
256
+ input: RuntimeSessionCommandEnvelope | unknown,
257
+ options: ExecuteRuntimeProtocolCommandOptions
258
+ ): Promise<RuntimeSessionResultEnvelope> => {
259
+ let command: RuntimeSessionCommandEnvelope
260
+ try {
261
+ command = parseRuntimeProtocolCommand(input)
262
+ } catch (error) {
263
+ return toErrorResult(fallbackProtocolCommand(input), error)
264
+ }
265
+
266
+ try {
267
+ switch (command.type) {
268
+ case 'session.start': {
269
+ const hostSessionId = trimOptional(command.hostSessionId) ??
270
+ trimOptional(options.env?.__ONEWORKS_AGENT_ROOM_HOST_SESSION_ID__)
271
+ const roomId = trimOptional(command.roomId) ?? trimOptional(options.env?.__ONEWORKS_AGENT_ROOM_ID__)
272
+ const roomTitle = trimOptional(command.roomTitle) ?? trimOptional(options.env?.__ONEWORKS_AGENT_ROOM_TITLE__)
273
+ const adapter = optionalString(command.adapter) ??
274
+ envDefault(options.env, '__ONEWORKS_RUNTIME_PROTOCOL_DEFAULT_ADAPTER__')
275
+ const model = optionalString(command.model) ??
276
+ envDefault(options.env, '__ONEWORKS_RUNTIME_PROTOCOL_DEFAULT_MODEL__')
277
+ const effort = optionalEffort(command.effort) ??
278
+ optionalEffort(envDefault(options.env, '__ONEWORKS_RUNTIME_PROTOCOL_DEFAULT_EFFORT__'))
279
+ const permissionMode = optionalPermissionMode(command.permissionMode) ??
280
+ optionalPermissionMode(envDefault(options.env, '__ONEWORKS_RUNTIME_PROTOCOL_DEFAULT_PERMISSION_MODE__'))
281
+ const entity = requiredString(command, 'entity')
282
+ const message = command.message ?? command.content ?? ''
283
+ const result = await createRuntimeSession({
284
+ cwd: options.cwd,
285
+ commandId: command.commandId,
286
+ entity,
287
+ adapter,
288
+ effort,
289
+ model,
290
+ env: options.env,
291
+ hostSessionId,
292
+ memberAvatar: command.memberAvatar,
293
+ memberKey: command.memberKey,
294
+ memberLabel: command.memberLabel,
295
+ message,
296
+ now: options.now,
297
+ parentSessionId: command.parentSessionId ?? hostSessionId,
298
+ priority: command.priority,
299
+ roomId,
300
+ roomTitle,
301
+ runId: command.runId,
302
+ runTitle: command.runTitle,
303
+ sessionId: command.sessionId,
304
+ source: command.source,
305
+ permissionMode,
306
+ title: command.title
307
+ })
308
+ const consumerPid = shouldStartRuntimeConsumer(command, options.env)
309
+ ? startRuntimeConsumer({
310
+ adapter,
311
+ command,
312
+ cwd: options.cwd,
313
+ effort,
314
+ entity,
315
+ env: options.env,
316
+ message,
317
+ model,
318
+ permissionMode,
319
+ sessionId: result.sessionId
320
+ })
321
+ : undefined
322
+ return toSuccessResult(
323
+ command,
324
+ {
325
+ ...result,
326
+ ...(consumerPid != null ? { consumerPid } : {})
327
+ }
328
+ )
329
+ }
330
+ case 'session.message':
331
+ return toSuccessResult(
332
+ command,
333
+ await appendFollowUpRuntimeCommand(command, options, 'send_message')
334
+ )
335
+ case 'session.resume':
336
+ return toSuccessResult(
337
+ command,
338
+ await appendFollowUpRuntimeCommand(command, options, 'resume')
339
+ )
340
+ case 'session.stop':
341
+ return toSuccessResult(
342
+ command,
343
+ await appendRuntimeCommand({
344
+ cwd: options.cwd,
345
+ commandId: command.commandId,
346
+ env: options.env,
347
+ memberKey: command.memberKey,
348
+ now: options.now,
349
+ priority: command.priority,
350
+ roomId: command.roomId,
351
+ runId: command.runId,
352
+ sessionId: requiredString(command, 'sessionId'),
353
+ source: command.source,
354
+ type: command.mode === 'force' || command.mode === 'kill' ? 'kill' : 'stop'
355
+ })
356
+ )
357
+ case 'session.submit':
358
+ return toSuccessResult(
359
+ command,
360
+ await appendRuntimeCommand({
361
+ cwd: options.cwd,
362
+ commandId: command.commandId,
363
+ data: command.data,
364
+ env: options.env,
365
+ memberKey: command.memberKey,
366
+ now: options.now,
367
+ priority: command.priority,
368
+ requestId: command.requestId ?? command.interactionId,
369
+ roomId: command.roomId,
370
+ runId: command.runId,
371
+ sessionId: requiredString(command, 'sessionId'),
372
+ source: command.source,
373
+ type: 'submit_input',
374
+ value: command.value
375
+ })
376
+ )
377
+ case 'session.status':
378
+ return toSuccessResult(
379
+ command,
380
+ await readRuntimeStatus(options.cwd, requiredString(command, 'sessionId'), options.env)
381
+ )
382
+ case 'session.events':
383
+ return toSuccessResult(command, {
384
+ sessionId: requiredString(command, 'sessionId'),
385
+ events: await readRuntimeEvents(options.cwd, requiredString(command, 'sessionId'), options.env)
386
+ })
387
+ }
388
+ } catch (error) {
389
+ return toErrorResult(command, error)
390
+ }
391
+ }
@@ -0,0 +1,190 @@
1
+ import type { ChatMessageContent } from '@oneworks/core'
2
+ import { orderRuntimeCommands } from '@oneworks/runtime-store'
3
+ import type { RuntimeCommand } from '@oneworks/runtime-store'
4
+
5
+ import { readRuntimeCommands, readRuntimeEvents } from '../agent/runtime-store'
6
+ import type { RuntimeEventSink } from './runtime-event-sink'
7
+ import type { CliInputSession } from './types'
8
+
9
+ export interface RuntimeCommandBridgeSession extends CliInputSession {
10
+ kill?: () => void
11
+ }
12
+
13
+ export interface RuntimeCommandBridgeOptions {
14
+ cwd: string
15
+ env?: NodeJS.ProcessEnv
16
+ intervalMs?: number
17
+ session: RuntimeCommandBridgeSession
18
+ sessionId: string
19
+ sink: RuntimeEventSink
20
+ submitInput?: (params: { interactionId?: string; data: string | string[] }) => Promise<void>
21
+ }
22
+
23
+ const toMessageContent = (command: RuntimeCommand): ChatMessageContent[] => {
24
+ const runtimeContentItems = normalizeCommandContentItems(command.runtimeContentItems)
25
+ if (runtimeContentItems != null) {
26
+ return runtimeContentItems
27
+ }
28
+
29
+ const contentItems = normalizeCommandContentItems(command.contentItems)
30
+ if (contentItems != null) {
31
+ return contentItems
32
+ }
33
+
34
+ const content = typeof command.runtimeMessage === 'string'
35
+ ? command.runtimeMessage
36
+ : command.content ?? command.message ?? ''
37
+ return [{ type: 'text', text: content }]
38
+ }
39
+
40
+ const isChatMessageContent = (item: unknown): item is ChatMessageContent => {
41
+ if (item == null || typeof item !== 'object') {
42
+ return false
43
+ }
44
+ const record = item as Record<string, unknown>
45
+ switch (record.type) {
46
+ case 'text':
47
+ return typeof record.text === 'string'
48
+ case 'image':
49
+ return typeof record.url === 'string'
50
+ case 'file':
51
+ return typeof record.path === 'string'
52
+ case 'tool_use':
53
+ return typeof record.id === 'string' && typeof record.name === 'string'
54
+ case 'tool_result':
55
+ return typeof record.tool_use_id === 'string'
56
+ default:
57
+ return false
58
+ }
59
+ }
60
+
61
+ const normalizeCommandContentItems = (value: unknown): ChatMessageContent[] | undefined => {
62
+ if (!Array.isArray(value) || !value.every(isChatMessageContent)) {
63
+ return undefined
64
+ }
65
+ return structuredClone(value)
66
+ }
67
+
68
+ const toSubmitData = (command: RuntimeCommand): string | string[] => {
69
+ if (typeof command.data === 'string' || Array.isArray(command.data)) {
70
+ return command.data
71
+ }
72
+ if (typeof command.value === 'string') {
73
+ return command.value
74
+ }
75
+ if (Array.isArray(command.value) && command.value.every(item => typeof item === 'string')) {
76
+ return command.value
77
+ }
78
+ if (command.value == null) {
79
+ throw new Error('submit_input command requires value or data.')
80
+ }
81
+ return JSON.stringify(command.value)
82
+ }
83
+
84
+ const dispatchCommand = async (
85
+ command: RuntimeCommand,
86
+ options: RuntimeCommandBridgeOptions
87
+ ) => {
88
+ switch (command.type) {
89
+ case 'send_message':
90
+ case 'resume':
91
+ await options.sink.ackCommand(command)
92
+ await options.sink.recordMessageCommand(command)
93
+ options.session.emit({ type: 'message', content: toMessageContent(command) })
94
+ return
95
+ case 'start':
96
+ if (command.messageDelivery === 'bridge') {
97
+ await options.sink.ackCommand(command)
98
+ await options.sink.recordMessageCommand(command)
99
+ options.session.emit({ type: 'message', content: toMessageContent(command) })
100
+ }
101
+ return
102
+ case 'submit_input':
103
+ await options.sink.ackCommand(command)
104
+ await options.submitInput?.({
105
+ interactionId: command.requestId ?? command.interactionId,
106
+ data: toSubmitData(command)
107
+ })
108
+ await options.sink.recordInputSubmitted(command)
109
+ return
110
+ case 'stop':
111
+ await options.sink.ackCommand(command)
112
+ options.session.emit({ type: 'stop' })
113
+ return
114
+ case 'kill':
115
+ await options.sink.ackCommand(command)
116
+ if (options.session.kill != null) {
117
+ options.session.kill()
118
+ } else {
119
+ options.session.emit({ type: 'stop' })
120
+ }
121
+ break
122
+ }
123
+ }
124
+
125
+ const PROCESSED_COMMAND_EVENT_TYPES = new Set([
126
+ 'command_ack',
127
+ 'command_failed',
128
+ 'command_cancelled',
129
+ 'input_submitted'
130
+ ])
131
+
132
+ const readProcessedCommandIds = async (options: RuntimeCommandBridgeOptions) => {
133
+ const processed = new Set<string>()
134
+ const events = await readRuntimeEvents(options.cwd, options.sessionId, options.env)
135
+ for (const event of events) {
136
+ if (
137
+ typeof event.commandId === 'string' &&
138
+ event.commandId.trim() !== '' &&
139
+ PROCESSED_COMMAND_EVENT_TYPES.has(event.type)
140
+ ) {
141
+ processed.add(event.commandId)
142
+ }
143
+ }
144
+ return processed
145
+ }
146
+
147
+ export const attachRuntimeCommandBridge = async (options: RuntimeCommandBridgeOptions) => {
148
+ const processed = await readProcessedCommandIds(options)
149
+ let stopped = false
150
+ let tickQueue = Promise.resolve()
151
+
152
+ const tick = async () => {
153
+ const commands = await readRuntimeCommands(options.cwd, options.sessionId, options.env)
154
+ const pending = orderRuntimeCommands(commands.filter(command => !processed.has(command.id)))
155
+ for (const command of pending) {
156
+ processed.add(command.id)
157
+ try {
158
+ await dispatchCommand(command, options)
159
+ } catch (error) {
160
+ await options.sink.failCommand(command, error)
161
+ }
162
+ }
163
+ }
164
+
165
+ const runTick = () => {
166
+ tickQueue = tickQueue
167
+ .catch(() => {})
168
+ .then(() => tick())
169
+ .catch((error) => {
170
+ console.error(
171
+ `[runtime-protocol] Failed to process runtime command: ${
172
+ error instanceof Error ? error.message : String(error)
173
+ }`
174
+ )
175
+ })
176
+ }
177
+
178
+ runTick()
179
+ const timer = setInterval(() => {
180
+ if (!stopped) {
181
+ runTick()
182
+ }
183
+ }, options.intervalMs ?? 1000)
184
+
185
+ return async () => {
186
+ stopped = true
187
+ clearInterval(timer)
188
+ await tickQueue
189
+ }
190
+ }