clisbot 0.1.11 → 0.1.15

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
@@ -47740,10 +47740,10 @@ var require_view2 = __commonJS((exports, module) => {
47740
47740
  var debug = require_src()("express:view");
47741
47741
  var path2 = __require("node:path");
47742
47742
  var fs2 = __require("node:fs");
47743
- var dirname12 = path2.dirname;
47743
+ var dirname13 = path2.dirname;
47744
47744
  var basename = path2.basename;
47745
47745
  var extname = path2.extname;
47746
- var join8 = path2.join;
47746
+ var join9 = path2.join;
47747
47747
  var resolve = path2.resolve;
47748
47748
  module.exports = View;
47749
47749
  function View(name, options) {
@@ -47779,7 +47779,7 @@ var require_view2 = __commonJS((exports, module) => {
47779
47779
  for (var i = 0;i < roots.length && !path3; i++) {
47780
47780
  var root = roots[i];
47781
47781
  var loc = resolve(root, name);
47782
- var dir = dirname12(loc);
47782
+ var dir = dirname13(loc);
47783
47783
  var file = basename(loc);
47784
47784
  path3 = this.resolve(dir, file);
47785
47785
  }
@@ -47805,12 +47805,12 @@ var require_view2 = __commonJS((exports, module) => {
47805
47805
  };
47806
47806
  View.prototype.resolve = function resolve2(dir, file) {
47807
47807
  var ext = this.ext;
47808
- var path3 = join8(dir, file);
47808
+ var path3 = join9(dir, file);
47809
47809
  var stat = tryStat(path3);
47810
47810
  if (stat && stat.isFile()) {
47811
47811
  return path3;
47812
47812
  }
47813
- path3 = join8(dir, basename(file, ext), "index" + ext);
47813
+ path3 = join9(dir, basename(file, ext), "index" + ext);
47814
47814
  stat = tryStat(path3);
47815
47815
  if (stat && stat.isFile()) {
47816
47816
  return path3;
@@ -51153,7 +51153,7 @@ var require_send = __commonJS((exports, module) => {
51153
51153
  var Stream = __require("stream");
51154
51154
  var util3 = __require("util");
51155
51155
  var extname = path2.extname;
51156
- var join8 = path2.join;
51156
+ var join9 = path2.join;
51157
51157
  var normalize = path2.normalize;
51158
51158
  var resolve = path2.resolve;
51159
51159
  var sep2 = path2.sep;
@@ -51325,7 +51325,7 @@ var require_send = __commonJS((exports, module) => {
51325
51325
  return res;
51326
51326
  }
51327
51327
  parts = path3.split(sep2);
51328
- path3 = normalize(join8(root, path3));
51328
+ path3 = normalize(join9(root, path3));
51329
51329
  } else {
51330
51330
  if (UP_PATH_REGEXP.test(path3)) {
51331
51331
  debug('malicious path "%s"', path3);
@@ -51465,7 +51465,7 @@ var require_send = __commonJS((exports, module) => {
51465
51465
  return self2.onStatError(err);
51466
51466
  return self2.error(404);
51467
51467
  }
51468
- var p = join8(path3, self2._index[i]);
51468
+ var p = join9(path3, self2._index[i]);
51469
51469
  debug('stat "%s"', p);
51470
51470
  fs2.stat(p, function(err2, stat) {
51471
51471
  if (err2)
@@ -54225,7 +54225,11 @@ function resolveSlackCredential(params) {
54225
54225
  function materializeRuntimeChannelCredentials(config, options = {}) {
54226
54226
  const env = options.env ?? process.env;
54227
54227
  const nextConfig = structuredClone(config);
54228
- if (nextConfig.channels.telegram.enabled) {
54228
+ const materializeChannels = options.materializeChannels ?? [];
54229
+ const materializeAll = materializeChannels.length === 0;
54230
+ const shouldMaterializeTelegram = materializeAll || materializeChannels.includes("telegram");
54231
+ const shouldMaterializeSlack = materializeAll || materializeChannels.includes("slack");
54232
+ if (shouldMaterializeTelegram && nextConfig.channels.telegram.enabled) {
54229
54233
  const accountIds = Object.keys(getAccountsRecord(nextConfig.channels.telegram.accounts));
54230
54234
  const ids = accountIds.length > 0 ? accountIds : [getConfiguredDefaultAccountId({
54231
54235
  defaultAccount: nextConfig.channels.telegram.defaultAccount,
@@ -54263,7 +54267,7 @@ function materializeRuntimeChannelCredentials(config, options = {}) {
54263
54267
  const fallbackTelegramAccountId = preferredDefaultTelegramAccountId && resolvedAccounts[preferredDefaultTelegramAccountId] ? preferredDefaultTelegramAccountId : Object.keys(resolvedAccounts)[0];
54264
54268
  nextConfig.channels.telegram.botToken = fallbackTelegramAccountId ? resolvedAccounts[fallbackTelegramAccountId]?.botToken ?? "" : "";
54265
54269
  }
54266
- if (nextConfig.channels.slack.enabled) {
54270
+ if (shouldMaterializeSlack && nextConfig.channels.slack.enabled) {
54267
54271
  const accountIds = Object.keys(getAccountsRecord(nextConfig.channels.slack.accounts));
54268
54272
  const ids = accountIds.length > 0 ? accountIds : [getConfiguredDefaultAccountId({
54269
54273
  defaultAccount: nextConfig.channels.slack.defaultAccount,
@@ -54361,53 +54365,6 @@ function getConfigReloadMtimeMs(configPath) {
54361
54365
  return statSync(expandHomePath(configPath)).mtimeMs;
54362
54366
  }
54363
54367
 
54364
- // src/channels/privilege-help.ts
54365
- function renderGenericPrivilegeCommandHelpLines(prefix = "") {
54366
- return [
54367
- `${prefix}Privilege command setup:`,
54368
- `${prefix} - enable for a Slack route: \`clisbot channels privilege enable slack-channel <channelId>\` or \`clisbot channels privilege enable slack-group <groupId>\``,
54369
- `${prefix} - allow a Slack user: \`clisbot channels privilege allow-user slack-channel <channelId> <userId>\``,
54370
- `${prefix} - enable Slack DM privilege commands: \`clisbot channels privilege enable slack-dm\``,
54371
- `${prefix} - allow a Slack DM user: \`clisbot channels privilege allow-user slack-dm <userId>\``,
54372
- `${prefix} - enable for a Telegram route: \`clisbot channels privilege enable telegram-group <chatId> [--topic <topicId>]\``,
54373
- `${prefix} - allow a Telegram user: \`clisbot channels privilege allow-user telegram-group <chatId> <userId> [--topic <topicId>]\``,
54374
- `${prefix} - enable Telegram DM privilege commands: \`clisbot channels privilege enable telegram-dm\``,
54375
- `${prefix} - allow a Telegram DM user: \`clisbot channels privilege allow-user telegram-dm <userId>\``
54376
- ];
54377
- }
54378
- function renderPrivilegeCommandHelpLines(identity, prefix = "") {
54379
- const target = buildPrivilegeCommandTarget(identity);
54380
- if (!target) {
54381
- return [];
54382
- }
54383
- const allowUserSuffix = identity.senderId ? ` ${identity.senderId}` : " <userId>";
54384
- return [
54385
- `${prefix}Operator commands:`,
54386
- `${prefix} - enable privilege commands: \`clisbot channels privilege enable ${target}\``,
54387
- `${prefix} - allow this user: \`clisbot channels privilege allow-user ${target}${allowUserSuffix}\``,
54388
- `${prefix} - disable privilege commands: \`clisbot channels privilege disable ${target}\``,
54389
- `${prefix} - remove this user: \`clisbot channels privilege remove-user ${target}${allowUserSuffix}\``
54390
- ];
54391
- }
54392
- function buildPrivilegeCommandTarget(identity) {
54393
- if (identity.platform === "slack") {
54394
- if (identity.conversationKind === "dm") {
54395
- return "slack-dm";
54396
- }
54397
- if (identity.conversationKind === "group") {
54398
- return identity.channelId ? `slack-group ${identity.channelId}` : null;
54399
- }
54400
- return identity.channelId ? `slack-channel ${identity.channelId}` : null;
54401
- }
54402
- if (identity.conversationKind === "dm") {
54403
- return "telegram-dm";
54404
- }
54405
- if (!identity.chatId) {
54406
- return null;
54407
- }
54408
- return identity.conversationKind === "topic" && identity.topicId ? `telegram-group ${identity.chatId} --topic ${identity.topicId}` : `telegram-group ${identity.chatId}`;
54409
- }
54410
-
54411
54368
  // src/control/startup-bootstrap.ts
54412
54369
  var CHANNEL_ACCOUNT_DOC_PATH = "docs/user-guide/channel-accounts.md";
54413
54370
  var USER_GUIDE_DOC_PATH = "docs/user-guide/README.md";
@@ -54532,7 +54489,7 @@ function renderTmuxDebugHelpLines(prefix = "") {
54532
54489
  `${prefix} - attach to a session: \`tmux -S ${socketPath} attach -t <session-name>\``
54533
54490
  ];
54534
54491
  }
54535
- function renderChannelSetupHelpLines(prefix = "", options = {}) {
54492
+ function renderChannelSetupHelpLines(prefix = "", _options = {}) {
54536
54493
  return [
54537
54494
  `${prefix}Channel setup docs: ${CHANNEL_ACCOUNT_DOC_PATH}`,
54538
54495
  `${prefix}Operator guide: ${USER_GUIDE_DOC_PATH}`,
@@ -54542,7 +54499,6 @@ function renderChannelSetupHelpLines(prefix = "", options = {}) {
54542
54499
  telegramDirectMessagesPolicy: "pairing"
54543
54500
  }),
54544
54501
  ...renderTmuxDebugHelpLines(prefix),
54545
- ...options.includePrivilegeHelp === false ? [] : renderGenericPrivilegeCommandHelpLines(prefix),
54546
54502
  ...renderRepoHelpLines(prefix)
54547
54503
  ];
54548
54504
  }
@@ -54670,7 +54626,7 @@ function renderCliHelp() {
54670
54626
  " Fresh bootstrap only enables channels named by flags; ambient env vars alone do not auto-enable extra channels.",
54671
54627
  "",
54672
54628
  "Usage:",
54673
- " clisbot start [--cli <codex|claude>] [--bot-type <personal|team>] [--persist]",
54629
+ " clisbot start [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54674
54630
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54675
54631
  " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54676
54632
  " clisbot restart",
@@ -54684,7 +54640,7 @@ function renderCliHelp() {
54684
54640
  " clisbot message <subcommand>",
54685
54641
  " clisbot agents <subcommand>",
54686
54642
  " clisbot pairing <subcommand>",
54687
- " clisbot init [--cli <codex|claude>] [--bot-type <personal|team>] [--persist]",
54643
+ " clisbot init [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54688
54644
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54689
54645
  " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
54690
54646
  " clis <same-command>",
@@ -55104,7 +55060,7 @@ async function runPairingCli(args, writer = console) {
55104
55060
  }
55105
55061
 
55106
55062
  // src/config/agent-tool-presets.ts
55107
- var SUPPORTED_AGENT_CLI_TOOLS = ["codex", "claude"];
55063
+ var SUPPORTED_AGENT_CLI_TOOLS = ["codex", "claude", "gemini"];
55108
55064
  var SUPPORTED_BOOTSTRAP_MODES = ["personal-assistant", "team-assistant"];
55109
55065
  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
55066
  var DEFAULT_AGENT_TOOL_TEMPLATES = {
@@ -55169,6 +55125,41 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
55169
55125
  ]
55170
55126
  }
55171
55127
  }
55128
+ },
55129
+ gemini: {
55130
+ command: "gemini",
55131
+ startupOptions: ["--approval-mode=yolo", "--sandbox=false"],
55132
+ trustWorkspace: true,
55133
+ startupDelayMs: 15000,
55134
+ startupReadyPattern: "Type your message or @path/to/file",
55135
+ startupBlockers: [
55136
+ {
55137
+ pattern: "Please visit the following URL to authorize the application|Enter the authorization code:",
55138
+ 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."
55139
+ },
55140
+ {
55141
+ 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",
55142
+ 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."
55143
+ }
55144
+ ],
55145
+ promptSubmitDelayMs: 200,
55146
+ sessionId: {
55147
+ create: {
55148
+ mode: "runner",
55149
+ args: []
55150
+ },
55151
+ capture: {
55152
+ mode: "status-command",
55153
+ statusCommand: "/stats session",
55154
+ pattern: SESSION_ID_PATTERN,
55155
+ timeoutMs: 8000,
55156
+ pollIntervalMs: 250
55157
+ },
55158
+ resume: {
55159
+ mode: "command",
55160
+ args: ["--resume", "{sessionId}", "--approval-mode=yolo", "--sandbox=false"]
55161
+ }
55162
+ }
55172
55163
  }
55173
55164
  };
55174
55165
  function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
@@ -55179,6 +55170,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55179
55170
  args: [...options, "-C", "{workspace}"],
55180
55171
  trustWorkspace: template.trustWorkspace,
55181
55172
  startupDelayMs: template.startupDelayMs,
55173
+ startupReadyPattern: template.startupReadyPattern,
55174
+ startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55182
55175
  promptSubmitDelayMs: template.promptSubmitDelayMs,
55183
55176
  sessionId: {
55184
55177
  ...template.sessionId,
@@ -55201,6 +55194,8 @@ function buildRunnerFromToolTemplate(toolId, template, startupOptions) {
55201
55194
  args: [...options],
55202
55195
  trustWorkspace: template.trustWorkspace,
55203
55196
  startupDelayMs: template.startupDelayMs,
55197
+ startupReadyPattern: template.startupReadyPattern,
55198
+ startupBlockers: template.startupBlockers?.map((entry) => ({ ...entry })),
55204
55199
  promptSubmitDelayMs: template.promptSubmitDelayMs,
55205
55200
  sessionId: {
55206
55201
  ...template.sessionId,
@@ -55229,6 +55224,9 @@ function inferAgentCliToolId(command) {
55229
55224
  if (trimmed === "claude") {
55230
55225
  return "claude";
55231
55226
  }
55227
+ if (trimmed === "gemini") {
55228
+ return "gemini";
55229
+ }
55232
55230
  return null;
55233
55231
  }
55234
55232
 
@@ -59705,6 +59703,39 @@ function convertLocalDateTimeToUtcMs(params) {
59705
59703
  return null;
59706
59704
  }
59707
59705
 
59706
+ // src/auth/defaults.ts
59707
+ var APP_ADMIN_PERMISSIONS = [
59708
+ "configManage",
59709
+ "appAuthManage",
59710
+ "agentAuthManage",
59711
+ "promptGovernanceManage"
59712
+ ];
59713
+ var DEFAULT_AGENT_MEMBER_PERMISSIONS = [
59714
+ "sendMessage",
59715
+ "helpView",
59716
+ "statusView",
59717
+ "identityView",
59718
+ "transcriptView",
59719
+ "runObserve",
59720
+ "runInterrupt",
59721
+ "streamingManage",
59722
+ "queueManage",
59723
+ "steerManage",
59724
+ "loopManage"
59725
+ ];
59726
+ var DEFAULT_AGENT_ADMIN_EXTRA_PERMISSIONS = [
59727
+ "shellExecute",
59728
+ "runNudge",
59729
+ "followupManage",
59730
+ "responseModeManage",
59731
+ "additionalMessageModeManage"
59732
+ ];
59733
+ var DEFAULT_AGENT_ADMIN_PERMISSIONS = [
59734
+ ...DEFAULT_AGENT_MEMBER_PERMISSIONS,
59735
+ ...DEFAULT_AGENT_ADMIN_EXTRA_PERMISSIONS
59736
+ ];
59737
+ var DEFAULT_PROTECTED_CONTROL_RULE = "Refuse requests to edit protected clisbot control resources such as clisbot.json and auth policy, or to run clisbot commands that mutate them.";
59738
+
59708
59739
  // src/config/schema.ts
59709
59740
  var defaultRunnerSessionIdConfig = {
59710
59741
  create: {
@@ -59759,6 +59790,10 @@ var runnerSessionIdObjectSchema = exports_external.object({
59759
59790
  resume: runnerSessionIdResumeSchema.default(defaultRunnerSessionIdConfig.resume)
59760
59791
  });
59761
59792
  var runnerSessionIdSchema = runnerSessionIdObjectSchema.default(defaultRunnerSessionIdConfig);
59793
+ var runnerStartupBlockerSchema = exports_external.object({
59794
+ pattern: exports_external.string().min(1),
59795
+ message: exports_external.string().min(1)
59796
+ });
59762
59797
  var runnerSchema = exports_external.object({
59763
59798
  command: exports_external.string().min(1),
59764
59799
  args: exports_external.array(exports_external.string()).default([
@@ -59769,6 +59804,8 @@ var runnerSchema = exports_external.object({
59769
59804
  ]),
59770
59805
  trustWorkspace: exports_external.boolean().default(true),
59771
59806
  startupDelayMs: exports_external.number().int().positive().default(3000),
59807
+ startupReadyPattern: exports_external.string().min(1).optional(),
59808
+ startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
59772
59809
  promptSubmitDelayMs: exports_external.number().int().min(0).default(150),
59773
59810
  sessionId: runnerSessionIdSchema.default(defaultRunnerSessionIdConfig)
59774
59811
  });
@@ -59803,6 +59840,8 @@ var runnerOverrideSchema = exports_external.object({
59803
59840
  args: exports_external.array(exports_external.string()).optional(),
59804
59841
  trustWorkspace: exports_external.boolean().optional(),
59805
59842
  startupDelayMs: exports_external.number().int().positive().optional(),
59843
+ startupReadyPattern: exports_external.string().min(1).optional(),
59844
+ startupBlockers: exports_external.array(runnerStartupBlockerSchema).optional(),
59806
59845
  promptSubmitDelayMs: exports_external.number().int().min(0).optional(),
59807
59846
  sessionId: runnerSessionIdObjectSchema.partial().extend({
59808
59847
  create: runnerSessionIdCreateSchema.partial().optional(),
@@ -59813,10 +59852,46 @@ var runnerOverrideSchema = exports_external.object({
59813
59852
  var agentBootstrapSchema = exports_external.object({
59814
59853
  mode: exports_external.enum(SUPPORTED_BOOTSTRAP_MODES).default("personal-assistant")
59815
59854
  });
59855
+ var authRoleSchema = exports_external.object({
59856
+ allow: exports_external.array(exports_external.string().min(1)).default([]),
59857
+ users: exports_external.array(exports_external.string().min(1)).default([])
59858
+ });
59859
+ var appAuthSchema = exports_external.object({
59860
+ ownerClaimWindowMinutes: exports_external.number().int().positive().default(30),
59861
+ defaultRole: exports_external.string().min(1).default("member"),
59862
+ roles: exports_external.record(exports_external.string(), authRoleSchema).default({
59863
+ owner: {
59864
+ allow: [...APP_ADMIN_PERMISSIONS],
59865
+ users: []
59866
+ },
59867
+ admin: {
59868
+ allow: [...APP_ADMIN_PERMISSIONS],
59869
+ users: []
59870
+ },
59871
+ member: {
59872
+ allow: [],
59873
+ users: []
59874
+ }
59875
+ })
59876
+ });
59877
+ var agentAuthSchema = exports_external.object({
59878
+ defaultRole: exports_external.string().min(1).default("member"),
59879
+ roles: exports_external.record(exports_external.string(), authRoleSchema).default({
59880
+ admin: {
59881
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
59882
+ users: []
59883
+ },
59884
+ member: {
59885
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
59886
+ users: []
59887
+ }
59888
+ })
59889
+ });
59816
59890
  var agentOverrideSchema = exports_external.object({
59817
59891
  workspace: exports_external.string().optional(),
59818
59892
  responseMode: exports_external.enum(["capture-pane", "message-tool"]).optional(),
59819
59893
  additionalMessageMode: exports_external.enum(["queue", "steer"]).optional(),
59894
+ auth: agentAuthSchema.optional(),
59820
59895
  runner: runnerOverrideSchema.optional(),
59821
59896
  stream: streamSchema.partial().optional(),
59822
59897
  session: sessionSchema.partial().optional()
@@ -59831,6 +59906,19 @@ var agentEntrySchema = agentOverrideSchema.extend({
59831
59906
  });
59832
59907
  var agentDefaultsSchema = exports_external.object({
59833
59908
  workspace: exports_external.string().default("~/.clisbot/workspaces/{agentId}"),
59909
+ auth: agentAuthSchema.default({
59910
+ defaultRole: "member",
59911
+ roles: {
59912
+ admin: {
59913
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
59914
+ users: []
59915
+ },
59916
+ member: {
59917
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
59918
+ users: []
59919
+ }
59920
+ }
59921
+ }),
59834
59922
  runner: runnerSchema.default({
59835
59923
  command: "codex",
59836
59924
  args: [
@@ -59897,6 +59985,7 @@ var channelAgentPromptSchema = exports_external.object({
59897
59985
  });
59898
59986
  var channelResponseModeSchema = exports_external.enum(["capture-pane", "message-tool"]);
59899
59987
  var channelAdditionalMessageModeSchema = exports_external.enum(["queue", "steer"]);
59988
+ var channelVerboseSchema = exports_external.enum(["off", "minimal"]);
59900
59989
  var timezoneSchema = exports_external.string().refine(isValidLoopTimezone, {
59901
59990
  message: "Expected a valid IANA timezone such as Asia/Ho_Chi_Minh"
59902
59991
  });
@@ -59910,6 +59999,7 @@ var slackRouteSchema = exports_external.object({
59910
59999
  response: slackResponseSchema.optional(),
59911
60000
  responseMode: channelResponseModeSchema.optional(),
59912
60001
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60002
+ verbose: channelVerboseSchema.optional(),
59913
60003
  followUp: slackFollowUpOverrideSchema.optional(),
59914
60004
  timezone: timezoneSchema.optional()
59915
60005
  });
@@ -59923,6 +60013,7 @@ var telegramTopicRouteSchema = exports_external.object({
59923
60013
  response: slackResponseSchema.optional(),
59924
60014
  responseMode: channelResponseModeSchema.optional(),
59925
60015
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60016
+ verbose: channelVerboseSchema.optional(),
59926
60017
  followUp: slackFollowUpOverrideSchema.optional(),
59927
60018
  timezone: timezoneSchema.optional()
59928
60019
  });
@@ -59936,6 +60027,7 @@ var telegramGroupRouteSchema = exports_external.object({
59936
60027
  response: slackResponseSchema.optional(),
59937
60028
  responseMode: channelResponseModeSchema.optional(),
59938
60029
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60030
+ verbose: channelVerboseSchema.optional(),
59939
60031
  followUp: slackFollowUpOverrideSchema.optional(),
59940
60032
  timezone: timezoneSchema.optional(),
59941
60033
  topics: exports_external.record(exports_external.string(), telegramTopicRouteSchema).default({})
@@ -59953,6 +60045,7 @@ var telegramDirectMessagesSchema = exports_external.object({
59953
60045
  response: slackResponseSchema.optional(),
59954
60046
  responseMode: channelResponseModeSchema.optional(),
59955
60047
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60048
+ verbose: channelVerboseSchema.optional(),
59956
60049
  followUp: slackFollowUpOverrideSchema.optional(),
59957
60050
  timezone: timezoneSchema.optional()
59958
60051
  });
@@ -59992,6 +60085,7 @@ var telegramSchema = exports_external.object({
59992
60085
  response: slackResponseSchema.default("final"),
59993
60086
  responseMode: channelResponseModeSchema.default("message-tool"),
59994
60087
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60088
+ verbose: channelVerboseSchema.default("minimal"),
59995
60089
  followUp: slackFollowUpSchema.default({
59996
60090
  mode: "auto",
59997
60091
  participationTtlMin: 5
@@ -60022,6 +60116,7 @@ var directMessagesSchema = exports_external.object({
60022
60116
  response: slackResponseSchema.optional(),
60023
60117
  responseMode: channelResponseModeSchema.optional(),
60024
60118
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60119
+ verbose: channelVerboseSchema.optional(),
60025
60120
  followUp: slackFollowUpOverrideSchema.optional(),
60026
60121
  timezone: timezoneSchema.optional()
60027
60122
  });
@@ -60068,6 +60163,7 @@ var slackSchema = exports_external.object({
60068
60163
  response: slackResponseSchema.default("final"),
60069
60164
  responseMode: channelResponseModeSchema.default("message-tool"),
60070
60165
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60166
+ verbose: channelVerboseSchema.default("minimal"),
60071
60167
  followUp: slackFollowUpSchema.default({
60072
60168
  mode: "auto",
60073
60169
  participationTtlMin: 5
@@ -60127,6 +60223,45 @@ var clisbotConfigSchema = exports_external.object({
60127
60223
  identityLinks: {},
60128
60224
  storePath: "~/.clisbot/state/sessions.json"
60129
60225
  }),
60226
+ app: exports_external.object({
60227
+ auth: appAuthSchema.default({
60228
+ ownerClaimWindowMinutes: 30,
60229
+ defaultRole: "member",
60230
+ roles: {
60231
+ owner: {
60232
+ allow: [...APP_ADMIN_PERMISSIONS],
60233
+ users: []
60234
+ },
60235
+ admin: {
60236
+ allow: [...APP_ADMIN_PERMISSIONS],
60237
+ users: []
60238
+ },
60239
+ member: {
60240
+ allow: [],
60241
+ users: []
60242
+ }
60243
+ }
60244
+ })
60245
+ }).default({
60246
+ auth: {
60247
+ ownerClaimWindowMinutes: 30,
60248
+ defaultRole: "member",
60249
+ roles: {
60250
+ owner: {
60251
+ allow: [...APP_ADMIN_PERMISSIONS],
60252
+ users: []
60253
+ },
60254
+ admin: {
60255
+ allow: [...APP_ADMIN_PERMISSIONS],
60256
+ users: []
60257
+ },
60258
+ member: {
60259
+ allow: [],
60260
+ users: []
60261
+ }
60262
+ }
60263
+ }
60264
+ }),
60130
60265
  agents: exports_external.object({
60131
60266
  defaults: agentDefaultsSchema,
60132
60267
  list: exports_external.array(agentEntrySchema).default([
@@ -60179,6 +60314,7 @@ var clisbotConfigSchema = exports_external.object({
60179
60314
  response: "final",
60180
60315
  responseMode: "message-tool",
60181
60316
  additionalMessageMode: "steer",
60317
+ verbose: "minimal",
60182
60318
  followUp: {
60183
60319
  mode: "auto",
60184
60320
  participationTtlMin: 5
@@ -60218,17 +60354,19 @@ function resolveMaxRuntimeMs(stream) {
60218
60354
  defaultMinutes: 15
60219
60355
  });
60220
60356
  }
60221
- async function loadConfig(configPath = getDefaultConfigPath()) {
60357
+ async function loadConfig(configPath = getDefaultConfigPath(), options = {}) {
60222
60358
  const expandedConfigPath = expandHomePath(configPath);
60223
60359
  const text = await readTextFile(expandedConfigPath);
60224
60360
  const parsed = JSON.parse(text);
60361
+ assertNoLegacyPrivilegeCommands(parsed);
60225
60362
  const withDynamicDefaults = clisbotConfigSchema.parse(applyDynamicPathDefaults(parsed));
60226
60363
  const substituted = resolveConfigEnvVars(withDynamicDefaults, process.env, {
60227
60364
  skipPaths: getCredentialSkipPaths(withDynamicDefaults)
60228
60365
  });
60229
60366
  const validated = clisbotConfigSchema.parse(substituted);
60230
60367
  const materialized = materializeRuntimeChannelCredentials(validated, {
60231
- env: process.env
60368
+ env: process.env,
60369
+ materializeChannels: options.materializeChannels
60232
60370
  });
60233
60371
  return materializeLoadedConfig(expandedConfigPath, materialized);
60234
60372
  }
@@ -60236,6 +60374,7 @@ async function loadConfigWithoutEnvResolution(configPath = getDefaultConfigPath(
60236
60374
  const expandedConfigPath = expandHomePath(configPath);
60237
60375
  const text = await readTextFile(expandedConfigPath);
60238
60376
  const parsed = JSON.parse(text);
60377
+ assertNoLegacyPrivilegeCommands(parsed);
60239
60378
  const validated = clisbotConfigSchema.parse(applyDynamicPathDefaults(parsed));
60240
60379
  return materializeLoadedConfig(expandedConfigPath, validated);
60241
60380
  }
@@ -60298,6 +60437,21 @@ function applyDynamicPathDefaults(parsed, env = process.env) {
60298
60437
  function isRecord2(value) {
60299
60438
  return typeof value === "object" && value !== null && !Array.isArray(value);
60300
60439
  }
60440
+ function assertNoLegacyPrivilegeCommands(value, path2 = "root") {
60441
+ if (Array.isArray(value)) {
60442
+ value.forEach((entry, index) => assertNoLegacyPrivilegeCommands(entry, `${path2}[${index}]`));
60443
+ return;
60444
+ }
60445
+ if (!isRecord2(value)) {
60446
+ return;
60447
+ }
60448
+ if (Object.prototype.hasOwnProperty.call(value, "privilegeCommands")) {
60449
+ throw new Error(`Unsupported config key at ${path2}.privilegeCommands. Move routed permissions to app.auth and agents.<id>.auth.`);
60450
+ }
60451
+ for (const [key, entry] of Object.entries(value)) {
60452
+ assertNoLegacyPrivilegeCommands(entry, `${path2}.${key}`);
60453
+ }
60454
+ }
60301
60455
  function getAgentEntry(config, agentId) {
60302
60456
  return config.raw.agents.list.find((entry) => entry.id === agentId);
60303
60457
  }
@@ -60334,9 +60488,42 @@ function renderDefaultConfigTemplate(options = {}) {
60334
60488
  identityLinks: {},
60335
60489
  storePath: sessionStorePath
60336
60490
  },
60491
+ app: {
60492
+ auth: {
60493
+ ownerClaimWindowMinutes: 30,
60494
+ defaultRole: "member",
60495
+ roles: {
60496
+ owner: {
60497
+ allow: [...APP_ADMIN_PERMISSIONS],
60498
+ users: []
60499
+ },
60500
+ admin: {
60501
+ allow: [...APP_ADMIN_PERMISSIONS],
60502
+ users: []
60503
+ },
60504
+ member: {
60505
+ allow: [],
60506
+ users: []
60507
+ }
60508
+ }
60509
+ }
60510
+ },
60337
60511
  agents: {
60338
60512
  defaults: {
60339
60513
  workspace: workspaceTemplate,
60514
+ auth: {
60515
+ defaultRole: "member",
60516
+ roles: {
60517
+ admin: {
60518
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
60519
+ users: []
60520
+ },
60521
+ member: {
60522
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
60523
+ users: []
60524
+ }
60525
+ }
60526
+ },
60340
60527
  runner: {
60341
60528
  command: "codex",
60342
60529
  args: [
@@ -60347,6 +60534,7 @@ function renderDefaultConfigTemplate(options = {}) {
60347
60534
  ],
60348
60535
  trustWorkspace: true,
60349
60536
  startupDelayMs: 3000,
60537
+ startupReadyPattern: undefined,
60350
60538
  promptSubmitDelayMs: 150,
60351
60539
  sessionId: {
60352
60540
  create: {
@@ -60435,10 +60623,6 @@ function renderDefaultConfigTemplate(options = {}) {
60435
60623
  channelPolicy: "allowlist",
60436
60624
  groupPolicy: "allowlist",
60437
60625
  defaultAgentId: "default",
60438
- privilegeCommands: {
60439
- enabled: false,
60440
- allowUsers: []
60441
- },
60442
60626
  commandPrefixes: {
60443
60627
  slash: ["::", "\\"],
60444
60628
  bash: ["!"]
@@ -60447,6 +60631,7 @@ function renderDefaultConfigTemplate(options = {}) {
60447
60631
  response: "final",
60448
60632
  responseMode: "message-tool",
60449
60633
  additionalMessageMode: "steer",
60634
+ verbose: "minimal",
60450
60635
  followUp: {
60451
60636
  mode: "auto",
60452
60637
  participationTtlMin: 5
@@ -60458,11 +60643,7 @@ function renderDefaultConfigTemplate(options = {}) {
60458
60643
  policy: "pairing",
60459
60644
  allowFrom: [],
60460
60645
  requireMention: false,
60461
- agentId: "default",
60462
- privilegeCommands: {
60463
- enabled: false,
60464
- allowUsers: []
60465
- }
60646
+ agentId: "default"
60466
60647
  }
60467
60648
  },
60468
60649
  telegram: {
@@ -60483,10 +60664,6 @@ function renderDefaultConfigTemplate(options = {}) {
60483
60664
  allowBots: false,
60484
60665
  groupPolicy: "allowlist",
60485
60666
  defaultAgentId: "default",
60486
- privilegeCommands: {
60487
- enabled: false,
60488
- allowUsers: []
60489
- },
60490
60667
  commandPrefixes: {
60491
60668
  slash: ["::", "\\"],
60492
60669
  bash: ["!"]
@@ -60495,6 +60672,7 @@ function renderDefaultConfigTemplate(options = {}) {
60495
60672
  response: "final",
60496
60673
  responseMode: "message-tool",
60497
60674
  additionalMessageMode: "steer",
60675
+ verbose: "minimal",
60498
60676
  followUp: {
60499
60677
  mode: "auto",
60500
60678
  participationTtlMin: 5
@@ -60510,11 +60688,7 @@ function renderDefaultConfigTemplate(options = {}) {
60510
60688
  allowFrom: [],
60511
60689
  requireMention: false,
60512
60690
  allowBots: false,
60513
- agentId: "default",
60514
- privilegeCommands: {
60515
- enabled: false,
60516
- allowUsers: []
60517
- }
60691
+ agentId: "default"
60518
60692
  }
60519
60693
  }
60520
60694
  }
@@ -60542,16 +60716,26 @@ async function readEditableConfig(configPath = getDefaultConfigPath()) {
60542
60716
  async function writeEditableConfig(configPath, config) {
60543
60717
  const expandedConfigPath = expandHomePath(configPath);
60544
60718
  await ensureDir2(dirname4(expandedConfigPath));
60545
- const nextConfig = {
60719
+ const nextConfig = stripLegacyPrivilegeCommands({
60546
60720
  ...config,
60547
60721
  meta: {
60548
60722
  ...config.meta,
60549
60723
  lastTouchedAt: new Date().toISOString()
60550
60724
  }
60551
- };
60725
+ });
60552
60726
  await writeTextFile(expandedConfigPath, `${JSON.stringify(nextConfig, null, 2)}
60553
60727
  `);
60554
60728
  }
60729
+ function stripLegacyPrivilegeCommands(value) {
60730
+ if (Array.isArray(value)) {
60731
+ return value.map((entry) => stripLegacyPrivilegeCommands(entry));
60732
+ }
60733
+ if (!value || typeof value !== "object") {
60734
+ return value;
60735
+ }
60736
+ const nextEntries = Object.entries(value).filter(([key]) => key !== "privilegeCommands").map(([key, entry]) => [key, stripLegacyPrivilegeCommands(entry)]);
60737
+ return Object.fromEntries(nextEntries);
60738
+ }
60555
60739
 
60556
60740
  // src/agents/bootstrap.ts
60557
60741
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -60575,7 +60759,8 @@ var CUSTOMIZED_TEMPLATE_DIR = join4(TEMPLATE_ROOT, "customized");
60575
60759
  var CUSTOMIZED_DEFAULT_TEMPLATE_DIR = join4(CUSTOMIZED_TEMPLATE_DIR, "default");
60576
60760
  var TOOL_BOOTSTRAP_FILE = {
60577
60761
  codex: "AGENTS.md",
60578
- claude: "CLAUDE.md"
60762
+ claude: "CLAUDE.md",
60763
+ gemini: "GEMINI.md"
60579
60764
  };
60580
60765
  function shouldIncludeTemplateFile(toolId, relativePath) {
60581
60766
  const normalized = relativePath.replaceAll("\\", "/");
@@ -60585,6 +60770,9 @@ function shouldIncludeTemplateFile(toolId, relativePath) {
60585
60770
  if (normalized.endsWith("CLAUDE.md")) {
60586
60771
  return toolId === "claude";
60587
60772
  }
60773
+ if (normalized.endsWith("GEMINI.md")) {
60774
+ return toolId === "gemini";
60775
+ }
60588
60776
  return true;
60589
60777
  }
60590
60778
  function collectTemplateFiles(rootDir, toolId, prefix = "") {
@@ -60882,11 +61070,11 @@ async function addAgentToEditableConfig(params) {
60882
61070
  async function addAgent(args) {
60883
61071
  const agentId = args[0]?.trim();
60884
61072
  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]>]...");
61073
+ 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
61074
  }
60887
61075
  const cliTool = parseSingleOption(args, "--cli");
60888
61076
  if (!cliTool || !(cliTool in DEFAULT_AGENT_TOOL_TEMPLATES)) {
60889
- throw new Error("agents add requires --cli codex or --cli claude");
61077
+ throw new Error("agents add requires --cli codex, --cli claude, or --cli gemini");
60890
61078
  }
60891
61079
  const workspace = parseSingleOption(args, "--workspace");
60892
61080
  const startupOptions = parseRepeatedOption(args, "--startup-option");
@@ -61088,7 +61276,7 @@ async function runAgentsCli(args) {
61088
61276
 
61089
61277
  // src/control/accounts-cli.ts
61090
61278
  import { setTimeout as sleep2 } from "node:timers/promises";
61091
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
61279
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
61092
61280
 
61093
61281
  // src/config/channel-account-management.ts
61094
61282
  function getFirstAccountId(accounts) {
@@ -61437,24 +61625,105 @@ class RuntimeHealthStore {
61437
61625
  }
61438
61626
 
61439
61627
  // 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";
61442
- import { dirname as dirname9 } from "node:path";
61628
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
61629
+ import { closeSync, existsSync as existsSync6, openSync, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
61630
+ import { dirname as dirname10 } from "node:path";
61443
61631
  import { kill } from "node:process";
61444
61632
 
61445
61633
  // src/control/clisbot-wrapper.ts
61446
61634
  import { chmod } from "node:fs/promises";
61447
61635
  import { fileURLToPath as fileURLToPath3 } from "node:url";
61448
- import { dirname as dirname7, join as join5, sep } from "node:path";
61636
+ import { dirname as dirname8, join as join6, sep } from "node:path";
61637
+
61638
+ // src/control/runner-exit-diagnostics.ts
61639
+ import { unlink } from "node:fs/promises";
61640
+ import { dirname as dirname7, join as join5 } from "node:path";
61641
+ function shellQuote(value) {
61642
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
61643
+ return value;
61644
+ }
61645
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
61646
+ }
61647
+ function buildCommandString(command, args) {
61648
+ return [command, ...args].map(shellQuote).join(" ");
61649
+ }
61650
+ function sanitizeSessionName2(sessionName) {
61651
+ return sessionName.replace(/[^a-zA-Z0-9._-]+/g, "_");
61652
+ }
61653
+ function getRunnerExitRecordPath(stateDir, sessionName) {
61654
+ return join5(stateDir, "runner-exits", `${sanitizeSessionName2(sessionName)}.json`);
61655
+ }
61656
+ function buildRunnerLaunchCommand(params) {
61657
+ const runnerCommand = buildCommandString(params.command, params.args);
61658
+ const exitRecordPath = getRunnerExitRecordPath(params.stateDir, params.sessionName);
61659
+ const exitWriterScript = [
61660
+ "const fs = require('fs');",
61661
+ "const path = require('path');",
61662
+ "const filePath = process.argv[1];",
61663
+ "const sessionName = process.argv[2];",
61664
+ "const exitCode = Number(process.argv[3]);",
61665
+ "const command = process.argv[4];",
61666
+ "fs.mkdirSync(path.dirname(filePath), { recursive: true });",
61667
+ "fs.writeFileSync(filePath, JSON.stringify({ sessionName, exitCode, command, exitedAt: new Date().toISOString() }) + '\\n');"
61668
+ ].join(" ");
61669
+ const exports = [
61670
+ `export PATH=${shellQuote(params.wrapperDir)}:"$PATH"`,
61671
+ `export CLISBOT_BIN=${shellQuote(params.wrapperPath)}`
61672
+ ];
61673
+ return [
61674
+ ...exports,
61675
+ `rm -f ${shellQuote(exitRecordPath)}`,
61676
+ runnerCommand,
61677
+ "status=$?",
61678
+ `node -e ${shellQuote(exitWriterScript)} ${shellQuote(exitRecordPath)} ${shellQuote(params.sessionName)} "$status" ${shellQuote(runnerCommand)} || true`,
61679
+ 'exit "$status"'
61680
+ ].join("; ");
61681
+ }
61682
+ async function clearRunnerExitRecord(stateDir, sessionName) {
61683
+ const exitRecordPath = getRunnerExitRecordPath(stateDir, sessionName);
61684
+ if (!await fileExists(exitRecordPath)) {
61685
+ return;
61686
+ }
61687
+ try {
61688
+ await unlink(exitRecordPath);
61689
+ } catch {}
61690
+ }
61691
+ async function readRunnerExitRecord(stateDir, sessionName) {
61692
+ const exitRecordPath = getRunnerExitRecordPath(stateDir, sessionName);
61693
+ if (!await fileExists(exitRecordPath)) {
61694
+ return null;
61695
+ }
61696
+ try {
61697
+ const text = await readTextFile(exitRecordPath);
61698
+ const parsed = JSON.parse(text);
61699
+ const exitCode = parsed.exitCode;
61700
+ if (typeof parsed.sessionName !== "string" || typeof exitCode !== "number" || !Number.isFinite(exitCode) || typeof parsed.command !== "string" || typeof parsed.exitedAt !== "string") {
61701
+ return null;
61702
+ }
61703
+ return {
61704
+ sessionName: parsed.sessionName,
61705
+ exitCode,
61706
+ command: parsed.command,
61707
+ exitedAt: parsed.exitedAt
61708
+ };
61709
+ } catch {
61710
+ return null;
61711
+ }
61712
+ }
61713
+ async function ensureRunnerExitRecordDir(stateDir, sessionName) {
61714
+ await ensureDir(dirname7(getRunnerExitRecordPath(stateDir, sessionName)));
61715
+ }
61716
+
61717
+ // src/control/clisbot-wrapper.ts
61449
61718
  function getDefaultClisbotBinDir(env = process.env) {
61450
- return join5(resolveAppHomeDir(env), "bin");
61719
+ return join6(resolveAppHomeDir(env), "bin");
61451
61720
  }
61452
61721
  function getDefaultClisbotWrapperPath(env = process.env) {
61453
- return join5(getDefaultClisbotBinDir(env), "clisbot");
61722
+ return join6(getDefaultClisbotBinDir(env), "clisbot");
61454
61723
  }
61455
61724
  var DEFAULT_CLISBOT_BIN_DIR = getDefaultClisbotBinDir();
61456
61725
  var DEFAULT_CLISBOT_WRAPPER_PATH = getDefaultClisbotWrapperPath();
61457
- function shellQuote(value) {
61726
+ function shellQuote2(value) {
61458
61727
  if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
61459
61728
  return value;
61460
61729
  }
@@ -61477,7 +61746,7 @@ function getClisbotPromptCommand() {
61477
61746
  return isPackagedRuntime() ? "clis" : getClisbotWrapperPath();
61478
61747
  }
61479
61748
  function getClisbotWrapperDir() {
61480
- return dirname7(getClisbotWrapperPath());
61749
+ return dirname8(getClisbotWrapperPath());
61481
61750
  }
61482
61751
  function renderClisbotWrapperScript() {
61483
61752
  const execPath = process.execPath;
@@ -61485,14 +61754,14 @@ function renderClisbotWrapperScript() {
61485
61754
  return [
61486
61755
  "#!/usr/bin/env bash",
61487
61756
  "set -euo pipefail",
61488
- `exec ${shellQuote(execPath)} ${shellQuote(mainScriptPath)} "$@"`,
61757
+ `exec ${shellQuote2(execPath)} ${shellQuote2(mainScriptPath)} "$@"`,
61489
61758
  ""
61490
61759
  ].join(`
61491
61760
  `);
61492
61761
  }
61493
61762
  async function ensureClisbotWrapper() {
61494
61763
  const wrapperPath = getClisbotWrapperPath();
61495
- const wrapperDir = dirname7(wrapperPath);
61764
+ const wrapperDir = dirname8(wrapperPath);
61496
61765
  await ensureDir2(wrapperDir);
61497
61766
  const nextScript = renderClisbotWrapperScript();
61498
61767
  const existing = await fileExists(wrapperPath) ? await readTextFile(wrapperPath) : null;
@@ -61509,7 +61778,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
61509
61778
  // src/shared/process.ts
61510
61779
  import { spawn } from "node:child_process";
61511
61780
  import { existsSync as existsSync5 } from "node:fs";
61512
- import { delimiter, join as join6 } from "node:path";
61781
+ import { delimiter, join as join7 } from "node:path";
61513
61782
  function sleep(ms) {
61514
61783
  return new Promise((resolve) => {
61515
61784
  setTimeout(resolve, ms);
@@ -61527,7 +61796,7 @@ function commandExists(command) {
61527
61796
  const executableNames = getExecutableNames(command);
61528
61797
  for (const directory of pathEntries) {
61529
61798
  for (const executableName of executableNames) {
61530
- if (existsSync5(join6(directory, executableName))) {
61799
+ if (existsSync5(join7(directory, executableName))) {
61531
61800
  return true;
61532
61801
  }
61533
61802
  }
@@ -61807,6 +62076,12 @@ class StartDetachedRuntimeError extends Error {
61807
62076
  this.name = "StartDetachedRuntimeError";
61808
62077
  }
61809
62078
  }
62079
+ var DEFAULT_PROCESS_LIVENESS_DEPENDENCIES = {
62080
+ platform: process.platform,
62081
+ signalCheck: signalCheckProcess,
62082
+ readLinuxProcStat: readLinuxProcStatLiveness,
62083
+ readPsStat: readPsStatLiveness
62084
+ };
61810
62085
  function readRuntimePid(pidPath) {
61811
62086
  const expandedPidPath = resolvePidPath(pidPath);
61812
62087
  if (!existsSync6(expandedPidPath)) {
@@ -61819,17 +62094,33 @@ function readRuntimePid(pidPath) {
61819
62094
  });
61820
62095
  }
61821
62096
  function isProcessRunning(pid) {
61822
- try {
61823
- kill(pid, 0);
61824
- return true;
61825
- } catch {
61826
- return false;
62097
+ return getProcessLiveness(pid) === "running";
62098
+ }
62099
+ function getProcessLiveness(pid, dependencies = {}) {
62100
+ const resolvedDependencies = {
62101
+ ...DEFAULT_PROCESS_LIVENESS_DEPENDENCIES,
62102
+ ...dependencies
62103
+ };
62104
+ if (!resolvedDependencies.signalCheck(pid)) {
62105
+ return "missing";
61827
62106
  }
62107
+ if (resolvedDependencies.platform === "win32") {
62108
+ return "running";
62109
+ }
62110
+ const linuxState = resolvedDependencies.readLinuxProcStat(pid);
62111
+ if (linuxState !== "unknown") {
62112
+ return linuxState;
62113
+ }
62114
+ const psState = resolvedDependencies.readPsStat(pid);
62115
+ if (psState !== "unknown") {
62116
+ return psState;
62117
+ }
62118
+ return "running";
61828
62119
  }
61829
62120
  async function ensureConfigFile(configPath, options = {}) {
61830
62121
  await ensureClisbotWrapper();
61831
62122
  const expandedConfigPath = resolveConfigPath(configPath);
61832
- await ensureDir2(dirname9(expandedConfigPath));
62123
+ await ensureDir2(dirname10(expandedConfigPath));
61833
62124
  if (existsSync6(expandedConfigPath)) {
61834
62125
  return {
61835
62126
  configPath: expandedConfigPath,
@@ -61866,8 +62157,8 @@ async function startDetachedRuntime(params) {
61866
62157
  rmSync2(pidPath, { force: true });
61867
62158
  }
61868
62159
  const configResult = await ensureConfigFile(params.configPath);
61869
- await ensureDir2(dirname9(pidPath));
61870
- await ensureDir2(dirname9(logPath));
62160
+ await ensureDir2(dirname10(pidPath));
62161
+ await ensureDir2(dirname10(logPath));
61871
62162
  const logStartOffset = getLogSize(logPath);
61872
62163
  const logFd = openSync(logPath, "a");
61873
62164
  const child = spawn2(process.execPath, [params.scriptPath, "serve-foreground"], {
@@ -61907,18 +62198,27 @@ async function startDetachedRuntime(params) {
61907
62198
  logPath
61908
62199
  };
61909
62200
  }
61910
- async function stopDetachedRuntime(params) {
62201
+ async function stopDetachedRuntime(params, dependencies = {}) {
61911
62202
  const pidPath = resolvePidPath(params.pidPath);
61912
62203
  const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath);
61913
62204
  const existingPid = await readRuntimePid(pidPath);
61914
62205
  let stopped = false;
61915
- if (existingPid && isProcessRunning(existingPid)) {
61916
- kill(existingPid, "SIGTERM");
61917
- const exited = await waitForProcessExit(existingPid, STOP_WAIT_TIMEOUT_MS);
62206
+ const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
62207
+ const sendSignal = dependencies.sendSignal ?? kill;
62208
+ const sleepFn = dependencies.sleep ?? sleep;
62209
+ const existingLiveness = existingPid ? processLiveness(existingPid) : "missing";
62210
+ if (existingPid && existingLiveness === "running") {
62211
+ sendSignal(existingPid, "SIGTERM");
62212
+ const exited = await waitForProcessExit(existingPid, STOP_WAIT_TIMEOUT_MS, {
62213
+ processLiveness,
62214
+ sleep: sleepFn
62215
+ });
61918
62216
  if (!exited) {
61919
62217
  throw new Error(`clisbot did not stop within ${STOP_WAIT_TIMEOUT_MS}ms`);
61920
62218
  }
61921
62219
  stopped = true;
62220
+ } else if (existingPid && existingLiveness === "zombie") {
62221
+ stopped = true;
61922
62222
  }
61923
62223
  rmSync2(pidPath, { force: true });
61924
62224
  removeRuntimeCredentials(runtimeCredentialsPath);
@@ -61948,7 +62248,7 @@ async function disableExpiredMemAccountsInConfig(configPath) {
61948
62248
  }
61949
62249
  async function writeRuntimePid(pidPath, pid = process.pid) {
61950
62250
  const expandedPidPath = resolvePidPath(pidPath);
61951
- await ensureDir2(dirname9(expandedPidPath));
62251
+ await ensureDir2(dirname10(expandedPidPath));
61952
62252
  await writeTextFile(expandedPidPath, `${pid}
61953
62253
  `);
61954
62254
  }
@@ -61960,9 +62260,10 @@ async function getRuntimeStatus(params = {}) {
61960
62260
  const pidPath = resolvePidPath(params.pidPath);
61961
62261
  const logPath = resolveLogPath(params.logPath);
61962
62262
  const pid = await readRuntimePid(pidPath);
62263
+ const liveness = pid ? getProcessLiveness(pid) : "missing";
61963
62264
  return {
61964
- running: Boolean(pid && isProcessRunning(pid)),
61965
- pid: pid && isProcessRunning(pid) ? pid : undefined,
62265
+ running: liveness === "running",
62266
+ pid: liveness === "running" && pid ? pid : undefined,
61966
62267
  configPath,
61967
62268
  pidPath,
61968
62269
  logPath,
@@ -62032,15 +62333,17 @@ async function waitForStart(params) {
62032
62333
  childPid: params.childPid
62033
62334
  };
62034
62335
  }
62035
- async function waitForProcessExit(pid, timeoutMs) {
62336
+ async function waitForProcessExit(pid, timeoutMs, dependencies = {}) {
62337
+ const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
62338
+ const sleepFn = dependencies.sleep ?? sleep;
62036
62339
  const deadline = Date.now() + timeoutMs;
62037
62340
  while (Date.now() < deadline) {
62038
- if (!isProcessRunning(pid)) {
62341
+ if (processLiveness(pid) !== "running") {
62039
62342
  return true;
62040
62343
  }
62041
- await sleep(PROCESS_POLL_INTERVAL_MS);
62344
+ await sleepFn(PROCESS_POLL_INTERVAL_MS);
62042
62345
  }
62043
- return !isProcessRunning(pid);
62346
+ return processLiveness(pid) !== "running";
62044
62347
  }
62045
62348
  async function cleanupFailedStartChild(result) {
62046
62349
  if (result.reason === "child-exited-before-pid") {
@@ -62087,6 +62390,66 @@ async function resolveTmuxSocketPath(configPath) {
62087
62390
  }
62088
62391
  return getDefaultTmuxSocketPath();
62089
62392
  }
62393
+ function signalCheckProcess(pid) {
62394
+ try {
62395
+ kill(pid, 0);
62396
+ return true;
62397
+ } catch {
62398
+ return false;
62399
+ }
62400
+ }
62401
+ function readLinuxProcStatLiveness(pid) {
62402
+ if (process.platform !== "linux") {
62403
+ return "unknown";
62404
+ }
62405
+ try {
62406
+ const raw = readFileSync3(`/proc/${pid}/stat`, "utf8");
62407
+ const state = extractLinuxProcState(raw);
62408
+ if (!state) {
62409
+ return "unknown";
62410
+ }
62411
+ return state.includes("Z") ? "zombie" : "running";
62412
+ } catch (error) {
62413
+ const code = error.code;
62414
+ if (code === "ENOENT") {
62415
+ return "missing";
62416
+ }
62417
+ return "unknown";
62418
+ }
62419
+ }
62420
+ function readPsStatLiveness(pid) {
62421
+ try {
62422
+ const raw = execFileSync("ps", ["-o", "stat=", "-p", String(pid)], {
62423
+ encoding: "utf8",
62424
+ stdio: ["ignore", "pipe", "ignore"]
62425
+ }).trim();
62426
+ if (!raw) {
62427
+ return "missing";
62428
+ }
62429
+ return raw.includes("Z") ? "zombie" : "running";
62430
+ } catch (error) {
62431
+ const commandError = error;
62432
+ if (commandError.code === "ENOENT") {
62433
+ return "unknown";
62434
+ }
62435
+ if (commandError.status === 1) {
62436
+ return "missing";
62437
+ }
62438
+ return "unknown";
62439
+ }
62440
+ }
62441
+ function extractLinuxProcState(raw) {
62442
+ const closingParenIndex = raw.lastIndexOf(")");
62443
+ if (closingParenIndex < 0) {
62444
+ return null;
62445
+ }
62446
+ const remainder = raw.slice(closingParenIndex + 1).trim();
62447
+ if (!remainder) {
62448
+ return null;
62449
+ }
62450
+ const [state] = remainder.split(/\s+/, 1);
62451
+ return state?.trim() || null;
62452
+ }
62090
62453
 
62091
62454
  // src/control/accounts-cli.ts
62092
62455
  function getEditableConfigPath2() {
@@ -62111,7 +62474,7 @@ function readRuntimeCredentialDocument() {
62111
62474
  if (!existsSync7(path2)) {
62112
62475
  return {};
62113
62476
  }
62114
- const text = readFileSync3(path2, "utf8").trim();
62477
+ const text = readFileSync4(path2, "utf8").trim();
62115
62478
  return text ? JSON.parse(text) : {};
62116
62479
  }
62117
62480
  async function waitForReloadResult(configPath, deps, timeoutMs = 12000) {
@@ -62506,7 +62869,13 @@ function buildConfiguredTargetFromIdentity(identity) {
62506
62869
  };
62507
62870
  }
62508
62871
  function renderFieldLabel(field) {
62509
- return field === "responseMode" ? "response-mode" : "additional-message-mode";
62872
+ if (field === "responseMode") {
62873
+ return "response-mode";
62874
+ }
62875
+ if (field === "additionalMessageMode") {
62876
+ return "additional-message-mode";
62877
+ }
62878
+ return "streaming";
62510
62879
  }
62511
62880
 
62512
62881
  // src/channels/additional-message-mode-config.ts
@@ -62598,218 +62967,22 @@ async function setConfiguredResponseMode(params) {
62598
62967
  }
62599
62968
 
62600
62969
  // src/control/channel-privilege-cli.ts
62601
- function getEditableConfigPath5() {
62602
- return process.env.CLISBOT_CONFIG_PATH;
62603
- }
62604
- function parseTarget(raw) {
62605
- if (raw === "slack-dm" || raw === "slack-channel" || raw === "slack-group" || raw === "telegram-dm" || raw === "telegram-group") {
62606
- return raw;
62607
- }
62608
- throw new Error(renderPrivilegeCliHelp());
62609
- }
62610
- function parseOptionValue2(args, name) {
62611
- const index = args.findIndex((arg) => arg === name);
62612
- if (index === -1) {
62613
- return;
62614
- }
62615
- const value = args[index + 1]?.trim();
62616
- if (!value) {
62617
- throw new Error(`Missing value for ${name}`);
62618
- }
62619
- return value;
62620
- }
62621
- function ensureAllowUsersList(value) {
62622
- return {
62623
- enabled: value?.enabled ?? false,
62624
- allowUsers: value?.allowUsers ?? []
62625
- };
62626
- }
62627
- function renderPrivilegeCliHelp() {
62970
+ function renderChannelPrivilegeCliRemovedMessage() {
62628
62971
  return [
62629
- "Usage:",
62630
- " clisbot channels privilege enable slack-dm",
62631
- " clisbot channels privilege disable slack-dm",
62632
- " clisbot channels privilege allow-user slack-dm <userId>",
62633
- " clisbot channels privilege remove-user slack-dm <userId>",
62634
- " clisbot channels privilege enable slack-channel <channelId>",
62635
- " clisbot channels privilege allow-user slack-channel <channelId> <userId>",
62636
- " clisbot channels privilege enable slack-group <groupId>",
62637
- " clisbot channels privilege allow-user slack-group <groupId> <userId>",
62638
- " clisbot channels privilege enable telegram-dm",
62639
- " clisbot channels privilege allow-user telegram-dm <userId>",
62640
- " clisbot channels privilege enable telegram-group <chatId> [--topic <topicId>]",
62641
- " clisbot channels privilege allow-user telegram-group <chatId> <userId> [--topic <topicId>]"
62972
+ "`clisbot channels privilege` has been removed.",
62973
+ "Manage routed permissions through `app.auth` and `agents.<id>.auth` instead.",
62974
+ "Grant `shellExecute` on the target agent role when `/bash` should be allowed."
62642
62975
  ].join(`
62643
62976
  `);
62644
62977
  }
62645
- function addUniqueUser(users, userId) {
62646
- const normalized = userId.trim();
62647
- return normalized && !users.includes(normalized) ? [...users, normalized] : users;
62978
+ async function runChannelPrivilegeCli(_args) {
62979
+ throw new Error(renderChannelPrivilegeCliRemovedMessage());
62648
62980
  }
62649
- function removeUser(users, userId) {
62650
- const normalized = userId.trim();
62651
- return users.filter((value) => value !== normalized);
62652
- }
62653
- async function runChannelPrivilegeCli(args) {
62654
- const action = args[0];
62655
- const target = parseTarget(args[1]);
62656
- const rest = args.slice(2);
62657
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62658
- if (target === "slack-dm") {
62659
- const current2 = ensureAllowUsersList(config.channels.slack.directMessages.privilegeCommands);
62660
- await applyPrivilegeAction({
62661
- action,
62662
- current: current2,
62663
- args: rest,
62664
- set: (next) => {
62665
- config.channels.slack.directMessages.privilegeCommands = next;
62666
- },
62667
- configPath,
62668
- label: "slack direct messages",
62669
- save: async () => writeEditableConfig(configPath, config)
62670
- });
62671
- return;
62672
- }
62673
- if (target === "telegram-dm") {
62674
- const current2 = ensureAllowUsersList(config.channels.telegram.directMessages.privilegeCommands);
62675
- await applyPrivilegeAction({
62676
- action,
62677
- current: current2,
62678
- args: rest,
62679
- set: (next) => {
62680
- config.channels.telegram.directMessages.privilegeCommands = next;
62681
- },
62682
- configPath,
62683
- label: "telegram direct messages",
62684
- save: async () => writeEditableConfig(configPath, config)
62685
- });
62686
- return;
62687
- }
62688
- if (target === "slack-channel" || target === "slack-group") {
62689
- const routeId = rest[0]?.trim();
62690
- if (!routeId) {
62691
- throw new Error(renderPrivilegeCliHelp());
62692
- }
62693
- const routes = target === "slack-channel" ? config.channels.slack.channels : config.channels.slack.groups;
62694
- const route = routes[routeId];
62695
- if (!route) {
62696
- throw new Error(`Route not configured yet: ${target} ${routeId}. Add the route first with \`clisbot channels add ...\`.`);
62697
- }
62698
- const current2 = ensureAllowUsersList(route.privilegeCommands);
62699
- await applyPrivilegeAction({
62700
- action,
62701
- current: current2,
62702
- args: rest.slice(1),
62703
- set: (next) => {
62704
- route.privilegeCommands = next;
62705
- },
62706
- configPath,
62707
- label: `${target} ${routeId}`,
62708
- save: async () => writeEditableConfig(configPath, config)
62709
- });
62710
- return;
62711
- }
62712
- const chatId = rest[0]?.trim();
62713
- if (!chatId) {
62714
- throw new Error(renderPrivilegeCliHelp());
62715
- }
62716
- const topicId = parseOptionValue2(rest, "--topic");
62717
- const group = config.channels.telegram.groups[chatId];
62718
- if (!group) {
62719
- throw new Error(renderTelegramRouteChoiceMessage({ chatId }));
62720
- }
62721
- if (topicId) {
62722
- const topic = group.topics?.[topicId];
62723
- if (!topic) {
62724
- throw new Error(renderTelegramRouteChoiceMessage({ chatId, topicId }));
62725
- }
62726
- const current2 = ensureAllowUsersList(topic.privilegeCommands);
62727
- await applyPrivilegeAction({
62728
- action,
62729
- current: current2,
62730
- args: rest.filter((value, index) => {
62731
- if (index === 0) {
62732
- return false;
62733
- }
62734
- return value !== "--topic" && value !== topicId;
62735
- }),
62736
- set: (next) => {
62737
- topic.privilegeCommands = next;
62738
- },
62739
- configPath,
62740
- label: `telegram topic ${chatId}/${topicId}`,
62741
- save: async () => writeEditableConfig(configPath, config)
62742
- });
62743
- return;
62744
- }
62745
- const current = ensureAllowUsersList(group.privilegeCommands);
62746
- await applyPrivilegeAction({
62747
- action,
62748
- current,
62749
- args: rest.slice(1),
62750
- set: (next) => {
62751
- group.privilegeCommands = next;
62752
- },
62753
- configPath,
62754
- label: `telegram group ${chatId}`,
62755
- save: async () => writeEditableConfig(configPath, config)
62756
- });
62757
- }
62758
- async function applyPrivilegeAction(params) {
62759
- if (params.action === "enable") {
62760
- params.set({
62761
- enabled: true,
62762
- allowUsers: params.current.allowUsers
62763
- });
62764
- await params.save();
62765
- console.log(`enabled privilege commands for ${params.label}`);
62766
- console.log(`config: ${params.configPath}`);
62767
- return;
62768
- }
62769
- if (params.action === "disable") {
62770
- params.set({
62771
- enabled: false,
62772
- allowUsers: params.current.allowUsers
62773
- });
62774
- await params.save();
62775
- console.log(`disabled privilege commands for ${params.label}`);
62776
- console.log(`config: ${params.configPath}`);
62777
- return;
62778
- }
62779
- if (params.action === "allow-user") {
62780
- const userId = params.args[0]?.trim();
62781
- if (!userId) {
62782
- throw new Error(renderPrivilegeCliHelp());
62783
- }
62784
- params.set({
62785
- enabled: params.current.enabled,
62786
- allowUsers: addUniqueUser(params.current.allowUsers, userId)
62787
- });
62788
- await params.save();
62789
- console.log(`allowed ${userId} to use privilege commands for ${params.label}`);
62790
- console.log(`config: ${params.configPath}`);
62791
- return;
62792
- }
62793
- if (params.action === "remove-user") {
62794
- const userId = params.args[0]?.trim();
62795
- if (!userId) {
62796
- throw new Error(renderPrivilegeCliHelp());
62797
- }
62798
- params.set({
62799
- enabled: params.current.enabled,
62800
- allowUsers: removeUser(params.current.allowUsers, userId)
62801
- });
62802
- await params.save();
62803
- console.log(`removed ${userId} from privilege commands for ${params.label}`);
62804
- console.log(`config: ${params.configPath}`);
62805
- return;
62806
- }
62807
- throw new Error(renderPrivilegeCliHelp());
62808
- }
62809
-
62810
- // src/control/channels-cli.ts
62811
- function getEditableConfigPath6() {
62812
- return process.env.CLISBOT_CONFIG_PATH;
62981
+
62982
+ // src/control/channels-cli.ts
62983
+ var AUTH_USER_GUIDE_DOC_PATH = "docs/user-guide/auth-and-roles.md";
62984
+ function getEditableConfigPath5() {
62985
+ return process.env.CLISBOT_CONFIG_PATH;
62813
62986
  }
62814
62987
  function renderChannelsHelp() {
62815
62988
  return [
@@ -62826,7 +62999,6 @@ function renderChannelsHelp() {
62826
62999
  " clisbot channels remove slack-channel <channelId>",
62827
63000
  " clisbot channels add slack-group <groupId> [--agent <id>] [--require-mention true|false]",
62828
63001
  " clisbot channels remove slack-group <groupId>",
62829
- " clisbot channels privilege <enable|disable|allow-user|remove-user> <target> ...",
62830
63002
  " clisbot channels response-mode status --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
62831
63003
  " clisbot channels response-mode set <capture-pane|message-tool> --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
62832
63004
  " clisbot channels additional-message-mode status --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
@@ -62842,7 +63014,8 @@ function renderChannelsHelp() {
62842
63014
  " - Telegram groups need channels.telegram.groups.<chatId>",
62843
63015
  " - Telegram forum topics need channels.telegram.groups.<chatId>.topics.<topicId>",
62844
63016
  " - Adding a route puts that surface on the allowlist; other channels, groups, or topics still need to be added explicitly",
62845
- " - Tune route settings such as requireMention, privilegeCommands, and followUp in clisbot.json when a surface should behave differently",
63017
+ " - Tune route settings such as requireMention and followUp in clisbot.json when a surface should behave differently",
63018
+ ` - Manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`,
62846
63019
  " - Response delivery can be tuned with responseMode: `capture-pane` or `message-tool`",
62847
63020
  " - Busy-session follow-up can be tuned with additionalMessageMode: `steer` or `queue`",
62848
63021
  " - Slack response-mode targets use `channel:<id>`, `group:<id>`, or `dm:<id>`",
@@ -62875,7 +63048,6 @@ function renderChannelsHelp() {
62875
63048
  "Next steps:",
62876
63049
  " - Run `clisbot status` to inspect routes and current channel state",
62877
63050
  " - Run `clisbot logs` if the bot is still not responding",
62878
- ...renderGenericPrivilegeCommandHelpLines(),
62879
63051
  ...renderChannelSetupHelpLines("", { includePrivilegeHelp: false })
62880
63052
  ].join(`
62881
63053
  `);
@@ -62930,7 +63102,7 @@ function parseResponseModeTarget(channel, raw) {
62930
63102
  }
62931
63103
  return target;
62932
63104
  }
62933
- function parseOptionValue3(args, name) {
63105
+ function parseOptionValue2(args, name) {
62934
63106
  const index = args.findIndex((arg) => arg === name);
62935
63107
  if (index === -1) {
62936
63108
  return;
@@ -62942,7 +63114,7 @@ function parseOptionValue3(args, name) {
62942
63114
  return value;
62943
63115
  }
62944
63116
  function parseBooleanOption(args, name, fallback) {
62945
- const raw = parseOptionValue3(args, name);
63117
+ const raw = parseOptionValue2(args, name);
62946
63118
  if (!raw) {
62947
63119
  return fallback;
62948
63120
  }
@@ -62955,10 +63127,10 @@ function parseBooleanOption(args, name, fallback) {
62955
63127
  throw new Error(`${name} requires true or false`);
62956
63128
  }
62957
63129
  function getAgentId(args) {
62958
- return parseOptionValue3(args, "--agent") ?? "default";
63130
+ return parseOptionValue2(args, "--agent") ?? "default";
62959
63131
  }
62960
63132
  async function setChannelEnabled(action, channel) {
62961
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63133
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62962
63134
  const enabled = action === "enable";
62963
63135
  const current = config.channels[channel].enabled;
62964
63136
  if (current === enabled) {
@@ -62978,8 +63150,8 @@ async function addTelegramGroup(args) {
62978
63150
  if (!chatId) {
62979
63151
  throw new Error("Usage: clisbot channels add telegram-group <chatId> [--topic <topicId>] [--agent <id>] [--require-mention true|false]");
62980
63152
  }
62981
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
62982
- const topicId = parseOptionValue3(args, "--topic");
63153
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63154
+ const topicId = parseOptionValue2(args, "--topic");
62983
63155
  const agentId = getAgentId(args);
62984
63156
  const requireMention = parseBooleanOption(args, "--require-mention", true);
62985
63157
  const groupRoute = config.channels.telegram.groups[chatId] ?? {
@@ -63031,8 +63203,8 @@ async function removeTelegramGroup(args) {
63031
63203
  if (!chatId) {
63032
63204
  throw new Error("Usage: clisbot channels remove telegram-group <chatId> [--topic <topicId>]");
63033
63205
  }
63034
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63035
- const topicId = parseOptionValue3(args, "--topic");
63206
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63207
+ const topicId = parseOptionValue2(args, "--topic");
63036
63208
  const groupRoute = config.channels.telegram.groups[chatId];
63037
63209
  if (!groupRoute) {
63038
63210
  console.log(`telegram group route ${chatId} is not configured`);
@@ -63061,7 +63233,7 @@ async function addSlackRoute(kind, args) {
63061
63233
  if (!routeId) {
63062
63234
  throw new Error(`Usage: clisbot channels add slack-${kind} <${kind}Id> [--agent <id>] [--require-mention true|false]`);
63063
63235
  }
63064
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63236
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63065
63237
  const agentId = getAgentId(args);
63066
63238
  const requireMention = parseBooleanOption(args, "--require-mention", false);
63067
63239
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
@@ -63085,7 +63257,7 @@ async function removeSlackRoute(kind, args) {
63085
63257
  if (!routeId) {
63086
63258
  throw new Error(`Usage: clisbot channels remove slack-${kind} <${kind}Id>`);
63087
63259
  }
63088
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63260
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63089
63261
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
63090
63262
  if (!target[routeId]) {
63091
63263
  console.log(`slack ${kind} route ${routeId} is not configured`);
@@ -63098,7 +63270,7 @@ async function removeSlackRoute(kind, args) {
63098
63270
  console.log(`config: ${configPath}`);
63099
63271
  }
63100
63272
  async function setToken(target, value) {
63101
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63273
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63102
63274
  if (target === "slack-app") {
63103
63275
  config.channels.slack.appToken = value;
63104
63276
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63130,7 +63302,7 @@ async function setToken(target, value) {
63130
63302
  console.log(`config: ${configPath}`);
63131
63303
  }
63132
63304
  async function clearToken(target) {
63133
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63305
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63134
63306
  if (target === "slack-app") {
63135
63307
  config.channels.slack.appToken = "";
63136
63308
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63174,9 +63346,9 @@ async function runResponseModeCli(args) {
63174
63346
  }
63175
63347
  const responseMode = action === "set" ? parseResponseMode2(args[1]) : undefined;
63176
63348
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
63177
- const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
63178
- const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
63179
- const topic = parseOptionValue3(optionArgs, "--topic");
63349
+ const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
63350
+ const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
63351
+ const topic = parseOptionValue2(optionArgs, "--topic");
63180
63352
  if (channel === "slack" && topic) {
63181
63353
  throw new Error("Slack response-mode commands do not support --topic");
63182
63354
  }
@@ -63208,9 +63380,9 @@ async function runAdditionalMessageModeCli(args) {
63208
63380
  }
63209
63381
  const additionalMessageMode = action === "set" ? parseAdditionalMessageMode2(args[1]) : undefined;
63210
63382
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
63211
- const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
63212
- const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
63213
- const topic = parseOptionValue3(optionArgs, "--topic");
63383
+ const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
63384
+ const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
63385
+ const topic = parseOptionValue2(optionArgs, "--topic");
63214
63386
  if (channel === "slack" && topic) {
63215
63387
  throw new Error("Slack additional-message-mode commands do not support --topic");
63216
63388
  }
@@ -63273,16 +63445,10 @@ function renderRouteAddGuidance(params) {
63273
63445
  console.log("Slack route next steps:");
63274
63446
  console.log(` - route added: ${routePath}`);
63275
63447
  console.log(" - direct messages still follow channels.slack.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
63276
- console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention, privilegeCommands, and followUp`);
63448
+ console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention and followUp`);
63277
63449
  console.log(" - if you want pairing-style access control for DMs, set channels.slack.directMessages.policy to `pairing`");
63278
63450
  console.log(" - if you want stricter route access, keep Slack groups on allowlist and only add the channels/groups you trust");
63279
- for (const line of renderPrivilegeCommandHelpLines({
63280
- platform: "slack",
63281
- conversationKind: params.kind === "group" ? "group" : "channel",
63282
- channelId: params.routeId
63283
- }, " ")) {
63284
- console.log(line);
63285
- }
63451
+ console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
63286
63452
  } else {
63287
63453
  const [chatId, topicId] = params.routeId.split("/");
63288
63454
  const routePath = params.kind === "topic" ? `channels.telegram.groups."${chatId}".topics."${topicId}"` : `channels.telegram.groups."${params.routeId}"`;
@@ -63291,15 +63457,8 @@ function renderRouteAddGuidance(params) {
63291
63457
  console.log(" - direct messages still follow channels.telegram.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
63292
63458
  console.log(` - this ${params.kind} is now on the Telegram allowlist; other groups or topics still need to be added explicitly`);
63293
63459
  console.log(" - if you want pairing-style access control for DMs, set channels.telegram.directMessages.policy to `pairing`");
63294
- console.log(" - tune route settings such as requireMention, privilegeCommands, and followUp in clisbot.json if this surface should behave differently");
63295
- for (const line of renderPrivilegeCommandHelpLines({
63296
- platform: "telegram",
63297
- conversationKind: params.kind === "topic" ? "topic" : "group",
63298
- chatId: chatId ?? params.routeId,
63299
- topicId
63300
- }, " ")) {
63301
- console.log(line);
63302
- }
63460
+ console.log(" - tune route settings such as requireMention and followUp in clisbot.json if this surface should behave differently");
63461
+ console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
63303
63462
  }
63304
63463
  console.log("Run `clisbot status` to inspect routes and current channel state.");
63305
63464
  console.log("Run `clisbot logs` if the bot is still not responding.");
@@ -63380,6 +63539,9 @@ async function runChannelsCli(args) {
63380
63539
  }
63381
63540
 
63382
63541
  // src/control/channel-bootstrap-flags.ts
63542
+ function isLiteralToken(token) {
63543
+ return token?.kind === "mem";
63544
+ }
63383
63545
  function parseBotType(rawValue) {
63384
63546
  const value = rawValue.trim().toLowerCase();
63385
63547
  if (value === "personal" || value === "personal-assistant") {
@@ -63390,7 +63552,7 @@ function parseBotType(rawValue) {
63390
63552
  }
63391
63553
  throw new Error(`Invalid bot type: ${rawValue}`);
63392
63554
  }
63393
- function parseOptionValue4(args, name, index) {
63555
+ function parseOptionValue3(args, name, index) {
63394
63556
  const value = args[index + 1]?.trim();
63395
63557
  if (!value) {
63396
63558
  throw new Error(`Missing value for ${name}`);
@@ -63434,7 +63596,6 @@ function validateTelegramAccount(account) {
63434
63596
  function parseBootstrapFlags(args) {
63435
63597
  const slackAccounts = [];
63436
63598
  const telegramAccounts = [];
63437
- const literalWarnings = [];
63438
63599
  let currentSlackAccountId;
63439
63600
  let currentTelegramAccountId;
63440
63601
  let cliTool;
@@ -63446,17 +63607,17 @@ function parseBootstrapFlags(args) {
63446
63607
  for (let index = 0;index < args.length; index += 1) {
63447
63608
  const arg = args[index];
63448
63609
  if (arg === "--cli") {
63449
- cliTool = parseOptionValue4(args, arg, index);
63610
+ cliTool = parseOptionValue3(args, arg, index);
63450
63611
  index += 1;
63451
63612
  continue;
63452
63613
  }
63453
63614
  if (arg === "--bootstrap") {
63454
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
63615
+ bootstrap = parseBotType(parseOptionValue3(args, arg, index));
63455
63616
  index += 1;
63456
63617
  continue;
63457
63618
  }
63458
63619
  if (arg === "--bot-type") {
63459
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
63620
+ bootstrap = parseBotType(parseOptionValue3(args, arg, index));
63460
63621
  index += 1;
63461
63622
  continue;
63462
63623
  }
@@ -63465,7 +63626,7 @@ function parseBootstrapFlags(args) {
63465
63626
  continue;
63466
63627
  }
63467
63628
  if (arg === "--slack-account") {
63468
- const accountId = parseOptionValue4(args, arg, index);
63629
+ const accountId = parseOptionValue3(args, arg, index);
63469
63630
  ensureUniqueAccount(slackAccounts, accountId, "--slack-account");
63470
63631
  currentSlackAccountId = accountId;
63471
63632
  getOrCreateSlackAccount(slackAccounts, accountId);
@@ -63474,7 +63635,7 @@ function parseBootstrapFlags(args) {
63474
63635
  continue;
63475
63636
  }
63476
63637
  if (arg === "--telegram-account") {
63477
- const accountId = parseOptionValue4(args, arg, index);
63638
+ const accountId = parseOptionValue3(args, arg, index);
63478
63639
  ensureUniqueAccount(telegramAccounts, accountId, "--telegram-account");
63479
63640
  currentTelegramAccountId = accountId;
63480
63641
  getOrCreateTelegramAccount(telegramAccounts, accountId);
@@ -63483,36 +63644,27 @@ function parseBootstrapFlags(args) {
63483
63644
  continue;
63484
63645
  }
63485
63646
  if (arg === "--slack-app-token") {
63486
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63647
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63487
63648
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63488
63649
  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
63650
  sawCredentialFlags = true;
63493
63651
  sawSlackFlags = true;
63494
63652
  index += 1;
63495
63653
  continue;
63496
63654
  }
63497
63655
  if (arg === "--slack-bot-token") {
63498
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63656
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63499
63657
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63500
63658
  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
63659
  sawCredentialFlags = true;
63505
63660
  sawSlackFlags = true;
63506
63661
  index += 1;
63507
63662
  continue;
63508
63663
  }
63509
63664
  if (arg === "--telegram-bot-token") {
63510
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63665
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63511
63666
  const account = getOrCreateTelegramAccount(telegramAccounts, currentTelegramAccountId ?? "default");
63512
63667
  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
63668
  sawCredentialFlags = true;
63517
63669
  sawTelegramFlags = true;
63518
63670
  index += 1;
@@ -63535,9 +63687,12 @@ function parseBootstrapFlags(args) {
63535
63687
  sawCredentialFlags,
63536
63688
  sawSlackFlags,
63537
63689
  sawTelegramFlags,
63538
- literalWarnings
63690
+ literalWarnings: []
63539
63691
  };
63540
63692
  }
63693
+ function hasLiteralBootstrapCredentials(flags) {
63694
+ return flags.slackAccounts.some((account) => isLiteralToken(account.appToken) || isLiteralToken(account.botToken)) || flags.telegramAccounts.some((account) => isLiteralToken(account.botToken));
63695
+ }
63541
63696
 
63542
63697
  // src/agents/session-state.ts
63543
63698
  class AgentSessionState {
@@ -63591,6 +63746,8 @@ class AgentSessionState {
63591
63746
  startedAt: entry?.runtime?.startedAt,
63592
63747
  detachedAt: entry?.runtime?.detachedAt,
63593
63748
  finalReplyAt: entry?.runtime?.finalReplyAt,
63749
+ lastMessageToolReplyAt: entry?.runtime?.lastMessageToolReplyAt,
63750
+ messageToolFinalReplyAt: entry?.runtime?.messageToolFinalReplyAt,
63594
63751
  sessionKey: target.sessionKey,
63595
63752
  agentId: target.agentId
63596
63753
  };
@@ -63602,6 +63759,8 @@ class AgentSessionState {
63602
63759
  startedAt: entry.runtime.startedAt,
63603
63760
  detachedAt: entry.runtime.detachedAt,
63604
63761
  finalReplyAt: entry.runtime.finalReplyAt,
63762
+ lastMessageToolReplyAt: entry.runtime.lastMessageToolReplyAt,
63763
+ messageToolFinalReplyAt: entry.runtime.messageToolFinalReplyAt,
63605
63764
  sessionKey: entry.sessionKey,
63606
63765
  agentId: entry.agentId
63607
63766
  }));
@@ -63735,7 +63894,7 @@ class AgentSessionState {
63735
63894
  }
63736
63895
  return this.resetConversationFollowUpMode(resolved);
63737
63896
  }
63738
- async recordConversationReply(resolved, kind = "reply") {
63897
+ async recordConversationReply(resolved, kind = "reply", source = "channel") {
63739
63898
  const repliedAt = Date.now();
63740
63899
  return this.upsertSessionEntry(resolved, (existing) => ({
63741
63900
  sessionId: existing?.sessionId,
@@ -63744,9 +63903,17 @@ class AgentSessionState {
63744
63903
  lastBotReplyAt: repliedAt
63745
63904
  },
63746
63905
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
63747
- runtime: kind === "final" && existing?.runtime && existing.runtime.state !== "idle" ? {
63906
+ runtime: existing?.runtime && existing.runtime.state !== "idle" ? {
63748
63907
  ...existing.runtime,
63749
- finalReplyAt: repliedAt
63908
+ ...kind === "final" ? {
63909
+ finalReplyAt: repliedAt
63910
+ } : {},
63911
+ ...source === "message-tool" ? {
63912
+ lastMessageToolReplyAt: repliedAt,
63913
+ ...kind === "final" ? {
63914
+ messageToolFinalReplyAt: repliedAt
63915
+ } : {}
63916
+ } : {}
63750
63917
  } : existing?.runtime,
63751
63918
  intervalLoops: existing?.intervalLoops
63752
63919
  }));
@@ -63773,7 +63940,7 @@ function hasActiveRuntime(entry) {
63773
63940
  }
63774
63941
 
63775
63942
  // src/agents/session-store.ts
63776
- import { dirname as dirname10 } from "node:path";
63943
+ import { dirname as dirname11 } from "node:path";
63777
63944
  import { randomUUID as randomUUID3 } from "node:crypto";
63778
63945
  import { rename } from "node:fs/promises";
63779
63946
  class SessionStore {
@@ -63866,7 +64033,7 @@ class SessionStore {
63866
64033
  return parsed;
63867
64034
  }
63868
64035
  async writeStore(store) {
63869
- await ensureDir2(dirname10(this.storePath));
64036
+ await ensureDir2(dirname11(this.storePath));
63870
64037
  const tempPath = `${this.storePath}.${process.pid}.${randomUUID3()}.tmp`;
63871
64038
  await writeTextFile(tempPath, JSON.stringify(store, null, 2));
63872
64039
  await rename(tempPath, this.storePath);
@@ -63874,7 +64041,7 @@ class SessionStore {
63874
64041
  }
63875
64042
 
63876
64043
  // src/control/loops-cli.ts
63877
- function getEditableConfigPath7() {
64044
+ function getEditableConfigPath6() {
63878
64045
  return process.env.CLISBOT_CONFIG_PATH;
63879
64046
  }
63880
64047
  function renderLoopsHelp() {
@@ -63932,7 +64099,7 @@ function getSessionState(sessionStorePath) {
63932
64099
  return new AgentSessionState(new SessionStore(sessionStorePath));
63933
64100
  }
63934
64101
  async function loadLoopControlState() {
63935
- const configPath = await ensureEditableConfigFile(getEditableConfigPath7());
64102
+ const configPath = await ensureEditableConfigFile(getEditableConfigPath6());
63936
64103
  const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
63937
64104
  const sessionStorePath = resolveSessionStorePath(loadedConfig);
63938
64105
  return {
@@ -64001,6 +64168,141 @@ async function runLoopsCli(args) {
64001
64168
  // src/agents/agent-service.ts
64002
64169
  import { randomUUID as randomUUID4 } from "node:crypto";
64003
64170
 
64171
+ // src/channels/agent-prompt.ts
64172
+ function buildAgentPromptText(params) {
64173
+ if (!params.config.enabled) {
64174
+ return params.text;
64175
+ }
64176
+ const systemBlock = renderAgentPromptInstruction(params);
64177
+ return `<system>
64178
+ ${systemBlock}
64179
+ </system>
64180
+
64181
+ <user>
64182
+ ${params.text}
64183
+ </user>`;
64184
+ }
64185
+ function renderAgentPromptInstruction(params) {
64186
+ const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
64187
+ const progressAllowed = messageToolMode && (params.streaming ?? "all") !== "off";
64188
+ const lines = [
64189
+ `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
64190
+ "",
64191
+ "You are operating inside clisbot.",
64192
+ messageToolMode ? progressAllowed ? "To send a user-visible progress update or final reply, use the following CLI command:" : "To send the final user-visible 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"
64193
+ ];
64194
+ if (messageToolMode) {
64195
+ const replyCommand = buildReplyCommand({
64196
+ command: getClisbotPromptCommand(),
64197
+ identity: params.identity
64198
+ });
64199
+ lines.push(replyCommand, "When replying to the user:", "- put the user-facing message inside the --message body of that command", progressAllowed ? "- use that command to send progress updates and the final reply back to the conversation" : "- use that command only for the final user-facing reply", ...progressAllowed ? [`- send at most ${params.config.maxProgressMessages} progress updates`] : ["- do not send user-facing progress updates for this conversation"], params.config.requireFinalResponse ? "- send exactly 1 final user-facing response" : "- final response is optional", ...progressAllowed ? [
64200
+ "- keep progress updates short and meaningful",
64201
+ "- do not send progress updates for trivial internal steps"
64202
+ ] : []);
64203
+ }
64204
+ if (params.protectedControlMutationRule) {
64205
+ lines.push("", params.protectedControlMutationRule);
64206
+ }
64207
+ return lines.join(`
64208
+ `);
64209
+ }
64210
+ function renderPromptTimestamp() {
64211
+ const date = new Date;
64212
+ const formatter = new Intl.DateTimeFormat("en-CA", {
64213
+ year: "numeric",
64214
+ month: "2-digit",
64215
+ day: "2-digit",
64216
+ hour: "2-digit",
64217
+ minute: "2-digit",
64218
+ second: "2-digit",
64219
+ hour12: false,
64220
+ timeZoneName: "shortOffset"
64221
+ });
64222
+ return formatter.format(date).replace(",", "");
64223
+ }
64224
+ function renderIdentitySummary(identity) {
64225
+ const segments = [renderConversationSummary(identity)];
64226
+ const sender = renderSenderSummary(identity);
64227
+ if (sender) {
64228
+ segments.push(sender);
64229
+ }
64230
+ return segments.join(" | ");
64231
+ }
64232
+ function renderConversationSummary(identity) {
64233
+ if (identity.platform === "slack") {
64234
+ const scopeLabel = identity.conversationKind === "dm" ? "Slack direct message" : identity.conversationKind === "group" ? "Slack group" : "Slack channel";
64235
+ const segments = [scopeLabel];
64236
+ const channel = renderLabeledTarget(identity.channelName, identity.channelId, "#");
64237
+ if (channel) {
64238
+ segments.push(channel);
64239
+ }
64240
+ if (identity.threadTs) {
64241
+ segments.push(`thread ${identity.threadTs}`);
64242
+ }
64243
+ return segments.join(" ");
64244
+ }
64245
+ if (identity.conversationKind === "dm") {
64246
+ return ["Telegram direct message", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
64247
+ }
64248
+ if (identity.conversationKind === "topic") {
64249
+ const topic = renderNamedValue("topic", identity.topicName, identity.topicId);
64250
+ const group = renderNamedValue("in group", identity.chatName, identity.chatId);
64251
+ return [topic, group].filter(Boolean).join(" ");
64252
+ }
64253
+ return ["Telegram group", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
64254
+ }
64255
+ function renderSenderSummary(identity) {
64256
+ const sender = renderLabeledTarget(identity.senderName, identity.senderId);
64257
+ return sender ? `sender ${sender}` : "";
64258
+ }
64259
+ function renderLabeledTarget(name, id, namePrefix = "") {
64260
+ const normalizedName = name?.trim();
64261
+ const normalizedId = id?.trim();
64262
+ if (normalizedName && normalizedId) {
64263
+ return `${namePrefix}${normalizedName} (${normalizedId})`;
64264
+ }
64265
+ if (normalizedName) {
64266
+ return `${namePrefix}${normalizedName}`;
64267
+ }
64268
+ return normalizedId ?? "";
64269
+ }
64270
+ function renderNamedValue(label, name, id) {
64271
+ const value = renderLabeledTarget(name, id);
64272
+ return value ? `${label} ${value}` : "";
64273
+ }
64274
+ function buildReplyCommand(params) {
64275
+ const lines = [`${params.command} message send \\`];
64276
+ if (params.identity.platform === "slack") {
64277
+ lines.push(" --channel slack \\");
64278
+ lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
64279
+ if (params.identity.threadTs) {
64280
+ lines.push(` --thread-id ${params.identity.threadTs} \\`);
64281
+ }
64282
+ lines.push(" --final \\");
64283
+ lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
64284
+ lines.push("<short progress update>");
64285
+ lines.push("__CLISBOT_MESSAGE__");
64286
+ lines.push(')" \\');
64287
+ lines.push(" [--media /absolute/path/to/file]");
64288
+ return lines.join(`
64289
+ `);
64290
+ }
64291
+ lines.push(" --channel telegram \\");
64292
+ lines.push(` --target ${params.identity.chatId ?? ""} \\`);
64293
+ if (params.identity.topicId) {
64294
+ lines.push(` --thread-id ${params.identity.topicId} \\`);
64295
+ }
64296
+ lines.push(" --final \\");
64297
+ lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
64298
+ lines.push("<short progress update>");
64299
+ lines.push("__CLISBOT_MESSAGE__");
64300
+ lines.push(')" \\');
64301
+ lines.push(" [--media /absolute/path/to/file]");
64302
+ return lines.join(`
64303
+ `);
64304
+ }
64305
+
64004
64306
  // src/agents/session-key.ts
64005
64307
  var DEFAULT_MAIN_KEY = "main";
64006
64308
  var DEFAULT_ACCOUNT_ID = "default";
@@ -64272,7 +64574,7 @@ class AgentJobQueue {
64272
64574
  }
64273
64575
 
64274
64576
  // src/agents/runner-session.ts
64275
- import { dirname as dirname11 } from "node:path";
64577
+ import { dirname as dirname12 } from "node:path";
64276
64578
 
64277
64579
  // src/shared/transcript-normalization.ts
64278
64580
  import { stripVTControlCharacters } from "node:util";
@@ -64393,6 +64695,12 @@ function looksLikeClaudeSnapshot(lines) {
64393
64695
  return trimmed.includes("Claude Code v") || trimmed.includes("Welcome back!") || trimmed.includes("Tips for getting started") || trimmed.startsWith("❯") || trimmed.startsWith("⏺");
64394
64696
  });
64395
64697
  }
64698
+ function looksLikeGeminiSnapshot(lines) {
64699
+ return lines.some((line) => {
64700
+ const trimmed = line.trim();
64701
+ 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)");
64702
+ });
64703
+ }
64396
64704
  function isProgressLine(line) {
64397
64705
  const trimmed = line.trim();
64398
64706
  const normalized = trimmed.replace(/^(?::eight_spoked_asterisk:|[✽✶])\s+/, "");
@@ -64491,6 +64799,9 @@ function dropClaudePromptBlocks(lines) {
64491
64799
  }
64492
64800
  return filtered;
64493
64801
  }
64802
+ function dropGeminiPromptBlocks(lines) {
64803
+ return dropPromptBlocks(lines, /^\s*>\s/);
64804
+ }
64494
64805
  function isInterruptStatusLine(line) {
64495
64806
  const trimmed = line.trim();
64496
64807
  if (!trimmed) {
@@ -64512,6 +64823,13 @@ function shouldDropClaudeChromeLine(line) {
64512
64823
  }
64513
64824
  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
64825
  }
64826
+ function shouldDropGeminiChromeLine(line) {
64827
+ const trimmed = line.trim();
64828
+ if (!trimmed) {
64829
+ return false;
64830
+ }
64831
+ 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);
64832
+ }
64515
64833
  function normalizeBoundaryLine(line) {
64516
64834
  return line.trim().replace(/^(?::eight_spoked_asterisk:|[-*•◦·✽✶])\s+/, "");
64517
64835
  }
@@ -64548,7 +64866,8 @@ function cleanInteractionSnapshot(raw) {
64548
64866
  const lines = splitNormalizedLines(raw);
64549
64867
  const isCodex = looksLikeCodexSnapshot(lines);
64550
64868
  const isClaude = looksLikeClaudeSnapshot(lines);
64551
- const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : lines;
64869
+ const isGemini = looksLikeGeminiSnapshot(lines);
64870
+ const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : isGemini ? dropGeminiPromptBlocks(lines) : lines;
64552
64871
  const filtered = promptStripped.filter((line) => {
64553
64872
  if (shouldDropDeliveryReportLine(line)) {
64554
64873
  return false;
@@ -64559,9 +64878,12 @@ function cleanInteractionSnapshot(raw) {
64559
64878
  if (isClaude && shouldDropClaudeChromeLine(line)) {
64560
64879
  return false;
64561
64880
  }
64881
+ if (isGemini && shouldDropGeminiChromeLine(line)) {
64882
+ return false;
64883
+ }
64562
64884
  return true;
64563
64885
  });
64564
- const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : filtered;
64886
+ const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : isGemini ? filtered.map((line) => line.replace(/^\s*>\s*/, "")) : filtered;
64565
64887
  const unwrapped = unwrapSoftWrappedLines(normalized);
64566
64888
  return collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
64567
64889
  `));
@@ -64747,6 +65069,7 @@ function extractFinalAnswer(raw) {
64747
65069
  const rawLines = splitNormalizedLines(raw);
64748
65070
  const isCodex = looksLikeCodexSnapshot(rawLines);
64749
65071
  const isClaude = looksLikeClaudeSnapshot(rawLines);
65072
+ const isGemini = looksLikeGeminiSnapshot(rawLines);
64750
65073
  const cleaned = cleanInteractionSnapshot(raw);
64751
65074
  if (!cleaned) {
64752
65075
  return "";
@@ -64773,14 +65096,14 @@ function extractFinalAnswer(raw) {
64773
65096
  if (answerBlocks.length > 1 && answerBlocks.every(isShortAtomicAnswerBlock)) {
64774
65097
  const lastAnswer = answerBlocks.at(-1)?.trim() ?? "";
64775
65098
  if (lastAnswer) {
64776
- return isCodex || isClaude ? stripSingleLineAssistantEnvelope(lastAnswer) : lastAnswer;
65099
+ return isCodex || isClaude || isGemini ? stripSingleLineAssistantEnvelope(lastAnswer) : lastAnswer;
64777
65100
  }
64778
65101
  }
64779
65102
  const answer = answerBlocks.join(`
64780
65103
 
64781
65104
  `).trim();
64782
65105
  const extracted = answer || cleaned;
64783
- if (isCodex || isClaude) {
65106
+ if (isCodex || isClaude || isGemini) {
64784
65107
  return stripSingleLineAssistantEnvelope(extracted);
64785
65108
  }
64786
65109
  return extracted;
@@ -64815,7 +65138,11 @@ ${body}` : queueNote;
64815
65138
  _Timed out waiting for more output._` : "_Timed out waiting for visible output._";
64816
65139
  }
64817
65140
  if (params.status === "detached") {
64818
- const note = params.note ?? "This session is still running. Use `/transcript` anytime to check it.";
65141
+ const note = resolveDetachedInteractionNote({
65142
+ baseNote: params.note,
65143
+ allowTranscriptInspection: params.allowTranscriptInspection,
65144
+ transcriptCommand: "`/transcript`"
65145
+ });
64819
65146
  return body ? `${body}
64820
65147
 
64821
65148
  _${note}_` : `_${note}_`;
@@ -64844,7 +65171,11 @@ ${body}` : queueNote;
64844
65171
  Timed out waiting for more output.` : "Timed out waiting for visible output.";
64845
65172
  }
64846
65173
  if (params.status === "detached") {
64847
- const note = params.note ?? "This session is still running. Use /transcript anytime to check it.";
65174
+ const note = resolveDetachedInteractionNote({
65175
+ baseNote: params.note,
65176
+ allowTranscriptInspection: params.allowTranscriptInspection,
65177
+ transcriptCommand: "/transcript"
65178
+ });
64848
65179
  return body ? `${body}
64849
65180
 
64850
65181
  ${note}` : note;
@@ -64880,6 +65211,16 @@ ${body}
64880
65211
  }
64881
65212
  var renderSlackSnapshot = renderSlackTranscript;
64882
65213
  var renderChannelSnapshot = renderSlackSnapshot;
65214
+ function resolveDetachedInteractionNote(params) {
65215
+ const note = params.baseNote ?? "This session is still running. Use `/attach`, `/watch every 30s`, or `/stop` to manage it.";
65216
+ if (!params.allowTranscriptInspection) {
65217
+ return note;
65218
+ }
65219
+ if (note.includes("/transcript")) {
65220
+ return note;
65221
+ }
65222
+ return `${note} You can also use ${params.transcriptCommand} to inspect the current session snapshot.`;
65223
+ }
64883
65224
  // src/control/latency-debug.ts
64884
65225
  function isLatencyDebugEnabled() {
64885
65226
  return process.env.CLISBOT_DEBUG_LATENCY === "1";
@@ -64899,14 +65240,23 @@ function logLatencyDebug(stage, context = {}, details = {}) {
64899
65240
  // src/runners/tmux/session-handshake.ts
64900
65241
  var TRUST_PROMPT_POLL_INTERVAL_MS = 250;
64901
65242
  var TRUST_PROMPT_MAX_WAIT_MS = 1e4;
64902
- var TRUST_PROMPT_SETTLE_DELAY_MS = 1500;
64903
65243
  var SESSION_BOOTSTRAP_POLL_INTERVAL_MS = 100;
65244
+ var PASTE_SETTLE_POLL_INTERVAL_MS = 40;
65245
+ var PASTE_SETTLE_QUIET_WINDOW_MS = 60;
65246
+ var PASTE_SETTLE_MULTILINE_MAX_WAIT_MS = 800;
65247
+ var PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS = 80;
64904
65248
  var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
64905
65249
  var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
64906
65250
  async function submitTmuxSessionInput(params) {
65251
+ const prePasteState = await params.tmux.getPaneState(params.sessionName);
64907
65252
  await params.tmux.sendLiteral(params.sessionName, params.text);
64908
- await sleep(params.promptSubmitDelayMs);
64909
- const preSubmitState = await params.tmux.getPaneState(params.sessionName);
65253
+ const preSubmitState = await waitForPanePasteSettlement({
65254
+ tmux: params.tmux,
65255
+ sessionName: params.sessionName,
65256
+ baseline: prePasteState,
65257
+ text: params.text,
65258
+ minDelayMs: params.promptSubmitDelayMs
65259
+ });
64910
65260
  await params.tmux.sendKey(params.sessionName, "Enter");
64911
65261
  if (await waitForPaneSubmitConfirmation({
64912
65262
  tmux: params.tmux,
@@ -64932,7 +65282,6 @@ async function submitTmuxSessionInput(params) {
64932
65282
  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
65283
  }
64934
65284
  async function captureTmuxSessionIdentity(params) {
64935
- let deadline = Date.now() + params.timeoutMs;
64936
65285
  await submitTmuxSessionInput({
64937
65286
  tmux: params.tmux,
64938
65287
  sessionName: params.sessionName,
@@ -64940,11 +65289,16 @@ async function captureTmuxSessionIdentity(params) {
64940
65289
  promptSubmitDelayMs: params.promptSubmitDelayMs,
64941
65290
  timingContext: undefined
64942
65291
  });
65292
+ let deadline = Date.now() + params.timeoutMs;
64943
65293
  while (Date.now() < deadline) {
64944
65294
  await sleep(params.pollIntervalMs);
64945
65295
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
64946
65296
  if (hasTrustPrompt(snapshot)) {
64947
- await dismissTrustPrompt(params.tmux, params.sessionName);
65297
+ await dismissTrustPrompt({
65298
+ tmux: params.tmux,
65299
+ sessionName: params.sessionName,
65300
+ captureLines: params.captureLines
65301
+ });
64948
65302
  deadline = Date.now() + params.timeoutMs;
64949
65303
  await submitTmuxSessionInput({
64950
65304
  tmux: params.tmux,
@@ -64973,11 +65327,21 @@ async function dismissTmuxTrustPromptIfPresent(params) {
64973
65327
  if (!hasTrustPrompt(snapshot)) {
64974
65328
  return;
64975
65329
  }
64976
- await dismissTrustPrompt(params.tmux, params.sessionName);
65330
+ await dismissTrustPrompt({
65331
+ tmux: params.tmux,
65332
+ sessionName: params.sessionName,
65333
+ captureLines: params.captureLines
65334
+ });
64977
65335
  }
64978
65336
  }
64979
65337
  async function waitForTmuxSessionBootstrap(params) {
64980
65338
  const deadline = Date.now() + Math.max(params.startupDelayMs, SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65339
+ const readyRegex = params.readyPattern ? new RegExp(params.readyPattern, "i") : null;
65340
+ const blockerPatterns = (params.blockers ?? []).map((entry) => ({
65341
+ regex: new RegExp(entry.pattern, "i"),
65342
+ message: entry.message
65343
+ }));
65344
+ let lastSnapshot = "";
64981
65345
  while (Date.now() <= deadline) {
64982
65346
  let snapshot = "";
64983
65347
  try {
@@ -64985,23 +65349,60 @@ async function waitForTmuxSessionBootstrap(params) {
64985
65349
  } catch (error) {
64986
65350
  const message = error instanceof Error ? error.message : String(error);
64987
65351
  if (message.includes("can't find session:") || message.includes("no server running on ")) {
64988
- return "";
65352
+ return {
65353
+ status: "timeout",
65354
+ snapshot: lastSnapshot
65355
+ };
64989
65356
  }
64990
65357
  throw error;
64991
65358
  }
64992
65359
  if (snapshot) {
64993
- return snapshot;
65360
+ lastSnapshot = snapshot;
65361
+ if (params.trustWorkspace && hasTrustPrompt(snapshot)) {
65362
+ await dismissTrustPrompt({
65363
+ tmux: params.tmux,
65364
+ sessionName: params.sessionName,
65365
+ captureLines: params.captureLines
65366
+ });
65367
+ await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65368
+ continue;
65369
+ }
65370
+ for (const blocker of blockerPatterns) {
65371
+ if (blocker.regex.test(snapshot)) {
65372
+ return {
65373
+ status: "blocked",
65374
+ snapshot,
65375
+ message: blocker.message
65376
+ };
65377
+ }
65378
+ }
65379
+ if (readyRegex && !readyRegex.test(snapshot)) {
65380
+ await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
65381
+ continue;
65382
+ }
65383
+ return {
65384
+ status: "ready",
65385
+ snapshot
65386
+ };
64994
65387
  }
64995
65388
  await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
64996
65389
  }
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");
65390
+ return {
65391
+ status: "timeout",
65392
+ snapshot: lastSnapshot
65393
+ };
65001
65394
  }
65002
- async function dismissTrustPrompt(tmux, sessionName) {
65003
- await tmux.sendKey(sessionName, "Enter");
65004
- await sleep(TRUST_PROMPT_SETTLE_DELAY_MS);
65395
+ async function dismissTrustPrompt(params) {
65396
+ await params.tmux.sendKey(params.sessionName, "Enter");
65397
+ const deadline = Date.now() + TRUST_PROMPT_MAX_WAIT_MS;
65398
+ while (Date.now() <= deadline) {
65399
+ await sleep(TRUST_PROMPT_POLL_INTERVAL_MS);
65400
+ const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
65401
+ if (!snapshot || hasTrustPrompt(snapshot)) {
65402
+ continue;
65403
+ }
65404
+ return;
65405
+ }
65005
65406
  }
65006
65407
  async function waitForPaneSubmitConfirmation(params) {
65007
65408
  const deadline = Date.now() + SUBMIT_CONFIRM_MAX_WAIT_MS;
@@ -65017,9 +65418,50 @@ async function waitForPaneSubmitConfirmation(params) {
65017
65418
  await sleep(Math.min(SUBMIT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
65018
65419
  }
65019
65420
  }
65421
+ async function waitForPanePasteSettlement(params) {
65422
+ await sleep(params.minDelayMs);
65423
+ let currentState = await params.tmux.getPaneState(params.sessionName);
65424
+ let sawChange = hasPaneStateChanged(params.baseline, currentState);
65425
+ let lastChangeAt = Date.now();
65426
+ const deadline = Date.now() + (shouldWaitForVisiblePaste(params.text) ? PASTE_SETTLE_MULTILINE_MAX_WAIT_MS : PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS);
65427
+ while (true) {
65428
+ if (sawChange && Date.now() - lastChangeAt >= PASTE_SETTLE_QUIET_WINDOW_MS) {
65429
+ return currentState;
65430
+ }
65431
+ const remainingMs = deadline - Date.now();
65432
+ if (remainingMs <= 0) {
65433
+ return currentState;
65434
+ }
65435
+ await sleep(Math.min(PASTE_SETTLE_POLL_INTERVAL_MS, remainingMs));
65436
+ const nextState = await params.tmux.getPaneState(params.sessionName);
65437
+ if (!arePaneStatesEqual(currentState, nextState)) {
65438
+ currentState = nextState;
65439
+ if (hasPaneStateChanged(params.baseline, currentState)) {
65440
+ sawChange = true;
65441
+ }
65442
+ lastChangeAt = Date.now();
65443
+ }
65444
+ }
65445
+ }
65020
65446
  function hasPaneStateChanged(left, right) {
65021
65447
  return left.cursorX !== right.cursorX || left.cursorY !== right.cursorY || left.historySize !== right.historySize;
65022
65448
  }
65449
+ function arePaneStatesEqual(left, right) {
65450
+ return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
65451
+ }
65452
+ function looksLikeClaudeTrustPrompt(snapshot) {
65453
+ return snapshot.includes("Quick safety check:") && snapshot.includes("Yes, I trust this folder") || snapshot.includes("Enter to confirm · Esc to cancel");
65454
+ }
65455
+ function looksLikeGeminiTrustPrompt(snapshot) {
65456
+ 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)");
65457
+ }
65458
+ function hasTrustPrompt(snapshot) {
65459
+ return snapshot.includes("Do you trust the contents of this directory?") || snapshot.includes("Press enter to continue") || looksLikeClaudeTrustPrompt(snapshot) || looksLikeGeminiTrustPrompt(snapshot);
65460
+ }
65461
+ function shouldWaitForVisiblePaste(text) {
65462
+ return text.includes(`
65463
+ `);
65464
+ }
65023
65465
 
65024
65466
  // src/runners/tmux/shell-command.ts
65025
65467
  var BASH_WINDOW_NAME = "bash";
@@ -65129,23 +65571,10 @@ function escapeRegExp(raw) {
65129
65571
  // src/agents/runner-session.ts
65130
65572
  var TMUX_MISSING_SESSION_PATTERN = /(?:can't find session:|no server running on )/i;
65131
65573
  var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
65132
- function shellQuote2(value) {
65133
- if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
65134
- return value;
65135
- }
65136
- return `'${value.replaceAll("'", `'"'"'`)}'`;
65137
- }
65138
- function buildCommandString(command, args) {
65139
- return [command, ...args].map(shellQuote2).join(" ");
65140
- }
65141
- function buildRunnerLaunchCommand(command, args) {
65142
- const wrapperDir = getClisbotWrapperDir();
65143
- const wrapperPath = getClisbotWrapperPath();
65144
- const exports = [
65145
- `export PATH=${shellQuote2(wrapperDir)}:"$PATH"`,
65146
- `export CLISBOT_BIN=${shellQuote2(wrapperPath)}`
65147
- ];
65148
- return `${exports.join("; ")}; exec ${buildCommandString(command, args)}`;
65574
+ function summarizeSnapshot(snapshot) {
65575
+ const compact = snapshot.split(`
65576
+ `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
65577
+ return compact ? ` Last visible pane: ${compact}` : "";
65149
65578
  }
65150
65579
  function isTmuxDuplicateSessionError(error) {
65151
65580
  const message = error instanceof Error ? error.message : String(error);
@@ -65167,8 +65596,17 @@ class RunnerSessionService {
65167
65596
  this.sessionState = sessionState;
65168
65597
  this.resolveTarget = resolveTarget;
65169
65598
  }
65170
- mapSessionError(error, sessionName, action) {
65599
+ async mapSessionError(error, sessionName, action, lastSnapshot = "") {
65171
65600
  if (isMissingTmuxSessionError(error)) {
65601
+ const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, sessionName);
65602
+ console.error("runner session disappeared", {
65603
+ sessionName,
65604
+ action,
65605
+ exitCode: exitRecord?.exitCode,
65606
+ exitedAt: exitRecord?.exitedAt,
65607
+ runnerCommand: exitRecord?.command,
65608
+ lastVisiblePane: lastSnapshot ? summarizeSnapshot(lastSnapshot).trim() : undefined
65609
+ });
65172
65610
  return new Error(`Runner session "${sessionName}" disappeared ${action}.`);
65173
65611
  }
65174
65612
  return error instanceof Error ? error : new Error(String(error));
@@ -65239,7 +65677,11 @@ class RunnerSessionService {
65239
65677
  allowFreshRetry: options.nextAllowFreshRetry
65240
65678
  });
65241
65679
  }
65242
- async runSessionCleanup() {
65680
+ async abortUnreadySession(resolved, reason, snapshot) {
65681
+ await this.tmux.killSession(resolved.sessionName);
65682
+ throw new Error(`${reason}${summarizeSnapshot(snapshot)}`);
65683
+ }
65684
+ async runSessionCleanup() {
65243
65685
  if (this.cleanupInFlight) {
65244
65686
  return;
65245
65687
  }
@@ -65283,7 +65725,8 @@ class RunnerSessionService {
65283
65725
  };
65284
65726
  logLatencyDebug("ensure-session-ready-start", timingContext);
65285
65727
  await ensureDir2(resolved.workspacePath);
65286
- await ensureDir2(dirname11(this.loadedConfig.raw.tmux.socketPath));
65728
+ await ensureDir2(dirname12(this.loadedConfig.raw.tmux.socketPath));
65729
+ await ensureRunnerExitRecordDir(this.loadedConfig.stateDir, resolved.sessionName);
65287
65730
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
65288
65731
  const serverRunning = await this.tmux.isServerRunning();
65289
65732
  if (serverRunning && await this.tmux.hasSession(resolved.sessionName)) {
@@ -65291,9 +65734,10 @@ class RunnerSessionService {
65291
65734
  hasStoredSessionId: Boolean(existing?.sessionId)
65292
65735
  });
65293
65736
  try {
65737
+ await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
65294
65738
  await this.syncSessionIdentity(resolved);
65295
65739
  } catch (error) {
65296
- throw this.mapSessionError(error, resolved.sessionName, "during startup");
65740
+ throw await this.mapSessionError(error, resolved.sessionName, "during startup");
65297
65741
  }
65298
65742
  logLatencyDebug("ensure-session-ready-complete", timingContext, {
65299
65743
  startupDelayMs: 0,
@@ -65310,7 +65754,15 @@ class RunnerSessionService {
65310
65754
  sessionId: startupSessionId || undefined,
65311
65755
  resume: resumingExistingSession
65312
65756
  });
65313
- const command = buildRunnerLaunchCommand(runnerLaunch.command, runnerLaunch.args);
65757
+ await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
65758
+ const command = buildRunnerLaunchCommand({
65759
+ command: runnerLaunch.command,
65760
+ args: runnerLaunch.args,
65761
+ wrapperDir: getClisbotWrapperDir(),
65762
+ wrapperPath: getClisbotWrapperPath(),
65763
+ sessionName: resolved.sessionName,
65764
+ stateDir: this.loadedConfig.stateDir
65765
+ });
65314
65766
  try {
65315
65767
  await this.tmux.newSession({
65316
65768
  sessionName: resolved.sessionName,
@@ -65328,11 +65780,14 @@ class RunnerSessionService {
65328
65780
  resumingExistingSession,
65329
65781
  hasStoredSessionId: Boolean(existing?.sessionId)
65330
65782
  });
65331
- await waitForTmuxSessionBootstrap({
65783
+ const bootstrapResult = await waitForTmuxSessionBootstrap({
65332
65784
  tmux: this.tmux,
65333
65785
  sessionName: resolved.sessionName,
65334
65786
  captureLines: resolved.stream.captureLines,
65335
- startupDelayMs: resolved.runner.startupDelayMs
65787
+ startupDelayMs: resolved.runner.startupDelayMs,
65788
+ trustWorkspace: resolved.runner.trustWorkspace,
65789
+ readyPattern: resolved.runner.startupReadyPattern,
65790
+ blockers: resolved.runner.startupBlockers
65336
65791
  });
65337
65792
  const sessionStillExists = await this.tmux.hasSession(resolved.sessionName);
65338
65793
  if (!sessionStillExists) {
@@ -65347,6 +65802,12 @@ class RunnerSessionService {
65347
65802
  }
65348
65803
  throw new Error(`Runner session "${resolved.sessionName}" disappeared during startup.`);
65349
65804
  }
65805
+ if (bootstrapResult.status === "blocked") {
65806
+ await this.abortUnreadySession(resolved, bootstrapResult.message, bootstrapResult.snapshot);
65807
+ }
65808
+ if (bootstrapResult.status === "timeout" && resolved.runner.startupReadyPattern) {
65809
+ await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
65810
+ }
65350
65811
  try {
65351
65812
  await this.finalizeSessionStartup(target, resolved, {
65352
65813
  startupSessionId,
@@ -65355,7 +65816,7 @@ class RunnerSessionService {
65355
65816
  allowFreshRetry: options.allowFreshRetry
65356
65817
  });
65357
65818
  } catch (error) {
65358
- throw this.mapSessionError(error, resolved.sessionName, "during startup");
65819
+ throw await this.mapSessionError(error, resolved.sessionName, "during startup");
65359
65820
  }
65360
65821
  logLatencyDebug("ensure-session-ready-complete", timingContext, {
65361
65822
  startupDelayMs: resolved.runner.startupDelayMs,
@@ -65424,14 +65885,14 @@ class RunnerSessionService {
65424
65885
  } catch (error) {
65425
65886
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
65426
65887
  if (options.allowFreshRetryBeforePrompt === false || !existing?.sessionId || !isMissingTmuxSessionError(error)) {
65427
- throw this.mapSessionError(error, resolved.sessionName, "before prompt submission");
65888
+ throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65428
65889
  }
65429
65890
  const retried = await this.retryFreshStartWithClearedSessionId(target, resolved, {
65430
65891
  allowRetry: true,
65431
65892
  nextAllowFreshRetry: false
65432
65893
  });
65433
65894
  if (!retried) {
65434
- throw this.mapSessionError(error, resolved.sessionName, "before prompt submission");
65895
+ throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65435
65896
  }
65436
65897
  resolved = retried;
65437
65898
  return {
@@ -65550,8 +66011,8 @@ class RunnerSessionService {
65550
66011
  workspacePath: resolved.workspacePath
65551
66012
  };
65552
66013
  }
65553
- mapRunError(error, sessionName) {
65554
- return this.mapSessionError(error, sessionName, "while the prompt was running");
66014
+ async mapRunError(error, sessionName, lastSnapshot = "") {
66015
+ return await this.mapSessionError(error, sessionName, "while the prompt was running", lastSnapshot);
65555
66016
  }
65556
66017
  }
65557
66018
 
@@ -65710,6 +66171,7 @@ class ActiveRunManager {
65710
66171
  runnerSessions;
65711
66172
  resolveTarget;
65712
66173
  activeRuns = new Map;
66174
+ stopping = false;
65713
66175
  constructor(tmux, sessionState, runnerSessions, resolveTarget) {
65714
66176
  this.tmux = tmux;
65715
66177
  this.sessionState = sessionState;
@@ -65759,6 +66221,9 @@ class ActiveRunManager {
65759
66221
  }
65760
66222
  }
65761
66223
  async executePrompt(target, prompt, observer, options = {}) {
66224
+ if (this.stopping) {
66225
+ throw new Error("Runtime is stopping and cannot accept a new prompt.");
66226
+ }
65762
66227
  const existingActiveRun = this.activeRuns.get(target.sessionKey);
65763
66228
  if (existingActiveRun) {
65764
66229
  throw new ActiveRunInProgressError(existingActiveRun.latestUpdate);
@@ -65804,6 +66269,9 @@ class ActiveRunManager {
65804
66269
  const startedAt = Date.now();
65805
66270
  const run = this.activeRuns.get(provisionalResolved.sessionKey);
65806
66271
  if (!run) {
66272
+ if (this.stopping) {
66273
+ throw new Error("Runtime stopped before the active run finished startup.");
66274
+ }
65807
66275
  throw new Error(`Active run disappeared during startup for ${provisionalResolved.sessionKey}.`);
65808
66276
  }
65809
66277
  run.resolved = resolved;
@@ -65880,6 +66348,16 @@ class ActiveRunManager {
65880
66348
  hasActiveRun(target) {
65881
66349
  return this.activeRuns.has(target.sessionKey);
65882
66350
  }
66351
+ async stop() {
66352
+ this.stopping = true;
66353
+ const activeRuns = [...this.activeRuns.values()];
66354
+ for (const run of activeRuns) {
66355
+ await this.sessionState.setSessionRuntime(run.resolved, {
66356
+ state: "idle"
66357
+ });
66358
+ }
66359
+ this.activeRuns.clear();
66360
+ }
65883
66361
  buildDetachedNote(resolved) {
65884
66362
  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
66363
  }
@@ -66046,7 +66524,7 @@ class ActiveRunManager {
66046
66524
  }
66047
66525
  });
66048
66526
  } catch (error) {
66049
- await this.failActiveRun(sessionKey, this.runnerSessions.mapRunError(error, run.resolved.sessionName));
66527
+ await this.failActiveRun(sessionKey, await this.runnerSessions.mapRunError(error, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
66050
66528
  }
66051
66529
  })();
66052
66530
  }
@@ -66060,6 +66538,7 @@ class AgentService {
66060
66538
  sessionState;
66061
66539
  runnerSessions;
66062
66540
  activeRuns;
66541
+ stopping = false;
66063
66542
  cleanupTimer;
66064
66543
  loopTimers = new Set;
66065
66544
  intervalLoops = new Map;
@@ -66095,6 +66574,7 @@ class AgentService {
66095
66574
  }, cleanup.intervalMinutes * 60000);
66096
66575
  }
66097
66576
  async stop() {
66577
+ this.stopping = true;
66098
66578
  if (this.cleanupTimer) {
66099
66579
  clearInterval(this.cleanupTimer);
66100
66580
  this.cleanupTimer = undefined;
@@ -66109,6 +66589,7 @@ class AgentService {
66109
66589
  clearTimeout(timer);
66110
66590
  }
66111
66591
  this.loopTimers.clear();
66592
+ await this.activeRuns.stop();
66112
66593
  }
66113
66594
  async cleanupStaleSessions() {
66114
66595
  await this.runnerSessions.runSessionCleanup();
@@ -66149,8 +66630,8 @@ class AgentService {
66149
66630
  sessionKey: this.loadedConfig.raw.session.mainKey
66150
66631
  });
66151
66632
  }
66152
- async recordConversationReply(target, kind = "reply") {
66153
- return this.sessionState.recordConversationReply(this.resolveTarget(target), kind);
66633
+ async recordConversationReply(target, kind = "reply", source = "channel") {
66634
+ return this.sessionState.recordConversationReply(this.resolveTarget(target), kind, source);
66154
66635
  }
66155
66636
  async runShellCommand(target, command) {
66156
66637
  return this.queue.enqueue(`${target.sessionKey}:bash`, async () => this.runnerSessions.runShellCommand(target, command)).result;
@@ -66207,10 +66688,13 @@ class AgentService {
66207
66688
  updatedAt: Date.now(),
66208
66689
  nextRunAt: Date.now(),
66209
66690
  promptText: params.promptText,
66691
+ canonicalPromptText: params.canonicalPromptText,
66692
+ protectedControlMutationRule: params.protectedControlMutationRule,
66210
66693
  promptSummary: params.promptSummary,
66211
66694
  promptSource: params.promptSource,
66212
66695
  createdBy: params.createdBy,
66213
- force: params.force
66696
+ force: params.force,
66697
+ surfaceBinding: params.surfaceBinding
66214
66698
  };
66215
66699
  const resolved = this.resolveTarget(params.target);
66216
66700
  await this.sessionState.setIntervalLoop(resolved, loop);
@@ -66248,6 +66732,8 @@ class AgentService {
66248
66732
  updatedAt: Date.now(),
66249
66733
  nextRunAt,
66250
66734
  promptText: params.promptText,
66735
+ canonicalPromptText: params.canonicalPromptText,
66736
+ protectedControlMutationRule: params.protectedControlMutationRule,
66251
66737
  promptSummary: params.promptSummary,
66252
66738
  promptSource: params.promptSource,
66253
66739
  createdBy: params.createdBy,
@@ -66257,7 +66743,8 @@ class AgentService {
66257
66743
  hour: params.hour,
66258
66744
  minute: params.minute,
66259
66745
  timezone: params.timezone,
66260
- force: false
66746
+ force: false,
66747
+ surfaceBinding: params.surfaceBinding
66261
66748
  };
66262
66749
  const resolved = this.resolveTarget(params.target);
66263
66750
  await this.sessionState.setIntervalLoop(resolved, loop);
@@ -66413,13 +66900,17 @@ class AgentService {
66413
66900
  this.dropManagedIntervalLoop(loopId);
66414
66901
  return;
66415
66902
  }
66416
- const { result } = this.enqueuePrompt(managed.target, nextLoopState.promptText, {
66903
+ const promptText = this.buildManagedLoopPrompt(managed.target.agentId, nextLoopState);
66904
+ const { result } = this.enqueuePrompt(managed.target, promptText, {
66417
66905
  observerId: `loop:${loopId}:${attemptedRuns}`,
66418
66906
  onUpdate: async () => {
66419
66907
  return;
66420
66908
  }
66421
66909
  });
66422
66910
  result.catch((error) => {
66911
+ if (this.shouldSuppressLoopShutdownError(error)) {
66912
+ return;
66913
+ }
66423
66914
  console.error("loop execution failed", error);
66424
66915
  });
66425
66916
  if (attemptedRuns >= managed.loop.maxRuns) {
@@ -66445,6 +66936,9 @@ class AgentService {
66445
66936
  }
66446
66937
  current.timer = undefined;
66447
66938
  this.runIntervalLoopIteration(loopId).catch((error) => {
66939
+ if (this.shouldSuppressLoopShutdownError(error)) {
66940
+ return;
66941
+ }
66448
66942
  console.error("loop execution failed", error);
66449
66943
  });
66450
66944
  }, delayMs);
@@ -66459,6 +66953,13 @@ class AgentService {
66459
66953
  managed.loop = nextLoopState;
66460
66954
  return true;
66461
66955
  }
66956
+ shouldSuppressLoopShutdownError(error) {
66957
+ if (!this.stopping) {
66958
+ return false;
66959
+ }
66960
+ const message = error instanceof Error ? error.message : String(error);
66961
+ return /Runtime stopped before the active run finished startup|Runtime is stopping and cannot accept a new prompt/i.test(message);
66962
+ }
66462
66963
  computeNextManagedLoopRunAtMs(loop, nowMs) {
66463
66964
  if (loop.kind === "calendar") {
66464
66965
  return computeNextCalendarLoopRunAtMs({
@@ -66472,6 +66973,52 @@ class AgentService {
66472
66973
  }
66473
66974
  return nowMs + loop.intervalMs;
66474
66975
  }
66976
+ buildManagedLoopPrompt(agentId, loop) {
66977
+ if (!loop.canonicalPromptText || !loop.surfaceBinding) {
66978
+ return loop.promptText;
66979
+ }
66980
+ const identity = this.buildLoopChannelIdentity(loop.surfaceBinding);
66981
+ const channelConfig = identity.platform === "slack" ? this.loadedConfig.raw.channels.slack : this.loadedConfig.raw.channels.telegram;
66982
+ const { responseMode, streaming } = this.resolveLoopSurfaceModes(identity);
66983
+ return buildAgentPromptText({
66984
+ text: loop.canonicalPromptText,
66985
+ identity,
66986
+ config: channelConfig.agentPrompt,
66987
+ cliTool: getAgentEntry(this.loadedConfig, agentId)?.cliTool,
66988
+ responseMode,
66989
+ streaming,
66990
+ protectedControlMutationRule: loop.protectedControlMutationRule
66991
+ });
66992
+ }
66993
+ buildLoopChannelIdentity(binding) {
66994
+ return {
66995
+ platform: binding.platform,
66996
+ conversationKind: binding.conversationKind,
66997
+ channelId: binding.channelId,
66998
+ chatId: binding.chatId,
66999
+ threadTs: binding.threadTs,
67000
+ topicId: binding.topicId
67001
+ };
67002
+ }
67003
+ resolveLoopSurfaceModes(identity) {
67004
+ const channelConfig = identity.platform === "slack" ? this.loadedConfig.raw.channels.slack : this.loadedConfig.raw.channels.telegram;
67005
+ let responseMode = channelConfig.responseMode;
67006
+ let streaming = channelConfig.streaming;
67007
+ if (identity.conversationKind === "dm") {
67008
+ responseMode = channelConfig.directMessages.responseMode ?? responseMode;
67009
+ streaming = channelConfig.directMessages.streaming ?? streaming;
67010
+ }
67011
+ try {
67012
+ responseMode = resolveConfiguredSurfaceModeTarget(this.loadedConfig.raw, "responseMode", buildConfiguredTargetFromIdentity(identity)).get() ?? responseMode;
67013
+ } catch {}
67014
+ try {
67015
+ streaming = resolveConfiguredSurfaceModeTarget(this.loadedConfig.raw, "streaming", buildConfiguredTargetFromIdentity(identity)).get() ?? streaming;
67016
+ } catch {}
67017
+ return {
67018
+ responseMode,
67019
+ streaming
67020
+ };
67021
+ }
66475
67022
  }
66476
67023
 
66477
67024
  // src/config/channel-accounts.ts
@@ -66794,6 +67341,37 @@ function parseAgentCommand(text, options = {}) {
66794
67341
  action: "status"
66795
67342
  };
66796
67343
  }
67344
+ if (lowered === "streaming") {
67345
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
67346
+ if (!action || action === "status") {
67347
+ return {
67348
+ type: "control",
67349
+ name: "streaming",
67350
+ action: "status"
67351
+ };
67352
+ }
67353
+ if (action === "on") {
67354
+ return {
67355
+ type: "control",
67356
+ name: "streaming",
67357
+ action: "on",
67358
+ streaming: "all"
67359
+ };
67360
+ }
67361
+ if (action === "off" || action === "latest" || action === "all") {
67362
+ return {
67363
+ type: "control",
67364
+ name: "streaming",
67365
+ action,
67366
+ streaming: action
67367
+ };
67368
+ }
67369
+ return {
67370
+ type: "control",
67371
+ name: "streaming",
67372
+ action: "status"
67373
+ };
67374
+ }
66797
67375
  if (lowered === "additionalmessagemode") {
66798
67376
  const action = withoutSlash.slice(command.length).trim().toLowerCase();
66799
67377
  if (!action || action === "status") {
@@ -66922,7 +67500,7 @@ function renderAgentControlSlashHelp() {
66922
67500
  "- `/status`: show the current route status and operator setup commands",
66923
67501
  "- `/help`: show available control slash commands",
66924
67502
  "- `/whoami`: show the current platform, route, and sender identity details",
66925
- "- `/transcript`: show the current conversation session transcript when the route enables sensitive commands",
67503
+ "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
66926
67504
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
66927
67505
  "- `/detach`: stop live updates for this thread while still allowing final settlement here",
66928
67506
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
@@ -66933,6 +67511,11 @@ function renderAgentControlSlashHelp() {
66933
67511
  "- `/followup mention-only`: require explicit mention for each later turn",
66934
67512
  "- `/followup pause`: stop passive follow-up until the next explicit mention",
66935
67513
  "- `/followup resume`: clear the runtime override and restore config defaults",
67514
+ "- `/streaming status`: show the configured streaming mode for this surface",
67515
+ "- `/streaming on`: enable streaming for this surface using the current `all` preview behavior",
67516
+ "- `/streaming off`: disable surface streaming previews for this surface",
67517
+ "- `/streaming latest`: prefer the latest preview shape for this surface",
67518
+ "- `/streaming all`: retain the full current preview shape for this surface",
66936
67519
  "- `/responsemode status`: show the configured response mode for this surface",
66937
67520
  "- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
66938
67521
  "- `/responsemode message-tool`: expect the agent to reply through `clisbot message send` for this surface",
@@ -66944,8 +67527,8 @@ function renderAgentControlSlashHelp() {
66944
67527
  "- `/queue-list`: show queued messages that have not started yet",
66945
67528
  "- `/queue-clear`: clear queued messages that have not started yet",
66946
67529
  ...renderLoopHelpLines(),
66947
- "- `/bash` followed by a shell command: requires `privilegeCommands.enabled: true` on the current route",
66948
- "- shortcut prefixes such as `!` run bash when the route allows privilege commands",
67530
+ "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
67531
+ "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
66949
67532
  "",
66950
67533
  "Other slash commands are forwarded to the agent unchanged."
66951
67534
  ].join(`
@@ -66982,6 +67565,7 @@ function buildRenderedMessageState(params) {
66982
67565
  queuePosition: params.queuePosition,
66983
67566
  maxChars: params.maxChars,
66984
67567
  note: params.note,
67568
+ allowTranscriptInspection: params.allowTranscriptInspection,
66985
67569
  responsePolicy: params.responsePolicy
66986
67570
  }),
66987
67571
  body
@@ -67011,35 +67595,45 @@ function formatChannelFollowUpStatus(params) {
67011
67595
  `);
67012
67596
  }
67013
67597
 
67014
- // src/channels/privilege-commands.ts
67015
- function resolvePrivilegeCommands(rootConfig, override) {
67598
+ // src/channels/streaming-config.ts
67599
+ function getEditableConfigPath7() {
67600
+ return process.env.CLISBOT_CONFIG_PATH;
67601
+ }
67602
+ async function getConversationStreaming(params) {
67603
+ const { config } = await readEditableConfig(getEditableConfigPath7());
67604
+ const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67016
67605
  return {
67017
- enabled: override?.enabled ?? rootConfig.enabled,
67018
- allowUsers: override?.allowUsers ?? rootConfig.allowUsers
67606
+ label: target.label,
67607
+ streaming: target.get()
67019
67608
  };
67020
67609
  }
67021
- function canUsePrivilegeCommands(params) {
67022
- if (!params.config.enabled) {
67023
- return false;
67024
- }
67025
- if (!params.config.allowUsers.length) {
67026
- return true;
67027
- }
67028
- const normalizedUserId = params.userId?.trim() ?? "";
67029
- if (!normalizedUserId) {
67030
- return false;
67031
- }
67032
- return params.config.allowUsers.includes(normalizedUserId);
67610
+ async function setConversationStreaming(params) {
67611
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
67612
+ const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67613
+ target.set(params.streaming);
67614
+ await writeEditableConfig(configPath, config);
67615
+ return {
67616
+ configPath,
67617
+ label: target.label,
67618
+ streaming: params.streaming
67619
+ };
67033
67620
  }
67034
67621
 
67035
67622
  // src/channels/interaction-processing.ts
67036
- import { join as join7 } from "node:path";
67037
- function renderSensitiveCommandDisabledMessage(identity) {
67623
+ import { join as join8 } from "node:path";
67624
+ var MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS = 3000;
67625
+ var MESSAGE_TOOL_FINAL_GRACE_POLL_MS = 100;
67626
+ function renderSensitiveCommandDisabledMessage() {
67038
67627
  return [
67039
- "Privilege commands are not allowed for this route or user.",
67040
- "Enable `privilegeCommands.enabled` on the route to allow transcript and bash commands. Use `privilegeCommands.allowUsers` to restrict access to specific user ids.",
67041
- "",
67042
- ...renderPrivilegeCommandHelpLines(identity)
67628
+ "Shell execution is not allowed for your current role on this agent.",
67629
+ "Ask an app or agent admin to grant `shellExecute` if this surface should allow `/bash`."
67630
+ ].join(`
67631
+ `);
67632
+ }
67633
+ function renderTranscriptDisabledMessage() {
67634
+ return [
67635
+ "Transcript inspection is disabled for this route.",
67636
+ 'Set `verbose: "minimal"` on the route or channel to allow `/transcript`.'
67043
67637
  ].join(`
67044
67638
  `);
67045
67639
  }
@@ -67067,7 +67661,7 @@ function renderWhoAmIMessage(params) {
67067
67661
  if (params.identity.topicId) {
67068
67662
  lines.push(`topicId: \`${params.identity.topicId}\``);
67069
67663
  }
67070
- lines.push(`privilegeCommands.enabled: \`${params.route.privilegeCommands.enabled}\``, `privilegeCommands.allowUsers: \`${params.route.privilegeCommands.allowUsers.join(", ") || "(all users on route)"}\``);
67664
+ lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
67071
67665
  return lines.join(`
67072
67666
  `);
67073
67667
  }
@@ -67095,7 +67689,7 @@ function renderRouteStatusMessage(params) {
67095
67689
  if (params.identity.topicId) {
67096
67690
  lines.push(`topicId: \`${params.identity.topicId}\``);
67097
67691
  }
67098
- lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `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}\``, `privilegeCommands.enabled: \`${params.route.privilegeCommands.enabled}\``, `privilegeCommands.allowUsers: \`${params.route.privilegeCommands.allowUsers.join(", ") || "(all users on route)"}\``);
67692
+ lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `verbose: \`${params.route.verbose}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `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}\``);
67099
67693
  if (params.runtimeState.startedAt) {
67100
67694
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
67101
67695
  }
@@ -67109,11 +67703,13 @@ function renderRouteStatusMessage(params) {
67109
67703
  lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
67110
67704
  }
67111
67705
  }
67112
- lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue <message>`, `/steer <message>`", "- `/queue-list`, `/queue-clear`", "- `/transcript` and `/bash` require privilege commands");
67113
- lines.push("", ...renderPrivilegeCommandHelpLines(params.identity));
67706
+ lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/streaming status`", "- `/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`");
67114
67707
  return lines.join(`
67115
67708
  `);
67116
67709
  }
67710
+ function allowTranscriptInspectionForRoute(route) {
67711
+ return route.verbose === "minimal";
67712
+ }
67117
67713
  function renderResponseModeStatusMessage(params) {
67118
67714
  const lines = [
67119
67715
  "clisbot response mode",
@@ -67128,6 +67724,18 @@ function renderResponseModeStatusMessage(params) {
67128
67724
  return lines.join(`
67129
67725
  `);
67130
67726
  }
67727
+ function renderStreamingStatusMessage(params) {
67728
+ const lines = [
67729
+ `clisbot streaming mode: \`${params.route.streaming}\``
67730
+ ];
67731
+ if (params.persisted) {
67732
+ lines.push("");
67733
+ lines.push(`config.target: \`${params.persisted.label}\``);
67734
+ }
67735
+ lines.push("", "Available values:", "- `off`: do not show live surface preview updates", "- `on`: slash-command shorthand that persists as `all`", "- `all`: keep streaming enabled with the current full preview behavior", "- `latest`: keep streaming enabled; current runtime behavior still matches `all` until preview shaping is refined");
67736
+ return lines.join(`
67737
+ `);
67738
+ }
67131
67739
  function renderAdditionalMessageModeStatusMessage(params) {
67132
67740
  const lines = [
67133
67741
  "clisbot additional message mode",
@@ -67153,14 +67761,22 @@ function buildChannelObserverId(identity) {
67153
67761
  identity.topicId ?? ""
67154
67762
  ].join(":");
67155
67763
  }
67156
- function buildSteeringMessage(text) {
67764
+ function buildSteeringMessage(text, protectedControlMutationRule) {
67765
+ const systemLines = [
67766
+ "A new user message arrived while you were still working.",
67767
+ "Adjust your current work if needed and continue."
67768
+ ];
67769
+ if (protectedControlMutationRule) {
67770
+ systemLines.push("", protectedControlMutationRule);
67771
+ }
67157
67772
  return [
67773
+ "<system>",
67774
+ ...systemLines,
67775
+ "</system>",
67158
67776
  "",
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
67777
+ "<user>",
67778
+ text,
67779
+ "</user>"
67164
67780
  ].join(`
67165
67781
  `);
67166
67782
  }
@@ -67294,7 +67910,7 @@ async function resolveLoopPromptText(params) {
67294
67910
  };
67295
67911
  }
67296
67912
  const workspacePath = params.agentService.getWorkspacePath(params.sessionTarget);
67297
- const loopPromptPath = join7(workspacePath, "LOOP.md");
67913
+ const loopPromptPath = join8(workspacePath, "LOOP.md");
67298
67914
  if (!await fileExists(loopPromptPath)) {
67299
67915
  throw new Error(`No loop prompt was provided and LOOP.md was not found in \`${workspacePath}\`. Create LOOP.md there if you want maintenance loops.`);
67300
67916
  }
@@ -67307,29 +67923,93 @@ async function resolveLoopPromptText(params) {
67307
67923
  maintenancePrompt: true
67308
67924
  };
67309
67925
  }
67926
+ function buildLoopSurfaceBinding(identity) {
67927
+ return {
67928
+ platform: identity.platform,
67929
+ conversationKind: identity.conversationKind,
67930
+ channelId: identity.channelId,
67931
+ chatId: identity.chatId,
67932
+ threadTs: identity.threadTs,
67933
+ topicId: identity.topicId
67934
+ };
67935
+ }
67310
67936
  async function executePromptDelivery(params) {
67311
67937
  let responseChunks = [];
67312
67938
  let renderedState;
67313
67939
  let renderChain = Promise.resolve();
67314
67940
  let replyRecorded = false;
67941
+ let finalReplyRecorded = false;
67315
67942
  let loggedFirstRunningUpdate = false;
67316
- const channelManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67317
- async function recordReplyIfNeeded() {
67943
+ let activePreviewStartedAt;
67944
+ let lastFrozenPreviewText;
67945
+ const paneManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67946
+ const messageToolPreview = params.route.responseMode === "message-tool" && params.forceQueuedDelivery !== true && params.route.streaming !== "off";
67947
+ const previewEnabled = params.route.streaming !== "off" && (paneManagedDelivery || messageToolPreview);
67948
+ async function recordVisibleReply(kind = "reply", source = "channel") {
67949
+ if (kind === "final") {
67950
+ if (finalReplyRecorded) {
67951
+ return;
67952
+ }
67953
+ await params.agentService.recordConversationReply(params.sessionTarget, "final", source);
67954
+ finalReplyRecorded = true;
67955
+ replyRecorded = true;
67956
+ return;
67957
+ }
67318
67958
  if (replyRecorded) {
67319
67959
  return;
67320
67960
  }
67321
- await params.agentService.recordConversationReply(params.sessionTarget);
67961
+ await params.agentService.recordConversationReply(params.sessionTarget, "reply", source);
67322
67962
  replyRecorded = true;
67323
67963
  }
67324
67964
  async function renderResponseText(nextText) {
67325
67965
  if (!responseChunks.length) {
67326
67966
  responseChunks = await params.postText(nextText);
67327
- if (responseChunks.length > 0) {
67328
- await recordReplyIfNeeded();
67329
- }
67330
- return;
67967
+ return responseChunks.length > 0;
67331
67968
  }
67332
67969
  responseChunks = await params.reconcileText(responseChunks, nextText);
67970
+ return false;
67971
+ }
67972
+ async function clearResponseText() {
67973
+ if (!responseChunks.length) {
67974
+ return;
67975
+ }
67976
+ responseChunks = await params.reconcileText(responseChunks, "");
67977
+ renderedState = undefined;
67978
+ activePreviewStartedAt = undefined;
67979
+ }
67980
+ function resetPreviewBoundary() {
67981
+ lastFrozenPreviewText = renderedState?.text ?? lastFrozenPreviewText;
67982
+ responseChunks = [];
67983
+ renderedState = undefined;
67984
+ activePreviewStartedAt = undefined;
67985
+ }
67986
+ async function getMessageToolRuntimeSignals() {
67987
+ if (params.route.responseMode !== "message-tool" || params.forceQueuedDelivery === true) {
67988
+ return {
67989
+ lastMessageToolReplyAt: undefined,
67990
+ messageToolFinalReplyAt: undefined
67991
+ };
67992
+ }
67993
+ const runtime = await params.agentService.getSessionRuntime?.(params.sessionTarget);
67994
+ return {
67995
+ lastMessageToolReplyAt: runtime?.lastMessageToolReplyAt,
67996
+ messageToolFinalReplyAt: runtime?.messageToolFinalReplyAt
67997
+ };
67998
+ }
67999
+ async function waitForMessageToolFinalReply() {
68000
+ const deadline = Date.now() + MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS;
68001
+ while (true) {
68002
+ const signals = await getMessageToolRuntimeSignals();
68003
+ const toolFinalSeen = typeof signals.messageToolFinalReplyAt === "number" && Number.isFinite(signals.messageToolFinalReplyAt);
68004
+ if (toolFinalSeen) {
68005
+ return true;
68006
+ }
68007
+ const remainingMs = deadline - Date.now();
68008
+ if (remainingMs <= 0) {
68009
+ return false;
68010
+ }
68011
+ await sleep(Math.min(MESSAGE_TOOL_FINAL_GRACE_POLL_MS, remainingMs));
68012
+ }
67333
68013
  }
67334
68014
  logLatencyDebug("channel-enqueue-start", params.timingContext, {
67335
68015
  agentId: params.route.agentId,
@@ -67340,7 +68020,7 @@ async function executePromptDelivery(params) {
67340
68020
  observerId: params.observerId,
67341
68021
  timingContext: params.timingContext,
67342
68022
  onUpdate: async (update) => {
67343
- if (!channelManagedDelivery) {
68023
+ if (!paneManagedDelivery && !messageToolPreview) {
67344
68024
  return;
67345
68025
  }
67346
68026
  if (params.route.streaming === "off" && update.status === "running") {
@@ -67354,6 +68034,15 @@ async function executePromptDelivery(params) {
67354
68034
  });
67355
68035
  }
67356
68036
  await (renderChain = renderChain.then(async () => {
68037
+ const signals = await getMessageToolRuntimeSignals();
68038
+ if (messageToolPreview && typeof activePreviewStartedAt === "number" && typeof signals.messageToolFinalReplyAt === "number" && signals.messageToolFinalReplyAt >= activePreviewStartedAt) {
68039
+ lastFrozenPreviewText = renderedState?.text ?? lastFrozenPreviewText;
68040
+ activePreviewStartedAt = undefined;
68041
+ return;
68042
+ }
68043
+ if (messageToolPreview && typeof activePreviewStartedAt === "number" && typeof signals.lastMessageToolReplyAt === "number" && signals.lastMessageToolReplyAt >= activePreviewStartedAt) {
68044
+ resetPreviewBoundary();
68045
+ }
67357
68046
  const nextState2 = buildRenderedMessageState({
67358
68047
  platform: params.identity.platform,
67359
68048
  status: update.status,
@@ -67361,21 +68050,26 @@ async function executePromptDelivery(params) {
67361
68050
  queuePosition: positionAhead,
67362
68051
  maxChars: Number.POSITIVE_INFINITY,
67363
68052
  note: update.note,
68053
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
67364
68054
  previousState: renderedState,
67365
68055
  responsePolicy: params.route.response
67366
68056
  });
67367
68057
  if (renderedState?.text === nextState2.text) {
67368
68058
  return;
67369
68059
  }
67370
- if (!responseChunks.length) {
68060
+ if (!renderedState && lastFrozenPreviewText === nextState2.text) {
67371
68061
  return;
67372
68062
  }
67373
- await renderResponseText(nextState2.text);
68063
+ const postedNew2 = await renderResponseText(nextState2.text);
68064
+ if (postedNew2) {
68065
+ await recordVisibleReply("reply", "channel");
68066
+ activePreviewStartedAt = Date.now();
68067
+ }
67374
68068
  renderedState = nextState2;
67375
68069
  }));
67376
68070
  }
67377
68071
  });
67378
- if (channelManagedDelivery && params.route.streaming !== "off") {
68072
+ if (previewEnabled) {
67379
68073
  const placeholderText = renderPlatformInteraction({
67380
68074
  platform: params.identity.platform,
67381
68075
  status: positionAhead > 0 ? "queued" : "running",
@@ -67384,13 +68078,16 @@ async function executePromptDelivery(params) {
67384
68078
  maxChars: Number.POSITIVE_INFINITY,
67385
68079
  note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
67386
68080
  });
67387
- responseChunks = await params.postText(placeholderText);
67388
- await recordReplyIfNeeded();
68081
+ const postedNew2 = await renderResponseText(placeholderText);
68082
+ if (postedNew2) {
68083
+ await recordVisibleReply("reply", "channel");
68084
+ activePreviewStartedAt = Date.now();
68085
+ }
67389
68086
  renderedState = {
67390
68087
  text: placeholderText,
67391
68088
  body: ""
67392
68089
  };
67393
- } else if (channelManagedDelivery && positionAhead > 0) {
68090
+ } else if (paneManagedDelivery && positionAhead > 0 && params.route.streaming !== "off") {
67394
68091
  const queuedText = renderPlatformInteraction({
67395
68092
  platform: params.identity.platform,
67396
68093
  status: "queued",
@@ -67399,8 +68096,10 @@ async function executePromptDelivery(params) {
67399
68096
  maxChars: Number.POSITIVE_INFINITY,
67400
68097
  note: "Waiting for the agent queue to clear."
67401
68098
  });
67402
- responseChunks = await params.postText(queuedText);
67403
- await recordReplyIfNeeded();
68099
+ const postedNew2 = await renderResponseText(queuedText);
68100
+ if (postedNew2) {
68101
+ await recordVisibleReply("reply", "channel");
68102
+ }
67404
68103
  renderedState = {
67405
68104
  text: queuedText,
67406
68105
  body: ""
@@ -67408,55 +68107,86 @@ async function executePromptDelivery(params) {
67408
68107
  }
67409
68108
  const finalResult = await result;
67410
68109
  await renderChain;
67411
- if (!channelManagedDelivery) {
67412
- if (finalResult.status !== "error") {
67413
- return;
67414
- }
67415
- await params.postText(renderPlatformInteraction({
67416
- platform: params.identity.platform,
67417
- status: finalResult.status,
67418
- content: finalResult.note ?? finalResult.snapshot,
67419
- maxChars: Number.POSITIVE_INFINITY,
67420
- note: finalResult.note,
67421
- responsePolicy: "final"
67422
- }));
67423
- await recordReplyIfNeeded();
67424
- return;
67425
- }
67426
68110
  const nextState = buildRenderedMessageState({
67427
68111
  platform: params.identity.platform,
67428
68112
  status: finalResult.status,
67429
68113
  snapshot: finalResult.snapshot,
67430
68114
  maxChars: Number.POSITIVE_INFINITY,
67431
68115
  note: finalResult.note,
68116
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
67432
68117
  previousState: renderedState,
67433
68118
  responsePolicy: params.route.response
67434
68119
  });
67435
- if (params.route.streaming === "off") {
67436
- await params.postText(renderPlatformInteraction({
68120
+ if (paneManagedDelivery) {
68121
+ if (params.route.streaming === "off") {
68122
+ const postedNew3 = await renderResponseText(renderPlatformInteraction({
68123
+ platform: params.identity.platform,
68124
+ status: finalResult.status,
68125
+ content: nextState.body,
68126
+ maxChars: Number.POSITIVE_INFINITY,
68127
+ note: finalResult.note,
68128
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68129
+ responsePolicy: params.route.response
68130
+ }));
68131
+ if (postedNew3 || finalResult.status === "completed") {
68132
+ await recordVisibleReply(finalResult.status === "completed" ? "final" : "reply", "channel");
68133
+ }
68134
+ return;
68135
+ }
68136
+ const postedNew2 = await renderResponseText(nextState.text);
68137
+ if (postedNew2) {
68138
+ await recordVisibleReply("reply", "channel");
68139
+ }
68140
+ if (finalResult.status === "completed") {
68141
+ await recordVisibleReply("final", "channel");
68142
+ }
68143
+ return;
68144
+ }
68145
+ const toolFinalSeen = finalResult.status === "completed" ? await waitForMessageToolFinalReply() : false;
68146
+ if (finalResult.status === "completed" && toolFinalSeen) {
68147
+ if (params.route.response === "final") {
68148
+ await clearResponseText();
68149
+ }
68150
+ return;
68151
+ }
68152
+ if (finalResult.status === "completed") {
68153
+ return;
68154
+ }
68155
+ if (params.route.streaming === "off" || responseChunks.length === 0) {
68156
+ const postedNew2 = await renderResponseText(renderPlatformInteraction({
67437
68157
  platform: params.identity.platform,
67438
68158
  status: finalResult.status,
67439
68159
  content: nextState.body,
67440
68160
  maxChars: Number.POSITIVE_INFINITY,
67441
68161
  note: finalResult.note,
67442
- responsePolicy: "final"
68162
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68163
+ responsePolicy: params.route.response
67443
68164
  }));
67444
- await recordReplyIfNeeded();
68165
+ if (postedNew2) {
68166
+ await recordVisibleReply("reply", "channel");
68167
+ }
67445
68168
  return;
67446
68169
  }
67447
- await renderResponseText(nextState.text);
68170
+ const postedNew = await renderResponseText(nextState.text);
68171
+ if (postedNew) {
68172
+ await recordVisibleReply("reply", "channel");
68173
+ }
67448
68174
  } catch (error) {
67449
68175
  if (error instanceof ClearedQueuedTaskError) {
67450
68176
  return;
67451
68177
  }
67452
68178
  if (error instanceof ActiveRunInProgressError) {
67453
- const activeText = error.update.note ?? String(error);
68179
+ const activeText = error.update.status === "detached" ? resolveDetachedInteractionNote({
68180
+ baseNote: error.update.note ?? String(error),
68181
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68182
+ transcriptCommand: params.identity.platform === "telegram" ? "/transcript" : "`/transcript`"
68183
+ }) : error.update.note ?? String(error);
67454
68184
  if (params.route.streaming !== "off" && responseChunks.length > 0) {
67455
68185
  await params.reconcileText(responseChunks, activeText);
67456
68186
  } else {
67457
68187
  await params.postText(activeText);
67458
68188
  }
67459
- await params.agentService.recordConversationReply(params.sessionTarget);
68189
+ await recordVisibleReply("reply", "channel");
67460
68190
  return;
67461
68191
  }
67462
68192
  const errorText = renderPlatformInteraction({
@@ -67470,12 +68200,24 @@ async function executePromptDelivery(params) {
67470
68200
  } else {
67471
68201
  await params.postText(errorText);
67472
68202
  }
68203
+ await recordVisibleReply("reply", "channel");
67473
68204
  }
67474
68205
  }
67475
68206
  async function processChannelInteraction(params) {
68207
+ const interactionResult = {
68208
+ processingIndicatorLifecycle: "handler"
68209
+ };
67476
68210
  let responseChunks = [];
67477
68211
  let renderedState;
67478
68212
  const observerId = buildChannelObserverId(params.identity);
68213
+ const auth = params.auth ?? {
68214
+ principal: params.senderId ? `${params.identity.platform}:${params.senderId}` : undefined,
68215
+ appRole: "member",
68216
+ agentRole: "member",
68217
+ mayBypassPairing: false,
68218
+ mayManageProtectedResources: false,
68219
+ canUseShell: false
68220
+ };
67479
68221
  let replyRecorded = false;
67480
68222
  let renderChain = Promise.resolve();
67481
68223
  async function recordReplyIfNeeded() {
@@ -67532,14 +68274,12 @@ async function processChannelInteraction(params) {
67532
68274
  const sessionBusy = await (params.agentService.isAwaitingFollowUpRouting?.(params.sessionTarget) ?? params.agentService.isSessionBusy?.(params.sessionTarget) ?? false);
67533
68275
  const queueByMode = !explicitQueueMessage && params.route.additionalMessageMode === "queue" && sessionBusy;
67534
68276
  const forceQueuedDelivery = typeof explicitQueueMessage === "string" || queueByMode;
67535
- const isSensitiveCommand = slashCommand?.type === "bash" || slashCommand?.type === "control" && slashCommand.name === "transcript";
67536
- if (isSensitiveCommand && !canUsePrivilegeCommands({
67537
- config: params.route.privilegeCommands,
67538
- userId: params.senderId
67539
- })) {
67540
- await params.postText(renderSensitiveCommandDisabledMessage(params.identity));
68277
+ const delayedPromptText = explicitQueueMessage ? params.agentPromptBuilder ? params.agentPromptBuilder(explicitQueueMessage) : explicitQueueMessage : params.agentPromptText ?? params.text;
68278
+ const isSensitiveCommand = slashCommand?.type === "bash";
68279
+ if (isSensitiveCommand && !auth.canUseShell) {
68280
+ await params.postText(renderSensitiveCommandDisabledMessage());
67541
68281
  await params.agentService.recordConversationReply(params.sessionTarget);
67542
- return;
68282
+ return interactionResult;
67543
68283
  }
67544
68284
  if (slashCommand?.type === "control") {
67545
68285
  if (slashCommand.name === "start" || slashCommand.name === "status") {
@@ -67551,6 +68291,7 @@ async function processChannelInteraction(params) {
67551
68291
  await params.postText(renderRouteStatusMessage({
67552
68292
  identity: params.identity,
67553
68293
  route: params.route,
68294
+ auth,
67554
68295
  sessionTarget: params.sessionTarget,
67555
68296
  followUpState,
67556
68297
  runtimeState,
@@ -67560,23 +68301,29 @@ async function processChannelInteraction(params) {
67560
68301
  }
67561
68302
  }));
67562
68303
  await params.agentService.recordConversationReply(params.sessionTarget);
67563
- return;
68304
+ return interactionResult;
67564
68305
  }
67565
68306
  if (slashCommand.name === "help") {
67566
68307
  await params.postText(renderAgentControlSlashHelp());
67567
68308
  await params.agentService.recordConversationReply(params.sessionTarget);
67568
- return;
68309
+ return interactionResult;
67569
68310
  }
67570
68311
  if (slashCommand.name === "whoami") {
67571
68312
  await params.postText(renderWhoAmIMessage({
67572
68313
  identity: params.identity,
67573
68314
  route: params.route,
68315
+ auth,
67574
68316
  sessionTarget: params.sessionTarget
67575
68317
  }));
67576
68318
  await params.agentService.recordConversationReply(params.sessionTarget);
67577
- return;
68319
+ return interactionResult;
67578
68320
  }
67579
68321
  if (slashCommand.name === "transcript") {
68322
+ if (params.route.verbose === "off") {
68323
+ await params.postText(renderTranscriptDisabledMessage());
68324
+ await params.agentService.recordConversationReply(params.sessionTarget);
68325
+ return interactionResult;
68326
+ }
67580
68327
  const transcript = await params.agentService.captureTranscript(params.sessionTarget);
67581
68328
  await params.postText(renderChannelSnapshot({
67582
68329
  agentId: transcript.agentId,
@@ -67587,20 +68334,20 @@ async function processChannelInteraction(params) {
67587
68334
  maxChars: params.maxChars,
67588
68335
  note: "transcript command"
67589
68336
  }));
67590
- return;
68337
+ return interactionResult;
67591
68338
  }
67592
68339
  if (slashCommand.name === "attach") {
67593
68340
  const observation = await params.agentService.observeRun(params.sessionTarget, buildRunObserver({
67594
68341
  mode: "live"
67595
68342
  }));
67596
68343
  await applyRunUpdate(observation.update);
67597
- return;
68344
+ return interactionResult;
67598
68345
  }
67599
68346
  if (slashCommand.name === "detach") {
67600
68347
  const detached = await params.agentService.detachRunObserver(params.sessionTarget, observerId);
67601
68348
  await params.postText(detached.detached ? "Detached this thread from live updates. clisbot will still post the final settlement here when the run completes." : "This thread was not attached to an active run.");
67602
68349
  await params.agentService.recordConversationReply(params.sessionTarget);
67603
- return;
68350
+ return interactionResult;
67604
68351
  }
67605
68352
  if (slashCommand.name === "watch") {
67606
68353
  const observation = await params.agentService.observeRun(params.sessionTarget, buildRunObserver({
@@ -67609,19 +68356,19 @@ async function processChannelInteraction(params) {
67609
68356
  durationMs: slashCommand.durationMs
67610
68357
  }));
67611
68358
  await applyRunUpdate(observation.update);
67612
- return;
68359
+ return interactionResult;
67613
68360
  }
67614
68361
  if (slashCommand.name === "stop") {
67615
68362
  const stopped = await params.agentService.interruptSession(params.sessionTarget);
67616
68363
  await params.postText(stopped.interrupted ? `Interrupted agent \`${stopped.agentId}\` session \`${stopped.sessionName}\`.` : `Agent \`${stopped.agentId}\` session \`${stopped.sessionName}\` was not running.`);
67617
68364
  await params.agentService.recordConversationReply(params.sessionTarget);
67618
- return;
68365
+ return interactionResult;
67619
68366
  }
67620
68367
  if (slashCommand.name === "nudge") {
67621
68368
  const nudged = await params.agentService.nudgeSession(params.sessionTarget);
67622
68369
  await params.postText(nudged.nudged ? `Sent one extra Enter to agent \`${nudged.agentId}\` session \`${nudged.sessionName}\`.` : `No active or resumable session to nudge for agent \`${nudged.agentId}\`.`);
67623
68370
  await params.agentService.recordConversationReply(params.sessionTarget);
67624
- return;
68371
+ return interactionResult;
67625
68372
  }
67626
68373
  if (slashCommand.name === "followup") {
67627
68374
  if (slashCommand.action === "status") {
@@ -67640,7 +68387,32 @@ async function processChannelInteraction(params) {
67640
68387
  await params.postText(slashCommand.mode === "paused" ? "Follow-up paused for this conversation until the next explicit mention." : `Follow-up mode set to \`${slashCommand.mode}\` for this conversation.`);
67641
68388
  }
67642
68389
  await params.agentService.recordConversationReply(params.sessionTarget);
67643
- return;
68390
+ return interactionResult;
68391
+ }
68392
+ if (slashCommand.name === "streaming") {
68393
+ if (slashCommand.action === "status") {
68394
+ const persisted = await getConversationStreaming({
68395
+ identity: params.identity
68396
+ });
68397
+ await params.postText(renderStreamingStatusMessage({
68398
+ route: params.route,
68399
+ persisted
68400
+ }));
68401
+ } else if (slashCommand.streaming) {
68402
+ const persisted = await setConversationStreaming({
68403
+ identity: params.identity,
68404
+ streaming: slashCommand.streaming
68405
+ });
68406
+ await params.postText([
68407
+ `Updated streaming mode for \`${persisted.label}\`.`,
68408
+ `config.streaming: \`${persisted.streaming}\``,
68409
+ `config: \`${persisted.configPath}\``,
68410
+ slashCommand.action === "on" ? "`/streaming on` persists as `all` until `latest` and `all` diverge in runtime behavior." : "If config reload is enabled, the new mode should apply automatically shortly."
68411
+ ].join(`
68412
+ `));
68413
+ }
68414
+ await params.agentService.recordConversationReply(params.sessionTarget);
68415
+ return interactionResult;
67644
68416
  }
67645
68417
  if (slashCommand.name === "responsemode") {
67646
68418
  if (slashCommand.action === "status") {
@@ -67665,7 +68437,7 @@ async function processChannelInteraction(params) {
67665
68437
  `));
67666
68438
  }
67667
68439
  await params.agentService.recordConversationReply(params.sessionTarget);
67668
- return;
68440
+ return interactionResult;
67669
68441
  }
67670
68442
  if (slashCommand.name === "additionalmessagemode") {
67671
68443
  if (slashCommand.action === "status") {
@@ -67690,19 +68462,19 @@ async function processChannelInteraction(params) {
67690
68462
  `));
67691
68463
  }
67692
68464
  await params.agentService.recordConversationReply(params.sessionTarget);
67693
- return;
68465
+ return interactionResult;
67694
68466
  }
67695
68467
  if (slashCommand.name === "queue-list") {
67696
68468
  const queuedItems = params.agentService.listQueuedPrompts?.(params.sessionTarget) ?? [];
67697
68469
  await params.postText(renderQueuedMessagesList(queuedItems));
67698
68470
  await params.agentService.recordConversationReply(params.sessionTarget);
67699
- return;
68471
+ return interactionResult;
67700
68472
  }
67701
68473
  if (slashCommand.name === "queue-clear") {
67702
68474
  const clearedCount = params.agentService.clearQueuedPrompts?.(params.sessionTarget) ?? 0;
67703
68475
  await params.postText(clearedCount > 0 ? `Cleared ${clearedCount} queued message${clearedCount === 1 ? "" : "s"}.` : "Queue was already empty.");
67704
68476
  await params.agentService.recordConversationReply(params.sessionTarget);
67705
- return;
68477
+ return interactionResult;
67706
68478
  }
67707
68479
  }
67708
68480
  if (slashCommand?.type === "loop-control") {
@@ -67714,7 +68486,7 @@ async function processChannelInteraction(params) {
67714
68486
  globalLoopCount: params.agentService.getActiveIntervalLoopCount?.() ?? 0
67715
68487
  }));
67716
68488
  await params.agentService.recordConversationReply(params.sessionTarget);
67717
- return;
68489
+ return interactionResult;
67718
68490
  }
67719
68491
  const sessionLoops = params.agentService.listIntervalLoops?.({
67720
68492
  sessionKey: params.sessionTarget.sessionKey
@@ -67723,31 +68495,31 @@ async function processChannelInteraction(params) {
67723
68495
  const cancelled2 = await params.agentService.cancelAllIntervalLoops();
67724
68496
  await params.postText(cancelled2 > 0 ? `Cancelled ${cancelled2} active loop${cancelled2 === 1 ? "" : "s"} across the whole app.` : "No active loops to cancel across the whole app.");
67725
68497
  await params.agentService.recordConversationReply(params.sessionTarget);
67726
- return;
68498
+ return interactionResult;
67727
68499
  }
67728
68500
  if (slashCommand.all) {
67729
68501
  const cancelled2 = await params.agentService.cancelIntervalLoopsForSession(params.sessionTarget);
67730
68502
  await params.postText(cancelled2 > 0 ? `Cancelled ${cancelled2} active loop${cancelled2 === 1 ? "" : "s"} for this session.` : "No active loops to cancel for this session.");
67731
68503
  await params.agentService.recordConversationReply(params.sessionTarget);
67732
- return;
68504
+ return interactionResult;
67733
68505
  }
67734
68506
  const targetLoopId = slashCommand.loopId || (sessionLoops.length === 1 ? sessionLoops[0]?.id : undefined);
67735
68507
  if (!targetLoopId) {
67736
68508
  await params.postText(sessionLoops.length === 0 ? "No active loops to cancel for this session." : "Multiple active loops exist for this session. Use `/loop cancel <id>` or `/loop cancel --all`.");
67737
68509
  await params.agentService.recordConversationReply(params.sessionTarget);
67738
- return;
68510
+ return interactionResult;
67739
68511
  }
67740
68512
  const cancelled = await params.agentService.cancelIntervalLoop(targetLoopId);
67741
68513
  await params.postText(cancelled ? `Cancelled loop \`${targetLoopId}\`.` : `No active loop found with id \`${targetLoopId}\`.`);
67742
68514
  await params.agentService.recordConversationReply(params.sessionTarget);
67743
- return;
68515
+ return interactionResult;
67744
68516
  }
67745
68517
  if (slashCommand?.type === "loop-error") {
67746
68518
  await params.postText(`${slashCommand.message}
67747
68519
 
67748
68520
  ${renderLoopUsage()}`);
67749
68521
  await params.agentService.recordConversationReply(params.sessionTarget);
67750
- return;
68522
+ return interactionResult;
67751
68523
  }
67752
68524
  if (slashCommand?.type === "loop") {
67753
68525
  const loopConfig = params.agentService.getLoopConfig();
@@ -67758,7 +68530,7 @@ ${renderLoopUsage()}`);
67758
68530
 
67759
68531
  ${renderLoopUsage()}`);
67760
68532
  await params.agentService.recordConversationReply(params.sessionTarget);
67761
- return;
68533
+ return interactionResult;
67762
68534
  }
67763
68535
  if (slashCommand.params.mode === "interval") {
67764
68536
  const intervalValidation = validateLoopInterval({
@@ -67770,7 +68542,7 @@ ${renderLoopUsage()}`);
67770
68542
 
67771
68543
  ${renderLoopUsage()}`);
67772
68544
  await params.agentService.recordConversationReply(params.sessionTarget);
67773
- return;
68545
+ return interactionResult;
67774
68546
  }
67775
68547
  }
67776
68548
  let resolvedLoopPrompt;
@@ -67783,7 +68555,7 @@ ${renderLoopUsage()}`);
67783
68555
  } catch (error) {
67784
68556
  await params.postText(String(error));
67785
68557
  await params.agentService.recordConversationReply(params.sessionTarget);
67786
- return;
68558
+ return interactionResult;
67787
68559
  }
67788
68560
  const buildLoopPromptText = (text) => params.agentPromptBuilder ? params.agentPromptBuilder(text) : text;
67789
68561
  if (slashCommand.params.mode === "times") {
@@ -67806,7 +68578,7 @@ ${renderLoopUsage()}`);
67806
68578
  observerId: `${observerId}:loop:${index + 1}`
67807
68579
  });
67808
68580
  }
67809
- return;
68581
+ return interactionResult;
67810
68582
  }
67811
68583
  if (slashCommand.params.mode === "calendar") {
67812
68584
  const effectiveTimezone = resolveEffectiveLoopTimezone({
@@ -67816,8 +68588,10 @@ ${renderLoopUsage()}`);
67816
68588
  const createdLoop2 = await params.agentService.createCalendarLoop({
67817
68589
  target: params.sessionTarget,
67818
68590
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
68591
+ canonicalPromptText: resolvedLoopPrompt.text,
67819
68592
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
67820
68593
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
68594
+ surfaceBinding: buildLoopSurfaceBinding(params.identity),
67821
68595
  cadence: slashCommand.params.cadence,
67822
68596
  dayOfWeek: slashCommand.params.dayOfWeek,
67823
68597
  localTime: slashCommand.params.localTime,
@@ -67825,7 +68599,8 @@ ${renderLoopUsage()}`);
67825
68599
  minute: slashCommand.params.minute,
67826
68600
  timezone: effectiveTimezone,
67827
68601
  maxRuns: maxRunsPerLoop,
67828
- createdBy: params.senderId
68602
+ createdBy: params.senderId,
68603
+ protectedControlMutationRule: params.protectedControlMutationRule
67829
68604
  });
67830
68605
  await params.postText(renderLoopStartedMessage({
67831
68606
  mode: "calendar",
@@ -67845,17 +68620,20 @@ ${renderLoopUsage()}`);
67845
68620
  globalLoopCount: params.agentService.getActiveIntervalLoopCount()
67846
68621
  }));
67847
68622
  await params.agentService.recordConversationReply(params.sessionTarget);
67848
- return;
68623
+ return interactionResult;
67849
68624
  }
67850
68625
  const createdLoop = await params.agentService.createIntervalLoop({
67851
68626
  target: params.sessionTarget,
67852
68627
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
68628
+ canonicalPromptText: resolvedLoopPrompt.text,
67853
68629
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
67854
68630
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
68631
+ surfaceBinding: buildLoopSurfaceBinding(params.identity),
67855
68632
  intervalMs: effectiveIntervalMs,
67856
68633
  maxRuns: maxRunsPerLoop,
67857
68634
  createdBy: params.senderId,
67858
- force: slashCommand.params.force
68635
+ force: slashCommand.params.force,
68636
+ protectedControlMutationRule: params.protectedControlMutationRule
67859
68637
  });
67860
68638
  await params.postText(renderLoopStartedMessage({
67861
68639
  mode: "interval",
@@ -67873,55 +68651,59 @@ ${renderLoopUsage()}`);
67873
68651
  }).warning
67874
68652
  }));
67875
68653
  await params.agentService.recordConversationReply(params.sessionTarget);
67876
- return;
68654
+ return interactionResult;
67877
68655
  }
67878
68656
  if (slashCommand?.type === "bash") {
67879
68657
  if (!slashCommand.command.trim()) {
67880
68658
  await params.postText("Usage: `/bash <command>` or a configured bash shortcut such as `!<command>`");
67881
- return;
68659
+ return interactionResult;
67882
68660
  }
67883
- const result = await params.agentService.runShellCommand(params.sessionTarget, slashCommand.command);
68661
+ const shellResult = await params.agentService.runShellCommand(params.sessionTarget, slashCommand.command);
67884
68662
  const header = [
67885
- `Bash in \`${result.workspacePath}\``,
67886
- `command: \`${result.command}\``,
67887
- result.timedOut ? "exit: `124` timed out" : `exit: \`${result.exitCode}\``
68663
+ `Bash in \`${shellResult.workspacePath}\``,
68664
+ `command: \`${shellResult.command}\``,
68665
+ shellResult.timedOut ? "exit: `124` timed out" : `exit: \`${shellResult.exitCode}\``
67888
68666
  ].join(`
67889
68667
  `);
67890
- const body = result.output ? `
68668
+ const body = shellResult.output ? `
67891
68669
 
67892
68670
  \`\`\`text
67893
- ${escapeCodeFence(result.output)}
68671
+ ${escapeCodeFence(shellResult.output)}
67894
68672
  \`\`\`` : "\n\n`(no output)`";
67895
68673
  await params.postText(`${header}${body}`);
67896
68674
  await params.agentService.recordConversationReply(params.sessionTarget);
67897
- return;
68675
+ return interactionResult;
67898
68676
  }
67899
68677
  if (slashCommand?.type === "queue" && !explicitQueueMessage) {
67900
68678
  await params.postText("Usage: `/queue <message>` or `\\q <message>`");
67901
68679
  await params.agentService.recordConversationReply(params.sessionTarget);
67902
- return;
68680
+ return interactionResult;
67903
68681
  }
67904
68682
  if (slashCommand?.type === "steer" && !explicitSteerMessage) {
67905
68683
  await params.postText("Usage: `/steer <message>` or `\\s <message>`");
67906
68684
  await params.agentService.recordConversationReply(params.sessionTarget);
67907
- return;
68685
+ return interactionResult;
67908
68686
  }
67909
68687
  if (explicitSteerMessage) {
67910
68688
  const hasActiveRun = params.agentService.hasActiveRun?.(params.sessionTarget) ?? false;
67911
68689
  if (!hasActiveRun) {
67912
68690
  await params.postText("No active run to steer.");
67913
68691
  await params.agentService.recordConversationReply(params.sessionTarget);
67914
- return;
68692
+ return interactionResult;
67915
68693
  }
67916
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage));
68694
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage, params.protectedControlMutationRule));
67917
68695
  await params.postText("Steered.");
67918
68696
  await params.agentService.recordConversationReply(params.sessionTarget);
67919
- return;
68697
+ return {
68698
+ processingIndicatorLifecycle: "active-run"
68699
+ };
67920
68700
  }
67921
68701
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
67922
68702
  if (sessionBusy) {
67923
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text));
67924
- return;
68703
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text, params.protectedControlMutationRule));
68704
+ return {
68705
+ processingIndicatorLifecycle: "active-run"
68706
+ };
67925
68707
  }
67926
68708
  }
67927
68709
  await executePromptDelivery({
@@ -67930,13 +68712,14 @@ ${escapeCodeFence(result.output)}
67930
68712
  identity: params.identity,
67931
68713
  route: params.route,
67932
68714
  maxChars: params.maxChars,
67933
- promptText: forceQueuedDelivery ? explicitQueueMessage : params.agentPromptText ?? params.text,
68715
+ promptText: delayedPromptText,
67934
68716
  postText: params.postText,
67935
68717
  reconcileText: params.reconcileText,
67936
68718
  observerId,
67937
68719
  timingContext: params.timingContext,
67938
68720
  forceQueuedDelivery
67939
68721
  });
68722
+ return interactionResult;
67940
68723
  }
67941
68724
 
67942
68725
  // src/channels/pairing/access.ts
@@ -67980,141 +68763,97 @@ function isTelegramSenderAllowed(params) {
67980
68763
  const userId = params.userId?.trim() ?? "";
67981
68764
  const username = params.username?.trim() ? `@${params.username.trim().replace(/^@+/, "").toLowerCase()}` : "";
67982
68765
  const normalizedAllowFrom = params.allowFrom.map((entry) => normalizeAllowEntry("telegram", entry)).filter(Boolean);
67983
- if (userId && normalizedAllowFrom.includes(userId)) {
67984
- return true;
67985
- }
67986
- if (username && normalizedAllowFrom.includes(username)) {
67987
- return true;
67988
- }
67989
- return false;
67990
- }
67991
-
67992
- // src/channels/agent-prompt.ts
67993
- function buildAgentPromptText(params) {
67994
- if (!params.config.enabled) {
67995
- return params.text;
67996
- }
67997
- const systemBlock = renderAgentPromptInstruction(params);
67998
- return `<system>
67999
- ${systemBlock}
68000
- </system>
68001
-
68002
- <user>
68003
- ${params.text}
68004
- </user>`;
68005
- }
68006
- function renderAgentPromptInstruction(params) {
68007
- const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
68008
- const lines = [
68009
- `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
68010
- "",
68011
- "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"
68013
- ];
68014
- if (messageToolMode) {
68015
- const replyCommand = buildReplyCommand({
68016
- command: getClisbotPromptCommand(),
68017
- identity: params.identity
68018
- });
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");
68020
- }
68021
- return lines.join(`
68022
- `);
68023
- }
68024
- function renderPromptTimestamp() {
68025
- const date = new Date;
68026
- const formatter = new Intl.DateTimeFormat("en-CA", {
68027
- year: "numeric",
68028
- month: "2-digit",
68029
- day: "2-digit",
68030
- hour: "2-digit",
68031
- minute: "2-digit",
68032
- second: "2-digit",
68033
- hour12: false,
68034
- timeZoneName: "shortOffset"
68035
- });
68036
- return formatter.format(date).replace(",", "");
68037
- }
68038
- function renderIdentitySummary(identity) {
68039
- const segments = [renderConversationSummary(identity)];
68040
- const sender = renderSenderSummary(identity);
68041
- if (sender) {
68042
- segments.push(sender);
68766
+ if (userId && normalizedAllowFrom.includes(userId)) {
68767
+ return true;
68043
68768
  }
68044
- return segments.join(" | ");
68769
+ if (username && normalizedAllowFrom.includes(username)) {
68770
+ return true;
68771
+ }
68772
+ return false;
68045
68773
  }
68046
- function renderConversationSummary(identity) {
68047
- if (identity.platform === "slack") {
68048
- const scopeLabel = identity.conversationKind === "dm" ? "Slack direct message" : identity.conversationKind === "group" ? "Slack group" : "Slack channel";
68049
- const segments = [scopeLabel];
68050
- const channel = renderLabeledTarget(identity.channelName, identity.channelId, "#");
68051
- if (channel) {
68052
- segments.push(channel);
68053
- }
68054
- if (identity.threadTs) {
68055
- segments.push(`thread ${identity.threadTs}`);
68056
- }
68057
- return segments.join(" ");
68774
+
68775
+ // src/auth/resolve.ts
68776
+ function normalizePrincipal(principal) {
68777
+ const trimmed = principal.trim();
68778
+ if (!trimmed) {
68779
+ return "";
68058
68780
  }
68059
- if (identity.conversationKind === "dm") {
68060
- return ["Telegram direct message", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
68781
+ const [platform, userId] = trimmed.split(":", 2);
68782
+ if (!platform || !userId) {
68783
+ return trimmed;
68061
68784
  }
68062
- if (identity.conversationKind === "topic") {
68063
- const topic = renderNamedValue("topic", identity.topicName, identity.topicId);
68064
- const group = renderNamedValue("in group", identity.chatName, identity.chatId);
68065
- return [topic, group].filter(Boolean).join(" ");
68785
+ if (platform === "slack") {
68786
+ return `slack:${userId.trim().toUpperCase()}`;
68066
68787
  }
68067
- return ["Telegram group", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
68788
+ if (platform === "telegram") {
68789
+ return `telegram:${userId.trim()}`;
68790
+ }
68791
+ return `${platform}:${userId.trim()}`;
68068
68792
  }
68069
- function renderSenderSummary(identity) {
68070
- const sender = renderLabeledTarget(identity.senderName, identity.senderId);
68071
- return sender ? `sender ${sender}` : "";
68793
+ function normalizeRoleUsers(users) {
68794
+ return (users ?? []).map(normalizePrincipal).filter(Boolean);
68072
68795
  }
68073
- function renderLabeledTarget(name, id, namePrefix = "") {
68074
- const normalizedName = name?.trim();
68075
- const normalizedId = id?.trim();
68076
- if (normalizedName && normalizedId) {
68077
- return `${namePrefix}${normalizedName} (${normalizedId})`;
68796
+ function resolvePrincipal(identity) {
68797
+ const senderId = identity.senderId?.trim();
68798
+ if (!senderId) {
68799
+ return;
68078
68800
  }
68079
- if (normalizedName) {
68080
- return `${namePrefix}${normalizedName}`;
68801
+ if (identity.platform === "slack") {
68802
+ return normalizePrincipal(`slack:${senderId}`);
68081
68803
  }
68082
- return normalizedId ?? "";
68083
- }
68084
- function renderNamedValue(label, name, id) {
68085
- const value = renderLabeledTarget(name, id);
68086
- return value ? `${label} ${value}` : "";
68804
+ return normalizePrincipal(`telegram:${senderId}`);
68087
68805
  }
68088
- function buildReplyCommand(params) {
68089
- const lines = [`${params.command} message send \\`];
68090
- if (params.identity.platform === "slack") {
68091
- lines.push(" --channel slack \\");
68092
- lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
68093
- if (params.identity.threadTs) {
68094
- lines.push(` --thread-id ${params.identity.threadTs} \\`);
68806
+ function findExplicitRole(roles, principal) {
68807
+ if (!principal || !roles) {
68808
+ return;
68809
+ }
68810
+ for (const [roleName, roleDefinition] of Object.entries(roles)) {
68811
+ if (normalizeRoleUsers(roleDefinition.users).includes(principal)) {
68812
+ return roleName;
68095
68813
  }
68096
- lines.push(" --final \\");
68097
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
68098
- lines.push("<short progress update>");
68099
- lines.push("__CLISBOT_MESSAGE__");
68100
- lines.push(')" \\');
68101
- lines.push(" [--media /absolute/path/to/file]");
68102
- return lines.join(`
68103
- `);
68104
68814
  }
68105
- lines.push(" --channel telegram \\");
68106
- lines.push(` --target ${params.identity.chatId ?? ""} \\`);
68107
- if (params.identity.topicId) {
68108
- lines.push(` --thread-id ${params.identity.topicId} \\`);
68815
+ return;
68816
+ }
68817
+ function getAgentAuth(config, agentId) {
68818
+ const defaults = config.agents.defaults.auth;
68819
+ const entry = config.agents.list.find((item) => item.id === agentId);
68820
+ const override = entry?.auth;
68821
+ return {
68822
+ defaultRole: override?.defaultRole ?? defaults.defaultRole,
68823
+ roles: {
68824
+ ...defaults.roles,
68825
+ ...override?.roles ?? {}
68826
+ }
68827
+ };
68828
+ }
68829
+ function getAllowedPermissions(roles, role) {
68830
+ return new Set(roles?.[role]?.allow ?? []);
68831
+ }
68832
+ function hasAppPermission(config, appRole, permission) {
68833
+ if (appRole === "owner") {
68834
+ return true;
68109
68835
  }
68110
- lines.push(" --final \\");
68111
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
68112
- lines.push("<short progress update>");
68113
- lines.push("__CLISBOT_MESSAGE__");
68114
- lines.push(')" \\');
68115
- lines.push(" [--media /absolute/path/to/file]");
68116
- return lines.join(`
68117
- `);
68836
+ return getAllowedPermissions(config.app.auth.roles, appRole).has(permission);
68837
+ }
68838
+ function resolveChannelAuth(params) {
68839
+ const principal = resolvePrincipal(params.identity);
68840
+ const appAuth = params.config.app.auth;
68841
+ const explicitAppRole = findExplicitRole(appAuth.roles, principal);
68842
+ const appRole = explicitAppRole ?? appAuth.defaultRole;
68843
+ const appAdminLike = appRole === "owner" || appRole === "admin";
68844
+ const agentAuth = getAgentAuth(params.config, params.agentId);
68845
+ const explicitAgentRole = findExplicitRole(agentAuth.roles, principal);
68846
+ const agentRole = explicitAgentRole ?? agentAuth.defaultRole;
68847
+ const agentPermissions = getAllowedPermissions(agentAuth.roles, agentRole);
68848
+ const mayManageProtectedResources = appAdminLike || hasAppPermission(params.config, appRole, "configManage") || hasAppPermission(params.config, appRole, "appAuthManage") || hasAppPermission(params.config, appRole, "agentAuthManage") || hasAppPermission(params.config, appRole, "promptGovernanceManage");
68849
+ return {
68850
+ principal,
68851
+ appRole,
68852
+ agentRole,
68853
+ mayBypassPairing: appAdminLike,
68854
+ mayManageProtectedResources,
68855
+ canUseShell: appAdminLike || agentPermissions.has("shellExecute")
68856
+ };
68118
68857
  }
68119
68858
 
68120
68859
  // src/channels/slack/session-routing.ts
@@ -68172,6 +68911,14 @@ function resolveSlackConversationTarget(params) {
68172
68911
  };
68173
68912
  }
68174
68913
 
68914
+ // src/channels/privilege-commands.ts
68915
+ function resolvePrivilegeCommands(rootConfig, override) {
68916
+ return {
68917
+ enabled: override?.enabled ?? rootConfig.enabled,
68918
+ allowUsers: override?.allowUsers ?? rootConfig.allowUsers
68919
+ };
68920
+ }
68921
+
68175
68922
  // src/channels/route-policy.ts
68176
68923
  function buildSharedChannelRoute(params) {
68177
68924
  const privilegeCommands = resolvePrivilegeCommands(params.channelConfig.privilegeCommands, params.route?.privilegeCommands);
@@ -68196,6 +68943,7 @@ function buildSharedChannelRoute(params) {
68196
68943
  response: params.route?.response ?? params.channelConfig.response,
68197
68944
  responseMode: params.route?.responseMode ?? agentEntry?.responseMode ?? params.channelConfig.responseMode,
68198
68945
  additionalMessageMode: params.route?.additionalMessageMode ?? agentEntry?.additionalMessageMode ?? params.channelConfig.additionalMessageMode,
68946
+ verbose: params.route?.verbose ?? params.channelConfig.verbose,
68199
68947
  followUp: {
68200
68948
  mode: params.route?.followUp?.mode ?? params.channelConfig.followUp.mode,
68201
68949
  participationTtlMs: resolveConfigDurationMs({
@@ -68376,6 +69124,178 @@ async function clearSlackAssistantThreadStatus(client, target) {
68376
69124
  }
68377
69125
  }
68378
69126
 
69127
+ // src/channels/processing-indicator.ts
69128
+ function shouldResolveIndicatorWait(update) {
69129
+ return isTerminalRunStatus(update.status) || update.status === "detached";
69130
+ }
69131
+ async function waitForProcessingIndicatorLifecycle(params) {
69132
+ if (params.lifecycle !== "active-run") {
69133
+ return;
69134
+ }
69135
+ if (!params.agentService.hasActiveRun(params.sessionTarget)) {
69136
+ return;
69137
+ }
69138
+ let settled = false;
69139
+ const settle = () => {
69140
+ if (settled) {
69141
+ return;
69142
+ }
69143
+ settled = true;
69144
+ };
69145
+ try {
69146
+ await new Promise(async (resolve, reject) => {
69147
+ const resolveOnce = () => {
69148
+ if (settled) {
69149
+ return;
69150
+ }
69151
+ settled = true;
69152
+ resolve();
69153
+ };
69154
+ try {
69155
+ const observation = await params.agentService.observeRun(params.sessionTarget, {
69156
+ id: params.observerId,
69157
+ mode: "live",
69158
+ onUpdate: async (update) => {
69159
+ if (shouldResolveIndicatorWait(update)) {
69160
+ resolveOnce();
69161
+ }
69162
+ }
69163
+ });
69164
+ if (!observation.active || shouldResolveIndicatorWait(observation.update)) {
69165
+ resolveOnce();
69166
+ }
69167
+ } catch (error) {
69168
+ reject(error);
69169
+ }
69170
+ });
69171
+ } finally {
69172
+ settle();
69173
+ await params.agentService.detachRunObserver(params.sessionTarget, params.observerId).catch(() => {
69174
+ return;
69175
+ });
69176
+ }
69177
+ }
69178
+
69179
+ class ConversationProcessingIndicatorCoordinator {
69180
+ entries = new Map;
69181
+ async acquire(params) {
69182
+ let entry = this.entries.get(params.key);
69183
+ if (!entry) {
69184
+ entry = {
69185
+ activeRunHold: false,
69186
+ indicatorActive: false,
69187
+ key: params.key,
69188
+ operationChain: Promise.resolve(),
69189
+ refCount: 0
69190
+ };
69191
+ this.entries.set(params.key, entry);
69192
+ }
69193
+ entry.refCount += 1;
69194
+ await this.ensureIndicatorActive(entry, params.activate, params.onError);
69195
+ let released = false;
69196
+ return {
69197
+ setLifecycle: async (lifecycleParams) => {
69198
+ if (released || lifecycleParams.lifecycle !== "active-run" || entry.activeRunHold) {
69199
+ return;
69200
+ }
69201
+ entry.activeRunHold = true;
69202
+ entry.activeRunWait = waitForProcessingIndicatorLifecycle(lifecycleParams).catch((error) => {
69203
+ params.onError?.("active-run", error);
69204
+ }).finally(() => {
69205
+ entry.activeRunHold = false;
69206
+ entry.activeRunWait = undefined;
69207
+ this.maybeDeactivate(entry, params.onError);
69208
+ });
69209
+ },
69210
+ release: async () => {
69211
+ if (released) {
69212
+ return;
69213
+ }
69214
+ released = true;
69215
+ entry.refCount = Math.max(0, entry.refCount - 1);
69216
+ await this.maybeDeactivate(entry, params.onError);
69217
+ }
69218
+ };
69219
+ }
69220
+ async ensureIndicatorActive(entry, activate, onError) {
69221
+ entry.operationChain = entry.operationChain.then(async () => {
69222
+ if (entry.indicatorActive) {
69223
+ return;
69224
+ }
69225
+ try {
69226
+ const cleanup = await activate();
69227
+ entry.cleanup = typeof cleanup === "function" ? cleanup : undefined;
69228
+ entry.indicatorActive = true;
69229
+ } catch (error) {
69230
+ onError?.("activate", error);
69231
+ }
69232
+ });
69233
+ await entry.operationChain;
69234
+ }
69235
+ async maybeDeactivate(entry, onError) {
69236
+ if (entry.refCount > 0 || entry.activeRunHold) {
69237
+ return;
69238
+ }
69239
+ entry.operationChain = entry.operationChain.then(async () => {
69240
+ if (entry.refCount > 0 || entry.activeRunHold || !entry.indicatorActive) {
69241
+ return;
69242
+ }
69243
+ try {
69244
+ await entry.cleanup?.();
69245
+ } catch (error) {
69246
+ onError?.("deactivate", error);
69247
+ } finally {
69248
+ entry.cleanup = undefined;
69249
+ entry.indicatorActive = false;
69250
+ if (entry.refCount === 0 && !entry.activeRunHold) {
69251
+ this.entries.delete(entry.key);
69252
+ }
69253
+ }
69254
+ });
69255
+ await entry.operationChain;
69256
+ }
69257
+ }
69258
+
69259
+ // src/channels/slack/processing-decoration.ts
69260
+ async function activateSlackProcessingDecoration(params) {
69261
+ const [reactionResult, statusResult] = await Promise.allSettled([
69262
+ params.addReaction(),
69263
+ params.setStatus()
69264
+ ]);
69265
+ const reactionApplied = reactionResult.status === "fulfilled" ? reactionResult.value === true : false;
69266
+ const statusApplied = statusResult.status === "fulfilled" ? statusResult.value === true : false;
69267
+ if (reactionResult.status === "rejected") {
69268
+ params.onUnexpectedError?.("add-reaction", reactionResult.reason);
69269
+ }
69270
+ if (statusResult.status === "rejected") {
69271
+ params.onUnexpectedError?.("set-status", statusResult.reason);
69272
+ }
69273
+ if (!reactionApplied && !statusApplied) {
69274
+ if (reactionResult.status === "rejected") {
69275
+ throw reactionResult.reason;
69276
+ }
69277
+ if (statusResult.status === "rejected") {
69278
+ throw statusResult.reason;
69279
+ }
69280
+ }
69281
+ return async () => {
69282
+ if (reactionApplied) {
69283
+ try {
69284
+ await params.removeReaction();
69285
+ } catch (error) {
69286
+ params.onUnexpectedError?.("remove-reaction", error);
69287
+ }
69288
+ }
69289
+ if (statusApplied) {
69290
+ try {
69291
+ await params.clearStatus();
69292
+ } catch (error) {
69293
+ params.onUnexpectedError?.("clear-status", error);
69294
+ }
69295
+ }
69296
+ };
69297
+ }
69298
+
68379
69299
  // src/channels/slack/bolt-compat.ts
68380
69300
  var SlackBolt = __toESM(require_dist7(), 1);
68381
69301
  var { App } = SlackBolt;
@@ -68543,7 +69463,7 @@ async function downloadRemoteBuffer(params) {
68543
69463
 
68544
69464
  // src/agents/attachments/storage.ts
68545
69465
  import { access as access2 } from "node:fs/promises";
68546
- import { extname, join as join8 } from "node:path";
69466
+ import { extname, join as join9 } from "node:path";
68547
69467
  var CONTENT_TYPE_EXTENSION_MAP = {
68548
69468
  "application/json": ".json",
68549
69469
  "application/pdf": ".pdf",
@@ -68576,12 +69496,12 @@ function buildAttachmentFilename(params) {
68576
69496
  async function resolveUniquePath(directoryPath, fileName) {
68577
69497
  const extension = extname(fileName);
68578
69498
  const baseName = extension ? fileName.slice(0, -extension.length) : fileName;
68579
- let candidatePath = join8(directoryPath, fileName);
69499
+ let candidatePath = join9(directoryPath, fileName);
68580
69500
  let index = 2;
68581
69501
  while (true) {
68582
69502
  try {
68583
69503
  await access2(candidatePath);
68584
- candidatePath = join8(directoryPath, `${baseName}-${index}${extension}`);
69504
+ candidatePath = join9(directoryPath, `${baseName}-${index}${extension}`);
68585
69505
  index += 1;
68586
69506
  } catch {
68587
69507
  return candidatePath;
@@ -68589,7 +69509,7 @@ async function resolveUniquePath(directoryPath, fileName) {
68589
69509
  }
68590
69510
  }
68591
69511
  async function saveWorkspaceAttachment(params) {
68592
- const attachmentDir = join8(params.workspacePath, ".attachments", sanitizeSessionName(params.sessionKey), sanitizeSessionName(params.messageId));
69512
+ const attachmentDir = join9(params.workspacePath, ".attachments", sanitizeSessionName(params.sessionKey), sanitizeSessionName(params.messageId));
68593
69513
  await ensureDir2(attachmentDir);
68594
69514
  const fileName = buildAttachmentFilename({
68595
69515
  originalFilename: params.originalFilename,
@@ -68979,6 +69899,7 @@ class SlackSocketService {
68979
69899
  accountId;
68980
69900
  accountConfig;
68981
69901
  app;
69902
+ processingIndicators = new ConversationProcessingIndicatorCoordinator;
68982
69903
  botUserId = "";
68983
69904
  botLabel = "";
68984
69905
  teamId = "";
@@ -69122,12 +70043,22 @@ class SlackSocketService {
69122
70043
  if (params.conversationKind === "dm") {
69123
70044
  const directUserId = typeof event.user === "string" ? event.user.trim() : "";
69124
70045
  const dmConfig = this.loadedConfig.raw.channels.slack.directMessages;
70046
+ const auth2 = resolveChannelAuth({
70047
+ config: this.loadedConfig.raw,
70048
+ agentId: params.route.agentId,
70049
+ identity: {
70050
+ platform: "slack",
70051
+ conversationKind: params.conversationKind,
70052
+ senderId: directUserId || undefined,
70053
+ channelId
70054
+ }
70055
+ });
69125
70056
  if (!directUserId || dmConfig.policy === "disabled") {
69126
70057
  debugSlackEvent("drop-dm-disabled", { eventId, directUserId });
69127
70058
  await this.processedEventsStore.markCompleted(eventId);
69128
70059
  return;
69129
70060
  }
69130
- if (dmConfig.policy !== "open") {
70061
+ if (dmConfig.policy !== "open" && !auth2.mayBypassPairing) {
69131
70062
  const storedAllowFrom = await readChannelAllowFromStore("slack");
69132
70063
  const allowed = isSlackSenderAllowed({
69133
70064
  allowFrom: [...dmConfig.allowFrom, ...storedAllowFrom],
@@ -69238,17 +70169,28 @@ class SlackSocketService {
69238
70169
  timestamp: messageTs
69239
70170
  };
69240
70171
  let responseChunks = [];
70172
+ const cliTool = getAgentEntry(this.loadedConfig, params.route.agentId)?.cliTool;
70173
+ const identity = {
70174
+ platform: "slack",
70175
+ conversationKind: params.conversationKind,
70176
+ senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
70177
+ channelId,
70178
+ threadTs
70179
+ };
70180
+ const auth = resolveChannelAuth({
70181
+ config: this.loadedConfig.raw,
70182
+ agentId: params.route.agentId,
70183
+ identity
70184
+ });
70185
+ const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
69241
70186
  const agentPromptText = buildAgentPromptText({
69242
70187
  text,
69243
- identity: {
69244
- platform: "slack",
69245
- conversationKind: params.conversationKind,
69246
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69247
- channelId,
69248
- threadTs
69249
- },
70188
+ identity,
69250
70189
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69251
- responseMode: params.route.responseMode
70190
+ cliTool,
70191
+ responseMode: params.route.responseMode,
70192
+ streaming: params.route.streaming,
70193
+ protectedControlMutationRule
69252
70194
  });
69253
70195
  const timingContext = {
69254
70196
  platform: "slack",
@@ -69264,39 +70206,46 @@ class SlackSocketService {
69264
70206
  accountId: this.accountId
69265
70207
  });
69266
70208
  const ackReactionTask = waitForBackgroundSlackTask(addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.ackReaction, reactionTarget));
69267
- const processingDecorationTask = waitForBackgroundSlackTask(Promise.all([
69268
- addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
69269
- setSlackAssistantThreadStatus(this.app.client, this.loadedConfig.raw.channels.slack.processingStatus, {
69270
- channel: channelId,
69271
- threadTs
69272
- })
69273
- ]));
70209
+ const processingLease = await this.processingIndicators.acquire({
70210
+ key: `slack:${this.accountId}:${channelId}:${threadTs}`,
70211
+ activate: async () => activateSlackProcessingDecoration({
70212
+ addReaction: () => addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
70213
+ removeReaction: () => removeConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
70214
+ setStatus: () => setSlackAssistantThreadStatus(this.app.client, this.loadedConfig.raw.channels.slack.processingStatus, {
70215
+ channel: channelId,
70216
+ threadTs
70217
+ }),
70218
+ clearStatus: () => clearSlackAssistantThreadStatus(this.app.client, {
70219
+ channel: channelId,
70220
+ threadTs
70221
+ }),
70222
+ onUnexpectedError: (phase, error) => {
70223
+ console.error(`slack processing indicator ${phase} failed`, error);
70224
+ }
70225
+ }),
70226
+ onError: (phase, error) => {
70227
+ console.error(`slack processing indicator ${phase} failed`, error);
70228
+ }
70229
+ });
69274
70230
  try {
69275
- await processChannelInteraction({
70231
+ const interaction = await processChannelInteraction({
69276
70232
  agentService: this.agentService,
69277
70233
  sessionTarget,
69278
- identity: {
69279
- platform: "slack",
69280
- conversationKind: params.conversationKind,
69281
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69282
- channelId,
69283
- threadTs
69284
- },
70234
+ identity,
70235
+ auth,
69285
70236
  senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69286
70237
  text,
69287
70238
  agentPromptText,
69288
70239
  agentPromptBuilder: (nextText) => buildAgentPromptText({
69289
70240
  text: nextText,
69290
- identity: {
69291
- platform: "slack",
69292
- conversationKind: params.conversationKind,
69293
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69294
- channelId,
69295
- threadTs
69296
- },
70241
+ identity,
69297
70242
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69298
- responseMode: params.route.responseMode
70243
+ cliTool,
70244
+ responseMode: params.route.responseMode,
70245
+ streaming: params.route.streaming,
70246
+ protectedControlMutationRule
69299
70247
  }),
70248
+ protectedControlMutationRule,
69300
70249
  route: params.route,
69301
70250
  maxChars: this.getSlackMaxChars(params.route.agentId),
69302
70251
  timingContext,
@@ -69318,6 +70267,12 @@ class SlackSocketService {
69318
70267
  return responseChunks;
69319
70268
  }
69320
70269
  });
70270
+ await processingLease.setLifecycle({
70271
+ agentService: this.agentService,
70272
+ sessionTarget,
70273
+ observerId: `slack-processing:${channelId}:${threadTs}`,
70274
+ lifecycle: interaction.processingIndicatorLifecycle
70275
+ });
69321
70276
  await this.processedEventsStore.markCompleted(eventId);
69322
70277
  } catch (error) {
69323
70278
  console.error("slack handler error", error);
@@ -69325,12 +70280,7 @@ class SlackSocketService {
69325
70280
  return;
69326
70281
  } finally {
69327
70282
  await ackReactionTask;
69328
- await processingDecorationTask;
69329
- await removeConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget);
69330
- await clearSlackAssistantThreadStatus(this.app.client, {
69331
- channel: channelId,
69332
- threadTs
69333
- });
70283
+ await processingLease.release();
69334
70284
  }
69335
70285
  }
69336
70286
  registerEvents() {
@@ -70290,22 +71240,17 @@ function startTelegramTypingHeartbeat(params) {
70290
71240
  }
70291
71241
  };
70292
71242
  }
70293
- async function runWithTelegramTypingHeartbeat(params) {
71243
+ async function beginTelegramTypingHeartbeat(params) {
70294
71244
  try {
70295
71245
  await params.sendTyping();
70296
71246
  } catch (error) {
70297
71247
  logTelegramTypingError(params.onError, error);
70298
71248
  }
70299
- const stopHeartbeat = startTelegramTypingHeartbeat({
71249
+ return startTelegramTypingHeartbeat({
70300
71250
  sendTyping: params.sendTyping,
70301
71251
  intervalMs: params.intervalMs ?? TELEGRAM_TYPING_HEARTBEAT_MS,
70302
71252
  onError: params.onError
70303
71253
  });
70304
- try {
70305
- return await params.run();
70306
- } finally {
70307
- stopHeartbeat();
70308
- }
70309
71254
  }
70310
71255
 
70311
71256
  // src/channels/telegram/service.ts
@@ -70324,6 +71269,7 @@ var TELEGRAM_FULL_COMMANDS = [
70324
71269
  { command: "stop", description: "Interrupt current run" },
70325
71270
  { command: "nudge", description: "Send one extra Enter to the session" },
70326
71271
  { command: "followup", description: "Show or change follow-up mode" },
71272
+ { command: "streaming", description: "Show or change streaming mode" },
70327
71273
  { command: "responsemode", description: "Show or change response mode" },
70328
71274
  { command: "queue", description: "Queue a later message behind the active run" },
70329
71275
  { command: "steer", description: "Steer the active run immediately" },
@@ -70411,6 +71357,7 @@ class TelegramPollingService {
70411
71357
  loopPromise;
70412
71358
  activePollController;
70413
71359
  inFlightUpdates = new Set;
71360
+ processingIndicators = new ConversationProcessingIndicatorCoordinator;
70414
71361
  constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig) {
70415
71362
  this.loadedConfig = loadedConfig;
70416
71363
  this.agentService = agentService;
@@ -70551,11 +71498,21 @@ class TelegramPollingService {
70551
71498
  const directMessages = this.loadedConfig.raw.channels.telegram.directMessages;
70552
71499
  const senderId = message.from?.id != null ? String(message.from.id) : "";
70553
71500
  const senderUsername = message.from?.username;
71501
+ const auth = resolveChannelAuth({
71502
+ config: this.loadedConfig.raw,
71503
+ agentId: routeInfo.route.agentId,
71504
+ identity: {
71505
+ platform: "telegram",
71506
+ conversationKind: routeInfo.conversationKind,
71507
+ senderId: senderId || undefined,
71508
+ chatId: String(message.chat.id)
71509
+ }
71510
+ });
70554
71511
  if (!senderId || directMessages.policy === "disabled") {
70555
71512
  await this.processedEventsStore.markCompleted(eventId);
70556
71513
  return;
70557
71514
  }
70558
- if (directMessages.policy !== "open") {
71515
+ if (directMessages.policy !== "open" && !auth.mayBypassPairing) {
70559
71516
  const storedAllowFrom = await readChannelAllowFromStore("telegram");
70560
71517
  const allowed = isTelegramSenderAllowed({
70561
71518
  allowFrom: [...directMessages.allowFrom, ...storedAllowFrom],
@@ -70644,11 +71601,21 @@ class TelegramPollingService {
70644
71601
  chatName: message.chat.title?.trim() || undefined,
70645
71602
  topicId: routeInfo.topicId != null ? String(routeInfo.topicId) : undefined
70646
71603
  };
71604
+ const cliTool = getAgentEntry(this.loadedConfig, routeInfo.route.agentId)?.cliTool;
71605
+ const auth = resolveChannelAuth({
71606
+ config: this.loadedConfig.raw,
71607
+ agentId: routeInfo.route.agentId,
71608
+ identity
71609
+ });
71610
+ const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
70647
71611
  const agentPromptText = buildAgentPromptText({
70648
71612
  text,
70649
71613
  identity,
70650
71614
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
70651
- responseMode: routeInfo.route.responseMode
71615
+ cliTool,
71616
+ responseMode: routeInfo.route.responseMode,
71617
+ streaming: routeInfo.route.streaming,
71618
+ protectedControlMutationRule
70652
71619
  });
70653
71620
  const timingContext = {
70654
71621
  platform: "telegram",
@@ -70663,52 +71630,71 @@ class TelegramPollingService {
70663
71630
  responseMode: routeInfo.route.responseMode,
70664
71631
  accountId: this.accountId
70665
71632
  });
70666
- await runWithTelegramTypingHeartbeat({
70667
- sendTyping: () => this.sendTyping(message.chat.id, routeInfo.topicId),
70668
- onError: (error) => {
70669
- console.error("telegram typing failed", error);
70670
- },
70671
- run: async () => {
70672
- await processChannelInteraction({
70673
- agentService: this.agentService,
70674
- sessionTarget: routeInfo.sessionTarget,
70675
- identity,
70676
- senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
70677
- text,
70678
- agentPromptText,
70679
- agentPromptBuilder: (nextText) => buildAgentPromptText({
70680
- text: nextText,
70681
- identity,
70682
- config: this.loadedConfig.raw.channels.telegram.agentPrompt,
70683
- responseMode: routeInfo.route.responseMode
70684
- }),
70685
- route: routeInfo.route,
70686
- maxChars: this.getTelegramMaxChars(routeInfo.route.agentId),
70687
- timingContext,
70688
- postText: async (nextText) => {
70689
- responseChunks = await postTelegramText({
70690
- token: this.accountConfig.botToken,
70691
- chatId: message.chat.id,
70692
- text: nextText,
70693
- topicId: routeInfo.topicId,
70694
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
70695
- });
70696
- return responseChunks;
70697
- },
70698
- reconcileText: async (chunks, nextText) => {
70699
- responseChunks = await reconcileTelegramText({
70700
- token: this.accountConfig.botToken,
70701
- chatId: message.chat.id,
70702
- chunks,
70703
- text: nextText,
70704
- topicId: routeInfo.topicId,
70705
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
70706
- });
70707
- return responseChunks;
70708
- }
70709
- });
71633
+ const processingLease = await this.processingIndicators.acquire({
71634
+ key: `telegram:${this.accountId}:${message.chat.id}:${routeInfo.topicId ?? "root"}`,
71635
+ activate: async () => beginTelegramTypingHeartbeat({
71636
+ sendTyping: () => this.sendTyping(message.chat.id, routeInfo.topicId),
71637
+ onError: (error) => {
71638
+ console.error("telegram typing failed", error);
71639
+ }
71640
+ }),
71641
+ onError: (_phase, error) => {
71642
+ console.error("telegram processing indicator failed", error);
70710
71643
  }
70711
71644
  });
71645
+ try {
71646
+ const interaction = await processChannelInteraction({
71647
+ agentService: this.agentService,
71648
+ sessionTarget: routeInfo.sessionTarget,
71649
+ identity,
71650
+ auth,
71651
+ senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
71652
+ text,
71653
+ agentPromptText,
71654
+ agentPromptBuilder: (nextText) => buildAgentPromptText({
71655
+ text: nextText,
71656
+ identity,
71657
+ config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71658
+ cliTool,
71659
+ responseMode: routeInfo.route.responseMode,
71660
+ streaming: routeInfo.route.streaming,
71661
+ protectedControlMutationRule
71662
+ }),
71663
+ protectedControlMutationRule,
71664
+ route: routeInfo.route,
71665
+ maxChars: this.getTelegramMaxChars(routeInfo.route.agentId),
71666
+ timingContext,
71667
+ postText: async (nextText) => {
71668
+ responseChunks = await postTelegramText({
71669
+ token: this.accountConfig.botToken,
71670
+ chatId: message.chat.id,
71671
+ text: nextText,
71672
+ topicId: routeInfo.topicId,
71673
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71674
+ });
71675
+ return responseChunks;
71676
+ },
71677
+ reconcileText: async (chunks, nextText) => {
71678
+ responseChunks = await reconcileTelegramText({
71679
+ token: this.accountConfig.botToken,
71680
+ chatId: message.chat.id,
71681
+ chunks,
71682
+ text: nextText,
71683
+ topicId: routeInfo.topicId,
71684
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71685
+ });
71686
+ return responseChunks;
71687
+ }
71688
+ });
71689
+ await processingLease.setLifecycle({
71690
+ agentService: this.agentService,
71691
+ sessionTarget: routeInfo.sessionTarget,
71692
+ observerId: `telegram-processing:${message.chat.id}:${routeInfo.topicId ?? "root"}`,
71693
+ lifecycle: interaction.processingIndicatorLifecycle
71694
+ });
71695
+ } finally {
71696
+ await processingLease.release();
71697
+ }
70712
71698
  await this.processedEventsStore.markCompleted(eventId);
70713
71699
  } catch (error) {
70714
71700
  console.error("telegram handler error", error);
@@ -71109,9 +72095,9 @@ var defaultMessageCliDependencies = {
71109
72095
  loadConfig,
71110
72096
  plugins: listChannelPlugins(),
71111
72097
  print: (text) => console.log(text),
71112
- recordConversationReply: async ({ loadedConfig, target, kind }) => {
72098
+ recordConversationReply: async ({ loadedConfig, target, kind, source }) => {
71113
72099
  const agentService = new AgentService(loadedConfig);
71114
- await agentService.recordConversationReply(target, kind);
72100
+ await agentService.recordConversationReply(target, kind, source);
71115
72101
  }
71116
72102
  };
71117
72103
  function parseRepeatedOption2(args, name) {
@@ -71128,12 +72114,12 @@ function parseRepeatedOption2(args, name) {
71128
72114
  }
71129
72115
  return values;
71130
72116
  }
71131
- function parseOptionValue5(args, name) {
72117
+ function parseOptionValue4(args, name) {
71132
72118
  const values = parseRepeatedOption2(args, name);
71133
72119
  return values.length > 0 ? values.at(-1) : undefined;
71134
72120
  }
71135
72121
  function parseIntegerOption(args, name) {
71136
- const raw = parseOptionValue5(args, name);
72122
+ const raw = parseOptionValue4(args, name);
71137
72123
  if (!raw) {
71138
72124
  return;
71139
72125
  }
@@ -71162,25 +72148,25 @@ function parseMessageCommand(args) {
71162
72148
  }
71163
72149
  const action = rawAction;
71164
72150
  const rest = args.slice(1);
71165
- const channel = parseOptionValue5(rest, "--channel");
72151
+ const channel = parseOptionValue4(rest, "--channel");
71166
72152
  if (channel !== "slack" && channel !== "telegram") {
71167
72153
  throw new Error("--channel <slack|telegram> is required");
71168
72154
  }
71169
72155
  return {
71170
72156
  action,
71171
72157
  channel,
71172
- account: parseOptionValue5(rest, "--account"),
71173
- target: parseOptionValue5(rest, "--target"),
71174
- message: parseOptionValue5(rest, "--message") ?? parseOptionValue5(rest, "-m"),
71175
- media: parseOptionValue5(rest, "--media"),
71176
- messageId: parseOptionValue5(rest, "--message-id"),
71177
- emoji: parseOptionValue5(rest, "--emoji"),
72158
+ account: parseOptionValue4(rest, "--account"),
72159
+ target: parseOptionValue4(rest, "--target"),
72160
+ message: parseOptionValue4(rest, "--message") ?? parseOptionValue4(rest, "-m"),
72161
+ media: parseOptionValue4(rest, "--media"),
72162
+ messageId: parseOptionValue4(rest, "--message-id"),
72163
+ emoji: parseOptionValue4(rest, "--emoji"),
71178
72164
  remove: hasFlag3(rest, "--remove"),
71179
- threadId: parseOptionValue5(rest, "--thread-id"),
71180
- replyTo: parseOptionValue5(rest, "--reply-to"),
72165
+ threadId: parseOptionValue4(rest, "--thread-id"),
72166
+ replyTo: parseOptionValue4(rest, "--reply-to"),
71181
72167
  limit: parseIntegerOption(rest, "--limit"),
71182
- query: parseOptionValue5(rest, "--query"),
71183
- pollQuestion: parseOptionValue5(rest, "--poll-question"),
72168
+ query: parseOptionValue4(rest, "--query"),
72169
+ pollQuestion: parseOptionValue4(rest, "--poll-question"),
71184
72170
  pollOptions: parseRepeatedOption2(rest, "--poll-option"),
71185
72171
  forceDocument: hasFlag3(rest, "--force-document"),
71186
72172
  silent: hasFlag3(rest, "--silent"),
@@ -71223,7 +72209,9 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71223
72209
  throw new Error("--progress and --final cannot be used together");
71224
72210
  }
71225
72211
  assertTarget(command);
71226
- const loadedConfig = await dependencies.loadConfig(getConfigPath());
72212
+ const loadedConfig = await dependencies.loadConfig(getConfigPath(), {
72213
+ materializeChannels: [command.channel]
72214
+ });
71227
72215
  const plugin = dependencies.plugins.find((entry) => entry.id === command.channel);
71228
72216
  if (!plugin) {
71229
72217
  throw new Error(`Unsupported message channel: ${command.channel}`);
@@ -71238,7 +72226,8 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71238
72226
  await dependencies.recordConversationReply({
71239
72227
  loadedConfig,
71240
72228
  target: replyTarget,
71241
- kind: resolveReplyKind(command)
72229
+ kind: resolveReplyKind(command),
72230
+ source: "message-tool"
71242
72231
  });
71243
72232
  }
71244
72233
  if (command.json) {
@@ -71250,7 +72239,7 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71250
72239
 
71251
72240
  // src/control/runtime-supervisor.ts
71252
72241
  import { statSync as statSync4, watch } from "node:fs";
71253
- import { basename as basename4, dirname as dirname13 } from "node:path";
72242
+ import { basename as basename4, dirname as dirname14 } from "node:path";
71254
72243
 
71255
72244
  // src/channels/processed-events-store.ts
71256
72245
  import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
@@ -71328,7 +72317,7 @@ class ProcessedEventsStore {
71328
72317
  }
71329
72318
 
71330
72319
  // src/control/activity-store.ts
71331
- import { dirname as dirname12 } from "node:path";
72320
+ import { dirname as dirname13 } from "node:path";
71332
72321
  class ActivityStore {
71333
72322
  filePath;
71334
72323
  constructor(filePath = getDefaultActivityStorePath()) {
@@ -71367,7 +72356,7 @@ class ActivityStore {
71367
72356
  };
71368
72357
  }
71369
72358
  async write(document2) {
71370
- await ensureDir2(dirname12(this.filePath));
72359
+ await ensureDir2(dirname13(this.filePath));
71371
72360
  await writeTextFile(this.filePath, `${JSON.stringify(document2, null, 2)}
71372
72361
  `);
71373
72362
  }
@@ -71609,7 +72598,7 @@ class RuntimeSupervisor {
71609
72598
  if (this.configWatcher) {
71610
72599
  return;
71611
72600
  }
71612
- const watchedDir = dirname13(loadedConfig.configPath);
72601
+ const watchedDir = dirname14(loadedConfig.configPath);
71613
72602
  const watchedFile = basename4(loadedConfig.configPath);
71614
72603
  this.configWatcher = watch(watchedDir, (_eventType, filename) => {
71615
72604
  if (filename && filename.toString() !== watchedFile) {
@@ -71711,7 +72700,7 @@ async function getRuntimeOperatorSummary(params) {
71711
72700
  const agentSummaries = loadedConfig.raw.agents.list.map((entry) => {
71712
72701
  const resolved = new AgentService(loadedConfig).getResolvedAgentConfig(entry.id);
71713
72702
  const tool = deriveAgentTool(loadedConfig, entry.id);
71714
- const bootstrapState = getBootstrapWorkspaceState(resolved.workspacePath, entry.bootstrap?.mode, tool.cliTool === "codex" || tool.cliTool === "claude" ? tool.cliTool : undefined);
72703
+ const bootstrapState = getBootstrapWorkspaceState(resolved.workspacePath, entry.bootstrap?.mode, tool.cliTool === "codex" || tool.cliTool === "claude" || tool.cliTool === "gemini" ? tool.cliTool : undefined);
71715
72704
  return {
71716
72705
  id: entry.id,
71717
72706
  cliTool: tool.cliTool,
@@ -72048,6 +73037,9 @@ function printCommandOutcomeBanner(outcome) {
72048
73037
  console.log("+---------+");
72049
73038
  console.log("");
72050
73039
  }
73040
+ function printCommandOutcomeFooter(outcome) {
73041
+ printCommandOutcomeBanner(outcome);
73042
+ }
72051
73043
  function getPrimaryWorkspacePath(summary) {
72052
73044
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
72053
73045
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -72068,13 +73060,18 @@ function printMissingBootstrapOptions(commandName) {
72068
73060
  console.log(` clisbot ${commandName} --cli codex --bot-type team`);
72069
73061
  console.log(` clisbot ${commandName} --cli claude --bot-type personal`);
72070
73062
  console.log(` clisbot ${commandName} --cli claude --bot-type team`);
73063
+ console.log(` clisbot ${commandName} --cli gemini --bot-type personal`);
73064
+ console.log(` clisbot ${commandName} --cli gemini --bot-type team`);
72071
73065
  console.log("Manual setup is still available with `clisbot agents add ...`.");
72072
73066
  for (const line of renderOperatorHelpLines()) {
72073
73067
  console.log(line);
72074
73068
  }
73069
+ if (commandName === "start") {
73070
+ printCommandOutcomeFooter("failure");
73071
+ }
72075
73072
  }
72076
73073
  function hasLiteralMemCredentials(flags) {
72077
- return flags.literalWarnings.length > 0;
73074
+ return hasLiteralBootstrapCredentials(flags);
72078
73075
  }
72079
73076
  async function prepareBootstrapState(rawArgs, commandName, options = {
72080
73077
  runtimeRunning: false
@@ -72090,6 +73087,9 @@ async function prepareBootstrapState(rawArgs, commandName, options = {
72090
73087
  for (const line of renderMissingTokenWarningLines()) {
72091
73088
  console.log(line);
72092
73089
  }
73090
+ if (commandName === "start") {
73091
+ printCommandOutcomeFooter("failure");
73092
+ }
72093
73093
  return null;
72094
73094
  }
72095
73095
  if (commandName === "init" && hasLiteralMemCredentials(bootstrapFlags) && !bootstrapFlags.persist) {
@@ -72151,6 +73151,7 @@ async function ensureDefaultAgentBootstrap(state, options, commandName) {
72151
73151
  for (const line of renderOperatorHelpLines()) {
72152
73152
  console.log(line);
72153
73153
  }
73154
+ printCommandOutcomeFooter("failure");
72154
73155
  return false;
72155
73156
  }
72156
73157
  }
@@ -72224,11 +73225,14 @@ async function serveForeground() {
72224
73225
  async function start(args = []) {
72225
73226
  const runtimeStatus = await getRuntimeStatus();
72226
73227
  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.");
73228
+ const restartForLiteralBootstrap = runtimeStatus.running && hasLiteralMemCredentials(bootstrapFlags);
73229
+ if (restartForLiteralBootstrap) {
73230
+ await stopDetachedRuntime({
73231
+ configPath: runtimeStatus.configPath
73232
+ });
72229
73233
  }
72230
73234
  const state = await prepareBootstrapState(args, "start", {
72231
- runtimeRunning: runtimeStatus.running,
73235
+ runtimeRunning: restartForLiteralBootstrap ? false : runtimeStatus.running,
72232
73236
  bootstrapFlags
72233
73237
  });
72234
73238
  if (!state) {
@@ -72244,13 +73248,14 @@ async function start(args = []) {
72244
73248
  for (const line of renderConfiguredChannelTokenStatusLines(state.config, runtimeMemEnv)) {
72245
73249
  console.log(line);
72246
73250
  }
72247
- if (!runtimeStatus.running) {
73251
+ if (restartForLiteralBootstrap || !runtimeStatus.running) {
72248
73252
  const tokenIssueLines = renderConfiguredChannelTokenIssueLines(state.config, runtimeMemEnv);
72249
73253
  for (const line of tokenIssueLines) {
72250
73254
  console.log(line);
72251
73255
  }
72252
73256
  if (tokenIssueLines.length > 0) {
72253
73257
  printCommandOutcomeBanner("failure");
73258
+ printCommandOutcomeFooter("failure");
72254
73259
  return;
72255
73260
  }
72256
73261
  }
@@ -72266,7 +73271,7 @@ async function start(args = []) {
72266
73271
  const result = await startDetachedRuntime({
72267
73272
  scriptPath: fileURLToPath4(import.meta.url),
72268
73273
  configPath: state.configResult.configPath,
72269
- extraEnv: runtimeStatus.running ? undefined : runtimeMemEnv,
73274
+ extraEnv: restartForLiteralBootstrap || !runtimeStatus.running ? runtimeMemEnv : undefined,
72270
73275
  runtimeCredentialsPath: getDefaultRuntimeCredentialsPath()
72271
73276
  });
72272
73277
  if (result.alreadyRunning) {
@@ -72284,6 +73289,7 @@ async function start(args = []) {
72284
73289
  console.log(`config: ${result.configPath}`);
72285
73290
  console.log(`log: ${result.logPath}`);
72286
73291
  console.log(renderStartSummary(summary));
73292
+ printCommandOutcomeFooter("success");
72287
73293
  } catch (error) {
72288
73294
  printCommandOutcomeBanner("success");
72289
73295
  console.log(`clisbot is already running with pid: ${result.pid}`);
@@ -72294,6 +73300,7 @@ async function start(args = []) {
72294
73300
  for (const line of renderRuntimeErrorLines("failed to render already-running summary", error)) {
72295
73301
  console.error(line);
72296
73302
  }
73303
+ printCommandOutcomeFooter("success");
72297
73304
  }
72298
73305
  return;
72299
73306
  }
@@ -72314,6 +73321,7 @@ async function start(args = []) {
72314
73321
  console.log(`config: ${result.configPath}`);
72315
73322
  console.log(`log: ${result.logPath}`);
72316
73323
  console.log(renderStartSummary(summary));
73324
+ printCommandOutcomeFooter("success");
72317
73325
  } catch (error) {
72318
73326
  printCommandOutcomeBanner("success");
72319
73327
  console.log(`clisbot started with pid: ${result.pid}`);
@@ -72322,6 +73330,7 @@ async function start(args = []) {
72322
73330
  for (const line of renderRuntimeErrorLines("failed to render start summary", error)) {
72323
73331
  console.error(line);
72324
73332
  }
73333
+ printCommandOutcomeFooter("success");
72325
73334
  }
72326
73335
  }
72327
73336
  async function printCliError(error) {
@@ -72363,15 +73372,18 @@ async function stop(hard = false) {
72363
73372
  if (!result.stopped && !hard) {
72364
73373
  printCommandOutcomeBanner("failure");
72365
73374
  console.log("clisbot is not running");
73375
+ printCommandOutcomeFooter("failure");
72366
73376
  return;
72367
73377
  }
72368
73378
  if (hard) {
72369
73379
  printCommandOutcomeBanner("success");
72370
73380
  console.log(result.stopped ? "clisbot stopped and tmux sessions cleaned up" : "clisbot was not running, but tmux sessions were cleaned up");
73381
+ printCommandOutcomeFooter("success");
72371
73382
  return;
72372
73383
  }
72373
73384
  printCommandOutcomeBanner("success");
72374
73385
  console.log("clisbot stopped");
73386
+ printCommandOutcomeFooter("success");
72375
73387
  }
72376
73388
  async function restart() {
72377
73389
  await stopDetachedRuntime({