@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,194 @@
1
+ import type {
2
+ AdapterErrorData,
3
+ AskUserQuestionParams,
4
+ ChatMessage,
5
+ PermissionInteractionContext,
6
+ PermissionInteractionDecision,
7
+ SessionPermissionMode
8
+ } from '@oneworks/types'
9
+ import { normalizePermissionToolName } from '@oneworks/utils'
10
+ import type { PermissionToolSubject } from '@oneworks/utils'
11
+
12
+ import type { CliSessionPermissionRecoveryRecord } from '#~/session-permission-cache.js'
13
+
14
+ const PERMISSION_PROJECT_CONFIG_PATH = '.oo.config.json'
15
+ export const PERMISSION_DECISION_CANCEL = 'cancel'
16
+ export const PERMISSION_RECOVERY_CONTINUE_PROMPT = '权限规则已更新。请继续刚才被权限拦截的工作,并重试被阻止的操作。'
17
+
18
+ const uniqueStrings = (values: string[]) => [...new Set(values)]
19
+
20
+ const buildPermissionOption = (
21
+ label: string,
22
+ value: PermissionInteractionDecision,
23
+ description: string
24
+ ) => ({ label, value, description })
25
+
26
+ const resolvePermissionToolSubject = (value: string): PermissionToolSubject | undefined => (
27
+ normalizePermissionToolName(value)
28
+ )
29
+
30
+ export interface CliPermissionErrorContext {
31
+ subjectKeys: string[]
32
+ deniedTools: string[]
33
+ reasons: string[]
34
+ }
35
+
36
+ export const rememberPermissionToolUses = (cache: Map<string, string>, message: ChatMessage) => {
37
+ if (!Array.isArray(message.content)) return
38
+
39
+ for (const item of message.content) {
40
+ if (
41
+ item == null ||
42
+ typeof item !== 'object' ||
43
+ item.type !== 'tool_use' ||
44
+ typeof item.id !== 'string' ||
45
+ item.id.trim() === ''
46
+ ) {
47
+ continue
48
+ }
49
+
50
+ const rawName = typeof item.name === 'string' && item.name.trim() !== ''
51
+ ? item.name.trim()
52
+ : undefined
53
+ const normalizedToolName = rawName?.startsWith('adapter:')
54
+ ? rawName.split(':').at(-1)?.trim() ?? rawName
55
+ : rawName
56
+ const subject = normalizePermissionToolName(normalizedToolName ?? rawName)
57
+ if (subject == null) continue
58
+
59
+ cache.set(item.id.trim(), subject.key)
60
+ }
61
+
62
+ while (cache.size > 128) {
63
+ const firstKey = cache.keys().next().value as string | undefined
64
+ if (firstKey == null) break
65
+ cache.delete(firstKey)
66
+ }
67
+ }
68
+
69
+ export const extractPermissionErrorContext = (
70
+ error: AdapterErrorData,
71
+ input: { toolUseSubjects?: Map<string, string> } = {}
72
+ ): CliPermissionErrorContext => {
73
+ const details = error.details != null && typeof error.details === 'object'
74
+ ? error.details as Record<string, unknown>
75
+ : {}
76
+ const rawDeniedTools = new Set<string>()
77
+ const reasons = new Set<string>()
78
+
79
+ const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
80
+ for (const denial of permissionDenials) {
81
+ if (denial == null || typeof denial !== 'object') continue
82
+ const record = denial as Record<string, unknown>
83
+ if (typeof record.message === 'string' && record.message.trim() !== '') reasons.add(record.message.trim())
84
+ if (Array.isArray(record.deniedTools)) {
85
+ for (const tool of record.deniedTools) {
86
+ if (typeof tool === 'string' && tool.trim() !== '') rawDeniedTools.add(tool.trim())
87
+ }
88
+ }
89
+ }
90
+
91
+ if (Array.isArray(details.deniedTools)) {
92
+ for (const tool of details.deniedTools) {
93
+ if (typeof tool === 'string' && tool.trim() !== '') rawDeniedTools.add(tool.trim())
94
+ }
95
+ }
96
+ if (typeof details.toolName === 'string' && details.toolName.trim() !== '') {
97
+ rawDeniedTools.add(details.toolName.trim())
98
+ }
99
+ if (typeof error.message === 'string' && error.message.trim() !== '') reasons.add(error.message.trim())
100
+
101
+ const toolUseId = typeof details.toolUseId === 'string' && details.toolUseId.trim() !== ''
102
+ ? details.toolUseId.trim()
103
+ : undefined
104
+ const cachedSubjectKey = toolUseId == null ? undefined : input.toolUseSubjects?.get(toolUseId)
105
+ if (cachedSubjectKey != null && cachedSubjectKey.trim() !== '') rawDeniedTools.add(cachedSubjectKey)
106
+
107
+ const deniedTools = [...rawDeniedTools]
108
+ const subjectKeys = uniqueStrings(
109
+ deniedTools
110
+ .map(tool => resolvePermissionToolSubject(tool)?.key)
111
+ .filter((key): key is string => key != null && key.trim() !== '')
112
+ )
113
+
114
+ return {
115
+ subjectKeys,
116
+ deniedTools,
117
+ reasons: [...reasons]
118
+ }
119
+ }
120
+
121
+ export const resolvePermissionInteractionDecision = (
122
+ answer: string | string[]
123
+ ): PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL | undefined => {
124
+ const normalizedAnswer = Array.isArray(answer) ? answer[0] : answer
125
+ if (typeof normalizedAnswer !== 'string') return undefined
126
+ const raw = normalizedAnswer.trim()
127
+ if (raw === '') return undefined
128
+ if (raw === PERMISSION_DECISION_CANCEL) return PERMISSION_DECISION_CANCEL
129
+
130
+ return raw === 'allow_once' || raw === 'allow_session' || raw === 'allow_project' ||
131
+ raw === 'deny_once' || raw === 'deny_session' || raw === 'deny_project'
132
+ ? raw
133
+ : undefined
134
+ }
135
+
136
+ export const buildPermissionRecoveryRecord = (params: {
137
+ sessionId: string
138
+ adapter?: string
139
+ currentMode?: SessionPermissionMode
140
+ context: CliPermissionErrorContext
141
+ }): CliSessionPermissionRecoveryRecord | undefined => {
142
+ const subjectKeys = uniqueStrings(
143
+ params.context.subjectKeys.map((value) => resolvePermissionToolSubject(value)?.key ?? value)
144
+ )
145
+ .filter(value => value.trim() !== '')
146
+ if (subjectKeys.length === 0) return undefined
147
+
148
+ const primarySubjectKey = subjectKeys[0] ?? 'UnknownTool'
149
+ const subjectLabel = subjectKeys.length <= 1 ? primarySubjectKey : `${subjectKeys[0]} 等 ${subjectKeys.length} 项工具`
150
+ const deniedTools = uniqueStrings([...params.context.deniedTools, ...subjectKeys])
151
+ const permissionContext: PermissionInteractionContext = {
152
+ adapter: params.adapter,
153
+ currentMode: params.currentMode,
154
+ deniedTools,
155
+ reasons: uniqueStrings(params.context.reasons),
156
+ subjectKey: primarySubjectKey,
157
+ subjectLabel,
158
+ scope: 'tool',
159
+ projectConfigPath: PERMISSION_PROJECT_CONFIG_PATH
160
+ }
161
+ const payload: AskUserQuestionParams = {
162
+ sessionId: params.sessionId,
163
+ kind: 'permission',
164
+ question: subjectKeys.length <= 1
165
+ ? `当前任务需要使用 ${subjectLabel} 才能继续,请选择处理方式。`
166
+ : `当前任务涉及 ${subjectKeys.join('、')} 等工具,请选择处理方式。`,
167
+ options: [
168
+ buildPermissionOption('同意本次', 'allow_once', '仅继续这次被拦截的操作。'),
169
+ buildPermissionOption('同意并在当前会话忽略类似调用', 'allow_session', '本会话内同类工具不再重复询问。'),
170
+ buildPermissionOption(
171
+ '同意并在当前项目忽略类似调用',
172
+ 'allow_project',
173
+ `写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
174
+ ),
175
+ buildPermissionOption('拒绝本次', 'deny_once', '拒绝当前这次操作。'),
176
+ buildPermissionOption('拒绝并在当前会话阻止类似调用', 'deny_session', '本会话内同类工具直接拒绝。'),
177
+ buildPermissionOption(
178
+ '拒绝并在当前项目阻止类似调用',
179
+ 'deny_project',
180
+ `写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
181
+ )
182
+ ],
183
+ permissionContext
184
+ }
185
+
186
+ return {
187
+ version: 1,
188
+ sessionId: params.sessionId,
189
+ adapter: params.adapter,
190
+ permissionMode: params.currentMode,
191
+ subjectKeys,
192
+ payload
193
+ }
194
+ }
@@ -0,0 +1,177 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { buildConfigJsonVariables, buildConfigSections, loadConfigState, updateConfigFile } from '@oneworks/config'
6
+ import type { Config, PermissionInteractionDecision } from '@oneworks/types'
7
+ import {
8
+ createEmptySessionPermissionState,
9
+ migrateProjectHomeSegment,
10
+ normalizePermissionToolName,
11
+ normalizeSessionPermissionState,
12
+ resolvePermissionMirrorPath
13
+ } from '@oneworks/utils'
14
+ import type { SessionPermissionState } from '@oneworks/utils'
15
+
16
+ const uniqueStrings = (values: string[]) => [...new Set(values)]
17
+
18
+ const normalizeKeys = (values: string[]) =>
19
+ uniqueStrings(
20
+ values
21
+ .map((value) => normalizePermissionToolName(value)?.key ?? value.trim())
22
+ .filter((value): value is string => value.trim() !== '')
23
+ )
24
+
25
+ const removeKeys = (values: string[], keys: Set<string>) => (
26
+ values.filter((value) => {
27
+ const normalized = normalizePermissionToolName(value)?.key ?? value.trim()
28
+ return !keys.has(normalized)
29
+ })
30
+ )
31
+
32
+ const buildGeneralSectionValue = (config: Config | undefined, permissions: Config['permissions']) => (
33
+ buildConfigSections({
34
+ ...(config ?? {}),
35
+ permissions
36
+ }).general
37
+ )
38
+
39
+ const mutateSessionPermissionState = (
40
+ state: SessionPermissionState,
41
+ keys: string[],
42
+ action: PermissionInteractionDecision
43
+ ) => {
44
+ const targetKeys = normalizeKeys(keys)
45
+ const keySet = new Set(targetKeys)
46
+ const next = normalizeSessionPermissionState(state)
47
+
48
+ if (action === 'allow_once') {
49
+ next.onceAllow = uniqueStrings([...removeKeys(next.onceAllow, keySet), ...targetKeys])
50
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
51
+ return next
52
+ }
53
+ if (action === 'allow_session' || action === 'allow_project') {
54
+ next.allow = uniqueStrings([...removeKeys(next.allow, keySet), ...targetKeys])
55
+ next.deny = removeKeys(next.deny, keySet)
56
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
57
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
58
+ return next
59
+ }
60
+ if (action === 'deny_session' || action === 'deny_project') {
61
+ next.deny = uniqueStrings([...removeKeys(next.deny, keySet), ...targetKeys])
62
+ next.allow = removeKeys(next.allow, keySet)
63
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
64
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
65
+ return next
66
+ }
67
+ return next
68
+ }
69
+
70
+ const loadTaskConfigState = async (cwd: string) =>
71
+ await loadConfigState({
72
+ cwd,
73
+ jsonVariables: buildConfigJsonVariables(cwd, process.env)
74
+ })
75
+
76
+ const readSessionPermissionState = async (cwd: string, adapter: string | undefined, sessionId: string) => {
77
+ if (adapter !== 'claude-code' && adapter !== 'opencode') return createEmptySessionPermissionState()
78
+ await migrateProjectHomeSegment(cwd, process.env, '.mock').catch(() => undefined)
79
+
80
+ try {
81
+ const raw = await readFile(resolvePermissionMirrorPath(cwd, adapter, sessionId), 'utf8')
82
+ const parsed = JSON.parse(raw) as { permissionState?: SessionPermissionState }
83
+ return normalizeSessionPermissionState(parsed.permissionState)
84
+ } catch {
85
+ return createEmptySessionPermissionState()
86
+ }
87
+ }
88
+
89
+ const buildMergedProjectPermissions = async (cwd: string) => {
90
+ const { effectiveProjectConfig, projectConfig, userConfig } = await loadTaskConfigState(cwd)
91
+ const config = effectiveProjectConfig ?? projectConfig
92
+ return {
93
+ allow: [...(config?.permissions?.allow ?? []), ...(userConfig?.permissions?.allow ?? [])],
94
+ deny: [...(config?.permissions?.deny ?? []), ...(userConfig?.permissions?.deny ?? [])],
95
+ ask: [...(config?.permissions?.ask ?? []), ...(userConfig?.permissions?.ask ?? [])]
96
+ }
97
+ }
98
+
99
+ const updateProjectPermissionLists = async (cwd: string, keys: string[], target: 'allow' | 'deny') => {
100
+ const targetKeys = normalizeKeys(keys)
101
+ const keySet = new Set(targetKeys)
102
+ const configState = await loadTaskConfigState(cwd)
103
+ const projectConfig = configState.projectSource?.rawConfig
104
+ const existingPermissions = projectConfig?.permissions ?? {}
105
+ const nextPermissions: Config['permissions'] = {
106
+ ...existingPermissions,
107
+ allow: removeKeys(existingPermissions.allow ?? [], keySet),
108
+ deny: removeKeys(existingPermissions.deny ?? [], keySet),
109
+ ask: removeKeys(existingPermissions.ask ?? [], keySet)
110
+ }
111
+ nextPermissions[target] = uniqueStrings([...(nextPermissions[target] ?? []), ...targetKeys])
112
+
113
+ await updateConfigFile({
114
+ workspaceFolder: cwd,
115
+ source: 'project',
116
+ section: 'general',
117
+ value: buildGeneralSectionValue(projectConfig, nextPermissions)
118
+ })
119
+ }
120
+
121
+ const syncPermissionStateMirror = async (params: {
122
+ cwd: string
123
+ adapter?: string
124
+ sessionId: string
125
+ permissionState: SessionPermissionState
126
+ }) => {
127
+ if (params.adapter !== 'claude-code' && params.adapter !== 'opencode') return
128
+ await migrateProjectHomeSegment(params.cwd, process.env, '.mock').catch(() => undefined)
129
+
130
+ const mirrorPath = resolvePermissionMirrorPath(params.cwd, params.adapter, params.sessionId)
131
+ const projectPermissions = await buildMergedProjectPermissions(params.cwd)
132
+ await mkdir(dirname(mirrorPath), { recursive: true })
133
+ await writeFile(
134
+ mirrorPath,
135
+ `${
136
+ JSON.stringify(
137
+ {
138
+ sessionId: params.sessionId,
139
+ adapter: params.adapter,
140
+ permissionState: normalizeSessionPermissionState(params.permissionState),
141
+ projectPermissions,
142
+ updatedAt: Date.now()
143
+ },
144
+ null,
145
+ 2
146
+ )
147
+ }\n`,
148
+ 'utf8'
149
+ )
150
+ }
151
+
152
+ export const applyCliPermissionDecision = async (params: {
153
+ cwd: string
154
+ sessionId: string
155
+ adapter?: string
156
+ subjectKeys: string[]
157
+ action: PermissionInteractionDecision
158
+ }) => {
159
+ const subjectKeys = normalizeKeys(params.subjectKeys)
160
+ if (subjectKeys.length === 0) return createEmptySessionPermissionState()
161
+
162
+ if (params.action === 'allow_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'allow')
163
+ if (params.action === 'deny_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'deny')
164
+
165
+ const nextState = mutateSessionPermissionState(
166
+ await readSessionPermissionState(params.cwd, params.adapter, params.sessionId),
167
+ subjectKeys,
168
+ params.action
169
+ )
170
+ await syncPermissionStateMirror({
171
+ cwd: params.cwd,
172
+ adapter: params.adapter,
173
+ sessionId: params.sessionId,
174
+ permissionState: nextState
175
+ })
176
+ return nextState
177
+ }
@@ -0,0 +1,47 @@
1
+ import { InvalidArgumentError } from 'commander'
2
+
3
+ export const parsePrintIdleTimeoutSeconds = (value: string) => {
4
+ const seconds = Number(value)
5
+ if (!Number.isFinite(seconds) || seconds <= 0) {
6
+ throw new InvalidArgumentError('--print-idle-timeout must be a positive number of seconds.')
7
+ }
8
+ return seconds
9
+ }
10
+
11
+ export const createPrintIdleTimeoutController = (params: {
12
+ timeoutSeconds: number
13
+ onTimeout: () => void
14
+ setTimer?: typeof setTimeout
15
+ clearTimer?: typeof clearTimeout
16
+ }) => {
17
+ const timeoutMs = Math.ceil(params.timeoutSeconds * 1000)
18
+ const setTimer = params.setTimer ?? setTimeout
19
+ const clearTimer = params.clearTimer ?? clearTimeout
20
+ let timer: ReturnType<typeof setTimeout> | undefined
21
+ let stopped = false
22
+
23
+ const clear = () => {
24
+ if (timer == null) return
25
+ clearTimer(timer)
26
+ timer = undefined
27
+ }
28
+
29
+ const arm = () => {
30
+ if (stopped) return
31
+ clear()
32
+ timer = setTimer(() => {
33
+ timer = undefined
34
+ if (stopped) return
35
+ params.onTimeout()
36
+ }, timeoutMs)
37
+ }
38
+
39
+ return {
40
+ start: arm,
41
+ recordEvent: arm,
42
+ stop: () => {
43
+ stopped = true
44
+ clear()
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,111 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import {
4
+ DEFAULT_SUPPORTED_PROTOCOL_RANGE,
5
+ RuntimeSessionCommandEnvelopeSchema,
6
+ assertProtocolCompatible,
7
+ getCurrentProtocolVersion
8
+ } from '@oneworks/runtime-protocol'
9
+ import type { RuntimeSessionCommandEnvelope, RuntimeSessionResultEnvelope } from '@oneworks/runtime-protocol'
10
+
11
+ const runtimeSessionCommandTypes = [
12
+ 'session.start',
13
+ 'session.message',
14
+ 'session.resume',
15
+ 'session.stop',
16
+ 'session.submit',
17
+ 'session.status',
18
+ 'session.events'
19
+ ] as const satisfies readonly RuntimeSessionCommandEnvelope['type'][]
20
+
21
+ const isRecord = (value: unknown): value is Record<string, unknown> => (
22
+ value != null && typeof value === 'object' && !Array.isArray(value)
23
+ )
24
+
25
+ const defaultProtocolFields = (input: unknown) => {
26
+ if (input == null || typeof input !== 'object' || Array.isArray(input)) return input
27
+ const record = input as Record<string, unknown>
28
+ const payload = isRecord(record.payload) ? record.payload : {}
29
+ return {
30
+ protocolVersion: getCurrentProtocolVersion(),
31
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
32
+ commandId: `cmdreq_${randomUUID()}`,
33
+ source: 'cli',
34
+ ...payload,
35
+ ...record
36
+ }
37
+ }
38
+
39
+ const isRuntimeSessionCommandType = (value: unknown): value is RuntimeSessionCommandEnvelope['type'] =>
40
+ typeof value === 'string' &&
41
+ runtimeSessionCommandTypes.includes(value as RuntimeSessionCommandEnvelope['type'])
42
+
43
+ const readStringField = (input: unknown, field: string) => {
44
+ if (input == null || typeof input !== 'object' || Array.isArray(input)) return undefined
45
+ const value = (input as Record<string, unknown>)[field]
46
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined
47
+ }
48
+
49
+ export const fallbackProtocolCommand = (input: unknown): RuntimeSessionCommandEnvelope => {
50
+ const type = readStringField(input, 'type')
51
+ return {
52
+ protocolVersion: getCurrentProtocolVersion(),
53
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
54
+ commandId: readStringField(input, 'commandId') ?? `cmdreq_${randomUUID()}`,
55
+ type: isRuntimeSessionCommandType(type) ? type : 'session.status',
56
+ sessionId: readStringField(input, 'sessionId')
57
+ }
58
+ }
59
+
60
+ export const parseRuntimeProtocolCommand = (input: unknown) => {
61
+ const command = RuntimeSessionCommandEnvelopeSchema.parse(
62
+ defaultProtocolFields(input)
63
+ ) as RuntimeSessionCommandEnvelope
64
+ assertProtocolCompatible(command.protocolVersion, DEFAULT_SUPPORTED_PROTOCOL_RANGE)
65
+ if (command.supportedProtocolRange != null) {
66
+ assertProtocolCompatible(getCurrentProtocolVersion(), command.supportedProtocolRange)
67
+ }
68
+ return command
69
+ }
70
+
71
+ const resultType = (type: RuntimeSessionCommandEnvelope['type']) =>
72
+ `${type}.result` as RuntimeSessionResultEnvelope['type']
73
+
74
+ export const toSuccessResult = (
75
+ command: RuntimeSessionCommandEnvelope,
76
+ result: Record<string, unknown>
77
+ ): RuntimeSessionResultEnvelope => ({
78
+ protocolVersion: getCurrentProtocolVersion(),
79
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
80
+ commandId: command.commandId,
81
+ type: resultType(command.type),
82
+ ok: true,
83
+ sessionId: typeof result.sessionId === 'string' ? result.sessionId : command.sessionId,
84
+ status: typeof result.status === 'string' ? result.status : undefined,
85
+ storePath: typeof result.storePath === 'string' ? result.storePath : undefined,
86
+ result
87
+ })
88
+
89
+ export const toErrorResult = (
90
+ command: RuntimeSessionCommandEnvelope,
91
+ error: unknown
92
+ ): RuntimeSessionResultEnvelope => ({
93
+ protocolVersion: getCurrentProtocolVersion(),
94
+ supportedProtocolRange: DEFAULT_SUPPORTED_PROTOCOL_RANGE,
95
+ commandId: command.commandId,
96
+ type: resultType(command.type),
97
+ ok: false,
98
+ sessionId: command.sessionId,
99
+ error: error instanceof Error ? error.message : String(error)
100
+ })
101
+
102
+ export const requiredString = (
103
+ command: RuntimeSessionCommandEnvelope,
104
+ field: keyof RuntimeSessionCommandEnvelope
105
+ ) => {
106
+ const value = command[field]
107
+ if (typeof value !== 'string' || value.trim() === '') {
108
+ throw new Error(`${String(field)} is required for ${command.type}.`)
109
+ }
110
+ return value.trim()
111
+ }
@@ -0,0 +1,71 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import type { Readable, Writable } from 'node:stream'
3
+
4
+ import type { RuntimeSessionResultEnvelope } from '@oneworks/runtime-protocol'
5
+
6
+ import { executeRuntimeProtocolCommand } from './protocol'
7
+ import type { ExecuteRuntimeProtocolCommandOptions } from './protocol'
8
+ import type { RunInputFormat, RunOutputFormat } from './types'
9
+
10
+ export interface RuntimeProtocolStdioOptions extends ExecuteRuntimeProtocolCommandOptions {
11
+ inputFormat: RunInputFormat
12
+ outputFormat: RunOutputFormat
13
+ stdin: Readable
14
+ stdout: Writable
15
+ }
16
+
17
+ const readStreamText = async (stream: Readable) => {
18
+ const chunks: Buffer[] = []
19
+ for await (const chunk of stream) {
20
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)))
21
+ }
22
+ return Buffer.concat(chunks).toString('utf8')
23
+ }
24
+
25
+ const parseJsonlCommands = (text: string) =>
26
+ text
27
+ .split('\n')
28
+ .filter(line => line.trim() !== '')
29
+ .map(line => JSON.parse(line) as unknown)
30
+
31
+ const parseCommands = (text: string, format: RunInputFormat) => {
32
+ const trimmed = text.trim()
33
+ if (trimmed === '') return []
34
+ if (format === 'stream-json') return parseJsonlCommands(trimmed)
35
+ if (format !== 'json') {
36
+ throw new Error(`Runtime protocol input does not support --input-format ${format}.`)
37
+ }
38
+
39
+ try {
40
+ const parsed = JSON.parse(trimmed) as unknown
41
+ return Array.isArray(parsed) ? parsed : [parsed]
42
+ } catch (error) {
43
+ if (trimmed.includes('\n')) return parseJsonlCommands(trimmed)
44
+ throw error
45
+ }
46
+ }
47
+
48
+ const writeResults = (
49
+ results: RuntimeSessionResultEnvelope[],
50
+ outputFormat: RunOutputFormat,
51
+ stdout: Writable
52
+ ) => {
53
+ if (outputFormat === 'stream-json') {
54
+ for (const result of results) stdout.write(`${JSON.stringify(result)}\n`)
55
+ return
56
+ }
57
+ if (outputFormat !== 'json') {
58
+ throw new Error('Runtime protocol mode requires --output-format json or stream-json.')
59
+ }
60
+
61
+ stdout.write(`${JSON.stringify(results.length === 1 ? results[0] : results, null, 2)}\n`)
62
+ }
63
+
64
+ export const runRuntimeProtocolStdio = async (options: RuntimeProtocolStdioOptions) => {
65
+ const commands = parseCommands(await readStreamText(options.stdin), options.inputFormat)
66
+ const results = []
67
+ for (const command of commands) {
68
+ results.push(await executeRuntimeProtocolCommand(command, options))
69
+ }
70
+ writeResults(results, options.outputFormat, options.stdout)
71
+ }