@use-aistack/cli 0.14.0 → 0.16.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
@@ -377,12 +377,12 @@ function apiEquivalentCost(modelKey, t, atMs) {
377
377
  }
378
378
 
379
379
  // src/version.ts
380
- var CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
380
+ var CLI_VERSION = true ? "0.16.0" : "0.0.0-dev";
381
381
 
382
382
  // src/api.ts
383
383
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
384
- async function request(path9, options = {}) {
385
- return fetch(`${BASE_URL}${path9}`, {
384
+ async function request(path14, options = {}) {
385
+ return fetch(`${BASE_URL}${path14}`, {
386
386
  ...options,
387
387
  headers: {
388
388
  "Content-Type": "application/json",
@@ -800,9 +800,9 @@ function canonicalizeRepoUrl(input) {
800
800
  function repoNameFromCanonical(canonical) {
801
801
  return parseRepo(canonical)?.repo ?? "";
802
802
  }
803
- function normalizeUpstreamPath(path9) {
804
- if (!path9) return "";
805
- return path9.split("/").filter(Boolean).join("/");
803
+ function normalizeUpstreamPath(path14) {
804
+ if (!path14) return "";
805
+ return path14.split("/").filter(Boolean).join("/");
806
806
  }
807
807
 
808
808
  // src/git.ts
@@ -848,16 +848,16 @@ function buildRepoLinkResource(canonical) {
848
848
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
849
849
  import { homedir as homedir2 } from "node:os";
850
850
  import { join as join2 } from "node:path";
851
- function readJson(path9) {
851
+ function readJson(path14) {
852
852
  try {
853
- if (!existsSync2(path9)) return null;
854
- return JSON.parse(readFileSync2(path9, "utf-8"));
853
+ if (!existsSync2(path14)) return null;
854
+ return JSON.parse(readFileSync2(path14, "utf-8"));
855
855
  } catch {
856
856
  return null;
857
857
  }
858
858
  }
859
- function hooksFrom(path9, source, out, seen) {
860
- const hooks = readJson(path9)?.hooks;
859
+ function hooksFrom(path14, source, out, seen) {
860
+ const hooks = readJson(path14)?.hooks;
861
861
  if (!hooks || typeof hooks !== "object") return;
862
862
  for (const [event, config] of Object.entries(hooks)) {
863
863
  const stableKey = `hooks:${source}:${event}`;
@@ -1006,16 +1006,16 @@ function buildMcpResource(name, group, pkg) {
1006
1006
  pkg
1007
1007
  };
1008
1008
  }
1009
- function readText(path9) {
1009
+ function readText(path14) {
1010
1010
  try {
1011
- if (!existsSync3(path9)) return null;
1012
- return readFileSync3(path9, "utf-8");
1011
+ if (!existsSync3(path14)) return null;
1012
+ return readFileSync3(path14, "utf-8");
1013
1013
  } catch {
1014
1014
  return null;
1015
1015
  }
1016
1016
  }
1017
- function readParsed(path9, parse2) {
1018
- const raw = readText(path9);
1017
+ function readParsed(path14, parse2) {
1018
+ const raw = readText(path14);
1019
1019
  if (raw === null) return null;
1020
1020
  try {
1021
1021
  return parse2(raw);
@@ -1023,14 +1023,14 @@ function readParsed(path9, parse2) {
1023
1023
  return null;
1024
1024
  }
1025
1025
  }
1026
- function readJson2(path9) {
1027
- return readParsed(path9, JSON.parse);
1026
+ function readJson2(path14) {
1027
+ return readParsed(path14, JSON.parse);
1028
1028
  }
1029
- function readYaml(path9) {
1030
- return readParsed(path9, parseYaml);
1029
+ function readYaml(path14) {
1030
+ return readParsed(path14, parseYaml);
1031
1031
  }
1032
- function readToml(path9) {
1033
- return readParsed(path9, parseToml);
1032
+ function readToml(path14) {
1033
+ return readParsed(path14, parseToml);
1034
1034
  }
1035
1035
  function continueListToMap(file) {
1036
1036
  if (!file?.mcpServers?.length) return void 0;
@@ -1169,8 +1169,8 @@ function resolveSource(entry, mpRepoUrl) {
1169
1169
  const src = entry.source;
1170
1170
  if (typeof src === "string") {
1171
1171
  if (!mpRepoUrl) return null;
1172
- const path9 = src.replace(/^\.\//, "").replace(/\/+$/, "");
1173
- return { url: mpRepoUrl, path: path9 || void 0 };
1172
+ const path14 = src.replace(/^\.\//, "").replace(/\/+$/, "");
1173
+ return { url: mpRepoUrl, path: path14 || void 0 };
1174
1174
  }
1175
1175
  if (src && typeof src === "object" && src.url) {
1176
1176
  return { url: src.url, path: src.path, sha: src.sha };
@@ -1207,10 +1207,10 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
1207
1207
  }
1208
1208
  return out;
1209
1209
  }
1210
- function readJson3(path9) {
1210
+ function readJson3(path14) {
1211
1211
  try {
1212
- if (!existsSync4(path9)) return null;
1213
- return JSON.parse(readFileSync4(path9, "utf-8"));
1212
+ if (!existsSync4(path14)) return null;
1213
+ return JSON.parse(readFileSync4(path14, "utf-8"));
1214
1214
  } catch {
1215
1215
  return null;
1216
1216
  }
@@ -1299,8 +1299,8 @@ function loadGitignore(cwd) {
1299
1299
  }
1300
1300
  function readFileSafe(filePath) {
1301
1301
  try {
1302
- const stat7 = statSync(filePath);
1303
- if (stat7.size > MAX_FILE_SIZE) return null;
1302
+ const stat8 = statSync(filePath);
1303
+ if (stat8.size > MAX_FILE_SIZE) return null;
1304
1304
  return readFileSync5(filePath, "utf-8");
1305
1305
  } catch {
1306
1306
  return null;
@@ -1519,9 +1519,9 @@ function lines(items) {
1519
1519
  console.log(`${BAR} ${item}`);
1520
1520
  }
1521
1521
  }
1522
- function section(label, count) {
1522
+ function section(label, count2) {
1523
1523
  console.log(`${BAR}`);
1524
- const countStr = count !== void 0 ? ` ${dim(String(count))}` : "";
1524
+ const countStr = count2 !== void 0 ? ` ${dim(String(count2))}` : "";
1525
1525
  console.log(`${BAR} ${bold(label.toUpperCase())}${countStr}`);
1526
1526
  }
1527
1527
  function divider() {
@@ -1844,7 +1844,7 @@ function diffResources(current, existing) {
1844
1844
  // src/commands/connect.ts
1845
1845
  import { spawnSync } from "node:child_process";
1846
1846
  import { cpSync, existsSync as existsSync6 } from "node:fs";
1847
- import { homedir as homedir11 } from "node:os";
1847
+ import { homedir as homedir14 } from "node:os";
1848
1848
  import { dirname as dirname3, join as join6 } from "node:path";
1849
1849
  import { fileURLToPath } from "node:url";
1850
1850
  import * as p3 from "@clack/prompts";
@@ -2442,7 +2442,7 @@ function filterAtoms(atoms, sets) {
2442
2442
  }
2443
2443
  merged.set(published, (merged.get(published) ?? 0) + atom.count);
2444
2444
  }
2445
- const allowed = [...merged].map(([name, count]) => ({ name, count }));
2445
+ const allowed = [...merged].map(([name, count2]) => ({ name, count: count2 }));
2446
2446
  allowed.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2447
2447
  keptPrivate.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2448
2448
  return { allowed, keptPrivate, withheld: keptPrivate.length };
@@ -3199,6 +3199,7 @@ var HANDOFF_MARKERS = {
3199
3199
  "mcp__curia__request_review"
3200
3200
  ],
3201
3201
  codex: ["request_user_input"],
3202
+ cursor: ["ask_question"],
3202
3203
  "grok-build": ["ask_user_question", "request_user_input"],
3203
3204
  opencode: ["question"],
3204
3205
  "pi-mono": []
@@ -3633,13 +3634,13 @@ function canonicalJson(value) {
3633
3634
  function fnv1a64(text) {
3634
3635
  const prime = 0x100000001b3n;
3635
3636
  const mask = 0xffffffffffffffffn;
3636
- let hash = 0xcbf29ce484222325n;
3637
+ let hash2 = 0xcbf29ce484222325n;
3637
3638
  const bytes = new TextEncoder().encode(text);
3638
3639
  for (const byte of bytes) {
3639
- hash ^= BigInt(byte);
3640
- hash = hash * prime & mask;
3640
+ hash2 ^= BigInt(byte);
3641
+ hash2 = hash2 * prime & mask;
3641
3642
  }
3642
- return hash.toString(16).padStart(16, "0");
3643
+ return hash2.toString(16).padStart(16, "0");
3643
3644
  }
3644
3645
  function dayFingerprint(day) {
3645
3646
  return fnv1a64(
@@ -3732,7 +3733,7 @@ function sanitizeModelId(id) {
3732
3733
  var round4 = (n) => Math.round(n * 1e4) / 1e4;
3733
3734
  var round2 = (n) => Math.round(n * 100) / 100;
3734
3735
  var utcDate = (ms) => new Date(ms).toISOString().slice(0, 10);
3735
- var toAtoms = (pairs) => pairs.map(([name, count]) => ({ name, count }));
3736
+ var toAtoms = (pairs) => pairs.map(([name, count2]) => ({ name, count: count2 }));
3736
3737
  function buildCategory(observed, curated, optIns, denominator) {
3737
3738
  const publishable = /* @__PURE__ */ new Set([...curated, ...optIns]);
3738
3739
  const {
@@ -4025,7 +4026,7 @@ var bump2 = (map, key2, amount = 1) => {
4025
4026
  var utcDateOf2 = (ms) => new Date(ms).toISOString().slice(0, 10);
4026
4027
  function asBuckets(map, field) {
4027
4028
  return [...map].map(
4028
- ([bucket, count]) => ({ bucket, [field]: count })
4029
+ ([bucket, count2]) => ({ bucket, [field]: count2 })
4029
4030
  ).sort((a, b) => a.bucket - b.bucket);
4030
4031
  }
4031
4032
  var PHASE_RANK = {
@@ -4410,7 +4411,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
4410
4411
  0
4411
4412
  );
4412
4413
  const unknown = attributed === 0 ? 0 : windowPhaseSec.unknown / attributed;
4413
- const routesModels = harness === "claude-code" || harness === "opencode" || harness === "grok-build";
4414
+ const routesModels = harness === "claude-code" || harness === "opencode" || harness === "grok-build" || harness === "cursor";
4414
4415
  const asRows = (map) => {
4415
4416
  const safe = /* @__PURE__ */ new Map();
4416
4417
  for (const [model, tokens] of map) {
@@ -4433,7 +4434,7 @@ function createHarnessWorkflowReducer(harness, localSources = createWorkflowLoca
4433
4434
  date,
4434
4435
  harness,
4435
4436
  sessions: day.sessions,
4436
- 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),
4437
4438
  ...day.phase.sessions > 0 ? {
4438
4439
  phase: {
4439
4440
  ruleVersion: PHASE_RULES_V1,
@@ -4561,12 +4562,12 @@ function ingestRecord(agg, raw, ctx) {
4561
4562
  const sessionId = asStr(rec.sessionId);
4562
4563
  if (sessionId) agg.sessions.add(sessionId);
4563
4564
  let tsMs = null;
4564
- const timestamp = asStr(rec.timestamp);
4565
- if (timestamp) {
4566
- const ts = Date.parse(timestamp);
4565
+ const timestamp2 = asStr(rec.timestamp);
4566
+ if (timestamp2) {
4567
+ const ts = Date.parse(timestamp2);
4567
4568
  if (!Number.isNaN(ts)) {
4568
4569
  tsMs = ts;
4569
- agg.activeDays.add(timestamp.slice(0, 10));
4570
+ agg.activeDays.add(timestamp2.slice(0, 10));
4570
4571
  agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
4571
4572
  agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
4572
4573
  }
@@ -4859,8 +4860,8 @@ function applyContribution(agg, c, sign) {
4859
4860
  agg.webFetchRequests += sign * c.webFetch;
4860
4861
  agg.fallbackAttempts += sign * c.fallbackAttempts;
4861
4862
  agg.untypedMirrors += sign * c.untypedMirrors;
4862
- for (const [type, count] of c.mirroredIterationTypes) {
4863
- bump(agg.mirroredIterationTypes, type, sign * count);
4863
+ for (const [type, count2] of c.mirroredIterationTypes) {
4864
+ bump(agg.mirroredIterationTypes, type, sign * count2);
4864
4865
  }
4865
4866
  }
4866
4867
  function ingestContentBlocks(agg, content) {
@@ -5126,9 +5127,9 @@ function ingestLine(agg, raw, state, sinceMs) {
5126
5127
  if (!rec) return;
5127
5128
  agg.records++;
5128
5129
  let tsMs = null;
5129
- const timestamp = asStr(rec.timestamp);
5130
- if (timestamp) {
5131
- const ts = Date.parse(timestamp);
5130
+ const timestamp2 = asStr(rec.timestamp);
5131
+ if (timestamp2) {
5132
+ const ts = Date.parse(timestamp2);
5132
5133
  if (!Number.isNaN(ts)) tsMs = ts;
5133
5134
  }
5134
5135
  const inWindow = sinceMs === void 0 || tsMs !== null && tsMs >= sinceMs;
@@ -5151,8 +5152,8 @@ function ingestLine(agg, raw, state, sinceMs) {
5151
5152
  state.sawResponse = true;
5152
5153
  }
5153
5154
  if (!inWindow) return;
5154
- if (tsMs !== null && timestamp) {
5155
- agg.activeDays.add(timestamp.slice(0, 10));
5155
+ if (tsMs !== null && timestamp2) {
5156
+ agg.activeDays.add(timestamp2.slice(0, 10));
5156
5157
  agg.firstTs = agg.firstTs === null ? tsMs : Math.min(agg.firstTs, tsMs);
5157
5158
  agg.lastTs = agg.lastTs === null ? tsMs : Math.max(agg.lastTs, tsMs);
5158
5159
  }
@@ -5440,11 +5441,11 @@ function ingestWithRetry(agg, file, opts) {
5440
5441
  }
5441
5442
  }
5442
5443
  function ingestFile2(agg, file, opts) {
5443
- const readFile2 = opts.readFileImpl ?? readFileSync6;
5444
+ const readFile4 = opts.readFileImpl ?? readFileSync6;
5444
5445
  let text;
5445
5446
  if (file.endsWith(".zst")) {
5446
5447
  if (zstdDecompress === null) throw readError("zstd-unsupported");
5447
- const raw = readFile2(file);
5448
+ const raw = readFile4(file);
5448
5449
  try {
5449
5450
  text = zstdDecompress(
5450
5451
  Buffer.isBuffer(raw) ? raw : Buffer.from(raw)
@@ -5453,7 +5454,7 @@ function ingestFile2(agg, file, opts) {
5453
5454
  throw readError("zstd-corrupt");
5454
5455
  }
5455
5456
  } else {
5456
- text = readFile2(file).toString("utf8");
5457
+ text = readFile4(file).toString("utf8");
5457
5458
  }
5458
5459
  const records = [];
5459
5460
  let nonEmptyLines = 0;
@@ -5556,6 +5557,817 @@ var codexAdapter = {
5556
5557
  }
5557
5558
  };
5558
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
+
5559
6371
  // src/harness/grok/analyzer.ts
5560
6372
  function createAggregate4() {
5561
6373
  const workflowLocal = createWorkflowLocalSources();
@@ -5572,41 +6384,41 @@ var createGrokEventState = (parentSession) => ({
5572
6384
  var bump3 = (map, key2) => {
5573
6385
  map.set(key2, (map.get(key2) ?? 0) + 1);
5574
6386
  };
5575
- var toolMetadata = (update) => {
5576
- const meta = asObj(update._meta);
6387
+ var toolMetadata = (update2) => {
6388
+ const meta = asObj(update2._meta);
5577
6389
  return meta && asObj(meta["x.ai/tool"]);
5578
6390
  };
5579
6391
  function ingestUpdate(agg, state, value, projectDir, sinceMs) {
5580
6392
  const root = asObj(value);
5581
6393
  const params = root && asObj(root.params);
5582
- const update = params && asObj(params.update);
6394
+ const update2 = params && asObj(params.update);
5583
6395
  const session = params && asStr(params.sessionId);
5584
6396
  const tsMs = timestampMs(
5585
6397
  asObj(params?._meta)?.agentTimestampMs ?? root?.timestamp
5586
6398
  );
5587
- if (!update || !session || tsMs === null) return;
5588
- const kind = asStr(update.sessionUpdate);
6399
+ if (!update2 || !session || tsMs === null) return;
6400
+ const kind = asStr(update2.sessionUpdate);
5589
6401
  if (kind === "tool_call") {
5590
- const id = asStr(update.toolCallId);
5591
- const metadata = toolMetadata(update);
5592
- const name = asName(metadata?.name ?? update.toolName);
6402
+ const id = asStr(update2.toolCallId);
6403
+ const metadata = toolMetadata(update2);
6404
+ const name = asName(metadata?.name ?? update2.toolName);
5593
6405
  if (!id || !name || state.tools.has(id)) return;
5594
- const raw = asObj(update.input);
6406
+ const raw = asObj(update2.input);
5595
6407
  const arg = asStr(raw?.command ?? raw?.query ?? raw?.skill ?? raw?.name);
5596
6408
  state.tools.set(id, { name, ...arg ? { arg } : {}, tsMs });
5597
6409
  return;
5598
6410
  }
5599
6411
  if (sinceMs !== void 0 && tsMs < sinceMs) return;
5600
6412
  if (kind === "tool_call_update") {
5601
- const id = asStr(update.toolCallId);
5602
- if (!id || asStr(update.status) !== "completed") return;
6413
+ const id = asStr(update2.toolCallId);
6414
+ if (!id || asStr(update2.status) !== "completed") return;
5603
6415
  completeTool(agg, state, id, session, projectDir, tsMs);
5604
6416
  return;
5605
6417
  }
5606
6418
  if (kind !== "turn_completed") return;
5607
- const usage = asObj(update.usage);
6419
+ const usage = asObj(update2.usage);
5608
6420
  if (!usage) return;
5609
- const prompt = asStr(update.prompt_id) ?? `turn:${tsMs}`;
6421
+ const prompt = asStr(update2.prompt_id) ?? `turn:${tsMs}`;
5610
6422
  for (const [model, raw] of Object.entries(asObj(usage.modelUsage) ?? {})) {
5611
6423
  const row = asObj(raw);
5612
6424
  agg.workflow.ingest({
@@ -5620,7 +6432,7 @@ function ingestUpdate(agg, state, value, projectDir, sinceMs) {
5620
6432
  thinkingTokens: asNum(row?.reasoningTokens),
5621
6433
  responseTokens: asNum(row?.outputTokens),
5622
6434
  routingTokens: asNum(row?.outputTokens),
5623
- ...asNum(row?.apiDurationMs) > 0 ? { durationSec: asNum(row?.apiDurationMs) / 1e3 } : asNum(update.elapsed_ms) > 0 ? { durationSec: asNum(update.elapsed_ms) / 1e3 } : {}
6435
+ ...asNum(row?.apiDurationMs) > 0 ? { durationSec: asNum(row?.apiDurationMs) / 1e3 } : asNum(update2.elapsed_ms) > 0 ? { durationSec: asNum(update2.elapsed_ms) / 1e3 } : {}
5624
6436
  });
5625
6437
  }
5626
6438
  agg.workflow.ingest({
@@ -5630,7 +6442,7 @@ function ingestUpdate(agg, state, value, projectDir, sinceMs) {
5630
6442
  parentSession: state.parentSession,
5631
6443
  tsMs,
5632
6444
  turnId: prompt,
5633
- questionBack: asStr(update.stop_reason) === "question"
6445
+ questionBack: asStr(update2.stop_reason) === "question"
5634
6446
  });
5635
6447
  }
5636
6448
  function completeTool(agg, state, id, session, projectDir, tsMs) {
@@ -5750,10 +6562,10 @@ function sidecarContributions(value, projectDir) {
5750
6562
  function terminalContribution(value, projectDir) {
5751
6563
  const root = asObj(value);
5752
6564
  const params = root && asObj(root.params);
5753
- const update = params && asObj(params.update);
6565
+ const update2 = params && asObj(params.update);
5754
6566
  const meta = params && asObj(params._meta);
5755
- if (!update || asStr(update.sessionUpdate) !== "turn_completed") return null;
5756
- const usage = asObj(update.usage);
6567
+ if (!update2 || asStr(update2.sessionUpdate) !== "turn_completed") return null;
6568
+ const usage = asObj(update2.usage);
5757
6569
  const sessionId = params && asStr(params.sessionId);
5758
6570
  const tsMs = timestampMs(meta?.agentTimestampMs ?? root?.timestamp);
5759
6571
  if (!usage || !sessionId || tsMs === null) return null;
@@ -5766,7 +6578,7 @@ function terminalContribution(value, projectDir) {
5766
6578
  sessionId,
5767
6579
  projectDir,
5768
6580
  tsMs,
5769
- ...asNum(update.elapsed_ms) > 0 ? { durationMs: asNum(update.elapsed_ms) } : {},
6581
+ ...asNum(update2.elapsed_ms) > 0 ? { durationMs: asNum(update2.elapsed_ms) } : {},
5770
6582
  models
5771
6583
  };
5772
6584
  }
@@ -5798,15 +6610,129 @@ function ingestContribution(agg, row) {
5798
6610
 
5799
6611
  // src/harness/grok/scan.ts
5800
6612
  import { createReadStream as createReadStream2 } from "node:fs";
5801
- import { readdir as readdir4, readFile, stat as stat4 } from "node:fs/promises";
5802
- import { homedir as homedir8 } from "node:os";
5803
- import path4 from "node:path";
6613
+ import { readdir as readdir5, readFile as readFile3, stat as stat5 } from "node:fs/promises";
6614
+ import { homedir as homedir11 } from "node:os";
6615
+ import path9 from "node:path";
5804
6616
  import readline2 from "node:readline";
5805
6617
  import { parse as parseToml3 } from "smol-toml";
6618
+
6619
+ // src/harness/grok/context.ts
6620
+ import { createHash as createHash2 } from "node:crypto";
6621
+ import { appendFile, mkdir as mkdir2, readdir as readdir4, readFile as readFile2, unlink } from "node:fs/promises";
6622
+ import { homedir as homedir10 } from "node:os";
6623
+ import path8 from "node:path";
6624
+ var DAY_MS = 864e5;
6625
+ var hash = (value) => createHash2("sha256").update(value).digest("hex");
6626
+ function contextCall(value) {
6627
+ const row = asObj(value);
6628
+ if (row?.src !== "shell" || row.msg !== "shell.turn.inference_done")
6629
+ return null;
6630
+ const session = asStr(row.sid);
6631
+ const ts = asStr(row.ts);
6632
+ const ctx = asObj(row.ctx);
6633
+ const tokens = ctx?.prompt_tokens;
6634
+ const loop = ctx?.loop_index;
6635
+ const tsMs = ts ? Date.parse(ts) : NaN;
6636
+ if (!session || !Number.isFinite(tsMs) || typeof tokens !== "number" || !Number.isSafeInteger(tokens) || tokens < 0 || typeof loop !== "number" || !Number.isSafeInteger(loop) || loop < 0)
6637
+ return null;
6638
+ return {
6639
+ id: hash(JSON.stringify([session, tsMs, row.pid ?? null, loop, tokens])),
6640
+ session,
6641
+ tsMs,
6642
+ tokens
6643
+ };
6644
+ }
6645
+ function contextCacheDirectory(root) {
6646
+ return path8.join(
6647
+ homedir10(),
6648
+ ".config",
6649
+ "aistack",
6650
+ "grok-context",
6651
+ hash(path8.resolve(root))
6652
+ );
6653
+ }
6654
+ async function optionalRead(file) {
6655
+ try {
6656
+ return await readFile2(file, "utf8");
6657
+ } catch (error) {
6658
+ if (error.code === "ENOENT") return "";
6659
+ throw error;
6660
+ }
6661
+ }
6662
+ async function retainedContextCalls(root, sessions, cacheDir = contextCacheDirectory(root), now = Date.now()) {
6663
+ const cutoff = now - 400 * DAY_MS;
6664
+ const calls = /* @__PURE__ */ new Map();
6665
+ let files = [];
6666
+ try {
6667
+ files = await readdir4(cacheDir);
6668
+ } catch (error) {
6669
+ if (error.code !== "ENOENT") throw error;
6670
+ }
6671
+ for (const file of files) {
6672
+ if (!/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file)) continue;
6673
+ if (Date.parse(file.slice(0, 10)) + DAY_MS < cutoff) {
6674
+ await unlink(path8.join(cacheDir, file)).catch(
6675
+ (error) => {
6676
+ if (error.code !== "ENOENT") throw error;
6677
+ }
6678
+ );
6679
+ continue;
6680
+ }
6681
+ const raw2 = await optionalRead(path8.join(cacheDir, file));
6682
+ if (raw2 && !raw2.endsWith("\n"))
6683
+ throw new Error("Incomplete Grok context cache");
6684
+ for (const line of raw2.split("\n").filter(Boolean)) {
6685
+ const call = asObj(JSON.parse(line));
6686
+ if (!call || typeof call.id !== "string" || !/^[a-f0-9]{64}$/.test(call.id) || typeof call.session !== "string" || typeof call.tsMs !== "number" || !Number.isFinite(call.tsMs) || typeof call.tokens !== "number" || !Number.isSafeInteger(call.tokens) || call.tokens < 0)
6687
+ throw new Error("Invalid Grok context cache");
6688
+ if (call.tsMs >= cutoff && call.tsMs <= now)
6689
+ calls.set(call.id, {
6690
+ id: call.id,
6691
+ session: call.session,
6692
+ tsMs: call.tsMs,
6693
+ tokens: call.tokens
6694
+ });
6695
+ }
6696
+ }
6697
+ const raw = await optionalRead(
6698
+ path8.join(root, "..", "logs", "unified.jsonl")
6699
+ );
6700
+ const additions = /* @__PURE__ */ new Map();
6701
+ for (const line of raw.slice(0, raw.lastIndexOf("\n") + 1).split("\n")) {
6702
+ let value;
6703
+ try {
6704
+ value = JSON.parse(line);
6705
+ } catch {
6706
+ continue;
6707
+ }
6708
+ const call = contextCall(value);
6709
+ if (!call || !sessions.has(call.session) || call.tsMs < cutoff || call.tsMs > now || calls.has(call.id))
6710
+ continue;
6711
+ calls.set(call.id, call);
6712
+ const date = new Date(call.tsMs).toISOString().slice(0, 10);
6713
+ const lines2 = additions.get(date) ?? [];
6714
+ lines2.push(JSON.stringify(call));
6715
+ additions.set(date, lines2);
6716
+ }
6717
+ if (additions.size) {
6718
+ await mkdir2(cacheDir, { recursive: true, mode: 448 });
6719
+ for (const [date, lines2] of additions) {
6720
+ await appendFile(
6721
+ path8.join(cacheDir, `${date}.jsonl`),
6722
+ `${lines2.join("\n")}
6723
+ `,
6724
+ { mode: 384 }
6725
+ );
6726
+ }
6727
+ }
6728
+ return [...calls.values()].filter((call) => sessions.has(call.session)).sort((a, b) => a.tsMs - b.tsMs || a.id.localeCompare(b.id));
6729
+ }
6730
+
6731
+ // src/harness/grok/scan.ts
5806
6732
  function sessionRoots() {
5807
6733
  return [
5808
- path4.join(
5809
- process.env.GROK_HOME || path4.join(homedir8(), ".grok"),
6734
+ path9.join(
6735
+ process.env.GROK_HOME || path9.join(homedir11(), ".grok"),
5810
6736
  "sessions"
5811
6737
  )
5812
6738
  ];
@@ -5818,7 +6744,7 @@ async function stableRead(file, jsonl) {
5818
6744
  for (const delay of delays) {
5819
6745
  if (delay) await pause(delay);
5820
6746
  try {
5821
- const before = await stat4(file);
6747
+ const before = await stat5(file);
5822
6748
  if (!before.isFile()) return { complete: false };
5823
6749
  if (jsonl) {
5824
6750
  const lines2 = [];
@@ -5834,12 +6760,12 @@ async function stableRead(file, jsonl) {
5834
6760
  lines2.push(null);
5835
6761
  }
5836
6762
  }
5837
- const after = await stat4(file);
6763
+ const after = await stat5(file);
5838
6764
  if (before.size === after.size && before.mtimeMs === after.mtimeMs)
5839
6765
  return { lines: lines2, complete: true };
5840
6766
  } else {
5841
- const raw = await readFile(file, "utf8");
5842
- const after = await stat4(file);
6767
+ const raw = await readFile3(file, "utf8");
6768
+ const after = await stat5(file);
5843
6769
  if (before.size === after.size && before.mtimeMs === after.mtimeMs)
5844
6770
  return { json: JSON.parse(raw), complete: true };
5845
6771
  }
@@ -5850,16 +6776,16 @@ async function stableRead(file, jsonl) {
5850
6776
  }
5851
6777
  async function directories(root) {
5852
6778
  try {
5853
- const workspaces = await readdir4(root, { withFileTypes: true });
6779
+ const workspaces = await readdir5(root, { withFileTypes: true });
5854
6780
  const out = [];
5855
6781
  for (const workspace of workspaces) {
5856
6782
  if (!workspace.isDirectory() || workspace.isSymbolicLink()) continue;
5857
- const workspacePath = path4.join(root, workspace.name);
5858
- for (const session of await readdir4(workspacePath, {
6783
+ const workspacePath = path9.join(root, workspace.name);
6784
+ for (const session of await readdir5(workspacePath, {
5859
6785
  withFileTypes: true
5860
6786
  })) {
5861
6787
  if (session.isDirectory() && !session.isSymbolicLink())
5862
- out.push(path4.join(workspacePath, session.name));
6788
+ out.push(path9.join(workspacePath, session.name));
5863
6789
  }
5864
6790
  }
5865
6791
  return out.sort();
@@ -5871,7 +6797,7 @@ async function modelAliasesForRoot(root) {
5871
6797
  if (process.env.GROK_MODELS_BASE_URL) return /* @__PURE__ */ new Map();
5872
6798
  try {
5873
6799
  const parsed = asObj(
5874
- parseToml3(await readFile(path4.join(root, "..", "config.toml"), "utf8"))
6800
+ parseToml3(await readFile3(path9.join(root, "..", "config.toml"), "utf8"))
5875
6801
  );
5876
6802
  if (!parsed || asObj(parsed.endpoints)?.models_base_url) return /* @__PURE__ */ new Map();
5877
6803
  const models = asObj(parsed.model);
@@ -5887,13 +6813,14 @@ async function modelAliasesForRoot(root) {
5887
6813
  return /* @__PURE__ */ new Map();
5888
6814
  }
5889
6815
  }
5890
- async function scan3(agg, opts = {}) {
6816
+ async function scan4(agg, opts = {}) {
5891
6817
  const stats = emptyScanStats();
5892
6818
  const sessionDates = /* @__PURE__ */ new Map();
5893
6819
  const candidates = /* @__PURE__ */ new Map();
5894
6820
  let complete = true;
5895
6821
  for (const root of opts.roots ?? sessionRoots()) {
5896
6822
  const aliases = await modelAliasesForRoot(root);
6823
+ const contextSessions = /* @__PURE__ */ new Map();
5897
6824
  const dirs = await directories(root);
5898
6825
  if (dirs === null) {
5899
6826
  complete = false;
@@ -5902,7 +6829,7 @@ async function scan3(agg, opts = {}) {
5902
6829
  for (const dir of dirs) {
5903
6830
  let entries;
5904
6831
  try {
5905
- entries = await readdir4(dir, { withFileTypes: true });
6832
+ entries = await readdir5(dir, { withFileTypes: true });
5906
6833
  } catch {
5907
6834
  complete = false;
5908
6835
  continue;
@@ -5910,41 +6837,46 @@ async function scan3(agg, opts = {}) {
5910
6837
  const files = new Map(
5911
6838
  entries.filter(
5912
6839
  (e) => e.isFile() && (isGrokEvidenceFile(e.name) || e.name === "summary.json")
5913
- ).map((e) => [e.name, path4.join(dir, e.name)])
6840
+ ).map((e) => [e.name, path9.join(dir, e.name)])
5914
6841
  );
5915
6842
  let projectDir = dir;
5916
- let sessionFallback = path4.basename(dir);
6843
+ let sessionFallback = path9.basename(dir);
5917
6844
  let child = false;
5918
6845
  let parentSession;
5919
6846
  const summary = files.get("summary.json");
5920
6847
  if (summary) {
5921
- const read2 = await stableRead(summary, false);
5922
- if (!read2.complete) {
6848
+ const read3 = await stableRead(summary, false);
6849
+ if (!read3.complete) {
5923
6850
  complete = false;
5924
6851
  stats.filesUnreadable++;
5925
6852
  continue;
5926
6853
  }
5927
- const value = asObj(read2.json);
6854
+ const value = asObj(read3.json);
5928
6855
  const info = value && asObj(value.info);
5929
6856
  projectDir = asStr(info?.cwd) ?? asStr(value?.cwd) ?? dir;
5930
6857
  sessionFallback = asStr(value?.sessionId) ?? sessionFallback;
5931
6858
  parentSession = asStr(value?.parentSessionId) ?? asStr(value?.parent_session_id) ?? asStr(info?.parentSessionId) ?? asStr(info?.parent_session_id) ?? void 0;
5932
6859
  child = parentSession !== void 0;
5933
6860
  }
6861
+ contextSessions.set(sessionFallback, { projectDir, parentSession });
5934
6862
  let rows = [];
5935
6863
  let precedence = 0;
5936
6864
  const usage = files.get("usage.json");
5937
6865
  if (usage) {
5938
6866
  stats.filesFound++;
5939
- const read2 = await stableRead(usage, false);
5940
- if (!read2.complete) {
6867
+ const read3 = await stableRead(usage, false);
6868
+ if (!read3.complete) {
5941
6869
  complete = false;
5942
6870
  stats.filesUnreadable++;
5943
6871
  continue;
5944
6872
  }
5945
6873
  stats.filesRead++;
5946
- rows = sidecarContributions(read2.json, projectDir);
5947
- if (rows.length > 0) precedence = 2;
6874
+ rows = sidecarContributions(read3.json, projectDir);
6875
+ if (rows.length > 0) {
6876
+ precedence = 2;
6877
+ for (const row of rows)
6878
+ contextSessions.set(row.sessionId, { projectDir, parentSession });
6879
+ }
5948
6880
  }
5949
6881
  if (child) {
5950
6882
  rows = [];
@@ -5955,14 +6887,14 @@ async function scan3(agg, opts = {}) {
5955
6887
  if (updates) {
5956
6888
  const useTerminalUsage = rows.length === 0 && !child;
5957
6889
  stats.filesFound++;
5958
- const read2 = await stableRead(updates, true);
5959
- if (!read2.complete) {
6890
+ const read3 = await stableRead(updates, true);
6891
+ if (!read3.complete) {
5960
6892
  complete = false;
5961
6893
  stats.filesUnreadable++;
5962
6894
  continue;
5963
6895
  }
5964
6896
  stats.filesRead++;
5965
- for (const value of read2.lines ?? []) {
6897
+ for (const value of read3.lines ?? []) {
5966
6898
  if (value === null) {
5967
6899
  agg.parseErrors++;
5968
6900
  continue;
@@ -5978,14 +6910,14 @@ async function scan3(agg, opts = {}) {
5978
6910
  const events = files.get("events.jsonl");
5979
6911
  if (events) {
5980
6912
  stats.filesFound++;
5981
- const read2 = await stableRead(events, true);
5982
- if (!read2.complete) {
6913
+ const read3 = await stableRead(events, true);
6914
+ if (!read3.complete) {
5983
6915
  complete = false;
5984
6916
  stats.filesUnreadable++;
5985
6917
  continue;
5986
6918
  }
5987
6919
  stats.filesRead++;
5988
- for (const value of read2.lines ?? []) {
6920
+ for (const value of read3.lines ?? []) {
5989
6921
  if (value === null) agg.parseErrors++;
5990
6922
  else
5991
6923
  ingestEvent2(
@@ -6017,6 +6949,32 @@ async function scan3(agg, opts = {}) {
6017
6949
  }
6018
6950
  opts.onProgress?.(stats.filesFound);
6019
6951
  }
6952
+ try {
6953
+ const calls = await retainedContextCalls(
6954
+ root,
6955
+ new Set(contextSessions.keys()),
6956
+ opts.contextCacheDir
6957
+ );
6958
+ for (const call of calls) {
6959
+ if (opts.sinceMs !== void 0 && call.tsMs < opts.sinceMs) continue;
6960
+ const source = contextSessions.get(call.session);
6961
+ agg.workflow.ingest({
6962
+ type: "response",
6963
+ session: call.session,
6964
+ projectWorkspace: source?.projectDir,
6965
+ parentSession: source?.parentSession,
6966
+ tsMs: call.tsMs,
6967
+ responseId: `context:${call.id}`,
6968
+ contextTokens: call.tokens
6969
+ });
6970
+ const dates = sessionDates.get(call.session) ?? /* @__PURE__ */ new Set();
6971
+ dates.add(new Date(call.tsMs).toISOString().slice(0, 10));
6972
+ sessionDates.set(call.session, dates);
6973
+ }
6974
+ } catch {
6975
+ complete = false;
6976
+ stats.filesUnreadable++;
6977
+ }
6020
6978
  }
6021
6979
  for (const { rows } of candidates.values()) {
6022
6980
  for (const row of rows) {
@@ -6048,7 +7006,7 @@ var grokAdapter = {
6048
7006
  ),
6049
7007
  async scan(opts) {
6050
7008
  const aggregate = createAggregate4();
6051
- const result = await scan3(aggregate, opts);
7009
+ const result = await scan4(aggregate, opts);
6052
7010
  return {
6053
7011
  aggregate,
6054
7012
  stats: result.stats,
@@ -6244,14 +7202,14 @@ function noteConfiguredMcpServers2(agg, state, serverNames) {
6244
7202
 
6245
7203
  // src/harness/opencode/scan.ts
6246
7204
  import { readFileSync as readFileSync7 } from "node:fs";
6247
- import { readdir as readdir5, stat as stat5 } from "node:fs/promises";
6248
- import { homedir as homedir9 } from "node:os";
6249
- import path5 from "node:path";
7205
+ import { readdir as readdir6, stat as stat6 } from "node:fs/promises";
7206
+ import { homedir as homedir12 } from "node:os";
7207
+ import path10 from "node:path";
6250
7208
  var OPENCODE_MIGRATION_CEILING = 20260622202450;
6251
7209
  function opencodeDataDirs() {
6252
7210
  const xdg = process.env.XDG_DATA_HOME;
6253
- const base = xdg || path5.join(homedir9(), ".local", "share");
6254
- return [path5.join(base, "opencode")];
7211
+ const base = xdg || path10.join(homedir12(), ".local", "share");
7212
+ return [path10.join(base, "opencode")];
6255
7213
  }
6256
7214
  function isStoreFile(basename2) {
6257
7215
  return basename2 === "opencode.db" || /^opencode-[^/]+\.db$/.test(basename2);
@@ -6260,8 +7218,8 @@ async function dbFilesIn(root) {
6260
7218
  const override = process.env.OPENCODE_DB;
6261
7219
  if (override) return [override];
6262
7220
  try {
6263
- const entries = await readdir5(root, { withFileTypes: true });
6264
- return entries.filter((e) => e.isFile() && isStoreFile(e.name)).map((e) => path5.join(root, e.name)).sort();
7221
+ const entries = await readdir6(root, { withFileTypes: true });
7222
+ return entries.filter((e) => e.isFile() && isStoreFile(e.name)).map((e) => path10.join(root, e.name)).sort();
6265
7223
  } catch {
6266
7224
  return [];
6267
7225
  }
@@ -6281,7 +7239,7 @@ function errorClass2(e) {
6281
7239
  return e instanceof Error ? e.constructor.name : "unknown";
6282
7240
  }
6283
7241
  var readError2 = (reason) => Object.assign(new Error(reason), { code: reason });
6284
- async function scan4(agg, opts = {}) {
7242
+ async function scan5(agg, opts = {}) {
6285
7243
  const stats = emptyScanStats();
6286
7244
  const open2 = await loadSqlite();
6287
7245
  const sinceMs = opts.sinceMs ?? 0;
@@ -6298,7 +7256,7 @@ async function scan4(agg, opts = {}) {
6298
7256
  } catch (e) {
6299
7257
  stats.filesUnreadable++;
6300
7258
  stats.unreadableFiles.push({
6301
- path: path5.basename(file),
7259
+ path: path10.basename(file),
6302
7260
  reason: errorClass2(e)
6303
7261
  });
6304
7262
  }
@@ -6465,8 +7423,8 @@ function pickTs(jsonTs, columnTs) {
6465
7423
  }
6466
7424
  function opencodeConfigFile() {
6467
7425
  const xdg = process.env.XDG_CONFIG_HOME;
6468
- const base = xdg || path5.join(homedir9(), ".config");
6469
- return path5.join(base, "opencode", "opencode.json");
7426
+ const base = xdg || path10.join(homedir12(), ".config");
7427
+ return path10.join(base, "opencode", "opencode.json");
6470
7428
  }
6471
7429
  function readConfiguredMcpServers2(agg, state, configFile) {
6472
7430
  const file = configFile ?? opencodeConfigFile();
@@ -6518,7 +7476,7 @@ async function detectOpencode(opts) {
6518
7476
  if (open2 === null) return false;
6519
7477
  for (const root of opts.roots ?? opencodeDataDirs()) {
6520
7478
  for (const file of await dbFilesIn(root)) {
6521
- if (!await exists3(file)) continue;
7479
+ if (!await exists4(file)) continue;
6522
7480
  let db = null;
6523
7481
  try {
6524
7482
  db = open2(file);
@@ -6538,9 +7496,9 @@ async function detectOpencode(opts) {
6538
7496
  }
6539
7497
  return false;
6540
7498
  }
6541
- async function exists3(p8) {
7499
+ async function exists4(p8) {
6542
7500
  try {
6543
- await stat5(p8);
7501
+ await stat6(p8);
6544
7502
  return true;
6545
7503
  } catch {
6546
7504
  return false;
@@ -6560,7 +7518,7 @@ var opencodeAdapter = {
6560
7518
  },
6561
7519
  async scan(opts) {
6562
7520
  const aggregate = createAggregate5();
6563
- const stats = await scan4(aggregate, {
7521
+ const stats = await scan5(aggregate, {
6564
7522
  sinceMs: opts.sinceMs,
6565
7523
  ...opts.onProgress ? { onProgress: opts.onProgress } : {}
6566
7524
  });
@@ -6768,17 +7726,17 @@ function readCounts2(usageRaw) {
6768
7726
 
6769
7727
  // src/harness/pi/scan.ts
6770
7728
  import { createReadStream as createReadStream3 } from "node:fs";
6771
- import { readdir as readdir6, realpath as realpath3, stat as stat6 } from "node:fs/promises";
6772
- import { homedir as homedir10 } from "node:os";
6773
- import path6 from "node:path";
7729
+ import { readdir as readdir7, realpath as realpath3, stat as stat7 } from "node:fs/promises";
7730
+ import { homedir as homedir13 } from "node:os";
7731
+ import path11 from "node:path";
6774
7732
  import readline3 from "node:readline";
6775
7733
  var MAX_SESSION_VERSION = 3;
6776
7734
  function piAgentDir() {
6777
- return process.env.PI_CODING_AGENT_DIR || path6.join(homedir10(), ".pi", "agent");
7735
+ return process.env.PI_CODING_AGENT_DIR || path11.join(homedir13(), ".pi", "agent");
6778
7736
  }
6779
7737
  function sessionRoots2() {
6780
7738
  const override = process.env.PI_CODING_AGENT_SESSION_DIR;
6781
- return [override || path6.join(piAgentDir(), "sessions")];
7739
+ return [override || path11.join(piAgentDir(), "sessions")];
6782
7740
  }
6783
7741
  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$/;
6784
7742
  function isSessionFile(basename2) {
@@ -6787,22 +7745,22 @@ function isSessionFile(basename2) {
6787
7745
  async function* walkSessions(dir) {
6788
7746
  let entries;
6789
7747
  try {
6790
- entries = await readdir6(dir, { withFileTypes: true });
7748
+ entries = await readdir7(dir, { withFileTypes: true });
6791
7749
  } catch {
6792
7750
  return;
6793
7751
  }
6794
7752
  for (const e of entries) {
6795
- const full = path6.join(dir, e.name);
7753
+ const full = path11.join(dir, e.name);
6796
7754
  if (e.isDirectory()) yield* walkSessions(full);
6797
7755
  else if (e.isFile() && isSessionFile(e.name)) yield full;
6798
7756
  }
6799
7757
  }
6800
- async function scan5(agg, opts = {}) {
7758
+ async function scan6(agg, opts = {}) {
6801
7759
  const stats = emptyScanStats();
6802
7760
  const visited = /* @__PURE__ */ new Set();
6803
7761
  const fold = createFoldState();
6804
7762
  for (const root of opts.roots ?? sessionRoots2()) {
6805
- if (!await exists4(root)) continue;
7763
+ if (!await exists5(root)) continue;
6806
7764
  for await (const file of walkSessions(root)) {
6807
7765
  stats.filesFound++;
6808
7766
  let resolved;
@@ -6818,7 +7776,7 @@ async function scan5(agg, opts = {}) {
6818
7776
  visited.add(resolved);
6819
7777
  if (opts.sinceMs !== void 0) {
6820
7778
  try {
6821
- const st = await stat6(file);
7779
+ const st = await stat7(file);
6822
7780
  if (st.mtimeMs < opts.sinceMs) {
6823
7781
  stats.filesSkippedByMtime++;
6824
7782
  continue;
@@ -6840,7 +7798,7 @@ async function scan5(agg, opts = {}) {
6840
7798
  stats.filesUnreadable++;
6841
7799
  stats.filesRead--;
6842
7800
  stats.unreadableFiles.push({
6843
- path: path6.relative(root, file),
7801
+ path: path11.relative(root, file),
6844
7802
  reason: "version-too-new"
6845
7803
  });
6846
7804
  }
@@ -6848,7 +7806,7 @@ async function scan5(agg, opts = {}) {
6848
7806
  stats.filesUnreadable++;
6849
7807
  stats.filesRead--;
6850
7808
  stats.unreadableFiles.push({
6851
- path: path6.relative(root, file),
7809
+ path: path11.relative(root, file),
6852
7810
  reason: "read-error"
6853
7811
  });
6854
7812
  }
@@ -6856,9 +7814,9 @@ async function scan5(agg, opts = {}) {
6856
7814
  }
6857
7815
  return stats;
6858
7816
  }
6859
- async function exists4(p8) {
7817
+ async function exists5(p8) {
6860
7818
  try {
6861
- await stat6(p8);
7819
+ await stat7(p8);
6862
7820
  return true;
6863
7821
  } catch {
6864
7822
  return false;
@@ -6926,7 +7884,7 @@ var piAdapter = {
6926
7884
  },
6927
7885
  async scan(opts) {
6928
7886
  const aggregate = createAggregate6();
6929
- const stats = await scan5(aggregate, {
7887
+ const stats = await scan6(aggregate, {
6930
7888
  sinceMs: opts.sinceMs,
6931
7889
  ...opts.onProgress ? { onProgress: opts.onProgress } : {}
6932
7890
  });
@@ -6944,12 +7902,14 @@ var HARNESS_ADAPTERS = [
6944
7902
  claudeAdapter,
6945
7903
  codexAdapter,
6946
7904
  grokAdapter,
7905
+ cursorAdapter,
6947
7906
  opencodeAdapter,
6948
7907
  piAdapter
6949
7908
  ];
6950
7909
  function harnessLabel2(name) {
6951
7910
  if (name === CLAUDE_HARNESS_NAME) return "Claude Code";
6952
7911
  if (name === CODEX_HARNESS_NAME) return "Codex";
7912
+ if (name === CURSOR_HARNESS_NAME) return "Cursor";
6953
7913
  if (name === GROK_HARNESS_NAME) return "Grok Build";
6954
7914
  if (name === OPENCODE_HARNESS_NAME) return "opencode";
6955
7915
  if (name === PI_HARNESS_NAME) return "Pi";
@@ -6984,7 +7944,7 @@ var MCP_ADD_ARGS = [
6984
7944
  "mcp"
6985
7945
  ];
6986
7946
  var MCP_REMOVE_ARGS = ["mcp", "remove", "--scope", "user", "aistack"];
6987
- var SKILL_DEST = join6(homedir11(), ".claude", "skills", "aistack-sync");
7947
+ var SKILL_DEST = join6(homedir14(), ".claude", "skills", "aistack-sync");
6988
7948
  function runClaude(args) {
6989
7949
  const r = spawnSync("claude", args, { encoding: "utf-8" });
6990
7950
  const notFound = r.error !== void 0 && r.error.code === "ENOENT";
@@ -7217,9 +8177,9 @@ async function createCommand() {
7217
8177
  import { hostname } from "node:os";
7218
8178
  import * as p5 from "@clack/prompts";
7219
8179
  import open from "open";
7220
- function proposedMachineName(read2 = hostname) {
8180
+ function proposedMachineName(read3 = hostname) {
7221
8181
  try {
7222
- const name = read2().trim().replace(/\.local$/i, "");
8182
+ const name = read3().trim().replace(/\.local$/i, "");
7223
8183
  if (!name || name.length > 64) return void 0;
7224
8184
  return name;
7225
8185
  } catch {
@@ -7306,13 +8266,13 @@ async function loginCommand(options = {}) {
7306
8266
  import * as p7 from "@clack/prompts";
7307
8267
 
7308
8268
  // src/autosync/codexHook.ts
7309
- import { createHash } from "node:crypto";
8269
+ import { createHash as createHash3 } from "node:crypto";
7310
8270
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
7311
- import { homedir as homedir12 } from "node:os";
8271
+ import { homedir as homedir15 } from "node:os";
7312
8272
  import { dirname as dirname5, join as join8 } from "node:path";
7313
8273
  import { parse } from "smol-toml";
7314
8274
  function codexHome2() {
7315
- return process.env.CODEX_HOME || join8(homedir12(), ".codex");
8275
+ return process.env.CODEX_HOME || join8(homedir15(), ".codex");
7316
8276
  }
7317
8277
  function codexHooksFile() {
7318
8278
  return join8(codexHome2(), "hooks.json");
@@ -7338,9 +8298,9 @@ function readHooksJson(file) {
7338
8298
  }
7339
8299
  var CODEX_TRUST_INSTRUCTION = "Codex hook written - open Codex and run /hooks once to trust it, or it will not run.";
7340
8300
  function installCodexAutoSyncHook(file = codexHooksFile()) {
7341
- const read2 = readHooksJson(file);
7342
- if ("error" in read2) return { ok: false, message: read2.error };
7343
- const settings = read2.settings;
8301
+ const read3 = readHooksJson(file);
8302
+ if ("error" in read3) return { ok: false, message: read3.error };
8303
+ const settings = read3.settings;
7344
8304
  const hooks = settings.hooks ?? {};
7345
8305
  const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
7346
8306
  const kept = sessionStart.map((m) => ({
@@ -7360,9 +8320,9 @@ function installCodexAutoSyncHook(file = codexHooksFile()) {
7360
8320
  function removeCodexAutoSyncHook(file = codexHooksFile()) {
7361
8321
  if (!existsSync8(file))
7362
8322
  return { ok: true, message: "no Codex hook to remove" };
7363
- const read2 = readHooksJson(file);
7364
- if ("error" in read2) return { ok: false, message: read2.error };
7365
- const settings = read2.settings;
8323
+ const read3 = readHooksJson(file);
8324
+ if ("error" in read3) return { ok: false, message: read3.error };
8325
+ const settings = read3.settings;
7366
8326
  const sessionStart = settings.hooks?.SessionStart;
7367
8327
  if (!Array.isArray(sessionStart)) {
7368
8328
  return { ok: true, message: "no Codex hook to remove" };
@@ -7387,18 +8347,18 @@ function removeCodexAutoSyncHook(file = codexHooksFile()) {
7387
8347
  return { ok: true, message: `hook removed from ${file}` };
7388
8348
  }
7389
8349
  function codexAutoSyncHookInstalled(file = codexHooksFile()) {
7390
- const read2 = readHooksJson(file);
7391
- if ("error" in read2) return false;
7392
- const sessionStart = read2.settings.hooks?.SessionStart;
8350
+ const read3 = readHooksJson(file);
8351
+ if ("error" in read3) return false;
8352
+ const sessionStart = read3.settings.hooks?.SessionStart;
7393
8353
  if (!Array.isArray(sessionStart)) return false;
7394
8354
  return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs(h)));
7395
8355
  }
7396
8356
  function codexHookTrusted(configFile = codexConfigFile(), hooksFile = codexHooksFile()) {
7397
8357
  try {
7398
8358
  const config = parse(readFileSync9(configFile, "utf-8"));
7399
- const read2 = readHooksJson(hooksFile);
7400
- if ("error" in read2) return null;
7401
- const sessionStart = read2.settings.hooks?.SessionStart;
8359
+ const read3 = readHooksJson(hooksFile);
8360
+ if ("error" in read3) return null;
8361
+ const sessionStart = read3.settings.hooks?.SessionStart;
7402
8362
  if (!Array.isArray(sessionStart)) return false;
7403
8363
  const matches = [];
7404
8364
  for (const [groupIndex, group] of sessionStart.entries()) {
@@ -7421,7 +8381,7 @@ function codexHookTrusted(configFile = codexConfigFile(), hooksFile = codexHooks
7421
8381
  hooks: [normalizedHandler]
7422
8382
  };
7423
8383
  if (typeof group.matcher === "string") identity.matcher = group.matcher;
7424
- const currentHash = `sha256:${createHash("sha256").update(JSON.stringify(canonicalJson2(identity))).digest("hex")}`;
8384
+ const currentHash = `sha256:${createHash3("sha256").update(JSON.stringify(canonicalJson2(identity))).digest("hex")}`;
7425
8385
  const key2 = `${hooksFile}:session_start:${groupIndex}:${handlerIndex}`;
7426
8386
  matches.push(config.hooks?.state?.[key2]?.trusted_hash === currentHash);
7427
8387
  }
@@ -7451,23 +8411,177 @@ function canonicalJson2(value) {
7451
8411
  // src/autosync/optin.ts
7452
8412
  import * as p6 from "@clack/prompts";
7453
8413
 
8414
+ // src/autosync/cursorHook.ts
8415
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
8416
+ import { homedir as homedir17, platform as platform2 } from "node:os";
8417
+ import { dirname as dirname7, join as join10 } from "node:path";
8418
+
8419
+ // src/autosync/hook.ts
8420
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
8421
+ import { homedir as homedir16 } from "node:os";
8422
+ import { dirname as dirname6, join as join9 } from "node:path";
8423
+ var CLAUDE_SETTINGS_FILE = join9(homedir16(), ".claude", "settings.json");
8424
+ var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
8425
+ function isOurs2(entry) {
8426
+ return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
8427
+ }
8428
+ function readClaudeSettings(file) {
8429
+ if (!existsSync9(file)) return { settings: {} };
8430
+ try {
8431
+ const raw = JSON.parse(readFileSync10(file, "utf-8"));
8432
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
8433
+ return { settings: raw };
8434
+ }
8435
+ return { error: `${file} does not hold a JSON object` };
8436
+ } catch {
8437
+ return { error: `${file} is not valid JSON - fix it, then retry` };
8438
+ }
8439
+ }
8440
+ function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
8441
+ const read3 = readClaudeSettings(file);
8442
+ if ("error" in read3) return { ok: false, message: read3.error };
8443
+ const settings = read3.settings;
8444
+ const hooks = settings.hooks ?? {};
8445
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
8446
+ const kept = sessionStart.map((m) => ({
8447
+ ...m,
8448
+ hooks: (m.hooks ?? []).filter((h) => !isOurs2(h))
8449
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
8450
+ kept.push({
8451
+ hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
8452
+ });
8453
+ settings.hooks = { ...hooks, SessionStart: kept };
8454
+ mkdirSync4(dirname6(file), { recursive: true });
8455
+ writeFileSync4(file, `${JSON.stringify(settings, null, 2)}
8456
+ `);
8457
+ return { ok: true, message: `SessionStart hook written to ${file}` };
8458
+ }
8459
+ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
8460
+ if (!existsSync9(file)) return { ok: true, message: "no hook to remove" };
8461
+ const read3 = readClaudeSettings(file);
8462
+ if ("error" in read3) return { ok: false, message: read3.error };
8463
+ const settings = read3.settings;
8464
+ const sessionStart = settings.hooks?.SessionStart;
8465
+ if (!Array.isArray(sessionStart)) {
8466
+ return { ok: true, message: "no hook to remove" };
8467
+ }
8468
+ const kept = sessionStart.map((m) => ({
8469
+ ...m,
8470
+ hooks: (m.hooks ?? []).filter((h) => !isOurs2(h))
8471
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
8472
+ const hooks = { ...settings.hooks };
8473
+ if (kept.length > 0) {
8474
+ hooks.SessionStart = kept;
8475
+ } else {
8476
+ delete hooks.SessionStart;
8477
+ }
8478
+ if (Object.keys(hooks).length > 0) {
8479
+ settings.hooks = hooks;
8480
+ } else {
8481
+ delete settings.hooks;
8482
+ }
8483
+ writeFileSync4(file, `${JSON.stringify(settings, null, 2)}
8484
+ `);
8485
+ return { ok: true, message: `hook removed from ${file}` };
8486
+ }
8487
+ function autoSyncHookInstalled(file = CLAUDE_SETTINGS_FILE) {
8488
+ const read3 = readClaudeSettings(file);
8489
+ if ("error" in read3) return false;
8490
+ const sessionStart = read3.settings.hooks?.SessionStart;
8491
+ if (!Array.isArray(sessionStart)) return false;
8492
+ return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs2(h)));
8493
+ }
8494
+
8495
+ // src/autosync/cursorHook.ts
8496
+ var CURSOR_HOOK_FILE = join10(homedir17(), ".cursor", "hooks.json");
8497
+ function cursorHookCommand(os = platform2()) {
8498
+ if (os === "win32") {
8499
+ 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"'`;
8500
+ return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
8501
+ }
8502
+ const detach = os === "darwin" ? "nohup" : "setsid nohup";
8503
+ return `${detach} sh -c 'export AISTACK_HOOK_SOURCE=cursor; ${AUTO_SYNC_HOOK_COMMAND}' </dev/null >/dev/null 2>&1 &`;
8504
+ }
8505
+ function read(file) {
8506
+ if (!existsSync10(file)) return {};
8507
+ const value = JSON.parse(readFileSync11(file, "utf8"));
8508
+ if (!value || typeof value !== "object" || Array.isArray(value))
8509
+ throw new Error("expected a JSON object");
8510
+ const config = value;
8511
+ if (config.version !== void 0 && config.version !== 1)
8512
+ throw new Error("unsupported hooks version");
8513
+ if (config.hooks !== void 0 && (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks) || Object.values(config.hooks).some(
8514
+ (entries) => !Array.isArray(entries) || entries.some(
8515
+ (entry) => !entry || typeof entry !== "object" || Array.isArray(entry)
8516
+ )
8517
+ )))
8518
+ throw new Error("malformed hooks object");
8519
+ return config;
8520
+ }
8521
+ function isOurs3(entry) {
8522
+ return ["linux", "darwin", "win32"].some(
8523
+ (os) => entry.command === cursorHookCommand(os)
8524
+ );
8525
+ }
8526
+ function update(file, install, os) {
8527
+ try {
8528
+ const config = read(file);
8529
+ const existing = config.hooks?.stop ?? [];
8530
+ const kept = existing.filter((entry) => !isOurs3(entry));
8531
+ if (!install && kept.length === existing.length)
8532
+ return { ok: true, message: "no Cursor hook to remove" };
8533
+ if (install) kept.push({ command: cursorHookCommand(os) });
8534
+ const hooks = { ...config.hooks };
8535
+ if (kept.length) hooks.stop = kept;
8536
+ else delete hooks.stop;
8537
+ if (Object.keys(hooks).length) config.hooks = hooks;
8538
+ else delete config.hooks;
8539
+ if (install) config.version = 1;
8540
+ mkdirSync5(dirname7(file), { recursive: true });
8541
+ writeFileSync5(file, `${JSON.stringify(config, null, 2)}
8542
+ `);
8543
+ return {
8544
+ ok: true,
8545
+ message: `Cursor stop hook ${install ? "written to" : "removed from"} ${file}`
8546
+ };
8547
+ } catch (error) {
8548
+ return {
8549
+ ok: false,
8550
+ message: `Could not ${install ? "install" : "remove"} Cursor hook in ${file}: ${error instanceof Error ? error.message : String(error)}`
8551
+ };
8552
+ }
8553
+ }
8554
+ function installCursorAutoSyncHook(file = CURSOR_HOOK_FILE, os = platform2()) {
8555
+ return update(file, true, os);
8556
+ }
8557
+ function removeCursorAutoSyncHook(file = CURSOR_HOOK_FILE) {
8558
+ return update(file, false, platform2());
8559
+ }
8560
+ function cursorAutoSyncHookInstalled(file = CURSOR_HOOK_FILE) {
8561
+ try {
8562
+ return (read(file).hooks?.stop ?? []).some(isOurs3);
8563
+ } catch {
8564
+ return false;
8565
+ }
8566
+ }
8567
+
7454
8568
  // src/autosync/grokHook.ts
7455
8569
  import {
7456
- existsSync as existsSync9,
7457
- mkdirSync as mkdirSync4,
7458
- readFileSync as readFileSync10,
8570
+ existsSync as existsSync11,
8571
+ mkdirSync as mkdirSync6,
8572
+ readFileSync as readFileSync12,
7459
8573
  unlinkSync,
7460
- writeFileSync as writeFileSync4
8574
+ writeFileSync as writeFileSync6
7461
8575
  } from "node:fs";
7462
- import { homedir as homedir13, platform as platform2 } from "node:os";
7463
- import { dirname as dirname6, join as join9 } from "node:path";
7464
- var GROK_HOOK_FILE = join9(
7465
- process.env.GROK_HOME || join9(homedir13(), ".grok"),
8576
+ import { homedir as homedir18, platform as platform3 } from "node:os";
8577
+ import { dirname as dirname8, join as join11 } from "node:path";
8578
+ var GROK_HOOK_FILE = join11(
8579
+ process.env.GROK_HOME || join11(homedir18(), ".grok"),
7466
8580
  "hooks",
7467
8581
  "aistack.json"
7468
8582
  );
7469
8583
  var SYNC_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
7470
- function grokHookCommand(os = platform2()) {
8584
+ function grokHookCommand(os = platform3()) {
7471
8585
  if (os === "win32") {
7472
8586
  return `Start-Process -WindowStyle Hidden -FilePath "cmd.exe" -ArgumentList '/d /s /c "set AISTACK_HOOK_SOURCE=grok&& (${SYNC_COMMAND}) >NUL 2>&1"'`;
7473
8587
  }
@@ -7477,9 +8591,9 @@ function grokHookCommand(os = platform2()) {
7477
8591
  return `setsid nohup sh -c 'export AISTACK_HOOK_SOURCE=grok; ${SYNC_COMMAND}' >/dev/null 2>&1 &`;
7478
8592
  }
7479
8593
  function readHookFile(file) {
7480
- if (!existsSync9(file)) return { value: {} };
8594
+ if (!existsSync11(file)) return { value: {} };
7481
8595
  try {
7482
- const value = JSON.parse(readFileSync10(file, "utf-8"));
8596
+ const value = JSON.parse(readFileSync12(file, "utf-8"));
7483
8597
  if (value && typeof value === "object" && !Array.isArray(value)) {
7484
8598
  const candidate = value;
7485
8599
  if (candidate.hooks !== void 0 && (!candidate.hooks || typeof candidate.hooks !== "object" || Array.isArray(candidate.hooks) || Object.values(candidate.hooks).some(
@@ -7496,19 +8610,19 @@ function readHookFile(file) {
7496
8610
  }
7497
8611
  return { error: `${file} does not hold a JSON object` };
7498
8612
  }
7499
- function isOurs2(entry) {
8613
+ function isOurs4(entry) {
7500
8614
  return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
7501
8615
  }
7502
- function installGrokAutoSyncHook(file = GROK_HOOK_FILE, os = platform2()) {
7503
- const read2 = readHookFile(file);
7504
- if ("error" in read2) return { ok: false, message: read2.error };
7505
- const foreignKeys = Object.keys(read2.value).filter((key2) => key2 !== "hooks");
7506
- const foreignEvents = Object.keys(read2.value.hooks ?? {}).filter(
8616
+ function installGrokAutoSyncHook(file = GROK_HOOK_FILE, os = platform3()) {
8617
+ const read3 = readHookFile(file);
8618
+ if ("error" in read3) return { ok: false, message: read3.error };
8619
+ const foreignKeys = Object.keys(read3.value).filter((key2) => key2 !== "hooks");
8620
+ const foreignEvents = Object.keys(read3.value.hooks ?? {}).filter(
7507
8621
  (key2) => key2 !== "SessionStart"
7508
8622
  );
7509
- const sessionStart = read2.value.hooks?.SessionStart ?? [];
8623
+ const sessionStart = read3.value.hooks?.SessionStart ?? [];
7510
8624
  const foreignHandlers = sessionStart.flatMap(
7511
- (group) => (group.hooks ?? []).filter((entry) => !isOurs2(entry))
8625
+ (group) => (group.hooks ?? []).filter((entry) => !isOurs4(entry))
7512
8626
  );
7513
8627
  if (foreignKeys.length > 0 || foreignEvents.length > 0 || foreignHandlers.length > 0) {
7514
8628
  return {
@@ -7527,8 +8641,8 @@ function installGrokAutoSyncHook(file = GROK_HOOK_FILE, os = platform2()) {
7527
8641
  ]
7528
8642
  }
7529
8643
  };
7530
- mkdirSync4(dirname6(file), { recursive: true });
7531
- writeFileSync4(file, `${JSON.stringify(value, null, 2)}
8644
+ mkdirSync6(dirname8(file), { recursive: true });
8645
+ writeFileSync6(file, `${JSON.stringify(value, null, 2)}
7532
8646
  `);
7533
8647
  return {
7534
8648
  ok: true,
@@ -7536,15 +8650,15 @@ function installGrokAutoSyncHook(file = GROK_HOOK_FILE, os = platform2()) {
7536
8650
  };
7537
8651
  }
7538
8652
  function removeGrokAutoSyncHook(file = GROK_HOOK_FILE) {
7539
- if (!existsSync9(file))
8653
+ if (!existsSync11(file))
7540
8654
  return { ok: true, message: "no Grok Build hook to remove" };
7541
- const read2 = readHookFile(file);
7542
- if ("error" in read2) return { ok: false, message: read2.error };
7543
- const entries = read2.value.hooks?.SessionStart ?? [];
7544
- const onlyOurs = Object.keys(read2.value).every((key2) => key2 === "hooks") && Object.keys(read2.value.hooks ?? {}).every(
8655
+ const read3 = readHookFile(file);
8656
+ if ("error" in read3) return { ok: false, message: read3.error };
8657
+ const entries = read3.value.hooks?.SessionStart ?? [];
8658
+ const onlyOurs = Object.keys(read3.value).every((key2) => key2 === "hooks") && Object.keys(read3.value.hooks ?? {}).every(
7545
8659
  (key2) => key2 === "SessionStart"
7546
8660
  ) && entries.every(
7547
- (group) => (group.hooks ?? []).every((entry) => isOurs2(entry))
8661
+ (group) => (group.hooks ?? []).every((entry) => isOurs4(entry))
7548
8662
  );
7549
8663
  if (!onlyOurs) {
7550
8664
  return {
@@ -7556,89 +8670,13 @@ function removeGrokAutoSyncHook(file = GROK_HOOK_FILE) {
7556
8670
  return { ok: true, message: `Grok Build hook removed from ${file}` };
7557
8671
  }
7558
8672
  function grokAutoSyncHookInstalled(file = GROK_HOOK_FILE) {
7559
- const read2 = readHookFile(file);
7560
- if ("error" in read2) return false;
7561
- return (read2.value.hooks?.SessionStart ?? []).some(
7562
- (group) => (group.hooks ?? []).some((entry) => isOurs2(entry))
8673
+ const read3 = readHookFile(file);
8674
+ if ("error" in read3) return false;
8675
+ return (read3.value.hooks?.SessionStart ?? []).some(
8676
+ (group) => (group.hooks ?? []).some((entry) => isOurs4(entry))
7563
8677
  );
7564
8678
  }
7565
8679
 
7566
- // src/autosync/hook.ts
7567
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
7568
- import { homedir as homedir14 } from "node:os";
7569
- import { dirname as dirname7, join as join10 } from "node:path";
7570
- var CLAUDE_SETTINGS_FILE = join10(homedir14(), ".claude", "settings.json");
7571
- var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
7572
- function isOurs3(entry) {
7573
- return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
7574
- }
7575
- function readClaudeSettings(file) {
7576
- if (!existsSync10(file)) return { settings: {} };
7577
- try {
7578
- const raw = JSON.parse(readFileSync11(file, "utf-8"));
7579
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
7580
- return { settings: raw };
7581
- }
7582
- return { error: `${file} does not hold a JSON object` };
7583
- } catch {
7584
- return { error: `${file} is not valid JSON - fix it, then retry` };
7585
- }
7586
- }
7587
- function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
7588
- const read2 = readClaudeSettings(file);
7589
- if ("error" in read2) return { ok: false, message: read2.error };
7590
- const settings = read2.settings;
7591
- const hooks = settings.hooks ?? {};
7592
- const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
7593
- const kept = sessionStart.map((m) => ({
7594
- ...m,
7595
- hooks: (m.hooks ?? []).filter((h) => !isOurs3(h))
7596
- })).filter((m) => (m.hooks?.length ?? 0) > 0);
7597
- kept.push({
7598
- hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
7599
- });
7600
- settings.hooks = { ...hooks, SessionStart: kept };
7601
- mkdirSync5(dirname7(file), { recursive: true });
7602
- writeFileSync5(file, `${JSON.stringify(settings, null, 2)}
7603
- `);
7604
- return { ok: true, message: `SessionStart hook written to ${file}` };
7605
- }
7606
- function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
7607
- if (!existsSync10(file)) return { ok: true, message: "no hook to remove" };
7608
- const read2 = readClaudeSettings(file);
7609
- if ("error" in read2) return { ok: false, message: read2.error };
7610
- const settings = read2.settings;
7611
- const sessionStart = settings.hooks?.SessionStart;
7612
- if (!Array.isArray(sessionStart)) {
7613
- return { ok: true, message: "no hook to remove" };
7614
- }
7615
- const kept = sessionStart.map((m) => ({
7616
- ...m,
7617
- hooks: (m.hooks ?? []).filter((h) => !isOurs3(h))
7618
- })).filter((m) => (m.hooks?.length ?? 0) > 0);
7619
- const hooks = { ...settings.hooks };
7620
- if (kept.length > 0) {
7621
- hooks.SessionStart = kept;
7622
- } else {
7623
- delete hooks.SessionStart;
7624
- }
7625
- if (Object.keys(hooks).length > 0) {
7626
- settings.hooks = hooks;
7627
- } else {
7628
- delete settings.hooks;
7629
- }
7630
- writeFileSync5(file, `${JSON.stringify(settings, null, 2)}
7631
- `);
7632
- return { ok: true, message: `hook removed from ${file}` };
7633
- }
7634
- function autoSyncHookInstalled(file = CLAUDE_SETTINGS_FILE) {
7635
- const read2 = readClaudeSettings(file);
7636
- if ("error" in read2) return false;
7637
- const sessionStart = read2.settings.hooks?.SessionStart;
7638
- if (!Array.isArray(sessionStart)) return false;
7639
- return sessionStart.some((m) => (m.hooks ?? []).some((h) => isOurs3(h)));
7640
- }
7641
-
7642
8680
  // src/autosync/optin.ts
7643
8681
  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.";
7644
8682
  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.`;
@@ -7678,6 +8716,10 @@ async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {
7678
8716
  const grokResult = (deps.installGrokHook ?? installGrokAutoSyncHook)();
7679
8717
  if (!grokResult.ok) return grokResult;
7680
8718
  }
8719
+ if (names.has(CURSOR_HARNESS_NAME)) {
8720
+ const result = (deps.installCursorHook ?? installCursorAutoSyncHook)();
8721
+ if (!result.ok) return result;
8722
+ }
7681
8723
  saveSettings(
7682
8724
  {
7683
8725
  autoSyncAnswered: true,
@@ -7688,7 +8730,7 @@ async function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {
7688
8730
  return {
7689
8731
  ok: true,
7690
8732
  message: [
7691
- `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`,
8733
+ `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`,
7692
8734
  ...trustLine ? [trustLine] : []
7693
8735
  ].join("\n")
7694
8736
  };
@@ -7710,7 +8752,8 @@ async function disableAutoSync(deps = {}) {
7710
8752
  const result = (deps.removeHook ?? removeAutoSyncHook)();
7711
8753
  const codexResult = (deps.removeCodexHook ?? removeCodexAutoSyncHook)();
7712
8754
  const grokResult = (deps.removeGrokHook ?? removeGrokAutoSyncHook)();
7713
- const failures = [result, codexResult, grokResult].filter((r) => !r.ok).map((r) => r.message);
8755
+ const cursorResult = (deps.removeCursorHook ?? removeCursorAutoSyncHook)();
8756
+ const failures = [result, codexResult, grokResult, cursorResult].filter((r) => !r.ok).map((r) => r.message);
7714
8757
  const token = (deps.getTokenImpl ?? getToken)();
7715
8758
  if (token !== null) {
7716
8759
  try {
@@ -7751,7 +8794,8 @@ async function reconcileAutoSync(permission, deps = {}) {
7751
8794
  const results = [
7752
8795
  (deps.removeHook ?? removeAutoSyncHook)(),
7753
8796
  (deps.removeCodexHook ?? removeCodexAutoSyncHook)(),
7754
- (deps.removeGrokHook ?? removeGrokAutoSyncHook)()
8797
+ (deps.removeGrokHook ?? removeGrokAutoSyncHook)(),
8798
+ (deps.removeCursorHook ?? removeCursorAutoSyncHook)()
7755
8799
  ];
7756
8800
  const failures2 = results.filter((result) => !result.ok);
7757
8801
  if (failures2.length > 0) {
@@ -7793,6 +8837,11 @@ async function reconcileAutoSync(permission, deps = {}) {
7793
8837
  deps.grokHookInstalledImpl ?? grokAutoSyncHookInstalled,
7794
8838
  deps.installGrokHook ?? installGrokAutoSyncHook
7795
8839
  );
8840
+ install(
8841
+ CURSOR_HARNESS_NAME,
8842
+ deps.cursorHookInstalledImpl ?? cursorAutoSyncHookInstalled,
8843
+ deps.installCursorHook ?? installCursorAutoSyncHook
8844
+ );
7796
8845
  if (failures.length > 0) {
7797
8846
  return {
7798
8847
  ok: false,
@@ -7811,7 +8860,7 @@ async function reconcileAutoSync(permission, deps = {}) {
7811
8860
  if (installed.length === 0 && mirrored) return null;
7812
8861
  return {
7813
8862
  ok: true,
7814
- 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.`
8863
+ 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"}.`
7815
8864
  };
7816
8865
  }
7817
8866
  async function settleAutoSync(permission, deps = {}) {
@@ -7829,13 +8878,14 @@ async function offerAutoSyncOptIn(deps = {}) {
7829
8878
  return false;
7830
8879
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
7831
8880
  if (detected.length === 0) return false;
8881
+ const names = new Set(detected.map((adapter) => adapter.name));
7832
8882
  const answer = await p6.select({
7833
8883
  message: "Keep this stack fresh automatically every 6 hours?",
7834
8884
  options: [
7835
8885
  {
7836
8886
  value: "enable",
7837
8887
  label: "Enable",
7838
- hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} session starts`
8888
+ hint: `a silent sync at most every 6 hours when a ${harnessListLabel(detected)} ${names.has(CURSOR_HARNESS_NAME) ? "session is active" : "session starts"}`
7839
8889
  },
7840
8890
  {
7841
8891
  value: "later",
@@ -7878,17 +8928,17 @@ async function offerAutoSyncOptIn(deps = {}) {
7878
8928
  import {
7879
8929
  appendFileSync,
7880
8930
  closeSync,
7881
- mkdirSync as mkdirSync7,
8931
+ mkdirSync as mkdirSync8,
7882
8932
  openSync,
7883
- readFileSync as readFileSync13,
8933
+ readFileSync as readFileSync14,
7884
8934
  unlinkSync as unlinkSync2,
7885
- writeFileSync as writeFileSync7
8935
+ writeFileSync as writeFileSync8
7886
8936
  } from "node:fs";
7887
- import { homedir as homedir16 } from "node:os";
7888
- import { dirname as dirname8, join as join11 } from "node:path";
8937
+ import { homedir as homedir20 } from "node:os";
8938
+ import { dirname as dirname9, join as join12 } from "node:path";
7889
8939
 
7890
8940
  // src/sync/stage.ts
7891
- import { createHash as createHash3 } from "node:crypto";
8941
+ import { createHash as createHash5 } from "node:crypto";
7892
8942
 
7893
8943
  // src/usage/days.ts
7894
8944
  var round6 = (n) => Math.round(n * 1e6) / 1e6;
@@ -8003,11 +9053,11 @@ function buildMeasuredDays(input) {
8003
9053
 
8004
9054
  // src/usage/diff.ts
8005
9055
  var MAX_DAY_WINDOW = 400;
8006
- var DAY_MS = 864e5;
9056
+ var DAY_MS2 = 864e5;
8007
9057
  function retentionFloor(todayUtc, days) {
8008
9058
  const span = Math.max(1, Math.min(days, MAX_DAY_WINDOW));
8009
9059
  const todayMs = Date.parse(`${todayUtc}T00:00:00.000Z`);
8010
- return new Date(todayMs - (span - 1) * DAY_MS).toISOString().slice(0, 10);
9060
+ return new Date(todayMs - (span - 1) * DAY_MS2).toISOString().slice(0, 10);
8011
9061
  }
8012
9062
  function selectDaysToPublish(input) {
8013
9063
  const { local, manifest, todayUtc } = input;
@@ -8044,7 +9094,7 @@ function selectDaysToPublish(input) {
8044
9094
 
8045
9095
  // src/workflow/git.ts
8046
9096
  import { execFile, execFileSync as execFileSync2 } from "node:child_process";
8047
- import path7 from "node:path";
9097
+ import path12 from "node:path";
8048
9098
  var TEST_FILE_RULE_VERSION = "test-files/v2";
8049
9099
  var FILE_TYPE_RULE_VERSION = "file-types/v2";
8050
9100
  var COMMIT_SET_RULE_VERSION = "commit-set/v1";
@@ -8308,10 +9358,10 @@ function reduceGitHistories(histories, options) {
8308
9358
  const field = fields[fieldIndex] ?? "";
8309
9359
  if (field.replace(/^\n+/, "") === COMMIT_MARKER) {
8310
9360
  finishCommit();
8311
- const hash = fields[++fieldIndex] ?? "";
9361
+ const hash2 = fields[++fieldIndex] ?? "";
8312
9362
  const authoredAt = fields[++fieldIndex] ?? "";
8313
9363
  const authoredMs = Date.parse(authoredAt);
8314
- const included = Number.isFinite(authoredMs) && authoredMs >= options.fromMs && authoredMs <= options.toMs && !seenCommits.has(hash);
9364
+ const included = Number.isFinite(authoredMs) && authoredMs >= options.fromMs && authoredMs <= options.toMs && !seenCommits.has(hash2);
8315
9365
  current = {
8316
9366
  included,
8317
9367
  date: included ? new Date(authoredMs).toISOString().slice(0, 10) : "",
@@ -8324,12 +9374,12 @@ function reduceGitHistories(histories, options) {
8324
9374
  withheldLines: 0,
8325
9375
  extensionLines: /* @__PURE__ */ new Map()
8326
9376
  };
8327
- if (included) seenCommits.add(hash);
9377
+ if (included) seenCommits.add(hash2);
8328
9378
  continue;
8329
9379
  }
8330
- const stat7 = parseNumstat(field);
8331
- if (!stat7) continue;
8332
- let file = stat7.file;
9380
+ const stat8 = parseNumstat(field);
9381
+ if (!stat8) continue;
9382
+ let file = stat8.file;
8333
9383
  if (file.length === 0) {
8334
9384
  fieldIndex += 2;
8335
9385
  file = fields[fieldIndex] ?? fields[fieldIndex - 1] ?? "";
@@ -8337,13 +9387,13 @@ function reduceGitHistories(histories, options) {
8337
9387
  if (!current?.included) continue;
8338
9388
  if (isUnauthoredPath(file)) continue;
8339
9389
  current.authored = true;
8340
- const fileChangedLines = stat7.additions + stat7.removals;
8341
- current.additions += stat7.additions;
8342
- current.removals += stat7.removals;
9390
+ const fileChangedLines = stat8.additions + stat8.removals;
9391
+ current.additions += stat8.additions;
9392
+ current.removals += stat8.removals;
8343
9393
  current.changedLines += fileChangedLines;
8344
9394
  if (isTestFile(file)) current.touchesTest = true;
8345
9395
  if (fileChangedLines <= 0) continue;
8346
- const extension = path7.extname(file).toLowerCase();
9396
+ const extension = path12.extname(file).toLowerCase();
8347
9397
  if (APPROVED_EXTENSIONS.has(extension)) {
8348
9398
  current.extensionLines.set(
8349
9399
  extension,
@@ -8447,36 +9497,36 @@ function buildWorkflowExtraction(harnessWorkflows, git, utcOffsetMinutes = machi
8447
9497
  }
8448
9498
 
8449
9499
  // src/sync/grokDateCache.ts
8450
- import { createHash as createHash2 } from "node:crypto";
8451
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
8452
- import { homedir as homedir15 } from "node:os";
8453
- import path8 from "node:path";
8454
- var defaultFile = path8.join(
8455
- homedir15(),
9500
+ import { createHash as createHash4 } from "node:crypto";
9501
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
9502
+ import { homedir as homedir19 } from "node:os";
9503
+ import path13 from "node:path";
9504
+ var defaultFile = path13.join(
9505
+ homedir19(),
8456
9506
  ".config",
8457
9507
  "aistack",
8458
9508
  "grok-session-dates.json"
8459
9509
  );
8460
9510
  function grokCacheScope(baseUrl, stack, token) {
8461
- return createHash2("sha256").update(`${baseUrl}\0${stack}\0${token}`).digest("hex");
9511
+ return createHash4("sha256").update(`${baseUrl}\0${stack}\0${token}`).digest("hex");
8462
9512
  }
8463
- function read(file) {
8464
- if (!existsSync11(file)) return {};
9513
+ function read2(file) {
9514
+ if (!existsSync12(file)) return {};
8465
9515
  try {
8466
- const value = JSON.parse(readFileSync12(file, "utf8"));
9516
+ const value = JSON.parse(readFileSync13(file, "utf8"));
8467
9517
  return value && typeof value === "object" ? value : {};
8468
9518
  } catch {
8469
9519
  return {};
8470
9520
  }
8471
9521
  }
8472
9522
  function loadGrokDateHints(scope, file = defaultFile) {
8473
- return read(file)[scope] ?? {};
9523
+ return read2(file)[scope] ?? {};
8474
9524
  }
8475
9525
  function saveGrokDateHints(scope, hints, file = defaultFile) {
8476
- const cache = read(file);
9526
+ const cache = read2(file);
8477
9527
  cache[scope] = hints;
8478
- mkdirSync6(path8.dirname(file), { recursive: true });
8479
- writeFileSync6(file, JSON.stringify(cache, null, 2));
9528
+ mkdirSync7(path13.dirname(file), { recursive: true });
9529
+ writeFileSync7(file, JSON.stringify(cache, null, 2));
8480
9530
  }
8481
9531
  function mapToHints(value, floor) {
8482
9532
  return Object.fromEntries(
@@ -8887,7 +9937,7 @@ function buildGateSummary(ctx) {
8887
9937
  // src/sync/stage.ts
8888
9938
  var utcDate2 = (ms) => new Date(ms).toISOString().slice(0, 10);
8889
9939
  function stageId(bodyJson) {
8890
- return createHash3("sha256").update(bodyJson).digest("hex").slice(0, 12);
9940
+ return createHash5("sha256").update(bodyJson).digest("hex").slice(0, 12);
8891
9941
  }
8892
9942
  async function stageSync(deps) {
8893
9943
  const now = (deps.now ?? Date.now)();
@@ -8941,11 +9991,12 @@ async function stageSync(deps) {
8941
9991
  const active2 = await adapters(sinceMs);
8942
9992
  const historical = await adapters(daysSinceMs);
8943
9993
  let dayScansComplete = true;
8944
- let grokCurrentDates = null;
9994
+ const sessionDatesByHarness = /* @__PURE__ */ new Map();
8945
9995
  for (const adapter of active2) {
8946
9996
  progress(`Scanning recent ${adapter.name} usage`);
8947
9997
  const { aggregate, stats } = await adapter.scan({
8948
9998
  sinceMs,
9999
+ publishWorkflow: false,
8949
10000
  onProgress: (files) => progress(`Scanning recent ${adapter.name} usage \xB7 ${files} files`)
8950
10001
  });
8951
10002
  scanStats[adapter.name] = stats;
@@ -8966,11 +10017,12 @@ async function stageSync(deps) {
8966
10017
  progress(`Reading historical ${adapter.name} days`);
8967
10018
  const { aggregate, workflow: workflow2, workflowLocal, scanComplete, sessionDates } = await adapter.scan({
8968
10019
  sinceMs: daysSinceMs,
10020
+ publishWorkflow: config.publishWorkflow,
8969
10021
  onProgress: (files) => progress(`Reading historical ${adapter.name} days \xB7 ${files} files`)
8970
10022
  });
8971
10023
  if (scanComplete === false) dayScansComplete = false;
8972
- if (adapter.name === "grok-build")
8973
- grokCurrentDates = sessionDates ?? /* @__PURE__ */ new Map();
10024
+ if (adapter.name === "grok-build" || adapter.name === "cursor")
10025
+ sessionDatesByHarness.set(adapter.name, sessionDates ?? /* @__PURE__ */ new Map());
8974
10026
  workflowScans.push({ aggregate: workflow2, local: workflowLocal });
8975
10027
  usageScans.push(
8976
10028
  buildUsageDays({
@@ -8998,19 +10050,29 @@ async function stageSync(deps) {
8998
10050
  }
8999
10051
  const correctionDates = /* @__PURE__ */ new Set();
9000
10052
  let acknowledgePublish;
9001
- if (grokCurrentDates && token && config.stack) {
9002
- const scope = grokCacheScope(deps.baseUrl, config.stack.slug, token);
10053
+ const acknowledgements = [];
10054
+ for (const [harness, currentDates] of sessionDatesByHarness) {
10055
+ if (!token || !config.stack) continue;
10056
+ const scope = grokCacheScope(
10057
+ harness === "grok-build" ? deps.baseUrl : `${deps.baseUrl}\0${harness}`,
10058
+ config.stack.slug,
10059
+ token
10060
+ );
9003
10061
  const floor = utcDate2(daysSinceMs);
9004
10062
  const previous = loadGrokDateHints(scope);
9005
- const current = mapToHints(grokCurrentDates, floor);
10063
+ const current = mapToHints(currentDates, floor);
9006
10064
  for (const dates of Object.values(previous))
9007
10065
  for (const date of dates) correctionDates.add(date);
9008
10066
  for (const dates of Object.values(current))
9009
10067
  for (const date of dates) correctionDates.add(date);
9010
10068
  if (Object.keys(previous).some((id) => !(id in current)))
9011
10069
  dayScansComplete = false;
9012
- acknowledgePublish = () => saveGrokDateHints(scope, current);
10070
+ acknowledgements.push(() => saveGrokDateHints(scope, current));
9013
10071
  }
10072
+ if (acknowledgements.length)
10073
+ acknowledgePublish = () => {
10074
+ for (const acknowledge of acknowledgements) acknowledge();
10075
+ };
9014
10076
  const localDays = applyDayConsent(
9015
10077
  buildMeasuredDays({
9016
10078
  usage: mergeUsageDays(usageScans),
@@ -9080,31 +10142,31 @@ async function stageSync(deps) {
9080
10142
  }
9081
10143
 
9082
10144
  // src/autosync/run.ts
9083
- var SYNC_LOG_FILE = join11(homedir16(), ".config", "aistack", "sync.log");
10145
+ var SYNC_LOG_FILE = join12(homedir20(), ".config", "aistack", "sync.log");
9084
10146
  var SYNC_LOG_MAX_LINES = 200;
9085
10147
  var FIX_COMMAND = "npx @use-aistack/cli sync";
9086
10148
  var REVOKED_RESULT = "off - auto-sync is switched off for this stack";
9087
10149
  function appendLogLine(file, line) {
9088
- mkdirSync7(dirname8(file), { recursive: true });
10150
+ mkdirSync8(dirname9(file), { recursive: true });
9089
10151
  appendFileSync(file, `${line}
9090
10152
  `);
9091
- const lines2 = readFileSync13(file, "utf-8").split("\n").filter(Boolean);
10153
+ const lines2 = readFileSync14(file, "utf-8").split("\n").filter(Boolean);
9092
10154
  if (lines2.length > SYNC_LOG_MAX_LINES) {
9093
- writeFileSync7(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
10155
+ writeFileSync8(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
9094
10156
  `);
9095
10157
  }
9096
10158
  }
9097
10159
  function reserveAttempt(file, now, windowMs) {
9098
- mkdirSync7(dirname8(file), { recursive: true });
10160
+ mkdirSync8(dirname9(file), { recursive: true });
9099
10161
  for (; ; ) {
9100
10162
  try {
9101
10163
  const fd = openSync(file, "wx");
9102
- writeFileSync7(fd, String(now));
10164
+ writeFileSync8(fd, String(now));
9103
10165
  closeSync(fd);
9104
10166
  return true;
9105
10167
  } catch (error) {
9106
10168
  if (error.code !== "EEXIST") throw error;
9107
- const held = Number(readFileSync13(file, "utf-8"));
10169
+ const held = Number(readFileSync14(file, "utf-8"));
9108
10170
  if (Number.isFinite(held) && now - held < windowMs) return false;
9109
10171
  try {
9110
10172
  unlinkSync2(file);
@@ -9133,7 +10195,7 @@ async function runAutoSync(deps) {
9133
10195
  const state = settings.autoSyncState ?? {};
9134
10196
  const lastRunAt = state.lastRunAt ?? 0;
9135
10197
  if (now - lastRunAt < windowMs) return;
9136
- const reservationFile = deps.reservationFile ?? `${settingsFile ?? join11(homedir16(), ".config", "aistack", "settings.json")}.auto-sync-attempt`;
10198
+ const reservationFile = deps.reservationFile ?? `${settingsFile ?? join12(homedir20(), ".config", "aistack", "settings.json")}.auto-sync-attempt`;
9137
10199
  if (!reserveAttempt(reservationFile, now, windowMs)) return;
9138
10200
  saveSettings({ autoSyncState: { ...state, lastRunAt: now } }, settingsFile);
9139
10201
  const stage = deps.stageImpl ?? stageSync;
@@ -9226,7 +10288,7 @@ async function runAutoSync(deps) {
9226
10288
  logFile,
9227
10289
  `${stamp} fail (${consecutiveFailures} in a row) - ${failure2}`
9228
10290
  );
9229
- if (shouldWarn && deps.suppressOutput !== true && process.env.AISTACK_HOOK_SOURCE !== "grok") {
10291
+ if (shouldWarn && deps.suppressOutput !== true && process.env.AISTACK_HOOK_SOURCE !== "grok" && process.env.AISTACK_HOOK_SOURCE !== "cursor") {
9230
10292
  emit(
9231
10293
  JSON.stringify({
9232
10294
  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.`
@@ -9720,7 +10782,7 @@ program.command("mcp").description(
9720
10782
  });
9721
10783
  program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").option(
9722
10784
  "--auto [state]",
9723
- "silent background sync; 'on' asks your stack for the permission and installs the SessionStart hooks, 'off' revokes both"
10785
+ "silent background sync; 'on' asks your stack for the permission and installs harness hooks, 'off' revokes both"
9724
10786
  ).option(
9725
10787
  "--every <hours>",
9726
10788
  "with --auto on: hours between auto-syncs (default 6)"