@anhlp/alp-client 0.0.0-stage → 0.0.1-beta.4
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 +28 -2
- 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 +151 -0
- package/dist/index.js +533 -0
- package/dist/integration-lock.d.ts +35 -0
- package/dist/integration-lock.js +179 -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 +19 -0
- package/dist/portable.js +33 -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/registry.d.ts +211 -0
- package/dist/rpc/registry.js +145 -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 +14 -0
- package/dist/supervise.js +87 -0
- package/dist/websocket.d.ts +51 -0
- package/dist/websocket.js +190 -0
- package/package.json +174 -4
- 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 +596 -0
- package/src/integration-lock.js +179 -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 +28 -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/registry.js +161 -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 +14 -0
- package/src/supervise.js +92 -0
- package/src/websocket.js +259 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON framing for stream transports (a Unix socket, a named pipe, a TCP
|
|
3
|
+
* stream). Portable: strings in, strings out; the stream owner decodes bytes to text first.
|
|
4
|
+
* Message transports such as a WebSocket carry whole frames and need none of this.
|
|
5
|
+
*/
|
|
6
|
+
/** UTF-8 length of `text` without Buffer or TextEncoder; an unpaired surrogate counts as U+FFFD (3 bytes). @param {string} text */
|
|
7
|
+
export function utf8Length(text: string): number;
|
|
8
|
+
/**
|
|
9
|
+
* Splits decoded text chunks into frames. `push` returns the complete frames in order, blank
|
|
10
|
+
* lines skipped, and throws once a frame (complete or still arriving) exceeds `maxFrame` bytes;
|
|
11
|
+
* the decoder is unusable after that. Each character is measured once, so a frame that arrives
|
|
12
|
+
* in many chunks costs linear time.
|
|
13
|
+
* @param {{ maxFrame: number }} options
|
|
14
|
+
* @returns {{ push(chunk: string): string[], readonly buffered: number }}
|
|
15
|
+
*/
|
|
16
|
+
export function createLineDecoder({ maxFrame }: {
|
|
17
|
+
maxFrame: number;
|
|
18
|
+
}): {
|
|
19
|
+
push(chunk: string): string[];
|
|
20
|
+
readonly buffered: number;
|
|
21
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// packages/client/src/rpc/framing.js
|
|
2
|
+
function utf8Length(text) {
|
|
3
|
+
let bytes = 0;
|
|
4
|
+
for (let index = 0; index < text.length; index++) {
|
|
5
|
+
const code = text.charCodeAt(index);
|
|
6
|
+
if (code < 128) bytes += 1;
|
|
7
|
+
else if (code < 2048) bytes += 2;
|
|
8
|
+
else if (code >= 55296 && code <= 56319 && index + 1 < text.length && (text.charCodeAt(index + 1) & 64512) === 56320) {
|
|
9
|
+
bytes += 4;
|
|
10
|
+
index++;
|
|
11
|
+
} else bytes += 3;
|
|
12
|
+
}
|
|
13
|
+
return bytes;
|
|
14
|
+
}
|
|
15
|
+
function createLineDecoder({ maxFrame }) {
|
|
16
|
+
let parts = [];
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
let failed = false;
|
|
19
|
+
const overflow = () => {
|
|
20
|
+
failed = true;
|
|
21
|
+
parts = [];
|
|
22
|
+
bytes = 0;
|
|
23
|
+
return new RangeError(`Frame exceeds ${maxFrame} bytes`);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
push(chunk) {
|
|
27
|
+
if (failed) throw new RangeError("Line decoder already failed");
|
|
28
|
+
const frames = [];
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (let index = chunk.indexOf("\n"); index >= 0; index = chunk.indexOf("\n", start)) {
|
|
31
|
+
const piece = chunk.slice(start, index);
|
|
32
|
+
bytes += utf8Length(piece);
|
|
33
|
+
if (bytes > maxFrame) throw overflow();
|
|
34
|
+
parts.push(piece);
|
|
35
|
+
const line = parts.length === 1 ? parts[0] : parts.join("");
|
|
36
|
+
parts = [];
|
|
37
|
+
bytes = 0;
|
|
38
|
+
start = index + 1;
|
|
39
|
+
if (line.trim()) frames.push(line);
|
|
40
|
+
}
|
|
41
|
+
if (start < chunk.length) {
|
|
42
|
+
const rest = chunk.slice(start);
|
|
43
|
+
bytes += utf8Length(rest);
|
|
44
|
+
if (bytes > maxFrame) throw overflow();
|
|
45
|
+
parts.push(rest);
|
|
46
|
+
}
|
|
47
|
+
return frames;
|
|
48
|
+
},
|
|
49
|
+
get buffered() {
|
|
50
|
+
return bytes;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
createLineDecoder,
|
|
56
|
+
utf8Length
|
|
57
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @typedef {{ name: string, version: string }} ClientInfo */
|
|
2
|
+
/**
|
|
3
|
+
* The version handshake every ALP connection starts with (plans/reference/ALPD.md §3): alpd
|
|
4
|
+
* answers nothing else before it. Shared by the Node `connect` and portable clients, so a change
|
|
5
|
+
* to the handshake (Task 06) lands in one place.
|
|
6
|
+
* @param {{ request(method: string, params?: unknown, options?: import('./client.js').RequestOptions): Promise<any> }} client
|
|
7
|
+
* @param {ClientInfo} [info]
|
|
8
|
+
* @param {import('./client.js').RequestOptions} [options]
|
|
9
|
+
* @returns {Promise<{ protocolVersion: number } & Record<string, unknown>>}
|
|
10
|
+
*/
|
|
11
|
+
export function hello(client: {
|
|
12
|
+
request(method: string, params?: unknown, options?: import("./client.js").RequestOptions): Promise<any>;
|
|
13
|
+
}, info?: ClientInfo, options?: import("./client.js").RequestOptions): Promise<{
|
|
14
|
+
protocolVersion: number;
|
|
15
|
+
} & Record<string, unknown>>;
|
|
16
|
+
export type ClientInfo = {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// packages/client/src/rpc/handshake.js
|
|
2
|
+
import { PROTOCOL_VERSION } from "@anhlp/alp-protocol";
|
|
3
|
+
function hello(client, info = { name: "alp", version: "0" }, options) {
|
|
4
|
+
return client.request("daemon.hello", { protocolVersion: PROTOCOL_VERSION, client: info }, options);
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
hello
|
|
8
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps an ALP connection up: connects, runs the caller's `onConnect` (handshake, reattach and
|
|
3
|
+
* replay as the protocol defines them), and after a disconnect connects again with exponential
|
|
4
|
+
* backoff until stopped. A connection counts as up only once it has stayed open `stableMs`; one
|
|
5
|
+
* that drops sooner (a gateway closing right after the handshake) counts as a failed attempt, so
|
|
6
|
+
* backoff and `maxAttempts` still apply. It never resends a request: requests in flight when the connection drops
|
|
7
|
+
* reject as `disconnected`, and requests made while it is down reject at once. Whether and how to
|
|
8
|
+
* retry belongs to the caller, who knows which methods are safe to repeat.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./client.js').AlpTransport} AlpTransport
|
|
12
|
+
* @typedef {import('./client.js').AlpRpcClientOptions} AlpRpcClientOptions
|
|
13
|
+
* @typedef {import('./client.js').RequestOptions} RequestOptions
|
|
14
|
+
* @typedef {import('./client.js').AbortSignalLike} AbortSignalLike
|
|
15
|
+
* @typedef {'connecting' | 'open' | 'waiting' | 'stopped'} ReconnectState
|
|
16
|
+
* @typedef {{ initialMs?: number, maxMs?: number, factor?: number, jitter?: number }} Backoff
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* connect(context: { attempt: number, signal: AbortSignalLike }): Promise<AlpTransport>,
|
|
19
|
+
* onConnect?(client: AlpRpcClient, context: { attempt: number, reconnect: boolean, signal: AbortSignalLike }): unknown,
|
|
20
|
+
* onDisconnect?(error: Error, context: { reconnect: boolean }): void,
|
|
21
|
+
* backoff?: Backoff,
|
|
22
|
+
* maxAttempts?: number,
|
|
23
|
+
* stableMs?: number,
|
|
24
|
+
* shouldRetry?(error: Error): boolean,
|
|
25
|
+
* client?: AlpRpcClientOptions,
|
|
26
|
+
* random?: () => number,
|
|
27
|
+
* }} ReconnectOptions
|
|
28
|
+
* @typedef {{
|
|
29
|
+
* readonly state: ReconnectState,
|
|
30
|
+
* readonly client: AlpRpcClient | undefined,
|
|
31
|
+
* readonly attempt: number,
|
|
32
|
+
* ready(options?: { signal?: AbortSignalLike }): Promise<AlpRpcClient>,
|
|
33
|
+
* request<T = any>(method: string, params?: unknown, options?: RequestOptions): Promise<T>,
|
|
34
|
+
* onEvent(listener: (envelope: any) => void): () => boolean,
|
|
35
|
+
* onNotification(listener: (method: string, params: any) => void): () => boolean,
|
|
36
|
+
* onState(listener: (state: ReconnectState, error?: Error) => void): () => boolean,
|
|
37
|
+
* stop(): void,
|
|
38
|
+
* }} ReconnectingClient
|
|
39
|
+
*/
|
|
40
|
+
/** The delay before attempt `attempt` (1-based), with ±jitter. @param {number} attempt @param {Backoff} backoff @param {() => number} random */
|
|
41
|
+
export function backoffDelay(attempt: number, { initialMs, maxMs, factor, jitter }?: Backoff, random?: () => number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a failed connect or onConnect is worth another attempt: everything but alpd refusing the
|
|
44
|
+
* handshake (ERROR.protocol, an unsupported protocol version), which retrying cannot fix.
|
|
45
|
+
* @param {Error} error
|
|
46
|
+
*/
|
|
47
|
+
export function retryable(error: Error): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* @param {ReconnectOptions} options
|
|
50
|
+
* @returns {ReconnectingClient}
|
|
51
|
+
*/
|
|
52
|
+
export function createReconnectingClient({ connect, onConnect, onDisconnect, backoff, maxAttempts, stableMs, shouldRetry, client: clientOptions, random }: ReconnectOptions): ReconnectingClient;
|
|
53
|
+
export type AlpTransport = import("./client.js").AlpTransport;
|
|
54
|
+
export type AlpRpcClientOptions = import("./client.js").AlpRpcClientOptions;
|
|
55
|
+
export type RequestOptions = import("./client.js").RequestOptions;
|
|
56
|
+
export type AbortSignalLike = import("./client.js").AbortSignalLike;
|
|
57
|
+
export type ReconnectState = "connecting" | "open" | "waiting" | "stopped";
|
|
58
|
+
export type Backoff = {
|
|
59
|
+
initialMs?: number;
|
|
60
|
+
maxMs?: number;
|
|
61
|
+
factor?: number;
|
|
62
|
+
jitter?: number;
|
|
63
|
+
};
|
|
64
|
+
export type ReconnectOptions = {
|
|
65
|
+
connect(context: {
|
|
66
|
+
attempt: number;
|
|
67
|
+
signal: AbortSignalLike;
|
|
68
|
+
}): Promise<AlpTransport>;
|
|
69
|
+
onConnect?(client: AlpRpcClient, context: {
|
|
70
|
+
attempt: number;
|
|
71
|
+
reconnect: boolean;
|
|
72
|
+
signal: AbortSignalLike;
|
|
73
|
+
}): unknown;
|
|
74
|
+
onDisconnect?(error: Error, context: {
|
|
75
|
+
reconnect: boolean;
|
|
76
|
+
}): void;
|
|
77
|
+
backoff?: Backoff;
|
|
78
|
+
maxAttempts?: number;
|
|
79
|
+
stableMs?: number;
|
|
80
|
+
shouldRetry?(error: Error): boolean;
|
|
81
|
+
client?: AlpRpcClientOptions;
|
|
82
|
+
random?: () => number;
|
|
83
|
+
};
|
|
84
|
+
export type ReconnectingClient = {
|
|
85
|
+
readonly state: ReconnectState;
|
|
86
|
+
readonly client: AlpRpcClient | undefined;
|
|
87
|
+
readonly attempt: number;
|
|
88
|
+
ready(options?: {
|
|
89
|
+
signal?: AbortSignalLike;
|
|
90
|
+
}): Promise<AlpRpcClient>;
|
|
91
|
+
request<T = any>(method: string, params?: unknown, options?: RequestOptions): Promise<T>;
|
|
92
|
+
onEvent(listener: (envelope: any) => void): () => boolean;
|
|
93
|
+
onNotification(listener: (method: string, params: any) => void): () => boolean;
|
|
94
|
+
onState(listener: (state: ReconnectState, error?: Error) => void): () => boolean;
|
|
95
|
+
stop(): void;
|
|
96
|
+
};
|
|
97
|
+
import { AlpRpcClient } from './client.js';
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// packages/client/src/rpc/reconnect.js
|
|
2
|
+
import { AlpRpcError, ERROR } from "@anhlp/alp-protocol";
|
|
3
|
+
import { AlpRpcClient } from "./client.js";
|
|
4
|
+
import { AlpClientError } from "./errors.js";
|
|
5
|
+
function backoffDelay(attempt, { initialMs = 250, maxMs = 1e4, factor = 2, jitter = 0.2 } = {}, random = Math.random) {
|
|
6
|
+
const base = Math.min(maxMs, initialMs * factor ** Math.max(0, attempt - 1));
|
|
7
|
+
return Math.max(0, Math.round(base * (1 + jitter * (2 * random() - 1))));
|
|
8
|
+
}
|
|
9
|
+
function retryable(error) {
|
|
10
|
+
return !(error instanceof AlpRpcError && error.code === ERROR.protocol);
|
|
11
|
+
}
|
|
12
|
+
function controller() {
|
|
13
|
+
if (typeof globalThis.AbortController === "function") {
|
|
14
|
+
const native = new globalThis.AbortController();
|
|
15
|
+
return {
|
|
16
|
+
signal: (
|
|
17
|
+
/** @type {AbortSignalLike} */
|
|
18
|
+
native.signal
|
|
19
|
+
),
|
|
20
|
+
/** @param {unknown} reason */
|
|
21
|
+
abort: (reason) => native.abort(reason)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
25
|
+
const signal = {
|
|
26
|
+
aborted: false,
|
|
27
|
+
/** @type {unknown} */
|
|
28
|
+
reason: void 0,
|
|
29
|
+
/** @param {'abort'} _type @param {() => void} listener */
|
|
30
|
+
addEventListener(_type, listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
},
|
|
33
|
+
/** @param {'abort'} _type @param {() => void} listener */
|
|
34
|
+
removeEventListener(_type, listener) {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
signal,
|
|
40
|
+
/** @param {unknown} reason */
|
|
41
|
+
abort(reason) {
|
|
42
|
+
if (signal.aborted) return;
|
|
43
|
+
signal.aborted = true;
|
|
44
|
+
signal.reason = reason;
|
|
45
|
+
for (const listener of [...listeners]) listener();
|
|
46
|
+
listeners.clear();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createReconnectingClient({ connect, onConnect, onDisconnect, backoff = {}, maxAttempts = Infinity, stableMs = 5e3, shouldRetry = retryable, client: clientOptions, random = Math.random }) {
|
|
51
|
+
if (typeof connect !== "function") throw new TypeError("createReconnectingClient needs connect()");
|
|
52
|
+
let state = "connecting";
|
|
53
|
+
let current;
|
|
54
|
+
let attempt = 0;
|
|
55
|
+
let connectedBefore = false;
|
|
56
|
+
let timer;
|
|
57
|
+
const run = controller();
|
|
58
|
+
const events = /* @__PURE__ */ new Set();
|
|
59
|
+
const notifications = /* @__PURE__ */ new Set();
|
|
60
|
+
const states = /* @__PURE__ */ new Set();
|
|
61
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
62
|
+
const onListenerError = clientOptions?.onListenerError ?? ((error) => {
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
throw error;
|
|
65
|
+
}, 0);
|
|
66
|
+
});
|
|
67
|
+
const dispatch = (listener, ...args) => {
|
|
68
|
+
try {
|
|
69
|
+
listener(...args);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
onListenerError(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const enter = (next, error) => {
|
|
75
|
+
state = next;
|
|
76
|
+
for (const listener of [...states]) {
|
|
77
|
+
dispatch(listener, next, error);
|
|
78
|
+
if (state !== next) return;
|
|
79
|
+
}
|
|
80
|
+
if (next === "open" && current) for (const waiter of [...waiters]) {
|
|
81
|
+
waiters.delete(waiter);
|
|
82
|
+
waiter.resolve(current);
|
|
83
|
+
}
|
|
84
|
+
if (next === "stopped") for (const waiter of [...waiters]) {
|
|
85
|
+
waiters.delete(waiter);
|
|
86
|
+
waiter.reject(error ?? new AlpClientError("ALP connection stopped", { reason: "closed" }));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const retry = (error) => {
|
|
90
|
+
if (state === "stopped") return;
|
|
91
|
+
if (attempt >= maxAttempts || !shouldRetry(error)) return enter("stopped", error);
|
|
92
|
+
enter("waiting", error);
|
|
93
|
+
if (state !== "waiting") return;
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
timer = void 0;
|
|
96
|
+
void open();
|
|
97
|
+
}, backoffDelay(attempt, backoff, random));
|
|
98
|
+
};
|
|
99
|
+
const open = async () => {
|
|
100
|
+
if (state === "stopped") return;
|
|
101
|
+
attempt++;
|
|
102
|
+
enter("connecting");
|
|
103
|
+
const { signal } = run;
|
|
104
|
+
if (state !== "connecting" || signal.aborted) return;
|
|
105
|
+
let candidate;
|
|
106
|
+
const closeCandidate = () => candidate?.close();
|
|
107
|
+
signal.addEventListener("abort", closeCandidate, { once: true });
|
|
108
|
+
try {
|
|
109
|
+
const transport = await connect({ attempt, signal });
|
|
110
|
+
if (signal.aborted) {
|
|
111
|
+
try {
|
|
112
|
+
transport.close();
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
candidate = new AlpRpcClient(transport, clientOptions);
|
|
118
|
+
candidate.onEvent((envelope) => {
|
|
119
|
+
for (const listener of [...events]) dispatch(listener, envelope);
|
|
120
|
+
});
|
|
121
|
+
candidate.onNotification((method, params) => {
|
|
122
|
+
for (const listener of [...notifications]) dispatch(listener, method, params);
|
|
123
|
+
});
|
|
124
|
+
if (onConnect) await onConnect(candidate, { attempt, reconnect: connectedBefore, signal });
|
|
125
|
+
if (signal.aborted) {
|
|
126
|
+
candidate.close();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (candidate.closed) throw new AlpClientError("ALP connection closed during onConnect", { reason: "disconnected" });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
candidate?.close();
|
|
132
|
+
if (signal.aborted) return;
|
|
133
|
+
return retry(error instanceof Error ? error : new Error(String(error)));
|
|
134
|
+
} finally {
|
|
135
|
+
signal.removeEventListener("abort", closeCandidate);
|
|
136
|
+
}
|
|
137
|
+
const connected = (
|
|
138
|
+
/** @type {AlpRpcClient} */
|
|
139
|
+
candidate
|
|
140
|
+
);
|
|
141
|
+
current = connected;
|
|
142
|
+
const reconnect = connectedBefore;
|
|
143
|
+
connectedBefore = true;
|
|
144
|
+
const openedAt = Date.now();
|
|
145
|
+
connected.onClose((error) => {
|
|
146
|
+
if (current !== connected) return;
|
|
147
|
+
current = void 0;
|
|
148
|
+
if (Date.now() - openedAt >= stableMs) attempt = 0;
|
|
149
|
+
dispatch(() => onDisconnect?.(error, { reconnect }));
|
|
150
|
+
retry(error);
|
|
151
|
+
});
|
|
152
|
+
enter("open");
|
|
153
|
+
};
|
|
154
|
+
void open();
|
|
155
|
+
return {
|
|
156
|
+
get state() {
|
|
157
|
+
return state;
|
|
158
|
+
},
|
|
159
|
+
get client() {
|
|
160
|
+
return current;
|
|
161
|
+
},
|
|
162
|
+
get attempt() {
|
|
163
|
+
return attempt;
|
|
164
|
+
},
|
|
165
|
+
ready({ signal } = {}) {
|
|
166
|
+
if (state === "open" && current) return Promise.resolve(current);
|
|
167
|
+
if (state === "stopped") return Promise.reject(new AlpClientError("ALP connection stopped", { reason: "closed" }));
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const waiter = {
|
|
170
|
+
/** @param {AlpRpcClient} client */
|
|
171
|
+
resolve: (client) => {
|
|
172
|
+
signal?.removeEventListener("abort", onAbort);
|
|
173
|
+
resolve(client);
|
|
174
|
+
},
|
|
175
|
+
/** @param {Error} error */
|
|
176
|
+
reject: (error) => {
|
|
177
|
+
signal?.removeEventListener("abort", onAbort);
|
|
178
|
+
reject(error);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const onAbort = () => {
|
|
182
|
+
waiters.delete(waiter);
|
|
183
|
+
reject(new AlpClientError("Waiting for the ALP connection was aborted", { reason: "aborted", cause: signal?.reason }));
|
|
184
|
+
};
|
|
185
|
+
if (signal?.aborted) return onAbort();
|
|
186
|
+
waiters.add(waiter);
|
|
187
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
request(method, params, options) {
|
|
191
|
+
if (state !== "open" || !current) return Promise.reject(new AlpClientError("ALP daemon connection closed", { reason: state === "stopped" ? "closed" : "disconnected" }));
|
|
192
|
+
return current.request(method, params, options);
|
|
193
|
+
},
|
|
194
|
+
onEvent(listener) {
|
|
195
|
+
events.add(listener);
|
|
196
|
+
return () => events.delete(listener);
|
|
197
|
+
},
|
|
198
|
+
onNotification(listener) {
|
|
199
|
+
notifications.add(listener);
|
|
200
|
+
return () => notifications.delete(listener);
|
|
201
|
+
},
|
|
202
|
+
onState(listener) {
|
|
203
|
+
states.add(listener);
|
|
204
|
+
return () => states.delete(listener);
|
|
205
|
+
},
|
|
206
|
+
stop() {
|
|
207
|
+
if (state === "stopped") return;
|
|
208
|
+
if (timer !== void 0) {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
timer = void 0;
|
|
211
|
+
}
|
|
212
|
+
run.abort(new AlpClientError("ALP connection stopped", { reason: "closed" }));
|
|
213
|
+
const last = current;
|
|
214
|
+
current = void 0;
|
|
215
|
+
enter("stopped");
|
|
216
|
+
last?.close();
|
|
217
|
+
events.clear();
|
|
218
|
+
notifications.clear();
|
|
219
|
+
states.clear();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
backoffDelay,
|
|
225
|
+
createReconnectingClient,
|
|
226
|
+
retryable
|
|
227
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Typed registry methods for portable clients. Mutations are sent once: callers retain requestId
|
|
2
|
+
* and ifRevision to reconcile a timeout by repeating the identical request. @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client */
|
|
3
|
+
export function workspaceRegistry(client: {
|
|
4
|
+
request<T = any>(method: string, params?: unknown): Promise<T>;
|
|
5
|
+
}): {
|
|
6
|
+
/** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.options']['result']>} */
|
|
7
|
+
worktreeOptions: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.options"]["result"]>;
|
|
8
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['result']>} */
|
|
9
|
+
createWorktree: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.create"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.create"]["result"]>;
|
|
10
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['result']>} */
|
|
11
|
+
removeWorktree: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.remove"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.remove"]["result"]>;
|
|
12
|
+
/** Read-only Git status of a registry workspace (capability `workspace.git`, §12.5). @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.status']['result']>} */
|
|
13
|
+
gitStatus: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.status"]["result"]>;
|
|
14
|
+
/** One page of a read-only Git diff; pass `nextCursor` back as `cursor` with the same comparison and path. @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.diff']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.diff']['result']>} */
|
|
15
|
+
gitDiff: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.diff"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.diff"]["result"]>;
|
|
16
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.list']['result']>} */
|
|
17
|
+
projects: (params?: import("@anhlp/alp-protocol").RpcMethodMap["project.list"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.list"]["result"]>;
|
|
18
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['result']>} */
|
|
19
|
+
workspaces: (params?: import("@anhlp/alp-protocol").RpcMethodMap["workspace.list"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.list"]["result"]>;
|
|
20
|
+
/** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.get']['result']>} */
|
|
21
|
+
get: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.get"]["result"]>;
|
|
22
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.register']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.register']['result']>} */
|
|
23
|
+
register: (params: import("@anhlp/alp-protocol").RpcMethodMap["project.register"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.register"]["result"]>;
|
|
24
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['result']>} */
|
|
25
|
+
remove: (params: import("@anhlp/alp-protocol").RpcMethodMap["project.remove"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.remove"]["result"]>;
|
|
26
|
+
/** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['result']>} */
|
|
27
|
+
update: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.update"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.update"]["result"]>;
|
|
28
|
+
};
|
|
29
|
+
/** Canonical base64 of `bytes`, without Buffer or btoa, so it runs in browsers and Node alike. @param {Uint8Array} bytes */
|
|
30
|
+
export function base64Encode(bytes: Uint8Array): string;
|
|
31
|
+
/** Lowercase hex SHA-256 of `bytes` through WebCrypto (browsers' secure contexts, Node 19+). @param {Uint8Array} bytes */
|
|
32
|
+
export function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Typed attachment methods (capability `attachments`, docs/protocol/alp-json-rpc-v1.md §12.6) and a
|
|
35
|
+
* chunked upload that keeps to the limits the daemon (or gateway) advertised. Mutations are sent
|
|
36
|
+
* once: `upload` resumes an interrupted upload by repeating its begin with the same requestId and
|
|
37
|
+
* continues from what the daemon holds.
|
|
38
|
+
* @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client
|
|
39
|
+
*/
|
|
40
|
+
export function attachments(client: {
|
|
41
|
+
request<T = any>(method: string, params?: unknown): Promise<T>;
|
|
42
|
+
}): {
|
|
43
|
+
/** @param {Map['attachment.begin']['params']} params @returns {Promise<Map['attachment.begin']['result']>} */
|
|
44
|
+
begin: (params: import("@anhlp/alp-protocol").PrincipalContext & {
|
|
45
|
+
sessionId: string;
|
|
46
|
+
name: string;
|
|
47
|
+
size: number;
|
|
48
|
+
mimeType: string;
|
|
49
|
+
sha256?: string;
|
|
50
|
+
requestId: string;
|
|
51
|
+
}) => Promise<{
|
|
52
|
+
uploadId: string;
|
|
53
|
+
state: "uploading" | "committed";
|
|
54
|
+
received: number;
|
|
55
|
+
size: number;
|
|
56
|
+
maxChunkBytes: number;
|
|
57
|
+
expiresAt?: string;
|
|
58
|
+
attachmentId?: string;
|
|
59
|
+
}>;
|
|
60
|
+
/** @param {Map['attachment.chunk']['params']} params @returns {Promise<Map['attachment.chunk']['result']>} */
|
|
61
|
+
chunk: (params: import("@anhlp/alp-protocol").PrincipalContext & {
|
|
62
|
+
uploadId: string;
|
|
63
|
+
offset: number;
|
|
64
|
+
data: string;
|
|
65
|
+
}) => Promise<{
|
|
66
|
+
received: number;
|
|
67
|
+
size: number;
|
|
68
|
+
}>;
|
|
69
|
+
/** @param {Map['attachment.commit']['params']} params @returns {Promise<Map['attachment.commit']['result']>} */
|
|
70
|
+
commit: (params: import("@anhlp/alp-protocol").PrincipalContext & {
|
|
71
|
+
uploadId: string;
|
|
72
|
+
sha256: string;
|
|
73
|
+
}) => Promise<{
|
|
74
|
+
attachment: import("@anhlp/alp-protocol").AttachmentInfo;
|
|
75
|
+
}>;
|
|
76
|
+
/** @param {string} uploadId @returns {Promise<Map['attachment.abort']['result']>} */
|
|
77
|
+
abort: (uploadId: string) => Promise<{
|
|
78
|
+
aborted: true;
|
|
79
|
+
}>;
|
|
80
|
+
/** Metadata only (no bytes, no path). @param {string} attachmentId @returns {Promise<Map['attachment.get']['result']>} */
|
|
81
|
+
get: (attachmentId: string) => Promise<{
|
|
82
|
+
attachment: import("@anhlp/alp-protocol").AttachmentInfo;
|
|
83
|
+
}>;
|
|
84
|
+
/** Refused (1004 attachment_referenced) while a prompt references it. @param {string} attachmentId @returns {Promise<Map['attachment.delete']['result']>} */
|
|
85
|
+
delete: (attachmentId: string) => Promise<{
|
|
86
|
+
deleted: true;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Uploads `data` and commits it: begin (or resume, with the same requestId), chunks of at most
|
|
90
|
+
* the smaller of `maxChunkBytes` and what begin answered, commit with its SHA-256. A chunk
|
|
91
|
+
* refused `offset_mismatch` continues from the daemon's `received`. Returns the attachment.
|
|
92
|
+
* @param {{ sessionId: string, name: string, mimeType: string, data: Uint8Array, requestId: string, sha256?: string, maxChunkBytes?: number, onProgress?: (received: number, size: number) => void }} options
|
|
93
|
+
*/
|
|
94
|
+
upload({ sessionId, name, mimeType, data, requestId, sha256, maxChunkBytes, onProgress }: {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
name: string;
|
|
97
|
+
mimeType: string;
|
|
98
|
+
data: Uint8Array;
|
|
99
|
+
requestId: string;
|
|
100
|
+
sha256?: string;
|
|
101
|
+
maxChunkBytes?: number;
|
|
102
|
+
onProgress?: (received: number, size: number) => void;
|
|
103
|
+
}): Promise<import("@anhlp/alp-protocol").AttachmentInfo>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Typed conversation methods (capabilities `session.fork` and `session.rewind`, docs/protocol/
|
|
107
|
+
* alp-json-rpc-v1.md §12.7; `session.reload` and `session.resumeCommand`, §12.8). Both change the conversation only, never a file. Mutations are sent
|
|
108
|
+
* once: keep `requestId` (and `ifGeneration`) to repeat the identical request after a timeout, which
|
|
109
|
+
* answers the first result with `duplicate: true`. After a rewind, attached clients get a
|
|
110
|
+
* `history.reset` event: drop cached history, fetch a new tail and attach with its generation.
|
|
111
|
+
* @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client
|
|
112
|
+
*/
|
|
113
|
+
export function conversation(client: {
|
|
114
|
+
request<T = any>(method: string, params?: unknown): Promise<T>;
|
|
115
|
+
}): {
|
|
116
|
+
/**
|
|
117
|
+
* A new closed root whose conversation is the source root's through or before a turn (`boundary`),
|
|
118
|
+
* or a branch a rewind set aside (`branchId`); resume it with session.create { sessionId, resume: true }.
|
|
119
|
+
* Principal fields are set by a trusted gateway on its own connection, never by a remote client.
|
|
120
|
+
* @param {Map['session.fork']['params']} params @returns {Promise<Map['session.fork']['result']>}
|
|
121
|
+
*/
|
|
122
|
+
fork: (params: import("@anhlp/alp-protocol").PrincipalContext & {
|
|
123
|
+
sessionId: string;
|
|
124
|
+
requestId: string;
|
|
125
|
+
ifGeneration: string;
|
|
126
|
+
boundary?: import("@anhlp/alp-protocol").ForkBoundary;
|
|
127
|
+
branchId?: string;
|
|
128
|
+
workspaceId?: string;
|
|
129
|
+
newSessionId?: string;
|
|
130
|
+
title?: string;
|
|
131
|
+
}) => Promise<{
|
|
132
|
+
session: import("@anhlp/alp-protocol").SessionSummary;
|
|
133
|
+
forkedFrom: import("@anhlp/alp-protocol").ForkProvenance;
|
|
134
|
+
duplicate?: true;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* Continues the root's conversation from before `beforeTurnId` under the same session id; the
|
|
138
|
+
* later turns stay a branch (`branch.branchId`) that `fork({ branchId })` recovers.
|
|
139
|
+
* @param {Map['session.rewind']['params']} params @returns {Promise<Map['session.rewind']['result']>}
|
|
140
|
+
*/
|
|
141
|
+
rewind: (params: {
|
|
142
|
+
sessionId: string;
|
|
143
|
+
requestId: string;
|
|
144
|
+
ifGeneration: string;
|
|
145
|
+
beforeTurnId: string;
|
|
146
|
+
}) => Promise<{
|
|
147
|
+
session: import("@anhlp/alp-protocol").SessionSummary;
|
|
148
|
+
generation: string;
|
|
149
|
+
branch: import("@anhlp/alp-protocol").ConversationBranch;
|
|
150
|
+
duplicate?: true;
|
|
151
|
+
}>;
|
|
152
|
+
/**
|
|
153
|
+
* Restarts a live root's native process on its own conversation and configuration at the idle
|
|
154
|
+
* barrier (capability `session.reload`, §12.8); id, history and receipts stay. Sent once: repeat
|
|
155
|
+
* the same `requestId` after a lost answer.
|
|
156
|
+
* @param {Map['session.reload']['params']} params @returns {Promise<Map['session.reload']['result']>}
|
|
157
|
+
*/
|
|
158
|
+
reload: (params: {
|
|
159
|
+
sessionId: string;
|
|
160
|
+
requestId: string;
|
|
161
|
+
ifGeneration: string;
|
|
162
|
+
}) => Promise<{
|
|
163
|
+
session: import("@anhlp/alp-protocol").SessionSummary;
|
|
164
|
+
duplicate?: true;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* The ALP CLI command that follows the session in a terminal (capability `session.resumeCommand`,
|
|
168
|
+
* §12.8), built by alpd: for the clipboard only, never to run.
|
|
169
|
+
* @param {Map['session.resumeCommand']['params']} params @returns {Promise<Map['session.resumeCommand']['result']>}
|
|
170
|
+
*/
|
|
171
|
+
resumeCommand: (params: {
|
|
172
|
+
sessionId: string;
|
|
173
|
+
}) => Promise<import("@anhlp/alp-protocol").ResumeCommand>;
|
|
174
|
+
/**
|
|
175
|
+
* Changes a live root's model, effort or team (capability `session.configure.live`, §12.9) at the
|
|
176
|
+
* idle barrier, through a restart of its native process on the same conversation; `configured` says
|
|
177
|
+
* what it runs with now. Sent once: repeat the same `requestId` after a lost answer. A change to
|
|
178
|
+
* another runtime is refused (1005 runtime_mismatch, `data.action: 'session.create'`).
|
|
179
|
+
* @param {Map['session.configure']['params'] & { requestId: string; ifGeneration: string }} params @returns {Promise<Map['session.configure']['result']>}
|
|
180
|
+
*/
|
|
181
|
+
configure: (params: {
|
|
182
|
+
sessionId: string;
|
|
183
|
+
mode?: string;
|
|
184
|
+
model?: string;
|
|
185
|
+
thinking?: string;
|
|
186
|
+
team?: string;
|
|
187
|
+
requestId?: string;
|
|
188
|
+
ifGeneration?: string;
|
|
189
|
+
} & {
|
|
190
|
+
requestId: string;
|
|
191
|
+
ifGeneration: string;
|
|
192
|
+
}) => Promise<{
|
|
193
|
+
session: import("@anhlp/alp-protocol").SessionSnapshot | import("@anhlp/alp-protocol").SessionSummary;
|
|
194
|
+
configured?: import("@anhlp/alp-protocol").ConfigureOutcome;
|
|
195
|
+
duplicate?: true;
|
|
196
|
+
}>;
|
|
197
|
+
/**
|
|
198
|
+
* What a change of model, effort, team or mode would run a live root with, and whether it would be
|
|
199
|
+
* taken now (capability `session.configure.live`, §12.9). Reads only.
|
|
200
|
+
* @param {Map['session.configure.preview']['params']} params @returns {Promise<Map['session.configure.preview']['result']>}
|
|
201
|
+
*/
|
|
202
|
+
configurePreview: (params: {
|
|
203
|
+
sessionId: string;
|
|
204
|
+
mode?: string;
|
|
205
|
+
model?: string;
|
|
206
|
+
thinking?: string;
|
|
207
|
+
team?: string;
|
|
208
|
+
}) => Promise<{
|
|
209
|
+
preview: import("@anhlp/alp-protocol").ConfigurePreview;
|
|
210
|
+
}>;
|
|
211
|
+
};
|