golem-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +11 -6
- package/docs/agents.md +64 -0
- package/docs/app-backend.md +259 -0
- package/docs/architecture.md +93 -0
- package/docs/builder.md +15 -0
- package/docs/knowledge.md +35 -0
- package/docs/local-cli.md +31 -15
- package/docs/source-development.md +31 -0
- package/index.html +9 -0
- package/package.json +24 -5
- package/src/backend/accounts.ts +287 -0
- package/src/backend/app.ts +269 -0
- package/src/backend/files.ts +68 -0
- package/src/backend/http.ts +276 -0
- package/src/backend/index.ts +10 -0
- package/src/backend/jobs.ts +302 -0
- package/src/backend/jsonl.ts +87 -0
- package/src/backend/knowledge.ts +264 -0
- package/src/backend/model.ts +129 -0
- package/src/backend/rules.ts +53 -0
- package/src/backend/sqlite.ts +73 -0
- package/src/backend/views.ts +216 -0
- package/src/brain.ts +94 -0
- package/src/browser/adapters.ts +229 -53
- package/src/browser/ansi.ts +104 -0
- package/src/browser/app.d.ts +5 -2
- package/src/browser/app.tsx +167 -39
- package/src/browser/groups.tsx +29 -0
- package/src/browser/main.tsx +1 -0
- package/src/browser/panekeys.ts +34 -0
- package/src/browser/sources.tsx +113 -0
- package/src/browser/styles.css +36 -0
- package/src/browser/terminal.tsx +89 -0
- package/src/browser-build.ts +20 -7
- package/src/chat.ts +74 -0
- package/src/cli.ts +91 -17
- package/src/client.ts +205 -0
- package/src/config.ts +169 -0
- package/src/dev-server.ts +339 -38
- package/src/entry.mjs +19 -0
- package/src/eslint.mjs +55 -0
- package/src/operations.ts +169 -0
- package/src/runtime/assistant.ts +141 -0
- package/src/runtime/discovery.ts +13 -7
- package/src/runtime/harness/agent-status.js +388 -0
- package/src/runtime/harness/claude-tmux.js +573 -0
- package/src/runtime/harness/codex-notify.js +95 -0
- package/src/runtime/harness/codex-tmux.js +292 -0
- package/src/runtime/harness/fake.js +430 -0
- package/src/runtime/harness/package.json +1 -0
- package/src/runtime/harness/port.js +208 -0
- package/src/runtime/harness/tmux-session.js +556 -0
- package/src/runtime/harness/tmux.js +285 -0
- package/src/runtime/harness/turnend-hook.js +105 -0
- package/src/runtime/session.ts +171 -34
- package/src/runtime/tmux.ts +173 -0
- package/src/runtime/tool-names.ts +19 -0
- package/src/source-mode.ts +56 -0
- package/vite.config.ts +2 -4
- package/src/runtime/codex.ts +0 -119
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Vendored from bridge-commander 5bf87e4b harness/agent-status.js (zero-dependency). Local patches are marked 'golem:'.
|
|
2
|
+
'use strict';
|
|
3
|
+
// agent-status — session status (model, context usage, rate limits) read from
|
|
4
|
+
// the files the harnesses ALREADY write; no statusline dependency, nothing new
|
|
5
|
+
// is persisted. Backs the OPTIONAL `status`/`runCommand` capability verbs
|
|
6
|
+
// (port.js): claude-tmux reads the session transcript under ~/.claude/projects,
|
|
7
|
+
// codex-tmux reads the rollout log under ~/.codex/sessions. Everything here is
|
|
8
|
+
// best-effort by contract: a missing/unreadable/foreign-shaped file returns
|
|
9
|
+
// null, never a throw.
|
|
10
|
+
//
|
|
11
|
+
// Status shape (the port's `status(ref)` return value):
|
|
12
|
+
// { model, contextUsed, contextWindow, effort?, rateLimits? }
|
|
13
|
+
// effort (reasoning level, e.g. "high") — from the claude sidecar or the codex
|
|
14
|
+
// turn_context; absent when unknown (e.g. the claude transcript fallback).
|
|
15
|
+
// rateLimits (codex only — claude does not persist them): { primary?,
|
|
16
|
+
// secondary? } each { usedPercent, windowMinutes, resetsAt (epoch secs) }.
|
|
17
|
+
//
|
|
18
|
+
// Files can grow to tens of MB, so reads are TAIL reads (last N bytes, from
|
|
19
|
+
// the end); the interesting lines — claude's last assistant message, codex's
|
|
20
|
+
// last token_count event — always sit near the bottom. claude alone gets one
|
|
21
|
+
// escalation step: a huge tool-result line can push the last assistant line
|
|
22
|
+
// past a small tail window.
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const TAIL_BYTES = 256 * 1024;
|
|
29
|
+
|
|
30
|
+
// tailRead — the last maxBytes of a file, decoded; null when missing/unreadable.
|
|
31
|
+
// When the file is smaller than maxBytes the whole file comes back.
|
|
32
|
+
function tailRead(file, maxBytes) {
|
|
33
|
+
let fd;
|
|
34
|
+
try {
|
|
35
|
+
fd = fs.openSync(file, 'r');
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const size = fs.fstatSync(fd).size;
|
|
41
|
+
const want = Math.min(size, maxBytes);
|
|
42
|
+
const buf = Buffer.alloc(want);
|
|
43
|
+
fs.readSync(fd, buf, 0, want, size - want);
|
|
44
|
+
return buf.toString('utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
} finally {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------- claude ----------
|
|
53
|
+
// Transcript path: ~/.claude/projects/<slug(cwd)>/<sessionId>.jsonl — the slug
|
|
54
|
+
// replaces every non-alphanumeric cwd character with '-' (verified against
|
|
55
|
+
// real transcript dirs: /home/ai/.treehouse/x → -home-ai--treehouse-x).
|
|
56
|
+
function claudeProjectSlug(cwd) {
|
|
57
|
+
return String(cwd).replace(/[^A-Za-z0-9]/g, '-');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Context window per model — matched by substring so versioned ids
|
|
61
|
+
// (claude-fable-5, claude-opus-4-8, …) hit without an exhaustive list.
|
|
62
|
+
// Extend by adding a pair; unknown models get the conservative default.
|
|
63
|
+
const CLAUDE_CONTEXT_WINDOWS = [
|
|
64
|
+
['fable', 1000000],
|
|
65
|
+
['opus', 200000],
|
|
66
|
+
['sonnet', 200000],
|
|
67
|
+
['haiku', 200000],
|
|
68
|
+
];
|
|
69
|
+
const CLAUDE_DEFAULT_WINDOW = 200000;
|
|
70
|
+
function claudeContextWindow(model) {
|
|
71
|
+
const m = String(model || '').toLowerCase();
|
|
72
|
+
for (const [needle, window] of CLAUDE_CONTEXT_WINDOWS) {
|
|
73
|
+
if (m.includes(needle)) return window;
|
|
74
|
+
}
|
|
75
|
+
return CLAUDE_DEFAULT_WINDOW;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------- claude statusline sidecar ----------
|
|
79
|
+
// harness/statusline.js (the workspace's Claude Code `statusLine` command) tees
|
|
80
|
+
// each stdin payload to <workspace>/.bridge-commander/statusline/<session_id>.json
|
|
81
|
+
// — the ONLY source of the REAL context window (context_window_size), the
|
|
82
|
+
// account rate limits, and the model display name. Lieutenants run with cwd =
|
|
83
|
+
// workspace root, so the sidecar exists for them; workers in worktree cwds have
|
|
84
|
+
// none and fall through to the transcript+map path below (accepted).
|
|
85
|
+
const STATE_DIR_NAME = '.bridge-commander';
|
|
86
|
+
function findBridgeWorkspace(startDir) {
|
|
87
|
+
if (!startDir) return null;
|
|
88
|
+
let dir = path.resolve(startDir);
|
|
89
|
+
for (;;) {
|
|
90
|
+
try {
|
|
91
|
+
if (fs.statSync(path.join(dir, STATE_DIR_NAME)).isDirectory()) return dir;
|
|
92
|
+
} catch { /* keep walking up */ }
|
|
93
|
+
const parent = path.dirname(dir);
|
|
94
|
+
if (parent === dir) return null;
|
|
95
|
+
dir = parent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Map the statusline payload's rate_limits (five_hour/seven_day, each
|
|
100
|
+
// {used_percentage, resets_at}) onto the shared status rateLimits shape
|
|
101
|
+
// (primary/secondary, each {usedPercent, windowMinutes, resetsAt epoch secs}),
|
|
102
|
+
// so formatStatus labels them 5h / 1w just like codex.
|
|
103
|
+
function claudeSidecarRateLimits(rl) {
|
|
104
|
+
if (!rl || typeof rl !== 'object') return null;
|
|
105
|
+
const toEpochSecs = (v) => {
|
|
106
|
+
if (v == null) return undefined;
|
|
107
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v > 1e11 ? Math.floor(v / 1000) : Math.floor(v);
|
|
108
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
109
|
+
const n = Number(v);
|
|
110
|
+
if (Number.isFinite(n)) return n > 1e11 ? Math.floor(n / 1000) : Math.floor(n);
|
|
111
|
+
const p = Date.parse(v);
|
|
112
|
+
if (!Number.isNaN(p)) return Math.floor(p / 1000);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
const pick = (w, windowMinutes) => {
|
|
117
|
+
if (!w || typeof w !== 'object' || w.used_percentage == null) return undefined;
|
|
118
|
+
const out = { usedPercent: Number(w.used_percentage), windowMinutes };
|
|
119
|
+
const r = toEpochSecs(w.resets_at);
|
|
120
|
+
if (r !== undefined) out.resetsAt = r;
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
const out = {};
|
|
124
|
+
const primary = pick(rl.five_hour, 300);
|
|
125
|
+
const secondary = pick(rl.seven_day, 10080);
|
|
126
|
+
if (primary) out.primary = primary;
|
|
127
|
+
if (secondary) out.secondary = secondary;
|
|
128
|
+
return Object.keys(out).length ? out : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// claudeSidecarStatus(ref, opts?) -> status | null
|
|
132
|
+
// Prefer the sidecar when one exists for this session (file name == resumeId)
|
|
133
|
+
// and it carries a real context_window_size. Any missing file / bad JSON /
|
|
134
|
+
// absent window → null, so the caller falls back to the transcript path.
|
|
135
|
+
function claudeSidecarStatus(ref, opts = {}) {
|
|
136
|
+
const dir = opts.sidecarDir
|
|
137
|
+
|| (() => {
|
|
138
|
+
const ws = opts.workspace || findBridgeWorkspace(ref.cwd);
|
|
139
|
+
return ws ? path.join(ws, STATE_DIR_NAME, 'statusline') : null;
|
|
140
|
+
})();
|
|
141
|
+
if (!dir) return null;
|
|
142
|
+
let doc;
|
|
143
|
+
try {
|
|
144
|
+
doc = JSON.parse(fs.readFileSync(path.join(dir, ref.resumeId + '.json'), 'utf8'));
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const p = doc && doc.payload;
|
|
149
|
+
const cw = p && p.context_window;
|
|
150
|
+
const window = cw && Number(cw.context_window_size);
|
|
151
|
+
if (!p || !cw || !Number.isFinite(window) || window <= 0) return null;
|
|
152
|
+
const out = {
|
|
153
|
+
model: (p.model && (p.model.id || p.model.display_name)) || null,
|
|
154
|
+
contextUsed: Number(cw.total_input_tokens) || 0,
|
|
155
|
+
contextWindow: window,
|
|
156
|
+
};
|
|
157
|
+
const effort = p.effort && p.effort.level;
|
|
158
|
+
if (effort) out.effort = String(effort);
|
|
159
|
+
const rl = claudeSidecarRateLimits(p.rate_limits);
|
|
160
|
+
if (rl) out.rateLimits = rl;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// claudeStatus(ref, opts?) -> status | null
|
|
165
|
+
// Prefer the statusline sidecar (real window + rate limits); else the last
|
|
166
|
+
// assistant line's message.usage: contextUsed = input + cache_read +
|
|
167
|
+
// cache_creation + output (what the next turn starts from), with the context
|
|
168
|
+
// window guessed from the model→window map.
|
|
169
|
+
function claudeStatus(ref, opts = {}) {
|
|
170
|
+
if (!ref || !ref.cwd || !ref.resumeId) return null;
|
|
171
|
+
const sidecar = claudeSidecarStatus(ref, opts);
|
|
172
|
+
if (sidecar) return sidecar;
|
|
173
|
+
const projectsDir = opts.projectsDir || process.env.BC_CLAUDE_PROJECTS_DIR
|
|
174
|
+
|| path.join(os.homedir(), '.claude', 'projects');
|
|
175
|
+
const file = path.join(projectsDir, claudeProjectSlug(ref.cwd), ref.resumeId + '.jsonl');
|
|
176
|
+
let size = 0;
|
|
177
|
+
try {
|
|
178
|
+
size = fs.statSync(file).size;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
// One escalation: 256KB tail first, 4MB if no assistant line surfaced.
|
|
183
|
+
for (const maxBytes of [TAIL_BYTES, 16 * TAIL_BYTES]) {
|
|
184
|
+
const text = tailRead(file, maxBytes);
|
|
185
|
+
if (text === null) return null;
|
|
186
|
+
const lines = text.split('\n');
|
|
187
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
188
|
+
if (!lines[i].includes('"type":"assistant"')) continue;
|
|
189
|
+
let doc;
|
|
190
|
+
try {
|
|
191
|
+
doc = JSON.parse(lines[i]);
|
|
192
|
+
} catch {
|
|
193
|
+
continue; // the tail window's first line may be cut mid-JSON
|
|
194
|
+
}
|
|
195
|
+
const msg = doc && doc.type === 'assistant' && doc.message;
|
|
196
|
+
const u = msg && msg.usage;
|
|
197
|
+
if (!u || typeof u !== 'object') continue;
|
|
198
|
+
const used = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0)
|
|
199
|
+
+ (u.cache_creation_input_tokens || 0) + (u.output_tokens || 0);
|
|
200
|
+
return {
|
|
201
|
+
model: msg.model || null,
|
|
202
|
+
contextUsed: used,
|
|
203
|
+
contextWindow: claudeContextWindow(msg.model),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (size <= maxBytes) break; // whole file scanned — a bigger tail finds nothing new
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------- codex ----------
|
|
212
|
+
// Rollout path: ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<threadId>.jsonl
|
|
213
|
+
// (threadId — see codexThreadId). The date dirs are walked newest-first and the
|
|
214
|
+
// first match wins, so a thread resumed on a later day resolves to its newest
|
|
215
|
+
// rollout file.
|
|
216
|
+
function codexRolloutFile(threadId, sessionsDir) {
|
|
217
|
+
const suffix = '-' + threadId + '.jsonl';
|
|
218
|
+
const listDesc = (dir) => {
|
|
219
|
+
try {
|
|
220
|
+
return fs.readdirSync(dir).sort().reverse();
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
for (const year of listDesc(sessionsDir)) {
|
|
226
|
+
for (const month of listDesc(path.join(sessionsDir, year))) {
|
|
227
|
+
for (const day of listDesc(path.join(sessionsDir, year, month))) {
|
|
228
|
+
const dir = path.join(sessionsDir, year, month, day);
|
|
229
|
+
const hit = listDesc(dir).find((f) => f.startsWith('rollout-') && f.endsWith(suffix));
|
|
230
|
+
if (hit) return path.join(dir, hit);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function codexRateLimits(rl) {
|
|
238
|
+
if (!rl || typeof rl !== 'object') return null;
|
|
239
|
+
const pick = (w) => (w && typeof w === 'object' ? {
|
|
240
|
+
usedPercent: w.used_percent,
|
|
241
|
+
windowMinutes: w.window_minutes,
|
|
242
|
+
resetsAt: w.resets_at,
|
|
243
|
+
} : undefined);
|
|
244
|
+
const out = {};
|
|
245
|
+
if (pick(rl.primary)) out.primary = pick(rl.primary);
|
|
246
|
+
if (pick(rl.secondary)) out.secondary = pick(rl.secondary);
|
|
247
|
+
return Object.keys(out).length ? out : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// codexStatus(ref, opts?) -> status | null
|
|
251
|
+
// The thread-id comes from codexThreadId (recorded session-id file, then the
|
|
252
|
+
// ref) — NOT from ref.resumeId alone: a codex ref is born without one.
|
|
253
|
+
// The last token_count event carries current context occupancy
|
|
254
|
+
// (info.last_token_usage.total_tokens = input+cached+output of the last turn)
|
|
255
|
+
// and the model context window; the model rides every turn_context line, so the
|
|
256
|
+
// tail always has a fresh one. rate_limits come from the same token_count.
|
|
257
|
+
// NOTE: info.total_token_usage is the CUMULATIVE session total (grows forever,
|
|
258
|
+
// exceeds the window) — it is NOT occupancy. Selecting on last_token_usage is
|
|
259
|
+
// deliberate: rollouts where info is populated always carry it (verified on
|
|
260
|
+
// real rollouts), and the only ones missing it have info === null, which the
|
|
261
|
+
// null-guards below already reject — so no total_token_usage fallback is needed.
|
|
262
|
+
// codexThreadId(ref, opts) -> the thread-id whose rollout to read, or null.
|
|
263
|
+
// The notify relay rewrites <stateDir>/<key>.session-id at every turn, so that
|
|
264
|
+
// file is ground truth; ref.resumeId is adopted once from a turn-end POST and a
|
|
265
|
+
// ref can live its whole life without one (codex assigns the id itself — see
|
|
266
|
+
// codex-tmux.js). Same order resume() uses: recorded file first, ref second.
|
|
267
|
+
// opts.stateDir comes from the caller that knows where harness state lives
|
|
268
|
+
// (codex-tmux status()); without it only the ref can answer.
|
|
269
|
+
function codexThreadId(ref, opts = {}) {
|
|
270
|
+
if (ref && ref.session && opts.stateDir) {
|
|
271
|
+
const key = ref.window ? ref.session + ':' + ref.window : ref.session;
|
|
272
|
+
try {
|
|
273
|
+
const rec = fs.readFileSync(path.join(opts.stateDir, key + '.session-id'), 'utf8').trim();
|
|
274
|
+
if (rec) return rec;
|
|
275
|
+
} catch {
|
|
276
|
+
// no recorded id — the ref's is all there is
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return (ref && ref.resumeId) || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function codexStatus(ref, opts = {}) {
|
|
283
|
+
const threadId = codexThreadId(ref, opts);
|
|
284
|
+
if (!threadId) return null;
|
|
285
|
+
const sessionsDir = opts.sessionsDir || process.env.BC_CODEX_SESSIONS_DIR
|
|
286
|
+
|| path.join(os.homedir(), '.codex', 'sessions');
|
|
287
|
+
const file = codexRolloutFile(threadId, sessionsDir);
|
|
288
|
+
if (!file) return null;
|
|
289
|
+
const text = tailRead(file, TAIL_BYTES);
|
|
290
|
+
if (text === null) return null;
|
|
291
|
+
const lines = text.split('\n');
|
|
292
|
+
let usage = null;
|
|
293
|
+
let model = null;
|
|
294
|
+
let effort = null;
|
|
295
|
+
for (let i = lines.length - 1; i >= 0 && !(usage && model); i--) {
|
|
296
|
+
const line = lines[i];
|
|
297
|
+
let doc = null;
|
|
298
|
+
if (!usage && line.includes('"token_count"')) {
|
|
299
|
+
try { doc = JSON.parse(line); } catch { continue; }
|
|
300
|
+
const p = doc && doc.payload;
|
|
301
|
+
if (p && p.type === 'token_count' && p.info && p.info.last_token_usage) usage = p;
|
|
302
|
+
} else if (!model && line.includes('"turn_context"')) {
|
|
303
|
+
try { doc = JSON.parse(line); } catch { continue; }
|
|
304
|
+
const p = doc && doc.payload;
|
|
305
|
+
if (doc.type === 'turn_context' && p && p.model) {
|
|
306
|
+
model = String(p.model);
|
|
307
|
+
if (p.effort) effort = String(p.effort);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!usage) return null;
|
|
312
|
+
const out = {
|
|
313
|
+
model,
|
|
314
|
+
contextUsed: usage.info.last_token_usage.total_tokens || 0,
|
|
315
|
+
contextWindow: usage.info.model_context_window || null,
|
|
316
|
+
};
|
|
317
|
+
if (effort) out.effort = effort;
|
|
318
|
+
const rl = codexRateLimits(usage.rate_limits);
|
|
319
|
+
if (rl) out.rateLimits = rl;
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------- shared slash-command surface ----------
|
|
324
|
+
// The commands every status-capable harness answers; runCommand semantics:
|
|
325
|
+
// /status formats status(), /compact rides the verified-submit send path
|
|
326
|
+
// (the harness's OWN /compact runs in-session), /help renders this list.
|
|
327
|
+
const SLASH_COMMANDS = [
|
|
328
|
+
{ name: '/status', description: 'model, context usage and rate limits' },
|
|
329
|
+
{ name: '/compact', description: 'compact the conversation to free context' },
|
|
330
|
+
{ name: '/help', description: 'list the available commands' },
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
// Replies render as markdown in the chat thread, where a single newline
|
|
334
|
+
// collapses — blank-line separators keep each line its own paragraph.
|
|
335
|
+
function helpText(cmds) {
|
|
336
|
+
return cmds.map((c) => c.name + ' — ' + c.description).join('\n\n');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function fmtInt(n) {
|
|
340
|
+
return Number.isFinite(n) ? Math.round(n).toLocaleString('en-US') : '?';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function fmtWindowLabel(minutes) {
|
|
344
|
+
if (!Number.isFinite(minutes)) return 'rate';
|
|
345
|
+
if (minutes % 10080 === 0) return (minutes / 10080) + 'w';
|
|
346
|
+
if (minutes % 1440 === 0) return (minutes / 1440) + 'd';
|
|
347
|
+
if (minutes % 60 === 0) return (minutes / 60) + 'h';
|
|
348
|
+
return minutes + 'min';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// formatStatus(status) -> the human /status reply.
|
|
352
|
+
function formatStatus(st) {
|
|
353
|
+
const modelLine = 'model: ' + (st.model || 'unknown') + (st.effort ? ' (' + st.effort + ')' : '');
|
|
354
|
+
const lines = [modelLine];
|
|
355
|
+
if (Number.isFinite(st.contextUsed) && st.contextWindow > 0) {
|
|
356
|
+
const pct = Math.round((st.contextUsed / st.contextWindow) * 100);
|
|
357
|
+
lines.push('context: ' + fmtInt(st.contextUsed) + ' / ' + fmtInt(st.contextWindow)
|
|
358
|
+
+ ' tokens (' + pct + '%)');
|
|
359
|
+
} else if (Number.isFinite(st.contextUsed)) {
|
|
360
|
+
lines.push('context: ' + fmtInt(st.contextUsed) + ' tokens');
|
|
361
|
+
}
|
|
362
|
+
const rl = st.rateLimits || {};
|
|
363
|
+
for (const key of ['primary', 'secondary']) {
|
|
364
|
+
const w = rl[key];
|
|
365
|
+
if (!w) continue;
|
|
366
|
+
let line = fmtWindowLabel(w.windowMinutes) + ' limit: ' + (Number.isFinite(w.usedPercent) ? Math.round(w.usedPercent) : '?') + '% used';
|
|
367
|
+
if (Number.isFinite(w.resetsAt)) {
|
|
368
|
+
line += ' (resets ' + new Date(w.resetsAt * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC)';
|
|
369
|
+
}
|
|
370
|
+
lines.push(line);
|
|
371
|
+
}
|
|
372
|
+
return lines.join('\n\n');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
tailRead,
|
|
377
|
+
claudeProjectSlug,
|
|
378
|
+
claudeContextWindow,
|
|
379
|
+
findBridgeWorkspace,
|
|
380
|
+
claudeSidecarStatus,
|
|
381
|
+
claudeStatus,
|
|
382
|
+
codexRolloutFile,
|
|
383
|
+
codexThreadId,
|
|
384
|
+
codexStatus,
|
|
385
|
+
SLASH_COMMANDS,
|
|
386
|
+
helpText,
|
|
387
|
+
formatStatus,
|
|
388
|
+
};
|