@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,108 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer'
|
|
2
|
+
import { createInterface } from 'node:readline'
|
|
3
|
+
|
|
4
|
+
import type { ChatMessageContent } from '@oneworks/core'
|
|
5
|
+
|
|
6
|
+
import { parseCliInputControlEvent } from './input-control'
|
|
7
|
+
import type { CliInputControlEvent, CliInputSession, RunInputFormat } from './types'
|
|
8
|
+
|
|
9
|
+
const dispatchCliInputControlEvent = async (params: {
|
|
10
|
+
session: CliInputSession
|
|
11
|
+
event: CliInputControlEvent
|
|
12
|
+
submitInput?: (event: Extract<CliInputControlEvent, { type: 'submit_input' }>) => void | Promise<void>
|
|
13
|
+
}) => {
|
|
14
|
+
const { session, event, submitInput } = params
|
|
15
|
+
if (event.type === 'interrupt') {
|
|
16
|
+
session.emit({ type: 'interrupt' })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
if (event.type === 'stop') {
|
|
20
|
+
session.emit({ type: 'stop' })
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
if (event.type === 'submit_input') {
|
|
24
|
+
if (submitInput == null) {
|
|
25
|
+
throw new TypeError('The current session does not support submit_input events.')
|
|
26
|
+
}
|
|
27
|
+
await submitInput(event)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = typeof event.content === 'string'
|
|
32
|
+
? [{ type: 'text', text: event.content } satisfies ChatMessageContent]
|
|
33
|
+
: event.content ?? []
|
|
34
|
+
|
|
35
|
+
session.emit({
|
|
36
|
+
type: 'message',
|
|
37
|
+
content
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const attachInputBridge = (params: {
|
|
42
|
+
format: RunInputFormat
|
|
43
|
+
session: CliInputSession
|
|
44
|
+
stdin: NodeJS.ReadStream
|
|
45
|
+
onError: (message: string) => void
|
|
46
|
+
onInputClosed: () => void
|
|
47
|
+
submitInput?: (event: Extract<CliInputControlEvent, { type: 'submit_input' }>) => void | Promise<void>
|
|
48
|
+
}) => {
|
|
49
|
+
const { format, session, stdin, onError, onInputClosed, submitInput } = params
|
|
50
|
+
stdin.setEncoding('utf8')
|
|
51
|
+
|
|
52
|
+
if (format === 'stream-json') {
|
|
53
|
+
const rl = createInterface({ input: stdin, crlfDelay: Infinity })
|
|
54
|
+
const onEnd = () => {
|
|
55
|
+
onInputClosed()
|
|
56
|
+
}
|
|
57
|
+
rl.on('line', (line) => {
|
|
58
|
+
const trimmed = line.trim()
|
|
59
|
+
if (trimmed === '') return
|
|
60
|
+
try {
|
|
61
|
+
void dispatchCliInputControlEvent({
|
|
62
|
+
session,
|
|
63
|
+
event: parseCliInputControlEvent(JSON.parse(trimmed)),
|
|
64
|
+
submitInput
|
|
65
|
+
}).catch((error) => {
|
|
66
|
+
onError(error instanceof Error ? error.message : String(error))
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
onError(error instanceof Error ? error.message : String(error))
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
stdin.once('end', onEnd)
|
|
73
|
+
return () => {
|
|
74
|
+
stdin.off('end', onEnd)
|
|
75
|
+
rl.close()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const chunks: string[] = []
|
|
80
|
+
const onData = (chunk: string | Buffer) => {
|
|
81
|
+
chunks.push(String(chunk))
|
|
82
|
+
}
|
|
83
|
+
const onEnd = () => {
|
|
84
|
+
const raw = chunks.join('')
|
|
85
|
+
if (raw.trim() !== '') {
|
|
86
|
+
try {
|
|
87
|
+
const payload = format === 'json' ? JSON.parse(raw) : raw
|
|
88
|
+
void dispatchCliInputControlEvent({
|
|
89
|
+
session,
|
|
90
|
+
event: parseCliInputControlEvent(payload),
|
|
91
|
+
submitInput
|
|
92
|
+
}).catch((error) => {
|
|
93
|
+
onError(error instanceof Error ? error.message : String(error))
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
onError(error instanceof Error ? error.message : String(error))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
onInputClosed()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
stdin.on('data', onData)
|
|
103
|
+
stdin.once('end', onEnd)
|
|
104
|
+
return () => {
|
|
105
|
+
stdin.off('data', onData)
|
|
106
|
+
stdin.off('end', onEnd)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ChatMessageContent } from '@oneworks/core'
|
|
2
|
+
|
|
3
|
+
import type { CliInputControlEvent, RunInputFormat } from './types'
|
|
4
|
+
|
|
5
|
+
export const supportsPrintInteractionResponses = (format: RunInputFormat | undefined) => format === 'stream-json'
|
|
6
|
+
|
|
7
|
+
export const supportsRuntimeInteractionResponses = (
|
|
8
|
+
format: RunInputFormat | undefined,
|
|
9
|
+
isRuntimeProtocolConsumer: boolean
|
|
10
|
+
) => isRuntimeProtocolConsumer || supportsPrintInteractionResponses(format)
|
|
11
|
+
|
|
12
|
+
const isChatTextContent = (value: unknown): value is Extract<ChatMessageContent, { type: 'text' }> => (
|
|
13
|
+
value != null &&
|
|
14
|
+
typeof value === 'object' &&
|
|
15
|
+
(value as { type?: unknown }).type === 'text' &&
|
|
16
|
+
typeof (value as { text?: unknown }).text === 'string'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const isChatImageContent = (value: unknown): value is Extract<ChatMessageContent, { type: 'image' }> => (
|
|
20
|
+
value != null &&
|
|
21
|
+
typeof value === 'object' &&
|
|
22
|
+
(value as { type?: unknown }).type === 'image' &&
|
|
23
|
+
typeof (value as { url?: unknown }).url === 'string'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const normalizeChatInputContent = (value: unknown): string | ChatMessageContent[] => {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
const items = value.filter((item): item is ChatMessageContent =>
|
|
33
|
+
isChatTextContent(item) || isChatImageContent(item)
|
|
34
|
+
)
|
|
35
|
+
if (items.length > 0) {
|
|
36
|
+
return items
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error('Unsupported message content. Use a string or an array of text/image content items.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const normalizeSubmitInputData = (value: unknown): string | string[] => {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
const trimmed = value.trim()
|
|
46
|
+
if (trimmed !== '') {
|
|
47
|
+
return trimmed
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
const items = value
|
|
53
|
+
.filter((item): item is string => typeof item === 'string' && item.trim() !== '')
|
|
54
|
+
.map(item => item.trim())
|
|
55
|
+
if (items.length > 0) {
|
|
56
|
+
return items
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error('Submit input requires a non-empty string or string array in "data" or "response".')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const parseCliInputControlEvent = (value: unknown): CliInputControlEvent => {
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
return {
|
|
66
|
+
type: 'message',
|
|
67
|
+
content: value
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return {
|
|
73
|
+
type: 'message',
|
|
74
|
+
content: normalizeChatInputContent(value)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value == null || typeof value !== 'object') {
|
|
79
|
+
throw new Error('Invalid input payload. Expected a string, array, or object.')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const record = value as Record<string, unknown>
|
|
83
|
+
const type = typeof record.type === 'string' ? record.type : 'message'
|
|
84
|
+
if (type === 'interrupt') {
|
|
85
|
+
return { type: 'interrupt' }
|
|
86
|
+
}
|
|
87
|
+
if (type === 'stop') {
|
|
88
|
+
return { type: 'stop' }
|
|
89
|
+
}
|
|
90
|
+
if (type === 'submit_input' || type === 'interaction_response' || type === 'respond_interaction') {
|
|
91
|
+
const data = record.data ?? record.response ?? record.answer ?? record.value
|
|
92
|
+
return {
|
|
93
|
+
type: 'submit_input',
|
|
94
|
+
interactionId: typeof record.interactionId === 'string' && record.interactionId.trim() !== ''
|
|
95
|
+
? record.interactionId.trim()
|
|
96
|
+
: undefined,
|
|
97
|
+
data: normalizeSubmitInputData(data)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (type === 'message' || type === 'user_message') {
|
|
101
|
+
const content = record.content ?? record.text ?? record.message
|
|
102
|
+
if (content == null) {
|
|
103
|
+
throw new Error('Message input requires "content" or "text".')
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
type: 'message',
|
|
107
|
+
content: normalizeChatInputContent(content)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`Unsupported input event type: ${type}`)
|
|
112
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer'
|
|
2
|
+
import { createInterface } from 'node:readline'
|
|
3
|
+
|
|
4
|
+
import { parseCliInputControlEvent } from './input-control'
|
|
5
|
+
import type { RunInputFormat } from './types'
|
|
6
|
+
|
|
7
|
+
type PermissionDecisionInput = NodeJS.ReadableStream & {
|
|
8
|
+
setEncoding(encoding: BufferEncoding): void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const resolveDecisionFromPayload = (payload: unknown) => {
|
|
12
|
+
const event = parseCliInputControlEvent(payload)
|
|
13
|
+
if (event.type === 'submit_input') {
|
|
14
|
+
return event.data
|
|
15
|
+
}
|
|
16
|
+
if (event.type === 'message' && typeof event.content === 'string' && event.content.trim() !== '') {
|
|
17
|
+
return event.content.trim()
|
|
18
|
+
}
|
|
19
|
+
throw new TypeError('Permission recovery expects submit_input or a plain text decision like allow_once.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const readFirstLine = async (stdin: PermissionDecisionInput) =>
|
|
23
|
+
await new Promise<string>((resolve, reject) => {
|
|
24
|
+
const rl = createInterface({ input: stdin, crlfDelay: Infinity })
|
|
25
|
+
let settled = false
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
stdin.off('error', onError)
|
|
28
|
+
rl.close()
|
|
29
|
+
}
|
|
30
|
+
const onError = (error: Error) => {
|
|
31
|
+
if (settled) return
|
|
32
|
+
settled = true
|
|
33
|
+
cleanup()
|
|
34
|
+
reject(error)
|
|
35
|
+
}
|
|
36
|
+
rl.on('line', (line) => {
|
|
37
|
+
const trimmed = line.trim()
|
|
38
|
+
if (trimmed === '') return
|
|
39
|
+
settled = true
|
|
40
|
+
cleanup()
|
|
41
|
+
resolve(trimmed)
|
|
42
|
+
})
|
|
43
|
+
rl.once('close', () => {
|
|
44
|
+
if (settled) return
|
|
45
|
+
settled = true
|
|
46
|
+
reject(new TypeError('Permission recovery input closed before any decision was provided.'))
|
|
47
|
+
})
|
|
48
|
+
stdin.once('error', onError)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const readAll = async (stdin: PermissionDecisionInput) =>
|
|
52
|
+
await new Promise<string>((resolve, reject) => {
|
|
53
|
+
const chunks: string[] = []
|
|
54
|
+
const onData = (chunk: string | Buffer) => {
|
|
55
|
+
chunks.push(String(chunk))
|
|
56
|
+
}
|
|
57
|
+
const onEnd = () => {
|
|
58
|
+
cleanup()
|
|
59
|
+
resolve(chunks.join(''))
|
|
60
|
+
}
|
|
61
|
+
const onError = (error: Error) => {
|
|
62
|
+
cleanup()
|
|
63
|
+
reject(error)
|
|
64
|
+
}
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
stdin.off('data', onData)
|
|
67
|
+
stdin.off('end', onEnd)
|
|
68
|
+
stdin.off('error', onError)
|
|
69
|
+
}
|
|
70
|
+
stdin.on('data', onData)
|
|
71
|
+
stdin.once('end', onEnd)
|
|
72
|
+
stdin.once('error', onError)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const readCliPermissionDecision = async (params: {
|
|
76
|
+
format: RunInputFormat
|
|
77
|
+
stdin: PermissionDecisionInput
|
|
78
|
+
}) => {
|
|
79
|
+
params.stdin.setEncoding('utf8')
|
|
80
|
+
if (params.format === 'stream-json') {
|
|
81
|
+
return resolveDecisionFromPayload(JSON.parse(await readFirstLine(params.stdin)))
|
|
82
|
+
}
|
|
83
|
+
if (params.format === 'json') {
|
|
84
|
+
return resolveDecisionFromPayload(JSON.parse((await readAll(params.stdin)).trim()))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resolveDecisionFromPayload(await readFirstLine(params.stdin))
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Command, OptionValueSource } from 'commander'
|
|
2
|
+
|
|
3
|
+
import type { CliSessionResumeRecord } from '#~/session-cache.js'
|
|
4
|
+
|
|
5
|
+
import type { RunOptions, RunOutputFormat } from './types'
|
|
6
|
+
|
|
7
|
+
const getRunMode = (print: boolean): 'stream' | 'direct' => print ? 'stream' : 'direct'
|
|
8
|
+
|
|
9
|
+
export const getOutputFormat = (
|
|
10
|
+
value: RunOutputFormat | undefined,
|
|
11
|
+
source: OptionValueSource | undefined,
|
|
12
|
+
fallback: RunOutputFormat
|
|
13
|
+
) => source === 'default' ? fallback : (value ?? 'text')
|
|
14
|
+
|
|
15
|
+
export const resolveRunMode = (
|
|
16
|
+
print: boolean,
|
|
17
|
+
source: OptionValueSource | undefined,
|
|
18
|
+
fallback: 'stream' | 'direct'
|
|
19
|
+
) => source === 'default' ? 'direct' : getRunMode(print)
|
|
20
|
+
|
|
21
|
+
export const resolveInjectDefaultSystemPromptOption = (
|
|
22
|
+
value: boolean | undefined,
|
|
23
|
+
source: OptionValueSource | undefined
|
|
24
|
+
) => source === 'default' ? undefined : value
|
|
25
|
+
|
|
26
|
+
export const resolveDefaultOneworksMcpServerOption = (
|
|
27
|
+
value: boolean | undefined,
|
|
28
|
+
source: OptionValueSource | undefined
|
|
29
|
+
) => source === 'default' ? undefined : value
|
|
30
|
+
|
|
31
|
+
export const resolvePermissionModeOption = (
|
|
32
|
+
permissionMode: RunOptions['permissionMode'],
|
|
33
|
+
yolo: RunOptions['yolo']
|
|
34
|
+
): RunOptions['permissionMode'] => {
|
|
35
|
+
if (yolo !== true) return permissionMode
|
|
36
|
+
if (permissionMode != null && permissionMode !== 'bypassPermissions') {
|
|
37
|
+
throw new Error('--yolo cannot be combined with --permission-mode unless the mode is bypassPermissions.')
|
|
38
|
+
}
|
|
39
|
+
return 'bypassPermissions'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const getDisallowedResumeFlags = (
|
|
43
|
+
opts: RunOptions,
|
|
44
|
+
command: Command
|
|
45
|
+
) => {
|
|
46
|
+
const disallowed: string[] = []
|
|
47
|
+
|
|
48
|
+
if (opts.adapter) disallowed.push('--adapter')
|
|
49
|
+
if (opts.account) disallowed.push('--account')
|
|
50
|
+
if (opts.systemPrompt) disallowed.push('--system-prompt')
|
|
51
|
+
if (opts.sessionId) disallowed.push('--session-id')
|
|
52
|
+
if (opts.spec) disallowed.push('--spec')
|
|
53
|
+
if (opts.entity) disallowed.push('--entity')
|
|
54
|
+
if (opts.workspace) disallowed.push('--workspace')
|
|
55
|
+
if ((opts.includeMcpServer?.length ?? 0) > 0) disallowed.push('--include-mcp-server')
|
|
56
|
+
if ((opts.excludeMcpServer?.length ?? 0) > 0) disallowed.push('--exclude-mcp-server')
|
|
57
|
+
if ((opts.includeSkill?.length ?? 0) > 0) disallowed.push('--include-skill')
|
|
58
|
+
if ((opts.excludeSkill?.length ?? 0) > 0) disallowed.push('--exclude-skill')
|
|
59
|
+
if (command.getOptionValueSource('injectDefaultSystemPrompt') !== 'default') {
|
|
60
|
+
disallowed.push('--no-inject-default-system-prompt')
|
|
61
|
+
}
|
|
62
|
+
if (command.getOptionValueSource('defaultOneworksMcpServer') !== 'default') {
|
|
63
|
+
disallowed.push('--no-default-oneworks-mcp-server')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return disallowed
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const mergeListConfig = (
|
|
70
|
+
config: { include?: string[]; exclude?: string[] } | undefined,
|
|
71
|
+
includeOpts: string[] | undefined,
|
|
72
|
+
excludeOpts: string[] | undefined
|
|
73
|
+
) => {
|
|
74
|
+
const include = config?.include || includeOpts
|
|
75
|
+
? [
|
|
76
|
+
...(config?.include ?? []),
|
|
77
|
+
...(includeOpts ?? [])
|
|
78
|
+
]
|
|
79
|
+
: undefined
|
|
80
|
+
|
|
81
|
+
const exclude = config?.exclude || excludeOpts
|
|
82
|
+
? [
|
|
83
|
+
...(config?.exclude ?? []),
|
|
84
|
+
...(excludeOpts ?? [])
|
|
85
|
+
]
|
|
86
|
+
: undefined
|
|
87
|
+
|
|
88
|
+
return include || exclude
|
|
89
|
+
? {
|
|
90
|
+
include,
|
|
91
|
+
exclude
|
|
92
|
+
}
|
|
93
|
+
: undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const resolveResumeAdapterOptions = (
|
|
97
|
+
cached: CliSessionResumeRecord['adapterOptions'],
|
|
98
|
+
opts: Pick<RunOptions, 'model' | 'effort' | 'includeTool' | 'excludeTool'>
|
|
99
|
+
): CliSessionResumeRecord['adapterOptions'] => ({
|
|
100
|
+
...cached,
|
|
101
|
+
...(opts.model != null ? { model: opts.model } : {}),
|
|
102
|
+
...(opts.effort != null ? { effort: opts.effort } : {}),
|
|
103
|
+
tools: mergeListConfig(cached.tools, opts.includeTool, opts.excludeTool)
|
|
104
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ChatMessage } from '@oneworks/core'
|
|
2
|
+
import type { AdapterErrorData, AdapterOutputEvent, AskUserQuestionParams, TaskDetail } from '@oneworks/types'
|
|
3
|
+
import { extractTextFromMessage } from '@oneworks/utils/chat-message'
|
|
4
|
+
|
|
5
|
+
import type { RunOutputFormat } from './types'
|
|
6
|
+
|
|
7
|
+
export const getPrintableAssistantText = (message: ChatMessage | undefined) => {
|
|
8
|
+
if (message?.role !== 'assistant') return undefined
|
|
9
|
+
|
|
10
|
+
const text = extractTextFromMessage(message).trim()
|
|
11
|
+
return text === '' ? undefined : text
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const resolvePrintableStopText = (
|
|
15
|
+
message: ChatMessage | undefined,
|
|
16
|
+
lastAssistantText: string | undefined
|
|
17
|
+
) => getPrintableAssistantText(message) ?? lastAssistantText
|
|
18
|
+
|
|
19
|
+
export const getAdapterErrorMessage = (data: AdapterErrorData) => {
|
|
20
|
+
if (data.code === 'permission_required') {
|
|
21
|
+
const details = data.details != null && typeof data.details === 'object'
|
|
22
|
+
? data.details as { permissionDenials?: Array<{ message?: string; deniedTools?: string[] }> }
|
|
23
|
+
: undefined
|
|
24
|
+
const deniedTools = [
|
|
25
|
+
...new Set(
|
|
26
|
+
(details?.permissionDenials ?? []).flatMap(item => Array.isArray(item.deniedTools) ? item.deniedTools : [])
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
return deniedTools.length > 0
|
|
30
|
+
? `${data.message}\nDenied tools: ${deniedTools.join(', ')}`
|
|
31
|
+
: data.message
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const details = data.details == null
|
|
35
|
+
? undefined
|
|
36
|
+
: typeof data.details === 'string'
|
|
37
|
+
? data.details
|
|
38
|
+
: JSON.stringify(data.details, null, 2)
|
|
39
|
+
|
|
40
|
+
return details ? `${data.message}\n${details}` : data.message
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const getAdapterInteractionMessage = (payload: AskUserQuestionParams) => {
|
|
44
|
+
const header = payload.kind === 'permission' ? 'Permission required' : 'Input required'
|
|
45
|
+
const optionLines = (payload.options ?? [])
|
|
46
|
+
.map((option) => {
|
|
47
|
+
const value = option.value ?? option.label
|
|
48
|
+
return option.description != null && option.description.trim() !== ''
|
|
49
|
+
? `- ${value}: ${option.description}`
|
|
50
|
+
: `- ${value}`
|
|
51
|
+
})
|
|
52
|
+
return optionLines.length > 0
|
|
53
|
+
? `${header}\n${payload.question}\nOptions:\n${optionLines.join('\n')}`
|
|
54
|
+
: `${header}\n${payload.question}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const shouldPrintResumeHint = (input: {
|
|
58
|
+
shouldPrintOutput: boolean
|
|
59
|
+
status: TaskDetail['status']
|
|
60
|
+
}) => !(input.shouldPrintOutput && input.status === 'completed')
|
|
61
|
+
|
|
62
|
+
export const handlePrintEvent = (input: {
|
|
63
|
+
event: AdapterOutputEvent
|
|
64
|
+
outputFormat: RunOutputFormat
|
|
65
|
+
lastAssistantText: string | undefined
|
|
66
|
+
didExitAfterError: boolean
|
|
67
|
+
exitOnInteractionRequest?: boolean
|
|
68
|
+
stopExitsStreamJson?: boolean
|
|
69
|
+
log: (message: string) => void
|
|
70
|
+
errorLog: (message: string) => void
|
|
71
|
+
requestExit: (code: number) => void
|
|
72
|
+
}) => {
|
|
73
|
+
const nextAssistantText = input.event.type === 'message'
|
|
74
|
+
? (getPrintableAssistantText(input.event.data) ?? input.lastAssistantText)
|
|
75
|
+
: input.lastAssistantText
|
|
76
|
+
|
|
77
|
+
if (input.event.type === 'interaction_request') {
|
|
78
|
+
switch (input.outputFormat) {
|
|
79
|
+
case 'stream-json':
|
|
80
|
+
case 'json':
|
|
81
|
+
input.log(JSON.stringify(input.event, null, 2))
|
|
82
|
+
if (input.exitOnInteractionRequest === true) {
|
|
83
|
+
input.requestExit(1)
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
lastAssistantText: nextAssistantText,
|
|
87
|
+
didExitAfterError: input.didExitAfterError || input.exitOnInteractionRequest === true
|
|
88
|
+
}
|
|
89
|
+
case 'text':
|
|
90
|
+
input.errorLog(getAdapterInteractionMessage(input.event.data.payload))
|
|
91
|
+
if (input.exitOnInteractionRequest === true) {
|
|
92
|
+
input.requestExit(1)
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
lastAssistantText: nextAssistantText,
|
|
96
|
+
didExitAfterError: input.didExitAfterError || input.exitOnInteractionRequest === true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (input.event.type === 'error') {
|
|
102
|
+
const isFatal = input.event.data.fatal !== false
|
|
103
|
+
|
|
104
|
+
switch (input.outputFormat) {
|
|
105
|
+
case 'stream-json':
|
|
106
|
+
input.log(JSON.stringify(input.event, null, 2))
|
|
107
|
+
if (isFatal) {
|
|
108
|
+
input.requestExit(1)
|
|
109
|
+
return {
|
|
110
|
+
lastAssistantText: nextAssistantText,
|
|
111
|
+
didExitAfterError: true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
lastAssistantText: nextAssistantText,
|
|
116
|
+
didExitAfterError: input.didExitAfterError
|
|
117
|
+
}
|
|
118
|
+
case 'json':
|
|
119
|
+
if (isFatal) {
|
|
120
|
+
input.log(JSON.stringify(input.event, null, 2))
|
|
121
|
+
input.requestExit(1)
|
|
122
|
+
return {
|
|
123
|
+
lastAssistantText: nextAssistantText,
|
|
124
|
+
didExitAfterError: true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
lastAssistantText: nextAssistantText,
|
|
129
|
+
didExitAfterError: input.didExitAfterError
|
|
130
|
+
}
|
|
131
|
+
case 'text':
|
|
132
|
+
if (isFatal) {
|
|
133
|
+
input.errorLog(getAdapterErrorMessage(input.event.data))
|
|
134
|
+
input.requestExit(1)
|
|
135
|
+
return {
|
|
136
|
+
lastAssistantText: nextAssistantText,
|
|
137
|
+
didExitAfterError: true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
lastAssistantText: nextAssistantText,
|
|
142
|
+
didExitAfterError: input.didExitAfterError
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
switch (input.outputFormat) {
|
|
148
|
+
case 'stream-json':
|
|
149
|
+
input.log(JSON.stringify(input.event, null, 2))
|
|
150
|
+
if (input.event.type === 'stop' && input.stopExitsStreamJson === true && !input.didExitAfterError) {
|
|
151
|
+
input.requestExit(0)
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
lastAssistantText: nextAssistantText,
|
|
155
|
+
didExitAfterError: input.didExitAfterError
|
|
156
|
+
}
|
|
157
|
+
case 'json':
|
|
158
|
+
if (input.event.type === 'stop' && !input.didExitAfterError) {
|
|
159
|
+
input.log(JSON.stringify(input.event, null, 2))
|
|
160
|
+
input.requestExit(0)
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
lastAssistantText: nextAssistantText,
|
|
164
|
+
didExitAfterError: input.didExitAfterError
|
|
165
|
+
}
|
|
166
|
+
case 'text':
|
|
167
|
+
if (input.event.type === 'stop' && !input.didExitAfterError) {
|
|
168
|
+
const output = resolvePrintableStopText(input.event.data, nextAssistantText)
|
|
169
|
+
if (output != null) {
|
|
170
|
+
input.log(output)
|
|
171
|
+
}
|
|
172
|
+
input.requestExit(0)
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
lastAssistantText: nextAssistantText,
|
|
176
|
+
didExitAfterError: input.didExitAfterError
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PermissionInteractionDecision } from '@oneworks/types'
|
|
2
|
+
|
|
3
|
+
import { PERMISSION_DECISION_CANCEL } from './permission-recovery'
|
|
4
|
+
|
|
5
|
+
export const shouldApplyPermissionDecision = (
|
|
6
|
+
decision: PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL
|
|
7
|
+
) => decision !== PERMISSION_DECISION_CANCEL && decision !== 'deny_once'
|
|
8
|
+
|
|
9
|
+
export const shouldClearPermissionRecoveryCache = (
|
|
10
|
+
decision: PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL
|
|
11
|
+
) => decision !== PERMISSION_DECISION_CANCEL
|
|
12
|
+
|
|
13
|
+
export const isTerminalPermissionDecision = (
|
|
14
|
+
decision: PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL
|
|
15
|
+
) => (
|
|
16
|
+
decision === 'deny_once' ||
|
|
17
|
+
decision === 'deny_session' ||
|
|
18
|
+
decision === 'deny_project'
|
|
19
|
+
)
|