@vibe-forge/core 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "imports": {
5
5
  "#~/*.js": {
6
6
  "__vibe-forge__": {
@@ -3,4 +3,4 @@ import type { Adapter } from './type'
3
3
  export * from './loader'
4
4
  export * from './type'
5
5
 
6
- export const defineAdapter = (adapter: Adapter) => adapter
6
+ export const defineAdapter = <T extends Adapter>(adapter: T): Adapter => adapter
@@ -1,9 +1,11 @@
1
1
  import type { Adapter } from './type'
2
2
 
3
+ const resolveAdapterPackageName = (type: string) => (
4
+ type.startsWith('@') ? type : `@vibe-forge/adapter-${type}`
5
+ )
6
+
3
7
  export const loadAdapter = async (type: string) =>
4
8
  (
5
9
  // eslint-disable-next-line ts/no-require-imports
6
- require(
7
- type.startsWith('@') ? type : `@vibe-forge/adapter-${type}`
8
- )
10
+ require(resolveAdapterPackageName(type))
9
11
  ).default as Adapter
@@ -87,10 +87,6 @@ export interface AdapterQueryOptions {
87
87
  onEvent: (event: AdapterOutputEvent) => void
88
88
  }
89
89
 
90
- export interface AdapterInitOptions {
91
- force?: boolean
92
- }
93
-
94
90
  export interface AdapterSession {
95
91
  kill: () => void
96
92
  emit: (event: AdapterEvent) => void
@@ -99,8 +95,7 @@ export interface AdapterSession {
99
95
 
100
96
  export interface Adapter {
101
97
  init?: (
102
- ctx: AdapterCtx,
103
- options: AdapterInitOptions
98
+ ctx: AdapterCtx
104
99
  ) => Promise<void>
105
100
  query: (
106
101
  ctx: AdapterCtx,
@@ -6,6 +6,8 @@ import { getCache, setCache } from '@vibe-forge/core/utils/cache'
6
6
  import { createLogger } from '@vibe-forge/core/utils/create-logger'
7
7
  import { uuid } from '@vibe-forge/core/utils/uuid'
8
8
 
9
+ import { resolveServerLogLevel } from '#~/env.js'
10
+
9
11
  import type { RunTaskOptions } from './type'
10
12
 
11
13
  export const prepare = async (
@@ -36,7 +38,13 @@ export const prepare = async (
36
38
  // 移除 NODE_OPTIONS 环境变量,防止干扰子进程的运行环境
37
39
  NODE_OPTIONS: undefined
38
40
  }
39
- const logger = createLogger(cwd, ctxId, sessionId, env?.LOG_PREFIX ?? '')
41
+ const logger = createLogger(
42
+ cwd,
43
+ ctxId,
44
+ sessionId,
45
+ env?.LOG_PREFIX ?? '',
46
+ resolveServerLogLevel(env)
47
+ )
40
48
 
41
49
  const jsonVariables: Record<string, string | null | undefined> = {
42
50
  ...env,
@@ -1,8 +1,8 @@
1
1
  import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions } from '#~/adapter/index.js'
2
2
  import { loadAdapter } from '#~/adapter/index.js'
3
3
  import type { ModelServiceConfig } from '#~/config.js'
4
+ import { callHook } from '#~/hooks/call.js'
4
5
  import type { TaskDetail } from '#~/types.js'
5
- import { callHook } from '#~/utils/api.js'
6
6
 
7
7
  import { prepare } from './prepare'
8
8
  import type { RunTaskOptions } from './type'
@@ -162,6 +162,8 @@ export const run = async (
162
162
  }
163
163
 
164
164
  const adapter = await loadAdapter(adapterType)
165
+ await adapter.init?.(ctx)
166
+
165
167
  const resolvedModel = resolveQueryModel({
166
168
  config,
167
169
  userConfig,
package/src/env.ts CHANGED
@@ -1,18 +1,45 @@
1
1
  import { env as processEnv } from 'node:process'
2
2
 
3
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
4
+
3
5
  export interface ServerEnv {
4
6
  __VF_PROJECT_AI_SERVER_HOST__: string
5
7
  __VF_PROJECT_AI_SERVER_PORT__: number
6
8
  __VF_PROJECT_AI_SERVER_WS_PATH__: string
7
9
  __VF_PROJECT_AI_SERVER_DATA_DIR__: string
8
10
  __VF_PROJECT_AI_SERVER_LOG_DIR__: string
9
- __VF_PROJECT_AI_SERVER_LOG_LEVEL__: 'debug' | 'info' | 'warn' | 'error'
11
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__: LogLevel
12
+ __VF_PROJECT_AI_SERVER_DEBUG__: boolean
10
13
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__: boolean
11
14
  __VF_PROJECT_AI_CLIENT_MODE__?: 'dev' | 'static'
12
15
  __VF_PROJECT_AI_CLIENT_BASE__?: string
13
16
  __VF_PROJECT_AI_CLIENT_DIST_PATH__?: string
14
17
  }
15
18
 
19
+ const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const satisfies readonly LogLevel[]
20
+
21
+ export function normalizeLogLevel(value: unknown): LogLevel | undefined {
22
+ if (typeof value !== 'string') return undefined
23
+ const normalized = value.trim().toLowerCase()
24
+ return LOG_LEVELS.includes(normalized as LogLevel)
25
+ ? normalized as LogLevel
26
+ : undefined
27
+ }
28
+
29
+ export function resolveServerLogLevel(
30
+ env: {
31
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__?: unknown
32
+ __VF_PROJECT_AI_SERVER_DEBUG__?: unknown
33
+ },
34
+ fallback: LogLevel = 'info'
35
+ ): LogLevel {
36
+ if (env.__VF_PROJECT_AI_SERVER_DEBUG__ === true || env.__VF_PROJECT_AI_SERVER_DEBUG__ === 'true') {
37
+ return 'debug'
38
+ }
39
+
40
+ return normalizeLogLevel(env.__VF_PROJECT_AI_SERVER_LOG_LEVEL__) ?? fallback
41
+ }
42
+
16
43
  export function loadEnv(): ServerEnv {
17
44
  const {
18
45
  __VF_PROJECT_AI_SERVER_HOST__ = 'localhost',
@@ -21,6 +48,7 @@ export function loadEnv(): ServerEnv {
21
48
  __VF_PROJECT_AI_SERVER_DATA_DIR__ = '.data',
22
49
  __VF_PROJECT_AI_SERVER_LOG_DIR__ = '.logs',
23
50
  __VF_PROJECT_AI_SERVER_LOG_LEVEL__ = 'info',
51
+ __VF_PROJECT_AI_SERVER_DEBUG__,
24
52
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__,
25
53
  __VF_PROJECT_AI_CLIENT_MODE__ = 'static',
26
54
  __VF_PROJECT_AI_CLIENT_BASE__,
@@ -32,8 +60,8 @@ export function loadEnv(): ServerEnv {
32
60
  __VF_PROJECT_AI_SERVER_WS_PATH__,
33
61
  __VF_PROJECT_AI_SERVER_DATA_DIR__,
34
62
  __VF_PROJECT_AI_SERVER_LOG_DIR__,
35
- __VF_PROJECT_AI_SERVER_LOG_LEVEL__:
36
- __VF_PROJECT_AI_SERVER_LOG_LEVEL__ as ServerEnv['__VF_PROJECT_AI_SERVER_LOG_LEVEL__'],
63
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__: normalizeLogLevel(__VF_PROJECT_AI_SERVER_LOG_LEVEL__) ?? 'info',
64
+ __VF_PROJECT_AI_SERVER_DEBUG__: __VF_PROJECT_AI_SERVER_DEBUG__ === 'true',
37
65
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__: __VF_PROJECT_AI_SERVER_ALLOW_CORS__ != null
38
66
  ? __VF_PROJECT_AI_SERVER_ALLOW_CORS__ === 'true'
39
67
  : true,
@@ -0,0 +1,74 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { spawn } from 'node:child_process'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+
6
+ import type { HookInputs, HookOutputs } from './type'
7
+
8
+ export type HookEventName = keyof HookInputs
9
+
10
+ type HookInputPayload<K extends HookEventName> = Omit<HookInputs[K], 'hookEventName'>
11
+
12
+ const pickHookEnv = (env: Record<string, unknown>): Record<string, string> => {
13
+ const result: Record<string, string> = {}
14
+ for (const [key, value] of Object.entries(env)) {
15
+ if (typeof value === 'string') {
16
+ result[key] = value
17
+ }
18
+ }
19
+ return result
20
+ }
21
+
22
+ const resolveHookCliJs = () => {
23
+ try {
24
+ const pkgJsonPath = require.resolve('@vibe-forge/cli/package.json')
25
+ return path.resolve(path.dirname(pkgJsonPath), 'call-hook.js')
26
+ } catch (error) {
27
+ throw new Error('Failed to resolve @vibe-forge/cli hook entry', { cause: error })
28
+ }
29
+ }
30
+
31
+ export const callHook = async <K extends HookEventName>(
32
+ hookEventName: K,
33
+ input: HookInputPayload<K>,
34
+ env: Record<string, unknown> = process.env
35
+ ): Promise<HookOutputs[K]> => {
36
+ const childEnv = pickHookEnv(env)
37
+ const child = spawn(process.execPath, [resolveHookCliJs()], {
38
+ cwd: typeof input.cwd === 'string' ? input.cwd : process.cwd(),
39
+ env: childEnv,
40
+ stdio: ['pipe', 'pipe', 'pipe']
41
+ })
42
+
43
+ const stdoutChunks: Buffer[] = []
44
+ const stderrChunks: Buffer[] = []
45
+
46
+ child.stdout.on('data', chunk => stdoutChunks.push(chunk))
47
+ child.stderr.on('data', chunk => stderrChunks.push(chunk))
48
+
49
+ const exitCode = await new Promise<number>((resolve, reject) => {
50
+ child.once('error', reject)
51
+ child.once('close', code => resolve(code ?? 0))
52
+ child.stdin.end(JSON.stringify({
53
+ ...input,
54
+ hookEventName
55
+ }))
56
+ })
57
+
58
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8').trim()
59
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim()
60
+
61
+ if (exitCode !== 0) {
62
+ throw new Error(`Failed to call hook: process exited with code ${exitCode}${stderr ? ` - ${stderr}` : ''}`)
63
+ }
64
+
65
+ if (stdout === '') {
66
+ return { continue: true } as HookOutputs[K]
67
+ }
68
+
69
+ try {
70
+ return JSON.parse(stdout) as HookOutputs[K]
71
+ } catch (error) {
72
+ throw new Error(`Failed to parse hook output: ${stdout}${stderr ? `\nstderr: ${stderr}` : ''}`, { cause: error })
73
+ }
74
+ }
@@ -33,5 +33,7 @@ export type PluginConfig =
33
33
  | Record<string, Record<string, unknown>>
34
34
  | Partial<PluginMap>
35
35
 
36
+ export * from './call'
36
37
  export * from './loader'
38
+ export * from './runtime'
37
39
  export * from './type'
@@ -0,0 +1,139 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import process from 'node:process'
3
+
4
+ import { loadConfig, resetConfigCache } from '#~/config/load.js'
5
+ import { resolveServerLogLevel } from '#~/env.js'
6
+ import { createLogger } from '#~/utils/create-logger.js'
7
+ import { transformCamelKey } from '#~/utils/string-transform.js'
8
+
9
+ import type { HookInput, HookInputs, HookOutputCore, HookOutputs } from './type'
10
+ import type { HookContext, Plugin } from './index'
11
+ import { resolvePlugins } from './loader'
12
+
13
+ export const callPluginHook = async <K extends keyof HookInputs>(
14
+ eventName: K,
15
+ context: HookContext,
16
+ input: HookInputs[K],
17
+ plugins: Partial<Plugin>[] = []
18
+ ): Promise<HookOutputs[K]> => {
19
+ const { logger } = context
20
+ const filteredPlugins = plugins.filter(
21
+ (
22
+ item
23
+ ): item is
24
+ & {
25
+ name?: string
26
+ }
27
+ & {
28
+ [P in K]: NonNullable<Plugin[P]>
29
+ } => !!item && !!item[eventName]
30
+ )
31
+
32
+ let index = 0
33
+
34
+ const next = async (): Promise<HookOutputs[K]> => {
35
+ if (index >= filteredPlugins.length) {
36
+ return { continue: true }
37
+ }
38
+
39
+ const currentPlugin = filteredPlugins[index]
40
+ const { name = '<anonymous>', [eventName]: hook } = currentPlugin
41
+ index++
42
+
43
+ const withNameLogger = {
44
+ ...logger,
45
+ info: logger.info.bind(logger, `[plugin.${name}]`),
46
+ warn: logger.warn.bind(logger, `[plugin.${name}]`),
47
+ debug: logger.debug.bind(logger, `[plugin.${name}]`),
48
+ error: logger.error.bind(logger, `[plugin.${name}]`)
49
+ }
50
+
51
+ try {
52
+ return await hook(
53
+ {
54
+ ...context,
55
+ logger: withNameLogger
56
+ },
57
+ input,
58
+ next
59
+ )
60
+ } catch (error) {
61
+ if (error instanceof Error && !error.name.includes('[plugin.')) {
62
+ error.name = `${error.name}[plugin.${name}]`
63
+ }
64
+ throw error
65
+ }
66
+ }
67
+
68
+ return next()
69
+ }
70
+
71
+ export const executeHookInput = async (
72
+ input: HookInput,
73
+ env: Record<string, string | null | undefined> = process.env
74
+ ) => {
75
+ const workspaceFolder = env.__VF_PROJECT_WORKSPACE_FOLDER__ ?? input.cwd ?? process.env.HOME ?? '/'
76
+ const ctxId = env.__VF_PROJECT_AI_CTX_ID__ ?? input.sessionId ?? 'default'
77
+ const logPrefix = env.__VF_PROJECT_AI_LOG_PREFIX__ ?? ''
78
+ const loggerBase = createLogger(
79
+ workspaceFolder,
80
+ ctxId,
81
+ input.sessionId,
82
+ logPrefix,
83
+ resolveServerLogLevel(env)
84
+ )
85
+
86
+ const logger: typeof loggerBase = {
87
+ ...loggerBase,
88
+ info: (...args) => loggerBase.info(`[${input.hookEventName}]`, ...args),
89
+ warn: (...args) => loggerBase.warn(`[${input.hookEventName}]`, ...args),
90
+ debug: (...args) => loggerBase.debug(`[${input.hookEventName}]`, ...args),
91
+ error: (...args) => loggerBase.error(`[${input.hookEventName}]`, ...args)
92
+ }
93
+
94
+ resetConfigCache()
95
+ const jsonVariables = {
96
+ ...env,
97
+ WORKSPACE_FOLDER: workspaceFolder,
98
+ __VF_PROJECT_WORKSPACE_FOLDER__: workspaceFolder
99
+ }
100
+ const [config, userConfig] = await loadConfig({ jsonVariables })
101
+ const plugins = [
102
+ ...await resolvePlugins(config?.plugins ?? []),
103
+ ...await resolvePlugins(userConfig?.plugins ?? [])
104
+ ]
105
+
106
+ return callPluginHook(
107
+ input.hookEventName,
108
+ { logger },
109
+ input as HookInputs[typeof input.hookEventName],
110
+ plugins
111
+ )
112
+ }
113
+
114
+ export const readHookInput = async () => {
115
+ const stdoutBuffer = await new Promise<Buffer>((resolve) => {
116
+ const chunks: Buffer[] = []
117
+ process.stdin.on('data', chunk => chunks.push(chunk))
118
+ process.stdin.once('end', () => resolve(Buffer.concat(chunks)))
119
+ })
120
+
121
+ return transformCamelKey<HookInput>(
122
+ JSON.parse(stdoutBuffer.toString() || '{}')
123
+ )
124
+ }
125
+
126
+ export const runHookCli = async () => {
127
+ try {
128
+ const input = await readHookInput()
129
+ const result = await executeHookInput(input)
130
+ console.log(JSON.stringify(result))
131
+ } catch (error) {
132
+ console.log(
133
+ JSON.stringify({
134
+ continue: false,
135
+ stopReason: `run hook error: ${String(error)}`
136
+ } satisfies HookOutputCore)
137
+ )
138
+ }
139
+ }
@@ -1,6 +1,8 @@
1
1
  import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
2
2
  import { dirname, resolve } from 'node:path'
3
3
 
4
+ import type { LogLevel } from '#~/env.js'
5
+
4
6
  export interface Logger {
5
7
  stream: NodeJS.WritableStream
6
8
  info: (...args: unknown[]) => void
@@ -9,19 +11,29 @@ export interface Logger {
9
11
  error: (...args: unknown[]) => void
10
12
  }
11
13
 
14
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
15
+ debug: 10,
16
+ info: 20,
17
+ warn: 30,
18
+ error: 40
19
+ }
20
+
12
21
  export const createLogger = (
13
22
  cwd: string,
14
23
  taskId: string,
15
24
  sessionId: string,
16
- logPrefix = ''
25
+ logPrefix = '',
26
+ level: LogLevel = 'info'
17
27
  ) => {
18
- const date = new Date()
19
- // 年-月-日-小时 作为一级目录名
20
- const dateDir = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}-${date.getHours()}`
28
+ const normalizedSessionId = sessionId ?? 'default'
29
+ const taskDir = resolve(
30
+ cwd,
31
+ `.ai${logPrefix}/logs/${taskId}`
32
+ )
21
33
 
22
34
  const loggerFilePath = resolve(
23
- cwd,
24
- `.ai${logPrefix}/logs/${taskId}/${dateDir}/${sessionId ?? 'default'}.log.md`
35
+ taskDir,
36
+ `${normalizedSessionId}.log.md`
25
37
  )
26
38
  // 默认日志文件不存在时,创建一个默认的日志文件
27
39
  if (!existsSync(loggerFilePath)) {
@@ -30,7 +42,10 @@ export const createLogger = (
30
42
  const loggerStream = createWriteStream(loggerFilePath, {
31
43
  flags: 'a'
32
44
  })
33
- const createLog = (tag: string) => (...args: unknown[]) => {
45
+ const createLog = (tag: string, currentLevel: LogLevel) => (...args: unknown[]) => {
46
+ if (LOG_LEVEL_PRIORITY[currentLevel] < LOG_LEVEL_PRIORITY[level]) {
47
+ return
48
+ }
34
49
  const msg = args
35
50
  .map((arg) => {
36
51
  if (typeof arg === 'string') {
@@ -66,9 +81,9 @@ export const createLogger = (
66
81
  }
67
82
  return {
68
83
  stream: loggerStream,
69
- info: createLog('I'),
70
- warn: createLog('W'),
71
- debug: createLog('D'),
72
- error: createLog('E')
84
+ info: createLog('I', 'info'),
85
+ warn: createLog('W', 'warn'),
86
+ debug: createLog('D', 'debug'),
87
+ error: createLog('E', 'error')
73
88
  }
74
89
  }
package/src/utils/api.ts DELETED
@@ -1,32 +0,0 @@
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
- }