@use-aistack/cli 0.13.0 → 0.15.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.
package/dist/index.js CHANGED
@@ -9,7 +9,11 @@ var LOCAL_PRICING_TABLE_VERSION = "local-no-charge";
9
9
  var PROVIDER_VENDOR = {
10
10
  anthropic: "anthropic",
11
11
  openai: "openai",
12
- google: "google"
12
+ google: "google",
13
+ xai: "xai"
14
+ };
15
+ var PRICING_ALIASES = {
16
+ "grok-4.6-build": "grok-4.6"
13
17
  };
14
18
  var LOCAL_PROVIDERS = /* @__PURE__ */ new Set([
15
19
  "ollama",
@@ -105,6 +109,11 @@ var Pricer = class {
105
109
  firstLayerWith(slug, provider) {
106
110
  return this.layers.find((l) => l.has(slug, provider)) ?? null;
107
111
  }
112
+ lookupSlug(slug, provider) {
113
+ if (this.firstLayerWith(slug, provider)) return slug;
114
+ const alias = PRICING_ALIASES[slug];
115
+ return alias && this.firstLayerWith(alias, provider) ? alias : null;
116
+ }
108
117
  /**
109
118
  * Every period that applies to a pricing key, or an empty list when none
110
119
  * can be cited.
@@ -117,14 +126,19 @@ var Pricer = class {
117
126
  periodsFor(modelKey) {
118
127
  const { provider, model } = splitModelKey(modelKey);
119
128
  if (provider === null) {
120
- return this.firstLayerWith(model, null)?.rowsFor(model, null) ?? [];
129
+ const slug = this.lookupSlug(model, null);
130
+ return slug ? this.firstLayerWith(slug, null)?.rowsFor(slug, null) ?? [] : [];
121
131
  }
122
132
  if (LOCAL_PROVIDERS.has(provider)) return [FREE_PERIOD];
123
- const own = this.firstLayerWith(model, provider);
124
- if (own) return own.rowsFor(model, provider);
133
+ const ownSlug = this.lookupSlug(model, provider);
134
+ if (ownSlug) {
135
+ return this.firstLayerWith(ownSlug, provider)?.rowsFor(ownSlug, provider) ?? [];
136
+ }
125
137
  const vendor = PROVIDER_VENDOR[provider];
126
- if (!vendor || this.vendorOf(model) !== vendor) return [];
127
- return this.firstLayerWith(model, null)?.rowsFor(model, null) ?? [];
138
+ const vendorSlug = this.lookupSlug(model, null);
139
+ if (!vendor || !vendorSlug || this.vendorOf(vendorSlug) !== vendor)
140
+ return [];
141
+ return this.firstLayerWith(vendorSlug, null)?.rowsFor(vendorSlug, null) ?? [];
128
142
  }
129
143
  isLocal(modelKey) {
130
144
  const { provider } = splitModelKey(modelKey);
@@ -176,7 +190,7 @@ function parsePriceTable(body) {
176
190
  if (typeof r?.modelSlug !== "string" || r.modelSlug.length === 0 || !num(r.from) || !num(r.input) || !num(r.output) || typeof r.source !== "string") {
177
191
  continue;
178
192
  }
179
- const vendor = r.vendor === "anthropic" || r.vendor === "openai" || r.vendor === "google" || r.vendor === "local" ? r.vendor : void 0;
193
+ const vendor = r.vendor === "anthropic" || r.vendor === "openai" || r.vendor === "google" || r.vendor === "xai" || r.vendor === "local" ? r.vendor : void 0;
180
194
  rows.push({
181
195
  modelSlug: r.modelSlug,
182
196
  ...typeof r.provider === "string" && r.provider.length > 0 ? { provider: r.provider } : {},
@@ -198,7 +212,8 @@ function parsePriceTable(body) {
198
212
  var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
199
213
  var OPENAI_PRICING_TABLE_VERSION = "openai-list-2026-08-02";
200
214
  var GOOGLE_PRICING_TABLE_VERSION = "google-list-2026-08-09";
201
- var BUNDLED_PRICE_TABLE_ID = "bundled-2026-08-29";
215
+ var XAI_PRICING_TABLE_VERSION = "models.dev@2026-08-29";
216
+ var BUNDLED_PRICE_TABLE_ID = "bundled-2026-09-10";
202
217
  var CACHE_WRITE_5M_MULTIPLIER = 1.25;
203
218
  var CACHE_WRITE_1H_MULTIPLIER = 2;
204
219
  var CACHE_READ_MULTIPLIER = 0.1;
@@ -213,6 +228,11 @@ var GOOGLE_CACHE_MULTIPLIERS = {
213
228
  write1h: 1,
214
229
  read: 0.1
215
230
  };
231
+ var XAI_CACHE_MULTIPLIERS = {
232
+ write5m: 0,
233
+ write1h: 0,
234
+ read: 0.25
235
+ };
216
236
  var anthropic = (periods) => ({
217
237
  vendor: "anthropic",
218
238
  table: PRICING_TABLE_VERSION,
@@ -231,6 +251,12 @@ var google = (periods) => ({
231
251
  periods,
232
252
  cache: GOOGLE_CACHE_MULTIPLIERS
233
253
  });
254
+ var xai = (periods) => ({
255
+ vendor: "xai",
256
+ table: XAI_PRICING_TABLE_VERSION,
257
+ periods,
258
+ cache: XAI_CACHE_MULTIPLIERS
259
+ });
234
260
  var flat = (input, output) => [
235
261
  { from: null, input, output }
236
262
  ];
@@ -288,7 +314,10 @@ var PRICES = {
288
314
  "gemini-3-pro-preview": google(flat(2, 12)),
289
315
  // A real Anthropic model with no row until #123. Measured in #122 as
290
316
  // `claude-opus-4-5-20251101`, which the dated-suffix rule strips to this key.
291
- "claude-opus-4-5": anthropic(flat(5, 25))
317
+ "claude-opus-4-5": anthropic(flat(5, 25)),
318
+ // models.dev's xAI row mirrored into the live table on 2026-08-29. Grok
319
+ // Build's explicit `grok-4.6-build` pricing alias reaches this base rate.
320
+ "grok-4.6": xai(flat(2, 6))
292
321
  };
293
322
  function bundledPriceTable() {
294
323
  const rows = [];
@@ -348,12 +377,12 @@ function apiEquivalentCost(modelKey, t, atMs) {
348
377
  }
349
378
 
350
379
  // src/version.ts
351
- var CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
380
+ var CLI_VERSION = true ? "0.15.0" : "0.0.0-dev";
352
381
 
353
382
  // src/api.ts
354
383
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
355
- async function request(path7, options = {}) {
356
- return fetch(`${BASE_URL}${path7}`, {
384
+ async function request(path13, options = {}) {
385
+ return fetch(`${BASE_URL}${path13}`, {
357
386
  ...options,
358
387
  headers: {
359
388
  "Content-Type": "application/json",
@@ -771,9 +800,9 @@ function canonicalizeRepoUrl(input) {
771
800
  function repoNameFromCanonical(canonical) {
772
801
  return parseRepo(canonical)?.repo ?? "";
773
802
  }
774
- function normalizeUpstreamPath(path7) {
775
- if (!path7) return "";
776
- return path7.split("/").filter(Boolean).join("/");
803
+ function normalizeUpstreamPath(path13) {
804
+ if (!path13) return "";
805
+ return path13.split("/").filter(Boolean).join("/");
777
806
  }
778
807
 
779
808
  // src/git.ts
@@ -819,16 +848,16 @@ function buildRepoLinkResource(canonical) {
819
848
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
820
849
  import { homedir as homedir2 } from "node:os";
821
850
  import { join as join2 } from "node:path";
822
- function readJson(path7) {
851
+ function readJson(path13) {
823
852
  try {
824
- if (!existsSync2(path7)) return null;
825
- return JSON.parse(readFileSync2(path7, "utf-8"));
853
+ if (!existsSync2(path13)) return null;
854
+ return JSON.parse(readFileSync2(path13, "utf-8"));
826
855
  } catch {
827
856
  return null;
828
857
  }
829
858
  }
830
- function hooksFrom(path7, source, out, seen) {
831
- const hooks = readJson(path7)?.hooks;
859
+ function hooksFrom(path13, source, out, seen) {
860
+ const hooks = readJson(path13)?.hooks;
832
861
  if (!hooks || typeof hooks !== "object") return;
833
862
  for (const [event, config] of Object.entries(hooks)) {
834
863
  const stableKey = `hooks:${source}:${event}`;
@@ -917,10 +946,19 @@ function splitImageTag(image) {
917
946
  }
918
947
  function parseMcpPackage(server) {
919
948
  if (server.url) {
949
+ let safeUrl;
950
+ try {
951
+ const parsed = new URL(server.url);
952
+ parsed.username = "";
953
+ parsed.password = "";
954
+ safeUrl = parsed.toString();
955
+ } catch {
956
+ return null;
957
+ }
920
958
  const t = (server.type ?? server.transport ?? "").toLowerCase();
921
959
  return {
922
960
  registry: "url",
923
- id: server.url,
961
+ id: safeUrl,
924
962
  transport: t === "sse" ? "sse" : "http"
925
963
  };
926
964
  }
@@ -968,16 +1006,16 @@ function buildMcpResource(name, group, pkg) {
968
1006
  pkg
969
1007
  };
970
1008
  }
971
- function readText(path7) {
1009
+ function readText(path13) {
972
1010
  try {
973
- if (!existsSync3(path7)) return null;
974
- return readFileSync3(path7, "utf-8");
1011
+ if (!existsSync3(path13)) return null;
1012
+ return readFileSync3(path13, "utf-8");
975
1013
  } catch {
976
1014
  return null;
977
1015
  }
978
1016
  }
979
- function readParsed(path7, parse2) {
980
- const raw = readText(path7);
1017
+ function readParsed(path13, parse2) {
1018
+ const raw = readText(path13);
981
1019
  if (raw === null) return null;
982
1020
  try {
983
1021
  return parse2(raw);
@@ -985,14 +1023,14 @@ function readParsed(path7, parse2) {
985
1023
  return null;
986
1024
  }
987
1025
  }
988
- function readJson2(path7) {
989
- return readParsed(path7, JSON.parse);
1026
+ function readJson2(path13) {
1027
+ return readParsed(path13, JSON.parse);
990
1028
  }
991
- function readYaml(path7) {
992
- return readParsed(path7, parseYaml);
1029
+ function readYaml(path13) {
1030
+ return readParsed(path13, parseYaml);
993
1031
  }
994
- function readToml(path7) {
995
- return readParsed(path7, parseToml);
1032
+ function readToml(path13) {
1033
+ return readParsed(path13, parseToml);
996
1034
  }
997
1035
  function continueListToMap(file) {
998
1036
  if (!file?.mcpServers?.length) return void 0;
@@ -1027,6 +1065,25 @@ function detectMcpServers(cwd, home = homedir3()) {
1027
1065
  out.push(resource);
1028
1066
  }
1029
1067
  };
1068
+ const grokHome = process.env.GROK_HOME ?? join3(home, ".grok");
1069
+ const grokGlobal = readToml(join3(grokHome, "config.toml"));
1070
+ const grokProject = readToml(join3(cwd, ".grok", "config.toml"));
1071
+ const disabled = /* @__PURE__ */ new Set([
1072
+ ...grokGlobal?.disabled_mcp_servers ?? [],
1073
+ ...grokProject?.disabled_mcp_servers ?? []
1074
+ ]);
1075
+ const effectiveGrok = {
1076
+ ...grokGlobal?.mcp_servers ?? {},
1077
+ ...grokProject?.mcp_servers ?? {}
1078
+ };
1079
+ add(
1080
+ Object.fromEntries(
1081
+ Object.entries(effectiveGrok).filter(
1082
+ ([name, config]) => config.enabled !== false && !disabled.has(name)
1083
+ )
1084
+ ),
1085
+ "grok-build"
1086
+ );
1030
1087
  add(readJson2(join3(cwd, ".mcp.json"))?.mcpServers, "claude-code");
1031
1088
  add(readJson2(join3(cwd, "mcp.json"))?.mcpServers, "generic");
1032
1089
  add(
@@ -1112,8 +1169,8 @@ function resolveSource(entry, mpRepoUrl) {
1112
1169
  const src = entry.source;
1113
1170
  if (typeof src === "string") {
1114
1171
  if (!mpRepoUrl) return null;
1115
- const path7 = src.replace(/^\.\//, "").replace(/\/+$/, "");
1116
- return { url: mpRepoUrl, path: path7 || void 0 };
1172
+ const path13 = src.replace(/^\.\//, "").replace(/\/+$/, "");
1173
+ return { url: mpRepoUrl, path: path13 || void 0 };
1117
1174
  }
1118
1175
  if (src && typeof src === "object" && src.url) {
1119
1176
  return { url: src.url, path: src.path, sha: src.sha };
@@ -1150,10 +1207,10 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
1150
1207
  }
1151
1208
  return out;
1152
1209
  }
1153
- function readJson3(path7) {
1210
+ function readJson3(path13) {
1154
1211
  try {
1155
- if (!existsSync4(path7)) return null;
1156
- return JSON.parse(readFileSync4(path7, "utf-8"));
1212
+ if (!existsSync4(path13)) return null;
1213
+ return JSON.parse(readFileSync4(path13, "utf-8"));
1157
1214
  } catch {
1158
1215
  return null;
1159
1216
  }
@@ -1187,6 +1244,7 @@ var LOCAL_PATTERNS = [
1187
1244
  // Rules
1188
1245
  { path: "CLAUDE.md", type: "rule", group: "claude-code" },
1189
1246
  { path: "AGENTS.md", type: "rule", group: "claude-code" },
1247
+ { path: "GROK.md", type: "rule", group: "grok-build" },
1190
1248
  { path: "GEMINI.md", type: "rule", group: "gemini" },
1191
1249
  { path: ".cursorrules", type: "rule", group: "cursor" },
1192
1250
  { path: ".windsurfrules", type: "rule", group: "windsurf" },
@@ -1217,8 +1275,16 @@ var LOCAL_DIR_PATTERNS = [
1217
1275
  { dir: ".github/instructions", type: "rule", group: "copilot" },
1218
1276
  { dir: ".github/prompts", type: "prompt", group: "copilot" },
1219
1277
  { dir: ".claude/commands", type: "command", group: "claude-code" },
1278
+ { dir: ".claude/skills", type: "skill", group: "claude-code" },
1220
1279
  { dir: ".claude/agents", type: "subagent", group: "claude-code" },
1221
1280
  { dir: ".claude/hooks", type: "hook", group: "claude-code" },
1281
+ { dir: ".grok/skills", type: "skill", group: "grok-build" },
1282
+ { dir: ".grok/commands", type: "command", group: "grok-build" },
1283
+ { dir: ".grok/agents", type: "subagent", group: "grok-build" },
1284
+ { dir: ".grok/hooks", type: "hook", group: "grok-build" },
1285
+ { dir: ".cursor/skills", type: "skill", group: "cursor" },
1286
+ { dir: ".agents/skills", type: "skill", group: "generic" },
1287
+ { dir: ".agents/commands", type: "command", group: "generic" },
1222
1288
  { dir: "prompts", type: "prompt", group: "generic" },
1223
1289
  { dir: ".ai", type: "custom", group: "generic" }
1224
1290
  ];
@@ -1233,8 +1299,8 @@ function loadGitignore(cwd) {
1233
1299
  }
1234
1300
  function readFileSafe(filePath) {
1235
1301
  try {
1236
- const stat6 = statSync(filePath);
1237
- if (stat6.size > MAX_FILE_SIZE) return null;
1302
+ const stat8 = statSync(filePath);
1303
+ if (stat8.size > MAX_FILE_SIZE) return null;
1238
1304
  return readFileSync5(filePath, "utf-8");
1239
1305
  } catch {
1240
1306
  return null;
@@ -1299,7 +1365,9 @@ function scanLocal(cwd) {
1299
1365
  for (const entry of readdirSync2(cwd, { withFileTypes: true }).filter(
1300
1366
  (e) => e.isDirectory()
1301
1367
  )) {
1302
- if (ig.ignores(entry.name + "/")) continue;
1368
+ if ([".grok", ".claude", ".cursor", ".agents"].includes(entry.name))
1369
+ continue;
1370
+ if (ig.ignores(`${entry.name}/`)) continue;
1303
1371
  scanSkillDirs(join5(cwd, entry.name), cwd, ig, results, 1);
1304
1372
  }
1305
1373
  } catch {
@@ -1332,7 +1400,7 @@ function scanSkillDirs(dir, cwd, ig, results, depth) {
1332
1400
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1333
1401
  if (entry.isDirectory()) {
1334
1402
  const rel = relative(cwd, join5(dir, entry.name));
1335
- if (!ig.ignores(rel + "/")) {
1403
+ if (!ig.ignores(`${rel}/`)) {
1336
1404
  scanSkillDirs(join5(dir, entry.name), cwd, ig, results, depth + 1);
1337
1405
  }
1338
1406
  }
@@ -1345,6 +1413,7 @@ function scanGlobal() {
1345
1413
  const results = [];
1346
1414
  const globalPatterns = [
1347
1415
  { path: ".claude/CLAUDE.md", type: "rule", group: "claude-code" },
1416
+ { path: ".grok/GROK.md", type: "rule", group: "grok-build" },
1348
1417
  { path: ".claude/settings.json", type: "config", group: "claude-code" },
1349
1418
  { path: ".continue/config.json", type: "config", group: "continue" },
1350
1419
  { path: ".continue/config.yaml", type: "config", group: "continue" },
@@ -1376,7 +1445,14 @@ function scanGlobal() {
1376
1445
  { dir: ".claude/commands", type: "command", group: "claude-code" },
1377
1446
  { dir: ".claude/agents", type: "subagent", group: "claude-code" },
1378
1447
  { dir: ".claude/hooks", type: "hook", group: "claude-code" },
1379
- { dir: ".cursor/rules", type: "rule", group: "cursor" }
1448
+ { dir: ".grok/skills", type: "skill", group: "grok-build" },
1449
+ { dir: ".grok/commands", type: "command", group: "grok-build" },
1450
+ { dir: ".grok/agents", type: "subagent", group: "grok-build" },
1451
+ { dir: ".grok/hooks", type: "hook", group: "grok-build" },
1452
+ { dir: ".cursor/rules", type: "rule", group: "cursor" },
1453
+ { dir: ".cursor/skills", type: "skill", group: "cursor" },
1454
+ { dir: ".agents/skills", type: "skill", group: "generic" },
1455
+ { dir: ".agents/commands", type: "command", group: "generic" }
1380
1456
  ];
1381
1457
  for (const { dir, type, group } of globalDirs) {
1382
1458
  const dirPath = join5(home, dir);
@@ -1443,9 +1519,9 @@ function lines(items) {
1443
1519
  console.log(`${BAR} ${item}`);
1444
1520
  }
1445
1521
  }
1446
- function section(label, count) {
1522
+ function section(label, count2) {
1447
1523
  console.log(`${BAR}`);
1448
- const countStr = count !== void 0 ? ` ${dim(String(count))}` : "";
1524
+ const countStr = count2 !== void 0 ? ` ${dim(String(count2))}` : "";
1449
1525
  console.log(`${BAR} ${bold(label.toUpperCase())}${countStr}`);
1450
1526
  }
1451
1527
  function divider() {
@@ -1768,7 +1844,7 @@ function diffResources(current, existing) {
1768
1844
  // src/commands/connect.ts
1769
1845
  import { spawnSync } from "node:child_process";
1770
1846
  import { cpSync, existsSync as existsSync6 } from "node:fs";
1771
- import { homedir as homedir10 } from "node:os";
1847
+ import { homedir as homedir13 } from "node:os";
1772
1848
  import { dirname as dirname3, join as join6 } from "node:path";
1773
1849
  import { fileURLToPath } from "node:url";
1774
1850
  import * as p3 from "@clack/prompts";
@@ -1914,12 +1990,12 @@ function emptyUsage() {
1914
1990
  };
1915
1991
  }
1916
1992
  var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1917
- function addModelUsage(agg, modelKey, counts, costUSD, messages = 1, at) {
1993
+ function addModelUsage(agg, modelKey, counts2, costUSD, messages = 1, at) {
1918
1994
  if (at) {
1919
1995
  noteUsageResponse(agg, {
1920
1996
  tsMs: at.tsMs,
1921
1997
  modelKey,
1922
- counts,
1998
+ counts: counts2,
1923
1999
  costUSD,
1924
2000
  ...at.sidechain ? { sidechain: true } : {}
1925
2001
  });
@@ -1930,13 +2006,13 @@ function addModelUsage(agg, modelKey, counts, costUSD, messages = 1, at) {
1930
2006
  agg.byModel.set(modelKey, m);
1931
2007
  }
1932
2008
  m.messages += messages;
1933
- m.input += counts.input;
1934
- m.output += counts.output;
1935
- m.cacheWrite5m += counts.cacheWrite5m;
1936
- m.cacheWrite1h += counts.cacheWrite1h;
1937
- m.cacheWriteUnsplit += counts.cacheWriteUnsplit;
1938
- m.cacheRead += counts.cacheRead;
1939
- if (costUSD === null) m.unpricedTokens += countsTotal(counts);
2009
+ m.input += counts2.input;
2010
+ m.output += counts2.output;
2011
+ m.cacheWrite5m += counts2.cacheWrite5m;
2012
+ m.cacheWrite1h += counts2.cacheWrite1h;
2013
+ m.cacheWriteUnsplit += counts2.cacheWriteUnsplit;
2014
+ m.cacheRead += counts2.cacheRead;
2015
+ if (costUSD === null) m.unpricedTokens += countsTotal(counts2);
1940
2016
  else m.costUSD += costUSD;
1941
2017
  }
1942
2018
  function buildModelRows(agg) {
@@ -2366,7 +2442,7 @@ function filterAtoms(atoms, sets) {
2366
2442
  }
2367
2443
  merged.set(published, (merged.get(published) ?? 0) + atom.count);
2368
2444
  }
2369
- const allowed = [...merged].map(([name, count]) => ({ name, count }));
2445
+ const allowed = [...merged].map(([name, count2]) => ({ name, count: count2 }));
2370
2446
  allowed.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2371
2447
  keptPrivate.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2372
2448
  return { allowed, keptPrivate, withheld: keptPrivate.length };
@@ -2781,13 +2857,13 @@ function playbookHarnesses(reading) {
2781
2857
  return reading.harnesses.filter((harness) => harness.phase !== void 0);
2782
2858
  }
2783
2859
  function startHoursUtc(reading) {
2784
- const counts = /* @__PURE__ */ new Map();
2860
+ const counts2 = /* @__PURE__ */ new Map();
2785
2861
  for (const harness of reading.harnesses) {
2786
2862
  for (const cell of harness.startHours) {
2787
- counts.set(cell.hourUtc, (counts.get(cell.hourUtc) ?? 0) + cell.sessions);
2863
+ counts2.set(cell.hourUtc, (counts2.get(cell.hourUtc) ?? 0) + cell.sessions);
2788
2864
  }
2789
2865
  }
2790
- return counts;
2866
+ return counts2;
2791
2867
  }
2792
2868
  function ownerLocalHour(hourUtc, offsetMinutes) {
2793
2869
  return Math.floor(((hourUtc * 60 + offsetMinutes) / 60 % 24 + 24) % 24);
@@ -2795,13 +2871,13 @@ function ownerLocalHour(hourUtc, offsetMinutes) {
2795
2871
  function modalStartHour(reading) {
2796
2872
  const offsetMinutes = reading.utcOffsetMinutes;
2797
2873
  if (offsetMinutes === void 0) return void 0;
2798
- const counts = /* @__PURE__ */ new Map();
2874
+ const counts2 = /* @__PURE__ */ new Map();
2799
2875
  for (const [hourUtc, sessions] of startHoursUtc(reading)) {
2800
2876
  const hour = ownerLocalHour(hourUtc, offsetMinutes);
2801
- counts.set(hour, (counts.get(hour) ?? 0) + sessions);
2877
+ counts2.set(hour, (counts2.get(hour) ?? 0) + sessions);
2802
2878
  }
2803
- if (counts.size === 0) return void 0;
2804
- return [...counts.entries()].sort(
2879
+ if (counts2.size === 0) return void 0;
2880
+ return [...counts2.entries()].sort(
2805
2881
  (a, b) => b[1] - a[1] || a[0] - b[0]
2806
2882
  )[0]?.[0];
2807
2883
  }
@@ -2809,10 +2885,10 @@ function modalStartHour(reading) {
2809
2885
  // ../workflow-rules/src/componentRules.ts
2810
2886
  var COMPONENT_RULES_V2 = "component-rules/v2";
2811
2887
  var gitCoverage = () => 1;
2812
- function harnessShare(input, counts) {
2888
+ function harnessShare(input, counts2) {
2813
2889
  const synced = input.reading.harnesses.length;
2814
2890
  if (synced === 0) return 0;
2815
- return counts(input) / synced;
2891
+ return counts2(input) / synced;
2816
2892
  }
2817
2893
  function topShare(entries) {
2818
2894
  const total = entries.reduce((sum, entry) => sum + entry.value, 0);
@@ -3123,6 +3199,8 @@ var HANDOFF_MARKERS = {
3123
3199
  "mcp__curia__request_review"
3124
3200
  ],
3125
3201
  codex: ["request_user_input"],
3202
+ cursor: ["ask_question"],
3203
+ "grok-build": ["ask_user_question", "request_user_input"],
3126
3204
  opencode: ["question"],
3127
3205
  "pi-mono": []
3128
3206
  };
@@ -3153,7 +3231,11 @@ var SCOUT_TOOLS = [
3153
3231
  "web_search",
3154
3232
  "tool_search",
3155
3233
  "codebase_search",
3156
- "find"
3234
+ "find",
3235
+ "search_tool",
3236
+ "search_files",
3237
+ "list_dir",
3238
+ "read_file"
3157
3239
  ];
3158
3240
  var EDIT_TOOLS = [
3159
3241
  "Edit",
@@ -3164,11 +3246,20 @@ var EDIT_TOOLS = [
3164
3246
  "write",
3165
3247
  "patch",
3166
3248
  "multiedit",
3167
- "apply_patch"
3249
+ "apply_patch",
3250
+ "write_file",
3251
+ "edit_file"
3168
3252
  ];
3169
3253
  var REVIEW_SKILLS = ["code-review", "security-review", "review"];
3170
3254
  var SCOUT_AGENTS = ["Explore", "Plan", "research"];
3171
- var SHELL_TOOLS = ["Bash", "bash", "shell", "local_shell", "exec_command"];
3255
+ var SHELL_TOOLS = [
3256
+ "Bash",
3257
+ "bash",
3258
+ "shell",
3259
+ "local_shell",
3260
+ "exec_command",
3261
+ "run_terminal_command"
3262
+ ];
3172
3263
  var SKILL_TOOLS = ["Skill", "skill"];
3173
3264
  var AGENT_TOOLS = ["Agent", "Task", "task", "agent"];
3174
3265
  var BOOKKEEPING_TOOLS = [
@@ -3642,7 +3733,7 @@ function sanitizeModelId(id) {
3642
3733
  var round4 = (n) => Math.round(n * 1e4) / 1e4;
3643
3734
  var round2 = (n) => Math.round(n * 100) / 100;
3644
3735
  var utcDate = (ms) => new Date(ms).toISOString().slice(0, 10);
3645
- var toAtoms = (pairs) => pairs.map(([name, count]) => ({ name, count }));
3736
+ var toAtoms = (pairs) => pairs.map(([name, count2]) => ({ name, count: count2 }));
3646
3737
  function buildCategory(observed, curated, optIns, denominator) {
3647
3738
  const publishable = /* @__PURE__ */ new Set([...curated, ...optIns]);
3648
3739
  const {
@@ -3935,7 +4026,7 @@ var bump2 = (map, key2, amount = 1) => {
3935
4026
  var utcDateOf2 = (ms) => new Date(ms).toISOString().slice(0, 10);
3936
4027
  function asBuckets(map, field) {
3937
4028
  return [...map].map(
3938
- ([bucket, count]) => ({ bucket, [field]: count })
4029
+ ([bucket, count2]) => ({ bucket, [field]: count2 })
3939
4030
  ).sort((a, b) => a.bucket - b.bucket);
3940
4031
  }
3941
4032
  var PHASE_RANK = {
@@ -4320,7 +4411,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
4320
4411
  0
4321
4412
  );
4322
4413
  const unknown = attributed === 0 ? 0 : windowPhaseSec.unknown / attributed;
4323
- const routesModels = harness === "claude-code" || harness === "opencode";
4414
+ const routesModels = harness === "claude-code" || harness === "opencode" || harness === "grok-build" || harness === "cursor";
4324
4415
  const asRows = (map) => {
4325
4416
  const safe = /* @__PURE__ */ new Map();
4326
4417
  for (const [model, tokens] of map) {
@@ -4343,7 +4434,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
4343
4434
  date,
4344
4435
  harness,
4345
4436
  sessions: day.sessions,
4346
- startHours: [...day.startHours].map(([hourUtc, count]) => ({ hourUtc, sessions: count })).sort((a, b) => a.hourUtc - b.hourUtc),
4437
+ startHours: [...day.startHours].map(([hourUtc, count2]) => ({ hourUtc, sessions: count2 })).sort((a, b) => a.hourUtc - b.hourUtc),
4347
4438
  ...day.phase.sessions > 0 ? {
4348
4439
  phase: {
4349
4440
  ruleVersion: PHASE_RULES_V1,
@@ -4471,12 +4562,12 @@ function ingestRecord(agg, raw, ctx) {
4471
4562
  const sessionId = asStr(rec.sessionId);
4472
4563
  if (sessionId) agg.sessions.add(sessionId);
4473
4564
  let tsMs = null;
4474
- const timestamp = asStr(rec.timestamp);
4475
- if (timestamp) {
4476
- const ts = Date.parse(timestamp);
4565
+ const timestamp2 = asStr(rec.timestamp);
4566
+ if (timestamp2) {
4567
+ const ts = Date.parse(timestamp2);
4477
4568
  if (!Number.isNaN(ts)) {
4478
4569
  tsMs = ts;
4479
- agg.activeDays.add(timestamp.slice(0, 10));
4570
+ agg.activeDays.add(timestamp2.slice(0, 10));
4480
4571
  agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
4481
4572
  agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
4482
4573
  }
@@ -4505,8 +4596,8 @@ function ingestClaudeCompaction(agg, rec, ctx, tsMs) {
4505
4596
  ...rec.isSidechain === true ? { sidechain: true } : {}
4506
4597
  });
4507
4598
  }
4508
- function contextOf(counts) {
4509
- return counts.input + counts.cacheWrite5m + counts.cacheWrite1h + counts.cacheWriteUnsplit + counts.cacheRead;
4599
+ function contextOf(counts2) {
4600
+ return counts2.input + counts2.cacheWrite5m + counts2.cacheWrite1h + counts2.cacheWriteUnsplit + counts2.cacheRead;
4510
4601
  }
4511
4602
  function ingestClaudeTurnDuration(agg, rec, ctx, tsMs) {
4512
4603
  if (tsMs === null || asStr(rec.subtype) !== "turn_duration" || asNum(rec.durationMs) <= 0) {
@@ -4534,12 +4625,12 @@ function ingestClaudeWorkflow(agg, rec, ctx, tsMs) {
4534
4625
  const session = sidechain ? `${baseSession}:agent:${agentId ?? "unknown"}` : baseSession;
4535
4626
  const parentSession = sidechain ? baseSession : void 0;
4536
4627
  const usage = asObj(msg.usage);
4537
- const counts = usage ? readCounts(usage) : null;
4628
+ const counts2 = usage ? readCounts(usage) : null;
4538
4629
  const messageId = asStr(msg.id);
4539
4630
  const newTurn = messageId ? !agg.workflowSeenTurns.has(messageId) : true;
4540
4631
  if (messageId) agg.workflowSeenTurns.add(messageId);
4541
4632
  let context = null;
4542
- if (counts && isApiCall(rec)) {
4633
+ if (counts2 && isApiCall(rec)) {
4543
4634
  const held = agg.contextFirstCall.get(session);
4544
4635
  let first = false;
4545
4636
  if (held === void 0) {
@@ -4547,11 +4638,11 @@ function ingestClaudeWorkflow(agg, rec, ctx, tsMs) {
4547
4638
  first = true;
4548
4639
  } else if (held !== null && held === messageId) first = true;
4549
4640
  context = {
4550
- contextTokens: contextOf(counts),
4641
+ contextTokens: contextOf(counts2),
4551
4642
  ...first ? {
4552
4643
  firstCall: {
4553
- harnessTokens: counts.cacheRead,
4554
- instructionsTokens: contextOf(counts) - counts.cacheRead
4644
+ harnessTokens: counts2.cacheRead,
4645
+ instructionsTokens: contextOf(counts2) - counts2.cacheRead
4555
4646
  }
4556
4647
  } : {}
4557
4648
  };
@@ -4565,8 +4656,8 @@ function ingestClaudeWorkflow(agg, rec, ctx, tsMs) {
4565
4656
  ...parentSession ? { parentSession } : {},
4566
4657
  ...messageId ? { responseId: messageId } : {},
4567
4658
  ...asName(msg.model) ? { model: asName(msg.model) } : {},
4568
- ...counts ? { responseTokens: counts.output } : {},
4569
- ...counts ? { routingTokens: countsTotal(counts) } : {},
4659
+ ...counts2 ? { responseTokens: counts2.output } : {},
4660
+ ...counts2 ? { routingTokens: countsTotal(counts2) } : {},
4570
4661
  ...asStr(rec.effort) ?? asStr(msg.effort) ? { effort: asStr(rec.effort) ?? asStr(msg.effort) } : {},
4571
4662
  ...context ?? {}
4572
4663
  });
@@ -4693,11 +4784,11 @@ function readCounts(usage) {
4693
4784
  function modelKeyFor2(model, speed) {
4694
4785
  return normalizeModel(speed === "fast" ? `${model}#fast` : model);
4695
4786
  }
4696
- function makeEntry(modelKey, counts, tsMs) {
4787
+ function makeEntry(modelKey, counts2, tsMs) {
4697
4788
  return {
4698
4789
  modelKey,
4699
- counts,
4700
- costUSD: apiEquivalentCost(modelKey, counts, tsMs)
4790
+ counts: counts2,
4791
+ costUSD: apiEquivalentCost(modelKey, counts2, tsMs)
4701
4792
  };
4702
4793
  }
4703
4794
  function buildContribution(usage, model, sidechain, tsMs) {
@@ -4742,24 +4833,24 @@ function buildContribution(usage, model, sidechain, tsMs) {
4742
4833
  };
4743
4834
  }
4744
4835
  function applyContribution(agg, c, sign) {
4745
- c.entries.forEach(({ modelKey, counts, costUSD }, i) => {
4836
+ c.entries.forEach(({ modelKey, counts: counts2, costUSD }, i) => {
4746
4837
  let m = agg.byModel.get(modelKey);
4747
4838
  if (!m) {
4748
4839
  m = emptyUsage();
4749
4840
  agg.byModel.set(modelKey, m);
4750
4841
  }
4751
4842
  if (i === 0) m.messages += sign;
4752
- m.input += sign * counts.input;
4753
- m.output += sign * counts.output;
4754
- m.cacheWrite5m += sign * counts.cacheWrite5m;
4755
- m.cacheWrite1h += sign * counts.cacheWrite1h;
4756
- m.cacheWriteUnsplit += sign * counts.cacheWriteUnsplit;
4757
- m.cacheRead += sign * counts.cacheRead;
4758
- if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
4843
+ m.input += sign * counts2.input;
4844
+ m.output += sign * counts2.output;
4845
+ m.cacheWrite5m += sign * counts2.cacheWrite5m;
4846
+ m.cacheWrite1h += sign * counts2.cacheWrite1h;
4847
+ m.cacheWriteUnsplit += sign * counts2.cacheWriteUnsplit;
4848
+ m.cacheRead += sign * counts2.cacheRead;
4849
+ if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts2);
4759
4850
  else m.costUSD += sign * costUSD;
4760
4851
  noteUsageResponse(
4761
4852
  agg,
4762
- { tsMs: c.tsMs, modelKey, counts, costUSD, sidechain: c.sidechain },
4853
+ { tsMs: c.tsMs, modelKey, counts: counts2, costUSD, sidechain: c.sidechain },
4763
4854
  sign
4764
4855
  );
4765
4856
  });
@@ -4769,8 +4860,8 @@ function applyContribution(agg, c, sign) {
4769
4860
  agg.webFetchRequests += sign * c.webFetch;
4770
4861
  agg.fallbackAttempts += sign * c.fallbackAttempts;
4771
4862
  agg.untypedMirrors += sign * c.untypedMirrors;
4772
- for (const [type, count] of c.mirroredIterationTypes) {
4773
- bump(agg.mirroredIterationTypes, type, sign * count);
4863
+ for (const [type, count2] of c.mirroredIterationTypes) {
4864
+ bump(agg.mirroredIterationTypes, type, sign * count2);
4774
4865
  }
4775
4866
  }
4776
4867
  function ingestContentBlocks(agg, content) {
@@ -5013,7 +5104,7 @@ function genuineDelta(payload, state, tsMs) {
5013
5104
  if (!last) return null;
5014
5105
  const inputTotal = asNum(last.input_tokens);
5015
5106
  const cached = Math.min(asNum(last.cached_input_tokens), inputTotal);
5016
- const counts = {
5107
+ const counts2 = {
5017
5108
  input: inputTotal - cached,
5018
5109
  output: asNum(last.output_tokens),
5019
5110
  cacheWrite5m: 0,
@@ -5021,11 +5112,11 @@ function genuineDelta(payload, state, tsMs) {
5021
5112
  cacheWriteUnsplit: 0,
5022
5113
  cacheRead: cached
5023
5114
  };
5024
- if (countsTotal(counts) === 0) return null;
5115
+ if (countsTotal(counts2) === 0) return null;
5025
5116
  if (tsMs !== null && state.metaTsMs !== null && tsMs - state.metaTsMs < FORK_REPLAY_WINDOW_MS)
5026
5117
  return null;
5027
5118
  return {
5028
- counts,
5119
+ counts: counts2,
5029
5120
  last,
5030
5121
  contextWindow: info ? asNum(info.model_context_window) : 0
5031
5122
  };
@@ -5036,9 +5127,9 @@ function ingestLine(agg, raw, state, sinceMs) {
5036
5127
  if (!rec) return;
5037
5128
  agg.records++;
5038
5129
  let tsMs = null;
5039
- const timestamp = asStr(rec.timestamp);
5040
- if (timestamp) {
5041
- const ts = Date.parse(timestamp);
5130
+ const timestamp2 = asStr(rec.timestamp);
5131
+ if (timestamp2) {
5132
+ const ts = Date.parse(timestamp2);
5042
5133
  if (!Number.isNaN(ts)) tsMs = ts;
5043
5134
  }
5044
5135
  const inWindow = sinceMs === void 0 || tsMs !== null && tsMs >= sinceMs;
@@ -5061,8 +5152,8 @@ function ingestLine(agg, raw, state, sinceMs) {
5061
5152
  state.sawResponse = true;
5062
5153
  }
5063
5154
  if (!inWindow) return;
5064
- if (tsMs !== null && timestamp) {
5065
- agg.activeDays.add(timestamp.slice(0, 10));
5155
+ if (tsMs !== null && timestamp2) {
5156
+ agg.activeDays.add(timestamp2.slice(0, 10));
5066
5157
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
5067
5158
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
5068
5159
  }
@@ -5092,8 +5183,8 @@ function noteActivity(agg, state, tsMs) {
5092
5183
  function ingestEvent(agg, payload, state, tsMs, firstCall) {
5093
5184
  const delta = genuineDelta(payload, state, tsMs);
5094
5185
  if (!delta) return;
5095
- const { counts, last, contextWindow } = delta;
5096
- const total = countsTotal(counts);
5186
+ const { counts: counts2, last, contextWindow } = delta;
5187
+ const total = countsTotal(counts2);
5097
5188
  if (state.modelKey === null) return;
5098
5189
  if (tsMs === null) agg.untimestampedResponses++;
5099
5190
  agg.distinctResponses++;
@@ -5101,8 +5192,8 @@ function ingestEvent(agg, payload, state, tsMs, firstCall) {
5101
5192
  addModelUsage(
5102
5193
  agg,
5103
5194
  modelKey,
5104
- counts,
5105
- apiEquivalentCost(modelKey, counts, tsMs),
5195
+ counts2,
5196
+ apiEquivalentCost(modelKey, counts2, tsMs),
5106
5197
  1,
5107
5198
  { tsMs }
5108
5199
  );
@@ -5116,7 +5207,7 @@ function ingestEvent(agg, payload, state, tsMs, firstCall) {
5116
5207
  projectWorkspace: state.cwd ?? void 0,
5117
5208
  tsMs,
5118
5209
  model: modelKey,
5119
- responseTokens: counts.output,
5210
+ responseTokens: counts2.output,
5120
5211
  routingTokens: total,
5121
5212
  thinkingTokens: asNum(last.reasoning_output_tokens),
5122
5213
  ...state.effort ? { effort: state.effort } : {},
@@ -5125,12 +5216,12 @@ function ingestEvent(agg, payload, state, tsMs, firstCall) {
5125
5216
  // instructions and tool specs, cached by an earlier session) and
5126
5217
  // the fresh part is the instructions: AGENTS.md, environment,
5127
5218
  // skills and the first prompt. Same method as Claude Code (#358).
5128
- contextTokens: counts.input + counts.cacheRead,
5219
+ contextTokens: counts2.input + counts2.cacheRead,
5129
5220
  ...contextWindow > 0 ? { contextWindow } : {},
5130
5221
  ...firstCall && !state.forked ? {
5131
5222
  firstCall: {
5132
- harnessTokens: counts.cacheRead,
5133
- instructionsTokens: counts.input
5223
+ harnessTokens: counts2.cacheRead,
5224
+ instructionsTokens: counts2.input
5134
5225
  }
5135
5226
  } : {}
5136
5227
  });
@@ -5350,11 +5441,11 @@ function ingestWithRetry(agg, file, opts) {
5350
5441
  }
5351
5442
  }
5352
5443
  function ingestFile2(agg, file, opts) {
5353
- const readFile = opts.readFileImpl ?? readFileSync6;
5444
+ const readFile3 = opts.readFileImpl ?? readFileSync6;
5354
5445
  let text;
5355
5446
  if (file.endsWith(".zst")) {
5356
5447
  if (zstdDecompress === null) throw readError("zstd-unsupported");
5357
- const raw = readFile(file);
5448
+ const raw = readFile3(file);
5358
5449
  try {
5359
5450
  text = zstdDecompress(
5360
5451
  Buffer.isBuffer(raw) ? raw : Buffer.from(raw)
@@ -5363,7 +5454,7 @@ function ingestFile2(agg, file, opts) {
5363
5454
  throw readError("zstd-corrupt");
5364
5455
  }
5365
5456
  } else {
5366
- text = readFile(file).toString("utf8");
5457
+ text = readFile3(file).toString("utf8");
5367
5458
  }
5368
5459
  const records = [];
5369
5460
  let nonEmptyLines = 0;
@@ -5466,6 +5557,1321 @@ var codexAdapter = {
5466
5557
  }
5467
5558
  };
5468
5559
 
5560
+ // src/harness/cursor/cache.ts
5561
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
5562
+ import { homedir as homedir9 } from "node:os";
5563
+ import path5 from "node:path";
5564
+
5565
+ // src/harness/cursor/account.ts
5566
+ import { createHash } from "node:crypto";
5567
+
5568
+ // src/harness/cursor/evidence.ts
5569
+ function timestamp(value) {
5570
+ const n = typeof value === "number" ? value : typeof value === "string" ? /^\d+$/.test(value) ? Number(value) : Date.parse(value) : NaN;
5571
+ return Number.isFinite(n) && n > 0 && n < 864e13 ? n : null;
5572
+ }
5573
+ function count(value) {
5574
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
5575
+ }
5576
+ function tokenEvidence(value) {
5577
+ const raw = asObj(value) ?? {};
5578
+ const direct = asObj(raw.tokenCount);
5579
+ const usage = asObj(raw.usage);
5580
+ for (const [obj, fields] of [
5581
+ [
5582
+ direct,
5583
+ ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]
5584
+ ],
5585
+ [
5586
+ usage,
5587
+ [
5588
+ "input_tokens",
5589
+ "output_tokens",
5590
+ "cache_read_input_tokens",
5591
+ "cache_creation_input_tokens"
5592
+ ]
5593
+ ]
5594
+ ]) {
5595
+ for (const field of fields)
5596
+ if (obj?.[field] != null && count(obj[field]) === void 0)
5597
+ throw new Error("Invalid Cursor token evidence");
5598
+ }
5599
+ const out = {};
5600
+ out.input = count(direct?.inputTokens) ?? count(usage?.input_tokens);
5601
+ out.output = count(direct?.outputTokens) ?? count(usage?.output_tokens);
5602
+ out.cacheRead = count(direct?.cacheReadTokens) ?? count(usage?.cache_read_input_tokens);
5603
+ out.cacheWrite = count(direct?.cacheWriteTokens) ?? count(usage?.cache_creation_input_tokens);
5604
+ if (out.input !== void 0) out.inputSource = "reported";
5605
+ if (out.output !== void 0) out.outputSource = "reported";
5606
+ if (out.input === void 0) {
5607
+ const context = count(asObj(raw.contextWindowStatusAtCreation)?.tokensUsed);
5608
+ if (context !== void 0) {
5609
+ out.input = context;
5610
+ out.inputSource = "context";
5611
+ } else if (typeof raw.promptDryRunInfo === "string") {
5612
+ try {
5613
+ const dry = asObj(JSON.parse(raw.promptDryRunInfo));
5614
+ const estimate = count(asObj(dry?.fullConversationTokenCount)?.numTokens) ?? count(asObj(dry?.userMessageTokenCount)?.numTokens);
5615
+ if (estimate !== void 0) {
5616
+ out.input = estimate;
5617
+ out.inputSource = "dry-run";
5618
+ }
5619
+ } catch {
5620
+ }
5621
+ }
5622
+ }
5623
+ return out;
5624
+ }
5625
+ var directTimes = /* @__PURE__ */ new Set([
5626
+ "composer-created-at",
5627
+ "composer-timing",
5628
+ "store-turn-timing"
5629
+ ]);
5630
+ function messageTimes(session, api = []) {
5631
+ const times = session.messages.map(
5632
+ (m) => directTimes.has(m.timestampSource ?? "") ? timestamp(m.timestamp) : null
5633
+ );
5634
+ const anchors = [];
5635
+ for (const [index, time] of times.entries())
5636
+ if (time !== null) anchors.push({ index, time });
5637
+ const start = session.createdAtSource !== "epoch-unknown" ? timestamp(session.timestamp) : null;
5638
+ const end = session.lastUpdatedAtSource !== "epoch-unknown" ? timestamp(session.metadata?.lastModified) : null;
5639
+ const apiTimes = api.filter((e) => e.session === session.id).map((e) => e.tsMs);
5640
+ if (!anchors.some((a) => a.index === 0)) {
5641
+ const time = start ?? (apiTimes.length ? Math.min(...apiTimes) : null);
5642
+ if (time !== null) anchors.unshift({ index: -1, time });
5643
+ }
5644
+ const last = session.messages.length;
5645
+ if (last && !anchors.some((a) => a.index === last - 1)) {
5646
+ const time = end ?? (apiTimes.length ? Math.max(...apiTimes) : null);
5647
+ if (time !== null) anchors.push({ index: last, time });
5648
+ }
5649
+ for (let i = 0; i < times.length; i++) {
5650
+ if (times[i] !== null) continue;
5651
+ const before = anchors.filter((a) => a.index < i).at(-1);
5652
+ const after = anchors.find((a) => a.index > i);
5653
+ if (before && after && after.time >= before.time)
5654
+ times[i] = Math.round(
5655
+ before.time + (after.time - before.time) * (i - before.index) / (after.index - before.index)
5656
+ );
5657
+ else times[i] = before?.time ?? after?.time ?? null;
5658
+ }
5659
+ return times;
5660
+ }
5661
+ function localContributions(local, api = []) {
5662
+ const { session, tokens } = local;
5663
+ const times = messageTimes(session, api);
5664
+ const out = [];
5665
+ let pending;
5666
+ const seen = /* @__PURE__ */ new Set();
5667
+ for (const [i, message] of session.messages.entries()) {
5668
+ if (message.id && seen.has(message.id)) continue;
5669
+ if (message.id) seen.add(message.id);
5670
+ if (message.role === "user") {
5671
+ pending = message;
5672
+ continue;
5673
+ }
5674
+ const own = tokens.get(message.id ?? "") ?? {};
5675
+ const user = tokens.get(pending?.id ?? "") ?? {};
5676
+ const input = own.input !== void 0 ? own : user;
5677
+ const buckets = {
5678
+ ...own,
5679
+ input: input.input ?? (pending ? Math.ceil(pending.content.length / 4) : void 0),
5680
+ inputSource: input.inputSource ?? (pending ? "text" : void 0),
5681
+ output: own.output ?? Math.ceil(message.content.length / 4),
5682
+ outputSource: own.outputSource ?? "text"
5683
+ };
5684
+ pending = void 0;
5685
+ const tsMs = times[i];
5686
+ if (tsMs === null) continue;
5687
+ out.push({
5688
+ session: session.id,
5689
+ ...message.id ? { id: message.id } : {},
5690
+ ...message.identityOrigin === "composer-native" && message.id ? { nativeId: message.id } : {},
5691
+ tsMs,
5692
+ model: asStr(message.model) ?? "unknown",
5693
+ buckets,
5694
+ source: "local",
5695
+ ...message.isSidechain ? { sidechain: true } : {}
5696
+ });
5697
+ }
5698
+ return out;
5699
+ }
5700
+ function reconcile(local, api) {
5701
+ const grain = (c) => `${c.session}\0${new Date(c.tsMs).toISOString().slice(0, 10)}`;
5702
+ const reported = api.filter(
5703
+ (row) => Object.values(row.buckets).some((value) => typeof value === "number")
5704
+ );
5705
+ const grains = new Set(reported.map(grain));
5706
+ return [...local.filter((c) => !grains.has(grain(c))), ...reported];
5707
+ }
5708
+
5709
+ // src/harness/cursor/local.ts
5710
+ import { stat as stat4 } from "node:fs/promises";
5711
+ import { homedir as homedir8 } from "node:os";
5712
+ import path4 from "node:path";
5713
+ function dataPath(env = process.env, home = homedir8(), platform4 = process.platform) {
5714
+ if (env.CURSOR_DATA_PATH) return path4.resolve(env.CURSOR_DATA_PATH);
5715
+ const base = platform4 === "darwin" ? path4.join(home, "Library", "Application Support") : platform4 === "win32" ? env.APPDATA || path4.join(home, "AppData", "Roaming") : env.XDG_CONFIG_HOME || path4.join(home, ".config");
5716
+ return path4.join(base, "Cursor", "User", "workspaceStorage");
5717
+ }
5718
+ var storeRoot = () => process.env.CURSOR_STORE_ROOT || path4.join(homedir8(), ".cursor");
5719
+ var globalDb = (root) => path4.join(path4.dirname(root), "globalStorage", "state.vscdb");
5720
+ async function exists3(file) {
5721
+ try {
5722
+ await stat4(file);
5723
+ return true;
5724
+ } catch (error) {
5725
+ return error.code !== "ENOENT";
5726
+ }
5727
+ }
5728
+ async function hasLocalSource(root = dataPath()) {
5729
+ return (await Promise.all(
5730
+ [
5731
+ root,
5732
+ globalDb(root),
5733
+ path4.join(storeRoot(), "projects"),
5734
+ path4.join(storeRoot(), "chats"),
5735
+ path4.join(storeRoot(), "acp-sessions")
5736
+ ].map(exists3)
5737
+ )).some(Boolean);
5738
+ }
5739
+ async function readTokenEvidence(root, session) {
5740
+ const out = /* @__PURE__ */ new Map();
5741
+ if (!session.messages.some((m) => m.identityOrigin?.startsWith("composer")))
5742
+ return out;
5743
+ const file = globalDb(root);
5744
+ if (!await exists3(file)) return out;
5745
+ const { DatabaseSync } = await import("node:sqlite");
5746
+ const db = new DatabaseSync(file, { readOnly: true });
5747
+ try {
5748
+ db.exec("BEGIN");
5749
+ const table = db.prepare(
5750
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'cursorDiskKV'"
5751
+ ).get();
5752
+ if (!table) return out;
5753
+ const query = db.prepare(`SELECT json_object(
5754
+ 'tokenCount', json_extract(value, '$.tokenCount'),
5755
+ 'usage', json_extract(value, '$.usage'),
5756
+ 'contextWindowStatusAtCreation', json_extract(value, '$.contextWindowStatusAtCreation'),
5757
+ 'promptDryRunInfo', json_extract(value, '$.promptDryRunInfo')) AS evidence
5758
+ FROM cursorDiskKV WHERE key = ?`);
5759
+ for (const message of session.messages) {
5760
+ if (!message.id || message.identityOrigin !== "composer-native") continue;
5761
+ const row = query.get(`bubbleId:${session.id}:${message.id}`);
5762
+ if (typeof row?.evidence === "string")
5763
+ out.set(message.id, tokenEvidence(JSON.parse(row.evidence)));
5764
+ }
5765
+ const header = db.prepare(
5766
+ "SELECT json_extract(value, '$.conversation') AS conversation FROM cursorDiskKV WHERE key = ?"
5767
+ ).get(`composerData:${session.id}`);
5768
+ if (typeof header?.conversation === "string") {
5769
+ const conversation = JSON.parse(header.conversation);
5770
+ if (Array.isArray(conversation))
5771
+ for (const [index, raw] of conversation.entries()) {
5772
+ const obj = asObj(raw);
5773
+ const id = asStr(obj?.bubbleId) ?? asStr(obj?.id) ?? `msg:${index}`;
5774
+ if (!out.has(id)) out.set(id, tokenEvidence(raw));
5775
+ }
5776
+ }
5777
+ return out;
5778
+ } finally {
5779
+ db.close();
5780
+ }
5781
+ }
5782
+ async function sourceStamp(root) {
5783
+ const values = await Promise.all(
5784
+ [globalDb(root), `${globalDb(root)}-wal`].map(async (file) => {
5785
+ try {
5786
+ const info = await stat4(file);
5787
+ return `${info.size}:${info.mtimeMs}`;
5788
+ } catch {
5789
+ return "unavailable";
5790
+ }
5791
+ })
5792
+ );
5793
+ return values.join("/");
5794
+ }
5795
+ async function readLocal(root = dataPath(), onProgress) {
5796
+ const stats = emptyScanStats();
5797
+ const out = { sessions: [], complete: true, stats };
5798
+ if (!await hasLocalSource(root)) return out;
5799
+ const before = await sourceStamp(root);
5800
+ let context;
5801
+ try {
5802
+ const reader = await import("cursor-history");
5803
+ const options = {
5804
+ dataPath: root,
5805
+ sqliteDriver: "node:sqlite",
5806
+ onDiagnostic: () => {
5807
+ out.complete = false;
5808
+ },
5809
+ signal: AbortSignal.timeout(12e4)
5810
+ };
5811
+ context = reader.createSessionReadContext(options);
5812
+ const config = { ...options, readContext: context };
5813
+ let offset = 0;
5814
+ while (true) {
5815
+ const page = await reader.listSessionSummaries({
5816
+ ...config,
5817
+ offset,
5818
+ limit: 100
5819
+ });
5820
+ for (const summary of page.data) {
5821
+ stats.filesFound++;
5822
+ if (summary.resolutionState === "ambiguous") {
5823
+ out.complete = false;
5824
+ }
5825
+ try {
5826
+ const session = await reader.getSession(summary.id, config);
5827
+ if (session.resolutionState !== "complete" || session.messages.some((m) => m.metadata?.corrupted))
5828
+ out.complete = false;
5829
+ const tokens = await readTokenEvidence(root, session);
5830
+ out.sessions.push({ session, tokens });
5831
+ stats.filesRead++;
5832
+ onProgress?.(stats.filesRead);
5833
+ } catch {
5834
+ out.complete = false;
5835
+ stats.filesUnreadable++;
5836
+ } finally {
5837
+ context.releaseSession(summary.id);
5838
+ }
5839
+ }
5840
+ if (!page.pagination.hasMore) break;
5841
+ if (page.data.length === 0 || offset >= 1e5) {
5842
+ out.complete = false;
5843
+ break;
5844
+ }
5845
+ offset += page.data.length;
5846
+ }
5847
+ } catch {
5848
+ out.complete = false;
5849
+ stats.filesUnreadable++;
5850
+ } finally {
5851
+ try {
5852
+ await context?.dispose();
5853
+ } catch {
5854
+ out.complete = false;
5855
+ }
5856
+ }
5857
+ if (before !== await sourceStamp(root)) out.complete = false;
5858
+ return out;
5859
+ }
5860
+
5861
+ // src/harness/cursor/account.ts
5862
+ var digest = (value) => createHash("sha256").update(value).digest("hex");
5863
+ async function existingAccount(root) {
5864
+ let db;
5865
+ try {
5866
+ const { DatabaseSync } = await import("node:sqlite");
5867
+ db = new DatabaseSync(globalDb(root), { readOnly: true });
5868
+ const row = db.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
5869
+ if (typeof row?.value !== "string") return null;
5870
+ const token = row.value.trim().replace(/^"|"$/g, "");
5871
+ const claims = asObj(
5872
+ JSON.parse(
5873
+ Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8")
5874
+ )
5875
+ );
5876
+ const subject = asStr(claims?.sub);
5877
+ if (!subject || claims?.type === "api_key_token" || !/^[\w|:-]+$/.test(subject) || !/^[A-Za-z0-9_.-]+$/.test(token))
5878
+ return null;
5879
+ const user = subject.split("|").at(-1);
5880
+ if (!user) return null;
5881
+ return { scope: digest(subject), cookie: `${user}%3A%3A${token}` };
5882
+ } catch {
5883
+ return null;
5884
+ } finally {
5885
+ db?.close();
5886
+ }
5887
+ }
5888
+ function apiContribution(value) {
5889
+ const raw = asObj(value);
5890
+ if (!raw) throw new Error("Invalid Cursor event");
5891
+ const session = asStr(raw.conversationId);
5892
+ if (!session || raw.cloudAgentId || session.startsWith("bc-")) return null;
5893
+ const tsMs = timestamp(raw.timestamp);
5894
+ const usage = raw.tokenUsage == null ? {} : asObj(raw.tokenUsage);
5895
+ if (tsMs === null || !usage) throw new Error("Invalid Cursor event");
5896
+ const buckets = {};
5897
+ for (const [key2, field] of [
5898
+ ["input", "inputTokens"],
5899
+ ["output", "outputTokens"],
5900
+ ["cacheRead", "cacheReadTokens"],
5901
+ ["cacheWrite", "cacheWriteTokens"]
5902
+ ]) {
5903
+ if (usage[field] === void 0 || usage[field] === null) continue;
5904
+ const value2 = count(usage[field]);
5905
+ if (value2 === void 0) throw new Error("Invalid Cursor token bucket");
5906
+ buckets[key2] = value2;
5907
+ }
5908
+ const id = asStr(raw.id) ?? asStr(raw.eventId);
5909
+ return {
5910
+ session,
5911
+ ...id ? { id } : {},
5912
+ tsMs,
5913
+ model: asStr(raw.model) ?? "unknown",
5914
+ buckets,
5915
+ source: "api"
5916
+ };
5917
+ }
5918
+ async function fetchWindow(account, from, to, fetchImpl = fetch) {
5919
+ const out = [];
5920
+ const pages = /* @__PURE__ */ new Set();
5921
+ const eventIds = /* @__PURE__ */ new Set();
5922
+ let total;
5923
+ let retrieved = 0;
5924
+ const pageSize = 100;
5925
+ for (let page = 1; page <= 100; page++) {
5926
+ const response = await fetchImpl(
5927
+ "https://cursor.com/api/dashboard/get-filtered-usage-events",
5928
+ {
5929
+ method: "POST",
5930
+ redirect: "error",
5931
+ signal: AbortSignal.timeout(15e3),
5932
+ headers: {
5933
+ "Content-Type": "application/json",
5934
+ Origin: "https://cursor.com",
5935
+ Cookie: `WorkosCursorSessionToken=${account.cookie}`
5936
+ },
5937
+ body: JSON.stringify({
5938
+ startDate: from,
5939
+ endDate: to - 1,
5940
+ page,
5941
+ pageSize
5942
+ })
5943
+ }
5944
+ );
5945
+ if (!response.ok) throw new Error("Cursor usage unavailable");
5946
+ const payload = asObj(await response.json());
5947
+ const events = payload?.usageEventsDisplay;
5948
+ if (!Array.isArray(events) || events.length > pageSize)
5949
+ throw new Error("Invalid Cursor page");
5950
+ if (payload?.totalUsageEventsCount !== void 0) {
5951
+ const reported = count(payload.totalUsageEventsCount);
5952
+ if (reported === void 0 || !Number.isInteger(reported) || total !== void 0 && total !== reported)
5953
+ throw new Error("Cursor page count changed");
5954
+ total = reported;
5955
+ }
5956
+ if (events.length) {
5957
+ const key2 = digest(JSON.stringify(events));
5958
+ if (pages.has(key2)) throw new Error("Repeated Cursor page");
5959
+ pages.add(key2);
5960
+ }
5961
+ retrieved += events.length;
5962
+ for (const value of events) {
5963
+ const event = apiContribution(value);
5964
+ if (!event) continue;
5965
+ if (event.tsMs < from || event.tsMs >= to)
5966
+ throw new Error("Cursor event outside query");
5967
+ if (event.id && eventIds.has(event.id)) continue;
5968
+ if (event.id) eventIds.add(event.id);
5969
+ out.push(event);
5970
+ }
5971
+ if (total !== void 0 && retrieved > total)
5972
+ throw new Error("Invalid Cursor page count");
5973
+ if (total !== void 0 && retrieved === total || total === void 0 && events.length < pageSize)
5974
+ return out;
5975
+ if (!events.length) throw new Error("Incomplete Cursor pages");
5976
+ }
5977
+ throw new Error("Cursor page limit");
5978
+ }
5979
+
5980
+ // src/harness/cursor/cache.ts
5981
+ var emptyCache = () => ({
5982
+ version: 1,
5983
+ account: null,
5984
+ windows: {},
5985
+ local: [],
5986
+ sessions: []
5987
+ });
5988
+ var cacheFile = (root, store) => path5.join(
5989
+ homedir9(),
5990
+ ".config",
5991
+ "aistack",
5992
+ "cursor",
5993
+ `${digest(`${root}\0${store}`)}.json`
5994
+ );
5995
+ function validContribution(value) {
5996
+ const row = asObj(value);
5997
+ const buckets = asObj(row?.buckets);
5998
+ return !!row && typeof row.session === "string" && typeof row.model === "string" && timestamp(row.tsMs) !== null && (row.id === void 0 || typeof row.id === "string") && (row.source === "api" || row.source === "local") && !!buckets && ["input", "output", "cacheRead", "cacheWrite"].every(
5999
+ (k) => buckets[k] === void 0 || count(buckets[k]) !== void 0
6000
+ );
6001
+ }
6002
+ async function loadCache(file) {
6003
+ try {
6004
+ const raw = asObj(JSON.parse(await readFile(file, "utf8")));
6005
+ const windows = asObj(raw?.windows);
6006
+ if (raw?.version !== 1 || !(raw.account === null || typeof raw.account === "string") || !Array.isArray(raw.sessions) || !raw.sessions.every((id) => typeof id === "string") || !Array.isArray(raw.local) || !raw.local.every(validContribution) || !windows)
6007
+ throw new Error("Invalid cache");
6008
+ for (const value of Object.values(windows)) {
6009
+ const w = asObj(value);
6010
+ if (!w || timestamp(w.from) === null || timestamp(w.to) === null || timestamp(w.fetchedAt) === null || !Array.isArray(w.events) || !w.events.every(validContribution))
6011
+ throw new Error("Invalid window");
6012
+ }
6013
+ return { value: raw, complete: true };
6014
+ } catch (error) {
6015
+ return {
6016
+ value: emptyCache(),
6017
+ complete: error.code === "ENOENT"
6018
+ };
6019
+ }
6020
+ }
6021
+ async function saveCache(file, value) {
6022
+ await mkdir(path5.dirname(file), { recursive: true, mode: 448 });
6023
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
6024
+ await writeFile(temporary, JSON.stringify(value), { mode: 384 });
6025
+ await rename(temporary, file);
6026
+ }
6027
+ var WINDOW_MS = 30 * 864e5;
6028
+ async function refreshAccount(input) {
6029
+ const { cache, account, now, sessionIds } = input;
6030
+ if (!account) return cache;
6031
+ const changed = cache.account !== account.scope;
6032
+ const next = {
6033
+ ...cache,
6034
+ account: account.scope,
6035
+ windows: changed ? {} : { ...cache.windows }
6036
+ };
6037
+ for (let from = Math.floor(input.sinceMs / WINDOW_MS) * WINDOW_MS; from <= now; from += WINDOW_MS) {
6038
+ const to = Math.min(from + WINDOW_MS, now + 1);
6039
+ const key2 = String(from);
6040
+ const previous = next.windows[key2];
6041
+ if (previous && now - previous.fetchedAt < 3e5) continue;
6042
+ try {
6043
+ const events = (await fetchWindow(account, from, to, input.fetchImpl)).filter((e) => sessionIds.has(e.session));
6044
+ if (previous?.events.length && events.length === 0 && to <= now) continue;
6045
+ next.windows[key2] = { from, to, fetchedAt: now, events };
6046
+ } catch {
6047
+ break;
6048
+ }
6049
+ }
6050
+ return next;
6051
+ }
6052
+ function cachedEvents(cache, sessionIds) {
6053
+ const seen = /* @__PURE__ */ new Set();
6054
+ return Object.values(cache.windows).sort((a, b) => b.fetchedAt - a.fetchedAt || b.from - a.from).flatMap((w) => w.events).filter((e) => {
6055
+ if (!sessionIds.has(e.session) || e.id && seen.has(e.id)) return false;
6056
+ if (e.id) seen.add(e.id);
6057
+ return true;
6058
+ });
6059
+ }
6060
+
6061
+ // src/harness/cursor/scan.ts
6062
+ import path7 from "node:path";
6063
+
6064
+ // src/harness/cursor/workflow.ts
6065
+ import path6 from "node:path";
6066
+ var TOOLS = {
6067
+ read_file: "Read",
6068
+ read_file_v2: "Read",
6069
+ list_dir: "Glob",
6070
+ glob_file_search: "Glob",
6071
+ grep: "Grep",
6072
+ search: "Grep",
6073
+ codebase_search: "Grep",
6074
+ edit_file: "Edit",
6075
+ edit_file_v2: "Edit",
6076
+ search_replace: "Edit",
6077
+ write: "Write",
6078
+ write_file: "Write",
6079
+ delete_file: "Edit",
6080
+ run_terminal_cmd: "Bash",
6081
+ run_terminal_command: "Bash",
6082
+ execute_command: "Bash",
6083
+ web_search: "WebSearch",
6084
+ web_fetch: "WebFetch",
6085
+ ask_question: "ask_question",
6086
+ todo_write: "TodoWrite",
6087
+ task: "Task",
6088
+ skill: "Skill"
6089
+ };
6090
+ var CURSOR_BUILTIN_TOOLS = new Set(
6091
+ Object.keys(TOOLS)
6092
+ );
6093
+ function projectHistory(options) {
6094
+ const { locals, api, usage, aggregate, workflow, sinceMs, now } = options;
6095
+ const nativeOwners = /* @__PURE__ */ new Map();
6096
+ for (const { session } of locals)
6097
+ for (const message of session.messages)
6098
+ if (message.id && message.identityOrigin === "composer-native" && !message.isSidechain && !nativeOwners.has(message.id))
6099
+ nativeOwners.set(message.id, session.id);
6100
+ const seenMessages = /* @__PURE__ */ new Set();
6101
+ const seenTools = /* @__PURE__ */ new Set();
6102
+ const configuredServers = /* @__PURE__ */ new Map();
6103
+ const scopes = /* @__PURE__ */ new Map();
6104
+ const inWindow = (t) => t !== null && t >= sinceMs && t <= now;
6105
+ for (const { session } of locals) {
6106
+ const projectWorkspace = session.canonicalWorkspacePath && path6.isAbsolute(session.canonicalWorkspacePath) ? session.canonicalWorkspacePath : void 0;
6107
+ const times = messageTimes(session, api);
6108
+ const scopeOf = (message) => {
6109
+ const parent = message.parentMessageId && nativeOwners.get(message.parentMessageId);
6110
+ return message.isSidechain ? {
6111
+ session: `${session.id}:sidechain`,
6112
+ sidechain: true,
6113
+ ...parent ? { parentSession: parent } : {}
6114
+ } : { session: session.id };
6115
+ };
6116
+ for (const [index, message] of session.messages.entries()) {
6117
+ const identity = message.identityOrigin === "composer-native" && message.id ? `native:${message.id}` : `${session.id}:${message.id ?? index}`;
6118
+ if (seenMessages.has(identity)) continue;
6119
+ seenMessages.add(identity);
6120
+ const tsMs = times[index];
6121
+ const scope = scopeOf(message);
6122
+ if (message.id) scopes.set(`${session.id}:${message.id}`, scope);
6123
+ if (tsMs !== null && !inWindow(tsMs)) continue;
6124
+ const common = { ...scope, projectWorkspace, tsMs: tsMs ?? 0 };
6125
+ if (message.role === "user") {
6126
+ if (inWindow(tsMs))
6127
+ workflow?.ingest({
6128
+ ...common,
6129
+ type: "response",
6130
+ responseId: `anchor:${message.id ?? index}`
6131
+ });
6132
+ continue;
6133
+ }
6134
+ let questionBack = false;
6135
+ for (const [toolIndex, call] of (message.toolCalls ?? []).entries()) {
6136
+ const id = call.identityOrigin === "source-native" && call.id ? `native:${call.id}` : `${identity}:${call.id ?? toolIndex}`;
6137
+ if (seenTools.has(id)) continue;
6138
+ seenTools.add(id);
6139
+ bump(aggregate.toolCalls, call.name);
6140
+ const tool = TOOLS[call.name] ?? call.name;
6141
+ const arg = asStr(call.params?.command) ?? asStr(call.params?.cmd) ?? asStr(call.params?.skill) ?? asStr(call.params?.subagent_type) ?? "";
6142
+ if (tool === "WebSearch") aggregate.webSearchRequests++;
6143
+ if (tool === "WebFetch") aggregate.webFetchRequests++;
6144
+ if (tool === "Skill" && arg) bump(aggregate.skillCalls, arg);
6145
+ if (tool === "Task") bump(aggregate.subagentCalls, arg || "(unknown)");
6146
+ const mcp = /^mcp__(.+?)__(.+)$/.exec(call.name);
6147
+ let server = mcp?.[1];
6148
+ if (!server && call.name.startsWith("mcp_")) {
6149
+ const directory = projectWorkspace ?? process.cwd();
6150
+ let names = configuredServers.get(directory);
6151
+ if (!names) {
6152
+ names = detectMcpServers(directory).filter((r) => r.group === "cursor").map((r) => r.name).sort((a, b) => b.length - a.length);
6153
+ configuredServers.set(directory, names);
6154
+ }
6155
+ server = names.find((name) => call.name.startsWith(`mcp_${name}_`));
6156
+ }
6157
+ if (server) {
6158
+ bump(aggregate.mcpServerCalls, server);
6159
+ bump(aggregate.mcpToolCalls, call.name);
6160
+ }
6161
+ questionBack ||= tool === "ask_question";
6162
+ if (inWindow(tsMs))
6163
+ workflow?.ingest({
6164
+ ...common,
6165
+ type: "event",
6166
+ tool,
6167
+ arg,
6168
+ batchId: id
6169
+ });
6170
+ }
6171
+ if (inWindow(tsMs))
6172
+ workflow?.ingest({
6173
+ ...common,
6174
+ type: "turn",
6175
+ turnId: message.id ?? `assistant:${index}`,
6176
+ questionBack
6177
+ });
6178
+ }
6179
+ }
6180
+ if (!workflow) return;
6181
+ const projects = new Map(
6182
+ locals.map(({ session }) => [session.id, session.canonicalWorkspacePath])
6183
+ );
6184
+ for (const [index, row] of usage.entries()) {
6185
+ if (!inWindow(row.tsMs)) continue;
6186
+ const project = projects.get(row.session);
6187
+ const scope = row.id && row.source === "local" ? scopes.get(`${row.session}:${row.id}`) : void 0;
6188
+ workflow.ingest({
6189
+ ...scope ?? {
6190
+ session: row.sidechain ? `${row.session}:sidechain` : row.session,
6191
+ sidechain: row.sidechain
6192
+ },
6193
+ ...project && path6.isAbsolute(project) ? { projectWorkspace: project } : {},
6194
+ type: "response",
6195
+ tsMs: row.tsMs,
6196
+ responseId: `${row.source}:${row.id ?? index}`,
6197
+ model: normalizeModel(row.model),
6198
+ routingTokens: (row.buckets.input ?? 0) + (row.buckets.output ?? 0) + (row.buckets.cacheRead ?? 0) + (row.buckets.cacheWrite ?? 0)
6199
+ });
6200
+ }
6201
+ }
6202
+
6203
+ // src/harness/cursor/scan.ts
6204
+ async function scan3(options) {
6205
+ const root = options.root ?? dataPath();
6206
+ const now = options.now ?? Date.now();
6207
+ const file = options.cachePath ?? cacheFile(root, storeRoot());
6208
+ const local = await (options.readLocalImpl ?? readLocal)(
6209
+ root,
6210
+ options.onProgress
6211
+ );
6212
+ const loaded = await loadCache(file);
6213
+ let complete = local.complete && loaded.complete;
6214
+ const ids = new Set(local.sessions.map((s) => s.session.id));
6215
+ if ([
6216
+ ...loaded.value.local,
6217
+ ...Object.values(loaded.value.windows).flatMap((w) => w.events)
6218
+ ].some((row) => row.tsMs >= options.sinceMs && !ids.has(row.session)))
6219
+ complete = false;
6220
+ const account = await (options.accountImpl ?? existingAccount)(root);
6221
+ const cache = await refreshAccount({
6222
+ cache: loaded.value,
6223
+ account,
6224
+ sinceMs: options.sinceMs,
6225
+ now,
6226
+ sessionIds: ids,
6227
+ fetchImpl: options.fetchImpl
6228
+ });
6229
+ const api = cachedEvents(cache, ids);
6230
+ const native = /* @__PURE__ */ new Set();
6231
+ const contributions = local.sessions.flatMap((s) => localContributions(s, api)).filter((row) => {
6232
+ if (!row.nativeId) return true;
6233
+ if (native.has(row.nativeId)) return false;
6234
+ native.add(row.nativeId);
6235
+ return true;
6236
+ });
6237
+ const dated = new Set(contributions.map((row) => row.session));
6238
+ if (loaded.value.local.some(
6239
+ (row) => row.tsMs >= options.sinceMs && !dated.has(row.session)
6240
+ ))
6241
+ complete = false;
6242
+ const previousDates = /* @__PURE__ */ new Map();
6243
+ for (const row of [
6244
+ ...loaded.value.local,
6245
+ ...cachedEvents(loaded.value, ids)
6246
+ ]) {
6247
+ const dates = previousDates.get(row.session) ?? /* @__PURE__ */ new Set();
6248
+ dates.add(utcDateOf(row.tsMs));
6249
+ previousDates.set(row.session, dates);
6250
+ }
6251
+ if (complete) {
6252
+ cache.local = contributions;
6253
+ cache.sessions = [...ids].sort();
6254
+ try {
6255
+ await saveCache(file, cache);
6256
+ } catch {
6257
+ complete = false;
6258
+ }
6259
+ }
6260
+ const aggregate = createAggregate();
6261
+ const workflowLocal = createWorkflowLocalSources();
6262
+ const workflow = createHarnessWorkflowReducer("cursor", workflowLocal);
6263
+ const sessions = new Map(local.sessions.map((s) => [s.session.id, s]));
6264
+ const sessionDates = previousDates;
6265
+ for (const { session } of local.sessions) {
6266
+ for (const message of session.messages)
6267
+ if (message.model) {
6268
+ const model = normalizeModel(message.model);
6269
+ if (!aggregate.byModel.has(model))
6270
+ aggregate.byModel.set(model, emptyUsage());
6271
+ }
6272
+ const project = session.canonicalWorkspacePath;
6273
+ if (project && path7.isAbsolute(project)) aggregate.projectDirs.add(project);
6274
+ if (session.metadata?.cursorVersion)
6275
+ aggregate.ccVersions.add(session.metadata.cursorVersion);
6276
+ const times = messageTimes(session, api).filter(
6277
+ (t) => t !== null && t >= options.sinceMs && t <= now
6278
+ );
6279
+ if (times.length) {
6280
+ aggregate.sessions.add(session.id);
6281
+ noteSessionStart(aggregate, session.id, Math.min(...times));
6282
+ }
6283
+ }
6284
+ const usage = reconcile(contributions, api);
6285
+ projectHistory({
6286
+ locals: local.sessions,
6287
+ api,
6288
+ usage,
6289
+ aggregate,
6290
+ workflow: options.publishWorkflow === false ? void 0 : workflow,
6291
+ sinceMs: options.sinceMs,
6292
+ now
6293
+ });
6294
+ for (const contribution of usage) {
6295
+ const { tsMs, buckets, session, sidechain } = contribution;
6296
+ const dates = sessionDates.get(session) ?? /* @__PURE__ */ new Set();
6297
+ dates.add(utcDateOf(tsMs));
6298
+ sessionDates.set(session, dates);
6299
+ if (tsMs < options.sinceMs || tsMs > now) continue;
6300
+ const model = normalizeModel(contribution.model);
6301
+ const counts2 = {
6302
+ input: buckets.input ?? 0,
6303
+ output: buckets.output ?? 0,
6304
+ cacheRead: buckets.cacheRead ?? 0,
6305
+ cacheWrite5m: 0,
6306
+ cacheWrite1h: 0,
6307
+ cacheWriteUnsplit: buckets.cacheWrite ?? 0
6308
+ };
6309
+ addModelUsage(
6310
+ aggregate,
6311
+ model,
6312
+ counts2,
6313
+ apiEquivalentCost(model, counts2, tsMs),
6314
+ 1,
6315
+ { tsMs, sidechain }
6316
+ );
6317
+ aggregate.records++;
6318
+ aggregate.assistantRecords++;
6319
+ aggregate.distinctResponses++;
6320
+ aggregate.sessions.add(session);
6321
+ aggregate.activeDays.add(utcDateOf(tsMs));
6322
+ aggregate.firstTs = Math.min(aggregate.firstTs ?? tsMs, tsMs);
6323
+ aggregate.lastTs = Math.max(aggregate.lastTs ?? tsMs, tsMs);
6324
+ if (sidechain) aggregate.sidechainTokens += countsTotal(counts2);
6325
+ else aggregate.mainTokens += countsTotal(counts2);
6326
+ noteSessionStart(aggregate, session, tsMs);
6327
+ const project = sessions.get(session)?.session.canonicalWorkspacePath;
6328
+ if (project && path7.isAbsolute(project))
6329
+ noteProjectDay(aggregate, project, tsMs);
6330
+ }
6331
+ aggregate.files = local.stats.filesRead;
6332
+ return {
6333
+ aggregate,
6334
+ stats: local.stats,
6335
+ workflow: workflow.finish(),
6336
+ workflowLocal,
6337
+ scanComplete: complete,
6338
+ sessionDates
6339
+ };
6340
+ }
6341
+
6342
+ // src/harness/cursor/adapter.ts
6343
+ var CURSOR_HARNESS_NAME = "cursor";
6344
+ var cursorAdapter = {
6345
+ name: CURSOR_HARNESS_NAME,
6346
+ builtinTools: CURSOR_BUILTIN_TOOLS,
6347
+ async detect(options) {
6348
+ const roots = options.roots ?? [dataPath()];
6349
+ for (const root of roots) {
6350
+ const held = await loadCache(cacheFile(root, storeRoot()));
6351
+ if (!held.complete || [
6352
+ ...held.value.local,
6353
+ ...cachedEvents(held.value, new Set(held.value.sessions))
6354
+ ].some((row) => row.tsMs >= options.sinceMs))
6355
+ return true;
6356
+ const local = await readLocal(root);
6357
+ if (!local.complete || local.sessions.some(
6358
+ ({ session }) => messageTimes(session).some((t) => t !== null && t >= options.sinceMs)
6359
+ ))
6360
+ return true;
6361
+ if (local.sessions.some(
6362
+ ({ session }) => session.messages.length > 0 && messageTimes(session).every((t) => t === null)
6363
+ ))
6364
+ return true;
6365
+ }
6366
+ return false;
6367
+ },
6368
+ scan: scan3
6369
+ };
6370
+
6371
+ // src/harness/grok/analyzer.ts
6372
+ function createAggregate4() {
6373
+ const workflowLocal = createWorkflowLocalSources();
6374
+ return Object.assign(createAggregate(), {
6375
+ workflow: createHarnessWorkflowReducer("grok-build", workflowLocal),
6376
+ workflowLocal
6377
+ });
6378
+ }
6379
+ var createGrokEventState = (parentSession) => ({
6380
+ tools: /* @__PURE__ */ new Map(),
6381
+ completedTools: /* @__PURE__ */ new Set(),
6382
+ ...parentSession ? { parentSession } : {}
6383
+ });
6384
+ var bump3 = (map, key2) => {
6385
+ map.set(key2, (map.get(key2) ?? 0) + 1);
6386
+ };
6387
+ var toolMetadata = (update2) => {
6388
+ const meta = asObj(update2._meta);
6389
+ return meta && asObj(meta["x.ai/tool"]);
6390
+ };
6391
+ function ingestUpdate(agg, state, value, projectDir, sinceMs) {
6392
+ const root = asObj(value);
6393
+ const params = root && asObj(root.params);
6394
+ const update2 = params && asObj(params.update);
6395
+ const session = params && asStr(params.sessionId);
6396
+ const tsMs = timestampMs(
6397
+ asObj(params?._meta)?.agentTimestampMs ?? root?.timestamp
6398
+ );
6399
+ if (!update2 || !session || tsMs === null) return;
6400
+ const kind = asStr(update2.sessionUpdate);
6401
+ if (kind === "tool_call") {
6402
+ const id = asStr(update2.toolCallId);
6403
+ const metadata = toolMetadata(update2);
6404
+ const name = asName(metadata?.name ?? update2.toolName);
6405
+ if (!id || !name || state.tools.has(id)) return;
6406
+ const raw = asObj(update2.input);
6407
+ const arg = asStr(raw?.command ?? raw?.query ?? raw?.skill ?? raw?.name);
6408
+ state.tools.set(id, { name, ...arg ? { arg } : {}, tsMs });
6409
+ return;
6410
+ }
6411
+ if (sinceMs !== void 0 && tsMs < sinceMs) return;
6412
+ if (kind === "tool_call_update") {
6413
+ const id = asStr(update2.toolCallId);
6414
+ if (!id || asStr(update2.status) !== "completed") return;
6415
+ completeTool(agg, state, id, session, projectDir, tsMs);
6416
+ return;
6417
+ }
6418
+ if (kind !== "turn_completed") return;
6419
+ const usage = asObj(update2.usage);
6420
+ if (!usage) return;
6421
+ const prompt = asStr(update2.prompt_id) ?? `turn:${tsMs}`;
6422
+ for (const [model, raw] of Object.entries(asObj(usage.modelUsage) ?? {})) {
6423
+ const row = asObj(raw);
6424
+ agg.workflow.ingest({
6425
+ type: "response",
6426
+ session,
6427
+ projectWorkspace: projectDir,
6428
+ parentSession: state.parentSession,
6429
+ tsMs,
6430
+ responseId: `${prompt}:${model}`,
6431
+ model,
6432
+ thinkingTokens: asNum(row?.reasoningTokens),
6433
+ responseTokens: asNum(row?.outputTokens),
6434
+ routingTokens: asNum(row?.outputTokens),
6435
+ ...asNum(row?.apiDurationMs) > 0 ? { durationSec: asNum(row?.apiDurationMs) / 1e3 } : asNum(update2.elapsed_ms) > 0 ? { durationSec: asNum(update2.elapsed_ms) / 1e3 } : {}
6436
+ });
6437
+ }
6438
+ agg.workflow.ingest({
6439
+ type: "turn",
6440
+ session,
6441
+ projectWorkspace: projectDir,
6442
+ parentSession: state.parentSession,
6443
+ tsMs,
6444
+ turnId: prompt,
6445
+ questionBack: asStr(update2.stop_reason) === "question"
6446
+ });
6447
+ }
6448
+ function completeTool(agg, state, id, session, projectDir, tsMs) {
6449
+ if (state.completedTools.has(id)) return;
6450
+ const tool = state.tools.get(id);
6451
+ if (!tool) return;
6452
+ state.completedTools.add(id);
6453
+ bump3(agg.toolCalls, tool.name);
6454
+ if (["web_search", "websearch", "search_web"].includes(tool.name))
6455
+ agg.webSearchRequests++;
6456
+ if (["skill", "use_skill"].includes(tool.name) && tool.arg)
6457
+ bump3(agg.skillCalls, tool.arg);
6458
+ const mcp = /^(?:mcp__|mcp:)([^_:]+)[_:](.+)$/.exec(tool.name);
6459
+ if (mcp) {
6460
+ bump3(agg.mcpServerCalls, mcp[1]);
6461
+ bump3(agg.mcpToolCalls, tool.name);
6462
+ } else if (tool.name === "use_tool" && tool.arg?.includes("__")) {
6463
+ const [server] = tool.arg.split("__", 1);
6464
+ if (server) {
6465
+ bump3(agg.mcpServerCalls, server);
6466
+ bump3(agg.mcpToolCalls, tool.arg);
6467
+ }
6468
+ }
6469
+ agg.workflow.ingest({
6470
+ type: "event",
6471
+ session,
6472
+ projectWorkspace: projectDir,
6473
+ parentSession: state.parentSession,
6474
+ tsMs: tool.tsMs || tsMs,
6475
+ tool: tool.name,
6476
+ ...tool.arg ? { arg: tool.arg } : {},
6477
+ batchId: id
6478
+ });
6479
+ }
6480
+ function ingestEvent2(agg, state, value, sessionFallback, projectDir, sinceMs) {
6481
+ const row = asObj(value);
6482
+ if (!row) return;
6483
+ const session = asStr(row.session_id) ?? sessionFallback;
6484
+ const tsMs = timestampMs(row.ts);
6485
+ if (!session || tsMs === null) return;
6486
+ if (asStr(row.type) === "tool_started") {
6487
+ const id = asStr(row.tool_call_id);
6488
+ const name = asName(row.tool_name);
6489
+ if (id && name && !state.tools.has(id)) state.tools.set(id, { name, tsMs });
6490
+ } else if (sinceMs !== void 0 && tsMs < sinceMs) {
6491
+ return;
6492
+ } else if (asStr(row.type) === "tool_completed") {
6493
+ const id = asStr(row.tool_call_id);
6494
+ if (id) completeTool(agg, state, id, session, projectDir, tsMs);
6495
+ } else if (asStr(row.type) === "compaction") {
6496
+ agg.workflow.ingest({
6497
+ type: "compaction",
6498
+ session,
6499
+ projectWorkspace: projectDir,
6500
+ parentSession: state.parentSession,
6501
+ tsMs
6502
+ });
6503
+ }
6504
+ }
6505
+ var timestampMs = (value) => {
6506
+ if (typeof value === "string") {
6507
+ const parsed = Date.parse(value);
6508
+ return Number.isFinite(parsed) ? parsed : null;
6509
+ }
6510
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
6511
+ return value < 1e10 ? value * 1e3 : value;
6512
+ };
6513
+ function counts(value) {
6514
+ const row = asObj(value);
6515
+ if (!row) return null;
6516
+ const totalInput = asNum(row.inputTokens);
6517
+ const cacheRead = asNum(row.cachedReadTokens);
6518
+ const cacheWrite = asNum(row.cacheCreationTokens);
6519
+ if (totalInput < cacheRead + cacheWrite) return null;
6520
+ const result = {
6521
+ input: totalInput - cacheRead - cacheWrite,
6522
+ output: asNum(row.outputTokens),
6523
+ cacheWrite5m: 0,
6524
+ cacheWrite1h: 0,
6525
+ cacheWriteUnsplit: cacheWrite,
6526
+ cacheRead
6527
+ };
6528
+ return countsTotal(result) > 0 ? result : null;
6529
+ }
6530
+ function sidecarContributions(value, projectDir) {
6531
+ const root = asObj(value);
6532
+ const sessionId = root && asStr(root.sessionId);
6533
+ if (!root || !sessionId) return [];
6534
+ const turns = Array.isArray(root.turns) ? root.turns : [];
6535
+ const out = [];
6536
+ for (const raw of turns) {
6537
+ const turn = asObj(raw);
6538
+ const tsMs = turn && timestampMs(turn.endedAt);
6539
+ if (!turn || tsMs === null) continue;
6540
+ const models = [];
6541
+ const perModel = asObj(turn.modelUsage);
6542
+ for (const [model, usage] of Object.entries(perModel ?? {})) {
6543
+ const c = counts(usage);
6544
+ if (c) models.push({ model, counts: c });
6545
+ }
6546
+ if (models.length === 0) {
6547
+ const c = counts(turn);
6548
+ const model = asStr(turn.primaryModelId);
6549
+ if (c && model) models.push({ model, counts: c });
6550
+ }
6551
+ if (models.length > 0)
6552
+ out.push({
6553
+ sessionId,
6554
+ projectDir,
6555
+ tsMs,
6556
+ ...asNum(turn.apiDurationMs) > 0 ? { durationMs: asNum(turn.apiDurationMs) } : {},
6557
+ models
6558
+ });
6559
+ }
6560
+ return out;
6561
+ }
6562
+ function terminalContribution(value, projectDir) {
6563
+ const root = asObj(value);
6564
+ const params = root && asObj(root.params);
6565
+ const update2 = params && asObj(params.update);
6566
+ const meta = params && asObj(params._meta);
6567
+ if (!update2 || asStr(update2.sessionUpdate) !== "turn_completed") return null;
6568
+ const usage = asObj(update2.usage);
6569
+ const sessionId = params && asStr(params.sessionId);
6570
+ const tsMs = timestampMs(meta?.agentTimestampMs ?? root?.timestamp);
6571
+ if (!usage || !sessionId || tsMs === null) return null;
6572
+ const models = [];
6573
+ for (const [model, row] of Object.entries(asObj(usage.modelUsage) ?? {})) {
6574
+ const c = counts(row);
6575
+ if (c) models.push({ model, counts: c });
6576
+ }
6577
+ return models.length === 0 ? null : {
6578
+ sessionId,
6579
+ projectDir,
6580
+ tsMs,
6581
+ ...asNum(update2.elapsed_ms) > 0 ? { durationMs: asNum(update2.elapsed_ms) } : {},
6582
+ models
6583
+ };
6584
+ }
6585
+ function ingestContribution(agg, row) {
6586
+ agg.records++;
6587
+ agg.assistantRecords++;
6588
+ agg.distinctResponses++;
6589
+ agg.sessions.add(row.sessionId);
6590
+ agg.activeDays.add(new Date(row.tsMs).toISOString().slice(0, 10));
6591
+ agg.projectDirs.add(row.projectDir);
6592
+ agg.firstTs = agg.firstTs === null ? row.tsMs : Math.min(agg.firstTs, row.tsMs);
6593
+ agg.lastTs = agg.lastTs === null ? row.tsMs : Math.max(agg.lastTs, row.tsMs);
6594
+ noteSessionStart(agg, row.sessionId, row.tsMs);
6595
+ noteProjectDay(agg, row.projectDir, row.tsMs);
6596
+ for (const { model, counts: tokenCounts } of row.models) {
6597
+ const key2 = normalizeModel(model);
6598
+ addModelUsage(
6599
+ agg,
6600
+ key2,
6601
+ tokenCounts,
6602
+ apiEquivalentCost(key2, tokenCounts, row.tsMs),
6603
+ 1,
6604
+ {
6605
+ tsMs: row.tsMs
6606
+ }
6607
+ );
6608
+ }
6609
+ }
6610
+
6611
+ // src/harness/grok/scan.ts
6612
+ import { createReadStream as createReadStream2 } from "node:fs";
6613
+ import { readdir as readdir4, readFile as readFile2, stat as stat5 } from "node:fs/promises";
6614
+ import { homedir as homedir10 } from "node:os";
6615
+ import path8 from "node:path";
6616
+ import readline2 from "node:readline";
6617
+ import { parse as parseToml3 } from "smol-toml";
6618
+ function sessionRoots() {
6619
+ return [
6620
+ path8.join(
6621
+ process.env.GROK_HOME || path8.join(homedir10(), ".grok"),
6622
+ "sessions"
6623
+ )
6624
+ ];
6625
+ }
6626
+ var isGrokEvidenceFile = (name) => name === "usage.json" || name === "updates.jsonl" || name === "events.jsonl";
6627
+ var delays = [0, 100, 300];
6628
+ var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6629
+ async function stableRead(file, jsonl) {
6630
+ for (const delay of delays) {
6631
+ if (delay) await pause(delay);
6632
+ try {
6633
+ const before = await stat5(file);
6634
+ if (!before.isFile()) return { complete: false };
6635
+ if (jsonl) {
6636
+ const lines2 = [];
6637
+ const input = readline2.createInterface({
6638
+ input: createReadStream2(file),
6639
+ crlfDelay: Infinity
6640
+ });
6641
+ for await (const line of input) {
6642
+ if (!line.trim()) continue;
6643
+ try {
6644
+ lines2.push(JSON.parse(line));
6645
+ } catch {
6646
+ lines2.push(null);
6647
+ }
6648
+ }
6649
+ const after = await stat5(file);
6650
+ if (before.size === after.size && before.mtimeMs === after.mtimeMs)
6651
+ return { lines: lines2, complete: true };
6652
+ } else {
6653
+ const raw = await readFile2(file, "utf8");
6654
+ const after = await stat5(file);
6655
+ if (before.size === after.size && before.mtimeMs === after.mtimeMs)
6656
+ return { json: JSON.parse(raw), complete: true };
6657
+ }
6658
+ } catch {
6659
+ }
6660
+ }
6661
+ return { complete: false };
6662
+ }
6663
+ async function directories(root) {
6664
+ try {
6665
+ const workspaces = await readdir4(root, { withFileTypes: true });
6666
+ const out = [];
6667
+ for (const workspace of workspaces) {
6668
+ if (!workspace.isDirectory() || workspace.isSymbolicLink()) continue;
6669
+ const workspacePath = path8.join(root, workspace.name);
6670
+ for (const session of await readdir4(workspacePath, {
6671
+ withFileTypes: true
6672
+ })) {
6673
+ if (session.isDirectory() && !session.isSymbolicLink())
6674
+ out.push(path8.join(workspacePath, session.name));
6675
+ }
6676
+ }
6677
+ return out.sort();
6678
+ } catch (error) {
6679
+ return error.code === "ENOENT" ? [] : null;
6680
+ }
6681
+ }
6682
+ async function modelAliasesForRoot(root) {
6683
+ if (process.env.GROK_MODELS_BASE_URL) return /* @__PURE__ */ new Map();
6684
+ try {
6685
+ const parsed = asObj(
6686
+ parseToml3(await readFile2(path8.join(root, "..", "config.toml"), "utf8"))
6687
+ );
6688
+ if (!parsed || asObj(parsed.endpoints)?.models_base_url) return /* @__PURE__ */ new Map();
6689
+ const models = asObj(parsed.model);
6690
+ const aliases = /* @__PURE__ */ new Map();
6691
+ for (const [alias, raw] of Object.entries(models ?? {})) {
6692
+ const entry = asObj(raw);
6693
+ const model = entry && asStr(entry.model);
6694
+ if (!model || entry?.base_url || entry?.model_provider) continue;
6695
+ aliases.set(alias, model);
6696
+ }
6697
+ return aliases;
6698
+ } catch {
6699
+ return /* @__PURE__ */ new Map();
6700
+ }
6701
+ }
6702
+ async function scan4(agg, opts = {}) {
6703
+ const stats = emptyScanStats();
6704
+ const sessionDates = /* @__PURE__ */ new Map();
6705
+ const candidates = /* @__PURE__ */ new Map();
6706
+ let complete = true;
6707
+ for (const root of opts.roots ?? sessionRoots()) {
6708
+ const aliases = await modelAliasesForRoot(root);
6709
+ const dirs = await directories(root);
6710
+ if (dirs === null) {
6711
+ complete = false;
6712
+ continue;
6713
+ }
6714
+ for (const dir of dirs) {
6715
+ let entries;
6716
+ try {
6717
+ entries = await readdir4(dir, { withFileTypes: true });
6718
+ } catch {
6719
+ complete = false;
6720
+ continue;
6721
+ }
6722
+ const files = new Map(
6723
+ entries.filter(
6724
+ (e) => e.isFile() && (isGrokEvidenceFile(e.name) || e.name === "summary.json")
6725
+ ).map((e) => [e.name, path8.join(dir, e.name)])
6726
+ );
6727
+ let projectDir = dir;
6728
+ let sessionFallback = path8.basename(dir);
6729
+ let child = false;
6730
+ let parentSession;
6731
+ const summary = files.get("summary.json");
6732
+ if (summary) {
6733
+ const read3 = await stableRead(summary, false);
6734
+ if (!read3.complete) {
6735
+ complete = false;
6736
+ stats.filesUnreadable++;
6737
+ continue;
6738
+ }
6739
+ const value = asObj(read3.json);
6740
+ const info = value && asObj(value.info);
6741
+ projectDir = asStr(info?.cwd) ?? asStr(value?.cwd) ?? dir;
6742
+ sessionFallback = asStr(value?.sessionId) ?? sessionFallback;
6743
+ parentSession = asStr(value?.parentSessionId) ?? asStr(value?.parent_session_id) ?? asStr(info?.parentSessionId) ?? asStr(info?.parent_session_id) ?? void 0;
6744
+ child = parentSession !== void 0;
6745
+ }
6746
+ let rows = [];
6747
+ let precedence = 0;
6748
+ const usage = files.get("usage.json");
6749
+ if (usage) {
6750
+ stats.filesFound++;
6751
+ const read3 = await stableRead(usage, false);
6752
+ if (!read3.complete) {
6753
+ complete = false;
6754
+ stats.filesUnreadable++;
6755
+ continue;
6756
+ }
6757
+ stats.filesRead++;
6758
+ rows = sidecarContributions(read3.json, projectDir);
6759
+ if (rows.length > 0) precedence = 2;
6760
+ }
6761
+ if (child) {
6762
+ rows = [];
6763
+ precedence = 0;
6764
+ }
6765
+ const eventState = createGrokEventState(parentSession);
6766
+ const updates = files.get("updates.jsonl");
6767
+ if (updates) {
6768
+ const useTerminalUsage = rows.length === 0 && !child;
6769
+ stats.filesFound++;
6770
+ const read3 = await stableRead(updates, true);
6771
+ if (!read3.complete) {
6772
+ complete = false;
6773
+ stats.filesUnreadable++;
6774
+ continue;
6775
+ }
6776
+ stats.filesRead++;
6777
+ for (const value of read3.lines ?? []) {
6778
+ if (value === null) {
6779
+ agg.parseErrors++;
6780
+ continue;
6781
+ }
6782
+ ingestUpdate(agg, eventState, value, projectDir, opts.sinceMs);
6783
+ if (useTerminalUsage) {
6784
+ const row = terminalContribution(value, projectDir);
6785
+ if (row) rows.push(row);
6786
+ }
6787
+ }
6788
+ if (rows.length > 0) precedence = 1;
6789
+ }
6790
+ const events = files.get("events.jsonl");
6791
+ if (events) {
6792
+ stats.filesFound++;
6793
+ const read3 = await stableRead(events, true);
6794
+ if (!read3.complete) {
6795
+ complete = false;
6796
+ stats.filesUnreadable++;
6797
+ continue;
6798
+ }
6799
+ stats.filesRead++;
6800
+ for (const value of read3.lines ?? []) {
6801
+ if (value === null) agg.parseErrors++;
6802
+ else
6803
+ ingestEvent2(
6804
+ agg,
6805
+ eventState,
6806
+ value,
6807
+ sessionFallback,
6808
+ projectDir,
6809
+ opts.sinceMs
6810
+ );
6811
+ }
6812
+ }
6813
+ rows = rows.filter(
6814
+ (row) => opts.sinceMs === void 0 || row.tsMs >= opts.sinceMs
6815
+ );
6816
+ rows = rows.map((row) => ({
6817
+ ...row,
6818
+ models: row.models.map(({ model, counts: counts2 }) => ({
6819
+ model: aliases.get(model) ?? model,
6820
+ counts: counts2
6821
+ }))
6822
+ }));
6823
+ if (rows.length > 0) {
6824
+ const sessionId = rows[0]?.sessionId;
6825
+ const held = candidates.get(sessionId);
6826
+ const total = (values) => values.flatMap((value) => value.models).reduce((sum, model) => sum + countsTotal(model.counts), 0);
6827
+ if (!held || precedence > held.precedence || precedence === held.precedence && total(rows) > total(held.rows))
6828
+ candidates.set(sessionId, { precedence, rows });
6829
+ }
6830
+ opts.onProgress?.(stats.filesFound);
6831
+ }
6832
+ }
6833
+ for (const { rows } of candidates.values()) {
6834
+ for (const row of rows) {
6835
+ ingestContribution(agg, row);
6836
+ const dates = sessionDates.get(row.sessionId) ?? /* @__PURE__ */ new Set();
6837
+ dates.add(new Date(row.tsMs).toISOString().slice(0, 10));
6838
+ sessionDates.set(row.sessionId, dates);
6839
+ }
6840
+ }
6841
+ return { stats, complete, sessionDates };
6842
+ }
6843
+
6844
+ // src/harness/grok/adapter.ts
6845
+ var GROK_HARNESS_NAME = "grok-build";
6846
+ var GROK_BUILTIN_TOOLS = /* @__PURE__ */ new Set([
6847
+ "run_terminal_command",
6848
+ "read_file",
6849
+ "write_file",
6850
+ "search",
6851
+ "web_search"
6852
+ ]);
6853
+ var grokAdapter = {
6854
+ name: GROK_HARNESS_NAME,
6855
+ builtinTools: GROK_BUILTIN_TOOLS,
6856
+ detect: (opts) => hasRecentFile(
6857
+ opts.roots ?? sessionRoots(),
6858
+ isGrokEvidenceFile,
6859
+ opts.sinceMs
6860
+ ),
6861
+ async scan(opts) {
6862
+ const aggregate = createAggregate4();
6863
+ const result = await scan4(aggregate, opts);
6864
+ return {
6865
+ aggregate,
6866
+ stats: result.stats,
6867
+ workflow: aggregate.workflow.finish(),
6868
+ workflowLocal: aggregate.workflowLocal,
6869
+ scanComplete: result.complete,
6870
+ sessionDates: result.sessionDates
6871
+ };
6872
+ }
6873
+ };
6874
+
5469
6875
  // src/harness/opencode/analyzer.ts
5470
6876
  var OPENCODE_BUILTIN_TOOLS = /* @__PURE__ */ new Set([
5471
6877
  "apply_patch",
@@ -5485,7 +6891,7 @@ var OPENCODE_BUILTIN_TOOLS = /* @__PURE__ */ new Set([
5485
6891
  "websearch",
5486
6892
  "write"
5487
6893
  ]);
5488
- function createAggregate4() {
6894
+ function createAggregate5() {
5489
6895
  const workflowLocal = createWorkflowLocalSources();
5490
6896
  return Object.assign(createAggregate(), {
5491
6897
  workflow: createHarnessWorkflowReducer("opencode", workflowLocal),
@@ -5534,7 +6940,7 @@ function ingestMessageRow(agg, state, row) {
5534
6940
  agg.assistantRecords++;
5535
6941
  agg.distinctResponses++;
5536
6942
  if (tsMs === null) agg.untimestampedResponses++;
5537
- const counts = {
6943
+ const counts2 = {
5538
6944
  input: asNum(row.input),
5539
6945
  output: asNum(row.output),
5540
6946
  cacheWrite5m: 0,
@@ -5542,7 +6948,7 @@ function ingestMessageRow(agg, state, row) {
5542
6948
  cacheWriteUnsplit: asNum(row.cacheWrite),
5543
6949
  cacheRead: asNum(row.cacheRead)
5544
6950
  };
5545
- const total = counts.input + counts.output + counts.cacheWriteUnsplit + counts.cacheRead;
6951
+ const total = counts2.input + counts2.output + counts2.cacheWriteUnsplit + counts2.cacheRead;
5546
6952
  if (session?.parentId) agg.sidechainTokens += total;
5547
6953
  else agg.mainTokens += total;
5548
6954
  const provider = asStr(row.providerId);
@@ -5551,8 +6957,8 @@ function ingestMessageRow(agg, state, row) {
5551
6957
  addModelUsage(
5552
6958
  agg,
5553
6959
  modelKey,
5554
- counts,
5555
- apiEquivalentCost(modelKey, counts, tsMs),
6960
+ counts2,
6961
+ apiEquivalentCost(modelKey, counts2, tsMs),
5556
6962
  1,
5557
6963
  { tsMs, sidechain: Boolean(session?.parentId) }
5558
6964
  );
@@ -5567,7 +6973,7 @@ function ingestMessageRow(agg, state, row) {
5567
6973
  tsMs,
5568
6974
  ...provider && model ? { model: `${provider}:${model}` } : {},
5569
6975
  thinkingTokens: asNum(row.reasoning),
5570
- responseTokens: counts.output,
6976
+ responseTokens: counts2.output,
5571
6977
  routingTokens: total,
5572
6978
  ...completed > tsMs ? { durationSec: (completed - tsMs) / 1e3 } : {}
5573
6979
  });
@@ -5650,14 +7056,14 @@ function noteConfiguredMcpServers2(agg, state, serverNames) {
5650
7056
 
5651
7057
  // src/harness/opencode/scan.ts
5652
7058
  import { readFileSync as readFileSync7 } from "node:fs";
5653
- import { readdir as readdir4, stat as stat4 } from "node:fs/promises";
5654
- import { homedir as homedir8 } from "node:os";
5655
- import path4 from "node:path";
7059
+ import { readdir as readdir5, stat as stat6 } from "node:fs/promises";
7060
+ import { homedir as homedir11 } from "node:os";
7061
+ import path9 from "node:path";
5656
7062
  var OPENCODE_MIGRATION_CEILING = 20260622202450;
5657
7063
  function opencodeDataDirs() {
5658
7064
  const xdg = process.env.XDG_DATA_HOME;
5659
- const base = xdg || path4.join(homedir8(), ".local", "share");
5660
- return [path4.join(base, "opencode")];
7065
+ const base = xdg || path9.join(homedir11(), ".local", "share");
7066
+ return [path9.join(base, "opencode")];
5661
7067
  }
5662
7068
  function isStoreFile(basename2) {
5663
7069
  return basename2 === "opencode.db" || /^opencode-[^/]+\.db$/.test(basename2);
@@ -5666,8 +7072,8 @@ async function dbFilesIn(root) {
5666
7072
  const override = process.env.OPENCODE_DB;
5667
7073
  if (override) return [override];
5668
7074
  try {
5669
- const entries = await readdir4(root, { withFileTypes: true });
5670
- return entries.filter((e) => e.isFile() && isStoreFile(e.name)).map((e) => path4.join(root, e.name)).sort();
7075
+ const entries = await readdir5(root, { withFileTypes: true });
7076
+ return entries.filter((e) => e.isFile() && isStoreFile(e.name)).map((e) => path9.join(root, e.name)).sort();
5671
7077
  } catch {
5672
7078
  return [];
5673
7079
  }
@@ -5687,7 +7093,7 @@ function errorClass2(e) {
5687
7093
  return e instanceof Error ? e.constructor.name : "unknown";
5688
7094
  }
5689
7095
  var readError2 = (reason) => Object.assign(new Error(reason), { code: reason });
5690
- async function scan3(agg, opts = {}) {
7096
+ async function scan5(agg, opts = {}) {
5691
7097
  const stats = emptyScanStats();
5692
7098
  const open2 = await loadSqlite();
5693
7099
  const sinceMs = opts.sinceMs ?? 0;
@@ -5704,7 +7110,7 @@ async function scan3(agg, opts = {}) {
5704
7110
  } catch (e) {
5705
7111
  stats.filesUnreadable++;
5706
7112
  stats.unreadableFiles.push({
5707
- path: path4.basename(file),
7113
+ path: path9.basename(file),
5708
7114
  reason: errorClass2(e)
5709
7115
  });
5710
7116
  }
@@ -5871,8 +7277,8 @@ function pickTs(jsonTs, columnTs) {
5871
7277
  }
5872
7278
  function opencodeConfigFile() {
5873
7279
  const xdg = process.env.XDG_CONFIG_HOME;
5874
- const base = xdg || path4.join(homedir8(), ".config");
5875
- return path4.join(base, "opencode", "opencode.json");
7280
+ const base = xdg || path9.join(homedir11(), ".config");
7281
+ return path9.join(base, "opencode", "opencode.json");
5876
7282
  }
5877
7283
  function readConfiguredMcpServers2(agg, state, configFile) {
5878
7284
  const file = configFile ?? opencodeConfigFile();
@@ -5924,7 +7330,7 @@ async function detectOpencode(opts) {
5924
7330
  if (open2 === null) return false;
5925
7331
  for (const root of opts.roots ?? opencodeDataDirs()) {
5926
7332
  for (const file of await dbFilesIn(root)) {
5927
- if (!await exists3(file)) continue;
7333
+ if (!await exists4(file)) continue;
5928
7334
  let db = null;
5929
7335
  try {
5930
7336
  db = open2(file);
@@ -5944,9 +7350,9 @@ async function detectOpencode(opts) {
5944
7350
  }
5945
7351
  return false;
5946
7352
  }
5947
- async function exists3(p8) {
7353
+ async function exists4(p8) {
5948
7354
  try {
5949
- await stat4(p8);
7355
+ await stat6(p8);
5950
7356
  return true;
5951
7357
  } catch {
5952
7358
  return false;
@@ -5965,8 +7371,8 @@ var opencodeAdapter = {
5965
7371
  });
5966
7372
  },
5967
7373
  async scan(opts) {
5968
- const aggregate = createAggregate4();
5969
- const stats = await scan3(aggregate, {
7374
+ const aggregate = createAggregate5();
7375
+ const stats = await scan5(aggregate, {
5970
7376
  sinceMs: opts.sinceMs,
5971
7377
  ...opts.onProgress ? { onProgress: opts.onProgress } : {}
5972
7378
  });
@@ -5980,7 +7386,7 @@ var opencodeAdapter = {
5980
7386
  };
5981
7387
 
5982
7388
  // src/harness/pi/analyzer.ts
5983
- function createAggregate5() {
7389
+ function createAggregate6() {
5984
7390
  const workflowLocal = createWorkflowLocalSources();
5985
7391
  return Object.assign(createAggregate(), {
5986
7392
  workflow: createHarnessWorkflowReducer("pi-mono", workflowLocal),
@@ -6049,7 +7455,7 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
6049
7455
  if (outcome !== "duplicate") {
6050
7456
  if (state.sessionId && tsMs !== null) {
6051
7457
  const usage = asObj(message.usage);
6052
- const counts = readCounts2(message.usage);
7458
+ const counts2 = readCounts2(message.usage);
6053
7459
  agg.workflow.ingest({
6054
7460
  type: "response",
6055
7461
  session: state.sessionId,
@@ -6058,8 +7464,8 @@ function ingestEntry(agg, raw, state, fold, sinceMs) {
6058
7464
  tsMs,
6059
7465
  ...state.modelKey ? { model: state.modelKey } : {},
6060
7466
  thinkingTokens: usage ? asNum(usage.reasoning) : 0,
6061
- responseTokens: counts?.output ?? 0,
6062
- routingTokens: counts ? countsTotal(counts) : 0
7467
+ responseTokens: counts2?.output ?? 0,
7468
+ routingTokens: counts2 ? countsTotal(counts2) : 0
6063
7469
  });
6064
7470
  ingestContent(agg, message.content, state.sessionId, state.cwd, tsMs);
6065
7471
  agg.workflow.ingest({
@@ -6086,9 +7492,9 @@ function noteActivity2(agg, state, tsMs) {
6086
7492
  agg.projectDirs.add(state.cwd ?? "(unknown)");
6087
7493
  }
6088
7494
  function countUsage(agg, fold, rec, msgTsMs, usageRaw, modelKey, tsMs, priceable = true) {
6089
- const counts = readCounts2(usageRaw);
6090
- if (!counts) return "none";
6091
- const total = countsTotal(counts);
7495
+ const counts2 = readCounts2(usageRaw);
7496
+ if (!counts2) return "none";
7497
+ const total = countsTotal(counts2);
6092
7498
  if (total === 0) return "none";
6093
7499
  const id = asStr(rec.id);
6094
7500
  if (id) {
@@ -6107,8 +7513,8 @@ function countUsage(agg, fold, rec, msgTsMs, usageRaw, modelKey, tsMs, priceable
6107
7513
  addModelUsage(
6108
7514
  agg,
6109
7515
  key2,
6110
- counts,
6111
- priceable ? apiEquivalentCost(key2, counts, tsMs) : null,
7516
+ counts2,
7517
+ priceable ? apiEquivalentCost(key2, counts2, tsMs) : null,
6112
7518
  1,
6113
7519
  { tsMs }
6114
7520
  );
@@ -6173,18 +7579,18 @@ function readCounts2(usageRaw) {
6173
7579
  }
6174
7580
 
6175
7581
  // src/harness/pi/scan.ts
6176
- import { createReadStream as createReadStream2 } from "node:fs";
6177
- import { readdir as readdir5, realpath as realpath3, stat as stat5 } from "node:fs/promises";
6178
- import { homedir as homedir9 } from "node:os";
6179
- import path5 from "node:path";
6180
- import readline2 from "node:readline";
7582
+ import { createReadStream as createReadStream3 } from "node:fs";
7583
+ import { readdir as readdir6, realpath as realpath3, stat as stat7 } from "node:fs/promises";
7584
+ import { homedir as homedir12 } from "node:os";
7585
+ import path10 from "node:path";
7586
+ import readline3 from "node:readline";
6181
7587
  var MAX_SESSION_VERSION = 3;
6182
7588
  function piAgentDir() {
6183
- return process.env.PI_CODING_AGENT_DIR || path5.join(homedir9(), ".pi", "agent");
7589
+ return process.env.PI_CODING_AGENT_DIR || path10.join(homedir12(), ".pi", "agent");
6184
7590
  }
6185
- function sessionRoots() {
7591
+ function sessionRoots2() {
6186
7592
  const override = process.env.PI_CODING_AGENT_SESSION_DIR;
6187
- return [override || path5.join(piAgentDir(), "sessions")];
7593
+ return [override || path10.join(piAgentDir(), "sessions")];
6188
7594
  }
6189
7595
  var SESSION_FILE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/;
6190
7596
  function isSessionFile(basename2) {
@@ -6193,22 +7599,22 @@ function isSessionFile(basename2) {
6193
7599
  async function* walkSessions(dir) {
6194
7600
  let entries;
6195
7601
  try {
6196
- entries = await readdir5(dir, { withFileTypes: true });
7602
+ entries = await readdir6(dir, { withFileTypes: true });
6197
7603
  } catch {
6198
7604
  return;
6199
7605
  }
6200
7606
  for (const e of entries) {
6201
- const full = path5.join(dir, e.name);
7607
+ const full = path10.join(dir, e.name);
6202
7608
  if (e.isDirectory()) yield* walkSessions(full);
6203
7609
  else if (e.isFile() && isSessionFile(e.name)) yield full;
6204
7610
  }
6205
7611
  }
6206
- async function scan4(agg, opts = {}) {
7612
+ async function scan6(agg, opts = {}) {
6207
7613
  const stats = emptyScanStats();
6208
7614
  const visited = /* @__PURE__ */ new Set();
6209
7615
  const fold = createFoldState();
6210
- for (const root of opts.roots ?? sessionRoots()) {
6211
- if (!await exists4(root)) continue;
7616
+ for (const root of opts.roots ?? sessionRoots2()) {
7617
+ if (!await exists5(root)) continue;
6212
7618
  for await (const file of walkSessions(root)) {
6213
7619
  stats.filesFound++;
6214
7620
  let resolved;
@@ -6224,7 +7630,7 @@ async function scan4(agg, opts = {}) {
6224
7630
  visited.add(resolved);
6225
7631
  if (opts.sinceMs !== void 0) {
6226
7632
  try {
6227
- const st = await stat5(file);
7633
+ const st = await stat7(file);
6228
7634
  if (st.mtimeMs < opts.sinceMs) {
6229
7635
  stats.filesSkippedByMtime++;
6230
7636
  continue;
@@ -6246,7 +7652,7 @@ async function scan4(agg, opts = {}) {
6246
7652
  stats.filesUnreadable++;
6247
7653
  stats.filesRead--;
6248
7654
  stats.unreadableFiles.push({
6249
- path: path5.relative(root, file),
7655
+ path: path10.relative(root, file),
6250
7656
  reason: "version-too-new"
6251
7657
  });
6252
7658
  }
@@ -6254,7 +7660,7 @@ async function scan4(agg, opts = {}) {
6254
7660
  stats.filesUnreadable++;
6255
7661
  stats.filesRead--;
6256
7662
  stats.unreadableFiles.push({
6257
- path: path5.relative(root, file),
7663
+ path: path10.relative(root, file),
6258
7664
  reason: "read-error"
6259
7665
  });
6260
7666
  }
@@ -6262,17 +7668,17 @@ async function scan4(agg, opts = {}) {
6262
7668
  }
6263
7669
  return stats;
6264
7670
  }
6265
- async function exists4(p8) {
7671
+ async function exists5(p8) {
6266
7672
  try {
6267
- await stat5(p8);
7673
+ await stat7(p8);
6268
7674
  return true;
6269
7675
  } catch {
6270
7676
  return false;
6271
7677
  }
6272
7678
  }
6273
7679
  async function ingestFile3(agg, fold, file, sinceMs) {
6274
- const rl = readline2.createInterface({
6275
- input: createReadStream2(file, { encoding: "utf8" }),
7680
+ const rl = readline3.createInterface({
7681
+ input: createReadStream3(file, { encoding: "utf8" }),
6276
7682
  crlfDelay: Number.POSITIVE_INFINITY
6277
7683
  });
6278
7684
  const state = createFileState2();
@@ -6325,14 +7731,14 @@ var piAdapter = {
6325
7731
  builtinTools: PI_BUILTIN_TOOLS,
6326
7732
  async detect(opts) {
6327
7733
  return hasRecentFile(
6328
- opts.roots ?? sessionRoots(),
7734
+ opts.roots ?? sessionRoots2(),
6329
7735
  isSessionFile,
6330
7736
  opts.sinceMs
6331
7737
  );
6332
7738
  },
6333
7739
  async scan(opts) {
6334
- const aggregate = createAggregate5();
6335
- const stats = await scan4(aggregate, {
7740
+ const aggregate = createAggregate6();
7741
+ const stats = await scan6(aggregate, {
6336
7742
  sinceMs: opts.sinceMs,
6337
7743
  ...opts.onProgress ? { onProgress: opts.onProgress } : {}
6338
7744
  });
@@ -6349,12 +7755,16 @@ var piAdapter = {
6349
7755
  var HARNESS_ADAPTERS = [
6350
7756
  claudeAdapter,
6351
7757
  codexAdapter,
7758
+ grokAdapter,
7759
+ cursorAdapter,
6352
7760
  opencodeAdapter,
6353
7761
  piAdapter
6354
7762
  ];
6355
7763
  function harnessLabel2(name) {
6356
7764
  if (name === CLAUDE_HARNESS_NAME) return "Claude Code";
6357
7765
  if (name === CODEX_HARNESS_NAME) return "Codex";
7766
+ if (name === CURSOR_HARNESS_NAME) return "Cursor";
7767
+ if (name === GROK_HARNESS_NAME) return "Grok Build";
6358
7768
  if (name === OPENCODE_HARNESS_NAME) return "opencode";
6359
7769
  if (name === PI_HARNESS_NAME) return "Pi";
6360
7770
  return name;
@@ -6388,7 +7798,7 @@ var MCP_ADD_ARGS = [
6388
7798
  "mcp"
6389
7799
  ];
6390
7800
  var MCP_REMOVE_ARGS = ["mcp", "remove", "--scope", "user", "aistack"];
6391
- var SKILL_DEST = join6(homedir10(), ".claude", "skills", "aistack-sync");
7801
+ var SKILL_DEST = join6(homedir13(), ".claude", "skills", "aistack-sync");
6392
7802
  function runClaude(args) {
6393
7803
  const r = spawnSync("claude", args, { encoding: "utf-8" });
6394
7804
  const notFound = r.error !== void 0 && r.error.code === "ENOENT";
@@ -6621,9 +8031,9 @@ async function createCommand() {
6621
8031
  import { hostname } from "node:os";
6622
8032
  import * as p5 from "@clack/prompts";
6623
8033
  import open from "open";
6624
- function proposedMachineName(read = hostname) {
8034
+ function proposedMachineName(read3 = hostname) {
6625
8035
  try {
6626
- const name = read().trim().replace(/\.local$/i, "");
8036
+ const name = read3().trim().replace(/\.local$/i, "");
6627
8037
  if (!name || name.length > 64) return void 0;
6628
8038
  return name;
6629
8039
  } catch {
@@ -6710,13 +8120,13 @@ async function loginCommand(options = {}) {
6710
8120
  import * as p7 from "@clack/prompts";
6711
8121
 
6712
8122
  // src/autosync/codexHook.ts
6713
- import { createHash } from "node:crypto";
8123
+ import { createHash as createHash2 } from "node:crypto";
6714
8124
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
6715
- import { homedir as homedir11 } from "node:os";
8125
+ import { homedir as homedir14 } from "node:os";
6716
8126
  import { dirname as dirname5, join as join8 } from "node:path";
6717
8127
  import { parse } from "smol-toml";
6718
8128
  function codexHome2() {
6719
- return process.env.CODEX_HOME || join8(homedir11(), ".codex");
8129
+ return process.env.CODEX_HOME || join8(homedir14(), ".codex");
6720
8130
  }
6721
8131
  function codexHooksFile() {
6722
8132
  return join8(codexHome2(), "hooks.json");
@@ -6742,9 +8152,9 @@ function readHooksJson(file) {
6742
8152
  }
6743
8153
  var CODEX_TRUST_INSTRUCTION = "Codex hook written - open Codex and run /hooks once to trust it, or it will not run.";
6744
8154
  function installCodexAutoSyncHook(file = codexHooksFile()) {
6745
- const read = readHooksJson(file);
6746
- if ("error" in read) return { ok: false, message: read.error };
6747
- const settings = read.settings;
8155
+ const read3 = readHooksJson(file);
8156
+ if ("error" in read3) return { ok: false, message: read3.error };
8157
+ const settings = read3.settings;
6748
8158
  const hooks = settings.hooks ?? {};
6749
8159
  const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
6750
8160
  const kept = sessionStart.map((m) => ({
@@ -6764,9 +8174,9 @@ function installCodexAutoSyncHook(file = codexHooksFile()) {
6764
8174
  function removeCodexAutoSyncHook(file = codexHooksFile()) {
6765
8175
  if (!existsSync8(file))
6766
8176
  return { ok: true, message: "no Codex hook to remove" };
6767
- const read = readHooksJson(file);
6768
- if ("error" in read) return { ok: false, message: read.error };
6769
- const settings = read.settings;
8177
+ const read3 = readHooksJson(file);
8178
+ if ("error" in read3) return { ok: false, message: read3.error };
8179
+ const settings = read3.settings;
6770
8180
  const sessionStart = settings.hooks?.SessionStart;
6771
8181
  if (!Array.isArray(sessionStart)) {
6772
8182
  return { ok: true, message: "no Codex hook to remove" };
@@ -6791,18 +8201,18 @@ function removeCodexAutoSyncHook(file = codexHooksFile()) {
6791
8201
  return { ok: true, message: `hook removed from ${file}` };
6792
8202
  }
6793
8203
  function codexAutoSyncHookInstalled(file = codexHooksFile()) {
6794
- const read = readHooksJson(file);
6795
- if ("error" in read) return false;
6796
- const sessionStart = read.settings.hooks?.SessionStart;
8204
+ const read3 = readHooksJson(file);
8205
+ if ("error" in read3) return false;
8206
+ const sessionStart = read3.settings.hooks?.SessionStart;
6797
8207
  if (!Array.isArray(sessionStart)) return false;
6798
8208
  return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs(h)));
6799
8209
  }
6800
8210
  function codexHookTrusted(configFile = codexConfigFile(), hooksFile = codexHooksFile()) {
6801
8211
  try {
6802
8212
  const config = parse(readFileSync9(configFile, "utf-8"));
6803
- const read = readHooksJson(hooksFile);
6804
- if ("error" in read) return null;
6805
- const sessionStart = read.settings.hooks?.SessionStart;
8213
+ const read3 = readHooksJson(hooksFile);
8214
+ if ("error" in read3) return null;
8215
+ const sessionStart = read3.settings.hooks?.SessionStart;
6806
8216
  if (!Array.isArray(sessionStart)) return false;
6807
8217
  const matches = [];
6808
8218
  for (const [groupIndex, group] of sessionStart.entries()) {
@@ -6825,7 +8235,7 @@ function codexHookTrusted(configFile = codexConfigFile(), hooksFile = codexHooks
6825
8235
  hooks: [normalizedHandler]
6826
8236
  };
6827
8237
  if (typeof group.matcher === "string") identity.matcher = group.matcher;
6828
- const currentHash = `sha256:${createHash("sha256").update(JSON.stringify(canonicalJson2(identity))).digest("hex")}`;
8238
+ const currentHash = `sha256:${createHash2("sha256").update(JSON.stringify(canonicalJson2(identity))).digest("hex")}`;
6829
8239
  const key2 = `${hooksFile}:session_start:${groupIndex}:${handlerIndex}`;
6830
8240
  matches.push(config.hooks?.state?.[key2]?.trusted_hash === currentHash);
6831
8241
  }
@@ -6855,11 +8265,16 @@ function canonicalJson2(value) {
6855
8265
  // src/autosync/optin.ts
6856
8266
  import * as p6 from "@clack/prompts";
6857
8267
 
8268
+ // src/autosync/cursorHook.ts
8269
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
8270
+ import { homedir as homedir16, platform as platform2 } from "node:os";
8271
+ import { dirname as dirname7, join as join10 } from "node:path";
8272
+
6858
8273
  // src/autosync/hook.ts
6859
8274
  import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
6860
- import { homedir as homedir12 } from "node:os";
8275
+ import { homedir as homedir15 } from "node:os";
6861
8276
  import { dirname as dirname6, join as join9 } from "node:path";
6862
- var CLAUDE_SETTINGS_FILE = join9(homedir12(), ".claude", "settings.json");
8277
+ var CLAUDE_SETTINGS_FILE = join9(homedir15(), ".claude", "settings.json");
6863
8278
  var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
6864
8279
  function isOurs2(entry) {
6865
8280
  return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
@@ -6877,9 +8292,9 @@ function readClaudeSettings(file) {
6877
8292
  }
6878
8293
  }
6879
8294
  function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
6880
- const read = readClaudeSettings(file);
6881
- if ("error" in read) return { ok: false, message: read.error };
6882
- const settings = read.settings;
8295
+ const read3 = readClaudeSettings(file);
8296
+ if ("error" in read3) return { ok: false, message: read3.error };
8297
+ const settings = read3.settings;
6883
8298
  const hooks = settings.hooks ?? {};
6884
8299
  const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
6885
8300
  const kept = sessionStart.map((m) => ({
@@ -6897,9 +8312,9 @@ function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
6897
8312
  }
6898
8313
  function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
6899
8314
  if (!existsSync9(file)) return { ok: true, message: "no hook to remove" };
6900
- const read = readClaudeSettings(file);
6901
- if ("error" in read) return { ok: false, message: read.error };
6902
- const settings = read.settings;
8315
+ const read3 = readClaudeSettings(file);
8316
+ if ("error" in read3) return { ok: false, message: read3.error };
8317
+ const settings = read3.settings;
6903
8318
  const sessionStart = settings.hooks?.SessionStart;
6904
8319
  if (!Array.isArray(sessionStart)) {
6905
8320
  return { ok: true, message: "no hook to remove" };
@@ -6924,16 +8339,201 @@ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
6924
8339
  return { ok: true, message: `hook removed from ${file}` };
6925
8340
  }
6926
8341
  function autoSyncHookInstalled(file = CLAUDE_SETTINGS_FILE) {
6927
- const read = readClaudeSettings(file);
6928
- if ("error" in read) return false;
6929
- const sessionStart = read.settings.hooks?.SessionStart;
8342
+ const read3 = readClaudeSettings(file);
8343
+ if ("error" in read3) return false;
8344
+ const sessionStart = read3.settings.hooks?.SessionStart;
6930
8345
  if (!Array.isArray(sessionStart)) return false;
6931
8346
  return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs2(h)));
6932
8347
  }
6933
8348
 
8349
+ // src/autosync/cursorHook.ts
8350
+ var CURSOR_HOOK_FILE = join10(homedir16(), ".cursor", "hooks.json");
8351
+ function cursorHookCommand(os = platform2()) {
8352
+ if (os === "win32") {
8353
+ const script = `Start-Process -WindowStyle Hidden -FilePath 'cmd.exe' -ArgumentList '/d /s /c "set AISTACK_HOOK_SOURCE=cursor&& (${AUTO_SYNC_HOOK_COMMAND}) <NUL >NUL 2>&1"'`;
8354
+ return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
8355
+ }
8356
+ const detach = os === "darwin" ? "nohup" : "setsid nohup";
8357
+ return `${detach} sh -c 'export AISTACK_HOOK_SOURCE=cursor; ${AUTO_SYNC_HOOK_COMMAND}' </dev/null >/dev/null 2>&1 &`;
8358
+ }
8359
+ function read(file) {
8360
+ if (!existsSync10(file)) return {};
8361
+ const value = JSON.parse(readFileSync11(file, "utf8"));
8362
+ if (!value || typeof value !== "object" || Array.isArray(value))
8363
+ throw new Error("expected a JSON object");
8364
+ const config = value;
8365
+ if (config.version !== void 0 && config.version !== 1)
8366
+ throw new Error("unsupported hooks version");
8367
+ if (config.hooks !== void 0 && (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks) || Object.values(config.hooks).some(
8368
+ (entries) => !Array.isArray(entries) || entries.some(
8369
+ (entry) => !entry || typeof entry !== "object" || Array.isArray(entry)
8370
+ )
8371
+ )))
8372
+ throw new Error("malformed hooks object");
8373
+ return config;
8374
+ }
8375
+ function isOurs3(entry) {
8376
+ return ["linux", "darwin", "win32"].some(
8377
+ (os) => entry.command === cursorHookCommand(os)
8378
+ );
8379
+ }
8380
+ function update(file, install, os) {
8381
+ try {
8382
+ const config = read(file);
8383
+ const existing = config.hooks?.stop ?? [];
8384
+ const kept = existing.filter((entry) => !isOurs3(entry));
8385
+ if (!install && kept.length === existing.length)
8386
+ return { ok: true, message: "no Cursor hook to remove" };
8387
+ if (install) kept.push({ command: cursorHookCommand(os) });
8388
+ const hooks = { ...config.hooks };
8389
+ if (kept.length) hooks.stop = kept;
8390
+ else delete hooks.stop;
8391
+ if (Object.keys(hooks).length) config.hooks = hooks;
8392
+ else delete config.hooks;
8393
+ if (install) config.version = 1;
8394
+ mkdirSync5(dirname7(file), { recursive: true });
8395
+ writeFileSync5(file, `${JSON.stringify(config, null, 2)}
8396
+ `);
8397
+ return {
8398
+ ok: true,
8399
+ message: `Cursor stop hook ${install ? "written to" : "removed from"} ${file}`
8400
+ };
8401
+ } catch (error) {
8402
+ return {
8403
+ ok: false,
8404
+ message: `Could not ${install ? "install" : "remove"} Cursor hook in ${file}: ${error instanceof Error ? error.message : String(error)}`
8405
+ };
8406
+ }
8407
+ }
8408
+ function installCursorAutoSyncHook(file = CURSOR_HOOK_FILE, os = platform2()) {
8409
+ return update(file, true, os);
8410
+ }
8411
+ function removeCursorAutoSyncHook(file = CURSOR_HOOK_FILE) {
8412
+ return update(file, false, platform2());
8413
+ }
8414
+ function cursorAutoSyncHookInstalled(file = CURSOR_HOOK_FILE) {
8415
+ try {
8416
+ return (read(file).hooks?.stop ?? []).some(isOurs3);
8417
+ } catch {
8418
+ return false;
8419
+ }
8420
+ }
8421
+
8422
+ // src/autosync/grokHook.ts
8423
+ import {
8424
+ existsSync as existsSync11,
8425
+ mkdirSync as mkdirSync6,
8426
+ readFileSync as readFileSync12,
8427
+ unlinkSync,
8428
+ writeFileSync as writeFileSync6
8429
+ } from "node:fs";
8430
+ import { homedir as homedir17, platform as platform3 } from "node:os";
8431
+ import { dirname as dirname8, join as join11 } from "node:path";
8432
+ var GROK_HOOK_FILE = join11(
8433
+ process.env.GROK_HOME || join11(homedir17(), ".grok"),
8434
+ "hooks",
8435
+ "aistack.json"
8436
+ );
8437
+ var SYNC_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
8438
+ function grokHookCommand(os = platform3()) {
8439
+ if (os === "win32") {
8440
+ return `Start-Process -WindowStyle Hidden -FilePath "cmd.exe" -ArgumentList '/d /s /c "set AISTACK_HOOK_SOURCE=grok&& (${SYNC_COMMAND}) >NUL 2>&1"'`;
8441
+ }
8442
+ if (os === "darwin") {
8443
+ return `nohup sh -c 'export AISTACK_HOOK_SOURCE=grok; ${SYNC_COMMAND}' </dev/null >/dev/null 2>&1 &`;
8444
+ }
8445
+ return `setsid nohup sh -c 'export AISTACK_HOOK_SOURCE=grok; ${SYNC_COMMAND}' >/dev/null 2>&1 &`;
8446
+ }
8447
+ function readHookFile(file) {
8448
+ if (!existsSync11(file)) return { value: {} };
8449
+ try {
8450
+ const value = JSON.parse(readFileSync12(file, "utf-8"));
8451
+ if (value && typeof value === "object" && !Array.isArray(value)) {
8452
+ const candidate = value;
8453
+ if (candidate.hooks !== void 0 && (!candidate.hooks || typeof candidate.hooks !== "object" || Array.isArray(candidate.hooks) || Object.values(candidate.hooks).some(
8454
+ (groups) => !Array.isArray(groups)
8455
+ ))) {
8456
+ return {
8457
+ error: `${file} has a malformed hooks object. Fix it, then retry.`
8458
+ };
8459
+ }
8460
+ return { value: candidate };
8461
+ }
8462
+ } catch {
8463
+ return { error: `${file} is not valid JSON. Fix it, then retry.` };
8464
+ }
8465
+ return { error: `${file} does not hold a JSON object` };
8466
+ }
8467
+ function isOurs4(entry) {
8468
+ return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
8469
+ }
8470
+ function installGrokAutoSyncHook(file = GROK_HOOK_FILE, os = platform3()) {
8471
+ const read3 = readHookFile(file);
8472
+ if ("error" in read3) return { ok: false, message: read3.error };
8473
+ const foreignKeys = Object.keys(read3.value).filter((key2) => key2 !== "hooks");
8474
+ const foreignEvents = Object.keys(read3.value.hooks ?? {}).filter(
8475
+ (key2) => key2 !== "SessionStart"
8476
+ );
8477
+ const sessionStart = read3.value.hooks?.SessionStart ?? [];
8478
+ const foreignHandlers = sessionStart.flatMap(
8479
+ (group) => (group.hooks ?? []).filter((entry) => !isOurs4(entry))
8480
+ );
8481
+ if (foreignKeys.length > 0 || foreignEvents.length > 0 || foreignHandlers.length > 0) {
8482
+ return {
8483
+ ok: false,
8484
+ message: `${file} contains hooks not owned by AI Stack. Move them to another Grok hook file, then retry.`
8485
+ };
8486
+ }
8487
+ const value = {
8488
+ hooks: {
8489
+ SessionStart: [
8490
+ {
8491
+ hooks: [
8492
+ { type: "command", command: grokHookCommand(os), timeout: 5 }
8493
+ ]
8494
+ }
8495
+ ]
8496
+ }
8497
+ };
8498
+ mkdirSync6(dirname8(file), { recursive: true });
8499
+ writeFileSync6(file, `${JSON.stringify(value, null, 2)}
8500
+ `);
8501
+ return {
8502
+ ok: true,
8503
+ message: `Grok Build SessionStart hook written to ${file}. Start a new Grok session or reload hooks before expecting it to run.`
8504
+ };
8505
+ }
8506
+ function removeGrokAutoSyncHook(file = GROK_HOOK_FILE) {
8507
+ if (!existsSync11(file))
8508
+ return { ok: true, message: "no Grok Build hook to remove" };
8509
+ const read3 = readHookFile(file);
8510
+ if ("error" in read3) return { ok: false, message: read3.error };
8511
+ const entries = read3.value.hooks?.SessionStart ?? [];
8512
+ const onlyOurs = Object.keys(read3.value).every((key2) => key2 === "hooks") && Object.keys(read3.value.hooks ?? {}).every(
8513
+ (key2) => key2 === "SessionStart"
8514
+ ) && entries.every(
8515
+ (group) => (group.hooks ?? []).every((entry) => isOurs4(entry))
8516
+ );
8517
+ if (!onlyOurs) {
8518
+ return {
8519
+ ok: false,
8520
+ message: `${file} contains hooks not owned by AI Stack and was not removed.`
8521
+ };
8522
+ }
8523
+ unlinkSync(file);
8524
+ return { ok: true, message: `Grok Build hook removed from ${file}` };
8525
+ }
8526
+ function grokAutoSyncHookInstalled(file = GROK_HOOK_FILE) {
8527
+ const read3 = readHookFile(file);
8528
+ if ("error" in read3) return false;
8529
+ return (read3.value.hooks?.SessionStart ?? []).some(
8530
+ (group) => (group.hooks ?? []).some((entry) => isOurs4(entry))
8531
+ );
8532
+ }
8533
+
6934
8534
  // src/autosync/optin.ts
6935
8535
  var NOT_LINKED = "This machine is not linked to an aistack account, and the auto-sync permission lives on your stack. Run `npx @use-aistack/cli sync` first.";
6936
- var NOTHING_TO_TRIGGER = `No Claude Code or Codex session on this machine in the last ${DEFAULT_WINDOW_DAYS} days, so nothing would trigger an auto-sync. Nothing was changed.`;
8536
+ var NOTHING_TO_TRIGGER = `No supported session on this machine in the last ${DEFAULT_WINDOW_DAYS} days, so nothing would trigger an auto-sync. Nothing was changed.`;
6937
8537
  async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
6938
8538
  frequencyHours = normalizeFrequencyHours(frequencyHours);
6939
8539
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
@@ -6966,6 +8566,14 @@ async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {
6966
8566
  trustLine = codexResult.message;
6967
8567
  }
6968
8568
  }
8569
+ if (names.has(GROK_HARNESS_NAME)) {
8570
+ const grokResult = (deps.installGrokHook ?? installGrokAutoSyncHook)();
8571
+ if (!grokResult.ok) return grokResult;
8572
+ }
8573
+ if (names.has(CURSOR_HARNESS_NAME)) {
8574
+ const result = (deps.installCursorHook ?? installCursorAutoSyncHook)();
8575
+ if (!result.ok) return result;
8576
+ }
6969
8577
  saveSettings(
6970
8578
  {
6971
8579
  autoSyncAnswered: true,
@@ -6976,7 +8584,7 @@ async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {
6976
8584
  return {
6977
8585
  ok: true,
6978
8586
  message: [
6979
- `Auto-sync is on. It runs about every ${frequencyHours}h when a ${harnessListLabel(detected)} session starts. Turn it off any time: npx @use-aistack/cli sync --auto off`,
8587
+ `Auto-sync is on. It runs about every ${frequencyHours}h when a ${harnessListLabel(detected)} ${names.has(CURSOR_HARNESS_NAME) ? "session is active" : "session starts"}. Turn it off any time: npx @use-aistack/cli sync --auto off`,
6980
8588
  ...trustLine ? [trustLine] : []
6981
8589
  ].join("\n")
6982
8590
  };
@@ -6997,7 +8605,9 @@ async function disableAutoSync(deps = {}) {
6997
8605
  );
6998
8606
  const result = (deps.removeHook ?? removeAutoSyncHook)();
6999
8607
  const codexResult = (deps.removeCodexHook ?? removeCodexAutoSyncHook)();
7000
- const failures = [result, codexResult].filter((r) => !r.ok).map((r) => r.message);
8608
+ const grokResult = (deps.removeGrokHook ?? removeGrokAutoSyncHook)();
8609
+ const cursorResult = (deps.removeCursorHook ?? removeCursorAutoSyncHook)();
8610
+ const failures = [result, codexResult, grokResult, cursorResult].filter((r) => !r.ok).map((r) => r.message);
7001
8611
  const token = (deps.getTokenImpl ?? getToken)();
7002
8612
  if (token !== null) {
7003
8613
  try {
@@ -7020,7 +8630,41 @@ async function disableAutoSync(deps = {}) {
7020
8630
  };
7021
8631
  }
7022
8632
  async function reconcileAutoSync(permission, deps = {}) {
7023
- if (permission?.enabled !== true) return null;
8633
+ if (permission === null) return null;
8634
+ if (permission.enabled !== true) {
8635
+ const settings = getSettings(deps.settingsFile);
8636
+ saveSettings(
8637
+ {
8638
+ autoSyncAnswered: true,
8639
+ autoSync: {
8640
+ enabled: false,
8641
+ frequencyHours: normalizeFrequencyHours(
8642
+ permission.frequencyHours ?? settings.autoSync?.frequencyHours
8643
+ )
8644
+ }
8645
+ },
8646
+ deps.settingsFile
8647
+ );
8648
+ const results = [
8649
+ (deps.removeHook ?? removeAutoSyncHook)(),
8650
+ (deps.removeCodexHook ?? removeCodexAutoSyncHook)(),
8651
+ (deps.removeGrokHook ?? removeGrokAutoSyncHook)(),
8652
+ (deps.removeCursorHook ?? removeCursorAutoSyncHook)()
8653
+ ];
8654
+ const failures2 = results.filter((result) => !result.ok);
8655
+ if (failures2.length > 0) {
8656
+ return {
8657
+ ok: false,
8658
+ message: `Auto-sync is off on this machine, but a trigger could not be removed: ${failures2.map((result) => result.message).join("; ")}. The next interactive sync will retry.`
8659
+ };
8660
+ }
8661
+ const changed = settings.autoSync?.enabled !== false || results.some((result) => !result.message.toLowerCase().startsWith("no "));
8662
+ if (!changed) return null;
8663
+ return {
8664
+ ok: true,
8665
+ message: "Auto-sync is off on this machine. Removed its local triggers."
8666
+ };
8667
+ }
7024
8668
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
7025
8669
  if (detected.length === 0) return null;
7026
8670
  const names = new Set(detected.map((a) => a.name));
@@ -7042,6 +8686,16 @@ async function reconcileAutoSync(permission, deps = {}) {
7042
8686
  deps.codexHookInstalledImpl ?? codexAutoSyncHookInstalled,
7043
8687
  deps.installCodexHook ?? installCodexAutoSyncHook
7044
8688
  );
8689
+ install(
8690
+ GROK_HARNESS_NAME,
8691
+ deps.grokHookInstalledImpl ?? grokAutoSyncHookInstalled,
8692
+ deps.installGrokHook ?? installGrokAutoSyncHook
8693
+ );
8694
+ install(
8695
+ CURSOR_HARNESS_NAME,
8696
+ deps.cursorHookInstalledImpl ?? cursorAutoSyncHookInstalled,
8697
+ deps.installCursorHook ?? installCursorAutoSyncHook
8698
+ );
7045
8699
  if (failures.length > 0) {
7046
8700
  return {
7047
8701
  ok: false,
@@ -7060,7 +8714,7 @@ async function reconcileAutoSync(permission, deps = {}) {
7060
8714
  if (installed.length === 0 && mirrored) return null;
7061
8715
  return {
7062
8716
  ok: true,
7063
- message: installed.length > 0 ? `Auto-sync is on for this stack. Installed the ${installed.join(" and ")} trigger on this machine; it runs about every ${frequencyHours}h when a session starts.` : `Auto-sync is on for this stack. It runs about every ${frequencyHours}h when a ${harnessListLabel(detected)} session starts.`
8717
+ message: installed.length > 0 ? `Auto-sync is on for this stack. Installed the ${installed.join(" and ")} trigger on this machine; it runs about every ${frequencyHours}h during session activity.` : `Auto-sync is on for this stack. It runs about every ${frequencyHours}h when a ${harnessListLabel(detected)} ${names.has(CURSOR_HARNESS_NAME) ? "session is active" : "session starts"}.`
7064
8718
  };
7065
8719
  }
7066
8720
  async function settleAutoSync(permission, deps = {}) {
@@ -7078,13 +8732,14 @@ async function offerAutoSyncOptIn(deps = {}) {
7078
8732
  return false;
7079
8733
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
7080
8734
  if (detected.length === 0) return false;
8735
+ const names = new Set(detected.map((adapter) => adapter.name));
7081
8736
  const answer = await p6.select({
7082
8737
  message: "Keep this stack fresh automatically every 6 hours?",
7083
8738
  options: [
7084
8739
  {
7085
8740
  value: "enable",
7086
8741
  label: "Enable",
7087
- hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} session starts`
8742
+ hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} ${names.has(CURSOR_HARNESS_NAME) ? "session is active" : "session starts"}`
7088
8743
  },
7089
8744
  {
7090
8745
  value: "later",
@@ -7126,15 +8781,18 @@ async function offerAutoSyncOptIn(deps = {}) {
7126
8781
  // src/autosync/run.ts
7127
8782
  import {
7128
8783
  appendFileSync,
7129
- mkdirSync as mkdirSync5,
7130
- readFileSync as readFileSync11,
7131
- writeFileSync as writeFileSync5
8784
+ closeSync,
8785
+ mkdirSync as mkdirSync8,
8786
+ openSync,
8787
+ readFileSync as readFileSync14,
8788
+ unlinkSync as unlinkSync2,
8789
+ writeFileSync as writeFileSync8
7132
8790
  } from "node:fs";
7133
- import { homedir as homedir13 } from "node:os";
7134
- import { dirname as dirname7, join as join10 } from "node:path";
8791
+ import { homedir as homedir19 } from "node:os";
8792
+ import { dirname as dirname9, join as join12 } from "node:path";
7135
8793
 
7136
8794
  // src/sync/stage.ts
7137
- import { createHash as createHash2 } from "node:crypto";
8795
+ import { createHash as createHash4 } from "node:crypto";
7138
8796
 
7139
8797
  // src/usage/days.ts
7140
8798
  var round6 = (n) => Math.round(n * 1e6) / 1e6;
@@ -7227,7 +8885,13 @@ function mergeUsageDays(perHarness) {
7227
8885
  function buildMeasuredDays(input) {
7228
8886
  const workflowByDate = /* @__PURE__ */ new Map();
7229
8887
  for (const day of input.workflow ?? []) workflowByDate.set(day.date, day);
7230
- const dates = [.../* @__PURE__ */ new Set([...input.usage.keys(), ...workflowByDate.keys()])].filter(
8888
+ const dates = [
8889
+ .../* @__PURE__ */ new Set([
8890
+ ...input.usage.keys(),
8891
+ ...workflowByDate.keys(),
8892
+ ...input.includeDates ?? []
8893
+ ])
8894
+ ].filter(
7231
8895
  (d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= input.from && d <= input.to
7232
8896
  ).sort();
7233
8897
  return dates.map((date) => {
@@ -7284,7 +8948,7 @@ function selectDaysToPublish(input) {
7284
8948
 
7285
8949
  // src/workflow/git.ts
7286
8950
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
7287
- import path6 from "node:path";
8951
+ import path11 from "node:path";
7288
8952
  var TEST_FILE_RULE_VERSION = "test-files/v2";
7289
8953
  var FILE_TYPE_RULE_VERSION = "file-types/v2";
7290
8954
  var COMMIT_SET_RULE_VERSION = "commit-set/v1";
@@ -7567,9 +9231,9 @@ function reduceGitHistories(histories, options) {
7567
9231
  if (included) seenCommits.add(hash);
7568
9232
  continue;
7569
9233
  }
7570
- const stat6 = parseNumstat(field);
7571
- if (!stat6) continue;
7572
- let file = stat6.file;
9234
+ const stat8 = parseNumstat(field);
9235
+ if (!stat8) continue;
9236
+ let file = stat8.file;
7573
9237
  if (file.length === 0) {
7574
9238
  fieldIndex += 2;
7575
9239
  file = fields[fieldIndex] ?? fields[fieldIndex - 1] ?? "";
@@ -7577,13 +9241,13 @@ function reduceGitHistories(histories, options) {
7577
9241
  if (!current?.included) continue;
7578
9242
  if (isUnauthoredPath(file)) continue;
7579
9243
  current.authored = true;
7580
- const fileChangedLines = stat6.additions + stat6.removals;
7581
- current.additions += stat6.additions;
7582
- current.removals += stat6.removals;
9244
+ const fileChangedLines = stat8.additions + stat8.removals;
9245
+ current.additions += stat8.additions;
9246
+ current.removals += stat8.removals;
7583
9247
  current.changedLines += fileChangedLines;
7584
9248
  if (isTestFile(file)) current.touchesTest = true;
7585
9249
  if (fileChangedLines <= 0) continue;
7586
- const extension = path6.extname(file).toLowerCase();
9250
+ const extension = path11.extname(file).toLowerCase();
7587
9251
  if (APPROVED_EXTENSIONS.has(extension)) {
7588
9252
  current.extensionLines.set(
7589
9253
  extension,
@@ -7686,6 +9350,47 @@ function buildWorkflowExtraction(harnessWorkflows, git, utcOffsetMinutes = machi
7686
9350
  };
7687
9351
  }
7688
9352
 
9353
+ // src/sync/grokDateCache.ts
9354
+ import { createHash as createHash3 } from "node:crypto";
9355
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
9356
+ import { homedir as homedir18 } from "node:os";
9357
+ import path12 from "node:path";
9358
+ var defaultFile = path12.join(
9359
+ homedir18(),
9360
+ ".config",
9361
+ "aistack",
9362
+ "grok-session-dates.json"
9363
+ );
9364
+ function grokCacheScope(baseUrl, stack, token) {
9365
+ return createHash3("sha256").update(`${baseUrl}\0${stack}\0${token}`).digest("hex");
9366
+ }
9367
+ function read2(file) {
9368
+ if (!existsSync12(file)) return {};
9369
+ try {
9370
+ const value = JSON.parse(readFileSync13(file, "utf8"));
9371
+ return value && typeof value === "object" ? value : {};
9372
+ } catch {
9373
+ return {};
9374
+ }
9375
+ }
9376
+ function loadGrokDateHints(scope, file = defaultFile) {
9377
+ return read2(file)[scope] ?? {};
9378
+ }
9379
+ function saveGrokDateHints(scope, hints, file = defaultFile) {
9380
+ const cache = read2(file);
9381
+ cache[scope] = hints;
9382
+ mkdirSync7(path12.dirname(file), { recursive: true });
9383
+ writeFileSync7(file, JSON.stringify(cache, null, 2));
9384
+ }
9385
+ function mapToHints(value, floor) {
9386
+ return Object.fromEntries(
9387
+ [...value].map(([id, dates]) => [
9388
+ id,
9389
+ [...dates].filter((d) => d >= floor).sort()
9390
+ ])
9391
+ );
9392
+ }
9393
+
7689
9394
  // src/sync/summary.ts
7690
9395
  function fmtTokens(n) {
7691
9396
  const sig = (v) => {
@@ -8086,7 +9791,7 @@ function buildGateSummary(ctx) {
8086
9791
  // src/sync/stage.ts
8087
9792
  var utcDate2 = (ms) => new Date(ms).toISOString().slice(0, 10);
8088
9793
  function stageId(bodyJson) {
8089
- return createHash2("sha256").update(bodyJson).digest("hex").slice(0, 12);
9794
+ return createHash4("sha256").update(bodyJson).digest("hex").slice(0, 12);
8090
9795
  }
8091
9796
  async function stageSync(deps) {
8092
9797
  const now = (deps.now ?? Date.now)();
@@ -8138,10 +9843,14 @@ async function stageSync(deps) {
8138
9843
  const sinceMs = windowStartMs(now, windowDays);
8139
9844
  const daysSinceMs = windowStartMs(now, retentionDays);
8140
9845
  const active2 = await adapters(sinceMs);
9846
+ const historical = await adapters(daysSinceMs);
9847
+ let dayScansComplete = true;
9848
+ const sessionDatesByHarness = /* @__PURE__ */ new Map();
8141
9849
  for (const adapter of active2) {
8142
9850
  progress(`Scanning recent ${adapter.name} usage`);
8143
9851
  const { aggregate, stats } = await adapter.scan({
8144
9852
  sinceMs,
9853
+ publishWorkflow: false,
8145
9854
  onProgress: (files) => progress(`Scanning recent ${adapter.name} usage \xB7 ${files} files`)
8146
9855
  });
8147
9856
  scanStats[adapter.name] = stats;
@@ -8158,12 +9867,16 @@ async function stageSync(deps) {
8158
9867
  })
8159
9868
  );
8160
9869
  }
8161
- for (const adapter of active2) {
9870
+ for (const adapter of historical) {
8162
9871
  progress(`Reading historical ${adapter.name} days`);
8163
- const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
9872
+ const { aggregate, workflow: workflow2, workflowLocal, scanComplete, sessionDates } = await adapter.scan({
8164
9873
  sinceMs: daysSinceMs,
9874
+ publishWorkflow: config.publishWorkflow,
8165
9875
  onProgress: (files) => progress(`Reading historical ${adapter.name} days \xB7 ${files} files`)
8166
9876
  });
9877
+ if (scanComplete === false) dayScansComplete = false;
9878
+ if (adapter.name === "grok-build" || adapter.name === "cursor")
9879
+ sessionDatesByHarness.set(adapter.name, sessionDates ?? /* @__PURE__ */ new Map());
8167
9880
  workflowScans.push({ aggregate: workflow2, local: workflowLocal });
8168
9881
  usageScans.push(
8169
9882
  buildUsageDays({
@@ -8189,17 +9902,43 @@ async function stageSync(deps) {
8189
9902
  toMs: now
8190
9903
  });
8191
9904
  }
9905
+ const correctionDates = /* @__PURE__ */ new Set();
9906
+ let acknowledgePublish;
9907
+ const acknowledgements = [];
9908
+ for (const [harness, currentDates] of sessionDatesByHarness) {
9909
+ if (!token || !config.stack) continue;
9910
+ const scope = grokCacheScope(
9911
+ harness === "grok-build" ? deps.baseUrl : `${deps.baseUrl}\0${harness}`,
9912
+ config.stack.slug,
9913
+ token
9914
+ );
9915
+ const floor = utcDate2(daysSinceMs);
9916
+ const previous = loadGrokDateHints(scope);
9917
+ const current = mapToHints(currentDates, floor);
9918
+ for (const dates of Object.values(previous))
9919
+ for (const date of dates) correctionDates.add(date);
9920
+ for (const dates of Object.values(current))
9921
+ for (const date of dates) correctionDates.add(date);
9922
+ if (Object.keys(previous).some((id) => !(id in current)))
9923
+ dayScansComplete = false;
9924
+ acknowledgements.push(() => saveGrokDateHints(scope, current));
9925
+ }
9926
+ if (acknowledgements.length)
9927
+ acknowledgePublish = () => {
9928
+ for (const acknowledge of acknowledgements) acknowledge();
9929
+ };
8192
9930
  const localDays = applyDayConsent(
8193
9931
  buildMeasuredDays({
8194
9932
  usage: mergeUsageDays(usageScans),
8195
9933
  ...workflow ? { workflow: workflow.days } : {},
8196
9934
  from: utcDate2(daysSinceMs),
8197
- to: utcDate2(now)
9935
+ to: utcDate2(now),
9936
+ includeDates: correctionDates
8198
9937
  }),
8199
9938
  config
8200
9939
  );
8201
9940
  const days = selectDaysToPublish({
8202
- local: localDays,
9941
+ local: dayScansComplete ? localDays : [],
8203
9942
  manifest,
8204
9943
  todayUtc: utcDate2(now)
8205
9944
  });
@@ -8208,7 +9947,7 @@ async function stageSync(deps) {
8208
9947
  config,
8209
9948
  settings.autoSync,
8210
9949
  deps.trigger,
8211
- active2.length > 0 ? {
9950
+ historical.length > 0 && dayScansComplete ? {
8212
9951
  aggregateVersion: MEASURED_DAYS_V1,
8213
9952
  utcOffsetMinutes: workflow?.utcOffsetMinutes ?? machineUtcOffsetMinutes(),
8214
9953
  days: days.send
@@ -8232,8 +9971,8 @@ async function stageSync(deps) {
8232
9971
  width: process.stdout.columns
8233
9972
  };
8234
9973
  let blockedReason = null;
8235
- if (built.length === 0) {
8236
- blockedReason = `No active harness on this machine - no Claude Code and no Codex transcript from the last ${windowDays} days to read.`;
9974
+ if (historical.length === 0) {
9975
+ blockedReason = `No supported harness transcript from the last ${retentionDays} days to read.`;
8237
9976
  } else if (token === null) {
8238
9977
  blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
8239
9978
  } else if (config.stack === null) {
@@ -8251,25 +9990,47 @@ async function stageSync(deps) {
8251
9990
  stagedAt: now,
8252
9991
  blockedReason,
8253
9992
  days,
8254
- prices
9993
+ prices,
9994
+ ...acknowledgePublish ? { acknowledgePublish } : {}
8255
9995
  };
8256
9996
  }
8257
9997
 
8258
9998
  // src/autosync/run.ts
8259
- var SYNC_LOG_FILE = join10(homedir13(), ".config", "aistack", "sync.log");
9999
+ var SYNC_LOG_FILE = join12(homedir19(), ".config", "aistack", "sync.log");
8260
10000
  var SYNC_LOG_MAX_LINES = 200;
8261
10001
  var FIX_COMMAND = "npx @use-aistack/cli sync";
8262
10002
  var REVOKED_RESULT = "off - auto-sync is switched off for this stack";
8263
10003
  function appendLogLine(file, line) {
8264
- mkdirSync5(dirname7(file), { recursive: true });
10004
+ mkdirSync8(dirname9(file), { recursive: true });
8265
10005
  appendFileSync(file, `${line}
8266
10006
  `);
8267
- const lines2 = readFileSync11(file, "utf-8").split("\n").filter(Boolean);
10007
+ const lines2 = readFileSync14(file, "utf-8").split("\n").filter(Boolean);
8268
10008
  if (lines2.length > SYNC_LOG_MAX_LINES) {
8269
- writeFileSync5(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
10009
+ writeFileSync8(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
8270
10010
  `);
8271
10011
  }
8272
10012
  }
10013
+ function reserveAttempt(file, now, windowMs) {
10014
+ mkdirSync8(dirname9(file), { recursive: true });
10015
+ for (; ; ) {
10016
+ try {
10017
+ const fd = openSync(file, "wx");
10018
+ writeFileSync8(fd, String(now));
10019
+ closeSync(fd);
10020
+ return true;
10021
+ } catch (error) {
10022
+ if (error.code !== "EEXIST") throw error;
10023
+ const held = Number(readFileSync14(file, "utf-8"));
10024
+ if (Number.isFinite(held) && now - held < windowMs) return false;
10025
+ try {
10026
+ unlinkSync2(file);
10027
+ } catch (unlinkError) {
10028
+ if (unlinkError.code !== "ENOENT")
10029
+ throw unlinkError;
10030
+ }
10031
+ }
10032
+ }
10033
+ }
8273
10034
  async function runAutoSync(deps) {
8274
10035
  const now = (deps.now ?? Date.now)();
8275
10036
  const settingsFile = deps.settingsFile;
@@ -8284,9 +10045,13 @@ async function runAutoSync(deps) {
8284
10045
  return;
8285
10046
  }
8286
10047
  const frequencyHours = normalizeFrequencyHours(config.frequencyHours);
10048
+ const windowMs = frequencyHours * 36e5;
8287
10049
  const state = settings.autoSyncState ?? {};
8288
10050
  const lastRunAt = state.lastRunAt ?? 0;
8289
- if (now - lastRunAt < frequencyHours * 36e5) return;
10051
+ if (now - lastRunAt < windowMs) return;
10052
+ const reservationFile = deps.reservationFile ?? `${settingsFile ?? join12(homedir19(), ".config", "aistack", "settings.json")}.auto-sync-attempt`;
10053
+ if (!reserveAttempt(reservationFile, now, windowMs)) return;
10054
+ saveSettings({ autoSyncState: { ...state, lastRunAt: now } }, settingsFile);
8290
10055
  const stage = deps.stageImpl ?? stageSync;
8291
10056
  const publish = deps.publishImpl ?? syncPublish;
8292
10057
  const loadConfig = deps.loadConfigImpl ?? loadSyncConfig;
@@ -8377,7 +10142,7 @@ async function runAutoSync(deps) {
8377
10142
  logFile,
8378
10143
  `${stamp} fail (${consecutiveFailures} in a row) - ${failure2}`
8379
10144
  );
8380
- if (shouldWarn) {
10145
+ if (shouldWarn && deps.suppressOutput !== true && process.env.AISTACK_HOOK_SOURCE !== "grok" && process.env.AISTACK_HOOK_SOURCE !== "cursor") {
8381
10146
  emit(
8382
10147
  JSON.stringify({
8383
10148
  systemMessage: `aistack auto-sync failed ${consecutiveFailures} times in a row (${failure2}). Run \`${FIX_COMMAND}\` in a terminal to fix it, or \`${FIX_COMMAND} --auto off\` to stop these runs.`
@@ -8522,6 +10287,7 @@ async function syncCommand(options = {}) {
8522
10287
  s.start("Publishing");
8523
10288
  try {
8524
10289
  const res = await syncPublish(staged.token, staged.bodyJson);
10290
+ staged.acknowledgePublish?.();
8525
10291
  s.stop("Published");
8526
10292
  const lines2 = [
8527
10293
  `Snapshot received ${fmtReceivedAt(res.receivedAt)}`,
@@ -8752,6 +10518,7 @@ function createSyncServer(deps, send) {
8752
10518
  log7(`consent received, sending stage ${approvedStage.id}`);
8753
10519
  publish(approvedStage.token, approvedStage.bodyJson).then(
8754
10520
  (res) => {
10521
+ approvedStage.acknowledgePublish?.();
8755
10522
  if (staged?.id === approvedStage.id) staged = null;
8756
10523
  const lines2 = [
8757
10524
  `Published. Snapshot received ${fmtReceivedAt(res.receivedAt)}.`,
@@ -8869,7 +10636,7 @@ program.command("mcp").description(
8869
10636
  });
8870
10637
  program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").option(
8871
10638
  "--auto [state]",
8872
- "silent background sync; 'on' asks your stack for the permission and installs the SessionStart hooks, 'off' revokes both"
10639
+ "silent background sync; 'on' asks your stack for the permission and installs harness hooks, 'off' revokes both"
8873
10640
  ).option(
8874
10641
  "--every <hours>",
8875
10642
  "with --auto on: hours between auto-syncs (default 6)"