clisbot 0.1.45-beta.1 → 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 +333 -57
  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 {
@@ -67743,7 +67871,7 @@ function collapseBlankLines(lines) {
67743
67871
  return collapsed;
67744
67872
  }
67745
67873
  var DURATION_STATUS_PATTERN = String.raw`(?:\d+(?:h|m|s))(?:\s+\d+(?:h|m|s)){0,2}`;
67746
- var CODEX_WORKING_STATUS_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?Working(?:\s*\()?${DURATION_STATUS_PATTERN}\b.*(?:esc to interrupt|interrupt)\)?$`, "i");
67874
+ var CODEX_WORKING_STATUS_PATTERN = new RegExp(String.raw`^(?=.*\b${DURATION_STATUS_PATTERN}\b)(?=.*(?:esc\s+to\s+(?:interrupt|cancel)|interrupt|cancel|ctrl\+c))(?:[•◦·✻✽*]\s*)?Working(?:\.{3}|…)?\s*.*\)?$`, "i");
67747
67875
  var CODEX_INTERRUPT_FOOTER_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?${DURATION_STATUS_PATTERN}\s*[•◦·]?\s*esc to interrupt\)?$`, "i");
67748
67876
  var GEMINI_THINKING_STATUS_PATTERN = new RegExp(String.raw`^Thinking\.\.\. \(esc to cancel,\s*${DURATION_STATUS_PATTERN}\)$`, "i");
67749
67877
  var CLAUDE_WORKED_STATUS_PATTERN = new RegExp(String.raw`^(?:[✻✽*]\s*)?(?:Worked|Cooked) for ${DURATION_STATUS_PATTERN}$`, "i");
