claude-tempo 0.1.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/dist/server.js ADDED
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const crypto = __importStar(require("crypto"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const child_process_1 = require("child_process");
41
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
42
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
43
+ const client_1 = require("@temporalio/client");
44
+ const config_1 = require("./config");
45
+ const worker_1 = require("./worker");
46
+ const ensemble_1 = require("./tools/ensemble");
47
+ const cue_1 = require("./tools/cue");
48
+ const set_part_1 = require("./tools/set-part");
49
+ const listen_1 = require("./tools/listen");
50
+ const recruit_1 = require("./tools/recruit");
51
+ const report_1 = require("./tools/report");
52
+ const terminate_1 = require("./tools/terminate");
53
+ const set_name_1 = require("./tools/set-name");
54
+ const channel_1 = require("./channel");
55
+ const log = (...args) => console.error('[claude-tempo]', ...args);
56
+ function getGitInfo(workDir) {
57
+ try {
58
+ const gitRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
59
+ cwd: workDir,
60
+ encoding: 'utf-8',
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ }).trim();
63
+ let gitBranch;
64
+ try {
65
+ gitBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
66
+ cwd: workDir,
67
+ encoding: 'utf-8',
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ }).trim();
70
+ }
71
+ catch {
72
+ // not on a branch
73
+ }
74
+ return { gitRoot, gitBranch };
75
+ }
76
+ catch {
77
+ return {};
78
+ }
79
+ }
80
+ async function main() {
81
+ // Only activate when explicitly opted in via CLAUDE_TEMPO_ENSEMBLE
82
+ if (!process.env.CLAUDE_TEMPO_ENSEMBLE) {
83
+ log('CLAUDE_TEMPO_ENSEMBLE not set — MCP server idle (no workflow started)');
84
+ // Keep the process alive so Claude Code doesn't see a crash, but do nothing
85
+ const transport = new stdio_js_1.StdioServerTransport();
86
+ const idleServer = new mcp_js_1.McpServer({ name: 'claude-tempo', version: '0.1.0' });
87
+ await idleServer.connect(transport);
88
+ return;
89
+ }
90
+ const config = (0, config_1.getConfig)();
91
+ const isConductor = process.env.CLAUDE_TEMPO_CONDUCTOR === 'true';
92
+ let playerId = isConductor ? 'conductor' : crypto.randomBytes(4).toString('hex');
93
+ const getPlayerId = () => playerId;
94
+ const setPlayerId = (id) => { playerId = id; };
95
+ const workDir = process.cwd();
96
+ const { gitRoot, gitBranch } = getGitInfo(workDir);
97
+ log(`Starting ${isConductor ? 'conductor' : `peer ${playerId}`} in ${workDir}`);
98
+ // Connect Temporal client
99
+ const connection = await client_1.Connection.connect({
100
+ address: config.temporalAddress,
101
+ });
102
+ const client = new client_1.Client({
103
+ connection,
104
+ namespace: config.temporalNamespace,
105
+ });
106
+ // Start the Temporal worker (runs in background)
107
+ const worker = await (0, worker_1.createWorker)(config);
108
+ const workerRunPromise = worker.run();
109
+ workerRunPromise.catch((err) => {
110
+ log('Worker error:', err);
111
+ process.exit(1);
112
+ });
113
+ // Start the session workflow
114
+ const workflowId = isConductor
115
+ ? (0, config_1.conductorWorkflowId)(config.ensemble)
116
+ : `claude-session-${config.ensemble}-${playerId}`;
117
+ const sessionInput = {
118
+ metadata: {
119
+ playerId,
120
+ ensemble: config.ensemble,
121
+ hostname: os.hostname(),
122
+ workDir,
123
+ gitRoot,
124
+ gitBranch,
125
+ isConductor,
126
+ },
127
+ autoSummary: `Session in ${path.basename(workDir)}`,
128
+ };
129
+ const handle = await client.workflow.start('claudeSessionWorkflow', {
130
+ workflowId,
131
+ taskQueue: config.taskQueue,
132
+ args: [sessionInput],
133
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
134
+ workflowExecutionTimeout: '24 hours',
135
+ searchAttributes: {
136
+ ...(gitRoot ? { ClaudeTempoGitRoot: [gitRoot] } : {}),
137
+ ClaudeTempoHostname: [os.hostname()],
138
+ ClaudeTempoEnsemble: [config.ensemble],
139
+ ClaudeTempoPlayerId: [playerId],
140
+ },
141
+ });
142
+ log(`Workflow ${workflowId} started (or reconnected)`);
143
+ // If there's a conductor running, announce ourselves
144
+ if (!isConductor) {
145
+ try {
146
+ const conductorHandle = client.workflow.getHandle((0, config_1.conductorWorkflowId)(config.ensemble));
147
+ await conductorHandle.signal('receiveMessage', {
148
+ from: playerId,
149
+ text: `Player ${playerId} joined from ${workDir}`,
150
+ });
151
+ }
152
+ catch {
153
+ // No conductor running — that's fine
154
+ }
155
+ }
156
+ // Create MCP server
157
+ const serverInstructions = `You are part of the "${config.ensemble}" ensemble of Claude Code sessions coordinated via Temporal. ` +
158
+ `Your temporary player ID is "${playerId}". ` +
159
+ `IMPORTANT: If you receive a message instructing you to call \`set_name\`, do so immediately before anything else. ` +
160
+ `When you receive a message from another session, treat it like a coworker asking for help — respond promptly, then resume your work. ` +
161
+ `Use \`set_name\` to give yourself a human-readable name. ` +
162
+ `Use \`ensemble\` to see who else is active. ` +
163
+ `Use \`cue\` to reply directly to the player who messaged you, or to ask others for help. ` +
164
+ `Use \`recruit\` if you need a session in a directory where none exists. ` +
165
+ `Use \`report\` to notify the conductor of task completion, blockers, or questions — always report when you finish a recruited task.`;
166
+ const mcpServer = new mcp_js_1.McpServer({
167
+ name: 'claude-tempo',
168
+ version: '0.1.0',
169
+ }, {
170
+ capabilities: {
171
+ experimental: { 'claude/channel': {} },
172
+ },
173
+ instructions: serverInstructions,
174
+ });
175
+ // Register tools
176
+ (0, ensemble_1.registerEnsembleTool)(mcpServer, client, config, getPlayerId, workflowId);
177
+ (0, cue_1.registerCueTool)(mcpServer, client, config, getPlayerId);
178
+ (0, set_part_1.registerSetPartTool)(mcpServer, handle);
179
+ (0, set_name_1.registerSetNameTool)(mcpServer, client, config, handle, getPlayerId, setPlayerId);
180
+ (0, listen_1.registerListenTool)(mcpServer, handle);
181
+ (0, recruit_1.registerRecruitTool)(mcpServer, client, config, getPlayerId);
182
+ (0, report_1.registerReportTool)(mcpServer, client, config, getPlayerId);
183
+ (0, terminate_1.registerTerminateTool)(mcpServer, client, config, getPlayerId);
184
+ // Start message poller — push messages into Claude Code via channel notifications
185
+ const stopPoller = (0, channel_1.startMessagePoller)(handle, async (messages) => {
186
+ for (const msg of messages) {
187
+ log(`Message from ${msg.from}: ${msg.text}`);
188
+ try {
189
+ await mcpServer.server.notification({
190
+ method: 'notifications/claude/channel',
191
+ params: {
192
+ content: msg.text,
193
+ meta: {
194
+ from_player: msg.from,
195
+ sent_at: msg.timestamp,
196
+ },
197
+ },
198
+ });
199
+ }
200
+ catch (err) {
201
+ log('Channel notification error:', err);
202
+ }
203
+ }
204
+ });
205
+ // Connect MCP transport
206
+ const transport = new stdio_js_1.StdioServerTransport();
207
+ await mcpServer.connect(transport);
208
+ log('MCP server connected');
209
+ // Graceful shutdown
210
+ const shutdown = async () => {
211
+ log('Shutting down...');
212
+ stopPoller();
213
+ try {
214
+ await handle.signal('shutdown');
215
+ }
216
+ catch {
217
+ try {
218
+ await handle.cancel();
219
+ }
220
+ catch {
221
+ // workflow may already be gone
222
+ }
223
+ }
224
+ worker.shutdown();
225
+ await workerRunPromise.catch(() => { });
226
+ process.exit(0);
227
+ };
228
+ process.on('SIGINT', shutdown);
229
+ process.on('SIGTERM', shutdown);
230
+ }
231
+ main().catch((err) => {
232
+ console.error('Fatal error:', err);
233
+ process.exit(1);
234
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "claude-tempo",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
+ "type": "commonjs",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "claude-tempo": "dist/cli.js",
9
+ "claude-tempo-server": "dist/server.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && node -e \"const{bundleWorkflowCode}=require('@temporalio/worker');const path=require('path');const fs=require('fs');bundleWorkflowCode({workflowsPath:path.resolve('dist/workflows/session.js')}).then(b=>{fs.writeFileSync('workflow-bundle.js',b.code);console.log('Workflow bundle created')})\"",
13
+ "dev": "ts-node src/server.ts",
14
+ "test": "echo \"No tests yet\" && exit 0"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.28.0",
18
+ "@temporalio/activity": "~1.11.7",
19
+ "@temporalio/client": "~1.11.7",
20
+ "@temporalio/worker": "~1.11.7",
21
+ "@temporalio/workflow": "~1.11.7",
22
+ "zod": "^3.25.76"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "ts-node": "^10.9.0",
27
+ "typescript": "^5.5.0"
28
+ },
29
+ "license": "MIT",
30
+ "trustedDependencies": [
31
+ "@swc/core",
32
+ "protobufjs"
33
+ ]
34
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { WorkflowHandle } from '@temporalio/client';
2
+ import { Message } from './types';
3
+
4
+ const log = (...args: unknown[]) => console.error('[claude-tempo:poller]', ...args);
5
+
6
+ const POLL_INTERVAL_MS = 2000;
7
+
8
+ export function startMessagePoller(
9
+ handle: WorkflowHandle,
10
+ onMessages: (messages: Message[]) => void,
11
+ ): () => void {
12
+ let stopped = false;
13
+
14
+ const poll = async () => {
15
+ if (stopped) return;
16
+ try {
17
+ const messages: Message[] = await handle.query('pendingMessages');
18
+ if (messages.length > 0) {
19
+ const ids = messages.map((m) => m.id);
20
+ await handle.signal('markDelivered', ids);
21
+ onMessages(messages);
22
+ }
23
+ } catch (err) {
24
+ // Workflow may be continuing-as-new or shutting down
25
+ log('Poll error (may be transient):', err);
26
+ }
27
+ };
28
+
29
+ const interval = setInterval(poll, POLL_INTERVAL_MS);
30
+
31
+ return () => {
32
+ stopped = true;
33
+ clearInterval(interval);
34
+ };
35
+ }