tokentracker-cli 0.22.3 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +4 -3
  3. package/dashboard/dist/assets/{Card-Ce7Bq6YO.js → Card-BgXt23Ak.js} +1 -1
  4. package/dashboard/dist/assets/{DashboardPage-K_pYvsl8.js → DashboardPage-NVAsY99I.js} +1 -1
  5. package/dashboard/dist/assets/{DevicePage-B0t441Pp.js → DevicePage-CWTFzGYj.js} +1 -1
  6. package/dashboard/dist/assets/{FadeIn-rL5OMeU2.js → FadeIn-r11-zwBU.js} +1 -1
  7. package/dashboard/dist/assets/{HeaderGithubStar-CGS4pFqm.js → HeaderGithubStar-ZDYMF__6.js} +1 -1
  8. package/dashboard/dist/assets/{IpCheckPage-Dg8ZSzBs.js → IpCheckPage-cCSBXnv2.js} +1 -1
  9. package/dashboard/dist/assets/{LandingPage-BRTdgQhg.js → LandingPage-DDUROVcN.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardPage-DMQ92NBo.js → LeaderboardPage-BaETRtBF.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardProfilePage-BYY0tPSp.js → LeaderboardProfilePage-BthYFghU.js} +1 -1
  12. package/dashboard/dist/assets/{LimitsPage-IguYTVRL.js → LimitsPage-BQpint0k.js} +1 -1
  13. package/dashboard/dist/assets/{LoginPage-Ct_y3yvb.js → LoginPage-EUjSrjOv.js} +1 -1
  14. package/dashboard/dist/assets/{PopoverPopup-BKxuCZoU.js → PopoverPopup-DyjmUoRY.js} +1 -1
  15. package/dashboard/dist/assets/{ProviderIcon-BW8DsxOn.js → ProviderIcon-D7GhGvvD.js} +1 -1
  16. package/dashboard/dist/assets/{SettingsPage-BxDpsq6h.js → SettingsPage-9lfQ1c28.js} +1 -1
  17. package/dashboard/dist/assets/{SkillsPage-DGrfA2Ts.js → SkillsPage-Bt7d1kd0.js} +1 -1
  18. package/dashboard/dist/assets/{WidgetsPage-DYLXbN7i.js → WidgetsPage-QfPgS0dO.js} +1 -1
  19. package/dashboard/dist/assets/{WrappedPage-C-NcEsy1.js → WrappedPage-0iiBrhBo.js} +1 -1
  20. package/dashboard/dist/assets/check-CtXuxG1H.js +1 -0
  21. package/dashboard/dist/assets/{chevron-down-sxrI4ACd.js → chevron-down-Br4ZSPLP.js} +1 -1
  22. package/dashboard/dist/assets/{download-tZXJTa2X.js → download-CnQNKpma.js} +1 -1
  23. package/dashboard/dist/assets/{leaderboard-columns-DjTC-eio.js → leaderboard-columns-Bdgb7vS7.js} +1 -1
  24. package/dashboard/dist/assets/main-0bSXJNLn.css +1 -0
  25. package/dashboard/dist/assets/{main-DsrMFwQm.js → main-D7f_DoMp.js} +2 -2
  26. package/dashboard/dist/assets/{use-limits-display-prefs-CzOLYgu5.js → use-limits-display-prefs-B5TQdyrf.js} +1 -1
  27. package/dashboard/dist/assets/{use-native-settings-D6YjGLZJ.js → use-native-settings-CSxKG_Na.js} +1 -1
  28. package/dashboard/dist/assets/{use-reduced-motion-0uADS5dP.js → use-reduced-motion-C9rLBEnX.js} +1 -1
  29. package/dashboard/dist/assets/{use-usage-limits-xh-A-nDC.js → use-usage-limits-Bmok4oKO.js} +1 -1
  30. package/dashboard/dist/index.html +2 -2
  31. package/dashboard/dist/share.html +2 -2
  32. package/package.json +1 -1
  33. package/src/lib/antigravity-paths.js +48 -0
  34. package/src/lib/cursor-config.js +20 -47
  35. package/src/lib/pricing/seed-snapshot.json +1 -1
  36. package/src/lib/rollout.js +81 -157
  37. package/src/lib/skills-manager.js +76 -48
  38. package/src/lib/sqlite-reader.js +136 -0
  39. package/dashboard/dist/assets/check-fi-rlObU.js +0 -1
  40. package/dashboard/dist/assets/main-D55hG1yw.css +0 -1
