pinomad 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-b457q0tm.js → chunk-41q0saz0.js} +11 -8
  3. package/dist/chunk-42nqtx5r.js +22 -0
  4. package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
  5. package/dist/{chunk-xqycs43g.js → chunk-47ff1d1t.js} +35 -7
  6. package/dist/{chunk-n1m1tjrk.js → chunk-5qf1grkk.js} +19 -13
  7. package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
  8. package/dist/{chunk-sd5fkxjj.js → chunk-7wj0jh3t.js} +1 -1
  9. package/dist/{chunk-4dcs8sbj.js → chunk-agq4tn9r.js} +43 -21
  10. package/dist/{chunk-as6pyf00.js → chunk-aqj9wfmb.js} +1 -1
  11. package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
  12. package/dist/{chunk-gtdpjqa3.js → chunk-gg2dcqbg.js} +186 -45
  13. package/dist/{chunk-6d16c2fv.js → chunk-jeqsdcj1.js} +13 -6
  14. package/dist/{chunk-67gq282t.js → chunk-ksfxqd8s.js} +491 -34
  15. package/dist/{chunk-9h5vawnm.js → chunk-mnmqyq7d.js} +237 -27
  16. package/dist/{chunk-jt34g8st.js → chunk-np4jd070.js} +154 -2
  17. package/dist/{chunk-44pnts98.js → chunk-nppgxh6c.js} +1 -1
  18. package/dist/chunk-nq57fdnq.js +106 -0
  19. package/dist/{chunk-vvj9b1gc.js → chunk-t5abqsva.js} +174 -52
  20. package/dist/{chunk-tj57aqav.js → chunk-vqkb70d7.js} +2 -2
  21. package/dist/{chunk-h5q2y57m.js → chunk-vx7dy07y.js} +10 -4
  22. package/dist/pinomad.js +15 -14
  23. package/package.json +2 -1
  24. package/web/assets/astro-DOly5024.js +1 -0
  25. package/web/assets/c-BIGW1oBm.js +1 -0
  26. package/web/assets/core-DnpzcQzs.js +4 -0
  27. package/web/assets/cpp-DddhVWhK.js +1 -0
  28. package/web/assets/csharp-B_TYUkTI.js +1 -0
  29. package/web/assets/css-Z72CF80r.js +1 -0
  30. package/web/assets/dart-CtKP3W-j.js +1 -0
  31. package/web/assets/diff-D97Zzqfu.js +1 -0
  32. package/web/assets/dockerfile-BcOcwvcX.js +1 -0
  33. package/web/assets/elixir-DkJLSxtU.js +1 -0
  34. package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
  35. package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
  36. package/web/assets/fish-BvzEVeQv.js +1 -0
  37. package/web/assets/go-C27-OAKa.js +1 -0
  38. package/web/assets/graphql-ChdNCCLP.js +1 -0
  39. package/web/assets/haskell-Df6bDoY_.js +1 -0
  40. package/web/assets/hcl-BWvSN4gD.js +1 -0
  41. package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
  42. package/web/assets/highlights-DnHv6NBM.js +4 -0
  43. package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
  44. package/web/assets/html-BlS82ilI.js +1 -0
  45. package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
  46. package/web/assets/index-DNsTTgPE.js +396 -0
  47. package/web/assets/ini-BEwlwnbL.js +1 -0
  48. package/web/assets/java-CylS5w8V.js +1 -0
  49. package/web/assets/javascript-wDzz0qaB.js +1 -0
  50. package/web/assets/json-Cp-IABpG.js +1 -0
  51. package/web/assets/jsonc-Des-eS-w.js +1 -0
  52. package/web/assets/jsx-g9-lgVsj.js +1 -0
  53. package/web/assets/kotlin-BdnUsdx6.js +1 -0
  54. package/web/assets/less-B1dDrJ26.js +1 -0
  55. package/web/assets/lua-BaeVxFsk.js +1 -0
  56. package/web/assets/make-CHLpvVh8.js +1 -0
  57. package/web/assets/markdown-Cvjx9yec.js +1 -0
  58. package/web/assets/nix-CwoSXNpI.js +1 -0
  59. package/web/assets/objective-c-DXmwc3jG.js +1 -0
  60. package/web/assets/php-DoruYMF0.js +1 -0
  61. package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
  62. package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
  63. package/web/assets/pierre-light-480U9XYS.js +1 -0
  64. package/web/assets/pierre-light-BVSW-Xev.js +1 -0
  65. package/web/assets/postcss-DnzrXTVv.js +1 -0
  66. package/web/assets/powershell-Dd0kTxKP.js +1 -0
  67. package/web/assets/prisma-Vru482bI.js +1 -0
  68. package/web/assets/proto-C7zT0LnQ.js +1 -0
  69. package/web/assets/python-B6aJPvgy.js +1 -0
  70. package/web/assets/ruby-B43yHlf9.js +1 -0
  71. package/web/assets/rust-DPvrY3IQ.js +1 -0
  72. package/web/assets/scala-CqE71os6.js +1 -0
  73. package/web/assets/scss-Cw6XMfVs.js +1 -0
  74. package/web/assets/shellscript-Yzrsuije.js +1 -0
  75. package/web/assets/shiki-BuPP5HCg.js +2 -0
  76. package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
  77. package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
  78. package/web/assets/sql-DkvoVjww.js +1 -0
  79. package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
  80. package/web/assets/svelte-DoIXUgJc.js +1 -0
  81. package/web/assets/swift-C2oV4EkX.js +1 -0
  82. package/web/assets/terraform-BETggiCN.js +1 -0
  83. package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
  84. package/web/assets/toml-vGWfd6FD.js +1 -0
  85. package/web/assets/tsx-COt5Ahok.js +1 -0
  86. package/web/assets/typescript-BPQ3VLAy.js +1 -0
  87. package/web/assets/vue-CwSeJimo.js +1 -0
  88. package/web/assets/xml-sdJ4AIDG.js +1 -0
  89. package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
  90. package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
  91. package/web/assets/yaml-Buea-lGh.js +1 -0
  92. package/web/assets/zig-VOosw3JB.js +1 -0
  93. package/web/index.html +2 -2
  94. package/dist/chunk-etf7ef97.js +0 -55
  95. package/dist/chunk-greb3dp6.js +0 -22
  96. package/web/assets/index-BhHrKpoo.js +0 -395
