@specship/specship-win32-arm64 0.11.8 → 0.12.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 +141 -66
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/check.md +5 -0
- package/lib/dist/commands/specship/spec.md +235 -15
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +1 -0
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/enforce/enforce.d.ts +15 -0
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +43 -1
- package/lib/dist/enforce/enforce.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +6 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/index.d.ts +9 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +53 -9
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +35 -0
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +2 -2
- package/lib/dist/installer/instructions-template.js +3 -3
- package/lib/dist/installer/targets/claude.d.ts +10 -0
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +33 -7
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/mcp/designer-tools.js +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +1 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +1 -1
- package/lib/dist/mcp/spec-tools.d.ts +10 -0
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +228 -2
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +12 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +34 -1
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/reflect/index.d.ts +1 -1
- package/lib/dist/reflect/index.d.ts.map +1 -1
- package/lib/dist/reflect/index.js +2 -1
- package/lib/dist/reflect/index.js.map +1 -1
- package/lib/dist/reflect/miner.d.ts +8 -0
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +64 -36
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/resolution/brief-link-resolver.d.ts +20 -0
- package/lib/dist/resolution/brief-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/brief-link-resolver.js +65 -1
- package/lib/dist/resolution/brief-link-resolver.js.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +16 -2
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +19 -1
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/dist/server/cli.js +5 -49
- package/lib/dist/server/ingest/ingestor.js +17 -17
- package/lib/dist/server/ingest/pricing-backfill.js +55 -0
- package/lib/dist/server/ingest/pricing.js +3 -0
- package/lib/dist/server/ingest/project-paths.js +152 -0
- package/lib/dist/server/project-registry.js +15 -2
- package/lib/dist/server/routes/claude.js +47 -18
- package/lib/dist/server/routes/events.js +20 -1
- package/lib/dist/server/routes/projects.js +6 -3
- package/lib/dist/server/routes/spec.js +22 -2
- package/lib/dist/server/server.js +38 -4
- package/lib/dist/server/ssr/md.mjs +83 -0
- package/lib/dist/server/ssr/public/app.css +1 -0
- package/lib/dist/server/ssr/render.mjs +564 -0
- package/lib/dist/server/ssr/routes.js +73 -0
- package/lib/dist/server/static-handler.js +30 -0
- package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -3
- package/lib/dist/workflows/defaults/spec-implement.yaml +41 -9
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +3 -3
- package/package.json +1 -1
- package/lib/dist/commands/specship/design-implement.md +0 -84
- package/lib/dist/commands/specship/design-loop.md +0 -125
- package/lib/dist/commands/ss-behaviour.md +0 -116
- package/lib/dist/web/chunk-2AJCHB7P.js +0 -1
- package/lib/dist/web/chunk-2CPLUFCH.js +0 -2
- package/lib/dist/web/chunk-2I7L37NS.js +0 -1
- package/lib/dist/web/chunk-2IVSOVQZ.js +0 -1
- package/lib/dist/web/chunk-2NAWAJB5.js +0 -1
- package/lib/dist/web/chunk-2OJBIPE4.js +0 -1
- package/lib/dist/web/chunk-3E2WB6D5.js +0 -1
- package/lib/dist/web/chunk-3EBFYSCH.js +0 -2
- package/lib/dist/web/chunk-3QCQ4BXS.js +0 -1
- package/lib/dist/web/chunk-42XVAQ6I.js +0 -1
- package/lib/dist/web/chunk-4G7XU55L.js +0 -1
- package/lib/dist/web/chunk-4IMMPEYM.js +0 -1
- package/lib/dist/web/chunk-4TJQJPCZ.js +0 -1
- package/lib/dist/web/chunk-4WZIHTPC.js +0 -1
- package/lib/dist/web/chunk-4YVSYOSD.js +0 -1
- package/lib/dist/web/chunk-5BQIOYKW.js +0 -1
- package/lib/dist/web/chunk-5HGWHUJA.js +0 -1
- package/lib/dist/web/chunk-5NTGUSXJ.js +0 -1
- package/lib/dist/web/chunk-5Y244R4G.js +0 -1
- package/lib/dist/web/chunk-6RRDPT5Z.js +0 -1
- package/lib/dist/web/chunk-6VKB2ZWM.js +0 -1
- package/lib/dist/web/chunk-7656MA24.js +0 -1
- package/lib/dist/web/chunk-7DMFVTU4.js +0 -1
- package/lib/dist/web/chunk-7KWBINA7.js +0 -1
- package/lib/dist/web/chunk-7MUNFW56.js +0 -1
- package/lib/dist/web/chunk-7SMPKVEP.js +0 -1
- package/lib/dist/web/chunk-AZJVTPLU.js +0 -1
- package/lib/dist/web/chunk-AZUUBUEJ.js +0 -1
- package/lib/dist/web/chunk-BLBRMCN2.js +0 -1
- package/lib/dist/web/chunk-BMIAXD2V.js +0 -2
- package/lib/dist/web/chunk-BPCJLNBS.js +0 -47
- package/lib/dist/web/chunk-BUXWEHIY.js +0 -1
- package/lib/dist/web/chunk-CYXQILZI.js +0 -1
- package/lib/dist/web/chunk-DLQPZWSI.css +0 -1
- package/lib/dist/web/chunk-DOKZOFKM.js +0 -1
- package/lib/dist/web/chunk-DTWIJ3GJ.js +0 -1
- package/lib/dist/web/chunk-DYRFLPJA.js +0 -1
- package/lib/dist/web/chunk-E27QAKAB.js +0 -1
- package/lib/dist/web/chunk-E3J3CXR5.js +0 -1
- package/lib/dist/web/chunk-E73OX2P7.js +0 -1
- package/lib/dist/web/chunk-EAXRKDLV.js +0 -1
- package/lib/dist/web/chunk-EBKKDHYI.js +0 -1
- package/lib/dist/web/chunk-EE7V7Q5P.js +0 -1
- package/lib/dist/web/chunk-EKY2FUHU.js +0 -1
- package/lib/dist/web/chunk-EP6XOPXH.js +0 -1
- package/lib/dist/web/chunk-ESGDLJOJ.js +0 -1
- package/lib/dist/web/chunk-ETJG7NCY.js +0 -1
- package/lib/dist/web/chunk-EUUEFEDI.js +0 -1
- package/lib/dist/web/chunk-EXBUKHH2.js +0 -17
- package/lib/dist/web/chunk-F5TBUYDH.js +0 -1
- package/lib/dist/web/chunk-FC3R7YHR.js +0 -4
- package/lib/dist/web/chunk-FGNZDHTL.js +0 -11
- package/lib/dist/web/chunk-FIJW2UNJ.js +0 -1
- package/lib/dist/web/chunk-FMV5PXRC.js +0 -5
- package/lib/dist/web/chunk-G7RXFHYO.js +0 -1
- package/lib/dist/web/chunk-G7VZT5KB.js +0 -3
- package/lib/dist/web/chunk-GRZYXPSO.js +0 -7
- package/lib/dist/web/chunk-GYGPS3AN.js +0 -1
- package/lib/dist/web/chunk-H723TYYD.js +0 -2
- package/lib/dist/web/chunk-H7AF7YS4.js +0 -1
- package/lib/dist/web/chunk-HDZDQILN.js +0 -1
- package/lib/dist/web/chunk-HMK6UO6N.js +0 -1
- package/lib/dist/web/chunk-HSJNHNOH.js +0 -6
- package/lib/dist/web/chunk-IHEE5NYJ.js +0 -1
- package/lib/dist/web/chunk-IOB4JJT4.js +0 -1
- package/lib/dist/web/chunk-ISNEBICW.js +0 -1
- package/lib/dist/web/chunk-J2GZVLHH.js +0 -1
- package/lib/dist/web/chunk-JGDK6YKG.js +0 -1
- package/lib/dist/web/chunk-JLOJCDIY.js +0 -1
- package/lib/dist/web/chunk-JPSYO24K.js +0 -1
- package/lib/dist/web/chunk-JTFXTIPE.js +0 -903
- package/lib/dist/web/chunk-K7UD24Z4.js +0 -1
- package/lib/dist/web/chunk-KYSTCHAH.js +0 -1
- package/lib/dist/web/chunk-LB6JPLX2.js +0 -1
- package/lib/dist/web/chunk-LNSVDHCI.js +0 -1
- package/lib/dist/web/chunk-LVGIY3SO.js +0 -1
- package/lib/dist/web/chunk-LXLHIHEN.js +0 -1
- package/lib/dist/web/chunk-MNFTHCSJ.js +0 -1
- package/lib/dist/web/chunk-N6SS4G6S.js +0 -1
- package/lib/dist/web/chunk-NKPK3BR3.js +0 -1
- package/lib/dist/web/chunk-NTBJG6SJ.js +0 -1
- package/lib/dist/web/chunk-NUDB3Q2Y.js +0 -3
- package/lib/dist/web/chunk-OTQR5VFM.js +0 -1
- package/lib/dist/web/chunk-OXEF5E3E.js +0 -1
- package/lib/dist/web/chunk-P62EJGSF.js +0 -1
- package/lib/dist/web/chunk-PCW6R7OP.js +0 -1
- package/lib/dist/web/chunk-PGGJPDJG.js +0 -1
- package/lib/dist/web/chunk-POCVU4VK.js +0 -2
- package/lib/dist/web/chunk-PS6HRYQQ.js +0 -3
- package/lib/dist/web/chunk-PUYSJNJR.js +0 -1
- package/lib/dist/web/chunk-PZSU53SS.js +0 -1
- package/lib/dist/web/chunk-Q2RVFS45.js +0 -1
- package/lib/dist/web/chunk-Q7L6LLAK.js +0 -1
- package/lib/dist/web/chunk-QCMKJIWY.js +0 -1
- package/lib/dist/web/chunk-QH6CF3M3.js +0 -1
- package/lib/dist/web/chunk-QIZ65CGB.js +0 -1
- package/lib/dist/web/chunk-QPA2REUE.js +0 -1
- package/lib/dist/web/chunk-QQ5LD7PI.js +0 -1
- package/lib/dist/web/chunk-QR6L3KAC.js +0 -1
- package/lib/dist/web/chunk-R2DLK4HO.js +0 -1
- package/lib/dist/web/chunk-RD6TVPOT.js +0 -1
- package/lib/dist/web/chunk-RKY4EJYJ.js +0 -1
- package/lib/dist/web/chunk-RONYWVY7.js +0 -1
- package/lib/dist/web/chunk-RXKXYF2C.js +0 -1
- package/lib/dist/web/chunk-SQPYSOV6.js +0 -1
- package/lib/dist/web/chunk-SWKJRNYY.js +0 -1
- package/lib/dist/web/chunk-SYGIPC2P.js +0 -4
- package/lib/dist/web/chunk-T7AZ65JP.js +0 -1
- package/lib/dist/web/chunk-TCZDVOHD.js +0 -1
- package/lib/dist/web/chunk-TPTITA3V.js +0 -1
- package/lib/dist/web/chunk-TR335633.js +0 -1
- package/lib/dist/web/chunk-TWVVIWKO.js +0 -1
- package/lib/dist/web/chunk-U67LRNER.js +0 -1
- package/lib/dist/web/chunk-UR5KDXPX.js +0 -1
- package/lib/dist/web/chunk-UR6O2GEH.js +0 -1
- package/lib/dist/web/chunk-USVLLFVR.js +0 -1
- package/lib/dist/web/chunk-UTNMGWTP.js +0 -1
- package/lib/dist/web/chunk-V74ZPP4B.js +0 -1
- package/lib/dist/web/chunk-VECWMHJP.js +0 -1
- package/lib/dist/web/chunk-VUACT35R.js +0 -3
- package/lib/dist/web/chunk-VZI7H4SZ.js +0 -1
- package/lib/dist/web/chunk-WB6YHOD4.js +0 -1
- package/lib/dist/web/chunk-WBJ5VEB5.js +0 -1
- package/lib/dist/web/chunk-WBT64AWV.js +0 -1
- package/lib/dist/web/chunk-WFXJIXZE.js +0 -4
- package/lib/dist/web/chunk-WTGYRH3Z.js +0 -298
- package/lib/dist/web/chunk-WXTCVDTP.js +0 -1
- package/lib/dist/web/chunk-XAUQEKZX.js +0 -1
- package/lib/dist/web/chunk-XCDHWLVH.js +0 -1
- package/lib/dist/web/chunk-Y3H6FFUZ.js +0 -1
- package/lib/dist/web/chunk-Y4F5ULGJ.js +0 -1
- package/lib/dist/web/chunk-YEGKAAEE.js +0 -1
- package/lib/dist/web/chunk-YM2KU57F.js +0 -1
- package/lib/dist/web/chunk-YNYVOPWF.js +0 -1
- package/lib/dist/web/chunk-YRERBP6T.js +0 -1
- package/lib/dist/web/chunk-YV5LB22R.js +0 -1
- package/lib/dist/web/chunk-YWIRU745.js +0 -1
- package/lib/dist/web/chunk-YXYPTZ6E.js +0 -1
- package/lib/dist/web/chunk-ZLV4VCDG.js +0 -3
- package/lib/dist/web/chunk-ZTVI5KFF.js +0 -1
- package/lib/dist/web/favicon-16.png +0 -0
- package/lib/dist/web/favicon-180.png +0 -0
- package/lib/dist/web/favicon-32.png +0 -0
- package/lib/dist/web/favicon-512.png +0 -0
- package/lib/dist/web/favicon-small.svg +0 -15
- package/lib/dist/web/favicon.ico +0 -0
- package/lib/dist/web/favicon.svg +0 -20
- package/lib/dist/web/icon-192.png +0 -0
- package/lib/dist/web/icon-512.png +0 -0
- package/lib/dist/web/index.html +0 -146
- package/lib/dist/web/main-ESADRXN2.css +0 -1
- package/lib/dist/web/main-FY3HQ4PX.js +0 -1
- package/lib/dist/web/manifest.webmanifest +0 -15
- package/lib/dist/web/media/codicon-LN6W7LCM.ttf +0 -0
- package/lib/dist/web/styles-KSOPUVDA.css +0 -1
- package/lib/dist/web/sw.js +0 -69
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-link-resolver.js","sourceRoot":"","sources":["../../src/resolution/spec-link-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;
|
|
1
|
+
{"version":3,"file":"spec-link-resolver.js","sourceRoot":"","sources":["../../src/resolution/spec-link-resolver.ts"],"names":[],"mappings":"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"}
|
package/lib/dist/server/cli.js
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
* The JSONL ingest watcher is started inside `createServer()` by default —
|
|
12
12
|
* pass `--no-ingest` to disable.
|
|
13
13
|
*/
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
14
|
import { spawn } from 'node:child_process';
|
|
18
15
|
import { createServer } from './server.js';
|
|
19
16
|
function parseArgs() {
|
|
@@ -64,45 +61,6 @@ Options:
|
|
|
64
61
|
}
|
|
65
62
|
return args;
|
|
66
63
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Locate the built Angular UI. Tries (in order):
|
|
69
|
-
* 1. Explicit --web-dir / env var
|
|
70
|
-
* 2. Sibling `public/web/` next to the running script (production layout)
|
|
71
|
-
* 3. Sibling `../web-ng/dist/web-ng/browser/` (monorepo dev layout)
|
|
72
|
-
* 4. Walk up looking for `packages/web-ng/dist/web-ng/browser/`
|
|
73
|
-
* Returns null when nothing is found — the server then runs API-only.
|
|
74
|
-
*/
|
|
75
|
-
function locateWebDir(explicit) {
|
|
76
|
-
const envDir = process.env.SPECSHIP_WEB_DIR ?? process.env.SPECSHIP_WEB_DIR;
|
|
77
|
-
const candidates = [];
|
|
78
|
-
if (explicit)
|
|
79
|
-
candidates.push(path.resolve(explicit));
|
|
80
|
-
if (envDir)
|
|
81
|
-
candidates.push(path.resolve(envDir));
|
|
82
|
-
// __dirname equivalent in ESM
|
|
83
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
84
|
-
// Production layout: <pkg>/dist/cli.js, UI shipped at <pkg>/public/web/
|
|
85
|
-
candidates.push(path.resolve(here, '..', 'public', 'web'));
|
|
86
|
-
candidates.push(path.resolve(here, '..', '..', 'public', 'web'));
|
|
87
|
-
// Monorepo dev layout: packages/server/dist/cli.js → packages/web-ng/dist/web-ng/browser
|
|
88
|
-
candidates.push(path.resolve(here, '..', '..', 'web-ng', 'dist', 'web-ng', 'browser'));
|
|
89
|
-
candidates.push(path.resolve(here, '..', '..', '..', 'web-ng', 'dist', 'web-ng', 'browser'));
|
|
90
|
-
// Walk up from cwd looking for the monorepo layout (handy when run via npx
|
|
91
|
-
// inside a checkout).
|
|
92
|
-
let cur = process.cwd();
|
|
93
|
-
for (let depth = 0; depth < 6; depth++) {
|
|
94
|
-
candidates.push(path.join(cur, 'packages', 'web-ng', 'dist', 'web-ng', 'browser'));
|
|
95
|
-
const parent = path.dirname(cur);
|
|
96
|
-
if (parent === cur)
|
|
97
|
-
break;
|
|
98
|
-
cur = parent;
|
|
99
|
-
}
|
|
100
|
-
for (const c of candidates) {
|
|
101
|
-
if (existsSync(path.join(c, 'index.html')))
|
|
102
|
-
return c;
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
64
|
function tryOpenInBrowser(url) {
|
|
107
65
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
108
66
|
try {
|
|
@@ -114,26 +72,24 @@ function tryOpenInBrowser(url) {
|
|
|
114
72
|
}
|
|
115
73
|
async function main() {
|
|
116
74
|
const args = parseArgs();
|
|
117
|
-
const
|
|
75
|
+
const serveUi = !args.noWeb;
|
|
118
76
|
const handle = await createServer({
|
|
119
77
|
projectRoot: args.projectRoot ?? undefined,
|
|
120
78
|
host: args.host,
|
|
121
79
|
port: args.port,
|
|
122
80
|
ingest: args.ingest,
|
|
123
81
|
verbose: args.verbose,
|
|
124
|
-
webDir,
|
|
82
|
+
webDir: null,
|
|
83
|
+
ssr: serveUi,
|
|
125
84
|
});
|
|
126
85
|
console.error(`[specship-desktop] listening on ${handle.url}`);
|
|
127
86
|
if (args.projectRoot) {
|
|
128
87
|
console.error(`[specship-desktop] project: ${args.projectRoot}`);
|
|
129
88
|
}
|
|
130
89
|
else {
|
|
131
|
-
console.error(`[specship-desktop] project: (none — pick one in the
|
|
90
|
+
console.error(`[specship-desktop] project: (none — pick one in the dashboard)`);
|
|
132
91
|
}
|
|
133
|
-
|
|
134
|
-
console.error(`[specship-desktop] UI: ${webDir}`);
|
|
135
|
-
else
|
|
136
|
-
console.error(`[specship-desktop] UI: (none — running headless)`);
|
|
92
|
+
console.error(`[specship-desktop] UI: ${serveUi ? handle.url + '/ (server-rendered)' : '(none — running headless)'}`);
|
|
137
93
|
if (args.ingest)
|
|
138
94
|
console.error(`[specship-desktop] Claude Code transcript ingest active`);
|
|
139
95
|
if (args.open)
|
|
@@ -97,6 +97,7 @@ export function listTranscriptFiles(claudeRoot) {
|
|
|
97
97
|
* the ingestor seeds on first pass instead of failing silently.
|
|
98
98
|
*/
|
|
99
99
|
const DEFAULT_PRICING = [
|
|
100
|
+
['claude-fable-5', 10.0, 50.0, 12.5, 1.0],
|
|
100
101
|
['claude-opus-4-7', 15.0, 75.0, 18.75, 1.5],
|
|
101
102
|
['claude-opus-4', 15.0, 75.0, 18.75, 1.5],
|
|
102
103
|
['claude-sonnet-4-6', 3.0, 15.0, 3.75, 0.3],
|
|
@@ -105,25 +106,24 @@ const DEFAULT_PRICING = [
|
|
|
105
106
|
['claude-haiku-4-5', 0.80, 4.0, 1.0, 0.08],
|
|
106
107
|
['claude-haiku-4', 0.80, 4.0, 1.0, 0.08],
|
|
107
108
|
];
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Load the pricing table once per ingest pass. Always INSERT OR IGNOREs the
|
|
111
|
+
* default rows first — self-healing for DBs seeded by an older binary that
|
|
112
|
+
* lacked newer model families (e.g. fable), while user-edited rates survive
|
|
113
|
+
* untouched (REQ-DASHINT-001).
|
|
114
|
+
*/
|
|
115
|
+
export function loadPricing(db) {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const ins = db.prepare(`
|
|
118
|
+
INSERT OR IGNORE INTO claude_pricing
|
|
119
|
+
(model, input_per_mtok, output_per_mtok, cache_creation_per_mtok, cache_read_per_mtok, updated_at)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
121
|
+
`);
|
|
122
|
+
for (const r of DEFAULT_PRICING)
|
|
123
|
+
ins.run(r[0], r[1], r[2], r[3], r[4], now);
|
|
124
|
+
return db
|
|
111
125
|
.prepare('SELECT model, input_per_mtok, output_per_mtok, cache_creation_per_mtok, cache_read_per_mtok FROM claude_pricing')
|
|
112
126
|
.all();
|
|
113
|
-
if (rows.length === 0) {
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
const ins = db.prepare(`
|
|
116
|
-
INSERT OR IGNORE INTO claude_pricing
|
|
117
|
-
(model, input_per_mtok, output_per_mtok, cache_creation_per_mtok, cache_read_per_mtok, updated_at)
|
|
118
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
119
|
-
`);
|
|
120
|
-
for (const r of DEFAULT_PRICING)
|
|
121
|
-
ins.run(r[0], r[1], r[2], r[3], r[4], now);
|
|
122
|
-
rows = db
|
|
123
|
-
.prepare('SELECT model, input_per_mtok, output_per_mtok, cache_creation_per_mtok, cache_read_per_mtok FROM claude_pricing')
|
|
124
|
-
.all();
|
|
125
|
-
}
|
|
126
|
-
return rows;
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* Read a file's tail from `fromOffset` to end. Returns the full text plus
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-cost prompts that were ingested while their model had no pricing row
|
|
3
|
+
* (REQ-DASHINT-001.A2). Before the fable family was priced, every
|
|
4
|
+
* `claude-fable-5` prompt landed with `cost_usd = 0` despite real token
|
|
5
|
+
* usage — poisoning session totals, the dashboard's last-session tile, and
|
|
6
|
+
* the Costs/Compare rankings.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: only rows with `cost_usd = 0` AND non-zero tokens are
|
|
9
|
+
* touched, and only when their model now resolves to a pricing row with a
|
|
10
|
+
* non-zero computed cost. Session totals are bumped by the same delta the
|
|
11
|
+
* prompt rows gained. Run once per server boot (cheap: indexed scan).
|
|
12
|
+
*/
|
|
13
|
+
import { loadPricing } from './ingestor.js';
|
|
14
|
+
import { computeCost, resolvePricing } from './pricing.js';
|
|
15
|
+
/** Returns the number of prompt rows re-costed. */
|
|
16
|
+
export function recostUnpricedPrompts(db) {
|
|
17
|
+
const pricingRows = loadPricing(db);
|
|
18
|
+
const candidates = db
|
|
19
|
+
.prepare(`
|
|
20
|
+
SELECT id, session_id, model, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
|
|
21
|
+
FROM claude_prompts
|
|
22
|
+
WHERE cost_usd = 0 AND model IS NOT NULL
|
|
23
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_creation_tokens > 0 OR cache_read_tokens > 0)
|
|
24
|
+
`)
|
|
25
|
+
.all();
|
|
26
|
+
if (candidates.length === 0)
|
|
27
|
+
return 0;
|
|
28
|
+
const updatePrompt = db.prepare('UPDATE claude_prompts SET cost_usd = ? WHERE id = ?');
|
|
29
|
+
const bumpSession = db.prepare('UPDATE claude_sessions SET total_cost_usd = total_cost_usd + ? WHERE id = ?');
|
|
30
|
+
let updated = 0;
|
|
31
|
+
const run = db.transaction(() => {
|
|
32
|
+
const sessionDeltas = new Map();
|
|
33
|
+
for (const row of candidates) {
|
|
34
|
+
const pricing = resolvePricing(row.model, pricingRows);
|
|
35
|
+
if (!pricing)
|
|
36
|
+
continue;
|
|
37
|
+
const cost = computeCost({
|
|
38
|
+
input_tokens: row.input_tokens,
|
|
39
|
+
output_tokens: row.output_tokens,
|
|
40
|
+
cache_creation_input_tokens: row.cache_creation_tokens,
|
|
41
|
+
cache_read_input_tokens: row.cache_read_tokens,
|
|
42
|
+
}, pricing);
|
|
43
|
+
if (cost <= 0)
|
|
44
|
+
continue;
|
|
45
|
+
updatePrompt.run(cost, row.id);
|
|
46
|
+
sessionDeltas.set(row.session_id, (sessionDeltas.get(row.session_id) ?? 0) + cost);
|
|
47
|
+
updated++;
|
|
48
|
+
}
|
|
49
|
+
for (const [sessionId, delta] of sessionDeltas) {
|
|
50
|
+
bumpSession.run(delta, sessionId);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
run();
|
|
54
|
+
return updated;
|
|
55
|
+
}
|
|
@@ -15,6 +15,9 @@ const FAMILY_FALLBACKS = {
|
|
|
15
15
|
opus: 'claude-opus-4',
|
|
16
16
|
sonnet: 'claude-sonnet-4',
|
|
17
17
|
haiku: 'claude-haiku-4',
|
|
18
|
+
fable: 'claude-fable-5',
|
|
19
|
+
// Claude Mythos shares Fable's capabilities and pricing.
|
|
20
|
+
mythos: 'claude-fable-5',
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
23
|
* Normalize a Claude model ID:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug→path resolution for Claude Code project dirs (REQ-SLUGRES-001).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code names `~/.claude/projects/<slug>` by replacing every character
|
|
5
|
+
* outside [A-Za-z0-9] in the project's absolute path with '-'. The encoding
|
|
6
|
+
* is lossy — '-', '/', '.', '_' all collapse into '-' — so decoding by
|
|
7
|
+
* mapping '-' back to '/' mangles any real path containing those characters
|
|
8
|
+
* (`/Users/a/dev/claude-projects/x` → `/Users/a/dev/claude/projects/x`).
|
|
9
|
+
*
|
|
10
|
+
* Real paths are recovered from authoritative local sources instead:
|
|
11
|
+
* 1. `~/.claude.json` — its `projects` object is keyed by real absolute
|
|
12
|
+
* paths. We slug-encode each key into a reverse index.
|
|
13
|
+
* 2. Transcript sniffing — JSONL lines carry a `"cwd"` field; a cwd (or
|
|
14
|
+
* one of its ancestors) whose encoding equals the slug is the real path.
|
|
15
|
+
* 3. The legacy lossy decode, preserving behavior for hyphen-free paths.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { decodeProjectSlug } from './ingestor.js';
|
|
21
|
+
/** Claude Code's encoding: every char outside [A-Za-z0-9] becomes '-'. */
|
|
22
|
+
export function encodeProjectSlug(realPath) {
|
|
23
|
+
return realPath.replace(/[^A-Za-z0-9]/g, '-');
|
|
24
|
+
}
|
|
25
|
+
/** Max bytes read per transcript while sniffing for a cwd line. */
|
|
26
|
+
const SNIFF_BYTES = 64 * 1024;
|
|
27
|
+
/** Newest transcripts to try per slug before giving up. */
|
|
28
|
+
const SNIFF_FILES = 3;
|
|
29
|
+
// Keyed by claudeJsonPath so tests with distinct temp files don't collide.
|
|
30
|
+
const indexCache = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Reverse index of slug → real path from `~/.claude.json`'s projects keys.
|
|
33
|
+
* Cached per file and refreshed when the file's mtime changes. Any read or
|
|
34
|
+
* parse failure yields an empty index (the caller falls through to sniffing).
|
|
35
|
+
*/
|
|
36
|
+
function claudeJsonIndex(claudeJsonPath) {
|
|
37
|
+
let mtimeMs;
|
|
38
|
+
try {
|
|
39
|
+
mtimeMs = fs.statSync(claudeJsonPath).mtimeMs;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return new Map();
|
|
43
|
+
}
|
|
44
|
+
const cached = indexCache.get(claudeJsonPath);
|
|
45
|
+
if (cached && cached.mtimeMs === mtimeMs)
|
|
46
|
+
return cached.bySlug;
|
|
47
|
+
const bySlug = new Map();
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
|
|
50
|
+
for (const realPath of Object.keys(parsed.projects ?? {})) {
|
|
51
|
+
bySlug.set(encodeProjectSlug(realPath), realPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* malformed — serve the empty index, retry on next mtime change */
|
|
56
|
+
}
|
|
57
|
+
indexCache.set(claudeJsonPath, { mtimeMs, bySlug });
|
|
58
|
+
return bySlug;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Scan the newest transcripts in `<claudeRoot>/<slug>/` for a `"cwd"` whose
|
|
62
|
+
* encoding matches the slug. Bounded read (first SNIFF_BYTES of up to
|
|
63
|
+
* SNIFF_FILES files). Also accepts an ancestor of the cwd — sessions can
|
|
64
|
+
* record a subdirectory cwd while the slug is for the project root.
|
|
65
|
+
*/
|
|
66
|
+
function sniffCwd(claudeRoot, slug) {
|
|
67
|
+
const dir = path.join(claudeRoot, slug);
|
|
68
|
+
let files;
|
|
69
|
+
try {
|
|
70
|
+
files = fs
|
|
71
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
72
|
+
.filter((f) => f.isFile() && f.name.toLowerCase().endsWith('.jsonl'))
|
|
73
|
+
.map((f) => {
|
|
74
|
+
try {
|
|
75
|
+
return { name: f.name, mtimeMs: fs.statSync(path.join(dir, f.name)).mtimeMs };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { name: f.name, mtimeMs: 0 };
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
86
|
+
const cwdRe = /"cwd"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
87
|
+
for (const f of files.slice(0, SNIFF_FILES)) {
|
|
88
|
+
let chunk;
|
|
89
|
+
try {
|
|
90
|
+
const fd = fs.openSync(path.join(dir, f.name), 'r');
|
|
91
|
+
try {
|
|
92
|
+
const buf = Buffer.alloc(SNIFF_BYTES);
|
|
93
|
+
const read = fs.readSync(fd, buf, 0, SNIFF_BYTES, 0);
|
|
94
|
+
chunk = buf.toString('utf8', 0, read);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
cwdRe.lastIndex = 0;
|
|
104
|
+
for (let m = cwdRe.exec(chunk); m; m = cwdRe.exec(chunk)) {
|
|
105
|
+
let cwd;
|
|
106
|
+
try {
|
|
107
|
+
cwd = JSON.parse(`"${m[1]}"`);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Exact match, then walk up: a subdir cwd still identifies the root.
|
|
113
|
+
for (let p = cwd;;) {
|
|
114
|
+
if (encodeProjectSlug(p) === slug)
|
|
115
|
+
return p;
|
|
116
|
+
const parent = path.dirname(p);
|
|
117
|
+
if (parent === p)
|
|
118
|
+
break;
|
|
119
|
+
p = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build a resolver closure. Positive sniff results are memoized per slug so
|
|
127
|
+
* per-request resolution stays cheap; the claude.json index refreshes on
|
|
128
|
+
* mtime change (REQ-SLUGRES-001).
|
|
129
|
+
*/
|
|
130
|
+
export function createSlugResolver(opts = {}) {
|
|
131
|
+
const claudeJsonPath = opts.claudeJsonPath ?? path.join(os.homedir(), '.claude.json');
|
|
132
|
+
const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
|
|
133
|
+
const sniffed = new Map();
|
|
134
|
+
return (slug) => {
|
|
135
|
+
const fromIndex = claudeJsonIndex(claudeJsonPath).get(slug);
|
|
136
|
+
if (fromIndex)
|
|
137
|
+
return fromIndex;
|
|
138
|
+
const cached = sniffed.get(slug);
|
|
139
|
+
if (cached)
|
|
140
|
+
return cached;
|
|
141
|
+
const fromCwd = sniffCwd(claudeRoot, slug);
|
|
142
|
+
if (fromCwd) {
|
|
143
|
+
sniffed.set(slug, fromCwd);
|
|
144
|
+
return fromCwd;
|
|
145
|
+
}
|
|
146
|
+
return decodeProjectSlug(slug);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** One-shot convenience for callers without a resolver to hold on to. */
|
|
150
|
+
export function resolveProjectSlug(slug, opts = {}) {
|
|
151
|
+
return createSlugResolver(opts)(slug);
|
|
152
|
+
}
|
|
@@ -14,16 +14,19 @@
|
|
|
14
14
|
* "primary" instance instead, since their data (JSONL ingest, CLAUDE.md
|
|
15
15
|
* hierarchy) is global to the user, not scoped to one project.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { createSlugResolver } from './ingest/project-paths.js';
|
|
18
18
|
export class ProjectRegistry {
|
|
19
19
|
cache = new Map();
|
|
20
|
+
pinned = new Set();
|
|
20
21
|
maxOpen;
|
|
21
22
|
verbose;
|
|
22
23
|
openImpl;
|
|
24
|
+
resolveSlug;
|
|
23
25
|
constructor(opts, defaultOpenImpl) {
|
|
24
26
|
this.maxOpen = opts.maxOpen ?? 5;
|
|
25
27
|
this.verbose = opts.verbose ?? false;
|
|
26
28
|
this.openImpl = opts.openImpl ?? defaultOpenImpl;
|
|
29
|
+
this.resolveSlug = opts.resolveSlug ?? createSlugResolver();
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* Return the cached SpecShip for `projectPath`, opening it on demand.
|
|
@@ -55,7 +58,7 @@ export class ProjectRegistry {
|
|
|
55
58
|
* (`-Users-alice-foo` → `/Users/alice/foo`).
|
|
56
59
|
*/
|
|
57
60
|
async getBySlug(slug) {
|
|
58
|
-
return this.get(
|
|
61
|
+
return this.get(this.resolveSlug(slug));
|
|
59
62
|
}
|
|
60
63
|
/** Currently-cached project paths (most-recent-access first). */
|
|
61
64
|
openPaths() {
|
|
@@ -64,6 +67,14 @@ export class ProjectRegistry {
|
|
|
64
67
|
.map(([k]) => k);
|
|
65
68
|
}
|
|
66
69
|
has(projectPath) { return this.cache.has(projectPath); }
|
|
70
|
+
/**
|
|
71
|
+
* Exempt `projectPath` from LRU eviction. The server pins its primary:
|
|
72
|
+
* routes capture the primary instance by reference (`app.primaryCg`), so
|
|
73
|
+
* an eviction CLOSES a handle they still use and every request 500s with
|
|
74
|
+
* "database connection is not open". Surfaced when the /api/events sweep
|
|
75
|
+
* started opening many projects through this cache.
|
|
76
|
+
*/
|
|
77
|
+
pin(projectPath) { this.pinned.add(projectPath); }
|
|
67
78
|
/** Close + drop every cached instance. Called on server shutdown. */
|
|
68
79
|
closeAll() {
|
|
69
80
|
for (const [, v] of this.cache) {
|
|
@@ -80,6 +91,8 @@ export class ProjectRegistry {
|
|
|
80
91
|
let oldestKey = null;
|
|
81
92
|
let oldestTime = Infinity;
|
|
82
93
|
for (const [k, v] of this.cache) {
|
|
94
|
+
if (this.pinned.has(k))
|
|
95
|
+
continue; // never close a pinned instance
|
|
83
96
|
if (v.lastAccess < oldestTime) {
|
|
84
97
|
oldestTime = v.lastAccess;
|
|
85
98
|
oldestKey = k;
|
|
@@ -69,6 +69,20 @@ function priorWindow(key) {
|
|
|
69
69
|
* Claude-specific aggregate queries directly. SpecShip exposes this via
|
|
70
70
|
* `getDb()`-style accessors. Falls back to digging via the queries property.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* SQL expression extracting `path` from a tool call's input JSON. Prefers
|
|
74
|
+
* the full v7 `input_json` column and falls back to `input_summary`; BOTH
|
|
75
|
+
* are guarded with json_valid() because input_summary is truncated to 400
|
|
76
|
+
* chars by the ingestor — long inputs are malformed JSON and an unguarded
|
|
77
|
+
* json_extract throws SQLITE_ERROR, 500ing the route (REQ-DASHINT-003).
|
|
78
|
+
* `col` is the optional table alias prefix (e.g. 't.').
|
|
79
|
+
*/
|
|
80
|
+
function jsonInputExtract(path, col = '') {
|
|
81
|
+
return `COALESCE(
|
|
82
|
+
CASE WHEN json_valid(${col}input_json) THEN json_extract(${col}input_json, '${path}') END,
|
|
83
|
+
CASE WHEN json_valid(${col}input_summary) THEN json_extract(${col}input_summary, '${path}') END
|
|
84
|
+
)`;
|
|
85
|
+
}
|
|
72
86
|
function getDb(cg) {
|
|
73
87
|
// SpecShip exposes the underlying DB via its DatabaseConnection. Look it
|
|
74
88
|
// up via the private field as a fallback — works because it's the same
|
|
@@ -309,13 +323,20 @@ export async function registerClaudeRoutes(app) {
|
|
|
309
323
|
ORDER BY ops DESC
|
|
310
324
|
LIMIT 30
|
|
311
325
|
`).all(sessionId);
|
|
312
|
-
// Skills invoked via the Skill tool.
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
326
|
+
// Skills invoked via the Skill tool. Read the skill name from the full
|
|
327
|
+
// v7 `input_json` column first — `input_summary` is truncated to 400
|
|
328
|
+
// chars by the ingestor, so long inputs are INVALID JSON and a bare
|
|
329
|
+
// json_extract on them throws SQLITE_ERROR ("malformed JSON"), 500ing
|
|
330
|
+
// the whole summary (REQ-DASHINT-003). Every extraction is guarded by
|
|
331
|
+
// json_valid(); rows malformed in both columns degrade to 'unknown'.
|
|
316
332
|
const skills = db.prepare(`
|
|
317
333
|
SELECT
|
|
318
|
-
COALESCE(
|
|
334
|
+
COALESCE(
|
|
335
|
+
CASE WHEN json_valid(input_json)
|
|
336
|
+
THEN COALESCE(json_extract(input_json, '$.skill'), json_extract(input_json, '$.skill_name')) END,
|
|
337
|
+
CASE WHEN json_valid(input_summary)
|
|
338
|
+
THEN COALESCE(json_extract(input_summary, '$.skill'), json_extract(input_summary, '$.skill_name')) END,
|
|
339
|
+
'unknown') as name,
|
|
319
340
|
COUNT(*) as count
|
|
320
341
|
FROM claude_tool_calls
|
|
321
342
|
WHERE session_id = ? AND tool_name = 'Skill'
|
|
@@ -421,12 +442,12 @@ export async function registerClaudeRoutes(app) {
|
|
|
421
442
|
WHERE p.ts >= ?
|
|
422
443
|
GROUP BY type
|
|
423
444
|
`).all(since);
|
|
424
|
-
// Subagent breakdown by name — Task tool calls grouped by subagent_type
|
|
425
|
-
//
|
|
426
|
-
//
|
|
445
|
+
// Subagent breakdown by name — Task tool calls grouped by subagent_type
|
|
446
|
+
// (defaults to 'general-purpose' when unset). Guarded extraction: Task
|
|
447
|
+
// inputs carry long prompts, so input_summary is almost always truncated.
|
|
427
448
|
const subagentByName = db.prepare(`
|
|
428
449
|
SELECT
|
|
429
|
-
COALESCE(NULLIF(
|
|
450
|
+
COALESCE(NULLIF(${jsonInputExtract('$.subagent_type')}, ''), 'general-purpose') as name,
|
|
430
451
|
COUNT(*) as calls,
|
|
431
452
|
MIN(ts) as firstSeen,
|
|
432
453
|
MAX(ts) as lastSeen
|
|
@@ -529,19 +550,19 @@ export async function registerClaudeRoutes(app) {
|
|
|
529
550
|
SELECT COUNT(*) as calls, COUNT(DISTINCT session_id) as sessions
|
|
530
551
|
FROM claude_tool_calls
|
|
531
552
|
WHERE tool_name = 'Task' AND ts >= ?
|
|
532
|
-
AND COALESCE(NULLIF(
|
|
553
|
+
AND COALESCE(NULLIF(${jsonInputExtract('$.subagent_type')}, ''), 'general-purpose') = ?
|
|
533
554
|
`).get(since, type);
|
|
534
555
|
const invocations = db.prepare(`
|
|
535
556
|
SELECT
|
|
536
557
|
t.session_id,
|
|
537
558
|
t.ts,
|
|
538
|
-
COALESCE(
|
|
539
|
-
COALESCE(
|
|
559
|
+
COALESCE(${jsonInputExtract('$.description', 't.')}, '') as description,
|
|
560
|
+
COALESCE(${jsonInputExtract('$.prompt', 't.')}, '') as prompt,
|
|
540
561
|
s.last_model
|
|
541
562
|
FROM claude_tool_calls t
|
|
542
563
|
LEFT JOIN claude_sessions s ON s.id = t.session_id
|
|
543
564
|
WHERE t.tool_name = 'Task' AND t.ts >= ?
|
|
544
|
-
AND COALESCE(NULLIF(
|
|
565
|
+
AND COALESCE(NULLIF(${jsonInputExtract('$.subagent_type', 't.')}, ''), 'general-purpose') = ?
|
|
545
566
|
ORDER BY t.ts DESC
|
|
546
567
|
LIMIT 50
|
|
547
568
|
`).all(since, type);
|
|
@@ -798,17 +819,24 @@ export async function registerClaudeRoutes(app) {
|
|
|
798
819
|
return;
|
|
799
820
|
const db = getDb(cg);
|
|
800
821
|
const tips = [];
|
|
822
|
+
// The claude_* tables span every project's sessions; tips for the primary
|
|
823
|
+
// project must cite only its own (REQ-DASHINT-006). Sessions store the
|
|
824
|
+
// project path in the mangled slug round-trip form, so match both it and
|
|
825
|
+
// the real path.
|
|
826
|
+
const realRoot = (cg.getProjectRoot ? cg.getProjectRoot() : '').replace(/\/+$/, '');
|
|
827
|
+
const mangledRoot = realRoot.replace(/[^A-Za-z0-9]/g, '/');
|
|
828
|
+
const inProject = `session_id IN (SELECT id FROM claude_sessions WHERE project_path IN (?, ?))`;
|
|
801
829
|
// Rule 1: "you read X N times" — same file path Read more than 10 times in
|
|
802
830
|
// a single session.
|
|
803
831
|
const wastefulReads = db.prepare(`
|
|
804
832
|
SELECT session_id, input_summary as file, COUNT(*) as n
|
|
805
833
|
FROM claude_tool_calls
|
|
806
|
-
WHERE tool_name = 'Read' AND input_summary != ''
|
|
834
|
+
WHERE tool_name = 'Read' AND input_summary != '' AND ${inProject}
|
|
807
835
|
GROUP BY session_id, input_summary
|
|
808
836
|
HAVING n >= 10
|
|
809
837
|
ORDER BY n DESC
|
|
810
838
|
LIMIT 5
|
|
811
|
-
`).all();
|
|
839
|
+
`).all(realRoot, mangledRoot);
|
|
812
840
|
for (const r of wastefulReads) {
|
|
813
841
|
tips.push({
|
|
814
842
|
id: 'wasteful_reads:' + r.session_id + ':' + r.file,
|
|
@@ -825,10 +853,10 @@ export async function registerClaudeRoutes(app) {
|
|
|
825
853
|
const heavyResults = db.prepare(`
|
|
826
854
|
SELECT id, session_id, tool_name, input_summary, result_length
|
|
827
855
|
FROM claude_tool_calls
|
|
828
|
-
WHERE result_length > 50000
|
|
856
|
+
WHERE result_length > 50000 AND ${inProject}
|
|
829
857
|
ORDER BY result_length DESC
|
|
830
858
|
LIMIT 5
|
|
831
|
-
`).all();
|
|
859
|
+
`).all(realRoot, mangledRoot);
|
|
832
860
|
for (const r of heavyResults) {
|
|
833
861
|
tips.push({
|
|
834
862
|
id: 'heavy_result:' + r.id,
|
|
@@ -847,9 +875,10 @@ export async function registerClaudeRoutes(app) {
|
|
|
847
875
|
FROM claude_sessions s
|
|
848
876
|
WHERE s.prompt_count >= 10
|
|
849
877
|
AND (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
|
|
878
|
+
AND s.project_path IN (?, ?)
|
|
850
879
|
ORDER BY (CAST(s.total_cache_read_tokens AS REAL) / (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens)) ASC
|
|
851
880
|
LIMIT 3
|
|
852
|
-
`).all();
|
|
881
|
+
`).all(realRoot, mangledRoot);
|
|
853
882
|
for (const r of lowCache) {
|
|
854
883
|
const total = r.ti + r.cw + r.cr;
|
|
855
884
|
const rate = total > 0 ? r.cr / total : 0;
|
|
@@ -32,6 +32,7 @@ export async function registerEventsRoutes(app) {
|
|
|
32
32
|
const lastStatus = new Map(); // `${slug}:${runId}` -> status
|
|
33
33
|
const seenDrift = new Set(); // `${slug}:${linkId}`
|
|
34
34
|
const lastSweep = new Map(); // slug -> last reflection sweep ms
|
|
35
|
+
const lastIndexSig = new Map(); // slug -> lastIndexed signature
|
|
35
36
|
let primed = false; // first pass seeds state silently (no burst on connect)
|
|
36
37
|
let closed = false;
|
|
37
38
|
const send = (ev) => {
|
|
@@ -50,8 +51,13 @@ export async function registerEventsRoutes(app) {
|
|
|
50
51
|
catch {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
54
|
+
// Bound the sweep to the most-recently-active initialized projects
|
|
55
|
+
// (enumerate() sorts by recency): every project here is opened via the
|
|
56
|
+
// registry, and an unbounded sweep churns the LRU open/close cycle on
|
|
57
|
+
// every poll once slugs resolve to real paths.
|
|
58
|
+
projects = projects.filter((p) => p.initialized).slice(0, 10);
|
|
53
59
|
for (const p of projects) {
|
|
54
|
-
if (
|
|
60
|
+
if (closed)
|
|
55
61
|
continue;
|
|
56
62
|
let cg;
|
|
57
63
|
try {
|
|
@@ -83,6 +89,19 @@ export async function registerEventsRoutes(app) {
|
|
|
83
89
|
send({ kind: 'runDone', project: p.slug, projectPath: p.path, id: r.id, title: `Run ${r.status}`, detail: r.workflowName, status: r.status });
|
|
84
90
|
}
|
|
85
91
|
}
|
|
92
|
+
// Index/ingest activity (REQ-DASHUX-004): emit when the project's
|
|
93
|
+
// index freshness changes so open pages can refetch instead of
|
|
94
|
+
// going stale between manual refreshes.
|
|
95
|
+
try {
|
|
96
|
+
const sig = String(cg.getLastIndexedAt() ?? '');
|
|
97
|
+
if (lastIndexSig.get(p.slug) !== sig) {
|
|
98
|
+
lastIndexSig.set(p.slug, sig);
|
|
99
|
+
if (primed) {
|
|
100
|
+
send({ kind: 'activity', project: p.slug, projectPath: p.path, id: sig, title: 'Index updated' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore — activity detection must not break the stream */ }
|
|
86
105
|
// Newly-drifted links.
|
|
87
106
|
let links = [];
|
|
88
107
|
try {
|