tokentracker-cli 0.3.0 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{b as p,al as G,d as ie,am as j,an as U,ao as ce,ap as ue,aq as le,ar as fe,as as de,at as ge,au as ke,a6 as pe,av as me,aw as ye,ax as he}from"./main-DSjEYl46.js";const be="Backend runtime unavailable (InsForge). Please retry later.",g={usageSummary:"vibeusage-usage-summary",usageDaily:"vibeusage-usage-daily",usageHourly:"vibeusage-usage-hourly",usageMonthly:"vibeusage-usage-monthly",usageHeatmap:"vibeusage-usage-heatmap",usageModelBreakdown:"vibeusage-usage-model-breakdown",projectUsageSummary:"vibeusage-project-usage-summary",leaderboard:"vibeusage-leaderboard",leaderboardProfile:"vibeusage-leaderboard-profile",userStatus:"vibeusage-user-status",linkCodeInit:"vibeusage-link-code-init",publicViewProfile:"vibeusage-public-view-profile",publicVisibility:"vibeusage-public-visibility"},J="/functions",we="/api/functions",y={business:"business",probe:"probe"};let z=null;async function d(e){return await ie(e)}async function xe({baseUrl:e,accessToken:t,signal:s}={}){const a=await d(t),n=he(new Date);return await m({baseUrl:e,accessToken:a,slug:g.usageSummary,params:{from:n,to:n},fetchOptions:{cache:"no-store",signal:s},retry:!1,requestKind:y.probe}),{status:200}}async function Ee({baseUrl:e,accessToken:t,from:s,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:o,rolling:c=!1}={}){const u=await d(t);if(p())return ue({from:s,to:a,seed:u,rolling:c});const i=M({timeZone:l,tzOffsetMinutes:o}),f=A({source:n,model:r}),_=c?{rolling:"1"}:{};return m({baseUrl:e,accessToken:u,slug:g.usageSummary,params:{from:s,to:a,...f,...i,..._}})}async function Re({baseUrl:e,accessToken:t,from:s,to:a,source:n,limit:r,timeZone:l,tzOffsetMinutes:o}={}){const c=await d(t);if(p())return fe({seed:c,limit:r});const u=M({timeZone:l,tzOffsetMinutes:o}),f={...A({source:n}),...u};return s&&(f.from=s),a&&(f.to=a),r!=null&&(f.limit=String(r)),m({baseUrl:e,accessToken:c,slug:g.projectUsageSummary,params:f})}async function Ce({baseUrl:e,accessToken:t,period:s,metric:a,limit:n,offset:r}={}){const l=await d(t);if(p())return G({seed:l,period:s,metric:a,limit:n,offset:r});const c=(typeof s=="string"?s:"week").trim().toLowerCase(),i={period:c==="month"||c==="total"||c==="week"?c:"week"};return a&&(i.metric=String(a)),n!=null&&(i.limit=String(n)),r!=null&&(i.offset=String(r)),m({baseUrl:e,accessToken:l,slug:g.leaderboard,params:i})}async function De({baseUrl:e,accessToken:t}={}){const s=await d(t);return p()?{enabled:!1,updated_at:null,share_token:null}:m({baseUrl:e,accessToken:s,slug:g.publicVisibility})}async function ze({baseUrl:e,accessToken:t,enabled:s}={}){const a=await d(t);return p()?{enabled:!!s,updated_at:new Date().toISOString(),share_token:s?"pv1-mock-token":null}:X({baseUrl:e,accessToken:a,slug:g.publicVisibility,body:{enabled:!!s}})}async function Ue({baseUrl:e,accessToken:t,userId:s,period:a}={}){const n=await d(t);if(p()){const r=G({seed:n,period:a,metric:"all",limit:250,offset:0}),o=(Array.isArray(r?.entries)?r.entries:[]).find(c=>c?.user_id===s)||null;return{period:r?.period??"week",from:r?.from??null,to:r?.to??null,generated_at:r?.generated_at??new Date().toISOString(),entry:o?{user_id:o.user_id??null,display_name:o.display_name??null,avatar_url:o.avatar_url??null,rank:o.rank??null,gpt_tokens:o.gpt_tokens??"0",claude_tokens:o.claude_tokens??"0",other_tokens:o.other_tokens??"0",total_tokens:o.total_tokens??"0"}:null}}return m({baseUrl:e,accessToken:n,slug:g.leaderboardProfile,params:{user_id:String(s||""),period:String(a||"")}})}async function Ie({baseUrl:e,accessToken:t}={}){const s=await d(t);if(p()){const a=new Date().toISOString();return{user_id:"mock-user",created_at:a,pro:{active:!0,sources:["mock"],expires_at:null,partial:!1,as_of:a},subscriptions:{partial:!1,as_of:a,items:[{tool:"codex",provider:"openai",product:"chatgpt",plan_type:"pro",updated_at:a},{tool:"claude",provider:"anthropic",product:"subscription",plan_type:"max",rate_limit_tier:"default_claude_max_5x",updated_at:a}]},install:{partial:!1,as_of:a,has_active_device_token:!1,has_active_device:!1,active_device_tokens:0,active_devices:0,latest_token_activity_at:null,latest_device_seen_at:null}}}return m({baseUrl:e,accessToken:s,slug:g.userStatus})}async function je({signal:e}={}){const t=await fetch("/functions/vibeusage-local-sync",{method:"POST",headers:{Accept:"application/json"},cache:"no-store",signal:e}),s=await t.json().catch(()=>({ok:!1,error:`Local sync request failed with HTTP ${t.status}`}));if(!t.ok||s?.ok===!1){const a=s?.error||s?.message||`Local sync request failed with HTTP ${t.status}`,n=new Error(a);throw n.status=t.status,n.statusCode=t.status,n.payload=s,n}return s}async function He({baseUrl:e,accessToken:t,from:s,to:a,source:n,timeZone:r,tzOffsetMinutes:l}={}){const o=await d(t);if(p())return le({from:s,to:a,seed:o});const c=M({timeZone:r,tzOffsetMinutes:l}),u=A({source:n});return m({baseUrl:e,accessToken:o,slug:g.usageModelBreakdown,params:{from:s,to:a,...u,...c}})}async function Le({baseUrl:e,accessToken:t,from:s,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:o}={}){const c=await d(t);if(p())return ce({from:s,to:a,seed:c});const u=M({timeZone:l,tzOffsetMinutes:o}),i=A({source:n,model:r});return m({baseUrl:e,accessToken:c,slug:g.usageDaily,params:{from:s,to:a,...i,...u}})}async function qe({baseUrl:e,accessToken:t,day:s,source:a,model:n,timeZone:r,tzOffsetMinutes:l}={}){const o=await d(t);if(p())return de({day:s,seed:o});const c=M({timeZone:r,tzOffsetMinutes:l}),u=A({source:a,model:n});return m({baseUrl:e,accessToken:o,slug:g.usageHourly,params:s?{day:s,...u,...c}:{...u,...c}})}async function Be({baseUrl:e,accessToken:t,months:s,to:a,source:n,model:r,timeZone:l,tzOffsetMinutes:o}={}){const c=await d(t);if(p())return ge({months:s,to:a,seed:c});const u=M({timeZone:l,tzOffsetMinutes:o}),i=A({source:n,model:r});return m({baseUrl:e,accessToken:c,slug:g.usageMonthly,params:{...s?{months:String(s)}:{},...a?{to:a}:{},...i,...u}})}async function Fe({baseUrl:e,accessToken:t,weeks:s,to:a,weekStartsOn:n,source:r,model:l,timeZone:o,tzOffsetMinutes:c}={}){const u=await d(t);if(p())return ke({weeks:s,to:a,weekStartsOn:n,seed:u});const i=M({timeZone:o,tzOffsetMinutes:c}),f=A({source:r,model:l});return m({baseUrl:e,accessToken:u,slug:g.usageHeatmap,params:{weeks:String(s),to:a,week_starts_on:n,...f,...i}})}async function Ne({baseUrl:e,accessToken:t}={}){const s=await d(t);return p()?{link_code:"mock_link_code",expires_at:new Date(Date.now()+10*6e4).toISOString()}:X({baseUrl:e,accessToken:s,slug:g.linkCodeInit,body:{}})}async function Ve({baseUrl:e,accessToken:t}={}){const s=await d(t);return m({baseUrl:e,accessToken:s,slug:g.publicViewProfile})}function M({timeZone:e,tzOffsetMinutes:t}={}){const s={},a=typeof e=="string"?e.trim():"";return a&&(s.tz=a),Number.isFinite(t)&&(s.tz_offset_minutes=String(Math.trunc(t))),s}function A({source:e,model:t}={}){const s={},a=typeof e=="string"?e.trim().toLowerCase():"";a&&(s.source=a);const n=typeof t=="string"?t.trim():"";return n&&(s.model=n),s}async function m({baseUrl:e,accessToken:t,slug:s,params:a,fetchOptions:n,errorPrefix:r,retry:l,requestKind:o=y.business,skipSessionExpiry:c=!1,allowRefresh:u=!0}={}){let i=await d(t),f=x(i),_=j({baseUrl:e,accessToken:i??void 0}).getHttpClient();const E=te(l,"GET"),T=c?y.probe:o;let v=0;const{primaryPath:R,fallbackPath:C}=O(s);for(;;)try{const S=await $({http:_,primaryPath:R,fallbackPath:C,params:a,fetchOptions:n});return H({hadAccessToken:f,accessToken:i}),S}catch(S){const h=S;if(h?.name==="AbortError")throw S;let b=null;const L=h?.statusCode??h?.status;if(u&&Z({status:L,requestKind:T,hadAccessToken:f,accessToken:i})){const k=(await ee())?.accessToken??null;if(x(k)){const D=j({baseUrl:e,accessToken:k}).getHttpClient();i=k,f=!0,_=D;try{const w=await $({http:D,primaryPath:R,fallbackPath:C,params:a,fetchOptions:n});return H({hadAccessToken:!0,accessToken:k}),w}catch(w){const B=w?.statusCode??w?.status;V({status:B,hadAccessToken:!0,accessToken:k,skipSessionExpiry:T===y.probe})&&U(),b=P(w,{errorPrefix:r,hadAccessToken:!0,accessToken:k,skipSessionExpiry:!0})}}else I({hadAccessToken:f,accessToken:i,skipSessionExpiry:T===y.probe})&&U();b??=P(h,{errorPrefix:r,hadAccessToken:f,accessToken:i,skipSessionExpiry:!0})}if(b??=P(h,{errorPrefix:r,hadAccessToken:f,accessToken:i,skipSessionExpiry:T===y.probe}),!se({err:b,attempt:v,retryOptions:E}))throw b;const q=ae({retryOptions:E,attempt:v});await ne(q),v+=1}}async function X({baseUrl:e,accessToken:t,slug:s,body:a,fetchOptions:n,errorPrefix:r,retry:l,requestKind:o=y.business,skipSessionExpiry:c=!1,allowRefresh:u=!0}={}){let i=await d(t),f=x(i),_=j({baseUrl:e,accessToken:i??void 0}).getHttpClient();const E=te(l,"POST"),T=c?y.probe:o;let v=0;const{primaryPath:R,fallbackPath:C}=O(s);for(;;)try{const S=await K({http:_,primaryPath:R,fallbackPath:C,body:a,fetchOptions:n});return H({hadAccessToken:f,accessToken:i}),S}catch(S){const h=S;if(h?.name==="AbortError")throw S;let b=null;const L=h?.statusCode??h?.status;if(u&&Z({status:L,requestKind:T,hadAccessToken:f,accessToken:i})){const k=(await ee())?.accessToken??null;if(x(k)){const D=j({baseUrl:e,accessToken:k}).getHttpClient();i=k,f=!0,_=D;try{const w=await K({http:D,primaryPath:R,fallbackPath:C,body:a,fetchOptions:n});return H({hadAccessToken:!0,accessToken:k}),w}catch(w){const B=w?.statusCode??w?.status;V({status:B,hadAccessToken:!0,accessToken:k,skipSessionExpiry:T===y.probe})&&U(),b=P(w,{errorPrefix:r,hadAccessToken:!0,accessToken:k,skipSessionExpiry:!0})}}else I({hadAccessToken:f,accessToken:i,skipSessionExpiry:T===y.probe})&&U();b??=P(h,{errorPrefix:r,hadAccessToken:f,accessToken:i,skipSessionExpiry:!0})}if(b??=P(h,{errorPrefix:r,hadAccessToken:f,accessToken:i,skipSessionExpiry:T===y.probe}),!se({err:b,attempt:v,retryOptions:E}))throw b;const q=ae({retryOptions:E,attempt:v});await ne(q),v+=1}}function O(e){const t=Te(e),s=`${N(J)}/${t}`,a=`${N(we)}/${t}`;return{primaryPath:s,fallbackPath:a}}function Te(e){return(typeof e=="string"?e.trim():"").replace(/^\/+/,"")}function N(e){const t=typeof e=="string"?e.trim():"";return t.endsWith("/")?t.slice(0,-1):t}async function $({http:e,primaryPath:t,fallbackPath:s,params:a,fetchOptions:n}={}){try{return await e.get(t,{params:a,...n||{}})}catch(r){if(!Q(r,t))throw r;return await e.get(s,{params:a,...n||{}})}}async function K({http:e,primaryPath:t,fallbackPath:s,body:a,fetchOptions:n}={}){try{return await W({http:e,path:t,body:a,fetchOptions:n})}catch(r){if(!Q(r,t))throw r;return await W({http:e,path:s,body:a,fetchOptions:n})}}async function W({http:e,path:t,body:s,fetchOptions:a}={}){return await e.post(t,s,{...a||{}})}function Q(e,t){return!t||!t.startsWith(`${N(J)}/`)?!1:(e?.statusCode??e?.status)===404}function P(e,{errorPrefix:t,hadAccessToken:s,accessToken:a,skipSessionExpiry:n}={}){const r=typeof e?.message=="string"?e.message.trim():"",l=typeof e?.error=="string"?e.error.trim():"",o=r||l||String(e||"Unknown error"),c=_e(o),u=new Error(t?`${t}: ${c}`:c);u.cause=e;const i=e?.statusCode??e?.status;return V({status:i,hadAccessToken:s,accessToken:a,skipSessionExpiry:n})&&U(),typeof i=="number"&&(u.status=i,u.statusCode=i),u.retryable=ve(i)||Me(o),c!==o&&(u.originalMessage=o),e?.nextActions&&(u.nextActions=e.nextActions),e?.error&&(u.error=e.error),u}function I({hadAccessToken:e,accessToken:t,skipSessionExpiry:s}={}){return s||!e||!x(t)?!1:Ae(t)}function V({status:e,hadAccessToken:t,accessToken:s,skipSessionExpiry:a}={}){return e!==401?!1:I({hadAccessToken:t,accessToken:s,skipSessionExpiry:a})}function Se({hadAccessToken:e,accessToken:t}={}){return I({hadAccessToken:e,accessToken:t})}function H({hadAccessToken:e,accessToken:t}={}){Se({hadAccessToken:e,accessToken:t})&&me()}function _e(e){return Y(e)?be:String(e||"Unknown error")}function Y(e){const t=String(e||"").toLowerCase();return t?!!(t.includes("deno:")||t.includes("deno")||t.includes("econnreset")||t.includes("econnrefused")||t.includes("etimedout")||t.includes("timeout")&&t.includes("request")||t.includes("upstream")&&(t.includes("deno")||t.includes("connect"))):!1}function Z({status:e,requestKind:t,hadAccessToken:s,accessToken:a}={}){return e!==401||t!==y.business?!1:I({hadAccessToken:s,accessToken:a})}async function ee(){return z||(z=ye.auth.getCurrentSession().then(({data:e})=>e?.session??null).catch(()=>null).finally(()=>{z=null}),z)}function ve(e){return e===502||e===503||e===504}function Me(e){const t=String(e||"").toLowerCase();return t?!!(Y(t)||t.includes("econnreset")||t.includes("econnrefused")||t.includes("etimedout")||t.includes("timeout")||t.includes("networkerror")||t.includes("failed to fetch")||t.includes("socket hang up")||t.includes("connection reset")):!1}function te(e,t){const a=(t||"GET").toUpperCase()==="GET"?{maxRetries:2,baseDelayMs:300,maxDelayMs:1500,jitterRatio:.2}:{maxRetries:0,baseDelayMs:0,maxDelayMs:0,jitterRatio:0};if(e==null)return a;if(e===!1)return{maxRetries:0,baseDelayMs:0,maxDelayMs:0,jitterRatio:0};const n=F(e.maxRetries??a.maxRetries,0,10),r=F(e.baseDelayMs??a.baseDelayMs,50,6e4),l=F(e.maxDelayMs??a.maxDelayMs,r,12e4),o=typeof e.jitterRatio=="number"?Math.max(0,Math.min(.5,e.jitterRatio)):a.jitterRatio;return{maxRetries:n,baseDelayMs:r,maxDelayMs:l,jitterRatio:o}}function x(e){return!!pe(e)}function Ae(e){if(!x(e))return!1;const t=e.split(".");return t.length!==3?!1:t.every(s=>/^[A-Za-z0-9_-]+$/.test(s))}function se({err:e,attempt:t,retryOptions:s}={}){return!s||s.maxRetries<=0||t>=s.maxRetries?!1:!!(e&&e.retryable)}function ae({retryOptions:e,attempt:t}={}){if(!e||e.maxRetries<=0)return 0;const s=e.baseDelayMs*Math.pow(2,t),a=Math.min(e.maxDelayMs,s),n=a*e.jitterRatio*Math.random();return Math.round(a+n)}function F(e,t,s){const a=Number(e);return Number.isFinite(a)?Math.min(s,Math.max(t,Math.floor(a))):t}function ne(e){return!e||e<=0?Promise.resolve():new Promise(t=>setTimeout(t,e))}export{Le as a,Re as b,qe as c,Be as d,Ee as e,He as f,Fe as g,De as h,Ve as i,Ie as j,Ce as k,Ue as l,xe as p,Ne as r,ze as s,je as t};
@@ -107,8 +107,8 @@
107
107
  ]
