@miphamai/cli 0.2.3

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.
Files changed (67) hide show
  1. package/README.md +76 -0
  2. package/assets/icon.icns +0 -0
  3. package/assets/icon.jpg +0 -0
  4. package/bin/mipham +51 -0
  5. package/bin/mipham.ts +8 -0
  6. package/package.json +59 -0
  7. package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
  8. package/skills/mipham/om-security.mipham-skill.md +21 -0
  9. package/skills/standard/code-review.SKILL.md +116 -0
  10. package/skills/standard/compassionate-communication.SKILL.md +80 -0
  11. package/skills/standard/doc-generator.SKILL.md +21 -0
  12. package/skills/standard/github-ops.SKILL.md +22 -0
  13. package/skills/standard/memory.SKILL.md +22 -0
  14. package/skills/standard/mipham-code-setup.SKILL.md +113 -0
  15. package/skills/standard/security-review.SKILL.md +122 -0
  16. package/skills/standard/self-review.SKILL.md +20 -0
  17. package/skills/standard/superpower.SKILL.md +19 -0
  18. package/skills/standard/tdd.SKILL.md +20 -0
  19. package/skills/standard/web-search.SKILL.md +23 -0
  20. package/src/agent/sub-agent.ts +122 -0
  21. package/src/config/defaults.ts +10 -0
  22. package/src/config/loader.ts +30 -0
  23. package/src/core/context.ts +135 -0
  24. package/src/core/engine.ts +193 -0
  25. package/src/core/hooks.ts +116 -0
  26. package/src/core/instructions.ts +126 -0
  27. package/src/core/permission.ts +54 -0
  28. package/src/core/session-store.ts +136 -0
  29. package/src/index.tsx +93 -0
  30. package/src/mcp/client.ts +170 -0
  31. package/src/mcp/protocol.ts +104 -0
  32. package/src/mcp/transport.ts +169 -0
  33. package/src/mcp/types.ts +112 -0
  34. package/src/providers/anthropic.ts +286 -0
  35. package/src/providers/bootstrap.ts +28 -0
  36. package/src/providers/openai-compat.ts +158 -0
  37. package/src/providers/registry.ts +70 -0
  38. package/src/security/path.ts +90 -0
  39. package/src/security/url.ts +96 -0
  40. package/src/shared/constants.ts +368 -0
  41. package/src/shared/index.ts +2 -0
  42. package/src/shared/types.ts +161 -0
  43. package/src/skills/loader.ts +109 -0
  44. package/src/skills/mipham/runtime.ts +63 -0
  45. package/src/skills/standard/runtime.ts +59 -0
  46. package/src/tools/agent/agent.ts +51 -0
  47. package/src/tools/agent/memory.ts +51 -0
  48. package/src/tools/agent/plan.ts +87 -0
  49. package/src/tools/agent/skill.ts +58 -0
  50. package/src/tools/exec/bash.ts +111 -0
  51. package/src/tools/exec/git.ts +63 -0
  52. package/src/tools/exec/task.ts +69 -0
  53. package/src/tools/file/edit.ts +57 -0
  54. package/src/tools/file/glob.ts +29 -0
  55. package/src/tools/file/grep.ts +52 -0
  56. package/src/tools/file/read.ts +38 -0
  57. package/src/tools/file/write.ts +27 -0
  58. package/src/tools/index.ts +126 -0
  59. package/src/tools/network/web-fetch.ts +61 -0
  60. package/src/tools/network/web-search.ts +32 -0
  61. package/src/tools/system/config.ts +68 -0
  62. package/src/tools/system/mcp.ts +75 -0
  63. package/src/ui/app.tsx +317 -0
  64. package/src/ui/chat.tsx +76 -0
  65. package/src/ui/commands.ts +2232 -0
  66. package/src/ui/input.tsx +83 -0
  67. package/src/ui/picker.tsx +209 -0
