@vibe-forge/adapter-opencode 0.1.2
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/AGENTS.md +13 -0
- package/LICENSE +21 -0
- package/__tests__/runtime-common.spec.ts +95 -0
- package/__tests__/runtime-config.spec.ts +124 -0
- package/__tests__/runtime-permissions.spec.ts +181 -0
- package/__tests__/runtime-test-helpers.ts +142 -0
- package/__tests__/session-runtime-config.spec.ts +156 -0
- package/__tests__/session-runtime-direct.spec.ts +141 -0
- package/__tests__/session-runtime-stream.spec.ts +181 -0
- package/package.json +59 -0
- package/src/AGENTS.md +38 -0
- package/src/adapter-config.ts +21 -0
- package/src/icon.ts +17 -0
- package/src/index.ts +11 -0
- package/src/models.ts +24 -0
- package/src/paths.ts +30 -0
- package/src/runtime/common/agent.ts +11 -0
- package/src/runtime/common/inline-config.ts +45 -0
- package/src/runtime/common/mcp.ts +47 -0
- package/src/runtime/common/model.ts +115 -0
- package/src/runtime/common/object-utils.ts +35 -0
- package/src/runtime/common/permission-node.ts +119 -0
- package/src/runtime/common/permissions.ts +73 -0
- package/src/runtime/common/prompt.ts +56 -0
- package/src/runtime/common/session-records.ts +61 -0
- package/src/runtime/common/tools.ts +129 -0
- package/src/runtime/common.ts +17 -0
- package/src/runtime/init.ts +55 -0
- package/src/runtime/session/child-env.ts +100 -0
- package/src/runtime/session/direct.ts +109 -0
- package/src/runtime/session/process.ts +55 -0
- package/src/runtime/session/shared.ts +72 -0
- package/src/runtime/session/skill-config.ts +84 -0
- package/src/runtime/session/stream.ts +198 -0
- package/src/runtime/session.ts +13 -0
- package/src/schema.ts +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdapterQueryOptions } from '@vibe-forge/core/adapter'
|
|
2
|
+
|
|
3
|
+
export const resolveOpenCodeAgent = (params: {
|
|
4
|
+
agent?: string
|
|
5
|
+
planAgent?: string | false
|
|
6
|
+
permissionMode?: AdapterQueryOptions['permissionMode']
|
|
7
|
+
}) => {
|
|
8
|
+
if (params.permissionMode !== 'plan') return params.agent
|
|
9
|
+
if (params.planAgent === false) return params.agent
|
|
10
|
+
return params.planAgent ?? params.agent ?? 'plan'
|
|
11
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Config } from '@vibe-forge/core'
|
|
2
|
+
import type { AdapterQueryOptions } from '@vibe-forge/core/adapter'
|
|
3
|
+
|
|
4
|
+
import { mapMcpServersToOpenCode } from './mcp'
|
|
5
|
+
import { deepMerge } from './object-utils'
|
|
6
|
+
import { mergePermissionNodes, normalizePermissionNode, rewritePermissionMode } from './permission-node'
|
|
7
|
+
import { buildToolPermissionConfig, mapPermissionModeToOpenCode } from './permissions'
|
|
8
|
+
import { buildToolConfig } from './tools'
|
|
9
|
+
|
|
10
|
+
export const buildInlineConfigContent = (params: {
|
|
11
|
+
adapterConfigContent?: Record<string, unknown>
|
|
12
|
+
envConfigContent?: Record<string, unknown>
|
|
13
|
+
permissionMode?: AdapterQueryOptions['permissionMode']
|
|
14
|
+
tools?: AdapterQueryOptions['tools']
|
|
15
|
+
mcpServers?: AdapterQueryOptions['mcpServers']
|
|
16
|
+
availableMcpServers?: Config['mcpServers']
|
|
17
|
+
systemPromptFile?: string
|
|
18
|
+
providerConfig?: Record<string, unknown>
|
|
19
|
+
}) => {
|
|
20
|
+
const mergedBaseConfig = deepMerge(params.envConfigContent ?? {}, params.adapterConfigContent ?? {})
|
|
21
|
+
const inheritedPermission = normalizePermissionNode(mergedBaseConfig.permission)
|
|
22
|
+
const hasToolFilter = (params.tools?.include?.length ?? 0) > 0 || (params.tools?.exclude?.length ?? 0) > 0
|
|
23
|
+
const basePermission = params.permissionMode === 'dontAsk' || params.permissionMode === 'bypassPermissions'
|
|
24
|
+
? rewritePermissionMode(inheritedPermission, params.permissionMode)
|
|
25
|
+
: mergePermissionNodes(inheritedPermission, mapPermissionModeToOpenCode(params.permissionMode))
|
|
26
|
+
const permission = params.permissionMode == null && !hasToolFilter
|
|
27
|
+
? undefined
|
|
28
|
+
: mergePermissionNodes(basePermission, buildToolPermissionConfig(params.tools, basePermission))
|
|
29
|
+
const tools = buildToolConfig(params.tools)
|
|
30
|
+
const mcp = mapMcpServersToOpenCode(params.availableMcpServers, params.mcpServers)
|
|
31
|
+
const inheritedInstructions = Array.isArray(mergedBaseConfig.instructions)
|
|
32
|
+
? mergedBaseConfig.instructions.filter((value): value is string => typeof value === 'string' && value.trim() !== '')
|
|
33
|
+
: []
|
|
34
|
+
const instructions = params.systemPromptFile == null
|
|
35
|
+
? inheritedInstructions
|
|
36
|
+
: [...inheritedInstructions, params.systemPromptFile]
|
|
37
|
+
|
|
38
|
+
return deepMerge(mergedBaseConfig, {
|
|
39
|
+
...(instructions.length > 0 ? { instructions } : {}),
|
|
40
|
+
...(permission != null ? { permission } : {}),
|
|
41
|
+
...(tools ? { tools } : {}),
|
|
42
|
+
...(mcp ? { mcp } : {}),
|
|
43
|
+
...(params.providerConfig ? { provider: params.providerConfig } : {})
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Config } from '@vibe-forge/core'
|
|
2
|
+
import type { AdapterQueryOptions } from '@vibe-forge/core/adapter'
|
|
3
|
+
|
|
4
|
+
const filterMcpServers = (
|
|
5
|
+
mcpServers: Config['mcpServers'] | undefined,
|
|
6
|
+
selection: AdapterQueryOptions['mcpServers']
|
|
7
|
+
) => {
|
|
8
|
+
if (mcpServers == null) return {}
|
|
9
|
+
|
|
10
|
+
const include = selection?.include != null && selection.include.length > 0
|
|
11
|
+
? new Set(selection.include)
|
|
12
|
+
: undefined
|
|
13
|
+
const exclude = new Set(selection?.exclude ?? [])
|
|
14
|
+
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.entries(mcpServers).filter(([name]) => (include == null || include.has(name)) && !exclude.has(name))
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const mapMcpServersToOpenCode = (
|
|
21
|
+
mcpServers: Config['mcpServers'] | undefined,
|
|
22
|
+
selection: AdapterQueryOptions['mcpServers']
|
|
23
|
+
) => {
|
|
24
|
+
const filtered = filterMcpServers(mcpServers, selection)
|
|
25
|
+
const result: Record<string, Record<string, unknown>> = {}
|
|
26
|
+
|
|
27
|
+
for (const [name, server] of Object.entries(filtered)) {
|
|
28
|
+
if ('url' in server) {
|
|
29
|
+
result[name] = {
|
|
30
|
+
type: 'remote',
|
|
31
|
+
url: server.url,
|
|
32
|
+
enabled: server.enabled ?? true,
|
|
33
|
+
...(server.headers != null ? { headers: server.headers } : {})
|
|
34
|
+
}
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result[name] = {
|
|
39
|
+
type: 'local',
|
|
40
|
+
command: [server.command, ...server.args],
|
|
41
|
+
enabled: server.enabled ?? true,
|
|
42
|
+
...(server.env != null ? { environment: server.env } : {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
47
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ModelServiceConfig } from '@vibe-forge/core'
|
|
2
|
+
|
|
3
|
+
import { asPlainRecord, normalizeStringRecord } from './object-utils'
|
|
4
|
+
|
|
5
|
+
const normalizePositiveInteger = (value: unknown): number | undefined => (
|
|
6
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
7
|
+
? Math.floor(value)
|
|
8
|
+
: undefined
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const appendQueryParams = (baseURL: string, queryParams: Record<string, string>) => {
|
|
12
|
+
if (Object.keys(queryParams).length === 0) return baseURL
|
|
13
|
+
|
|
14
|
+
const url = new URL(baseURL)
|
|
15
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
16
|
+
url.searchParams.set(key, value)
|
|
17
|
+
}
|
|
18
|
+
return url.toString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalizeProviderBaseURL = (baseURL: string, npmPackage: string) => {
|
|
22
|
+
if (npmPackage === '@ai-sdk/openai-compatible') return baseURL.replace(/\/chat\/completions\/?$/u, '')
|
|
23
|
+
if (npmPackage === '@ai-sdk/openai') return baseURL.replace(/\/responses\/?$/u, '')
|
|
24
|
+
return baseURL
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getProviderExtra = (service: ModelServiceConfig) => ({
|
|
28
|
+
...asPlainRecord(asPlainRecord(service.extra)?.codex),
|
|
29
|
+
...asPlainRecord(asPlainRecord(service.extra)?.opencode)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const inferProviderPackage = (service: ModelServiceConfig, providerExtra: Record<string, unknown>) => {
|
|
33
|
+
if (typeof providerExtra.npm === 'string' && providerExtra.npm.trim() !== '') return providerExtra.npm
|
|
34
|
+
const wireApi = typeof providerExtra.wireApi === 'string' ? providerExtra.wireApi : undefined
|
|
35
|
+
return wireApi === 'responses' || service.apiBaseUrl.includes('/responses')
|
|
36
|
+
? '@ai-sdk/openai'
|
|
37
|
+
: '@ai-sdk/openai-compatible'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const resolveOpenCodeModel = (
|
|
41
|
+
rawModel: string | undefined,
|
|
42
|
+
modelServices: Record<string, ModelServiceConfig>
|
|
43
|
+
) => {
|
|
44
|
+
const normalized = rawModel?.trim()
|
|
45
|
+
if (normalized == null || normalized === '' || normalized.toLowerCase() === 'default') {
|
|
46
|
+
return { cliModel: undefined, providerConfig: undefined }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!normalized.includes(',')) {
|
|
50
|
+
return { cliModel: normalized, providerConfig: undefined }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const commaIndex = normalized.indexOf(',')
|
|
54
|
+
const serviceKey = normalized.slice(0, commaIndex).trim()
|
|
55
|
+
const modelId = normalized.slice(commaIndex + 1).trim()
|
|
56
|
+
const service = modelServices[serviceKey]
|
|
57
|
+
|
|
58
|
+
if (!service || modelId === '') {
|
|
59
|
+
return {
|
|
60
|
+
cliModel: serviceKey !== '' && modelId !== '' ? `${serviceKey}/${modelId}` : (modelId || normalized),
|
|
61
|
+
providerConfig: undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const providerExtra = getProviderExtra(service)
|
|
66
|
+
const providerId = typeof providerExtra.providerId === 'string' && providerExtra.providerId.trim() !== ''
|
|
67
|
+
? providerExtra.providerId
|
|
68
|
+
: serviceKey
|
|
69
|
+
const npm = inferProviderPackage(service, providerExtra)
|
|
70
|
+
const baseURL = appendQueryParams(
|
|
71
|
+
normalizeProviderBaseURL(service.apiBaseUrl, npm),
|
|
72
|
+
normalizeStringRecord(providerExtra.queryParams)
|
|
73
|
+
)
|
|
74
|
+
const normalizedTimeoutMs = normalizePositiveInteger(service.timeoutMs)
|
|
75
|
+
const normalizedMaxOutputTokens = normalizePositiveInteger(service.maxOutputTokens)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
cliModel: `${providerId}/${modelId}`,
|
|
79
|
+
providerConfig: {
|
|
80
|
+
[providerId]: {
|
|
81
|
+
npm,
|
|
82
|
+
name: service.title?.trim() !== '' ? service.title : serviceKey,
|
|
83
|
+
options: {
|
|
84
|
+
apiKey: service.apiKey,
|
|
85
|
+
baseURL,
|
|
86
|
+
...(normalizedTimeoutMs != null
|
|
87
|
+
? {
|
|
88
|
+
timeout: normalizedTimeoutMs,
|
|
89
|
+
chunkTimeout: normalizedTimeoutMs
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
92
|
+
...(() => {
|
|
93
|
+
const headers = normalizeStringRecord(providerExtra.headers)
|
|
94
|
+
return Object.keys(headers).length > 0 ? { headers } : {}
|
|
95
|
+
})()
|
|
96
|
+
},
|
|
97
|
+
models: {
|
|
98
|
+
[modelId]: {
|
|
99
|
+
name: modelId,
|
|
100
|
+
...(normalizedMaxOutputTokens != null
|
|
101
|
+
? {
|
|
102
|
+
limit: {
|
|
103
|
+
output: normalizedMaxOutputTokens
|
|
104
|
+
},
|
|
105
|
+
options: {
|
|
106
|
+
maxOutputTokens: normalizedMaxOutputTokens
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
: {})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const asPlainRecord = (value: unknown): Record<string, unknown> | undefined => (
|
|
2
|
+
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value as Record<string, unknown>
|
|
4
|
+
: undefined
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
export const normalizeStringRecord = (value: unknown) => {
|
|
8
|
+
const result: Record<string, string> = {}
|
|
9
|
+
const record = asPlainRecord(value)
|
|
10
|
+
if (!record) return result
|
|
11
|
+
|
|
12
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
13
|
+
if (typeof entry === 'string' && entry !== '') {
|
|
14
|
+
result[key] = entry
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const deepMerge = (base: Record<string, unknown>, override: Record<string, unknown>) => {
|
|
22
|
+
const result: Record<string, unknown> = { ...base }
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(override)) {
|
|
25
|
+
const baseRecord = asPlainRecord(result[key])
|
|
26
|
+
const valueRecord = asPlainRecord(value)
|
|
27
|
+
if (baseRecord && valueRecord) {
|
|
28
|
+
result[key] = deepMerge(baseRecord, valueRecord)
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
result[key] = value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { asPlainRecord } from './object-utils'
|
|
2
|
+
import { LEGACY_TOOL_PERMISSION_ALIASES } from './tools'
|
|
3
|
+
|
|
4
|
+
export type PermissionValue = 'allow' | 'ask' | 'deny'
|
|
5
|
+
export type PermissionNode = PermissionValue | PermissionRecord
|
|
6
|
+
export interface PermissionRecord {
|
|
7
|
+
[key: string]: PermissionNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const isPermissionValue = (value: unknown): value is PermissionValue => (
|
|
11
|
+
value === 'allow' || value === 'ask' || value === 'deny'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const normalizePermissionNode = (value: unknown): PermissionNode | undefined => {
|
|
15
|
+
if (isPermissionValue(value)) return value
|
|
16
|
+
|
|
17
|
+
const record = asPlainRecord(value)
|
|
18
|
+
if (!record) return undefined
|
|
19
|
+
|
|
20
|
+
const result: PermissionRecord = {}
|
|
21
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
22
|
+
const normalized = normalizePermissionNode(entry)
|
|
23
|
+
if (normalized != null) result[key] = normalized
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Object.keys(result).length > 0 ? result : {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const clonePermissionNode = (value: PermissionNode | undefined): PermissionNode | undefined => {
|
|
30
|
+
if (value == null || isPermissionValue(value)) return value
|
|
31
|
+
const result: PermissionRecord = {}
|
|
32
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
33
|
+
const cloned = clonePermissionNode(entry)
|
|
34
|
+
if (cloned != null) result[key] = cloned
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const asPermissionRecord = (value: PermissionNode | undefined): PermissionRecord | undefined => (
|
|
40
|
+
value != null && !isPermissionValue(value) ? value : undefined
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const transformPermissionNode = (
|
|
44
|
+
value: PermissionNode | undefined,
|
|
45
|
+
transform: (entry: PermissionValue) => PermissionValue
|
|
46
|
+
): PermissionNode | undefined => {
|
|
47
|
+
if (value == null || isPermissionValue(value)) return value == null ? undefined : transform(value)
|
|
48
|
+
|
|
49
|
+
const result: PermissionRecord = {}
|
|
50
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
51
|
+
const transformed = transformPermissionNode(entry, transform)
|
|
52
|
+
if (transformed != null) result[key] = transformed
|
|
53
|
+
}
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const applyPermissionLevel = (base: PermissionNode | undefined, level: PermissionValue): PermissionNode => {
|
|
58
|
+
if (base == null) return level
|
|
59
|
+
if (isPermissionValue(base)) {
|
|
60
|
+
if (level === 'deny') return 'deny'
|
|
61
|
+
if (level === 'allow') return base === 'ask' ? 'allow' : base
|
|
62
|
+
return base === 'allow' ? 'ask' : base
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (level === 'deny') return 'deny'
|
|
66
|
+
const record = asPermissionRecord(transformPermissionNode(base, (entry) => (
|
|
67
|
+
level === 'allow'
|
|
68
|
+
? (entry === 'ask' ? 'allow' : entry)
|
|
69
|
+
: (entry === 'allow' ? 'ask' : entry)
|
|
70
|
+
))) ?? {}
|
|
71
|
+
if (!('*' in record)) record['*'] = level
|
|
72
|
+
return record
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const mergePermissionNodes = (
|
|
76
|
+
base: PermissionNode | undefined,
|
|
77
|
+
override: PermissionNode | undefined
|
|
78
|
+
): PermissionNode | undefined => {
|
|
79
|
+
if (override == null) return clonePermissionNode(base)
|
|
80
|
+
if (base == null) return clonePermissionNode(override)
|
|
81
|
+
if (isPermissionValue(base)) return isPermissionValue(override) ? override : mergePermissionNodes({ '*': base }, override)
|
|
82
|
+
if (isPermissionValue(override)) return applyPermissionLevel(base, override)
|
|
83
|
+
|
|
84
|
+
const result: PermissionRecord = { ...(clonePermissionNode(base) as PermissionRecord) }
|
|
85
|
+
for (const [key, value] of Object.entries(override)) {
|
|
86
|
+
result[key] = mergePermissionNodes(result[key], value) as PermissionNode
|
|
87
|
+
}
|
|
88
|
+
return result
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const rewritePermissionMode = (
|
|
92
|
+
value: PermissionNode | undefined,
|
|
93
|
+
mode: 'dontAsk' | 'bypassPermissions'
|
|
94
|
+
): PermissionNode => {
|
|
95
|
+
const transformed = transformPermissionNode(value, (entry) => (
|
|
96
|
+
mode === 'bypassPermissions' || entry === 'ask' ? 'allow' : entry
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
if (transformed == null || isPermissionValue(transformed)) return transformed ?? { '*': 'allow' }
|
|
100
|
+
if (!('*' in transformed)) transformed['*'] = 'allow'
|
|
101
|
+
return transformed
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const findPermissionEntry = (
|
|
105
|
+
permission: PermissionNode | undefined,
|
|
106
|
+
key: string
|
|
107
|
+
): PermissionNode | undefined => {
|
|
108
|
+
const record = asPermissionRecord(permission)
|
|
109
|
+
if (!record) return undefined
|
|
110
|
+
if (key in record) return clonePermissionNode(record[key])
|
|
111
|
+
|
|
112
|
+
for (const [alias, normalized] of Object.entries(LEGACY_TOOL_PERMISSION_ALIASES)) {
|
|
113
|
+
if (normalized === key && alias in record) {
|
|
114
|
+
return clonePermissionNode(record[alias] as PermissionNode)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AdapterQueryOptions } from '@vibe-forge/core/adapter'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findPermissionEntry,
|
|
5
|
+
isPermissionValue,
|
|
6
|
+
type PermissionNode,
|
|
7
|
+
type PermissionRecord,
|
|
8
|
+
type PermissionValue
|
|
9
|
+
} from './permission-node'
|
|
10
|
+
import { LEGACY_TOOL_PERMISSION_ALIASES } from './tools'
|
|
11
|
+
|
|
12
|
+
const OPENCODE_PERMISSION_KEYS = [
|
|
13
|
+
'bash', 'edit', 'glob', 'grep', 'question', 'read', 'list', 'lsp', 'skill',
|
|
14
|
+
'task', 'todoread', 'todowrite', 'webfetch', 'websearch', 'codesearch'
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const mapPermissionModeToOpenCode = (
|
|
18
|
+
permissionMode: AdapterQueryOptions['permissionMode']
|
|
19
|
+
): PermissionRecord | undefined => {
|
|
20
|
+
if (permissionMode == null) return undefined
|
|
21
|
+
if (permissionMode === 'dontAsk' || permissionMode === 'bypassPermissions') return { '*': 'allow' as const }
|
|
22
|
+
if (permissionMode === 'acceptEdits') return { '*': 'allow' as const, bash: 'ask', edit: 'allow', task: 'ask' }
|
|
23
|
+
if (permissionMode === 'plan') return { '*': 'allow' as const, bash: 'ask', edit: 'deny', task: 'deny' }
|
|
24
|
+
return { '*': 'allow' as const, bash: 'ask', edit: 'ask', task: 'ask' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizePermissionToolKey = (value: string) => {
|
|
28
|
+
const key = value.trim()
|
|
29
|
+
if (key === '') return undefined
|
|
30
|
+
return LEGACY_TOOL_PERMISSION_ALIASES[key] ?? key
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolvePermissionValue = (
|
|
34
|
+
key: string,
|
|
35
|
+
basePermission: PermissionNode | undefined
|
|
36
|
+
): PermissionValue => {
|
|
37
|
+
const explicit = findPermissionEntry(basePermission, key)
|
|
38
|
+
if (isPermissionValue(explicit)) return explicit
|
|
39
|
+
|
|
40
|
+
const wildcard = findPermissionEntry(basePermission, '*')
|
|
41
|
+
if (isPermissionValue(wildcard)) return wildcard
|
|
42
|
+
|
|
43
|
+
return isPermissionValue(basePermission) ? basePermission : 'allow'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const buildToolPermissionConfig = (
|
|
47
|
+
tools: AdapterQueryOptions['tools'],
|
|
48
|
+
basePermission: PermissionNode | undefined = undefined
|
|
49
|
+
) => {
|
|
50
|
+
const includes = new Set((tools?.include ?? []).map(normalizePermissionToolKey).filter(Boolean) as string[])
|
|
51
|
+
const excludes = new Set((tools?.exclude ?? []).map(normalizePermissionToolKey).filter(Boolean) as string[])
|
|
52
|
+
const allowAllTools = includes.delete('*')
|
|
53
|
+
|
|
54
|
+
if (includes.size === 0 && excludes.size === 0) return undefined
|
|
55
|
+
|
|
56
|
+
const result: PermissionRecord = {}
|
|
57
|
+
const explicitKeys = Object.keys(
|
|
58
|
+
isPermissionValue(basePermission) || basePermission == null ? {} : basePermission
|
|
59
|
+
).map(normalizePermissionToolKey).filter((value): value is string => value != null && value !== '*')
|
|
60
|
+
|
|
61
|
+
if (includes.size > 0 && !allowAllTools) {
|
|
62
|
+
result['*'] = 'deny'
|
|
63
|
+
for (const key of new Set([...OPENCODE_PERMISSION_KEYS, ...explicitKeys])) {
|
|
64
|
+
if (!includes.has(key)) result[key] = 'deny'
|
|
65
|
+
}
|
|
66
|
+
for (const key of includes) {
|
|
67
|
+
result[key] = findPermissionEntry(basePermission, key) ?? resolvePermissionValue(key, basePermission)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const key of excludes) result[key] = 'deny'
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import type { AdapterMessageContent } from '@vibe-forge/core/adapter'
|
|
5
|
+
|
|
6
|
+
export const resolveLocalAttachmentPath = (value: string) => {
|
|
7
|
+
if (value.startsWith('file://')) {
|
|
8
|
+
try {
|
|
9
|
+
return fileURLToPath(value)
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return isAbsolute(value) ? value : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const normalizeOpenCodePrompt = (content: AdapterMessageContent[]) => {
|
|
19
|
+
const promptParts: string[] = []
|
|
20
|
+
const files = new Set<string>()
|
|
21
|
+
|
|
22
|
+
for (const item of content) {
|
|
23
|
+
if (item.type === 'text') {
|
|
24
|
+
const text = item.text.trim()
|
|
25
|
+
if (text !== '') promptParts.push(text)
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (item.type === 'image') {
|
|
30
|
+
const filePath = resolveLocalAttachmentPath(item.url)
|
|
31
|
+
if (filePath) files.add(filePath)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (item.type === 'tool_result') {
|
|
36
|
+
promptParts.push(String(item.content))
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (item.type === 'tool_use') {
|
|
41
|
+
promptParts.push(`Tool request: ${item.name}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
prompt: promptParts.join('\n\n').trim() || (
|
|
47
|
+
files.size > 0 ? 'Please inspect the attached file(s).' : 'Continue.'
|
|
48
|
+
),
|
|
49
|
+
files: Array.from(files)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const buildOpenCodeSessionTitle = (
|
|
54
|
+
sessionId: string,
|
|
55
|
+
titlePrefix: string = 'Vibe Forge'
|
|
56
|
+
) => `${titlePrefix}:${sessionId}`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { asPlainRecord } from './object-utils'
|
|
2
|
+
|
|
3
|
+
const parseTimestamp = (value: unknown) => {
|
|
4
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
5
|
+
|
|
6
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
7
|
+
const parsed = Date.parse(value)
|
|
8
|
+
return Number.isNaN(parsed) ? undefined : parsed
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OpenCodeSessionRecord {
|
|
15
|
+
id: string
|
|
16
|
+
title?: string
|
|
17
|
+
updatedAt?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const extractOpenCodeSessionRecords = (input: unknown): OpenCodeSessionRecord[] => {
|
|
21
|
+
const parsed = typeof input === 'string' ? JSON.parse(input) : input
|
|
22
|
+
const record = asPlainRecord(parsed)
|
|
23
|
+
const payload = Array.isArray(parsed)
|
|
24
|
+
? parsed
|
|
25
|
+
: Array.isArray(record?.sessions)
|
|
26
|
+
? record.sessions as unknown[]
|
|
27
|
+
: Array.isArray(record?.items)
|
|
28
|
+
? record.items as unknown[]
|
|
29
|
+
: []
|
|
30
|
+
|
|
31
|
+
return payload.flatMap((entry) => {
|
|
32
|
+
const current = asPlainRecord(entry)
|
|
33
|
+
if (!current) return []
|
|
34
|
+
|
|
35
|
+
const id = [current.id, current.sessionId, current.sessionID]
|
|
36
|
+
.find((value): value is string => typeof value === 'string' && value.trim() !== '')
|
|
37
|
+
if (id == null) return []
|
|
38
|
+
|
|
39
|
+
return [{
|
|
40
|
+
id,
|
|
41
|
+
title: typeof current.title === 'string' && current.title.trim() !== '' ? current.title : undefined,
|
|
42
|
+
updatedAt: parseTimestamp(
|
|
43
|
+
current.updatedAt ??
|
|
44
|
+
current.updated_at ??
|
|
45
|
+
current.modifiedAt ??
|
|
46
|
+
current.modified_at ??
|
|
47
|
+
current.createdAt ??
|
|
48
|
+
current.created_at
|
|
49
|
+
)
|
|
50
|
+
}]
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const selectOpenCodeSessionByTitle = (
|
|
55
|
+
sessions: OpenCodeSessionRecord[],
|
|
56
|
+
title: string
|
|
57
|
+
) => (
|
|
58
|
+
sessions
|
|
59
|
+
.filter(session => session.title === title)
|
|
60
|
+
.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0]
|
|
61
|
+
)
|