coban 0.0.0-stage → 0.2.2

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/dist/usage.js ADDED
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export const WINDOW_5H = 5 * 3600_000;
5
+ const WINDOW_7D = 7 * 24 * 3600_000;
6
+ // Rough per-5h budget (tokens) when the profile sets no limit5h. Plans differ; tune it in config.json.
7
+ export const DEFAULT_LIMIT_5H = 5_000_000;
8
+ const cache = new Map(); // keeps hub-side re-scans cheap
9
+ export const isClaude = (p) => /^claude\b/.test(p.cmd.trim());
10
+ const configDir = (p) => p.env?.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
11
+ function jsonlFiles(dir, since, out = []) {
12
+ let ents;
13
+ try {
14
+ ents = fs.readdirSync(dir, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ return out;
18
+ }
19
+ for (const e of ents) {
20
+ const f = path.join(dir, e.name);
21
+ if (e.isDirectory())
22
+ jsonlFiles(f, since, out);
23
+ else if (e.name.endsWith('.jsonl')) {
24
+ try {
25
+ if (fs.statSync(f).mtimeMs >= since)
26
+ out.push(f);
27
+ }
28
+ catch { }
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ // One entry per API message: Claude Code writes a line per content block, all sharing message.id.
34
+ function readFile(f) {
35
+ const st = fs.statSync(f);
36
+ const c = cache.get(f);
37
+ if (c && c.mtime === st.mtimeMs && c.size === st.size)
38
+ return c.entries;
39
+ const entries = new Map();
40
+ for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
41
+ if (!line.includes('"assistant"') || !line.includes('"usage"'))
42
+ continue;
43
+ let d;
44
+ try {
45
+ d = JSON.parse(line);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ const u = d?.message?.usage;
51
+ if (d.type !== 'assistant' || !u)
52
+ continue;
53
+ const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
54
+ const key = d.message.id ?? d.requestId ?? d.uuid;
55
+ const prev = entries.get(key);
56
+ if (!prev || tokens > prev.tokens)
57
+ entries.set(key, { ts: Date.parse(d.timestamp), tokens });
58
+ }
59
+ cache.set(f, { mtime: st.mtimeMs, size: st.size, entries });
60
+ return entries;
61
+ }
62
+ export function profileUsage(p, limitedAt, now = Date.now()) {
63
+ const limited = !!limitedAt && now - limitedAt < WINDOW_5H;
64
+ const base = { profileId: p.id, title: p.title, limited };
65
+ if (!isClaude(p))
66
+ return { ...base, used5h: 0, used7d: 0, limit5h: 0, pct5h: 0, resetAt: null };
67
+ const limit5h = p.limit5h ?? DEFAULT_LIMIT_5H;
68
+ const seen = new Set(); // a message can appear in several files (resumed/forked sessions)
69
+ let used5h = 0, used7d = 0, oldest5h = Infinity;
70
+ for (const f of jsonlFiles(path.join(configDir(p), 'projects'), now - WINDOW_7D)) {
71
+ let entries;
72
+ try {
73
+ entries = readFile(f);
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ for (const [id, e] of entries) {
79
+ if (!(now - e.ts < WINDOW_7D) || seen.has(id))
80
+ continue;
81
+ seen.add(id);
82
+ used7d += e.tokens;
83
+ if (now - e.ts < WINDOW_5H) {
84
+ used5h += e.tokens;
85
+ oldest5h = Math.min(oldest5h, e.ts);
86
+ }
87
+ }
88
+ }
89
+ return { ...base, used5h, used7d, limit5h, pct5h: used5h / limit5h, resetAt: oldest5h === Infinity ? null : oldest5h + WINDOW_5H };
90
+ }
91
+ // Least-spent account first; accounts with unknown usage (non-Claude) last, limited ones after the rest.
92
+ export function sortUsage(list) {
93
+ const rank = (u) => (u.limited ? 2 : 0) + (u.limit5h ? 0 : 1);
94
+ return [...list].sort((a, b) => rank(a) - rank(b) || a.pct5h - b.pct5h || a.used7d - b.used7d);
95
+ }
96
+ // limitedAt: profileId → last time one of its agents went 'limited' (the hub tracks this).
97
+ export function allUsage(cfg, limitedAt) {
98
+ const now = Date.now();
99
+ return sortUsage(cfg.profiles.map((p) => profileUsage(p, limitedAt?.get(p.id), now)));
100
+ }
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "coban",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.2",
4
+ "description": "A terminal app where one orchestrator agent dispatches work to many AI coding agents — several Claude Code accounts, Codex, OpenCode, Cursor — that message each other over a shared board.",
5
+ "scripts": {
6
+ "postinstall": "node -e \"for(const a of ['darwin-arm64','darwin-x64','linux-x64','linux-arm64']){try{require('fs').chmodSync(require.resolve('node-pty/package.json').replace('package.json','prebuilds/'+a+'/spawn-helper'),0o755)}catch{}}\"",
7
+ "test": "node --disable-warning=ExperimentalWarning --test --test-concurrency=1 test/*.test.ts",
8
+ "typecheck": "tsc -p tsconfig.json",
9
+ "snapshot": "node --disable-warning=ExperimentalWarning scripts/snapshot.ts",
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
12
+ },
13
+ "keywords": [
14
+ "ai",
15
+ "agents",
16
+ "multi-agent",
17
+ "orchestrator",
18
+ "claude-code",
19
+ "codex",
20
+ "opencode",
21
+ "cursor",
22
+ "terminal",
23
+ "tui"
24
+ ],
25
+ "author": "cenkerarin",
26
+ "license": "MIT",
27
+ "type": "module",
28
+ "dependencies": {
29
+ "@xterm/headless": "^6.0.0",
30
+ "node-pty": "^1.1.0"
31
+ },
32
+ "bin": {
33
+ "coban": "bin/coban"
34
+ },
35
+ "engines": {
36
+ "node": ">=24"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^26.6.6",
40
+ "typescript": "^7.0.2"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/cenkerarin/coban.git"
45
+ },
46
+ "homepage": "https://github.com/cenkerarin/coban#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/cenkerarin/coban/issues"
49
+ },
50
+ "files": [
51
+ "bin/",
52
+ "dist/",
53
+ "README.md",
54
+ "LICENSE"
55
+ ],
56
+ "os": [
57
+ "darwin",
58
+ "linux"
59
+ ]
60
+ }