@anhlp/alp-client 0.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +24 -0
  4. package/dist/acp-probe.d.ts +44 -0
  5. package/dist/acp-probe.js +37 -0
  6. package/dist/antigravity-probe.d.ts +34 -0
  7. package/dist/antigravity-probe.js +95 -0
  8. package/dist/command.d.ts +5 -0
  9. package/dist/command.js +33 -0
  10. package/dist/doctor.d.ts +13 -0
  11. package/dist/doctor.js +587 -0
  12. package/dist/index.d.ts +101 -0
  13. package/dist/index.js +422 -0
  14. package/dist/ipc.d.ts +42 -0
  15. package/dist/ipc.js +69 -0
  16. package/dist/library-test.d.ts +94 -0
  17. package/dist/library-test.js +37 -0
  18. package/dist/mcp-probe.d.ts +24 -0
  19. package/dist/mcp-probe.js +147 -0
  20. package/dist/portable.d.ts +18 -0
  21. package/dist/portable.js +27 -0
  22. package/dist/prompt.d.ts +32 -0
  23. package/dist/prompt.js +59 -0
  24. package/dist/render.d.ts +6 -0
  25. package/dist/render.js +120 -0
  26. package/dist/rpc/client.d.ts +82 -0
  27. package/dist/rpc/client.js +292 -0
  28. package/dist/rpc/errors.d.ts +42 -0
  29. package/dist/rpc/errors.js +34 -0
  30. package/dist/rpc/framing.d.ts +21 -0
  31. package/dist/rpc/framing.js +57 -0
  32. package/dist/rpc/handshake.d.ts +19 -0
  33. package/dist/rpc/handshake.js +8 -0
  34. package/dist/rpc/reconnect.d.ts +97 -0
  35. package/dist/rpc/reconnect.js +227 -0
  36. package/dist/rpc/timers.d.ts +11 -0
  37. package/dist/rpc/timers.js +11 -0
  38. package/dist/service.d.ts +50 -0
  39. package/dist/service.js +143 -0
  40. package/dist/setup.d.ts +2 -0
  41. package/dist/setup.js +146 -0
  42. package/dist/supervise.d.ts +12 -0
  43. package/dist/supervise.js +81 -0
  44. package/dist/websocket.d.ts +51 -0
  45. package/dist/websocket.js +190 -0
  46. package/package.json +168 -0
  47. package/src/acp-probe.js +46 -0
  48. package/src/antigravity-probe.js +106 -0
  49. package/src/command.js +18 -0
  50. package/src/doctor.d.ts +13 -0
  51. package/src/doctor.js +570 -0
  52. package/src/index.js +457 -0
  53. package/src/ipc.js +92 -0
  54. package/src/library-test.js +41 -0
  55. package/src/mcp-probe.js +118 -0
  56. package/src/portable.js +27 -0
  57. package/src/prompt.js +64 -0
  58. package/src/render.js +136 -0
  59. package/src/rpc/client.js +296 -0
  60. package/src/rpc/errors.js +54 -0
  61. package/src/rpc/framing.js +61 -0
  62. package/src/rpc/handshake.js +16 -0
  63. package/src/rpc/reconnect.js +226 -0
  64. package/src/rpc/timers.js +16 -0
  65. package/src/service.js +154 -0
  66. package/src/setup.js +150 -0
  67. package/src/supervise.d.ts +12 -0
  68. package/src/supervise.js +85 -0
  69. package/src/websocket.js +259 -0
