@agentconnect.md/daemon 1.0.0-rc.230 → 1.0.0-rc.232

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -68502,8 +68502,8 @@ const [{ Command }, { fileURLToPath: fileURLToPath$1 }, { runChat }, { runLogin
68502
68502
  import("./commander-B5QVEFzH.js"),
68503
68503
  import("node:url"),
68504
68504
  import("./chat-Di5QFvjw.js"),
68505
- import("./login-8pfSiMc2.js"),
68506
- import("./run-foreground-DxSBKNXU.js"),
68505
+ import("./login-BCxHYEeG.js"),
68506
+ import("./run-foreground-BQl2a8y9.js"),
68507
68507
  import("./service-6w0WKhdx.js"),
68508
68508
  import("./lock-CA9Bm8_s.js"),
68509
68509
  import("./paths-CcRUjFjy.js")
@@ -1,5 +1,5 @@
1
1
  import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
2
- import { n as ClientTransport, t as runForeground } from "./run-foreground-Cg2ewYjn.js";
2
+ import { n as ClientTransport, t as runForeground } from "./run-foreground-DN1BSygk.js";
3
3
  import { c as ConfigSchema } from "./load-agents-tb8Rcwan.js";
4
4
  import { l as resolveRoot, t as configPath } from "./paths-DEcUO8lC.js";
5
5
  import { f as buildEnvelope, l as isFrame, m as encode, p as decodeEnvelope } from "./acp-host-CXUQ91Gh.js";
@@ -226,4 +226,4 @@ async function runLogin(opts, partial = {}) {
226
226
  //#endregion
227
227
  export { buildInstallOpts, persistCredentials, runLogin };
228
228
 
229
- //# sourceMappingURL=login-8pfSiMc2.js.map
229
+ //# sourceMappingURL=login-BCxHYEeG.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login-8pfSiMc2.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from './transport.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"login-BCxHYEeG.js","names":["realRunForeground"],"sources":["../src/cp/auth-probe.ts","../src/cli/login.ts"],"sourcesContent":["/**\n * `probeAuth` — a one-shot CP auth check used by `agentconnect login`. It dials\n * the CP, sends ONLY the `auth` frame (no `register`), and resolves with the\n * authoritative daemonId from `auth/ok`, or a human-readable failure reason on\n * a `4401` close, a correlated `error`, a dial failure, or a timeout. It always\n * closes the socket and never leaves a connection open.\n */\nimport { buildEnvelope, decodeEnvelope, encode, isFrame } from '@agentconnect.md/protocol'\nimport { ClientTransport, type Transport } from './transport.js'\n\nexport interface ProbeResult {\n ok: boolean\n daemonId?: string\n reason?: string\n}\n\nexport interface ProbeOpts {\n url: string\n token: string\n daemonId?: string\n agentVersion?: string\n timeoutMs?: number\n /** Injectable for tests; defaults to dialing the real WebSocket. */\n connect?: () => Promise<Transport>\n}\n\nexport async function probeAuth(opts: ProbeOpts): Promise<ProbeResult> {\n const timeoutMs = opts.timeoutMs ?? 10_000\n const dial = opts.connect ?? (() => ClientTransport.dial(opts.url))\n\n let transport: Transport\n try {\n transport = await dial()\n } catch (err) {\n return { ok: false, reason: `cannot reach ${opts.url}: ${(err as Error).message}` }\n }\n\n const authPayload: Record<string, unknown> = {\n apiKey: opts.token,\n agentVersion: opts.agentVersion ?? 'probe'\n }\n if (opts.daemonId) authPayload.daemonId = opts.daemonId\n const frame = buildEnvelope('auth', authPayload)\n\n return await new Promise<ProbeResult>((resolve) => {\n let settled = false\n const done = (r: ProbeResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n try {\n transport.close(1000, 'probe done')\n } catch {\n // socket may already be closed\n }\n resolve(r)\n }\n\n const timer = setTimeout(\n () => done({ ok: false, reason: `timed out after ${timeoutMs}ms waiting for auth/ok` }),\n timeoutMs\n )\n\n transport.onMessage((text) => {\n const decoded = decodeEnvelope(text)\n if (!decoded.ok) return\n const f = decoded.frame\n if (f.corr !== frame.id) return\n if (f.type === 'error') {\n const e = f.payload as { code: string; message: string }\n done({ ok: false, reason: `${e.code}: ${e.message}` })\n return\n }\n if (isFrame('auth/ok')(f)) {\n const ok = f.payload as { daemonId: string }\n done({ ok: true, daemonId: ok.daemonId })\n }\n })\n\n transport.onClose((code) => {\n if (code === 4401) done({ ok: false, reason: 'authentication failed (4401) — check the daemon token' })\n else done({ ok: false, reason: `connection closed (${code}) before auth/ok` })\n })\n\n transport.send(encode(frame))\n })\n}\n","/**\n * `agentconnect login` — interactive onboarding. It tests the Control Plane auth\n * flow, persists credentials only on success, then offers to install the OS\n * service. If installed it starts the service (`up`) and returns; otherwise it\n * runs the daemon in the foreground (the same path as `agentconnect run`).\n *\n * `persistCredentials` is the pure config-merge half (token-only onboarding:\n * `daemonId` is omitted unless given — the first connect adopts it from the\n * token's `sub`). `runLogin` is the orchestrator, with injectable deps for tests.\n */\nimport { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigSchema } from '../config/config-schema.js'\nimport { resolveRoot, configPath } from '../paths.js'\nimport { DAEMON_VERSION } from '../version.js'\nimport { probeAuth, type ProbeResult } from '../cp/auth-probe.js'\nimport { resolveController, type InstallOpts } from '../service/index.js'\nimport { runForeground as realRunForeground } from './run-foreground.js'\n\nexport interface PersistCredsOpts {\n cpUrl: string\n cpKey: string\n daemonId?: string\n root?: string\n configPath?: string\n}\n\n/** Merge url/token into config.json (validated), returning the written path. */\nexport function persistCredentials(opts: PersistCredsOpts): string {\n const root = resolveRoot(opts.root)\n const file = opts.configPath ?? configPath(root)\n const raw: Record<string, unknown> = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { version: 1 }\n\n const cp = (raw.controlPlane as Record<string, unknown> | undefined) ?? {}\n raw.controlPlane = { ...cp, url: opts.cpUrl, key: opts.cpKey, enabled: true }\n if (opts.daemonId) raw.daemonId = opts.daemonId\n\n ConfigSchema.parse(raw) // fail before writing if the merge is invalid\n\n mkdirSync(dirname(file), { recursive: true })\n writeFileSync(file, JSON.stringify(raw, null, 2) + '\\n')\n return file\n}\n\nexport interface RunLoginOpts {\n cpUrl?: string\n cpKey?: string\n daemonId?: string\n root?: string\n configPath?: string\n /** The resolved CLI entry path. Must be supplied by the CLI entry point\n * (index.ts) so the correct bundle path is used rather than inferring it\n * from login.ts's own import.meta.url (which is the same bundle but the\n * `../` path calculation would be wrong). Falls back to import.meta.url\n * for the pure test path where login.ts is loaded standalone. */\n cliEntry?: string\n}\n\n/**\n * Build the InstallOpts for the OS service installer.\n *\n * Exported as a pure helper so it can be unit-tested independently of the\n * full `runLogin` orchestration. The caller (index.ts) passes `cliEntry` so\n * the correct bundle path always flows through; if omitted we fall back to\n * `import.meta.url` (correct only when login.ts is the entry, e.g. in tests).\n */\nexport function buildInstallOpts(opts: { root?: string; cliEntry?: string }): InstallOpts {\n return {\n execPath: process.execPath,\n cliEntry: opts.cliEntry ?? fileURLToPath(import.meta.url),\n includeRootEnv: resolveRoot(opts.root) !== resolveRoot(undefined)\n }\n}\n\nexport interface LoginDeps {\n probe: (o: { url: string; token: string }) => Promise<ProbeResult>\n installService: () => Promise<void>\n runForeground: () => Promise<void>\n out: NodeJS.WritableStream\n input: NodeJS.ReadableStream\n isTTY: boolean\n}\n\nfunction realDeps(opts: RunLoginOpts): LoginDeps {\n return {\n probe: (o) => probeAuth({ url: o.url, token: o.token, agentVersion: DAEMON_VERSION }),\n installService: async () => {\n const controller = resolveController({ root: opts.root })\n await controller.install(buildInstallOpts(opts))\n await controller.up()\n },\n runForeground: () =>\n realRunForeground({\n root: opts.root,\n overrides: { cpUrl: opts.cpUrl, cpKey: opts.cpKey, daemonId: opts.daemonId }\n }),\n out: process.stdout,\n input: process.stdin,\n isTTY: Boolean(process.stdin.isTTY)\n }\n}\n\n/** Read the next line from a readline interface via async iterator. */\nasync function nextLine(\n iter: AsyncIterableIterator<string>,\n prompt: string,\n out: NodeJS.WritableStream\n): Promise<string> {\n out.write(prompt)\n const { value, done } = await iter.next()\n if (done) throw new Error(`unexpected end of input waiting for: ${prompt}`)\n return (value as string).trim()\n}\n\nexport async function runLogin(opts: RunLoginOpts, partial: Partial<LoginDeps> = {}): Promise<void> {\n const deps: LoginDeps = { ...realDeps(opts), ...partial }\n const { out } = deps\n\n // ── Non-interactive: flags required; probe; persist. No prompts, no handoff. ──\n if (!deps.isTTY) {\n if (!opts.cpUrl) throw new Error('login requires --cp-url <url>')\n if (!opts.cpKey) throw new Error('login requires --cp-key <key>')\n const r = await deps.probe({ url: opts.cpUrl, token: opts.cpKey })\n if (!r.ok) throw new Error(`authentication failed: ${r.reason}`)\n persistCredentials({ ...opts, cpUrl: opts.cpUrl, cpKey: opts.cpKey })\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''} — credentials saved\\n`)\n return\n }\n\n // Both interactive handoffs read `<root>/config.json`: the installed service\n // runs `… run` (no `--config`), and the foreground `Daemon` loads by root. A\n // custom `--config` would persist credentials the daemon never reads, so the\n // install/run branch would look successful but start without them. Reject it;\n // `--root` is the knob that keeps persist + service + foreground in sync.\n if (opts.configPath) {\n throw new Error(\n 'interactive login does not support --config — use --root <dir> so the daemon and the installed service read the same config'\n )\n }\n\n // ── Interactive ──\n const rl = createInterface({ input: deps.input, output: out })\n const iter = rl[Symbol.asyncIterator]()\n let url = opts.cpUrl\n let token = opts.cpKey\n try {\n if (!url) url = await nextLine(iter, 'Control Plane URL: ', out)\n\n for (let attempt = 0; ; attempt++) {\n if (!token) token = await nextLine(iter, 'Daemon API key: ', out)\n out.write(`Testing connection to ${url}…\\n`)\n const r = await deps.probe({ url: url!, token: token! })\n if (r.ok) {\n out.write(`✓ authenticated${r.daemonId ? ` (daemon ${r.daemonId})` : ''}\\n`)\n break\n }\n out.write(`✗ ${r.reason}\\n`)\n if (attempt >= 1) throw new Error('authentication failed')\n token = undefined // re-prompt on retry\n }\n\n persistCredentials({ ...opts, cpUrl: url!, cpKey: token! })\n\n const ans = (await nextLine(iter, 'Install AgentConnect as a background service? (y/N) ', out)).toLowerCase()\n if (ans === 'y' || ans === 'yes') {\n await deps.installService()\n out.write('Service installed and started. Manage it with `agentconnect up` / `down` / `status`.\\n')\n return\n }\n out.write('Starting in the foreground (Ctrl-C to stop)…\\n')\n } finally {\n rl.close()\n }\n\n await deps.runForeground()\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { t as runForeground } from "./run-foreground-DN1BSygk.js";
2
+ export { runForeground };
@@ -5517,6 +5517,19 @@ var LocalStore = class {
5517
5517
  if (agentId !== void 0) return this.db.prepare("SELECT * FROM sessions WHERE acpSessionId IS NOT NULL AND agentId = ? ORDER BY updatedAt DESC").all(agentId);
5518
5518
  return this.db.prepare("SELECT * FROM sessions WHERE acpSessionId IS NOT NULL ORDER BY updatedAt DESC").all();
5519
5519
  }
5520
+ /** The most recently active addressable session (has an ACP id) for an agent in a
5521
+ * channel, or undefined. Backs `/status` when the command message itself doesn't fall
5522
+ * in a session's thread (a bare Telegram `/status` keys to its own reply thread) — we
5523
+ * then report the channel's latest session rather than "nothing here". */
5524
+ latestSession(agentId, channel) {
5525
+ return this.db.prepare("SELECT * FROM sessions WHERE agentId = ? AND channel = ? AND acpSessionId IS NOT NULL ORDER BY updatedAt DESC LIMIT 1").get(agentId, channel);
5526
+ }
5527
+ /** The most recently active addressable session (has an ACP id) in a channel, across
5528
+ * ALL agents — or undefined. Used to resolve which agent a bare command targets when
5529
+ * routing can't (e.g. a group `/status@bot` with no mention entity / thread). */
5530
+ latestSessionInChannel(channel) {
5531
+ return this.db.prepare("SELECT * FROM sessions WHERE channel = ? AND acpSessionId IS NOT NULL ORDER BY updatedAt DESC LIMIT 1").get(channel);
5532
+ }
5520
5533
  /** Lookup by ACP session id (the protocol-facing `sessionId`), or undefined. */
5521
5534
  getSessionByAcpId(acpSessionId) {
5522
5535
  return this.db.prepare("SELECT * FROM sessions WHERE acpSessionId = ?").get(acpSessionId);
@@ -10169,6 +10182,13 @@ const CANCEL_WORDS = /* @__PURE__ */ new Set(["cancel"]);
10169
10182
  const QUEUE_WORDS = /* @__PURE__ */ new Set(["queue"]);
10170
10183
  const STATUS_WORDS = /* @__PURE__ */ new Set(["status"]);
10171
10184
  const FAST_WORDS = /* @__PURE__ */ new Set(["fast"]);
10185
+ const MODEL_WORDS = /* @__PURE__ */ new Set(["model", "models"]);
10186
+ const EFFORT_WORDS = /* @__PURE__ */ new Set(["effort"]);
10187
+ const PERMISSION_WORDS = /* @__PURE__ */ new Set([
10188
+ "permission",
10189
+ "permissions",
10190
+ "perm"
10191
+ ]);
10172
10192
  /**
10173
10193
  * Parse a leading control command from a message's text. Returns `null` when the
10174
10194
  * text is not a recognized command (so it flows to the agent unchanged). The
@@ -10199,6 +10219,18 @@ function parseCommand(raw) {
10199
10219
  enable: a === "on" ? true : a === "off" ? false : null
10200
10220
  };
10201
10221
  }
10222
+ if (MODEL_WORDS.has(word)) return {
10223
+ kind: "model",
10224
+ value: arg || null
10225
+ };
10226
+ if (EFFORT_WORDS.has(word)) return {
10227
+ kind: "effort",
10228
+ value: arg || null
10229
+ };
10230
+ if (PERMISSION_WORDS.has(word)) return {
10231
+ kind: "permission",
10232
+ value: arg || null
10233
+ };
10202
10234
  return null;
10203
10235
  }
10204
10236
  //#endregion
@@ -117446,6 +117478,18 @@ const BOT_COMMANDS = [
117446
117478
  command: "fast",
117447
117479
  description: "Toggle fast mode — /fast on | off"
117448
117480
  },
117481
+ {
117482
+ command: "models",
117483
+ description: "Choose the model — /models [name] (bare = list)"
117484
+ },
117485
+ {
117486
+ command: "effort",
117487
+ description: "Choose reasoning effort — /effort [level] (bare = list)"
117488
+ },
117489
+ {
117490
+ command: "permission",
117491
+ description: "Choose permission mode — /permission [mode] (bare = list)"
117492
+ },
117449
117493
  {
117450
117494
  command: "queue",
117451
117495
  description: "Queue a message to run when idle — /queue <text>"
@@ -117485,12 +117529,22 @@ function defaultFactory(token) {
117485
117529
  onMessage(handler) {
117486
117530
  bot.on("message", (ctx) => handler(ctx.message));
117487
117531
  },
117532
+ onCallbackQuery(handler) {
117533
+ bot.on("callback_query:data", (ctx) => handler(ctx.callbackQuery));
117534
+ },
117488
117535
  start(onStart) {
117489
117536
  bot.start(onStart ? { onStart: () => onStart() } : void 0);
117490
117537
  },
117491
117538
  stop: () => bot.stop()
117492
117539
  };
117493
117540
  }
117541
+ /** Map our button rows to grammY's `InlineKeyboardMarkup` wire shape. */
117542
+ function toInlineKeyboard(buttons) {
117543
+ return { inline_keyboard: buttons.map((row) => row.map((b) => ({
117544
+ text: b.text,
117545
+ callback_data: b.callbackData
117546
+ }))) };
117547
+ }
117494
117548
  const DEFAULT_MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
117495
117549
  /** Cap on admins enriched per listMembers call (bounds the response). */
117496
117550
  const MEMBER_CAP = 50;
@@ -117526,6 +117580,19 @@ var TelegramConnection = class {
117526
117580
  log?.debug(`telegram: inbound ch=${msg.channel} user=${msg.sender.id} isBot=${msg.sender.isBot} isDm=${msg.isDm} topic=${msg.telegramTopicId ?? "-"} root=${msg.telegramThreadRoot ?? "-"} replyTo=${msg.replyTo ?? "-"} mentions=[${msg.mentionedBots.join(",")}] text=${JSON.stringify(msg.text.slice(0, 80))}`);
117527
117581
  this.deps.onMessage(msg);
117528
117582
  });
117583
+ this.bot.onCallbackQuery((q) => {
117584
+ if (!q.message || q.data == null) return;
117585
+ const cb = {
117586
+ id: q.id,
117587
+ data: q.data,
117588
+ channel: String(q.message.chat.id),
117589
+ messageId: q.message.message_id,
117590
+ userId: q.from ? String(q.from.id) : "unknown",
117591
+ ...q.message.message_thread_id != null ? { topicId: String(q.message.message_thread_id) } : {}
117592
+ };
117593
+ log?.debug(`telegram: callback ch=${cb.channel} user=${cb.userId} data=${JSON.stringify(cb.data)}`);
117594
+ this.deps.onCallback?.(cb);
117595
+ });
117529
117596
  try {
117530
117597
  await this.bot.api.setMyCommands(BOT_COMMANDS);
117531
117598
  log?.debug("telegram: setMyCommands ok");
@@ -117581,6 +117648,53 @@ var TelegramConnection = class {
117581
117648
  }
117582
117649
  });
117583
117650
  }
117651
+ /**
117652
+ * Post an interactive "card": a message carrying an inline keyboard (rows of tappable
117653
+ * buttons) — the session-control pickers (/models, /effort, /permission). Best-effort;
117654
+ * returns the new message id so the card can be edited in place after a tap.
117655
+ */
117656
+ async postCard(channel, text, buttons, opts = {}) {
117657
+ const thread = opts.threadTs != null && /^\d+$/.test(opts.threadTs) ? Number(opts.threadTs) : void 0;
117658
+ return this.queue.enqueue(async () => {
117659
+ try {
117660
+ const res = await this.bot.api.sendMessage(channel, text, {
117661
+ ...thread !== void 0 ? { message_thread_id: thread } : {},
117662
+ ...opts.parseMode ? { parse_mode: opts.parseMode } : {},
117663
+ ...opts.replyTo !== void 0 ? { reply_parameters: {
117664
+ message_id: opts.replyTo,
117665
+ allow_sending_without_reply: true
117666
+ } } : {},
117667
+ reply_markup: toInlineKeyboard(buttons)
117668
+ });
117669
+ return res?.message_id != null ? String(res.message_id) : void 0;
117670
+ } catch (err) {
117671
+ this.deps.log?.debug(`telegram: sendMessage (card) failed (ch=${channel}): ${err.message}`);
117672
+ return;
117673
+ }
117674
+ });
117675
+ }
117676
+ /** Re-render a card in place (new text + keyboard) after a tap. Best-effort. */
117677
+ async editCard(channel, messageId, text, buttons, opts = {}) {
117678
+ await this.queue.enqueue(async () => {
117679
+ try {
117680
+ await this.bot.api.editMessageText(channel, messageId, text, {
117681
+ ...opts.parseMode ? { parse_mode: opts.parseMode } : {},
117682
+ reply_markup: toInlineKeyboard(buttons)
117683
+ });
117684
+ } catch (err) {
117685
+ this.deps.log?.debug(`telegram: editMessageText (card) failed (ch=${channel} id=${messageId}): ${err.message}`);
117686
+ }
117687
+ });
117688
+ }
117689
+ /** Acknowledge an inline-keyboard tap (clears the client's loading spinner; the
117690
+ * optional `text` shows as a transient toast). Not queued — a quick standalone ack. */
117691
+ async answerCallback(callbackId, text) {
117692
+ try {
117693
+ await this.bot.api.answerCallbackQuery(callbackId, text ? { text } : void 0);
117694
+ } catch (err) {
117695
+ this.deps.log?.debug(`telegram: answerCallbackQuery failed: ${err.message}`);
117696
+ }
117697
+ }
117584
117698
  /** Edit a previously-posted message in place (editMessageText). Best-effort. */
117585
117699
  async updateMessage(channel, messageId, text, opts = {}) {
117586
117700
  await this.queue.enqueue(async () => {
@@ -136307,6 +136421,16 @@ function formatErr(err) {
136307
136421
  return e?.stack ?? String(err);
136308
136422
  }
136309
136423
  const MAX_QUEUED_PER_SESSION = 10;
136424
+ const SELECT_KIND_CODE = {
136425
+ model: "m",
136426
+ effort: "e",
136427
+ permission: "p"
136428
+ };
136429
+ const SELECT_CODE_KIND = {
136430
+ m: "model",
136431
+ e: "effort",
136432
+ p: "permission"
136433
+ };
136310
136434
  const WEBCHAT_CHUNK_BYTES = 32 * 1024;
136311
136435
  /** Split `text` into pieces whose UTF-8 byte length each stays under WEBCHAT_CHUNK_BYTES
136312
136436
  * so no single webchat/output frame exceeds the 256 KiB cap. Splits on byte budget by
@@ -136801,6 +136925,7 @@ var Daemon = class Daemon {
136801
136925
  group,
136802
136926
  newTraceId: () => randomUUID(),
136803
136927
  onMessage: (msg) => this.onInbound(msg),
136928
+ onCallback: (cb) => this.handleTelegramCallback(cb, conn),
136804
136929
  log: this.log
136805
136930
  });
136806
136931
  try {
@@ -137280,28 +137405,59 @@ var Daemon = class Daemon {
137280
137405
  }
137281
137406
  queued = /* @__PURE__ */ new Map();
137282
137407
  /**
137408
+ * Resolve a command's target from the channel's latest session when the routing ladder
137409
+ * couldn't (no mention entity / thread / dm rule matched — e.g. a group `/status@bot`).
137410
+ * Picks the agent that owns the most-recent session in the channel and its integration
137411
+ * for this platform, enforcing that integration's `allowedUserIds` so a command can't
137412
+ * bypass the authz the routing rules would have applied. Null when there's no session,
137413
+ * no matching integration, or the sender isn't allowed.
137414
+ */
137415
+ resolveCommandTargetFromLatest(msg) {
137416
+ const latest = this.store.latestSessionInChannel(msg.channel);
137417
+ if (!latest) return null;
137418
+ const integ = this.agents.get(latest.agentId)?.integrations.find((i) => i.platform === msg.platform);
137419
+ if (!integ) return null;
137420
+ const allowed = integ.platform === "telegram" ? integ.telegram.allowedUserIds : integ.slack.allowedUserIds;
137421
+ if (allowed.length > 0 && !allowed.includes(msg.sender.id)) return null;
137422
+ return {
137423
+ agentId: latest.agentId,
137424
+ integrationId: integ.id,
137425
+ via: "thread"
137426
+ };
137427
+ }
137428
+ /**
137283
137429
  * Handle an in-conversation control command. Resolves the target agent via the
137284
137430
  * same routing ladder as a normal message (so thread-affinity + per-integration
137285
137431
  * `allowedUserIds` authz apply), then acts on that agent's session in this
137286
137432
  * (channel, thread).
137287
137433
  */
137288
137434
  handleCommand(command, msg) {
137289
- const target = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
137435
+ let target = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
137436
+ if (!target) target = this.resolveCommandTargetFromLatest(msg);
137290
137437
  if (!target) {
137291
137438
  this.log.debug(`command: '${command.kind}' in ch=${msg.channel} — no agent resolved, ignoring`);
137292
137439
  return;
137293
137440
  }
137294
137441
  const conn = this.replyConnFor(target.agentId, target.integrationId);
137295
- const thread = msg.thread ?? msg.msgId;
137296
- const key = sessionKey(msg.platform, msg.channel, thread, target.agentId);
137297
- const rec = this.store.getSession(key);
137442
+ const replyThread = msg.thread ?? msg.msgId;
137443
+ let thread = replyThread;
137444
+ let key = sessionKey(msg.platform, msg.channel, thread, target.agentId);
137445
+ let rec = this.store.getSession(key);
137446
+ if (!rec) {
137447
+ const latest = this.store.latestSession(target.agentId, msg.channel);
137448
+ if (latest) {
137449
+ rec = latest;
137450
+ key = latest.key;
137451
+ thread = latest.thread;
137452
+ }
137453
+ }
137298
137454
  const acpSessionId = rec?.acpSessionId;
137299
137455
  const inflight = !!(acpSessionId && this.pending.has(acpSessionId));
137300
137456
  const tgReplyTo = this.telegramReplyTarget(msg);
137301
137457
  const reply = (text) => {
137302
137458
  if (!conn) return;
137303
- if (msg.platform === "telegram") conn.postMessage(msg.channel, text, thread, tgReplyTo);
137304
- else conn.postMessage(msg.channel, text, thread);
137459
+ if (msg.platform === "telegram") conn.postMessage(msg.channel, text, replyThread, tgReplyTo);
137460
+ else conn.postMessage(msg.channel, text, replyThread);
137305
137461
  };
137306
137462
  if (command.kind === "stop") {
137307
137463
  if (rec) this.store.setSessionMuted(key, true);
@@ -137332,7 +137488,7 @@ var Daemon = class Daemon {
137332
137488
  const link = acpSessionId ? this.sessionLink(acpSessionId) : void 0;
137333
137489
  if (msg.platform === "telegram") conn?.postChrome(msg.channel, renderStatusReply(info, link), {
137334
137490
  parseMode: "HTML",
137335
- threadTs: thread,
137491
+ threadTs: replyThread,
137336
137492
  replyTo: tgReplyTo
137337
137493
  });
137338
137494
  else reply(link ? `${renderStatusBar(info)} · <${link}|View session>` : renderStatusBar(info));
@@ -137351,13 +137507,26 @@ var Daemon = class Daemon {
137351
137507
  reply(command.enable ? "⚡ Fast mode on." : "🐢 Fast mode off.");
137352
137508
  return;
137353
137509
  }
137510
+ if (command.kind === "model" || command.kind === "effort" || command.kind === "permission") {
137511
+ if (!rec) {
137512
+ reply("No active session here to configure.");
137513
+ return;
137514
+ }
137515
+ const card = msg.platform === "telegram" && conn ? (text, buttons) => void conn.postCard(msg.channel, text, buttons, {
137516
+ threadTs: replyThread,
137517
+ replyTo: tgReplyTo
137518
+ }) : void 0;
137519
+ this.handleSelectCommand(command.kind, command.value, target.agentId, key, acpSessionId ?? void 0, reply, card);
137520
+ return;
137521
+ }
137354
137522
  if (!command.text) {
137355
137523
  reply("Usage: `!queue <message>` — runs when the current turn finishes.");
137356
137524
  return;
137357
137525
  }
137358
137526
  const payload = {
137359
137527
  ...msg,
137360
- text: command.text
137528
+ text: command.text,
137529
+ thread
137361
137530
  };
137362
137531
  if (!inflight) {
137363
137532
  this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
@@ -137375,6 +137544,134 @@ var Daemon = class Daemon {
137375
137544
  this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
137376
137545
  reply(`📥 Queued (#${q.length}) — will run when the current turn finishes.`);
137377
137546
  }
137547
+ selectLabel(kind) {
137548
+ return kind === "model" ? "Model" : kind === "effort" ? "Reasoning effort" : "Permission mode";
137549
+ }
137550
+ /** Current value + selectable options for a select kind, from a status snapshot. */
137551
+ selectOptions(kind, info) {
137552
+ if (kind === "model") return {
137553
+ current: info.model,
137554
+ options: info.models ?? []
137555
+ };
137556
+ if (kind === "effort") return {
137557
+ current: info.effort,
137558
+ options: info.efforts ?? []
137559
+ };
137560
+ return {
137561
+ current: info.permissionMode,
137562
+ options: info.permissionModes ?? []
137563
+ };
137564
+ }
137565
+ /** Apply a resolved select value to a session key via the matching sticky-override setter. */
137566
+ applySelect(kind, key, value) {
137567
+ if (kind === "model") this.setModelByKey(key, value);
137568
+ else if (kind === "effort") this.setEffortByKey(key, value);
137569
+ else this.setPermissionModeByKey(key, value);
137570
+ }
137571
+ /** Build a session-control card: a header line + one tappable button per option (the
137572
+ * current one flagged), with `callback_data` = `<kindCode>:<optionIndex>`. */
137573
+ buildSelectCard(kind, current, options) {
137574
+ const code = SELECT_KIND_CODE[kind];
137575
+ const buttons = options.map((o, i) => [{
137576
+ text: `${o === current ? "✅ " : ""}${o}`,
137577
+ callbackData: `${code}:${i}`
137578
+ }]);
137579
+ return {
137580
+ text: `${this.selectLabel(kind)} — tap to switch (current: ${current ?? "default"}):`,
137581
+ buttons
137582
+ };
137583
+ }
137584
+ /**
137585
+ * Back the `/models` `/effort` `/permission` commands. A bare command lists the current
137586
+ * selectable values — as a tappable inline-keyboard card when `card` is provided
137587
+ * (Telegram), else a numbered text list. An argument applies a choice, matched by exact
137588
+ * id, unique case-insensitive substring, or 1-based list index. Options come from the
137589
+ * live host's config selectors (statusInfoFrom); when the host is cold a given value is
137590
+ * accepted optimistically and takes effect on the next turn.
137591
+ */
137592
+ handleSelectCommand(kind, value, agentId, key, acpSessionId, reply, card) {
137593
+ const label = this.selectLabel(kind);
137594
+ const info = this.statusInfoFrom(agentId, key, acpSessionId);
137595
+ const { current, options } = this.selectOptions(kind, info);
137596
+ const cmd = kind === "model" ? "models" : kind;
137597
+ if (value === null) {
137598
+ if (options.length === 0) {
137599
+ reply(current ? `${label}: ${current} (no other options offered${kind === "effort" ? " — the current model may not support effort" : ""}).` : `No ${label.toLowerCase()} options available yet — send me a message first, then try /${cmd} again.`);
137600
+ return;
137601
+ }
137602
+ if (card) {
137603
+ const { text, buttons } = this.buildSelectCard(kind, current, options);
137604
+ card(text, buttons);
137605
+ return;
137606
+ }
137607
+ reply(`${label} — reply \`/${cmd} <name or number>\`:\n${options.map((o, i) => `${i + 1}. ${o}${o === current ? " ✓ (current)" : ""}`).join("\n")}`);
137608
+ return;
137609
+ }
137610
+ let resolved;
137611
+ if (options.length === 0) resolved = value.trim();
137612
+ else {
137613
+ const v = value.trim();
137614
+ const idx = Number(v);
137615
+ if (Number.isInteger(idx) && idx >= 1 && idx <= options.length) resolved = options[idx - 1];
137616
+ else {
137617
+ const lc = v.toLowerCase();
137618
+ resolved = options.find((o) => o.toLowerCase() === lc) ?? (options.filter((o) => o.toLowerCase().includes(lc)).length === 1 ? options.find((o) => o.toLowerCase().includes(lc)) : void 0);
137619
+ }
137620
+ }
137621
+ if (!resolved) {
137622
+ reply(`Unknown ${label.toLowerCase()} "${value.trim()}".${options.length ? ` Options: ${options.join(", ")}` : ""}`);
137623
+ return;
137624
+ }
137625
+ this.applySelect(kind, key, resolved);
137626
+ reply(options.length === 0 ? `${label} set to ${resolved} — applies on your next message.` : `✅ ${label} set to ${resolved}.`);
137627
+ }
137628
+ /**
137629
+ * Handle a tapped session-control card button (Telegram inline keyboard). Decodes
137630
+ * `<kindCode>:<optionIndex>`, resolves the channel's latest session (with the same
137631
+ * allowedUserIds authz as a command), applies the picked value, acks the tap, and
137632
+ * re-renders the card with the new current marked. Best-effort throughout — a tap
137633
+ * must never throw out of the update pump.
137634
+ */
137635
+ handleTelegramCallback(cb, conn) {
137636
+ const m = /^([mep]):(\d+)$/.exec(cb.data);
137637
+ if (!m) {
137638
+ conn.answerCallback(cb.id);
137639
+ return;
137640
+ }
137641
+ const kind = SELECT_CODE_KIND[m[1]];
137642
+ const idx = Number(m[2]);
137643
+ const session = this.commandSessionForLatest(cb.channel, cb.userId);
137644
+ if (!session) {
137645
+ conn.answerCallback(cb.id, "No active session here.");
137646
+ return;
137647
+ }
137648
+ const info = this.statusInfoFrom(session.agentId, session.key, session.acpSessionId);
137649
+ const { options } = this.selectOptions(kind, info);
137650
+ const value = options[idx];
137651
+ if (value === void 0) {
137652
+ conn.answerCallback(cb.id, "Options changed — reopen the menu.");
137653
+ return;
137654
+ }
137655
+ this.applySelect(kind, session.key, value);
137656
+ conn.answerCallback(cb.id, `${this.selectLabel(kind)} → ${value}`);
137657
+ const { text, buttons } = this.buildSelectCard(kind, value, options);
137658
+ conn.editCard(cb.channel, cb.messageId, text, buttons);
137659
+ }
137660
+ /** The channel's latest session for a Telegram command/callback, gated by the agent's
137661
+ * telegram integration allowedUserIds. Null when there's no session, no telegram
137662
+ * integration, or the user isn't allowed. */
137663
+ commandSessionForLatest(channel, userId) {
137664
+ const latest = this.store.latestSessionInChannel(channel);
137665
+ if (!latest) return null;
137666
+ const integ = this.agents.get(latest.agentId)?.integrations.find((i) => i.platform === "telegram");
137667
+ if (!integ || integ.platform !== "telegram") return null;
137668
+ if (integ.telegram.allowedUserIds.length > 0 && !integ.telegram.allowedUserIds.includes(userId)) return null;
137669
+ return {
137670
+ agentId: latest.agentId,
137671
+ key: latest.key,
137672
+ acpSessionId: latest.acpSessionId ?? void 0
137673
+ };
137674
+ }
137378
137675
  /** Drain one buffered message for a session whose turn just finished (FIFO). */
137379
137676
  flushQueued(agentId, sessionId, integrationId) {
137380
137677
  const q = this.queued.get(sessionId);
@@ -138637,4 +138934,4 @@ async function runForeground(opts, deps = {}) {
138637
138934
  //#endregion
138638
138935
  export { ClientTransport as n, runForeground as t };
138639
138936
 
138640
- //# sourceMappingURL=run-foreground-Cg2ewYjn.js.map
138937
+ //# sourceMappingURL=run-foreground-DN1BSygk.js.map