@vibe-forge/core 0.2.0 → 0.4.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.
@@ -0,0 +1,179 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { execCommand, execShellCommand, linkPreparedNodeModules, pathExists } from './utils'
6
+
7
+ interface CategoryWorkspaceInput {
8
+ workspaceFolder?: string
9
+ category: string
10
+ baseCommit: string
11
+ setupCommand: string
12
+ timeoutSec: number
13
+ }
14
+
15
+ interface CaseWorkspaceInput {
16
+ workspaceFolder?: string
17
+ category: string
18
+ title: string
19
+ runId: string
20
+ baseCommit: string
21
+ setupCommand: string
22
+ timeoutSec: number
23
+ }
24
+
25
+ export interface CategoryWorkspaceState {
26
+ workspacePath: string
27
+ gitRoot: string
28
+ }
29
+
30
+ export interface CaseWorkspaceState extends CategoryWorkspaceState {
31
+ caseWorkspacePath: string
32
+ }
33
+
34
+ const categoryWorkspaceInflight = new Map<string, Promise<CategoryWorkspaceState>>()
35
+
36
+ const resolveWorktreeRoot = (workspaceFolder = process.cwd()) =>
37
+ resolve(workspaceFolder, '.ai/worktress/benchmark')
38
+
39
+ const findGitRoot = async (workspaceFolder: string) => {
40
+ const result = await execCommand({
41
+ command: 'git',
42
+ args: ['rev-parse', '--show-toplevel'],
43
+ cwd: workspaceFolder
44
+ })
45
+ if (result.exitCode !== 0) {
46
+ throw new Error(result.stderr || 'Failed to resolve git root')
47
+ }
48
+ return result.stdout.trim()
49
+ }
50
+
51
+ const detachWorktree = async (gitRoot: string, worktreePath: string) => {
52
+ const result = await execCommand({
53
+ command: 'git',
54
+ args: ['worktree', 'remove', '--force', worktreePath],
55
+ cwd: gitRoot
56
+ })
57
+ const output = `${result.stdout}\n${result.stderr}`
58
+ if (result.exitCode !== 0 && !output.includes('is not a working tree')) {
59
+ throw new Error(output.trim() || `Failed to remove worktree: ${worktreePath}`)
60
+ }
61
+ }
62
+
63
+ export const ensureCategoryWorkspace = async (input: CategoryWorkspaceInput): Promise<CategoryWorkspaceState> => {
64
+ const workspaceFolder = input.workspaceFolder ?? process.cwd()
65
+ const lockKey = `${workspaceFolder}:${input.category}`
66
+ const inflight = categoryWorkspaceInflight.get(lockKey)
67
+ if (inflight != null) {
68
+ return inflight
69
+ }
70
+
71
+ const promise = (async () => {
72
+ const gitRoot = await findGitRoot(workspaceFolder)
73
+ const worktreeRoot = resolveWorktreeRoot(workspaceFolder)
74
+ const workspacePath = resolve(worktreeRoot, input.category)
75
+ const statePath = resolve(workspacePath, '.benchmark-state.json')
76
+
77
+ await mkdir(worktreeRoot, { recursive: true })
78
+
79
+ const currentState = await pathExists(statePath)
80
+ ? JSON.parse(await readFile(statePath, 'utf-8')) as {
81
+ baseCommit?: string
82
+ setupCommand?: string
83
+ }
84
+ : null
85
+
86
+ const needsRecreate = !await pathExists(workspacePath) ||
87
+ currentState?.baseCommit !== input.baseCommit ||
88
+ currentState?.setupCommand !== input.setupCommand
89
+
90
+ if (needsRecreate) {
91
+ if (await pathExists(workspacePath)) {
92
+ await detachWorktree(gitRoot, workspacePath)
93
+ await rm(workspacePath, { force: true, recursive: true })
94
+ }
95
+ const addResult = await execCommand({
96
+ command: 'git',
97
+ args: ['worktree', 'add', '--force', '--detach', workspacePath, input.baseCommit],
98
+ cwd: gitRoot
99
+ })
100
+ if (addResult.exitCode !== 0) {
101
+ throw new Error(addResult.stderr || `Failed to create worktree for ${input.category}`)
102
+ }
103
+ if (input.setupCommand.trim() !== '') {
104
+ const setupResult = await execShellCommand({
105
+ command: input.setupCommand,
106
+ cwd: workspacePath,
107
+ timeoutMs: input.timeoutSec * 1000
108
+ })
109
+ if (setupResult.exitCode !== 0) {
110
+ throw new Error(setupResult.stderr || setupResult.stdout || 'Failed to prepare category workspace')
111
+ }
112
+ }
113
+ await writeFile(statePath, `${JSON.stringify({
114
+ baseCommit: input.baseCommit,
115
+ setupCommand: input.setupCommand
116
+ }, null, 2)}\n`, 'utf-8')
117
+ }
118
+
119
+ return {
120
+ workspacePath,
121
+ gitRoot
122
+ }
123
+ })()
124
+
125
+ categoryWorkspaceInflight.set(lockKey, promise)
126
+
127
+ try {
128
+ return await promise
129
+ } finally {
130
+ categoryWorkspaceInflight.delete(lockKey)
131
+ }
132
+ }
133
+
134
+ export const createCaseWorkspace = async (input: CaseWorkspaceInput): Promise<CaseWorkspaceState> => {
135
+ const categoryWorkspace = await ensureCategoryWorkspace(input)
136
+ const worktreeRoot = resolveWorktreeRoot(input.workspaceFolder ?? process.cwd())
137
+ const caseRoot = resolve(worktreeRoot, '.cases', input.category)
138
+ const caseWorkspacePath = resolve(caseRoot, `${input.title}-${input.runId}`)
139
+
140
+ await mkdir(caseRoot, { recursive: true })
141
+ if (await pathExists(caseWorkspacePath)) {
142
+ await detachWorktree(categoryWorkspace.gitRoot, caseWorkspacePath)
143
+ await rm(caseWorkspacePath, { force: true, recursive: true })
144
+ }
145
+
146
+ const addResult = await execCommand({
147
+ command: 'git',
148
+ args: ['worktree', 'add', '--force', '--detach', caseWorkspacePath, input.baseCommit],
149
+ cwd: categoryWorkspace.gitRoot
150
+ })
151
+ if (addResult.exitCode !== 0) {
152
+ throw new Error(addResult.stderr || `Failed to create case workspace for ${input.category}/${input.title}`)
153
+ }
154
+
155
+ await linkPreparedNodeModules(categoryWorkspace.workspacePath, caseWorkspacePath)
156
+
157
+ if (input.setupCommand.trim() !== '') {
158
+ const setupResult = await execShellCommand({
159
+ command: input.setupCommand,
160
+ cwd: caseWorkspacePath,
161
+ timeoutMs: input.timeoutSec * 1000
162
+ })
163
+ if (setupResult.exitCode !== 0) {
164
+ throw new Error(setupResult.stderr || setupResult.stdout || 'Failed to prepare case workspace')
165
+ }
166
+ }
167
+
168
+ return {
169
+ ...categoryWorkspace,
170
+ caseWorkspacePath
171
+ }
172
+ }
173
+
174
+ export const disposeCaseWorkspace = async (state: CaseWorkspaceState) => {
175
+ await detachWorktree(state.gitRoot, state.caseWorkspacePath)
176
+ if (await pathExists(state.caseWorkspacePath)) {
177
+ await rm(state.caseWorkspacePath, { force: true, recursive: true })
178
+ }
179
+ }
@@ -134,6 +134,13 @@ const updateConfigSection = (config: Config, section: string, value: unknown): C
134
134
  )
