@planckspace/cli 0.1.1 → 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/README.md +24 -1
- package/dist/anomaly/constants.d.ts +13 -0
- package/dist/anomaly/constants.d.ts.map +1 -0
- package/dist/anomaly/constants.js +13 -0
- package/dist/anomaly/constants.js.map +1 -0
- package/dist/anomaly/detector.d.ts +12 -0
- package/dist/anomaly/detector.d.ts.map +1 -0
- package/dist/anomaly/detector.js +98 -0
- package/dist/anomaly/detector.js.map +1 -0
- package/dist/anomaly/types.d.ts +19 -0
- package/dist/anomaly/types.d.ts.map +1 -0
- package/dist/anomaly/types.js +2 -0
- package/dist/anomaly/types.js.map +1 -0
- package/dist/commands/budget.d.ts +26 -0
- package/dist/commands/budget.d.ts.map +1 -0
- package/dist/commands/budget.js +91 -0
- package/dist/commands/budget.js.map +1 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +45 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/connect.d.ts +17 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +191 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/daemon.d.ts +8 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +310 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/diagnose.d.ts +2 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +81 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +67 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/export.d.ts +40 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +184 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/insights.d.ts +2 -0
- package/dist/commands/insights.d.ts.map +1 -0
- package/dist/commands/insights.js +71 -0
- package/dist/commands/insights.js.map +1 -0
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +81 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/integrations.d.ts +2 -0
- package/dist/commands/integrations.d.ts.map +1 -0
- package/dist/commands/integrations.js +46 -0
- package/dist/commands/integrations.js.map +1 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +45 -6
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/metrics.d.ts +6 -0
- package/dist/commands/metrics.d.ts.map +1 -0
- package/dist/commands/metrics.js +85 -0
- package/dist/commands/metrics.js.map +1 -0
- package/dist/commands/reconcile.d.ts +2 -0
- package/dist/commands/reconcile.d.ts.map +1 -0
- package/dist/commands/reconcile.js +63 -0
- package/dist/commands/reconcile.js.map +1 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +53 -10
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +84 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/subscription.d.ts +15 -0
- package/dist/commands/subscription.d.ts.map +1 -0
- package/dist/commands/subscription.js +62 -0
- package/dist/commands/subscription.js.map +1 -0
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +77 -10
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/value.d.ts +5 -0
- package/dist/commands/value.d.ts.map +1 -0
- package/dist/commands/value.js +95 -0
- package/dist/commands/value.js.map +1 -0
- package/dist/config.d.ts +28 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/correlate.d.ts +7 -0
- package/dist/correlate.d.ts.map +1 -1
- package/dist/correlate.js +102 -15
- package/dist/correlate.js.map +1 -1
- package/dist/daemon/daemon.d.ts +2 -0
- package/dist/daemon/daemon.d.ts.map +1 -0
- package/dist/daemon/daemon.js +188 -0
- package/dist/daemon/daemon.js.map +1 -0
- package/dist/daemon/daemonState.d.ts +25 -0
- package/dist/daemon/daemonState.d.ts.map +1 -0
- package/dist/daemon/daemonState.js +82 -0
- package/dist/daemon/daemonState.js.map +1 -0
- package/dist/daemon/logger.d.ts +7 -0
- package/dist/daemon/logger.d.ts.map +1 -0
- package/dist/daemon/logger.js +61 -0
- package/dist/daemon/logger.js.map +1 -0
- package/dist/daemon/syncLoop.d.ts +38 -0
- package/dist/daemon/syncLoop.d.ts.map +1 -0
- package/dist/daemon/syncLoop.js +119 -0
- package/dist/daemon/syncLoop.js.map +1 -0
- package/dist/daemon/watcher.d.ts +26 -0
- package/dist/daemon/watcher.d.ts.map +1 -0
- package/dist/daemon/watcher.js +187 -0
- package/dist/daemon/watcher.js.map +1 -0
- package/dist/db/store.d.ts +123 -2
- package/dist/db/store.d.ts.map +1 -1
- package/dist/db/store.js +397 -11
- package/dist/db/store.js.map +1 -1
- package/dist/detectors/cache-gap.d.ts +3 -0
- package/dist/detectors/cache-gap.d.ts.map +1 -0
- package/dist/detectors/cache-gap.js +70 -0
- package/dist/detectors/cache-gap.js.map +1 -0
- package/dist/detectors/context-bloat.d.ts +3 -0
- package/dist/detectors/context-bloat.d.ts.map +1 -0
- package/dist/detectors/context-bloat.js +68 -0
- package/dist/detectors/context-bloat.js.map +1 -0
- package/dist/detectors/fileTokens.d.ts +3 -0
- package/dist/detectors/fileTokens.d.ts.map +1 -0
- package/dist/detectors/fileTokens.js +12 -0
- package/dist/detectors/fileTokens.js.map +1 -0
- package/dist/detectors/index.d.ts +20 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +41 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/model-routing.d.ts +3 -0
- package/dist/detectors/model-routing.d.ts.map +1 -0
- package/dist/detectors/model-routing.js +71 -0
- package/dist/detectors/model-routing.js.map +1 -0
- package/dist/detectors/repeat-read.d.ts +3 -0
- package/dist/detectors/repeat-read.d.ts.map +1 -0
- package/dist/detectors/repeat-read.js +69 -0
- package/dist/detectors/repeat-read.js.map +1 -0
- package/dist/detectors/seat-efficiency.d.ts +4 -0
- package/dist/detectors/seat-efficiency.d.ts.map +1 -0
- package/dist/detectors/seat-efficiency.js +86 -0
- package/dist/detectors/seat-efficiency.js.map +1 -0
- package/dist/detectors/types.d.ts +46 -0
- package/dist/detectors/types.d.ts.map +1 -0
- package/dist/detectors/types.js +2 -0
- package/dist/detectors/types.js.map +1 -0
- package/dist/health.d.ts +59 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +106 -0
- package/dist/health.js.map +1 -0
- package/dist/index.js +389 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +29 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +205 -0
- package/dist/metrics.js.map +1 -0
- package/dist/scrapers/claudeCode.d.ts +1 -0
- package/dist/scrapers/claudeCode.d.ts.map +1 -1
- package/dist/scrapers/claudeCode.js +43 -13
- package/dist/scrapers/claudeCode.js.map +1 -1
- package/dist/scrapers/cursor.d.ts +3 -2
- package/dist/scrapers/cursor.d.ts.map +1 -1
- package/dist/scrapers/cursor.js +56 -16
- package/dist/scrapers/cursor.js.map +1 -1
- package/dist/scrapers/jetbrains.d.ts +15 -0
- package/dist/scrapers/jetbrains.d.ts.map +1 -0
- package/dist/scrapers/jetbrains.js +232 -0
- package/dist/scrapers/jetbrains.js.map +1 -0
- package/dist/scrapers/types.d.ts +4 -1
- package/dist/scrapers/types.d.ts.map +1 -1
- package/dist/scrapers/windsurf.d.ts +3 -2
- package/dist/scrapers/windsurf.d.ts.map +1 -1
- package/dist/scrapers/windsurf.js +25 -9
- package/dist/scrapers/windsurf.js.map +1 -1
- package/dist/sync/payload.d.ts +4 -5
- package/dist/sync/payload.d.ts.map +1 -1
- package/dist/sync/payload.js +88 -7
- package/dist/sync/payload.js.map +1 -1
- package/dist/sync/syncEngine.d.ts +19 -3
- package/dist/sync/syncEngine.d.ts.map +1 -1
- package/dist/sync/syncEngine.js +116 -10
- package/dist/sync/syncEngine.js.map +1 -1
- package/install.sh +27 -10
- package/package.json +43 -42
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { estimateFileTokens } from "./fileTokens.js";
|
|
2
|
+
const DAYS_30_MS = 30 * 24 * 60 * 60 * 1000;
|
|
3
|
+
const CACHE_READ_RATE_PER_M = 0.30; // Sonnet 4 rate, $/M tokens
|
|
4
|
+
const MIN_SESSIONS = 3;
|
|
5
|
+
const REREAD_THRESHOLD = 5; // > 5 sessions
|
|
6
|
+
const TOKEN_THRESHOLD = 5_000;
|
|
7
|
+
function isClaudeMd(filePath) {
|
|
8
|
+
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
9
|
+
return normalized === "claude.md" || normalized.endsWith("/claude.md");
|
|
10
|
+
}
|
|
11
|
+
export const contextBloatDetector = {
|
|
12
|
+
id: "context-bloat",
|
|
13
|
+
description: "CLAUDE.md re-read in many sessions — splitting it reduces per-session cache costs",
|
|
14
|
+
minSessionsRequired: MIN_SESSIONS,
|
|
15
|
+
detect(sessions) {
|
|
16
|
+
if (sessions.length < MIN_SESSIONS)
|
|
17
|
+
return [];
|
|
18
|
+
const since = new Date(Date.now() - DAYS_30_MS).toISOString();
|
|
19
|
+
const recent = sessions.filter((s) => s.startedAt >= since);
|
|
20
|
+
const fileIndex = new Map();
|
|
21
|
+
for (const session of recent) {
|
|
22
|
+
for (const turn of session.turns) {
|
|
23
|
+
for (const file of turn.filesTouched) {
|
|
24
|
+
if (!isClaudeMd(file))
|
|
25
|
+
continue;
|
|
26
|
+
if (!fileIndex.has(file)) {
|
|
27
|
+
fileIndex.set(file, { sessions: new Set(), repoName: session.repoName });
|
|
28
|
+
}
|
|
29
|
+
fileIndex.get(file).sessions.add(session.id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const insights = [];
|
|
34
|
+
for (const [filePath, { sessions: sessionIds, repoName }] of fileIndex) {
|
|
35
|
+
if (sessionIds.size <= REREAD_THRESHOLD)
|
|
36
|
+
continue;
|
|
37
|
+
const tokenCount = estimateFileTokens(filePath);
|
|
38
|
+
if (tokenCount <= TOKEN_THRESHOLD)
|
|
39
|
+
continue;
|
|
40
|
+
const reReadCount = sessionIds.size;
|
|
41
|
+
const monthlySavingsUsd = (reReadCount * tokenCount * CACHE_READ_RATE_PER_M) / 1_000_000;
|
|
42
|
+
insights.push({
|
|
43
|
+
detectorId: "context-bloat",
|
|
44
|
+
repoName,
|
|
45
|
+
workspaceScope: "repo",
|
|
46
|
+
titleShort: `CLAUDE.md re-read in ${reReadCount} sessions (last 30 days)`,
|
|
47
|
+
estimatedMonthlySavingsUsd: monthlySavingsUsd,
|
|
48
|
+
confidence: "high",
|
|
49
|
+
evidence: {
|
|
50
|
+
sessionIds: [...sessionIds],
|
|
51
|
+
metrics: {
|
|
52
|
+
reReadCount,
|
|
53
|
+
estimatedTokenCount: tokenCount,
|
|
54
|
+
cacheReadRatePerM: CACHE_READ_RATE_PER_M,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
manualFix: `Split \`CLAUDE.md\` (~${tokenCount.toLocaleString()} tokens) into a root index under` +
|
|
58
|
+
` 50 lines that uses \`@import\` directives to load scoped sub-files. This lets Claude` +
|
|
59
|
+
` load only the sections relevant to each task, cutting the per-session re-read from` +
|
|
60
|
+
` ~${tokenCount.toLocaleString()} tokens to a few hundred.\n\n` +
|
|
61
|
+
`See the [Claude Code memory docs](https://docs.anthropic.com/en/docs/claude-code/memory)` +
|
|
62
|
+
` for the import syntax.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return insights;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=context-bloat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-bloat.js","sourceRoot":"","sources":["../../src/detectors/context-bloat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileTokens.d.ts","sourceRoot":"","sources":["../../src/detectors/fileTokens.ts"],"names":[],"mappings":"AAEA,qHAAqH;AACrH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO3D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
/** Estimate token count by reading file bytes and dividing by 4 (chars/token heuristic). Returns 0 if unreadable. */
|
|
3
|
+
export function estimateFileTokens(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const buf = fs.readFileSync(filePath);
|
|
6
|
+
return Math.ceil(buf.length / 4);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=fileTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileTokens.js","sourceRoot":"","sources":["../../src/detectors/fileTokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,qHAAqH;AACrH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Detector, Insight, Session } from "./types.js";
|
|
2
|
+
import type { SubscriptionContext } from "../config.js";
|
|
3
|
+
import type { SessionRow } from "../db/store.js";
|
|
4
|
+
export type { Session, Insight, Detector };
|
|
5
|
+
/** Parse a DB row's turnsJson into a Session the detectors can consume. */
|
|
6
|
+
export declare function parseSession(row: SessionRow): Session;
|
|
7
|
+
export declare function getDetectors(subscriptions: SubscriptionContext[]): Detector[];
|
|
8
|
+
export type RunResult = {
|
|
9
|
+
insights: Insight[];
|
|
10
|
+
skipped: {
|
|
11
|
+
detectorId: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Run all detectors. Detectors whose minSessionsRequired threshold is not met are
|
|
17
|
+
* skipped and recorded in `skipped` so the caller can surface honest empty states.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runDetectors(sessions: Session[], subscriptions: SubscriptionContext[]): RunResult;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAQjD,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAE3C,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAIrD;AAED,wBAAgB,YAAY,CAAC,aAAa,EAAE,mBAAmB,EAAE,GAAG,QAAQ,EAAE,CAQ7E;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,OAAO,EAAE,EACnB,aAAa,EAAE,mBAAmB,EAAE,GACnC,SAAS,CAiBX"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { contextBloatDetector } from "./context-bloat.js";
|
|
2
|
+
import { repeatReadDetector } from "./repeat-read.js";
|
|
3
|
+
import { modelRoutingDetector } from "./model-routing.js";
|
|
4
|
+
import { cacheGapDetector } from "./cache-gap.js";
|
|
5
|
+
import { createSeatEfficiencyDetector } from "./seat-efficiency.js";
|
|
6
|
+
/** Parse a DB row's turnsJson into a Session the detectors can consume. */
|
|
7
|
+
export function parseSession(row) {
|
|
8
|
+
const turns = JSON.parse(row.turnsJson);
|
|
9
|
+
const { turnsJson: _omit, ...rest } = row;
|
|
10
|
+
return { ...rest, turns };
|
|
11
|
+
}
|
|
12
|
+
export function getDetectors(subscriptions) {
|
|
13
|
+
return [
|
|
14
|
+
contextBloatDetector,
|
|
15
|
+
repeatReadDetector,
|
|
16
|
+
modelRoutingDetector,
|
|
17
|
+
cacheGapDetector,
|
|
18
|
+
createSeatEfficiencyDetector(subscriptions),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run all detectors. Detectors whose minSessionsRequired threshold is not met are
|
|
23
|
+
* skipped and recorded in `skipped` so the caller can surface honest empty states.
|
|
24
|
+
*/
|
|
25
|
+
export function runDetectors(sessions, subscriptions) {
|
|
26
|
+
const detectors = getDetectors(subscriptions);
|
|
27
|
+
const insights = [];
|
|
28
|
+
const skipped = [];
|
|
29
|
+
for (const detector of detectors) {
|
|
30
|
+
if (sessions.length < detector.minSessionsRequired) {
|
|
31
|
+
skipped.push({
|
|
32
|
+
detectorId: detector.id,
|
|
33
|
+
reason: `need ≥${detector.minSessionsRequired} sessions, have ${sessions.length}`,
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
insights.push(...detector.detect(sessions));
|
|
38
|
+
}
|
|
39
|
+
return { insights, skipped };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAIpE,2EAA2E;AAC3E,MAAM,UAAU,YAAY,CAAC,GAAe;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAW,CAAC;IAClD,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC;IAC1C,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,aAAoC;IAC/D,OAAO;QACL,oBAAoB;QACpB,kBAAkB;QAClB,oBAAoB;QACpB,gBAAgB;QAChB,4BAA4B,CAAC,aAAa,CAAC;KAC5C,CAAC;AACJ,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAmB,EACnB,aAAoC;IAEpC,MAAM,SAAS,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAA6C,EAAE,CAAC;IAE7D,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,mBAAmB,EAAE,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC;gBACX,UAAU,EAAE,QAAQ,CAAC,EAAE;gBACvB,MAAM,EAAE,SAAS,QAAQ,CAAC,mBAAmB,mBAAmB,QAAQ,CAAC,MAAM,EAAE;aAClF,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-routing.d.ts","sourceRoot":"","sources":["../../src/detectors/model-routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAqB7D,eAAO,MAAM,oBAAoB,EAAE,QA2DlC,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const DAYS_30_MS = 30 * 24 * 60 * 60 * 1000;
|
|
2
|
+
const OPUS_OUTPUT_RATE_PER_M = 25.00; // $/M output tokens
|
|
3
|
+
const HAIKU_OUTPUT_RATE_PER_M = 5.00; // $/M output tokens
|
|
4
|
+
const RATE_DIFF_PER_M = OPUS_OUTPUT_RATE_PER_M - HAIKU_OUTPUT_RATE_PER_M; // 20.00
|
|
5
|
+
const P50_OUTPUT_THRESHOLD = 800;
|
|
6
|
+
const MIN_SESSIONS = 5;
|
|
7
|
+
function isOpus(modelId) {
|
|
8
|
+
// Matches claude-opus-4-* but not the deprecated claude-opus-4-1 tier
|
|
9
|
+
return modelId.startsWith("claude-opus-4") && !modelId.startsWith("claude-opus-4-1");
|
|
10
|
+
}
|
|
11
|
+
function median(values) {
|
|
12
|
+
if (values.length === 0)
|
|
13
|
+
return 0;
|
|
14
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
15
|
+
const mid = Math.floor(sorted.length / 2);
|
|
16
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
17
|
+
}
|
|
18
|
+
export const modelRoutingDetector = {
|
|
19
|
+
id: "model-routing",
|
|
20
|
+
description: "Opus used for low-output tasks — Haiku handles the same work at 5× lower cost",
|
|
21
|
+
minSessionsRequired: MIN_SESSIONS,
|
|
22
|
+
detect(sessions) {
|
|
23
|
+
if (sessions.length < MIN_SESSIONS)
|
|
24
|
+
return [];
|
|
25
|
+
const since = new Date(Date.now() - DAYS_30_MS).toISOString();
|
|
26
|
+
const recent = sessions.filter((s) => s.startedAt >= since);
|
|
27
|
+
const opusSessions = recent.filter((s) => isOpus(s.model));
|
|
28
|
+
if (opusSessions.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
const qualifying = [];
|
|
31
|
+
for (const session of opusSessions) {
|
|
32
|
+
if (session.turns.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
const p50 = median(session.turns.map((t) => t.outputTokens));
|
|
35
|
+
if (p50 < P50_OUTPUT_THRESHOLD)
|
|
36
|
+
qualifying.push(session);
|
|
37
|
+
}
|
|
38
|
+
if (qualifying.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const totalOutputTokens = qualifying.reduce((sum, s) => sum + s.turns.reduce((ts, t) => ts + t.outputTokens, 0), 0);
|
|
41
|
+
const totalTurns = qualifying.reduce((sum, s) => sum + s.turns.length, 0);
|
|
42
|
+
const monthlySavingsUsd = (totalOutputTokens * RATE_DIFF_PER_M) / 1_000_000;
|
|
43
|
+
const avgTurnOutput = totalTurns > 0 ? Math.round(totalOutputTokens / totalTurns) : 0;
|
|
44
|
+
return [{
|
|
45
|
+
detectorId: "model-routing",
|
|
46
|
+
repoName: null,
|
|
47
|
+
workspaceScope: "workspace",
|
|
48
|
+
titleShort: `${qualifying.length} Opus session(s) with p50 output <${P50_OUTPUT_THRESHOLD} tokens — likely cheap-class work`,
|
|
49
|
+
estimatedMonthlySavingsUsd: monthlySavingsUsd,
|
|
50
|
+
confidence: "low",
|
|
51
|
+
evidence: {
|
|
52
|
+
sessionIds: qualifying.map((s) => s.id),
|
|
53
|
+
metrics: {
|
|
54
|
+
opusSessionCount: qualifying.length,
|
|
55
|
+
totalTurns,
|
|
56
|
+
avgTurnOutputTokens: avgTurnOutput,
|
|
57
|
+
rateDiffPerM: RATE_DIFF_PER_M,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
manualFix: `**Confidence is low — output token count is a weak proxy for task complexity.**\n\n` +
|
|
61
|
+
`These Opus sessions had a median turn output below ${P50_OUTPUT_THRESHOLD} tokens,` +
|
|
62
|
+
` which often indicates quick file edits, short Q&A, or simple completions that` +
|
|
63
|
+
` Haiku 4.5 handles equally well at 5× lower output cost.\n\n` +
|
|
64
|
+
`Review a sample of these sessions before acting. If the tasks genuinely only need` +
|
|
65
|
+
` short responses, configure Claude Code to default to a smaller model for that project.` +
|
|
66
|
+
` Keep Opus for complex multi-step reasoning, long-context synthesis, and agentic tasks` +
|
|
67
|
+
` with many tool calls.`,
|
|
68
|
+
}];
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=model-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-routing.js","sourceRoot":"","sources":["../../src/detectors/model-routing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeat-read.d.ts","sourceRoot":"","sources":["../../src/detectors/repeat-read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAW7D,eAAO,MAAM,kBAAkB,EAAE,QAqEhC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { estimateFileTokens } from "./fileTokens.js";
|
|
2
|
+
const DAYS_7_MS = 7 * 24 * 60 * 60 * 1000;
|
|
3
|
+
const CACHE_READ_RATE_PER_M = 0.30; // Sonnet 4 rate, $/M tokens
|
|
4
|
+
const MONTHLY_PROJECTION = 30 / 7; // scale 7-day window to 30 days
|
|
5
|
+
const MIN_SESSIONS = 3;
|
|
6
|
+
const READ_COUNT_THRESHOLD = 10; // > 10 total reads
|
|
7
|
+
const SESSION_COUNT_THRESHOLD = 3; // > 3 distinct sessions
|
|
8
|
+
const TOKEN_THRESHOLD = 2_000;
|
|
9
|
+
export const repeatReadDetector = {
|
|
10
|
+
id: "repeat-read",
|
|
11
|
+
description: "Same file read >10× across >3 sessions in 7 days — a prompt caching opportunity",
|
|
12
|
+
minSessionsRequired: MIN_SESSIONS,
|
|
13
|
+
detect(sessions) {
|
|
14
|
+
if (sessions.length < MIN_SESSIONS)
|
|
15
|
+
return [];
|
|
16
|
+
const since = new Date(Date.now() - DAYS_7_MS).toISOString();
|
|
17
|
+
const recent = sessions.filter((s) => s.startedAt >= since);
|
|
18
|
+
const fileIndex = new Map();
|
|
19
|
+
for (const session of recent) {
|
|
20
|
+
for (const turn of session.turns) {
|
|
21
|
+
for (const file of turn.filesTouched) {
|
|
22
|
+
if (!fileIndex.has(file)) {
|
|
23
|
+
fileIndex.set(file, { totalReads: 0, sessions: new Set(), repoName: session.repoName });
|
|
24
|
+
}
|
|
25
|
+
const entry = fileIndex.get(file);
|
|
26
|
+
entry.totalReads++;
|
|
27
|
+
entry.sessions.add(session.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const insights = [];
|
|
32
|
+
for (const [filePath, { totalReads, sessions: sessionIds, repoName }] of fileIndex) {
|
|
33
|
+
if (totalReads <= READ_COUNT_THRESHOLD)
|
|
34
|
+
continue;
|
|
35
|
+
if (sessionIds.size <= SESSION_COUNT_THRESHOLD)
|
|
36
|
+
continue;
|
|
37
|
+
const tokenCount = estimateFileTokens(filePath);
|
|
38
|
+
if (tokenCount <= TOKEN_THRESHOLD)
|
|
39
|
+
continue;
|
|
40
|
+
// Savings = (reads - 1) × tokens × rate, projected to 30 days
|
|
41
|
+
const monthlySavingsUsd = ((totalReads - 1) * tokenCount * CACHE_READ_RATE_PER_M * MONTHLY_PROJECTION) / 1_000_000;
|
|
42
|
+
const fileName = filePath.replace(/\\/g, "/").split("/").pop() ?? filePath;
|
|
43
|
+
insights.push({
|
|
44
|
+
detectorId: "repeat-read",
|
|
45
|
+
repoName,
|
|
46
|
+
workspaceScope: "repo",
|
|
47
|
+
titleShort: `${fileName} read ${totalReads}× across ${sessionIds.size} sessions (last 7 days)`,
|
|
48
|
+
estimatedMonthlySavingsUsd: monthlySavingsUsd,
|
|
49
|
+
confidence: "medium",
|
|
50
|
+
evidence: {
|
|
51
|
+
sessionIds: [...sessionIds],
|
|
52
|
+
metrics: {
|
|
53
|
+
totalReads,
|
|
54
|
+
distinctSessions: sessionIds.size,
|
|
55
|
+
estimatedTokenCount: tokenCount,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
manualFix: `\`${fileName}\` (${tokenCount.toLocaleString()} tokens) is loaded repeatedly without prompt` +
|
|
59
|
+
` caching. Add a \`cache_control: ephemeral\` block to the system prompt section that` +
|
|
60
|
+
` includes this file, or restructure prompts so the file content sits in a stable prefix` +
|
|
61
|
+
` covered by Claude Code's 5-minute cache TTL.\n\n` +
|
|
62
|
+
`If the file changes frequently, split it: keep the stable reference sections in a` +
|
|
63
|
+
` separate file that changes less often, and only load the volatile parts dynamically.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return insights;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=repeat-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeat-read.js","sourceRoot":"","sources":["../../src/detectors/repeat-read.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-efficiency.d.ts","sourceRoot":"","sources":["../../src/detectors/seat-efficiency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAOxD,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,mBAAmB,EAAE,GAAG,QAAQ,CAqG3F"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const DAYS_30_MS = 30 * 24 * 60 * 60 * 1000;
|
|
2
|
+
const DORMANT_THRESHOLD = 0.10; // < 10% of seat cost → dormant
|
|
3
|
+
const OVERLOADED_THRESHOLD = 4.0; // > 4× seat cost → under-priced
|
|
4
|
+
const MIN_SESSIONS = 5;
|
|
5
|
+
export function createSeatEfficiencyDetector(subscriptions) {
|
|
6
|
+
return {
|
|
7
|
+
id: "seat-efficiency",
|
|
8
|
+
description: "Dormant or over-capacity seats relative to declared subscription cost",
|
|
9
|
+
minSessionsRequired: MIN_SESSIONS,
|
|
10
|
+
detect(sessions) {
|
|
11
|
+
if (sessions.length < MIN_SESSIONS)
|
|
12
|
+
return [];
|
|
13
|
+
if (subscriptions.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const since = new Date(Date.now() - DAYS_30_MS).toISOString();
|
|
16
|
+
const recent = sessions.filter((s) => s.startedAt >= since);
|
|
17
|
+
const insights = [];
|
|
18
|
+
for (const sub of subscriptions) {
|
|
19
|
+
const toolSessions = recent.filter((s) => s.tool === sub.tool);
|
|
20
|
+
const seatCost = sub.seatCostUsdMonthly;
|
|
21
|
+
const perSeat = new Map();
|
|
22
|
+
for (const session of toolSessions) {
|
|
23
|
+
const key = session.gitAuthorEmail ?? "(unattributed)";
|
|
24
|
+
if (!perSeat.has(key))
|
|
25
|
+
perSeat.set(key, { consumption: 0, sessionIds: [] });
|
|
26
|
+
const entry = perSeat.get(key);
|
|
27
|
+
entry.consumption += session.costUsd;
|
|
28
|
+
entry.sessionIds.push(session.id);
|
|
29
|
+
}
|
|
30
|
+
const dormant = [...perSeat.entries()].filter(([, e]) => e.consumption < seatCost * DORMANT_THRESHOLD);
|
|
31
|
+
const overCapacity = [...perSeat.entries()].filter(([, e]) => e.consumption > seatCost * OVERLOADED_THRESHOLD);
|
|
32
|
+
if (dormant.length > 0) {
|
|
33
|
+
const monthlySavingsUsd = dormant.length * seatCost;
|
|
34
|
+
const avgConsumption = dormant.reduce((s, [, e]) => s + e.consumption, 0) / dormant.length;
|
|
35
|
+
const dormantEmails = dormant.map(([email]) => email).join(", ");
|
|
36
|
+
insights.push({
|
|
37
|
+
detectorId: "seat-efficiency",
|
|
38
|
+
repoName: null,
|
|
39
|
+
workspaceScope: "workspace",
|
|
40
|
+
titleShort: `${dormant.length} dormant ${sub.tool} ${sub.plan} seat(s) — <10% of seat cost consumed`,
|
|
41
|
+
estimatedMonthlySavingsUsd: monthlySavingsUsd,
|
|
42
|
+
confidence: "high",
|
|
43
|
+
evidence: {
|
|
44
|
+
sessionIds: dormant.flatMap(([, e]) => e.sessionIds),
|
|
45
|
+
metrics: {
|
|
46
|
+
dormantSeatCount: dormant.length,
|
|
47
|
+
seatCostUsdMonthly: seatCost,
|
|
48
|
+
avgConsumptionUsd: parseFloat(avgConsumption.toFixed(4)),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
manualFix: `${dormant.length} seat(s) on the ${sub.tool} ${sub.plan} plan` +
|
|
52
|
+
` ($${seatCost}/seat/mo) consumed less than 10% of their seat cost over the last` +
|
|
53
|
+
` 30 days. Consider removing or reassigning them to save ~$${monthlySavingsUsd.toFixed(2)}/mo.\n\n` +
|
|
54
|
+
`Dormant users: ${dormantEmails}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (overCapacity.length > 0) {
|
|
58
|
+
const avgConsumption = overCapacity.reduce((s, [, e]) => s + e.consumption, 0) / overCapacity.length;
|
|
59
|
+
const highValueEmails = overCapacity.map(([email]) => email).join(", ");
|
|
60
|
+
insights.push({
|
|
61
|
+
detectorId: "seat-efficiency",
|
|
62
|
+
repoName: null,
|
|
63
|
+
workspaceScope: "workspace",
|
|
64
|
+
titleShort: `${overCapacity.length} over-capacity ${sub.tool} ${sub.plan} seat(s) — >4× seat cost consumed`,
|
|
65
|
+
estimatedMonthlySavingsUsd: 0, // contract defend, not a cost saving
|
|
66
|
+
confidence: "high",
|
|
67
|
+
evidence: {
|
|
68
|
+
sessionIds: overCapacity.flatMap(([, e]) => e.sessionIds),
|
|
69
|
+
metrics: {
|
|
70
|
+
overCapacitySeatCount: overCapacity.length,
|
|
71
|
+
seatCostUsdMonthly: seatCost,
|
|
72
|
+
avgConsumptionUsd: parseFloat(avgConsumption.toFixed(4)),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
manualFix: `${overCapacity.length} seat(s) consumed more than 4× their seat cost` +
|
|
76
|
+
` ($${seatCost}/seat/mo) in the last 30 days. These users deliver strong ROI —` +
|
|
77
|
+
` use this data to defend or expand the contract at renewal.\n\n` +
|
|
78
|
+
`High-value users: ${highValueEmails}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return insights;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=seat-efficiency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-efficiency.js","sourceRoot":"","sources":["../../src/detectors/seat-efficiency.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Turn } from "../scrapers/types.js";
|
|
2
|
+
/** SessionRow with turnsJson pre-parsed. Avoids importing SessionRow to break circular deps. */
|
|
3
|
+
export type Session = {
|
|
4
|
+
id: string;
|
|
5
|
+
tool: string;
|
|
6
|
+
model: string;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
endedAt: string;
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
cacheReadTokens: number;
|
|
12
|
+
cacheWriteTokens: number;
|
|
13
|
+
costUsd: number;
|
|
14
|
+
costType: string;
|
|
15
|
+
subscriptionPlan: string | null;
|
|
16
|
+
unmatchedTurns: number;
|
|
17
|
+
repoName: string | null;
|
|
18
|
+
gitBranch: string | null;
|
|
19
|
+
gitAuthorEmail: string | null;
|
|
20
|
+
workingDir: string;
|
|
21
|
+
turnCount: number;
|
|
22
|
+
filesTouchedCount: number;
|
|
23
|
+
outcome: string;
|
|
24
|
+
scrapedAt: string;
|
|
25
|
+
turns: Turn[];
|
|
26
|
+
};
|
|
27
|
+
export type Insight = {
|
|
28
|
+
detectorId: string;
|
|
29
|
+
repoName: string | null;
|
|
30
|
+
workspaceScope: "workspace" | "repo" | "developer";
|
|
31
|
+
titleShort: string;
|
|
32
|
+
estimatedMonthlySavingsUsd: number;
|
|
33
|
+
confidence: "high" | "medium" | "low";
|
|
34
|
+
evidence: {
|
|
35
|
+
sessionIds: string[];
|
|
36
|
+
metrics: Record<string, number>;
|
|
37
|
+
};
|
|
38
|
+
manualFix: string;
|
|
39
|
+
};
|
|
40
|
+
export type Detector = {
|
|
41
|
+
id: string;
|
|
42
|
+
description: string;
|
|
43
|
+
minSessionsRequired: number;
|
|
44
|
+
detect(sessions: Session[]): Insight[];
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/detectors/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,gGAAgG;AAChG,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,0BAA0B,EAAE,MAAM,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,QAAQ,EAAE;QACR,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACjC,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;CACxC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/detectors/types.ts"],"names":[],"mappings":""}
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type HealthOk = {
|
|
2
|
+
ok: true;
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
workspaceName: string;
|
|
5
|
+
dbInstanceId: string;
|
|
6
|
+
};
|
|
7
|
+
export type HealthFailure = {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: "no-token";
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: "invalid-token";
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: "network";
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
reason: "workspace-changed";
|
|
19
|
+
previous: string;
|
|
20
|
+
current: string;
|
|
21
|
+
/** New workspace name (for display). */
|
|
22
|
+
workspaceName: string;
|
|
23
|
+
/** DB instance ID at the new workspace (unchanged from our perspective). */
|
|
24
|
+
dbInstanceId: string;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: "db-reset";
|
|
28
|
+
previous: string;
|
|
29
|
+
current: string;
|
|
30
|
+
/** Workspace ID (unchanged). */
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
workspaceName: string;
|
|
33
|
+
};
|
|
34
|
+
export type HealthResult = HealthOk | HealthFailure;
|
|
35
|
+
/**
|
|
36
|
+
* Check /api/me/health and detect three classes of drift:
|
|
37
|
+
* - workspace-changed (token now bound to a different workspace)
|
|
38
|
+
* - db-reset (backend DB was wiped/restored since last check)
|
|
39
|
+
*
|
|
40
|
+
* On success, updates last_backend_db_instance_id / last_workspace_id /
|
|
41
|
+
* last_health_check_at in connection_state.
|
|
42
|
+
*
|
|
43
|
+
* On drift, returns the failure type with `current` values but does NOT
|
|
44
|
+
* update stored state — callers must call commitHealthState() after resolving.
|
|
45
|
+
*/
|
|
46
|
+
export declare function preflightHealth(): Promise<HealthResult>;
|
|
47
|
+
/** Persist health state; call after a successful preflight or after reconcile resolves drift. */
|
|
48
|
+
export declare function commitHealthState(dbInstanceId: string, workspaceId: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Fetch the backend session manifest and reset sync state for any local
|
|
51
|
+
* sessions that are marked synced but absent from the backend.
|
|
52
|
+
*
|
|
53
|
+
* Returns the count of sessions whose sync state was reset (they will be
|
|
54
|
+
* re-sent on the next `planck sync`).
|
|
55
|
+
*/
|
|
56
|
+
export declare function reconcileWithManifest(): Promise<{
|
|
57
|
+
reset: number;
|
|
58
|
+
}>;
|
|
59
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,IAAI,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAChC;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mBAAmB,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEN,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,aAAa,CAAC;AAUpD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC,CAoD7D;AAID,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAIjF;AAID;;;;;;GAMG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCxE"}
|