clisbot 0.1.17 → 0.1.19

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",
@@ -62705,40 +62761,329 @@ async function runAccountsCli(args, deps = {}) {
62705
62761
  throw new Error(renderAccountsHelp());
62706
62762
  }
62707
62763
 
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.",
62764
+ // src/control/auth-cli.ts
62765
+ function getEditableConfigPath3() {
62766
+ return process.env.CLISBOT_CONFIG_PATH;
62767
+ }
62768
+ function parseRepeatedOption2(args, name) {
62769
+ const values = [];
62770
+ for (let index = 0;index < args.length; index += 1) {
62771
+ if (args[index] !== name) {
62772
+ continue;
62773
+ }
62774
+ const value = args[index + 1]?.trim();
62775
+ if (!value) {
62776
+ throw new Error(`Missing value for ${name}`);
62777
+ }
62778
+ values.push(value);
62779
+ }
62780
+ return values;
62781
+ }
62782
+ function parseSingleOption2(args, name) {
62783
+ const values = parseRepeatedOption2(args, name);
62784
+ if (values.length === 0) {
62785
+ return;
62786
+ }
62787
+ return values[values.length - 1];
62788
+ }
62789
+ function hasFlag3(args, name) {
62790
+ return args.includes(name);
62791
+ }
62792
+ function parseScope(raw, args) {
62793
+ if (raw === "app") {
62794
+ return { kind: "app" };
62795
+ }
62796
+ if (raw === "agent-defaults") {
62797
+ return { kind: "agent-defaults" };
62798
+ }
62799
+ if (raw === "agent") {
62800
+ const agentId = parseSingleOption2(args, "--agent");
62801
+ if (!agentId) {
62802
+ throw new Error("Missing value for --agent");
62803
+ }
62804
+ return { kind: "agent", agentId };
62805
+ }
62806
+ throw new Error("Scope required: app | agent-defaults | agent");
62807
+ }
62808
+ function renderAuthCliHelp() {
62809
+ return [
62810
+ "clisbot auth",
62714
62811
  "",
62715
- "Ask the bot owner to choose one of these:",
62812
+ "Manage auth roles, principals, and permissions in config.",
62716
62813
  "",
62717
- "Add the whole group with the default agent:",
62718
- `\`clisbot channels add telegram-group ${chatId}\``,
62814
+ "Usage:",
62815
+ " clisbot auth list [--json]",
62816
+ " clisbot auth show <app|agent-defaults|agent> [--agent <id>] [--json]",
62817
+ " clisbot auth add-user <app|agent-defaults|agent> --role <role> --user <principal> [--agent <id>]",
62818
+ " clisbot auth remove-user <app|agent-defaults|agent> --role <role> --user <principal> [--agent <id>]",
62819
+ " clisbot auth add-permission <app|agent-defaults|agent> --role <role> --permission <permission> [--agent <id>]",
62820
+ " clisbot auth remove-permission <app|agent-defaults|agent> --role <role> --permission <permission> [--agent <id>]",
62719
62821
  "",
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>\``);
62822
+ "Scopes:",
62823
+ " app edit app.auth",
62824
+ " agent-defaults edit agents.defaults.auth",
62825
+ " agent edit one agents.list[].auth override; requires --agent <id>",
62826
+ "",
62827
+ "Permission sets:",
62828
+ ` app ${APP_ADMIN_PERMISSIONS.join(", ")}`,
62829
+ ` agent ${DEFAULT_AGENT_ADMIN_PERMISSIONS.join(", ")}`,
62830
+ "",
62831
+ "Notes:",
62832
+ " add-user/remove-user mutate roles.<role>.users",
62833
+ " add-permission/remove-permission mutate roles.<role>.allow",
62834
+ " agent role edits clone the inherited agent-defaults role into the target agent override on first write",
62835
+ "",
62836
+ "Examples:",
62837
+ " clisbot auth add-user app --role owner --user telegram:1276408333",
62838
+ " clisbot auth remove-user app --role admin --user slack:U123",
62839
+ " clisbot auth add-user agent --agent default --role admin --user slack:U123",
62840
+ " clisbot auth add-permission agent-defaults --role member --permission shellExecute",
62841
+ " clisbot auth remove-permission agent --agent default --role member --permission shellExecute",
62842
+ " clisbot auth show agent-defaults",
62843
+ " clisbot auth list --json"
62844
+ ].join(`
62845
+ `);
62846
+ }
62847
+ function cloneRoleDefinition(value) {
62848
+ return {
62849
+ allow: [...value?.allow ?? []],
62850
+ users: [...value?.users ?? []]
62851
+ };
62852
+ }
62853
+ function mergeRoleDefinitions(inherited, override) {
62854
+ return {
62855
+ allow: [...override?.allow ?? inherited?.allow ?? []],
62856
+ users: [...override?.users ?? inherited?.users ?? []]
62857
+ };
62858
+ }
62859
+ function mergeRoleRecord(defaults, overrides) {
62860
+ const merged = {};
62861
+ const roleNames = new Set([
62862
+ ...Object.keys(defaults ?? {}),
62863
+ ...Object.keys(overrides ?? {})
62864
+ ]);
62865
+ for (const roleName of roleNames) {
62866
+ merged[roleName] = mergeRoleDefinitions(defaults?.[roleName], overrides?.[roleName]);
62725
62867
  }
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.");
62868
+ return merged;
62869
+ }
62870
+ function normalizeUnique(values) {
62871
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
62872
+ }
62873
+ function ensureAgentEntry(config, agentId) {
62874
+ const entry = config.agents.list.find((item) => item.id === agentId);
62875
+ if (!entry) {
62876
+ throw new Error(`Unknown agent: ${agentId}`);
62730
62877
  }
62731
- return lines.join(`
62732
- `);
62878
+ return entry;
62879
+ }
62880
+ function getAuthLabel(scope) {
62881
+ if (scope.kind === "app") {
62882
+ return "app.auth";
62883
+ }
62884
+ if (scope.kind === "agent-defaults") {
62885
+ return "agents.defaults.auth";
62886
+ }
62887
+ return `agents.list[${scope.agentId}].auth`;
62888
+ }
62889
+ function ensureEditableRoleDefinition(role) {
62890
+ role.allow = role.allow ?? [];
62891
+ role.users = role.users ?? [];
62892
+ return role;
62893
+ }
62894
+ function resolveAppRole(config, roleName) {
62895
+ const role = config.app.auth.roles[roleName];
62896
+ if (!role) {
62897
+ throw new Error(`Unknown app role: ${roleName}`);
62898
+ }
62899
+ return ensureEditableRoleDefinition(role);
62900
+ }
62901
+ function resolveAgentDefaultsRole(config, roleName) {
62902
+ const role = config.agents.defaults.auth.roles[roleName];
62903
+ if (!role) {
62904
+ throw new Error(`Unknown agent-defaults role: ${roleName}`);
62905
+ }
62906
+ return ensureEditableRoleDefinition(role);
62907
+ }
62908
+ function ensureAgentOverrideAuth(entry, config) {
62909
+ if (!entry.auth) {
62910
+ entry.auth = {
62911
+ defaultRole: config.agents.defaults.auth.defaultRole,
62912
+ roles: {}
62913
+ };
62914
+ }
62915
+ return entry.auth;
62916
+ }
62917
+ function resolveAgentRoleForEdit(config, agentId, roleName) {
62918
+ const entry = ensureAgentEntry(config, agentId);
62919
+ const explicitRole = entry.auth?.roles?.[roleName];
62920
+ if (explicitRole) {
62921
+ return ensureEditableRoleDefinition(explicitRole);
62922
+ }
62923
+ const inheritedRole = config.agents.defaults.auth.roles[roleName];
62924
+ if (!inheritedRole) {
62925
+ throw new Error(`Unknown agent role: ${roleName}`);
62926
+ }
62927
+ const auth = ensureAgentOverrideAuth(entry, config);
62928
+ auth.roles[roleName] = cloneRoleDefinition(inheritedRole);
62929
+ return ensureEditableRoleDefinition(auth.roles[roleName]);
62930
+ }
62931
+ function resolveRoleForEdit(config, scope, roleName) {
62932
+ if (scope.kind === "app") {
62933
+ return resolveAppRole(config, roleName);
62934
+ }
62935
+ if (scope.kind === "agent-defaults") {
62936
+ return resolveAgentDefaultsRole(config, roleName);
62937
+ }
62938
+ return resolveAgentRoleForEdit(config, scope.agentId, roleName);
62939
+ }
62940
+ function validatePermission(scope, permission) {
62941
+ const trimmed = permission.trim();
62942
+ if (!trimmed) {
62943
+ throw new Error("Missing value for --permission");
62944
+ }
62945
+ const allowedPermissions = new Set(scope.kind === "app" ? APP_ADMIN_PERMISSIONS : DEFAULT_AGENT_ADMIN_PERMISSIONS);
62946
+ if (!allowedPermissions.has(trimmed)) {
62947
+ const sorted = [...allowedPermissions].sort().join(", ");
62948
+ throw new Error(`Unknown permission for ${scope.kind}: ${trimmed}. Allowed: ${sorted}`);
62949
+ }
62950
+ return trimmed;
62951
+ }
62952
+ function buildShowPayload(config, scope) {
62953
+ if (scope.kind === "app") {
62954
+ return config.app.auth;
62955
+ }
62956
+ if (scope.kind === "agent-defaults") {
62957
+ return config.agents.defaults.auth;
62958
+ }
62959
+ const entry = ensureAgentEntry(config, scope.agentId);
62960
+ return {
62961
+ defaultRole: entry.auth?.defaultRole ?? config.agents.defaults.auth.defaultRole,
62962
+ roles: mergeRoleRecord(config.agents.defaults.auth.roles, entry.auth?.roles)
62963
+ };
62964
+ }
62965
+ async function listAuth(args) {
62966
+ const { config } = await readEditableConfig(getEditableConfigPath3());
62967
+ const payload = {
62968
+ app: config.app.auth,
62969
+ agentDefaults: config.agents.defaults.auth,
62970
+ agents: config.agents.list.map((entry) => ({
62971
+ agentId: entry.id,
62972
+ auth: buildShowPayload(config, { kind: "agent", agentId: entry.id })
62973
+ }))
62974
+ };
62975
+ if (hasFlag3(args, "--json")) {
62976
+ console.log(JSON.stringify(payload, null, 2));
62977
+ return;
62978
+ }
62979
+ console.log(JSON.stringify(payload, null, 2));
62980
+ }
62981
+ async function showAuth(args) {
62982
+ const scope = parseScope(args[0], args.slice(1));
62983
+ const { config } = await readEditableConfig(getEditableConfigPath3());
62984
+ const payload = buildShowPayload(config, scope);
62985
+ if (hasFlag3(args, "--json")) {
62986
+ console.log(JSON.stringify(payload, null, 2));
62987
+ return;
62988
+ }
62989
+ console.log(JSON.stringify(payload, null, 2));
62990
+ }
62991
+ async function mutateUsers(mode, args) {
62992
+ const scope = parseScope(args[0], args.slice(1));
62993
+ const roleName = parseSingleOption2(args, "--role");
62994
+ const principal = parseSingleOption2(args, "--user")?.trim();
62995
+ if (!roleName) {
62996
+ throw new Error("Missing value for --role");
62997
+ }
62998
+ if (!principal) {
62999
+ throw new Error("Missing value for --user");
63000
+ }
63001
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63002
+ const role = resolveRoleForEdit(config, scope, roleName);
63003
+ role.users = normalizeUnique(mode === "add" ? [...role.users, principal] : role.users.filter((value) => value !== principal));
63004
+ await writeEditableConfig(configPath, config);
63005
+ console.log(`${mode === "add" ? "Added" : "Removed"} user ${principal} ${mode === "add" ? "to" : "from"} ${getAuthLabel(scope)} role ${roleName}.`);
63006
+ }
63007
+ async function mutatePermissions(mode, args) {
63008
+ const scope = parseScope(args[0], args.slice(1));
63009
+ const roleName = parseSingleOption2(args, "--role");
63010
+ const permission = validatePermission(scope, parseSingleOption2(args, "--permission") ?? "");
63011
+ if (!roleName) {
63012
+ throw new Error("Missing value for --role");
63013
+ }
63014
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63015
+ const role = resolveRoleForEdit(config, scope, roleName);
63016
+ role.allow = normalizeUnique(mode === "add" ? [...role.allow, permission] : role.allow.filter((value) => value !== permission));
63017
+ await writeEditableConfig(configPath, config);
63018
+ console.log(`${mode === "add" ? "Added" : "Removed"} permission ${permission} ${mode === "add" ? "to" : "from"} ${getAuthLabel(scope)} role ${roleName}.`);
63019
+ }
63020
+ async function runAuthCli(args) {
63021
+ const [command, ...rest] = args;
63022
+ if (!command || command === "--help" || command === "-h" || command === "help") {
63023
+ console.log(renderAuthCliHelp());
63024
+ return;
63025
+ }
63026
+ if (command === "list") {
63027
+ await listAuth(rest);
63028
+ return;
63029
+ }
63030
+ if (command === "show") {
63031
+ await showAuth(rest);
63032
+ return;
63033
+ }
63034
+ if (command === "add-user") {
63035
+ await mutateUsers("add", rest);
63036
+ return;
63037
+ }
63038
+ if (command === "remove-user") {
63039
+ await mutateUsers("remove", rest);
63040
+ return;
63041
+ }
63042
+ if (command === "add-permission") {
63043
+ await mutatePermissions("add", rest);
63044
+ return;
63045
+ }
63046
+ if (command === "remove-permission") {
63047
+ await mutatePermissions("remove", rest);
63048
+ return;
63049
+ }
63050
+ throw new Error(renderAuthCliHelp());
62733
63051
  }
