codesesh 1.0.7 → 1.0.8
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 +1 -1
- package/dist/{chunk-EYZ73CYP.js → chunk-AGUAXTKC.js} +2 -2
- package/dist/{chunk-VVCSQU5B.js → chunk-DHQPM5FF.js} +6 -6
- package/dist/chunk-DHQPM5FF.js.map +1 -0
- package/dist/{chunk-YEQTRALH.js → chunk-KVTLNX7V.js} +3 -3
- package/dist/chunk-KVTLNX7V.js.map +1 -0
- package/dist/{chunk-WIWK7BXS.js → chunk-L4G3LE7I.js} +25 -26
- package/dist/chunk-L4G3LE7I.js.map +1 -0
- package/dist/{chunk-SII3M66U.js → chunk-RBCCNCDO.js} +8 -8
- package/dist/chunk-RBCCNCDO.js.map +1 -0
- package/dist/{chunk-DE6UJMOY.js → chunk-V3HKVTIO.js} +3 -2
- package/dist/chunk-V3HKVTIO.js.map +1 -0
- package/dist/{chunk-QHWSZFRM.js → chunk-YLXHGL4S.js} +4 -4
- package/dist/chunk-YLXHGL4S.js.map +1 -0
- package/dist/{discovery-4WAEYSTR.js → discovery-KEZQU6RL.js} +5 -5
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/project-identity-worker.js +4 -4
- package/dist/scan-refresh-worker.js +6 -6
- package/dist/search-index-worker.js +6 -6
- package/dist/session-detail-worker.js +6 -6
- package/dist/smart-tag-worker.js +3 -3
- package/dist/web/assets/CustomTimeWindowDialog-z5xtqkBF.js +1 -0
- package/dist/web/assets/DetailLanding-CpV0jLmT.js +1 -0
- package/dist/web/assets/DialogTitle-B8zPwJB8.js +1 -0
- package/dist/web/assets/InteractiveReceipt-Bsj37HtN.js +1 -0
- package/dist/web/assets/OverviewScreen-B9qZqEQA.js +1 -0
- package/dist/web/assets/PrismHighlighter-DWS6Nx_X.js +6 -0
- package/dist/web/assets/Projects-oY3KCgS4.js +1 -0
- package/dist/web/assets/SearchResultsPanel-BAManMmt.js +1 -0
- package/dist/web/assets/SessionDetail-KSgkmLV7.js +53 -0
- package/dist/web/assets/SmartTagChips-BMP9pPRw.js +1 -0
- package/dist/web/assets/{api-ToidoNW6.js → api-CtZGdxlN.js} +1 -1
- package/dist/web/assets/index-BSLHxV0w.css +1 -0
- package/dist/web/assets/index-DNw59Qsn.js +1423 -0
- package/dist/web/assets/jsx-runtime-D3jfb0Ew.js +1 -0
- package/dist/web/assets/panel-DcOuALYX.js +1 -0
- package/dist/web/assets/session-indexes-DKukxoe1.js +4 -0
- package/dist/web/assets/translate-CLoII3EO.js +2 -0
- package/dist/web/assets/utils-Dk_44zMo.js +1 -0
- package/dist/web/index.html +9 -8
- package/package.json +2 -2
- package/dist/chunk-DE6UJMOY.js.map +0 -1
- package/dist/chunk-QHWSZFRM.js.map +0 -1
- package/dist/chunk-SII3M66U.js.map +0 -1
- package/dist/chunk-VVCSQU5B.js.map +0 -1
- package/dist/chunk-WIWK7BXS.js.map +0 -1
- package/dist/chunk-YEQTRALH.js.map +0 -1
- package/dist/web/assets/CustomTimeWindowDialog-CVURkiVO.js +0 -1
- package/dist/web/assets/InteractiveReceipt-DGC-xV7U.js +0 -1
- package/dist/web/assets/OverviewScreen-DoXrZ5qp.js +0 -1
- package/dist/web/assets/PrismHighlighter-02695Lmx.js +0 -6
- package/dist/web/assets/Projects-DZiYh6hp.js +0 -1
- package/dist/web/assets/SearchResultsPanel-CAQe8Di0.js +0 -1
- package/dist/web/assets/SessionDetail-Da8EBygt.js +0 -53
- package/dist/web/assets/index-Bl8pbRFj.js +0 -1423
- package/dist/web/assets/index-CRI1m3xN.css +0 -1
- package/dist/web/assets/jsx-runtime-B-hcVAMW.js +0 -1
- package/dist/web/assets/session-indexes-D5gps7K8.js +0 -4
- package/dist/web/assets/translate-BDFu3ZiK.js +0 -2
- package/dist/web/assets/utils-BIZi2up3.js +0 -1
- /package/dist/{chunk-EYZ73CYP.js.map → chunk-AGUAXTKC.js.map} +0 -0
- /package/dist/{discovery-4WAEYSTR.js.map → discovery-KEZQU6RL.js.map} +0 -0
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
getProjectIdentityKey,
|
|
5
5
|
getSessionAgentKey,
|
|
6
6
|
matchesProjectIdentity
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YLXHGL4S.js";
|
|
8
8
|
|
|
9
|
-
// ../core/dist/chunk-
|
|
9
|
+
// ../core/dist/chunk-O3HCRG2L.mjs
|
|
10
10
|
import * as os from "os";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
import { createHash } from "crypto";
|
|
@@ -324,4 +324,4 @@ export {
|
|
|
324
324
|
normalizeProjectScopePath,
|
|
325
325
|
buildProjectGroups
|
|
326
326
|
};
|
|
327
|
-
//# sourceMappingURL=chunk-
|
|
327
|
+
//# sourceMappingURL=chunk-KVTLNX7V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../core/src/projects/identity.ts","../../core/src/projects/display-name.ts","../../core/src/projects/fs.ts","../../core/src/projects/scope.ts","../../core/src/projects/groups.ts"],"sourcesContent":["import * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport type { ProjectIdentity, ProjectIdentityKind } from \"../types/index.js\";\nimport { fallbackDisplayName } from \"./display-name.js\";\nimport { realFs, type IdentityFs } from \"./fs.js\";\n\nexport type { IdentityFs } from \"./fs.js\";\n\nexport const PROJECT_IDENTITY_RESOLVER_REVISION = \"project-identity-v2\";\n\nexport interface ProjectIdentityProjection {\n identity: ProjectIdentity;\n resolverRevision: string;\n inputSignature: string;\n}\n\nconst MANIFESTS = [\n \"package.json\",\n \"Cargo.toml\",\n \"pyproject.toml\",\n \"go.mod\",\n \"Gemfile\",\n \"pom.xml\",\n \"build.gradle\",\n] as const;\n\nconst PARSEABLE_MANIFESTS = [\"package.json\", \"Cargo.toml\", \"pyproject.toml\"] as const;\n\nconst LOOSE_DIRS = new Set([\"/tmp\", \"/private/tmp\"]);\nconst LOOSE_HOME_DIRS = [\"Desktop\", \"Downloads\", \"Documents\"];\n\ntype PathOps = Pick<\n typeof path.posix,\n \"dirname\" | \"isAbsolute\" | \"join\" | \"relative\" | \"resolve\" | \"sep\"\n>;\n\n// Identity semantics live in the contract; this module only discovers them\n// from the filesystem and git.\nexport {\n getProjectIdentityKey,\n isProjectIdentityKind,\n matchesProjectIdentity,\n} from \"../contract/project-identity.js\";\n\nexport function normalizeGitRemote(url: string): string | null {\n if (!url) return null;\n let value = url.trim();\n if (/^[a-z][a-z\\d+.-]*:\\/\\//i.test(value)) {\n try {\n const parsed = new URL(value);\n value = `${parsed.hostname}${parsed.pathname}`;\n } catch {\n return null;\n }\n } else {\n const sshMatch = value.match(/^[^@]+@([^:]+):(.+)$/);\n if (sshMatch) value = `${sshMatch[1]}/${sshMatch[2]}`;\n }\n value = value.replace(/\\.git$/, \"\");\n if (!value.includes(\"/\")) return null;\n return value.toLowerCase();\n}\n\n// Process-lifetime identity cache, keyed by directory. Only used for the real\n// filesystem (fs === realFs); tests inject fake IdentityFs implementations and\n// must bypass the cache to stay isolated from each other. git remotes change\n// rarely, so a coarse TTL is enough — no file-watch invalidation needed.\nconst IDENTITY_CACHE_TTL_MS = 10 * 60 * 1000;\nexport const IDENTITY_CACHE_MAX_ENTRIES = 512;\n\ninterface IdentityCacheEntry {\n projection: ProjectIdentityProjection;\n resolvedAt: number;\n}\n\nconst identityCache = new Map<string, IdentityCacheEntry>();\n\n/** Clears the process-lifetime identity cache. For tests and explicit invalidation. */\nexport function clearIdentityCache(): void {\n identityCache.clear();\n}\n\nexport function computeIdentity(cwd: string | null | undefined, fs: IdentityFs): ProjectIdentity {\n return computeIdentityProjection(cwd, fs).identity;\n}\n\nexport function normalizeProjectDirectory(cwd: string | null | undefined): string {\n if (!cwd) return \"\";\n return getPathOps(cwd).resolve(cwd);\n}\n\nexport function computeIdentityProjection(\n cwd: string | null | undefined,\n fs: IdentityFs = realFs,\n resolverRevision = PROJECT_IDENTITY_RESOLVER_REVISION,\n): ProjectIdentityProjection {\n if (fs !== realFs) return resolveIdentityProjection(cwd, fs, resolverRevision);\n\n const key = normalizeProjectDirectory(cwd);\n const cached = identityCache.get(key);\n if (cached) {\n if (\n cached.projection.resolverRevision === resolverRevision &&\n Date.now() - cached.resolvedAt < IDENTITY_CACHE_TTL_MS\n ) {\n identityCache.delete(key);\n identityCache.set(key, cached);\n return cached.projection;\n }\n identityCache.delete(key);\n }\n const projection = resolveIdentityProjection(cwd, fs, resolverRevision);\n identityCache.set(key, { projection, resolvedAt: Date.now() });\n while (identityCache.size > IDENTITY_CACHE_MAX_ENTRIES) {\n const oldestKey = identityCache.keys().next().value;\n if (oldestKey == null) break;\n identityCache.delete(oldestKey);\n }\n return projection;\n}\n\nfunction resolveIdentityProjection(\n cwd: string | null | undefined,\n fs: IdentityFs,\n resolverRevision: string,\n): ProjectIdentityProjection {\n if (!cwd) return projectIdentityProjection(loose(), resolverRevision, [\"loose\", \"missing\"]);\n\n const pathOps = getPathOps(cwd);\n const absoluteCwd = pathOps.resolve(cwd);\n const homeDir = os.homedir();\n const homePathOps = getPathOps(homeDir);\n const home = homePathOps === pathOps ? pathOps.resolve(homeDir) : homeDir;\n if (absoluteCwd === home || LOOSE_DIRS.has(absoluteCwd)) {\n return projectIdentityProjection(loose(), resolverRevision, [\"loose\", absoluteCwd]);\n }\n if (\n homePathOps === pathOps &&\n LOOSE_HOME_DIRS.some((dir) => absoluteCwd === pathOps.join(home, dir))\n ) {\n return projectIdentityProjection(loose(), resolverRevision, [\"loose\", absoluteCwd]);\n }\n\n const gitRoot = findGitRoot(absoluteCwd, fs, pathOps);\n if (gitRoot) {\n const remote = fs.spawn(\"git\", [\"config\", \"--get\", \"remote.origin.url\"], { cwd: gitRoot });\n if (remote.exitCode === 0) {\n const normalized = normalizeGitRemote(remote.stdout.trim());\n if (normalized) {\n const identity: ProjectIdentity = {\n kind: \"git_remote\",\n key: normalized,\n displayName: deriveDisplayName({ kind: \"git_remote\", key: normalized, gitRoot, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"git_remote\",\n gitRoot,\n normalized,\n identity.displayName,\n ]);\n }\n }\n\n const common = fs.spawn(\"git\", [\"rev-parse\", \"--git-common-dir\"], { cwd: gitRoot });\n if (common.exitCode === 0) {\n const raw = common.stdout.trim();\n if (raw) {\n const key = pathOps.isAbsolute(raw) ? raw : pathOps.resolve(gitRoot, raw);\n const identity: ProjectIdentity = {\n kind: \"git_common_dir\",\n key,\n displayName: deriveDisplayName({ kind: \"git_common_dir\", key, gitRoot, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"git_common_dir\",\n gitRoot,\n key,\n identity.displayName,\n ]);\n }\n }\n }\n\n const manifestDir = findManifestDir(absoluteCwd, fs, pathOps);\n if (manifestDir) {\n const identity: ProjectIdentity = {\n kind: \"manifest_path\",\n key: manifestDir,\n displayName: deriveDisplayName({ kind: \"manifest_path\", key: manifestDir, fs }),\n };\n return projectIdentityProjection(identity, resolverRevision, [\n \"manifest_path\",\n manifestDir,\n identity.displayName,\n ]);\n }\n\n if (homePathOps === pathOps) {\n const synthetic = synthesizeCodexScratchIdentity(absoluteCwd, home, pathOps);\n if (synthetic) {\n return projectIdentityProjection(synthetic, resolverRevision, [\"synthetic\", synthetic.key]);\n }\n }\n\n const identity: ProjectIdentity = {\n kind: \"path\",\n key: absoluteCwd,\n displayName: fallbackDisplayName(absoluteCwd),\n };\n return projectIdentityProjection(identity, resolverRevision, [\"path\", absoluteCwd]);\n}\n\nfunction projectIdentityProjection(\n identity: ProjectIdentity,\n resolverRevision: string,\n inputs: readonly string[],\n): ProjectIdentityProjection {\n return {\n identity,\n resolverRevision,\n inputSignature: createHash(\"sha256\").update(JSON.stringify(inputs)).digest(\"hex\"),\n };\n}\n\nfunction loose(): ProjectIdentity {\n return { kind: \"loose\", key: \"loose\", displayName: \"Loose\" };\n}\n\nfunction synthesizeCodexScratchIdentity(\n absoluteCwd: string,\n home: string,\n pathOps: PathOps,\n): ProjectIdentity | null {\n const root = pathOps.resolve(pathOps.join(home, \"Documents\", \"Codex\"));\n const child = pathOps.relative(root, absoluteCwd);\n if (\n !child ||\n child === \"..\" ||\n child.startsWith(`..${pathOps.sep}`) ||\n pathOps.isAbsolute(child)\n ) {\n return null;\n }\n return { kind: \"synthetic\", key: \"codex:scratch\", displayName: \"Chats\" };\n}\n\nfunction getPathOps(input: string): PathOps {\n if (/^[a-zA-Z]:[\\\\/]/.test(input) || input.startsWith(\"\\\\\\\\\")) {\n return path.win32;\n }\n if (input.startsWith(\"/\")) return path.posix;\n return path;\n}\n\nfunction findGitRoot(start: string, fs: IdentityFs, pathOps: PathOps): string | null {\n let current = start;\n while (current) {\n if (fs.exists(pathOps.join(current, \".git\"))) return current;\n const parent = pathOps.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n return null;\n}\n\nfunction findManifestDir(start: string, fs: IdentityFs, pathOps: PathOps): string | null {\n let current = start;\n while (current) {\n for (const manifest of MANIFESTS) {\n if (fs.exists(pathOps.join(current, manifest))) return current;\n }\n const parent = pathOps.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n return null;\n}\n\ninterface DisplayNameInput {\n kind: ProjectIdentityKind;\n key: string;\n gitRoot?: string;\n fs: IdentityFs;\n}\n\nfunction deriveDisplayName(input: DisplayNameInput): string {\n const pathOps = getPathOps(input.gitRoot ?? input.key);\n const dir = input.gitRoot ?? (input.kind === \"manifest_path\" ? input.key : null);\n if (dir) {\n for (const manifest of PARSEABLE_MANIFESTS) {\n const manifestPath = pathOps.join(dir, manifest);\n if (input.fs.exists(manifestPath)) {\n const name = parseManifestName(manifest, input.fs.readText(manifestPath) ?? \"\");\n if (name) return name;\n }\n }\n }\n\n if (input.kind === \"git_remote\") {\n return input.key.split(\"/\").at(-1) || input.key;\n }\n if (input.gitRoot) return fallbackDisplayName(input.gitRoot);\n return fallbackDisplayName(input.key);\n}\n\nfunction parseManifestName(file: string, text: string): string | null {\n if (!text) return null;\n if (file === \"package.json\" || file === \"Cargo.toml\" || file === \"pyproject.toml\") {\n const match = text.match(/\"name\"\\s*:\\s*\"([^\"]+)\"/) || text.match(/^\\s*name\\s*=\\s*\"([^\"]+)\"/m);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n","export function fallbackDisplayName(input: string): string {\n if (input === \"/\") return \"(root)\";\n const trimmed = input.replace(/[/\\\\]+$/, \"\");\n const parts = trimmed.split(/[/\\\\]+/).filter(Boolean);\n return parts.at(-1) ?? trimmed;\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { spawnSync } from \"node:child_process\";\n\nexport interface IdentityFs {\n exists(path: string): boolean;\n readText(path: string): string | null;\n spawn(cmd: string, args: string[], opts: { cwd: string }): { stdout: string; exitCode: number };\n}\n\nexport const realFs: IdentityFs = {\n exists(path) {\n return existsSync(path);\n },\n readText(path) {\n try {\n return readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n },\n spawn(cmd, args, opts) {\n const result = spawnSync(cmd, args, { cwd: opts.cwd, encoding: \"utf8\", timeout: 1000 });\n return {\n stdout: typeof result.stdout === \"string\" ? result.stdout : \"\",\n exitCode: result.status ?? 1,\n };\n },\n};\n","import type { ProjectIdentityRef, SessionHead } from \"../types/index.js\";\nimport {\n computeIdentity,\n matchesProjectIdentity,\n normalizeProjectDirectory,\n type IdentityFs,\n} from \"./identity.js\";\nimport { realFs } from \"./fs.js\";\n\nexport interface ProjectScopeMatcher {\n identity: ProjectIdentityRef;\n path: string;\n}\n\nexport function createProjectScopeMatcher(\n queryPath: string,\n fs: IdentityFs = realFs,\n): ProjectScopeMatcher {\n const identity = computeIdentity(queryPath, fs);\n return createProjectScopeMatcherFromIdentity(queryPath, identity);\n}\n\nexport function createProjectScopeMatcherFromIdentity(\n queryPath: string,\n identity: ProjectIdentityRef,\n): ProjectScopeMatcher {\n return {\n identity: { kind: identity.kind, key: identity.key },\n path: normalizeProjectScopePath(queryPath),\n };\n}\n\nexport function matchesProjectScope(session: SessionHead, scope: ProjectScopeMatcher): boolean {\n if (matchesProjectIdentity(session.project_identity, scope.identity)) return true;\n return session.directory ? isPathScopeMatch(scope.path, session.directory) : false;\n}\n\nexport function filterSessionsByProjectScope<T extends SessionHead>(\n sessions: T[],\n queryPath: string,\n fs?: IdentityFs,\n): T[] {\n const scope = createProjectScopeMatcher(queryPath, fs);\n return sessions.filter((session) => matchesProjectScope(session, scope));\n}\n\nfunction isPathScopeMatch(queryPath: string, sessionPath: string): boolean {\n const session = normalizeProjectScopePath(sessionPath);\n return (\n session === queryPath ||\n session.startsWith(queryPath + \"/\") ||\n queryPath.startsWith(session + \"/\")\n );\n}\n\nexport function normalizeProjectScopePath(path: string): string {\n return normalizeProjectDirectory(path).replaceAll(\"\\\\\", \"/\");\n}\n","import type { ProjectGroup, ProjectIdentity, SessionHead } from \"../types/index.js\";\nimport { buildSessionTree, getSessionAgentKey } from \"../contract/index.js\";\nimport { getProjectIdentityKey } from \"./identity.js\";\n\nexport function buildProjectGroups(sessions: SessionHead[]): ProjectGroup[] {\n const groups = new Map<\n string,\n { identity: ProjectIdentity; sources: Set<string>; sessionCount: number; lastActivity: number }\n >();\n\n const tree = buildSessionTree(sessions);\n for (const session of sessions) {\n if (tree.mountStateOf(session) === \"mounted-child\") continue;\n const identity = session.project_identity;\n if (!identity) continue;\n const activity = session.time_updated ?? session.time_created;\n const groupKey = getProjectIdentityKey(identity);\n const current = groups.get(groupKey);\n if (current) {\n current.sources.add(getSessionAgentKey(session));\n current.sessionCount += 1;\n current.lastActivity = Math.max(current.lastActivity, activity);\n } else {\n groups.set(groupKey, {\n identity,\n sources: new Set([getSessionAgentKey(session)]),\n sessionCount: 1,\n lastActivity: activity,\n });\n }\n }\n\n return [...groups.values()]\n .map((group) => ({\n identityKind: group.identity.kind,\n identityKey: group.identity.key,\n displayName: group.identity.displayName,\n sources: [...group.sources].sort(),\n sessionCount: group.sessionCount,\n lastActivity: group.lastActivity || null,\n }))\n .sort((a, b) => {\n if (a.identityKind === \"loose\" && b.identityKind !== \"loose\") return 1;\n if (b.identityKind === \"loose\" && a.identityKind !== \"loose\") return -1;\n return (b.lastActivity ?? 0) - (a.lastActivity ?? 0);\n });\n}\n"],"mappings":"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","names":["path","identity"]}
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
getSessionAgentKey,
|
|
9
9
|
getSessionReferenceKey,
|
|
10
10
|
toCalendarDayKey
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YLXHGL4S.js";
|
|
12
12
|
|
|
13
|
-
// ../core/dist/chunk-
|
|
13
|
+
// ../core/dist/chunk-XNDXGBK6.mjs
|
|
14
14
|
var COST_ABSOLUTE_TOLERANCE = 1e-8;
|
|
15
15
|
var COST_RELATIVE_TOLERANCE = 1e-6;
|
|
16
16
|
function indexFacts(facts) {
|
|
@@ -147,7 +147,8 @@ function visitAttributedUsage(tree, options, visit) {
|
|
|
147
147
|
inputTokens: detailedTokens ? message.inputTokens : 0,
|
|
148
148
|
outputTokens,
|
|
149
149
|
cacheReadTokens: detailedTokens ? message.cacheReadTokens : 0,
|
|
150
|
-
cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0
|
|
150
|
+
cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0,
|
|
151
|
+
modelUsage: detailedTokens && message.model ? { [message.model]: message.inputTokens + outputTokens } : void 0
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -167,15 +168,22 @@ function visitAttributedUsage(tree, options, visit) {
|
|
|
167
168
|
if (!detailedTokens) {
|
|
168
169
|
const inputTokens = nonNegative(session.stats.total_input_tokens);
|
|
169
170
|
const outputTokens = nonNegative(session.stats.total_output_tokens);
|
|
171
|
+
const totalTokens = nonNegative(session.stats.total_tokens ?? inputTokens + outputTokens);
|
|
172
|
+
const modelTotal = Object.values(session.model_usage ?? {}).reduce(
|
|
173
|
+
(sum, tokens) => sum + tokens,
|
|
174
|
+
0
|
|
175
|
+
);
|
|
170
176
|
visit({
|
|
171
177
|
entry,
|
|
172
178
|
time: fallbackTime,
|
|
173
179
|
messages: 0,
|
|
174
|
-
totalTokens
|
|
180
|
+
totalTokens,
|
|
175
181
|
inputTokens,
|
|
176
182
|
outputTokens,
|
|
177
183
|
cacheReadTokens: nonNegative(session.stats.total_cache_read_tokens),
|
|
178
|
-
cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens)
|
|
184
|
+
cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens),
|
|
185
|
+
// Unreconciled model totals stay unassigned rather than inflating their shares.
|
|
186
|
+
modelUsage: modelTotal <= totalTokens ? session.model_usage : void 0
|
|
179
187
|
});
|
|
180
188
|
}
|
|
181
189
|
}
|
|
@@ -246,14 +254,6 @@ function createAccumulator(byAgentNames, scope, to) {
|
|
|
246
254
|
recent: []
|
|
247
255
|
};
|
|
248
256
|
}
|
|
249
|
-
function foldModelUsage(node, into) {
|
|
250
|
-
if (node.session.model_usage) {
|
|
251
|
-
for (const [model, tokens] of Object.entries(node.session.model_usage)) {
|
|
252
|
-
into.set(model, (into.get(model) ?? 0) + tokens);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
for (const child of node.children) foldModelUsage(child, into);
|
|
256
|
-
}
|
|
257
257
|
function getOrCreateProject(session, acc) {
|
|
258
258
|
const identity = session.project_identity;
|
|
259
259
|
if (!identity) return void 0;
|
|
@@ -301,17 +301,6 @@ function accumulateActivity(node, acc) {
|
|
|
301
301
|
acc.daily.set(dayKey, bucket);
|
|
302
302
|
}
|
|
303
303
|
bucket.sessions += 1;
|
|
304
|
-
const usage = /* @__PURE__ */ new Map();
|
|
305
|
-
foldModelUsage(node, usage);
|
|
306
|
-
for (const [model, tokens] of usage) {
|
|
307
|
-
const entry = acc.models.get(model);
|
|
308
|
-
if (entry) {
|
|
309
|
-
entry.tokens += tokens;
|
|
310
|
-
entry.sessions += 1;
|
|
311
|
-
} else {
|
|
312
|
-
acc.models.set(model, { tokens, sessions: 1 });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
304
|
trackProjectActivity(node, acc, agentKey);
|
|
316
305
|
let recentIndex = acc.recent.length;
|
|
317
306
|
for (let i = 0; i < acc.recent.length; i += 1) {
|
|
@@ -333,6 +322,16 @@ function addUsage(acc, entry, time, usage) {
|
|
|
333
322
|
acc.messages += messages;
|
|
334
323
|
acc.tokens += totalTokens;
|
|
335
324
|
acc.cacheReadTokens += cacheReadTokens;
|
|
325
|
+
for (const [model, tokens] of Object.entries(usage.modelUsage ?? {})) {
|
|
326
|
+
if (tokens <= 0) continue;
|
|
327
|
+
const current = acc.models.get(model);
|
|
328
|
+
if (current) {
|
|
329
|
+
current.tokens += tokens;
|
|
330
|
+
current.sessions.add(entry);
|
|
331
|
+
} else {
|
|
332
|
+
acc.models.set(model, { tokens, sessions: /* @__PURE__ */ new Set([entry]) });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
336
335
|
const agentKey = getSessionAgentName(entry.session);
|
|
337
336
|
let metric = acc.agents.get(agentKey);
|
|
338
337
|
if (!metric) {
|
|
@@ -478,7 +477,7 @@ function buildDashboard(sessions, options) {
|
|
|
478
477
|
};
|
|
479
478
|
}).filter((item) => item.sessions > 0 || item.messages > 0 || item.tokens > 0 || item.cost > 0).sort((a, b) => b.sessions - a.sessions || b.cost - a.cost);
|
|
480
479
|
const dailyActivity = [...acc.daily.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
481
|
-
const modelDistribution = [...acc.models.entries()].map(([model, { tokens, sessions:
|
|
480
|
+
const modelDistribution = [...acc.models.entries()].map(([model, { tokens, sessions: sessions2 }]) => ({ model, tokens, sessions: sessions2.size })).sort((a, b) => b.tokens - a.tokens);
|
|
482
481
|
const modelCost = costFactsAvailable ? [...acc.modelCosts.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_COST_LIMIT) : null;
|
|
483
482
|
const rankedProjects = [...acc.projects.values()].sort((a, b) => b.cost - a.cost);
|
|
484
483
|
const perProject = rankedProjects.slice(0, DASHBOARD_PROJECT_LIMIT).map(toProjectStat);
|
|
@@ -667,4 +666,4 @@ export {
|
|
|
667
666
|
summarizeProjects,
|
|
668
667
|
attachProjectMetricsFromTree
|
|
669
668
|
};
|
|
670
|
-
//# sourceMappingURL=chunk-
|
|
669
|
+
//# sourceMappingURL=chunk-L4G3LE7I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../core/src/analytics/cost-attribution.ts","../../core/src/analytics/dashboard.ts","../../core/src/analytics/projects.ts"],"sourcesContent":["import type { CostSource, SessionHead } from \"../types/session.js\";\nimport {\n getSessionReferenceKey,\n type SessionTree,\n type SessionTreeNode,\n} from \"../contract/index.js\";\nimport type {\n DashboardCostFacts,\n MessageCostFact,\n SessionCostSummary,\n SessionModelCostFact,\n} from \"./cost-facts.js\";\n\nconst COST_ABSOLUTE_TOLERANCE = 1e-8;\nconst COST_RELATIVE_TOLERANCE = 1e-6;\n\ninterface MetricFactIndex {\n messagesBySession: Map<string, MessageCostFact[]>;\n summariesBySession: Map<string, SessionCostSummary>;\n}\n\nexport interface AttributedCost {\n entry: SessionTreeNode;\n time: number;\n cost: number;\n source: CostSource;\n modelCosts: SessionModelCostFact[];\n}\n\nexport interface CostAttributionOptions {\n from?: number;\n to: number;\n facts?: DashboardCostFacts | null;\n matchesEntry?: (session: SessionHead) => boolean;\n}\n\nexport interface AttributedUsage {\n entry: SessionTreeNode;\n time: number;\n messages: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheCreateTokens: number;\n modelUsage?: Readonly<Record<string, number>>;\n}\n\nexport type UsageAttributionOptions = CostAttributionOptions;\n\nfunction indexFacts(facts: DashboardCostFacts | null | undefined): MetricFactIndex {\n const messagesBySession = new Map<string, MessageCostFact[]>();\n const summariesBySession = new Map<string, SessionCostSummary>();\n if (!facts) return { messagesBySession, summariesBySession };\n\n for (const summary of facts.sessions) {\n summariesBySession.set(getSessionReferenceKey(summary.reference), summary);\n }\n for (const message of facts.messages) {\n const key = getSessionReferenceKey(message.reference);\n const messages = messagesBySession.get(key);\n if (messages) messages.push(message);\n else messagesBySession.set(key, [message]);\n }\n return { messagesBySession, summariesBySession };\n}\n\nfunction nonNegative(value: number | undefined): number {\n return Math.max(0, Number(value ?? 0));\n}\n\nfunction costsReconcile(left: number, right: number): boolean {\n const tolerance = Math.max(\n COST_ABSOLUTE_TOLERANCE,\n Math.max(Math.abs(left), Math.abs(right)) * COST_RELATIVE_TOLERANCE,\n );\n return Math.abs(left - right) <= tolerance;\n}\n\nfunction hasDetailedCost(session: SessionHead, summary: SessionCostSummary | undefined): boolean {\n const totalCost = Math.max(0, session.stats.total_cost);\n return (\n totalCost > 0 &&\n summary != null &&\n summary.untimedMessageCost <= COST_ABSOLUTE_TOLERANCE &&\n costsReconcile(summary.messageCost, totalCost)\n );\n}\n\nfunction hasDetailedMessages(\n session: SessionHead,\n summary: SessionCostSummary | undefined,\n): boolean {\n return (\n summary != null &&\n summary.untimedMessageCount === 0 &&\n summary.messageCount === nonNegative(session.stats.message_count)\n );\n}\n\n// Provider payloads disagree on whether reasoning is already included in output.\nfunction detailedOutputIncludesReasoning(\n session: SessionHead,\n summary: SessionCostSummary | undefined,\n): boolean | null {\n if (!summary) return null;\n const stats = session.stats;\n const sessionInput = nonNegative(stats.total_input_tokens);\n const sessionOutput = nonNegative(stats.total_output_tokens);\n const sessionCacheRead = nonNegative(stats.total_cache_read_tokens);\n const sessionCacheCreate = nonNegative(stats.total_cache_create_tokens);\n const sessionTotal = nonNegative(stats.total_tokens ?? sessionInput + sessionOutput);\n const outputIncludesReasoning = summary.outputTokens + summary.reasoningTokens;\n const outputMatches = summary.outputTokens === sessionOutput;\n const outputWithReasoningMatches = outputIncludesReasoning === sessionOutput;\n const effectiveOutput = outputMatches\n ? summary.outputTokens\n : outputWithReasoningMatches\n ? outputIncludesReasoning\n : null;\n\n if (\n effectiveOutput == null ||\n summary.inputTokens !== sessionInput ||\n summary.cacheReadTokens !== sessionCacheRead ||\n summary.cacheCreateTokens !== sessionCacheCreate ||\n summary.inputTokens + effectiveOutput !== sessionTotal ||\n summary.untimedInputTokens > 0 ||\n summary.untimedOutputTokens > 0 ||\n summary.untimedReasoningTokens > 0 ||\n summary.untimedCacheReadTokens > 0 ||\n summary.untimedCacheCreateTokens > 0\n ) {\n return null;\n }\n return !outputMatches && outputWithReasoningMatches;\n}\n\nfunction isInWindow(time: number, from: number | undefined, to: number): boolean {\n return (from == null || time >= from) && time <= to;\n}\n\nfunction resolvedCostSource(session: SessionHead, source?: CostSource): CostSource {\n return source ?? session.stats.cost_source ?? \"recorded\";\n}\n\nfunction fallbackModelCosts(\n summary: SessionCostSummary | undefined,\n totalCost: number,\n): SessionModelCostFact[] {\n if (!summary) return [];\n const modelTotal = summary.modelCosts.reduce((sum, model) => sum + model.cost, 0);\n return modelTotal > totalCost && !costsReconcile(modelTotal, totalCost) ? [] : summary.modelCosts;\n}\n\nexport function visitAttributedCosts(\n tree: SessionTree,\n options: CostAttributionOptions,\n visit: (attributed: AttributedCost) => void,\n): void {\n const factIndex = indexFacts(options.facts);\n const factsAvailable = options.facts != null;\n\n for (const entry of tree.entries) {\n if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;\n const fallbackTime = entry.session.time_updated ?? entry.session.time_created;\n const pending = [entry];\n\n while (pending.length > 0) {\n const node = pending.pop()!;\n for (const child of node.children) pending.push(child);\n\n const session = node.session;\n const totalCost = Math.max(0, session.stats.total_cost);\n if (totalCost <= 0) continue;\n const key = getSessionReferenceKey(session.reference);\n const summary = factIndex.summariesBySession.get(key);\n\n if (factsAvailable && hasDetailedCost(session, summary)) {\n for (const message of factIndex.messagesBySession.get(key) ?? []) {\n if (!isInWindow(message.time, options.from, options.to) || message.cost <= 0) continue;\n const source = resolvedCostSource(session, message.costSource);\n visit({\n entry,\n time: message.time,\n cost: message.cost,\n source,\n modelCosts: message.model\n ? [\n {\n model: message.model,\n cost: message.cost,\n costRecorded: source === \"recorded\" ? message.cost : 0,\n },\n ]\n : [],\n });\n }\n continue;\n }\n\n if (!isInWindow(fallbackTime, options.from, options.to)) continue;\n visit({\n entry,\n time: fallbackTime,\n cost: totalCost,\n source: resolvedCostSource(session),\n modelCosts: factsAvailable ? fallbackModelCosts(summary, totalCost) : [],\n });\n }\n }\n}\n\nexport function visitAttributedUsage(\n tree: SessionTree,\n options: UsageAttributionOptions,\n visit: (attributed: AttributedUsage) => void,\n): void {\n const factIndex = indexFacts(options.facts);\n const factsAvailable = options.facts != null;\n\n for (const entry of tree.entries) {\n if (options.matchesEntry && !options.matchesEntry(entry.session)) continue;\n const fallbackTime = entry.session.time_updated ?? entry.session.time_created;\n const pending = [entry];\n\n while (pending.length > 0) {\n const node = pending.pop()!;\n for (const child of node.children) pending.push(child);\n\n const session = node.session;\n const key = getSessionReferenceKey(session.reference);\n const summary = factIndex.summariesBySession.get(key);\n const detailedMessages = factsAvailable && hasDetailedMessages(session, summary);\n const outputIncludesReasoning = factsAvailable\n ? detailedOutputIncludesReasoning(session, summary)\n : null;\n const detailedTokens = outputIncludesReasoning != null;\n\n if (detailedMessages || detailedTokens) {\n for (const message of factIndex.messagesBySession.get(key) ?? []) {\n if (!isInWindow(message.time, options.from, options.to)) continue;\n const outputTokens = detailedTokens\n ? message.outputTokens + (outputIncludesReasoning ? message.reasoningTokens : 0)\n : 0;\n visit({\n entry,\n time: message.time,\n messages: detailedMessages ? 1 : 0,\n totalTokens: detailedTokens ? message.inputTokens + outputTokens : 0,\n inputTokens: detailedTokens ? message.inputTokens : 0,\n outputTokens,\n cacheReadTokens: detailedTokens ? message.cacheReadTokens : 0,\n cacheCreateTokens: detailedTokens ? message.cacheCreateTokens : 0,\n modelUsage:\n detailedTokens && message.model\n ? { [message.model]: message.inputTokens + outputTokens }\n : undefined,\n });\n }\n }\n\n if (!isInWindow(fallbackTime, options.from, options.to)) continue;\n if (!detailedMessages) {\n visit({\n entry,\n time: fallbackTime,\n messages: nonNegative(session.stats.message_count),\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheCreateTokens: 0,\n });\n }\n if (!detailedTokens) {\n const inputTokens = nonNegative(session.stats.total_input_tokens);\n const outputTokens = nonNegative(session.stats.total_output_tokens);\n const totalTokens = nonNegative(session.stats.total_tokens ?? inputTokens + outputTokens);\n const modelTotal = Object.values(session.model_usage ?? {}).reduce(\n (sum, tokens) => sum + tokens,\n 0,\n );\n visit({\n entry,\n time: fallbackTime,\n messages: 0,\n totalTokens,\n inputTokens,\n outputTokens,\n cacheReadTokens: nonNegative(session.stats.total_cache_read_tokens),\n cacheCreateTokens: nonNegative(session.stats.total_cache_create_tokens),\n // Unreconciled model totals stay unassigned rather than inflating their shares.\n modelUsage: modelTotal <= totalTokens ? session.model_usage : undefined,\n });\n }\n }\n }\n}\n","/**\n * Dashboard analytics — pure aggregation over SessionHead[] with no HTTP or DB\n * coupling. The handler parses request params and supplies file-activity; this\n * module owns the domain math: per-agent metrics, daily buckets, model\n * distribution, project ranking and the recent-sessions window.\n *\n * Metrics are *inclusive*: the unit of work is a top-level node (root or\n * orphan) and every descendant's stats roll into it. See contract/session-tree.\n */\nimport type { AgentInfo } from \"../types/index.js\";\nimport type { CostSource, ProjectIdentityKind, SessionHead } from \"../types/session.js\";\nimport type {\n DashboardAgentStat,\n DashboardAggregate,\n DashboardDailyBucket,\n DashboardData,\n DashboardPreviousTotals,\n DashboardProjectRollup,\n DashboardProjectStat,\n DashboardRecentSession,\n DashboardTotals,\n ModelCostEntry,\n ModelDistributionEntry,\n SessionTree,\n SessionTreeNode,\n} from \"../contract/index.js\";\nimport {\n addCalendarDays,\n buildSessionTree,\n countCalendarDays,\n filterSessionTreeEntriesByActivityWindow,\n getProjectIdentityKey,\n getSessionAgentKey,\n toCalendarDayKey,\n} from \"../contract/index.js\";\nimport type { DashboardCostFacts } from \"./cost-facts.js\";\nimport {\n visitAttributedCosts,\n visitAttributedUsage,\n type AttributedUsage,\n} from \"./cost-attribution.js\";\n\nexport type {\n DashboardAgentStat,\n DashboardAggregate,\n DashboardDailyBucket,\n DashboardData,\n DashboardPreviousTotals,\n DashboardProjectRollup,\n DashboardProjectStat,\n DashboardRecentSession,\n DashboardTotals,\n ModelDistributionEntry,\n};\n\nexport const DASHBOARD_RECENT_LIMIT = 10;\nexport const DASHBOARD_PROJECT_LIMIT = 12;\nexport const PROJECT_SPARKLINE_DAYS = 14;\nconst MODEL_COST_LIMIT = 20;\n\nexport interface DashboardScope {\n agent?: string;\n projectKind?: ProjectIdentityKind;\n projectKey?: string;\n}\n\ninterface DashboardAgentAggregate {\n name: string;\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n}\n\ninterface DashboardProjectAggregate {\n identityKind: ProjectIdentityKind;\n identityKey: string;\n displayName: string;\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n hasEstimatedCost: boolean;\n agentSessions: Map<string, number>;\n sparkline: number[];\n}\n\ninterface DashboardRecentCandidate {\n session: SessionHead;\n activity: number;\n}\n\ninterface DashboardAccumulator {\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n costRecorded: number;\n costEstimated: number;\n cacheReadTokens: number;\n hasEstimatedCost: boolean;\n agents: Map<string, DashboardAgentAggregate>;\n agentKeys: Set<string>;\n daily: Map<string, DashboardDailyBucket>;\n models: Map<string, { tokens: number; sessions: Set<SessionTreeNode> }>;\n modelCosts: Map<string, ModelCostEntry>;\n projects: Map<string, DashboardProjectAggregate>;\n projectKeys: Set<string>;\n /** Calendar day key → sparkline slot; anchored at the window's `to`. */\n sparklineSlots: Map<string, number>;\n /** Activity-desc, capped at DASHBOARD_RECENT_LIMIT; index 0 is the latest entry. */\n recent: DashboardRecentCandidate[];\n}\n\nexport interface DashboardOptions {\n byAgentNames: string[];\n scope: DashboardScope;\n from?: number;\n to: number;\n agentInfoMap?: Map<string, AgentInfo>;\n /** Immediately preceding window of equal length; drives `totals.previous`. */\n compare?: { from: number; to: number };\n /** Message-level cost and usage facts; null/undefined keeps session-activity fallback behavior. */\n costFacts?: DashboardCostFacts | null;\n}\n\n// --- SessionHead domain helpers (shared with other handlers via re-export) ---\n\nexport function getTotalTokens(stats: SessionHead[\"stats\"]): number {\n return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;\n}\n\nexport function getSessionAgentName(session: SessionHead): string {\n return getSessionAgentKey(session);\n}\n\nexport function getSessionActivityTime(session: SessionHead): number {\n return session.time_updated ?? session.time_created;\n}\n\nfunction matchesScope(session: SessionHead, scope: DashboardScope): boolean {\n if (scope.agent && getSessionAgentName(session) !== scope.agent) return false;\n if (scope.projectKind == null && scope.projectKey == null) return true;\n const identity = session.project_identity;\n // A half-specified scope matches nothing: identity fields are never undefined.\n return (\n identity != null && identity.kind === scope.projectKind && identity.key === scope.projectKey\n );\n}\n\n/** Top-level nodes (roots ∪ orphans) inside the window that match the scope. */\nfunction scopedEntries(\n tree: SessionTree,\n scope: DashboardScope,\n from: number | undefined,\n to: number,\n): SessionTreeNode[] {\n return filterSessionTreeEntriesByActivityWindow(tree, from, to).filter((node) =>\n matchesScope(node.session, scope),\n );\n}\n\nfunction emptyDailyBucket(date: string): DashboardDailyBucket {\n return {\n date,\n sessions: 0,\n messages: 0,\n cost: 0,\n input: 0,\n output: 0,\n cache_read: 0,\n cache_create: 0,\n };\n}\n\nfunction createAccumulator(\n byAgentNames: string[],\n scope: DashboardScope,\n to: number,\n): DashboardAccumulator {\n const agents = new Map<string, DashboardAgentAggregate>();\n for (const name of byAgentNames) {\n const key = name.toLowerCase();\n if (scope.agent && key !== scope.agent) continue;\n agents.set(key, { name, sessions: 0, messages: 0, tokens: 0, cost: 0 });\n }\n\n const sparklineSlots = new Map<string, number>();\n for (let slot = 0; slot < PROJECT_SPARKLINE_DAYS; slot += 1) {\n const day = addCalendarDays(to, slot - (PROJECT_SPARKLINE_DAYS - 1));\n sparklineSlots.set(toCalendarDayKey(day), slot);\n }\n\n return {\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n costRecorded: 0,\n costEstimated: 0,\n cacheReadTokens: 0,\n hasEstimatedCost: false,\n agents,\n agentKeys: new Set(),\n daily: new Map(),\n models: new Map(),\n modelCosts: new Map(),\n projects: new Map(),\n projectKeys: new Set(),\n sparklineSlots,\n recent: [],\n };\n}\n\nfunction getOrCreateProject(\n session: SessionHead,\n acc: DashboardAccumulator,\n): DashboardProjectAggregate | undefined {\n const identity = session.project_identity;\n if (!identity) return undefined;\n\n const key = getProjectIdentityKey(identity);\n let project = acc.projects.get(key);\n if (!project) {\n project = {\n identityKind: identity.kind,\n identityKey: identity.key,\n displayName: identity.displayName,\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n hasEstimatedCost: false,\n agentSessions: new Map(),\n sparkline: Array.from({ length: PROJECT_SPARKLINE_DAYS }, () => 0),\n };\n acc.projects.set(key, project);\n }\n return project;\n}\n\nfunction trackProjectActivity(\n node: SessionTreeNode,\n acc: DashboardAccumulator,\n agentKey: string,\n): void {\n const identity = node.session.project_identity;\n const project = getOrCreateProject(node.session, acc);\n if (!identity || !project) return;\n\n project.sessions += 1;\n project.agentSessions.set(agentKey, (project.agentSessions.get(agentKey) ?? 0) + 1);\n acc.projectKeys.add(getProjectIdentityKey(identity));\n}\n\n/** Fold activity metrics for one top-level node and its inclusive subtree. */\nfunction accumulateActivity(node: SessionTreeNode, acc: DashboardAccumulator): void {\n const session = node.session;\n const activity = getSessionActivityTime(session);\n const agentKey = getSessionAgentName(session);\n\n acc.sessions += 1;\n acc.agentKeys.add(agentKey);\n\n const metric = acc.agents.get(agentKey);\n if (metric) {\n metric.sessions += 1;\n }\n\n const dayKey = toCalendarDayKey(activity);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.sessions += 1;\n\n trackProjectActivity(node, acc, agentKey);\n\n let recentIndex = acc.recent.length;\n for (let i = 0; i < acc.recent.length; i += 1) {\n if (activity > acc.recent[i]!.activity) {\n recentIndex = i;\n break;\n }\n }\n if (recentIndex < DASHBOARD_RECENT_LIMIT) {\n acc.recent.splice(recentIndex, 0, { session, activity });\n if (acc.recent.length > DASHBOARD_RECENT_LIMIT) acc.recent.pop();\n }\n}\n\nfunction addUsage(\n acc: DashboardAccumulator,\n entry: SessionTreeNode,\n time: number,\n usage: Omit<AttributedUsage, \"entry\" | \"time\">,\n): void {\n const { messages, totalTokens, inputTokens, outputTokens, cacheReadTokens, cacheCreateTokens } =\n usage;\n if (\n messages <= 0 &&\n totalTokens <= 0 &&\n inputTokens <= 0 &&\n outputTokens <= 0 &&\n cacheReadTokens <= 0 &&\n cacheCreateTokens <= 0\n ) {\n return;\n }\n\n acc.messages += messages;\n acc.tokens += totalTokens;\n acc.cacheReadTokens += cacheReadTokens;\n for (const [model, tokens] of Object.entries(usage.modelUsage ?? {})) {\n if (tokens <= 0) continue;\n const current = acc.models.get(model);\n if (current) {\n current.tokens += tokens;\n current.sessions.add(entry);\n } else {\n acc.models.set(model, { tokens, sessions: new Set([entry]) });\n }\n }\n\n const agentKey = getSessionAgentName(entry.session);\n let metric = acc.agents.get(agentKey);\n if (!metric) {\n metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };\n acc.agents.set(agentKey, metric);\n }\n metric.messages += messages;\n metric.tokens += totalTokens;\n\n const dayKey = toCalendarDayKey(time);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.messages += messages;\n bucket.input += Math.max(0, inputTokens - cacheReadTokens - cacheCreateTokens);\n bucket.output += outputTokens;\n bucket.cache_read += cacheReadTokens;\n bucket.cache_create += cacheCreateTokens;\n\n const project = getOrCreateProject(entry.session, acc);\n if (!project) return;\n project.messages += messages;\n project.tokens += totalTokens;\n}\n\nfunction accumulateUsage(\n tree: SessionTree,\n scope: DashboardScope,\n acc: DashboardAccumulator,\n from: number | undefined,\n to: number,\n costFacts: DashboardCostFacts | null | undefined,\n): void {\n visitAttributedUsage(\n tree,\n { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },\n ({ entry, time, ...usage }) => addUsage(acc, entry, time, usage),\n );\n}\n\nfunction addModelCost(\n acc: DashboardAccumulator,\n model: string | undefined,\n cost: number,\n recordedCost: number,\n): void {\n if (!model || cost <= 0) return;\n const recorded = Math.max(0, Math.min(cost, recordedCost));\n const current = acc.modelCosts.get(model);\n if (current) {\n current.cost += cost;\n current.costRecorded += recorded;\n current.costEstimated += cost - recorded;\n return;\n }\n acc.modelCosts.set(model, {\n model,\n cost,\n costRecorded: recorded,\n costEstimated: cost - recorded,\n });\n}\n\nfunction addCost(\n acc: DashboardAccumulator,\n entry: SessionTreeNode,\n time: number,\n cost: number,\n source: CostSource,\n): void {\n if (cost <= 0) return;\n acc.cost += cost;\n if (source === \"recorded\") acc.costRecorded += cost;\n else {\n acc.costEstimated += cost;\n acc.hasEstimatedCost = true;\n }\n\n const agentKey = getSessionAgentName(entry.session);\n let metric = acc.agents.get(agentKey);\n if (!metric) {\n metric = { name: agentKey, sessions: 0, messages: 0, tokens: 0, cost: 0 };\n acc.agents.set(agentKey, metric);\n }\n metric.cost += cost;\n\n const dayKey = toCalendarDayKey(time);\n let bucket = acc.daily.get(dayKey);\n if (!bucket) {\n bucket = emptyDailyBucket(dayKey);\n acc.daily.set(dayKey, bucket);\n }\n bucket.cost += cost;\n\n const project = getOrCreateProject(entry.session, acc);\n if (!project) return;\n project.cost += cost;\n if (source === \"estimated\") project.hasEstimatedCost = true;\n const slot = acc.sparklineSlots.get(dayKey);\n if (slot != null) project.sparkline[slot]! += cost;\n}\n\nfunction accumulateCosts(\n tree: SessionTree,\n scope: DashboardScope,\n acc: DashboardAccumulator,\n from: number | undefined,\n to: number,\n costFacts: DashboardCostFacts | null | undefined,\n): void {\n visitAttributedCosts(\n tree,\n { from, to, facts: costFacts, matchesEntry: (session) => matchesScope(session, scope) },\n ({ entry, time, cost, source, modelCosts }) => {\n addCost(acc, entry, time, cost, source);\n for (const model of modelCosts) {\n addModelCost(acc, model.model, model.cost, model.costRecorded);\n }\n },\n );\n}\n\nfunction toPreviousTotals(acc: DashboardAccumulator): DashboardPreviousTotals {\n return { sessions: acc.sessions, messages: acc.messages, tokens: acc.tokens, cost: acc.cost };\n}\n\nfunction toProjectStat(project: DashboardProjectAggregate): DashboardProjectStat {\n return {\n identityKind: project.identityKind,\n identityKey: project.identityKey,\n displayName: project.displayName,\n sessions: project.sessions,\n messages: project.messages,\n tokens: project.tokens,\n cost: project.cost,\n cost_source:\n project.cost > 0 ? (project.hasEstimatedCost ? \"estimated\" : \"recorded\") : undefined,\n agents: [...project.agentSessions.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([agentKey]) => agentKey),\n sparkline: project.sparkline,\n };\n}\n\n/**\n * Aggregate sessions into dashboard metrics, daily buckets, model distribution,\n * project ranking and the recent-sessions window. Pure — no HTTP, no DB.\n */\nexport function buildDashboard(\n sessions: SessionHead[],\n options: DashboardOptions,\n): DashboardAggregate {\n const { byAgentNames, scope, from, to, agentInfoMap, compare, costFacts } = options;\n const tree = buildSessionTree(sessions);\n const costFactsAvailable = costFacts != null;\n\n const acc = createAccumulator(byAgentNames, scope, to);\n if (from != null) {\n const bucketDays = countCalendarDays(from, to);\n for (let i = 0; i < bucketDays; i += 1) {\n const key = toCalendarDayKey(addCalendarDays(from, i));\n acc.daily.set(key, emptyDailyBucket(key));\n }\n }\n for (const node of scopedEntries(tree, scope, from, to)) accumulateActivity(node, acc);\n accumulateUsage(tree, scope, acc, from, to, costFacts);\n accumulateCosts(tree, scope, acc, from, to, costFacts);\n\n let previous: DashboardPreviousTotals | undefined;\n if (compare) {\n const compareAcc = createAccumulator(byAgentNames, scope, compare.to);\n for (const node of scopedEntries(tree, scope, compare.from, compare.to)) {\n accumulateActivity(node, compareAcc);\n }\n accumulateUsage(tree, scope, compareAcc, compare.from, compare.to, costFacts);\n accumulateCosts(tree, scope, compareAcc, compare.from, compare.to, costFacts);\n previous = toPreviousTotals(compareAcc);\n }\n\n const perAgent: DashboardAgentStat[] = [...acc.agents.values()]\n .map((metrics) => {\n const info = agentInfoMap?.get(metrics.name);\n return {\n name: metrics.name,\n displayName: info?.displayName ?? metrics.name,\n icon: info?.icon ?? \"\",\n iconColored: info?.iconColored,\n sessions: metrics.sessions,\n messages: metrics.messages,\n tokens: metrics.tokens,\n cost: metrics.cost,\n };\n })\n .filter((item) => item.sessions > 0 || item.messages > 0 || item.tokens > 0 || item.cost > 0)\n .sort((a, b) => b.sessions - a.sessions || b.cost - a.cost);\n\n const dailyActivity = [...acc.daily.values()].sort((a, b) => a.date.localeCompare(b.date));\n const modelDistribution: ModelDistributionEntry[] = [...acc.models.entries()]\n .map(([model, { tokens, sessions }]) => ({ model, tokens, sessions: sessions.size }))\n .sort((a, b) => b.tokens - a.tokens);\n const modelCost = costFactsAvailable\n ? [...acc.modelCosts.values()].sort((a, b) => b.cost - a.cost).slice(0, MODEL_COST_LIMIT)\n : null;\n\n const rankedProjects = [...acc.projects.values()].sort((a, b) => b.cost - a.cost);\n const perProject = rankedProjects.slice(0, DASHBOARD_PROJECT_LIMIT).map(toProjectStat);\n const projectRollup = rankedProjects\n .slice(DASHBOARD_PROJECT_LIMIT)\n .reduce<DashboardProjectRollup>(\n (rollup, project) => ({\n projects: rollup.projects + 1,\n sessions: rollup.sessions + project.sessions,\n tokens: rollup.tokens + project.tokens,\n cost: rollup.cost + project.cost,\n }),\n { projects: 0, sessions: 0, tokens: 0, cost: 0 },\n );\n\n const recentSessions: DashboardRecentSession[] = acc.recent.map(({ session }) => ({\n reference: session.reference,\n session,\n }));\n\n const latest = acc.recent[0];\n return {\n totals: {\n sessions: acc.sessions,\n messages: acc.messages,\n tokens: acc.tokens,\n cost: acc.cost,\n costRecorded: acc.costRecorded,\n costEstimated: acc.costEstimated,\n cacheReadTokens: acc.cacheReadTokens,\n cost_source: acc.cost > 0 ? (acc.hasEstimatedCost ? \"estimated\" : \"recorded\") : undefined,\n latestActivity: latest?.activity || undefined,\n latestActivityProject: latest?.session.project_identity?.displayName,\n latestActivityAgent: latest ? getSessionAgentName(latest.session) : undefined,\n previous,\n },\n scopeCounts: { projects: acc.projectKeys.size, agents: acc.agentKeys.size },\n perAgent,\n dailyActivity,\n modelDistribution,\n modelCost,\n perProject,\n projectRollup,\n recentSessions,\n };\n}\n","/**\n * Project analytics — pure aggregation over SessionHead[], the project-level\n * counterpart to dashboard.ts. No HTTP, no DB: the caller supplies the groups\n * and the sessions, this module owns the arithmetic.\n *\n * Metrics are inclusive of sub-sessions and `sessionCount` is recomputed from\n * the sessions actually handed in, so a windowed caller gets windowed counts\n * instead of the cache-wide count carried by ProjectGroup.\n */\nimport type { ProjectGroup, SessionHead } from \"../types/index.js\";\nimport type {\n ApiProjectAgentStat,\n ApiProjectGroup,\n ApiProjectSummary,\n SessionTree,\n SessionTreeNode,\n} from \"../contract/index.js\";\nimport { getProjectIdentityKey } from \"../contract/project-identity.js\";\nimport {\n buildSessionTree,\n filterSessionTreeEntriesByActivityWindow,\n} from \"../contract/session-tree.js\";\nimport { getSessionAgentName } from \"./dashboard.js\";\nimport type { DashboardCostFacts } from \"./cost-facts.js\";\nimport { visitAttributedCosts, visitAttributedUsage } from \"./cost-attribution.js\";\n\ninterface ProjectMetrics {\n sessions: number;\n messages: number;\n tokens: number;\n cost: number;\n hasEstimatedCost: boolean;\n agentStats: Map<string, ApiProjectAgentStat>;\n}\n\nexport function summarizeProjects(projects: readonly ApiProjectGroup[]): ApiProjectSummary {\n const summary: ApiProjectSummary = {\n projects: projects.length,\n sessions: 0,\n tokens: 0,\n cost: 0,\n latestActivity: null,\n };\n\n for (const project of projects) {\n summary.sessions += project.sessionCount;\n summary.tokens += project.tokens;\n summary.cost += project.cost;\n if (project.lastActivity != null) {\n summary.latestActivity =\n summary.latestActivity == null\n ? project.lastActivity\n : Math.max(summary.latestActivity, project.lastActivity);\n }\n }\n\n return summary;\n}\n\nfunction emptyMetrics(): ProjectMetrics {\n return {\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost: 0,\n hasEstimatedCost: false,\n agentStats: new Map(),\n };\n}\n\n/**\n * Fold per-session totals into the project groups they belong to. Sessions with\n * no resolved project identity contribute to nothing.\n */\nexport function attachProjectMetrics(\n projects: ProjectGroup[],\n sessions: SessionHead[],\n): ApiProjectGroup[] {\n const tree = buildSessionTree(sessions);\n return attachProjectMetricsToEntries(projects, tree, tree.entries);\n}\n\nexport function attachProjectMetricsFromTree(\n projects: ProjectGroup[],\n tree: SessionTree,\n from?: number,\n to?: number,\n costFacts?: DashboardCostFacts | null,\n): ApiProjectGroup[] {\n return attachProjectMetricsToEntries(\n projects,\n tree,\n filterSessionTreeEntriesByActivityWindow(tree, from, to),\n from,\n to,\n costFacts,\n );\n}\n\nfunction attachProjectMetricsToEntries(\n projects: ProjectGroup[],\n tree: SessionTree,\n entries: SessionTreeNode[],\n from?: number,\n to?: number,\n costFacts?: DashboardCostFacts | null,\n): ApiProjectGroup[] {\n const metrics = new Map<string, ProjectMetrics>();\n\n for (const node of entries) {\n const identity = node.session.project_identity;\n if (!identity) continue;\n\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n\n current.sessions += 1;\n\n const agentName = getSessionAgentName(node.session);\n const agent = current.agentStats.get(agentName);\n if (agent) {\n agent.sessions += 1;\n } else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 1,\n messages: 0,\n tokens: 0,\n cost: 0,\n });\n }\n }\n\n visitAttributedUsage(\n tree,\n { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },\n ({ entry, messages, totalTokens }) => {\n const identity = entry.session.project_identity;\n if (!identity) return;\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n current.messages += messages;\n current.tokens += totalTokens;\n\n const agentName = getSessionAgentName(entry.session);\n const agent = current.agentStats.get(agentName);\n if (agent) {\n agent.messages += messages;\n agent.tokens += totalTokens;\n } else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 0,\n messages,\n tokens: totalTokens,\n cost: 0,\n });\n }\n },\n );\n\n visitAttributedCosts(\n tree,\n { from, to: to ?? Number.POSITIVE_INFINITY, facts: costFacts },\n ({ entry, cost, source }) => {\n const identity = entry.session.project_identity;\n if (!identity) return;\n const key = getProjectIdentityKey(identity);\n let current = metrics.get(key);\n if (!current) {\n current = emptyMetrics();\n metrics.set(key, current);\n }\n current.cost += cost;\n if (source === \"estimated\") current.hasEstimatedCost = true;\n\n const agentName = getSessionAgentName(entry.session);\n const agent = current.agentStats.get(agentName);\n if (agent) agent.cost += cost;\n else {\n current.agentStats.set(agentName, {\n name: agentName,\n sessions: 0,\n messages: 0,\n tokens: 0,\n cost,\n });\n }\n },\n );\n\n return projects.map((project) => {\n const metric = metrics.get(\n getProjectIdentityKey({ kind: project.identityKind, key: project.identityKey }),\n );\n return {\n ...project,\n sessionCount: metric?.sessions ?? 0,\n messages: metric?.messages ?? 0,\n tokens: metric?.tokens ?? 0,\n cost: metric?.cost ?? 0,\n cost_source:\n metric && metric.cost > 0\n ? metric.hasEstimatedCost\n ? \"estimated\"\n : \"recorded\"\n : undefined,\n agentStats: [...(metric?.agentStats.values() ?? [])].sort((a, b) => b.sessions - a.sessions),\n };\n });\n}\n"],"mappings":"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","names":["sessions"]}
|
|
@@ -32,9 +32,9 @@ import {
|
|
|
32
32
|
reportFieldMismatch,
|
|
33
33
|
resolveSessionTitle,
|
|
34
34
|
safeParseJsonRecord
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-YLXHGL4S.js";
|
|
36
36
|
|
|
37
|
-
// ../core/dist/chunk-
|
|
37
|
+
// ../core/dist/chunk-V6GZEKLJ.mjs
|
|
38
38
|
import { statSync } from "fs";
|
|
39
39
|
import { readdirSync, statSync as statSync2 } from "fs";
|
|
40
40
|
import { join } from "path";
|
|
@@ -1387,8 +1387,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
|
|
|
1387
1387
|
name = AGENT_METADATA.name;
|
|
1388
1388
|
displayName = AGENT_METADATA.displayName;
|
|
1389
1389
|
basePath = this.configuredSourceRoot;
|
|
1390
|
-
|
|
1391
|
-
sessionsIndexCache = {};
|
|
1390
|
+
sessionsIndexCache = /* @__PURE__ */ new Map();
|
|
1392
1391
|
sessionsIndexMtime = {};
|
|
1393
1392
|
childContextsBySource = /* @__PURE__ */ new Map();
|
|
1394
1393
|
childContextCache = /* @__PURE__ */ new Map();
|
|
@@ -1690,8 +1689,9 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
|
|
|
1690
1689
|
const cacheKey = basename(projectDir);
|
|
1691
1690
|
const indexPath = this.getSessionsIndexPath(projectDir);
|
|
1692
1691
|
const mtime = this.readFileMtimeMs(indexPath);
|
|
1693
|
-
|
|
1694
|
-
|
|
1692
|
+
const cached = this.sessionsIndexCache.get(cacheKey);
|
|
1693
|
+
if (cached && this.sessionsIndexMtime[cacheKey] === mtime) {
|
|
1694
|
+
return cached;
|
|
1695
1695
|
}
|
|
1696
1696
|
const map = /* @__PURE__ */ new Map();
|
|
1697
1697
|
if (existsSync2(indexPath)) {
|
|
@@ -1707,7 +1707,7 @@ var ClaudeCodeAgent = class extends SingleFileSessionSource {
|
|
|
1707
1707
|
} catch {
|
|
1708
1708
|
}
|
|
1709
1709
|
}
|
|
1710
|
-
this.sessionsIndexCache
|
|
1710
|
+
this.sessionsIndexCache.set(cacheKey, map);
|
|
1711
1711
|
this.sessionsIndexMtime[cacheKey] = mtime;
|
|
1712
1712
|
return map;
|
|
1713
1713
|
}
|
|
@@ -8387,4 +8387,4 @@ export {
|
|
|
8387
8387
|
createRegisteredAgents,
|
|
8388
8388
|
getAgentInfoMap
|
|
8389
8389
|
};
|
|
8390
|
-
//# sourceMappingURL=chunk-
|
|
8390
|
+
//# sourceMappingURL=chunk-RBCCNCDO.js.map
|