@anhlp/alp-client 0.0.0-stage → 0.0.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +28 -2
  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 +151 -0
  13. package/dist/index.js +533 -0
  14. package/dist/integration-lock.d.ts +35 -0
  15. package/dist/integration-lock.js +179 -0
  16. package/dist/ipc.d.ts +42 -0
  17. package/dist/ipc.js +69 -0
  18. package/dist/library-test.d.ts +94 -0
  19. package/dist/library-test.js +37 -0
  20. package/dist/mcp-probe.d.ts +24 -0
  21. package/dist/mcp-probe.js +147 -0
  22. package/dist/portable.d.ts +19 -0
  23. package/dist/portable.js +33 -0
  24. package/dist/prompt.d.ts +32 -0
  25. package/dist/prompt.js +59 -0
  26. package/dist/render.d.ts +6 -0
  27. package/dist/render.js +120 -0
  28. package/dist/rpc/client.d.ts +82 -0
  29. package/dist/rpc/client.js +292 -0
  30. package/dist/rpc/errors.d.ts +42 -0
  31. package/dist/rpc/errors.js +34 -0
  32. package/dist/rpc/framing.d.ts +21 -0
  33. package/dist/rpc/framing.js +57 -0
  34. package/dist/rpc/handshake.d.ts +19 -0
  35. package/dist/rpc/handshake.js +8 -0
  36. package/dist/rpc/reconnect.d.ts +97 -0
  37. package/dist/rpc/reconnect.js +227 -0
  38. package/dist/rpc/registry.d.ts +211 -0
  39. package/dist/rpc/registry.js +145 -0
  40. package/dist/rpc/timers.d.ts +11 -0
  41. package/dist/rpc/timers.js +11 -0
  42. package/dist/service.d.ts +50 -0
  43. package/dist/service.js +143 -0
  44. package/dist/setup.d.ts +2 -0
  45. package/dist/setup.js +146 -0
  46. package/dist/supervise.d.ts +14 -0
  47. package/dist/supervise.js +87 -0
  48. package/dist/websocket.d.ts +51 -0
  49. package/dist/websocket.js +190 -0
  50. package/package.json +174 -4
  51. package/src/acp-probe.js +46 -0
  52. package/src/antigravity-probe.js +106 -0
  53. package/src/command.js +18 -0
  54. package/src/doctor.d.ts +13 -0
  55. package/src/doctor.js +570 -0
  56. package/src/index.js +596 -0
  57. package/src/integration-lock.js +179 -0
  58. package/src/ipc.js +92 -0
  59. package/src/library-test.js +41 -0
  60. package/src/mcp-probe.js +118 -0
  61. package/src/portable.js +28 -0
  62. package/src/prompt.js +64 -0
  63. package/src/render.js +136 -0
  64. package/src/rpc/client.js +296 -0
  65. package/src/rpc/errors.js +54 -0
  66. package/src/rpc/framing.js +61 -0
  67. package/src/rpc/handshake.js +16 -0
  68. package/src/rpc/reconnect.js +226 -0
  69. package/src/rpc/registry.js +161 -0
  70. package/src/rpc/timers.js +16 -0
  71. package/src/service.js +154 -0
  72. package/src/setup.js +150 -0
  73. package/src/supervise.d.ts +14 -0
  74. package/src/supervise.js +92 -0
  75. package/src/websocket.js +259 -0
