@mrciphersmith/keryx 0.3.56 → 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 +1597 -87
- 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"],
|
|
@@ -187348,6 +188287,46 @@ function createHttpBotApi(options) {
|
|
|
187348
188287
|
}
|
|
187349
188288
|
return call("sendMessage", body);
|
|
187350
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
|
+
},
|
|
187351
188330
|
async createForumTopic(params) {
|
|
187352
188331
|
return call("createForumTopic", { chat_id: params.chatId, name: params.name });
|
|
187353
188332
|
},
|
|
@@ -187808,6 +188787,22 @@ async function sendHtml(api, params, onFallback) {
|
|
|
187808
188787
|
return api.sendMessage(plain);
|
|
187809
188788
|
}
|
|
187810
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
|
+
}
|
|
187811
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;
|
|
187812
188807
|
var init_format_html = __esm(() => {
|
|
187813
188808
|
init_format2();
|
|
@@ -188168,6 +189163,7 @@ class OutboundQueue {
|
|
|
188168
189163
|
onDrop;
|
|
188169
189164
|
onFallback;
|
|
188170
189165
|
retryDelayMs;
|
|
189166
|
+
sentHooks = new Map;
|
|
188171
189167
|
blockedUntil = 0;
|
|
188172
189168
|
flushing;
|
|
188173
189169
|
constructor(options) {
|
|
@@ -188208,8 +189204,12 @@ class OutboundQueue {
|
|
|
188208
189204
|
...message.keyboard === undefined || index !== parts.length - 1 ? {} : { keyboard: message.keyboard },
|
|
188209
189205
|
createdAt: this.now()
|
|
188210
189206
|
};
|
|
189207
|
+
if (message.onSent !== undefined && index === parts.length - 1) {
|
|
189208
|
+
this.sentHooks.set(entry.id, message.onSent);
|
|
189209
|
+
}
|
|
188211
189210
|
const { dropped } = this.log.add(entry);
|
|
188212
189211
|
for (const gone of dropped) {
|
|
189212
|
+
this.sentHooks.delete(gone.id);
|
|
188213
189213
|
this.onDrop(gone, "outbound queue is full; oldest message dropped");
|
|
188214
189214
|
}
|
|
188215
189215
|
entries.push(entry);
|
|
@@ -188220,6 +189220,7 @@ class OutboundQueue {
|
|
|
188220
189220
|
let removed = 0;
|
|
188221
189221
|
for (const entry of this.log.pending()) {
|
|
188222
189222
|
if (entry.chatId === chatId && entry.threadId === threadId) {
|
|
189223
|
+
this.sentHooks.delete(entry.id);
|
|
188223
189224
|
this.log.ack(entry.id);
|
|
188224
189225
|
removed += 1;
|
|
188225
189226
|
}
|
|
@@ -188234,6 +189235,21 @@ class OutboundQueue {
|
|
|
188234
189235
|
}
|
|
188235
189236
|
return this.flushing;
|
|
188236
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
|
+
}
|
|
188237
189253
|
async run() {
|
|
188238
189254
|
let sent = 0;
|
|
188239
189255
|
let dropped = 0;
|
|
@@ -188247,7 +189263,7 @@ class OutboundQueue {
|
|
|
188247
189263
|
return { sent, dropped, remaining: this.log.size, retryInMs: wait, stoppedBy: "rate limited" };
|
|
188248
189264
|
}
|
|
188249
189265
|
try {
|
|
188250
|
-
await sendHtml(this.api, {
|
|
189266
|
+
const result = await sendHtml(this.api, {
|
|
188251
189267
|
chatId: entry.chatId,
|
|
188252
189268
|
text: entry.text,
|
|
188253
189269
|
...entry.threadId === undefined ? {} : { messageThreadId: entry.threadId },
|
|
@@ -188255,6 +189271,7 @@ class OutboundQueue {
|
|
|
188255
189271
|
}, () => this.onFallback(entry));
|
|
188256
189272
|
this.log.ack(entry.id);
|
|
188257
189273
|
sent += 1;
|
|
189274
|
+
this.notifySent(entry, result.message_id);
|
|
188258
189275
|
} catch (error) {
|
|
188259
189276
|
const description = describe(error);
|
|
188260
189277
|
if (isBotApiError(error) && error.kind === "rate-limited") {
|
|
@@ -188266,6 +189283,7 @@ class OutboundQueue {
|
|
|
188266
189283
|
return { sent, dropped, remaining: this.log.size, retryInMs: this.retryDelayMs, stoppedBy: description };
|
|
188267
189284
|
}
|
|
188268
189285
|
this.log.ack(entry.id);
|
|
189286
|
+
this.sentHooks.delete(entry.id);
|
|
188269
189287
|
dropped += 1;
|
|
188270
189288
|
this.onDrop(entry, description);
|
|
188271
189289
|
}
|
|
@@ -188285,6 +189303,24 @@ var init_outbound_queue = __esm(() => {
|
|
|
188285
189303
|
init_types11();
|
|
188286
189304
|
});
|
|
188287
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
|
+
|
|
188288
189324
|
// src/remote/poller.ts
|
|
188289
189325
|
function defaultSleep2(ms, signal) {
|
|
188290
189326
|
return new Promise((resolve) => {
|
|
@@ -188563,6 +189599,9 @@ class RemoteHub {
|
|
|
188563
189599
|
flushTimer;
|
|
188564
189600
|
sweepTimer;
|
|
188565
189601
|
stopped = false;
|
|
189602
|
+
tracked = new Map;
|
|
189603
|
+
typing = new Map;
|
|
189604
|
+
reactionsOff = false;
|
|
188566
189605
|
constructor(options) {
|
|
188567
189606
|
this.api = options.api;
|
|
188568
189607
|
this.config = options.config;
|
|
@@ -188636,6 +189675,14 @@ class RemoteHub {
|
|
|
188636
189675
|
for (const record of this.registry.records()) {
|
|
188637
189676
|
this.dispatch(nameKey(record.name));
|
|
188638
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
|
+
}
|
|
188639
189686
|
}
|
|
188640
189687
|
async stop() {
|
|
188641
189688
|
this.stopped = true;
|
|
@@ -188651,6 +189698,10 @@ class RemoteHub {
|
|
|
188651
189698
|
this.timers.clearTimeout(handle);
|
|
188652
189699
|
}
|
|
188653
189700
|
this.retryTimers.clear();
|
|
189701
|
+
for (const handle of this.typing.values()) {
|
|
189702
|
+
this.timers.clearInterval(handle);
|
|
189703
|
+
}
|
|
189704
|
+
this.typing.clear();
|
|
188654
189705
|
await this.poller.stop();
|
|
188655
189706
|
}
|
|
188656
189707
|
pollerStatus() {
|
|
@@ -188753,6 +189804,7 @@ class RemoteHub {
|
|
|
188753
189804
|
record.unavailableSince = Math.min(now, record.lastHeartbeat + SESSION_LEASE_STALE_MS);
|
|
188754
189805
|
this.registry.save();
|
|
188755
189806
|
this.event("session-unavailable", record.name);
|
|
189807
|
+
this.endActivity(record.sessionId);
|
|
188756
189808
|
if (now - record.unavailableSince < this.config.orphanMs) {
|
|
188757
189809
|
this.queueStatus(record, `Session unavailable: the shell stopped responding. This topic is deleted in ${formatDuration(this.config.orphanMs)} unless the session returns.`);
|
|
188758
189810
|
}
|
|
@@ -188773,11 +189825,31 @@ class RemoteHub {
|
|
|
188773
189825
|
chatId: record.chatId,
|
|
188774
189826
|
threadId: record.threadId,
|
|
188775
189827
|
text,
|
|
188776
|
-
...options.keyboard === undefined ? {} : { keyboard: options.keyboard }
|
|
189828
|
+
...options.keyboard === undefined ? {} : { keyboard: options.keyboard },
|
|
189829
|
+
...options.onSent === undefined ? {} : { onSent: options.onSent }
|
|
188777
189830
|
});
|
|
188778
189831
|
await this.flushOutbound();
|
|
188779
189832
|
return true;
|
|
188780
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
|
+
}
|
|
188781
189853
|
async flushOutbound() {
|
|
188782
189854
|
const result = await this.outbound.flush();
|
|
188783
189855
|
if (result.retryInMs !== undefined && !this.stopped && this.flushTimer === undefined) {
|
|
@@ -188946,6 +190018,7 @@ class RemoteHub {
|
|
|
188946
190018
|
}
|
|
188947
190019
|
async retire(record, why, strict = false) {
|
|
188948
190020
|
this.outbound.discardForThread(record.chatId, record.threadId);
|
|
190021
|
+
this.stopTyping(record);
|
|
188949
190022
|
try {
|
|
188950
190023
|
await this.api.deleteForumTopic({ chatId: record.chatId, messageThreadId: record.threadId });
|
|
188951
190024
|
} catch (error) {
|
|
@@ -188993,7 +190066,11 @@ class RemoteHub {
|
|
|
188993
190066
|
if (typeof message.text !== "string" || message.text.length === 0) {
|
|
188994
190067
|
return;
|
|
188995
190068
|
}
|
|
188996
|
-
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
|
+
}
|
|
188997
190074
|
return { key, dropped: appended.dropped };
|
|
188998
190075
|
}
|
|
188999
190076
|
if (query?.data === undefined) {
|
|
@@ -189006,6 +190083,108 @@ class RemoteHub {
|
|
|
189006
190083
|
});
|
|
189007
190084
|
return { key, dropped: appended.dropped, callbackQueryId: query.id };
|
|
189008
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
|
+
}
|
|
189009
190188
|
dispatch(key) {
|
|
189010
190189
|
const running = this.dispatching.get(key);
|
|
189011
190190
|
if (running !== undefined) {
|
|
@@ -189028,6 +190207,9 @@ class RemoteHub {
|
|
|
189028
190207
|
}
|
|
189029
190208
|
try {
|
|
189030
190209
|
if (entry.kind === "text") {
|
|
190210
|
+
if (entry.messageId !== undefined) {
|
|
190211
|
+
this.track(record, entry.updateId, entry.messageId);
|
|
190212
|
+
}
|
|
189031
190213
|
await this.consumer.deliver(record.sessionId, entry.text ?? "", {
|
|
189032
190214
|
updateId: entry.updateId,
|
|
189033
190215
|
threadId: record.threadId,
|
|
@@ -189073,6 +190255,8 @@ var init_hub = __esm(() => {
|
|
|
189073
190255
|
init_journal2();
|
|
189074
190256
|
init_naming();
|
|
189075
190257
|
init_outbound_queue();
|
|
190258
|
+
init_command_gateway();
|
|
190259
|
+
init_message_state();
|
|
189076
190260
|
init_poller();
|
|
189077
190261
|
init_registry6();
|
|
189078
190262
|
init_types11();
|
|
@@ -189186,6 +190370,7 @@ class ShellStream {
|
|
|
189186
190370
|
onEnded;
|
|
189187
190371
|
controller;
|
|
189188
190372
|
closed = false;
|
|
190373
|
+
endedBecause;
|
|
189189
190374
|
keepalive;
|
|
189190
190375
|
encoder = new TextEncoder;
|
|
189191
190376
|
constructor(sessionId, timers, onEnded) {
|
|
@@ -189209,6 +190394,7 @@ class ShellStream {
|
|
|
189209
190394
|
if (this.closed) {
|
|
189210
190395
|
return;
|
|
189211
190396
|
}
|
|
190397
|
+
this.endedBecause = kind;
|
|
189212
190398
|
if (kind !== undefined) {
|
|
189213
190399
|
this.write(encodeSseEvent("status", { kind }));
|
|
189214
190400
|
}
|
|
@@ -189229,6 +190415,18 @@ class ShellStream {
|
|
|
189229
190415
|
this.onEnded(this);
|
|
189230
190416
|
}
|
|
189231
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
|
+
}
|
|
189232
190430
|
function isChannelsRoute(route) {
|
|
189233
190431
|
return Object.hasOwn(CHANNELS_ROUTE_METHODS, route);
|
|
189234
190432
|
}
|
|
@@ -189245,12 +190443,15 @@ class RemoteHttpSurface {
|
|
|
189245
190443
|
keepaliveMs;
|
|
189246
190444
|
ackTimeoutMs;
|
|
189247
190445
|
newApprovalId;
|
|
190446
|
+
newPromptId;
|
|
189248
190447
|
streams = new Map;
|
|
189249
190448
|
completed = new Map;
|
|
189250
190449
|
sent = new Map;
|
|
189251
190450
|
bound = new Map;
|
|
189252
190451
|
pendingAcks = new Map;
|
|
189253
190452
|
approvals = new Map;
|
|
190453
|
+
choices = new Map;
|
|
190454
|
+
finished = new Map;
|
|
189254
190455
|
constructor(options) {
|
|
189255
190456
|
this.options = options;
|
|
189256
190457
|
this.now = options.now ?? Date.now;
|
|
@@ -189258,6 +190459,7 @@ class RemoteHttpSurface {
|
|
|
189258
190459
|
this.keepaliveMs = options.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
|
|
189259
190460
|
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
189260
190461
|
this.newApprovalId = options.approvalIds ?? (() => `ap${randomBytes14(6).toString("hex")}`);
|
|
190462
|
+
this.newPromptId = options.promptIds ?? (() => `pk${randomBytes14(6).toString("hex")}`);
|
|
189261
190463
|
this.consumer = {
|
|
189262
190464
|
deliver: (sessionId, line, meta) => this.deliver(sessionId, line, meta),
|
|
189263
190465
|
deliverCallback: (sessionId, callback) => this.deliverCallback(sessionId, callback)
|
|
@@ -189316,6 +190518,10 @@ class RemoteHttpSurface {
|
|
|
189316
190518
|
return this.reply(hub, request);
|
|
189317
190519
|
case "approval":
|
|
189318
190520
|
return this.requestApproval(hub, request);
|
|
190521
|
+
case "prompt":
|
|
190522
|
+
return this.requestPrompt(hub, request);
|
|
190523
|
+
case "state":
|
|
190524
|
+
return this.messageState(hub, request);
|
|
189319
190525
|
case "ack":
|
|
189320
190526
|
return this.ack(hub, request);
|
|
189321
190527
|
case "stream":
|
|
@@ -189336,6 +190542,9 @@ class RemoteHttpSurface {
|
|
|
189336
190542
|
for (const id of [...this.approvals.keys()]) {
|
|
189337
190543
|
this.dropApproval(id);
|
|
189338
190544
|
}
|
|
190545
|
+
for (const id of [...this.choices.keys()]) {
|
|
190546
|
+
this.dropChoice(id);
|
|
190547
|
+
}
|
|
189339
190548
|
}
|
|
189340
190549
|
setChannels(handler) {
|
|
189341
190550
|
this.channels = handler;
|
|
@@ -189443,6 +190652,9 @@ class RemoteHttpSurface {
|
|
|
189443
190652
|
if (data.startsWith(RESERVED_CALLBACK_PREFIX)) {
|
|
189444
190653
|
return { ok: false, message: `button data may not start with "${RESERVED_CALLBACK_PREFIX}" (reserved for approvals).` };
|
|
189445
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
|
+
}
|
|
189446
190658
|
out.push({ text: redactSensitiveText(text), callback_data: data });
|
|
189447
190659
|
}
|
|
189448
190660
|
keyboard.push(out);
|
|
@@ -189521,7 +190733,8 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189521
190733
|
{ text: "Allow", callback_data: approvalCallbackData(approvalId, "allow") },
|
|
189522
190734
|
{ text: "Deny", callback_data: approvalCallbackData(approvalId, "deny") }
|
|
189523
190735
|
]
|
|
189524
|
-
]
|
|
190736
|
+
],
|
|
190737
|
+
onSent: (info) => this.onApprovalSent(approvalId, sessionId, info.messageId, info.text)
|
|
189525
190738
|
});
|
|
189526
190739
|
if (!sent) {
|
|
189527
190740
|
this.dropApproval(approvalId);
|
|
@@ -189529,6 +190742,111 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189529
190742
|
}
|
|
189530
190743
|
return ok2({ approvalId, expiresAt });
|
|
189531
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
|
+
}
|
|
189532
190850
|
async ack(hub, request) {
|
|
189533
190851
|
const body = await readJsonBody(request);
|
|
189534
190852
|
if (!body.ok) {
|
|
@@ -189666,15 +190984,12 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189666
190984
|
const stream = this.streams.get(sessionId);
|
|
189667
190985
|
const approval = parseApprovalCallback(callback.data);
|
|
189668
190986
|
if (approval !== undefined) {
|
|
189669
|
-
|
|
189670
|
-
|
|
189671
|
-
|
|
189672
|
-
|
|
189673
|
-
|
|
189674
|
-
|
|
189675
|
-
this.noteDecision(sessionId, approval.decision);
|
|
189676
|
-
}
|
|
189677
|
-
}
|
|
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);
|
|
189678
190993
|
return Promise.resolve();
|
|
189679
190994
|
}
|
|
189680
190995
|
const event = {
|
|
@@ -189687,8 +191002,181 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189687
191002
|
stream?.write(encodeSseEvent("callback", event, callback.updateId));
|
|
189688
191003
|
return Promise.resolve();
|
|
189689
191004
|
}
|
|
189690
|
-
|
|
189691
|
-
|
|
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(() => {
|
|
189692
191180
|
return;
|
|
189693
191181
|
});
|
|
189694
191182
|
}
|
|
@@ -189705,9 +191193,11 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189705
191193
|
if (entry === undefined) {
|
|
189706
191194
|
return;
|
|
189707
191195
|
}
|
|
191196
|
+
this.timers.clearTimeout(entry.timer);
|
|
189708
191197
|
this.approvals.delete(approvalId);
|
|
189709
|
-
this.
|
|
189710
|
-
|
|
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."
|
|
189711
191201
|
});
|
|
189712
191202
|
}
|
|
189713
191203
|
onStreamEnded(stream) {
|
|
@@ -189726,8 +191216,28 @@ ${asCodeBlock(redactSensitiveText(value.prompt))}`, {
|
|
|
189726
191216
|
for (const [id, entry] of [...this.approvals.entries()]) {
|
|
189727
191217
|
if (entry.sessionId === stream.sessionId) {
|
|
189728
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
|
+
}
|
|
191225
|
+
}
|
|
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
|
+
}
|
|
189729
191236
|
}
|
|
189730
191237
|
}
|
|
191238
|
+
if (stream.endedBecause === "closing") {
|
|
191239
|
+
this.hub?.endActivity(stream.sessionId);
|
|
191240
|
+
}
|
|
189731
191241
|
}
|
|
189732
191242
|
}
|
|
189733
191243
|
function asCodeBlock(text) {
|
|
@@ -189739,7 +191249,7 @@ function asCodeBlock(text) {
|
|
|
189739
191249
|
${safe}
|
|
189740
191250
|
${fence}`;
|
|
189741
191251
|
}
|
|
189742
|
-
var MAX_REPLY_CHARS = 20000, MAX_PENDING_APPROVALS_PER_SESSION = 8, MAX_REMEMBERED_IDS_PER_SESSION = 512, MAX_PROJECT_NAME_CHARS = 128, CONTROL_CHARS2, JSON_HEADERS2, MAX_BACKTICK_RUN = 8;
|
|
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;
|
|
189743
191253
|
var init_http_surface = __esm(() => {
|
|
189744
191254
|
init_service2();
|
|
189745
191255
|
init_hub();
|