@anhlp/alp 0.0.1-beta.3 → 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 (54) hide show
  1. package/README.md +18 -16
  2. package/dist/acp.js +37 -4
  3. package/dist/alpd.js +10350 -3346
  4. package/docs/modular-monorepo.md +17 -13
  5. package/package.json +8 -6
  6. package/packages/cli/dist/THIRD-PARTY-NOTICES.txt +25 -0
  7. package/packages/cli/dist/acp/main.js +1 -1
  8. package/packages/cli/dist/acp.js +37 -4
  9. package/packages/cli/dist/alp-web/THIRD-PARTY-NOTICES.txt +2590 -0
  10. package/packages/cli/dist/alp-web/alp-boundary.json +15 -0
  11. package/packages/cli/dist/alp-web/apple-touch-icon.png +0 -0
  12. package/packages/cli/dist/alp-web/assets/TerminalPanel-DLuoa74B.css +1 -0
  13. package/packages/cli/dist/alp-web/assets/TerminalPanel-PrU2l3u5.js +16 -0
  14. package/packages/cli/dist/alp-web/assets/index-BJdBTDFX.css +1 -0
  15. package/packages/cli/dist/alp-web/assets/index-DpfiPv5s.js +52 -0
  16. package/packages/cli/dist/alp-web/favicon-attention.svg +6 -0
  17. package/packages/cli/dist/alp-web/favicon-running.svg +6 -0
  18. package/packages/cli/dist/alp-web/favicon.svg +5 -0
  19. package/packages/cli/dist/alp-web/index.html +20 -0
  20. package/packages/cli/dist/alp-web.js +9640 -0
  21. package/packages/cli/dist/alpd.js +10350 -3346
  22. package/packages/cli/dist/backup.js +2 -2
  23. package/packages/cli/dist/build-manifest.json +99 -1
  24. package/packages/cli/dist/cli.js +4 -2
  25. package/packages/cli/dist/interfaces.d.ts +9 -1
  26. package/packages/cli/dist/interfaces.js +99 -39
  27. package/packages/cli/dist/schedules.d.ts +2 -0
  28. package/packages/cli/dist/schedules.js +45 -0
  29. package/packages/cli/dist/sessions.d.ts +3 -1
  30. package/packages/cli/dist/sessions.js +22 -11
  31. package/packages/cli/dist/terminal.d.ts +8 -0
  32. package/packages/cli/dist/terminal.js +79 -0
  33. package/packages/cli/dist/usage.d.ts +1 -1
  34. package/packages/cli/dist/usage.js +8 -2
  35. package/packages/cli/dist/web-service.d.ts +38 -0
  36. package/packages/cli/dist/web-service.js +119 -0
  37. package/packages/client/README.md +6 -1
  38. package/packages/client/dist/index.d.ts +37 -2
  39. package/packages/client/dist/index.js +55 -22
  40. package/packages/client/dist/integration-lock.d.ts +35 -0
  41. package/packages/client/dist/integration-lock.js +179 -0
  42. package/packages/client/dist/portable.d.ts +1 -0
  43. package/packages/client/dist/portable.js +7 -1
  44. package/packages/client/dist/rpc/registry.d.ts +211 -0
  45. package/packages/client/dist/rpc/registry.js +145 -0
  46. package/packages/client/dist/supervise.d.ts +2 -0
  47. package/packages/client/dist/supervise.js +4 -4
  48. package/packages/core/dist/backup.js +103 -4
  49. package/packages/core/dist/index.d.ts +1 -0
  50. package/packages/core/dist/index.js +2 -0
  51. package/packages/core/dist/migrations.js +1 -1
  52. package/packages/core/dist/user-worktree.d.ts +29 -0
  53. package/packages/core/dist/user-worktree.js +242 -0
  54. package/packages/core/dist/validation.js +3 -2
