@specship/specship-win32-arm64 0.12.0 → 0.13.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/lib/dist/bin/specship.js +48 -12
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +16 -1
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/schema.sql +10 -0
- package/lib/dist/db/spec-queries.d.ts +6 -0
- package/lib/dist/db/spec-queries.d.ts.map +1 -1
- package/lib/dist/db/spec-queries.js +8 -0
- package/lib/dist/db/spec-queries.js.map +1 -1
- package/lib/dist/index.d.ts +8 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +9 -0
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/server/cli.js +9 -7
- package/lib/dist/server/product-version.js +44 -0
- package/lib/dist/server/routes/claude.js +153 -42
- package/lib/dist/server/routes/config.js +35 -0
- package/lib/dist/server/routes/graph.js +22 -1
- package/lib/dist/server/routes/mcp.js +510 -0
- package/lib/dist/server/routes/projects.js +31 -3
- package/lib/dist/server/routes/status.js +3 -0
- package/lib/dist/server/routes/workflow.js +44 -3
- package/lib/dist/server/server-config.js +38 -0
- package/lib/dist/server/server.js +128 -58
- package/lib/dist/statusline/active-run.d.ts +2 -2
- package/lib/dist/statusline/active-run.d.ts.map +1 -1
- package/lib/dist/statusline/active-run.js +2 -2
- package/lib/dist/statusline/active-run.js.map +1 -1
- package/lib/dist/statusline/render.d.ts.map +1 -1
- package/lib/dist/statusline/render.js +41 -5
- package/lib/dist/statusline/render.js.map +1 -1
- package/lib/dist/statusline/types.d.ts +15 -0
- package/lib/dist/statusline/types.d.ts.map +1 -1
- package/lib/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/lib/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/lib/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/lib/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- package/lib/dist/ui/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- package/lib/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/lib/dist/ui/assets/index-D8e4LiVb.js +51 -0
- package/lib/dist/ui/assets/index-ezwX2EfG.css +1 -0
- package/lib/dist/ui/favicon-16.png +0 -0
- package/lib/dist/ui/favicon-180.png +0 -0
- package/lib/dist/ui/favicon-32.png +0 -0
- package/lib/dist/ui/favicon.svg +20 -0
- package/lib/dist/ui/index.html +17 -0
- package/lib/dist/update/resolve-latest.d.ts +14 -0
- package/lib/dist/update/resolve-latest.d.ts.map +1 -0
- package/lib/dist/update/resolve-latest.js +29 -0
- package/lib/dist/update/resolve-latest.js.map +1 -0
- package/lib/dist/update/run-installer.d.ts +13 -0
- package/lib/dist/update/run-installer.d.ts.map +1 -0
- package/lib/dist/update/run-installer.js +43 -0
- package/lib/dist/update/run-installer.js.map +1 -0
- package/lib/dist/update/updater.d.ts +53 -0
- package/lib/dist/update/updater.d.ts.map +1 -0
- package/lib/dist/update/updater.js +164 -0
- package/lib/dist/update/updater.js.map +1 -0
- package/lib/dist/workflows/eta.d.ts +42 -0
- package/lib/dist/workflows/eta.d.ts.map +1 -0
- package/lib/dist/workflows/eta.js +146 -0
- package/lib/dist/workflows/eta.js.map +1 -0
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +25 -6
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/dist/workflows/runners/prompt.d.ts +6 -0
- package/lib/dist/workflows/runners/prompt.d.ts.map +1 -1
- package/lib/dist/workflows/runners/prompt.js +10 -0
- package/lib/dist/workflows/runners/prompt.js.map +1 -1
- package/lib/dist/workflows/runners/types.d.ts +12 -0
- package/lib/dist/workflows/runners/types.d.ts.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +3 -2
- package/package.json +1 -1
- package/lib/dist/server/ssr/md.mjs +0 -83
- package/lib/dist/server/ssr/public/app.css +0 -1
- package/lib/dist/server/ssr/render.mjs +0 -564
- package/lib/dist/server/ssr/routes.js +0 -73
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Endpoints:
|
|
9
9
|
* GET /api/claude/projects — every indexed Claude project
|
|
10
|
-
* GET /api/claude/sessions?project=&limit= — sessions list
|
|
10
|
+
* GET /api/claude/sessions?project=&limit=&range=&model= — sessions list
|
|
11
|
+
* GET /api/claude/prompts/recent?project=&limit= — newest user prompts (⌘K palette)
|
|
11
12
|
* GET /api/claude/session/:id — session detail (prompts + tools)
|
|
12
|
-
* GET /api/claude/heatmap?range=
|
|
13
|
-
* GET /api/claude/costs?range=
|
|
13
|
+
* GET /api/claude/heatmap?range=&project= — file/tool/subagent heatmaps
|
|
14
|
+
* GET /api/claude/costs?range=&project=&model= — cost rollup, timeseries, per-model
|
|
14
15
|
* GET /api/claude/compare — per-project cost comparison
|
|
15
|
-
* GET /api/claude/tips — rule-based tips engine output
|
|
16
|
+
* GET /api/claude/tips — rule-based tips engine output (minus dismissed)
|
|
17
|
+
* POST /api/claude/tips/state — persist a tip's Apply/Dismiss state
|
|
16
18
|
* GET /api/claude/specship-impact?range=&project= — SpecShip token-impact aggregation
|
|
17
19
|
* POST /api/claude/ingest — force a one-shot ingest pass
|
|
18
20
|
*/
|
|
@@ -94,6 +96,7 @@ function getDb(cg) {
|
|
|
94
96
|
return anyCg.queries.db;
|
|
95
97
|
throw new Error('specship DB handle not accessible from server context');
|
|
96
98
|
}
|
|
99
|
+
// @implements REQ-DESKTOP-019 (GET /api/claude/prompts/recent — ⌘K palette prompts source)
|
|
97
100
|
export async function registerClaudeRoutes(app) {
|
|
98
101
|
/**
|
|
99
102
|
* Analytics routes share one SQLite — the boot-time "primary" project's
|
|
@@ -135,20 +138,54 @@ export async function registerClaudeRoutes(app) {
|
|
|
135
138
|
const limit = Math.min(parseInt(req.query.limit ?? '100', 10) || 100, 500);
|
|
136
139
|
const since = rangeStart(rangeKey(req.query.range));
|
|
137
140
|
const params = [since];
|
|
138
|
-
let
|
|
141
|
+
let where = '';
|
|
139
142
|
if (req.query.project) {
|
|
140
|
-
|
|
143
|
+
where += ' AND project_path = ?';
|
|
141
144
|
params.push(normalizeProjectFilter(req.query.project));
|
|
142
145
|
}
|
|
146
|
+
if (req.query.model) {
|
|
147
|
+
where += ' AND last_model = ?';
|
|
148
|
+
params.push(req.query.model);
|
|
149
|
+
}
|
|
143
150
|
params.push(limit);
|
|
144
151
|
const sessions = db.prepare(`
|
|
145
152
|
SELECT * FROM claude_sessions
|
|
146
|
-
WHERE started_at >= ?${
|
|
153
|
+
WHERE started_at >= ?${where}
|
|
147
154
|
ORDER BY started_at DESC
|
|
148
155
|
LIMIT ?
|
|
149
156
|
`).all(...params);
|
|
150
157
|
return { sessions };
|
|
151
158
|
});
|
|
159
|
+
/**
|
|
160
|
+
* Newest user prompts across sessions — feeds the ⌘K command palette's
|
|
161
|
+
* "recent prompts" results (REQ-DESKTOP-019). Main-chain prompts with
|
|
162
|
+
* non-empty text only: sidechain rows are subagent task bodies, and blank
|
|
163
|
+
* rows would render as empty palette entries. Text is capped at 200 chars —
|
|
164
|
+
* the palette shows a one-line label, never the full prompt.
|
|
165
|
+
*/
|
|
166
|
+
app.get('/api/claude/prompts/recent', async (req, reply) => {
|
|
167
|
+
const cg = requirePrimary(reply);
|
|
168
|
+
if (!cg)
|
|
169
|
+
return;
|
|
170
|
+
const db = getDb(cg);
|
|
171
|
+
const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
|
|
172
|
+
const params = [];
|
|
173
|
+
let whereProject = '';
|
|
174
|
+
if (req.query.project) {
|
|
175
|
+
whereProject = ' AND s.project_path = ?';
|
|
176
|
+
params.push(normalizeProjectFilter(req.query.project));
|
|
177
|
+
}
|
|
178
|
+
params.push(limit);
|
|
179
|
+
const prompts = db.prepare(`
|
|
180
|
+
SELECT p.id, p.session_id, substr(p.text, 1, 200) as text, p.ts
|
|
181
|
+
FROM claude_prompts p
|
|
182
|
+
JOIN claude_sessions s ON s.id = p.session_id
|
|
183
|
+
WHERE p.is_sidechain = 0 AND p.text IS NOT NULL AND TRIM(p.text) != ''${whereProject}
|
|
184
|
+
ORDER BY p.ts DESC
|
|
185
|
+
LIMIT ?
|
|
186
|
+
`).all(...params);
|
|
187
|
+
return { prompts };
|
|
188
|
+
});
|
|
152
189
|
app.get('/api/claude/session/:id', async (req, reply) => {
|
|
153
190
|
const cg = requirePrimary(reply);
|
|
154
191
|
if (!cg)
|
|
@@ -392,15 +429,20 @@ export async function registerClaudeRoutes(app) {
|
|
|
392
429
|
return;
|
|
393
430
|
const db = getDb(cg);
|
|
394
431
|
const since = rangeStart(rangeKey(req.query.range));
|
|
432
|
+
// Optional project narrowing (REQ-DESKTOP-024.A2) — tool calls and prompts
|
|
433
|
+
// carry only session_id, so scope through claude_sessions.project_path.
|
|
434
|
+
const project = req.query.project ? normalizeProjectFilter(req.query.project) : undefined;
|
|
435
|
+
const inProject = (col = '') => project ? ` AND ${col}session_id IN (SELECT id FROM claude_sessions WHERE project_path = ?)` : '';
|
|
436
|
+
const withProject = (...lead) => (project ? [...lead, project] : lead);
|
|
395
437
|
// Files heatmap — input_summary doubles as the file path for Read/Edit/Write.
|
|
396
438
|
const files = db.prepare(`
|
|
397
439
|
SELECT input_summary as path, COUNT(*) as calls, SUM(result_length) as resultBytes
|
|
398
440
|
FROM claude_tool_calls
|
|
399
|
-
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
|
|
441
|
+
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''${inProject()}
|
|
400
442
|
GROUP BY input_summary
|
|
401
443
|
ORDER BY calls DESC
|
|
402
444
|
LIMIT 100
|
|
403
|
-
`).all(since);
|
|
445
|
+
`).all(...withProject(since));
|
|
404
446
|
// Per-file 7-day call trend — one sparkline (oldest→newest) per file cell.
|
|
405
447
|
// Always a fixed 7-day window, independent of `range`, so the trend reads
|
|
406
448
|
// consistently regardless of the heatmap's selected window.
|
|
@@ -409,9 +451,9 @@ export async function registerClaudeRoutes(app) {
|
|
|
409
451
|
const trendRows = db.prepare(`
|
|
410
452
|
SELECT input_summary as path, CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as calls
|
|
411
453
|
FROM claude_tool_calls
|
|
412
|
-
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''
|
|
454
|
+
WHERE ts >= ? AND tool_name IN ('Read','Edit','Write','NotebookEdit') AND input_summary != ''${inProject()}
|
|
413
455
|
GROUP BY input_summary, bucket
|
|
414
|
-
`).all(trend7Start, dayMs, trend7Start);
|
|
456
|
+
`).all(...withProject(trend7Start, dayMs, trend7Start));
|
|
415
457
|
const trendByPath = new Map();
|
|
416
458
|
for (const r of trendRows) {
|
|
417
459
|
if (r.bucket < 0 || r.bucket > 6)
|
|
@@ -427,10 +469,10 @@ export async function registerClaudeRoutes(app) {
|
|
|
427
469
|
const tools = db.prepare(`
|
|
428
470
|
SELECT tool_name as name, COUNT(*) as calls, SUM(result_length) as resultBytes
|
|
429
471
|
FROM claude_tool_calls
|
|
430
|
-
WHERE ts >=
|
|
472
|
+
WHERE ts >= ?${inProject()}
|
|
431
473
|
GROUP BY tool_name
|
|
432
474
|
ORDER BY calls DESC
|
|
433
|
-
`).all(since);
|
|
475
|
+
`).all(...withProject(since));
|
|
434
476
|
// Subagent attribution (is_sidechain at the prompt level — main vs. sidechain rollup).
|
|
435
477
|
const subagents = db.prepare(`
|
|
436
478
|
SELECT
|
|
@@ -439,9 +481,9 @@ export async function registerClaudeRoutes(app) {
|
|
|
439
481
|
SUM(p.input_tokens + p.output_tokens + p.cache_creation_tokens + p.cache_read_tokens) as tokens,
|
|
440
482
|
SUM(p.cost_usd) as cost
|
|
441
483
|
FROM claude_prompts p
|
|
442
|
-
WHERE p.ts >=
|
|
484
|
+
WHERE p.ts >= ?${inProject('p.')}
|
|
443
485
|
GROUP BY type
|
|
444
|
-
`).all(since);
|
|
486
|
+
`).all(...withProject(since));
|
|
445
487
|
// Subagent breakdown by name — Task tool calls grouped by subagent_type
|
|
446
488
|
// (defaults to 'general-purpose' when unset). Guarded extraction: Task
|
|
447
489
|
// inputs carry long prompts, so input_summary is almost always truncated.
|
|
@@ -452,10 +494,10 @@ export async function registerClaudeRoutes(app) {
|
|
|
452
494
|
MIN(ts) as firstSeen,
|
|
453
495
|
MAX(ts) as lastSeen
|
|
454
496
|
FROM claude_tool_calls
|
|
455
|
-
WHERE ts >= ? AND tool_name = 'Task'
|
|
497
|
+
WHERE ts >= ? AND tool_name = 'Task'${inProject()}
|
|
456
498
|
GROUP BY name
|
|
457
499
|
ORDER BY calls DESC
|
|
458
|
-
`).all(since);
|
|
500
|
+
`).all(...withProject(since));
|
|
459
501
|
return { files, tools, subagents, subagentByName };
|
|
460
502
|
});
|
|
461
503
|
/**
|
|
@@ -627,40 +669,72 @@ export async function registerClaudeRoutes(app) {
|
|
|
627
669
|
return;
|
|
628
670
|
const db = getDb(cg);
|
|
629
671
|
const since = rangeStart(rangeKey(req.query.range));
|
|
630
|
-
|
|
672
|
+
// Optional narrowing (REQ-DESKTOP-024.A2). Both filters live at the prompt
|
|
673
|
+
// level (project via the owning session), so every prompt query below
|
|
674
|
+
// shares one fragment; the sessions join is only added when needed.
|
|
675
|
+
const project = req.query.project ? normalizeProjectFilter(req.query.project) : undefined;
|
|
676
|
+
const model = req.query.model || undefined;
|
|
677
|
+
const filtered = Boolean(project || model);
|
|
678
|
+
const pJoin = project ? ' JOIN claude_sessions s ON s.id = p.session_id' : '';
|
|
679
|
+
const pFilter = (project ? ' AND s.project_path = ?' : '') + (model ? ' AND p.model = ?' : '');
|
|
680
|
+
const pParams = (...lead) => {
|
|
681
|
+
const out = [...lead];
|
|
682
|
+
if (project)
|
|
683
|
+
out.push(project);
|
|
684
|
+
if (model)
|
|
685
|
+
out.push(model);
|
|
686
|
+
return out;
|
|
687
|
+
};
|
|
688
|
+
// Total spend in the window. Unfiltered keeps the session-level rollup
|
|
689
|
+
// (matches the dashboard's historical numbers, which include prompts with
|
|
690
|
+
// a NULL model); filtered totals must come from the same prompt rows the
|
|
691
|
+
// filters apply to, or the headline wouldn't narrow with the breakdowns.
|
|
692
|
+
const totalOf = (start, end) => {
|
|
693
|
+
if (!filtered) {
|
|
694
|
+
const where = end == null ? 'started_at >= ?' : 'started_at >= ? AND started_at < ?';
|
|
695
|
+
const row = db.prepare(`SELECT SUM(total_cost_usd) as t FROM claude_sessions WHERE ${where}`)
|
|
696
|
+
.get(...(end == null ? [start] : [start, end]));
|
|
697
|
+
return row.t ?? 0;
|
|
698
|
+
}
|
|
699
|
+
const where = end == null ? 'p.ts >= ?' : 'p.ts >= ? AND p.ts < ?';
|
|
700
|
+
const row = db.prepare(`SELECT SUM(p.cost_usd) as t FROM claude_prompts p${pJoin} WHERE ${where}${pFilter}`)
|
|
701
|
+
.get(...pParams(...(end == null ? [start] : [start, end])));
|
|
702
|
+
return row.t ?? 0;
|
|
703
|
+
};
|
|
704
|
+
const total = totalOf(since, null);
|
|
631
705
|
// Week-over-week spend delta: fractional change in total cost vs the prior
|
|
632
706
|
// equal-length window (e.g. -0.08 → "-8%"). Zero for 'all' or no prior data.
|
|
633
707
|
const prior = priorWindow(rangeKey(req.query.range));
|
|
634
708
|
let wowDelta = 0;
|
|
635
709
|
if (prior.end > prior.start) {
|
|
636
|
-
const
|
|
637
|
-
const pt = pTotal.t ?? 0;
|
|
710
|
+
const pt = totalOf(prior.start, prior.end);
|
|
638
711
|
if (pt > 0)
|
|
639
|
-
wowDelta = (
|
|
712
|
+
wowDelta = (total - pt) / pt;
|
|
640
713
|
}
|
|
641
|
-
// Top prompts by cost.
|
|
714
|
+
// Top prompts by cost. is_sidechain rides along so the dashboard's
|
|
715
|
+
// Recent-prompts module can badge subagent turns (REQ-DESKTOP-020).
|
|
642
716
|
const topPrompts = db.prepare(`
|
|
643
|
-
SELECT id, session_id, substr(text, 1, 200) as text, model, cost_usd,
|
|
644
|
-
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ts
|
|
645
|
-
FROM claude_prompts
|
|
646
|
-
WHERE ts >= ? AND cost_usd > 0
|
|
647
|
-
ORDER BY cost_usd DESC
|
|
717
|
+
SELECT p.id, p.session_id, substr(p.text, 1, 200) as text, p.model, p.cost_usd, p.is_sidechain,
|
|
718
|
+
p.input_tokens, p.output_tokens, p.cache_creation_tokens, p.cache_read_tokens, p.ts
|
|
719
|
+
FROM claude_prompts p${pJoin}
|
|
720
|
+
WHERE p.ts >= ? AND p.cost_usd > 0${pFilter}
|
|
721
|
+
ORDER BY p.cost_usd DESC
|
|
648
722
|
LIMIT 50
|
|
649
|
-
`).all(since);
|
|
723
|
+
`).all(...pParams(since));
|
|
650
724
|
// Daily timeseries: bucket by 24h windows.
|
|
651
725
|
const days = 30;
|
|
652
726
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
653
727
|
const dayBoundary = Math.floor(Date.now() / dayMs) * dayMs - (days - 1) * dayMs;
|
|
654
728
|
const series = db.prepare(`
|
|
655
729
|
SELECT
|
|
656
|
-
CAST((ts - ?) / ? AS INTEGER) as bucket,
|
|
657
|
-
SUM(cost_usd) as cost,
|
|
730
|
+
CAST((p.ts - ?) / ? AS INTEGER) as bucket,
|
|
731
|
+
SUM(p.cost_usd) as cost,
|
|
658
732
|
COUNT(*) as prompts
|
|
659
|
-
FROM claude_prompts
|
|
660
|
-
WHERE ts >=
|
|
733
|
+
FROM claude_prompts p${pJoin}
|
|
734
|
+
WHERE p.ts >= ?${pFilter}
|
|
661
735
|
GROUP BY bucket
|
|
662
736
|
ORDER BY bucket ASC
|
|
663
|
-
`).all(dayBoundary, dayMs, dayBoundary);
|
|
737
|
+
`).all(...pParams(dayBoundary, dayMs, dayBoundary));
|
|
664
738
|
// Densify with zeros for missing days.
|
|
665
739
|
const dense = [];
|
|
666
740
|
for (let i = 0; i < days; i++) {
|
|
@@ -669,13 +743,13 @@ export async function registerClaudeRoutes(app) {
|
|
|
669
743
|
}
|
|
670
744
|
// By model.
|
|
671
745
|
const byModel = db.prepare(`
|
|
672
|
-
SELECT model, COUNT(*) as prompts, SUM(cost_usd) as cost
|
|
673
|
-
FROM claude_prompts
|
|
674
|
-
WHERE ts >= ? AND model IS NOT NULL
|
|
675
|
-
GROUP BY model
|
|
746
|
+
SELECT p.model, COUNT(*) as prompts, SUM(p.cost_usd) as cost
|
|
747
|
+
FROM claude_prompts p${pJoin}
|
|
748
|
+
WHERE p.ts >= ? AND p.model IS NOT NULL${pFilter}
|
|
749
|
+
GROUP BY p.model
|
|
676
750
|
ORDER BY cost DESC
|
|
677
|
-
`).all(since);
|
|
678
|
-
return { total
|
|
751
|
+
`).all(...pParams(since));
|
|
752
|
+
return { total, topPrompts, series: dense, byModel, wowDelta };
|
|
679
753
|
});
|
|
680
754
|
/**
|
|
681
755
|
* GET /api/claude/stats?range= — the four dashboard stat tiles, each with a
|
|
@@ -742,7 +816,11 @@ export async function registerClaudeRoutes(app) {
|
|
|
742
816
|
// --- Drift (live graph count, no time series) ---
|
|
743
817
|
const driftCount = cg.getSpecQueries().getLinksByState(['drifted', 'broken', 'orphaned']).length;
|
|
744
818
|
const drift = { value: driftCount, delta: 0, series: [] };
|
|
745
|
-
|
|
819
|
+
// Total ingested sessions (all time, unfiltered). Zero means "nothing
|
|
820
|
+
// ingested yet" — the dashboard renders enable-ingest guidance instead of
|
|
821
|
+
// presenting $0 / 0-call zeros as truth (REQ-DESKTOP-020.A4).
|
|
822
|
+
const sessionCount = countSince(`SELECT COUNT(*) c FROM claude_sessions`);
|
|
823
|
+
return { lastSessionCost, toolCalls, subagentPct, drift, sessionCount };
|
|
746
824
|
});
|
|
747
825
|
app.get('/api/claude/compare', async (_req, reply) => {
|
|
748
826
|
const cg = requirePrimary(reply);
|
|
@@ -898,7 +976,40 @@ export async function registerClaudeRoutes(app) {
|
|
|
898
976
|
// Sort: errors before warns before info, then within bucket by saving heuristic.
|
|
899
977
|
const order = { error: 0, warn: 1, info: 2 };
|
|
900
978
|
tips.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9));
|
|
901
|
-
|
|
979
|
+
// Apply persisted tip state (REQ-DESKTOP-020.A2): dismissed tips never
|
|
980
|
+
// resurface even though the rule re-mines the pattern; applied ones stay
|
|
981
|
+
// in the list annotated so the UI can render their applied treatment.
|
|
982
|
+
const states = db.prepare(`SELECT tip_id, state FROM claude_tip_state`).all();
|
|
983
|
+
const stateById = new Map(states.map((s) => [s.tip_id, s.state]));
|
|
984
|
+
const visible = tips.filter((t) => stateById.get(t.id) !== 'dismissed');
|
|
985
|
+
for (const t of visible) {
|
|
986
|
+
if (stateById.get(t.id) === 'applied')
|
|
987
|
+
t.state = 'applied';
|
|
988
|
+
}
|
|
989
|
+
return { tips: visible };
|
|
990
|
+
});
|
|
991
|
+
/**
|
|
992
|
+
* Persist a tip's Apply / Dismiss state (REQ-DESKTOP-020.A2). Tip ids embed
|
|
993
|
+
* `/` and `:` (rule + session + file), so the id travels in the JSON body
|
|
994
|
+
* rather than a path param. Upsert: re-applying or re-dismissing converges
|
|
995
|
+
* to one row per tip.
|
|
996
|
+
*/
|
|
997
|
+
app.post('/api/claude/tips/state', async (req, reply) => {
|
|
998
|
+
const cg = requirePrimary(reply);
|
|
999
|
+
if (!cg)
|
|
1000
|
+
return;
|
|
1001
|
+
const { id, state } = req.body ?? {};
|
|
1002
|
+
if (!id || typeof id !== 'string')
|
|
1003
|
+
return reply.code(400).send({ error: 'id required' });
|
|
1004
|
+
if (state !== 'applied' && state !== 'dismissed') {
|
|
1005
|
+
return reply.code(400).send({ error: "state must be 'applied' or 'dismissed'" });
|
|
1006
|
+
}
|
|
1007
|
+
const db = getDb(cg);
|
|
1008
|
+
db.prepare(`
|
|
1009
|
+
INSERT INTO claude_tip_state (tip_id, state, ts) VALUES (?, ?, ?)
|
|
1010
|
+
ON CONFLICT(tip_id) DO UPDATE SET state = excluded.state, ts = excluded.ts
|
|
1011
|
+
`).run(id, state, Date.now());
|
|
1012
|
+
return { ok: true, id, state };
|
|
902
1013
|
});
|
|
903
1014
|
/**
|
|
904
1015
|
* GET /api/claude/specship-impact?range=&project=
|
|
@@ -906,7 +1017,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
906
1017
|
* Aggregate SpecShip token-impact metrics: how many tokens specship calls
|
|
907
1018
|
* spent vs. how many tokens' worth of Read calls they displaced (saved),
|
|
908
1019
|
* with per-prompt dedup so a file referenced by two specship calls in the
|
|
909
|
-
* same prompt counts only once. See
|
|
1020
|
+
* same prompt counts only once. See server/src/ingest/impact-query.ts
|
|
910
1021
|
* for the full algorithm.
|
|
911
1022
|
*
|
|
912
1023
|
* Query params:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET/PUT /api/config — the dashboard-visible server runtime configuration
|
|
3
|
+
* (REQ-DESKTOP-028.A2/.A3). Currently one knob: the Claude Code transcript
|
|
4
|
+
* ingest toggle, persisted to `~/.specship/server-config.json` so it
|
|
5
|
+
* survives restarts, plus the real product version for the About section.
|
|
6
|
+
*
|
|
7
|
+
* PUT flips the live watcher through `app.ingestControl` (server.ts): off
|
|
8
|
+
* stops the JSONL watcher immediately, on starts it (when a primary project
|
|
9
|
+
* with a DB handle exists — otherwise the intent is recorded and honored on
|
|
10
|
+
* the next boot with a primary).
|
|
11
|
+
*/
|
|
12
|
+
import { productVersion } from '../product-version.js';
|
|
13
|
+
import { writeServerConfig } from '../server-config.js';
|
|
14
|
+
export async function registerConfigRoutes(app) {
|
|
15
|
+
app.get('/api/config', async () => ({
|
|
16
|
+
ingestEnabled: app.ingestControl.enabled(),
|
|
17
|
+
version: productVersion(),
|
|
18
|
+
}));
|
|
19
|
+
app.put('/api/config', async (req, reply) => {
|
|
20
|
+
const body = (req.body ?? {});
|
|
21
|
+
if (typeof body.ingestEnabled !== 'boolean') {
|
|
22
|
+
return reply.code(400).send({ error: 'ingestEnabled must be a boolean', code: 'bad_request' });
|
|
23
|
+
}
|
|
24
|
+
writeServerConfig({ ingestEnabled: body.ingestEnabled });
|
|
25
|
+
if (body.ingestEnabled)
|
|
26
|
+
await app.ingestControl.start();
|
|
27
|
+
else
|
|
28
|
+
app.ingestControl.stop();
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
ingestEnabled: app.ingestControl.enabled(),
|
|
32
|
+
version: productVersion(),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -160,6 +160,7 @@ export async function registerGraphRoutes(app) {
|
|
|
160
160
|
* - linkHealth: spec-link counts per state (verified/drifted/broken/orphaned/…)
|
|
161
161
|
* - edgeKinds: edge counts grouped into calls / implements-documents / tests
|
|
162
162
|
* - hubs: the most-connected nodes (by total degree), for the "Most connected" list
|
|
163
|
+
* - anchored: nodes that are spec_links targets (by degree), for the "Anchored" list
|
|
163
164
|
*/
|
|
164
165
|
app.get('/api/graph/health', async (req, reply) => {
|
|
165
166
|
const cg = await resolveCg(app, req, reply);
|
|
@@ -206,7 +207,27 @@ export async function registerGraphRoutes(app) {
|
|
|
206
207
|
JOIN nodes n ON n.id = deg.node
|
|
207
208
|
ORDER BY deg.degree DESC
|
|
208
209
|
`).all();
|
|
209
|
-
|
|
210
|
+
// Anchored nodes — code that specs point at (resolved spec_links targets),
|
|
211
|
+
// most-connected first. Degree is LEFT-joined: an anchored leaf with no
|
|
212
|
+
// edges still shows up, at degree 0.
|
|
213
|
+
const anchored = db.prepare(`
|
|
214
|
+
SELECT n.id, n.name, n.kind, n.file_path as filePath, COALESCE(deg.degree, 0) as degree
|
|
215
|
+
FROM (
|
|
216
|
+
SELECT DISTINCT resolved_node_id AS node FROM spec_links
|
|
217
|
+
WHERE resolved_node_id IS NOT NULL
|
|
218
|
+
) sl
|
|
219
|
+
JOIN nodes n ON n.id = sl.node
|
|
220
|
+
LEFT JOIN (
|
|
221
|
+
SELECT node, COUNT(*) as degree FROM (
|
|
222
|
+
SELECT source as node FROM edges
|
|
223
|
+
UNION ALL
|
|
224
|
+
SELECT target as node FROM edges
|
|
225
|
+
) GROUP BY node
|
|
226
|
+
) deg ON deg.node = n.id
|
|
227
|
+
ORDER BY degree DESC
|
|
228
|
+
LIMIT 8
|
|
229
|
+
`).all();
|
|
230
|
+
return { linkHealth, edgeKinds: edgeKindMap, hubs, anchored };
|
|
210
231
|
});
|
|
211
232
|
/**
|
|
212
233
|
* Full-graph overview for the Graph page's whole-repo view. The graph can be
|