@@ -5,7 +5,7 @@ import {
5
5
  openHostSettings,
6
6
  sameList,
7
7
  settingsDefaults
8
- } from "./chunk-44pnts98.js";
8
+ } from "./chunk-nppgxh6c.js";
9
9
  import {
10
10
  DevicesDoc,
11
11
  RevocationsDoc,
@@ -16,19 +16,21 @@ import {
16
16
  pairingOffers
17
17
  } from "./chunk-t91zt5f0.js";
18
18
  import {
19
+ REMOVAL_NOTICE_FILE,
20
+ markRemoved,
19
21
  normalizeOrigin,
20
22
  readServerBinding,
21
23
  readServerConfig,
22
24
  resolveRelayOrigin
23
- } from "./chunk-etf7ef97.js";
25
+ } from "./chunk-nq57fdnq.js";
24
26
  import {
25
27
  serveWebClient
26
- } from "./chunk-wb10efyq.js";
28
+ } from "./chunk-445cevpm.js";
27
29
  import {
28
30
  relayHostId,
29
31
  relayWsBase,
30
32
  startRelayLink
31
- } from "./chunk-y127fe1a.js";
33
+ } from "./chunk-g3ahp7b9.js";
32
34
  import {
33
35
  ATTACHMENT_TTL_MS,
34
36
  INVALID_FRAME_CLOSE_CODE,
@@ -54,7 +56,7 @@ import {
54
56
  rtcCandidateType,
55
57
  sdpCandidates,
56
58
  withRtcCandidates
57
- } from "./chunk-xqycs43g.js";
59
+ } from "./chunk-47ff1d1t.js";
58
60
  import {
59
61
  ENDPOINT_SET_SCHEMA_VERSION,
60
62
  MAX_HOST_ENDPOINT_URL_LENGTH,
@@ -77,13 +79,13 @@ import {
77
79
  import {
78
80
  packagedVersion,
79
81
  webRoot
80
- } from "./chunk-sd5fkxjj.js";
82
+ } from "./chunk-7wj0jh3t.js";
81
83
  import {
82
84
  __require
83
85
  } from "./chunk-df32avw6.js";
84
86
 
85
87
  // apps/host/src/main.ts
86
- import { randomUUID as randomUUID3 } from "node:crypto";
88
+ import { randomUUID as randomUUID4 } from "node:crypto";
87
89
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
88
90
  import { homedir as homedir3 } from "node:os";
89
91
  import { join as join12, resolve as resolve7 } from "node:path";
@@ -712,11 +714,7 @@ async function checkoutInfo(harness, input, context) {
712
714
  }
713
715
  };
714
716
  }
715
- async function cleanupArchivedWorktrees(harness, rootId, context) {
716
- const index = await harness.snapshot(IndexDoc, context);
717
- const checkouts = index?.checkouts ?? [];
718
- if (checkouts.length === 0)
719
- return [];
717
+ async function conversationSubtree(harness, rootId, context) {
720
718
  const all = [];
721
719
  let cursor;
722
720
  do {
@@ -743,6 +741,27 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
743
741
  }
744
742
  }
745
743
  }
744
+ return subtree;
745
+ }
746
+ async function familyRoot(harness, id, context) {
747
+ let current = id;
748
+ const seen = new Set;
749
+ while (!seen.has(current)) {
750
+ seen.add(current);
751
+ const record = await harness.commit((tx) => tx.conversation(current), context);
752
+ const parent = record?.parent?.conversationId ?? record?.owner?.conversationId;
753
+ if (parent === undefined)
754
+ return current;
755
+ current = parent;
756
+ }
757
+ return current;
758
+ }
759
+ async function cleanupArchivedWorktrees(harness, rootId, context) {
760
+ const index = await harness.snapshot(IndexDoc, context);
761
+ const checkouts = index?.checkouts ?? [];
762
+ if (checkouts.length === 0)
763
+ return [];
764
+ const subtree = await conversationSubtree(harness, rootId, context);
746
765
  const kept = [];
747
766
  for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
748
767
  const reason = await removeWorktree(record);
@@ -1451,6 +1470,7 @@ var todo = {
1451
1470
  };
1452
1471
 
1453
1472
  // apps/host/src/gateway.ts
1473
+ import { stat as stat6 } from "node:fs/promises";
1454
1474
  import { createServer } from "node:http";
1455
1475
  import { homedir, networkInterfaces } from "node:os";
1456
1476
  import { join as join7, sep as sep5 } from "node:path";
@@ -2056,6 +2076,329 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
2056
2076
  };
2057
2077
  }
2058
2078
 
