pi-cc-loop 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-cc-loop
2
2
 
3
- > 在 Pi 里复刻 Claude Code 的 `/loop`:固定间隔 cron、自选间隔、兜底心跳和可选状态条。
3
+ > 在 Pi 里复刻 Claude Code 的 `/loop`:固定间隔 cron、自选间隔、兜底心跳和底部状态摘要。
4
4
 
5
5
  ## 快速开始
6
6
 
@@ -14,100 +14,64 @@ pi install npm:pi-cc-loop
14
14
 
15
15
  安装后执行 `/reload`。
16
16
 
17
- ## 功能
17
+ ## 工作方式
18
+
19
+ `/loop` 和 CC 一样是一个**技能**,不是用代码直接排任务:你输入的内容会作为一条普通 user 消息进入对话,
20
+ 模型自己解析间隔并调用工具完成。所以对话里始终能看到你要求了什么、模型做了什么。
18
21
 
19
22
  | 功能 | 说明 | 入口 |
20
23
  | --- | --- | --- |
21
- | 固定间隔 | 5 字段 cron;`30s`、`5m`、`2h`、`1d`;秒向上取整到分钟 | `/loop 5m <prompt>` |
22
- | 自选间隔 | 模型每轮用 `schedule_wakeup` 决定下次延迟(60–3600s) | `/loop <prompt>` |
23
- | 自然语言间隔 | 命令层认不出时交给模型解析(中英文均可) | `/loop 每30秒 ...` |
24
- | loop.md | 不带 prompt 时运行 `.pi/loop.md`(每次触发重读,改了下一轮生效),没有则跑内置维护 prompt | `/loop`、`/loop 15m` |
25
- | 立即执行 | 创建后马上跑一次,不等首个 cron | 自动 |
24
+ | 固定间隔 | 模型用 `cron_create` 建一个 cron(`5m` → `*/5 * * * *`),然后立即执行一次;触发时按"定时任务"呈现 | `/loop 5m <prompt>`、`/loop 每分钟 ...` |
25
+ | 自选间隔 | 运行一次后用 `schedule_wakeup` 决定下次延迟(60–3600s) | `/loop <prompt>` |
26
+ | loop.md | 请求本身没有任务时(裸 `/loop` 或只给间隔)由 `.pi/loop.md` 提供,每次触发重读;首轮由模型自己读文件 | `/loop`、`/loop 15m` |
27
+ | 停止 / 查看 | 也走对话:模型用 `cron_list` / `cron_delete`(自选间隔用 `schedule_wakeup` + `stop: true`) | `/loop stop`、`/loop status` |
26
28
  | 漏跑合并 | agent 忙时到点只记一次 pending,空闲后发一条 | 自动 |
27
29
  | 兜底心跳 | 模型忘记调度时 arm 一次 20 分钟心跳,预算 1 次 | 自动 |
28
30
  | Esc 取消 | 打断属于某个自定步调 loop 的回合(它的 tick、它的 Monitor 唤醒的回合)时取消该 loop;无关回合、空闲时不受影响;固定间隔不受影响 | Esc |
29
31
  | 错峰 | 周期任务按 id 固定延后最多半个周期(≤30 分钟),间隔本身不变;`:00`/`:30` 的一次性任务最多提前 90 秒 | 自动 |
30
32
  | 过期 | 周期任务 7 天后最后执行一次再删除;一次性任务不过期 | 自动 |
31
- | 次数上限 | `--max N`,到顶自动删除 | 标志 |
32
- | 持久化 | `--durable` 写入 `.pi/scheduled_tasks.json`;同一项目只由一个 Pi 会话执行 | 标志 |
33
- | 状态条 | 输入框上方状态行,默认关闭 | `/loop widget on\|off` |
33
+ | 次数上限 | `maxFires`,到顶自动删除 | 模型传参 |
34
+ | 持久化 | `durable: true` 写入 `.pi/scheduled_tasks.json`;同一项目只由一个 Pi 会话执行 | 模型传参 |
35
+ | 底部摘要 | Pi 底部状态栏一行:`loop 2 · next 3m`,无任务时不占位 | `/loop footer on\|off` |
36
+ | 界面设置 | 仍是本地命令,不进对话 | `/loop settings ...` |
34
37
 
35
38
  ## 命令
36
39
 
