@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/setup.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// packages/client/src/setup.js
|
|
2
|
+
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { initProject } from "@anhlp/alp-core/init";
|
|
6
|
+
import { MODELS } from "@anhlp/alp-core/models";
|
|
7
|
+
import { fitEffort, modelEfforts } from "@anhlp/alp-core/profiles";
|
|
8
|
+
import { resolveTeam } from "@anhlp/alp-core/teams";
|
|
9
|
+
import { validateSettings } from "@anhlp/alp-core/validation";
|
|
10
|
+
import { Cancelled, select, text } from "./prompt.js";
|
|
11
|
+
var PROVIDERS = ["claude", "codex"];
|
|
12
|
+
var PROVIDER_LABEL = { claude: "Claude", codex: "Codex" };
|
|
13
|
+
var DEFAULTS = {
|
|
14
|
+
main: { claude: ["claude-opus-5-5", "high"], codex: ["gpt-6.1-sol", "high"] },
|
|
15
|
+
worker: { claude: ["claude-opus-5-5", "high"], codex: ["gpt-6.1-sol", "high"] },
|
|
16
|
+
supervisor: { claude: ["claude-sonnet-5", "medium"], codex: ["gpt-6-luna", "medium"] },
|
|
17
|
+
oracle: { claude: ["claude-fable-5-1", "high"], codex: ["gpt-6-astra", "high"] }
|
|
18
|
+
};
|
|
19
|
+
var FLAGS = { main: "model", worker: "model", supervisor: "supervisor", oracle: "oracle" };
|
|
20
|
+
var SEAT_LABEL = { main: "main", worker: "lead, reviewer", supervisor: "supervisor", oracle: "oracle" };
|
|
21
|
+
var ANY_EFFORT = ["low", "medium", "high", "xhigh", "max"];
|
|
22
|
+
async function setup(args, home) {
|
|
23
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: Object.fromEntries(
|
|
24
|
+
["runtime", "prefer", ...PROVIDERS.flatMap((provider) => Object.values(FLAGS).map((flag) => `${provider}-${flag}`))].map((name) => [name, { type: "string" }])
|
|
25
|
+
) });
|
|
26
|
+
if (positionals.length > 1) throw new Error("setup takes at most one directory");
|
|
27
|
+
const root = path.resolve(positionals[0] ?? process.cwd());
|
|
28
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
29
|
+
try {
|
|
30
|
+
const runtime = values.runtime ?? (interactive ? await select({ message: "Runtime", initial: "mix", choices: [
|
|
31
|
+
{ label: "Mix", value: "mix", hint: "Claude and Codex: main runs on one, the others can run on either" },
|
|
32
|
+
{ label: "Codex only", value: "codex" },
|
|
33
|
+
{ label: "Claude only", value: "claude" }
|
|
34
|
+
] }) : void 0);
|
|
35
|
+
if (runtime === void 0) throw new Error("Pass --runtime codex, claude or mix when setup is not running in a terminal");
|
|
36
|
+
if (!["codex", "claude", "mix"].includes(runtime)) throw new Error("--runtime must be codex, claude or mix");
|
|
37
|
+
const prefer = values.prefer ?? (runtime !== "mix" ? runtime : interactive ? await select({ message: "Main runs on", initial: "claude", choices: [
|
|
38
|
+
{ label: "Claude", value: "claude", hint: "Claude first for the others too" },
|
|
39
|
+
{ label: "Codex", value: "codex", hint: "Codex first for the others too" }
|
|
40
|
+
] }) : "claude");
|
|
41
|
+
if (!PROVIDERS.includes(prefer) || runtime !== "mix" && prefer !== runtime) throw new Error("--prefer must be an enabled runtime");
|
|
42
|
+
const enabled = runtime === "mix" ? [prefer, ...PROVIDERS.filter((provider) => provider !== prefer)] : [runtime];
|
|
43
|
+
for (const provider of PROVIDERS) {
|
|
44
|
+
for (const flag of Object.values(FLAGS)) {
|
|
45
|
+
if (!enabled.includes(provider) && values[`${provider}-${flag}`]) throw new Error(`--${provider}-${flag} is unavailable in ${runtime}-only mode`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const seats = {};
|
|
49
|
+
for (const [seat, flag] of Object.entries(FLAGS)) {
|
|
50
|
+
seats[seat] = (seat === "main" ? [prefer] : enabled).map((provider) => {
|
|
51
|
+
const option = `${provider}-${flag}`;
|
|
52
|
+
const [fallback, effort] = DEFAULTS[seat][provider];
|
|
53
|
+
const model = nativeModel(values[option] ?? fallback, provider, `--${option}`);
|
|
54
|
+
return { provider, model, effort: fitEffort(`${provider}:${model}`, effort) };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const peers = enabled.map((provider) => `${provider}:*`);
|
|
58
|
+
const file = path.join(root, ".alp/settings.json");
|
|
59
|
+
const settings = await readFile(file, "utf8").then((content) => {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(content);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`${file} is not valid JSON: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}, (error) => {
|
|
66
|
+
if (error.code === "ENOENT") return void 0;
|
|
67
|
+
throw new Error(`${file}: ${error.message}`);
|
|
68
|
+
});
|
|
69
|
+
const show = () => {
|
|
70
|
+
for (const [seat, choices] of Object.entries(seats)) console.log(` ${SEAT_LABEL[seat]}: ${choices.map(describe).join(", ")}`);
|
|
71
|
+
console.log(` peer: ${peers.join(", ")} (any model: whoever assigns a peer picks its model and effort for the task)`);
|
|
72
|
+
};
|
|
73
|
+
console.log(`Profiles for Ph\u1EDF and Cafe in ${file}:`);
|
|
74
|
+
show();
|
|
75
|
+
const replaced = ["pho", "cafe"].filter((id) => (settings?.team ?? settings?.profile)?.[id]);
|
|
76
|
+
if (replaced.length) console.log(`These replace the ${replaced.join(" and ")} profiles already in the file; a backup is kept in ${file}.bak.`);
|
|
77
|
+
if (interactive) {
|
|
78
|
+
for (; ; ) {
|
|
79
|
+
const next2 = await select({ message: "Save these profiles?", initial: "save", choices: [
|
|
80
|
+
{ label: "Save", value: "save" },
|
|
81
|
+
{ label: "Change a model", value: "change" },
|
|
82
|
+
{ label: "Cancel", value: "cancel" }
|
|
83
|
+
] });
|
|
84
|
+
if (next2 === "cancel") throw new Cancelled();
|
|
85
|
+
if (next2 === "save") break;
|
|
86
|
+
await change(seats);
|
|
87
|
+
show();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const profile = await profiles(root, home, seats, peers, settings);
|
|
91
|
+
validateSettings({ ...settings ?? {}, team: { ...settings?.profile ?? {}, ...settings?.team ?? {}, ...profile } }, file);
|
|
92
|
+
if (!settings) await initProject(root, { library: home });
|
|
93
|
+
else if (replaced.length) await copyFile(file, `${file}.bak`);
|
|
94
|
+
const written = settings ?? JSON.parse(await readFile(file, "utf8"));
|
|
95
|
+
const next = { ...written, team: { ...written.profile ?? {}, ...written.team ?? {}, ...profile } };
|
|
96
|
+
delete next.profile;
|
|
97
|
+
await writeFile(file, JSON.stringify(next, null, 2) + "\n");
|
|
98
|
+
console.log("Setup saved. New sessions in this project use these profiles; edit the file to change one agent. Run alp doctor to check that each CLI is installed and logged in.");
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!(error instanceof Cancelled)) throw error;
|
|
101
|
+
console.log(error.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
var describe = (choice) => `${choice.provider}:${choice.model}${choice.effort ? `:${choice.effort}` : ""}`;
|
|
105
|
+
async function change(seats) {
|
|
106
|
+
const [seat, at] = await select({ message: "Which model?", choices: Object.entries(seats).flatMap(([seat2, choices]) => choices.map((choice, at2) => ({ label: `${SEAT_LABEL[seat2]} \xB7 ${PROVIDER_LABEL[choice.provider]}`, value: [seat2, at2], hint: describe(choice) }))) });
|
|
107
|
+
const current = seats[seat][at];
|
|
108
|
+
const offered = MODELS.filter((model2) => model2.runtime === current.provider);
|
|
109
|
+
const picked = await select({ message: `${SEAT_LABEL[seat]} on ${PROVIDER_LABEL[current.provider]}`, initial: current.model, choices: [
|
|
110
|
+
...offered.map((model2) => ({ label: model2.label, value: model2.id, hint: `${model2.id} \xB7 ${model2.description}` })),
|
|
111
|
+
{ label: "Other\u2026", value: void 0, hint: "type a model ID" }
|
|
112
|
+
] });
|
|
113
|
+
const model = picked ?? nativeModel(await text(`${PROVIDER_LABEL[current.provider]} model ID:`), current.provider, "The model ID");
|
|
114
|
+
const efforts = modelEfforts(`${current.provider}:${model}`) ?? ANY_EFFORT;
|
|
115
|
+
const effort = efforts.length > 1 ? await select({
|
|
116
|
+
message: "Effort",
|
|
117
|
+
initial: efforts.includes(current.effort) ? current.effort : DEFAULTS[seat][current.provider][1],
|
|
118
|
+
choices: efforts.map((effort2) => ({ label: effort2, value: effort2 }))
|
|
119
|
+
}) : efforts[0];
|
|
120
|
+
seats[seat][at] = { provider: current.provider, model, ...effort ? { effort } : {} };
|
|
121
|
+
}
|
|
122
|
+
async function profiles(root, home, seats, peers, settings) {
|
|
123
|
+
const profile = {};
|
|
124
|
+
for (const id of ["pho", "cafe"]) {
|
|
125
|
+
const team = await resolveTeam(root, id, { library: home });
|
|
126
|
+
const agents = {};
|
|
127
|
+
const seat = (name, choices) => {
|
|
128
|
+
agents[name] = { profiles: choices.map((choice) => typeof choice === "string" ? choice : describe(choice)), autocompact: (settings?.team ?? settings?.profile)?.[id]?.[name]?.autocompact ?? "auto" };
|
|
129
|
+
};
|
|
130
|
+
for (const [name, member] of Object.entries(team.members)) {
|
|
131
|
+
if (name !== "oracle") seat(name, name === team.main ? seats.main : member.role === "peer" ? peers : seats.worker);
|
|
132
|
+
}
|
|
133
|
+
if (team.supervisor) seat(team.supervisor.agent, seats.supervisor);
|
|
134
|
+
if (team.members.oracle) seat("oracle", seats.oracle);
|
|
135
|
+
profile[id] = agents;
|
|
136
|
+
}
|
|
137
|
+
return profile;
|
|
138
|
+
}
|
|
139
|
+
function nativeModel(id, provider, where) {
|
|
140
|
+
const model = id.startsWith(`${provider}:`) ? id.slice(provider.length + 1) : id;
|
|
141
|
+
if (!model || /\s/.test(model) || /^(codex|claude):/.test(model) || model.includes(":") && !id.startsWith(`${provider}:`)) throw new Error(`${where} expects a ${provider} model, got '${id}'`);
|
|
142
|
+
return model;
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
setup
|
|
146
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function holdDaemon(home: string, by?: string): Promise<void>;
|
|
2
|
+
export function releaseHold(home: string): Promise<void>;
|
|
3
|
+
export function daemonHeld(home: string): Promise<boolean>;
|
|
4
|
+
export function startDaemon(options: { home: string; entry?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }): Promise<string>;
|
|
5
|
+
export function superviseDaemon(options: {
|
|
6
|
+
home: string;
|
|
7
|
+
entry?: string | (() => string | undefined);
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
intervalMs?: number;
|
|
10
|
+
misses?: number;
|
|
11
|
+
log?: (message: string) => void;
|
|
12
|
+
}): (() => Promise<void>) & { started: Promise<void> };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// packages/client/src/supervise.js
|
|
2
|
+
import { mkdir, unlink, writeFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDaemon, lockAlive, readLock } from "./index.js";
|
|
5
|
+
import { installedProgram, serviceFor, startService } from "./service.js";
|
|
6
|
+
var holdFile = (home) => path.join(home, "state", "alpd.held");
|
|
7
|
+
async function holdDaemon(home, by = "alp daemon stop") {
|
|
8
|
+
await mkdir(path.dirname(holdFile(home)), { recursive: true, mode: 448 });
|
|
9
|
+
await writeFile(holdFile(home), JSON.stringify({ by, at: (/* @__PURE__ */ new Date()).toISOString() }) + "\n", { mode: 384 });
|
|
10
|
+
}
|
|
11
|
+
async function releaseHold(home) {
|
|
12
|
+
await unlink(holdFile(home)).catch(() => {
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function daemonHeld(home) {
|
|
16
|
+
return stat(holdFile(home)).then(() => true, () => false);
|
|
17
|
+
}
|
|
18
|
+
async function whenReady(home, timeoutMs) {
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
const lock = await readLock(home);
|
|
22
|
+
if (lockAlive(lock) && lock.ready) return lock.socket;
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`alpd did not become ready within ${timeoutMs / 1e3} s; see ${path.join(home, "logs")}`);
|
|
26
|
+
}
|
|
27
|
+
async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3 }) {
|
|
28
|
+
await releaseHold(home);
|
|
29
|
+
const lock = await readLock(home);
|
|
30
|
+
if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
|
|
31
|
+
const service = serviceFor({ home, env });
|
|
32
|
+
if (service && await installedProgram(service)) {
|
|
33
|
+
await startService(service);
|
|
34
|
+
return whenReady(home, timeoutMs);
|
|
35
|
+
}
|
|
36
|
+
return ensureDaemon({ home, entry, env, timeoutMs });
|
|
37
|
+
}
|
|
38
|
+
function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
|
|
39
|
+
} }) {
|
|
40
|
+
let stopped = false;
|
|
41
|
+
let missed = 0;
|
|
42
|
+
let starting;
|
|
43
|
+
const start = (reason) => {
|
|
44
|
+
starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env }).then(() => log(`alpd ${reason}`), (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
|
|
45
|
+
starting = void 0;
|
|
46
|
+
missed = 0;
|
|
47
|
+
});
|
|
48
|
+
return starting;
|
|
49
|
+
};
|
|
50
|
+
const check = async () => {
|
|
51
|
+
if (stopped || starting) return;
|
|
52
|
+
if (lockAlive(await readLock(home))) {
|
|
53
|
+
missed = 0;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (await daemonHeld(home)) {
|
|
57
|
+
missed = 0;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (++missed >= misses && !stopped) await start("was down; started it again");
|
|
61
|
+
};
|
|
62
|
+
const initial = start("started with Paseo");
|
|
63
|
+
const timer = setInterval(() => {
|
|
64
|
+
void check();
|
|
65
|
+
}, intervalMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
const stop = () => {
|
|
68
|
+
stopped = true;
|
|
69
|
+
clearInterval(timer);
|
|
70
|
+
return starting ?? Promise.resolve();
|
|
71
|
+
};
|
|
72
|
+
stop.started = initial;
|
|
73
|
+
return stop;
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
daemonHeld,
|
|
77
|
+
holdDaemon,
|
|
78
|
+
releaseHold,
|
|
79
|
+
startDaemon,
|
|
80
|
+
superviseDaemon
|
|
81
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,137 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "ALP client module",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
"./acp-probe": {
|
|
12
|
+
"alp-source": {
|
|
13
|
+
"types": "./src/acp-probe.js",
|
|
14
|
+
"default": "./src/acp-probe.js"
|
|
15
|
+
},
|
|
16
|
+
"types": "./dist/acp-probe.d.ts",
|
|
17
|
+
"default": "./src/acp-probe.js"
|
|
18
|
+
},
|
|
19
|
+
"./antigravity-probe": {
|
|
20
|
+
"alp-source": {
|
|
21
|
+
"types": "./src/antigravity-probe.js",
|
|
22
|
+
"default": "./src/antigravity-probe.js"
|
|
23
|
+
},
|
|
24
|
+
"types": "./dist/antigravity-probe.d.ts",
|
|
25
|
+
"default": "./src/antigravity-probe.js"
|
|
26
|
+
},
|
|
27
|
+
"./command": {
|
|
28
|
+
"alp-source": {
|
|
29
|
+
"types": "./src/command.js",
|
|
30
|
+
"default": "./src/command.js"
|
|
31
|
+
},
|
|
32
|
+
"types": "./dist/command.d.ts",
|
|
33
|
+
"default": "./src/command.js"
|
|
34
|
+
},
|
|
35
|
+
"./doctor": {
|
|
36
|
+
"alp-source": {
|
|
37
|
+
"types": "./src/doctor.js",
|
|
38
|
+
"default": "./src/doctor.js"
|
|
39
|
+
},
|
|
40
|
+
"types": "./dist/doctor.d.ts",
|
|
41
|
+
"default": "./src/doctor.js"
|
|
42
|
+
},
|
|
43
|
+
"./index": {
|
|
44
|
+
"alp-source": {
|
|
45
|
+
"types": "./src/index.js",
|
|
46
|
+
"default": "./src/index.js"
|
|
47
|
+
},
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"default": "./src/index.js"
|
|
50
|
+
},
|
|
51
|
+
".": {
|
|
52
|
+
"alp-source": {
|
|
53
|
+
"types": "./src/index.js",
|
|
54
|
+
"default": "./src/index.js"
|
|
55
|
+
},
|
|
56
|
+
"types": "./dist/index.d.ts",
|
|
57
|
+
"default": "./src/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./library-test": {
|
|
60
|
+
"alp-source": {
|
|
61
|
+
"types": "./src/library-test.js",
|
|
62
|
+
"default": "./src/library-test.js"
|
|
63
|
+
},
|
|
64
|
+
"types": "./dist/library-test.d.ts",
|
|
65
|
+
"default": "./src/library-test.js"
|
|
66
|
+
},
|
|
67
|
+
"./mcp-probe": {
|
|
68
|
+
"alp-source": {
|
|
69
|
+
"types": "./src/mcp-probe.js",
|
|
70
|
+
"default": "./src/mcp-probe.js"
|
|
71
|
+
},
|
|
72
|
+
"types": "./dist/mcp-probe.d.ts",
|
|
73
|
+
"default": "./src/mcp-probe.js"
|
|
74
|
+
},
|
|
75
|
+
"./prompt": {
|
|
76
|
+
"alp-source": {
|
|
77
|
+
"types": "./src/prompt.js",
|
|
78
|
+
"default": "./src/prompt.js"
|
|
79
|
+
},
|
|
80
|
+
"types": "./dist/prompt.d.ts",
|
|
81
|
+
"default": "./src/prompt.js"
|
|
82
|
+
},
|
|
83
|
+
"./render": {
|
|
84
|
+
"alp-source": {
|
|
85
|
+
"types": "./src/render.js",
|
|
86
|
+
"default": "./src/render.js"
|
|
87
|
+
},
|
|
88
|
+
"types": "./dist/render.d.ts",
|
|
89
|
+
"default": "./src/render.js"
|
|
90
|
+
},
|
|
91
|
+
"./service": {
|
|
92
|
+
"alp-source": {
|
|
93
|
+
"types": "./src/service.js",
|
|
94
|
+
"default": "./src/service.js"
|
|
95
|
+
},
|
|
96
|
+
"types": "./dist/service.d.ts",
|
|
97
|
+
"default": "./src/service.js"
|
|
98
|
+
},
|
|
99
|
+
"./setup": {
|
|
100
|
+
"alp-source": {
|
|
101
|
+
"types": "./src/setup.js",
|
|
102
|
+
"default": "./src/setup.js"
|
|
103
|
+
},
|
|
104
|
+
"types": "./dist/setup.d.ts",
|
|
105
|
+
"default": "./src/setup.js"
|
|
106
|
+
},
|
|
107
|
+
"./supervise": {
|
|
108
|
+
"alp-source": {
|
|
109
|
+
"types": "./src/supervise.js",
|
|
110
|
+
"default": "./src/supervise.js"
|
|
111
|
+
},
|
|
112
|
+
"types": "./dist/supervise.d.ts",
|
|
113
|
+
"default": "./src/supervise.js"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=20"
|
|
118
|
+
},
|
|
119
|
+
"dependencies": {
|
|
120
|
+
"@anhlp/alp-core": "0.10.0",
|
|
121
|
+
"@anhlp/alp-protocol": "0.10.0"
|
|
122
|
+
},
|
|
123
|
+
"scripts": {
|
|
124
|
+
"build": "node ../../scripts/build-workspace.mjs client",
|
|
125
|
+
"test": "node ../../scripts/test-package.mjs client",
|
|
126
|
+
"prepack": "npm run build"
|
|
127
|
+
},
|
|
128
|
+
"publishConfig": {
|
|
129
|
+
"access": "public",
|
|
130
|
+
"registry": "https://registry.npmjs.org/"
|
|
131
|
+
},
|
|
132
|
+
"repository": {
|
|
133
|
+
"type": "git",
|
|
134
|
+
"url": "git+https://github.com/phucanh08/alp-paseo.git",
|
|
135
|
+
"directory": "packages/client"
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/acp-probe.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { stdioSession } from './mcp-probe.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts an ACP provider once and runs initialize (ALPD §46), for `alp provider test`
|
|
5
|
+
* and the settings screen. It reports who the agent says it is and what it supports;
|
|
6
|
+
* a provider that needs a login usually says so here, in authMethods. With `discover`,
|
|
7
|
+
* it also opens a session in `cwd`, as Paseo does, to read the models and modes the agent
|
|
8
|
+
* offers (ALPD §65); an agent that offers none leaves them out.
|
|
9
|
+
* @param {{ command: string, args?: string[], env?: Record<string, string>, label?: string, id?: string }} provider
|
|
10
|
+
* @param {{ cwd?: string, timeoutMs?: number, discover?: boolean }} [options]
|
|
11
|
+
* @returns {Promise<{ protocolVersion: number, agent?: { name?: string, title?: string, version?: string }, loadSession: boolean, mcpHttp: boolean, authMethods: string[],
|
|
12
|
+
* models?: Array<{ id: string, label: string, description?: string }>, currentModel?: string, modes?: Array<{ id: string, label: string, description?: string }>, currentMode?: string }>}
|
|
13
|
+
*/
|
|
14
|
+
export async function probeAcp(provider, { cwd, timeoutMs = 20_000, discover = false } = {}) {
|
|
15
|
+
const session = stdioSession({ command: provider.command, args: provider.args ?? [], env: provider.env ?? {} }, cwd, timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
const result = await session.request('initialize', {
|
|
18
|
+
protocolVersion: 1,
|
|
19
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
20
|
+
clientInfo: { name: 'alp', title: 'ALP', version: '1' },
|
|
21
|
+
});
|
|
22
|
+
if (result.protocolVersion !== 1) throw new Error(`${provider.label ?? provider.id} speaks ACP version ${result.protocolVersion}; ALP speaks version 1`);
|
|
23
|
+
const info = result.agentInfo;
|
|
24
|
+
const offered = discover ? await session.request('session/new', { cwd, mcpServers: [] }) : undefined;
|
|
25
|
+
const choices = list => Array.isArray(list) ? list.filter(item => typeof (item?.modelId ?? item?.id) === 'string')
|
|
26
|
+
.map(item => ({ id: item.modelId ?? item.id, label: typeof item.name === 'string' && item.name ? item.name : item.modelId ?? item.id, ...(typeof item.description === 'string' && item.description ? { description: item.description } : {}) })) : undefined;
|
|
27
|
+
const config = offered?.configOptions?.find(option => option.category === 'model' && option.type === 'select');
|
|
28
|
+
const modeConfig = offered?.configOptions?.find(option => option.category === 'mode' && option.type === 'select');
|
|
29
|
+
const flatten = options => options?.flatMap(option => option.options ?? [option]).map(option => ({ ...option, id: option.value }));
|
|
30
|
+
const modelState = offered?.models ?? (config ? { availableModels: flatten(config.options), currentModelId: config.currentValue } : undefined);
|
|
31
|
+
const modeState = offered?.modes ?? (modeConfig ? { availableModes: flatten(modeConfig.options), currentModeId: modeConfig.currentValue } : undefined);
|
|
32
|
+
const models = choices(modelState?.availableModels);
|
|
33
|
+
const modes = choices(modeState?.availableModes);
|
|
34
|
+
return {
|
|
35
|
+
...(models ? { models, ...(typeof modelState.currentModelId === 'string' ? { currentModel: modelState.currentModelId } : {}) } : {}),
|
|
36
|
+
...(modes ? { modes, ...(typeof modeState.currentModeId === 'string' ? { currentMode: modeState.currentModeId } : {}) } : {}),
|
|
37
|
+
protocolVersion: result.protocolVersion,
|
|
38
|
+
...(info ? { agent: { ...(info.name ? { name: info.name } : {}), ...(info.title ? { title: info.title } : {}), ...(info.version ? { version: info.version } : {}) } } : {}),
|
|
39
|
+
loadSession: Boolean(result.agentCapabilities?.loadSession),
|
|
40
|
+
mcpHttp: Boolean(result.agentCapabilities?.mcpCapabilities?.http),
|
|
41
|
+
authMethods: (result.authMethods ?? []).map(method => method?.name ?? method?.id).filter(Boolean),
|
|
42
|
+
};
|
|
43
|
+
} finally {
|
|
44
|
+
await session.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ANTIGRAVITY_MODELS = [
|
|
4
|
+
{ id: 'gemini-3.8-flash-high', label: 'Gemini 3.8 Flash (High)' },
|
|
5
|
+
{ id: 'gemini-3.8-flash-medium', label: 'Gemini 3.8 Flash (Medium)' },
|
|
6
|
+
{ id: 'gemini-3.8-flash-low', label: 'Gemini 3.8 Flash (Low)' },
|
|
7
|
+
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
|
8
|
+
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
|
9
|
+
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 (Thinking)' },
|
|
10
|
+
{ id: 'claude-opus-4-6-thinking', label: 'Claude Opus 4.6 (Thinking)' },
|
|
11
|
+
{ id: 'gpt-oss-120b-medium', label: 'GPT-OSS 120B (Medium)' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const ANTIGRAVITY_MODES = [
|
|
15
|
+
{
|
|
16
|
+
id: 'full-access',
|
|
17
|
+
label: 'Full access',
|
|
18
|
+
description: 'Antigravity runs with full access (--dangerously-skip-permissions).',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function runCommand(command, args, { env = process.env, cwd, timeoutMs = 20_000 } = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, {
|
|
25
|
+
cwd,
|
|
26
|
+
env,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let stdout = '';
|
|
32
|
+
let stderr = '';
|
|
33
|
+
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
child.kill('SIGKILL');
|
|
36
|
+
reject(new Error(`${command} ${args.join(' ')} timed out after ${timeoutMs / 1000}s`));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
|
|
39
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString(); });
|
|
40
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
41
|
+
|
|
42
|
+
child.on('error', error => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(error);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on('close', code => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
if (code === 0) resolve({ stdout, stderr });
|
|
50
|
+
else reject(new Error(`${command} exited with code ${code}: ${stderr.trim() || stdout.trim()}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Probes Antigravity CLI for version, status and available models.
|
|
57
|
+
* @param {{ command?: string, args?: string[], env?: Record<string, string>, label?: string, id?: string, readOnlyMode?: 'instructions' | 'reject' }} [provider]
|
|
58
|
+
* @param {{ cwd?: string, timeoutMs?: number }} [options]
|
|
59
|
+
*/
|
|
60
|
+
export async function probeAntigravity(provider = {}, { cwd, timeoutMs = 20_000 } = {}) {
|
|
61
|
+
const command = provider.command || 'agy';
|
|
62
|
+
const args = provider.args ?? [];
|
|
63
|
+
const env = { ...process.env, ...(provider.env ?? {}) };
|
|
64
|
+
|
|
65
|
+
// Check version
|
|
66
|
+
let version = '';
|
|
67
|
+
try {
|
|
68
|
+
const res = await runCommand(command, [...args, '--version'], { env, cwd, timeoutMs: Math.min(timeoutMs, 5000) });
|
|
69
|
+
version = res.stdout.trim().split('\n')[0] ?? '';
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`Antigravity command '${command}' is not available: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Probe models
|
|
75
|
+
let models = [];
|
|
76
|
+
try {
|
|
77
|
+
const res = await runCommand(command, [...args, 'models'], { env, cwd, timeoutMs });
|
|
78
|
+
for (const line of res.stdout.trim().split(/\r?\n/)) {
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
const [id, label] = line.split('\t');
|
|
81
|
+
if (id && label) {
|
|
82
|
+
models.push({ id: id.trim(), label: label.trim() });
|
|
83
|
+
} else if (id) {
|
|
84
|
+
models.push({ id: id.trim(), label: id.trim() });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
models = [...DEFAULT_ANTIGRAVITY_MODELS];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!models.length) {
|
|
92
|
+
models = [...DEFAULT_ANTIGRAVITY_MODELS];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
version,
|
|
97
|
+
agent: { name: 'antigravity', title: 'Antigravity', version },
|
|
98
|
+
models,
|
|
99
|
+
currentModel: models[0]?.id,
|
|
100
|
+
modes: provider.readOnlyMode === 'instructions' ? [...ANTIGRAVITY_MODES, {
|
|
101
|
+
id: 'read-only', label: 'Read only (instructions)',
|
|
102
|
+
description: 'For new sessions: prompt-only read-only behavior. agy still runs with full access; no OS sandbox enforces this mode.',
|
|
103
|
+
}] : ANTIGRAVITY_MODES,
|
|
104
|
+
currentMode: 'full-access',
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/command.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/** Runs a command; resolves with its exit code and output, never rejects. */
|
|
4
|
+
export function runCommand(command, args, { cwd, env = process.env, timeoutMs = 15_000 } = {}) {
|
|
5
|
+
return new Promise(resolve => {
|
|
6
|
+
let child;
|
|
7
|
+
try { child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
8
|
+
catch (error) { resolve({ code: -1, stdout: '', stderr: error.message }); return; }
|
|
9
|
+
let stdout = '', stderr = '';
|
|
10
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
11
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
12
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
|
|
13
|
+
child.on('error', error => { clearTimeout(timer); resolve({ code: -1, stdout, stderr: error.message }); });
|
|
14
|
+
child.on('close', code => { clearTimeout(timer); resolve({ code: code ?? -1, stdout, stderr }); });
|
|
15
|
+
});
|
|
16
|
+
}
|
package/src/doctor.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DoctorCheck = {
|
|
2
|
+
id: string;
|
|
3
|
+
status: 'ok' | 'info' | 'warn' | 'fail';
|
|
4
|
+
summary: string;
|
|
5
|
+
details?: string[];
|
|
6
|
+
hint?: string;
|
|
7
|
+
fix?: { describe: string; apply(): Promise<string> };
|
|
8
|
+
};
|
|
9
|
+
export type CommandResult = { code: number; stdout: string; stderr: string };
|
|
10
|
+
export type CommandRunner = (command: string, args: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number }) => Promise<CommandResult>;
|
|
11
|
+
export const runCommand: CommandRunner;
|
|
12
|
+
export function diagnose(options?: { home?: string; project?: string; env?: NodeJS.ProcessEnv; run?: CommandRunner; sandbox?: boolean; daemonEntry?: string }): Promise<DoctorCheck[]>;
|
|
13
|
+
export function repair(checks: DoctorCheck[]): Promise<Array<{ id: string; result: string }>>;
|