@mrciphersmith/keryx 0.3.48 → 0.3.49

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/dist/cli.js CHANGED
@@ -20094,7 +20094,7 @@ var package_default;
20094
20094
  var init_package = __esm(() => {
20095
20095
  package_default = {
20096
20096
  name: "@mrciphersmith/keryx",
20097
- version: "0.3.48",
20097
+ version: "0.3.49",
20098
20098
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
20099
20099
  private: false,
20100
20100
  publishConfig: {
@@ -99482,6 +99482,12 @@ var init_help_groups = __esm(() => {
99482
99482
  group: "Automation",
99483
99483
  summary: "Drive this session from a Telegram topic: send lines, get replies, answer approvals. Off by default; [name|off|status]."
99484
99484
  },
99485
+ {
99486
+ kind: "slash",
99487
+ name: "/channels",
99488
+ group: "Automation",
99489
+ summary: "Connect, test or disconnect Telegram for this machine: bot token, one-time code, group. [status]."
99490
+ },
99485
99491
  {
99486
99492
  kind: "cli",
99487
99493
  name: "acp",
@@ -102043,6 +102049,9 @@ function ensureHost(otui, chrome) {
102043
102049
  if (!state.open || state.input === undefined) {
102044
102050
  return;
102045
102051
  }
102052
+ if (state.input.ownsKeys?.() === true) {
102053
+ return;
102054
+ }
102046
102055
  const focused = r.currentFocusedRenderable;
102047
102056
  const inBody = focused !== null && containsNode(state.scroll, focused);
102048
102057
  if (key.name === "escape") {
@@ -108724,6 +108733,9 @@ var init_serve_config = __esm(() => {
108724
108733
  function remoteRoutePath(route) {
108725
108734
  return `${REMOTE_API_PREFIX}/${route}`;
108726
108735
  }
108736
+ function channelsRoutePath(route) {
108737
+ return `${REMOTE_API_PREFIX}/${route}`;
108738
+ }
108727
108739
  function isSessionId(value) {
108728
108740
  return typeof value === "string" && SESSION_ID_PATTERN.test(value);
108729
108741
  }
@@ -108786,7 +108798,7 @@ class SseParser {
108786
108798
  return frames;
108787
108799
  }
108788
108800
  }
108789
- var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", SSE_KEEPALIVE_FRAME = `: keepalive
108801
+ var REMOTE_API_PREFIX = "/v1/remote", REMOTE_SCHEMA_VERSION = "1.0.0", REMOTE_ROUTE_METHODS, CHANNELS_ROUTE_METHODS, MAX_REMOTE_BODY_BYTES, MAX_PROJECT_CHARS = 256, MAX_APPROVAL_PROMPT_CHARS = 3000, MAX_KEYBOARD_ROWS = 8, MAX_KEYBOARD_BUTTONS_PER_ROW = 8, MAX_BUTTON_TEXT_CHARS = 64, MAX_CALLBACK_DATA_BYTES = 64, DEFAULT_APPROVAL_TIMEOUT_MS, MAX_APPROVAL_TIMEOUT_MS, MIN_APPROVAL_TIMEOUT_MS = 100, DEFAULT_KEEPALIVE_MS = 15000, DEFAULT_ACK_TIMEOUT_MS = 30000, SESSION_ID_PATTERN, APPROVAL_CALLBACK_PATTERN, RESERVED_CALLBACK_PREFIX = "ap:", SSE_KEEPALIVE_FRAME = `: keepalive
108790
108802
 
108791
108803
  `;
108792
108804
  var init_protocol2 = __esm(() => {
@@ -108799,6 +108811,15 @@ var init_protocol2 = __esm(() => {
108799
108811
  ack: "POST",
108800
108812
  stream: "GET"
108801
108813
  };
108814
+ CHANNELS_ROUTE_METHODS = {
108815
+ "channels-status": "GET",
108816
+ "channels-pair": "POST",
108817
+ "channels-pairing": "GET",
108818
+ "channels-cancel": "POST",
108819
+ "channels-reload": "POST",
108820
+ "channels-test": "POST",
108821
+ "channels-disconnect": "POST"
108822
+ };
108802
108823
  MAX_REMOTE_BODY_BYTES = 64 * 1024;
108803
108824
  DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60000;
108804
108825
  MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
@@ -109803,6 +109824,1457 @@ var init_shell_bridge = __esm(() => {
109803
109824
  init_protocol2();
109804
109825
  });
109805
109826
 
109827
+ // src/remote/config.ts
109828
+ import { statSync as statSync17, unlinkSync as unlinkSync6 } from "fs";
109829
+ function isSafeInt(value) {
109830
+ return typeof value === "number" && Number.isSafeInteger(value);
109831
+ }
109832
+ function parseRemoteConfig(value) {
109833
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
109834
+ return { ok: false, reason: "remote config must be a JSON object" };
109835
+ }
109836
+ const doc = value;
109837
+ for (const key of Object.keys(doc)) {
109838
+ if (!CONFIG_KEYS2.has(key)) {
109839
+ const shown = /^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(key) ? key : "<not shown>";
109840
+ return { ok: false, reason: `remote config has an unknown key "${shown}" (the schema is closed)` };
109841
+ }
109842
+ }
109843
+ if (doc.schemaVersion !== REMOTE_CONFIG_SCHEMA_VERSION) {
109844
+ return { ok: false, reason: `remote config schemaVersion must be ${REMOTE_CONFIG_SCHEMA_VERSION}` };
109845
+ }
109846
+ if (!isSafeInt(doc.chatId) || doc.chatId === 0) {
109847
+ return { ok: false, reason: "remote config chatId must be a non-zero integer (the supergroup id)" };
109848
+ }
109849
+ const ids = doc.allowedUserIds;
109850
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isSafeInt(id) && id > 0)) {
109851
+ return { ok: false, reason: "remote config allowedUserIds must be a non-empty array of positive integers" };
109852
+ }
109853
+ const timeout = (key, fallback) => {
109854
+ const raw = doc[key];
109855
+ if (raw === undefined) {
109856
+ return { ok: true, value: fallback };
109857
+ }
109858
+ if (!isSafeInt(raw) || raw < 1 || raw > MAX_TIMEOUT_MS3) {
109859
+ return { ok: false, reason: `remote config ${key} must be an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
109860
+ }
109861
+ return { ok: true, value: raw };
109862
+ };
109863
+ const orphan = timeout("orphanMs", DEFAULT_ORPHAN_MS);
109864
+ if (!orphan.ok) {
109865
+ return orphan;
109866
+ }
109867
+ const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
109868
+ if (!run.ok) {
109869
+ return run;
109870
+ }
109871
+ return {
109872
+ ok: true,
109873
+ value: {
109874
+ schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
109875
+ chatId: doc.chatId,
109876
+ allowedUserIds: [...new Set(ids)],
109877
+ orphanMs: orphan.value,
109878
+ runTimeoutMs: run.value
109879
+ }
109880
+ };
109881
+ }
109882
+ function loadRemoteConfig(dir) {
109883
+ const file = remoteConfigPath(dir);
109884
+ const read = readConfigFile(file);
109885
+ if (!read.ok) {
109886
+ return {
109887
+ ok: false,
109888
+ reason: read.reason === "absent" ? `no remote-control config at ${file}; create it with chatId and allowedUserIds` : `remote-control config at ${file} cannot be read (${read.reason})`
109889
+ };
109890
+ }
109891
+ let parsed;
109892
+ try {
109893
+ parsed = JSON.parse(read.text);
109894
+ } catch {
109895
+ return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
109896
+ }
109897
+ return parseRemoteConfig(parsed);
109898
+ }
109899
+ function saveRemoteConfig(config, dir) {
109900
+ const checked = parseRemoteConfig(config);
109901
+ if (!checked.ok) {
109902
+ return checked;
109903
+ }
109904
+ ensureRemoteDir(dir);
109905
+ writeOwnerOnlyFileAtomic(remoteConfigPath(dir), `${JSON.stringify(checked.value, null, 2)}
109906
+ `);
109907
+ return checked;
109908
+ }
109909
+ function loadBotToken(dir) {
109910
+ const file = botTokenPath(dir);
109911
+ let mode;
109912
+ try {
109913
+ const stats = statSync17(file);
109914
+ if (!stats.isFile()) {
109915
+ return { ok: false, reason: `bot token path ${file} is not a regular file` };
109916
+ }
109917
+ mode = stats.mode & 511;
109918
+ } catch {
109919
+ return {
109920
+ ok: false,
109921
+ reason: `no bot token file at ${file}; put the token on one line in it and run: chmod 600 ${file}`
109922
+ };
109923
+ }
109924
+ if (process.platform !== "win32" && (mode & 63) !== 0) {
109925
+ return {
109926
+ ok: false,
109927
+ reason: `bot token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}`
109928
+ };
109929
+ }
109930
+ const read = readConfigFile(file);
109931
+ if (!read.ok) {
109932
+ return { ok: false, reason: `bot token file ${file} cannot be read (${read.reason})` };
109933
+ }
109934
+ const token = read.text.trim();
109935
+ if (!TOKEN_SHAPE2.test(token)) {
109936
+ return {
109937
+ ok: false,
109938
+ reason: `bot token file ${file} does not hold a bot token (expected <digits>:<secret> on a single line)`
109939
+ };
109940
+ }
109941
+ return { ok: true, value: token };
109942
+ }
109943
+ function saveBotToken(token, dir) {
109944
+ const trimmed = token.trim();
109945
+ if (!TOKEN_SHAPE2.test(trimmed)) {
109946
+ return { ok: false, reason: "that does not look like a bot token (expected <digits>:<secret>, as BotFather gives it)" };
109947
+ }
109948
+ const file = botTokenPath(dir);
109949
+ try {
109950
+ ensureRemoteDir(dir);
109951
+ writeOwnerOnlyFileAtomic(file, `${trimmed}
109952
+ `);
109953
+ } catch {
109954
+ return { ok: false, reason: `could not write the bot token file at ${file}` };
109955
+ }
109956
+ return { ok: true, value: true };
109957
+ }
109958
+ function removeFile2(file) {
109959
+ try {
109960
+ unlinkSync6(file);
109961
+ return true;
109962
+ } catch (error) {
109963
+ return error.code === "ENOENT";
109964
+ }
109965
+ }
109966
+ function removeBotToken(dir) {
109967
+ return removeFile2(botTokenPath(dir));
109968
+ }
109969
+ function removeRemoteConfig(dir) {
109970
+ return removeFile2(remoteConfigPath(dir));
109971
+ }
109972
+ var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS2, TOKEN_SHAPE2;
109973
+ var init_config10 = __esm(() => {
109974
+ init_config_dir();
109975
+ init_paths4();
109976
+ DEFAULT_ORPHAN_MS = 10 * 60000;
109977
+ DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
109978
+ MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
109979
+ CONFIG_KEYS2 = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs"]);
109980
+ TOKEN_SHAPE2 = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
109981
+ });
109982
+
109983
+ // src/remote/channels-client.ts
109984
+ function fileExists(file) {
109985
+ const read = readConfigFile(file);
109986
+ return read.ok || read.reason !== "absent";
109987
+ }
109988
+ function isObject2(value) {
109989
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109990
+ }
109991
+ function plural2(count, one, many) {
109992
+ return `${count} ${count === 1 ? one : many}`;
109993
+ }
109994
+
109995
+ class ChannelsClient {
109996
+ options;
109997
+ fetchImpl;
109998
+ requestTimeoutMs;
109999
+ isAlive;
110000
+ constructor(options = {}) {
110001
+ this.options = options;
110002
+ this.fetchImpl = options.fetchImpl ?? fetch;
110003
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
110004
+ this.isAlive = options.isAlive ?? ownProcessIsAlive;
110005
+ }
110006
+ localFiles() {
110007
+ return {
110008
+ tokenFile: fileExists(botTokenPath(this.options.dir)),
110009
+ configFile: fileExists(remoteConfigPath(this.options.dir))
110010
+ };
110011
+ }
110012
+ status() {
110013
+ return this.call("channels-status", (body) => {
110014
+ const telegram = body.telegram;
110015
+ return typeof body.machine === "string" && isObject2(telegram) && typeof telegram.state === "string" && typeof telegram.sessions === "number";
110016
+ });
110017
+ }
110018
+ async startPairing(token) {
110019
+ const reachable = this.reachable();
110020
+ if (!reachable.ok) {
110021
+ return reachable;
110022
+ }
110023
+ const file = botTokenPath(this.options.dir);
110024
+ const before = readConfigFile(file);
110025
+ const saved = saveBotToken(token, this.options.dir);
110026
+ if (!saved.ok) {
110027
+ return { ok: false, code: "invalid", reason: saved.reason };
110028
+ }
110029
+ const result = await this.call("channels-pair", isPairing);
110030
+ if (!result.ok) {
110031
+ this.restore(file, before);
110032
+ }
110033
+ return result;
110034
+ }
110035
+ pairingStatus() {
110036
+ return this.call("channels-pairing", isPairing);
110037
+ }
110038
+ async cancelPairing() {
110039
+ const result = await this.call("channels-cancel", (body) => typeof body.cancelled === "boolean");
110040
+ if (!fileExists(remoteConfigPath(this.options.dir))) {
110041
+ removeBotToken(this.options.dir);
110042
+ }
110043
+ return result;
110044
+ }
110045
+ async connectFinish(ids) {
110046
+ const reachable = this.reachable();
110047
+ if (!reachable.ok) {
110048
+ this.dropUnusedToken();
110049
+ return reachable;
110050
+ }
110051
+ const configFile = remoteConfigPath(this.options.dir);
110052
+ const before = readConfigFile(configFile);
110053
+ const saved = saveRemoteConfig({
110054
+ schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
110055
+ chatId: ids.chatId,
110056
+ allowedUserIds: [ids.userId],
110057
+ orphanMs: DEFAULT_ORPHAN_MS,
110058
+ runTimeoutMs: DEFAULT_RUN_TIMEOUT_MS
110059
+ }, this.options.dir);
110060
+ if (!saved.ok) {
110061
+ this.dropUnusedToken();
110062
+ return { ok: false, code: "invalid", reason: saved.reason };
110063
+ }
110064
+ const result = await this.call("channels-reload", (body) => typeof body.state === "string");
110065
+ if (!result.ok && result.code !== "no-answer") {
110066
+ this.restore(configFile, before);
110067
+ if (!before.ok) {
110068
+ removeBotToken(this.options.dir);
110069
+ }
110070
+ }
110071
+ return result;
110072
+ }
110073
+ dropUnusedToken() {
110074
+ if (!fileExists(remoteConfigPath(this.options.dir))) {
110075
+ removeBotToken(this.options.dir);
110076
+ }
110077
+ }
110078
+ test() {
110079
+ return this.call("channels-test", (body) => body.delivered === true && typeof body.machine === "string");
110080
+ }
110081
+ async disconnect() {
110082
+ const result = await this.call("channels-disconnect", (body) => typeof body.deleted === "number" && typeof body.remaining === "number" && typeof body.hubWasRunning === "boolean");
110083
+ if (!result.ok && result.code !== "serve-down") {
110084
+ return result.code === "no-answer" ? { ...result, reason: `${result.reason} Nothing was erased on this machine.` } : result;
110085
+ }
110086
+ const erasedToken = removeBotToken(this.options.dir);
110087
+ const erasedConfig = removeRemoteConfig(this.options.dir);
110088
+ const erased = erasedToken && erasedConfig;
110089
+ const gone = erased ? "The bot token and the config were erased." : "The bot token or the config could not be erased; remove the files in the keryx remote directory by hand.";
110090
+ if (!result.ok) {
110091
+ return {
110092
+ ok: true,
110093
+ value: {
110094
+ deleted: 0,
110095
+ remaining: 0,
110096
+ topicsDeleted: false,
110097
+ erased,
110098
+ message: `keryx serve is not running, so the topics could not be deleted: they remain in the group. ${gone}`
110099
+ }
110100
+ };
110101
+ }
110102
+ const { deleted, remaining, hubWasRunning } = result.value;
110103
+ const topics = !hubWasRunning ? "Telegram was not running in serve, so the topics could not be deleted: they remain in the group." : remaining > 0 ? `${plural2(deleted, "topic", "topics")} deleted; ${plural2(remaining, "topic", "topics")} could not be deleted and remain in the group.` : `${plural2(deleted, "topic", "topics")} deleted.`;
110104
+ return { ok: true, value: { deleted, remaining, topicsDeleted: hubWasRunning && remaining === 0, erased, message: `${topics} ${gone}` } };
110105
+ }
110106
+ target(route) {
110107
+ const endpoint = readEndpoint(this.options.dir);
110108
+ if (!endpoint.ok) {
110109
+ throw new ServeDown("serve-down", `keryx serve is not running (${endpoint.reason})`);
110110
+ }
110111
+ if (!isLoopbackAddress(endpoint.value.address)) {
110112
+ throw new ServeDown("unsafe-endpoint", "the serve endpoint is not a loopback address; refusing to send the shell token there");
110113
+ }
110114
+ if (!this.isAlive(endpoint.value.pid)) {
110115
+ throw new ServeDown("serve-down", `keryx serve is not running (pid ${endpoint.value.pid} is gone); start it with \`keryx serve\``);
110116
+ }
110117
+ const token = readShellToken(this.options.dir);
110118
+ if (!token.ok) {
110119
+ throw new ServeDown("serve-down", `keryx serve is not accepting the shell yet (${token.reason})`);
110120
+ }
110121
+ return { url: `http://${authority(endpoint.value.address, endpoint.value.port)}${channelsRoutePath(route)}`, token: token.value };
110122
+ }
110123
+ async post(route) {
110124
+ const { url, token } = this.target(route);
110125
+ return this.fetchImpl(url, {
110126
+ method: "POST",
110127
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
110128
+ body: "{}",
110129
+ signal: AbortSignal.timeout(route === "channels-disconnect" ? this.requestTimeoutMs * DISCONNECT_TIMEOUT_FACTOR : this.requestTimeoutMs)
110130
+ });
110131
+ }
110132
+ async get(route) {
110133
+ const { url, token } = this.target(route);
110134
+ return this.fetchImpl(url, {
110135
+ method: "GET",
110136
+ headers: { authorization: `Bearer ${token}` },
110137
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
110138
+ });
110139
+ }
110140
+ reachable() {
110141
+ try {
110142
+ this.target("channels-status");
110143
+ return { ok: true };
110144
+ } catch (error) {
110145
+ if (error instanceof ServeDown) {
110146
+ return { ok: false, code: error.code, reason: error.message };
110147
+ }
110148
+ throw error;
110149
+ }
110150
+ }
110151
+ async call(route, guard) {
110152
+ let response;
110153
+ try {
110154
+ response = CHANNELS_ROUTE_METHODS[route] === "GET" ? await this.get(route) : await this.post(route);
110155
+ } catch (error) {
110156
+ if (error instanceof ServeDown) {
110157
+ return { ok: false, code: error.code, reason: error.message };
110158
+ }
110159
+ if (refusedConnection(error)) {
110160
+ return { ok: false, code: "serve-down", reason: "keryx serve is not accepting connections; is it still running?" };
110161
+ }
110162
+ return {
110163
+ ok: false,
110164
+ code: "no-answer",
110165
+ reason: "keryx serve did not answer in time or dropped the connection, so the outcome is unknown. Check /channels, then retry if needed."
110166
+ };
110167
+ }
110168
+ let parsed;
110169
+ try {
110170
+ parsed = JSON.parse(await response.text());
110171
+ } catch {
110172
+ parsed = undefined;
110173
+ }
110174
+ if (!response.ok) {
110175
+ const error = isObject2(parsed) && isObject2(parsed.error) ? parsed.error : undefined;
110176
+ const code = typeof error?.code === "string" ? error.code : `http-${response.status}`;
110177
+ const message = typeof error?.message === "string" ? error.message : `keryx serve refused the request (HTTP ${response.status})`;
110178
+ return { ok: false, code, reason: message };
110179
+ }
110180
+ if (!isObject2(parsed) || !guard(parsed)) {
110181
+ return { ok: false, code: "unexpected-response", reason: "keryx serve sent an answer this shell does not understand; update keryx on both sides" };
110182
+ }
110183
+ return { ok: true, value: parsed };
110184
+ }
110185
+ restore(file, before) {
110186
+ try {
110187
+ if (before.ok) {
110188
+ ensureRemoteDir(this.options.dir);
110189
+ writeOwnerOnlyFileAtomic(file, before.text);
110190
+ } else if (file === botTokenPath(this.options.dir)) {
110191
+ removeBotToken(this.options.dir);
110192
+ } else {
110193
+ removeRemoteConfig(this.options.dir);
110194
+ }
110195
+ } catch {}
110196
+ }
110197
+ }
110198
+ function refusedConnection(error) {
110199
+ const seen = new Set;
110200
+ for (let current = error;isObject2(current) || current instanceof Error; ) {
110201
+ if (seen.has(current))
110202
+ break;
110203
+ seen.add(current);
110204
+ const { code, message, cause } = current;
110205
+ if (code === "ECONNREFUSED" || code === "ConnectionRefused" || code === "EHOSTUNREACH" || code === "ENETUNREACH")
110206
+ return true;
110207
+ if (typeof message === "string" && /ECONNREFUSED|Unable to connect|connection refused/i.test(message))
110208
+ return true;
110209
+ current = cause;
110210
+ }
110211
+ return false;
110212
+ }
110213
+ function isPairing(body) {
110214
+ return typeof body.state === "string" && typeof body.expiresAt === "number" && Array.isArray(body.problems);
110215
+ }
110216
+ var DEFAULT_REQUEST_TIMEOUT_MS2 = 15000, DISCONNECT_TIMEOUT_FACTOR = 8, ServeDown;
110217
+ var init_channels_client = __esm(() => {
110218
+ init_serve_config();
110219
+ init_config_dir();
110220
+ init_client3();
110221
+ init_config10();
110222
+ init_endpoint();
110223
+ init_paths4();
110224
+ init_protocol2();
110225
+ init_shell_token();
110226
+ ServeDown = class ServeDown extends Error {
110227
+ code;
110228
+ constructor(code, message) {
110229
+ super(message);
110230
+ this.code = code;
110231
+ }
110232
+ };
110233
+ });
110234
+
110235
+ // src/tui/action-button.ts
110236
+ function smallActionButton(otui, r, label, id, color, onMouseDown) {
110237
+ const box = new otui.BoxRenderable(r, {
110238
+ id,
110239
+ flexShrink: 0,
110240
+ marginLeft: 1,
110241
+ paddingLeft: 1,
110242
+ paddingRight: 1,
110243
+ onMouseDown: (event) => {
110244
+ event.stopPropagation();
110245
+ onMouseDown();
110246
+ }
110247
+ });
110248
+ const text = new otui.TextRenderable(r, { id: `${id}-t`, content: `[${label}]` });
110249
+ text.fg = color;
110250
+ box.add(text);
110251
+ const setActive = (active) => {
110252
+ box.backgroundColor = active ? getTheme().highlight : undefined;
110253
+ text.content = active ? otui.t`${boldChunk(otui, `[${label}]`)}` : `[${label}]`;
110254
+ text.fg = color;
110255
+ };
110256
+ return { box, setActive };
110257
+ }
110258
+ var init_action_button = __esm(() => {
110259
+ init_theme();
110260
+ init_theme_text();
110261
+ });
110262
+
110263
+ // src/tui/queue-nav.ts
110264
+ function stepQueueNavIndex(selected, count, direction) {
110265
+ if (count <= 0)
110266
+ return 0;
110267
+ if (direction === "up") {
110268
+ return selected > 0 ? selected - 1 : count - 1;
110269
+ }
110270
+ return selected < count - 1 ? selected + 1 : 0;
110271
+ }
110272
+ function clampQueueNavIndex(selected, count) {
110273
+ if (count <= 0)
110274
+ return 0;
110275
+ return Math.min(Math.max(selected, 0), count - 1);
110276
+ }
110277
+ function stepQueueNavAction(selected, direction) {
110278
+ const idx = QUEUE_NAV_ACTIONS.indexOf(selected);
110279
+ const count = QUEUE_NAV_ACTIONS.length;
110280
+ const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
110281
+ return QUEUE_NAV_ACTIONS[nextIdx];
110282
+ }
110283
+ function stepConnectNavAction(selected, direction) {
110284
+ const idx = CONNECT_NAV_ACTIONS.indexOf(selected);
110285
+ const count = CONNECT_NAV_ACTIONS.length;
110286
+ const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
110287
+ return CONNECT_NAV_ACTIONS[nextIdx];
110288
+ }
110289
+ var QUEUE_NAV_ACTIONS, CONNECT_NAV_ACTIONS;
110290
+ var init_queue_nav = __esm(() => {
110291
+ QUEUE_NAV_ACTIONS = ["force", "edit", "delete"];
110292
+ CONNECT_NAV_ACTIONS = ["label", "test", "disconnect"];
110293
+ });
110294
+
110295
+ // src/tui/settings-modal.ts
110296
+ function isSettingsCommand(line) {
110297
+ const token = line.trim().split(/\s+/)[0] ?? "";
110298
+ return token === SETTINGS_COMMAND;
110299
+ }
110300
+ function settingsSidebarHint(readOnlyShown) {
110301
+ return readOnlyShown ? undefined : SETTINGS_COMMAND;
110302
+ }
110303
+ function settingsWindow(rows, selected, top, budget) {
110304
+ let start = Math.min(top, selected);
110305
+ while (start < selected && lineCount(rows, start, selected) > budget)
110306
+ start++;
110307
+ let end = start;
110308
+ while (end + 1 < rows.length && lineCount(rows, start, end + 1) <= budget)
110309
+ end++;
110310
+ return { top: start, end };
110311
+ }
110312
+ function openSettings(otui, chrome, options) {
110313
+ const core = otui;
110314
+ const r = options.renderer;
110315
+ const now = options.now ?? Date.now;
110316
+ let rows = options.rows;
110317
+ let selectedRow = 0;
110318
+ let selectedAction = 0;
110319
+ let top = 0;
110320
+ let armed;
110321
+ let busy = false;
110322
+ let closed = false;
110323
+ let body;
110324
+ let budget = Number.MAX_SAFE_INTEGER;
110325
+ let blocks = [];
110326
+ const activeIndex = (row) => Math.max(0, row?.actions.findIndex((a) => a.active) ?? 0);
110327
+ const disarm = () => {
110328
+ armed = undefined;
110329
+ };
110330
+ const blocked = () => closed || options.inputBlocked?.() === true;
110331
+ function paint() {
110332
+ if (closed || body === undefined)
110333
+ return;
110334
+ for (const block of blocks) {
110335
+ try {
110336
+ body.remove(block);
110337
+ } catch {}
110338
+ }
110339
+ blocks = [];
110340
+ selectedRow = clampQueueNavIndex(selectedRow, rows.length);
110341
+ const win = settingsWindow(rows, selectedRow, top, budget);
110342
+ top = win.top;
110343
+ const labelWidth = Math.max(...rows.map((row) => row.label.length));
110344
+ const valueWidth = Math.max(...rows.map((row) => row.value.length));
110345
+ for (let i = win.top;i <= win.end; i++) {
110346
+ const row = rows[i];
110347
+ const selected = i === selectedRow;
110348
+ if (i === win.top || rows[i - 1].group !== row.group) {
110349
+ const heading = new core.TextRenderable(r, { id: `st-group-${row.group}`, content: core.t`${boldChunk(core, row.group)}` });
110350
+ body.add(heading);
110351
+ blocks.push(heading);
110352
+ }
110353
+ const outer = new core.BoxRenderable(r, {
110354
+ id: `st-${row.id}`,
110355
+ width: "100%",
110356
+ flexDirection: "column",
110357
+ ...selected ? { backgroundColor: getTheme().highlight } : {},
110358
+ onMouseDown: (event) => {
110359
+ event.stopPropagation();
110360
+ if (blocked())
110361
+ return;
110362
+ select(i, activeIndex(row));
110363
+ }
110364
+ });
110365
+ const label = row.label.padEnd(labelWidth);
110366
+ const confirming = armed?.id === row.id ? row.actions[armed.action] : undefined;
110367
+ const detail = row.detail === undefined ? "" : dimChunk(core, ` ${row.detail}`);
110368
+ const armedNote = confirming === undefined ? "" : roleChunk(core, "attention", ` Enter again to confirm ${confirming.label}: it skips confirmation for every action`);
110369
+ outer.add(new core.TextRenderable(r, {
110370
+ id: `st-${row.id}-line`,
110371
+ marginLeft: 2,
110372
+ content: core.t`${selected ? boldChunk(core, label) : label} ${roleChunk(core, "ok", row.value.padEnd(valueWidth))} ${dimChunk(core, `[${row.scope}]`)}${detail}${armedNote}`
110373
+ }));
110374
+ const buttons = new core.BoxRenderable(r, { id: `st-${row.id}-buttons`, width: "100%", flexDirection: "row", marginLeft: 1 });
110375
+ if (row.actions.length === 0) {
110376
+ buttons.add(new core.TextRenderable(r, { id: `st-${row.id}-hint`, marginLeft: 1, content: core.t`${dimChunk(core, `run ${row.usage}`)}` }));
110377
+ }
110378
+ row.actions.forEach((action, index) => {
110379
+ const color = action.active ? getTheme().ok : action.confirm ? getTheme().error : getTheme().tool;
110380
+ const button = smallActionButton(core, r, action.active ? `\u2713 ${action.label}` : action.label, `st-${row.id}-${index}`, color, () => {
110381
+ if (blocked())
110382
+ return;
110383
+ select(i, index);
110384
+ press();
110385
+ });
110386
+ button.setActive(selected && index === selectedAction);
110387
+ buttons.add(button.box);
110388
+ });
110389
+ outer.add(buttons);
110390
+ body.add(outer);
110391
+ blocks.push(outer);
110392
+ }
110393
+ }
110394
+ function select(row, action) {
110395
+ if (row !== selectedRow || action !== selectedAction)
110396
+ disarm();
110397
+ selectedRow = row;
110398
+ selectedAction = action;
110399
+ paint();
110400
+ }
110401
+ const report = (what, error) => {
110402
+ options.onError?.(`/settings: ${what}: ${error instanceof Error ? error.message : String(error)}
110403
+ `);
110404
+ };
110405
+ async function press(viaKey = false) {
110406
+ if (blocked())
110407
+ return;
110408
+ const row = rows[selectedRow];
110409
+ const action = row?.actions[selectedAction];
110410
+ if (row === undefined || action === undefined || busy)
110411
+ return;
110412
+ if (action.confirm) {
110413
+ if (armed?.id !== row.id || armed.action !== selectedAction) {
110414
+ armed = { id: row.id, action: selectedAction, at: now() };
110415
+ paint();
110416
+ return;
110417
+ }
110418
+ if (viaKey && now() - armed.at < CONFIRM_MIN_GAP_MS)
110419
+ return;
110420
+ }
110421
+ disarm();
110422
+ busy = true;
110423
+ try {
110424
+ try {
110425
+ await options.run(action.command);
110426
+ } catch (error) {
110427
+ report(action.command, error);
110428
+ }
110429
+ try {
110430
+ rows = await options.load();
110431
+ } catch (error) {
110432
+ report("could not reload the rows", error);
110433
+ }
110434
+ } finally {
110435
+ busy = false;
110436
+ }
110437
+ const at = rows.findIndex((candidate) => candidate.id === row.id);
110438
+ if (at >= 0)
110439
+ selectedRow = at;
110440
+ selectedAction = clampQueueNavIndex(selectedAction, rows[selectedRow]?.actions.length ?? 0);
110441
+ paint();
110442
+ }
110443
+ const swallow = (key) => {
110444
+ key.preventDefault?.();
110445
+ key.stopPropagation?.();
110446
+ };
110447
+ const unsubscribeKeys = options.onKeypress((key) => {
110448
+ if (blocked())
110449
+ return;
110450
+ if (key.name === "escape") {
110451
+ if (armed === undefined)
110452
+ return;
110453
+ disarm();
110454
+ paint();
110455
+ swallow(key);
110456
+ return;
110457
+ }
110458
+ if (busy)
110459
+ return;
110460
+ const row = rows[selectedRow];
110461
+ if (key.name === "up" || key.name === "down") {
110462
+ const next = stepQueueNavIndex(selectedRow, rows.length, key.name);
110463
+ select(next, activeIndex(rows[next]));
110464
+ } else if (key.name === "left" || key.name === "right") {
110465
+ const count = row?.actions.length ?? 0;
110466
+ if (count > 0)
110467
+ select(selectedRow, stepQueueNavIndex(selectedAction, count, key.name === "left" ? "up" : "down"));
110468
+ } else if (key.name === "return" || key.name === "linefeed" || key.name === "kpenter") {
110469
+ press(true);
110470
+ } else {
110471
+ return;
110472
+ }
110473
+ swallow(key);
110474
+ });
110475
+ let unsubscribeTheme = () => {};
110476
+ const groups = new Set(rows.map((row) => row.group)).size;
110477
+ selectedAction = activeIndex(rows[0]);
110478
+ const handle = openModal(core, chrome, {
110479
+ title: SETTINGS_COMMAND,
110480
+ tabs: [{ id: "settings", label: "Settings" }],
110481
+ footer: SETTINGS_FOOTER,
110482
+ contentRows: rows.length * 2 + groups,
110483
+ renderTab: (_tabId, tabBody, ctx) => {
110484
+ body = tabBody;
110485
+ budget = ctx.height;
110486
+ blocks = [];
110487
+ paint();
110488
+ },
110489
+ onClose: () => {
110490
+ closed = true;
110491
+ unsubscribeKeys();
110492
+ unsubscribeTheme();
110493
+ }
110494
+ });
110495
+ if (handle === undefined) {
110496
+ unsubscribeKeys();
110497
+ return;
110498
+ }
110499
+ unsubscribeTheme = onThemeChange(guardedThemeRepaint("settings-modal", paint, () => closed || isRenderableGone(body) || r.isDestroyed === true));
110500
+ return handle;
110501
+ }
110502
+ var SETTINGS_COMMAND = "/settings", CONFIRM_MIN_GAP_MS = 400, SETTINGS_FOOTER, lineCount = (rows, from, to) => {
110503
+ let lines = 0;
110504
+ for (let i = from;i <= to; i++) {
110505
+ lines += 2 + (i === from || rows[i - 1].group !== rows[i].group ? 1 : 0);
110506
+ }
110507
+ return lines;
110508
+ };
110509
+ var init_settings_modal = __esm(() => {
110510
+ init_action_button();
110511
+ init_modal_host();
110512
+ init_queue_nav();
110513
+ init_theme();
110514
+ init_theme_repaint();
110515
+ init_theme_text();
110516
+ SETTINGS_FOOTER = [
110517
+ { key: "\u2191/\u2193", label: "setting" },
110518
+ { key: "\u2190/\u2192", label: "value" },
110519
+ { key: "Enter", label: "apply" },
110520
+ { key: "esc", label: "close" }
110521
+ ];
110522
+ });
110523
+
110524
+ // src/tui/channels-surface.ts
110525
+ function isChannelsCommand(line) {
110526
+ return (line.trim().split(/\s+/)[0] ?? "") === CHANNELS_COMMAND;
110527
+ }
110528
+ function parseChannelsArgs(line) {
110529
+ const words = line.trim().split(/\s+/).slice(1);
110530
+ if (words.length === 0 || words.length === 1 && words[0].toLowerCase() === "status")
110531
+ return { action: "open" };
110532
+ return { action: "invalid", message: CHANNELS_USAGE };
110533
+ }
110534
+ async function loadChannelsSnapshot(client) {
110535
+ const status = await client.status();
110536
+ const local = client.localFiles();
110537
+ if (status.ok) {
110538
+ const { machine, telegram } = status.value;
110539
+ return {
110540
+ state: telegram.state,
110541
+ configured: telegram.state !== "not-connected" || local.tokenFile || local.configFile,
110542
+ machine,
110543
+ sessions: telegram.sessions,
110544
+ ...telegram.reason !== undefined ? { reason: telegram.reason } : {}
110545
+ };
110546
+ }
110547
+ const configured = local.tokenFile || local.configFile;
110548
+ const down = status.code === "serve-down" || status.code === "unsafe-endpoint";
110549
+ return { state: down ? "serve-down" : "error", configured, reason: status.reason };
110550
+ }
110551
+ function fit2(text, width) {
110552
+ if (width <= 0)
110553
+ return "";
110554
+ return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
110555
+ }
110556
+ function projectChannelsRow(snapshot, width) {
110557
+ if (snapshot === undefined)
110558
+ return { text: fit2("\u2026", width), role: "muted", action: "open" };
110559
+ switch (snapshot.state) {
110560
+ case "connected":
110561
+ return { text: fit2("connected", width), role: "ok", action: "open" };
110562
+ case "pairing":
110563
+ return { text: fit2("pairing", width), role: "attention", action: "open" };
110564
+ case "off":
110565
+ return { text: fit2("off", width), role: "attention", action: "open" };
110566
+ case "serve-down":
110567
+ return snapshot.configured ? { text: fit2("serve down", width), role: "attention", action: "open" } : { text: fit2("off", width), role: "muted", action: "open" };
110568
+ case "error":
110569
+ return { text: fit2("error", width), role: "attention", action: "open" };
110570
+ default:
110571
+ return { text: fit2("off", width), role: "muted", action: "open" };
110572
+ }
110573
+ }
110574
+ function channelsStatusLines(snapshot) {
110575
+ const machine = snapshot.machine === undefined ? [] : [`Machine: ${snapshot.machine}`];
110576
+ switch (snapshot.state) {
110577
+ case "not-connected":
110578
+ return [
110579
+ "Telegram: not connected",
110580
+ ...machine,
110581
+ ...snapshot.configured ? ["A bot token is left on this machine from an unfinished connection. Connect starts over; Disconnect erases it."] : [
110582
+ "This machine has no bot yet. Connect it to drive keryx sessions from Telegram topics.",
110583
+ "You need a bot token from @BotFather and a Telegram group with topics turned on."
110584
+ ]
110585
+ ];
110586
+ case "pairing":
110587
+ return [
110588
+ "Telegram: pairing in progress",
110589
+ ...machine,
110590
+ "Resume shows the one-time code again; Cancel pairing abandons it and erases the token."
110591
+ ];
110592
+ case "connected":
110593
+ return [
110594
+ "Telegram: connected",
110595
+ ...machine,
110596
+ `Sessions in Telegram: ${snapshot.sessions ?? 0}`,
110597
+ "Test sends one message to the General topic. Disconnect deletes the topics and erases the token.",
110598
+ "Sessions join through /remote-control in each shell."
110599
+ ];
110600
+ case "off":
110601
+ return [
110602
+ "Telegram: configured, but not running",
110603
+ ...machine,
110604
+ ...snapshot.reason === undefined ? [] : [`Why: ${snapshot.reason}`],
110605
+ "Disconnect erases the token and config. Test works once Telegram is running again."
110606
+ ];
110607
+ case "serve-down":
110608
+ return [
110609
+ "Telegram: keryx serve is not running",
110610
+ ...snapshot.reason === undefined ? [] : [snapshot.reason],
110611
+ snapshot.configured ? "A bot is configured on this machine; it works again once `keryx serve` runs. Disconnect still erases the token." : "Start `keryx serve`, then connect."
110612
+ ];
110613
+ case "error":
110614
+ return ["Telegram: status unavailable", ...snapshot.reason === undefined ? [] : [snapshot.reason]];
110615
+ }
110616
+ }
110617
+ function readlineChannelsText(line, snapshot) {
110618
+ const request = parseChannelsArgs(line);
110619
+ if (request.action === "invalid")
110620
+ return `${request.message}
110621
+ `;
110622
+ const how = snapshot.state === "not-connected" || snapshot.state === "serve-down" || snapshot.state === "error" ? "Connecting needs a hidden token entry, which this line-based shell cannot give: run `keryx shell` (the full-screen shell) and use /channels there." : "Test and Disconnect are in the full-screen shell: run `keryx shell` and use /channels there.";
110623
+ return `${[...channelsStatusLines(snapshot), "", how].join(`
110624
+ `)}
110625
+ `;
110626
+ }
110627
+ function codeLifetime(expiresAt, now) {
110628
+ const left = expiresAt - now;
110629
+ if (left <= 0)
110630
+ return "expired";
110631
+ if (left < 60000)
110632
+ return "less than a minute";
110633
+ return `${Math.ceil(left / 60000)} min`;
110634
+ }
110635
+ function channelsViewLines(view, snapshot, ctx) {
110636
+ switch (view.kind) {
110637
+ case "list": {
110638
+ const lines = [];
110639
+ if (view.notice !== undefined)
110640
+ lines.push({ text: view.notice.text, tone: view.notice.tone }, { text: "", tone: "text" });
110641
+ if (snapshot === undefined)
110642
+ return [...lines, { text: "Reading the Telegram state\u2026", tone: "muted" }];
110643
+ const [first, ...rest] = channelsStatusLines(snapshot);
110644
+ return [...lines, { text: first, tone: "heading" }, ...rest.map((text) => ({ text, tone: "text" }))];
110645
+ }
110646
+ case "token":
110647
+ return [
110648
+ { text: "Step 1 of 4 \xB7 Bot token", tone: "heading" },
110649
+ { text: "Create a bot with @BotFather in Telegram, then paste its token here.", tone: "text" },
110650
+ { text: "", tone: "text" },
110651
+ { text: `Token: ${"*".repeat(Math.min(ctx.tokenLength, 60))}${ctx.tokenLength === 0 ? "(waiting for the token)" : ""}`, tone: "code" },
110652
+ ...view.error === undefined ? [] : [{ text: view.error, tone: "error" }],
110653
+ { text: "", tone: "text" },
110654
+ { text: "Typed hidden: the token is not shown, not kept in history and not logged. It goes only to this machine's owner-only keryx config.", tone: "muted" },
110655
+ { text: "Enter to continue \xB7 Esc to go back", tone: "muted" }
110656
+ ];
110657
+ case "working":
110658
+ return [{ text: view.text, tone: "heading" }];
110659
+ case "pairing": {
110660
+ const { pairing } = view;
110661
+ const bot = pairing.botUsername === undefined ? "the bot" : `@${pairing.botUsername}`;
110662
+ if (pairing.state === "waiting-for-user") {
110663
+ return [
110664
+ { text: "Step 2 of 4 \xB7 Send the code to the bot", tone: "heading" },
110665
+ { text: `Open a private chat with ${bot} in Telegram and send it this code:`, tone: "text" },
110666
+ { text: "", tone: "text" },
110667
+ { text: ` ${pairing.code ?? "(no code: cancel and connect again)"}`, tone: "code" },
110668
+ { text: "", tone: "text" },
110669
+ { text: `Waiting for your message\u2026 the code is valid for ${codeLifetime(pairing.expiresAt, ctx.now)} and works once.`, tone: "muted" },
110670
+ ...pairing.problems.length === 0 ? [] : pairing.problems.map((text) => ({ text, tone: "attention" }))
110671
+ ];
110672
+ }
110673
+ return [
110674
+ { text: "Step 3 of 4 \xB7 Add the bot to your group", tone: "heading" },
110675
+ { text: `Add ${bot} to your Telegram group as an administrator with the "Manage topics" right. The group must have topics turned on.`, tone: "text" },
110676
+ ...pairing.chatTitle === undefined ? [] : [{ text: `Group: ${pairing.chatTitle}`, tone: "text" }],
110677
+ ...pairing.problems.length === 0 ? [{ text: "Waiting for the group\u2026", tone: "muted" }] : [{ text: "Still missing:", tone: "attention" }, ...pairing.problems.map((text) => ({ text: ` - ${text}`, tone: "attention" }))]
110678
+ ];
110679
+ }
110680
+ case "connected":
110681
+ return [
110682
+ { text: "Step 4 of 4 \xB7 Connected", tone: "heading" },
110683
+ ...view.group === undefined ? [] : [{ text: `Group: ${view.group}`, tone: "text" }],
110684
+ ...snapshot?.machine === undefined ? [] : [{ text: `Machine: ${snapshot.machine}`, tone: "text" }],
110685
+ { text: view.test.text, tone: view.test.tone },
110686
+ { text: "Turn a session on with /remote-control in each shell.", tone: "muted" }
110687
+ ];
110688
+ case "failed":
110689
+ return [
110690
+ { text: view.title, tone: "error" },
110691
+ { text: view.reason, tone: "text" },
110692
+ { text: "Nothing was kept on this machine for the attempt that failed.", tone: "muted" }
110693
+ ];
110694
+ case "confirm-disconnect":
110695
+ return [
110696
+ { text: "Disconnect Telegram from this machine?", tone: "heading" },
110697
+ { text: "This deletes every keryx topic in the group, stops polling, and erases the bot token and the config.", tone: "attention" },
110698
+ { text: "The group itself and the bot stay in Telegram.", tone: "muted" }
110699
+ ];
110700
+ }
110701
+ }
110702
+ function defaultSchedule(fn, ms) {
110703
+ const timer = setInterval(fn, ms);
110704
+ return () => clearInterval(timer);
110705
+ }
110706
+ function openChannels(otui, chrome, options) {
110707
+ const core = otui;
110708
+ const r = options.renderer;
110709
+ const { client } = options;
110710
+ const now = options.now ?? Date.now;
110711
+ const schedule = options.schedule ?? defaultSchedule;
110712
+ let snapshot = options.snapshot;
110713
+ let view = { kind: "list" };
110714
+ let secret = "";
110715
+ let selected = 0;
110716
+ let busy = false;
110717
+ let closed = false;
110718
+ let epoch = 0;
110719
+ let confirmArmedAt = 0;
110720
+ let stopPolling = () => {};
110721
+ let polling = false;
110722
+ let body;
110723
+ let blocks = [];
110724
+ const blocked = () => closed || options.inputBlocked?.() === true;
110725
+ const learn = (next) => {
110726
+ snapshot = next;
110727
+ options.onChanged?.(next);
110728
+ };
110729
+ const reload = async () => {
110730
+ try {
110731
+ learn(await loadChannelsSnapshot(client));
110732
+ } catch {}
110733
+ };
110734
+ function actions() {
110735
+ switch (view.kind) {
110736
+ case "list": {
110737
+ if (snapshot === undefined)
110738
+ return [];
110739
+ const disconnectButton = { id: "disconnect", label: "Disconnect", tone: "danger", run: () => askDisconnect() };
110740
+ switch (snapshot.state) {
110741
+ case "pairing":
110742
+ return [
110743
+ { id: "resume", label: "Resume", tone: "primary", run: () => void resume() },
110744
+ { id: "cancel-pairing", label: "Cancel pairing", tone: "danger", run: () => void cancelPairing() }
110745
+ ];
110746
+ case "connected":
110747
+ return [{ id: "test", label: "Test", tone: "primary", run: () => void runTest(false) }, disconnectButton];
110748
+ case "not-connected":
110749
+ return [{ id: "connect", label: "Connect", tone: "primary", run: () => void beginConnect() }, ...snapshot.configured ? [disconnectButton] : []];
110750
+ default:
110751
+ return snapshot.configured ? [disconnectButton] : [];
110752
+ }
110753
+ }
110754
+ case "token":
110755
+ return [
110756
+ { id: "continue", label: "Continue", tone: "primary", run: () => void submitToken() },
110757
+ { id: "back", label: "Back", tone: "plain", run: () => backToList() }
110758
+ ];
110759
+ case "pairing":
110760
+ return [
110761
+ { id: "cancel-pairing", label: "Cancel pairing", tone: "danger", run: () => void cancelPairing() },
110762
+ { id: "back", label: "Back", tone: "plain", run: () => backToList() }
110763
+ ];
110764
+ case "connected":
110765
+ return [
110766
+ { id: "test", label: "Test again", tone: "primary", run: () => void runTest(true) },
110767
+ { id: "back", label: "Done", tone: "plain", run: () => backToList() }
110768
+ ];
110769
+ case "failed":
110770
+ return [
110771
+ { id: "retry", label: "Try again", tone: "primary", run: () => void beginConnect() },
110772
+ { id: "back", label: "Back", tone: "plain", run: () => backToList() }
110773
+ ];
110774
+ case "confirm-disconnect":
110775
+ return [
110776
+ { id: "cancel", label: "Cancel", tone: "plain", run: () => backToList() },
110777
+ { id: "confirm-disconnect", label: "Yes, disconnect", tone: "danger", run: () => void disconnect() }
110778
+ ];
110779
+ case "working":
110780
+ return [];
110781
+ }
110782
+ }
110783
+ function chunkFor(line) {
110784
+ const text = line.text === "" ? " " : line.text;
110785
+ switch (line.tone) {
110786
+ case "heading":
110787
+ return boldChunk(core, text);
110788
+ case "muted":
110789
+ return dimChunk(core, text);
110790
+ case "code":
110791
+ return core.bold(roleChunk(core, "accent", text));
110792
+ case "text":
110793
+ return roleChunk(core, "text", text);
110794
+ default:
110795
+ return roleChunk(core, line.tone, text);
110796
+ }
110797
+ }
110798
+ function paint() {
110799
+ if (closed || body === undefined)
110800
+ return;
110801
+ for (const block of blocks) {
110802
+ try {
110803
+ body.remove(block);
110804
+ } catch {}
110805
+ }
110806
+ blocks = [];
110807
+ const lines = channelsViewLines(view, snapshot, { tokenLength: secret.length, now: now() });
110808
+ lines.forEach((line, index) => {
110809
+ const node = new core.TextRenderable(r, { id: `ch-line-${index}`, content: core.t`${chunkFor(line)}` });
110810
+ body.add(node);
110811
+ blocks.push(node);
110812
+ });
110813
+ const available = actions();
110814
+ if (available.length > 0) {
110815
+ selected = Math.min(selected, available.length - 1);
110816
+ const row = new core.BoxRenderable(r, { id: "ch-buttons", width: "100%", flexDirection: "row", marginTop: 1 });
110817
+ available.forEach((action, index) => {
110818
+ const color = action.tone === "primary" ? getTheme().ok : action.tone === "danger" ? getTheme().error : getTheme().tool;
110819
+ const button = smallActionButton(core, r, action.label, `ch-btn-${action.id}`, color, () => {
110820
+ if (blocked() || busy)
110821
+ return;
110822
+ selected = index;
110823
+ press(false);
110824
+ });
110825
+ button.setActive(index === selected);
110826
+ row.add(button.box);
110827
+ });
110828
+ body.add(row);
110829
+ blocks.push(row);
110830
+ }
110831
+ }
110832
+ function go(next, selection = 0) {
110833
+ epoch += 1;
110834
+ stopPolling();
110835
+ stopPolling = () => {};
110836
+ polling = false;
110837
+ view = next;
110838
+ selected = selection;
110839
+ if (next.kind === "pairing") {
110840
+ stopPolling = schedule(() => void pollPairing(), options.pollMs ?? PAIRING_POLL_MS);
110841
+ }
110842
+ paint();
110843
+ }
110844
+ function backToList(notice) {
110845
+ secret = "";
110846
+ go(notice === undefined ? { kind: "list" } : { kind: "list", notice });
110847
+ }
110848
+ function press(viaKey) {
110849
+ const action = actions()[selected];
110850
+ if (action === undefined)
110851
+ return;
110852
+ if (view.kind === "confirm-disconnect" && action.id === "confirm-disconnect" && viaKey && now() - confirmArmedAt < CONFIRM_MIN_GAP_MS) {
110853
+ return;
110854
+ }
110855
+ action.run();
110856
+ }
110857
+ async function beginConnect() {
110858
+ if (busy)
110859
+ return;
110860
+ busy = true;
110861
+ const mine = epoch + 1;
110862
+ go({ kind: "working", text: "Looking for keryx serve\u2026" });
110863
+ try {
110864
+ await reload();
110865
+ if (closed || epoch !== mine)
110866
+ return;
110867
+ if (snapshot?.state === "serve-down" || snapshot?.state === "error") {
110868
+ backToList({ tone: "attention", text: snapshot.reason ?? "keryx serve is not reachable; start it with `keryx serve`." });
110869
+ return;
110870
+ }
110871
+ if (snapshot?.state === "connected" || snapshot?.state === "off") {
110872
+ backToList({ tone: "attention", text: "Telegram is already configured on this machine. Disconnect it first to connect a different bot." });
110873
+ return;
110874
+ }
110875
+ if (snapshot?.state === "pairing") {
110876
+ busy = false;
110877
+ await resume();
110878
+ return;
110879
+ }
110880
+ secret = "";
110881
+ go({ kind: "token" });
110882
+ } finally {
110883
+ busy = false;
110884
+ }
110885
+ }
110886
+ async function submitToken() {
110887
+ if (view.kind !== "token" || busy || secret.length === 0)
110888
+ return;
110889
+ busy = true;
110890
+ const token = secret;
110891
+ secret = "";
110892
+ go({ kind: "working", text: "Checking the token with Telegram\u2026" });
110893
+ const mine = epoch;
110894
+ try {
110895
+ const result = await client.startPairing(token);
110896
+ if (closed || epoch !== mine)
110897
+ return;
110898
+ if (!result.ok) {
110899
+ go({ kind: "token", error: result.reason });
110900
+ return;
110901
+ }
110902
+ await reload();
110903
+ if (closed || epoch !== mine)
110904
+ return;
110905
+ go({ kind: "pairing", pairing: result.value });
110906
+ } finally {
110907
+ busy = false;
110908
+ }
110909
+ }
110910
+ async function resume() {
110911
+ if (busy)
110912
+ return;
110913
+ busy = true;
110914
+ const mine = epoch + 1;
110915
+ go({ kind: "working", text: "Reading the pairing\u2026" });
110916
+ try {
110917
+ const result = await client.pairingStatus();
110918
+ if (closed || epoch !== mine)
110919
+ return;
110920
+ if (!result.ok) {
110921
+ if (result.code === "no-pairing") {
110922
+ await settlePairing(lostPairing());
110923
+ return;
110924
+ }
110925
+ backToList({ tone: "attention", text: result.reason });
110926
+ return;
110927
+ }
110928
+ await settlePairing(result.value);
110929
+ } finally {
110930
+ busy = false;
110931
+ }
110932
+ }
110933
+ async function settlePairing(pairing) {
110934
+ if (pairing.state === "ready") {
110935
+ await finishConnect(pairing);
110936
+ } else if (pairing.state === "waiting-for-user" || pairing.state === "waiting-for-group") {
110937
+ go({ kind: "pairing", pairing });
110938
+ } else {
110939
+ const mine = epoch;
110940
+ await client.cancelPairing();
110941
+ await reload();
110942
+ if (closed || epoch !== mine)
110943
+ return;
110944
+ go({
110945
+ kind: "failed",
110946
+ title: pairing.state === "expired" ? "The pairing code expired" : pairing.state === "cancelled" ? "Pairing was cancelled" : "Pairing failed",
110947
+ reason: pairing.reason ?? (pairing.state === "expired" ? "The code is valid for 10 minutes. Connect again for a new one." : "Connect again to start over.")
110948
+ });
110949
+ }
110950
+ }
110951
+ function lostPairing() {
110952
+ return {
110953
+ schemaVersion: "1",
110954
+ state: "expired",
110955
+ expiresAt: 0,
110956
+ problems: [],
110957
+ reason: "keryx serve no longer has this pairing (it was restarted or the pairing was dropped), so the code on screen does not work. Connect again for a new one."
110958
+ };
110959
+ }
110960
+ async function pollPairing() {
110961
+ if (polling || closed || view.kind !== "pairing" || busy)
110962
+ return;
110963
+ polling = true;
110964
+ const mine = epoch;
110965
+ try {
110966
+ const result = await client.pairingStatus();
110967
+ if (closed || epoch !== mine)
110968
+ return;
110969
+ if (!result.ok && result.code === "no-pairing") {
110970
+ polling = false;
110971
+ busy = true;
110972
+ try {
110973
+ await settlePairing(lostPairing());
110974
+ } finally {
110975
+ busy = false;
110976
+ }
110977
+ return;
110978
+ }
110979
+ if (!result.ok) {
110980
+ view = { kind: "pairing", pairing: { ...view.pairing, problems: [...view.pairing.problems.filter((p) => !p.startsWith("serve: ")), `serve: ${result.reason}`] } };
110981
+ paint();
110982
+ return;
110983
+ }
110984
+ polling = false;
110985
+ const next = result.value;
110986
+ if (next.state === "ready" || next.state === "failed" || next.state === "expired" || next.state === "cancelled") {
110987
+ busy = true;
110988
+ try {
110989
+ await settlePairing(next);
110990
+ } finally {
110991
+ busy = false;
110992
+ }
110993
+ return;
110994
+ }
110995
+ view = { kind: "pairing", pairing: next };
110996
+ paint();
110997
+ } finally {
110998
+ polling = false;
110999
+ }
111000
+ }
111001
+ async function finishConnect(pairing) {
111002
+ if (pairing.userId === undefined || pairing.chatId === undefined) {
111003
+ await client.cancelPairing();
111004
+ await reload();
111005
+ go({ kind: "failed", title: "Pairing is incomplete", reason: "Telegram did not report both the user and the group. Connect again." });
111006
+ return;
111007
+ }
111008
+ go({ kind: "working", text: "Connecting\u2026" });
111009
+ const mine = epoch;
111010
+ const connected = await client.connectFinish({ userId: pairing.userId, chatId: pairing.chatId });
111011
+ if (closed || epoch !== mine)
111012
+ return;
111013
+ if (!connected.ok) {
111014
+ await reload();
111015
+ if (closed || epoch !== mine)
111016
+ return;
111017
+ go({ kind: "failed", title: "Could not connect", reason: connected.reason });
111018
+ return;
111019
+ }
111020
+ await reload();
111021
+ const group = pairing.chatTitle;
111022
+ const test = await client.test();
111023
+ if (closed || epoch !== mine)
111024
+ return;
111025
+ go({ kind: "connected", ...group === undefined ? {} : { group }, test: testNotice(test) });
111026
+ }
111027
+ function testNotice(result) {
111028
+ return result.ok ? { tone: "ok", text: `Test message delivered to the General topic (machine: ${result.value.machine}).` } : { tone: "error", text: `Test message not delivered: ${result.reason}` };
111029
+ }
111030
+ async function runTest(fromConnected) {
111031
+ if (busy)
111032
+ return;
111033
+ busy = true;
111034
+ const group = view.kind === "connected" ? view.group : undefined;
111035
+ go({ kind: "working", text: "Sending a test message\u2026" });
111036
+ const mine = epoch;
111037
+ try {
111038
+ const result = await client.test();
111039
+ if (closed || epoch !== mine)
111040
+ return;
111041
+ const notice = testNotice(result);
111042
+ await reload();
111043
+ if (closed || epoch !== mine)
111044
+ return;
111045
+ if (fromConnected)
111046
+ go({ kind: "connected", ...group === undefined ? {} : { group }, test: notice });
111047
+ else
111048
+ backToList(notice);
111049
+ } finally {
111050
+ busy = false;
111051
+ }
111052
+ }
111053
+ async function cancelPairing() {
111054
+ if (busy)
111055
+ return;
111056
+ busy = true;
111057
+ go({ kind: "working", text: "Cancelling the pairing\u2026" });
111058
+ const mine = epoch;
111059
+ try {
111060
+ const result = await client.cancelPairing();
111061
+ await reload();
111062
+ if (closed || epoch !== mine)
111063
+ return;
111064
+ backToList(result.ok ? { tone: "ok", text: "Pairing cancelled. The token was erased." } : { tone: "attention", text: result.reason });
111065
+ } finally {
111066
+ busy = false;
111067
+ }
111068
+ }
111069
+ function askDisconnect() {
111070
+ confirmArmedAt = now();
111071
+ go({ kind: "confirm-disconnect" }, 0);
111072
+ }
111073
+ async function disconnect() {
111074
+ if (busy)
111075
+ return;
111076
+ busy = true;
111077
+ go({ kind: "working", text: "Deleting the topics and erasing the token\u2026" });
111078
+ const mine = epoch;
111079
+ try {
111080
+ const result = await client.disconnect();
111081
+ await reload();
111082
+ if (closed || epoch !== mine)
111083
+ return;
111084
+ if (!result.ok) {
111085
+ backToList({ tone: "error", text: result.code === "no-answer" ? result.reason : `Not disconnected: ${result.reason}` });
111086
+ return;
111087
+ }
111088
+ backToList({ tone: result.value.erased && result.value.remaining === 0 && result.value.topicsDeleted ? "ok" : "attention", text: result.value.message });
111089
+ } finally {
111090
+ busy = false;
111091
+ }
111092
+ }
111093
+ function appendToken(text) {
111094
+ for (const ch of text) {
111095
+ if (TOKEN_CHAR.test(ch) && secret.length < TOKEN_MAX)
111096
+ secret += ch;
111097
+ }
111098
+ paint();
111099
+ }
111100
+ function tokenKey(key) {
111101
+ const name = key.name;
111102
+ if (key.ctrl === true && name === "c")
111103
+ return;
111104
+ swallow(key);
111105
+ if (name === "escape") {
111106
+ backToList();
111107
+ } else if (name === "return" || name === "linefeed" || name === "kpenter") {
111108
+ submitToken();
111109
+ } else if (name === "backspace" || name === "delete") {
111110
+ secret = secret.slice(0, -1);
111111
+ paint();
111112
+ } else if (key.ctrl === true && name === "u") {
111113
+ secret = "";
111114
+ paint();
111115
+ } else if (key.ctrl !== true && key.meta !== true && key.sequence.length === 1) {
111116
+ appendToken(key.sequence);
111117
+ }
111118
+ }
111119
+ const unsubscribeKeys = options.onKeypress((key) => {
111120
+ if (blocked())
111121
+ return;
111122
+ if (view.kind === "token") {
111123
+ tokenKey(key);
111124
+ return;
111125
+ }
111126
+ const name = key.name;
111127
+ const isEnter = name === "return" || name === "linefeed" || name === "kpenter";
111128
+ if (name === "escape") {
111129
+ if (view.kind === "list" || view.kind === "working")
111130
+ return;
111131
+ backToList();
111132
+ swallow(key);
111133
+ return;
111134
+ }
111135
+ if (busy)
111136
+ return;
111137
+ const available = actions();
111138
+ if (name === "left" || name === "right") {
111139
+ if (available.length < 2)
111140
+ return;
111141
+ selected = name === "left" ? Math.max(0, selected - 1) : Math.min(available.length - 1, selected + 1);
111142
+ paint();
111143
+ swallow(key);
111144
+ } else if (isEnter) {
111145
+ if (available.length === 0)
111146
+ return;
111147
+ press(true);
111148
+ swallow(key);
111149
+ } else if (view.kind === "list" || view.kind === "connected") {
111150
+ const shortcuts = { c: "connect", t: "test", d: "disconnect" };
111151
+ const shortcut = shortcuts[key.sequence.toLowerCase()];
111152
+ const hit = shortcut === undefined ? undefined : available.find((a) => a.id === shortcut || shortcut === "connect" && a.id === "resume");
111153
+ if (hit !== undefined && key.ctrl !== true && key.meta !== true) {
111154
+ hit.run();
111155
+ swallow(key);
111156
+ }
111157
+ }
111158
+ });
111159
+ const unsubscribePaste = options.onPaste?.((event) => {
111160
+ if (blocked() || view.kind !== "token")
111161
+ return;
111162
+ swallow(event);
111163
+ appendToken(new TextDecoder().decode(event.bytes));
111164
+ });
111165
+ let unsubscribeTheme = () => {};
111166
+ const handle = openModal(core, chrome, {
111167
+ title: CHANNELS_COMMAND,
111168
+ tabs: [{ id: "telegram", label: "Telegram" }],
111169
+ ownsKeys: () => view.kind === "token",
111170
+ footer: CHANNELS_FOOTER,
111171
+ contentRows: 14,
111172
+ renderTab: (_tabId, tabBody) => {
111173
+ body = tabBody;
111174
+ blocks = [];
111175
+ paint();
111176
+ },
111177
+ onClose: () => {
111178
+ closed = true;
111179
+ secret = "";
111180
+ stopPolling();
111181
+ unsubscribeKeys();
111182
+ unsubscribePaste?.();
111183
+ unsubscribeTheme();
111184
+ }
111185
+ });
111186
+ if (handle === undefined) {
111187
+ closed = true;
111188
+ unsubscribeKeys();
111189
+ unsubscribePaste?.();
111190
+ return;
111191
+ }
111192
+ unsubscribeTheme = onThemeChange(guardedThemeRepaint("channels-modal", paint, () => closed || isRenderableGone(body) || r.isDestroyed === true));
111193
+ reload().then(() => {
111194
+ if (!closed && view.kind === "list")
111195
+ paint();
111196
+ });
111197
+ return handle;
111198
+ }
111199
+ function mountChannelsPanel(otui, renderer, parent, options) {
111200
+ const core = otui;
111201
+ const box = new core.BoxRenderable(renderer, { id: "sb-channels", flexDirection: "column", flexShrink: 0 });
111202
+ parent.add(box);
111203
+ let disposed = false;
111204
+ let snapshot;
111205
+ const label = new core.TextRenderable(renderer, { id: "sb-channels-k", content: core.t`${dimChunk(core, "Telegram")}`, marginTop: 1 });
111206
+ const value = new core.TextRenderable(renderer, { id: "sb-channels-v", content: "" });
111207
+ box.add(label);
111208
+ box.add(value);
111209
+ const currentRow = () => projectChannelsRow(snapshot, options.width);
111210
+ const draw = () => {
111211
+ if (disposed)
111212
+ return;
111213
+ const row = currentRow();
111214
+ label.content = core.t`${dimChunk(core, "Telegram")}`;
111215
+ value.content = core.t`${roleChunk(core, row.role, row.text)}`;
111216
+ };
111217
+ const activate = () => {
111218
+ options.onOpen();
111219
+ return "open";
111220
+ };
111221
+ label.onMouseDown = () => {
111222
+ activate();
111223
+ };
111224
+ value.onMouseDown = () => {
111225
+ activate();
111226
+ };
111227
+ const refresh = async () => {
111228
+ try {
111229
+ const next = await options.load();
111230
+ if (disposed)
111231
+ return;
111232
+ snapshot = next;
111233
+ draw();
111234
+ } catch {}
111235
+ };
111236
+ const stop = (options.schedule ?? defaultSchedule)(() => void refresh(), options.intervalMs ?? CHANNELS_ROW_POLL_MS);
111237
+ const unsubscribeTheme = onThemeChange(guardedThemeRepaint("channels-panel", draw, () => disposed || isRenderableGone(box)));
111238
+ draw();
111239
+ refresh();
111240
+ return {
111241
+ refresh,
111242
+ update(next) {
111243
+ snapshot = next;
111244
+ draw();
111245
+ },
111246
+ row: currentRow,
111247
+ activate,
111248
+ dispose() {
111249
+ disposed = true;
111250
+ stop();
111251
+ unsubscribeTheme();
111252
+ }
111253
+ };
111254
+ }
111255
+ var CHANNELS_COMMAND = "/channels", CHANNELS_USAGE, CHANNELS_FOOTER, TOKEN_CHAR, TOKEN_MAX = 200, PAIRING_POLL_MS = 2000, swallow = (event) => {
111256
+ event.preventDefault?.();
111257
+ event.stopPropagation?.();
111258
+ }, CHANNELS_ROW_POLL_MS = 1e4;
111259
+ var init_channels_surface = __esm(() => {
111260
+ init_action_button();
111261
+ init_modal_host();
111262
+ init_settings_modal();
111263
+ init_theme();
111264
+ init_theme_repaint();
111265
+ init_theme_text();
111266
+ CHANNELS_USAGE = `Usage: ${CHANNELS_COMMAND} [status]`;
111267
+ CHANNELS_FOOTER = [
111268
+ { key: "c", label: "connect" },
111269
+ { key: "t", label: "test" },
111270
+ { key: "d", label: "disconnect" },
111271
+ { key: "\u2190/\u2192", label: "button" },
111272
+ { key: "Enter", label: "press" },
111273
+ { key: "esc", label: "back" }
111274
+ ];
111275
+ TOKEN_CHAR = /^[A-Za-z0-9_:-]$/;
111276
+ });
111277
+
109806
111278
  // src/tui/remote-control-surface.ts
109807
111279
  function labelTelegramLine(text) {
109808
111280
  return `[${TG_LABEL}] ${text}`;
@@ -109946,17 +111418,17 @@ function presentRemoteControl(open, otui, chrome, options) {
109946
111418
  function openRemoteControl(otui, chrome, options) {
109947
111419
  return presentRemoteControl((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
109948
111420
  }
109949
- function fit2(text, width) {
111421
+ function fit3(text, width) {
109950
111422
  if (width <= 0)
109951
111423
  return "";
109952
111424
  return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
109953
111425
  }
109954
111426
  function projectRemoteRow(status, width) {
109955
111427
  if (status === undefined || status.state === "off")
109956
- return { text: fit2("off", width), role: "muted", action: "open" };
111428
+ return { text: fit3("off", width), role: "muted", action: "open" };
109957
111429
  if (status.state === "offline")
109958
- return { text: fit2("offline", width), role: "attention", action: "open" };
109959
- return { text: fit2(status.name ?? "connecting", width), role: "ok", action: "open" };
111430
+ return { text: fit3("offline", width), role: "attention", action: "open" };
111431
+ return { text: fit3(status.name ?? "connecting", width), role: "ok", action: "open" };
109960
111432
  }
109961
111433
  function mountRemotePanel(otui, renderer, parent, options) {
109962
111434
  const core = otui;
@@ -110241,6 +111713,11 @@ var init_agent_commands = __esm(() => {
110241
111713
  description: "Drive this session from a Telegram topic: /remote-control [name|off|status]. Off by default",
110242
111714
  modes: AGENT_ONLY
110243
111715
  },
111716
+ {
111717
+ name: "/channels",
111718
+ description: "Connect, test or disconnect Telegram for this machine: /channels [status]",
111719
+ modes: AGENT_ONLY
111720
+ },
110244
111721
  {
110245
111722
  name: "/product",
110246
111723
  description: "Intents closed in code that nobody looked back at, with their outcome criteria",
@@ -110474,6 +111951,8 @@ function classifyBusyDispatch(params) {
110474
111951
  return "reviews";
110475
111952
  if (commandName === "/remote-control")
110476
111953
  return "remote-control";
111954
+ if (commandName === "/channels")
111955
+ return "channels";
110477
111956
  if (commandName === "/schedules")
110478
111957
  return "schedules";
110479
111958
  if (commandName === "/approvals")
@@ -111063,26 +112542,26 @@ function createGovernanceRunner(options) {
111063
112542
  function formatReportDate(iso) {
111064
112543
  return iso.trim().replace("T", " ").slice(0, 16);
111065
112544
  }
111066
- function fit3(text, width) {
112545
+ function fit4(text, width) {
111067
112546
  if (width <= 0)
111068
112547
  return "";
111069
112548
  return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
111070
112549
  }
111071
112550
  function projectGovernanceRow(read, run, width, options = {}) {
111072
112551
  if (run.kind === "running")
111073
- return { text: fit3(GOVERNANCE_RUNNING, width), role: "accent", action: "none" };
112552
+ return { text: fit4(GOVERNANCE_RUNNING, width), role: "accent", action: "none" };
111074
112553
  const stored = read?.state === "present" ? read.report.generatedAt : undefined;
111075
112554
  if (run.kind === "failed" && !(stored !== undefined && stored > run.at)) {
111076
- return { text: fit3(GOVERNANCE_FAILED, width), role: "error", action: "run" };
112555
+ return { text: fit4(GOVERNANCE_FAILED, width), role: "error", action: "run" };
111077
112556
  }
111078
112557
  const ran = run.kind === "done" && options.readIsStale === true ? run.generatedAt : undefined;
111079
112558
  const newest = stored === undefined ? ran : ran === undefined || stored >= ran ? stored : ran;
111080
112559
  if (newest === undefined) {
111081
112560
  if (read?.state === "malformed")
111082
- return { text: fit3(GOVERNANCE_UNREADABLE, width), role: "error", action: "open" };
111083
- return { text: fit3(GOVERNANCE_NO_REPORT, width), role: "attention", action: "run" };
112561
+ return { text: fit4(GOVERNANCE_UNREADABLE, width), role: "error", action: "open" };
112562
+ return { text: fit4(GOVERNANCE_NO_REPORT, width), role: "attention", action: "run" };
111084
112563
  }
111085
- return { text: fit3(`last report ${formatReportDate(newest)}`, width), role: "muted", action: "open" };
112564
+ return { text: fit4(`last report ${formatReportDate(newest)}`, width), role: "muted", action: "open" };
111086
112565
  }
111087
112566
  function mountGovernancePanel(otui, renderer, parent, options) {
111088
112567
  const core = otui;
@@ -111526,7 +113005,7 @@ function outcomeRole(outcome) {
111526
113005
  return "attention";
111527
113006
  return "muted";
111528
113007
  }
111529
- function fit4(text, width) {
113008
+ function fit5(text, width) {
111530
113009
  if (width <= 0)
111531
113010
  return "";
111532
113011
  return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
@@ -111545,23 +113024,23 @@ function projectTriggersPanel(view, options) {
111545
113024
  const list = { kind: "list" };
111546
113025
  const rows = [];
111547
113026
  if (options.loadError !== undefined) {
111548
- rows.push({ id: "sb-triggers-error", chunks: [{ role: "error", text: fit4(`could not read: ${options.loadError}`, w) }], target: list });
113027
+ rows.push({ id: "sb-triggers-error", chunks: [{ role: "error", text: fit5(`could not read: ${options.loadError}`, w) }], target: list });
111549
113028
  return { visible: true, rows };
111550
113029
  }
111551
113030
  if (view === undefined || view.config.kind === "absent")
111552
113031
  return { visible: false, rows: [] };
111553
113032
  if (view.config.kind === "broken") {
111554
- rows.push({ id: "sb-triggers-broken", chunks: [{ role: "error", text: fit4(`config unreadable (${view.config.problem})`, w) }], target: list });
113033
+ rows.push({ id: "sb-triggers-broken", chunks: [{ role: "error", text: fit5(`config unreadable (${view.config.problem})`, w) }], target: list });
111555
113034
  return { visible: true, rows };
111556
113035
  }
111557
113036
  const spend = formatTriggerSpend(view.spend);
111558
- rows.push({ id: "sb-triggers-spend", chunks: [{ role: spend.role, text: fit4(spend.text, w) }], target: list });
113037
+ rows.push({ id: "sb-triggers-spend", chunks: [{ role: spend.role, text: fit5(spend.text, w) }], target: list });
111559
113038
  const open = view.openReservations.length;
111560
113039
  if (open > 0) {
111561
113040
  const held = usd(view.openReservations.reduce((sum, r) => sum + r.usd, 0));
111562
113041
  rows.push({
111563
113042
  id: "sb-triggers-reservations",
111564
- chunks: [{ role: "attention", text: fit4(`! ${open} open \xB7 ${held} reserved`, w) }],
113043
+ chunks: [{ role: "attention", text: fit5(`! ${open} open \xB7 ${held} reserved`, w) }],
111565
113044
  target: list
111566
113045
  });
111567
113046
  }
@@ -111577,18 +113056,18 @@ function projectTriggersPanel(view, options) {
111577
113056
  const lastRole = latest === undefined ? "muted" : outcomeRole(latest.outcome);
111578
113057
  const state = ` \xB7 ${entry.enabled ? "enabled" : "disabled"} \xB7 `;
111579
113058
  const nameRoom = Math.max(1, budget - state.length - last.length);
111580
- chunks.push({ role: "text", text: `${fit4(entry.name, nameRoom)}${state}` });
111581
- chunks.push({ role: lastRole, text: fit4(last, Math.max(0, budget - Math.min(entry.name.length, nameRoom) - state.length)) });
113059
+ chunks.push({ role: "text", text: `${fit5(entry.name, nameRoom)}${state}` });
113060
+ chunks.push({ role: lastRole, text: fit5(last, Math.max(0, budget - Math.min(entry.name.length, nameRoom) - state.length)) });
111582
113061
  rows.push({ id: `sb-triggers-${entry.name}`, chunks, target: { kind: "trigger", name: entry.name } });
111583
113062
  }
111584
113063
  const scheduled = scheduledEntries(view).length;
111585
113064
  if (scheduled > 0) {
111586
- rows.push({ id: "sb-triggers-scheduled", chunks: [{ role: "muted", text: fit4(`${scheduled} scheduled`, w) }], target: list });
113065
+ rows.push({ id: "sb-triggers-scheduled", chunks: [{ role: "muted", text: fit5(`${scheduled} scheduled`, w) }], target: list });
111587
113066
  }
111588
113067
  if (view.rejected.length > 0) {
111589
113068
  rows.push({
111590
113069
  id: "sb-triggers-rejected",
111591
- chunks: [{ role: "error", text: fit4(`${view.rejected.length} rejected entr${view.rejected.length === 1 ? "y" : "ies"}`, w) }],
113070
+ chunks: [{ role: "error", text: fit5(`${view.rejected.length} rejected entr${view.rejected.length === 1 ? "y" : "ies"}`, w) }],
111592
113071
  target: list
111593
113072
  });
111594
113073
  }
@@ -111994,7 +113473,7 @@ var init_triggers_inspector = __esm(() => {
111994
113473
 
111995
113474
  // src/tui/trigger-run-now.ts
111996
113475
  import { spawn as nodeSpawn2 } from "child_process";
111997
- import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync8, mkdirSync as mkdirSync8, openSync as openSync6, readdirSync as readdirSync8, readSync as readSync3, unlinkSync as unlinkSync6, writeSync } from "fs";
113476
+ import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync8, mkdirSync as mkdirSync8, openSync as openSync6, readdirSync as readdirSync8, readSync as readSync3, unlinkSync as unlinkSync7, writeSync } from "fs";
111998
113477
  import path251 from "path";
111999
113478
  function triggerRunArgv(name, invocation = resolveKeryxInvocation(), options = {}) {
112000
113479
  return [...invocationArgv(invocation), "trigger", "run", ...options.schedule === true ? ["--schedule"] : [], name];
@@ -112080,7 +113559,7 @@ function pruneRunLogs(dir, name, keep) {
112080
113559
  }
112081
113560
  for (const old of names.slice(0, Math.max(0, names.length - keep))) {
112082
113561
  try {
112083
- unlinkSync6(path251.join(dir, old));
113562
+ unlinkSync7(path251.join(dir, old));
112084
113563
  } catch {}
112085
113564
  }
112086
113565
  }
@@ -114382,7 +115861,7 @@ function compactOutcome(record) {
114382
115861
  const cost = usd === undefined ? "" : usd > 0 && usd < 0.001 ? " <$0.001" : ` $${Number(usd.toFixed(3))}`;
114383
115862
  return `${shortOutcome(record.outcome)}${cost}`;
114384
115863
  }
114385
- function fit5(text, width) {
115864
+ function fit6(text, width) {
114386
115865
  if (width <= 0)
114387
115866
  return "";
114388
115867
  return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
@@ -114390,7 +115869,7 @@ function fit5(text, width) {
114390
115869
  function projectSchedulesPanel(view, options) {
114391
115870
  const w = options.width;
114392
115871
  if (options.loadError !== undefined) {
114393
- return { visible: true, rows: [{ id: "sb-schedules-error", name: "", chunks: [{ role: "error", text: fit5(`could not read: ${options.loadError}`, w) }] }] };
115872
+ return { visible: true, rows: [{ id: "sb-schedules-error", name: "", chunks: [{ role: "error", text: fit6(`could not read: ${options.loadError}`, w) }] }] };
114394
115873
  }
114395
115874
  if (view === undefined)
114396
115875
  return { visible: false, rows: [] };
@@ -114406,7 +115885,7 @@ function projectSchedulesPanel(view, options) {
114406
115885
  const last = compactOutcome(latest);
114407
115886
  const middle = ` ${state} `;
114408
115887
  const nameRoom = Math.max(1, w - middle.length - last.length);
114409
- const name = fit5(entry.name, nameRoom);
115888
+ const name = fit6(entry.name, nameRoom);
114410
115889
  const lastRoom = Math.max(0, w - name.length - middle.length);
114411
115890
  return {
114412
115891
  id: `sb-schedules-${entry.name}`,
@@ -114414,7 +115893,7 @@ function projectSchedulesPanel(view, options) {
114414
115893
  chunks: [
114415
115894
  { role: "text", text: name },
114416
115895
  { role: entry.enabled && !missing ? "dim" : "attention", text: middle },
114417
- { role: latest === undefined ? "muted" : scheduleOutcomeRole(latest.outcome), text: fit5(last, lastRoom) }
115896
+ { role: latest === undefined ? "muted" : scheduleOutcomeRole(latest.outcome), text: fit6(last, lastRoom) }
114418
115897
  ]
114419
115898
  };
114420
115899
  });
@@ -127332,7 +128811,7 @@ async function loadMcpConfig(cwd) {
127332
128811
  return mergeMcpConfig(parsed);
127333
128812
  }
127334
128813
  var MCP_CONFIG_DEFAULTS;
127335
- var init_config10 = __esm(() => {
128814
+ var init_config11 = __esm(() => {
127336
128815
  init_fs();
127337
128816
  init_json();
127338
128817
  MCP_CONFIG_DEFAULTS = {
@@ -127796,7 +129275,7 @@ var MCP_MANAGED_KEY = "_keryxManaged", MCP_MANAGED_SENTINEL = "mcp-client-config
127796
129275
  var init_client_config = __esm(() => {
127797
129276
  init_fs();
127798
129277
  init_contained_write();
127799
- init_config10();
129278
+ init_config11();
127800
129279
  MCP_SERVER_ARGS = ["serve-mcp"];
127801
129280
  CURSOR_RUNTIME = fileRuntime("cursor", ".cursor/mcp.json");
127802
129281
  CLAUDE_RUNTIME = fileRuntime("claude", ".mcp.json");
@@ -128537,34 +130016,6 @@ var init_session_info = __esm(() => {
128537
130016
  ];
128538
130017
  });
128539
130018
 
128540
- // src/tui/action-button.ts
128541
- function smallActionButton(otui, r, label, id, color, onMouseDown) {
128542
- const box = new otui.BoxRenderable(r, {
128543
- id,
128544
- flexShrink: 0,
128545
- marginLeft: 1,
128546
- paddingLeft: 1,
128547
- paddingRight: 1,
128548
- onMouseDown: (event) => {
128549
- event.stopPropagation();
128550
- onMouseDown();
128551
- }
128552
- });
128553
- const text = new otui.TextRenderable(r, { id: `${id}-t`, content: `[${label}]` });
128554
- text.fg = color;
128555
- box.add(text);
128556
- const setActive = (active) => {
128557
- box.backgroundColor = active ? getTheme().highlight : undefined;
128558
- text.content = active ? otui.t`${boldChunk(otui, `[${label}]`)}` : `[${label}]`;
128559
- text.fg = color;
128560
- };
128561
- return { box, setActive };
128562
- }
128563
- var init_action_button = __esm(() => {
128564
- init_theme();
128565
- init_theme_text();
128566
- });
128567
-
128568
130019
  // src/tui/help-modal.ts
128569
130020
  function tabStripWidth(labels) {
128570
130021
  return labels.reduce((total, label, i) => total + label.length + 2 + (i === 0 ? 0 : 1), 0);
@@ -129110,267 +130561,6 @@ function settingsOpenDecision(state) {
129110
130561
  return { open: true };
129111
130562
  }
129112
130563
 
129113
- // src/tui/queue-nav.ts
129114
- function stepQueueNavIndex(selected, count, direction) {
129115
- if (count <= 0)
129116
- return 0;
129117
- if (direction === "up") {
129118
- return selected > 0 ? selected - 1 : count - 1;
129119
- }
129120
- return selected < count - 1 ? selected + 1 : 0;
129121
- }
129122
- function clampQueueNavIndex(selected, count) {
129123
- if (count <= 0)
129124
- return 0;
129125
- return Math.min(Math.max(selected, 0), count - 1);
129126
- }
129127
- function stepQueueNavAction(selected, direction) {
129128
- const idx = QUEUE_NAV_ACTIONS.indexOf(selected);
129129
- const count = QUEUE_NAV_ACTIONS.length;
129130
- const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
129131
- return QUEUE_NAV_ACTIONS[nextIdx];
129132
- }
129133
- function stepConnectNavAction(selected, direction) {
129134
- const idx = CONNECT_NAV_ACTIONS.indexOf(selected);
129135
- const count = CONNECT_NAV_ACTIONS.length;
129136
- const nextIdx = direction === "left" ? idx > 0 ? idx - 1 : count - 1 : idx < count - 1 ? idx + 1 : 0;
129137
- return CONNECT_NAV_ACTIONS[nextIdx];
129138
- }
129139
- var QUEUE_NAV_ACTIONS, CONNECT_NAV_ACTIONS;
129140
- var init_queue_nav = __esm(() => {
129141
- QUEUE_NAV_ACTIONS = ["force", "edit", "delete"];
129142
- CONNECT_NAV_ACTIONS = ["label", "test", "disconnect"];
129143
- });
129144
-
129145
- // src/tui/settings-modal.ts
129146
- function isSettingsCommand(line) {
129147
- const token = line.trim().split(/\s+/)[0] ?? "";
129148
- return token === SETTINGS_COMMAND;
129149
- }
129150
- function settingsSidebarHint(readOnlyShown) {
129151
- return readOnlyShown ? undefined : SETTINGS_COMMAND;
129152
- }
129153
- function settingsWindow(rows, selected, top, budget) {
129154
- let start = Math.min(top, selected);
129155
- while (start < selected && lineCount(rows, start, selected) > budget)
129156
- start++;
129157
- let end = start;
129158
- while (end + 1 < rows.length && lineCount(rows, start, end + 1) <= budget)
129159
- end++;
129160
- return { top: start, end };
129161
- }
129162
- function openSettings(otui, chrome, options) {
129163
- const core = otui;
129164
- const r = options.renderer;
129165
- const now = options.now ?? Date.now;
129166
- let rows = options.rows;
129167
- let selectedRow = 0;
129168
- let selectedAction = 0;
129169
- let top = 0;
129170
- let armed;
129171
- let busy = false;
129172
- let closed = false;
129173
- let body;
129174
- let budget = Number.MAX_SAFE_INTEGER;
129175
- let blocks = [];
129176
- const activeIndex = (row) => Math.max(0, row?.actions.findIndex((a) => a.active) ?? 0);
129177
- const disarm = () => {
129178
- armed = undefined;
129179
- };
129180
- const blocked = () => closed || options.inputBlocked?.() === true;
129181
- function paint() {
129182
- if (closed || body === undefined)
129183
- return;
129184
- for (const block of blocks) {
129185
- try {
129186
- body.remove(block);
129187
- } catch {}
129188
- }
129189
- blocks = [];
129190
- selectedRow = clampQueueNavIndex(selectedRow, rows.length);
129191
- const win = settingsWindow(rows, selectedRow, top, budget);
129192
- top = win.top;
129193
- const labelWidth = Math.max(...rows.map((row) => row.label.length));
129194
- const valueWidth = Math.max(...rows.map((row) => row.value.length));
129195
- for (let i = win.top;i <= win.end; i++) {
129196
- const row = rows[i];
129197
- const selected = i === selectedRow;
129198
- if (i === win.top || rows[i - 1].group !== row.group) {
129199
- const heading = new core.TextRenderable(r, { id: `st-group-${row.group}`, content: core.t`${boldChunk(core, row.group)}` });
129200
- body.add(heading);
129201
- blocks.push(heading);
129202
- }
129203
- const outer = new core.BoxRenderable(r, {
129204
- id: `st-${row.id}`,
129205
- width: "100%",
129206
- flexDirection: "column",
129207
- ...selected ? { backgroundColor: getTheme().highlight } : {},
129208
- onMouseDown: (event) => {
129209
- event.stopPropagation();
129210
- if (blocked())
129211
- return;
129212
- select(i, activeIndex(row));
129213
- }
129214
- });
129215
- const label = row.label.padEnd(labelWidth);
129216
- const confirming = armed?.id === row.id ? row.actions[armed.action] : undefined;
129217
- const detail = row.detail === undefined ? "" : dimChunk(core, ` ${row.detail}`);
129218
- const armedNote = confirming === undefined ? "" : roleChunk(core, "attention", ` Enter again to confirm ${confirming.label}: it skips confirmation for every action`);
129219
- outer.add(new core.TextRenderable(r, {
129220
- id: `st-${row.id}-line`,
129221
- marginLeft: 2,
129222
- content: core.t`${selected ? boldChunk(core, label) : label} ${roleChunk(core, "ok", row.value.padEnd(valueWidth))} ${dimChunk(core, `[${row.scope}]`)}${detail}${armedNote}`
129223
- }));
129224
- const buttons = new core.BoxRenderable(r, { id: `st-${row.id}-buttons`, width: "100%", flexDirection: "row", marginLeft: 1 });
129225
- if (row.actions.length === 0) {
129226
- buttons.add(new core.TextRenderable(r, { id: `st-${row.id}-hint`, marginLeft: 1, content: core.t`${dimChunk(core, `run ${row.usage}`)}` }));
129227
- }
129228
- row.actions.forEach((action, index) => {
129229
- const color = action.active ? getTheme().ok : action.confirm ? getTheme().error : getTheme().tool;
129230
- const button = smallActionButton(core, r, action.active ? `\u2713 ${action.label}` : action.label, `st-${row.id}-${index}`, color, () => {
129231
- if (blocked())
129232
- return;
129233
- select(i, index);
129234
- press();
129235
- });
129236
- button.setActive(selected && index === selectedAction);
129237
- buttons.add(button.box);
129238
- });
129239
- outer.add(buttons);
129240
- body.add(outer);
129241
- blocks.push(outer);
129242
- }
129243
- }
129244
- function select(row, action) {
129245
- if (row !== selectedRow || action !== selectedAction)
129246
- disarm();
129247
- selectedRow = row;
129248
- selectedAction = action;
129249
- paint();
129250
- }
129251
- const report = (what, error) => {
129252
- options.onError?.(`/settings: ${what}: ${error instanceof Error ? error.message : String(error)}
129253
- `);
129254
- };
129255
- async function press(viaKey = false) {
129256
- if (blocked())
129257
- return;
129258
- const row = rows[selectedRow];
129259
- const action = row?.actions[selectedAction];
129260
- if (row === undefined || action === undefined || busy)
129261
- return;
129262
- if (action.confirm) {
129263
- if (armed?.id !== row.id || armed.action !== selectedAction) {
129264
- armed = { id: row.id, action: selectedAction, at: now() };
129265
- paint();
129266
- return;
129267
- }
129268
- if (viaKey && now() - armed.at < CONFIRM_MIN_GAP_MS)
129269
- return;
129270
- }
129271
- disarm();
129272
- busy = true;
129273
- try {
129274
- try {
129275
- await options.run(action.command);
129276
- } catch (error) {
129277
- report(action.command, error);
129278
- }
129279
- try {
129280
- rows = await options.load();
129281
- } catch (error) {
129282
- report("could not reload the rows", error);
129283
- }
129284
- } finally {
129285
- busy = false;
129286
- }
129287
- const at = rows.findIndex((candidate) => candidate.id === row.id);
129288
- if (at >= 0)
129289
- selectedRow = at;
129290
- selectedAction = clampQueueNavIndex(selectedAction, rows[selectedRow]?.actions.length ?? 0);
129291
- paint();
129292
- }
129293
- const swallow = (key) => {
129294
- key.preventDefault?.();
129295
- key.stopPropagation?.();
129296
- };
129297
- const unsubscribeKeys = options.onKeypress((key) => {
129298
- if (blocked())
129299
- return;
129300
- if (key.name === "escape") {
129301
- if (armed === undefined)
129302
- return;
129303
- disarm();
129304
- paint();
129305
- swallow(key);
129306
- return;
129307
- }
129308
- if (busy)
129309
- return;
129310
- const row = rows[selectedRow];
129311
- if (key.name === "up" || key.name === "down") {
129312
- const next = stepQueueNavIndex(selectedRow, rows.length, key.name);
129313
- select(next, activeIndex(rows[next]));
129314
- } else if (key.name === "left" || key.name === "right") {
129315
- const count = row?.actions.length ?? 0;
129316
- if (count > 0)
129317
- select(selectedRow, stepQueueNavIndex(selectedAction, count, key.name === "left" ? "up" : "down"));
129318
- } else if (key.name === "return" || key.name === "linefeed" || key.name === "kpenter") {
129319
- press(true);
129320
- } else {
129321
- return;
129322
- }
129323
- swallow(key);
129324
- });
129325
- let unsubscribeTheme = () => {};
129326
- const groups = new Set(rows.map((row) => row.group)).size;
129327
- selectedAction = activeIndex(rows[0]);
129328
- const handle = openModal(core, chrome, {
129329
- title: SETTINGS_COMMAND,
129330
- tabs: [{ id: "settings", label: "Settings" }],
129331
- footer: SETTINGS_FOOTER,
129332
- contentRows: rows.length * 2 + groups,
129333
- renderTab: (_tabId, tabBody, ctx) => {
129334
- body = tabBody;
129335
- budget = ctx.height;
129336
- blocks = [];
129337
- paint();
129338
- },
129339
- onClose: () => {
129340
- closed = true;
129341
- unsubscribeKeys();
129342
- unsubscribeTheme();
129343
- }
129344
- });
129345
- if (handle === undefined) {
129346
- unsubscribeKeys();
129347
- return;
129348
- }
129349
- unsubscribeTheme = onThemeChange(guardedThemeRepaint("settings-modal", paint, () => closed || isRenderableGone(body) || r.isDestroyed === true));
129350
- return handle;
129351
- }
129352
- var SETTINGS_COMMAND = "/settings", CONFIRM_MIN_GAP_MS = 400, SETTINGS_FOOTER, lineCount = (rows, from, to) => {
129353
- let lines = 0;
129354
- for (let i = from;i <= to; i++) {
129355
- lines += 2 + (i === from || rows[i - 1].group !== rows[i].group ? 1 : 0);
129356
- }
129357
- return lines;
129358
- };
129359
- var init_settings_modal = __esm(() => {
129360
- init_action_button();
129361
- init_modal_host();
129362
- init_queue_nav();
129363
- init_theme();
129364
- init_theme_repaint();
129365
- init_theme_text();
129366
- SETTINGS_FOOTER = [
129367
- { key: "\u2191/\u2193", label: "setting" },
129368
- { key: "\u2190/\u2192", label: "value" },
129369
- { key: "Enter", label: "apply" },
129370
- { key: "esc", label: "close" }
129371
- ];
129372
- });
129373
-
129374
130564
  // src/tui/theme-picker.ts
129375
130565
  function isThemeCommand(line) {
129376
130566
  const token = line.trim().split(/\s+/)[0] ?? "";
@@ -130551,7 +131741,7 @@ function booleanOr(value, fallback) {
130551
131741
  return typeof value === "boolean" ? value : fallback;
130552
131742
  }
130553
131743
  var DEFAULT_WIKI_CONFIG;
130554
- var init_config11 = __esm(() => {
131744
+ var init_config12 = __esm(() => {
130555
131745
  init_fs();
130556
131746
  init_json();
130557
131747
  DEFAULT_WIKI_CONFIG = {
@@ -131998,7 +133188,7 @@ var init_enrich2 = __esm(() => {
131998
133188
  init_fs();
131999
133189
  init_shell_config();
132000
133190
  init_collect();
132001
- init_config11();
133191
+ init_config12();
132002
133192
  init_deep_enrich();
132003
133193
  init_service4();
132004
133194
  init_staleness2();
@@ -134220,13 +135410,13 @@ function formatExternalWork(events, options = {}) {
134220
135410
  }
134221
135411
  function formatTranscriptFooter(summary) {
134222
135412
  const parts = [
134223
- plural2(summary.toolCalls, "tool call"),
134224
- `${plural2(summary.turns, "turn")} (derived)`,
134225
- plural2(summary.retries, "retry", "retries"),
135413
+ plural3(summary.toolCalls, "tool call"),
135414
+ `${plural3(summary.turns, "turn")} (derived)`,
135415
+ plural3(summary.retries, "retry", "retries"),
134226
135416
  `cost ${formatCostUnits(summary.costUnits)}`
134227
135417
  ];
134228
135418
  if (summary.operatorMessages > 0)
134229
- parts.push(plural2(summary.operatorMessages, "operator message"));
135419
+ parts.push(plural3(summary.operatorMessages, "operator message"));
134230
135420
  const status = summary.outcome === undefined ? "running" : summary.outcome.kind === "finished" ? "finished" : "failed";
134231
135421
  return `\u2500\u2500 ${status} \xB7 ${parts.join(" \xB7 ")}`;
134232
135422
  }
@@ -134374,7 +135564,7 @@ function describeSkips(skippedLines) {
134374
135564
  return "(not counted)";
134375
135565
  return skippedLines === 0 ? "0" : `${skippedLines} (possible CLI version drift)`;
134376
135566
  }
134377
- function plural2(count, singular, pluralForm) {
135567
+ function plural3(count, singular, pluralForm) {
134378
135568
  return `${count} ${count === 1 ? singular : pluralForm ?? `${singular}s`}`;
134379
135569
  }
134380
135570
  function clip6(value, width) {
@@ -137985,7 +139175,7 @@ import http from "http";
137985
139175
  import https from "https";
137986
139176
  import net2 from "net";
137987
139177
  import dns from "dns";
137988
- import { unlinkSync as unlinkSync7 } from "fs";
139178
+ import { unlinkSync as unlinkSync8 } from "fs";
137989
139179
  function normalizeHost(host) {
137990
139180
  return host.toLowerCase().replace(/\.$/, "");
137991
139181
  }
@@ -138298,7 +139488,7 @@ async function createAllowlistProxy(opts) {
138298
139488
  });
138299
139489
  if (opts.unixSocketPath !== undefined) {
138300
139490
  try {
138301
- unlinkSync7(opts.unixSocketPath);
139491
+ unlinkSync8(opts.unixSocketPath);
138302
139492
  } catch {}
138303
139493
  await new Promise((resolve) => server.listen(opts.unixSocketPath, () => resolve()));
138304
139494
  } else {
@@ -142171,6 +143361,8 @@ async function launchTuiAgentShell(opts) {
142171
143361
  let liveReviewsPanel;
142172
143362
  let remoteBridge;
142173
143363
  let liveRemotePanel;
143364
+ let liveChannelsPanel;
143365
+ const channelsClient = new ChannelsClient;
142174
143366
  let liveApprovals;
142175
143367
  let liveExternalDiff;
142176
143368
  let detachExternal;
@@ -142199,6 +143391,7 @@ async function launchTuiAgentShell(opts) {
142199
143391
  liveSchedules?.dispose();
142200
143392
  liveReviewsPanel?.dispose();
142201
143393
  liveRemotePanel?.dispose();
143394
+ liveChannelsPanel?.dispose();
142202
143395
  remoteBridge?.disable();
142203
143396
  liveApprovals?.dispose();
142204
143397
  liveExternalDiff?.dispose();
@@ -142498,6 +143691,11 @@ async function launchTuiAgentShell(opts) {
142498
143691
  getStatus: remoteStatus,
142499
143692
  onOpen: () => showRemoteControl()
142500
143693
  });
143694
+ liveChannelsPanel = mountChannelsPanel(otui, r, sidebar, {
143695
+ width: SIDEBAR_TEXT_WIDTH,
143696
+ load: () => loadChannelsSnapshot(channelsClient),
143697
+ onOpen: () => showChannels()
143698
+ });
142501
143699
  const approvals = mountApprovalsSidebar({
142502
143700
  otui,
142503
143701
  chrome,
@@ -143942,6 +145140,28 @@ Staying in the current session.
143942
145140
  enableRemoteControl(request.name);
143943
145141
  }
143944
145142
  };
145143
+ const showChannels = () => {
145144
+ openChannels(otui, chrome, {
145145
+ client: channelsClient,
145146
+ renderer: r,
145147
+ ...inspectorKeys,
145148
+ onPaste: (handler) => {
145149
+ r._internalKeyInput.onInternal("paste", handler);
145150
+ return () => r._internalKeyInput.offInternal("paste", handler);
145151
+ },
145152
+ inputBlocked: () => chrome.keyboardOwnedElsewhere(),
145153
+ onChanged: (next) => liveChannelsPanel?.update(next)
145154
+ });
145155
+ };
145156
+ const runChannelsCommand = (line) => {
145157
+ const request = parseChannelsArgs(line);
145158
+ if (request.action === "invalid") {
145159
+ io.onSystem?.(`${request.message}
145160
+ `);
145161
+ return;
145162
+ }
145163
+ showChannels();
145164
+ };
143945
145165
  const showGame = (line) => {
143946
145166
  const timeoutMatch = /\/game\s+(\d+)/.exec(line);
143947
145167
  openGamesModal(otui, chrome, {
@@ -145030,6 +146250,10 @@ ${formatThemeList(getThemeId())}`);
145030
146250
  runRemoteControlCommand(line);
145031
146251
  return;
145032
146252
  }
146253
+ case "channels": {
146254
+ runChannelsCommand(line);
146255
+ return;
146256
+ }
145033
146257
  case "schedules": {
145034
146258
  routeSchedulesCommand(line, true, schedules);
145035
146259
  return;
@@ -145352,6 +146576,10 @@ ${formatThemeList(getThemeId())}`);
145352
146576
  runRemoteControlCommand(line);
145353
146577
  return;
145354
146578
  }
146579
+ if (isChannelsCommand(command.name)) {
146580
+ runChannelsCommand(line);
146581
+ return;
146582
+ }
145355
146583
  if (routeSchedulesCommand(line, false, schedules)) {
145356
146584
  return;
145357
146585
  }
@@ -146160,6 +147388,8 @@ var init_tui_shell = __esm(() => {
146160
147388
  init_product_open_surface();
146161
147389
  init_reviews_inspector();
146162
147390
  init_shell_bridge();
147391
+ init_channels_client();
147392
+ init_channels_surface();
146163
147393
  init_remote_control_surface();
146164
147394
  init_flow_check_ac();
146165
147395
  init_ops_sidebar();
@@ -146708,7 +147938,7 @@ var init_chat_shell = __esm(() => {
146708
147938
 
146709
147939
  // src/tui/debug-watcher.ts
146710
147940
  import { spawn as spawn8, spawnSync as spawnSync7 } from "child_process";
146711
- import { closeSync as closeSync7, constants as constants4, openSync as openSync8, readdirSync as readdirSync11, readFileSync as readFileSync24, readlinkSync as readlinkSync3, statSync as statSync17 } from "fs";
147941
+ import { closeSync as closeSync7, constants as constants4, openSync as openSync8, readdirSync as readdirSync11, readFileSync as readFileSync24, readlinkSync as readlinkSync3, statSync as statSync18 } from "fs";
146712
147942
  import path284 from "path";
146713
147943
  function parseEpollFdinfo(text) {
146714
147944
  const out = [];
@@ -146956,7 +148186,7 @@ async function runDebugWatcher(argv) {
146956
148186
  sample.pending = pendingOf(ttyFd);
146957
148187
  }
146958
148188
  try {
146959
- sample.heartbeatAgeMs = Date.now() - statSync17(shellLog).mtimeMs;
148189
+ sample.heartbeatAgeMs = Date.now() - statSync18(shellLog).mtimeMs;
146960
148190
  } catch {
146961
148191
  sample.heartbeatAgeMs = undefined;
146962
148192
  }
@@ -148764,6 +149994,8 @@ ${GUTTER}${turnSeparator()}
148764
149994
  }
148765
149995
  } else if (isRemoteControlCommand(command)) {
148766
149996
  agentIo.onSystem?.(readlineRemoteControlText(line));
149997
+ } else if (isChannelsCommand(command)) {
149998
+ agentIo.onSystem?.(readlineChannelsText(line, await loadChannelsSnapshot(new ChannelsClient)));
148767
149999
  } else if (isSessionInfoCommand(command)) {
148768
150000
  const cwd = sessionCwd;
148769
150001
  const [workspaces, flows] = await Promise.all([
@@ -149900,6 +151132,8 @@ var init_shell = __esm(() => {
149900
151132
  init_metrics();
149901
151133
  init_reviews_inspector();
149902
151134
  init_remote_control_surface();
151135
+ init_channels_client();
151136
+ init_channels_surface();
149903
151137
  init_session_info();
149904
151138
  init_model_limits();
149905
151139
  init_shell_config();
@@ -149962,6 +151196,7 @@ var init_shell = __esm(() => {
149962
151196
  "/setup",
149963
151197
  "/reviews",
149964
151198
  "/remote-control",
151199
+ "/channels",
149965
151200
  "/theme",
149966
151201
  "/settings",
149967
151202
  "/mode",
@@ -158100,7 +159335,7 @@ var init_authoring_lint = __esm(() => {
158100
159335
 
158101
159336
  // src/gdskills/governance/stocktake.ts
158102
159337
  import { createHash as createHash68 } from "crypto";
158103
- import { existsSync as existsSync54, readdirSync as readdirSync13, readFileSync as readFileSync26, statSync as statSync18 } from "fs";
159338
+ import { existsSync as existsSync54, readdirSync as readdirSync13, readFileSync as readFileSync26, statSync as statSync19 } from "fs";
158104
159339
  import path306 from "path";
158105
159340
  function cacheKeyMatches(cached, inputs, quick) {
158106
159341
  return cached.algorithmVersion === ALGORITHM_VERSION && cached.quick === quick && cached.sha256 === inputs.sha256 && cached.catalogFingerprint === inputs.catalogFingerprint && cached.verificationReportHash === inputs.verificationReportHash && cached.referencesHash === inputs.referencesHash;
@@ -158129,7 +159364,7 @@ function referencesHash(entry) {
158129
159364
  for (const name of readdirSync13(dir).sort()) {
158130
159365
  const full = path306.join(dir, name);
158131
159366
  const rel = relative.length > 0 ? `${relative}/${name}` : name;
158132
- if (statSync18(full).isDirectory()) {
159367
+ if (statSync19(full).isDirectory()) {
158133
159368
  walk(full, rel);
158134
159369
  } else {
158135
159370
  files.push(rel);
@@ -175788,7 +177023,7 @@ async function dispatchReadResource(ctx, uri) {
175788
177023
  }
175789
177024
  }
175790
177025
  var init_dispatch3 = __esm(() => {
175791
- init_config10();
177026
+ init_config11();
175792
177027
  init_discovery();
175793
177028
  init_tools3();
175794
177029
  init_resources();
@@ -183277,7 +184512,14 @@ var init_serve_server = __esm(() => {
183277
184512
  ["reply", "POST"],
183278
184513
  ["approval", "POST"],
183279
184514
  ["ack", "POST"],
183280
- ["stream", "GET"]
184515
+ ["stream", "GET"],
184516
+ ["channels-status", "GET"],
184517
+ ["channels-pair", "POST"],
184518
+ ["channels-pairing", "GET"],
184519
+ ["channels-cancel", "POST"],
184520
+ ["channels-reload", "POST"],
184521
+ ["channels-test", "POST"],
184522
+ ["channels-disconnect", "POST"]
183281
184523
  ]);
183282
184524
  JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
183283
184525
  });
@@ -183309,6 +184551,68 @@ var init_serve_runner = __esm(() => {
183309
184551
  init_serve_turn();
183310
184552
  });
183311
184553
 
184554
+ // src/remote/naming.ts
184555
+ import { createHash as createHash77 } from "crypto";
184556
+ import { hostname as hostname5 } from "os";
184557
+ import path348 from "path";
184558
+ function nameKey(name) {
184559
+ return name.normalize("NFC").trim().toLowerCase();
184560
+ }
184561
+ function checkName(raw) {
184562
+ const name = raw.normalize("NFC").replace(/\s+/g, " ").trim();
184563
+ if (name.length === 0) {
184564
+ return { ok: false, reason: "the topic name is empty" };
184565
+ }
184566
+ if (name.length > MAX_TOPIC_NAME_LENGTH) {
184567
+ return { ok: false, reason: `the topic name is longer than ${MAX_TOPIC_NAME_LENGTH} characters` };
184568
+ }
184569
+ if (/[\u0000-\u001f\u007f]/.test(name)) {
184570
+ return { ok: false, reason: "the topic name contains control characters" };
184571
+ }
184572
+ return { ok: true, name };
184573
+ }
184574
+ function projectSlug(project) {
184575
+ const base = path348.basename(project.replace(/[\\/]+$/, "")) || project;
184576
+ const slug = base.normalize("NFKD").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, MAX_PROJECT_SLUG);
184577
+ return slug.length > 0 ? slug : "project";
184578
+ }
184579
+ function idChars(sessionId) {
184580
+ const chars = sessionId.replace(/[^A-Za-z0-9]/g, "");
184581
+ return chars.length > 0 ? chars : "session";
184582
+ }
184583
+ function machineSlug(machine) {
184584
+ const slug = machine.normalize("NFKD").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, MAX_MACHINE_SLUG);
184585
+ return slug.length > 0 ? slug : `host-${createHash77("sha256").update(machine.normalize("NFC")).digest("hex").slice(0, 6)}`;
184586
+ }
184587
+ function localMachineName(read = hostname5) {
184588
+ let name = "";
184589
+ try {
184590
+ name = read().trim();
184591
+ } catch {}
184592
+ return name.length > 0 ? name : "this-machine";
184593
+ }
184594
+ function defaultName(project, sessionId, length = SHORT_ID_START, machine) {
184595
+ const base = `${projectSlug(project)}-${idChars(sessionId).slice(0, length)}`;
184596
+ return machine === undefined ? base : `${machineSlug(machine)}-${base}`;
184597
+ }
184598
+ function* defaultNameCandidates(project, sessionId, machine) {
184599
+ const total = idChars(sessionId).length;
184600
+ const seen = new Set;
184601
+ for (let length = SHORT_ID_START;; length += 4) {
184602
+ const bounded = Math.min(length, total);
184603
+ const name = defaultName(project, sessionId, bounded, machine);
184604
+ if (!seen.has(name)) {
184605
+ seen.add(name);
184606
+ yield name;
184607
+ }
184608
+ if (bounded >= total) {
184609
+ return;
184610
+ }
184611
+ }
184612
+ }
184613
+ var MAX_TOPIC_NAME_LENGTH = 128, MAX_PROJECT_SLUG = 40, MAX_MACHINE_SLUG = 24, SHORT_ID_START = 8;
184614
+ var init_naming = () => {};
184615
+
183312
184616
  // src/remote/types.ts
183313
184617
  function isBotApiError(value) {
183314
184618
  return value instanceof BotApiError;
@@ -183420,7 +184724,7 @@ function createHttpBotApi(options) {
183420
184724
  const timeoutSec = params.timeoutSec ?? 0;
183421
184725
  const body = {
183422
184726
  timeout: timeoutSec,
183423
- allowed_updates: ["message", "callback_query"]
184727
+ allowed_updates: ["message", "callback_query", "my_chat_member"]
183424
184728
  };
183425
184729
  if (params.offset !== undefined) {
183426
184730
  body.offset = params.offset;
@@ -183463,6 +184767,15 @@ function createHttpBotApi(options) {
183463
184767
  body.text = params.text;
183464
184768
  }
183465
184769
  await call("answerCallbackQuery", body);
184770
+ },
184771
+ async getMe() {
184772
+ return call("getMe", {});
184773
+ },
184774
+ async getChat(params) {
184775
+ return call("getChat", { chat_id: params.chatId });
184776
+ },
184777
+ async getChatMember(params) {
184778
+ return call("getChatMember", { chat_id: params.chatId, user_id: params.userId });
183466
184779
  }
183467
184780
  };
183468
184781
  }
@@ -183472,123 +184785,6 @@ var init_bot_api_http = __esm(() => {
183472
184785
  init_types11();
183473
184786
  });
183474
184787
 
183475
- // src/remote/config.ts
183476
- import { statSync as statSync19 } from "fs";
183477
- function isSafeInt(value) {
183478
- return typeof value === "number" && Number.isSafeInteger(value);
183479
- }
183480
- function parseRemoteConfig(value) {
183481
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
183482
- return { ok: false, reason: "remote config must be a JSON object" };
183483
- }
183484
- const doc = value;
183485
- for (const key of Object.keys(doc)) {
183486
- if (!CONFIG_KEYS2.has(key)) {
183487
- const shown = /^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(key) ? key : "<not shown>";
183488
- return { ok: false, reason: `remote config has an unknown key "${shown}" (the schema is closed)` };
183489
- }
183490
- }
183491
- if (doc.schemaVersion !== REMOTE_CONFIG_SCHEMA_VERSION) {
183492
- return { ok: false, reason: `remote config schemaVersion must be ${REMOTE_CONFIG_SCHEMA_VERSION}` };
183493
- }
183494
- if (!isSafeInt(doc.chatId) || doc.chatId === 0) {
183495
- return { ok: false, reason: "remote config chatId must be a non-zero integer (the supergroup id)" };
183496
- }
183497
- const ids = doc.allowedUserIds;
183498
- if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isSafeInt(id) && id > 0)) {
183499
- return { ok: false, reason: "remote config allowedUserIds must be a non-empty array of positive integers" };
183500
- }
183501
- const timeout = (key, fallback) => {
183502
- const raw = doc[key];
183503
- if (raw === undefined) {
183504
- return { ok: true, value: fallback };
183505
- }
183506
- if (!isSafeInt(raw) || raw < 1 || raw > MAX_TIMEOUT_MS3) {
183507
- return { ok: false, reason: `remote config ${key} must be an integer from 1 to ${MAX_TIMEOUT_MS3} (milliseconds)` };
183508
- }
183509
- return { ok: true, value: raw };
183510
- };
183511
- const orphan = timeout("orphanMs", DEFAULT_ORPHAN_MS);
183512
- if (!orphan.ok) {
183513
- return orphan;
183514
- }
183515
- const run = timeout("runTimeoutMs", DEFAULT_RUN_TIMEOUT_MS);
183516
- if (!run.ok) {
183517
- return run;
183518
- }
183519
- return {
183520
- ok: true,
183521
- value: {
183522
- schemaVersion: REMOTE_CONFIG_SCHEMA_VERSION,
183523
- chatId: doc.chatId,
183524
- allowedUserIds: [...new Set(ids)],
183525
- orphanMs: orphan.value,
183526
- runTimeoutMs: run.value
183527
- }
183528
- };
183529
- }
183530
- function loadRemoteConfig(dir) {
183531
- const file = remoteConfigPath(dir);
183532
- const read = readConfigFile(file);
183533
- if (!read.ok) {
183534
- return {
183535
- ok: false,
183536
- reason: read.reason === "absent" ? `no remote-control config at ${file}; create it with chatId and allowedUserIds` : `remote-control config at ${file} cannot be read (${read.reason})`
183537
- };
183538
- }
183539
- let parsed;
183540
- try {
183541
- parsed = JSON.parse(read.text);
183542
- } catch {
183543
- return { ok: false, reason: `remote-control config at ${file} is not valid JSON` };
183544
- }
183545
- return parseRemoteConfig(parsed);
183546
- }
183547
- function loadBotToken(dir) {
183548
- const file = botTokenPath(dir);
183549
- let mode;
183550
- try {
183551
- const stats = statSync19(file);
183552
- if (!stats.isFile()) {
183553
- return { ok: false, reason: `bot token path ${file} is not a regular file` };
183554
- }
183555
- mode = stats.mode & 511;
183556
- } catch {
183557
- return {
183558
- ok: false,
183559
- reason: `no bot token file at ${file}; put the token on one line in it and run: chmod 600 ${file}`
183560
- };
183561
- }
183562
- if (process.platform !== "win32" && (mode & 63) !== 0) {
183563
- return {
183564
- ok: false,
183565
- reason: `bot token file ${file} is accessible to other users (mode ${mode.toString(8)}); run: chmod 600 ${file}`
183566
- };
183567
- }
183568
- const read = readConfigFile(file);
183569
- if (!read.ok) {
183570
- return { ok: false, reason: `bot token file ${file} cannot be read (${read.reason})` };
183571
- }
183572
- const token = read.text.trim();
183573
- if (!TOKEN_SHAPE2.test(token)) {
183574
- return {
183575
- ok: false,
183576
- reason: `bot token file ${file} does not hold a bot token (expected <digits>:<secret> on a single line)`
183577
- };
183578
- }
183579
- return { ok: true, value: token };
183580
- }
183581
- var REMOTE_CONFIG_SCHEMA_VERSION = 1, DEFAULT_ORPHAN_MS, DEFAULT_RUN_TIMEOUT_MS, MAX_TIMEOUT_MS3, CONFIG_KEYS2, TOKEN_SHAPE2;
183582
- var init_config12 = __esm(() => {
183583
- init_config_dir();
183584
- init_paths4();
183585
- DEFAULT_ORPHAN_MS = 10 * 60000;
183586
- DEFAULT_RUN_TIMEOUT_MS = 30 * 60000;
183587
- MAX_TIMEOUT_MS3 = 7 * 24 * 60 * 60000;
183588
- CONFIG_KEYS2 = new Set(["schemaVersion", "chatId", "allowedUserIds", "orphanMs", "runTimeoutMs"]);
183589
- TOKEN_SHAPE2 = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
183590
- });
183591
-
183592
184788
  // src/remote/durable-log.ts
183593
184789
  import { renameSync as renameSync6 } from "fs";
183594
184790
 
@@ -183730,9 +184926,9 @@ var init_durable_log = __esm(() => {
183730
184926
  });
183731
184927
 
183732
184928
  // src/remote/inbound.ts
183733
- import { createHash as createHash77 } from "crypto";
183734
- import { unlinkSync as unlinkSync8 } from "fs";
183735
- import path348 from "path";
184929
+ import { createHash as createHash78 } from "crypto";
184930
+ import { unlinkSync as unlinkSync9 } from "fs";
184931
+ import path349 from "path";
183736
184932
  function parseEntry4(value) {
183737
184933
  if (typeof value !== "object" || value === null) {
183738
184934
  return;
@@ -183763,8 +184959,8 @@ class InboundQueues {
183763
184959
  this.directory = ensureRemoteDir(this.dir, INBOUND_DIRNAME);
183764
184960
  }
183765
184961
  fileFor(bindingKey) {
183766
- const name = createHash77("sha256").update(bindingKey).digest("hex").slice(0, 32);
183767
- return path348.join(this.directory, `${name}.jsonl`);
184962
+ const name = createHash78("sha256").update(bindingKey).digest("hex").slice(0, 32);
184963
+ return path349.join(this.directory, `${name}.jsonl`);
183768
184964
  }
183769
184965
  log(bindingKey) {
183770
184966
  let log = this.logs.get(bindingKey);
@@ -183791,7 +184987,7 @@ class InboundQueues {
183791
184987
  remove(bindingKey) {
183792
184988
  this.logs.delete(bindingKey);
183793
184989
  try {
183794
- unlinkSync8(this.fileFor(bindingKey));
184990
+ unlinkSync9(this.fileFor(bindingKey));
183795
184991
  } catch {}
183796
184992
  }
183797
184993
  }
@@ -183801,7 +184997,7 @@ class PollerState {
183801
184997
  ids = new Set;
183802
184998
  top = 0;
183803
184999
  constructor(options) {
183804
- this.file = path348.join(ensureRemoteDir(options.dir), POLLER_STATE_FILE);
185000
+ this.file = path349.join(ensureRemoteDir(options.dir), POLLER_STATE_FILE);
183805
185001
  }
183806
185002
  load() {
183807
185003
  this.ids.clear();
@@ -183872,13 +185068,13 @@ var init_inbound = __esm(() => {
183872
185068
 
183873
185069
  // src/remote/journal.ts
183874
185070
  import { renameSync as renameSync7, statSync as statSync20 } from "fs";
183875
- import path349 from "path";
185071
+ import path350 from "path";
183876
185072
 
183877
185073
  class RejectedJournal {
183878
185074
  file;
183879
185075
  now;
183880
185076
  constructor(options) {
183881
- this.file = path349.join(ensureRemoteDir(options.dir), REJECTED_JOURNAL_FILE);
185077
+ this.file = path350.join(ensureRemoteDir(options.dir), REJECTED_JOURNAL_FILE);
183882
185078
  this.now = options.now;
183883
185079
  }
183884
185080
  get path() {
@@ -183903,54 +185099,6 @@ var init_journal2 = __esm(() => {
183903
185099
  init_paths4();
183904
185100
  });
183905
185101
 
183906
- // src/remote/naming.ts
183907
- import path350 from "path";
183908
- function nameKey(name) {
183909
- return name.normalize("NFC").trim().toLowerCase();
183910
- }
183911
- function checkName(raw) {
183912
- const name = raw.normalize("NFC").replace(/\s+/g, " ").trim();
183913
- if (name.length === 0) {
183914
- return { ok: false, reason: "the topic name is empty" };
183915
- }
183916
- if (name.length > MAX_TOPIC_NAME_LENGTH) {
183917
- return { ok: false, reason: `the topic name is longer than ${MAX_TOPIC_NAME_LENGTH} characters` };
183918
- }
183919
- if (/[\u0000-\u001f\u007f]/.test(name)) {
183920
- return { ok: false, reason: "the topic name contains control characters" };
183921
- }
183922
- return { ok: true, name };
183923
- }
183924
- function projectSlug(project) {
183925
- const base = path350.basename(project.replace(/[\\/]+$/, "")) || project;
183926
- const slug = base.normalize("NFKD").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, MAX_PROJECT_SLUG);
183927
- return slug.length > 0 ? slug : "project";
183928
- }
183929
- function idChars(sessionId) {
183930
- const chars = sessionId.replace(/[^A-Za-z0-9]/g, "");
183931
- return chars.length > 0 ? chars : "session";
183932
- }
183933
- function defaultName(project, sessionId, length = SHORT_ID_START) {
183934
- return `${projectSlug(project)}-${idChars(sessionId).slice(0, length)}`;
183935
- }
183936
- function* defaultNameCandidates(project, sessionId) {
183937
- const total = idChars(sessionId).length;
183938
- const seen = new Set;
183939
- for (let length = SHORT_ID_START;; length += 4) {
183940
- const bounded = Math.min(length, total);
183941
- const name = defaultName(project, sessionId, bounded);
183942
- if (!seen.has(name)) {
183943
- seen.add(name);
183944
- yield name;
183945
- }
183946
- if (bounded >= total) {
183947
- return;
183948
- }
183949
- }
183950
- }
183951
- var MAX_TOPIC_NAME_LENGTH = 128, MAX_PROJECT_SLUG = 40, SHORT_ID_START = 8;
183952
- var init_naming = () => {};
183953
-
183954
185102
  // src/remote/format.ts
183955
185103
  function openingFence(line) {
183956
185104
  const match = FENCE_LINE2.exec(line);
@@ -184464,6 +185612,9 @@ var init_registry6 = __esm(() => {
184464
185612
  function describeError4(error) {
184465
185613
  return redactSensitiveText(error instanceof Error ? error.message : String(error));
184466
185614
  }
185615
+ function isThreadGone(error) {
185616
+ return error.kind === "rejected" && /thread not found|topic_id_invalid|topic.*not found/i.test(error.message);
185617
+ }
184467
185618
  function formatDuration(ms) {
184468
185619
  if (ms % 60000 === 0) {
184469
185620
  const minutes = ms / 60000;
@@ -184476,6 +185627,7 @@ function formatDuration(ms) {
184476
185627
  class RemoteHub {
184477
185628
  api;
184478
185629
  config;
185630
+ machine;
184479
185631
  consumer;
184480
185632
  now;
184481
185633
  timers;
@@ -184500,6 +185652,7 @@ class RemoteHub {
184500
185652
  constructor(options) {
184501
185653
  this.api = options.api;
184502
185654
  this.config = options.config;
185655
+ this.machine = options.machine;
184503
185656
  this.consumer = { deliver: options.deliver, ...options.deliverCallback === undefined ? {} : { deliverCallback: options.deliverCallback } };
184504
185657
  this.now = options.now ?? Date.now;
184505
185658
  this.timers = options.timers ?? realTimers;
@@ -184624,6 +185777,19 @@ class RemoteHub {
184624
185777
  return { ok: true, existed: true };
184625
185778
  });
184626
185779
  }
185780
+ deleteAllTopics() {
185781
+ return this.serial(async () => {
185782
+ const records = this.registry.records();
185783
+ for (const record of records) {
185784
+ await this.retire(record, "channel disconnected", true);
185785
+ }
185786
+ const remaining = this.registry.records().length;
185787
+ return { deleted: records.length - remaining, remaining };
185788
+ });
185789
+ }
185790
+ async sendGeneral(text) {
185791
+ await this.api.sendMessage({ chatId: this.config.chatId, text });
185792
+ }
184627
185793
  heartbeat(sessionId) {
184628
185794
  return this.serial(async () => {
184629
185795
  const record = this.registry.bySession(sessionId);
@@ -184792,7 +185958,7 @@ class RemoteHub {
184792
185958
  name = existing.name;
184793
185959
  } else {
184794
185960
  let chosen;
184795
- for (const candidate of defaultNameCandidates(input.project, input.sessionId)) {
185961
+ for (const candidate of defaultNameCandidates(input.project, input.sessionId, this.machine)) {
184796
185962
  const holder = this.registry.byNameKey(candidate);
184797
185963
  if (holder === undefined || holder.sessionId === input.sessionId || !isLive(holder, now)) {
184798
185964
  chosen = candidate;
@@ -184849,12 +186015,13 @@ class RemoteHub {
184849
186015
  await this.flushOutbound();
184850
186016
  return { ok: true, name, threadId: record.threadId, reused: false };
184851
186017
  }
184852
- async retire(record, why) {
186018
+ async retire(record, why, strict = false) {
184853
186019
  this.outbound.discardForThread(record.chatId, record.threadId);
184854
186020
  try {
184855
186021
  await this.api.deleteForumTopic({ chatId: record.chatId, messageThreadId: record.threadId });
184856
186022
  } catch (error) {
184857
- if (isBotApiError(error) && isRetryable(error)) {
186023
+ const stillThere = strict && isBotApiError(error) && !isThreadGone(error);
186024
+ if (isBotApiError(error) && (isRetryable(error) || stillThere)) {
184858
186025
  record.sessionId = "";
184859
186026
  record.status = "unavailable";
184860
186027
  record.unavailableSince = this.now() - this.config.orphanMs;
@@ -184871,6 +186038,9 @@ class RemoteHub {
184871
186038
  route(update) {
184872
186039
  const message = update.message;
184873
186040
  const query = update.callback_query;
186041
+ if (message === undefined && query === undefined) {
186042
+ return;
186043
+ }
184874
186044
  const fromId = message?.from?.id ?? query?.from.id;
184875
186045
  if (fromId === undefined || !this.config.allowedUserIds.includes(fromId)) {
184876
186046
  this.journal.record(fromId);
@@ -184985,151 +186155,43 @@ var init_hub = __esm(() => {
184985
186155
  });
184986
186156
 
184987
186157
  // src/remote/bootstrap.ts
186158
+ function openBotApi(options) {
186159
+ if (options.api !== undefined) {
186160
+ return { ok: true, api: options.api };
186161
+ }
186162
+ const token = loadBotToken(options.dir);
186163
+ if (!token.ok) {
186164
+ return { ok: false, reason: token.reason };
186165
+ }
186166
+ return {
186167
+ ok: true,
186168
+ api: createHttpBotApi({
186169
+ token: token.value,
186170
+ ...options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl },
186171
+ ...options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl }
186172
+ })
186173
+ };
186174
+ }
184988
186175
  function openRemoteHub(options) {
184989
186176
  const config = loadRemoteConfig(options.dir);
184990
186177
  if (!config.ok) {
184991
186178
  return { ok: false, reason: config.reason };
184992
186179
  }
184993
- let api = options.api;
184994
- if (api === undefined) {
184995
- const token = loadBotToken(options.dir);
184996
- if (!token.ok) {
184997
- return { ok: false, reason: token.reason };
184998
- }
184999
- api = createHttpBotApi({
185000
- token: token.value,
185001
- ...options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl },
185002
- ...options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl }
185003
- });
186180
+ const opened = openBotApi(options);
186181
+ if (!opened.ok) {
186182
+ return { ok: false, reason: opened.reason };
185004
186183
  }
185005
186184
  const { api: _api, fetchImpl: _fetch, baseUrl: _base, ...rest } = options;
185006
- return { ok: true, hub: new RemoteHub({ ...rest, api, config: config.value }) };
186185
+ return { ok: true, hub: new RemoteHub({ ...rest, api: opened.api, config: config.value }) };
185007
186186
  }
185008
186187
  var init_bootstrap2 = __esm(() => {
185009
186188
  init_bot_api_http();
185010
- init_config12();
186189
+ init_config10();
185011
186190
  init_hub();
185012
186191
  });
185013
186192
 
185014
- // src/remote/poll-lock.ts
185015
- import { randomBytes as randomBytes13 } from "crypto";
185016
- import { linkSync, renameSync as renameSync8, statSync as statSync21, unlinkSync as unlinkSync9 } from "fs";
185017
- import path353 from "path";
185018
- function pollerLockPath(dir) {
185019
- return path353.join(remoteDirPath(dir), POLLER_LOCK_FILE);
185020
- }
185021
- function processIsAlive3(pid) {
185022
- try {
185023
- process.kill(pid, 0);
185024
- return true;
185025
- } catch (error) {
185026
- return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
185027
- }
185028
- }
185029
- function parsePid(text) {
185030
- const pid = Number.parseInt(text.trim(), 10);
185031
- return Number.isInteger(pid) && pid > 0 ? pid : Number.NaN;
185032
- }
185033
- function refusal4(holder) {
185034
- return {
185035
- ok: false,
185036
- holderPid: holder,
185037
- reason: `another keryx serve (pid ${holder}) already polls this bot token on this machine; this one will not poll. Stop it, or use a different bot.`
185038
- };
185039
- }
185040
- function createWhole(file, pid) {
185041
- const staging = `${file}.${pid}.${randomBytes13(6).toString("hex")}.new`;
185042
- createOwnerOnlyFileExclusive(staging, `${pid}
185043
- `);
185044
- try {
185045
- linkSync(staging, file);
185046
- return true;
185047
- } catch (error) {
185048
- if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
185049
- return false;
185050
- }
185051
- return createOwnerOnlyFileExclusive(file, `${pid}
185052
- `);
185053
- } finally {
185054
- try {
185055
- unlinkSync9(staging);
185056
- } catch {}
185057
- }
185058
- }
185059
- function acquirePollLock(options = {}) {
185060
- ensureRemoteDir(options.dir);
185061
- const file = pollerLockPath(options.dir);
185062
- const pid = options.pid ?? process.pid;
185063
- const isAlive = options.isAlive ?? processIsAlive3;
185064
- const now = options.now ?? Date.now;
185065
- for (let attempt = 0;attempt < 4; attempt += 1) {
185066
- if (createWhole(file, pid)) {
185067
- return {
185068
- ok: true,
185069
- release() {
185070
- const current = readConfigFile(file);
185071
- if (current.ok && parsePid(current.text) === pid) {
185072
- try {
185073
- unlinkSync9(file);
185074
- } catch {}
185075
- }
185076
- }
185077
- };
185078
- }
185079
- const held = readConfigFile(file);
185080
- if (!held.ok && held.reason === "absent") {
185081
- continue;
185082
- }
185083
- const holder = held.ok ? parsePid(held.text) : Number.NaN;
185084
- if (Number.isInteger(holder) && isAlive(holder)) {
185085
- return refusal4(holder);
185086
- }
185087
- if (!Number.isInteger(holder)) {
185088
- let youngerThanGrace;
185089
- try {
185090
- youngerThanGrace = now() - statSync21(file).mtimeMs < EMPTY_LOCK_GRACE_MS;
185091
- } catch {
185092
- continue;
185093
- }
185094
- if (youngerThanGrace) {
185095
- return {
185096
- ok: false,
185097
- holderPid: 0,
185098
- reason: `another keryx serve is creating ${file} right now; this one will not poll. Try again in a moment.`
185099
- };
185100
- }
185101
- }
185102
- const aside = `${file}.stale-${pid}-${randomBytes13(6).toString("hex")}`;
185103
- try {
185104
- renameSync8(file, aside);
185105
- } catch {
185106
- continue;
185107
- }
185108
- const moved = readConfigFile(aside);
185109
- const movedPid = moved.ok ? parsePid(moved.text) : Number.NaN;
185110
- if (Number.isInteger(movedPid) && movedPid !== holder && isAlive(movedPid)) {
185111
- try {
185112
- linkSync(aside, file);
185113
- } catch {}
185114
- try {
185115
- unlinkSync9(aside);
185116
- } catch {}
185117
- return refusal4(movedPid);
185118
- }
185119
- try {
185120
- unlinkSync9(aside);
185121
- } catch {}
185122
- }
185123
- return { ok: false, holderPid: 0, reason: `could not take the poller lock at ${file}` };
185124
- }
185125
- var POLLER_LOCK_FILE = "poller.lock", EMPTY_LOCK_GRACE_MS = 5000;
185126
- var init_poll_lock = __esm(() => {
185127
- init_config_dir();
185128
- init_paths4();
185129
- });
185130
-
185131
186193
  // src/remote/http-surface.ts
185132
- import { randomBytes as randomBytes14 } from "crypto";
186194
+ import { randomBytes as randomBytes13 } from "crypto";
185133
186195
  function ok2(body, status = 200) {
185134
186196
  return new Response(`${JSON.stringify({ schemaVersion: REMOTE_SCHEMA_VERSION, ...body })}
185135
186197
  `, { status, headers: JSON_HEADERS2 });
@@ -185237,11 +186299,15 @@ class ShellStream {
185237
186299
  this.onEnded(this);
185238
186300
  }
185239
186301
  }
186302
+ function isChannelsRoute(route) {
186303
+ return Object.hasOwn(CHANNELS_ROUTE_METHODS, route);
186304
+ }
185240
186305
 
185241
186306
  class RemoteHttpSurface {
185242
186307
  options;
185243
186308
  consumer;
185244
186309
  hub;
186310
+ channels;
185245
186311
  unavailableReason = "remote control is not running in this serve";
185246
186312
  closed = false;
185247
186313
  now;
@@ -185261,7 +186327,7 @@ class RemoteHttpSurface {
185261
186327
  this.timers = options.timers ?? realTimers;
185262
186328
  this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
185263
186329
  this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
185264
- this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes14(6).toString("hex")}`);
186330
+ this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes13(6).toString("hex")}`);
185265
186331
  this.consumer = {
185266
186332
  deliver: (sessionId, line, meta) => this.deliver(sessionId, line, meta),
185267
186333
  deliverCallback: (sessionId, callback) => this.deliverCallback(sessionId, callback)
@@ -185271,6 +186337,15 @@ class RemoteHttpSurface {
185271
186337
  return this.options.verifyShellToken(presented);
185272
186338
  }
185273
186339
  async handle(route, request, io) {
186340
+ if (isChannelsRoute(route)) {
186341
+ if (this.closed || this.channels === undefined) {
186342
+ return fail4(503, "remote-unavailable", "channels are not available in this serve");
186343
+ }
186344
+ if (request.method !== CHANNELS_ROUTE_METHODS[route]) {
186345
+ return fail4(405, "method-not-allowed", "Method not allowed.", { allow: CHANNELS_ROUTE_METHODS[route] });
186346
+ }
186347
+ return this.channels(route, request);
186348
+ }
185274
186349
  const hub = this.hub;
185275
186350
  if (this.closed || hub === undefined) {
185276
186351
  return fail4(503, "remote-unavailable", this.unavailableReason);
@@ -185310,6 +186385,9 @@ class RemoteHttpSurface {
185310
186385
  this.dropApproval(id);
185311
186386
  }
185312
186387
  }
186388
+ setChannels(handler) {
186389
+ this.channels = handler;
186390
+ }
185313
186391
  attach(hub) {
185314
186392
  this.hub = hub;
185315
186393
  }
@@ -185709,20 +186787,536 @@ var init_http_surface = __esm(() => {
185709
186787
  JSON_HEADERS2 = { "content-type": "application/json; charset=utf-8" };
185710
186788
  });
185711
186789
 
185712
- // src/remote/service.ts
185713
- import { unlinkSync as unlinkSync10 } from "fs";
185714
- function openRemoteService(options = {}) {
185715
- const present = readConfigFile(remoteConfigPath(options.dir));
185716
- if (!present.ok && present.reason === "absent") {
185717
- return { status: "disabled" };
186790
+ // src/remote/pairing.ts
186791
+ import { randomInt as randomInt2 } from "crypto";
186792
+ function newPairingCode() {
186793
+ let code = "";
186794
+ for (let i = 0;i < CODE_LENGTH; i += 1) {
186795
+ code += CODE_ALPHABET[randomInt2(CODE_ALPHABET.length)];
185718
186796
  }
185719
- const config = loadRemoteConfig(options.dir);
185720
- if (!config.ok) {
185721
- return { status: "unavailable", reason: config.reason };
186797
+ return code;
186798
+ }
186799
+ function describe3(error) {
186800
+ return redactSensitiveText(error instanceof Error ? error.message : String(error));
186801
+ }
186802
+ function normalise2(text) {
186803
+ return text.toUpperCase().replace(/[^A-Z0-9]/g, "");
186804
+ }
186805
+
186806
+ class Pairing {
186807
+ options;
186808
+ bot;
186809
+ state = "waiting-for-user";
186810
+ code;
186811
+ expiresAt;
186812
+ userId;
186813
+ chatId;
186814
+ chatTitle;
186815
+ problems = [];
186816
+ reason;
186817
+ candidate;
186818
+ poller;
186819
+ now;
186820
+ timers;
186821
+ expiryTimer;
186822
+ finished = false;
186823
+ queue = Promise.resolve();
186824
+ constructor(options, bot) {
186825
+ this.options = options;
186826
+ this.bot = bot;
186827
+ this.now = options.now ?? Date.now;
186828
+ this.timers = options.timers ?? realTimers;
186829
+ this.code = options.code ?? newPairingCode();
186830
+ this.expiresAt = this.now() + (options.ttlMs ?? PAIRING_CODE_TTL_MS);
186831
+ this.poller = new UpdatePoller({
186832
+ api: options.api,
186833
+ sink: { accept: (updates) => this.accept(updates) },
186834
+ ...options.pollTimeoutSec === undefined ? {} : { timeoutSec: options.pollTimeoutSec },
186835
+ ...options.pollBackoffMs === undefined ? {} : { backoffMs: options.pollBackoffMs },
186836
+ ...options.pollSleep === undefined ? {} : { sleep: options.pollSleep },
186837
+ onStatus: (status) => this.onPollerStatus(status)
186838
+ });
186839
+ }
186840
+ static async open(options) {
186841
+ let bot;
186842
+ try {
186843
+ bot = await options.api.getMe();
186844
+ } catch (error) {
186845
+ if (isBotApiError(error) && error.kind === "rejected") {
186846
+ return { ok: false, kind: "rejected", reason: "Telegram does not accept this bot token; check it with BotFather and try again." };
186847
+ }
186848
+ return { ok: false, kind: "unreachable", reason: `could not reach Telegram: ${describe3(error)}` };
186849
+ }
186850
+ const pairing = new Pairing(options, bot);
186851
+ pairing.begin();
186852
+ return { ok: true, pairing };
186853
+ }
186854
+ begin() {
186855
+ const handle = this.timers.setTimeout(() => this.expireIfDue(), Math.max(0, this.expiresAt - this.now()) + 1);
186856
+ handle?.unref?.();
186857
+ this.expiryTimer = handle;
186858
+ this.poller.start();
186859
+ }
186860
+ snapshot() {
186861
+ this.expireIfDue();
186862
+ return {
186863
+ state: this.state,
186864
+ ...this.code === undefined ? {} : { code: this.code },
186865
+ ...this.bot.username === undefined ? {} : { botUsername: this.bot.username },
186866
+ expiresAt: this.expiresAt,
186867
+ ...this.userId === undefined ? {} : { userId: this.userId },
186868
+ ...this.chatId === undefined ? {} : { chatId: this.chatId },
186869
+ ...this.chatTitle === undefined ? {} : { chatTitle: this.chatTitle },
186870
+ problems: [...this.problems],
186871
+ ...this.reason === undefined ? {} : { reason: this.reason }
186872
+ };
186873
+ }
186874
+ isFinal() {
186875
+ this.expireIfDue();
186876
+ return FINAL.has(this.state);
186877
+ }
186878
+ async recheck() {
186879
+ if (this.state === "waiting-for-group" && this.candidate !== undefined) {
186880
+ const chatId = this.candidate;
186881
+ await this.enqueue(() => this.inspectGroup(chatId));
186882
+ }
186883
+ }
186884
+ async cancel() {
186885
+ if (!FINAL.has(this.state)) {
186886
+ this.settle("cancelled");
186887
+ }
186888
+ await this.poller.stop();
186889
+ }
186890
+ async stop() {
186891
+ await this.poller.stop();
186892
+ }
186893
+ onPollerStatus(status) {
186894
+ if (status.state === "conflict" && !FINAL.has(this.state)) {
186895
+ this.reason = status.reason ?? "another poller owns this bot token";
186896
+ this.settle("failed");
186897
+ }
186898
+ }
186899
+ expireIfDue() {
186900
+ if (!FINAL.has(this.state) && this.now() > this.expiresAt) {
186901
+ this.code = undefined;
186902
+ this.settle("expired");
186903
+ }
186904
+ }
186905
+ settle(state) {
186906
+ this.state = state;
186907
+ if (FINAL.has(state)) {
186908
+ this.code = undefined;
186909
+ if (this.expiryTimer !== undefined) {
186910
+ this.timers.clearTimeout(this.expiryTimer);
186911
+ this.expiryTimer = undefined;
186912
+ }
186913
+ this.poller.stop();
186914
+ if (!this.finished) {
186915
+ this.finished = true;
186916
+ this.options.onFinished?.(this.snapshot());
186917
+ }
186918
+ }
186919
+ }
186920
+ enqueue(work) {
186921
+ const next = this.queue.then(work, work);
186922
+ this.queue = next.catch(() => {
186923
+ return;
186924
+ });
186925
+ return next;
186926
+ }
186927
+ accept(updates) {
186928
+ return this.enqueue(async () => {
186929
+ for (const update of updates) {
186930
+ if (FINAL.has(this.state)) {
186931
+ return;
186932
+ }
186933
+ if (this.now() > this.expiresAt) {
186934
+ this.expireIfDue();
186935
+ return;
186936
+ }
186937
+ if (this.state === "waiting-for-user") {
186938
+ await this.takeCode(update);
186939
+ } else if (this.state === "waiting-for-group") {
186940
+ await this.takeGroup(update);
186941
+ }
186942
+ }
186943
+ });
186944
+ }
186945
+ async takeCode(update) {
186946
+ const message = update.message;
186947
+ if (message === undefined || message.chat.type !== "private" || message.from === undefined || typeof message.text !== "string") {
186948
+ return;
186949
+ }
186950
+ if (message.forward_origin !== undefined || message.forward_date !== undefined) {
186951
+ return;
186952
+ }
186953
+ const sent = message.text.trim().replace(/^\/start(?:@\w+)?\s+/i, "");
186954
+ if (this.code === undefined || normalise2(sent) !== this.code) {
186955
+ return;
186956
+ }
186957
+ this.userId = message.from.id;
186958
+ this.code = undefined;
186959
+ this.state = "waiting-for-group";
186960
+ await this.options.api.sendMessage({ chatId: message.chat.id, text: "Paired. Now add me to your group (a group with Topics turned on) and make me an administrator." }).catch(() => {
186961
+ return;
186962
+ });
186963
+ }
186964
+ async takeGroup(update) {
186965
+ const change = update.my_chat_member;
186966
+ if (change === undefined || change.from.id !== this.userId) {
186967
+ return;
186968
+ }
186969
+ const type = change.chat.type;
186970
+ if (type !== "group" && type !== "supergroup") {
186971
+ return;
186972
+ }
186973
+ const status = change.new_chat_member.status;
186974
+ if (status !== "member" && status !== "administrator") {
186975
+ return;
186976
+ }
186977
+ this.candidate = change.chat.id;
186978
+ if (change.chat.title !== undefined) {
186979
+ this.chatTitle = change.chat.title;
186980
+ }
186981
+ await this.inspectGroup(change.chat.id);
186982
+ }
186983
+ async inspectGroup(chatId) {
186984
+ if (FINAL.has(this.state)) {
186985
+ return;
186986
+ }
186987
+ const problems = [];
186988
+ try {
186989
+ const chat = await this.options.api.getChat({ chatId });
186990
+ if (chat.title !== undefined) {
186991
+ this.chatTitle = chat.title;
186992
+ }
186993
+ if (chat.is_forum !== true) {
186994
+ problems.push("Topics are off in this group: open the group settings and turn Topics on.");
186995
+ }
186996
+ const member = await this.options.api.getChatMember({ chatId, userId: this.bot.id });
186997
+ if (member.status !== "administrator" && member.status !== "creator") {
186998
+ problems.push("Make the bot an administrator of the group.");
186999
+ } else if (member.status === "administrator" && member.can_manage_topics !== true) {
187000
+ problems.push("Give the bot the Manage Topics right in the group's administrator settings.");
187001
+ }
187002
+ } catch (error) {
187003
+ problems.push(`could not inspect the group: ${describe3(error)}`);
187004
+ }
187005
+ this.problems = problems;
187006
+ if (problems.length === 0) {
187007
+ this.chatId = chatId;
187008
+ this.settle("ready");
187009
+ }
187010
+ }
187011
+ }
187012
+ var PAIRING_CODE_TTL_MS, CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789", CODE_LENGTH = 8, FINAL;
187013
+ var init_pairing = __esm(() => {
187014
+ init_service2();
187015
+ init_hub();
187016
+ init_poller();
187017
+ init_types11();
187018
+ PAIRING_CODE_TTL_MS = 10 * 60000;
187019
+ FINAL = new Set(["ready", "failed", "expired", "cancelled"]);
187020
+ });
187021
+
187022
+ // src/remote/channels.ts
187023
+ function describe4(error) {
187024
+ return redactSensitiveText(error instanceof Error ? error.message : String(error));
187025
+ }
187026
+
187027
+ class ChannelsController {
187028
+ options;
187029
+ pairing;
187030
+ generation = 0;
187031
+ chain = Promise.resolve();
187032
+ constructor(options) {
187033
+ this.options = options;
187034
+ }
187035
+ get host() {
187036
+ return this.options.host;
187037
+ }
187038
+ handle(route, _request) {
187039
+ switch (route) {
187040
+ case "channels-status":
187041
+ return Promise.resolve(this.status());
187042
+ case "channels-pair":
187043
+ return this.pair();
187044
+ case "channels-pairing":
187045
+ return this.pairingStatus();
187046
+ case "channels-cancel":
187047
+ return this.cancel();
187048
+ case "channels-reload":
187049
+ return this.reload();
187050
+ case "channels-test":
187051
+ return this.test();
187052
+ case "channels-disconnect":
187053
+ return this.disconnect();
187054
+ }
187055
+ }
187056
+ async stop() {
187057
+ await this.dropPairing();
187058
+ }
187059
+ exclusive(work) {
187060
+ const next = this.chain.then(work, work);
187061
+ this.chain = next.catch(() => {
187062
+ return;
187063
+ });
187064
+ return next;
187065
+ }
187066
+ configured() {
187067
+ const read = readConfigFile(remoteConfigPath(this.host.dir));
187068
+ return read.ok || read.reason !== "absent";
187069
+ }
187070
+ state() {
187071
+ if (this.host.hub() !== undefined) {
187072
+ return { state: "connected" };
187073
+ }
187074
+ if (this.pairing !== undefined && !this.pairing.isFinal()) {
187075
+ return { state: "pairing" };
187076
+ }
187077
+ if (this.configured()) {
187078
+ const reason = this.host.hubReason();
187079
+ return { state: "off", ...reason === undefined ? {} : { reason } };
187080
+ }
187081
+ return { state: "not-connected" };
187082
+ }
187083
+ status() {
187084
+ const { state, reason } = this.state();
187085
+ return ok2({
187086
+ machine: this.host.machine,
187087
+ telegram: { state, ...reason === undefined ? {} : { reason }, sessions: this.host.hub()?.list().length ?? 0 }
187088
+ });
187089
+ }
187090
+ async dropPairing() {
187091
+ const running = this.pairing;
187092
+ this.pairing = undefined;
187093
+ await running?.cancel();
187094
+ }
187095
+ pair() {
187096
+ const mine = ++this.generation;
187097
+ const superseded = () => fail4(409, "superseded", "Another change to this channel came in while the pairing was starting; start again.");
187098
+ return this.exclusive(async () => {
187099
+ if (mine !== this.generation) {
187100
+ return superseded();
187101
+ }
187102
+ if (this.host.hub() !== undefined) {
187103
+ return fail4(409, "already-connected", "Telegram is already connected on this machine; disconnect it first.");
187104
+ }
187105
+ await this.dropPairing();
187106
+ const api = this.host.openApi();
187107
+ if (!api.ok) {
187108
+ return fail4(422, "no-token", api.reason);
187109
+ }
187110
+ const opened = await Pairing.open({
187111
+ api: api.api,
187112
+ ...this.options.now === undefined ? {} : { now: this.options.now },
187113
+ ...this.options.pairing
187114
+ });
187115
+ if (!opened.ok) {
187116
+ return fail4(opened.kind === "rejected" ? 422 : 502, opened.kind === "rejected" ? "token-rejected" : "telegram-unreachable", opened.reason);
187117
+ }
187118
+ if (mine !== this.generation) {
187119
+ await opened.pairing.cancel();
187120
+ return superseded();
187121
+ }
187122
+ this.pairing = opened.pairing;
187123
+ return ok2({ ...opened.pairing.snapshot() });
187124
+ });
185722
187125
  }
187126
+ async pairingStatus() {
187127
+ const current = this.pairing;
187128
+ if (current === undefined) {
187129
+ return fail4(404, "no-pairing", "No pairing is open; start one first.");
187130
+ }
187131
+ await current.recheck();
187132
+ return ok2({ ...current.snapshot() });
187133
+ }
187134
+ async cancel() {
187135
+ this.generation += 1;
187136
+ const had = this.pairing !== undefined;
187137
+ await this.dropPairing();
187138
+ return ok2({ cancelled: had });
187139
+ }
187140
+ reload() {
187141
+ this.generation += 1;
187142
+ return this.exclusive(async () => {
187143
+ await this.dropPairing();
187144
+ const config = loadRemoteConfig(this.host.dir);
187145
+ const token = loadBotToken(this.host.dir);
187146
+ if (!config.ok || !token.ok) {
187147
+ const reason = !config.ok ? config.reason : !token.ok ? token.reason : "";
187148
+ return fail4(422, "cannot-connect", this.host.hub() === undefined ? reason : `${reason} The running channel was left as it was.`);
187149
+ }
187150
+ if (this.host.hub() !== undefined) {
187151
+ await this.host.stopHub("reloading remote control");
187152
+ }
187153
+ const started = await this.host.startHub();
187154
+ if (!started.ok) {
187155
+ return fail4(422, "cannot-connect", started.reason);
187156
+ }
187157
+ return ok2({ state: "connected" });
187158
+ });
187159
+ }
187160
+ async test() {
187161
+ const hub = this.host.hub();
187162
+ if (hub === undefined) {
187163
+ const reason = this.host.hubReason();
187164
+ return fail4(409, "not-connected", reason ?? "Telegram is not connected on this machine.");
187165
+ }
187166
+ try {
187167
+ await hub.sendGeneral(`Keryx test message from ${this.host.machine}: Telegram is connected.`);
187168
+ } catch (error) {
187169
+ return fail4(502, "send-failed", `Telegram did not accept the test message: ${describe4(error)}`);
187170
+ }
187171
+ return ok2({ delivered: true, machine: this.host.machine });
187172
+ }
187173
+ disconnect() {
187174
+ this.generation += 1;
187175
+ return this.exclusive(async () => {
187176
+ await this.dropPairing();
187177
+ const hub = this.host.hub();
187178
+ if (hub === undefined) {
187179
+ return ok2({ deleted: 0, remaining: 0, hubWasRunning: false });
187180
+ }
187181
+ const result = await hub.deleteAllTopics();
187182
+ await this.host.stopHub("Telegram was disconnected");
187183
+ return ok2({ deleted: result.deleted, remaining: result.remaining, hubWasRunning: true });
187184
+ });
187185
+ }
187186
+ }
187187
+ var init_channels = __esm(() => {
187188
+ init_service2();
187189
+ init_config_dir();
187190
+ init_config10();
187191
+ init_http_surface();
187192
+ init_pairing();
187193
+ init_paths4();
187194
+ });
187195
+
187196
+ // src/remote/poll-lock.ts
187197
+ import { randomBytes as randomBytes14 } from "crypto";
187198
+ import { linkSync, renameSync as renameSync8, statSync as statSync21, unlinkSync as unlinkSync10 } from "fs";
187199
+ import path353 from "path";
187200
+ function pollerLockPath(dir) {
187201
+ return path353.join(remoteDirPath(dir), POLLER_LOCK_FILE);
187202
+ }
187203
+ function processIsAlive3(pid) {
187204
+ try {
187205
+ process.kill(pid, 0);
187206
+ return true;
187207
+ } catch (error) {
187208
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EPERM";
187209
+ }
187210
+ }
187211
+ function parsePid(text) {
187212
+ const pid = Number.parseInt(text.trim(), 10);
187213
+ return Number.isInteger(pid) && pid > 0 ? pid : Number.NaN;
187214
+ }
187215
+ function refusal4(holder) {
187216
+ return {
187217
+ ok: false,
187218
+ holderPid: holder,
187219
+ reason: `another keryx serve (pid ${holder}) already polls this bot token on this machine; this one will not poll. Stop it, or use a different bot.`
187220
+ };
187221
+ }
187222
+ function createWhole(file, pid) {
187223
+ const staging = `${file}.${pid}.${randomBytes14(6).toString("hex")}.new`;
187224
+ createOwnerOnlyFileExclusive(staging, `${pid}
187225
+ `);
187226
+ try {
187227
+ linkSync(staging, file);
187228
+ return true;
187229
+ } catch (error) {
187230
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
187231
+ return false;
187232
+ }
187233
+ return createOwnerOnlyFileExclusive(file, `${pid}
187234
+ `);
187235
+ } finally {
187236
+ try {
187237
+ unlinkSync10(staging);
187238
+ } catch {}
187239
+ }
187240
+ }
187241
+ function acquirePollLock(options = {}) {
187242
+ ensureRemoteDir(options.dir);
187243
+ const file = pollerLockPath(options.dir);
187244
+ const pid = options.pid ?? process.pid;
187245
+ const isAlive = options.isAlive ?? processIsAlive3;
187246
+ const now = options.now ?? Date.now;
187247
+ for (let attempt = 0;attempt < 4; attempt += 1) {
187248
+ if (createWhole(file, pid)) {
187249
+ return {
187250
+ ok: true,
187251
+ release() {
187252
+ const current = readConfigFile(file);
187253
+ if (current.ok && parsePid(current.text) === pid) {
187254
+ try {
187255
+ unlinkSync10(file);
187256
+ } catch {}
187257
+ }
187258
+ }
187259
+ };
187260
+ }
187261
+ const held = readConfigFile(file);
187262
+ if (!held.ok && held.reason === "absent") {
187263
+ continue;
187264
+ }
187265
+ const holder = held.ok ? parsePid(held.text) : Number.NaN;
187266
+ if (Number.isInteger(holder) && isAlive(holder)) {
187267
+ return refusal4(holder);
187268
+ }
187269
+ if (!Number.isInteger(holder)) {
187270
+ let youngerThanGrace;
187271
+ try {
187272
+ youngerThanGrace = now() - statSync21(file).mtimeMs < EMPTY_LOCK_GRACE_MS;
187273
+ } catch {
187274
+ continue;
187275
+ }
187276
+ if (youngerThanGrace) {
187277
+ return {
187278
+ ok: false,
187279
+ holderPid: 0,
187280
+ reason: `another keryx serve is creating ${file} right now; this one will not poll. Try again in a moment.`
187281
+ };
187282
+ }
187283
+ }
187284
+ const aside = `${file}.stale-${pid}-${randomBytes14(6).toString("hex")}`;
187285
+ try {
187286
+ renameSync8(file, aside);
187287
+ } catch {
187288
+ continue;
187289
+ }
187290
+ const moved = readConfigFile(aside);
187291
+ const movedPid = moved.ok ? parsePid(moved.text) : Number.NaN;
187292
+ if (Number.isInteger(movedPid) && movedPid !== holder && isAlive(movedPid)) {
187293
+ try {
187294
+ linkSync(aside, file);
187295
+ } catch {}
187296
+ try {
187297
+ unlinkSync10(aside);
187298
+ } catch {}
187299
+ return refusal4(movedPid);
187300
+ }
187301
+ try {
187302
+ unlinkSync10(aside);
187303
+ } catch {}
187304
+ }
187305
+ return { ok: false, holderPid: 0, reason: `could not take the poller lock at ${file}` };
187306
+ }
187307
+ var POLLER_LOCK_FILE = "poller.lock", EMPTY_LOCK_GRACE_MS = 5000;
187308
+ var init_poll_lock = __esm(() => {
187309
+ init_config_dir();
187310
+ init_paths4();
187311
+ });
187312
+
187313
+ // src/remote/service.ts
187314
+ import { unlinkSync as unlinkSync11 } from "fs";
187315
+ function openRemoteService(options = {}) {
185723
187316
  const notice = options.onNotice ?? (() => {
185724
187317
  return;
185725
187318
  });
187319
+ const machine = options.machine ?? localMachineName();
185726
187320
  let verifyShellToken;
185727
187321
  const surface = new RemoteHttpSurface({
185728
187322
  verifyShellToken: (presented) => verifyShellToken?.(presented) ?? false,
@@ -185734,18 +187328,130 @@ function openRemoteService(options = {}) {
185734
187328
  let hub;
185735
187329
  let lock;
185736
187330
  let endpointWritten = false;
185737
- let attached = false;
185738
- let conflictWhileStarting;
187331
+ let hubReason = "remote control has not started yet";
185739
187332
  let stopping;
187333
+ let hubChain = Promise.resolve();
187334
+ const exclusive = (work) => {
187335
+ const next = hubChain.then(work, work);
187336
+ hubChain = next.catch(() => {
187337
+ return;
187338
+ });
187339
+ return next;
187340
+ };
187341
+ const stopHubNow = async (reason) => {
187342
+ const running = hub;
187343
+ hub = undefined;
187344
+ hubReason = reason;
187345
+ surface.detach(reason);
187346
+ if (running !== undefined) {
187347
+ await running.stop();
187348
+ }
187349
+ };
187350
+ const startHubNow = async () => {
187351
+ if (hub !== undefined) {
187352
+ return { ok: true };
187353
+ }
187354
+ const present = readConfigFile(remoteConfigPath(options.dir));
187355
+ if (!present.ok && present.reason === "absent") {
187356
+ hubReason = "Telegram is not connected on this machine";
187357
+ surface.detach("Telegram is not connected on this machine; connect it from the shell with /channels");
187358
+ return { ok: false, reason: hubReason, notConnected: true };
187359
+ }
187360
+ let started = false;
187361
+ let conflictWhileStarting;
187362
+ let mine;
187363
+ const opened = openRemoteHub({
187364
+ ...options.dir === undefined ? {} : { dir: options.dir },
187365
+ ...options.api === undefined ? {} : { api: options.api },
187366
+ ...options.now === undefined ? {} : { now: options.now },
187367
+ ...options.timers === undefined ? {} : { timers: options.timers },
187368
+ ...options.sweepIntervalMs === undefined ? {} : { sweepIntervalMs: options.sweepIntervalMs },
187369
+ ...options.deliverRetryMs === undefined ? {} : { deliverRetryMs: options.deliverRetryMs },
187370
+ ...options.pollTimeoutSec === undefined ? {} : { pollTimeoutSec: options.pollTimeoutSec },
187371
+ ...options.pollBackoffMs === undefined ? {} : { pollBackoffMs: options.pollBackoffMs },
187372
+ ...options.pollSleep === undefined ? {} : { pollSleep: options.pollSleep },
187373
+ ...options.onEvent === undefined ? {} : { onEvent: options.onEvent },
187374
+ ...options.beforeAck === undefined ? {} : { beforeAck: options.beforeAck },
187375
+ ...options.nameTopicsByMachine === true ? { machine } : {},
187376
+ deliver: surface.consumer.deliver,
187377
+ ...surface.consumer.deliverCallback === undefined ? {} : { deliverCallback: surface.consumer.deliverCallback },
187378
+ onPollerStatus: (status) => {
187379
+ if (status.state !== "conflict") {
187380
+ return;
187381
+ }
187382
+ const reason = status.reason ?? "another poller owns this bot token";
187383
+ if (!started) {
187384
+ conflictWhileStarting = reason;
187385
+ return;
187386
+ }
187387
+ exclusive(async () => {
187388
+ if (hub !== mine) {
187389
+ return;
187390
+ }
187391
+ notice(`remote control is off: ${reason}`);
187392
+ await stopHubNow(reason);
187393
+ });
187394
+ }
187395
+ });
187396
+ if (!opened.ok) {
187397
+ notice(`remote control is off: ${opened.reason}`);
187398
+ hubReason = opened.reason;
187399
+ surface.detach(opened.reason);
187400
+ return { ok: false, reason: opened.reason };
187401
+ }
187402
+ mine = opened.hub;
187403
+ try {
187404
+ await mine.start();
187405
+ } catch (error) {
187406
+ const reason = `could not start remote control: ${error instanceof Error ? error.message : String(error)}`;
187407
+ await mine.stop().catch(() => {
187408
+ return;
187409
+ });
187410
+ notice(`remote control is off: ${reason}`);
187411
+ hubReason = reason;
187412
+ surface.detach(reason);
187413
+ return { ok: false, reason };
187414
+ }
187415
+ if (conflictWhileStarting !== undefined) {
187416
+ await mine.stop().catch(() => {
187417
+ return;
187418
+ });
187419
+ notice(`remote control is off: ${conflictWhileStarting}`);
187420
+ hubReason = conflictWhileStarting;
187421
+ surface.detach(conflictWhileStarting);
187422
+ return { ok: false, reason: conflictWhileStarting };
187423
+ }
187424
+ hub = mine;
187425
+ hubReason = undefined;
187426
+ surface.attach(mine);
187427
+ started = true;
187428
+ return { ok: true };
187429
+ };
187430
+ const controller = new ChannelsController({
187431
+ host: {
187432
+ dir: options.dir,
187433
+ machine,
187434
+ hub: () => hub,
187435
+ hubReason: () => hubReason,
187436
+ startHub: () => exclusive(startHubNow),
187437
+ stopHub: (reason) => exclusive(() => stopHubNow(reason)),
187438
+ openApi: () => openBotApi({ ...options.api === undefined ? {} : { api: options.api }, ...options.dir === undefined ? {} : { dir: options.dir } })
187439
+ },
187440
+ ...options.now === undefined ? {} : { now: options.now },
187441
+ pairing: {
187442
+ ...options.timers === undefined ? {} : { timers: options.timers },
187443
+ ...options.pollTimeoutSec === undefined ? {} : { pollTimeoutSec: options.pollTimeoutSec },
187444
+ ...options.pollBackoffMs === undefined ? {} : { pollBackoffMs: options.pollBackoffMs },
187445
+ ...options.pollSleep === undefined ? {} : { pollSleep: options.pollSleep },
187446
+ ...options.pairing
187447
+ }
187448
+ });
185740
187449
  const shutdown = (reason) => {
185741
187450
  stopping ??= (async () => {
185742
- const running = hub;
185743
- hub = undefined;
185744
187451
  surface.detach(reason);
185745
187452
  await surface.close();
185746
- if (running !== undefined) {
185747
- await running.stop();
185748
- }
187453
+ await controller.stop();
187454
+ await exclusive(() => stopHubNow(reason));
185749
187455
  if (endpointWritten) {
185750
187456
  endpointWritten = false;
185751
187457
  removeEndpointIfOurs(options.dir);
@@ -185764,78 +187470,36 @@ function openRemoteService(options = {}) {
185764
187470
  return { ok: false, reason };
185765
187471
  };
185766
187472
  return {
185767
- status: "ready",
185768
- service: {
185769
- surface,
185770
- hub: () => hub,
185771
- async start(endpoint) {
185772
- if (!isLoopbackAddress(endpoint.address)) {
185773
- return refuse(`serve is bound to ${endpoint.address}, which is not a loopback address. Remote control is local-only; bind serve to 127.0.0.1 or ::1 to use it.`);
185774
- }
185775
- if (options.lock?.disabled !== true) {
185776
- lock = acquirePollLock({ dir: options.dir, ...options.lock });
185777
- if (!lock.ok) {
185778
- return refuse(lock.reason);
185779
- }
185780
- }
185781
- const token = mintShellToken(options.dir);
185782
- if (!token.ok) {
185783
- return refuse(token.reason);
185784
- }
185785
- verifyShellToken = createShellTokenVerifier(token.value);
185786
- const opened = openRemoteHub({
185787
- ...options.dir === undefined ? {} : { dir: options.dir },
185788
- ...options.api === undefined ? {} : { api: options.api },
185789
- ...options.now === undefined ? {} : { now: options.now },
185790
- ...options.timers === undefined ? {} : { timers: options.timers },
185791
- ...options.sweepIntervalMs === undefined ? {} : { sweepIntervalMs: options.sweepIntervalMs },
185792
- ...options.deliverRetryMs === undefined ? {} : { deliverRetryMs: options.deliverRetryMs },
185793
- ...options.pollTimeoutSec === undefined ? {} : { pollTimeoutSec: options.pollTimeoutSec },
185794
- ...options.pollBackoffMs === undefined ? {} : { pollBackoffMs: options.pollBackoffMs },
185795
- ...options.pollSleep === undefined ? {} : { pollSleep: options.pollSleep },
185796
- ...options.onEvent === undefined ? {} : { onEvent: options.onEvent },
185797
- ...options.beforeAck === undefined ? {} : { beforeAck: options.beforeAck },
185798
- deliver: surface.consumer.deliver,
185799
- ...surface.consumer.deliverCallback === undefined ? {} : { deliverCallback: surface.consumer.deliverCallback },
185800
- onPollerStatus: (status) => {
185801
- if (status.state === "conflict") {
185802
- const reason = status.reason ?? "another poller owns this bot token";
185803
- if (!attached) {
185804
- conflictWhileStarting = reason;
185805
- return;
185806
- }
185807
- (async () => {
185808
- surface.detach(reason);
185809
- notice(`remote control is off: ${reason}`);
185810
- await hub?.stop();
185811
- })();
185812
- }
185813
- }
185814
- });
185815
- if (!opened.ok) {
185816
- return refuse(opened.reason);
187473
+ surface,
187474
+ hub: () => hub,
187475
+ async start(endpoint) {
187476
+ if (!isLoopbackAddress(endpoint.address)) {
187477
+ return refuse(`serve is bound to ${endpoint.address}, which is not a loopback address. Remote control is local-only; bind serve to 127.0.0.1 or ::1 to use it.`);
187478
+ }
187479
+ if (options.lock?.disabled !== true) {
187480
+ lock = acquirePollLock({ dir: options.dir, ...options.lock });
187481
+ if (!lock.ok) {
187482
+ return refuse(lock.reason);
185817
187483
  }
185818
- hub = opened.hub;
185819
- try {
185820
- await hub.start();
185821
- ensureRemoteDir(options.dir);
185822
- const body = { address: endpoint.address, port: endpoint.port, pid: process.pid };
185823
- writeOwnerOnlyFileAtomic(endpointPath(options.dir), `${JSON.stringify(body)}
187484
+ }
187485
+ const token = mintShellToken(options.dir);
187486
+ if (!token.ok) {
187487
+ return refuse(token.reason);
187488
+ }
187489
+ verifyShellToken = createShellTokenVerifier(token.value);
187490
+ try {
187491
+ ensureRemoteDir(options.dir);
187492
+ const body = { address: endpoint.address, port: endpoint.port, pid: process.pid };
187493
+ writeOwnerOnlyFileAtomic(endpointPath(options.dir), `${JSON.stringify(body)}
185824
187494
  `);
185825
- endpointWritten = true;
185826
- } catch (error) {
185827
- hub = undefined;
185828
- return refuse(`could not start remote control: ${error instanceof Error ? error.message : String(error)}`);
185829
- }
185830
- if (conflictWhileStarting !== undefined) {
185831
- return refuse(conflictWhileStarting);
185832
- }
185833
- surface.attach(hub);
185834
- attached = true;
185835
- return { ok: true };
185836
- },
185837
- stop
185838
- }
187495
+ endpointWritten = true;
187496
+ } catch (error) {
187497
+ return refuse(`could not publish the local endpoint: ${error instanceof Error ? error.message : String(error)}`);
187498
+ }
187499
+ surface.setChannels((route, request) => controller.handle(route, request));
187500
+ return exclusive(startHubNow);
187501
+ },
187502
+ stop
185839
187503
  };
185840
187504
  }
185841
187505
  function removeEndpointIfOurs(dir) {
@@ -185847,7 +187511,7 @@ function removeEndpointIfOurs(dir) {
185847
187511
  try {
185848
187512
  const parsed = JSON.parse(read.text);
185849
187513
  if (parsed.pid === process.pid) {
185850
- unlinkSync10(file);
187514
+ unlinkSync11(file);
185851
187515
  }
185852
187516
  } catch {}
185853
187517
  }
@@ -185855,8 +187519,9 @@ var init_service18 = __esm(() => {
185855
187519
  init_serve_config();
185856
187520
  init_config_dir();
185857
187521
  init_bootstrap2();
185858
- init_config12();
187522
+ init_channels();
185859
187523
  init_endpoint();
187524
+ init_naming();
185860
187525
  init_paths4();
185861
187526
  init_poll_lock();
185862
187527
  init_http_surface();
@@ -185951,20 +187616,20 @@ async function runServe(args) {
185951
187616
  const stored = loadServeConfig(undefined, warn);
185952
187617
  const runtimeAck = parsed.parsed.flags.has(ACK_FLAG);
185953
187618
  const config = stored === null ? null : overlay(stored, parsed.parsed, port, runtimeAck);
185954
- const remote = openRemoteService({ onNotice: (message) => console.log(` ${style.yellow(symbols.bullet)} ${sanitizeForDisplay(message)}`) });
185955
- if (remote.status === "unavailable") {
185956
- console.log(` ${style.yellow(symbols.bullet)} remote control is off: ${sanitizeForDisplay(remote.reason)}`);
185957
- }
185958
- const remoteService = remote.status === "ready" ? remote.service : undefined;
187619
+ const remoteService = openRemoteService({
187620
+ machine: localMachineName(),
187621
+ nameTopicsByMachine: true,
187622
+ onNotice: (message) => console.log(` ${style.yellow(symbols.bullet)} ${sanitizeForDisplay(message)}`)
187623
+ });
185959
187624
  const outcome = await startServeListener({
185960
187625
  config,
185961
187626
  credential: readServeCredential(),
185962
- remote: remoteService?.surface,
187627
+ remote: remoteService.surface,
185963
187628
  configState: serveConfigState(),
185964
187629
  makeSubmitTurn: assembleSubmitTurn
185965
187630
  });
185966
187631
  if (!outcome.ok) {
185967
- await remoteService?.stop();
187632
+ await remoteService.stop();
185968
187633
  fail5(sanitizeForDisplay(outcome.message));
185969
187634
  if (outcome.reason === "non-loopback-not-acknowledged" && stored !== null) {
185970
187635
  if (stored.bind.acknowledgeNonLoopback !== true) {
@@ -185982,11 +187647,11 @@ async function runServe(args) {
185982
187647
  if (!isLoopbackAddress(listener.address)) {
185983
187648
  console.log(` ${style.yellow(symbols.bullet)} this bind is reachable beyond loopback and there is no TLS in this release`);
185984
187649
  }
185985
- if (remoteService !== undefined) {
185986
- const started = await remoteService.start({ address: listener.address, port: listener.port });
185987
- if (started.ok) {
185988
- console.log(` ${style.dim("remote control on: shells register at /v1/remote/*")}`);
185989
- }
187650
+ const started = await remoteService.start({ address: listener.address, port: listener.port });
187651
+ if (started.ok) {
187652
+ console.log(` ${style.dim("remote control on: shells register at /v1/remote/*")}`);
187653
+ } else if (started.notConnected === true) {
187654
+ console.log(` ${style.dim("channels ready: connect Telegram from the shell with /channels")}`);
185990
187655
  }
185991
187656
  await new Promise((resolve) => {
185992
187657
  let draining = false;
@@ -185996,7 +187661,7 @@ async function runServe(args) {
185996
187661
  }
185997
187662
  draining = true;
185998
187663
  console.log(` ${style.dim("draining\u2026")}`);
185999
- listener.drain().then(() => remoteService?.stop()).then(() => {
187664
+ listener.drain().then(() => remoteService.stop()).then(() => {
186000
187665
  console.log(` ${style.green(symbols.ok)} stopped`);
186001
187666
  resolve();
186002
187667
  });
@@ -186347,6 +188012,7 @@ var init_serve = __esm(() => {
186347
188012
  init_serve_server();
186348
188013
  init_serve_approvals_store();
186349
188014
  init_serve_runner();
188015
+ init_naming();
186350
188016
  init_service18();
186351
188017
  init_ui();
186352
188018
  BIND_FLAGS = ["--bind", "--port", "--profile"];