37
40
  ```text
38
- /loop [间隔] <prompt> 按间隔重复跑
39
- /loop <prompt> 间隔交给模型判断
40
- /loop [间隔] 运行 .pi/loop.md 或内置维护 prompt(不带间隔则自定步调)
41
- /loop status 列出当前 loop
42
- /loop stop [id] 停止一个或全部
43
- /loop widget on|off 状态条
44
- /loop settings ... 界面设置
45
- /loop help 帮助
41
+ /loop [间隔] <prompt> 交给模型:按间隔重复,或自选节奏
42
+ /loop <prompt> 交给模型:自选间隔
43
+ /loop [间隔] 交给模型:跑 .pi/loop.md 或内置维护 prompt
44
+ /loop status 交给模型:列出当前 loop
45
+ /loop stop [id] 交给模型:停止一个或全部
46
+ /loop footer on|off 本地:底部摘要
47
+ /loop settings ... 本地:界面设置
48
+ /loop help 本地:帮助
46
49
  ```
47
50
 
48
- 标志:`--durable` 写入 `.pi/scheduled_tasks.json`;`--max N` 触发 N 次后自动停止。
49
-
50
- agent 忙时输入 `/loop`,会等当前回合结束再开始。
51
+ 前五个会作为普通 user 消息进入对话(内容就是你输入的那行),模型再按技能说明调用
52
+ `cron_create`、`cron_list`、`cron_delete`、`schedule_wakeup`。后三个只改界面,不进对话。
51
53
 
52
- 间隔写法:
54
+ 间隔写法没有约束,模型自己解析:`30s`、`5m`、`2h`、`1d`、`check the deploy every 20m`、
55
+ `每2分钟输出一次`、`每分钟打印一次hi` 都可以。小于一分钟会向上取整到每分钟。
53
56
 
54
- - `30s`、`5m`、`2h`、`1d`
55
- - `check the deploy every 20m`、`every 5 min`、`every 2 hrs`
56
- - 秒向上取整到分钟;不整除的间隔按 CC 规则取整并提示:`7m` → `*/6`,`30s` → 每分钟
57
- - 其他语言(如「每30秒」)交给模型解析
57
+ ## 底部摘要
58
58
 
59
- ## 工具
59
+ `ctx.ui.setStatus("loop", …)` 是 Pi 核心 API,不依赖任何其他扩展:
60
60
 
61
- | 工具 | 说明 |
61
+ | 场景 | 显示 |
62
62
  | --- | --- |
63
- | `cron_create` | 创建固定间隔任务;支持 `recurring`、`durable`、`maxFires` |
64
- | `cron_delete` | 按 id 取消 |
65
- | `cron_list` | 列出任务 |
66
- | `schedule_wakeup` | 自选间隔:安排下次唤醒,或 `stop: true` 结束 |
67
-
68
- `schedule_wakeup` 参数:
69
-
70
- - `delaySeconds`:60–3600,越界自动钳制
71
- - `reason`:一句原因
72
- - `prompt`:原样传回 `/loop` 的 prompt;裸 `/loop` 传 `<<loop.md>>`
73
- - `stop`:结束当前自选间隔 loop
74
-
75
- `schedule_wakeup` 调用后本回合结束(工具批次里只有它时),所以模型会先写进度再调用;
76
- 延迟按你给的值使用(四舍五入到整秒),只有超出 60–3600 秒才会被 clamp 并提示。CC 会把 `*/5` 任务提前 15 秒、把 286–300 秒的延迟压到 285 秒来保温 5 分钟的 prompt 缓存,我们没有跟:现在的缓存能活几十分钟到几小时,这样改只会让间隔变成 4 分 45 秒。
77
-
78
- ## 持久化与恢复
79
-
80
- - `--durable` / `durable: true` 的任务存在 `.pi/scheduled_tasks.json`,并自动加入 `.git/info/exclude`
81
- - 同一项目只有持有 `.pi/scheduled_tasks.lock` 的 Pi 会话执行它们;该会话退出(或 60 秒没有心跳)后由其他会话接管
82
- - Pi 没运行时错过的 durable 一次性任务会被移除,下一轮先问你要不要补跑
83
- - 从别的目录拷来的任务只列出、不执行;`.pi` 或任务文件是符号链接时拒绝写入
84
- - 恢复会话:会话内任务照常恢复,但过期的周期任务、已过时间的一次性任务除外,错过的运行不补跑;自定步调 loop 不恢复(需重新 `/loop`),`/reload` 时保留
85
-
86
- ## 设置
87
-
88
- 两个 pi-cc 扩展共享 `~/.pi/agent/pi-cc.json`:
89
-
90
- ```jsonc
91
- {
92
- "compactMessages": true, // 两行 marker / 旧的带底色卡片
93
- "compactToolRows": true, // 一行工具结果 / Pi 默认工具行
94
- "managePiCcExtensions": true // 自动维护 pi-cc-extensions 的 excludeRenderers
95
- }
96
- ```
97
-
98
- 用命令切换:
99
-
100
- ```text
101
- /loop settings
102
- /loop settings messages on|off
103
- /loop settings tools on|off
104
- ```
63
+ | 没有任务 | 不设置状态,footer 不会被多占一行 |
64
+ | 有任务 | `loop 3 · next 4m`(数量 + 最近一次触发) |
65
+ | 一分钟内 | `loop 1 · next <1m` |
66
+ | 超过一小时 | `loop 1 · next 2h 15m` |
105
67
 
