@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// packages/client/src/mcp-probe.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
var PROTOCOL = "2025-06-18";
|
|
5
|
+
var client = { name: "alp", version: "1" };
|
|
6
|
+
async function probeMcp(server, { cwd, timeoutMs = 15e3 } = {}) {
|
|
7
|
+
const session = server.url ? await httpSession(server, timeoutMs) : stdioSession(server, cwd, timeoutMs);
|
|
8
|
+
try {
|
|
9
|
+
const init = await session.request("initialize", { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: client });
|
|
10
|
+
await session.notify("notifications/initialized");
|
|
11
|
+
const tools = [];
|
|
12
|
+
let cursor;
|
|
13
|
+
for (let page = 0; page < 20; page++) {
|
|
14
|
+
const result = await session.request("tools/list", cursor ? { cursor } : {});
|
|
15
|
+
for (const tool of result.tools ?? []) tools.push({ name: tool.name, ...tool.description ? { description: tool.description } : {} });
|
|
16
|
+
cursor = result.nextCursor;
|
|
17
|
+
if (!cursor) break;
|
|
18
|
+
}
|
|
19
|
+
return { ...init.serverInfo ? { server: { name: init.serverInfo.name, version: init.serverInfo.version } } : {}, tools };
|
|
20
|
+
} finally {
|
|
21
|
+
await session.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function stdioSession(server, cwd, timeoutMs) {
|
|
25
|
+
const child = spawn(server.command, server.args ?? [], { cwd: server.cwd ?? cwd, env: { ...process.env, ...server.env ?? {} }, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
26
|
+
const signal = (name) => {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(-child.pid, name);
|
|
29
|
+
} catch {
|
|
30
|
+
child.kill(name);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
let stderr = "";
|
|
34
|
+
child.stderr.setEncoding("utf8");
|
|
35
|
+
child.stderr.on("data", (chunk) => {
|
|
36
|
+
if (stderr.length < 4e3) stderr += chunk;
|
|
37
|
+
});
|
|
38
|
+
const pending = /* @__PURE__ */ new Map();
|
|
39
|
+
let exited;
|
|
40
|
+
const failAll = (error) => {
|
|
41
|
+
for (const { reject } of pending.values()) reject(error);
|
|
42
|
+
pending.clear();
|
|
43
|
+
};
|
|
44
|
+
child.on("error", (error) => {
|
|
45
|
+
exited = error;
|
|
46
|
+
failAll(error);
|
|
47
|
+
});
|
|
48
|
+
child.on("exit", (code) => {
|
|
49
|
+
exited = new Error(`${server.command} exited (${code ?? "signal"})${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : ""}`);
|
|
50
|
+
failAll(exited);
|
|
51
|
+
});
|
|
52
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
53
|
+
let message;
|
|
54
|
+
try {
|
|
55
|
+
message = JSON.parse(line);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const waiting = pending.get(message.id);
|
|
60
|
+
if (!waiting) return;
|
|
61
|
+
pending.delete(message.id);
|
|
62
|
+
if (message.error) waiting.reject(new Error(`${message.error.message ?? "MCP error"} (${message.error.code})`));
|
|
63
|
+
else waiting.resolve(message.result ?? {});
|
|
64
|
+
});
|
|
65
|
+
child.stdin.on("error", () => {
|
|
66
|
+
});
|
|
67
|
+
let id = 0;
|
|
68
|
+
return {
|
|
69
|
+
request(method, params) {
|
|
70
|
+
if (exited) return Promise.reject(exited);
|
|
71
|
+
const current = ++id;
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
pending.delete(current);
|
|
75
|
+
reject(new Error(`${method} got no answer within ${timeoutMs / 1e3} s`));
|
|
76
|
+
}, timeoutMs);
|
|
77
|
+
pending.set(current, { resolve: (value) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
resolve(value);
|
|
80
|
+
}, reject: (error) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(error);
|
|
83
|
+
} });
|
|
84
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: current, method, params }) + "\n");
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
async notify(method) {
|
|
88
|
+
if (!exited) child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method }) + "\n");
|
|
89
|
+
},
|
|
90
|
+
async close() {
|
|
91
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
92
|
+
const done = new Promise((resolve) => child.once("exit", resolve));
|
|
93
|
+
child.stdin.end();
|
|
94
|
+
signal("SIGTERM");
|
|
95
|
+
const timer = setTimeout(() => signal("SIGKILL"), 2e3);
|
|
96
|
+
await done;
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function httpSession(server, timeoutMs) {
|
|
102
|
+
let sessionId;
|
|
103
|
+
let id = 0;
|
|
104
|
+
const post = async (body) => {
|
|
105
|
+
const response = await fetch(server.url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { ...server.headers ?? {}, "content-type": "application/json", accept: "application/json, text/event-stream", ...sessionId ? { "mcp-session-id": sessionId } : {}, "mcp-protocol-version": PROTOCOL },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
110
|
+
});
|
|
111
|
+
sessionId = response.headers.get("mcp-session-id") ?? sessionId;
|
|
112
|
+
if (!response.ok) throw new Error(`${server.url} answered ${response.status} ${response.statusText}`);
|
|
113
|
+
return response;
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async request(method, params) {
|
|
117
|
+
const current = ++id;
|
|
118
|
+
const response = await post({ jsonrpc: "2.0", id: current, method, params });
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
const messages = (response.headers.get("content-type") ?? "").includes("text/event-stream") ? text.split("\n").filter((line) => line.startsWith("data:")).map((line) => {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(line.slice(5));
|
|
123
|
+
} catch {
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
}) : [JSON.parse(text)];
|
|
127
|
+
const message = messages.find((entry) => entry?.id === current);
|
|
128
|
+
if (!message) throw new Error(`${method}: no answer in the response`);
|
|
129
|
+
if (message.error) throw new Error(`${message.error.message ?? "MCP error"} (${message.error.code})`);
|
|
130
|
+
return message.result ?? {};
|
|
131
|
+
},
|
|
132
|
+
async notify(method) {
|
|
133
|
+
await post({ jsonrpc: "2.0", method });
|
|
134
|
+
},
|
|
135
|
+
async close() {
|
|
136
|
+
if (sessionId) await fetch(server.url, { method: "DELETE", headers: { ...server.headers ?? {}, "mcp-session-id": sessionId }, signal: AbortSignal.timeout(timeoutMs) }).catch(() => {
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
probeMcp,
|
|
143
|
+
stdioSession
|
|
144
|
+
};
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A select box in the terminal: ↑/↓ (or k/j) move, Enter picks, Ctrl-C or Esc cancels.
|
|
3
|
+
* Choices are { label, value, hint? }; `initial` is the value highlighted first.
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {{ message: string, choices: Array<{ label: string, value: T, hint?: string }>, initial?: T }} question
|
|
6
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
7
|
+
* @returns {Promise<T>}
|
|
8
|
+
*/
|
|
9
|
+
export function select<T>({ message, choices, initial }: {
|
|
10
|
+
message: string;
|
|
11
|
+
choices: Array<{
|
|
12
|
+
label: string;
|
|
13
|
+
value: T;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}>;
|
|
16
|
+
initial?: T;
|
|
17
|
+
}, { input, output }?: {
|
|
18
|
+
input?: NodeJS.ReadStream;
|
|
19
|
+
output?: NodeJS.WriteStream;
|
|
20
|
+
}): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* A line of text, for a value the list does not offer.
|
|
23
|
+
* @param {string} message
|
|
24
|
+
* @param {{ input?: NodeJS.ReadStream, output?: NodeJS.WriteStream }} [io]
|
|
25
|
+
*/
|
|
26
|
+
export function text(message: string, { input, output }?: {
|
|
27
|
+
input?: NodeJS.ReadStream;
|
|
28
|
+
output?: NodeJS.WriteStream;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
export class Cancelled extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// packages/client/src/prompt.js
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
var dim = (text2) => `\x1B[2m${text2}\x1B[22m`;
|
|
4
|
+
var cyan = (text2) => `\x1B[36m${text2}\x1B[39m`;
|
|
5
|
+
var Cancelled = class extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Setup cancelled.");
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function select({ message, choices, initial }, { input = process.stdin, output = process.stdout } = {}) {
|
|
11
|
+
let index = Math.max(0, choices.findIndex((choice) => choice.value === initial));
|
|
12
|
+
let drawn = 0;
|
|
13
|
+
const draw = () => {
|
|
14
|
+
if (drawn) output.write(`\x1B[${drawn}A\x1B[0J`);
|
|
15
|
+
const lines = [`${cyan("?")} ${message} ${dim("(\u2191/\u2193, Enter)")}`, ...choices.map((choice, at) => at === index ? cyan(`\u276F ${choice.label}`) + (choice.hint ? ` ${dim(choice.hint)}` : "") : ` ${choice.label}${choice.hint ? ` ${dim(choice.hint)}` : ""}`)];
|
|
16
|
+
output.write(lines.join("\n") + "\n");
|
|
17
|
+
drawn = lines.length;
|
|
18
|
+
};
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
emitKeypressEvents(input);
|
|
21
|
+
const raw = input.isRaw;
|
|
22
|
+
input.setRawMode?.(true);
|
|
23
|
+
input.resume();
|
|
24
|
+
output.write("\x1B[?25l");
|
|
25
|
+
const done = (settle, value) => {
|
|
26
|
+
input.off("keypress", onKey);
|
|
27
|
+
input.setRawMode?.(raw ?? false);
|
|
28
|
+
input.pause();
|
|
29
|
+
output.write(`\x1B[${drawn}A\x1B[0J\x1B[?25h`);
|
|
30
|
+
if (settle === resolve) output.write(`${cyan("\u2714")} ${message} ${cyan(choices[index].label)}
|
|
31
|
+
`);
|
|
32
|
+
settle(value);
|
|
33
|
+
};
|
|
34
|
+
const onKey = (text2, key = {}) => {
|
|
35
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") return done(reject, new Cancelled());
|
|
36
|
+
if (key.name === "return" || key.name === "enter") return done(resolve, choices[index].value);
|
|
37
|
+
if (key.name === "up" || key.name === "k") index = (index + choices.length - 1) % choices.length;
|
|
38
|
+
else if (key.name === "down" || key.name === "j") index = (index + 1) % choices.length;
|
|
39
|
+
else return;
|
|
40
|
+
draw();
|
|
41
|
+
};
|
|
42
|
+
input.on("keypress", onKey);
|
|
43
|
+
draw();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function text(message, { input = process.stdin, output = process.stdout } = {}) {
|
|
47
|
+
const { createInterface } = await import("node:readline/promises");
|
|
48
|
+
const rl = createInterface({ input, output });
|
|
49
|
+
try {
|
|
50
|
+
return (await rl.question(`${cyan("?")} ${message} `)).trim();
|
|
51
|
+
} finally {
|
|
52
|
+
rl.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
Cancelled,
|
|
57
|
+
select,
|
|
58
|
+
text
|
|
59
|
+
};
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** `alp ps`: sessions as a tree, one line each. */
|
|
2
|
+
export function renderPs(sessions: any): any[];
|
|
3
|
+
/** `alp log`: one line per run-log entry, with its time. */
|
|
4
|
+
export function renderLog(rootId: any, entries: any): string[];
|
|
5
|
+
export function ago(since: any, now?: number): string;
|
|
6
|
+
export function duration(ms: any): string;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// packages/client/src/render.js
|
|
2
|
+
var TASK_PAST = { create: "created", update: "updated", link: "linked", start: "started", close: "closed", reopen: "reopened", delegate: "delegated", submit: "submitted", release: "released", gate: "gated", clear: "cleared a gate of", orphaned: "reopened (its assignment ended with alpd)" };
|
|
3
|
+
var ago = (since, now = Date.now()) => {
|
|
4
|
+
const seconds = Math.max(0, Math.round((now - Date.parse(since)) / 1e3));
|
|
5
|
+
return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s` : `${Math.floor(seconds / 3600)}h${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
6
|
+
};
|
|
7
|
+
var duration = (ms) => ago((/* @__PURE__ */ new Date(0)).toISOString(), ms);
|
|
8
|
+
var tokens = (count) => typeof count !== "number" ? "?" : count >= 1e6 && count % 1e6 < 5e4 ? `${Math.round(count / 1e6)}M` : `${Math.round(count / 1e3)}k`;
|
|
9
|
+
function renderPs(sessions) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
const children = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const session of sessions) {
|
|
13
|
+
const key = session.parentId ?? "";
|
|
14
|
+
children.set(key, [...children.get(key) ?? [], session]);
|
|
15
|
+
}
|
|
16
|
+
const print = (session, depth) => {
|
|
17
|
+
const status = session.parked ? `parked (${session.parked})` : session.status === "running" || session.status === "idle" ? session.activeTurnId ? "running" : session.busy ? "waiting" : "idle" : session.lastError?.code ?? session.status;
|
|
18
|
+
lines.push(`${" ".repeat(depth)}${session.id} ${session.agent} ${session.runtime}:${session.model} ${session.mode} ${status}${depth ? "" : ` ${session.projectRoot}${session.title ? ` "${session.title}"` : ""}`}`);
|
|
19
|
+
for (const child of children.get(session.id) ?? []) print(child, depth + 1);
|
|
20
|
+
};
|
|
21
|
+
for (const root of children.get("") ?? []) print(root, 0);
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function renderLog(rootId, entries) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const agents = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const time = entry.ts.slice(11, 19);
|
|
29
|
+
let text;
|
|
30
|
+
switch (entry.event) {
|
|
31
|
+
case "assignment.started":
|
|
32
|
+
agents.set(entry.assignmentId, entry.agent);
|
|
33
|
+
text = `${entry.parentAgent} \u2192 ${entry.agent} (${entry.mode}${entry.isolation === "worktree" ? ", worktree" : ""}, ${entry.model}${entry.wait === false ? ", async" : ""}): ${entry.task.split("\n")[0].slice(0, 120)}`;
|
|
34
|
+
break;
|
|
35
|
+
case "assignment.finished":
|
|
36
|
+
text = `${entry.agent} ${entry.status} after ${duration(entry.durationMs ?? 0)}${entry.handoff ? `, handoff ${entry.handoff.outcome}${entry.handoff.verdict ? `, verdict ${entry.handoff.verdict.result.toUpperCase()}` : ""}: ${entry.handoff.summary.split("\n")[0].slice(0, 120)}` : ""}${entry.error ? `: ${entry.error}` : ""}${entry.reconciled ? " (after a restart)" : ""}`;
|
|
37
|
+
break;
|
|
38
|
+
case "mail":
|
|
39
|
+
if (entry.kind === "board") continue;
|
|
40
|
+
text = `\u2709 ${entry.kind} ${entry.from} \u2192 ${entry.to === rootId ? "main" : agents.get(entry.to) ?? entry.to}${entry.body ? `: ${entry.body.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
41
|
+
break;
|
|
42
|
+
case "board.pin":
|
|
43
|
+
text = `${entry.kind === "claim" ? "\u2691" : entry.kind === "decision" ? "\u25C6" : "\u2022"} ${entry.agent} pins ${entry.kind} ${entry.pinId}${entry.task ? ` for ${entry.task}` : ""}${entry.paths ? ` [${entry.paths.join(", ")}]` : ""}: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
44
|
+
break;
|
|
45
|
+
case "task":
|
|
46
|
+
text = `\u2610 ${entry.agent} ${TASK_PAST[entry.action] ?? entry.action} ${entry.id} "${entry.title}" \u2192 ${entry.status}${entry.detail ? `: ${entry.detail.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
47
|
+
break;
|
|
48
|
+
case "board.unpin":
|
|
49
|
+
text = `\u2690 ${entry.pinId} by ${entry.agent} ${entry.reason === "session_ended" ? "released when its session ended" : "taken down"}`;
|
|
50
|
+
break;
|
|
51
|
+
case "human.question":
|
|
52
|
+
text = `? ${entry.agent} asks the user [${entry.questionId}]: ${entry.body.split("\n")[0].slice(0, 120)}`;
|
|
53
|
+
break;
|
|
54
|
+
case "verify":
|
|
55
|
+
text = `${entry.skipped ? "\u2013" : entry.passed ? "\u2713" : "\u2717"} verify in the ${entry.where}${entry.taskId ? ` for ${entry.taskId}` : ""}${entry.assignmentId ? ` (${entry.assignmentId})` : ""}: ${entry.skipped ? `skipped: ${entry.skipped}` : entry.detail}`;
|
|
56
|
+
break;
|
|
57
|
+
case "epic.landed":
|
|
58
|
+
text = `\u25C6 ${entry.agent} closed ${entry.id} "${entry.title}": ${entry.tasks} tasks, ${entry.reworked} reworked`;
|
|
59
|
+
break;
|
|
60
|
+
case "notice":
|
|
61
|
+
text = `${entry.level === "error" ? "\u203C" : entry.level === "warning" ? "!" : "\u2139"} ${entry.text}`;
|
|
62
|
+
break;
|
|
63
|
+
case "assignment.parked":
|
|
64
|
+
text = `\u23F8 ${entry.agent} ${entry.assignmentId} parked: ${entry.reason}`;
|
|
65
|
+
break;
|
|
66
|
+
case "assignment.resumed":
|
|
67
|
+
text = `\u25B6 ${entry.agent} ${entry.assignmentId} resumed`;
|
|
68
|
+
break;
|
|
69
|
+
case "instructions":
|
|
70
|
+
text = `# ${entry.agent} instructions ${entry.sha} (ALP.md ${entry.parts?.project}, AGENT.md ${entry.parts?.agent}, ${entry.chars} chars)`;
|
|
71
|
+
break;
|
|
72
|
+
case "context":
|
|
73
|
+
text = entry.level === "soon" ? `\u25D4 ${entry.agent} context ${tokens(entry.tokens)}, compacts at ${tokens(entry.compactAt)}: told it is near` : `\u25D4 ${entry.agent} context ${entry.percent}% full: ${entry.level === "now" ? "told to hand off now" : "told to plan a handoff"}`;
|
|
74
|
+
break;
|
|
75
|
+
case "skill":
|
|
76
|
+
text = Array.isArray(entry.roles) ? `\u2726 ${entry.agent} saved skill ${entry.name} for ${entry.roles.join(", ")}` : `\u2726 ${entry.agent} uses skill ${entry.skill}${entry.via === "skill" ? " (Skill tool)" : ""}`;
|
|
77
|
+
break;
|
|
78
|
+
case "compacted":
|
|
79
|
+
text = entry.failed ? `\u2717 ${entry.agent}'s context could not be compacted${entry.error ? `: ${entry.error}` : ""}` : `\u25D1 ${entry.agent}'s context compacted${entry.preTokens !== void 0 ? ` from ${tokens(entry.preTokens)}` : ""}${entry.postTokens !== void 0 ? ` to ${tokens(entry.postTokens)}` : ""}${entry.trigger === "manual" ? " on request" : ""}`;
|
|
80
|
+
break;
|
|
81
|
+
case "assignment.interrupted":
|
|
82
|
+
text = `\u23F9 ${entry.agent} ${entry.assignmentId} interrupted: ${entry.reason}; the next alpd continues it`;
|
|
83
|
+
break;
|
|
84
|
+
case "assignment.recovered":
|
|
85
|
+
text = `\u21BB ${entry.agent} ${entry.assignmentId} reopened after alpd restarted`;
|
|
86
|
+
break;
|
|
87
|
+
case "session.restarted":
|
|
88
|
+
text = `\u21BB ${entry.agent}'s native process stopped (${entry.error}); restart ${entry.restarts}`;
|
|
89
|
+
break;
|
|
90
|
+
case "session.revived":
|
|
91
|
+
text = `\u25B6 ${entry.agent} running again`;
|
|
92
|
+
break;
|
|
93
|
+
case "session.revive_failed":
|
|
94
|
+
text = `\u2717 ${entry.agent} could not be restarted: ${entry.error}`;
|
|
95
|
+
break;
|
|
96
|
+
case "human.answer":
|
|
97
|
+
text = `\u21B3 ${entry.questionId} ${entry.outcome}${entry.answer !== void 0 ? `: ${entry.answer.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
98
|
+
break;
|
|
99
|
+
case "hook":
|
|
100
|
+
text = entry.skipped ? `\u21AA ${entry.agent}'s ${entry.on} hook ${entry.hook} skipped: the workspace's hooks are not trusted` : `${entry.blocked ? "\u26D4" : entry.error || entry.exitCode !== 0 || entry.timedOut ? "\u2717" : "\u21AA"} ${entry.agent}'s ${entry.on} hook ${entry.hook} ${entry.error ? `could not run: ${entry.error}` : entry.timedOut ? "timed out" : `exit ${entry.exitCode}`}${entry.durationMs !== void 0 ? ` in ${duration(entry.durationMs)}` : ""}${entry.blocked ? ", refused it" : ""}${entry.output ? `: ${entry.output.split("\n")[0].slice(0, 120)}` : ""}`;
|
|
101
|
+
break;
|
|
102
|
+
case "hook.trust":
|
|
103
|
+
text = `${entry.trusted ? "\u2713 the user trusts" : "\u2717 the user did not trust"} the hooks of ${entry.project}${entry.outcome ? ` (${entry.outcome})` : ""}`;
|
|
104
|
+
break;
|
|
105
|
+
case "runtime.slip":
|
|
106
|
+
text = `\u26A0 ALP could not handle ${entry.agent}'s ${entry.method}: ${entry.error}; the session went on`;
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
text = `${entry.event}${entry.branch ? ` ${entry.branch}` : ""}${entry.status ? ` ${entry.status}` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
lines.push(`${time} ${text}`);
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
ago,
|
|
117
|
+
duration,
|
|
118
|
+
renderLog,
|
|
119
|
+
renderPs
|
|
120
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where and how alpd for `home` is defined as a service on `platform`, or undefined
|
|
3
|
+
* where ALP has no service support. Each ALP_HOME gets its own service.
|
|
4
|
+
*/
|
|
5
|
+
export function serviceFor({ home, env, platform, uid }: {
|
|
6
|
+
home: any;
|
|
7
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
8
|
+
platform?: NodeJS.Platform | undefined;
|
|
9
|
+
uid?: number | undefined;
|
|
10
|
+
}): {
|
|
11
|
+
kind: string;
|
|
12
|
+
name: string;
|
|
13
|
+
file: string;
|
|
14
|
+
install: (string | string[] | {
|
|
15
|
+
ignore: boolean;
|
|
16
|
+
})[][];
|
|
17
|
+
start: (string | string[])[][];
|
|
18
|
+
uninstall: (string | string[] | {
|
|
19
|
+
ignore: boolean;
|
|
20
|
+
})[][];
|
|
21
|
+
afterUninstall?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
kind: string;
|
|
24
|
+
name: string;
|
|
25
|
+
file: string;
|
|
26
|
+
install: (string | string[])[][];
|
|
27
|
+
start: (string | string[])[][];
|
|
28
|
+
uninstall: (string | string[] | {
|
|
29
|
+
ignore: boolean;
|
|
30
|
+
})[][];
|
|
31
|
+
afterUninstall: (string | string[] | {
|
|
32
|
+
ignore: boolean;
|
|
33
|
+
})[][];
|
|
34
|
+
} | undefined;
|
|
35
|
+
/** The service definition that runs `entry` with `execPath` for `home`. */
|
|
36
|
+
export function serviceDefinition(service: any, { home, entry, execPath, env }: {
|
|
37
|
+
home: any;
|
|
38
|
+
entry: any;
|
|
39
|
+
execPath?: string | undefined;
|
|
40
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
41
|
+
}): string;
|
|
42
|
+
/** The program an installed definition runs: [node, alpd.js], or undefined when it is not installed. */
|
|
43
|
+
export function installedProgram(service: any): Promise<any[] | undefined>;
|
|
44
|
+
/** Writes the definition and loads it, which starts alpd. */
|
|
45
|
+
export function installService(service: any, options: any, run?: typeof runCommand): Promise<void>;
|
|
46
|
+
/** Starts an installed service's alpd. */
|
|
47
|
+
export function startService(service: any, run?: typeof runCommand): Promise<void>;
|
|
48
|
+
/** Unloads the service, which stops its alpd, and removes the definition; false when none was installed. */
|
|
49
|
+
export function uninstallService(service: any, run?: typeof runCommand): Promise<boolean>;
|
|
50
|
+
import { runCommand } from './command.js';
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// packages/client/src/service.js
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { runCommand } from "./command.js";
|
|
7
|
+
var CARRIED = ["ALP_RUN_LOG_DIR", "ALP_CODEX_BIN", "ALP_CLAUDE_BIN", "ALP_ANTIGRAVITY_BIN", "ALP_GH_BIN"];
|
|
8
|
+
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
9
|
+
var quoteUnit = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
10
|
+
function serviceFor({ home, env = process.env, platform = process.platform, uid = process.getuid?.() ?? 0 }) {
|
|
11
|
+
const custom = path.resolve(home) !== path.join(os.homedir(), ".alp");
|
|
12
|
+
const suffix = custom ? createHash("sha256").update(path.resolve(home)).digest("hex").slice(0, 8) : "";
|
|
13
|
+
if (platform === "darwin") {
|
|
14
|
+
const label = `com.alp.alpd${suffix ? `.${suffix}` : ""}`;
|
|
15
|
+
const directory = env.ALP_SERVICE_DIR || path.join(os.homedir(), "Library", "LaunchAgents");
|
|
16
|
+
const launchctl = env.ALP_LAUNCHCTL || "launchctl";
|
|
17
|
+
const target = `gui/${uid}/${label}`;
|
|
18
|
+
const file = path.join(directory, `${label}.plist`);
|
|
19
|
+
return {
|
|
20
|
+
kind: "launchd",
|
|
21
|
+
name: label,
|
|
22
|
+
file,
|
|
23
|
+
install: [[launchctl, ["bootout", target], { ignore: true }], [launchctl, ["bootstrap", `gui/${uid}`, file]]],
|
|
24
|
+
start: [[launchctl, ["kickstart", target]]],
|
|
25
|
+
uninstall: [[launchctl, ["bootout", target], { ignore: true }]]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (platform === "linux") {
|
|
29
|
+
const name = `alpd${suffix ? `-${suffix}` : ""}`;
|
|
30
|
+
const directory = env.ALP_SERVICE_DIR || path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "systemd", "user");
|
|
31
|
+
const systemctl = env.ALP_SYSTEMCTL || "systemctl";
|
|
32
|
+
return {
|
|
33
|
+
kind: "systemd",
|
|
34
|
+
name,
|
|
35
|
+
file: path.join(directory, `${name}.service`),
|
|
36
|
+
install: [[systemctl, ["--user", "daemon-reload"]], [systemctl, ["--user", "enable", "--now", `${name}.service`]]],
|
|
37
|
+
start: [[systemctl, ["--user", "start", `${name}.service`]]],
|
|
38
|
+
uninstall: [[systemctl, ["--user", "disable", "--now", `${name}.service`], { ignore: true }]],
|
|
39
|
+
afterUninstall: [[systemctl, ["--user", "daemon-reload"], { ignore: true }]]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
function serviceDefinition(service, { home, entry, execPath = process.execPath, env = process.env }) {
|
|
45
|
+
const variables = { ALP_HOME: path.resolve(home), PATH: env.PATH ?? "" };
|
|
46
|
+
for (const name of CARRIED) if (env[name]) variables[name] = env[name];
|
|
47
|
+
const program = [execPath, entry, "--service"];
|
|
48
|
+
const fatal = path.join(path.resolve(home), "logs", "alpd.service.log");
|
|
49
|
+
if (service.kind === "launchd") {
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
52
|
+
<plist version="1.0">
|
|
53
|
+
<dict>
|
|
54
|
+
<key>Label</key>
|
|
55
|
+
<string>${escapeXml(service.name)}</string>
|
|
56
|
+
<key>ProgramArguments</key>
|
|
57
|
+
<array>
|
|
58
|
+
${program.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n")}
|
|
59
|
+
</array>
|
|
60
|
+
<key>EnvironmentVariables</key>
|
|
61
|
+
<dict>
|
|
62
|
+
${Object.entries(variables).map(([key, value]) => ` <key>${escapeXml(key)}</key>
|
|
63
|
+
<string>${escapeXml(value)}</string>`).join("\n")}
|
|
64
|
+
</dict>
|
|
65
|
+
<key>RunAtLoad</key>
|
|
66
|
+
<true/>
|
|
67
|
+
<key>KeepAlive</key>
|
|
68
|
+
<dict>
|
|
69
|
+
<key>Crashed</key>
|
|
70
|
+
<true/>
|
|
71
|
+
<key>SuccessfulExit</key>
|
|
72
|
+
<false/>
|
|
73
|
+
</dict>
|
|
74
|
+
<key>ThrottleInterval</key>
|
|
75
|
+
<integer>10</integer>
|
|
76
|
+
<key>ProcessType</key>
|
|
77
|
+
<string>Background</string>
|
|
78
|
+
<key>StandardOutPath</key>
|
|
79
|
+
<string>${escapeXml(fatal)}</string>
|
|
80
|
+
<key>StandardErrorPath</key>
|
|
81
|
+
<string>${escapeXml(fatal)}</string>
|
|
82
|
+
</dict>
|
|
83
|
+
</plist>
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
return `[Unit]
|
|
87
|
+
Description=ALP daemon (alpd) for ${path.resolve(home)}
|
|
88
|
+
|
|
89
|
+
[Service]
|
|
90
|
+
ExecStart=${program.map(quoteUnit).join(" ")}
|
|
91
|
+
${Object.entries(variables).map(([key, value]) => `Environment=${quoteUnit(`${key}=${value}`)}`).join("\n")}
|
|
92
|
+
Restart=on-failure
|
|
93
|
+
RestartSec=10
|
|
94
|
+
StandardOutput=append:${fatal}
|
|
95
|
+
StandardError=append:${fatal}
|
|
96
|
+
|
|
97
|
+
[Install]
|
|
98
|
+
WantedBy=default.target
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
async function installedProgram(service) {
|
|
102
|
+
const text = await readFile(service.file, "utf8").catch(() => void 0);
|
|
103
|
+
if (text === void 0) return void 0;
|
|
104
|
+
if (service.kind === "launchd") {
|
|
105
|
+
const array = text.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
106
|
+
const unescape = (value) => value.replace(/"/g, '"').replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
|
|
107
|
+
return [...array.matchAll(/<string>([\s\S]*?)<\/string>/g)].map((match) => unescape(match[1])).slice(0, 2);
|
|
108
|
+
}
|
|
109
|
+
const line = text.match(/^ExecStart=(.*)$/m)?.[1] ?? "";
|
|
110
|
+
return [...line.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1].replace(/\\(.)/g, "$1")).slice(0, 2);
|
|
111
|
+
}
|
|
112
|
+
async function runAll(steps, run) {
|
|
113
|
+
for (const [command, args, { ignore = false } = {}] of steps) {
|
|
114
|
+
const result = await run(command, args);
|
|
115
|
+
if (result.code !== 0 && !ignore) throw new Error(`${command} ${args.join(" ")} failed: ${(result.stderr || result.stdout).trim() || `exit ${result.code}`}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function installService(service, options, run = runCommand) {
|
|
119
|
+
await mkdir(path.dirname(service.file), { recursive: true });
|
|
120
|
+
await mkdir(path.join(path.resolve(options.home), "logs"), { recursive: true, mode: 448 });
|
|
121
|
+
await writeFile(service.file, serviceDefinition(service, options), { mode: 420 });
|
|
122
|
+
await runAll(service.install, run);
|
|
123
|
+
}
|
|
124
|
+
async function startService(service, run = runCommand) {
|
|
125
|
+
await runAll(service.start, run);
|
|
126
|
+
}
|
|
127
|
+
async function uninstallService(service, run = runCommand) {
|
|
128
|
+
if (await installedProgram(service) === void 0) return false;
|
|
129
|
+
await runAll(service.uninstall, run);
|
|
130
|
+
await unlink(service.file).catch((error) => {
|
|
131
|
+
if (error.code !== "ENOENT") throw error;
|
|
132
|
+
});
|
|
133
|
+
await runAll(service.afterUninstall ?? [], run);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
installService,
|
|
138
|
+
installedProgram,
|
|
139
|
+
serviceDefinition,
|
|
140
|
+
serviceFor,
|
|
141
|
+
startService,
|
|
142
|
+
uninstallService
|
|
143
|
+
};
|
package/dist/setup.d.ts
ADDED