ccdeck 3.41.3 → 3.42.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.
@@ -0,0 +1,126 @@
1
+ // Append-only, local metadata. Never stores raw command arguments or tool outputs.
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { appendFile, mkdir, readFile, readdir, stat, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { Worker } from 'node:worker_threads';
6
+ import { deckDataDir } from './deck-home.mjs';
7
+ import { writeFileAtomic } from './atomic-write.mjs';
8
+ import { buildToolAuditReport } from './tool-audit-report.mjs';
9
+ import { auditOperations } from './tool-audit-operations.mjs';
10
+ const DAY = 86400000;
11
+ const digest = text => createHash('sha256').update(text).digest('hex');
12
+ const date = t => new Date(t).toISOString().slice(0, 10);
13
+ const validMeta = m => m?.v === 1 && /^[a-f0-9-]{36}$/.test(m.deviceId ?? '') && /^[a-f0-9-]{36}$/.test(m.epoch ?? '') && [30, 60, 90].includes(m.retentionDays) && Number.isFinite(m.startedAt);
14
+
15
+ export function createToolAudit({ dir, now = Date.now, runReport = workerReport } = {}) {
16
+ let meta, readyPromise, serial = Promise.resolve(), pending = [], timer, storageOk = true, dropped = 0, reportFlight = null, prunedDay = null;
17
+ const enqueue = fn => { const task = serial.then(fn); serial = task.catch(() => {}); return task; };
18
+ const ready = () => readyPromise ??= (async () => {
19
+ await mkdir(dir, { recursive: true });
20
+ try { meta = JSON.parse(await readFile(join(dir, 'settings.json'), 'utf8')); }
21
+ catch (e) { if (e.code !== 'ENOENT') throw e; }
22
+ if (meta && !validMeta(meta)) throw new Error('Unreadable tool history settings');
23
+ if (!meta) { meta = { v: 1, deviceId: randomUUID(), epoch: randomUUID(), retentionDays: 90, startedAt: now() }; await saveMeta(); }
24
+ await prune();
25
+ })();
26
+ const saveMeta = () => writeFileAtomic(join(dir, 'settings.json'), JSON.stringify(meta));
27
+ const files = async () => (await readdir(dir)).filter(n => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(n)).sort();
28
+ const prune = async () => {
29
+ const floor = date(now() - (meta.retentionDays - 1) * DAY);
30
+ for (const file of await files()) if (file.slice(0, 10) < floor) await unlink(join(dir, file));
31
+ };
32
+ const flush = async () => {
33
+ if (timer) { clearTimeout(timer); timer = null; }
34
+ await ready();
35
+ if (prunedDay !== date(now())) { await prune(); prunedDay = date(now()); }
36
+ const batch = pending.splice(0);
37
+ if (!batch.length) return;
38
+ const days = new Map();
39
+ for (const row of batch) { const d = date(row.t); if (!days.has(d)) days.set(d, []); days.get(d).push(row); }
40
+ for (const [d, rows] of days) {
41
+ try {
42
+ await appendFile(join(dir, `${d}.jsonl`), rows.map(row => JSON.stringify({ ...row, id: digest(`${meta.deviceId}|${meta.epoch}|${row.id}`) })).join('\n') + '\n', { mode: 0o600 });
43
+ storageOk = true;
44
+ } catch { storageOk = false; dropped += rows.length; }
45
+ }
46
+ };
47
+ const inventory = async () => {
48
+ await prune();
49
+ const journal = await Promise.all((await files()).map(async name => ({ path: join(dir, name), size: (await stat(join(dir, name))).size })));
50
+ return { journal, bytes: journal.reduce((n, f) => n + f.size, 0) + (await stat(join(dir, 'settings.json'))).size };
51
+ };
52
+ return {
53
+ async start() { await ready(); },
54
+ observe(raw, source, opts = {}) {
55
+ if (opts.replay || !['hook', 'codex'].includes(source)) return;
56
+ const kind = raw?.hook_event_name === 'PreToolUse' ? 'start' : ['PostToolUse', 'PostToolUseFailure'].includes(raw?.hook_event_name) ? 'end' : null;
57
+ if (!kind || typeof raw.session_id !== 'string' || typeof raw.tool_use_id !== 'string' || raw.session_id.length > 256 || raw.tool_use_id.length > 256) return;
58
+ const provider = raw.provider === 'codex' || source === 'codex' ? 'codex' : 'claude';
59
+ const t = now();
60
+ const clock = provider === 'codex' ? 'rollout' : 'hook';
61
+ const at = provider === 'codex' ? opts.toolAuditAt : (opts.receivedAt ?? t);
62
+ const measuredAt = Number.isFinite(at) && at >= t - 7 * DAY && at <= t + 60000 ? at : null;
63
+ const row = { id: `${provider}|${raw.session_id}|${raw.tool_use_id}`, provider, kind, t, at: measuredAt, clock };
64
+ if (kind === 'start') Object.assign(row, auditOperations(raw.tool_name, raw.tool_input));
65
+ else row.failed = kind === 'end' && raw.hook_event_name === 'PostToolUseFailure';
66
+ if (pending.length >= 25000) { dropped++; return; }
67
+ pending.push(row);
68
+ if (!timer) { timer = setTimeout(() => { void enqueue(flush).catch(() => { storageOk = false; }); }, 1000); timer.unref?.(); }
69
+ },
70
+ async status() { return enqueue(async () => { await flush(); const { bytes } = await inventory(); return { ok: true, ...meta, bytes, storageOk, dropped }; }); },
71
+ async configure(retentionDays) {
72
+ if (![30, 60, 90].includes(retentionDays)) throw new Error('Invalid retention');
73
+ return enqueue(async () => { await flush(); const previous = meta.retentionDays; meta.retentionDays = retentionDays;
74
+ try { await saveMeta(); } catch (e) { meta.retentionDays = previous; throw e; }
75
+ await prune(); return { ok: true }; });
76
+ },
77
+ async clear() { return enqueue(async () => {
78
+ await flush();
79
+ for (const name of await files()) await unlink(join(dir, name));
80
+ meta = { ...meta, epoch: randomUUID(), startedAt: now() }; await saveMeta(); dropped = 0; storageOk = true;
81
+ return { ok: true };
82
+ }); },
83
+ async report(query, signal) {
84
+ if (reportFlight) return { ok: false, reason: 'busy' };
85
+ reportFlight = (async () => {
86
+ const snapshot = await enqueue(async () => { await flush(); const inv = await inventory(); return { ...inv, meta: { ...meta }, storageOk, dropped }; });
87
+ const result = await runReport({ files: snapshot.journal, query, now: now(), meta: snapshot.meta }, signal);
88
+ if (meta.epoch !== snapshot.meta.epoch) return { ok: false, reason: 'history_cleared' };
89
+ return { ...result, bytes: snapshot.bytes, storageOk: snapshot.storageOk, dropped: snapshot.dropped, deviceId: snapshot.meta.deviceId, startedAt: snapshot.meta.startedAt, retentionDays: snapshot.meta.retentionDays };
90
+ })();
91
+ try { return await reportFlight; } finally { reportFlight = null; }
92
+ },
93
+ async close() { await enqueue(flush); },
94
+ };
95
+ }
96
+ function workerReport(input, signal) {
97
+ return new Promise((resolve, reject) => {
98
+ if (signal?.aborted) return reject(new Error('Report cancelled'));
99
+ // Serialize the already-loaded function so self-update cannot mix builds.
100
+ const code = `const { parentPort, workerData } = require('node:worker_threads'); (${buildToolAuditReport.toString()})(workerData, { createReadStream: require('node:fs').createReadStream, createInterface: require('node:readline').createInterface }).then(result => parentPort.postMessage(result));`;
101
+ const worker = new Worker(code, { eval: true, workerData: input, resourceLimits: { maxOldGenerationSizeMb: 256 } });
102
+ let done = false;
103
+ const finish = (error, result) => {
104
+ if (done) return; done = true; clearTimeout(timeout); signal?.removeEventListener('abort', abort);
105
+ error ? reject(error) : resolve(result);
106
+ };
107
+ const abort = () => { void worker.terminate(); finish(new Error('Report cancelled')); };
108
+ const timeout = setTimeout(() => { void worker.terminate(); finish(new Error('Report timed out')); }, 60000);
109
+ signal?.addEventListener('abort', abort, { once: true });
110
+ worker.once('message', result => finish(null, result));
111
+ worker.once('error', e => finish(e));
112
+ worker.once('exit', () => { if (!done) finish(new Error('Report worker ended without a result')); });
113
+ });
114
+ }
115
+ let audit;
116
+ export async function startToolAudit() { audit ??= createToolAudit({ dir: join(deckDataDir(), 'tool-audit') }); await audit.start().catch(() => {}); }
117
+ export function noteToolAudit(raw, source, opts) { audit?.observe(raw, source, opts); }
118
+ export const toolAudit = () => audit;
119
+
120
+ /** Graceful launcher exit drains the batch; a wedged disk cannot hold shutdown. */
121
+ export async function drainToolAudit(ms = 3000) {
122
+ if (!audit) return;
123
+ let timer;
124
+ try { await Promise.race([audit.close().catch(() => {}), new Promise(resolve => { timer = setTimeout(resolve, ms); })]); }
125
+ finally { clearTimeout(timer); }
126
+ }
@@ -1 +0,0 @@
1
- import{a as O,f as p,r as i,s as Z,p as _,j as a,b as g,c as G,u as ee,d as se,e as ae,g as te,R as ne,h as E,i as oe,k as le}from"./index-C80Hp_2s.js";function re(s){return!s.answer||s.answer.range!==s.want?{phase:"busy",stale:!1}:s.answer.ok?{phase:s.answer.days>0?"chart":"empty",stale:s.loading}:{phase:"error",stale:s.loading}}function I(s){const e=new Map;for(const n of s)for(const o of n.agents??[]){if(!o||typeof o.agent!="string"||o.agent==="")continue;const r=e.get(o.agent)??{id:o.agent,cost:0,tokens:0};r.cost+=Number.isFinite(o.totalCost)?o.totalCost:0,r.tokens+=Number.isFinite(o.totalTokens)?o.totalTokens:0,e.set(o.agent,r)}return[...e.values()].sort((n,o)=>o.cost-n.cost||n.id.localeCompare(o.id))}function ce(s){const e=I([{agents:s}]);return e.length<2?null:e.map(n=>`${O(n.id)} ${p(n.cost)}`).join(" · ")}function F(s){switch(s.toLowerCase()){case"claude":return"var(--model-opus)";case"codex":return"var(--model-codex)";case"gemini":return"var(--model-gemini)";case"copilot":return"var(--model-haiku)";default:return"var(--model-other)"}}function ie(s,e){if(!(e>0)||!(s>0))return"0%";const n=s/e*100;return n<.1?"<0.1%":`${n.toFixed(1)}%`}const u=s=>typeof s=="number"&&Number.isFinite(s)?s:0,$=s=>typeof s=="string"?s:"",U=s=>Array.isArray(s)?s.filter(e=>typeof e=="string"):[];function ue(s){const e=s??{};return{modelName:$(e.modelName),cost:u(e.cost),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens)}}function de(s){const e=s??{},n=Array.isArray(e.agents)?e.agents.map(o=>{const r=o??{};return{agent:$(r.agent),totalCost:u(r.totalCost),totalTokens:u(r.totalTokens)}}):void 0;return{period:$(e.period),totalCost:u(e.totalCost),totalTokens:u(e.totalTokens),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens),modelsUsed:U(e.modelsUsed),modelBreakdowns:Array.isArray(e.modelBreakdowns)?e.modelBreakdowns.map(ue):[],...n?{agents:n}:{},...e.metadata&&typeof e.metadata=="object"?{metadata:{agents:U(e.metadata.agents)}}:{}}}function he(s){const e=s??{};return{...e,ok:e.ok===!0,days:Array.isArray(e.days)?e.days.map(de):void 0}}function C(s){const e=s.toLowerCase();return e.includes("opus")?"var(--model-opus)":e.includes("sonnet")?"var(--model-sonnet)":e.includes("haiku")?"var(--model-haiku)":e.includes("gpt-5")||e.includes("gpt5")?"var(--model-gpt5)":e.includes("gpt")?"var(--model-gpt)":e.includes("gemini")?"var(--model-gemini)":e.includes("codex")?"var(--model-codex)":e.includes("fable")||e.includes("mythos")?"var(--model-fable)":"var(--model-other)"}function me(s){let e=0,n=0,o=0,r=0;const m=new Map;for(const l of s){e+=l.totalCost,n+=l.totalTokens,o+=l.inputTokens+l.outputTokens,r+=l.cacheReadTokens;for(const d of l.modelBreakdowns)m.set(d.modelName,(m.get(d.modelName)??0)+d.cost)}return{totalCost:e,totalTok:n,inOut:o,cacheRead:r,modelCosts:m}}function pe(s){return Array.from(s.entries()).sort((e,n)=>n[1]-e[1])}function q(s){return s.slice().sort((e,n)=>n.cost-e.cost)}function v(s,e){return e>0?s/e*100:0}function ge(s){var n,o;return ce(s.agents)??((o=(n=s.metadata)==null?void 0:n.agents)!=null&&o.length?s.metadata.agents.join(" · "):null)}function fe(){let s=0;return{begin(){const e=++s;return()=>e===s},cancel(){s++}}}function xe(s){const[e,n]=i.useState(null),[o,r]=i.useState(!1),m=i.useState(fe)[0],l=i.useRef(!1),d=(f=!1)=>{if(f&&!Z(l.current))return;const x=m.begin();l.current=f;const j=s;r(!0);const h=`/api/ccusage?since=${_(j)}${f?"&refresh=1":""}`;fetch(h).then(c=>c.json()).then(c=>{x()&&n({range:j,resp:he(c)})}).catch(()=>{x()&&n({range:j,resp:{ok:!1,reason:"unreachable"}})}).finally(()=>{x()&&(l.current=!1,r(!1))})};return i.useEffect(()=>(d(!1),()=>m.cancel()),[s]),{landed:e,loading:o,reload:()=>d(!0)}}function je({selectedDay:s}){const e=ge(s);return a.jsxs("div",{className:"uh-detail",children:[a.jsxs("div",{className:"uh-detail-head",children:[a.jsx("span",{className:"uh-detail-date",children:s.period}),a.jsx("span",{className:"uh-detail-cost",children:p(s.totalCost)}),e&&a.jsx("span",{className:"uh-detail-agents",title:e,children:e})]}),a.jsxs("div",{className:"uh-detail-mini",children:[a.jsx(b,{label:"input",val:g(s.inputTokens)}),a.jsx(b,{label:"output",val:g(s.outputTokens)}),a.jsx(b,{label:"cache write",val:g(s.cacheCreationTokens)}),a.jsx(b,{label:"cache read",val:g(s.cacheReadTokens)})]}),a.jsx("div",{className:"uh-detail-models",children:q(s.modelBreakdowns).map(n=>{const o=v(n.cost,s.totalCost);return a.jsxs("div",{className:"uh-model-row",title:n.modelName,children:[a.jsxs("span",{className:"uh-model-name",children:[a.jsx("span",{className:"uh-legend-dot",style:{background:C(n.modelName)}}),a.jsx("span",{className:"uh-model-label",children:G(n.modelName)})]}),a.jsx("span",{className:"uh-model-bar",children:a.jsx("span",{className:"uh-model-bar-fill",style:{width:`${o}%`,background:C(n.modelName)}})}),a.jsx("span",{className:"uh-model-cost",children:p(n.cost)})]},n.modelName)})})]})}function b({label:s,val:e}){return a.jsxs("div",{className:"uh-ministat",children:[a.jsx("span",{className:"uh-ministat-val",children:e}),a.jsx("span",{className:"uh-ministat-label",children:s})]})}const ke=[7,14,30,90];function be({onClose:s,providers:e}){var P;const[n,o]=i.useState(30),[r,m]=i.useState(null),{landed:l,loading:d,reload:f}=xe(n),x=i.useRef(null),j=ee(s,{focusRef:x}),S=se(s),h=re({loading:d,want:n,answer:l&&{range:l.range,ok:l.resp.ok===!0,days:((P=l.resp.days)==null?void 0:P.length)??0}}),c=h.phase==="chart"?l.resp.days??[]:[],M=i.useRef(null),H=ae(h.phase!=="error",M),V=i.useMemo(()=>c.reduce((t,k)=>Math.max(t,k.totalCost),0),[c]),A=i.useRef(null);i.useLayoutEffect(()=>{const t=A.current;t&&(t.scrollLeft=t.scrollWidth)},[c]);const{totalCost:T,totalTok:W,inOut:z,cacheRead:D,modelCosts:L}=i.useMemo(()=>me(c),[c]),J=i.useMemo(()=>pe(L),[L]),N=i.useMemo(()=>I(c),[c]),K=N.length>1,B=r?c.find(t=>t.period===r)??null:null;return a.jsx("div",{className:"modal-backdrop",...S,role:"presentation",children:a.jsxs("div",{ref:j,className:"modal uh-modal",onClick:t=>t.stopPropagation(),role:"dialog","aria-modal":"true","aria-label":"Usage history",children:[a.jsxs("header",{className:"uh-head",children:[a.jsxs("div",{className:"uh-titlewrap",children:[a.jsx("div",{className:"uh-title",children:"Usage history"}),a.jsx("div",{className:"uh-sub",children:h.stale?"running ccusage…":te(e,N.map(t=>t.id))})]}),a.jsx("div",{className:"uh-range",role:"group","aria-label":"Range",children:ke.map(t=>a.jsxs("button",{type:"button","aria-pressed":n===t,className:"uh-range-btn",onClick:()=>{o(t),m(null)},children:[t,"d"]},t))}),a.jsx("button",{ref:M,className:"glyph-btn uh-reload",onClick:f,...E(d),title:"Re-run ccusage","aria-label":"Reload",children:d?"…":a.jsx(ne,{})}),a.jsx("button",{ref:x,className:"glyph-btn uh-close",onClick:s,"aria-label":"Close",children:"×"})]}),h.phase==="busy"?a.jsxs("div",{className:"uh-status uh-status-wait","aria-busy":"true",children:[a.jsx("span",{className:"uh-wait-word",children:l?"running ccusage…":"running ccusage… (first run downloads the package)"}),a.jsx("span",{className:"uh-wait-axis","aria-hidden":!0})]}):h.phase==="error"?a.jsxs("div",{className:"uh-status uh-err",title:le(l.resp)||void 0,children:[oe(l.resp,"ccusage did not report usage"),a.jsx("div",{children:a.jsx("button",{className:"btn uh-retry",onClick:()=>{H(),f()},...E(d),children:d?"trying…":"Try again"})})]}):h.phase==="empty"?a.jsx("div",{className:`uh-status${h.stale?" uh-stale":""}`,"aria-busy":h.stale||void 0,children:"no usage in this range"}):a.jsxs("div",{className:h.stale?"uh-stale":void 0,"aria-busy":h.stale||void 0,children:[a.jsxs("div",{className:"uh-totals",children:[a.jsx(y,{label:"total cost",val:p(T),accent:!0}),a.jsx(y,{label:"tokens",val:g(W)}),a.jsx(y,{label:"input+output",val:g(z)}),a.jsx(y,{label:"cache reads",val:g(D)})]}),K&&a.jsxs("div",{className:"uh-agents",role:"group","aria-label":"Cost by CLI",children:[a.jsx("div",{className:"uh-agent-bar","aria-hidden":"true",children:N.map(t=>a.jsx("span",{className:"uh-agent-seg",style:{width:`${v(t.cost,T)}%`,background:F(t.id)}},t.id))}),a.jsx("div",{className:"uh-agent-keys",children:N.map(t=>a.jsxs("span",{className:"uh-agent-key",title:`${t.id} · ${g(t.tokens)} tokens`,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:F(t.id)}}),O(t.id),a.jsx("span",{className:"uh-agent-cost",children:p(t.cost)}),a.jsx("span",{className:"uh-agent-share",children:ie(t.cost,T)})]},t.id))})]}),a.jsx("div",{ref:A,className:"uh-chart",role:"group","aria-label":"Daily cost by model",children:c.map((t,k)=>{const Q=v(t.totalCost,V),w=t.period===r,X=`${t.period}, ${p(t.totalCost)}`;return a.jsxs("button",{className:`uh-bar-col${w?" sel":""}`,onClick:()=>m(w?null:t.period),"aria-pressed":w,"aria-label":X,title:`${t.period} · ${p(t.totalCost)}`,style:{flexBasis:`${100/c.length}%`,"--uh-i":k,"--uh-step":`${Math.min(10,200/Math.max(c.length,1))}ms`},children:[a.jsx("div",{className:"uh-bar",style:{height:`${Math.max(Q,t.totalCost>0?2:0)}%`},children:q(t.modelBreakdowns).map(R=>{const Y=v(R.cost,t.totalCost);return a.jsx("div",{className:"uh-bar-seg",style:{height:`${Y}%`,background:C(R.modelName)}},R.modelName)})}),a.jsx("span",{className:"uh-bar-label",children:t.period.slice(5)})]},t.period)})}),a.jsx("div",{className:"uh-legend",children:J.map(([t,k])=>a.jsxs("span",{className:"uh-legend-item",title:t,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:C(t)}}),G(t)," ",a.jsx("span",{className:"uh-legend-cost",children:p(k)})]},t))}),B&&a.jsx(je,{selectedDay:B})]})]})})}function y({label:s,val:e,accent:n}){return a.jsxs("div",{className:"uh-stat",children:[a.jsx("span",{className:`uh-stat-val${n?" accent":""}`,children:e}),a.jsx("span",{className:"uh-stat-label",children:s})]})}export{be as default};