clisbot 0.1.11 → 0.1.13

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/main.js CHANGED
@@ -54670,7 +54670,7 @@ function renderCliHelp() {
54670
54670
  " Fresh bootstrap only enables channels named by flags; ambient env vars alone do not auto-enable extra channels.",
54671
54671
  "",
54672
54672
  "Usage:",
54673
- " clisbot start [--cli <codex|claude>] [--bot-type <personal|team>] [--persist]",
54673
+ " clisbot start [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54674
54674
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54675
54675
  " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54676
54676
  " clisbot restart",
@@ -54684,7 +54684,7 @@ function renderCliHelp() {
54684
54684
  " clisbot message <subcommand>",
54685
54685
  " clisbot agents <subcommand>",
54686
54686
  " clisbot pairing <subcommand>",
54687
- " clisbot init [--cli <codex|claude>] [--bot-type <personal|team>] [--persist]",
54687
+ " clisbot init [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54688
54688
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54689
54689
  " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54690
54690
  " clis <same-command>",
@@ -55104,7 +55104,7 @@ async function runPairingCli(args, writer = console) {
55104
55104
  }
55105
55105
 
55106
55106
  // src/config/agent-tool-presets.ts
55107
- var SUPPORTED_AGENT_CLI_TOOLS = ["codex", "claude"];
55107
+ var SUPPORTED_AGENT_CLI_TOOLS = ["codex", "claude", "gemini"];
55108
55108
  var SUPPORTED_BOOTSTRAP_MODES = ["personal-assistant", "team-assistant"];
55109
55109
  var SESSION_ID_PATTERN = "\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b";
55110
55110
  var DEFAULT_AGENT_TOOL_TEMPLATES = {
@@ -55169,6 +55169,41 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
55169
55169
  ]
55170
55170
  }
55171
55171
  }
55172
+ },
55173
+ gemini: {
55174
+ command: "gemini",
55175
+ startupOptions: ["--approval-mode=yolo", "--sandbox=false"],
55176
+ trustWorkspace: true,
55177
+ startupDelayMs: 15000,
55178
+ startupReadyPattern: "Type your message or @path/to/file",
55179
+ startupBlockers: [
55180
+ {
55181
+ pattern: "Please visit the following URL to authorize the application|Enter the authorization code:",
55182
+ message: "Gemini CLI is waiting for manual OAuth authorization. Authenticate Gemini once in a direct interactive terminal, or configure headless auth such as GEMINI_API_KEY or Vertex AI before routing Gemini through clisbot."
55183
+ },
55184
+ {
55185
+ pattern: "How would you like to authenticate for this project\\?|Failed to sign in\\.|Manual authorization is required but the current session is non-interactive",
55186
+ message: "Gemini CLI is blocked in its authentication setup flow or sign-in recovery. Complete Gemini authentication directly first, or switch clisbot to a headless auth path such as GEMINI_API_KEY or Vertex AI before routing prompts."
55187
+ }
55188
+ ],
55189
+ promptSubmitDelayMs: 200,
55190
+ sessionId: {
55191
+ create: {
55192
+ mode: "runner",
55193
+ args: []
55194
+ },
55195
+ capture: {
55196
+ mode: "status-command",
55197
+ statusCommand: "/stats session",
55198
+ pattern: SESSION_ID_PATTERN,
55199
+ timeoutMs: 8000,
55200
+ pollIntervalMs: 250
55201
+ },
55202
+ resume: {
55203
+ mode: "command",
55204
+ args: ["--resume", "{sessionId}", "--approval-mode=yolo", "--sandbox=false"]
55205
+ }
55206
+ }
55172
55207
  }
55173
55208
  };
55174
55209
  function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
@@ -55179,6 +55214,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55179
55214
  args: [...options, "-C", "{workspace}"],
55180
55215
  trustWorkspace: template.trustWorkspace,
55181
55216
  startupDelayMs: template.startupDelayMs,
55217
+ startupReadyPattern: template.startupReadyPattern,
55218
+ startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55182
55219
  promptSubmitDelayMs: template.promptSubmitDelayMs,
