claude-tempo 0.8.0 → 0.9.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 +13 -4
- package/assets/icon-32.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/icon-dark-32.png +0 -0
- package/assets/icon-dark-64.png +0 -0
- package/assets/icon-dark.svg +9 -0
- package/assets/icon.svg +9 -0
- package/assets/logo-dark.svg +11 -0
- package/assets/logo-light.svg +11 -0
- package/dist/activities/outbox.d.ts +60 -0
- package/dist/activities/outbox.js +203 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/server.js +33 -15
- package/dist/spawn.d.ts +10 -0
- package/dist/spawn.js +120 -2
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +11 -18
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +16 -138
- package/dist/tools/report.d.ts +2 -3
- package/dist/tools/report.js +11 -19
- package/dist/tools/stop.d.ts +2 -2
- package/dist/tools/stop.js +10 -17
- package/dist/types.d.ts +49 -0
- package/dist/worker.d.ts +13 -0
- package/dist/worker.js +36 -3
- package/dist/workflows/session.js +112 -1
- package/dist/workflows/signals.d.ts +4 -2
- package/dist/workflows/signals.js +4 -1
- package/package.json +2 -1
- package/workflow-bundle.js +117 -3
package/CLAUDE.md
CHANGED
|
@@ -21,16 +21,19 @@ src/
|
|
|
21
21
|
├── workflows/
|
|
22
22
|
│ ├── session.ts # claude-session workflow
|
|
23
23
|
│ └── signals.ts # Signal/query type definitions
|
|
24
|
+
├── activities/
|
|
25
|
+
│ ├── outbox.ts # Outbox delivery activities (cue, report, stop, recruit)
|
|
26
|
+
│ └── schedule-fire.ts # Schedule fire activity
|
|
24
27
|
├── tools/
|
|
25
28
|
│ ├── ensemble.ts # Discover active sessions
|
|
26
|
-
│ ├── cue.ts # Send message to peer
|
|
29
|
+
│ ├── cue.ts # Send message to peer (via outbox)
|
|
27
30
|
│ ├── set-name.ts # Set session name
|
|
28
31
|
│ ├── set-part.ts # Update own summary
|
|
29
32
|
│ ├── resolve.ts # Search-attribute session lookup
|
|
30
33
|
│ ├── listen.ts # Manual message check
|
|
31
|
-
│ ├── recruit.ts # Spawn new session
|
|
32
|
-
│ ├── report.ts # Report to conductor
|
|
33
|
-
│ ├── stop.ts # Stop a session
|
|
34
|
+
│ ├── recruit.ts # Spawn new session (via outbox)
|
|
35
|
+
│ ├── report.ts # Report to conductor (via outbox)
|
|
36
|
+
│ ├── stop.ts # Stop a session (via outbox)
|
|
34
37
|
│ └── helpers.ts # Zod/MCP tool registration wrapper
|
|
35
38
|
├── types.ts # Shared type definitions
|
|
36
39
|
├── channel.ts # Claude channel notification helper
|
|
@@ -60,6 +63,10 @@ npm test
|
|
|
60
63
|
> **Important**: Always run `npm run build` after changing workflow code (`src/workflows/`).
|
|
61
64
|
> The build pre-bundles workflows into `workflow-bundle.js` so all workers use identical code.
|
|
62
65
|
|
|
66
|
+
> **Dual workers**: Each session runs two Temporal workers — a shared `claude-tempo` queue
|
|
67
|
+
> (workflows + delivery activities) and a per-host `claude-tempo-{hostname}` queue (spawn activities only).
|
|
68
|
+
> Both are created via `createWorkers()` in `src/worker.ts`.
|
|
69
|
+
|
|
63
70
|
## Key Concepts
|
|
64
71
|
|
|
65
72
|
- **Player**: A Claude Code session registered as a Temporal workflow
|
|
@@ -70,6 +77,8 @@ npm test
|
|
|
70
77
|
- **Recruit**: Spawning a new Claude Code session as a player. The workflow is pre-created with the initial message before the process spawns, ensuring reliable delivery.
|
|
71
78
|
- **set_name**: Players start with a random hex ID; `set_name` updates the `ClaudeTempoPlayerId` search attribute to a human-readable name
|
|
72
79
|
- **Session status**: Each session has a status (`pending` → `active` → `stale`) tracked via `ClaudeTempoStatus` search attribute. Pre-created workflows start as `pending`, transition to `active` when the process connects, and become `stale` if messages go undelivered for 3+ minutes.
|
|
80
|
+
- **Outbox**: Outbound requests (cue, report, stop, recruit) go through the session's own workflow outbox instead of directly signaling other workflows. The workflow's dispatch loop processes entries via activities, decoupling tools from cross-workflow signaling.
|
|
81
|
+
- **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.
|
|
73
82
|
|
|
74
83
|
## Dashboard
|
|
75
84
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<!-- Ultra-minimal metronome icon: triangle + pendulum (dark mode) -->
|
|
3
|
+
<!-- Metronome body — single-stroke triangle -->
|
|
4
|
+
<path d="M32 8 L14 54 L50 54 Z" stroke="#FAF3EE" stroke-width="3" fill="none" stroke-linejoin="round"/>
|
|
5
|
+
<!-- Pendulum arm (angled right) -->
|
|
6
|
+
<line x1="32" y1="46" x2="44" y2="14" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
|
|
7
|
+
<!-- Pivot dot -->
|
|
8
|
+
<circle cx="32" cy="46" r="3" fill="#E07A5F"/>
|
|
9
|
+
</svg>
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<!-- Ultra-minimal metronome icon: triangle + pendulum -->
|
|
3
|
+
<!-- Metronome body — single-stroke triangle -->
|
|
4
|
+
<path d="M32 8 L14 54 L50 54 Z" stroke="#1B2838" stroke-width="3" fill="none" stroke-linejoin="round"/>
|
|
5
|
+
<!-- Pendulum arm (angled right) -->
|
|
6
|
+
<line x1="32" y1="46" x2="44" y2="14" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
|
|
7
|
+
<!-- Pivot dot -->
|
|
8
|
+
<circle cx="32" cy="46" r="3" fill="#E07A5F"/>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 140" fill="none">
|
|
2
|
+
<!-- Ultra-minimal metronome: triangle outline + pendulum line (dark mode) -->
|
|
3
|
+
<!-- Metronome body — single-stroke triangle -->
|
|
4
|
+
<path d="M160 18 L122 100 L198 100 Z" stroke="#FAF3EE" stroke-width="3" fill="none" stroke-linejoin="round"/>
|
|
5
|
+
<!-- Pendulum arm (angled right ~18deg) -->
|
|
6
|
+
<line x1="160" y1="88" x2="182" y2="24" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
|
|
7
|
+
<!-- Pivot dot -->
|
|
8
|
+
<circle cx="160" cy="88" r="3.5" fill="#E07A5F"/>
|
|
9
|
+
<!-- Text -->
|
|
10
|
+
<text x="160" y="132" text-anchor="middle" font-family="'JetBrains Mono','SF Mono','Consolas',monospace" font-size="18" font-weight="600" fill="#FAF3EE" letter-spacing="-0.5">claude-tempo</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 140" fill="none">
|
|
2
|
+
<!-- Ultra-minimal metronome: triangle outline + pendulum line -->
|
|
3
|
+
<!-- Metronome body — single-stroke triangle -->
|
|
4
|
+
<path d="M160 18 L122 100 L198 100 Z" stroke="#1B2838" stroke-width="3" fill="none" stroke-linejoin="round"/>
|
|
5
|
+
<!-- Pendulum arm (angled right ~18deg) -->
|
|
6
|
+
<line x1="160" y1="88" x2="182" y2="24" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
|
|
7
|
+
<!-- Pivot dot -->
|
|
8
|
+
<circle cx="160" cy="88" r="3.5" fill="#E07A5F"/>
|
|
9
|
+
<!-- Text -->
|
|
10
|
+
<text x="160" y="132" text-anchor="middle" font-family="'JetBrains Mono','SF Mono','Consolas',monospace" font-size="18" font-weight="600" fill="#1B2838" letter-spacing="-0.5">claude-tempo</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Client } from '@temporalio/client';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
import { AgentType } from '../types';
|
|
4
|
+
export interface DeliverCueInput {
|
|
5
|
+
ensemble: string;
|
|
6
|
+
fromPlayerId: string;
|
|
7
|
+
targetPlayerId: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DeliverReportInput {
|
|
11
|
+
ensemble: string;
|
|
12
|
+
fromPlayerId: string;
|
|
13
|
+
text: string;
|
|
14
|
+
reportType: 'result' | 'blocker' | 'question';
|
|
15
|
+
}
|
|
16
|
+
export interface TerminateSessionInput {
|
|
17
|
+
ensemble: string;
|
|
18
|
+
targetPlayerId: string;
|
|
19
|
+
terminatedBy: string;
|
|
20
|
+
}
|
|
21
|
+
export interface StartRecruitedSessionInput {
|
|
22
|
+
ensemble: string;
|
|
23
|
+
targetName: string;
|
|
24
|
+
workDir: string;
|
|
25
|
+
isConductor: boolean;
|
|
26
|
+
initialMessage?: string;
|
|
27
|
+
fromPlayerId: string;
|
|
28
|
+
agent: AgentType;
|
|
29
|
+
systemPrompt?: string;
|
|
30
|
+
taskQueue: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SpawnProcessInput {
|
|
33
|
+
targetName: string;
|
|
34
|
+
workDir: string;
|
|
35
|
+
isConductor: boolean;
|
|
36
|
+
agent: AgentType;
|
|
37
|
+
systemPrompt?: string;
|
|
38
|
+
ensemble: string;
|
|
39
|
+
temporalAddress: string;
|
|
40
|
+
temporalNamespace: string;
|
|
41
|
+
temporalApiKey?: string;
|
|
42
|
+
temporalTlsCertPath?: string;
|
|
43
|
+
temporalTlsKeyPath?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface OutboxActivityResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface OutboxActivities {
|
|
50
|
+
deliverCue(input: DeliverCueInput): Promise<OutboxActivityResult>;
|
|
51
|
+
deliverReport(input: DeliverReportInput): Promise<OutboxActivityResult>;
|
|
52
|
+
terminateSession(input: TerminateSessionInput): Promise<OutboxActivityResult>;
|
|
53
|
+
startRecruitedSession(input: StartRecruitedSessionInput): Promise<OutboxActivityResult>;
|
|
54
|
+
spawnProcess(input: SpawnProcessInput): Promise<OutboxActivityResult>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create outbox delivery activities bound to a Temporal client and config.
|
|
58
|
+
* The returned object is registered with the worker as activities.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createOutboxActivities(client: Client, config: Config): OutboxActivities;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createOutboxActivities = createOutboxActivities;
|
|
37
|
+
const client_1 = require("@temporalio/client");
|
|
38
|
+
const activity_1 = require("@temporalio/activity");
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const config_1 = require("../config");
|
|
41
|
+
const git_info_1 = require("../git-info");
|
|
42
|
+
const spawn_1 = require("../spawn");
|
|
43
|
+
const config_2 = require("../config");
|
|
44
|
+
const log = (...args) => console.error('[claude-tempo:outbox]', ...args);
|
|
45
|
+
// ── Helper: resolve session by player name ──
|
|
46
|
+
async function resolveSession(client, ensemble, playerName) {
|
|
47
|
+
const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
|
|
48
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
49
|
+
try {
|
|
50
|
+
const handle = client.workflow.getHandle(wf.workflowId);
|
|
51
|
+
const metadata = await handle.query('getMetadata');
|
|
52
|
+
if (metadata.ensemble === ensemble && metadata.playerId === playerName) {
|
|
53
|
+
return handle;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Workflow may have just completed — skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create outbox delivery activities bound to a Temporal client and config.
|
|
64
|
+
* The returned object is registered with the worker as activities.
|
|
65
|
+
*/
|
|
66
|
+
function createOutboxActivities(client, config) {
|
|
67
|
+
return {
|
|
68
|
+
async deliverCue(input) {
|
|
69
|
+
const { ensemble, fromPlayerId, targetPlayerId, message } = input;
|
|
70
|
+
const handle = await resolveSession(client, ensemble, targetPlayerId);
|
|
71
|
+
if (!handle) {
|
|
72
|
+
throw activity_1.ApplicationFailure.nonRetryable(`No active session found for "${targetPlayerId}"`);
|
|
73
|
+
}
|
|
74
|
+
await handle.signal('receiveMessage', { from: fromPlayerId, text: message });
|
|
75
|
+
return { success: true };
|
|
76
|
+
},
|
|
77
|
+
async deliverReport(input) {
|
|
78
|
+
const { ensemble, fromPlayerId, text, reportType } = input;
|
|
79
|
+
const conductorId = (0, config_1.conductorWorkflowId)(ensemble);
|
|
80
|
+
const handle = client.workflow.getHandle(conductorId);
|
|
81
|
+
await handle.signal('playerReport', { playerId: fromPlayerId, text, type: reportType });
|
|
82
|
+
return { success: true };
|
|
83
|
+
},
|
|
84
|
+
async terminateSession(input) {
|
|
85
|
+
const { ensemble, targetPlayerId, terminatedBy } = input;
|
|
86
|
+
const handle = await resolveSession(client, ensemble, targetPlayerId);
|
|
87
|
+
if (!handle) {
|
|
88
|
+
throw activity_1.ApplicationFailure.nonRetryable(`No active session found for "${targetPlayerId}"`);
|
|
89
|
+
}
|
|
90
|
+
// Signal target to mark as terminated
|
|
91
|
+
await handle.signal('updateMetadata', { status: 'terminated', terminatedBy });
|
|
92
|
+
// Notify conductor about the termination (best effort)
|
|
93
|
+
try {
|
|
94
|
+
const conductorId = (0, config_1.conductorWorkflowId)(ensemble);
|
|
95
|
+
const conductorHandle = client.workflow.getHandle(conductorId);
|
|
96
|
+
await conductorHandle.signal('receiveMessage', {
|
|
97
|
+
from: 'system',
|
|
98
|
+
text: `Session "${targetPlayerId}" was terminated by ${terminatedBy}.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Conductor may not exist — that's fine
|
|
103
|
+
}
|
|
104
|
+
return { success: true };
|
|
105
|
+
},
|
|
106
|
+
async startRecruitedSession(input) {
|
|
107
|
+
const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue } = input;
|
|
108
|
+
try {
|
|
109
|
+
const workflowId = isConductor
|
|
110
|
+
? (0, config_1.conductorWorkflowId)(ensemble)
|
|
111
|
+
: (0, config_1.sessionWorkflowId)(ensemble, targetName);
|
|
112
|
+
const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
|
|
113
|
+
const sessionInput = {
|
|
114
|
+
metadata: {
|
|
115
|
+
playerId: targetName,
|
|
116
|
+
ensemble,
|
|
117
|
+
hostname: os.hostname(),
|
|
118
|
+
workDir,
|
|
119
|
+
gitRoot,
|
|
120
|
+
gitBranch,
|
|
121
|
+
isConductor,
|
|
122
|
+
agentType: agent,
|
|
123
|
+
status: 'pending',
|
|
124
|
+
},
|
|
125
|
+
autoSummary: `Session in ${require('path').basename(workDir)}`,
|
|
126
|
+
disableStaleDetection: true,
|
|
127
|
+
...(initialMessage ? {
|
|
128
|
+
messages: [{
|
|
129
|
+
id: require('crypto').randomUUID(),
|
|
130
|
+
from: fromPlayerId,
|
|
131
|
+
text: initialMessage,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
delivered: false,
|
|
134
|
+
}],
|
|
135
|
+
} : {}),
|
|
136
|
+
};
|
|
137
|
+
await client.workflow.start('claudeSessionWorkflow', {
|
|
138
|
+
workflowId,
|
|
139
|
+
taskQueue,
|
|
140
|
+
args: [sessionInput],
|
|
141
|
+
workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
|
|
142
|
+
searchAttributes: {
|
|
143
|
+
...(gitRoot ? { ClaudeTempoGitRoot: [gitRoot] } : {}),
|
|
144
|
+
ClaudeTempoHostname: [os.hostname()],
|
|
145
|
+
ClaudeTempoEnsemble: [ensemble],
|
|
146
|
+
ClaudeTempoPlayerId: [targetName],
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
log(`Pre-created workflow ${workflowId} for recruit "${targetName}"`);
|
|
150
|
+
return { success: true };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw activity_1.ApplicationFailure.nonRetryable(`Failed to start recruited session "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async spawnProcess(input) {
|
|
157
|
+
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = input;
|
|
158
|
+
try {
|
|
159
|
+
if (agent === 'copilot') {
|
|
160
|
+
const { pid } = (0, spawn_1.spawnCopilotBridge)({
|
|
161
|
+
name: targetName,
|
|
162
|
+
ensemble,
|
|
163
|
+
temporalAddress,
|
|
164
|
+
temporalNamespace,
|
|
165
|
+
temporalApiKey,
|
|
166
|
+
temporalTlsCertPath,
|
|
167
|
+
temporalTlsKeyPath,
|
|
168
|
+
isConductor,
|
|
169
|
+
workDir,
|
|
170
|
+
});
|
|
171
|
+
log(`Spawned copilot-bridge (pid ${pid}) in ${workDir} as "${targetName}"`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const spawnArgs = [
|
|
175
|
+
'--dangerously-skip-permissions',
|
|
176
|
+
'--dangerously-load-development-channels', 'server:claude-tempo',
|
|
177
|
+
'-n', targetName,
|
|
178
|
+
...(systemPrompt ? ['--system-prompt', systemPrompt] : []),
|
|
179
|
+
];
|
|
180
|
+
const envVars = {
|
|
181
|
+
[config_2.ENV.ENSEMBLE]: ensemble,
|
|
182
|
+
[config_2.ENV.CONDUCTOR]: isConductor ? 'true' : '',
|
|
183
|
+
[config_2.ENV.PLAYER_NAME]: targetName,
|
|
184
|
+
[config_2.ENV.TEMPORAL_ADDRESS]: temporalAddress,
|
|
185
|
+
[config_2.ENV.TEMPORAL_NAMESPACE]: temporalNamespace,
|
|
186
|
+
};
|
|
187
|
+
if (temporalApiKey)
|
|
188
|
+
envVars[config_2.ENV.TEMPORAL_API_KEY] = temporalApiKey;
|
|
189
|
+
if (temporalTlsCertPath)
|
|
190
|
+
envVars[config_2.ENV.TEMPORAL_TLS_CERT_PATH] = temporalTlsCertPath;
|
|
191
|
+
if (temporalTlsKeyPath)
|
|
192
|
+
envVars[config_2.ENV.TEMPORAL_TLS_KEY_PATH] = temporalTlsKeyPath;
|
|
193
|
+
const { pid } = (0, spawn_1.spawnInTerminal)(spawnArgs, workDir, envVars);
|
|
194
|
+
log(`Spawned claude process (pid ${pid}) in ${workDir} as "${targetName}"`);
|
|
195
|
+
}
|
|
196
|
+
return { success: true };
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
throw activity_1.ApplicationFailure.nonRetryable(`Failed to spawn process for "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface ConfigWithSources {
|
|
|
85
85
|
* Used by `claude-tempo config show` to help users debug.
|
|
86
86
|
*/
|
|
87
87
|
export declare function getConfigWithSources(overrides?: CliOverrides): ConfigWithSources;
|
|
88
|
+
/** Build a per-host task queue name for cross-machine activities: {taskQueue}-{hostname} */
|
|
89
|
+
export declare function hostTaskQueue(taskQueue: string, hostname: string): string;
|
|
88
90
|
/** Build a workflow ID for a player session: claude-session-{ensemble}-{playerId} */
|
|
89
91
|
export declare function sessionWorkflowId(ensemble: string, playerId: string): string;
|
|
90
92
|
/** Build a workflow ID for a conductor: claude-session-{ensemble}-conductor */
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.loadTemporalCliConfig = loadTemporalCliConfig;
|
|
|
7
7
|
exports.parseTemporalYaml = parseTemporalYaml;
|
|
8
8
|
exports.getConfig = getConfig;
|
|
9
9
|
exports.getConfigWithSources = getConfigWithSources;
|
|
10
|
+
exports.hostTaskQueue = hostTaskQueue;
|
|
10
11
|
exports.sessionWorkflowId = sessionWorkflowId;
|
|
11
12
|
exports.conductorWorkflowId = conductorWorkflowId;
|
|
12
13
|
exports.schedulerWorkflowId = schedulerWorkflowId;
|
|
@@ -239,6 +240,10 @@ function getConfigWithSources(overrides = {}) {
|
|
|
239
240
|
},
|
|
240
241
|
};
|
|
241
242
|
}
|
|
243
|
+
/** Build a per-host task queue name for cross-machine activities: {taskQueue}-{hostname} */
|
|
244
|
+
function hostTaskQueue(taskQueue, hostname) {
|
|
245
|
+
return `${taskQueue}-${hostname}`;
|
|
246
|
+
}
|
|
242
247
|
/** Build a workflow ID for a player session: claude-session-{ensemble}-{playerId} */
|
|
243
248
|
function sessionWorkflowId(ensemble, playerId) {
|
|
244
249
|
return `claude-session-${ensemble}-${playerId}`;
|
package/dist/server.js
CHANGED
|
@@ -88,11 +88,16 @@ async function main() {
|
|
|
88
88
|
connection,
|
|
89
89
|
namespace: config.temporalNamespace,
|
|
90
90
|
});
|
|
91
|
-
// Start the Temporal
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
// Start the Temporal workers (runs in background)
|
|
92
|
+
const { sharedWorker, hostWorker } = await (0, worker_1.createWorkers)(config);
|
|
93
|
+
const sharedWorkerRunPromise = sharedWorker.run();
|
|
94
|
+
const hostWorkerRunPromise = hostWorker.run();
|
|
95
|
+
sharedWorkerRunPromise.catch((err) => {
|
|
96
|
+
log('Shared worker error:', err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
hostWorkerRunPromise.catch((err) => {
|
|
100
|
+
log('Host worker error:', err);
|
|
96
101
|
process.exit(1);
|
|
97
102
|
});
|
|
98
103
|
// Start the session workflow
|
|
@@ -112,6 +117,14 @@ async function main() {
|
|
|
112
117
|
agentType: isBridgeMode ? 'copilot' : 'claude',
|
|
113
118
|
},
|
|
114
119
|
autoSummary: `Session in ${path.basename(workDir)}`,
|
|
120
|
+
temporalConfig: {
|
|
121
|
+
temporalAddress: config.temporalAddress,
|
|
122
|
+
temporalNamespace: config.temporalNamespace,
|
|
123
|
+
temporalApiKey: config.temporalApiKey,
|
|
124
|
+
temporalTlsCertPath: config.temporalTlsCertPath,
|
|
125
|
+
temporalTlsKeyPath: config.temporalTlsKeyPath,
|
|
126
|
+
taskQueue: config.taskQueue,
|
|
127
|
+
},
|
|
115
128
|
};
|
|
116
129
|
const handle = await client.workflow.start('claudeSessionWorkflow', {
|
|
117
130
|
workflowId,
|
|
@@ -129,19 +142,25 @@ async function main() {
|
|
|
129
142
|
log(`Workflow ${workflowId} started (or reconnected)`);
|
|
130
143
|
// Watch for workflow completion — exit the process when the workflow ends
|
|
131
144
|
// (e.g., via stop tool setting status to 'terminated')
|
|
145
|
+
const shutdownWorkers = () => {
|
|
146
|
+
sharedWorker.shutdown();
|
|
147
|
+
hostWorker.shutdown();
|
|
148
|
+
return Promise.all([
|
|
149
|
+
sharedWorkerRunPromise.catch(() => { }),
|
|
150
|
+
hostWorkerRunPromise.catch(() => { }),
|
|
151
|
+
]);
|
|
152
|
+
};
|
|
132
153
|
handle.result().then(() => {
|
|
133
154
|
log('Workflow completed — shutting down');
|
|
134
155
|
stopPoller();
|
|
135
|
-
|
|
136
|
-
workerRunPromise.catch(() => { }).then(() => process.exit(0));
|
|
156
|
+
shutdownWorkers().then(() => process.exit(0));
|
|
137
157
|
}).catch((err) => {
|
|
138
158
|
// Only exit on workflow-level errors (cancelled, failed), not transient connection errors
|
|
139
159
|
const name = err?.name || '';
|
|
140
160
|
if (name.includes('WorkflowFailed') || name.includes('WorkflowCancelled') || name.includes('WorkflowNotFound')) {
|
|
141
161
|
log('Workflow ended unexpectedly — shutting down');
|
|
142
162
|
stopPoller();
|
|
143
|
-
|
|
144
|
-
workerRunPromise.catch(() => { }).then(() => process.exit(1));
|
|
163
|
+
shutdownWorkers().then(() => process.exit(1));
|
|
145
164
|
}
|
|
146
165
|
else {
|
|
147
166
|
log('Transient error watching workflow result:', err?.message || err);
|
|
@@ -192,13 +211,13 @@ async function main() {
|
|
|
192
211
|
});
|
|
193
212
|
// Register tools
|
|
194
213
|
(0, ensemble_1.registerEnsembleTool)(mcpServer, client, config, getPlayerId, workflowId);
|
|
195
|
-
(0, cue_1.registerCueTool)(mcpServer, client, config, getPlayerId);
|
|
214
|
+
(0, cue_1.registerCueTool)(mcpServer, client, config, getPlayerId, handle);
|
|
196
215
|
(0, set_part_1.registerSetPartTool)(mcpServer, handle);
|
|
197
216
|
(0, set_name_1.registerSetNameTool)(mcpServer, client, config, handle, getPlayerId, setPlayerId);
|
|
198
217
|
(0, listen_1.registerListenTool)(mcpServer, handle);
|
|
199
|
-
(0, recruit_1.registerRecruitTool)(mcpServer, client, config, getPlayerId, isBridgeMode ? 'copilot' : 'claude');
|
|
200
|
-
(0, report_1.registerReportTool)(mcpServer,
|
|
201
|
-
(0, stop_1.registerStopTool)(mcpServer, client, config, getPlayerId);
|
|
218
|
+
(0, recruit_1.registerRecruitTool)(mcpServer, client, config, getPlayerId, handle, isBridgeMode ? 'copilot' : 'claude');
|
|
219
|
+
(0, report_1.registerReportTool)(mcpServer, handle);
|
|
220
|
+
(0, stop_1.registerStopTool)(mcpServer, client, config, getPlayerId, handle);
|
|
202
221
|
(0, schedule_1.registerScheduleTool)(mcpServer, client, config, getPlayerId);
|
|
203
222
|
(0, unschedule_1.registerUnscheduleTool)(mcpServer, client, config);
|
|
204
223
|
(0, schedules_1.registerSchedulesTool)(mcpServer, client, config);
|
|
@@ -246,8 +265,7 @@ async function main() {
|
|
|
246
265
|
catch {
|
|
247
266
|
// workflow may already be gone
|
|
248
267
|
}
|
|
249
|
-
|
|
250
|
-
await workerRunPromise.catch(() => { });
|
|
268
|
+
await shutdownWorkers();
|
|
251
269
|
process.exit(0);
|
|
252
270
|
};
|
|
253
271
|
process.on('SIGINT', shutdown);
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** Resolve the absolute path to the package's icon file (PNG for Windows Terminal). */
|
|
2
|
+
export declare function resolveIconPath(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Ensure a "claude-tempo" profile exists in Windows Terminal settings.json
|
|
5
|
+
* with our icon. Returns true if the profile is ready for use.
|
|
6
|
+
*
|
|
7
|
+
* Windows Terminal settings path:
|
|
8
|
+
* %LOCALAPPDATA%/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureWindowsTerminalProfile(): boolean;
|
|
1
11
|
/** POSIX shell-safe single-quoting (works in bash, zsh, and fish) */
|
|
2
12
|
export declare function shellQuote(s: string): string;
|
|
3
13
|
/** Resolve the absolute path to the `claude` binary */
|
package/dist/spawn.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveIconPath = resolveIconPath;
|
|
4
|
+
exports.ensureWindowsTerminalProfile = ensureWindowsTerminalProfile;
|
|
3
5
|
exports.shellQuote = shellQuote;
|
|
4
6
|
exports.resolveClaudePath = resolveClaudePath;
|
|
5
7
|
exports.detectMacTerminal = detectMacTerminal;
|
|
@@ -13,6 +15,117 @@ const path_1 = require("path");
|
|
|
13
15
|
const os_1 = require("os");
|
|
14
16
|
const config_1 = require("./config");
|
|
15
17
|
const log = (...args) => console.error('[claude-tempo:spawn]', ...args);
|
|
18
|
+
/** Stable GUID for the claude-tempo Windows Terminal profile. */
|
|
19
|
+
const WT_PROFILE_GUID = '{c1a0d300-0e30-4000-a000-c1a0de00e300}';
|
|
20
|
+
const WT_PROFILE_NAME = 'claude-tempo';
|
|
21
|
+
/** Resolve the absolute path to the package's icon file (PNG for Windows Terminal). */
|
|
22
|
+
function resolveIconPath() {
|
|
23
|
+
// __dirname is src/ in dev or dist/ in production; assets/ is at the package root
|
|
24
|
+
const packageRoot = (0, path_1.resolve)(__dirname, '..');
|
|
25
|
+
return (0, path_1.join)(packageRoot, 'assets', 'icon-dark-32.png');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Strip // and /* comments from JSON-with-comments (JSONC), leaving strings intact.
|
|
29
|
+
* Handles escaped quotes inside strings correctly.
|
|
30
|
+
*/
|
|
31
|
+
function stripJsonComments(text) {
|
|
32
|
+
let result = '';
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < text.length) {
|
|
35
|
+
// String literal — copy verbatim until closing quote
|
|
36
|
+
if (text[i] === '"') {
|
|
37
|
+
result += '"';
|
|
38
|
+
i++;
|
|
39
|
+
while (i < text.length && text[i] !== '"') {
|
|
40
|
+
if (text[i] === '\\') {
|
|
41
|
+
result += text[i++];
|
|
42
|
+
} // skip escaped char
|
|
43
|
+
if (i < text.length) {
|
|
44
|
+
result += text[i++];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (i < text.length) {
|
|
48
|
+
result += text[i++];
|
|
49
|
+
} // closing quote
|
|
50
|
+
// Line comment
|
|
51
|
+
}
|
|
52
|
+
else if (text[i] === '/' && text[i + 1] === '/') {
|
|
53
|
+
while (i < text.length && text[i] !== '\n')
|
|
54
|
+
i++;
|
|
55
|
+
// Block comment
|
|
56
|
+
}
|
|
57
|
+
else if (text[i] === '/' && text[i + 1] === '*') {
|
|
58
|
+
i += 2;
|
|
59
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
60
|
+
i++;
|
|
61
|
+
i += 2; // skip closing */
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result += text[i++];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Ensure a "claude-tempo" profile exists in Windows Terminal settings.json
|
|
71
|
+
* with our icon. Returns true if the profile is ready for use.
|
|
72
|
+
*
|
|
73
|
+
* Windows Terminal settings path:
|
|
74
|
+
* %LOCALAPPDATA%/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json
|
|
75
|
+
*/
|
|
76
|
+
function ensureWindowsTerminalProfile() {
|
|
77
|
+
if (process.platform !== 'win32')
|
|
78
|
+
return false;
|
|
79
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
80
|
+
if (!localAppData)
|
|
81
|
+
return false;
|
|
82
|
+
const settingsPath = (0, path_1.join)(localAppData, 'Packages', 'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'LocalState', 'settings.json');
|
|
83
|
+
if (!(0, fs_1.existsSync)(settingsPath)) {
|
|
84
|
+
log('Windows Terminal settings.json not found at', settingsPath);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const raw = (0, fs_1.readFileSync)(settingsPath, 'utf8');
|
|
89
|
+
// Windows Terminal settings.json may contain comments — strip them for JSON.parse.
|
|
90
|
+
// Naive regex would eat "//" inside strings (e.g., URLs). Walk char-by-char instead.
|
|
91
|
+
const settings = JSON.parse(stripJsonComments(raw));
|
|
92
|
+
if (!settings.profiles?.list)
|
|
93
|
+
return false;
|
|
94
|
+
const iconPath = resolveIconPath().replace(/\\/g, '/');
|
|
95
|
+
if (!(0, fs_1.existsSync)(iconPath.replace(/\//g, '\\'))) {
|
|
96
|
+
log('Icon file not found at', iconPath);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const profiles = settings.profiles.list;
|
|
100
|
+
// Check if our profile already exists (by GUID or name)
|
|
101
|
+
const existing = profiles.find((p) => p.guid === WT_PROFILE_GUID || p.name === WT_PROFILE_NAME);
|
|
102
|
+
if (existing) {
|
|
103
|
+
// Update icon path if it changed (e.g. package moved)
|
|
104
|
+
if (existing.icon !== iconPath) {
|
|
105
|
+
existing.icon = iconPath;
|
|
106
|
+
(0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
107
|
+
log('Updated claude-tempo profile icon in Windows Terminal');
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Add new profile
|
|
112
|
+
profiles.push({
|
|
113
|
+
guid: WT_PROFILE_GUID,
|
|
114
|
+
name: WT_PROFILE_NAME,
|
|
115
|
+
commandline: 'cmd.exe',
|
|
116
|
+
icon: iconPath,
|
|
117
|
+
hidden: true, // Hide from dropdown — only used programmatically
|
|
118
|
+
});
|
|
119
|
+
// Write back with original formatting style (4-space indent to match WT default)
|
|
120
|
+
(0, fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
121
|
+
log('Created claude-tempo profile in Windows Terminal with icon:', iconPath);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
log('Failed to update Windows Terminal settings:', e);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
16
129
|
/** POSIX shell-safe single-quoting (works in bash, zsh, and fish) */
|
|
17
130
|
function shellQuote(s) {
|
|
18
131
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -186,6 +299,8 @@ function spawnInTerminal(claudeArgs, workDir, envVars) {
|
|
|
186
299
|
const tabTitle = nameIdx !== -1 && nameIdx + 1 < claudeArgs.length
|
|
187
300
|
? claudeArgs[nameIdx + 1]
|
|
188
301
|
: 'claude-tempo';
|
|
302
|
+
// Ensure our profile with icon exists in Windows Terminal settings
|
|
303
|
+
const hasProfile = ensureWindowsTerminalProfile();
|
|
189
304
|
// Build inline env var assignments for cmd /c since wt.exe spawns
|
|
190
305
|
// a new process that won't inherit our env.
|
|
191
306
|
// Escape values for cmd.exe: wrap in quotes and escape inner special chars.
|
|
@@ -198,14 +313,17 @@ function spawnInTerminal(claudeArgs, workDir, envVars) {
|
|
|
198
313
|
? `${setCmds} && ${claudeCmd}`
|
|
199
314
|
: claudeCmd;
|
|
200
315
|
// Use `cmd.exe /c start "" wt.exe ...` to resolve the UWP app alias
|
|
201
|
-
|
|
316
|
+
// When our profile exists, use --profile to get the tab icon
|
|
317
|
+
const wtArgs = [
|
|
202
318
|
'/c', 'start', '',
|
|
203
319
|
'wt.exe', '-w', '0',
|
|
204
320
|
'new-tab',
|
|
321
|
+
...(hasProfile ? ['--profile', WT_PROFILE_NAME] : []),
|
|
205
322
|
'--title', tabTitle,
|
|
206
323
|
'-d', workDir,
|
|
207
324
|
'cmd', '/k', innerCmd,
|
|
208
|
-
]
|
|
325
|
+
];
|
|
326
|
+
const child = (0, child_process_1.spawn)('cmd.exe', wtArgs, {
|
|
209
327
|
detached: true,
|
|
210
328
|
stdio: 'ignore',
|
|
211
329
|
});
|