plumb-bridge 0.1.1 → 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.
@@ -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 { execFile } from 'node:child_process';
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(_task: AgentTask, _config: PlumbConfig): string[] { return []; }
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
- try {
37
- const [cmd] = this.cliCommand.trim().split(/\s+/);
38
- if (!cmd) return null;
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
  }
@@ -1,31 +1,8 @@
1
1
  // PLUMB — OpenCode Adapter
2
- // Wraps `opencode run --format json`. JSON event stream.
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(_task: AgentTask, _config: PlumbConfig): string[] {
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: OpenCodeEvent;
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
- // End of agent step primary completion signal for `opencode run --format json`
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
- async detect(): Promise<DetectionResult | null> {
97
- try {
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
  }
@@ -1,30 +1,15 @@
1
1
  // PLUMB — Pi Adapter
2
- // Wraps `pi --mode rpc --print`. JSONL protocol. Highest value adapter.
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 = 'persistent' as const;
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', 'rpc', '--print', '--no-session'];
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: PiRpcEvent;
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
- // Filter extension UI requests — these are status updates, not task output
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
- // Pi message_update streaming text from assistant
72
- // Format: { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'text' } }
73
- if (event.type === 'message_update') {
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
- return [{ type: 'status', state: 'completed' }];
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
- async detect(): Promise<DetectionResult | null> {
117
- try {
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
  }
@@ -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
+ }