@cmodjs/core 0.1.12 → 0.1.13

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/docs/commands.md CHANGED
@@ -68,8 +68,12 @@ cmod try <owner/repo | path>... [--yes] [--home <folder>] [-- claude arguments]
68
68
 
69
69
  Starts one throwaway Claude Code session with mods set up, for trying a mod by hand or testing it with an agent. Installs each mod's packages, installs the cmod plugin when Claude Code lacks it, and runs each mod's install step after asking consent. Then starts one Claude Code session with every mod loaded through `--plugin-dir`.
70
70
 
71
- - The session runs with a new, empty home folder, so it sees only the mods named here and changes none of your Claude Code settings, mods, key bindings, or history. The folder is deleted when the session ends.
72
- - The session keeps `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY`. Set one of them to skip the login. `claude setup-token` prints a token for `CLAUDE_CODE_OAUTH_TOKEN`.
71
+ - The session runs with a new home folder, so it sees only the mods named here and changes none of your Claude Code settings, mods, key bindings, or history. The folder is deleted when the session ends.
72
+ - The session stays logged in as you, with no login screen:
73
+ - On macOS, Claude Code reads your login from the keychain, which `cmod try` links into the new home as `~/Library/Keychains`. Deleting the home removes only the link.
74
+ - Elsewhere, `CLAUDE_SECURESTORAGE_CONFIG_DIR` points Claude Code at the folder that holds your login, so it reads and refreshes that login where it is.
75
+ - `ANTHROPIC_CONFIG_DIR` points at your `~/.config/anthropic` login profiles, and `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` pass through.
76
+ - The session keeps how Claude Code reaches the model and what it must never do, so a try is as safe as your own session. From your `settings.json` it keeps `env` (a proxy or `ANTHROPIC_BASE_URL`, without `cmod link`'s `CLAUDE_CODE_PLUGIN_DIRS`), the credential helpers (`apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `gcpAuthRefresh`, `otelHeadersHelper`, `proxyAuthHelper`), the login pins (`forceLoginMethod`, `forceLoginOrgUUID`, `forceLoginGatewayUrl`, `gatewayInternalNetworks`), `sandbox`, and `permissions.deny`. From `~/.claude.json` it keeps your account and the API keys you approved. A `~/` in a helper still means your own home. Hooks, plugins, allow rules, the model, and every other preference stay behind. Managed settings apply as always.
73
77
  - `--home <folder>` runs in that folder and keeps it, so the next `cmod try --home <folder>` starts from it. `--home ~` runs in your own home, with your settings and mods.
74
78
  - Unlike `cmod link`, it builds no program from `cli/`. A mod whose `package.json` `cmod.program` names a program downloads it from the GitHub release of the mod's version, so it fails until `cmod publish` has released that version.
75
79
  - When the session ends, even when its terminal closes, it runs each mod's uninstall step and deletes its record, data folder, approval, key bindings, and program, so the mod stays uninstalled. A checkout already set up from the same folder keeps its setup.
@@ -109,7 +113,7 @@ It needs, and refuses to start without:
109
113
  - the network, to hash the cmod plugin release it lists, unless the mod is cmod itself
110
114
  - for a real publish, a working tree with no uncommitted changes, a `v<version>` tag that does not exist yet, a `git push` to `origin` that succeeds, and the `gh` command, which creates the GitHub release
111
115
 
112
- `--dry-run` builds and writes everything, and pushes nothing. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
116
+ `--dry-run` builds everything into a temporary folder, and pushes nothing. It writes `marketplace.json` there too, so the working tree stays as it was. It allows uncommitted changes and an existing tag, and builds from the last commit. It prints the folder that holds the release, so you can read what the directory will read.
113
117
 
114
118
  ## Install mods
115
119
 
package/docs/index.md CHANGED
@@ -56,10 +56,11 @@ A program the mod needs lives as source in `cli/`. `cmod publish` builds it and
56
56
  | --- | --- |
57
57
  | How do I define a mod, and what can `mod` call? | [mod.md](mod.md) |
58
58
  | How does a mod watch something outside Claude Code, tell Claude about it, or ask a model? | [mod.md](mod.md#mod-every) |
59
+ | How does a mod give Claude a prompt on a schedule, like `/loop`? | [jobs.md](jobs.md#schedule) |
59
60
  | How do I keep values across prompts, sessions, and projects? How does a person change them? | [state.md](state.md) |
60
61
  | How do I react to a Claude Code event, and what can a hook answer? | [hooks.md](hooks.md) |
61
62
  | How do I draw a pane, change a row Claude Code draws, or ask the person something? | [ui.md](ui.md) |
62
- | How do I add a slash command, a tool, a permission rule, a check, a prompt, a status line, or a background program? | [jobs.md](jobs.md) |
63
+ | How do I add a slash command, a tool, a permission rule, a check, a prompt, a schedule, a status line, or a background program? | [jobs.md](jobs.md) |
63
64
  | How does one mod call another? | [dependencies.md](dependencies.md) |
64
65
  | How does a mod change the machine, bind a key, ship a program, or clean up after itself? | [install-steps.md](install-steps.md) |
65
66
  | How do I test a mod without Claude Code? | [testing.md](testing.md) |
@@ -89,6 +90,7 @@ A mod imports from the files below, and every other file in this package is inte
89
90
  | `jobs/permissions.js` | `permissions`, `Rule`, `PermissionRules`, `Target`, `CommandCall`, `FileCall`, `FetchCall`, `ToolCall` | [jobs.md](jobs.md) |
90
91
  | `jobs/check.js` | `check` | [jobs.md](jobs.md) |
91
92
  | `jobs/prompt.js` | `prompt` | [jobs.md](jobs.md) |
93
+ | `jobs/schedule.js` | `schedule`, `Schedule` | [jobs.md](jobs.md) |
92
94
  | `jobs/status-line.js` | `statusLine`, `Usage` | [jobs.md](jobs.md) |
93
95
  | `jobs/program.js` | `program`, `Program` | [jobs.md](jobs.md) |
94
96
  | `jobs/context.js` | `afterCall`, `modWithin`, `modOf`, `workspaceReader` | [jobs.md](jobs.md) |
@@ -38,11 +38,13 @@ Before a step runs for the first time, cmod shows the person the commands and th
38
38
 
39
39
  - In Claude Code, the mod asks `<mod> runs <install> to install, runs <uninstall> when you remove it and binds shift+tab to /mode. Install it now?` with `Install` and `Not now`, naming only what the mod has. A progress line above the prompt shows the install.
40
40
  - In a session with no screen, such as `claude -p`, the mod waits and logs `<mod> waits for consent to run <install>. Run cmod install <mod> in a terminal.`
41
- - In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands and ask `Run them? [y/N]`. `--yes` approves without asking.
41
+ - In a terminal, `cmod install`, `cmod link`, `cmod try`, and `cmod update` print the commands, the program, and the keys, and ask `Set up <mod>? [y/N]`. `--yes` approves without asking.
42
42
  - `Not now` leaves the mod off and logs `<mod> is not installed. Run cmod install <mod> to install it.`
43
43
 
44
44
  The mod starts only once its install step has run ([mod.md](mod.md)).
45
45
 
46
+ In Claude Code, the mod sets itself up through the `cmod` that PATH finds, and only through one that knows every step it has. A mod with `keys` waits for cmod 0.1.12 or later, because an older cmod skips the keys. While PATH finds an older one, the line above the prompt says `PATH finds cmod <version>, and <mod> needs cmod 0.1.12 or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.` The mod sets up as soon as a new enough cmod answers, such as the one the cmod plugin installs into `~/.local/bin` at its first start.
47
+
46
48
  ## Key bindings
47
49
 
48
50
  A plugin cannot bind a key in Claude Code: Claude Code reads key bindings only from the person's `keybindings.json`. So cmod writes the mod's `keys` there, with consent, as the setup's last step:
package/docs/jobs.md CHANGED
@@ -10,7 +10,7 @@ type Job<Handle, State> = (context: JobContext<State>) => Handle
10
10
 
11
11
  Each job function, such as `slashCommand(options)`, returns a `Job`. `mod.use` runs the job and returns its handle. A job that has nothing to hand back returns `void`.
12
12
 
13
- A job's bad options throw at one of two times. `check`, `permissions`, and `prompt` check their options when the job function is called. `slashCommand`, `tool`, `statusLine`, and `program` check theirs when the job runs, at `mod.use`, so the error surfaces as `its setup function threw: …`.
13
+ A job's bad options throw at one of two times. `check`, `permissions`, `prompt`, and `schedule` check their options when the job function is called. `slashCommand`, `tool`, `statusLine`, and `program` check theirs when the job runs, at `mod.use`, so the error surfaces as `its setup function threw: …`.
14
14
 
15
15
  | Job | Import | Adds |
16
16
  | --- | --- | --- |
@@ -19,6 +19,7 @@ A job's bad options throw at one of two times. `check`, `permissions`, and `prom
19
19
  | `permissions` | `jobs/permissions.js` | Rules that deny a call, or ask the person first |
20
20
  | `check` | `jobs/check.js` | A command that runs after matching calls, and reports a failure to Claude |
21
21
  | `prompt` | `jobs/prompt.js` | Text Claude reads: kept current with the mod's state, before matching prompts, or after matching calls |
22
+ | `schedule` | `jobs/schedule.js` | A prompt Claude Code sends Claude on a cron schedule, kept current with the mod's state |
22
23
  | `statusLine` | `jobs/status-line.js` | A status line |
23
24
  | `program` | `jobs/program.js` | A background program the mod talks to over HTTP |
24
25
 
@@ -285,6 +286,35 @@ export const conventions = defineMod({
285
286
  })
286
287
  ```
287
288
 
289
+ ## schedule
290
+
291
+ ```ts
292
+ schedule<State>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State>
293
+
294
+ type Schedule = { readonly cron: string; readonly prompt: string }
295
+ ```
296
+
297
+ `schedule` gives Claude a prompt on a schedule, through the cron jobs Claude Code keeps for `/loop`. `cron` is a standard 5-field cron in local time, such as `*/10 * * * *` for every 10 minutes. At each fire Claude Code shows one `Running scheduled task` row and Claude answers the prompt.
298
+
299
+ - A function of the state follows it. After each change to `mod.state`, cmod calls it again, and when its answer differs, cmod deletes the cron and makes the new one. `undefined` deletes the cron and makes none, which is how a mod pauses.
300
+ - Claude Code fires a cron only while Claude is idle, on whole minutes, up to a tenth of the period late. A recurring cron expires after 7 days.
301
+ - A cron lasts only as long as the session. The job makes it again each time the mod starts, and deletes the one it made before `/reload-plugins`, so a cron is never doubled.
302
+ - A blank `cron` or `prompt` throws. A function that throws keeps the last cron, and a cron Claude Code refuses writes a log line naming why.
303
+ - Work that needs no turn of Claude's, such as polling a file, belongs in `mod.every`. A mod pauses a schedule by turning it off: a hook that drops a fire still leaves two rows, `Running scheduled task` and `Prompt dropped by a hook:`.
304
+
305
+ ```ts
306
+ import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
307
+ import { schedule } from '../node_modules/@cmodjs/core/jobs/schedule.js'
308
+
309
+ export const loop = defineMod({
310
+ name: 'loop',
311
+ state: { project: { isOn: false, cron: '*/10 * * * *', prompt: 'Check the CI and fix what failed.' } },
312
+ setup(mod) {
313
+ mod.use(schedule((state) => (state.project.isOn ? { cron: state.project.cron, prompt: state.project.prompt } : undefined)))
314
+ },
315
+ })
316
+ ```
317
+
288
318
  ## statusLine
289
319
 
290
320
  ```ts
@@ -441,6 +471,7 @@ Each member calls the member of the same name in Claude Code's hooks API, `$`, w
441
471
  | `model.complete(request, options?)` | What `mod.model.complete` calls. |
442
472
  | `command.register(spec)` | Adds a slash command. `slashCommand` is built on it. |
443
473
  | `tool.register(spec)` | Adds a tool. `tool` is built on it. |
474
+ | `tool.call({ tool, ...input })` | Calls a tool the way Claude does, through the permission check, and resolves `{ result, text, isError? }` or `{ deny }`. `schedule` is built on it, calling `CronCreate` and `CronDelete`. |
444
475
  | `agent.list()` | The session's subagents and teammates so far. |
445
476
  | `agent.spawn(args)` | What `mod.agent.spawn` calls. |
446
477
  | `env.home()` | The `HOME` variable, or `undefined` when it is unset. |
package/docs/mod.md CHANGED
@@ -257,7 +257,7 @@ mod.on('SubagentStop', async ({ agent_id }) => {
257
257
 
258
258
  `append` adds `text` to the conversation as a user message the person does not see, so Claude reads it on its next call to the model. It starts no turn. It rejects with `<mod>: Claude Code refused the note: <reason>` when a plugin's `session.append` handler refuses it.
259
259
 
260
- `submit` sends `text` as the person's next prompt, so Claude answers it as a turn of its own. Claude Code holds it until the session is idle, so it never cuts into a turn. It rejects with `<mod>: Claude Code dropped the prompt: <reason>` when a plugin drops it.
260
+ `submit` sends `text` as the person's next prompt, so Claude answers it as a turn of its own. Claude Code holds it until the session is idle, so it never cuts into a turn, and shows it under `The <mod> plugin sent a message:`. It rejects with `<mod>: Claude Code dropped the prompt: <reason>` when a plugin drops it. A prompt that comes on a clock, such as every 10 minutes, belongs in a `schedule` job instead, which Claude Code shows as one `Running scheduled task` row ([jobs.md](jobs.md#schedule)).
261
261
 
262
262
  ```ts
263
263
  mod.every(60_000, async () => {
package/docs/testing.md CHANGED
@@ -220,6 +220,7 @@ type Fakes = {
220
220
  agent: { list?, spawn? }
221
221
  clock: { after?, every? }
222
222
  cmod: { call? }
223
+ tool: { call? }
223
224
  }
224
225
  ```
225
226
 
@@ -233,6 +234,7 @@ Set a fake to answer a call:
233
234
  - `clock.after(ms, fn)` and `clock.every(ms, fn)` each return `{ cancel() }`, and so must a fake of either. `clock.after` runs a real timer by default. `clock.every` never fires unless a fake keeps `fn` for the test to call, so a test of `mod.every` keeps it and calls it once per tick.
234
235
  - `session.append` and `prompt.submit` take every note and prompt by default, into `shown.notes` and `shown.prompts`. A fake that answers `{ deny }` or `{ drop }` refuses one.
235
236
  - `model.complete` has no default: set it to the answer the model gives, such as `async () => ({ isAnswered: true, text: 'no', usage })`.
237
+ - `tool.call` answers `CronCreate`, `CronDelete`, and `CronList` by default, keeping the crons a `schedule` makes in `shown.schedules`. A call to any other tool needs a fake.
236
238
 
237
239
  ```ts
238
240
  import { expect, test } from 'bun:test'
@@ -263,10 +265,11 @@ type Shown = {
263
265
  readonly openPanes: Set<string>
264
266
  readonly commands: string[]
265
267
  readonly tools: string[]
268
+ readonly schedules: { readonly id: string; readonly cron: string; readonly prompt: string }[]
266
269
  }
267
270
  ```
268
271
 
269
- What the person and Claude would see: the toasts, the notes the mod added for Claude with `mod.session.append` or a `prompt` that follows the state, the prompts it sent with `mod.session.submit`, the log lines, the debug log lines, each status line text, the open panes, and the slash commands and tools the mod added.
272
+ What the person and Claude would see: the toasts, the notes the mod added for Claude with `mod.session.append` or a `prompt` that follows the state, the prompts it sent with `mod.session.submit`, the log lines, the debug log lines, each status line text, the open panes, the slash commands and tools the mod added, and the crons its `schedule` keeps now.
270
273
 
271
274
  ```ts
272
275
  test('the Mode prompt follows the mode', async () => {
@@ -0,0 +1,7 @@
1
+ import type { Job } from '../mod.js';
2
+ export type Schedule = {
3
+ readonly cron: string;
4
+ readonly prompt: string;
5
+ };
6
+ export declare function schedule<State extends object = Record<never, never>>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State>;
7
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/jobs/schedule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAIpC,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAIzE,wBAAgB,QAAQ,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,QAAQ,GAAG,SAAS,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAmDlJ"}
@@ -0,0 +1,66 @@
1
+ import { onStateChange } from '../runtime/state.js';
2
+ import { messageOf } from '../utils/text.js';
3
+ const scheduleCounts = new WeakMap();
4
+ export function schedule(entry) {
5
+ if (typeof entry !== 'function')
6
+ checked(entry);
7
+ return (job) => {
8
+ const { mod, claude } = job;
9
+ const order = scheduleCounts.get(mod) ?? 0;
10
+ scheduleCounts.set(mod, order + 1);
11
+ const storeKey = `cmod:schedule:${order}`;
12
+ const log = (reason) => claude.ui.log(`${mod.name}: the schedule ${reason}`);
13
+ let current;
14
+ let isQueued = false;
15
+ const remove = async (id) => {
16
+ await claude.tool.call({ tool: 'CronDelete', id }).catch(() => undefined);
17
+ await claude.store.delete(storeKey);
18
+ };
19
+ const update = async () => {
20
+ isQueued = false;
21
+ let next;
22
+ try {
23
+ next = typeof entry === 'function' ? entry(mod.state) : entry;
24
+ if (next !== undefined)
25
+ checked(next);
26
+ }
27
+ catch (error) {
28
+ log(`kept its last cron: ${messageOf(error)}`);
29
+ return;
30
+ }
31
+ const wanted = next === undefined ? undefined : JSON.stringify([next.cron, next.prompt]);
32
+ if (wanted === current?.wanted)
33
+ return;
34
+ if (current !== undefined)
35
+ await remove(current.id);
36
+ current = undefined;
37
+ if (next === undefined)
38
+ return;
39
+ const answer = await claude.tool.call({ tool: 'CronCreate', cron: next.cron, prompt: next.prompt, recurring: true });
40
+ if (answer.deny !== undefined)
41
+ return log(`was refused: ${answer.deny}`);
42
+ if (answer.isError === true)
43
+ return log(`failed: ${answer.text ?? 'CronCreate answered an error'}`);
44
+ current = { id: answer.result.id, wanted: wanted };
45
+ await claude.store.set(storeKey, current.id);
46
+ };
47
+ let updates = claude.store
48
+ .get(storeKey)
49
+ .then((left) => (typeof left === 'string' ? remove(left) : undefined))
50
+ .then(update)
51
+ .catch((error) => log(`failed: ${messageOf(error)}`));
52
+ onStateChange(mod.state, () => {
53
+ if (isQueued)
54
+ return;
55
+ isQueued = true;
56
+ updates = updates.then(update).catch((error) => log(`failed: ${messageOf(error)}`));
57
+ });
58
+ job.announce(typeof entry === 'function' ? 'a schedule from its state' : `a schedule at ${entry.cron}`);
59
+ };
60
+ }
61
+ function checked(entry) {
62
+ if (entry.cron.trim() === '')
63
+ throw new Error('schedule: give it a cron, such as "*/10 * * * *" for every 10 minutes.');
64
+ if (entry.prompt.trim() === '')
65
+ throw new Error('schedule: give it the prompt Claude gets at each fire.');
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmodjs/core",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Build Claude Code mods: hooks, panes, permissions, checks, and an install that just works.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/records.d.ts CHANGED
@@ -53,6 +53,7 @@ export declare function configFolders(env: Record<string, string | undefined>, n
53
53
  export declare function readRecord(read: ReadFile, store: string, name: string): Promise<InstallRecord | undefined>;
54
54
  export declare function writeRecord(write: (path: string, text: string) => Promise<void>, store: string, record: InstallRecord): Promise<void>;
55
55
  export declare function readSteps(pkg: unknown): Steps | undefined;
56
+ export declare function oldestCmodFor(keys: Readonly<Record<string, string>>): string;
56
57
  export declare function keyWords(keys: Readonly<Record<string, string>> | undefined): string;
57
58
  export declare function scriptPaths(command: string): string[];
58
59
  export declare function scriptsSha256(steps: Steps, files: {
package/records.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["src/records.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["src/records.ts"],"names":[],"mappings":"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"}
package/records.js CHANGED
@@ -110,6 +110,9 @@ function readKeys(keys) {
110
110
  }
111
111
  return { keys: commands };
112
112
  }
113
+ export function oldestCmodFor(keys) {
114
+ return Object.keys(keys).length > 0 ? '0.1.12' : '0.0.0';
115
+ }
113
116
  export function keyWords(keys) {
114
117
  return listed(Object.entries(keys ?? {}).map(([key, command]) => `${key} to /${command}`));
115
118
  }
package/register.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAkF7C,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CA8BrG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAIzD"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["src/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAgB,MAAM,aAAa,CAAA;AACrG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAkF7C,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CA8BrG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,CAIzD"}
package/register.js CHANGED
@@ -54,7 +54,7 @@ async function startMod($, eventInput, passOn) {
54
54
  prompt: { submit: (args) => $.prompt.submit(args) },
55
55
  model: { complete: (request, options) => $.model.complete(request, options) },
56
56
  command: { register: (command) => $.command.register(command) },
57
- tool: { register: (tool) => $.tool.register(tool) },
57
+ tool: { register: (tool) => $.tool.register(tool), call: ((input) => $.tool.call(input)) },
58
58
  agent: {
59
59
  list: () => $.agent.list(),
60
60
  spawn: (args) => $.agent.spawn(args),
@@ -70,6 +70,7 @@ export type Claude = {
70
70
  register(tool: ToolSpec): Promise<{
71
71
  tool: string;
72
72
  }>;
73
+ call: EngineInterface['tool']['call'];
73
74
  };
74
75
  readonly agent: {
75
76
  list(): Promise<AgentInfo[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../src/runtime/claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../src/runtime/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,gBAAgB,EAChB,IAAI,EACJ,eAAe,EACf,UAAU,EACV,WAAW,EACX,YAAY,EACZ,OAAO,EACP,MAAM,EACN,aAAa,EACb,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,YAAY,EACZ,cAAc,EACf,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,QAAQ,CAAC,EAAE,EAAE;QACX,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;QACjD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;QACtC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;QAC/C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;QACzD,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,IAAI,CAAA;QAC/C,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAChF,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;QAC/C,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QACzC,KAAK,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;QACnC,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;QACnD,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC,aAAa,CAAC,CAAA;KACtD,CAAA;IACD,QAAQ,CAAC,OAAO,EAAE;QAChB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;QAC9E,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAAA;KACvF,CAAA;IACD,QAAQ,CAAC,EAAE,EAAE;QACX,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QACnC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAChD,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QACvC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAC7D,CAAA;IACD,QAAQ,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;KAAE,CAAA;IAC7E,QAAQ,CAAC,QAAQ,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IACvE,QAAQ,CAAC,KAAK,EAAE;QACd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;KAC1B,CAAA;IACD,QAAQ,CAAC,KAAK,EAAE;QACd,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;QACtB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,KAAK,CAAA;QACxC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,KAAK,CAAA;KACzC,CAAA;IACD,QAAQ,CAAC,OAAO,EAAE;QAChB,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;QACrB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;QACvB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;QACtB,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;QACxB,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;QAC9B,QAAQ,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAA;QAC7C,QAAQ,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAA;QAChD,MAAM,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAA;KAC7C,CAAA;IACD,QAAQ,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAChE,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAA;KAAE,CAAA;IAClE,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAA;IAClF,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7G,QAAQ,CAAC,KAAK,EAAE;QACd,IAAI,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;QAC5B,KAAK,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;KACvD,CAAA;IACD,QAAQ,CAAC,GAAG,EAAE;QACZ,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;QACnC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;QACvC,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;KAC1C,CAAA;IACD,QAAQ,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;CACpE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAIjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAkCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAE,MAAM,OAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,CAsOvJ"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAIjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAkCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAE,MAAM,OAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,CAiPvJ"}
@@ -1,4 +1,4 @@
1
- import { dataFolder, isAtLeast, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder } from '../records.js';
1
+ import { dataFolder, isAtLeast, oldestCmodFor, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder } from '../records.js';
2
2
  import { relativePath } from '../utils/paths.js';
3
3
  import { formatExit, listed, messageOf } from '../utils/text.js';
4
4
  import { beforeDeadline } from './deadline.js';
@@ -189,8 +189,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
189
189
  if (plugin.name === cmodPluginName)
190
190
  return bootstrap(plugin.root);
191
191
  phase = 'installing';
192
- if ((await cmodVersion(claude())) === undefined)
193
- return waitForcmod();
192
+ const found = await cmodVersion(claude());
193
+ if (!fits(found))
194
+ return waitForcmod(found);
194
195
  endLine();
195
196
  const progress = showLine();
196
197
  let outcome;
@@ -209,9 +210,11 @@ export function createLifecycle(definition, checksPermissions = () => true) {
209
210
  const reason = outcome?.kind === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
210
211
  fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
211
212
  };
212
- const waitForcmod = () => {
213
+ const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.keys ?? {}));
214
+ const waitForcmod = (found) => {
213
215
  phase = 'waiting';
214
216
  showLine().wait('Waiting for cmod');
217
+ let latest = found;
215
218
  let check;
216
219
  const stop = () => {
217
220
  timer.cancel();
@@ -221,7 +224,8 @@ export function createLifecycle(definition, checksPermissions = () => true) {
221
224
  check ??= cmodVersion(claude())
222
225
  .then((version) => {
223
226
  check = undefined;
224
- if (version === undefined)
227
+ latest = version;
228
+ if (!fits(version))
225
229
  return;
226
230
  stop();
227
231
  return install();
@@ -231,7 +235,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
231
235
  report(error);
232
236
  });
233
237
  });
234
- const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for cmod to download cmod. See cmod's own line."));
238
+ const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait(latest === undefined
239
+ ? "Still waiting for cmod to download cmod. See cmod's own line."
240
+ : `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.keys ?? {})} or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.`));
235
241
  };
236
242
  const bootstrap = async (root) => {
237
243
  const progress = showLine();
@@ -0,0 +1,65 @@
1
+ import type { Job } from '../mod.js'
2
+ import { onStateChange } from '../runtime/state.js'
3
+ import { messageOf } from '../utils/text.js'
4
+
5
+ export type Schedule = { readonly cron: string; readonly prompt: string }
6
+
7
+ const scheduleCounts = new WeakMap<object, number>()
8
+
9
+ export function schedule<State extends object = Record<never, never>>(entry: Schedule | ((state: State) => Schedule | undefined)): Job<void, State> {
10
+ if (typeof entry !== 'function') checked(entry)
11
+
12
+ return (job) => {
13
+ const { mod, claude } = job
14
+ const order = scheduleCounts.get(mod) ?? 0
15
+ scheduleCounts.set(mod, order + 1)
16
+ const storeKey = `cmod:schedule:${order}`
17
+ const log = (reason: string) => claude.ui.log(`${mod.name}: the schedule ${reason}`)
18
+ let current: { readonly id: string; readonly wanted: string } | undefined
19
+ let isQueued = false
20
+
21
+ const remove = async (id: string) => {
22
+ await claude.tool.call({ tool: 'CronDelete', id }).catch(() => undefined)
23
+ await claude.store.delete(storeKey)
24
+ }
25
+
26
+ const update = async () => {
27
+ isQueued = false
28
+ let next: Schedule | undefined
29
+ try {
30
+ next = typeof entry === 'function' ? entry(mod.state as State) : entry
31
+ if (next !== undefined) checked(next)
32
+ } catch (error) {
33
+ log(`kept its last cron: ${messageOf(error)}`)
34
+ return
35
+ }
36
+ const wanted = next === undefined ? undefined : JSON.stringify([next.cron, next.prompt])
37
+ if (wanted === current?.wanted) return
38
+ if (current !== undefined) await remove(current.id)
39
+ current = undefined
40
+ if (next === undefined) return
41
+ const answer = await claude.tool.call({ tool: 'CronCreate', cron: next.cron, prompt: next.prompt, recurring: true })
42
+ if (answer.deny !== undefined) return log(`was refused: ${answer.deny}`)
43
+ if (answer.isError === true) return log(`failed: ${answer.text ?? 'CronCreate answered an error'}`)
44
+ current = { id: answer.result.id, wanted: wanted as string }
45
+ await claude.store.set(storeKey, current.id)
46
+ }
47
+
48
+ let updates = claude.store
49
+ .get(storeKey)
50
+ .then((left) => (typeof left === 'string' ? remove(left) : undefined))
51
+ .then(update)
52
+ .catch((error: unknown) => log(`failed: ${messageOf(error)}`))
53
+ onStateChange(mod.state, () => {
54
+ if (isQueued) return
55
+ isQueued = true
56
+ updates = updates.then(update).catch((error: unknown) => log(`failed: ${messageOf(error)}`))
57
+ })
58
+ job.announce(typeof entry === 'function' ? 'a schedule from its state' : `a schedule at ${entry.cron}`)
59
+ }
60
+ }
61
+
62
+ function checked(entry: Schedule): void {
63
+ if (entry.cron.trim() === '') throw new Error('schedule: give it a cron, such as "*/10 * * * *" for every 10 minutes.')
64
+ if (entry.prompt.trim() === '') throw new Error('schedule: give it the prompt Claude gets at each fire.')
65
+ }
package/src/records.ts CHANGED
@@ -127,6 +127,10 @@ function readKeys(keys: unknown): Steps {
127
127
  return { keys: commands }
128
128
  }
129
129
 
130
+ export function oldestCmodFor(keys: Readonly<Record<string, string>>): string {
131
+ return Object.keys(keys).length > 0 ? '0.1.12' : '0.0.0'
132
+ }
133
+
130
134
  export function keyWords(keys: Readonly<Record<string, string>> | undefined): string {
131
135
  return listed(Object.entries(keys ?? {}).map(([key, command]) => `${key} to /${command}`))
132
136
  }
package/src/register.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Args, EngineInterface, EventResult, Frozen, Next, On } from 'claude-code'
1
+ import type { Args, EngineInterface, EventResult, Frozen, Next, On, ToolCallArgs } from 'claude-code'
2
2
  import type { ModDefinition } from './mod.js'
3
3
  import type { Claude } from './runtime/claude.js'
4
4
  import type { RoutedEvent } from './runtime/hooks.js'
@@ -61,7 +61,7 @@ async function startMod($: EngineInterface, eventInput: Frozen<Args<'session.sta
61
61
  prompt: { submit: (args) => $.prompt.submit(args) },
62
62
  model: { complete: (request, options) => $.model.complete(request, options) },
63
63
  command: { register: (command) => $.command.register(command) },
64
- tool: { register: (tool) => $.tool.register(tool) },
64
+ tool: { register: (tool) => $.tool.register(tool), call: ((input: ToolCallArgs) => $.tool.call(input)) as Claude['tool']['call'] },
65
65
  agent: {
66
66
  list: () => $.agent.list(),
67
67
  spawn: (args) => $.agent.spawn(args),
@@ -88,7 +88,7 @@ export type Claude = {
88
88
  readonly prompt: { submit: EngineInterface['prompt']['submit'] }
89
89
  readonly model: { complete: EngineInterface['model']['complete'] }
90
90
  readonly command: { register(command: CommandSpec): Promise<{ command: string }> }
91
- readonly tool: { register(tool: ToolSpec): Promise<{ tool: string }> }
91
+ readonly tool: { register(tool: ToolSpec): Promise<{ tool: string }>; call: EngineInterface['tool']['call'] }
92
92
  readonly agent: {
93
93
  list(): Promise<AgentInfo[]>
94
94
  spawn(args: AgentSpawnArgs): Promise<AgentSpawnResult>
@@ -1,6 +1,6 @@
1
1
  import type { Args, EventResult, Frozen, HookBudget, HookStream, ProcessSpawnChunk, ProcessSpawnResult } from 'claude-code'
2
2
  import type { HookInput, Mod, ModDefinition, ModEvent, ModHook } from '../mod.js'
3
- import { dataFolder, isAtLeast, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder, type ReadFile, type RunnerEvent } from '../records.js'
3
+ import { dataFolder, isAtLeast, oldestCmodFor, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder, type ReadFile, type RunnerEvent } from '../records.js'
4
4
  import { relativePath } from '../utils/paths.js'
5
5
  import { formatExit, listed, messageOf } from '../utils/text.js'
6
6
  import type { Claude } from './claude.js'
@@ -233,7 +233,8 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
233
233
  if (plugin === undefined) return
234
234
  if (plugin.name === cmodPluginName) return bootstrap(plugin.root)
235
235
  phase = 'installing'
236
- if ((await cmodVersion(claude())) === undefined) return waitForcmod()
236
+ const found = await cmodVersion(claude())
237
+ if (!fits(found)) return waitForcmod(found)
237
238
  endLine()
238
239
  const progress = showLine()
239
240
  let outcome: RunnerEvent | undefined
@@ -249,9 +250,12 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
249
250
  fail(reason, `Fix the cause, then run: cmod install ${definition.name}`)
250
251
  }
251
252
 
252
- const waitForcmod = () => {
253
+ const fits = (found: string | undefined): found is string => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.keys ?? {}))
254
+
255
+ const waitForcmod = (found: string | undefined) => {
253
256
  phase = 'waiting'
254
257
  showLine().wait('Waiting for cmod')
258
+ let latest = found
255
259
  let check: Promise<void> | undefined
256
260
  const stop = () => {
257
261
  timer.cancel()
@@ -261,7 +265,8 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
261
265
  check ??= cmodVersion(claude())
262
266
  .then((version) => {
263
267
  check = undefined
264
- if (version === undefined) return
268
+ latest = version
269
+ if (!fits(version)) return
265
270
  stop()
266
271
  return install()
267
272
  })
@@ -270,7 +275,13 @@ export function createLifecycle<State extends object>(definition: ModDefinition<
270
275
  report(error)
271
276
  })
272
277
  })
273
- const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait("Still waiting for cmod to download cmod. See cmod's own line."))
278
+ const longWait = claude().clock.after(cmodWaitMs, () =>
279
+ showLine().wait(
280
+ latest === undefined
281
+ ? "Still waiting for cmod to download cmod. See cmod's own line."
282
+ : `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.keys ?? {})} or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.`,
283
+ ),
284
+ )
274
285
  }
275
286
 
276
287
  const bootstrap = async (root: string) => {
@@ -19,8 +19,11 @@ export type Fakes = {
19
19
  agent: { list?: Claude['agent']['list']; spawn?: Claude['agent']['spawn'] }
20
20
  clock: { after?: Claude['clock']['after']; every?: Claude['clock']['every'] }
21
21
  cmod: { call?: Claude['cmod']['call'] }
22
+ tool: { call?: Claude['tool']['call'] }
22
23
  }
23
24
 
25
+ export type ShownSchedule = { readonly id: string; readonly cron: string; readonly prompt: string }
26
+
24
27
  export type Shown = {
25
28
  readonly toasts: string[]
26
29
  readonly notes: string[]
@@ -31,6 +34,7 @@ export type Shown = {
31
34
  readonly openPanes: Set<string>
32
35
  readonly commands: string[]
33
36
  readonly tools: string[]
37
+ readonly schedules: ShownSchedule[]
34
38
  }
35
39
 
36
40
  export type FakeClaude = {
@@ -44,8 +48,8 @@ export type FakeClaude = {
44
48
 
45
49
  export function fakeClaude(plugin: { readonly name: string; readonly root: string }): FakeClaude {
46
50
  const calls: TestCall[] = []
47
- const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {} }
48
- const shown: Shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] }
51
+ const fakes: Fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} }
52
+ const shown: Shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] }
49
53
  const unplacedPanes = new Set<string>()
50
54
  let focusedPane: string | undefined
51
55
  const store = new Map<string, unknown>()
@@ -73,6 +77,22 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
73
77
  shown.prompts.push(text)
74
78
  return { text, origin: { kind: 'plugin', name: plugin.name } } as unknown as Awaited<ReturnType<Claude['prompt']['submit']>>
75
79
  }
80
+ let scheduled = 0
81
+ const scheduleTools = async (input: Record<string, unknown>): Promise<unknown> => {
82
+ if (input['tool'] === 'CronCreate') {
83
+ const created = { id: `cron-${(scheduled += 1)}`, cron: String(input['cron']), prompt: String(input['prompt']) }
84
+ shown.schedules.push(created)
85
+ return { result: { id: created.id, humanSchedule: created.cron, recurring: input['recurring'] !== false } }
86
+ }
87
+ if (input['tool'] === 'CronDelete') {
88
+ const at = shown.schedules.findIndex((created) => created.id === input['id'])
89
+ if (at === -1) return { result: { id: input['id'] }, text: `No job ${String(input['id'])}`, isError: true }
90
+ shown.schedules.splice(at, 1)
91
+ return { result: { id: input['id'] }, text: `Cancelled job ${String(input['id'])}.` }
92
+ }
93
+ if (input['tool'] === 'CronList') return { result: { jobs: shown.schedules.map(({ id, cron, prompt }) => ({ id, cron, humanSchedule: cron, prompt })) } }
94
+ throw new Error(`tool.call(${JSON.stringify(input)}) has no fake answer. Set fakes.tool.call on the tested mod.`)
95
+ }
76
96
 
77
97
  const claude: Claude = {
78
98
  plugin,
@@ -170,6 +190,7 @@ export function fakeClaude(plugin: { readonly name: string; readonly root: strin
170
190
  shown.tools.push(tool.name)
171
191
  return { tool: `mcp__${plugin.name}__${tool.name}` }
172
192
  },
193
+ call: rejected('tool.call', () => fakes.tool.call ?? scheduleTools) as Claude['tool']['call'],
173
194
  },
174
195
  env: {
175
196
  home: async () => '/test/home',
@@ -47,6 +47,14 @@ export type Fakes = {
47
47
  cmod: {
48
48
  call?: Claude['cmod']['call'];
49
49
  };
50
+ tool: {
51
+ call?: Claude['tool']['call'];
52
+ };
53
+ };
54
+ export type ShownSchedule = {
55
+ readonly id: string;
56
+ readonly cron: string;
57
+ readonly prompt: string;
50
58
  };
51
59
  export type Shown = {
52
60
  readonly toasts: string[];
@@ -58,6 +66,7 @@ export type Shown = {
58
66
  readonly openPanes: Set<string>;
59
67
  readonly commands: string[];
60
68
  readonly tools: string[];
69
+ readonly schedules: ShownSchedule[];
61
70
  };
62
71
  export type FakeClaude = {
63
72
  readonly claude: Claude;
@@ -1 +1 @@
1
- {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC/F,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC3F,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC/C,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAA;KAAE,CAAA;IACjD,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC3E,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC7E,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IACvC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnG,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAmK/F"}
@@ -1,8 +1,8 @@
1
1
  import { elements } from './fake-elements.js';
2
2
  export function fakeClaude(plugin) {
3
3
  const calls = [];
4
- const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {} };
5
- const shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] };
4
+ const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} };
5
+ const shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] };
6
6
  const unplacedPanes = new Set();
7
7
  let focusedPane;
8
8
  const store = new Map();
@@ -28,6 +28,24 @@ export function fakeClaude(plugin) {
28
28
  shown.prompts.push(text);
29
29
  return { text, origin: { kind: 'plugin', name: plugin.name } };
30
30
  };
31
+ let scheduled = 0;
32
+ const scheduleTools = async (input) => {
33
+ if (input['tool'] === 'CronCreate') {
34
+ const created = { id: `cron-${(scheduled += 1)}`, cron: String(input['cron']), prompt: String(input['prompt']) };
35
+ shown.schedules.push(created);
36
+ return { result: { id: created.id, humanSchedule: created.cron, recurring: input['recurring'] !== false } };
37
+ }
38
+ if (input['tool'] === 'CronDelete') {
39
+ const at = shown.schedules.findIndex((created) => created.id === input['id']);
40
+ if (at === -1)
41
+ return { result: { id: input['id'] }, text: `No job ${String(input['id'])}`, isError: true };
42
+ shown.schedules.splice(at, 1);
43
+ return { result: { id: input['id'] }, text: `Cancelled job ${String(input['id'])}.` };
44
+ }
45
+ if (input['tool'] === 'CronList')
46
+ return { result: { jobs: shown.schedules.map(({ id, cron, prompt }) => ({ id, cron, humanSchedule: cron, prompt })) } };
47
+ throw new Error(`tool.call(${JSON.stringify(input)}) has no fake answer. Set fakes.tool.call on the tested mod.`);
48
+ };
31
49
  const claude = {
32
50
  plugin,
33
51
  ui: {
@@ -127,6 +145,7 @@ export function fakeClaude(plugin) {
127
145
  shown.tools.push(tool.name);
128
146
  return { tool: `mcp__${plugin.name}__${tool.name}` };
129
147
  },
148
+ call: rejected('tool.call', () => fakes.tool.call ?? scheduleTools),
130
149
  },
131
150
  env: {
132
151
  home: async () => '/test/home',