@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,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
|
+
}
|