@@ -0,0 +1,179 @@
1
+ // packages/client/src/integration-lock.js
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, open, rename, rmdir, unlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { assertIntegrationName, bootTime } from "./index.js";
6
+ function createIntegrationLock(home, name) {
7
+ const file = path.join(home, "integrations", `${assertIntegrationName(name)}.lock`);
8
+ const guard = `${file}.reclaim`;
9
+ let phase = "new";
10
+ let owned;
11
+ const recovery = (reason) => new Error(`${reason}. Stop ALL integration processes using this ALP_HOME before manual lock-only recovery of ${file} and ${guard}; see docs/modular-monorepo.md`);
12
+ async function guarded(run) {
13
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
14
+ const deadline = Date.now() + 2e4;
15
+ for (; ; ) {
16
+ try {
17
+ await mkdir(guard, { mode: 448 });
18
+ break;
19
+ } catch (error) {
20
+ if (
21
+ /** @type {any} */
22
+ error?.code !== "EEXIST"
23
+ ) throw error;
24
+ if (Date.now() >= deadline) throw recovery("Integration mutation guard remains busy or abandoned");
25
+ await new Promise((resolve) => setTimeout(resolve, 25));
26
+ }
27
+ }
28
+ const token = path.join(guard, `owner-${process.pid}-${randomUUID()}`);
29
+ try {
30
+ const handle = await open(token, "wx", 384);
31
+ await handle.close();
32
+ } catch (error) {
33
+ throw recovery(`Integration guard initialization failed: ${String(error)}`);
34
+ }
35
+ try {
36
+ return await run();
37
+ } finally {
38
+ try {
39
+ await unlink(token);
40
+ } catch (error) {
41
+ throw recovery(
42
+ /** @type {any} */
43
+ error?.code === "ENOENT" ? "Integration guard ownership was lost" : `Integration guard token cleanup failed: ${String(error)}`
44
+ );
45
+ }
46
+ try {
47
+ await rmdir(guard);
48
+ } catch (error) {
49
+ throw recovery(`Integration guard cleanup failed: ${String(error)}`);
50
+ }
51
+ }
52
+ }
53
+ async function snapshot() {
54
+ let handle;
55
+ try {
56
+ handle = await open(file, "r");
57
+ } catch (error) {
58
+ if (
59
+ /** @type {any} */
60
+ error?.code === "ENOENT"
61
+ ) return void 0;
62
+ throw error;
63
+ }
64
+ try {
65
+ const info = await handle.stat();
66
+ const raw = await handle.readFile("utf8");
67
+ let value;
68
+ try {
69
+ value = JSON.parse(raw);
70
+ } catch {
71
+ value = void 0;
72
+ }
73
+ return { value, info };
74
+ } finally {
75
+ await handle.close();
76
+ }
77
+ }
78
+ function complete(value) {
79
+ return value && Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.bootTime === "number" && Number.isFinite(value.bootTime) && value.bootTime > 0 && typeof value.startedAt === "string" && Number.isFinite(Date.parse(value.startedAt)) && typeof value.ready === "boolean" && (value.lockVersion === void 0 && value.owner === void 0 || value.lockVersion === 1 && typeof value.owner === "string" && /^[a-f0-9-]{36}$/.test(value.owner));
80
+ }
81
+ function dead(value) {
82
+ try {
83
+ process.kill(value.pid, 0);
84
+ return false;
85
+ } catch (error) {
86
+ if (
87
+ /** @type {any} */
88
+ error?.code !== "ESRCH"
89
+ ) return false;
90
+ if (Math.abs(value.bootTime - bootTime()) > 30) throw recovery("Integration owner boot context is uncertain");
91
+ return true;
92
+ }
93
+ }
94
+ function matches(current) {
95
+ return !!owned && current?.value?.owner === owned.owner && current.info.dev === owned.dev && current.info.ino === owned.ino;
96
+ }
97
+ return {
98
+ async acquire() {
99
+ if (phase !== "new") throw new Error("Integration lock instance has already been used");
100
+ phase = "acquiring";
101
+ try {
102
+ return await guarded(async () => {
103
+ const current = await snapshot();
104
+ if (current) {
105
+ if (!complete(current.value)) throw recovery("Integration lock is malformed, incomplete or unsupported");
106
+ if (!dead(current.value)) {
107
+ phase = "retired";
108
+ return { acquired: false, lock: current.value };
109
+ }
110
+ await unlink(file);
111
+ }
112
+ const value = { pid: process.pid, bootTime: bootTime(), startedAt: (/* @__PURE__ */ new Date()).toISOString(), ready: false, lockVersion: 1, owner: randomUUID() };
113
+ const handle = await open(file, "wx", 384);
114
+ let info;
115
+ try {
116
+ await handle.writeFile(JSON.stringify(value));
117
+ info = await handle.stat();
118
+ } finally {
119
+ await handle.close();
120
+ }
121
+ owned = { owner: value.owner, dev: info.dev, ino: info.ino };
122
+ phase = "owned";
123
+ return { acquired: true, lock: value };
124
+ });
125
+ } catch (error) {
126
+ phase = "retired";
127
+ owned = void 0;
128
+ throw error;
129
+ }
130
+ },
131
+ async setReady(ready) {
132
+ if (phase !== "owned") return false;
133
+ return guarded(async () => {
134
+ if (phase !== "owned") return false;
135
+ const current = await snapshot();
136
+ if (!current || !matches(current)) {
137
+ phase = "retired";
138
+ owned = void 0;
139
+ return false;
140
+ }
141
+ const value = { ...current.value, ready };
142
+ const tmp = `${file}.${randomUUID()}.tmp`;
143
+ const handle = await open(tmp, "wx", 384);
144
+ let info;
145
+ try {
146
+ try {
147
+ await handle.writeFile(JSON.stringify(value));
148
+ info = await handle.stat();
149
+ } finally {
150
+ await handle.close();
151
+ }
152
+ await rename(tmp, file);
153
+ owned = { owner: value.owner, dev: info.dev, ino: info.ino };
154
+ return true;
155
+ } finally {
156
+ await unlink(tmp).catch((error) => {
157
+ if (error?.code !== "ENOENT") throw error;
158
+ });
159
+ }
160
+ });
161
+ },
162
+ async release() {
163
+ if (phase !== "owned") return false;
164
+ return guarded(async () => {
165
+ if (phase !== "owned") return false;
166
+ const current = await snapshot();
167
+ const ours = matches(current);
168
+ phase = "retired";
169
+ owned = void 0;
170
+ if (!ours) return false;
171
+ await unlink(file);
172
+ return true;
173
+ });
174
+ }
175
+ };
176
+ }
177
+ export {
178
+ createIntegrationLock
179
+ };
@@ -16,3 +16,4 @@ export { AlpClientError, CLIENT_ERROR, isAlpClientError } from "./rpc/errors.js"
16
16
  export { createLineDecoder, utf8Length } from "./rpc/framing.js";
