@amalgm/chat 0.1.2 → 0.2.0
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/AGENTS.md +98 -0
- package/PURPOSE.md +143 -112
- package/README.md +82 -54
- package/dist/acp/index.d.ts +7 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +7 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/projection.d.ts +30 -0
- package/dist/acp/projection.d.ts.map +1 -0
- package/dist/acp/projection.js +147 -0
- package/dist/acp/projection.js.map +1 -0
- package/dist/api/conversations.d.ts +66 -0
- package/dist/api/conversations.d.ts.map +1 -0
- package/dist/api/conversations.js +202 -0
- package/dist/api/conversations.js.map +1 -0
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -2
- package/dist/api/index.js.map +1 -1
- package/dist/conversations/index.d.ts +3 -0
- package/dist/conversations/index.d.ts.map +1 -0
- package/dist/conversations/index.js +3 -0
- package/dist/conversations/index.js.map +1 -0
- package/dist/conversations/transcript.d.ts +13 -0
- package/dist/conversations/transcript.d.ts.map +1 -0
- package/dist/conversations/transcript.js +171 -0
- package/dist/conversations/transcript.js.map +1 -0
- package/dist/conversations/types.d.ts +147 -0
- package/dist/conversations/types.d.ts.map +1 -0
- package/dist/conversations/types.js +9 -0
- package/dist/conversations/types.js.map +1 -0
- package/dist/events/constructors.d.ts +9 -15
- package/dist/events/constructors.d.ts.map +1 -1
- package/dist/events/constructors.js +6 -12
- package/dist/events/constructors.js.map +1 -1
- package/dist/events/index.d.ts +2 -3
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +2 -3
- package/dist/events/index.js.map +1 -1
- package/dist/events/schema.d.ts +2 -5
- package/dist/events/schema.d.ts.map +1 -1
- package/dist/events/schema.js +2 -5
- package/dist/events/schema.js.map +1 -1
- package/dist/execution/contract.d.ts +51 -0
- package/dist/execution/contract.d.ts.map +1 -0
- package/dist/execution/contract.js +127 -0
- package/dist/execution/contract.js.map +1 -0
- package/dist/execution/index.d.ts +6 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +4 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/revision.d.ts +5 -0
- package/dist/execution/revision.d.ts.map +1 -0
- package/dist/execution/revision.js +17 -0
- package/dist/execution/revision.js.map +1 -0
- package/dist/execution/store.d.ts +26 -0
- package/dist/execution/store.d.ts.map +1 -0
- package/dist/execution/store.js +38 -0
- package/dist/execution/store.js.map +1 -0
- package/dist/index.d.ts +8 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -25
- package/dist/index.js.map +1 -1
- package/dist/input/index.d.ts +3 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +2 -0
- package/dist/input/index.js.map +1 -0
- package/dist/input/prompt.d.ts +17 -0
- package/dist/input/prompt.d.ts.map +1 -0
- package/dist/input/prompt.js +104 -0
- package/dist/input/prompt.js.map +1 -0
- package/dist/normalizers/claude.d.ts +6 -9
- package/dist/normalizers/claude.d.ts.map +1 -1
- package/dist/normalizers/claude.js +5 -8
- package/dist/normalizers/claude.js.map +1 -1
- package/dist/normalizers/codex.d.ts +5 -8
- package/dist/normalizers/codex.d.ts.map +1 -1
- package/dist/normalizers/codex.js +4 -7
- package/dist/normalizers/codex.js.map +1 -1
- package/dist/normalizers/cursor.d.ts +1 -2
- package/dist/normalizers/cursor.d.ts.map +1 -1
- package/dist/normalizers/cursor.js +2 -3
- package/dist/normalizers/cursor.js.map +1 -1
- package/dist/normalizers/index.d.ts +3 -5
- package/dist/normalizers/index.d.ts.map +1 -1
- package/dist/normalizers/index.js +3 -5
- package/dist/normalizers/index.js.map +1 -1
- package/dist/normalizers/opencode.d.ts +2 -4
- package/dist/normalizers/opencode.d.ts.map +1 -1
- package/dist/normalizers/opencode.js +2 -4
- package/dist/normalizers/opencode.js.map +1 -1
- package/dist/normalizers/pi.d.ts +3 -4
- package/dist/normalizers/pi.d.ts.map +1 -1
- package/dist/normalizers/pi.js +3 -4
- package/dist/normalizers/pi.js.map +1 -1
- package/dist/sessions/chat.d.ts +176 -0
- package/dist/sessions/chat.d.ts.map +1 -0
- package/dist/sessions/chat.js +701 -0
- package/dist/sessions/chat.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/transcript/index.d.ts +1 -1
- package/dist/transcript/index.js +1 -1
- package/dist/transcript/message-reducer.d.ts +1 -1
- package/dist/transcript/message-reducer.js +1 -1
- package/dist/transcript/part-accumulator.d.ts +1 -1
- package/dist/transcript/part-accumulator.js +1 -1
- package/dist/transcript/tool-display.d.ts +1 -1
- package/dist/transcript/tool-display.js +1 -1
- package/dist/transcript/tool-shape.d.ts +1 -1
- package/dist/transcript/tool-shape.js +1 -1
- package/dist/usage/finalize.d.ts +7 -9
- package/dist/usage/finalize.d.ts.map +1 -1
- package/dist/usage/finalize.js +11 -13
- package/dist/usage/finalize.js.map +1 -1
- package/dist/usage/index.d.ts +2 -3
- package/dist/usage/index.d.ts.map +1 -1
- package/dist/usage/index.js +2 -3
- package/dist/usage/index.js.map +1 -1
- package/dist/usage/model-catalog.d.ts +2 -3
- package/dist/usage/model-catalog.d.ts.map +1 -1
- package/dist/usage/model-catalog.js +10 -3
- package/dist/usage/model-catalog.js.map +1 -1
- package/dist/usage/normalize.d.ts +5 -7
- package/dist/usage/normalize.d.ts.map +1 -1
- package/dist/usage/normalize.js +3 -5
- package/dist/usage/normalize.js.map +1 -1
- package/dist/usage/ports.d.ts +14 -24
- package/dist/usage/ports.d.ts.map +1 -1
- package/dist/usage/ports.js +9 -17
- package/dist/usage/ports.js.map +1 -1
- package/dist/usage/steps.d.ts +70 -0
- package/dist/usage/steps.d.ts.map +1 -0
- package/dist/usage/steps.js +152 -0
- package/dist/usage/steps.js.map +1 -0
- package/dist/usage/tokens.d.ts +4 -7
- package/dist/usage/tokens.d.ts.map +1 -1
- package/dist/usage/tokens.js +4 -7
- package/dist/usage/tokens.js.map +1 -1
- package/docs/contracts/acp-and-step-usage.md +51 -0
- package/docs/contracts/conversation-persistence.md +47 -0
- package/docs/contracts/input-and-execution.md +177 -0
- package/docs/contracts/platform-authorization.md +17 -0
- package/host/adapters/acp-capabilities.js +30 -0
- package/host/adapters/acp-client.js +156 -0
- package/host/adapters/acp.js +284 -0
- package/host/adapters/claude.js +235 -0
- package/host/adapters/codex.js +733 -0
- package/host/adapters/cursor.js +347 -0
- package/host/adapters/input-capabilities.js +89 -0
- package/host/adapters/opencode.js +486 -0
- package/host/adapters/pi.js +519 -0
- package/host/adapters/prompt.js +212 -0
- package/host/auth.js +257 -0
- package/host/credentials/store.js +209 -0
- package/host/http.d.ts +32 -0
- package/host/http.js +238 -0
- package/host/index.d.ts +84 -0
- package/host/index.js +59 -0
- package/host/lib/runtime-paths.js +44 -0
- package/host/model-resolution.js +313 -0
- package/host/native-contract.js +185 -0
- package/host/native-runtime.js +194 -0
- package/host/permissions.js +27 -0
- package/host/platform-egress.js +114 -0
- package/host/recorder.js +61 -0
- package/host/sqlite/index.d.ts +69 -0
- package/host/sqlite/index.js +704 -0
- package/host/tooling/mcp-bundle.js +214 -0
- package/host/tooling/mcp-relay.js +83 -0
- package/host/tooling/native-binaries.js +667 -0
- package/host/tooling/native-config.js +460 -0
- package/host/tooling/package-import.js +111 -0
- package/host/tooling/runtime-home.js +26 -0
- package/host/tooling/system-instructions.js +69 -0
- package/host/tooling/system-prompt.js +35 -0
- package/package.json +44 -24
- package/skills/chat/SKILL.md +97 -0
- package/skills/chat/agents/openai.yaml +4 -0
- package/skills/chat/references/contracts.md +131 -0
- package/dist/api/chat-payloads.d.ts +0 -10
- package/dist/api/chat-payloads.d.ts.map +0 -1
- package/dist/api/chat-payloads.js +0 -57
- package/dist/api/chat-payloads.js.map +0 -1
- package/dist/contract/chat-input.d.ts +0 -144
- package/dist/contract/chat-input.d.ts.map +0 -1
- package/dist/contract/chat-input.js +0 -261
- package/dist/contract/chat-input.js.map +0 -1
- package/dist/contract/create.d.ts +0 -191
- package/dist/contract/create.d.ts.map +0 -1
- package/dist/contract/create.js +0 -249
- package/dist/contract/create.js.map +0 -1
- package/dist/contract/freeze.d.ts +0 -85
- package/dist/contract/freeze.d.ts.map +0 -1
- package/dist/contract/freeze.js +0 -151
- package/dist/contract/freeze.js.map +0 -1
- package/dist/contract/harnesses.d.ts +0 -21
- package/dist/contract/harnesses.d.ts.map +0 -1
- package/dist/contract/harnesses.js +0 -39
- package/dist/contract/harnesses.js.map +0 -1
- package/dist/contract/index.d.ts +0 -17
- package/dist/contract/index.d.ts.map +0 -1
- package/dist/contract/index.js +0 -17
- package/dist/contract/index.js.map +0 -1
- package/dist/contract/input.d.ts +0 -45
- package/dist/contract/input.d.ts.map +0 -1
- package/dist/contract/input.js +0 -78
- package/dist/contract/input.js.map +0 -1
- package/dist/contract/models.d.ts +0 -57
- package/dist/contract/models.d.ts.map +0 -1
- package/dist/contract/models.js +0 -380
- package/dist/contract/models.js.map +0 -1
- package/dist/contract/payload-merge.d.ts +0 -76
- package/dist/contract/payload-merge.d.ts.map +0 -1
- package/dist/contract/payload-merge.js +0 -215
- package/dist/contract/payload-merge.js.map +0 -1
- package/dist/events/tool-display.d.ts +0 -22
- package/dist/events/tool-display.d.ts.map +0 -1
- package/dist/events/tool-display.js +0 -60
- package/dist/events/tool-display.js.map +0 -1
- package/dist/events/tool-shape.d.ts +0 -48
- package/dist/events/tool-shape.d.ts.map +0 -1
- package/dist/events/tool-shape.js +0 -225
- package/dist/events/tool-shape.js.map +0 -1
- package/dist/relay/index.d.ts +0 -65
- package/dist/relay/index.d.ts.map +0 -1
- package/dist/relay/index.js +0 -197
- package/dist/relay/index.js.map +0 -1
- package/dist/turns/index.d.ts +0 -11
- package/dist/turns/index.d.ts.map +0 -1
- package/dist/turns/index.js +0 -11
- package/dist/turns/index.js.map +0 -1
- package/dist/turns/queries.d.ts +0 -36
- package/dist/turns/queries.d.ts.map +0 -1
- package/dist/turns/queries.js +0 -37
- package/dist/turns/queries.js.map +0 -1
- package/dist/turns/replay.d.ts +0 -105
- package/dist/turns/replay.d.ts.map +0 -1
- package/dist/turns/replay.js +0 -163
- package/dist/turns/replay.js.map +0 -1
- package/dist/turns/status.d.ts +0 -49
- package/dist/turns/status.d.ts.map +0 -1
- package/dist/turns/status.js +0 -56
- package/dist/turns/status.js.map +0 -1
- package/dist/turns/turn-store.d.ts +0 -109
- package/dist/turns/turn-store.d.ts.map +0 -1
- package/dist/turns/turn-store.js +0 -169
- package/dist/turns/turn-store.js.map +0 -1
- package/dist/warm/client-cache.d.ts +0 -47
- package/dist/warm/client-cache.d.ts.map +0 -1
- package/dist/warm/client-cache.js +0 -48
- package/dist/warm/client-cache.js.map +0 -1
- package/dist/warm/envelope-cache.d.ts +0 -79
- package/dist/warm/envelope-cache.d.ts.map +0 -1
- package/dist/warm/envelope-cache.js +0 -85
- package/dist/warm/envelope-cache.js.map +0 -1
- package/dist/warm/index.d.ts +0 -18
- package/dist/warm/index.d.ts.map +0 -1
- package/dist/warm/index.js +0 -12
- package/dist/warm/index.js.map +0 -1
- package/dist/warm/payload.d.ts +0 -137
- package/dist/warm/payload.d.ts.map +0 -1
- package/dist/warm/payload.js +0 -112
- package/dist/warm/payload.js.map +0 -1
- package/dist/warm/revision.d.ts +0 -70
- package/dist/warm/revision.d.ts.map +0 -1
- package/dist/warm/revision.js +0 -63
- package/dist/warm/revision.js.map +0 -1
- package/dist/warm/store-policy.d.ts +0 -57
- package/dist/warm/store-policy.d.ts.map +0 -1
- package/dist/warm/store-policy.js +0 -89
- package/dist/warm/store-policy.js.map +0 -1
- package/dist/warm/store.d.ts +0 -42
- package/dist/warm/store.d.ts.map +0 -1
- package/dist/warm/store.js +0 -69
- package/dist/warm/store.js.map +0 -1
- package/dist/wire/frames.d.ts +0 -208
- package/dist/wire/frames.d.ts.map +0 -1
- package/dist/wire/frames.js +0 -245
- package/dist/wire/frames.js.map +0 -1
- package/dist/wire/index.d.ts +0 -7
- package/dist/wire/index.d.ts.map +0 -1
- package/dist/wire/index.js +0 -7
- package/dist/wire/index.js.map +0 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// Pi --mode rpc JSONL-over-stdio adapter.
|
|
2
|
+
//
|
|
3
|
+
// Laws come from ../../dist: event constructors via `createEventConstructors`,
|
|
4
|
+
// the normalizer via `createPiNormalizer` (`normalizePiEvent`,
|
|
5
|
+
// `piSessionStatsUsage`).
|
|
6
|
+
|
|
7
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createEventConstructors } from '../../dist/events/index.js';
|
|
12
|
+
import { createPiNormalizer } from '../../dist/normalizers/pi.js';
|
|
13
|
+
import { allowed, requestPermission } from '../permissions.js';
|
|
14
|
+
import { recordNativeEvent } from '../recorder.js';
|
|
15
|
+
import { relayedMcpServers } from '../tooling/mcp-bundle.js';
|
|
16
|
+
import { executableExists, findOnPath } from '../tooling/native-binaries.js';
|
|
17
|
+
import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
|
|
18
|
+
import { composeSystemPrompt } from '../tooling/system-prompt.js';
|
|
19
|
+
import {
|
|
20
|
+
COMMON_REASONING_SETTINGS,
|
|
21
|
+
nativeInputCapabilities,
|
|
22
|
+
UnsupportedExecutionInputError,
|
|
23
|
+
} from './input-capabilities.js';
|
|
24
|
+
import { piPrompt } from './prompt.js';
|
|
25
|
+
|
|
26
|
+
const events = createEventConstructors({ clockMs: () => Date.now() });
|
|
27
|
+
const { done, errorEvent } = events;
|
|
28
|
+
const { normalizePiEvent, piSessionStatsUsage } = createPiNormalizer(events);
|
|
29
|
+
|
|
30
|
+
const GATEWAY_PROVIDER = 'vercel-ai-gateway';
|
|
31
|
+
|
|
32
|
+
// Pi's RPC protocol is JSONL over stdio, but NOT JSON-RPC: commands are
|
|
33
|
+
// {id?, type, ...}, responses are {id?, type:'response', command, success, data},
|
|
34
|
+
// and events are un-id'd JSON lines. Framing is LF-only by spec.
|
|
35
|
+
class PiRpc {
|
|
36
|
+
constructor({ binary, args, cwd, env }) {
|
|
37
|
+
this.id = 1;
|
|
38
|
+
this.pending = new Map();
|
|
39
|
+
this.eventHandlers = new Set();
|
|
40
|
+
this.extensionUiHandler = null;
|
|
41
|
+
this.buffer = '';
|
|
42
|
+
this.closed = false;
|
|
43
|
+
this.closeError = null;
|
|
44
|
+
this.child = spawn(binary, args, {
|
|
45
|
+
cwd,
|
|
46
|
+
env,
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
detached: process.platform !== 'win32',
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
});
|
|
51
|
+
this.child.stdout.setEncoding('utf8');
|
|
52
|
+
this.child.stdout.on('data', (chunk) => this.onData(chunk));
|
|
53
|
+
this.child.stderr.setEncoding('utf8');
|
|
54
|
+
this.child.stderr.on('data', (chunk) => {
|
|
55
|
+
const text = String(chunk || '').trim();
|
|
56
|
+
if (text && process.env.AMALGM_CHAT_DEBUG) console.warn('[Pi]', text.slice(0, 1000));
|
|
57
|
+
});
|
|
58
|
+
this.child.on('error', (err) => this.failAll(err));
|
|
59
|
+
this.child.on('exit', (code, signal) => {
|
|
60
|
+
this.failAll(new Error(`pi rpc agent exited (${code ?? signal ?? 'unknown'})`));
|
|
61
|
+
});
|
|
62
|
+
this.child.stdin.on('error', (err) => this.failAll(err));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
failAll(err) {
|
|
66
|
+
this.closed = true;
|
|
67
|
+
this.closeError = err;
|
|
68
|
+
for (const pending of this.pending.values()) pending.reject(err);
|
|
69
|
+
this.pending.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onData(chunk) {
|
|
73
|
+
this.buffer += chunk;
|
|
74
|
+
const lines = this.buffer.split('\n');
|
|
75
|
+
this.buffer = lines.pop() || '';
|
|
76
|
+
for (const rawLine of lines) {
|
|
77
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
78
|
+
if (!line.trim()) continue;
|
|
79
|
+
let msg;
|
|
80
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
81
|
+
if (process.env.AMALGM_CHAT_DEBUG_PI === '1') {
|
|
82
|
+
console.log('[Pi:recv]', JSON.stringify(msg).slice(0, 4000));
|
|
83
|
+
}
|
|
84
|
+
if (msg.type === 'response') {
|
|
85
|
+
const pending = msg.id !== undefined ? this.pending.get(msg.id) : null;
|
|
86
|
+
if (pending) {
|
|
87
|
+
this.pending.delete(msg.id);
|
|
88
|
+
if (msg.success === false) {
|
|
89
|
+
pending.reject(new Error(msg.error || `pi ${msg.command} failed`));
|
|
90
|
+
} else {
|
|
91
|
+
pending.resolve(msg.data ?? {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (msg.type === 'extension_ui_request') {
|
|
97
|
+
this.handleExtensionUiRequest(msg);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const handler of this.eventHandlers) handler(msg);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async handleExtensionUiRequest(message) {
|
|
105
|
+
if (!this.extensionUiHandler) return;
|
|
106
|
+
try {
|
|
107
|
+
const response = await this.extensionUiHandler(message);
|
|
108
|
+
if (response) this.write({ type: 'extension_ui_response', id: message.id, ...response });
|
|
109
|
+
} catch {
|
|
110
|
+
this.write({ type: 'extension_ui_response', id: message.id, cancelled: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
write(value) {
|
|
115
|
+
if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
116
|
+
try {
|
|
117
|
+
this.child.stdin.write(`${JSON.stringify(value)}\n`, (error) => {
|
|
118
|
+
if (error) this.failAll(error);
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.failAll(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
request(command, params = {}, timeoutMs = 180000) {
|
|
126
|
+
if (this.closeError) return Promise.reject(this.closeError);
|
|
127
|
+
if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) {
|
|
128
|
+
return Promise.reject(new Error('pi rpc agent is not running'));
|
|
129
|
+
}
|
|
130
|
+
const id = `req-${this.id++}`;
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = timeoutMs > 0
|
|
133
|
+
? setTimeout(() => {
|
|
134
|
+
this.pending.delete(id);
|
|
135
|
+
reject(new Error(`pi ${command} timed out`));
|
|
136
|
+
}, timeoutMs)
|
|
137
|
+
: null;
|
|
138
|
+
this.pending.set(id, {
|
|
139
|
+
resolve: (value) => { if (timer) clearTimeout(timer); resolve(value); },
|
|
140
|
+
reject: (err) => { if (timer) clearTimeout(timer); reject(err); },
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
this.child.stdin.write(`${JSON.stringify({ id, type: command, ...params })}\n`, (err) => {
|
|
144
|
+
if (err) this.failAll(err);
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.failAll(err);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onEvent(handler) {
|
|
153
|
+
this.eventHandlers.add(handler);
|
|
154
|
+
return () => this.eventHandlers.delete(handler);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onExtensionUi(handler) {
|
|
158
|
+
this.extensionUiHandler = handler;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
close() {
|
|
162
|
+
try { this.child.stdin.end(); } catch {}
|
|
163
|
+
try {
|
|
164
|
+
if (process.platform !== 'win32' && this.child.pid) process.kill(-this.child.pid, 'SIGTERM');
|
|
165
|
+
else this.child.kill('SIGTERM');
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveBinary() {
|
|
171
|
+
const binaryName = process.platform === 'win32' ? 'pi.cmd' : 'pi';
|
|
172
|
+
const homes = [...new Set([process.env.AMALGM_NATIVE_HOME, os.homedir()].filter(Boolean))];
|
|
173
|
+
const candidates = [
|
|
174
|
+
process.env.PI_BINARY,
|
|
175
|
+
...homes.map((home) => path.join(home, '.npm-global', 'bin', binaryName)),
|
|
176
|
+
'/opt/homebrew/bin/pi',
|
|
177
|
+
'/usr/local/bin/pi',
|
|
178
|
+
findOnPath('pi'),
|
|
179
|
+
].filter(Boolean);
|
|
180
|
+
const failures = [];
|
|
181
|
+
for (const candidate of candidates) {
|
|
182
|
+
if (!executableExists(candidate)) {
|
|
183
|
+
failures.push(`${candidate} (missing or not executable)`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
187
|
+
if (res.status === 0) return candidate;
|
|
188
|
+
failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Pi CLI is not available. Checked: ${failures.join(', ') || 'no candidates'}. Install it with: npm install -g @mariozechner/pi-coding-agent`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Under amalgm auth every gateway request must flow through the local egress
|
|
194
|
+
// proxy. Pi's provider baseUrl is overridden by a tiny extension; the URL rides
|
|
195
|
+
// in on an env var so the extension file itself is static.
|
|
196
|
+
function ensureEgressExtension(runtimeHome) {
|
|
197
|
+
const file = path.join(runtimeHome, '.pi', 'amalgm-egress-extension.js');
|
|
198
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
199
|
+
const source = [
|
|
200
|
+
'// Generated by @amalgm/chat: routes the Vercel AI Gateway provider',
|
|
201
|
+
'// through the per-session amalgm egress proxy for billing and audit.',
|
|
202
|
+
'export default function (pi) {',
|
|
203
|
+
' const baseUrl = process.env.AMALGM_PI_GATEWAY_BASE_URL;',
|
|
204
|
+
` if (baseUrl) pi.registerProvider(${JSON.stringify(GATEWAY_PROVIDER)}, { baseUrl });`,
|
|
205
|
+
'}',
|
|
206
|
+
'',
|
|
207
|
+
].join('\n');
|
|
208
|
+
fs.writeFileSync(file, source, { mode: 0o600 });
|
|
209
|
+
return file;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function piProvider(contract) {
|
|
213
|
+
if (contract.authMethod === 'amalgm') return GATEWAY_PROVIDER;
|
|
214
|
+
const provider = String(contract.providerId || '').trim();
|
|
215
|
+
if (provider === 'vercel' || provider === 'ai-gateway' || provider === 'ai_gateway') return GATEWAY_PROVIDER;
|
|
216
|
+
return provider;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function piModel(contract, provider) {
|
|
220
|
+
const model = String(contract.providerModelId || contract.cliModel || '').trim();
|
|
221
|
+
if (provider === GATEWAY_PROVIDER && contract.authMethod === 'amalgm' && !model.includes('/')) {
|
|
222
|
+
return `${contract.providerId}/${model}`;
|
|
223
|
+
}
|
|
224
|
+
if (provider !== GATEWAY_PROVIDER && model.startsWith(`${provider}/`)) return model.slice(provider.length + 1);
|
|
225
|
+
return model;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const PI_KEY_ENV = Object.freeze({
|
|
229
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
230
|
+
openai: 'OPENAI_API_KEY',
|
|
231
|
+
'openai-codex': 'OPENAI_API_KEY',
|
|
232
|
+
google: 'GEMINI_API_KEY',
|
|
233
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
234
|
+
'azure-openai-responses': 'AZURE_OPENAI_API_KEY',
|
|
235
|
+
[GATEWAY_PROVIDER]: 'AI_GATEWAY_API_KEY',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
function applyPiByokEnvironment(env, contract, provider) {
|
|
239
|
+
if (contract.authMethod !== 'byok') return;
|
|
240
|
+
const variable = PI_KEY_ENV[provider];
|
|
241
|
+
if (!variable) {
|
|
242
|
+
throw new UnsupportedExecutionInputError(
|
|
243
|
+
'pi',
|
|
244
|
+
'auth + provider',
|
|
245
|
+
`${provider} has no declared BYOK credential environment mapping`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
for (const key of Object.values(PI_KEY_ENV)) delete env[key];
|
|
249
|
+
env[variable] = contract.auth.tokenRef || '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function piMcpExtensionSource(configFile, permissionMode) {
|
|
253
|
+
const clientModule = import.meta.resolve('@modelcontextprotocol/sdk/client/index.js');
|
|
254
|
+
const stdioModule = import.meta.resolve('@modelcontextprotocol/sdk/client/stdio.js');
|
|
255
|
+
const httpModule = import.meta.resolve('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
256
|
+
const sseModule = import.meta.resolve('@modelcontextprotocol/sdk/client/sse.js');
|
|
257
|
+
return [
|
|
258
|
+
'// Generated by @amalgm/chat. Pi has no native MCP or permission UI.',
|
|
259
|
+
"import { readFileSync } from 'node:fs';",
|
|
260
|
+
`import { Client } from ${JSON.stringify(clientModule)};`,
|
|
261
|
+
`import { StdioClientTransport } from ${JSON.stringify(stdioModule)};`,
|
|
262
|
+
`import { StreamableHTTPClientTransport } from ${JSON.stringify(httpModule)};`,
|
|
263
|
+
`import { SSEClientTransport } from ${JSON.stringify(sseModule)};`,
|
|
264
|
+
`const servers = JSON.parse(readFileSync(${JSON.stringify(configFile)}, 'utf8'));`,
|
|
265
|
+
'const clients = [];',
|
|
266
|
+
'const headers = (entries = []) => Object.fromEntries(entries.map(({ name, value }) => [name, value]));',
|
|
267
|
+
'async function connect(server) {',
|
|
268
|
+
" const client = new Client({ name: 'amalgm-chat-pi', version: '0.1.2' });",
|
|
269
|
+
' const transport = server.type === "stdio"',
|
|
270
|
+
' ? new StdioClientTransport({ command: server.command, args: server.args, env: { ...process.env, ...headers(server.env) } })',
|
|
271
|
+
' : server.type === "sse"',
|
|
272
|
+
' ? new SSEClientTransport(new URL(server.url), { requestInit: { headers: headers(server.headers) } })',
|
|
273
|
+
' : new StreamableHTTPClientTransport(new URL(server.url), { requestInit: { headers: headers(server.headers) } });',
|
|
274
|
+
' await client.connect(transport);',
|
|
275
|
+
' clients.push(client);',
|
|
276
|
+
' return client;',
|
|
277
|
+
'}',
|
|
278
|
+
'function resultContent(result) {',
|
|
279
|
+
' return (result.content || []).map((part) => {',
|
|
280
|
+
' if (part.type === "text" || part.type === "image") return part;',
|
|
281
|
+
' return { type: "text", text: JSON.stringify(part) };',
|
|
282
|
+
' });',
|
|
283
|
+
'}',
|
|
284
|
+
'export default function (pi) {',
|
|
285
|
+
...(permissionMode === 'ask' ? [
|
|
286
|
+
' const alwaysAllowed = new Set();',
|
|
287
|
+
' pi.on("tool_call", async (event, context) => {',
|
|
288
|
+
' if (alwaysAllowed.has(event.toolName)) return;',
|
|
289
|
+
' const choice = await context.ui.select(`Allow ${event.toolName}?`, ["Allow once", "Always allow", "Deny"]);',
|
|
290
|
+
' if (choice === "Always allow") alwaysAllowed.add(event.toolName);',
|
|
291
|
+
' if (choice !== "Allow once" && choice !== "Always allow") return { block: true, reason: "Denied by the user" };',
|
|
292
|
+
' });',
|
|
293
|
+
] : []),
|
|
294
|
+
' pi.on("session_start", async () => {',
|
|
295
|
+
' for (const server of servers) {',
|
|
296
|
+
' const client = await connect(server);',
|
|
297
|
+
' const listed = await client.listTools();',
|
|
298
|
+
' for (const tool of listed.tools) {',
|
|
299
|
+
' const exposedName = `${server.name}__${tool.name}`.replace(/[^A-Za-z0-9_-]/g, "_");',
|
|
300
|
+
' pi.registerTool({',
|
|
301
|
+
' name: exposedName,',
|
|
302
|
+
' label: tool.title || tool.name,',
|
|
303
|
+
' description: tool.description || `${server.name} MCP tool ${tool.name}`,',
|
|
304
|
+
' parameters: tool.inputSchema || { type: "object", additionalProperties: true },',
|
|
305
|
+
' async execute(_id, parameters) {',
|
|
306
|
+
' const result = await client.callTool({ name: tool.name, arguments: parameters });',
|
|
307
|
+
' return { content: resultContent(result), details: result, isError: result.isError === true };',
|
|
308
|
+
' },',
|
|
309
|
+
' });',
|
|
310
|
+
' }',
|
|
311
|
+
' }',
|
|
312
|
+
' });',
|
|
313
|
+
' pi.on("session_shutdown", async () => {',
|
|
314
|
+
' await Promise.all(clients.map((client) => client.close().catch(() => {})));',
|
|
315
|
+
' });',
|
|
316
|
+
'}',
|
|
317
|
+
'',
|
|
318
|
+
].join('\n');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function ensureAmalgmExtension(runtimeHome, contract) {
|
|
322
|
+
const directory = path.join(runtimeHome, '.pi', 'amalgm');
|
|
323
|
+
const configFile = path.join(directory, 'mcp-servers.json');
|
|
324
|
+
const extensionFile = path.join(directory, 'chat-extension.js');
|
|
325
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
326
|
+
fs.writeFileSync(configFile, `${JSON.stringify(relayedMcpServers(contract), null, 2)}\n`, { mode: 0o600 });
|
|
327
|
+
fs.writeFileSync(extensionFile, piMcpExtensionSource(configFile, contract.permissionMode), { mode: 0o600 });
|
|
328
|
+
return extensionFile;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function thinkingLevelFor(contract) {
|
|
332
|
+
const effort = String(contract.reasoningEffort || '').toLowerCase();
|
|
333
|
+
if (!effort) return null;
|
|
334
|
+
if (['off', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(effort)) return effort;
|
|
335
|
+
throw new UnsupportedExecutionInputError(
|
|
336
|
+
'pi',
|
|
337
|
+
'model.settings.reasoning',
|
|
338
|
+
`unsupported reasoning level ${JSON.stringify(contract.reasoningEffort)}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildArgs(contract, runtimeHome) {
|
|
343
|
+
const provider = piProvider(contract);
|
|
344
|
+
const args = ['--mode', 'rpc', '--provider', provider, '--model', piModel(contract, provider)];
|
|
345
|
+
const thinking = thinkingLevelFor(contract);
|
|
346
|
+
if (thinking) args.push('--thinking', thinking);
|
|
347
|
+
const systemPrompt = composeSystemPrompt(contract);
|
|
348
|
+
if (systemPrompt) args.push('--system-prompt', systemPrompt);
|
|
349
|
+
if (contract.providerSessionId) args.push('--session', contract.providerSessionId);
|
|
350
|
+
if (contract.authMethod === 'amalgm') args.push('--extension', ensureEgressExtension(runtimeHome));
|
|
351
|
+
const hasSelectedTools = Array.isArray(contract.mcpServers) && contract.mcpServers.length > 0;
|
|
352
|
+
if (contract.permissionMode === 'ask' || hasSelectedTools) {
|
|
353
|
+
args.push('--extension', ensureAmalgmExtension(runtimeHome, contract));
|
|
354
|
+
}
|
|
355
|
+
return args;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
class PiAdapter {
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
this.permissionPort = options.requestPermission || null;
|
|
361
|
+
this.inputCapabilities = nativeInputCapabilities({
|
|
362
|
+
authModes: ['platform', 'subscription', 'byok'],
|
|
363
|
+
modelSettings: COMMON_REASONING_SETTINGS,
|
|
364
|
+
tools: true,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
validatePrepared(contract) {
|
|
369
|
+
const provider = piProvider(contract);
|
|
370
|
+
applyPiByokEnvironment({}, contract, provider);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async create(contract) {
|
|
374
|
+
const runtime = prepareHarnessRuntime(contract);
|
|
375
|
+
const binary = resolveBinary();
|
|
376
|
+
const env = { ...runtime.env };
|
|
377
|
+
const provider = piProvider(contract);
|
|
378
|
+
applyPiByokEnvironment(env, contract, provider);
|
|
379
|
+
if (contract.authMethod === 'amalgm' && contract.auth.baseUrl) {
|
|
380
|
+
env.AMALGM_PI_GATEWAY_BASE_URL = contract.auth.baseUrl;
|
|
381
|
+
}
|
|
382
|
+
const client = new PiRpc({
|
|
383
|
+
binary,
|
|
384
|
+
args: buildArgs(contract, runtime.runtimeHome),
|
|
385
|
+
cwd: contract.cwd,
|
|
386
|
+
env,
|
|
387
|
+
});
|
|
388
|
+
client.onExtensionUi(async (request) => {
|
|
389
|
+
if (!['select', 'confirm', 'input', 'editor'].includes(request.method)) return null;
|
|
390
|
+
const decision = await requestPermission(this.permissionPort, {
|
|
391
|
+
adapterId: 'pi',
|
|
392
|
+
providerSessionId: contract.providerSessionId,
|
|
393
|
+
kind: 'tool',
|
|
394
|
+
title: request.title || 'Pi requests permission',
|
|
395
|
+
description: request.message || null,
|
|
396
|
+
input: request,
|
|
397
|
+
});
|
|
398
|
+
if (request.method === 'select') {
|
|
399
|
+
const wanted = decision.outcome === 'allow_always'
|
|
400
|
+
? 'Always allow'
|
|
401
|
+
: decision.outcome === 'allow_once'
|
|
402
|
+
? 'Allow once'
|
|
403
|
+
: 'Deny';
|
|
404
|
+
return request.options?.includes(wanted) ? { value: wanted } : { cancelled: true };
|
|
405
|
+
}
|
|
406
|
+
if (request.method === 'confirm') return { confirmed: allowed(decision) };
|
|
407
|
+
return allowed(decision) && typeof decision.updatedInput?.value === 'string'
|
|
408
|
+
? { value: decision.updatedInput.value }
|
|
409
|
+
: { cancelled: true };
|
|
410
|
+
});
|
|
411
|
+
// Sessions persist as files under the managed home; the file path is the
|
|
412
|
+
// durable resume handle (pi's --session flag takes a path).
|
|
413
|
+
const stats = await client.request('get_session_stats', {}, 60000);
|
|
414
|
+
return {
|
|
415
|
+
sessionId: contract.sessionId,
|
|
416
|
+
providerSessionId: stats.sessionFile || contract.providerSessionId || null,
|
|
417
|
+
client,
|
|
418
|
+
cancelRequested: false,
|
|
419
|
+
activePrompt: null,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async resume(session, contract) {
|
|
424
|
+
if (session.client && !session.client.closed) return session;
|
|
425
|
+
return this.create({ ...contract, providerSessionId: session.providerSessionId || contract.providerSessionId });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async stop(session) {
|
|
429
|
+
session.cancelRequested = true;
|
|
430
|
+
if (!session.activePrompt) return;
|
|
431
|
+
await session.client.request('abort', {}, 15000).catch(() => {});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async destroy(session) {
|
|
435
|
+
session.client.close();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async *prompt(session, input, contract) {
|
|
439
|
+
const queue = [];
|
|
440
|
+
let complete = false;
|
|
441
|
+
let wake = null;
|
|
442
|
+
const wakeLoop = () => { if (wake) { wake(); wake = null; } };
|
|
443
|
+
// Unlike the other adapters, an `error` event does NOT complete the turn:
|
|
444
|
+
// pi still emits agent_end afterwards, and finish() owns the done event.
|
|
445
|
+
const push = (e) => {
|
|
446
|
+
if (e.type === 'done') complete = true;
|
|
447
|
+
queue.push(e);
|
|
448
|
+
wakeLoop();
|
|
449
|
+
};
|
|
450
|
+
const state = { providerSessionId: session.providerSessionId, tools: new Set() };
|
|
451
|
+
let sawError = false;
|
|
452
|
+
session.cancelRequested = false;
|
|
453
|
+
session.activePrompt = state;
|
|
454
|
+
const finish = async () => {
|
|
455
|
+
// Attach the post-turn context snapshot before done so the context circle
|
|
456
|
+
// lands on the final number even if per-message events raced.
|
|
457
|
+
const stats = await session.client.request('get_session_stats', {}, 30000).catch(() => null);
|
|
458
|
+
recordNativeEvent('pi.rpc.session_stats', stats, {
|
|
459
|
+
providerSessionId: session.providerSessionId,
|
|
460
|
+
sessionId: contract.sessionId,
|
|
461
|
+
assistantMessageId: contract.assistantMessageId,
|
|
462
|
+
});
|
|
463
|
+
const snapshot = stats ? piSessionStatsUsage(stats, { providerSessionId: session.providerSessionId }) : null;
|
|
464
|
+
if (snapshot) push(snapshot);
|
|
465
|
+
push(done({
|
|
466
|
+
providerSessionId: session.providerSessionId,
|
|
467
|
+
stopReason: session.cancelRequested ? 'cancelled' : sawError ? 'error' : 'end_turn',
|
|
468
|
+
}));
|
|
469
|
+
};
|
|
470
|
+
const off = session.client.onEvent((event) => {
|
|
471
|
+
recordNativeEvent('pi.rpc.event', event, {
|
|
472
|
+
providerSessionId: session.providerSessionId,
|
|
473
|
+
sessionId: contract.sessionId,
|
|
474
|
+
assistantMessageId: contract.assistantMessageId,
|
|
475
|
+
eventType: event?.type,
|
|
476
|
+
});
|
|
477
|
+
if (event?.type === 'agent_end') {
|
|
478
|
+
finish().catch(() => push(done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
for (const e of normalizePiEvent(event, state)) {
|
|
482
|
+
if (e.type === 'error') sawError = true;
|
|
483
|
+
push(e);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
session.client.request('prompt', piPrompt(input), 0).catch((err) => {
|
|
487
|
+
if (session.cancelRequested) {
|
|
488
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
|
|
492
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
|
|
493
|
+
});
|
|
494
|
+
try {
|
|
495
|
+
while (!complete || queue.length) {
|
|
496
|
+
if (!queue.length) await new Promise((resolve) => { wake = resolve; });
|
|
497
|
+
else yield queue.shift();
|
|
498
|
+
}
|
|
499
|
+
} finally {
|
|
500
|
+
off();
|
|
501
|
+
session.activePrompt = null;
|
|
502
|
+
session.cancelRequested = false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const __private = {
|
|
508
|
+
PiRpc,
|
|
509
|
+
applyPiByokEnvironment,
|
|
510
|
+
buildArgs,
|
|
511
|
+
ensureAmalgmExtension,
|
|
512
|
+
ensureEgressExtension,
|
|
513
|
+
piModel,
|
|
514
|
+
piProvider,
|
|
515
|
+
resolveBinary,
|
|
516
|
+
thinkingLevelFor,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export { PiAdapter, __private };
|