clisbot 0.1.17 → 0.1.20

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
@@ -54471,14 +54471,22 @@ function renderPairingSetupHelpLines(prefix = "", options = {}) {
54471
54471
  return lines;
54472
54472
  }
54473
54473
  lines.push(`${prefix}Pairing notes:`);
54474
+ if (shouldRenderSlack && shouldRenderTelegram) {
54475
+ lines.push(`${prefix} - Send a direct message (DM) to the Telegram or Slack bot. Send \`/start\` or \`hi\` to receive a pairing code.`);
54476
+ }
54474
54477
  if (shouldRenderTelegram) {
54475
- lines.push(`${prefix} - Telegram DMs use \`pairing\`. Send \`/start\` or \`hi\` to the Telegram bot to get a pairing code.`);
54478
+ if (!shouldRenderSlack) {
54479
+ lines.push(`${prefix} - Send a direct message (DM) to the Telegram bot. Send \`/start\` or \`hi\` to receive a pairing code.`);
54480
+ }
54476
54481
  lines.push(`${prefix} - Approve the returned Telegram code with: \`clisbot pairing approve telegram <code>\``);
54477
54482
  }
54478
54483
  if (shouldRenderSlack) {
54479
- lines.push(`${prefix} - Slack DMs use \`pairing\`. Say \`hi\` to the Slack bot to get a pairing code.`);
54484
+ if (!shouldRenderTelegram) {
54485
+ lines.push(`${prefix} - Send a direct message (DM) to the Slack bot. Say \`hi\` to receive a pairing code.`);
54486
+ }
54480
54487
  lines.push(`${prefix} - Approve the returned Slack code with: \`clisbot pairing approve slack <code>\``);
54481
54488
  }
54489
+ lines.push(`${prefix} - Configured app owner/admin principals bypass pairing in DMs.`);
54482
54490
  return lines;
54483
54491
  }
54484
54492
  function renderTmuxDebugHelpLines(prefix = "") {
@@ -54585,6 +54593,12 @@ function parseCliArgs(argv) {
54585
54593
  args: args.slice(1)
54586
54594
  };
54587
54595
  }
