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/command-catalog.mjs
CHANGED
|
@@ -1,118 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// different surfaces: Claude mixes skills with TUI commands, Codex App Server
|
|
3
|
-
// publishes no slash catalog, and Hermes publishes ACP metadata. Keep the safe
|
|
4
|
-
// room controls here so all three runtimes share names, descriptions, hints,
|
|
5
|
-
// and routing while runtime-owned commands remain evidence-gated.
|
|
6
|
-
|
|
7
|
-
const command = (name, description, route, inputHint, runtimes = ['claude', 'codex', 'hermes']) => Object.freeze({
|
|
8
|
-
name,
|
|
9
|
-
description,
|
|
10
|
-
route,
|
|
11
|
-
...(inputHint ? { inputHint } : {}),
|
|
12
|
-
runtimes: Object.freeze(runtimes),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export const CODE_ROOM_COMMANDS = Object.freeze([
|
|
16
|
-
command('/side', 'investigate beside this terminal', 'side', 'task'),
|
|
17
|
-
command('/help', 'list commands available in this lane', 'control'),
|
|
18
|
-
command('/status', 'runtime, model, permissions, and busy state', 'control'),
|
|
19
|
-
command('/usage', 'session usage and provider limits', 'control'),
|
|
20
|
-
command('/context', 'current context-window usage', 'control'),
|
|
21
|
-
command('/diff', 'Git changes and branch status', 'control'),
|
|
22
|
-
command('/find', 'find relevant files in this repository', 'control', 'query'),
|
|
23
|
-
command('/compact', 'compact context', 'runtime'),
|
|
24
|
-
command('/clear', 'clear context · confirms', 'clear'),
|
|
25
|
-
command('/model', 'pick model — opens selector', 'model'),
|
|
26
|
-
command('/mode', 'set permission mode', 'mode', 'normal | auto-accept | plan | bypass'),
|
|
27
|
-
command('/effort', 'set reasoning effort', 'effort', 'low | medium | high | xhigh | max'),
|
|
28
|
-
command('/queue', 'run a prompt after the active turn', 'queue', 'prompt'),
|
|
29
|
-
command('/steer', 'guide the active turn', 'steer', 'prompt', ['codex', 'hermes']),
|
|
30
|
-
command('/credits', 'provider credit balance', 'credits', null, ['codex', 'hermes']),
|
|
31
|
-
command('/review', 'review uncommitted changes', 'runtime', null, ['codex']),
|
|
32
|
-
])
|
|
33
|
-
|
|
34
|
-
// Native catalogs are runtime evidence, never portable session metadata.
|
|
35
|
-
// Claude's adapter default-denies raw SDK commands and passes only proven room
|
|
36
|
-
// controls plus installed Skills. Hermes' isolated ACP bootstrap owns its safe
|
|
37
|
-
// native catalog. Codex App Server publishes no native slash catalog, so a
|
|
38
|
-
// restored/foreign list must not manufacture one. Compatibility aliases stay
|
|
39
|
-
// callable at their runtime boundary without remaining discoverable.
|
|
40
|
-
const NATIVE_COMMAND_POLICY = Object.freeze({
|
|
41
|
-
claude: Object.freeze({ accept: true, hidden: Object.freeze(new Set(['/flow'])) }),
|
|
42
|
-
codex: Object.freeze({ accept: false, hidden: Object.freeze(new Set(['/flow'])) }),
|
|
43
|
-
hermes: Object.freeze({ accept: true, hidden: Object.freeze(new Set(['/flow', '/reasoning'])) }),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const cleanRuntime = (runtime) => runtime === 'thinkpool' ? 'hermes' : runtime
|
|
47
|
-
const cleanName = (value) => {
|
|
48
|
-
const raw = typeof value === 'string' ? value : value?.name
|
|
49
|
-
const name = `/${String(raw || '').trim().replace(/^\/+/, '')}`
|
|
50
|
-
return /^\/[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(name) ? name : ''
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const normalizeNative = (value) => {
|
|
54
|
-
const name = cleanName(value)
|
|
55
|
-
if (!name) return null
|
|
56
|
-
if (typeof value === 'string') return { name, description: 'runtime command', route: 'runtime' }
|
|
57
|
-
const description = String(value?.description || '').trim() || 'runtime command'
|
|
58
|
-
const inputHint = String(value?.inputHint || value?.input_hint || value?.input?.hint || '').trim()
|
|
59
|
-
const route = String(value?.route || '').trim() || 'runtime'
|
|
60
|
-
return { name, description, route, ...(inputHint ? { inputHint } : {}) }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Merge native metadata into the shared catalog without allowing a runtime to
|
|
64
|
-
// redefine the routing of a bridge-owned control. Unknown native commands are
|
|
65
|
-
// retained only after the runtime-specific adapter has already allowlisted them.
|
|
66
|
-
export function commandCatalogForRuntime(runtime, nativeCommands = []) {
|
|
67
|
-
const id = cleanRuntime(runtime)
|
|
68
|
-
const nativePolicy = NATIVE_COMMAND_POLICY[id] || NATIVE_COMMAND_POLICY.codex
|
|
69
|
-
const byName = new Map()
|
|
70
|
-
for (const item of CODE_ROOM_COMMANDS) {
|
|
71
|
-
if (!item.runtimes.includes(id)) continue
|
|
72
|
-
byName.set(item.name, {
|
|
73
|
-
name: item.name,
|
|
74
|
-
description: item.description,
|
|
75
|
-
route: item.route,
|
|
76
|
-
...(item.inputHint ? { inputHint: item.inputHint } : {}),
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
for (const raw of (Array.isArray(nativeCommands) ? nativeCommands : [])) {
|
|
80
|
-
const item = normalizeNative(raw)
|
|
81
|
-
if (!item || !nativePolicy.accept || nativePolicy.hidden.has(item.name)) continue
|
|
82
|
-
const shared = byName.get(item.name)
|
|
83
|
-
const hermesNativeControl = id === 'hermes' && ['/help', '/status', '/context', '/credits'].includes(item.name)
|
|
84
|
-
byName.set(item.name, shared
|
|
85
|
-
? { ...item, ...shared, description: item.description === 'runtime command' ? shared.description : item.description, ...(item.inputHint ? { inputHint: item.inputHint } : {}), ...(hermesNativeControl ? { route: 'runtime' } : {}) }
|
|
86
|
-
: item)
|
|
87
|
-
}
|
|
88
|
-
return [...byName.values()]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const commandNeedsInput = (item) => !!String(item?.inputHint || '').trim()
|
|
92
|
-
export const commandRoute = (item) => String(item?.route || 'runtime')
|
|
93
|
-
|
|
94
|
-
export function commandHelpLine(commands) {
|
|
95
|
-
return (Array.isArray(commands) ? commands : [])
|
|
96
|
-
.map((item) => `${item.name}${item.inputHint ? ` <${item.inputHint}>` : ''}`)
|
|
97
|
-
.join(' ')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function normalizePermissionMode(value) {
|
|
101
|
-
const mode = String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
|
|
102
|
-
if (['normal', 'default', 'manual'].includes(mode)) return 'default'
|
|
103
|
-
if (['auto', 'autoaccept', 'autoacceptedits', 'acceptedits'].includes(mode)) return 'acceptEdits'
|
|
104
|
-
if (mode === 'plan') return 'plan'
|
|
105
|
-
if (['bypass', 'bypasspermissions', 'fullauto'].includes(mode)) return 'bypassPermissions'
|
|
106
|
-
return null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Command catalogs are session chrome, but their ACP system event is also the
|
|
110
|
-
// lifecycle-ready signal. Keep those concerns separate so a reconstructed
|
|
111
|
-
// identical catalog cannot swallow warm/recap/auto-resume work.
|
|
112
|
-
export function reconcileCommandCatalog({ current, incoming, onChanged, onLifecycle } = {}) {
|
|
113
|
-
const hasIncoming = Array.isArray(incoming) && incoming.length > 0
|
|
114
|
-
const duplicate = hasIncoming && JSON.stringify(current || []) === JSON.stringify(incoming)
|
|
115
|
-
if (hasIncoming && !duplicate) onChanged?.(incoming)
|
|
116
|
-
onLifecycle?.({ duplicate, hasIncoming })
|
|
117
|
-
return duplicate
|
|
118
|
-
}
|
|
1
|
+
const e=(e,t,n,i,r=["claude","codex","hermes"])=>Object.freeze({name:e,description:t,route:n,...i?{inputHint:i}:{},runtimes:Object.freeze(r)});export const CODE_ROOM_COMMANDS=Object.freeze([e("/side","investigate beside this terminal","side","task"),e("/help","list commands available in this lane","control"),e("/status","runtime, model, permissions, and busy state","control"),e("/usage","session usage and provider limits","control"),e("/context","current context-window usage","control"),e("/diff","Git changes and branch status","control"),e("/find","find relevant files in this repository","control","query"),e("/compact","compact context","runtime"),e("/clear","clear context · confirms","clear"),e("/model","pick model — opens selector","model"),e("/mode","set permission mode","mode","normal | auto-accept | plan | bypass"),e("/effort","set reasoning effort","effort","low | medium | high | xhigh | max"),e("/queue","run a prompt after the active turn","queue","prompt"),e("/steer","guide the active turn","steer","prompt",["codex","hermes"]),e("/credits","provider credit balance","credits",null,["codex","hermes"]),e("/review","review uncommitted changes","runtime",null,["codex"])]);const t=Object.freeze({claude:Object.freeze({accept:!0,hidden:Object.freeze(new Set(["/flow"]))}),codex:Object.freeze({accept:!1,hidden:Object.freeze(new Set(["/flow"]))}),hermes:Object.freeze({accept:!0,hidden:Object.freeze(new Set(["/flow","/reasoning"]))})}),n=e=>{const t=(e=>{const t=`/${String(("string"==typeof e?e:e?.name)||"").trim().replace(/^\/+/,"")}`;return/^\/[A-Za-z0-9][A-Za-z0-9:_-]*$/.test(t)?t:""})(e);if(!t)return null;if("string"==typeof e)return{name:t,description:"runtime command",route:"runtime"};const n=String(e?.description||"").trim()||"runtime command",i=String(e?.inputHint||e?.input_hint||e?.input?.hint||"").trim();return{name:t,description:n,route:String(e?.route||"").trim()||"runtime",...i?{inputHint:i}:{}}};export function commandCatalogForRuntime(e,i=[]){const r=(e=>"thinkpool"===e?"hermes":e)(e),o=t[r]||t.codex,c=new Map;for(const e of CODE_ROOM_COMMANDS)e.runtimes.includes(r)&&c.set(e.name,{name:e.name,description:e.description,route:e.route,...e.inputHint?{inputHint:e.inputHint}:{}});for(const e of Array.isArray(i)?i:[]){const t=n(e);if(!t||!o.accept||o.hidden.has(t.name))continue;const i=c.get(t.name),s="hermes"===r&&["/help","/status","/context","/credits"].includes(t.name);c.set(t.name,i?{...t,...i,description:"runtime command"===t.description?i.description:t.description,...t.inputHint?{inputHint:t.inputHint}:{},...s?{route:"runtime"}:{}}:t)}return[...c.values()]}export const commandNeedsInput=e=>!!String(e?.inputHint||"").trim();export const commandRoute=e=>String(e?.route||"runtime");export function commandHelpLine(e){return(Array.isArray(e)?e:[]).map(e=>`${e.name}${e.inputHint?` <${e.inputHint}>`:""}`).join(" ")}export function normalizePermissionMode(e){const t=String(e||"").trim().toLowerCase().replace(/[\s_-]+/g,"");return["normal","default","manual"].includes(t)?"default":["auto","autoaccept","autoacceptedits","acceptedits"].includes(t)?"acceptEdits":"plan"===t?"plan":["bypass","bypasspermissions","fullauto"].includes(t)?"bypassPermissions":null}export function reconcileCommandCatalog({current:e,incoming:t,onChanged:n,onLifecycle:i}={}){const r=Array.isArray(t)&&t.length>0,o=r&&JSON.stringify(e||[])===JSON.stringify(t);return r&&!o&&n?.(t),i?.({duplicate:o,hasIncoming:r}),o}
|
package/command-guidance.mjs
CHANGED
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const PAIR_CLI = 'npx thinkpool-pair@latest'
|
|
3
|
-
|
|
4
|
-
export function pairCli(...args) {
|
|
5
|
-
return [PAIR_CLI, ...args.filter((arg) => arg !== undefined && arg !== null && String(arg).length > 0)]
|
|
6
|
-
.map(String)
|
|
7
|
-
.join(' ')
|
|
8
|
-
}
|
|
1
|
+
export const PAIR_CLI="npx thinkpool-pair@latest";export function pairCli(...t){return[PAIR_CLI,...t.filter(t=>null!=t&&String(t).length>0)].map(String).join(" ")}
|
package/context-contract.mjs
CHANGED
|
@@ -1,95 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// to a native runtime. This is deliberately not a transcript store.
|
|
3
|
-
|
|
4
|
-
import { createHash } from 'node:crypto'
|
|
5
|
-
|
|
6
|
-
export const CONTEXT_CONTRACT_VERSION = 1
|
|
7
|
-
export const CONTEXT_OMISSIONS = Object.freeze(['raw_pty', 'credentials', 'absolute_paths', 'unapproved_cross_room'])
|
|
8
|
-
export const CONTEXT_SOURCE_POLICY = Object.freeze([
|
|
9
|
-
Object.freeze({ kind: 'current_user_turn', maxBytes: 8192 }),
|
|
10
|
-
Object.freeze({ kind: 'room_now_delta', maxBytes: 4096 }),
|
|
11
|
-
Object.freeze({ kind: 'flow_slice_digest', maxBytes: 8192 }),
|
|
12
|
-
Object.freeze({ kind: 'approved_human_response', maxBytes: 4096 }),
|
|
13
|
-
])
|
|
14
|
-
|
|
15
|
-
const BY_KIND = new Map(CONTEXT_SOURCE_POLICY.map((source) => [source.kind, source]))
|
|
16
|
-
const HOST_PATH = /(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i
|
|
17
|
-
const SECRET_KEY = /(secret|token|password|authorization|api.?key|private.?key)/i
|
|
18
|
-
const SECRET_VALUE = /(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i
|
|
19
|
-
const PROHIBITED = /(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i
|
|
20
|
-
const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
|
|
21
|
-
const bytes = (value) => {
|
|
22
|
-
try {
|
|
23
|
-
const serialized = typeof value === 'string' ? value : JSON.stringify(value)
|
|
24
|
-
return new TextEncoder().encode(serialized ?? '').length
|
|
25
|
-
} catch {
|
|
26
|
-
return Infinity
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const stable = (value) => Array.isArray(value)
|
|
30
|
-
? value.map(stable)
|
|
31
|
-
: value && typeof value === 'object'
|
|
32
|
-
? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]))
|
|
33
|
-
: value
|
|
34
|
-
|
|
35
|
-
export function isSafeContextValue(value, maxBytes = 8192) {
|
|
36
|
-
if (value == null || bytes(value) > maxBytes) return false
|
|
37
|
-
const visit = (node) => {
|
|
38
|
-
if (typeof node === 'string') return node.length <= 2048 && !HOST_PATH.test(node) && !SECRET_VALUE.test(node) && !PROHIBITED.test(node)
|
|
39
|
-
if (node == null || typeof node === 'number' || typeof node === 'boolean') return true
|
|
40
|
-
if (Array.isArray(node)) return node.length <= 64 && node.every(visit)
|
|
41
|
-
if (typeof node !== 'object') return false
|
|
42
|
-
return Object.entries(node).every(([key, child]) => !SECRET_KEY.test(key) && visit(child))
|
|
43
|
-
}
|
|
44
|
-
return visit(value)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function safeRef(ref) {
|
|
48
|
-
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) return null
|
|
49
|
-
if (!SAFE_REF.test(String(ref.type || '')) || !SAFE_REF.test(String(ref.id || ''))) return null
|
|
50
|
-
return { type: String(ref.type), id: String(ref.id) }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// The visible room receives only lane identity/status aggregates, never terminal
|
|
54
|
-
// snippets or host worktree paths. This intentionally trades detail for privacy.
|
|
55
|
-
export function projectRoomContext(roomNow) {
|
|
56
|
-
const lines = String(roomNow || '').split('\n')
|
|
57
|
-
const lanes = []
|
|
58
|
-
let worktrees = 0
|
|
59
|
-
for (const line of lines) {
|
|
60
|
-
const lane = line.match(/^\s*-\s*([A-Za-z0-9._:@-]{4,160})\s+·\s+"([^"\n]{1,160})"\s+·\s+[^·\n]+\s+·\s+([^·\n]+)/)
|
|
61
|
-
if (lane && isSafeContextValue(lane[2], 512) && isSafeContextValue(lane[3], 128)) {
|
|
62
|
-
lanes.push({ ref: lane[1], title: lane[2].trim(), state: lane[3].trim() })
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
if (/^\s*-\s*\S+\s+[0-9a-f]{7,40}\s+\[/.test(line)) worktrees++
|
|
66
|
-
}
|
|
67
|
-
const projection = { lanes: lanes.slice(0, 32), worktreeCount: Math.min(worktrees, 999) }
|
|
68
|
-
return isSafeContextValue(projection, 4096) ? projection : { lanes: [], worktreeCount: 0 }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function formatRoomContextProjection(roomNow) {
|
|
72
|
-
const projection = projectRoomContext(roomNow)
|
|
73
|
-
if (!projection.lanes.length && !projection.worktreeCount) return ''
|
|
74
|
-
const laneText = projection.lanes.map((lane) => `${lane.ref} · ${lane.title} · ${lane.state}`).join('\n')
|
|
75
|
-
return ['ROOM NOW (safe projection):', laneText, projection.worktreeCount ? `Active git worktrees: ${projection.worktreeCount}` : ''].filter(Boolean).join('\n')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function buildContextManifest({ promptBundle = {}, sources = [] } = {}) {
|
|
79
|
-
const accepted = new Map()
|
|
80
|
-
for (const candidate of Array.isArray(sources) ? sources : []) {
|
|
81
|
-
const policy = BY_KIND.get(candidate?.kind)
|
|
82
|
-
if (!policy || accepted.has(policy.kind)) continue
|
|
83
|
-
const ref = candidate.ref == null ? null : safeRef(candidate.ref)
|
|
84
|
-
if (candidate.ref != null && !ref) continue
|
|
85
|
-
if (candidate.value != null && !isSafeContextValue(candidate.value, policy.maxBytes)) continue
|
|
86
|
-
accepted.set(policy.kind, ref ? { kind: policy.kind, ref, maxBytes: policy.maxBytes } : { kind: policy.kind, maxBytes: policy.maxBytes })
|
|
87
|
-
}
|
|
88
|
-
const manifest = {
|
|
89
|
-
version: CONTEXT_CONTRACT_VERSION,
|
|
90
|
-
promptBundle: { version: Number(promptBundle?.version) || 1, hash: /^[a-f0-9]{64}$/i.test(String(promptBundle?.hash || '')) ? String(promptBundle.hash).toLowerCase() : null },
|
|
91
|
-
sources: CONTEXT_SOURCE_POLICY.map((policy) => accepted.get(policy.kind)).filter(Boolean),
|
|
92
|
-
omissions: [...CONTEXT_OMISSIONS],
|
|
93
|
-
}
|
|
94
|
-
return Object.freeze({ ...manifest, hash: createHash('sha256').update(JSON.stringify(stable(manifest))).digest('hex') })
|
|
95
|
-
}
|
|
1
|
+
import{createHash as e}from"node:crypto";export const CONTEXT_CONTRACT_VERSION=1;export const CONTEXT_OMISSIONS=Object.freeze(["raw_pty","credentials","absolute_paths","unapproved_cross_room"]);export const CONTEXT_SOURCE_POLICY=Object.freeze([Object.freeze({kind:"current_user_turn",maxBytes:8192}),Object.freeze({kind:"room_now_delta",maxBytes:4096}),Object.freeze({kind:"flow_slice_digest",maxBytes:8192}),Object.freeze({kind:"approved_human_response",maxBytes:4096})]);const t=new Map(CONTEXT_SOURCE_POLICY.map(e=>[e.kind,e])),n=/(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\|(?:^|[\\/])\.\.(?:[\\/]|$))/i,r=/(secret|token|password|authorization|api.?key|private.?key)/i,o=/(?:sk-[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|xox[baprs]-[a-z0-9-]{10,}|AKIA[0-9A-Z]{16}|sbp_[a-z0-9]{20,}|eyJ[a-z0-9_-]{16,}\.[a-z0-9_-]{16,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/i,s=/(?:raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/i,a=/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/,i=e=>Array.isArray(e)?e.map(i):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,i(e[t])])):e;export function isSafeContextValue(e,t=8192){if(null==e||(e=>{try{const t="string"==typeof e?e:JSON.stringify(e);return(new TextEncoder).encode(t??"").length}catch{return 1/0}})(e)>t)return!1;const a=e=>"string"==typeof e?e.length<=2048&&!n.test(e)&&!o.test(e)&&!s.test(e):null==e||"number"==typeof e||"boolean"==typeof e||(Array.isArray(e)?e.length<=64&&e.every(a):"object"==typeof e&&Object.entries(e).every(([e,t])=>!r.test(e)&&a(t)));return a(e)}function p(e){return!e||"object"!=typeof e||Array.isArray(e)?null:a.test(String(e.type||""))&&a.test(String(e.id||""))?{type:String(e.type),id:String(e.id)}:null}export function projectRoomContext(e){const t=String(e||"").split("\n"),n=[];let r=0;for(const e of t){const t=e.match(/^\s*-\s*([A-Za-z0-9._:@-]{4,160})\s+·\s+"([^"\n]{1,160})"\s+·\s+[^·\n]+\s+·\s+([^·\n]+)/);t&&isSafeContextValue(t[2],512)&&isSafeContextValue(t[3],128)?n.push({ref:t[1],title:t[2].trim(),state:t[3].trim()}):/^\s*-\s*\S+\s+[0-9a-f]{7,40}\s+\[/.test(e)&&r++}const o={lanes:n.slice(0,32),worktreeCount:Math.min(r,999)};return isSafeContextValue(o,4096)?o:{lanes:[],worktreeCount:0}}export function formatRoomContextProjection(e){const t=projectRoomContext(e);return t.lanes.length||t.worktreeCount?["ROOM NOW (safe projection):",t.lanes.map(e=>`${e.ref} · ${e.title} · ${e.state}`).join("\n"),t.worktreeCount?`Active git worktrees: ${t.worktreeCount}`:""].filter(Boolean).join("\n"):""}export function buildContextManifest({promptBundle:n={},sources:r=[]}={}){const o=new Map;for(const e of Array.isArray(r)?r:[]){const n=t.get(e?.kind);if(!n||o.has(n.kind))continue;const r=null==e.ref?null:p(e.ref);(null==e.ref||r)&&(null==e.value||isSafeContextValue(e.value,n.maxBytes))&&o.set(n.kind,r?{kind:n.kind,ref:r,maxBytes:n.maxBytes}:{kind:n.kind,maxBytes:n.maxBytes})}const s={version:1,promptBundle:{version:Number(n?.version)||1,hash:/^[a-f0-9]{64}$/i.test(String(n?.hash||""))?String(n.hash).toLowerCase():null},sources:CONTEXT_SOURCE_POLICY.map(e=>o.get(e.kind)).filter(Boolean),omissions:[...CONTEXT_OMISSIONS]};return Object.freeze({...s,hash:e("sha256").update(JSON.stringify(i(s))).digest("hex")})}
|
package/context-windows.mjs
CHANGED
|
@@ -1,107 +1 @@
|
|
|
1
|
-
|
|
2
|
-
context-windows — correct the /code context meter for non-Claude BYOK models.
|
|
3
|
-
|
|
4
|
-
The Claude Agent SDK computes its context-usage meter
|
|
5
|
-
(getContextUsage → { totalTokens, maxTokens, percentage }) from the model
|
|
6
|
-
STRING it believes it is talking to. When a user bridges a NON-Claude model
|
|
7
|
-
in over an Anthropic-compatible endpoint (ANTHROPIC_BASE_URL — GLM, DeepSeek,
|
|
8
|
-
Kimi, Qwen, … via OpenRouter or a provider's own compat endpoint), the SDK's
|
|
9
|
-
maxTokens is for the WRONG model, so the meter fills at the wrong rate and can
|
|
10
|
-
shoot past 100% (Max, 2026-07-04: GLM-4.6 "runs up to 100 and goes past pretty
|
|
11
|
-
fast").
|
|
12
|
-
|
|
13
|
-
This is a DISPLAY-ONLY correction applied on the bridge's EMIT path: we
|
|
14
|
-
override the emitted { max, pct, over } using the real window for the reported
|
|
15
|
-
model id. The SDK's own context management is untouched — no behavior change,
|
|
16
|
-
no racy control request (H42-safe by construction).
|
|
17
|
-
|
|
18
|
-
Numbers are the provider-HEADLINED context windows (verified per row below).
|
|
19
|
-
The meter is inherently approximate, so we match what each provider's own docs
|
|
20
|
-
advertise rather than power-of-two token counts.
|
|
21
|
-
───────────────────────────────────────────────────────────── */
|
|
22
|
-
|
|
23
|
-
// id substring (case-insensitive) → context window in tokens.
|
|
24
|
-
// Ordered MOST-SPECIFIC first (glm-4.6 before glm-4.5) — first hit wins.
|
|
25
|
-
// Sources verified 2026-07-04:
|
|
26
|
-
// glm-4.6 200K — https://openrouter.ai/z-ai/glm-4.6 · https://docs.z.ai
|
|
27
|
-
// glm-4.5 128K — https://github.com/zai-org/GLM-4.5 (README)
|
|
28
|
-
// deepseek v3.x 128K — https://api-docs.deepseek.com (deepseek-chat/-reasoner, V3.1)
|
|
29
|
-
// kimi-k2 256K — https://huggingface.co/moonshotai/Kimi-K2-Instruct-0905
|
|
30
|
-
// qwen3-coder 256K — https://huggingface.co/Qwen/Qwen3-Coder-Next (native)
|
|
31
|
-
const KNOWN_WINDOWS = [
|
|
32
|
-
{ match: 'glm-4.6', max: 200_000 },
|
|
33
|
-
{ match: 'glm-4.5', max: 128_000 },
|
|
34
|
-
{ match: 'deepseek', max: 128_000 },
|
|
35
|
-
{ match: 'kimi-k2', max: 256_000 },
|
|
36
|
-
{ match: 'qwen3-coder', max: 256_000 },
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
// A Claude model id — the ONLY ids whose SDK-reported window we trust verbatim.
|
|
40
|
-
// Everything else is a candidate for correction. (Kept broad on purpose: any
|
|
41
|
-
// real Claude id matches, so the 1M/200k Claude paths never regress.)
|
|
42
|
-
export function isClaudeModel(id) {
|
|
43
|
-
return /claude|anthropic|opus|sonnet|haiku/i.test(String(id || ''))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Look up a corrected window (tokens) for a model id. Returns null if unknown.
|
|
47
|
-
export function windowForModel(modelId) {
|
|
48
|
-
const id = String(modelId || '').toLowerCase()
|
|
49
|
-
if (!id) return null
|
|
50
|
-
for (const w of KNOWN_WINDOWS) {
|
|
51
|
-
if (id.includes(w.match)) return w.max
|
|
52
|
-
}
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Parse TP_CONTEXT_MAX (a positive integer token count) from the environment.
|
|
57
|
-
// Per-bridge override for models not in the map. null if unset/non-numeric/<=0.
|
|
58
|
-
export function envContextMax(env = process.env) {
|
|
59
|
-
const raw = env?.TP_CONTEXT_MAX
|
|
60
|
-
if (raw == null || raw === '') return null
|
|
61
|
-
const n = Number(raw)
|
|
62
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Correct a raw ctx object from the SDK's getContextUsage().
|
|
66
|
-
//
|
|
67
|
-
// raw: { used, max, pct, model } (any field may be missing)
|
|
68
|
-
// env: process.env-like (for the TP_CONTEXT_MAX override)
|
|
69
|
-
//
|
|
70
|
-
// Returns a NEW ctx object. Precedence:
|
|
71
|
-
// 1. TP_CONTEXT_MAX override — wins for ANY model (incl. ids not in the map).
|
|
72
|
-
// 2. A known non-Claude window from the map.
|
|
73
|
-
// 3. Otherwise passthrough — Claude models and unknown ids keep the SDK's max.
|
|
74
|
-
// When a correction applies, max is replaced, pct is recomputed and clamped to
|
|
75
|
-
// 0..100, and `over: true` is set when used exceeds the corrected window.
|
|
76
|
-
// In every case pct is clamped so a lying >100% never reaches the UI.
|
|
77
|
-
export function correctContext(raw, env = process.env) {
|
|
78
|
-
if (!raw || typeof raw !== 'object') return raw
|
|
79
|
-
const used = Number(raw.used)
|
|
80
|
-
const override = envContextMax(env)
|
|
81
|
-
// The map fires only for non-Claude ids (point 4: Claude untouched); the
|
|
82
|
-
// env override is explicit and deliberate, so it applies even to Claude.
|
|
83
|
-
const mapped = override == null && !isClaudeModel(raw.model)
|
|
84
|
-
? windowForModel(raw.model)
|
|
85
|
-
: null
|
|
86
|
-
const correctedMax = override ?? mapped
|
|
87
|
-
|
|
88
|
-
if (correctedMax == null) {
|
|
89
|
-
// No correction available. For a NON-Claude model the SDK's max is computed for the
|
|
90
|
-
// wrong model id (it believes it's talking to Claude over ANTHROPIC_BASE_URL) → a %
|
|
91
|
-
// off that wrong max misleads (the GLM-ran-past-100 bug, 2026-07-04). Rather than ship
|
|
92
|
-
// a wrong number, SUPPRESS the meter: return null so the mode row + Session info show
|
|
93
|
-
// nothing / "—" instead. We only display ctx when we know the true window — a Claude
|
|
94
|
-
// id (SDK max trusted), a known-map model (corrected above), or TP_CONTEXT_MAX (override).
|
|
95
|
-
if (!isClaudeModel(raw.model)) return null
|
|
96
|
-
const pct = Number(raw.pct)
|
|
97
|
-
if (Number.isFinite(pct) && pct > 100) return { ...raw, pct: 100, over: true }
|
|
98
|
-
return raw
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const max = correctedMax
|
|
102
|
-
const pct = Number.isFinite(used) && max > 0
|
|
103
|
-
? Math.min(100, Math.max(0, Math.round((used / max) * 100)))
|
|
104
|
-
: raw.pct
|
|
105
|
-
const over = Number.isFinite(used) && used > max
|
|
106
|
-
return { ...raw, max, pct, over }
|
|
107
|
-
}
|
|
1
|
+
const e=[{match:"glm-4.6",max:2e5},{match:"glm-4.5",max:128e3},{match:"deepseek",max:128e3},{match:"kimi-k2",max:256e3},{match:"qwen3-coder",max:256e3}];export function isClaudeModel(e){return/claude|anthropic|opus|sonnet|haiku/i.test(String(e||""))}export function windowForModel(t){const n=String(t||"").toLowerCase();if(!n)return null;for(const t of e)if(n.includes(t.match))return t.max;return null}export function envContextMax(e=process.env){const t=e?.TP_CONTEXT_MAX;if(null==t||""===t)return null;const n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):null}export function correctContext(e,t=process.env){if(!e||"object"!=typeof e)return e;const n=Number(e.used),o=envContextMax(t),r=null!=o||isClaudeModel(e.model)?null:windowForModel(e.model),u=o??r;if(null==u){if(!isClaudeModel(e.model))return null;const t=Number(e.pct);return Number.isFinite(t)&&t>100?{...e,pct:100,over:!0}:e}const i=u,l=Number.isFinite(n)&&i>0?Math.min(100,Math.max(0,Math.round(n/i*100))):e.pct,c=Number.isFinite(n)&&n>i;return{...e,max:i,pct:l,over:c}}
|