106
- - 已画出的旧行需要 `/reload` 才会换样式
107
- - 环境变量覆盖:`PI_CC_COMPACT_MESSAGES=0`、`PI_CC_COMPACT_TOOL_ROWS=0`、`PI_CC_MANAGE_CCSTYLE=0`
68
+ - 倒计时取整到分钟,配合"文本相同就不调用"的去重,最多每分钟重绘一次(Pi 每次 `setStatus` 都会触发重绘)
69
+ - 装了 `pi-cc-extensions`:这一行会作为它的一个 chip 出现在第 2 行,可在它的配置面板里移动或隐藏;它会把 ANSI 去掉,所以这里只用纯文本
70
+ - 没装 `pi-cc-extensions`:Pi 内置 footer 会把它和其他扩展的状态拼成一行,加在用量行下面,只在有任务时出现
108
71
 
109
- > 如果装了 `pi-cc-extensions`,`tools on` 会把本扩展的工具名加入它的 `excludeRenderers`;
110
- > `tools off` 只移除自己加过的名字。它只在启动时读配置,切换后需要 `/reload`。
72
+ > 本扩展不写其他插件的配置文件。如果你之前在 `pi-cc-extensions.json` 里因为旧版本加过
73
+ > `cron_create`、`cron_delete`、`cron_list`、`schedule_wakeup` 这几条 `excludeRenderers`,
74
+ > 现在可以删掉:工具行由 Pi 或 pi-cc-extensions 自己渲染。
111
75
 
112
76
  ## 环境变量
113
77
 
@@ -117,6 +81,7 @@ agent 忙时输入 `/loop`,会等当前回合结束再开始。
117
81
  | `PI_LOOP_KEEPALIVE_SECONDS` | `1200` | 兜底心跳;`0` 禁用 |
118
82
  | `PI_LOOP_JITTER` | `0.5` | 周期任务最多延后的周期比例;`0` 关闭错峰 |
119
83
  | `PI_LOOP_SCAN_MS` | `1000` | 调度扫描间隔 |
84
+ | `PI_CC_FOOTER` | `1` | 底部摘要;`0` 关闭(两个 pi-cc 包共用) |
120
85
 
121
86
  ## 兼容性
122
87
 
@@ -135,7 +100,7 @@ npx --yes typescript@5.9.3 tsc --noEmit -p tsconfig.json
135
100
 
136
101
  - 行为对齐 Claude Code v2.1.306 的 `/loop`,用 Pi 扩展 API 实现。
137
102
  - Esc 只取消被打断回合所属的自定步调 loop;CC 在任何时候按 Esc(包括空闲、打断无关回合)都会取消所有自定步调 loop。其他情况用 `/loop stop`。
138
- - 状态条默认关闭,和 CC 主会话一致。
103
+ - 底部摘要默认开启,是 CC 没有的(CC 主会话不显示 loop 状态);详细面板默认关闭。
139
104
  - `cron_create` 的 `maxFires` 是额外加固;CC 没有次数上限。
140
105
  - `/loop stop` 会连带静默停掉该 chain 绑定的 Monitor。
141
106
  - Monitor 事件不会取消待触发的 wakeup,它始终是兜底心跳(与 CC 一致)。
