@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
@@ -7,7 +7,7 @@ import { acquireDaemonLock, alpHome, assertPrivateDirectory, daemonPaths, lockAl
7
7
  import { daemonHeld, holdDaemon, releaseHold } from "@anhlp/alp-client/supervise";
8
8
  import { start, stop } from "./daemon.js";
9
9
  import { UsageError } from "./usage.js";
10
- var VERSION = true ? "0.0.1-beta.3" : "dev";
10
+ var VERSION = true ? "0.0.1-beta.4" : "dev";
11
11
  var BY = `alp ${VERSION}`;
12
12
  var size = (bytes) => bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${bytes} B`;
13
13
  var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
@@ -138,7 +138,7 @@ async function restoreCommand(args) {
138
138
  for (const { root, reason } of result.skipped) console.log(` skipped ${root}: ${reason}`);
139
139
  for (const warning of result.warnings) console.log(` note: ${warning}`);
140
140
  if (result.stateSchema !== null && result.stateSchema !== void 0) console.log(" sessions that were working when the backup was made wait for you; alpd continues none by itself");
141
- if (result.safety) console.log(`the data it replaced is in ${result.safety}; alp restore ${result.safety} puts it back`);
141
+ if (result.safety) console.log(`the data it replaced is in ${result.safety}; alp restore ${result.safety} restores that data with the same workspace authority checks`);
142
142
  }
143
143
  export {
144
144
  backupCommand,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.1-beta.3",
2
+ "version": "0.0.1-beta.4",
3
3
  "entries": {
4
4
  "packages/cli/dist/alpd.js": {
5
5
  "inputs": [
@@ -11,16 +11,19 @@
11
11
  "packages/client/src/rpc/framing.js",
12
12
  "packages/client/src/rpc/handshake.js",
13
13
  "packages/client/src/rpc/reconnect.js",
14
+ "packages/client/src/rpc/registry.js",
14
15
  "packages/client/src/rpc/timers.js",
15
16
  "packages/client/src/websocket.js",
16
17
  "packages/core/src/adapter.js",
17
18
  "packages/core/src/atomic.js",
18
19
  "packages/core/src/backup.js",
20
+ "packages/core/src/beads.js",
19
21
  "packages/core/src/delegation.js",
20
22
  "packages/core/src/errors.js",
21
23
  "packages/core/src/formulas.js",
22
24
  "packages/core/src/hook-run.js",
23
25
  "packages/core/src/hooks.js",
26
+ "packages/core/src/index.js",
24
27
  "packages/core/src/init.js",
25
28
  "packages/core/src/library-edit.js",
26
29
  "packages/core/src/library.js",
@@ -40,18 +43,30 @@
40
43
  "packages/core/src/teams.js",
41
44
  "packages/core/src/template-path.js",
42
45
  "packages/core/src/trust.js",
46
+ "packages/core/src/upgrade.js",
43
47
  "packages/core/src/user-settings.js",
48
+ "packages/core/src/user-worktree.js",
44
49
  "packages/core/src/validation.js",
45
50
  "packages/core/src/verify.js",
46
51
  "packages/core/src/workflow.js",
52
+ "packages/daemon/src/attachments.ts",
53
+ "packages/daemon/src/conversation.ts",
47
54
  "packages/daemon/src/files.ts",
55
+ "packages/daemon/src/git.ts",
48
56
  "packages/daemon/src/gitignore.ts",
57
+ "packages/daemon/src/history.ts",
58
+ "packages/daemon/src/lifecycle.ts",
49
59
  "packages/daemon/src/lock.ts",
50
60
  "packages/daemon/src/main.ts",
51
61
  "packages/daemon/src/operations.ts",
62
+ "packages/daemon/src/registry.ts",
52
63
  "packages/daemon/src/rpc-error.ts",
64
+ "packages/daemon/src/scheduler.ts",
65
+ "packages/daemon/src/schedules.ts",
53
66
  "packages/daemon/src/server.ts",
54
67
  "packages/daemon/src/store.ts",
68
+ "packages/daemon/src/terminals.ts",
69
+ "packages/daemon/src/worktrees.ts",
55
70
  "packages/protocol/src/contract-v1.js",
56
71
  "packages/protocol/src/contract.js",
57
72
  "packages/protocol/src/index.ts",
@@ -59,11 +74,15 @@
59
74
  "packages/protocol/src/wire.js",
60
75
  "packages/runtime/src/acp-transport.ts",
61
76
  "packages/runtime/src/antigravity-transport.ts",
77
+ "packages/runtime/src/assignment-results.ts",
78
+ "packages/runtime/src/attachments.ts",
62
79
  "packages/runtime/src/board-domain.ts",
63
80
  "packages/runtime/src/board.ts",
64
81
  "packages/runtime/src/catalog.ts",
65
82
  "packages/runtime/src/claude-transport.ts",
83
+ "packages/runtime/src/configure.ts",
66
84
  "packages/runtime/src/contracts.ts",
85
+ "packages/runtime/src/conversation.ts",
67
86
  "packages/runtime/src/github.ts",
68
87
  "packages/runtime/src/handoff.ts",
69
88
  "packages/runtime/src/index.ts",
@@ -94,6 +113,7 @@
94
113
  "node:crypto",
95
114
  "node:fs",
96
115
  "node:fs/promises",
116
+ "node:module",
97
117
  "node:net",
98
118
  "node:os",
99
119
  "node:path",
@@ -120,6 +140,7 @@
120
140
  "packages/client/src/rpc/framing.js",
121
141
  "packages/client/src/rpc/handshake.js",
122
142
  "packages/client/src/rpc/reconnect.js",
143
+ "packages/client/src/rpc/registry.js",
123
144
  "packages/client/src/rpc/timers.js",
124
145
  "packages/client/src/service.js",
125
146
  "packages/client/src/supervise.js",
@@ -137,6 +158,83 @@
137
158
  "node:path",
138
159
  "node:stream"
139
160
  ]
161
+ },
162
+ "packages/cli/dist/alp-web.js": {
163
+ "inputs": [
164
+ "apps/gateway/src/config.js",
165
+ "apps/gateway/src/connection.js",
166
+ "apps/gateway/src/credentials.js",
167
+ "apps/gateway/src/index.js",
168
+ "apps/gateway/src/log.js",
169
+ "apps/gateway/src/policy.js",
170
+ "apps/gateway/src/server.js",
171
+ "apps/gateway/src/tickets.js",
172
+ "apps/gateway/src/upstream.js",
173
+ "packages/cli/src/web/grant.js",
174
+ "packages/cli/src/web/main.js",
175
+ "packages/cli/src/web/serve.js",
176
+ "packages/client/src/command.js",
177
+ "packages/client/src/index.js",
178
+ "packages/client/src/integration-lock.js",
179
+ "packages/client/src/ipc.js",
180
+ "packages/client/src/portable.js",
181
+ "packages/client/src/rpc/client.js",
182
+ "packages/client/src/rpc/errors.js",
183
+ "packages/client/src/rpc/framing.js",
184
+ "packages/client/src/rpc/handshake.js",
185
+ "packages/client/src/rpc/reconnect.js",
186
+ "packages/client/src/rpc/registry.js",
187
+ "packages/client/src/rpc/timers.js",
188
+ "packages/client/src/service.js",
189
+ "packages/client/src/supervise.js",
190
+ "packages/client/src/websocket.js",
191
+ "packages/core/src/atomic.js",
192
+ "packages/core/src/errors.js",
193
+ "packages/core/src/library-edit.js",
194
+ "packages/core/src/library.js",
195
+ "packages/core/src/log.js",
196
+ "packages/core/src/models.js",
197
+ "packages/core/src/permissions.js",
198
+ "packages/core/src/providers.js",
199
+ "packages/core/src/resolver.js",
200
+ "packages/core/src/safe-path.js",
201
+ "packages/core/src/shell.js",
202
+ "packages/core/src/teams.js",
203
+ "packages/core/src/template-path.js",
204
+ "packages/core/src/validation.js",
205
+ "packages/core/src/verify.js",
206
+ "packages/protocol/src/contract-v1.js",
207
+ "packages/protocol/src/contract.js",
208
+ "packages/protocol/src/index.ts",
209
+ "packages/protocol/src/validate.js",
210
+ "packages/protocol/src/wire.js"
211
+ ],
212
+ "external": [
213
+ "buffer",
214
+ "bufferutil",
215
+ "crypto",
216
+ "events",
217
+ "http",
218
+ "https",
219
+ "net",
220
+ "node:child_process",
221
+ "node:crypto",
222
+ "node:fs",
223
+ "node:fs/promises",
224
+ "node:http",
225
+ "node:https",
226
+ "node:net",
227
+ "node:os",
228
+ "node:path",
229
+ "node:url",
230
+ "node:util",
231
+ "stream",
232
+ "tls",
233
+ "url",
234
+ "utf-8-validate",
235
+ "util",
236
+ "zlib"
237
+ ]
140
238
  }
141
239
  }
142
240
  }
@@ -9,14 +9,16 @@ import { project, permissionsCommand, trustCommand, language, verify, doctor } f
9
9
  import { daemon } from "./daemon.js";
10
10
  import { run, ps, attach, send, questions, answer, top, log, board, pause, resume, recall, interrupt } from "./sessions.js";
11
11
  import { libraryCommand, editCommand, teamsCommand } from "./library.js";
12
+ import { scheduleCommand } from "./schedules.js";
12
13
  import { tasksCommand, formulaCommand, taskCommand } from "./tasks.js";
13
14
  import { webCommand, acpCommand } from "./interfaces.js";
14
15
  import { backupCommand, restoreCommand } from "./backup.js";
15
- var commands = { setup: (args2) => setup(args2, alpHome()), init: (args2) => project("init", args2), upgrade: (args2) => project("upgrade", args2), update: (args2) => project("upgrade", args2), daemon, doctor, run, ps, top, attach, send, questions, answer, log, board, tasks: tasksCommand, task: taskCommand, formula: formulaCommand, permissions: permissionsCommand, agents: (args2) => libraryCommand("agents", args2), skills: (args2) => libraryCommand("skills", args2), mcp: (args2) => ["add", "rm", "mv", "cp", "test", "show", "edit"].includes(args2[0]) ? editCommand("mcp", args2) : libraryCommand("mcp", args2), hooks: (args2) => libraryCommand("hooks", args2), providers: (args2) => libraryCommand("providers", args2), provider: (args2) => editCommand("providers", args2), teams: teamsCommand, trust: trustCommand, agent: (args2) => editCommand("agents", args2), team: (args2) => editCommand("teams", args2), skill: (args2) => editCommand("skills", args2), hook: (args2) => editCommand("hooks", args2), verify, recall, pause, resume, language, interrupt, acp: acpCommand, web: webCommand, backup: backupCommand, restore: restoreCommand };
16
+ import { terminalCommand } from "./terminal.js";
17
+ var commands = { schedule: scheduleCommand, setup: (args2) => setup(args2, alpHome()), init: (args2) => project("init", args2), upgrade: (args2) => project("upgrade", args2), update: (args2) => project("upgrade", args2), daemon, doctor, run, ps, top, attach, send, questions, answer, log, board, tasks: tasksCommand, task: taskCommand, formula: formulaCommand, permissions: permissionsCommand, agents: (args2) => libraryCommand("agents", args2), skills: (args2) => libraryCommand("skills", args2), mcp: (args2) => ["add", "rm", "mv", "cp", "test", "show", "edit"].includes(args2[0]) ? editCommand("mcp", args2) : libraryCommand("mcp", args2), hooks: (args2) => libraryCommand("hooks", args2), providers: (args2) => libraryCommand("providers", args2), provider: (args2) => editCommand("providers", args2), teams: teamsCommand, trust: trustCommand, agent: (args2) => editCommand("agents", args2), team: (args2) => editCommand("teams", args2), skill: (args2) => editCommand("skills", args2), hook: (args2) => editCommand("hooks", args2), verify, recall, pause, resume, language, interrupt, acp: acpCommand, web: webCommand, backup: backupCommand, restore: restoreCommand, terminal: terminalCommand };
16
18
  var [command, ...args] = process.argv.slice(2);
17
19
  try {
18
20
  if (command === "--version") {
19
- console.log(true ? "0.0.1-beta.3" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
21
+ console.log(true ? "0.0.1-beta.4" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
20
22
  } else {
21
23
  if (!Object.hasOwn(commands, command)) throw new UsageError();
22
24
  await commands[command](args);
@@ -1,6 +1,14 @@
1
+ /**
2
+ * Starts the Paseo integration. For one release, a home that still has only the descriptor
3
+ * registered before the rename (`integrations/web.json`) keeps working, with a hint to re-register.
4
+ * Exported for tests.
5
+ * @param {string} home
6
+ */
7
+ export function startPaseoIntegration(home: string): Promise<any>;
1
8
  /**
2
9
  * alp web pair: the link that pairs Paseo's app on another device with alpd through Paseo's relay
3
- * (ALPD §64). The device then signs in with the token that `alp web --print` shows.
10
+ * (ALPD §64). The device then signs in with the password this command prints (the token kept in
11
+ * ~/.alp/paseo.json).
4
12
  */
5
13
  /**
6
14
  * `web.pairing` is an extension the integration registers on its alpd connection. Right after an
@@ -1,55 +1,111 @@
1
1
  // packages/cli/src/interfaces.js
2
- import { spawn } from "node:child_process";
3
- import { readFile } from "node:fs/promises";
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { readFile, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { parseArgs } from "node:util";
6
- import { alpHome, lockAlive, readLock, startIntegration } from "@anhlp/alp-client/index";
6
+ import { alpHome, discoverIntegration, startIntegration } from "@anhlp/alp-client/index";
7
7
  import { UsageError } from "./usage.js";
8
8
  import { built, start, running } from "./daemon.js";
9
9
  import { DAEMON_ENTRY, distributionEntry } from "./paths.js";
10
+ import { alpWebEntry, refusal, stopPreRenamePaseo, stopWebService, webOwner } from "./web-service.js";
11
+ var PASEO = "paseo";
12
+ var WEB = "web";
13
+ async function startPaseoIntegration(home) {
14
+ const entry = await discoverIntegration(PASEO, { home });
15
+ if (!entry) {
16
+ const legacy = path.join(home, "integrations", "web.json");
17
+ const legacyEntry = await legacyDescriptorEntry(legacy, home);
18
+ if (legacyEntry) {
19
+ console.error(`Using the Paseo integration registered before its rename (${legacy}); run \`alp-paseo-integration --register\` to register it again.`);
20
+ return startIntegration({ name: PASEO, entry: legacyEntry, home });
21
+ }
22
+ }
23
+ return startIntegration({ name: PASEO, home });
24
+ }
25
+ async function legacyDescriptorEntry(file, home) {
26
+ const raw = await readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => void 0);
27
+ const isFile = (candidate) => stat(candidate).then((info) => info.isFile(), () => false);
28
+ for (const field of ["main", "entry", "path", "command"]) {
29
+ const value = raw?.[field];
30
+ if (typeof value !== "string" || !value.trim()) continue;
31
+ const candidates = path.isAbsolute(value) ? [value] : [path.resolve(path.dirname(file), value), path.resolve(home, value)];
32
+ for (const candidate of candidates) if (await isFile(candidate)) return candidate;
33
+ }
34
+ return void 0;
35
+ }
36
+ async function projectArgs() {
37
+ const cwd = process.cwd();
38
+ return await stat(path.join(cwd, ".alp")).then((info) => info.isDirectory(), () => false) ? ["--project", cwd] : [];
39
+ }
40
+ async function webDisabled(home) {
41
+ const settings = await readFile(path.join(home, "settings.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
42
+ return settings?.web?.enabled === false;
43
+ }
44
+ var DISABLED = 'alp web is disabled by settings ("web.enabled": false).';
45
+ async function serveWeb(home, entry) {
46
+ await stopPreRenamePaseo(home);
47
+ const held = await webOwner(home);
48
+ const published = await readFile(path.join(home, "web.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
49
+ const claimed = published?.app === "alp-web" && published.pid === await readFile(path.join(home, "integrations", "web.lock"), "utf8").then((text) => JSON.parse(text)?.pid, () => void 0);
50
+ if ("reason" in held && held.alive && held.kind === "foreign" && !claimed) throw new Error(`${refusal(home)} (${held.reason}). Stop it, then run alp web again.`);
51
+ let lock;
52
+ try {
53
+ lock = await startIntegration({ name: WEB, entry, home, args: await projectArgs() });
54
+ } catch (error) {
55
+ const owner = await webOwner(home);
56
+ if ("reason" in owner && owner.alive) throw new Error(`${error?.message ?? error}
57
+ ${refusal(home)} (${owner.reason}). If it is not alp web, stop it, then run alp web again.`, { cause: error });
58
+ throw error;
59
+ }
60
+ const info = await readFile(path.join(home, "web.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
61
+ if (info?.app !== "alp-web" || info.pid !== lock.pid) {
62
+ const owner = await webOwner(home);
63
+ throw new Error(`${refusal(home)}${"reason" in owner ? ` (${owner.reason})` : ""}. Stop pid ${lock.pid}, then run alp web again.`);
64
+ }
65
+ if (typeof info.url !== "string" || typeof info.token !== "string") throw new Error(`alp web published no address; see ${path.join(home, "logs", "integrations", "alp-web.log")}`);
66
+ return `${info.url}#token=${info.token}`;
67
+ }
10
68
  async function webCommand(args) {
11
69
  if (args[0] === "pair") return pairCommand(args.slice(1));
70
+ if (args[0] === "rotate") return rotateCommand(args.slice(1));
12
71
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { print: { type: "boolean" } } });
13
72
  if (positionals.length) throw new UsageError();
14
- const lock = await readLock(alpHome()).catch(() => void 0);
15
- await start();
16
- try {
17
- const settings = JSON.parse(await readFile(path.join(alpHome(), "settings.json"), "utf8"));
18
- if (settings?.web?.enabled === false) throw new Error('ALP web app is disabled by settings ("web.enabled": false).');
19
- } catch (error) {
20
- if (/disabled by settings/.test(error?.message ?? "")) throw error;
21
- }
22
- await startIntegration();
73
+ const entry = alpWebEntry();
23
74
  const home = alpHome();
24
- const file = path.join(home, "web.json");
25
- let info;
26
- for (let i = 0; i < 100; i++) {
27
- info = await readFile(file, "utf8").then(JSON.parse, () => void 0);
28
- let alive = false;
29
- try {
30
- if (info?.pid) {
31
- process.kill(info.pid, 0);
32
- alive = true;
33
- }
34
- } catch {
35
- alive = false;
36
- }
37
- let integrationPid = void 0;
38
- try {
39
- integrationPid = JSON.parse(await readFile(path.join(home, "integrations", "web.lock"), "utf8"))?.pid;
40
- } catch {
41
- }
42
- if (info?.token && info?.url && alive && info.pid !== lock?.pid && (!integrationPid || info.pid === integrationPid)) break;
43
- info = void 0;
44
- await new Promise((resolve) => setTimeout(resolve, 100));
45
- }
46
- if (!info) throw new Error(lockAlive(lock) ? 'alpd serves no web app: it is older than this alp (alp daemon restart), or "web": { "enabled": false } is set in ~/.alp/settings.json' : "alpd started without its web app; see ~/.alp/logs/alpd.log");
47
- const url = `${info.url}#token=${info.token}`;
75
+ if (await webDisabled(home)) throw new Error(DISABLED);
76
+ await start();
77
+ const url = await serveWeb(home, entry);
48
78
  console.log(url);
49
79
  if (values.print) return;
50
80
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
51
81
  spawn(opener, [url], { stdio: "ignore", detached: true }).on("error", () => console.error("Open the address above in your browser")).unref();
52
82
  }
83
+ async function rotateCommand(args) {
84
+ if (args.length) throw new UsageError();
85
+ const entry = alpWebEntry();
86
+ const home = alpHome();
87
+ await start();
88
+ const project = await projectArgs();
89
+ for (let attempt = 1; ; attempt++) {
90
+ await stopWebService(home);
91
+ const rotated = await new Promise((resolve) => {
92
+ execFile(
93
+ process.execPath,
94
+ [entry, "--rotate", ...project],
95
+ { env: { ...process.env, ALP_HOME: home }, encoding: "utf8", timeout: 6e4, killSignal: "SIGTERM" },
96
+ (error, stdout, stderr) => resolve({ code: error ? typeof error.code === "number" ? error.code : 1 : 0, output: `${stderr}${stdout}`.trim() })
97
+ );
98
+ });
99
+ if (rotated.code === 0) break;
100
+ if (rotated.code !== 3 || attempt >= 3) throw new Error(`alp web could not rotate its token${rotated.output ? `:
101
+ ${rotated.output.slice(-2e3)}` : ""}`);
102
+ }
103
+ if (await webDisabled(home)) {
104
+ console.log(`alp web has a new token; the old one no longer works. ${DISABLED.replace(/\.$/, "")}, so it was not started.`);
105
+ return;
106
+ }
107
+ console.log(await serveWeb(home, entry));
108
+ }
53
109
  async function requestPairing(client, params, attempts = 50) {
54
110
  let last;
55
111
  for (let attempt = 0; attempt < attempts; attempt += 1) {
@@ -66,7 +122,8 @@ async function pairCommand(args) {
66
122
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { on: { type: "boolean" }, off: { type: "boolean" } } });
67
123
  if (positionals.length || values.on && values.off) throw new UsageError();
68
124
  await start();
69
- await startIntegration();
125
+ const home = alpHome();
126
+ await startPaseoIntegration(home);
70
127
  const client = await running();
71
128
  try {
72
129
  const pairing = await requestPairing(client, values.on ? { enable: true } : values.off ? { enable: false } : {});
@@ -75,9 +132,11 @@ async function pairCommand(args) {
75
132
  return;
76
133
  }
77
134
  console.log(`Relay ${pairing.endpoint}: ${pairing.connected ? "connected" : "connecting"}`);
78
- console.log(`Open this link on the device, or scan the QR code in the web app (Settings \u2192 host \u2192 Pair device):
135
+ console.log(`Open this link on the device that runs Paseo's app:
79
136
  ${pairing.url}`);
80
- console.log("When Paseo's app asks for a password, give it alpd's token: the part after #token= in alp web --print.");
137
+ const password = await readFile(path.join(home, `${PASEO}.json`), "utf8").then((text) => JSON.parse(text)?.token).catch(() => void 0);
138
+ if (typeof password === "string" && password) console.log(`When Paseo's app asks for a password, give it: ${password}`);
139
+ else console.log(`When Paseo's app asks for a password, give it the token in ${path.join(home, `${PASEO}.json`)}.`);
81
140
  } finally {
82
141
  client.close();
83
142
  }
@@ -92,5 +151,6 @@ export {
92
151
  acpCommand,
93
152
  pairCommand,
94
153
  requestPairing,
154
+ startPaseoIntegration,
95
155
  webCommand
96
156
  };
@@ -0,0 +1,2 @@
1
+ /** One-shot continuations of an existing root session; timestamps need an explicit offset. */
2
+ export function scheduleCommand(args: any): Promise<void>;
@@ -0,0 +1,45 @@
1
+ // packages/cli/src/schedules.js
2
+ import path from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { running } from "./daemon.js";
5
+ import { UsageError } from "./usage.js";
6
+ async function scheduleCommand(args) {
7
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
8
+ project: { type: "string" },
9
+ session: { type: "string" },
10
+ at: { type: "string" },
11
+ prompt: { type: "string" },
12
+ status: { type: "string" },
13
+ json: { type: "boolean" }
14
+ } });
15
+ const [action, id, ...extra] = positionals;
16
+ if (!["create", "list", "show", "cancel"].includes(action) || extra.length) throw new UsageError();
17
+ if (action === "show" || action === "cancel" ? !id : !!id) throw new UsageError();
18
+ if (action !== "create" && (values.session || values.at || values.prompt)) throw new UsageError();
19
+ if (action !== "list" && values.status) throw new UsageError();
20
+ if (action === "create" && (!values.session || !values.prompt?.trim() || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(values.at ?? "") || !Number.isFinite(Date.parse(values.at)))) throw new Error("create needs --session, --prompt and --at ISO timestamp with Z or a UTC offset (for example 2026-10-20T16:00:00+07:00)");
21
+ const projectRoot = path.resolve(values.project ?? process.cwd());
22
+ const params = action === "create" ? { projectRoot, sessionId: values.session, dueAt: values.at, prompt: values.prompt } : { projectRoot, ...id ? { id } : {}, ...values.status ? { status: values.status } : {} };
23
+ const client = await running();
24
+ try {
25
+ const result = await client.request(`schedule.${action === "show" ? "get" : action}`, params);
26
+ if (values.json) {
27
+ console.log(JSON.stringify(result));
28
+ return;
29
+ }
30
+ const jobs = result.jobs ?? [result.job];
31
+ if (!jobs.length) console.log("No scheduled continuations");
32
+ for (const job of jobs) {
33
+ console.log(`${job.id} ${job.status} ${job.dueAt} session ${job.sessionId}`);
34
+ console.log(` ${job.prompt}`);
35
+ if (job.attemptedAt) console.log(` attempted ${job.attemptedAt}; attempts ${job.attempts}; late ${job.lateByMs ?? 0} ms`);
36
+ for (const field of ["deliveredAt", "failedAt", "cancelledAt"]) if (job[field]) console.log(` ${field}: ${job[field]}`);
37
+ if (job.lastError) console.log(` ${job.lastError.code ?? "error"}: ${job.lastError.message}`);
38
+ }
39
+ } finally {
40
+ client.close();
41
+ }
42
+ }
43
+ export {
44
+ scheduleCommand
45
+ };
@@ -7,9 +7,11 @@ export function answerer(client: any): {
7
7
  close(): void;
8
8
  };
9
9
  /** Streams a tree until its root is idle; the first Ctrl-C interrupts it, the second detaches. */
10
- export function follow(client: any, rootId: any, print: any, { untilIdle, interactive }: {
10
+ export function follow(client: any, rootId: any, print: any, { untilIdle, interactive, targetOnly, promptId }: {
11
11
  untilIdle: any;
12
12
  interactive?: boolean | undefined;
13
+ targetOnly?: boolean | undefined;
14
+ promptId: any;
13
15
  }): Promise<any>;
14
16
  export function run(args: any): Promise<void>;
15
17
  export function ps(args: any): Promise<void>;
@@ -121,7 +121,7 @@ function answerer(client) {
121
121
  }
122
122
  };
123
123
  }
124
- function follow(client, rootId, print, { untilIdle, interactive = false }) {
124
+ function follow(client, rootId, print, { untilIdle, interactive = false, targetOnly = false, promptId }) {
125
125
  const answering = interactive ? answerer(client) : void 0;
126
126
  const done = new Promise((resolve, reject) => {
127
127
  let failed = false;
@@ -144,10 +144,13 @@ function follow(client, rootId, print, { untilIdle, interactive = false }) {
144
144
  process.on("SIGINT", onSignal);
145
145
  client.onClose((error) => {
146
146
  process.off("SIGINT", onSignal);
147
+ answering?.close();
147
148
  reject(error);
148
149
  });
149
150
  let rootEnded = false;
150
151
  client.onEvent((envelope) => {
152
+ if (targetOnly && envelope.sessionId !== rootId) return;
153
+ if (promptId && envelope.event.type === "prompt.failed" && envelope.event.clientMessageId !== promptId) return;
151
154
  print(envelope);
152
155
  answering?.accept(envelope.event);
153
156
  const { sessionId, event } = envelope;
@@ -234,28 +237,36 @@ async function attach(args) {
234
237
  }
235
238
  }
236
239
  async function send(args) {
237
- const { values, positionals: [sessionId, ...words] } = parseArgs({ args, allowPositionals: true, options: { json: { type: "boolean" } } });
240
+ const { values, positionals: [sessionId, ...words] } = parseArgs({ args, allowPositionals: true, options: { json: { type: "boolean" }, prompt: { type: "boolean" } } });
238
241
  const text = words.join(" ").trim();
239
242
  if (!sessionId || !text) throw new UsageError();
240
243
  const client = await connect(await start(), { name: "alp-cli", version: "1" });
244
+ let subscriptionId;
241
245
  try {
242
246
  const { session } = await client.request("session.get", { sessionId });
243
- if (session.parentId) {
247
+ if (session.parentId && !values.prompt) {
244
248
  await client.request("session.message", { sessionId, text });
245
249
  if (!values.json) console.error(`Sent to ${session.agent} as mail from the user`);
246
250
  return;
247
251
  }
248
- const done = follow(client, sessionId, printer(values.json), { untilIdle: true, interactive: !values.json && process.stdin.isTTY });
249
252
  const live = session.status === "idle" || session.status === "running";
250
- if (live) await client.request("session.attach", { sessionId, replay: false });
251
- else await client.request("session.create", { sessionId, spec: { cwd: session.projectRoot } });
252
- const steer = live && !!session.activeTurnId;
253
- await client.request("session.prompt", { sessionId, clientMessageId: randomUUID(), delivery: steer ? "steer" : "auto", content: [{ type: "text", text }] });
254
- if (!values.json) console.error(steer ? "Steered the running turn" : live ? "Started a turn" : "Resumed the session");
253
+ if (values.prompt && !live) throw new Error("Explicit prompts require a live session; use default alp send to reopen a root");
254
+ const clientMessageId = randomUUID();
255
+ const done = follow(client, sessionId, printer(values.json), { untilIdle: true, interactive: !values.json && process.stdin.isTTY, targetOnly: !!session.parentId, promptId: clientMessageId });
256
+ if (live) {
257
+ const attached = await client.request("session.attach", { sessionId, replay: false });
258
+ subscriptionId = attached.session.id;
259
+ } else {
260
+ await client.request("session.create", { sessionId, spec: { cwd: session.projectRoot } });
261
+ subscriptionId = sessionId;
262
+ }
263
+ const steer = !values.prompt && live && !!session.activeTurnId;
264
+ await client.request("session.prompt", { sessionId, clientMessageId, delivery: steer ? "steer" : "auto", content: [{ type: "text", text }] });
265
+ if (!values.json) console.error(values.prompt ? "Submitted an explicit prompt" : steer ? "Steered the running turn" : live ? "Started a turn" : "Resumed the session");
255
266
  if (!await done) process.exitCode = 1;
256
- await client.request("session.release", { sessionId }).catch(() => {
257
- });
258
267
  } finally {
268
+ if (subscriptionId) await client.request("session.release", { sessionId: subscriptionId }).catch(() => {
269
+ });
259
270
  client.close();
260
271
  }
261
272
  }
@@ -0,0 +1,8 @@
1
+ export function terminalCommand(args: any, { home, out }?: {
2
+ home?: string | undefined;
3
+ out?: {
4
+ (...data: any[]): void;
5
+ (message?: any, ...optionalParams: any[]): void;
6
+ } | undefined;
7
+ }): Promise<void>;
8
+ export function TERMINAL_WARNING(user?: string): string;
@@ -0,0 +1,79 @@
1
+ // packages/cli/src/terminal.js
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { alpHome } from "@anhlp/alp-client/index";
7
+ import { UsageError } from "./usage.js";
8
+ var TERMINAL_WARNING = (user = os.userInfo().username) => [
9
+ `A remote terminal runs a real shell on this computer as ${user}, with that account's full access:`,
10
+ "every file it can read or write, any program, and the network.",
11
+ "A workspace only picks the directory the shell starts in. It is NOT a sandbox.",
12
+ "Only gateway credentials that hold the terminal.exec permission can open one (never a default);",
13
+ "Paseo app devices paired with this host only after alp terminal allow --paseo."
14
+ ].join("\n");
15
+ var file = (home) => path.join(home, "terminal.json");
16
+ async function read(home) {
17
+ const info = await lstat(file(home)).catch(() => void 0);
18
+ if (!info) return { exists: false, value: void 0, private: true };
19
+ const own = !process.getuid || info.uid === process.getuid();
20
+ const value = info.isFile() ? await readFile(file(home), "utf8").then(JSON.parse, () => void 0) : void 0;
21
+ return { exists: true, value, private: info.isFile() && own && !(info.mode & 63) };
22
+ }
23
+ async function write(home, value) {
24
+ await mkdir(home, { recursive: true, mode: 448 });
25
+ const temporary = `${file(home)}.${randomUUID().slice(0, 8)}.tmp`;
26
+ await writeFile(temporary, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
27
+ await chmod(temporary, 384);
28
+ await rename(temporary, file(home)).catch(async (error) => {
29
+ await rm(temporary, { force: true });
30
+ throw error;
31
+ });
32
+ }
33
+ async function terminalCommand(args, { home = alpHome(), out = console.log } = {}) {
34
+ const [action, ...rest] = args;
35
+ const json = rest.includes("--json");
36
+ const current = await read(home);
37
+ const limits = current.value && typeof current.value.limits === "object" && current.value.limits ? { limits: current.value.limits } : {};
38
+ const paseo = rest.includes("--paseo");
39
+ const paseoBefore = current.private && current.value?.allowPaseoTerminal === true;
40
+ if (action === "allow") {
41
+ if (!rest.includes("--yes")) {
42
+ out(`${TERMINAL_WARNING()}
43
+
44
+ Run again with --yes to allow remote terminals on this host.`);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ const allowPaseoTerminal = paseo || paseoBefore;
49
+ await write(home, { version: 1, allowRemoteTerminal: true, ...allowPaseoTerminal ? { allowPaseoTerminal: true } : {}, ...limits });
50
+ out(json ? JSON.stringify({ allowRemoteTerminal: true, allowPaseoTerminal }) : `${TERMINAL_WARNING()}
51
+
52
+ Remote terminals are allowed on this host (${file(home)})${allowPaseoTerminal ? ", also for every Paseo app device paired with it" : ""}. alp terminal deny turns them off and ends every open one.`);
53
+ return;
54
+ }
55
+ if (action === "deny") {
56
+ if (paseo) {
57
+ const allowed = current.private && current.value?.allowRemoteTerminal === true;
58
+ await write(home, { version: 1, allowRemoteTerminal: allowed, ...limits });
59
+ out(json ? JSON.stringify({ allowRemoteTerminal: allowed, allowPaseoTerminal: false }) : "Paseo app devices can no longer open terminals on this host; the bridge ends the terminals it holds.");
60
+ return;
61
+ }
62
+ await write(home, { version: 1, allowRemoteTerminal: false, ...limits });
63
+ out(json ? JSON.stringify({ allowRemoteTerminal: false, allowPaseoTerminal: false }) : "Remote terminals are off on this host; alpd ends every open terminal within 5 seconds.");
64
+ return;
65
+ }
66
+ if (action === "status") {
67
+ const allowed = current.private && current.value?.version === 1 && current.value.allowRemoteTerminal === true;
68
+ const allowPaseoTerminal = allowed && current.value.allowPaseoTerminal === true;
69
+ const problem = current.exists && !current.private ? `${file(home)} is not a private file of this user, so terminals stay off` : void 0;
70
+ if (json) out(JSON.stringify({ allowRemoteTerminal: allowed, allowPaseoTerminal, ...limits, ...problem ? { problem } : {} }));
71
+ else out([`Remote terminals: ${allowed ? "allowed" : "off"}${allowPaseoTerminal ? " (also for paired Paseo app devices)" : ""}`, ...problem ? [problem] : [], ...allowed ? ["", TERMINAL_WARNING()] : []].join("\n"));
72
+ return;
73
+ }
74
+ throw new UsageError();
75
+ }
76
+ export {
77
+ TERMINAL_WARNING,
78
+ terminalCommand
79
+ };