@token-dashboard/codex-usage-uploader 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/codex-usage-uploader.js +5 -0
- package/dist/cli.mjs +1 -0
- package/package.json +9 -4
- package/bin/codex-usage-uploader.js +0 -9
- package/src/auth.js +0 -56
- package/src/cli.js +0 -759
- package/src/collector.js +0 -676
- package/src/constants.js +0 -44
- package/src/install.js +0 -156
- package/src/launchd.js +0 -170
- package/src/local-usage.js +0 -342
- package/src/parser.js +0 -180
- package/src/runtime-config.js +0 -182
- package/src/state-db.js +0 -325
- package/src/utils.js +0 -68
package/dist/cli.mjs
ADDED
|
@@ -0,0 +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 r}from"node:process";import{Buffer as a}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";function h(t){try{const n=JSON.parse(e.readFileSync(t,"utf8")),o=n?.tokens;if(!o||"object"!=typeof o)return{};const s=function(e){if(!e)return{};const t=String(e).split(".");if(t.length<2||!t[1])return{};try{const e=Buffer.from(t[1],"base64url").toString("utf8"),n=JSON.parse(e);return n&&"object"==typeof n?n:{}}catch{return{}}}(o.id_token),i={};return"string"==typeof s.email&&s.email.trim()&&(i.employeeEmail=s.email.trim()),"string"==typeof s.name&&s.name.trim()&&(i.employeeName=s.name.trim()),i}catch{return{}}}function p(e){return Boolean(e.employeeId||e.employeeEmail||e.employeeName)}async function m(e,t){const n=s.createInterface({input:r,output:i});try{const o=t?` [${t}]`:"";return(await n.question(`${e}${o}: `)).trim()||t||null}finally{n.close()}}function g(){return Date.now()/1e3}function f(e){return new Promise(t=>setTimeout(t,e))}function y(e){return JSON.stringify(b(e))}function b(e){return Array.isArray(e)?e.map(b):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,b(e[t])])):e}function S(e){if(!e)return null;const t=String(e).replace("Z","+00:00"),n=Date.parse(t);return Number.isNaN(n)?null:n}function E(e,n){if(e){let t=e.replace(/\/+$/,"").split("/").pop()??"";if(t.endsWith(".git")&&(t=t.slice(0,-4)),t)return t}return n&&t.basename(n)||null}function k(e,t,n){const o=a.from(`${e}|${t}|${n}`,"utf8");return l("sha1").update(o).digest("hex")}class _{constructor(e,t,n={}){this.collectorIdentity=e,this.relpath=t,this.currentSession=n.current_session??null,this.currentTurn=n.current_turn??null}exportState(){return{current_session:this.currentSession,current_turn:this.currentTurn}}normalizeSession(e){if(!e?.id)return null;const t=e.git&&"object"==typeof e.git?e.git:{},n=e.cwd??null,o=t.repository_url??null;return{sessionId:String(e.id),sessionTimestamp:S(e.timestamp),cwd:n,originator:e.originator??null,source:e.source??null,cliVersion:e.cli_version??null,modelProvider:e.model_provider??null,repoName:E(o,n),gitBranch:t.branch??null,gitCommitHash:t.commit_hash??null,repositoryUrl:o}}normalizeTurn(e,t){const n=e?.turn_id,o=this.currentSession?.sessionId;if(!n||!o)return null;const s=e.sandbox_policy&&"object"==typeof e.sandbox_policy?e.sandbox_policy:{},i=Array.isArray(s.writable_roots)?s.writable_roots:null,r=e.collaboration_mode&&"object"==typeof e.collaboration_mode?e.collaboration_mode.mode:null;return{turnId:String(n),sessionId:String(o),eventTimestamp:t,cwd:e.cwd??null,currentDate:e.current_date??null,timezone:e.timezone??null,approvalPolicy:e.approval_policy??null,sandboxPolicyType:s.type??null,sandboxNetworkAccess:s.network_access??null,sandboxWritableRootsJson:i?y(i):null,sandboxPolicyJson:Object.keys(s).length?y(s):null,model:e.model??null,personality:e.personality??null,collaborationMode:r??null,effort:e.effort??null,summary:e.summary??null,truncationPolicyJson:null!=e.truncation_policy?y(e.truncation_policy):null}}normalizeTokenEvent(e,t,n){if(!this.currentSession||null==t)return null;const o=e?.info;if(!o||"object"!=typeof o)return null;const s=o.total_token_usage&&"object"==typeof o.total_token_usage?o.total_token_usage:{},i=o.last_token_usage&&"object"==typeof o.last_token_usage?o.last_token_usage:{},r=e.rate_limits&&"object"==typeof e.rate_limits?e.rate_limits:{},a=r.primary&&"object"==typeof r.primary?r.primary:{},l=r.secondary&&"object"==typeof r.secondary?r.secondary:{},c=this.currentSession,u=this.currentTurn??{};return{eventUid:k(this.collectorIdentity.collectorId,this.relpath,n),sessionId:c.sessionId,turnId:u.turnId??null,sourceFileRelpath:this.relpath,lineNo:n,timestamp:t,model:u.model??null,cwd:u.cwd??c.cwd??null,timezone:u.timezone??null,approvalPolicy:u.approvalPolicy??null,sandboxPolicyType:u.sandboxPolicyType??null,source:c.source??null,originator:c.originator??null,cliVersion:c.cliVersion??null,repositoryUrl:c.repositoryUrl??null,repoName:c.repoName??null,gitBranch:c.gitBranch??null,gitCommitHash:c.gitCommitHash??null,totalInputTokens:s.input_tokens??null,totalCachedInputTokens:s.cached_input_tokens??null,totalOutputTokens:s.output_tokens??null,totalReasoningOutputTokens:s.reasoning_output_tokens??null,totalTokens:s.total_tokens??null,lastInputTokens:i.input_tokens??null,lastCachedInputTokens:i.cached_input_tokens??null,lastOutputTokens:i.output_tokens??null,lastReasoningOutputTokens:i.reasoning_output_tokens??null,lastTotalTokens:i.total_tokens??null,modelContextWindow:o.model_context_window??null,rateLimitPlanType:r.plan_type??null,primaryUsedPercent:a.used_percent??null,primaryWindowMinutes:a.window_minutes??null,primaryResetsAt:a.resets_at??null,secondaryUsedPercent:l.used_percent??null,secondaryWindowMinutes:l.window_minutes??null,secondaryResetsAt:l.resets_at??null,credits:T(r.credits),rawRateLimitsJson:Object.keys(r).length?y(r):null}}processLine(e,t){let n;try{n=JSON.parse(t)}catch{return{sessions:[],turns:[],events:[]}}const o=n.type,s=n.payload??{},i=S(n.timestamp),r=[],a=[],l=[];if("session_meta"===o){const e=this.normalizeSession(s);e&&(this.currentSession=e,r.push(e))}else if("turn_context"===o){const e=this.normalizeTurn(s,i);e&&(this.currentTurn=e,a.push(e))}else if("event_msg"===o&&"token_count"===s.type){const t=this.normalizeTokenEvent(s,i,e);t&&l.push(t)}return{sessions:r,turns:a,events:l}}}function T(e){if("number"==typeof e&&Number.isFinite(e))return e;if(e&&"object"==typeof e){const t=e.balance;if("number"==typeof t&&Number.isFinite(t))return t}return null}const v="Codex 用量上报",P="codex-usage-uploader",w="sessions",L="archived_sessions",N=t.join(n.homedir(),".codex","auth.json"),I=t.join(n.homedir(),".codex","sessions"),$=t.join(n.homedir(),".codex-usage-uploader"),D=t.join($,"config.json"),B=t.join($,"state.sqlite"),C=t.join($,"logs"),A=t.join(C,"stdout.log"),R=t.join(C,"stderr.log"),F=t.join($,"app");t.join(F,"current");const O=t.join($,"bin");t.join(O,P),t.join(n.homedir(),"bin",P);const x="com.token-dashboard.codex-usage-uploader";t.join($,"launchd",`${x}.plist`);const U=t.join(n.homedir(),"Library","LaunchAgents",`${x}.plist`),j=t.join(n.homedir(),".codex-usage-uploader-collector-id"),M="http://101.126.66.51:8086",z=1e6;class H{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=g(),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=g();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,y(i),g())}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=y(e),o=Buffer.byteLength(n,"utf8"),s=e.sessions?.length??0,i=e.turns?.length??0,r=e.events?.length??0,a=g();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,r,a,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,r,a,a),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=g(),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)>=z||g()-Number(e.created_at)>=60;if(t){const t=g();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),g(),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(g()-n))}}}class q{constructor({sessionsDir:e,stateDbPath:t,backendUrl:n,intervalSeconds:o,codexAuthPath:s,persistentCollectorIdPath:i=j,scanChunkMaxEvents:r=50,scanChunkMaxBytes:a=262144}){this.sessionsDir=e,this.stateDb=new H(t),this.backendUrl=n?.replace(/\/+$/,"")||null,this.intervalSeconds=o,this.codexAuthPath=s,this.persistentCollectorIdPath=i,this.scanChunkMaxEvents=r,this.scanChunkMaxBytes=a,this.identity=this.ensureIdentity()}close(){this.stateDb.close()}ensureIdentity(){const t=this.stateDb.getIdentity();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)}`}!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());const o=h(this.codexAuthPath);for(const e of["employeeEmail","employeeName"])!t[e]&&o[e]&&(t[e]=o[e]);return this.stateDb.setIdentity(t),this.stateDb.getIdentity()}resolveIdentityValues({employeeId:e,employeeEmail:t,employeeName:n,deviceId:o,hostname:s}={}){const i=this.stateDb.getIdentity(),r=h(this.codexAuthPath);return{employeeId:void 0!==e?e:i.employeeId??null,employeeEmail:void 0!==t?t:i.employeeEmail??r.employeeEmail??null,employeeName:void 0!==n?n:i.employeeName??r.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(!p(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={}){const t=this.resolveIdentityValues(e);return this.configureIdentity(t)}async configureIdentityInteractive(e={}){const t=this.resolveIdentityValues(e),n=h(this.codexAuthPath);return(n.employeeEmail||n.employeeName)&&console.log("Detected identity defaults from ~/.codex/auth.json. Press Enter to accept."),void 0===e.employeeEmail&&(t.employeeEmail=await m("Employee email",t.employeeEmail)),p(t)||(void 0===e.employeeName&&(t.employeeName=await m("Employee name",t.employeeName)),p(t)||void 0===e.employeeId&&(t.employeeId=await m("Employee ID",t.employeeId))),this.configureIdentity(t)}resetBackfillState(){this.stateDb.resetBackfillState()}getQueueStats(){return this.stateDb.getQueueStats()}getArchivedSessionsDir(){return t.join(t.dirname(this.sessionsDir),L)}getScanRoots({includeArchived:e=!1}={}){const t=[{sourceRoot:w,dir:this.sessionsDir}];return e&&t.push({sourceRoot:L,dir:this.getArchivedSessionsDir()}),t}iterRolloutFiles(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()&&/^rollout-.*\.jsonl$/.test(i.name)&&o.push(e)}};return s(n),o.sort(),o}buildSnapshot({includeArchived:n=!1}={}){const o=[];for(const s of this.getScanRoots({includeArchived:n}))for(const n of this.iterRolloutFiles(s.dir)){const i=t.relative(s.dir,n).split(t.sep).join("/"),r=e.statSync(n,{bigint:!0}),a=this.stateDb.getFileState(s.sourceRoot,i),l=a||s.sourceRoot!==L?null:this.stateDb.getFileState(w,i);a&&a.file_size===r.size&&a.file_mtime_ns===r.mtimeNs||o.push({filePath:n,relpath:i,sourceRoot:s.sourceRoot,progressPath:`${s.sourceRoot}/${i}`,snapshotSize:r.size,snapshotMtimeNs:r.mtimeNs,fallbackState:l})}return o}buildBackfillSnapshot(){return this.buildSnapshot({includeArchived:!0})}emptyPayload(){return{sessions:[],turns:[],events:[]}}payloadHasData(e){return Boolean(e.sessions.length||e.turns.length||e.events.length)}payloadBytes(e){return Buffer.byteLength(y(e),"utf8")}mergePayload(e,t){const n=new Map((e.sessions??[]).map(e=>[e.sessionId,e])),o=new Map((e.turns??[]).map(e=>[e.turnId,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.turns??[])o.set(e.turnId,e);for(const e of t.events??[])s.set(e.eventUid,e);return{sessions:[...n.values()],turns:[...o.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)>z)}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,turns:0,events:0,batchesQueued:0};for(const i of t){const{filePath:t,relpath:r,sourceRoot:a,snapshotSize:l,snapshotMtimeNs:c,fallbackState:u}=i,d=this.stateDb.getFileState(a,r),h=d??u??null;if(s.filesScanned+=1,!d&&u&&u.file_size===l&&u.file_mtime_ns===c){this.stateDb.upsertFileState(a,r,l,c,Number(u.last_line_no),JSON.parse(u.state_json||"{}")),n?.({entry:i,filesProcessed:s.filesScanned,totals:{filesScanned:s.filesScanned,sessions:s.sessions,turns:s.turns,events:s.events,batchesQueued:s.batchesQueued}});continue}let p={},m=0;h&&l>h.file_size&&(m=Number(h.last_line_no),p=JSON.parse(h.state_json||"{}"));const g=new _(this.identity,r,p);let f=this.emptyPayload(),y=0,b=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(b+=1,!e)continue;if(y=b,b<=m)continue;const t=g.processLine(b,e);s.sessions+=t.sessions.length,s.turns+=t.turns.length,s.events+=t.events.length,f=this.mergePayload(f,t),this.shouldFlushChunk(f)&&(s.batchesQueued+=this.appendPayloadToBuffer(f),f=this.emptyPayload())}}finally{i.close()}}this.payloadHasData(f)&&(s.batchesQueued+=this.appendPayloadToBuffer(f)),this.stateDb.upsertFileState(a,r,l,c,y,g.exportState()),n?.({entry:i,filesProcessed:s.filesScanned,totals:{filesScanned:s.filesScanned,sessions:s.sessions,turns:s.turns,events:s.events,batchesQueued:s.batchesQueued}})}return s}async scanRollouts(){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}}sanitizeUploadPayload(e){return{sessions:Array.isArray(e.sessions)?e.sessions:[],turns:Array.isArray(e.turns)?e.turns:[],events:Array.isArray(e.events)?e.events.map(e=>({...e,credits:"number"==typeof e?.credits&&Number.isFinite(e.credits)?e.credits:e?.credits&&"object"==typeof e.credits&&"number"==typeof e.credits.balance&&Number.isFinite(e.credits.balance)?e.credits.balance: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&&g()-Number(e)<600||(await this.postJson("/codex-usage/collectors/register",this.collectorRequestBody()),this.stateDb.setCheckpoint("last_register_at",String(g())))}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 r=this.collectorRequestBody(),a=this.stateDb.iterPendingBatches(),l=async e=>{const t=this.sanitizeUploadPayload(JSON.parse(e.payload_json)),n={idempotencyKey:e.batch_key,collector:r,payloadSizeBytes:Number(e.payload_bytes),sessions:t.sessions??[],turns:t.turns??[],events:t.events??[]};await this.postJson("/codex-usage/upload",n)},c=new Set;let u=0;const d=()=>{for(;c.size<t&&u<a.length;){const t=a[u++],r=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(r)});c.add(r)}};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.scanRollouts();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.buildBackfillSnapshot(),s=o.length;let i;t?.({phase:"start",totalFiles:s,filesProcessed:0,eventsParsed:0,batchesQueued:0});try{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})}})}catch(e){throw t?.({phase:"error",stage:"scan",message:e instanceof Error?e.message:String(e)}),e}i.batchesQueued+=this.stateDb.sealStaleBatches(!0),await this.ensureRemoteRegistration();const r=this.getQueueStats(),a={totalFiles:s,filesProcessed:s,eventsParsed:i.events,sessionsParsed:i.sessions,turnsParsed:i.turns,batchesQueued:i.batchesQueued,pendingBatches:r.pendingBatchCount+r.retryingBatchCount,pendingEvents:r.queuedEvents,durationMs:Date.now()-n};return t?.({phase:"done",...a}),a}async watch(){let e=!0;for(;;){const t=await this.runCycle({forceSeal:e});console.log(`[uploader] scanned_files=${t.filesScanned} sessions=${t.sessions} turns=${t.turns} events=${t.events} queued_batches=${t.batchesQueued} uploaded_batches=${t.uploadedBatches} remaining_batches=${t.remainingQueuedBatches}`),e=!1,await f(1e3*this.intervalSeconds)}}}const Q=t.basename($);function W(e){const n=t.basename(e);if(!n.startsWith(`${Q}-`))return"";const o=n.slice(Q.length+1);return o?`-${o}`:""}function Y(o=D){const s=function(e=D){const o=t.dirname(e),s=W(o),i=t.join(o,"app"),r=t.join(i,"current"),a=`${x}${s}`;return{configFile:e,installRoot:o,appRoot:i,currentAppDir:r,backendUrl:M,intervalSeconds:30,codexAuthPath:N,sessionsDir:I,stateDbPath:t.join(o,t.basename(B)),nodePath:process.execPath,entryFile:"",packageSpec:"",localBinDir:t.join(o,"bin"),localBinPath:t.join(o,"bin",P),homeBinLink:t.join(n.homedir(),"bin",`${P}${s}`),stdoutLogPath:t.join(o,"logs",t.basename(A)),stderrLogPath:t.join(o,"logs",t.basename(R)),launchdLabel:a,plistPath:t.join(o,"launchd",`${a}.plist`),launchAgentPath:t.join(t.dirname(U),`${a}.plist`),autoStartOnLogin:!0}}(o);try{const t=JSON.parse(e.readFileSync(o,"utf8"));return t&&"object"==typeof t?J({...s,...t,configFile:o}):s}catch{return s}}function J(e){const o=e.installRoot||$,s=W(o),i=e.appRoot||t.join(o,"app"),r=e.currentAppDir||t.join(i,"current"),a=e.localBinDir||t.join(o,"bin"),l=e.launchdLabel||`${x}${s}`,c=t.join(o,"launchd",`${l}.plist`),u=t.join(t.dirname(U),`${l}.plist`),d=t.join(t.dirname(U),`${l}.plist`),h=e.plistPath||c;return{...e,configFile:e.configFile||D,installRoot:o,appRoot:i,currentAppDir:r,backendUrl:e.backendUrl?.trim()?e.backendUrl.replace(/\/+$/,""):null,intervalSeconds:Number(e.intervalSeconds)||30,codexAuthPath:e.codexAuthPath||N,sessionsDir:e.sessionsDir||I,stateDbPath:e.stateDbPath||t.join(o,"state.sqlite"),nodePath:e.nodePath||process.execPath,entryFile:e.entryFile||"",packageSpec:e.packageSpec||"",localBinDir:a,localBinPath:e.localBinPath||t.join(a,P),homeBinLink:e.homeBinLink||t.join(n.homedir(),"bin",`${P}${s}`),stdoutLogPath:e.stdoutLogPath||t.join(o,"logs","stdout.log"),stderrLogPath:e.stderrLogPath||t.join(o,"logs","stderr.log"),launchdLabel:l,plistPath:h===d?c:h,launchAgentPath:e.launchAgentPath||u,autoStartOnLogin:void 0===e.autoStartOnLogin||Boolean(e.autoStartOnLogin)}}function G(n){const o=J(n);return e.mkdirSync(t.dirname(o.configFile),{recursive:!0}),e.mkdirSync(t.dirname(o.stdoutLogPath),{recursive:!0}),e.writeFileSync(o.configFile,`${y({backendUrl:o.backendUrl,intervalSeconds:o.intervalSeconds,codexAuthPath:o.codexAuthPath,sessionsDir:o.sessionsDir,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,autoStartOnLogin:o.autoStartOnLogin})}\n`),o}function X(e,t={}){return J({...Y(e),...t,configFile:e})}function V(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,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 K="# Added by codex-usage-uploader",Z=[t.join(n.homedir(),".zshrc"),t.join(n.homedir(),".bash_profile")];function ee(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 te(e){return String(e).replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}class ne{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>${te(e.launchdLabel)}</string>\n <key>ProgramArguments</key>\n <array>\n${t.map(e=>` <string>${te(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>${te(e.installRoot)}</string>\n <key>StandardOutPath</key>\n <string>${te(e.stdoutLogPath)}</string>\n <key>StandardErrorPath</key>\n <string>${te(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 oe=Object.freeze({collectorId:"local-usage"}),se=new Set(["today","7d","30d","all"]),ie=/^\d{4}-\d{2}-\d{2}$/;function re(){return Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"}function ae(){return{inputTokens:0,cachedInputTokens:0,outputTokens:0,reasoningOutputTokens:0,totalTokens:0}}function le(e){return{model:e.model??"(unknown)",inputTokens:ue(e.lastInputTokens),cachedInputTokens:ue(e.lastCachedInputTokens),outputTokens:ue(e.lastOutputTokens),reasoningOutputTokens:ue(e.lastReasoningOutputTokens),totalTokens:ue(e.lastTotalTokens)}}function ce(e,t){e.inputTokens+=t.inputTokens,e.cachedInputTokens+=t.cachedInputTokens,e.outputTokens+=t.outputTokens,e.reasoningOutputTokens+=t.reasoningOutputTokens,e.totalTokens+=t.totalTokens}function ue(e){return"number"==typeof e&&Number.isFinite(e)?e:0}function de(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 he(e,t){if(!ie.test(String(e)))throw new Error(`${t} must be in YYYY-MM-DD format`);const[n,o,s]=String(e).split("-"),i=Number(n),r=Number(o),a=Number(s),l=new Date(Date.UTC(i,r-1,a));if(Number.isNaN(l.getTime())||l.getUTCFullYear()!==i||l.getUTCMonth()!==r-1||l.getUTCDate()!==a)throw new Error(`${t} must be a valid calendar date`);return`${n}-${o}-${s}`}function pe(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")}`}function me(e,t,n){return!(t&&e<t||n&&e>n)}const ge=`${v}\n\nUsage:\n ${P} init [--backend-url <url>] [--interval 30] [--yes] [--package-spec <spec>]\n ${P} bind [--email <email>] [--employee-name <name>] [--employee-id <id>] [--yes]\n ${P} clear [--yes]\n ${P} start\n ${P} stop\n ${P} restart\n ${P} status\n ${P} usage [--period <today|7d|30d|all>] [--from <date>] [--to <date>]\n ${P} logs [--lines 100]\n ${P} uninstall\n\nCommon options:\n --config-file <path> Runtime config path. Default: ${D}\n --backend-url <url> Dashboard backend URL\n --interval <seconds> Scan interval in seconds\n --codex-auth <path> Codex auth.json path\n --sessions-dir <path> Codex rollout root directory\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 fe extends Error{constructor(e){super(e),this.name="CliOperationalError",this.showUsage=!1}}const ye={findPackageRoot:ee,installCurrentPackage:function(n,{packageRoot:o,packageSpec:s,nodePath:i=process.execPath}={}){const r=function(e,t){return t||`file:${e}`}(o,s),a=n.appRoot,l=t.join(a,`.staging-${Date.now()}`);e.mkdirSync(l,{recursive:!0}),e.writeFileSync(t.join(l,"package.json"),`${JSON.stringify({private:!0,name:"codex-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",r],{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 ${r}`);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?.[P]??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=r,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="\${CODEX_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),G(n),n},ensurePathInShellConfigs:function(t){const n=`export PATH="${t}:$PATH"`;for(const t of Z)try{if(!e.existsSync(t))continue;const o=e.readFileSync(t,"utf8");if(o.includes(K))continue;const s=o.length>0&&!o.endsWith("\n")?"\n":"";e.appendFileSync(t,`${s}\n${K}\n${n}\n`)}catch{}},removePathFromShellConfigs:function(){for(const t of Z)try{if(!e.existsSync(t))continue;const n=e.readFileSync(t,"utf8");if(!n.includes(K))continue;const o=n.split("\n"),s=[];for(let e=0;e<o.length;e++)o[e]!==K?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 m(`${e} (${t?"Y/n":"y/N"})`,n);return/^(y|yes)$/i.test(String(o??"").trim())},createUploader:e=>new q({sessionsDir:e.sessionsDir,stateDbPath:e.stateDbPath,backendUrl:e.backendUrl,intervalSeconds:e.intervalSeconds,codexAuthPath:e.codexAuthPath,persistentCollectorIdPath:e.persistentCollectorIdPath}),createServiceManager:e=>new ne(e)};function be(e){const t={configFile:D,interval:void 0,yes:!1,lines:100},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,r]=s.split("=",2);if(!new Set(["--config-file","--backend-url","--interval","--codex-auth","--sessions-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 a=r??e[++o];if(null==a||a.startsWith("-"))throw new Error(`Missing value for ${i}`);Se(t,i,a)}return{command:n[0]??null,subcommand:n[1]??null,extraPositionals:n.slice(2),options:t}}function Se(e,t,n){switch(t){case"--config-file":e.configFile=n;break;case"--backend-url":e.backendUrl=n;break;case"--interval":e.interval=Ee(n,"--interval");break;case"--codex-auth":e.codexAuthPath=n;break;case"--sessions-dir":e.sessionsDir=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=Ee(n,"--lines");break;default:throw new Error(`Unknown argument: ${t}`)}}function Ee(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 ke(e){const t={};return void 0!==e.backendUrl&&(t.backendUrl=e.backendUrl),void 0!==e.interval&&(t.intervalSeconds=e.interval),void 0!==e.codexAuthPath&&(t.codexAuthPath=e.codexAuthPath),void 0!==e.sessionsDir&&(t.sessionsDir=e.sessionsDir),void 0!==e.stateDbPath&&(t.stateDbPath=e.stateDbPath),t}function _e(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 Te(e){if(!p(e.identity))throw new fe("No employee identity is bound yet. Run `codex-usage-uploader bind` or rerun `init` with --email.")}function ve(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 Pe(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 we(e,t){const n=_e(t);if(t.yes){const t=e.configureIdentityWithDefaults(n);return Te(e),t}const o=await e.configureIdentityInteractive(n);return Te(e),o}function Le(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`}`);case"error":return void console.error(`[scan] failed stage=${e.stage} message=${e.message}`)}var t}function Ne(e){return Number(e??0).toLocaleString("en-US")}function Ie(e){const[t,n,o]=e.split("-").map(Number);return new Intl.DateTimeFormat("en-US",{timeZone:"UTC",month:"short",day:"numeric",year:"numeric"}).format(new Date(Date.UTC(t,n-1,o)))}async function $e(n){const s=X(n.configFile,ke(n));let i;console.log("Scanning local Codex usage...");try{i=await async function({sessionsDir:n,period:s,from:i,to:r,now:a=Date.now(),timeZone:l=re(),includeArchived:c=!0}={}){const u=function({period:e,from:t,to:n,now:o=Date.now(),timeZone:s=re()}={}){if(e&&(t||n))throw new Error("--period cannot be combined with --from or --to");if(e&&!se.has(e))throw new Error("--period must be one of: today, 7d, 30d, all");const i=de(o,s);if("today"===e)return{mode:"today",from:i,to:i,trendFrom:i,trendTo:i};if("7d"===e)return{mode:"7d",from:pe(i,-6),to:i,trendFrom:pe(i,-6),trendTo:i};if("30d"===e)return{mode:"30d",from:pe(i,-29),to:i,trendFrom:pe(i,-29),trendTo:i};if("all"===e||!e&&!t&&!n)return{mode:"all",from:null,to:null,trendFrom:pe(i,-29),trendTo:i};const r=null==t?null:he(t,"--from"),a=null==n?null:he(n,"--to");if(r&&a&&r>a)throw new Error("--from cannot be later than --to");return{mode:"custom",from:r,to:a,trendFrom:r,trendTo:a}}({period:s,from:i,to:r,now:a,timeZone:l}),d=t.join(t.dirname(n),L),h=[{sourceRoot:w,dir:n}];c&&h.push({sourceRoot:L,dir:d});const p=h.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()&&/^rollout-.*\.jsonl$/.test(i.name)&&o.push(e)}};return s(n),o}(n.dir).map(e=>({...n,filePath:e,relpath:t.relative(n.dir,e).split(t.sep).join("/")})));p.sort((e,t)=>e.filePath.localeCompare(t.filePath));const m={inputTokens:0,cachedInputTokens:0,outputTokens:0,reasoningOutputTokens:0,totalTokens:0},g=new Map,f=new Map,y=new Map;let b=0;for(const t of p){const n=new _(oe,`${t.sourceRoot}/${t.relpath}`),s=e.createReadStream(t.filePath,{encoding:"utf8"}),i=o({input:s,crlfDelay:1/0});let r=0;try{for await(const e of i){if(r+=1,!e)continue;const t=n.processLine(r,e);for(const e of t.events){const t=de(e.timestamp,l);if(!me(t,u.from,u.to))continue;const n=le(e);b+=1,ce(m,n);const o=g.get(t)??{date:t,eventCount:0,models:new Set,...ae()};o.eventCount+=1,o.models.add(n.model),ce(o,n),g.set(t,o);const s=y.get(n.model)??{model:n.model,eventCount:0,...ae()};if(s.eventCount+=1,ce(s,n),y.set(n.model,s),me(t,u.trendFrom,u.trendTo)){const e=f.get(t)??{date:t,eventCount:0,...ae()};e.eventCount+=1,ce(e,n),f.set(t,e)}}}}finally{i.close()}}const S=[...g.values()].sort((e,t)=>e.date.localeCompare(t.date)).map(e=>({...e,models:[...e.models].sort((e,t)=>e.localeCompare(t))})),E=function(e,t){const n=[...t.values()].sort((e,t)=>e.date.localeCompare(t.date));if(0===n.length)return[];if(!e.trendFrom||!e.trendTo||e.trendFrom>e.trendTo)return n;const o=[];for(let n=e.trendFrom;n<=e.trendTo;n=pe(n,1))o.push(t.get(n)??{date:n,eventCount:0,...ae()});return o}(u,f),k=[...y.values()].sort((e,t)=>t.totalTokens!==e.totalTokens?t.totalTokens-e.totalTokens:e.model.localeCompare(t.model));return{scope:"local-machine",timezone:l,range:u,sources:{sessionsDir:n,archivedSessionsDir:d,includeArchived:c},filesScanned:p.length,tokenEventCount:b,summary:m,days:S,trend:E,byModel:k}}({sessionsDir:s.sessionsDir,period:n.period,from:n.from,to:n.to,timeZone:re()})}catch(e){throw function(e){return new fe(`Usage query failed: ${e instanceof Error?e.message:String(e)}`)}(e)}!function(e){if(0===e.tokenEventCount)return void console.log("No local Codex usage found for selected range.");const t=e.days.map(e=>[Ie(e.date),e.models.join(", "),Ne(e.inputTokens),Ne(e.outputTokens),Ne(e.reasoningOutputTokens),Ne(e.cachedInputTokens),Ne(e.totalTokens)]);t.push(["Total","",Ne(e.summary.inputTokens),Ne(e.summary.outputTokens),Ne(e.summary.reasoningOutputTokens),Ne(e.summary.cachedInputTokens),Ne(e.summary.totalTokens)]),console.log(function(e,t,n=[]){const o=e.map((e,n)=>Math.max(e.length,...t.map(e=>String(e[n]??"").length))),s=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();return[s(e),...t.map(e=>s(e))].join("\n")}(["Date","Models","Input","Output","Reasoning","Cache Read","Total Tokens"],t,["left","left","right","right","right","right","right"]))}(i)}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 H(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 fe(`Uploader is not initialized yet. Run \`${P} init\` first.`)}async function Ce(n=process.argv.slice(2)){try{const{command:o,subcommand:s,extraPositionals:i,options:r}=be(n);if(r.version)return console.log(function(n=import.meta.url){const o=ee(n);return JSON.parse(e.readFileSync(t.join(o,"package.json"),"utf8")).version??"unknown"}()),0;if(!o||r.help)return console.log(ge),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=ke(e);n.backendUrl||(n.backendUrl=M);let o=X(e.configFile,n);if(!o.backendUrl)throw new Error("--backend-url is required for init unless already configured in config.json");o=ye.installCurrentPackage(o,{packageRoot:ye.findPackageRoot(),packageSpec:e.packageSpec,nodePath:process.execPath});let s=ye.createServiceManager(o);s.stop();const i=ye.createUploader(o);try{let n,r;n=!Object.values(_e(e)).some(e=>void 0!==e)&&p(i.identity)?i.identity:await we(i,e),console.log(`${v} install is ready.`),console.log(`Backend: ${o.backendUrl}`),console.log(`Install root: ${o.installRoot}`),console.log(`Config file: ${o.configFile}`),ve(n),console.log("Scanning local Codex sessions...");try{r=await i.runForegroundCatchUp({onProgress:Le})}catch(e){throw function(e){return new fe(`Foreground catch-up failed: ${e instanceof Error?e.message:String(e)}`)}(e)}o.autoStartOnLogin=await async function(e,t){return t.yes?e.autoStartOnLogin:ye.promptConfirm("Start automatically when you log in on this Mac?",e.autoStartOnLogin)}(o,e),o=G(o),s=ye.createServiceManager(o),s.start(),console.log(`${v} initialized and started.`),console.log(o.autoStartOnLogin?"Login auto-start is enabled.":"Login auto-start is disabled for future logins."),r.pendingBatches>0&&console.log(`Background service is uploading ${r.pendingBatches} batch(es) with ${r.pendingEvents} event(s).`),console.log(`Use \`${P} status\` to check the local service.`);const a=t.dirname(o.homeBinLink);(process.env.PATH||"").split(t.delimiter).includes(a)||(ye.ensurePathInShellConfigs(a),console.log(`Open a new terminal to use \`${P}\` directly.`))}finally{i.close()}}(r),0;case"bind":return await async function(t){const n=X(t.configFile,ke(t)),o=ye.createUploader(n);try{ve(await we(o,t)),await async function(t){if("darwin"!==process.platform)return!1;if(!e.existsSync(t.configFile)||!t.entryFile)return!1;const n=ye.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()}}(r),0;case"clear":return await async function(e){const t=X(e.configFile,ke(e));if(Be(t),ye.createServiceManager(t).status().running)throw new fe(`Background service is still running. Stop it first with \`${P} stop\`.`);if(!e.yes&&!await ye.promptConfirm("Clear local backfill state and queued uploads?",!1))return void console.log("Clear cancelled.");const n=ye.createUploader(t);try{n.resetBackfillState()}finally{n.close()}console.log("Local backfill state has been cleared."),console.log(`Run \`${P} init\` when you want to backfill history again.`)}(r),0;case"usage":return await $e(r),0;case"run":return await async function(e){const t=X(e.configFile,ke(e)),n=ye.createUploader(t);try{Te(n),console.log(`[run] backend=${t.backendUrl??"-"} interval=${t.intervalSeconds}s sessions_dir=${t.sessionsDir}`),await n.watch()}finally{n.close()}}(r),0;case"start":case"stop":case"restart":case"status":case"logs":case"uninstall":return function(t,n){const o=X(n.configFile,ke(n)),s=ye.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={...V(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}/codex-usage/upload`:"-"],["Register URL",n.backendUrl?`${n.backendUrl}/codex-usage/collectors/register`:"-"],["Interval",`${n.intervalSeconds}s`],["Buffering batches",n.bufferingBatchCount],["Pending batches",n.pendingBatchCount],["Retrying batches",n.retryingBatchCount],["Queued sessions",n.queuedSessions],["Queued turns",n.queuedTurns],["Queued events",n.queuedEvents],["Oldest pending age",null==n.oldestPendingAgeSeconds?"-":`${n.oldestPendingAgeSeconds}s`],["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=Pe(o.stdoutLogPath,n.lines),t=Pe(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(),ye.removePathFromShellConfigs(),e.rmSync(o.installRoot,{recursive:!0,force:!0}),void console.log(`${v} uninstalled from ${o.installRoot}`);default:throw new Error(`Unknown lifecycle command: ${t??"(empty)"}`)}}(o,r),0}}catch(e){return console.error(e instanceof Error?e.message:String(e)),!1!==e?.showUsage&&console.error(`Run \`${P} --help\` for usage.`),1}}export{ye as cliDeps,Ce as main,be as parseCliArgs};
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@token-dashboard/codex-usage-uploader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Codex 用量上报 CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codex-usage-uploader": "
|
|
7
|
+
"codex-usage-uploader": "dist/bin/codex-usage-uploader.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"src/",
|
|
10
|
+
"dist/",
|
|
12
11
|
"README.md"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
14
|
+
"build": "node scripts/build.mjs",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
15
16
|
"init": "node ./bin/codex-usage-uploader.js --config-file $HOME/.codex-usage-uploader-dev/config.json init --backend-url http://localhost:8086",
|
|
16
17
|
"start": "node bin/codex-usage-uploader.js --config-file $HOME/.codex-usage-uploader-dev/config.json start",
|
|
17
18
|
"stop": "node bin/codex-usage-uploader.js --config-file $HOME/.codex-usage-uploader-dev/config.json stop",
|
|
@@ -24,5 +25,9 @@
|
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=22.13.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
31
|
+
"rollup": "^4.60.2"
|
|
27
32
|
}
|
|
28
33
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
if (!process.env.NODE_NO_WARNINGS) {
|
|
3
|
-
process.env.NODE_NO_WARNINGS = '1';
|
|
4
|
-
}
|
|
5
|
-
process.emitWarning = () => {};
|
|
6
|
-
|
|
7
|
-
const { main } = await import('../src/cli.js');
|
|
8
|
-
const exitCode = await main(process.argv.slice(2));
|
|
9
|
-
process.exit(exitCode);
|
package/src/auth.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import readline from 'node:readline/promises';
|
|
3
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
-
|
|
5
|
-
export function decodeJwtClaims(token) {
|
|
6
|
-
if (!token) return {};
|
|
7
|
-
const parts = String(token).split('.');
|
|
8
|
-
if (parts.length < 2 || !parts[1]) return {};
|
|
9
|
-
try {
|
|
10
|
-
const decoded = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
11
|
-
const data = JSON.parse(decoded);
|
|
12
|
-
return data && typeof data === 'object' ? data : {};
|
|
13
|
-
} catch {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function loadCodexAuthIdentity(authPath) {
|
|
19
|
-
try {
|
|
20
|
-
const payload = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
21
|
-
const tokens = payload?.tokens;
|
|
22
|
-
if (!tokens || typeof tokens !== 'object') return {};
|
|
23
|
-
const claims = decodeJwtClaims(tokens.id_token);
|
|
24
|
-
const identity = {};
|
|
25
|
-
if (typeof claims.email === 'string' && claims.email.trim()) {
|
|
26
|
-
identity.employeeEmail = claims.email.trim();
|
|
27
|
-
}
|
|
28
|
-
if (typeof claims.name === 'string' && claims.name.trim()) {
|
|
29
|
-
identity.employeeName = claims.name.trim();
|
|
30
|
-
}
|
|
31
|
-
return identity;
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function identityIsBound(values) {
|
|
38
|
-
return Boolean(values.employeeId || values.employeeEmail || values.employeeName);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function promptValue(label, defaultValue) {
|
|
42
|
-
const rl = readline.createInterface({ input, output });
|
|
43
|
-
try {
|
|
44
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
45
|
-
const answer = (await rl.question(`${label}${suffix}: `)).trim();
|
|
46
|
-
return answer || defaultValue || null;
|
|
47
|
-
} finally {
|
|
48
|
-
rl.close();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function promptConfirm(label, defaultYes = false) {
|
|
53
|
-
const defaultValue = defaultYes ? 'y' : 'n';
|
|
54
|
-
const answer = await promptValue(`${label} (${defaultYes ? 'Y/n' : 'y/N'})`, defaultValue);
|
|
55
|
-
return /^(y|yes)$/i.test(String(answer ?? '').trim());
|
|
56
|
-
}
|