17
17
  export { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
18
18
  export { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
19
+ export { attachments, base64Encode, conversation, sha256Hex, workspaceRegistry } from "./rpc/registry.js";
@@ -6,6 +6,7 @@ import { createLineDecoder, utf8Length } from "./rpc/framing.js";
6
6
  import { MAX_TIMER_MS, timerDelay } from "./rpc/timers.js";
7
7
  import { hello } from "./rpc/handshake.js";
8
8
  import { backoffDelay, createReconnectingClient, retryable } from "./rpc/reconnect.js";
9
+ import { attachments, base64Encode, conversation, sha256Hex, workspaceRegistry } from "./rpc/registry.js";
9
10
  export {
10
11
  AlpClientError,
11
12
  AlpRpcClient,
@@ -16,12 +17,17 @@ export {
16
17
  MAX_FRAME,
17
18
  MAX_TIMER_MS,
18
19
  PROTOCOL_VERSION,
20
+ attachments,
19
21
  backoffDelay,
22
+ base64Encode,
23
+ conversation,
20
24
  createLineDecoder,
21
25
  createReconnectingClient,
22
26
  hello,
23
27
  isAlpClientError,
24
28
  retryable,
29
+ sha256Hex,
25
30
  timerDelay,
26
- utf8Length
31
+ utf8Length,
32
+ workspaceRegistry
27
33
  };
@@ -0,0 +1,211 @@
1
+ /** Typed registry methods for portable clients. Mutations are sent once: callers retain requestId
2
+ * and ifRevision to reconcile a timeout by repeating the identical request. @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client */
3
+ export function workspaceRegistry(client: {
4
+ request<T = any>(method: string, params?: unknown): Promise<T>;
5
+ }): {
6
+ /** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.options']['result']>} */
7
+ worktreeOptions: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.options"]["result"]>;
8
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.create']['result']>} */
9
+ createWorktree: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.create"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.create"]["result"]>;
10
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.worktree.remove']['result']>} */
11
+ removeWorktree: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.remove"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.worktree.remove"]["result"]>;
12
+ /** 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']>} */
13
+ gitStatus: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.status"]["result"]>;
14
+ /** 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']>} */
15
+ gitDiff: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.diff"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.git.diff"]["result"]>;
16
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.list']['result']>} */
17
+ projects: (params?: import("@anhlp/alp-protocol").RpcMethodMap["project.list"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.list"]["result"]>;
18
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['params']} [params] @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.list']['result']>} */
19
+ workspaces: (params?: import("@anhlp/alp-protocol").RpcMethodMap["workspace.list"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.list"]["result"]>;
20
+ /** @param {string} workspaceId @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.get']['result']>} */
21
+ get: (workspaceId: string) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.get"]["result"]>;
22
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.register']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.register']['result']>} */
23
+ register: (params: import("@anhlp/alp-protocol").RpcMethodMap["project.register"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.register"]["result"]>;
24
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['project.remove']['result']>} */
25
+ remove: (params: import("@anhlp/alp-protocol").RpcMethodMap["project.remove"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["project.remove"]["result"]>;
26
+ /** @param {import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['params']} params @returns {Promise<import('@anhlp/alp-protocol').RpcMethodMap['workspace.update']['result']>} */
27
+ update: (params: import("@anhlp/alp-protocol").RpcMethodMap["workspace.update"]["params"]) => Promise<import("@anhlp/alp-protocol").RpcMethodMap["workspace.update"]["result"]>;
28
+ };
29
+ /** Canonical base64 of `bytes`, without Buffer or btoa, so it runs in browsers and Node alike. @param {Uint8Array} bytes */
30
+ export function base64Encode(bytes: Uint8Array): string;
31
+ /** Lowercase hex SHA-256 of `bytes` through WebCrypto (browsers' secure contexts, Node 19+). @param {Uint8Array} bytes */
32
+ export function sha256Hex(bytes: Uint8Array): Promise<string>;
33
+ /**
34
+ * Typed attachment methods (capability `attachments`, docs/protocol/alp-json-rpc-v1.md §12.6) and a
35
+ * chunked upload that keeps to the limits the daemon (or gateway) advertised. Mutations are sent
36
+ * once: `upload` resumes an interrupted upload by repeating its begin with the same requestId and
37
+ * continues from what the daemon holds.
38
+ * @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client
39
+ */
40
+ export function attachments(client: {
41
+ request<T = any>(method: string, params?: unknown): Promise<T>;
42
+ }): {
43
+ /** @param {Map['attachment.begin']['params']} params @returns {Promise<Map['attachment.begin']['result']>} */
44
+ begin: (params: import("@anhlp/alp-protocol").PrincipalContext & {
45
+ sessionId: string;
46
+ name: string;
47
+ size: number;
48
+ mimeType: string;
49
+ sha256?: string;
50
+ requestId: string;
51
+ }) => Promise<{
52
+ uploadId: string;
53
+ state: "uploading" | "committed";
54
+ received: number;
55
+ size: number;
56
+ maxChunkBytes: number;
57
+ expiresAt?: string;
58
+ attachmentId?: string;
59
+ }>;
60
+ /** @param {Map['attachment.chunk']['params']} params @returns {Promise<Map['attachment.chunk']['result']>} */
61
+ chunk: (params: import("@anhlp/alp-protocol").PrincipalContext & {
62
+ uploadId: string;
63
+ offset: number;
64
+ data: string;
65
+ }) => Promise<{
66
+ received: number;
67
+ size: number;
68
+ }>;
69
+ /** @param {Map['attachment.commit']['params']} params @returns {Promise<Map['attachment.commit']['result']>} */
70
+ commit: (params: import("@anhlp/alp-protocol").PrincipalContext & {
71
+ uploadId: string;
72
+ sha256: string;
73
+ }) => Promise<{
74
+ attachment: import("@anhlp/alp-protocol").AttachmentInfo;
75
+ }>;
76
+ /** @param {string} uploadId @returns {Promise<Map['attachment.abort']['result']>} */
77
+ abort: (uploadId: string) => Promise<{
78
+ aborted: true;
79
+ }>;
80
+ /** Metadata only (no bytes, no path). @param {string} attachmentId @returns {Promise<Map['attachment.get']['result']>} */
81
+ get: (attachmentId: string) => Promise<{
82
+ attachment: import("@anhlp/alp-protocol").AttachmentInfo;
83
+ }>;
84
+ /** Refused (1004 attachment_referenced) while a prompt references it. @param {string} attachmentId @returns {Promise<Map['attachment.delete']['result']>} */
85
+ delete: (attachmentId: string) => Promise<{
86
+ deleted: true;
87
+ }>;
88
+ /**
89
+ * Uploads `data` and commits it: begin (or resume, with the same requestId), chunks of at most
90
+ * the smaller of `maxChunkBytes` and what begin answered, commit with its SHA-256. A chunk
91
+ * refused `offset_mismatch` continues from the daemon's `received`. Returns the attachment.
92
+ * @param {{ sessionId: string, name: string, mimeType: string, data: Uint8Array, requestId: string, sha256?: string, maxChunkBytes?: number, onProgress?: (received: number, size: number) => void }} options
93
+ */
94
+ upload({ sessionId, name, mimeType, data, requestId, sha256, maxChunkBytes, onProgress }: {
95
+ sessionId: string;
96
+ name: string;
97
+ mimeType: string;
98
+ data: Uint8Array;
99
+ requestId: string;
100
+ sha256?: string;
101
+ maxChunkBytes?: number;
102
+ onProgress?: (received: number, size: number) => void;
103
+ }): Promise<import("@anhlp/alp-protocol").AttachmentInfo>;
104
+ };
105
+ /**
106
+ * Typed conversation methods (capabilities `session.fork` and `session.rewind`, docs/protocol/
107
+ * alp-json-rpc-v1.md §12.7; `session.reload` and `session.resumeCommand`, §12.8). Both change the conversation only, never a file. Mutations are sent
108
+ * once: keep `requestId` (and `ifGeneration`) to repeat the identical request after a timeout, which
109
+ * answers the first result with `duplicate: true`. After a rewind, attached clients get a
110
+ * `history.reset` event: drop cached history, fetch a new tail and attach with its generation.
111
+ * @param {{request<T = any>(method: string, params?: unknown): Promise<T>}} client
112
+ */
113
+ export function conversation(client: {
114
+ request<T = any>(method: string, params?: unknown): Promise<T>;
115
+ }): {
116
+ /**
117
+ * A new closed root whose conversation is the source root's through or before a turn (`boundary`),
118
+ * or a branch a rewind set aside (`branchId`); resume it with session.create { sessionId, resume: true }.
119
+ * Principal fields are set by a trusted gateway on its own connection, never by a remote client.
120
+ * @param {Map['session.fork']['params']} params @returns {Promise<Map['session.fork']['result']>}
121
+ */
122
+ fork: (params: import("@anhlp/alp-protocol").PrincipalContext & {
123
+ sessionId: string;
124
+ requestId: string;
125
+ ifGeneration: string;
126
+ boundary?: import("@anhlp/alp-protocol").ForkBoundary;
127
+ branchId?: string;
128
+ workspaceId?: string;
129
+ newSessionId?: string;
130
+ title?: string;
131
+ }) => Promise<{
132
+ session: import("@anhlp/alp-protocol").SessionSummary;
133
+ forkedFrom: import("@anhlp/alp-protocol").ForkProvenance;
134
+ duplicate?: true;
135
+ }>;
136
+ /**
137
+ * Continues the root's conversation from before `beforeTurnId` under the same session id; the
138
+ * later turns stay a branch (`branch.branchId`) that `fork({ branchId })` recovers.
139
+ * @param {Map['session.rewind']['params']} params @returns {Promise<Map['session.rewind']['result']>}
140
+ */
141
+ rewind: (params: {
142
+ sessionId: string;
143
+ requestId: string;
144
+ ifGeneration: string;
145
+ beforeTurnId: string;
146
+ }) => Promise<{
147
+ session: import("@anhlp/alp-protocol").SessionSummary;
148
+ generation: string;
149
+ branch: import("@anhlp/alp-protocol").ConversationBranch;
150
+ duplicate?: true;
151
+ }>;
152
+ /**
153
+ * Restarts a live root's native process on its own conversation and configuration at the idle
154
+ * barrier (capability `session.reload`, §12.8); id, history and receipts stay. Sent once: repeat
155
+ * the same `requestId` after a lost answer.
156
+ * @param {Map['session.reload']['params']} params @returns {Promise<Map['session.reload']['result']>}
157
+ */
158
+ reload: (params: {
159
+ sessionId: string;
160
+ requestId: string;
161
+ ifGeneration: string;
162
+ }) => Promise<{
163
+ session: import("@anhlp/alp-protocol").SessionSummary;
164
+ duplicate?: true;
165
+ }>;
166
+ /**
167
+ * The ALP CLI command that follows the session in a terminal (capability `session.resumeCommand`,
168
+ * §12.8), built by alpd: for the clipboard only, never to run.
169
+ * @param {Map['session.resumeCommand']['params']} params @returns {Promise<Map['session.resumeCommand']['result']>}
170
+ */
171
+ resumeCommand: (params: {
172
+ sessionId: string;
173
+ }) => Promise<import("@anhlp/alp-protocol").ResumeCommand>;
174
+ /**
175
+ * Changes a live root's model, effort or team (capability `session.configure.live`, §12.9) at the
176
+ * idle barrier, through a restart of its native process on the same conversation; `configured` says
177
+ * what it runs with now. Sent once: repeat the same `requestId` after a lost answer. A change to
178
+ * another runtime is refused (1005 runtime_mismatch, `data.action: 'session.create'`).
179
+ * @param {Map['session.configure']['params'] & { requestId: string; ifGeneration: string }} params @returns {Promise<Map['session.configure']['result']>}
180
+ */
181
+ configure: (params: {
182
+ sessionId: string;
183
+ mode?: string;
184
+ model?: string;
185
+ thinking?: string;
186
+ team?: string;
187
+ requestId?: string;
188
+ ifGeneration?: string;
189
+ } & {
190
+ requestId: string;
191
+ ifGeneration: string;
192
+ }) => Promise<{
193
+ session: import("@anhlp/alp-protocol").SessionSnapshot | import("@anhlp/alp-protocol").SessionSummary;
194
+ configured?: import("@anhlp/alp-protocol").ConfigureOutcome;
195
+ duplicate?: true;
196
+ }>;
197
+ /**
198
+ * What a change of model, effort, team or mode would run a live root with, and whether it would be
199
+ * taken now (capability `session.configure.live`, §12.9). Reads only.
200
+ * @param {Map['session.configure.preview']['params']} params @returns {Promise<Map['session.configure.preview']['result']>}
201
+ */
202
+ configurePreview: (params: {
203
+ sessionId: string;
204
+ mode?: string;
205
+ model?: string;
206
+ thinking?: string;
207
+ team?: string;
208
+ }) => Promise<{
209
+ preview: import("@anhlp/alp-protocol").ConfigurePreview;
210
+ }>;
211
+ };
@@ -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
+ };
@@ -9,4 +9,6 @@ export function superviseDaemon(options: {
9
9
  intervalMs?: number;
10
10
  misses?: number;
11
11
  log?: (message: string) => void;
12
+ /** Who keeps alpd up, for the log: "alpd started with <by>". */
13
+ by?: string;
12
14
  }): (() => Promise<void>) & { started: Promise<void> };
@@ -40,12 +40,12 @@ async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3, a
40
40
  });
41
41
  }
42
42
  function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
43
- } }) {
43
+ }, by }) {
44
44
  let stopped = false;
45
45
  let missed = 0;
46
46
  let starting;
47
- const start = (reason) => {
48
- starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env, automatic: reason !== "started with Paseo" }).then((socket) => {
47
+ const start = (reason, automatic = true) => {
48
+ starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env, automatic }).then((socket) => {
49
49
  if (socket) log(`alpd ${reason}`);
50
50
  }, (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
51
51
  starting = void 0;
@@ -65,7 +65,7 @@ function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, mis
65
65
  }
66
66
  if (++missed >= misses && !stopped) await start("was down; started it again");
67
67
  };
68
- const initial = start("started with Paseo");
68
+ const initial = start(by ? `started with ${by}` : "started", false);
69
69
  const timer = setInterval(() => {
70
70
  void check();
71
71
  }, intervalMs);