gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitc613d26bc613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mrec613d26b/lib/quota.hl

5.3 KB

  1. // quota.hl — the Claude usage of this host's Claude account (mission 020; concept §3 "Claude quota per account:
  2. // reserve threshold, e.g. no new starts above 70 % of the 5 h window").
  3. //
  4. // HOW (tested 2026-09-24, Claude Code 2.1.280 on Loreana, .scratch/m020/usage-probe/): `claude -p /usage` is a LOCAL
  5. // command — no model call: result `total_cost_usd: 0`, `num_turns: 0`, no tokens, ~1–4 s. With
  6. // `--output-format json --verbose` the answer is a message list whose assistant message carries
  7. // usage_report.rate_limits.limits = [{ kind: 'session', percent: 5, resets_at: '2026-09-24T18:19:59.77+00:00' },
  8. // { kind: 'weekly_all', percent: 41, … }, { kind: 'weekly_scoped', … }]
  9. // and the text "Current session: 5% used · resets …" (the fallback when the structured part is missing).
  10. // The probe runs with `--permission-mode plan --permission-prompts none --strict-mcp-config --model haiku
  11. // --max-turns 1` so that, if `/usage` ever stopped being local, it would still be one cheap refused turn;
  12. // a probe that cost anything is reported (`cost`).
  13. import { readJson, isList, isObject, isString } from './util.hl'
  14. import { runClaude } from './claude.hl'
  15. static digit = (c) => { return c >= 48 && c <= 57 }
  16. // integer division for non-negative numbers (no Math)
  17. static idiv = (a, b) => { return (a - a % b) / b }
  18. // days since 1970-01-01 of a civil date (H. Hinnant's days_from_civil)
  19. static daysFromCivil = (y0, m, d) => {
  20. let y = m <= 2 ? y0 - 1 : y0
  21. let era = idiv(y, 400)
  22. let yoe = y - era * 400
  23. let mp = m > 2 ? m - 3 : m + 9
  24. let doy = idiv(153 * mp + 2, 5) + d - 1
  25. let doe = yoe * 365 + idiv(yoe, 4) - idiv(yoe, 100) + doy
  26. return era * 146097 + doe - 719468
  27. }
  28. // 'YYYY-MM-DDTHH:MM:SS[.frac][Z|±HH:MM]' → epoch ms, or null
  29. static isoToMs = (s) => {
  30. if (s == null || s.length < 19) { return null }
  31. let y = toNumber(s.slice(0, 4))
  32. let mo = toNumber(s.slice(5, 7))
  33. let d = toNumber(s.slice(8, 10))
  34. let h = toNumber(s.slice(11, 13))
  35. let mi = toNumber(s.slice(14, 16))
  36. let se = toNumber(s.slice(17, 19))
  37. if (y == null || mo == null || d == null || h == null || mi == null || se == null) { return null }
  38. let ms = ((daysFromCivil(y, mo, d) * 24 + h) * 60 + mi) * 60000 + se * 1000
  39. let i = 19
  40. if (i < s.length && s.charCodeAt(i) == 46) {
  41. i = i + 1
  42. let frac = ''
  43. while (i < s.length && digit(s.charCodeAt(i))) {
  44. frac = frac + s.slice(i, i + 1)
  45. i = i + 1
  46. }
  47. if (frac.length >= 3) { ms = ms + toNumber(frac.slice(0, 3)) }
  48. }
  49. if (i < s.length) {
  50. let sign = s.slice(i, i + 1)
  51. if ((sign == '+' || sign == '-') && s.length >= i + 6) {
  52. let oh = toNumber(s.slice(i + 1, i + 3))
  53. let om = toNumber(s.slice(i + 4, i + 6))
  54. if (oh != null && om != null) {
  55. let off = (oh * 60 + om) * 60000
  56. ms = sign == '+' ? ms - off : ms + off
  57. }
  58. }
  59. }
  60. return ms
  61. }
  62. // the number in 'Current session: 5% used' → 5 or null
  63. static percentAfter = (text, label) => {
  64. let i = text.indexOf(label)
  65. if (i < 0) { return null }
  66. let j = i + label.length
  67. let k = j
  68. while (k < text.length && digit(text.charCodeAt(k))) { k = k + 1 }
  69. if (k == j) { return null }
  70. return toNumber(text.slice(j, k))
  71. }
  72. // the probe's output (parsed JSON) → { percent, resetsMs, resetsAt, weekly, cost, how } or { error }
  73. static parseUsage = (value) => {
  74. let limits = null
  75. let text = null
  76. let cost = null
  77. let msgs = isList(value) ? value : [value]
  78. for (m of msgs) {
  79. if (isObject(m)) {
  80. if (m.type == 'assistant' && m.usage_report != null && m.usage_report.rate_limits != null && isList(m.usage_report.rate_limits.limits)) { limits = m.usage_report.rate_limits.limits }
  81. if (m.type == 'result') {
  82. if (isString(m.result)) { text = m.result }
  83. cost = m.total_cost_usd
  84. }
  85. }
  86. }
  87. if (limits != null) {
  88. let out = { percent = null resetsMs = null resetsAt = null weekly = null weeklyResetsMs = null cost = cost how = 'usage_report' }
  89. for (l of limits) {
  90. if (isObject(l) && l.kind == 'session') {
  91. out.percent = l.percent
  92. out.resetsAt = l.resets_at
  93. out.resetsMs = isoToMs(l.resets_at)
  94. }
  95. if (isObject(l) && l.kind == 'weekly_all') {
  96. out.weekly = l.percent
  97. out.weeklyResetsMs = isoToMs(l.resets_at)
  98. }
  99. }
  100. if (out.percent != null) { return out }
  101. }
  102. if (text != null) {
  103. let p = percentAfter(text, 'Current session: ')
  104. if (p != null) { return { percent = p resetsMs = null resetsAt = null weekly = percentAfter(text, 'Current week (all models): ') cost = cost how = 'text' } }
  105. }
  106. return { error = 'no "session" limit in the /usage answer' }
  107. }
  108. // runs the probe (bin = the Claude binary, dir = where its files go); calls done(reading) with
  109. // { percent, resetsMs, weekly, cost, … } or { error }. → the process
  110. static probeUsage = (bin, dir, done) => {
  111. let outFile = dir + '/.quota-probe.json'
  112. let errFile = dir + '/.quota-probe.err'
  113. let args = ['-p' '/usage' '--output-format' 'json' '--verbose' '--model' 'haiku' '--max-turns' '1' '--permission-mode' 'plan' '--permission-prompts' 'none' '--strict-mcp-config']
  114. return runClaude({ label = 'quota' folder = dir inFile = '/dev/null' outFile = outFile errFile = errFile bin = bin args = args timeout = '60' }, (x) => {
  115. if (x.timedOut) {
  116. done({ error = 'the /usage probe timed out (60 s)' })
  117. return null
  118. }
  119. let rj = readJson(outFile)
  120. if (rj.error != null) {
  121. done({ error = 'the /usage probe gave no JSON (exit ' + x.ev.exit + '): ' + rj.error })
  122. return null
  123. }
  124. done(parseUsage(rj.value))
  125. return null
  126. })
  127. }

Branches

Latest commits

  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre