specrails-desktop 2.2.0 → 2.3.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.
Files changed (63) hide show
  1. package/client/dist/assets/ActivityFeedPage-3Veccrvk.js +1 -0
  2. package/client/dist/assets/AgentsPage-2mFPghP4.js +86 -0
  3. package/client/dist/assets/{AnalyticsPage-D6LE6wG2.js → AnalyticsPage-Dyyz1ht3.js} +1 -1
  4. package/client/dist/assets/{BarChart-B366kDEj.js → BarChart-CMdLa6Es.js} +2 -2
  5. package/client/dist/assets/CodePage-D7Xwjhut.js +2 -0
  6. package/client/dist/assets/{DesktopAnalyticsPage-DG5LA_WO.js → DesktopAnalyticsPage-CTNwb639.js} +1 -1
  7. package/client/dist/assets/{DocsDialog-ChQ1oXLC.js → DocsDialog-D8yoyZDD.js} +2 -2
  8. package/client/dist/assets/{DocsPage-BfGH8NUf.js → DocsPage-CeO-fAxy.js} +2 -2
  9. package/client/dist/assets/{ExportDropdown-9tRrlfM7.js → ExportDropdown-DuoZcdYN.js} +1 -1
  10. package/client/dist/assets/{IntegrationsPage-DANIzihd.js → IntegrationsPage-iIZ0UEzf.js} +3 -3
  11. package/client/dist/assets/JobDetailPage-DgJHAH2m.js +16 -0
  12. package/client/dist/assets/JobsPage-Bv_RpRAE.js +1 -0
  13. package/client/dist/assets/code-BtsmPQLV.js +1 -0
  14. package/client/dist/assets/code-CY85RXZU.js +1 -0
  15. package/client/dist/assets/code-Coa8f2Sh.js +1 -0
  16. package/client/dist/assets/code-D1z-YDt-.js +1 -0
  17. package/client/dist/assets/code-DDU0CRS0.js +1 -0
  18. package/client/dist/assets/code-L35Loak_.js +1 -0
  19. package/client/dist/assets/code-g0qFMzyg.js +1 -0
  20. package/client/dist/assets/code-zCwBt3Uu.js +1 -0
  21. package/client/dist/assets/{dist-js-BvQ52Q67.js → dist-js-4UEGaKhD.js} +1 -1
  22. package/client/dist/assets/{dist-js-XEilFTNz.js → dist-js-H6hyhSuv.js} +1 -1
  23. package/client/dist/assets/{index-CNiaj7Sj.js → index-CGHKpC-N.js} +13 -13
  24. package/client/dist/assets/index-D17R4Cjc.css +2 -0
  25. package/client/dist/assets/{lib-DZJmnErt.js → lib-Cs5FrUJI.js} +1 -1
  26. package/client/dist/assets/{useProjectCache-H0T8Ot9j.js → useProjectCache-BZWYV-w-.js} +1 -1
  27. package/client/dist/index.html +3 -3
  28. package/package.json +1 -1
  29. package/server/dist/agent-refine-manager.js +128 -153
  30. package/server/dist/chat-manager.js +246 -0
  31. package/server/dist/code-explorer-router.js +78 -0
  32. package/server/dist/command-resolver.js +17 -0
  33. package/server/dist/contract-refine-runner.js +42 -10
  34. package/server/dist/db.js +6 -0
  35. package/server/dist/desktop-db.js +3 -0
  36. package/server/dist/explore-stdin-session.js +129 -0
  37. package/server/dist/mobile/mobile-auth.js +16 -0
  38. package/server/dist/project-router-chat.js +218 -0
  39. package/server/dist/project-router-helpers.js +275 -0
  40. package/server/dist/project-router-jobs.js +389 -0
  41. package/server/dist/project-router-settings.js +312 -0
  42. package/server/dist/project-router-setup.js +456 -0
  43. package/server/dist/project-router-spending.js +320 -0
  44. package/server/dist/project-router-terminals.js +312 -0
  45. package/server/dist/project-router-tickets.js +1767 -0
  46. package/server/dist/project-router.js +27 -3943
  47. package/server/dist/providers/claude-adapter.js +58 -17
  48. package/server/dist/providers/codex-adapter.js +6 -0
  49. package/server/dist/spawn-lifecycle.js +117 -0
  50. package/client/dist/assets/ActivityFeedPage-BupGdGjj.js +0 -1
  51. package/client/dist/assets/AgentsPage-F3xksiLd.js +0 -86
  52. package/client/dist/assets/CodePage-DLwCJgQ0.js +0 -2
  53. package/client/dist/assets/JobDetailPage-1RtejIOB.js +0 -16
  54. package/client/dist/assets/JobsPage-NuDf5Zbx.js +0 -1
  55. package/client/dist/assets/code-AL1rVIMb.js +0 -1
  56. package/client/dist/assets/code-C0BKpkht.js +0 -1
  57. package/client/dist/assets/code-C0FTS3ew.js +0 -1
  58. package/client/dist/assets/code-CPcHxzxw.js +0 -1
  59. package/client/dist/assets/code-D3ryDniw.js +0 -1
  60. package/client/dist/assets/code-D3zVVQTj.js +0 -1
  61. package/client/dist/assets/code-PCmfS3dn.js +0 -1
  62. package/client/dist/assets/code-exI0G5Wd.js +0 -1
  63. package/client/dist/assets/index-DgFfrrTX.css +0 -2
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerSettingsRoutes = registerSettingsRoutes;
7
+ // Domain routes extracted from project-router.ts (settings).
8
+ // Registered on the shared router by createProjectRouter — behaviour-preserving.
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const db_1 = require("./db");
12
+ const telemetry_export_1 = require("./telemetry-export");
13
+ const context_budget_1 = require("./context-budget");
14
+ const context_scope_1 = require("./context-scope");
15
+ const terminal_settings_1 = require("./terminal-settings");
16
+ const terminal_marks_store_1 = require("./terminal-marks-store");
17
+ const project_router_helpers_1 = require("./project-router-helpers");
18
+ function registerSettingsRoutes(deps) {
19
+ const { router, registry, ctx, ticketPath } = deps;
20
+ // ─── Project settings (pipeline telemetry) ───────────────────────────────────
21
+ router.get('/:projectId/settings', (req, res) => {
22
+ const settings = (0, db_1.getProjectSettings)(ctx(req).db);
23
+ res.json(settings);
24
+ });
25
+ // ─── Per-project Quick mode Contract Refine last-used value ─────────────────
26
+ router.get('/:projectId/add-spec-quick-contract-refine-last', (req, res) => {
27
+ res.json({
28
+ enabled: (0, db_1.getQuickContractRefineLast)(ctx(req).db),
29
+ configured: (0, db_1.hasQuickContractRefineLast)(ctx(req).db),
30
+ });
31
+ });
32
+ router.patch('/:projectId/add-spec-quick-contract-refine-last', (req, res) => {
33
+ const enabled = req.body?.enabled;
34
+ if (typeof enabled !== 'boolean') {
35
+ res.status(400).json({ error: 'enabled must be a boolean' });
36
+ return;
37
+ }
38
+ (0, db_1.setQuickContractRefineLast)(ctx(req).db, enabled);
39
+ res.json({ enabled: (0, db_1.getQuickContractRefineLast)(ctx(req).db) });
40
+ });
41
+ // ─── Add Spec context scope ────────────────────────────────────────────────
42
+ router.get('/:projectId/context-budget', (req, res) => {
43
+ const { project } = ctx(req);
44
+ try {
45
+ const budget = (0, context_budget_1.getContextBudget)(project.id, project.path);
46
+ res.json(budget);
47
+ }
48
+ catch (err) {
49
+ console.error('[project-router] context-budget failed:', err);
50
+ res.status(500).json({ error: 'failed to compute context budget' });
51
+ }
52
+ });
53
+ router.get('/:projectId/context-scope-last', (req, res) => {
54
+ const scope = (0, context_scope_1.getLastContextScope)(ctx(req).db, 'explore');
55
+ res.json({ scope });
56
+ });
57
+ router.patch('/:projectId/context-scope-last', (req, res) => {
58
+ const body = req.body;
59
+ if (!body || typeof body !== 'object') {
60
+ res.status(400).json({ error: 'body must be an object' });
61
+ return;
62
+ }
63
+ // Validate booleans-only for any provided key.
64
+ for (const key of ['specrails', 'openspec', 'full', 'mcp', 'contractRefine', 'userMcp']) {
65
+ if (body[key] !== undefined && typeof body[key] !== 'boolean') {
66
+ res.status(400).json({ error: `${key} must be a boolean` });
67
+ return;
68
+ }
69
+ }
70
+ const current = (0, context_scope_1.getLastContextScope)(ctx(req).db, 'explore');
71
+ const merged = (0, context_scope_1.normalizeContextScope)({ ...current, ...body }, current);
72
+ (0, context_scope_1.setLastContextScope)(ctx(req).db, merged);
73
+ res.json({ scope: merged });
74
+ });
75
+ router.patch('/:projectId/settings', (req, res) => {
76
+ const { pipelineTelemetryEnabled, orchestratorModel, prePrompt, ultraPrePrompt } = req.body ?? {};
77
+ const patch = {};
78
+ if (pipelineTelemetryEnabled !== undefined) {
79
+ patch.pipelineTelemetryEnabled = Boolean(pipelineTelemetryEnabled);
80
+ }
81
+ const VALID_MODELS = ['sonnet', 'opus', 'haiku'];
82
+ if (orchestratorModel !== undefined) {
83
+ if (typeof orchestratorModel !== 'string' || !VALID_MODELS.includes(orchestratorModel)) {
84
+ res.status(400).json({ error: `orchestratorModel must be one of: ${VALID_MODELS.join(', ')}` });
85
+ return;
86
+ }
87
+ patch.orchestratorModel = orchestratorModel;
88
+ }
89
+ if (prePrompt !== undefined) {
90
+ if (typeof prePrompt !== 'string') {
91
+ res.status(400).json({ error: 'prePrompt must be a string' });
92
+ return;
93
+ }
94
+ patch.prePrompt = prePrompt;
95
+ }
96
+ if (ultraPrePrompt !== undefined) {
97
+ if (typeof ultraPrePrompt !== 'string') {
98
+ res.status(400).json({ error: 'ultraPrePrompt must be a string' });
99
+ return;
100
+ }
101
+ patch.ultraPrePrompt = ultraPrePrompt;
102
+ }
103
+ try {
104
+ (0, db_1.updateProjectSettings)(ctx(req).db, patch);
105
+ res.json({ ok: true, settings: (0, db_1.getProjectSettings)(ctx(req).db) });
106
+ }
107
+ catch (err) {
108
+ console.error('[project-router] settings patch error:', err);
109
+ res.status(500).json({ error: 'Failed to update settings' });
110
+ }
111
+ });
112
+ // ─── Agent models ────────────────────────────────────────────────────────────
113
+ router.get('/:projectId/agent-models', (req, res) => {
114
+ const { project } = ctx(req);
115
+ const agents = (0, project_router_helpers_1.readAgentModels)(project.path);
116
+ res.json({ agents });
117
+ });
118
+ router.patch('/:projectId/agent-models', (req, res) => {
119
+ const { project } = ctx(req);
120
+ const { defaultModel, overrides } = req.body ?? {};
121
+ // Validate defaultModel if provided
122
+ if (defaultModel !== undefined) {
123
+ if (typeof defaultModel !== 'string' || !project_router_helpers_1.VALID_MODEL_ALIASES.includes(defaultModel)) {
124
+ res.status(400).json({ error: `Invalid model alias. Must be one of: ${project_router_helpers_1.VALID_MODEL_ALIASES.join(', ')}` });
125
+ return;
126
+ }
127
+ }
128
+ // Validate overrides map if provided
129
+ if (overrides !== undefined) {
130
+ if (typeof overrides !== 'object' || Array.isArray(overrides) || overrides === null) {
131
+ res.status(400).json({ error: 'overrides must be an object' });
132
+ return;
133
+ }
134
+ for (const [agentName, modelValue] of Object.entries(overrides)) {
135
+ if (typeof modelValue !== 'string' || !project_router_helpers_1.VALID_MODEL_ALIASES.includes(modelValue)) {
136
+ res.status(400).json({ error: `Invalid model alias for agent "${agentName}". Must be one of: ${project_router_helpers_1.VALID_MODEL_ALIASES.join(', ')}` });
137
+ return;
138
+ }
139
+ }
140
+ }
141
+ const configDir = path_1.default.join(project.path, '.specrails');
142
+ const configPath = path_1.default.join(configDir, 'install-config.yaml');
143
+ // Read existing config or build default shape
144
+ let existingConfig = {
145
+ version: 1,
146
+ provider: 'claude',
147
+ tier: 'quick',
148
+ agents: { selected: [], excluded: [] },
149
+ models: { preset: 'balanced', defaults: { model: 'sonnet' }, overrides: {} },
150
+ agent_teams: false,
151
+ };
152
+ if (fs_1.default.existsSync(configPath)) {
153
+ try {
154
+ const text = fs_1.default.readFileSync(configPath, 'utf-8');
155
+ // Parse fields we care about from the existing config text
156
+ const versionMatch = text.match(/^version:\s*(\d+)/m);
157
+ const providerMatch = text.match(/^provider:\s*(\S+)/m);
158
+ const tierMatch = text.match(/^tier:\s*(\S+)/m);
159
+ const presetMatch = text.match(/preset:\s*(\S+)/);
160
+ const agentTeamsMatch = text.match(/^agent_teams:\s*(\S+)/m);
161
+ // Parse selected agents list
162
+ const selectedMatch = text.match(/selected:\s*\[([^\]]*)\]/);
163
+ const excludedMatch = text.match(/excluded:\s*\[([^\]]*)\]/);
164
+ const parsedSelected = selectedMatch
165
+ ? selectedMatch[1].split(',').map(s => s.trim()).filter(Boolean)
166
+ : [];
167
+ const parsedExcluded = excludedMatch
168
+ ? excludedMatch[1].split(',').map(s => s.trim()).filter(Boolean)
169
+ : [];
170
+ // Parse existing overrides to merge
171
+ const existingOverrides = {};
172
+ const overridesBlockMatch = text.match(/overrides:([\s\S]*?)(?:\n\S|$)/);
173
+ if (overridesBlockMatch) {
174
+ const block = overridesBlockMatch[1];
175
+ const overrideLines = block.match(/^ {2,}(\S+):\s*(\S+)/gm) ?? [];
176
+ for (const line of overrideLines) {
177
+ const m = line.match(/^\s+(\S+):\s*(\S+)/);
178
+ if (m)
179
+ existingOverrides[m[1]] = m[2];
180
+ }
181
+ }
182
+ existingConfig = {
183
+ version: versionMatch ? parseInt(versionMatch[1], 10) : 1,
184
+ provider: providerMatch ? providerMatch[1] : 'claude',
185
+ tier: tierMatch ? tierMatch[1] : 'quick',
186
+ agents: { selected: parsedSelected, excluded: parsedExcluded },
187
+ models: {
188
+ preset: presetMatch ? presetMatch[1] : 'balanced',
189
+ defaults: { model: 'sonnet' },
190
+ overrides: existingOverrides,
191
+ },
192
+ agent_teams: agentTeamsMatch ? agentTeamsMatch[1] === 'true' : false,
193
+ };
194
+ }
195
+ catch {
196
+ // use defaults
197
+ }
198
+ }
199
+ // Merge new values into config
200
+ const mergedModels = existingConfig.models;
201
+ if (defaultModel !== undefined) {
202
+ mergedModels.defaults = { model: defaultModel };
203
+ }
204
+ if (overrides !== undefined) {
205
+ mergedModels.overrides = overrides;
206
+ }
207
+ existingConfig.models = mergedModels;
208
+ try {
209
+ fs_1.default.mkdirSync(configDir, { recursive: true });
210
+ const yaml = (0, project_router_helpers_1.serializeInstallConfigYaml)(existingConfig);
211
+ fs_1.default.writeFileSync(configPath, yaml, 'utf-8');
212
+ (0, project_router_helpers_1.applyModelConfig)(project.path);
213
+ const agents = (0, project_router_helpers_1.readAgentModels)(project.path);
214
+ res.json({ agents });
215
+ }
216
+ catch (err) {
217
+ console.error('[project-router] agent-models patch error:', err);
218
+ res.status(500).json({ error: `Failed to apply model config: ${err}` });
219
+ }
220
+ });
221
+ // ─── Diagnostic export ───────────────────────────────────────────────────────
222
+ router.get('/:projectId/jobs/:jobId/diagnostic', async (req, res) => {
223
+ const { db } = ctx(req);
224
+ const jobId = req.params.jobId;
225
+ const blob = (0, db_1.getTelemetryBlob)(db, jobId);
226
+ if (!blob) {
227
+ res.status(404).json({ error: 'No telemetry data for this job' });
228
+ return;
229
+ }
230
+ if (blob.state === 'expired') {
231
+ res.status(410).json({ error: 'Telemetry data has been expired and is no longer available' });
232
+ return;
233
+ }
234
+ const job = (0, db_1.getJob)(db, jobId);
235
+ if (!job) {
236
+ res.status(404).json({ error: 'Job not found' });
237
+ return;
238
+ }
239
+ const summaries = (0, db_1.getTelemetrySummaries)(db, jobId);
240
+ const events = (0, db_1.getJobEvents)(db, jobId);
241
+ try {
242
+ const dateStr = new Date().toISOString().slice(0, 10);
243
+ const filename = `specrails-diagnostic-${jobId}-${dateStr}.zip`;
244
+ res.setHeader('Content-Type', 'application/zip');
245
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
246
+ const profileRow = db
247
+ .prepare(`SELECT profile_name, profile_json FROM job_profiles WHERE job_id = ?`)
248
+ .get(jobId);
249
+ const { homeJobSnapshotPath } = require('./plugins/paths');
250
+ const pluginSnap = homeJobSnapshotPath(req.projectCtx.project.slug, jobId);
251
+ await (0, telemetry_export_1.createDiagnosticZip)(res, {
252
+ job,
253
+ blob,
254
+ summaries,
255
+ events,
256
+ profile: profileRow ? { name: profileRow.profile_name, json: profileRow.profile_json } : null,
257
+ pluginSnapshotPath: pluginSnap,
258
+ });
259
+ }
260
+ catch (err) {
261
+ if (!res.headersSent) {
262
+ console.error('[project-router] diagnostic export error:', err);
263
+ res.status(500).json({ error: 'Failed to create diagnostic export' });
264
+ }
265
+ }
266
+ });
267
+ // ─── Terminal command marks ────────────────────────────────────────────────
268
+ // GET /api/projects/:projectId/terminals/:id/marks?limit=&before=
269
+ router.get('/:projectId/terminals/:id/marks', (req, res) => {
270
+ const projectCtx = ctx(req);
271
+ const sessionId = req.params.id;
272
+ const limit = parseInt(req.query.limit ?? '100', 10);
273
+ const before = req.query.before ? parseInt(req.query.before, 10) : undefined;
274
+ const marks = (0, terminal_marks_store_1.listMarks)(projectCtx.db, sessionId, {
275
+ limit: Number.isFinite(limit) ? limit : 100,
276
+ before: typeof before === 'number' && Number.isFinite(before) ? before : undefined,
277
+ });
278
+ res.json({ marks });
279
+ });
280
+ // ─── Terminal settings (per-project override layer) ────────────────────────
281
+ // GET /api/projects/:projectId/terminal-settings — returns { resolved, override, desktopDefaults }
282
+ router.get('/:projectId/terminal-settings', (req, res) => {
283
+ const projectCtx = ctx(req);
284
+ const desktopDefaults = (0, terminal_settings_1.getDesktopTerminalSettings)(registry.desktopDb);
285
+ const override = (0, terminal_settings_1.getProjectOverride)(projectCtx.db);
286
+ const resolved = (0, terminal_settings_1.resolveTerminalSettings)(registry.desktopDb, projectCtx.db);
287
+ res.json({ resolved, override, desktopDefaults });
288
+ });
289
+ // PATCH /api/projects/:projectId/terminal-settings — partial update of override
290
+ // (null value for a field clears that override)
291
+ router.patch('/:projectId/terminal-settings', (req, res) => {
292
+ const projectCtx = ctx(req);
293
+ if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
294
+ res.status(400).json({ error: 'invalid body' });
295
+ return;
296
+ }
297
+ try {
298
+ (0, terminal_settings_1.patchProjectOverride)(projectCtx.db, req.body);
299
+ const desktopDefaults = (0, terminal_settings_1.getDesktopTerminalSettings)(registry.desktopDb);
300
+ const override = (0, terminal_settings_1.getProjectOverride)(projectCtx.db);
301
+ const resolved = (0, terminal_settings_1.resolveTerminalSettings)(registry.desktopDb, projectCtx.db);
302
+ res.json({ resolved, override, desktopDefaults });
303
+ }
304
+ catch (err) {
305
+ if (err instanceof terminal_settings_1.TerminalSettingsValidationError) {
306
+ res.status(400).json({ error: 'validation_failed', field: err.field, message: err.message });
307
+ return;
308
+ }
309
+ throw err;
310
+ }
311
+ });
312
+ }