thinkpool-pair 0.7.364 → 0.7.366

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 (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
package/model-prices.mjs CHANGED
@@ -1,113 +1 @@
1
- // Public model-price resolver for the bridge's usage ledger.
2
- // Runtime model catalogs tell us what ran, but not what a provider bills for
3
- // it. LiteLLM maintains a public catalog; cache its validated data locally and
4
- // persist the resolved rate beside each turn. A failed refresh never blocks a
5
- // turn or drops token accounting.
6
-
7
- import fs from 'node:fs'
8
- import os from 'node:os'
9
- import path from 'node:path'
10
-
11
- const CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
12
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000
13
- const CACHE_MAX_BYTES = 4 * 1024 * 1024
14
- const cachePath = () => process.env.TP_MODEL_PRICE_CACHE || path.join(process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair'), 'model-prices.json')
15
-
16
- // First-party runtimes remain priced while offline or before the catalog loads.
17
- const FALLBACKS = Object.freeze({
18
- 'gpt-5.6-sol': { input: 5, cached: 0.5, output: 30 },
19
- 'gpt-5.6-terra': { input: 2.5, cached: 0.25, output: 15 },
20
- 'gpt-5.6-luna': { input: 1, cached: 0.1, output: 6 },
21
- 'gpt-5.4': { input: 2.5, cached: 0.25, output: 15 },
22
- 'gpt-5.4-mini': { input: 0.75, cached: 0.075, output: 4.5 },
23
- 'gpt-5.4-nano': { input: 0.2, cached: 0.02, output: 1.25 },
24
- 'claude-opus-4-8': { input: 5, cached: 0.5, output: 25 },
25
- 'claude-fable-5': { input: 10, cached: 1, output: 50 },
26
- 'claude-sonnet-5': { input: 2, cached: 0.2, output: 10 },
27
- 'claude-haiku-4-5': { input: 1, cached: 0.1, output: 5 },
28
- 'glm-5.2': { input: 1.4, cached: 0.26, output: 4.4 },
29
- 'glm-4.6': { input: 0.6, cached: 0.11, output: 2.2 },
30
- })
31
-
32
- function cleanRate(raw) {
33
- // Catalog rates are per token; normalize at the persistence boundary so a
34
- // decimal like 0.0000001 does not become 0.09999999999999999 in Settings.
35
- const perMillion = (value) => Math.round(Number(value) * 1_000_000 * 1_000_000) / 1_000_000
36
- const input = perMillion(raw?.input_cost_per_token)
37
- const output = perMillion(raw?.output_cost_per_token)
38
- const cached = perMillion(raw?.cache_read_input_token_cost ?? raw?.cache_read_input_tokens_cost)
39
- if (!Number.isFinite(input) || input < 0 || !Number.isFinite(output) || output < 0) return null
40
- return { input, output, cached: Number.isFinite(cached) && cached >= 0 ? cached : null }
41
- }
42
-
43
- export function modelPriceKeys(model) {
44
- const raw = String(model || '').trim()
45
- if (!raw) return []
46
- const withoutMarker = raw.replace(/\[[^\]]+\]/g, '')
47
- const withoutPrefix = withoutMarker.replace(/^[^:/]+:/, '')
48
- const tail = withoutPrefix.includes('/') ? withoutPrefix.slice(withoutPrefix.lastIndexOf('/') + 1) : withoutPrefix
49
- return [...new Set([raw, withoutMarker, withoutPrefix, tail].filter(Boolean))]
50
- }
51
-
52
- function readCache() {
53
- try {
54
- const file = cachePath()
55
- if ((Number(fs.statSync(file).size) || 0) > CACHE_MAX_BYTES) return null
56
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
57
- return parsed?.models && typeof parsed.models === 'object' ? parsed : null
58
- } catch { return null }
59
- }
60
-
61
- function writeCache(models) {
62
- try {
63
- const file = cachePath()
64
- fs.mkdirSync(path.dirname(file), { recursive: true })
65
- fs.writeFileSync(file, JSON.stringify({ fetchedAt: Date.now(), models }), { mode: 0o600 })
66
- } catch { /* cache is opportunistic */ }
67
- }
68
-
69
- export function rateFromCatalog(model, models) {
70
- for (const key of modelPriceKeys(model)) {
71
- const rate = cleanRate(models?.[key])
72
- if (rate) return rate
73
- }
74
- for (const key of modelPriceKeys(model)) {
75
- const fallback = FALLBACKS[key.toLowerCase()]
76
- if (fallback) return fallback
77
- }
78
- return null
79
- }
80
-
81
- export function createModelPriceResolver({ fetchImpl = globalThis.fetch } = {}) {
82
- let cached = readCache()
83
- let refresh = null
84
- const refreshCatalog = async () => {
85
- if (refresh) return refresh
86
- refresh = (async () => {
87
- let timer
88
- try {
89
- const controller = new AbortController()
90
- timer = setTimeout(() => controller.abort(), 4_000)
91
- const response = await fetchImpl(CATALOG_URL, { signal: controller.signal })
92
- if (!response?.ok) return cached?.models || null
93
- const models = await response.json()
94
- if (!models || typeof models !== 'object') return cached?.models || null
95
- cached = { fetchedAt: Date.now(), models }
96
- writeCache(models)
97
- return models
98
- } catch { return cached?.models || null }
99
- finally { clearTimeout(timer); refresh = null }
100
- })()
101
- return refresh
102
- }
103
- return async (model) => {
104
- const current = rateFromCatalog(model, cached?.models)
105
- if (current) return { ...current, source: cached?.models ? 'litellm-cache' : 'bridge-fallback' }
106
- const stale = !cached || (Date.now() - Number(cached.fetchedAt || 0)) > CACHE_TTL_MS
107
- const models = stale ? await refreshCatalog() : cached?.models
108
- const rate = rateFromCatalog(model, models)
109
- return rate ? { ...rate, source: models ? 'litellm' : 'bridge-fallback' } : null
110
- }
111
- }
112
-
113
- export const priceForModel = createModelPriceResolver()
1
+ import e from"node:fs";import t from"node:os";import o from"node:path";const n=()=>process.env.TP_MODEL_PRICE_CACHE||o.join(process.env.TP_PAIR_ROOT||o.join(t.homedir(),".thinkpool-pair"),"model-prices.json"),c=Object.freeze({"gpt-5.6-sol":{input:5,cached:.5,output:30},"gpt-5.6-terra":{input:2.5,cached:.25,output:15},"gpt-5.6-luna":{input:1,cached:.1,output:6},"gpt-5.4":{input:2.5,cached:.25,output:15},"gpt-5.4-mini":{input:.75,cached:.075,output:4.5},"gpt-5.4-nano":{input:.2,cached:.02,output:1.25},"claude-opus-4-8":{input:5,cached:.5,output:25},"claude-fable-5":{input:10,cached:1,output:50},"claude-sonnet-5":{input:2,cached:.2,output:10},"claude-haiku-4-5":{input:1,cached:.1,output:5},"glm-5.2":{input:1.4,cached:.26,output:4.4},"glm-4.6":{input:.6,cached:.11,output:2.2}});function r(e){const t=e=>Math.round(1e6*Number(e)*1e6)/1e6,o=t(e?.input_cost_per_token),n=t(e?.output_cost_per_token),c=t(e?.cache_read_input_token_cost??e?.cache_read_input_tokens_cost);return!Number.isFinite(o)||o<0||!Number.isFinite(n)||n<0?null:{input:o,output:n,cached:Number.isFinite(c)&&c>=0?c:null}}export function modelPriceKeys(e){const t=String(e||"").trim();if(!t)return[];const o=t.replace(/\[[^\]]+\]/g,""),n=o.replace(/^[^:/]+:/,""),c=n.includes("/")?n.slice(n.lastIndexOf("/")+1):n;return[...new Set([t,o,n,c].filter(Boolean))]}export function rateFromCatalog(e,t){for(const o of modelPriceKeys(e)){const e=r(t?.[o]);if(e)return e}for(const t of modelPriceKeys(e)){const e=c[t.toLowerCase()];if(e)return e}return null}export function createModelPriceResolver({fetchImpl:t=globalThis.fetch}={}){let c=function(){try{const t=n();if((Number(e.statSync(t).size)||0)>4194304)return null;const o=JSON.parse(e.readFileSync(t,"utf8"));return o?.models&&"object"==typeof o.models?o:null}catch{return null}}(),r=null;return async u=>{const l=rateFromCatalog(u,c?.models);if(l)return{...l,source:c?.models?"litellm-cache":"bridge-fallback"};const i=!c||Date.now()-Number(c.fetchedAt||0)>864e5?await(async()=>r||(r=(async()=>{let u;try{const r=new AbortController;u=setTimeout(()=>r.abort(),4e3);const l=await t("https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",{signal:r.signal});if(!l?.ok)return c?.models||null;const i=await l.json();return i&&"object"==typeof i?(c={fetchedAt:Date.now(),models:i},function(t){try{const c=n();e.mkdirSync(o.dirname(c),{recursive:!0}),e.writeFileSync(c,JSON.stringify({fetchedAt:Date.now(),models:t}),{mode:384})}catch{}}(i),i):c?.models||null}catch{return c?.models||null}finally{clearTimeout(u),r=null}})(),r))():c?.models,a=rateFromCatalog(u,i);return a?{...a,source:i?"litellm":"bridge-fallback"}:null}}export const priceForModel=createModelPriceResolver();
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.364",
3
+ "version": "0.7.366",
4
4
  "description": "Connect Claude Code, Codex, or Hermes on your computer to a Thinkpool Code room.",