54596
+ if (command === "auth") {
54597
+ return {
54598
+ name: "auth",
54599
+ args: args.slice(1)
54600
+ };
54601
+ }
54588
54602
  if (command === "pairing") {
54589
54603
  return {
54590
54604
  name: "pairing",
@@ -54607,6 +54621,10 @@ function renderCliHelp() {
54607
54621
  return [
54608
54622
  `clisbot v${getClisbotVersion()}`,
54609
54623
  "",
54624
+ "Platform support:",
54625
+ " Linux/macOS Supported.",
54626
+ " Windows Native Windows is not supported yet. Use WSL2.",
54627
+ "",
54610
54628
  "Fastest start:",
54611
54629
  " 1. Choose the channels you want to bootstrap explicitly.",
54612
54630
  " 2. Run one of these commands:",
@@ -54639,6 +54657,7 @@ function renderCliHelp() {
54639
54657
  " clisbot loops <subcommand>",
54640
54658
  " clisbot message <subcommand>",
54641
54659
  " clisbot agents <subcommand>",
54660
+ " clisbot auth <subcommand>",
54642
54661
  " clisbot pairing <subcommand>",
54643
54662
  " clisbot init [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]",
54644
54663
  " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
@@ -54675,6 +54694,7 @@ function renderCliHelp() {
54675
54694
  " cancel --all",
54676
54695
  " message Run provider message actions such as send, react, read, edit, delete, and pins.",
54677
54696
  " agents Manage configured agents and top-level bindings.",
54697
+ " auth Manage app and agent auth roles, principals, and permissions in config. See `clisbot auth --help`.",
54678
54698
  " pairing Run the pairing control CLI.",
54679
54699
  ` init Seed ${configPath} and optionally create the first agent without starting clisbot.`,
54680
54700
  " --version, -v Show the installed clisbot version.",
@@ -59856,6 +59876,10 @@ var authRoleSchema = exports_external.object({
59856
59876
  allow: exports_external.array(exports_external.string().min(1)).default([]),
59857
59877
  users: exports_external.array(exports_external.string().min(1)).default([])
59858
59878
  });
59879
+ var authRoleOverrideSchema = exports_external.object({
59880
+ allow: exports_external.array(exports_external.string().min(1)).optional(),
59881
+ users: exports_external.array(exports_external.string().min(1)).optional()
59882
+ });
59859
59883
  var appAuthSchema = exports_external.object({
59860
59884
  ownerClaimWindowMinutes: exports_external.number().int().positive().default(30),
59861
59885
  defaultRole: exports_external.string().min(1).default("member"),
@@ -59887,11 +59911,15 @@ var agentAuthSchema = exports_external.object({
59887
59911
  }
59888
59912
  })
59889
59913
  });
59914
+ var agentAuthOverrideSchema = exports_external.object({
59915
+ defaultRole: exports_external.string().min(1).optional(),
59916
+ roles: exports_external.record(exports_external.string(), authRoleOverrideSchema).default({})
59917
+ });
59890
59918
  var agentOverrideSchema = exports_external.object({
59891
59919
  workspace: exports_external.string().optional(),
59892
59920
  responseMode: exports_external.enum(["capture-pane", "message-tool"]).optional(),
59893
59921
  additionalMessageMode: exports_external.enum(["queue", "steer"]).optional(),
59894
- auth: agentAuthSchema.optional(),
59922
+ auth: agentAuthOverrideSchema.optional(),
59895
59923
  runner: runnerOverrideSchema.optional(),
59896
59924
  stream: streamSchema.partial().optional(),
59897
59925
  session: sessionSchema.partial().optional()
@@ -59986,9 +60014,18 @@ var channelAgentPromptSchema = exports_external.object({
59986
60014
  var channelResponseModeSchema = exports_external.enum(["capture-pane", "message-tool"]);
59987
60015
  var channelAdditionalMessageModeSchema = exports_external.enum(["queue", "steer"]);
59988
60016
  var channelVerboseSchema = exports_external.enum(["off", "minimal"]);
60017
+ var surfaceNotificationModeSchema = exports_external.enum(["none", "brief", "full"]);
59989
60018
  var timezoneSchema = exports_external.string().refine(isValidLoopTimezone, {
59990
60019
  message: "Expected a valid IANA timezone such as Asia/Ho_Chi_Minh"
59991
60020
  });
60021
+ var surfaceNotificationsSchema = exports_external.object({
60022
+ queueStart: surfaceNotificationModeSchema.default("brief"),
60023
+ loopStart: surfaceNotificationModeSchema.default("brief")
60024
+ });
60025
+ var surfaceNotificationsOverrideSchema = exports_external.object({
60026
+ queueStart: surfaceNotificationModeSchema.optional(),
60027
+ loopStart: surfaceNotificationModeSchema.optional()
60028
+ });
59992
60029
  var slackRouteSchema = exports_external.object({
59993
60030
  requireMention: exports_external.boolean().default(true),
59994
60031
  allowBots: exports_external.boolean().default(false),
@@ -59999,6 +60036,7 @@ var slackRouteSchema = exports_external.object({
59999
60036
  response: slackResponseSchema.optional(),
60000
60037
  responseMode: channelResponseModeSchema.optional(),
60001
60038
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60039
+ surfaceNotifications: surfaceNotificationsOverrideSchema.optional(),
60002
60040
  verbose: channelVerboseSchema.optional(),
60003
60041
  followUp: slackFollowUpOverrideSchema.optional(),
60004
60042
  timezone: timezoneSchema.optional()
@@ -60013,6 +60051,7 @@ var telegramTopicRouteSchema = exports_external.object({
60013
60051
  response: slackResponseSchema.optional(),
60014
60052
  responseMode: channelResponseModeSchema.optional(),
60015
60053
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60054
+ surfaceNotifications: surfaceNotificationsOverrideSchema.optional(),
60016
60055
  verbose: channelVerboseSchema.optional(),
60017
60056
  followUp: slackFollowUpOverrideSchema.optional(),
60018
60057
  timezone: timezoneSchema.optional()
@@ -60027,6 +60066,7 @@ var telegramGroupRouteSchema = exports_external.object({
60027
60066
  response: slackResponseSchema.optional(),
60028
60067
  responseMode: channelResponseModeSchema.optional(),
60029
60068
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60069
+ surfaceNotifications: surfaceNotificationsOverrideSchema.optional(),
60030
60070
  verbose: channelVerboseSchema.optional(),
60031
60071
  followUp: slackFollowUpOverrideSchema.optional(),
60032
60072
  timezone: timezoneSchema.optional(),
@@ -60045,6 +60085,7 @@ var telegramDirectMessagesSchema = exports_external.object({
60045
60085
  response: slackResponseSchema.optional(),
60046
60086
  responseMode: channelResponseModeSchema.optional(),
60047
60087
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60088
+ surfaceNotifications: surfaceNotificationsOverrideSchema.optional(),
60048
60089
  verbose: channelVerboseSchema.optional(),
60049
60090
  followUp: slackFollowUpOverrideSchema.optional(),
60050
60091
  timezone: timezoneSchema.optional()
@@ -60082,6 +60123,7 @@ var telegramSchema = exports_external.object({
60082
60123
  response: slackResponseSchema.default("final"),
60083
60124
  responseMode: channelResponseModeSchema.default("message-tool"),
60084
60125
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60126
+ surfaceNotifications: surfaceNotificationsSchema.optional(),
60085
60127
  verbose: channelVerboseSchema.default("minimal"),
60086
60128
  followUp: slackFollowUpSchema.default({
60087
60129
  mode: "auto",
@@ -60113,6 +60155,7 @@ var directMessagesSchema = exports_external.object({
60113
60155
  response: slackResponseSchema.optional(),
60114
60156
  responseMode: channelResponseModeSchema.optional(),
60115
60157
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60158
+ surfaceNotifications: surfaceNotificationsOverrideSchema.optional(),
60116
60159
  verbose: channelVerboseSchema.optional(),
60117
60160
  followUp: slackFollowUpOverrideSchema.optional(),
60118
60161
  timezone: timezoneSchema.optional()
@@ -60157,6 +60200,7 @@ var slackSchema = exports_external.object({
60157
60200
  response: slackResponseSchema.default("final"),
60158
60201
  responseMode: channelResponseModeSchema.default("message-tool"),
60159
60202
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60203
+ surfaceNotifications: surfaceNotificationsSchema.optional(),
60160
60204
  verbose: channelVerboseSchema.default("minimal"),
60161
60205
  followUp: slackFollowUpSchema.default({
60162
60206
  mode: "auto",
@@ -60308,6 +60352,10 @@ var clisbotConfigSchema = exports_external.object({
60308
60352
  response: "final",
60309
60353
  responseMode: "message-tool",
60310
60354
  additionalMessageMode: "steer",
60355
+ surfaceNotifications: {
60356
+ queueStart: "brief",
60357
+ loopStart: "brief"
60358
+ },
60311
60359
  verbose: "minimal",
60312
60360
  followUp: {
60313
60361
  mode: "auto",
@@ -60625,6 +60673,10 @@ function renderDefaultConfigTemplate(options = {}) {
60625
60673
  response: "final",
60626
60674
  responseMode: "message-tool",
60627
60675
  additionalMessageMode: "steer",
60676
+ surfaceNotifications: {
60677
+ queueStart: "brief",
60678
+ loopStart: "brief"
60679
+ },
60628
60680
  verbose: "minimal",
60629
60681
  followUp: {
60630
60682
  mode: "auto",
@@ -60666,6 +60718,10 @@ function renderDefaultConfigTemplate(options = {}) {
60666
60718
  response: "final",
60667
60719
  responseMode: "message-tool",
60668
60720
  additionalMessageMode: "steer",
60721
+ surfaceNotifications: {
60722
+ queueStart: "brief",
60723
+ loopStart: "brief"
60724
+ },
60669
60725
  verbose: "minimal",
60670
60726
  followUp: {
60671
60727
  mode: "auto",
@@ -61834,6 +61890,8 @@ function getExecutableNames(command) {
61834
61890
  // src/runners/tmux/client.ts
61835
61891
  var MAIN_WINDOW_NAME = "main";
61836
61892
  var TMUX_NOT_FOUND_CODE = "ENOENT";
61893
+ var TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS = 1000;
61894
+ var TMUX_SERVER_BOOTSTRAP_POLL_MS = 25;
61837
61895
  var TMUX_SERVER_DEFAULTS = [
61838
61896
  ["exit-empty", "off"],
61839
61897
  ["destroy-unattached", "off"]
@@ -61844,6 +61902,9 @@ class TmuxClient {
61844
61902
  constructor(socketPath) {
61845
61903
  this.socketPath = socketPath;
61846
61904
  }
61905
+ isServerUnavailableOutput(output) {
61906
+ return /no server running|No such file or directory|failed to connect to server|error connecting to/i.test(output);
61907
+ }
61847
61908
  async exec(args, options = {}) {
61848
61909
  if (!commandExists("tmux")) {
61849
61910
  throw new Error("tmux is not installed or not available on PATH. Install tmux and restart clisbot.");
@@ -61895,7 +61956,10 @@ class TmuxClient {
61895
61956
  }
61896
61957
  const output = `${result.stderr}
61897
61958
  ${result.stdout}`.trim();
61898
- return !output.includes("no server running");
61959
+ if (this.isServerUnavailableOutput(output)) {
61960
+ return false;
61961
+ }
61962
+ return false;
61899
61963
  }
61900
61964
  async ensureServerDefaults() {
61901
61965
  if (!await this.isServerRunning()) {
@@ -61905,6 +61969,35 @@ ${result.stdout}`.trim();
61905
61969
  await this.execOrThrow(["set-option", "-g", name, value]);
61906
61970
  }
61907
61971
  }
61972
+ isBootstrapRetryableError(error) {
61973
+ const message = error instanceof Error ? error.message : String(error);
61974
+ return this.isServerUnavailableOutput(message);
61975
+ }
61976
+ async withServerBootstrapRetry(task) {
61977
+ const deadline = Date.now() + TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS;
61978
+ while (true) {
61979
+ try {
61980
+ return await task();
61981
+ } catch (error) {
61982
+ if (!this.isBootstrapRetryableError(error) || Date.now() >= deadline) {
61983
+ throw error;
61984
+ }
61985
+ await sleep(TMUX_SERVER_BOOTSTRAP_POLL_MS);
61986
+ }
61987
+ }
61988
+ }
61989
+ async waitForSessionBootstrap(sessionName) {
61990
+ const deadline = Date.now() + TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS;
61991
+ while (true) {
61992
+ if (await this.hasSession(sessionName)) {
61993
+ return;
61994
+ }
61995
+ if (Date.now() >= deadline) {
61996
+ throw new Error(`tmux session "${sessionName}" did not become reachable on socket ${this.socketPath} within ${TMUX_SERVER_BOOTSTRAP_TIMEOUT_MS}ms.`);
61997
+ }
61998
+ await sleep(TMUX_SERVER_BOOTSTRAP_POLL_MS);
61999
+ }
62000
+ }
61908
62001
  async newSession(params) {
61909
62002
  await this.execOrThrow([
61910
62003
  "new-session",
@@ -61917,8 +62010,13 @@ ${result.stdout}`.trim();
61917
62010
  params.cwd,
61918
62011
  params.command
61919
62012
  ]);
61920
- await this.ensureServerDefaults();
61921
- await this.freezeWindowName(`${params.sessionName}:${MAIN_WINDOW_NAME}`);
62013
+ await this.waitForSessionBootstrap(params.sessionName);
62014
+ await this.withServerBootstrapRetry(async () => {
62015
+ await this.ensureServerDefaults();
62016
+ });
62017
+ await this.withServerBootstrapRetry(async () => {
62018
+ await this.freezeWindowName(`${params.sessionName}:${MAIN_WINDOW_NAME}`);
62019
+ });
61922
62020
  }
61923
62021
  async newWindow(params) {
61924
62022
  const paneId = await this.execOrThrow([
@@ -62705,40 +62803,329 @@ async function runAccountsCli(args, deps = {}) {
62705
62803
  throw new Error(renderAccountsHelp());
62706
62804
  }
62707
62805
 
62708
- // src/channels/telegram/route-guidance.ts
62709
- function renderTelegramRouteChoiceMessage(params) {
62710
- const chatId = String(params.chatId);
62711
- const topicId = params.topicId != null ? String(params.topicId) : undefined;
62712
- const lines = [
62713
- topicId != null ? "clisbot: this Telegram topic is not configured yet." : "clisbot: this Telegram group is not configured yet.",
62806
+ // src/control/auth-cli.ts
62807
+ function getEditableConfigPath3() {
62808
+ return process.env.CLISBOT_CONFIG_PATH;
62809
+ }
62810
+ function parseRepeatedOption2(args, name) {
62811
+ const values = [];
62812
+ for (let index = 0;index < args.length; index += 1) {
62813
+ if (args[index] !== name) {
62814
+ continue;
62815
+ }
62816
+ const value = args[index + 1]?.trim();
62817
+ if (!value) {
62818
+ throw new Error(`Missing value for ${name}`);
62819
+ }
62820
+ values.push(value);
62821
+ }
62822
+ return values;
62823
+ }
62824
+ function parseSingleOption2(args, name) {
62825
+ const values = parseRepeatedOption2(args, name);
62826
+ if (values.length === 0) {
62827
+ return;
62828
+ }
62829
+ return values[values.length - 1];
62830
+ }
62831
+ function hasFlag3(args, name) {
62832
+ return args.includes(name);
62833
+ }
62834
+ function parseScope(raw, args) {
62835
+ if (raw === "app") {
62836
+ return { kind: "app" };
62837
+ }
62838
+ if (raw === "agent-defaults") {
62839
+ return { kind: "agent-defaults" };
62840
+ }
62841
+ if (raw === "agent") {
62842
+ const agentId = parseSingleOption2(args, "--agent");
62843
+ if (!agentId) {
62844
+ throw new Error("Missing value for --agent");
62845
+ }
62846
+ return { kind: "agent", agentId };
62847
+ }
62848
+ throw new Error("Scope required: app | agent-defaults | agent");
62849
+ }
62850
+ function renderAuthCliHelp() {
62851
+ return [
62852
+ "clisbot auth",
62714
62853
  "",
62715
- "Ask the bot owner to choose one of these:",
62854
+ "Manage auth roles, principals, and permissions in config.",
62716
62855
  "",
62717
- "Add the whole group with the default agent:",
62718
- `\`clisbot channels add telegram-group ${chatId}\``,
62856
+ "Usage:",
62857
+ " clisbot auth list [--json]",
62858
+ " clisbot auth show <app|agent-defaults|agent> [--agent <id>] [--json]",
62859
+ " clisbot auth add-user <app|agent-defaults|agent> --role <role> --user <principal> [--agent <id>]",
62860
+ " clisbot auth remove-user <app|agent-defaults|agent> --role <role> --user <principal> [--agent <id>]",
62861
+ " clisbot auth add-permission <app|agent-defaults|agent> --role <role> --permission <permission> [--agent <id>]",
62862
+ " clisbot auth remove-permission <app|agent-defaults|agent> --role <role> --permission <permission> [--agent <id>]",
62719
62863
  "",
62720
- "Add the whole group with a specific agent:",
62721
- `\`clisbot channels add telegram-group ${chatId} --agent <id>\``
62722
- ];
62723
- if (topicId != null) {
62724
- lines.push("", "Add only this topic with a specific agent:", `\`clisbot channels add telegram-group ${chatId} --topic ${topicId} --agent <id>\``);
62864
+ "Scopes:",
62865
+ " app edit app.auth",
62866
+ " agent-defaults edit agents.defaults.auth",
62867
+ " agent edit one agents.list[].auth override; requires --agent <id>",
62868
+ "",
62869
+ "Permission sets:",
62870
+ ` app ${APP_ADMIN_PERMISSIONS.join(", ")}`,
62871
+ ` agent ${DEFAULT_AGENT_ADMIN_PERMISSIONS.join(", ")}`,
62872
+ "",
62873
+ "Notes:",
62874
+ " add-user/remove-user mutate roles.<role>.users",
62875
+ " add-permission/remove-permission mutate roles.<role>.allow",
62876
+ " agent role edits clone the inherited agent-defaults role into the target agent override on first write",
62877
+ "",
62878
+ "Examples:",
62879
+ " clisbot auth add-user app --role owner --user telegram:1276408333",
62880
+ " clisbot auth remove-user app --role admin --user slack:U123",
62881
+ " clisbot auth add-user agent --agent default --role admin --user slack:U123",
62882
+ " clisbot auth add-permission agent-defaults --role member --permission shellExecute",
62883
+ " clisbot auth remove-permission agent --agent default --role member --permission shellExecute",
62884
+ " clisbot auth show agent-defaults",
62885
+ " clisbot auth list --json"
62886
+ ].join(`
62887
+ `);
62888
+ }
62889
+ function cloneRoleDefinition(value) {
62890
+ return {
62891
+ allow: [...value?.allow ?? []],
62892
+ users: [...value?.users ?? []]
62893
+ };
62894
+ }
62895
+ function mergeRoleDefinitions(inherited, override) {
62896
+ return {
62897
+ allow: [...override?.allow ?? inherited?.allow ?? []],
62898
+ users: [...override?.users ?? inherited?.users ?? []]
62899
+ };
62900
+ }
62901
+ function mergeRoleRecord(defaults, overrides) {
62902
+ const merged = {};
62903
+ const roleNames = new Set([
62904
+ ...Object.keys(defaults ?? {}),
62905
+ ...Object.keys(overrides ?? {})
62906
+ ]);
62907
+ for (const roleName of roleNames) {
62908
+ merged[roleName] = mergeRoleDefinitions(defaults?.[roleName], overrides?.[roleName]);
62725
62909
  }
62726
- if (params.includeConfigPath) {
62727
- lines.push("", topicId != null ? `Config path: \`channels.telegram.groups."${chatId}".topics."${topicId}"\`` : `Config path: \`channels.telegram.groups."${chatId}"\``);
62728
- } else {
62729
- lines.push("", "After that, routed commands such as `/status`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
62910
+ return merged;
62911
+ }
62912
+ function normalizeUnique(values) {
62913
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
62914
+ }
62915
+ function ensureAgentEntry(config, agentId) {
62916
+ const entry = config.agents.list.find((item) => item.id === agentId);
62917
+ if (!entry) {
62918
+ throw new Error(`Unknown agent: ${agentId}`);
62730
62919
  }
62731
- return lines.join(`
62732
- `);
62920
+ return entry;
62921
+ }
62922
+ function getAuthLabel(scope) {
62923
+ if (scope.kind === "app") {
62924
+ return "app.auth";
62925
+ }
62926
+ if (scope.kind === "agent-defaults") {
62927
+ return "agents.defaults.auth";
62928
+ }
62929
+ return `agents.list[${scope.agentId}].auth`;
62930
+ }
62931
+ function ensureEditableRoleDefinition(role) {
62932
+ role.allow = role.allow ?? [];
62933
+ role.users = role.users ?? [];
62934
+ return role;
62935
+ }
62936
+ function resolveAppRole(config, roleName) {
62937
+ const role = config.app.auth.roles[roleName];
62938
+ if (!role) {
62939
+ throw new Error(`Unknown app role: ${roleName}`);
62940
+ }
62941
+ return ensureEditableRoleDefinition(role);
62942
+ }
62943
+ function resolveAgentDefaultsRole(config, roleName) {
62944
+ const role = config.agents.defaults.auth.roles[roleName];
62945
+ if (!role) {
62946
+ throw new Error(`Unknown agent-defaults role: ${roleName}`);
62947
+ }
62948
+ return ensureEditableRoleDefinition(role);
62949
+ }
62950
+ function ensureAgentOverrideAuth(entry, config) {
62951
+ if (!entry.auth) {
62952
+ entry.auth = {
62953
+ defaultRole: config.agents.defaults.auth.defaultRole,
62954
+ roles: {}
62955
+ };
62956
+ }
62957
+ return entry.auth;
62958
+ }
62959
+ function resolveAgentRoleForEdit(config, agentId, roleName) {
62960
+ const entry = ensureAgentEntry(config, agentId);
62961
+ const explicitRole = entry.auth?.roles?.[roleName];
62962
+ if (explicitRole) {
62963
+ return ensureEditableRoleDefinition(explicitRole);
62964
+ }
62965
+ const inheritedRole = config.agents.defaults.auth.roles[roleName];
62966
+ if (!inheritedRole) {
62967
+ throw new Error(`Unknown agent role: ${roleName}`);
62968
+ }
62969
+ const auth = ensureAgentOverrideAuth(entry, config);
62970
+ auth.roles[roleName] = cloneRoleDefinition(inheritedRole);
62971
+ return ensureEditableRoleDefinition(auth.roles[roleName]);
62972
+ }
62973
+ function resolveRoleForEdit(config, scope, roleName) {
62974
+ if (scope.kind === "app") {
62975
+ return resolveAppRole(config, roleName);
62976
+ }
62977
+ if (scope.kind === "agent-defaults") {
62978
+ return resolveAgentDefaultsRole(config, roleName);
62979
+ }
62980
+ return resolveAgentRoleForEdit(config, scope.agentId, roleName);
62981
+ }
62982
+ function validatePermission(scope, permission) {
62983
+ const trimmed = permission.trim();
62984
+ if (!trimmed) {
62985
+ throw new Error("Missing value for --permission");
62986
+ }
62987
+ const allowedPermissions = new Set(scope.kind === "app" ? APP_ADMIN_PERMISSIONS : DEFAULT_AGENT_ADMIN_PERMISSIONS);
62988
+ if (!allowedPermissions.has(trimmed)) {
62989
+ const sorted = [...allowedPermissions].sort().join(", ");
62990
+ throw new Error(`Unknown permission for ${scope.kind}: ${trimmed}. Allowed: ${sorted}`);
62991
+ }
62992
+ return trimmed;
62993
+ }
62994
+ function buildShowPayload(config, scope) {
62995
+ if (scope.kind === "app") {
62996
+ return config.app.auth;
62997
+ }
62998
+ if (scope.kind === "agent-defaults") {
62999
+ return config.agents.defaults.auth;
63000
+ }
63001
+ const entry = ensureAgentEntry(config, scope.agentId);
63002
+ return {
63003
+ defaultRole: entry.auth?.defaultRole ?? config.agents.defaults.auth.defaultRole,
63004
+ roles: mergeRoleRecord(config.agents.defaults.auth.roles, entry.auth?.roles)
63005
+ };
63006
+ }
63007
+ async function listAuth(args) {
63008
+ const { config } = await readEditableConfig(getEditableConfigPath3());
63009
+ const payload = {
63010
+ app: config.app.auth,
63011
+ agentDefaults: config.agents.defaults.auth,
63012
+ agents: config.agents.list.map((entry) => ({
63013
+ agentId: entry.id,
63014
+ auth: buildShowPayload(config, { kind: "agent", agentId: entry.id })
63015
+ }))
63016
+ };
63017
+ if (hasFlag3(args, "--json")) {
63018
+ console.log(JSON.stringify(payload, null, 2));
63019
+ return;
63020
+ }
63021
+ console.log(JSON.stringify(payload, null, 2));
63022
+ }
63023
+ async function showAuth(args) {
63024
+ const scope = parseScope(args[0], args.slice(1));
63025
+ const { config } = await readEditableConfig(getEditableConfigPath3());
63026
+ const payload = buildShowPayload(config, scope);
63027
+ if (hasFlag3(args, "--json")) {
63028
+ console.log(JSON.stringify(payload, null, 2));
63029
+ return;
63030
+ }
63031
+ console.log(JSON.stringify(payload, null, 2));
63032
+ }
63033
+ async function mutateUsers(mode, args) {
63034
+ const scope = parseScope(args[0], args.slice(1));
63035
+ const roleName = parseSingleOption2(args, "--role");
63036
+ const principal = parseSingleOption2(args, "--user")?.trim();
63037
+ if (!roleName) {
63038
+ throw new Error("Missing value for --role");
63039
+ }
63040
+ if (!principal) {
63041
+ throw new Error("Missing value for --user");
63042
+ }
63043
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63044
+ const role = resolveRoleForEdit(config, scope, roleName);
63045
+ role.users = normalizeUnique(mode === "add" ? [...role.users, principal] : role.users.filter((value) => value !== principal));
63046
+ await writeEditableConfig(configPath, config);
63047
+ console.log(`${mode === "add" ? "Added" : "Removed"} user ${principal} ${mode === "add" ? "to" : "from"} ${getAuthLabel(scope)} role ${roleName}.`);
63048
+ }
63049
+ async function mutatePermissions(mode, args) {
63050
+ const scope = parseScope(args[0], args.slice(1));
63051
+ const roleName = parseSingleOption2(args, "--role");
63052
+ const permission = validatePermission(scope, parseSingleOption2(args, "--permission") ?? "");
63053
+ if (!roleName) {
63054
+ throw new Error("Missing value for --role");
63055
+ }
63056
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63057
+ const role = resolveRoleForEdit(config, scope, roleName);
63058
+ role.allow = normalizeUnique(mode === "add" ? [...role.allow, permission] : role.allow.filter((value) => value !== permission));
63059
+ await writeEditableConfig(configPath, config);
63060
+ console.log(`${mode === "add" ? "Added" : "Removed"} permission ${permission} ${mode === "add" ? "to" : "from"} ${getAuthLabel(scope)} role ${roleName}.`);
63061
+ }
63062
+ async function runAuthCli(args) {
63063
+ const [command, ...rest] = args;
63064
+ if (!command || command === "--help" || command === "-h" || command === "help") {
63065
+ console.log(renderAuthCliHelp());
63066
+ return;
63067
+ }
63068
+ if (command === "list") {
63069
+ await listAuth(rest);
63070
+ return;
63071
+ }
63072
+ if (command === "show") {
63073
+ await showAuth(rest);
63074
+ return;
63075
+ }
63076
+ if (command === "add-user") {
63077
+ await mutateUsers("add", rest);
63078
+ return;
63079
+ }
63080
+ if (command === "remove-user") {
63081
+ await mutateUsers("remove", rest);
63082
+ return;
63083
+ }
63084
+ if (command === "add-permission") {
63085
+ await mutatePermissions("add", rest);
63086
+ return;
63087
+ }
63088
+ if (command === "remove-permission") {
63089
+ await mutatePermissions("remove", rest);
63090
+ return;
63091
+ }
63092
+ throw new Error(renderAuthCliHelp());
62733
63093
  }
62734
63094
 
62735
63095
  // src/channels/mode-config-shared.ts
63096
+ function createTelegramGroupRoute() {
63097
+ return {
63098
+ requireMention: true,
63099
+ allowBots: false,
63100
+ topics: {}
63101
+ };
63102
+ }
63103
+ function ensureTelegramGroupRoute(config, chatId) {
63104
+ const existing = config.channels.telegram.groups[chatId];
63105
+ if (existing) {
63106
+ return existing;
63107
+ }
63108
+ const created = createTelegramGroupRoute();
63109
+ config.channels.telegram.groups[chatId] = created;
63110
+ return created;
63111
+ }
63112
+ function ensureTelegramTopicRoute(config, chatId, topicId) {
63113
+ const group = ensureTelegramGroupRoute(config, chatId);
63114
+ const existing = group.topics[topicId];
63115
+ if (existing) {
63116
+ return existing;
63117
+ }
63118
+ const created = {};
63119
+ group.topics[topicId] = created;
63120
+ return created;
63121
+ }
62736
63122
  function getModeValue(source, field) {
62737
63123
  return source[field];
62738
63124
  }
62739
63125
  function setModeValue(source, field, value) {
62740
63126
  source[field] = value;
62741
63127
  }
63128
+ var EMPTY_MODE_SOURCE = {};
62742
63129
  function resolveSlackConfigTarget(config, field, params) {
62743
63130
  if (!params.target) {
62744
63131
  return {
@@ -62820,22 +63207,25 @@ function resolveTelegramConfigTarget(config, field, params) {
62820
63207
  };
62821
63208
  }
62822
63209
  const group = config.channels.telegram.groups[chatId];
62823
- if (!group) {
62824
- throw new Error(renderTelegramRouteChoiceMessage({ chatId }));
62825
- }
62826
63210
  if (topicId) {
62827
- const topic = group.topics?.[topicId];
62828
- if (!topic) {
62829
- throw new Error(renderTelegramRouteChoiceMessage({ chatId, topicId }));
62830
- }
63211
+ const topic = group?.topics?.[topicId];
62831
63212
  return {
62832
- get: () => getModeValue(topic, field) ?? getModeValue(group, field) ?? getModeValue(config.channels.telegram, field),
63213
+ get: () => getModeValue(topic ?? EMPTY_MODE_SOURCE, field) ?? getModeValue(group ?? EMPTY_MODE_SOURCE, field) ?? getModeValue(config.channels.telegram, field),
62833
63214
  set: (value) => {
62834
- setModeValue(topic, field, value);
63215
+ setModeValue(ensureTelegramTopicRoute(config, chatId, topicId), field, value);
62835
63216
  },
62836
63217
  label: `telegram topic ${chatId}/${topicId}`
62837
63218
  };
62838
63219
  }
63220
+ if (!group) {
63221
+ return {
63222
+ get: () => getModeValue(config.channels.telegram, field),
63223
+ set: (value) => {
63224
+ setModeValue(ensureTelegramGroupRoute(config, chatId), field, value);
63225
+ },
63226
+ label: `telegram group ${chatId}`
63227
+ };
63228
+ }
62839
63229
  return {
62840
63230
  get: () => getModeValue(group, field) ?? getModeValue(config.channels.telegram, field),
62841
63231
  set: (value) => {
@@ -62876,11 +63266,11 @@ function renderFieldLabel(field) {
62876
63266
  }
62877
63267
 
62878
63268
  // src/channels/additional-message-mode-config.ts
62879
- function getEditableConfigPath3() {
63269
+ function getEditableConfigPath4() {
62880
63270
  return process.env.CLISBOT_CONFIG_PATH;
62881
63271
  }
62882
63272
  async function getConversationAdditionalMessageMode(params) {
62883
- const { config } = await readEditableConfig(getEditableConfigPath3());
63273
+ const { config } = await readEditableConfig(getEditableConfigPath4());
62884
63274
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", buildConfiguredTargetFromIdentity(params.identity));
62885
63275
  return {
62886
63276
  label: target.label,
@@ -62888,7 +63278,7 @@ async function getConversationAdditionalMessageMode(params) {
62888
63278
  };
62889
63279
  }
62890
63280
  async function setConversationAdditionalMessageMode(params) {
62891
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63281
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62892
63282
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", buildConfiguredTargetFromIdentity(params.identity));
62893
63283
  target.set(params.additionalMessageMode);
62894
63284
  await writeEditableConfig(configPath, config);
@@ -62899,7 +63289,7 @@ async function setConversationAdditionalMessageMode(params) {
62899
63289
  };
62900
63290
  }
62901
63291
  async function getConfiguredAdditionalMessageMode(params) {
62902
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63292
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62903
63293
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", params);
62904
63294
  return {
62905
63295
  configPath,
@@ -62908,7 +63298,7 @@ async function getConfiguredAdditionalMessageMode(params) {
62908
63298
  };
62909
63299
  }
62910
63300
  async function setConfiguredAdditionalMessageMode(params) {
62911
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63301
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62912
63302
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", params);
62913
63303
  target.set(params.additionalMessageMode);
62914
63304
  await writeEditableConfig(configPath, config);
@@ -62920,11 +63310,11 @@ async function setConfiguredAdditionalMessageMode(params) {
62920
63310
  }
62921
63311
 
62922
63312
  // src/channels/response-mode-config.ts
62923
- function getEditableConfigPath4() {
63313
+ function getEditableConfigPath5() {
62924
63314
  return process.env.CLISBOT_CONFIG_PATH;
62925
63315
  }
62926
63316
  async function getConversationResponseMode(params) {
62927
- const { config } = await readEditableConfig(getEditableConfigPath4());
63317
+ const { config } = await readEditableConfig(getEditableConfigPath5());
62928
63318
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", buildConfiguredTargetFromIdentity(params.identity));
62929
63319
  return {
62930
63320
  label: target.label,
@@ -62932,7 +63322,7 @@ async function getConversationResponseMode(params) {
62932
63322
  };
62933
63323
  }
62934
63324
  async function setConversationResponseMode(params) {
62935
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63325
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62936
63326
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", buildConfiguredTargetFromIdentity(params.identity));
62937
63327
  target.set(params.responseMode);
62938
63328
  await writeEditableConfig(configPath, config);
@@ -62943,7 +63333,7 @@ async function setConversationResponseMode(params) {
62943
63333
  };
62944
63334
  }
62945
63335
  async function getConfiguredResponseMode(params) {
62946
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63336
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62947
63337
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", params);
62948
63338
  return {
62949
63339
  configPath,
@@ -62952,7 +63342,7 @@ async function getConfiguredResponseMode(params) {
62952
63342
  };
62953
63343
  }
62954
63344
  async function setConfiguredResponseMode(params) {
62955
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63345
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62956
63346
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", params);
62957
63347
  target.set(params.responseMode);
62958
63348
  await writeEditableConfig(configPath, config);
@@ -62978,7 +63368,7 @@ async function runChannelPrivilegeCli(_args) {
62978
63368
 
62979
63369
  // src/control/channels-cli.ts
62980
63370
  var AUTH_USER_GUIDE_DOC_PATH = "docs/user-guide/auth-and-roles.md";
62981
- function getEditableConfigPath5() {
63371
+ function getEditableConfigPath6() {
62982
63372
  return process.env.CLISBOT_CONFIG_PATH;
62983
63373
  }
62984
63374
  function renderChannelsHelp() {
@@ -63127,7 +63517,7 @@ function getAgentId(args) {
63127
63517
  return parseOptionValue2(args, "--agent") ?? "default";
63128
63518
  }
63129
63519
  async function setChannelEnabled(action, channel) {
63130
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63520
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63131
63521
  const enabled = action === "enable";
63132
63522
  const current = config.channels[channel].enabled;
63133
63523
  if (current === enabled) {
@@ -63147,7 +63537,7 @@ async function addTelegramGroup(args) {
63147
63537
  if (!chatId) {
63148
63538
  throw new Error("Usage: clisbot channels add telegram-group <chatId> [--topic <topicId>] [--agent <id>] [--require-mention true|false]");
63149
63539
  }
63150
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63540
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63151
63541
  const topicId = parseOptionValue2(args, "--topic");
63152
63542
  const agentId = getAgentId(args);
63153
63543
  const requireMention = parseBooleanOption(args, "--require-mention", true);
@@ -63200,7 +63590,7 @@ async function removeTelegramGroup(args) {
63200
63590
  if (!chatId) {
63201
63591
  throw new Error("Usage: clisbot channels remove telegram-group <chatId> [--topic <topicId>]");
63202
63592
  }
63203
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63593
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63204
63594
  const topicId = parseOptionValue2(args, "--topic");
63205
63595
  const groupRoute = config.channels.telegram.groups[chatId];
63206
63596
  if (!groupRoute) {
@@ -63230,7 +63620,7 @@ async function addSlackRoute(kind, args) {
63230
63620
  if (!routeId) {
63231
63621
  throw new Error(`Usage: clisbot channels add slack-${kind} <${kind}Id> [--agent <id>] [--require-mention true|false]`);
63232
63622
  }
63233
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63623
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63234
63624
  const agentId = getAgentId(args);
63235
63625
  const requireMention = parseBooleanOption(args, "--require-mention", false);
63236
63626
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
@@ -63254,7 +63644,7 @@ async function removeSlackRoute(kind, args) {
63254
63644
  if (!routeId) {
63255
63645
  throw new Error(`Usage: clisbot channels remove slack-${kind} <${kind}Id>`);
63256
63646
  }
63257
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63647
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63258
63648
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
63259
63649
  if (!target[routeId]) {
63260
63650
  console.log(`slack ${kind} route ${routeId} is not configured`);
@@ -63267,7 +63657,7 @@ async function removeSlackRoute(kind, args) {
63267
63657
  console.log(`config: ${configPath}`);
63268
63658
  }
63269
63659
  async function setToken(target, value) {
63270
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63660
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63271
63661
  if (target === "slack-app") {
63272
63662
  config.channels.slack.appToken = value;
63273
63663
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63299,7 +63689,7 @@ async function setToken(target, value) {
63299
63689
  console.log(`config: ${configPath}`);
63300
63690
  }
63301
63691
  async function clearToken(target) {
63302
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63692
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63303
63693
  if (target === "slack-app") {
63304
63694
  config.channels.slack.appToken = "";
63305
63695
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -64038,7 +64428,7 @@ class SessionStore {
64038
64428
  }
64039
64429
 
64040
64430
  // src/control/loops-cli.ts
64041
- function getEditableConfigPath6() {
64431
+ function getEditableConfigPath7() {
64042
64432
  return process.env.CLISBOT_CONFIG_PATH;
64043
64433
  }
64044
64434
  function renderLoopsHelp() {
@@ -64096,7 +64486,7 @@ function getSessionState(sessionStorePath) {
64096
64486
  return new AgentSessionState(new SessionStore(sessionStorePath));
64097
64487
  }
64098
64488
  async function loadLoopControlState() {
64099
- const configPath = await ensureEditableConfigFile(getEditableConfigPath6());
64489
+ const configPath = await ensureEditableConfigFile(getEditableConfigPath7());
64100
64490
  const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
64101
64491
  const sessionStorePath = resolveSessionStorePath(loadedConfig);
64102
64492
  return {
@@ -64272,6 +64662,9 @@ function buildReplyCommand(params) {
64272
64662
  const lines = [`${params.command} message send \\`];
64273
64663
  if (params.identity.platform === "slack") {
64274
64664
  lines.push(" --channel slack \\");
64665
+ if (params.identity.accountId) {
64666
+ lines.push(` --account ${params.identity.accountId} \\`);
64667
+ }
64275
64668
  lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
64276
64669
  if (params.identity.threadTs) {
64277
64670
  lines.push(` --thread-id ${params.identity.threadTs} \\`);
@@ -64286,6 +64679,9 @@ function buildReplyCommand(params) {
64286
64679
  `);
64287
64680
  }
64288
64681
  lines.push(" --channel telegram \\");
64682
+ if (params.identity.accountId) {
64683
+ lines.push(` --account ${params.identity.accountId} \\`);
64684
+ }
64289
64685
  lines.push(` --target ${params.identity.chatId ?? ""} \\`);
64290
64686
  if (params.identity.topicId) {
64291
64687
  lines.push(` --thread-id ${params.identity.topicId} \\`);
@@ -64300,6 +64696,57 @@ function buildReplyCommand(params) {
64300
64696
  `);
64301
64697
  }
64302
64698
 
64699
+ // src/channels/surface-notifications.ts
64700
+ function sanitizeInlineCode(text) {
64701
+ return text.replaceAll("`", "'");
64702
+ }
64703
+ function summarizeSurfaceNotificationText(text, maxLength = 60) {
64704
+ const singleLine = sanitizeInlineCode(text.replace(/\s+/g, " ").trim());
64705
+ if (!singleLine) {
64706
+ return "(empty)";
64707
+ }
64708
+ if (singleLine.length <= maxLength) {
64709
+ return singleLine;
64710
+ }
64711
+ return `${singleLine.slice(0, maxLength - 3)}...`;
64712
+ }
64713
+ function renderQueueStartNotification(params) {
64714
+ if (params.mode === "none") {
64715
+ return;
64716
+ }
64717
+ const summary = summarizeSurfaceNotificationText(params.promptSummary);
64718
+ if (params.mode === "full") {
64719
+ return `Queued message is now running for agent \`${params.agentId}\`: \`${summary}\`.`;
64720
+ }
64721
+ return `Queued message is now running: \`${summary}\`.`;
64722
+ }
64723
+ function renderLoopScheduleSegment(params) {
64724
+ if (params.kind === "calendar") {
64725
+ return formatCalendarLoopSchedule({
64726
+ cadence: params.cadence,
64727
+ dayOfWeek: params.dayOfWeek,
64728
+ localTime: params.localTime
64729
+ });
64730
+ }
64731
+ return `every ${formatLoopIntervalShort(params.intervalMs)}`;
64732
+ }
64733
+ function renderLoopStartNotification(params) {
64734
+ if (params.mode === "none") {
64735
+ return;
64736
+ }
64737
+ const summary = summarizeSurfaceNotificationText(params.promptSummary);
64738
+ const segments = [
64739
+ `\`${summary}\``,
64740
+ renderLoopScheduleSegment(params),
64741
+ `next run \`${new Date(params.nextRunAt).toISOString()}\``,
64742
+ `remaining \`${params.remainingRuns}/${params.maxRuns}\``
64743
+ ];
64744
+ if (params.mode === "full") {
64745
+ return `Loop \`${params.loopId}\` is now running for agent \`${params.agentId}\`: ${segments.join(" · ")}.`;
64746
+ }
64747
+ return `Loop \`${params.loopId}\` is now running: ${segments.join(" · ")}.`;
64748
+ }
64749
+
64303
64750
  // src/agents/session-key.ts
64304
64751
  var DEFAULT_MAIN_KEY = "main";
64305
64752
  var DEFAULT_ACCOUNT_ID = "default";
@@ -66583,6 +67030,7 @@ class AgentService {
66583
67030
  cleanupTimer;
66584
67031
  loopTimers = new Set;
66585
67032
  intervalLoops = new Map;
67033
+ surfaceNotificationHandlers = new Map;
66586
67034
  constructor(loadedConfig, deps = {}) {
66587
67035
  this.loadedConfig = loadedConfig;
66588
67036
  this.tmuxClient = deps.tmux ?? new TmuxClient(this.loadedConfig.raw.tmux.socketPath);
@@ -66611,7 +67059,9 @@ class AgentService {
66611
67059
  }
66612
67060
  await this.runnerSessions.runSessionCleanup();
66613
67061
  this.cleanupTimer = setInterval(() => {
66614
- this.runnerSessions.runSessionCleanup();
67062
+ this.runnerSessions.runSessionCleanup().catch((error) => {
67063
+ console.error("session cleanup failed", error);
67064
+ });
66615
67065
  }, cleanup.intervalMinutes * 60000);
66616
67066
  }
66617
67067
  async stop() {
@@ -66635,6 +67085,12 @@ class AgentService {
66635
67085
  async cleanupStaleSessions() {
66636
67086
  await this.runnerSessions.runSessionCleanup();
66637
67087
  }
67088
+ registerSurfaceNotificationHandler(params) {
67089
+ this.surfaceNotificationHandlers.set(this.getSurfaceNotificationHandlerKey(params.platform, params.accountId), params.handler);
67090
+ }
67091
+ unregisterSurfaceNotificationHandler(params) {
67092
+ this.surfaceNotificationHandlers.delete(this.getSurfaceNotificationHandlerKey(params.platform, params.accountId));
67093
+ }
66638
67094
  resolveTarget(target) {
66639
67095
  return resolveAgentTarget(this.loadedConfig, target);
66640
67096
  }
@@ -66743,7 +67199,9 @@ class AgentService {
66743
67199
  target: params.target,
66744
67200
  loop
66745
67201
  });
66746
- await this.runIntervalLoopIteration(loop.id);
67202
+ await this.runIntervalLoopIteration(loop.id, {
67203
+ notifyStart: false
67204
+ });
66747
67205
  return this.getIntervalLoop(loop.id);
66748
67206
  }
66749
67207
  async createCalendarLoop(params) {
@@ -66905,7 +67363,7 @@ class AgentService {
66905
67363
  }
66906
67364
  this.intervalLoops.delete(loopId);
66907
67365
  }
66908
- async runIntervalLoopIteration(loopId) {
67366
+ async runIntervalLoopIteration(loopId, options = {}) {
66909
67367
  const managed = this.intervalLoops.get(loopId);
66910
67368
  if (!managed) {
66911
67369
  return;
@@ -66941,6 +67399,9 @@ class AgentService {
66941
67399
  this.dropManagedIntervalLoop(loopId);
66942
67400
  return;
66943
67401
  }
67402
+ if (options.notifyStart !== false) {
67403
+ await this.notifyManagedLoopStart(managed.target, nextLoopState);
67404
+ }
66944
67405
  const promptText = this.buildManagedLoopPrompt(managed.target.agentId, nextLoopState);
66945
67406
  const { result } = this.enqueuePrompt(managed.target, promptText, {
66946
67407
  observerId: `loop:${loopId}:${attemptedRuns}`,
@@ -66960,6 +67421,101 @@ class AgentService {
66960
67421
  }
66961
67422
  this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
66962
67423
  }
67424
+ getSurfaceNotificationHandlerKey(platform, accountId) {
67425
+ return `${platform}:${accountId?.trim() || "default"}`;
67426
+ }
67427
+ async notifySurface(request) {
67428
+ const handler = this.surfaceNotificationHandlers.get(this.getSurfaceNotificationHandlerKey(request.binding.platform, request.binding.accountId));
67429
+ if (!handler) {
67430
+ return;
67431
+ }
67432
+ await handler(request);
67433
+ }
67434
+ resolveLoopSurfaceNotifications(identity) {
67435
+ if (identity.platform === "slack") {
67436
+ const channelConfig2 = this.loadedConfig.raw.channels.slack;
67437
+ let resolved2 = {
67438
+ queueStart: channelConfig2.surfaceNotifications?.queueStart ?? "brief",
67439
+ loopStart: channelConfig2.surfaceNotifications?.loopStart ?? "brief"
67440
+ };
67441
+ if (identity.conversationKind === "dm") {
67442
+ return {
67443
+ ...resolved2,
67444
+ ...channelConfig2.directMessages.surfaceNotifications ?? {}
67445
+ };
67446
+ }
67447
+ const routeCollection = identity.conversationKind === "group" ? channelConfig2.groups : channelConfig2.channels;
67448
+ const route = identity.channelId ? routeCollection[identity.channelId] ?? routeCollection["*"] : undefined;
67449
+ return {
67450
+ ...resolved2,
67451
+ ...route?.surfaceNotifications ?? {}
67452
+ };
67453
+ }
67454
+ const channelConfig = this.loadedConfig.raw.channels.telegram;
67455
+ let resolved = {
67456
+ queueStart: channelConfig.surfaceNotifications?.queueStart ?? "brief",
67457
+ loopStart: channelConfig.surfaceNotifications?.loopStart ?? "brief"
67458
+ };
67459
+ if (identity.conversationKind === "dm") {
67460
+ return {
67461
+ ...resolved,
67462
+ ...channelConfig.directMessages.surfaceNotifications ?? {}
67463
+ };
67464
+ }
67465
+ const groupRoute = identity.chatId ? channelConfig.groups[identity.chatId] ?? channelConfig.groups["*"] : undefined;
67466
+ resolved = {
67467
+ ...resolved,
67468
+ ...groupRoute?.surfaceNotifications ?? {}
67469
+ };
67470
+ if (identity.conversationKind === "topic" && identity.topicId) {
67471
+ return {
67472
+ ...resolved,
67473
+ ...groupRoute?.topics?.[identity.topicId]?.surfaceNotifications ?? {}
67474
+ };
67475
+ }
67476
+ return resolved;
67477
+ }
67478
+ async notifyManagedLoopStart(target, loop) {
67479
+ if (!loop.surfaceBinding) {
67480
+ return;
67481
+ }
67482
+ const identity = this.buildLoopChannelIdentity(loop.surfaceBinding);
67483
+ const notifications = this.resolveLoopSurfaceNotifications(identity);
67484
+ const text = loop.kind === "calendar" ? renderLoopStartNotification({
67485
+ mode: notifications.loopStart,
67486
+ agentId: target.agentId,
67487
+ loopId: loop.id,
67488
+ promptSummary: loop.promptSummary,
67489
+ cadence: loop.cadence,
67490
+ dayOfWeek: loop.dayOfWeek,
67491
+ localTime: loop.localTime,
67492
+ timezone: loop.timezone,
67493
+ nextRunAt: loop.nextRunAt,
67494
+ remainingRuns: Math.max(0, loop.maxRuns - loop.attemptedRuns),
67495
+ maxRuns: loop.maxRuns,
67496
+ kind: "calendar"
67497
+ }) : renderLoopStartNotification({
67498
+ mode: notifications.loopStart,
67499
+ agentId: target.agentId,
67500
+ loopId: loop.id,
67501
+ promptSummary: loop.promptSummary,
67502
+ intervalMs: loop.intervalMs,
67503
+ nextRunAt: loop.nextRunAt,
67504
+ remainingRuns: Math.max(0, loop.maxRuns - loop.attemptedRuns),
67505
+ maxRuns: loop.maxRuns
67506
+ });
67507
+ if (!text) {
67508
+ return;
67509
+ }
67510
+ try {
67511
+ await this.notifySurface({
67512
+ binding: loop.surfaceBinding,
67513
+ text
67514
+ });
67515
+ } catch (error) {
67516
+ console.error("loop start notification failed", error);
67517
+ }
67518
+ }
66963
67519
  scheduleIntervalLoopTimer(loopId, delayMs) {
66964
67520
  const managed = this.intervalLoops.get(loopId);
66965
67521
  if (!managed) {
@@ -66976,7 +67532,9 @@ class AgentService {
66976
67532
  return;
66977
67533
  }
66978
67534
  current.timer = undefined;
66979
- this.runIntervalLoopIteration(loopId).catch((error) => {
67535
+ this.runIntervalLoopIteration(loopId, {
67536
+ notifyStart: true
67537
+ }).catch((error) => {
66980
67538
  if (this.shouldSuppressLoopShutdownError(error)) {
66981
67539
  return;
66982
67540
  }
@@ -67034,6 +67592,7 @@ class AgentService {
67034
67592
  buildLoopChannelIdentity(binding) {
67035
67593
  return {
67036
67594
  platform: binding.platform,
67595
+ accountId: binding.accountId,
67037
67596
  conversationKind: binding.conversationKind,
67038
67597
  channelId: binding.channelId,
67039
67598
  chatId: binding.chatId,
@@ -67649,11 +68208,11 @@ function formatChannelFollowUpStatus(params) {
67649
68208
  }
67650
68209
 
67651
68210
  // src/channels/streaming-config.ts
67652
- function getEditableConfigPath7() {
68211
+ function getEditableConfigPath8() {
67653
68212
  return process.env.CLISBOT_CONFIG_PATH;
67654
68213
  }
67655
68214
  async function getConversationStreaming(params) {
67656
- const { config } = await readEditableConfig(getEditableConfigPath7());
68215
+ const { config } = await readEditableConfig(getEditableConfigPath8());
67657
68216
  const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67658
68217
  return {
67659
68218
  label: target.label,
@@ -67661,7 +68220,7 @@ async function getConversationStreaming(params) {
67661
68220
  };
67662
68221
  }
67663
68222
  async function setConversationStreaming(params) {
67664
- const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
68223
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
67665
68224
  const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67666
68225
  target.set(params.streaming);
67667
68226
  await writeEditableConfig(configPath, config);
@@ -67742,7 +68301,7 @@ function renderRouteStatusMessage(params) {
67742
68301
  if (params.identity.topicId) {
67743
68302
  lines.push(`topicId: \`${params.identity.topicId}\``);
67744
68303
  }
67745
- 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}\``);
68304
+ lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `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}\``);
67746
68305
  if (params.runtimeState.startedAt) {
67747
68306
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
67748
68307
  }
@@ -67979,6 +68538,7 @@ async function resolveLoopPromptText(params) {
67979
68538
  function buildLoopSurfaceBinding(identity) {
67980
68539
  return {
67981
68540
  platform: identity.platform,
68541
+ accountId: identity.accountId,
67982
68542
  conversationKind: identity.conversationKind,
67983
68543
  channelId: identity.channelId,
67984
68544
  chatId: identity.chatId,
@@ -67995,6 +68555,8 @@ async function executePromptDelivery(params) {
67995
68555
  let loggedFirstRunningUpdate = false;
67996
68556
  let activePreviewStartedAt;
67997
68557
  let messageToolPreviewHandedOff = false;
68558
+ let queueStartPending = false;
68559
+ let deferredQueueStartPreview = false;
67998
68560
  const paneManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67999
68561
  const messageToolPreview = params.route.responseMode === "message-tool" && params.forceQueuedDelivery !== true && params.route.streaming !== "off";
68000
68562
  const previewEnabled = params.route.streaming !== "off" && (paneManagedDelivery || messageToolPreview);
@@ -68069,6 +68631,68 @@ async function executePromptDelivery(params) {
68069
68631
  agentId: params.route.agentId,
68070
68632
  sessionKey: params.sessionTarget.sessionKey
68071
68633
  });
68634
+ async function maybeRenderQueueStartNotification() {
68635
+ if (!queueStartPending) {
68636
+ return false;
68637
+ }
68638
+ const text = renderQueueStartNotification({
68639
+ mode: params.queueStartMode ?? "none",
68640
+ agentId: params.route.agentId,
68641
+ promptSummary: params.notificationPromptSummary ?? summarizeSurfaceNotificationText(params.promptText)
68642
+ });
68643
+ if (!text) {
68644
+ queueStartPending = false;
68645
+ return false;
68646
+ }
68647
+ if (previewEnabled && responseChunks.length === 0) {
68648
+ deferredQueueStartPreview = true;
68649
+ return true;
68650
+ }
68651
+ queueStartPending = false;
68652
+ if (responseChunks.length > 0) {
68653
+ const postedNew = await renderResponseText(text);
68654
+ if (postedNew) {
68655
+ await recordVisibleReply("reply", "channel");
68656
+ activePreviewStartedAt = Date.now();
68657
+ }
68658
+ renderedState = {
68659
+ text,
68660
+ body: ""
68661
+ };
68662
+ return true;
68663
+ }
68664
+ const posted = await params.postText(text);
68665
+ if (posted.length > 0) {
68666
+ await recordVisibleReply("reply", "channel");
68667
+ }
68668
+ return posted.length > 0;
68669
+ }
68670
+ function buildInitialPlaceholderText(positionAhead) {
68671
+ if (deferredQueueStartPreview && queueStartPending) {
68672
+ deferredQueueStartPreview = false;
68673
+ queueStartPending = false;
68674
+ return renderQueueStartNotification({
68675
+ mode: params.queueStartMode ?? "none",
68676
+ agentId: params.route.agentId,
68677
+ promptSummary: params.notificationPromptSummary ?? summarizeSurfaceNotificationText(params.promptText)
68678
+ }) ?? renderPlatformInteraction({
68679
+ platform: params.identity.platform,
68680
+ status: positionAhead > 0 ? "queued" : "running",
68681
+ content: "",
68682
+ queuePosition: positionAhead,
68683
+ maxChars: Number.POSITIVE_INFINITY,
68684
+ note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
68685
+ });
68686
+ }
68687
+ return renderPlatformInteraction({
68688
+ platform: params.identity.platform,
68689
+ status: positionAhead > 0 ? "queued" : "running",
68690
+ content: "",
68691
+ queuePosition: positionAhead,
68692
+ maxChars: Number.POSITIVE_INFINITY,
68693
+ note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
68694
+ });
68695
+ }
68072
68696
  try {
68073
68697
  const { positionAhead, result } = params.agentService.enqueuePrompt(params.sessionTarget, params.promptText, {
68074
68698
  observerId: params.observerId,
@@ -68077,9 +68701,6 @@ async function executePromptDelivery(params) {
68077
68701
  if (!paneManagedDelivery && !messageToolPreview) {
68078
68702
  return;
68079
68703
  }
68080
- if (params.route.streaming === "off" && update.status === "running") {
68081
- return;
68082
- }
68083
68704
  if (update.status === "running" && !loggedFirstRunningUpdate) {
68084
68705
  loggedFirstRunningUpdate = true;
68085
68706
  logLatencyDebug("channel-first-running-update", params.timingContext, {
@@ -68088,14 +68709,21 @@ async function executePromptDelivery(params) {
68088
68709
  });
68089
68710
  }
68090
68711
  await (renderChain = renderChain.then(async () => {
68712
+ let renderedQueueStart = false;
68713
+ if (update.status === "running") {
68714
+ renderedQueueStart = await maybeRenderQueueStartNotification();
68715
+ }
68716
+ if (params.route.streaming === "off" && update.status === "running") {
68717
+ return;
68718
+ }
68719
+ if (renderedQueueStart) {
68720
+ return;
68721
+ }
68091
68722
  const signals = await getMessageToolRuntimeSignals();
68092
68723
  if (messageToolPreview && typeof activePreviewStartedAt === "number" && (typeof signals.messageToolFinalReplyAt === "number" && signals.messageToolFinalReplyAt >= activePreviewStartedAt || typeof signals.lastMessageToolReplyAt === "number" && signals.lastMessageToolReplyAt >= activePreviewStartedAt)) {
68093
68724
  await handoffMessageToolPreview();
68094
68725
  return;
68095
68726
  }
68096
- if (messageToolPreview) {
68097
- return;
68098
- }
68099
68727
  const nextState2 = buildRenderedMessageState({
68100
68728
  platform: params.identity.platform,
68101
68729
  status: update.status,
@@ -68119,15 +68747,9 @@ async function executePromptDelivery(params) {
68119
68747
  }));
68120
68748
  }
68121
68749
  });
68750
+ queueStartPending = positionAhead > 0 && (params.queueStartMode ?? "none") !== "none";
68122
68751
  if (previewEnabled) {
68123
- const placeholderText = renderPlatformInteraction({
68124
- platform: params.identity.platform,
68125
- status: positionAhead > 0 ? "queued" : "running",
68126
- content: "",
68127
- queuePosition: positionAhead,
68128
- maxChars: Number.POSITIVE_INFINITY,
68129
- note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
68130
- });
68752
+ const placeholderText = buildInitialPlaceholderText(positionAhead);
68131
68753
  const postedNew2 = await renderResponseText(placeholderText);
68132
68754
  if (postedNew2) {
68133
68755
  await recordVisibleReply("reply", "channel");
@@ -68157,6 +68779,7 @@ async function executePromptDelivery(params) {
68157
68779
  }
68158
68780
  const finalResult = await result;
68159
68781
  await renderChain;
68782
+ await maybeRenderQueueStartNotification();
68160
68783
  if (!paneManagedDelivery && messageToolPreviewHandedOff) {
68161
68784
  return;
68162
68785
  }
@@ -68641,6 +69264,8 @@ ${renderLoopUsage()}`);
68641
69264
  route: params.route,
68642
69265
  maxChars: params.maxChars,
68643
69266
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
69267
+ queueStartMode: params.route.surfaceNotifications.queueStart,
69268
+ notificationPromptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
68644
69269
  postText: params.postText,
68645
69270
  reconcileText: params.reconcileText,
68646
69271
  observerId: `${observerId}:loop:${index + 1}`
@@ -68781,6 +69406,8 @@ ${escapeCodeFence(shellResult.output)}
68781
69406
  route: params.route,
68782
69407
  maxChars: params.maxChars,
68783
69408
  promptText: delayedPromptText,
69409
+ queueStartMode: params.route.surfaceNotifications.queueStart,
69410
+ notificationPromptSummary: explicitQueueMessage ?? summarizeSurfaceNotificationText(params.text),
68784
69411
  postText: params.postText,
68785
69412
  reconcileText: params.reconcileText,
68786
69413
  observerId,
@@ -68841,6 +69468,23 @@ function isTelegramSenderAllowed(params) {
68841
69468
  }
68842
69469
 
68843
69470
  // src/auth/resolve.ts
69471
+ function mergeRoleDefinitions2(inherited, override) {
69472
+ return {
69473
+ allow: override?.allow ?? inherited?.allow ?? [],
69474
+ users: override?.users ?? inherited?.users ?? []
69475
+ };
69476
+ }
69477
+ function mergeRoleRecord2(defaults, overrides) {
69478
+ const merged = {};
69479
+ const roleNames = new Set([
69480
+ ...Object.keys(defaults ?? {}),
69481
+ ...Object.keys(overrides ?? {})
69482
+ ]);
69483
+ for (const roleName of roleNames) {
69484
+ merged[roleName] = mergeRoleDefinitions2(defaults?.[roleName], overrides?.[roleName]);
69485
+ }
69486
+ return merged;
69487
+ }
68844
69488
  function normalizePrincipal(principal) {
68845
69489
  const trimmed = principal.trim();
68846
69490
  if (!trimmed) {
@@ -68888,10 +69532,7 @@ function getAgentAuth(config, agentId) {
68888
69532
  const override = entry?.auth;
68889
69533
  return {
68890
69534
  defaultRole: override?.defaultRole ?? defaults.defaultRole,
68891
- roles: {
68892
- ...defaults.roles,
68893
- ...override?.roles ?? {}
68894
- }
69535
+ roles: mergeRoleRecord2(defaults.roles, override?.roles)
68895
69536
  };
68896
69537
  }
68897
69538
  function getAllowedPermissions(roles, role) {
@@ -69014,6 +69655,10 @@ function buildSharedChannelRoute(params) {
69014
69655
  response: params.route?.response ?? params.channelConfig.response,
69015
69656
  responseMode: params.route?.responseMode ?? agentEntry?.responseMode ?? params.channelConfig.responseMode,
69016
69657
  additionalMessageMode: params.route?.additionalMessageMode ?? agentEntry?.additionalMessageMode ?? params.channelConfig.additionalMessageMode,
69658
+ surfaceNotifications: {
69659
+ queueStart: params.route?.surfaceNotifications?.queueStart ?? params.channelConfig.surfaceNotifications?.queueStart ?? "brief",
69660
+ loopStart: params.route?.surfaceNotifications?.loopStart ?? params.channelConfig.surfaceNotifications?.loopStart ?? "brief"
69661
+ },
69017
69662
  verbose: params.route?.verbose ?? params.channelConfig.verbose,
69018
69663
  followUp: {
69019
69664
  mode: params.route?.followUp?.mode ?? params.channelConfig.followUp.mode,
@@ -69990,6 +70635,20 @@ class SlackSocketService {
69990
70635
  appToken: this.accountConfig.appToken,
69991
70636
  socketMode: true
69992
70637
  });
70638
+ this.agentService.registerSurfaceNotificationHandler({
70639
+ platform: "slack",
70640
+ accountId: this.accountId,
70641
+ handler: async ({ binding, text }) => {
70642
+ if (!binding.channelId) {
70643
+ return;
70644
+ }
70645
+ await postSlackText(this.app.client, {
70646
+ channel: binding.channelId,
70647
+ threadTs: binding.threadTs,
70648
+ text
70649
+ });
70650
+ }
70651
+ });
69993
70652
  this.registerEvents();
69994
70653
  }
69995
70654
  getSlackMaxChars(agentId) {
@@ -70243,6 +70902,7 @@ class SlackSocketService {
70243
70902
  const cliTool = getAgentEntry(this.loadedConfig, params.route.agentId)?.cliTool;
70244
70903
  const identity = {
70245
70904
  platform: "slack",
70905
+ accountId: this.accountId,
70246
70906
  conversationKind: params.conversationKind,
70247
70907
  senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
70248
70908
  channelId,
@@ -70426,6 +71086,10 @@ class SlackSocketService {
70426
71086
  await this.startPromise;
70427
71087
  }
70428
71088
  async stop() {
71089
+ this.agentService.unregisterSurfaceNotificationHandler({
71090
+ platform: "slack",
71091
+ accountId: this.accountId
71092
+ });
70429
71093
  await this.app.stop();
70430
71094
  }
70431
71095
  getBotUserLabel() {
@@ -71280,6 +71944,33 @@ async function resolveTelegramAttachmentPaths(params) {
71280
71944
  return attachmentPaths;
71281
71945
  }
71282
71946
 
71947
+ // src/channels/telegram/route-guidance.ts
71948
+ function renderTelegramRouteChoiceMessage(params) {
71949
+ const chatId = String(params.chatId);
71950
+ const topicId = params.topicId != null ? String(params.topicId) : undefined;
71951
+ const lines = [
71952
+ topicId != null ? "clisbot: this Telegram topic is not configured yet." : "clisbot: this Telegram group is not configured yet.",
71953
+ "",
71954
+ "Ask the bot owner to choose one of these:",
71955
+ "",
71956
+ "Add the whole group with the default agent:",
71957
+ `\`clisbot channels add telegram-group ${chatId}\``,
71958
+ "",
71959
+ "Add the whole group with a specific agent:",
71960
+ `\`clisbot channels add telegram-group ${chatId} --agent <id>\``
71961
+ ];
71962
+ if (topicId != null) {
71963
+ lines.push("", "Add only this topic with a specific agent:", `\`clisbot channels add telegram-group ${chatId} --topic ${topicId} --agent <id>\``);
71964
+ }
71965
+ if (params.includeConfigPath) {
71966
+ lines.push("", topicId != null ? `Config path: \`channels.telegram.groups."${chatId}".topics."${topicId}"\`` : `Config path: \`channels.telegram.groups."${chatId}"\``);
71967
+ } else {
71968
+ lines.push("", "After that, routed commands such as `/status`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
71969
+ }
71970
+ return lines.join(`
71971
+ `);
71972
+ }
71973
+
71283
71974
  // src/channels/telegram/typing.ts
71284
71975
  var TELEGRAM_TYPING_HEARTBEAT_MS = 4500;
71285
71976
  function logTelegramTypingError(onError, error) {
@@ -71423,6 +72114,7 @@ class TelegramPollingService {
71423
72114
  activityStore;
71424
72115
  accountId;
71425
72116
  accountConfig;
72117
+ reportLifecycle;
71426
72118
  botUserId = 0;
71427
72119
  botUsername = "";
71428
72120
  running = false;
@@ -71431,13 +72123,35 @@ class TelegramPollingService {
71431
72123
  activePollController;
71432
72124
  inFlightUpdates = new Set;
71433
72125
  processingIndicators = new ConversationProcessingIndicatorCoordinator;
71434
- constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig) {
72126
+ constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig, reportLifecycle) {
71435
72127
  this.loadedConfig = loadedConfig;
71436
72128
  this.agentService = agentService;
71437
72129
  this.processedEventsStore = processedEventsStore;
71438
72130
  this.activityStore = activityStore;
71439
72131
  this.accountId = accountId;
71440
72132
  this.accountConfig = accountConfig;
72133
+ this.reportLifecycle = reportLifecycle;
72134
+ this.agentService.registerSurfaceNotificationHandler({
72135
+ platform: "telegram",
72136
+ accountId: this.accountId,
72137
+ handler: async ({ binding, text }) => {
72138
+ if (!binding.chatId) {
72139
+ return;
72140
+ }
72141
+ const chatId = Number(binding.chatId);
72142
+ if (!Number.isFinite(chatId)) {
72143
+ return;
72144
+ }
72145
+ const topicId = binding.topicId ? Number(binding.topicId) : undefined;
72146
+ await postTelegramText({
72147
+ token: this.accountConfig.botToken,
72148
+ chatId,
72149
+ text,
72150
+ topicId: Number.isFinite(topicId) ? topicId : undefined,
72151
+ omitThreadId: shouldOmitTelegramThreadId(Number.isFinite(topicId) ? topicId : undefined)
72152
+ });
72153
+ }
72154
+ });
71441
72155
  }
71442
72156
  async start() {
71443
72157
  const me = await callTelegramApi(this.accountConfig.botToken, "getMe", {});
@@ -71454,6 +72168,10 @@ class TelegramPollingService {
71454
72168
  this.activePollController?.abort();
71455
72169
  await this.loopPromise;
71456
72170
  await Promise.allSettled([...this.inFlightUpdates]);
72171
+ this.agentService.unregisterSurfaceNotificationHandler({
72172
+ platform: "telegram",
72173
+ accountId: this.accountId
72174
+ });
71457
72175
  }
71458
72176
  getBotLabel() {
71459
72177
  return this.botUsername ? `@${this.botUsername}` : `${this.botUserId || "unknown"}`;
@@ -71500,6 +72218,15 @@ class TelegramPollingService {
71500
72218
  }
71501
72219
  if (isTelegramPollingConflict(error)) {
71502
72220
  this.running = false;
72221
+ await this.reportLifecycle?.({
72222
+ connection: "failed",
72223
+ summary: "Telegram polling stopped because another instance is already using this bot token.",
72224
+ detail: error instanceof Error ? error.message : String(error),
72225
+ actions: [
72226
+ "stop the other Telegram poller that is using the same bot token",
72227
+ "run `clisbot start` again after the token is no longer in use elsewhere"
72228
+ ]
72229
+ });
71503
72230
  console.error("telegram polling stopped: another bot instance is already calling getUpdates for this token");
71504
72231
  return;
71505
72232
  }
@@ -71667,6 +72394,7 @@ class TelegramPollingService {
71667
72394
  const senderName = [message.from?.first_name, message.from?.last_name].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").trim();
71668
72395
  const identity = {
71669
72396
  platform: "telegram",
72397
+ accountId: this.accountId,
71670
72398
  conversationKind: routeInfo.conversationKind,
71671
72399
  senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
71672
72400
  senderName: senderName || message.from?.username?.trim() || undefined,
@@ -72079,7 +72807,7 @@ var telegramChannelPlugin = {
72079
72807
  accountId,
72080
72808
  config
72081
72809
  })),
72082
- createRuntimeService: (context, account) => new TelegramPollingService(context.loadedConfig, context.agentService, context.processedEventsStore, context.activityStore, account.accountId, account.config),
72810
+ createRuntimeService: (context, account) => new TelegramPollingService(context.loadedConfig, context.agentService, context.processedEventsStore, context.activityStore, account.accountId, account.config, context.reportLifecycle),
72083
72811
  renderHealthSummary: (state) => {
72084
72812
  switch (state) {
72085
72813
  case "starting":
@@ -72173,7 +72901,7 @@ var defaultMessageCliDependencies = {
72173
72901
  await agentService.recordConversationReply(target, kind, source);
72174
72902
  }
72175
72903
  };
72176
- function parseRepeatedOption2(args, name) {
72904
+ function parseRepeatedOption3(args, name) {
72177
72905
  const values = [];
72178
72906
  for (let index = 0;index < args.length; index += 1) {
72179
72907
  if (args[index] !== name) {
@@ -72188,7 +72916,7 @@ function parseRepeatedOption2(args, name) {
72188
72916
  return values;
72189
72917
  }
72190
72918
  function parseOptionValue4(args, name) {
72191
- const values = parseRepeatedOption2(args, name);
72919
+ const values = parseRepeatedOption3(args, name);
72192
72920
  return values.length > 0 ? values.at(-1) : undefined;
72193
72921
  }
72194
72922
  function parseIntegerOption(args, name) {
@@ -72202,7 +72930,7 @@ function parseIntegerOption(args, name) {
72202
72930
  }
72203
72931
  return parsed;
72204
72932
  }
72205
- function hasFlag3(args, name) {
72933
+ function hasFlag4(args, name) {
72206
72934
  return args.includes(name);
72207
72935
  }
72208
72936
  function resolveReplyKind(command) {
@@ -72234,18 +72962,18 @@ function parseMessageCommand(args) {
72234
72962
  media: parseOptionValue4(rest, "--media"),
72235
72963
  messageId: parseOptionValue4(rest, "--message-id"),
72236
72964
  emoji: parseOptionValue4(rest, "--emoji"),
72237
- remove: hasFlag3(rest, "--remove"),
72965
+ remove: hasFlag4(rest, "--remove"),
72238
72966
  threadId: parseOptionValue4(rest, "--thread-id"),
72239
72967
  replyTo: parseOptionValue4(rest, "--reply-to"),
72240
72968
  limit: parseIntegerOption(rest, "--limit"),
72241
72969
  query: parseOptionValue4(rest, "--query"),
72242
72970
  pollQuestion: parseOptionValue4(rest, "--poll-question"),
72243
- pollOptions: parseRepeatedOption2(rest, "--poll-option"),
72244
- forceDocument: hasFlag3(rest, "--force-document"),
72245
- silent: hasFlag3(rest, "--silent"),
72246
- progress: hasFlag3(rest, "--progress"),
72247
- final: hasFlag3(rest, "--final"),
72248
- json: hasFlag3(rest, "--json")
72971
+ pollOptions: parseRepeatedOption3(rest, "--poll-option"),
72972
+ forceDocument: hasFlag4(rest, "--force-document"),
72973
+ silent: hasFlag4(rest, "--silent"),
72974
+ progress: hasFlag4(rest, "--progress"),
72975
+ final: hasFlag4(rest, "--final"),
72976
+ json: hasFlag4(rest, "--json")
72249
72977
  };
72250
72978
  }
72251
72979
  function renderMessageHelp() {
@@ -72468,6 +73196,7 @@ class RuntimeSupervisor {
72468
73196
  reloadInFlight = false;
72469
73197
  reloadRequested = false;
72470
73198
  configWatchDebounceMs = 250;
73199
+ nextRuntimeId = 1;
72471
73200
  dependencies;
72472
73201
  constructor(configPath, dependencies) {
72473
73202
  this.configPath = configPath;
@@ -72484,15 +73213,50 @@ class RuntimeSupervisor {
72484
73213
  async start() {
72485
73214
  await this.reload("initial");
72486
73215
  }
72487
- async stop() {
73216
+ async stop(options = {}) {
72488
73217
  this.clearReloadTimer();
72489
73218
  this.stopWatchingConfig();
72490
73219
  await this.stopActiveRuntime();
73220
+ if (options.markChannelsStopped !== false) {
73221
+ for (const plugin of this.dependencies.listChannelPlugins()) {
73222
+ await this.dependencies.runtimeHealthStore.setChannel({
73223
+ channel: plugin.id,
73224
+ connection: "stopped",
73225
+ summary: plugin.renderHealthSummary("stopped")
73226
+ });
73227
+ }
73228
+ }
73229
+ }
73230
+ async markFatalFailure(error) {
73231
+ const activeRuntime = this.activeRuntime;
73232
+ if (!activeRuntime) {
73233
+ return;
73234
+ }
73235
+ const detail = error instanceof Error ? error.message : String(error);
73236
+ const instancesByChannel = new Map;
73237
+ for (const entry of activeRuntime.channelServices) {
73238
+ const identity = entry.service.getRuntimeIdentity?.();
73239
+ if (!identity) {
73240
+ continue;
73241
+ }
73242
+ const existing = instancesByChannel.get(entry.channel) ?? [];
73243
+ existing.push(identity);
73244
+ instancesByChannel.set(entry.channel, existing);
73245
+ }
72491
73246
  for (const plugin of this.dependencies.listChannelPlugins()) {
73247
+ if (!plugin.isEnabled(activeRuntime.loadedConfig)) {
73248
+ continue;
73249
+ }
72492
73250
  await this.dependencies.runtimeHealthStore.setChannel({
72493
73251
  channel: plugin.id,
72494
- connection: "stopped",
72495
- summary: plugin.renderHealthSummary("stopped")
73252
+ connection: "failed",
73253
+ summary: "Runtime crashed due to a fatal error.",
73254
+ detail,
73255
+ actions: [
73256
+ "run `clisbot logs` and inspect the fatal error",
73257
+ "fix the underlying runtime fault, then restart with `clisbot start`"
73258
+ ],
73259
+ instances: instancesByChannel.get(plugin.id) ?? []
72496
73260
  });
72497
73261
  }
72498
73262
  }
@@ -72570,6 +73334,7 @@ class RuntimeSupervisor {
72570
73334
  }
72571
73335
  }
72572
73336
  async createRuntime(loadedConfig) {
73337
+ const runtimeId = this.nextRuntimeId++;
72573
73338
  const agentService = this.dependencies.createAgentService(loadedConfig);
72574
73339
  const processedEventsStore = this.dependencies.createProcessedEventsStore(loadedConfig.processedEventsPath);
72575
73340
  const activityStore = this.dependencies.createActivityStore();
@@ -72591,7 +73356,14 @@ class RuntimeSupervisor {
72591
73356
  loadedConfig,
72592
73357
  agentService,
72593
73358
  processedEventsStore,
72594
- activityStore
73359
+ activityStore,
73360
+ reportLifecycle: (event) => this.reportChannelLifecycle({
73361
+ runtimeId,
73362
+ plugin,
73363
+ channelServices,
73364
+ accountId: account.accountId,
73365
+ event
73366
+ })
72595
73367
  }, account)
72596
73368
  });
72597
73369
  }
@@ -72622,6 +73394,8 @@ class RuntimeSupervisor {
72622
73394
  });
72623
73395
  }
72624
73396
  return {
73397
+ id: runtimeId,
73398
+ loadedConfig,
72625
73399
  agentService,
72626
73400
  channelServices
72627
73401
  };
@@ -72661,6 +73435,38 @@ class RuntimeSupervisor {
72661
73435
  });
72662
73436
  }
72663
73437
  }
73438
+ getChannelInstances(channelServices, channel) {
73439
+ return channelServices.filter((entry) => entry.channel === channel).map((entry) => entry.service.getRuntimeIdentity?.()).filter((identity) => identity != null);
73440
+ }
73441
+ async reportChannelLifecycle(params) {
73442
+ if (this.activeRuntime?.id !== params.runtimeId) {
73443
+ return;
73444
+ }
73445
+ const instances = this.getChannelInstances(params.channelServices, params.plugin.id);
73446
+ if (params.event.connection === "active") {
73447
+ await this.dependencies.runtimeHealthStore.setChannel({
73448
+ channel: params.plugin.id,
73449
+ connection: "active",
73450
+ summary: params.event.summary ?? params.plugin.renderActiveHealthSummary(Math.max(1, instances.length)),
73451
+ detail: params.event.detail,
73452
+ actions: params.event.actions,
73453
+ instances
73454
+ });
73455
+ return;
73456
+ }
73457
+ const detailPrefix = `account=${params.accountId}`;
73458
+ await this.dependencies.runtimeHealthStore.setChannel({
73459
+ channel: params.plugin.id,
73460
+ connection: "failed",
73461
+ summary: params.event.summary ?? `${params.plugin.id} channel failed after startup.`,
73462
+ detail: params.event.detail ? `${detailPrefix}; ${params.event.detail}` : detailPrefix,
73463
+ actions: params.event.actions ?? [
73464
+ "run `clisbot logs` and inspect the latest channel error",
73465
+ "restart `clisbot` after fixing the channel-level issue"
73466
+ ],
73467
+ instances
73468
+ });
73469
+ }
72664
73470
  async reconcileConfigWatcher(loadedConfig) {
72665
73471
  const configReload = loadedConfig.raw.control.configReload;
72666
73472
  this.configWatchDebounceMs = configReload.watchDebounceMs;
@@ -72851,6 +73657,11 @@ async function getRuntimeOperatorSummary(params) {
72851
73657
  ];
72852
73658
  return {
72853
73659
  loadedConfig,
73660
+ ownerSummary: {
73661
+ ownerPrincipals: loadedConfig.raw.app.auth.roles.owner?.users ?? [],
73662
+ adminPrincipals: loadedConfig.raw.app.auth.roles.admin?.users ?? [],
73663
+ ownerClaimWindowMinutes: loadedConfig.raw.app.auth.ownerClaimWindowMinutes
73664
+ },
72854
73665
  agentSummaries,
72855
73666
  channelSummaries,
72856
73667
  activeRuns: await agentService.listActiveSessionRuntimes(),
@@ -72932,11 +73743,75 @@ function renderAgentSummaryLines(summary) {
72932
73743
  })
72933
73744
  ];
72934
73745
  }
73746
+ function renderOwnerSummaryLines(summary) {
73747
+ const ownerPrincipals = summary.ownerSummary.ownerPrincipals;
73748
+ const adminPrincipals = summary.ownerSummary.adminPrincipals;
73749
+ const configuredOwners = ownerPrincipals.length > 0 ? ownerPrincipals.join(",") : "none";
73750
+ const claimWindow = `${summary.ownerSummary.ownerClaimWindowMinutes}m`;
73751
+ const lines = [
73752
+ "Owner:",
73753
+ ` - configured=${ownerPrincipals.length > 0 ? "yes" : "no"} principals=${configuredOwners} claimWindow=${claimWindow}`,
73754
+ " - access=full app control, DM pairing bypass, implicit admin across all agents/channels"
73755
+ ];
73756
+ if (adminPrincipals.length > 0) {
73757
+ lines.push(` - appAdmins=${adminPrincipals.join(",")}`);
73758
+ }
73759
+ return lines;
73760
+ }
73761
+ function hasConfiguredPrivilegedPrincipal(summary) {
73762
+ return summary.ownerSummary.ownerPrincipals.length > 0 || summary.ownerSummary.adminPrincipals.length > 0;
73763
+ }
73764
+ function renderPrivilegedChatHint(summary, action) {
73765
+ if (hasConfiguredPrivilegedPrincipal(summary)) {
73766
+ return `chat with the bot from that owner/admin account to ${action}`;
73767
+ }
73768
+ return `after you configure an owner/admin principal, use that account to ${action}`;
73769
+ }
73770
+ function appendChannelNextStepLines(lines, summary, prefix = "") {
73771
+ const slackEnabled = summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled);
73772
+ const telegramEnabled = summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled);
73773
+ const hasEnabledChannel = slackEnabled || telegramEnabled;
73774
+ if (!hasEnabledChannel) {
73775
+ lines.push(`${prefix}- run \`clisbot channels enable <slack|telegram>\` for the first channel you want to expose`);
73776
+ return;
73777
+ }
73778
+ if (telegramEnabled && slackEnabled) {
73779
+ lines.push(`${prefix}- DM the Telegram or Slack bot first to confirm it responds normally`);
73780
+ } else if (telegramEnabled) {
73781
+ lines.push(`${prefix}- DM the Telegram bot first to confirm it responds normally`);
73782
+ } else {
73783
+ lines.push(`${prefix}- DM the Slack bot first to confirm it responds normally`);
73784
+ }
73785
+ lines.push(`${prefix}- after DM works, add the bot to the target Slack channel or Telegram group/topic`);
73786
+ lines.push(`${prefix}- route that surface with \`clisbot channels add slack-channel <channelId> --agent <id>\` or \`clisbot channels add telegram-group <chatId> --agent <id>\``);
73787
+ if (telegramEnabled) {
73788
+ lines.push(`${prefix}- Telegram: send \`/start\` in the target DM, group, or topic to get onboarding or pairing guidance`);
73789
+ }
73790
+ if (slackEnabled) {
73791
+ lines.push(`${prefix}- Slack: mention \`@<botname> \\start\` in the target channel to verify mention flow`);
73792
+ }
73793
+ }
73794
+ function appendAuthOnboardingLines(lines, summary, prefix = "") {
73795
+ lines.push(`${prefix}Auth onboarding:`);
73796
+ if (!hasConfiguredPrivilegedPrincipal(summary)) {
73797
+ lines.push(`${prefix} - get the principal from a surface the bot can already see; Telegram groups or topics can use \`/whoami\` before routing, while DMs with pairing must pair first`);
73798
+ lines.push(`${prefix} - set the first owner with: \`clisbot auth add-user app --role owner --user <principal>\``);
73799
+ } else {
73800
+ lines.push(`${prefix} - inspect current app roles with: \`clisbot auth show app\``);
73801
+ }
73802
+ lines.push(`${prefix} - inspect default agent roles with: \`clisbot auth show agent-defaults\``);
73803
+ lines.push(`${prefix} - add or remove principals with: \`clisbot auth add-user ...\` and \`clisbot auth remove-user ...\``);
73804
+ lines.push(`${prefix} - tune role permissions with: \`clisbot auth add-permission ...\` and \`clisbot auth remove-permission ...\``);
73805
+ lines.push(`${prefix} - run \`clisbot auth --help\` or read docs/user-guide/auth-and-roles.md for scopes and permission names`);
73806
+ }
72935
73807
  function renderChannelSummaryLines(summary) {
72936
73808
  return [
72937
73809
  "",
72938
73810
  "Channels:",
72939
73811
  ...summary.channelSummaries.map((channel) => {
73812
+ if (!channel.enabled) {
73813
+ return ` - ${channel.channel} enabled=no`;
73814
+ }
72940
73815
  const last = channel.lastActivityAt ? ` last=${formatTime(channel.lastActivityAt)} via ${channel.lastActivityAgentId ?? "unknown"}` : " last=never";
72941
73816
  const dm = ` dm=${channel.directMessagesEnabled ? channel.directMessagesPolicy : "disabled"}`;
72942
73817
  const group = channel.groupPolicy ? ` groups=${channel.groupPolicy}` : "";
@@ -72992,8 +73867,9 @@ function renderRuntimeDiagnosticsSummary(summary) {
72992
73867
  `).trim();
72993
73868
  }
72994
73869
  function renderStartSummary(summary) {
72995
- const configPath = collapseHomePath(getDefaultConfigPath());
72996
73870
  const lines = [
73871
+ ...renderOwnerSummaryLines(summary),
73872
+ "",
72997
73873
  ...renderAgentSummaryLines(summary),
72998
73874
  ...renderChannelSummaryLines(summary)
72999
73875
  ];
@@ -73031,7 +73907,10 @@ function renderStartSummary(summary) {
73031
73907
  lines.push("");
73032
73908
  lines.push(" Next steps after bootstrap:");
73033
73909
  lines.push(" - chat with the bot or open the workspace, then follow BOOTSTRAP.md");
73034
- lines.push(` - configure Slack channels or Telegram groups/topics in ${configPath}`);
73910
+ appendChannelNextStepLines(lines, summary, " ");
73911
+ lines.push(` - ${renderPrivilegedChatHint(summary, "verify DM access and adjust in-chat settings")}`);
73912
+ lines.push("");
73913
+ appendAuthOnboardingLines(lines, summary, " ");
73035
73914
  lines.push(" - run `clisbot status` to recheck runtime and bootstrap state");
73036
73915
  lines.push(" - run `clisbot logs` if the bot does not answer as expected");
73037
73916
  lines.push(...renderPairingSetupHelpLines(" ", {
@@ -73049,9 +73928,11 @@ function renderStartSummary(summary) {
73049
73928
  }
73050
73929
  lines.push("");
73051
73930
  lines.push("Next steps:");
73052
- lines.push(` - configure Slack channels or Telegram groups/topics in ${configPath}`);
73053
- lines.push(" - verify routing and defaultAgentId values match the agent you want to expose");
73054
- lines.push(" - send a test message from Slack or Telegram");
73931
+ appendChannelNextStepLines(lines, summary, " ");
73932
+ lines.push(" - verify routes and defaultAgentId values match the agent you want to expose");
73933
+ lines.push(` - ${renderPrivilegedChatHint(summary, "adjust in-chat surface settings")}`);
73934
+ lines.push("");
73935
+ appendAuthOnboardingLines(lines, summary);
73055
73936
  lines.push(" - run `clisbot status` to inspect agents, channels, and tmux session state");
73056
73937
  lines.push(" - run `clisbot logs` if anything looks wrong");
73057
73938
  lines.push(...renderPairingSetupHelpLines("", {
@@ -73078,20 +73959,24 @@ function appendChannelSetupNotes(lines, summary, prefix = "") {
73078
73959
  if (channel.channel === "telegram") {
73079
73960
  lines.push(`${prefix} - telegram: no explicit group or topic routes are configured yet`);
73080
73961
  lines.push(`${prefix} dms: ${channel.directMessagesEnabled ? `enabled (${channel.directMessagesPolicy})` : "disabled"}`);
73081
- lines.push(`${prefix} route: add channels.telegram.groups.<chatId> in ${collapseHomePath(getDefaultConfigPath())}`);
73082
- lines.push(`${prefix} example: channels.telegram.groups."-1001234567890".agentId = "default"`);
73083
- lines.push(`${prefix} forum topics: use channels.telegram.groups.<chatId>.topics.<topicId>`);
73962
+ lines.push(`${prefix} add group: \`clisbot channels add telegram-group <chatId> --agent <id>\``);
73963
+ lines.push(`${prefix} add topic: \`clisbot channels add telegram-group <chatId> --topic <topicId> --agent <id>\``);
73964
+ lines.push(`${prefix} adjust later: ${renderPrivilegedChatHint(summary, "run in-chat commands here")}`);
73084
73965
  continue;
73085
73966
  }
73086
73967
  lines.push(`${prefix} - slack: no explicit channel or group routes are configured yet`);
73087
73968
  lines.push(`${prefix} dms: ${channel.directMessagesEnabled ? `enabled (${channel.directMessagesPolicy})` : "disabled"}`);
73088
73969
  lines.push(`${prefix} groups: ${channel.groupPolicy ?? "n/a"}`);
73089
- lines.push(`${prefix} route: configure channels.slack.channels.<channelId> or channels.slack.groups.<groupId>`);
73970
+ lines.push(`${prefix} add channel: \`clisbot channels add slack-channel <channelId> --agent <id>\``);
73971
+ lines.push(`${prefix} add group: \`clisbot channels add slack-group <groupId> --agent <id>\``);
73972
+ lines.push(`${prefix} adjust later: ${renderPrivilegedChatHint(summary, "run in-chat commands here")}`);
73090
73973
  }
73091
73974
  }
73092
73975
  function renderStatusSummary(summary) {
73093
73976
  const lines = [
73094
73977
  `stats agents=${summary.configuredAgents} bootstrapped=${summary.bootstrappedAgents} pendingBootstrap=${summary.bootstrapPendingAgents} tmuxSessions=${summary.runningTmuxSessions}`,
73978
+ ...renderOwnerSummaryLines(summary),
73979
+ "",
73095
73980
  ...renderAgentSummaryLines(summary),
73096
73981
  ...renderChannelSummaryLines(summary),
73097
73982
  ...renderChannelDiagnosticLines(summary),
@@ -73113,6 +73998,15 @@ function printCommandOutcomeBanner(outcome) {
73113
73998
  function printCommandOutcomeFooter(outcome) {
73114
73999
  printCommandOutcomeBanner(outcome);
73115
74000
  }
74001
+ function assertSupportedPlatform(command) {
74002
+ if (process.platform !== "win32") {
74003
+ return;
74004
+ }
74005
+ if (command.name === "help" || command.name === "version") {
74006
+ return;
74007
+ }
74008
+ throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
74009
+ }
73116
74010
  function getPrimaryWorkspacePath(summary) {
73117
74011
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
73118
74012
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -73262,25 +74156,57 @@ async function serveForeground() {
73262
74156
  const runtimeCredentialsPath = getDefaultRuntimeCredentialsPath();
73263
74157
  const runtimeSupervisor = new RuntimeSupervisor(configPath);
73264
74158
  let shuttingDown = false;
73265
- const shutdown = async (exitCode = 0) => {
74159
+ let fatalHandling = false;
74160
+ const shutdown = async (exitCode = 0, options = {}) => {
73266
74161
  if (shuttingDown) {
73267
74162
  return;
73268
74163
  }
73269
74164
  shuttingDown = true;
73270
74165
  try {
73271
- await runtimeSupervisor.stop();
74166
+ await runtimeSupervisor.stop({
74167
+ markChannelsStopped: options.markChannelsStopped
74168
+ });
73272
74169
  } finally {
73273
74170
  removeRuntimeCredentials(runtimeCredentialsPath);
73274
74171
  removeRuntimePid(pidPath);
73275
74172
  process.exit(exitCode);
73276
74173
  }
73277
74174
  };
74175
+ const handleFatal = async (source, error) => {
74176
+ if (fatalHandling || shuttingDown) {
74177
+ return;
74178
+ }
74179
+ fatalHandling = true;
74180
+ const detail = error instanceof Error ? error.message : String(error);
74181
+ const fatalError = new Error(`fatal ${source}: ${detail}`);
74182
+ console.error(`clisbot fatal ${source}`, error);
74183
+ const forceExitTimer = setTimeout(() => {
74184
+ process.exit(1);
74185
+ }, 5000);
74186
+ forceExitTimer.unref?.();
74187
+ try {
74188
+ await runtimeSupervisor.markFatalFailure(fatalError);
74189
+ } catch (markError) {
74190
+ console.error("failed to record fatal runtime health", markError);
74191
+ }
74192
+ try {
74193
+ await shutdown(1, { markChannelsStopped: false });
74194
+ } finally {
74195
+ clearTimeout(forceExitTimer);
74196
+ }
74197
+ };
73278
74198
  process.once("SIGINT", () => {
73279
74199
  shutdown(0);
73280
74200
  });
73281
74201
  process.once("SIGTERM", () => {
73282
74202
  shutdown(0);
73283
74203
  });
74204
+ process.once("uncaughtException", (error) => {
74205
+ handleFatal("uncaughtException", error);
74206
+ });
74207
+ process.once("unhandledRejection", (error) => {
74208
+ handleFatal("unhandledRejection", error);
74209
+ });
73284
74210
  try {
73285
74211
  await runtimeSupervisor.start();
73286
74212
  await writeRuntimePid(pidPath, process.pid);
@@ -73357,7 +74283,7 @@ async function start(args = []) {
73357
74283
  console.log(`clisbot is already running with pid: ${result.pid}`);
73358
74284
  const workspacePath = getPrimaryWorkspacePath(summary);
73359
74285
  if (workspacePath) {
73360
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74286
+ console.log(`workspace: ${workspacePath}`);
73361
74287
  }
73362
74288
  console.log(`config: ${result.configPath}`);
73363
74289
  console.log(`log: ${result.logPath}`);
@@ -73389,7 +74315,7 @@ async function start(args = []) {
73389
74315
  console.log(`clisbot started with pid: ${result.pid}`);
73390
74316
  const workspacePath = getPrimaryWorkspacePath(summary);
73391
74317
  if (workspacePath) {
73392
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74318
+ console.log(`workspace: ${workspacePath}`);
73393
74319
  }
73394
74320
  console.log(`config: ${result.configPath}`);
73395
74321
  console.log(`log: ${result.logPath}`);
@@ -73478,7 +74404,7 @@ async function status() {
73478
74404
  });
73479
74405
  const workspacePath = getPrimaryWorkspacePath(summary);
73480
74406
  if (workspacePath) {
73481
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74407
+ console.log(`workspace: ${workspacePath}`);
73482
74408
  }
73483
74409
  console.log(`config: ${runtimeStatus.configPath}`);
73484
74410
  console.log(`pid file: ${runtimeStatus.pidPath}`);
@@ -73521,6 +74447,7 @@ async function logs(lines) {
73521
74447
  } catch {}
73522
74448
  }
73523
74449
  async function main(command = parseCliArgs(process.argv)) {
74450
+ assertSupportedPlatform(command);
73524
74451
  if (command.name === "help") {
73525
74452
  console.log(renderCliHelp());
73526
74453
  return;
@@ -73577,6 +74504,10 @@ async function main(command = parseCliArgs(process.argv)) {
73577
74504
  await runAgentsCli(command.args);
73578
74505
  return;
73579
74506
  }
74507
+ if (command.name === "auth") {
74508
+ await runAuthCli(command.args);
74509
+ return;
74510
+ }
73580
74511
  if (command.name === "pairing") {
73581
74512
  await runPairingCli(command.args);
73582
74513
  return;