pinomad 0.10.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.
- package/README.md +11 -9
- package/dist/chunk-41q0saz0.js +73 -0
- package/dist/chunk-42nqtx5r.js +22 -0
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-jgd38rna.js → chunk-47ff1d1t.js} +206 -178
- package/dist/chunk-5qf1grkk.js +220 -0
- package/dist/{chunk-4h7pkkzr.js → chunk-7mn38n4f.js} +3 -3
- package/dist/{chunk-zehwx009.js → chunk-7wj0jh3t.js} +1 -1
- package/dist/{chunk-5791mb0b.js → chunk-agq4tn9r.js} +47 -23
- package/dist/{chunk-c5p9xkd8.js → chunk-aqj9wfmb.js} +3 -3
- package/dist/{chunk-cx8k63by.js → chunk-g3ahp7b9.js} +12 -1
- package/dist/chunk-gg2dcqbg.js +458 -0
- package/dist/{chunk-73efzaf1.js → chunk-jeqsdcj1.js} +30 -13
- package/dist/{chunk-czj9nfrz.js → chunk-ksfxqd8s.js} +500 -41
- package/dist/{chunk-p978qj9m.js → chunk-mnmqyq7d.js} +565 -39
- package/dist/{chunk-4sepcfpy.js → chunk-np4jd070.js} +155 -3
- package/dist/{chunk-v414b5pp.js → chunk-nppgxh6c.js} +2 -2
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-sygeb9qb.js → chunk-pqe7q1s7.js} +1 -1
- package/dist/{chunk-w2m9xkks.js → chunk-t5abqsva.js} +199 -72
- package/dist/{chunk-8gywfw5n.js → chunk-t91zt5f0.js} +2 -51
- package/dist/{chunk-vax4zspw.js → chunk-vqkb70d7.js} +3 -3
- package/dist/chunk-vx7dy07y.js +26 -0
- package/dist/pinomad.js +19 -12
- package/package.json +2 -1
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-DnpzcQzs.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
- package/web/assets/highlights-DnHv6NBM.js +4 -0
- package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
- package/web/assets/index-DNsTTgPE.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BuPP5HCg.js +2 -0
- package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-bhtx2c8a.js +0 -16
- package/dist/chunk-c05g401k.js +0 -170
- package/web/assets/index-BdBjP1qJ.js +0 -395
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
serveWebClient
|
|
4
|
-
} from "./chunk-wb10efyq.js";
|
|
5
2
|
import {
|
|
6
3
|
Published,
|
|
7
4
|
constantValue,
|
|
8
5
|
openHostSettings,
|
|
9
6
|
sameList,
|
|
10
7
|
settingsDefaults
|
|
11
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-nppgxh6c.js";
|
|
12
9
|
import {
|
|
13
10
|
DevicesDoc,
|
|
14
11
|
RevocationsDoc,
|
|
@@ -16,21 +13,24 @@ import {
|
|
|
16
13
|
ensureDevices,
|
|
17
14
|
loadHostKey,
|
|
18
15
|
loadRelayKey,
|
|
16
|
+
pairingOffers
|
|
17
|
+
} from "./chunk-t91zt5f0.js";
|
|
18
|
+
import {
|
|
19
|
+
REMOVAL_NOTICE_FILE,
|
|
20
|
+
markRemoved,
|
|
19
21
|
normalizeOrigin,
|
|
20
|
-
pairingOffers,
|
|
21
22
|
readServerBinding,
|
|
22
23
|
readServerConfig,
|
|
23
24
|
resolveRelayOrigin
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-nq57fdnq.js";
|
|
26
|
+
import {
|
|
27
|
+
serveWebClient
|
|
28
|
+
} from "./chunk-445cevpm.js";
|
|
25
29
|
import {
|
|
26
30
|
relayHostId,
|
|
27
31
|
relayWsBase,
|
|
28
32
|
startRelayLink
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import {
|
|
31
|
-
packagedVersion,
|
|
32
|
-
webRoot
|
|
33
|
-
} from "./chunk-zehwx009.js";
|
|
33
|
+
} from "./chunk-g3ahp7b9.js";
|
|
34
34
|
import {
|
|
35
35
|
ATTACHMENT_TTL_MS,
|
|
36
36
|
INVALID_FRAME_CLOSE_CODE,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
rtcCandidateType,
|
|
57
57
|
sdpCandidates,
|
|
58
58
|
withRtcCandidates
|
|
59
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-47ff1d1t.js";
|
|
60
60
|
import {
|
|
61
61
|
ENDPOINT_SET_SCHEMA_VERSION,
|
|
62
62
|
MAX_HOST_ENDPOINT_URL_LENGTH,
|
|
@@ -72,16 +72,20 @@ import {
|
|
|
72
72
|
parseEndpointSet,
|
|
73
73
|
respondIK,
|
|
74
74
|
toBase64Url
|
|
75
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-pqe7q1s7.js";
|
|
76
76
|
import {
|
|
77
77
|
DEFAULT_DATA_DIR
|
|
78
78
|
} from "./chunk-ngf1zatm.js";
|
|
79
|
+
import {
|
|
80
|
+
packagedVersion,
|
|
81
|
+
webRoot
|
|
82
|
+
} from "./chunk-7wj0jh3t.js";
|
|
79
83
|
import {
|
|
80
84
|
__require
|
|
81
85
|
} from "./chunk-df32avw6.js";
|
|
82
86
|
|
|
83
87
|
// apps/host/src/main.ts
|
|
84
|
-
import { randomUUID as
|
|
88
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
85
89
|
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
86
90
|
import { homedir as homedir3 } from "node:os";
|
|
87
91
|
import { join as join12, resolve as resolve7 } from "node:path";
|
|
@@ -710,11 +714,7 @@ async function checkoutInfo(harness, input, context) {
|
|
|
710
714
|
}
|
|
711
715
|
};
|
|
712
716
|
}
|
|
713
|
-
async function
|
|
714
|
-
const index = await harness.snapshot(IndexDoc, context);
|
|
715
|
-
const checkouts = index?.checkouts ?? [];
|
|
716
|
-
if (checkouts.length === 0)
|
|
717
|
-
return [];
|
|
717
|
+
async function conversationSubtree(harness, rootId, context) {
|
|
718
718
|
const all = [];
|
|
719
719
|
let cursor;
|
|
720
720
|
do {
|
|
@@ -741,6 +741,27 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
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);
|
|
744
765
|
const kept = [];
|
|
745
766
|
for (const record of checkouts.filter((checkout) => subtree.has(checkout.conversationId))) {
|
|
746
767
|
const reason = await removeWorktree(record);
|
|
@@ -1449,6 +1470,7 @@ var todo = {
|
|
|
1449
1470
|
};
|
|
1450
1471
|
|
|
1451
1472
|
// apps/host/src/gateway.ts
|
|
1473
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
1452
1474
|
import { createServer } from "node:http";
|
|
1453
1475
|
import { homedir, networkInterfaces } from "node:os";
|
|
1454
1476
|
import { join as join7, sep as sep5 } from "node:path";
|
|
@@ -2054,6 +2076,329 @@ function dataChannelCarrier(channel, timers, log, onClosed) {
|
|
|
2054
2076
|
};
|
|
2055
2077
|
}
|
|
2056
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
|
+
|
|
2057
2402
|
// apps/host/src/folders.ts
|
|
2058
2403
|
import { readdir, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
2059
2404
|
import { isAbsolute, join as join6 } from "node:path";
|
|
@@ -3349,6 +3694,7 @@ async function startGateway(options) {
|
|
|
3349
3694
|
const clients = new Set;
|
|
3350
3695
|
let rtc;
|
|
3351
3696
|
const rtcHello = () => rtc?.hello();
|
|
3697
|
+
const terminals = await startTerminals(options.terminals);
|
|
3352
3698
|
const broadcast = (level, message) => {
|
|
3353
3699
|
for (const client of clients)
|
|
3354
3700
|
client.send({ type: "notice", level, message });
|
|
@@ -3363,10 +3709,12 @@ async function startGateway(options) {
|
|
|
3363
3709
|
}
|
|
3364
3710
|
rtc?.closeDevice(publicKey);
|
|
3365
3711
|
};
|
|
3712
|
+
const reachableRelay = () => relay?.state().status === "removed" ? undefined : relay;
|
|
3366
3713
|
const currentAdvertisement = () => {
|
|
3367
3714
|
const advertiseUrl = remote?.advertiseUrl;
|
|
3715
|
+
const relay2 = reachableRelay();
|
|
3368
3716
|
const endpoints = remoteEndpointCandidates({
|
|
3369
|
-
...
|
|
3717
|
+
...relay2 === undefined ? {} : { relay: { origin: relay2.origin, hostId: relay2.hostId } },
|
|
3370
3718
|
...advertiseUrl === undefined ? {} : { direct: { advertiseUrl } }
|
|
3371
3719
|
});
|
|
3372
3720
|
return { endpoints, ...advertiseUrl === undefined ? {} : { advertiseUrl } };
|
|
@@ -3383,6 +3731,7 @@ async function startGateway(options) {
|
|
|
3383
3731
|
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
|
|
3384
3732
|
devicePublicKey,
|
|
3385
3733
|
rtcHello,
|
|
3734
|
+
terminals,
|
|
3386
3735
|
closed: (self) => {
|
|
3387
3736
|
clients.delete(self);
|
|
3388
3737
|
connections.delete(self);
|
|
@@ -3403,11 +3752,12 @@ async function startGateway(options) {
|
|
|
3403
3752
|
const fragment = (secret2, target2, endpointSet2) => `${pair(secret2)}&url=${encodeURIComponent(target2)}${endpointSet2 === undefined ? "" : `&endpointSet=${encodeEndpointSetParam(endpointSet2)}`}`;
|
|
3404
3753
|
const advertisement = currentAdvertisement();
|
|
3405
3754
|
const endpointSet = checkedEndpointSet(advertisement.endpoints);
|
|
3755
|
+
const relay2 = reachableRelay();
|
|
3406
3756
|
const relayReachable = endpointSet.endpoints.some((endpoint) => endpoint.role === "relay");
|
|
3407
|
-
if (relayReachable &&
|
|
3757
|
+
if (relayReachable && relay2 !== undefined) {
|
|
3408
3758
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3409
3759
|
return {
|
|
3410
|
-
url: `${
|
|
3760
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId), endpointSet)}`,
|
|
3411
3761
|
expiresAt: expiresAt2
|
|
3412
3762
|
};
|
|
3413
3763
|
}
|
|
@@ -3420,10 +3770,10 @@ async function startGateway(options) {
|
|
|
3420
3770
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3421
3771
|
return { url: `${advertise}/#${fragment(secret2, direct.url, endpointSet)}`, expiresAt: expiresAt2 };
|
|
3422
3772
|
}
|
|
3423
|
-
if (
|
|
3773
|
+
if (relay2 !== undefined) {
|
|
3424
3774
|
const { secret: secret2, expiresAt: expiresAt2 } = spec.offers.create("qr");
|
|
3425
3775
|
return {
|
|
3426
|
-
url: `${
|
|
3776
|
+
url: `${relay2.origin}/#${fragment(secret2, relaySocketTarget(relay2.origin, relay2.hostId))}`,
|
|
3427
3777
|
expiresAt: expiresAt2
|
|
3428
3778
|
};
|
|
3429
3779
|
}
|
|
@@ -3517,6 +3867,10 @@ async function startGateway(options) {
|
|
|
3517
3867
|
});
|
|
3518
3868
|
if (secure?.relay !== undefined) {
|
|
3519
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;
|
|
3520
3874
|
const currentBinding = async () => {
|
|
3521
3875
|
const config = await readServerConfig(options.dataDir).catch(() => {
|
|
3522
3876
|
return;
|
|
@@ -3570,6 +3924,16 @@ async function startGateway(options) {
|
|
|
3570
3924
|
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId, push }));
|
|
3571
3925
|
} else if (state.status === "connecting") {
|
|
3572
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
|
+
});
|
|
3573
3937
|
} else {
|
|
3574
3938
|
broadcast("warning", state.reason);
|
|
3575
3939
|
console.error(state.reason);
|
|
@@ -3604,6 +3968,7 @@ async function startGateway(options) {
|
|
|
3604
3968
|
detachSettingsWatch();
|
|
3605
3969
|
await detachPush?.close();
|
|
3606
3970
|
conversations.dispose();
|
|
3971
|
+
terminals?.closeAll();
|
|
3607
3972
|
await rtc?.close();
|
|
3608
3973
|
await Promise.all([...connections].map((connection) => connection.dispose()));
|
|
3609
3974
|
for (const socket of server.clients)
|
|
@@ -3954,6 +4319,7 @@ class ConversationList {
|
|
|
3954
4319
|
facts.label = subagent.label;
|
|
3955
4320
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
3956
4321
|
facts.role = subagent.role;
|
|
4322
|
+
facts.model = modelRefOf(subagent?.model);
|
|
3957
4323
|
}
|
|
3958
4324
|
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
3959
4325
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
@@ -4019,6 +4385,7 @@ class ConversationList {
|
|
|
4019
4385
|
const value = change.value;
|
|
4020
4386
|
facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
|
|
4021
4387
|
facts.role = typeof value?.role === "string" && value.role !== "" ? value.role : undefined;
|
|
4388
|
+
facts.model = modelRefOf(value?.model);
|
|
4022
4389
|
touched.add(id);
|
|
4023
4390
|
} else if (change.record.kind === TitleDoc.definition.kind) {
|
|
4024
4391
|
const named = change.value?.title;
|
|
@@ -4106,6 +4473,14 @@ function roleNameOf(value) {
|
|
|
4106
4473
|
return;
|
|
4107
4474
|
return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
|
|
4108
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
|
+
}
|
|
4109
4484
|
function rolesOf(index) {
|
|
4110
4485
|
const roles = new Map;
|
|
4111
4486
|
for (const entry of index?.conversations ?? []) {
|
|
@@ -4136,6 +4511,8 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4136
4511
|
return {
|
|
4137
4512
|
id: record.id,
|
|
4138
4513
|
kind: record.owner !== undefined ? "subagent" : record.parent !== undefined ? "fork" : "conversation",
|
|
4514
|
+
...record.owner === undefined ? {} : { lifecycle: "foreground" },
|
|
4515
|
+
...facts.model === undefined ? {} : { model: facts.model },
|
|
4139
4516
|
...parent === undefined ? {} : { parent },
|
|
4140
4517
|
...record.parent === undefined ? {} : { forkedAt: String(record.parent.at) },
|
|
4141
4518
|
...(facts.named ?? facts.title) === undefined ? {} : { title: facts.named ?? facts.title },
|
|
@@ -4148,7 +4525,7 @@ function summaryOf(record, facts, role, currentAgent) {
|
|
|
4148
4525
|
};
|
|
4149
4526
|
}
|
|
4150
4527
|
function sameSummary(a, b) {
|
|
4151
|
-
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;
|
|
4152
4529
|
}
|
|
4153
4530
|
function titleOf(entry) {
|
|
4154
4531
|
const message = entry?.model?.[0];
|
|
@@ -4291,6 +4668,7 @@ class GatewayClient {
|
|
|
4291
4668
|
}
|
|
4292
4669
|
#hostValues;
|
|
4293
4670
|
#subscriptions = new Map;
|
|
4671
|
+
#terminalSizes = new Map;
|
|
4294
4672
|
#disposed = false;
|
|
4295
4673
|
constructor(connection, options, conversations, files, skills, imageVariants, hooks) {
|
|
4296
4674
|
this.#connection = connection;
|
|
@@ -4353,7 +4731,8 @@ class GatewayClient {
|
|
|
4353
4731
|
})),
|
|
4354
4732
|
toolPresentations: options.toolPresentations ?? {},
|
|
4355
4733
|
endpointSet: this.#hooks.endpointSet(),
|
|
4356
|
-
...rtc === undefined ? {} : { rtc }
|
|
4734
|
+
...rtc === undefined ? {} : { rtc },
|
|
4735
|
+
...this.#hooks.terminals === undefined ? {} : { terminals: { limit: this.#hooks.terminals.limit } }
|
|
4357
4736
|
});
|
|
4358
4737
|
}
|
|
4359
4738
|
send(frame) {
|
|
@@ -4366,15 +4745,15 @@ class GatewayClient {
|
|
|
4366
4745
|
}
|
|
4367
4746
|
async#receive(frame) {
|
|
4368
4747
|
if (frame.type === "subscribe")
|
|
4369
|
-
await this.#subscribe(frame.stream, frame.images === "ref");
|
|
4748
|
+
await this.#subscribe(frame.stream, frame.images === "ref", frame.since);
|
|
4370
4749
|
else if (frame.type === "unsubscribe")
|
|
4371
4750
|
await this.#unsubscribe(frame.stream);
|
|
4372
4751
|
else
|
|
4373
4752
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
4374
4753
|
}
|
|
4375
|
-
async#subscribe(stream, imageRefs) {
|
|
4754
|
+
async#subscribe(stream, imageRefs, since) {
|
|
4376
4755
|
const previous = this.#subscriptions.get(stream);
|
|
4377
|
-
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
|
|
4756
|
+
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : since === undefined ? {} : { since };
|
|
4378
4757
|
this.#subscriptions.set(stream, state);
|
|
4379
4758
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
4380
4759
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -4418,6 +4797,24 @@ class GatewayClient {
|
|
|
4418
4797
|
}
|
|
4419
4798
|
if (stream === "tasks")
|
|
4420
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
|
+
}
|
|
4421
4818
|
if (stream.startsWith("doc:")) {
|
|
4422
4819
|
const at = stream.lastIndexOf(":");
|
|
4423
4820
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
@@ -4567,10 +4964,43 @@ class GatewayClient {
|
|
|
4567
4964
|
await this.send({ type: "result", id, ok: true, value: value ?? null });
|
|
4568
4965
|
} catch (error) {
|
|
4569
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);
|
|
4570
4972
|
}
|
|
4571
4973
|
}
|
|
4572
4974
|
async#call(method, args) {
|
|
4573
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;
|
|
4574
5004
|
case "addProject":
|
|
4575
5005
|
return addProject(this.#options.harness, args.path, context3);
|
|
4576
5006
|
case "removeProject":
|
|
@@ -4634,6 +5064,7 @@ class GatewayClient {
|
|
|
4634
5064
|
const input = args;
|
|
4635
5065
|
await archive(this.#options.harness, input.conversationId, input.archived, context3);
|
|
4636
5066
|
if (input.archived) {
|
|
5067
|
+
this.#hooks.terminals?.closeOwners(await conversationSubtree(this.#options.harness, input.conversationId, context3));
|
|
4637
5068
|
const kept = await cleanupArchivedWorktrees(this.#options.harness, input.conversationId, context3);
|
|
4638
5069
|
for (const { record, reason } of kept) {
|
|
4639
5070
|
await this.send({ type: "notice", level: "warning", message: `Kept worktree ${record.path} (branch ${record.branch}): ${reason}` });
|
|
@@ -4650,7 +5081,8 @@ class GatewayClient {
|
|
|
4650
5081
|
case "revokeDevice": {
|
|
4651
5082
|
const { publicKey } = args;
|
|
4652
5083
|
await revokeDevice(this.#options.harness, publicKey, context3);
|
|
4653
|
-
this
|
|
5084
|
+
if (publicKey !== this.devicePublicKey)
|
|
5085
|
+
this.#hooks.closeDevice(publicKey);
|
|
4654
5086
|
return null;
|
|
4655
5087
|
}
|
|
4656
5088
|
case "rtcOffer": {
|
|
@@ -4933,8 +5365,34 @@ class GatewayClient {
|
|
|
4933
5365
|
case "changes": {
|
|
4934
5366
|
return await this.#changes(conversation, context3);
|
|
4935
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
|
+
}
|
|
4936
5381
|
}
|
|
4937
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
|
+
}
|
|
4938
5396
|
async#expandSkills(text, scope, bound) {
|
|
4939
5397
|
if (bound === undefined ? !text.includes("$") : bound.length === 0)
|
|
4940
5398
|
return text;
|
|
@@ -5522,7 +5980,7 @@ import {
|
|
|
5522
5980
|
readdir as readdir3,
|
|
5523
5981
|
rename as rename2,
|
|
5524
5982
|
rm as rm3,
|
|
5525
|
-
stat as
|
|
5983
|
+
stat as stat7,
|
|
5526
5984
|
writeFile as writeFile2
|
|
5527
5985
|
} from "node:fs/promises";
|
|
5528
5986
|
import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
|
|
@@ -5711,7 +6169,7 @@ class AttachmentStore {
|
|
|
5711
6169
|
throw new Error(problem);
|
|
5712
6170
|
const decoded = base64DecodedLength(data);
|
|
5713
6171
|
const path = this.#dataPath(meta);
|
|
5714
|
-
const received = (await
|
|
6172
|
+
const received = (await stat7(path)).size;
|
|
5715
6173
|
if (received === meta.size) {
|
|
5716
6174
|
await chmod(path, 292);
|
|
5717
6175
|
await this.#writeMeta({ ...meta, state: "complete" });
|
|
@@ -5754,13 +6212,13 @@ class AttachmentStore {
|
|
|
5754
6212
|
if (meta.state === "uploading")
|
|
5755
6213
|
throw new Error(`Attachment ${index + 1} is still uploading`);
|
|
5756
6214
|
const path = this.#dataPath(meta);
|
|
5757
|
-
let
|
|
6215
|
+
let info2;
|
|
5758
6216
|
try {
|
|
5759
|
-
|
|
6217
|
+
info2 = await lstat(path);
|
|
5760
6218
|
} catch {
|
|
5761
6219
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5762
6220
|
}
|
|
5763
|
-
if (!
|
|
6221
|
+
if (!info2.isFile() || info2.size !== meta.size) {
|
|
5764
6222
|
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
5765
6223
|
}
|
|
5766
6224
|
checked.push({ meta, path });
|
|
@@ -5803,7 +6261,7 @@ class AttachmentStore {
|
|
|
5803
6261
|
} else {
|
|
5804
6262
|
this.#pending.delete(entry.name);
|
|
5805
6263
|
try {
|
|
5806
|
-
if (now - (await
|
|
6264
|
+
if (now - (await stat7(directory)).mtimeMs <= this.#ttlMs)
|
|
5807
6265
|
continue;
|
|
5808
6266
|
} catch {
|
|
5809
6267
|
continue;
|
|
@@ -6002,6 +6460,7 @@ async function openHost(options) {
|
|
|
6002
6460
|
}
|
|
6003
6461
|
},
|
|
6004
6462
|
...options.rtc === undefined ? {} : { rtc: options.rtc },
|
|
6463
|
+
...options.terminals === undefined ? {} : { terminals: options.terminals },
|
|
6005
6464
|
secure: {
|
|
6006
6465
|
hostKey,
|
|
6007
6466
|
offers,
|
|
@@ -6083,7 +6542,7 @@ async function openHost(options) {
|
|
|
6083
6542
|
}
|
|
6084
6543
|
|
|
6085
6544
|
// apps/host/src/service/log-rotation.ts
|
|
6086
|
-
import { copyFile, rename as rename3, rm as rm4, stat as
|
|
6545
|
+
import { copyFile, rename as rename3, rm as rm4, stat as stat8, truncate } from "node:fs/promises";
|
|
6087
6546
|
import { join as join11 } from "node:path";
|
|
6088
6547
|
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
6089
6548
|
var HOST_LOG_HISTORY = 3;
|
|
@@ -6097,7 +6556,7 @@ async function rotateHostLog(dataDir, copy = copyFile) {
|
|
|
6097
6556
|
await rm4(temp, { force: true });
|
|
6098
6557
|
let size;
|
|
6099
6558
|
try {
|
|
6100
|
-
size = (await
|
|
6559
|
+
size = (await stat8(log)).size;
|
|
6101
6560
|
} catch (error) {
|
|
6102
6561
|
if (isMissing(error))
|
|
6103
6562
|
return false;
|
|
@@ -6145,7 +6604,7 @@ function startHostLogRotation(dataDir) {
|
|
|
6145
6604
|
}
|
|
6146
6605
|
|
|
6147
6606
|
// apps/host/src/model-auth.ts
|
|
6148
|
-
import { randomUUID as
|
|
6607
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6149
6608
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
|
|
6150
6609
|
import { dirname as dirname3 } from "node:path";
|
|
6151
6610
|
import { getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
@@ -6449,7 +6908,7 @@ async function openModelAuth(runtime, options) {
|
|
|
6449
6908
|
if ([...logins.values()].some((login2) => login2.flow.providerId === providerId && login2.flow.state === "running")) {
|
|
6450
6909
|
throw new Error(`A login for ${provider.name} is already in progress`);
|
|
6451
6910
|
}
|
|
6452
|
-
const id =
|
|
6911
|
+
const id = randomUUID3();
|
|
6453
6912
|
const login = {
|
|
6454
6913
|
flow: { id, providerId, method, events: [], state: "running" },
|
|
6455
6914
|
controller: new AbortController,
|
|
@@ -6824,7 +7283,7 @@ async function installId(dataDir) {
|
|
|
6824
7283
|
const path = join12(dataDir, "install-id");
|
|
6825
7284
|
let id = (await readFile7(path, "utf8").catch(() => "")).trim();
|
|
6826
7285
|
if (id === "") {
|
|
6827
|
-
id =
|
|
7286
|
+
id = randomUUID4();
|
|
6828
7287
|
await mkdir6(dataDir, { recursive: true });
|
|
6829
7288
|
await writeFile5(path, `${id}
|
|
6830
7289
|
`, { mode: 384 });
|