@anhlp/alp-client 0.0.0-beta.1
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/NOTICE +7 -0
- package/README.md +24 -0
- package/dist/acp-probe.d.ts +44 -0
- package/dist/acp-probe.js +37 -0
- package/dist/antigravity-probe.d.ts +34 -0
- package/dist/antigravity-probe.js +95 -0
- package/dist/command.d.ts +5 -0
- package/dist/command.js +33 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.js +587 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +422 -0
- package/dist/ipc.d.ts +42 -0
- package/dist/ipc.js +69 -0
- package/dist/library-test.d.ts +94 -0
- package/dist/library-test.js +37 -0
- package/dist/mcp-probe.d.ts +24 -0
- package/dist/mcp-probe.js +147 -0
- package/dist/portable.d.ts +18 -0
- package/dist/portable.js +27 -0
- package/dist/prompt.d.ts +32 -0
- package/dist/prompt.js +59 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.js +120 -0
- package/dist/rpc/client.d.ts +82 -0
- package/dist/rpc/client.js +292 -0
- package/dist/rpc/errors.d.ts +42 -0
- package/dist/rpc/errors.js +34 -0
- package/dist/rpc/framing.d.ts +21 -0
- package/dist/rpc/framing.js +57 -0
- package/dist/rpc/handshake.d.ts +19 -0
- package/dist/rpc/handshake.js +8 -0
- package/dist/rpc/reconnect.d.ts +97 -0
- package/dist/rpc/reconnect.js +227 -0
- package/dist/rpc/timers.d.ts +11 -0
- package/dist/rpc/timers.js +11 -0
- package/dist/service.d.ts +50 -0
- package/dist/service.js +143 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +146 -0
- package/dist/supervise.d.ts +12 -0
- package/dist/supervise.js +81 -0
- package/dist/websocket.d.ts +51 -0
- package/dist/websocket.js +190 -0
- package/package.json +168 -0
- package/src/acp-probe.js +46 -0
- package/src/antigravity-probe.js +106 -0
- package/src/command.js +18 -0
- package/src/doctor.d.ts +13 -0
- package/src/doctor.js +570 -0
- package/src/index.js +457 -0
- package/src/ipc.js +92 -0
- package/src/library-test.js +41 -0
- package/src/mcp-probe.js +118 -0
- package/src/portable.js +27 -0
- package/src/prompt.js +64 -0
- package/src/render.js +136 -0
- package/src/rpc/client.js +296 -0
- package/src/rpc/errors.js +54 -0
- package/src/rpc/framing.js +61 -0
- package/src/rpc/handshake.js +16 -0
- package/src/rpc/reconnect.js +226 -0
- package/src/rpc/timers.js +16 -0
- package/src/service.js +154 -0
- package/src/setup.js +150 -0
- package/src/supervise.d.ts +12 -0
- package/src/supervise.js +85 -0
- package/src/websocket.js +259 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tries an MCP server, a hook or an ACP provider out (ALPD §43, §46). An MCP server is
|
|
3
|
+
* started and its tools listed; a provider is started and initialized; a hook runs once with a sample payload for its event, in the project (or the
|
|
4
|
+
* library when there is none), with the variables ALP gives hooks.
|
|
5
|
+
*/
|
|
6
|
+
export function testEntry(kind: any, name: any, { root, library, templates, scope, timeoutMs }?: {}): Promise<{
|
|
7
|
+
server?: {
|
|
8
|
+
name?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
};
|
|
11
|
+
tools: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}>;
|
|
15
|
+
kind: any;
|
|
16
|
+
name: any;
|
|
17
|
+
source: "library" | "project" | "builtin";
|
|
18
|
+
ok: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
signal: string | null;
|
|
22
|
+
timedOut: boolean;
|
|
23
|
+
stdout: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
payload: {
|
|
27
|
+
branch?: string | undefined;
|
|
28
|
+
files?: never[] | undefined;
|
|
29
|
+
task?: any;
|
|
30
|
+
reason?: string | undefined;
|
|
31
|
+
handoff?: {
|
|
32
|
+
outcome: string;
|
|
33
|
+
summary: string;
|
|
34
|
+
} | undefined;
|
|
35
|
+
event: any;
|
|
36
|
+
sample: boolean;
|
|
37
|
+
project: any;
|
|
38
|
+
session: string;
|
|
39
|
+
agent: string;
|
|
40
|
+
};
|
|
41
|
+
wouldBlock?: boolean | undefined;
|
|
42
|
+
kind: any;
|
|
43
|
+
name: any;
|
|
44
|
+
source: "library" | "project" | "builtin";
|
|
45
|
+
ok: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
protocolVersion: number;
|
|
48
|
+
agent?: {
|
|
49
|
+
name?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
};
|
|
53
|
+
loadSession: boolean;
|
|
54
|
+
mcpHttp: boolean;
|
|
55
|
+
authMethods: string[];
|
|
56
|
+
models?: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
}>;
|
|
61
|
+
currentModel?: string;
|
|
62
|
+
modes?: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
}>;
|
|
67
|
+
currentMode?: string;
|
|
68
|
+
kind: any;
|
|
69
|
+
name: any;
|
|
70
|
+
source: "library" | "project" | "builtin";
|
|
71
|
+
ok: boolean;
|
|
72
|
+
} | {
|
|
73
|
+
version: string;
|
|
74
|
+
agent: {
|
|
75
|
+
name: string;
|
|
76
|
+
title: string;
|
|
77
|
+
version: string;
|
|
78
|
+
};
|
|
79
|
+
models: {
|
|
80
|
+
id: any;
|
|
81
|
+
label: any;
|
|
82
|
+
}[];
|
|
83
|
+
currentModel: any;
|
|
84
|
+
modes: {
|
|
85
|
+
id: string;
|
|
86
|
+
label: string;
|
|
87
|
+
description: string;
|
|
88
|
+
}[];
|
|
89
|
+
currentMode: string;
|
|
90
|
+
kind: any;
|
|
91
|
+
name: any;
|
|
92
|
+
source: "library" | "project" | "builtin";
|
|
93
|
+
ok: boolean;
|
|
94
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// packages/client/src/library-test.js
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AlpError } from "@anhlp/alp-core/errors";
|
|
4
|
+
import { getEntry } from "@anhlp/alp-core/library-edit";
|
|
5
|
+
import { runHook, samplePayload } from "@anhlp/alp-core/hook-run";
|
|
6
|
+
import { normalizeMcp } from "@anhlp/alp-core/validation";
|
|
7
|
+
import { probeMcp } from "./mcp-probe.js";
|
|
8
|
+
import { probeAcp } from "./acp-probe.js";
|
|
9
|
+
import { probeAntigravity } from "./antigravity-probe.js";
|
|
10
|
+
import { loadProvider } from "@anhlp/alp-core/providers";
|
|
11
|
+
async function testEntry(kind, name, { root, library, templates, scope, timeoutMs } = {}) {
|
|
12
|
+
const entry = await getEntry(kind, name, { root, library, templates, scope });
|
|
13
|
+
const home = entry.source === "project" ? path.join(path.resolve(root), ".alp") : library;
|
|
14
|
+
if (kind === "mcp") {
|
|
15
|
+
const server = normalizeMcp({ mcpServers: { [name]: entry.content.server } }, path.join(home, "mcp"), `${name}.json`).mcpServers[name];
|
|
16
|
+
const result = await probeMcp(server, { cwd: root ? path.resolve(root) : library, ...timeoutMs ? { timeoutMs } : {} });
|
|
17
|
+
return { kind, name, source: entry.source, ok: true, ...result };
|
|
18
|
+
}
|
|
19
|
+
if (kind === "hooks") {
|
|
20
|
+
const hook = entry.content.hook;
|
|
21
|
+
const project = root ? path.resolve(root) : void 0;
|
|
22
|
+
const payload = samplePayload(hook.event, { project, agent: hook.match?.agent ?? "main" });
|
|
23
|
+
const env = { ALP_EVENT: hook.event, ALP_SESSION: payload.session, ALP_AGENT: payload.agent, ALP_PROJECT: project ?? "", ALP_TASK: payload.task ?? "" };
|
|
24
|
+
const result = await runHook(hook, payload, { cwd: project ?? library, env, ...timeoutMs ? { timeoutMs } : {} });
|
|
25
|
+
const ok = result.exitCode === 0 && !result.timedOut;
|
|
26
|
+
return { kind, name, source: entry.source, ok, ...hook.blocking && !ok ? { wouldBlock: true } : {}, payload, ...result };
|
|
27
|
+
}
|
|
28
|
+
if (kind === "providers") {
|
|
29
|
+
const provider = await loadProvider(library, name);
|
|
30
|
+
const result = provider.kind === "antigravity" ? await probeAntigravity(provider, { cwd: root ? path.resolve(root) : library, ...timeoutMs ? { timeoutMs } : {} }) : await probeAcp(provider, { cwd: root ? path.resolve(root) : library, ...timeoutMs ? { timeoutMs } : {} });
|
|
31
|
+
return { kind, name, source: entry.source, ok: true, ...result };
|
|
32
|
+
}
|
|
33
|
+
throw new AlpError("INVALID_KIND", `Only mcp servers, hooks and providers can be tested, not ${kind}`);
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
testEntry
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts an MCP server once and lists its tools (ALPD §43), for the Test button and
|
|
3
|
+
* `alp mcp test`. `server` is a normalized definition: { command, args, env, cwd } for
|
|
4
|
+
* stdio, or { url, headers } for HTTP. The server is stopped afterwards.
|
|
5
|
+
* @returns {Promise<{ server?: { name?: string, version?: string }, tools: Array<{ name: string, description?: string }> }>}
|
|
6
|
+
*/
|
|
7
|
+
export function probeMcp(server: any, { cwd, timeoutMs }?: {
|
|
8
|
+
timeoutMs?: number | undefined;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
server?: {
|
|
11
|
+
name?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
};
|
|
14
|
+
tools: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
/** A JSON-RPC session with a process over stdio; ACP providers are probed with it too. */
|
|
20
|
+
export function stdioSession(server: any, cwd: any, timeoutMs: any): {
|
|
21
|
+
request(method: any, params: any): Promise<any>;
|
|
22
|
+
notify(method: any): Promise<void>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// packages/client/src/mcp-probe.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { launchCommand } from "@anhlp/alp-core/providers";
|
|
5
|
+
var PROTOCOL = "2025-06-18";
|
|
6
|
+
var client = { name: "alp", version: "1" };
|
|
7
|
+
async function probeMcp(server, { cwd, timeoutMs = 15e3 } = {}) {
|
|
8
|
+
const session = server.url ? await httpSession(server, timeoutMs) : stdioSession(server, cwd, timeoutMs);
|
|
9
|
+
try {
|
|
10
|
+
const init = await session.request("initialize", { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: client });
|
|
11
|
+
await session.notify("notifications/initialized");
|
|
12
|
+
const tools = [];
|
|
13
|
+
let cursor;
|
|
14
|
+
for (let page = 0; page < 20; page++) {
|
|
15
|
+
const result = await session.request("tools/list", cursor ? { cursor } : {});
|
|
16
|
+
for (const tool of result.tools ?? []) tools.push({ name: tool.name, ...tool.description ? { description: tool.description } : {} });
|
|
17
|
+
cursor = result.nextCursor;
|
|
18
|
+
if (!cursor) break;
|
|
19
|
+
}
|
|
20
|
+
return { ...init.serverInfo ? { server: { name: init.serverInfo.name, version: init.serverInfo.version } } : {}, tools };
|
|
21
|
+
} finally {
|
|
22
|
+
await session.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function stdioSession(server, cwd, timeoutMs) {
|
|
26
|
+
const env = { ...process.env, ...server.env ?? {} };
|
|
27
|
+
const launch = launchCommand(server.command, server.args ?? [], env);
|
|
28
|
+
const child = spawn(launch.command, launch.args, { cwd: server.cwd ?? cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
29
|
+
const signal = (name) => {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-child.pid, name);
|
|
32
|
+
} catch {
|
|
33
|
+
child.kill(name);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
let stderr = "";
|
|
37
|
+
child.stderr.setEncoding("utf8");
|
|
38
|
+
child.stderr.on("data", (chunk) => {
|
|
39
|
+
if (stderr.length < 4e3) stderr += chunk;
|
|
40
|
+
});
|
|
41
|
+
const pending = /* @__PURE__ */ new Map();
|
|
42
|
+
let exited;
|
|
43
|
+
const failAll = (error) => {
|
|
44
|
+
for (const { reject } of pending.values()) reject(error);
|
|
45
|
+
pending.clear();
|
|
46
|
+
};
|
|
47
|
+
child.on("error", (error) => {
|
|
48
|
+
exited = error;
|
|
49
|
+
failAll(error);
|
|
50
|
+
});
|
|
51
|
+
child.on("exit", (code) => {
|
|
52
|
+
exited = new Error(`${server.command} exited (${code ?? "signal"})${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : ""}`);
|
|
53
|
+
failAll(exited);
|
|
54
|
+
});
|
|
55
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
56
|
+
let message;
|
|
57
|
+
try {
|
|
58
|
+
message = JSON.parse(line);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const waiting = pending.get(message.id);
|
|
63
|
+
if (!waiting) return;
|
|
64
|
+
pending.delete(message.id);
|
|
65
|
+
if (message.error) waiting.reject(new Error(`${message.error.message ?? "MCP error"} (${message.error.code})`));
|
|
66
|
+
else waiting.resolve(message.result ?? {});
|
|
67
|
+
});
|
|
68
|
+
child.stdin.on("error", () => {
|
|
69
|
+
});
|
|
70
|
+
let id = 0;
|
|
71
|
+
return {
|
|
72
|
+
request(method, params) {
|
|
73
|
+
if (exited) return Promise.reject(exited);
|
|
74
|
+
const current = ++id;
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
pending.delete(current);
|
|
78
|
+
reject(new Error(`${method} got no answer within ${timeoutMs / 1e3} s`));
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
pending.set(current, { resolve: (value) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolve(value);
|
|
83
|
+
}, reject: (error) => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
reject(error);
|
|
86
|
+
} });
|
|
87
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: current, method, params }) + "\n");
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async notify(method) {
|
|
91
|
+
if (!exited) child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method }) + "\n");
|
|
92
|
+
},
|
|
93
|
+
async close() {
|
|
94
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
95
|
+
const done = new Promise((resolve) => child.once("exit", resolve));
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
signal("SIGTERM");
|
|
98
|
+
const timer = setTimeout(() => signal("SIGKILL"), 2e3);
|
|
99
|
+
await done;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function httpSession(server, timeoutMs) {
|
|
105
|
+
let sessionId;
|
|
106
|
+
let id = 0;
|
|
107
|
+
const post = async (body) => {
|
|
108
|
+
const response = await fetch(server.url, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { ...server.headers ?? {}, "content-type": "application/json", accept: "application/json, text/event-stream", ...sessionId ? { "mcp-session-id": sessionId } : {}, "mcp-protocol-version": PROTOCOL },
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
113
|
+
});
|
|
114
|
+
sessionId = response.headers.get("mcp-session-id") ?? sessionId;
|
|
115
|
+
if (!response.ok) throw new Error(`${server.url} answered ${response.status} ${response.statusText}`);
|
|
116
|
+
return response;
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
async request(method, params) {
|
|
120
|
+
const current = ++id;
|
|
121
|
+
const response = await post({ jsonrpc: "2.0", id: current, method, params });
|
|
122
|
+
const text = await response.text();
|
|
123
|
+
const messages = (response.headers.get("content-type") ?? "").includes("text/event-stream") ? text.split("\n").filter((line) => line.startsWith("data:")).map((line) => {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(line.slice(5));
|
|
126
|
+
} catch {
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
}) : [JSON.parse(text)];
|
|
130
|
+
const message = messages.find((entry) => entry?.id === current);
|
|
131
|
+
if (!message) throw new Error(`${method}: no answer in the response`);
|
|
132
|
+
if (message.error) throw new Error(`${message.error.message ?? "MCP error"} (${message.error.code})`);
|
|
133
|
+
return message.result ?? {};
|
|
134
|
+
},
|
|
135
|
+
async notify(method) {
|
|
136
|
+
await post({ jsonrpc: "2.0", method });
|
|
137
|
+
},
|
|
138
|
+
async close() {
|
|
139
|
+
if (sessionId) await fetch(server.url, { method: "DELETE", headers: { ...server.headers ?? {}, "mcp-session-id": sessionId }, signal: AbortSignal.timeout(timeoutMs) }).catch(() => {
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
probeMcp,
|
|
146
|
+
stdioSession
|
|
147
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { hello } from "./rpc/handshake.js";
|
|
2
|
+
export type AlpTransport = import("./rpc/client.js").AlpTransport;
|
|
3
|
+
export type TransportClose = import("./rpc/client.js").TransportClose;
|
|
4
|
+
export type TransportState = import("./rpc/client.js").TransportState;
|
|
5
|
+
export type AbortSignalLike = import("./rpc/client.js").AbortSignalLike;
|
|
6
|
+
export type RequestOptions = import("./rpc/client.js").RequestOptions;
|
|
7
|
+
export type AlpRpcClientOptions = import("./rpc/client.js").AlpRpcClientOptions;
|
|
8
|
+
export type ClientInfo = import("./rpc/handshake.js").ClientInfo;
|
|
9
|
+
export type ReconnectOptions = import("./rpc/reconnect.js").ReconnectOptions;
|
|
10
|
+
export type ReconnectingClient = import("./rpc/reconnect.js").ReconnectingClient;
|
|
11
|
+
export type ReconnectState = import("./rpc/reconnect.js").ReconnectState;
|
|
12
|
+
export type Backoff = import("./rpc/reconnect.js").Backoff;
|
|
13
|
+
export { PROTOCOL_VERSION, MAX_FRAME, AlpRpcError, ERROR } from "@anhlp/alp-protocol";
|
|
14
|
+
export { AlpRpcClient, DEFAULT_MAX_PENDING } from "./rpc/client.js";
|
|
15
|
+
export { AlpClientError, CLIENT_ERROR, isAlpClientError } from "./rpc/errors.js";
|
|
16
|
+
export { createLineDecoder, utf8Length } from "./rpc/framing.js";
|
|
17
|
+
export { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
|
|
18
|
+
export { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
|
package/dist/portable.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// packages/client/src/portable.js
|
|
2
|
+
import { PROTOCOL_VERSION, MAX_FRAME, AlpRpcError, ERROR } from "@anhlp/alp-protocol";
|
|
3
|
+
import { AlpRpcClient, DEFAULT_MAX_PENDING } from "./rpc/client.js";
|
|
4
|
+
import { AlpClientError, CLIENT_ERROR, isAlpClientError } from "./rpc/errors.js";
|
|
5
|
+
import { createLineDecoder, utf8Length } from "./rpc/framing.js";
|
|
6
|
+
import { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
|
|
7
|
+
import { hello } from "./rpc/handshake.js";
|
|
8
|
+
import { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
|
|
9
|
+
export {
|
|
10
|
+
AlpClientError,
|
|
11
|
+
AlpRpcClient,
|
|
12
|
+
AlpRpcError,
|
|
13
|
+
CLIENT_ERROR,
|
|
14
|
+
DEFAULT_MAX_PENDING,
|
|
15
|
+
ERROR,
|
|
16
|
+
MAX_FRAME,
|
|
17
|
+
MAX_TIMER_MS,
|
|
18
|
+
PROTOCOL_VERSION,
|
|
19
|
+
backoffDelay,
|
|
20
|
+
createLineDecoder,
|
|
21
|
+
createReconnectingClient,
|
|
22
|
+
hello,
|
|
23
|
+
isAlpClientError,
|
|
24
|
+
retryable,
|
|
25
|
+
timerDelay,
|
|
26
|
+
utf8Length
|
|
27
|
+
};
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A select box in the terminal: ↑/↓ (or k/j) move, Enter picks, Ctrl-C or Esc cancels.
|
|
3
|
+
* Choices are { label, value, hint? }; `initial` is the value highlighted first.
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {{ message: string, choices: Array<{ label: string, value: T, hint?: string }>, initial?: T }} question
|
|
6
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
7
|
+
* @returns {Promise<T>}
|
|
8
|
+
*/
|
|
9
|
+
export function select<T>({ message, choices, initial }: {
|
|
10
|
+
message: string;
|
|
11
|
+
choices: Array<{
|
|
12
|
+
label: string;
|
|
13
|
+
value: T;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}>;
|
|
16
|
+
initial?: T;
|
|
17
|
+
}, { input, output }?: {
|
|
18
|
+
input?: NodeJS.ReadStream;
|
|
19
|
+
output?: NodeJS.WriteStream;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A line of text, for a value the list does not offer.
|
|
23
|
+
* @param {string} message
|
|
24
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
25
|
+
*/
|
|
26
|
+
export function text(message: string, { input, output }?: {
|
|
27
|
+
input?: NodeJS.ReadStream;
|
|
28
|
+
output?: NodeJS.WriteStream;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
export class Cancelled extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// packages/client/src/prompt.js
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
var dim = (text2) => `\x1B[2m${text2}\x1B[22m`;
|
|
4
|
+
var cyan = (text2) => `\x1B[36m${text2}\x1B[39m`;
|
|
5
|
+
var Cancelled = class extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Setup cancelled.");
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function select({ message, choices, initial }, { input = process.stdin, output = process.stdout } = {}) {
|
|
11
|
+
let index = Math.max(0, choices.findIndex((choice) => choice.value === initial));
|
|
12
|
+
let drawn = 0;
|
|
13
|
+
const draw = () => {
|
|
14
|
+
if (drawn) output.write(`\x1B[${drawn}A\x1B[0J`);
|
|
15
|
+
const lines = [`${cyan("?")} ${message} ${dim("(\u2191/\u2193, Enter)")}`, ...choices.map((choice, at) => at === index ? cyan(`\u276F ${choice.label}`) + (choice.hint ? ` ${dim(choice.hint)}` : "") : ` ${choice.label}${choice.hint ? ` ${dim(choice.hint)}` : ""}`)];
|
|
16
|
+
output.write(lines.join("\n") + "\n");
|
|
17
|
+
drawn = lines.length;
|
|
18
|
+
};
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
emitKeypressEvents(input);
|
|
21
|
+
const raw = input.isRaw;
|
|
22
|
+
input.setRawMode?.(true);
|
|
23
|
+
input.resume();
|
|
24
|
+
output.write("\x1B[?25l");
|
|
25
|
+
const done = (settle, value) => {
|
|
26
|
+
input.off("keypress", onKey);
|
|
27
|
+
input.setRawMode?.(raw ?? false);
|
|
28
|
+
input.pause();
|
|
29
|
+
output.write(`\x1B[${drawn}A\x1B[0J\x1B[?25h`);
|
|
30
|
+
if (settle === resolve) output.write(`${cyan("\u2714")} ${message} ${cyan(choices[index].label)}
|
|
31
|
+
`);
|
|
32
|
+
settle(value);
|
|
33
|
+
};
|
|
34
|
+
const onKey = (text2, key = {}) => {
|
|
35
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") return done(reject, new Cancelled());
|
|
36
|
+
if (key.name === "return" || key.name === "enter") return done(resolve, choices[index].value);
|
|
37
|
+
if (key.name === "up" || key.name === "k") index = (index + choices.length - 1) % choices.length;
|
|
38
|
+
else if (key.name === "down" || key.name === "j") index = (index + 1) % choices.length;
|
|
39
|
+
else return;
|
|
40
|
+
draw();
|
|
41
|
+
};
|
|
42
|
+
input.on("keypress", onKey);
|
|
43
|
+
draw();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function text(message, { input = process.stdin, output = process.stdout } = {}) {
|
|
47
|
+
const { createInterface } = await import("node:readline/promises");
|
|
48
|
+
const rl = createInterface({ input, output });
|
|
49
|
+
try {
|
|
50
|
+
return (await rl.question(`${cyan("?")} ${message} `)).trim();
|
|
51
|
+
} finally {
|
|
52
|
+
rl.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
Cancelled,
|
|
57
|
+
select,
|
|
58
|
+
text
|
|
59
|
+
};
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** `alp ps`: sessions as a tree, one line each. */
|
|
2
|
+
export function renderPs(sessions: any): any[];
|
|
3
|
+
/** `alp log`: one line per run-log entry, with its time. */
|
|
4
|
+
export function renderLog(rootId: any, entries: any): string[];
|
|
5
|
+
export function ago(since: any, now?: number): string;
|
|
6
|
+
export function duration(ms: any): string;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// packages/client/src/render.js
|
|
2
|
+
var TASK_PAST = { create: "created", update: "updated", link: "linked", start: "started", close: "closed", reopen: "reopened", delegate: "delegated", submit: "submitted", release: "released", gate: "gated", clear: "cleared a gate of", orphaned: "reopened (its assignment ended with alpd)" };
|
|
3
|
+
var ago = (since, now = Date.now()) => {
|
|
4
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(since)) / 1e3));
|
|
5
|
+
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s` : `${Math.floor(seconds / 3600)}h${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
6
|
+
};
|
|
7
|
+
var duration = (ms) => ago((/* @__PURE__ */ new Date(0)).toISOString(), ms);
|
|
8
|
+
var tokens = (count) => typeof count !== "number" ? "?" : count >= 1e6 && count % 1e6 < 5e4 ? `${Math.round(count / 1e6)}M` : `${Math.round(count / 1e3)}k`;
|
|
9
|
+
function renderPs(sessions) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
const children = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const session of sessions) {
|
|
13
|
+
const key = session.parentId ?? "";
|
|
14
|
+
children.set(key, [...children.get(key) ?? [], session]);
|
|
15
|
+
}
|
|
16
|
+
const print = (session, depth) => {
|
|
17
|
+
const status = session.parked ? `parked (${session.parked})` : session.status === "running" || session.status === "idle" ? session.activeTurnId ? "running" : session.busy ? "waiting" : "idle" : session.lastError?.code ?? session.status;
|
|
18
|
+
lines.push(`${" ".repeat(depth)}${session.id} ${session.agent} ${session.runtime}:${session.model} ${session.mode} ${status}${depth ? "" : ` ${session.projectRoot}${session.title ? ` "${session.title}"` : ""}`}`);
|
|
19
|
+
for (const child of children.get(session.id) ?? []) print(child, depth + 1);
|
|
20
|
+
};
|
|
21
|
+
for (const root of children.get("") ?? []) print(root, 0);
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function renderLog(rootId, entries) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const agents = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const time = entry.ts.slice(11, 19);
|
|
29
|
+
let text;
|
|
30
|
+
switch (entry.event) {
|
|
31
|
+
case "assignment.started":
|
|
32
|
+
agents.set(entry.assignmentId, entry.agent);
|
|
33
|
+
text = `${entry.parentAgent} \u2192 ${entry.agent} (${entry.mode}${entry.isolation === "worktree" ? ", worktree" : ""}, ${entry.model}${entry.wait === false ? ", async" : ""}): ${entry.task.split("\n")[0].slice(0, 120)}`;
|
|
34
|
+
break;
|
|
35
|
+
case "assignment.finished":
|
|
36
|
+
text = `${entry.agent} ${entry.status} after ${duration(entry.durationMs ?? 0)}${entry.handoff ? `, handoff ${entry.handoff.outcome}${entry.handoff.verdict ? `, verdict ${entry.handoff.verdict.result.toUpperCase()}` : ""}: ${entry.handoff.summary.split("\n")[0].slice(0, 120)}` : ""}${entry.error ? `: ${entry.error}` : ""}${entry.reconciled ? " (after a restart)" : ""}`;
|
|
37
|
+
break;
|
|
38
|
+
case "mail":
|
|
39
|
+
if (entry.kind === "board") continue;
|
|
40
|
+
text = `\u2709 ${entry.kind} ${entry.from} \u2192 ${entry.to === rootId ? "main" : agents.get(entry.to) ?? entry.to}${entry.body ? `: ${entry.body.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
41
|
+
break;
|
|
42
|
+
case "board.pin":
|
|
43
|
+
text = `${entry.kind === "claim" ? "\u2691" : entry.kind === "decision" ? "\u25C6" : "\u2022"} ${entry.agent} pins ${entry.kind} ${entry.pinId}${entry.task ? ` for ${entry.task}` : ""}${entry.paths ? ` [${entry.paths.join(", ")}]` : ""}: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
44
|
+
break;
|
|
45
|
+
case "task":
|
|
46
|
+
text = `\u2610 ${entry.agent} ${TASK_PAST[entry.action] ?? entry.action} ${entry.id} "${entry.title}" \u2192 ${entry.status}${entry.detail ? `: ${entry.detail.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
47
|
+
break;
|
|
48
|
+
case "board.unpin":
|
|
49
|
+
text = `\u2690 ${entry.pinId} by ${entry.agent} ${entry.reason === "session_ended" ? "released when its session ended" : "taken down"}`;
|
|
50
|
+
break;
|
|
51
|
+
case "human.question":
|
|
52
|
+
text = `? ${entry.agent} asks the user [${entry.questionId}]: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
53
|
+
break;
|
|
54
|
+
case "verify":
|
|
55
|
+
text = `${entry.skipped ? "\u2013" : entry.passed ? "\u2713" : "\u2717"} verify in the ${entry.where}${entry.taskId ? ` for ${entry.taskId}` : ""}${entry.assignmentId ? ` (${entry.assignmentId})` : ""}: ${entry.skipped ? `skipped: ${entry.skipped}` : entry.detail}`;
|
|
56
|
+
break;
|
|
57
|
+
case "epic.landed":
|
|
58
|
+
text = `\u25C6 ${entry.agent} closed ${entry.id} "${entry.title}": ${entry.tasks} tasks, ${entry.reworked} reworked`;
|
|
59
|
+
break;
|
|
60
|
+
case "notice":
|
|
61
|
+
text = `${entry.level === "error" ? "\u203C" : entry.level === "warning" ? "!" : "\u2139"} ${entry.text}`;
|
|
62
|
+
break;
|
|
63
|
+
case "assignment.parked":
|
|
64
|
+
text = `\u23F8 ${entry.agent} ${entry.assignmentId} parked: ${entry.reason}`;
|
|
65
|
+
break;
|
|
66
|
+
case "assignment.resumed":
|
|
67
|
+
text = `\u25B6 ${entry.agent} ${entry.assignmentId} resumed`;
|
|
68
|
+
break;
|
|
69
|
+
case "instructions":
|
|
70
|
+
text = `# ${entry.agent} instructions ${entry.sha} (ALP.md ${entry.parts?.project}, AGENT.md ${entry.parts?.agent}, ${entry.chars} chars)`;
|
|
71
|
+
break;
|
|
72
|
+
case "context":
|
|
73
|
+
text = entry.level === "soon" ? `\u25D4 ${entry.agent} context ${tokens(entry.tokens)}, compacts at ${tokens(entry.compactAt)}: told it is near` : `\u25D4 ${entry.agent} context ${entry.percent}% full: ${entry.level === "now" ? "told to hand off now" : "told to plan a handoff"}`;
|
|
74
|
+
break;
|
|
75
|
+
case "skill":
|
|
76
|
+
text = Array.isArray(entry.roles) ? `\u2726 ${entry.agent} saved skill ${entry.name} for ${entry.roles.join(", ")}` : `\u2726 ${entry.agent} uses skill ${entry.skill}${entry.via === "skill" ? " (Skill tool)" : ""}`;
|
|
77
|
+
break;
|
|
78
|
+
case "compacted":
|
|
79
|
+
text = entry.failed ? `\u2717 ${entry.agent}'s context could not be compacted${entry.error ? `: ${entry.error}` : ""}` : `\u25D1 ${entry.agent}'s context compacted${entry.preTokens !== void 0 ? ` from ${tokens(entry.preTokens)}` : ""}${entry.postTokens !== void 0 ? ` to ${tokens(entry.postTokens)}` : ""}${entry.trigger === "manual" ? " on request" : ""}`;
|
|
80
|
+
break;
|
|
81
|
+
case "assignment.interrupted":
|
|
82
|
+
text = `\u23F9 ${entry.agent} ${entry.assignmentId} interrupted: ${entry.reason}; the next alpd continues it`;
|
|
83
|
+
break;
|
|
84
|
+
case "assignment.recovered":
|
|
85
|
+
text = `\u21BB ${entry.agent} ${entry.assignmentId} reopened after alpd restarted`;
|
|
86
|
+
break;
|
|
87
|
+
case "session.restarted":
|
|
88
|
+
text = `\u21BB ${entry.agent}'s native process stopped (${entry.error}); restart ${entry.restarts}`;
|
|
89
|
+
break;
|
|
90
|
+
case "session.revived":
|
|
91
|
+
text = `\u25B6 ${entry.agent} running again`;
|
|
92
|
+
break;
|
|
93
|
+
case "session.revive_failed":
|
|
94
|
+
text = `\u2717 ${entry.agent} could not be restarted: ${entry.error}`;
|
|
95
|
+
break;
|
|
96
|
+
case "human.answer":
|
|
97
|
+
text = `\u21B3 ${entry.questionId} ${entry.outcome}${entry.answer !== void 0 ? `: ${entry.answer.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
98
|
+
break;
|
|
99
|
+
case "hook":
|
|
100
|
+
text = entry.skipped ? `\u21AA ${entry.agent}'s ${entry.on} hook ${entry.hook} skipped: the workspace's hooks are not trusted` : `${entry.blocked ? "\u26D4" : entry.error || entry.exitCode !== 0 || entry.timedOut ? "\u2717" : "\u21AA"} ${entry.agent}'s ${entry.on} hook ${entry.hook} ${entry.error ? `could not run: ${entry.error}` : entry.timedOut ? "timed out" : `exit ${entry.exitCode}`}${entry.durationMs !== void 0 ? ` in ${duration(entry.durationMs)}` : ""}${entry.blocked ? ", refused it" : ""}${entry.output ? `: ${entry.output.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
101
|
+
break;
|
|
102
|
+
case "hook.trust":
|
|
103
|
+
text = `${entry.trusted ? "\u2713 the user trusts" : "\u2717 the user did not trust"} the hooks of ${entry.project}${entry.outcome ? ` (${entry.outcome})` : ""}`;
|
|
104
|
+
break;
|
|
105
|
+
case "runtime.slip":
|
|
106
|
+
text = `\u26A0 ALP could not handle ${entry.agent}'s ${entry.method}: ${entry.error}; the session went on`;
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
text = `${entry.event}${entry.branch ? ` ${entry.branch}` : ""}${entry.status ? ` ${entry.status}` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
lines.push(`${time} ${text}`);
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
ago,
|
|
117
|
+
duration,
|
|
118
|
+
renderLog,
|
|
119
|
+
renderPs
|
|
120
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Requests waiting for an answer at once; beyond it a request fails as `overloaded` and is not sent. */
|
|
2
|
+
export const DEFAULT_MAX_PENDING: 4096;
|
|
3
|
+
export class AlpRpcClient {
|
|
4
|
+
/**
|
|
5
|
+
* @param {AlpTransport} transport an open transport; the client owns it from now on
|
|
6
|
+
* @param {AlpRpcClientOptions} [options]
|
|
7
|
+
*/
|
|
8
|
+
constructor(transport: AlpTransport, { maxPending, maxFrame, timeoutMs, onListenerError }?: AlpRpcClientOptions);
|
|
9
|
+
/** True once the connection is gone, for whatever reason. */
|
|
10
|
+
get closed(): boolean;
|
|
11
|
+
/** Requests sent and still waiting for an answer. */
|
|
12
|
+
get pendingCount(): number;
|
|
13
|
+
/**
|
|
14
|
+
* Sends one request and resolves with its result, or rejects with AlpRpcError (the daemon
|
|
15
|
+
* answered with an error) or AlpClientError (the client gave up: see CLIENT_ERROR). Requests are
|
|
16
|
+
* never resent, also not after a reconnect.
|
|
17
|
+
* @template [T=any]
|
|
18
|
+
* @param {string} method
|
|
19
|
+
* @param {unknown} [params]
|
|
20
|
+
* @param {RequestOptions} [options]
|
|
21
|
+
* @returns {Promise<T>}
|
|
22
|
+
*/
|
|
23
|
+
request<T = any>(method: string, params?: unknown, { signal, timeoutMs }?: RequestOptions): Promise<T>;
|
|
24
|
+
/** Every `event` notification: an envelope of a session the connection attached to. Returns a remover. @param {(envelope: any) => void} listener */
|
|
25
|
+
onEvent(listener: (envelope: any) => void): () => boolean;
|
|
26
|
+
/** Every notification from the daemon, `event` included; for methods this client does not know yet. @param {(method: string, params: any) => void} listener */
|
|
27
|
+
onNotification(listener: (method: string, params: any) => void): () => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Answers requests the daemon sends this client (the extension RPC of ALPD §61). The first
|
|
30
|
+
* handler answers; its result, or its error's numeric `code`, `message` and `data`, goes back.
|
|
31
|
+
* @param {(method: string, params: any) => unknown} handler
|
|
32
|
+
*/
|
|
33
|
+
onRequest(handler: (method: string, params: any) => unknown): () => boolean;
|
|
34
|
+
/** Called once when the connection ends, with the error pending requests were rejected with. @param {(error: Error) => void} listener */
|
|
35
|
+
onClose(listener: (error: Error) => void): () => boolean;
|
|
36
|
+
/** Ends the connection; waiting requests reject with reason `closed`. Idempotent. */
|
|
37
|
+
close(): void;
|
|
38
|
+
#private;
|
|
39
|
+
}
|
|
40
|
+
export type TransportState = "open" | "closed";
|
|
41
|
+
/**
|
|
42
|
+
* Why a transport closed: `error` when it failed, `code`/`reason` when the peer said so.
|
|
43
|
+
*/
|
|
44
|
+
export type TransportClose = {
|
|
45
|
+
error?: Error;
|
|
46
|
+
code?: number;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Contract (docs/alp/client-sdk.md): `send` delivers or throws, synchronously, and a throw means
|
|
51
|
+
* the frame did not leave; `onMessage` delivers frames in arrival order; `onClose` fires at most
|
|
52
|
+
* once, also after `close()`; nothing is delivered after it; `close` is idempotent.
|
|
53
|
+
*/
|
|
54
|
+
export type AlpTransport = {
|
|
55
|
+
readonly state: TransportState;
|
|
56
|
+
send(frame: string): void;
|
|
57
|
+
onMessage(listener: (frame: string) => void): () => void;
|
|
58
|
+
onClose(listener: (close: TransportClose) => void): () => void;
|
|
59
|
+
close(): void;
|
|
60
|
+
};
|
|
61
|
+
export type AbortSignalLike = {
|
|
62
|
+
readonly aborted: boolean;
|
|
63
|
+
readonly reason?: unknown;
|
|
64
|
+
addEventListener(type: "abort", listener: () => void, options?: {
|
|
65
|
+
once?: boolean;
|
|
66
|
+
}): void;
|
|
67
|
+
removeEventListener(type: "abort", listener: () => void): void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Both end only this client's wait; neither tells the daemon to stop. A timeout of Infinity, or
|
|
71
|
+
* longer than a timer can wait (2^31-1 ms), means none.
|
|
72
|
+
*/
|
|
73
|
+
export type RequestOptions = {
|
|
74
|
+
signal?: AbortSignalLike;
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
};
|
|
77
|
+
export type AlpRpcClientOptions = {
|
|
78
|
+
maxPending?: number;
|
|
79
|
+
maxFrame?: number;
|
|
80
|
+
timeoutMs?: number;
|
|
81
|
+
onListenerError?: (error: unknown) => void;
|
|
82
|
+
};
|