@anhlp/alp-client 0.0.0-stage → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -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 +32 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.js +386 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +391 -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 +144 -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/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/package.json +135 -4
- package/src/acp-probe.js +46 -0
- package/src/antigravity-probe.js +106 -0
- package/src/command.js +16 -0
- package/src/doctor.d.ts +13 -0
- package/src/doctor.js +375 -0
- package/src/index.js +408 -0
- package/src/library-test.js +41 -0
- package/src/mcp-probe.js +115 -0
- package/src/prompt.js +64 -0
- package/src/render.js +136 -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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function alpHome(env?: NodeJS.ProcessEnv): string;
|
|
2
|
+
/** Windows uses named pipes; long Unix homes get a short socket in the temp directory. */
|
|
3
|
+
export function daemonPaths(home: any): {
|
|
4
|
+
home: any;
|
|
5
|
+
lock: string;
|
|
6
|
+
socket: string;
|
|
7
|
+
log: string;
|
|
8
|
+
install: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Finds alpd.js for a client that cannot locate it beside itself, such as the Paseo
|
|
12
|
+
* plugin, which Paseo re-bundles: the first existing candidate, then where alpd last
|
|
13
|
+
* recorded itself ($ALP_HOME/alpd.json), then the ALP CLI (`alp`) on PATH.
|
|
14
|
+
*/
|
|
15
|
+
export function findDaemonEntry({ home, env, candidates }?: {
|
|
16
|
+
home?: string | undefined;
|
|
17
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
18
|
+
candidates?: (string | undefined)[] | undefined;
|
|
19
|
+
}): Promise<string | undefined>;
|
|
20
|
+
/** Wall-clock boot time; a lock from an earlier boot is stale even if its pid is reused. */
|
|
21
|
+
export function bootTime(): number;
|
|
22
|
+
export function readLock(home: any): Promise<any>;
|
|
23
|
+
export function lockAlive(lock: any): boolean;
|
|
24
|
+
/** Connects and performs the version handshake. */
|
|
25
|
+
export function connect(socketPath: any, client?: {
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
}): Promise<AlpClient>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the socket of a ready daemon, starting one when none runs.
|
|
31
|
+
* `entry` is the daemon script; it is started through its own detaching launcher so the
|
|
32
|
+
* daemon outlives the client that started it.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureDaemon({ home, entry, env, execPath, timeoutMs }?: {
|
|
35
|
+
home?: string | undefined;
|
|
36
|
+
entry?: string | undefined;
|
|
37
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
38
|
+
execPath?: string | undefined;
|
|
39
|
+
timeoutMs?: number | undefined;
|
|
40
|
+
}): Promise<any>;
|
|
41
|
+
export function discoverIntegration(name?: string, options?: {}): Promise<string | undefined>;
|
|
42
|
+
export function startIntegration(nameOrOptions?: string): Promise<any>;
|
|
43
|
+
/** One JSON-RPC 2.0 connection to alpd; events arrive as `event` notifications. */
|
|
44
|
+
export class AlpClient {
|
|
45
|
+
constructor(socket: any);
|
|
46
|
+
get closed(): boolean;
|
|
47
|
+
request(method: any, params?: {}): Promise<any>;
|
|
48
|
+
onEvent(listener: any): () => boolean;
|
|
49
|
+
onRequest(handler: any): () => boolean;
|
|
50
|
+
onClose(listener: any): () => boolean;
|
|
51
|
+
close(): void;
|
|
52
|
+
#private;
|
|
53
|
+
}
|
|
54
|
+
export { PROTOCOL_VERSION, MAX_FRAME, AlpRpcError, encodeFrame } from "@anhlp/alp-protocol";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// packages/client/src/index.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { PROTOCOL_VERSION, MAX_FRAME, AlpRpcError, encodeFrame } from "@anhlp/alp-protocol";
|
|
9
|
+
import { PROTOCOL_VERSION as PROTOCOL_VERSION2, MAX_FRAME as MAX_FRAME2, AlpRpcError as AlpRpcError2, encodeFrame as encodeFrame2 } from "@anhlp/alp-protocol";
|
|
10
|
+
function alpHome(env = process.env) {
|
|
11
|
+
return path.resolve(env.ALP_HOME || path.join(os.homedir(), ".alp"));
|
|
12
|
+
}
|
|
13
|
+
function daemonPaths(home) {
|
|
14
|
+
const preferred = path.join(home, "alpd.sock");
|
|
15
|
+
const socket = process.platform === "win32" ? `\\\\.\\pipe\\alpd-${createHash("sha256").update(home).digest("hex").slice(0, 24)}` : Buffer.byteLength(preferred) < 100 ? preferred : path.join(os.tmpdir(), `alpd-${createHash("sha256").update(home).digest("hex").slice(0, 12)}.sock`);
|
|
16
|
+
return { home, lock: path.join(home, "alpd.lock"), socket, log: path.join(home, "logs", "alpd.log"), install: path.join(home, "alpd.json") };
|
|
17
|
+
}
|
|
18
|
+
var isFile = (file) => stat(file).then((info) => info.isFile(), () => false);
|
|
19
|
+
var BIN_DIRECTORIES = [path.join(os.homedir(), ".npm-global", "bin"), path.join(os.homedir(), ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin"];
|
|
20
|
+
var CLI_PACKAGES = ["@anhlp/alp", "@anhlp/alp-cli"];
|
|
21
|
+
async function findDaemonEntry({ home = alpHome(), env = process.env, candidates = (
|
|
22
|
+
/** @type {Array<string | undefined>} */
|
|
23
|
+
[]
|
|
24
|
+
) } = {}) {
|
|
25
|
+
for (const candidate of candidates) if (candidate && await isFile(candidate)) return candidate;
|
|
26
|
+
try {
|
|
27
|
+
const { entry } = JSON.parse(await readFile(daemonPaths(home).install, "utf8"));
|
|
28
|
+
if (typeof entry === "string" && await isFile(entry)) return entry;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
for (const directory of [...(env.PATH ?? "").split(path.delimiter).filter(Boolean), ...BIN_DIRECTORIES]) {
|
|
32
|
+
const cli = await realpath(path.join(directory, "alp")).catch(() => void 0);
|
|
33
|
+
if (!cli || path.basename(cli) !== "cli.js") continue;
|
|
34
|
+
const root = path.resolve(path.dirname(cli), "..");
|
|
35
|
+
const name = await readFile(path.join(root, "package.json"), "utf8").then((text) => JSON.parse(text).name, () => void 0);
|
|
36
|
+
const entry = path.join(root, "dist", "alpd.js");
|
|
37
|
+
if (CLI_PACKAGES.includes(name) && await isFile(entry)) return entry;
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
function bootTime() {
|
|
42
|
+
return Math.round((Date.now() - os.uptime() * 1e3) / 1e3);
|
|
43
|
+
}
|
|
44
|
+
async function readLock(home) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(await readFile(daemonPaths(home).lock, "utf8"));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return void 0;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function lockAlive(lock) {
|
|
53
|
+
if (!lock || !Number.isSafeInteger(lock.pid) || Math.abs((lock.bootTime ?? 0) - bootTime()) > 30) return false;
|
|
54
|
+
try {
|
|
55
|
+
process.kill(lock.pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return error?.code === "EPERM";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var AlpClient = class {
|
|
62
|
+
#socket;
|
|
63
|
+
#sequence = 0;
|
|
64
|
+
#pending = /* @__PURE__ */ new Map();
|
|
65
|
+
#events = /* @__PURE__ */ new Set();
|
|
66
|
+
#closes = /* @__PURE__ */ new Set();
|
|
67
|
+
#requestHandlers = /* @__PURE__ */ new Set();
|
|
68
|
+
#closed = false;
|
|
69
|
+
constructor(socket) {
|
|
70
|
+
this.#socket = socket;
|
|
71
|
+
let buffer = "";
|
|
72
|
+
socket.setEncoding("utf8");
|
|
73
|
+
socket.on("data", (chunk) => {
|
|
74
|
+
buffer += chunk;
|
|
75
|
+
if (buffer.length > MAX_FRAME) {
|
|
76
|
+
socket.destroy(new Error("ALP daemon frame exceeds size limit"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (let index = buffer.indexOf("\n"); index >= 0; index = buffer.indexOf("\n")) {
|
|
80
|
+
const line = buffer.slice(0, index);
|
|
81
|
+
buffer = buffer.slice(index + 1);
|
|
82
|
+
if (line.trim()) this.#receive(line);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
socket.on("close", () => this.#finish(new Error("ALP daemon connection closed")));
|
|
86
|
+
socket.on("error", (error) => this.#finish(error));
|
|
87
|
+
}
|
|
88
|
+
#receive(line) {
|
|
89
|
+
let message;
|
|
90
|
+
try {
|
|
91
|
+
message = JSON.parse(line);
|
|
92
|
+
} catch {
|
|
93
|
+
this.#socket.destroy(new Error("Invalid frame from ALP daemon"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (message.method === "event") {
|
|
97
|
+
for (const listener of this.#events) listener(message.params);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (message.method && (message.id !== void 0 && message.id !== null) && !Object.prototype.hasOwnProperty.call(message, "result") && !Object.prototype.hasOwnProperty.call(message, "error")) {
|
|
101
|
+
const handler = [...this.#requestHandlers][0];
|
|
102
|
+
if (handler) {
|
|
103
|
+
Promise.resolve().then(async () => {
|
|
104
|
+
try {
|
|
105
|
+
const result = await handler(message.method, message.params ?? {});
|
|
106
|
+
this.#socket.write(encodeFrame({ jsonrpc: "2.0", id: message.id, result: result === void 0 ? {} : result }));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const code = error && typeof error.code === "number" ? error.code : 1e3;
|
|
109
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
110
|
+
const data = error && error.data !== void 0 ? { data: error.data } : {};
|
|
111
|
+
this.#socket.write(encodeFrame({ jsonrpc: "2.0", id: message.id, error: { code, message: msg.length > 2e3 ? msg.slice(0, 2e3) + "..." : msg, ...data } }));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#socket.write(encodeFrame({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `Unknown method '${message.method}'` } }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const waiter = this.#pending.get(message.id);
|
|
120
|
+
if (!waiter) return;
|
|
121
|
+
this.#pending.delete(message.id);
|
|
122
|
+
if (message.error) waiter.reject(new AlpRpcError(message.error));
|
|
123
|
+
else waiter.resolve(message.result);
|
|
124
|
+
}
|
|
125
|
+
#finish(error) {
|
|
126
|
+
if (this.#closed) return;
|
|
127
|
+
this.#closed = true;
|
|
128
|
+
for (const waiter of this.#pending.values()) waiter.reject(error);
|
|
129
|
+
this.#pending.clear();
|
|
130
|
+
for (const listener of this.#closes) listener(error);
|
|
131
|
+
}
|
|
132
|
+
get closed() {
|
|
133
|
+
return this.#closed;
|
|
134
|
+
}
|
|
135
|
+
request(method, params = {}) {
|
|
136
|
+
if (this.#closed) return Promise.reject(new Error("ALP daemon connection closed"));
|
|
137
|
+
const id = ++this.#sequence;
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
this.#pending.set(id, { resolve, reject });
|
|
140
|
+
this.#socket.write(encodeFrame({ jsonrpc: "2.0", id, method, params }));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
onEvent(listener) {
|
|
144
|
+
this.#events.add(listener);
|
|
145
|
+
return () => this.#events.delete(listener);
|
|
146
|
+
}
|
|
147
|
+
onRequest(handler) {
|
|
148
|
+
this.#requestHandlers.add(handler);
|
|
149
|
+
return () => this.#requestHandlers.delete(handler);
|
|
150
|
+
}
|
|
151
|
+
onClose(listener) {
|
|
152
|
+
this.#closes.add(listener);
|
|
153
|
+
return () => this.#closes.delete(listener);
|
|
154
|
+
}
|
|
155
|
+
close() {
|
|
156
|
+
this.#socket.end();
|
|
157
|
+
this.#finish(new Error("ALP daemon connection closed"));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
async function connect(socketPath, client = { name: "alp", version: "0" }) {
|
|
161
|
+
const socket = await new Promise((resolve, reject) => {
|
|
162
|
+
const candidate = net.createConnection(socketPath);
|
|
163
|
+
candidate.once("connect", () => {
|
|
164
|
+
candidate.off("error", reject);
|
|
165
|
+
resolve(candidate);
|
|
166
|
+
});
|
|
167
|
+
candidate.once("error", reject);
|
|
168
|
+
});
|
|
169
|
+
const connection = new AlpClient(socket);
|
|
170
|
+
try {
|
|
171
|
+
await connection.request("daemon.hello", { protocolVersion: PROTOCOL_VERSION, client });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
connection.close();
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
return connection;
|
|
177
|
+
}
|
|
178
|
+
async function tail(file, lines = 30) {
|
|
179
|
+
try {
|
|
180
|
+
return (await readFile(file, "utf8")).split("\n").slice(-lines).join("\n");
|
|
181
|
+
} catch {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function ensureDaemon({ home = alpHome(), entry = (
|
|
186
|
+
/** @type {string | undefined} */
|
|
187
|
+
void 0
|
|
188
|
+
), env = process.env, execPath = process.execPath, timeoutMs = 15e3 } = {}) {
|
|
189
|
+
const ready = async () => {
|
|
190
|
+
const lock = await readLock(home);
|
|
191
|
+
return lockAlive(lock) && lock.ready ? lock.socket : void 0;
|
|
192
|
+
};
|
|
193
|
+
const live = await ready();
|
|
194
|
+
if (live) return live;
|
|
195
|
+
if (!lockAlive(await readLock(home))) {
|
|
196
|
+
entry ??= await findDaemonEntry({ home, env });
|
|
197
|
+
if (!entry) throw new Error("ALP daemon is not running and alpd.js was not found. Install the ALP CLI and run `alp daemon start`, or set ALP_DAEMON_ENTRY to alpd.js");
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
const launcher = spawn(execPath, [entry, "--detach"], {
|
|
200
|
+
env: { ...env, ALP_HOME: home, ELECTRON_RUN_AS_NODE: "1" },
|
|
201
|
+
stdio: "ignore",
|
|
202
|
+
windowsHide: true
|
|
203
|
+
});
|
|
204
|
+
launcher.once("error", reject);
|
|
205
|
+
launcher.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`ALP daemon launcher exited (${code})`)));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const deadline = Date.now() + timeoutMs;
|
|
209
|
+
while (Date.now() < deadline) {
|
|
210
|
+
const socket = await ready();
|
|
211
|
+
if (socket) return socket;
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
213
|
+
}
|
|
214
|
+
const log = await tail(daemonPaths(home).log);
|
|
215
|
+
throw new Error(`ALP daemon did not become ready within ${timeoutMs} ms${log ? `
|
|
216
|
+
${log}` : ""}`);
|
|
217
|
+
}
|
|
218
|
+
var INTEGRATION_DESCRIPTOR_FIELDS = ["main", "entry", "path", "command"];
|
|
219
|
+
async function readIntegrationDescriptor(file, baseDirs = []) {
|
|
220
|
+
try {
|
|
221
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
222
|
+
for (const field of INTEGRATION_DESCRIPTOR_FIELDS) {
|
|
223
|
+
const value = raw?.[field];
|
|
224
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
225
|
+
if (path.isAbsolute(value)) {
|
|
226
|
+
if (await isFile(value)) return value;
|
|
227
|
+
} else {
|
|
228
|
+
const bases = [path.dirname(file), ...baseDirs];
|
|
229
|
+
for (const base of bases) {
|
|
230
|
+
const resolved = path.resolve(base, value);
|
|
231
|
+
if (await isFile(resolved)) return resolved;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
async function integrationCandidateFromHome(home, name) {
|
|
240
|
+
const descriptors = [
|
|
241
|
+
path.join(home, "integrations", `${name}.json`),
|
|
242
|
+
path.join(home, "integrations", name, "integration.json")
|
|
243
|
+
];
|
|
244
|
+
for (const descriptor of descriptors) {
|
|
245
|
+
const entry = await readIntegrationDescriptor(descriptor, [home]);
|
|
246
|
+
if (entry) return entry;
|
|
247
|
+
}
|
|
248
|
+
const legacy = path.join(home, "integrations", name, "main.js");
|
|
249
|
+
return await isFile(legacy) ? legacy : void 0;
|
|
250
|
+
}
|
|
251
|
+
function integrationBinaryName(name) {
|
|
252
|
+
return name === "web" ? "alp-web-integration" : `alp-${name}-integration`;
|
|
253
|
+
}
|
|
254
|
+
async function distributionRoots() {
|
|
255
|
+
const roots = [];
|
|
256
|
+
const argv = process.argv[1] ? await realpath(process.argv[1]).catch(() => process.argv[1]) : void 0;
|
|
257
|
+
if (argv) {
|
|
258
|
+
let directory = path.dirname(argv);
|
|
259
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
260
|
+
roots.push(path.resolve(directory, ".."));
|
|
261
|
+
directory = path.resolve(directory, "..");
|
|
262
|
+
}
|
|
263
|
+
for (let ancestor = path.dirname(argv); ; ancestor = path.dirname(ancestor)) {
|
|
264
|
+
if (await isFile(path.join(ancestor, "package.json"))) roots.push(ancestor);
|
|
265
|
+
if (path.dirname(ancestor) === ancestor) break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
roots.push(process.cwd());
|
|
269
|
+
return [...new Set(roots)];
|
|
270
|
+
}
|
|
271
|
+
async function discoverIntegration(name = "web", options = {}) {
|
|
272
|
+
const envCandidates = [
|
|
273
|
+
process.env.ALP_WEB_INTEGRATION,
|
|
274
|
+
process.env.ALP_INTEGRATION_ENTRY,
|
|
275
|
+
name !== "web" ? process.env[`ALP_${name.toUpperCase()}_INTEGRATION`] : void 0
|
|
276
|
+
].filter(Boolean);
|
|
277
|
+
for (const candidate of envCandidates) if (await isFile(candidate)) return candidate;
|
|
278
|
+
const home = options?.home ? path.resolve(options.home) : alpHome();
|
|
279
|
+
const fromHome = await integrationCandidateFromHome(home, name);
|
|
280
|
+
if (fromHome) return fromHome;
|
|
281
|
+
for (const root of await distributionRoots()) {
|
|
282
|
+
for (const descriptorPath of [path.join(root, "integrations", `${name}.json`), path.join(root, "integrations", name, "integration.json")]) {
|
|
283
|
+
const entry = await readIntegrationDescriptor(descriptorPath, [root]);
|
|
284
|
+
if (entry) return entry;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const binary = integrationBinaryName(name);
|
|
288
|
+
for (const directory of [...(process.env.PATH ?? "").split(path.delimiter).filter(Boolean), ...BIN_DIRECTORIES]) {
|
|
289
|
+
const candidate = await realpath(path.join(directory, binary)).catch(() => void 0);
|
|
290
|
+
if (candidate && await isFile(candidate)) return candidate;
|
|
291
|
+
}
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
function integrationAlive(lock) {
|
|
295
|
+
if (!lock || !Number.isSafeInteger(lock.pid)) return false;
|
|
296
|
+
try {
|
|
297
|
+
process.kill(lock.pid, 0);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return error?.code === "EPERM";
|
|
300
|
+
}
|
|
301
|
+
if (typeof lock.bootTime === "number" && Math.abs(lock.bootTime - bootTime()) > 30) return false;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
async function startIntegration(nameOrOptions = "web") {
|
|
305
|
+
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
306
|
+
const name = options.name ?? "web";
|
|
307
|
+
const fs = await import("node:fs/promises");
|
|
308
|
+
const pathMod = await import("node:path");
|
|
309
|
+
const home = options.home ? pathMod.resolve(options.home) : alpHome();
|
|
310
|
+
const lockPath = pathMod.join(home, "integrations", `${name}.lock`);
|
|
311
|
+
const webPath = pathMod.join(home, `${name}.json`);
|
|
312
|
+
let settings = {};
|
|
313
|
+
try {
|
|
314
|
+
settings = JSON.parse(await fs.readFile(pathMod.join(home, "settings.json"), "utf8"));
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
if (settings?.web?.enabled === false) {
|
|
318
|
+
throw new Error('ALP web app is disabled by settings ("web.enabled": false).');
|
|
319
|
+
}
|
|
320
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
321
|
+
const readLock2 = async () => {
|
|
322
|
+
try {
|
|
323
|
+
return JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
324
|
+
} catch {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const readWeb = async () => {
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(await fs.readFile(webPath, "utf8"));
|
|
331
|
+
} catch {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
async function waitForReady() {
|
|
336
|
+
const deadline = Date.now() + 3e4;
|
|
337
|
+
while (Date.now() < deadline) {
|
|
338
|
+
const lock2 = await readLock2();
|
|
339
|
+
if (lock2 && integrationAlive(lock2) && lock2.ready) {
|
|
340
|
+
const info = await readWeb();
|
|
341
|
+
if (info && info.pid === lock2.pid) return lock2;
|
|
342
|
+
}
|
|
343
|
+
await sleep(100);
|
|
344
|
+
}
|
|
345
|
+
const lock = await readLock2();
|
|
346
|
+
if (lock && !lock.ready) throw new Error("ALP web integration started but did not become ready; see ~/.alp/logs/integrations/paseo.log");
|
|
347
|
+
if (!lock) throw new Error("ALP web app is provided by an optional integration package. Install it (for example `npm install -g alp-paseo-plugin`) and run `alp web` again.");
|
|
348
|
+
throw new Error("ALP web integration did not publish a matching web.json lock");
|
|
349
|
+
}
|
|
350
|
+
const existing = await readLock2();
|
|
351
|
+
if (existing && integrationAlive(existing)) {
|
|
352
|
+
return waitForReady();
|
|
353
|
+
}
|
|
354
|
+
const entry = await discoverIntegration(name, { home });
|
|
355
|
+
if (!entry) {
|
|
356
|
+
throw new Error("ALP web app is provided by an optional integration package. Install it (for example `npm install -g alp-paseo-plugin`) and run `alp web` again.");
|
|
357
|
+
}
|
|
358
|
+
const { spawn: spawn2 } = await import("node:child_process");
|
|
359
|
+
const child = spawn2(process.execPath, [entry, "--service"], { stdio: "ignore", detached: true, env: { ...process.env, ALP_HOME: home } });
|
|
360
|
+
let spawnError = null;
|
|
361
|
+
child.on("error", (error) => {
|
|
362
|
+
spawnError = error;
|
|
363
|
+
});
|
|
364
|
+
let exited = false;
|
|
365
|
+
child.on("exit", (code) => {
|
|
366
|
+
if (code !== 0 && code !== null) exited = true;
|
|
367
|
+
});
|
|
368
|
+
child.unref();
|
|
369
|
+
if (spawnError) throw spawnError;
|
|
370
|
+
await sleep(150);
|
|
371
|
+
if (spawnError) throw spawnError;
|
|
372
|
+
if (exited) throw new Error("ALP web integration failed to start; see ~/.alp/logs/integrations/paseo.log");
|
|
373
|
+
return waitForReady();
|
|
374
|
+
}
|
|
375
|
+
export {
|
|
376
|
+
AlpClient,
|
|
377
|
+
AlpRpcError2 as AlpRpcError,
|
|
378
|
+
MAX_FRAME2 as MAX_FRAME,
|
|
379
|
+
PROTOCOL_VERSION2 as PROTOCOL_VERSION,
|
|
380
|
+
alpHome,
|
|
381
|
+
bootTime,
|
|
382
|
+
connect,
|
|
383
|
+
daemonPaths,
|
|
384
|
+
discoverIntegration,
|
|
385
|
+
encodeFrame2 as encodeFrame,
|
|
386
|
+
ensureDaemon,
|
|
387
|
+
findDaemonEntry,
|
|
388
|
+
lockAlive,
|
|
389
|
+
readLock,
|
|
390
|
+
startIntegration
|
|
391
|
+
};
|
|
@@ -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: "project" | "builtin" | "library";
|
|
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: "project" | "builtin" | "library";
|
|
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: "project" | "builtin" | "library";
|
|
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: "project" | "builtin" | "library";
|
|
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
|
+
};
|