62734
63052
 
62735
63053
  // src/channels/mode-config-shared.ts
63054
+ function createTelegramGroupRoute() {
63055
+ return {
63056
+ requireMention: true,
63057
+ allowBots: false,
63058
+ topics: {}
63059
+ };
63060
+ }
63061
+ function ensureTelegramGroupRoute(config, chatId) {
63062
+ const existing = config.channels.telegram.groups[chatId];
63063
+ if (existing) {
63064
+ return existing;
63065
+ }
63066
+ const created = createTelegramGroupRoute();
63067
+ config.channels.telegram.groups[chatId] = created;
63068
+ return created;
63069
+ }
63070
+ function ensureTelegramTopicRoute(config, chatId, topicId) {
63071
+ const group = ensureTelegramGroupRoute(config, chatId);
63072
+ const existing = group.topics[topicId];
63073
+ if (existing) {
63074
+ return existing;
63075
+ }
63076
+ const created = {};
63077
+ group.topics[topicId] = created;
63078
+ return created;
63079
+ }
62736
63080
  function getModeValue(source, field) {
62737
63081
  return source[field];
62738
63082
  }
62739
63083
  function setModeValue(source, field, value) {
62740
63084
  source[field] = value;
62741
63085
  }
63086
+ var EMPTY_MODE_SOURCE = {};
62742
63087
  function resolveSlackConfigTarget(config, field, params) {
62743
63088
  if (!params.target) {
62744
63089
  return {
@@ -62820,22 +63165,25 @@ function resolveTelegramConfigTarget(config, field, params) {
62820
63165
  };
62821
63166
  }
62822
63167
  const group = config.channels.telegram.groups[chatId];
62823
- if (!group) {
62824
- throw new Error(renderTelegramRouteChoiceMessage({ chatId }));
62825
- }
62826
63168
  if (topicId) {
62827
- const topic = group.topics?.[topicId];
62828
- if (!topic) {
62829
- throw new Error(renderTelegramRouteChoiceMessage({ chatId, topicId }));
62830
- }
63169
+ const topic = group?.topics?.[topicId];
62831
63170
  return {
62832
- get: () => getModeValue(topic, field) ?? getModeValue(group, field) ?? getModeValue(config.channels.telegram, field),
63171
+ get: () => getModeValue(topic ?? EMPTY_MODE_SOURCE, field) ?? getModeValue(group ?? EMPTY_MODE_SOURCE, field) ?? getModeValue(config.channels.telegram, field),
62833
63172
  set: (value) => {
62834
- setModeValue(topic, field, value);
63173
+ setModeValue(ensureTelegramTopicRoute(config, chatId, topicId), field, value);
62835
63174
  },
62836
63175
  label: `telegram topic ${chatId}/${topicId}`
62837
63176
  };
62838
63177
  }
63178
+ if (!group) {
63179
+ return {
63180
+ get: () => getModeValue(config.channels.telegram, field),
63181
+ set: (value) => {
63182
+ setModeValue(ensureTelegramGroupRoute(config, chatId), field, value);
63183
+ },
63184
+ label: `telegram group ${chatId}`
63185
+ };
63186
+ }
62839
63187
  return {
62840
63188
  get: () => getModeValue(group, field) ?? getModeValue(config.channels.telegram, field),
62841
63189
  set: (value) => {
@@ -62876,11 +63224,11 @@ function renderFieldLabel(field) {
62876
63224
  }
62877
63225
 
62878
63226
  // src/channels/additional-message-mode-config.ts
62879
- function getEditableConfigPath3() {
63227
+ function getEditableConfigPath4() {
62880
63228
  return process.env.CLISBOT_CONFIG_PATH;
62881
63229
  }
62882
63230
  async function getConversationAdditionalMessageMode(params) {
62883
- const { config } = await readEditableConfig(getEditableConfigPath3());
63231
+ const { config } = await readEditableConfig(getEditableConfigPath4());
62884
63232
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", buildConfiguredTargetFromIdentity(params.identity));
62885
63233
  return {
62886
63234
  label: target.label,
@@ -62888,7 +63236,7 @@ async function getConversationAdditionalMessageMode(params) {
62888
63236
  };
62889
63237
  }
62890
63238
  async function setConversationAdditionalMessageMode(params) {
62891
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63239
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62892
63240
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", buildConfiguredTargetFromIdentity(params.identity));
62893
63241
  target.set(params.additionalMessageMode);
62894
63242
  await writeEditableConfig(configPath, config);
@@ -62899,7 +63247,7 @@ async function setConversationAdditionalMessageMode(params) {
62899
63247
  };
62900
63248
  }
62901
63249
  async function getConfiguredAdditionalMessageMode(params) {
62902
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63250
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62903
63251
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", params);
62904
63252
  return {
62905
63253
  configPath,
@@ -62908,7 +63256,7 @@ async function getConfiguredAdditionalMessageMode(params) {
62908
63256
  };
62909
63257
  }
62910
63258
  async function setConfiguredAdditionalMessageMode(params) {
62911
- const { config, configPath } = await readEditableConfig(getEditableConfigPath3());
63259
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
62912
63260
  const target = resolveConfiguredSurfaceModeTarget(config, "additionalMessageMode", params);
62913
63261
  target.set(params.additionalMessageMode);
62914
63262
  await writeEditableConfig(configPath, config);
@@ -62920,11 +63268,11 @@ async function setConfiguredAdditionalMessageMode(params) {
62920
63268
  }
62921
63269
 
62922
63270
  // src/channels/response-mode-config.ts
62923
- function getEditableConfigPath4() {
63271
+ function getEditableConfigPath5() {
62924
63272
  return process.env.CLISBOT_CONFIG_PATH;
62925
63273
  }
62926
63274
  async function getConversationResponseMode(params) {
62927
- const { config } = await readEditableConfig(getEditableConfigPath4());
63275
+ const { config } = await readEditableConfig(getEditableConfigPath5());
62928
63276
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", buildConfiguredTargetFromIdentity(params.identity));
62929
63277
  return {
62930
63278
  label: target.label,
@@ -62932,7 +63280,7 @@ async function getConversationResponseMode(params) {
62932
63280
  };
62933
63281
  }
62934
63282
  async function setConversationResponseMode(params) {
62935
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63283
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62936
63284
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", buildConfiguredTargetFromIdentity(params.identity));
62937
63285
  target.set(params.responseMode);
62938
63286
  await writeEditableConfig(configPath, config);
@@ -62943,7 +63291,7 @@ async function setConversationResponseMode(params) {
62943
63291
  };
62944
63292
  }
62945
63293
  async function getConfiguredResponseMode(params) {
62946
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63294
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62947
63295
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", params);
62948
63296
  return {
62949
63297
  configPath,
@@ -62952,7 +63300,7 @@ async function getConfiguredResponseMode(params) {
62952
63300
  };
62953
63301
  }
62954
63302
  async function setConfiguredResponseMode(params) {
62955
- const { config, configPath } = await readEditableConfig(getEditableConfigPath4());
63303
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62956
63304
  const target = resolveConfiguredSurfaceModeTarget(config, "responseMode", params);
62957
63305
  target.set(params.responseMode);
62958
63306
  await writeEditableConfig(configPath, config);
@@ -62978,7 +63326,7 @@ async function runChannelPrivilegeCli(_args) {
62978
63326
 
62979
63327
  // src/control/channels-cli.ts
62980
63328
  var AUTH_USER_GUIDE_DOC_PATH = "docs/user-guide/auth-and-roles.md";
62981
- function getEditableConfigPath5() {
63329
+ function getEditableConfigPath6() {
62982
63330
  return process.env.CLISBOT_CONFIG_PATH;
62983
63331
  }
62984
63332
  function renderChannelsHelp() {
@@ -63127,7 +63475,7 @@ function getAgentId(args) {
63127
63475
  return parseOptionValue2(args, "--agent") ?? "default";
63128
63476
  }
63129
63477
  async function setChannelEnabled(action, channel) {
63130
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63478
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63131
63479
  const enabled = action === "enable";
63132
63480
  const current = config.channels[channel].enabled;
63133
63481
  if (current === enabled) {
@@ -63147,7 +63495,7 @@ async function addTelegramGroup(args) {
63147
63495
  if (!chatId) {
63148
63496
  throw new Error("Usage: clisbot channels add telegram-group <chatId> [--topic <topicId>] [--agent <id>] [--require-mention true|false]");
63149
63497
  }
63150
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63498
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63151
63499
  const topicId = parseOptionValue2(args, "--topic");
63152
63500
  const agentId = getAgentId(args);
63153
63501
  const requireMention = parseBooleanOption(args, "--require-mention", true);
@@ -63200,7 +63548,7 @@ async function removeTelegramGroup(args) {
63200
63548
  if (!chatId) {
63201
63549
  throw new Error("Usage: clisbot channels remove telegram-group <chatId> [--topic <topicId>]");
63202
63550
  }
63203
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63551
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63204
63552
  const topicId = parseOptionValue2(args, "--topic");
63205
63553
  const groupRoute = config.channels.telegram.groups[chatId];
63206
63554
  if (!groupRoute) {
@@ -63230,7 +63578,7 @@ async function addSlackRoute(kind, args) {
63230
63578
  if (!routeId) {
63231
63579
  throw new Error(`Usage: clisbot channels add slack-${kind} <${kind}Id> [--agent <id>] [--require-mention true|false]`);
63232
63580
  }
63233
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63581
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63234
63582
  const agentId = getAgentId(args);
63235
63583
  const requireMention = parseBooleanOption(args, "--require-mention", false);
63236
63584
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
@@ -63254,7 +63602,7 @@ async function removeSlackRoute(kind, args) {
63254
63602
  if (!routeId) {
63255
63603
  throw new Error(`Usage: clisbot channels remove slack-${kind} <${kind}Id>`);
63256
63604
  }
63257
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63605
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63258
63606
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
63259
63607
  if (!target[routeId]) {
63260
63608
  console.log(`slack ${kind} route ${routeId} is not configured`);
@@ -63267,7 +63615,7 @@ async function removeSlackRoute(kind, args) {
63267
63615
  console.log(`config: ${configPath}`);
63268
63616
  }
63269
63617
  async function setToken(target, value) {
63270
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63618
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63271
63619
  if (target === "slack-app") {
63272
63620
  config.channels.slack.appToken = value;
63273
63621
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63299,7 +63647,7 @@ async function setToken(target, value) {
63299
63647
  console.log(`config: ${configPath}`);
63300
63648
  }
63301
63649
  async function clearToken(target) {
63302
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63650
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63303
63651
  if (target === "slack-app") {
63304
63652
  config.channels.slack.appToken = "";
63305
63653
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -64038,7 +64386,7 @@ class SessionStore {
64038
64386
  }
64039
64387
 
64040
64388
  // src/control/loops-cli.ts
64041
- function getEditableConfigPath6() {
64389
+ function getEditableConfigPath7() {
64042
64390
  return process.env.CLISBOT_CONFIG_PATH;
64043
64391
  }
64044
64392
  function renderLoopsHelp() {
@@ -64096,7 +64444,7 @@ function getSessionState(sessionStorePath) {
64096
64444
  return new AgentSessionState(new SessionStore(sessionStorePath));
64097
64445
  }
64098
64446
  async function loadLoopControlState() {
64099
- const configPath = await ensureEditableConfigFile(getEditableConfigPath6());
64447
+ const configPath = await ensureEditableConfigFile(getEditableConfigPath7());
64100
64448
  const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
64101
64449
  const sessionStorePath = resolveSessionStorePath(loadedConfig);
64102
64450
  return {
@@ -64272,6 +64620,9 @@ function buildReplyCommand(params) {
64272
64620
  const lines = [`${params.command} message send \\`];
64273
64621
  if (params.identity.platform === "slack") {
64274
64622
  lines.push(" --channel slack \\");
64623
+ if (params.identity.accountId) {
64624
+ lines.push(` --account ${params.identity.accountId} \\`);
64625
+ }
64275
64626
  lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
64276
64627
  if (params.identity.threadTs) {
64277
64628
  lines.push(` --thread-id ${params.identity.threadTs} \\`);
@@ -64286,6 +64637,9 @@ function buildReplyCommand(params) {
64286
64637
  `);
64287
64638
  }
64288
64639
  lines.push(" --channel telegram \\");
64640
+ if (params.identity.accountId) {
64641
+ lines.push(` --account ${params.identity.accountId} \\`);
64642
+ }
64289
64643
  lines.push(` --target ${params.identity.chatId ?? ""} \\`);
64290
64644
  if (params.identity.topicId) {
64291
64645
  lines.push(` --thread-id ${params.identity.topicId} \\`);
@@ -64300,6 +64654,57 @@ function buildReplyCommand(params) {
64300
64654
  `);
64301
64655
  }
64302
64656
 
64657
+ // src/channels/surface-notifications.ts
64658
+ function sanitizeInlineCode(text) {
64659
+ return text.replaceAll("`", "'");
64660
+ }
64661
+ function summarizeSurfaceNotificationText(text, maxLength = 60) {
64662
+ const singleLine = sanitizeInlineCode(text.replace(/\s+/g, " ").trim());
64663
+ if (!singleLine) {
64664
+ return "(empty)";
64665
+ }
64666
+ if (singleLine.length <= maxLength) {
64667
+ return singleLine;
64668
+ }
64669
+ return `${singleLine.slice(0, maxLength - 3)}...`;
64670
+ }
64671
+ function renderQueueStartNotification(params) {
64672
+ if (params.mode === "none") {
64673
+ return;
64674
+ }
64675
+ const summary = summarizeSurfaceNotificationText(params.promptSummary);
64676
+ if (params.mode === "full") {
64677
+ return `Queued message is now running for agent \`${params.agentId}\`: \`${summary}\`.`;
64678
+ }
64679
+ return `Queued message is now running: \`${summary}\`.`;
64680
+ }
64681
+ function renderLoopScheduleSegment(params) {
64682
+ if (params.kind === "calendar") {
64683
+ return formatCalendarLoopSchedule({
64684
+ cadence: params.cadence,
64685
+ dayOfWeek: params.dayOfWeek,
64686
+ localTime: params.localTime
64687
+ });
64688
+ }
64689
+ return `every ${formatLoopIntervalShort(params.intervalMs)}`;
64690
+ }
64691
+ function renderLoopStartNotification(params) {
64692
+ if (params.mode === "none") {
64693
+ return;
64694
+ }
64695
+ const summary = summarizeSurfaceNotificationText(params.promptSummary);
64696
+ const segments = [
64697
+ `\`${summary}\``,
64698
+ renderLoopScheduleSegment(params),
64699
+ `next run \`${new Date(params.nextRunAt).toISOString()}\``,
64700
+ `remaining \`${params.remainingRuns}/${params.maxRuns}\``
64701
+ ];
64702
+ if (params.mode === "full") {
64703
+ return `Loop \`${params.loopId}\` is now running for agent \`${params.agentId}\`: ${segments.join(" · ")}.`;
64704
+ }
64705
+ return `Loop \`${params.loopId}\` is now running: ${segments.join(" · ")}.`;
64706
+ }
64707
+
64303
64708
  // src/agents/session-key.ts
64304
64709
  var DEFAULT_MAIN_KEY = "main";
64305
64710
  var DEFAULT_ACCOUNT_ID = "default";
@@ -66583,6 +66988,7 @@ class AgentService {
66583
66988
  cleanupTimer;
66584
66989
  loopTimers = new Set;
66585
66990
  intervalLoops = new Map;
66991
+ surfaceNotificationHandlers = new Map;
66586
66992
  constructor(loadedConfig, deps = {}) {
66587
66993
  this.loadedConfig = loadedConfig;
66588
66994
  this.tmuxClient = deps.tmux ?? new TmuxClient(this.loadedConfig.raw.tmux.socketPath);
@@ -66611,7 +67017,9 @@ class AgentService {
66611
67017
  }
66612
67018
  await this.runnerSessions.runSessionCleanup();
66613
67019
  this.cleanupTimer = setInterval(() => {
66614
- this.runnerSessions.runSessionCleanup();
67020
+ this.runnerSessions.runSessionCleanup().catch((error) => {
67021
+ console.error("session cleanup failed", error);
67022
+ });
66615
67023
  }, cleanup.intervalMinutes * 60000);
66616
67024
  }
66617
67025
  async stop() {
@@ -66635,6 +67043,12 @@ class AgentService {
66635
67043
  async cleanupStaleSessions() {
66636
67044
  await this.runnerSessions.runSessionCleanup();
66637
67045
  }
67046
+ registerSurfaceNotificationHandler(params) {
67047
+ this.surfaceNotificationHandlers.set(this.getSurfaceNotificationHandlerKey(params.platform, params.accountId), params.handler);
67048
+ }
67049
+ unregisterSurfaceNotificationHandler(params) {
67050
+ this.surfaceNotificationHandlers.delete(this.getSurfaceNotificationHandlerKey(params.platform, params.accountId));
67051
+ }
66638
67052
  resolveTarget(target) {
66639
67053
  return resolveAgentTarget(this.loadedConfig, target);
66640
67054
  }
@@ -66743,7 +67157,9 @@ class AgentService {
66743
67157
  target: params.target,
66744
67158
  loop
66745
67159
  });
66746
- await this.runIntervalLoopIteration(loop.id);
67160
+ await this.runIntervalLoopIteration(loop.id, {
67161
+ notifyStart: false
67162
+ });
66747
67163
  return this.getIntervalLoop(loop.id);
66748
67164
  }
66749
67165
  async createCalendarLoop(params) {
@@ -66905,7 +67321,7 @@ class AgentService {
66905
67321
  }
66906
67322
  this.intervalLoops.delete(loopId);
66907
67323
  }
66908
- async runIntervalLoopIteration(loopId) {
67324
+ async runIntervalLoopIteration(loopId, options = {}) {
66909
67325
  const managed = this.intervalLoops.get(loopId);
66910
67326
  if (!managed) {
66911
67327
  return;
@@ -66941,6 +67357,9 @@ class AgentService {
66941
67357
  this.dropManagedIntervalLoop(loopId);
66942
67358
  return;
66943
67359
  }
67360
+ if (options.notifyStart !== false) {
67361
+ await this.notifyManagedLoopStart(managed.target, nextLoopState);
67362
+ }
66944
67363
  const promptText = this.buildManagedLoopPrompt(managed.target.agentId, nextLoopState);
66945
67364
  const { result } = this.enqueuePrompt(managed.target, promptText, {
66946
67365
  observerId: `loop:${loopId}:${attemptedRuns}`,
@@ -66960,6 +67379,101 @@ class AgentService {
66960
67379
  }
66961
67380
  this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
66962
67381
  }
67382
+ getSurfaceNotificationHandlerKey(platform, accountId) {
67383
+ return `${platform}:${accountId?.trim() || "default"}`;
67384
+ }
67385
+ async notifySurface(request) {
67386
+ const handler = this.surfaceNotificationHandlers.get(this.getSurfaceNotificationHandlerKey(request.binding.platform, request.binding.accountId));
67387
+ if (!handler) {
67388
+ return;
67389
+ }
67390
+ await handler(request);
67391
+ }
67392
+ resolveLoopSurfaceNotifications(identity) {
67393
+ if (identity.platform === "slack") {
67394
+ const channelConfig2 = this.loadedConfig.raw.channels.slack;
67395
+ let resolved2 = {
67396
+ queueStart: channelConfig2.surfaceNotifications?.queueStart ?? "brief",
67397
+ loopStart: channelConfig2.surfaceNotifications?.loopStart ?? "brief"
67398
+ };
67399
+ if (identity.conversationKind === "dm") {
67400
+ return {
67401
+ ...resolved2,
67402
+ ...channelConfig2.directMessages.surfaceNotifications ?? {}
67403
+ };
67404
+ }
67405
+ const routeCollection = identity.conversationKind === "group" ? channelConfig2.groups : channelConfig2.channels;
67406
+ const route = identity.channelId ? routeCollection[identity.channelId] ?? routeCollection["*"] : undefined;
67407
+ return {
67408
+ ...resolved2,
67409
+ ...route?.surfaceNotifications ?? {}
67410
+ };
67411
+ }
67412
+ const channelConfig = this.loadedConfig.raw.channels.telegram;
67413
+ let resolved = {
67414
+ queueStart: channelConfig.surfaceNotifications?.queueStart ?? "brief",
67415
+ loopStart: channelConfig.surfaceNotifications?.loopStart ?? "brief"
67416
+ };
67417
+ if (identity.conversationKind === "dm") {
67418
+ return {
67419
+ ...resolved,
67420
+ ...channelConfig.directMessages.surfaceNotifications ?? {}
67421
+ };
67422
+ }
67423
+ const groupRoute = identity.chatId ? channelConfig.groups[identity.chatId] ?? channelConfig.groups["*"] : undefined;
67424
+ resolved = {
67425
+ ...resolved,
67426
+ ...groupRoute?.surfaceNotifications ?? {}
67427
+ };
67428
+ if (identity.conversationKind === "topic" && identity.topicId) {
67429
+ return {
67430
+ ...resolved,
67431
+ ...groupRoute?.topics?.[identity.topicId]?.surfaceNotifications ?? {}
67432
+ };
67433
+ }
67434
+ return resolved;
67435
+ }
67436
+ async notifyManagedLoopStart(target, loop) {
67437
+ if (!loop.surfaceBinding) {
67438
+ return;
67439
+ }
67440
+ const identity = this.buildLoopChannelIdentity(loop.surfaceBinding);
67441
+ const notifications = this.resolveLoopSurfaceNotifications(identity);
67442
+ const text = loop.kind === "calendar" ? renderLoopStartNotification({
67443
+ mode: notifications.loopStart,
67444
+ agentId: target.agentId,
67445
+ loopId: loop.id,
67446
+ promptSummary: loop.promptSummary,
67447
+ cadence: loop.cadence,
67448
+ dayOfWeek: loop.dayOfWeek,
67449
+ localTime: loop.localTime,
67450
+ timezone: loop.timezone,
67451
+ nextRunAt: loop.nextRunAt,
67452
+ remainingRuns: Math.max(0, loop.maxRuns - loop.attemptedRuns),
67453
+ maxRuns: loop.maxRuns,
67454
+ kind: "calendar"
67455
+ }) : renderLoopStartNotification({
67456
+ mode: notifications.loopStart,
67457
+ agentId: target.agentId,
67458
+ loopId: loop.id,
67459
+ promptSummary: loop.promptSummary,
67460
+ intervalMs: loop.intervalMs,
67461
+ nextRunAt: loop.nextRunAt,
67462
+ remainingRuns: Math.max(0, loop.maxRuns - loop.attemptedRuns),
67463
+ maxRuns: loop.maxRuns
67464
+ });
67465
+ if (!text) {
67466
+ return;
67467
+ }
67468
+ try {
67469
+ await this.notifySurface({
67470
+ binding: loop.surfaceBinding,
67471
+ text
67472
+ });
67473
+ } catch (error) {
67474
+ console.error("loop start notification failed", error);
67475
+ }
67476
+ }
66963
67477
  scheduleIntervalLoopTimer(loopId, delayMs) {
66964
67478
  const managed = this.intervalLoops.get(loopId);
66965
67479
  if (!managed) {
@@ -66976,7 +67490,9 @@ class AgentService {
66976
67490
  return;
66977
67491
  }
66978
67492
  current.timer = undefined;
66979
- this.runIntervalLoopIteration(loopId).catch((error) => {
67493
+ this.runIntervalLoopIteration(loopId, {
67494
+ notifyStart: true
67495
+ }).catch((error) => {
66980
67496
  if (this.shouldSuppressLoopShutdownError(error)) {
66981
67497
  return;
66982
67498
  }
@@ -67034,6 +67550,7 @@ class AgentService {
67034
67550
  buildLoopChannelIdentity(binding) {
67035
67551
  return {
67036
67552
  platform: binding.platform,
67553
+ accountId: binding.accountId,
67037
67554
  conversationKind: binding.conversationKind,
67038
67555
  channelId: binding.channelId,
67039
67556
  chatId: binding.chatId,
@@ -67649,11 +68166,11 @@ function formatChannelFollowUpStatus(params) {
67649
68166
  }
67650
68167
 
67651
68168
  // src/channels/streaming-config.ts
67652
- function getEditableConfigPath7() {
68169
+ function getEditableConfigPath8() {
67653
68170
  return process.env.CLISBOT_CONFIG_PATH;
67654
68171
  }
67655
68172
  async function getConversationStreaming(params) {
67656
- const { config } = await readEditableConfig(getEditableConfigPath7());
68173
+ const { config } = await readEditableConfig(getEditableConfigPath8());
67657
68174
  const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67658
68175
  return {
67659
68176
  label: target.label,
@@ -67661,7 +68178,7 @@ async function getConversationStreaming(params) {
67661
68178
  };
67662
68179
  }
67663
68180
  async function setConversationStreaming(params) {
67664
- const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
68181
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
67665
68182
  const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67666
68183
  target.set(params.streaming);
67667
68184
  await writeEditableConfig(configPath, config);
@@ -67742,7 +68259,7 @@ function renderRouteStatusMessage(params) {
67742
68259
  if (params.identity.topicId) {
67743
68260
  lines.push(`topicId: \`${params.identity.topicId}\``);
67744
68261
  }
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}\``);
68262
+ 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
68263
  if (params.runtimeState.startedAt) {
67747
68264
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
67748
68265
  }
@@ -67979,6 +68496,7 @@ async function resolveLoopPromptText(params) {
67979
68496
  function buildLoopSurfaceBinding(identity) {
67980
68497
  return {
67981
68498
  platform: identity.platform,
68499
+ accountId: identity.accountId,
67982
68500
  conversationKind: identity.conversationKind,
67983
68501
  channelId: identity.channelId,
67984
68502
  chatId: identity.chatId,
@@ -67995,6 +68513,8 @@ async function executePromptDelivery(params) {
67995
68513
  let loggedFirstRunningUpdate = false;
67996
68514
  let activePreviewStartedAt;
67997
68515
  let messageToolPreviewHandedOff = false;
68516
+ let queueStartPending = false;
68517
+ let deferredQueueStartPreview = false;
67998
68518
  const paneManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67999
68519
  const messageToolPreview = params.route.responseMode === "message-tool" && params.forceQueuedDelivery !== true && params.route.streaming !== "off";
68000
68520
  const previewEnabled = params.route.streaming !== "off" && (paneManagedDelivery || messageToolPreview);
@@ -68069,6 +68589,68 @@ async function executePromptDelivery(params) {
68069
68589
  agentId: params.route.agentId,
68070
68590
  sessionKey: params.sessionTarget.sessionKey
68071
68591
  });
68592
+ async function maybeRenderQueueStartNotification() {
68593
+ if (!queueStartPending) {
68594
+ return false;
68595
+ }
68596
+ const text = renderQueueStartNotification({
68597
+ mode: params.queueStartMode ?? "none",
68598
+ agentId: params.route.agentId,
68599
+ promptSummary: params.notificationPromptSummary ?? summarizeSurfaceNotificationText(params.promptText)
68600
+ });
68601
+ if (!text) {
68602
+ queueStartPending = false;
68603
+ return false;
68604
+ }
68605
+ if (previewEnabled && responseChunks.length === 0) {
68606
+ deferredQueueStartPreview = true;
68607
+ return true;
68608
+ }
68609
+ queueStartPending = false;
68610
+ if (responseChunks.length > 0) {
68611
+ const postedNew = await renderResponseText(text);
68612
+ if (postedNew) {
68613
+ await recordVisibleReply("reply", "channel");
68614
+ activePreviewStartedAt = Date.now();
68615
+ }
68616
+ renderedState = {
68617
+ text,
68618
+ body: ""
68619
+ };
68620
+ return true;
68621
+ }
68622
+ const posted = await params.postText(text);
68623
+ if (posted.length > 0) {
68624
+ await recordVisibleReply("reply", "channel");
68625
+ }
68626
+ return posted.length > 0;
68627
+ }
68628
+ function buildInitialPlaceholderText(positionAhead) {
68629
+ if (deferredQueueStartPreview && queueStartPending) {
68630
+ deferredQueueStartPreview = false;
68631
+ queueStartPending = false;
68632
+ return renderQueueStartNotification({
68633
+ mode: params.queueStartMode ?? "none",
68634
+ agentId: params.route.agentId,
68635
+ promptSummary: params.notificationPromptSummary ?? summarizeSurfaceNotificationText(params.promptText)
68636
+ }) ?? renderPlatformInteraction({
68637
+ platform: params.identity.platform,
68638
+ status: positionAhead > 0 ? "queued" : "running",
68639
+ content: "",
68640
+ queuePosition: positionAhead,
68641
+ maxChars: Number.POSITIVE_INFINITY,
68642
+ note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
68643
+ });
68644
+ }
68645
+ return renderPlatformInteraction({
68646
+ platform: params.identity.platform,
68647
+ status: positionAhead > 0 ? "queued" : "running",
68648
+ content: "",
68649
+ queuePosition: positionAhead,
68650
+ maxChars: Number.POSITIVE_INFINITY,
68651
+ note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
68652
+ });
68653
+ }
68072
68654
  try {
68073
68655
  const { positionAhead, result } = params.agentService.enqueuePrompt(params.sessionTarget, params.promptText, {
68074
68656
  observerId: params.observerId,
@@ -68077,9 +68659,6 @@ async function executePromptDelivery(params) {
68077
68659
  if (!paneManagedDelivery && !messageToolPreview) {
68078
68660
  return;
68079
68661
  }
68080
- if (params.route.streaming === "off" && update.status === "running") {
68081
- return;
68082
- }
68083
68662
  if (update.status === "running" && !loggedFirstRunningUpdate) {
68084
68663
  loggedFirstRunningUpdate = true;
68085
68664
  logLatencyDebug("channel-first-running-update", params.timingContext, {
@@ -68088,14 +68667,21 @@ async function executePromptDelivery(params) {
68088
68667
  });
68089
68668
  }
68090
68669
  await (renderChain = renderChain.then(async () => {
68670
+ let renderedQueueStart = false;
68671
+ if (update.status === "running") {
68672
+ renderedQueueStart = await maybeRenderQueueStartNotification();
68673
+ }
68674
+ if (params.route.streaming === "off" && update.status === "running") {
68675
+ return;
68676
+ }
68677
+ if (renderedQueueStart) {
68678
+ return;
68679
+ }
68091
68680
  const signals = await getMessageToolRuntimeSignals();
68092
68681
  if (messageToolPreview && typeof activePreviewStartedAt === "number" && (typeof signals.messageToolFinalReplyAt === "number" && signals.messageToolFinalReplyAt >= activePreviewStartedAt || typeof signals.lastMessageToolReplyAt === "number" && signals.lastMessageToolReplyAt >= activePreviewStartedAt)) {
68093
68682
  await handoffMessageToolPreview();
68094
68683
  return;
68095
68684
  }
68096
- if (messageToolPreview) {
68097
- return;
68098
- }
68099
68685
  const nextState2 = buildRenderedMessageState({
68100
68686
  platform: params.identity.platform,
68101
68687
  status: update.status,
@@ -68119,15 +68705,9 @@ async function executePromptDelivery(params) {
68119
68705
  }));
68120
68706
  }
68121
68707
  });
68708
+ queueStartPending = positionAhead > 0 && (params.queueStartMode ?? "none") !== "none";
68122
68709
  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
- });
68710
+ const placeholderText = buildInitialPlaceholderText(positionAhead);
68131
68711
  const postedNew2 = await renderResponseText(placeholderText);
68132
68712
  if (postedNew2) {
68133
68713
  await recordVisibleReply("reply", "channel");
@@ -68157,6 +68737,7 @@ async function executePromptDelivery(params) {
68157
68737
  }
68158
68738
  const finalResult = await result;
68159
68739
  await renderChain;
68740
+ await maybeRenderQueueStartNotification();
68160
68741
  if (!paneManagedDelivery && messageToolPreviewHandedOff) {
68161
68742
  return;
68162
68743
  }
@@ -68641,6 +69222,8 @@ ${renderLoopUsage()}`);
68641
69222
  route: params.route,
68642
69223
  maxChars: params.maxChars,
68643
69224
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
69225
+ queueStartMode: params.route.surfaceNotifications.queueStart,
69226
+ notificationPromptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
68644
69227
  postText: params.postText,
68645
69228
  reconcileText: params.reconcileText,
68646
69229
  observerId: `${observerId}:loop:${index + 1}`
@@ -68781,6 +69364,8 @@ ${escapeCodeFence(shellResult.output)}
68781
69364
  route: params.route,
68782
69365
  maxChars: params.maxChars,
68783
69366
  promptText: delayedPromptText,
69367
+ queueStartMode: params.route.surfaceNotifications.queueStart,
69368
+ notificationPromptSummary: explicitQueueMessage ?? summarizeSurfaceNotificationText(params.text),
68784
69369
  postText: params.postText,
68785
69370
  reconcileText: params.reconcileText,
68786
69371
  observerId,
@@ -68841,6 +69426,23 @@ function isTelegramSenderAllowed(params) {
68841
69426
  }
68842
69427
 
68843
69428
  // src/auth/resolve.ts
69429
+ function mergeRoleDefinitions2(inherited, override) {
69430
+ return {
69431
+ allow: override?.allow ?? inherited?.allow ?? [],
69432
+ users: override?.users ?? inherited?.users ?? []
69433
+ };
69434
+ }
69435
+ function mergeRoleRecord2(defaults, overrides) {
69436
+ const merged = {};
69437
+ const roleNames = new Set([
69438
+ ...Object.keys(defaults ?? {}),
69439
+ ...Object.keys(overrides ?? {})
69440
+ ]);
69441
+ for (const roleName of roleNames) {
69442
+ merged[roleName] = mergeRoleDefinitions2(defaults?.[roleName], overrides?.[roleName]);
69443
+ }
69444
+ return merged;
69445
+ }
68844
69446
  function normalizePrincipal(principal) {
68845
69447
  const trimmed = principal.trim();
68846
69448
  if (!trimmed) {
@@ -68888,10 +69490,7 @@ function getAgentAuth(config, agentId) {
68888
69490
  const override = entry?.auth;
68889
69491
  return {
68890
69492
  defaultRole: override?.defaultRole ?? defaults.defaultRole,
68891
- roles: {
68892
- ...defaults.roles,
68893
- ...override?.roles ?? {}
68894
- }
69493
+ roles: mergeRoleRecord2(defaults.roles, override?.roles)
68895
69494
  };
68896
69495
  }
68897
69496
  function getAllowedPermissions(roles, role) {
@@ -69014,6 +69613,10 @@ function buildSharedChannelRoute(params) {
69014
69613
  response: params.route?.response ?? params.channelConfig.response,
69015
69614
  responseMode: params.route?.responseMode ?? agentEntry?.responseMode ?? params.channelConfig.responseMode,
69016
69615
  additionalMessageMode: params.route?.additionalMessageMode ?? agentEntry?.additionalMessageMode ?? params.channelConfig.additionalMessageMode,
69616
+ surfaceNotifications: {
69617
+ queueStart: params.route?.surfaceNotifications?.queueStart ?? params.channelConfig.surfaceNotifications?.queueStart ?? "brief",
69618
+ loopStart: params.route?.surfaceNotifications?.loopStart ?? params.channelConfig.surfaceNotifications?.loopStart ?? "brief"
69619
+ },
69017
69620
  verbose: params.route?.verbose ?? params.channelConfig.verbose,
69018
69621
  followUp: {
69019
69622
  mode: params.route?.followUp?.mode ?? params.channelConfig.followUp.mode,
@@ -69990,6 +70593,20 @@ class SlackSocketService {
69990
70593
  appToken: this.accountConfig.appToken,
69991
70594
  socketMode: true
69992
70595
  });
70596
+ this.agentService.registerSurfaceNotificationHandler({
70597
+ platform: "slack",
70598
+ accountId: this.accountId,
70599
+ handler: async ({ binding, text }) => {
70600
+ if (!binding.channelId) {
70601
+ return;
70602
+ }
70603
+ await postSlackText(this.app.client, {
70604
+ channel: binding.channelId,
70605
+ threadTs: binding.threadTs,
70606
+ text
70607
+ });
70608
+ }
70609
+ });
69993
70610
  this.registerEvents();
69994
70611
  }
69995
70612
  getSlackMaxChars(agentId) {
@@ -70243,6 +70860,7 @@ class SlackSocketService {
70243
70860
  const cliTool = getAgentEntry(this.loadedConfig, params.route.agentId)?.cliTool;
70244
70861
  const identity = {
70245
70862
  platform: "slack",
70863
+ accountId: this.accountId,
70246
70864
  conversationKind: params.conversationKind,
70247
70865
  senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
70248
70866
  channelId,
@@ -70426,6 +71044,10 @@ class SlackSocketService {
70426
71044
  await this.startPromise;
70427
71045
  }
70428
71046
  async stop() {
71047
+ this.agentService.unregisterSurfaceNotificationHandler({
71048
+ platform: "slack",
71049
+ accountId: this.accountId
71050
+ });
70429
71051
  await this.app.stop();
70430
71052
  }
70431
71053
  getBotUserLabel() {
@@ -71280,6 +71902,33 @@ async function resolveTelegramAttachmentPaths(params) {
71280
71902
  return attachmentPaths;
71281
71903
  }
71282
71904
 
71905
+ // src/channels/telegram/route-guidance.ts
71906
+ function renderTelegramRouteChoiceMessage(params) {
71907
+ const chatId = String(params.chatId);
71908
+ const topicId = params.topicId != null ? String(params.topicId) : undefined;
71909
+ const lines = [
71910
+ topicId != null ? "clisbot: this Telegram topic is not configured yet." : "clisbot: this Telegram group is not configured yet.",
71911
+ "",
71912
+ "Ask the bot owner to choose one of these:",
71913
+ "",
71914
+ "Add the whole group with the default agent:",
71915
+ `\`clisbot channels add telegram-group ${chatId}\``,
71916
+ "",
71917
+ "Add the whole group with a specific agent:",
71918
+ `\`clisbot channels add telegram-group ${chatId} --agent <id>\``
71919
+ ];
71920
+ if (topicId != null) {
71921
+ lines.push("", "Add only this topic with a specific agent:", `\`clisbot channels add telegram-group ${chatId} --topic ${topicId} --agent <id>\``);
71922
+ }
71923
+ if (params.includeConfigPath) {
71924
+ lines.push("", topicId != null ? `Config path: \`channels.telegram.groups."${chatId}".topics."${topicId}"\`` : `Config path: \`channels.telegram.groups."${chatId}"\``);
71925
+ } else {
71926
+ lines.push("", "After that, routed commands such as `/status`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
71927
+ }
71928
+ return lines.join(`
71929
+ `);
71930
+ }
71931
+
71283
71932
  // src/channels/telegram/typing.ts
71284
71933
  var TELEGRAM_TYPING_HEARTBEAT_MS = 4500;
71285
71934
  function logTelegramTypingError(onError, error) {
@@ -71423,6 +72072,7 @@ class TelegramPollingService {
71423
72072
  activityStore;
71424
72073
  accountId;
71425
72074
  accountConfig;
72075
+ reportLifecycle;
71426
72076
  botUserId = 0;
71427
72077
  botUsername = "";
71428
72078
  running = false;
@@ -71431,13 +72081,35 @@ class TelegramPollingService {
71431
72081
  activePollController;
71432
72082
  inFlightUpdates = new Set;
71433
72083
  processingIndicators = new ConversationProcessingIndicatorCoordinator;
71434
- constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig) {
72084
+ constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig, reportLifecycle) {
71435
72085
  this.loadedConfig = loadedConfig;
71436
72086
  this.agentService = agentService;
71437
72087
  this.processedEventsStore = processedEventsStore;
71438
72088
  this.activityStore = activityStore;
71439
72089
  this.accountId = accountId;
71440
72090
  this.accountConfig = accountConfig;
72091
+ this.reportLifecycle = reportLifecycle;
72092
+ this.agentService.registerSurfaceNotificationHandler({
72093
+ platform: "telegram",
72094
+ accountId: this.accountId,
72095
+ handler: async ({ binding, text }) => {
72096
+ if (!binding.chatId) {
72097
+ return;
72098
+ }
72099
+ const chatId = Number(binding.chatId);
72100
+ if (!Number.isFinite(chatId)) {
72101
+ return;
72102
+ }
72103
+ const topicId = binding.topicId ? Number(binding.topicId) : undefined;
72104
+ await postTelegramText({
72105
+ token: this.accountConfig.botToken,
72106
+ chatId,
72107
+ text,
72108
+ topicId: Number.isFinite(topicId) ? topicId : undefined,
72109
+ omitThreadId: shouldOmitTelegramThreadId(Number.isFinite(topicId) ? topicId : undefined)
72110
+ });
72111
+ }
72112
+ });
71441
72113
  }
71442
72114
  async start() {
71443
72115
  const me = await callTelegramApi(this.accountConfig.botToken, "getMe", {});
@@ -71454,6 +72126,10 @@ class TelegramPollingService {
71454
72126
  this.activePollController?.abort();
71455
72127
  await this.loopPromise;
71456
72128
  await Promise.allSettled([...this.inFlightUpdates]);
72129
+ this.agentService.unregisterSurfaceNotificationHandler({
72130
+ platform: "telegram",
72131
+ accountId: this.accountId
72132
+ });
71457
72133
  }
71458
72134
  getBotLabel() {
71459
72135
  return this.botUsername ? `@${this.botUsername}` : `${this.botUserId || "unknown"}`;
@@ -71500,6 +72176,15 @@ class TelegramPollingService {
71500
72176
  }
71501
72177
  if (isTelegramPollingConflict(error)) {
71502
72178
  this.running = false;
72179
+ await this.reportLifecycle?.({
72180
+ connection: "failed",
72181
+ summary: "Telegram polling stopped because another instance is already using this bot token.",
72182
+ detail: error instanceof Error ? error.message : String(error),
72183
+ actions: [
72184
+ "stop the other Telegram poller that is using the same bot token",
72185
+ "run `clisbot start` again after the token is no longer in use elsewhere"
72186
+ ]
72187
+ });
71503
72188
  console.error("telegram polling stopped: another bot instance is already calling getUpdates for this token");
71504
72189
  return;
71505
72190
  }
@@ -71667,6 +72352,7 @@ class TelegramPollingService {
71667
72352
  const senderName = [message.from?.first_name, message.from?.last_name].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").trim();
71668
72353
  const identity = {
71669
72354
  platform: "telegram",
72355
+ accountId: this.accountId,
71670
72356
  conversationKind: routeInfo.conversationKind,
71671
72357
  senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
71672
72358
  senderName: senderName || message.from?.username?.trim() || undefined,
@@ -72079,7 +72765,7 @@ var telegramChannelPlugin = {
72079
72765
  accountId,
72080
72766
  config
72081
72767
  })),
72082
- createRuntimeService: (context, account) => new TelegramPollingService(context.loadedConfig, context.agentService, context.processedEventsStore, context.activityStore, account.accountId, account.config),
72768
+ createRuntimeService: (context, account) => new TelegramPollingService(context.loadedConfig, context.agentService, context.processedEventsStore, context.activityStore, account.accountId, account.config, context.reportLifecycle),
72083
72769
  renderHealthSummary: (state) => {
72084
72770
  switch (state) {
72085
72771
  case "starting":
@@ -72173,7 +72859,7 @@ var defaultMessageCliDependencies = {
72173
72859
  await agentService.recordConversationReply(target, kind, source);
72174
72860
  }
72175
72861
  };
72176
- function parseRepeatedOption2(args, name) {
72862
+ function parseRepeatedOption3(args, name) {
72177
72863
  const values = [];
72178
72864
  for (let index = 0;index < args.length; index += 1) {
72179
72865
  if (args[index] !== name) {
@@ -72188,7 +72874,7 @@ function parseRepeatedOption2(args, name) {
72188
72874
  return values;
72189
72875
  }
72190
72876
  function parseOptionValue4(args, name) {
72191
- const values = parseRepeatedOption2(args, name);
72877
+ const values = parseRepeatedOption3(args, name);
72192
72878
  return values.length > 0 ? values.at(-1) : undefined;
72193
72879
  }
72194
72880
  function parseIntegerOption(args, name) {
@@ -72202,7 +72888,7 @@ function parseIntegerOption(args, name) {
72202
72888
  }
72203
72889
  return parsed;
72204
72890
  }
72205
- function hasFlag3(args, name) {
72891
+ function hasFlag4(args, name) {
72206
72892
  return args.includes(name);
72207
72893
  }
72208
72894
  function resolveReplyKind(command) {
@@ -72234,18 +72920,18 @@ function parseMessageCommand(args) {
72234
72920
  media: parseOptionValue4(rest, "--media"),
72235
72921
  messageId: parseOptionValue4(rest, "--message-id"),
72236
72922
  emoji: parseOptionValue4(rest, "--emoji"),
72237
- remove: hasFlag3(rest, "--remove"),
72923
+ remove: hasFlag4(rest, "--remove"),
72238
72924
  threadId: parseOptionValue4(rest, "--thread-id"),
72239
72925
  replyTo: parseOptionValue4(rest, "--reply-to"),
72240
72926
  limit: parseIntegerOption(rest, "--limit"),
72241
72927
  query: parseOptionValue4(rest, "--query"),
72242
72928
  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")
72929
+ pollOptions: parseRepeatedOption3(rest, "--poll-option"),
72930
+ forceDocument: hasFlag4(rest, "--force-document"),
72931
+ silent: hasFlag4(rest, "--silent"),
72932
+ progress: hasFlag4(rest, "--progress"),
72933
+ final: hasFlag4(rest, "--final"),
72934
+ json: hasFlag4(rest, "--json")
72249
72935
  };
72250
72936
  }
72251
72937
  function renderMessageHelp() {
@@ -72468,6 +73154,7 @@ class RuntimeSupervisor {
72468
73154
  reloadInFlight = false;
72469
73155
  reloadRequested = false;
72470
73156
  configWatchDebounceMs = 250;
73157
+ nextRuntimeId = 1;
72471
73158
  dependencies;
72472
73159
  constructor(configPath, dependencies) {
72473
73160
  this.configPath = configPath;
@@ -72484,15 +73171,50 @@ class RuntimeSupervisor {
72484
73171
  async start() {
72485
73172
  await this.reload("initial");
72486
73173
  }
72487
- async stop() {
73174
+ async stop(options = {}) {
72488
73175
  this.clearReloadTimer();
72489
73176
  this.stopWatchingConfig();
72490
73177
  await this.stopActiveRuntime();
73178
+ if (options.markChannelsStopped !== false) {
73179
+ for (const plugin of this.dependencies.listChannelPlugins()) {
73180
+ await this.dependencies.runtimeHealthStore.setChannel({
73181
+ channel: plugin.id,
73182
+ connection: "stopped",
73183
+ summary: plugin.renderHealthSummary("stopped")
73184
+ });
73185
+ }
73186
+ }
73187
+ }
73188
+ async markFatalFailure(error) {
73189
+ const activeRuntime = this.activeRuntime;
73190
+ if (!activeRuntime) {
73191
+ return;
73192
+ }
73193
+ const detail = error instanceof Error ? error.message : String(error);
73194
+ const instancesByChannel = new Map;
73195
+ for (const entry of activeRuntime.channelServices) {
73196
+ const identity = entry.service.getRuntimeIdentity?.();
73197
+ if (!identity) {
73198
+ continue;
73199
+ }
73200
+ const existing = instancesByChannel.get(entry.channel) ?? [];
73201
+ existing.push(identity);
73202
+ instancesByChannel.set(entry.channel, existing);
73203
+ }
72491
73204
  for (const plugin of this.dependencies.listChannelPlugins()) {
73205
+ if (!plugin.isEnabled(activeRuntime.loadedConfig)) {
73206
+ continue;
73207
+ }
72492
73208
  await this.dependencies.runtimeHealthStore.setChannel({
72493
73209
  channel: plugin.id,
72494
- connection: "stopped",
72495
- summary: plugin.renderHealthSummary("stopped")
73210
+ connection: "failed",
73211
+ summary: "Runtime crashed due to a fatal error.",
73212
+ detail,
73213
+ actions: [
73214
+ "run `clisbot logs` and inspect the fatal error",
73215
+ "fix the underlying runtime fault, then restart with `clisbot start`"
73216
+ ],
73217
+ instances: instancesByChannel.get(plugin.id) ?? []
72496
73218
  });
72497
73219
  }
72498
73220
  }
@@ -72570,6 +73292,7 @@ class RuntimeSupervisor {
72570
73292
  }
72571
73293
  }
72572
73294
  async createRuntime(loadedConfig) {
73295
+ const runtimeId = this.nextRuntimeId++;
72573
73296
  const agentService = this.dependencies.createAgentService(loadedConfig);
72574
73297
  const processedEventsStore = this.dependencies.createProcessedEventsStore(loadedConfig.processedEventsPath);
72575
73298
  const activityStore = this.dependencies.createActivityStore();
@@ -72591,7 +73314,14 @@ class RuntimeSupervisor {
72591
73314
  loadedConfig,
72592
73315
  agentService,
72593
73316
  processedEventsStore,
72594
- activityStore
73317
+ activityStore,
73318
+ reportLifecycle: (event) => this.reportChannelLifecycle({
73319
+ runtimeId,
73320
+ plugin,
73321
+ channelServices,
73322
+ accountId: account.accountId,
73323
+ event
73324
+ })
72595
73325
  }, account)
72596
73326
  });
72597
73327
  }
@@ -72622,6 +73352,8 @@ class RuntimeSupervisor {
72622
73352
  });
72623
73353
  }
72624
73354
  return {
73355
+ id: runtimeId,
73356
+ loadedConfig,
72625
73357
  agentService,
72626
73358
  channelServices
72627
73359
  };
@@ -72661,6 +73393,38 @@ class RuntimeSupervisor {
72661
73393
  });
72662
73394
  }
72663
73395
  }
73396
+ getChannelInstances(channelServices, channel) {
73397
+ return channelServices.filter((entry) => entry.channel === channel).map((entry) => entry.service.getRuntimeIdentity?.()).filter((identity) => identity != null);
73398
+ }
73399
+ async reportChannelLifecycle(params) {
73400
+ if (this.activeRuntime?.id !== params.runtimeId) {
73401
+ return;
73402
+ }
73403
+ const instances = this.getChannelInstances(params.channelServices, params.plugin.id);
73404
+ if (params.event.connection === "active") {
73405
+ await this.dependencies.runtimeHealthStore.setChannel({
73406
+ channel: params.plugin.id,
73407
+ connection: "active",
73408
+ summary: params.event.summary ?? params.plugin.renderActiveHealthSummary(Math.max(1, instances.length)),
73409
+ detail: params.event.detail,
73410
+ actions: params.event.actions,
73411
+ instances
73412
+ });
73413
+ return;
73414
+ }
73415
+ const detailPrefix = `account=${params.accountId}`;
73416
+ await this.dependencies.runtimeHealthStore.setChannel({
73417
+ channel: params.plugin.id,
73418
+ connection: "failed",
73419
+ summary: params.event.summary ?? `${params.plugin.id} channel failed after startup.`,
73420
+ detail: params.event.detail ? `${detailPrefix}; ${params.event.detail}` : detailPrefix,
73421
+ actions: params.event.actions ?? [
73422
+ "run `clisbot logs` and inspect the latest channel error",
73423
+ "restart `clisbot` after fixing the channel-level issue"
73424
+ ],
73425
+ instances
73426
+ });
73427
+ }
72664
73428
  async reconcileConfigWatcher(loadedConfig) {
72665
73429
  const configReload = loadedConfig.raw.control.configReload;
72666
73430
  this.configWatchDebounceMs = configReload.watchDebounceMs;
@@ -72851,6 +73615,11 @@ async function getRuntimeOperatorSummary(params) {
72851
73615
  ];
72852
73616
  return {
72853
73617
  loadedConfig,
73618
+ ownerSummary: {
73619
+ ownerPrincipals: loadedConfig.raw.app.auth.roles.owner?.users ?? [],
73620
+ adminPrincipals: loadedConfig.raw.app.auth.roles.admin?.users ?? [],
73621
+ ownerClaimWindowMinutes: loadedConfig.raw.app.auth.ownerClaimWindowMinutes
73622
+ },
72854
73623
  agentSummaries,
72855
73624
  channelSummaries,
72856
73625
  activeRuns: await agentService.listActiveSessionRuntimes(),
@@ -72932,11 +73701,75 @@ function renderAgentSummaryLines(summary) {
72932
73701
  })
72933
73702
  ];
72934
73703
  }
73704
+ function renderOwnerSummaryLines(summary) {
73705
+ const ownerPrincipals = summary.ownerSummary.ownerPrincipals;
73706
+ const adminPrincipals = summary.ownerSummary.adminPrincipals;
73707
+ const configuredOwners = ownerPrincipals.length > 0 ? ownerPrincipals.join(",") : "none";
73708
+ const claimWindow = `${summary.ownerSummary.ownerClaimWindowMinutes}m`;
73709
+ const lines = [
73710
+ "Owner:",
73711
+ ` - configured=${ownerPrincipals.length > 0 ? "yes" : "no"} principals=${configuredOwners} claimWindow=${claimWindow}`,
73712
+ " - access=full app control, DM pairing bypass, implicit admin across all agents/channels"
73713
+ ];
73714
+ if (adminPrincipals.length > 0) {
73715
+ lines.push(` - appAdmins=${adminPrincipals.join(",")}`);
73716
+ }
73717
+ return lines;
73718
+ }
73719
+ function hasConfiguredPrivilegedPrincipal(summary) {
73720
+ return summary.ownerSummary.ownerPrincipals.length > 0 || summary.ownerSummary.adminPrincipals.length > 0;
73721
+ }
73722
+ function renderPrivilegedChatHint(summary, action) {
73723
+ if (hasConfiguredPrivilegedPrincipal(summary)) {
73724
+ return `chat with the bot from that owner/admin account to ${action}`;
73725
+ }
73726
+ return `after you configure an owner/admin principal, use that account to ${action}`;
73727
+ }
73728
+ function appendChannelNextStepLines(lines, summary, prefix = "") {
73729
+ const slackEnabled = summary.channelSummaries.some((channel) => channel.channel === "slack" && channel.enabled);
73730
+ const telegramEnabled = summary.channelSummaries.some((channel) => channel.channel === "telegram" && channel.enabled);
73731
+ const hasEnabledChannel = slackEnabled || telegramEnabled;
73732
+ if (!hasEnabledChannel) {
73733
+ lines.push(`${prefix}- run \`clisbot channels enable <slack|telegram>\` for the first channel you want to expose`);
73734
+ return;
73735
+ }
73736
+ if (telegramEnabled && slackEnabled) {
73737
+ lines.push(`${prefix}- DM the Telegram or Slack bot first to confirm it responds normally`);
73738
+ } else if (telegramEnabled) {
73739
+ lines.push(`${prefix}- DM the Telegram bot first to confirm it responds normally`);
73740
+ } else {
73741
+ lines.push(`${prefix}- DM the Slack bot first to confirm it responds normally`);
73742
+ }
73743
+ lines.push(`${prefix}- after DM works, add the bot to the target Slack channel or Telegram group/topic`);
73744
+ lines.push(`${prefix}- route that surface with \`clisbot channels add slack-channel <channelId> --agent <id>\` or \`clisbot channels add telegram-group <chatId> --agent <id>\``);
73745
+ if (telegramEnabled) {
73746
+ lines.push(`${prefix}- Telegram: send \`/start\` in the target DM, group, or topic to get onboarding or pairing guidance`);
73747
+ }
73748
+ if (slackEnabled) {
73749
+ lines.push(`${prefix}- Slack: mention \`@<botname> \\start\` in the target channel to verify mention flow`);
73750
+ }
73751
+ }
73752
+ function appendAuthOnboardingLines(lines, summary, prefix = "") {
73753
+ lines.push(`${prefix}Auth onboarding:`);
73754
+ if (!hasConfiguredPrivilegedPrincipal(summary)) {
73755
+ 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`);
73756
+ lines.push(`${prefix} - set the first owner with: \`clisbot auth add-user app --role owner --user <principal>\``);
73757
+ } else {
73758
+ lines.push(`${prefix} - inspect current app roles with: \`clisbot auth show app\``);
73759
+ }
73760
+ lines.push(`${prefix} - inspect default agent roles with: \`clisbot auth show agent-defaults\``);
73761
+ lines.push(`${prefix} - add or remove principals with: \`clisbot auth add-user ...\` and \`clisbot auth remove-user ...\``);
73762
+ lines.push(`${prefix} - tune role permissions with: \`clisbot auth add-permission ...\` and \`clisbot auth remove-permission ...\``);
73763
+ lines.push(`${prefix} - run \`clisbot auth --help\` or read docs/user-guide/auth-and-roles.md for scopes and permission names`);
73764
+ }
72935
73765
  function renderChannelSummaryLines(summary) {
72936
73766
  return [
72937
73767
  "",
72938
73768
  "Channels:",
72939
73769
  ...summary.channelSummaries.map((channel) => {
73770
+ if (!channel.enabled) {
73771
+ return ` - ${channel.channel} enabled=no`;
73772
+ }
72940
73773
  const last = channel.lastActivityAt ? ` last=${formatTime(channel.lastActivityAt)} via ${channel.lastActivityAgentId ?? "unknown"}` : " last=never";
72941
73774
  const dm = ` dm=${channel.directMessagesEnabled ? channel.directMessagesPolicy : "disabled"}`;
72942
73775
  const group = channel.groupPolicy ? ` groups=${channel.groupPolicy}` : "";
@@ -72992,8 +73825,9 @@ function renderRuntimeDiagnosticsSummary(summary) {
72992
73825
  `).trim();
72993
73826
  }
72994
73827
  function renderStartSummary(summary) {
72995
- const configPath = collapseHomePath(getDefaultConfigPath());
72996
73828
  const lines = [
73829
+ ...renderOwnerSummaryLines(summary),
73830
+ "",
72997
73831
  ...renderAgentSummaryLines(summary),
72998
73832
  ...renderChannelSummaryLines(summary)
72999
73833
  ];
@@ -73031,7 +73865,10 @@ function renderStartSummary(summary) {
73031
73865
  lines.push("");
73032
73866
  lines.push(" Next steps after bootstrap:");
73033
73867
  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}`);
73868
+ appendChannelNextStepLines(lines, summary, " ");
73869
+ lines.push(` - ${renderPrivilegedChatHint(summary, "verify DM access and adjust in-chat settings")}`);
73870
+ lines.push("");
73871
+ appendAuthOnboardingLines(lines, summary, " ");
73035
73872
  lines.push(" - run `clisbot status` to recheck runtime and bootstrap state");
73036
73873
  lines.push(" - run `clisbot logs` if the bot does not answer as expected");
73037
73874
  lines.push(...renderPairingSetupHelpLines(" ", {
@@ -73049,9 +73886,11 @@ function renderStartSummary(summary) {
73049
73886
  }
73050
73887
  lines.push("");
73051
73888
  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");
73889
+ appendChannelNextStepLines(lines, summary, " ");
73890
+ lines.push(" - verify routes and defaultAgentId values match the agent you want to expose");
73891
+ lines.push(` - ${renderPrivilegedChatHint(summary, "adjust in-chat surface settings")}`);
73892
+ lines.push("");
73893
+ appendAuthOnboardingLines(lines, summary);
73055
73894
  lines.push(" - run `clisbot status` to inspect agents, channels, and tmux session state");
73056
73895
  lines.push(" - run `clisbot logs` if anything looks wrong");
73057
73896
  lines.push(...renderPairingSetupHelpLines("", {
@@ -73078,20 +73917,24 @@ function appendChannelSetupNotes(lines, summary, prefix = "") {
73078
73917
  if (channel.channel === "telegram") {
73079
73918
  lines.push(`${prefix} - telegram: no explicit group or topic routes are configured yet`);
73080
73919
  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>`);
73920
+ lines.push(`${prefix} add group: \`clisbot channels add telegram-group <chatId> --agent <id>\``);
73921
+ lines.push(`${prefix} add topic: \`clisbot channels add telegram-group <chatId> --topic <topicId> --agent <id>\``);
73922
+ lines.push(`${prefix} adjust later: ${renderPrivilegedChatHint(summary, "run in-chat commands here")}`);
73084
73923
  continue;
73085
73924
  }
73086
73925
  lines.push(`${prefix} - slack: no explicit channel or group routes are configured yet`);
73087
73926
  lines.push(`${prefix} dms: ${channel.directMessagesEnabled ? `enabled (${channel.directMessagesPolicy})` : "disabled"}`);
73088
73927
  lines.push(`${prefix} groups: ${channel.groupPolicy ?? "n/a"}`);
73089
- lines.push(`${prefix} route: configure channels.slack.channels.<channelId> or channels.slack.groups.<groupId>`);
73928
+ lines.push(`${prefix} add channel: \`clisbot channels add slack-channel <channelId> --agent <id>\``);
73929
+ lines.push(`${prefix} add group: \`clisbot channels add slack-group <groupId> --agent <id>\``);
73930
+ lines.push(`${prefix} adjust later: ${renderPrivilegedChatHint(summary, "run in-chat commands here")}`);
73090
73931
  }
73091
73932
  }
73092
73933
  function renderStatusSummary(summary) {
73093
73934
  const lines = [
73094
73935
  `stats agents=${summary.configuredAgents} bootstrapped=${summary.bootstrappedAgents} pendingBootstrap=${summary.bootstrapPendingAgents} tmuxSessions=${summary.runningTmuxSessions}`,
73936
+ ...renderOwnerSummaryLines(summary),
73937
+ "",
73095
73938
  ...renderAgentSummaryLines(summary),
73096
73939
  ...renderChannelSummaryLines(summary),
73097
73940
  ...renderChannelDiagnosticLines(summary),
@@ -73113,6 +73956,15 @@ function printCommandOutcomeBanner(outcome) {
73113
73956
  function printCommandOutcomeFooter(outcome) {
73114
73957
  printCommandOutcomeBanner(outcome);
73115
73958
  }
73959
+ function assertSupportedPlatform(command) {
73960
+ if (process.platform !== "win32") {
73961
+ return;
73962
+ }
73963
+ if (command.name === "help" || command.name === "version") {
73964
+ return;
73965
+ }
73966
+ throw new Error("Native Windows is not supported yet. Run clisbot from WSL2 or use Linux/macOS instead.");
73967
+ }
73116
73968
  function getPrimaryWorkspacePath(summary) {
73117
73969
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
73118
73970
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -73262,25 +74114,57 @@ async function serveForeground() {
73262
74114
  const runtimeCredentialsPath = getDefaultRuntimeCredentialsPath();
73263
74115
  const runtimeSupervisor = new RuntimeSupervisor(configPath);
73264
74116
  let shuttingDown = false;
73265
- const shutdown = async (exitCode = 0) => {
74117
+ let fatalHandling = false;
74118
+ const shutdown = async (exitCode = 0, options = {}) => {
73266
74119
  if (shuttingDown) {
73267
74120
  return;
73268
74121
  }
73269
74122
  shuttingDown = true;
73270
74123
  try {
73271
- await runtimeSupervisor.stop();
74124
+ await runtimeSupervisor.stop({
74125
+ markChannelsStopped: options.markChannelsStopped
74126
+ });
73272
74127
  } finally {
73273
74128
  removeRuntimeCredentials(runtimeCredentialsPath);
73274
74129
  removeRuntimePid(pidPath);
73275
74130
  process.exit(exitCode);
73276
74131
  }
73277
74132
  };
74133
+ const handleFatal = async (source, error) => {
74134
+ if (fatalHandling || shuttingDown) {
74135
+ return;
74136
+ }
74137
+ fatalHandling = true;
74138
+ const detail = error instanceof Error ? error.message : String(error);
74139
+ const fatalError = new Error(`fatal ${source}: ${detail}`);
74140
+ console.error(`clisbot fatal ${source}`, error);
74141
+ const forceExitTimer = setTimeout(() => {
74142
+ process.exit(1);
74143
+ }, 5000);
74144
+ forceExitTimer.unref?.();
74145
+ try {
74146
+ await runtimeSupervisor.markFatalFailure(fatalError);
74147
+ } catch (markError) {
74148
+ console.error("failed to record fatal runtime health", markError);
74149
+ }
74150
+ try {
74151
+ await shutdown(1, { markChannelsStopped: false });
74152
+ } finally {
74153
+ clearTimeout(forceExitTimer);
74154
+ }
74155
+ };
73278
74156
  process.once("SIGINT", () => {
73279
74157
  shutdown(0);
73280
74158
  });
73281
74159
  process.once("SIGTERM", () => {
73282
74160
  shutdown(0);
73283
74161
  });
74162
+ process.once("uncaughtException", (error) => {
74163
+ handleFatal("uncaughtException", error);
74164
+ });
74165
+ process.once("unhandledRejection", (error) => {
74166
+ handleFatal("unhandledRejection", error);
74167
+ });
73284
74168
  try {
73285
74169
  await runtimeSupervisor.start();
73286
74170
  await writeRuntimePid(pidPath, process.pid);
@@ -73357,7 +74241,7 @@ async function start(args = []) {
73357
74241
  console.log(`clisbot is already running with pid: ${result.pid}`);
73358
74242
  const workspacePath = getPrimaryWorkspacePath(summary);
73359
74243
  if (workspacePath) {
73360
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74244
+ console.log(`workspace: ${workspacePath}`);
73361
74245
  }
73362
74246
  console.log(`config: ${result.configPath}`);
73363
74247
  console.log(`log: ${result.logPath}`);
@@ -73389,7 +74273,7 @@ async function start(args = []) {
73389
74273
  console.log(`clisbot started with pid: ${result.pid}`);
73390
74274
  const workspacePath = getPrimaryWorkspacePath(summary);
73391
74275
  if (workspacePath) {
73392
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74276
+ console.log(`workspace: ${workspacePath}`);
73393
74277
  }
73394
74278
  console.log(`config: ${result.configPath}`);
73395
74279
  console.log(`log: ${result.logPath}`);
@@ -73478,7 +74362,7 @@ async function status() {
73478
74362
  });
73479
74363
  const workspacePath = getPrimaryWorkspacePath(summary);
73480
74364
  if (workspacePath) {
73481
- console.log(`workspace: ${workspacePath} (agent workspace: default work dir; contains state, sessions, personality, and guidance files)`);
74365
+ console.log(`workspace: ${workspacePath}`);
73482
74366
  }
73483
74367
  console.log(`config: ${runtimeStatus.configPath}`);
73484
74368
  console.log(`pid file: ${runtimeStatus.pidPath}`);
@@ -73521,6 +74405,7 @@ async function logs(lines) {
73521
74405
  } catch {}
73522
74406
  }
73523
74407
  async function main(command = parseCliArgs(process.argv)) {
74408
+ assertSupportedPlatform(command);
73524
74409
  if (command.name === "help") {
73525
74410
  console.log(renderCliHelp());
73526
74411
  return;
@@ -73577,6 +74462,10 @@ async function main(command = parseCliArgs(process.argv)) {
73577
74462
  await runAgentsCli(command.args);
73578
74463
  return;
73579
74464
  }
74465
+ if (command.name === "auth") {
74466
+ await runAuthCli(command.args);
74467
+ return;
74468
+ }
73580
74469
  if (command.name === "pairing") {
73581
74470
  await runPairingCli(command.args);
73582
74471
  return;