clisbot 0.1.26 → 0.1.28

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
@@ -54694,6 +54694,7 @@ function renderCliHelp() {
54694
54694
  "",
54695
54695
  "Commands:",
54696
54696
  ` start Seed ${configPath} if missing, apply explicit channel-account bootstrap intent, and start clisbot in the background.`,
54697
+ " See `clisbot start --help` for bootstrap-focused flags and examples.",
54697
54698
  " restart Stop the running clisbot process, then start it again.",
54698
54699
  " stop Stop the running clisbot process.",
54699
54700
  " stop --hard Stop clisbot and kill all tmux sessions on the configured clisbot socket.",
@@ -54710,20 +54711,26 @@ function renderCliHelp() {
54710
54711
  " remove slack-group <groupId>",
54711
54712
  " set-token <slack-app|slack-bot|telegram-bot> <value>",
54712
54713
  " clear-token <slack-app|slack-bot|telegram-bot>",
54714
+ " See `clisbot channels --help` for route policy notes and defaults such as `requireMention`.",
54713
54715
  " accounts Manage Slack and Telegram provider accounts plus persistence state.",
54714
54716
  " add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54715
54717
  " add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54716
54718
  " persist --channel <slack|telegram> --account <id>",
54717
54719
  " persist --all",
54720
+ " See `clisbot accounts --help` for env-vs-mem-vs-persist behavior.",
54718
54721
  " loops Inspect or cancel managed recurring loops persisted by `/loop`.",
54719
54722
  " list|status",
54720
54723
  " cancel <id>",
54721
54724
  " cancel --all",
54725
+ " See `clisbot loops --help` for behavior notes.",
54722
54726
  " message Run provider message actions such as send, react, read, edit, delete, and pins.",
54727
+ " See `clisbot message --help` for channel-specific syntax.",
54723
54728
  " agents Manage configured agents and top-level bindings.",
54729
+ " See `clisbot agents --help` for focused add/bootstrap/binding help.",
54724
54730
  " auth Manage app and agent auth roles, principals, and permissions in config. See `clisbot auth --help`.",
54725
- " pairing Run the pairing control CLI.",
54731
+ " pairing Run the pairing control CLI. See `clisbot pairing --help`.",
54726
54732
  ` init Seed ${configPath} and optionally create the first agent without starting clisbot.`,
54733
+ " See `clisbot init --help` for bootstrap-focused flags and examples.",
54727
54734
  " --version, -v Show the installed clisbot version.",
54728
54735
  " --help Show this help text.",
54729
54736
  "",
@@ -54766,6 +54773,35 @@ function buildPairingReply(params) {
54766
54773
  ].join(`
54767
54774
  `);
54768
54775
  }
54776
+ function buildPairingQueueFullReply(params) {
54777
+ return [
54778
+ "clisbot: access not configured.",
54779
+ "",
54780
+ params.idLine,
54781
+ "",
54782
+ "Pairing queue is full right now.",
54783
+ "",
54784
+ "Ask the bot owner to inspect or clear pending requests with:",
54785
+ `clisbot pairing list ${params.channel}`,
54786
+ `clisbot pairing reject ${params.channel} <code>`,
54787
+ `clisbot pairing clear ${params.channel}`
54788
+ ].join(`
54789
+ `);
54790
+ }
54791
+ function buildPairingReplyFromRequest(params) {
54792
+ const code = params.pairingRequest.code.trim();
54793
+ if (!code) {
54794
+ return buildPairingQueueFullReply({
54795
+ channel: params.channel,
54796
+ idLine: params.idLine
54797
+ });
54798
+ }
54799
+ return buildPairingReply({
54800
+ channel: params.channel,
54801
+ idLine: params.idLine,
54802
+ code
54803
+ });
54804
+ }
54769
54805
  function renderPairingRequests(params) {
54770
54806
  if (!params.requests.length) {
54771
54807
  return `No pending ${params.channel} pairing requests.`;
@@ -54788,7 +54824,7 @@ import path from "node:path";
54788
54824
  var PAIRING_CODE_LENGTH = 8;
54789
54825
  var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
54790
54826
  var PAIRING_PENDING_TTL_MS = 60 * 60 * 1000;
54791
- var PAIRING_PENDING_MAX = 3;
54827
+ var PAIRING_PENDING_MAX = 20;
54792
54828
  var PAIRING_STORE_LOCK_OPTIONS = {
54793
54829
  retries: {
54794
54830
  retries: 10,
@@ -55067,8 +55103,63 @@ async function approveChannelPairingCode(params) {
55067
55103
  return { id: entry.id, entry };
55068
55104
  });
55069
55105
  }
55106
+ async function rejectChannelPairingCode(params) {
55107
+ const code = params.code.trim().toUpperCase();
55108
+ if (!code) {
55109
+ return null;
55110
+ }
55111
+ const filePath = resolvePairingPath(params.channel, params.baseDir);
55112
+ return withFileLock(filePath, { version: 1, requests: [] }, async () => {
55113
+ const { value } = await readJsonFile(filePath, {
55114
+ version: 1,
55115
+ requests: []
55116
+ });
55117
+ const requests = Array.isArray(value.requests) ? value.requests : [];
55118
+ const { requests: pruned, removed } = pruneExpiredRequests(requests, Date.now());
55119
+ const matchIndex = pruned.findIndex((request) => String(request.code ?? "").trim().toUpperCase() === code);
55120
+ if (matchIndex < 0) {
55121
+ if (removed) {
55122
+ await writeJsonFile(filePath, {
55123
+ version: 1,
55124
+ requests: pruned
55125
+ });
55126
+ }
55127
+ return null;
55128
+ }
55129
+ const [rejected] = pruned.splice(matchIndex, 1);
55130
+ await writeJsonFile(filePath, {
55131
+ version: 1,
55132
+ requests: pruned
55133
+ });
55134
+ return rejected ?? null;
55135
+ });
55136
+ }
55137
+ async function clearChannelPairingRequests(params) {
55138
+ const filePath = resolvePairingPath(params.channel, params.baseDir);
55139
+ return withFileLock(filePath, { version: 1, requests: [] }, async () => {
55140
+ const { value } = await readJsonFile(filePath, {
55141
+ version: 1,
55142
+ requests: []
55143
+ });
55144
+ const requests = Array.isArray(value.requests) ? value.requests : [];
55145
+ const { requests: pruned } = pruneExpiredRequests(requests, Date.now());
55146
+ await writeJsonFile(filePath, {
55147
+ version: 1,
55148
+ requests: []
55149
+ });
55150
+ return { cleared: pruned.length };
55151
+ });
55152
+ }
55070
55153
 
55071
55154
  // src/channels/pairing/cli.ts
55155
+ function resolvePairingBaseDir(env = process.env) {
55156
+ const configured = env.CLISBOT_PAIRING_DIR?.trim();
55157
+ if (configured) {
55158
+ return configured;
55159
+ }
55160
+ const legacy = env.TMUX_TALK_PAIRING_DIR?.trim();
55161
+ return legacy || undefined;
55162
+ }
55072
55163
  function parseChannel(raw) {
55073
55164
  const value = raw?.trim().toLowerCase();
55074
55165
  if (value === "slack" || value === "telegram") {
@@ -55076,9 +55167,33 @@ function parseChannel(raw) {
55076
55167
  }
55077
55168
  throw new Error("Channel required: slack | telegram");
55078
55169
  }
55170
+ function renderPairingCliHelp() {
55171
+ return [
55172
+ "clisbot pairing",
55173
+ "",
55174
+ "Usage:",
55175
+ " clisbot pairing --help",
55176
+ " clisbot pairing help",
55177
+ " clisbot pairing list <slack|telegram> [--json]",
55178
+ " clisbot pairing approve <slack|telegram> <code>",
55179
+ " clisbot pairing reject <slack|telegram> <code>",
55180
+ " clisbot pairing clear <slack|telegram>",
55181
+ "",
55182
+ "Notes:",
55183
+ " - `list` shows pending pairing requests for one channel only",
55184
+ " - `approve` moves that sender into the channel allowlist",
55185
+ " - `reject` removes one pending request without allowlisting the sender",
55186
+ " - `clear` drops every pending request for that channel when the queue needs a reset"
55187
+ ].join(`
55188
+ `);
55189
+ }
55079
55190
  async function runPairingCli(args, writer = console) {
55080
55191
  const [command, ...rest] = args;
55081
- const baseDir = process.env.TMUX_TALK_PAIRING_DIR;
55192
+ const baseDir = resolvePairingBaseDir();
55193
+ if (!command || command === "--help" || command === "-h" || command === "help") {
55194
+ writer.log(renderPairingCliHelp());
55195
+ return;
55196
+ }
55082
55197
  if (command === "list") {
55083
55198
  const wantsJson = rest.includes("--json");
55084
55199
  const channel = parseChannel(rest.find((value) => !value.startsWith("--")));
@@ -55103,7 +55218,34 @@ async function runPairingCli(args, writer = console) {
55103
55218
  writer.log(`Approved ${channel} sender ${approved.id}.`);
55104
55219
  return;
55105
55220
  }
55106
- throw new Error("Usage: pairing list <channel> [--json] | pairing approve <channel> <code>");
55221
+ if (command === "reject") {
55222
+ const [channelArg, code] = rest;
55223
+ const channel = parseChannel(channelArg);
55224
+ if (!code?.trim()) {
55225
+ throw new Error("Usage: pairing reject <channel> <code>");
55226
+ }
55227
+ const rejected = await rejectChannelPairingCode({
55228
+ channel,
55229
+ code,
55230
+ baseDir
55231
+ });
55232
+ if (!rejected) {
55233
+ throw new Error(`No pending pairing request found for code: ${code}`);
55234
+ }
55235
+ writer.log(`Rejected ${channel} sender ${rejected.id}.`);
55236
+ return;
55237
+ }
55238
+ if (command === "clear") {
55239
+ const [channelArg] = rest;
55240
+ const channel = parseChannel(channelArg);
55241
+ const result = await clearChannelPairingRequests({
55242
+ channel,
55243
+ baseDir
55244
+ });
55245
+ writer.log(`Cleared ${result.cleared} pending ${channel} pairing request(s).`);
55246
+ return;
55247
+ }
55248
+ throw new Error(renderPairingCliHelp());
55107
55249
  }
55108
55250
 
55109
55251
  // src/config/agent-tool-presets.ts
@@ -59283,7 +59425,7 @@ function formatConfiguredRuntimeLimit(params) {
59283
59425
  if (typeof params.maxRuntimeMin === "number" && Number.isFinite(params.maxRuntimeMin)) {
59284
59426
  return `${params.maxRuntimeMin} minute${params.maxRuntimeMin === 1 ? "" : "s"}`;
59285
59427
  }
59286
- return "15 minutes";
59428
+ return "30 minutes";
59287
59429
  }
59288
59430
  function parseCommandDurationMs(raw) {
59289
59431
  const match = raw.trim().match(/^(\d+)(ms|s|m|h)$/i);
@@ -59991,7 +60133,7 @@ var agentDefaultsSchema = exports_external.object({
59991
60133
  updateIntervalMs: 2000,
59992
60134
  idleTimeoutMs: 6000,
59993
60135
  noOutputTimeoutMs: 20000,
59994
- maxRuntimeMin: 15,
60136
+ maxRuntimeMin: 30,
59995
60137
  maxMessageChars: 3500
59996
60138
  }),
59997
60139
  session: sessionSchema.default({
@@ -60679,7 +60821,7 @@ function renderDefaultConfigTemplate(options = {}) {
60679
60821
  updateIntervalMs: 2000,
60680
60822
  idleTimeoutMs: 6000,
60681
60823
  noOutputTimeoutMs: 20000,
60682
- maxRuntimeMin: 15,
60824
+ maxRuntimeMin: 30,
60683
60825
  maxMessageChars: 3500
60684
60826
  },
60685
60827
  session: {
@@ -61044,6 +61186,32 @@ function parseSingleOption(args, name) {
61044
61186
  function hasFlag(args, name) {
61045
61187
  return args.includes(name);
61046
61188
  }
61189
+ function renderAgentsHelp() {
61190
+ return [
61191
+ "clisbot agents",
61192
+ "",
61193
+ "Usage:",
61194
+ " clisbot agents --help",
61195
+ " clisbot agents help",
61196
+ " clisbot agents list [--bindings] [--json]",
61197
+ " clisbot agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...",
61198
+ " clisbot agents bootstrap <id> --mode <personal-assistant|team-assistant> [--force]",
61199
+ " clisbot agents bindings [--agent <id>] [--json]",
61200
+ " clisbot agents bind --agent <id> --bind <channel[:accountId]>",
61201
+ " clisbot agents unbind --agent <id> [--bind <channel[:accountId]> | --all]",
61202
+ " clisbot agents response-mode <status|set|clear> --agent <id> [capture-pane|message-tool]",
61203
+ " clisbot agents additional-message-mode <status|set|clear> --agent <id> [queue|steer]",
61204
+ "",
61205
+ "Notes:",
61206
+ " - `agents add` is the lower-level manual surface; first-run `clisbot start` and `clisbot init` can bootstrap the first `default` agent for you",
61207
+ " - `--cli` is required on `agents add`; supported tools are `codex`, `claude`, and `gemini`",
61208
+ " - omit `--startup-option` to inherit the built-in startup args for the selected CLI tool",
61209
+ " - `--bind slack`, `--bind telegram`, or `--bind <channel>:<accountId>` creates top-level fallback bindings",
61210
+ " - explicit route `agentId` on Slack or Telegram still wins before these fallback bindings",
61211
+ " - `response-mode` and `additional-message-mode` mutate per-agent overrides under `agents.list[]`"
61212
+ ].join(`
61213
+ `);
61214
+ }
61047
61215
  function parseResponseMode(raw) {
61048
61216
  if (raw === "capture-pane" || raw === "message-tool") {
61049
61217
  return raw;
@@ -61373,7 +61541,11 @@ async function runAgentAdditionalMessageModeCli(args) {
61373
61541
  async function runAgentsCli(args) {
61374
61542
  const subcommand = args[0];
61375
61543
  const rest = args.slice(1);
61376
- if (!subcommand || subcommand === "list") {
61544
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
61545
+ console.log(renderAgentsHelp());
61546
+ return;
61547
+ }
61548
+ if (subcommand === "list") {
61377
61549
  await listAgents(rest);
61378
61550
  return;
61379
61551
  }
@@ -61405,7 +61577,7 @@ async function runAgentsCli(args) {
61405
61577
  await unbindAgent(rest);
61406
61578
  return;
61407
61579
  }
61408
- throw new Error(`Unknown agents subcommand: ${subcommand}`);
61580
+ throw new Error(renderAgentsHelp());
61409
61581
  }
61410
61582
 
61411
61583
  // src/control/accounts-cli.ts
@@ -63505,6 +63677,12 @@ function collapseBlankLines(lines) {
63505
63677
  }
63506
63678
  return collapsed;
63507
63679
  }
63680
+ var DURATION_STATUS_PATTERN = String.raw`(?:\d+(?:h|m|s))(?:\s+\d+(?:h|m|s)){0,2}`;
63681
+ var CODEX_WORKING_STATUS_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?Working(?:\s*\()?${DURATION_STATUS_PATTERN}\b.*(?:esc to interrupt|interrupt)\)?$`, "i");
63682
+ var CODEX_INTERRUPT_FOOTER_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?${DURATION_STATUS_PATTERN}\s*[•◦·]?\s*esc to interrupt\)?$`, "i");
63683
+ var GEMINI_THINKING_STATUS_PATTERN = new RegExp(String.raw`^Thinking\.\.\. \(esc to cancel,\s*${DURATION_STATUS_PATTERN}\)$`, "i");
63684
+ var CLAUDE_WORKED_STATUS_PATTERN = new RegExp(String.raw`^(?:[✻✽*]\s*)?(?:Worked|Cooked) for ${DURATION_STATUS_PATTERN}$`, "i");
63685
+ var CLAUDE_TIMER_FOOTER_PATTERN = new RegExp(String.raw`\|\s*claude\s*\|.*\|\s*${DURATION_STATUS_PATTERN}\s*$`, "i");
63508
63686
  function looksLikeUrlContinuation(line) {
63509
63687
  const trimmed = line.trim();
63510
63688
  return trimmed.startsWith("(") || trimmed.startsWith("http://") || trimmed.startsWith("https://");
@@ -63702,7 +63880,14 @@ function isInterruptStatusLine(line) {
63702
63880
  if (!trimmed) {
63703
63881
  return false;
63704
63882
  }
63705
- return /^(?:[•◦·]\s*)?Working(?:\s*\()?\d+s\b.*(?:esc to interrupt|interrupt)\)?$/i.test(trimmed) || /^(?:[•◦·]\s*)?\d+s\s*[•◦·]?\s*esc to interrupt\)?$/i.test(trimmed);
63883
+ return CODEX_WORKING_STATUS_PATTERN.test(trimmed) || CODEX_INTERRUPT_FOOTER_PATTERN.test(trimmed);
63884
+ }
63885
+ function isTimerDrivenStatusLine(line) {
63886
+ const trimmed = line.trim();
63887
+ if (!trimmed) {
63888
+ return false;
63889
+ }
63890
+ return isInterruptStatusLine(trimmed) || GEMINI_THINKING_STATUS_PATTERN.test(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed) || CLAUDE_TIMER_FOOTER_PATTERN.test(trimmed);
63706
63891
  }
63707
63892
  function shouldDropCodexChromeLine(line) {
63708
63893
  const trimmed = line.trim();
@@ -63716,14 +63901,14 @@ function shouldDropClaudeChromeLine(line) {
63716
63901
  if (!trimmed) {
63717
63902
  return false;
63718
63903
  }
63719
- 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);
63904
+ 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 |") || isTimerDrivenStatusLine(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);
63720
63905
  }
63721
63906
  function shouldDropGeminiChromeLine(line) {
63722
63907
  const trimmed = line.trim();
63723
63908
  if (!trimmed) {
63724
63909
  return false;
63725
63910
  }
63726
- 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);
63911
+ 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) || isTimerDrivenStatusLine(trimmed) || /^[╭╰│]/.test(trimmed) || /^[-▀▄]{10,}$/.test(trimmed) || /^─+$/.test(trimmed);
63727
63912
  }
63728
63913
  function normalizeBoundaryLine(line) {
63729
63914
  return line.trim().replace(/^(?::eight_spoked_asterisk:|[-*•◦·✽✶])\s+/, "");
@@ -63757,16 +63942,21 @@ function collapseAdjacentDuplicateLines(raw) {
63757
63942
  return collapseBlankLines(collapsed).join(`
63758
63943
  `).trim();
63759
63944
  }
63760
- function cleanInteractionSnapshot(raw) {
63945
+ function cleanInteractionSnapshotInternal(raw, options) {
63761
63946
  const lines = splitNormalizedLines(raw);
63762
63947
  const isCodex = looksLikeCodexSnapshot(lines);
63763
63948
  const isClaude = looksLikeClaudeSnapshot(lines);
63764
63949
  const isGemini = looksLikeGeminiSnapshot(lines);
63765
63950
  const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : isGemini ? dropGeminiPromptBlocks(lines) : lines;
63951
+ const timerStatusLines = [];
63766
63952
  const filtered = promptStripped.filter((line) => {
63767
63953
  if (shouldDropDeliveryReportLine(line)) {
63768
63954
  return false;
63769
63955
  }
63956
+ if (options?.preserveTimerStatusLines && isTimerDrivenStatusLine(line)) {
63957
+ timerStatusLines.push(line.trim());
63958
+ return false;
63959
+ }
63770
63960
  if (isCodex && shouldDropCodexChromeLine(line)) {
63771
63961
  return false;
63772
63962
  }
@@ -63780,8 +63970,21 @@ function cleanInteractionSnapshot(raw) {
63780
63970
  });
63781
63971
  const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : isGemini ? filtered.map((line) => line.replace(/^\s*>\s*/, "")) : filtered;
63782
63972
  const unwrapped = unwrapSoftWrappedLines(normalized);
63783
- return collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
63973
+ const cleanedBody = collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
63784
63974
  `));
63975
+ const cleanedTimerStatus = options?.preserveTimerStatusLines ? collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(timerStatusLines)).join(`
63976
+ `)) : "";
63977
+ return [cleanedBody, cleanedTimerStatus].filter(Boolean).join(`
63978
+
63979
+ `).trim();
63980
+ }
63981
+ function cleanInteractionSnapshot(raw) {
63982
+ return cleanInteractionSnapshotInternal(raw);
63983
+ }
63984
+ function cleanRunningInteractionSnapshot(raw) {
63985
+ return cleanInteractionSnapshotInternal(raw, {
63986
+ preserveTimerStatusLines: true
63987
+ });
63785
63988
  }
63786
63989
  // src/shared/transcript-delta.ts
63787
63990
  function diffText(previous, current) {
@@ -63842,16 +64045,8 @@ function extractScrolledAppend(previous, current) {
63842
64045
  }
63843
64046
  return "";
63844
64047
  }
63845
- function deriveRunningInteractionText(previousSnapshot, currentSnapshot) {
63846
- const previous = cleanInteractionSnapshot(previousSnapshot);
63847
- const current = cleanInteractionSnapshot(currentSnapshot);
63848
- if (!current || current === previous) {
63849
- return "";
63850
- }
63851
- if (!previous) {
63852
- return current;
63853
- }
63854
- return extractScrolledAppend(previous, current);
64048
+ function deriveRunningInteractionSnapshot(currentSnapshot) {
64049
+ return cleanRunningInteractionSnapshot(currentSnapshot);
63855
64050
  }
63856
64051
  function deriveInteractionText(initialSnapshot, currentSnapshot) {
63857
64052
  const previous = cleanInteractionSnapshot(initialSnapshot);
@@ -64593,6 +64788,7 @@ var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
64593
64788
  var TMUX_TRANSIENT_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
64594
64789
  var SESSION_READY_CAPTURE_RETRY_COUNT = 5;
64595
64790
  var SESSION_READY_CAPTURE_RETRY_DELAY_MS = 100;
64791
+ var SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS = 15000;
64596
64792
  function summarizeSnapshot(snapshot) {
64597
64793
  const compact = snapshot.split(`
64598
64794
  `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
@@ -64624,6 +64820,7 @@ class RunnerService {
64624
64820
  sessionState;
64625
64821
  resolveTarget;
64626
64822
  cleanupInFlight = false;
64823
+ sessionIdentityCaptureRetryAt = new Map;
64627
64824
  constructor(loadedConfig, tmux, sessionState, resolveTarget) {
64628
64825
  this.loadedConfig = loadedConfig;
64629
64826
  this.tmux = tmux;
@@ -64675,12 +64872,24 @@ class RunnerService {
64675
64872
  async syncSessionIdentity(resolved) {
64676
64873
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
64677
64874
  if (existing?.sessionId) {
64875
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64678
64876
  return this.sessionState.touchSessionEntry(resolved, {
64679
64877
  sessionId: existing.sessionId,
64680
64878
  runnerCommand: resolved.runner.command
64681
64879
  });
64682
64880
  }
64881
+ const retryAt = this.sessionIdentityCaptureRetryAt.get(resolved.sessionKey) ?? 0;
64882
+ if (retryAt > Date.now()) {
64883
+ return this.sessionState.touchSessionEntry(resolved, {
64884
+ runnerCommand: resolved.runner.command
64885
+ });
64886
+ }
64683
64887
  const sessionId = await this.captureSessionIdentity(resolved);
64888
+ if (sessionId) {
64889
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64890
+ } else {
64891
+ this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
64892
+ }
64684
64893
  return this.sessionState.touchSessionEntry(resolved, {
64685
64894
  sessionId,
64686
64895
  runnerCommand: resolved.runner.command
@@ -64835,6 +65044,7 @@ class RunnerService {
64835
65044
  sessionName: resolved.sessionName,
64836
65045
  stateDir: this.loadedConfig.stateDir
64837
65046
  });
65047
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64838
65048
  try {
64839
65049
  await this.tmux.newSession({
64840
65050
  sessionName: resolved.sessionName,
@@ -65101,11 +65311,12 @@ function buildRunRecoveryNote(kind, params) {
65101
65311
  var FIRST_OUTPUT_POLL_INTERVAL_MS = 250;
65102
65312
  async function monitorTmuxRun(params) {
65103
65313
  let previousSnapshot = params.initialSnapshot;
65104
- let lastChangeAt = Date.now();
65105
- let sawChange = false;
65106
- let cumulativeInteractionSnapshot = "";
65314
+ let previousRunningSnapshot = "";
65315
+ let lastActivityAt = params.startedAt;
65316
+ let sawActivity = false;
65107
65317
  let detachedNotified = params.detachedAlready;
65108
65318
  let firstMeaningfulDeltaLogged = false;
65319
+ let noOutputThresholdLogged = false;
65109
65320
  if (params.prompt) {
65110
65321
  logLatencyDebug("tmux-submit-start", params.timingContext, {
65111
65322
  sessionName: params.sessionName,
@@ -65126,61 +65337,57 @@ async function monitorTmuxRun(params) {
65126
65337
  });
65127
65338
  }
65128
65339
  while (true) {
65129
- await sleep(sawChange ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
65340
+ await sleep(sawActivity ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
65130
65341
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65131
65342
  const now = Date.now();
65132
- if (snapshot !== previousSnapshot) {
65133
- const priorSnapshot = previousSnapshot;
65134
- lastChangeAt = now;
65135
- previousSnapshot = snapshot;
65136
- const interactionDelta = deriveRunningInteractionText(priorSnapshot, snapshot);
65137
- const nextInteractionSnapshot = appendInteractionText(cumulativeInteractionSnapshot, interactionDelta);
65138
- if (nextInteractionSnapshot && nextInteractionSnapshot !== cumulativeInteractionSnapshot) {
65139
- sawChange = true;
65140
- cumulativeInteractionSnapshot = nextInteractionSnapshot;
65141
- if (!firstMeaningfulDeltaLogged) {
65142
- firstMeaningfulDeltaLogged = true;
65143
- logLatencyDebug("tmux-first-meaningful-delta", params.timingContext, {
65144
- sessionName: params.sessionName,
65145
- elapsedMs: now - params.startedAt
65146
- });
65147
- }
65148
- await params.onRunning({
65149
- snapshot: cumulativeInteractionSnapshot,
65150
- fullSnapshot: snapshot,
65151
- initialSnapshot: params.initialSnapshot
65343
+ const runningSnapshot = deriveRunningInteractionSnapshot(snapshot);
65344
+ previousSnapshot = snapshot;
65345
+ if (runningSnapshot && runningSnapshot !== previousRunningSnapshot) {
65346
+ previousRunningSnapshot = runningSnapshot;
65347
+ lastActivityAt = now;
65348
+ sawActivity = true;
65349
+ if (!firstMeaningfulDeltaLogged) {
65350
+ firstMeaningfulDeltaLogged = true;
65351
+ logLatencyDebug("tmux-first-meaningful-delta", params.timingContext, {
65352
+ sessionName: params.sessionName,
65353
+ elapsedMs: now - params.startedAt
65152
65354
  });
65153
65355
  }
65356
+ await params.onRunning({
65357
+ snapshot: runningSnapshot,
65358
+ fullSnapshot: snapshot,
65359
+ initialSnapshot: params.initialSnapshot
65360
+ });
65154
65361
  }
65155
65362
  if (!detachedNotified && now - params.startedAt >= params.maxRuntimeMs) {
65156
65363
  detachedNotified = true;
65157
65364
  await params.onDetached({
65158
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65365
+ snapshot: previousRunningSnapshot || deriveInteractionText(params.initialSnapshot, snapshot),
65159
65366
  fullSnapshot: previousSnapshot,
65160
65367
  initialSnapshot: params.initialSnapshot
65161
65368
  });
65162
65369
  }
65163
- if (sawChange && now - lastChangeAt >= params.idleTimeoutMs) {
65370
+ if (sawActivity && now - lastActivityAt >= params.idleTimeoutMs) {
65164
65371
  await params.onCompleted({
65165
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65372
+ snapshot: deriveInteractionText(params.initialSnapshot, previousSnapshot),
65166
65373
  fullSnapshot: previousSnapshot,
65167
65374
  initialSnapshot: params.initialSnapshot
65168
65375
  });
65169
65376
  return;
65170
65377
  }
65171
- if (!sawChange && now - params.startedAt >= params.noOutputTimeoutMs) {
65172
- await params.onTimeout({
65173
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65174
- fullSnapshot: previousSnapshot,
65175
- initialSnapshot: params.initialSnapshot
65378
+ if (!noOutputThresholdLogged && !sawActivity && now - params.startedAt >= params.noOutputTimeoutMs) {
65379
+ noOutputThresholdLogged = true;
65380
+ logLatencyDebug("tmux-no-output-threshold-crossed", params.timingContext, {
65381
+ sessionName: params.sessionName,
65382
+ elapsedMs: now - params.startedAt
65176
65383
  });
65177
- return;
65178
65384
  }
65179
65385
  }
65180
65386
  }
65181
65387
 
65182
65388
  // src/agents/session-service.ts
65183
65389
  var OBSERVER_RETRYABLE_FAILURE_LIMIT = 3;
65390
+ var DETACHED_OBSERVER_INTERVAL_MS = 5 * 60000;
65184
65391
  function formatObserverError(error) {
65185
65392
  return error instanceof Error ? error.stack ?? error.message : String(error);
65186
65393
  }
@@ -65428,7 +65635,10 @@ class SessionService {
65428
65635
  detached: false
65429
65636
  };
65430
65637
  }
65431
- observer.mode = "passive-final";
65638
+ observer.mode = "poll";
65639
+ observer.intervalMs = DETACHED_OBSERVER_INTERVAL_MS;
65640
+ observer.expiresAt = undefined;
65641
+ observer.lastSentAt = Date.now();
65432
65642
  run.observerFailures.delete(observerId);
65433
65643
  return {
65434
65644
  detached: true
@@ -65451,7 +65661,19 @@ class SessionService {
65451
65661
  this.activeRuns.clear();
65452
65662
  }
65453
65663
  buildDetachedNote(resolved) {
65454
- 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.`;
65664
+ return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot will keep monitoring it, switch this thread to sparse progress updates, and 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.`;
65665
+ }
65666
+ applyDetachedObserverPolicy(run) {
65667
+ const now = Date.now();
65668
+ for (const observer of run.observers.values()) {
65669
+ if (observer.mode !== "live") {
65670
+ continue;
65671
+ }
65672
+ observer.mode = "poll";
65673
+ observer.intervalMs = DETACHED_OBSERVER_INTERVAL_MS;
65674
+ observer.expiresAt = undefined;
65675
+ observer.lastSentAt = now;
65676
+ }
65455
65677
  }
65456
65678
  createRunUpdate(params) {
65457
65679
  return {
@@ -65682,7 +65904,8 @@ class SessionService {
65682
65904
  startedAt: params.startedAt,
65683
65905
  detachedAt: Date.now()
65684
65906
  });
65685
- currentRun.latestUpdate = detachedUpdate;
65907
+ await this.notifyRunObservers(currentRun, detachedUpdate);
65908
+ this.applyDetachedObserverPolicy(currentRun);
65686
65909
  currentRun.initialResult.resolve(detachedUpdate);
65687
65910
  },
65688
65911
  onCompleted: async (update) => {
@@ -65694,16 +65917,6 @@ class SessionService {
65694
65917
  initialSnapshot: update.initialSnapshot
65695
65918
  });
65696
65919
  await this.finishActiveRun(sessionKey, runUpdate);
65697
- },
65698
- onTimeout: async (update) => {
65699
- const runUpdate = this.createRunUpdate({
65700
- resolved: run.resolved,
65701
- status: "timeout",
65702
- snapshot: mergeRunSnapshot(params.snapshotPrefix ?? "", update.snapshot),
65703
- fullSnapshot: update.fullSnapshot,
65704
- initialSnapshot: update.initialSnapshot
65705
- });
65706
- await this.finishActiveRun(sessionKey, runUpdate);
65707
65920
  }
65708
65921
  });
65709
65922
  } catch (error) {
@@ -65720,6 +65933,28 @@ class SessionService {
65720
65933
  }
65721
65934
 
65722
65935
  // src/agents/agent-service.ts
65936
+ function shellQuote3(value) {
65937
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
65938
+ return value;
65939
+ }
65940
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
65941
+ }
65942
+ function buildCommandString2(command, args) {
65943
+ return [command, ...args].map(shellQuote3).join(" ");
65944
+ }
65945
+ function stripWorkspaceArgs(args) {
65946
+ const filtered = [];
65947
+ for (let index = 0;index < args.length; index += 1) {
65948
+ const current = args[index];
65949
+ if (current === "-C") {
65950
+ index += 1;
65951
+ continue;
65952
+ }
65953
+ filtered.push(current);
65954
+ }
65955
+ return filtered;
65956
+ }
65957
+
65723
65958
  class AgentService {
65724
65959
  loadedConfig;
65725
65960
  tmuxClient;
@@ -65810,6 +66045,15 @@ class AgentService {
65810
66045
  async getSessionRuntime(target) {
65811
66046
  return this.sessionState.getSessionRuntime(target);
65812
66047
  }
66048
+ async getSessionDiagnostics(target) {
66049
+ const resolved = this.resolveTarget(target);
66050
+ const entry = await this.sessionState.getEntry(target.sessionKey);
66051
+ const sessionId = entry?.sessionId?.trim() || undefined;
66052
+ return {
66053
+ sessionId,
66054
+ resumeCommand: this.buildResumeCommandPreview(resolved, sessionId)
66055
+ };
66056
+ }
65813
66057
  async listActiveSessionRuntimes() {
65814
66058
  return this.sessionState.listActiveSessionRuntimes();
65815
66059
  }
@@ -66052,6 +66296,21 @@ class AgentService {
66052
66296
  this.scheduleIntervalLoopTimer(persisted.id, Math.max(0, persisted.nextRunAt - Date.now()));
66053
66297
  }
66054
66298
  }
66299
+ buildResumeCommandPreview(resolved, sessionId) {
66300
+ if (!sessionId || resolved.runner.sessionId.resume.mode !== "command") {
66301
+ return;
66302
+ }
66303
+ const values = {
66304
+ agentId: resolved.agentId,
66305
+ workspace: resolved.workspacePath,
66306
+ sessionName: resolved.sessionName,
66307
+ sessionKey: resolved.sessionKey,
66308
+ sessionId
66309
+ };
66310
+ const command = resolved.runner.sessionId.resume.command ?? resolved.runner.command;
66311
+ const args = stripWorkspaceArgs(resolved.runner.sessionId.resume.args.map((value) => applyTemplate(value, values)));
66312
+ return buildCommandString2(command, args);
66313
+ }
66055
66314
  async isManagedLoopPersisted(managed) {
66056
66315
  const entry = await this.sessionState.getEntry(managed.target.sessionKey);
66057
66316
  return (entry?.intervalLoops ?? []).some((loop) => loop.id === managed.loop.id);
@@ -66556,6 +66815,12 @@ function parseAgentCommand(text, options = {}) {
66556
66815
  if (lowered === "loop") {
66557
66816
  const loopText = withoutSlash.slice(command.length).trim();
66558
66817
  const loweredLoopText = loopText.toLowerCase();
66818
+ if (!loweredLoopText || loweredLoopText === "help") {
66819
+ return {
66820
+ type: "control",
66821
+ name: "loop-help"
66822
+ };
66823
+ }
66559
66824
  if (loweredLoopText === "status") {
66560
66825
  return {
66561
66826
  type: "loop-control",
@@ -66603,6 +66868,12 @@ function parseAgentCommand(text, options = {}) {
66603
66868
  const queueText = withoutSlash.slice(command.length).trim();
66604
66869
  const normalizedQueueText = queueText.toLowerCase();
66605
66870
  if (lowered === "queue") {
66871
+ if (normalizedQueueText === "help") {
66872
+ return {
66873
+ type: "control",
66874
+ name: "queue-help"
66875
+ };
66876
+ }
66606
66877
  if (normalizedQueueText === "list") {
66607
66878
  return {
66608
66879
  type: "control",
@@ -66669,7 +66940,7 @@ function renderAgentControlSlashHelp() {
66669
66940
  "- `/whoami`: show the current platform, route, and sender identity details",
66670
66941
  "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
66671
66942
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
66672
- "- `/detach`: stop live updates for this thread while still allowing final settlement here",
66943
+ "- `/detach`: stop live updates for this thread, switch to sparse progress updates, and still allow final settlement here",
66673
66944
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
66674
66945
  "- `/stop`: send Escape to interrupt the current conversation session",
66675
66946
  "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
@@ -66686,9 +66957,11 @@ function renderAgentControlSlashHelp() {
66686
66957
  "- `/additionalmessagemode steer`: send later user messages straight into the active session",
66687
66958
  "- `/additionalmessagemode queue`: queue later user messages behind the active run for this surface",
66688
66959
  "- `/queue <message>` or `\\q <message>`: enqueue a later message behind the active run and let clisbot deliver it in order",
66960
+ "- `/queue help`: show queue-specific help and examples",
66689
66961
  "- `/steer <message>` or `\\s <message>`: inject a steering message into the active run immediately",
66690
66962
  "- `/queue list`: show queued messages that have not started yet",
66691
66963
  "- `/queue clear`: clear queued messages that have not started yet",
66964
+ "- `/loop help`: show loop-specific help and syntax examples",
66692
66965
  ...renderLoopHelpLines(),
66693
66966
  "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
66694
66967
  "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
@@ -66697,6 +66970,15 @@ function renderAgentControlSlashHelp() {
66697
66970
  ].join(`
66698
66971
  `);
66699
66972
  }
66973
+ function renderQueueHelpLines() {
66974
+ return [
66975
+ "- `/queue <message>` or `\\q <message>`: enqueue one later message behind the active run",
66976
+ "- `/queue list`: show queued messages that have not started yet",
66977
+ "- `/queue clear`: clear queued messages that have not started yet",
66978
+ "- `/queue help`: show this queue help again",
66979
+ "- `/steer <message>` or `\\s <message>`: inject an immediate steering message instead of queueing"
66980
+ ];
66981
+ }
66700
66982
  function parseWatchCommand(raw) {
66701
66983
  const match = raw.match(/^every\s+(\S+)(?:\s+for\s+(\S+))?$/i);
66702
66984
  if (!match) {
@@ -66918,7 +67200,8 @@ function renderWhoAmIMessage(params) {
66918
67200
  `platform: \`${params.identity.platform}\``,
66919
67201
  `conversationKind: \`${params.identity.conversationKind}\``,
66920
67202
  `agentId: \`${params.route.agentId}\``,
66921
- `sessionKey: \`${params.sessionTarget.sessionKey}\``
67203
+ `sessionKey: \`${params.sessionTarget.sessionKey}\``,
67204
+ `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
66922
67205
  ];
66923
67206
  if (params.identity.senderId) {
66924
67207
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -66935,7 +67218,7 @@ function renderWhoAmIMessage(params) {
66935
67218
  if (params.identity.topicId) {
66936
67219
  lines.push(`topicId: \`${params.identity.topicId}\``);
66937
67220
  }
66938
- lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
67221
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
66939
67222
  return lines.join(`
66940
67223
  `);
66941
67224
  }
@@ -66946,7 +67229,8 @@ function renderRouteStatusMessage(params) {
66946
67229
  `platform: \`${params.identity.platform}\``,
66947
67230
  `conversationKind: \`${params.identity.conversationKind}\``,
66948
67231
  `agentId: \`${params.route.agentId}\``,
66949
- `sessionKey: \`${params.sessionTarget.sessionKey}\``
67232
+ `sessionKey: \`${params.sessionTarget.sessionKey}\``,
67233
+ `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
66950
67234
  ];
66951
67235
  if (params.identity.senderId) {
66952
67236
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -66963,7 +67247,7 @@ function renderRouteStatusMessage(params) {
66963
67247
  if (params.identity.topicId) {
66964
67248
  lines.push(`topicId: \`${params.identity.topicId}\``);
66965
67249
  }
66966
- lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
67250
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
66967
67251
  if (params.runtimeState.startedAt) {
66968
67252
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
66969
67253
  }
@@ -66977,7 +67261,7 @@ function renderRouteStatusMessage(params) {
66977
67261
  lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
66978
67262
  }
66979
67263
  }
66980
- lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/streaming status|on|off|latest|all`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue <message>`, `/steer <message>`", "- `/queue list`, `/queue clear`", params.route.verbose === "off" ? "- `/transcript` disabled on this route (`verbose: off`)" : "- `/transcript` enabled on this route (`verbose: minimal`)", "- `/bash` requires `shellExecute`");
67264
+ lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/streaming status|on|off|latest|all`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop help`, `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue help`, `/queue <message>`, `/steer <message>`", "- `/queue list`, `/queue clear`", params.route.verbose === "off" ? "- `/transcript` disabled on this route (`verbose: off`)" : "- `/transcript` enabled on this route (`verbose: minimal`)", "- `/bash` requires `shellExecute`");
66981
67265
  return lines.join(`
66982
67266
  `);
66983
67267
  }
@@ -67049,9 +67333,22 @@ function renderQueuedMessagesList(items) {
67049
67333
  return lines.join(`
67050
67334
  `).trimEnd();
67051
67335
  }
67336
+ function renderQueueUsage() {
67337
+ return [
67338
+ "Queue commands",
67339
+ "",
67340
+ ...renderQueueHelpLines(),
67341
+ "",
67342
+ "Notes:",
67343
+ "- use queue when the current run should finish first and the next user request can wait in order",
67344
+ "- use steer when the active run should be nudged or redirected immediately"
67345
+ ].join(`
67346
+ `);
67347
+ }
67052
67348
  function renderLoopUsage() {
67053
67349
  return [
67054
67350
  "Usage:",
67351
+ "- `/loop help`",
67055
67352
  "- `/loop 5m check CI`",
67056
67353
  "- `/loop 1m --force check CI`",
67057
67354
  "- `/loop 5m`",
@@ -67602,6 +67899,7 @@ async function processChannelInteraction(params) {
67602
67899
  };
67603
67900
  let replyRecorded = false;
67604
67901
  let renderChain = Promise.resolve();
67902
+ const sessionDiagnostics = await params.agentService.getSessionDiagnostics?.(params.sessionTarget) ?? {};
67605
67903
  async function recordReplyIfNeeded() {
67606
67904
  if (replyRecorded) {
67607
67905
  return;
@@ -67676,6 +67974,7 @@ async function processChannelInteraction(params) {
67676
67974
  route: params.route,
67677
67975
  auth,
67678
67976
  sessionTarget: params.sessionTarget,
67977
+ sessionDiagnostics,
67679
67978
  followUpState,
67680
67979
  runtimeState,
67681
67980
  loopState: {
@@ -67696,7 +67995,8 @@ async function processChannelInteraction(params) {
67696
67995
  identity: params.identity,
67697
67996
  route: params.route,
67698
67997
  auth,
67699
- sessionTarget: params.sessionTarget
67998
+ sessionTarget: params.sessionTarget,
67999
+ sessionDiagnostics
67700
68000
  }));
67701
68001
  await params.agentService.recordConversationReply(params.sessionTarget);
67702
68002
  return interactionResult;
@@ -67853,6 +68153,11 @@ async function processChannelInteraction(params) {
67853
68153
  await params.agentService.recordConversationReply(params.sessionTarget);
67854
68154
  return interactionResult;
67855
68155
  }
68156
+ if (slashCommand.name === "queue-help") {
68157
+ await params.postText(renderQueueUsage());
68158
+ await params.agentService.recordConversationReply(params.sessionTarget);
68159
+ return interactionResult;
68160
+ }
67856
68161
  if (slashCommand.name === "queue-clear") {
67857
68162
  const clearedCount = params.agentService.clearQueuedPrompts?.(params.sessionTarget) ?? 0;
67858
68163
  await params.postText(clearedCount > 0 ? `Cleared ${clearedCount} queued message${clearedCount === 1 ? "" : "s"}.` : "Queue was already empty.");
@@ -67860,6 +68165,11 @@ async function processChannelInteraction(params) {
67860
68165
  return interactionResult;
67861
68166
  }
67862
68167
  }
68168
+ if (slashCommand?.type === "control" && slashCommand.name === "loop-help") {
68169
+ await params.postText(renderLoopUsage());
68170
+ await params.agentService.recordConversationReply(params.sessionTarget);
68171
+ return interactionResult;
68172
+ }
67863
68173
  if (slashCommand?.type === "loop-control") {
67864
68174
  if (slashCommand.action === "status") {
67865
68175
  await params.postText(renderLoopStatusMessage({
@@ -68686,7 +68996,7 @@ async function clearSlackAssistantThreadStatus(client, target) {
68686
68996
 
68687
68997
  // src/channels/processing-indicator.ts
68688
68998
  function shouldResolveIndicatorWait(update) {
68689
- return isTerminalRunStatus(update.status) || update.status === "detached";
68999
+ return isTerminalRunStatus(update.status);
68690
69000
  }
68691
69001
  async function waitForProcessingIndicatorLifecycle(params) {
68692
69002
  if (params.lifecycle !== "active-run") {
@@ -68817,6 +69127,7 @@ class ConversationProcessingIndicatorCoordinator {
68817
69127
  }
68818
69128
 
68819
69129
  // src/channels/slack/processing-decoration.ts
69130
+ var DEFAULT_STATUS_REFRESH_INTERVAL_MS = 2000;
68820
69131
  async function activateSlackProcessingDecoration(params) {
68821
69132
  const [reactionResult, statusResult] = await Promise.allSettled([
68822
69133
  params.addReaction(),
@@ -68838,7 +69149,38 @@ async function activateSlackProcessingDecoration(params) {
68838
69149
  throw statusResult.reason;
68839
69150
  }
68840
69151
  }
69152
+ let statusRefreshTimer;
69153
+ let closed = false;
69154
+ let refreshInFlight = false;
69155
+ let refreshPromise;
69156
+ if (statusApplied) {
69157
+ const refreshIntervalMs = Math.max(0, params.statusRefreshIntervalMs ?? DEFAULT_STATUS_REFRESH_INTERVAL_MS);
69158
+ if (refreshIntervalMs > 0) {
69159
+ statusRefreshTimer = setInterval(() => {
69160
+ if (closed || refreshInFlight) {
69161
+ return;
69162
+ }
69163
+ refreshInFlight = true;
69164
+ refreshPromise = params.setStatus().then(() => {
69165
+ return;
69166
+ }).catch((error) => {
69167
+ if (!closed) {
69168
+ params.onUnexpectedError?.("refresh-status", error);
69169
+ }
69170
+ }).finally(() => {
69171
+ refreshInFlight = false;
69172
+ refreshPromise = undefined;
69173
+ });
69174
+ }, refreshIntervalMs);
69175
+ }
69176
+ }
68841
69177
  return async () => {
69178
+ closed = true;
69179
+ if (statusRefreshTimer) {
69180
+ clearInterval(statusRefreshTimer);
69181
+ statusRefreshTimer = undefined;
69182
+ }
69183
+ await refreshPromise;
68842
69184
  if (reactionApplied) {
68843
69185
  try {
68844
69186
  await params.removeReaction();
@@ -69736,20 +70078,21 @@ class SlackSocketService {
69736
70078
  });
69737
70079
  if (!allowed) {
69738
70080
  if (dmConfig.policy === "pairing") {
69739
- const { code, created } = await upsertChannelPairingRequest({
70081
+ const pairingRequest = await upsertChannelPairingRequest({
69740
70082
  channel: "slack",
69741
70083
  id: directUserId
69742
70084
  });
69743
- if (created && code) {
70085
+ const pairingReply = buildPairingReplyFromRequest({
70086
+ channel: "slack",
70087
+ idLine: `Your Slack user id: ${directUserId}`,
70088
+ pairingRequest
70089
+ });
70090
+ if (pairingReply) {
69744
70091
  try {
69745
70092
  await postSlackText(this.app.client, {
69746
70093
  channel: channelId,
69747
70094
  threadTs: directReplyThreadTs,
69748
- text: buildPairingReply({
69749
- channel: "slack",
69750
- idLine: `Your Slack user id: ${directUserId}`,
69751
- code
69752
- })
70095
+ text: pairingReply
69753
70096
  });
69754
70097
  } catch (error) {
69755
70098
  console.error("slack pairing reply failed", error);
@@ -71344,7 +71687,7 @@ class TelegramPollingService {
71344
71687
  });
71345
71688
  if (!allowed) {
71346
71689
  if (directMessages.policy === "pairing") {
71347
- const { code, created } = await upsertChannelPairingRequest({
71690
+ const pairingRequest = await upsertChannelPairingRequest({
71348
71691
  channel: "telegram",
71349
71692
  id: senderId,
71350
71693
  meta: {
@@ -71353,15 +71696,16 @@ class TelegramPollingService {
71353
71696
  lastName: message.from?.last_name
71354
71697
  }
71355
71698
  });
71356
- if (created && code) {
71699
+ const pairingReply = buildPairingReplyFromRequest({
71700
+ channel: "telegram",
71701
+ idLine: `Your Telegram user id: ${senderId}`,
71702
+ pairingRequest
71703
+ });
71704
+ if (pairingReply) {
71357
71705
  try {
71358
71706
  await callTelegramApi(this.accountConfig.botToken, "sendMessage", {
71359
71707
  chat_id: message.chat.id,
71360
- text: buildPairingReply({
71361
- channel: "telegram",
71362
- idLine: `Your Telegram user id: ${senderId}`,
71363
- code
71364
- })
71708
+ text: pairingReply
71365
71709
  });
71366
71710
  } catch (error) {
71367
71711
  console.error("telegram pairing reply failed", error);
@@ -73035,10 +73379,17 @@ function renderAccountsHelp() {
73035
73379
  "",
73036
73380
  "Usage:",
73037
73381
  " clisbot accounts --help",
73382
+ " clisbot accounts help",
73038
73383
  " clisbot accounts add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
73039
73384
  " clisbot accounts add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
73040
73385
  " clisbot accounts persist --channel <slack|telegram> --account <id>",
73041
- " clisbot accounts persist --all"
73386
+ " clisbot accounts persist --all",
73387
+ "",
73388
+ "Notes:",
73389
+ " - env-style input such as `TELEGRAM_BOT_TOKEN` or `${TELEGRAM_BOT_TOKEN}` keeps the account env-backed in config",
73390
+ " - literal token input without `--persist` stays runtime-only and requires a running clisbot runtime",
73391
+ " - `--persist` writes canonical token files so later plain `clisbot start` can reuse the account safely",
73392
+ " - `persist --all` converts every configured `credentialType=mem` account into canonical token files"
73042
73393
  ].join(`
73043
73394
  `);
73044
73395
  }
@@ -73215,7 +73566,7 @@ async function runAccountsCli(args, deps = {}) {
73215
73566
  ...deps
73216
73567
  };
73217
73568
  const action = args[0];
73218
- if (!action || action === "--help" || action === "-h") {
73569
+ if (!action || action === "--help" || action === "-h" || action === "help") {
73219
73570
  console.log(renderAccountsHelp());
73220
73571
  return;
73221
73572
  }
@@ -73319,6 +73670,7 @@ function renderAuthCliHelp() {
73319
73670
  " add-user/remove-user mutate roles.<role>.users",
73320
73671
  " add-permission/remove-permission mutate roles.<role>.allow",
73321
73672
  " agent role edits clone the inherited agent-defaults role into the target agent override on first write",
73673
+ " app `owner` and `admin` principals bypass DM pairing automatically once they are granted",
73322
73674
  "",
73323
73675
  "Examples:",
73324
73676
  " clisbot auth add-user app --role owner --user telegram:1276408333",
@@ -73581,6 +73933,7 @@ function renderChannelsHelp() {
73581
73933
  " - Slack private groups need channels.slack.groups.<groupId>",
73582
73934
  " - Telegram groups need channels.telegram.groups.<chatId>",
73583
73935
  " - Telegram forum topics need channels.telegram.groups.<chatId>.topics.<topicId>",
73936
+ " - route adds for Slack channels, Slack groups, Telegram groups, and Telegram topics default to `requireMention: true` unless you pass `--require-mention false`",
73584
73937
  " - Adding a route puts that surface on the allowlist; other channels, groups, or topics still need to be added explicitly",
73585
73938
  " - Tune route settings such as requireMention and followUp in clisbot.json when a surface should behave differently",
73586
73939
  ` - Manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`,
@@ -73659,6 +74012,7 @@ function renderRouteAddGuidance(params) {
73659
74012
  console.log(` - route added: ${routePath}`);
73660
74013
  console.log(" - direct messages still follow channels.slack.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
73661
74014
  console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention and followUp`);
74015
+ console.log(" - new Slack channel/group routes default to `requireMention: true` unless you passed `--require-mention false`");
73662
74016
  console.log(" - if you want pairing-style access control for DMs, set channels.slack.directMessages.policy to `pairing`");
73663
74017
  console.log(" - if you want stricter route access, keep Slack groups on allowlist and only add the channels/groups you trust");
73664
74018
  console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
@@ -73669,6 +74023,7 @@ function renderRouteAddGuidance(params) {
73669
74023
  console.log(` - route added: ${routePath}`);
73670
74024
  console.log(" - direct messages still follow channels.telegram.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
73671
74025
  console.log(` - this ${params.kind} is now on the Telegram allowlist; other groups or topics still need to be added explicitly`);
74026
+ console.log(" - new Telegram group/topic routes default to `requireMention: true` unless you passed `--require-mention false`");
73672
74027
  console.log(" - if you want pairing-style access control for DMs, set channels.telegram.directMessages.policy to `pairing`");
73673
74028
  console.log(" - tune route settings such as requireMention and followUp in clisbot.json if this surface should behave differently");
73674
74029
  console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
@@ -73867,7 +74222,7 @@ async function addSlackRoute(kind, args) {
73867
74222
  }
73868
74223
  const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
73869
74224
  const agentId = getAgentId(args);
73870
- const requireMention = parseBooleanOption(args, "--require-mention", false);
74225
+ const requireMention = parseBooleanOption(args, "--require-mention", true);
73871
74226
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
73872
74227
  target[routeId] = {
73873
74228
  ...target[routeId] ?? {},
@@ -75098,6 +75453,41 @@ function assertSupportedPlatform(command) {
75098
75453
  }
75099
75454
 
75100
75455
  // src/control/runtime-bootstrap-cli.ts
75456
+ function hasHelpFlag(args) {
75457
+ return args.includes("--help") || args.includes("-h") || args.includes("help");
75458
+ }
75459
+ function renderBootstrapCommandHelp(commandName) {
75460
+ const behavior = commandName === "start" ? "seed config if needed and start the detached runtime" : "seed config and optionally bootstrap the first agent without starting runtime";
75461
+ return [
75462
+ `clisbot ${commandName}`,
75463
+ "",
75464
+ "Usage:",
75465
+ ` clisbot ${commandName} --help`,
75466
+ ` clisbot ${commandName} [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]`,
75467
+ " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
75468
+ " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
75469
+ "",
75470
+ "Behavior:",
75471
+ ` - ${behavior}`,
75472
+ " - first-run agent bootstrap needs both `--cli` and `--bot-type`",
75473
+ " - `--bot-type personal` maps to `personal-assistant`; `--bot-type team` maps to `team-assistant`",
75474
+ " - explicit credential flags only enable the channels and accounts you named in this command",
75475
+ " - env-style values such as `SLACK_APP_TOKEN` or `${SLACK_APP_TOKEN}` stay env-backed in config",
75476
+ commandName === "start" ? " - literal token values without `--persist` stay runtime-only for this start invocation" : " - literal token values on `init` require `--persist` because no runtime exists yet",
75477
+ " - `--persist` writes canonical credential files so later plain `clisbot start` can reuse them",
75478
+ "",
75479
+ "Examples:",
75480
+ ` clisbot ${commandName} --cli codex --bot-type personal --telegram-bot-token TELEGRAM_BOT_TOKEN`,
75481
+ ` clisbot ${commandName} --cli codex --bot-type team --slack-app-token SLACK_APP_TOKEN --slack-bot-token SLACK_BOT_TOKEN`,
75482
+ ` clisbot ${commandName} --cli gemini --bot-type personal --telegram-bot-token "$TELEGRAM_BOT_TOKEN" --persist`,
75483
+ "",
75484
+ "Related help:",
75485
+ " - `clisbot agents --help` for lower-level agent bootstrap and binding control",
75486
+ " - `clisbot accounts --help` for account persistence after first run",
75487
+ " - `clisbot channels --help` for route setup after bootstrap"
75488
+ ].join(`
75489
+ `);
75490
+ }
75101
75491
  function getPrimaryWorkspacePath(summary) {
75102
75492
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
75103
75493
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -75314,6 +75704,10 @@ async function printStartedRuntimeSummary(pid, configPath, logPath) {
75314
75704
  }
75315
75705
  }
75316
75706
  async function initConfig(args = []) {
75707
+ if (hasHelpFlag(args)) {
75708
+ console.log(renderBootstrapCommandHelp("init"));
75709
+ return;
75710
+ }
75317
75711
  const state = await prepareBootstrapState(args, "init");
75318
75712
  if (!state) {
75319
75713
  return;
@@ -75332,6 +75726,10 @@ async function initConfig(args = []) {
75332
75726
  }
75333
75727
  }
75334
75728
  async function start(args = []) {
75729
+ if (hasHelpFlag(args)) {
75730
+ console.log(renderBootstrapCommandHelp("start"));
75731
+ return;
75732
+ }
75335
75733
  const runtimeStatus = await getRuntimeStatus();
75336
75734
  const bootstrapFlags = parseBootstrapFlags(args);
75337
75735
  const restartForLiteralBootstrap = runtimeStatus.running && hasLiteralMemCredentials(bootstrapFlags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",
@@ -90,7 +90,7 @@ These are broad or legacy-looking for the current `clisbot` Slack design and sho
90
90
  | `function_executed` event | Not consumed by current code |
91
91
  | `app_home.messages_tab_enabled` and related App Home surface config | Not required for current chat-routing behavior |
92
92
  | `org_deploy_enabled` | Not needed for the current local or workspace-level setup story |
93
- | `hermes_app_type` and `function_runtime` | Not needed for the current Slack Socket Mode bot |
93
+ | `function_runtime` | Not needed for the current Slack Socket Mode bot |
94
94
 
95
95
  ## Practical Recommendation
96
96
 
@@ -51,7 +51,6 @@
51
51
  },
52
52
  "org_deploy_enabled": false,
53
53
  "socket_mode_enabled": true,
54
- "token_rotation_enabled": false,
55
- "hermes_app_type": "remote"
54
+ "token_rotation_enabled": false
56
55
  }
57
56
  }