@@ -0,0 +1,145 @@
1
+ // packages/client/src/rpc/registry.js
2
+ function workspaceRegistry(client) {
3
+ return {
4
+ /** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.options']['result']>} */
5
+ worktreeOptions: (workspaceId) => client.request("workspace.worktree.options", { workspaceId }),
6
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['result']>} */
7
+ createWorktree: (params) => client.request("workspace.worktree.create", params),
8
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['result']>} */
9
+ removeWorktree: (params) => client.request("workspace.worktree.remove", params),
10
+ /** Read-only Git status of a registry workspace (capability `workspace.git`, §12.5). @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.status']['result']>} */
11
+ gitStatus: (workspaceId) => client.request("workspace.git.status", { workspaceId }),
12
+ /** One page of a read-only Git diff; pass `nextCursor` back as `cursor` with the same comparison and path. @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.diff']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.git.diff']['result']>} */
13
+ gitDiff: (params) => client.request("workspace.git.diff", params),
14
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.list']['result']>} */
15
+ projects: (params = {}) => client.request("project.list", params),
16
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['result']>} */
17
+ workspaces: (params = {}) => client.request("workspace.list", params),
18
+ /** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.get']['result']>} */
19
+ get: (workspaceId) => client.request("workspace.get", { workspaceId }),
20
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.register']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.register']['result']>} */
21
+ register: (params) => client.request("project.register", params),
22
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['result']>} */
23
+ remove: (params) => client.request("project.remove", params),
24
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['result']>} */
25
+ update: (params) => client.request("workspace.update", params)
26
+ };
27
+ }
28
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
29
+ function base64Encode(bytes) {
30
+ let out = "";
31
+ let index = 0;
32
+ for (; index + 2 < bytes.length; index += 3) {
33
+ const n = bytes[index] << 16 | bytes[index + 1] << 8 | bytes[index + 2];
34
+ out += BASE64[n >> 18] + BASE64[n >> 12 & 63] + BASE64[n >> 6 & 63] + BASE64[n & 63];
35
+ }
36
+ if (index < bytes.length) {
37
+ const n = bytes[index] << 16 | (index + 1 < bytes.length ? bytes[index + 1] : 0) << 8;
38
+ out += BASE64[n >> 18] + BASE64[n >> 12 & 63] + (index + 1 < bytes.length ? BASE64[n >> 6 & 63] : "=") + "=";
39
+ }
40
+ return out;
41
+ }
42
+ async function sha256Hex(bytes) {
43
+ const subtle = globalThis.crypto?.subtle;
44
+ if (!subtle) throw new Error("SHA-256 needs WebCrypto here; pass sha256 yourself");
45
+ const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
46
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
47
+ }
48
+ function attachments(client) {
49
+ const api = {
50
+ /** @param {Map['attachment.begin']['params']} params @returns {Promise<Map['attachment.begin']['result']>} */
51
+ begin: (params) => client.request("attachment.begin", params),
52
+ /** @param {Map['attachment.chunk']['params']} params @returns {Promise<Map['attachment.chunk']['result']>} */
53
+ chunk: (params) => client.request("attachment.chunk", params),
54
+ /** @param {Map['attachment.commit']['params']} params @returns {Promise<Map['attachment.commit']['result']>} */
55
+ commit: (params) => client.request("attachment.commit", params),
56
+ /** @param {string} uploadId @returns {Promise<Map['attachment.abort']['result']>} */
57
+ abort: (uploadId) => client.request("attachment.abort", { uploadId }),
58
+ /** Metadata only (no bytes, no path). @param {string} attachmentId @returns {Promise<Map['attachment.get']['result']>} */
59
+ get: (attachmentId) => client.request("attachment.get", { attachmentId }),
60
+ /** Refused (1004 attachment_referenced) while a prompt references it. @param {string} attachmentId @returns {Promise<Map['attachment.delete']['result']>} */
61
+ delete: (attachmentId) => client.request("attachment.delete", { attachmentId }),
62
+ /**
63
+ * Uploads `data` and commits it: begin (or resume, with the same requestId), chunks of at most
64
+ * the smaller of `maxChunkBytes` and what begin answered, commit with its SHA-256. A chunk
65
+ * refused `offset_mismatch` continues from the daemon's `received`. Returns the attachment.
66
+ * @param {{ sessionId: string, name: string, mimeType: string, data: Uint8Array, requestId: string, sha256?: string, maxChunkBytes?: number, onProgress?: (received: number, size: number) => void }} options
67
+ */
68
+ async upload({ sessionId, name, mimeType, data, requestId, sha256, maxChunkBytes, onProgress }) {
69
+ const digest = sha256 ?? await sha256Hex(data);
70
+ const begun = await api.begin({ sessionId, name, size: data.length, mimeType, sha256: digest, requestId });
71
+ if (begun.state === "committed" && begun.attachmentId) return (await api.get(begun.attachmentId)).attachment;
72
+ const size = Math.max(1, Math.min(begun.maxChunkBytes, maxChunkBytes ?? Infinity));
73
+ let offset = begun.received;
74
+ for (let realigned = 0; offset < data.length; ) {
75
+ const end = Math.min(data.length, offset + size);
76
+ try {
77
+ const { received } = await api.chunk({ uploadId: begun.uploadId, offset, data: base64Encode(data.subarray(offset, end)) });
78
+ offset = received;
79
+ } catch (error) {
80
+ const held = (
81
+ /** @type {any} */
82
+ error?.data
83
+ );
84
+ if (held?.reason !== "offset_mismatch" || !Number.isSafeInteger(held.received) || ++realigned > 3) throw error;
85
+ offset = held.received;
86
+ continue;
87
+ }
88
+ onProgress?.(offset, data.length);
89
+ }
90
+ return (await api.commit({ uploadId: begun.uploadId, sha256: digest })).attachment;
91
+ }
92
+ };
93
+ return api;
94
+ }
95
+ function conversation(client) {
96
+ return {
97
+ /**
98
+ * A new closed root whose conversation is the source root's through or before a turn (`boundary`),
99
+ * or a branch a rewind set aside (`branchId`); resume it with session.create { sessionId, resume: true }.
100
+ * Principal fields are set by a trusted gateway on its own connection, never by a remote client.
101
+ * @param {Map['session.fork']['params']} params @returns {Promise<Map['session.fork']['result']>}
102
+ */
103
+ fork: (params) => client.request("session.fork", params),
104
+ /**
105
+ * Continues the root's conversation from before `beforeTurnId` under the same session id; the
106
+ * later turns stay a branch (`branch.branchId`) that `fork({ branchId })` recovers.
107
+ * @param {Map['session.rewind']['params']} params @returns {Promise<Map['session.rewind']['result']>}
108
+ */
109
+ rewind: (params) => client.request("session.rewind", params),
110
+ /**
111
+ * Restarts a live root's native process on its own conversation and configuration at the idle
112
+ * barrier (capability `session.reload`, §12.8); id, history and receipts stay. Sent once: repeat
113
+ * the same `requestId` after a lost answer.
114
+ * @param {Map['session.reload']['params']} params @returns {Promise<Map['session.reload']['result']>}
115
+ */
116
+ reload: (params) => client.request("session.reload", params),
117
+ /**
118
+ * The ALP CLI command that follows the session in a terminal (capability `session.resumeCommand`,
119
+ * §12.8), built by alpd: for the clipboard only, never to run.
120
+ * @param {Map['session.resumeCommand']['params']} params @returns {Promise<Map['session.resumeCommand']['result']>}
121
+ */
122
+ resumeCommand: (params) => client.request("session.resumeCommand", params),
123
+ /**
124
+ * Changes a live root's model, effort or team (capability `session.configure.live`, §12.9) at the
125
+ * idle barrier, through a restart of its native process on the same conversation; `configured` says
126
+ * what it runs with now. Sent once: repeat the same `requestId` after a lost answer. A change to
127
+ * another runtime is refused (1005 runtime_mismatch, `data.action: 'session.create'`).
128
+ * @param {Map['session.configure']['params'] & { requestId: string; ifGeneration: string }} params @returns {Promise<Map['session.configure']['result']>}
129
+ */
130
+ configure: (params) => client.request("session.configure", params),
131
+ /**
132
+ * What a change of model, effort, team or mode would run a live root with, and whether it would be
133
+ * taken now (capability `session.configure.live`, §12.9). Reads only.
134
+ * @param {Map['session.configure.preview']['params']} params @returns {Promise<Map['session.configure.preview']['result']>}
135
+ */
136
+ configurePreview: (params) => client.request("session.configure.preview", params)
137
+ };
138
+ }
139
+ export {
140
+ attachments,
141
+ base64Encode,
142
+ conversation,
143
+ sha256Hex,
144
+ workspaceRegistry
145
+ };
@@ -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,14 @@
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
+ /** Who keeps alpd up, for the log: "alpd started with <by>". */
13
+ by?: string;
14
+ }): (() => Promise<void>) & { started: Promise<void> };
@@ -0,0 +1,87 @@
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, withDaemonLifecycle } 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, automatic = false }) {
28
+ return withDaemonLifecycle(home, "start", async () => {
29
+ if (automatic) {
30
+ if (await daemonHeld(home)) return;
31
+ } else await releaseHold(home);
32
+ const lock = await readLock(home);
33
+ if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
34
+ const service = serviceFor({ home, env });
35
+ if (service && await installedProgram(service)) {
36
+ await startService(service);
37
+ return whenReady(home, timeoutMs);
38
+ }
39
+ return ensureDaemon({ home, entry, env, timeoutMs, lifecycleLocked: true });
40
+ });
41
+ }
42
+ function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
43
+ }, by }) {
44
+ let stopped = false;
45
+ let missed = 0;
46
+ let starting;
47
+ const start = (reason, automatic = true) => {
48
+ starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env, automatic }).then((socket) => {
49
+ if (socket) log(`alpd ${reason}`);
50
+ }, (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
51
+ starting = void 0;
52
+ missed = 0;
53
+ });
54
+ return starting;
55
+ };
56
+ const check = async () => {
57
+ if (stopped || starting) return;
58
+ if (lockAlive(await readLock(home))) {
59
+ missed = 0;
60
+ return;
61
+ }
62
+ if (await daemonHeld(home)) {
63
+ missed = 0;
64
+ return;
65
+ }
66
+ if (++missed >= misses && !stopped) await start("was down; started it again");
67
+ };
68
+ const initial = start(by ? `started with ${by}` : "started", false);
69
+ const timer = setInterval(() => {
70
+ void check();
71
+ }, intervalMs);
72
+ timer.unref?.();
73
+ const stop = () => {
74
+ stopped = true;
75
+ clearInterval(timer);
76
+ return starting ?? Promise.resolve();
77
+ };
78
+ stop.started = initial;
79
+ return stop;
80
+ }
81
+ export {
82
+ daemonHeld,
83
+ holdDaemon,
84
+ releaseHold,
85
+ startDaemon,
86
+ superviseDaemon
87
+ };