@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,81 @@
|
|
|
1
|
+
// packages/client/src/supervise.js
|
|
2
|
+
import { mkdir, unlink, writeFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDaemon, lockAlive, readLock } from "./index.js";
|
|
5
|
+
import { installedProgram, serviceFor, startService } from "./service.js";
|
|
6
|
+
var holdFile = (home) => path.join(home, "state", "alpd.held");
|
|
7
|
+
async function holdDaemon(home, by = "alp daemon stop") {
|
|
8
|
+
await mkdir(path.dirname(holdFile(home)), { recursive: true, mode: 448 });
|
|
9
|
+
await writeFile(holdFile(home), JSON.stringify({ by, at: (/* @__PURE__ */ new Date()).toISOString() }) + "\n", { mode: 384 });
|
|
10
|
+
}
|
|
11
|
+
async function releaseHold(home) {
|
|
12
|
+
await unlink(holdFile(home)).catch(() => {
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function daemonHeld(home) {
|
|
16
|
+
return stat(holdFile(home)).then(() => true, () => false);
|
|
17
|
+
}
|
|
18
|
+
async function whenReady(home, timeoutMs) {
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
const lock = await readLock(home);
|
|
22
|
+
if (lockAlive(lock) && lock.ready) return lock.socket;
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`alpd did not become ready within ${timeoutMs / 1e3} s; see ${path.join(home, "logs")}`);
|
|
26
|
+
}
|
|
27
|
+
async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3 }) {
|
|
28
|
+
await releaseHold(home);
|
|
29
|
+
const lock = await readLock(home);
|
|
30
|
+
if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
|
|
31
|
+
const service = serviceFor({ home, env });
|
|
32
|
+
if (service && await installedProgram(service)) {
|
|
33
|
+
await startService(service);
|
|
34
|
+
return whenReady(home, timeoutMs);
|
|
35
|
+
}
|
|
36
|
+
return ensureDaemon({ home, entry, env, timeoutMs });
|
|
37
|
+
}
|
|
38
|
+
function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
|
|
39
|
+
} }) {
|
|
40
|
+
let stopped = false;
|
|
41
|
+
let missed = 0;
|
|
42
|
+
let starting;
|
|
43
|
+
const start = (reason) => {
|
|
44
|
+
starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env }).then(() => log(`alpd ${reason}`), (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
|
|
45
|
+
starting = void 0;
|
|
46
|
+
missed = 0;
|
|
47
|
+
});
|
|
48
|
+
return starting;
|
|
49
|
+
};
|
|
50
|
+
const check = async () => {
|
|
51
|
+
if (stopped || starting) return;
|
|
52
|
+
if (lockAlive(await readLock(home))) {
|
|
53
|
+
missed = 0;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (await daemonHeld(home)) {
|
|
57
|
+
missed = 0;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (++missed >= misses && !stopped) await start("was down; started it again");
|
|
61
|
+
};
|
|
62
|
+
const initial = start("started with Paseo");
|
|
63
|
+
const timer = setInterval(() => {
|
|
64
|
+
void check();
|
|
65
|
+
}, intervalMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
const stop = () => {
|
|
68
|
+
stopped = true;
|
|
69
|
+
clearInterval(timer);
|
|
70
|
+
return starting ?? Promise.resolve();
|
|
71
|
+
};
|
|
72
|
+
stop.started = initial;
|
|
73
|
+
return stop;
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
daemonHeld,
|
|
77
|
+
holdDaemon,
|
|
78
|
+
releaseHold,
|
|
79
|
+
startDaemon,
|
|
80
|
+
superviseDaemon
|
|
81
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A URL fit for an error message: no credentials, query or fragment, which may carry a token. @param {string} url */
|
|
2
|
+
export function redactUrl(url: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Wraps an open WebSocket in an ALP transport. The transport owns the socket from then on.
|
|
5
|
+
* @param {WebSocketLike} socket
|
|
6
|
+
* @param {WebSocketTransportOptions} [options]
|
|
7
|
+
* @returns {AlpTransport}
|
|
8
|
+
*/
|
|
9
|
+
export function createWebSocketTransport(socket: WebSocketLike, { maxFrame, maxBufferedBytes }?: WebSocketTransportOptions): AlpTransport;
|
|
10
|
+
/**
|
|
11
|
+
* Opens a WebSocket to `url` and resolves with its transport once it is open. The socket comes
|
|
12
|
+
* from `createSocket`, else `new WebSocket(url, protocols)` with the injected or global WebSocket.
|
|
13
|
+
* Errors name the URL without its credentials, query or fragment.
|
|
14
|
+
* @param {string} url
|
|
15
|
+
* @param {ConnectWebSocketOptions} [options]
|
|
16
|
+
* @returns {Promise<AlpTransport>}
|
|
17
|
+
*/
|
|
18
|
+
export function connectWebSocket(url: string, { WebSocket: Implementation, protocols, createSocket, signal, timeoutMs, ...options }?: ConnectWebSocketOptions): Promise<AlpTransport>;
|
|
19
|
+
/** Close codes this transport sends when the peer breaks the protocol. */
|
|
20
|
+
export const CLOSE_CODE: Readonly<{
|
|
21
|
+
unsupportedData: 4003;
|
|
22
|
+
tooLarge: 4009;
|
|
23
|
+
}>;
|
|
24
|
+
export type AlpTransport = import("./rpc/client.js").AlpTransport;
|
|
25
|
+
export type TransportClose = import("./rpc/client.js").TransportClose;
|
|
26
|
+
export type AbortSignalLike = import("./rpc/client.js").AbortSignalLike;
|
|
27
|
+
export type WebSocketLike = {
|
|
28
|
+
readonly readyState: number;
|
|
29
|
+
readonly bufferedAmount?: number;
|
|
30
|
+
binaryType?: string;
|
|
31
|
+
send(data: string): void;
|
|
32
|
+
close(code?: number, reason?: string): void;
|
|
33
|
+
addEventListener?(type: string, listener: (event: any) => void): void;
|
|
34
|
+
removeEventListener?(type: string, listener: (event: any) => void): void;
|
|
35
|
+
onopen?: ((event: any) => void) | null;
|
|
36
|
+
onmessage?: ((event: any) => void) | null;
|
|
37
|
+
onerror?: ((event: any) => void) | null;
|
|
38
|
+
onclose?: ((event: any) => void) | null;
|
|
39
|
+
};
|
|
40
|
+
export type WebSocketConstructor = new (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
41
|
+
export type WebSocketTransportOptions = {
|
|
42
|
+
maxFrame?: number;
|
|
43
|
+
maxBufferedBytes?: number;
|
|
44
|
+
};
|
|
45
|
+
export type ConnectWebSocketOptions = WebSocketTransportOptions & {
|
|
46
|
+
WebSocket?: WebSocketConstructor;
|
|
47
|
+
protocols?: string | string[];
|
|
48
|
+
createSocket?: (url: string, protocols?: string | string[]) => WebSocketLike;
|
|
49
|
+
signal?: AbortSignalLike;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// packages/client/src/websocket.js
|
|
2
|
+
import { MAX_FRAME } from "@anhlp/alp-protocol";
|
|
3
|
+
import { AlpClientError } from "./rpc/errors.js";
|
|
4
|
+
import { utf8Length } from "./rpc/framing.js";
|
|
5
|
+
import { timerDelay } from "./rpc/timers.js";
|
|
6
|
+
var CONNECTING = 0;
|
|
7
|
+
var OPEN = 1;
|
|
8
|
+
var CLOSE_CODE = Object.freeze({ unsupportedData: 4003, tooLarge: 4009 });
|
|
9
|
+
var CLOSE_AFTER_ERROR_MS = 250;
|
|
10
|
+
var MAX_CLOSE_REASON = 123;
|
|
11
|
+
function redactUrl(url) {
|
|
12
|
+
const match = /^([a-z][a-z0-9+.-]*:\/\/)(?:[^/?#\\]*@)?([^/?#\\@]*)([^?#]*)/i.exec(String(url));
|
|
13
|
+
return match ? `${match[1]}${match[2]}${match[3]}` : "<websocket>";
|
|
14
|
+
}
|
|
15
|
+
function scrubbed(error, url) {
|
|
16
|
+
const text = error instanceof Error ? error.message : String(error ?? "");
|
|
17
|
+
const message = (text.split(String(url)).join(redactUrl(url)) || "WebSocket error").replace(/[a-z][a-z0-9+.-]*:\/\/[^\s'"<>]+/gi, (found) => redactUrl(found));
|
|
18
|
+
const clean = new Error(message);
|
|
19
|
+
if (error instanceof Error && error.name) clean.name = error.name;
|
|
20
|
+
return clean;
|
|
21
|
+
}
|
|
22
|
+
function closeReason(reason) {
|
|
23
|
+
let cut = reason;
|
|
24
|
+
while (utf8Length(cut) > MAX_CLOSE_REASON) cut = cut.slice(0, -1);
|
|
25
|
+
return cut;
|
|
26
|
+
}
|
|
27
|
+
function release(socket, code, reason) {
|
|
28
|
+
if (socket.readyState !== CONNECTING && socket.readyState !== OPEN) return;
|
|
29
|
+
try {
|
|
30
|
+
if (code === void 0) socket.close();
|
|
31
|
+
else socket.close(code, closeReason(reason ?? ""));
|
|
32
|
+
return;
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
socket.close();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function absorbErrors(socket) {
|
|
41
|
+
if (typeof socket.addEventListener === "function") socket.addEventListener("error", () => {
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function listen(socket, type, listener) {
|
|
45
|
+
if (typeof socket.addEventListener === "function") {
|
|
46
|
+
socket.addEventListener(type, listener);
|
|
47
|
+
return () => socket.removeEventListener?.(type, listener);
|
|
48
|
+
}
|
|
49
|
+
const key = (
|
|
50
|
+
/** @type {'onopen' | 'onmessage' | 'onerror' | 'onclose'} */
|
|
51
|
+
`on${type}`
|
|
52
|
+
);
|
|
53
|
+
const previous = socket[key];
|
|
54
|
+
socket[key] = (event) => {
|
|
55
|
+
previous?.(event);
|
|
56
|
+
listener(event);
|
|
57
|
+
};
|
|
58
|
+
return () => {
|
|
59
|
+
socket[key] = previous ?? null;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var errorOf = (event) => {
|
|
63
|
+
const message = event?.error instanceof Error && event.error.message || typeof event?.message === "string" && event.message || "WebSocket error";
|
|
64
|
+
return event?.error instanceof Error && event.error.message ? event.error : new Error(message);
|
|
65
|
+
};
|
|
66
|
+
function createWebSocketTransport(socket, { maxFrame = MAX_FRAME, maxBufferedBytes = 4 * MAX_FRAME } = {}) {
|
|
67
|
+
if (socket.readyState !== OPEN) throw new AlpClientError("WebSocket is not open", { reason: "disconnected" });
|
|
68
|
+
absorbErrors(socket);
|
|
69
|
+
try {
|
|
70
|
+
socket.binaryType = "arraybuffer";
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
const messages = /* @__PURE__ */ new Set();
|
|
74
|
+
const closes = /* @__PURE__ */ new Set();
|
|
75
|
+
let ended;
|
|
76
|
+
const detach = [];
|
|
77
|
+
let failed;
|
|
78
|
+
let failTimer;
|
|
79
|
+
const end = (close) => {
|
|
80
|
+
if (ended) return;
|
|
81
|
+
ended = close;
|
|
82
|
+
if (failTimer !== void 0) clearTimeout(failTimer);
|
|
83
|
+
for (const remove of detach.splice(0)) remove();
|
|
84
|
+
messages.clear();
|
|
85
|
+
const listeners = [...closes];
|
|
86
|
+
closes.clear();
|
|
87
|
+
for (const listener of listeners) listener(close);
|
|
88
|
+
};
|
|
89
|
+
const violate = (message, code) => {
|
|
90
|
+
const error = new AlpClientError(message, { reason: "protocol" });
|
|
91
|
+
release(socket, code, message);
|
|
92
|
+
end({ error });
|
|
93
|
+
};
|
|
94
|
+
detach.push(listen(socket, "message", (event) => {
|
|
95
|
+
if (ended) return;
|
|
96
|
+
const data = event?.data;
|
|
97
|
+
if (typeof data !== "string") return violate("Binary WebSocket messages are not ALP frames", CLOSE_CODE.unsupportedData);
|
|
98
|
+
if (utf8Length(data) > maxFrame) return violate(`ALP frame exceeds ${maxFrame} bytes`, CLOSE_CODE.tooLarge);
|
|
99
|
+
for (const line of data.includes("\n") ? data.split("\n") : [data]) {
|
|
100
|
+
if (ended) return;
|
|
101
|
+
if (line.trim()) for (const listener of [...messages]) listener(line);
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
detach.push(listen(socket, "close", (event) => end({ error: failed, code: typeof event?.code === "number" ? event.code : void 0, reason: typeof event?.reason === "string" && event.reason ? event.reason : void 0 })));
|
|
105
|
+
detach.push(listen(socket, "error", (event) => {
|
|
106
|
+
if (failed) return;
|
|
107
|
+
failed = errorOf(event);
|
|
108
|
+
failTimer = setTimeout(() => end({ error: failed }), CLOSE_AFTER_ERROR_MS);
|
|
109
|
+
release(socket);
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
get state() {
|
|
113
|
+
return ended || socket.readyState !== OPEN ? "closed" : "open";
|
|
114
|
+
},
|
|
115
|
+
send(frame) {
|
|
116
|
+
if (ended || socket.readyState !== OPEN) throw new AlpClientError("WebSocket is not open", { reason: "send_failed" });
|
|
117
|
+
if ((socket.bufferedAmount ?? 0) + utf8Length(frame) > maxBufferedBytes) throw new AlpClientError(`WebSocket send buffer is over ${maxBufferedBytes} bytes`, { reason: "send_failed" });
|
|
118
|
+
socket.send(frame);
|
|
119
|
+
},
|
|
120
|
+
onMessage(listener) {
|
|
121
|
+
if (ended) return () => false;
|
|
122
|
+
messages.add(listener);
|
|
123
|
+
return () => messages.delete(listener);
|
|
124
|
+
},
|
|
125
|
+
onClose(listener) {
|
|
126
|
+
if (ended) return () => false;
|
|
127
|
+
closes.add(listener);
|
|
128
|
+
return () => closes.delete(listener);
|
|
129
|
+
},
|
|
130
|
+
close() {
|
|
131
|
+
release(socket, 1e3, "client closed");
|
|
132
|
+
end({ code: 1e3, reason: "client closed" });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function connectWebSocket(url, { WebSocket: Implementation, protocols, createSocket, signal, timeoutMs = 15e3, ...options } = {}) {
|
|
137
|
+
const where = redactUrl(url);
|
|
138
|
+
let delay;
|
|
139
|
+
try {
|
|
140
|
+
delay = timerDelay(timeoutMs);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return Promise.reject(error);
|
|
143
|
+
}
|
|
144
|
+
if (signal?.aborted) return Promise.reject(new AlpClientError(`Connecting to ${where} was aborted`, { reason: "aborted", cause: signal.reason }));
|
|
145
|
+
const Constructor = Implementation ?? /** @type {any} */
|
|
146
|
+
globalThis.WebSocket;
|
|
147
|
+
if (!createSocket && typeof Constructor !== "function") return Promise.reject(new TypeError("No WebSocket implementation: pass options.WebSocket or options.createSocket"));
|
|
148
|
+
let socket;
|
|
149
|
+
try {
|
|
150
|
+
socket = createSocket ? createSocket(url, protocols) : protocols === void 0 ? new Constructor(url) : new Constructor(url, protocols);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return Promise.reject(new AlpClientError(`Could not connect to ${where}`, { reason: "disconnected", cause: scrubbed(error, url) }));
|
|
153
|
+
}
|
|
154
|
+
absorbErrors(socket);
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const detach = [];
|
|
157
|
+
let timer;
|
|
158
|
+
const settle = () => {
|
|
159
|
+
for (const remove of detach.splice(0)) remove();
|
|
160
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
161
|
+
signal?.removeEventListener("abort", onAbort);
|
|
162
|
+
};
|
|
163
|
+
const fail = (error) => {
|
|
164
|
+
settle();
|
|
165
|
+
release(socket);
|
|
166
|
+
reject(error);
|
|
167
|
+
};
|
|
168
|
+
const onAbort = () => fail(new AlpClientError(`Connecting to ${where} was aborted`, { reason: "aborted", cause: signal?.reason }));
|
|
169
|
+
const open = () => {
|
|
170
|
+
settle();
|
|
171
|
+
try {
|
|
172
|
+
resolve(createWebSocketTransport(socket, options));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
reject(error);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
if (socket.readyState === OPEN) return open();
|
|
178
|
+
detach.push(listen(socket, "open", open));
|
|
179
|
+
detach.push(listen(socket, "error", (event) => fail(new AlpClientError(`Could not connect to ${where}`, { reason: "disconnected", cause: scrubbed(errorOf(event), url) }))));
|
|
180
|
+
detach.push(listen(socket, "close", (event) => fail(new AlpClientError(`Could not connect to ${where}`, { reason: "disconnected", data: { code: event?.code, reason: event?.reason || void 0 } }))));
|
|
181
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
182
|
+
if (delay !== void 0) timer = setTimeout(() => fail(new AlpClientError(`Connecting to ${where} timed out after ${timeoutMs} ms`, { reason: "timeout" })), delay);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
CLOSE_CODE,
|
|
187
|
+
connectWebSocket,
|
|
188
|
+
createWebSocketTransport,
|
|
189
|
+
redactUrl
|
|
190
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anhlp/alp-client",
|
|
3
|
+
"version": "0.0.0-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "ALP client: portable JSON-RPC SDK, WebSocket and Node IPC transports, daemon discovery and setup",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"dist",
|
|
9
|
+
"NOTICE"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
"./acp-probe": {
|
|
13
|
+
"alp-source": {
|
|
14
|
+
"types": "./src/acp-probe.js",
|
|
15
|
+
"default": "./src/acp-probe.js"
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/acp-probe.d.ts",
|
|
18
|
+
"default": "./src/acp-probe.js"
|
|
19
|
+
},
|
|
20
|
+
"./antigravity-probe": {
|
|
21
|
+
"alp-source": {
|
|
22
|
+
"types": "./src/antigravity-probe.js",
|
|
23
|
+
"default": "./src/antigravity-probe.js"
|
|
24
|
+
},
|
|
25
|
+
"types": "./dist/antigravity-probe.d.ts",
|
|
26
|
+
"default": "./src/antigravity-probe.js"
|
|
27
|
+
},
|
|
28
|
+
"./command": {
|
|
29
|
+
"alp-source": {
|
|
30
|
+
"types": "./src/command.js",
|
|
31
|
+
"default": "./src/command.js"
|
|
32
|
+
},
|
|
33
|
+
"types": "./dist/command.d.ts",
|
|
34
|
+
"default": "./src/command.js"
|
|
35
|
+
},
|
|
36
|
+
"./doctor": {
|
|
37
|
+
"alp-source": {
|
|
38
|
+
"types": "./src/doctor.js",
|
|
39
|
+
"default": "./src/doctor.js"
|
|
40
|
+
},
|
|
41
|
+
"types": "./dist/doctor.d.ts",
|
|
42
|
+
"default": "./src/doctor.js"
|
|
43
|
+
},
|
|
44
|
+
"./index": {
|
|
45
|
+
"alp-source": {
|
|
46
|
+
"types": "./src/index.js",
|
|
47
|
+
"default": "./src/index.js"
|
|
48
|
+
},
|
|
49
|
+
"types": "./dist/index.d.ts",
|
|
50
|
+
"default": "./src/index.js"
|
|
51
|
+
},
|
|
52
|
+
".": {
|
|
53
|
+
"alp-source": {
|
|
54
|
+
"types": "./src/index.js",
|
|
55
|
+
"default": "./src/index.js"
|
|
56
|
+
},
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"default": "./src/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./ipc": {
|
|
61
|
+
"alp-source": {
|
|
62
|
+
"types": "./src/ipc.js",
|
|
63
|
+
"default": "./src/ipc.js"
|
|
64
|
+
},
|
|
65
|
+
"types": "./dist/ipc.d.ts",
|
|
66
|
+
"default": "./src/ipc.js"
|
|
67
|
+
},
|
|
68
|
+
"./library-test": {
|
|
69
|
+
"alp-source": {
|
|
70
|
+
"types": "./src/library-test.js",
|
|
71
|
+
"default": "./src/library-test.js"
|
|
72
|
+
},
|
|
73
|
+
"types": "./dist/library-test.d.ts",
|
|
74
|
+
"default": "./src/library-test.js"
|
|
75
|
+
},
|
|
76
|
+
"./mcp-probe": {
|
|
77
|
+
"alp-source": {
|
|
78
|
+
"types": "./src/mcp-probe.js",
|
|
79
|
+
"default": "./src/mcp-probe.js"
|
|
80
|
+
},
|
|
81
|
+
"types": "./dist/mcp-probe.d.ts",
|
|
82
|
+
"default": "./src/mcp-probe.js"
|
|
83
|
+
},
|
|
84
|
+
"./portable": {
|
|
85
|
+
"alp-source": {
|
|
86
|
+
"types": "./src/portable.js",
|
|
87
|
+
"default": "./src/portable.js"
|
|
88
|
+
},
|
|
89
|
+
"types": "./dist/portable.d.ts",
|
|
90
|
+
"default": "./src/portable.js"
|
|
91
|
+
},
|
|
92
|
+
"./prompt": {
|
|
93
|
+
"alp-source": {
|
|
94
|
+
"types": "./src/prompt.js",
|
|
95
|
+
"default": "./src/prompt.js"
|
|
96
|
+
},
|
|
97
|
+
"types": "./dist/prompt.d.ts",
|
|
98
|
+
"default": "./src/prompt.js"
|
|
99
|
+
},
|
|
100
|
+
"./render": {
|
|
101
|
+
"alp-source": {
|
|
102
|
+
"types": "./src/render.js",
|
|
103
|
+
"default": "./src/render.js"
|
|
104
|
+
},
|
|
105
|
+
"types": "./dist/render.d.ts",
|
|
106
|
+
"default": "./src/render.js"
|
|
107
|
+
},
|
|
108
|
+
"./service": {
|
|
109
|
+
"alp-source": {
|
|
110
|
+
"types": "./src/service.js",
|
|
111
|
+
"default": "./src/service.js"
|
|
112
|
+
},
|
|
113
|
+
"types": "./dist/service.d.ts",
|
|
114
|
+
"default": "./src/service.js"
|
|
115
|
+
},
|
|
116
|
+
"./setup": {
|
|
117
|
+
"alp-source": {
|
|
118
|
+
"types": "./src/setup.js",
|
|
119
|
+
"default": "./src/setup.js"
|
|
120
|
+
},
|
|
121
|
+
"types": "./dist/setup.d.ts",
|
|
122
|
+
"default": "./src/setup.js"
|
|
123
|
+
},
|
|
124
|
+
"./supervise": {
|
|
125
|
+
"alp-source": {
|
|
126
|
+
"types": "./src/supervise.js",
|
|
127
|
+
"default": "./src/supervise.js"
|
|
128
|
+
},
|
|
129
|
+
"types": "./dist/supervise.d.ts",
|
|
130
|
+
"default": "./src/supervise.js"
|
|
131
|
+
},
|
|
132
|
+
"./websocket": {
|
|
133
|
+
"alp-source": {
|
|
134
|
+
"types": "./src/websocket.js",
|
|
135
|
+
"default": "./src/websocket.js"
|
|
136
|
+
},
|
|
137
|
+
"types": "./dist/websocket.d.ts",
|
|
138
|
+
"default": "./src/websocket.js"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"engines": {
|
|
142
|
+
"node": ">=22.12"
|
|
143
|
+
},
|
|
144
|
+
"dependencies": {
|
|
145
|
+
"@anhlp/alp-core": "0.0.0-beta.1",
|
|
146
|
+
"@anhlp/alp-protocol": "0.0.0-beta.1"
|
|
147
|
+
},
|
|
148
|
+
"scripts": {
|
|
149
|
+
"build": "node ../../scripts/build-workspace.mjs client",
|
|
150
|
+
"test": "node ../../scripts/test-package.mjs client",
|
|
151
|
+
"prepack": "npm run build"
|
|
152
|
+
},
|
|
153
|
+
"publishConfig": {
|
|
154
|
+
"access": "public",
|
|
155
|
+
"registry": "https://registry.npmjs.org/",
|
|
156
|
+
"tag": "beta"
|
|
157
|
+
},
|
|
158
|
+
"license": "Apache-2.0",
|
|
159
|
+
"repository": {
|
|
160
|
+
"type": "git",
|
|
161
|
+
"url": "git+https://github.com/phucanh08/alp.git",
|
|
162
|
+
"directory": "packages/client"
|
|
163
|
+
},
|
|
164
|
+
"homepage": "https://github.com/phucanh08/alp/tree/main/packages/client#readme",
|
|
165
|
+
"bugs": {
|
|
166
|
+
"url": "https://github.com/phucanh08/alp/issues"
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/acp-probe.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { stdioSession } from './mcp-probe.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts an ACP provider once and runs initialize (ALPD §46), for `alp provider test`
|
|
5
|
+
* and the settings screen. It reports who the agent says it is and what it supports;
|
|
6
|
+
* a provider that needs a login usually says so here, in authMethods. With `discover`,
|
|
7
|
+
* it also opens a session in `cwd`, as Paseo does, to read the models and modes the agent
|
|
8
|
+
* offers (ALPD §65); an agent that offers none leaves them out.
|
|
9
|
+
* @param {{ command: string, args?: string[], env?: Record<string, string>, label?: string, id?: string }} provider
|
|
10
|
+
* @param {{ cwd?: string, timeoutMs?: number, discover?: boolean }} [options]
|
|
11
|
+
* @returns {Promise<{ protocolVersion: number, agent?: { name?: string, title?: string, version?: string }, loadSession: boolean, mcpHttp: boolean, authMethods: string[],
|
|
12
|
+
* models?: Array<{ id: string, label: string, description?: string }>, currentModel?: string, modes?: Array<{ id: string, label: string, description?: string }>, currentMode?: string }>}
|
|
13
|
+
*/
|
|
14
|
+
export async function probeAcp(provider, { cwd, timeoutMs = 20_000, discover = false } = {}) {
|
|
15
|
+
const session = stdioSession({ command: provider.command, args: provider.args ?? [], env: provider.env ?? {} }, cwd, timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
const result = await session.request('initialize', {
|
|
18
|
+
protocolVersion: 1,
|
|
19
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
20
|
+
clientInfo: { name: 'alp', title: 'ALP', version: '1' },
|
|
21
|
+
});
|
|
22
|
+
if (result.protocolVersion !== 1) throw new Error(`${provider.label ?? provider.id} speaks ACP version ${result.protocolVersion}; ALP speaks version 1`);
|
|
23
|
+
const info = result.agentInfo;
|
|
24
|
+
const offered = discover ? await session.request('session/new', { cwd, mcpServers: [] }) : undefined;
|
|
25
|
+
const choices = list => Array.isArray(list) ? list.filter(item => typeof (item?.modelId ?? item?.id) === 'string')
|
|
26
|
+
.map(item => ({ id: item.modelId ?? item.id, label: typeof item.name === 'string' && item.name ? item.name : item.modelId ?? item.id, ...(typeof item.description === 'string' && item.description ? { description: item.description } : {}) })) : undefined;
|
|
27
|
+
const config = offered?.configOptions?.find(option => option.category === 'model' && option.type === 'select');
|
|
28
|
+
const modeConfig = offered?.configOptions?.find(option => option.category === 'mode' && option.type === 'select');
|
|
29
|
+
const flatten = options => options?.flatMap(option => option.options ?? [option]).map(option => ({ ...option, id: option.value }));
|
|
30
|
+
const modelState = offered?.models ?? (config ? { availableModels: flatten(config.options), currentModelId: config.currentValue } : undefined);
|
|
31
|
+
const modeState = offered?.modes ?? (modeConfig ? { availableModes: flatten(modeConfig.options), currentModeId: modeConfig.currentValue } : undefined);
|
|
32
|
+
const models = choices(modelState?.availableModels);
|
|
33
|
+
const modes = choices(modeState?.availableModes);
|
|
34
|
+
return {
|
|
35
|
+
...(models ? { models, ...(typeof modelState.currentModelId === 'string' ? { currentModel: modelState.currentModelId } : {}) } : {}),
|
|
36
|
+
...(modes ? { modes, ...(typeof modeState.currentModeId === 'string' ? { currentMode: modeState.currentModeId } : {}) } : {}),
|
|
37
|
+
protocolVersion: result.protocolVersion,
|
|
38
|
+
...(info ? { agent: { ...(info.name ? { name: info.name } : {}), ...(info.title ? { title: info.title } : {}), ...(info.version ? { version: info.version } : {}) } } : {}),
|
|
39
|
+
loadSession: Boolean(result.agentCapabilities?.loadSession),
|
|
40
|
+
mcpHttp: Boolean(result.agentCapabilities?.mcpCapabilities?.http),
|
|
41
|
+
authMethods: (result.authMethods ?? []).map(method => method?.name ?? method?.id).filter(Boolean),
|
|
42
|
+
};
|
|
43
|
+
} finally {
|
|
44
|
+
await session.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ANTIGRAVITY_MODELS = [
|
|
4
|
+
{ id: 'gemini-3.8-flash-high', label: 'Gemini 3.8 Flash (High)' },
|
|
5
|
+
{ id: 'gemini-3.8-flash-medium', label: 'Gemini 3.8 Flash (Medium)' },
|
|
6
|
+
{ id: 'gemini-3.8-flash-low', label: 'Gemini 3.8 Flash (Low)' },
|
|
7
|
+
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
8
|
+
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
9
|
+
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 (Thinking)' },
|
|
10
|
+
{ id: 'claude-opus-4-6-thinking', label: 'Claude Opus 4.6 (Thinking)' },
|
|
11
|
+
{ id: 'gpt-oss-120b-medium', label: 'GPT-OSS 120B (Medium)' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const ANTIGRAVITY_MODES = [
|
|
15
|
+
{
|
|
16
|
+
id: 'full-access',
|
|
17
|
+
label: 'Full access',
|
|
18
|
+
description: 'Antigravity runs with full access (--dangerously-skip-permissions).',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function runCommand(command, args, { env = process.env, cwd, timeoutMs = 20_000 } = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, {
|
|
25
|
+
cwd,
|
|
26
|
+
env,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
child.kill('SIGKILL');
|
|
36
|
+
reject(new Error(`${command} ${args.join(' ')} timed out after ${timeoutMs / 1000}s`));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
|
|
39
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString(); });
|
|
40
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
41
|
+
|
|
42
|
+
child.on('error', error => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(error);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on('close', code => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
if (code === 0) resolve({ stdout, stderr });
|
|
50
|
+
else reject(new Error(`${command} exited with code ${code}: ${stderr.trim() || stdout.trim()}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Probes Antigravity CLI for version, status and available models.
|
|
57
|
+
* @param {{ command?: string, args?: string[], env?: Record<string, string>, label?: string, id?: string, readOnlyMode?: 'instructions' | 'reject' }} [provider]
|
|
58
|
+
* @param {{ cwd?: string, timeoutMs?: number }} [options]
|
|
59
|
+
*/
|
|
60
|
+
export async function probeAntigravity(provider = {}, { cwd, timeoutMs = 20_000 } = {}) {
|
|
61
|
+
const command = provider.command || 'agy';
|
|
62
|
+
const args = provider.args ?? [];
|
|
63
|
+
const env = { ...process.env, ...(provider.env ?? {}) };
|
|
64
|
+
|
|
65
|
+
// Check version
|
|
66
|
+
let version = '';
|
|
67
|
+
try {
|
|
68
|
+
const res = await runCommand(command, [...args, '--version'], { env, cwd, timeoutMs: Math.min(timeoutMs, 5000) });
|
|
69
|
+
version = res.stdout.trim().split('\n')[0] ?? '';
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`Antigravity command '${command}' is not available: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Probe models
|
|
75
|
+
let models = [];
|
|
76
|
+
try {
|
|
77
|
+
const res = await runCommand(command, [...args, 'models'], { env, cwd, timeoutMs });
|
|
78
|
+
for (const line of res.stdout.trim().split(/\r?\n/)) {
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
const [id, label] = line.split('\t');
|
|
81
|
+
if (id && label) {
|
|
82
|
+
models.push({ id: id.trim(), label: label.trim() });
|
|
83
|
+
} else if (id) {
|
|
84
|
+
models.push({ id: id.trim(), label: id.trim() });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
models = [...DEFAULT_ANTIGRAVITY_MODELS];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!models.length) {
|
|
92
|
+
models = [...DEFAULT_ANTIGRAVITY_MODELS];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
version,
|
|
97
|
+
agent: { name: 'antigravity', title: 'Antigravity', version },
|
|
98
|
+
models,
|
|
99
|
+
currentModel: models[0]?.id,
|
|
100
|
+
modes: provider.readOnlyMode === 'instructions' ? [...ANTIGRAVITY_MODES, {
|
|
101
|
+
id: 'read-only', label: 'Read only (instructions)',
|
|
102
|
+
description: 'For new sessions: prompt-only read-only behavior. agy still runs with full access; no OS sandbox enforces this mode.',
|
|
103
|
+
}] : ANTIGRAVITY_MODES,
|
|
104
|
+
currentMode: 'full-access',
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/command.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/** Runs a command; resolves with its exit code and output, never rejects. */
|
|
4
|
+
export function runCommand(command, args, { cwd, env = process.env, timeoutMs = 15_000 } = {}) {
|
|
5
|
+
return new Promise(resolve => {
|
|
6
|
+
let child;
|
|
7
|
+
// Windows cannot exec a script by its shebang; a JavaScript command runs under this Node, as the Codex transport does.
|
|
8
|
+
const script = process.platform === 'win32' && /\.[cm]?js$/i.test(command);
|
|
9
|
+
try { child = spawn(script ? process.execPath : command, script ? [command, ...args] : args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
10
|
+
catch (error) { resolve({ code: -1, stdout: '', stderr: error.message }); return; }
|
|
11
|
+
let stdout = '', stderr = '';
|
|
12
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
13
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
14
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
|
|
15
|
+
child.on('error', error => { clearTimeout(timer); resolve({ code: -1, stdout, stderr: error.message }); });
|
|
16
|
+
child.on('close', code => { clearTimeout(timer); resolve({ code: code ?? -1, stdout, stderr }); });
|
|
17
|
+
});
|
|
18
|
+
}
|
package/src/doctor.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DoctorCheck = {
|
|
2
|
+
id: string;
|
|
3
|
+
status: 'ok' | 'info' | 'warn' | 'fail';
|
|
4
|
+
summary: string;
|
|
5
|
+
details?: string[];
|
|
6
|
+
hint?: string;
|
|
7
|
+
fix?: { describe: string; apply(): Promise<string> };
|
|
8
|
+
};
|
|
9
|
+
export type CommandResult = { code: number; stdout: string; stderr: string };
|
|
10
|
+
export type CommandRunner = (command: string, args: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }) => Promise<CommandResult>;
|
|
11
|
+
export const runCommand: CommandRunner;
|
|
12
|
+
export function diagnose(options?: { home?: string; project?: string; env?: NodeJS.ProcessEnv; run?: CommandRunner; sandbox?: boolean; daemonEntry?: string; timeoutMs?: number }): Promise<DoctorCheck[]>;
|
|
13
|
+
export function repair(checks: DoctorCheck[]): Promise<Array<{ id: string; result: string }>>;
|