@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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic agent loop — streams LLM responses, collects tool calls,
|
|
3
|
+
* executes them via a caller-provided executor, and iterates.
|
|
4
|
+
*
|
|
5
|
+
* The loop never touches provider internals. Adding a new provider (Codex,
|
|
6
|
+
* Gemini, etc.) requires zero loop changes — just implement AgentProvider.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentProvider, AgentMessage, ToolDefinition, ToolExecutor, AgentLoopOptions, AgentLoopResult } from './types.js';
|
|
9
|
+
export declare function runAgentLoop(provider: AgentProvider, tools: ToolDefinition[], executor: ToolExecutor, messages: AgentMessage[], options?: AgentLoopOptions): Promise<AgentLoopResult>;
|
|
10
|
+
//# sourceMappingURL=agent-loop.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic agent loop — streams LLM responses, collects tool calls,
|
|
3
|
+
* executes them via a caller-provided executor, and iterates.
|
|
4
|
+
*
|
|
5
|
+
* The loop never touches provider internals. Adding a new provider (Codex,
|
|
6
|
+
* Gemini, etc.) requires zero loop changes — just implement AgentProvider.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_MAX_ITERATIONS = 15;
|
|
9
|
+
const TOOL_RESULT_CAP = 10_000; // bytes
|
|
10
|
+
export async function runAgentLoop(provider, tools, executor, messages, options) {
|
|
11
|
+
const maxIterations = options?.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
12
|
+
const signal = options?.signal;
|
|
13
|
+
const onStreamEvent = options?.onStreamEvent;
|
|
14
|
+
const onToolEvent = options?.onToolEvent;
|
|
15
|
+
const conversation = [...messages];
|
|
16
|
+
let totalPromptTokens = 0;
|
|
17
|
+
let totalCompletionTokens = 0;
|
|
18
|
+
let toolCallCount = 0;
|
|
19
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
return buildResult(false, 'Aborted', conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
|
|
22
|
+
}
|
|
23
|
+
// Stream from provider
|
|
24
|
+
let text = '';
|
|
25
|
+
let finishReason = 'stop';
|
|
26
|
+
const collectedToolCalls = [];
|
|
27
|
+
const activeToolNames = new Map();
|
|
28
|
+
const stream = provider.stream(conversation, tools, {
|
|
29
|
+
systemPrompt: options?.systemPrompt,
|
|
30
|
+
model: options?.model,
|
|
31
|
+
maxTokens: options?.maxTokens,
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
for await (const event of stream) {
|
|
35
|
+
onStreamEvent?.(event);
|
|
36
|
+
switch (event.type) {
|
|
37
|
+
case 'text_delta':
|
|
38
|
+
text += event.text;
|
|
39
|
+
break;
|
|
40
|
+
case 'tool_use_start':
|
|
41
|
+
activeToolNames.set(event.id, event.name);
|
|
42
|
+
break;
|
|
43
|
+
case 'tool_use_delta':
|
|
44
|
+
// Partial JSON — tracked by provider, nothing to do here
|
|
45
|
+
break;
|
|
46
|
+
case 'tool_use_end':
|
|
47
|
+
collectedToolCalls.push({
|
|
48
|
+
id: event.id,
|
|
49
|
+
name: activeToolNames.get(event.id) ?? 'unknown',
|
|
50
|
+
arguments: event.arguments,
|
|
51
|
+
});
|
|
52
|
+
activeToolNames.delete(event.id);
|
|
53
|
+
break;
|
|
54
|
+
case 'usage':
|
|
55
|
+
totalPromptTokens += event.promptTokens;
|
|
56
|
+
totalCompletionTokens += event.completionTokens;
|
|
57
|
+
break;
|
|
58
|
+
case 'message_stop':
|
|
59
|
+
finishReason = event.finishReason;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Add assistant message to conversation
|
|
64
|
+
if (collectedToolCalls.length > 0) {
|
|
65
|
+
conversation.push({
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
content: text || '',
|
|
68
|
+
toolCalls: collectedToolCalls,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (text) {
|
|
72
|
+
conversation.push({ role: 'assistant', content: text });
|
|
73
|
+
}
|
|
74
|
+
// If no tool calls, we're done
|
|
75
|
+
if (finishReason !== 'tool_calls' || collectedToolCalls.length === 0) {
|
|
76
|
+
return buildResult(finishReason !== 'error', text || 'Task completed', conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
|
|
77
|
+
}
|
|
78
|
+
// Execute tool calls and add results to conversation
|
|
79
|
+
for (const tc of collectedToolCalls) {
|
|
80
|
+
if (signal?.aborted)
|
|
81
|
+
break;
|
|
82
|
+
toolCallCount++;
|
|
83
|
+
onToolEvent?.({ type: 'tool_call_start', name: tc.name, args: tc.arguments });
|
|
84
|
+
let result;
|
|
85
|
+
let isError;
|
|
86
|
+
try {
|
|
87
|
+
const res = await executor(tc.name, tc.arguments);
|
|
88
|
+
result = res.result;
|
|
89
|
+
isError = res.isError;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
result = err instanceof Error ? err.message : String(err);
|
|
93
|
+
isError = true;
|
|
94
|
+
}
|
|
95
|
+
onToolEvent?.({ type: 'tool_call_result', name: tc.name, result: result.slice(0, 200), isError });
|
|
96
|
+
// Add tool result to conversation (cap size to prevent context overflow)
|
|
97
|
+
conversation.push({
|
|
98
|
+
role: 'tool',
|
|
99
|
+
content: result.slice(0, TOOL_RESULT_CAP),
|
|
100
|
+
toolCallId: tc.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return buildResult(false, `Reached max iterations (${maxIterations})`, conversation, toolCallCount, totalPromptTokens, totalCompletionTokens);
|
|
105
|
+
}
|
|
106
|
+
function buildResult(success, summary, messages, toolCallCount, promptTokens, completionTokens) {
|
|
107
|
+
return {
|
|
108
|
+
success,
|
|
109
|
+
summary,
|
|
110
|
+
messages,
|
|
111
|
+
toolCallCount,
|
|
112
|
+
usage: { promptTokens, completionTokens },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=agent-loop.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent CLI session manager — eliminates cold-start delay by keeping
|
|
3
|
+
* the Claude CLI process alive between messages.
|
|
4
|
+
*
|
|
5
|
+
* Instead of spawning a new CLI process per message (~5s cold start + MCP
|
|
6
|
+
* handshake), we spawn once with `--input-format stream-json` and feed
|
|
7
|
+
* messages via stdin. The CLI maintains conversation context internally.
|
|
8
|
+
*
|
|
9
|
+
* KEY LEARNINGS (Claude Code CLI v2.1.76):
|
|
10
|
+
*
|
|
11
|
+
* 1. Stdin message format (NDJSON):
|
|
12
|
+
* {"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null}
|
|
13
|
+
*
|
|
14
|
+
* 2. The CLI stays alive between messages when stdin is kept open.
|
|
15
|
+
* Each message gets its own system/init → stream_events → result cycle.
|
|
16
|
+
*
|
|
17
|
+
* 3. Turn boundary: the `result` event marks the end of a CLI turn, NOT
|
|
18
|
+
* `message_stop` from stream_event.
|
|
19
|
+
*
|
|
20
|
+
* 4. The CLI uses NDJSON for MCP stdio transport (not Content-Length framing).
|
|
21
|
+
*/
|
|
22
|
+
import { type ChildProcess } from 'node:child_process';
|
|
23
|
+
import type { StreamEvent, CliSessionOptions } from './types.js';
|
|
24
|
+
export declare class CliSession {
|
|
25
|
+
private readonly options;
|
|
26
|
+
readonly sessionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
27
|
+
private child;
|
|
28
|
+
private alive;
|
|
29
|
+
private stderrBuf;
|
|
30
|
+
private stdoutBuffer;
|
|
31
|
+
private activeTurn;
|
|
32
|
+
private idleTimer;
|
|
33
|
+
private cleanupFn;
|
|
34
|
+
private parser;
|
|
35
|
+
private readonly log;
|
|
36
|
+
private readonly spawnFn;
|
|
37
|
+
private readonly idleTimeout;
|
|
38
|
+
constructor(options: CliSessionOptions);
|
|
39
|
+
get ready(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Inject a mock child process for testing.
|
|
42
|
+
* @internal — test only
|
|
43
|
+
*/
|
|
44
|
+
_injectForTest(child: ChildProcess): void;
|
|
45
|
+
/**
|
|
46
|
+
* Spawn the CLI process. Must be called before send().
|
|
47
|
+
*/
|
|
48
|
+
spawn(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Send a user message and stream back events.
|
|
51
|
+
* Auto-respawns if the process has died.
|
|
52
|
+
*/
|
|
53
|
+
send(userMessage: string, systemPromptPrefix?: string): AsyncGenerator<StreamEvent>;
|
|
54
|
+
/**
|
|
55
|
+
* Kill the CLI process.
|
|
56
|
+
*/
|
|
57
|
+
kill(): void;
|
|
58
|
+
private pushEvent;
|
|
59
|
+
private completeTurn;
|
|
60
|
+
private markDead;
|
|
61
|
+
private onStdoutData;
|
|
62
|
+
private resetIdleTimer;
|
|
63
|
+
private clearIdleTimer;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get an existing session or create a new one.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getOrCreateCliSession(key: string, options: CliSessionOptions): CliSession;
|
|
69
|
+
/**
|
|
70
|
+
* Kill a specific session.
|
|
71
|
+
*/
|
|
72
|
+
export declare function killCliSession(key: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Kill all CLI sessions (for shutdown).
|
|
75
|
+
*/
|
|
76
|
+
export declare function killAllCliSessions(): void;
|
|
77
|
+
//# sourceMappingURL=cli-session.d.ts.map
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent CLI session manager — eliminates cold-start delay by keeping
|
|
3
|
+
* the Claude CLI process alive between messages.
|
|
4
|
+
*
|
|
5
|
+
* Instead of spawning a new CLI process per message (~5s cold start + MCP
|
|
6
|
+
* handshake), we spawn once with `--input-format stream-json` and feed
|
|
7
|
+
* messages via stdin. The CLI maintains conversation context internally.
|
|
8
|
+
*
|
|
9
|
+
* KEY LEARNINGS (Claude Code CLI v2.1.76):
|
|
10
|
+
*
|
|
11
|
+
* 1. Stdin message format (NDJSON):
|
|
12
|
+
* {"type":"user","message":{"role":"user","content":"..."},"parent_tool_use_id":null}
|
|
13
|
+
*
|
|
14
|
+
* 2. The CLI stays alive between messages when stdin is kept open.
|
|
15
|
+
* Each message gets its own system/init → stream_events → result cycle.
|
|
16
|
+
*
|
|
17
|
+
* 3. Turn boundary: the `result` event marks the end of a CLI turn, NOT
|
|
18
|
+
* `message_stop` from stream_event.
|
|
19
|
+
*
|
|
20
|
+
* 4. The CLI uses NDJSON for MCP stdio transport (not Content-Length framing).
|
|
21
|
+
*/
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
24
|
+
import { StreamJsonParser } from './streaming.js';
|
|
25
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
26
|
+
export class CliSession {
|
|
27
|
+
options;
|
|
28
|
+
sessionId = randomUUID();
|
|
29
|
+
child = null;
|
|
30
|
+
alive = false;
|
|
31
|
+
stderrBuf = '';
|
|
32
|
+
stdoutBuffer = '';
|
|
33
|
+
activeTurn = null;
|
|
34
|
+
idleTimer = null;
|
|
35
|
+
cleanupFn = null;
|
|
36
|
+
parser;
|
|
37
|
+
log;
|
|
38
|
+
spawnFn;
|
|
39
|
+
idleTimeout;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.log = options.logger;
|
|
43
|
+
this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
|
|
44
|
+
this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
45
|
+
// Parser delegates to pushEvent which routes to activeTurn
|
|
46
|
+
this.parser = new StreamJsonParser((event) => this.pushEvent(event));
|
|
47
|
+
}
|
|
48
|
+
get ready() {
|
|
49
|
+
return this.alive;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Inject a mock child process for testing.
|
|
53
|
+
* @internal — test only
|
|
54
|
+
*/
|
|
55
|
+
_injectForTest(child) {
|
|
56
|
+
this.child = child;
|
|
57
|
+
this.alive = true;
|
|
58
|
+
this.stderrBuf = '';
|
|
59
|
+
this.stdoutBuffer = '';
|
|
60
|
+
child.stdout.on('data', (chunk) => {
|
|
61
|
+
this.onStdoutData(chunk.toString());
|
|
62
|
+
});
|
|
63
|
+
child.on('exit', () => this.markDead());
|
|
64
|
+
child.on('error', () => this.markDead());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Spawn the CLI process. Must be called before send().
|
|
68
|
+
*/
|
|
69
|
+
async spawn() {
|
|
70
|
+
const { binPath, cwd, env, model, mcpConfigPath } = this.options;
|
|
71
|
+
const args = [
|
|
72
|
+
'-p',
|
|
73
|
+
'--input-format',
|
|
74
|
+
'stream-json',
|
|
75
|
+
'--output-format',
|
|
76
|
+
'stream-json',
|
|
77
|
+
'--include-partial-messages',
|
|
78
|
+
'--verbose',
|
|
79
|
+
'--dangerously-skip-permissions',
|
|
80
|
+
'--permission-mode',
|
|
81
|
+
'bypassPermissions',
|
|
82
|
+
'--setting-sources',
|
|
83
|
+
'user,local',
|
|
84
|
+
'--model',
|
|
85
|
+
model,
|
|
86
|
+
];
|
|
87
|
+
if (mcpConfigPath) {
|
|
88
|
+
args.push('--mcp-config', mcpConfigPath, '--strict-mcp-config');
|
|
89
|
+
}
|
|
90
|
+
const spawnResult = this.spawnFn(binPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: env ?? process.env });
|
|
91
|
+
const child = 'child' in spawnResult ? spawnResult.child : spawnResult;
|
|
92
|
+
const cleanup = 'cleanup' in spawnResult ? spawnResult.cleanup : undefined;
|
|
93
|
+
this.child = child;
|
|
94
|
+
this.cleanupFn = cleanup ?? null;
|
|
95
|
+
this.alive = true;
|
|
96
|
+
this.stderrBuf = '';
|
|
97
|
+
this.stdoutBuffer = '';
|
|
98
|
+
child.stderr.on('data', (chunk) => {
|
|
99
|
+
this.stderrBuf += chunk.toString();
|
|
100
|
+
});
|
|
101
|
+
child.stdout.on('data', (chunk) => {
|
|
102
|
+
this.onStdoutData(chunk.toString());
|
|
103
|
+
});
|
|
104
|
+
child.on('exit', (code) => {
|
|
105
|
+
this.log?.info('CLI session process exited', { sessionId: this.sessionId, exitCode: code });
|
|
106
|
+
this.markDead();
|
|
107
|
+
});
|
|
108
|
+
child.on('error', (err) => {
|
|
109
|
+
this.log?.error('CLI session process error', { sessionId: this.sessionId, err });
|
|
110
|
+
this.markDead();
|
|
111
|
+
});
|
|
112
|
+
this.resetIdleTimer();
|
|
113
|
+
this.log?.info('CLI session spawned', { sessionId: this.sessionId, cwd, model });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Send a user message and stream back events.
|
|
117
|
+
* Auto-respawns if the process has died.
|
|
118
|
+
*/
|
|
119
|
+
async *send(userMessage, systemPromptPrefix) {
|
|
120
|
+
if (!this.alive) {
|
|
121
|
+
this.log?.info('CLI session dead, respawning', { sessionId: this.sessionId });
|
|
122
|
+
await this.spawn();
|
|
123
|
+
}
|
|
124
|
+
this.resetIdleTimer();
|
|
125
|
+
this.parser.reset();
|
|
126
|
+
const content = systemPromptPrefix ? `${systemPromptPrefix}\n\n${userMessage}` : userMessage;
|
|
127
|
+
// Write NDJSON message to stdin
|
|
128
|
+
const ndjsonMessage = JSON.stringify({
|
|
129
|
+
type: 'user',
|
|
130
|
+
message: { role: 'user', content },
|
|
131
|
+
parent_tool_use_id: null,
|
|
132
|
+
}) + '\n';
|
|
133
|
+
const turn = { resolve: () => { }, events: [], done: false };
|
|
134
|
+
this.activeTurn = turn;
|
|
135
|
+
// Track whether this turn saw a 'result' event (definitive turn end)
|
|
136
|
+
let sawResult = false;
|
|
137
|
+
// Wrap pushEvent to detect result-driven message_stop as turn end
|
|
138
|
+
const originalPush = this.pushEvent.bind(this);
|
|
139
|
+
this.parser = new StreamJsonParser((event) => {
|
|
140
|
+
// The result event emits message_stop — but in session mode,
|
|
141
|
+
// we need to detect it as the turn boundary
|
|
142
|
+
if (event.type === 'message_stop' && !sawResult) {
|
|
143
|
+
// This is a stream_event message_stop (API turn), not CLI turn end.
|
|
144
|
+
// Push it but don't complete the turn.
|
|
145
|
+
originalPush(event);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
originalPush(event);
|
|
149
|
+
});
|
|
150
|
+
// Override parser feed to detect result events for turn completion
|
|
151
|
+
const baseFeed = this.parser.feed.bind(this.parser);
|
|
152
|
+
this.parser.feed = (line) => {
|
|
153
|
+
// Check if this line is a result event before parsing
|
|
154
|
+
try {
|
|
155
|
+
let parsed = JSON.parse(line);
|
|
156
|
+
if (parsed.type === 'stream_event' && parsed.event)
|
|
157
|
+
parsed = parsed.event;
|
|
158
|
+
if (parsed.type === 'result') {
|
|
159
|
+
sawResult = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Not JSON, let parser handle it
|
|
164
|
+
}
|
|
165
|
+
baseFeed(line);
|
|
166
|
+
if (sawResult) {
|
|
167
|
+
this.completeTurn();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
try {
|
|
171
|
+
this.child.stdin.write(ndjsonMessage, (err) => {
|
|
172
|
+
if (err) {
|
|
173
|
+
this.log?.error('stdin write error', { sessionId: this.sessionId, err });
|
|
174
|
+
this.markDead();
|
|
175
|
+
turn.done = true;
|
|
176
|
+
turn.resolve();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
this.log?.error('stdin write exception', { sessionId: this.sessionId, err });
|
|
182
|
+
this.markDead();
|
|
183
|
+
throw new Error('CLI session stdin write failed');
|
|
184
|
+
}
|
|
185
|
+
// Yield events as they arrive
|
|
186
|
+
while (!turn.done || turn.events.length > 0) {
|
|
187
|
+
if (turn.events.length > 0) {
|
|
188
|
+
yield turn.events.shift();
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
await new Promise((r) => {
|
|
192
|
+
turn.resolve = r;
|
|
193
|
+
setTimeout(r, 50);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Yield any remaining events
|
|
198
|
+
while (turn.events.length > 0) {
|
|
199
|
+
yield turn.events.shift();
|
|
200
|
+
}
|
|
201
|
+
this.activeTurn = null;
|
|
202
|
+
this.resetIdleTimer();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Kill the CLI process.
|
|
206
|
+
*/
|
|
207
|
+
kill() {
|
|
208
|
+
this.clearIdleTimer();
|
|
209
|
+
if (this.child && this.alive) {
|
|
210
|
+
this.log?.info('Killing CLI session', { sessionId: this.sessionId });
|
|
211
|
+
this.child.kill('SIGTERM');
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (this.child && !this.child.killed) {
|
|
214
|
+
this.child.kill('SIGKILL');
|
|
215
|
+
}
|
|
216
|
+
}, 2000);
|
|
217
|
+
}
|
|
218
|
+
this.markDead();
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Private
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
pushEvent(event) {
|
|
224
|
+
if (!this.activeTurn)
|
|
225
|
+
return;
|
|
226
|
+
this.activeTurn.events.push(event);
|
|
227
|
+
this.activeTurn.resolve();
|
|
228
|
+
}
|
|
229
|
+
completeTurn() {
|
|
230
|
+
if (!this.activeTurn)
|
|
231
|
+
return;
|
|
232
|
+
this.activeTurn.done = true;
|
|
233
|
+
this.activeTurn.resolve();
|
|
234
|
+
}
|
|
235
|
+
markDead() {
|
|
236
|
+
this.alive = false;
|
|
237
|
+
this.cleanupFn?.();
|
|
238
|
+
this.cleanupFn = null;
|
|
239
|
+
if (this.activeTurn && !this.activeTurn.done) {
|
|
240
|
+
if (!this.activeTurn.events.some((e) => e.type === 'message_stop')) {
|
|
241
|
+
this.activeTurn.events.push({ type: 'message_stop', finishReason: 'error' });
|
|
242
|
+
}
|
|
243
|
+
this.activeTurn.done = true;
|
|
244
|
+
this.activeTurn.resolve();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
onStdoutData(data) {
|
|
248
|
+
this.stdoutBuffer += data;
|
|
249
|
+
const lines = this.stdoutBuffer.split('\n');
|
|
250
|
+
this.stdoutBuffer = lines.pop() || '';
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
this.parser.feed(line);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
resetIdleTimer() {
|
|
256
|
+
this.clearIdleTimer();
|
|
257
|
+
this.idleTimer = setTimeout(() => {
|
|
258
|
+
this.log?.info('CLI session idle timeout, killing', { sessionId: this.sessionId });
|
|
259
|
+
this.kill();
|
|
260
|
+
}, this.idleTimeout);
|
|
261
|
+
}
|
|
262
|
+
clearIdleTimer() {
|
|
263
|
+
if (this.idleTimer) {
|
|
264
|
+
clearTimeout(this.idleTimer);
|
|
265
|
+
this.idleTimer = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Session manager — cache keyed by identifier
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
const sessions = new Map();
|
|
273
|
+
/**
|
|
274
|
+
* Get an existing session or create a new one.
|
|
275
|
+
*/
|
|
276
|
+
export function getOrCreateCliSession(key, options) {
|
|
277
|
+
const existing = sessions.get(key);
|
|
278
|
+
if (existing && existing.ready) {
|
|
279
|
+
return existing;
|
|
280
|
+
}
|
|
281
|
+
// Kill stale session if present
|
|
282
|
+
if (existing) {
|
|
283
|
+
existing.kill();
|
|
284
|
+
sessions.delete(key);
|
|
285
|
+
}
|
|
286
|
+
const session = new CliSession(options);
|
|
287
|
+
sessions.set(key, session);
|
|
288
|
+
return session;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Kill a specific session.
|
|
292
|
+
*/
|
|
293
|
+
export function killCliSession(key) {
|
|
294
|
+
const session = sessions.get(key);
|
|
295
|
+
if (session) {
|
|
296
|
+
session.kill();
|
|
297
|
+
sessions.delete(key);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Kill all CLI sessions (for shutdown).
|
|
302
|
+
*/
|
|
303
|
+
export function killAllCliSessions() {
|
|
304
|
+
for (const [, session] of sessions) {
|
|
305
|
+
session.kill();
|
|
306
|
+
}
|
|
307
|
+
sessions.clear();
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=cli-session.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlisted environment variables for spawned CLI processes.
|
|
3
|
+
*
|
|
4
|
+
* Only these variables are forwarded from the host process to prevent
|
|
5
|
+
* leaking secrets, user identity, SSH agent sockets, workspace paths,
|
|
6
|
+
* and other sensitive host data to AI CLI tools.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Minimal PATH for sandboxed AI CLI processes. Contains only standard
|
|
10
|
+
* system directories. CLI binaries are invoked via `process.execPath`
|
|
11
|
+
* (node) directly, bypassing shebang resolution, so the node binary
|
|
12
|
+
* directory is NOT needed here.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MINIMAL_PATH = "/usr/local/bin:/usr/bin:/bin";
|
|
15
|
+
export declare const ENV_ALLOWLIST: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* Build a safe environment object from the current process, forwarding
|
|
18
|
+
* only allowlisted keys. Callers can spread additional overrides on top.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildSafeEnv(overrides?: Record<string, string | undefined>): NodeJS.ProcessEnv;
|
|
21
|
+
/**
|
|
22
|
+
* Convenience helper that returns both `cwd` and `env` in one object,
|
|
23
|
+
* making it harder to forget either when spawning a child process.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSafeSpawnOpts(cwd: string, envOverrides?: Record<string, string | undefined>): {
|
|
26
|
+
cwd: string;
|
|
27
|
+
env: NodeJS.ProcessEnv;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=env-allowlist.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlisted environment variables for spawned CLI processes.
|
|
3
|
+
*
|
|
4
|
+
* Only these variables are forwarded from the host process to prevent
|
|
5
|
+
* leaking secrets, user identity, SSH agent sockets, workspace paths,
|
|
6
|
+
* and other sensitive host data to AI CLI tools.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Minimal PATH for sandboxed AI CLI processes. Contains only standard
|
|
10
|
+
* system directories. CLI binaries are invoked via `process.execPath`
|
|
11
|
+
* (node) directly, bypassing shebang resolution, so the node binary
|
|
12
|
+
* directory is NOT needed here.
|
|
13
|
+
*/
|
|
14
|
+
export const MINIMAL_PATH = '/usr/local/bin:/usr/bin:/bin';
|
|
15
|
+
export const ENV_ALLOWLIST = [
|
|
16
|
+
// System basics
|
|
17
|
+
'PATH',
|
|
18
|
+
'LANG',
|
|
19
|
+
'LC_ALL',
|
|
20
|
+
'LC_CTYPE',
|
|
21
|
+
'TERM',
|
|
22
|
+
'TMPDIR',
|
|
23
|
+
'TZ',
|
|
24
|
+
// Node/runtime
|
|
25
|
+
'NODE_ENV',
|
|
26
|
+
'NO_COLOR',
|
|
27
|
+
'FORCE_COLOR',
|
|
28
|
+
// TLS/certificates
|
|
29
|
+
'NODE_EXTRA_CA_CERTS',
|
|
30
|
+
'SSL_CERT_FILE',
|
|
31
|
+
'SSL_CERT_DIR',
|
|
32
|
+
// Network proxy (needed behind corporate proxies)
|
|
33
|
+
'HTTP_PROXY',
|
|
34
|
+
'HTTPS_PROXY',
|
|
35
|
+
'NO_PROXY',
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Build a safe environment object from the current process, forwarding
|
|
39
|
+
* only allowlisted keys. Callers can spread additional overrides on top.
|
|
40
|
+
*/
|
|
41
|
+
export function buildSafeEnv(overrides) {
|
|
42
|
+
const env = {};
|
|
43
|
+
for (const key of ENV_ALLOWLIST) {
|
|
44
|
+
if (process.env[key] !== undefined) {
|
|
45
|
+
env[key] = process.env[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (overrides) {
|
|
49
|
+
Object.assign(env, overrides);
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convenience helper that returns both `cwd` and `env` in one object,
|
|
55
|
+
* making it harder to forget either when spawning a child process.
|
|
56
|
+
*/
|
|
57
|
+
export function buildSafeSpawnOpts(cwd, envOverrides) {
|
|
58
|
+
return { cwd, env: buildSafeEnv(envOverrides) };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=env-allowlist.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synergenius/flow-weaver/agent
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic agent loop with MCP bridge for tool execution.
|
|
5
|
+
* Two built-in providers: Anthropic API (raw fetch) and Claude CLI.
|
|
6
|
+
*/
|
|
7
|
+
export type { StreamEvent, AgentMessage, AgentProvider, ToolDefinition, ToolExecutor, ToolEvent, McpBridge, AgentLoopOptions, AgentLoopResult, StreamOptions, SpawnFn, ClaudeCliProviderOptions, CliSessionOptions, Logger, } from './types.js';
|
|
8
|
+
export { runAgentLoop } from './agent-loop.js';
|
|
9
|
+
export { AnthropicProvider, createAnthropicProvider } from './providers/anthropic.js';
|
|
10
|
+
export type { AnthropicProviderOptions } from './providers/anthropic.js';
|
|
11
|
+
export { ClaudeCliProvider, createClaudeCliProvider } from './providers/claude-cli.js';
|
|
12
|
+
export { createMcpBridge } from './mcp-bridge.js';
|
|
13
|
+
export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions, } from './cli-session.js';
|
|
14
|
+
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
15
|
+
export { StreamJsonParser } from './streaming.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synergenius/flow-weaver/agent
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic agent loop with MCP bridge for tool execution.
|
|
5
|
+
* Two built-in providers: Anthropic API (raw fetch) and Claude CLI.
|
|
6
|
+
*/
|
|
7
|
+
// Agent loop
|
|
8
|
+
export { runAgentLoop } from './agent-loop.js';
|
|
9
|
+
// Providers
|
|
10
|
+
export { AnthropicProvider, createAnthropicProvider } from './providers/anthropic.js';
|
|
11
|
+
export { ClaudeCliProvider, createClaudeCliProvider } from './providers/claude-cli.js';
|
|
12
|
+
// MCP bridge
|
|
13
|
+
export { createMcpBridge } from './mcp-bridge.js';
|
|
14
|
+
// CLI session (warm persistent sessions)
|
|
15
|
+
export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions, } from './cli-session.js';
|
|
16
|
+
// Env utilities
|
|
17
|
+
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
18
|
+
// Stream parser (for custom providers)
|
|
19
|
+
export { StreamJsonParser } from './streaming.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
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 type { ToolDefinition, ToolExecutor, ToolEvent, McpBridge, Logger } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Create an MCP bridge that the Claude CLI can connect to for tool execution.
|
|
15
|
+
*
|
|
16
|
+
* @param tools Tool definitions to advertise to the CLI
|
|
17
|
+
* @param executor Function that executes a tool call
|
|
18
|
+
* @param onToolEvent Optional callback for relaying tool events
|
|
19
|
+
* @param logger Optional logger
|
|
20
|
+
*/
|
|
21
|
+
export declare function createMcpBridge(tools: ToolDefinition[], executor: ToolExecutor, onToolEvent?: (event: ToolEvent) => void, logger?: Logger): Promise<McpBridge>;
|
|
22
|
+
//# sourceMappingURL=mcp-bridge.d.ts.map
|