2079
+ // apps/host/src/terminals.ts
2080
+ import { randomUUID as randomUUID2 } from "node:crypto";
2081
+ import { userInfo } from "node:os";
2082
+ var TERMINAL_LIMIT = 8;
2083
+ var BUFFER_SIZE = 512 * 1024;
2084
+ var FLUSH_MS = 8;
2085
+ var MAX_CHUNK = 16 * 1024;
2086
+ var MAX_TITLE = 1024;
2087
+ var TITLE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
2088
+
2089
+ class Terminals {
2090
+ #options;
2091
+ #limit;
2092
+ #terminals = new Map;
2093
+ #numbers = new Map;
2094
+ #watchers = new Map;
2095
+ constructor(options) {
2096
+ this.#options = options;
2097
+ this.#limit = options.limit ?? TERMINAL_LIMIT;
2098
+ }
2099
+ get limit() {
2100
+ return this.#limit;
2101
+ }
2102
+ list(owner) {
2103
+ const terminals = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner).map(info);
2104
+ return { owner, limit: this.#limit, terminals };
2105
+ }
2106
+ watch(owner, listener) {
2107
+ let set = this.#watchers.get(owner);
2108
+ if (set === undefined)
2109
+ this.#watchers.set(owner, set = new Set);
2110
+ set.add(listener);
2111
+ listener(this.list(owner));
2112
+ return () => {
2113
+ set.delete(listener);
2114
+ if (set.size === 0)
2115
+ this.#watchers.delete(owner);
2116
+ };
2117
+ }
2118
+ open(owner, cwd, size) {
2119
+ const count = [...this.#terminals.values()].filter((terminal2) => terminal2.owner === owner).length;
2120
+ if (count >= this.#limit)
2121
+ throw new Error(`Terminal limit reached (${this.#limit})`);
2122
+ const number = (this.#numbers.get(owner) ?? 0) + 1;
2123
+ this.#numbers.set(owner, number);
2124
+ const terminal = {
2125
+ id: randomUUID2(),
2126
+ owner,
2127
+ cwd,
2128
+ number,
2129
+ pty: undefined,
2130
+ generation: 0,
2131
+ cols: size.cols,
2132
+ rows: size.rows,
2133
+ title: undefined,
2134
+ rename: undefined,
2135
+ exit: undefined,
2136
+ buffer: "",
2137
+ start: 0,
2138
+ pending: "",
2139
+ flushTimer: undefined,
2140
+ titleCarry: "",
2141
+ viewers: new Set
2142
+ };
2143
+ this.#terminals.set(terminal.id, terminal);
2144
+ try {
2145
+ this.#start(terminal);
2146
+ } catch (error) {
2147
+ this.#terminals.delete(terminal.id);
2148
+ throw error;
2149
+ }
2150
+ this.#changed(owner);
2151
+ return terminal.id;
2152
+ }
2153
+ input(id, data, size) {
2154
+ const terminal = this.#get(id);
2155
+ if (size !== undefined)
2156
+ this.#resize(terminal, size);
2157
+ terminal.pty?.write(data);
2158
+ }
2159
+ resize(id, size) {
2160
+ this.#resize(this.#get(id), size);
2161
+ }
2162
+ rename(id, name) {
2163
+ const terminal = this.#get(id);
2164
+ const trimmed = name?.trim();
2165
+ terminal.rename = trimmed === undefined || trimmed === "" ? undefined : trimmed;
2166
+ this.#changed(terminal.owner);
2167
+ }
2168
+ restart(id, size) {
2169
+ const terminal = this.#get(id);
2170
+ this.#stop(terminal);
2171
+ terminal.start += terminal.buffer.length + terminal.pending.length + 1;
2172
+ terminal.buffer = "";
2173
+ terminal.pending = "";
2174
+ terminal.titleCarry = "";
2175
+ terminal.title = undefined;
2176
+ terminal.exit = undefined;
2177
+ terminal.cols = size.cols;
2178
+ terminal.rows = size.rows;
2179
+ for (const viewer of terminal.viewers)
2180
+ viewer.position = undefined;
2181
+ try {
2182
+ this.#start(terminal);
2183
+ } catch (error) {
2184
+ terminal.exit = { code: null };
2185
+ this.#changed(terminal.owner);
2186
+ throw error;
2187
+ }
2188
+ this.#changed(terminal.owner);
2189
+ }
2190
+ close(id) {
2191
+ const terminal = this.#get(id);
2192
+ this.#remove(terminal);
2193
+ this.#changed(terminal.owner);
2194
+ }
2195
+ closeOwners(owners) {
2196
+ for (const owner of new Set(owners)) {
2197
+ const owned = [...this.#terminals.values()].filter((terminal) => terminal.owner === owner);
2198
+ if (owned.length === 0)
2199
+ continue;
2200
+ for (const terminal of owned)
2201
+ this.#remove(terminal);
2202
+ this.#changed(owner);
2203
+ }
2204
+ }
2205
+ closeAll() {
2206
+ this.closeOwners([...this.#terminals.values()].map((terminal) => terminal.owner));
2207
+ }
2208
+ attach(id, since, sink) {
2209
+ const terminal = this.#get(id);
2210
+ const end = terminal.start + terminal.buffer.length;
2211
+ const position = since !== undefined && since >= terminal.start && since <= end ? since : undefined;
2212
+ const viewer = { sink, position, sending: false };
2213
+ terminal.viewers.add(viewer);
2214
+ this.#pump(terminal, viewer);
2215
+ return () => {
2216
+ terminal.viewers.delete(viewer);
2217
+ };
2218
+ }
2219
+ #get(id) {
2220
+ const terminal = this.#terminals.get(id);
2221
+ if (terminal === undefined)
2222
+ throw new Error("Unknown terminal");
2223
+ return terminal;
2224
+ }
2225
+ #start(terminal) {
2226
+ const generation = ++terminal.generation;
2227
+ const { shell, env } = this.#options;
2228
+ const pty = this.#options.spawn(shell.file, [...shell.args], {
2229
+ name: "xterm-256color",
2230
+ cols: terminal.cols,
2231
+ rows: terminal.rows,
2232
+ cwd: terminal.cwd,
2233
+ env: { ...env, TERM: "xterm-256color", COLORTERM: "truecolor" }
2234
+ });
2235
+ terminal.pty = pty;
2236
+ pty.onData((data) => {
2237
+ if (terminal.generation !== generation)
2238
+ return;
2239
+ terminal.pending += data;
2240
+ if (terminal.pending.length >= (this.#options.maxChunk ?? MAX_CHUNK))
2241
+ this.#flush(terminal);
2242
+ else
2243
+ terminal.flushTimer ??= setTimeout(() => this.#flush(terminal), this.#options.flushMs ?? FLUSH_MS);
2244
+ });
2245
+ pty.onExit(({ exitCode, signal }) => {
2246
+ if (terminal.generation !== generation || !this.#terminals.has(terminal.id))
2247
+ return;
2248
+ this.#flush(terminal);
2249
+ terminal.pty = undefined;
2250
+ terminal.exit = signal === undefined || signal === 0 ? { code: exitCode } : { code: exitCode, signal };
2251
+ this.#changed(terminal.owner);
2252
+ });
2253
+ }
2254
+ #stop(terminal) {
2255
+ if (terminal.flushTimer !== undefined)
2256
+ clearTimeout(terminal.flushTimer);
2257
+ terminal.flushTimer = undefined;
2258
+ terminal.generation++;
2259
+ const pty = terminal.pty;
2260
+ terminal.pty = undefined;
2261
+ try {
2262
+ pty?.kill();
2263
+ } catch {}
2264
+ }
2265
+ #remove(terminal) {
2266
+ this.#stop(terminal);
2267
+ this.#terminals.delete(terminal.id);
2268
+ for (const viewer of terminal.viewers)
2269
+ viewer.sink.ended("Terminal closed");
2270
+ terminal.viewers.clear();
2271
+ }
2272
+ #resize(terminal, size) {
2273
+ if (terminal.cols === size.cols && terminal.rows === size.rows)
2274
+ return;
2275
+ terminal.cols = size.cols;
2276
+ terminal.rows = size.rows;
2277
+ try {
2278
+ terminal.pty?.resize(size.cols, size.rows);
2279
+ } catch {}
2280
+ this.#changed(terminal.owner);
2281
+ }
2282
+ #flush(terminal) {
2283
+ if (terminal.flushTimer !== undefined)
2284
+ clearTimeout(terminal.flushTimer);
2285
+ terminal.flushTimer = undefined;
2286
+ const data = terminal.pending;
2287
+ if (data === "")
2288
+ return;
2289
+ terminal.pending = "";
2290
+ terminal.buffer += data;
2291
+ const bufferSize = this.#options.bufferSize ?? BUFFER_SIZE;
2292
+ if (terminal.buffer.length > bufferSize) {
2293
+ let cut = terminal.buffer.length - bufferSize;
2294
+ const line = terminal.buffer.indexOf(`
2295
+ `, cut);
2296
+ if (line !== -1 && line - cut < 1024)
2297
+ cut = line + 1;
2298
+ terminal.buffer = terminal.buffer.slice(cut);
2299
+ terminal.start += cut;
2300
+ }
2301
+ if (this.#readTitle(terminal, data))
2302
+ this.#changed(terminal.owner);
2303
+ for (const viewer of terminal.viewers)
2304
+ this.#pump(terminal, viewer);
2305
+ }
2306
+ #readTitle(terminal, data) {
2307
+ const text = terminal.titleCarry + data;
2308
+ let title;
2309
+ let consumed = 0;
2310
+ for (const match of text.matchAll(TITLE)) {
2311
+ title = match[1];
2312
+ consumed = match.index + match[0].length;
2313
+ }
2314
+ const open = text.lastIndexOf("\x1B]");
2315
+ terminal.titleCarry = open >= consumed && text.length - open <= MAX_TITLE ? text.slice(open) : "";
2316
+ if (title === undefined || title === terminal.title)
2317
+ return false;
2318
+ terminal.title = title;
2319
+ return terminal.rename === undefined;
2320
+ }
2321
+ async#pump(terminal, viewer) {
2322
+ if (viewer.sending)
2323
+ return;
2324
+ viewer.sending = true;
2325
+ try {
2326
+ const maxChunk = this.#options.maxChunk ?? MAX_CHUNK;
2327
+ while (terminal.viewers.has(viewer)) {
2328
+ const end = terminal.start + terminal.buffer.length;
2329
+ const reset = viewer.position === undefined || viewer.position < terminal.start;
2330
+ const from = reset ? terminal.start : viewer.position;
2331
+ if (!reset && from >= end)
2332
+ return;
2333
+ let to = reset ? end : Math.min(end, from + maxChunk);
2334
+ if (to < end && isHighSurrogate(terminal.buffer.charCodeAt(to - terminal.start - 1)))
2335
+ to--;
2336
+ const data = terminal.buffer.slice(from - terminal.start, to - terminal.start);
2337
+ viewer.position = to;
2338
+ await viewer.sink.output(reset ? { seq: to, data, reset: true } : { seq: to, data });
2339
+ }
2340
+ } finally {
2341
+ viewer.sending = false;
2342
+ }
2343
+ }
2344
+ #changed(owner) {
2345
+ const watchers = this.#watchers.get(owner);
2346
+ if (watchers === undefined)
2347
+ return;
2348
+ const list = this.list(owner);
2349
+ for (const watcher of watchers)
2350
+ watcher(list);
2351
+ }
2352
+ }
2353
+ async function startTerminals(options = {}) {
2354
+ let spawn3 = options.spawn;
2355
+ if (spawn3 === undefined) {
2356
+ try {
2357
+ const loaded = options.load === undefined ? await import("@lydell/node-pty") : await options.load();
2358
+ if (typeof loaded.spawn !== "function")
2359
+ throw new Error("the module has no spawn()");
2360
+ spawn3 = loaded.spawn;
2361
+ } catch (error) {
2362
+ const detail = error instanceof Error ? error.message : String(error);
2363
+ (options.diagnostic ?? console.error)(`Terminals unavailable: cannot load @lydell/node-pty on ${process.platform} (${process.arch}); missing @lydell/node-pty-${process.platform}-${process.arch}. Install without --omit=optional; the host continues without hello.terminals. (${detail})`);
2364
+ return;
2365
+ }
2366
+ }
2367
+ return new Terminals({ ...options, spawn: spawn3, shell: options.shell ?? loginShell(), env: options.env ?? shellEnv(process.env) });
2368
+ }
2369
+ function loginShell() {
2370
+ let file = process.env.SHELL;
2371
+ if (file === undefined || file === "") {
2372
+ try {
2373
+ file = userInfo().shell ?? undefined;
2374
+ } catch {
2375
+ file = undefined;
2376
+ }
2377
+ }
2378
+ return { file: file === undefined || file === "" ? "/bin/sh" : file, args: ["-l"] };
2379
+ }
2380
+ function shellEnv(env) {
2381
+ const result2 = {};
2382
+ for (const [key, value] of Object.entries(env)) {
2383
+ if (value !== undefined && !key.startsWith("PINOMAD_"))
2384
+ result2[key] = value;
2385
+ }
2386
+ return result2;
2387
+ }
2388
+ function info(terminal) {
2389
+ return {
2390
+ id: terminal.id,
2391
+ name: terminal.rename ?? terminal.title ?? `Terminal ${terminal.number}`,
2392
+ renamed: terminal.rename !== undefined,
2393
+ cols: terminal.cols,
2394
+ rows: terminal.rows,
2395
+ ...terminal.exit === undefined ? {} : { exit: terminal.exit }
2396
+ };
2397
+ }
2398
+ function isHighSurrogate(code) {
2399
+ return code >= 55296 && code <= 56319;
2400
+ }
2401
+
2059
2402
  // apps/host/src/folders.ts
