nilometer 0.0.0-stage → 0.1.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 (78) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +462 -2
  4. package/core/pricing/prices.json +348 -0
  5. package/core/schema/001_ingestion.sql +57 -0
  6. package/core/schema/002_line_count.sql +7 -0
  7. package/core/schema/003_derived.sql +70 -0
  8. package/core/schema/004_requests_dedup.sql +50 -0
  9. package/core/schema/005_repositories.sql +25 -0
  10. package/core/schema/006_status_readings.sql +33 -0
  11. package/core/schema/007_pricing.sql +95 -0
  12. package/core/schema/008_interruptions.sql +263 -0
  13. package/core/schema/009_headroom.sql +84 -0
  14. package/core/schema/010_attribution.sql +106 -0
  15. package/core/schema/011_projections.sql +83 -0
  16. package/core/schema/012_unsplit_cache_writes.sql +49 -0
  17. package/core/schema/013_repository_revalidation.sql +104 -0
  18. package/core/schema/014_observed_readings.sql +204 -0
  19. package/core/schema/015_response_observations.sql +127 -0
  20. package/core/schema/016_windows_repository_paths.sql +41 -0
  21. package/core/schema/017_unattributed_usage_once.sql +47 -0
  22. package/core/schema/018_request_time_index.sql +21 -0
  23. package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
  24. package/core/schema/020_limit_quota_fields.sql +131 -0
  25. package/core/schema/021_plan_periods.sql +121 -0
  26. package/core/schema/022_attachment_walk.sql +223 -0
  27. package/core/schema/023_auto_continuations.sql +129 -0
  28. package/core/schema/024_counted_limit_groups.sql +29 -0
  29. package/core/schema/025_model_fallback.sql +77 -0
  30. package/core/schema/026_changed_readings.sql +106 -0
  31. package/core/schema/027_weekly_crossings.sql +32 -0
  32. package/core/schema/028_stale_readings.sql +196 -0
  33. package/core/schema/029_lockout_sessions.sql +68 -0
  34. package/core/schema/030_repository_aliases.sql +81 -0
  35. package/core/schema/031_cte_cursor_budget.sql +153 -0
  36. package/dist/cli/main.cli.js +39 -0
  37. package/dist/cli/program.js +801 -0
  38. package/dist/core/db/database.js +182 -0
  39. package/dist/core/db/fingerprint.js +71 -0
  40. package/dist/core/ingest/attribution.js +131 -0
  41. package/dist/core/ingest/classify.js +382 -0
  42. package/dist/core/ingest/command.js +95 -0
  43. package/dist/core/ingest/derive.js +212 -0
  44. package/dist/core/ingest/discover.js +126 -0
  45. package/dist/core/ingest/ingest.js +160 -0
  46. package/dist/core/ingest/reader.js +89 -0
  47. package/dist/core/ingest/reset-time.js +97 -0
  48. package/dist/core/ingest/results.js +201 -0
  49. package/dist/core/ingest/spool.js +165 -0
  50. package/dist/core/ingest/store.js +99 -0
  51. package/dist/core/install/install.js +185 -0
  52. package/dist/core/install/locations.js +89 -0
  53. package/dist/core/install/private-files.js +259 -0
  54. package/dist/core/install/record.js +103 -0
  55. package/dist/core/plans/plan-prices.js +128 -0
  56. package/dist/core/plans/repository-aliases.js +136 -0
  57. package/dist/core/pricing/prices.js +147 -0
  58. package/dist/core/settings/settings-file.js +215 -0
  59. package/dist/core/settings/statusline.js +240 -0
  60. package/dist/core/util/commands.js +91 -0
  61. package/dist/core/util/node-version.js +58 -0
  62. package/dist/core/util/open-page.js +69 -0
  63. package/dist/core/util/paths.js +36 -0
  64. package/dist/core/verify/compare.js +77 -0
  65. package/dist/core/verify/verify.js +220 -0
  66. package/dist/viewer/explain.js +574 -0
  67. package/dist/viewer/format.js +150 -0
  68. package/dist/viewer/html-charts.js +1292 -0
  69. package/dist/viewer/html-contract.js +2 -0
  70. package/dist/viewer/html-data.js +718 -0
  71. package/dist/viewer/html.js +1654 -0
  72. package/dist/viewer/queries.js +128 -0
  73. package/dist/viewer/render.js +602 -0
  74. package/dist/viewer/report.js +120 -0
  75. package/dist/viewer/save.js +110 -0
  76. package/docs/requirements.md +113 -0
  77. package/hooks/statusline.sh +104 -0
  78. package/package.json +61 -4