@@ -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);
@@ -68407,16 +68570,20 @@ function renderErrorInteractionBody(body, footer) {
68407
68570
  ${footer}`;
68408
68571
  }
68409
68572
  function renderSlackRunningInteraction(body, note) {
68410
- const statusNote = note ? `_${note}_` : "_Working..._";
68411
- return body ? `${body}
68573
+ if (note) {
68574
+ return body ? `${body}
68412
68575
 
68413
- ${statusNote}` : statusNote;
68576
+ _${note}_` : `_${note}_`;
68577
+ }
68578
+ return body || "_Working..._";
68414
68579
  }
68415
68580
  function renderTelegramRunningInteraction(body, note) {
68416
- const statusNote = note || "Working...";
68417
- return body ? `${body}
68581
+ if (note) {
68582
+ return body ? `${body}
68418
68583
 
68419
- ${statusNote}` : statusNote;
68584
+ ${note}` : note;
68585
+ }
68586
+ return body || "Working...";
68420
68587
  }
68421
68588
  function renderSlackInteraction(params) {
68422
68589
  const body = renderInteractionBody(params);
@@ -69433,7 +69600,10 @@ class RunnerService {
69433
69600
  });
69434
69601
  try {
69435
69602
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69436
- await this.syncSessionIdentity(resolved);
69603
+ await this.sessionState.touchSessionEntry(resolved, {
69604
+ sessionId: existing?.sessionId,
69605
+ runnerCommand: resolved.runner.command
69606
+ });
69437
69607
  } catch (error) {
69438
69608
  throw await this.mapSessionError(error, resolved.sessionName, "during startup");
69439
69609
  }
@@ -69786,9 +69956,10 @@ async function monitorTmuxRun(params) {
69786
69956
  }
69787
69957
  const hasActiveTimer = hasActiveTimerStatus(snapshot);
69788
69958
  const currentRunningSnapshot = deriveRunningInteractionSnapshot(snapshot);
69789
- const baselineRunningSnapshot = deriveInteractionText(baselineSnapshot, snapshot) || currentRunningSnapshot;
69790
- const runningDelta = priorSnapshot ? deriveRunningInteractionText(priorSnapshot, snapshot) : currentRunningSnapshot;
69791
- 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;
69792
69963
  const nextRunningTruth = runningDelta ? previousRunningTruth ? appendInteractionText(previousRunningTruth, runningDelta) : runningDelta : shouldReplaceRunningSnapshot ? baselineRunningSnapshot : previousRunningTruth;
69793
69964
  const runningSnapshot = runningDelta ? nextRunningTruth : shouldReplaceRunningSnapshot ? deriveBoundedRunningRewritePreview({
69794
69965
  previousSnapshot: previousRunningTruth,
@@ -69822,8 +69993,9 @@ async function monitorTmuxRun(params) {
69822
69993
  });
69823
69994
  }
69824
69995
  if (!hasActiveTimer && (sawActivity || sawPaneChange || sawPromptSubmission) && now - lastPaneChangeAt >= params.idleTimeoutMs) {
69996
+ const completedSnapshot = deriveLatestPromptInteractionSnapshot(previousSnapshot) || deriveInteractionText(baselineSnapshot, previousSnapshot);
69825
69997
  await params.onCompleted({
69826
- snapshot: deriveInteractionText(baselineSnapshot, previousSnapshot),
69998
+ snapshot: completedSnapshot,
69827
69999
  fullSnapshot: previousSnapshot,
69828
70000
  initialSnapshot: baselineSnapshot
69829
70001
  });
@@ -71812,16 +71984,12 @@ function buildLoopSurfaceBinding(identity) {
71812
71984
  };
71813
71985
  }
71814
71986
  function buildLoopSender(identity) {
71815
- const providerId = identity.senderId?.trim();
71816
- if (!providerId) {
71817
- return;
71818
- }
71819
- return {
71820
- senderId: identity.platform === "slack" ? `slack:${providerId.toUpperCase()}` : `telegram:${providerId}`,
71821
- providerId,
71987
+ return buildStoredLoopSender({
71988
+ platform: identity.platform,
71989
+ providerId: identity.senderId ?? "",
71822
71990
  displayName: identity.senderName,
71823
71991
  handle: identity.senderHandle
71824
- };
71992
+ });
71825
71993
  }
71826
71994
  async function executePromptDelivery(params) {
71827
71995
  let responseChunks = [];
@@ -79977,16 +80145,17 @@ function renderLoopsHelp() {
79977
80145
  "Usage:",
79978
80146
  ` ${renderCliCommand("loops")}`,
79979
80147
  ` ${renderCliCommand("loops --help")}`,
80148
+ ` ${renderCliCommand("loops create --help")}`,
79980
80149
  ` ${renderCliCommand("loops list")}`,
79981
80150
  ` ${renderCliCommand("loops status")}`,
79982
80151
  ` ${renderCliCommand("loops status --channel slack --target group:C1234567890 --thread-id 1712345678.123456")}`,
79983
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 every day at 07:00 check CI")}`,
79984
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 every day at 07:00 check CI --confirm")}`,
79985
- ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --timezone America/Los_Angeles every day at 07:00 check tickets")}`,
79986
- ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --new-thread every day at 07:00 check CI")}`,
79987
- ` ${renderCliCommand("loops --channel slack --target group:C1234567890 --thread-id 1712345678.123456 5m check CI")}`,
79988
- ` ${renderCliCommand("loops create --channel telegram --target -1001234567890 --topic-id 42 every weekday at 07:00 standup")}`,
79989
- ` ${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")}`,
79990
80159
  ` ${renderCliCommand("loops cancel <id>")}`,
79991
80160
  ` ${renderCliCommand("loops cancel --all")}`,
79992
80161
  ` ${renderCliCommand("loops cancel --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --all")}`,
@@ -80000,6 +80169,8 @@ function renderLoopsHelp() {
80000
80169
  " - use `--topic-id` for a Telegram topic id",
80001
80170
  " - omitting the sub-surface flag targets the parent Slack channel/group/DM or the parent Telegram chat",
80002
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>`",
80003
80174
  " - `--timezone <iana>` is a one-off wall-clock loop override and is frozen on the created loop record",
80004
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",
80005
80176
  "",
@@ -80012,13 +80183,14 @@ function renderLoopsHelp() {
80012
80183
  "",
80013
80184
  "Examples:",
80014
80185
  ` ${renderCliCommand("loops status --channel slack --target group:C1234567890 --thread-id 1712345678.123456")}`,
80015
- ` ${renderCliCommand("loops --channel telegram --target -1001234567890 --topic-id 42 5m")}`,
80016
- ` ${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")}`,
80017
80188
  ` ${renderCliCommand("loops cancel --channel slack --target group:C1234567890 --thread-id 1712345678.123456 abc123")}`,
80018
80189
  "Behavior:",
80019
80190
  " - `list` always renders the global persisted loop inventory",
80020
80191
  " - bare `status` is global; scoped `status --channel ... --target ...` matches `/loop status` for one routed session",
80021
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",
80022
80194
  " - the first wall-clock loop returns `confirmation_required` and does not persist until rerun with `--confirm`",
80023
80195
  " - recurring interval loops and confirmed wall-clock loops are persisted immediately and picked up by the runtime when it is running",
80024
80196
  " - if runtime is stopped, recurring loops activate on the next `clisbot start`",
@@ -80030,6 +80202,39 @@ function renderLoopsHelp() {
80030
80202
  ].join(`
80031
80203
  `);
80032
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
+ }
80033
80238
  function renderLoopInventory(params) {
80034
80239
  const lines = [
80035
80240
  renderCliCommand(`loops ${params.commandLabel}`),
@@ -80237,6 +80442,9 @@ async function getScopedLoopCounts(params) {
80237
80442
  // src/control/loops-cli.ts
80238
80443
  var LOOP_BUSY_RETRY_MS = 250;
80239
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";
80240
80448
  function getEditableConfigPath8() {
80241
80449
  return process.env.CLISBOT_CONFIG_PATH;
80242
80450
  }
@@ -80422,8 +80630,29 @@ async function executeCountLoop(params) {
80422
80630
  function stripConfirmFlag(args) {
80423
80631
  return args.filter((arg) => arg !== LOOP_CONFIRM_FLAG);
80424
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
+ }
80425
80654
  function parseCreateExpression(rawArgs, explicitCreateSubcommand) {
80426
- const expressionArgs = stripLoopContextArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs));
80655
+ const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs)));
80427
80656
  const expression = expressionArgs.join(" ").trim();
80428
80657
  if (!expression) {
80429
80658
  throw new Error("Loop creation requires an interval, count, or schedule expression.");
@@ -80444,6 +80673,43 @@ function parseLoopTimezone(args) {
80444
80673
  }
80445
80674
  return parseTimezone(timezone, "--timezone");
80446
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
+ }
80447
80713
  async function enforceLoopCreateLimits(state, parsed, maxRunsPerLoop, maxActiveLoops) {
80448
80714
  const globalLoops = await state.sessionState.listIntervalLoops();
80449
80715
  if (parsed.mode !== "times" && globalLoops.length >= maxActiveLoops) {
@@ -80469,6 +80735,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80469
80735
  const expression = parseCreateExpression(rawArgs, explicitCreateSubcommand);
80470
80736
  const parsed = parseCreateCommand(expression);
80471
80737
  let addressing = parseAddressing(rawArgs);
80738
+ const creator = parseLoopCreator(rawArgs, addressing);
80472
80739
  if (addressing.channel === "telegram" && addressing.threadId) {
80473
80740
  throw new Error("Telegram loop commands use `--topic-id`, not `--thread-id`.");
80474
80741
  }
@@ -80486,6 +80753,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80486
80753
  return {
80487
80754
  addressing,
80488
80755
  context: provisionalContext,
80756
+ creator,
80489
80757
  parsed,
80490
80758
  resolvedPrompt,
80491
80759
  resolvedTarget: provisionalResolvedTarget,
@@ -80516,6 +80784,7 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
80516
80784
  addressing,
80517
80785
  context,
80518
80786
  deliveryContext,
80787
+ creator,
80519
80788
  parsed,
80520
80789
  resolvedPrompt,
80521
80790
  resolvedTarget,
@@ -80556,6 +80825,8 @@ function buildRecurringLoopPromptMetadata(request) {
80556
80825
  promptSummary: summarizeLoopPrompt(request.resolvedPrompt.text, request.resolvedPrompt.maintenancePrompt),
80557
80826
  promptSource: request.resolvedPrompt.maintenancePrompt ? "LOOP.md" : "custom",
80558
80827
  maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
80828
+ createdBy: request.creator.providerId,
80829
+ sender: request.creator,
80559
80830
  surfaceBinding: buildLoopSurfaceBinding2(request)
80560
80831
  };
80561
80832
  }
@@ -80629,7 +80900,8 @@ function renderCalendarConfirmation(params) {
80629
80900
  nowMs: Date.now()
80630
80901
  });
80631
80902
  const timezoneClause = params.request.loopTimezone ? ` --timezone ${params.request.loopTimezone}` : "";
80632
- 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}`;
80633
80905
  return [
80634
80906
  "confirmation_required: first wall-clock loop",
80635
80907
  `proposed schedule: ${formatCalendarLoopSchedule(parsed)}`,
@@ -80742,6 +81014,10 @@ async function runLoopsCli(args) {
80742
81014
  console.log(renderLoopsHelp());
80743
81015
  return;
80744
81016
  }
81017
+ if (subcommand === "create" && (hasFlag4(args, "--help") || hasFlag4(args, "-h"))) {
81018
+ console.log(renderLoopsCreateHelp());
81019
+ return;
81020
+ }
80745
81021
  const state = await loadLoopControlState();
80746
81022
  const addressing = parseAddressing(args);
80747
81023
  if (subcommand === "list") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.45-beta.1",
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",