@vibe-forge/core 0.1.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/src/env.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { env as processEnv } from 'node:process'
2
+
3
+ export interface ServerEnv {
4
+ __VF_PROJECT_AI_SERVER_HOST__: string
5
+ __VF_PROJECT_AI_SERVER_PORT__: number
6
+ __VF_PROJECT_AI_SERVER_WS_PATH__: string
7
+ __VF_PROJECT_AI_SERVER_DATA_DIR__: string
8
+ __VF_PROJECT_AI_SERVER_LOG_DIR__: string
9
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__: 'debug' | 'info' | 'warn' | 'error'
10
+ __VF_PROJECT_AI_SERVER_ALLOW_CORS__: boolean
11
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__?: string
12
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__?: string
13
+ }
14
+
15
+ export function loadEnv(): ServerEnv {
16
+ const {
17
+ __VF_PROJECT_AI_SERVER_HOST__ = 'localhost',
18
+ __VF_PROJECT_AI_SERVER_PORT__ = '8787',
19
+ __VF_PROJECT_AI_SERVER_WS_PATH__ = '/ws',
20
+ __VF_PROJECT_AI_SERVER_DATA_DIR__ = '.data',
21
+ __VF_PROJECT_AI_SERVER_LOG_DIR__ = '.logs',
22
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__ = 'info',
23
+ __VF_PROJECT_AI_SERVER_ALLOW_CORS__,
24
+
25
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
26
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
27
+ } = processEnv || {}
28
+ return {
29
+ __VF_PROJECT_AI_SERVER_HOST__,
30
+ __VF_PROJECT_AI_SERVER_PORT__: Number(__VF_PROJECT_AI_SERVER_PORT__),
31
+ __VF_PROJECT_AI_SERVER_WS_PATH__,
32
+ __VF_PROJECT_AI_SERVER_DATA_DIR__,
33
+ __VF_PROJECT_AI_SERVER_LOG_DIR__,
34
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__:
35
+ __VF_PROJECT_AI_SERVER_LOG_LEVEL__ as ServerEnv['__VF_PROJECT_AI_SERVER_LOG_LEVEL__'],
36
+ __VF_PROJECT_AI_SERVER_ALLOW_CORS__: __VF_PROJECT_AI_SERVER_ALLOW_CORS__ != null
37
+ ? __VF_PROJECT_AI_SERVER_ALLOW_CORS__ === 'true'
38
+ : true,
39
+
40
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
41
+ __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
42
+ }
43
+ }
@@ -0,0 +1,37 @@
1
+ import type { Writable } from 'node:stream'
2
+
3
+ import type { HookInputs, HookOutputs } from './type'
4
+
5
+ export interface HookContext {
6
+ logger: {
7
+ stream: Writable
8
+ info: (...args: unknown[]) => void
9
+ warn: (...args: unknown[]) => void
10
+ debug: (...args: unknown[]) => void
11
+ error: (...args: unknown[]) => void
12
+ }
13
+ }
14
+
15
+ export type Plugin =
16
+ & {
17
+ name?: string
18
+ }
19
+ & {
20
+ [P in keyof HookInputs]: (
21
+ ctx: HookContext,
22
+ input: HookInputs[P],
23
+ next: () => Promise<HookOutputs[P]>
24
+ ) => Promise<HookOutputs[P]>
25
+ }
26
+
27
+ export const definePlugin = (plugin: Partial<Plugin>) => plugin
28
+
29
+ export interface PluginMap {}
30
+
31
+ export type PluginConfig =
32
+ | (Partial<Plugin> | (() => Partial<Plugin>))[]
33
+ | Record<string, Record<string, unknown>>
34
+ | Partial<PluginMap>
35
+
36
+ export * from './loader'
37
+ export * from './type'
@@ -0,0 +1,75 @@
1
+ import type { Plugin, PluginConfig } from './index'
2
+
3
+ /**
4
+ * 解析单个插件配置
5
+ */
6
+ const loadPlugin = async (
7
+ name: string,
8
+ config: Record<string, unknown>
9
+ ): Promise<Partial<Plugin> | null> => {
10
+ try {
11
+ // eslint-disable-next-line ts/no-require-imports
12
+ const module = require(`${name}/hooks`)
13
+
14
+ // 兼容 ESM default export 和 CJS module.exports
15
+ const factory: (
16
+ // 直接导出插件对象
17
+ | Partial<Plugin>
18
+ // 导出工厂函数,接受配置并返回插件对象
19
+ | ((config: Record<string, unknown>) => Partial<Plugin>)
20
+ ) = module.default ?? module
21
+
22
+ if (typeof factory === 'function') {
23
+ // TODO: 这里可以注入更多上下文,如全局配置、版本信息等
24
+ return factory(config)
25
+ } else if (typeof factory === 'object' && factory !== null) {
26
+ return factory
27
+ }
28
+
29
+ console.warn(`Plugin ${name} does not export a valid plugin factory or object.`)
30
+ return null
31
+ } catch (e) {
32
+ console.error(`Failed to load plugin ${name}:`, e)
33
+ return null
34
+ }
35
+ }
36
+
37
+ export const resolvePlugins = async (config: PluginConfig): Promise<Partial<Plugin>[]> => {
38
+ // 1. 处理数组形式配置 (直接实例化或函数调用)
39
+ if (Array.isArray(config)) {
40
+ return config.map((p) => (typeof p === 'function' ? p() : p))
41
+ }
42
+
43
+ // 2. 处理对象形式配置 (动态加载)
44
+ const entries = Object.entries(config)
45
+ if (entries.length === 0) return []
46
+
47
+ // 并行加载所有插件
48
+ const modules = await Promise.allSettled(
49
+ entries.map(([pkgName, pkgConfig]) => {
50
+ // 如果不是以 @ 或 @vibe-forge/plugin- 开头,则默认加上 @vibe-forge/plugin- 前缀
51
+ const resolvedName = pkgName.startsWith('@') ? pkgName : `@vibe-forge/plugin-${pkgName}`
52
+ // dprint-ignore
53
+ return (
54
+ loadPlugin(resolvedName, pkgConfig as Record<string, unknown>) ??
55
+ loadPlugin(pkgName, pkgConfig as Record<string, unknown>)
56
+ )
57
+ })
58
+ )
59
+
60
+ // 收集成功加载的插件
61
+ const plugins: Partial<Plugin>[] = []
62
+
63
+ modules.forEach((result, index) => {
64
+ const pkgName = entries[index][0]
65
+ if (result.status === 'fulfilled') {
66
+ if (result.value) {
67
+ plugins.push(result.value)
68
+ }
69
+ } else {
70
+ console.error(`Error loading plugin ${pkgName}:`, result.reason)
71
+ }
72
+ })
73
+
74
+ return plugins
75
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input
3
+ */
4
+ export interface HookInputCore {
5
+ cwd: string
6
+ sessionId: string
7
+ 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
+ }
76
+
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
+ export interface HookInputs {
99
+ /**
100
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-input
101
+ */
102
+ PreToolUse: HookInputCore & ToolInput
103
+ /**
104
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#posttooluse-input
105
+ */
106
+ PostToolUse: HookInputCore & ToolOutput
107
+ Notification: HookInputCore
108
+ UserPromptSubmit: HookInputCore & { prompt: string }
109
+ Stop: HookInputCore
110
+ SubagentStop: HookInputCore
111
+ PreCompact: HookInputCore
112
+ SessionStart: HookInputCore
113
+ SessionEnd: HookInputCore & {
114
+ reason: string
115
+ }
116
+ }
117
+
118
+ export type HookInput = HookInputs[keyof HookInputs]
119
+
120
+ /**
121
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields
122
+ */
123
+ export interface HookOutputCore {
124
+ /**
125
+ * Whether Claude should continue after hook execution
126
+ * @default true
127
+ */
128
+ continue?: boolean
129
+ /**
130
+ * Message shown when continue is false
131
+ */
132
+ stopReason?: string
133
+ /**
134
+ * Hide stdout from transcript mode
135
+ * @default false
136
+ */
137
+ suppressOutput?: boolean
138
+ /**
139
+ * Optional warning message shown to the user
140
+ */
141
+ systemMessage?: string
142
+ }
143
+
144
+ export interface HookOutputs {
145
+ /**
146
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#pretooluse-decision-control
147
+ */
148
+ PreToolUse: HookOutputCore & {
149
+ hookSpecificOutput?: {
150
+ hookEventName: 'PreToolUse'
151
+ permissionDecision: 'allow' | 'deny' | 'ask'
152
+ permissionDecisionReason: string
153
+ }
154
+ }
155
+ /**
156
+ * https://docs.anthropic.com/en/docs/claude-code/hooks#posttooluse-decision-control
157
+ */
158
+ PostToolUse: HookOutputCore & {
159
+ hookSpecificOutput?: {
160
+ hookEventName: 'PostToolUse'
161
+ additionalContext: string
162
+ }
163
+ }
164
+ Notification: HookOutputCore
165
+ UserPromptSubmit: HookOutputCore
166
+ Stop: HookOutputCore
167
+ SessionStart: HookOutputCore
168
+ SessionEnd: HookOutputCore
169
+ SubagentStop: HookOutputCore
170
+ PreCompact: HookOutputCore
171
+ }
172
+
173
+ export type HookOutput = HookOutputs[keyof HookOutputs]
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './adapter'
2
+ export * from './config'
3
+ export * from './controllers/config'
4
+ export * from './controllers/system'
5
+ export * from './env'
6
+ export * from './hooks'
7
+ export * from './schema'
8
+ export * from './types'
9
+ export * from './ws'
package/src/schema.ts ADDED
@@ -0,0 +1,13 @@
1
+ import z from 'zod'
2
+
3
+ export const InteractionOptionSchema = z.object({
4
+ label: z.string(),
5
+ description: z.string().optional()
6
+ })
7
+
8
+ export const AskUserQuestionParamsSchema = z.object({
9
+ sessionId: z.string(),
10
+ question: z.string(),
11
+ options: z.array(InteractionOptionSchema).optional(),
12
+ multiselect: z.boolean().optional()
13
+ })
package/src/types.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { z } from 'zod'
2
+ import type { AskUserQuestionParamsSchema, InteractionOptionSchema } from './schema.js'
3
+
4
+ export interface Project {
5
+ id: string
6
+ name: string
7
+ path: string
8
+ }
9
+
10
+ export type SessionStatus = 'running' | 'completed' | 'failed' | 'terminated' | 'waiting_input'
11
+
12
+ export interface Session {
13
+ id: string
14
+ parentSessionId?: string
15
+ title?: string
16
+ createdAt: number
17
+ messageCount?: number
18
+ lastMessage?: string
19
+ lastUserMessage?: string
20
+ isStarred?: boolean
21
+ isArchived?: boolean
22
+ tags?: string[]
23
+ status?: SessionStatus
24
+ }
25
+
26
+ export type ChatMessageContent =
27
+ | { type: 'text'; text: string }
28
+ | { type: 'tool_use'; id: string; name: string; input: any }
29
+ | { type: 'tool_result'; tool_use_id: string; content: any; is_error?: boolean }
30
+
31
+ export interface ChatMessage {
32
+ id: string
33
+ role: 'user' | 'assistant' | 'system'
34
+ content: string | ChatMessageContent[]
35
+ model?: string
36
+ usage?: {
37
+ input_tokens: number
38
+ output_tokens: number
39
+ cache_read_input_tokens?: number
40
+ cache_creation_input_tokens?: number
41
+ }
42
+ toolCall?: {
43
+ id?: string
44
+ name: string
45
+ args: Record<string, unknown>
46
+ status?: 'pending' | 'success' | 'error'
47
+ output?: unknown
48
+ }
49
+ createdAt: number
50
+ }
51
+
52
+ export type InteractionOption = z.infer<typeof InteractionOptionSchema>
53
+ export type AskUserQuestionParams = z.infer<typeof AskUserQuestionParamsSchema>
54
+
55
+ export interface TaskDetail {
56
+ ctxId: string
57
+ sessionId: string
58
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'stopped'
59
+ pid?: number
60
+ startTime: number
61
+ endTime?: number
62
+ description?: string
63
+ adapter: string
64
+ model?: string
65
+ exitCode?: number
66
+ }
@@ -0,0 +1,51 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import { dirname, resolve } from 'node:path'
3
+
4
+ import type { Cache } from '@vibe-forge/core'
5
+
6
+ declare module '@vibe-forge/core' {
7
+ interface Cache {
8
+ }
9
+ }
10
+
11
+ export const getCachePath = (
12
+ cwd: string,
13
+ taskId: string,
14
+ sessionId: string | undefined,
15
+ key: keyof Cache
16
+ ) => {
17
+ const taskDir = resolve(cwd, '.ai/caches', taskId)
18
+ const cacheDir = sessionId ? resolve(taskDir, sessionId) : taskDir
19
+ return resolve(cacheDir, `${key}.json`)
20
+ }
21
+
22
+ export const setCache = async <K extends keyof Cache>(
23
+ cwd: string,
24
+ taskId: string,
25
+ sessionId: string | undefined,
26
+ key: K,
27
+ value: Cache[K]
28
+ ) => {
29
+ const cachePath = getCachePath(cwd, taskId, sessionId, key)
30
+ const cacheDir = dirname(cachePath)
31
+ try {
32
+ await fs.access(cacheDir)
33
+ } catch {
34
+ await fs.mkdir(cacheDir, { recursive: true })
35
+ }
36
+ await fs.writeFile(cachePath, JSON.stringify(value, null, 2), {
37
+ flag: 'w'
38
+ })
39
+ return { cachePath }
40
+ }
41
+
42
+ export const getCache = async <K extends keyof Cache>(
43
+ cwd: string,
44
+ taskId: string,
45
+ sessionId: string | undefined,
46
+ key: K
47
+ ): Promise<Cache[K] | undefined> => {
48
+ const cachePath = getCachePath(cwd, taskId, sessionId, key)
49
+ await fs.access(cachePath)
50
+ return JSON.parse(await fs.readFile(cachePath, 'utf-8'))
51
+ }
@@ -0,0 +1,74 @@
1
+ import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+
4
+ export interface Logger {
5
+ stream: NodeJS.WritableStream
6
+ info: (...args: unknown[]) => void
7
+ warn: (...args: unknown[]) => void
8
+ debug: (...args: unknown[]) => void
9
+ error: (...args: unknown[]) => void
10
+ }
11
+
12
+ export const createLogger = (
13
+ cwd: string,
14
+ taskId: string,
15
+ sessionId: string,
16
+ logPrefix = ''
17
+ ) => {
18
+ const date = new Date()
19
+ // 以 年-月-日-小时 作为一级目录名
20
+ const dateDir = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}-${date.getHours()}`
21
+
22
+ const loggerFilePath = resolve(
23
+ cwd,
24
+ `.ai${logPrefix}/logs/${taskId}/${dateDir}/${sessionId ?? 'default'}.log.md`
25
+ )
26
+ // 默认日志文件不存在时,创建一个默认的日志文件
27
+ if (!existsSync(loggerFilePath)) {
28
+ mkdirSync(dirname(loggerFilePath), { recursive: true })
29
+ }
30
+ const loggerStream = createWriteStream(loggerFilePath, {
31
+ flags: 'a'
32
+ })
33
+ const createLog = (tag: string) => (...args: unknown[]) => {
34
+ const msg = args
35
+ .map((arg) => {
36
+ if (typeof arg === 'string') {
37
+ return arg
38
+ }
39
+ if (arg instanceof Error) {
40
+ return (
41
+ '\n```text\n' +
42
+ `${arg.stack}\n` +
43
+ '```'
44
+ )
45
+ }
46
+ return (
47
+ '\n```json\n' +
48
+ `${JSON.stringify(arg, null, 2)}\n` +
49
+ '```'
50
+ )
51
+ })
52
+ .join(' ')
53
+ const now = new Date().toLocaleString()
54
+ if (loggerStream.writableEnded) {
55
+ const tempLoggerStream = createWriteStream(loggerFilePath, {
56
+ flags: 'a'
57
+ })
58
+ tempLoggerStream.write(
59
+ `# [${now}] __E__ UNEXPECTED LOGGER STREAM ENDED\n`
60
+ )
61
+ tempLoggerStream.write(`# [${now}] __${tag}__ ${msg}\n`)
62
+ tempLoggerStream.end()
63
+ return
64
+ }
65
+ loggerStream.write(`# [${now}] __${tag}__ ${msg}\n`)
66
+ }
67
+ return {
68
+ stream: loggerStream,
69
+ info: createLog('I'),
70
+ warn: createLog('W'),
71
+ debug: createLog('D'),
72
+ error: createLog('E')
73
+ }
74
+ }