@@ -2,10 +2,10 @@ const fs = require("node:fs/promises");
2
2
  const fssync = require("node:fs");
3
3
  const path = require("node:path");
4
4
  const readline = require("node:readline");
5
- const cp = require("node:child_process");
6
5
 
7
6
  const crypto = require("node:crypto");
8
7
  const { ensureDir } = require("./fs");
8
+ const { readSqliteJsonRows } = require("./sqlite-reader");
9
9
 
10
10
  const DEFAULT_SOURCE = "codex";
11
11
  const DEFAULT_MODEL = "unknown";
@@ -2381,27 +2381,15 @@ async function walkOpencodeMessages(dir, out) {
2381
2381
  // OpenCode SQLite DB reader (v1.2+ stores messages in opencode.db)
2382
2382
  // ---------------------------------------------------------------------------
2383
2383
 
2384
- function readOpencodeDbMessages(dbPath) {
2384
+ function readOpencodeDbMessages(dbPath, sqliteOptions = {}) {
2385
2385
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
2386
2386
  const sql = `SELECT id, session_id, time_updated, data FROM message WHERE json_extract(data, '$.role') = 'assistant' ORDER BY time_created ASC`;
2387
- let raw;
2388
- try {
2389
- raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
2390
- encoding: "utf8",
2391
- maxBuffer: 50 * 1024 * 1024,
2392
- timeout: 30_000,
2393
- });
2394
- } catch (_e) {
2395
- return [];
2396
- }
2397
- if (!raw || !raw.trim()) return [];
2398
- let rows;
2399
- try {
2400
- rows = JSON.parse(raw);
2401
- } catch (_e) {
2402
- return [];
2403
- }
2404
- if (!Array.isArray(rows)) return [];
2387
+ const rows = readSqliteJsonRows(dbPath, sql, {
2388
+ label: "OpenCode",
2389
+ maxBuffer: 50 * 1024 * 1024,
2390
+ timeout: 30_000,
2391
+ ...sqliteOptions,
2392
+ });
2405
2393
  const out = [];
2406
2394
  for (const row of rows) {
2407
2395
  if (!row || typeof row.data !== "string") continue;
@@ -2717,28 +2705,16 @@ function resolveKiroJsonlPath() {
2717
2705
  return path.join(resolveKiroBasePath(), "dev_data", "tokens_generated.jsonl");
2718
2706
  }
2719
2707
 
2720
- function readKiroDbTokens(dbPath, sinceId) {
2708
+ function readKiroDbTokens(dbPath, sinceId, sqliteOptions = {}) {
2721
2709
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
2722
2710
  const minId = Number.isFinite(sinceId) && sinceId > 0 ? sinceId : 0;
2723
2711
  const sql = `SELECT id, model, provider, tokens_prompt, tokens_generated, timestamp FROM tokens_generated WHERE id > ${minId} ORDER BY id ASC`;
2724
- let raw;
2725
- try {
2726
- raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
2727
- encoding: "utf8",
2728
- maxBuffer: 10 * 1024 * 1024,
2729
- timeout: 15_000,
2730
- });
2731
- } catch (_e) {
2732
- return [];
2733
- }
2734
- if (!raw || !raw.trim()) return [];
2735
- let rows;
2736
- try {
2737
- rows = JSON.parse(raw);
2738
- } catch (_e) {
2739
- return [];
2740
- }
2741
- return Array.isArray(rows) ? rows : [];
2712
+ return readSqliteJsonRows(dbPath, sql, {
2713
+ label: "Kiro",
2714
+ maxBuffer: 10 * 1024 * 1024,
2715
+ timeout: 15_000,
2716
+ ...sqliteOptions,
2717
+ });
2742
2718
  }
2743
2719
 
2744
2720
  // Read Kiro token data from JSONL fallback (tokens_generated.jsonl).
@@ -2880,7 +2856,7 @@ function normalizeKiroModelName(raw) {
2880
2856
  return name || null;
2881
2857
  }
2882
2858
 
2883
- async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onProgress }) {
2859
+ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
2884
2860
  await ensureDir(path.dirname(queuePath));
2885
2861
  const kiroState = cursors.kiro && typeof cursors.kiro === "object" ? cursors.kiro : {};
2886
2862
  const lastDbId = typeof kiroState.lastDbId === "number"
@@ -2898,7 +2874,7 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
2898
2874
  let nextJsonlLine = lastJsonlLine;
2899
2875
  let usingDb = false;
2900
2876
  if (fssync.existsSync(resolvedDbPath)) {
2901
- rows = readKiroDbTokens(resolvedDbPath, lastDbId);
2877
+ rows = readKiroDbTokens(resolvedDbPath, lastDbId, sqliteOptions);
2902
2878
  usingDb = true;
2903
2879
  // DB and JSONL are siblings for the same usage events. If the DB ever
2904
2880
  // disappears (corrupted / wiped) and we fall back to JSONL in a later
@@ -3088,7 +3064,7 @@ function snapshotSqliteDb(dbPath) {
3088
3064
  };
3089
3065
  }
3090
3066
 
3091
- function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = []) {
3067
+ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [], sqliteOptions = {}) {
3092
3068
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
3093
3069
  const since = Number.isFinite(lastCompletedEpoch) && lastCompletedEpoch > 0 ? lastCompletedEpoch : 0;
3094
3070
  const forceIds = Array.isArray(unfinishedSessionIds)
@@ -3116,24 +3092,12 @@ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = [
3116
3092
  }
3117
3093
 
3118
3094
  try {
3119
- let raw;
3120
- try {
3121
- raw = cp.execFileSync("sqlite3", ["-json", effectiveDbPath, sql], {
3122
- encoding: "utf8",
3123
- maxBuffer: 10 * 1024 * 1024,
3124
- timeout: 15_000,
3125
- });
3126
- } catch (_e) {
3127
- return [];
3128
- }
3129
- if (!raw || !raw.trim()) return [];
3130
- let rows;
3131
- try {
3132
- rows = JSON.parse(raw);
3133
- } catch (_e) {
3134
- return [];
3135
- }
3136
- return Array.isArray(rows) ? rows : [];
3095
+ return readSqliteJsonRows(effectiveDbPath, sql, {
3096
+ label: "Hermes",
3097
+ maxBuffer: 10 * 1024 * 1024,
3098
+ timeout: 15_000,
3099
+ ...sqliteOptions,
3100
+ });
3137
3101
  } finally {
3138
3102
  if (snapshot) snapshot.cleanup();
3139
3103
  }
@@ -3147,7 +3111,7 @@ function hasLegacyHermesDefaultState(hermesState) {
3147
3111
  );
3148
3112
  }
