zcode-acp-server 0.46.6 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/backend/listener.d.ts.map +1 -1
  3. package/dist/backend/listener.js +5 -1
  4. package/dist/backend/listener.js.map +1 -1
  5. package/dist/code-fingerprint.json +2 -2
  6. package/dist/config/auto-compact.d.ts.map +1 -1
  7. package/dist/config/auto-compact.js +4 -2
  8. package/dist/config/auto-compact.js.map +1 -1
  9. package/dist/config/model-cache.js +4 -1
  10. package/dist/config/model-cache.js.map +1 -1
  11. package/dist/config/options.js +4 -1
  12. package/dist/config/options.js.map +1 -1
  13. package/dist/goal-loop/driver.d.ts +14 -3
  14. package/dist/goal-loop/driver.d.ts.map +1 -1
  15. package/dist/goal-loop/driver.js +39 -17
  16. package/dist/goal-loop/driver.js.map +1 -1
  17. package/dist/handlers/replay.d.ts +39 -0
  18. package/dist/handlers/replay.d.ts.map +1 -1
  19. package/dist/handlers/replay.js +37 -1
  20. package/dist/handlers/replay.js.map +1 -1
  21. package/dist/handlers/session.d.ts.map +1 -1
  22. package/dist/handlers/session.js +53 -13
  23. package/dist/handlers/session.js.map +1 -1
  24. package/dist/remote/endpoint.d.ts.map +1 -1
  25. package/dist/remote/endpoint.js +6 -0
  26. package/dist/remote/endpoint.js.map +1 -1
  27. package/dist/remote/hub-server.d.ts.map +1 -1
  28. package/dist/remote/hub-server.js +102 -0
  29. package/dist/remote/hub-server.js.map +1 -1
  30. package/dist/remote/settings-endpoint.d.ts +45 -0
  31. package/dist/remote/settings-endpoint.d.ts.map +1 -0
  32. package/dist/remote/settings-endpoint.js +920 -0
  33. package/dist/remote/settings-endpoint.js.map +1 -0
  34. package/dist/settings/agents-config.d.ts +158 -0
  35. package/dist/settings/agents-config.d.ts.map +1 -0
  36. package/dist/settings/agents-config.js +598 -0
  37. package/dist/settings/agents-config.js.map +1 -0
  38. package/dist/settings/app-update.d.ts +193 -0
  39. package/dist/settings/app-update.d.ts.map +1 -0
  40. package/dist/settings/app-update.js +659 -0
  41. package/dist/settings/app-update.js.map +1 -0
  42. package/dist/settings/atomic-write.d.ts +106 -0
  43. package/dist/settings/atomic-write.d.ts.map +1 -0
  44. package/dist/settings/atomic-write.js +228 -0
  45. package/dist/settings/atomic-write.js.map +1 -0
  46. package/dist/settings/cli-config.d.ts +145 -0
  47. package/dist/settings/cli-config.d.ts.map +1 -0
  48. package/dist/settings/cli-config.js +320 -0
  49. package/dist/settings/cli-config.js.map +1 -0
  50. package/dist/settings/coding-plan.d.ts +106 -0
  51. package/dist/settings/coding-plan.d.ts.map +1 -0
  52. package/dist/settings/coding-plan.js +299 -0
  53. package/dist/settings/coding-plan.js.map +1 -0
  54. package/dist/settings/file-lock.d.ts +39 -0
  55. package/dist/settings/file-lock.d.ts.map +1 -0
  56. package/dist/settings/file-lock.js +282 -0
  57. package/dist/settings/file-lock.js.map +1 -0
  58. package/dist/settings/provider-config.d.ts +123 -0
  59. package/dist/settings/provider-config.d.ts.map +1 -0
  60. package/dist/settings/provider-config.js +311 -0
  61. package/dist/settings/provider-config.js.map +1 -0
  62. package/dist/settings/skills.d.ts +72 -0
  63. package/dist/settings/skills.d.ts.map +1 -0
  64. package/dist/settings/skills.js +245 -0
  65. package/dist/settings/skills.js.map +1 -0
  66. package/dist/settings/usage-stats.d.ts +74 -0
  67. package/dist/settings/usage-stats.d.ts.map +1 -0
  68. package/dist/settings/usage-stats.js +235 -0
  69. package/dist/settings/usage-stats.js.map +1 -0
  70. package/dist/translators/projection-differ.d.ts +12 -0
  71. package/dist/translators/projection-differ.d.ts.map +1 -1
  72. package/dist/translators/projection-differ.js +17 -0
  73. package/dist/translators/projection-differ.js.map +1 -1
  74. package/dist/utils.d.ts +14 -0
  75. package/dist/utils.d.ts.map +1 -1
  76. package/dist/utils.js +22 -0
  77. package/dist/utils.js.map +1 -1
  78. package/docs/ARCHITECTURE.md +65 -0
  79. package/docs/REMOTE-CLIENTS.md +252 -0
  80. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Skill management: discovery plus the actions the app's skills page offers.
