project-cypher 0.21.2 → 0.22.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/bin/cypher.js +69 -0
- package/package.json +1 -1
package/bin/cypher.js
CHANGED
|
@@ -12,6 +12,61 @@ function readRegistry() {
|
|
|
12
12
|
}
|
|
13
13
|
const writeRegistry = list => { fs.mkdirSync(ROOT_DIR, { recursive: true }); fs.writeFileSync(REG, JSON.stringify(list, null, 2)); };
|
|
14
14
|
const SERVER = 'https://hub.reubenreny.in/cypher', OLD_SERVERS = ['http://140.238.248.30:3300'];
|
|
15
|
+
// Claude plan usage of THIS computer's Claude sign-in (same numbers as /usage in Claude Code). Read-only: never refreshes or rewrites the token.
|
|
16
|
+
let usageCache = null;
|
|
17
|
+
async function claudeUsage() {
|
|
18
|
+
if (usageCache && Date.now() - usageCache.at < 60000) return usageCache.r;
|
|
19
|
+
const dir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
20
|
+
let cred = null;
|
|
21
|
+
try { cred = JSON.parse(fs.readFileSync(path.join(dir, '.credentials.json'), 'utf8')).claudeAiOauth; } catch {}
|
|
22
|
+
if (!cred && process.platform === 'darwin') try { cred = JSON.parse(require('child_process').execFileSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], { encoding: 'utf8', timeout: 5000 })).claudeAiOauth; } catch {}
|
|
23
|
+
const token = cred?.accessToken || process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
24
|
+
const plan = cred?.subscriptionType ? String(cred.subscriptionType) : '';
|
|
25
|
+
if (!token) return { ok: false, message: process.env.ANTHROPIC_API_KEY ? 'Claude here uses an API key, which has no plan limits.' : 'Claude is not signed in on this computer. Run claude and log in.' };
|
|
26
|
+
if (cred?.expiresAt && cred.expiresAt < Date.now()) return { ok: false, plan, message: 'The Claude sign-in on this computer has expired for now. It renews the next time Claude runs here; send any message and try again.' };
|
|
27
|
+
let r, j;
|
|
28
|
+
try { r = await fetch('https://api.anthropic.com/api/oauth/usage', { headers: { authorization: 'Bearer ' + token, 'anthropic-beta': 'oauth-2025-04-20', 'content-type': 'application/json' }, signal: AbortSignal.timeout(15000) }); j = await r.json(); }
|
|
29
|
+
catch { return { ok: false, plan, message: 'Could not reach Anthropic from this computer.' }; }
|
|
30
|
+
if (r.status === 401) return { ok: false, plan, message: 'The Claude sign-in on this computer was not accepted. It renews the next time Claude runs here.' };
|
|
31
|
+
if (!r.ok) return { ok: false, plan, message: `Anthropic answered ${r.status}. Try again later.` };
|
|
32
|
+
const lab = { session: 'Current session (5 h)', weekly_all: 'This week, all models' }, limits = [];
|
|
33
|
+
if (Array.isArray(j.limits) && j.limits.length) j.limits.forEach(l => limits.push({ label: lab[l.kind] || (l.group === 'weekly' ? 'This week, ' + (l.scope?.model?.display_name || l.scope?.surface || 'scoped') : String(l.kind || 'limit').replace(/_/g, ' ')), percent: +l.percent || 0, resets: l.resets_at || null, severity: l.severity || '' }));
|
|
34
|
+
else [['five_hour', 'Current session (5 h)'], ['seven_day', 'This week, all models'], ['seven_day_opus', 'This week, Opus'], ['seven_day_sonnet', 'This week, Sonnet']].forEach(([k, label]) => j[k] && limits.push({ label, percent: +j[k].utilization || 0, resets: j[k].resets_at || null }));
|
|
35
|
+
const x = j.extra_usage, d = 10 ** (x?.decimal_places ?? 2);
|
|
36
|
+
const extra = x?.is_enabled ? { used: x.used_credits != null ? x.used_credits / d : null, limit: x.monthly_limit != null ? x.monthly_limit / d : null, currency: x.currency || 'USD' } : null;
|
|
37
|
+
const breakdown = (j.seven_day_breakdown?.rows || []).filter(b => b.percent > 0).map(b => ({ label: String(b.display_name || b.key), percent: +b.percent || 0 }));
|
|
38
|
+
const res = { ok: true, plan, limits, extra, breakdown, at: Date.now() };
|
|
39
|
+
usageCache = { at: Date.now(), r: res }; return res;
|
|
40
|
+
}
|
|
41
|
+
// Daily activity of the last 7 days from Claude Code's own transcripts (~/.claude/projects/**/*.jsonl), bucketed by the
|
|
42
|
+
// asking device's timezone (tz = minutes, like Date#getTimezoneOffset). Each API reply counted once (message.id + requestId).
|
|
43
|
+
let actCache = null;
|
|
44
|
+
async function claudeActivity(tz = 0) {
|
|
45
|
+
tz = Math.max(-840, Math.min(840, +tz || 0));
|
|
46
|
+
if (actCache && actCache.tz === tz && Date.now() - actCache.at < 60000) return actCache.r;
|
|
47
|
+
const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'), 'projects');
|
|
48
|
+
const dayOf = ms => new Date(ms - tz * 60000).toISOString().slice(0, 10);
|
|
49
|
+
const days = []; for (let i = 6; i >= 0; i--) days.push({ date: dayOf(Date.now() - i * 86400000), replies: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, models: {} });
|
|
50
|
+
const by = Object.fromEntries(days.map(d => [d.date, d])), since = Date.now() - 8 * 86400000, seen = new Set(), sess = new Set();
|
|
51
|
+
const files = []; const walk = (d, depth) => { let es = []; try { es = fs.readdirSync(d, { withFileTypes: true }); } catch {} for (const e of es) { const p = path.join(d, e.name); if (e.isDirectory() && depth < 4) walk(p, depth + 1); else if (e.name.endsWith('.jsonl')) { try { if (fs.statSync(p).mtimeMs >= since) files.push(p); } catch {} } } };
|
|
52
|
+
walk(dir, 0);
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
let txt = ''; try { txt = await fs.promises.readFile(f, 'utf8'); } catch { continue; }
|
|
55
|
+
for (const line of txt.split('\n')) {
|
|
56
|
+
if (!line.includes('"usage"') || !line.includes('"assistant"')) continue;
|
|
57
|
+
let j; try { j = JSON.parse(line); } catch { continue; }
|
|
58
|
+
const u = j.message?.usage, t = Date.parse(j.timestamp); if (j.type !== 'assistant' || !u || !t) continue;
|
|
59
|
+
const d = by[dayOf(t)]; if (!d) continue;
|
|
60
|
+
const k = (j.message.id || '') + ':' + (j.requestId || ''); if (k !== ':') { if (seen.has(k)) continue; seen.add(k); }
|
|
61
|
+
const model = String(j.message.model || ''); if (model === '<synthetic>') continue;
|
|
62
|
+
d.replies++; d.input += u.input_tokens || 0; d.output += u.output_tokens || 0; d.cacheRead += u.cache_read_input_tokens || 0; d.cacheWrite += u.cache_creation_input_tokens || 0;
|
|
63
|
+
const short = model.replace(/^claude-/, '').replace(/-\d{8}$/, ''); if (short) d.models[short] = (d.models[short] || 0) + 1;
|
|
64
|
+
if (j.sessionId && !sess.has(d.date + j.sessionId)) { sess.add(d.date + j.sessionId); d.sessions++; }
|
|
65
|
+
}
|
|
66
|
+
await new Promise(r => setImmediate(r)); // keep the agent responsive while scanning
|
|
67
|
+
}
|
|
68
|
+
const r = { days }; actCache = { at: Date.now(), tz, r }; return r;
|
|
69
|
+
}
|
|
15
70
|
const rawCfg = () => { try { const c = JSON.parse(fs.readFileSync(CFG, 'utf8')); if (!c.server || OLD_SERVERS.includes(String(c.server).replace(/\/$/, ''))) c.server = SERVER; return c; } catch { return { server: SERVER, token: '', sessions: {}, permissionMode: 'acceptEdits' }; } };
|
|
16
71
|
const load = () => { const c = rawCfg(); if (c.projects?.length && !fs.existsSync(REG)) writeRegistry(c.projects); delete c.projects; return { ...c, projects: readRegistry() }; };
|
|
17
72
|
const save = c => { const { projects, ...rest } = c; fs.mkdirSync(CFG_DIR, { recursive: true }); fs.writeFileSync(CFG, JSON.stringify(rest, null, 2), { mode: 0o600 }); };
|
|
@@ -254,6 +309,17 @@ const cmds = {
|
|
|
254
309
|
console.log(`service: ${SVC.running() ? 'running' : 'stopped'}${SVC.enabled() ? ' (starts automatically)' : ' (auto-start off)'}\ninternet/server: ${r?.ok ? 'reachable' : 'unreachable'} ${c.server}\nlinked: ${c.token ? 'yes, as "' + devName() + '"' : 'no (run cypher init)'}\nprojects: ${c.projects.length}\nlog: ${LOG}`);
|
|
255
310
|
try { const t = fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(-3).join('\n'); if (t) console.log('\nrecent:\n' + t); } catch {}
|
|
256
311
|
},
|
|
312
|
+
async usage() {
|
|
313
|
+
const [u, a] = await Promise.all([claudeUsage(), claudeActivity(new Date().getTimezoneOffset())]);
|
|
314
|
+
const t = s => s ? new Date(s).toLocaleString([], { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : '';
|
|
315
|
+
if (!u.ok) console.log(u.message); else {
|
|
316
|
+
console.log(`Claude plan: ${u.plan || 'unknown'}`); u.limits.forEach(l => console.log(`${l.label.padEnd(28)} ${String(Math.round(l.percent)).padStart(3)}% used${l.resets ? ' resets ' + t(l.resets) : ''}`));
|
|
317
|
+
if (u.extra) console.log(`Extra usage: ${u.extra.used ?? 0}${u.extra.limit != null ? ' / ' + u.extra.limit : ''} ${u.extra.currency}`);
|
|
318
|
+
}
|
|
319
|
+
const n = x => x >= 1e6 ? (x / 1e6).toFixed(1) + 'M' : x >= 1e3 ? (x / 1e3).toFixed(1) + 'k' : String(x);
|
|
320
|
+
console.log('\nLast 7 days (this computer) replies sessions output total tokens');
|
|
321
|
+
a.days.forEach(d => console.log(`${d.date.padEnd(32)} ${String(d.replies).padStart(8)} ${String(d.sessions).padStart(9)} ${n(d.output).padStart(8)} ${n(d.input + d.output + d.cacheRead + d.cacheWrite).padStart(14)}`));
|
|
322
|
+
},
|
|
257
323
|
version() { console.log(require('../package.json').version); },
|
|
258
324
|
update() { // npm install -g project-cypher@latest, then restart the background service
|
|
259
325
|
const cur = require('../package.json').version; const here = fs.realpathSync(__filename);
|
|
@@ -404,6 +470,8 @@ const cmds = {
|
|
|
404
470
|
});
|
|
405
471
|
return;
|
|
406
472
|
}
|
|
473
|
+
if (m.type === 'usage') return Promise.all([claudeUsage().catch(e => ({ ok: false, message: String(e.message || e).slice(0, 200) })), claudeActivity(m.tz).catch(() => null)])
|
|
474
|
+
.then(([u, a]) => tx({ type: 'usage', reqId: m.reqId, ...u, activity: a }));
|
|
407
475
|
if (m.type === 'sessions') { if (!proj) return; return tx({ ...ctx, type: 'sessions', list: listSessions(proj.path, load().sessions?.[proj.id]) }); }
|
|
408
476
|
if (m.type === 'usesession') { // switch the project to an older Claude session and send its chat back
|
|
409
477
|
if (!proj || !SID.test(String(m.id))) return tx({ ...ctx, type: 'error', message: 'Unknown session.' });
|
|
@@ -476,6 +544,7 @@ const help = () => console.log(`Project Cypher
|
|
|
476
544
|
cypher resume [project-id] continue the phone's Claude session for this folder in the terminal
|
|
477
545
|
cypher update | version install the newest CLI from npm and restart the service / show version
|
|
478
546
|
cypher stats | logs service + connection status / recent log
|
|
547
|
+
cypher usage Claude plan usage (session / weekly limits) of this computer
|
|
479
548
|
cypher status | latest server check / latest app build
|
|
480
549
|
|
|
481
550
|
Config: ~/.cypher/config.json (permissionMode defaults to acceptEdits)`);
|