package/config.ts CHANGED
@@ -8,13 +8,13 @@
8
8
  * ~/.pi/agent/pi-cc.json
9
9
  * {
10
10
  * "compactMessages": true,
11
- * "compactToolRows": true
11
+ * "footer": true
12
12
  * }
13
13
  *
14
14
  * Environment overrides (useful for tests and quick experiments):
15
15
  * PI_CC_CONFIG_FILE=/path/to/pi-cc.json
16
16
  * PI_CC_COMPACT_MESSAGES=0|1
17
- * PI_CC_COMPACT_TOOL_ROWS=0|1
17
+ * PI_CC_FOOTER=0|1
18
18
  */
19
19
 
20
20
  import fs from "node:fs";
@@ -24,22 +24,16 @@ import path from "node:path";
24
24
  export interface PiCcConfig {
25
25
  /** Two-line marker blocks instead of the padded card. */
26
26
  compactMessages: boolean;
27
- /** One-line tool results instead of Pi's "N lines returned" preview. */
28
- compactToolRows: boolean;
29
27
  /**
30
- * Keep pi-cc tools out of pi-cc-extensions' global tool styling so the
31
- * compact rows above are visible. Only names this extension added are
32
- * removed again when the setting is turned off.
28
+ * Show a one-line summary in Pi's footer (loops armed, monitors running).
29
+ * Shared so one switch silences both packages.
33
30
  */
34
- managePiCcExtensions: boolean;
35
- /** Internal: names this extension added to pi-cc-extensions.json. */
36
- managedExcludes?: string[];
31
+ footer: boolean;
37
32
  }
38
33
 
39
34
  export const DEFAULT_CONFIG: PiCcConfig = {
40
35
  compactMessages: true,
41
- compactToolRows: true,
42
- managePiCcExtensions: true,
36
+ footer: true,
43
37
  };
44
38
 
45
39
  function envBool(name: string): boolean | undefined {
@@ -69,17 +63,8 @@ function readConfig(): PiCcConfig {
69
63
  compactMessages:
70
64
  envBool("PI_CC_COMPACT_MESSAGES") ??
71
65
  (typeof file.compactMessages === "boolean" ? file.compactMessages : DEFAULT_CONFIG.compactMessages),
72
- compactToolRows:
73
- envBool("PI_CC_COMPACT_TOOL_ROWS") ??
74
- (typeof file.compactToolRows === "boolean" ? file.compactToolRows : DEFAULT_CONFIG.compactToolRows),
75
- managePiCcExtensions:
76
- envBool("PI_CC_MANAGE_CCSTYLE") ??
77
- (typeof file.managePiCcExtensions === "boolean"
78
- ? file.managePiCcExtensions
79
- : DEFAULT_CONFIG.managePiCcExtensions),
80
- managedExcludes: Array.isArray(file.managedExcludes)
81
- ? file.managedExcludes.filter((name): name is string => typeof name === "string")
82
- : [],
66
+ footer:
67
+ envBool("PI_CC_FOOTER") ?? (typeof file.footer === "boolean" ? file.footer : DEFAULT_CONFIG.footer),
83
68
  };
84
69
  }
85
70
 