5
+ "license": "SEE LICENSE IN LICENSE",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "thinkpool-pair": "bridge.mjs"
8
9
  },
9
10
  "files": [
11
+ "LICENSE",
10
12
  "bridge.mjs",
11
13
  "bridge-service.sh",
12
14
  "command-guidance.mjs",
@@ -15,7 +17,6 @@
15
17
  "keep-awake.mjs",
16
18
  "sdk-smoke.mjs",
17
19
  "sdk-admission.mjs",
18
- "sdk-admission.mjs",
19
20
  "launcher.mjs",
20
21
  "privacy-report.mjs",
21
22
  "agent-visibility.mjs",
@@ -122,10 +123,13 @@
122
123
  "providers.mjs",
123
124
  "provider-resilience.mjs",
124
125
  "model-prices.mjs",
125
- "README.md"
126
+ "README.md",
127
+ "publish-guard.mjs",
128
+ "publish-manifest.json"
126
129
  ],
127
130
  "scripts": {
128
- "release": "./publish.sh"
131
+ "prepack": "node publish-guard.mjs",
132
+ "prepublishOnly": "node publish-guard.mjs"
129
133
  },
130
134
  "engines": {
131
135
  "node": ">=18"
@@ -141,5 +145,10 @@
141
145
  },
142
146
  "optionalDependencies": {
143
147
  "node-pty": "^1.2.0-beta.13"
148
+ },
149
+ "thinkpoolArtifact": {
150
+ "format": 1,
151
+ "kind": "minified",
152
+ "manifest": "publish-manifest.json"
144
153
  }
145
154
  }
