@token-dashboard/claude-code-usage-uploader 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - `~/.config/claude/projects`
8
8
  - `~/.claude/projects`
9
9
 
10
- 上传前只保留 session、项目展示名、路径 hash、模型、token 计数和费用字段,不上传 `message.content`。
10
+ 上传前只保留 session、项目展示名、路径 hash、模型、token 计数、费用字段,以及结构化 diff 的新增/删除/变更行数,不上传 `message.content` 或代码内容。
11
11
 
12
12
  初始化时默认从 `~/.claude.json` 的 `oauthAccount.emailAddress` 读取用户邮箱;已有绑定或 `--email` 参数优先。
13
13
 
package/dist/cli.mjs CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:os";import{createInterface as o}from"node:readline";import s from"node:readline/promises";import{stdout as i,stdin as a}from"node:process";import{Buffer as r}from"node:buffer";import{createHash as l}from"node:crypto";import{DatabaseSync as c}from"node:sqlite";import{spawnSync as u}from"node:child_process";import{fileURLToPath as d}from"node:url";const h=t.join(n.homedir(),".claude.json"),p=/^[^@\s]+@[^@\s]+\.[^@\s]+$/;function m(e){return Boolean(e.employeeId||e.employeeEmail||e.employeeName)}function g(t=h){try{return function(e){const t=e?.oauthAccount?.emailAddress;if("string"!=typeof t)return null;const n=t.trim();return p.test(n)?n:null}(JSON.parse(e.readFileSync(t,"utf8")))}catch{return null}}async function f(e,t){const n=s.createInterface({input:a,output:i});try{const o=t?` [${t}]`:"";return(await n.question(`${e}${o}: `)).trim()||t||null}finally{n.close()}}function y(){return Date.now()/1e3}function b(e){return new Promise(t=>setTimeout(t,e))}function S(e){return JSON.stringify(E(e))}function E(e){return Array.isArray(e)?e.map(E):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,E(e[t])])):e}function k(e){return l("sha1").update(r.from(String(e),"utf8")).digest("hex")}class v{constructor(e,n,o){this.collectorIdentity=e,this.sourceRoot=n,this.relpath=o,this.fileSessionId=t.basename(o,".jsonl")}normalizeRecord(e,n){const o=e?.message&&"object"==typeof e.message?e.message:null,s=o?.usage&&"object"==typeof o.usage?o.usage:null;if(!s)return null;if(!0===e.isApiErrorMessage)return null;if(o.role&&"assistant"!==o.role)return null;const i=function(e){if(!e)return null;const t=String(e).replace("Z","+00:00"),n=Date.parse(t);return Number.isNaN(n)?null:n}(e.timestamp);if(null==i)return null;const a=P(s.input_tokens),r=P(s.output_tokens),l=_(s.cache_creation_input_tokens),c=_(s.cache_read_input_tokens),u=[a,r,l,c].reduce((e,t)=>e+(t??0),0);if(u<=0)return null;const d=String(e.sessionId||this.fileSessionId),h=function(e){if("string"!=typeof e||!e.trim())return{cwdHash:null,cwdBasename:null};const n=e.trim();return{cwdHash:k(n),cwdBasename:t.basename(n)||null}}(e.cwd),p=h.cwdBasename||function(e){const t=(e.split("/")[0]||"").replace(/^-+/,"").replace(/-+$/,"");return t.split("-").filter(Boolean).at(-1)||t||null}(this.relpath),m="string"==typeof o.id&&o.id.trim()?o.id.trim():null,g="string"==typeof e.requestId&&e.requestId.trim()?e.requestId.trim():null,f=function({collectorId:e,sourceRoot:t,relpath:n,lineNo:o,messageId:s,requestId:i}){return k(s&&i?`${s}|${i}`:s?`${e}|${s}`:`${e}|${t}|${n}|${o}`)}({collectorId:this.collectorIdentity.collectorId,sourceRoot:this.sourceRoot,relpath:this.relpath,lineNo:n,messageId:m,requestId:g});return{session:{sessionId:d,sessionTimestamp:i,sourceRoot:this.sourceRoot,sourceFileRelpath:T(this.relpath),projectName:p,cwdHash:h.cwdHash,cwdBasename:h.cwdBasename,cliVersion:I(e.version)},event:{eventUid:f,sessionId:d,sourceRoot:this.sourceRoot,sourceFileRelpath:T(this.relpath),lineNo:n,timestamp:i,projectName:p,cwdHash:h.cwdHash,cwdBasename:h.cwdBasename,cliVersion:I(e.version),model:I(o.model),messageId:m,requestId:g,inputTokens:a,outputTokens:r,cacheCreationInputTokens:l,cacheReadInputTokens:c,totalTokens:u,reportedCostUsd:P(e.costUSD),rawUsageJson:S(s)}}}processLine(e,t){let n;try{n=JSON.parse(t)}catch{return{sessions:[],events:[]}}const o=this.normalizeRecord(n,e);return o?{sessions:[o.session],events:[o.event]}:{sessions:[],events:[]}}}function T(e){const t=e.split("/").filter(Boolean),n=t.pop()||"unknown.jsonl",o=t.join("/");return o?`${k(o).slice(0,12)}/${n}`:n}function P(e){const t=Number(e);return Number.isFinite(t)&&t>=0?t:null}function _(e){const t=Number(e);return Number.isFinite(t)&&t>=0?t:0}function I(e){return"string"==typeof e&&e.trim()?e.trim():null}const L="Claude Code 用量上报",w="claude-code-usage-uploader",N="projects",$="legacy_projects",C=[t.join(n.homedir(),".config","claude","projects"),t.join(n.homedir(),".claude","projects")],D=t.join(n.homedir(),".claude-code-usage-uploader"),B=t.join(D,"config.json"),R=t.join(D,"state.sqlite"),j=t.join(D,"logs"),U=t.join(j,"stdout.log"),F=t.join(j,"stderr.log"),A=t.join(D,"app");t.join(A,"current");const O=t.join(D,"bin");t.join(O,w),t.join(n.homedir(),"bin",w);const x="com.token-dashboard.claude-code-usage-uploader";t.join(D,"launchd",`${x}.plist`);const M=t.join(n.homedir(),"Library","LaunchAgents",`${x}.plist`),H=t.join(n.homedir(),".claude-code-usage-uploader-collector-id"),q="http://101.126.66.51:8086",W=1e6;class Y{constructor(n){this.dbPath=n,e.mkdirSync(t.dirname(n),{recursive:!0}),this.db=new c(n,{readBigInts:!0}),this.initSchema()}close(){this.db.close()}initSchema(){this.db.exec("\n CREATE TABLE IF NOT EXISTS identity_config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at REAL NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS pending_batches (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n batch_key TEXT NOT NULL UNIQUE,\n status TEXT NOT NULL,\n payload_json TEXT NOT NULL,\n payload_bytes INTEGER NOT NULL DEFAULT 0,\n session_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0,\n event_count INTEGER NOT NULL DEFAULT 0,\n attempt_count INTEGER NOT NULL DEFAULT 0,\n next_retry_at REAL,\n last_error TEXT,\n created_at REAL NOT NULL,\n updated_at REAL NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS upload_checkpoint (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at REAL NOT NULL\n );\n "),this.ensureIngestionFilesTable()}ensureIngestionFilesTable(){if(this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ingestion_files'").get()){if(!this.db.prepare("PRAGMA table_info(ingestion_files)").all().some(e=>"source_root"===e.name)){this.db.exec("BEGIN");try{this.db.exec("ALTER TABLE ingestion_files RENAME TO ingestion_files_legacy"),this.createIngestionFilesTable(),this.db.prepare("\n INSERT INTO ingestion_files(\n source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n )\n SELECT 'sessions', relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n FROM ingestion_files_legacy\n ").run(),this.db.exec("DROP TABLE ingestion_files_legacy"),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}else this.createIngestionFilesTable()}createIngestionFilesTable(){this.db.exec("\n CREATE TABLE IF NOT EXISTS ingestion_files (\n source_root TEXT NOT NULL,\n relpath TEXT NOT NULL,\n file_size INTEGER NOT NULL,\n file_mtime_ns INTEGER NOT NULL,\n last_line_no INTEGER NOT NULL,\n state_json TEXT,\n updated_at REAL NOT NULL,\n PRIMARY KEY (source_root, relpath)\n );\n ")}getIdentity(){const e=this.db.prepare("SELECT key, value FROM identity_config").all();return Object.fromEntries(e.map(e=>[e.key,e.value]))}setIdentity(e){const t=y(),n=this.db.prepare("\n INSERT INTO identity_config(key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n "),o=this.db.prepare("DELETE FROM identity_config WHERE key = ?");this.db.exec("BEGIN");try{for(const[s,i]of Object.entries(e))null==i?o.run(s):n.run(s,i,t);this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getCheckpoint(e){const t=this.db.prepare("SELECT value FROM upload_checkpoint WHERE key = ?").get(e);return t?.value??null}setCheckpoint(e,t){const n=y();this.db.prepare("\n INSERT INTO upload_checkpoint(key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n ").run(e,t,n)}getFileState(e,t){const n=this.db.prepare("SELECT * FROM ingestion_files WHERE source_root = ? AND relpath = ?");return n.setReadBigInts(!0),n.get(e,t)??null}upsertFileState(e,t,n,o,s,i){this.db.prepare("\n INSERT INTO ingestion_files(\n source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(source_root, relpath) DO UPDATE SET\n file_size = excluded.file_size,\n file_mtime_ns = excluded.file_mtime_ns,\n last_line_no = excluded.last_line_no,\n state_json = excluded.state_json,\n updated_at = excluded.updated_at\n ").run(e,t,n,o,s,S(i),y())}getBufferingBatch(){const e=this.db.prepare("\n SELECT * FROM pending_batches\n WHERE status = 'buffering'\n ORDER BY id ASC\n LIMIT 1\n ");return e.setReadBigInts(!0),e.get()??null}saveBufferingPayload(e){const t=this.getBufferingBatch(),n=S(e),o=Buffer.byteLength(n,"utf8"),s=e.sessions?.length??0,i=e.turns?.length??0,a=e.events?.length??0,r=y();return t?this.db.prepare("\n UPDATE pending_batches\n SET payload_json = ?, payload_bytes = ?, session_count = ?, turn_count = ?, event_count = ?, updated_at = ?\n WHERE id = ?\n ").run(n,o,s,i,a,r,t.id):this.db.prepare("\n INSERT INTO pending_batches(\n batch_key, status, payload_json, payload_bytes, session_count, turn_count, event_count,\n attempt_count, created_at, updated_at\n ) VALUES (?, 'buffering', ?, ?, ?, ?, ?, 0, ?, ?)\n ").run(`${Date.now().toString(16)}${Math.random().toString(16).slice(2,14)}`,n,o,s,i,a,r,r),this.getBufferingBatch()}sealStaleBatches(e=!1){const t=this.db.prepare("SELECT id, created_at FROM pending_batches WHERE status = 'buffering'");t.setReadBigInts(!0);const n=t.all();let o=0;const s=y(),i=this.db.prepare("\n UPDATE pending_batches\n SET status = 'pending', updated_at = ?\n WHERE id = ?\n ");for(const t of n)(e||s-Number(t.created_at)>=60)&&(i.run(s,t.id),o+=1);return o}sealBufferIfThresholdHit(){const e=this.getBufferingBatch();if(!e)return!1;const t=Number(e.event_count)>=200||Number(e.payload_bytes)>=W||y()-Number(e.created_at)>=60;if(t){const t=y();this.db.prepare("\n UPDATE pending_batches\n SET status = 'pending', updated_at = ?\n WHERE id = ?\n ").run(t,e.id)}return t}iterPendingBatches(){const e=this.db.prepare("\n SELECT * FROM pending_batches\n WHERE status = 'pending'\n ORDER BY created_at ASC, id ASC\n ");return e.setReadBigInts(!0),e.all()}markBatchUploaded(e){this.db.prepare("DELETE FROM pending_batches WHERE id = ?").run(e)}markBatchFailed(e,t,n){this.db.prepare("\n UPDATE pending_batches\n SET attempt_count = ?, last_error = ?, updated_at = ?\n WHERE id = ?\n ").run(t,String(n).slice(0,1e3),y(),e)}resetBackfillState(){this.db.exec("BEGIN");try{this.db.prepare("DELETE FROM ingestion_files").run(),this.db.prepare("DELETE FROM pending_batches").run(),this.db.prepare("DELETE FROM upload_checkpoint").run(),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getQueueStats(){const e=this.db.prepare("\n SELECT\n COALESCE(SUM(CASE WHEN status = 'buffering' THEN 1 ELSE 0 END), 0) AS buffering_batch_count,\n COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count = 0 THEN 1 ELSE 0 END), 0) AS pending_batch_count,\n COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count > 0 THEN 1 ELSE 0 END), 0) AS retrying_batch_count,\n COALESCE(SUM(session_count), 0) AS queued_sessions,\n COALESCE(SUM(turn_count), 0) AS queued_turns,\n COALESCE(SUM(event_count), 0) AS queued_events,\n MIN(created_at) AS oldest_created_at\n FROM pending_batches\n ");e.setReadBigInts(!0);const t=e.get()??{},n=null==t.oldest_created_at?null:Number(t.oldest_created_at);return{bufferingBatchCount:Number(t.buffering_batch_count??0),pendingBatchCount:Number(t.pending_batch_count??0),retryingBatchCount:Number(t.retrying_batch_count??0),queuedSessions:Number(t.queued_sessions??0),queuedTurns:Number(t.queued_turns??0),queuedEvents:Number(t.queued_events??0),oldestPendingAgeSeconds:null==n?null:Math.max(0,Math.floor(y()-n))}}}class Q{constructor({claudeProjectsDirs:e,stateDbPath:t,backendUrl:n,intervalSeconds:o,persistentCollectorIdPath:s=H,claudeJsonPath:i,scanChunkMaxEvents:a=50,scanChunkMaxBytes:r=262144}){var l;this.claudeProjectsDirs=(l=e,[...new Set((Array.isArray(l)?l:[l]).filter(Boolean).map(String))]),this.stateDb=new Y(t),this.backendUrl=n?.replace(/\/+$/,"")||null,this.intervalSeconds=o,this.persistentCollectorIdPath=s,this.claudeJsonPath=i,this.scanChunkMaxEvents=a,this.scanChunkMaxBytes=r,this.identity=this.ensureIdentity()}close(){this.stateDb.close()}ensureIdentity(){const t=this.stateDb.getIdentity();if(!t.employeeEmail){const e=g(this.claudeJsonPath);e&&(t.employeeEmail=e)}if(!t.collectorId){const n=function(t){try{return e.readFileSync(t,"utf8").trim()||null}catch{return null}}(this.persistentCollectorIdPath);t.collectorId=n??`${Date.now().toString(16)}${Math.random().toString(16).slice(2,14)}`}return function(t,n){const o=`${t}.${process.pid}.tmp`;e.writeFileSync(o,n,"utf8"),e.renameSync(o,t)}(this.persistentCollectorIdPath,t.collectorId),t.deviceId||(t.deviceId=`${n.hostname()}-${Math.random().toString(16).slice(2,14)}`),t.hostname||(t.hostname=n.hostname()),this.stateDb.setIdentity(t),this.stateDb.getIdentity()}resolveIdentityValues({employeeId:e,employeeEmail:t,employeeName:n,deviceId:o,hostname:s}={}){const i=this.stateDb.getIdentity(),a=i.employeeEmail?null:g(this.claudeJsonPath);return{employeeId:void 0!==e?e:i.employeeId??null,employeeEmail:void 0!==t?t:i.employeeEmail??a??null,employeeName:void 0!==n?n:i.employeeName??null,deviceId:void 0!==o?o:i.deviceId??null,hostname:void 0!==s?s:i.hostname??null}}configureIdentity({employeeId:e=null,employeeEmail:t=null,employeeName:n=null,deviceId:o,hostname:s}={}){const i={employeeId:e,employeeEmail:t,employeeName:n};if(!m(i))throw new Error("At least one of employeeId, employeeEmail, or employeeName must be provided.");return void 0!==o&&(i.deviceId=o),void 0!==s&&(i.hostname=s),this.stateDb.setIdentity(i),this.identity=this.ensureIdentity(),this.identity}configureIdentityWithDefaults(e={}){return this.configureIdentity(this.resolveIdentityValues(e))}async configureIdentityInteractive(e={}){const t=this.resolveIdentityValues(e);return void 0===e.employeeEmail&&(t.employeeEmail=await f("Employee email",t.employeeEmail)),m(t)||(void 0===e.employeeName&&(t.employeeName=await f("Employee name",t.employeeName)),m(t)||void 0===e.employeeId&&(t.employeeId=await f("Employee ID",t.employeeId))),this.configureIdentity(t)}resetBackfillState(){this.stateDb.resetBackfillState()}getQueueStats(){return this.stateDb.getQueueStats()}getScanRoots(){return this.claudeProjectsDirs.map((e,t)=>({sourceRoot:0===t?N:1===t?$:`${N}_${t+1}`,dir:e}))}iterJsonlFiles(n){if(!e.existsSync(n))return[];const o=[],s=n=>{for(const i of e.readdirSync(n,{withFileTypes:!0})){const e=t.join(n,i.name);i.isDirectory()?s(e):i.isFile()&&i.name.endsWith(".jsonl")&&o.push(e)}};return s(n),o.sort(),o}buildSnapshot(){const n=[];for(const o of this.getScanRoots())for(const s of this.iterJsonlFiles(o.dir)){const i=t.relative(o.dir,s).split(t.sep).join("/"),a=e.statSync(s,{bigint:!0}),r=this.stateDb.getFileState(o.sourceRoot,i);r&&r.file_size===a.size&&r.file_mtime_ns===a.mtimeNs||n.push({filePath:s,relpath:i,sourceRoot:o.sourceRoot,progressPath:`${o.sourceRoot}/${i}`,snapshotSize:a.size,snapshotMtimeNs:a.mtimeNs})}return n}emptyPayload(){return{sessions:[],events:[]}}payloadHasData(e){return Boolean(e.sessions.length||e.events.length)}payloadBytes(e){return Buffer.byteLength(S(e),"utf8")}mergePayload(e,t){const n=new Map((e.sessions??[]).map(e=>[e.sessionId,e])),o=new Map((e.events??[]).map(e=>[e.eventUid,e]));for(const e of t.sessions??[])n.set(e.sessionId,e);for(const e of t.events??[])o.set(e.eventUid,e);return{sessions:[...n.values()],events:[...o.values()]}}shouldFlushChunk(e){return!!this.payloadHasData(e)&&(e.events.length>=this.scanChunkMaxEvents||this.payloadBytes(e)>=this.scanChunkMaxBytes)}wouldExceedUploadThreshold(e){return!!this.payloadHasData(e)&&(e.events.length>200||this.payloadBytes(e)>W)}appendPayloadToBuffer(e){if(!this.payloadHasData(e))return 0;const t=this.stateDb.getBufferingBatch();let n=0,o=e;if(t){const s=JSON.parse(t.payload_json),i=this.mergePayload(s,e);this.wouldExceedUploadThreshold(i)?n+=this.stateDb.sealStaleBatches(!0):o=i}return this.stateDb.saveBufferingPayload(o),n+(this.stateDb.sealBufferIfThresholdHit()?1:0)}async scanSnapshotEntries(t,{onFileProcessed:n}={}){const s={filesScanned:0,sessions:0,events:0,batchesQueued:0};for(const i of t){const{filePath:t,relpath:a,sourceRoot:r,snapshotSize:l,snapshotMtimeNs:c}=i,u=this.stateDb.getFileState(r,a);s.filesScanned+=1;let d=0;u&&l>u.file_size&&(d=Number(u.last_line_no));const h=new v(this.identity,r,a);let p=this.emptyPayload(),m=0,g=0;if(Number(l)>0){const n=e.createReadStream(t,{encoding:"utf8",start:0,end:Number(l)-1}),i=o({input:n,crlfDelay:1/0});try{for await(const e of i){if(g+=1,!e)continue;if(m=g,g<=d)continue;const t=h.processLine(g,e);s.sessions+=t.sessions.length,s.events+=t.events.length,p=this.mergePayload(p,t),this.shouldFlushChunk(p)&&(s.batchesQueued+=this.appendPayloadToBuffer(p),p=this.emptyPayload())}}finally{i.close()}}this.payloadHasData(p)&&(s.batchesQueued+=this.appendPayloadToBuffer(p)),this.stateDb.upsertFileState(r,a,l,c,m,{}),n?.({entry:i,filesProcessed:s.filesScanned,totals:s})}return s}async scanClaudeCode(){return this.scanSnapshotEntries(this.buildSnapshot())}collectorRequestBody(){return{collectorId:this.identity.collectorId,deviceId:this.identity.deviceId,hostname:this.identity.hostname??null,employeeId:this.identity.employeeId??null,employeeEmail:this.identity.employeeEmail??null,employeeName:this.identity.employeeName??null}}async postJson(e,t){if(!this.backendUrl)throw new Error("backendUrl is not configured");const n=await fetch(`${this.backendUrl}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){const e=await n.text(),t=e?`: ${e.slice(0,500)}`:"";throw new Error(`HTTP ${n.status} ${n.statusText}${t}`)}const o=await n.text();return o?JSON.parse(o):{}}async ensureRemoteRegistration(){if(!this.backendUrl)return;const e=this.stateDb.getCheckpoint("last_register_at");e&&y()-Number(e)<600||(await this.postJson("/claude-code-usage/collectors/register",this.collectorRequestBody()),this.stateDb.setCheckpoint("last_register_at",String(y())))}async flushPendingBatches({failFast:e=!1,concurrency:t=5,onBatchUploaded:n}={}){if(!this.backendUrl)return{uploadedBatches:0,failedBatches:0,lastError:null};try{await this.ensureRemoteRegistration()}catch(t){if(e)throw new Error(`Collector registration failed: ${t instanceof Error?t.message:String(t)}`);return{uploadedBatches:0,failedBatches:1,lastError:t}}let o=0,s=0,i=null;const a=this.collectorRequestBody(),r=this.stateDb.iterPendingBatches(),l=async e=>{const t=JSON.parse(e.payload_json),n={idempotencyKey:e.batch_key,collector:a,payloadSizeBytes:Number(e.payload_bytes),sessions:Array.isArray(t.sessions)?t.sessions:[],events:Array.isArray(t.events)?t.events:[]};await this.postJson("/claude-code-usage/upload",n)},c=new Set;let u=0;const d=()=>{for(;c.size<t&&u<r.length;){const t=r[u++],a=l(t).then(()=>{this.stateDb.markBatchUploaded(t.id),o+=1,n?.({row:t,uploadedBatches:o})}).catch(n=>{if(this.stateDb.markBatchFailed(t.id,Number(t.attempt_count)+1,n instanceof Error?n.message:String(n)),s+=1,i=n,e)throw n}).finally(()=>{c.delete(a)});c.add(a)}};for(d();c.size>0;){try{await Promise.race(c)}catch{if(e)break}d()}if(e&&i)throw new Error(`Upload batch failed: ${i instanceof Error?i.message:String(i)}`);return{uploadedBatches:o,failedBatches:s,lastError:i}}async runCycle({forceSeal:e=!1}={}){const t=await this.scanClaudeCode();t.batchesQueued+=this.stateDb.sealStaleBatches(e);const n=await this.flushPendingBatches();t.uploadedBatches=n.uploadedBatches,t.failedBatches=n.failedBatches;const o=this.getQueueStats();return{...t,...o,remainingQueuedBatches:o.bufferingBatchCount+o.pendingBatchCount+o.retryingBatchCount,remainingQueuedEvents:o.queuedEvents}}async runForegroundCatchUp({snapshot:e,onProgress:t}={}){const n=Date.now(),o=e??this.buildSnapshot(),s=o.length;t?.({phase:"start",totalFiles:s,filesProcessed:0,eventsParsed:0,batchesQueued:0});const i=await this.scanSnapshotEntries(o,{onFileProcessed:({entry:e,filesProcessed:n,totals:o})=>{t?.({phase:"file",file:e.progressPath??e.relpath,totalFiles:s,filesProcessed:n,eventsParsed:o.events,batchesQueued:o.batchesQueued})}});i.batchesQueued+=this.stateDb.sealStaleBatches(!0),await this.ensureRemoteRegistration();const a=this.getQueueStats(),r={totalFiles:s,filesProcessed:s,eventsParsed:i.events,sessionsParsed:i.sessions,batchesQueued:i.batchesQueued,pendingBatches:a.pendingBatchCount+a.retryingBatchCount,pendingEvents:a.queuedEvents,durationMs:Date.now()-n};return t?.({phase:"done",...r}),r}async watch(){let e=!0;for(;;){const t=await this.runCycle({forceSeal:e});console.log(`[uploader] scanned_files=${t.filesScanned} sessions=${t.sessions} events=${t.events} queued_batches=${t.batchesQueued} uploaded_batches=${t.uploadedBatches} remaining_batches=${t.remainingQueuedBatches}`),e=!1,await b(1e3*this.intervalSeconds)}}}const z=t.basename(D);function J(e){const n=t.basename(e);if(!n.startsWith(`${z}-`))return"";const o=n.slice(z.length+1);return o?`-${o}`:""}function G(o=B){const s=function(e=B){const o=t.dirname(e),s=J(o),i=t.join(o,"app"),a=t.join(i,"current"),r=`${x}${s}`;return{configFile:e,installRoot:o,appRoot:i,currentAppDir:a,backendUrl:q,intervalSeconds:30,claudeProjectsDirs:C,stateDbPath:t.join(o,t.basename(R)),nodePath:process.execPath,entryFile:"",packageSpec:"",localBinDir:t.join(o,"bin"),localBinPath:t.join(o,"bin",w),homeBinLink:t.join(n.homedir(),"bin",`${w}${s}`),stdoutLogPath:t.join(o,"logs",t.basename(U)),stderrLogPath:t.join(o,"logs",t.basename(F)),launchdLabel:r,plistPath:t.join(o,"launchd",`${r}.plist`),launchAgentPath:t.join(t.dirname(M),`${r}.plist`),persistentCollectorIdPath:t.join(n.homedir(),`${z}${s}-collector-id`),autoStartOnLogin:!0}}(o);try{const t=JSON.parse(e.readFileSync(o,"utf8"));return t&&"object"==typeof t?X({...s,...t,configFile:o}):s}catch{return s}}function X(e){const o=e.installRoot||D,s=J(o),i=e.appRoot||t.join(o,"app"),a=e.currentAppDir||t.join(i,"current"),r=e.localBinDir||t.join(o,"bin"),l=e.launchdLabel||`${x}${s}`,c=Array.isArray(e.claudeProjectsDirs)?e.claudeProjectsDirs:e.claudeProjectsDir?[e.claudeProjectsDir]:C,u=[...new Set(c.filter(Boolean).map(String))];return{...e,configFile:e.configFile||B,installRoot:o,appRoot:i,currentAppDir:a,backendUrl:e.backendUrl?.trim()?e.backendUrl.replace(/\/+$/,""):null,intervalSeconds:Number(e.intervalSeconds)||30,claudeProjectsDirs:u,stateDbPath:e.stateDbPath||t.join(o,"state.sqlite"),nodePath:e.nodePath||process.execPath,entryFile:e.entryFile||"",packageSpec:e.packageSpec||"",localBinDir:r,localBinPath:e.localBinPath||t.join(r,w),homeBinLink:e.homeBinLink||t.join(n.homedir(),"bin",`${w}${s}`),stdoutLogPath:e.stdoutLogPath||t.join(o,"logs","stdout.log"),stderrLogPath:e.stderrLogPath||t.join(o,"logs","stderr.log"),launchdLabel:l,plistPath:e.plistPath||t.join(o,"launchd",`${l}.plist`),launchAgentPath:e.launchAgentPath||t.join(t.dirname(M),`${l}.plist`),persistentCollectorIdPath:e.persistentCollectorIdPath||t.join(n.homedir(),`${z}${s}-collector-id`),autoStartOnLogin:void 0===e.autoStartOnLogin||Boolean(e.autoStartOnLogin)}}function V(n){const o=X(n);return e.mkdirSync(t.dirname(o.configFile),{recursive:!0}),e.mkdirSync(t.dirname(o.stdoutLogPath),{recursive:!0}),e.writeFileSync(o.configFile,`${S({backendUrl:o.backendUrl,intervalSeconds:o.intervalSeconds,claudeProjectsDirs:o.claudeProjectsDirs,stateDbPath:o.stateDbPath,installRoot:o.installRoot,appRoot:o.appRoot,currentAppDir:o.currentAppDir,nodePath:o.nodePath,entryFile:o.entryFile,packageSpec:o.packageSpec,localBinDir:o.localBinDir,localBinPath:o.localBinPath,homeBinLink:o.homeBinLink,stdoutLogPath:o.stdoutLogPath,stderrLogPath:o.stderrLogPath,launchdLabel:o.launchdLabel,plistPath:o.plistPath,launchAgentPath:o.launchAgentPath,persistentCollectorIdPath:o.persistentCollectorIdPath,autoStartOnLogin:o.autoStartOnLogin})}\n`),o}function K(e,t={}){return X({...G(e),...t,configFile:e})}function Z(t,n){return{configExists:e.existsSync(t.configFile),loaded:n.loaded,running:n.running,pid:n.pid,state:n.state,lastExitCode:n.lastExitCode,backendUrl:t.backendUrl,intervalSeconds:t.intervalSeconds,claudeProjectsDirs:t.claudeProjectsDirs,configFile:t.configFile,stateDbPath:t.stateDbPath,stdoutLogPath:t.stdoutLogPath,stderrLogPath:t.stderrLogPath,plistPath:t.plistPath,launchAgentPath:t.launchAgentPath,label:t.launchdLabel,autoStartOnLogin:t.autoStartOnLogin,onlineThresholdSeconds:600}}const ee="# Added by claude-code-usage-uploader",te=[t.join(n.homedir(),".zshrc"),t.join(n.homedir(),".bash_profile")];function ne(n=import.meta.url){let o=t.dirname(d(n));for(;;){const n=t.join(o,"package.json");if(e.existsSync(n))return o;const s=t.dirname(o);if(s===o)throw new Error("package.json not found from current runtime");o=s}}function oe(e){return String(e).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}class se{constructor(e){this.runtime=e}ensureMacos(){if("darwin"!==process.platform)throw new Error("launchd management is only supported on macOS.")}domainTarget(){return`gui/${process.getuid()}`}serviceTarget(){return`${this.domainTarget()}/${this.runtime.launchdLabel}`}run(e,{check:t=!0}={}){const n=u(e[0],e.slice(1),{encoding:"utf8"});if(t&&0!==n.status)throw new Error(n.stderr?.trim()||n.stdout?.trim()||`command failed: ${e.join(" ")}`);return n}ensurePlist(){if(this.ensureMacos(),!this.runtime.entryFile||!e.existsSync(this.runtime.entryFile))throw new Error(`installed runtime entry not found: ${this.runtime.entryFile||"(empty)"}`);const n=function(e){const t=[e.nodePath,e.entryFile,"--config-file",e.configFile,"run"];return`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${oe(e.launchdLabel)}</string>\n <key>ProgramArguments</key>\n <array>\n${t.map(e=>` <string>${oe(e)}</string>`).join("\n")}\n </array>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n <key>ProcessType</key>\n <string>Background</string>\n <key>WorkingDirectory</key>\n <string>${oe(e.installRoot)}</string>\n <key>StandardOutPath</key>\n <string>${oe(e.stdoutLogPath)}</string>\n <key>StandardErrorPath</key>\n <string>${oe(e.stderrLogPath)}</string>\n <key>EnvironmentVariables</key>\n <dict>\n <key>NODE_NO_WARNINGS</key>\n <string>1</string>\n </dict>\n </dict>\n</plist>\n`}(this.runtime);e.mkdirSync(t.dirname(this.runtime.plistPath),{recursive:!0}),e.mkdirSync(t.dirname(this.runtime.stdoutLogPath),{recursive:!0}),e.writeFileSync(this.runtime.plistPath,n),this.syncLaunchAgent(n)}syncLaunchAgent(n){const o=this.runtime.launchAgentPath;if(o)return this.runtime.autoStartOnLogin?(e.mkdirSync(t.dirname(o),{recursive:!0}),void e.writeFileSync(o,n)):void(e.existsSync(o)&&e.rmSync(o,{force:!0}))}start(){this.ensurePlist(),this.stop(),this.run(["launchctl","bootstrap",this.domainTarget(),this.runtime.plistPath])}stop(){this.ensureMacos(),this.run(["launchctl","bootout",this.serviceTarget()],{check:!1})}restart(){this.start()}status(){this.ensureMacos();const e=this.run(["launchctl","print",this.serviceTarget()],{check:!1});return 0!==e.status?{loaded:!1,running:!1,pid:null,state:null,lastExitCode:null}:function(e){const t=e.match(/^\s*state = ([^\n]+)$/m)?.[1]?.trim()??null,n=e.match(/^\s*pid = (\d+)$/m)?.[1]??null,o=e.match(/^\s*last exit code = (-?\d+)$/m)?.[1]??null;return{loaded:!0,running:"running"===t||null!=n,pid:n?Number(n):null,state:t,lastExitCode:o?Number(o):null}}(e.stdout)}uninstall(){if(this.stop(),e.existsSync(this.runtime.plistPath)&&e.unlinkSync(this.runtime.plistPath),this.runtime.launchAgentPath&&e.existsSync(this.runtime.launchAgentPath)&&e.unlinkSync(this.runtime.launchAgentPath),e.existsSync(this.runtime.homeBinLink)&&e.lstatSync(this.runtime.homeBinLink).isSymbolicLink())try{e.realpathSync.native(this.runtime.homeBinLink)===this.runtime.localBinPath&&e.unlinkSync(this.runtime.homeBinLink)}catch{e.unlinkSync(this.runtime.homeBinLink)}}}const ie=Object.freeze({collectorId:"local-usage"}),ae=new Set(["today","7d","30d","all"]),re=/^\d{4}-\d{2}-\d{2}$/;function le(){return Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}function ce(){return{inputTokens:0,outputTokens:0,cacheCreationInputTokens:0,cacheReadInputTokens:0,totalTokens:0,reportedCostUsd:0}}function ue(e){return{model:e.model??"(unknown)",inputTokens:e.inputTokens??0,outputTokens:e.outputTokens??0,cacheCreationInputTokens:e.cacheCreationInputTokens??0,cacheReadInputTokens:e.cacheReadInputTokens??0,totalTokens:e.totalTokens??0,reportedCostUsd:e.reportedCostUsd??0}}function de(e,t){e.inputTokens+=t.inputTokens,e.outputTokens+=t.outputTokens,e.cacheCreationInputTokens+=t.cacheCreationInputTokens,e.cacheReadInputTokens+=t.cacheReadInputTokens,e.totalTokens+=t.totalTokens,e.reportedCostUsd+=t.reportedCostUsd}function he(e,t){const n=new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(new Date(e)),o=Object.fromEntries(n.map(e=>[e.type,e.value]));return`${o.year}-${o.month}-${o.day}`}function pe(e,t){if(!re.test(String(e)))throw new Error(`${t} must be in YYYY-MM-DD format`);const[n,o,s]=String(e).split("-"),i=Number(n),a=Number(o),r=Number(s),l=new Date(Date.UTC(i,a-1,r));if(Number.isNaN(l.getTime())||l.getUTCFullYear()!==i||l.getUTCMonth()!==a-1||l.getUTCDate()!==r)throw new Error(`${t} must be a valid calendar date`);return`${n}-${o}-${s}`}function me(e,t){const[n,o,s]=e.split("-").map(Number),i=new Date(Date.UTC(n,o-1,s+t));return[i.getUTCFullYear(),String(i.getUTCMonth()+1).padStart(2,"0"),String(i.getUTCDate()).padStart(2,"0")].join("-")}function ge(e,t,n){return!(t&&e<t||n&&e>n)}const fe=`${L}\n\nUsage:\n ${w} init [--backend-url <url>] [--interval 30] [--yes] [--package-spec <spec>]\n ${w} bind [--email <email>] [--employee-name <name>] [--employee-id <id>] [--yes]\n ${w} clear [--yes]\n ${w} start\n ${w} stop\n ${w} restart\n ${w} status\n ${w} usage [--period <today|7d|30d|all>] [--from <date>] [--to <date>]\n ${w} logs [--lines 100]\n ${w} uninstall\n\nCommon options:\n --config-file <path> Runtime config path. Default: ${B}\n --backend-url <url> Dashboard backend URL\n --interval <seconds> Scan interval in seconds\n --claude-projects-dir <path> Claude Code projects directory. Can be repeated\n --state-db <path> Local SQLite state DB path\n --employee-id <id> Bind employee ID\n --email <email> Bind employee email\n --employee-name <name> Bind employee name\n --device-id <id> Override device ID\n --hostname <name> Override hostname\n --period <value> Usage range: today, 7d, 30d, or all\n --from <date> Usage range start date (YYYY-MM-DD)\n --to <date> Usage range end date (YYYY-MM-DD)\n --yes Skip interactive prompts\n --package-spec <spec> npm install spec used by init\n --lines <n> Number of log lines for service logs\n -v, --version Show version\n -h, --help Show help\n`;class ye extends Error{constructor(e){super(e),this.name="CliOperationalError",this.showUsage=!1}}const be={findPackageRoot:ne,installCurrentPackage:function(n,{packageRoot:o,packageSpec:s,nodePath:i=process.execPath}={}){const a=function(e,t){return t||`file:${e}`}(o,s),r=n.appRoot,l=t.join(r,`.staging-${Date.now()}`);e.mkdirSync(l,{recursive:!0}),e.writeFileSync(t.join(l,"package.json"),`${JSON.stringify({private:!0,name:"claude-code-usage-uploader-runtime"},null,2)}\n`);const c="win32"===process.platform?"npm.cmd":"npm",d=u(c,["install","--no-save","--omit=dev","--no-package-lock",a],{cwd:l,stdio:"inherit",env:{...process.env,npm_config_fund:"false",npm_config_audit:"false"}});if(d.error)throw new Error(`failed to execute ${c}: ${d.error.message}`);if(0!==d.status)throw new Error(`npm install failed for ${a}`);const h=JSON.parse(e.readFileSync(t.join(o,"package.json"),"utf8")),p=t.join(l,"node_modules",...String(h.name).split("/")),m=JSON.parse(e.readFileSync(t.join(p,"package.json"),"utf8")).bin,g="string"==typeof m?m:m?.[w]??Object.values(m??{})[0];if(!g)throw new Error("installed package does not expose the expected CLI binary");const f=n.currentAppDir;return e.rmSync(f,{recursive:!0,force:!0}),e.mkdirSync(t.dirname(f),{recursive:!0}),e.renameSync(l,f),n.nodePath=i,n.packageSpec=a,n.entryFile=t.join(f,"node_modules",...String(h.name).split("/"),g),function(t){e.mkdirSync(t.localBinDir,{recursive:!0});const n=`#!/bin/sh\nset -eu\nCONFIG_FILE="\${CLAUDE_CODE_USAGE_UPLOADER_CONFIG:-${t.configFile}}"\nexec "${t.nodePath}" "${t.entryFile}" --config-file "$CONFIG_FILE" "$@"\n`;e.writeFileSync(t.localBinPath,n,{mode:493}),e.chmodSync(t.localBinPath,493)}(n),function(n){e.mkdirSync(t.dirname(n.homeBinLink),{recursive:!0});try{(e.existsSync(n.homeBinLink)||e.lstatSync(n.homeBinLink).isSymbolicLink())&&e.rmSync(n.homeBinLink,{force:!0})}catch{}e.symlinkSync(n.localBinPath,n.homeBinLink)}(n),V(n),n},ensurePathInShellConfigs:function(t){const n=`export PATH="${t}:$PATH"`;for(const t of te)try{if(!e.existsSync(t))continue;const o=e.readFileSync(t,"utf8");if(o.includes(ee))continue;const s=o.length>0&&!o.endsWith("\n")?"\n":"";e.appendFileSync(t,`${s}\n${ee}\n${n}\n`)}catch{}},removePathFromShellConfigs:function(){for(const t of te)try{if(!e.existsSync(t))continue;const n=e.readFileSync(t,"utf8");if(!n.includes(ee))continue;const o=n.split("\n"),s=[];for(let e=0;e<o.length;e++)o[e]!==ee?s.push(o[e]):(e+1<o.length&&o[e+1].startsWith("export PATH=")&&(e+=1),s.length>0&&""===s[s.length-1]&&s.pop());e.writeFileSync(t,s.join("\n"))}catch{}},promptConfirm:async function(e,t=!1){const n=t?"y":"n",o=await f(`${e} (${t?"Y/n":"y/N"})`,n);return/^(y|yes)$/i.test(String(o??"").trim())},createUploader:e=>new Q({claudeProjectsDirs:e.claudeProjectsDirs,stateDbPath:e.stateDbPath,backendUrl:e.backendUrl,intervalSeconds:e.intervalSeconds,persistentCollectorIdPath:e.persistentCollectorIdPath}),createServiceManager:e=>new se(e)};function Se(e){const t={configFile:B,interval:void 0,yes:!1,lines:100,claudeProjectsDirs:[]},n=[];for(let o=0;o<e.length;o+=1){const s=e[o];if(!s.startsWith("-")){n.push(s);continue}if("-h"===s||"--help"===s){t.help=!0;continue}if("-v"===s||"--version"===s){t.version=!0;continue}if("--yes"===s){t.yes=!0;continue}const[i,a]=s.split("=",2);if(!new Set(["--config-file","--backend-url","--interval","--claude-projects-dir","--state-db","--employee-id","--email","--employee-name","--device-id","--hostname","--period","--from","--to","--package-spec","--lines"]).has(i))throw new Error(`Unknown argument: ${s}`);const r=a??e[++o];if(null==r||r.startsWith("-"))throw new Error(`Missing value for ${i}`);Ee(t,i,r)}return{command:n[0]??null,subcommand:n[1]??null,extraPositionals:n.slice(2),options:t}}function Ee(e,t,n){switch(t){case"--config-file":e.configFile=n;break;case"--backend-url":e.backendUrl=n;break;case"--interval":e.interval=ke(n,"--interval");break;case"--claude-projects-dir":e.claudeProjectsDirs.push(n);break;case"--state-db":e.stateDbPath=n;break;case"--employee-id":e.employeeId=n;break;case"--email":e.employeeEmail=n;break;case"--employee-name":e.employeeName=n;break;case"--device-id":e.deviceId=n;break;case"--hostname":e.hostname=n;break;case"--period":e.period=n;break;case"--from":e.from=n;break;case"--to":e.to=n;break;case"--package-spec":e.packageSpec=n;break;case"--lines":e.lines=ke(n,"--lines");break;default:throw new Error(`Unknown argument: ${t}`)}}function ke(e,t){const n=Number.parseInt(String(e),10);if(!Number.isFinite(n)||n<=0)throw new Error(`${t} must be a positive integer`);return n}function ve(e){const t={};return void 0!==e.backendUrl&&(t.backendUrl=e.backendUrl),void 0!==e.interval&&(t.intervalSeconds=e.interval),e.claudeProjectsDirs?.length&&(t.claudeProjectsDirs=e.claudeProjectsDirs),void 0!==e.stateDbPath&&(t.stateDbPath=e.stateDbPath),t}function Te(e){const t={};return void 0!==e.employeeId&&(t.employeeId=e.employeeId),void 0!==e.employeeEmail&&(t.employeeEmail=e.employeeEmail),void 0!==e.employeeName&&(t.employeeName=e.employeeName),void 0!==e.deviceId&&(t.deviceId=e.deviceId),void 0!==e.hostname&&(t.hostname=e.hostname),t}function Pe(e){if(!m(e.identity))throw new ye(`No employee identity is bound yet. Run \`${w} bind\` or rerun \`init\` with --email.`)}function _e(e){console.log("Identity binding:"),console.log(` collectorId: ${e.collectorId}`),console.log(` employeeEmail: ${e.employeeEmail??"-"}`),console.log(` employeeName: ${e.employeeName??"-"}`),console.log(` employeeId: ${e.employeeId??"-"}`),console.log(` deviceId: ${e.deviceId??"-"}`),console.log(` hostname: ${e.hostname??"-"}`)}function Ie(t,n){return e.existsSync(t)?e.readFileSync(t,"utf8").split(/\r?\n/).filter((e,t,n)=>!(t===n.length-1&&""===e)).slice(-n):[]}async function Le(e,t){const n=Te(t),o=t.yes?e.configureIdentityWithDefaults(n):await e.configureIdentityInteractive(n);return Pe(e),o}function we(e){switch(e.phase){case"start":return void console.log(`[scan] start files=${e.totalFiles}`);case"file":return void console.log(`[scan] file ${e.filesProcessed}/${e.totalFiles} current=${e.file} events=${e.eventsParsed}`);case"done":return void console.log(`[scan] complete files=${e.filesProcessed}/${e.totalFiles} events=${e.eventsParsed} pending_batches=${e.pendingBatches} duration=${t=e.durationMs,`${Math.max(0,Math.round(t/1e3))}s`}`)}var t}function Ne(e){return Number(e??0).toLocaleString("en-US")}function $e(e){return`$${Number(e??0).toFixed(4)}`}async function Ce(n){const s=K(n.configFile,ve(n));console.log("Scanning local Claude Code usage..."),function(e){if(0===e.eventCount)return void console.log("No local Claude Code usage found for selected range.");const t=e.days.map(e=>[e.date,e.models.join(", "),Ne(e.inputTokens),Ne(e.outputTokens),Ne(e.cacheCreationInputTokens),Ne(e.cacheReadInputTokens),Ne(e.totalTokens),$e(e.reportedCostUsd)]);t.push(["Total","",Ne(e.summary.inputTokens),Ne(e.summary.outputTokens),Ne(e.summary.cacheCreationInputTokens),Ne(e.summary.cacheReadInputTokens),Ne(e.summary.totalTokens),$e(e.summary.reportedCostUsd)]),console.log(function(e,t,n=[]){const o=e.map((e,n)=>Math.max(e.length,...t.map(e=>String(e[n]??"").length)));return[e,...t].map(e=>e.map((e,t)=>((e,t)=>{const s=String(e??"");return"right"===n[t]?s.padStart(o[t]):s.padEnd(o[t])})(e,t)).join(" ").trimEnd()).join("\n")}(["Date","Models","Input","Output","Cache Create","Cache Read","Total Tokens","Cost USD"],t,["left","left","right","right","right","right","right","right"]))}(await async function({claudeProjectsDirs:n,period:s,from:i,to:a,now:r=Date.now(),timeZone:l=le()}={}){const c=function({period:e,from:t,to:n,now:o=Date.now(),timeZone:s=le()}={}){if(e&&(t||n))throw new Error("--period cannot be combined with --from or --to");if(e&&!ae.has(e))throw new Error("--period must be one of: today, 7d, 30d, all");const i=he(o,s);if("today"===e)return{mode:"today",from:i,to:i};if("7d"===e)return{mode:"7d",from:me(i,-6),to:i};if("30d"===e)return{mode:"30d",from:me(i,-29),to:i};if("all"===e||!e&&!t&&!n)return{mode:"all",from:null,to:null};const a=null==t?null:pe(t,"--from"),r=null==n?null:pe(n,"--to");if(a&&r&&a>r)throw new Error("--from cannot be later than --to");return{mode:"custom",from:a,to:r}}({period:s,from:i,to:a,now:r,timeZone:l}),u=n.map((e,t)=>({sourceRoot:0===t?N:$,dir:e})).flatMap(n=>function(n){if(!e.existsSync(n))return[];const o=[],s=n=>{for(const i of e.readdirSync(n,{withFileTypes:!0})){const e=t.join(n,i.name);i.isDirectory()?s(e):i.isFile()&&i.name.endsWith(".jsonl")&&o.push(e)}};return s(n),o}(n.dir).map(e=>({...n,filePath:e,relpath:t.relative(n.dir,e).split(t.sep).join("/")})));u.sort((e,t)=>e.filePath.localeCompare(t.filePath));const d={inputTokens:0,outputTokens:0,cacheCreationInputTokens:0,cacheReadInputTokens:0,totalTokens:0,reportedCostUsd:0},h=new Map,p=new Map;let m=0;for(const t of u){const n=new v(ie,t.sourceRoot,t.relpath),s=e.createReadStream(t.filePath,{encoding:"utf8"}),i=o({input:s,crlfDelay:1/0});let a=0;try{for await(const e of i){if(a+=1,!e)continue;const t=n.processLine(a,e);for(const e of t.events){const t=he(e.timestamp,l);if(!ge(t,c.from,c.to))continue;const n=ue(e);m+=1,de(d,n);const o=h.get(t)??{date:t,eventCount:0,models:new Set,...ce()};o.eventCount+=1,o.models.add(n.model),de(o,n),h.set(t,o);const s=p.get(n.model)??{model:n.model,eventCount:0,...ce()};s.eventCount+=1,de(s,n),p.set(n.model,s)}}}finally{i.close()}}return{scope:"local-machine",timezone:l,range:c,sources:{claudeProjectsDirs:n},filesScanned:u.length,eventCount:m,summary:d,days:[...h.values()].sort((e,t)=>e.date.localeCompare(t.date)).map(e=>({...e,models:[...e.models].sort((e,t)=>e.localeCompare(t))})),byModel:[...p.values()].sort((e,t)=>t.totalTokens!==e.totalTokens?t.totalTokens-e.totalTokens:e.model.localeCompare(t.model))}}({claudeProjectsDirs:s.claudeProjectsDirs,period:n.period,from:n.from,to:n.to,timeZone:le()}))}function De(t){if(!e.existsSync(t))return{collectorId:null,bufferingBatchCount:0,pendingBatchCount:0,retryingBatchCount:0,queuedSessions:0,queuedTurns:0,queuedEvents:0,oldestPendingAgeSeconds:null};const n=new Y(t);try{return{collectorId:n.getIdentity().collectorId??null,...n.getQueueStats()}}finally{n.close()}}function Be(t){if(!e.existsSync(t.configFile)||!t.entryFile)throw new ye(`Uploader is not initialized yet. Run \`${w} init\` first.`)}async function Re(n=process.argv.slice(2)){try{const{command:o,subcommand:s,extraPositionals:i,options:a}=Se(n);if(a.version)return console.log(function(n=import.meta.url){const o=ne(n);return JSON.parse(e.readFileSync(t.join(o,"package.json"),"utf8")).version??"unknown"}()),0;if(!o||a.help)return console.log(fe),0;if(!new Set(["init","bind","clear","start","stop","restart","status","usage","logs","uninstall","run"]).has(o))throw new Error(`Unknown command: ${o}`);switch(function(e,t,n){if(t)throw new Error(`Unexpected subcommand for ${e}: ${t}`);if(n.length>0)throw new Error(`Unexpected extra arguments: ${n.join(" ")}`)}(o,s,i),o){case"init":return await async function(e){const n=ve(e);n.backendUrl||(n.backendUrl=q);let o=K(e.configFile,n);if(!o.backendUrl)throw new Error("--backend-url is required for init unless already configured in config.json");o=be.installCurrentPackage(o,{packageRoot:be.findPackageRoot(),packageSpec:e.packageSpec,nodePath:process.execPath});let s=be.createServiceManager(o);s.stop();const i=be.createUploader(o);try{const n=!Object.values(Te(e)).some(e=>void 0!==e)&&m(i.identity)?i.identity:await Le(i,e);console.log(`${L} install is ready.`),console.log(`Backend: ${o.backendUrl}`),console.log(`Projects dirs: ${o.claudeProjectsDirs.join(", ")}`),console.log(`Install root: ${o.installRoot}`),console.log(`Config file: ${o.configFile}`),_e(n),console.log("Scanning local Claude Code usage...");const a=await i.runForegroundCatchUp({onProgress:we});o.autoStartOnLogin=await async function(e,t){return t.yes?e.autoStartOnLogin:be.promptConfirm("Start automatically when you log in on this Mac?",e.autoStartOnLogin)}(o,e),o=V(o),s=be.createServiceManager(o),s.start(),console.log(`${L} initialized and started.`),console.log(o.autoStartOnLogin?"Login auto-start is enabled.":"Login auto-start is disabled for future logins."),a.pendingBatches>0&&console.log(`Background service is uploading ${a.pendingBatches} batch(es) with ${a.pendingEvents} event(s).`);const r=t.dirname(o.homeBinLink);(process.env.PATH||"").split(t.delimiter).includes(r)||(be.ensurePathInShellConfigs(r),console.log(`Open a new terminal to use \`${w}\` directly.`))}finally{i.close()}}(a),0;case"bind":return await async function(t){const n=K(t.configFile,ve(t)),o=be.createUploader(n);try{_e(await Le(o,t)),await async function(t){if("darwin"!==process.platform)return!1;if(!e.existsSync(t.configFile)||!t.entryFile)return!1;const n=be.createServiceManager(t);let o;try{o=n.status()}catch{return!1}return!!o.running&&(n.restart(),!0)}(n)&&console.log("Background service restarted to apply the updated identity.")}finally{o.close()}}(a),0;case"clear":return await async function(e){const t=K(e.configFile,ve(e));if(Be(t),be.createServiceManager(t).status().running)throw new ye(`Background service is still running. Stop it first with \`${w} stop\`.`);if(!e.yes&&!await be.promptConfirm("Clear local backfill state and queued uploads?",!1))return void console.log("Clear cancelled.");const n=be.createUploader(t);try{n.resetBackfillState()}finally{n.close()}console.log("Local backfill state has been cleared.")}(a),0;case"usage":return await Ce(a),0;case"run":return await async function(e){const t=K(e.configFile,ve(e)),n=be.createUploader(t);try{Pe(n),console.log(`[run] backend=${t.backendUrl??"-"} interval=${t.intervalSeconds}s projects_dirs=${t.claudeProjectsDirs.join(",")}`),await n.watch()}finally{n.close()}}(a),0;case"start":case"stop":case"restart":case"status":case"logs":case"uninstall":return function(t,n){const o=K(n.configFile,ve(n)),s=be.createServiceManager(o);switch(t){case"start":return Be(o),s.start(),void console.log(`Service started: ${o.launchdLabel}`);case"stop":return s.stop(),void console.log(`Service stopped: ${o.launchdLabel}`);case"restart":return Be(o),s.restart(),void console.log(`Service restarted: ${o.launchdLabel}`);case"status":return void function(e,t){const n={...Z(e,t),...De(e.stateDbPath)},o=[["Config exists",n.configExists?"yes":"no"],["Loaded",n.loaded?"yes":"no"],["Running",n.running?"yes":"no"],["Auto start on login",n.autoStartOnLogin?"yes":"no"],["PID",n.pid??"-"],["State",n.state??"-"],["Last exit code",n.lastExitCode??"-"],["Collector ID",n.collectorId??"-"],["Upload URL",n.backendUrl?`${n.backendUrl}/claude-code-usage/upload`:"-"],["Register URL",n.backendUrl?`${n.backendUrl}/claude-code-usage/collectors/register`:"-"],["Interval",`${n.intervalSeconds}s`],["Projects dirs",n.claudeProjectsDirs.join(", ")],["Buffering batches",n.bufferingBatchCount],["Pending batches",n.pendingBatchCount],["Retrying batches",n.retryingBatchCount],["Queued sessions",n.queuedSessions],["Queued events",n.queuedEvents],["Config file",n.configFile],["State DB",n.stateDbPath],["Stdout log",n.stdoutLogPath],["Stderr log",n.stderrLogPath],["Service plist",n.plistPath],["LaunchAgent plist",n.launchAgentPath],["Label",n.label]];for(const[e,t]of o)console.log(`${e}: ${t}`)}(o,s.status());case"logs":{const e=Ie(o.stdoutLogPath,n.lines),t=Ie(o.stderrLogPath,n.lines);return console.log(`== stdout (${o.stdoutLogPath}) ==`),console.log(e.length?e.join("\n"):"(empty)"),console.log(`== stderr (${o.stderrLogPath}) ==`),void console.log(t.length?t.join("\n"):"(empty)")}case"uninstall":return s.uninstall(),be.removePathFromShellConfigs(),e.rmSync(o.installRoot,{recursive:!0,force:!0}),void console.log(`${L} uninstalled from ${o.installRoot}`);default:throw new Error(`Unknown lifecycle command: ${t??"(empty)"}`)}}(o,a),0}}catch(e){return console.error(e instanceof Error?e.message:String(e)),!1!==e?.showUsage&&console.error(`Run \`${w} --help\` for usage.`),1}}export{be as cliDeps,Re as main,Se as parseCliArgs};
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import{createInterface as s}from"node:readline";import o from"node:readline/promises";import{stdout as i,stdin as a}from"node:process";import{Buffer as r}from"node:buffer";import{createHash as l}from"node:crypto";import{DatabaseSync as c}from"node:sqlite";import{spawnSync as d}from"node:child_process";import{fileURLToPath as u}from"node:url";const h=t.join(n.homedir(),".claude.json"),p=/^[^@\s]+@[^@\s]+\.[^@\s]+$/;function m(e){return Boolean(e.employeeId||e.employeeEmail||e.employeeName)}function g(t=h){try{return function(e){const t=e?.oauthAccount?.emailAddress;if("string"!=typeof t)return null;const n=t.trim();return p.test(n)?n:null}(JSON.parse(e.readFileSync(t,"utf8")))}catch{return null}}async function f(e,t){const n=o.createInterface({input:a,output:i});try{const s=t?` [${t}]`:"";return(await n.question(`${e}${s}: `)).trim()||t||null}finally{n.close()}}function y(){return Date.now()/1e3}function b(e){return new Promise(t=>setTimeout(t,e))}function S(e){return JSON.stringify(E(e))}function E(e){return Array.isArray(e)?e.map(E):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,E(e[t])])):e}function k(e){if(!e)return null;const t=String(e).replace("Z","+00:00"),n=Date.parse(t);return Number.isNaN(n)?null:n}function T(e){return l("sha1").update(r.from(String(e),"utf8")).digest("hex")}class v{constructor(e,n,s){this.collectorIdentity=e,this.sourceRoot=n,this.relpath=s,this.fileSessionId=t.basename(s,".jsonl")}normalizeRecord(e,t){const n=this.normalizeUsageRecord(e,t),s=this.normalizeCodeChangeRecord(e,t),o=[],i=[];return n&&(o.push(n.session),i.push(n.event)),s&&(o.push(s.session),i.push(s.event)),0===i.length?null:{sessions:o,events:i}}normalizeUsageRecord(e,t){const n=e?.message&&"object"==typeof e.message?e.message:null,s=n?.usage&&"object"==typeof n.usage?n.usage:null;if(!s)return null;if(!0===e.isApiErrorMessage)return null;if(n.role&&"assistant"!==n.role)return null;if(null==k(e.timestamp))return null;const o=I(s.input_tokens),i=I(s.output_tokens),a=_(s.cache_creation_input_tokens),r=_(s.cache_read_input_tokens),l=[o,i,a,r].reduce((e,t)=>e+(t??0),0);if(l<=0)return null;const c=this.buildRecordMeta(e,t),d="string"==typeof n.id&&n.id.trim()?n.id.trim():null,u="string"==typeof e.requestId&&e.requestId.trim()?e.requestId.trim():null,h=L({collectorId:this.collectorIdentity.collectorId,sourceRoot:this.sourceRoot,relpath:this.relpath,lineNo:t,messageId:d,requestId:u,eventType:"usage"});return{session:{sessionId:c.sessionId,sessionTimestamp:c.timestamp,sourceRoot:c.sourceRoot,sourceFileRelpath:c.sourceFileRelpath,projectName:c.projectName,cwdHash:c.cwdHash,cwdBasename:c.cwdBasename,cliVersion:c.cliVersion},event:{eventUid:h,eventType:"usage",sessionId:c.sessionId,sourceRoot:c.sourceRoot,sourceFileRelpath:c.sourceFileRelpath,lineNo:t,timestamp:c.timestamp,projectName:c.projectName,cwdHash:c.cwdHash,cwdBasename:c.cwdBasename,cliVersion:c.cliVersion,model:w(n.model),messageId:d,requestId:u,inputTokens:o,outputTokens:i,cacheCreationInputTokens:a,cacheReadInputTokens:r,totalTokens:l,reportedCostUsd:I(e.costUSD),addedLines:0,deletedLines:0,changedLines:0,rawUsageJson:S(s)}}}normalizeCodeChangeRecord(e,t){const n=function(e){const t={addedLines:0,deletedLines:0,changedLines:0};if(!Array.isArray(e))return t;for(const n of e){const e=Array.isArray(n?.lines)?n.lines:[];for(const n of e)"string"==typeof n&&(n.startsWith("+")&&!n.startsWith("+++")?t.addedLines+=1:n.startsWith("-")&&!n.startsWith("---")&&(t.deletedLines+=1))}return t.changedLines=t.addedLines+t.deletedLines,t}(e?.toolUseResult?.structuredPatch);if(n.changedLines<=0)return null;const s=this.buildRecordMeta(e,t);if(!s)return null;const o=L({collectorId:this.collectorIdentity.collectorId,sourceRoot:this.sourceRoot,relpath:this.relpath,lineNo:t,eventType:"code_change"});return{session:{sessionId:s.sessionId,sessionTimestamp:s.timestamp,sourceRoot:s.sourceRoot,sourceFileRelpath:s.sourceFileRelpath,projectName:s.projectName,cwdHash:s.cwdHash,cwdBasename:s.cwdBasename,cliVersion:s.cliVersion},event:{eventUid:o,eventType:"code_change",sessionId:s.sessionId,sourceRoot:s.sourceRoot,sourceFileRelpath:s.sourceFileRelpath,lineNo:t,timestamp:s.timestamp,projectName:s.projectName,cwdHash:s.cwdHash,cwdBasename:s.cwdBasename,cliVersion:s.cliVersion,model:null,messageId:null,requestId:null,inputTokens:null,outputTokens:null,cacheCreationInputTokens:null,cacheReadInputTokens:null,totalTokens:null,reportedCostUsd:null,addedLines:n.addedLines,deletedLines:n.deletedLines,changedLines:n.changedLines,rawUsageJson:null}}}buildRecordMeta(e){const n=k(e.timestamp);if(null==n)return null;const s=String(e.sessionId||this.fileSessionId),o=function(e){if("string"!=typeof e||!e.trim())return{cwdHash:null,cwdBasename:null};const n=e.trim();return{cwdHash:T(n),cwdBasename:t.basename(n)||null}}(e.cwd),i=o.cwdBasename||function(e){const t=(e.split("/")[0]||"").replace(/^-+/,"").replace(/-+$/,"");return t.split("-").filter(Boolean).at(-1)||t||null}(this.relpath);return{sessionId:s,timestamp:n,sourceRoot:this.sourceRoot,sourceFileRelpath:P(this.relpath),projectName:i,cwdHash:o.cwdHash,cwdBasename:o.cwdBasename,cliVersion:w(e.version)}}processLine(e,t){let n;try{n=JSON.parse(t)}catch{return{sessions:[],events:[]}}return this.normalizeRecord(n,e)||{sessions:[],events:[]}}}function L({collectorId:e,sourceRoot:t,relpath:n,lineNo:s,messageId:o,requestId:i,eventType:a="usage"}){return T(o&&i?`${o}|${i}`:o?`${e}|${o}`:`${e}|${t}|${n}|${s}${"usage"===a?"":`|${a}`}`)}function P(e){const t=e.split("/").filter(Boolean),n=t.pop()||"unknown.jsonl",s=t.join("/");return s?`${T(s).slice(0,12)}/${n}`:n}function I(e){const t=Number(e);return Number.isFinite(t)&&t>=0?t:null}function _(e){const t=Number(e);return Number.isFinite(t)&&t>=0?t:0}function w(e){return"string"==typeof e&&e.trim()?e.trim():null}const N="Claude Code 用量上报",C="claude-code-usage-uploader",$="projects",R="legacy_projects",D=[t.join(n.homedir(),".config","claude","projects"),t.join(n.homedir(),".claude","projects")],B=t.join(n.homedir(),".claude-code-usage-uploader"),j=t.join(B,"config.json"),U=t.join(B,"state.sqlite"),F=t.join(B,"logs"),A=t.join(F,"stdout.log"),O=t.join(F,"stderr.log"),M=t.join(B,"app");t.join(M,"current");const x=t.join(B,"bin");t.join(x,C),t.join(n.homedir(),"bin",C);const H="com.token-dashboard.claude-code-usage-uploader";t.join(B,"launchd",`${H}.plist`);const q=t.join(n.homedir(),"Library","LaunchAgents",`${H}.plist`),W=t.join(n.homedir(),".claude-code-usage-uploader-collector-id"),z="http://101.126.66.51:8086",Y=1e6;class Q{constructor(n){this.dbPath=n,e.mkdirSync(t.dirname(n),{recursive:!0}),this.db=new c(n,{readBigInts:!0}),this.initSchema()}close(){this.db.close()}initSchema(){this.db.exec("\n CREATE TABLE IF NOT EXISTS identity_config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at REAL NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS pending_batches (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n batch_key TEXT NOT NULL UNIQUE,\n status TEXT NOT NULL,\n payload_json TEXT NOT NULL,\n payload_bytes INTEGER NOT NULL DEFAULT 0,\n session_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0,\n event_count INTEGER NOT NULL DEFAULT 0,\n attempt_count INTEGER NOT NULL DEFAULT 0,\n next_retry_at REAL,\n last_error TEXT,\n created_at REAL NOT NULL,\n updated_at REAL NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS upload_checkpoint (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at REAL NOT NULL\n );\n "),this.ensureIngestionFilesTable()}ensureIngestionFilesTable(){if(this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ingestion_files'").get()){if(!this.db.prepare("PRAGMA table_info(ingestion_files)").all().some(e=>"source_root"===e.name)){this.db.exec("BEGIN");try{this.db.exec("ALTER TABLE ingestion_files RENAME TO ingestion_files_legacy"),this.createIngestionFilesTable(),this.db.prepare("\n INSERT INTO ingestion_files(\n source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n )\n SELECT 'sessions', relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n FROM ingestion_files_legacy\n ").run(),this.db.exec("DROP TABLE ingestion_files_legacy"),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}else this.createIngestionFilesTable()}createIngestionFilesTable(){this.db.exec("\n CREATE TABLE IF NOT EXISTS ingestion_files (\n source_root TEXT NOT NULL,\n relpath TEXT NOT NULL,\n file_size INTEGER NOT NULL,\n file_mtime_ns INTEGER NOT NULL,\n last_line_no INTEGER NOT NULL,\n state_json TEXT,\n updated_at REAL NOT NULL,\n PRIMARY KEY (source_root, relpath)\n );\n ")}getIdentity(){const e=this.db.prepare("SELECT key, value FROM identity_config").all();return Object.fromEntries(e.map(e=>[e.key,e.value]))}setIdentity(e){const t=y(),n=this.db.prepare("\n INSERT INTO identity_config(key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n "),s=this.db.prepare("DELETE FROM identity_config WHERE key = ?");this.db.exec("BEGIN");try{for(const[o,i]of Object.entries(e))null==i?s.run(o):n.run(o,i,t);this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getCheckpoint(e){const t=this.db.prepare("SELECT value FROM upload_checkpoint WHERE key = ?").get(e);return t?.value??null}setCheckpoint(e,t){const n=y();this.db.prepare("\n INSERT INTO upload_checkpoint(key, value, updated_at)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n ").run(e,t,n)}getFileState(e,t){const n=this.db.prepare("SELECT * FROM ingestion_files WHERE source_root = ? AND relpath = ?");return n.setReadBigInts(!0),n.get(e,t)??null}upsertFileState(e,t,n,s,o,i){this.db.prepare("\n INSERT INTO ingestion_files(\n source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(source_root, relpath) DO UPDATE SET\n file_size = excluded.file_size,\n file_mtime_ns = excluded.file_mtime_ns,\n last_line_no = excluded.last_line_no,\n state_json = excluded.state_json,\n updated_at = excluded.updated_at\n ").run(e,t,n,s,o,S(i),y())}getBufferingBatch(){const e=this.db.prepare("\n SELECT * FROM pending_batches\n WHERE status = 'buffering'\n ORDER BY id ASC\n LIMIT 1\n ");return e.setReadBigInts(!0),e.get()??null}saveBufferingPayload(e){const t=this.getBufferingBatch(),n=S(e),s=Buffer.byteLength(n,"utf8"),o=e.sessions?.length??0,i=e.turns?.length??0,a=e.events?.length??0,r=y();return t?this.db.prepare("\n UPDATE pending_batches\n SET payload_json = ?, payload_bytes = ?, session_count = ?, turn_count = ?, event_count = ?, updated_at = ?\n WHERE id = ?\n ").run(n,s,o,i,a,r,t.id):this.db.prepare("\n INSERT INTO pending_batches(\n batch_key, status, payload_json, payload_bytes, session_count, turn_count, event_count,\n attempt_count, created_at, updated_at\n ) VALUES (?, 'buffering', ?, ?, ?, ?, ?, 0, ?, ?)\n ").run(`${Date.now().toString(16)}${Math.random().toString(16).slice(2,14)}`,n,s,o,i,a,r,r),this.getBufferingBatch()}sealStaleBatches(e=!1){const t=this.db.prepare("SELECT id, created_at FROM pending_batches WHERE status = 'buffering'");t.setReadBigInts(!0);const n=t.all();let s=0;const o=y(),i=this.db.prepare("\n UPDATE pending_batches\n SET status = 'pending', updated_at = ?\n WHERE id = ?\n ");for(const t of n)(e||o-Number(t.created_at)>=60)&&(i.run(o,t.id),s+=1);return s}sealBufferIfThresholdHit(){const e=this.getBufferingBatch();if(!e)return!1;const t=Number(e.event_count)>=200||Number(e.payload_bytes)>=Y||y()-Number(e.created_at)>=60;if(t){const t=y();this.db.prepare("\n UPDATE pending_batches\n SET status = 'pending', updated_at = ?\n WHERE id = ?\n ").run(t,e.id)}return t}iterPendingBatches(){const e=this.db.prepare("\n SELECT * FROM pending_batches\n WHERE status = 'pending'\n ORDER BY created_at ASC, id ASC\n ");return e.setReadBigInts(!0),e.all()}markBatchUploaded(e){this.db.prepare("DELETE FROM pending_batches WHERE id = ?").run(e)}markBatchFailed(e,t,n){this.db.prepare("\n UPDATE pending_batches\n SET attempt_count = ?, last_error = ?, updated_at = ?\n WHERE id = ?\n ").run(t,String(n).slice(0,1e3),y(),e)}resetBackfillState(){this.db.exec("BEGIN");try{this.db.prepare("DELETE FROM ingestion_files").run(),this.db.prepare("DELETE FROM pending_batches").run(),this.db.prepare("DELETE FROM upload_checkpoint").run(),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getQueueStats(){const e=this.db.prepare("\n SELECT\n COALESCE(SUM(CASE WHEN status = 'buffering' THEN 1 ELSE 0 END), 0) AS buffering_batch_count,\n COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count = 0 THEN 1 ELSE 0 END), 0) AS pending_batch_count,\n COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count > 0 THEN 1 ELSE 0 END), 0) AS retrying_batch_count,\n COALESCE(SUM(session_count), 0) AS queued_sessions,\n COALESCE(SUM(turn_count), 0) AS queued_turns,\n COALESCE(SUM(event_count), 0) AS queued_events,\n MIN(created_at) AS oldest_created_at\n FROM pending_batches\n ");e.setReadBigInts(!0);const t=e.get()??{},n=null==t.oldest_created_at?null:Number(t.oldest_created_at);return{bufferingBatchCount:Number(t.buffering_batch_count??0),pendingBatchCount:Number(t.pending_batch_count??0),retryingBatchCount:Number(t.retrying_batch_count??0),queuedSessions:Number(t.queued_sessions??0),queuedTurns:Number(t.queued_turns??0),queuedEvents:Number(t.queued_events??0),oldestPendingAgeSeconds:null==n?null:Math.max(0,Math.floor(y()-n))}}}class J{constructor({claudeProjectsDirs:e,stateDbPath:t,backendUrl:n,intervalSeconds:s,persistentCollectorIdPath:o=W,claudeJsonPath:i,scanChunkMaxEvents:a=50,scanChunkMaxBytes:r=262144}){var l;this.claudeProjectsDirs=(l=e,[...new Set((Array.isArray(l)?l:[l]).filter(Boolean).map(String))]),this.stateDb=new Q(t),this.backendUrl=n?.replace(/\/+$/,"")||null,this.intervalSeconds=s,this.persistentCollectorIdPath=o,this.claudeJsonPath=i,this.scanChunkMaxEvents=a,this.scanChunkMaxBytes=r,this.identity=this.ensureIdentity()}close(){this.stateDb.close()}ensureIdentity(){const t=this.stateDb.getIdentity();if(!t.employeeEmail){const e=g(this.claudeJsonPath);e&&(t.employeeEmail=e)}if(!t.collectorId){const n=function(t){try{return e.readFileSync(t,"utf8").trim()||null}catch{return null}}(this.persistentCollectorIdPath);t.collectorId=n??`${Date.now().toString(16)}${Math.random().toString(16).slice(2,14)}`}return function(t,n){const s=`${t}.${process.pid}.tmp`;e.writeFileSync(s,n,"utf8"),e.renameSync(s,t)}(this.persistentCollectorIdPath,t.collectorId),t.deviceId||(t.deviceId=`${n.hostname()}-${Math.random().toString(16).slice(2,14)}`),t.hostname||(t.hostname=n.hostname()),this.stateDb.setIdentity(t),this.stateDb.getIdentity()}resolveIdentityValues({employeeId:e,employeeEmail:t,employeeName:n,deviceId:s,hostname:o}={}){const i=this.stateDb.getIdentity(),a=i.employeeEmail?null:g(this.claudeJsonPath);return{employeeId:void 0!==e?e:i.employeeId??null,employeeEmail:void 0!==t?t:i.employeeEmail??a??null,employeeName:void 0!==n?n:i.employeeName??null,deviceId:void 0!==s?s:i.deviceId??null,hostname:void 0!==o?o:i.hostname??null}}configureIdentity({employeeId:e=null,employeeEmail:t=null,employeeName:n=null,deviceId:s,hostname:o}={}){const i={employeeId:e,employeeEmail:t,employeeName:n};if(!m(i))throw new Error("At least one of employeeId, employeeEmail, or employeeName must be provided.");return void 0!==s&&(i.deviceId=s),void 0!==o&&(i.hostname=o),this.stateDb.setIdentity(i),this.identity=this.ensureIdentity(),this.identity}configureIdentityWithDefaults(e={}){return this.configureIdentity(this.resolveIdentityValues(e))}async configureIdentityInteractive(e={}){const t=this.resolveIdentityValues(e);return void 0===e.employeeEmail&&(t.employeeEmail=await f("Employee email",t.employeeEmail)),m(t)||(void 0===e.employeeName&&(t.employeeName=await f("Employee name",t.employeeName)),m(t)||void 0===e.employeeId&&(t.employeeId=await f("Employee ID",t.employeeId))),this.configureIdentity(t)}resetBackfillState(){this.stateDb.resetBackfillState()}getQueueStats(){return this.stateDb.getQueueStats()}getScanRoots(){return this.claudeProjectsDirs.map((e,t)=>({sourceRoot:0===t?$:1===t?R:`${$}_${t+1}`,dir:e}))}iterJsonlFiles(n){if(!e.existsSync(n))return[];const s=[],o=n=>{for(const i of e.readdirSync(n,{withFileTypes:!0})){const e=t.join(n,i.name);i.isDirectory()?o(e):i.isFile()&&i.name.endsWith(".jsonl")&&s.push(e)}};return o(n),s.sort(),s}buildSnapshot(){const n=[];for(const s of this.getScanRoots())for(const o of this.iterJsonlFiles(s.dir)){const i=t.relative(s.dir,o).split(t.sep).join("/"),a=e.statSync(o,{bigint:!0}),r=this.stateDb.getFileState(s.sourceRoot,i);r&&r.file_size===a.size&&r.file_mtime_ns===a.mtimeNs||n.push({filePath:o,relpath:i,sourceRoot:s.sourceRoot,progressPath:`${s.sourceRoot}/${i}`,snapshotSize:a.size,snapshotMtimeNs:a.mtimeNs})}return n}emptyPayload(){return{sessions:[],events:[]}}payloadHasData(e){return Boolean(e.sessions.length||e.events.length)}payloadBytes(e){return Buffer.byteLength(S(e),"utf8")}mergePayload(e,t){const n=new Map((e.sessions??[]).map(e=>[e.sessionId,e])),s=new Map((e.events??[]).map(e=>[e.eventUid,e]));for(const e of t.sessions??[])n.set(e.sessionId,e);for(const e of t.events??[])s.set(e.eventUid,e);return{sessions:[...n.values()],events:[...s.values()]}}shouldFlushChunk(e){return!!this.payloadHasData(e)&&(e.events.length>=this.scanChunkMaxEvents||this.payloadBytes(e)>=this.scanChunkMaxBytes)}wouldExceedUploadThreshold(e){return!!this.payloadHasData(e)&&(e.events.length>200||this.payloadBytes(e)>Y)}appendPayloadToBuffer(e){if(!this.payloadHasData(e))return 0;const t=this.stateDb.getBufferingBatch();let n=0,s=e;if(t){const o=JSON.parse(t.payload_json),i=this.mergePayload(o,e);this.wouldExceedUploadThreshold(i)?n+=this.stateDb.sealStaleBatches(!0):s=i}return this.stateDb.saveBufferingPayload(s),n+(this.stateDb.sealBufferIfThresholdHit()?1:0)}async scanSnapshotEntries(t,{onFileProcessed:n}={}){const o={filesScanned:0,sessions:0,events:0,batchesQueued:0};for(const i of t){const{filePath:t,relpath:a,sourceRoot:r,snapshotSize:l,snapshotMtimeNs:c}=i,d=this.stateDb.getFileState(r,a);o.filesScanned+=1;let u=0;d&&l>d.file_size&&(u=Number(d.last_line_no));const h=new v(this.identity,r,a);let p=this.emptyPayload(),m=0,g=0;if(Number(l)>0){const n=e.createReadStream(t,{encoding:"utf8",start:0,end:Number(l)-1}),i=s({input:n,crlfDelay:1/0});try{for await(const e of i){if(g+=1,!e)continue;if(m=g,g<=u)continue;const t=h.processLine(g,e);o.sessions+=t.sessions.length,o.events+=t.events.length,p=this.mergePayload(p,t),this.shouldFlushChunk(p)&&(o.batchesQueued+=this.appendPayloadToBuffer(p),p=this.emptyPayload())}}finally{i.close()}}this.payloadHasData(p)&&(o.batchesQueued+=this.appendPayloadToBuffer(p)),this.stateDb.upsertFileState(r,a,l,c,m,{}),n?.({entry:i,filesProcessed:o.filesScanned,totals:o})}return o}async scanClaudeCode(){return this.scanSnapshotEntries(this.buildSnapshot())}collectorRequestBody(){return{collectorId:this.identity.collectorId,deviceId:this.identity.deviceId,hostname:this.identity.hostname??null,employeeId:this.identity.employeeId??null,employeeEmail:this.identity.employeeEmail??null,employeeName:this.identity.employeeName??null}}async postJson(e,t){if(!this.backendUrl)throw new Error("backendUrl is not configured");const n=await fetch(`${this.backendUrl}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){const e=await n.text(),t=e?`: ${e.slice(0,500)}`:"";throw new Error(`HTTP ${n.status} ${n.statusText}${t}`)}const s=await n.text();return s?JSON.parse(s):{}}async ensureRemoteRegistration(){if(!this.backendUrl)return;const e=this.stateDb.getCheckpoint("last_register_at");e&&y()-Number(e)<600||(await this.postJson("/claude-code-usage/collectors/register",this.collectorRequestBody()),this.stateDb.setCheckpoint("last_register_at",String(y())))}async flushPendingBatches({failFast:e=!1,concurrency:t=5,onBatchUploaded:n}={}){if(!this.backendUrl)return{uploadedBatches:0,failedBatches:0,lastError:null};try{await this.ensureRemoteRegistration()}catch(t){if(e)throw new Error(`Collector registration failed: ${t instanceof Error?t.message:String(t)}`);return{uploadedBatches:0,failedBatches:1,lastError:t}}let s=0,o=0,i=null;const a=this.collectorRequestBody(),r=this.stateDb.iterPendingBatches(),l=async e=>{const t=JSON.parse(e.payload_json),n={idempotencyKey:e.batch_key,collector:a,payloadSizeBytes:Number(e.payload_bytes),sessions:Array.isArray(t.sessions)?t.sessions:[],events:Array.isArray(t.events)?t.events:[]};await this.postJson("/claude-code-usage/upload",n)},c=new Set;let d=0;const u=()=>{for(;c.size<t&&d<r.length;){const t=r[d++],a=l(t).then(()=>{this.stateDb.markBatchUploaded(t.id),s+=1,n?.({row:t,uploadedBatches:s})}).catch(n=>{if(this.stateDb.markBatchFailed(t.id,Number(t.attempt_count)+1,n instanceof Error?n.message:String(n)),o+=1,i=n,e)throw n}).finally(()=>{c.delete(a)});c.add(a)}};for(u();c.size>0;){try{await Promise.race(c)}catch{if(e)break}u()}if(e&&i)throw new Error(`Upload batch failed: ${i instanceof Error?i.message:String(i)}`);return{uploadedBatches:s,failedBatches:o,lastError:i}}async runCycle({forceSeal:e=!1}={}){const t=await this.scanClaudeCode();t.batchesQueued+=this.stateDb.sealStaleBatches(e);const n=await this.flushPendingBatches();t.uploadedBatches=n.uploadedBatches,t.failedBatches=n.failedBatches;const s=this.getQueueStats();return{...t,...s,remainingQueuedBatches:s.bufferingBatchCount+s.pendingBatchCount+s.retryingBatchCount,remainingQueuedEvents:s.queuedEvents}}async runForegroundCatchUp({snapshot:e,onProgress:t}={}){const n=Date.now(),s=e??this.buildSnapshot(),o=s.length;t?.({phase:"start",totalFiles:o,filesProcessed:0,eventsParsed:0,batchesQueued:0});const i=await this.scanSnapshotEntries(s,{onFileProcessed:({entry:e,filesProcessed:n,totals:s})=>{t?.({phase:"file",file:e.progressPath??e.relpath,totalFiles:o,filesProcessed:n,eventsParsed:s.events,batchesQueued:s.batchesQueued})}});i.batchesQueued+=this.stateDb.sealStaleBatches(!0),await this.ensureRemoteRegistration();const a=this.getQueueStats(),r={totalFiles:o,filesProcessed:o,eventsParsed:i.events,sessionsParsed:i.sessions,batchesQueued:i.batchesQueued,pendingBatches:a.pendingBatchCount+a.retryingBatchCount,pendingEvents:a.queuedEvents,durationMs:Date.now()-n};return t?.({phase:"done",...r}),r}async watch(){let e=!0;for(;;){const t=await this.runCycle({forceSeal:e});console.log(`[uploader] scanned_files=${t.filesScanned} sessions=${t.sessions} events=${t.events} queued_batches=${t.batchesQueued} uploaded_batches=${t.uploadedBatches} remaining_batches=${t.remainingQueuedBatches}`),e=!1,await b(1e3*this.intervalSeconds)}}}const G=t.basename(B);function V(e){const n=t.basename(e);if(!n.startsWith(`${G}-`))return"";const s=n.slice(G.length+1);return s?`-${s}`:""}function X(s=j){const o=function(e=j){const s=t.dirname(e),o=V(s),i=t.join(s,"app"),a=t.join(i,"current"),r=`${H}${o}`;return{configFile:e,installRoot:s,appRoot:i,currentAppDir:a,backendUrl:z,intervalSeconds:30,claudeProjectsDirs:D,stateDbPath:t.join(s,t.basename(U)),nodePath:process.execPath,entryFile:"",packageSpec:"",localBinDir:t.join(s,"bin"),localBinPath:t.join(s,"bin",C),homeBinLink:t.join(n.homedir(),"bin",`${C}${o}`),stdoutLogPath:t.join(s,"logs",t.basename(A)),stderrLogPath:t.join(s,"logs",t.basename(O)),launchdLabel:r,plistPath:t.join(s,"launchd",`${r}.plist`),launchAgentPath:t.join(t.dirname(q),`${r}.plist`),persistentCollectorIdPath:t.join(n.homedir(),`${G}${o}-collector-id`),autoStartOnLogin:!0}}(s);try{const t=JSON.parse(e.readFileSync(s,"utf8"));return t&&"object"==typeof t?K({...o,...t,configFile:s}):o}catch{return o}}function K(e){const s=e.installRoot||B,o=V(s),i=e.appRoot||t.join(s,"app"),a=e.currentAppDir||t.join(i,"current"),r=e.localBinDir||t.join(s,"bin"),l=e.launchdLabel||`${H}${o}`,c=Array.isArray(e.claudeProjectsDirs)?e.claudeProjectsDirs:e.claudeProjectsDir?[e.claudeProjectsDir]:D,d=[...new Set(c.filter(Boolean).map(String))];return{...e,configFile:e.configFile||j,installRoot:s,appRoot:i,currentAppDir:a,backendUrl:e.backendUrl?.trim()?e.backendUrl.replace(/\/+$/,""):null,intervalSeconds:Number(e.intervalSeconds)||30,claudeProjectsDirs:d,stateDbPath:e.stateDbPath||t.join(s,"state.sqlite"),nodePath:e.nodePath||process.execPath,entryFile:e.entryFile||"",packageSpec:e.packageSpec||"",localBinDir:r,localBinPath:e.localBinPath||t.join(r,C),homeBinLink:e.homeBinLink||t.join(n.homedir(),"bin",`${C}${o}`),stdoutLogPath:e.stdoutLogPath||t.join(s,"logs","stdout.log"),stderrLogPath:e.stderrLogPath||t.join(s,"logs","stderr.log"),launchdLabel:l,plistPath:e.plistPath||t.join(s,"launchd",`${l}.plist`),launchAgentPath:e.launchAgentPath||t.join(t.dirname(q),`${l}.plist`),persistentCollectorIdPath:e.persistentCollectorIdPath||t.join(n.homedir(),`${G}${o}-collector-id`),autoStartOnLogin:void 0===e.autoStartOnLogin||Boolean(e.autoStartOnLogin)}}function Z(n){const s=K(n);return e.mkdirSync(t.dirname(s.configFile),{recursive:!0}),e.mkdirSync(t.dirname(s.stdoutLogPath),{recursive:!0}),e.writeFileSync(s.configFile,`${S({backendUrl:s.backendUrl,intervalSeconds:s.intervalSeconds,claudeProjectsDirs:s.claudeProjectsDirs,stateDbPath:s.stateDbPath,installRoot:s.installRoot,appRoot:s.appRoot,currentAppDir:s.currentAppDir,nodePath:s.nodePath,entryFile:s.entryFile,packageSpec:s.packageSpec,localBinDir:s.localBinDir,localBinPath:s.localBinPath,homeBinLink:s.homeBinLink,stdoutLogPath:s.stdoutLogPath,stderrLogPath:s.stderrLogPath,launchdLabel:s.launchdLabel,plistPath:s.plistPath,launchAgentPath:s.launchAgentPath,persistentCollectorIdPath:s.persistentCollectorIdPath,autoStartOnLogin:s.autoStartOnLogin})}\n`),s}function ee(e,t={}){return K({...X(e),...t,configFile:e})}function te(t,n){return{configExists:e.existsSync(t.configFile),loaded:n.loaded,running:n.running,pid:n.pid,state:n.state,lastExitCode:n.lastExitCode,backendUrl:t.backendUrl,intervalSeconds:t.intervalSeconds,claudeProjectsDirs:t.claudeProjectsDirs,configFile:t.configFile,stateDbPath:t.stateDbPath,stdoutLogPath:t.stdoutLogPath,stderrLogPath:t.stderrLogPath,plistPath:t.plistPath,launchAgentPath:t.launchAgentPath,label:t.launchdLabel,autoStartOnLogin:t.autoStartOnLogin,onlineThresholdSeconds:600}}const ne="# Added by claude-code-usage-uploader",se=[t.join(n.homedir(),".zshrc"),t.join(n.homedir(),".bash_profile")];function oe(n=import.meta.url){let s=t.dirname(u(n));for(;;){const n=t.join(s,"package.json");if(e.existsSync(n))return s;const o=t.dirname(s);if(o===s)throw new Error("package.json not found from current runtime");s=o}}function ie(e){return String(e).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}class ae{constructor(e){this.runtime=e}ensureMacos(){if("darwin"!==process.platform)throw new Error("launchd management is only supported on macOS.")}domainTarget(){return`gui/${process.getuid()}`}serviceTarget(){return`${this.domainTarget()}/${this.runtime.launchdLabel}`}run(e,{check:t=!0}={}){const n=d(e[0],e.slice(1),{encoding:"utf8"});if(t&&0!==n.status)throw new Error(n.stderr?.trim()||n.stdout?.trim()||`command failed: ${e.join(" ")}`);return n}ensurePlist(){if(this.ensureMacos(),!this.runtime.entryFile||!e.existsSync(this.runtime.entryFile))throw new Error(`installed runtime entry not found: ${this.runtime.entryFile||"(empty)"}`);const n=function(e){const t=[e.nodePath,e.entryFile,"--config-file",e.configFile,"run"];return`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n <dict>\n <key>Label</key>\n <string>${ie(e.launchdLabel)}</string>\n <key>ProgramArguments</key>\n <array>\n${t.map(e=>` <string>${ie(e)}</string>`).join("\n")}\n </array>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <true/>\n <key>ProcessType</key>\n <string>Background</string>\n <key>WorkingDirectory</key>\n <string>${ie(e.installRoot)}</string>\n <key>StandardOutPath</key>\n <string>${ie(e.stdoutLogPath)}</string>\n <key>StandardErrorPath</key>\n <string>${ie(e.stderrLogPath)}</string>\n <key>EnvironmentVariables</key>\n <dict>\n <key>NODE_NO_WARNINGS</key>\n <string>1</string>\n </dict>\n </dict>\n</plist>\n`}(this.runtime);e.mkdirSync(t.dirname(this.runtime.plistPath),{recursive:!0}),e.mkdirSync(t.dirname(this.runtime.stdoutLogPath),{recursive:!0}),e.writeFileSync(this.runtime.plistPath,n),this.syncLaunchAgent(n)}syncLaunchAgent(n){const s=this.runtime.launchAgentPath;if(s)return this.runtime.autoStartOnLogin?(e.mkdirSync(t.dirname(s),{recursive:!0}),void e.writeFileSync(s,n)):void(e.existsSync(s)&&e.rmSync(s,{force:!0}))}start(){this.ensurePlist(),this.stop(),this.run(["launchctl","bootstrap",this.domainTarget(),this.runtime.plistPath])}stop(){this.ensureMacos(),this.run(["launchctl","bootout",this.serviceTarget()],{check:!1})}restart(){this.start()}status(){this.ensureMacos();const e=this.run(["launchctl","print",this.serviceTarget()],{check:!1});return 0!==e.status?{loaded:!1,running:!1,pid:null,state:null,lastExitCode:null}:function(e){const t=e.match(/^\s*state = ([^\n]+)$/m)?.[1]?.trim()??null,n=e.match(/^\s*pid = (\d+)$/m)?.[1]??null,s=e.match(/^\s*last exit code = (-?\d+)$/m)?.[1]??null;return{loaded:!0,running:"running"===t||null!=n,pid:n?Number(n):null,state:t,lastExitCode:s?Number(s):null}}(e.stdout)}uninstall(){if(this.stop(),e.existsSync(this.runtime.plistPath)&&e.unlinkSync(this.runtime.plistPath),this.runtime.launchAgentPath&&e.existsSync(this.runtime.launchAgentPath)&&e.unlinkSync(this.runtime.launchAgentPath),e.existsSync(this.runtime.homeBinLink)&&e.lstatSync(this.runtime.homeBinLink).isSymbolicLink())try{e.realpathSync.native(this.runtime.homeBinLink)===this.runtime.localBinPath&&e.unlinkSync(this.runtime.homeBinLink)}catch{e.unlinkSync(this.runtime.homeBinLink)}}}const re=Object.freeze({collectorId:"local-usage"}),le=new Set(["today","7d","30d","all"]),ce=/^\d{4}-\d{2}-\d{2}$/;function de(){return Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}function ue(){return{inputTokens:0,outputTokens:0,cacheCreationInputTokens:0,cacheReadInputTokens:0,totalTokens:0,addedLines:0,deletedLines:0,changedLines:0,reportedCostUsd:0}}function he(e){return{model:e.model??"(unknown)",inputTokens:e.inputTokens??0,outputTokens:e.outputTokens??0,cacheCreationInputTokens:e.cacheCreationInputTokens??0,cacheReadInputTokens:e.cacheReadInputTokens??0,totalTokens:e.totalTokens??0,addedLines:e.addedLines??0,deletedLines:e.deletedLines??0,changedLines:e.changedLines??0,reportedCostUsd:e.reportedCostUsd??0}}function pe(e,t){e.inputTokens+=t.inputTokens,e.outputTokens+=t.outputTokens,e.cacheCreationInputTokens+=t.cacheCreationInputTokens,e.cacheReadInputTokens+=t.cacheReadInputTokens,e.totalTokens+=t.totalTokens,e.addedLines+=t.addedLines,e.deletedLines+=t.deletedLines,e.changedLines+=t.changedLines,e.reportedCostUsd+=t.reportedCostUsd}function me(e,t){const n=new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(new Date(e)),s=Object.fromEntries(n.map(e=>[e.type,e.value]));return`${s.year}-${s.month}-${s.day}`}function ge(e,t){if(!ce.test(String(e)))throw new Error(`${t} must be in YYYY-MM-DD format`);const[n,s,o]=String(e).split("-"),i=Number(n),a=Number(s),r=Number(o),l=new Date(Date.UTC(i,a-1,r));if(Number.isNaN(l.getTime())||l.getUTCFullYear()!==i||l.getUTCMonth()!==a-1||l.getUTCDate()!==r)throw new Error(`${t} must be a valid calendar date`);return`${n}-${s}-${o}`}function fe(e,t){const[n,s,o]=e.split("-").map(Number),i=new Date(Date.UTC(n,s-1,o+t));return[i.getUTCFullYear(),String(i.getUTCMonth()+1).padStart(2,"0"),String(i.getUTCDate()).padStart(2,"0")].join("-")}function ye(e,t,n){return!(t&&e<t||n&&e>n)}const be=`${N}\n\nUsage:\n ${C} init [--backend-url <url>] [--interval 30] [--yes] [--package-spec <spec>]\n ${C} bind [--email <email>] [--employee-name <name>] [--employee-id <id>] [--yes]\n ${C} clear [--yes]\n ${C} start\n ${C} stop\n ${C} restart\n ${C} status\n ${C} usage [--period <today|7d|30d|all>] [--from <date>] [--to <date>]\n ${C} logs [--lines 100]\n ${C} uninstall\n\nCommon options:\n --config-file <path> Runtime config path. Default: ${j}\n --backend-url <url> Dashboard backend URL\n --interval <seconds> Scan interval in seconds\n --claude-projects-dir <path> Claude Code projects directory. Can be repeated\n --state-db <path> Local SQLite state DB path\n --employee-id <id> Bind employee ID\n --email <email> Bind employee email\n --employee-name <name> Bind employee name\n --device-id <id> Override device ID\n --hostname <name> Override hostname\n --period <value> Usage range: today, 7d, 30d, or all\n --from <date> Usage range start date (YYYY-MM-DD)\n --to <date> Usage range end date (YYYY-MM-DD)\n --yes Skip interactive prompts\n --package-spec <spec> npm install spec used by init\n --lines <n> Number of log lines for service logs\n -v, --version Show version\n -h, --help Show help\n`;class Se extends Error{constructor(e){super(e),this.name="CliOperationalError",this.showUsage=!1}}const Ee={findPackageRoot:oe,installCurrentPackage:function(n,{packageRoot:s,packageSpec:o,nodePath:i=process.execPath}={}){const a=function(e,t){return t||`file:${e}`}(s,o),r=n.appRoot,l=t.join(r,`.staging-${Date.now()}`);e.mkdirSync(l,{recursive:!0}),e.writeFileSync(t.join(l,"package.json"),`${JSON.stringify({private:!0,name:"claude-code-usage-uploader-runtime"},null,2)}\n`);const c="win32"===process.platform?"npm.cmd":"npm",u=d(c,["install","--no-save","--omit=dev","--no-package-lock",a],{cwd:l,stdio:"inherit",env:{...process.env,npm_config_fund:"false",npm_config_audit:"false"}});if(u.error)throw new Error(`failed to execute ${c}: ${u.error.message}`);if(0!==u.status)throw new Error(`npm install failed for ${a}`);const h=JSON.parse(e.readFileSync(t.join(s,"package.json"),"utf8")),p=t.join(l,"node_modules",...String(h.name).split("/")),m=JSON.parse(e.readFileSync(t.join(p,"package.json"),"utf8")).bin,g="string"==typeof m?m:m?.[C]??Object.values(m??{})[0];if(!g)throw new Error("installed package does not expose the expected CLI binary");const f=n.currentAppDir;return e.rmSync(f,{recursive:!0,force:!0}),e.mkdirSync(t.dirname(f),{recursive:!0}),e.renameSync(l,f),n.nodePath=i,n.packageSpec=a,n.entryFile=t.join(f,"node_modules",...String(h.name).split("/"),g),function(t){e.mkdirSync(t.localBinDir,{recursive:!0});const n=`#!/bin/sh\nset -eu\nCONFIG_FILE="\${CLAUDE_CODE_USAGE_UPLOADER_CONFIG:-${t.configFile}}"\nexec "${t.nodePath}" "${t.entryFile}" --config-file "$CONFIG_FILE" "$@"\n`;e.writeFileSync(t.localBinPath,n,{mode:493}),e.chmodSync(t.localBinPath,493)}(n),function(n){e.mkdirSync(t.dirname(n.homeBinLink),{recursive:!0});try{(e.existsSync(n.homeBinLink)||e.lstatSync(n.homeBinLink).isSymbolicLink())&&e.rmSync(n.homeBinLink,{force:!0})}catch{}e.symlinkSync(n.localBinPath,n.homeBinLink)}(n),Z(n),n},ensurePathInShellConfigs:function(t){const n=`export PATH="${t}:$PATH"`;for(const t of se)try{if(!e.existsSync(t))continue;const s=e.readFileSync(t,"utf8");if(s.includes(ne))continue;const o=s.length>0&&!s.endsWith("\n")?"\n":"";e.appendFileSync(t,`${o}\n${ne}\n${n}\n`)}catch{}},removePathFromShellConfigs:function(){for(const t of se)try{if(!e.existsSync(t))continue;const n=e.readFileSync(t,"utf8");if(!n.includes(ne))continue;const s=n.split("\n"),o=[];for(let e=0;e<s.length;e++)s[e]!==ne?o.push(s[e]):(e+1<s.length&&s[e+1].startsWith("export PATH=")&&(e+=1),o.length>0&&""===o[o.length-1]&&o.pop());e.writeFileSync(t,o.join("\n"))}catch{}},promptConfirm:async function(e,t=!1){const n=t?"y":"n",s=await f(`${e} (${t?"Y/n":"y/N"})`,n);return/^(y|yes)$/i.test(String(s??"").trim())},createUploader:e=>new J({claudeProjectsDirs:e.claudeProjectsDirs,stateDbPath:e.stateDbPath,backendUrl:e.backendUrl,intervalSeconds:e.intervalSeconds,persistentCollectorIdPath:e.persistentCollectorIdPath}),createServiceManager:e=>new ae(e)};function ke(e){const t={configFile:j,interval:void 0,yes:!1,lines:100,claudeProjectsDirs:[]},n=[];for(let s=0;s<e.length;s+=1){const o=e[s];if(!o.startsWith("-")){n.push(o);continue}if("-h"===o||"--help"===o){t.help=!0;continue}if("-v"===o||"--version"===o){t.version=!0;continue}if("--yes"===o){t.yes=!0;continue}const[i,a]=o.split("=",2);if(!new Set(["--config-file","--backend-url","--interval","--claude-projects-dir","--state-db","--employee-id","--email","--employee-name","--device-id","--hostname","--period","--from","--to","--package-spec","--lines"]).has(i))throw new Error(`Unknown argument: ${o}`);const r=a??e[++s];if(null==r||r.startsWith("-"))throw new Error(`Missing value for ${i}`);Te(t,i,r)}return{command:n[0]??null,subcommand:n[1]??null,extraPositionals:n.slice(2),options:t}}function Te(e,t,n){switch(t){case"--config-file":e.configFile=n;break;case"--backend-url":e.backendUrl=n;break;case"--interval":e.interval=ve(n,"--interval");break;case"--claude-projects-dir":e.claudeProjectsDirs.push(n);break;case"--state-db":e.stateDbPath=n;break;case"--employee-id":e.employeeId=n;break;case"--email":e.employeeEmail=n;break;case"--employee-name":e.employeeName=n;break;case"--device-id":e.deviceId=n;break;case"--hostname":e.hostname=n;break;case"--period":e.period=n;break;case"--from":e.from=n;break;case"--to":e.to=n;break;case"--package-spec":e.packageSpec=n;break;case"--lines":e.lines=ve(n,"--lines");break;default:throw new Error(`Unknown argument: ${t}`)}}function ve(e,t){const n=Number.parseInt(String(e),10);if(!Number.isFinite(n)||n<=0)throw new Error(`${t} must be a positive integer`);return n}function Le(e){const t={};return void 0!==e.backendUrl&&(t.backendUrl=e.backendUrl),void 0!==e.interval&&(t.intervalSeconds=e.interval),e.claudeProjectsDirs?.length&&(t.claudeProjectsDirs=e.claudeProjectsDirs),void 0!==e.stateDbPath&&(t.stateDbPath=e.stateDbPath),t}function Pe(e){const t={};return void 0!==e.employeeId&&(t.employeeId=e.employeeId),void 0!==e.employeeEmail&&(t.employeeEmail=e.employeeEmail),void 0!==e.employeeName&&(t.employeeName=e.employeeName),void 0!==e.deviceId&&(t.deviceId=e.deviceId),void 0!==e.hostname&&(t.hostname=e.hostname),t}function Ie(e){if(!m(e.identity))throw new Se(`No employee identity is bound yet. Run \`${C} bind\` or rerun \`init\` with --email.`)}function _e(e){console.log("Identity binding:"),console.log(` collectorId: ${e.collectorId}`),console.log(` employeeEmail: ${e.employeeEmail??"-"}`),console.log(` employeeName: ${e.employeeName??"-"}`),console.log(` employeeId: ${e.employeeId??"-"}`),console.log(` deviceId: ${e.deviceId??"-"}`),console.log(` hostname: ${e.hostname??"-"}`)}function we(t,n){return e.existsSync(t)?e.readFileSync(t,"utf8").split(/\r?\n/).filter((e,t,n)=>!(t===n.length-1&&""===e)).slice(-n):[]}async function Ne(e,t){const n=Pe(t),s=t.yes?e.configureIdentityWithDefaults(n):await e.configureIdentityInteractive(n);return Ie(e),s}function Ce(e){switch(e.phase){case"start":return void console.log(`[scan] start files=${e.totalFiles}`);case"file":return void console.log(`[scan] file ${e.filesProcessed}/${e.totalFiles} current=${e.file} events=${e.eventsParsed}`);case"done":return void console.log(`[scan] complete files=${e.filesProcessed}/${e.totalFiles} events=${e.eventsParsed} pending_batches=${e.pendingBatches} duration=${t=e.durationMs,`${Math.max(0,Math.round(t/1e3))}s`}`)}var t}function $e(e){return Number(e??0).toLocaleString("en-US")}function Re(e){return`$${Number(e??0).toFixed(4)}`}async function De(n){const o=ee(n.configFile,Le(n));console.log("Scanning local Claude Code usage..."),function(e){if(0===e.eventCount)return void console.log("No local Claude Code usage found for selected range.");const t=e.days.map(e=>[e.date,e.models.join(", "),$e(e.inputTokens),$e(e.outputTokens),$e(e.cacheCreationInputTokens),$e(e.cacheReadInputTokens),$e(e.totalTokens),$e(e.addedLines),$e(e.deletedLines),$e(e.changedLines),Re(e.reportedCostUsd)]);t.push(["Total","",$e(e.summary.inputTokens),$e(e.summary.outputTokens),$e(e.summary.cacheCreationInputTokens),$e(e.summary.cacheReadInputTokens),$e(e.summary.totalTokens),$e(e.summary.addedLines),$e(e.summary.deletedLines),$e(e.summary.changedLines),Re(e.summary.reportedCostUsd)]),console.log(function(e,t,n=[]){const s=e.map((e,n)=>Math.max(e.length,...t.map(e=>String(e[n]??"").length)));return[e,...t].map(e=>e.map((e,t)=>((e,t)=>{const o=String(e??"");return"right"===n[t]?o.padStart(s[t]):o.padEnd(s[t])})(e,t)).join(" ").trimEnd()).join("\n")}(["Date","Models","Input","Output","Cache Create","Cache Read","Total Tokens","Added","Deleted","Changed","Cost USD"],t,["left","left","right","right","right","right","right","right","right","right","right"]))}(await async function({claudeProjectsDirs:n,period:o,from:i,to:a,now:r=Date.now(),timeZone:l=de()}={}){const c=function({period:e,from:t,to:n,now:s=Date.now(),timeZone:o=de()}={}){if(e&&(t||n))throw new Error("--period cannot be combined with --from or --to");if(e&&!le.has(e))throw new Error("--period must be one of: today, 7d, 30d, all");const i=me(s,o);if("today"===e)return{mode:"today",from:i,to:i};if("7d"===e)return{mode:"7d",from:fe(i,-6),to:i};if("30d"===e)return{mode:"30d",from:fe(i,-29),to:i};if("all"===e||!e&&!t&&!n)return{mode:"all",from:null,to:null};const a=null==t?null:ge(t,"--from"),r=null==n?null:ge(n,"--to");if(a&&r&&a>r)throw new Error("--from cannot be later than --to");return{mode:"custom",from:a,to:r}}({period:o,from:i,to:a,now:r,timeZone:l}),d=n.map((e,t)=>({sourceRoot:0===t?$:R,dir:e})).flatMap(n=>function(n){if(!e.existsSync(n))return[];const s=[],o=n=>{for(const i of e.readdirSync(n,{withFileTypes:!0})){const e=t.join(n,i.name);i.isDirectory()?o(e):i.isFile()&&i.name.endsWith(".jsonl")&&s.push(e)}};return o(n),s}(n.dir).map(e=>({...n,filePath:e,relpath:t.relative(n.dir,e).split(t.sep).join("/")})));d.sort((e,t)=>e.filePath.localeCompare(t.filePath));const u={inputTokens:0,outputTokens:0,cacheCreationInputTokens:0,cacheReadInputTokens:0,totalTokens:0,addedLines:0,deletedLines:0,changedLines:0,reportedCostUsd:0},h=new Map,p=new Map;let m=0;for(const t of d){const n=new v(re,t.sourceRoot,t.relpath),o=e.createReadStream(t.filePath,{encoding:"utf8"}),i=s({input:o,crlfDelay:1/0});let a=0;try{for await(const e of i){if(a+=1,!e)continue;const t=n.processLine(a,e);for(const e of t.events){const t=me(e.timestamp,l);if(!ye(t,c.from,c.to))continue;const n=he(e);m+=1,pe(u,n);const s=h.get(t)??{date:t,eventCount:0,models:new Set,...ue()};s.eventCount+=1,s.models.add(n.model),pe(s,n),h.set(t,s);const o=p.get(n.model)??{model:n.model,eventCount:0,...ue()};o.eventCount+=1,pe(o,n),p.set(n.model,o)}}}finally{i.close()}}return{scope:"local-machine",timezone:l,range:c,sources:{claudeProjectsDirs:n},filesScanned:d.length,eventCount:m,summary:u,days:[...h.values()].sort((e,t)=>e.date.localeCompare(t.date)).map(e=>({...e,models:[...e.models].sort((e,t)=>e.localeCompare(t))})),byModel:[...p.values()].sort((e,t)=>t.totalTokens!==e.totalTokens?t.totalTokens-e.totalTokens:e.model.localeCompare(t.model))}}({claudeProjectsDirs:o.claudeProjectsDirs,period:n.period,from:n.from,to:n.to,timeZone:de()}))}function Be(t){if(!e.existsSync(t))return{collectorId:null,bufferingBatchCount:0,pendingBatchCount:0,retryingBatchCount:0,queuedSessions:0,queuedTurns:0,queuedEvents:0,oldestPendingAgeSeconds:null};const n=new Q(t);try{return{collectorId:n.getIdentity().collectorId??null,...n.getQueueStats()}}finally{n.close()}}function je(t){if(!e.existsSync(t.configFile)||!t.entryFile)throw new Se(`Uploader is not initialized yet. Run \`${C} init\` first.`)}async function Ue(n=process.argv.slice(2)){try{const{command:s,subcommand:o,extraPositionals:i,options:a}=ke(n);if(a.version)return console.log(function(n=import.meta.url){const s=oe(n);return JSON.parse(e.readFileSync(t.join(s,"package.json"),"utf8")).version??"unknown"}()),0;if(!s||a.help)return console.log(be),0;if(!new Set(["init","bind","clear","start","stop","restart","status","usage","logs","uninstall","run"]).has(s))throw new Error(`Unknown command: ${s}`);switch(function(e,t,n){if(t)throw new Error(`Unexpected subcommand for ${e}: ${t}`);if(n.length>0)throw new Error(`Unexpected extra arguments: ${n.join(" ")}`)}(s,o,i),s){case"init":return await async function(e){const n=Le(e);n.backendUrl||(n.backendUrl=z);let s=ee(e.configFile,n);if(!s.backendUrl)throw new Error("--backend-url is required for init unless already configured in config.json");s=Ee.installCurrentPackage(s,{packageRoot:Ee.findPackageRoot(),packageSpec:e.packageSpec,nodePath:process.execPath});let o=Ee.createServiceManager(s);o.stop();const i=Ee.createUploader(s);try{const n=!Object.values(Pe(e)).some(e=>void 0!==e)&&m(i.identity)?i.identity:await Ne(i,e);console.log(`${N} install is ready.`),console.log(`Backend: ${s.backendUrl}`),console.log(`Projects dirs: ${s.claudeProjectsDirs.join(", ")}`),console.log(`Install root: ${s.installRoot}`),console.log(`Config file: ${s.configFile}`),_e(n),console.log("Scanning local Claude Code usage...");const a=await i.runForegroundCatchUp({onProgress:Ce});s.autoStartOnLogin=await async function(e,t){return t.yes?e.autoStartOnLogin:Ee.promptConfirm("Start automatically when you log in on this Mac?",e.autoStartOnLogin)}(s,e),s=Z(s),o=Ee.createServiceManager(s),o.start(),console.log(`${N} initialized and started.`),console.log(s.autoStartOnLogin?"Login auto-start is enabled.":"Login auto-start is disabled for future logins."),a.pendingBatches>0&&console.log(`Background service is uploading ${a.pendingBatches} batch(es) with ${a.pendingEvents} event(s).`);const r=t.dirname(s.homeBinLink);(process.env.PATH||"").split(t.delimiter).includes(r)||(Ee.ensurePathInShellConfigs(r),console.log(`Open a new terminal to use \`${C}\` directly.`))}finally{i.close()}}(a),0;case"bind":return await async function(t){const n=ee(t.configFile,Le(t)),s=Ee.createUploader(n);try{_e(await Ne(s,t)),await async function(t){if("darwin"!==process.platform)return!1;if(!e.existsSync(t.configFile)||!t.entryFile)return!1;const n=Ee.createServiceManager(t);let s;try{s=n.status()}catch{return!1}return!!s.running&&(n.restart(),!0)}(n)&&console.log("Background service restarted to apply the updated identity.")}finally{s.close()}}(a),0;case"clear":return await async function(e){const t=ee(e.configFile,Le(e));if(je(t),Ee.createServiceManager(t).status().running)throw new Se(`Background service is still running. Stop it first with \`${C} stop\`.`);if(!e.yes&&!await Ee.promptConfirm("Clear local backfill state and queued uploads?",!1))return void console.log("Clear cancelled.");const n=Ee.createUploader(t);try{n.resetBackfillState()}finally{n.close()}console.log("Local backfill state has been cleared.")}(a),0;case"usage":return await De(a),0;case"run":return await async function(e){const t=ee(e.configFile,Le(e)),n=Ee.createUploader(t);try{Ie(n),console.log(`[run] backend=${t.backendUrl??"-"} interval=${t.intervalSeconds}s projects_dirs=${t.claudeProjectsDirs.join(",")}`),await n.watch()}finally{n.close()}}(a),0;case"start":case"stop":case"restart":case"status":case"logs":case"uninstall":return function(t,n){const s=ee(n.configFile,Le(n)),o=Ee.createServiceManager(s);switch(t){case"start":return je(s),o.start(),void console.log(`Service started: ${s.launchdLabel}`);case"stop":return o.stop(),void console.log(`Service stopped: ${s.launchdLabel}`);case"restart":return je(s),o.restart(),void console.log(`Service restarted: ${s.launchdLabel}`);case"status":return void function(e,t){const n={...te(e,t),...Be(e.stateDbPath)},s=[["Config exists",n.configExists?"yes":"no"],["Loaded",n.loaded?"yes":"no"],["Running",n.running?"yes":"no"],["Auto start on login",n.autoStartOnLogin?"yes":"no"],["PID",n.pid??"-"],["State",n.state??"-"],["Last exit code",n.lastExitCode??"-"],["Collector ID",n.collectorId??"-"],["Upload URL",n.backendUrl?`${n.backendUrl}/claude-code-usage/upload`:"-"],["Register URL",n.backendUrl?`${n.backendUrl}/claude-code-usage/collectors/register`:"-"],["Interval",`${n.intervalSeconds}s`],["Projects dirs",n.claudeProjectsDirs.join(", ")],["Buffering batches",n.bufferingBatchCount],["Pending batches",n.pendingBatchCount],["Retrying batches",n.retryingBatchCount],["Queued sessions",n.queuedSessions],["Queued events",n.queuedEvents],["Config file",n.configFile],["State DB",n.stateDbPath],["Stdout log",n.stdoutLogPath],["Stderr log",n.stderrLogPath],["Service plist",n.plistPath],["LaunchAgent plist",n.launchAgentPath],["Label",n.label]];for(const[e,t]of s)console.log(`${e}: ${t}`)}(s,o.status());case"logs":{const e=we(s.stdoutLogPath,n.lines),t=we(s.stderrLogPath,n.lines);return console.log(`== stdout (${s.stdoutLogPath}) ==`),console.log(e.length?e.join("\n"):"(empty)"),console.log(`== stderr (${s.stderrLogPath}) ==`),void console.log(t.length?t.join("\n"):"(empty)")}case"uninstall":return o.uninstall(),Ee.removePathFromShellConfigs(),e.rmSync(s.installRoot,{recursive:!0,force:!0}),void console.log(`${N} uninstalled from ${s.installRoot}`);default:throw new Error(`Unknown lifecycle command: ${t??"(empty)"}`)}}(s,a),0}}catch(e){return console.error(e instanceof Error?e.message:String(e)),!1!==e?.showUsage&&console.error(`Run \`${C} --help\` for usage.`),1}}export{Ee as cliDeps,Ue as main,ke as parseCliArgs};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@token-dashboard/claude-code-usage-uploader",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Claude Code 用量上报 CLI",
5
5
  "type": "module",
6
6
  "bin": {