@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,560 @@
1
+ /* eslint-disable max-lines -- runtime sink maps all adapter events into protocol store events */
2
+ import { randomUUID } from 'node:crypto'
3
+ import process from 'node:process'
4
+
5
+ import type { ChatMessage, ChatMessageContent } from '@oneworks/core'
6
+ import {
7
+ DEFAULT_RUNTIME_PROTOCOL_VERSION,
8
+ DEFAULT_SUPPORTED_PROTOCOL_RANGE,
9
+ FileRuntimeStore,
10
+ resolveRuntimeRoot
11
+ } from '@oneworks/runtime-store'
12
+ import type {
13
+ RuntimeCommand,
14
+ RuntimeEvent,
15
+ RuntimeEventDraft,
16
+ RuntimeHeartbeat,
17
+ RuntimeMeta,
18
+ RuntimeState
19
+ } from '@oneworks/runtime-store'
20
+ import type { AdapterOutputEvent } from '@oneworks/types'
21
+ import { extractTextFromMessage } from '@oneworks/utils/chat-message'
22
+
23
+ type RuntimeEventAppendDraft = Omit<RuntimeEventDraft, 'sessionId'> & {
24
+ sessionId?: string
25
+ }
26
+
27
+ export interface RuntimeEventSinkOptions {
28
+ cwd: string
29
+ env?: NodeJS.ProcessEnv
30
+ runtimeId?: string
31
+ sessionId: string
32
+ }
33
+
34
+ export interface CliRuntimeEventSinkOptions extends RuntimeEventSinkOptions {
35
+ adapter?: string
36
+ createdAt?: number
37
+ effort?: RuntimeMeta['effort']
38
+ message?: string
39
+ model?: string
40
+ permissionMode?: RuntimeMeta['permissionMode']
41
+ title?: string
42
+ }
43
+
44
+ export interface RuntimeStartupRecord {
45
+ startAlreadyAcked: boolean
46
+ startCommand?: RuntimeCommand
47
+ shouldRunInitialPrompt: boolean
48
+ }
49
+
50
+ const statusForEvent = (event: RuntimeEvent): RuntimeState['status'] | undefined => {
51
+ switch (event.type) {
52
+ case 'session_started':
53
+ case 'session_resumed':
54
+ case 'message':
55
+ return 'running'
56
+ case 'approval_requested':
57
+ case 'input_requested':
58
+ return 'waiting_input'
59
+ case 'session_completed':
60
+ return 'completed'
61
+ case 'session_failed':
62
+ return 'failed'
63
+ case 'session_stopped':
64
+ return 'stopped'
65
+ case 'status_changed':
66
+ return event.status
67
+ default:
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ const textFromMessage = (message: ChatMessage) => {
73
+ const text = extractTextFromMessage(message).trim()
74
+ return text === '' ? undefined : text
75
+ }
76
+
77
+ const textFromContent = (content: RuntimeEventDraft['content']) => {
78
+ if (typeof content === 'string') {
79
+ const text = content.trim()
80
+ return text === '' ? undefined : text
81
+ }
82
+ if (Array.isArray(content)) {
83
+ const text = content
84
+ .flatMap(item => item.type === 'text' && typeof item.text === 'string' ? [item.text] : [])
85
+ .join('\n')
86
+ .trim()
87
+ return text === '' ? undefined : text
88
+ }
89
+ return undefined
90
+ }
91
+
92
+ const isChatMessageContent = (item: unknown): item is ChatMessageContent => {
93
+ if (item == null || typeof item !== 'object') {
94
+ return false
95
+ }
96
+ const record = item as Record<string, unknown>
97
+ switch (record.type) {
98
+ case 'text':
99
+ return typeof record.text === 'string'
100
+ case 'image':
101
+ return typeof record.url === 'string'
102
+ case 'file':
103
+ return typeof record.path === 'string'
104
+ case 'tool_use':
105
+ return typeof record.id === 'string' && typeof record.name === 'string'
106
+ case 'tool_result':
107
+ return typeof record.tool_use_id === 'string'
108
+ default:
109
+ return false
110
+ }
111
+ }
112
+
113
+ const normalizeCommandContent = (command: RuntimeCommand): RuntimeEventDraft['content'] => {
114
+ const contentItems = command.contentItems
115
+ if (Array.isArray(contentItems) && contentItems.every(isChatMessageContent)) {
116
+ return structuredClone(contentItems)
117
+ }
118
+ return command.content ?? command.message
119
+ }
120
+
121
+ const normalizeSubmitValue = (value: unknown) => {
122
+ if (typeof value === 'string') return value
123
+ if (Array.isArray(value) && value.every(item => typeof item === 'string')) return value.join('\n')
124
+ if (value == null) return undefined
125
+ return JSON.stringify(value)
126
+ }
127
+
128
+ const trimOptional = (value: string | undefined) => {
129
+ const normalized = value?.trim()
130
+ return normalized == null || normalized === '' ? undefined : normalized
131
+ }
132
+
133
+ const buildCliRuntimeTitle = (options: CliRuntimeEventSinkOptions) =>
134
+ trimOptional(options.title) ??
135
+ trimOptional(options.message)?.split('\n')[0]?.trim() ??
136
+ options.sessionId
137
+
138
+ const INITIAL_PROMPT_DELIVERY = 'initial_prompt'
139
+
140
+ export class RuntimeEventSink {
141
+ private queue = Promise.resolve()
142
+ private readonly commandBackedUserMessageTexts: string[] = []
143
+ private lastAssistantText: string | undefined
144
+ private sawFatalError = false
145
+ private readonly runtimeId: string
146
+
147
+ constructor(
148
+ private readonly store: FileRuntimeStore,
149
+ private readonly sessionId: string,
150
+ private readonly meta: RuntimeMeta | undefined,
151
+ runtimeId?: string
152
+ ) {
153
+ this.runtimeId = runtimeId ?? `oneworks-run-${process.pid}`
154
+ }
155
+
156
+ async recordStartup(commands: RuntimeCommand[]): Promise<RuntimeStartupRecord> {
157
+ const startCommand = commands.find(command => command.type === 'start')
158
+ if (startCommand != null) {
159
+ const events = await this.store.session(this.sessionId).replayEvents()
160
+ const alreadyAcked = events.some(event => event.type === 'command_ack' && event.commandId === startCommand.id)
161
+ if (alreadyAcked) {
162
+ await this.writeHeartbeat('running')
163
+ return {
164
+ startAlreadyAcked: true,
165
+ startCommand,
166
+ shouldRunInitialPrompt: false
167
+ }
168
+ }
169
+
170
+ if (startCommand.messageDelivery === 'bridge') {
171
+ await this.writeHeartbeat('running')
172
+ return {
173
+ startAlreadyAcked: false,
174
+ startCommand,
175
+ shouldRunInitialPrompt: false
176
+ }
177
+ }
178
+
179
+ if (startCommand.messageDelivery === INITIAL_PROMPT_DELIVERY) {
180
+ this.rememberCommandBackedUserContent(startCommand.content ?? startCommand.message)
181
+ await this.ackCommand(startCommand)
182
+ await this.writeHeartbeat('running')
183
+ return {
184
+ startAlreadyAcked: false,
185
+ startCommand,
186
+ shouldRunInitialPrompt: true
187
+ }
188
+ }
189
+
190
+ await this.ackCommand(startCommand)
191
+ const content = startCommand.content ?? startCommand.message
192
+ if (content != null && content.trim() !== '') {
193
+ await this.append({
194
+ type: 'message',
195
+ role: 'user',
196
+ content,
197
+ causedByCommandId: startCommand.id,
198
+ commandId: startCommand.commandId,
199
+ visibility: 'private'
200
+ })
201
+ this.rememberCommandBackedUserContent(content)
202
+ }
203
+ await this.writeHeartbeat('running')
204
+ return {
205
+ startAlreadyAcked: false,
206
+ startCommand,
207
+ shouldRunInitialPrompt: true
208
+ }
209
+ }
210
+ await this.writeHeartbeat('running')
211
+ return {
212
+ startAlreadyAcked: false,
213
+ shouldRunInitialPrompt: false
214
+ }
215
+ }
216
+
217
+ ackCommand(command: RuntimeCommand) {
218
+ return this.append({
219
+ type: 'command_ack',
220
+ commandId: command.id,
221
+ causedByCommandId: command.commandId,
222
+ visibility: 'audit',
223
+ message: command.type
224
+ })
225
+ }
226
+
227
+ failCommand(command: RuntimeCommand, error: unknown) {
228
+ return this.append({
229
+ type: 'command_failed',
230
+ commandId: command.id,
231
+ causedByCommandId: command.commandId,
232
+ visibility: 'audit',
233
+ error: error instanceof Error ? error.message : String(error),
234
+ message: command.type
235
+ })
236
+ }
237
+
238
+ recordInputSubmitted(command: RuntimeCommand) {
239
+ return this.append({
240
+ type: 'input_submitted',
241
+ commandId: command.id,
242
+ causedByCommandId: command.commandId,
243
+ requestId: command.requestId,
244
+ visibility: 'audit',
245
+ message: normalizeSubmitValue(command.value ?? command.data)
246
+ })
247
+ }
248
+
249
+ recordMessageCommand(command: RuntimeCommand) {
250
+ const content = normalizeCommandContent(command)
251
+ if (typeof content === 'string' && content.trim() === '') {
252
+ return Promise.resolve()
253
+ }
254
+ if (content == null) {
255
+ return Promise.resolve()
256
+ }
257
+
258
+ this.rememberCommandBackedUserContent(content)
259
+ return this.append({
260
+ type: 'message',
261
+ role: 'user',
262
+ content,
263
+ causedByCommandId: command.id,
264
+ commandId: command.commandId,
265
+ ...(command.source != null ? { source: command.source } : {}),
266
+ visibility: 'private'
267
+ })
268
+ }
269
+
270
+ handleAdapterEvent(event: AdapterOutputEvent) {
271
+ if (event.type === 'init') {
272
+ return this.append({
273
+ type: 'session_started',
274
+ status: 'running',
275
+ title: event.data.title ?? this.meta?.title,
276
+ adapter: event.data.adapter,
277
+ model: event.data.model,
278
+ visibility: 'system'
279
+ })
280
+ }
281
+
282
+ if (event.type === 'message') {
283
+ if (event.data.role === 'user' && this.consumeCommandBackedUserMessage(event.data)) {
284
+ return Promise.resolve()
285
+ }
286
+ const text = textFromMessage(event.data)
287
+ if (event.data.role === 'assistant' && text != null) {
288
+ this.lastAssistantText = text
289
+ }
290
+ return this.append({
291
+ type: 'message',
292
+ role: event.data.role,
293
+ content: event.data.content,
294
+ ...(event.data.model != null ? { model: event.data.model } : {}),
295
+ visibility: 'private'
296
+ })
297
+ }
298
+
299
+ if (event.type === 'interaction_request') {
300
+ return this.append({
301
+ type: 'approval_requested',
302
+ requestId: event.data.id,
303
+ question: event.data.payload.question,
304
+ kind: event.data.payload.kind,
305
+ requestKind: event.data.payload.kind === 'permission' ? 'confirmation' : 'input',
306
+ options: event.data.payload.options as RuntimeEventDraft['options'],
307
+ ...(event.data.payload.permissionContext != null
308
+ ? { permissionContext: event.data.payload.permissionContext }
309
+ : {}),
310
+ publicSummary: event.data.payload.question,
311
+ visibility: 'room'
312
+ })
313
+ }
314
+
315
+ if (event.type === 'error') {
316
+ if (event.data.fatal !== false) {
317
+ this.sawFatalError = true
318
+ }
319
+ return this.append({
320
+ type: event.data.fatal === false ? 'command_failed' : 'session_failed',
321
+ status: event.data.fatal === false ? undefined : 'failed',
322
+ error: event.data.message,
323
+ message: event.data.message,
324
+ summary: event.data.message,
325
+ visibility: 'room'
326
+ })
327
+ }
328
+
329
+ if (event.type === 'stop') {
330
+ if (this.sawFatalError) {
331
+ return Promise.resolve()
332
+ }
333
+ return this.append({
334
+ type: 'session_completed',
335
+ status: 'completed',
336
+ summary: event.data != null ? textFromMessage(event.data) : this.lastAssistantText,
337
+ visibility: 'room'
338
+ })
339
+ }
340
+
341
+ if (event.type === 'exit') {
342
+ if (this.sawFatalError) {
343
+ return Promise.resolve()
344
+ }
345
+ return this.append({
346
+ type: event.data.exitCode == null || event.data.exitCode === 0 ? 'session_completed' : 'session_failed',
347
+ status: event.data.exitCode == null || event.data.exitCode === 0 ? 'completed' : 'failed',
348
+ ...(event.data.stderr != null && event.data.stderr.trim() !== '' ? { error: event.data.stderr } : {}),
349
+ ...(event.data.exitCode == null || event.data.exitCode === 0
350
+ ? {}
351
+ : { summary: event.data.stderr ?? `Exited with code ${event.data.exitCode}` }),
352
+ visibility: 'room'
353
+ })
354
+ }
355
+
356
+ return Promise.resolve()
357
+ }
358
+
359
+ recordFailure(error: unknown) {
360
+ const message = error instanceof Error ? error.message : String(error)
361
+ return this.append({
362
+ type: 'session_failed',
363
+ status: 'failed',
364
+ error: message,
365
+ message,
366
+ summary: message,
367
+ visibility: 'room'
368
+ })
369
+ }
370
+
371
+ append(draft: RuntimeEventAppendDraft) {
372
+ this.queue = this.queue
373
+ .catch(() => {})
374
+ .then(async () => {
375
+ await this.appendNow(draft)
376
+ })
377
+ .catch((error) => {
378
+ console.error(
379
+ `[runtime-protocol] Failed to append runtime event: ${error instanceof Error ? error.message : String(error)}`
380
+ )
381
+ })
382
+ return this.queue
383
+ }
384
+
385
+ async flush() {
386
+ await this.queue
387
+ }
388
+
389
+ private async appendNow(draft: RuntimeEventAppendDraft) {
390
+ const session = this.store.session(this.sessionId)
391
+ const eventDraft = {
392
+ ...this.metadataFields(),
393
+ ...draft,
394
+ sessionId: this.sessionId
395
+ } as RuntimeEventDraft
396
+ const event = await session.appendEvent(eventDraft)
397
+ await this.writeStateForEvent(event)
398
+ return event
399
+ }
400
+
401
+ private metadataFields(): Partial<RuntimeEventAppendDraft> {
402
+ return {
403
+ ...(this.meta?.parentSessionId != null ? { parentSessionId: this.meta.parentSessionId } : {}),
404
+ ...(this.meta?.roomId != null ? { roomId: this.meta.roomId } : {}),
405
+ ...(this.meta?.roomTitle != null ? { roomTitle: this.meta.roomTitle } : {}),
406
+ ...(this.meta?.hostSessionId != null ? { hostSessionId: this.meta.hostSessionId } : {}),
407
+ ...(this.meta?.memberKey != null ? { memberKey: this.meta.memberKey } : {}),
408
+ ...(this.meta?.memberKind != null ? { memberKind: this.meta.memberKind } : {}),
409
+ ...(this.meta?.memberLabel != null ? { memberLabel: this.meta.memberLabel } : {}),
410
+ ...(this.meta?.memberAvatar != null ? { memberAvatar: this.meta.memberAvatar } : {}),
411
+ ...(this.meta?.memberSubtitle != null ? { memberSubtitle: this.meta.memberSubtitle } : {}),
412
+ ...(this.meta?.runId != null ? { runId: this.meta.runId } : {}),
413
+ ...(this.meta?.runTitle != null ? { runTitle: this.meta.runTitle } : {}),
414
+ ...(this.meta?.operationId != null ? { operationId: this.meta.operationId } : {})
415
+ }
416
+ }
417
+
418
+ private rememberCommandBackedUserContent(content: RuntimeEventDraft['content']) {
419
+ const text = textFromContent(content)
420
+ if (text != null) {
421
+ this.commandBackedUserMessageTexts.push(text)
422
+ }
423
+ }
424
+
425
+ private consumeCommandBackedUserMessage(message: ChatMessage) {
426
+ const text = textFromMessage(message)
427
+ if (text == null) {
428
+ return false
429
+ }
430
+ const index = this.commandBackedUserMessageTexts.indexOf(text)
431
+ if (index < 0) {
432
+ return false
433
+ }
434
+ this.commandBackedUserMessageTexts.splice(index, 1)
435
+ return true
436
+ }
437
+
438
+ private async writeStateForEvent(event: RuntimeEvent) {
439
+ const session = this.store.session(this.sessionId)
440
+ const previous = await session.readState()
441
+ const nextStatus = statusForEvent(event) ?? previous?.status ?? 'running'
442
+ const lastMessage = textFromContent(event.content) ?? event.publicSummary ?? event.summary ?? event.message
443
+ const nextState: RuntimeState = {
444
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
445
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
446
+ sessionId: this.sessionId,
447
+ status: nextStatus,
448
+ title: previous?.title ?? this.meta?.title,
449
+ lastSeq: event.seq,
450
+ ...(lastMessage != null && lastMessage.trim() !== '' ? { lastMessage } : previous?.lastMessage != null
451
+ ? { lastMessage: previous.lastMessage }
452
+ : {}),
453
+ ...(event.type === 'approval_requested' && event.requestId != null
454
+ ? { pendingInput: { requestId: event.requestId, kind: event.kind } }
455
+ : nextStatus === 'waiting_input' && previous?.pendingInput != null
456
+ ? { pendingInput: previous.pendingInput }
457
+ : {}),
458
+ updatedAt: event.ts
459
+ }
460
+ await Promise.all([
461
+ session.writeState(nextState),
462
+ this.writeHeartbeat(nextStatus),
463
+ this.store.updateIndex(this.sessionId, {
464
+ storePath: `sessions/${this.sessionId}`,
465
+ cwd: this.meta?.cwd,
466
+ status: nextStatus,
467
+ updatedAt: event.ts
468
+ })
469
+ ])
470
+ }
471
+
472
+ private async writeHeartbeat(status: RuntimeState['status']) {
473
+ const session = this.store.session(this.sessionId)
474
+ const heartbeat: RuntimeHeartbeat = {
475
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
476
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
477
+ sessionId: this.sessionId,
478
+ runtimeId: this.runtimeId,
479
+ pid: process.pid,
480
+ status,
481
+ updatedAt: Date.now()
482
+ }
483
+ await session.writeHeartbeat(heartbeat)
484
+ }
485
+ }
486
+
487
+ export const createRuntimeEventSink = async (options: RuntimeEventSinkOptions) => {
488
+ const root = await resolveRuntimeRoot({ cwd: options.cwd, env: options.env })
489
+ const store = new FileRuntimeStore(root)
490
+ const session = store.session(options.sessionId)
491
+ const meta = await session.readMeta()
492
+ return new RuntimeEventSink(store, options.sessionId, meta, options.runtimeId)
493
+ }
494
+
495
+ export const createCliRuntimeEventSink = async (options: CliRuntimeEventSinkOptions) => {
496
+ const root = await resolveRuntimeRoot({ cwd: options.cwd, env: options.env })
497
+ const store = new FileRuntimeStore(root)
498
+ const ts = options.createdAt ?? Date.now()
499
+ const title = buildCliRuntimeTitle(options)
500
+ const message = trimOptional(options.message)
501
+ const session = await store.createSession(
502
+ {
503
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
504
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
505
+ sessionId: options.sessionId,
506
+ title,
507
+ cwd: options.cwd,
508
+ ...(trimOptional(options.adapter) != null ? { adapter: trimOptional(options.adapter) } : {}),
509
+ ...(options.effort != null ? { effort: options.effort } : {}),
510
+ ...(trimOptional(options.model) != null ? { model: trimOptional(options.model) } : {}),
511
+ ...(options.permissionMode != null ? { permissionMode: options.permissionMode } : {}),
512
+ createdAt: ts
513
+ } satisfies RuntimeMeta
514
+ )
515
+
516
+ await Promise.all([
517
+ session.writeState({
518
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
519
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
520
+ sessionId: options.sessionId,
521
+ status: 'starting',
522
+ title,
523
+ lastSeq: 0,
524
+ updatedAt: ts
525
+ }),
526
+ session.writeHeartbeat({
527
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
528
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
529
+ sessionId: options.sessionId,
530
+ runtimeId: options.runtimeId ?? `oneworks-run-${process.pid}`,
531
+ pid: process.pid,
532
+ status: 'starting',
533
+ updatedAt: ts
534
+ })
535
+ ])
536
+
537
+ const startCommand = message == null
538
+ ? undefined
539
+ : await session.appendCommand(
540
+ {
541
+ protocolVersion: DEFAULT_RUNTIME_PROTOCOL_VERSION,
542
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
543
+ id: `cmd_start_${randomUUID()}`,
544
+ ts,
545
+ sessionId: options.sessionId,
546
+ type: 'start',
547
+ priority: 20,
548
+ source: 'cli',
549
+ content: message,
550
+ message,
551
+ title
552
+ } satisfies RuntimeCommand
553
+ )
554
+
555
+ const sink = new RuntimeEventSink(store, options.sessionId, await session.readMeta(), options.runtimeId)
556
+ if (startCommand != null) {
557
+ await sink.recordStartup([startCommand])
558
+ }
559
+ return sink
560
+ }
@@ -0,0 +1,45 @@
1
+ import process from 'node:process'
2
+
3
+ import type { ExitControllableSession } from './types'
4
+
5
+ export const createSessionExitController = <T extends ExitControllableSession>(params?: {
6
+ exit?: (code: number) => never | void
7
+ }) => {
8
+ let session: T | undefined
9
+ let pendingExitCode: number | undefined
10
+ let didRequestExit = false
11
+ let didExit = false
12
+ const exit = params?.exit ?? process.exit
13
+
14
+ const signalSessionExit = (target: T) => {
15
+ if (pendingExitCode === 0 && typeof target.stop === 'function') {
16
+ target.stop()
17
+ return
18
+ }
19
+ target.kill()
20
+ }
21
+
22
+ return {
23
+ bindSession(nextSession: T) {
24
+ session = nextSession
25
+ if (pendingExitCode == null) return
26
+ signalSessionExit(session)
27
+ },
28
+ requestExit(code: number) {
29
+ if (didRequestExit) return
30
+ didRequestExit = true
31
+ pendingExitCode = code
32
+ if (session != null) {
33
+ signalSessionExit(session)
34
+ }
35
+ },
36
+ handleSessionExit(code: number) {
37
+ if (didExit) return
38
+ didExit = true
39
+ exit(pendingExitCode ?? code)
40
+ },
41
+ getPendingExitCode() {
42
+ return pendingExitCode
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,65 @@
1
+ import type { ChatMessageContent } from '@oneworks/core'
2
+ import type { TaskDetail } from '@oneworks/types'
3
+
4
+ import type { CliSessionResumeRecord } from '#~/session-cache.js'
5
+
6
+ export const RUN_OUTPUT_FORMATS = ['text', 'json', 'stream-json'] as const
7
+ export const RUN_INPUT_FORMATS = ['text', 'json', 'stream-json'] as const
8
+
9
+ export type RunOutputFormat = (typeof RUN_OUTPUT_FORMATS)[number]
10
+ export type RunInputFormat = (typeof RUN_INPUT_FORMATS)[number]
11
+
12
+ export interface RunOptions {
13
+ print: boolean
14
+ printIdleTimeout?: number
15
+ model?: string
16
+ effort?: 'low' | 'medium' | 'high' | 'max'
17
+ adapter?: string
18
+ account?: string
19
+ systemPrompt?: string
20
+ permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
21
+ yolo?: boolean
22
+ sessionId?: string
23
+ resume?: string | boolean
24
+ fork?: string | boolean
25
+ spec?: string
26
+ entity?: string
27
+ workspace?: string
28
+ outputFormat?: RunOutputFormat
29
+ inputFormat?: RunInputFormat
30
+ includeMcpServer?: string[]
31
+ excludeMcpServer?: string[]
32
+ includeTool?: string[]
33
+ excludeTool?: string[]
34
+ includeSkill?: string[]
35
+ excludeSkill?: string[]
36
+ injectDefaultSystemPrompt?: boolean
37
+ defaultOneworksMcpServer?: boolean
38
+ updateSkills?: boolean
39
+ }
40
+
41
+ export interface ActiveCliSessionRecord {
42
+ resume: CliSessionResumeRecord
43
+ detail: TaskDetail
44
+ }
45
+
46
+ export interface ExitControllableSession {
47
+ kill(): void
48
+ stop?(): void
49
+ flushHooks?(): Promise<void>
50
+ }
51
+
52
+ export type CliInputControlEvent =
53
+ | { type: 'message'; content: string | ChatMessageContent[] }
54
+ | { type: 'interrupt' }
55
+ | { type: 'stop' }
56
+ | { type: 'submit_input'; interactionId?: string; data: string | string[] }
57
+
58
+ export interface CliInputSession {
59
+ emit(
60
+ event:
61
+ | { type: 'message'; content: ChatMessageContent[] }
62
+ | { type: 'interrupt' }
63
+ | { type: 'stop' }
64
+ ): void
65
+ }