@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/README.md +12 -6
- package/dist/cli.js +2410 -744
- package/dist/core.js +6 -0
- package/package.json +1 -1
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.
|
|
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
|
|
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:
|
|
111428
|
+
return { text: fit3("off", width), role: "muted", action: "open" };
|
|
109957
111429
|
if (status.state === "offline")
|
|
109958
|
-
return { text:
|
|
109959
|
-
return { text:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
111083
|
-
return { text:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: `${
|
|
111581
|
-
chunks.push({ role: lastRole, text:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
134224
|
-
`${
|
|
134225
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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() -
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
183734
|
-
import { unlinkSync as
|
|
183735
|
-
import
|
|
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 =
|
|
183767
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
|
185071
|
+
import path350 from "path";
|
|
183876
185072
|
|
|
183877
185073
|
class RejectedJournal {
|
|
183878
185074
|
file;
|
|
183879
185075
|
now;
|
|
183880
185076
|
constructor(options) {
|
|
183881
|
-
this.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
|
-
|
|
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
|
-
|
|
184994
|
-
if (
|
|
184995
|
-
|
|
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
|
-
|
|
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
|
|
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${
|
|
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/
|
|
185713
|
-
import {
|
|
185714
|
-
function
|
|
185715
|
-
|
|
185716
|
-
|
|
185717
|
-
|
|
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
|
-
|
|
185720
|
-
|
|
185721
|
-
|
|
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
|
|
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
|
-
|
|
185747
|
-
|
|
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
|
-
|
|
185768
|
-
|
|
185769
|
-
|
|
185770
|
-
|
|
185771
|
-
|
|
185772
|
-
|
|
185773
|
-
|
|
185774
|
-
}
|
|
185775
|
-
if (
|
|
185776
|
-
|
|
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
|
-
|
|
185819
|
-
|
|
185820
|
-
|
|
185821
|
-
|
|
185822
|
-
|
|
185823
|
-
|
|
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
|
-
|
|
185826
|
-
|
|
185827
|
-
|
|
185828
|
-
|
|
185829
|
-
|
|
185830
|
-
|
|
185831
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
185955
|
-
|
|
185956
|
-
|
|
185957
|
-
}
|
|
185958
|
-
|
|
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
|
|
187627
|
+
remote: remoteService.surface,
|
|
185963
187628
|
configState: serveConfigState(),
|
|
185964
187629
|
makeSubmitTurn: assembleSubmitTurn
|
|
185965
187630
|
});
|
|
185966
187631
|
if (!outcome.ok) {
|
|
185967
|
-
await remoteService
|
|
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
|
-
|
|
185986
|
-
|
|
185987
|
-
|
|
185988
|
-
|
|
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
|
|
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"];
|