135
135
  return nextConfig
136
136
  }
137
+ case 'channels': {
138
+ updateField(
139
+ 'channels',
140
+ mergeMaskedValues(sectionValue, config.channels) as Config['channels']
141
+ )
142
+ return nextConfig
143
+ }
137
144
  case 'adapters': {
138
145
  updateField('adapters', mergeMaskedValues(sectionValue, config.adapters) as Config['adapters'])
139
146
  return nextConfig
@@ -8,7 +8,7 @@ export async function generateAdapterQueryOptions(
8
8
  type: 'spec' | 'entity' | undefined,
9
9
  name?: string,
10
10
  cwd: string = process.cwd()
11
- ): Promise<Partial<AdapterQueryOptions>> {
11
+ ) {
12
12
  const loader = new DefinitionLoader(cwd)
13
13
  const options: Partial<AdapterQueryOptions> = {}
14
14
  const systemPromptParts: string[] = []
@@ -82,5 +82,15 @@ export async function generateAdapterQueryOptions(
82
82
  )
83
83
 
84
84
  options.systemPrompt = systemPromptParts.join('\n\n')
85
- return options
85
+ return [
86
+ {
87
+ rules,
88
+ targetSkills,
89
+ entities,
90
+ skills,
91
+ specs,
92
+ targetBody
93
+ },
94
+ options
95
+ ] as const
86
96
  }
@@ -1,10 +1,82 @@
1
- import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions, TaskDetail } from '@vibe-forge/core'
1
+ import type {
2
+ AdapterCtx,
3
+ AdapterOutputEvent,
4
+ AdapterQueryOptions,
5
+ ModelServiceConfig,
6
+ TaskDetail
7
+ } from '@vibe-forge/core'
2
8
  import { loadAdapter } from '@vibe-forge/core'
