@miphamai/cli 0.38.0 → 0.39.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/bin/mipham.ts +25 -0
- package/package.json +1 -1
- package/src/agent/sub-agent.ts +4 -3
- package/src/config/defaults.ts +19 -1
- package/src/config/keys-manager.ts +11 -1
- package/src/config/loader.ts +20 -10
- package/src/core/context.ts +86 -56
- package/src/core/credential-masker/output-scrub.ts +5 -0
- package/src/core/crsi-sandbox.ts +19 -7
- package/src/core/engine.ts +50 -33
- package/src/core/instructions.ts +4 -42
- package/src/core/permission.ts +7 -1
- package/src/core/red-team.ts +4 -4
- package/src/core/self-critique.ts +3 -1
- package/src/core/session-log.ts +225 -0
- package/src/core/session-store.ts +119 -34
- package/src/daemon/auth.ts +9 -6
- package/src/daemon/cors.ts +24 -16
- package/src/daemon/rate-limiter.ts +4 -0
- package/src/daemon/server.ts +24 -3
- package/src/index.tsx +54 -28
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/oauth.ts +1 -1
- package/src/providers/llm-replay.ts +36 -0
- package/src/providers/llm.ts +16 -0
- package/src/providers/registry.ts +2 -1
- package/src/security/gate.ts +10 -0
- package/src/security/url.ts +37 -14
- package/src/shared/constants.ts +4 -486
- package/src/shared/types.ts +6 -437
- package/src/skills/fork-executor.ts +3 -1
- package/src/skills/loader.ts +2 -1
- package/src/skills/seam.ts +18 -0
- package/src/tools/agent/agent.ts +1 -0
- package/src/tools/agent/skill.ts +1 -0
- package/src/tools/exec/bash.ts +140 -128
- package/src/tools/exec/git.ts +17 -0
- package/src/tools/file/read.ts +73 -66
- package/src/tools/file/write.ts +1 -1
- package/src/tools/index.ts +54 -132
- package/src/tools/network/web-fetch.ts +41 -26
- package/src/tools/seam.ts +25 -0
- package/src/tools/validation.ts +91 -0
- package/src/ui/commands.ts +17 -5
- package/src/vajra/compose/assemble.ts +19 -0
- package/src/vajra/compose/bundle.ts +39 -0
- package/src/vajra/compose/dump.ts +5 -0
- package/src/vajra/compose/index.ts +4 -0
- package/src/vajra/compose/mount.ts +30 -0
- package/src/vajra/context.ts +183 -0
- package/src/vajra/events.ts +13 -0
- package/src/vajra/index.ts +4 -0
- package/src/vajra/leaf/plan-runner.ts +94 -0
- package/src/vajra/service.ts +15 -0
- package/src/workflow/primitives/agent.ts +3 -1
- package/src/workflow/runtime.ts +23 -15
- package/src/workflow/sandbox.ts +51 -9
- package/src/core/compaction-progress.ts +0 -205
- package/src/core/context-compact.ts +0 -124
- package/src/core/context-drain.ts +0 -113
package/src/tools/index.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import type { ToolDefinition
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import type { TranslationMap } from '../i18n-core/types'
|
|
7
|
-
import { readTool } from './file/read'
|
|
1
|
+
import type { ToolDefinition } from '../shared/index.ts'
|
|
2
|
+
import { Context } from '../vajra'
|
|
3
|
+
import { withValidation } from './validation'
|
|
4
|
+
import { toolService, collectTools } from './seam'
|
|
5
|
+
import { readToolService } from './file/read'
|
|
8
6
|
import { writeTool } from './file/write'
|
|
9
7
|
import { editTool } from './file/edit'
|
|
10
8
|
import { globTool } from './file/glob'
|
|
11
9
|
import { grepTool } from './file/grep'
|
|
12
|
-
import {
|
|
10
|
+
import { bashToolService } from './exec/bash'
|
|
13
11
|
import { gitTool } from './exec/git'
|
|
14
12
|
import { taskTool } from './exec/task'
|
|
15
13
|
import { enterWorktreeTool } from './exec/enter-worktree'
|
|
@@ -33,141 +31,65 @@ import { listAgentsTool } from './agent/list-agents'
|
|
|
33
31
|
import { computerUseTool } from './computer/computer-use'
|
|
34
32
|
import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
|
|
35
33
|
import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
|
|
34
|
+
import { DISABLED_CREDENTIAL_MASKING_CONFIG } from '../config/defaults'
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
function defaultToolContext(): Context {
|
|
37
|
+
const ctx = new Context()
|
|
38
|
+
// 掩码中立默认:无参调用(daemon/workflow)保持 Read/Bash 挂载,但不启用掩码,
|
|
39
|
+
// 对齐 pre-seam 行为(那些路径从不调用 setter)。显式开启掩码走 index.tsx 的加载配置。
|
|
40
|
+
ctx.provide('credentials', DISABLED_CREDENTIAL_MASKING_CONFIG)
|
|
41
|
+
return ctx
|
|
40
42
|
}
|
|
41
|
-
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
schema: Record<string, unknown>,
|
|
49
|
-
params: Record<string, unknown>,
|
|
50
|
-
): string[] {
|
|
51
|
-
const errors: string[] = []
|
|
52
|
-
|
|
53
|
-
// Check required fields
|
|
54
|
-
const required = schema.required as string[] | undefined
|
|
55
|
-
if (required) {
|
|
56
|
-
for (const field of required) {
|
|
57
|
-
if (params[field] === undefined || params[field] === null) {
|
|
58
|
-
errors.push(t('errors.missing_param', { param: field }))
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check types for provided fields
|
|
64
|
-
const properties = schema.properties as
|
|
65
|
-
Record<string, { type: string; enum?: string[] }> | undefined
|
|
66
|
-
if (properties) {
|
|
67
|
-
for (const [key, def] of Object.entries(properties)) {
|
|
68
|
-
const value = params[key]
|
|
69
|
-
if (value === undefined || value === null) continue // already caught by required check
|
|
70
|
-
|
|
71
|
-
switch (def.type) {
|
|
72
|
-
case 'string':
|
|
73
|
-
if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
|
|
74
|
-
else if (def.enum && !def.enum.includes(value)) {
|
|
75
|
-
errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
|
|
76
|
-
}
|
|
77
|
-
break
|
|
78
|
-
case 'integer':
|
|
79
|
-
case 'number':
|
|
80
|
-
if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
|
|
81
|
-
break
|
|
82
|
-
case 'boolean':
|
|
83
|
-
if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
|
|
84
|
-
break
|
|
85
|
-
case 'object':
|
|
86
|
-
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
-
errors.push(t('errors.type_object', { key }))
|
|
88
|
-
}
|
|
89
|
-
break
|
|
90
|
-
case 'array':
|
|
91
|
-
if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return errors
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Wrap a tool's execute with parameter validation.
|
|
102
|
-
*/
|
|
103
|
-
function withValidation(tool: ToolDefinition): ToolDefinition {
|
|
104
|
-
const schema = tool.parameters as Record<string, unknown>
|
|
105
|
-
if (!schema || !schema.properties) return tool // no schema to validate against
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
...tool,
|
|
109
|
-
async execute(params, ctx): Promise<ToolResult> {
|
|
110
|
-
// Sanitize dangerous Unicode from all inputs
|
|
111
|
-
const cleanParams = sanitizeParams(params)
|
|
112
|
-
const errors = validateParams(schema, cleanParams)
|
|
113
|
-
if (errors.length > 0) {
|
|
114
|
-
return {
|
|
115
|
-
success: false,
|
|
116
|
-
content: '',
|
|
117
|
-
error: t('errors.invalid_params', { errors: errors.join('; ') }),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return tool.execute(cleanParams, ctx)
|
|
121
|
-
},
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
126
|
-
const tools: ToolDefinition[] = [
|
|
44
|
+
export function createToolRegistry(
|
|
45
|
+
ctx: Context = defaultToolContext(),
|
|
46
|
+
): Map<string, ToolDefinition> {
|
|
47
|
+
// 普通工具:包 withValidation 后作为 Service 挂载
|
|
48
|
+
const plainTools: ToolDefinition[] = [
|
|
127
49
|
// File tools
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
withValidation(grepTool),
|
|
50
|
+
writeTool,
|
|
51
|
+
editTool,
|
|
52
|
+
globTool,
|
|
53
|
+
grepTool,
|
|
133
54
|
// Exec tools
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
withValidation(exitWorktreeTool),
|
|
55
|
+
gitTool,
|
|
56
|
+
taskTool,
|
|
57
|
+
enterWorktreeTool,
|
|
58
|
+
exitWorktreeTool,
|
|
139
59
|
// Agent tools
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
60
|
+
agentTool,
|
|
61
|
+
skillTool,
|
|
62
|
+
planTool,
|
|
63
|
+
enterPlanModeTool,
|
|
64
|
+
exitPlanModeTool,
|
|
65
|
+
memoryTool,
|
|
66
|
+
workflowTool,
|
|
67
|
+
reportFindingsTool,
|
|
68
|
+
sendMessageTool,
|
|
69
|
+
listAgentsTool,
|
|
150
70
|
// Network tools
|
|
151
|
-
|
|
152
|
-
|
|
71
|
+
webFetchTool,
|
|
72
|
+
webSearchTool,
|
|
153
73
|
// System tools
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
74
|
+
configTool,
|
|
75
|
+
mcpTool,
|
|
76
|
+
toolSearchTool,
|
|
157
77
|
// Artifact tools
|
|
158
|
-
|
|
78
|
+
artifactTool,
|
|
159
79
|
// Computer Use tools
|
|
160
|
-
|
|
80
|
+
computerUseTool,
|
|
161
81
|
// Scheduling tools
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
82
|
+
scheduleWakeupTool,
|
|
83
|
+
cronCreateTool,
|
|
84
|
+
cronDeleteTool,
|
|
85
|
+
cronListTool,
|
|
166
86
|
]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
for (const tool of tools) {
|
|
170
|
-
map.set(tool.name, tool)
|
|
87
|
+
for (const tool of plainTools) {
|
|
88
|
+
ctx.mount(toolService(withValidation(tool)))
|
|
171
89
|
}
|
|
172
|
-
|
|
90
|
+
// 注入工具(credentials 依赖):read + bash
|
|
91
|
+
ctx.mount(readToolService)
|
|
92
|
+
ctx.mount(bashToolService)
|
|
93
|
+
|
|
94
|
+
return collectTools(ctx)
|
|
173
95
|
}
|
|
@@ -177,7 +177,7 @@ export const webFetchTool: ToolDefinition = {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// SSRF protection: validate URL before fetching
|
|
180
|
-
const validationError = validateUrl(url)
|
|
180
|
+
const validationError = await validateUrl(url)
|
|
181
181
|
if (validationError) {
|
|
182
182
|
return { success: false, content: '', error: validationError }
|
|
183
183
|
}
|
|
@@ -186,40 +186,55 @@ export const webFetchTool: ToolDefinition = {
|
|
|
186
186
|
const controller = new AbortController()
|
|
187
187
|
const timer = setTimeout(() => controller.abort(), 30_000) // 30s timeout
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// SSRF defense:
|
|
213
|
-
const redirectError = validateUrl(
|
|
189
|
+
// Manual redirect handling: follow same-host redirects one hop at a time,
|
|
190
|
+
// validating the target BEFORE each request (closes the SSRF-via-redirect gap).
|
|
191
|
+
let currentUrl = url
|
|
192
|
+
const originalHost = new URL(url).hostname
|
|
193
|
+
let response!: Response
|
|
194
|
+
|
|
195
|
+
for (let hop = 0; hop <= 5; hop++) {
|
|
196
|
+
response = await fetch(currentUrl, {
|
|
197
|
+
headers: {
|
|
198
|
+
'User-Agent': 'Mipham-Code/0.24.0',
|
|
199
|
+
Accept: 'text/html,application/xhtml+xml,*/*',
|
|
200
|
+
},
|
|
201
|
+
redirect: 'manual',
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Not a redirect (or no Location) → proceed with this response
|
|
206
|
+
if (response.status < 300 || response.status >= 400) break
|
|
207
|
+
const location = response.headers.get('location')
|
|
208
|
+
if (!location || hop === 5) break
|
|
209
|
+
|
|
210
|
+
const nextUrl = new URL(location, currentUrl).toString()
|
|
211
|
+
|
|
212
|
+
// SSRF defense: validate the redirect target BEFORE following
|
|
213
|
+
const redirectError = await validateUrl(nextUrl)
|
|
214
214
|
if (redirectError) {
|
|
215
|
+
clearTimeout(timer)
|
|
215
216
|
return {
|
|
216
217
|
success: false,
|
|
217
218
|
content: '',
|
|
218
219
|
error: `Redirect blocked: ${redirectError}`,
|
|
219
220
|
}
|
|
220
221
|
}
|
|
222
|
+
|
|
223
|
+
// Cross-host redirects are returned to the caller (tool contract)
|
|
224
|
+
if (new URL(nextUrl).hostname !== originalHost) {
|
|
225
|
+
clearTimeout(timer)
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
content: `Redirected to: ${nextUrl}\n\nFetch from this URL directly to retrieve content.`,
|
|
229
|
+
metadata: { redirected: true, originalUrl: url, finalUrl: nextUrl },
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
currentUrl = nextUrl
|
|
221
234
|
}
|
|
222
235
|
|
|
236
|
+
clearTimeout(timer)
|
|
237
|
+
|
|
223
238
|
// Determine content type; only convert HTML to markdown
|
|
224
239
|
const contentType = response.headers.get('content-type') || ''
|
|
225
240
|
const isHtml = contentType.includes('text/html') || contentType.includes('application/xhtml')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Context, Service } from '../vajra'
|
|
2
|
+
import type { ToolDefinition } from '../shared'
|
|
3
|
+
|
|
4
|
+
/** 工具服务键前缀。一个已挂载工具以 `tool:<name>` 注册于 Context。 */
|
|
5
|
+
export const toolKey = (name: string): string => `tool:${name}`
|
|
6
|
+
|
|
7
|
+
/** 把普通 ToolDefinition 包装成 Vajra Service(无注入依赖)。 */
|
|
8
|
+
export function toolService(tool: ToolDefinition): Service {
|
|
9
|
+
return {
|
|
10
|
+
apply(ctx) {
|
|
11
|
+
ctx.provide(toolKey(tool.name), tool)
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 从 Context 已挂载的工具服务派生 name → definition 的 Map(engine 消费)。 */
|
|
17
|
+
export function collectTools(ctx: Context): Map<string, ToolDefinition> {
|
|
18
|
+
const map = new Map<string, ToolDefinition>()
|
|
19
|
+
for (const key of ctx.keysRecursive()) {
|
|
20
|
+
if (!key.startsWith('tool:')) continue
|
|
21
|
+
const tool = ctx.get<ToolDefinition>(key)
|
|
22
|
+
if (tool) map.set(tool.name, tool)
|
|
23
|
+
}
|
|
24
|
+
return map
|
|
25
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { sanitizeParams } from '../shared/sanitize'
|
|
2
|
+
import { createT } from '../i18n-core/t'
|
|
3
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
4
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
5
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
6
|
+
import type { ToolDefinition, ToolResult } from '../shared'
|
|
7
|
+
|
|
8
|
+
const bundles: Record<string, TranslationMap> = {
|
|
9
|
+
'en-US': enUS as TranslationMap,
|
|
10
|
+
'zh-CN': zhCN as TranslationMap,
|
|
11
|
+
}
|
|
12
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate tool parameters against the tool's JSON Schema definition.
|
|
16
|
+
* Returns an array of error messages (empty = valid).
|
|
17
|
+
*/
|
|
18
|
+
export function validateParams(
|
|
19
|
+
schema: Record<string, unknown>,
|
|
20
|
+
params: Record<string, unknown>,
|
|
21
|
+
): string[] {
|
|
22
|
+
const errors: string[] = []
|
|
23
|
+
|
|
24
|
+
const required = schema.required as string[] | undefined
|
|
25
|
+
if (required) {
|
|
26
|
+
for (const field of required) {
|
|
27
|
+
if (params[field] === undefined || params[field] === null) {
|
|
28
|
+
errors.push(t('errors.missing_param', { param: field }))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const properties = schema.properties as
|
|
34
|
+
Record<string, { type: string; enum?: string[] }> | undefined
|
|
35
|
+
if (properties) {
|
|
36
|
+
for (const [key, def] of Object.entries(properties)) {
|
|
37
|
+
const value = params[key]
|
|
38
|
+
if (value === undefined || value === null) continue
|
|
39
|
+
|
|
40
|
+
switch (def.type) {
|
|
41
|
+
case 'string':
|
|
42
|
+
if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
|
|
43
|
+
else if (def.enum && !def.enum.includes(value)) {
|
|
44
|
+
errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
|
|
45
|
+
}
|
|
46
|
+
break
|
|
47
|
+
case 'integer':
|
|
48
|
+
case 'number':
|
|
49
|
+
if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
|
|
50
|
+
break
|
|
51
|
+
case 'boolean':
|
|
52
|
+
if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
|
|
53
|
+
break
|
|
54
|
+
case 'object':
|
|
55
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
errors.push(t('errors.type_object', { key }))
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
case 'array':
|
|
60
|
+
if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return errors
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wrap a tool's execute with parameter validation.
|
|
71
|
+
*/
|
|
72
|
+
export function withValidation(tool: ToolDefinition): ToolDefinition {
|
|
73
|
+
const schema = tool.parameters as Record<string, unknown>
|
|
74
|
+
if (!schema || !schema.properties) return tool
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...tool,
|
|
78
|
+
async execute(params, ctx): Promise<ToolResult> {
|
|
79
|
+
const cleanParams = sanitizeParams(params)
|
|
80
|
+
const errors = validateParams(schema, cleanParams)
|
|
81
|
+
if (errors.length > 0) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
content: '',
|
|
85
|
+
error: t('errors.invalid_params', { errors: errors.join('; ') }),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return tool.execute(cleanParams, ctx)
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -2840,11 +2840,20 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
2840
2840
|
const { SessionStore } = await import('../core/session-store')
|
|
2841
2841
|
const saved = SessionStore.load(ctx.sessionId)
|
|
2842
2842
|
if (saved) {
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2843
|
+
const log = ctx.engine.getContext().getLog()
|
|
2844
|
+
if (log) {
|
|
2845
|
+
SessionStore.saveLog(ctx.sessionId, log, {
|
|
2846
|
+
provider: saved.metadata.provider,
|
|
2847
|
+
model: saved.metadata.model,
|
|
2848
|
+
cwd: resolved,
|
|
2849
|
+
})
|
|
2850
|
+
} else {
|
|
2851
|
+
SessionStore.save(ctx.sessionId, saved.messages, {
|
|
2852
|
+
provider: saved.metadata.provider,
|
|
2853
|
+
model: saved.metadata.model,
|
|
2854
|
+
cwd: resolved,
|
|
2855
|
+
})
|
|
2856
|
+
}
|
|
2848
2857
|
}
|
|
2849
2858
|
} catch {
|
|
2850
2859
|
/* session persistence is best-effort */
|
|
@@ -4049,6 +4058,9 @@ const forkCmd: CommandHandler = async (ctx, args) => {
|
|
|
4049
4058
|
ctx.engine.getRegistry(),
|
|
4050
4059
|
ctx.engine.getTools(),
|
|
4051
4060
|
ctx.engine.getPermission(),
|
|
4061
|
+
undefined,
|
|
4062
|
+
undefined,
|
|
4063
|
+
ctx.engine.getLlm(),
|
|
4052
4064
|
)
|
|
4053
4065
|
const parentContext = ctx.engine.getContext()
|
|
4054
4066
|
const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Profile, Bundle, BundleLine } from './bundle'
|
|
2
|
+
|
|
3
|
+
export function assemble(profile: Profile, resolveBundle: (name: string) => Bundle): BundleLine[] {
|
|
4
|
+
// 浅拷贝每行,使 patch 不回写共享的 bundle 行对象(不同 profile 共享同一 bundle 时补丁隔离)
|
|
5
|
+
const lines = profile.bundles.flatMap((name) =>
|
|
6
|
+
resolveBundle(name).lines.map((l) => ({ ...l, config: { ...l.config } })),
|
|
7
|
+
)
|
|
8
|
+
if (profile.patch) {
|
|
9
|
+
for (const [id, partial] of Object.entries(profile.patch)) {
|
|
10
|
+
const target = lines.find((l) => l.id === id)
|
|
11
|
+
if (target) {
|
|
12
|
+
Object.assign(target, partial)
|
|
13
|
+
} else {
|
|
14
|
+
lines.push({ id, kind: partial.kind ?? 'tool', config: partial.config ?? {} })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return lines
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { parse as parseYaml } from 'yaml'
|
|
3
|
+
|
|
4
|
+
export type BundleLine = {
|
|
5
|
+
id: string
|
|
6
|
+
kind: 'tool' | 'provider' | 'skill' | 'service'
|
|
7
|
+
config: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
export type Bundle = { name: string; lines: BundleLine[] }
|
|
10
|
+
export type Profile = {
|
|
11
|
+
name: string
|
|
12
|
+
bundles: string[]
|
|
13
|
+
patch?: Record<string, Partial<BundleLine>>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadBundle(path: string): Bundle {
|
|
17
|
+
const raw = readFileSync(path, 'utf-8')
|
|
18
|
+
const data = parseYaml(raw) as { name?: unknown; lines?: unknown }
|
|
19
|
+
if (data.lines !== undefined && !Array.isArray(data.lines)) {
|
|
20
|
+
throw new Error(`bundle "${path}": "lines" must be an array`)
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
name: typeof data.name === 'string' ? data.name : path,
|
|
24
|
+
lines: (data.lines ?? []) as BundleLine[],
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadProfile(path: string): Profile {
|
|
29
|
+
const raw = readFileSync(path, 'utf-8')
|
|
30
|
+
const data = parseYaml(raw) as { name?: unknown; bundles?: unknown; patch?: unknown }
|
|
31
|
+
if (data.bundles !== undefined && !Array.isArray(data.bundles)) {
|
|
32
|
+
throw new Error(`profile "${path}": "bundles" must be an array`)
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name: typeof data.name === 'string' ? data.name : path,
|
|
36
|
+
bundles: (data.bundles ?? []) as string[],
|
|
37
|
+
patch: data.patch as Profile['patch'] | undefined,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Context, Mounted, Service } from '..'
|
|
2
|
+
import type { Bundle, BundleLine, Profile } from './bundle'
|
|
3
|
+
import { assemble } from './assemble'
|
|
4
|
+
|
|
5
|
+
/** 服务解析器:bundle 行的「要挂的代码」——按行解析出要挂载的 Service;纯数据行(如 package-info)返回 undefined 跳过。 */
|
|
6
|
+
export type ServiceResolver = (line: BundleLine) => Service | undefined
|
|
7
|
+
|
|
8
|
+
/** 按行顺序把解析出的 Service 挂进 ctx(依赖未就位则挂起,语义同 ctx.mount)。 */
|
|
9
|
+
export function mountLines(
|
|
10
|
+
ctx: Context,
|
|
11
|
+
lines: BundleLine[],
|
|
12
|
+
resolveService: ServiceResolver,
|
|
13
|
+
): Mounted[] {
|
|
14
|
+
const mounted: Mounted[] = []
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const service = resolveService(line)
|
|
17
|
+
if (service) mounted.push(ctx.mount(service))
|
|
18
|
+
}
|
|
19
|
+
return mounted
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 从 profile 声明式挂载:assemble → mountLines 一整条链。 */
|
|
23
|
+
export function mountProfile(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
profile: Profile,
|
|
26
|
+
resolveBundle: (name: string) => Bundle,
|
|
27
|
+
resolveService: ServiceResolver,
|
|
28
|
+
): Mounted[] {
|
|
29
|
+
return mountLines(ctx, assemble(profile, resolveBundle), resolveService)
|
|
30
|
+
}
|