@automatalabs/pi-acp 0.1.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/LICENSE +202 -0
- package/README.md +57 -0
- package/dist/agent.d.ts +87 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +398 -0
- package/dist/auth.d.ts +4 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +40 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +46 -0
- package/dist/deps.d.ts +22 -0
- package/dist/deps.d.ts.map +1 -0
- package/dist/deps.js +39 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +104 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/lib.d.ts +4 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +2 -0
- package/dist/mcp-bridge.d.ts +58 -0
- package/dist/mcp-bridge.d.ts.map +1 -0
- package/dist/mcp-bridge.js +240 -0
- package/dist/permissions.d.ts +11 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +77 -0
- package/dist/prompt-content.d.ts +12 -0
- package/dist/prompt-content.d.ts.map +1 -0
- package/dist/prompt-content.js +35 -0
- package/dist/replay.d.ts +4 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +101 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +23 -0
- package/dist/session.d.ts +58 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +414 -0
- package/dist/stop-reason.d.ts +4 -0
- package/dist/stop-reason.d.ts.map +1 -0
- package/dist/stop-reason.js +18 -0
- package/dist/structured-output.d.ts +14 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/structured-output.js +71 -0
- package/dist/translate.d.ts +20 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +110 -0
- package/dist/usage.d.ts +35 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +36 -0
- package/package.json +47 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
2
|
+
const LABELS = {
|
|
3
|
+
auth_error: "provider credentials required",
|
|
4
|
+
rate_limit: "provider rate limit",
|
|
5
|
+
billing_error: "provider billing or quota wall",
|
|
6
|
+
provider_error: "provider error",
|
|
7
|
+
invalid_model: "unknown or unselectable model",
|
|
8
|
+
empty_prompt: "prompt has no text or images",
|
|
9
|
+
session_busy: "session has a turn in flight",
|
|
10
|
+
invalid_config_value: "invalid config option",
|
|
11
|
+
invalid_config_type: "invalid config option",
|
|
12
|
+
unknown_config_option: "invalid config option",
|
|
13
|
+
invalid_cwd: "invalid working directory",
|
|
14
|
+
unknown_session: "unknown session id",
|
|
15
|
+
session_already_open: "session already open",
|
|
16
|
+
session_terminated: "session terminated",
|
|
17
|
+
session_corrupt: "session file could not be read",
|
|
18
|
+
session_not_forkable: "session has no persisted history to fork",
|
|
19
|
+
mcp_init_error: "mcp server initialization failed",
|
|
20
|
+
unsupported_mcp_transport: "unsupported mcp transport",
|
|
21
|
+
structured_tool_collision: "structured-output tool unavailable",
|
|
22
|
+
invalid_output_schema: "invalid output schema",
|
|
23
|
+
invalid_cursor: "invalid list cursor",
|
|
24
|
+
unknown_auth_method: "unknown auth method",
|
|
25
|
+
notification_error: "notification delivery failed",
|
|
26
|
+
internal_error: "internal error",
|
|
27
|
+
};
|
|
28
|
+
const INVALID_KINDS = new Set([
|
|
29
|
+
"invalid_model",
|
|
30
|
+
"empty_prompt",
|
|
31
|
+
"session_busy",
|
|
32
|
+
"invalid_config_value",
|
|
33
|
+
"invalid_config_type",
|
|
34
|
+
"unknown_config_option",
|
|
35
|
+
"invalid_cwd",
|
|
36
|
+
"unknown_session",
|
|
37
|
+
"session_already_open",
|
|
38
|
+
"session_terminated",
|
|
39
|
+
"session_not_forkable",
|
|
40
|
+
"unsupported_mcp_transport",
|
|
41
|
+
"invalid_output_schema",
|
|
42
|
+
"invalid_cursor",
|
|
43
|
+
"unknown_auth_method",
|
|
44
|
+
]);
|
|
45
|
+
export function redactedDiagnostics(diagnostics) {
|
|
46
|
+
return diagnostics?.length
|
|
47
|
+
? diagnostics.map(({ type, timestamp }) => ({ type, timestamp }))
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
export function adapterError(kind, extras = {}) {
|
|
51
|
+
const data = { errorKind: kind, message: LABELS[kind] };
|
|
52
|
+
if (extras.server !== undefined)
|
|
53
|
+
data.server = extras.server;
|
|
54
|
+
if (extras.details !== undefined)
|
|
55
|
+
data.details = extras.details;
|
|
56
|
+
if (kind === "auth_error")
|
|
57
|
+
return RequestError.authRequired(data);
|
|
58
|
+
if (INVALID_KINDS.has(kind))
|
|
59
|
+
return RequestError.invalidParams(data);
|
|
60
|
+
return RequestError.internalError(data);
|
|
61
|
+
}
|
|
62
|
+
export function classifyPreflight(error) {
|
|
63
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
64
|
+
if (message.includes("no model selected"))
|
|
65
|
+
return adapterError("invalid_model");
|
|
66
|
+
if (message.includes("no api key found") ||
|
|
67
|
+
message.includes("authentication failed for") ||
|
|
68
|
+
message.includes("run '/login")) {
|
|
69
|
+
return adapterError("auth_error");
|
|
70
|
+
}
|
|
71
|
+
return adapterError("provider_error");
|
|
72
|
+
}
|
|
73
|
+
export function classifyTerminal(message) {
|
|
74
|
+
const diagnostics = message.diagnostics ?? [];
|
|
75
|
+
const haystack = [
|
|
76
|
+
message.errorMessage ?? "",
|
|
77
|
+
...diagnostics.flatMap((item) => [
|
|
78
|
+
item.type,
|
|
79
|
+
item.error?.name ?? "",
|
|
80
|
+
item.error?.message ?? "",
|
|
81
|
+
]),
|
|
82
|
+
]
|
|
83
|
+
.join("\n")
|
|
84
|
+
.toLowerCase();
|
|
85
|
+
if (/\b401\b|\b403\b|unauthorized|invalid api key|authentication|forbidden|expired/.test(haystack)) {
|
|
86
|
+
return adapterError("auth_error");
|
|
87
|
+
}
|
|
88
|
+
if (/quota|billing|insufficient|payment|credit|exceeded your/.test(haystack)) {
|
|
89
|
+
return adapterError("billing_error");
|
|
90
|
+
}
|
|
91
|
+
if (/\b429\b|rate limit|too many requests|overloaded/.test(haystack)) {
|
|
92
|
+
return adapterError("rate_limit");
|
|
93
|
+
}
|
|
94
|
+
return adapterError("provider_error", { details: redactedDiagnostics(diagnostics) });
|
|
95
|
+
}
|
|
96
|
+
export function unexpectedError(error, terminal) {
|
|
97
|
+
console.error("pi-acp internal error:", error);
|
|
98
|
+
return adapterError("internal_error", {
|
|
99
|
+
details: redactedDiagnostics(terminal?.diagnostics),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function isRequestError(error) {
|
|
103
|
+
return error instanceof RequestError;
|
|
104
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// No static pi/SDK/adapter imports: stdout is reserved before module evaluation.
|
|
3
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
4
|
+
const manifest = await import("../package.json", { with: { type: "json" } }).then((module) => module.default);
|
|
5
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
6
|
+
process.exit(0);
|
|
7
|
+
}
|
|
8
|
+
console.log = console.error;
|
|
9
|
+
console.info = console.error;
|
|
10
|
+
console.warn = console.error;
|
|
11
|
+
console.debug = console.error;
|
|
12
|
+
process.on("unhandledRejection", (reason) => {
|
|
13
|
+
console.error("unhandledRejection:", reason);
|
|
14
|
+
});
|
|
15
|
+
function withTimeout(promise, milliseconds) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const timer = setTimeout(() => reject(new Error("shutdown timed out")), milliseconds);
|
|
18
|
+
promise.then((value) => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
resolve(value);
|
|
21
|
+
}, (error) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const { runAcp } = await import("./server.js");
|
|
29
|
+
const { connection, agent } = await runAcp();
|
|
30
|
+
let shuttingDown;
|
|
31
|
+
const shutdown = (code) => {
|
|
32
|
+
shuttingDown ??= (async () => {
|
|
33
|
+
try {
|
|
34
|
+
await withTimeout(agent.dispose(), 5_000);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("shutdown error:", error);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
process.exit(code);
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
return shuttingDown;
|
|
44
|
+
};
|
|
45
|
+
connection.closed.then(() => shutdown(0), () => shutdown(1));
|
|
46
|
+
process.on("SIGTERM", () => { void shutdown(0); });
|
|
47
|
+
process.on("SIGINT", () => { void shutdown(0); });
|
|
48
|
+
process.stdin.resume();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("startup error:", error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
export {};
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { McpServer, McpServerStdio } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import type { PiAcpDeps } from "./deps.js";
|
|
5
|
+
export interface McpToolInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
inputSchema: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface McpListResult {
|
|
11
|
+
tools: McpToolInfo[];
|
|
12
|
+
nextCursor?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface McpClientHandle {
|
|
15
|
+
listTools(cursor: string | undefined, signal: AbortSignal, timeoutMs: number): Promise<McpListResult>;
|
|
16
|
+
callTool(name: string, args: unknown, signal: AbortSignal, timeoutMs: number): Promise<CallToolResult>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare class McpTimeoutError extends Error {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
22
|
+
export declare function bounded<T>(operation: Promise<T>, signal: AbortSignal, timeoutMs: number, sleep: PiAcpDeps["sleep"]): Promise<T>;
|
|
23
|
+
export declare function connectDefaultMcpClient(server: McpServerStdio, signal: AbortSignal, timeoutMs: number, sleep: PiAcpDeps["sleep"]): Promise<McpClientHandle>;
|
|
24
|
+
export declare function allocateAlias(server: string, tool: string, used: Set<string>): string;
|
|
25
|
+
type McpContent = CallToolResult["content"][number];
|
|
26
|
+
export declare function convertMcpContent(content: McpContent): {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "image";
|
|
31
|
+
data: string;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function convertMcpResult(result: CallToolResult): {
|
|
35
|
+
details?: {
|
|
36
|
+
[x: string]: unknown;
|
|
37
|
+
} | undefined;
|
|
38
|
+
content: ({
|
|
39
|
+
type: "text";
|
|
40
|
+
text: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: "image";
|
|
43
|
+
data: string;
|
|
44
|
+
mimeType: string;
|
|
45
|
+
})[];
|
|
46
|
+
};
|
|
47
|
+
export type McpResultProjection = ReturnType<typeof convertMcpResult>;
|
|
48
|
+
export interface McpBridge {
|
|
49
|
+
clients: McpClientHandle[];
|
|
50
|
+
tools: ToolDefinition[];
|
|
51
|
+
aliases: string[];
|
|
52
|
+
aliasServers: Map<string, string>;
|
|
53
|
+
failedResults: Map<string, McpResultProjection>;
|
|
54
|
+
}
|
|
55
|
+
export declare function bridgeMcpServers(servers: readonly McpServer[], openSignal: AbortSignal, deps: PiAcpDeps): Promise<McpBridge>;
|
|
56
|
+
export declare function disposeMcpBridge(clients: readonly McpClientHandle[], deps: PiAcpDeps): Promise<void>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=mcp-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-bridge.d.ts","sourceRoot":"","sources":["../src/mcp-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { adapterError } from "./errors.js";
|
|
4
|
+
export class McpTimeoutError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("MCP operation timed out");
|
|
7
|
+
this.name = "McpTimeoutError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function abortPromise(signal) {
|
|
11
|
+
return new Promise((_, reject) => {
|
|
12
|
+
if (signal.aborted)
|
|
13
|
+
reject(signal.reason);
|
|
14
|
+
else
|
|
15
|
+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function bounded(operation, signal, timeoutMs, sleep) {
|
|
19
|
+
const timeoutController = new AbortController();
|
|
20
|
+
const timeout = sleep(timeoutMs, timeoutController.signal).then(() => {
|
|
21
|
+
throw new McpTimeoutError();
|
|
22
|
+
});
|
|
23
|
+
operation.then(() => undefined, () => undefined);
|
|
24
|
+
try {
|
|
25
|
+
return await Promise.race([operation, abortPromise(signal), timeout]);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
timeoutController.abort();
|
|
29
|
+
timeout.catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function connectDefaultMcpClient(server, signal, timeoutMs, sleep) {
|
|
33
|
+
const client = new Client({ name: "@automatalabs/pi-acp", version: "0.0.0" });
|
|
34
|
+
const transport = new StdioClientTransport({
|
|
35
|
+
command: server.command,
|
|
36
|
+
args: server.args,
|
|
37
|
+
env: Object.fromEntries(server.env.map(({ name, value }) => [name, value])),
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
await bounded(client.connect(transport), signal, timeoutMs, sleep);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const pid = transport.pid;
|
|
44
|
+
const close = transport.close().catch(() => undefined);
|
|
45
|
+
try {
|
|
46
|
+
await bounded(close, new AbortController().signal, timeoutMs, sleep);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
if (pid !== null) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(pid, "SIGKILL");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// The child may have exited between the timeout and the kill.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await bounded(close, new AbortController().signal, timeoutMs, sleep);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
close.then(() => undefined, () => undefined);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
let closed = false;
|
|
67
|
+
return {
|
|
68
|
+
async listTools(cursor, requestSignal, requestTimeout) {
|
|
69
|
+
const result = await client.listTools(cursor ? { cursor } : undefined, {
|
|
70
|
+
signal: requestSignal,
|
|
71
|
+
timeout: requestTimeout,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
tools: result.tools.map((tool) => ({
|
|
75
|
+
name: tool.name,
|
|
76
|
+
description: tool.description,
|
|
77
|
+
inputSchema: tool.inputSchema,
|
|
78
|
+
})),
|
|
79
|
+
nextCursor: result.nextCursor,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
callTool(name, args, requestSignal, requestTimeout) {
|
|
83
|
+
return client.callTool({ name, arguments: typeof args === "object" && args !== null ? args : {} }, undefined, { signal: requestSignal, timeout: requestTimeout }).then((result) => {
|
|
84
|
+
if (!("content" in result))
|
|
85
|
+
throw new Error("MCP task result did not contain tool content");
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
async close() {
|
|
90
|
+
if (closed)
|
|
91
|
+
return;
|
|
92
|
+
closed = true;
|
|
93
|
+
await client.close();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function slug(value) {
|
|
98
|
+
const sanitized = value.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/_+/g, "_");
|
|
99
|
+
return sanitized || "_";
|
|
100
|
+
}
|
|
101
|
+
export function allocateAlias(server, tool, used) {
|
|
102
|
+
const base = `mcp__${slug(server)}__${slug(tool)}`;
|
|
103
|
+
let candidate = base.slice(0, 128);
|
|
104
|
+
if (!used.has(candidate)) {
|
|
105
|
+
used.add(candidate);
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
for (let index = 2;; index += 1) {
|
|
109
|
+
const suffix = `_${index}`;
|
|
110
|
+
candidate = `${base.slice(0, 128 - suffix.length)}${suffix}`;
|
|
111
|
+
if (!used.has(candidate)) {
|
|
112
|
+
used.add(candidate);
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function convertMcpContent(content) {
|
|
118
|
+
switch (content.type) {
|
|
119
|
+
case "text":
|
|
120
|
+
return { type: "text", text: content.text };
|
|
121
|
+
case "image":
|
|
122
|
+
return { type: "image", data: content.data, mimeType: content.mimeType };
|
|
123
|
+
case "audio":
|
|
124
|
+
return { type: "text", text: "[unsupported audio tool-result omitted]" };
|
|
125
|
+
case "resource_link":
|
|
126
|
+
return { type: "text", text: `[${content.title ?? content.name ?? content.uri}](${content.uri})` };
|
|
127
|
+
case "resource":
|
|
128
|
+
return {
|
|
129
|
+
type: "text",
|
|
130
|
+
text: "text" in content.resource
|
|
131
|
+
? content.resource.text
|
|
132
|
+
: `[embedded resource: ${content.resource.uri}]`,
|
|
133
|
+
};
|
|
134
|
+
default: {
|
|
135
|
+
const exhaustive = content;
|
|
136
|
+
return exhaustive;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function convertMcpResult(result) {
|
|
141
|
+
return {
|
|
142
|
+
content: result.content.map(convertMcpContent),
|
|
143
|
+
...(result.structuredContent === undefined ? {} : { details: result.structuredContent }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function closeClients(clients, deps) {
|
|
147
|
+
await Promise.allSettled(clients.map((client) => bounded(client.close(), new AbortController().signal, deps.mcpTimeoutMs, deps.sleep).catch((error) => {
|
|
148
|
+
console.error("pi-acp MCP close error:", error);
|
|
149
|
+
})));
|
|
150
|
+
}
|
|
151
|
+
export async function bridgeMcpServers(servers, openSignal, deps) {
|
|
152
|
+
const seenNames = new Set();
|
|
153
|
+
for (const server of servers) {
|
|
154
|
+
if (seenNames.has(server.name))
|
|
155
|
+
throw adapterError("mcp_init_error", { server: server.name });
|
|
156
|
+
seenNames.add(server.name);
|
|
157
|
+
if ("type" in server) {
|
|
158
|
+
throw adapterError("unsupported_mcp_transport", { server: server.name });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const clients = [];
|
|
162
|
+
const tools = [];
|
|
163
|
+
const aliases = [];
|
|
164
|
+
const aliasServers = new Map();
|
|
165
|
+
const failedResults = new Map();
|
|
166
|
+
const usedAliases = new Set();
|
|
167
|
+
try {
|
|
168
|
+
for (const server of servers) {
|
|
169
|
+
let handle;
|
|
170
|
+
try {
|
|
171
|
+
handle = await bounded(deps.connectMcpClient(server, openSignal), openSignal, deps.mcpTimeoutMs, deps.sleep);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (openSignal.aborted)
|
|
175
|
+
throw error;
|
|
176
|
+
throw adapterError("mcp_init_error", { server: server.name });
|
|
177
|
+
}
|
|
178
|
+
clients.push(handle);
|
|
179
|
+
const serverTools = [];
|
|
180
|
+
const cursors = new Set();
|
|
181
|
+
let cursor;
|
|
182
|
+
try {
|
|
183
|
+
do {
|
|
184
|
+
if (cursor !== undefined) {
|
|
185
|
+
if (cursors.has(cursor))
|
|
186
|
+
throw new Error("cycling tools/list cursor");
|
|
187
|
+
cursors.add(cursor);
|
|
188
|
+
}
|
|
189
|
+
const page = await bounded(handle.listTools(cursor, openSignal, deps.mcpTimeoutMs), openSignal, deps.mcpTimeoutMs, deps.sleep);
|
|
190
|
+
serverTools.push(...page.tools);
|
|
191
|
+
cursor = page.nextCursor;
|
|
192
|
+
} while (cursor !== undefined);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (openSignal.aborted)
|
|
196
|
+
throw error;
|
|
197
|
+
throw adapterError("mcp_init_error", { server: server.name });
|
|
198
|
+
}
|
|
199
|
+
for (const remoteTool of serverTools) {
|
|
200
|
+
const alias = allocateAlias(server.name, remoteTool.name, usedAliases);
|
|
201
|
+
aliases.push(alias);
|
|
202
|
+
aliasServers.set(alias, server.name);
|
|
203
|
+
const tool = {
|
|
204
|
+
name: alias,
|
|
205
|
+
label: remoteTool.name,
|
|
206
|
+
description: remoteTool.description ?? `MCP tool ${remoteTool.name}`,
|
|
207
|
+
parameters: remoteTool.inputSchema,
|
|
208
|
+
execute: async (_toolCallId, params, signal) => {
|
|
209
|
+
const turnSignal = signal ?? new AbortController().signal;
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await bounded(handle.callTool(remoteTool.name, params, turnSignal, deps.mcpTimeoutMs), turnSignal, deps.mcpTimeoutMs, deps.sleep);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof McpTimeoutError) {
|
|
216
|
+
throw new Error(`MCP tool ${alias} timed out`);
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`MCP tool ${alias} failed`);
|
|
219
|
+
}
|
|
220
|
+
const converted = convertMcpResult(result);
|
|
221
|
+
if (result.isError) {
|
|
222
|
+
failedResults.set(_toolCallId, converted);
|
|
223
|
+
throw new Error(`MCP tool ${alias} returned an error result`);
|
|
224
|
+
}
|
|
225
|
+
return converted;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
tools.push(tool);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { clients, tools, aliases, aliasServers, failedResults };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
await closeClients(clients, deps);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function disposeMcpBridge(clients, deps) {
|
|
239
|
+
await closeClients(clients, deps);
|
|
240
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentContext } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
interface PermissionHost {
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly client: AgentContext;
|
|
6
|
+
drain(): Promise<void>;
|
|
7
|
+
turnSignal(): AbortSignal | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function installPermissionWrapper(session: AgentSession, host: PermissionHost): void;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpE,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,UAAU,IAAI,WAAW,GAAG,SAAS,CAAC;CACvC;AAaD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CA2D1F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { methods } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { mapKind } from "./translate.js";
|
|
3
|
+
function raceSignal(promise, signal) {
|
|
4
|
+
promise.then(() => undefined, () => undefined);
|
|
5
|
+
if (signal.aborted)
|
|
6
|
+
return Promise.resolve(undefined);
|
|
7
|
+
return Promise.race([
|
|
8
|
+
promise,
|
|
9
|
+
new Promise((resolve) => {
|
|
10
|
+
signal.addEventListener("abort", () => resolve(undefined), { once: true });
|
|
11
|
+
}),
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
export function installPermissionWrapper(session, host) {
|
|
15
|
+
const inner = session.agent.beforeToolCall;
|
|
16
|
+
const alwaysAllowed = new Set();
|
|
17
|
+
session.agent.beforeToolCall = async (context, signal) => {
|
|
18
|
+
const toolName = context.toolCall.name;
|
|
19
|
+
let block = false;
|
|
20
|
+
let reason;
|
|
21
|
+
if (!alwaysAllowed.has(toolName)) {
|
|
22
|
+
await host.drain();
|
|
23
|
+
const turnSignal = host.turnSignal() ?? signal ?? new AbortController().signal;
|
|
24
|
+
try {
|
|
25
|
+
const pending = host.client.request(methods.client.session.requestPermission, {
|
|
26
|
+
sessionId: host.sessionId,
|
|
27
|
+
toolCall: {
|
|
28
|
+
toolCallId: context.toolCall.id,
|
|
29
|
+
title: toolName,
|
|
30
|
+
kind: mapKind(toolName),
|
|
31
|
+
_meta: { toolName },
|
|
32
|
+
},
|
|
33
|
+
options: [
|
|
34
|
+
{ optionId: "allow_always", name: `Always allow ${toolName}`, kind: "allow_always" },
|
|
35
|
+
{ optionId: "allow_once", name: "Allow once", kind: "allow_once" },
|
|
36
|
+
{ optionId: "reject_once", name: "Reject", kind: "reject_once" },
|
|
37
|
+
],
|
|
38
|
+
}, { cancellationSignal: turnSignal });
|
|
39
|
+
const response = await raceSignal(pending, turnSignal);
|
|
40
|
+
if (response === undefined) {
|
|
41
|
+
block = true;
|
|
42
|
+
reason = "cancelled";
|
|
43
|
+
}
|
|
44
|
+
else if (typeof response.outcome !== "object" || response.outcome === null || !("outcome" in response.outcome)) {
|
|
45
|
+
block = true;
|
|
46
|
+
reason = "unrecognized permission selection";
|
|
47
|
+
}
|
|
48
|
+
else if (response.outcome.outcome === "cancelled") {
|
|
49
|
+
block = true;
|
|
50
|
+
reason = "cancelled";
|
|
51
|
+
}
|
|
52
|
+
else if (response.outcome.optionId === "allow_once") {
|
|
53
|
+
block = false;
|
|
54
|
+
}
|
|
55
|
+
else if (response.outcome.optionId === "allow_always") {
|
|
56
|
+
alwaysAllowed.add(toolName);
|
|
57
|
+
block = false;
|
|
58
|
+
}
|
|
59
|
+
else if (response.outcome.optionId === "reject_once") {
|
|
60
|
+
block = true;
|
|
61
|
+
reason = "denied by user";
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
block = true;
|
|
65
|
+
reason = "unrecognized permission selection";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
block = true;
|
|
70
|
+
reason = "permission unavailable";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (block)
|
|
74
|
+
return { block: true, reason };
|
|
75
|
+
return inner ? inner(context, signal) : undefined;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
2
|
+
export interface PiImage {
|
|
3
|
+
type: "image";
|
|
4
|
+
data: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ConvertedPrompt {
|
|
8
|
+
text: string;
|
|
9
|
+
images: PiImage[];
|
|
10
|
+
}
|
|
11
|
+
export declare function convertPromptContent(blocks: readonly ContentBlock[]): ConvertedPrompt;
|
|
12
|
+
//# sourceMappingURL=prompt-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-content.d.ts","sourceRoot":"","sources":["../src/prompt-content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,CAmCrF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { adapterError } from "./errors.js";
|
|
2
|
+
export function convertPromptContent(blocks) {
|
|
3
|
+
const text = [];
|
|
4
|
+
const images = [];
|
|
5
|
+
for (const block of blocks) {
|
|
6
|
+
switch (block.type) {
|
|
7
|
+
case "text":
|
|
8
|
+
text.push(block.text);
|
|
9
|
+
break;
|
|
10
|
+
case "image":
|
|
11
|
+
images.push({ type: "image", data: block.data, mimeType: block.mimeType });
|
|
12
|
+
break;
|
|
13
|
+
case "resource_link":
|
|
14
|
+
text.push(`[${block.title ?? block.name ?? block.uri}](${block.uri})`);
|
|
15
|
+
break;
|
|
16
|
+
case "resource":
|
|
17
|
+
text.push("text" in block.resource
|
|
18
|
+
? block.resource.text
|
|
19
|
+
: `[embedded resource: ${block.resource.uri}]`);
|
|
20
|
+
break;
|
|
21
|
+
case "audio":
|
|
22
|
+
text.push("[unsupported audio content omitted]");
|
|
23
|
+
break;
|
|
24
|
+
default: {
|
|
25
|
+
const exhaustive = block;
|
|
26
|
+
return exhaustive;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const joined = text.join("\n\n");
|
|
31
|
+
if (!text.some((segment) => segment.length > 0) && images.length === 0) {
|
|
32
|
+
throw adapterError("empty_prompt");
|
|
33
|
+
}
|
|
34
|
+
return { text: joined, images };
|
|
35
|
+
}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoGpE,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,EAAE,CAwBhE"}
|