@synergenius/flow-weaver 0.22.6 → 0.22.7
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/dist/agent/agent-loop.d.ts +10 -0
- package/dist/agent/agent-loop.js +115 -0
- package/dist/agent/cli-session.d.ts +77 -0
- package/dist/agent/cli-session.js +309 -0
- package/dist/agent/env-allowlist.d.ts +29 -0
- package/dist/agent/env-allowlist.js +60 -0
- package/dist/agent/index.d.ts +16 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/mcp-bridge.d.ts +22 -0
- package/dist/agent/mcp-bridge.js +132 -0
- package/dist/agent/mcp-tool-server.d.ts +30 -0
- package/dist/agent/mcp-tool-server.js +210 -0
- package/dist/agent/providers/anthropic.d.ts +23 -0
- package/dist/agent/providers/anthropic.js +185 -0
- package/dist/agent/providers/claude-cli.d.ts +21 -0
- package/dist/agent/providers/claude-cli.js +155 -0
- package/dist/agent/streaming.d.ts +36 -0
- package/dist/agent/streaming.js +183 -0
- package/dist/agent/types.d.ts +152 -0
- package/dist/agent/types.js +7 -0
- package/dist/cli/flow-weaver.mjs +2 -2
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP bridge — creates a Unix domain socket server that the MCP tool server
|
|
3
|
+
* connects to for executing tools. Also generates the temporary MCP config
|
|
4
|
+
* and tool definition files needed by the Claude CLI.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const bridge = await createMcpBridge(tools, executor, onToolEvent);
|
|
8
|
+
* // pass bridge.configPath to --mcp-config
|
|
9
|
+
* // ...run CLI...
|
|
10
|
+
* bridge.cleanup();
|
|
11
|
+
*/
|
|
12
|
+
import * as net from 'node:net';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
/**
|
|
18
|
+
* Create an MCP bridge that the Claude CLI can connect to for tool execution.
|
|
19
|
+
*
|
|
20
|
+
* @param tools Tool definitions to advertise to the CLI
|
|
21
|
+
* @param executor Function that executes a tool call
|
|
22
|
+
* @param onToolEvent Optional callback for relaying tool events
|
|
23
|
+
* @param logger Optional logger
|
|
24
|
+
*/
|
|
25
|
+
export async function createMcpBridge(tools, executor, onToolEvent, logger) {
|
|
26
|
+
// Create temp directory for bridge files
|
|
27
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fw-mcp-bridge-'));
|
|
28
|
+
fs.chmodSync(tmpDir, 0o700);
|
|
29
|
+
const socketPath = path.join(tmpDir, 'bridge.sock');
|
|
30
|
+
const defsPath = path.join(tmpDir, 'tools.json');
|
|
31
|
+
const configPath = path.join(tmpDir, 'mcp-config.json');
|
|
32
|
+
// Write tool definitions in MCP format
|
|
33
|
+
const mcpToolDefs = tools.map((t) => ({
|
|
34
|
+
name: t.name,
|
|
35
|
+
description: t.description,
|
|
36
|
+
inputSchema: t.inputSchema,
|
|
37
|
+
}));
|
|
38
|
+
fs.writeFileSync(defsPath, JSON.stringify(mcpToolDefs), 'utf-8');
|
|
39
|
+
// Resolve the MCP tool server script path.
|
|
40
|
+
// In prod: sibling .js file. In dev (tsx): sibling .ts file.
|
|
41
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const jsPath = path.resolve(__dirname, 'mcp-tool-server.js');
|
|
43
|
+
const tsPath = path.resolve(__dirname, 'mcp-tool-server.ts');
|
|
44
|
+
const scriptPath = fs.existsSync(jsPath) ? jsPath : tsPath;
|
|
45
|
+
// In dev the script is .ts — use tsx as the loader.
|
|
46
|
+
const mcpCommand = process.execPath; // node
|
|
47
|
+
let mcpArgs = [scriptPath];
|
|
48
|
+
if (scriptPath.endsWith('.ts')) {
|
|
49
|
+
// Look for tsx in the package's node_modules
|
|
50
|
+
const tsxCli = path.resolve(__dirname, '../../node_modules/tsx/dist/cli.mjs');
|
|
51
|
+
if (fs.existsSync(tsxCli)) {
|
|
52
|
+
mcpArgs = [tsxCli, scriptPath];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Write MCP config
|
|
56
|
+
const mcpConfig = {
|
|
57
|
+
mcpServers: {
|
|
58
|
+
'fw-agent': {
|
|
59
|
+
type: 'stdio',
|
|
60
|
+
command: mcpCommand,
|
|
61
|
+
args: mcpArgs,
|
|
62
|
+
env: {
|
|
63
|
+
FW_TOOL_SOCKET: socketPath,
|
|
64
|
+
FW_TOOL_DEFS: defsPath,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(configPath, JSON.stringify(mcpConfig), 'utf-8');
|
|
70
|
+
// Mutable handlers — swapped per request via setHandlers()
|
|
71
|
+
let currentExecutor = executor;
|
|
72
|
+
let currentOnToolEvent = onToolEvent;
|
|
73
|
+
// Create Unix domain socket server.
|
|
74
|
+
// The tool server sends a newline-terminated JSON request and waits for a
|
|
75
|
+
// JSON response on the same connection.
|
|
76
|
+
const server = net.createServer((conn) => {
|
|
77
|
+
let buf = '';
|
|
78
|
+
conn.on('data', async (chunk) => {
|
|
79
|
+
buf += chunk.toString();
|
|
80
|
+
const nlIdx = buf.indexOf('\n');
|
|
81
|
+
if (nlIdx === -1)
|
|
82
|
+
return; // wait for complete line
|
|
83
|
+
const line = buf.slice(0, nlIdx).trim();
|
|
84
|
+
buf = ''; // consume — one request per connection
|
|
85
|
+
try {
|
|
86
|
+
const { name, args } = JSON.parse(line);
|
|
87
|
+
currentOnToolEvent?.({ type: 'tool_call_start', name, args });
|
|
88
|
+
const { result, isError } = await currentExecutor(name, args);
|
|
89
|
+
currentOnToolEvent?.({ type: 'tool_call_result', name, result, isError });
|
|
90
|
+
conn.end(JSON.stringify({ result, isError }));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
logger?.error('MCP bridge tool execution error', err);
|
|
95
|
+
conn.end(JSON.stringify({ result: msg, isError: true }));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
server.on('error', reject);
|
|
101
|
+
server.listen(socketPath, () => resolve());
|
|
102
|
+
});
|
|
103
|
+
let cleaned = false;
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
if (cleaned)
|
|
106
|
+
return;
|
|
107
|
+
cleaned = true;
|
|
108
|
+
server.close();
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(socketPath);
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
try {
|
|
114
|
+
fs.unlinkSync(defsPath);
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
try {
|
|
118
|
+
fs.unlinkSync(configPath);
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
try {
|
|
122
|
+
fs.rmdirSync(tmpDir);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
};
|
|
126
|
+
const setHandlers = (exec, onEvt) => {
|
|
127
|
+
currentExecutor = exec;
|
|
128
|
+
currentOnToolEvent = onEvt;
|
|
129
|
+
};
|
|
130
|
+
return { configPath, setHandlers, cleanup };
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=mcp-bridge.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal MCP stdio server that bridges tool calls to a parent process.
|
|
4
|
+
*
|
|
5
|
+
* Spawned by the Claude CLI via --mcp-config. Advertises tool definitions
|
|
6
|
+
* and delegates execution to the parent process through a Unix domain socket.
|
|
7
|
+
*
|
|
8
|
+
* Environment:
|
|
9
|
+
* FW_TOOL_SOCKET - Unix socket path for tool execution RPC
|
|
10
|
+
* FW_TOOL_DEFS - Path to JSON file with tool definitions
|
|
11
|
+
*
|
|
12
|
+
* Protocol over Unix socket (newline-delimited JSON):
|
|
13
|
+
* Request: {"name":"fw_run_command","args":{"command":"ls"}}
|
|
14
|
+
* Response: {"result":"...","isError":false}
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT — Transport compatibility:
|
|
17
|
+
* Claude Code CLI v2.1.76+ uses NDJSON (newline-delimited JSON) for MCP
|
|
18
|
+
* stdio, NOT the Content-Length framing from the MCP SDK spec. This server
|
|
19
|
+
* auto-detects the transport on the first stdin chunk: if the first
|
|
20
|
+
* non-whitespace character is '{', it uses NDJSON; otherwise Content-Length.
|
|
21
|
+
* Responses match the detected format.
|
|
22
|
+
*
|
|
23
|
+
* The CLI also sends protocolVersion "2025-11-25" (not the spec's
|
|
24
|
+
* "2024-11-05"). We echo back whatever version the client requests.
|
|
25
|
+
*
|
|
26
|
+
* If these behaviors change in a future CLI version, the auto-detect
|
|
27
|
+
* and version echo should adapt automatically.
|
|
28
|
+
*/
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=mcp-tool-server.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal MCP stdio server that bridges tool calls to a parent process.
|
|
4
|
+
*
|
|
5
|
+
* Spawned by the Claude CLI via --mcp-config. Advertises tool definitions
|
|
6
|
+
* and delegates execution to the parent process through a Unix domain socket.
|
|
7
|
+
*
|
|
8
|
+
* Environment:
|
|
9
|
+
* FW_TOOL_SOCKET - Unix socket path for tool execution RPC
|
|
10
|
+
* FW_TOOL_DEFS - Path to JSON file with tool definitions
|
|
11
|
+
*
|
|
12
|
+
* Protocol over Unix socket (newline-delimited JSON):
|
|
13
|
+
* Request: {"name":"fw_run_command","args":{"command":"ls"}}
|
|
14
|
+
* Response: {"result":"...","isError":false}
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT — Transport compatibility:
|
|
17
|
+
* Claude Code CLI v2.1.76+ uses NDJSON (newline-delimited JSON) for MCP
|
|
18
|
+
* stdio, NOT the Content-Length framing from the MCP SDK spec. This server
|
|
19
|
+
* auto-detects the transport on the first stdin chunk: if the first
|
|
20
|
+
* non-whitespace character is '{', it uses NDJSON; otherwise Content-Length.
|
|
21
|
+
* Responses match the detected format.
|
|
22
|
+
*
|
|
23
|
+
* The CLI also sends protocolVersion "2025-11-25" (not the spec's
|
|
24
|
+
* "2024-11-05"). We echo back whatever version the client requests.
|
|
25
|
+
*
|
|
26
|
+
* If these behaviors change in a future CLI version, the auto-detect
|
|
27
|
+
* and version echo should adapt automatically.
|
|
28
|
+
*/
|
|
29
|
+
import * as net from 'node:net';
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
const SOCKET_PATH = process.env.FW_TOOL_SOCKET;
|
|
32
|
+
const DEFS_PATH = process.env.FW_TOOL_DEFS;
|
|
33
|
+
if (!SOCKET_PATH || !DEFS_PATH) {
|
|
34
|
+
process.stderr.write('mcp-tool-server: FW_TOOL_SOCKET and FW_TOOL_DEFS are required\n');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// Load tool definitions (written by the bridge before spawning the CLI)
|
|
38
|
+
const toolDefs = JSON.parse(fs.readFileSync(DEFS_PATH, 'utf-8'));
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Unix socket RPC: send tool call, receive result
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function callTool(name, args) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const client = net.createConnection(SOCKET_PATH, () => {
|
|
45
|
+
client.write(JSON.stringify({ name, args }) + '\n');
|
|
46
|
+
});
|
|
47
|
+
let buf = '';
|
|
48
|
+
client.on('data', (chunk) => {
|
|
49
|
+
buf += chunk.toString();
|
|
50
|
+
});
|
|
51
|
+
client.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
resolve(JSON.parse(buf.trim()));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
reject(new Error(`Invalid response from tool bridge: ${buf}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
client.on('error', (err) => reject(err));
|
|
60
|
+
// Safety timeout
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
client.destroy();
|
|
63
|
+
reject(new Error('Tool execution timed out'));
|
|
64
|
+
}, 130_000);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// MCP stdio protocol (JSON-RPC over stdin/stdout)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Auto-detect transport: NDJSON (newline-delimited) or Content-Length framed.
|
|
71
|
+
// Claude Code CLI uses NDJSON; standard MCP SDK uses Content-Length.
|
|
72
|
+
let useNdjson = false;
|
|
73
|
+
let transportDetected = false;
|
|
74
|
+
function send(msg) {
|
|
75
|
+
const json = JSON.stringify(msg);
|
|
76
|
+
if (useNdjson) {
|
|
77
|
+
process.stdout.write(json + '\n');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const header = `Content-Length: ${Buffer.byteLength(json)}\r\n\r\n`;
|
|
81
|
+
process.stdout.write(header + json);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function handleMessage(msg) {
|
|
85
|
+
const { method, id, params } = msg;
|
|
86
|
+
if (method === 'initialize') {
|
|
87
|
+
// Echo back the client's protocol version for compatibility
|
|
88
|
+
const clientVersion = params?.protocolVersion || '2024-11-05';
|
|
89
|
+
send({
|
|
90
|
+
jsonrpc: '2.0',
|
|
91
|
+
id,
|
|
92
|
+
result: {
|
|
93
|
+
protocolVersion: clientVersion,
|
|
94
|
+
capabilities: { tools: {} },
|
|
95
|
+
serverInfo: { name: 'fw-agent-tools', version: '1.0.0' },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (method === 'notifications/initialized') {
|
|
101
|
+
// No response needed for notifications
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (method === 'tools/list') {
|
|
105
|
+
send({
|
|
106
|
+
jsonrpc: '2.0',
|
|
107
|
+
id,
|
|
108
|
+
result: { tools: toolDefs },
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (method === 'tools/call') {
|
|
113
|
+
const toolName = params?.name;
|
|
114
|
+
const toolArgs = params?.arguments ?? {};
|
|
115
|
+
try {
|
|
116
|
+
const { result, isError } = await callTool(toolName, toolArgs);
|
|
117
|
+
send({
|
|
118
|
+
jsonrpc: '2.0',
|
|
119
|
+
id,
|
|
120
|
+
result: {
|
|
121
|
+
content: [{ type: 'text', text: result }],
|
|
122
|
+
isError,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
send({
|
|
128
|
+
jsonrpc: '2.0',
|
|
129
|
+
id,
|
|
130
|
+
result: {
|
|
131
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
132
|
+
isError: true,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Unknown method — return empty result for requests, ignore notifications
|
|
139
|
+
if (id !== undefined) {
|
|
140
|
+
send({ jsonrpc: '2.0', id, result: {} });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Read JSON-RPC messages from stdin (auto-detect NDJSON vs Content-Length)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
let inputBuf = '';
|
|
147
|
+
process.stdin.on('data', (chunk) => {
|
|
148
|
+
inputBuf += chunk.toString();
|
|
149
|
+
// Auto-detect transport on first data: if it starts with '{', it's NDJSON
|
|
150
|
+
if (!transportDetected) {
|
|
151
|
+
const trimmed = inputBuf.trimStart();
|
|
152
|
+
if (trimmed.startsWith('{')) {
|
|
153
|
+
useNdjson = true;
|
|
154
|
+
}
|
|
155
|
+
transportDetected = true;
|
|
156
|
+
}
|
|
157
|
+
if (useNdjson) {
|
|
158
|
+
processNdjson();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
processContentLength();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
function processNdjson() {
|
|
165
|
+
const lines = inputBuf.split('\n');
|
|
166
|
+
inputBuf = lines.pop() || '';
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
if (!line.trim())
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const msg = JSON.parse(line);
|
|
172
|
+
handleMessage(msg).catch((err) => {
|
|
173
|
+
process.stderr.write(`mcp-tool-server error: ${err}\n`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
process.stderr.write(`mcp-tool-server: failed to parse line: ${line.slice(0, 200)}\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function processContentLength() {
|
|
182
|
+
while (true) {
|
|
183
|
+
const headerEnd = inputBuf.indexOf('\r\n\r\n');
|
|
184
|
+
if (headerEnd === -1)
|
|
185
|
+
break;
|
|
186
|
+
const header = inputBuf.slice(0, headerEnd);
|
|
187
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
188
|
+
if (!match) {
|
|
189
|
+
inputBuf = inputBuf.slice(headerEnd + 4);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const contentLength = parseInt(match[1], 10);
|
|
193
|
+
const bodyStart = headerEnd + 4;
|
|
194
|
+
if (inputBuf.length < bodyStart + contentLength)
|
|
195
|
+
break;
|
|
196
|
+
const body = inputBuf.slice(bodyStart, bodyStart + contentLength);
|
|
197
|
+
inputBuf = inputBuf.slice(bodyStart + contentLength);
|
|
198
|
+
try {
|
|
199
|
+
const msg = JSON.parse(body);
|
|
200
|
+
handleMessage(msg).catch((err) => {
|
|
201
|
+
process.stderr.write(`mcp-tool-server error: ${err}\n`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
process.stderr.write(`mcp-tool-server: failed to parse message: ${body}\n`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
process.stdin.on('end', () => process.exit(0));
|
|
210
|
+
//# sourceMappingURL=mcp-tool-server.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic API provider — streams messages via raw fetch + SSE parsing.
|
|
3
|
+
* No SDK dependency. Uses only Node.js native fetch (available since Node 18).
|
|
4
|
+
*
|
|
5
|
+
* Adapted from pack-weaver's streamAnthropicWithTools.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentProvider, AgentMessage, ToolDefinition, StreamEvent, StreamOptions } from '../types.js';
|
|
8
|
+
export interface AnthropicProviderOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class AnthropicProvider implements AgentProvider {
|
|
15
|
+
private apiKey;
|
|
16
|
+
private model;
|
|
17
|
+
private maxTokens;
|
|
18
|
+
private baseUrl;
|
|
19
|
+
constructor(options: AnthropicProviderOptions);
|
|
20
|
+
stream(messages: AgentMessage[], tools: ToolDefinition[], options?: StreamOptions): AsyncGenerator<StreamEvent>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createAnthropicProvider(options: AnthropicProviderOptions): AnthropicProvider;
|
|
23
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic API provider — streams messages via raw fetch + SSE parsing.
|
|
3
|
+
* No SDK dependency. Uses only Node.js native fetch (available since Node 18).
|
|
4
|
+
*
|
|
5
|
+
* Adapted from pack-weaver's streamAnthropicWithTools.
|
|
6
|
+
*/
|
|
7
|
+
export class AnthropicProvider {
|
|
8
|
+
apiKey;
|
|
9
|
+
model;
|
|
10
|
+
maxTokens;
|
|
11
|
+
baseUrl;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
if (!options.apiKey) {
|
|
14
|
+
throw new Error('AnthropicProvider requires an API key');
|
|
15
|
+
}
|
|
16
|
+
this.apiKey = options.apiKey;
|
|
17
|
+
this.model = options.model ?? 'claude-sonnet-4-20250514';
|
|
18
|
+
this.maxTokens = options.maxTokens ?? 8192;
|
|
19
|
+
this.baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
|
|
20
|
+
}
|
|
21
|
+
async *stream(messages, tools, options) {
|
|
22
|
+
const model = options?.model ?? this.model;
|
|
23
|
+
const maxTokens = options?.maxTokens ?? this.maxTokens;
|
|
24
|
+
// Build Anthropic API request body
|
|
25
|
+
const apiMessages = messages.map((m) => {
|
|
26
|
+
if (m.role === 'tool') {
|
|
27
|
+
return {
|
|
28
|
+
role: 'user',
|
|
29
|
+
content: [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
33
|
+
const blocks = [];
|
|
34
|
+
if (m.content)
|
|
35
|
+
blocks.push({ type: 'text', text: m.content });
|
|
36
|
+
for (const tc of m.toolCalls) {
|
|
37
|
+
blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments });
|
|
38
|
+
}
|
|
39
|
+
return { role: 'assistant', content: blocks };
|
|
40
|
+
}
|
|
41
|
+
return { role: m.role, content: m.content };
|
|
42
|
+
});
|
|
43
|
+
const apiTools = tools.map((t) => ({
|
|
44
|
+
name: t.name,
|
|
45
|
+
description: t.description,
|
|
46
|
+
input_schema: t.inputSchema,
|
|
47
|
+
}));
|
|
48
|
+
const body = JSON.stringify({
|
|
49
|
+
model,
|
|
50
|
+
max_tokens: maxTokens,
|
|
51
|
+
stream: true,
|
|
52
|
+
...(options?.systemPrompt ? { system: options.systemPrompt } : {}),
|
|
53
|
+
messages: apiMessages,
|
|
54
|
+
...(apiTools.length > 0 ? { tools: apiTools } : {}),
|
|
55
|
+
});
|
|
56
|
+
const response = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'x-api-key': this.apiKey,
|
|
60
|
+
'anthropic-version': '2025-04-15',
|
|
61
|
+
'content-type': 'application/json',
|
|
62
|
+
},
|
|
63
|
+
body,
|
|
64
|
+
signal: options?.signal ?? AbortSignal.timeout(300_000),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const err = await response.text();
|
|
68
|
+
throw new Error(`Anthropic API error ${response.status}: ${err.slice(0, 200)}`);
|
|
69
|
+
}
|
|
70
|
+
if (!response.body)
|
|
71
|
+
throw new Error('No response body');
|
|
72
|
+
// Parse SSE stream
|
|
73
|
+
const reader = response.body.getReader();
|
|
74
|
+
const decoder = new TextDecoder();
|
|
75
|
+
let buffer = '';
|
|
76
|
+
const activeToolUses = new Map();
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done)
|
|
81
|
+
break;
|
|
82
|
+
buffer += decoder.decode(value, { stream: true });
|
|
83
|
+
const lines = buffer.split('\n');
|
|
84
|
+
buffer = lines.pop() || '';
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line.startsWith('data: '))
|
|
87
|
+
continue;
|
|
88
|
+
const jsonStr = line.slice(6).trim();
|
|
89
|
+
if (jsonStr === '[DONE]')
|
|
90
|
+
continue;
|
|
91
|
+
let event;
|
|
92
|
+
try {
|
|
93
|
+
event = JSON.parse(jsonStr);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const eventType = event.type;
|
|
99
|
+
if (eventType === 'content_block_start') {
|
|
100
|
+
const block = event.content_block;
|
|
101
|
+
const index = event.index;
|
|
102
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
103
|
+
activeToolUses.set(index, { id: block.id, name: block.name, jsonChunks: [] });
|
|
104
|
+
yield { type: 'tool_use_start', id: block.id, name: block.name };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (eventType === 'content_block_delta') {
|
|
108
|
+
const delta = event.delta;
|
|
109
|
+
const index = event.index;
|
|
110
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
111
|
+
yield { type: 'text_delta', text: delta.text };
|
|
112
|
+
}
|
|
113
|
+
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
114
|
+
yield { type: 'thinking_delta', text: delta.thinking };
|
|
115
|
+
}
|
|
116
|
+
if (delta.type === 'input_json_delta' && delta.partial_json !== undefined) {
|
|
117
|
+
const active = activeToolUses.get(index);
|
|
118
|
+
if (active) {
|
|
119
|
+
active.jsonChunks.push(delta.partial_json);
|
|
120
|
+
yield { type: 'tool_use_delta', id: active.id, partialJson: delta.partial_json };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (eventType === 'content_block_stop') {
|
|
125
|
+
const index = event.index;
|
|
126
|
+
const active = activeToolUses.get(index);
|
|
127
|
+
if (active) {
|
|
128
|
+
activeToolUses.delete(index);
|
|
129
|
+
let args = {};
|
|
130
|
+
try {
|
|
131
|
+
args = JSON.parse(active.jsonChunks.join(''));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* malformed */
|
|
135
|
+
}
|
|
136
|
+
yield { type: 'tool_use_end', id: active.id, arguments: args };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (eventType === 'message_start' && event.message?.usage) {
|
|
140
|
+
const usage = event.message.usage;
|
|
141
|
+
yield {
|
|
142
|
+
type: 'usage',
|
|
143
|
+
promptTokens: usage.input_tokens ?? 0,
|
|
144
|
+
completionTokens: usage.output_tokens ?? 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (eventType === 'message_delta') {
|
|
148
|
+
const delta = event.delta;
|
|
149
|
+
if (event.usage) {
|
|
150
|
+
const usage = event.usage;
|
|
151
|
+
yield {
|
|
152
|
+
type: 'usage',
|
|
153
|
+
promptTokens: 0,
|
|
154
|
+
completionTokens: usage.output_tokens ?? 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (delta?.stop_reason === 'tool_use') {
|
|
158
|
+
yield { type: 'message_stop', finishReason: 'tool_calls' };
|
|
159
|
+
}
|
|
160
|
+
else if (delta?.stop_reason === 'end_turn') {
|
|
161
|
+
yield { type: 'message_stop', finishReason: 'stop' };
|
|
162
|
+
}
|
|
163
|
+
else if (delta?.stop_reason === 'max_tokens') {
|
|
164
|
+
yield { type: 'message_stop', finishReason: 'length' };
|
|
165
|
+
}
|
|
166
|
+
else if (delta?.stop_reason) {
|
|
167
|
+
yield { type: 'message_stop', finishReason: 'stop' };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (eventType === 'error') {
|
|
171
|
+
const errObj = event.error;
|
|
172
|
+
throw new Error(`Anthropic stream error: ${errObj?.message ?? 'unknown'}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
reader.releaseLock();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function createAnthropicProvider(options) {
|
|
183
|
+
return new AnthropicProvider(options);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI provider — spawns the Claude Code CLI with stream-json output
|
|
3
|
+
* and MCP bridge for tool execution.
|
|
4
|
+
*
|
|
5
|
+
* Adapted from platform's streamClaudeCliChat. Platform-specific dependencies
|
|
6
|
+
* (spawnSandboxed, getBinPath, config) are replaced with injectable options.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentProvider, AgentMessage, ToolDefinition, StreamEvent, StreamOptions, ClaudeCliProviderOptions } from '../types.js';
|
|
9
|
+
export declare class ClaudeCliProvider implements AgentProvider {
|
|
10
|
+
private binPath;
|
|
11
|
+
private cwd;
|
|
12
|
+
private env;
|
|
13
|
+
private model;
|
|
14
|
+
private mcpConfigPath;
|
|
15
|
+
private spawnFn;
|
|
16
|
+
private timeout;
|
|
17
|
+
constructor(options?: ClaudeCliProviderOptions);
|
|
18
|
+
stream(messages: AgentMessage[], tools: ToolDefinition[], options?: StreamOptions): AsyncGenerator<StreamEvent>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createClaudeCliProvider(options?: ClaudeCliProviderOptions): ClaudeCliProvider;
|
|
21
|
+
//# sourceMappingURL=claude-cli.d.ts.map
|