@@ -101,73 +86,17 @@ export function saveConfig(patch: Partial<PiCcConfig>): PiCcConfig {
101
86
  const next = { ...loadConfig(), ...patch };
102
87
  const file = configPath();
103
88
  fs.mkdirSync(path.dirname(file), { recursive: true });
104
- const temp = `${file}.tmp`;
105
- fs.writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
106
- fs.renameSync(temp, file);
107
- cached = next;
108
- return next;
109
- }
110
-
111
- /**
112
- * pi-cc-extensions' config file. That extension loads its config once at load
113
- * time, so changes made here apply after `/reload`.
114
- */
115
- export function piCcExtensionsConfigPath(): string {
116
- const agentDir =
117
- process.env.PI_CODING_AGENT_DIR ?? process.env.PI_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
118
- return process.env.PI_CC_EXTENSIONS_CONFIG_FILE ?? path.join(agentDir, "pi-cc-extensions.json");
119
- }
120
-
121
- /**
122
- * Add or remove our tool names in pi-cc-extensions' `excludeRenderers`.
123
- *
124
- * Returns true when the file changed (the caller can tell the user to reload).
125
- * Only names this extension added are removed again; a name the user put there
126
- * themselves is left alone.
127
- */
128
- export function syncPiCcExtensionsExcludes(toolNames: readonly string[], enabled: boolean): boolean {
129
- // Another package may have written managedExcludes since our last read.
130
- invalidateConfig();
131
- const config = loadConfig();
132
- if (!config.managePiCcExtensions) return false;
133
- const file = piCcExtensionsConfigPath();
134
- if (!fs.existsSync(file)) return false;
135
-
136
- let raw: Record<string, unknown>;
89
+ // Keep keys this version does not know about: the file is the user's.
90
+ let raw: Record<string, unknown> = {};
137
91
  try {
138
92
  const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
139
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
140
- raw = parsed as Record<string, unknown>;
93
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) raw = parsed as Record<string, unknown>;
141
94
  } catch {
142
- return false;
143
- }
144
-
145
- const current = Array.isArray(raw.excludeRenderers)
146
- ? raw.excludeRenderers.filter((name): name is string => typeof name === "string")
147
- : [];
148
- const managed = new Set(config.managedExcludes ?? []);
149
- const next = new Set(current);
150
- let changed = false;
151
-
152
- for (const name of toolNames) {
153
- if (enabled) {
154
- if (!next.has(name)) {
155
- next.add(name);
156
- managed.add(name);
157
- changed = true;
158
- }
159
- } else if (managed.has(name)) {
160
- if (next.delete(name)) changed = true;
161
- managed.delete(name);
162
- }
95
+ // A missing or malformed file is rewritten from scratch.
163
96
  }
164
-
165
- if (!changed && managed.size === (config.managedExcludes ?? []).length) return false;
166
-
167
- raw.excludeRenderers = [...next].sort((a, b) => a.localeCompare(b));
168
97
  const temp = `${file}.tmp`;
169
- fs.writeFileSync(temp, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
98
+ fs.writeFileSync(temp, `${JSON.stringify({ ...raw, ...next }, null, 2)}\n`, "utf8");
170
99
  fs.renameSync(temp, file);
171
- saveConfig({ managedExcludes: [...managed].sort((a, b) => a.localeCompare(b)) });
172
- return changed;
100
+ cached = next;
101
+ return next;
173
102
  }
package/cron.ts CHANGED
@@ -154,36 +154,7 @@ export function isUnreachable(expression: string): boolean {
154
154
  return nextFireAfter(fields, new Date(), 366) === null;
155
155
  }
156
156
 
157
- export interface IntervalSpec {
158
- /** Number of seconds the schedule actually uses. */
159
- seconds: number;
160
- /** Cron expression that implements the interval. */
161
- cron: string;
162
- /** Human readable cadence, e.g. "every 5 minutes". */
163
- human: string;
164
- /** True when the requested interval had to be adjusted. */
165
- adjusted: boolean;
166
- /** What the user asked for, in seconds. */
167
- requestedSeconds: number;
168
- /** Human readable explanation of the adjustment, for the user. */
169
- note?: string;
170
- }
171
-
172
- const UNIT_SECONDS: Record<string, number> = {
173
- s: 1,
174
- m: 60,
175
- h: 3600,
176
- d: 86400,
177
- };
178
-
179
157
  /** The unit words Claude Code's /loop accepts in a trailing "every N <unit>" clause. */
180
- const UNIT_WORDS: Record<string, string[]> = {
181
- s: ["s", "sec", "secs", "second", "seconds"],
182
- m: ["m", "min", "mins", "minute", "minutes"],
183
- h: ["h", "hr", "hrs", "hour", "hours"],
184
- d: ["d", "day", "days"],
185
- };
186
-
187
158
  /**
188
159
  * The divisor of `total` closest to `n`.
189
160
  *
@@ -206,99 +177,17 @@ function nearestDivisor(n: number, total: number): number {
206
177
  }
207
178
 
208
179
  /**
209
- * Convert a duration into a cron expression, following Claude Code's rule:
210
- *
211
- * "Supported suffixes: `s` -> ceil to nearest minute, `m` (minutes),
212
- * `h` (hours), `d` (days). Examples: `5m` -> a 5 minute step, `1h` -> `0 * * * *`,
213
- * `1d` -> `0 0 * * *`. If the interval doesn't cleanly divide its unit, round to the
214
- * nearest clean interval and tell the user what you rounded to."
180
+ * Parse `每2分钟输出一次` / `输出一次 每隔 2 小时`: an explicit interval written
181
+ * in Chinese. The digit is required on purpose: `每天都有一个测试失败` is a
182
+ * prompt, not a schedule, and Claude Code's own splitting is just as literal.
215
183
  */