@@ -0,0 +1,153 @@
1
+ -- 031_cte_cursor_budget: the lockout views read each costly view once, so they prepare on SQLite 3.54 (D-094, step G5.2).
2
+ --
3
+ -- SQLite 3.54.0 refuses a statement once a CTE's cursor number passes SQLITE_LIMIT_EXPR_DEPTH (1,000), with
4
+ -- "VIEWs and/or subqueries nested too deep". Every reference to a view expands it again, cursors and all, so a
5
+ -- view read four times costs four times its cursors. obs_lockout_intervals read the lockout hits four times
6
+ -- (593 by that measure), and obs_lockout_time and obs_interruptions_by_plan read it twice more: over the limit.
7
+ -- Node 26 bundles 3.54.0. Same rows, same columns, same order; only how often each view is read changes.
8
+
9
+ DROP VIEW obs_lockout_intervals;
10
+
11
+ -- Observed: elapsed lockout intervals (limit hit to reset, overlapping hits merged), with the next request after each reset, logged auto-continuations, and the sessions and refused requests in each (D-023, D-077, D-090).
12
+ CREATE VIEW obs_lockout_intervals AS
13
+ WITH continuation_counts AS (
14
+ -- Logged auto-continuations per limit hit, counted before the join so a hit stays one row.
15
+ SELECT hit_raw_line_id, COUNT(*) AS marks, MIN(continued_at_utc) AS first_continued_at_utc
16
+ FROM auto_continuation_marks
17
+ WHERE hit_raw_line_id IS NOT NULL
18
+ GROUP BY hit_raw_line_id
19
+ ),
20
+ grouped AS (
21
+ -- One pass over the lockout hits: each is one row (its raw line), and every join below is one-to-one.
22
+ SELECT
23
+ h.interval_number,
24
+ MIN(h.start_utc) AS locked_from_utc,
25
+ MAX(h.end_utc) AS locked_until_utc,
26
+ COUNT(*) AS hits,
27
+ -- A logged hit names its session; a status-line-only hit is a spool reading, which names one too.
28
+ COUNT(DISTINCT COALESCE(p.session_id, s.session_id)) AS sessions,
29
+ -- Hits from the session logs: each is a request the limit refused.
30
+ COUNT(p.raw_line_id) AS refused_requests,
31
+ SUM(m.marks) AS auto_continuations,
32
+ MIN(m.first_continued_at_utc) AS first_continued_at_utc
33
+ FROM obs_lockout_interval_hits h
34
+ LEFT JOIN parsed_lines p ON p.raw_line_id = h.raw_line_id
35
+ LEFT JOIN status_readings s ON s.raw_line_id = h.raw_line_id
36
+ LEFT JOIN continuation_counts m ON m.hit_raw_line_id = h.raw_line_id
37
+ GROUP BY h.interval_number
38
+ ),
39
+ with_next AS (
40
+ SELECT
41
+ g.*,
42
+ (SELECT MIN(d.timestamp_utc) FROM requests_dedup d WHERE d.timestamp_utc >= g.locked_until_utc) AS next_request_at_utc
43
+ FROM grouped g
44
+ )
45
+ SELECT
46
+ w.interval_number,
47
+ w.locked_from_utc,
48
+ w.locked_until_utc,
49
+ w.hits,
50
+ w.next_request_at_utc,
51
+ (round(unixepoch(w.locked_until_utc, 'subsec') * 1000) - round(unixepoch(w.locked_from_utc, 'subsec') * 1000)) / 1000.0 AS lockout_seconds,
52
+ (round(unixepoch(w.next_request_at_utc, 'subsec') * 1000) - round(unixepoch(w.locked_until_utc, 'subsec') * 1000)) / 1000.0 AS reset_to_next_request_seconds,
53
+ w.locked_until_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage,
54
+ -- How many sessions Claude Code resumed by itself after this reset, as logged; 0 when none.
55
+ COALESCE(w.auto_continuations, 0) AS auto_continuations,
56
+ -- The earliest of those, seconds after the reset; NULL when none. The reset is one time per lockout, so the
57
+ -- earliest continuation gives the smallest difference.
58
+ (round(unixepoch(w.first_continued_at_utc, 'subsec') * 1000) - round(unixepoch(w.locked_until_utc, 'subsec') * 1000)) / 1000.0
59
+ AS first_auto_continuation_seconds,
60
+ -- Distinct sessions with a hit in this lockout (D-090).
61
+ w.sessions,
62
+ -- Its logged hits, each a refused request; the rest of its hits are status line readings at 100% (D-023, D-090).
63
+ w.refused_requests
64
+ FROM with_next w;
65
+
66
+ DROP VIEW obs_lockout_time;
67
+
68
+ -- Observed: total elapsed lockout time (hit → reset, overlaps counted once), beside hits whose reset is unknown.
69
+ CREATE VIEW obs_lockout_time AS
70
+ SELECT
71
+ COUNT(*) AS intervals,
72
+ TOTAL(lockout_seconds) AS lockout_seconds,
73
+ -- Every hit with a known reset is in exactly one interval, so the intervals' hits add up to them.
74
+ COALESCE(SUM(hits), 0) AS hits_with_known_reset,
75
+ (SELECT COUNT(*) FROM obs_limit_hits_events WHERE reset_at_utc IS NULL OR hit_at_utc IS NULL) AS hits_with_unknown_reset,
76
+ (SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
77
+ (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
78
+ FROM obs_lockout_intervals;
79
+
80
+ DROP VIEW obs_interruptions_by_plan;
81
+
82
+ -- Observed: limit hits, lockouts, and elapsed lockout time per plan period, each with its own span (D-075).
83
+ CREATE VIEW obs_interruptions_by_plan AS
84
+ WITH periods AS (
85
+ SELECT 'plan' AS period_kind, from_month, until_month, plan_name FROM plan_periods
86
+ UNION ALL
87
+ -- Before the first entered plan; with no plan entered, the whole history.
88
+ SELECT 'none', NULL, (SELECT MIN(from_month) FROM plan_periods), NULL
89
+ UNION ALL
90
+ -- Hits with no time: counted, so the rows add up to obs_limit_hits, but in no period.
91
+ SELECT 'unknown', NULL, NULL, NULL
92
+ ),
93
+ -- Materialized: the hit view is among the costliest on real data (D-059), and two CTEs read it.
94
+ hits AS MATERIALIZED (
95
+ SELECT period_kind, from_month, window, hit_at_utc FROM obs_limit_hits_by_plan_events
96
+ ),
97
+ -- Counted per period once, then joined: a subquery per count would read the views once per count (D-094).
98
+ hit_counts AS (
99
+ SELECT
100
+ period_kind,
101
+ from_month,
102
+ COUNT(*) AS hits,
103
+ SUM(window = 'five_hour') AS five_hour_hits,
104
+ SUM(window = 'seven_day') AS seven_day_hits,
105
+ SUM(window IS NULL) AS unknown_window_hits
106
+ FROM hits
107
+ GROUP BY period_kind, from_month
108
+ ),
109
+ lock_counts AS (
110
+ SELECT period_kind, from_month, COUNT(*) AS lockouts, TOTAL(lockout_seconds) AS lockout_seconds
111
+ FROM obs_lockout_by_plan_events
112
+ GROUP BY period_kind, from_month
113
+ ),
114
+ -- What a period's span is drawn from: request times and hit times, each with its local month.
115
+ dated AS MATERIALIZED (
116
+ SELECT timestamp_utc AS at_utc, strftime('%Y-%m', timestamp_utc, 'localtime') AS month
117
+ FROM requests_dedup WHERE timestamp_utc IS NOT NULL
118
+ UNION ALL
119
+ SELECT hit_at_utc, strftime('%Y-%m', hit_at_utc, 'localtime') FROM hits WHERE hit_at_utc IS NOT NULL
120
+ ),
121
+ counted AS (
122
+ SELECT
123
+ p.period_kind,
124
+ p.from_month,
125
+ -- The last month the plan applies, for display: the month before until_month.
126
+ CASE WHEN p.until_month IS NOT NULL
127
+ THEN strftime('%Y-%m', p.until_month || '-01', '-1 month') END AS to_month,
128
+ p.until_month,
129
+ p.plan_name,
130
+ COALESCE(hc.hits, 0) AS hits,
131
+ COALESCE(hc.five_hour_hits, 0) AS five_hour_hits,
132
+ COALESCE(hc.seven_day_hits, 0) AS seven_day_hits,
133
+ COALESCE(hc.unknown_window_hits, 0) AS unknown_window_hits,
134
+ COALESCE(lc.lockouts, 0) AS lockouts,
135
+ -- TOTAL, as obs_lockout_time totals intervals: 0 over none.
136
+ COALESCE(lc.lockout_seconds, 0.0) AS lockout_seconds,
137
+ -- The span the period's data covers, from what is in it (principle 5); none for 'unknown'.
138
+ (SELECT MIN(d.at_utc) FROM dated d WHERE p.period_kind <> 'unknown'
139
+ AND (p.from_month IS NULL OR d.month >= p.from_month)
140
+ AND (p.until_month IS NULL OR d.month < p.until_month)) AS covers_from,
141
+ (SELECT MAX(d.at_utc) FROM dated d WHERE p.period_kind <> 'unknown'
142
+ AND (p.from_month IS NULL OR d.month >= p.from_month)
143
+ AND (p.until_month IS NULL OR d.month < p.until_month)) AS covers_to
144
+ FROM periods p
145
+ -- IS, not =: the 'none' and 'unknown' periods have no month, and NULL = NULL is never true.
146
+ LEFT JOIN hit_counts hc ON hc.period_kind = p.period_kind AND hc.from_month IS p.from_month
147
+ LEFT JOIN lock_counts lc ON lc.period_kind = p.period_kind AND lc.from_month IS p.from_month
148
+ )
149
+ SELECT *
150
+ FROM counted
151
+ -- A period appears when it holds a hit or any dated data; an entry for a month with no data, or
152
+ -- a plan entered for the future, has nothing to report.
153
+ WHERE hits > 0 OR covers_from IS NOT NULL;
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file Command-line entry point. Holds no logic, so it is excluded from coverage; everything it
4
+ * runs is tested through `checkNodeVersion()` in node-version.ts, `runCli()` in program.ts, and
5
+ * the CLI integration tests.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { checkNodeVersion } from "../core/util/node-version.js";
11
+ // Checked before the program is imported: on an older Node.js, the built-in `node:sqlite` is missing
12
+ // (22.11 throws on import without a flag) or experimental (22.23 warns on every run), and Nilometer
13
+ // isn't tested there (D-066, D-093). The program is loaded with a dynamic import below so nothing
14
+ // that loads `node:sqlite` runs before this check.
15
+ const nodeCheck = checkNodeVersion(process.versions.node);
16
+ if (nodeCheck.message !== undefined) {
17
+ console.error(nodeCheck.message);
18
+ }
19
+ if (nodeCheck.status === "too_old") {
20
+ process.exitCode = 1;
21
+ }
22
+ else {
23
+ const { findPackageRoot } = await import("../core/install/locations.js");
24
+ const { runCli } = await import("./program.js");
25
+ process.exitCode = runCli(process.argv.slice(2), {
26
+ home: homedir(),
27
+ env: process.env,
28
+ packageRoot: findPackageRoot(dirname(fileURLToPath(import.meta.url))),
29
+ now: () => new Date(),
30
+ print: (line) => {
31
+ console.log(line);
32
+ },
33
+ printError: (line) => {
34
+ console.error(line);
35
+ },
36
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
37
+ });
38
+ }
39
+ //# sourceMappingURL=main.cli.js.map