@mrciphersmith/keryx 0.3.55 → 0.3.57
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 +7 -1
- package/dist/cli.js +2064 -225
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -20104,7 +20104,7 @@ var package_default;
|
|
|
20104
20104
|
var init_package = __esm(() => {
|
|
20105
20105
|
package_default = {
|
|
20106
20106
|
name: "@mrciphersmith/keryx",
|
|
20107
|
-
version: "0.3.
|
|
20107
|
+
version: "0.3.57",
|
|
20108
20108
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
20109
20109
|
private: false,
|
|
20110
20110
|
publishConfig: {
|
|
@@ -20152,7 +20152,7 @@ var init_package = __esm(() => {
|
|
|
20152
20152
|
"test:core": "bun test src/cli src/core src/impact-evidence/ src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/product/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/ scripts/",
|
|
20153
20153
|
"test:client:terminal": "bun test src/tui/ src/commands/shell",
|
|
20154
20154
|
"test:client:streaming": "bun test src/harness/provider/",
|
|
20155
|
-
"test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/rewind/ src/remote/ src/commands/sessions",
|
|
20155
|
+
"test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/rewind/ src/remote/ src/docs/ src/commands/sessions",
|
|
20156
20156
|
"test:client:runtime": "bun test src/harness/ src/mcp-client/ src/mcp-servers/ src/agents/ src/commands/agent src/commands/harness src/commands/providers",
|
|
20157
20157
|
"test:client": "bun run test:client:terminal && bun run test:client:streaming && bun run test:client:cancel-resume && bun run test:client:runtime",
|
|
20158
20158
|
check: "bun run lint && bun run typecheck && bun run typecheck:scripts && bun test",
|
|
@@ -111379,6 +111379,19 @@ function parseApprovalCallback(data) {
|
|
|
111379
111379
|
}
|
|
111380
111380
|
return { approvalId: match[1], decision: match[2] };
|
|
111381
111381
|
}
|
|
111382
|
+
function choiceCallbackData(promptId, index) {
|
|
111383
|
+
return `pk:${promptId}:${index}`;
|
|
111384
|
+
}
|
|
111385
|
+
function parseChoiceCallback(data) {
|
|
111386
|
+
const match = CHOICE_CALLBACK_PATTERN.exec(data);
|
|
111387
|
+
if (match === null) {
|
|
111388
|
+
return;
|
|
111389
|
+
}
|
|
111390
|
+
return { promptId: match[1], index: Number(match[2]) };
|
|
111391
|
+
}
|
|
111392
|
+
function isMessageState(value) {
|
|
111393
|
+
return typeof value === "string" && MESSAGE_STATES.includes(value);
|
|
111394
|
+
}
|
|
111382
111395
|
function encodeSseEvent(name, data, id) {
|
|
111383
111396
|
return `event: ${name}
|
|
111384
111397
|
${id === undefined ? "" : `id: ${id}
|
|
@@ -111428,7 +111441,7 @@ class SseParser {
|
|
|
111428
111441
|
return frames;
|
|
111429
111442
|
}
|
|
111430
111443
|
}
|
|
111431
|
-
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
|
|
111444
|
+
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:", CHOICE_CALLBACK_PATTERN, RESERVED_CHOICE_PREFIX = "pk:", MAX_PROMPT_BUTTONS = 24, MESSAGE_STATES, SSE_KEEPALIVE_FRAME = `: keepalive
|
|
111432
111445
|
|
|
111433
111446
|
`;
|
|
111434
111447
|
var init_protocol2 = __esm(() => {
|
|
@@ -111438,6 +111451,8 @@ var init_protocol2 = __esm(() => {
|
|
|
111438
111451
|
heartbeat: "POST",
|
|
111439
111452
|
reply: "POST",
|
|
111440
111453
|
approval: "POST",
|
|
111454
|
+
prompt: "POST",
|
|
111455
|
+
state: "POST",
|
|
111441
111456
|
ack: "POST",
|
|
111442
111457
|
stream: "GET"
|
|
111443
111458
|
};
|
|
@@ -111455,6 +111470,8 @@ var init_protocol2 = __esm(() => {
|
|
|
111455
111470
|
MAX_APPROVAL_TIMEOUT_MS = 60 * 60000;
|
|
111456
111471
|
SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
111457
111472
|
APPROVAL_CALLBACK_PATTERN = /^ap:(ap[0-9a-f]{12}):(allow|deny)$/;
|
|
111473
|
+
CHOICE_CALLBACK_PATTERN = /^pk:(pk[0-9a-f]{12}):(\d{1,3})$/;
|
|
111474
|
+
MESSAGE_STATES = ["reading", "working", "done", "failed"];
|
|
111458
111475
|
});
|
|
111459
111476
|
|
|
111460
111477
|
// src/remote/paths.ts
|
|
@@ -111835,6 +111852,8 @@ class RemoteClient {
|
|
|
111835
111852
|
inboundChain = Promise.resolve();
|
|
111836
111853
|
waiters = new Map;
|
|
111837
111854
|
earlyDecisions = new Map;
|
|
111855
|
+
choiceWaiters = new Map;
|
|
111856
|
+
earlyChoices = new Map;
|
|
111838
111857
|
firstResult;
|
|
111839
111858
|
name;
|
|
111840
111859
|
threadId;
|
|
@@ -111950,6 +111969,45 @@ class RemoteClient {
|
|
|
111950
111969
|
this.waiters.set(approvalId, { resolve, timer });
|
|
111951
111970
|
});
|
|
111952
111971
|
}
|
|
111972
|
+
async requestChoice(text, rows, timeoutMs, forUserId) {
|
|
111973
|
+
if (!this.connected) {
|
|
111974
|
+
return;
|
|
111975
|
+
}
|
|
111976
|
+
const body = { sessionId: this.options.sessionId, text, rows, timeoutMs, ...forUserId === undefined ? {} : { forUserId } };
|
|
111977
|
+
let promptId;
|
|
111978
|
+
try {
|
|
111979
|
+
const response = await this.post("prompt", body);
|
|
111980
|
+
if (!response.ok) {
|
|
111981
|
+
return;
|
|
111982
|
+
}
|
|
111983
|
+
promptId = (await response.json()).promptId;
|
|
111984
|
+
} catch {
|
|
111985
|
+
return;
|
|
111986
|
+
}
|
|
111987
|
+
const early = this.earlyChoices.get(promptId);
|
|
111988
|
+
if (early !== undefined) {
|
|
111989
|
+
this.earlyChoices.delete(promptId);
|
|
111990
|
+
return early;
|
|
111991
|
+
}
|
|
111992
|
+
if (!this.connected) {
|
|
111993
|
+
return;
|
|
111994
|
+
}
|
|
111995
|
+
return new Promise((resolve) => {
|
|
111996
|
+
const timer = setTimeout(() => {
|
|
111997
|
+
this.choiceWaiters.delete(promptId);
|
|
111998
|
+
resolve(undefined);
|
|
111999
|
+
}, timeoutMs);
|
|
112000
|
+
this.choiceWaiters.set(promptId, { resolve, timer });
|
|
112001
|
+
});
|
|
112002
|
+
}
|
|
112003
|
+
async reportState(updateId, state) {
|
|
112004
|
+
if (!this.connected) {
|
|
112005
|
+
return;
|
|
112006
|
+
}
|
|
112007
|
+
try {
|
|
112008
|
+
await this.post("state", { sessionId: this.options.sessionId, updateId, state });
|
|
112009
|
+
} catch {}
|
|
112010
|
+
}
|
|
111953
112011
|
target(route, query = "") {
|
|
111954
112012
|
const token = readShellToken(this.options.dir);
|
|
111955
112013
|
const endpoint = readEndpoint(this.options.dir);
|
|
@@ -112142,6 +112200,9 @@ class RemoteClient {
|
|
|
112142
112200
|
} else if (event === "approval") {
|
|
112143
112201
|
const approval = parsed;
|
|
112144
112202
|
this.resolveApproval(approval.approvalId, approval.decision);
|
|
112203
|
+
} else if (event === "choice") {
|
|
112204
|
+
const choice = parsed;
|
|
112205
|
+
this.resolveChoice(choice.promptId, choice.index);
|
|
112145
112206
|
} else if (event === "callback") {
|
|
112146
112207
|
const callback = parsed;
|
|
112147
112208
|
if (this.options.onCallback !== undefined) {
|
|
@@ -112199,6 +112260,30 @@ class RemoteClient {
|
|
|
112199
112260
|
this.waiters.delete(approvalId);
|
|
112200
112261
|
waiter.resolve(decision);
|
|
112201
112262
|
}
|
|
112263
|
+
resolveChoice(promptId, index) {
|
|
112264
|
+
const waiter = this.choiceWaiters.get(promptId);
|
|
112265
|
+
if (waiter === undefined) {
|
|
112266
|
+
if (this.earlyChoices.size >= MAX_EARLY_DECISIONS) {
|
|
112267
|
+
const oldest = this.earlyChoices.keys().next().value;
|
|
112268
|
+
if (oldest !== undefined) {
|
|
112269
|
+
this.earlyChoices.delete(oldest);
|
|
112270
|
+
}
|
|
112271
|
+
}
|
|
112272
|
+
this.earlyChoices.set(promptId, index);
|
|
112273
|
+
return;
|
|
112274
|
+
}
|
|
112275
|
+
clearTimeout(waiter.timer);
|
|
112276
|
+
this.choiceWaiters.delete(promptId);
|
|
112277
|
+
waiter.resolve(index);
|
|
112278
|
+
}
|
|
112279
|
+
failChoices() {
|
|
112280
|
+
for (const [id, waiter] of [...this.choiceWaiters.entries()]) {
|
|
112281
|
+
clearTimeout(waiter.timer);
|
|
112282
|
+
this.choiceWaiters.delete(id);
|
|
112283
|
+
waiter.resolve(undefined);
|
|
112284
|
+
}
|
|
112285
|
+
this.earlyChoices.clear();
|
|
112286
|
+
}
|
|
112202
112287
|
failApprovals() {
|
|
112203
112288
|
for (const [id, waiter] of [...this.waiters.entries()]) {
|
|
112204
112289
|
clearTimeout(waiter.timer);
|
|
@@ -112206,6 +112291,7 @@ class RemoteClient {
|
|
|
112206
112291
|
waiter.resolve("deny");
|
|
112207
112292
|
}
|
|
112208
112293
|
this.earlyDecisions.clear();
|
|
112294
|
+
this.failChoices();
|
|
112209
112295
|
}
|
|
112210
112296
|
}
|
|
112211
112297
|
var UNVERIFIED_SERVE_MESSAGE = "the program answering on keryx serve's port did not prove it is the keryx serve this shell trusts; its answer was ignored. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides)", FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, MAX_COMPLETED_IDS = 256;
|
|
@@ -112226,6 +112312,539 @@ var init_client3 = __esm(() => {
|
|
|
112226
112312
|
};
|
|
112227
112313
|
});
|
|
112228
112314
|
|
|
112315
|
+
// src/remote/command-gateway.ts
|
|
112316
|
+
function menuName(name) {
|
|
112317
|
+
return name.replace(/-/g, "_");
|
|
112318
|
+
}
|
|
112319
|
+
function split(line) {
|
|
112320
|
+
const trimmed = line.trim();
|
|
112321
|
+
const space = trimmed.search(/\s/);
|
|
112322
|
+
const head = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
112323
|
+
const args = space === -1 ? "" : trimmed.slice(space).trim();
|
|
112324
|
+
const at = head.indexOf("@");
|
|
112325
|
+
const bare = (at === -1 ? head : head.slice(0, at)).replace(/^\/+/, "").toLowerCase();
|
|
112326
|
+
return { token: bare, args };
|
|
112327
|
+
}
|
|
112328
|
+
function refuse3(command, reason, known) {
|
|
112329
|
+
return { kind: "refuse", command, reason, known };
|
|
112330
|
+
}
|
|
112331
|
+
function refusalText(command, reason) {
|
|
112332
|
+
return `Not available remotely: /${command}. ${reason.charAt(0).toUpperCase()}${reason.slice(1)}${/[.!?]$/.test(reason) ? "" : "."}`;
|
|
112333
|
+
}
|
|
112334
|
+
function classifyRemoteCommand(line) {
|
|
112335
|
+
const { token, args } = split(line);
|
|
112336
|
+
if (token.length === 0) {
|
|
112337
|
+
return refuse3("", "an empty command", false);
|
|
112338
|
+
}
|
|
112339
|
+
const canonical = token.replace(/_/g, "-");
|
|
112340
|
+
const refusedReason = REMOTE_REFUSED[canonical] ?? REMOTE_REFUSED[token];
|
|
112341
|
+
const spec = BY_NAME.get(canonical) ?? BY_MENU.get(token);
|
|
112342
|
+
if (canonical === "remote-control") {
|
|
112343
|
+
const sub = args.split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
112344
|
+
return refuse3(canonical, sub === "off" ? "it would delete this topic; turn it off in the shell" : refusedReason ?? "its panel is local", true);
|
|
112345
|
+
}
|
|
112346
|
+
if (spec === undefined) {
|
|
112347
|
+
if (refusedReason !== undefined) {
|
|
112348
|
+
return refuse3(canonical, refusedReason, true);
|
|
112349
|
+
}
|
|
112350
|
+
return refuse3(canonical, "it is not on the list of commands a topic may run; /help shows the list", false);
|
|
112351
|
+
}
|
|
112352
|
+
const name = spec.name;
|
|
112353
|
+
const rest = args.trim();
|
|
112354
|
+
const first = rest.split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
112355
|
+
const asTyped = rest.length === 0 ? `/${name}` : `/${name} ${rest}`;
|
|
112356
|
+
switch (name) {
|
|
112357
|
+
case "mode":
|
|
112358
|
+
if (first === "trust" || first === "auto") {
|
|
112359
|
+
return { kind: "confirm", command: name, line: `/mode ${first}`, summary: `Switch the permission mode to ${first}?` };
|
|
112360
|
+
}
|
|
112361
|
+
if (first === "" || first === "ask") {
|
|
112362
|
+
return { kind: "run", command: name, line: asTyped };
|
|
112363
|
+
}
|
|
112364
|
+
return refuse3(name, "the mode is ask, trust or auto", true);
|
|
112365
|
+
case "plan":
|
|
112366
|
+
if (first === "off") {
|
|
112367
|
+
return { kind: "confirm", command: name, line: "/plan off", summary: "Turn read-only mode off?" };
|
|
112368
|
+
}
|
|
112369
|
+
if (first === "") {
|
|
112370
|
+
return { kind: "run", command: name, line: "/plan" };
|
|
112371
|
+
}
|
|
112372
|
+
return first === "on" ? { kind: "run", command: name, line: "/plan on" } : refuse3(name, "use /plan on or /plan off", true);
|
|
112373
|
+
case "theme":
|
|
112374
|
+
return first === "" ? refuse3(name, "bare /theme opens a picker in the shell; use /theme <name>", true) : { kind: "run", command: name, line: asTyped };
|
|
112375
|
+
case "think":
|
|
112376
|
+
return first === "auto" || first === "expand" || first === "hide" ? { kind: "run", command: name, line: `/think ${first}` } : refuse3(name, "bare /think and /think collapse only flip the view in the shell; use /think auto, expand or hide", true);
|
|
112377
|
+
case "delegate":
|
|
112378
|
+
return confirmDelegate(rest);
|
|
112379
|
+
case "external":
|
|
112380
|
+
if (first === "on" || first === "off") {
|
|
112381
|
+
return {
|
|
112382
|
+
kind: "confirm",
|
|
112383
|
+
command: name,
|
|
112384
|
+
line: `/external ${first}`,
|
|
112385
|
+
summary: `Switch sending private work to external providers ${first}? This changes what leaves this machine.`
|
|
112386
|
+
};
|
|
112387
|
+
}
|
|
112388
|
+
return first === "" ? { kind: "run", command: name, line: "/external" } : refuse3(name, "use /external, /external on or /external off", true);
|
|
112389
|
+
case "external-agents":
|
|
112390
|
+
if (first === "on" || first === "off") {
|
|
112391
|
+
return {
|
|
112392
|
+
kind: "confirm",
|
|
112393
|
+
command: name,
|
|
112394
|
+
line: `/external-agents ${first}`,
|
|
112395
|
+
summary: `Turn the external agent runtime ${first}? External agents are paid and run outside keryx.`
|
|
112396
|
+
};
|
|
112397
|
+
}
|
|
112398
|
+
return first === "" ? { kind: "run", command: name, line: "/external-agents" } : refuse3(name, "use /external_agents, /external_agents on or /external_agents off", true);
|
|
112399
|
+
default:
|
|
112400
|
+
break;
|
|
112401
|
+
}
|
|
112402
|
+
switch (spec.kind) {
|
|
112403
|
+
case "builtin":
|
|
112404
|
+
return { kind: "builtin", command: name, args: rest };
|
|
112405
|
+
case "picker":
|
|
112406
|
+
return { kind: "picker", command: name, args: rest };
|
|
112407
|
+
default:
|
|
112408
|
+
return { kind: "run", command: name, line: name === "new" || name === "clear" ? `/${name}` : asTyped };
|
|
112409
|
+
}
|
|
112410
|
+
}
|
|
112411
|
+
function confirmDelegate(rest) {
|
|
112412
|
+
const match = /^(\S+)\s+([\s\S]+)$/.exec(rest);
|
|
112413
|
+
if (match === null) {
|
|
112414
|
+
return refuse3("delegate", "it needs an agent and a task: /delegate <agent> <task>", true);
|
|
112415
|
+
}
|
|
112416
|
+
const agent = (match[1] ?? "").toLowerCase();
|
|
112417
|
+
const task = (match[2] ?? "").trim();
|
|
112418
|
+
const shown = task.length > 200 ? `${task.slice(0, 200)}... [shortened for this question: the full task is ${task.length} characters and runs as typed]` : task;
|
|
112419
|
+
return {
|
|
112420
|
+
kind: "confirm",
|
|
112421
|
+
command: "delegate",
|
|
112422
|
+
line: `/delegate ${match[1] ?? ""} ${task}`,
|
|
112423
|
+
agent,
|
|
112424
|
+
summary: `Hand this task to ${agent}? It is an external agent and runs outside keryx; the run is paid.
|
|
112425
|
+
Task: ${shown}`
|
|
112426
|
+
};
|
|
112427
|
+
}
|
|
112428
|
+
function remoteMenu() {
|
|
112429
|
+
return REMOTE_COMMANDS.map((spec) => ({ command: menuName(spec.name), description: spec.description.slice(0, 256) }));
|
|
112430
|
+
}
|
|
112431
|
+
function remoteHelpText() {
|
|
112432
|
+
const lines = ["Commands you can use from this topic:"];
|
|
112433
|
+
for (const spec of REMOTE_COMMANDS) {
|
|
112434
|
+
lines.push(`/${menuName(spec.name)} - ${spec.description}`);
|
|
112435
|
+
}
|
|
112436
|
+
lines.push("", "Anything else typed here goes to the model as a message. Other commands run in the shell.");
|
|
112437
|
+
return lines.join(`
|
|
112438
|
+
`);
|
|
112439
|
+
}
|
|
112440
|
+
function isBusyDeferred(name) {
|
|
112441
|
+
return BUSY_DEFERRED_COMMANDS.includes(name);
|
|
112442
|
+
}
|
|
112443
|
+
var REMOTE_COMMANDS, REMOTE_REFUSED, BUSY_DEFERRED_COMMANDS, BUSY_REASON = "main is busy: command deferred", BY_NAME, BY_MENU;
|
|
112444
|
+
var init_command_gateway = __esm(() => {
|
|
112445
|
+
REMOTE_COMMANDS = [
|
|
112446
|
+
{ name: "help", kind: "builtin", description: "List the commands you can use from this topic" },
|
|
112447
|
+
{ name: "status", kind: "text", description: "Show the session: model, mode, queue, usage" },
|
|
112448
|
+
{ name: "doctor", kind: "text", description: "Run the health checks" },
|
|
112449
|
+
{ name: "model", kind: "picker", description: "Pick another model with buttons" },
|
|
112450
|
+
{ name: "connect", kind: "picker", description: "Switch to a connected provider with buttons" },
|
|
112451
|
+
{ name: "resume", kind: "picker", description: "Return to an earlier session with buttons" },
|
|
112452
|
+
{ name: "sessions", kind: "builtin", description: "List recent sessions" },
|
|
112453
|
+
{ name: "new", kind: "text", description: "Start a new session; this topic stays bound to it" },
|
|
112454
|
+
{ name: "clear", kind: "text", description: "Same as /new" },
|
|
112455
|
+
{ name: "compact", kind: "text", description: "Compact the conversation: /compact [focus]" },
|
|
112456
|
+
{ name: "think", kind: "text", description: "Set how reasoning shows: /think <auto|expand|hide>" },
|
|
112457
|
+
{ name: "goal", kind: "text", description: "Show or set the session goal" },
|
|
112458
|
+
{ name: "queue", kind: "text", description: "Manage the queue: /queue <remove|edit|force> [N]" },
|
|
112459
|
+
{ name: "mode", kind: "confirm", description: "Show or set the permission mode; trust and auto ask for a button press" },
|
|
112460
|
+
{ name: "plan", kind: "confirm", description: "Read-only mode: /plan [on|off]; bare shows it, off asks for a button press" },
|
|
112461
|
+
{ name: "reasoning", kind: "text", description: "Show or set reasoning effort" },
|
|
112462
|
+
{ name: "theme", kind: "text", description: "Set the theme: /theme <name>" },
|
|
112463
|
+
{ name: "jevrules", kind: "text", description: "Show the Jev rules" },
|
|
112464
|
+
{ name: "staledocs", kind: "text", description: "Show stale documents" },
|
|
112465
|
+
{ name: "opencomments", kind: "text", description: "Show open review comments" },
|
|
112466
|
+
{ name: "contract", kind: "text", description: "Show the contract check" },
|
|
112467
|
+
{ name: "triage", kind: "text", description: "Advisory triage of the latest review package" },
|
|
112468
|
+
{ name: "risk", kind: "text", description: "Risk map of the working diff" },
|
|
112469
|
+
{ name: "scenarios", kind: "text", description: "User scenarios the working diff likely changes" },
|
|
112470
|
+
{ name: "delegate", kind: "confirm", description: "Hand a paid task to an external agent: /delegate <agent> <task>; asks first" },
|
|
112471
|
+
{ name: "external", kind: "confirm", description: "Show or switch sending work to external providers; on and off ask first" },
|
|
112472
|
+
{ name: "external-agents", kind: "confirm", description: "Show or switch the external agent runtime; on and off ask first" }
|
|
112473
|
+
];
|
|
112474
|
+
REMOTE_REFUSED = {
|
|
112475
|
+
exit: "it would close the shell",
|
|
112476
|
+
quit: "it would close the shell",
|
|
112477
|
+
channels: "it manages the Telegram connection itself",
|
|
112478
|
+
provider: "it asks for provider keys and addresses, which are never typed into a chat",
|
|
112479
|
+
"search-provider": "it asks for provider keys, which are never typed into a chat",
|
|
112480
|
+
"search-connect": "it changes where searches are sent",
|
|
112481
|
+
"remote-control": "turning remote control off would delete this topic, and its panel is local",
|
|
112482
|
+
integrate: "it edits other tools' configuration on this machine",
|
|
112483
|
+
copy: "it uses this machine's clipboard",
|
|
112484
|
+
game: "it needs the shell's screen",
|
|
112485
|
+
setup: "it asks for keys and changes the configuration",
|
|
112486
|
+
mcp: "it can trust tools and change server connections; use the shell",
|
|
112487
|
+
guard: "it changes a safety guard; use the shell",
|
|
112488
|
+
route: "it changes where work is routed; use the shell",
|
|
112489
|
+
editguard: "it changes a safety guard; use the shell",
|
|
112490
|
+
settings: "it edits settings, some of them secret; use the shell",
|
|
112491
|
+
bus: "it talks to other agents on this machine",
|
|
112492
|
+
review: "it needs the shell's screen",
|
|
112493
|
+
reviews: "it needs the shell's screen",
|
|
112494
|
+
product: "it needs the shell's screen",
|
|
112495
|
+
governance: "it needs the shell's screen",
|
|
112496
|
+
expand: "it needs the shell's screen",
|
|
112497
|
+
interrupt: "use the Stop button on a message, or stop it in the shell",
|
|
112498
|
+
demote: "it works on the shell's own tasks",
|
|
112499
|
+
models: "use /model",
|
|
112500
|
+
"external-diff": "listing and applying external patches stays in the shell, where the patch hash is typed",
|
|
112501
|
+
flows: "it opens a panel in the shell",
|
|
112502
|
+
ac: "it opens a panel in the shell",
|
|
112503
|
+
workspace: "it opens a panel in the shell",
|
|
112504
|
+
approvals: "it opens a panel in the shell",
|
|
112505
|
+
triggers: "it opens a panel in the shell",
|
|
112506
|
+
routing: "it opens a panel in the shell",
|
|
112507
|
+
schedules: "it opens a panel in the shell",
|
|
112508
|
+
schedule: "it opens a form in the shell",
|
|
112509
|
+
rewind: "it opens a picker of earlier points in the shell",
|
|
112510
|
+
conform: "it opens a picker in the shell",
|
|
112511
|
+
ci: "it opens a panel in the shell",
|
|
112512
|
+
jevprofile: "it opens a panel with toggles in the shell"
|
|
112513
|
+
};
|
|
112514
|
+
BUSY_DEFERRED_COMMANDS = ["new", "clear", "resume", "sessions", "compact", "model", "connect"];
|
|
112515
|
+
BY_NAME = new Map(REMOTE_COMMANDS.map((spec) => [spec.name, spec]));
|
|
112516
|
+
BY_MENU = new Map(REMOTE_COMMANDS.map((spec) => [menuName(spec.name), spec]));
|
|
112517
|
+
});
|
|
112518
|
+
|
|
112519
|
+
// src/remote/command-router.ts
|
|
112520
|
+
function ended(end, result) {
|
|
112521
|
+
try {
|
|
112522
|
+
end?.(result);
|
|
112523
|
+
} catch {}
|
|
112524
|
+
}
|
|
112525
|
+
|
|
112526
|
+
class RemoteCommandRouter {
|
|
112527
|
+
ctx;
|
|
112528
|
+
chain = Promise.resolve();
|
|
112529
|
+
detached = 0;
|
|
112530
|
+
detachedDone = [];
|
|
112531
|
+
constructor(ctx) {
|
|
112532
|
+
this.ctx = ctx;
|
|
112533
|
+
}
|
|
112534
|
+
handle(line, fromId, end) {
|
|
112535
|
+
const decision = classifyRemoteCommand(line);
|
|
112536
|
+
switch (decision.kind) {
|
|
112537
|
+
case "refuse":
|
|
112538
|
+
this.refuse(decision);
|
|
112539
|
+
ended(end, "failed");
|
|
112540
|
+
return;
|
|
112541
|
+
case "builtin":
|
|
112542
|
+
this.ctx.record(`/${decision.command}`);
|
|
112543
|
+
if (decision.command === "sessions") {
|
|
112544
|
+
this.detach(async () => {
|
|
112545
|
+
await this.listSessionsText();
|
|
112546
|
+
ended(end, "done");
|
|
112547
|
+
});
|
|
112548
|
+
} else {
|
|
112549
|
+
this.ctx.reply(this.ctx.compose(remoteHelpText())).catch(() => false);
|
|
112550
|
+
ended(end, "done");
|
|
112551
|
+
}
|
|
112552
|
+
return;
|
|
112553
|
+
case "run":
|
|
112554
|
+
this.enqueue(decision.line, decision.command, end);
|
|
112555
|
+
return;
|
|
112556
|
+
case "picker":
|
|
112557
|
+
this.detach(async () => {
|
|
112558
|
+
const result = await this.picker(decision.command, fromId, end);
|
|
112559
|
+
if (result !== "deferred") {
|
|
112560
|
+
ended(end, result);
|
|
112561
|
+
}
|
|
112562
|
+
});
|
|
112563
|
+
return;
|
|
112564
|
+
case "confirm":
|
|
112565
|
+
this.detach(async () => {
|
|
112566
|
+
const result = await this.confirm(decision, fromId, end);
|
|
112567
|
+
if (result !== "deferred") {
|
|
112568
|
+
ended(end, result);
|
|
112569
|
+
}
|
|
112570
|
+
});
|
|
112571
|
+
return;
|
|
112572
|
+
}
|
|
112573
|
+
}
|
|
112574
|
+
async idle() {
|
|
112575
|
+
await this.chain;
|
|
112576
|
+
if (this.detached > 0) {
|
|
112577
|
+
await new Promise((resolve) => this.detachedDone.push(resolve));
|
|
112578
|
+
await this.idle();
|
|
112579
|
+
}
|
|
112580
|
+
}
|
|
112581
|
+
detach(work) {
|
|
112582
|
+
this.detached += 1;
|
|
112583
|
+
work().catch(() => {
|
|
112584
|
+
return;
|
|
112585
|
+
}).finally(() => {
|
|
112586
|
+
this.detached -= 1;
|
|
112587
|
+
if (this.detached === 0) {
|
|
112588
|
+
for (const done of this.detachedDone.splice(0)) {
|
|
112589
|
+
done();
|
|
112590
|
+
}
|
|
112591
|
+
}
|
|
112592
|
+
});
|
|
112593
|
+
}
|
|
112594
|
+
refuse(decision) {
|
|
112595
|
+
const shown = decision.command.length === 0 ? "/" : `/${decision.command}`;
|
|
112596
|
+
this.ctx.record(`refused ${shown}: ${decision.reason}`);
|
|
112597
|
+
this.ctx.reply(refusalText(decision.command, decision.reason)).catch(() => false);
|
|
112598
|
+
}
|
|
112599
|
+
enqueue(line, command, end) {
|
|
112600
|
+
this.chain = this.chain.then(() => this.execute(command, line, async () => this.ctx.host.runCommand === undefined ? undefined : this.ctx.host.runCommand(line), end)).catch(() => {
|
|
112601
|
+
return;
|
|
112602
|
+
});
|
|
112603
|
+
}
|
|
112604
|
+
busyReason(line, command) {
|
|
112605
|
+
if (!this.ctx.host.isBusy()) {
|
|
112606
|
+
return;
|
|
112607
|
+
}
|
|
112608
|
+
const fromHost = this.ctx.host.busyRefusal?.(line);
|
|
112609
|
+
if (fromHost !== undefined) {
|
|
112610
|
+
return fromHost;
|
|
112611
|
+
}
|
|
112612
|
+
return this.ctx.host.busyRefusal === undefined && isBusyDeferred(command) ? BUSY_REASON : undefined;
|
|
112613
|
+
}
|
|
112614
|
+
async execute(command, line, work, end) {
|
|
112615
|
+
const { limits } = this.ctx;
|
|
112616
|
+
const busy = this.busyReason(line, command);
|
|
112617
|
+
if (busy !== undefined) {
|
|
112618
|
+
this.ctx.record(`/${command} not run: ${busy}`);
|
|
112619
|
+
ended(end, "failed");
|
|
112620
|
+
await this.ctx.reply(`/${command} was not run: ${busy}.`).catch(() => false);
|
|
112621
|
+
return;
|
|
112622
|
+
}
|
|
112623
|
+
this.ctx.record(`/${command} started`);
|
|
112624
|
+
const holds = SESSION_COMMANDS.includes(command);
|
|
112625
|
+
if (holds) {
|
|
112626
|
+
this.ctx.holdTopic(true);
|
|
112627
|
+
}
|
|
112628
|
+
let finished = false;
|
|
112629
|
+
const notice = setTimeout(() => {
|
|
112630
|
+
if (!finished) {
|
|
112631
|
+
this.ctx.reply(`Still running /${command}...`).catch(() => false);
|
|
112632
|
+
}
|
|
112633
|
+
}, limits.runningNoticeMs);
|
|
112634
|
+
let limitTimer;
|
|
112635
|
+
const limit = new Promise((resolve) => {
|
|
112636
|
+
limitTimer = setTimeout(() => resolve("limit"), limits.commandLimitMs);
|
|
112637
|
+
});
|
|
112638
|
+
let outcome;
|
|
112639
|
+
try {
|
|
112640
|
+
outcome = await Promise.race([work(), limit]);
|
|
112641
|
+
} catch (error) {
|
|
112642
|
+
outcome = { output: error instanceof Error ? error.message : String(error), ok: false };
|
|
112643
|
+
} finally {
|
|
112644
|
+
finished = true;
|
|
112645
|
+
clearTimeout(notice);
|
|
112646
|
+
if (limitTimer !== undefined) {
|
|
112647
|
+
clearTimeout(limitTimer);
|
|
112648
|
+
}
|
|
112649
|
+
if (holds) {
|
|
112650
|
+
this.ctx.holdTopic(false);
|
|
112651
|
+
}
|
|
112652
|
+
}
|
|
112653
|
+
if (outcome === "limit") {
|
|
112654
|
+
this.ctx.record(`/${command} still running: stopped waiting after the time limit`);
|
|
112655
|
+
ended(end, "failed");
|
|
112656
|
+
await this.ctx.reply(`/${command} is still running in the shell. Stopped waiting for it after ${Math.max(1, Math.round(limits.commandLimitMs / 1000))} seconds.`).catch(() => false);
|
|
112657
|
+
return;
|
|
112658
|
+
}
|
|
112659
|
+
if (outcome === undefined) {
|
|
112660
|
+
this.ctx.record(`/${command} not run: this shell cannot run it from the topic`);
|
|
112661
|
+
ended(end, "failed");
|
|
112662
|
+
await this.ctx.reply(`/${command} was not run: this shell cannot run it from the topic.`).catch(() => false);
|
|
112663
|
+
return;
|
|
112664
|
+
}
|
|
112665
|
+
this.ctx.record(`/${command} ${outcome.ok ? "done" : "failed"}`);
|
|
112666
|
+
ended(end, outcome.ok ? "done" : "failed");
|
|
112667
|
+
if (this.ctx.takeSwitched()) {
|
|
112668
|
+
return;
|
|
112669
|
+
}
|
|
112670
|
+
const text = outcome.output.trim();
|
|
112671
|
+
if (text.length === 0) {
|
|
112672
|
+
await this.ctx.reply(outcome.ok ? `/${command}: done. It printed nothing.` : `/${command} failed. The details are in the shell.`).catch(() => false);
|
|
112673
|
+
return;
|
|
112674
|
+
}
|
|
112675
|
+
await this.ctx.reply(this.ctx.compose(outcome.ok ? text : `/${command} failed:
|
|
112676
|
+
${text}`)).catch(() => false);
|
|
112677
|
+
}
|
|
112678
|
+
async confirm(decision, fromId, end) {
|
|
112679
|
+
const yes = decision.agent !== undefined ? `Yes, send to ${decision.agent} (paid)` : "Yes";
|
|
112680
|
+
this.ctx.record(`/${decision.command} waiting for a press in the topic`);
|
|
112681
|
+
const index = await this.ctx.choose(this.ctx.compose(decision.summary), [[this.label(yes), "No"]], this.ctx.limits.choiceTimeoutMs, fromId);
|
|
112682
|
+
if (index === 0) {
|
|
112683
|
+
this.ctx.record(`/${decision.command} confirmed in the topic`);
|
|
112684
|
+
this.enqueue(decision.line, decision.command, end);
|
|
112685
|
+
return "deferred";
|
|
112686
|
+
}
|
|
112687
|
+
this.ctx.record(index === 1 ? `/${decision.command} declined in the topic; nothing changed` : `/${decision.command} not confirmed in time; nothing changed`);
|
|
112688
|
+
return index === 1 ? "done" : "failed";
|
|
112689
|
+
}
|
|
112690
|
+
label(text) {
|
|
112691
|
+
const one = this.ctx.compose(text).replace(/\s+/g, " ").trim();
|
|
112692
|
+
const shown = one.length === 0 ? "(unnamed)" : one;
|
|
112693
|
+
return shown.length > LABEL_CHARS ? `${shown.slice(0, LABEL_CHARS - 3)}...` : shown;
|
|
112694
|
+
}
|
|
112695
|
+
async pick(title, options, fromId) {
|
|
112696
|
+
const pages = Math.max(1, Math.ceil(options.length / PICKER_PAGE_SIZE));
|
|
112697
|
+
let page = 0;
|
|
112698
|
+
for (;; ) {
|
|
112699
|
+
const slice = options.slice(page * PICKER_PAGE_SIZE, (page + 1) * PICKER_PAGE_SIZE);
|
|
112700
|
+
const rows = slice.map((option) => [this.label(option.label)]);
|
|
112701
|
+
const nav = [];
|
|
112702
|
+
if (page > 0) {
|
|
112703
|
+
nav.push("Previous");
|
|
112704
|
+
}
|
|
112705
|
+
if (page < pages - 1) {
|
|
112706
|
+
nav.push("More");
|
|
112707
|
+
}
|
|
112708
|
+
if (nav.length > 0) {
|
|
112709
|
+
rows.push([...nav]);
|
|
112710
|
+
}
|
|
112711
|
+
const text = pages > 1 ? `${title}
|
|
112712
|
+
Page ${page + 1} of ${pages}` : title;
|
|
112713
|
+
const index = await this.ctx.choose(this.ctx.compose(text), rows, this.ctx.limits.choiceTimeoutMs, fromId);
|
|
112714
|
+
if (index === undefined) {
|
|
112715
|
+
return;
|
|
112716
|
+
}
|
|
112717
|
+
if (index < slice.length) {
|
|
112718
|
+
return slice[index];
|
|
112719
|
+
}
|
|
112720
|
+
const pressed = nav[index - slice.length];
|
|
112721
|
+
page += pressed === "More" ? 1 : -1;
|
|
112722
|
+
}
|
|
112723
|
+
}
|
|
112724
|
+
async picker(command, fromId, end) {
|
|
112725
|
+
const line = `/${command}`;
|
|
112726
|
+
const busy = this.busyReason(line, command);
|
|
112727
|
+
if (busy !== undefined) {
|
|
112728
|
+
this.ctx.record(`${line} not run: ${busy}`);
|
|
112729
|
+
await this.ctx.reply(`${line} was not run: ${busy}.`).catch(() => false);
|
|
112730
|
+
return "failed";
|
|
112731
|
+
}
|
|
112732
|
+
this.ctx.record(line);
|
|
112733
|
+
switch (command) {
|
|
112734
|
+
case "model":
|
|
112735
|
+
return await this.pickModel(undefined, fromId);
|
|
112736
|
+
case "connect":
|
|
112737
|
+
return await this.pickProvider(fromId);
|
|
112738
|
+
case "resume":
|
|
112739
|
+
return await this.pickSession(fromId, end);
|
|
112740
|
+
default:
|
|
112741
|
+
await this.ctx.reply(`${line} is not available from this shell.`).catch(() => false);
|
|
112742
|
+
return "failed";
|
|
112743
|
+
}
|
|
112744
|
+
}
|
|
112745
|
+
async pickModel(providerId, fromId) {
|
|
112746
|
+
const host = this.ctx.host;
|
|
112747
|
+
if (host.listModels === undefined || host.switchModel === undefined) {
|
|
112748
|
+
await this.ctx.reply("/model is not available from this shell.").catch(() => false);
|
|
112749
|
+
return "failed";
|
|
112750
|
+
}
|
|
112751
|
+
const listing = await host.listModels(providerId);
|
|
112752
|
+
if (listing === undefined || listing.models.length === 0) {
|
|
112753
|
+
await this.ctx.reply(this.ctx.compose(`There are no models to pick from${providerId !== undefined ? ` for ${providerId}` : ""}. Use the shell to connect a provider.`)).catch(() => false);
|
|
112754
|
+
return "failed";
|
|
112755
|
+
}
|
|
112756
|
+
const current = listing.models.find((model) => model.current === true);
|
|
112757
|
+
const title = `Pick a model (${listing.provider})${current !== undefined ? `
|
|
112758
|
+
Now: ${current.label}` : ""}`;
|
|
112759
|
+
const options = listing.models.map((model) => ({ id: model.id, label: model.current === true ? `* ${model.label}` : model.label }));
|
|
112760
|
+
const chosen = await this.pick(title, options, fromId);
|
|
112761
|
+
if (chosen === undefined) {
|
|
112762
|
+
this.ctx.record("/model not answered in time; nothing changed");
|
|
112763
|
+
return "failed";
|
|
112764
|
+
}
|
|
112765
|
+
const line = "/model";
|
|
112766
|
+
const busy = this.busyReason(line, "model");
|
|
112767
|
+
if (busy !== undefined) {
|
|
112768
|
+
await this.ctx.reply(`/model was not run: ${busy}.`).catch(() => false);
|
|
112769
|
+
return "failed";
|
|
112770
|
+
}
|
|
112771
|
+
const outcome = await host.switchModel(chosen.id, providerId);
|
|
112772
|
+
this.ctx.record(`/model ${outcome.ok ? "switched" : "failed"}`);
|
|
112773
|
+
const text = outcome.output.trim();
|
|
112774
|
+
await this.ctx.reply(this.ctx.compose(text.length > 0 ? text : outcome.ok ? `Model: ${chosen.label.replace(/^\* /, "")}.` : "The model was not changed.")).catch(() => false);
|
|
112775
|
+
return outcome.ok ? "done" : "failed";
|
|
112776
|
+
}
|
|
112777
|
+
async pickProvider(fromId) {
|
|
112778
|
+
const host = this.ctx.host;
|
|
112779
|
+
if (host.listProviders === undefined || host.listModels === undefined || host.switchModel === undefined) {
|
|
112780
|
+
await this.ctx.reply("/connect is not available from this shell.").catch(() => false);
|
|
112781
|
+
return "failed";
|
|
112782
|
+
}
|
|
112783
|
+
const providers = await host.listProviders();
|
|
112784
|
+
if (providers.length === 0) {
|
|
112785
|
+
await this.ctx.reply("No provider is connected yet. Connecting one needs a key, which is never typed into a chat; use the shell.").catch(() => false);
|
|
112786
|
+
return "failed";
|
|
112787
|
+
}
|
|
112788
|
+
const options = providers.map((provider) => ({ id: provider.id, label: provider.current === true ? `* ${provider.label}` : provider.label }));
|
|
112789
|
+
const chosen = await this.pick("Connected providers. Pick one to switch to:", options, fromId);
|
|
112790
|
+
if (chosen === undefined) {
|
|
112791
|
+
this.ctx.record("/connect not answered in time; nothing changed");
|
|
112792
|
+
return "failed";
|
|
112793
|
+
}
|
|
112794
|
+
return await this.pickModel(chosen.id, fromId);
|
|
112795
|
+
}
|
|
112796
|
+
async pickSession(fromId, end) {
|
|
112797
|
+
const host = this.ctx.host;
|
|
112798
|
+
if (host.listSessions === undefined || host.resumeSession === undefined) {
|
|
112799
|
+
await this.ctx.reply("/resume is not available from this shell.").catch(() => false);
|
|
112800
|
+
return "failed";
|
|
112801
|
+
}
|
|
112802
|
+
const sessions = (await host.listSessions()).filter((session) => session.current !== true);
|
|
112803
|
+
if (sessions.length === 0) {
|
|
112804
|
+
await this.ctx.reply("There is no earlier session to return to.").catch(() => false);
|
|
112805
|
+
return "done";
|
|
112806
|
+
}
|
|
112807
|
+
const chosen = await this.pick("Return to an earlier session:", sessions.map((s) => ({ id: s.id, label: s.label })), fromId);
|
|
112808
|
+
if (chosen === undefined) {
|
|
112809
|
+
this.ctx.record("/resume not answered in time; nothing changed");
|
|
112810
|
+
return "failed";
|
|
112811
|
+
}
|
|
112812
|
+
const resume = host.resumeSession.bind(host);
|
|
112813
|
+
this.chain = this.chain.then(() => this.execute("resume", "/resume", () => resume(chosen.id), end)).catch(() => {
|
|
112814
|
+
return;
|
|
112815
|
+
});
|
|
112816
|
+
return "deferred";
|
|
112817
|
+
}
|
|
112818
|
+
async listSessionsText() {
|
|
112819
|
+
const host = this.ctx.host;
|
|
112820
|
+
const busy = this.busyReason("/sessions", "sessions");
|
|
112821
|
+
if (busy !== undefined) {
|
|
112822
|
+
this.ctx.record(`/sessions not run: ${busy}`);
|
|
112823
|
+
await this.ctx.reply(`/sessions was not run: ${busy}.`).catch(() => false);
|
|
112824
|
+
return;
|
|
112825
|
+
}
|
|
112826
|
+
if (host.listSessions === undefined) {
|
|
112827
|
+
await this.ctx.reply("/sessions is not available from this shell.").catch(() => false);
|
|
112828
|
+
return;
|
|
112829
|
+
}
|
|
112830
|
+
const sessions = await host.listSessions();
|
|
112831
|
+
if (sessions.length === 0) {
|
|
112832
|
+
await this.ctx.reply("There are no recorded sessions yet.").catch(() => false);
|
|
112833
|
+
return;
|
|
112834
|
+
}
|
|
112835
|
+
const lines = sessions.slice(0, 20).map((session) => `${session.current === true ? "* " : "- "}${session.label}`);
|
|
112836
|
+
await this.ctx.reply(this.ctx.compose(`Recent sessions:
|
|
112837
|
+
${lines.join(`
|
|
112838
|
+
`)}`)).catch(() => false);
|
|
112839
|
+
}
|
|
112840
|
+
}
|
|
112841
|
+
var DEFAULT_ROUTER_LIMITS, PICKER_PAGE_SIZE = 7, LABEL_CHARS = 56, SESSION_COMMANDS;
|
|
112842
|
+
var init_command_router = __esm(() => {
|
|
112843
|
+
init_command_gateway();
|
|
112844
|
+
DEFAULT_ROUTER_LIMITS = { runningNoticeMs: 4000, commandLimitMs: 120000, choiceTimeoutMs: 120000 };
|
|
112845
|
+
SESSION_COMMANDS = ["new", "clear", "resume"];
|
|
112846
|
+
});
|
|
112847
|
+
|
|
112229
112848
|
// src/remote/shell-bridge.ts
|
|
112230
112849
|
function preview2(text) {
|
|
112231
112850
|
const one = redactSensitiveText(text).replace(/\s+/g, " ").trim();
|
|
@@ -112287,11 +112906,59 @@ class RemoteBridge {
|
|
|
112287
112906
|
runTimer;
|
|
112288
112907
|
timedOut = false;
|
|
112289
112908
|
closing;
|
|
112909
|
+
router;
|
|
112910
|
+
keepTopic = false;
|
|
112911
|
+
switched = false;
|
|
112912
|
+
waiting = [];
|
|
112913
|
+
currentUpdate;
|
|
112290
112914
|
constructor(options) {
|
|
112291
112915
|
this.options = options;
|
|
112292
112916
|
this.host = options.host;
|
|
112293
112917
|
this.now = options.now ?? Date.now;
|
|
112294
112918
|
this.approvalTimeoutMs = options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
112919
|
+
this.router = new RemoteCommandRouter({
|
|
112920
|
+
host: {
|
|
112921
|
+
isBusy: () => this.host.isBusy(),
|
|
112922
|
+
cancelTurn: () => this.host.cancelTurn(),
|
|
112923
|
+
...this.host.runCommand !== undefined ? { runCommand: (line) => this.host.runCommand(line) } : {},
|
|
112924
|
+
...this.host.busyRefusal !== undefined ? { busyRefusal: (line) => this.host.busyRefusal(line) } : {},
|
|
112925
|
+
...this.host.listModels !== undefined ? { listModels: (id) => this.host.listModels(id) } : {},
|
|
112926
|
+
...this.host.listProviders !== undefined ? { listProviders: () => this.host.listProviders() } : {},
|
|
112927
|
+
...this.host.switchModel !== undefined ? { switchModel: (model, provider) => this.host.switchModel(model, provider) } : {},
|
|
112928
|
+
...this.host.listSessions !== undefined ? { listSessions: () => this.host.listSessions() } : {},
|
|
112929
|
+
...this.host.resumeSession !== undefined ? { resumeSession: (id) => this.host.resumeSession(id) } : {}
|
|
112930
|
+
},
|
|
112931
|
+
reply: async (text) => this.client !== undefined ? await this.client.reply(text) : false,
|
|
112932
|
+
record: (text) => this.push("command", text),
|
|
112933
|
+
compose: composeReply,
|
|
112934
|
+
choose: async (text, rows, timeoutMs, forUserId) => {
|
|
112935
|
+
const client = this.client;
|
|
112936
|
+
if (client === undefined || !client.connected) {
|
|
112937
|
+
return;
|
|
112938
|
+
}
|
|
112939
|
+
try {
|
|
112940
|
+
return await client.requestChoice(text, rows, timeoutMs, forUserId);
|
|
112941
|
+
} catch {
|
|
112942
|
+
return;
|
|
112943
|
+
}
|
|
112944
|
+
},
|
|
112945
|
+
holdTopic: (on) => {
|
|
112946
|
+
this.keepTopic = on;
|
|
112947
|
+
if (on) {
|
|
112948
|
+
this.switched = false;
|
|
112949
|
+
}
|
|
112950
|
+
},
|
|
112951
|
+
takeSwitched: () => {
|
|
112952
|
+
const was = this.switched;
|
|
112953
|
+
this.switched = false;
|
|
112954
|
+
return was;
|
|
112955
|
+
},
|
|
112956
|
+
limits: {
|
|
112957
|
+
runningNoticeMs: options.commandRunningNoticeMs ?? DEFAULT_ROUTER_LIMITS.runningNoticeMs,
|
|
112958
|
+
commandLimitMs: options.commandLimitMs ?? DEFAULT_ROUTER_LIMITS.commandLimitMs,
|
|
112959
|
+
choiceTimeoutMs: options.choiceTimeoutMs ?? DEFAULT_ROUTER_LIMITS.choiceTimeoutMs
|
|
112960
|
+
}
|
|
112961
|
+
});
|
|
112295
112962
|
}
|
|
112296
112963
|
get active() {
|
|
112297
112964
|
return this.client !== undefined;
|
|
@@ -112367,6 +113034,7 @@ class RemoteBridge {
|
|
|
112367
113034
|
this.client = undefined;
|
|
112368
113035
|
this.clientConnected = false;
|
|
112369
113036
|
this.endTurnState();
|
|
113037
|
+
this.waiting.length = 0;
|
|
112370
113038
|
this.host.recordOff();
|
|
112371
113039
|
const dropped = this.host.dropQueuedTelegramLines?.() ?? [];
|
|
112372
113040
|
if (dropped.length > 0) {
|
|
@@ -112406,26 +113074,73 @@ class RemoteBridge {
|
|
|
112406
113074
|
this.push("status", `stopped${status.reason !== undefined ? `: ${preview2(status.reason)}` : ""}`);
|
|
112407
113075
|
}
|
|
112408
113076
|
}
|
|
112409
|
-
accept(text,
|
|
113077
|
+
accept(text, meta) {
|
|
112410
113078
|
const line = text.trim();
|
|
112411
113079
|
if (line.length === 0 || this.client === undefined)
|
|
112412
113080
|
return;
|
|
112413
113081
|
if (line.startsWith("/")) {
|
|
112414
|
-
this.push("line", `Telegram:
|
|
112415
|
-
this.
|
|
113082
|
+
this.push("line", `Telegram: ${preview2(line)}`);
|
|
113083
|
+
this.router.handle(line, meta.fromId, (result) => this.report(meta.updateId, result));
|
|
112416
113084
|
return;
|
|
112417
113085
|
}
|
|
112418
113086
|
this.push("line", `Telegram: ${preview2(line)}`);
|
|
113087
|
+
this.waiting.push({ updateId: meta.updateId, line });
|
|
112419
113088
|
if (this.host.isBusy()) {
|
|
112420
113089
|
this.host.enqueue(line);
|
|
112421
113090
|
} else {
|
|
113091
|
+
this.report(meta.updateId, "reading");
|
|
112422
113092
|
this.host.runLine(line);
|
|
112423
113093
|
}
|
|
112424
113094
|
}
|
|
113095
|
+
report(updateId, state) {
|
|
113096
|
+
const client = this.client;
|
|
113097
|
+
if (client?.reportState === undefined)
|
|
113098
|
+
return;
|
|
113099
|
+
const previous = this.reportTails.get(updateId);
|
|
113100
|
+
const post = client.reportState.bind(client);
|
|
113101
|
+
const started = previous === undefined ? post(updateId, state) : previous.then(() => post(updateId, state));
|
|
113102
|
+
const tail = started.then(() => {
|
|
113103
|
+
return;
|
|
113104
|
+
}, () => {
|
|
113105
|
+
return;
|
|
113106
|
+
}).then(() => {
|
|
113107
|
+
if (this.reportTails.get(updateId) === tail)
|
|
113108
|
+
this.reportTails.delete(updateId);
|
|
113109
|
+
});
|
|
113110
|
+
this.reportTails.set(updateId, tail);
|
|
113111
|
+
}
|
|
113112
|
+
reportTails = new Map;
|
|
113113
|
+
idle() {
|
|
113114
|
+
return this.router.idle();
|
|
113115
|
+
}
|
|
113116
|
+
get keepingTopic() {
|
|
113117
|
+
return this.keepTopic && this.client !== undefined;
|
|
113118
|
+
}
|
|
113119
|
+
sessionLeaving() {
|
|
113120
|
+
if (this.client === undefined)
|
|
113121
|
+
return;
|
|
113122
|
+
this.endTurnState();
|
|
113123
|
+
this.host.recordOff();
|
|
113124
|
+
this.push("status", "session changing; this topic stays bound");
|
|
113125
|
+
}
|
|
113126
|
+
sessionEntered(kind = "new") {
|
|
113127
|
+
const client = this.client;
|
|
113128
|
+
if (client === undefined)
|
|
113129
|
+
return;
|
|
113130
|
+
this.switched = true;
|
|
113131
|
+
if (client.name !== undefined)
|
|
113132
|
+
this.host.recordOn(client.name);
|
|
113133
|
+
this.push("status", kind === "new" ? "new session; same topic" : "resumed session; same topic");
|
|
113134
|
+
client.reply(kind === "new" ? "--- new session ---" : "--- resumed session ---").catch(() => false);
|
|
113135
|
+
}
|
|
112425
113136
|
turnStarted(source) {
|
|
112426
113137
|
this.endTurnState();
|
|
112427
113138
|
if (source !== TG_SOURCE || this.client === undefined)
|
|
112428
113139
|
return;
|
|
113140
|
+
const next = this.waiting.shift();
|
|
113141
|
+
this.currentUpdate = next?.updateId;
|
|
113142
|
+
if (next !== undefined)
|
|
113143
|
+
this.report(next.updateId, "working");
|
|
112429
113144
|
this.tgTurn = true;
|
|
112430
113145
|
this.lastAssistantText = "";
|
|
112431
113146
|
this.timedOut = false;
|
|
@@ -112454,6 +113169,12 @@ class RemoteBridge {
|
|
|
112454
113169
|
if (client === undefined)
|
|
112455
113170
|
return;
|
|
112456
113171
|
this.push("line", `Telegram line removed from the queue: ${preview2(line)}`);
|
|
113172
|
+
const index = this.waiting.findIndex((entry) => entry.line === line.trim());
|
|
113173
|
+
if (index >= 0) {
|
|
113174
|
+
const [gone] = this.waiting.splice(index, 1);
|
|
113175
|
+
if (gone !== undefined)
|
|
113176
|
+
this.report(gone.updateId, "failed");
|
|
113177
|
+
}
|
|
112457
113178
|
client.reply(`Your message "${preview2(line)}" was removed from the queue in the shell, so it will not run.`).catch(() => false);
|
|
112458
113179
|
}
|
|
112459
113180
|
async turnSettled(outcome) {
|
|
@@ -112462,9 +113183,12 @@ class RemoteBridge {
|
|
|
112462
113183
|
const client = this.client;
|
|
112463
113184
|
const text = this.lastAssistantText;
|
|
112464
113185
|
const timedOut = this.timedOut;
|
|
113186
|
+
const updateId = this.currentUpdate;
|
|
112465
113187
|
this.endTurnState();
|
|
112466
113188
|
if (client === undefined)
|
|
112467
113189
|
return;
|
|
113190
|
+
if (updateId !== undefined)
|
|
113191
|
+
this.report(updateId, outcome.failed || timedOut ? "failed" : "done");
|
|
112468
113192
|
let body;
|
|
112469
113193
|
if (timedOut) {
|
|
112470
113194
|
body = "Stopped: this run went over the time limit for runs started from Telegram.";
|
|
@@ -112480,6 +113204,7 @@ class RemoteBridge {
|
|
|
112480
113204
|
}
|
|
112481
113205
|
endTurnState() {
|
|
112482
113206
|
this.tgTurn = false;
|
|
113207
|
+
this.currentUpdate = undefined;
|
|
112483
113208
|
this.lastAssistantText = "";
|
|
112484
113209
|
this.timedOut = false;
|
|
112485
113210
|
if (this.runTimer !== undefined) {
|
|
@@ -112508,6 +113233,7 @@ var TG_SOURCE = "tg", REMOTE_EVENT_LIMIT = 20, REMOTE_REPLY_MAX_CHARS = 16000, E
|
|
|
112508
113233
|
var init_shell_bridge = __esm(() => {
|
|
112509
113234
|
init_service2();
|
|
112510
113235
|
init_client3();
|
|
113236
|
+
init_command_router();
|
|
112511
113237
|
init_protocol2();
|
|
112512
113238
|
});
|
|
112513
113239
|
|
|
@@ -113995,6 +114721,9 @@ var init_channels_surface = __esm(() => {
|
|
|
113995
114721
|
});
|
|
113996
114722
|
|
|
113997
114723
|
// src/tui/remote-control-surface.ts
|
|
114724
|
+
function commandEchoText(line, source) {
|
|
114725
|
+
return source === TG_SOURCE ? `${TG_ECHO_MARKER} ${line}` : `\u276F ${line}`;
|
|
114726
|
+
}
|
|
113998
114727
|
function labelTelegramLine(text) {
|
|
113999
114728
|
return `[${TG_LABEL}] ${text}`;
|
|
114000
114729
|
}
|
|
@@ -114048,6 +114777,12 @@ function formatRemoteEventLines(status) {
|
|
|
114048
114777
|
return ["No remote events yet."];
|
|
114049
114778
|
return [...status.events].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.kind.padEnd(8)} ${event.text}`);
|
|
114050
114779
|
}
|
|
114780
|
+
function formatRemoteCommandLines(status) {
|
|
114781
|
+
const commands = status.events.filter((event) => event.kind === "command");
|
|
114782
|
+
if (commands.length === 0)
|
|
114783
|
+
return ["No remote commands yet."];
|
|
114784
|
+
return [...commands].reverse().slice(0, REMOTE_EVENT_LIMIT).map((event) => `${clock(event.at)} ${event.text}`);
|
|
114785
|
+
}
|
|
114051
114786
|
function renderRemoteControlText(status) {
|
|
114052
114787
|
return `${[...formatRemoteStatusLines(status), "", "Recent events:", ...formatRemoteEventLines(status)].join(`
|
|
114053
114788
|
`)}
|
|
@@ -114084,7 +114819,11 @@ function presentRemoteControl(open, otui, chrome, options) {
|
|
|
114084
114819
|
let unsubscribeKey;
|
|
114085
114820
|
const linesFor = (tab) => {
|
|
114086
114821
|
const status = options.getStatus();
|
|
114087
|
-
|
|
114822
|
+
if (tab === "events")
|
|
114823
|
+
return formatRemoteEventLines(status);
|
|
114824
|
+
if (tab === "commands")
|
|
114825
|
+
return formatRemoteCommandLines(status);
|
|
114826
|
+
return formatRemoteStatusLines(status);
|
|
114088
114827
|
};
|
|
114089
114828
|
const content = (tab) => {
|
|
114090
114829
|
const lines = linesFor(tab);
|
|
@@ -114211,7 +114950,8 @@ var init_remote_control_surface = __esm(() => {
|
|
|
114211
114950
|
STATE_WORDS = { off: "off", on: "on", offline: "offline" };
|
|
114212
114951
|
REMOTE_TABS = [
|
|
114213
114952
|
{ id: "status", label: "Status" },
|
|
114214
|
-
{ id: "events", label: "Events" }
|
|
114953
|
+
{ id: "events", label: "Events" },
|
|
114954
|
+
{ id: "commands", label: "Commands" }
|
|
114215
114955
|
];
|
|
114216
114956
|
});
|
|
114217
114957
|
|
|
@@ -118415,7 +119155,7 @@ function viewWriteRun(cwd, runIdOrPrefix, deps = {}) {
|
|
|
118415
119155
|
const patch = readVerifiedPatch(located.dir, located.record);
|
|
118416
119156
|
return patch.ok ? { record: located.record, patch: patch.text } : { record: located.record };
|
|
118417
119157
|
}
|
|
118418
|
-
function
|
|
119158
|
+
function refuse4(reason, detail) {
|
|
118419
119159
|
return { kind: "refused", reason, detail };
|
|
118420
119160
|
}
|
|
118421
119161
|
function decisionBody(decision) {
|
|
@@ -118425,33 +119165,33 @@ function decisionBody(decision) {
|
|
|
118425
119165
|
async function landWriteRun(input, deps = {}) {
|
|
118426
119166
|
const located = locate(input.cwd, input.runId, deps.dataDir);
|
|
118427
119167
|
if (located === undefined)
|
|
118428
|
-
return
|
|
119168
|
+
return refuse4("not-found", `no external write run "${input.runId}" in this checkout`);
|
|
118429
119169
|
const { record, dir } = located;
|
|
118430
119170
|
if (record.state !== "pending-review" || located.decided) {
|
|
118431
|
-
return
|
|
119171
|
+
return refuse4("not-pending", record.state === "refused" ? "the run was refused at capture; it can only be discarded" : "the run was already decided");
|
|
118432
119172
|
}
|
|
118433
119173
|
const patch = readVerifiedPatch(dir, record);
|
|
118434
119174
|
if (!patch.ok)
|
|
118435
|
-
return
|
|
119175
|
+
return refuse4(patch.reason, patch.detail);
|
|
118436
119176
|
if (input.confirmedPatchHash !== record.patchHash) {
|
|
118437
|
-
return
|
|
119177
|
+
return refuse4("hash-mismatch", "the confirmed hash is not the hash of the stored patch");
|
|
118438
119178
|
}
|
|
118439
119179
|
if (record.redacted) {
|
|
118440
|
-
return
|
|
119180
|
+
return refuse4("redacted", "the stored patch was altered by redaction, so landing it would land content the agent did not write");
|
|
118441
119181
|
}
|
|
118442
119182
|
const binary = record.files.filter((file) => file.binary === true).map((file) => file.path);
|
|
118443
119183
|
if (binary.length > 0) {
|
|
118444
|
-
return
|
|
119184
|
+
return refuse4("binary", `binary content is not carried by the patch: ${binary.join(", ")}`);
|
|
118445
119185
|
}
|
|
118446
119186
|
if (record.flaggedPaths.length > 0 && input.allowFlagged !== true) {
|
|
118447
|
-
return
|
|
119187
|
+
return refuse4("flagged", `the run changes flagged paths (${record.flaggedPaths.join(", ")}); allow them explicitly to land`);
|
|
118448
119188
|
}
|
|
118449
119189
|
const git = deps.git ?? defaultGit;
|
|
118450
119190
|
const repo = record.projectRoot;
|
|
118451
119191
|
const branch = `external/${record.runId}`;
|
|
118452
119192
|
try {
|
|
118453
119193
|
await git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], repo);
|
|
118454
|
-
return
|
|
119194
|
+
return refuse4("branch-exists", `branch ${branch} already exists`);
|
|
118455
119195
|
} catch {}
|
|
118456
119196
|
const ownedDir = deps.worktreesDir === undefined ? await mkdtemp6(path261.join(tmpdir12(), "keryx-external-land-")) : undefined;
|
|
118457
119197
|
const worktreesDir = deps.worktreesDir ?? ownedDir;
|
|
@@ -118473,7 +119213,7 @@ async function landWriteRun(input, deps = {}) {
|
|
|
118473
119213
|
return;
|
|
118474
119214
|
});
|
|
118475
119215
|
createdBranch = false;
|
|
118476
|
-
return
|
|
119216
|
+
return refuse4("not-pending", "the run was decided by someone else while it was being landed");
|
|
118477
119217
|
}
|
|
118478
119218
|
return { kind: "landed", branch, commit };
|
|
118479
119219
|
} catch (error) {
|
|
@@ -118481,7 +119221,7 @@ async function landWriteRun(input, deps = {}) {
|
|
|
118481
119221
|
await git(["branch", "-D", branch], repo).catch(() => {
|
|
118482
119222
|
return;
|
|
118483
119223
|
});
|
|
118484
|
-
return
|
|
119224
|
+
return refuse4("apply-failed", errorMessage3(error));
|
|
118485
119225
|
} finally {
|
|
118486
119226
|
await port.remove(worktreeId).catch(() => {
|
|
118487
119227
|
return;
|
|
@@ -145920,6 +146660,16 @@ async function launchTuiAgentShell(opts) {
|
|
|
145920
146660
|
detachExternal = attached.detach;
|
|
145921
146661
|
return attached.operator;
|
|
145922
146662
|
})();
|
|
146663
|
+
const commandWork = new Set;
|
|
146664
|
+
const commandWorkFailures = [];
|
|
146665
|
+
const trackCommandWork = (work) => {
|
|
146666
|
+
const settled = work().catch((error) => {
|
|
146667
|
+
commandWorkFailures.push(error instanceof Error ? error.message : String(error));
|
|
146668
|
+
}).finally(() => {
|
|
146669
|
+
commandWork.delete(settled);
|
|
146670
|
+
});
|
|
146671
|
+
commandWork.add(settled);
|
|
146672
|
+
};
|
|
145923
146673
|
const runDelegate = (line) => {
|
|
145924
146674
|
const parsed = parseDelegateCommand(line.trim().replace(/^\/\S+\s*/, ""));
|
|
145925
146675
|
if (!parsed.ok) {
|
|
@@ -146244,7 +146994,7 @@ async function launchTuiAgentShell(opts) {
|
|
|
146244
146994
|
}).catch(() => {});
|
|
146245
146995
|
};
|
|
146246
146996
|
const showExternalStatus = () => {
|
|
146247
|
-
loadExternalStatus().then((status) => {
|
|
146997
|
+
trackCommandWork(() => loadExternalStatus().then((status) => {
|
|
146248
146998
|
externalStatus = status;
|
|
146249
146999
|
refreshExternalSidebar();
|
|
146250
147000
|
const jevAvailable = resolveJevApiKey(process.env) !== undefined;
|
|
@@ -146255,7 +147005,7 @@ async function launchTuiAgentShell(opts) {
|
|
|
146255
147005
|
}).catch((error) => {
|
|
146256
147006
|
io.onSystem?.(`external: ${error instanceof Error ? error.message : String(error)}
|
|
146257
147007
|
`);
|
|
146258
|
-
});
|
|
147008
|
+
}));
|
|
146259
147009
|
};
|
|
146260
147010
|
const approvalContext = createApprovalContextLoader(opts.session?.cwd ?? process.cwd());
|
|
146261
147011
|
const confirmScheduleCard = async (card) => {
|
|
@@ -146727,6 +147477,10 @@ async function launchTuiAgentShell(opts) {
|
|
|
146727
147477
|
liveDeps = deps;
|
|
146728
147478
|
let splash = undefined;
|
|
146729
147479
|
const stopRemoteForSessionSwitch = () => {
|
|
147480
|
+
if (remoteBridge?.keepingTopic === true) {
|
|
147481
|
+
remoteBridge.sessionLeaving();
|
|
147482
|
+
return;
|
|
147483
|
+
}
|
|
146730
147484
|
if (remoteBridge?.active !== true)
|
|
146731
147485
|
return;
|
|
146732
147486
|
const closing = remoteBridge.disable();
|
|
@@ -146741,6 +147495,8 @@ async function launchTuiAgentShell(opts) {
|
|
|
146741
147495
|
}
|
|
146742
147496
|
stopRemoteForSessionSwitch();
|
|
146743
147497
|
liveSession = opened.handle;
|
|
147498
|
+
if (remoteBridge?.keepingTopic === true)
|
|
147499
|
+
remoteBridge.sessionEntered(opened.resumed ? "resumed" : "new");
|
|
146744
147500
|
io.trustedMcpTools?.clear();
|
|
146745
147501
|
history = previewHistory === true ? opened.history.slice(-SESSION_PREVIEW_MESSAGE_COUNT) : opened.history;
|
|
146746
147502
|
archive = opened.archive.length > 0 ? [...opened.archive] : [...opened.history];
|
|
@@ -147103,6 +147859,8 @@ Staying in the current session.
|
|
|
147103
147859
|
io.trustedMcpTools?.clear();
|
|
147104
147860
|
stopRemoteForSessionSwitch();
|
|
147105
147861
|
liveSession = opened.handle;
|
|
147862
|
+
if (remoteBridge?.keepingTopic === true)
|
|
147863
|
+
remoteBridge.sessionEntered("new");
|
|
147106
147864
|
history = [];
|
|
147107
147865
|
archive = [];
|
|
147108
147866
|
nextArchiveIndex = 0;
|
|
@@ -147115,8 +147873,10 @@ Staying in the current session.
|
|
|
147115
147873
|
}
|
|
147116
147874
|
return true;
|
|
147117
147875
|
};
|
|
147876
|
+
let pendingResumeTarget;
|
|
147118
147877
|
const resumeSessionInteractive = async () => {
|
|
147119
|
-
const found = await pickRecentSession();
|
|
147878
|
+
const found = pendingResumeTarget ?? await pickRecentSession();
|
|
147879
|
+
pendingResumeTarget = undefined;
|
|
147120
147880
|
if (found === undefined) {
|
|
147121
147881
|
return;
|
|
147122
147882
|
}
|
|
@@ -148473,7 +149233,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148473
149233
|
if (line.startsWith("/")) {
|
|
148474
149234
|
transcript.add(new otui.TextRenderable(r, {
|
|
148475
149235
|
id: `c${uid++}`,
|
|
148476
|
-
content: otui.t`${roleChunk(otui, "accent",
|
|
149236
|
+
content: otui.t`${roleChunk(otui, "accent", commandEchoText(line, source))}`,
|
|
148477
149237
|
marginTop: 1
|
|
148478
149238
|
}));
|
|
148479
149239
|
}
|
|
@@ -148726,7 +149486,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148726
149486
|
const cwd = inspectorCwd();
|
|
148727
149487
|
io.onSystem?.(`review-jev-risk: scoring the working diff's hunks\u2026
|
|
148728
149488
|
`);
|
|
148729
|
-
(async () => {
|
|
149489
|
+
trackCommandWork(async () => {
|
|
148730
149490
|
try {
|
|
148731
149491
|
io.onSystem?.(`${await runJevRiskForShell(cwd)}
|
|
148732
149492
|
`);
|
|
@@ -148734,14 +149494,14 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148734
149494
|
io.onSystem?.(`review-jev-risk: ${error instanceof Error ? error.message : String(error)}
|
|
148735
149495
|
`);
|
|
148736
149496
|
}
|
|
148737
|
-
})
|
|
149497
|
+
});
|
|
148738
149498
|
return;
|
|
148739
149499
|
}
|
|
148740
149500
|
if (isJevScenariosCommand(command.name)) {
|
|
148741
149501
|
const cwd = inspectorCwd();
|
|
148742
149502
|
io.onSystem?.(`review-jev-scenarios: checking likely-affected scenarios\u2026
|
|
148743
149503
|
`);
|
|
148744
|
-
(async () => {
|
|
149504
|
+
trackCommandWork(async () => {
|
|
148745
149505
|
try {
|
|
148746
149506
|
io.onSystem?.(`${await runJevScenariosForShell(cwd)}
|
|
148747
149507
|
`);
|
|
@@ -148749,14 +149509,14 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148749
149509
|
io.onSystem?.(`review-jev-scenarios: ${error instanceof Error ? error.message : String(error)}
|
|
148750
149510
|
`);
|
|
148751
149511
|
}
|
|
148752
|
-
})
|
|
149512
|
+
});
|
|
148753
149513
|
return;
|
|
148754
149514
|
}
|
|
148755
149515
|
if (isJevRulesCommand(command.name)) {
|
|
148756
149516
|
const cwd = inspectorCwd();
|
|
148757
149517
|
io.onSystem?.(`review-jev-rules: checking the working diff against project rules\u2026
|
|
148758
149518
|
`);
|
|
148759
|
-
(async () => {
|
|
149519
|
+
trackCommandWork(async () => {
|
|
148760
149520
|
try {
|
|
148761
149521
|
io.onSystem?.(`${await runJevRulesForShell(cwd)}
|
|
148762
149522
|
`);
|
|
@@ -148764,14 +149524,14 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148764
149524
|
io.onSystem?.(`review-jev-rules: ${error instanceof Error ? error.message : String(error)}
|
|
148765
149525
|
`);
|
|
148766
149526
|
}
|
|
148767
|
-
})
|
|
149527
|
+
});
|
|
148768
149528
|
return;
|
|
148769
149529
|
}
|
|
148770
149530
|
if (isStaledocsCommand(command.name)) {
|
|
148771
149531
|
const cwd = inspectorCwd();
|
|
148772
149532
|
io.onSystem?.(`review-jev-docs: checking the working diff against documentation\u2026
|
|
148773
149533
|
`);
|
|
148774
|
-
(async () => {
|
|
149534
|
+
trackCommandWork(async () => {
|
|
148775
149535
|
try {
|
|
148776
149536
|
io.onSystem?.(`${await runStaledocsForShell(cwd)}
|
|
148777
149537
|
`);
|
|
@@ -148779,7 +149539,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148779
149539
|
io.onSystem?.(`review-jev-docs: ${error instanceof Error ? error.message : String(error)}
|
|
148780
149540
|
`);
|
|
148781
149541
|
}
|
|
148782
|
-
})
|
|
149542
|
+
});
|
|
148783
149543
|
return;
|
|
148784
149544
|
}
|
|
148785
149545
|
if (isOpencommentsCommand(command.name)) {
|
|
@@ -148794,7 +149554,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148794
149554
|
}
|
|
148795
149555
|
io.onSystem?.(`review-jev-comments: checking open comments on ${repoArg}#${prNumber}\u2026
|
|
148796
149556
|
`);
|
|
148797
|
-
(async () => {
|
|
149557
|
+
trackCommandWork(async () => {
|
|
148798
149558
|
try {
|
|
148799
149559
|
io.onSystem?.(`${await runOpencommentsForShell(cwd, repoArg, prNumber)}
|
|
148800
149560
|
`);
|
|
@@ -148802,14 +149562,14 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148802
149562
|
io.onSystem?.(`review-jev-comments: ${error instanceof Error ? error.message : String(error)}
|
|
148803
149563
|
`);
|
|
148804
149564
|
}
|
|
148805
|
-
})
|
|
149565
|
+
});
|
|
148806
149566
|
return;
|
|
148807
149567
|
}
|
|
148808
149568
|
if (isJevContractCommand(command.name)) {
|
|
148809
149569
|
const cwd = inspectorCwd();
|
|
148810
149570
|
io.onSystem?.(`review-jev-contract: checking claims against the working diff\u2026
|
|
148811
149571
|
`);
|
|
148812
|
-
(async () => {
|
|
149572
|
+
trackCommandWork(async () => {
|
|
148813
149573
|
try {
|
|
148814
149574
|
io.onSystem?.(`${await runJevContractForShell(cwd)}
|
|
148815
149575
|
`);
|
|
@@ -148817,14 +149577,14 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148817
149577
|
io.onSystem?.(`review-jev-contract: ${error instanceof Error ? error.message : String(error)}
|
|
148818
149578
|
`);
|
|
148819
149579
|
}
|
|
148820
|
-
})
|
|
149580
|
+
});
|
|
148821
149581
|
return;
|
|
148822
149582
|
}
|
|
148823
149583
|
if (isJevTriageCommand(command.name)) {
|
|
148824
149584
|
const cwd = inspectorCwd();
|
|
148825
149585
|
io.onSystem?.(`review-jev-triage: triaging the latest review package\u2026
|
|
148826
149586
|
`);
|
|
148827
|
-
(async () => {
|
|
149587
|
+
trackCommandWork(async () => {
|
|
148828
149588
|
try {
|
|
148829
149589
|
io.onSystem?.(`${await runJevTriageForShell(cwd)}
|
|
148830
149590
|
`);
|
|
@@ -148832,7 +149592,7 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148832
149592
|
io.onSystem?.(`review-jev-triage: ${error instanceof Error ? error.message : String(error)}
|
|
148833
149593
|
`);
|
|
148834
149594
|
}
|
|
148835
|
-
})
|
|
149595
|
+
});
|
|
148836
149596
|
return;
|
|
148837
149597
|
}
|
|
148838
149598
|
if (isRewindCommand(command.name)) {
|
|
@@ -148888,13 +149648,13 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148888
149648
|
}
|
|
148889
149649
|
if (isExternalAgentsCommand(command.name)) {
|
|
148890
149650
|
const arg = line.trim().split(/\s+/).slice(1).join(" ");
|
|
148891
|
-
runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text));
|
|
149651
|
+
trackCommandWork(() => runExternalAgentsCommand(arg, opts.session?.cwd ?? process.cwd()).then((text) => io.onSystem?.(text)));
|
|
148892
149652
|
return;
|
|
148893
149653
|
}
|
|
148894
149654
|
if (isExternalCommand(command.name)) {
|
|
148895
149655
|
const arg = line.trim().split(/\s+/).slice(1).join(" ").trim().toLowerCase();
|
|
148896
149656
|
if (arg === "on" || arg === "off") {
|
|
148897
|
-
setExternalEnabled(arg);
|
|
149657
|
+
trackCommandWork(() => setExternalEnabled(arg));
|
|
148898
149658
|
return;
|
|
148899
149659
|
}
|
|
148900
149660
|
if (arg.length > 0) {
|
|
@@ -148938,11 +149698,11 @@ ${formatThemeList(getThemeId())}`);
|
|
|
148938
149698
|
return;
|
|
148939
149699
|
}
|
|
148940
149700
|
if (command.name === "/doctor") {
|
|
148941
|
-
(async () => {
|
|
149701
|
+
trackCommandWork(async () => {
|
|
148942
149702
|
const report = await buildDoctorReport(sessionCwd);
|
|
148943
149703
|
io.onSystem?.(`${formatDoctorReport2(report)}
|
|
148944
149704
|
`);
|
|
148945
|
-
})
|
|
149705
|
+
});
|
|
148946
149706
|
return;
|
|
148947
149707
|
}
|
|
148948
149708
|
if (command.name === "/setup") {
|
|
@@ -149414,6 +150174,110 @@ ${formatThemeList(getThemeId())}`);
|
|
|
149414
150174
|
runLine(drained.question, "operator", drained.source);
|
|
149415
150175
|
}
|
|
149416
150176
|
});
|
|
150177
|
+
const captureShellOutput = async (run) => {
|
|
150178
|
+
const chunks = [];
|
|
150179
|
+
let active = true;
|
|
150180
|
+
let ok = true;
|
|
150181
|
+
const previous = io.onSystem;
|
|
150182
|
+
const previousToast = chrome.showToast;
|
|
150183
|
+
const capture = (text) => {
|
|
150184
|
+
if (active)
|
|
150185
|
+
chunks.push(text);
|
|
150186
|
+
previous?.(text);
|
|
150187
|
+
};
|
|
150188
|
+
const captureToast = (message) => {
|
|
150189
|
+
if (active)
|
|
150190
|
+
chunks.push(`${message}
|
|
150191
|
+
`);
|
|
150192
|
+
previousToast(message);
|
|
150193
|
+
};
|
|
150194
|
+
const before = new Set(commandWork);
|
|
150195
|
+
commandWorkFailures.length = 0;
|
|
150196
|
+
io.onSystem = capture;
|
|
150197
|
+
chrome.showToast = captureToast;
|
|
150198
|
+
try {
|
|
150199
|
+
await run();
|
|
150200
|
+
let seen = chunks.length;
|
|
150201
|
+
let quiet = 0;
|
|
150202
|
+
while (quiet < 2 || [...commandWork].some((work) => !before.has(work))) {
|
|
150203
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
150204
|
+
if (chunks.length !== seen) {
|
|
150205
|
+
seen = chunks.length;
|
|
150206
|
+
quiet = 0;
|
|
150207
|
+
} else {
|
|
150208
|
+
quiet += 1;
|
|
150209
|
+
}
|
|
150210
|
+
}
|
|
150211
|
+
} catch (error) {
|
|
150212
|
+
ok = false;
|
|
150213
|
+
chunks.push(`${error instanceof Error ? error.message : String(error)}
|
|
150214
|
+
`);
|
|
150215
|
+
} finally {
|
|
150216
|
+
active = false;
|
|
150217
|
+
if (io.onSystem === capture) {
|
|
150218
|
+
if (previous)
|
|
150219
|
+
io.onSystem = previous;
|
|
150220
|
+
else
|
|
150221
|
+
delete io.onSystem;
|
|
150222
|
+
}
|
|
150223
|
+
if (chrome.showToast === captureToast)
|
|
150224
|
+
chrome.showToast = previousToast;
|
|
150225
|
+
}
|
|
150226
|
+
if (commandWorkFailures.length > 0) {
|
|
150227
|
+
ok = false;
|
|
150228
|
+
chunks.push(`${commandWorkFailures.splice(0).join(`
|
|
150229
|
+
`)}
|
|
150230
|
+
`);
|
|
150231
|
+
}
|
|
150232
|
+
return { output: chunks.join("").trim(), ok };
|
|
150233
|
+
};
|
|
150234
|
+
const echoRemoteCommand = (line) => {
|
|
150235
|
+
transcript.add(new otui.TextRenderable(r, {
|
|
150236
|
+
id: `c${uid++}`,
|
|
150237
|
+
content: otui.t`${roleChunk(otui, "accent", commandEchoText(line, TG_SOURCE))}`,
|
|
150238
|
+
marginTop: 1
|
|
150239
|
+
}));
|
|
150240
|
+
};
|
|
150241
|
+
const remoteStatusText = async () => {
|
|
150242
|
+
const cwd = inspectorCwd();
|
|
150243
|
+
const [workspaces, flows] = await Promise.all([loadInspectorWorkspaces(cwd), loadInspectorFlows(cwd)]);
|
|
150244
|
+
const snapshot = buildSessionInfoSnapshot({
|
|
150245
|
+
summary: liveSession.summary,
|
|
150246
|
+
selection: currentSel,
|
|
150247
|
+
version: package_default.version,
|
|
150248
|
+
usage: lastUsage,
|
|
150249
|
+
estimateTokens: estimateContextTokens(history),
|
|
150250
|
+
sessionText: history.map((message) => message.content).join(`
|
|
150251
|
+
`),
|
|
150252
|
+
workspaces,
|
|
150253
|
+
flows
|
|
150254
|
+
});
|
|
150255
|
+
return `${formatSessionInfoText(snapshot).trimEnd()}
|
|
150256
|
+
|
|
150257
|
+
Mode: ${permissionMode}${readOnly ? " (read-only on)" : ""}
|
|
150258
|
+
Turn: ${chrome.isBusy() ? "running" : "idle"} \xB7 queue: ${mainQueue.length} waiting`;
|
|
150259
|
+
};
|
|
150260
|
+
const runRemoteCommand = async (line) => {
|
|
150261
|
+
const name = findAgentCommand(line, "agent")?.name;
|
|
150262
|
+
const first = (line.trim().split(/\s+/)[1] ?? "").toLowerCase();
|
|
150263
|
+
if (name === "/status") {
|
|
150264
|
+
echoRemoteCommand(line);
|
|
150265
|
+
return await captureShellOutput(async () => {
|
|
150266
|
+
io.onSystem?.(`${await remoteStatusText()}
|
|
150267
|
+
`);
|
|
150268
|
+
});
|
|
150269
|
+
}
|
|
150270
|
+
if (name === "/mode" && (first === "" || first === "auto")) {
|
|
150271
|
+
echoRemoteCommand(line);
|
|
150272
|
+
return await captureShellOutput(() => {
|
|
150273
|
+
if (first === "auto")
|
|
150274
|
+
commitPermissionMode("auto");
|
|
150275
|
+
io.onSystem?.(`Permission mode: ${permissionMode} (ask, trust or auto)
|
|
150276
|
+
`);
|
|
150277
|
+
});
|
|
150278
|
+
}
|
|
150279
|
+
return await captureShellOutput(() => runLine(line, "operator", TG_SOURCE));
|
|
150280
|
+
};
|
|
149417
150281
|
remoteBridge = new RemoteBridge({
|
|
149418
150282
|
host: {
|
|
149419
150283
|
sessionId: () => liveSession.summary.id,
|
|
@@ -149452,6 +150316,78 @@ ${formatThemeList(getThemeId())}`);
|
|
|
149452
150316
|
paintMainQueue();
|
|
149453
150317
|
return dropped.map((item) => item.question);
|
|
149454
150318
|
},
|
|
150319
|
+
runCommand: runRemoteCommand,
|
|
150320
|
+
busyRefusal: (line) => {
|
|
150321
|
+
const command = findAgentCommand(line, "agent");
|
|
150322
|
+
const decision = classifyBusyDispatch({
|
|
150323
|
+
line,
|
|
150324
|
+
commandName: command?.name,
|
|
150325
|
+
isSessionInfo: isSessionInfoCommand(line),
|
|
150326
|
+
isFlows: isFlowsCommand(line),
|
|
150327
|
+
isWorkspace: isWorkspaceCommand(line),
|
|
150328
|
+
isReview: isReviewCommand(line),
|
|
150329
|
+
isMcp: isMcpToolsCommand(line),
|
|
150330
|
+
isMcpConsumer: isMcpConsumerCommand(line)
|
|
150331
|
+
});
|
|
150332
|
+
return decision === "deferred" || decision === "not-a-command" ? BUSY_REASON : undefined;
|
|
150333
|
+
},
|
|
150334
|
+
listProviders: async () => {
|
|
150335
|
+
const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
|
|
150336
|
+
const connected = await filterConnectedDetectedProviders(detected, {});
|
|
150337
|
+
return connected.map((prov) => ({
|
|
150338
|
+
id: prov.name,
|
|
150339
|
+
label: prov.label ?? prov.name,
|
|
150340
|
+
...prov.name === currentSel.provider ? { current: true } : {}
|
|
150341
|
+
}));
|
|
150342
|
+
},
|
|
150343
|
+
listModels: async (providerId) => {
|
|
150344
|
+
const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
|
|
150345
|
+
const name = providerId ?? currentSel.provider;
|
|
150346
|
+
const prov = detected.find((d) => d.name === name);
|
|
150347
|
+
if (prov === undefined)
|
|
150348
|
+
return;
|
|
150349
|
+
const resolved = await modelsForPicker(prov);
|
|
150350
|
+
return {
|
|
150351
|
+
provider: prov.label ?? prov.name,
|
|
150352
|
+
models: resolved.models.map((id) => ({
|
|
150353
|
+
id,
|
|
150354
|
+
label: id,
|
|
150355
|
+
...name === currentSel.provider && id === currentSel.model ? { current: true } : {}
|
|
150356
|
+
}))
|
|
150357
|
+
};
|
|
150358
|
+
},
|
|
150359
|
+
switchModel: async (modelId, providerId) => {
|
|
150360
|
+
const detected = opts.redetect !== undefined ? await opts.redetect() : opts.detected;
|
|
150361
|
+
const name = providerId ?? currentSel.provider;
|
|
150362
|
+
const prov = detected.find((d) => d.name === name);
|
|
150363
|
+
if (prov === undefined)
|
|
150364
|
+
return { output: "That provider is not connected here.", ok: false };
|
|
150365
|
+
const baseUrl = name === currentSel.provider ? currentSel.baseUrl : prov.baseUrl;
|
|
150366
|
+
await switchTo(baseUrl === undefined ? { provider: name, model: modelId } : { provider: name, model: modelId, baseUrl });
|
|
150367
|
+
try {
|
|
150368
|
+
await applyRuntimeSwitchToSlate({
|
|
150369
|
+
slateSession,
|
|
150370
|
+
runtime: { provider: currentSel.provider, model: currentSel.model },
|
|
150371
|
+
history,
|
|
150372
|
+
onHistoryChange: io.onHistoryChange
|
|
150373
|
+
});
|
|
150374
|
+
} catch {}
|
|
150375
|
+
return { output: `Model: ${currentSel.provider}/${currentSel.model}`, ok: true };
|
|
150376
|
+
},
|
|
150377
|
+
listSessions: async () => listSessions(sessionCwd).map((row) => ({
|
|
150378
|
+
id: row.id,
|
|
150379
|
+
label: `${shortSessionId(row.id)} \xB7 ${row.title}`,
|
|
150380
|
+
...row.id === liveSession.summary.id ? { current: true } : {}
|
|
150381
|
+
})),
|
|
150382
|
+
resumeSession: async (id) => {
|
|
150383
|
+
const found = findSession(sessionCwd, id);
|
|
150384
|
+
if (found === undefined)
|
|
150385
|
+
return { output: "Session not found in this project.", ok: false };
|
|
150386
|
+
pendingResumeTarget = found;
|
|
150387
|
+
const outcome = await captureShellOutput(() => resumeSessionInteractive());
|
|
150388
|
+
pendingResumeTarget = undefined;
|
|
150389
|
+
return liveSession.summary.id === found.id ? { ...outcome, ok: true } : { ...outcome, ok: false };
|
|
150390
|
+
},
|
|
149455
150391
|
onChange: () => liveRemotePanel?.refresh()
|
|
149456
150392
|
}
|
|
149457
150393
|
});
|
|
@@ -149504,6 +150440,7 @@ var init_tui_shell = __esm(() => {
|
|
|
149504
150440
|
init_product_open_surface();
|
|
149505
150441
|
init_reviews_inspector();
|
|
149506
150442
|
init_shell_bridge();
|
|
150443
|
+
init_command_gateway();
|
|
149507
150444
|
init_channels_client();
|
|
149508
150445
|
init_channels_surface();
|
|
149509
150446
|
init_remote_control_surface();
|
|
@@ -184248,7 +185185,7 @@ function finishReasonToStopReason(finishReason) {
|
|
|
184248
185185
|
return "end_turn";
|
|
184249
185186
|
}
|
|
184250
185187
|
}
|
|
184251
|
-
function
|
|
185188
|
+
function refuse5(error) {
|
|
184252
185189
|
throw new AcpError(error.code, error.message, error.data);
|
|
184253
185190
|
}
|
|
184254
185191
|
async function runAcpServer(options) {
|
|
@@ -184321,7 +185258,7 @@ async function runAcpServer(options) {
|
|
|
184321
185258
|
if (!activeTurns.has(sessionId)) {
|
|
184322
185259
|
return;
|
|
184323
185260
|
}
|
|
184324
|
-
|
|
185261
|
+
refuse5(invalidRequest({
|
|
184325
185262
|
reason: `${method}: a turn is already running for session ${sessionId}; ` + "wait for its session/prompt response \u2014 send session/cancel first if you do not want to wait for it to finish, " + "then prompt again once the cancelled turn has replied",
|
|
184326
185263
|
method,
|
|
184327
185264
|
sessionId,
|
|
@@ -184370,7 +185307,7 @@ async function runAcpServer(options) {
|
|
|
184370
185307
|
let stopping = false;
|
|
184371
185308
|
function refuseIfStopping(method) {
|
|
184372
185309
|
if (stopping) {
|
|
184373
|
-
|
|
185310
|
+
refuse5({
|
|
184374
185311
|
code: JSON_RPC_ERROR_CODES.invalidRequest,
|
|
184375
185312
|
message: `${method}: keryx acp is shutting down`,
|
|
184376
185313
|
data: { method, condition: "shutting-down" }
|
|
@@ -184379,7 +185316,7 @@ async function runAcpServer(options) {
|
|
|
184379
185316
|
}
|
|
184380
185317
|
function requireProvider(method) {
|
|
184381
185318
|
if (options.provider === undefined) {
|
|
184382
|
-
|
|
185319
|
+
refuse5({
|
|
184383
185320
|
code: JSON_RPC_ERROR_CODES.invalidRequest,
|
|
184384
185321
|
message: options.providerUnavailable ?? `${method}: keryx acp has no provider configured`,
|
|
184385
185322
|
data: { method, condition: "provider-not-configured" }
|
|
@@ -184494,7 +185431,7 @@ async function runAcpServer(options) {
|
|
|
184494
185431
|
const choice = findAcpModelChoice(choices, requested, allowBareModelId);
|
|
184495
185432
|
const available = choices.map((entry) => entry.value);
|
|
184496
185433
|
if (choice === undefined) {
|
|
184497
|
-
|
|
185434
|
+
refuse5(invalidParams(`${method}: "${requested.length > 80 ? `${requested.slice(0, 80)}\u2026` : requested}" is not a model this session can run; ` + `choose one of: ${available.join(", ")}`, { value: requested, available }));
|
|
184498
185435
|
}
|
|
184499
185436
|
const seq = beginSwitch(sessionId);
|
|
184500
185437
|
const current = sessionModel(sessionId, method);
|
|
@@ -184503,7 +185440,7 @@ async function runAcpServer(options) {
|
|
|
184503
185440
|
return "cancelled";
|
|
184504
185441
|
}
|
|
184505
185442
|
if (typeof built === "string") {
|
|
184506
|
-
|
|
185443
|
+
refuse5(invalidParams(`${method}: cannot switch to ${choice.value}: ${built}`, { value: choice.value, reason: built }));
|
|
184507
185444
|
}
|
|
184508
185445
|
const next = { choice, binding: built };
|
|
184509
185446
|
applySwitch(sessionId, seq, next);
|
|
@@ -184620,14 +185557,14 @@ async function runAcpServer(options) {
|
|
|
184620
185557
|
})();
|
|
184621
185558
|
function requireInitialized(method) {
|
|
184622
185559
|
if (!initialized) {
|
|
184623
|
-
|
|
185560
|
+
refuse5(invalidRequest({ reason: `${method}: call initialize before any other request`, method }));
|
|
184624
185561
|
}
|
|
184625
185562
|
}
|
|
184626
185563
|
function handleInitialize(params) {
|
|
184627
185564
|
const obj = requireObjectParams(params, ACP_AGENT_METHODS.initialize);
|
|
184628
185565
|
const negotiation = negotiateProtocolVersion(obj["protocolVersion"]);
|
|
184629
185566
|
if (negotiation.kind === "refuse") {
|
|
184630
|
-
|
|
185567
|
+
refuse5(negotiation.error);
|
|
184631
185568
|
}
|
|
184632
185569
|
initialized = true;
|
|
184633
185570
|
const rawCaps = obj["clientCapabilities"];
|
|
@@ -184751,13 +185688,13 @@ Switch with /model <model>; it applies from the next turn.`;
|
|
|
184751
185688
|
const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionPrompt);
|
|
184752
185689
|
const promptField = obj["prompt"];
|
|
184753
185690
|
if (!Array.isArray(promptField)) {
|
|
184754
|
-
|
|
185691
|
+
refuse5(invalidParams(`${ACP_AGENT_METHODS.sessionPrompt}: prompt must be an array of content blocks`, {
|
|
184755
185692
|
received: promptField
|
|
184756
185693
|
}));
|
|
184757
185694
|
}
|
|
184758
185695
|
const state = registry.get(sessionId);
|
|
184759
185696
|
if (state === undefined) {
|
|
184760
|
-
|
|
185697
|
+
refuse5({
|
|
184761
185698
|
code: JSON_RPC_ERROR_CODES.resourceNotFound,
|
|
184762
185699
|
message: `Unknown ACP session: ${sessionId}`,
|
|
184763
185700
|
data: { sessionId }
|
|
@@ -184849,7 +185786,7 @@ Switch with /model <model>; it applies from the next turn.`;
|
|
|
184849
185786
|
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionList);
|
|
184850
185787
|
const rawCwd = obj["cwd"];
|
|
184851
185788
|
if (rawCwd !== undefined && rawCwd !== null && typeof rawCwd !== "string") {
|
|
184852
|
-
|
|
185789
|
+
refuse5(invalidParams(`${ACP_AGENT_METHODS.sessionList}: cwd must be a string, null or omitted`, { received: rawCwd }));
|
|
184853
185790
|
}
|
|
184854
185791
|
const cwd = typeof rawCwd === "string" && rawCwd.length > 0 ? rawCwd : process.cwd();
|
|
184855
185792
|
const summaries = listSessions(cwd, options.dataDir);
|
|
@@ -184915,14 +185852,14 @@ Switch with /model <model>; it applies from the next turn.`;
|
|
|
184915
185852
|
if (!(cause instanceof AcpSessionTranscriptUnreadableError)) {
|
|
184916
185853
|
throw cause;
|
|
184917
185854
|
}
|
|
184918
|
-
|
|
185855
|
+
refuse5({
|
|
184919
185856
|
code: JSON_RPC_ERROR_CODES.internalError,
|
|
184920
185857
|
message: `Cannot load session ${sessionId}: ${cause.cause.message}`,
|
|
184921
185858
|
data: { sessionId, cwd, file: cause.cause.file, reason: cause.cause.reason }
|
|
184922
185859
|
});
|
|
184923
185860
|
}
|
|
184924
185861
|
if (state === undefined) {
|
|
184925
|
-
|
|
185862
|
+
refuse5({
|
|
184926
185863
|
code: JSON_RPC_ERROR_CODES.resourceNotFound,
|
|
184927
185864
|
message: `Unknown ACP session: ${sessionId}`,
|
|
184928
185865
|
data: { sessionId, cwd }
|
|
@@ -184944,17 +185881,17 @@ Switch with /model <model>; it applies from the next turn.`;
|
|
|
184944
185881
|
const configId = requireStringField(obj, "configId", method);
|
|
184945
185882
|
const value = obj["value"];
|
|
184946
185883
|
if (typeof value !== "string" || value.length === 0) {
|
|
184947
|
-
|
|
185884
|
+
refuse5(invalidParams(`${method}: value must be a non-empty string`, { received: value }));
|
|
184948
185885
|
}
|
|
184949
185886
|
if (registry.get(sessionId) === undefined) {
|
|
184950
|
-
|
|
185887
|
+
refuse5({
|
|
184951
185888
|
code: JSON_RPC_ERROR_CODES.resourceNotFound,
|
|
184952
185889
|
message: `Unknown ACP session: ${sessionId}`,
|
|
184953
185890
|
data: { sessionId }
|
|
184954
185891
|
});
|
|
184955
185892
|
}
|
|
184956
185893
|
if (configId !== ACP_MODEL_CONFIG_ID) {
|
|
184957
|
-
|
|
185894
|
+
refuse5(invalidParams(`${method}: keryx has no config option "${configId}"; the only one is "${ACP_MODEL_CONFIG_ID}"`, {
|
|
184958
185895
|
configId
|
|
184959
185896
|
}));
|
|
184960
185897
|
}
|
|
@@ -186676,40 +187613,40 @@ var init_serve_turn = __esm(() => {
|
|
|
186676
187613
|
});
|
|
186677
187614
|
|
|
186678
187615
|
// src/lib/serve-server.ts
|
|
186679
|
-
function
|
|
187616
|
+
function refuse6(reason, message) {
|
|
186680
187617
|
return { ok: false, state: "refused", reason, message };
|
|
186681
187618
|
}
|
|
186682
187619
|
function resolveServeStartup(input) {
|
|
186683
187620
|
const { config, credential } = input;
|
|
186684
187621
|
if (config === null) {
|
|
186685
|
-
return
|
|
187622
|
+
return refuse6("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
|
|
186686
187623
|
}
|
|
186687
187624
|
if (!config.enabled) {
|
|
186688
|
-
return
|
|
187625
|
+
return refuse6("disabled", serveConfigAdvice("valid"));
|
|
186689
187626
|
}
|
|
186690
187627
|
if (config.credentialRef.store !== "auth-json") {
|
|
186691
|
-
return
|
|
187628
|
+
return refuse6("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
|
|
186692
187629
|
}
|
|
186693
187630
|
if (credential.status === "unreadable") {
|
|
186694
|
-
return
|
|
187631
|
+
return refuse6("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
|
|
186695
187632
|
}
|
|
186696
187633
|
if (credential.status === "absent") {
|
|
186697
|
-
return
|
|
187634
|
+
return refuse6("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
|
|
186698
187635
|
}
|
|
186699
187636
|
if (credential.record.id !== config.credentialRef.id) {
|
|
186700
|
-
return
|
|
187637
|
+
return refuse6("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
|
|
186701
187638
|
}
|
|
186702
187639
|
const nonLoopback = !isLoopbackAddress(config.bind.address);
|
|
186703
187640
|
if (nonLoopback && config.bind.acknowledgeNonLoopback !== true) {
|
|
186704
|
-
return
|
|
187641
|
+
return refuse6("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
|
|
186705
187642
|
}
|
|
186706
187643
|
const remoteProfile = resolveRemoteProfile(config.profile);
|
|
186707
187644
|
if (remoteProfile === null) {
|
|
186708
|
-
return
|
|
187645
|
+
return refuse6("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
|
|
186709
187646
|
}
|
|
186710
187647
|
const comparison = compareProfiles((input.localBaseline ?? localBaselineProfile)(), remoteProfile);
|
|
186711
187648
|
if (!comparison.ok) {
|
|
186712
|
-
return
|
|
187649
|
+
return refuse6("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
|
|
186713
187650
|
}
|
|
186714
187651
|
return { ok: true, config, credential: credential.record, nonLoopback, profile: remoteProfile };
|
|
186715
187652
|
}
|
|
@@ -187057,11 +187994,11 @@ async function startServeListener(input) {
|
|
|
187057
187994
|
error: internalErrorResponse
|
|
187058
187995
|
});
|
|
187059
187996
|
} catch (error) {
|
|
187060
|
-
return
|
|
187997
|
+
return refuse6("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187061
187998
|
}
|
|
187062
187999
|
if (typeof server.port !== "number" || server.port <= 0) {
|
|
187063
188000
|
await server.stop(true);
|
|
187064
|
-
return
|
|
188001
|
+
return refuse6("bind-failed", "the listener reported no bound port");
|
|
187065
188002
|
}
|
|
187066
188003
|
boundPort = server.port;
|
|
187067
188004
|
try {
|
|
@@ -187111,6 +188048,8 @@ var init_serve_server = __esm(() => {
|
|
|
187111
188048
|
["heartbeat", "POST"],
|
|
187112
188049
|
["reply", "POST"],
|
|
187113
188050
|
["approval", "POST"],
|
|
188051
|
+
["prompt", "POST"],
|
|
188052
|
+
["state", "POST"],
|
|
187114
188053
|
["ack", "POST"],
|
|
187115
188054
|
["stream", "GET"],
|
|
187116
188055
|
["channels-status", "GET"],
|
|
@@ -187340,11 +188279,54 @@ function createHttpBotApi(options) {
|
|
|
187340
188279
|
if (params.messageThreadId !== undefined) {
|
|
187341
188280
|
body.message_thread_id = params.messageThreadId;
|
|
187342
188281
|
}
|
|
188282
|
+
if (params.parseMode !== undefined) {
|
|
188283
|
+
body.parse_mode = params.parseMode;
|
|
188284
|
+
}
|
|
187343
188285
|
if (params.inlineKeyboard !== undefined) {
|
|
187344
188286
|
body.reply_markup = { inline_keyboard: params.inlineKeyboard };
|
|
187345
188287
|
}
|
|
187346
188288
|
return call("sendMessage", body);
|
|
187347
188289
|
},
|
|
188290
|
+
async editMessageReplyMarkup(params) {
|
|
188291
|
+
await call("editMessageReplyMarkup", {
|
|
188292
|
+
chat_id: params.chatId,
|
|
188293
|
+
message_id: params.messageId,
|
|
188294
|
+
reply_markup: { inline_keyboard: params.inlineKeyboard ?? [] }
|
|
188295
|
+
});
|
|
188296
|
+
},
|
|
188297
|
+
async editMessageText(params) {
|
|
188298
|
+
const body = {
|
|
188299
|
+
chat_id: params.chatId,
|
|
188300
|
+
message_id: params.messageId,
|
|
188301
|
+
text: params.text,
|
|
188302
|
+
reply_markup: { inline_keyboard: params.inlineKeyboard ?? [] }
|
|
188303
|
+
};
|
|
188304
|
+
if (params.parseMode !== undefined) {
|
|
188305
|
+
body.parse_mode = params.parseMode;
|
|
188306
|
+
}
|
|
188307
|
+
await call("editMessageText", body);
|
|
188308
|
+
},
|
|
188309
|
+
async setMyCommands(params) {
|
|
188310
|
+
const body = { commands: params.commands };
|
|
188311
|
+
if (params.chatId !== undefined) {
|
|
188312
|
+
body.scope = { type: "chat", chat_id: params.chatId };
|
|
188313
|
+
}
|
|
188314
|
+
await call("setMyCommands", body);
|
|
188315
|
+
},
|
|
188316
|
+
async setMessageReaction(params) {
|
|
188317
|
+
await call("setMessageReaction", {
|
|
188318
|
+
chat_id: params.chatId,
|
|
188319
|
+
message_id: params.messageId,
|
|
188320
|
+
reaction: params.emoji === undefined ? [] : [{ type: "emoji", emoji: params.emoji }]
|
|
188321
|
+
});
|
|
188322
|
+
},
|
|
188323
|
+
async sendChatAction(params) {
|
|
188324
|
+
const body = { chat_id: params.chatId, action: params.action };
|
|
188325
|
+
if (params.messageThreadId !== undefined) {
|
|
188326
|
+
body.message_thread_id = params.messageThreadId;
|
|
188327
|
+
}
|
|
188328
|
+
await call("sendChatAction", body);
|
|
188329
|
+
},
|
|
187348
188330
|
async createForumTopic(params) {
|
|
187349
188331
|
return call("createForumTopic", { chat_id: params.chatId, name: params.name });
|
|
187350
188332
|
},
|
|
@@ -187385,6 +188367,461 @@ var init_bot_api_http = __esm(() => {
|
|
|
187385
188367
|
init_types11();
|
|
187386
188368
|
});
|
|
187387
188369
|
|
|
188370
|
+
// src/remote/format.ts
|
|
188371
|
+
function fenceOpening(line) {
|
|
188372
|
+
const match = FENCE_LINE2.exec(line);
|
|
188373
|
+
if (match === null) {
|
|
188374
|
+
return;
|
|
188375
|
+
}
|
|
188376
|
+
const marker = match[1];
|
|
188377
|
+
const info = match[2].trim();
|
|
188378
|
+
if (marker.length > MAX_FENCE_MARKER || marker.startsWith("`") && info.includes("`")) {
|
|
188379
|
+
return;
|
|
188380
|
+
}
|
|
188381
|
+
return { marker, info };
|
|
188382
|
+
}
|
|
188383
|
+
function openingFence(line) {
|
|
188384
|
+
const opening = fenceOpening(line);
|
|
188385
|
+
if (opening === undefined) {
|
|
188386
|
+
return;
|
|
188387
|
+
}
|
|
188388
|
+
const { marker, info } = opening;
|
|
188389
|
+
return { marker, header: info.length > 0 && info.length <= MAX_FENCE_INFO ? `${marker}${info}` : marker };
|
|
188390
|
+
}
|
|
188391
|
+
function closesFence(line, fence) {
|
|
188392
|
+
const match = FENCE_LINE2.exec(line);
|
|
188393
|
+
if (match === null) {
|
|
188394
|
+
return false;
|
|
188395
|
+
}
|
|
188396
|
+
const run = match[1];
|
|
188397
|
+
return run[0] === fence.marker[0] && run.length >= fence.marker.length && match[2].trim().length === 0;
|
|
188398
|
+
}
|
|
188399
|
+
function fenceOverhead(fence) {
|
|
188400
|
+
return fence === undefined ? 0 : fence.header.length + 1 + (fence.marker.length + 1);
|
|
188401
|
+
}
|
|
188402
|
+
function cutAt(text, max) {
|
|
188403
|
+
if (text.length <= max) {
|
|
188404
|
+
return text.length;
|
|
188405
|
+
}
|
|
188406
|
+
for (let index = max;index > 0; index -= 1) {
|
|
188407
|
+
const ch = text[index - 1];
|
|
188408
|
+
if (ch === " " || ch === "\t") {
|
|
188409
|
+
return index;
|
|
188410
|
+
}
|
|
188411
|
+
}
|
|
188412
|
+
const code = text.charCodeAt(max - 1);
|
|
188413
|
+
return code >= 55296 && code <= 56319 ? max - 1 : max;
|
|
188414
|
+
}
|
|
188415
|
+
function toPieces(text, capacity) {
|
|
188416
|
+
const pieces = [];
|
|
188417
|
+
let fence;
|
|
188418
|
+
for (const raw of text.split(/(?<=\n)/)) {
|
|
188419
|
+
const line = raw.endsWith(`
|
|
188420
|
+
`) ? raw.slice(0, -1) : raw;
|
|
188421
|
+
const before = fence;
|
|
188422
|
+
if (fence === undefined) {
|
|
188423
|
+
fence = openingFence(line);
|
|
188424
|
+
} else if (closesFence(line, fence)) {
|
|
188425
|
+
fence = undefined;
|
|
188426
|
+
}
|
|
188427
|
+
const room = Math.max(8, capacity - Math.max(fenceOverhead(before), fenceOverhead(fence)));
|
|
188428
|
+
let rest = raw;
|
|
188429
|
+
while (rest.length > room) {
|
|
188430
|
+
const cut = cutAt(rest, room);
|
|
188431
|
+
pieces.push({ text: rest.slice(0, cut), fence: before ?? fence });
|
|
188432
|
+
rest = rest.slice(cut);
|
|
188433
|
+
}
|
|
188434
|
+
if (rest.length > 0) {
|
|
188435
|
+
pieces.push({ text: rest, fence });
|
|
188436
|
+
}
|
|
188437
|
+
}
|
|
188438
|
+
return pieces;
|
|
188439
|
+
}
|
|
188440
|
+
function pack(text, capacity) {
|
|
188441
|
+
const parts = [];
|
|
188442
|
+
let current = "";
|
|
188443
|
+
let hasContent = false;
|
|
188444
|
+
let open;
|
|
188445
|
+
const finish = () => {
|
|
188446
|
+
let body = current.trimEnd();
|
|
188447
|
+
if (!hasContent || body.length === 0) {
|
|
188448
|
+
current = "";
|
|
188449
|
+
hasContent = false;
|
|
188450
|
+
return;
|
|
188451
|
+
}
|
|
188452
|
+
if (open !== undefined) {
|
|
188453
|
+
body += `
|
|
188454
|
+
${open.marker}`;
|
|
188455
|
+
}
|
|
188456
|
+
parts.push(body);
|
|
188457
|
+
current = open === undefined ? "" : `${open.header}
|
|
188458
|
+
`;
|
|
188459
|
+
hasContent = false;
|
|
188460
|
+
};
|
|
188461
|
+
for (const piece of toPieces(text, capacity)) {
|
|
188462
|
+
if (!hasContent && piece.text.trim().length === 0) {
|
|
188463
|
+
continue;
|
|
188464
|
+
}
|
|
188465
|
+
const closing = piece.fence === undefined ? 0 : piece.fence.marker.length + 1;
|
|
188466
|
+
if (hasContent && current.length + piece.text.length + closing > capacity) {
|
|
188467
|
+
finish();
|
|
188468
|
+
if (piece.text.trim().length === 0) {
|
|
188469
|
+
continue;
|
|
188470
|
+
}
|
|
188471
|
+
}
|
|
188472
|
+
current += piece.text;
|
|
188473
|
+
hasContent = true;
|
|
188474
|
+
open = piece.fence;
|
|
188475
|
+
}
|
|
188476
|
+
finish();
|
|
188477
|
+
return parts;
|
|
188478
|
+
}
|
|
188479
|
+
function formatReply(text, limit = TELEGRAM_MAX_TEXT) {
|
|
188480
|
+
if (limit < MIN_LIMIT) {
|
|
188481
|
+
throw new RangeError(`formatReply: limit must be at least ${MIN_LIMIT}`);
|
|
188482
|
+
}
|
|
188483
|
+
text = text.replace(/\r\n?/g, `
|
|
188484
|
+
`);
|
|
188485
|
+
if (text.trim().length === 0) {
|
|
188486
|
+
return [];
|
|
188487
|
+
}
|
|
188488
|
+
if (text.length <= limit) {
|
|
188489
|
+
return [text];
|
|
188490
|
+
}
|
|
188491
|
+
for (let digits = 1;digits <= 3; digits += 1) {
|
|
188492
|
+
const labelLength = 2 * digits + 4;
|
|
188493
|
+
const parts = pack(text, limit - labelLength);
|
|
188494
|
+
if (String(parts.length).length > digits) {
|
|
188495
|
+
continue;
|
|
188496
|
+
}
|
|
188497
|
+
if (parts.length <= 1) {
|
|
188498
|
+
return parts;
|
|
188499
|
+
}
|
|
188500
|
+
return parts.map((part, index) => `(${index + 1}/${parts.length})
|
|
188501
|
+
${part}`);
|
|
188502
|
+
}
|
|
188503
|
+
const parts = pack(text, limit - 12).slice(0, 999);
|
|
188504
|
+
return parts.map((part, index) => `(${index + 1}/${parts.length})
|
|
188505
|
+
${part}`);
|
|
188506
|
+
}
|
|
188507
|
+
var MIN_LIMIT = 64, MAX_FENCE_MARKER = 16, MAX_FENCE_INFO = 40, FENCE_LINE2;
|
|
188508
|
+
var init_format2 = __esm(() => {
|
|
188509
|
+
init_types11();
|
|
188510
|
+
FENCE_LINE2 = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
188511
|
+
});
|
|
188512
|
+
|
|
188513
|
+
// src/remote/format-html.ts
|
|
188514
|
+
function escapeHtml2(text) {
|
|
188515
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
188516
|
+
}
|
|
188517
|
+
function escapeAttribute(text) {
|
|
188518
|
+
return escapeHtml2(text).replace(/"/g, """);
|
|
188519
|
+
}
|
|
188520
|
+
function isWord(char) {
|
|
188521
|
+
return char !== undefined && WORD.test(char);
|
|
188522
|
+
}
|
|
188523
|
+
function isAlnum(char) {
|
|
188524
|
+
return char !== undefined && ALNUM.test(char);
|
|
188525
|
+
}
|
|
188526
|
+
function isSpace(char) {
|
|
188527
|
+
return char !== undefined && SPACE.test(char);
|
|
188528
|
+
}
|
|
188529
|
+
function codeSpan(text, start) {
|
|
188530
|
+
const run = backtickRun(text, start);
|
|
188531
|
+
let index = start + run;
|
|
188532
|
+
while (index < text.length) {
|
|
188533
|
+
if (text[index] === "`") {
|
|
188534
|
+
const next = backtickRun(text, index);
|
|
188535
|
+
if (next === run) {
|
|
188536
|
+
let content = text.slice(start + run, index);
|
|
188537
|
+
if (content.length > 2 && content.startsWith(" ") && content.endsWith(" ") && content.trim().length > 0) {
|
|
188538
|
+
content = content.slice(1, -1);
|
|
188539
|
+
}
|
|
188540
|
+
return content.length === 0 ? undefined : { content, end: index + run };
|
|
188541
|
+
}
|
|
188542
|
+
index += next;
|
|
188543
|
+
} else {
|
|
188544
|
+
index += 1;
|
|
188545
|
+
}
|
|
188546
|
+
}
|
|
188547
|
+
return;
|
|
188548
|
+
}
|
|
188549
|
+
function backtickRun(text, start) {
|
|
188550
|
+
let end = start;
|
|
188551
|
+
while (text[end] === "`") {
|
|
188552
|
+
end += 1;
|
|
188553
|
+
}
|
|
188554
|
+
return end - start;
|
|
188555
|
+
}
|
|
188556
|
+
function opens(text, index, marker) {
|
|
188557
|
+
const next = text[index + marker.length];
|
|
188558
|
+
const prev = text[index - 1];
|
|
188559
|
+
if (next === undefined || isSpace(next) || prev === "/" || prev === "\\") {
|
|
188560
|
+
return false;
|
|
188561
|
+
}
|
|
188562
|
+
const char = marker[0];
|
|
188563
|
+
if (marker.length === 1) {
|
|
188564
|
+
if (next === char || SINGLE_OPEN_BAD.test(next)) {
|
|
188565
|
+
return false;
|
|
188566
|
+
}
|
|
188567
|
+
return char === "*" ? !isAlnum(prev) : !isWord(prev);
|
|
188568
|
+
}
|
|
188569
|
+
return char === "_" ? !isWord(prev) : true;
|
|
188570
|
+
}
|
|
188571
|
+
function closes(text, index, marker) {
|
|
188572
|
+
const prev = text[index - 1];
|
|
188573
|
+
const after = text[index + marker.length];
|
|
188574
|
+
const char = marker[0];
|
|
188575
|
+
if (prev === undefined || isSpace(prev) || after === char) {
|
|
188576
|
+
return false;
|
|
188577
|
+
}
|
|
188578
|
+
if (marker.length === 1) {
|
|
188579
|
+
if (prev === char || SINGLE_CLOSE_BAD.test(prev)) {
|
|
188580
|
+
return false;
|
|
188581
|
+
}
|
|
188582
|
+
return char === "*" ? !isAlnum(after) : !isWord(after);
|
|
188583
|
+
}
|
|
188584
|
+
return char === "_" ? !isWord(after) : true;
|
|
188585
|
+
}
|
|
188586
|
+
function findCloser(text, from, marker) {
|
|
188587
|
+
let index = from;
|
|
188588
|
+
while (index < text.length) {
|
|
188589
|
+
if (text[index] === "`") {
|
|
188590
|
+
const span = codeSpan(text, index);
|
|
188591
|
+
index = span === undefined ? index + backtickRun(text, index) : span.end;
|
|
188592
|
+
continue;
|
|
188593
|
+
}
|
|
188594
|
+
if (text.startsWith(marker, index) && closes(text, index, marker)) {
|
|
188595
|
+
return index;
|
|
188596
|
+
}
|
|
188597
|
+
index += 1;
|
|
188598
|
+
}
|
|
188599
|
+
return -1;
|
|
188600
|
+
}
|
|
188601
|
+
function delimited(text, index, marker, flag, context) {
|
|
188602
|
+
if (!opens(text, index, marker)) {
|
|
188603
|
+
return;
|
|
188604
|
+
}
|
|
188605
|
+
const close = findCloser(text, index + marker.length + 1, marker);
|
|
188606
|
+
if (close < 0) {
|
|
188607
|
+
return;
|
|
188608
|
+
}
|
|
188609
|
+
const content = text.slice(index + marker.length, close);
|
|
188610
|
+
if (content.trim().length === 0 || marker === "__" && !/\s/.test(content.trim())) {
|
|
188611
|
+
return;
|
|
188612
|
+
}
|
|
188613
|
+
const inner = renderInline2(content, { ...context, [flag]: true });
|
|
188614
|
+
const html = context[flag] ? inner : `<${TAGS2[flag]}>${inner}</${TAGS2[flag]}>`;
|
|
188615
|
+
return { html, end: close + marker.length };
|
|
188616
|
+
}
|
|
188617
|
+
function link(text, index, context) {
|
|
188618
|
+
if (context.link) {
|
|
188619
|
+
return;
|
|
188620
|
+
}
|
|
188621
|
+
const middle = text.indexOf("](", index + 1);
|
|
188622
|
+
if (middle < 0) {
|
|
188623
|
+
return;
|
|
188624
|
+
}
|
|
188625
|
+
const label = text.slice(index + 1, middle);
|
|
188626
|
+
if (label.trim().length === 0 || label.includes("]")) {
|
|
188627
|
+
return;
|
|
188628
|
+
}
|
|
188629
|
+
let depth = 1;
|
|
188630
|
+
let end = middle + 2;
|
|
188631
|
+
while (end < text.length && depth > 0) {
|
|
188632
|
+
if (text[end] === "(") {
|
|
188633
|
+
depth += 1;
|
|
188634
|
+
} else if (text[end] === ")") {
|
|
188635
|
+
depth -= 1;
|
|
188636
|
+
}
|
|
188637
|
+
end += 1;
|
|
188638
|
+
}
|
|
188639
|
+
if (depth !== 0) {
|
|
188640
|
+
return;
|
|
188641
|
+
}
|
|
188642
|
+
const url = text.slice(middle + 2, end - 1);
|
|
188643
|
+
if (!HTTP_URL.test(url)) {
|
|
188644
|
+
return;
|
|
188645
|
+
}
|
|
188646
|
+
return { html: `<a href="${escapeAttribute(url)}">${renderInline2(label, { ...context, link: true })}</a>`, end };
|
|
188647
|
+
}
|
|
188648
|
+
function renderInline2(text, context) {
|
|
188649
|
+
let out = "";
|
|
188650
|
+
let index = 0;
|
|
188651
|
+
while (index < text.length) {
|
|
188652
|
+
const char = text[index];
|
|
188653
|
+
if (char === "`") {
|
|
188654
|
+
const span = context.link ? undefined : codeSpan(text, index);
|
|
188655
|
+
if (span === undefined) {
|
|
188656
|
+
const run = backtickRun(text, index);
|
|
188657
|
+
out += text.slice(index, index + run);
|
|
188658
|
+
index += run;
|
|
188659
|
+
} else {
|
|
188660
|
+
out += context.bold || context.italic || context.strike ? escapeHtml2(span.content) : `<code>${escapeHtml2(span.content)}</code>`;
|
|
188661
|
+
index = span.end;
|
|
188662
|
+
}
|
|
188663
|
+
continue;
|
|
188664
|
+
}
|
|
188665
|
+
if (char === "[") {
|
|
188666
|
+
const found = link(text, index, context);
|
|
188667
|
+
if (found !== undefined) {
|
|
188668
|
+
out += found.html;
|
|
188669
|
+
index = found.end;
|
|
188670
|
+
continue;
|
|
188671
|
+
}
|
|
188672
|
+
} else if (text.startsWith("**", index) || text.startsWith("__", index) || text.startsWith("~~", index)) {
|
|
188673
|
+
const marker = text.slice(index, index + 2);
|
|
188674
|
+
const found = delimited(text, index, marker, marker === "~~" ? "strike" : "bold", context);
|
|
188675
|
+
if (found === undefined) {
|
|
188676
|
+
out += marker;
|
|
188677
|
+
index += 2;
|
|
188678
|
+
} else {
|
|
188679
|
+
out += found.html;
|
|
188680
|
+
index = found.end;
|
|
188681
|
+
}
|
|
188682
|
+
continue;
|
|
188683
|
+
} else if (char === "*" || char === "_") {
|
|
188684
|
+
const found = delimited(text, index, char, "italic", context);
|
|
188685
|
+
if (found !== undefined) {
|
|
188686
|
+
out += found.html;
|
|
188687
|
+
index = found.end;
|
|
188688
|
+
continue;
|
|
188689
|
+
}
|
|
188690
|
+
}
|
|
188691
|
+
out += escapeHtml2(char);
|
|
188692
|
+
index += 1;
|
|
188693
|
+
}
|
|
188694
|
+
return out;
|
|
188695
|
+
}
|
|
188696
|
+
function renderTelegramHtml(part) {
|
|
188697
|
+
const lines = part.replace(/\r\n?/g, `
|
|
188698
|
+
`).split(`
|
|
188699
|
+
`);
|
|
188700
|
+
let out = "";
|
|
188701
|
+
let first = true;
|
|
188702
|
+
let afterBlock = false;
|
|
188703
|
+
const push = (html, block) => {
|
|
188704
|
+
if (!first && !afterBlock) {
|
|
188705
|
+
out += `
|
|
188706
|
+
`;
|
|
188707
|
+
}
|
|
188708
|
+
out += html;
|
|
188709
|
+
first = false;
|
|
188710
|
+
afterBlock = block;
|
|
188711
|
+
};
|
|
188712
|
+
let index = 0;
|
|
188713
|
+
while (index < lines.length) {
|
|
188714
|
+
const line = lines[index];
|
|
188715
|
+
const fence = fenceOpening(line);
|
|
188716
|
+
if (fence !== undefined) {
|
|
188717
|
+
const body = [];
|
|
188718
|
+
let cursor = index + 1;
|
|
188719
|
+
while (cursor < lines.length && !closesFence(lines[cursor], fence)) {
|
|
188720
|
+
body.push(lines[cursor]);
|
|
188721
|
+
cursor += 1;
|
|
188722
|
+
}
|
|
188723
|
+
const closed = cursor < lines.length;
|
|
188724
|
+
const code = body.join(`
|
|
188725
|
+
`);
|
|
188726
|
+
if (code.trim().length === 0) {
|
|
188727
|
+
push(escapeHtml2(lines.slice(index, closed ? cursor + 1 : cursor).join(`
|
|
188728
|
+
`)), false);
|
|
188729
|
+
} else {
|
|
188730
|
+
const language = fence.info.split(/\s+/)[0];
|
|
188731
|
+
const open = LANGUAGE.test(language) ? `<pre><code class="language-${escapeAttribute(language)}">` : "<pre><code>";
|
|
188732
|
+
push(`${open}${escapeHtml2(code)}</code></pre>`, true);
|
|
188733
|
+
}
|
|
188734
|
+
index = closed ? cursor + 1 : cursor;
|
|
188735
|
+
continue;
|
|
188736
|
+
}
|
|
188737
|
+
const quote = QUOTE.exec(line);
|
|
188738
|
+
if (quote !== null) {
|
|
188739
|
+
const quoted = [];
|
|
188740
|
+
while (index < lines.length) {
|
|
188741
|
+
const next = QUOTE.exec(lines[index]);
|
|
188742
|
+
if (next === null) {
|
|
188743
|
+
break;
|
|
188744
|
+
}
|
|
188745
|
+
quoted.push(next[1] ?? "");
|
|
188746
|
+
index += 1;
|
|
188747
|
+
}
|
|
188748
|
+
if (quoted.every((entry) => entry.trim().length === 0)) {
|
|
188749
|
+
push(escapeHtml2(lines.slice(index - quoted.length, index).join(`
|
|
188750
|
+
`)), false);
|
|
188751
|
+
} else {
|
|
188752
|
+
const tag = quoted.length >= EXPANDABLE_QUOTE_LINES ? "blockquote expandable" : "blockquote";
|
|
188753
|
+
push(`<${tag}>${quoted.map((entry) => renderInline2(entry, OUTSIDE)).join(`
|
|
188754
|
+
`)}</blockquote>`, true);
|
|
188755
|
+
}
|
|
188756
|
+
continue;
|
|
188757
|
+
}
|
|
188758
|
+
index += 1;
|
|
188759
|
+
const heading = HEADING2.exec(line);
|
|
188760
|
+
if (heading !== null && heading[1].trim().length > 0) {
|
|
188761
|
+
push(`<b>${renderInline2(heading[1], { ...OUTSIDE, bold: true })}</b>`, false);
|
|
188762
|
+
continue;
|
|
188763
|
+
}
|
|
188764
|
+
const bullet = BULLET.exec(line);
|
|
188765
|
+
if (bullet !== null) {
|
|
188766
|
+
push(`${bullet[1]}\u2022 ${renderInline2(bullet[2], OUTSIDE)}`, false);
|
|
188767
|
+
continue;
|
|
188768
|
+
}
|
|
188769
|
+
push(renderInline2(line, OUTSIDE), false);
|
|
188770
|
+
}
|
|
188771
|
+
return out;
|
|
188772
|
+
}
|
|
188773
|
+
function isEntityParseError(error) {
|
|
188774
|
+
return isBotApiError(error) && error.kind === "rejected" && error.status === 400 && /can't parse entities/i.test(error.message);
|
|
188775
|
+
}
|
|
188776
|
+
async function sendHtml(api, params, onFallback) {
|
|
188777
|
+
const { parseMode: _ignored, ...plain } = params;
|
|
188778
|
+
try {
|
|
188779
|
+
return await api.sendMessage({ ...plain, text: renderTelegramHtml(params.text), parseMode: "HTML" });
|
|
188780
|
+
} catch (error) {
|
|
188781
|
+
if (!isEntityParseError(error)) {
|
|
188782
|
+
throw error;
|
|
188783
|
+
}
|
|
188784
|
+
try {
|
|
188785
|
+
onFallback?.(error);
|
|
188786
|
+
} catch {}
|
|
188787
|
+
return api.sendMessage(plain);
|
|
188788
|
+
}
|
|
188789
|
+
}
|
|
188790
|
+
async function editHtml(api, params, onFallback) {
|
|
188791
|
+
try {
|
|
188792
|
+
await api.editMessageText({ ...params, text: renderTelegramHtml(params.text), parseMode: "HTML" });
|
|
188793
|
+
} catch (error) {
|
|
188794
|
+
if (!isEntityParseError(error)) {
|
|
188795
|
+
throw error;
|
|
188796
|
+
}
|
|
188797
|
+
try {
|
|
188798
|
+
onFallback?.(error);
|
|
188799
|
+
} catch {}
|
|
188800
|
+
await api.editMessageText(params);
|
|
188801
|
+
}
|
|
188802
|
+
}
|
|
188803
|
+
function isNotModified(error) {
|
|
188804
|
+
return isBotApiError(error) && error.kind === "rejected" && /message is not modified/i.test(error.message);
|
|
188805
|
+
}
|
|
188806
|
+
var OUTSIDE, WORD, ALNUM, SPACE, SINGLE_OPEN_BAD, SINGLE_CLOSE_BAD, HTTP_URL, LANGUAGE, EXPANDABLE_QUOTE_LINES = 8, TAGS2, HEADING2, BULLET, QUOTE, SIMPLE_TAGS;
|
|
188807
|
+
var init_format_html = __esm(() => {
|
|
188808
|
+
init_format2();
|
|
188809
|
+
init_types11();
|
|
188810
|
+
OUTSIDE = { bold: false, italic: false, strike: false, link: false };
|
|
188811
|
+
WORD = /[\p{L}\p{N}_]/u;
|
|
188812
|
+
ALNUM = /[\p{L}\p{N}]/u;
|
|
188813
|
+
SPACE = /\s/;
|
|
188814
|
+
SINGLE_OPEN_BAD = /[.,;:!?)\]}/\\|]/;
|
|
188815
|
+
SINGLE_CLOSE_BAD = /[([{/\\|]/;
|
|
188816
|
+
HTTP_URL = /^https?:\/\/[^\s<>"'`]+$/i;
|
|
188817
|
+
LANGUAGE = /^[A-Za-z0-9_+#.-]{1,32}$/;
|
|
188818
|
+
TAGS2 = { bold: "b", italic: "i", strike: "s" };
|
|
188819
|
+
HEADING2 = /^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
|
|
188820
|
+
BULLET = /^([ \t]*)[-*][ \t]+(.*)$/;
|
|
188821
|
+
QUOTE = /^ {0,3}>(?: (.*))?$/;
|
|
188822
|
+
SIMPLE_TAGS = new Set(["b", "strong", "i", "em", "u", "ins", "s", "strike", "del"]);
|
|
188823
|
+
});
|
|
188824
|
+
|
|
187388
188825
|
// src/remote/durable-log.ts
|
|
187389
188826
|
import { renameSync as renameSync6 } from "fs";
|
|
187390
188827
|
|
|
@@ -187699,139 +189136,6 @@ var init_journal2 = __esm(() => {
|
|
|
187699
189136
|
init_paths4();
|
|
187700
189137
|
});
|
|
187701
189138
|
|
|
187702
|
-
// src/remote/format.ts
|
|
187703
|
-
function openingFence(line) {
|
|
187704
|
-
const match = FENCE_LINE2.exec(line);
|
|
187705
|
-
if (match === null) {
|
|
187706
|
-
return;
|
|
187707
|
-
}
|
|
187708
|
-
const marker = match[1];
|
|
187709
|
-
const info = match[2].trim();
|
|
187710
|
-
if (marker.length > MAX_FENCE_MARKER || marker.startsWith("`") && info.includes("`")) {
|
|
187711
|
-
return;
|
|
187712
|
-
}
|
|
187713
|
-
return { marker, header: info.length > 0 && info.length <= MAX_FENCE_INFO ? `${marker}${info}` : marker };
|
|
187714
|
-
}
|
|
187715
|
-
function closesFence(line, fence) {
|
|
187716
|
-
const match = FENCE_LINE2.exec(line);
|
|
187717
|
-
if (match === null) {
|
|
187718
|
-
return false;
|
|
187719
|
-
}
|
|
187720
|
-
const run = match[1];
|
|
187721
|
-
return run[0] === fence.marker[0] && run.length >= fence.marker.length && match[2].trim().length === 0;
|
|
187722
|
-
}
|
|
187723
|
-
function fenceOverhead(fence) {
|
|
187724
|
-
return fence === undefined ? 0 : fence.header.length + 1 + (fence.marker.length + 1);
|
|
187725
|
-
}
|
|
187726
|
-
function cutAt(text, max) {
|
|
187727
|
-
if (text.length <= max) {
|
|
187728
|
-
return text.length;
|
|
187729
|
-
}
|
|
187730
|
-
for (let index = max;index > 0; index -= 1) {
|
|
187731
|
-
const ch = text[index - 1];
|
|
187732
|
-
if (ch === " " || ch === "\t") {
|
|
187733
|
-
return index;
|
|
187734
|
-
}
|
|
187735
|
-
}
|
|
187736
|
-
const code = text.charCodeAt(max - 1);
|
|
187737
|
-
return code >= 55296 && code <= 56319 ? max - 1 : max;
|
|
187738
|
-
}
|
|
187739
|
-
function toPieces(text, capacity) {
|
|
187740
|
-
const pieces = [];
|
|
187741
|
-
let fence;
|
|
187742
|
-
for (const raw of text.split(/(?<=\n)/)) {
|
|
187743
|
-
const line = raw.endsWith(`
|
|
187744
|
-
`) ? raw.slice(0, -1) : raw;
|
|
187745
|
-
const before = fence;
|
|
187746
|
-
if (fence === undefined) {
|
|
187747
|
-
fence = openingFence(line);
|
|
187748
|
-
} else if (closesFence(line, fence)) {
|
|
187749
|
-
fence = undefined;
|
|
187750
|
-
}
|
|
187751
|
-
const room = Math.max(8, capacity - Math.max(fenceOverhead(before), fenceOverhead(fence)));
|
|
187752
|
-
let rest = raw;
|
|
187753
|
-
while (rest.length > room) {
|
|
187754
|
-
const cut = cutAt(rest, room);
|
|
187755
|
-
pieces.push({ text: rest.slice(0, cut), fence: before ?? fence });
|
|
187756
|
-
rest = rest.slice(cut);
|
|
187757
|
-
}
|
|
187758
|
-
if (rest.length > 0) {
|
|
187759
|
-
pieces.push({ text: rest, fence });
|
|
187760
|
-
}
|
|
187761
|
-
}
|
|
187762
|
-
return pieces;
|
|
187763
|
-
}
|
|
187764
|
-
function pack(text, capacity) {
|
|
187765
|
-
const parts = [];
|
|
187766
|
-
let current = "";
|
|
187767
|
-
let hasContent = false;
|
|
187768
|
-
let open;
|
|
187769
|
-
const finish = () => {
|
|
187770
|
-
let body = current.trimEnd();
|
|
187771
|
-
if (!hasContent || body.length === 0) {
|
|
187772
|
-
current = "";
|
|
187773
|
-
hasContent = false;
|
|
187774
|
-
return;
|
|
187775
|
-
}
|
|
187776
|
-
if (open !== undefined) {
|
|
187777
|
-
body += `
|
|
187778
|
-
${open.marker}`;
|
|
187779
|
-
}
|
|
187780
|
-
parts.push(body);
|
|
187781
|
-
current = open === undefined ? "" : `${open.header}
|
|
187782
|
-
`;
|
|
187783
|
-
hasContent = false;
|
|
187784
|
-
};
|
|
187785
|
-
for (const piece of toPieces(text, capacity)) {
|
|
187786
|
-
if (!hasContent && piece.text.trim().length === 0) {
|
|
187787
|
-
continue;
|
|
187788
|
-
}
|
|
187789
|
-
const closing = piece.fence === undefined ? 0 : piece.fence.marker.length + 1;
|
|
187790
|
-
if (hasContent && current.length + piece.text.length + closing > capacity) {
|
|
187791
|
-
finish();
|
|
187792
|
-
if (piece.text.trim().length === 0) {
|
|
187793
|
-
continue;
|
|
187794
|
-
}
|
|
187795
|
-
}
|
|
187796
|
-
current += piece.text;
|
|
187797
|
-
hasContent = true;
|
|
187798
|
-
open = piece.fence;
|
|
187799
|
-
}
|
|
187800
|
-
finish();
|
|
187801
|
-
return parts;
|
|
187802
|
-
}
|
|
187803
|
-
function formatReply(text, limit = TELEGRAM_MAX_TEXT) {
|
|
187804
|
-
if (limit < MIN_LIMIT) {
|
|
187805
|
-
throw new RangeError(`formatReply: limit must be at least ${MIN_LIMIT}`);
|
|
187806
|
-
}
|
|
187807
|
-
if (text.trim().length === 0) {
|
|
187808
|
-
return [];
|
|
187809
|
-
}
|
|
187810
|
-
if (text.length <= limit) {
|
|
187811
|
-
return [text];
|
|
187812
|
-
}
|
|
187813
|
-
for (let digits = 1;digits <= 3; digits += 1) {
|
|
187814
|
-
const labelLength = 2 * digits + 4;
|
|
187815
|
-
const parts = pack(text, limit - labelLength);
|
|
187816
|
-
if (String(parts.length).length > digits) {
|
|
187817
|
-
continue;
|
|
187818
|
-
}
|
|
187819
|
-
if (parts.length <= 1) {
|
|
187820
|
-
return parts;
|
|
187821
|
-
}
|
|
187822
|
-
return parts.map((part, index) => `(${index + 1}/${parts.length})
|
|
187823
|
-
${part}`);
|
|
187824
|
-
}
|
|
187825
|
-
const parts = pack(text, limit - 12).slice(0, 999);
|
|
187826
|
-
return parts.map((part, index) => `(${index + 1}/${parts.length})
|
|
187827
|
-
${part}`);
|
|
187828
|
-
}
|
|
187829
|
-
var MIN_LIMIT = 64, MAX_FENCE_MARKER = 16, MAX_FENCE_INFO = 40, FENCE_LINE2;
|
|
187830
|
-
var init_format2 = __esm(() => {
|
|
187831
|
-
init_types11();
|
|
187832
|
-
FENCE_LINE2 = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
187833
|
-
});
|
|
187834
|
-
|
|
187835
189139
|
// src/remote/outbound-queue.ts
|
|
187836
189140
|
import { randomUUID as randomUUID48 } from "crypto";
|
|
187837
189141
|
import path356 from "path";
|
|
@@ -187857,7 +189161,9 @@ class OutboundQueue {
|
|
|
187857
189161
|
now;
|
|
187858
189162
|
log;
|
|
187859
189163
|
onDrop;
|
|
189164
|
+
onFallback;
|
|
187860
189165
|
retryDelayMs;
|
|
189166
|
+
sentHooks = new Map;
|
|
187861
189167
|
blockedUntil = 0;
|
|
187862
189168
|
flushing;
|
|
187863
189169
|
constructor(options) {
|
|
@@ -187866,6 +189172,9 @@ class OutboundQueue {
|
|
|
187866
189172
|
this.onDrop = options.onDrop ?? (() => {
|
|
187867
189173
|
return;
|
|
187868
189174
|
});
|
|
189175
|
+
this.onFallback = options.onFallback ?? (() => {
|
|
189176
|
+
return;
|
|
189177
|
+
});
|
|
187869
189178
|
this.retryDelayMs = options.retryDelayMs ?? 5000;
|
|
187870
189179
|
const directory = ensureRemoteDir(options.dir);
|
|
187871
189180
|
this.log = new DurableLog({
|
|
@@ -187895,8 +189204,12 @@ class OutboundQueue {
|
|
|
187895
189204
|
...message.keyboard === undefined || index !== parts.length - 1 ? {} : { keyboard: message.keyboard },
|
|
187896
189205
|
createdAt: this.now()
|
|
187897
189206
|
};
|
|
189207
|
+
if (message.onSent !== undefined && index === parts.length - 1) {
|
|
189208
|
+
this.sentHooks.set(entry.id, message.onSent);
|
|
189209
|
+
}
|
|
187898
189210
|
const { dropped } = this.log.add(entry);
|
|
187899
189211
|
for (const gone of dropped) {
|
|
189212
|
+
this.sentHooks.delete(gone.id);
|
|
187900
189213
|
this.onDrop(gone, "outbound queue is full; oldest message dropped");
|
|
187901
189214
|
}
|
|
187902
189215
|
entries.push(entry);
|
|
@@ -187907,6 +189220,7 @@ class OutboundQueue {
|
|
|
187907
189220
|
let removed = 0;
|
|
187908
189221
|
for (const entry of this.log.pending()) {
|
|
187909
189222
|
if (entry.chatId === chatId && entry.threadId === threadId) {
|
|
189223
|
+
this.sentHooks.delete(entry.id);
|
|
187910
189224
|
this.log.ack(entry.id);
|
|
187911
189225
|
removed += 1;
|
|
187912
189226
|
}
|
|
@@ -187921,6 +189235,21 @@ class OutboundQueue {
|
|
|
187921
189235
|
}
|
|
187922
189236
|
return this.flushing;
|
|
187923
189237
|
}
|
|
189238
|
+
notifySent(entry, messageId) {
|
|
189239
|
+
const hook = this.sentHooks.get(entry.id);
|
|
189240
|
+
if (hook === undefined) {
|
|
189241
|
+
return;
|
|
189242
|
+
}
|
|
189243
|
+
this.sentHooks.delete(entry.id);
|
|
189244
|
+
try {
|
|
189245
|
+
hook({
|
|
189246
|
+
messageId,
|
|
189247
|
+
chatId: entry.chatId,
|
|
189248
|
+
...entry.threadId === undefined ? {} : { threadId: entry.threadId },
|
|
189249
|
+
text: entry.text
|
|
189250
|
+
});
|
|
189251
|
+
} catch {}
|
|
189252
|
+
}
|
|
187924
189253
|
async run() {
|
|
187925
189254
|
let sent = 0;
|
|
187926
189255
|
let dropped = 0;
|
|
@@ -187934,14 +189263,15 @@ class OutboundQueue {
|
|
|
187934
189263
|
return { sent, dropped, remaining: this.log.size, retryInMs: wait, stoppedBy: "rate limited" };
|
|
187935
189264
|
}
|
|
187936
189265
|
try {
|
|
187937
|
-
await this.api
|
|
189266
|
+
const result = await sendHtml(this.api, {
|
|
187938
189267
|
chatId: entry.chatId,
|
|
187939
189268
|
text: entry.text,
|
|
187940
189269
|
...entry.threadId === undefined ? {} : { messageThreadId: entry.threadId },
|
|
187941
189270
|
...entry.keyboard === undefined ? {} : { inlineKeyboard: entry.keyboard }
|
|
187942
|
-
});
|
|
189271
|
+
}, () => this.onFallback(entry));
|
|
187943
189272
|
this.log.ack(entry.id);
|
|
187944
189273
|
sent += 1;
|
|
189274
|
+
this.notifySent(entry, result.message_id);
|
|
187945
189275
|
} catch (error) {
|
|
187946
189276
|
const description = describe(error);
|
|
187947
189277
|
if (isBotApiError(error) && error.kind === "rate-limited") {
|
|
@@ -187953,6 +189283,7 @@ class OutboundQueue {
|
|
|
187953
189283
|
return { sent, dropped, remaining: this.log.size, retryInMs: this.retryDelayMs, stoppedBy: description };
|
|
187954
189284
|
}
|
|
187955
189285
|
this.log.ack(entry.id);
|
|
189286
|
+
this.sentHooks.delete(entry.id);
|
|
187956
189287
|
dropped += 1;
|
|
187957
189288
|
this.onDrop(entry, description);
|
|
187958
189289
|
}
|
|
@@ -187968,9 +189299,28 @@ var init_outbound_queue = __esm(() => {
|
|
|
187968
189299
|
init_durable_log();
|
|
187969
189300
|
init_paths4();
|
|
187970
189301
|
init_format2();
|
|
189302
|
+
init_format_html();
|
|
187971
189303
|
init_types11();
|
|
187972
189304
|
});
|
|
187973
189305
|
|
|
189306
|
+
// src/remote/message-state.ts
|
|
189307
|
+
function isReactionForbidden(status, message) {
|
|
189308
|
+
if (status === 403) {
|
|
189309
|
+
return true;
|
|
189310
|
+
}
|
|
189311
|
+
return /REACTION_INVALID|REACTIONS?_(DISABLED|NOT_ALLOWED)|not enough rights|reactions? (are|is) (disabled|not allowed)|CHAT_ADMIN_REQUIRED/i.test(message);
|
|
189312
|
+
}
|
|
189313
|
+
var REACTION_FOR_STATE, TYPING_REFRESH_MS = 4000, STATE_CALL_TIMEOUT_MS = 5000, MAX_TRACKED_MESSAGES = 256;
|
|
189314
|
+
var init_message_state = __esm(() => {
|
|
189315
|
+
REACTION_FOR_STATE = {
|
|
189316
|
+
received: "\uD83D\uDC40",
|
|
189317
|
+
reading: "\uD83E\uDD14",
|
|
189318
|
+
working: "\u26A1",
|
|
189319
|
+
done: "\uD83D\uDC4D",
|
|
189320
|
+
failed: "\uD83D\uDC4E"
|
|
189321
|
+
};
|
|
189322
|
+
});
|
|
189323
|
+
|
|
187974
189324
|
// src/remote/poller.ts
|
|
187975
189325
|
function defaultSleep2(ms, signal) {
|
|
187976
189326
|
return new Promise((resolve) => {
|
|
@@ -188249,6 +189599,9 @@ class RemoteHub {
|
|
|
188249
189599
|
flushTimer;
|
|
188250
189600
|
sweepTimer;
|
|
188251
189601
|
stopped = false;
|
|
189602
|
+
tracked = new Map;
|
|
189603
|
+
typing = new Map;
|
|
189604
|
+
reactionsOff = false;
|
|
188252
189605
|
constructor(options) {
|
|
188253
189606
|
this.api = options.api;
|
|
188254
189607
|
this.config = options.config;
|
|
@@ -188275,7 +189628,8 @@ class RemoteHub {
|
|
|
188275
189628
|
api: this.api,
|
|
188276
189629
|
...options.dir === undefined ? {} : { dir: options.dir },
|
|
188277
189630
|
now: this.now,
|
|
188278
|
-
onDrop: (entry, reason) => this.event("outbound-dropped", `${reason}${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`)
|
|
189631
|
+
onDrop: (entry, reason) => this.event("outbound-dropped", `${reason}${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`),
|
|
189632
|
+
onFallback: (entry) => this.event("format-fallback", `Telegram refused the formatting; sent as plain text${entry.threadId === undefined ? "" : ` (topic ${entry.threadId})`}`)
|
|
188279
189633
|
});
|
|
188280
189634
|
this.journal = new RejectedJournal({ ...options.dir === undefined ? {} : { dir: options.dir }, now: this.now });
|
|
188281
189635
|
this.offset = new PollerState(options.dir === undefined ? {} : { dir: options.dir });
|
|
@@ -188321,6 +189675,14 @@ class RemoteHub {
|
|
|
188321
189675
|
for (const record of this.registry.records()) {
|
|
188322
189676
|
this.dispatch(nameKey(record.name));
|
|
188323
189677
|
}
|
|
189678
|
+
await this.publishCommandMenu();
|
|
189679
|
+
}
|
|
189680
|
+
async publishCommandMenu() {
|
|
189681
|
+
try {
|
|
189682
|
+
await this.api.setMyCommands({ commands: remoteMenu(), chatId: this.config.chatId });
|
|
189683
|
+
} catch (error) {
|
|
189684
|
+
this.event("menu-failed", describeError4(error));
|
|
189685
|
+
}
|
|
188324
189686
|
}
|
|
188325
189687
|
async stop() {
|
|
188326
189688
|
this.stopped = true;
|
|
@@ -188336,6 +189698,10 @@ class RemoteHub {
|
|
|
188336
189698
|
this.timers.clearTimeout(handle);
|
|
188337
189699
|
}
|
|
188338
189700
|
this.retryTimers.clear();
|
|
189701
|
+
for (const handle of this.typing.values()) {
|
|
189702
|
+
this.timers.clearInterval(handle);
|
|
189703
|
+
}
|
|
189704
|
+
this.typing.clear();
|
|
188339
189705
|
await this.poller.stop();
|
|
188340
189706
|
}
|
|
188341
189707
|
pollerStatus() {
|
|
@@ -188402,7 +189768,7 @@ class RemoteHub {
|
|
|
188402
189768
|
});
|
|
188403
189769
|
}
|
|
188404
189770
|
async sendGeneral(text) {
|
|
188405
|
-
await this.api
|
|
189771
|
+
await sendHtml(this.api, { chatId: this.config.chatId, text }, () => this.event("format-fallback", "Telegram refused the formatting; sent as plain text (General)"));
|
|
188406
189772
|
}
|
|
188407
189773
|
heartbeat(sessionId) {
|
|
188408
189774
|
return this.serial(async () => {
|
|
@@ -188438,6 +189804,7 @@ class RemoteHub {
|
|
|
188438
189804
|
record.unavailableSince = Math.min(now, record.lastHeartbeat + SESSION_LEASE_STALE_MS);
|
|
188439
189805
|
this.registry.save();
|
|
188440
189806
|
this.event("session-unavailable", record.name);
|
|
189807
|
+
this.endActivity(record.sessionId);
|
|
188441
189808
|
if (now - record.unavailableSince < this.config.orphanMs) {
|
|
188442
189809
|
this.queueStatus(record, `Session unavailable: the shell stopped responding. This topic is deleted in ${formatDuration(this.config.orphanMs)} unless the session returns.`);
|
|
188443
189810
|
}
|
|
@@ -188458,11 +189825,31 @@ class RemoteHub {
|
|
|
188458
189825
|
chatId: record.chatId,
|
|
188459
189826
|
threadId: record.threadId,
|
|
188460
189827
|
text,
|
|
188461
|
-
...options.keyboard === undefined ? {} : { keyboard: options.keyboard }
|
|
189828
|
+
...options.keyboard === undefined ? {} : { keyboard: options.keyboard },
|
|
189829
|
+
...options.onSent === undefined ? {} : { onSent: options.onSent }
|
|
188462
189830
|
});
|
|
188463
189831
|
await this.flushOutbound();
|
|
188464
189832
|
return true;
|
|
188465
189833
|
}
|
|
189834
|
+
async settleMessage(sessionId, messageId, text, shortReply = text) {
|
|
189835
|
+
const record = this.registry.bySession(sessionId);
|
|
189836
|
+
if (record === undefined) {
|
|
189837
|
+
return "gone";
|
|
189838
|
+
}
|
|
189839
|
+
try {
|
|
189840
|
+
await editHtml(this.api, { chatId: record.chatId, messageId, text }, () => this.event("format-fallback", `Telegram refused the formatting; edited as plain text (topic ${record.threadId})`));
|
|
189841
|
+
return "edited";
|
|
189842
|
+
} catch (error) {
|
|
189843
|
+
if (isNotModified(error)) {
|
|
189844
|
+
return "edited";
|
|
189845
|
+
}
|
|
189846
|
+
try {
|
|
189847
|
+
await this.api.editMessageReplyMarkup({ chatId: record.chatId, messageId });
|
|
189848
|
+
} catch {}
|
|
189849
|
+
await this.send(sessionId, shortReply);
|
|
189850
|
+
return "replied";
|
|
189851
|
+
}
|
|
189852
|
+
}
|
|
188466
189853
|
async flushOutbound() {
|
|
188467
189854
|
const result = await this.outbound.flush();
|
|
188468
189855
|
if (result.retryInMs !== undefined && !this.stopped && this.flushTimer === undefined) {
|
|
@@ -188631,6 +190018,7 @@ class RemoteHub {
|
|
|
188631
190018
|
}
|
|
188632
190019
|
async retire(record, why, strict = false) {
|
|
188633
190020
|
this.outbound.discardForThread(record.chatId, record.threadId);
|
|
190021
|
+
this.stopTyping(record);
|
|
188634
190022
|
try {
|
|
188635
190023
|
await this.api.deleteForumTopic({ chatId: record.chatId, messageThreadId: record.threadId });
|
|
188636
190024
|
} catch (error) {
|
|
@@ -188678,7 +190066,11 @@ class RemoteHub {
|
|
|
188678
190066
|
if (typeof message.text !== "string" || message.text.length === 0) {
|
|
188679
190067
|
return;
|
|
188680
190068
|
}
|
|
188681
|
-
const appended = this.inbound.append(key, { ...base, kind: "text", text: message.text });
|
|
190069
|
+
const appended = this.inbound.append(key, { ...base, kind: "text", text: message.text, messageId: message.message_id });
|
|
190070
|
+
if (appended.added) {
|
|
190071
|
+
const tracked = this.track(record, update.update_id, message.message_id);
|
|
190072
|
+
this.react(tracked, "received");
|
|
190073
|
+
}
|
|
188682
190074
|
return { key, dropped: appended.dropped };
|
|
188683
190075
|
}
|
|
188684
190076
|
if (query?.data === undefined) {
|
|
@@ -188691,6 +190083,108 @@ class RemoteHub {
|
|
|
188691
190083
|
});
|
|
188692
190084
|
return { key, dropped: appended.dropped, callbackQueryId: query.id };
|
|
188693
190085
|
}
|
|
190086
|
+
trackKey(threadId, updateId) {
|
|
190087
|
+
return `${threadId}:${updateId}`;
|
|
190088
|
+
}
|
|
190089
|
+
track(record, updateId, messageId) {
|
|
190090
|
+
const key = this.trackKey(record.threadId, updateId);
|
|
190091
|
+
const existing = this.tracked.get(key);
|
|
190092
|
+
if (existing !== undefined) {
|
|
190093
|
+
return existing;
|
|
190094
|
+
}
|
|
190095
|
+
const entry = { chatId: record.chatId, threadId: record.threadId, messageId, state: "received", chain: Promise.resolve() };
|
|
190096
|
+
this.tracked.set(key, entry);
|
|
190097
|
+
while (this.tracked.size > MAX_TRACKED_MESSAGES) {
|
|
190098
|
+
const oldest = this.tracked.keys().next().value;
|
|
190099
|
+
if (oldest === undefined) {
|
|
190100
|
+
break;
|
|
190101
|
+
}
|
|
190102
|
+
this.tracked.delete(oldest);
|
|
190103
|
+
}
|
|
190104
|
+
return entry;
|
|
190105
|
+
}
|
|
190106
|
+
react(message, state) {
|
|
190107
|
+
message.state = state;
|
|
190108
|
+
if (this.reactionsOff || this.stopped) {
|
|
190109
|
+
return;
|
|
190110
|
+
}
|
|
190111
|
+
message.chain = message.chain.then(async () => {
|
|
190112
|
+
if (this.reactionsOff) {
|
|
190113
|
+
return;
|
|
190114
|
+
}
|
|
190115
|
+
let timer;
|
|
190116
|
+
const timeout = new Promise((resolve) => {
|
|
190117
|
+
timer = this.timers.setTimeout(resolve, STATE_CALL_TIMEOUT_MS);
|
|
190118
|
+
});
|
|
190119
|
+
const call = this.api.setMessageReaction({ chatId: message.chatId, messageId: message.messageId, emoji: REACTION_FOR_STATE[state] }).then(() => {
|
|
190120
|
+
return;
|
|
190121
|
+
}, (error) => this.reactionFailed(error));
|
|
190122
|
+
await Promise.race([call, timeout]);
|
|
190123
|
+
this.timers.clearTimeout(timer);
|
|
190124
|
+
});
|
|
190125
|
+
}
|
|
190126
|
+
reactionFailed(error) {
|
|
190127
|
+
if (!isBotApiError(error) || error.kind !== "rejected" || !isReactionForbidden(error.status, error.message)) {
|
|
190128
|
+
return;
|
|
190129
|
+
}
|
|
190130
|
+
if (!this.reactionsOff) {
|
|
190131
|
+
this.reactionsOff = true;
|
|
190132
|
+
this.event("reactions-unavailable", `this bot may not react in the group; the topic shows typing only (${describeError4(error)})`);
|
|
190133
|
+
}
|
|
190134
|
+
}
|
|
190135
|
+
messageState(sessionId, updateId, state) {
|
|
190136
|
+
const record = this.registry.bySession(sessionId);
|
|
190137
|
+
if (record === undefined) {
|
|
190138
|
+
return false;
|
|
190139
|
+
}
|
|
190140
|
+
const message = this.tracked.get(this.trackKey(record.threadId, updateId));
|
|
190141
|
+
if (message === undefined) {
|
|
190142
|
+
return false;
|
|
190143
|
+
}
|
|
190144
|
+
this.react(message, state);
|
|
190145
|
+
if (state === "working") {
|
|
190146
|
+
this.startTyping(record);
|
|
190147
|
+
} else if (state === "done" || state === "failed") {
|
|
190148
|
+
this.stopTyping(record);
|
|
190149
|
+
}
|
|
190150
|
+
return true;
|
|
190151
|
+
}
|
|
190152
|
+
endActivity(sessionId) {
|
|
190153
|
+
const record = this.registry.bySession(sessionId);
|
|
190154
|
+
if (record === undefined) {
|
|
190155
|
+
return;
|
|
190156
|
+
}
|
|
190157
|
+
this.stopTyping(record);
|
|
190158
|
+
for (const message of this.tracked.values()) {
|
|
190159
|
+
if (message.threadId === record.threadId && (message.state === "reading" || message.state === "working")) {
|
|
190160
|
+
this.react(message, "failed");
|
|
190161
|
+
}
|
|
190162
|
+
}
|
|
190163
|
+
}
|
|
190164
|
+
typingKey(record) {
|
|
190165
|
+
return `${record.chatId}:${record.threadId}`;
|
|
190166
|
+
}
|
|
190167
|
+
startTyping(record) {
|
|
190168
|
+
const key = this.typingKey(record);
|
|
190169
|
+
if (this.typing.has(key) || this.stopped) {
|
|
190170
|
+
return;
|
|
190171
|
+
}
|
|
190172
|
+
const send = () => {
|
|
190173
|
+
this.api.sendChatAction({ chatId: record.chatId, action: "typing", messageThreadId: record.threadId }).catch(() => {
|
|
190174
|
+
return;
|
|
190175
|
+
});
|
|
190176
|
+
};
|
|
190177
|
+
send();
|
|
190178
|
+
this.typing.set(key, this.timers.setInterval(send, TYPING_REFRESH_MS));
|
|
190179
|
+
}
|
|
190180
|
+
stopTyping(record) {
|
|
190181
|
+
const key = this.typingKey(record);
|
|
190182
|
+
const handle = this.typing.get(key);
|
|
190183
|
+
if (handle !== undefined) {
|
|
190184
|
+
this.timers.clearInterval(handle);
|
|
190185
|
+
this.typing.delete(key);
|
|
190186
|
+
}
|
|
190187
|
+
}
|
|
188694
190188
|
dispatch(key) {
|
|
188695
190189
|
const running = this.dispatching.get(key);
|
|
188696
190190
|
if (running !== undefined) {
|
|
@@ -188713,6 +190207,9 @@ class RemoteHub {
|
|
|
188713
190207
|
}
|
|
188714
190208
|
try {
|
|
188715
190209
|
if (entry.kind === "text") {
|
|
190210
|
+
if (entry.messageId !== undefined) {
|
|
190211
|
+
this.track(record, entry.updateId, entry.messageId);
|
|
190212
|
+
}
|
|
188716
190213
|
await this.consumer.deliver(record.sessionId, entry.text ?? "", {
|
|
188717
190214
|
updateId: entry.updateId,
|
|
188718
190215
|
threadId: record.threadId,
|
|
@@ -188753,10 +190250,13 @@ var realTimers, MAX_EVENTS = 50, ANSWER_CALLBACK_TIMEOUT_MS = 5000;
|
|
|
188753
190250
|
var init_hub = __esm(() => {
|
|
188754
190251
|
init_service2();
|
|
188755
190252
|
init_lease();
|
|
190253
|
+
init_format_html();
|
|
188756
190254
|
init_inbound();
|
|
188757
190255
|
init_journal2();
|
|
188758
190256
|
init_naming();
|
|
188759
190257
|
init_outbound_queue();
|
|
190258
|
+
init_command_gateway();
|
|
190259
|
+
init_message_state();
|
|
188760
190260
|
init_poller();
|
|
188761
190261
|
init_registry6();
|
|
188762
190262
|
init_types11();
|
|
@@ -188870,6 +190370,7 @@ class ShellStream {
|
|
|
188870
190370
|
onEnded;
|
|
188871
190371
|
controller;
|
|
188872
190372
|
closed = false;
|
|
190373
|
+
endedBecause;
|
|
188873
190374
|
keepalive;
|
|
188874
190375
|
encoder = new TextEncoder;
|
|
188875
190376
|
constructor(sessionId, timers, onEnded) {
|
|
@@ -188893,6 +190394,7 @@ class ShellStream {
|
|
|
188893
190394
|
if (this.closed) {
|
|
188894
190395
|
return;
|
|
188895
190396
|
}
|
|
190397
|
+
this.endedBecause = kind;
|
|
188896
190398
|
if (kind !== undefined) {
|
|
188897
190399
|
this.write(encodeSseEvent("status", { kind }));
|
|
188898
190400
|
}
|
|
@@ -188913,6 +190415,18 @@ class ShellStream {
|
|
|
188913
190415
|
this.onEnded(this);
|
|
188914
190416
|
}
|
|
188915
190417
|
}
|
|
190418
|
+
function clockText(at) {
|
|
190419
|
+
return `${new Date(at).toISOString().slice(11, 19)} UTC`;
|
|
190420
|
+
}
|
|
190421
|
+
function settledText(original, result) {
|
|
190422
|
+
if (original === undefined || original.length === 0) {
|
|
190423
|
+
return result;
|
|
190424
|
+
}
|
|
190425
|
+
const kept = `${original}
|
|
190426
|
+
|
|
190427
|
+
${result}`;
|
|
190428
|
+
return kept.length <= MAX_SETTLED_TEXT_CHARS ? kept : result;
|
|
190429
|
+
}
|
|
188916
190430
|
function isChannelsRoute(route) {
|
|
188917
190431
|
return Object.hasOwn(CHANNELS_ROUTE_METHODS, route);
|
|
188918
190432
|
}
|
|
@@ -188929,12 +190443,15 @@ class RemoteHttpSurface {
|
|
|
188929
190443
|
keepaliveMs;
|
|
188930
190444
|
ackTimeoutMs;
|
|
188931
190445
|
newApprovalId;
|
|
190446
|
+
newPromptId;
|
|
188932
190447
|
streams = new Map;
|
|
188933
190448
|
completed = new Map;
|
|
188934
190449
|
sent = new Map;
|
|
188935
190450
|
bound = new Map;
|
|
188936
190451
|
pendingAcks = new Map;
|
|
188937
190452
|
approvals = new Map;
|
|
190453
|
+
choices = new Map;
|
|
190454
|
+
finished = new Map;
|
|
188938
190455
|
constructor(options) {
|
|
188939
190456
|
this.options = options;
|
|
188940
190457
|
this.now = options.now ?? Date.now;
|
|
@@ -188942,6 +190459,7 @@ class RemoteHttpSurface {
|
|
|
188942
190459
|
this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
|
|
188943
190460
|
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
188944
190461
|
this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes14(6).toString("hex")}`);
|
|
190462
|
+
this.newPromptId = options.promptIds ?? (() => `pk${randomBytes14(6).toString("hex")}`);
|
|
188945
190463
|
this.consumer = {
|
|
188946
190464
|
deliver: (sessionId, line, meta) => this.deliver(sessionId, line, meta),
|
|
188947
190465
|
deliverCallback: (sessionId, callback) => this.deliverCallback(sessionId, callback)
|
|
@@ -189000,6 +190518,10 @@ class RemoteHttpSurface {
|
|
|
189000
190518
|
return this.reply(hub, request);
|
|
189001
190519
|
case "approval":
|
|
189002
190520
|
return this.requestApproval(hub, request);
|
|
190521
|
+
case "prompt":
|
|
190522
|
+
return this.requestPrompt(hub, request);
|
|
190523
|
+
case "state":
|
|
190524
|
+
return this.messageState(hub, request);
|
|
189003
190525
|
case "ack":
|
|
189004
190526
|
return this.ack(hub, request);
|
|
189005
190527
|
case "stream":
|
|
@@ -189020,6 +190542,9 @@ class RemoteHttpSurface {
|
|
|
189020
190542
|
for (const id of [...this.approvals.keys()]) {
|
|
189021
190543
|
this.dropApproval(id);
|
|
189022
190544
|
}
|
|
190545
|
+
for (const id of [...this.choices.keys()]) {
|
|
190546
|
+
this.dropChoice(id);
|
|
190547
|
+
}
|
|
189023
190548
|
}
|
|
189024
190549
|
setChannels(handler) {
|
|
189025
190550
|
this.channels = handler;
|
|
@@ -189127,6 +190652,9 @@ class RemoteHttpSurface {
|
|
|
189127
190652
|
if (data.startsWith(RESERVED_CALLBACK_PREFIX)) {
|
|
189128
190653
|
return { ok: false, message: `button data may not start with "${RESERVED_CALLBACK_PREFIX}" (reserved for approvals).` };
|
|
189129
190654
|
}
|
|
190655
|
+
if (data.startsWith(RESERVED_CHOICE_PREFIX)) {
|
|
190656
|
+
return { ok: false, message: `button data may not start with "${RESERVED_CHOICE_PREFIX}" (reserved for pickers).` };
|
|
190657
|
+
}
|
|
189130
190658
|
out.push({ text: redactSensitiveText(text), callback_data: data });
|
|
189131
190659
|
}
|
|
189132
190660
|
keyboard.push(out);
|
|
@@ -189199,13 +190727,14 @@ class RemoteHttpSurface {
|
|
|
189199
190727
|
timer: this.timers.setTimeout(() => this.expireApproval(approvalId), timeoutMs)
|
|
189200
190728
|
});
|
|
189201
190729
|
const sent = await hub.send(sessionId, `Approval needed:
|
|
189202
|
-
${redactSensitiveText(value.prompt)}`, {
|
|
190730
|
+
${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
189203
190731
|
keyboard: [
|
|
189204
190732
|
[
|
|
189205
190733
|
{ text: "Allow", callback_data: approvalCallbackData(approvalId, "allow") },
|
|
189206
190734
|
{ text: "Deny", callback_data: approvalCallbackData(approvalId, "deny") }
|
|
189207
190735
|
]
|
|
189208
|
-
]
|
|
190736
|
+
],
|
|
190737
|
+
onSent: (info) => this.onApprovalSent(approvalId, sessionId, info.messageId, info.text)
|
|
189209
190738
|
});
|
|
189210
190739
|
if (!sent) {
|
|
189211
190740
|
this.dropApproval(approvalId);
|
|
@@ -189213,6 +190742,111 @@ ${redactSensitiveText(value.prompt)}`, {
|
|
|
189213
190742
|
}
|
|
189214
190743
|
return ok2({ approvalId, expiresAt });
|
|
189215
190744
|
}
|
|
190745
|
+
async requestPrompt(hub, request) {
|
|
190746
|
+
const body = await readJsonBody(request);
|
|
190747
|
+
if (!body.ok) {
|
|
190748
|
+
return body.response;
|
|
190749
|
+
}
|
|
190750
|
+
const value = body.value;
|
|
190751
|
+
if (!isSessionId(value.sessionId)) {
|
|
190752
|
+
return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
|
|
190753
|
+
}
|
|
190754
|
+
if (typeof value.text !== "string" || value.text.length === 0 || value.text.length > MAX_APPROVAL_PROMPT_CHARS) {
|
|
190755
|
+
return invalid2(`text must be a string of 1 to ${MAX_APPROVAL_PROMPT_CHARS} characters.`);
|
|
190756
|
+
}
|
|
190757
|
+
const timeoutMs = value.timeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
190758
|
+
if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < MIN_APPROVAL_TIMEOUT_MS || timeoutMs > MAX_APPROVAL_TIMEOUT_MS) {
|
|
190759
|
+
return invalid2(`timeoutMs must be an integer from ${MIN_APPROVAL_TIMEOUT_MS} to ${MAX_APPROVAL_TIMEOUT_MS}.`);
|
|
190760
|
+
}
|
|
190761
|
+
if (value.forUserId !== undefined && (typeof value.forUserId !== "number" || !Number.isSafeInteger(value.forUserId))) {
|
|
190762
|
+
return invalid2("forUserId must be an integer.");
|
|
190763
|
+
}
|
|
190764
|
+
const rows = value.rows;
|
|
190765
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > MAX_KEYBOARD_ROWS) {
|
|
190766
|
+
return invalid2(`rows must be 1 to ${MAX_KEYBOARD_ROWS} rows of button labels.`);
|
|
190767
|
+
}
|
|
190768
|
+
const labels = [];
|
|
190769
|
+
const layout = [];
|
|
190770
|
+
for (const row of rows) {
|
|
190771
|
+
if (!Array.isArray(row) || row.length === 0 || row.length > MAX_KEYBOARD_BUTTONS_PER_ROW) {
|
|
190772
|
+
return invalid2(`each row must hold 1 to ${MAX_KEYBOARD_BUTTONS_PER_ROW} labels.`);
|
|
190773
|
+
}
|
|
190774
|
+
for (const label of row) {
|
|
190775
|
+
if (typeof label !== "string" || label.length === 0 || label.length > MAX_BUTTON_TEXT_CHARS) {
|
|
190776
|
+
return invalid2(`a label must be 1 to ${MAX_BUTTON_TEXT_CHARS} characters.`);
|
|
190777
|
+
}
|
|
190778
|
+
labels.push(redactSensitiveText(label));
|
|
190779
|
+
}
|
|
190780
|
+
layout.push(row.length);
|
|
190781
|
+
}
|
|
190782
|
+
if (labels.length > MAX_PROMPT_BUTTONS) {
|
|
190783
|
+
return invalid2(`a prompt may carry at most ${MAX_PROMPT_BUTTONS} buttons.`);
|
|
190784
|
+
}
|
|
190785
|
+
const sessionId = value.sessionId;
|
|
190786
|
+
if (!hub.hasSession(sessionId)) {
|
|
190787
|
+
return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
|
|
190788
|
+
}
|
|
190789
|
+
if (this.streams.get(sessionId) === undefined) {
|
|
190790
|
+
return fail4(409, "no-stream", "No stream is connected for this session; the prompt cannot be answered.");
|
|
190791
|
+
}
|
|
190792
|
+
let held = 0;
|
|
190793
|
+
for (const entry of this.choices.values()) {
|
|
190794
|
+
if (entry.sessionId === sessionId) {
|
|
190795
|
+
held += 1;
|
|
190796
|
+
}
|
|
190797
|
+
}
|
|
190798
|
+
if (held >= MAX_PENDING_CHOICES_PER_SESSION) {
|
|
190799
|
+
return fail4(429, "too-many-prompts", "Too many questions are waiting for this session.");
|
|
190800
|
+
}
|
|
190801
|
+
const promptId = this.newPromptId();
|
|
190802
|
+
const expiresAt = this.now() + timeoutMs;
|
|
190803
|
+
this.choices.set(promptId, {
|
|
190804
|
+
sessionId,
|
|
190805
|
+
expiresAt,
|
|
190806
|
+
labels,
|
|
190807
|
+
...value.forUserId === undefined ? {} : { forUserId: value.forUserId },
|
|
190808
|
+
timer: this.timers.setTimeout(() => this.expireChoice(promptId), timeoutMs)
|
|
190809
|
+
});
|
|
190810
|
+
const keyboard = [];
|
|
190811
|
+
let position = 0;
|
|
190812
|
+
for (const width of layout) {
|
|
190813
|
+
const line = [];
|
|
190814
|
+
for (let column = 0;column < width; column += 1) {
|
|
190815
|
+
line.push({ text: labels[position], callback_data: choiceCallbackData(promptId, position) });
|
|
190816
|
+
position += 1;
|
|
190817
|
+
}
|
|
190818
|
+
keyboard.push(line);
|
|
190819
|
+
}
|
|
190820
|
+
const sent = await hub.send(sessionId, redactSensitiveText(value.text), {
|
|
190821
|
+
keyboard,
|
|
190822
|
+
onSent: (info) => this.onChoiceSent(promptId, sessionId, info.messageId, info.text)
|
|
190823
|
+
});
|
|
190824
|
+
if (!sent) {
|
|
190825
|
+
this.dropChoice(promptId);
|
|
190826
|
+
return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
|
|
190827
|
+
}
|
|
190828
|
+
return ok2({ promptId, expiresAt });
|
|
190829
|
+
}
|
|
190830
|
+
async messageState(hub, request) {
|
|
190831
|
+
const body = await readJsonBody(request);
|
|
190832
|
+
if (!body.ok) {
|
|
190833
|
+
return body.response;
|
|
190834
|
+
}
|
|
190835
|
+
const value = body.value;
|
|
190836
|
+
if (!isSessionId(value.sessionId)) {
|
|
190837
|
+
return invalid2("sessionId must be 1 to 64 characters of letters, digits, '-' and '_'.");
|
|
190838
|
+
}
|
|
190839
|
+
if (typeof value.updateId !== "number" || !Number.isSafeInteger(value.updateId) || value.updateId < 0) {
|
|
190840
|
+
return invalid2("updateId must be a non-negative integer.");
|
|
190841
|
+
}
|
|
190842
|
+
if (!isMessageState(value.state)) {
|
|
190843
|
+
return invalid2("state must be one of reading, working, done, failed.");
|
|
190844
|
+
}
|
|
190845
|
+
if (!hub.hasSession(value.sessionId)) {
|
|
190846
|
+
return fail4(404, "unknown-session", "This session is not registered for remote control; register again.");
|
|
190847
|
+
}
|
|
190848
|
+
return ok2({ shown: hub.messageState(value.sessionId, value.updateId, value.state) });
|
|
190849
|
+
}
|
|
189216
190850
|
async ack(hub, request) {
|
|
189217
190851
|
const body = await readJsonBody(request);
|
|
189218
190852
|
if (!body.ok) {
|
|
@@ -189350,15 +190984,12 @@ ${redactSensitiveText(value.prompt)}`, {
|
|
|
189350
190984
|
const stream = this.streams.get(sessionId);
|
|
189351
190985
|
const approval = parseApprovalCallback(callback.data);
|
|
189352
190986
|
if (approval !== undefined) {
|
|
189353
|
-
|
|
189354
|
-
|
|
189355
|
-
|
|
189356
|
-
|
|
189357
|
-
|
|
189358
|
-
|
|
189359
|
-
this.noteDecision(sessionId, approval.decision);
|
|
189360
|
-
}
|
|
189361
|
-
}
|
|
190987
|
+
this.pressApproval(sessionId, callback, approval, stream);
|
|
190988
|
+
return Promise.resolve();
|
|
190989
|
+
}
|
|
190990
|
+
const choice = parseChoiceCallback(callback.data);
|
|
190991
|
+
if (choice !== undefined) {
|
|
190992
|
+
this.pressChoice(sessionId, callback, choice, stream);
|
|
189362
190993
|
return Promise.resolve();
|
|
189363
190994
|
}
|
|
189364
190995
|
const event = {
|
|
@@ -189371,8 +191002,181 @@ ${redactSensitiveText(value.prompt)}`, {
|
|
|
189371
191002
|
stream?.write(encodeSseEvent("callback", event, callback.updateId));
|
|
189372
191003
|
return Promise.resolve();
|
|
189373
191004
|
}
|
|
189374
|
-
|
|
189375
|
-
|
|
191005
|
+
pressApproval(sessionId, callback, approval, stream) {
|
|
191006
|
+
const id = approval.approvalId;
|
|
191007
|
+
const entry = this.approvals.get(id);
|
|
191008
|
+
if (entry !== undefined && entry.sessionId !== sessionId) {
|
|
191009
|
+
return;
|
|
191010
|
+
}
|
|
191011
|
+
if (entry !== undefined && entry.expiresAt > this.now()) {
|
|
191012
|
+
this.dropApproval(id);
|
|
191013
|
+
const event = { updateId: callback.updateId, approvalId: id, decision: approval.decision };
|
|
191014
|
+
const told = stream?.write(encodeSseEvent("approval", event, callback.updateId)) === true;
|
|
191015
|
+
const messageId = entry.messageId ?? callback.messageId;
|
|
191016
|
+
const when = clockText(this.now());
|
|
191017
|
+
if (!told) {
|
|
191018
|
+
this.finishApproval(id, sessionId, messageId, entry.text, {
|
|
191019
|
+
result: `Not delivered at ${when}: the shell is not connected, so nothing was approved.`,
|
|
191020
|
+
shortReply: "Approval not delivered: the shell is not connected, so nothing was approved."
|
|
191021
|
+
});
|
|
191022
|
+
return;
|
|
191023
|
+
}
|
|
191024
|
+
const who = `user ${callback.fromId}`;
|
|
191025
|
+
this.finishApproval(id, sessionId, messageId, entry.text, {
|
|
191026
|
+
result: approval.decision === "allow" ? `Allowed by ${who} at ${when}.` : `Denied by ${who} at ${when}.`,
|
|
191027
|
+
shortReply: approval.decision === "allow" ? "Approval granted." : "Approval denied."
|
|
191028
|
+
});
|
|
191029
|
+
return;
|
|
191030
|
+
}
|
|
191031
|
+
if (entry !== undefined) {
|
|
191032
|
+
this.expireApproval(id);
|
|
191033
|
+
return;
|
|
191034
|
+
}
|
|
191035
|
+
const done = this.finished.get(id);
|
|
191036
|
+
if (done !== undefined) {
|
|
191037
|
+
if (done.sessionId === sessionId) {
|
|
191038
|
+
const messageId = callback.messageId ?? done.messageId;
|
|
191039
|
+
if (messageId !== undefined) {
|
|
191040
|
+
this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
|
|
191041
|
+
return;
|
|
191042
|
+
});
|
|
191043
|
+
}
|
|
191044
|
+
}
|
|
191045
|
+
return;
|
|
191046
|
+
}
|
|
191047
|
+
const pressed = callback.messageId;
|
|
191048
|
+
if (pressed !== undefined && !this.carriesLivePrompt(pressed)) {
|
|
191049
|
+
this.hub?.settleMessage(sessionId, pressed, "This request is no longer active.", "That request is no longer active.").catch(() => {
|
|
191050
|
+
return;
|
|
191051
|
+
});
|
|
191052
|
+
}
|
|
191053
|
+
}
|
|
191054
|
+
carriesLivePrompt(messageId) {
|
|
191055
|
+
return [...this.approvals.values(), ...this.choices.values()].some((live) => live.messageId === messageId);
|
|
191056
|
+
}
|
|
191057
|
+
pressChoice(sessionId, callback, choice, stream) {
|
|
191058
|
+
const id = choice.promptId;
|
|
191059
|
+
const entry = this.choices.get(id);
|
|
191060
|
+
if (entry !== undefined && entry.sessionId !== sessionId) {
|
|
191061
|
+
return;
|
|
191062
|
+
}
|
|
191063
|
+
if (entry !== undefined && entry.expiresAt > this.now()) {
|
|
191064
|
+
const sameMessage = entry.messageId !== undefined && callback.messageId === entry.messageId;
|
|
191065
|
+
const sameUser = entry.forUserId === undefined || entry.forUserId === callback.fromId;
|
|
191066
|
+
if (!sameMessage || !sameUser || choice.index >= entry.labels.length) {
|
|
191067
|
+
return;
|
|
191068
|
+
}
|
|
191069
|
+
this.dropChoice(id);
|
|
191070
|
+
const event = { updateId: callback.updateId, promptId: id, index: choice.index, fromId: callback.fromId };
|
|
191071
|
+
const told = stream?.write(encodeSseEvent("choice", event, callback.updateId)) === true;
|
|
191072
|
+
const when = clockText(this.now());
|
|
191073
|
+
if (!told) {
|
|
191074
|
+
this.finishApproval(id, sessionId, entry.messageId, entry.text, {
|
|
191075
|
+
result: `Not delivered at ${when}: the shell is not connected, so nothing changed.`,
|
|
191076
|
+
shortReply: "Not delivered: the shell is not connected, so nothing changed."
|
|
191077
|
+
});
|
|
191078
|
+
return;
|
|
191079
|
+
}
|
|
191080
|
+
this.finishApproval(id, sessionId, entry.messageId, entry.text, {
|
|
191081
|
+
result: `Chosen: ${entry.labels[choice.index]} (user ${callback.fromId}, ${when}).`,
|
|
191082
|
+
shortReply: `Chosen: ${entry.labels[choice.index]}.`
|
|
191083
|
+
});
|
|
191084
|
+
return;
|
|
191085
|
+
}
|
|
191086
|
+
if (entry !== undefined) {
|
|
191087
|
+
this.expireChoice(id);
|
|
191088
|
+
return;
|
|
191089
|
+
}
|
|
191090
|
+
const done = this.finished.get(id);
|
|
191091
|
+
if (done !== undefined) {
|
|
191092
|
+
if (done.sessionId === sessionId) {
|
|
191093
|
+
const messageId = callback.messageId ?? done.messageId;
|
|
191094
|
+
if (messageId !== undefined) {
|
|
191095
|
+
this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
|
|
191096
|
+
return;
|
|
191097
|
+
});
|
|
191098
|
+
}
|
|
191099
|
+
}
|
|
191100
|
+
return;
|
|
191101
|
+
}
|
|
191102
|
+
const pressed = callback.messageId;
|
|
191103
|
+
if (pressed !== undefined && !this.carriesLivePrompt(pressed)) {
|
|
191104
|
+
this.hub?.settleMessage(sessionId, pressed, "This request is no longer active.", "That request is no longer active.").catch(() => {
|
|
191105
|
+
return;
|
|
191106
|
+
});
|
|
191107
|
+
}
|
|
191108
|
+
}
|
|
191109
|
+
onChoiceSent(promptId, sessionId, messageId, text) {
|
|
191110
|
+
const live = this.choices.get(promptId);
|
|
191111
|
+
if (live !== undefined) {
|
|
191112
|
+
live.messageId = messageId;
|
|
191113
|
+
live.text = text;
|
|
191114
|
+
return;
|
|
191115
|
+
}
|
|
191116
|
+
this.onApprovalSent(promptId, sessionId, messageId, text);
|
|
191117
|
+
}
|
|
191118
|
+
dropChoice(promptId) {
|
|
191119
|
+
const entry = this.choices.get(promptId);
|
|
191120
|
+
if (entry === undefined) {
|
|
191121
|
+
return;
|
|
191122
|
+
}
|
|
191123
|
+
this.timers.clearTimeout(entry.timer);
|
|
191124
|
+
this.choices.delete(promptId);
|
|
191125
|
+
}
|
|
191126
|
+
expireChoice(promptId) {
|
|
191127
|
+
const entry = this.choices.get(promptId);
|
|
191128
|
+
if (entry === undefined) {
|
|
191129
|
+
return;
|
|
191130
|
+
}
|
|
191131
|
+
this.dropChoice(promptId);
|
|
191132
|
+
this.finishApproval(promptId, entry.sessionId, entry.messageId, entry.text, {
|
|
191133
|
+
result: `Expired at ${clockText(this.now())}: no answer, nothing changed.`,
|
|
191134
|
+
shortReply: "That question expired: no answer, nothing changed."
|
|
191135
|
+
});
|
|
191136
|
+
}
|
|
191137
|
+
onApprovalSent(approvalId, sessionId, messageId, text) {
|
|
191138
|
+
const live = this.approvals.get(approvalId);
|
|
191139
|
+
if (live !== undefined) {
|
|
191140
|
+
live.messageId = messageId;
|
|
191141
|
+
live.text = text;
|
|
191142
|
+
return;
|
|
191143
|
+
}
|
|
191144
|
+
const done = this.finished.get(approvalId);
|
|
191145
|
+
if (done !== undefined && done.messageId === undefined) {
|
|
191146
|
+
done.messageId = messageId;
|
|
191147
|
+
done.finalText = settledText(text, done.finalText);
|
|
191148
|
+
this.hub?.settleMessage(sessionId, messageId, done.finalText, done.shortReply).catch(() => {
|
|
191149
|
+
return;
|
|
191150
|
+
});
|
|
191151
|
+
}
|
|
191152
|
+
}
|
|
191153
|
+
finishApproval(approvalId, sessionId, messageId, original, outcome) {
|
|
191154
|
+
const finalText = messageId === undefined ? outcome.result : settledText(original, outcome.result);
|
|
191155
|
+
this.finished.delete(approvalId);
|
|
191156
|
+
this.finished.set(approvalId, {
|
|
191157
|
+
sessionId,
|
|
191158
|
+
...messageId === undefined ? {} : { messageId },
|
|
191159
|
+
finalText,
|
|
191160
|
+
shortReply: outcome.shortReply
|
|
191161
|
+
});
|
|
191162
|
+
while (this.finished.size > MAX_FINISHED_PROMPTS) {
|
|
191163
|
+
const oldest = this.finished.keys().next().value;
|
|
191164
|
+
if (oldest === undefined) {
|
|
191165
|
+
break;
|
|
191166
|
+
}
|
|
191167
|
+
this.finished.delete(oldest);
|
|
191168
|
+
}
|
|
191169
|
+
const hub = this.hub;
|
|
191170
|
+
if (hub === undefined) {
|
|
191171
|
+
return;
|
|
191172
|
+
}
|
|
191173
|
+
if (messageId === undefined) {
|
|
191174
|
+
hub.send(sessionId, outcome.shortReply).catch(() => {
|
|
191175
|
+
return;
|
|
191176
|
+
});
|
|
191177
|
+
return;
|
|
191178
|
+
}
|
|
191179
|
+
hub.settleMessage(sessionId, messageId, finalText, outcome.shortReply).catch(() => {
|
|
189376
191180
|
return;
|
|
189377
191181
|
});
|
|
189378
191182
|
}
|
|
@@ -189389,9 +191193,11 @@ ${redactSensitiveText(value.prompt)}`, {
|
|
|
189389
191193
|
if (entry === undefined) {
|
|
189390
191194
|
return;
|
|
189391
191195
|
}
|
|
191196
|
+
this.timers.clearTimeout(entry.timer);
|
|
189392
191197
|
this.approvals.delete(approvalId);
|
|
189393
|
-
this.
|
|
189394
|
-
|
|
191198
|
+
this.finishApproval(approvalId, entry.sessionId, entry.messageId, entry.text, {
|
|
191199
|
+
result: `Expired at ${clockText(this.now())}: no answer, so it was denied.`,
|
|
191200
|
+
shortReply: "Approval request expired: no answer, so it was denied."
|
|
189395
191201
|
});
|
|
189396
191202
|
}
|
|
189397
191203
|
onStreamEnded(stream) {
|
|
@@ -189410,11 +191216,40 @@ ${redactSensitiveText(value.prompt)}`, {
|
|
|
189410
191216
|
for (const [id, entry] of [...this.approvals.entries()]) {
|
|
189411
191217
|
if (entry.sessionId === stream.sessionId) {
|
|
189412
191218
|
this.dropApproval(id);
|
|
191219
|
+
if (!this.closed && entry.messageId !== undefined) {
|
|
191220
|
+
this.finishApproval(id, stream.sessionId, entry.messageId, entry.text, {
|
|
191221
|
+
result: `Cancelled at ${clockText(this.now())}: the shell disconnected, so it was denied.`,
|
|
191222
|
+
shortReply: "Approval cancelled: the shell disconnected, so it was denied."
|
|
191223
|
+
});
|
|
191224
|
+
}
|
|
189413
191225
|
}
|
|
189414
191226
|
}
|
|
191227
|
+
for (const [id, entry] of [...this.choices.entries()]) {
|
|
191228
|
+
if (entry.sessionId === stream.sessionId) {
|
|
191229
|
+
this.dropChoice(id);
|
|
191230
|
+
if (!this.closed && entry.messageId !== undefined) {
|
|
191231
|
+
this.finishApproval(id, stream.sessionId, entry.messageId, entry.text, {
|
|
191232
|
+
result: `Cancelled at ${clockText(this.now())}: the shell disconnected, nothing changed.`,
|
|
191233
|
+
shortReply: "Question cancelled: the shell disconnected, nothing changed."
|
|
191234
|
+
});
|
|
191235
|
+
}
|
|
191236
|
+
}
|
|
191237
|
+
}
|
|
191238
|
+
if (stream.endedBecause === "closing") {
|
|
191239
|
+
this.hub?.endActivity(stream.sessionId);
|
|
191240
|
+
}
|
|
189415
191241
|
}
|
|
189416
191242
|
}
|
|
189417
|
-
|
|
191243
|
+
function asCodeBlock(text) {
|
|
191244
|
+
const safe = (text.trim().length === 0 ? "(empty)" : text).replace(/\r\n?/g, `
|
|
191245
|
+
`).replace(/`{9,}/g, (run) => run.match(/`{1,8}/g).join("\u200B"));
|
|
191246
|
+
const longest = Math.max(0, ...(safe.match(/`+/g) ?? []).map((run) => run.length));
|
|
191247
|
+
const fence = "`".repeat(Math.max(3, Math.min(longest, MAX_BACKTICK_RUN) + 1));
|
|
191248
|
+
return `${fence}
|
|
191249
|
+
${safe}
|
|
191250
|
+
${fence}`;
|
|
191251
|
+
}
|
|
191252
|
+
var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_PENDING_CHOICES_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_FINISHED_PROMPTS = 128, MAX_SETTLED_TEXT_CHARS = 3600, MAX_BACKTICK_RUN = 8;
|
|
189418
191253
|
var init_http_surface = __esm(() => {
|
|
189419
191254
|
init_service2();
|
|
189420
191255
|
init_hub();
|
|
@@ -189629,7 +191464,10 @@ class Pairing {
|
|
|
189629
191464
|
this.state = "waiting-for-group";
|
|
189630
191465
|
this.expiresAt = this.now() + this.ttlMs;
|
|
189631
191466
|
this.armExpiryTimer();
|
|
189632
|
-
await this.options.api
|
|
191467
|
+
await sendHtml(this.options.api, {
|
|
191468
|
+
chatId: message.chat.id,
|
|
191469
|
+
text: "Paired. Now add me to your group (a group with Topics turned on) and make me an administrator."
|
|
191470
|
+
}).catch(() => {
|
|
189633
191471
|
return;
|
|
189634
191472
|
});
|
|
189635
191473
|
const early = [...this.earlyGroups.values()];
|
|
@@ -189743,6 +191581,7 @@ class Pairing {
|
|
|
189743
191581
|
var PAIRING_CODE_TTL_MS, CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789", CODE_LENGTH = 8, FINAL, EARLY_GROUP_EVENTS = 20;
|
|
189744
191582
|
var init_pairing = __esm(() => {
|
|
189745
191583
|
init_service2();
|
|
191584
|
+
init_format_html();
|
|
189746
191585
|
init_hub();
|
|
189747
191586
|
init_poller();
|
|
189748
191587
|
init_types11();
|