@@ -0,0 +1,227 @@
1
+ // packages/client/src/rpc/reconnect.js
2
+ import { AlpRpcError, ERROR } from "@anhlp/alp-protocol";
3
+ import { AlpRpcClient } from "./client.js";
4
+ import { AlpClientError } from "./errors.js";
5
+ function backoffDelay(attempt, { initialMs = 250, maxMs = 1e4, factor = 2, jitter = 0.2 } = {}, random = Math.random) {
6
+ const base = Math.min(maxMs, initialMs * factor ** Math.max(0, attempt - 1));
7
+ return Math.max(0, Math.round(base * (1 + jitter * (2 * random() - 1))));
8
+ }
9
+ function retryable(error) {
10
+ return !(error instanceof AlpRpcError && error.code === ERROR.protocol);
11
+ }
12
+ function controller() {
13
+ if (typeof globalThis.AbortController === "function") {
14
+ const native = new globalThis.AbortController();
15
+ return {
16
+ signal: (
17
+ /** @type {AbortSignalLike} */
18
+ native.signal
19
+ ),
20
+ /** @param {unknown} reason */
21
+ abort: (reason) => native.abort(reason)
22
+ };
23
+ }
24
+ const listeners = /* @__PURE__ */ new Set();
25
+ const signal = {
26
+ aborted: false,
27
+ /** @type {unknown} */
28
+ reason: void 0,
29
+ /** @param {'abort'} _type @param {() => void} listener */
30
+ addEventListener(_type, listener) {
31
+ listeners.add(listener);
32
+ },
33
+ /** @param {'abort'} _type @param {() => void} listener */
34
+ removeEventListener(_type, listener) {
35
+ listeners.delete(listener);
36
+ }
37
+ };
38
+ return {
39
+ signal,
40
+ /** @param {unknown} reason */
41
+ abort(reason) {
42
+ if (signal.aborted) return;
43
+ signal.aborted = true;
44
+ signal.reason = reason;
45
+ for (const listener of [...listeners]) listener();
46
+ listeners.clear();
47
+ }
48
+ };
49
+ }
50
+ function createReconnectingClient({ connect, onConnect, onDisconnect, backoff = {}, maxAttempts = Infinity, stableMs = 5e3, shouldRetry = retryable, client: clientOptions, random = Math.random }) {
51
+ if (typeof connect !== "function") throw new TypeError("createReconnectingClient needs connect()");
52
+ let state = "connecting";
53
+ let current;
54
+ let attempt = 0;
55
+ let connectedBefore = false;
56
+ let timer;
57
+ const run = controller();
58
+ const events = /* @__PURE__ */ new Set();
59
+ const notifications = /* @__PURE__ */ new Set();
60
+ const states = /* @__PURE__ */ new Set();
61
+ const waiters = /* @__PURE__ */ new Set();
62
+ const onListenerError = clientOptions?.onListenerError ?? ((error) => {
63
+ setTimeout(() => {
64
+ throw error;
65
+ }, 0);
66
+ });
67
+ const dispatch = (listener, ...args) => {
68
+ try {
69
+ listener(...args);
70
+ } catch (error) {
71
+ onListenerError(error);
72
+ }
73
+ };
74
+ const enter = (next, error) => {
75
+ state = next;
76
+ for (const listener of [...states]) {
77
+ dispatch(listener, next, error);
78
+ if (state !== next) return;
79
+ }
80
+ if (next === "open" && current) for (const waiter of [...waiters]) {
81
+ waiters.delete(waiter);
82
+ waiter.resolve(current);
83
+ }
84
+ if (next === "stopped") for (const waiter of [...waiters]) {
85
+ waiters.delete(waiter);
86
+ waiter.reject(error ?? new AlpClientError("ALP connection stopped", { reason: "closed" }));
87
+ }
88
+ };
89
+ const retry = (error) => {
90
+ if (state === "stopped") return;
91
+ if (attempt >= maxAttempts || !shouldRetry(error)) return enter("stopped", error);
92
+ enter("waiting", error);
93
+ if (state !== "waiting") return;
94
+ timer = setTimeout(() => {
95
+ timer = void 0;
96
+ void open();
97
+ }, backoffDelay(attempt, backoff, random));
98
+ };
99
+ const open = async () => {
100
+ if (state === "stopped") return;
101
+ attempt++;
102
+ enter("connecting");
103
+ const { signal } = run;
104
+ if (state !== "connecting" || signal.aborted) return;
105
+ let candidate;
106
+ const closeCandidate = () => candidate?.close();
107
+ signal.addEventListener("abort", closeCandidate, { once: true });
108
+ try {
109
+ const transport = await connect({ attempt, signal });
110
+ if (signal.aborted) {
111
+ try {
112
+ transport.close();
113
+ } catch {
114
+ }
115
+ return;
116
+ }
117
+ candidate = new AlpRpcClient(transport, clientOptions);
118
+ candidate.onEvent((envelope) => {
119
+ for (const listener of [...events]) dispatch(listener, envelope);
120
+ });
121
+ candidate.onNotification((method, params) => {
122
+ for (const listener of [...notifications]) dispatch(listener, method, params);
123
+ });
124
+ if (onConnect) await onConnect(candidate, { attempt, reconnect: connectedBefore, signal });
125
+ if (signal.aborted) {
126
+ candidate.close();
127
+ return;
128
+ }
129
+ if (candidate.closed) throw new AlpClientError("ALP connection closed during onConnect", { reason: "disconnected" });
130
+ } catch (error) {
131
+ candidate?.close();
132
+ if (signal.aborted) return;
133
+ return retry(error instanceof Error ? error : new Error(String(error)));
134
+ } finally {
135
+ signal.removeEventListener("abort", closeCandidate);
136
+ }
137
+ const connected = (
138
+ /** @type {AlpRpcClient} */
139
+ candidate
140
+ );
141
+ current = connected;
142
+ const reconnect = connectedBefore;
143
+ connectedBefore = true;
144
+ const openedAt = Date.now();
145
+ connected.onClose((error) => {
146
+ if (current !== connected) return;
147
+ current = void 0;
148
+ if (Date.now() - openedAt >= stableMs) attempt = 0;
149
+ dispatch(() => onDisconnect?.(error, { reconnect }));
150
+ retry(error);
151
+ });
152
+ enter("open");
153
+ };
154
+ void open();
155
+ return {
156
+ get state() {
157
+ return state;
158
+ },
159
+ get client() {
160
+ return current;
161
+ },
162
+ get attempt() {
163
+ return attempt;
164
+ },
165
+ ready({ signal } = {}) {
166
+ if (state === "open" && current) return Promise.resolve(current);
167
+ if (state === "stopped") return Promise.reject(new AlpClientError("ALP connection stopped", { reason: "closed" }));
168
+ return new Promise((resolve, reject) => {
169
+ const waiter = {
170
+ /** @param {AlpRpcClient} client */
171
+ resolve: (client) => {
172
+ signal?.removeEventListener("abort", onAbort);
173
+ resolve(client);
174
+ },
175
+ /** @param {Error} error */
176
+ reject: (error) => {
177
+ signal?.removeEventListener("abort", onAbort);
178
+ reject(error);
179
+ }
180
+ };
181
+ const onAbort = () => {
182
+ waiters.delete(waiter);
183
+ reject(new AlpClientError("Waiting for the ALP connection was aborted", { reason: "aborted", cause: signal?.reason }));
184
+ };
185
+ if (signal?.aborted) return onAbort();
186
+ waiters.add(waiter);
187
+ signal?.addEventListener("abort", onAbort, { once: true });
188
+ });
189
+ },
190
+ request(method, params, options) {
191
+ if (state !== "open" || !current) return Promise.reject(new AlpClientError("ALP daemon connection closed", { reason: state === "stopped" ? "closed" : "disconnected" }));
192
+ return current.request(method, params, options);
193
+ },
194
+ onEvent(listener) {
195
+ events.add(listener);
196
+ return () => events.delete(listener);
197
+ },
198
+ onNotification(listener) {
199
+ notifications.add(listener);
200
+ return () => notifications.delete(listener);
201
+ },
202
+ onState(listener) {
203
+ states.add(listener);
204
+ return () => states.delete(listener);
205
+ },
206
+ stop() {
207
+ if (state === "stopped") return;
208
+ if (timer !== void 0) {
209
+ clearTimeout(timer);
210
+ timer = void 0;
211
+ }
212
+ run.abort(new AlpClientError("ALP connection stopped", { reason: "closed" }));
213
+ const last = current;
214
+ current = void 0;
215
+ enter("stopped");
216
+ last?.close();
217
+ events.clear();
218
+ notifications.clear();
219
+ states.clear();
220
+ }
221
+ };
222
+ }
223
+ export {
224
+ backoffDelay,
225
+ createReconnectingClient,
226
+ retryable
227
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A timeout option as a setTimeout delay. `undefined` means no timer: no timeout given, Infinity,
3
+ * or a wait longer than any timer can (about 24.8 days). Anything that is not a non-negative
4
+ * number is a TypeError, so a typo cannot turn into an instant timeout.
5
+ * @param {unknown} value
6
+ * @param {string} [name]
7
+ * @returns {number | undefined}
8
+ */
9
+ export function timerDelay(value: unknown, name?: string): number | undefined;
10
+ /** The longest delay setTimeout honours; a longer one fires at once, in browsers, Node and Hermes. */
11
+ export const MAX_TIMER_MS: 2147483647;
@@ -0,0 +1,11 @@
1
+ // packages/client/src/rpc/timers.js
2
+ var MAX_TIMER_MS = 2147483647;
3
+ function timerDelay(value, name = "timeoutMs") {
4
+ if (value === void 0 || value === Infinity) return void 0;
5
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) throw new TypeError(`${name} must be a non-negative number of milliseconds`);
6
+ return value > MAX_TIMER_MS ? void 0 : value;
7
+ }
8
+ export {
9
+ MAX_TIMER_MS,
10
+ timerDelay
11
+ };
@@ -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';
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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(/&quot;/g, '"').replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/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
+ };
@@ -0,0 +1,2 @@
1
+ /** Write Phở and Cafe's agent profiles into a project's .alp/settings.json, without installing software or calling models. */
2
+ export function setup(args: any, home: any): Promise<void>;
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> };