108
108
  }
109
109
  </script>
110
- <script type="module" crossorigin src="/assets/main-CvjKTqMk.js"></script>
111
- <link rel="stylesheet" crossorigin href="/assets/main-DJvWJCv5.css">
110
+ <script type="module" crossorigin src="/assets/main-DSjEYl46.js"></script>
111
+ <link rel="stylesheet" crossorigin href="/assets/main-ByQlanyj.css">
112
112
  </head>
113
113
  <body>
114
114
  <main class="aeo-seed-content" aria-label="Token Tracker AI-readable summary">
@@ -51,8 +51,8 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-CvjKTqMk.js"></script>
55
- <link rel="stylesheet" crossorigin href="/assets/main-DJvWJCv5.css">
54
+ <script type="module" crossorigin src="/assets/main-DSjEYl46.js"></script>
55
+ <link rel="stylesheet" crossorigin href="/assets/main-ByQlanyj.css">
56
56
  </head>
57
57
  <body>
58
58
  <main class="aeo-seed-content" aria-label="Token Tracker share page summary">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.3.0",
4
- "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, OpenCode, OpenClaw)",
3
+ "version": "0.3.5",
4
+ "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tracker": "bin/tracker.js",
@@ -1,6 +1,7 @@
1
1
  const os = require("node:os");
