plumb-bridge 0.1.2 → 0.1.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/LICENSE +21 -0
- package/README.md +193 -0
- package/package.json +17 -3
- package/src/adapters/claude.ts +21 -62
- package/src/adapters/cursor.ts +213 -0
- package/src/adapters/detect.ts +27 -0
- package/src/adapters/echo.ts +5 -23
- package/src/adapters/generic.ts +5 -14
- package/src/adapters/opencode.ts +11 -59
- package/src/adapters/pi.ts +40 -66
- package/src/adapters/registry.ts +24 -1
- package/src/adapters/stream-json.ts +89 -0
- package/src/adapters/venom.ts +78 -0
- package/src/adapters/wolfy.ts +94 -0
- package/src/cli.ts +215 -10
- package/src/config.test.ts +170 -0
- package/src/config.ts +178 -0
- package/src/core/executor.ts +113 -77
- package/src/core/ledger.ts +15 -10
- package/src/core/log.ts +12 -0
- package/src/core/process.ts +193 -10
- package/src/core/server.ts +38 -7
- package/src/core/session-store.ts +158 -0
- package/src/core/task-store.ts +137 -0
- package/src/types.ts +30 -1
- package/test/adapter-parse.test.ts +328 -0
- package/test/persistent-process.test.ts +56 -0
- package/test/rpc.test.ts +57 -0
- package/test/session-store.test.ts +129 -0
- package/test/task-store.test.ts +95 -0
- package/tsconfig.json +1 -1
package/src/adapters/generic.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
// PLUMB — Generic Adapter
|
|
2
2
|
// Text passthrough. Wraps any CLI that reads stdin and writes to stdout.
|
|
3
|
-
// Every non-empty line becomes a progress event. Exit 0 = complete.
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from 'node:util';
|
|
4
|
+
import { detectBinary } from './detect.ts';
|
|
7
5
|
import type { AgentAdapter, AgentTask, AdapterEvent, DetectionResult, PlumbConfig } from '../types.ts';
|
|
8
6
|
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
7
|
export class GenericAdapter implements AgentAdapter {
|
|
12
8
|
readonly id = 'generic';
|
|
13
9
|
readonly binary = '';
|
|
@@ -22,7 +18,7 @@ export class GenericAdapter implements AgentAdapter {
|
|
|
22
18
|
this.cliCommand = cliCommand;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
buildArgs(
|
|
21
|
+
buildArgs(): string[] { return []; }
|
|
26
22
|
|
|
27
23
|
formatInput(task: AgentTask): string { return task.message + '\n'; }
|
|
28
24
|
|
|
@@ -33,13 +29,8 @@ export class GenericAdapter implements AgentAdapter {
|
|
|
33
29
|
|
|
34
30
|
async detect(): Promise<DetectionResult | null> {
|
|
35
31
|
if (!this.cliCommand) return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { stdout } = await execFileAsync('which', [cmd], { timeout: 5000 });
|
|
40
|
-
return { binary: cmd, version: 'unknown', path: stdout.trim(), tier: 3, protocol: 'text' };
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
32
|
+
const [cmd] = this.cliCommand.trim().split(/\s+/);
|
|
33
|
+
if (!cmd) return null;
|
|
34
|
+
return detectBinary(cmd, 3, 'text');
|
|
44
35
|
}
|
|
45
36
|
}
|
package/src/adapters/opencode.ts
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
// PLUMB — OpenCode Adapter
|
|
2
|
-
// Wraps `opencode run --format json`.
|
|
3
|
-
// Filters log/status events. Captures text content and results.
|
|
2
|
+
// Wraps `opencode run --format json`.
|
|
4
3
|
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
4
|
import type { AgentAdapter, AgentTask, AdapterEvent, DetectionResult, PlumbConfig } from '../types.ts';
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
interface OpenCodePart {
|
|
12
|
-
type?: string;
|
|
13
|
-
text?: string;
|
|
14
|
-
reason?: string;
|
|
15
|
-
[key: string]: unknown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface OpenCodeEvent {
|
|
19
|
-
type: string;
|
|
20
|
-
content?: string;
|
|
21
|
-
text?: string;
|
|
22
|
-
part?: OpenCodePart;
|
|
23
|
-
delta?: string;
|
|
24
|
-
error?: string;
|
|
25
|
-
message?: string;
|
|
26
|
-
result?: string;
|
|
27
|
-
[key: string]: unknown;
|
|
28
|
-
}
|
|
5
|
+
import { detectBinary } from './detect.ts';
|
|
29
6
|
|
|
30
7
|
export class OpenCodeAdapter implements AgentAdapter {
|
|
31
8
|
readonly id = 'opencode';
|
|
@@ -40,78 +17,53 @@ export class OpenCodeAdapter implements AgentAdapter {
|
|
|
40
17
|
{ id: 'read', name: 'Read files', tags: ['read', 'file'] },
|
|
41
18
|
];
|
|
42
19
|
|
|
43
|
-
buildArgs(
|
|
20
|
+
buildArgs(): string[] {
|
|
44
21
|
return ['run', '--format', 'json'];
|
|
45
22
|
}
|
|
46
23
|
|
|
47
24
|
formatInput(task: AgentTask): string {
|
|
48
|
-
// OpenCode reads one JSON line per stdin message with `prompt` (see fangai OpenCodeAdapter).
|
|
49
25
|
return JSON.stringify({ prompt: task.message }) + '\n';
|
|
50
26
|
}
|
|
51
27
|
|
|
52
28
|
parseLine(line: string): AdapterEvent[] {
|
|
53
29
|
if (!line.trim()) return [];
|
|
54
30
|
|
|
55
|
-
let event:
|
|
31
|
+
let event: Record<string, unknown>;
|
|
56
32
|
try {
|
|
57
33
|
event = JSON.parse(line);
|
|
58
34
|
} catch {
|
|
59
|
-
// Non-JSON line — treat as raw text
|
|
60
35
|
return [{ type: 'text-delta', text: line + '\n' }];
|
|
61
36
|
}
|
|
62
37
|
|
|
63
|
-
// Text content — OpenCode JSON uses part.text for assistant output
|
|
64
38
|
if (event.type === 'text' || event.type === 'content' || event.type === 'text-delta') {
|
|
65
|
-
const fromPart = typeof event.part?.text === 'string' ? event.part.text : '';
|
|
39
|
+
const fromPart = typeof (event.part as Record<string, unknown>)?.text === 'string' ? (event.part as Record<string, unknown>).text as string : '';
|
|
66
40
|
const text = fromPart || (event.text ?? event.content ?? event.delta ?? '');
|
|
67
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
41
|
+
if (text) return [{ type: 'text-delta', text: text as string }];
|
|
68
42
|
return [];
|
|
69
43
|
}
|
|
70
44
|
|
|
71
|
-
// Message part updated — streaming content
|
|
72
45
|
if (event.type === 'message.part.updated') {
|
|
73
46
|
const text = event.content ?? event.text ?? '';
|
|
74
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
47
|
+
if (text) return [{ type: 'text-delta', text: text as string }];
|
|
75
48
|
return [];
|
|
76
49
|
}
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
if (event.type === 'step_finish' && event.part?.reason === 'stop') {
|
|
51
|
+
if (event.type === 'step_finish' && (event.part as Record<string, unknown>)?.reason === 'stop') {
|
|
80
52
|
return [{ type: 'status', state: 'completed' }];
|
|
81
53
|
}
|
|
82
54
|
|
|
83
|
-
// Session / run completed (alternate event names)
|
|
84
55
|
if (event.type === 'session.completed' || event.type === 'done' || event.type === 'complete') {
|
|
85
56
|
return [{ type: 'status', state: 'completed' }];
|
|
86
57
|
}
|
|
87
58
|
|
|
88
|
-
// Error events
|
|
89
59
|
if (event.type === 'error') {
|
|
90
|
-
return [{ type: 'error', message: event.error ?? event.message ?? 'Unknown error' }];
|
|
60
|
+
return [{ type: 'error', message: (event.error ?? event.message ?? 'Unknown error') as string }];
|
|
91
61
|
}
|
|
92
62
|
|
|
93
63
|
return [];
|
|
94
64
|
}
|
|
95
65
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const { stdout } = await execFileAsync('which', ['opencode'], { timeout: 5000 });
|
|
99
|
-
let version = 'unknown';
|
|
100
|
-
try {
|
|
101
|
-
const { stdout: vOut } = await execFileAsync('opencode', ['--version'], { timeout: 5000 });
|
|
102
|
-
version = vOut.trim().split('\n')[0] ?? 'unknown';
|
|
103
|
-
} catch {
|
|
104
|
-
// Version check failed
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
binary: 'opencode',
|
|
108
|
-
version,
|
|
109
|
-
path: stdout.trim(),
|
|
110
|
-
tier: 2,
|
|
111
|
-
protocol: 'json-stream',
|
|
112
|
-
};
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
66
|
+
detect(): Promise<DetectionResult | null> {
|
|
67
|
+
return detectBinary('opencode', 2, 'json-stream');
|
|
116
68
|
}
|
|
117
69
|
}
|
package/src/adapters/pi.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
// PLUMB — Pi Adapter
|
|
2
|
-
// Wraps `pi --mode
|
|
3
|
-
// Extension UI requests are filtered. Text deltas and responses are captured.
|
|
2
|
+
// Wraps `pi --mode json --print --no-session`.
|
|
4
3
|
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
4
|
import type { AgentAdapter, AgentTask, AdapterEvent, DetectionResult, PlumbConfig } from '../types.ts';
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
|
|
11
|
-
interface PiRpcEvent {
|
|
12
|
-
type: string;
|
|
13
|
-
text?: string;
|
|
14
|
-
content?: string;
|
|
15
|
-
success?: boolean;
|
|
16
|
-
error?: string;
|
|
17
|
-
delta?: string;
|
|
18
|
-
assistantMessageEvent?: { type?: string; delta?: string };
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
}
|
|
5
|
+
import { detectBinary } from './detect.ts';
|
|
21
6
|
|
|
22
7
|
export class PiAdapter implements AgentAdapter {
|
|
23
8
|
readonly id = 'pi';
|
|
24
9
|
readonly binary = 'pi';
|
|
25
10
|
readonly tier = 1 as const;
|
|
26
11
|
readonly displayName = 'Pi';
|
|
27
|
-
readonly mode = '
|
|
12
|
+
readonly mode = 'oneshot' as const;
|
|
28
13
|
|
|
29
14
|
skills = [
|
|
30
15
|
{ id: 'code', name: 'Code generation and editing', tags: ['code', 'edit', 'write'] },
|
|
@@ -33,7 +18,7 @@ export class PiAdapter implements AgentAdapter {
|
|
|
33
18
|
];
|
|
34
19
|
|
|
35
20
|
buildArgs(_task: AgentTask, _config: PlumbConfig): string[] {
|
|
36
|
-
return ['--mode', '
|
|
21
|
+
return ['--mode', 'json', '--print', '--no-session'];
|
|
37
22
|
}
|
|
38
23
|
|
|
39
24
|
formatInput(task: AgentTask): string {
|
|
@@ -48,91 +33,80 @@ export class PiAdapter implements AgentAdapter {
|
|
|
48
33
|
parseLine(line: string): AdapterEvent[] {
|
|
49
34
|
if (!line.trim()) return [];
|
|
50
35
|
|
|
51
|
-
let event:
|
|
36
|
+
let event: Record<string, unknown>;
|
|
52
37
|
try {
|
|
53
38
|
event = JSON.parse(line);
|
|
54
39
|
} catch {
|
|
55
|
-
// Non-JSON line — treat as raw text
|
|
56
40
|
return [{ type: 'text-delta', text: line + '\n' }];
|
|
57
41
|
}
|
|
58
42
|
|
|
59
|
-
|
|
60
|
-
if (event.type === 'extension_ui_request') {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
43
|
+
if (event.type === 'extension_ui_request') return [];
|
|
63
44
|
|
|
64
|
-
// Text delta events — streaming output
|
|
65
45
|
if (event.type === 'text-delta' || event.type === 'delta') {
|
|
66
46
|
const text = event.text ?? event.delta ?? event.content ?? '';
|
|
67
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
47
|
+
if (text) return [{ type: 'text-delta', text: text as string }];
|
|
68
48
|
return [];
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const ame = event.assistantMessageEvent as { type?: string; delta?: string } | undefined;
|
|
75
|
-
if (ame?.type === 'text_delta' && typeof ame.delta === 'string') {
|
|
51
|
+
if (event.type === 'message_update' && event.assistantMessageEvent) {
|
|
52
|
+
const ame = event.assistantMessageEvent as Record<string, unknown>;
|
|
53
|
+
if (ame.type === 'text_delta' && typeof ame.delta === 'string' && ame.delta) {
|
|
76
54
|
return [{ type: 'text-delta', text: ame.delta }];
|
|
77
55
|
}
|
|
56
|
+
if (ame.type && String(ame.type).startsWith('thinking_')) return [];
|
|
57
|
+
if (ame.type === 'text_start' || ame.type === 'text_end') return [];
|
|
78
58
|
const text = event.text ?? event.delta ?? event.content ?? '';
|
|
79
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
59
|
+
if (text) return [{ type: 'text-delta', text: text as string }];
|
|
80
60
|
return [];
|
|
81
61
|
}
|
|
82
62
|
|
|
83
|
-
// Content block — full text
|
|
84
63
|
if (event.type === 'content' || event.type === 'text') {
|
|
85
64
|
const text = event.text ?? event.content ?? '';
|
|
86
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
65
|
+
if (text) return [{ type: 'text-delta', text: text as string }];
|
|
87
66
|
return [];
|
|
88
67
|
}
|
|
89
68
|
|
|
90
|
-
// Response event — task result
|
|
91
69
|
if (event.type === 'response') {
|
|
92
70
|
if (event.success === false && event.error) {
|
|
93
|
-
return [{ type: 'error', message: event.error }];
|
|
71
|
+
return [{ type: 'error', message: event.error as string }];
|
|
94
72
|
}
|
|
95
|
-
// Successful response with content
|
|
96
73
|
const text = event.text ?? event.content ?? '';
|
|
97
|
-
if (text) return [{ type: 'text-delta', text }];
|
|
98
|
-
return [];
|
|
74
|
+
if (text) return [{ type: 'text-delta', text: text as string }, { type: 'status', state: 'completed' }];
|
|
75
|
+
return [{ type: 'status', state: 'completed' }];
|
|
99
76
|
}
|
|
100
77
|
|
|
101
|
-
// Error event
|
|
102
78
|
if (event.type === 'error') {
|
|
103
|
-
return [{ type: 'error', message: event.error ?? 'Unknown error' }];
|
|
79
|
+
return [{ type: 'error', message: (event.error ?? 'Unknown error') as string }];
|
|
104
80
|
}
|
|
105
81
|
|
|
106
|
-
// Done/complete signals (Pi uses agent_end and turn_end)
|
|
107
82
|
if (event.type === 'done' || event.type === 'complete' || event.type === 'finished' ||
|
|
108
83
|
event.type === 'agent_end' || event.type === 'turn_end') {
|
|
109
|
-
|
|
84
|
+
const events: AdapterEvent[] = [];
|
|
85
|
+
if (event.type === 'agent_end' || event.type === 'turn_end') {
|
|
86
|
+
const messages = event.messages as Array<Record<string, unknown>> | undefined;
|
|
87
|
+
if (messages) {
|
|
88
|
+
for (const msg of messages) {
|
|
89
|
+
if (msg.role === 'assistant') {
|
|
90
|
+
const content = msg.content as Array<Record<string, unknown>> | undefined;
|
|
91
|
+
if (content) {
|
|
92
|
+
for (const block of content) {
|
|
93
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
94
|
+
events.push({ type: 'text-delta', text: block.text });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
events.push({ type: 'status', state: 'completed' });
|
|
103
|
+
return events;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
// Unknown event type — log but don't emit
|
|
113
106
|
return [];
|
|
114
107
|
}
|
|
115
108
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const { stdout } = await execFileAsync('which', ['pi'], { timeout: 5000 });
|
|
119
|
-
// Get version
|
|
120
|
-
let version = 'unknown';
|
|
121
|
-
try {
|
|
122
|
-
const { stdout: vOut } = await execFileAsync('pi', ['--version'], { timeout: 5000 });
|
|
123
|
-
version = vOut.trim().split('\n')[0] ?? 'unknown';
|
|
124
|
-
} catch {
|
|
125
|
-
// Version check failed, continue with unknown
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
binary: 'pi',
|
|
129
|
-
version,
|
|
130
|
-
path: stdout.trim(),
|
|
131
|
-
tier: 1,
|
|
132
|
-
protocol: 'jsonl-rpc',
|
|
133
|
-
};
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
109
|
+
detect(): Promise<DetectionResult | null> {
|
|
110
|
+
return detectBinary('pi', 1, 'jsonl-rpc');
|
|
137
111
|
}
|
|
138
112
|
}
|
package/src/adapters/registry.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// PLUMB — Adapter Registry
|
|
2
2
|
// detectAdapter: returns the first adapter whose binary matches the CLI command.
|
|
3
3
|
// EchoAdapter for 'cat'. GenericAdapter for everything else.
|
|
4
|
-
// Add real adapters here as they are built (Pi, Claude, Cursor...).
|
|
5
4
|
|
|
6
5
|
import { EchoAdapter } from './echo.ts';
|
|
7
6
|
import { PiAdapter } from './pi.ts';
|
|
8
7
|
import { ClaudeAdapter } from './claude.ts';
|
|
8
|
+
import { CursorAdapter } from './cursor.ts';
|
|
9
9
|
import { OpenCodeAdapter } from './opencode.ts';
|
|
10
|
+
import { WolfyAdapter } from './wolfy.ts';
|
|
11
|
+
import { VenomAdapter } from './venom.ts';
|
|
10
12
|
import { GenericAdapter } from './generic.ts';
|
|
11
13
|
import type { AgentAdapter } from '../types.ts';
|
|
12
14
|
|
|
@@ -14,14 +16,35 @@ import type { AgentAdapter } from '../types.ts';
|
|
|
14
16
|
const KNOWN_ADAPTERS: AgentAdapter[] = [
|
|
15
17
|
new EchoAdapter(),
|
|
16
18
|
new PiAdapter(),
|
|
19
|
+
new WolfyAdapter(),
|
|
17
20
|
new ClaudeAdapter(),
|
|
21
|
+
new CursorAdapter(),
|
|
18
22
|
new OpenCodeAdapter(),
|
|
23
|
+
new VenomAdapter(),
|
|
19
24
|
];
|
|
20
25
|
|
|
21
26
|
function escapeRegex(s: string): string {
|
|
22
27
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
/** Detect all registered adapters. Returns {name, found, version, path, error} for each. */
|
|
31
|
+
export async function detectAll(): Promise<Array<{ name: string; found: boolean; version?: string; path?: string; error?: string }>> {
|
|
32
|
+
const results: Array<{ name: string; found: boolean; version?: string; path?: string; error?: string }> = [];
|
|
33
|
+
for (const adapter of KNOWN_ADAPTERS) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await adapter.detect();
|
|
36
|
+
if (result) {
|
|
37
|
+
results.push({ name: adapter.displayName, found: true, version: result.version, path: result.path });
|
|
38
|
+
} else {
|
|
39
|
+
results.push({ name: adapter.displayName, found: false, error: 'Not found' });
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
results.push({ name: adapter.displayName, found: false, error: err instanceof Error ? err.message : String(err) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export function detectAdapter(cli: string): AgentAdapter {
|
|
26
49
|
for (const adapter of KNOWN_ADAPTERS) {
|
|
27
50
|
if (!adapter.binary) continue;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// PLUMB — Stream JSON Parser Utility
|
|
2
|
+
// Shared parseLine logic for stream-json adapters (Cursor, Claude, Venom).
|
|
3
|
+
// Reduces duplication: every oneshot JSON-line agent has the same skeleton.
|
|
4
|
+
|
|
5
|
+
import type { AdapterEvent } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
/** Minimal base for any stream-json line. `message` is intentionally `unknown`
|
|
8
|
+
* because different agents use it differently (string for some, object for others). */
|
|
9
|
+
interface StreamJsonBaseEvent {
|
|
10
|
+
type: string;
|
|
11
|
+
subtype?: string;
|
|
12
|
+
timestamp_ms?: number;
|
|
13
|
+
is_error?: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Event with a structured .message.content array (Cursor, Venom). */
|
|
19
|
+
export interface ContentBlockEvent {
|
|
20
|
+
type: string;
|
|
21
|
+
subtype?: string;
|
|
22
|
+
timestamp_ms?: number;
|
|
23
|
+
is_error?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
message?: {
|
|
26
|
+
role?: string;
|
|
27
|
+
content?: Array<{ type: string; text?: string }>;
|
|
28
|
+
};
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Parsed result from tryParseStreamLine. */
|
|
33
|
+
export interface ParsedLine {
|
|
34
|
+
json: StreamJsonBaseEvent | null;
|
|
35
|
+
raw: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Attempt to parse a stdout line as JSON. Fallback to raw text-delta.
|
|
40
|
+
* Returns { json: null, raw: '' } for empty/whitespace lines (callers skip these).
|
|
41
|
+
*/
|
|
42
|
+
export function tryParseLine(line: string): ParsedLine {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) return { json: null, raw: '' };
|
|
45
|
+
try {
|
|
46
|
+
return { json: JSON.parse(trimmed) as StreamJsonBaseEvent, raw: line };
|
|
47
|
+
} catch {
|
|
48
|
+
return { json: null, raw: line };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract text from a message.content array (Cursor/Venom format).
|
|
54
|
+
* Filters for { type: "text", text: string } blocks, joins with newline.
|
|
55
|
+
*/
|
|
56
|
+
export function extractContentText(event: ContentBlockEvent): string | null {
|
|
57
|
+
const content = event.message?.content;
|
|
58
|
+
if (!content || !Array.isArray(content)) return null;
|
|
59
|
+
const texts = content
|
|
60
|
+
.filter((c): c is { type: string; text: string } =>
|
|
61
|
+
c.type === 'text' && typeof c.text === 'string'
|
|
62
|
+
)
|
|
63
|
+
.map(c => c.text);
|
|
64
|
+
return texts.length > 0 ? texts.join('\n') : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a stream-json event is a consolidated (non-streaming) assistant event
|
|
69
|
+
* that should be skipped when streamPartial is enabled.
|
|
70
|
+
* Consolidated events lack timestamp_ms; streaming deltas have it.
|
|
71
|
+
*/
|
|
72
|
+
export function isConsolidatedAssistant(event: ContentBlockEvent, streamPartial: boolean): boolean {
|
|
73
|
+
return streamPartial && !event.timestamp_ms;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Build a text-delta event. */
|
|
77
|
+
export function textDelta(text: string): AdapterEvent {
|
|
78
|
+
return { type: 'text-delta', text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build a status event. */
|
|
82
|
+
export function statusEvent(state: 'working' | 'completed' | 'failed'): AdapterEvent {
|
|
83
|
+
return { type: 'status', state };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build an error event. */
|
|
87
|
+
export function errorEvent(message: string, code?: string): AdapterEvent {
|
|
88
|
+
return { type: 'error', message, ...(code ? { code } : {}) };
|
|
89
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// PLUMB — VENOM Adapter
|
|
2
|
+
// Wraps `venom --output-format stream-json --permission-mode danger-full-access`.
|
|
3
|
+
|
|
4
|
+
import type { AgentAdapter, AgentTask, AdapterEvent, DetectionResult, PlumbConfig } from '../types.ts';
|
|
5
|
+
import { tryParseLine, extractContentText, isConsolidatedAssistant, textDelta, statusEvent, errorEvent } from './stream-json.ts';
|
|
6
|
+
import type { ContentBlockEvent } from './stream-json.ts';
|
|
7
|
+
import { detectBinary } from './detect.ts';
|
|
8
|
+
|
|
9
|
+
export class VenomAdapter implements AgentAdapter {
|
|
10
|
+
readonly id = 'venom';
|
|
11
|
+
readonly binary = 'venom';
|
|
12
|
+
readonly tier = 3 as const;
|
|
13
|
+
readonly displayName = 'VENOM';
|
|
14
|
+
readonly mode = 'oneshot' as const;
|
|
15
|
+
|
|
16
|
+
/** Stream-json dedup: skip consolidated assistant events when streaming. */
|
|
17
|
+
streamPartial = true;
|
|
18
|
+
|
|
19
|
+
skills = [
|
|
20
|
+
{ id: 'code', name: 'Code generation and editing', tags: ['code', 'edit', 'write'] },
|
|
21
|
+
{ id: 'bash', name: 'Execute shell commands', tags: ['bash', 'shell', 'terminal'] },
|
|
22
|
+
{ id: 'read', name: 'Read files', tags: ['read', 'file'] },
|
|
23
|
+
{ id: 'rust', name: 'Rust development', tags: ['rust', 'cargo'] },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
buildArgs(): string[] {
|
|
27
|
+
return ['--output-format', 'stream-json', '--permission-mode', 'danger-full-access'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
formatInput(task: AgentTask): string {
|
|
31
|
+
return task.message + '\n';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
parseLine(line: string): AdapterEvent[] {
|
|
35
|
+
const { json, raw } = tryParseLine(line);
|
|
36
|
+
if (!json) {
|
|
37
|
+
if (!raw) return [];
|
|
38
|
+
return [textDelta(raw + '\n')];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// System/user events — metadata only
|
|
42
|
+
if (json.type === 'system' || json.type === 'user') return [];
|
|
43
|
+
|
|
44
|
+
if (json.type === 'assistant') {
|
|
45
|
+
const extracted = extractContentText(json as ContentBlockEvent);
|
|
46
|
+
if (isConsolidatedAssistant(json as ContentBlockEvent, this.streamPartial)) return [];
|
|
47
|
+
return extracted ? [textDelta(extracted)] : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Tool call
|
|
51
|
+
if (json.type === 'tool_call') {
|
|
52
|
+
const tc = (json as Record<string, unknown>).tool_call as { shellToolCall?: { args?: Record<string, unknown>; result?: string } } | undefined;
|
|
53
|
+
if (tc?.shellToolCall) {
|
|
54
|
+
return [{ type: 'tool-call', tool: 'shell', input: tc.shellToolCall.args ?? {} }];
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Result — completion or error
|
|
60
|
+
if (json.type === 'result') {
|
|
61
|
+
if (json.subtype === 'error' || json.is_error) {
|
|
62
|
+
return [errorEvent(json.error ?? 'VENOM execution failed')];
|
|
63
|
+
}
|
|
64
|
+
return [statusEvent('completed')];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Error event
|
|
68
|
+
if (json.type === 'error') {
|
|
69
|
+
return [errorEvent(String(json.error ?? json.message ?? 'Unknown error'))];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
detect(): Promise<DetectionResult | null> {
|
|
76
|
+
return detectBinary('venom', 3, 'stream-json');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// PLUMB — Wolfy Adapter
|
|
2
|
+
// Wraps `wolfy --mode json --print --no-session`.
|
|
3
|
+
|
|
4
|
+
import type { AgentAdapter, AgentTask, AdapterEvent, DetectionResult, PlumbConfig } from '../types.ts';
|
|
5
|
+
import { detectBinary } from './detect.ts';
|
|
6
|
+
|
|
7
|
+
export class WolfyAdapter implements AgentAdapter {
|
|
8
|
+
readonly id = 'wolfy';
|
|
9
|
+
readonly binary = 'wolfy';
|
|
10
|
+
readonly tier = 1 as const;
|
|
11
|
+
readonly displayName = 'Wolfy';
|
|
12
|
+
readonly mode = 'oneshot' as const;
|
|
13
|
+
|
|
14
|
+
skills = [
|
|
15
|
+
{ id: 'code', name: 'Code generation and editing', tags: ['code', 'edit', 'write', 'build'] },
|
|
16
|
+
{ id: 'bash', name: 'Shell command execution', tags: ['bash', 'shell', 'terminal', 'deploy'] },
|
|
17
|
+
{ id: 'read', name: 'File reading and analysis', tags: ['read', 'file', 'audit'] },
|
|
18
|
+
{ id: 'memory', name: 'Persistent memory with semantic search', tags: ['memory', 'search', 'context'] },
|
|
19
|
+
{ id: 'knowledge', name: 'Knowledge graph with temporal facts', tags: ['knowledge', 'facts', 'timeline'] },
|
|
20
|
+
{ id: 'subagents', name: 'Parallel and chained subagent delegation', tags: ['subagents', 'parallel', 'chain'] },
|
|
21
|
+
{ id: 'architecture', name: 'System architecture and API design', tags: ['architecture', 'api', 'schema'] },
|
|
22
|
+
{ id: 'deepseek', name: 'DeepSeek V4 Pro — deep reasoning, 200K context', tags: ['deepseek', 'reasoning'] },
|
|
23
|
+
{ id: 'kimi', name: 'Kimi K2.6 — long context analysis, 262K context', tags: ['kimi', 'analysis'] },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
buildArgs(): string[] {
|
|
27
|
+
return ['--mode', 'json', '--print', '--no-session'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
formatInput(task: AgentTask): string {
|
|
31
|
+
const meta = task.context?.metadata ?? {};
|
|
32
|
+
return `[${meta.source ?? 'plumb-mesh'}][${meta.project ?? 'general'}] ${task.message}\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parseLine(line: string): AdapterEvent[] {
|
|
36
|
+
if (!line.trim()) return [];
|
|
37
|
+
|
|
38
|
+
let event: Record<string, unknown>;
|
|
39
|
+
try {
|
|
40
|
+
event = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
return [{ type: 'text-delta', text: line + '\n' }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (event.type === 'extension_ui_request' || event.type === 'session') return [];
|
|
46
|
+
|
|
47
|
+
if (event.type === 'message_update' && event.assistantMessageEvent) {
|
|
48
|
+
const ame = event.assistantMessageEvent as Record<string, unknown>;
|
|
49
|
+
if (ame.type === 'text_delta' && typeof ame.delta === 'string' && ame.delta) {
|
|
50
|
+
return [{ type: 'text-delta', text: ame.delta }];
|
|
51
|
+
}
|
|
52
|
+
if (ame.type === 'thinking_delta' || ame.type === 'thinking_start' || ame.type === 'thinking_end') return [];
|
|
53
|
+
if (ame.type === 'text_start' || ame.type === 'text_end') return [];
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.type === 'message_end') return [];
|
|
58
|
+
|
|
59
|
+
if (event.type === 'turn_end' || event.type === 'agent_end') {
|
|
60
|
+
const events: AdapterEvent[] = [];
|
|
61
|
+
const messages = event.messages as Array<Record<string, unknown>> | undefined;
|
|
62
|
+
if (messages) {
|
|
63
|
+
for (const msg of messages) {
|
|
64
|
+
if (msg.role === 'assistant') {
|
|
65
|
+
const content = msg.content as Array<Record<string, unknown>> | undefined;
|
|
66
|
+
if (content) {
|
|
67
|
+
for (const block of content) {
|
|
68
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
69
|
+
events.push({ type: 'text-delta', text: block.text });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
events.push({ type: 'status', state: 'completed' });
|
|
77
|
+
return events;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (event.type === 'error') {
|
|
81
|
+
return [{ type: 'error', message: (event.error ?? 'Unknown error') as string }];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async detect(): Promise<DetectionResult | null> {
|
|
88
|
+
const result = await detectBinary('wolfy', 1, 'jsonl');
|
|
89
|
+
if (result) {
|
|
90
|
+
result.version = `Wolfy (Pi ${result.version})`;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|