claude-tempo 0.11.1 → 0.12.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/CLAUDE.md +4 -3
- package/README.md +29 -3
- package/dist/activities/outbox.d.ts +4 -0
- package/dist/activities/outbox.js +12 -1
- package/dist/activities/schedule-fire.d.ts +5 -0
- package/dist/activities/schedule-fire.js +6 -0
- package/dist/ensemble/agent-types.js +7 -0
- package/dist/ensemble/loader.js +3 -2
- package/dist/ensemble/saver.js +6 -1
- package/dist/ensemble/schema.d.ts +3 -0
- package/dist/tools/agent-types.js +4 -1
- package/dist/tools/load-lineup.js +28 -1
- package/dist/tools/recruit.js +3 -0
- package/dist/tools/schedule.js +48 -8
- package/dist/tools/schedules.js +3 -1
- package/dist/types.d.ts +8 -1
- package/dist/utils/validation.d.ts +2 -0
- package/dist/utils/validation.js +3 -1
- package/dist/workflows/scheduler.js +13 -1
- package/dist/workflows/session.js +3 -0
- package/package.json +2 -1
- package/workflow-bundle.js +17 -2
package/CLAUDE.md
CHANGED
|
@@ -9,7 +9,8 @@ claude-tempo is an MCP server that enables multiple Claude Code sessions to coor
|
|
|
9
9
|
- **Runtime**: Node.js 18+ with TypeScript
|
|
10
10
|
- **MCP**: `@modelcontextprotocol/sdk` (stdio transport)
|
|
11
11
|
- **Temporal**: `@temporalio/client`, `@temporalio/worker`, `@temporalio/workflow`, `@temporalio/activity`
|
|
12
|
-
- **
|
|
12
|
+
- **croner** — cron expression parsing and next-fire computation (used by `schedule` tool)
|
|
13
|
+
- **yaml**, **zod** — lineup parsing and schema validation
|
|
13
14
|
|
|
14
15
|
## Project Structure
|
|
15
16
|
|
|
@@ -111,9 +112,9 @@ npm test
|
|
|
111
112
|
- **Broadcast**: Fan-out variant of `cue` — sends a message to all active players in the ensemble in a single call. Optionally filtered by player type. Skips the sender, pending sessions, and (by default) stale sessions.
|
|
112
113
|
- **Recall**: Queries a session's own message history from the Temporal workflow. Shows received messages by default; pass `includeSent: true` to also see sent messages. Supports `limit`, `since`, and `from` filters.
|
|
113
114
|
- **Per-host task queues**: Each host runs a `claude-tempo-{hostname}` activity worker for local-only operations (e.g., `spawnProcess`). This enables cross-machine recruiting — the `recruit` tool accepts an optional `host` parameter to route the spawn to a remote machine's task queue.
|
|
114
|
-
- **Player types**: Reusable agent definitions in Claude Code's standard subagent format (`.md` files with YAML frontmatter). Ensemble lineups can reference types by name via a `type` field on players. Three-tier lookup: project `.claude/agents/` → user `~/.claude/agents/` → shipped `examples/agents/`. Players know their type via workflow metadata and the `who_am_i` tool.
|
|
115
|
+
- **Player types**: Reusable agent definitions in Claude Code's standard subagent format (`.md` files with YAML frontmatter). Ensemble lineups can reference types by name via a `type` field on players. Three-tier lookup: project `.claude/agents/` → user `~/.claude/agents/` → shipped `examples/agents/`. Players know their type via workflow metadata and the `who_am_i` tool. Agent type frontmatter may include an `allowedTools` array to restrict which MCP/CLI tools the spawned session can use (e.g., `allowedTools: [Read, Glob, Grep]`). When present, the type's `allowedTools` overrides any lineup-level setting and is passed to the Claude Code session via `--allowedTools`.
|
|
115
116
|
- **Agent type discovery**: The `agent_types` MCP tool and `claude-tempo agent-types` CLI command let conductors discover available player types. Shipped examples (tempo-conductor, tempo-composer, tempo-soloist, tempo-tuner, tempo-critic, tempo-roadie, tempo-improv, tempo-liner) work out of the box. Ensemble lineups: tempo-big-band (full lifecycle), tempo-dev-team (feature work), tempo-review-squad (parallel review), tempo-jam-session (exploration).
|
|
116
|
-
- **Schedule**: A one-shot or recurring message delivery configured via the `schedule` tool. Backed by a durable `claudeSchedulerWorkflow` — survives restarts. Supports delay,
|
|
117
|
+
- **Schedule**: A one-shot or recurring message delivery configured via the `schedule` tool. Backed by a durable `claudeSchedulerWorkflow` — survives restarts. Supports delay (`delay`), fixed time (`at`), recurring interval (`every`), and cron expressions (`cron`) with optional IANA timezone (`timezone`). Cron schedules use `croner` for expression parsing and next-fire computation. Managed via `schedule`, `unschedule`, and `schedules` tools.
|
|
117
118
|
- **Lineup**: A YAML file defining an ensemble configuration — which players to recruit, their types, working directories, and optional startup messages. Load via `load_lineup` to bootstrap a full ensemble in one step; save via `save_lineup` to snapshot a running ensemble's state for later reuse.
|
|
118
119
|
- **Wire protocol**: All Temporal signal, query, update, and workflow names are documented in [`docs/WIRE-PROTOCOL.md`](docs/WIRE-PROTOCOL.md). These names are stable as of v0.10 — renaming or removing any is a breaking change requiring a major version bump.
|
|
119
120
|
|
package/README.md
CHANGED
|
@@ -150,11 +150,20 @@ Tell your session things like:
|
|
|
150
150
|
- *"Schedule a check every hour called 'deploy-watch' — cue ops to check deployment status"*
|
|
151
151
|
- *"Remind me in 30 minutes to review PR #42"*
|
|
152
152
|
- *"Every 5 minutes for the next hour, ping frontend to check their progress"*
|
|
153
|
-
- *"Set up a daily standup
|
|
153
|
+
- *"Set up a daily standup at 9am New York time, weekdays only"*
|
|
154
154
|
- *"Cancel the deploy-watch schedule"*
|
|
155
155
|
- *"Show me all active schedules"*
|
|
156
156
|
|
|
157
|
-
Schedules support
|
|
157
|
+
Schedules support four timing modes — all accept optional bounds (`count` max fires, `until` end time):
|
|
158
|
+
|
|
159
|
+
| Mode | Parameter | Example |
|
|
160
|
+
|------|-----------|---------|
|
|
161
|
+
| One-shot delay | `delay` | `"10m"`, `"2h"`, `"1d"` |
|
|
162
|
+
| Fixed time | `at` | `"2026-04-03T20:00:00Z"` |
|
|
163
|
+
| Recurring interval | `every` | `"5m"`, `"1h"` |
|
|
164
|
+
| Cron expression | `cron` + optional `timezone` | `"0 9 * * 1-5"` (weekdays 9am) |
|
|
165
|
+
|
|
166
|
+
The `timezone` parameter accepts any IANA timezone (e.g. `"America/New_York"`, `"Europe/London"`). Defaults to UTC when omitted.
|
|
158
167
|
|
|
159
168
|
### How it works
|
|
160
169
|
|
|
@@ -241,7 +250,7 @@ players:
|
|
|
241
250
|
|
|
242
251
|
## Player Types
|
|
243
252
|
|
|
244
|
-
Player types are reusable agent definitions in Claude Code's standard subagent format — `.md` files with YAML frontmatter specifying name, description, and optional
|
|
253
|
+
Player types are reusable agent definitions in Claude Code's standard subagent format — `.md` files with YAML frontmatter specifying name, description, optional model, and optional tool restrictions. They let you define specialized roles once and reuse them across lineups.
|
|
245
254
|
|
|
246
255
|
### How player types work
|
|
247
256
|
|
|
@@ -257,6 +266,23 @@ players:
|
|
|
257
266
|
|
|
258
267
|
When a player is recruited with a type, the agent definition is resolved and passed to the session. Players know their type via the `who_am_i` tool.
|
|
259
268
|
|
|
269
|
+
### Tool restrictions (`allowedTools`)
|
|
270
|
+
|
|
271
|
+
Agent type frontmatter may include an `allowedTools` array to restrict which tools the spawned session can use. When present, it is passed to the Claude Code session via `--allowedTools` and overrides any lineup-level setting.
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
---
|
|
275
|
+
name: tempo-reviewer
|
|
276
|
+
description: Read-only code reviewer
|
|
277
|
+
allowedTools:
|
|
278
|
+
- Read
|
|
279
|
+
- Glob
|
|
280
|
+
- Grep
|
|
281
|
+
---
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
This is useful for security-sensitive roles (read-only reviewers, auditors) or to prevent specific players from making changes outside their scope. Sessions launched without a type, or with a type that omits `allowedTools`, receive no tool restrictions.
|
|
285
|
+
|
|
260
286
|
### Three-tier lookup
|
|
261
287
|
|
|
262
288
|
Player types are resolved in order (first match wins):
|
|
@@ -30,6 +30,7 @@ export interface StartRecruitedSessionInput {
|
|
|
30
30
|
taskQueue: string;
|
|
31
31
|
agentDefinition?: string;
|
|
32
32
|
agentDefinitionDescription?: string;
|
|
33
|
+
allowedTools?: string[];
|
|
33
34
|
}
|
|
34
35
|
export interface SpawnProcessInput {
|
|
35
36
|
targetName: string;
|
|
@@ -45,6 +46,8 @@ export interface SpawnProcessInput {
|
|
|
45
46
|
nativeResolvable?: boolean;
|
|
46
47
|
/** When true, use --resume instead of -n (reconnect to existing session). */
|
|
47
48
|
resume?: boolean;
|
|
49
|
+
/** Tool restrictions from the agent definition frontmatter. */
|
|
50
|
+
allowedTools?: string[];
|
|
48
51
|
}
|
|
49
52
|
export interface PerformEncoreInput {
|
|
50
53
|
ensemble: string;
|
|
@@ -60,6 +63,7 @@ export interface EncoreResult {
|
|
|
60
63
|
agentDefinition?: string;
|
|
61
64
|
agentDefinitionPath?: string;
|
|
62
65
|
nativeResolvable?: boolean;
|
|
66
|
+
allowedTools?: string[];
|
|
63
67
|
temporalAddress: string;
|
|
64
68
|
temporalNamespace: string;
|
|
65
69
|
}
|
|
@@ -145,11 +145,14 @@ function createOutboxActivities(client, config) {
|
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
async spawnProcess(input) {
|
|
148
|
-
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume } = input;
|
|
148
|
+
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, allowedTools } = input;
|
|
149
149
|
// Read secrets from the worker's config closure — never from workflow state
|
|
150
150
|
const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
|
|
151
151
|
try {
|
|
152
152
|
if (agent === 'copilot') {
|
|
153
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
154
|
+
log(`Warning: allowedTools [${allowedTools.join(', ')}] specified for copilot agent "${targetName}" — copilot bridge does not support --allowedTools, skipping`);
|
|
155
|
+
}
|
|
153
156
|
const { pid } = (0, spawn_1.spawnCopilotBridge)({
|
|
154
157
|
name: targetName,
|
|
155
158
|
ensemble,
|
|
@@ -177,11 +180,16 @@ function createOutboxActivities(client, config) {
|
|
|
177
180
|
}
|
|
178
181
|
// Use --resume for encore (reconnect to existing session) or -n for new sessions
|
|
179
182
|
const nameArgs = resume ? ['--resume', targetName] : ['-n', targetName];
|
|
183
|
+
// Build --allowedTools flag from agent definition frontmatter
|
|
184
|
+
const allowedToolsFlags = allowedTools && allowedTools.length > 0
|
|
185
|
+
? ['--allowedTools', ...allowedTools]
|
|
186
|
+
: [];
|
|
180
187
|
const spawnArgs = [
|
|
181
188
|
'--dangerously-skip-permissions',
|
|
182
189
|
'--dangerously-load-development-channels', 'server:claude-tempo',
|
|
183
190
|
...nameArgs,
|
|
184
191
|
...agentFlags,
|
|
192
|
+
...allowedToolsFlags,
|
|
185
193
|
];
|
|
186
194
|
const envVars = {
|
|
187
195
|
[config_2.ENV.ENSEMBLE]: ensemble,
|
|
@@ -249,12 +257,14 @@ function createOutboxActivities(client, config) {
|
|
|
249
257
|
const playerType = metadata.playerType;
|
|
250
258
|
let agentDefinitionPath;
|
|
251
259
|
let nativeResolvable;
|
|
260
|
+
let allowedTools;
|
|
252
261
|
if (playerType) {
|
|
253
262
|
try {
|
|
254
263
|
const info = (0, agent_types_1.resolveAgentType)(playerType);
|
|
255
264
|
if (info) {
|
|
256
265
|
agentDefinitionPath = info.path;
|
|
257
266
|
nativeResolvable = info.nativeResolvable;
|
|
267
|
+
allowedTools = info.allowedTools;
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
catch {
|
|
@@ -269,6 +279,7 @@ function createOutboxActivities(client, config) {
|
|
|
269
279
|
agentDefinition: playerType,
|
|
270
280
|
agentDefinitionPath,
|
|
271
281
|
nativeResolvable,
|
|
282
|
+
allowedTools,
|
|
272
283
|
temporalAddress: config.temporalAddress,
|
|
273
284
|
temporalNamespace: config.temporalNamespace,
|
|
274
285
|
};
|
|
@@ -10,9 +10,14 @@ export interface FireScheduleResult {
|
|
|
10
10
|
success: boolean;
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
|
+
export interface ComputeNextCronInput {
|
|
14
|
+
cronExpression: string;
|
|
15
|
+
timezone?: string;
|
|
16
|
+
}
|
|
13
17
|
/** Activity interface — used by proxyActivities in the scheduler workflow. */
|
|
14
18
|
export interface ScheduleActivities {
|
|
15
19
|
fireSchedule(input: FireScheduleInput): Promise<FireScheduleResult>;
|
|
20
|
+
computeNextCronFire(input: ComputeNextCronInput): Promise<string | null>;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Create the schedule-fire activity bound to a Temporal client.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createScheduleActivities = createScheduleActivities;
|
|
4
|
+
const croner_1 = require("croner");
|
|
4
5
|
const config_1 = require("../config");
|
|
5
6
|
const resolve_1 = require("./resolve");
|
|
6
7
|
/**
|
|
@@ -9,6 +10,11 @@ const resolve_1 = require("./resolve");
|
|
|
9
10
|
*/
|
|
10
11
|
function createScheduleActivities(client) {
|
|
11
12
|
return {
|
|
13
|
+
async computeNextCronFire(input) {
|
|
14
|
+
const job = new croner_1.Cron(input.cronExpression, { timezone: input.timezone || 'UTC' });
|
|
15
|
+
const next = job.nextRun();
|
|
16
|
+
return next ? next.toISOString() : null;
|
|
17
|
+
},
|
|
12
18
|
async fireSchedule(input) {
|
|
13
19
|
const { ensemble, scheduleName, message, target, createdBy } = input;
|
|
14
20
|
try {
|
|
@@ -77,6 +77,7 @@ function listAgentTypes(cwd) {
|
|
|
77
77
|
source,
|
|
78
78
|
path: filePath,
|
|
79
79
|
nativeResolvable,
|
|
80
|
+
...(Array.isArray(fm.allowedTools) ? { allowedTools: fm.allowedTools.map(String) } : {}),
|
|
80
81
|
});
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -98,6 +99,7 @@ function resolveAgentType(name, cwd) {
|
|
|
98
99
|
source,
|
|
99
100
|
path: filePath,
|
|
100
101
|
nativeResolvable,
|
|
102
|
+
...(Array.isArray(fm.allowedTools) ? { allowedTools: fm.allowedTools.map(String) } : {}),
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
return null;
|
|
@@ -120,6 +122,11 @@ function loadAndResolveLineup(filePath, cwd) {
|
|
|
120
122
|
}
|
|
121
123
|
player._agentDefinition = info.name;
|
|
122
124
|
player._agentDefinitionPath = info.path;
|
|
125
|
+
// Type's allowedTools is the security authority — overrides lineup-level setting
|
|
126
|
+
// Empty array means "not specified" (no restriction), so don't override
|
|
127
|
+
if (info.allowedTools && info.allowedTools.length > 0) {
|
|
128
|
+
player.allowedTools = info.allowedTools;
|
|
129
|
+
}
|
|
123
130
|
}
|
|
124
131
|
return lineup;
|
|
125
132
|
}
|
package/dist/ensemble/loader.js
CHANGED
|
@@ -45,8 +45,8 @@ function loadLineup(filePath) {
|
|
|
45
45
|
if (typeof s.target !== 'string' || !s.target) {
|
|
46
46
|
throw new Error(`Invalid lineup: schedules[${i}].target is required`);
|
|
47
47
|
}
|
|
48
|
-
if (!s.at && !s.delay && !s.every) {
|
|
49
|
-
throw new Error(`Invalid lineup: schedules[${i}] must have at least one of: at, delay, every`);
|
|
48
|
+
if (!s.at && !s.delay && !s.every && !s.cron) {
|
|
49
|
+
throw new Error(`Invalid lineup: schedules[${i}] must have at least one of: at, delay, every, cron`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -60,6 +60,7 @@ function loadLineup(filePath) {
|
|
|
60
60
|
...(p.workDir != null && { workDir: p.workDir }),
|
|
61
61
|
...(p.agent != null && { agent: p.agent }),
|
|
62
62
|
...(p.instructions != null && { instructions: p.instructions }),
|
|
63
|
+
...(Array.isArray(p.allowedTools) && { allowedTools: p.allowedTools.map(String) }),
|
|
63
64
|
})),
|
|
64
65
|
schedules: doc.schedules,
|
|
65
66
|
};
|
package/dist/ensemble/saver.js
CHANGED
|
@@ -67,7 +67,12 @@ async function saveLineup(client, ensemble, filePath, name) {
|
|
|
67
67
|
message: entry.message,
|
|
68
68
|
target: entry.target,
|
|
69
69
|
};
|
|
70
|
-
if (entry.
|
|
70
|
+
if (entry.cronExpression) {
|
|
71
|
+
sched.cron = entry.cronExpression;
|
|
72
|
+
if (entry.timezone)
|
|
73
|
+
sched.timezone = entry.timezone;
|
|
74
|
+
}
|
|
75
|
+
else if (entry.interval) {
|
|
71
76
|
sched.every = formatDurationMs(entry.interval);
|
|
72
77
|
}
|
|
73
78
|
if (entry.until) {
|
|
@@ -12,6 +12,7 @@ export interface EnsembleLineup {
|
|
|
12
12
|
workDir?: string;
|
|
13
13
|
agent?: string;
|
|
14
14
|
instructions?: string;
|
|
15
|
+
allowedTools?: string[];
|
|
15
16
|
/** Transient: resolved agent definition name (set by loadAndResolveLineup). */
|
|
16
17
|
_agentDefinition?: string;
|
|
17
18
|
/** Transient: resolved absolute path to .md file (set by loadAndResolveLineup). */
|
|
@@ -24,6 +25,8 @@ export interface EnsembleLineup {
|
|
|
24
25
|
at?: string;
|
|
25
26
|
delay?: string;
|
|
26
27
|
every?: string;
|
|
28
|
+
cron?: string;
|
|
29
|
+
timezone?: string;
|
|
27
30
|
until?: string;
|
|
28
31
|
count?: number;
|
|
29
32
|
}>;
|
|
@@ -11,7 +11,10 @@ function registerAgentTypesTool(server) {
|
|
|
11
11
|
}
|
|
12
12
|
const lines = types.map(t => {
|
|
13
13
|
const src = t.source === 'shipped' ? '(shipped)' : t.source === 'user' ? '(user)' : '(project)';
|
|
14
|
-
|
|
14
|
+
const tools = t.allowedTools && t.allowedTools.length > 0
|
|
15
|
+
? `\n Allowed tools: ${t.allowedTools.join(', ')}`
|
|
16
|
+
: '';
|
|
17
|
+
return `**${t.name}** ${src}\n ${t.description || 'No description'}${tools}`;
|
|
15
18
|
});
|
|
16
19
|
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
17
20
|
});
|
|
@@ -4,6 +4,7 @@ exports.registerLoadLineupTool = registerLoadLineupTool;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const path_1 = require("path");
|
|
7
|
+
const croner_1 = require("croner");
|
|
7
8
|
const client_1 = require("@temporalio/client");
|
|
8
9
|
const config_1 = require("../config");
|
|
9
10
|
const agent_types_1 = require("../ensemble/agent-types");
|
|
@@ -143,11 +144,16 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
143
144
|
else if (systemPrompt) {
|
|
144
145
|
agentFlags = ['--system-prompt', systemPrompt];
|
|
145
146
|
}
|
|
147
|
+
// Build --allowedTools flag from agent definition or lineup
|
|
148
|
+
const allowedToolsFlags = player.allowedTools && player.allowedTools.length > 0
|
|
149
|
+
? ['--allowedTools', ...player.allowedTools]
|
|
150
|
+
: [];
|
|
146
151
|
const spawnArgs = [
|
|
147
152
|
'--dangerously-skip-permissions',
|
|
148
153
|
'--dangerously-load-development-channels', 'server:claude-tempo',
|
|
149
154
|
'-n', playerName,
|
|
150
155
|
...agentFlags,
|
|
156
|
+
...allowedToolsFlags,
|
|
151
157
|
];
|
|
152
158
|
const envVars = {
|
|
153
159
|
[config_1.ENV.ENSEMBLE]: config.ensemble,
|
|
@@ -212,8 +218,17 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
212
218
|
const now = Date.now();
|
|
213
219
|
let nextFireAt;
|
|
214
220
|
let interval;
|
|
221
|
+
let cronExpression;
|
|
222
|
+
let timezone;
|
|
215
223
|
if (sched.at) {
|
|
216
224
|
nextFireAt = Date.parse(sched.at);
|
|
225
|
+
// Support at + every: use `at` as the initial fire time, `every` as the interval
|
|
226
|
+
if (sched.every) {
|
|
227
|
+
const ms = (0, duration_1.parseDuration)(sched.every);
|
|
228
|
+
if (!ms)
|
|
229
|
+
throw new Error(`Invalid interval: ${sched.every}`);
|
|
230
|
+
interval = ms;
|
|
231
|
+
}
|
|
217
232
|
}
|
|
218
233
|
else if (sched.delay) {
|
|
219
234
|
const ms = (0, duration_1.parseDuration)(sched.delay);
|
|
@@ -228,16 +243,28 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
228
243
|
nextFireAt = now + ms;
|
|
229
244
|
interval = ms;
|
|
230
245
|
}
|
|
246
|
+
else if (sched.cron) {
|
|
247
|
+
cronExpression = sched.cron;
|
|
248
|
+
timezone = sched.timezone || 'UTC';
|
|
249
|
+
const job = new croner_1.Cron(cronExpression, { timezone });
|
|
250
|
+
const next = job.nextRun();
|
|
251
|
+
if (!next)
|
|
252
|
+
throw new Error(`Cron expression "${sched.cron}" has no upcoming fire time`);
|
|
253
|
+
nextFireAt = next.getTime();
|
|
254
|
+
}
|
|
231
255
|
else {
|
|
232
256
|
throw new Error('No timing specified');
|
|
233
257
|
}
|
|
258
|
+
const type = sched.cron ? 'cron' : (sched.every || interval) ? 'interval' : 'once';
|
|
234
259
|
const scheduleEntry = {
|
|
235
260
|
name: sched.name,
|
|
236
261
|
message: sched.message,
|
|
237
262
|
target: sched.target,
|
|
238
|
-
type
|
|
263
|
+
type,
|
|
239
264
|
nextFireAt: new Date(nextFireAt).toISOString(),
|
|
240
265
|
interval,
|
|
266
|
+
cronExpression,
|
|
267
|
+
timezone,
|
|
241
268
|
until: sched.until,
|
|
242
269
|
remainingCount: sched.count,
|
|
243
270
|
firedCount: 0,
|
package/dist/tools/recruit.js
CHANGED
|
@@ -36,6 +36,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
|
|
|
36
36
|
let agentDefinitionPath;
|
|
37
37
|
let agentDefinitionDescription;
|
|
38
38
|
let nativeResolvable;
|
|
39
|
+
let allowedTools;
|
|
39
40
|
if (agentTypeName) {
|
|
40
41
|
const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
|
|
41
42
|
if (!info) {
|
|
@@ -52,6 +53,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
|
|
|
52
53
|
agentDefinitionPath = info.path;
|
|
53
54
|
agentDefinitionDescription = info.description;
|
|
54
55
|
nativeResolvable = info.nativeResolvable;
|
|
56
|
+
allowedTools = info.allowedTools;
|
|
55
57
|
}
|
|
56
58
|
// Validate name
|
|
57
59
|
const nameError = (0, validation_1.validatePlayerName)(name);
|
|
@@ -118,6 +120,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
|
|
|
118
120
|
agentDefinitionPath,
|
|
119
121
|
agentDefinitionDescription,
|
|
120
122
|
nativeResolvable,
|
|
123
|
+
allowedTools,
|
|
121
124
|
};
|
|
122
125
|
const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
|
|
123
126
|
return {
|
package/dist/tools/schedule.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerScheduleTool = registerScheduleTool;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
const croner_1 = require("croner");
|
|
5
6
|
const client_1 = require("@temporalio/client");
|
|
6
7
|
const config_1 = require("../config");
|
|
7
8
|
const duration_1 = require("../utils/duration");
|
|
@@ -9,29 +10,41 @@ const helpers_1 = require("./helpers");
|
|
|
9
10
|
const validation_1 = require("../utils/validation");
|
|
10
11
|
const log = (...args) => console.error('[claude-tempo:schedule]', ...args);
|
|
11
12
|
function registerScheduleTool(server, client, config, getPlayerId) {
|
|
12
|
-
(0, helpers_1.defineTool)(server, 'schedule', 'Schedule a message to be sent to a player at a specific time, after a delay,
|
|
13
|
+
(0, helpers_1.defineTool)(server, 'schedule', 'Schedule a message to be sent to a player at a specific time, after a delay, on a recurring interval, or via cron expression.', {
|
|
13
14
|
name: zod_1.z.string().max(validation_1.SCHEDULE_NAME_MAX).describe('Unique name for this schedule'),
|
|
14
15
|
message: zod_1.z.string().max(validation_1.SCHEDULE_MESSAGE_MAX).describe('The message to deliver'),
|
|
15
16
|
target: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('Player name to deliver to ("self" = this session)'),
|
|
16
17
|
at: zod_1.z.string().optional().describe('ISO datetime for one-shot delivery (e.g. "2026-04-03T20:00:00Z")'),
|
|
17
18
|
delay: zod_1.z.string().optional().describe('Duration until first delivery (e.g. "10m", "2h", "1d")'),
|
|
18
19
|
every: zod_1.z.string().optional().describe('Recurring interval (e.g. "5m", "1h")'),
|
|
20
|
+
cron: zod_1.z.string().max(validation_1.CRON_EXPRESSION_MAX).optional().describe('Cron expression for recurring delivery (e.g. "0 9 * * 1-5" = weekdays at 9am). Mutually exclusive with at/delay/every.'),
|
|
21
|
+
timezone: zod_1.z.string().optional().describe('IANA timezone for cron evaluation (e.g. "America/New_York"). Defaults to UTC. Only used with cron.'),
|
|
19
22
|
until: zod_1.z.string().optional().describe('ISO datetime — stop recurring after this time'),
|
|
20
23
|
count: zod_1.z.number().optional().describe('Max number of deliveries for recurring schedules'),
|
|
21
24
|
}, async (args) => {
|
|
22
|
-
const { name, message, at, delay, every, until, count } = args;
|
|
25
|
+
const { name, message, at, delay, every, cron, timezone, until, count } = args;
|
|
23
26
|
let target = args.target;
|
|
24
27
|
// Resolve "self" to the current player name
|
|
25
28
|
if (target === 'self') {
|
|
26
29
|
target = getPlayerId();
|
|
27
30
|
}
|
|
28
31
|
// Validate exactly one timing option
|
|
29
|
-
const timingCount = [at, delay, every].filter(Boolean).length;
|
|
32
|
+
const timingCount = [at, delay, every, cron].filter(Boolean).length;
|
|
30
33
|
if (timingCount !== 1) {
|
|
31
34
|
return {
|
|
32
35
|
content: [{
|
|
33
36
|
type: 'text',
|
|
34
|
-
text: 'Provide exactly one timing option: `at`, `delay`, or `
|
|
37
|
+
text: 'Provide exactly one timing option: `at`, `delay`, `every`, or `cron`.',
|
|
38
|
+
}],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// timezone only valid with cron
|
|
43
|
+
if (timezone && !cron) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: '`timezone` can only be used with `cron`.',
|
|
35
48
|
}],
|
|
36
49
|
isError: true,
|
|
37
50
|
};
|
|
@@ -59,8 +72,8 @@ function registerScheduleTool(server, client, config, getPlayerId) {
|
|
|
59
72
|
}
|
|
60
73
|
nextFireAt = now + ms;
|
|
61
74
|
}
|
|
62
|
-
else {
|
|
63
|
-
// every (recurring)
|
|
75
|
+
else if (every) {
|
|
76
|
+
// every (recurring interval)
|
|
64
77
|
const ms = (0, duration_1.parseDuration)(every);
|
|
65
78
|
if (ms === null || ms < 10_000) {
|
|
66
79
|
return {
|
|
@@ -71,6 +84,27 @@ function registerScheduleTool(server, client, config, getPlayerId) {
|
|
|
71
84
|
nextFireAt = now + ms;
|
|
72
85
|
interval = ms;
|
|
73
86
|
}
|
|
87
|
+
else {
|
|
88
|
+
// cron (recurring via cron expression)
|
|
89
|
+
try {
|
|
90
|
+
const job = new croner_1.Cron(cron, { timezone: timezone || 'UTC' });
|
|
91
|
+
const next = job.nextRun();
|
|
92
|
+
if (!next) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: `Cron expression "${cron}" has no upcoming fire time.` }],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
nextFireAt = next.getTime();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: `Invalid cron expression "${cron}": ${msg}` }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
74
108
|
// Parse optional until
|
|
75
109
|
let untilMs;
|
|
76
110
|
if (until) {
|
|
@@ -83,7 +117,7 @@ function registerScheduleTool(server, client, config, getPlayerId) {
|
|
|
83
117
|
}
|
|
84
118
|
untilMs = ts;
|
|
85
119
|
}
|
|
86
|
-
const type = every ? 'interval' : 'once';
|
|
120
|
+
const type = cron ? 'cron' : every ? 'interval' : 'once';
|
|
87
121
|
const scheduleEntry = {
|
|
88
122
|
name,
|
|
89
123
|
message,
|
|
@@ -91,6 +125,8 @@ function registerScheduleTool(server, client, config, getPlayerId) {
|
|
|
91
125
|
type,
|
|
92
126
|
nextFireAt: new Date(nextFireAt).toISOString(),
|
|
93
127
|
interval,
|
|
128
|
+
cronExpression: cron,
|
|
129
|
+
timezone: cron ? (timezone || 'UTC') : undefined,
|
|
94
130
|
until: untilMs ? new Date(untilMs).toISOString() : undefined,
|
|
95
131
|
remainingCount: count,
|
|
96
132
|
firedCount: 0,
|
|
@@ -118,7 +154,11 @@ function registerScheduleTool(server, client, config, getPlayerId) {
|
|
|
118
154
|
log(`Started scheduler workflow ${wfId}`);
|
|
119
155
|
}
|
|
120
156
|
const fireDate = new Date(nextFireAt).toISOString();
|
|
121
|
-
const recur =
|
|
157
|
+
const recur = cron
|
|
158
|
+
? ` (cron: ${cron}, tz: ${timezone || 'UTC'})`
|
|
159
|
+
: interval
|
|
160
|
+
? ` (repeating every ${every})`
|
|
161
|
+
: ' (one-shot)';
|
|
122
162
|
return {
|
|
123
163
|
content: [{
|
|
124
164
|
type: 'text',
|
package/dist/tools/schedules.js
CHANGED
|
@@ -39,7 +39,9 @@ function registerSchedulesTool(server, client, config) {
|
|
|
39
39
|
}
|
|
40
40
|
const lines = schedules.map((s) => {
|
|
41
41
|
const next = s.nextFireAt; // already ISO string
|
|
42
|
-
const recur = s.
|
|
42
|
+
const recur = s.cronExpression
|
|
43
|
+
? `cron: ${s.cronExpression} (${s.timezone || 'UTC'})`
|
|
44
|
+
: s.interval ? `every ${formatDuration(s.interval)}` : 'one-shot';
|
|
43
45
|
const bounds = [];
|
|
44
46
|
if (s.until)
|
|
45
47
|
bounds.push(`until ${s.until}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface AgentTypeInfo {
|
|
|
23
23
|
source: 'project' | 'user' | 'shipped';
|
|
24
24
|
path: string;
|
|
25
25
|
nativeResolvable: boolean;
|
|
26
|
+
allowedTools?: string[];
|
|
26
27
|
}
|
|
27
28
|
export interface SessionInput {
|
|
28
29
|
metadata: SessionMetadata;
|
|
@@ -110,6 +111,8 @@ export interface RecruitOutboxEntry extends OutboxEntryBase {
|
|
|
110
111
|
agentDefinitionDescription?: string;
|
|
111
112
|
/** Whether the agent definition is in a Claude Code-resolvable location. */
|
|
112
113
|
nativeResolvable?: boolean;
|
|
114
|
+
/** Tool restrictions from the agent definition frontmatter. */
|
|
115
|
+
allowedTools?: string[];
|
|
113
116
|
}
|
|
114
117
|
export interface ReportOutboxEntry extends OutboxEntryBase {
|
|
115
118
|
type: 'report';
|
|
@@ -151,6 +154,10 @@ export interface ScheduleEntry {
|
|
|
151
154
|
/** Total number of times this schedule has fired. */
|
|
152
155
|
firedCount: number;
|
|
153
156
|
/** Schedule type for display purposes. */
|
|
154
|
-
type: 'once' | 'interval';
|
|
157
|
+
type: 'once' | 'interval' | 'cron';
|
|
158
|
+
/** Cron expression string (e.g., "0 9 * * 1-5"). Stored for re-computing next fire. */
|
|
159
|
+
cronExpression?: string;
|
|
160
|
+
/** IANA timezone for cron evaluation (e.g., "America/New_York"). Defaults to UTC. */
|
|
161
|
+
timezone?: string;
|
|
155
162
|
}
|
|
156
163
|
export {};
|
|
@@ -20,6 +20,8 @@ export declare const PATH_MAX = 1024;
|
|
|
20
20
|
export declare const SCHEDULE_NAME_MAX = 64;
|
|
21
21
|
/** Maximum schedule message size (10KB). */
|
|
22
22
|
export declare const SCHEDULE_MESSAGE_MAX = 10240;
|
|
23
|
+
/** Maximum cron expression length. */
|
|
24
|
+
export declare const CRON_EXPRESSION_MAX = 128;
|
|
23
25
|
/** Default number of recent messages to include as context in an encore. */
|
|
24
26
|
export declare const ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
|
|
25
27
|
/** Maximum length for message preview truncation. */
|
package/dist/utils/validation.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Used by MCP tool Zod schemas and config validation.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
|
|
7
|
+
exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
|
|
8
8
|
exports.shouldIncludeInBroadcast = shouldIncludeInBroadcast;
|
|
9
9
|
exports.validatePlayerName = validatePlayerName;
|
|
10
10
|
exports.validateEnsembleName = validateEnsembleName;
|
|
@@ -26,6 +26,8 @@ exports.PATH_MAX = 1024;
|
|
|
26
26
|
exports.SCHEDULE_NAME_MAX = 64;
|
|
27
27
|
/** Maximum schedule message size (10KB). */
|
|
28
28
|
exports.SCHEDULE_MESSAGE_MAX = 10240;
|
|
29
|
+
/** Maximum cron expression length. */
|
|
30
|
+
exports.CRON_EXPRESSION_MAX = 128;
|
|
29
31
|
/** Default number of recent messages to include as context in an encore. */
|
|
30
32
|
exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
|
|
31
33
|
/** Maximum length for message preview truncation. */
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.claudeSchedulerWorkflow = claudeSchedulerWorkflow;
|
|
4
4
|
const workflow_1 = require("@temporalio/workflow");
|
|
5
5
|
const scheduler_signals_1 = require("./scheduler-signals");
|
|
6
|
-
const { fireSchedule } = (0, workflow_1.proxyActivities)({
|
|
6
|
+
const { fireSchedule, computeNextCronFire } = (0, workflow_1.proxyActivities)({
|
|
7
7
|
startToCloseTimeout: '30 seconds',
|
|
8
8
|
retry: { maximumAttempts: 3 },
|
|
9
9
|
});
|
|
@@ -83,6 +83,18 @@ async function claudeSchedulerWorkflow(input) {
|
|
|
83
83
|
else if (entry.type === 'interval' && entry.interval) {
|
|
84
84
|
entry.nextFireAt = new Date(Date.now() + entry.interval).toISOString();
|
|
85
85
|
}
|
|
86
|
+
else if ((0, workflow_1.patched)('v0.12-cron-schedule') && entry.type === 'cron' && entry.cronExpression) {
|
|
87
|
+
const nextFire = await computeNextCronFire({
|
|
88
|
+
cronExpression: entry.cronExpression,
|
|
89
|
+
timezone: entry.timezone,
|
|
90
|
+
});
|
|
91
|
+
if (nextFire) {
|
|
92
|
+
entry.nextFireAt = nextFire;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
entries = entries.filter((e) => e.name !== entry.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
99
|
// Prevent unbounded history growth
|
|
88
100
|
const info = (0, workflow_1.workflowInfo)();
|
|
@@ -258,6 +258,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
258
258
|
taskQueue: tc?.taskQueue || 'claude-tempo',
|
|
259
259
|
agentDefinition: entry.agentDefinition,
|
|
260
260
|
agentDefinitionDescription: entry.agentDefinitionDescription,
|
|
261
|
+
allowedTools: entry.allowedTools,
|
|
261
262
|
});
|
|
262
263
|
const targetHost = entry.targetHostname || input.metadata.hostname;
|
|
263
264
|
const spawnFn = getSpawnProxy(targetHost);
|
|
@@ -273,6 +274,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
273
274
|
agentDefinition: entry.agentDefinition,
|
|
274
275
|
agentDefinitionPath: entry.agentDefinitionPath,
|
|
275
276
|
nativeResolvable: entry.nativeResolvable,
|
|
277
|
+
allowedTools: entry.allowedTools,
|
|
276
278
|
});
|
|
277
279
|
break;
|
|
278
280
|
}
|
|
@@ -297,6 +299,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
297
299
|
agentDefinition: encoreResult.agentDefinition,
|
|
298
300
|
agentDefinitionPath: encoreResult.agentDefinitionPath,
|
|
299
301
|
nativeResolvable: encoreResult.nativeResolvable,
|
|
302
|
+
allowedTools: encoreResult.allowedTools,
|
|
300
303
|
resume: true,
|
|
301
304
|
});
|
|
302
305
|
}
|