2
2
  const path = require("node:path");
3
3
  const fs = require("node:fs/promises");
4
+ const fssync = require("node:fs");
4
5
  const cp = require("node:child_process");
5
6
 
6
7
  const { ensureDir, readJson, writeJson, openLock } = require("../lib/fs");
@@ -10,6 +11,7 @@ const {
10
11
  listGeminiSessionFiles,
11
12
  listOpencodeMessageFiles,
12
13
  readOpencodeDbMessages,
14
+ resolveKiroDbPath,
13
15
  parseRolloutIncremental,
14
16
  parseClaudeIncremental,
15
17
  parseGeminiIncremental,
@@ -17,6 +19,7 @@ const {
17
19
  parseOpencodeDbIncremental,
18
20
  parseOpenclawIncremental,
19
21
  parseCursorApiIncremental,
22
+ parseKiroIncremental,
20
23
  } = require("../lib/rollout");
21
24
  const { drainQueueToCloud } = require("../lib/uploader");
22
25
  const { collectLocalSubscriptions } = require("../lib/subscriptions");
@@ -299,6 +302,27 @@ async function cmdSync(argv) {
299
302
  }
300
303
  }
301
304
 
305
+ // ── Kiro (SQLite-based) ──
306
+ let kiroResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
307
+ const kiroDbPath = resolveKiroDbPath();
308
+ if (fssync.existsSync(kiroDbPath)) {
309
+ if (progress?.enabled) {
310
+ progress.start(`Parsing Kiro ${renderBar(0)} | buckets 0`);
311
+ }
312
+ kiroResult = await parseKiroIncremental({
313
+ dbPath: kiroDbPath,
314
+ cursors,
315
+ queuePath,
316
+ onProgress: (p) => {
317
+ if (!progress?.enabled) return;
318
+ const pct = p.total > 0 ? p.index / p.total : 1;
319
+ progress.update(
320
+ `Parsing Kiro ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} records | buckets ${formatNumber(p.bucketsQueued)}`,
321
+ );
322
+ },
323
+ });
324
+ }
325
+
302
326
  if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
