@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.
- package/README.md +76 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.jpg +0 -0
- package/bin/mipham +51 -0
- package/bin/mipham.ts +8 -0
- package/package.json +59 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
- package/skills/mipham/om-security.mipham-skill.md +21 -0
- package/skills/standard/code-review.SKILL.md +116 -0
- package/skills/standard/compassionate-communication.SKILL.md +80 -0
- package/skills/standard/doc-generator.SKILL.md +21 -0
- package/skills/standard/github-ops.SKILL.md +22 -0
- package/skills/standard/memory.SKILL.md +22 -0
- package/skills/standard/mipham-code-setup.SKILL.md +113 -0
- package/skills/standard/security-review.SKILL.md +122 -0
- package/skills/standard/self-review.SKILL.md +20 -0
- package/skills/standard/superpower.SKILL.md +19 -0
- package/skills/standard/tdd.SKILL.md +20 -0
- package/skills/standard/web-search.SKILL.md +23 -0
- package/src/agent/sub-agent.ts +122 -0
- package/src/config/defaults.ts +10 -0
- package/src/config/loader.ts +30 -0
- package/src/core/context.ts +135 -0
- package/src/core/engine.ts +193 -0
- package/src/core/hooks.ts +116 -0
- package/src/core/instructions.ts +126 -0
- package/src/core/permission.ts +54 -0
- package/src/core/session-store.ts +136 -0
- package/src/index.tsx +93 -0
- package/src/mcp/client.ts +170 -0
- package/src/mcp/protocol.ts +104 -0
- package/src/mcp/transport.ts +169 -0
- package/src/mcp/types.ts +112 -0
- package/src/providers/anthropic.ts +286 -0
- package/src/providers/bootstrap.ts +28 -0
- package/src/providers/openai-compat.ts +158 -0
- package/src/providers/registry.ts +70 -0
- package/src/security/path.ts +90 -0
- package/src/security/url.ts +96 -0
- package/src/shared/constants.ts +368 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +161 -0
- package/src/skills/loader.ts +109 -0
- package/src/skills/mipham/runtime.ts +63 -0
- package/src/skills/standard/runtime.ts +59 -0
- package/src/tools/agent/agent.ts +51 -0
- package/src/tools/agent/memory.ts +51 -0
- package/src/tools/agent/plan.ts +87 -0
- package/src/tools/agent/skill.ts +58 -0
- package/src/tools/exec/bash.ts +111 -0
- package/src/tools/exec/git.ts +63 -0
- package/src/tools/exec/task.ts +69 -0
- package/src/tools/file/edit.ts +57 -0
- package/src/tools/file/glob.ts +29 -0
- package/src/tools/file/grep.ts +52 -0
- package/src/tools/file/read.ts +38 -0
- package/src/tools/file/write.ts +27 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/network/web-fetch.ts +61 -0
- package/src/tools/network/web-search.ts +32 -0
- package/src/tools/system/config.ts +68 -0
- package/src/tools/system/mcp.ts +75 -0
- package/src/ui/app.tsx +317 -0
- package/src/ui/chat.tsx +76 -0
- package/src/ui/commands.ts +2232 -0
- package/src/ui/input.tsx +83 -0
- 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
|
+
}
|
package/src/mcp/types.ts
ADDED
|
@@ -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
|
+
}
|