3149
3113
 
3150
- async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath, onProgress }) {
3114
+ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath, onProgress, sqliteOptions } = {}) {
3151
3115
  await ensureDir(path.dirname(queuePath));
3152
3116
  const hermesState = cursors.hermes && typeof cursors.hermes === "object" ? cursors.hermes : {};
3153
3117
 
@@ -3168,7 +3132,12 @@ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath,
3168
3132
  const trackedUnfinishedSessionIds = Array.isArray(dbState.unfinishedSessionIds)
3169
3133
  ? dbState.unfinishedSessionIds
3170
3134
  : [];
3171
- const rows = readHermesSessions(dbPath, dbState.lastCompletedStartedAt, trackedUnfinishedSessionIds);
3135
+ const rows = readHermesSessions(
3136
+ dbPath,
3137
+ dbState.lastCompletedStartedAt,
3138
+ trackedUnfinishedSessionIds,
3139
+ sqliteOptions,
3140
+ );
3172
3141
  recordsProcessed += rows.length;
3173
3142
  if (rows.length === 0) {
3174
3143
  dbState.updatedAt = updatedAt;
@@ -3650,44 +3619,22 @@ function canonicalizeKiroCliModelId(raw) {
3650
3619
  // `conversation_id` and the inner JSON `continuation_id` are different
3651
3620
  // UUIDs on observed data; covering both means retraction fires whichever
3652
3621
  // side matches the live session's `session_id`.
3653
- function readKiroCliRequests(dbPath, env = process.env) {
3622
+ function readKiroCliRequests(dbPath, env = process.env, sqliteOptions = {}) {
3654
3623
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
3655
- let raw;
3656
- try {
3657
- raw = cp.execFileSync(
3658
- "sqlite3",
3659
- [
3660
- "-json",
3661
- dbPath,
3662
- "SELECT conversation_id, " +
3663
- "json_extract(value, '$.model_info.model_id') AS session_model_id, " +
3664
- "json_extract(value, '$.user_turn_metadata.continuation_id') AS continuation_id, " +
3665
- "json_extract(value, '$.user_turn_metadata.requests') AS requests_json " +
3666
- "FROM conversations_v2 " +
3667
- "WHERE json_extract(value, '$.user_turn_metadata.requests') IS NOT NULL",
3668
- ],
3669
- { encoding: "utf8", maxBuffer: 128 * 1024 * 1024, timeout: 120_000 },
3670
- );
3671
- } catch (err) {
3672
- // TASK-012 / D-8: debug-gated stderr log so a missing sqlite3 binary
3673
- // is distinguishable from an empty DB. Silent by default. env is
3674
- // threaded so tests can toggle debug hermetically.
3675
- const dbg = String((env && env.TOKENTRACKER_DEBUG) || "").toLowerCase();
3676
- if (dbg === "1" || dbg === "true") {
3677
- process.stderr.write(
3678
- `[kiro-cli] sqlite3 read failed: ${err?.message || err}\n`,
3679
- );
3680
- }
3681
- return [];
3682
- }
3683
- if (!raw || !raw.trim()) return [];
3684
- let rows;
3685
- try {
3686
- rows = JSON.parse(raw);
3687
- } catch {
3688
- return [];
3689
- }
3690
- if (!Array.isArray(rows)) return [];
3624
+ const sql =
3625
+ "SELECT conversation_id, " +
3626
+ "json_extract(value, '$.model_info.model_id') AS session_model_id, " +
3627
+ "json_extract(value, '$.user_turn_metadata.continuation_id') AS continuation_id, " +
3628
+ "json_extract(value, '$.user_turn_metadata.requests') AS requests_json " +
3629
+ "FROM conversations_v2 " +
3630
+ "WHERE json_extract(value, '$.user_turn_metadata.requests') IS NOT NULL";
3631
+ const rows = readSqliteJsonRows(dbPath, sql, {
3632
+ label: "Kiro CLI",
3633
+ env,
3634
+ maxBuffer: 128 * 1024 * 1024,
3635
+ timeout: 120_000,
3636
+ ...sqliteOptions,
3637
+ });
3691
3638
  const flat = [];
3692
3639
  for (const row of rows) {
3693
3640
  let requests;
@@ -3715,7 +3662,7 @@ function readKiroCliRequests(dbPath, env = process.env) {
3715
3662
  return flat;
3716
3663
  }
3717
3664
 
3718
- async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onProgress, env } = {}) {
3665
+ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onProgress, env, sqliteOptions } = {}) {
3719
3666
  await ensureDir(path.dirname(queuePath));
3720
3667
  const kiroCliState =
3721
3668
  cursors.kiroCli && typeof cursors.kiroCli === "object" ? cursors.kiroCli : {};
@@ -3750,7 +3697,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
3750
3697
  // SQLite conversation_id OR continuation_id to subtract the orphan
3751
3698
  // session-file cursor entry before the new SQLite row is processed.
3752
3699
  const flatDb = fssync.existsSync(dbPath)
3753
- ? readKiroCliRequests(dbPath, resolvedEnv)
3700
+ ? readKiroCliRequests(dbPath, resolvedEnv, sqliteOptions)
3754
3701
  : [];
3755
3702
  const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
3756
3703
  let flatSessions = [];
@@ -5203,16 +5150,16 @@ function extractZedTotals(thread) {
5203
5150
  // several `threads` schemas; older versions may omit created_at /
5204
5151
  // folder_paths. We dynamically detect via PRAGMA so the query never fails on
5205
5152
  // a missing column.
5206
- function buildZedThreadsQuery(dbPath, cursorUpdatedAt) {
5207
- const pragma = cp.execFileSync("sqlite3", [dbPath, "PRAGMA table_info(threads)"], {
5208
- encoding: "utf8",
5153
+ function buildZedThreadsQuery(dbPath, cursorUpdatedAt, sqliteOptions = {}) {
5154
+ const pragmaRows = readSqliteJsonRows(dbPath, "PRAGMA table_info(threads)", {
5155
+ label: "Zed",
5209
5156
  maxBuffer: 4 * 1024 * 1024,
5210
5157
  timeout: 10_000,
5158
+ ...sqliteOptions,
5211
5159
  });
5212
5160
  const columns = new Set(
5213
- pragma
5214
- .split("\n")
5215
- .map((line) => line.split("|")[1])
5161
+ pragmaRows
5162
+ .map((row) => row?.name)
5216
5163
  .filter(Boolean),
5217
5164
  );
5218
5165
  const optional = (col) => (columns.has(col) ? col : `NULL AS ${col}`);
@@ -5228,27 +5175,14 @@ function buildZedThreadsQuery(dbPath, cursorUpdatedAt) {
5228
5175
  return `SELECT id, updated_at, ${optional("created_at")}, data_type, hex(data) AS data_hex FROM threads${where}`;
5229
5176
  }
5230
5177
 
5231
- function readZedThreadRowsFromSqlite(dbPath, cursorUpdatedAt) {
5232
- const query = buildZedThreadsQuery(dbPath, cursorUpdatedAt);
5233
- let raw;
5234
- try {
5235
- raw = cp.execFileSync("sqlite3", ["-json", dbPath, query], {
5236
- encoding: "utf8",
5237
- maxBuffer: 256 * 1024 * 1024,
5238
- timeout: 60_000,
5239
- });
5240
- } catch (_e) {
5241
- return [];
5242
- }
5243
- if (!raw || !raw.trim()) return [];
5244
- let rows;
5245
- try {
5246
- rows = JSON.parse(raw);
5247
- } catch (_e) {
5248
- return [];
5249
- }
5250
- if (!Array.isArray(rows)) return [];
5251
- return rows;
5178
+ function readZedThreadRowsFromSqlite(dbPath, cursorUpdatedAt, sqliteOptions = {}) {
5179
+ const query = buildZedThreadsQuery(dbPath, cursorUpdatedAt, sqliteOptions);
5180
+ return readSqliteJsonRows(dbPath, query, {
5181
+ label: "Zed",
5182
+ maxBuffer: 256 * 1024 * 1024,
5183
+ timeout: 60_000,
5184
+ ...sqliteOptions,
5185
+ });
5252
5186
  }
5253
5187
 
5254
5188
  async function parseZedIncremental({
@@ -5257,6 +5191,7 @@ async function parseZedIncremental({
5257
5191
  queuePath,
5258
5192
  onProgress,
5259
5193
  env,
5194
+ sqliteOptions,
5260
5195
  } = {}) {
5261
5196
  await ensureDir(path.dirname(queuePath));
5262
5197
  const resolvedDb = dbPath || resolveZedDbPath(env || process.env);
@@ -5294,7 +5229,7 @@ async function parseZedIncremental({
5294
5229
  const snap = snapshotSqliteDb(resolvedDb);
5295
5230
  let rows = [];
5296
5231
  try {
5297
- rows = readZedThreadRowsFromSqlite(snap.path, cursorUpdatedAt);
5232
+ rows = readZedThreadRowsFromSqlite(snap.path, cursorUpdatedAt, sqliteOptions);
5298
5233
  } finally {
5299
5234
  snap.cleanup();
5300
5235
  }
@@ -5528,21 +5463,18 @@ function parseGooseCreatedAt(s) {
5528
5463
  return null;
5529
5464
  }
5530
5465
 
5531
- function readGooseSessionsFromSqlite(dbPath) {
5466
+ function readGooseSessionsFromSqlite(dbPath, sqliteOptions = {}) {
5532
5467
  // Probe columns: the `accumulated_*` fields were added in a later Goose
5533
5468
  // version; we keep the query forgiving so older installs still work.
5534
- let pragma;
5535
- try {
5536
- pragma = cp.execFileSync("sqlite3", [dbPath, "PRAGMA table_info(sessions)"], {
5537
- encoding: "utf8",
5538
- maxBuffer: 4 * 1024 * 1024,
5539
- timeout: 10_000,
5540
- });
5541
- } catch (_e) { return []; }
5469
+ const pragmaRows = readSqliteJsonRows(dbPath, "PRAGMA table_info(sessions)", {
5470
+ label: "Goose",
5471
+ maxBuffer: 4 * 1024 * 1024,
5472
+ timeout: 10_000,
5473
+ ...sqliteOptions,
5474
+ });
5542
5475
  const columns = new Set(
5543
- pragma
5544
- .split("\n")
5545
- .map((line) => line.split("|")[1])
5476
+ pragmaRows
5477
+ .map((row) => row?.name)
5546
5478
  .filter(Boolean),
5547
5479
  );
5548
5480
  const optional = (col) => (columns.has(col) ? col : `NULL AS ${col}`);
@@ -5562,21 +5494,12 @@ function readGooseSessionsFromSqlite(dbPath) {
5562
5494
  WHERE model_config_json IS NOT NULL
5563
5495
  AND TRIM(model_config_json) != ''
5564
5496
  `.trim();
5565
- let raw;
5566
- try {
5567
- raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
5568
- encoding: "utf8",
5569
- maxBuffer: 64 * 1024 * 1024,
5570
- timeout: 60_000,
5571
- });
5572
- } catch (_e) { return []; }
5573
- if (!raw || !raw.trim()) return [];
5574
- try {
5575
- const rows = JSON.parse(raw);
5576
- return Array.isArray(rows) ? rows : [];
5577
- } catch (_e) {
5578
- return [];
5579
- }
5497
+ return readSqliteJsonRows(dbPath, sql, {
5498
+ label: "Goose",
5499
+ maxBuffer: 64 * 1024 * 1024,
5500
+ timeout: 60_000,
5501
+ ...sqliteOptions,
5502
+ });
5580
5503
  }
5581
5504
 
5582
5505
  async function parseGooseIncremental({
@@ -5585,6 +5508,7 @@ async function parseGooseIncremental({
5585
5508
  queuePath,
5586
5509
  onProgress,
5587
5510
  env,
5511
+ sqliteOptions,
5588
5512
  } = {}) {
5589
5513
  await ensureDir(path.dirname(queuePath));
5590
5514
  const resolvedDb = dbPath || resolveGooseDbPath(env || process.env);
@@ -5618,7 +5542,7 @@ async function parseGooseIncremental({
5618
5542
  const snap = snapshotSqliteDb(resolvedDb);
5619
5543
  let rows = [];
5620
5544
  try {
5621
- rows = readGooseSessionsFromSqlite(snap.path);
5545
+ rows = readGooseSessionsFromSqlite(snap.path, sqliteOptions);
5622
5546
  } finally {
5623
5547
  snap.cleanup();
5624
5548
  }
@@ -2,6 +2,7 @@ const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
4
  const { resolveGrokHome } = require("./grok-hook");
5
+ const { resolveAntigravitySkillDirs } = require("./antigravity-paths");
5
6
 
6
7
  const DEFAULT_REPOS = [
7
8
  { owner: "anthropics", name: "skills", branch: "main", enabled: true },
@@ -14,12 +15,28 @@ const TARGETS = {
14
15
  claude: { id: "claude", label: "Claude", dir: () => path.join(os.homedir(), ".claude", "skills") },
15
16
  codex: { id: "codex", label: "Codex", dir: () => path.join(os.homedir(), ".codex", "skills") },
16
17
  grok: { id: "grok", label: "Grok", dir: () => path.join(resolveGrokHome(process.env), "skills") },
18
+ antigravity: { id: "antigravity", label: "Antigravity", dirs: () => resolveAntigravitySkillDirs(process.env) },
17
19
  gemini: { id: "gemini", label: "Gemini", dir: () => path.join(os.homedir(), ".gemini", "skills") },
18
20
  opencode: { id: "opencode", label: "OpenCode", dir: () => path.join(os.homedir(), ".config", "opencode", "skills") },
19
21
  hermes: { id: "hermes", label: "Hermes", dir: () => path.join(os.homedir(), ".hermes", "skills") },
20
22
  agents: { id: "agents", label: "Agents", visible: false, dir: () => path.join(os.homedir(), ".agents", "skills") },
21
23
  };
22
24
 
25
+ // Dual contract: a target exposes either dir() → string (single path) or
26
+ // dirs() → string[] (parallel-write to multiple paths, e.g. Antigravity which
27
+ // has separate user-skills dirs for the main app and the IDE). Consumers must
28
+ // route through these helpers so the single-path targets stay zero-overhead.
29
+ function targetDirs(target) {
30
+ if (typeof target.dirs === "function") return target.dirs();
31
+ return [target.dir()];
32
+ }
33
+
34
+ // Used for surfacing one path in UI/local-api responses. For multi-dir targets
35
+ // this is the canonical "first" entry by convention (main app before IDE).
36
+ function targetPrimaryDir(target) {
37
+ return targetDirs(target)[0];
38
+ }
39
+
23
40
  const FETCH_TIMEOUT_MS = 20_000;
24
41
  const DISCOVER_CONCURRENCY = 4;
25
42
  const DISCOVER_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -130,7 +147,7 @@ function targetList() {
130
147
  .map((target) => ({
131
148
  id: target.id,
132
149
  label: target.label,
133
- path: target.dir(),
150
+ path: targetPrimaryDir(target),
134
151
  }));
135
152
  }
136
153
 
@@ -342,27 +359,35 @@ function syncSkillToTarget(directory, targetId) {
342
359
  const target = TARGETS[targetId];
343
360
  if (!target) throw new Error(`Unsupported target: ${targetId}`);
344
361
  const source = path.join(ssotDir(), directory);
345
- const dest = path.join(target.dir(), directory);
346
362
  if (!fs.existsSync(source)) throw new Error(`Managed skill not found: ${directory}`);
347
- ensureDir(path.dirname(dest));
348
- removePath(dest);
349
- try {
350
- fs.symlinkSync(source, dest, "dir");
351
- } catch (_e) {
352
- copyDir(source, dest);
363
+ for (const baseDir of targetDirs(target)) {
364
+ const dest = path.join(baseDir, directory);
365
+ ensureDir(path.dirname(dest));
366
+ removePath(dest);
367
+ try {
368
+ fs.symlinkSync(source, dest, "dir");
369
+ } catch (_e) {
370
+ copyDir(source, dest);
371
+ }
353
372
  }
354
373
  }
355
374
 
356
375
  function removeSkillFromTarget(directory, targetId) {
357
376
  const target = TARGETS[targetId];
358
377
  if (!target) return;
359
- removePath(path.join(target.dir(), directory));
378
+ for (const baseDir of targetDirs(target)) {
379
+ removePath(path.join(baseDir, directory));
380
+ }
360
381
  }
361
382
 
362
383
  function scanTargetSkill(directory, targetId) {
363
384
  const target = TARGETS[targetId];
364
385
  if (!target) return false;
365
- return fs.existsSync(path.join(target.dir(), directory)) || isSymlink(path.join(target.dir(), directory));
386
+ for (const baseDir of targetDirs(target)) {
387
+ const candidate = path.join(baseDir, directory);
388
+ if (fs.existsSync(candidate) || isSymlink(candidate)) return true;
389
+ }
390
+ return false;
366
391
  }
367
392
 
368
393
  function listInstalledSkills() {
@@ -378,41 +403,42 @@ function listInstalledSkills() {
378
403
  const managedDirs = new Set(managed.map((skill) => skill.directory.toLowerCase()));
379
404
  const unmanaged = new Map();
380
405
  for (const target of Object.values(TARGETS)) {
381
- const dir = target.dir();
382
- let entries = [];
383
- try {
384
- entries = fs.readdirSync(dir, { withFileTypes: true });
385
- } catch (_e) {
386
- continue;
387
- }
388
- for (const entry of entries) {
389
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
390
- const directory = entry.name;
391
- if (!directory || directory.startsWith(".") || managedDirs.has(directory.toLowerCase())) continue;
392
- const skillPath = path.join(dir, directory, "SKILL.md");
393
- if (!fs.existsSync(skillPath)) continue;
394
- const metadata = readSkillMetadata(fs.readFileSync(skillPath, "utf8"), directory);
395
- const key = directory.toLowerCase();
396
- if (!unmanaged.has(key)) {
397
- unmanaged.set(key, {
398
- id: `local:${directory}`,
399
- key: `local:${directory}`,
400
- name: metadata.name,
401
- description: metadata.description,
402
- directory,
403
- readmeUrl: null,
404
- repoOwner: null,
405
- repoName: null,
406
- repoBranch: null,
407
- installedAt: null,
408
- managed: false,
409
- targets: [],
410
- targetPaths: {},
411
- });
406
+ for (const dir of targetDirs(target)) {
407
+ let entries = [];
408
+ try {
409
+ entries = fs.readdirSync(dir, { withFileTypes: true });
410
+ } catch (_e) {
411
+ continue;
412
+ }
413
+ for (const entry of entries) {
414
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
415
+ const directory = entry.name;
416
+ if (!directory || directory.startsWith(".") || managedDirs.has(directory.toLowerCase())) continue;
417
+ const skillPath = path.join(dir, directory, "SKILL.md");
418
+ if (!fs.existsSync(skillPath)) continue;
419
+ const metadata = readSkillMetadata(fs.readFileSync(skillPath, "utf8"), directory);
420
+ const key = directory.toLowerCase();
421
+ if (!unmanaged.has(key)) {
422
+ unmanaged.set(key, {
423
+ id: `local:${directory}`,
424
+ key: `local:${directory}`,
425
+ name: metadata.name,
426
+ description: metadata.description,
427
+ directory,
428
+ readmeUrl: null,
429
+ repoOwner: null,
430
+ repoName: null,
431
+ repoBranch: null,
432
+ installedAt: null,
433
+ managed: false,
434
+ targets: [],
435
+ targetPaths: {},
436
+ });
437
+ }
438
+ const skill = unmanaged.get(key);
439
+ if (!skill.targets.includes(target.id)) skill.targets.push(target.id);
440
+ if (!skill.targetPaths[target.id]) skill.targetPaths[target.id] = path.join(dir, directory);
412
441
  }
413
- const skill = unmanaged.get(key);
414
- skill.targets.push(target.id);
415
- skill.targetPaths[target.id] = path.join(dir, directory);
416
442
  }
417
443
  }
418
444
 
@@ -596,10 +622,12 @@ function findLocalSkillSource(directory) {
596
622
  const installName = sanitizePathSegment(directory);
597
623
  if (!installName) return null;
598
624
  for (const target of Object.values(TARGETS)) {
599
- const skillPath = path.join(target.dir(), installName);
600
- const docPath = path.join(skillPath, "SKILL.md");
601
- if (fs.existsSync(docPath)) {
602
- return { path: skillPath, targetId: target.id };
625
+ for (const baseDir of targetDirs(target)) {
626
+ const skillPath = path.join(baseDir, installName);
627
+ const docPath = path.join(skillPath, "SKILL.md");
628
+ if (fs.existsSync(docPath)) {
629
+ return { path: skillPath, targetId: target.id };
630
+ }
603
631
  }
604
632
  }
605
633
  return null;