@@ -0,0 +1,104 @@
1
+ import type {
2
+ InitializeResult,
3
+ ToolDefinition,
4
+ ToolCallResult,
5
+ ResourceDefinition,
6
+ ResourceReadResult,
7
+ } from './types'
8
+ import { StdioTransport } from './transport'
9
+
10
+ const MCP_VERSION = '2024-11-05'
11
+
12
+ /**
13
+ * MCP protocol layer — implements the initialize/tools/resources
14
+ * lifecycle on top of a StdioTransport.
15
+ */
16
+ export class McpProtocol {
17
+ private serverCapabilities: {
18
+ tools?: { listChanged?: boolean }
19
+ resources?: { subscribe?: boolean; listChanged?: boolean }
20
+ } = {}
21
+
22
+ constructor(private transport: StdioTransport) {}
23
+
24
+ async initialize(
25
+ serverCommand: string,
26
+ serverArgs: string[],
27
+ env?: Record<string, string>,
28
+ ): Promise<InitializeResult> {
29
+ // Start transport
30
+ await this.transport.start(serverCommand, serverArgs, env)
31
+
32
+ // Send initialize request
33
+ const result = (await this.transport.sendRequest('initialize', {
34
+ protocolVersion: MCP_VERSION,
35
+ capabilities: {
36
+ tools: {},
37
+ resources: {},
38
+ },
39
+ clientInfo: {
40
+ name: 'Mipham Code',
41
+ version: '0.2.0',
42
+ },
43
+ })) as InitializeResult
44
+
45
+ // Send initialized notification
46
+ this.transport.sendNotification('notifications/initialized')
47
+
48
+ this.serverCapabilities = result.capabilities
49
+
50
+ return result
51
+ }
52
+
53
+ async listTools(): Promise<ToolDefinition[]> {
54
+ const result = (await this.transport.sendRequest('tools/list')) as { tools: ToolDefinition[] }
55
+ return result.tools || []
56
+ }
57
+
58
+ async callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult> {
59
+ const result = (await this.transport.sendRequest('tools/call', {
60
+ name,
61
+ arguments: args || {},
62
+ })) as ToolCallResult
63
+ return result
64
+ }
65
+
66
+ async listResources(): Promise<ResourceDefinition[]> {
67
+ if (!this.serverCapabilities.resources) {
68
+ return []
69
+ }
70
+ const result = (await this.transport.sendRequest('resources/list')) as {
71
+ resources: ResourceDefinition[]
72
+ }
73
+ return result.resources || []
74
+ }
75
+
76
+ async readResource(uri: string): Promise<ResourceReadResult> {
77
+ const result = (await this.transport.sendRequest('resources/read', {
78
+ uri,
79
+ })) as ResourceReadResult
80
+ return result
81
+ }
82
+
83
+ onNotification(handler: (method: string, params?: Record<string, unknown>) => void): void {
84
+ this.transport.onNotification((notification) => {
85
+ handler(notification.method, notification.params)
86
+ })
87
+ }
88
+
89
+ hasTools(): boolean {
90
+ return !!this.serverCapabilities.tools
91
+ }
92
+
93
+ hasResources(): boolean {
94
+ return !!this.serverCapabilities.resources
95
+ }
96
+
97
+ getCapabilities() {
98
+ return { ...this.serverCapabilities }
99
+ }
100
+
101
+ async close(): Promise<void> {
102
+ await this.transport.close()
103
+ }
104
+ }
@@ -0,0 +1,169 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from './types'
3
+
4
+ type ResponseHandler = (response: JsonRpcResponse) => void
5
+ type NotificationHandler = (notification: JsonRpcNotification) => void
6
+
7
+ const REQUEST_TIMEOUT_MS = 30_000
8
+
9
+ /**
10
+ * MCP stdio transport — spawns a subprocess and communicates via
11
+ * newline-delimited JSON-RPC 2.0 messages on stdin/stdout.
12
+ *
13
+ * Uses Node.js child_process for portability across Bun and Node runtimes.
14
+ */
15
+ export class StdioTransport {
16
+ private proc: ChildProcess | null = null
17
+ private msgId = 0
18
+ private pending = new Map<
19
+ number,
20
+ {
21
+ resolve: (v: unknown) => void
22
+ reject: (e: Error) => void
23
+ timer: ReturnType<typeof setTimeout>
24
+ }
25
+ >()
26
+ private notificationHandlers: NotificationHandler[] = []
27
+ private buffer = ''
28
+ private closed = false
29
+
30
+ async start(command: string, args: string[], env?: Record<string, string>): Promise<void> {
31
+ this.closed = false
32
+
33
+ const procEnv: Record<string, string> = {
34
+ ...(process.env as Record<string, string>),
35
+ ...env,
36
+ }
37
+
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn(command, args, {
40
+ stdio: ['pipe', 'pipe', 'pipe'],
41
+ env: procEnv,
42
+ })
43
+
44
+ child.on('error', (err) => {
45
+ this.closed = true
46
+ reject(err)
47
+ })
48
+
49
+ child.stdout?.on('data', (data: Buffer) => {
50
+ this.buffer += data.toString()
51
+ const lines = this.buffer.split('\n')
52
+ this.buffer = lines.pop() || ''
53
+
54
+ for (const line of lines) {
55
+ const trimmed = line.trim()
56
+ if (!trimmed) continue
57
+
58
+ try {
59
+ const msg = JSON.parse(trimmed)
60
+
61
+ // Check if it's a response (has id and result/error)
62
+ if (typeof msg.id === 'number' && ('result' in msg || 'error' in msg)) {
63
+ const response = msg as JsonRpcResponse
64
+ const entry = this.pending.get(response.id)
65
+ if (entry) {
66
+ clearTimeout(entry.timer)
67
+ this.pending.delete(response.id)
68
+ if (response.error) {
69
+ entry.reject(
70
+ new Error(`MCP error ${response.error.code}: ${response.error.message}`),
71
+ )
72
+ } else {
73
+ entry.resolve(response.result)
74
+ }
75
+ }
76
+ } else if (msg.method && msg.id === undefined) {
77
+ // Notification — no id field
78
+ for (const handler of this.notificationHandlers) {
79
+ handler(msg as JsonRpcNotification)
80
+ }
81
+ }
82
+ } catch {
83
+ // Skip unparseable lines
84
+ }
85
+ }
86
+ })
87
+
88
+ child.on('close', () => {
89
+ this.closed = true
90
+ // Reject any remaining pending requests
91
+ for (const [, entry] of this.pending) {
92
+ clearTimeout(entry.timer)
93
+ entry.reject(new Error('Transport closed'))
94
+ }
95
+ this.pending.clear()
96
+ })
97
+
98
+ this.proc = child
99
+ resolve()
100
+ })
101
+ }
102
+
103
+ async sendRequest(method: string, params?: Record<string, unknown>): Promise<unknown> {
104
+ if (!this.proc || this.closed) {
105
+ throw new Error('Transport not connected')
106
+ }
107
+
108
+ const id = ++this.msgId
109
+ const request: JsonRpcRequest = {
110
+ jsonrpc: '2.0',
111
+ id,
112
+ method,
113
+ ...(params ? { params } : {}),
114
+ }
115
+
116
+ return new Promise((resolve, reject) => {
117
+ const timer = setTimeout(() => {
118
+ this.pending.delete(id)
119
+ reject(new Error(`MCP request timeout: ${method} (${REQUEST_TIMEOUT_MS}ms)`))
120
+ }, REQUEST_TIMEOUT_MS)
121
+
122
+ this.pending.set(id, { resolve, reject, timer })
123
+
124
+ // Write to stdin
125
+ this.proc!.stdin!.write(JSON.stringify(request) + '\n')
126
+ })
127
+ }
128
+
129
+ sendNotification(method: string, params?: Record<string, unknown>): void {
130
+ if (!this.proc || this.closed) return
131
+
132
+ const notification: JsonRpcNotification = {
133
+ jsonrpc: '2.0',
134
+ method,
135
+ ...(params ? { params } : {}),
136
+ }
137
+
138
+ this.proc.stdin!.write(JSON.stringify(notification) + '\n')
139
+ }
140
+
141
+ onNotification(handler: NotificationHandler): void {
142
+ this.notificationHandlers.push(handler)
143
+ }
144
+
145
+ async close(): Promise<void> {
146
+ this.closed = true
147
+
148
+ // Reject all pending requests
149
+ for (const [, entry] of this.pending) {
150
+ clearTimeout(entry.timer)
151
+ entry.reject(new Error('Transport closed'))
152
+ }
153
+ this.pending.clear()
154
+
155
+ if (this.proc) {
156
+ try {
157
+ this.proc.stdin?.end()
158
+ } catch {
159
+ /* ignore */
160
+ }
161
+ this.proc.kill()
162
+ this.proc = null
163
+ }
164
+ }
165
+
166
+ isConnected(): boolean {
167
+ return this.proc !== null && !this.closed
168
+ }
169
+ }
@@ -0,0 +1,112 @@
1
+ // ── JSON-RPC 2.0 ──
2
+
3
+ export interface JsonRpcRequest {
4
+ jsonrpc: '2.0'
5
+ id: number
6
+ method: string
7
+ params?: Record<string, unknown>
8
+ }
9
+
10
+ export interface JsonRpcResponse {
11
+ jsonrpc: '2.0'
12
+ id: number
13
+ result?: unknown
14
+ error?: JsonRpcError
15
+ }
16
+
17
+ export interface JsonRpcNotification {
18
+ jsonrpc: '2.0'
19
+ method: string
20
+ params?: Record<string, unknown>
21
+ }
22
+
23
+ export interface JsonRpcError {
24
+ code: number
25
+ message: string
26
+ data?: unknown
27
+ }
28
+
29
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification
30
+
31
+ // ── MCP Initialize ──
32
+
33
+ export interface InitializeRequest {
34
+ protocolVersion: string
35
+ capabilities: ClientCapabilities
36
+ clientInfo: { name: string; version: string }
37
+ }
38
+
39
+ export interface ClientCapabilities {
40
+ tools?: Record<string, unknown>
41
+ resources?: Record<string, unknown>
42
+ }
43
+
44
+ export interface InitializeResult {
45
+ protocolVersion: string
46
+ capabilities: ServerCapabilities
47
+ serverInfo: { name: string; version: string }
48
+ }
49
+
50
+ export interface ServerCapabilities {
51
+ tools?: { listChanged?: boolean }
52
+ resources?: { subscribe?: boolean; listChanged?: boolean }
53
+ }
54
+
55
+ // ── MCP Tools ──
56
+
57
+ export interface ToolDefinition {
58
+ name: string
59
+ description?: string
60
+ inputSchema: {
61
+ type: 'object'
62
+ properties?: Record<string, unknown>
63
+ required?: string[]
64
+ }
65
+ }
66
+
67
+ export interface ToolCallParams {
68
+ name: string
69
+ arguments?: Record<string, unknown>
70
+ }
71
+
72
+ export interface ToolCallContent {
73
+ type: 'text' | 'image' | 'resource'
74
+ text?: string
75
+ data?: string
76
+ mimeType?: string
77
+ }
78
+
79
+ export interface ToolCallResult {
80
+ content: ToolCallContent[]
81
+ isError?: boolean
82
+ }
83
+
84
+ // ── MCP Resources ──
85
+
86
+ export interface ResourceDefinition {
87
+ uri: string
88
+ name: string
89
+ description?: string
90
+ mimeType?: string
91
+ }
92
+
93
+ export interface ResourceReadResult {
94
+ contents: Array<{
95
+ uri: string
96
+ mimeType?: string
97
+ text?: string
98
+ blob?: string
99
+ }>
100
+ }
101
+
102
+ // ── Connection State ──
103
+
104
+ export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
105
+
106
+ export interface ConnectionInfo {
107
+ config: { name: string; command: string; args: string[] }
108
+ status: ConnectionStatus
109
+ tools: ToolDefinition[]
110
+ error?: string
111
+ serverInfo?: { name: string; version: string }
112
+ }
@@ -0,0 +1,286 @@
1
+ import type {
2
+ ProviderConfig,
3
+ ModelInfo,
4
+ Message,
5
+ StreamChunk,
6
+ ContentBlock,
7
+ } from '../shared/index.ts'
8
+ import type { ProviderInstance, ChatRequest } from './registry'
9
+
10
+ interface AnthropicContentBlock {
11
+ type: string
12
+ text?: string
13
+ id?: string
14
+ name?: string
15
+ input?: Record<string, unknown>
16
+ source?: { type: string; media_type: string; data: string }
17
+ tool_use_id?: string
18
+ content?: string | AnthropicContentBlock[]
19
+ }
20
+
21
+ interface AnthropicSSEEvent {
22
+ type: string
23
+ message?: {
24
+ content: AnthropicContentBlock[]
25
+ stop_reason: string | null
26
+ }
27
+ index?: number
28
+ content_block?: AnthropicContentBlock
29
+ delta?: {
30
+ type: string
31
+ text?: string
32
+ partial_json?: string
33
+ }
34
+ error?: { type: string; message: string }
35
+ usage?: { input_tokens: number; output_tokens: number }
36
+ }
37
+
38
+ export class AnthropicProvider implements ProviderInstance {
39
+ private baseUrl = 'https://api.anthropic.com/v1'
40
+ private anthropicVersion = '2023-06-01'
41
+
42
+ constructor(public config: ProviderConfig) {}
43
+
44
+ async *chat(req: ChatRequest): AsyncGenerator<StreamChunk> {
45
+ const apiKey = this.resolveApiKey(this.config.apiKey)
46
+
47
+ // Collect accumulated tool use input (Anthropic streams tool input as partial JSON deltas)
48
+ let currentToolName = ''
49
+ let currentToolId = ''
50
+ let accumulatedToolInput = ''
51
+
52
+ const body: Record<string, unknown> = {
53
+ model: req.model,
54
+ max_tokens: req.maxTokens || 4096,
55
+ stream: true,
56
+ messages: this.convertMessages(req.messages),
57
+ }
58
+
59
+ if (req.systemPrompt) {
60
+ body.system = req.systemPrompt
61
+ }
62
+
63
+ if (req.temperature !== undefined) {
64
+ body.temperature = req.temperature
65
+ }
66
+
67
+ if (req.tools && req.tools.length > 0) {
68
+ body.tools = req.tools.map((t) => ({
69
+ name: t.name,
70
+ description: t.description,
71
+ input_schema: t.parameters || t.input_schema || { type: 'object', properties: {} },
72
+ }))
73
+ }
74
+
75
+ const response = await fetch(`${this.baseUrl}/messages`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'x-api-key': apiKey,
80
+ 'anthropic-version': this.anthropicVersion,
81
+ 'anthropic-beta': 'prompt-caching-2024-07-31',
82
+ },
83
+ body: JSON.stringify(body),
84
+ })
85
+
86
+ if (!response.ok) {
87
+ const errText = await response.text()
88
+ yield { type: 'error', error: `Anthropic API error ${response.status}: ${errText}` }
89
+ return
90
+ }
91
+
92
+ if (!response.body) {
93
+ yield { type: 'error', error: 'No response body' }
94
+ return
95
+ }
96
+
97
+ const reader = response.body.getReader()
98
+ const decoder = new TextDecoder()
99
+ let buffer = ''
100
+
101
+ while (true) {
102
+ const { done, value } = await reader.read()
103
+ if (done) break
104
+
105
+ buffer += decoder.decode(value, { stream: true })
106
+ const lines = buffer.split('\n')
107
+ buffer = lines.pop() || ''
108
+
109
+ for (const line of lines) {
110
+ const trimmed = line.trim()
111
+ if (!trimmed || !trimmed.startsWith('data: ')) continue
112
+ const data = trimmed.slice(6)
113
+
114
+ try {
115
+ const event = JSON.parse(data) as AnthropicSSEEvent
116
+
117
+ switch (event.type) {
118
+ case 'content_block_start': {
119
+ const cb = event.content_block
120
+ if (!cb) continue
121
+
122
+ if (cb.type === 'tool_use') {
123
+ currentToolName = cb.name || ''
124
+ currentToolId = cb.id || ''
125
+ accumulatedToolInput = ''
126
+ }
127
+ break
128
+ }
129
+
130
+ case 'content_block_delta': {
131
+ const delta = event.delta
132
+ if (!delta) continue
133
+
134
+ if (delta.type === 'text_delta' && delta.text) {
135
+ yield { type: 'text', content: delta.text }
136
+ }
137
+
138
+ if (delta.type === 'input_json_delta' && delta.partial_json) {
139
+ accumulatedToolInput += delta.partial_json
140
+ }
141
+ break
142
+ }
143
+
144
+ case 'content_block_stop': {
145
+ if (currentToolId && currentToolName && accumulatedToolInput) {
146
+ let parsedInput: Record<string, unknown> = {}
147
+ try {
148
+ parsedInput = JSON.parse(accumulatedToolInput)
149
+ } catch {
150
+ parsedInput = { _raw: accumulatedToolInput }
151
+ }
152
+
153
+ yield {
154
+ type: 'tool_use',
155
+ toolUse: {
156
+ type: 'tool_use',
157
+ id: currentToolId,
158
+ name: currentToolName,
159
+ input: parsedInput,
160
+ },
161
+ }
162
+
163
+ // Reset accumulator
164
+ currentToolName = ''
165
+ currentToolId = ''
166
+ accumulatedToolInput = ''
167
+ }
168
+ break
169
+ }
170
+
171
+ case 'message_delta': {
172
+ // Contains stop_reason and usage info
173
+ if (event.delta?.type === 'input_json_delta' && event.delta.partial_json) {
174
+ accumulatedToolInput += event.delta.partial_json
175
+ }
176
+ break
177
+ }
178
+
179
+ case 'message_stop': {
180
+ yield { type: 'stop' }
181
+ return
182
+ }
183
+
184
+ case 'error': {
185
+ yield { type: 'error', error: event.error?.message || 'Unknown Anthropic error' }
186
+ return
187
+ }
188
+ }
189
+ } catch {
190
+ // Skip unparseable SSE events
191
+ }
192
+ }
193
+ }
194
+
195
+ yield { type: 'stop' }
196
+ }
197
+
198
+ async listModels(): Promise<ModelInfo[]> {
199
+ return this.config.models.filter((m) => m.status === 'active')
200
+ }
201
+
202
+ async healthCheck(): Promise<boolean> {
203
+ // Anthropic doesn't have a public models list endpoint
204
+ // Use a lightweight check — verify API key format exists
205
+ const apiKey = this.resolveApiKey(this.config.apiKey)
206
+ return apiKey.length > 0 && apiKey.startsWith('sk-ant-')
207
+ }
208
+
209
+ private convertMessages(messages: Message[]): Record<string, unknown>[] {
210
+ const result: Record<string, unknown>[] = []
211
+
212
+ for (const msg of messages) {
213
+ // Anthropic does not allow 'system' role in messages array — it goes to top-level system param
214
+ if (msg.role === 'system') continue
215
+
216
+ if (typeof msg.content === 'string') {
217
+ result.push({
218
+ role: msg.role,
219
+ content: [{ type: 'text', text: msg.content }],
220
+ })
221
+ } else {
222
+ const blocks = (msg.content as ContentBlock[]).map((block) => {
223
+ switch (block.type) {
224
+ case 'text':
225
+ return { type: 'text', text: block.text }
226
+
227
+ case 'image_url': {
228
+ const url = block.image_url.url
229
+ // Handle data URIs (base64) and regular URLs
230
+ if (url.startsWith('data:')) {
231
+ const [header, data] = url.split(',')
232
+ const mediaType = header?.match(/data:(image\/\w+);base64/)?.[1] || 'image/png'
233
+ return {
234
+ type: 'image',
235
+ source: {
236
+ type: 'base64',
237
+ media_type: mediaType,
238
+ data: data || '',
239
+ },
240
+ }
241
+ }
242
+ // For regular URLs, pass as image_url (Anthropic might not support directly)
243
+ return {
244
+ type: 'image',
245
+ source: {
246
+ type: 'url',
247
+ url,
248
+ },
249
+ }
250
+ }
251
+
252
+ case 'tool_use':
253
+ return {
254
+ type: 'tool_use',
255
+ id: block.id,
256
+ name: block.name,
257
+ input: block.input,
258
+ }
259
+
260
+ case 'tool_result':
261
+ return {
262
+ type: 'tool_result',
263
+ tool_use_id: block.tool_use_id,
264
+ content: block.content,
265
+ }
266
+
267
+ default:
268
+ return { type: 'text', text: '' }
269
+ }
270
+ })
271
+
272
+ result.push({ role: msg.role, content: blocks })
273
+ }
274
+ }
275
+
276
+ return result
277
+ }
278
+
279
+ private resolveApiKey(keyTemplate: string): string {
280
+ const match = keyTemplate.match(/^\$\{(.+)\}$/)
281
+ if (match?.[1]) {
282
+ return process.env[match[1]] || ''
283
+ }
284
+ return keyTemplate
285
+ }
286
+ }
@@ -0,0 +1,28 @@
1
+ import type { ProviderConfig } from '../shared/index.ts'
2
+ import { ProviderRegistry } from './registry'
3
+ import { OpenAICompatProvider } from './openai-compat'
4
+ import { AnthropicProvider } from './anthropic'
5
+
6
+ export function bootstrapProviders(
7
+ configs: ProviderConfig[],
8
+ defaultProvider: string,
9
+ defaultModel: string,
10
+ ): ProviderRegistry {
11
+ const registry = new ProviderRegistry(configs, defaultProvider, defaultModel)
12
+
13
+ for (const config of configs) {
14
+ if (config.status === 'upcoming') continue
15
+
16
+ switch (config.protocol) {
17
+ case 'openai-compatible':
18
+ registry.register(config.id, new OpenAICompatProvider(config))
19
+ break
20
+ case 'anthropic':
21
+ registry.register(config.id, new AnthropicProvider(config))
22
+ break
23
+ // custom protocol providers must be registered manually via registry.register()
24
+ }
25
+ }
26
+
27
+ return registry
28
+ }