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.
- package/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/model-prices.mjs
CHANGED
|
@@ -1,113 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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")}}
|
package/past-work-search.mjs
CHANGED
|
@@ -1,105 +1 @@
|
|
|
1
|
-
const
|
|
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
|
-
|
|
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}}
|