216
- export function intervalToCron(seconds: number): IntervalSpec | null {
217
- if (!Number.isFinite(seconds) || seconds <= 0) return null;
218
-
219
- // Sub-minute: the scheduler's granularity is one minute, so round up.
220
- if (seconds < 60) {
221
- return {
222
- seconds: 60,
223
- cron: "* * * * *",
224
- human: "every minute",
225
- adjusted: true,
226
- requestedSeconds: seconds,
227
- note: `${seconds}s rounded up to 1 minute (the scheduler's minimum granularity is 1 minute)`,
228
- };
229
- }
230
-
231
- // "Seconds are rounded up to the nearest minute since cron has one-minute
232
- // granularity."
233
- if (seconds < 3600 && Math.ceil(seconds / 60) < 60) {
234
- const requested = Math.ceil(seconds / 60);
235
- const minutes = nearestDivisor(requested, 60);
236
- return {
237
- seconds: minutes * 60,
238
- cron: `*/${minutes} * * * *`,
239
- human: `every ${minutes} minute${minutes === 1 ? "" : "s"}`,
240
- adjusted: minutes !== requested,
241
- requestedSeconds: seconds,
242
- ...(minutes !== requested
243
- ? { note: `${requested}m rounded to ${minutes}m, the nearest clean interval` }
244
- : {}),
245
- };
246
- }
247
-
248
- if (seconds < 86400) {
249
- const requested = Math.max(1, Math.round(seconds / 3600));
250
- const hours = nearestDivisor(requested, 24);
251
- return {
252
- seconds: hours * 3600,
253
- cron: hours === 1 ? "0 * * * *" : `0 */${hours} * * *`,
254
- human: `every ${hours} hour${hours === 1 ? "" : "s"}`,
255
- adjusted: hours !== requested,
256
- requestedSeconds: seconds,
257
- ...(hours !== requested
258
- ? { note: `${requested}h rounded to ${hours}h, the nearest clean interval` }
259
- : {}),
260
- };
261
- }
262
-
263
- const days = Math.max(1, Math.round(seconds / 86400));
264
- return {
265
- seconds: days * 86400,
266
- cron: days === 1 ? "0 0 * * *" : `0 0 */${days} * *`,
267
- human: days === 1 ? "every day" : `every ${days} days`,
268
- adjusted: days * 86400 !== seconds,
269
- requestedSeconds: seconds,
270
- };
271
- }
272
-
273
- /** Parse a duration token such as `5m`, `30s`, `2h`, `1d`. */
274
- export function parseIntervalToken(token: string): number | null {
275
- const m = /^(\d+)\s*([smhd])$/i.exec(token.trim());
276
- if (!m) return null;
277
- const unit = m[2].toLowerCase();
278
- const amount = Number(m[1]);
279
- if (!Number.isFinite(amount) || amount <= 0) return null;
280
- return amount * UNIT_SECONDS[unit];
281
- }
282
-
283
184
  /**
284
185
  * Parse a trailing `every 20m` / `every 5 min` / `every 2 hours` clause, with
285
186
  * the unit words Claude Code accepts.
286
187
  */
287
- export function parseEveryClause(input: string): { interval: number; prompt: string } | null {
288
- const m = /\bevery\s+(\d+)\s*([a-z]+)\s*$/i.exec(input.trim());
289
- if (!m) return null;
290
- const amount = Number(m[1]);
291
- if (!(amount > 0)) return null;
292
- const word = m[2].toLowerCase();
293
- const unit = Object.keys(UNIT_WORDS).find((key) => UNIT_WORDS[key].includes(word));
294
- if (!unit) return null;
295
- const unitSeconds = UNIT_SECONDS[unit];
296
- const prompt = input.trim().slice(0, m.index).trim();
297
- return { interval: amount * unitSeconds, prompt };
298
- }
299
-
300
188
  const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
301
189
 
190
+
302
191
  /**
303
192
  * A human readable cadence for a cron expression, with Claude Code's labels:
304
193
  * "Every minute", "Every 5 minutes", "Every hour at :07", "Every 2 hours",