2060
2403
  import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
2061
2404
  import { isAbsolute, join as join6 } from "node:path";
@@ -3351,6 +3694,7 @@ async function startGateway(options) {
3351
3694
  const clients = new Set;
3352
3695
  let rtc;
3353
3696
  const rtcHello = () => rtc?.hello();
3697
+ const terminals = await startTerminals(options.terminals);
3354
3698
  const broadcast = (level, message) => {
3355
3699
  for (const client of clients)
3356
3700
  client.send({ type: "notice", level, message });
@@ -3365,10 +3709,12 @@ async function startGateway(options) {
3365
3709
  }
3366
3710
  rtc?.closeDevice(publicKey);
3367
3711
  };
3712
+ const reachableRelay = () => relay?.state().status === "removed" ? undefined : relay;
3368
3713
  const currentAdvertisement = () => {
3369
3714
  const advertiseUrl = remote?.advertiseUrl;
3715
+ const relay2 = reachableRelay();
3370
3716
  const endpoints = remoteEndpointCandidates({
3371
- ...relay === undefined ? {} : { relay: { origin: relay.origin, hostId: relay.hostId } },
3717
+ ...relay2 === undefined ? {} : { relay: { origin: relay2.origin, hostId: relay2.hostId } },
3372
3718
  ...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
3373
3719
  });
3374
3720
  return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
@@ -3385,6 +3731,7 @@ async function startGateway(options) {
3385
3731
  const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
3386
3732
  devicePublicKey,
3387
3733
  rtcHello,
3734
+ terminals,
3388
3735
  closed: (self) => {
3389
3736
  clients.delete(self);
3390
3737
  connections.delete(self);
@@ -3405,11 +3752,12 @@ async function startGateway(options) {
3405
3752
  const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
3406
3753
  const advertisement = currentAdvertisement();
3407
3754
  const endpointSet = checkedEndpointSet(advertisement.endpoints);
3755
+ const relay2 = reachableRelay();
3408
3756
  const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
3409
- if (relayReachable && relay !== undefined) {
3757
+ if (relayReachable && relay2 !== undefined) {
3410
3758
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3411
3759
  return {
3412
- url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId), endpointSet)}`,
3760
+ url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId), endpointSet)}`,
3413
3761
  expiresAt: expiresAt2
3414
3762
  };
3415
3763
  }
@@ -3422,10 +3770,10 @@ async function startGateway(options) {
3422
3770
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3423
3771
  return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
3424
3772
  }
3425
- if (relay !== undefined) {
3773
+ if (relay2 !== undefined) {
3426
3774
  const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
3427
3775
  return {
3428
- url: `${relay.origin}/#${fragment(secret2, relaySocketTarget(relay.origin, relay.hostId))}`,
3776
+ url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId))}`,
3429
3777
  expiresAt: expiresAt2
3430
3778
  };
3431
3779
  }
@@ -3519,6 +3867,10 @@ async function startGateway(options) {
3519
3867
  });
3520
3868
  if (secure?.relay !== undefined) {
3521
3869
  const relayOrigin = secure.relay.origin;
3870
+ const startConfig = await readServerConfig(options.dataDir).catch(() => {
3871
+ return;
3872
+ });
3873
+ const linkJoin = startConfig !== undefined && normalizeOrigin(startConfig.origin) === normalizeOrigin(relayOrigin) ? startConfig : undefined;
3522
3874
  const currentBinding = async () => {
3523
3875
  const config = await readServerConfig(options.dataDir).catch(() => {
3524
3876
  return;
@@ -3572,6 +3924,16 @@ async function startGateway(options) {
3572
3924
  console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
3573
3925
  } else if (state.status === "connecting") {
3574
3926
  console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
3927
+ } else if (state.status === "removed") {
3928
+ broadcast("warning", state.reason);
3929
+ console.error(state.reason);
3930
+ markRemoved(options.dataDir, {
3931
+ origin: relayOrigin,
3932
+ ...linkJoin?.binding === undefined ? {} : { binding: linkJoin.binding },
3933
+ ...linkJoin?.username === undefined ? {} : { username: linkJoin.username }
3934
+ }).catch((error) => {
3935
+ console.error(`could not write ${REMOVAL_NOTICE_FILE}: ${error instanceof Error ? error.message : String(error)}`);
3936
+ });
3575
3937
  } else {
3576
3938
  broadcast("warning", state.reason);
3577
3939
  console.error(state.reason);
@@ -3606,6 +3968,7 @@ async function startGateway(options) {
3606
3968
  detachSettingsWatch();
3607
3969
  await detachPush?.close();
3608
3970
  conversations.dispose();
3971
+ terminals?.closeAll();
3609
3972
  await rtc?.close();
3610
3973
  await Promise.all([...connections].map((connection) => connection.dispose()));
3611
3974
  for (const socket of server.clients)
@@ -3956,6 +4319,7 @@ class ConversationList {
3956
4319
  facts.label = subagent.label;
3957
4320
  if (typeof subagent?.role === "string" && subagent.role !== "")
3958
4321
  facts.role = subagent.role;
4322
+ facts.model = modelRefOf(subagent?.model);
3959
4323
  }
3960
4324
  facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
3961
4325
  const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
@@ -4021,6 +4385,7 @@ class ConversationList {
4021
4385
  const value = change.value;
4022
4386
  facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
4023
4387
  facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
4388
+ facts.model = modelRefOf(value?.model);
4024
4389
  touched.add(id);
4025
4390
  } else if (change.record.kind === TitleDoc.definition.kind) {
4026
4391
  const named = change.value?.title;
@@ -4108,6 +4473,14 @@ function roleNameOf(value) {
4108
4473
  return;
4109
4474
  return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
4110
4475
  }
4476
+ function modelRefOf(value) {
4477
+ if (typeof value !== "string")
4478
+ return;
4479
+ const slash = value.indexOf("/");
4480
+ if (slash <= 0 || slash === value.length - 1)
4481
+ return;
4482
+ return { provider: value.slice(0, slash), modelId: value.slice(slash + 1) };
4483
+ }
4111
4484
  function rolesOf(index) {
4112
4485
  const roles = new Map;
4113
4486
  for (const entry of index?.conversations ?? []) {
@@ -4138,6 +4511,8 @@ function summaryOf(record, facts, role, currentAgent) {
4138
4511
  return {
4139
4512
  id: record.id,
4140
4513
  kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
4514
+ ...record.owner === undefined ? {} : { lifecycle: "foreground" },
4515
+ ...facts.model === undefined ? {} : { model: facts.model },
4141
4516
  ...parent === undefined ? {} : { parent },
4142
4517
  ...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
4143
4518
  ...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
@@ -4150,7 +4525,7 @@ function summaryOf(record, facts, role, currentAgent) {
4150
4525
  };
4151
4526
  }
4152
4527
  function sameSummary(a, b) {
4153
- return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
4528
+ return a.id === b.id && a.kind === b.kind && a.lifecycle === b.lifecycle && a.model?.provider === b.model?.provider && a.model?.modelId === b.model?.modelId && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
4154
4529
  }
4155
4530
  function titleOf(entry) {
4156
4531
  const message = entry?.model?.[0];
@@ -4293,6 +4668,7 @@ class GatewayClient {
4293
4668
  }
4294
4669
  #hostValues;
4295
4670
  #subscriptions = new Map;
4671
+ #terminalSizes = new Map;
4296
4672
  #disposed = false;
4297
4673
  constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
4298
4674
  this.#connection = connection;
@@ -4355,7 +4731,8 @@ class GatewayClient {
4355
4731
  })),
4356
4732
  toolPresentations: options.toolPresentations ?? {},
4357
4733
  endpointSet: this.#hooks.endpointSet(),
4358
- ...rtc === undefined ? {} : { rtc }
4734
+ ...rtc === undefined ? {} : { rtc },
4735
+ ...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
4359
4736
  });
4360
4737
  }
4361
4738
  send(frame) {
@@ -4368,15 +4745,15 @@ class GatewayClient {
4368
4745
  }
4369
4746
  async#receive(frame) {
4370
4747
  if (frame.type === "subscribe")
4371
- await this.#subscribe(frame.stream, frame.images === "ref");
4748
+ await this.#subscribe(frame.stream, frame.images === "ref", frame.since);
4372
4749
  else if (frame.type === "unsubscribe")
4373
4750
  await this.#unsubscribe(frame.stream);
4374
4751
  else
4375
4752
  await this.#answer(frame.id, frame.method, frame.args);
4376
4753
  }
4377
- async#subscribe(stream, imageRefs) {
4754
+ async#subscribe(stream, imageRefs, since) {
4378
4755
  const previous = this.#subscriptions.get(stream);
4379
- const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
4756
+ const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : since === undefined ? {} : { since };
4380
4757
  this.#subscriptions.set(stream, state);
4381
4758
  const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
4382
4759
  const send = (frame) => current() ? this.send(frame) : Promise.resolve();
@@ -4420,6 +4797,24 @@ class GatewayClient {
4420
4797
  }
4421
4798
  if (stream === "tasks")
4422
4799
  return this.#forward(stream, await this.#options.harness.watchTaskGraph(context3), send);
4800
+ if (stream.startsWith("terminals:")) {
4801
+ const terminals = this.#terminals();
4802
+ const conversation2 = await this.#conversation(Number(stream.slice("terminals:".length)));
4803
+ const { owner } = await this.#terminalOwner(conversation2);
4804
+ return { stop: terminals.watch(owner, (value) => void send({ type: "snapshot", stream, value })) };
4805
+ }
4806
+ if (stream.startsWith("terminal:")) {
4807
+ const terminalStream = stream;
4808
+ const detach = this.#terminals().attach(terminalStream.slice("terminal:".length), state?.since, {
4809
+ output: (frame) => send({ type: "output", stream: terminalStream, ...frame }),
4810
+ ended: (reason) => {
4811
+ send({ type: "ended", stream, reason });
4812
+ if (this.#subscriptions.get(stream) === state)
4813
+ this.#subscriptions.delete(stream);
4814
+ }
4815
+ });
4816
+ return { stop: detach };
4817
+ }
4423
4818
  if (stream.startsWith("doc:")) {
4424
4819
  const at = stream.lastIndexOf(":");
4425
4820
  return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
@@ -4569,10 +4964,43 @@ class GatewayClient {
4569
4964
  await this.send({ type: "result", id, ok: true, value: value ?? null });
4570
4965
  } catch (error) {
4571
4966
  await this.send({ type: "result", id, ok: false, error: error instanceof Error ? error.message : String(error) });
4967
+ return;
4968
+ }
4969
+ const device = this.devicePublicKey;
4970
+ if (method === "revokeDevice" && device !== undefined && args.publicKey === device) {
4971
+ this.#hooks.closeDevice(device);
4572
4972
  }
4573
4973
  }
4574
4974
  async#call(method, args) {
4575
4975
  switch (method) {
4976
+ case "terminalInput": {
4977
+ const input = args;
4978
+ this.#terminals().input(input.terminalId, input.data, this.#terminalSizes.get(input.terminalId));
4979
+ return null;
4980
+ }
4981
+ case "resizeTerminal": {
4982
+ const input = args;
4983
+ const size = { cols: input.cols, rows: input.rows };
4984
+ this.#terminals().resize(input.terminalId, size);
4985
+ this.#terminalSizes.set(input.terminalId, size);
4986
+ return null;
4987
+ }
4988
+ case "renameTerminal": {
4989
+ const input = args;
4990
+ this.#terminals().rename(input.terminalId, input.name);
4991
+ return null;
4992
+ }
4993
+ case "restartTerminal": {
4994
+ const input = args;
4995
+ const size = { cols: input.cols, rows: input.rows };
4996
+ this.#terminals().restart(input.terminalId, size);
4997
+ this.#terminalSizes.set(input.terminalId, size);
4998
+ return null;
4999
+ }
5000
+ case "closeTerminal":
5001
+ this.#terminals().close(args.terminalId);
5002
+ this.#terminalSizes.delete(args.terminalId);
5003
+ return null;
4576
5004
  case "addProject":
4577
5005
  return addProject(this.#options.harness, args.path, context3);
4578
5006
  case "removeProject":
@@ -4636,6 +5064,7 @@ class GatewayClient {
4636
5064
  const input = args;
4637
5065
  await archive(this.#options.harness, input.conversationId, input.archived, context3);
4638
5066
  if (input.archived) {
5067
+ this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
4639
5068
  const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
4640
5069
  for (const { record, reason } of kept) {
4641
5070
  await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
@@ -4652,7 +5081,8 @@ class GatewayClient {
4652
5081
  case "revokeDevice": {
4653
5082
  const { publicKey } = args;
4654
5083
  await revokeDevice(this.#options.harness, publicKey, context3);
4655
- this.#hooks.closeDevice(publicKey);
5084
+ if (publicKey !== this.devicePublicKey)
5085
+ this.#hooks.closeDevice(publicKey);
4656
5086
  return null;
4657
5087
  }
4658
5088
  case "rtcOffer": {
@@ -4935,8 +5365,34 @@ class GatewayClient {
4935
5365
  case "changes": {
4936
5366
  return await this.#changes(conversation, context3);
4937
5367
  }
5368
+ case "openTerminal": {
5369
+ const input = args;
5370
+ const terminals = this.#terminals();
5371
+ const { owner, cwd, record } = await this.#terminalOwner(conversation);
5372
+ const ready = record === undefined || await worktreeSettled(record);
5373
+ if (!ready || !await stat6(cwd).then((info2) => info2.isDirectory(), () => false)) {
5374
+ throw new Error(`The checkout is unavailable: ${cwd}`);
5375
+ }
5376
+ const size = { cols: input.cols, rows: input.rows };
5377
+ const terminalId = terminals.open(owner, cwd, size);
5378
+ this.#terminalSizes.set(terminalId, size);
5379
+ return { terminalId };
5380
+ }
4938
5381
  }
4939
5382
  }
5383
+ #terminals() {
5384
+ if (this.#hooks.terminals === undefined)
5385
+ throw new Error("Terminals are unavailable on this host");
5386
+ return this.#hooks.terminals;
5387
+ }
5388
+ async#terminalOwner(conversation) {
5389
+ const cwd = (await conversation.agent(context3)).cwd;
5390
+ if (cwd === undefined)
5391
+ throw new Error("No working directory");
5392
+ const record = checkoutAt((await this.#options.harness.snapshot(IndexDoc, context3))?.checkouts, cwd);
5393
+ const owner = record?.conversationId ?? await familyRoot(this.#options.harness, conversation.id, context3);
5394
+ return { owner, cwd, record };
5395
+ }
4940
5396
  async#expandSkills(text, scope, bound) {
4941
5397
  if (bound === undefined ? !text.includes("$") : bound.length === 0)
4942
5398
  return text;
@@ -5524,7 +5980,7 @@ import {
5524
5980
  readdir as readdir3,
5525
5981
  rename as rename2,
5526
5982
  rm as rm3,
5527
- stat as stat6,
5983
+ stat as stat7,
5528
5984
  writeFile as writeFile2
5529
5985
  } from "node:fs/promises";
5530
5986
  import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
@@ -5713,7 +6169,7 @@ class AttachmentStore {
5713
6169
  throw new Error(problem);
5714
6170
  const decoded = base64DecodedLength(data);
5715
6171
  const path = this.#dataPath(meta);
5716
- const received = (await stat6(path)).size;
6172
+ const received = (await stat7(path)).size;
5717
6173
  if (received === meta.size) {
5718
6174
  await chmod(path, 292);
5719
6175
  await this.#writeMeta({ ...meta, state: "complete" });
@@ -5756,13 +6212,13 @@ class AttachmentStore {
5756
6212
  if (meta.state === "uploading")
5757
6213
  throw new Error(`Attachment ${index + 1} is still uploading`);
5758
6214
  const path = this.#dataPath(meta);
5759
- let info;
6215
+ let info2;
5760
6216
  try {
5761
- info = await lstat(path);
6217
+ info2 = await lstat(path);
5762
6218
  } catch {
5763
6219
  throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
5764
6220
  }
5765
- if (!info.isFile() || info.size !== meta.size) {
6221
+ if (!info2.isFile() || info2.size !== meta.size) {
5766
6222
  throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
5767
6223
  }
5768
6224
  checked.push({ meta, path });
@@ -5805,7 +6261,7 @@ class AttachmentStore {
5805
6261
  } else {
5806
6262
  this.#pending.delete(entry.name);
5807
6263
  try {
5808
- if (now - (await stat6(directory)).mtimeMs <= this.#ttlMs)
6264
+ if (now - (await stat7(directory)).mtimeMs <= this.#ttlMs)
5809
6265
  continue;
5810
6266
  } catch {
5811
6267
  continue;
@@ -6004,6 +6460,7 @@ async function openHost(options) {
6004
6460
  }
6005
6461
  },
6006
6462
  ...options.rtc === undefined ? {} : { rtc: options.rtc },
6463
+ ...options.terminals === undefined ? {} : { terminals: options.terminals },
6007
6464
  secure: {
6008
6465
  hostKey,
6009
6466
  offers,
@@ -6085,7 +6542,7 @@ async function openHost(options) {
6085
6542
  }
6086
6543
 
6087
6544
  // apps/host/src/service/log-rotation.ts
6088
- import { copyFile, rename as rename3, rm as rm4, stat as stat7, truncate } from "node:fs/promises";
6545
+ import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
6089
6546
  import { join as join11 } from "node:path";
6090
6547
  var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
6091
6548
  var HOST_LOG_HISTORY = 3;
@@ -6099,7 +6556,7 @@ async function rotateHostLog(dataDir, copy = copyFile) {
6099
6556
  await rm4(temp, { force: true });
6100
6557
  let size;
6101
6558
  try {
6102
- size = (await stat7(log)).size;
6559
+ size = (await stat8(log)).size;
6103
6560
  } catch (error) {
6104
6561
  if (isMissing(error))
6105
6562
  return false;
@@ -6147,7 +6604,7 @@ function startHostLogRotation(dataDir) {
6147
6604
  }
6148
6605
 
6149
6606
  // apps/host/src/model-auth.ts
6150
- import { randomUUID as randomUUID2 } from "node:crypto";
6607
+ import { randomUUID as randomUUID3 } from "node:crypto";
6151
6608
  import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
6152
6609
  import { dirname as dirname3 } from "node:path";
6153
6610
  import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
@@ -6451,7 +6908,7 @@ async function openModelAuth(runtime, options) {
6451
6908
  if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
6452
6909
  throw new Error(`A login for ${provider.name} is already in progress`);
6453
6910
  }
6454
- const id = randomUUID2();
6911
+ const id = randomUUID3();
6455
6912
  const login = {
6456
6913
  flow: { id, providerId, method, events: [], state: "running" },
6457
6914
  controller: new AbortController,
@@ -6826,7 +7283,7 @@ async function installId(dataDir) {
6826
7283
  const path = join12(dataDir, "install-id");
6827
7284
  let id = (await readFile7(path, "utf8").catch(() => "")).trim();
6828
7285
  if (id === "") {
6829
- id = randomUUID3();
7286
+ id = randomUUID4();
6830
7287
  await mkdir6(dataDir, { recursive: true });
6831
7288
  await writeFile5(path, `${id}
6832
7289
  `, { mode: 384 });