55183
55220
  sessionId: {
55184
55221
  ...template.sessionId,
@@ -55201,6 +55238,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55201
55238
  args: [...options],
55202
55239
  trustWorkspace: template.trustWorkspace,
55203
55240
  startupDelayMs: template.startupDelayMs,
55241
+ startupReadyPattern: template.startupReadyPattern,
55242
+ startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55204
55243
  promptSubmitDelayMs: template.promptSubmitDelayMs,
55205
55244
  sessionId: {
55206
55245
  ...template.sessionId,
@@ -55229,6 +55268,9 @@ function inferAgentCliToolId(command) {
55229
55268
  if (trimmed === "claude") {
55230
55269
  return "claude";
55231
55270
  }
55271
+ if (trimmed === "gemini") {
55272
+ return "gemini";
55273
+ }
55232
55274
  return null;
55233
55275
  }
55234
55276
 
@@ -59759,6 +59801,10 @@ var runnerSessionIdObjectSchema = exports_external.object({
59759
59801
  resume: runnerSessionIdResumeSchema.default(defaultRunnerSessionIdConfig.resume)
59760
59802
  });
59761
59803
  var runnerSessionIdSchema = runnerSessionIdObjectSchema.default(defaultRunnerSessionIdConfig);
59804
+ var runnerStartupBlockerSchema = exports_external.object({
59805
+ pattern: exports_external.string().min(1),
59806
+ message: exports_external.string().min(1)
59807
+ });
59762
59808
  var runnerSchema = exports_external.object({
59763
59809
  command: exports_external.string().min(1),
59764
59810
  args: exports_external.array(exports_external.string()).default([
@@ -59769,6 +59815,8 @@ var runnerSchema = exports_external.object({
59769
59815
  ]),
59770
59816
  trustWorkspace: exports_external.boolean().default(true),
59771
59817
  startupDelayMs: exports_external.number().int().positive().default(3000),
59818
+ startupReadyPattern: exports_external.string().min(1).optional(),
59819
+ startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
59772
59820
  promptSubmitDelayMs: exports_external.number().int().min(0).default(150),
59773
59821
  sessionId: runnerSessionIdSchema.default(defaultRunnerSessionIdConfig)
59774
59822
  });
@@ -59803,6 +59851,8 @@ var runnerOverrideSchema = exports_external.object({
59803
59851
  args: exports_external.array(exports_external.string()).optional(),
59804
59852
  trustWorkspace: exports_external.boolean().optional(),
59805
59853
  startupDelayMs: exports_external.number().int().positive().optional(),
59854
+ startupReadyPattern: exports_external.string().min(1).optional(),
59855
+ startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
59806
59856
  promptSubmitDelayMs: exports_external.number().int().min(0).optional(),
59807
59857
  sessionId: runnerSessionIdObjectSchema.partial().extend({
59808
59858
  create: runnerSessionIdCreateSchema.partial().optional(),
@@ -60347,6 +60397,7 @@ function renderDefaultConfigTemplate(options = {}) {
60347
60397
  ],
60348
60398
  trustWorkspace: true,
60349
60399
  startupDelayMs: 3000,
60400
+ startupReadyPattern: undefined,
60350
60401
  promptSubmitDelayMs: 150,
60351
60402
  sessionId: {
60352
60403
  create: {
@@ -60575,7 +60626,8 @@ var CUSTOMIZED_TEMPLATE_DIR = join4(TEMPLATE_ROOT, "customized");
60575
60626
  var CUSTOMIZED_DEFAULT_TEMPLATE_DIR = join4(CUSTOMIZED_TEMPLATE_DIR, "default");
60576
60627
  var TOOL_BOOTSTRAP_FILE = {
60577
60628
  codex: "AGENTS.md",
60578
- claude: "CLAUDE.md"
60629
+ claude: "CLAUDE.md",
60630
+ gemini: "GEMINI.md"
60579
60631
  };
60580
60632
  function shouldIncludeTemplateFile(toolId, relativePath) {
60581
60633
  const normalized = relativePath.replaceAll("\\", "/");
@@ -60585,6 +60637,9 @@ function shouldIncludeTemplateFile(toolId, relativePath) {
60585
60637
  if (normalized.endsWith("CLAUDE.md")) {
60586
60638
  return toolId === "claude";
60587
60639
  }
60640
+ if (normalized.endsWith("GEMINI.md")) {
60641
+ return toolId === "gemini";
60642
+ }
60588
60643
  return true;
60589
60644
  }
60590
60645
  function collectTemplateFiles(rootDir, toolId, prefix = "") {
@@ -60882,11 +60937,11 @@ async function addAgentToEditableConfig(params) {
60882
60937
  async function addAgent(args) {
60883
60938
  const agentId = args[0]?.trim();
60884
60939
  if (!agentId) {
60885
- throw new Error("Usage: agents add <id> --cli <codex|claude> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...");
60940
+ throw new Error("Usage: agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...");
60886
60941
  }
60887
60942
  const cliTool = parseSingleOption(args, "--cli");
60888
60943
  if (!cliTool || !(cliTool in DEFAULT_AGENT_TOOL_TEMPLATES)) {
60889
- throw new Error("agents add requires --cli codex or --cli claude");
60944
+ throw new Error("agents add requires --cli codex, --cli claude, or --cli gemini");
60890
60945
  }
60891
60946
  const workspace = parseSingleOption(args, "--workspace");
60892
60947
  const startupOptions = parseRepeatedOption(args, "--startup-option");
@@ -61088,7 +61143,7 @@ async function runAgentsCli(args) {
61088
61143
 
61089
61144
  // src/control/accounts-cli.ts
61090
61145
  import { setTimeout as sleep2 } from "node:timers/promises";
61091
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
61146
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
61092
61147
 
61093
61148
  // src/config/channel-account-management.ts
61094
61149
  function getFirstAccountId(accounts) {
@@ -61437,8 +61492,8 @@ class RuntimeHealthStore {
61437
61492
  }
61438
61493
 
61439
61494
  // src/control/runtime-process.ts
61440
- import { spawn as spawn2 } from "node:child_process";
61441
- import { closeSync, existsSync as existsSync6, openSync, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
61495
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
61496
+ import { closeSync, existsSync as existsSync6, openSync, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
61442
61497
  import { dirname as dirname9 } from "node:path";
61443
61498
  import { kill } from "node:process";
61444
61499
 
@@ -61807,6 +61862,12 @@ class StartDetachedRuntimeError extends Error {
61807
61862
  this.name = "StartDetachedRuntimeError";
61808
61863
  }
61809
61864
  }
61865
+ var DEFAULT_PROCESS_LIVENESS_DEPENDENCIES = {
61866
+ platform: process.platform,
61867
+ signalCheck: signalCheckProcess,
61868
+ readLinuxProcStat: readLinuxProcStatLiveness,
61869
+ readPsStat: readPsStatLiveness
61870
+ };
61810
61871
  function readRuntimePid(pidPath) {
61811
61872
  const expandedPidPath = resolvePidPath(pidPath);
61812
61873
  if (!existsSync6(expandedPidPath)) {
@@ -61819,12 +61880,28 @@ function readRuntimePid(pidPath) {
61819
61880
  });
61820
61881
  }
61821
61882
  function isProcessRunning(pid) {
61822
- try {
61823
- kill(pid, 0);
61824
- return true;
61825
- } catch {
61826
- return false;
61883
+ return getProcessLiveness(pid) === "running";
61884
+ }
61885
+ function getProcessLiveness(pid, dependencies = {}) {
61886
+ const resolvedDependencies = {
61887
+ ...DEFAULT_PROCESS_LIVENESS_DEPENDENCIES,
61888
+ ...dependencies
61889
+ };
61890
+ if (!resolvedDependencies.signalCheck(pid)) {
61891
+ return "missing";
61827
61892
  }
61893
+ if (resolvedDependencies.platform === "win32") {
61894
+ return "running";
61895
+ }
61896
+ const linuxState = resolvedDependencies.readLinuxProcStat(pid);
61897
+ if (linuxState !== "unknown") {
61898
+ return linuxState;
61899
+ }
61900
+ const psState = resolvedDependencies.readPsStat(pid);
61901
+ if (psState !== "unknown") {
61902
+ return psState;
61903
+ }
61904
+ return "running";
61828
61905
  }
61829
61906
  async function ensureConfigFile(configPath, options = {}) {
61830
61907
  await ensureClisbotWrapper();
@@ -61907,18 +61984,27 @@ async function startDetachedRuntime(params) {
61907
61984
  logPath
61908
61985
  };
61909
61986
  }
61910
- async function stopDetachedRuntime(params) {
61987
+ async function stopDetachedRuntime(params, dependencies = {}) {
61911
61988
  const pidPath = resolvePidPath(params.pidPath);
61912
61989
  const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath);
61913
61990
  const existingPid = await readRuntimePid(pidPath);
61914
61991
  let stopped = false;
61915
- if (existingPid && isProcessRunning(existingPid)) {
61916
- kill(existingPid, "SIGTERM");
61917
- const exited = await waitForProcessExit(existingPid, STOP_WAIT_TIMEOUT_MS);
61992
+ const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
61993
+ const sendSignal = dependencies.sendSignal ?? kill;
61994
+ const sleepFn = dependencies.sleep ?? sleep;
61995
+ const existingLiveness = existingPid ? processLiveness(existingPid) : "missing";
61996
+ if (existingPid && existingLiveness === "running") {
61997
+ sendSignal(existingPid, "SIGTERM");
61998
+ const exited = await waitForProcessExit(existingPid, STOP_WAIT_TIMEOUT_MS, {
61999
+ processLiveness,
62000
+ sleep: sleepFn
62001
+ });
61918
62002
  if (!exited) {
61919
62003
  throw new Error(`clisbot did not stop within ${STOP_WAIT_TIMEOUT_MS}ms`);
61920
62004
  }
61921
62005
  stopped = true;
62006
+ } else if (existingPid && existingLiveness === "zombie") {
62007
+ stopped = true;
61922
62008
  }
61923
62009
  rmSync2(pidPath, { force: true });
61924
62010
  removeRuntimeCredentials(runtimeCredentialsPath);
@@ -61960,9 +62046,10 @@ async function getRuntimeStatus(params = {}) {
61960
62046
  const pidPath = resolvePidPath(params.pidPath);
61961
62047
  const logPath = resolveLogPath(params.logPath);
61962
62048
  const pid = await readRuntimePid(pidPath);
62049
+ const liveness = pid ? getProcessLiveness(pid) : "missing";
61963
62050
  return {
61964
- running: Boolean(pid && isProcessRunning(pid)),
61965
- pid: pid && isProcessRunning(pid) ? pid : undefined,
62051
+ running: liveness === "running",
62052
+ pid: liveness === "running" && pid ? pid : undefined,
61966
62053
  configPath,
61967
62054
  pidPath,
61968
62055
  logPath,
@@ -62032,15 +62119,17 @@ async function waitForStart(params) {
62032
62119
  childPid: params.childPid
62033
62120
  };
62034
62121
  }
62035
- async function waitForProcessExit(pid, timeoutMs) {
62122
+ async function waitForProcessExit(pid, timeoutMs, dependencies = {}) {
62123
+ const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
62124
+ const sleepFn = dependencies.sleep ?? sleep;
62036
62125
  const deadline = Date.now() + timeoutMs;
62037
62126
  while (Date.now() < deadline) {
62038
- if (!isProcessRunning(pid)) {
62127
+ if (processLiveness(pid) !== "running") {
62039
62128
  return true;
62040
62129
  }
62041
- await sleep(PROCESS_POLL_INTERVAL_MS);
62130
+ await sleepFn(PROCESS_POLL_INTERVAL_MS);
62042
62131
  }
62043
- return !isProcessRunning(pid);
62132
+ return processLiveness(pid) !== "running";
62044
62133
  }
62045
62134
  async function cleanupFailedStartChild(result) {
62046
62135
  if (result.reason === "child-exited-before-pid") {
@@ -62087,6 +62176,66 @@ async function resolveTmuxSocketPath(configPath) {
62087
62176
  }
62088
62177
  return getDefaultTmuxSocketPath();
62089
62178
  }
62179
+ function signalCheckProcess(pid) {
62180
+ try {
62181
+ kill(pid, 0);
62182
+ return true;
62183
+ } catch {
62184
+ return false;
62185
+ }
62186
+ }
62187
+ function readLinuxProcStatLiveness(pid) {
62188
+ if (process.platform !== "linux") {
62189
+ return "unknown";
62190
+ }
62191
+ try {
62192
+ const raw = readFileSync3(`/proc/${pid}/stat`, "utf8");
62193
+ const state = extractLinuxProcState(raw);
62194
+ if (!state) {
62195
+ return "unknown";
62196
+ }
62197
+ return state.includes("Z") ? "zombie" : "running";
62198
+ } catch (error) {
62199
+ const code = error.code;
62200
+ if (code === "ENOENT") {
62201
+ return "missing";
62202
+ }
62203
+ return "unknown";
62204
+ }
62205
+ }
62206
+ function readPsStatLiveness(pid) {
62207
+ try {
62208
+ const raw = execFileSync("ps", ["-o", "stat=", "-p", String(pid)], {
62209
+ encoding: "utf8",
62210
+ stdio: ["ignore", "pipe", "ignore"]
62211
+ }).trim();
62212
+ if (!raw) {
62213
+ return "missing";
62214
+ }
62215
+ return raw.includes("Z") ? "zombie" : "running";
62216
+ } catch (error) {
62217
+ const commandError = error;
62218
+ if (commandError.code === "ENOENT") {
62219
+ return "unknown";
62220
+ }
62221
+ if (commandError.status === 1) {
62222
+ return "missing";
62223
+ }
62224
+ return "unknown";
62225
+ }
62226
+ }
62227
+ function extractLinuxProcState(raw) {
62228
+ const closingParenIndex = raw.lastIndexOf(")");
62229
+ if (closingParenIndex < 0) {
62230
+ return null;
62231
+ }
62232
+ const remainder = raw.slice(closingParenIndex + 1).trim();
62233
+ if (!remainder) {
62234
+ return null;
62235
+ }
62236
+ const [state] = remainder.split(/\s+/, 1);
62237
+ return state?.trim() || null;
62238
+ }
62090
62239
 
62091
62240
  // src/control/accounts-cli.ts
62092
62241
  function getEditableConfigPath2() {
@@ -62111,7 +62260,7 @@ function readRuntimeCredentialDocument() {
62111
62260
  if (!existsSync7(path2)) {
62112
62261
  return {};
62113
62262
  }
62114
- const text = readFileSync3(path2, "utf8").trim();
62263
+ const text = readFileSync4(path2, "utf8").trim();
62115
62264
  return text ? JSON.parse(text) : {};
62116
62265
  }
62117
62266
  async function waitForReloadResult(configPath, deps, timeoutMs = 12000) {
@@ -63380,6 +63529,9 @@ async function runChannelsCli(args) {
63380
63529
  }
63381
63530
 
63382
63531
  // src/control/channel-bootstrap-flags.ts
63532
+ function isLiteralToken(token) {
63533
+ return token?.kind === "mem";
63534
+ }
63383
63535
  function parseBotType(rawValue) {
63384
63536
  const value = rawValue.trim().toLowerCase();
63385
63537
  if (value === "personal" || value === "personal-assistant") {
@@ -63434,7 +63586,6 @@ function validateTelegramAccount(account) {
63434
63586
  function parseBootstrapFlags(args) {
63435
63587
  const slackAccounts = [];
63436
63588
  const telegramAccounts = [];
63437
- const literalWarnings = [];
63438
63589
  let currentSlackAccountId;
63439
63590
  let currentTelegramAccountId;
63440
63591
  let cliTool;
@@ -63486,9 +63637,6 @@ function parseBootstrapFlags(args) {
63486
63637
  const token = parseTokenInput(parseOptionValue4(args, arg, index));
63487
63638
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63488
63639
  account.appToken = token;
63489
- if (token.kind === "mem") {
63490
- literalWarnings.push(`Slack account ${account.accountId} uses a literal CLI token; shell history or process inspection may expose it.`);
63491
- }
63492
63640
  sawCredentialFlags = true;
63493
63641
  sawSlackFlags = true;
63494
63642
  index += 1;
@@ -63498,9 +63646,6 @@ function parseBootstrapFlags(args) {
63498
63646
  const token = parseTokenInput(parseOptionValue4(args, arg, index));
63499
63647
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63500
63648
  account.botToken = token;
63501
- if (token.kind === "mem") {
63502
- literalWarnings.push(`Slack account ${account.accountId} uses a literal CLI token; shell history or process inspection may expose it.`);
63503
- }
63504
63649
  sawCredentialFlags = true;
63505
63650
  sawSlackFlags = true;
63506
63651
  index += 1;
@@ -63510,9 +63655,6 @@ function parseBootstrapFlags(args) {
63510
63655
  const token = parseTokenInput(parseOptionValue4(args, arg, index));
63511
63656
  const account = getOrCreateTelegramAccount(telegramAccounts, currentTelegramAccountId ?? "default");
63512
63657
  account.botToken = token;
63513
- if (token.kind === "mem") {
63514
- literalWarnings.push(`Telegram account ${account.accountId} uses a literal CLI token; shell history or process inspection may expose it.`);
63515
- }
63516
63658
  sawCredentialFlags = true;
63517
63659
  sawTelegramFlags = true;
63518
63660
  index += 1;
@@ -63535,9 +63677,12 @@ function parseBootstrapFlags(args) {
63535
63677
  sawCredentialFlags,
63536
63678
  sawSlackFlags,
63537
63679
  sawTelegramFlags,
63538
- literalWarnings
63680
+ literalWarnings: []
63539
63681
  };
63540
63682
  }
63683
+ function hasLiteralBootstrapCredentials(flags) {
63684
+ return flags.slackAccounts.some((account) => isLiteralToken(account.appToken) || isLiteralToken(account.botToken)) || flags.telegramAccounts.some((account) => isLiteralToken(account.botToken));
63685
+ }
63541
63686
 
63542
63687
  // src/agents/session-state.ts
63543
63688
  class AgentSessionState {
@@ -64393,6 +64538,12 @@ function looksLikeClaudeSnapshot(lines) {
64393
64538
  return trimmed.includes("Claude Code v") || trimmed.includes("Welcome back!") || trimmed.includes("Tips for getting started") || trimmed.startsWith("❯") || trimmed.startsWith("⏺");
64394
64539
  });
64395
64540
  }
64541
+ function looksLikeGeminiSnapshot(lines) {
64542
+ return lines.some((line) => {
64543
+ const trimmed = line.trim();
64544
+ return trimmed.includes("Gemini CLI v") || trimmed.includes("Signed in with Google") || trimmed.includes("YOLO Ctrl+Y") || trimmed.includes("Type your message or @path/to/file") || trimmed.includes("workspace (/directory)");
64545
+ });
64546
+ }
64396
64547
  function isProgressLine(line) {
64397
64548
  const trimmed = line.trim();
64398
64549
  const normalized = trimmed.replace(/^(?::eight_spoked_asterisk:|[✽✶])\s+/, "");
@@ -64491,6 +64642,9 @@ function dropClaudePromptBlocks(lines) {
64491
64642
  }
64492
64643
  return filtered;
64493
64644
  }
64645
+ function dropGeminiPromptBlocks(lines) {
64646
+ return dropPromptBlocks(lines, /^\s*>\s/);
64647
+ }
64494
64648
  function isInterruptStatusLine(line) {
64495
64649
  const trimmed = line.trim();
64496
64650
  if (!trimmed) {
@@ -64512,6 +64666,13 @@ function shouldDropClaudeChromeLine(line) {
64512
64666
  }
64513
64667
  return trimmed.includes("Claude Code v") || trimmed.includes("Welcome back!") || trimmed.includes("Tips for getting started") || trimmed.includes("Ask Claude to create a new app or clone a repository") || trimmed.includes("Recent activity") || trimmed.includes("No recent activity") || trimmed.includes("API Usage Billing") || trimmed.includes("shift+tab to cycle") || trimmed.includes("ctrl+o to expand") || trimmed.includes("ctrl+b ctrl+b") || trimmed.includes("run in background") || /^~\/\.clisbot\/(?:workspace\/)?[a-z0-9._/-]+$/i.test(trimmed) || trimmed.includes("| claude |") || /^(?:[✻*]\s*)?(?:Worked|Cooked) for \d+s$/i.test(trimmed) || trimmed.startsWith("⏵⏵") || trimmed.startsWith("❯") || isProgressLine(trimmed) || /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/.test(trimmed) || /^[╭╰│]/.test(trimmed) || /^─+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+.+$/.test(trimmed);
64514
64668
  }
64669
+ function shouldDropGeminiChromeLine(line) {
64670
+ const trimmed = line.trim();
64671
+ if (!trimmed) {
64672
+ return false;
64673
+ }
64674
+ return trimmed.includes("Gemini CLI v") || trimmed.includes("Signed in with Google") || trimmed.includes("Plan:") || /^[▝▜▄▗▟▀ ]+$/.test(trimmed) || trimmed.includes("We're making changes to Gemini CLI") || trimmed.includes("What's Changing:") || trimmed.includes("How it affects you:") || trimmed.includes("Read more: https://goo.gle/geminicli-updates") || trimmed.includes("Skipping project agents due to untrusted folder.") || trimmed.includes("Do you trust the files in this folder?") || trimmed.includes("Trusting a folder allows Gemini CLI to load its local configurations") || trimmed === "1. Trust folder (default)" || trimmed === "2. Trust parent folder (workspaces)" || trimmed === "3. Don't trust" || trimmed.includes("Tips for getting started") || /^Create GEMINI\.md files to customize your interactions$/i.test(trimmed) || /^\/help for more information$/i.test(trimmed) || /^Ask coding questions, edit code or run commands$/i.test(trimmed) || /^Be specific for the best results$/i.test(trimmed) || trimmed.includes("? for shortcuts") || trimmed.includes("YOLO Ctrl+Y") || trimmed.includes("Type your message or @path/to/file") || trimmed.includes("workspace (/directory)") || /^~\/.+\s+\S+\s+no sandbox\s+\S+/i.test(trimmed) || /^Thinking\.\.\. \(esc to cancel,\s*\d+s\)$/i.test(trimmed) || /^[╭╰│]/.test(trimmed) || /^[-▀▄]{10,}$/.test(trimmed) || /^─+$/.test(trimmed);
64675
+ }
64515
64676
  function normalizeBoundaryLine(line) {
64516
64677
  return line.trim().replace(/^(?::eight_spoked_asterisk:|[-*•◦·✽✶])\s+/, "");
64517
64678
  }
@@ -64548,7 +64709,8 @@ function cleanInteractionSnapshot(raw) {
64548
64709
  const lines = splitNormalizedLines(raw);
64549
64710
  const isCodex = looksLikeCodexSnapshot(lines);
64550
64711
  const isClaude = looksLikeClaudeSnapshot(lines);
64551
- const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : lines;
64712
+ const isGemini = looksLikeGeminiSnapshot(lines);
64713
+ const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : isGemini ? dropGeminiPromptBlocks(lines) : lines;
64552
64714
  const filtered = promptStripped.filter((line) => {
64553
64715
  if (shouldDropDeliveryReportLine(line)) {
64554
64716
  return false;
@@ -64559,9 +64721,12 @@ function cleanInteractionSnapshot(raw) {
64559
64721
  if (isClaude && shouldDropClaudeChromeLine(line)) {
64560
64722
  return false;
64561
64723
  }
64724
+ if (isGemini && shouldDropGeminiChromeLine(line)) {
64725
+ return false;
64726
+ }
64562
64727
  return true;
64563
64728
  });
64564
- const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : filtered;
64729
+ const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : isGemini ? filtered.map((line) => line.replace(/^\s*>\s*/, "")) : filtered;
64565
64730
  const unwrapped = unwrapSoftWrappedLines(normalized);
64566
64731
  return collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
64567
64732
  `));
@@ -64747,6 +64912,7 @@ function extractFinalAnswer(raw) {
64747
64912
  const rawLines = splitNormalizedLines(raw);
64748
64913
  const isCodex = looksLikeCodexSnapshot(rawLines);
64749
64914
  const isClaude = looksLikeClaudeSnapshot(rawLines);
64915
+ const isGemini = looksLikeGeminiSnapshot(rawLines);
64750
64916
  const cleaned = cleanInteractionSnapshot(raw);
64751
64917
  if (!cleaned) {
64752
64918
  return "";
@@ -64773,14 +64939,14 @@ function extractFinalAnswer(raw) {
64773
64939
  if (answerBlocks.length > 1 && answerBlocks.every(isShortAtomicAnswerBlock)) {
64774
64940
  const lastAnswer = answerBlocks.at(-1)?.trim() ?? "";
64775
64941
  if (lastAnswer) {
64776
- return isCodex || isClaude ? stripSingleLineAssistantEnvelope(lastAnswer) : lastAnswer;
64942
+ return isCodex || isClaude || isGemini ? stripSingleLineAssistantEnvelope(lastAnswer) : lastAnswer;
64777
64943
  }
64778
64944
  }
64779
64945
  const answer = answerBlocks.join(`
64780
64946
 
64781
64947
  `).trim();
64782
64948
  const extracted = answer || cleaned;
64783
- if (isCodex || isClaude) {
64949
+ if (isCodex || isClaude || isGemini) {
64784
64950
  return stripSingleLineAssistantEnvelope(extracted);
64785
64951
  }
64786
64952
  return extracted;
@@ -64899,14 +65065,23 @@ function logLatencyDebug(stage, context = {}, details = {}) {
64899
65065
  // src/runners/tmux/session-handshake.ts
64900
65066
  var TRUST_PROMPT_POLL_INTERVAL_MS = 250;
64901
65067
  var TRUST_PROMPT_MAX_WAIT_MS = 1e4;
64902
- var TRUST_PROMPT_SETTLE_DELAY_MS = 1500;
64903
65068
  var SESSION_BOOTSTRAP_POLL_INTERVAL_MS = 100;
65069
+ var PASTE_SETTLE_POLL_INTERVAL_MS = 40;
65070
+ var PASTE_SETTLE_QUIET_WINDOW_MS = 60;
65071
+ var PASTE_SETTLE_MULTILINE_MAX_WAIT_MS = 800;
65072
+ var PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS = 80;
64904
65073
  var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
64905
65074
  var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
64906
65075
  async function submitTmuxSessionInput(params) {
65076
+ const prePasteState = await params.tmux.getPaneState(params.sessionName);
64907
65077
  await params.tmux.sendLiteral(params.sessionName, params.text);
64908
- await sleep(params.promptSubmitDelayMs);
64909
- const preSubmitState = await params.tmux.getPaneState(params.sessionName);
65078
+ const preSubmitState = await waitForPanePasteSettlement({
65079
+ tmux: params.tmux,
65080
+ sessionName: params.sessionName,
65081
+ baseline: prePasteState,
65082
+ text: params.text,
65083
+ minDelayMs: params.promptSubmitDelayMs
65084
+ });
64910
65085
  await params.tmux.sendKey(params.sessionName, "Enter");
64911
65086
  if (await waitForPaneSubmitConfirmation({
64912
65087
  tmux: params.tmux,
@@ -64932,7 +65107,6 @@ async function submitTmuxSessionInput(params) {
64932
65107
  throw new Error("tmux submit was not confirmed after Enter. The pane state did not change, so clisbot did not treat the prompt as truthfully submitted.");
64933
65108
  }
64934
65109
  async function captureTmuxSessionIdentity(params) {
64935
- let deadline = Date.now() + params.timeoutMs;
64936
65110
  await submitTmuxSessionInput({
64937
65111
  tmux: params.tmux,
64938
65112
  sessionName: params.sessionName,
@@ -64940,11 +65114,16 @@ async function captureTmuxSessionIdentity(params) {
64940
65114
  promptSubmitDelayMs: params.promptSubmitDelayMs,
64941
65115
  timingContext: undefined
64942
65116
  });
65117
+ let deadline = Date.now() + params.timeoutMs;
64943
65118
  while (Date.now() < deadline) {
64944
65119
  await sleep(params.pollIntervalMs);
64945
65120
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
64946
65121
  if (hasTrustPrompt(snapshot)) {
64947
- await dismissTrustPrompt(params.tmux, params.sessionName);
65122
+ await dismissTrustPrompt({
65123
+ tmux: params.tmux,
65124
+ sessionName: params.sessionName,
65125
+ captureLines: params.captureLines
65126
+ });
64948
65127
  deadline = Date.now() + params.timeoutMs;
64949
65128
  await submitTmuxSessionInput({
64950
65129
  tmux: params.tmux,
@@ -64973,11 +65152,21 @@ async function dismissTmuxTrustPromptIfPresent(params) {
64973
65152
  if (!hasTrustPrompt(snapshot)) {
64974
65153
  return;
64975
65154
  }
64976
- await dismissTrustPrompt(params.tmux, params.sessionName);
65155
+ await dismissTrustPrompt({
65156
+ tmux: params.tmux,
65157
+ sessionName: params.sessionName,
65158
+ captureLines: params.captureLines
65159
+ });
64977
65160
  }
64978
65161
  }
64979
65162
  async function waitForTmuxSessionBootstrap(params) {
64980
65163
  const deadline = Date.now() + Math.max(params.startupDelayMs, SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65164
+ const readyRegex = params.readyPattern ? new RegExp(params.readyPattern, "i") : null;
65165
+ const blockerPatterns = (params.blockers ?? []).map((entry) => ({
65166
+ regex: new RegExp(entry.pattern, "i"),
65167
+ message: entry.message
65168
+ }));
65169
+ let lastSnapshot = "";
64981
65170
  while (Date.now() <= deadline) {
64982
65171
  let snapshot = "";
64983
65172
  try {
@@ -64985,23 +65174,60 @@ async function waitForTmuxSessionBootstrap(params) {
64985
65174
  } catch (error) {
64986
65175
  const message = error instanceof Error ? error.message : String(error);
64987
65176
  if (message.includes("can't find session:") || message.includes("no server running on ")) {
64988
- return "";
65177
+ return {
65178
+ status: "timeout",
65179
+ snapshot: lastSnapshot
65180
+ };
64989
65181
  }
64990
65182
  throw error;
64991
65183
  }
64992
65184
  if (snapshot) {
64993
- return snapshot;
65185
+ lastSnapshot = snapshot;
65186
+ if (params.trustWorkspace && hasTrustPrompt(snapshot)) {
65187
+ await dismissTrustPrompt({
65188
+ tmux: params.tmux,
65189
+ sessionName: params.sessionName,
65190
+ captureLines: params.captureLines
65191
+ });
65192
+ await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65193
+ continue;
65194
+ }
65195
+ for (const blocker of blockerPatterns) {
65196
+ if (blocker.regex.test(snapshot)) {
65197
+ return {
65198
+ status: "blocked",
65199
+ snapshot,
65200
+ message: blocker.message
65201
+ };
65202
+ }
65203
+ }
65204
+ if (readyRegex && !readyRegex.test(snapshot)) {
65205
+ await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65206
+ continue;
65207
+ }
65208
+ return {
65209
+ status: "ready",
65210
+ snapshot
65211
+ };
64994
65212
  }
64995
65213
  await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
64996
65214
  }
64997
- return "";
64998
- }
64999
- function hasTrustPrompt(snapshot) {
65000
- return snapshot.includes("Do you trust the contents of this directory?") || snapshot.includes("Press enter to continue");
65215
+ return {
65216
+ status: "timeout",
65217
+ snapshot: lastSnapshot
65218
+ };
65001
65219
  }
65002
- async function dismissTrustPrompt(tmux, sessionName) {
65003
- await tmux.sendKey(sessionName, "Enter");
65004
- await sleep(TRUST_PROMPT_SETTLE_DELAY_MS);
65220
+ async function dismissTrustPrompt(params) {
65221
+ await params.tmux.sendKey(params.sessionName, "Enter");
65222
+ const deadline = Date.now() + TRUST_PROMPT_MAX_WAIT_MS;
65223
+ while (Date.now() <= deadline) {
65224
+ await sleep(TRUST_PROMPT_POLL_INTERVAL_MS);
65225
+ const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65226
+ if (!snapshot || hasTrustPrompt(snapshot)) {
65227
+ continue;
65228
+ }
65229
+ return;
65230
+ }
65005
65231
  }
65006
65232
  async function waitForPaneSubmitConfirmation(params) {
65007
65233
  const deadline = Date.now() + SUBMIT_CONFIRM_MAX_WAIT_MS;
@@ -65017,9 +65243,50 @@ async function waitForPaneSubmitConfirmation(params) {
65017
65243
  await sleep(Math.min(SUBMIT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
65018
65244
  }
65019
65245
  }
65246
+ async function waitForPanePasteSettlement(params) {
65247
+ await sleep(params.minDelayMs);
65248
+ let currentState = await params.tmux.getPaneState(params.sessionName);
65249
+ let sawChange = hasPaneStateChanged(params.baseline, currentState);
65250
+ let lastChangeAt = Date.now();
65251
+ const deadline = Date.now() + (shouldWaitForVisiblePaste(params.text) ? PASTE_SETTLE_MULTILINE_MAX_WAIT_MS : PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS);
65252
+ while (true) {
65253
+ if (sawChange && Date.now() - lastChangeAt >= PASTE_SETTLE_QUIET_WINDOW_MS) {
65254
+ return currentState;
65255
+ }
65256
+ const remainingMs = deadline - Date.now();
65257
+ if (remainingMs <= 0) {
65258
+ return currentState;
65259
+ }
65260
+ await sleep(Math.min(PASTE_SETTLE_POLL_INTERVAL_MS, remainingMs));
65261
+ const nextState = await params.tmux.getPaneState(params.sessionName);
65262
+ if (!arePaneStatesEqual(currentState, nextState)) {
65263
+ currentState = nextState;
65264
+ if (hasPaneStateChanged(params.baseline, currentState)) {
65265
+ sawChange = true;
65266
+ }
65267
+ lastChangeAt = Date.now();
65268
+ }
65269
+ }
65270
+ }
65020
65271
  function hasPaneStateChanged(left, right) {
65021
65272
  return left.cursorX !== right.cursorX || left.cursorY !== right.cursorY || left.historySize !== right.historySize;
65022
65273
  }
65274
+ function arePaneStatesEqual(left, right) {
65275
+ return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
65276
+ }
65277
+ function looksLikeClaudeTrustPrompt(snapshot) {
65278
+ return snapshot.includes("Quick safety check:") && snapshot.includes("Yes, I trust this folder") || snapshot.includes("Enter to confirm · Esc to cancel");
65279
+ }
65280
+ function looksLikeGeminiTrustPrompt(snapshot) {
65281
+ return snapshot.includes("Skipping project agents due to untrusted folder.") && snapshot.includes("Do you trust the files in this folder?") || snapshot.includes("Trusting a folder allows Gemini CLI to load its local configurations") && snapshot.includes("Trust folder (default)");
65282
+ }
65283
+ function hasTrustPrompt(snapshot) {
65284
+ return snapshot.includes("Do you trust the contents of this directory?") || snapshot.includes("Press enter to continue") || looksLikeClaudeTrustPrompt(snapshot) || looksLikeGeminiTrustPrompt(snapshot);
65285
+ }
65286
+ function shouldWaitForVisiblePaste(text) {
65287
+ return text.includes(`
65288
+ `);
65289
+ }
65023
65290
 
65024
65291
  // src/runners/tmux/shell-command.ts
65025
65292
  var BASH_WINDOW_NAME = "bash";
@@ -65147,6 +65414,11 @@ function buildRunnerLaunchCommand(command, args) {
65147
65414
  ];
65148
65415
  return `${exports.join("; ")}; exec ${buildCommandString(command, args)}`;
65149
65416
  }
65417
+ function summarizeSnapshot(snapshot) {
65418
+ const compact = snapshot.split(`
65419
+ `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
65420
+ return compact ? ` Last visible pane: ${compact}` : "";
65421
+ }
65150
65422
  function isTmuxDuplicateSessionError(error) {
65151
65423
  const message = error instanceof Error ? error.message : String(error);
65152
65424
  return TMUX_DUPLICATE_SESSION_PATTERN.test(message);
@@ -65239,6 +65511,10 @@ class RunnerSessionService {
65239
65511
  allowFreshRetry: options.nextAllowFreshRetry
65240
65512
  });
65241
65513
  }
65514
+ async abortUnreadySession(resolved, reason, snapshot) {
65515
+ await this.tmux.killSession(resolved.sessionName);
65516
+ throw new Error(`${reason}${summarizeSnapshot(snapshot)}`);
65517
+ }
65242
65518
  async runSessionCleanup() {
65243
65519
  if (this.cleanupInFlight) {
65244
65520
  return;
@@ -65328,11 +65604,14 @@ class RunnerSessionService {
65328
65604
  resumingExistingSession,
65329
65605
  hasStoredSessionId: Boolean(existing?.sessionId)
65330
65606
  });
65331
- await waitForTmuxSessionBootstrap({
65607
+ const bootstrapResult = await waitForTmuxSessionBootstrap({
65332
65608
  tmux: this.tmux,
65333
65609
  sessionName: resolved.sessionName,
65334
65610
  captureLines: resolved.stream.captureLines,
65335
- startupDelayMs: resolved.runner.startupDelayMs
65611
+ startupDelayMs: resolved.runner.startupDelayMs,
65612
+ trustWorkspace: resolved.runner.trustWorkspace,
65613
+ readyPattern: resolved.runner.startupReadyPattern,
65614
+ blockers: resolved.runner.startupBlockers
65336
65615
  });
65337
65616
  const sessionStillExists = await this.tmux.hasSession(resolved.sessionName);
65338
65617
  if (!sessionStillExists) {
@@ -65347,6 +65626,12 @@ class RunnerSessionService {
65347
65626
  }
65348
65627
  throw new Error(`Runner session "${resolved.sessionName}" disappeared during startup.`);
65349
65628
  }
65629
+ if (bootstrapResult.status === "blocked") {
65630
+ await this.abortUnreadySession(resolved, bootstrapResult.message, bootstrapResult.snapshot);
65631
+ }
65632
+ if (bootstrapResult.status === "timeout" && resolved.runner.startupReadyPattern) {
65633
+ await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
65634
+ }
65350
65635
  try {
65351
65636
  await this.finalizeSessionStartup(target, resolved, {
65352
65637
  startupSessionId,
@@ -65710,6 +65995,7 @@ class ActiveRunManager {
65710
65995
  runnerSessions;
65711
65996
  resolveTarget;
65712
65997
  activeRuns = new Map;
65998
+ stopping = false;
65713
65999
  constructor(tmux, sessionState, runnerSessions, resolveTarget) {
65714
66000
  this.tmux = tmux;
65715
66001
  this.sessionState = sessionState;
@@ -65759,6 +66045,9 @@ class ActiveRunManager {
65759
66045
  }
65760
66046
  }
65761
66047
  async executePrompt(target, prompt, observer, options = {}) {
66048
+ if (this.stopping) {
66049
+ throw new Error("Runtime is stopping and cannot accept a new prompt.");
66050
+ }
65762
66051
  const existingActiveRun = this.activeRuns.get(target.sessionKey);
65763
66052
  if (existingActiveRun) {
65764
66053
  throw new ActiveRunInProgressError(existingActiveRun.latestUpdate);
@@ -65804,6 +66093,9 @@ class ActiveRunManager {
65804
66093
  const startedAt = Date.now();
65805
66094
  const run = this.activeRuns.get(provisionalResolved.sessionKey);
65806
66095
  if (!run) {
66096
+ if (this.stopping) {
66097
+ throw new Error("Runtime stopped before the active run finished startup.");
66098
+ }
65807
66099
  throw new Error(`Active run disappeared during startup for ${provisionalResolved.sessionKey}.`);
65808
66100
  }
65809
66101
  run.resolved = resolved;
@@ -65880,6 +66172,16 @@ class ActiveRunManager {
65880
66172
  hasActiveRun(target) {
65881
66173
  return this.activeRuns.has(target.sessionKey);
65882
66174
  }
66175
+ async stop() {
66176
+ this.stopping = true;
66177
+ const activeRuns = [...this.activeRuns.values()];
66178
+ for (const run of activeRuns) {
66179
+ await this.sessionState.setSessionRuntime(run.resolved, {
66180
+ state: "idle"
66181
+ });
66182
+ }
66183
+ this.activeRuns.clear();
66184
+ }
65883
66185
  buildDetachedNote(resolved) {
65884
66186
  return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot will keep monitoring it and will post the final result here when it completes. Use \`/attach\` to resume live updates, \`/watch every 30s\` for interval updates, or \`/stop\` to interrupt it.`;
65885
66187
  }
@@ -66060,6 +66362,7 @@ class AgentService {
66060
66362
  sessionState;
66061
66363
  runnerSessions;
66062
66364
  activeRuns;
66365
+ stopping = false;
66063
66366
  cleanupTimer;
66064
66367
  loopTimers = new Set;
66065
66368
  intervalLoops = new Map;
@@ -66095,6 +66398,7 @@ class AgentService {
66095
66398
  }, cleanup.intervalMinutes * 60000);
66096
66399
  }
66097
66400
  async stop() {
66401
+ this.stopping = true;
66098
66402
  if (this.cleanupTimer) {
66099
66403
  clearInterval(this.cleanupTimer);
66100
66404
  this.cleanupTimer = undefined;
@@ -66109,6 +66413,7 @@ class AgentService {
66109
66413
  clearTimeout(timer);
66110
66414
  }
66111
66415
  this.loopTimers.clear();
66416
+ await this.activeRuns.stop();
66112
66417
  }
66113
66418
  async cleanupStaleSessions() {
66114
66419
  await this.runnerSessions.runSessionCleanup();
@@ -66420,6 +66725,9 @@ class AgentService {
66420
66725
  }
66421
66726
  });
66422
66727
  result.catch((error) => {
66728
+ if (this.shouldSuppressLoopShutdownError(error)) {
66729
+ return;
66730
+ }
66423
66731
  console.error("loop execution failed", error);
66424
66732
  });
66425
66733
  if (attemptedRuns >= managed.loop.maxRuns) {
@@ -66445,6 +66753,9 @@ class AgentService {
66445
66753
  }
66446
66754
  current.timer = undefined;
66447
66755
  this.runIntervalLoopIteration(loopId).catch((error) => {
66756
+ if (this.shouldSuppressLoopShutdownError(error)) {
66757
+ return;
66758
+ }
66448
66759
  console.error("loop execution failed", error);
66449
66760
  });
66450
66761
  }, delayMs);
@@ -66459,6 +66770,13 @@ class AgentService {
66459
66770
  managed.loop = nextLoopState;
66460
66771
  return true;
66461
66772
  }
66773
+ shouldSuppressLoopShutdownError(error) {
66774
+ if (!this.stopping) {
66775
+ return false;
66776
+ }
66777
+ const message = error instanceof Error ? error.message : String(error);
66778
+ return /Runtime stopped before the active run finished startup|Runtime is stopping and cannot accept a new prompt/i.test(message);
66779
+ }
66462
66780
  computeNextManagedLoopRunAtMs(loop, nowMs) {
66463
66781
  if (loop.kind === "calendar") {
66464
66782
  return computeNextCalendarLoopRunAtMs({
@@ -67155,12 +67473,14 @@ function buildChannelObserverId(identity) {
67155
67473
  }
67156
67474
  function buildSteeringMessage(text) {
67157
67475
  return [
67476
+ "<system>",
67477
+ "A new user message arrived while you were still working.",
67478
+ "Adjust your current work if needed and continue.",
67479
+ "</system>",
67158
67480
  "",
67159
- "[clisbot steering message]",
67160
- "A new user message arrived while you were already processing the current run.",
67161
- "Adjust the current work if needed and continue.",
67162
- "",
67163
- text
67481
+ "<user>",
67482
+ text,
67483
+ "</user>"
67164
67484
  ].join(`
67165
67485
  `);
67166
67486
  }
@@ -68009,14 +68329,14 @@ function renderAgentPromptInstruction(params) {
68009
68329
  `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
68010
68330
  "",
68011
68331
  "You are operating inside clisbot.",
68012
- messageToolMode ? "channel auto-delivery is disabled for this conversation; send user-facing progress updates and the final response yourself with the reply command" : "channel auto-delivery remains enabled for this conversation; do not send user-facing progress updates or the final response with clisbot message send"
68332
+ messageToolMode ? "To send a user-visible progress update or final reply, use the following CLI command:" : "channel auto-delivery remains enabled for this conversation; do not send user-facing progress updates or the final response with clisbot message send"
68013
68333
  ];
68014
68334
  if (messageToolMode) {
68015
68335
  const replyCommand = buildReplyCommand({
68016
68336
  command: getClisbotPromptCommand(),
68017
68337
  identity: params.identity
68018
68338
  });
68019
- lines.push("Use the exact command below when you need to send progress updates, media attachments, or the final response back to the user.", "reply command:", replyCommand, `progress updates: at most ${params.config.maxProgressMessages}`, params.config.requireFinalResponse ? "final response: send exactly 1 final user-facing response" : "final response: optional", "keep progress updates short and meaningful", "do not send progress updates for trivial internal steps");
68339
+ lines.push(replyCommand, "When replying to the user:", "- put the user-facing message inside the --message body of that command", "- use that command to send progress updates and the final reply back to the conversation", `- send at most ${params.config.maxProgressMessages} progress updates`, params.config.requireFinalResponse ? "- send exactly 1 final user-facing response" : "- final response is optional", "- keep progress updates short and meaningful", "- do not send progress updates for trivial internal steps");
68020
68340
  }
68021
68341
  return lines.join(`
68022
68342
  `);
@@ -69238,6 +69558,7 @@ class SlackSocketService {
69238
69558
  timestamp: messageTs
69239
69559
  };
69240
69560
  let responseChunks = [];
69561
+ const cliTool = getAgentEntry(this.loadedConfig, params.route.agentId)?.cliTool;
69241
69562
  const agentPromptText = buildAgentPromptText({
69242
69563
  text,
69243
69564
  identity: {
@@ -69248,6 +69569,7 @@ class SlackSocketService {
69248
69569
  threadTs
69249
69570
  },
69250
69571
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69572
+ cliTool,
69251
69573
  responseMode: params.route.responseMode
69252
69574
  });
69253
69575
  const timingContext = {
@@ -69295,6 +69617,7 @@ class SlackSocketService {
69295
69617
  threadTs
69296
69618
  },
69297
69619
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69620
+ cliTool,
69298
69621
  responseMode: params.route.responseMode
69299
69622
  }),
69300
69623
  route: params.route,
@@ -70644,10 +70967,12 @@ class TelegramPollingService {
70644
70967
  chatName: message.chat.title?.trim() || undefined,
70645
70968
  topicId: routeInfo.topicId != null ? String(routeInfo.topicId) : undefined
70646
70969
  };
70970
+ const cliTool = getAgentEntry(this.loadedConfig, routeInfo.route.agentId)?.cliTool;
70647
70971
  const agentPromptText = buildAgentPromptText({
70648
70972
  text,
70649
70973
  identity,
70650
70974
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
70975
+ cliTool,
70651
70976
  responseMode: routeInfo.route.responseMode
70652
70977
  });
70653
70978
  const timingContext = {
@@ -70680,6 +71005,7 @@ class TelegramPollingService {
70680
71005
  text: nextText,
70681
71006
  identity,
70682
71007
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71008
+ cliTool,
70683
71009
  responseMode: routeInfo.route.responseMode
70684
71010
  }),
70685
71011
  route: routeInfo.route,
@@ -71711,7 +72037,7 @@ async function getRuntimeOperatorSummary(params) {
71711
72037
  const agentSummaries = loadedConfig.raw.agents.list.map((entry) => {
71712
72038
  const resolved = new AgentService(loadedConfig).getResolvedAgentConfig(entry.id);
71713
72039
  const tool = deriveAgentTool(loadedConfig, entry.id);
71714
- const bootstrapState = getBootstrapWorkspaceState(resolved.workspacePath, entry.bootstrap?.mode, tool.cliTool === "codex" || tool.cliTool === "claude" ? tool.cliTool : undefined);
72040
+ const bootstrapState = getBootstrapWorkspaceState(resolved.workspacePath, entry.bootstrap?.mode, tool.cliTool === "codex" || tool.cliTool === "claude" || tool.cliTool === "gemini" ? tool.cliTool : undefined);
71715
72041
  return {
71716
72042
  id: entry.id,
71717
72043
  cliTool: tool.cliTool,
@@ -72048,6 +72374,9 @@ function printCommandOutcomeBanner(outcome) {
72048
72374
  console.log("+---------+");
72049
72375
  console.log("");
72050
72376
  }
72377
+ function printCommandOutcomeFooter(outcome) {
72378
+ printCommandOutcomeBanner(outcome);
72379
+ }
72051
72380
  function getPrimaryWorkspacePath(summary) {
72052
72381
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
72053
72382
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -72068,13 +72397,18 @@ function printMissingBootstrapOptions(commandName) {
72068
72397
  console.log(` clisbot ${commandName} --cli codex --bot-type team`);
72069
72398
  console.log(` clisbot ${commandName} --cli claude --bot-type personal`);
72070
72399
  console.log(` clisbot ${commandName} --cli claude --bot-type team`);
72400
+ console.log(` clisbot ${commandName} --cli gemini --bot-type personal`);
72401
+ console.log(` clisbot ${commandName} --cli gemini --bot-type team`);
72071
72402
  console.log("Manual setup is still available with `clisbot agents add ...`.");
72072
72403
  for (const line of renderOperatorHelpLines()) {
72073
72404
  console.log(line);
72074
72405
  }
72406
+ if (commandName === "start") {
72407
+ printCommandOutcomeFooter("failure");
72408
+ }
72075
72409
  }
72076
72410
  function hasLiteralMemCredentials(flags) {
72077
- return flags.literalWarnings.length > 0;
72411
+ return hasLiteralBootstrapCredentials(flags);
72078
72412
  }
72079
72413
  async function prepareBootstrapState(rawArgs, commandName, options = {
72080
72414
  runtimeRunning: false
@@ -72090,6 +72424,9 @@ async function prepareBootstrapState(rawArgs, commandName, options = {
72090
72424
  for (const line of renderMissingTokenWarningLines()) {
72091
72425
  console.log(line);
72092
72426
  }
72427
+ if (commandName === "start") {
72428
+ printCommandOutcomeFooter("failure");
72429
+ }
72093
72430
  return null;
72094
72431
  }
72095
72432
  if (commandName === "init" && hasLiteralMemCredentials(bootstrapFlags) && !bootstrapFlags.persist) {
@@ -72151,6 +72488,7 @@ async function ensureDefaultAgentBootstrap(state, options, commandName) {
72151
72488
  for (const line of renderOperatorHelpLines()) {
72152
72489
  console.log(line);
72153
72490
  }
72491
+ printCommandOutcomeFooter("failure");
72154
72492
  return false;
72155
72493
  }
72156
72494
  }
@@ -72224,11 +72562,14 @@ async function serveForeground() {
72224
72562
  async function start(args = []) {
72225
72563
  const runtimeStatus = await getRuntimeStatus();
72226
72564
  const bootstrapFlags = parseBootstrapFlags(args);
72227
- if (runtimeStatus.running && hasLiteralMemCredentials(bootstrapFlags) && !bootstrapFlags.persist) {
72228
- throw new Error("Raw channel token input on `clisbot start` requires the runtime to be stopped first, unless you also pass --persist.");
72565
+ const restartForLiteralBootstrap = runtimeStatus.running && hasLiteralMemCredentials(bootstrapFlags);
72566
+ if (restartForLiteralBootstrap) {
72567
+ await stopDetachedRuntime({
72568
+ configPath: runtimeStatus.configPath
72569
+ });
72229
72570
  }
72230
72571
  const state = await prepareBootstrapState(args, "start", {
72231
- runtimeRunning: runtimeStatus.running,
72572
+ runtimeRunning: restartForLiteralBootstrap ? false : runtimeStatus.running,
72232
72573
  bootstrapFlags
72233
72574
  });
72234
72575
  if (!state) {
@@ -72244,13 +72585,14 @@ async function start(args = []) {
72244
72585
  for (const line of renderConfiguredChannelTokenStatusLines(state.config, runtimeMemEnv)) {
72245
72586
  console.log(line);
72246
72587
  }
72247
- if (!runtimeStatus.running) {
72588
+ if (restartForLiteralBootstrap || !runtimeStatus.running) {
72248
72589
  const tokenIssueLines = renderConfiguredChannelTokenIssueLines(state.config, runtimeMemEnv);
72249
72590
  for (const line of tokenIssueLines) {
72250
72591
  console.log(line);
72251
72592
  }
72252
72593
  if (tokenIssueLines.length > 0) {
72253
72594
  printCommandOutcomeBanner("failure");
72595
+ printCommandOutcomeFooter("failure");
72254
72596
  return;
72255
72597
  }
72256
72598
  }
@@ -72266,7 +72608,7 @@ async function start(args = []) {
72266
72608
  const result = await startDetachedRuntime({
72267
72609
  scriptPath: fileURLToPath4(import.meta.url),
72268
72610
  configPath: state.configResult.configPath,
72269
- extraEnv: runtimeStatus.running ? undefined : runtimeMemEnv,
72611
+ extraEnv: restartForLiteralBootstrap || !runtimeStatus.running ? runtimeMemEnv : undefined,
72270
72612
  runtimeCredentialsPath: getDefaultRuntimeCredentialsPath()
72271
72613
  });
72272
72614
  if (result.alreadyRunning) {
@@ -72284,6 +72626,7 @@ async function start(args = []) {
72284
72626
  console.log(`config: ${result.configPath}`);
72285
72627
  console.log(`log: ${result.logPath}`);
72286
72628
  console.log(renderStartSummary(summary));
72629
+ printCommandOutcomeFooter("success");
72287
72630
  } catch (error) {
72288
72631
  printCommandOutcomeBanner("success");
72289
72632
  console.log(`clisbot is already running with pid: ${result.pid}`);
@@ -72294,6 +72637,7 @@ async function start(args = []) {
72294
72637
  for (const line of renderRuntimeErrorLines("failed to render already-running summary", error)) {
72295
72638
  console.error(line);
72296
72639
  }
72640
+ printCommandOutcomeFooter("success");
72297
72641
  }
72298
72642
  return;
72299
72643
  }
@@ -72314,6 +72658,7 @@ async function start(args = []) {
72314
72658
  console.log(`config: ${result.configPath}`);
72315
72659
  console.log(`log: ${result.logPath}`);
72316
72660
  console.log(renderStartSummary(summary));
72661
+ printCommandOutcomeFooter("success");
72317
72662
  } catch (error) {
72318
72663
  printCommandOutcomeBanner("success");
72319
72664
  console.log(`clisbot started with pid: ${result.pid}`);
@@ -72322,6 +72667,7 @@ async function start(args = []) {
72322
72667
  for (const line of renderRuntimeErrorLines("failed to render start summary", error)) {
72323
72668
  console.error(line);
72324
72669
  }
72670
+ printCommandOutcomeFooter("success");
72325
72671
  }
72326
72672
  }
72327
72673
  async function printCliError(error) {
@@ -72363,15 +72709,18 @@ async function stop(hard = false) {
72363
72709
  if (!result.stopped && !hard) {
72364
72710
  printCommandOutcomeBanner("failure");
72365
72711
  console.log("clisbot is not running");
72712
+ printCommandOutcomeFooter("failure");
72366
72713
  return;
72367
72714
  }
72368
72715
  if (hard) {
72369
72716
  printCommandOutcomeBanner("success");
72370
72717
  console.log(result.stopped ? "clisbot stopped and tmux sessions cleaned up" : "clisbot was not running, but tmux sessions were cleaned up");
72718
+ printCommandOutcomeFooter("success");
72371
72719
  return;
72372
72720
  }
72373
72721
  printCommandOutcomeBanner("success");
72374
72722
  console.log("clisbot stopped");
72723
+ printCommandOutcomeFooter("success");
72375
72724
  }
72376
72725
  async function restart() {
72377
72726
  await stopDetachedRuntime({