circuschief 0.1.3 → 0.2.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 +2 -2
- package/packages/server/src/api/commands.js +50 -55
- package/packages/server/src/api/projects-helpers.js +13 -4
- package/packages/server/src/api/projects.js +33 -18
- package/packages/server/src/cli.js +82 -0
- package/packages/server/src/db/AgentCallLogRepository.js +30 -31
- package/packages/server/src/db/ConversationRepository.js +27 -16
- package/packages/server/src/db/ProjectRepository.js +21 -31
- package/packages/server/src/db/QuickResponseRepository.js +14 -19
- package/packages/server/src/db/migrations/sessionsMigrations.js +61 -61
- package/packages/server/src/index.js +42 -29
- package/packages/server/src/services/commandRunner.js +52 -99
- package/packages/server/src/services/kanbanTriggers.js +83 -56
- package/packages/server/src/services/schedulerService.js +68 -44
- package/packages/server/src/services/sessionExecution.js +102 -61
- package/packages/server/src/services/sessionManager.js +63 -37
- package/packages/server/src/services/summaryService.js +56 -53
- package/packages/server/src/services/templateTriggerService.js +58 -31
- package/packages/server/src/ws/WebSocketManager.js +5 -0
- package/packages/web/dist/assets/ActiveSessionsView-Bd8FWObJ.js +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-DfYXc6dz.css +1 -0
- package/packages/web/dist/assets/{AgentLogsView-c42v_j_5.js → AgentLogsView-C4FTXUH8.js} +1 -1
- package/packages/web/dist/assets/{ArchiveConfirmModal-DBuOmtXu.js → ArchiveConfirmModal-MpqhAjWZ.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-CkKJ3Htz.js → CommandButtonDetailView-_tOjEGjk.js} +1 -1
- package/packages/web/dist/assets/{EffortLevelSelector-BHJHSqul.js → EffortLevelSelector-B4z3zfJ6.js} +1 -1
- package/packages/web/dist/assets/{GeneralSettingsView-CdxfteZ2.js → GeneralSettingsView-BZ4x6T_N.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-DabnHhE4.js → InterpolationHelp-CIlrD5JA.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-DBvC-1OX.js +2 -0
- package/packages/web/dist/assets/{ModelSelector-BWIU4ud7.js → ModelSelector-BTmHxqCs.js} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BIZl8QlH.js → NewSessionView-Cdgt6wnk.js} +2 -2
- package/packages/web/dist/assets/{PathChooser-nhat_Pz4.js → PathChooser-CPEkT0uu.js} +1 -1
- package/packages/web/dist/assets/{ProjectEditView-DD-2_VrW.js → ProjectEditView-D59hr-v2.js} +1 -1
- package/packages/web/dist/assets/{ProjectListView-BOWbfoXQ.js → ProjectListView-gG4AR1i9.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-DC4uvSn2.js → ProjectNewView-BPjn1O4f.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-C04jD9NZ.js +1 -0
- package/packages/web/dist/assets/{QuickResponseSettings-Bk9mq96x.js → QuickResponseSettings-DaEXIp3-.js} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-BRvcnkQr.js → QuickResponsesPanel-CIdblIbt.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-CwGM4P3c.js → ResizableTextarea-DJekfIXO.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-BGDVHU9u.js → SessionCard-CAdetaVH.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-D20G3bX8.css → SessionCard-CcqIjL8q.css} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-CHYrx2Ab.js → SessionDetailView-BOdnH-cW.js} +17 -17
- package/packages/web/dist/assets/{SessionDetailView-7bWgC7Es.css → SessionDetailView-mnGRMaLY.css} +1 -1
- package/packages/web/dist/assets/{SessionFormOptions-8qvL25ca.js → SessionFormOptions-Dy57kl-x.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-BAIBtJF7.css → SessionListView-78k6TTz6.css} +1 -1
- package/packages/web/dist/assets/SessionListView-DUMUXfp4.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-B-w3n4c3.js → SessionLogStream-DHPxkaaK.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-Dd0ZJ4Nv.js → SettingsView-BzrkWbH3.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-CzyLjsdJ.js → SlashCommandWizard-BAC_oF5i.js} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-DTbh7uAF.js → SummarySettingsView-J8BQBEe9.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-BOnhkdtH.js → TemplateDetailView-2ilWbANb.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-CY87n64i.js → commandButtons-aIO6hqZn.js} +1 -1
- package/packages/web/dist/assets/{index-DcA6pqXV.js → index-B6W39ctH.js} +1 -1
- package/packages/web/dist/assets/{index-NzLFVaCi.js → index-BGmIjKYB.js} +1 -1
- package/packages/web/dist/assets/{index-CO4EBOFw.js → index-BwChYYnJ.js} +1 -1
- package/packages/web/dist/assets/{index-BshkV3r5.js → index-CDRRIqmL.js} +1 -1
- package/packages/web/dist/assets/{index-Dx0sYW7H.js → index-CSA0abwg.js} +1 -1
- package/packages/web/dist/assets/{index-Ce6sL47U.js → index-CWTVEGZv.js} +1 -1
- package/packages/web/dist/assets/{index-i1o916sk.js → index-CjJX0Eli.js} +1 -1
- package/packages/web/dist/assets/{index-BRUlEEHm.js → index-CpNgrGiE.js} +1 -1
- package/packages/web/dist/assets/{index-gMpnPf1V.js → index-DCxYGijD.js} +1 -1
- package/packages/web/dist/assets/{index-jGjvGBfk.js → index-DF6g7nEj.js} +1 -1
- package/packages/web/dist/assets/{index-aCw-iXPX.js → index-DMl4xPIQ.js} +1 -1
- package/packages/web/dist/assets/{index--OtPwBbF.js → index-DMsWg7Ax.js} +3 -3
- package/packages/web/dist/assets/{index-CjHb9rXv.js → index-DePUHO3n.js} +1 -1
- package/packages/web/dist/assets/{index-C6m-WfqP.js → index-DvfYqZgb.js} +1 -1
- package/packages/web/dist/assets/{index-DkLkDgig.js → index-DxUd3T5E.js} +23 -23
- package/packages/web/dist/assets/{index-BXUcbV4K.js → index-GLkcnEcc.js} +1 -1
- package/packages/web/dist/assets/{index-DPwwgloE.js → index-ZDCxncSd.js} +1 -1
- package/packages/web/dist/assets/{index-DxboI9i-.js → index-f24-2-RT.js} +1 -1
- package/packages/web/dist/assets/{index-Bi4bQ_UB.js → index-uZfsnHcN.js} +1 -1
- package/packages/web/dist/assets/{projects-C2Y29PSJ.js → projects-DGF_kWVA.js} +1 -1
- package/packages/web/dist/assets/{providers-CeJXuo0Q.js → providers-Cm7emO-N.js} +1 -1
- package/packages/web/dist/assets/sessions-CH7PeypH.js +1 -0
- package/packages/web/dist/assets/{settings-BplIxCbi.js → settings-C7sXXJ-n.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/ActiveSessionsView-BryJ-V3f.js +0 -1
- package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +0 -1
- package/packages/web/dist/assets/MarkdownEditor-k4zBLGqU.js +0 -2
- package/packages/web/dist/assets/ProvidersView-DT5afh1V.js +0 -1
- package/packages/web/dist/assets/SessionListView-927Yq6Il.js +0 -1
- package/packages/web/dist/assets/sessions-CMby7ij3.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circuschief",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first web UI for managing Claude Code sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"zod": "^4.2.1",
|
|
21
21
|
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
|
22
22
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
23
|
-
"better-sqlite3": "^
|
|
23
|
+
"better-sqlite3": "^12.1.0",
|
|
24
24
|
"cors": "^2.8.5",
|
|
25
25
|
"express": "^4.21.2",
|
|
26
26
|
"http-proxy-middleware": "^3.0.5",
|
|
@@ -71,6 +71,49 @@ router.get('/:name', async (req, res) => {
|
|
|
71
71
|
* - sessionId: Session to execute command in (required)
|
|
72
72
|
* - args: Object with argument values keyed by argument name (optional)
|
|
73
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* Handle execution of a skill invocation (skill body -> system prompt, args -> user message).
|
|
76
|
+
* Returns the response payload, or null if the command is not a skill.
|
|
77
|
+
*/
|
|
78
|
+
async function handleSkillExecution({ workingDirectory, name, args, sessionId, project }) {
|
|
79
|
+
const skillInvocation = await slashCommandService.buildSkillInvocation(
|
|
80
|
+
workingDirectory, name, args
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!skillInvocation) return null;
|
|
84
|
+
|
|
85
|
+
const skillSystemPrompt = slashCommandService.buildSkillSystemPrompt(
|
|
86
|
+
project.systemPrompt || null, skillInvocation
|
|
87
|
+
);
|
|
88
|
+
const userMessage = slashCommandService.buildSkillUserMessage(skillInvocation);
|
|
89
|
+
|
|
90
|
+
continueSession(sessionId, userMessage, workingDirectory, {
|
|
91
|
+
systemPrompt: skillSystemPrompt,
|
|
92
|
+
}).catch(err => {
|
|
93
|
+
console.error('Error executing skill:', err);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { success: true, command: name, message: userMessage };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle execution of a non-skill slash command.
|
|
101
|
+
* Returns the response payload.
|
|
102
|
+
*/
|
|
103
|
+
async function handleCommandExecution({ workingDirectory, name, args, sessionId, project }) {
|
|
104
|
+
const commandString = await slashCommandService.buildCommandString(
|
|
105
|
+
workingDirectory, name, args
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
continueSession(sessionId, commandString, workingDirectory, {
|
|
109
|
+
systemPrompt: project.systemPrompt || null,
|
|
110
|
+
}).catch(err => {
|
|
111
|
+
console.error('Error executing command:', err);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { success: true, command: name, message: commandString };
|
|
115
|
+
}
|
|
116
|
+
|
|
74
117
|
router.post('/:name/execute', async (req, res) => {
|
|
75
118
|
const { name } = req.params;
|
|
76
119
|
const { sessionId, args = {} } = req.body;
|
|
@@ -103,63 +146,15 @@ router.post('/:name/execute', async (req, res) => {
|
|
|
103
146
|
}
|
|
104
147
|
|
|
105
148
|
try {
|
|
106
|
-
// Check if this is a skill — skills need special handling
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
name,
|
|
111
|
-
args
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
if (skillInvocation) {
|
|
115
|
-
// Skill: inject body as system prompt context, args as user message
|
|
116
|
-
const skillSystemPrompt = slashCommandService.buildSkillSystemPrompt(
|
|
117
|
-
project.systemPrompt || null,
|
|
118
|
-
skillInvocation
|
|
119
|
-
);
|
|
120
|
-
const userMessage = slashCommandService.buildSkillUserMessage(skillInvocation);
|
|
121
|
-
|
|
122
|
-
continueSession(
|
|
123
|
-
sessionId,
|
|
124
|
-
userMessage,
|
|
125
|
-
workingDirectory,
|
|
126
|
-
{ systemPrompt: skillSystemPrompt } // No file attachments for slash commands
|
|
127
|
-
).catch(err => {
|
|
128
|
-
console.error('Error executing skill:', err);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
res.json({
|
|
132
|
-
success: true,
|
|
133
|
-
command: name,
|
|
134
|
-
message: userMessage,
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
149
|
+
// Check if this is a skill — skills need special handling
|
|
150
|
+
const skillResult = await handleSkillExecution({ workingDirectory, name, args, sessionId, project });
|
|
151
|
+
if (skillResult) {
|
|
152
|
+
return res.json(skillResult);
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
// Non-skill command
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
name,
|
|
143
|
-
args
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
// Use continueSession to send the command
|
|
147
|
-
// This handles the full message flow including broadcasting to WebSocket
|
|
148
|
-
continueSession(
|
|
149
|
-
sessionId,
|
|
150
|
-
commandString,
|
|
151
|
-
workingDirectory,
|
|
152
|
-
{ systemPrompt: project.systemPrompt || null } // No file attachments for slash commands
|
|
153
|
-
).catch(err => {
|
|
154
|
-
// Log but don't throw - continueSession runs asynchronously
|
|
155
|
-
console.error('Error executing command:', err);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
res.json({
|
|
159
|
-
success: true,
|
|
160
|
-
command: name,
|
|
161
|
-
message: commandString,
|
|
162
|
-
});
|
|
155
|
+
// Non-skill command
|
|
156
|
+
const cmdResult = await handleCommandExecution({ workingDirectory, name, args, sessionId, project });
|
|
157
|
+
res.json(cmdResult);
|
|
163
158
|
} catch (err) {
|
|
164
159
|
console.error('Error executing command:', err);
|
|
165
160
|
res.status(500).json({ error: err.message });
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { isGitRepo } from '../services/gitService.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Validate
|
|
4
|
+
* Validate and default git settings for git-backed projects.
|
|
5
|
+
* If gitMode or gitBranch are missing for a git project, defaults are applied
|
|
6
|
+
* (gitMode: 'none', gitBranch: 'main') instead of rejecting the request.
|
|
5
7
|
* @param {Object} config - The session configuration
|
|
6
8
|
* @param {Object} project - The project object
|
|
7
|
-
* @returns {Promise<string|null>}
|
|
9
|
+
* @returns {Promise<{config: Object, error: string|null}>} Updated config and error message if validation fails.
|
|
8
10
|
*/
|
|
9
11
|
export async function validateGitSettings(config, project) {
|
|
10
12
|
if (!config.gitMode || !config.gitBranch) {
|
|
11
13
|
const isGit = await isGitRepo(project.workingDirectory);
|
|
12
14
|
if (isGit) {
|
|
13
|
-
return
|
|
15
|
+
return {
|
|
16
|
+
config: {
|
|
17
|
+
...config,
|
|
18
|
+
gitMode: config.gitMode || 'none',
|
|
19
|
+
gitBranch: config.gitBranch || 'main',
|
|
20
|
+
},
|
|
21
|
+
error: null,
|
|
22
|
+
};
|
|
14
23
|
}
|
|
15
24
|
}
|
|
16
|
-
return null;
|
|
25
|
+
return { config, error: null };
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
/**
|
|
@@ -151,38 +151,53 @@ router.get('/:id/sessions', (req, res) => {
|
|
|
151
151
|
}
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const projectDefs = projectDefaults.getByProjectId(req.params.id);
|
|
154
|
+
/**
|
|
155
|
+
* Validate and prepare the session configuration from the request body.
|
|
156
|
+
* Returns { config, nextTemplateId } on success, or { error, status } on failure.
|
|
157
|
+
*/
|
|
158
|
+
async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, project) {
|
|
159
|
+
const projectDefs = projectDefaults.getByProjectId(projectId);
|
|
163
160
|
const systemDefaults = ProjectDefaultsRepository.getSystemDefaults();
|
|
164
|
-
const config = prepareSessionConfig(
|
|
165
|
-
config.files =
|
|
161
|
+
const config = prepareSessionConfig(reqBody, projectDefs, systemDefaults);
|
|
162
|
+
config.files = reqFiles || [];
|
|
166
163
|
|
|
167
164
|
if (!config.prompt) {
|
|
168
|
-
return
|
|
165
|
+
return { error: 'Prompt is required', status: 400 };
|
|
169
166
|
}
|
|
170
167
|
|
|
171
168
|
// Apply template overrides and resolve nextTemplateId
|
|
172
169
|
applyTemplateOverrides(config);
|
|
173
|
-
const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(
|
|
170
|
+
const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(reqBody, config.nextTemplateId || null);
|
|
174
171
|
if (nextTemplateError) {
|
|
175
|
-
return
|
|
172
|
+
return { error: nextTemplateError, status: 400 };
|
|
176
173
|
}
|
|
177
174
|
config.nextTemplateId = nextTemplateId;
|
|
178
175
|
|
|
179
|
-
const initialStatus = determineInitialStatus(config);
|
|
180
|
-
|
|
181
176
|
// Validate git settings for git repos
|
|
182
|
-
const gitError = await validateGitSettings(config, project);
|
|
177
|
+
const { config: updatedConfig, error: gitError } = await validateGitSettings(config, project);
|
|
183
178
|
if (gitError) {
|
|
184
|
-
return
|
|
179
|
+
return { error: gitError, status: 400 };
|
|
185
180
|
}
|
|
181
|
+
Object.assign(config, updatedConfig);
|
|
182
|
+
|
|
183
|
+
return { config, nextTemplateId };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// POST /api/projects/:id/sessions - Create session
|
|
187
|
+
// Supports both JSON and multipart/form-data (for file attachments)
|
|
188
|
+
router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, async (req, res) => {
|
|
189
|
+
const project = projects.getById(req.params.id);
|
|
190
|
+
if (!project) {
|
|
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);
|
|
186
201
|
|
|
187
202
|
const sessionName = config.name || generateInitialName(config.prompt);
|
|
188
203
|
const session = sessions.create(req.params.id, sessionName, config.prompt, {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { DEFAULT_SERVER_PORT } from '../../shared/src/index.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
function showHelp() {
|
|
11
|
+
console.log(`Usage: circuschief [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-p, --port <number> Port to listen on (env: PORT, default: ${DEFAULT_SERVER_PORT})
|
|
15
|
+
--no-analytics Disable anonymous usage analytics
|
|
16
|
+
-h, --help Show this help message
|
|
17
|
+
-V, --version Show version number`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(
|
|
23
|
+
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
24
|
+
);
|
|
25
|
+
return pkg.version;
|
|
26
|
+
} catch {
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseCliOptions(argv = process.argv) {
|
|
32
|
+
let values;
|
|
33
|
+
try {
|
|
34
|
+
({ values } = parseArgs({
|
|
35
|
+
args: argv.slice(2),
|
|
36
|
+
strict: true,
|
|
37
|
+
options: {
|
|
38
|
+
port: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
short: 'p',
|
|
41
|
+
default: process.env.PORT || String(DEFAULT_SERVER_PORT),
|
|
42
|
+
},
|
|
43
|
+
help: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
short: 'h',
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
version: {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
short: 'V',
|
|
51
|
+
default: false,
|
|
52
|
+
},
|
|
53
|
+
'no-analytics': {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(err.message);
|
|
61
|
+
showHelp();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (values.help) {
|
|
66
|
+
showHelp();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (values.version) {
|
|
71
|
+
console.log(getVersion());
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const port = parseInt(values.port, 10);
|
|
76
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
77
|
+
console.error(`Error: Invalid port "${values.port}". Must be 1-65535.`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { port, disableAnalytics: values['no-analytics'] };
|
|
82
|
+
}
|
|
@@ -133,6 +133,33 @@ export class AgentCallLogRepository extends BaseRepository {
|
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Build WHERE conditions and params from filter options.
|
|
138
|
+
* @param {Object} options - Filter options
|
|
139
|
+
* @returns {{ conditions: string[], params: any[] }}
|
|
140
|
+
*/
|
|
141
|
+
static buildFilters({ agentType, callType, status, model, sessionId, startDate, endDate }) {
|
|
142
|
+
const filterMap = [
|
|
143
|
+
[agentType, 'acl.agent_type = ?'],
|
|
144
|
+
[callType, 'acl.call_type = ?'],
|
|
145
|
+
[status, 'acl.status = ?'],
|
|
146
|
+
[model, 'acl.model = ?'],
|
|
147
|
+
[sessionId, 'acl.session_id = ?'],
|
|
148
|
+
[startDate, 'acl.started_at >= ?'],
|
|
149
|
+
[endDate, 'acl.started_at <= ?'],
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const conditions = [];
|
|
153
|
+
const params = [];
|
|
154
|
+
for (const [value, condition] of filterMap) {
|
|
155
|
+
if (value) {
|
|
156
|
+
conditions.push(condition);
|
|
157
|
+
params.push(value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { conditions, params };
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
/**
|
|
137
164
|
* Get all call logs with optional filtering, sorting, and pagination.
|
|
138
165
|
* Returns { rows, total } where total is the full count (before limit/offset).
|
|
@@ -162,37 +189,9 @@ export class AgentCallLogRepository extends BaseRepository {
|
|
|
162
189
|
const safeSortBy = SORTABLE_COLUMNS.includes(sortBy) ? sortBy : 'started_at';
|
|
163
190
|
const safeSortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC';
|
|
164
191
|
|
|
165
|
-
const conditions =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (agentType) {
|
|
169
|
-
conditions.push('acl.agent_type = ?');
|
|
170
|
-
params.push(agentType);
|
|
171
|
-
}
|
|
172
|
-
if (callType) {
|
|
173
|
-
conditions.push('acl.call_type = ?');
|
|
174
|
-
params.push(callType);
|
|
175
|
-
}
|
|
176
|
-
if (status) {
|
|
177
|
-
conditions.push('acl.status = ?');
|
|
178
|
-
params.push(status);
|
|
179
|
-
}
|
|
180
|
-
if (model) {
|
|
181
|
-
conditions.push('acl.model = ?');
|
|
182
|
-
params.push(model);
|
|
183
|
-
}
|
|
184
|
-
if (sessionId) {
|
|
185
|
-
conditions.push('acl.session_id = ?');
|
|
186
|
-
params.push(sessionId);
|
|
187
|
-
}
|
|
188
|
-
if (startDate) {
|
|
189
|
-
conditions.push('acl.started_at >= ?');
|
|
190
|
-
params.push(startDate);
|
|
191
|
-
}
|
|
192
|
-
if (endDate) {
|
|
193
|
-
conditions.push('acl.started_at <= ?');
|
|
194
|
-
params.push(endDate);
|
|
195
|
-
}
|
|
192
|
+
const { conditions, params } = AgentCallLogRepository.buildFilters({
|
|
193
|
+
agentType, callType, status, model, sessionId, startDate, endDate,
|
|
194
|
+
});
|
|
196
195
|
|
|
197
196
|
const whereClause = conditions.length > 0 ? `WHERE ${ conditions.join(' AND ')}` : '';
|
|
198
197
|
|
|
@@ -91,26 +91,31 @@ export class ConversationRepository extends BaseRepository {
|
|
|
91
91
|
* @param {Object} data - Fields to update
|
|
92
92
|
* @returns {Object} The updated conversation
|
|
93
93
|
*/
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Build the SET clause fields from data, collecting updates and values.
|
|
96
|
+
* Handles the isActive special case (deactivating other conversations).
|
|
97
|
+
* @param {string} id - Conversation ID
|
|
98
|
+
* @param {Object} data - Fields to update
|
|
99
|
+
* @returns {{ updates: string[], values: any[] }}
|
|
100
|
+
*/
|
|
101
|
+
#buildUpdateFields(id, data) {
|
|
102
|
+
const FIELD_MAP = {
|
|
103
|
+
name: 'name',
|
|
104
|
+
summary: 'summary',
|
|
105
|
+
summaryGeneratedAt: 'summary_generated_at',
|
|
106
|
+
claudeSessionId: 'claude_session_id',
|
|
107
|
+
};
|
|
108
|
+
|
|
95
109
|
const updates = [];
|
|
96
110
|
const values = [];
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
updates.push('summary = ?');
|
|
104
|
-
values.push(data.summary);
|
|
105
|
-
}
|
|
106
|
-
if (data.summaryGeneratedAt !== undefined) {
|
|
107
|
-
updates.push('summary_generated_at = ?');
|
|
108
|
-
values.push(data.summaryGeneratedAt);
|
|
109
|
-
}
|
|
110
|
-
if (data.claudeSessionId !== undefined) {
|
|
111
|
-
updates.push('claude_session_id = ?');
|
|
112
|
-
values.push(data.claudeSessionId);
|
|
112
|
+
for (const [key, column] of Object.entries(FIELD_MAP)) {
|
|
113
|
+
if (data[key] !== undefined) {
|
|
114
|
+
updates.push(`${column} = ?`);
|
|
115
|
+
values.push(data[key]);
|
|
116
|
+
}
|
|
113
117
|
}
|
|
118
|
+
|
|
114
119
|
if (data.isActive !== undefined) {
|
|
115
120
|
// If setting this conversation as active, deactivate others first
|
|
116
121
|
if (data.isActive) {
|
|
@@ -125,6 +130,12 @@ export class ConversationRepository extends BaseRepository {
|
|
|
125
130
|
values.push(data.isActive ? 1 : 0);
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
return { updates, values };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
update(id, data) {
|
|
137
|
+
const { updates, values } = this.#buildUpdateFields(id, data);
|
|
138
|
+
|
|
128
139
|
if (updates.length === 0) {
|
|
129
140
|
return this.getById(id);
|
|
130
141
|
}
|
|
@@ -61,42 +61,32 @@ export class ProjectRepository extends BaseRepository {
|
|
|
61
61
|
return this.mapAll(rows);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Field mapping from camelCase data keys to snake_case column names.
|
|
66
|
+
* Entries with a transform function apply that transform to the value.
|
|
67
|
+
*/
|
|
68
|
+
static #FIELD_MAP = {
|
|
69
|
+
name: { column: 'name' },
|
|
70
|
+
workingDirectory: { column: 'working_directory' },
|
|
71
|
+
systemPrompt: { column: 'system_prompt' },
|
|
72
|
+
onSessionCreated: { column: 'on_session_created' },
|
|
73
|
+
onSessionDeleted: { column: 'on_session_deleted' },
|
|
74
|
+
prPollInterval: { column: 'pr_poll_interval' },
|
|
75
|
+
repoUrl: { column: 'repo_url' },
|
|
76
|
+
kanbanEnabled: { column: 'kanban_enabled', transform: (v) => v ? 1 : 0 },
|
|
77
|
+
};
|
|
78
|
+
|
|
64
79
|
update(id, data) {
|
|
65
80
|
const updates = [];
|
|
66
81
|
const values = [];
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
updates.push('working_directory = ?');
|
|
74
|
-
values.push(data.workingDirectory);
|
|
75
|
-
}
|
|
76
|
-
if (data.systemPrompt !== undefined) {
|
|
77
|
-
updates.push('system_prompt = ?');
|
|
78
|
-
values.push(data.systemPrompt);
|
|
79
|
-
}
|
|
80
|
-
if (data.onSessionCreated !== undefined) {
|
|
81
|
-
updates.push('on_session_created = ?');
|
|
82
|
-
values.push(data.onSessionCreated);
|
|
83
|
-
}
|
|
84
|
-
if (data.onSessionDeleted !== undefined) {
|
|
85
|
-
updates.push('on_session_deleted = ?');
|
|
86
|
-
values.push(data.onSessionDeleted);
|
|
87
|
-
}
|
|
88
|
-
if (data.prPollInterval !== undefined) {
|
|
89
|
-
updates.push('pr_poll_interval = ?');
|
|
90
|
-
values.push(data.prPollInterval);
|
|
91
|
-
}
|
|
92
|
-
if (data.repoUrl !== undefined) {
|
|
93
|
-
updates.push('repo_url = ?');
|
|
94
|
-
values.push(data.repoUrl);
|
|
95
|
-
}
|
|
96
|
-
if (data.kanbanEnabled !== undefined) {
|
|
97
|
-
updates.push('kanban_enabled = ?');
|
|
98
|
-
values.push(data.kanbanEnabled ? 1 : 0);
|
|
83
|
+
for (const [key, { column, transform }] of Object.entries(ProjectRepository.#FIELD_MAP)) {
|
|
84
|
+
if (data[key] !== undefined) {
|
|
85
|
+
updates.push(`${column} = ?`);
|
|
86
|
+
values.push(transform ? transform(data[key]) : data[key]);
|
|
87
|
+
}
|
|
99
88
|
}
|
|
89
|
+
|
|
100
90
|
if (updates.length === 0) return this.getById(id);
|
|
101
91
|
|
|
102
92
|
updates.push('updated_at = ?');
|
|
@@ -102,6 +102,15 @@ export class QuickResponseRepository extends BaseRepository {
|
|
|
102
102
|
* @param {Object} data - Fields to update
|
|
103
103
|
* @returns {Object|null} Updated quick response or null if not found
|
|
104
104
|
*/
|
|
105
|
+
/** @type {Array<[string, string, ((v: any) => any)?]>} [dataKey, columnName, transform?] */
|
|
106
|
+
static #UPDATE_FIELDS = [
|
|
107
|
+
['label', 'label'],
|
|
108
|
+
['content', 'content'],
|
|
109
|
+
['autoSubmit', 'auto_submit', (v) => v ? 1 : 0],
|
|
110
|
+
['category', 'category'],
|
|
111
|
+
['sortOrder', 'sort_order'],
|
|
112
|
+
];
|
|
113
|
+
|
|
105
114
|
update(id, data) {
|
|
106
115
|
const existing = this.getById(id);
|
|
107
116
|
if (!existing) {
|
|
@@ -111,25 +120,11 @@ export class QuickResponseRepository extends BaseRepository {
|
|
|
111
120
|
const updates = [];
|
|
112
121
|
const values = [];
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
updates.push('content = ?');
|
|
120
|
-
values.push(data.content);
|
|
121
|
-
}
|
|
122
|
-
if (data.autoSubmit !== undefined) {
|
|
123
|
-
updates.push('auto_submit = ?');
|
|
124
|
-
values.push(data.autoSubmit ? 1 : 0);
|
|
125
|
-
}
|
|
126
|
-
if (data.category !== undefined) {
|
|
127
|
-
updates.push('category = ?');
|
|
128
|
-
values.push(data.category);
|
|
129
|
-
}
|
|
130
|
-
if (data.sortOrder !== undefined) {
|
|
131
|
-
updates.push('sort_order = ?');
|
|
132
|
-
values.push(data.sortOrder);
|
|
123
|
+
for (const [key, column, transform] of QuickResponseRepository.#UPDATE_FIELDS) {
|
|
124
|
+
if (data[key] !== undefined) {
|
|
125
|
+
updates.push(`${column} = ?`);
|
|
126
|
+
values.push(transform ? transform(data[key]) : data[key]);
|
|
127
|
+
}
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
if (updates.length > 0) {
|