@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.
- package/LICENSE +21 -0
- package/channel.js +7 -0
- package/cli.js +5 -0
- package/mem.js +7 -0
- package/package.json +59 -0
- package/postinstall.js +75 -0
- package/src/AGENTS.md +169 -0
- package/src/channel-cli.ts +19 -0
- package/src/cli-argv.ts +27 -0
- package/src/cli.ts +63 -0
- package/src/commands/@core/adapter-option.ts +85 -0
- package/src/commands/@core/extra-options.ts +12 -0
- package/src/commands/@core/plugin-install.ts +1 -0
- package/src/commands/@core/plugin-source.ts +1 -0
- package/src/commands/accounts.ts +204 -0
- package/src/commands/adapter/prepare-selection.ts +181 -0
- package/src/commands/adapter/prepare.ts +104 -0
- package/src/commands/adapter.ts +48 -0
- package/src/commands/agent/actions.ts +176 -0
- package/src/commands/agent/runtime-store-commands.ts +56 -0
- package/src/commands/agent/runtime-store-events.ts +23 -0
- package/src/commands/agent/runtime-store-session.ts +170 -0
- package/src/commands/agent/runtime-store-shared.ts +139 -0
- package/src/commands/agent/runtime-store.ts +4 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/benchmark.ts +198 -0
- package/src/commands/channel.ts +594 -0
- package/src/commands/clear.ts +140 -0
- package/src/commands/config/actions.ts +196 -0
- package/src/commands/config/display-state.ts +108 -0
- package/src/commands/config/index.ts +135 -0
- package/src/commands/config/interactive.ts +121 -0
- package/src/commands/config/read-state.ts +56 -0
- package/src/commands/config/section-state.ts +109 -0
- package/src/commands/config/shared.ts +195 -0
- package/src/commands/kill.ts +41 -0
- package/src/commands/list.ts +224 -0
- package/src/commands/memory/context.ts +76 -0
- package/src/commands/memory/entries.ts +131 -0
- package/src/commands/memory/shared.ts +89 -0
- package/src/commands/memory/store.ts +69 -0
- package/src/commands/memory/target.ts +54 -0
- package/src/commands/memory.ts +97 -0
- package/src/commands/plugin.ts +62 -0
- package/src/commands/report-targets.ts +149 -0
- package/src/commands/report.ts +232 -0
- package/src/commands/run/adapter-cli-version.ts +65 -0
- package/src/commands/run/command.ts +982 -0
- package/src/commands/run/input-bridge.ts +108 -0
- package/src/commands/run/input-control.ts +112 -0
- package/src/commands/run/input-decision.ts +88 -0
- package/src/commands/run/options.ts +104 -0
- package/src/commands/run/output.ts +179 -0
- package/src/commands/run/permission-decision.ts +19 -0
- package/src/commands/run/permission-recovery.ts +194 -0
- package/src/commands/run/permission-state.ts +177 -0
- package/src/commands/run/print-idle-timeout.ts +47 -0
- package/src/commands/run/protocol-envelope.ts +111 -0
- package/src/commands/run/protocol-stdio.ts +71 -0
- package/src/commands/run/protocol.ts +391 -0
- package/src/commands/run/runtime-command-bridge.ts +190 -0
- package/src/commands/run/runtime-event-sink.ts +560 -0
- package/src/commands/run/session-exit-controller.ts +45 -0
- package/src/commands/run/types.ts +65 -0
- package/src/commands/run.ts +62 -0
- package/src/commands/session-control.ts +133 -0
- package/src/commands/skills/add-command.ts +88 -0
- package/src/commands/skills/install-command.ts +105 -0
- package/src/commands/skills/install.ts +216 -0
- package/src/commands/skills/progress.ts +126 -0
- package/src/commands/skills/publish-command.ts +85 -0
- package/src/commands/skills/register.ts +17 -0
- package/src/commands/skills/remove-command.ts +102 -0
- package/src/commands/skills/shared.ts +117 -0
- package/src/commands/skills/sync.ts +571 -0
- package/src/commands/skills/types.ts +33 -0
- package/src/commands/skills.ts +1 -0
- package/src/commands/stop.ts +41 -0
- package/src/config.ts +1 -0
- package/src/default-skill-plugin.ts +29 -0
- package/src/env.ts +1 -0
- package/src/hooks/plugins/index.ts +66 -0
- package/src/mem-cli.ts +19 -0
- package/src/session-cache.ts +250 -0
- package/src/session-permission-cache.ts +40 -0
- package/src/utils.ts +25 -0
- 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
|
+
}
|