claudehq 1.0.2 → 1.0.5
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/lib/core/claude-events.js +2 -1
- package/lib/core/config.js +39 -1
- package/lib/data/orchestration.js +941 -0
- package/lib/index.js +211 -23
- package/lib/orchestration/executor.js +635 -0
- package/lib/routes/orchestration.js +417 -0
- package/lib/routes/spawner.js +335 -0
- package/lib/sessions/manager.js +36 -9
- package/lib/spawner/index.js +51 -0
- package/lib/spawner/path-validator.js +366 -0
- package/lib/spawner/projects-manager.js +421 -0
- package/lib/spawner/session-spawner.js +1010 -0
- package/package.json +1 -1
- package/public/index.html +399 -18
- package/lib/server.js +0 -9364
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawner API Routes - HTTP route handlers for session spawning
|
|
3
|
+
*
|
|
4
|
+
* Provides REST API endpoints for:
|
|
5
|
+
* - Spawning new Claude Code sessions
|
|
6
|
+
* - Managing existing sessions
|
|
7
|
+
* - Projects autocomplete and tracking
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create spawner route handlers
|
|
12
|
+
* @param {Object} deps - Dependencies
|
|
13
|
+
* @param {Object} deps.spawner - Session spawner instance
|
|
14
|
+
* @returns {Object} Route handlers
|
|
15
|
+
*/
|
|
16
|
+
function createSpawnerRoutes(deps) {
|
|
17
|
+
const { spawner } = deps;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
// =========================================================================
|
|
21
|
+
// Session Spawning
|
|
22
|
+
// =========================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* POST /api/spawner/sessions - Spawn a new session
|
|
26
|
+
*/
|
|
27
|
+
handleSpawnSession(req, res) {
|
|
28
|
+
let body = '';
|
|
29
|
+
req.on('data', chunk => body += chunk);
|
|
30
|
+
req.on('end', async () => {
|
|
31
|
+
try {
|
|
32
|
+
const {
|
|
33
|
+
name,
|
|
34
|
+
cwd,
|
|
35
|
+
model,
|
|
36
|
+
skipPermissions,
|
|
37
|
+
continue: continueConversation,
|
|
38
|
+
initialPrompt,
|
|
39
|
+
metadata
|
|
40
|
+
} = JSON.parse(body);
|
|
41
|
+
|
|
42
|
+
const result = await spawner.spawnSession({
|
|
43
|
+
name,
|
|
44
|
+
cwd,
|
|
45
|
+
model,
|
|
46
|
+
skipPermissions,
|
|
47
|
+
continue: continueConversation,
|
|
48
|
+
initialPrompt,
|
|
49
|
+
metadata
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result.error) {
|
|
53
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
54
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
55
|
+
} else {
|
|
56
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify({ ok: true, session: result.session }));
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* GET /api/spawner/sessions - List all sessions
|
|
68
|
+
*/
|
|
69
|
+
handleListSessions(req, res) {
|
|
70
|
+
const sessions = spawner.listSessions();
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
72
|
+
res.end(JSON.stringify({ ok: true, sessions }));
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GET /api/spawner/sessions/:id - Get a specific session
|
|
77
|
+
*/
|
|
78
|
+
handleGetSession(req, res, sessionId) {
|
|
79
|
+
const session = spawner.getSession(sessionId);
|
|
80
|
+
if (session) {
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify({ ok: true, session }));
|
|
83
|
+
} else {
|
|
84
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
85
|
+
res.end(JSON.stringify({ ok: false, error: 'Session not found' }));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* PATCH /api/spawner/sessions/:id - Update a session
|
|
91
|
+
*/
|
|
92
|
+
handleUpdateSession(req, res, sessionId) {
|
|
93
|
+
let body = '';
|
|
94
|
+
req.on('data', chunk => body += chunk);
|
|
95
|
+
req.on('end', () => {
|
|
96
|
+
try {
|
|
97
|
+
const updates = JSON.parse(body);
|
|
98
|
+
const result = spawner.updateSession(sessionId, updates);
|
|
99
|
+
|
|
100
|
+
if (result.error) {
|
|
101
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
103
|
+
} else {
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
105
|
+
res.end(JSON.stringify({ ok: true, session: result.session }));
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
109
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* DELETE /api/spawner/sessions/:id - Kill and remove a session
|
|
116
|
+
*/
|
|
117
|
+
handleKillSession(req, res, sessionId) {
|
|
118
|
+
spawner.killSession(sessionId).then((result) => {
|
|
119
|
+
if (result.error) {
|
|
120
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
122
|
+
} else {
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end(JSON.stringify({ ok: true }));
|
|
125
|
+
}
|
|
126
|
+
}).catch((e) => {
|
|
127
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
128
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* POST /api/spawner/sessions/:id/restart - Restart a session
|
|
134
|
+
*/
|
|
135
|
+
handleRestartSession(req, res, sessionId) {
|
|
136
|
+
spawner.restartSession(sessionId).then((result) => {
|
|
137
|
+
if (result.error) {
|
|
138
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
139
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
140
|
+
} else {
|
|
141
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
142
|
+
res.end(JSON.stringify({ ok: true, session: result.session }));
|
|
143
|
+
}
|
|
144
|
+
}).catch((e) => {
|
|
145
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* POST /api/spawner/sessions/:id/prompt - Send a prompt to a session
|
|
152
|
+
*/
|
|
153
|
+
handleSendPrompt(req, res, sessionId) {
|
|
154
|
+
let body = '';
|
|
155
|
+
req.on('data', chunk => body += chunk);
|
|
156
|
+
req.on('end', async () => {
|
|
157
|
+
try {
|
|
158
|
+
const { prompt } = JSON.parse(body);
|
|
159
|
+
if (!prompt) {
|
|
160
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
161
|
+
res.end(JSON.stringify({ ok: false, error: 'Prompt is required' }));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await spawner.sendPrompt(sessionId, prompt);
|
|
166
|
+
if (result.error) {
|
|
167
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
168
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
169
|
+
} else {
|
|
170
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
171
|
+
res.end(JSON.stringify({ ok: true }));
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
175
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* POST /api/spawner/sessions/:id/cancel - Cancel current operation
|
|
182
|
+
*/
|
|
183
|
+
handleCancelSession(req, res, sessionId) {
|
|
184
|
+
spawner.cancelSession(sessionId).then((result) => {
|
|
185
|
+
if (result.error) {
|
|
186
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
188
|
+
} else {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ ok: true }));
|
|
191
|
+
}
|
|
192
|
+
}).catch((e) => {
|
|
193
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* POST /api/spawner/sessions/:id/permission - Respond to permission prompt
|
|
200
|
+
*/
|
|
201
|
+
handlePermissionResponse(req, res, sessionId) {
|
|
202
|
+
let body = '';
|
|
203
|
+
req.on('data', chunk => body += chunk);
|
|
204
|
+
req.on('end', async () => {
|
|
205
|
+
try {
|
|
206
|
+
const { response } = JSON.parse(body);
|
|
207
|
+
if (!response) {
|
|
208
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
209
|
+
res.end(JSON.stringify({ ok: false, error: 'Response is required' }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await spawner.respondToPermission(sessionId, response);
|
|
214
|
+
if (result.error) {
|
|
215
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
216
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
217
|
+
} else {
|
|
218
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
219
|
+
res.end(JSON.stringify({ ok: true }));
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
223
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// =========================================================================
|
|
229
|
+
// Projects
|
|
230
|
+
// =========================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* GET /api/spawner/projects - List tracked projects
|
|
234
|
+
*/
|
|
235
|
+
handleListProjects(req, res, url) {
|
|
236
|
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
237
|
+
const sortBy = url.searchParams.get('sortBy') || 'lastUsed';
|
|
238
|
+
|
|
239
|
+
const projects = spawner.projectsManager.listProjects({ limit, sortBy });
|
|
240
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
241
|
+
res.end(JSON.stringify({ ok: true, projects }));
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* GET /api/spawner/projects/autocomplete - Get autocomplete suggestions
|
|
246
|
+
*/
|
|
247
|
+
handleAutocomplete(req, res, url) {
|
|
248
|
+
const query = url.searchParams.get('q') || '';
|
|
249
|
+
const limit = parseInt(url.searchParams.get('limit') || '15');
|
|
250
|
+
|
|
251
|
+
const suggestions = spawner.projectsManager.autocomplete(query, { limit });
|
|
252
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
253
|
+
res.end(JSON.stringify({ ok: true, suggestions }));
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* POST /api/spawner/projects - Track a new project
|
|
258
|
+
*/
|
|
259
|
+
handleTrackProject(req, res) {
|
|
260
|
+
let body = '';
|
|
261
|
+
req.on('data', chunk => body += chunk);
|
|
262
|
+
req.on('end', () => {
|
|
263
|
+
try {
|
|
264
|
+
const { path, name } = JSON.parse(body);
|
|
265
|
+
if (!path) {
|
|
266
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
267
|
+
res.end(JSON.stringify({ ok: false, error: 'Path is required' }));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result = spawner.projectsManager.trackProject(path, name);
|
|
272
|
+
if (result.error) {
|
|
273
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
274
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
275
|
+
} else {
|
|
276
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
277
|
+
res.end(JSON.stringify({ ok: true, project: result.project }));
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
281
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* DELETE /api/spawner/projects - Remove a tracked project
|
|
288
|
+
*/
|
|
289
|
+
handleRemoveProject(req, res) {
|
|
290
|
+
let body = '';
|
|
291
|
+
req.on('data', chunk => body += chunk);
|
|
292
|
+
req.on('end', () => {
|
|
293
|
+
try {
|
|
294
|
+
const { path } = JSON.parse(body);
|
|
295
|
+
if (!path) {
|
|
296
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
297
|
+
res.end(JSON.stringify({ ok: false, error: 'Path is required' }));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = spawner.projectsManager.removeProject(path);
|
|
302
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
303
|
+
res.end(JSON.stringify({ ok: true, removed: result.success }));
|
|
304
|
+
} catch (e) {
|
|
305
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* GET /api/spawner/projects/common - Get common directories
|
|
313
|
+
*/
|
|
314
|
+
handleGetCommonDirectories(req, res) {
|
|
315
|
+
const directories = spawner.projectsManager.getCommonDirectories();
|
|
316
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
317
|
+
res.end(JSON.stringify({ ok: true, directories }));
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// =========================================================================
|
|
321
|
+
// Health
|
|
322
|
+
// =========================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* POST /api/spawner/refresh - Force health check
|
|
326
|
+
*/
|
|
327
|
+
handleRefresh(req, res) {
|
|
328
|
+
spawner.checkHealth();
|
|
329
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
330
|
+
res.end(JSON.stringify({ ok: true, sessions: spawner.listSessions() }));
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { createSpawnerRoutes };
|
package/lib/sessions/manager.js
CHANGED
|
@@ -290,20 +290,24 @@ function linkClaudeSession(claudeSessionId, managedSessionId) {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
function discoverSession(event) {
|
|
293
|
+
function discoverSession(event, options = {}) {
|
|
294
294
|
const id = crypto.randomUUID();
|
|
295
295
|
const projectName = event.cwd ? path.basename(event.cwd) : 'Unknown Project';
|
|
296
296
|
|
|
297
|
+
// During initialization, set status to OFFLINE since we don't know if session is active
|
|
298
|
+
// During real-time discovery, set to WORKING since we're receiving live events
|
|
299
|
+
const initialStatus = options.isInitialization ? SESSION_STATUS.OFFLINE : SESSION_STATUS.WORKING;
|
|
300
|
+
|
|
297
301
|
const session = {
|
|
298
302
|
id,
|
|
299
303
|
name: projectName,
|
|
300
304
|
tmuxSession: null,
|
|
301
|
-
status:
|
|
305
|
+
status: initialStatus,
|
|
302
306
|
claudeSessionId: event.sessionId,
|
|
303
307
|
createdAt: Date.now(),
|
|
304
308
|
lastActivity: Date.now(),
|
|
305
309
|
cwd: event.cwd || null,
|
|
306
|
-
currentTool: event.tool || null,
|
|
310
|
+
currentTool: options.isInitialization ? null : (event.tool || null),
|
|
307
311
|
discovered: true
|
|
308
312
|
};
|
|
309
313
|
|
|
@@ -318,22 +322,36 @@ function discoverSession(event) {
|
|
|
318
322
|
return session;
|
|
319
323
|
}
|
|
320
324
|
|
|
321
|
-
function updateSessionFromEvent(event) {
|
|
325
|
+
function updateSessionFromEvent(event, options = {}) {
|
|
322
326
|
if (!event.sessionId) return;
|
|
323
327
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
+
// Skip auto-unhide during initialization (loading historical events)
|
|
329
|
+
// Only auto-unhide on new real-time events
|
|
330
|
+
if (!options.skipAutoUnhide) {
|
|
331
|
+
const hiddenIds = loadHiddenSessions();
|
|
332
|
+
if (hiddenIds.includes(event.sessionId)) {
|
|
333
|
+
console.log(` Auto-unhiding session ${event.sessionId.substring(0, 8)}... (received activity)`);
|
|
334
|
+
unhideSession(event.sessionId);
|
|
335
|
+
}
|
|
328
336
|
}
|
|
329
337
|
|
|
330
338
|
let managedSession = findManagedSessionByClaudeId(event.sessionId);
|
|
331
339
|
|
|
332
340
|
if (!managedSession) {
|
|
333
|
-
managedSession = discoverSession(event);
|
|
341
|
+
managedSession = discoverSession(event, { isInitialization: options.skipAutoUnhide });
|
|
334
342
|
}
|
|
335
343
|
|
|
336
344
|
const prevStatus = managedSession.status;
|
|
345
|
+
|
|
346
|
+
// During initialization, only update metadata (cwd), not status
|
|
347
|
+
// Status updates from historical events would show stale "working" state
|
|
348
|
+
if (options.skipAutoUnhide) {
|
|
349
|
+
// This is initialization - only update cwd if provided
|
|
350
|
+
if (event.cwd) managedSession.cwd = event.cwd;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Real-time event - update everything
|
|
337
355
|
managedSession.lastActivity = Date.now();
|
|
338
356
|
if (event.cwd) managedSession.cwd = event.cwd;
|
|
339
357
|
|
|
@@ -466,6 +484,15 @@ function updateManagedSession(id, updates) {
|
|
|
466
484
|
|
|
467
485
|
if (updates.name) {
|
|
468
486
|
session.name = updates.name;
|
|
487
|
+
|
|
488
|
+
// Also update customNames to ensure the new name takes effect
|
|
489
|
+
// (customNames overrides session.name in getManagedSessions)
|
|
490
|
+
const customNames = loadCustomNames();
|
|
491
|
+
customNames[id] = updates.name;
|
|
492
|
+
if (session.claudeSessionId) {
|
|
493
|
+
customNames[session.claudeSessionId] = updates.name;
|
|
494
|
+
}
|
|
495
|
+
saveCustomNames(customNames);
|
|
469
496
|
}
|
|
470
497
|
|
|
471
498
|
broadcastManagedSessions();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawner Module - Session spawning and management for Claude Code
|
|
3
|
+
*
|
|
4
|
+
* This module provides the ability to spawn and manage Claude Code sessions
|
|
5
|
+
* from the dashboard without needing to open a terminal.
|
|
6
|
+
*
|
|
7
|
+
* Main exports:
|
|
8
|
+
* - createSessionSpawner: Create a session spawner instance
|
|
9
|
+
* - createProjectsManager: Create a projects manager for directory tracking
|
|
10
|
+
* - Path validation utilities
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createSessionSpawner, SESSION_STATUS, CLAUDE_MODELS, DEFAULT_CONFIG } = require('./session-spawner');
|
|
14
|
+
const { createProjectsManager, MAX_TRACKED_PROJECTS, MAX_AUTOCOMPLETE_RESULTS } = require('./projects-manager');
|
|
15
|
+
const {
|
|
16
|
+
validateDirectoryPath,
|
|
17
|
+
validateFilePath,
|
|
18
|
+
validateTmuxSessionName,
|
|
19
|
+
isValidTmuxSessionName,
|
|
20
|
+
containsShellMetacharacters,
|
|
21
|
+
isPathTraversalAttempt,
|
|
22
|
+
isPathWithin,
|
|
23
|
+
normalizePath,
|
|
24
|
+
sanitizeForFilename,
|
|
25
|
+
MAX_PATH_LENGTH
|
|
26
|
+
} = require('./path-validator');
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
// Main factory functions
|
|
30
|
+
createSessionSpawner,
|
|
31
|
+
createProjectsManager,
|
|
32
|
+
|
|
33
|
+
// Path validation
|
|
34
|
+
validateDirectoryPath,
|
|
35
|
+
validateFilePath,
|
|
36
|
+
validateTmuxSessionName,
|
|
37
|
+
isValidTmuxSessionName,
|
|
38
|
+
containsShellMetacharacters,
|
|
39
|
+
isPathTraversalAttempt,
|
|
40
|
+
isPathWithin,
|
|
41
|
+
normalizePath,
|
|
42
|
+
sanitizeForFilename,
|
|
43
|
+
|
|
44
|
+
// Constants
|
|
45
|
+
SESSION_STATUS,
|
|
46
|
+
CLAUDE_MODELS,
|
|
47
|
+
DEFAULT_CONFIG,
|
|
48
|
+
MAX_PATH_LENGTH,
|
|
49
|
+
MAX_TRACKED_PROJECTS,
|
|
50
|
+
MAX_AUTOCOMPLETE_RESULTS
|
|
51
|
+
};
|