303
327
  for (const [projectKey, meta] of Object.entries(cursors.projectHourly.projects)) {
304
328
  if (!meta || typeof meta !== "object") continue;
@@ -473,14 +497,16 @@ async function cmdSync(argv) {
473
497
  claudeResult.filesProcessed +
474
498
  geminiResult.filesProcessed +
475
499
  opencodeResult.filesProcessed +
476
- cursorResult.recordsProcessed;
500
+ cursorResult.recordsProcessed +
501
+ kiroResult.recordsProcessed;
477
502
  const totalBuckets =
478
503
  parseResult.bucketsQueued +
479
504
  openclawResult.bucketsQueued +
480
505
  claudeResult.bucketsQueued +
481
506
  geminiResult.bucketsQueued +
482
507
  opencodeResult.bucketsQueued +
483
- cursorResult.bucketsQueued;
508
+ cursorResult.bucketsQueued +
509
+ kiroResult.bucketsQueued;
484
510
  process.stdout.write(
485
511
  [
486
512
  "Sync finished:",
@@ -2596,12 +2596,211 @@ async function parseCursorApiIncremental({
2596
2596
  return { recordsProcessed: total, eventsAggregated, bucketsQueued };
2597
2597
  }
2598
2598
 
2599
+ // ---------------------------------------------------------------------------
2600
+ // Kiro token tracking (reads from devdata.sqlite)
2601
+ // ---------------------------------------------------------------------------
2602
+
2603
+ function resolveKiroBasePath() {
2604
+ const home = require("node:os").homedir();
2605
+ return path.join(
2606
+ home,
2607
+ "Library",
2608
+ "Application Support",
2609
+ "Kiro",
2610
+ "User",
2611
+ "globalStorage",
2612
+ "kiro.kiroagent",
2613
+ );
2614
+ }
2615
+
2616
+ function resolveKiroDbPath() {
2617
+ return path.join(resolveKiroBasePath(), "dev_data", "devdata.sqlite");
2618
+ }
2619
+
2620
+ function readKiroDbTokens(dbPath, sinceId) {
2621
+ if (!dbPath || !fssync.existsSync(dbPath)) return [];
2622
+ const minId = Number.isFinite(sinceId) && sinceId > 0 ? sinceId : 0;
2623
+ const sql = `SELECT id, model, provider, tokens_prompt, tokens_generated, timestamp FROM tokens_generated WHERE id > ${minId} ORDER BY id ASC`;
2624
+ let raw;
2625
+ try {
2626
+ raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
2627
+ encoding: "utf8",
2628
+ maxBuffer: 10 * 1024 * 1024,
2629
+ timeout: 15_000,
2630
+ });
2631
+ } catch (_e) {
2632
+ return [];
2633
+ }
2634
+ if (!raw || !raw.trim()) return [];
2635
+ let rows;
2636
+ try {
2637
+ rows = JSON.parse(raw);
2638
+ } catch (_e) {
2639
+ return [];
2640
+ }
2641
+ return Array.isArray(rows) ? rows : [];
2642
+ }
2643
+
2644
+ // Build a sorted timeline of model usage from Kiro .chat metadata files
2645
+ function buildKiroModelTimeline(basePath) {
2646
+ const timeline = []; // [{ startMs, endMs, model }]
2647
+ if (!basePath || !fssync.existsSync(basePath)) return timeline;
2648
+ let dirs;
2649
+ try {
2650
+ dirs = fssync.readdirSync(basePath, { withFileTypes: true });
2651
+ } catch (_e) {
2652
+ return timeline;
2653
+ }
2654
+ for (const entry of dirs) {
2655
+ if (!entry.isDirectory()) continue;
2656
+ const dirPath = path.join(basePath, entry.name);
2657
+ let files;
2658
+ try {
2659
+ files = fssync.readdirSync(dirPath).filter((f) => f.endsWith(".chat"));
2660
+ } catch (_e) {
2661
+ continue;
2662
+ }
2663
+ for (const file of files) {
2664
+ try {
2665
+ const raw = fssync.readFileSync(path.join(dirPath, file), "utf8");
2666
+ const data = JSON.parse(raw);
2667
+ const meta = data?.metadata;
2668
+ if (!meta?.modelId || !meta?.startTime) continue;
2669
+ timeline.push({
2670
+ startMs: meta.startTime,
2671
+ endMs: meta.endTime || meta.startTime,
2672
+ model: String(meta.modelId),
2673
+ });
2674
+ } catch (_e) {}
2675
+ }
2676
+ }
2677
+ timeline.sort((a, b) => a.startMs - b.startMs);
2678
+ return timeline;
2679
+ }
2680
+
2681
+ // Find the model for a given UTC timestamp string using the .chat timeline
2682
+ function resolveKiroModel(timeline, utcTimestamp) {
2683
+ if (!timeline.length || !utcTimestamp) return null;
2684
+ const ts = new Date(utcTimestamp).getTime();
2685
+ if (!Number.isFinite(ts)) return null;
2686
+
2687
+ // Binary search for the closest .chat entry
2688
+ let lo = 0;
2689
+ let hi = timeline.length - 1;
2690
+ let best = null;
2691
+ let bestDist = Infinity;
2692
+ while (lo <= hi) {
2693
+ const mid = (lo + hi) >>> 1;
2694
+ const entry = timeline[mid];
2695
+ // Check if timestamp falls within the .chat execution window
2696
+ if (ts >= entry.startMs && ts <= entry.endMs) return entry.model;
2697
+ const dist = Math.min(Math.abs(ts - entry.startMs), Math.abs(ts - entry.endMs));
2698
+ if (dist < bestDist) {
2699
+ bestDist = dist;
2700
+ best = entry.model;
2701
+ }
2702
+ if (ts < entry.startMs) hi = mid - 1;
2703
+ else lo = mid + 1;
2704
+ }
2705
+ // Only match if within 10 minutes
2706
+ return bestDist < 10 * 60 * 1000 ? best : null;
2707
+ }
2708
+
2709
+ // Normalize Kiro internal model IDs to readable names
2710
+ // e.g. "CLAUDE_SONNET_4_20250514_V1_0" → "claude-sonnet-4"
2711
+ function normalizeKiroModelName(raw) {
2712
+ if (!raw || typeof raw !== "string") return null;
2713
+ let name = raw.trim();
2714
+ if (!name) return null;
2715
+ // Already lowercase with dashes (e.g. "claude-opus-4.5") → keep as-is
2716
+ if (name === name.toLowerCase() && name.includes("-")) return name;
2717
+ // UPPER_SNAKE_CASE internal names: strip date/version suffixes, convert to lowercase-dash
2718
+ name = name
2719
+ .replace(/_\d{8}_V\d+_\d+$/i, "") // remove _20250514_V1_0
2720
+ .replace(/_V\d+$/i, "") // remove _V1
2721
+ .toLowerCase()
2722
+ .replace(/_/g, "-");
2723
+ return name || null;
2724
+ }
2725
+
2726
+ async function parseKiroIncremental({ dbPath, cursors, queuePath, onProgress }) {
2727
+ await ensureDir(path.dirname(queuePath));
2728
+ const kiroState = cursors.kiro && typeof cursors.kiro === "object" ? cursors.kiro : {};
2729
+ const lastId = typeof kiroState.lastId === "number" ? kiroState.lastId : 0;
2730
+
2731
+ const resolvedDbPath = dbPath || resolveKiroDbPath();
2732
+ const rows = readKiroDbTokens(resolvedDbPath, lastId);
2733
+ if (rows.length === 0) {
2734
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2735
+ }
2736
+
2737
+ // Build model timeline from .chat files for model name resolution
2738
+ const basePath = resolveKiroBasePath();
2739
+ const modelTimeline = buildKiroModelTimeline(basePath);
2740
+
2741
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
2742
+ const touchedBuckets = new Set();
2743
+ const cb = typeof onProgress === "function" ? onProgress : null;
2744
+ let eventsAggregated = 0;
2745
+ let maxId = lastId;
2746
+
2747
+ for (let i = 0; i < rows.length; i++) {
2748
+ const row = rows[i];
2749
+ const inputTokens = toNonNegativeInt(row.tokens_prompt);
2750
+ const outputTokens = toNonNegativeInt(row.tokens_generated);
2751
+ if (inputTokens === 0 && outputTokens === 0) continue;
2752
+
2753
+ // timestamp format: "2026-01-09 15:25:30" (UTC from SQLite DEFAULT CURRENT_TIMESTAMP)
2754
+ const ts = row.timestamp ? row.timestamp.replace(" ", "T") + "Z" : null;
2755
+ const bucketStart = ts ? toUtcHalfHourStart(ts) : null;
2756
+ if (!bucketStart) continue;
2757
+
2758
+ // Resolve actual model from .chat timeline, fallback to "kiro-agent"
2759
+ const resolvedModel = resolveKiroModel(modelTimeline, ts);
2760
+ const model = normalizeKiroModelName(resolvedModel) || "kiro-agent";
2761
+
2762
+ const delta = {
2763
+ input_tokens: inputTokens,
2764
+ cached_input_tokens: 0,
2765
+ output_tokens: outputTokens,
2766
+ reasoning_output_tokens: 0,
2767
+ total_tokens: inputTokens + outputTokens,
2768
+ conversation_count: 1,
2769
+ };
2770
+
2771
+ const bucket = getHourlyBucket(hourlyState, "kiro", model, bucketStart);
2772
+ addTotals(bucket.totals, delta);
2773
+ touchedBuckets.add(bucketKey("kiro", model, bucketStart));
2774
+ eventsAggregated++;
2775
+
2776
+ if (row.id && row.id > maxId) maxId = row.id;
2777
+
2778
+ if (cb) {
2779
+ cb({
2780
+ index: i + 1,
2781
+ total: rows.length,
2782
+ recordsProcessed: i + 1,
2783
+ eventsAggregated,
2784
+ bucketsQueued: touchedBuckets.size,
2785
+ });
2786
+ }
2787
+ }
2788
+
2789
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
2790
+ hourlyState.updatedAt = new Date().toISOString();
2791
+ cursors.hourly = hourlyState;
2792
+ cursors.kiro = { lastId: maxId, updatedAt: new Date().toISOString() };
2793
+
2794
+ return { recordsProcessed: rows.length, eventsAggregated, bucketsQueued };
2795
+ }
2796
+
2599
2797
  module.exports = {
2600
2798
  listRolloutFiles,
2601
2799
  listClaudeProjectFiles,
2602
2800
  listGeminiSessionFiles,
2603
2801
  listOpencodeMessageFiles,
2604
2802
  readOpencodeDbMessages,
2803
+ resolveKiroDbPath,
2605
2804
  parseRolloutIncremental,
2606
2805
  parseClaudeIncremental,
2607
2806
  parseGeminiIncremental,
@@ -2609,4 +2808,5 @@ module.exports = {
2609
2808
  parseOpencodeDbIncremental,
2610
2809
  parseOpenclawIncremental,
2611
2810
  parseCursorApiIncremental,
2811
+ parseKiroIncremental,
2612
2812
  };