circuschief 0.2.1 → 0.4.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/package.json +1 -1
- package/packages/server/src/api/git.js +15 -0
- package/packages/server/src/api/index.js +13 -2
- package/packages/server/src/api/projects-commandButtons.js +96 -0
- package/packages/server/src/api/projects-session-helpers.js +51 -33
- package/packages/server/src/api/projects.js +109 -113
- package/packages/server/src/api/sessions-archive.js +2 -1
- package/packages/server/src/api/sessions-lifecycle.js +2 -1
- package/packages/server/src/api/sessions-patch.js +2 -0
- package/packages/server/src/config.js +6 -0
- package/packages/server/src/database.js +1 -0
- package/packages/server/src/db/DatabaseManager.js +12 -1
- package/packages/server/src/db/ProjectRepository.js +22 -5
- package/packages/server/src/db/index.js +4 -0
- package/packages/server/src/db/migrations/index.js +7 -0
- package/packages/server/src/db/migrations/miscMigrations.js +78 -1
- package/packages/server/src/db/migrations/projectsMigrations.js +4 -0
- package/packages/server/src/index.js +10 -4
- package/packages/server/src/services/gitService.js +42 -6
- package/packages/server/src/services/gitSessionSetup.js +4 -3
- package/packages/server/src/services/kanbanTriggers.js +1 -0
- package/packages/server/src/services/schedulerService.js +32 -0
- package/packages/server/src/services/sessionExecution.js +2 -2
- package/packages/server/src/services/templateTriggerService.js +1 -0
- package/packages/shared/src/contracts/projects.js +4 -1
- package/packages/shared/src/types.js +4 -3
- package/packages/web/dist/assets/ActiveSessionsView-BVco8bPU.css +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-D1daFFvI.js +1 -0
- package/packages/web/dist/assets/{AgentLogsView-D4l0N9ZA.js → AgentLogsView-B_NDIx2_.js} +1 -1
- package/packages/web/dist/assets/{ApiClient-Dbs1H78V.js → ApiClient-CcqJ-GAv.js} +1 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-BHqbCCX2.js +1 -0
- package/packages/web/dist/assets/{ArchiveConfirmModal-CQZeuYBz.css → ArchiveConfirmModal-BQ-4gI0R.css} +1 -1
- package/packages/web/dist/assets/CommandButtonDetailView-B9crZey8.js +1 -0
- package/packages/web/dist/assets/EffortLevelSelector-HuOPegKI.js +1 -0
- package/packages/web/dist/assets/{GeneralSettingsView-BqCzCX-z.js → GeneralSettingsView-CzBagrDs.js} +1 -1
- package/packages/web/dist/assets/{PathChooser-CXFxb8Oj.js → InputWithButton-Cplkm8Ze.js} +1 -1
- package/packages/web/dist/assets/InputWithButton-cYdrEmTs.css +1 -0
- package/packages/web/dist/assets/InterpolationHelp-bG_y10VY.js +1 -0
- package/packages/web/dist/assets/MarkdownEditor-CXDVTLvp.js +2 -0
- package/packages/web/dist/assets/{ModelSelector-DSxaZWBL.js → ModelSelector-CgpqdZtV.js} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BsI7JtO9.js → NewSessionView-91V-4qLi.js} +2 -2
- package/packages/web/dist/assets/{NewSessionView-Byoi1XdQ.css → NewSessionView-D_Hi7M9g.css} +1 -1
- package/packages/web/dist/assets/ProjectEditView-C3bOYnD6.js +1 -0
- package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +1 -0
- package/packages/web/dist/assets/ProjectListView-BEo1p4dO.js +1 -0
- package/packages/web/dist/assets/ProjectListView-CumumZN-.css +1 -0
- package/packages/web/dist/assets/ProjectNewView-CXGjy3OL.js +1 -0
- package/packages/web/dist/assets/ProjectNewView-CaLXfFzd.css +1 -0
- package/packages/web/dist/assets/{ProvidersView-CgAr0qms.js → ProvidersView-CMAnPTEX.js} +1 -1
- package/packages/web/dist/assets/{ProvidersView-B_QQF3RM.css → ProvidersView-uD8SKWpA.css} +1 -1
- package/packages/web/dist/assets/{QuickResponseSettings-uDDpwaza.js → QuickResponseSettings-Ds4X-J-t.js} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-D0qs0Fm_.js → QuickResponsesPanel-BY2v23c5.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-_kHi1Mg3.js → ResizableTextarea-C2s6_7X9.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-Be1-bK0C.js → SessionCard-C7vFzR16.js} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-mnGRMaLY.css → SessionDetailView-BL83oPiI.css} +1 -1
- package/packages/web/dist/assets/SessionDetailView-D3f0FEV1.js +36 -0
- package/packages/web/dist/assets/{SessionFormOptions-DvhOyP6z.js → SessionFormOptions-3LfqiLiR.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-CuHsWj85.js → SessionListView-NGW-u434.js} +1 -1
- package/packages/web/dist/assets/{SessionLogStream-Da_GniUZ.js → SessionLogStream-NR-AS676.js} +6 -6
- package/packages/web/dist/assets/SettingsView-DKINDb2z.js +1 -0
- package/packages/web/dist/assets/{SlashCommandWizard-B_8ifpxN.js → SlashCommandWizard-PXipO1yA.js} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-KvgSGHdd.js → SummarySettingsView-D_2bSsYD.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-BhOjYIvS.js → TemplateDetailView-C5rbgXwU.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-B4OYZP0J.js → commandButtons-D_-wR8zJ.js} +1 -1
- package/packages/web/dist/assets/index-BCazaXF8.js +1 -0
- package/packages/web/dist/assets/index-BmQRt229.js +3 -0
- package/packages/web/dist/assets/index-CCQGqJXX.js +1 -0
- package/packages/web/dist/assets/index-CcCiJkwz.js +1 -0
- package/packages/web/dist/assets/index-Cs-001Bx.js +1 -0
- package/packages/web/dist/assets/index-CxiSnR0R.js +1 -0
- package/packages/web/dist/assets/index-D-lQSDZh.js +1 -0
- package/packages/web/dist/assets/{index-CSOPrlmq.js → index-D1Lg5reX.js} +3 -3
- package/packages/web/dist/assets/index-D9VgH58U.js +1 -0
- package/packages/web/dist/assets/index-D9Z6zsGS.js +1 -0
- package/packages/web/dist/assets/index-DP2i58hO.js +1 -0
- package/packages/web/dist/assets/index-DaL3nu0U.js +1 -0
- package/packages/web/dist/assets/index-Dfy1JGZs.js +1 -0
- package/packages/web/dist/assets/index-DgIOe3cM.js +1 -0
- package/packages/web/dist/assets/index-Dp3Eg3c0.js +1 -0
- package/packages/web/dist/assets/{index-BHVnr8MO.js → index-DveLfEiG.js} +2 -2
- package/packages/web/dist/assets/index-DyQ22-ut.js +1 -0
- package/packages/web/dist/assets/{index-BqVgX_Jy.js → index-Krfrs3sc.js} +3 -3
- package/packages/web/dist/assets/index-gylMFbgn.js +7 -0
- package/packages/web/dist/assets/{projects-B2du-GX8.js → projects-CMJJca64.js} +1 -1
- package/packages/web/dist/assets/{providers-B__J6FX0.js → providers-BCtNZWYw.js} +1 -1
- package/packages/web/dist/assets/{sessions-VDrd87yA.js → sessions-CoRS-wuR.js} +1 -1
- package/packages/web/dist/assets/{settings-CZ7Pc-Pt.js → settings-BN_W4nwV.js} +1 -1
- package/packages/web/dist/assets/useSummaryHelpers-GVg7sMWF.js +1 -0
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/ActiveSessionsView-3697sD8N.js +0 -1
- package/packages/web/dist/assets/ActiveSessionsView-DfYXc6dz.css +0 -1
- package/packages/web/dist/assets/ArchiveConfirmModal-Bv3vGOMM.js +0 -1
- package/packages/web/dist/assets/CommandButtonDetailView-Bk_SHxpu.js +0 -1
- package/packages/web/dist/assets/EffortLevelSelector-VfBEelvO.js +0 -1
- package/packages/web/dist/assets/InterpolationHelp-Dc1Y0T6v.js +0 -1
- package/packages/web/dist/assets/MarkdownEditor-DwBQkZbs.js +0 -2
- package/packages/web/dist/assets/PathChooser-BoMGzeg2.css +0 -1
- package/packages/web/dist/assets/ProjectEditView-Bes4Mib4.js +0 -1
- package/packages/web/dist/assets/ProjectEditView-DNwBUNRk.css +0 -1
- package/packages/web/dist/assets/ProjectListView-C55H1JHQ.css +0 -1
- package/packages/web/dist/assets/ProjectListView-DzEu-C36.js +0 -1
- package/packages/web/dist/assets/ProjectNewView-CpgE4R-l.css +0 -1
- package/packages/web/dist/assets/ProjectNewView-Cv-iEAgl.js +0 -1
- package/packages/web/dist/assets/SessionDetailView-DUYb7qTA.js +0 -36
- package/packages/web/dist/assets/SettingsView-5RDCXNUa.js +0 -1
- package/packages/web/dist/assets/index-80Qu7W6P.js +0 -1
- package/packages/web/dist/assets/index-B8_Iqwcq.js +0 -1
- package/packages/web/dist/assets/index-B9JErft2.js +0 -1
- package/packages/web/dist/assets/index-BarVnQIj.js +0 -3
- package/packages/web/dist/assets/index-BsvRdU0B.js +0 -1
- package/packages/web/dist/assets/index-Bugg2M-E.js +0 -1
- package/packages/web/dist/assets/index-C2Pjy-M8.js +0 -1
- package/packages/web/dist/assets/index-CS_wb_Vj.js +0 -1
- package/packages/web/dist/assets/index-ClzNIdCp.js +0 -1
- package/packages/web/dist/assets/index-Cn9Ajkye.js +0 -1
- package/packages/web/dist/assets/index-CucpVX4L.js +0 -1
- package/packages/web/dist/assets/index-D9hZYvW3.js +0 -1
- package/packages/web/dist/assets/index-DA0dK_PG.js +0 -7
- package/packages/web/dist/assets/index-DgpSn-jR.js +0 -1
- package/packages/web/dist/assets/index-HZwIyC9t.js +0 -1
- package/packages/web/dist/assets/index-SS3wA2sI.js +0 -1
package/package.json
CHANGED
|
@@ -4,6 +4,21 @@ import * as gitService from '../services/gitService.js';
|
|
|
4
4
|
|
|
5
5
|
const router = Router();
|
|
6
6
|
|
|
7
|
+
// GET /api/git/detect-worktree-path?directory=/path/to/repo
|
|
8
|
+
router.get('/detect-worktree-path', async (req, res) => {
|
|
9
|
+
const { directory } = req.query;
|
|
10
|
+
if (!directory) {
|
|
11
|
+
return res.status(400).json({ error: 'directory query parameter is required' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await gitService.detectWorktreePath(directory);
|
|
16
|
+
res.json(result);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
res.status(500).json({ error: error.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
7
22
|
// GET /api/projects/:id/git/status - Check if git repo, get branches
|
|
8
23
|
router.get('/projects/:id/status', async (req, res) => {
|
|
9
24
|
const project = projects.getById(req.params.id);
|
|
@@ -11,13 +11,24 @@ import providersRouter from './providers.js';
|
|
|
11
11
|
import commandsRouter from './commands.js';
|
|
12
12
|
import metricsRouter from './metrics.js';
|
|
13
13
|
import kanbanRouter from './kanban.js';
|
|
14
|
+
import { getDbPath } from '../database.js';
|
|
15
|
+
import { schedulerService } from '../services/schedulerService.js';
|
|
14
16
|
|
|
15
17
|
const router = Router();
|
|
16
18
|
|
|
17
19
|
// Lightweight identity endpoint — lets tools (e.g. pw.sh) verify which
|
|
18
|
-
// worktree / working directory this server instance belongs to
|
|
20
|
+
// worktree / working directory this server instance belongs to, which
|
|
21
|
+
// DB file it is using, and whether VCR / scheduler are in the expected
|
|
22
|
+
// state. This endpoint is additive-safe: consumers must ignore unknown
|
|
23
|
+
// fields so new ones can be added freely.
|
|
19
24
|
router.get('/server-info', (_req, res) => {
|
|
20
|
-
|
|
25
|
+
const vcr = process.env.VCR_MODE;
|
|
26
|
+
res.json({
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
dbPath: getDbPath(),
|
|
29
|
+
vcrMode: vcr && vcr.length > 0 ? vcr : null,
|
|
30
|
+
schedulerRunning: schedulerService.isRunning(),
|
|
31
|
+
});
|
|
21
32
|
});
|
|
22
33
|
|
|
23
34
|
router.use('/projects', projectsRouter);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { projects, commandButtons } from '../database.js';
|
|
3
|
+
import { CreateCommandButtonRequest, UpdateCommandButtonRequest } from '../../../shared/src/contracts/commandButtons.js';
|
|
4
|
+
|
|
5
|
+
// Error message constants
|
|
6
|
+
const ERR_PROJECT_NOT_FOUND = 'Project not found';
|
|
7
|
+
const ERR_BUTTON_NOT_FOUND = 'Command button not found';
|
|
8
|
+
|
|
9
|
+
const router = Router({ mergeParams: true });
|
|
10
|
+
|
|
11
|
+
// GET /api/projects/:id/command-buttons - List all command buttons for project
|
|
12
|
+
router.get('/', (req, res) => {
|
|
13
|
+
const project = projects.getById(req.params.id);
|
|
14
|
+
if (!project) {
|
|
15
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const buttons = commandButtons.getByProjectId(req.params.id);
|
|
19
|
+
res.json(buttons);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// POST /api/projects/:id/command-buttons - Create new command button
|
|
23
|
+
router.post('/', (req, res) => {
|
|
24
|
+
const project = projects.getById(req.params.id);
|
|
25
|
+
if (!project) {
|
|
26
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = CreateCommandButtonRequest.safeParse(req.body);
|
|
30
|
+
if (!result.success) {
|
|
31
|
+
return res.status(400).json({ error: result.error.issues[0].message });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const button = commandButtons.create({
|
|
35
|
+
projectId: req.params.id,
|
|
36
|
+
label: result.data.label,
|
|
37
|
+
command: result.data.command,
|
|
38
|
+
sortOrder: result.data.sortOrder,
|
|
39
|
+
showOnList: result.data.showOnList,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
res.status(201).json(button);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// GET /api/projects/:id/command-buttons/:buttonId - Get single button
|
|
46
|
+
router.get('/:buttonId', (req, res) => {
|
|
47
|
+
const project = projects.getById(req.params.id);
|
|
48
|
+
if (!project) {
|
|
49
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const button = commandButtons.getById(req.params.buttonId);
|
|
53
|
+
if (!button) {
|
|
54
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
55
|
+
}
|
|
56
|
+
res.json(button);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// PATCH /api/projects/:id/command-buttons/:buttonId - Update button
|
|
60
|
+
router.patch('/:buttonId', (req, res) => {
|
|
61
|
+
const project = projects.getById(req.params.id);
|
|
62
|
+
if (!project) {
|
|
63
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const button = commandButtons.getById(req.params.buttonId);
|
|
67
|
+
if (!button) {
|
|
68
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = UpdateCommandButtonRequest.safeParse(req.body);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
return res.status(400).json({ error: result.error.issues[0].message });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const updated = commandButtons.update(req.params.buttonId, result.data);
|
|
77
|
+
res.json(updated);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// DELETE /api/projects/:id/command-buttons/:buttonId - Delete button
|
|
81
|
+
router.delete('/:buttonId', (req, res) => {
|
|
82
|
+
const project = projects.getById(req.params.id);
|
|
83
|
+
if (!project) {
|
|
84
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const button = commandButtons.getById(req.params.buttonId);
|
|
88
|
+
if (!button) {
|
|
89
|
+
return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
commandButtons.delete(req.params.buttonId);
|
|
93
|
+
res.status(204).send();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export default router;
|
|
@@ -220,6 +220,52 @@ export function buildSchedulingUpdate(config, initialStatus) {
|
|
|
220
220
|
return update;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Resolve working directory and optional worktree for a new session.
|
|
225
|
+
* Inherits the parent's worktree when applicable.
|
|
226
|
+
* @param {object} params
|
|
227
|
+
* @returns {Promise<{ workingDirectory: string, gitWorktree: string|null }>}
|
|
228
|
+
*/
|
|
229
|
+
async function resolveSessionWorkingDirectory({ session, config, project }) {
|
|
230
|
+
const parentSession = config.parentSessionId ? sessions.getById(config.parentSessionId) : null;
|
|
231
|
+
if (parentSession?.gitWorktree) {
|
|
232
|
+
return {
|
|
233
|
+
workingDirectory: parentSession.gitWorktree,
|
|
234
|
+
gitWorktree: parentSession.gitWorktree,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const gitSetup = await setupGitForSession({
|
|
239
|
+
projectDir: project.workingDirectory,
|
|
240
|
+
gitMode: config.gitMode || null,
|
|
241
|
+
gitBranch: config.gitBranch || null,
|
|
242
|
+
sessionId: session.id,
|
|
243
|
+
worktreeBasePath: project.worktreePath || null,
|
|
244
|
+
});
|
|
245
|
+
return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Start a session immediately via sessionManager, recording failures on the session.
|
|
250
|
+
*/
|
|
251
|
+
async function startSessionImmediately({ session, config, project, workingDirectory, sessionAttachments }) {
|
|
252
|
+
const resolved = await slashCommandService.resolvePromptSkillOrCommand(
|
|
253
|
+
workingDirectory, config.prompt, project.systemPrompt
|
|
254
|
+
);
|
|
255
|
+
const finalPrompt = resolved ? resolved.userMessage : config.prompt;
|
|
256
|
+
const finalSystemPrompt = resolved ? resolved.systemPrompt : project.systemPrompt;
|
|
257
|
+
|
|
258
|
+
const { runSession } = await import('../services/sessionManager.js');
|
|
259
|
+
runSession(session.id, finalPrompt, workingDirectory, {
|
|
260
|
+
systemPrompt: finalSystemPrompt,
|
|
261
|
+
fileAttachments: sessionAttachments,
|
|
262
|
+
model: config.model,
|
|
263
|
+
}).catch((error) => {
|
|
264
|
+
console.error('Session error:', error);
|
|
265
|
+
sessions.update(session.id, { status: 'error', error: error.message });
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
223
269
|
/**
|
|
224
270
|
* Handle git setup, session start, broadcasts, and hooks after session creation.
|
|
225
271
|
* @param {object} params
|
|
@@ -231,25 +277,9 @@ export function buildSchedulingUpdate(config, initialStatus) {
|
|
|
231
277
|
* @returns {Promise<{ updatedSession: object }>}
|
|
232
278
|
*/
|
|
233
279
|
export async function setupAndStartSession({ session, config, project, projectId, files }) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// If this is a child session and the parent has a worktree, inherit it
|
|
238
|
-
// (mirrors templateTriggerService behavior)
|
|
239
|
-
const parentSession = config.parentSessionId ? sessions.getById(config.parentSessionId) : null;
|
|
240
|
-
if (parentSession?.gitWorktree) {
|
|
241
|
-
workingDirectory = parentSession.gitWorktree;
|
|
242
|
-
gitWorktree = parentSession.gitWorktree;
|
|
243
|
-
} else {
|
|
244
|
-
const gitSetup = await setupGitForSession({
|
|
245
|
-
projectDir: project.workingDirectory,
|
|
246
|
-
gitMode: config.gitMode || null,
|
|
247
|
-
gitBranch: config.gitBranch || null,
|
|
248
|
-
sessionId: session.id,
|
|
249
|
-
});
|
|
250
|
-
workingDirectory = gitSetup.workingDirectory;
|
|
251
|
-
gitWorktree = gitSetup.gitWorktree;
|
|
252
|
-
}
|
|
280
|
+
const { workingDirectory, gitWorktree } = await resolveSessionWorkingDirectory({
|
|
281
|
+
session, config, project,
|
|
282
|
+
});
|
|
253
283
|
|
|
254
284
|
if (gitWorktree) {
|
|
255
285
|
sessions.update(session.id, { gitWorktree });
|
|
@@ -259,20 +289,8 @@ export async function setupAndStartSession({ session, config, project, projectId
|
|
|
259
289
|
|
|
260
290
|
const isScheduled = config.scheduledAt && config.scheduledAt > Date.now();
|
|
261
291
|
if (config.startImmediately && !isScheduled) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
);
|
|
265
|
-
const finalPrompt = resolved ? resolved.userMessage : config.prompt;
|
|
266
|
-
const finalSystemPrompt = resolved ? resolved.systemPrompt : project.systemPrompt;
|
|
267
|
-
|
|
268
|
-
const { runSession } = await import('../services/sessionManager.js');
|
|
269
|
-
runSession(session.id, finalPrompt, workingDirectory, {
|
|
270
|
-
systemPrompt: finalSystemPrompt,
|
|
271
|
-
fileAttachments: sessionAttachments,
|
|
272
|
-
model: config.model,
|
|
273
|
-
}).catch((error) => {
|
|
274
|
-
console.error('Session error:', error);
|
|
275
|
-
sessions.update(session.id, { status: 'error', error: error.message });
|
|
292
|
+
await startSessionImmediately({
|
|
293
|
+
session, config, project, workingDirectory, sessionAttachments,
|
|
276
294
|
});
|
|
277
295
|
}
|
|
278
296
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { projects, sessions, sessionTemplates,
|
|
2
|
+
import { projects, sessions, sessionTemplates, projectDefaults, commandRuns } from '../database.js';
|
|
3
3
|
import { commandRunner } from '../services/commandRunner.js';
|
|
4
4
|
import { CreateProjectRequest, UpdateProjectRequest, ProjectSessionDefaultsRequest } from '../../../shared/src/contracts/projects.js';
|
|
5
5
|
import { ProjectDefaultsRepository } from '../db/ProjectDefaultsRepository.js';
|
|
6
6
|
import { CreateSessionTemplateRequest } from '../../../shared/src/contracts/templates.js';
|
|
7
|
-
import { CreateCommandButtonRequest, UpdateCommandButtonRequest } from '../../../shared/src/contracts/commandButtons.js';
|
|
8
7
|
import { broadcastToProject } from '../websocket.js';
|
|
8
|
+
import projectCommandButtonsRouter from './projects-commandButtons.js';
|
|
9
9
|
import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
|
|
10
10
|
import { handleUploadError, uploadMiddleware } from '../middleware/upload.js';
|
|
11
11
|
import {
|
|
@@ -18,10 +18,37 @@ import {
|
|
|
18
18
|
setupAndStartSession,
|
|
19
19
|
} from './projects-session-helpers.js';
|
|
20
20
|
import { validateGitSettings, buildRunsBySession } from './projects-helpers.js';
|
|
21
|
+
import { access, constants } from 'fs/promises';
|
|
22
|
+
import { dirname, isAbsolute } from 'path';
|
|
21
23
|
|
|
22
24
|
// Error message constants
|
|
23
25
|
const ERR_PROJECT_NOT_FOUND = 'Project not found';
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Validate a worktree path value.
|
|
29
|
+
* Returns null if valid, or an error message string if invalid.
|
|
30
|
+
* @param {string|null|undefined} worktreePath
|
|
31
|
+
* @returns {Promise<string|null>}
|
|
32
|
+
*/
|
|
33
|
+
export async function validateWorktreePath(worktreePath) {
|
|
34
|
+
if (worktreePath === null || worktreePath === undefined || worktreePath === '') {
|
|
35
|
+
return null; // null/empty is valid (means use default)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isAbsolute(worktreePath)) {
|
|
39
|
+
return 'Worktree path must be an absolute path';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parent = dirname(worktreePath);
|
|
43
|
+
try {
|
|
44
|
+
await access(parent, constants.W_OK);
|
|
45
|
+
} catch {
|
|
46
|
+
return `Parent directory does not exist or is not writable: ${parent}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null; // valid
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
const router = Router();
|
|
26
53
|
|
|
27
54
|
// GET /api/projects - List all projects
|
|
@@ -31,17 +58,29 @@ router.get('/', (_req, res) => {
|
|
|
31
58
|
});
|
|
32
59
|
|
|
33
60
|
// POST /api/projects - Create project
|
|
34
|
-
router.post('/', (req, res) => {
|
|
61
|
+
router.post('/', async (req, res) => {
|
|
35
62
|
const result = CreateProjectRequest.safeParse(req.body);
|
|
36
63
|
if (!result.success) {
|
|
37
64
|
return res.status(400).json({ error: result.error.issues[0].message });
|
|
38
65
|
}
|
|
39
66
|
|
|
40
|
-
const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted } = result.data;
|
|
41
|
-
|
|
67
|
+
const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted, worktreePath, kanbanEnabled } = result.data;
|
|
68
|
+
|
|
69
|
+
const pathError = await validateWorktreePath(worktreePath);
|
|
70
|
+
if (pathError) {
|
|
71
|
+
return res.status(400).json({ error: pathError });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const createOptions = {
|
|
42
75
|
onSessionCreated: onSessionCreated || null,
|
|
43
76
|
onSessionDeleted: onSessionDeleted || null,
|
|
44
|
-
|
|
77
|
+
worktreePath: worktreePath || null,
|
|
78
|
+
};
|
|
79
|
+
if (kanbanEnabled !== undefined) {
|
|
80
|
+
createOptions.kanbanEnabled = kanbanEnabled;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const project = projects.create(name, workingDirectory, systemPrompt || null, createOptions);
|
|
45
84
|
res.status(201).json(project);
|
|
46
85
|
});
|
|
47
86
|
|
|
@@ -55,7 +94,7 @@ router.get('/:id', (req, res) => {
|
|
|
55
94
|
});
|
|
56
95
|
|
|
57
96
|
// PUT /api/projects/:id - Update project
|
|
58
|
-
router.put('/:id', (req, res) => {
|
|
97
|
+
router.put('/:id', async (req, res) => {
|
|
59
98
|
const project = projects.getById(req.params.id);
|
|
60
99
|
if (!project) {
|
|
61
100
|
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
@@ -66,6 +105,13 @@ router.put('/:id', (req, res) => {
|
|
|
66
105
|
return res.status(400).json({ error: result.error.issues[0].message });
|
|
67
106
|
}
|
|
68
107
|
|
|
108
|
+
if (result.data.worktreePath !== undefined) {
|
|
109
|
+
const pathError = await validateWorktreePath(result.data.worktreePath);
|
|
110
|
+
if (pathError) {
|
|
111
|
+
return res.status(400).json({ error: pathError });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
69
115
|
const updated = projects.update(req.params.id, result.data);
|
|
70
116
|
res.json(updated);
|
|
71
117
|
});
|
|
@@ -183,24 +229,13 @@ async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, pro
|
|
|
183
229
|
return { config, nextTemplateId };
|
|
184
230
|
}
|
|
185
231
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const prepared = await validateAndPrepareSessionConfig(req.body, req.files, req.params.id, project);
|
|
195
|
-
if (prepared.error) {
|
|
196
|
-
return res.status(prepared.status).json({ error: prepared.error });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { config, nextTemplateId } = prepared;
|
|
200
|
-
const initialStatus = determineInitialStatus(config);
|
|
201
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Create the session row and apply any post-create updates.
|
|
234
|
+
* Returns the created session (already persisted in DB).
|
|
235
|
+
*/
|
|
236
|
+
function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
|
|
202
237
|
const sessionName = config.name || generateInitialName(config.prompt);
|
|
203
|
-
const session = sessions.create(
|
|
238
|
+
const session = sessions.create(projectId, sessionName, config.prompt, {
|
|
204
239
|
mode: config.mode,
|
|
205
240
|
thinkingEnabled: config.thinkingEnabled,
|
|
206
241
|
gitBranch: config.gitBranch,
|
|
@@ -210,7 +245,6 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
|
|
|
210
245
|
effortLevel: config.effortLevel,
|
|
211
246
|
});
|
|
212
247
|
|
|
213
|
-
// Apply optional post-create updates (next template + scheduling) in one pass
|
|
214
248
|
const postCreateUpdate = {
|
|
215
249
|
...(nextTemplateId ? { nextTemplateId } : {}),
|
|
216
250
|
...buildSchedulingUpdate(config, initialStatus),
|
|
@@ -218,24 +252,68 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
|
|
|
218
252
|
if (Object.keys(postCreateUpdate).length > 0) {
|
|
219
253
|
sessions.update(session.id, postCreateUpdate);
|
|
220
254
|
}
|
|
255
|
+
return session;
|
|
256
|
+
}
|
|
221
257
|
|
|
222
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Run setupAndStartSession and translate any failure into an error response,
|
|
260
|
+
* marking the session as errored and broadcasting the update.
|
|
261
|
+
*/
|
|
262
|
+
async function startSessionOrFail(req, res, { session, config, project }) {
|
|
223
263
|
try {
|
|
224
264
|
const { updatedSession } = await setupAndStartSession({
|
|
225
265
|
session, config, project, projectId: req.params.id, files: config.files,
|
|
226
266
|
});
|
|
227
|
-
res.status(201).json(updatedSession);
|
|
267
|
+
return res.status(201).json(updatedSession);
|
|
228
268
|
} catch (error) {
|
|
229
269
|
console.error('Git setup error:', error);
|
|
230
270
|
const updatedSession = sessions.update(session.id, { status: 'error', error: error.message });
|
|
231
|
-
|
|
232
271
|
broadcastToProject(req.params.id, WS_MESSAGE_TYPES.SESSION_UPDATED, {
|
|
233
272
|
projectId: req.params.id,
|
|
234
273
|
sessionId: session.id,
|
|
235
274
|
session: updatedSession,
|
|
236
275
|
});
|
|
276
|
+
return res.status(500).json({ error: `Git setup failed: ${error.message}` });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// POST /api/projects/:id/sessions - Create session
|
|
281
|
+
// Supports both JSON and multipart/form-data (for file attachments)
|
|
282
|
+
router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, async (req, res) => {
|
|
283
|
+
// Outer try/catch so any synchronous or asynchronous throw produces an HTTP
|
|
284
|
+
// response rather than leaving the socket hanging (which manifests as
|
|
285
|
+
// "socket hang up" on the client side). Without this, an unhandled rejection
|
|
286
|
+
// from validation, DB repositories, or template resolution could cause the
|
|
287
|
+
// intermittent flake observed in file-attachments.test.js.
|
|
288
|
+
let session = null;
|
|
289
|
+
try {
|
|
290
|
+
const project = projects.getById(req.params.id);
|
|
291
|
+
if (!project) {
|
|
292
|
+
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const prepared = await validateAndPrepareSessionConfig(req.body, req.files, req.params.id, project);
|
|
296
|
+
if (prepared.error) {
|
|
297
|
+
return res.status(prepared.status).json({ error: prepared.error });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { config, nextTemplateId } = prepared;
|
|
301
|
+
const initialStatus = determineInitialStatus(config);
|
|
302
|
+
session = createSessionRow(req.params.id, config, nextTemplateId, initialStatus);
|
|
303
|
+
return await startSessionOrFail(req, res, { session, config, project });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Session creation error:', error);
|
|
306
|
+
|
|
307
|
+
// If the session row was already created, mark it as errored so it isn't left dangling.
|
|
308
|
+
if (session && session.id) {
|
|
309
|
+
try {
|
|
310
|
+
sessions.update(session.id, { status: 'error', error: error.message });
|
|
311
|
+
} catch (updateError) {
|
|
312
|
+
console.error('Failed to mark session as errored:', updateError);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
237
315
|
|
|
238
|
-
res.status(500).json({ error:
|
|
316
|
+
return res.status(500).json({ error: error.message || 'Internal server error' });
|
|
239
317
|
}
|
|
240
318
|
});
|
|
241
319
|
|
|
@@ -269,90 +347,8 @@ router.post('/:id/templates', (req, res) => {
|
|
|
269
347
|
res.status(201).json(template);
|
|
270
348
|
});
|
|
271
349
|
|
|
272
|
-
//
|
|
273
|
-
router.
|
|
274
|
-
const project = projects.getById(req.params.id);
|
|
275
|
-
if (!project) {
|
|
276
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const buttons = commandButtons.getByProjectId(req.params.id);
|
|
280
|
-
res.json(buttons);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// POST /api/projects/:id/command-buttons - Create new command button
|
|
284
|
-
router.post('/:id/command-buttons', (req, res) => {
|
|
285
|
-
const project = projects.getById(req.params.id);
|
|
286
|
-
if (!project) {
|
|
287
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const result = CreateCommandButtonRequest.safeParse(req.body);
|
|
291
|
-
if (!result.success) {
|
|
292
|
-
return res.status(400).json({ error: result.error.issues[0].message });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const button = commandButtons.create({
|
|
296
|
-
projectId: req.params.id,
|
|
297
|
-
label: result.data.label,
|
|
298
|
-
command: result.data.command,
|
|
299
|
-
sortOrder: result.data.sortOrder,
|
|
300
|
-
showOnList: result.data.showOnList,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
res.status(201).json(button);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// GET /api/projects/:id/command-buttons/:buttonId - Get single button
|
|
307
|
-
router.get('/:id/command-buttons/:buttonId', (req, res) => {
|
|
308
|
-
const project = projects.getById(req.params.id);
|
|
309
|
-
if (!project) {
|
|
310
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const button = commandButtons.getById(req.params.buttonId);
|
|
314
|
-
if (!button) {
|
|
315
|
-
return res.status(404).json({ error: 'Command button not found' });
|
|
316
|
-
}
|
|
317
|
-
res.json(button);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// PATCH /api/projects/:id/command-buttons/:buttonId - Update button
|
|
321
|
-
router.patch('/:id/command-buttons/:buttonId', (req, res) => {
|
|
322
|
-
const project = projects.getById(req.params.id);
|
|
323
|
-
if (!project) {
|
|
324
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const button = commandButtons.getById(req.params.buttonId);
|
|
328
|
-
if (!button) {
|
|
329
|
-
return res.status(404).json({ error: 'Command button not found' });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const result = UpdateCommandButtonRequest.safeParse(req.body);
|
|
333
|
-
if (!result.success) {
|
|
334
|
-
return res.status(400).json({ error: result.error.issues[0].message });
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const updated = commandButtons.update(req.params.buttonId, result.data);
|
|
338
|
-
res.json(updated);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// DELETE /api/projects/:id/command-buttons/:buttonId - Delete button
|
|
342
|
-
router.delete('/:id/command-buttons/:buttonId', (req, res) => {
|
|
343
|
-
const project = projects.getById(req.params.id);
|
|
344
|
-
if (!project) {
|
|
345
|
-
return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const button = commandButtons.getById(req.params.buttonId);
|
|
349
|
-
if (!button) {
|
|
350
|
-
return res.status(404).json({ error: 'Command button not found' });
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
commandButtons.delete(req.params.buttonId);
|
|
354
|
-
res.status(204).send();
|
|
355
|
-
});
|
|
350
|
+
// Command button routes are mounted as a sub-router
|
|
351
|
+
router.use('/:id/command-buttons', projectCommandButtonsRouter);
|
|
356
352
|
|
|
357
353
|
// GET /api/projects/:id/session-defaults - Get session defaults for project
|
|
358
354
|
router.get('/:id/session-defaults', (req, res) => {
|
|
@@ -19,8 +19,9 @@ router.post('/:id/archive', requireSessionAndProject, async (req, res) => {
|
|
|
19
19
|
const updated = sessions.update(req.params.id, { archived: true });
|
|
20
20
|
|
|
21
21
|
// Execute project cleanup command if cleanup requested and project has one configured
|
|
22
|
+
// Only run for worktree sessions - branch-mode sessions have nothing to clean up
|
|
22
23
|
// Skip for child sessions - they share parent's resources and shouldn't trigger teardown
|
|
23
|
-
if (cleanup && req.project?.onSessionDeleted && !req.session_.parentSessionId) {
|
|
24
|
+
if (cleanup && req.project?.onSessionDeleted && req.session_.gitWorktree && !req.session_.parentSessionId) {
|
|
24
25
|
executeHookAsync(req.project.onSessionDeleted, req.workingDirectory, {
|
|
25
26
|
sessionId: req.session_.id,
|
|
26
27
|
projectId: req.project.id,
|
|
@@ -175,8 +175,9 @@ router.delete('/:id', requireSessionAndProject, async (req, res) => {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Execute on_session_deleted hook if configured (non-blocking)
|
|
178
|
+
// Only run for worktree sessions - branch-mode sessions have nothing to clean up
|
|
178
179
|
// Skip for child sessions - they share parent's resources and shouldn't trigger teardown
|
|
179
|
-
if (req.project?.onSessionDeleted && !req.session_.parentSessionId) {
|
|
180
|
+
if (req.project?.onSessionDeleted && req.session_.gitWorktree && !req.session_.parentSessionId) {
|
|
180
181
|
executeHookAsync(req.project.onSessionDeleted, req.workingDirectory, {
|
|
181
182
|
sessionId: req.session_.id,
|
|
182
183
|
projectId: req.project.id,
|
|
@@ -115,6 +115,8 @@ const FIELD_DEFINITIONS = [
|
|
|
115
115
|
{ field: 'autoSendPendingPrompt', transform: Boolean },
|
|
116
116
|
{ field: 'providerId', validate: validateProviderId },
|
|
117
117
|
{ field: 'prUrl', validate: validatePrUrl },
|
|
118
|
+
// Git fields
|
|
119
|
+
{ field: 'gitWorktree' },
|
|
118
120
|
// Scheduling fields
|
|
119
121
|
{ field: 'scheduledAt' },
|
|
120
122
|
{ field: 'autoRescheduleEnabled', transform: Boolean },
|
|
@@ -13,13 +13,15 @@ const __dirname = dirname(__filename);
|
|
|
13
13
|
*/
|
|
14
14
|
export class DatabaseManager {
|
|
15
15
|
#db = null;
|
|
16
|
+
#dbPath = null;
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Initialize database with WAL mode and foreign keys
|
|
19
20
|
* @param {string} dbPath - Path to database file (use ':memory:' for in-memory)
|
|
20
21
|
* @returns {Database.Database}
|
|
21
22
|
*/
|
|
22
|
-
init(dbPath
|
|
23
|
+
init(dbPath) {
|
|
24
|
+
this.#dbPath = dbPath;
|
|
23
25
|
this.#db = new Database(dbPath);
|
|
24
26
|
|
|
25
27
|
// Enable WAL mode and foreign keys
|
|
@@ -36,6 +38,14 @@ export class DatabaseManager {
|
|
|
36
38
|
return this.#db;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Get the path the database was initialized with.
|
|
43
|
+
* @returns {string|null}
|
|
44
|
+
*/
|
|
45
|
+
getPath() {
|
|
46
|
+
return this.#dbPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
/**
|
|
40
50
|
* Run database migrations for existing databases.
|
|
41
51
|
* Iterates over the flat, ordered list of migrations exported from
|
|
@@ -66,6 +76,7 @@ export class DatabaseManager {
|
|
|
66
76
|
if (this.#db) {
|
|
67
77
|
this.#db.close();
|
|
68
78
|
this.#db = null;
|
|
79
|
+
this.#dbPath = null;
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
82
|
|