claude-multi-session 1.0.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.
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Delegate — The Control Loop for intelligent task delegation.
3
+ *
4
+ * This is what makes the system truly human-like. Instead of just
5
+ * sending messages and getting responses, the Delegate system:
6
+ *
7
+ * 1. Spawns a child session with the right permissions
8
+ * 2. Sends the task
9
+ * 3. Monitors the response for issues (permission denials, errors, going off-track)
10
+ * 4. Auto-handles common problems (permission retries, error recovery)
11
+ * 5. Returns structured output for the parent Claude to evaluate
12
+ * 6. Accepts follow-up instructions and continues the loop
13
+ *
14
+ * The parent Claude acts as the "brain" — it reads the structured output,
15
+ * decides if the work is good, and sends corrections or approvals.
16
+ *
17
+ * Usage:
18
+ * const delegate = new Delegate(manager);
19
+ *
20
+ * // Simple one-shot delegation
21
+ * const result = await delegate.run('fix-auth', {
22
+ * task: 'Fix the authentication bug in auth.service.ts',
23
+ * model: 'sonnet',
24
+ * maxCost: 0.50,
25
+ * });
26
+ * console.log(result.status); // 'completed', 'needs_input', 'failed', 'budget_exceeded'
27
+ * console.log(result.response); // The actual work done
28
+ *
29
+ * // Multi-step delegation with evaluation
30
+ * const r1 = await delegate.run('build-feature', { task: 'Build login page' });
31
+ * // Parent evaluates r1.response...
32
+ * const r2 = await delegate.continue('build-feature', 'Good, now add form validation');
33
+ * // Parent evaluates r2.response...
34
+ * const r3 = await delegate.continue('build-feature', 'Perfect. Now write tests.');
35
+ * delegate.finish('build-feature');
36
+ */
37
+
38
+ const SafetyNet = require('./safety-net');
39
+
40
+ // Permission mode recommendations based on task type
41
+ const PERMISSION_PRESETS = {
42
+ // Read-only: can search and read but not modify
43
+ 'read-only': {
44
+ permissionMode: 'default',
45
+ allowedTools: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'TaskOutput'],
46
+ },
47
+ // Code review: read + analyze
48
+ 'review': {
49
+ permissionMode: 'default',
50
+ allowedTools: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'TaskOutput'],
51
+ },
52
+ // Edit files: can read and edit but limited bash
53
+ 'edit': {
54
+ permissionMode: 'acceptEdits',
55
+ allowedTools: [], // All tools, but auto-accept edits
56
+ },
57
+ // Full access: can do anything (use with caution)
58
+ 'full': {
59
+ permissionMode: 'bypassPermissions',
60
+ allowedTools: [],
61
+ },
62
+ // Plan only: can explore but not execute
63
+ 'plan': {
64
+ permissionMode: 'plan',
65
+ allowedTools: [],
66
+ },
67
+ };
68
+
69
+ class Delegate {
70
+ /**
71
+ * @param {SessionManager} manager - The session manager instance
72
+ */
73
+ constructor(manager) {
74
+ this.manager = manager;
75
+ this.safetyNets = new Map(); // name -> SafetyNet
76
+ }
77
+
78
+ /**
79
+ * Run a task in a new delegated session.
80
+ *
81
+ * This is the main entry point. It:
82
+ * 1. Creates a session with appropriate permissions
83
+ * 2. Attaches safety limits
84
+ * 3. Sends the task
85
+ * 4. Detects and auto-handles permission issues
86
+ * 5. Returns structured output
87
+ *
88
+ * @param {string} name - Session name
89
+ * @param {object} options - Task configuration
90
+ * @param {string} options.task - The task description
91
+ * @param {string} [options.model='sonnet'] - Model to use
92
+ * @param {string} [options.preset='edit'] - Permission preset
93
+ * @param {string} [options.workDir] - Working directory
94
+ * @param {number} [options.maxCost=2.00] - Max cost in USD
95
+ * @param {number} [options.maxTurns=50] - Max agent turns
96
+ * @param {string} [options.context] - Extra context to prepend
97
+ * @param {string} [options.systemPrompt] - System prompt append
98
+ * @param {string} [options.agent] - Agent to use
99
+ * @param {boolean} [options.safety=true] - Enable safety net (set false to disable)
100
+ * @returns {Promise<DelegateResult>} Structured result
101
+ */
102
+ async run(name, options = {}) {
103
+ const task = options.task;
104
+ if (!task) throw new Error('options.task is required');
105
+
106
+ // Get permission preset
107
+ const preset = PERMISSION_PRESETS[options.preset || 'edit'] || PERMISSION_PRESETS.edit;
108
+
109
+ // Create safety net (only if not disabled)
110
+ // options.safety defaults to true — pass safety: false to disable
111
+ const useSafety = options.safety !== false;
112
+ const safety = useSafety
113
+ ? new SafetyNet({
114
+ maxCostUsd: options.maxCost ?? 2.00,
115
+ maxTurns: options.maxTurns ?? 50,
116
+ maxDurationMs: options.maxDuration ?? 300000,
117
+ protectedPaths: options.protectedPaths,
118
+ })
119
+ : null;
120
+
121
+ // Build the full prompt with context and instructions
122
+ const fullPrompt = this._buildPrompt(task, options.context);
123
+
124
+ // Spawn the session
125
+ let spawnResult;
126
+ try {
127
+ spawnResult = await this.manager.spawn(name, {
128
+ prompt: fullPrompt,
129
+ model: options.model || 'sonnet',
130
+ workDir: options.workDir || process.cwd(),
131
+ permissionMode: preset.permissionMode,
132
+ allowedTools: preset.allowedTools.length > 0 ? preset.allowedTools : undefined,
133
+ systemPrompt: options.systemPrompt,
134
+ agent: options.agent,
135
+ });
136
+ } catch (err) {
137
+ return this._makeResult('failed', null, null, {
138
+ error: err.message,
139
+ name,
140
+ });
141
+ }
142
+
143
+ // Attach safety net to the live session (only if enabled)
144
+ if (safety) {
145
+ const session = this.manager.sessions.get(name);
146
+ if (session) {
147
+ safety.attach(session);
148
+ this.safetyNets.set(name, safety);
149
+ }
150
+ }
151
+
152
+ const response = spawnResult.response;
153
+
154
+ // Check if the response indicates a permission issue
155
+ if (response && this._isPermissionDenied(response.text)) {
156
+ // Auto-retry with permission granted
157
+ const retryResult = await this._handlePermissionRetry(name, response.text);
158
+ if (retryResult) {
159
+ return this._makeResult('completed', retryResult, safety, { name });
160
+ }
161
+ }
162
+
163
+ // Check safety violations (only if safety is enabled)
164
+ if (safety && safety.violations.length > 0) {
165
+ const lastViolation = safety.violations[safety.violations.length - 1];
166
+ return this._makeResult(lastViolation.type, response, safety, { name });
167
+ }
168
+
169
+ // Return structured result
170
+ return this._makeResult('completed', response, safety, { name });
171
+ }
172
+
173
+ /**
174
+ * Continue a delegated task with follow-up instructions.
175
+ *
176
+ * The parent Claude evaluates the previous result and sends corrections,
177
+ * additional instructions, or approval.
178
+ *
179
+ * @param {string} name - Session name
180
+ * @param {string} message - Follow-up instruction
181
+ * @returns {Promise<DelegateResult>} Structured result
182
+ */
183
+ async continue(name, message) {
184
+ const safety = this.safetyNets.get(name);
185
+
186
+ let response;
187
+ try {
188
+ response = await this.manager.send(name, message);
189
+ } catch (err) {
190
+ // Session might be stopped — try auto-resume
191
+ try {
192
+ response = await this.manager.resume(name, message);
193
+ // Re-attach safety net to new session
194
+ const session = this.manager.sessions.get(name);
195
+ if (session && safety) {
196
+ safety.attach(session);
197
+ }
198
+ } catch (resumeErr) {
199
+ return this._makeResult('failed', null, safety, {
200
+ error: resumeErr.message,
201
+ name,
202
+ });
203
+ }
204
+ }
205
+
206
+ // Check for permission issues
207
+ if (response && this._isPermissionDenied(response.text)) {
208
+ const retryResult = await this._handlePermissionRetry(name, response.text);
209
+ if (retryResult) {
210
+ return this._makeResult('completed', retryResult, safety, { name });
211
+ }
212
+ }
213
+
214
+ // Check safety violations
215
+ if (safety && safety.violations.length > 0) {
216
+ const lastViolation = safety.violations[safety.violations.length - 1];
217
+ return this._makeResult(lastViolation.type, response, safety, { name });
218
+ }
219
+
220
+ return this._makeResult('completed', response, safety, { name });
221
+ }
222
+
223
+ /**
224
+ * Finish a delegated task — stop the session.
225
+ */
226
+ finish(name) {
227
+ try {
228
+ this.manager.stop(name);
229
+ } catch (e) {
230
+ // Session might already be stopped
231
+ }
232
+ this.safetyNets.delete(name);
233
+ }
234
+
235
+ /**
236
+ * Kill a delegated task immediately.
237
+ */
238
+ abort(name) {
239
+ try {
240
+ this.manager.kill(name);
241
+ } catch (e) {}
242
+ this.safetyNets.delete(name);
243
+ }
244
+
245
+ // ===========================================================================
246
+ // Private
247
+ // ===========================================================================
248
+
249
+ /**
250
+ * Build the full prompt with context and structured output instructions.
251
+ */
252
+ _buildPrompt(task, context) {
253
+ let prompt = '';
254
+
255
+ if (context) {
256
+ prompt += `CONTEXT:\n${context}\n\n`;
257
+ }
258
+
259
+ prompt += `TASK:\n${task}\n\n`;
260
+ prompt += `INSTRUCTIONS:\n`;
261
+ prompt += `- Complete the task thoroughly\n`;
262
+ prompt += `- If you encounter errors, try to fix them\n`;
263
+ prompt += `- If you need clarification, say what you need and stop\n`;
264
+ prompt += `- At the end, provide a brief summary of what you did\n`;
265
+
266
+ return prompt;
267
+ }
268
+
269
+ /**
270
+ * Detect if a response text indicates a permission denial.
271
+ */
272
+ _isPermissionDenied(text) {
273
+ if (!text) return false;
274
+ const patterns = [
275
+ 'permission to write',
276
+ 'permission to edit',
277
+ 'permission to create',
278
+ 'permission to run',
279
+ 'permission to execute',
280
+ 'haven\'t granted',
281
+ 'need your permission',
282
+ 'would you like to allow',
283
+ 'please approve',
284
+ 'permission denied',
285
+ ];
286
+ const lower = text.toLowerCase();
287
+ return patterns.some(p => lower.includes(p));
288
+ }
289
+
290
+ /**
291
+ * Handle a permission denial by sending approval.
292
+ */
293
+ async _handlePermissionRetry(name, deniedText) {
294
+ try {
295
+ const response = await this.manager.send(
296
+ name,
297
+ 'Yes, you have permission. Go ahead and proceed with all file operations. Do not ask for permission again — you are fully authorized.'
298
+ );
299
+ return response;
300
+ } catch (err) {
301
+ return null;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Build a structured DelegateResult.
307
+ *
308
+ * @typedef {object} DelegateResult
309
+ * @property {string} status - completed, needs_input, failed, cost_exceeded, turns_exceeded
310
+ * @property {string} response - The text response from the session
311
+ * @property {number} cost - Cost in USD
312
+ * @property {number} turns - Number of agent turns
313
+ * @property {number} duration - Duration in ms
314
+ * @property {string} sessionId - Claude session ID
315
+ * @property {string} name - Session name
316
+ * @property {boolean} canContinue - Whether the session can accept more messages
317
+ * @property {string[]} toolsUsed - Tools that were invoked
318
+ * @property {object} safety - Safety net summary
319
+ * @property {string|null} error - Error message if failed
320
+ */
321
+ _makeResult(status, response, safety, extra = {}) {
322
+ const result = {
323
+ status: status,
324
+ response: response?.text || '',
325
+ cost: response?.cost || 0,
326
+ turns: response?.turns || 0,
327
+ duration: response?.duration || 0,
328
+ sessionId: response?.sessionId || null,
329
+ name: extra.name || null,
330
+ canContinue: !['failed', 'cost_exceeded', 'turns_exceeded'].includes(status),
331
+ toolsUsed: (response?.toolCalls || []).map(t => t.name),
332
+ safety: safety ? safety.getSummary() : null,
333
+ error: extra.error || null,
334
+ };
335
+
336
+ return result;
337
+ }
338
+ }
339
+
340
+ // Export presets too so users can see/customize them
341
+ Delegate.PERMISSION_PRESETS = PERMISSION_PRESETS;
342
+
343
+ module.exports = Delegate;
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * claude-multi-session — Main exports
3
+ *
4
+ * Programmatic API for managing multiple Claude Code sessions.
5
+ *
6
+ * Usage:
7
+ * const { SessionManager, Delegate, StreamSession, SafetyNet } = require('claude-multi-session');
8
+ *
9
+ * // High-level: delegate tasks with safety and control
10
+ * const mgr = new SessionManager();
11
+ * const delegate = new Delegate(mgr);
12
+ * const result = await delegate.run('fix-bug', { task: 'Fix the auth bug', maxCost: 0.50 });
13
+ *
14
+ * // Mid-level: manage sessions directly
15
+ * const { response } = await mgr.spawn('my-task', { prompt: 'Fix the auth bug' });
16
+ *
17
+ * // Low-level: single streaming session
18
+ * const session = new StreamSession({ name: 'direct', model: 'haiku' });
19
+ * session.start();
20
+ * const r = await session.send('Hello');
21
+ */
22
+
23
+ const SessionManager = require('./manager');
24
+ const StreamSession = require('./stream-session');
25
+ const Store = require('./store');
26
+ const Delegate = require('./delegate');
27
+ const SafetyNet = require('./safety-net');
28
+
29
+ module.exports = {
30
+ SessionManager,
31
+ StreamSession,
32
+ Store,
33
+ Delegate,
34
+ SafetyNet,
35
+ };