3
- import { setCache } from '@vibe-forge/core/utils/cache'
9
+ import { callHook } from '@vibe-forge/core/utils/api'
4
10
 
5
11
  import { prepare } from './prepare'
6
12
  import type { RunTaskOptions } from './type'
7
13
 
14
+ const normalizeNonEmptyString = (value: unknown) => (
15
+ typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
16
+ )
17
+
18
+ const pickFirstNonEmptyString = (values: unknown[]) =>
19
+ values
20
+ .map(normalizeNonEmptyString)
21
+ .find((value): value is string => value != null)
22
+
23
+ const resolveQueryModel = (params: {
24
+ config: AdapterCtx['configs'][0]
25
+ userConfig: AdapterCtx['configs'][1]
26
+ inputModel?: string
27
+ }) => {
28
+ const inputModel = normalizeNonEmptyString(params.inputModel)
29
+ if (inputModel?.includes(',')) return inputModel
30
+
31
+ const mergedModelServices = {
32
+ ...(params.config?.modelServices ?? {}),
33
+ ...(params.userConfig?.modelServices ?? {})
34
+ }
35
+ const mergedDefaultModel = pickFirstNonEmptyString([params.userConfig?.defaultModel, params.config?.defaultModel])
36
+ const mergedDefaultModelService = pickFirstNonEmptyString([
37
+ params.userConfig?.defaultModelService,
38
+ params.config?.defaultModelService
39
+ ])
40
+
41
+ const serviceEntries = Object.entries(mergedModelServices)
42
+ const modelToService = new Map<string, string>()
43
+ const availableModels: string[] = []
44
+ for (const [serviceKey, serviceValue] of serviceEntries) {
45
+ const service = (serviceValue != null && typeof serviceValue === 'object')
46
+ ? serviceValue as ModelServiceConfig
47
+ : undefined
48
+ const models = Array.isArray(service?.models)
49
+ ? service?.models.filter(item => typeof item === 'string' && item.trim() !== '')
50
+ : []
51
+ for (const model of models) {
52
+ if (!modelToService.has(model)) modelToService.set(model, serviceKey)
53
+ availableModels.push(model)
54
+ }
55
+ }
56
+
57
+ const resolveDefaultModel = () => {
58
+ if (availableModels.length === 0) return undefined
59
+ if (mergedDefaultModel && modelToService.has(mergedDefaultModel)) return mergedDefaultModel
60
+ if (mergedDefaultModelService && mergedModelServices[mergedDefaultModelService]) {
61
+ const service = mergedModelServices[mergedDefaultModelService]
62
+ const models = Array.isArray(service?.models)
63
+ ? service?.models.filter(item => typeof item === 'string' && item.trim() !== '')
64
+ : []
65
+ if (models.length > 0) return models[0]
66
+ }
67
+ return availableModels[0]
68
+ }
69
+
70
+ const resolvedModel = inputModel ?? resolveDefaultModel()
71
+ if (!resolvedModel) return undefined
72
+
73
+ const resolvedService = modelToService.get(resolvedModel) ??
74
+ mergedDefaultModelService ??
75
+ serviceEntries[0]?.[0]
76
+
77
+ return resolvedService ? `${resolvedService},${resolvedModel}` : resolvedModel
78
+ }
79
+
8
80
  declare module '@vibe-forge/core' {
9
81
  interface Cache {
10
82
  base: Omit<AdapterCtx, 'logger' | 'cache'>
@@ -25,12 +97,6 @@ export const run = async (
25
97
 
26
98
  await cache.set('base', base)
27
99
 
28
- const startTime = Date.now()
29
- logger.info('[Framework] Process start', {
30
- ...base,
31
- adapterOptions,
32
- startDateTime: new Date(startTime).toLocaleString()
33
- })
34
100
  const adapters = {
35
101
  ...config?.adapters,
36
102
  ...userConfig?.adapters
@@ -52,47 +118,52 @@ export const run = async (
52
118
  return adapterNames[0]
53
119
  })()
54
120
 
55
- const detail: TaskDetail = {
56
- ctxId: ctx.ctxId,
57
- sessionId: adapterOptions.sessionId,
58
- status: 'running',
59
- startTime,
60
- description: adapterOptions.description,
61
- adapter: adapterType,
62
- model: adapterOptions.model
63
- }
64
-
65
- const saveDetail = async (d: TaskDetail) => {
66
- // Save to caches/ctxId/detail.json (ignoring sessionId)
67
- await setCache(ctx.cwd, ctx.ctxId, undefined, 'detail', d)
68
- }
69
-
70
- await saveDetail(detail)
71
-
72
121
  const originalOnEvent = adapterOptions.onEvent
73
122
  const wrappedOnEvent = (event: AdapterOutputEvent) => {
74
123
  if (event.type === 'exit') {
75
- detail.status = event.data.exitCode === 0 ? 'completed' : 'failed'
76
- detail.endTime = Date.now()
77
- detail.exitCode = event.data.exitCode ?? undefined
78
- void saveDetail(detail).catch(console.error)
124
+ const { data } = event
125
+
126
+ void callHook('TaskStop', {
127
+ adapter: adapterType,
128
+ cwd: ctx.cwd,
129
+ sessionId: adapterOptions.sessionId,
130
+
131
+ options,
132
+ adapterOptions,
133
+
134
+ exitCode: data.exitCode,
135
+ stderr: data.stderr
136
+ }, ctx.env)
137
+ .catch((e) => {
138
+ logger.error('[Hook] TaskStop failed', e)
139
+ })
79
140
  }
80
141
  originalOnEvent(event)
81
142
  }
82
143
 
83
144
  const adapter = await loadAdapter(adapterType)
145
+ const resolvedModel = resolveQueryModel({
146
+ config,
147
+ userConfig,
148
+ inputModel: adapterOptions.model
149
+ })
150
+
151
+ await callHook('TaskStart', {
152
+ adapter: adapterType,
153
+ cwd: ctx.cwd,
154
+ sessionId: adapterOptions.sessionId,
155
+
156
+ options,
157
+ adapterOptions
158
+ }, ctx.env)
84
159
  const session = await adapter.query(
85
160
  ctx,
86
161
  {
87
162
  ...adapterOptions,
163
+ model: resolvedModel,
88
164
  onEvent: wrappedOnEvent
89
165
  }
90
166
  )
91
167
 
92
- if (session.pid) {
93
- detail.pid = session.pid
94
- await saveDetail(detail)
95
- }
96
-
97
168
  return { session, ctx }
98
169
  }
package/src/env.ts CHANGED
@@ -8,6 +8,9 @@ export interface ServerEnv {
8
8
  __VF_PROJECT_AI_SERVER_LOG_DIR__: string
9
9
  __VF_PROJECT_AI_SERVER_LOG_LEVEL__: 'debug' | 'info' | 'warn' | 'error'
10
10
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__: boolean
11
+ __VF_PROJECT_AI_CLIENT_MODE__?: 'dev' | 'static'
12
+ __VF_PROJECT_AI_CLIENT_BASE__?: string
13
+ __VF_PROJECT_AI_CLIENT_DIST_PATH__?: string
11
14
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__?: string
12
15
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__?: string
13
16
  }
@@ -21,6 +24,9 @@ export function loadEnv(): ServerEnv {
21
24
  __VF_PROJECT_AI_SERVER_LOG_DIR__ = '.logs',
22
25
  __VF_PROJECT_AI_SERVER_LOG_LEVEL__ = 'info',
23
26
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__,
27
+ __VF_PROJECT_AI_CLIENT_MODE__ = 'static',
28
+ __VF_PROJECT_AI_CLIENT_BASE__,
29
+ __VF_PROJECT_AI_CLIENT_DIST_PATH__,
24
30
 
25
31
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
26
32
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
@@ -36,6 +42,10 @@ export function loadEnv(): ServerEnv {
36
42
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__: __VF_PROJECT_AI_SERVER_ALLOW_CORS__ != null
37
43
  ? __VF_PROJECT_AI_SERVER_ALLOW_CORS__ === 'true'
38
44
  : true,
45
+ __VF_PROJECT_AI_CLIENT_MODE__:
46
+ __VF_PROJECT_AI_CLIENT_MODE__ as ServerEnv['__VF_PROJECT_AI_CLIENT_MODE__'],
47
+ __VF_PROJECT_AI_CLIENT_BASE__,
48
+ __VF_PROJECT_AI_CLIENT_DIST_PATH__,
39
49
 
40
50
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
41
51
  __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
package/src/hooks/type.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ToolInput, ToolOutput } from '../tools'
2
+
1
3
  /**
2
4
  * https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input
3
5
  */
@@ -5,96 +7,8 @@ export interface HookInputCore {
5
7
  cwd: string
6
8
  sessionId: string
7
9
  hookEventName: keyof HookInputs
8
- transcriptPath: string
9
- }
10
-
11
- /**
12
- * https://docs.anthropic.com/en/docs/claude-code/settings#tools-available-to-claude
13
- */
14
- export interface ToolInputs {
15
- mcp__TmarAITools__notify: {
16
- title: string
17
- description: string
18
- sound?: boolean
19
- }
20
- 'mcp__TmarAITools__run-tasks': {
21
- taskId: string
22
- agents: number[]
23
- }
24
- Read: {
25
- filePath: string
26
- }
27
- LS: {
28
- path: string
29
- }
30
- Edit: {
31
- filePath: string
32
- newString: string
33
- oldString: string
34
- }
35
- Write: {
36
- filePath: string
37
- content: string
38
- }
39
- Bash: {
40
- command: string
41
- description: string
42
- }
43
- }
44
-
45
- export interface ToolOutputs {
46
- mcp__TmarAITools__notify: {}
47
- 'mcp__TmarAITools__run-tasks': {}
48
- Read: {
49
- type: 'text' | (string & {})
50
- file: {
51
- filePath: string
52
- content: string
53
- numLines: number
54
- startLine: number
55
- totalLines: number
56
- }
57
- }
58
- LS: string
59
- Edit: {
60
- filePath: string
61
- newString: string
62
- oldString: string
63
- originalFile: string
64
- }
65
- Write: {
66
- filePath: string
67
- content: string
68
- }
69
- Bash: {
70
- stdout: string
71
- stderr: string
72
- interrupted: boolean
73
- isImage: boolean
74
- }
75
10
  }
76
11
 
77
- // dprint-ignore
78
- export type ToolInput = keyof ToolInputs extends infer Keys
79
- ? Keys extends infer Key extends keyof ToolInputs
80
- ? {
81
- toolName: Key
82
- toolInput: ToolInputs[Key]
83
- }
84
- : never
85
- : never
86
-
87
- // dprint-ignore
88
- export type ToolOutput = keyof ToolOutputs extends infer Keys
89
- ? Keys extends infer Key extends keyof ToolOutputs
90
- ? {
91
- toolName: Key
92
- toolInput: ToolInputs[Key]
93
- toolResponse?: ToolOutputs[Key]
94
- }
95
- : never
96
- : never
97
-
98
12
  export interface HookInputs {
99
13
  /**
100
14
  * https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-input
@@ -113,6 +27,34 @@ export interface HookInputs {
113
27
  SessionEnd: HookInputCore & {
114
28
  reason: string
115
29
  }
30
+
31
+ StartTasks: HookInputCore & {
32
+ tasks: Array<{
33
+ description: string
34
+ type: 'default' | 'spec' | 'entity'
35
+ name?: string
36
+ adapter?: string
37
+ background?: boolean
38
+ }>
39
+ }
40
+ GenerateSystemPrompt: HookInputCore & {
41
+ type?: 'spec' | 'entity'
42
+ name?: string
43
+ data?: unknown
44
+ }
45
+ TaskStart: HookInputCore & {
46
+ adapter?: string
47
+ options: unknown
48
+ adapterOptions: unknown
49
+ }
50
+ TaskStop: HookInputCore & {
51
+ exitCode?: number
52
+ stderr?: string
53
+ adapter?: string
54
+
55
+ options: unknown
56
+ adapterOptions: unknown
57
+ }
116
58
  }
117
59
 
118
60
  export type HookInput = HookInputs[keyof HookInputs]
@@ -168,6 +110,11 @@ export interface HookOutputs {
168
110
  SessionEnd: HookOutputCore
169
111
  SubagentStop: HookOutputCore
170
112
  PreCompact: HookOutputCore
113
+
114
+ StartTasks: HookOutputCore
115
+ GenerateSystemPrompt: HookOutputCore
116
+ TaskStart: HookOutputCore
117
+ TaskStop: HookOutputCore
171
118
  }
172
119
 
173
120
  export type HookOutput = HookOutputs[keyof HookOutputs]
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from './adapter'
2
2
  export * from './config'
3
+ export * from './controllers/benchmark'
3
4
  export * from './controllers/config'
4
5
  export * from './controllers/system'
5
6
  export * from './env'
6
7
  export * from './hooks'
7
8
  export * from './schema'
9
+ export * from './tools'
8
10
  export * from './types'
9
11
  export * from './ws'
package/src/tools.ts ADDED
@@ -0,0 +1,46 @@
1
+ export interface StopTaskToolInput {
2
+ task_id?: string
3
+ }
4
+
5
+ export interface StartTasksToolInput {
6
+ tasks: Array<{
7
+ description?: string
8
+ type?: 'default' | 'spec' | 'entity'
9
+ name?: string
10
+ adapter?: string
11
+ permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
12
+ background?: boolean
13
+ }>
14
+ }
15
+
16
+ export interface GetTaskInfoToolInput {
17
+ taskId: string
18
+ }
19
+
20
+ export interface ListTasksToolInput {}
21
+
22
+ export interface ToolInputs {
23
+ StartTasks: StartTasksToolInput
24
+ GetTaskInfo: GetTaskInfoToolInput
25
+ ListTasks: ListTasksToolInput
26
+ StopTask: StopTaskToolInput
27
+ }
28
+
29
+ export interface ToolOutputs {}
30
+
31
+ export type ToolName = keyof ToolInputs
32
+
33
+ export type ToolInput = keyof ToolInputs extends infer Keys ? Keys extends infer Key extends keyof ToolInputs ? {
34
+ toolName: Key
35
+ toolInput: ToolInputs[Key]
36
+ }
37
+ : never
38
+ : never
39
+
40
+ export type ToolOutput = keyof ToolOutputs extends infer Keys ? Keys extends infer Key extends keyof ToolOutputs ? {
41
+ toolName: Key
42
+ toolInput: ToolInputs[Key]
43
+ toolResponse?: ToolOutputs[Key]
44
+ }
45
+ : never
46
+ : never
package/src/types.ts CHANGED
@@ -25,6 +25,7 @@ export interface Session {
25
25
 
26
26
  export type ChatMessageContent =
27
27
  | { type: 'text'; text: string }
28
+ | { type: 'image'; url: string; name?: string; size?: number; mimeType?: string }
28
29
  | { type: 'tool_use'; id: string; name: string; input: any }
29
30
  | { type: 'tool_result'; tool_use_id: string; content: any; is_error?: boolean }
30
31
 
@@ -60,7 +61,7 @@ export interface TaskDetail {
60
61
  startTime: number
61
62
  endTime?: number
62
63
  description?: string
63
- adapter: string
64
+ adapterType?: string
64
65
  model?: string
65
66
  exitCode?: number
66
67
  }
@@ -0,0 +1,32 @@
1
+ import process from 'node:process'
2
+
3
+ import type { HookInputs, HookOutputs } from '#~/hooks/type.js'
4
+
5
+ export type HookEventName = keyof HookInputs
6
+
7
+ type HookInputPayload<K extends HookEventName> = Omit<HookInputs[K], 'hookEventName'>
8
+
9
+ const host = process.env.__VF_PROJECT_AI_SERVER_HOST__ ?? 'localhost'
10
+ const port = process.env.__VF_PROJECT_AI_SERVER_PORT__ ?? '8787'
11
+ const baseUrl = `http://${host}:${port}`
12
+
13
+ export const callHook = async <K extends HookEventName>(
14
+ hookEventName: K,
15
+ input: HookInputPayload<K>,
16
+ env: Record<string, unknown> = process.env
17
+ ): Promise<HookOutputs[K]> => {
18
+ const response = await fetch(`${baseUrl}/api/hooks/call`, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({
22
+ hookEventName,
23
+ input,
24
+ env
25
+ })
26
+ })
27
+ if (!response.ok) {
28
+ const errorText = await response.text()
29
+ throw new Error(`Failed to call hook: ${response.statusText} - ${errorText}`)
30
+ }
31
+ return response.json()
32
+ }