package/pair-bus.mjs CHANGED
@@ -1,98 +1 @@
1
- // Deterministic request broker for the cross-machine pair bus.
2
- //
3
- // Supabase channel.send() is asynchronous and a channel is not usable until its
4
- // subscription callback reports SUBSCRIBED. Keeping those facts in one tested
5
- // primitive prevents a joining/dead channel from impersonating a reachable peer
6
- // and prevents a fast reply from arriving before its waiter exists.
7
-
8
- export const PAIR_BUS_STATUS = Object.freeze({
9
- JOINING: 'joining',
10
- SUBSCRIBED: 'subscribed',
11
- ERROR: 'error',
12
- CLOSED: 'closed',
13
- })
14
-
15
- export const pairBusStatusFromRealtime = (status) => {
16
- if (status === 'SUBSCRIBED') return PAIR_BUS_STATUS.SUBSCRIBED
17
- if (status === 'CLOSED') return PAIR_BUS_STATUS.CLOSED
18
- if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') return PAIR_BUS_STATUS.ERROR
19
- return PAIR_BUS_STATUS.JOINING
20
- }
21
-
22
- export const subscribedPairChannels = (channels) =>
23
- [...(channels?.values?.() || [])].filter((entry) =>
24
- entry?.status === PAIR_BUS_STATUS.SUBSCRIBED && entry?.channel?.send)
25
-
26
- export async function sendPairBroadcast (channels, event, payload) {
27
- const results = await Promise.all(subscribedPairChannels(channels).map(async ({ channel }) => {
28
- try { return (await channel.send({ type: 'broadcast', event, payload })) === 'ok' }
29
- catch { return false }
30
- }))
31
- return results.filter(Boolean).length
32
- }
33
-
34
- export function createPairBusBroker ({ channels, randomId, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
35
- if (!channels?.values || typeof randomId !== 'function') throw new TypeError('channels and randomId are required')
36
- const waiters = new Map()
37
-
38
- const resolve = (payload) => {
39
- if (!payload?.nonce) return false
40
- const waiter = waiters.get(payload.nonce)
41
- if (!waiter) return false
42
- if (waiter.mode === 'collect') { waiter.acc.push(payload); return true }
43
- clearTimer(waiter.timer); waiters.delete(payload.nonce); waiter.finish(payload)
44
- return true
45
- }
46
-
47
- const requestFirst = (event, payload = {}, timeoutMs) => new Promise((finish) => {
48
- const nonce = randomId()
49
- const timer = setTimer(() => { waiters.delete(nonce); finish(null) }, timeoutMs)
50
- timer?.unref?.()
51
- waiters.set(nonce, { mode: 'first', finish, timer })
52
- void sendPairBroadcast(channels, event, { ...payload, nonce }).then((sent) => {
53
- if (sent || !waiters.has(nonce)) return
54
- clearTimer(timer); waiters.delete(nonce); finish(null)
55
- })
56
- })
57
-
58
- const requestCollect = (event, payload = {}, windowMs) => new Promise((finish) => {
59
- const nonce = randomId()
60
- const acc = []
61
- const timer = setTimer(() => { waiters.delete(nonce); finish(acc) }, windowMs)
62
- timer?.unref?.()
63
- waiters.set(nonce, { mode: 'collect', acc, finish, timer })
64
- void sendPairBroadcast(channels, event, { ...payload, nonce }).then((sent) => {
65
- if (sent || !waiters.has(nonce)) return
66
- clearTimer(timer); waiters.delete(nonce); finish(acc)
67
- })
68
- })
69
-
70
- const close = () => {
71
- for (const [nonce, waiter] of waiters) {
72
- clearTimer(waiter.timer)
73
- waiter.finish(waiter.mode === 'collect' ? waiter.acc : null)
74
- waiters.delete(nonce)
75
- }
76
- }
77
-
78
- return Object.freeze({ resolve, requestFirst, requestCollect, close, pendingCount: () => waiters.size })
79
- }
80
-
81
- // The database is the authorization/discovery source; bus replies are the live
82
- // reachability source. Paired rooms remain visible while their host bridge is
83
- // offline and become online only when that host actually answers on the bus.
84
- export function mergePairRoomRoster ({ authorized = [], replies = [], myUid, localCodes = [] } = {}) {
85
- const local = new Set(localCodes)
86
- const rooms = new Map()
87
- for (const row of authorized) {
88
- if (!row?.code || row.owner_id === myUid || local.has(row.code)) continue
89
- rooms.set(row.code, { code: row.code, name: row.name || null, online: false })
90
- }
91
- for (const reply of replies) {
92
- for (const row of Array.isArray(reply?.rooms) ? reply.rooms : []) {
93
- if (!row?.code || local.has(row.code)) continue
94
- rooms.set(row.code, { code: row.code, name: row.name || rooms.get(row.code)?.name || null, host: row.host || null, online: true })
95
- }
96
- }
97
- return [...rooms.values()]
98
- }
1
+ export const PAIR_BUS_STATUS=Object.freeze({JOINING:"joining",SUBSCRIBED:"subscribed",ERROR:"error",CLOSED:"closed"});export const pairBusStatusFromRealtime=e=>"SUBSCRIBED"===e?PAIR_BUS_STATUS.SUBSCRIBED:"CLOSED"===e?PAIR_BUS_STATUS.CLOSED:"CHANNEL_ERROR"===e||"TIMED_OUT"===e?PAIR_BUS_STATUS.ERROR:PAIR_BUS_STATUS.JOINING;export const subscribedPairChannels=e=>[...e?.values?.()||[]].filter(e=>e?.status===PAIR_BUS_STATUS.SUBSCRIBED&&e?.channel?.send);export async function sendPairBroadcast(e,n,o){return(await Promise.all(subscribedPairChannels(e).map(async({channel:e})=>{try{return"ok"===await e.send({type:"broadcast",event:n,payload:o})}catch{return!1}}))).filter(Boolean).length}export function createPairBusBroker({channels:e,randomId:n,setTimer:o=setTimeout,clearTimer:r=clearTimeout}={}){if(!e?.values||"function"!=typeof n)throw new TypeError("channels and randomId are required");const t=new Map;return Object.freeze({resolve:e=>{if(!e?.nonce)return!1;const n=t.get(e.nonce);return!!n&&("collect"===n.mode?(n.acc.push(e),!0):(r(n.timer),t.delete(e.nonce),n.finish(e),!0))},requestFirst:(s,c={},a)=>new Promise(i=>{const l=n(),d=o(()=>{t.delete(l),i(null)},a);d?.unref?.(),t.set(l,{mode:"first",finish:i,timer:d}),sendPairBroadcast(e,s,{...c,nonce:l}).then(e=>{!e&&t.has(l)&&(r(d),t.delete(l),i(null))})}),requestCollect:(s,c={},a)=>new Promise(i=>{const l=n(),d=[],u=o(()=>{t.delete(l),i(d)},a);u?.unref?.(),t.set(l,{mode:"collect",acc:d,finish:i,timer:u}),sendPairBroadcast(e,s,{...c,nonce:l}).then(e=>{!e&&t.has(l)&&(r(u),t.delete(l),i(d))})}),close:()=>{for(const[e,n]of t)r(n.timer),n.finish("collect"===n.mode?n.acc:null),t.delete(e)},pendingCount:()=>t.size})}export function mergePairRoomRoster({authorized:e=[],replies:n=[],myUid:o,localCodes:r=[]}={}){const t=new Set(r),s=new Map;for(const n of e)n?.code&&n.owner_id!==o&&!t.has(n.code)&&s.set(n.code,{code:n.code,name:n.name||null,online:!1});for(const e of n)for(const n of Array.isArray(e?.rooms)?e.rooms:[])n?.code&&!t.has(n.code)&&s.set(n.code,{code:n.code,name:n.name||s.get(n.code)?.name||null,host:n.host||null,online:!0});return[...s.values()]}
@@ -1,89 +1 @@
1
- // Durable authority check for the bridge's public `code-perm` transport.
2
- // Realtime is only a wake-up hint: the owner-authenticated REST reads below
3
- // must reproduce the resolved item and its unique delivery-attempt record before
4
- // a local pending action is allowed to move.
5
-
6
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
7
- const SAFE_KEY = /^[A-Za-z0-9][A-Za-z0-9._:@-]{7,159}$/
8
- const FINAL_STATUS = new Set(['resolved', 'denied'])
9
-
10
- const denied = (code) => Object.freeze({ ok: false, code })
11
-
12
- export function verifyPairControlDeliveryAuthority ({ item, attempts, payload, roomCode, bridgeId } = {}) {
13
- if (!item || !payload || !UUID.test(String(payload.controlItemId || '')) ||
14
- !SAFE_KEY.test(String(payload.attemptKey || '')) ||
15
- !Number.isSafeInteger(payload.controlVersion) || payload.controlVersion < 1) {
16
- return denied('invalid_delivery_hint')
17
- }
18
- const context = item.request_context
19
- const resolution = item.resolution_payload
20
- if (item.id !== payload.controlItemId || item.session_code !== String(roomCode || '').toUpperCase() ||
21
- item.item_kind !== 'approval' || !['approval', 'dispatch'].includes(item.action_kind) ||
22
- item.resolution_policy !== 'either_member' || item.required_actor_id != null ||
23
- !FINAL_STATUS.has(item.status) || Number(item.version) !== payload.controlVersion ||
24
- item.bridge_authority_id !== bridgeId || item.local_authority_id !== payload.term ||
25
- !context || context.permission_id !== payload.id || context.terminal_ref !== payload.term ||
26
- !resolution || resolution.decision !== payload.decision || !item.resolved_by) {
27
- return denied('authority_mismatch')
28
- }
29
- if ((item.status === 'resolved' && !['approved', 'answered'].includes(item.resolution_code)) ||
30
- (item.status === 'denied' && item.resolution_code !== 'rejected')) {
31
- return denied('resolution_mismatch')
32
- }
33
- const matching = Array.isArray(attempts) ? attempts.filter((event) =>
34
- event?.actor_id === item.resolved_by && Number(event.item_version) === Number(item.version) &&
35
- event.correlation_key === payload.attemptKey && event.payload?.data?.transport === 'code-perm') : []
36
- if (matching.length !== 1) return denied('delivery_attempt_unverified')
37
- const durableResponder = resolution.responder
38
- const responder = durableResponder && durableResponder.id === item.resolved_by
39
- ? {
40
- id: item.resolved_by,
41
- name: typeof durableResponder.name === 'string' && durableResponder.name.trim()
42
- ? durableResponder.name.trim().slice(0, 120)
43
- : null,
44
- }
45
- : { id: item.resolved_by, name: null }
46
- return Object.freeze({
47
- ok: true,
48
- code: 'durable_delivery_authorized',
49
- decision: resolution.decision,
50
- // App Server input is recovered from the durable resolution only. The public
51
- // broadcast intentionally remains a wake-up hint and cannot supply answers.
52
- answers: resolution.answers && typeof resolution.answers === 'object' ? resolution.answers : {},
53
- responder: Object.freeze(responder),
54
- itemId: item.id,
55
- version: Number(item.version),
56
- attemptKey: payload.attemptKey,
57
- })
58
- }
59
-
60
- export async function fetchPairControlDeliveryAuthority ({
61
- fetchImpl = globalThis.fetch, supabaseUrl, anonKey, token, roomCode, bridgeId, payload,
62
- } = {}) {
63
- if (typeof fetchImpl !== 'function' || !supabaseUrl || !anonKey || !token || !roomCode || !bridgeId ||
64
- !UUID.test(String(payload?.controlItemId || '')) || !SAFE_KEY.test(String(payload?.attemptKey || ''))) {
65
- return denied('authority_unavailable')
66
- }
67
- const headers = { apikey: anonKey, Authorization: `Bearer ${token}` }
68
- const itemUrl = new URL('/rest/v1/code_pair_control_items', supabaseUrl)
69
- itemUrl.searchParams.set('id', `eq.${payload.controlItemId}`)
70
- itemUrl.searchParams.set('session_code', `eq.${String(roomCode).toUpperCase()}`)
71
- itemUrl.searchParams.set('select', 'id,session_code,item_kind,action_kind,resolution_policy,required_actor_id,status,version,resolution_code,resolution_payload,resolved_by,bridge_authority_id,local_authority_id,request_context')
72
- const eventUrl = new URL('/rest/v1/code_pair_control_events', supabaseUrl)
73
- eventUrl.searchParams.set('item_id', `eq.${payload.controlItemId}`)
74
- eventUrl.searchParams.set('event_type', 'eq.delivery_attempted')
75
- eventUrl.searchParams.set('correlation_key', `eq.${payload.attemptKey}`)
76
- eventUrl.searchParams.set('select', 'actor_id,item_version,correlation_key,payload')
77
- eventUrl.searchParams.set('limit', '2')
78
- try {
79
- const [itemResponse, eventResponse] = await Promise.all([
80
- fetchImpl(itemUrl, { headers }), fetchImpl(eventUrl, { headers }),
81
- ])
82
- if (!itemResponse.ok || !eventResponse.ok) return denied('authority_read_failed')
83
- const [items, attempts] = await Promise.all([itemResponse.json(), eventResponse.json()])
84
- if (!Array.isArray(items) || items.length !== 1 || !Array.isArray(attempts)) return denied('authority_read_invalid')
85
- return verifyPairControlDeliveryAuthority({ item: items[0], attempts, payload, roomCode, bridgeId })
86
- } catch {
87
- return denied('authority_read_failed')
88
- }
89
- }
1
+ const e=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,t=/^[A-Za-z0-9][A-Za-z0-9._:@-]{7,159}$/,r=new Set(["resolved","denied"]),o=e=>Object.freeze({ok:!1,code:e});export function verifyPairControlDeliveryAuthority({item:i,attempts:a,payload:s,roomCode:n,bridgeId:d}={}){if(!i||!s||!e.test(String(s.controlItemId||""))||!t.test(String(s.attemptKey||""))||!Number.isSafeInteger(s.controlVersion)||s.controlVersion<1)return o("invalid_delivery_hint");const l=i.request_context,c=i.resolution_payload;if(!(i.id===s.controlItemId&&i.session_code===String(n||"").toUpperCase()&&"approval"===i.item_kind&&["approval","dispatch"].includes(i.action_kind)&&"either_member"===i.resolution_policy&&null==i.required_actor_id&&r.has(i.status)&&Number(i.version)===s.controlVersion&&i.bridge_authority_id===d&&i.local_authority_id===s.term&&l&&l.permission_id===s.id&&l.terminal_ref===s.term&&c&&c.decision===s.decision&&i.resolved_by))return o("authority_mismatch");if("resolved"===i.status&&!["approved","answered"].includes(i.resolution_code)||"denied"===i.status&&"rejected"!==i.resolution_code)return o("resolution_mismatch");if(1!==(Array.isArray(a)?a.filter(e=>e?.actor_id===i.resolved_by&&Number(e.item_version)===Number(i.version)&&e.correlation_key===s.attemptKey&&"code-perm"===e.payload?.data?.transport):[]).length)return o("delivery_attempt_unverified");const _=c.responder,m=_&&_.id===i.resolved_by?{id:i.resolved_by,name:"string"==typeof _.name&&_.name.trim()?_.name.trim().slice(0,120):null}:{id:i.resolved_by,name:null};return Object.freeze({ok:!0,code:"durable_delivery_authorized",decision:c.decision,answers:c.answers&&"object"==typeof c.answers?c.answers:{},responder:Object.freeze(m),itemId:i.id,version:Number(i.version),attemptKey:s.attemptKey})}export async function fetchPairControlDeliveryAuthority({fetchImpl:r=globalThis.fetch,supabaseUrl:i,anonKey:a,token:s,roomCode:n,bridgeId:d,payload:l}={}){if(!("function"==typeof r&&i&&a&&s&&n&&d&&e.test(String(l?.controlItemId||""))&&t.test(String(l?.attemptKey||""))))return o("authority_unavailable");const c={apikey:a,Authorization:`Bearer ${s}`},_=new URL("/rest/v1/code_pair_control_items",i);_.searchParams.set("id",`eq.${l.controlItemId}`),_.searchParams.set("session_code",`eq.${String(n).toUpperCase()}`),_.searchParams.set("select","id,session_code,item_kind,action_kind,resolution_policy,required_actor_id,status,version,resolution_code,resolution_payload,resolved_by,bridge_authority_id,local_authority_id,request_context");const m=new URL("/rest/v1/code_pair_control_events",i);m.searchParams.set("item_id",`eq.${l.controlItemId}`),m.searchParams.set("event_type","eq.delivery_attempted"),m.searchParams.set("correlation_key",`eq.${l.attemptKey}`),m.searchParams.set("select","actor_id,item_version,correlation_key,payload"),m.searchParams.set("limit","2");try{const[e,t]=await Promise.all([r(_,{headers:c}),r(m,{headers:c})]);if(!e.ok||!t.ok)return o("authority_read_failed");const[i,a]=await Promise.all([e.json(),t.json()]);return Array.isArray(i)&&1===i.length&&Array.isArray(a)?verifyPairControlDeliveryAuthority({item:i[0],attempts:a,payload:l,roomCode:n,bridgeId:d}):o("authority_read_invalid")}catch{return o("authority_read_failed")}}
@@ -1,105 +1 @@
1
- const MAX_QUERY = 160
2
- const MAX_RESULTS = 8
3
- const MAX_RECORDS = 48
4
- const MAX_EVENTS = 800
5
- const MAX_EXCERPT = 260
6
- const SECRET_VALUE = /(?:sk[_-](?:proj[_-])?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|github_pat_[a-z0-9_]{20,}|glpat-[a-z0-9_-]{8,}|x(?:ox[baprsce]|app)-[a-z0-9_-]{8,}|(?:AKIA|ASIA)[0-9A-Z]{16}|npm_[a-z0-9]{24,}|bearer\s+[a-z0-9._-]{8,}|(?:api[_-]?key|access[_-]?token|auth(?:orization)?|token|secret)\s*[=:]\s*\S+)/i
7
- const PRIVATE_WORDS = /(?:system\s+prompt|hidden\s+reasoning|chain[ -]of[ -]thought|\bpassword\b|private[_ -]?key|\.env\b)/i
8
- const HOST_PATH = /(?:^|[\s=("'`])(?:~\/|\/(?:Users|home|var|tmp|private|workspace)\/|[A-Za-z]:\\|\\\\)[^\s)"'`]*/i
9
-
10
- function textContent(content) {
11
- if (typeof content === 'string') return content
12
- if (!Array.isArray(content)) return ''
13
- return content.map((item) => typeof item === 'string' ? item : item?.text || '').join(' ')
14
- }
15
-
16
- function safeExcerpt(value) {
17
- if (typeof value !== 'string') return null
18
- const line = value.replace(/\s+/g, ' ').trim()
19
- if (!line || SECRET_VALUE.test(line) || PRIVATE_WORDS.test(line) || HOST_PATH.test(line)) return null
20
- return line.slice(0, MAX_EXCERPT)
21
- }
22
-
23
- function eventCandidates(event) {
24
- if (!event || typeof event !== 'object') return []
25
- if (event.kind === 'flow-review-receipt') {
26
- const scope = event.scope
27
- return [
28
- `Flow ${event.outcome || 'review'}: ${event.taskKey || ''}`,
29
- ...(event.checks || []).map((line) => `Check: ${line}`),
30
- ...(event.reasons || []).map((line) => `Finding: ${line}`),
31
- ...((scope?.changed || []).map((line) => `Edited: ${line}`)),
32
- ...((scope?.unexpected || []).map((line) => `Unexpected: ${line}`)),
33
- ]
34
- }
35
- if (event.kind === 'assistant') {
36
- const out = []
37
- for (const block of event.blocks || []) {
38
- if (block?.type === 'text') out.push(block.text)
39
- if (block?.type === 'tool_use') {
40
- const name = String(block.name || '')
41
- if (/^(?:bash|exec_command|command_execution)$/i.test(name)) {
42
- const command = block.input?.command ?? block.input?.cmd
43
- if (command) out.push(`Command: ${command}`)
44
- }
45
- }
46
- }
47
- return out
48
- }
49
- if (event.kind === 'you') return [event.text]
50
- if (event.kind === 'tool_result') return [`Result: ${textContent(event.content)}`]
51
- if (event.kind === 'result') return [event.resultText]
52
- return []
53
- }
54
-
55
- function queryParts(query) {
56
- const text = String(query || '').replace(/\s+/g, ' ').trim()
57
- if (text.length < 2 || text.length > MAX_QUERY) throw new TypeError(`past-work query must contain 2-${MAX_QUERY} characters`)
58
- if (SECRET_VALUE.test(text) || PRIVATE_WORDS.test(text) || HOST_PATH.test(text)) return null
59
- return { text, lower: text.toLowerCase(), tokens: text.toLowerCase().split(/[^a-z0-9._/-]+/).filter((token) => token.length > 1) }
60
- }
61
-
62
- export function searchPastWork(records, query, { room, limit = MAX_RESULTS } = {}) {
63
- const parsed = queryParts(query)
64
- if (!parsed) return []
65
- const boundedLimit = Math.max(1, Math.min(MAX_RESULTS, Number(limit) || MAX_RESULTS))
66
- const results = []
67
- for (const record of (Array.isArray(records) ? records : []).slice(0, MAX_RECORDS)) {
68
- const terminal = safeExcerpt(record?.name) || 'Unnamed terminal'
69
- const events = Array.isArray(record?.log) ? record.log.slice(-MAX_EVENTS) : []
70
- for (const event of events) {
71
- for (const raw of eventCandidates(event)) {
72
- const excerpt = safeExcerpt(raw)
73
- if (!excerpt) continue
74
- const lower = excerpt.toLowerCase()
75
- const tokenHits = parsed.tokens.filter((token) => lower.includes(token)).length
76
- if (!lower.includes(parsed.lower) && (!parsed.tokens.length || tokenHits !== parsed.tokens.length)) continue
77
- results.push({
78
- room: String(room || '').trim() || 'current',
79
- terminal,
80
- terminalId: String(record?.id || '').slice(0, 80),
81
- ts: Number(event?.ts) || Number(record?.savedAt) || 0,
82
- seq: Number.isFinite(Number(event?.seq)) ? Number(event.seq) : null,
83
- excerpt,
84
- score: (lower.includes(parsed.lower) ? 10 : 0) + tokenHits + (excerpt.startsWith('Command:') ? 0 : 1),
85
- })
86
- }
87
- }
88
- }
89
- return results
90
- .sort((a, b) => b.score - a.score || b.ts - a.ts)
91
- .slice(0, boundedLimit)
92
- .map(({ score, ...result }) => result)
93
- }
94
-
95
- export function formatPastWorkSearch(results, query) {
96
- const safeQuery = safeExcerpt(String(query || '').trim())
97
- if (!results.length) return safeQuery
98
- ? `No cited past work matched “${safeQuery}” in this room.`
99
- : 'No cited past work matched that privacy-filtered query in this room.'
100
- return results.map((result, index) => {
101
- const date = result.ts ? new Date(result.ts).toISOString().slice(0, 10) : 'date unavailable'
102
- const event = result.seq == null ? 'saved transcript' : `event ${result.seq}`
103
- return `${index + 1}. ${result.excerpt}\n [Room ${result.room} · ${result.terminal} · ${date} · ${event}]`
104
- }).join('\n')
105
- }
1
+ const t=/(?:sk[_-](?:proj[_-])?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|github_pat_[a-z0-9_]{20,}|glpat-[a-z0-9_-]{8,}|x(?:ox[baprsce]|app)-[a-z0-9_-]{8,}|(?:AKIA|ASIA)[0-9A-Z]{16}|npm_[a-z0-9]{24,}|bearer\s+[a-z0-9._-]{8,}|(?:api[_-]?key|access[_-]?token|auth(?:orization)?|token|secret)\s*[=:]\s*\S+)/i,e=/(?:system\s+prompt|hidden\s+reasoning|chain[ -]of[ -]thought|\bpassword\b|private[_ -]?key|\.env\b)/i,r=/(?:^|[\s=("'`])(?:~\/|\/(?:Users|home|var|tmp|private|workspace)\/|[A-Za-z]:\\|\\\\)[^\s)"'`]*/i;function n(n){if("string"!=typeof n)return null;const s=n.replace(/\s+/g," ").trim();return!s||t.test(s)||e.test(s)||r.test(s)?null:s.slice(0,260)}function s(t){if(!t||"object"!=typeof t)return[];if("flow-review-receipt"===t.kind){const e=t.scope;return[`Flow ${t.outcome||"review"}: ${t.taskKey||""}`,...(t.checks||[]).map(t=>`Check: ${t}`),...(t.reasons||[]).map(t=>`Finding: ${t}`),...(e?.changed||[]).map(t=>`Edited: ${t}`),...(e?.unexpected||[]).map(t=>`Unexpected: ${t}`)]}if("assistant"===t.kind){const e=[];for(const r of t.blocks||[])if("text"===r?.type&&e.push(r.text),"tool_use"===r?.type){const t=String(r.name||"");if(/^(?:bash|exec_command|command_execution)$/i.test(t)){const t=r.input?.command??r.input?.cmd;t&&e.push(`Command: ${t}`)}}return e}return"you"===t.kind?[t.text]:"tool_result"===t.kind?[`Result: ${e=t.content,"string"==typeof e?e:Array.isArray(e)?e.map(t=>"string"==typeof t?t:t?.text||"").join(" "):""}`]:"result"===t.kind?[t.resultText]:[];var e}export function searchPastWork(o,i,{room:a,limit:c=8}={}){const l=function(n){const s=String(n||"").replace(/\s+/g," ").trim();if(s.length<2||s.length>160)throw new TypeError("past-work query must contain 2-160 characters");return t.test(s)||e.test(s)||r.test(s)?null:{text:s,lower:s.toLowerCase(),tokens:s.toLowerCase().split(/[^a-z0-9._/-]+/).filter(t=>t.length>1)}}(i);if(!l)return[];const m=Math.max(1,Math.min(8,Number(c)||8)),u=[];for(const t of(Array.isArray(o)?o:[]).slice(0,48)){const e=n(t?.name)||"Unnamed terminal",r=Array.isArray(t?.log)?t.log.slice(-800):[];for(const o of r)for(const r of s(o)){const s=n(r);if(!s)continue;const i=s.toLowerCase(),c=l.tokens.filter(t=>i.includes(t)).length;(i.includes(l.lower)||l.tokens.length&&c===l.tokens.length)&&u.push({room:String(a||"").trim()||"current",terminal:e,terminalId:String(t?.id||"").slice(0,80),ts:Number(o?.ts)||Number(t?.savedAt)||0,seq:Number.isFinite(Number(o?.seq))?Number(o.seq):null,excerpt:s,score:(i.includes(l.lower)?10:0)+c+(s.startsWith("Command:")?0:1)})}}return u.sort((t,e)=>e.score-t.score||e.ts-t.ts).slice(0,m).map(({score:t,...e})=>e)}export function formatPastWorkSearch(t,e){const r=n(String(e||"").trim());return t.length?t.map((t,e)=>{const r=t.ts?new Date(t.ts).toISOString().slice(0,10):"date unavailable",n=null==t.seq?"saved transcript":`event ${t.seq}`;return`${e+1}. ${t.excerpt}\n [Room ${t.room} · ${t.terminal} · ${r} · ${n}]`}).join("\n"):r?`No cited past work matched “${r}” in this room.`:"No cited past work matched that privacy-filtered query in this room."}
package/plan-meters.mjs CHANGED
@@ -1,144 +1 @@
1
- /* Plan / rate-limit meters for the /usage report.
2
- *
3
- * WHERE THE NUMBERS COME FROM. `GET https://api.anthropic.com/api/oauth/usage`
4
- * with the Claude Code OAuth access token — the SAME endpoint the CLI's own /usage
5
- * screen reads. So the percentages and, critically, the RESET TIMES are the
6
- * server's, not ours. We never infer a 5-hour block boundary from transcript
7
- * timestamps (the ccusage approach): an inferred reset is a number that looks
8
- * authoritative and is wrong, which is exactly the bug this module exists to avoid.
9
- *
10
- * READ-ONLY, AND DELIBERATELY SO. We read the access token and we never refresh it.
11
- * Refreshing rotates the refresh token; two processes rotating against one credential
12
- * store is how the bridge's login died before (memory: thinkpool_bridge_login_headless_recovery).
13
- * An expired token therefore degrades to "plan meters unavailable" rather than
14
- * quietly re-authenticating behind the user's back.
15
- *
16
- * DEGRADE HONESTLY. Every failure — no token, non-Claude provider, network error,
17
- * 401, shape change, a reset timestamp in the past — returns null, and the caller
18
- * prints "plan meters unavailable". A stale or invented meter is worse than no meter.
19
- */
20
-
21
- import { execFileSync } from 'node:child_process'
22
- import { readFileSync } from 'node:fs'
23
- import { homedir } from 'node:os'
24
- import { join } from 'node:path'
25
-
26
- const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage'
27
- const CACHE_MS = 45_000
28
- const FETCH_TIMEOUT_MS = 4_000
29
-
30
- // ── token ────────────────────────────────────────────────────────────────────
31
- // macOS keeps Claude Code's credentials in the login keychain; Linux writes
32
- // ~/.claude/.credentials.json. Same JSON shape either way: { claudeAiOauth: { accessToken } }.
33
- // Any failure here is a normal, expected state (BYOK lane, API-key auth, logged out).
34
- export function readOAuthToken (deps = {}) {
35
- const {
36
- platform = process.platform,
37
- env = process.env,
38
- exec = execFileSync,
39
- read = readFileSync,
40
- home = homedir()
41
- } = deps
42
- if (env.THINKPOOL_OAUTH_TOKEN) return env.THINKPOOL_OAUTH_TOKEN // test/override seam
43
- let raw = null
44
- try {
45
- if (platform === 'darwin') {
46
- raw = exec('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
47
- { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] })
48
- } else {
49
- raw = read(join(home, '.claude', '.credentials.json'), 'utf8')
50
- }
51
- const tok = JSON.parse(raw)?.claudeAiOauth?.accessToken
52
- return (typeof tok === 'string' && tok.startsWith('sk-ant-oat')) ? tok : null
53
- } catch { return null }
54
- }
55
-
56
- // ── fetch ────────────────────────────────────────────────────────────────────
57
- export async function fetchPlanUsage (token, fetchImpl = globalThis.fetch) {
58
- if (!token) return null
59
- const ac = new AbortController()
60
- const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS)
61
- try {
62
- const res = await fetchImpl(USAGE_URL, {
63
- method: 'GET',
64
- headers: { authorization: `Bearer ${token}`, 'anthropic-beta': 'oauth-2025-04-20' },
65
- signal: ac.signal
66
- })
67
- if (!res.ok) return null // 401 (expired) / 403 / 5xx → unavailable, never a guess
68
- return await res.json()
69
- } catch { return null } finally { clearTimeout(timer) }
70
- }
71
-
72
- // ── format (pure) ────────────────────────────────────────────────────────────
73
- const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
74
-
75
- // A reset renders in LOCAL time. Beyond ~20h out (the weekly meter) a bare HH:MM is
76
- // ambiguous, so it carries a weekday. Returns null for an unparseable timestamp OR one
77
- // already in the past — a past reset means the payload is stale, and printing "resets
78
- // 10:10" when 10:10 has been and gone is precisely the lie we refuse to tell.
79
- export function fmtReset (iso, now) {
80
- const t = new Date(iso).getTime()
81
- if (!Number.isFinite(t) || t <= now.getTime()) return null
82
- // ROUND to the nearest minute, don't floor. The endpoint stamps `resets_at` with
83
- // sub-second jitter around the true boundary — two calls seconds apart returned
84
- // 10:10:00.355Z and 10:09:59.873Z for the same reset. Flooring renders those as
85
- // 12:10 and 12:09, so the meter visibly flickers a minute back and forth on a
86
- // surface whose whole job is to be the trustworthy number. Observed 2026-07-10.
87
- const d = new Date(Math.round(t / 60000) * 60000)
88
- const hhmm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
89
- return (t - now.getTime()) > 20 * 3600 * 1000 ? `${DAYS[d.getDay()]} ${hhmm}` : hhmm
90
- }
91
-
92
- const fmtIn = (ms) => {
93
- const m = Math.max(0, Math.round(ms / 60000))
94
- return m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m` : `${m}m`
95
- }
96
-
97
- // Render one block ("5h" / "week") or null if it can't be stated truthfully.
98
- function block (label, node, now, withRelative) {
99
- const pct = node && typeof node.utilization === 'number' ? Math.round(node.utilization) : null
100
- if (pct == null) return null
101
- const at = fmtReset(node.resets_at, now)
102
- if (!at) return `${label}: ${pct}%` // percent is still true even if the reset isn't usable
103
- const rel = withRelative ? ` (${fmtIn(new Date(node.resets_at).getTime() - now.getTime())})` : ''
104
- return `${label}: ${pct}% · resets ${at}${rel}`
105
- }
106
-
107
- // json → "plan · 5h: 10% · resets 12:10 (1h04m) · week: 26% · resets Tue 11:00"
108
- // Returns null when nothing truthful can be said, so the caller prints the honest
109
- // "plan meters unavailable" instead of a half-line.
110
- export function formatPlanMeters (json, now = new Date()) {
111
- if (!json || typeof json !== 'object') return null
112
- const parts = [block('5h', json.five_hour, now, true), block('week', json.seven_day, now, false)]
113
- // A scoped weekly cap (e.g. a per-model ceiling) only matters while it's the binding
114
- // one — surface it, named, so a lane that's about to hit a model-specific wall sees it.
115
- const scoped = Array.isArray(json.limits)
116
- ? json.limits.find((l) => l && l.is_active && l.kind === 'weekly_scoped' && typeof l.percent === 'number')
117
- : null
118
- if (scoped) {
119
- const name = scoped.scope?.model?.display_name
120
- parts.push(`${name ? `week ${name}` : 'week scoped'}: ${Math.round(scoped.percent)}%`)
121
- }
122
- const live = parts.filter(Boolean)
123
- return live.length ? `plan · ${live.join(' · ')}` : null
124
- }
125
-
126
- // ── cached entry point ───────────────────────────────────────────────────────
127
- let cache = { at: 0, line: null }
128
-
129
- // The ONE line bridge.mjs appends under the token/cost line. Cached ~45s so a user
130
- // spamming /usage doesn't hammer the endpoint. Never throws.
131
- export async function planMeterLine (deps = {}) {
132
- const { now = () => Date.now(), clock = () => new Date(), token = readOAuthToken, fetchUsage = fetchPlanUsage } = deps
133
- const t = now()
134
- if (cache.line !== null && (t - cache.at) < CACHE_MS) return cache.line
135
- let line = 'plan meters unavailable'
136
- try {
137
- const json = await fetchUsage(token())
138
- line = formatPlanMeters(json, clock()) || 'plan meters unavailable'
139
- } catch { /* stays unavailable */ }
140
- cache = { at: t, line }
141
- return line
142
- }
143
-
144
- export function _resetCache () { cache = { at: 0, line: null } } // test seam
1
+ import{execFileSync as e}from"node:child_process";import{readFileSync as t}from"node:fs";import{homedir as n}from"node:os";import{join as r}from"node:path";export function readOAuthToken(a={}){const{platform:o=process.platform,env:i=process.env,exec:l=e,read:s=t,home:u=n()}=a;if(i.THINKPOOL_OAUTH_TOKEN)return i.THINKPOOL_OAUTH_TOKEN;let c=null;try{c="darwin"===o?l("security",["find-generic-password","-s","Claude Code-credentials","-w"],{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}):s(r(u,".claude",".credentials.json"),"utf8");const e=JSON.parse(c)?.claudeAiOauth?.accessToken;return"string"==typeof e&&e.startsWith("sk-ant-oat")?e:null}catch{return null}}export async function fetchPlanUsage(e,t=globalThis.fetch){if(!e)return null;const n=new AbortController,r=setTimeout(()=>n.abort(),4e3);try{const r=await t("https://api.anthropic.com/api/oauth/usage",{method:"GET",headers:{authorization:`Bearer ${e}`,"anthropic-beta":"oauth-2025-04-20"},signal:n.signal});return r.ok?await r.json():null}catch{return null}finally{clearTimeout(r)}}const a=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];export function fmtReset(e,t){const n=new Date(e).getTime();if(!Number.isFinite(n)||n<=t.getTime())return null;const r=new Date(6e4*Math.round(n/6e4)),o=`${String(r.getHours()).padStart(2,"0")}:${String(r.getMinutes()).padStart(2,"0")}`;return n-t.getTime()>72e6?`${a[r.getDay()]} ${o}`:o}function o(e,t,n,r){const a=t&&"number"==typeof t.utilization?Math.round(t.utilization):null;if(null==a)return null;const o=fmtReset(t.resets_at,n);return o?`${e}: ${a}% · resets ${o}${r?` (${(e=>{const t=Math.max(0,Math.round(e/6e4));return t>=60?`${Math.floor(t/60)}h${String(t%60).padStart(2,"0")}m`:`${t}m`})(new Date(t.resets_at).getTime()-n.getTime())})`:""}`:`${e}: ${a}%`}export function formatPlanMeters(e,t=new Date){if(!e||"object"!=typeof e)return null;const n=[o("5h",e.five_hour,t,!0),o("week",e.seven_day,t,!1)],r=Array.isArray(e.limits)?e.limits.find(e=>e&&e.is_active&&"weekly_scoped"===e.kind&&"number"==typeof e.percent):null;if(r){const e=r.scope?.model?.display_name;n.push(`${e?`week ${e}`:"week scoped"}: ${Math.round(r.percent)}%`)}const a=n.filter(Boolean);return a.length?`plan · ${a.join(" · ")}`:null}let i={at:0,line:null};export async function planMeterLine(e={}){const{now:t=()=>Date.now(),clock:n=()=>new Date,token:r=readOAuthToken,fetchUsage:a=fetchPlanUsage}=e,o=t();if(null!==i.line&&o-i.at<45e3)return i.line;let l="plan meters unavailable";try{l=formatPlanMeters(await a(r()),n())||"plan meters unavailable"}catch{}return i={at:o,line:l},l}export function _resetCache(){i={at:0,line:null}}