3
+ *
4
+ * `config/skill-discovery.ts` exists to build the editor's command list, so it
5
+ * filters out disabled skills and drops the SKILL.md path (its `input.hint` is
6
+ * the author's argument hint, not a location). Neither works for management —
7
+ * a disabled skill must stay visible so the user can re-enable it, and every
8
+ * action needs the real path. So discovery is re-done here, over the same roots
9
+ * and with the same frontmatter rules.
10
+ *
11
+ * Escape guards are the point of this module: a skill path is client-supplied
12
+ * text that becomes a filesystem operation. Every path is resolved through
13
+ * `realpath` (collapsing symlinks and `..`) BEFORE the containment check, so
14
+ * `~/.zcode/skills/../../../etc` or a symlink pointing outside the roots can
15
+ * neither delete nor copy anything outside them. The app's own delete path
16
+ * carries the same guard.
17
+ */
18
+ import { cp, mkdir, readFile, realpath, rm, stat } from "node:fs/promises";
19
+ import { homedir } from "node:os";
20
+ import path from "node:path";
21
+ import { setSkillEnabled } from "./cli-config.js";
22
+ import { log, zcodeHomeDir } from "../utils.js";
23
+ /**
24
+ * Discovery roots, in the app's own order: user scope first (its skills win a
25
+ * name clash), then the shared `~/.agents` root, then plugin caches.
26
+ */
27
+ export function skillRoots() {
28
+ const home = zcodeHomeDir();
29
+ return [
30
+ { dir: path.join(home, "skills"), scope: "user", deletable: true },
31
+ { dir: path.join(homedir(), ".agents", "skills"), scope: "agents", deletable: true },
32
+ { dir: path.join(home, "cli", "plugins", "cache"), scope: "plugin", deletable: false },
33
+ ];
34
+ }
35
+ /** The user-scope root new skills are copied into. */
36
+ export function userSkillsRoot() {
37
+ return path.join(zcodeHomeDir(), "skills");
38
+ }
39
+ /** Resolve through symlinks; fall back to the lexical form when absent. */
40
+ async function resolveReal(target) {
41
+ try {
42
+ return await realpath(target);
43
+ }
44
+ catch {
45
+ return path.resolve(target);
46
+ }
47
+ }
48
+ /** True when `target` is `root` itself or inside it — both sides resolved. */
49
+ async function isInside(target, root) {
50
+ const rel = path.relative(await resolveReal(root), await resolveReal(target));
51
+ return rel === "" || (!rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel));
52
+ }
53
+ /** Minimal SKILL.md frontmatter reader — name and description only. */
54
+ function parseSkillFrontmatter(content) {
55
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
56
+ if (!match)
57
+ return {};
58
+ const fm = {};
59
+ for (const line of match[1].split(/\r?\n/)) {
60
+ const idx = line.indexOf(":");
61
+ if (idx <= 0)
62
+ continue;
63
+ let value = line.slice(idx + 1).trim();
64
+ if ((value.startsWith('"') && value.endsWith('"')) ||
65
+ (value.startsWith("'") && value.endsWith("'"))) {
66
+ value = value.slice(1, -1);
67
+ }
68
+ fm[line.slice(0, idx).trim()] = value;
69
+ }
70
+ return fm;
71
+ }
72
+ /**
73
+ * The set of disabled SKILL.md paths from `cli/config.json`.
74
+ *
75
+ * Keys are normalized (backslashes to forward slashes) the way the app
76
+ * normalizes them, so a config written on Windows still matches a POSIX
77
+ * discovery path.
78
+ */
79
+ async function disabledSkillPaths() {
80
+ try {
81
+ const raw = await readFile(path.join(zcodeHomeDir(), "cli", "config.json"), "utf8");
82
+ const parsed = JSON.parse(raw);
83
+ const out = new Set();
84
+ for (const [key, value] of Object.entries(parsed.skills ?? {})) {
85
+ if (value?.enable === false)
86
+ out.add(key.replace(/\\/gu, "/"));
87
+ }
88
+ return out;
89
+ }
90
+ catch {
91
+ return new Set();
92
+ }
93
+ }
94
+ /**
95
+ * Every skill, enabled or not.
96
+ *
97
+ * Duplicate names are kept rather than de-duplicated: the app lists the same
98
+ * name from two scopes separately (they are different files), and hiding one
99
+ * would make its toggle unreachable.
100
+ */
101
+ export async function listSkills() {
102
+ const disabled = await disabledSkillPaths();
103
+ const out = [];
104
+ for (const root of skillRoots()) {
105
+ let entries;
106
+ try {
107
+ entries = await readdirSafe(root.dir);
108
+ }
109
+ catch {
110
+ continue; // root absent — normal on a machine without that source
111
+ }
112
+ for (const entry of entries) {
113
+ const dir = path.join(root.dir, entry);
114
+ const skillMd = path.join(dir, "SKILL.md");
115
+ try {
116
+ if (!(await stat(dir)).isDirectory())
117
+ continue;
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ let content;
123
+ try {
124
+ content = await readFile(skillMd, "utf8");
125
+ }
126
+ catch {
127
+ continue; // not a skill directory
128
+ }
129
+ const fm = parseSkillFrontmatter(content);
130
+ const name = fm["name"] ?? entry;
131
+ if (!name || !fm["description"])
132
+ continue; // the runtime skips these too
133
+ if (fm["user-invocable"] === "false")
134
+ continue;
135
+ const absPath = path.resolve(skillMd);
136
+ out.push({
137
+ name,
138
+ description: fm["description"],
139
+ path: absPath,
140
+ dir,
141
+ scope: root.scope,
142
+ enabled: !disabled.has(absPath.replace(/\\/gu, "/")),
143
+ });
144
+ }
145
+ }
146
+ out.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
147
+ return out;
148
+ }
149
+ async function readdirSafe(dir) {
150
+ const { readdir } = await import("node:fs/promises");
151
+ try {
152
+ return await readdir(dir);
153
+ }
154
+ catch {
155
+ return [];
156
+ }
157
+ }
158
+ /** Enable or disable a skill by its SKILL.md path. */
159
+ export async function setSkillEnabledByPath(skillPath, enable) {
160
+ assertSkillPath(skillPath);
161
+ // The stored key uses forward slashes regardless of platform.
162
+ await setSkillEnabled(skillPath.replace(/\\/gu, "/"), enable);
163
+ }
164
+ function assertSkillPath(skillPath) {
165
+ if (!path.isAbsolute(skillPath)) {
166
+ throw new Error("skill path must be absolute");
167
+ }
168
+ if (path.basename(skillPath) !== "SKILL.md") {
169
+ throw new Error("skill path must point at a SKILL.md file");
170
+ }
171
+ }
172
+ /**
173
+ * Delete a skill directory.
174
+ *
175
+ * Only skills inside a deletable root can go: a plugin's skill belongs to the
176
+ * plugin install and would reappear on the next update, so the API refuses
177
+ * rather than deleting something that comes back. The containment check runs on
178
+ * RESOLVED paths, so a symlinked skill directory cannot point the delete at
179
+ * something outside the roots.
180
+ */
181
+ export async function deleteSkill(skillPath) {
182
+ assertSkillPath(skillPath);
183
+ const dir = path.dirname(skillPath);
184
+ // Every deletable root counts, not just the first one — the agents scope
185
+ // (`~/.agents/skills`) is listed and copyable, so refusing to delete it made
186
+ // half the listed skills undeletable.
187
+ const roots = skillRoots().filter((r) => r.deletable);
188
+ let root;
189
+ for (const candidate of roots) {
190
+ if (await isInside(dir, candidate.dir)) {
191
+ root = candidate;
192
+ break;
193
+ }
194
+ }
195
+ if (!root) {
196
+ throw new Error("only skills under the user skill roots can be deleted");
197
+ }
198
+ // `isInside` treats the root itself as contained — refuse that explicitly.
199
+ if ((await resolveReal(dir)) === (await resolveReal(root.dir))) {
200
+ throw new Error("refusing to delete the skills root itself");
201
+ }
202
+ await rm(dir, { recursive: true, force: true });
203
+ }
204
+ /**
205
+ * Copy a skill into the user root.
206
+ *
207
+ * A name collision is refused rather than merged: silently merging two skill
208
+ * directories yields a SKILL.md whose description matches neither original.
209
+ */
210
+ export async function copySkillToUser(skillPath) {
211
+ assertSkillPath(skillPath);
212
+ const sourceDir = path.dirname(skillPath);
213
+ // Containment on resolved paths: a symlinked skill elsewhere on disk must not
214
+ // be copyable through this API.
215
+ let owned = false;
216
+ for (const root of skillRoots()) {
217
+ if (root.scope !== "plugin" && (await isInside(sourceDir, root.dir))) {
218
+ owned = true;
219
+ break;
220
+ }
221
+ }
222
+ if (!owned) {
223
+ throw new Error("only skills under the user skill roots can be copied");
224
+ }
225
+ const targetRoot = userSkillsRoot();
226
+ const name = path.basename(sourceDir);
227
+ const target = path.join(targetRoot, name);
228
+ if ((await resolveReal(target)) === (await resolveReal(sourceDir))) {
229
+ throw new Error("this skill already lives in the user skills root");
230
+ }
231
+ try {
232
+ if ((await stat(target)).isDirectory()) {
233
+ throw new Error(`a skill named '${name}' already exists in the user skills root`);
234
+ }
235
+ }
236
+ catch (error) {
237
+ if (error?.code !== "ENOENT")
238
+ throw error;
239
+ }
240
+ await mkdir(targetRoot, { recursive: true });
241
+ await cp(sourceDir, target, { recursive: true });
242
+ log(`settings: copied skill '${name}' to ${target}`);
243
+ return path.join(target, "SKILL.md");
244
+ }
245
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/settings/skills.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * App usage stats, aggregated from the local agent database.
3
+ *
4
+ * The desktop app's App Usage panel reads `~/.zcode/cli/db/db.sqlite`
5
+ * (`model_usage` / `turn_usage` / `tool_usage`). Rather than proxying the
6
+ * backend's `usage/stats` RPC — which couples a settings screen to a live
7
+ * backend process and whose `.strict()` params turn any schema drift into a
8
+ * hard failure — the same tables are aggregated here in read-only mode
9
+ * (ADR-0027).
10
+ *
11
+ * Two things this module is careful about:
12
+ *
13
+ * - **Never blocking the writer.** The connection is opened read-only and every
14
+ * query is a bounded aggregate. A `SQLITE_BUSY` degrades to the same
15
+ * `available: false` shape a missing database produces, because the caller
16
+ * renders an empty state either way.
17
+ * - **Never claiming data that is not there.** A machine that never ran an
18
+ * agent has no database; that is a normal state, so the response says
19
+ * `available: false` with zero values instead of erroring.
20
+ *
21
+ * Day buckets use the process timezone offset. The app does the same and notes
22
+ * the same ≤1h DST imprecision; for a usage overview that is not worth the
23
+ * complexity of a per-request timezone.
24
+ */
25
+ export type UsageRange = "7d" | "30d" | "all";
26
+ export interface ModelUsageRow {
27
+ modelId: string | null;
28
+ totalTokens: number;
29
+ inputTokens: number;
30
+ outputTokens: number;
31
+ reasoningTokens: number;
32
+ cacheReadTokens: number;
33
+ cacheCreationTokens: number;
34
+ requestCount: number;
35
+ /** Fraction of the window's total tokens, 0..1. */
36
+ share: number;
37
+ }
38
+ export interface DailyUsageRow {
39
+ /** Local calendar day, `YYYY-MM-DD`. */
40
+ date: string;
41
+ models: Array<{
42
+ modelId: string | null;
43
+ totalTokens: number;
44
+ }>;
45
+ }
46
+ export interface UsageSnapshot {
47
+ /** false when the database is absent — an empty state, not an error. */
48
+ available: boolean;
49
+ range: UsageRange;
50
+ summary: {
51
+ totalTokens: number;
52
+ inputTokens: number;
53
+ outputTokens: number;
54
+ reasoningTokens: number;
55
+ cacheReadTokens: number;
56
+ requestCount: number;
57
+ sessionCount: number;
58
+ turnCount: number;
59
+ toolCallCount: number;
60
+ models: number;
61
+ activeDays: number;
62
+ };
63
+ models: ModelUsageRow[];
64
+ daily: DailyUsageRow[];
65
+ }
66
+ /**
67
+ * Aggregate the window.
68
+ *
69
+ * Two queries: one per-model roll-up, one per-day roll-up. Both are grouped
70
+ * server-side by sqlite, so the bridge transfers only the aggregated rows —
71
+ * the raw table on a developed machine holds tens of thousands of rows.
72
+ */
73
+ export declare function readUsageStats(range?: UsageRange): Promise<UsageSnapshot>;
74
+ //# sourceMappingURL=usage-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-stats.d.ts","sourceRoot":"","sources":["../../src/settings/usage-stats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA6DH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE;QACP,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AA6BD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,KAAK,GAAE,UAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CA8HrF"}
@@ -0,0 +1,235 @@
1
+ /**
2
+ * App usage stats, aggregated from the local agent database.
3
+ *
4
+ * The desktop app's App Usage panel reads `~/.zcode/cli/db/db.sqlite`
5
+ * (`model_usage` / `turn_usage` / `tool_usage`). Rather than proxying the
6
+ * backend's `usage/stats` RPC — which couples a settings screen to a live
7
+ * backend process and whose `.strict()` params turn any schema drift into a
8
+ * hard failure — the same tables are aggregated here in read-only mode
9
+ * (ADR-0027).
10
+ *
11
+ * Two things this module is careful about:
12
+ *
13
+ * - **Never blocking the writer.** The connection is opened read-only and every
14
+ * query is a bounded aggregate. A `SQLITE_BUSY` degrades to the same
15
+ * `available: false` shape a missing database produces, because the caller
16
+ * renders an empty state either way.
17
+ * - **Never claiming data that is not there.** A machine that never ran an
18
+ * agent has no database; that is a normal state, so the response says
19
+ * `available: false` with zero values instead of erroring.
20
+ *
21
+ * Day buckets use the process timezone offset. The app does the same and notes
22
+ * the same ≤1h DST imprecision; for a usage overview that is not worth the
23
+ * complexity of a per-request timezone.
24
+ */
25
+ import { createRequire } from "node:module";
26
+ import { log, warn, zcodeUsageDbPath } from "../utils.js";
27
+ /**
28
+ * Cached constructor; null when node:sqlite is unavailable (Node < 22).
29
+ *
30
+ * Loaded through `createRequire` rather than a dynamic `import`: under vite
31
+ * (the test runner) `import("node:sqlite")` is intercepted and fails to
32
+ * resolve, which would silently disable usage stats in every test. `require`
33
+ * resolves Node built-ins directly.
34
+ */
35
+ let sqliteCtor;
36
+ function loadSqlite() {
37
+ if (sqliteCtor !== undefined)
38
+ return sqliteCtor;
39
+ try {
40
+ const require = createRequire(import.meta.url);
41
+ const mod = require("node:sqlite");
42
+ sqliteCtor = mod.DatabaseSync;
43
+ }
44
+ catch {
45
+ sqliteCtor = null;
46
+ }
47
+ return sqliteCtor;
48
+ }
49
+ /** A `database is locked/busy` throw from node:sqlite (see tasks-index.ts). */
50
+ function isBusyError(e) {
51
+ const msg = e instanceof Error ? e.message : String(e);
52
+ return /database is (busy|locked)/i.test(msg);
53
+ }
54
+ const BUSY_RETRY_DELAYS_MS = [200, 400];
55
+ /**
56
+ * Open the usage db read-only, retrying briefly on writer contention.
57
+ *
58
+ * Synchronous on purpose: a settings request is a single short operation, and
59
+ * blocking the event loop for at most 600ms is cheaper than making every caller
60
+ * async. `Atomics.wait` is the synchronous sleep that does not busy-spin.
61
+ */
62
+ function openReadOnly(dbPath) {
63
+ const Ctor = loadSqlite();
64
+ if (!Ctor)
65
+ return null;
66
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
67
+ for (let attempt = 0;; attempt += 1) {
68
+ try {
69
+ return new Ctor(dbPath, { readOnly: true });
70
+ }
71
+ catch (error) {
72
+ const delay = BUSY_RETRY_DELAYS_MS[attempt];
73
+ if (delay === undefined || !isBusyError(error))
74
+ return null;
75
+ Atomics.wait(sleeper, 0, 0, delay);
76
+ }
77
+ }
78
+ }
79
+ const EMPTY_SNAPSHOT = (range) => ({
80
+ available: false,
81
+ range,
82
+ summary: {
83
+ totalTokens: 0,
84
+ inputTokens: 0,
85
+ outputTokens: 0,
86
+ reasoningTokens: 0,
87
+ cacheReadTokens: 0,
88
+ requestCount: 0,
89
+ sessionCount: 0,
90
+ turnCount: 0,
91
+ toolCallCount: 0,
92
+ models: 0,
93
+ activeDays: 0,
94
+ },
95
+ models: [],
96
+ daily: [],
97
+ });
98
+ /** Range → the `started_at` floor (ms epoch). `all` has no floor. */
99
+ function rangeFloor(range, now) {
100
+ if (range === "7d")
101
+ return now - 7 * 86_400_000;
102
+ if (range === "30d")
103
+ return now - 30 * 86_400_000;
104
+ return 0;
105
+ }
106
+ /**
107
+ * Aggregate the window.
108
+ *
109
+ * Two queries: one per-model roll-up, one per-day roll-up. Both are grouped
110
+ * server-side by sqlite, so the bridge transfers only the aggregated rows —
111
+ * the raw table on a developed machine holds tens of thousands of rows.
112
+ */
113
+ export async function readUsageStats(range = "7d") {
114
+ const db = openReadOnly(zcodeUsageDbPath());
115
+ if (!db) {
116
+ log("settings: usage database unavailable — reporting an empty snapshot");
117
+ return EMPTY_SNAPSHOT(range);
118
+ }
119
+ try {
120
+ const floor = rangeFloor(range, Date.now());
121
+ const modelRows = db
122
+ .prepare(`SELECT model_id,
123
+ COALESCE(SUM(computed_total_tokens), 0) AS total_tokens,
124
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
125
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
126
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
127
+ COALESCE(SUM(cache_read_input_tokens), 0) AS cache_read_tokens,
128
+ COALESCE(SUM(cache_creation_input_tokens), 0) AS cache_creation_tokens,
129
+ COUNT(*) AS request_count
130
+ FROM model_usage
131
+ WHERE started_at > ?
132
+ GROUP BY model_id
133
+ ORDER BY total_tokens DESC`)
134
+ .all(floor);
135
+ // Group by LOCAL calendar day, computed by sqlite. Dividing the epoch by
136
+ // 86400000 would bucket on UTC, which puts a morning session in the wrong
137
+ // day for every timezone east or west of it — the same imprecision the app
138
+ // documents for its own day buckets.
139
+ const dayRows = db
140
+ .prepare(`SELECT date(started_at / 1000, 'unixepoch', 'localtime') AS day,
141
+ model_id,
142
+ COALESCE(SUM(computed_total_tokens), 0) AS total_tokens
143
+ FROM model_usage
144
+ WHERE started_at > ?
145
+ GROUP BY day, model_id
146
+ ORDER BY day ASC`)
147
+ .all(floor);
148
+ // Session/turn/tool counts describe the same window but come from their own
149
+ // tables; a missing table reads as zero rather than failing the snapshot.
150
+ const scalar = (sql) => {
151
+ try {
152
+ const row = db.prepare(sql).get(floor);
153
+ return Number(row?.["c"] ?? 0);
154
+ }
155
+ catch {
156
+ return 0;
157
+ }
158
+ };
159
+ const sessionCount = scalar("SELECT COUNT(DISTINCT session_id) AS c FROM turn_usage WHERE started_at > ?");
160
+ const turnCount = scalar("SELECT COUNT(*) AS c FROM turn_usage WHERE started_at > ?");
161
+ const toolCallCount = scalar("SELECT COUNT(*) AS c FROM tool_usage WHERE started_at > ?");
162
+ const grandTotal = modelRows.reduce((sum, r) => sum + Number(r["total_tokens"] ?? 0), 0);
163
+ const models = modelRows.map((r) => {
164
+ const total = Number(r["total_tokens"] ?? 0);
165
+ return {
166
+ modelId: typeof r["model_id"] === "string" ? r["model_id"] : null,
167
+ totalTokens: total,
168
+ inputTokens: Number(r["input_tokens"] ?? 0),
169
+ outputTokens: Number(r["output_tokens"] ?? 0),
170
+ reasoningTokens: Number(r["reasoning_tokens"] ?? 0),
171
+ cacheReadTokens: Number(r["cache_read_tokens"] ?? 0),
172
+ cacheCreationTokens: Number(r["cache_creation_tokens"] ?? 0),
173
+ requestCount: Number(r["request_count"] ?? 0),
174
+ share: grandTotal > 0 ? round4(total / grandTotal) : 0,
175
+ };
176
+ });
177
+ // The day rows arrive already grouped by sqlite; only the per-day model
178
+ // ordering (by volume, so a chart's legend matches its bars) is done here.
179
+ const byDay = new Map();
180
+ for (const row of dayRows) {
181
+ const day = typeof row["day"] === "string" ? row["day"] : "";
182
+ if (!day)
183
+ continue;
184
+ const modelId = typeof row["model_id"] === "string" ? row["model_id"] : "(unknown)";
185
+ const bucket = byDay.get(day) ?? new Map();
186
+ bucket.set(modelId, (bucket.get(modelId) ?? 0) + Number(row["total_tokens"] ?? 0));
187
+ byDay.set(day, bucket);
188
+ }
189
+ const daily = Array.from(byDay.entries())
190
+ .sort(([a], [b]) => (a < b ? -1 : 1))
191
+ .map(([date, bucket]) => ({
192
+ date,
193
+ models: Array.from(bucket.entries())
194
+ .map(([modelId, totalTokens]) => ({ modelId, totalTokens }))
195
+ .sort((a, b) => b.totalTokens - a.totalTokens),
196
+ }));
197
+ return {
198
+ available: true,
199
+ range,
200
+ summary: {
201
+ totalTokens: grandTotal,
202
+ inputTokens: models.reduce((s, m) => s + m.inputTokens, 0),
203
+ outputTokens: models.reduce((s, m) => s + m.outputTokens, 0),
204
+ reasoningTokens: models.reduce((s, m) => s + m.reasoningTokens, 0),
205
+ cacheReadTokens: models.reduce((s, m) => s + m.cacheReadTokens, 0),
206
+ requestCount: models.reduce((s, m) => s + m.requestCount, 0),
207
+ sessionCount,
208
+ turnCount,
209
+ toolCallCount,
210
+ models: models.length,
211
+ activeDays: daily.length,
212
+ },
213
+ models,
214
+ daily,
215
+ };
216
+ }
217
+ catch (error) {
218
+ // A schema drift (a renamed column) must not break the settings screen;
219
+ // the rest of the API is unaffected.
220
+ warn(`settings: usage aggregation failed: ${error instanceof Error ? error.message : String(error)}`);
221
+ return EMPTY_SNAPSHOT(range);
222
+ }
223
+ finally {
224
+ try {
225
+ db.close();
226
+ }
227
+ catch {
228
+ /* already closed */
229
+ }
230
+ }
231
+ }
232
+ function round4(value) {
233
+ return Math.round(value * 10_000) / 10_000;
234
+ }
235
+ //# sourceMappingURL=usage-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-stats.js","sourceRoot":"","sources":["../../src/settings/usage-stats.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,18 @@ export declare class ProjectionDiffer {
23
23
  private readonly seenMessageIds;
24
24
  private lastPlanSig;
25
25
  private readonly seenPatchHashes;
26
+ /**
27
+ * Id of the newest message this differ has baselined. Turn-internal reads
28
+ * scope themselves with `session/messages`' native `afterMessageId` cursor
29
+ * instead of re-transferring the whole store — everything before the anchor
30
+ * is either already seen or frozen history the differ never re-emits.
31
+ *
32
+ * markSeen takes the last id of the list it is given, so callers pass
33
+ * either the full history (turn entry) or a post-anchor window (every
34
+ * re-baseline) — both end at the newest stored message. A read that failed
35
+ * and degraded to [] leaves the anchor untouched.
36
+ */
37
+ historyAnchor: string | null;
26
38
  /** Whether any TextDelta fired this turn (used by fallback detection). */
27
39
  emittedTextThisTurn: boolean;
28
40
  /** Mark all given messages as seen (baseline so we don't re-emit history). */
@@ -1 +1 @@
1
- {"version":3,"file":"projection-differ.d.ts","sourceRoot":"","sources":["../../src/translators/projection-differ.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EACV,YAAY,EAGZ,aAAa,EACd,MAAM,qBAAqB,CAAC;AAS7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,0EAA0E;IAC1E,mBAAmB,UAAS;IAE5B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI;IAOxC,gEAAgE;IAChE,cAAc,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO;IAKxC;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKlC,4DAA4D;IAC5D,SAAS,IAAI,IAAI;IAIjB,4EAA4E;IAC5E,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,+CAA+C;IAC/C,IAAI,CAAC,WAAW,EAAE,aAAa,GAAG,IAAI,GAAG,aAAa,EAAE;IAiDxD;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,EAAE;IAevD,OAAO,CAAC,YAAY;IAmEpB,OAAO,CAAC,eAAe;CAYxB"}
1
+ {"version":3,"file":"projection-differ.d.ts","sourceRoot":"","sources":["../../src/translators/projection-differ.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EACV,YAAY,EAGZ,aAAa,EACd,MAAM,qBAAqB,CAAC;AAS7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD;;;;;;;;;;OAUG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IACpC,0EAA0E;IAC1E,mBAAmB,UAAS;IAE5B,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI;IAWxC,gEAAgE;IAChE,cAAc,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO;IAKxC;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAKlC,4DAA4D;IAC5D,SAAS,IAAI,IAAI;IAIjB,4EAA4E;IAC5E,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,+CAA+C;IAC/C,IAAI,CAAC,WAAW,EAAE,aAAa,GAAG,IAAI,GAAG,aAAa,EAAE;IAiDxD;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,EAAE;IAevD,OAAO,CAAC,YAAY;IAmEpB,OAAO,CAAC,eAAe;CAYxB"}
@@ -29,6 +29,18 @@ export class ProjectionDiffer {
29
29
  seenMessageIds = new Set();
30
30
  lastPlanSig = "__none__";
31
31
  seenPatchHashes = new Set();
32
+ /**
33
+ * Id of the newest message this differ has baselined. Turn-internal reads
34
+ * scope themselves with `session/messages`' native `afterMessageId` cursor
35
+ * instead of re-transferring the whole store — everything before the anchor
36
+ * is either already seen or frozen history the differ never re-emits.
37
+ *
38
+ * markSeen takes the last id of the list it is given, so callers pass
39
+ * either the full history (turn entry) or a post-anchor window (every
40
+ * re-baseline) — both end at the newest stored message. A read that failed
41
+ * and degraded to [] leaves the anchor untouched.
42
+ */
43
+ historyAnchor = null;
32
44
  /** Whether any TextDelta fired this turn (used by fallback detection). */
33
45
  emittedTextThisTurn = false;
34
46
  /** Mark all given messages as seen (baseline so we don't re-emit history). */
@@ -38,6 +50,11 @@ export class ProjectionDiffer {
38
50
  if (key)
39
51
  this.seenMessageIds.add(key);
40
52
  }
53
+ // The list arrives in store order (session/messages is ordered by
54
+ // sequence/time_created), so its last id is the newest message covered.
55
+ const last = messages[messages.length - 1]?.info?.id;
56
+ if (last)
57
+ this.historyAnchor = last;
41
58
  }
42
59
  /** Whether a message's dedup key has already been processed. */
43
60
  hasSeenMessage(m) {