clisbot 0.1.45-beta.2 → 0.1.45-beta.3

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.
Files changed (2) hide show
  1. package/dist/main.js +322 -50
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -59315,6 +59315,7 @@ var DEFAULT_AGENT_TOOL_TEMPLATES = {
59315
59315
  startupDelayMs: 3000,
59316
59316
  startupRetryCount: 2,
59317
59317
  startupRetryDelayMs: 1000,
59318
+ startupReadyPattern: "(?:^|\\s)›\\s",
59318
59319
  promptSubmitDelayMs: 150,
59319
59320
  sessionId: {
59320
59321
  create: {
@@ -60711,6 +60712,7 @@ function getRuntimeMonitorRestartPlan(restartBackoff, restartNumber) {
60711
60712
 
60712
60713
  // src/config/schema.ts
60713
60714
  var defaultSessionIdPattern = "\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b";
60715
+ var codexStartupReadyPattern = "(?:^|\\s)›\\s";
60714
60716
  var runnerSessionIdCreateSchema = exports_external.object({
60715
60717
  mode: exports_external.enum(["runner", "explicit"]).default("runner"),
60716
60718
  args: exports_external.array(exports_external.string()).default([])
@@ -61285,6 +61287,7 @@ var agentsDefaultsSchema = exports_external.object({
61285
61287
  "-C",
61286
61288
  "{workspace}"
61287
61289
  ],
61290
+ startupReadyPattern: codexStartupReadyPattern,
61288
61291
  sessionId: {
61289
61292
  create: {
61290
61293
  mode: "runner",
@@ -61734,6 +61737,7 @@ var clisbotConfigSchema = exports_external.object({
61734
61737
  "-C",
61735
61738
  "{workspace}"
61736
61739
  ],
61740
+ startupReadyPattern: codexStartupReadyPattern,
61737
61741
  sessionId: {
61738
61742
  create: {
61739
61743
  mode: "runner",
@@ -61860,6 +61864,7 @@ var clisbotConfigSchema = exports_external.object({
61860
61864
  "-C",
61861
61865
  "{workspace}"
61862
61866
  ],
61867
+ startupReadyPattern: codexStartupReadyPattern,
61863
61868
  sessionId: {
61864
61869
  create: {
61865
61870
  mode: "runner",
@@ -62010,6 +62015,127 @@ function assertNoLegacyPrivilegeCommands(value, path = "root") {
62010
62015
 
62011
62016
  // src/config/config-upgrade.ts
62012
62017
  import { basename, dirname as dirname4, join as join5 } from "node:path";
62018
+
62019
+ // src/config/persisted-config.ts
62020
+ var defaultOwnedRunnerFields = {
62021
+ codex: ["startupReadyPattern"],
62022
+ gemini: [
62023
+ "startupDelayMs",
62024
+ "startupRetryCount",
62025
+ "startupRetryDelayMs",
62026
+ "startupReadyPattern",
62027
+ "startupBlockers",
62028
+ "promptSubmitDelayMs"
62029
+ ]
62030
+ };
62031
+ function isRecord5(value) {
62032
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62033
+ }
62034
+ function cloneConfig(config) {
62035
+ return structuredClone(config);
62036
+ }
62037
+ function areJsonEqual(left, right) {
62038
+ return JSON.stringify(left) === JSON.stringify(right);
62039
+ }
62040
+ function nestedRecord(root, path) {
62041
+ let current = root;
62042
+ for (const segment of path) {
62043
+ if (!isRecord5(current)) {
62044
+ return;
62045
+ }
62046
+ current = current[segment];
62047
+ }
62048
+ return isRecord5(current) ? current : undefined;
62049
+ }
62050
+ function deleteIfEmpty(owner, key) {
62051
+ const value = owner[key];
62052
+ if (isRecord5(value) && Object.keys(value).length === 0) {
62053
+ delete owner[key];
62054
+ }
62055
+ }
62056
+ function defaultRunner(toolId) {
62057
+ return buildRunnerFromToolTemplate(toolId, DEFAULT_AGENT_TOOL_TEMPLATES[toolId], undefined);
62058
+ }
62059
+ function pruneDefaultOwnedFields(params) {
62060
+ const defaults = defaultRunner(params.toolId);
62061
+ for (const field of defaultOwnedRunnerFields[params.toolId] ?? []) {
62062
+ if (params.force || Object.hasOwn(params.target, field) && areJsonEqual(params.target[field], defaults[field])) {
62063
+ delete params.target[field];
62064
+ }
62065
+ }
62066
+ }
62067
+ function pruneAgentRunnerOverride(runner, cli) {
62068
+ const toolId = inferAgentCliToolId(typeof runner.command === "string" ? runner.command : cli) ?? cli;
62069
+ if (!toolId) {
62070
+ return;
62071
+ }
62072
+ const defaults = defaultRunner(toolId);
62073
+ for (const field of [
62074
+ "command",
62075
+ "args",
62076
+ "trustWorkspace",
62077
+ "startupDelayMs",
62078
+ "startupRetryCount",
62079
+ "startupRetryDelayMs",
62080
+ "startupReadyPattern",
62081
+ "startupBlockers",
62082
+ "promptSubmitDelayMs",
62083
+ "sessionId"
62084
+ ]) {
62085
+ if (Object.hasOwn(runner, field) && areJsonEqual(runner[field], defaults[field])) {
62086
+ delete runner[field];
62087
+ }
62088
+ }
62089
+ }
62090
+ function pruneRunnerDefaults(config, forceRunnerStartupDefaults) {
62091
+ const runner = nestedRecord(config, ["agents", "defaults", "runner"]);
62092
+ if (!runner) {
62093
+ return;
62094
+ }
62095
+ for (const toolId of ["codex", "gemini"]) {
62096
+ const target = runner[toolId];
62097
+ if (isRecord5(target)) {
62098
+ pruneDefaultOwnedFields({
62099
+ target,
62100
+ toolId,
62101
+ force: forceRunnerStartupDefaults
62102
+ });
62103
+ }
62104
+ }
62105
+ }
62106
+ function pruneAgentOverrides(config) {
62107
+ const agents = nestedRecord(config, ["agents"]);
62108
+ if (!Array.isArray(agents?.list)) {
62109
+ return;
62110
+ }
62111
+ for (const agent of agents.list) {
62112
+ if (!isRecord5(agent) || !isRecord5(agent.runner)) {
62113
+ continue;
62114
+ }
62115
+ const cli = typeof agent.cli === "string" ? inferAgentCliToolId(agent.cli) ?? undefined : undefined;
62116
+ pruneAgentRunnerOverride(agent.runner, cli);
62117
+ deleteIfEmpty(agent, "runner");
62118
+ }
62119
+ }
62120
+ function pruneRuntimeMonitorBackoff(config) {
62121
+ const runtimeMonitor = nestedRecord(config, ["app", "control", "runtimeMonitor"]);
62122
+ if (!runtimeMonitor) {
62123
+ return;
62124
+ }
62125
+ const restartBackoff = runtimeMonitor?.restartBackoff;
62126
+ if (isRecord5(restartBackoff) && areJsonEqual(normalizeRuntimeMonitorRestartBackoff(restartBackoff), getDefaultRuntimeMonitorRestartBackoff())) {
62127
+ delete runtimeMonitor.restartBackoff;
62128
+ }
62129
+ }
62130
+ function pruneConfigForPersistence(config, options = {}) {
62131
+ const nextConfig = cloneConfig(config);
62132
+ pruneRuntimeMonitorBackoff(nextConfig);
62133
+ pruneRunnerDefaults(nextConfig, options.forceRunnerStartupDefaults === true);
62134
+ pruneAgentOverrides(nextConfig);
62135
+ return nextConfig;
62136
+ }
62137
+
62138
+ // src/config/config-upgrade.ts
62013
62139
  function readSchemaVersion(value) {
62014
62140
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
62015
62141
  return;
@@ -62062,8 +62188,11 @@ async function upgradeEditableConfigFileIfNeeded(configPath) {
62062
62188
  exactAdmissionMode: "explicit"
62063
62189
  }));
62064
62190
  logUpgradeStage(`applying ${CURRENT_SCHEMA_VERSION} config to ${collapseHomePath(expandedConfigPath)}`);
62191
+ const persistedConfig = pruneConfigForPersistence(normalizedConfig, {
62192
+ forceRunnerStartupDefaults: true
62193
+ });
62065
62194
  await writeTextFile(expandedConfigPath, `${JSON.stringify({
62066
- ...normalizedConfig,
62195
+ ...persistedConfig,
62067
62196
  meta: {
62068
62197
  ...normalizedConfig.meta,
62069
62198
  lastTouchedAt: new Date().toISOString()
@@ -62090,7 +62219,6 @@ function renderDefaultConfigTemplate(options = {}) {
62090
62219
  const sessionStorePath = collapseHomePath(getDefaultSessionStorePath());
62091
62220
  const workspaceTemplate = collapseHomePath(getDefaultWorkspaceTemplate());
62092
62221
  const defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
62093
- const defaultRuntimeMonitorRestartBackoff2 = getDefaultRuntimeMonitorRestartBackoff();
62094
62222
  return JSON.stringify({
62095
62223
  meta: {
62096
62224
  schemaVersion: CURRENT_SCHEMA_VERSION,
@@ -62135,7 +62263,6 @@ function renderDefaultConfigTemplate(options = {}) {
62135
62263
  maxActiveLoops: 10
62136
62264
  },
62137
62265
  runtimeMonitor: {
62138
- restartBackoff: defaultRuntimeMonitorRestartBackoff2,
62139
62266
  ownerAlerts: {
62140
62267
  enabled: true,
62141
62268
  minIntervalMinutes: 30
@@ -62407,21 +62534,6 @@ function renderDefaultConfigTemplate(options = {}) {
62407
62534
  gemini: {
62408
62535
  command: "gemini",
62409
62536
  args: ["--approval-mode=yolo", "--sandbox=false"],
62410
- startupDelayMs: 15000,
62411
- startupRetryCount: 2,
62412
- startupRetryDelayMs: 1000,
62413
- startupReadyPattern: "Type your message or @path/to/file",
62414
- startupBlockers: [
62415
- {
62416
- pattern: "Please visit the following URL to authorize the application|Enter the authorization code:",
62417
- message: "Gemini CLI is waiting for manual OAuth authorization. Authenticate Gemini once in a direct interactive terminal, or configure headless auth such as GEMINI_API_KEY or Vertex AI before routing Gemini through clisbot."
62418
- },
62419
- {
62420
- pattern: "How would you like to authenticate for this project\\?|Failed to sign in\\.|Manual authorization is required but the current session is non-interactive",
62421
- message: "Gemini CLI is blocked in its authentication setup flow or sign-in recovery. Complete Gemini authentication directly first, or switch clisbot to a headless auth path such as GEMINI_API_KEY or Vertex AI before routing prompts."
62422
- }
62423
- ],
62424
- promptSubmitDelayMs: 200,
62425
62537
  sessionId: {
62426
62538
  create: {
62427
62539
  mode: "runner",
@@ -62482,7 +62594,7 @@ async function writeEditableConfig(configPath, config) {
62482
62594
  exactAdmissionMode: "explicit"
62483
62595
  }));
62484
62596
  const nextConfig = {
62485
- ...normalizedConfig,
62597
+ ...pruneConfigForPersistence(normalizedConfig),
62486
62598
  meta: {
62487
62599
  ...normalizedConfig.meta,
62488
62600
  lastTouchedAt: new Date().toISOString()
@@ -63381,7 +63493,7 @@ function resolveWorkspacePath(config, agentId, customWorkspace) {
63381
63493
  agentId
63382
63494
  }));
63383
63495
  }
63384
- function areJsonEqual(left, right) {
63496
+ function areJsonEqual2(left, right) {
63385
63497
  return JSON.stringify(left) === JSON.stringify(right);
63386
63498
  }
63387
63499
  function ensureAgentMissing(config, agentId) {
@@ -63451,8 +63563,8 @@ async function addAgentToEditableConfig(params) {
63451
63563
  const resolvedStartupOptions = params.startupOptions && params.startupOptions.length > 0 ? params.startupOptions : toolTemplate.startupOptions;
63452
63564
  const runner = buildRunnerFromToolTemplate(cliTool, toolTemplate, resolvedStartupOptions);
63453
63565
  const workspacePath = resolveWorkspacePath(config, params.agentId, params.workspace);
63454
- const defaultRunner = config.agents.defaults.runner[cliTool];
63455
- const runnerOverride = areJsonEqual(runner, defaultRunner) ? undefined : runner;
63566
+ const defaultRunner2 = config.agents.defaults.runner[cliTool];
63567
+ const runnerOverride = areJsonEqual2(runner, defaultRunner2) ? undefined : runner;
63456
63568
  if (params.bootstrap) {
63457
63569
  await applyBootstrapTemplate(workspacePath, params.bootstrap, cliTool);
63458
63570
  }
@@ -65912,6 +66024,19 @@ import { join as join10 } from "node:path";
65912
66024
  function createLoopId() {
65913
66025
  return randomUUID2().split("-")[0] ?? randomUUID2();
65914
66026
  }
66027
+ function buildStoredLoopSender(params) {
66028
+ const providerId = params.providerId.trim();
66029
+ if (!providerId) {
66030
+ return;
66031
+ }
66032
+ const normalizedProviderId = params.platform === "slack" ? providerId.toUpperCase() : providerId;
66033
+ return {
66034
+ senderId: `${params.platform}:${normalizedProviderId}`,
66035
+ providerId: normalizedProviderId,
66036
+ displayName: params.displayName,
66037
+ handle: params.handle
66038
+ };
66039
+ }
65915
66040
  function createStoredLoopBase(params) {
65916
66041
  const now = Date.now();
65917
66042
  return {
@@ -65941,10 +66066,13 @@ function deriveLegacyLoopSender(params) {
65941
66066
  if (!providerId) {
65942
66067
  return;
65943
66068
  }
65944
- return {
65945
- providerId,
65946
- senderId: params.surfaceBinding?.platform ? `${params.surfaceBinding.platform}:${params.surfaceBinding.platform === "slack" ? providerId.toUpperCase() : providerId}` : undefined
65947
- };
66069
+ if (!params.surfaceBinding?.platform) {
66070
+ return { providerId };
66071
+ }
66072
+ return buildStoredLoopSender({
66073
+ platform: params.surfaceBinding.platform,
66074
+ providerId
66075
+ });
65948
66076
  }
65949
66077
  function createStoredIntervalLoop(params) {
65950
66078
  return {
@@ -68134,6 +68262,41 @@ function deriveRunningInteractionText(previousSnapshot, currentSnapshot) {
68134
68262
  function deriveRunningInteractionSnapshot(currentSnapshot) {
68135
68263
  return cleanRunningInteractionSnapshot(currentSnapshot);
68136
68264
  }
68265
+ function getPromptMarker(lines) {
68266
+ if (looksLikeCodexSnapshot(lines)) {
68267
+ return /^\s*›\s/;
68268
+ }
68269
+ if (looksLikeClaudeSnapshot(lines)) {
68270
+ return /^\s*❯/;
68271
+ }
68272
+ if (looksLikeGeminiSnapshot(lines)) {
68273
+ return /^\s*>\s/;
68274
+ }
68275
+ return null;
68276
+ }
68277
+ function sliceFromLastPromptBlock(raw) {
68278
+ const lines = splitNormalizedLines(raw);
68279
+ const marker = getPromptMarker(lines);
68280
+ if (!marker) {
68281
+ return "";
68282
+ }
68283
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
68284
+ if (!marker.test((lines[index] ?? "").trimStart())) {
68285
+ continue;
68286
+ }
68287
+ return lines.slice(index).join(`
68288
+ `);
68289
+ }
68290
+ return "";
68291
+ }
68292
+ function deriveLatestPromptInteractionSnapshot(currentSnapshot) {
68293
+ const promptTail = sliceFromLastPromptBlock(currentSnapshot);
68294
+ return promptTail ? cleanInteractionSnapshot(promptTail) : "";
68295
+ }
68296
+ function deriveLatestPromptRunningInteractionSnapshot(currentSnapshot) {
68297
+ const promptTail = sliceFromLastPromptBlock(currentSnapshot);
68298
+ return promptTail ? cleanRunningInteractionSnapshot(promptTail) : "";
68299
+ }
68137
68300
  function deriveInteractionText(initialSnapshot, currentSnapshot) {
68138
68301
  const previous = cleanInteractionSnapshot(initialSnapshot);
68139
68302
  const current = cleanInteractionSnapshot(currentSnapshot);
@@ -69437,7 +69600,10 @@ class RunnerService {
69437
69600
  });
69438
69601
  try {
69439
69602
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69440
- await this.syncSessionIdentity(resolved);
69603
+ await this.sessionState.touchSessionEntry(resolved, {
69604
+ sessionId: existing?.sessionId,
69605
+ runnerCommand: resolved.runner.command
69606
+ });
69441
69607
  } catch (error) {
69442
69608
  throw await this.mapSessionError(error, resolved.sessionName, "during startup");
69443
69609
  }
@@ -69790,9 +69956,10 @@ async function monitorTmuxRun(params) {
69790
69956
  }
69791
69957
  const hasActiveTimer = hasActiveTimerStatus(snapshot);
69792
69958
  const currentRunningSnapshot = deriveRunningInteractionSnapshot(snapshot);
69793
- const baselineRunningSnapshot = deriveInteractionText(baselineSnapshot, snapshot) || currentRunningSnapshot;
69794
- const runningDelta = priorSnapshot ? deriveRunningInteractionText(priorSnapshot, snapshot) : currentRunningSnapshot;
69795
- const shouldReplaceRunningSnapshot = paneChanged && !runningDelta && Boolean(baselineRunningSnapshot) && baselineRunningSnapshot !== previousRunningTruth;
69959
+ const promptRunningSnapshot = deriveLatestPromptRunningInteractionSnapshot(snapshot);
69960
+ const baselineRunningSnapshot = promptRunningSnapshot || deriveInteractionText(baselineSnapshot, snapshot) || currentRunningSnapshot;
69961
+ const runningDelta = promptRunningSnapshot ? "" : priorSnapshot ? deriveRunningInteractionText(priorSnapshot, snapshot) : currentRunningSnapshot;
69962
+ const shouldReplaceRunningSnapshot = (paneChanged || Boolean(promptRunningSnapshot)) && !runningDelta && Boolean(baselineRunningSnapshot) && baselineRunningSnapshot !== previousRunningTruth;
69796
69963
  const nextRunningTruth = runningDelta ? previousRunningTruth ? appendInteractionText(previousRunningTruth, runningDelta) : runningDelta : shouldReplaceRunningSnapshot ? baselineRunningSnapshot : previousRunningTruth;
69797
69964
  const runningSnapshot = runningDelta ? nextRunningTruth : shouldReplaceRunningSnapshot ? deriveBoundedRunningRewritePreview({
69798
69965
  previousSnapshot: previousRunningTruth,
@@ -69826,8 +69993,9 @@ async function monitorTmuxRun(params) {
69826
69993
  });
69827
69994
  }
69828
69995
  if (!hasActiveTimer && (sawActivity || sawPaneChange || sawPromptSubmission) && now - lastPaneChangeAt >= params.idleTimeoutMs) {
69996
+ const completedSnapshot = deriveLatestPromptInteractionSnapshot(previousSnapshot) || deriveInteractionText(baselineSnapshot, previousSnapshot);
69829
69997
  await params.onCompleted({
69830
- snapshot: deriveInteractionText(baselineSnapshot, previousSnapshot),
69998
+ snapshot: completedSnapshot,
69831
69999
  fullSnapshot: previousSnapshot,
69832
70000
  initialSnapshot: baselineSnapshot
69833
70001
  });
@@ -71816,16 +71984,12 @@ function buildLoopSurfaceBinding(identity) {
71816
71984
  };
71817
71985
  }
71818
71986
  function buildLoopSender(identity) {
71819
- const providerId = identity.senderId?.trim();
71820
- if (!providerId) {
71821
- return;
71822
- }
71823
- return {
71824
- senderId: identity.platform === "slack" ? `slack:${providerId.toUpperCase()}` : `telegram:${providerId}`,
71825
- providerId,
71987
+ return buildStoredLoopSender({
71988
+ platform: identity.platform,
71989
+ providerId: identity.senderId ?? "",
71826
71990
  displayName: identity.senderName,
71827
71991
  handle: identity.senderHandle
71828
- };
71992
+ });
71829
71993
  }
71830
71994
  async function executePromptDelivery(params) {
71831
71995
  let responseChunks = [];
@@ -79981,16 +80145,17 @@ function renderLoopsHelp() {
79981
80145
  "Usage:",
79982
80146
  ` ${renderCliCommand("loops")}`,
79983
80147
  ` ${renderCliCommand("loops --help")}`,
80148
+ ` ${renderCliCommand("loops create --help")}`,
79984
80149
  ` ${renderCliCommand("loops list")}`,
79985
80150
  ` ${renderCliCommand("loops status")}`,
79986
80151
  ` ${renderCliCommand("loops status --channel slack --target group:C1234567890 --thread-id 1712345678.123456")}`,
79987
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 every day at 07:00 check CI")}`,
79988
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 every day at 07:00 check CI --confirm")}`,
79989
- ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --timezone America/Los_Angeles every day at 07:00 check tickets")}`,
79990
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --new-thread every day at 07:00 check CI")}`,
79991
- ` ${renderCliCommand("loops --channel slack --target group:C1234567890 --thread-id 1712345678.123456 5m check CI")}`,
79992
- ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --topic-id 42 every weekday at 07:00 standup")}`,
79993
- ` ${renderCliCommand("loops --channel slack --target group:C1234567890 --thread-id 1712345678.123456 3 review backlog")}`,
80152
+ ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI")}`,
80153
+ ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI --confirm")}`,
80154
+ ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --sender telegram:1276408333 --timezone America/Los_Angeles every day at 07:00 check tickets")}`,
80155
+ ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --new-thread --sender slack:U1234567890 every day at 07:00 check CI")}`,
80156
+ ` ${renderCliCommand("loops --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 5m check CI")}`,
80157
+ ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --topic-id 42 --sender telegram:1276408333 every weekday at 07:00 standup")}`,
80158
+ ` ${renderCliCommand("loops --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 3 review backlog")}`,
79994
80159
  ` ${renderCliCommand("loops cancel <id>")}`,
79995
80160
  ` ${renderCliCommand("loops cancel --all")}`,
79996
80161
  ` ${renderCliCommand("loops cancel --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --all")}`,
@@ -80004,6 +80169,8 @@ function renderLoopsHelp() {
80004
80169
  " - use `--topic-id` for a Telegram topic id",
80005
80170
  " - omitting the sub-surface flag targets the parent Slack channel/group/DM or the parent Telegram chat",
80006
80171
  " - `--new-thread` is Slack-only and creates a fresh thread anchor before the loop starts",
80172
+ " - `--sender <principal>` is required when creating loops, using `slack:<user-id>` or `telegram:<user-id>`",
80173
+ " - optional creator display fields: `--sender-name <name>` and `--sender-handle <handle>`",
80007
80174
  " - `--timezone <iana>` is a one-off wall-clock loop override and is frozen on the created loop record",
80008
80175
  " - in Telegram forum groups, omitting `--topic-id` targets the parent chat surface; sends then follow Telegram's normal no-`message_thread_id` behavior, which is the General topic when that forum has one",
80009
80176
  "",
@@ -80016,13 +80183,14 @@ function renderLoopsHelp() {
80016
80183
  "",
80017
80184
  "Examples:",
80018
80185
  ` ${renderCliCommand("loops status --channel slack --target group:C1234567890 --thread-id 1712345678.123456")}`,
80019
- ` ${renderCliCommand("loops --channel telegram --target -1001234567890 --topic-id 42 5m")}`,
80020
- ` ${renderCliCommand("loops --channel slack --target dm:U1234567890 --new-thread every day at 09:00 check inbox")}`,
80186
+ ` ${renderCliCommand("loops --channel telegram --target -1001234567890 --topic-id 42 --sender telegram:1276408333 5m")}`,
80187
+ ` ${renderCliCommand("loops --channel slack --target dm:U1234567890 --new-thread --sender slack:U1234567890 every day at 09:00 check inbox")}`,
80021
80188
  ` ${renderCliCommand("loops cancel --channel slack --target group:C1234567890 --thread-id 1712345678.123456 abc123")}`,
80022
80189
  "Behavior:",
80023
80190
  " - `list` always renders the global persisted loop inventory",
80024
80191
  " - bare `status` is global; scoped `status --channel ... --target ...` matches `/loop status` for one routed session",
80025
80192
  " - `create` and bare scoped syntax reuse the same loop parser as channel `/loop`",
80193
+ " - CLI loop creation fails without `--sender` so scheduled prompts can preserve creator identity",
80026
80194
  " - the first wall-clock loop returns `confirmation_required` and does not persist until rerun with `--confirm`",
80027
80195
  " - recurring interval loops and confirmed wall-clock loops are persisted immediately and picked up by the runtime when it is running",
80028
80196
  " - if runtime is stopped, recurring loops activate on the next `clisbot start`",
@@ -80034,6 +80202,39 @@ function renderLoopsHelp() {
80034
80202
  ].join(`
80035
80203
  `);
80036
80204
  }
80205
+ function renderLoopsCreateHelp() {
80206
+ return [
80207
+ renderCliCommand("loops create"),
80208
+ "",
80209
+ "Usage:",
80210
+ ` ${renderCliCommand("loops create --channel <slack|telegram> --target <surface> --sender <principal> <expression>")}`,
80211
+ ` ${renderCliCommand("loops --channel <slack|telegram> --target <surface> --sender <principal> <expression>")}`,
80212
+ "",
80213
+ "Required:",
80214
+ " - `--channel <slack|telegram>` and `--target <surface>` select the routed session",
80215
+ " - `--sender <principal>` records the human creator, for example `slack:U1234567890` or `telegram:1276408333`",
80216
+ "",
80217
+ "Optional:",
80218
+ " - `--sender-name <name>` stores a readable creator name for scheduled prompt context",
80219
+ " - `--sender-handle <handle>` stores a creator handle without `@`",
80220
+ " - `--thread-id <ts>` targets an existing Slack thread",
80221
+ " - `--topic-id <id>` targets a Telegram topic",
80222
+ " - `--new-thread` creates a Slack thread anchor before persisting the loop",
80223
+ " - `--timezone <iana>` freezes a one-off wall-clock timezone on the loop record",
80224
+ " - `--confirm` persists the first wall-clock loop after reviewing the confirmation output",
80225
+ "",
80226
+ "Examples:",
80227
+ ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI")}`,
80228
+ ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --topic-id 42 --sender telegram:1276408333 5m check CI")}`,
80229
+ ` ${renderCliCommand("loops create --channel slack --target dm:U1234567890 --new-thread --sender slack:U1234567890 every day at 09:00 check inbox")}`,
80230
+ "",
80231
+ "Behavior:",
80232
+ " - create without `--sender` fails by design",
80233
+ " - the `--sender` platform must match `--channel`",
80234
+ " - recurring CLI-created loops persist creator metadata into the session store"
80235
+ ].join(`
80236
+ `);
80237
+ }
80037
80238
  function renderLoopInventory(params) {
80038
80239
  const lines = [
80039
80240
  renderCliCommand(`loops ${params.commandLabel}`),
@@ -80241,6 +80442,9 @@ async function getScopedLoopCounts(params) {
80241
80442
  // src/control/loops-cli.ts
80242
80443
  var LOOP_BUSY_RETRY_MS = 250;
80243
80444
  var LOOP_CONFIRM_FLAG = "--confirm";
80445
+ var LOOP_SENDER_FLAG = "--sender";
80446
+ var LOOP_SENDER_NAME_FLAG = "--sender-name";
80447
+ var LOOP_SENDER_HANDLE_FLAG = "--sender-handle";
80244
80448
  function getEditableConfigPath8() {
80245
80449
  return process.env.CLISBOT_CONFIG_PATH;
80246
80450
  }
@@ -80426,8 +80630,29 @@ async function executeCountLoop(params) {
80426
80630
  function stripConfirmFlag(args) {
80427
80631
  return args.filter((arg) => arg !== LOOP_CONFIRM_FLAG);
80428
80632
  }
80633
+ function stripLoopCreatorArgs(args) {
80634
+ const remaining = [];
80635
+ const creatorFlags = new Set([
80636
+ LOOP_SENDER_FLAG,
80637
+ LOOP_SENDER_NAME_FLAG,
80638
+ LOOP_SENDER_HANDLE_FLAG
80639
+ ]);
80640
+ for (let index = 0;index < args.length; index += 1) {
80641
+ const current = args[index];
80642
+ if (current === "--") {
80643
+ remaining.push(...args.slice(index));
80644
+ break;
80645
+ }
80646
+ if (creatorFlags.has(current)) {
80647
+ index += 1;
80648
+ continue;
80649
+ }
80650
+ remaining.push(current);
80651
+ }
80652
+ return remaining;
80653
+ }
80429
80654
  function parseCreateExpression(rawArgs, explicitCreateSubcommand) {
80430
- const expressionArgs = stripLoopContextArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs));
80655
+ const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs)));
80431
80656
  const expression = expressionArgs.join(" ").trim();
80432
80657
  if (!expression) {
80433
80658
  throw new Error("Loop creation requires an interval, count, or schedule expression.");
@@ -80448,6 +80673,43 @@ function parseLoopTimezone(args) {
80448
80673
  }
80449
80674
  return parseTimezone(timezone, "--timezone");
80450
80675
  }
80676
+ function parseLoopCreator(args, addressing) {
80677
+ const sender = parseOptionValue3(args, LOOP_SENDER_FLAG)?.trim();
80678
+ if (!sender) {
80679
+ throw new Error(`Loop creation requires ${LOOP_SENDER_FLAG} <principal>, for example ${LOOP_SENDER_FLAG} telegram:1276408333 or ${LOOP_SENDER_FLAG} slack:U1234567890.`);
80680
+ }
80681
+ const [platform, ...providerParts] = sender.split(":");
80682
+ const providerId = providerParts.join(":").trim();
80683
+ if (platform !== "slack" && platform !== "telegram" || !providerId) {
80684
+ throw new Error(`${LOOP_SENDER_FLAG} must be a principal like telegram:<id> or slack:<user-id>.`);
80685
+ }
80686
+ if (addressing.channel && platform !== addressing.channel) {
80687
+ throw new Error(`${LOOP_SENDER_FLAG} platform must match --channel ${addressing.channel}.`);
80688
+ }
80689
+ const creator = buildStoredLoopSender({
80690
+ platform,
80691
+ providerId,
80692
+ displayName: parseOptionValue3(args, LOOP_SENDER_NAME_FLAG),
80693
+ handle: parseOptionValue3(args, LOOP_SENDER_HANDLE_FLAG)
80694
+ });
80695
+ if (!creator) {
80696
+ throw new Error(`${LOOP_SENDER_FLAG} must include a non-empty provider id.`);
80697
+ }
80698
+ return creator;
80699
+ }
80700
+ function quoteLoopCliValue(value) {
80701
+ if (/^[A-Za-z0-9_@.:/-]+$/.test(value)) {
80702
+ return value;
80703
+ }
80704
+ return `'${value.replace(/'/g, "'\\''")}'`;
80705
+ }
80706
+ function renderLoopCreatorArgs(creator) {
80707
+ return [
80708
+ `${LOOP_SENDER_FLAG} ${quoteLoopCliValue(creator.senderId ?? creator.providerId ?? "")}`,
80709
+ creator.displayName ? `${LOOP_SENDER_NAME_FLAG} ${quoteLoopCliValue(creator.displayName)}` : undefined,
80710
+ creator.handle ? `${LOOP_SENDER_HANDLE_FLAG} ${quoteLoopCliValue(creator.handle)}` : undefined
80711
+ ].filter(Boolean).join(" ");
80712
+ }
80451
80713
  async function enforceLoopCreateLimits(state, parsed, maxRunsPerLoop, maxActiveLoops) {
80452
80714
  const globalLoops = await state.sessionState.listIntervalLoops();
80453
80715
  if (parsed.mode !== "times" && globalLoops.length >= maxActiveLoops) {
@@ -80473,6 +80735,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80473
80735
  const expression = parseCreateExpression(rawArgs, explicitCreateSubcommand);
80474
80736
  const parsed = parseCreateCommand(expression);
80475
80737
  let addressing = parseAddressing(rawArgs);
80738
+ const creator = parseLoopCreator(rawArgs, addressing);
80476
80739
  if (addressing.channel === "telegram" && addressing.threadId) {
80477
80740
  throw new Error("Telegram loop commands use `--topic-id`, not `--thread-id`.");
80478
80741
  }
@@ -80490,6 +80753,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80490
80753
  return {
80491
80754
  addressing,
80492
80755
  context: provisionalContext,
80756
+ creator,
80493
80757
  parsed,
80494
80758
  resolvedPrompt,
80495
80759
  resolvedTarget: provisionalResolvedTarget,
@@ -80520,6 +80784,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80520
80784
  addressing,
80521
80785
  context,
80522
80786
  deliveryContext,
80787
+ creator,
80523
80788
  parsed,
80524
80789
  resolvedPrompt,
80525
80790
  resolvedTarget,
@@ -80560,6 +80825,8 @@ function buildRecurringLoopPromptMetadata(request) {
80560
80825
  promptSummary: summarizeLoopPrompt(request.resolvedPrompt.text, request.resolvedPrompt.maintenancePrompt),
80561
80826
  promptSource: request.resolvedPrompt.maintenancePrompt ? "LOOP.md" : "custom",
80562
80827
  maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
80828
+ createdBy: request.creator.providerId,
80829
+ sender: request.creator,
80563
80830
  surfaceBinding: buildLoopSurfaceBinding2(request)
80564
80831
  };
80565
80832
  }
@@ -80633,7 +80900,8 @@ function renderCalendarConfirmation(params) {
80633
80900
  nowMs: Date.now()
80634
80901
  });
80635
80902
  const timezoneClause = params.request.loopTimezone ? ` --timezone ${params.request.loopTimezone}` : "";
80636
- const retryCommand = `${renderScopedCommand("loops create", params.request.addressing)}${timezoneClause} ${params.request.expression} ${LOOP_CONFIRM_FLAG}`;
80903
+ const senderClause = ` ${renderLoopCreatorArgs(params.request.creator)}`;
80904
+ const retryCommand = `${renderScopedCommand("loops create", params.request.addressing)}${senderClause}${timezoneClause} ${params.request.expression} ${LOOP_CONFIRM_FLAG}`;
80637
80905
  return [
80638
80906
  "confirmation_required: first wall-clock loop",
80639
80907
  `proposed schedule: ${formatCalendarLoopSchedule(parsed)}`,
@@ -80746,6 +81014,10 @@ async function runLoopsCli(args) {
80746
81014
  console.log(renderLoopsHelp());
80747
81015
  return;
80748
81016
  }
81017
+ if (subcommand === "create" && (hasFlag4(args, "--help") || hasFlag4(args, "-h"))) {
81018
+ console.log(renderLoopsCreateHelp());
81019
+ return;
81020
+ }
80749
81021
  const state = await loadLoopControlState();
80750
81022
  const addressing = parseAddressing(args);
80751
81023
  if (subcommand === "list") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.45-beta.2",
3
+ "version": "0.1.45-beta.3",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",