reflex-skills 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -4
  2. package/dist/reflex.js +472 -115
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,7 @@ alias reflex="node $PWD/apps/cli/dist/reflex.js"
56
56
  | `improve [skill]` | For skills you already have: lists them with the sessions that loaded each; with a name, compares the skill with those sessions and proposes an edit (asks before reading the skill and before changing it; keeps a backup). `--file`, `--yes` |
57
57
  | `agents` | Sessions that launched agents (Cursor): how many, which kinds, and how many tool calls the agents made |
58
58
  | `handbook` | How work is done in the current repository, learned from past sessions: test, build and lint commands that worked (and ones that kept failing), and corrections you repeated, with the situation they came up in and the sessions behind each. `show <id>`, `ask "<question>"`, `export` (Markdown for a teammate, no request text), `--project`, `--all`, `--json`. Read-only, so agents can call it |
59
+ | `cursor-usage [on\|off]` | Record Cursor's token counts locally through a Cursor hook, since Cursor saves none in its transcripts. `on` adds Reflex's hook to `~/.cursor/hooks.json` (asks first, keeps your other hooks, backs the file up); `off` removes it |
59
60
  | `stats` | Counts from local data |
60
61
  | `reset` | Delete Reflex's own data (not installed skills or the model) |
61
62
  | `model [status\|fetch\|use\|remove]` | Optional local embedding model for matching requests by meaning. `fetch` downloads bge-m3 (multilingual, 587 MB); `fetch --light` the English-only all-MiniLM-L6-v2 (23 MB); `use <name>` switches between installed ones |
@@ -76,9 +77,9 @@ tool, model and project, the most expensive sessions, what caching saved), gener
76
77
  suggestions waiting for review, every repeated workflow with the evidence behind it, router
77
78
  suggestions (which model was cheapest and about as reliable, per kind of task), and a search over
78
79
  each session's opening request and steps. Enter reopens a Claude Code or Codex session, or opens a
79
- Cursor chat's workspace in Cursor. Cursor records no tokens, so its sessions have no cost; if you
80
- allow it, Reflex reads which model each Cursor chat used and Tokens counts Cursor sessions per
81
- model. Usage
80
+ Cursor chat's workspace in Cursor. Cursor saves no tokens in its transcripts, so its sessions have
81
+ no cost unless you turn on `reflex cursor-usage` (below); if you allow it, Reflex reads which model
82
+ each Cursor chat used and Tokens counts Cursor sessions per model. Usage
82
83
  limits are not recorded, so that block says `not measured yet`. Run `reflex scan` again (or press
83
84
  `r`) after upgrading: costs need the model of each session, which older scans did not store.
84
85
 
@@ -184,7 +185,8 @@ both sides have at least 3 runs, and never predicts savings in advance.
184
185
 
185
186
  - Tokens are whole-session totals as Claude Code and Codex record them, cache reads included, so
186
187
  other work in the same session counts too.
187
- - Cursor doesn't record tokens. Cursor runs count tool calls only.
188
+ - Cursor saves no tokens in its transcripts. Cursor runs count tool calls only, unless
189
+ `reflex cursor-usage` was on during them.
188
190
  - The skill's own cost is shown as an estimate from its file size (4 characters per token).
189
191
 
190
192
  ## Routing tasks to models
@@ -250,6 +252,14 @@ Threat model and mitigations: [docs/security.md](docs/security.md).
250
252
  | Codex | `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl` | `~/.codex/skills/<name>/SKILL.md` | Parser follows the Codex protocol types and is tested on fixtures; not yet run against real rollouts or a live Codex session |
251
253
  | Cursor | `~/.cursor/projects/*/agent-transcripts/*/*.jsonl` | `~/.cursor/skills/<name>/SKILL.md` | Parser tested on fixtures and on real agent transcripts. Cursor doesn't record tool results, so pass/fail outcomes aren't available for its sessions |
252
254
 
255
+ **Cursor token usage.** Cursor's transcripts and app database have no token counts (every
256
+ `tokenCount` is 0), but Cursor passes each agent turn's counts to its hooks. `reflex cursor-usage on`
257
+ adds one hook (`stop`, with `afterAgentResponse` as fallback) that appends the chat and turn ids, the
258
+ model and the four token counts to `~/.reflex/cursor-usage.jsonl`; never the prompt or the answer, no
259
+ network. The chat id is the transcript's file name, so `reflex scan` attaches those counts to each
260
+ Cursor chat and prices them like other sessions. Only turns from while the hook is on are counted;
261
+ Cursor's command-line agent in `-p` mode sends no end-of-turn hook. Not available on Windows yet.
262
+
253
263
  **Agents a session launched.** When a Cursor chat launches agents (its `Task` / `Subagent` tool),
254
264
  Reflex records each one (kind, model, background or not, its short description) and reads the
255
265
  agents' transcripts (`<chat>/subagents/*.jsonl`). Their commands, files and tool calls count toward
package/dist/reflex.js CHANGED
@@ -3461,12 +3461,12 @@ var require_src = __commonJS({
3461
3461
  else if (y3 > 0) ret += `${CSI}${y3}B`;
3462
3462
  return ret;
3463
3463
  },
3464
- up: (count2 = 1) => `${CSI}${count2}A`,
3465
- down: (count2 = 1) => `${CSI}${count2}B`,
3466
- forward: (count2 = 1) => `${CSI}${count2}C`,
3467
- backward: (count2 = 1) => `${CSI}${count2}D`,
3468
- nextLine: (count2 = 1) => `${CSI}E`.repeat(count2),
3469
- prevLine: (count2 = 1) => `${CSI}F`.repeat(count2),
3464
+ up: (count3 = 1) => `${CSI}${count3}A`,
3465
+ down: (count3 = 1) => `${CSI}${count3}B`,
3466
+ forward: (count3 = 1) => `${CSI}${count3}C`,
3467
+ backward: (count3 = 1) => `${CSI}${count3}D`,
3468
+ nextLine: (count3 = 1) => `${CSI}E`.repeat(count3),
3469
+ prevLine: (count3 = 1) => `${CSI}F`.repeat(count3),
3470
3470
  left: `${CSI}G`,
3471
3471
  hide: `${CSI}?25l`,
3472
3472
  show: `${CSI}?25h`,
@@ -3474,21 +3474,21 @@ var require_src = __commonJS({
3474
3474
  restore: `${ESC}8`
3475
3475
  };
3476
3476
  var scroll = {
3477
- up: (count2 = 1) => `${CSI}S`.repeat(count2),
3478
- down: (count2 = 1) => `${CSI}T`.repeat(count2)
3477
+ up: (count3 = 1) => `${CSI}S`.repeat(count3),
3478
+ down: (count3 = 1) => `${CSI}T`.repeat(count3)
3479
3479
  };
3480
3480
  var erase = {
3481
3481
  screen: `${CSI}2J`,
3482
- up: (count2 = 1) => `${CSI}1J`.repeat(count2),
3483
- down: (count2 = 1) => `${CSI}J`.repeat(count2),
3482
+ up: (count3 = 1) => `${CSI}1J`.repeat(count3),
3483
+ down: (count3 = 1) => `${CSI}J`.repeat(count3),
3484
3484
  line: `${CSI}2K`,
3485
3485
  lineEnd: `${CSI}K`,
3486
3486
  lineStart: `${CSI}1K`,
3487
- lines(count2) {
3487
+ lines(count3) {
3488
3488
  let clear = "";
3489
- for (let i = 0; i < count2; i++)
3490
- clear += this.line + (i < count2 - 1 ? cursor.up() : "");
3491
- if (count2)
3489
+ for (let i = 0; i < count3; i++)
3490
+ clear += this.line + (i < count3 - 1 ? cursor.up() : "");
3491
+ if (count3)
3492
3492
  clear += cursor.left;
3493
3493
  return clear;
3494
3494
  }
@@ -5242,9 +5242,9 @@ function assertNever(_x) {
5242
5242
  }
5243
5243
  function assert(_3) {
5244
5244
  }
5245
- function getEnumValues(entries) {
5246
- const numericValues = Object.values(entries).filter((v2) => typeof v2 === "number");
5247
- const values = Object.entries(entries).filter(([k3, _3]) => numericValues.indexOf(+k3) === -1).map(([_3, v2]) => v2);
5245
+ function getEnumValues(entries2) {
5246
+ const numericValues = Object.values(entries2).filter((v2) => typeof v2 === "number");
5247
+ const values = Object.entries(entries2).filter(([k3, _3]) => numericValues.indexOf(+k3) === -1).map(([_3, v2]) => v2);
5248
5248
  return values;
5249
5249
  }
5250
5250
  function joinValues(array2, separator = "|") {
@@ -5781,14 +5781,14 @@ function codePointLength(str3) {
5781
5781
  const units = str3.length;
5782
5782
  if (!highSurrogate.test(str3))
5783
5783
  return units;
5784
- let count2 = units;
5784
+ let count3 = units;
5785
5785
  for (let i = 0; i < units - 1; i++) {
5786
5786
  if ((str3.charCodeAt(i) & 64512) === 55296 && (str3.charCodeAt(i + 1) & 64512) === 56320) {
5787
- count2--;
5787
+ count3--;
5788
5788
  i++;
5789
5789
  }
5790
5790
  }
5791
- return count2;
5791
+ return count3;
5792
5792
  }
5793
5793
  function getLengthableOrigin(input2) {
5794
5794
  if (Array.isArray(input2))
@@ -6564,8 +6564,8 @@ function emoji() {
6564
6564
  }
6565
6565
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
6566
6566
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
6567
- var mac = (delimiter3) => {
6568
- const escapedDelim = escapeRegex(delimiter3 ?? ":");
6567
+ var mac = (delimiter4) => {
6568
+ const escapedDelim = escapeRegex(delimiter4 ?? ":");
6569
6569
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
6570
6570
  };
6571
6571
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -7067,16 +7067,16 @@ var $ZodCheckProperties = /* @__PURE__ */ $constructor("$ZodCheckProperties", (i
7067
7067
  hide(inst, Symbol.iterator, function* () {
7068
7068
  yield inst;
7069
7069
  });
7070
- let entries;
7070
+ let entries2;
7071
7071
  inst._zod.check = (payload) => {
7072
7072
  if (payload.value == null) {
7073
7073
  payload.issues.push({ expected: "object", code: "invalid_type", input: payload.value, inst });
7074
7074
  return void 0;
7075
7075
  }
7076
- entries ?? (entries = Reflect.ownKeys(def.shape).map((key3) => [key3, def.shape[key3]]));
7076
+ entries2 ?? (entries2 = Reflect.ownKeys(def.shape).map((key3) => [key3, def.shape[key3]]));
7077
7077
  const input2 = payload.value;
7078
7078
  let proms;
7079
- for (const [key3, schema] of entries) {
7079
+ for (const [key3, schema] of entries2) {
7080
7080
  const result = schema._zod.run({ value: input2[key3], issues: [] }, {});
7081
7081
  if (result instanceof Promise) {
7082
7082
  proms ?? (proms = []);
@@ -10143,8 +10143,8 @@ function az_default() {
10143
10143
  }
10144
10144
 
10145
10145
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/be.js
10146
- function getBelarusianPlural(count2, one, few, many) {
10147
- const absCount = Math.abs(count2);
10146
+ function getBelarusianPlural(count3, one, few, many) {
10147
+ const absCount = Math.abs(count3);
10148
10148
  const lastDigit = absCount % 10;
10149
10149
  const lastTwoDigits = absCount % 100;
10150
10150
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -12791,8 +12791,8 @@ function hu_default() {
12791
12791
  }
12792
12792
 
12793
12793
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/hy.js
12794
- function getArmenianPlural(count2, one, many) {
12795
- return Math.abs(count2) === 1 ? one : many;
12794
+ function getArmenianPlural(count3, one, many) {
12795
+ return Math.abs(count3) === 1 ? one : many;
12796
12796
  }
12797
12797
  function withDefiniteArticle(word) {
12798
12798
  if (!word)
@@ -15530,8 +15530,8 @@ function ro_default() {
15530
15530
  }
15531
15531
 
15532
15532
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/ru.js
15533
- function getRussianPlural(count2, one, few, many) {
15534
- const absCount = Math.abs(count2);
15533
+ function getRussianPlural(count3, one, few, many) {
15534
+ const absCount = Math.abs(count3);
15535
15535
  const lastDigit = absCount % 10;
15536
15536
  const lastTwoDigits = absCount % 100;
15537
15537
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -19858,18 +19858,18 @@ function _set(Class2, valueType, params) {
19858
19858
  }
19859
19859
  // @__NO_SIDE_EFFECTS__
19860
19860
  function _enum(Class2, values, params) {
19861
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
19861
+ const entries2 = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
19862
19862
  return new Class2({
19863
19863
  type: "enum",
19864
- entries,
19864
+ entries: entries2,
19865
19865
  ...normalizeParams(params)
19866
19866
  });
19867
19867
  }
19868
19868
  // @__NO_SIDE_EFFECTS__
19869
- function _nativeEnum(Class2, entries, params) {
19869
+ function _nativeEnum(Class2, entries2, params) {
19870
19870
  return new Class2({
19871
19871
  type: "enum",
19872
- entries,
19872
+ entries: entries2,
19873
19873
  ...normalizeParams(params)
19874
19874
  });
19875
19875
  }
@@ -22852,17 +22852,17 @@ var ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
22852
22852
  };
22853
22853
  });
22854
22854
  function _enum2(values, params) {
22855
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
22855
+ const entries2 = Array.isArray(values) ? Object.fromEntries(values.map((v2) => [v2, v2])) : values;
22856
22856
  return new ZodEnum({
22857
22857
  type: "enum",
22858
- entries,
22858
+ entries: entries2,
22859
22859
  ...util_exports.normalizeParams(params)
22860
22860
  });
22861
22861
  }
22862
- function nativeEnum(entries, params) {
22862
+ function nativeEnum(entries2, params) {
22863
22863
  return new ZodEnum({
22864
22864
  type: "enum",
22865
- entries,
22865
+ entries: entries2,
22866
22866
  ...util_exports.normalizeParams(params)
22867
22867
  });
22868
22868
  }
@@ -24249,7 +24249,7 @@ var RULES = [
24249
24249
  // DATABASE_URL=..., OPENAI_API_KEY="...", export AWS_SECRET_ACCESS_KEY=...
24250
24250
  name: "sensitive-assignment",
24251
24251
  pattern: new RegExp(`\\b(${SENSITIVE_KEY})(\\s*[=:]\\s*)("[^"\\n]*"|'[^'\\n]*'|[^\\s"',;]+)`, "g"),
24252
- replace: (_m, key3, sep3) => `${key3}${sep3.includes(":") ? ": " : "="}[REDACTED]`
24252
+ replace: (_m, key3, sep4) => `${key3}${sep4.includes(":") ? ": " : "="}[REDACTED]`
24253
24253
  },
24254
24254
  {
24255
24255
  // aws configure set aws_secret_access_key VALUE
@@ -24261,7 +24261,7 @@ var RULES = [
24261
24261
  // password=..., "token": "...", api_key: ...
24262
24262
  name: "credential-field",
24263
24263
  pattern: /(["']?)\b(password|passwd|pwd|pass|secret|token|api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|auth[_-]?token|private[_-]?key)\b\1(\s*[=:]\s*)("[^"\n]*"|'[^'\n]*'|[^\s&,;"'}]+)/gi,
24264
- replace: (_m, q2, key3, sep3) => `${q2}${key3}${q2}${sep3}[REDACTED]`
24264
+ replace: (_m, q2, key3, sep4) => `${q2}${key3}${q2}${sep4}[REDACTED]`
24265
24265
  },
24266
24266
  {
24267
24267
  name: "authorization-header",
@@ -24587,13 +24587,13 @@ function patchedFiles(patch) {
24587
24587
  async function listFiles(dir, ext, depth) {
24588
24588
  const out2 = [];
24589
24589
  async function walk(current, level) {
24590
- let entries;
24590
+ let entries2;
24591
24591
  try {
24592
- entries = await readdir(current, { withFileTypes: true });
24592
+ entries2 = await readdir(current, { withFileTypes: true });
24593
24593
  } catch {
24594
24594
  return;
24595
24595
  }
24596
- for (const entry of entries) {
24596
+ for (const entry of entries2) {
24597
24597
  if (entry.isSymbolicLink()) continue;
24598
24598
  const full = join2(current, entry.name);
24599
24599
  if (level < depth) {
@@ -25157,6 +25157,95 @@ var CursorModelIndex = class {
25157
25157
  }
25158
25158
  };
25159
25159
 
25160
+ // ../../packages/adapters/src/cursor-usage.ts
25161
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
25162
+ var MAX_LOG_BYTES = 64 * 1024 * 1024;
25163
+ var ID2 = /^[A-Za-z0-9-]{1,80}$/;
25164
+ var MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/;
25165
+ var CLAUDE = /^(?:claude-)?(?:(\d+(?:[.-]\d+)?)-(opus|sonnet|haiku|fable|mythos)|(opus|sonnet|haiku|fable|mythos)-(\d+(?:[.-]\d+)?))(?:-.*)?$/;
25166
+ function cursorModelName(id) {
25167
+ const m2 = id.toLowerCase().match(CLAUDE);
25168
+ if (!m2) return id;
25169
+ const name = `claude-${m2[2] ?? m2[3]}-${(m2[1] ?? m2[4]).replace(".", "-")}`;
25170
+ return priceOf(name) ? name : id;
25171
+ }
25172
+ function tail(path, max) {
25173
+ let fd;
25174
+ try {
25175
+ fd = openSync(path, "r");
25176
+ const size2 = fstatSync(fd).size;
25177
+ const start = Math.max(0, size2 - max);
25178
+ const buf = Buffer.alloc(size2 - start);
25179
+ readSync(fd, buf, 0, buf.length, start);
25180
+ const text = buf.toString("utf8");
25181
+ return start ? text.slice(text.indexOf("\n") + 1) : text;
25182
+ } catch {
25183
+ return "";
25184
+ } finally {
25185
+ if (fd !== void 0) closeSync(fd);
25186
+ }
25187
+ }
25188
+ var count = (v2) => typeof v2 === "number" && Number.isFinite(v2) && v2 > 0 ? Math.round(v2) : 0;
25189
+ var CursorUsageLog = class {
25190
+ constructor(path) {
25191
+ this.path = path;
25192
+ }
25193
+ path;
25194
+ turns;
25195
+ load() {
25196
+ if (this.turns) return this.turns;
25197
+ const turns = /* @__PURE__ */ new Map();
25198
+ this.turns = turns;
25199
+ for (const line2 of tail(this.path, MAX_LOG_BYTES).split("\n")) {
25200
+ let r2;
25201
+ try {
25202
+ r2 = JSON.parse(line2);
25203
+ } catch {
25204
+ continue;
25205
+ }
25206
+ const conv = typeof r2.conversation_id === "string" && ID2.test(r2.conversation_id) ? r2.conversation_id : void 0;
25207
+ const gen = typeof r2.generation_id === "string" && ID2.test(r2.generation_id) ? r2.generation_id : void 0;
25208
+ if (!conv || !gen) continue;
25209
+ const cacheRead = count(r2.cache_read_tokens);
25210
+ const cacheWrite = count(r2.cache_write_tokens);
25211
+ const usage2 = { input: Math.max(0, count(r2.input_tokens) - cacheRead - cacheWrite), output: count(r2.output_tokens), cacheRead, cacheWrite };
25212
+ const turn = {
25213
+ event: r2.event === "stop" ? "stop" : "afterAgentResponse",
25214
+ at: typeof r2.at === "string" ? Date.parse(r2.at) || 0 : 0,
25215
+ model: typeof r2.model === "string" && MODEL.test(r2.model) ? cursorModelName(r2.model) : void 0,
25216
+ usage: usage2
25217
+ };
25218
+ const byGen = turns.get(conv) ?? /* @__PURE__ */ new Map();
25219
+ turns.set(conv, byGen);
25220
+ const prev = byGen.get(gen);
25221
+ if (!prev || turn.event === "stop" || prev.event !== "stop") byGen.set(gen, turn);
25222
+ }
25223
+ return turns;
25224
+ }
25225
+ /** Usage for these conversations, in total and per model. */
25226
+ usageFor(ids) {
25227
+ let usage2;
25228
+ const byModel2 = {};
25229
+ for (const id of ids)
25230
+ for (const t of this.load().get(id)?.values() ?? []) {
25231
+ usage2 = addUsage(usage2, t.usage);
25232
+ if (t.model) byModel2[t.model] = addUsage(byModel2[t.model], t.usage);
25233
+ }
25234
+ return usage2 ? { usage: usage2, byModel: byModel2 } : void 0;
25235
+ }
25236
+ /** How many turns were recorded for these conversations, and when the latest was: part of a chat's identity for the scan cache. */
25237
+ signature(ids) {
25238
+ let turns = 0;
25239
+ let latest = 0;
25240
+ for (const id of ids)
25241
+ for (const t of this.load().get(id)?.values() ?? []) {
25242
+ turns++;
25243
+ latest = Math.max(latest, t.at);
25244
+ }
25245
+ return { turns, latest };
25246
+ }
25247
+ };
25248
+
25160
25249
  // ../../packages/adapters/src/cursor.ts
25161
25250
  var CONTROL = /[\x00-\x1f\x7f-\x9f]/g;
25162
25251
  var Block = external_exports.object({
@@ -25232,10 +25321,21 @@ var CursorAdapter = class {
25232
25321
  /** Cursor's app database, for model names. Undefined for a custom transcript folder, which reads only that folder. */
25233
25322
  stateDb;
25234
25323
  models;
25235
- constructor(root, stateDb) {
25324
+ /** Token counts recorded by Reflex's Cursor hook, when it was turned on. */
25325
+ usage;
25326
+ constructor(root, stateDb, opts = {}) {
25236
25327
  this.root = root ?? process.env.REFLEX_CURSOR_DIR ?? join6(homedir6(), ".cursor", "projects");
25237
25328
  const custom2 = root ?? process.env.REFLEX_CURSOR_DIR;
25238
25329
  this.stateDb = stateDb ?? process.env.REFLEX_CURSOR_STATE_DB ?? (custom2 ? void 0 : defaultCursorStateDb());
25330
+ if (opts.usageLog) this.usage = new CursorUsageLog(opts.usageLog);
25331
+ }
25332
+ /**
25333
+ * The conversation ids whose recorded turns belong to this chat: the chat's own. Agents it
25334
+ * launched may report their own turns too, and whether the chat's totals already include them
25335
+ * is not known yet, so they are left out rather than possibly counted twice.
25336
+ */
25337
+ conversationIds(chatFile) {
25338
+ return [basename4(chatFile, ".jsonl")];
25239
25339
  }
25240
25340
  /** Whether the user allowed reading model names from Cursor's app database in this scan. */
25241
25341
  get readsModels() {
@@ -25293,6 +25393,11 @@ var CursorAdapter = class {
25293
25393
  f.identity.ctimeMs = Math.max(f.identity.ctimeMs, agent.identity.ctimeMs);
25294
25394
  }
25295
25395
  }
25396
+ if (this.usage) {
25397
+ const sig = this.usage.signature(this.conversationIds(f.path));
25398
+ f.sizeBytes += sig.turns;
25399
+ if (f.identity && sig.latest) f.identity.ctimeMs = Math.max(f.identity.ctimeMs, sig.latest);
25400
+ }
25296
25401
  if (f.modifiedAt >= cutoff) out2.push(f);
25297
25402
  }
25298
25403
  return out2;
@@ -25337,6 +25442,11 @@ var CursorAdapter = class {
25337
25442
  }
25338
25443
  if (delegated.length) b3.events.splice(firstSpawn ?? b3.events.length, 0, ...delegated.slice(0, Math.max(0, MAX_EVENTS_PER_SESSION - b3.events.length)));
25339
25444
  b3.touch(file2.modifiedAt.toISOString());
25445
+ const recorded2 = this.usage?.usageFor(this.conversationIds(file2.path));
25446
+ if (recorded2) {
25447
+ b3.usage = recorded2.usage;
25448
+ if (Object.keys(recorded2.byModel).length) b3.usageByModel = recorded2.byModel;
25449
+ }
25340
25450
  const session = b3.build();
25341
25451
  if (spawned.length || transcripts) session.agents = { spawned, transcripts };
25342
25452
  if (this.models) session.models = this.models.modelsFor(b3.nativeId);
@@ -25989,14 +26099,14 @@ function buildTfIdf(docs, weights) {
25989
26099
  for (const doc of docs) for (const term of new Set(doc)) df.set(term, (df.get(term) ?? 0) + 1);
25990
26100
  const n = docs.length;
25991
26101
  const idf = /* @__PURE__ */ new Map();
25992
- for (const [term, count2] of df) idf.set(term, Math.log((n + 1) / (count2 + 1)) + 1);
26102
+ for (const [term, count3] of df) idf.set(term, Math.log((n + 1) / (count3 + 1)) + 1);
25993
26103
  const vectors = docs.map((doc) => {
25994
26104
  const tf = /* @__PURE__ */ new Map();
25995
26105
  for (const term of doc) tf.set(term, (tf.get(term) ?? 0) + 1);
25996
26106
  const v2 = /* @__PURE__ */ new Map();
25997
26107
  let norm = 0;
25998
- for (const [term, count2] of tf) {
25999
- const w2 = (1 + Math.log(count2)) * (idf.get(term) ?? 1) * (weights?.get(term) ?? 1);
26108
+ for (const [term, count3] of tf) {
26109
+ const w2 = (1 + Math.log(count3)) * (idf.get(term) ?? 1) * (weights?.get(term) ?? 1);
26000
26110
  v2.set(term, w2);
26001
26111
  norm += w2 * w2;
26002
26112
  }
@@ -27008,8 +27118,8 @@ function analyze(traces, opts = {}) {
27008
27118
  const repoNames = [...new Set(group.map((g2) => g2.primary.projectName).filter((x2) => !!x2))].sort();
27009
27119
  const sources = {};
27010
27120
  for (const id of SOURCE_IDS) {
27011
- const count2 = group.filter((g2) => g2.primary.source === id).length;
27012
- if (count2) sources[id] = count2;
27121
+ const count3 = group.filter((g2) => g2.primary.source === id).length;
27122
+ if (count3) sources[id] = count3;
27013
27123
  }
27014
27124
  const dates = group.map((g2) => g2.primary.startedAt).filter((d3) => !!d3).sort();
27015
27125
  const verifiedRate = group.filter((g2) => g2.primary.outcome.verified).length / group.length;
@@ -27437,12 +27547,12 @@ function scopeOf(group) {
27437
27547
  return hash2 ? { name: group[0].primary.projectName, hash: hash2 } : void 0;
27438
27548
  }
27439
27549
  function situation(group, others) {
27440
- const count2 = (xs, cat) => xs.filter((i) => i.primary.fileCategories.includes(cat)).length;
27550
+ const count3 = (xs, cat) => xs.filter((i) => i.primary.fileCategories.includes(cat)).length;
27441
27551
  let best;
27442
27552
  for (const s of SITUATIONS) {
27443
- const inside = count2(group, s.category) / group.length;
27553
+ const inside = count3(group, s.category) / group.length;
27444
27554
  if (inside < WHEN_SHARE) continue;
27445
- const lift = inside / ((count2(others, s.category) + 0.5) / (others.length + 1));
27555
+ const lift = inside / ((count3(others, s.category) + 0.5) / (others.length + 1));
27446
27556
  if (lift >= WHEN_LIFT && (!best || lift > best.lift)) best = { when: s.when, lift };
27447
27557
  }
27448
27558
  return best?.when;
@@ -28274,7 +28384,10 @@ var paths = {
28274
28384
  home: reflexHome,
28275
28385
  db: () => join13(reflexHome(), "reflex.db"),
28276
28386
  skills: () => join13(reflexHome(), "skills"),
28277
- models: () => join13(reflexHome(), "models")
28387
+ models: () => join13(reflexHome(), "models"),
28388
+ /** Token counts Reflex's Cursor hook records (`reflex cursor-usage on`). */
28389
+ cursorUsage: () => join13(reflexHome(), "cursor-usage.jsonl"),
28390
+ hooks: () => join13(reflexHome(), "hooks")
28278
28391
  };
28279
28392
 
28280
28393
  // ../../packages/storage/src/db.ts
@@ -28636,11 +28749,11 @@ var Store = class {
28636
28749
  }
28637
28750
  return out2;
28638
28751
  }
28639
- saveEmbeddings(model2, entries) {
28752
+ saveEmbeddings(model2, entries2) {
28640
28753
  const put = this.db.prepare("INSERT OR REPLACE INTO embeddings (key, model, vector) VALUES (?,?,COALESCE(?, zeroblob(0)))");
28641
28754
  this.db.exec("BEGIN IMMEDIATE");
28642
28755
  try {
28643
- for (const [key3, v2] of entries) put.run(key3, model2, new Uint8Array(v2.buffer, v2.byteOffset, v2.byteLength));
28756
+ for (const [key3, v2] of entries2) put.run(key3, model2, new Uint8Array(v2.buffer, v2.byteOffset, v2.byteLength));
28644
28757
  this.db.exec("COMMIT");
28645
28758
  } catch (err) {
28646
28759
  this.db.exec("ROLLBACK");
@@ -30767,13 +30880,13 @@ import { join as join17 } from "node:path";
30767
30880
  async function skillFiles() {
30768
30881
  const out2 = [];
30769
30882
  for (const t of await detectTargets()) {
30770
- let entries;
30883
+ let entries2;
30771
30884
  try {
30772
- entries = await readdir3(t.skillsDir, { withFileTypes: true });
30885
+ entries2 = await readdir3(t.skillsDir, { withFileTypes: true });
30773
30886
  } catch {
30774
30887
  continue;
30775
30888
  }
30776
- for (const e2 of entries) {
30889
+ for (const e2 of entries2) {
30777
30890
  if (!(e2.isDirectory() || e2.isSymbolicLink()) || !OWN_SKILL_NAME.test(e2.name)) continue;
30778
30891
  const entry = join17(t.skillsDir, e2.name);
30779
30892
  try {
@@ -30839,7 +30952,7 @@ function chooseAdapters(opts) {
30839
30952
  if (opts.cursorDir) adapters.push(new CursorAdapter(opts.cursorDir));
30840
30953
  return { adapters: only(adapters), mode: "custom" };
30841
30954
  }
30842
- return { adapters: only([new ClaudeCodeAdapter(), new CodexAdapter(), new CursorAdapter()]), mode: "local" };
30955
+ return { adapters: only([new ClaudeCodeAdapter(), new CodexAdapter(), new CursorAdapter(void 0, void 0, { usageLog: paths.cursorUsage() })]), mode: "local" };
30843
30956
  }
30844
30957
  async function scan(opts) {
30845
30958
  const { adapters, mode } = chooseAdapters(opts);
@@ -31525,14 +31638,168 @@ async function reject(ref) {
31525
31638
  // src/commands/misc.ts
31526
31639
  import { spawnSync as spawnSync4 } from "node:child_process";
31527
31640
  import { readdir as readdir4, rm as rm3, stat as stat3 } from "node:fs/promises";
31641
+ import { homedir as homedir11 } from "node:os";
31642
+ import { join as join20 } from "node:path";
31643
+
31644
+ // src/cursor-hook.ts
31645
+ import { realpathSync as realpathSync2 } from "node:fs";
31646
+ import { readFile as readFile7 } from "node:fs/promises";
31528
31647
  import { homedir as homedir10 } from "node:os";
31529
- import { join as join19 } from "node:path";
31530
- var conductorRoot = () => process.env.REFLEX_CONDUCTOR_DIR ?? join19(homedir10(), "conductor", "workspaces");
31648
+ import { delimiter, join as join19, sep as sep3 } from "node:path";
31649
+ var HOOK_EVENTS = ["stop", "afterAgentResponse"];
31650
+ var SCRIPT_NAME = "cursor-usage.mjs";
31651
+ function assertHookSupported(platform2 = process.platform) {
31652
+ if (platform2 === "win32") throw new Error("reflex cursor-usage isn't available on Windows yet: Reflex can't yet write a hook command Cursor runs there.");
31653
+ }
31654
+ var cursorHooksFile = () => process.env.REFLEX_CURSOR_HOOKS_FILE ?? join19(homedir10(), ".cursor", "hooks.json");
31655
+ var hookScript = () => join19(paths.hooks(), SCRIPT_NAME);
31656
+ function hookSource(logPath) {
31657
+ return `// Written by Reflex (reflex cursor-usage on). Records token counts per Cursor agent turn; never text.
31658
+ import { appendFileSync } from "node:fs";
31659
+ const LOG = ${JSON.stringify(logPath)};
31660
+ const ID = /^[A-Za-z0-9-]{1,80}$/;
31661
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:\\/-]{0,79}$/;
31662
+ const id = (v) => (typeof v === "string" && ID.test(v) ? v : undefined);
31663
+ const model = (v) => (typeof v === "string" && MODEL.test(v) ? v : undefined);
31664
+ const n = (v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : undefined);
31665
+ let raw = "";
31666
+ process.stdin.setEncoding("utf8");
31667
+ process.stdin.on("data", (c) => { if (raw.length < 8000000) raw += c; });
31668
+ process.stdin.on("end", () => {
31669
+ try {
31670
+ const p = JSON.parse(raw);
31671
+ const event = p.hook_event_name;
31672
+ const rec = {
31673
+ v: 1,
31674
+ at: new Date().toISOString(),
31675
+ event,
31676
+ conversation_id: id(p.conversation_id),
31677
+ generation_id: id(p.generation_id),
31678
+ model: model(p.model_id) ?? model(p.model),
31679
+ input_tokens: n(p.input_tokens),
31680
+ output_tokens: n(p.output_tokens),
31681
+ cache_read_tokens: n(p.cache_read_tokens),
31682
+ cache_write_tokens: n(p.cache_write_tokens),
31683
+ };
31684
+ const counted = rec.input_tokens !== undefined || rec.output_tokens !== undefined;
31685
+ if ((event === "stop" || event === "afterAgentResponse") && rec.conversation_id && rec.generation_id && counted) {
31686
+ appendFileSync(LOG, JSON.stringify(rec) + "\\n", { mode: 0o600 });
31687
+ }
31688
+ } catch {}
31689
+ process.stdout.write("{}");
31690
+ });
31691
+ `;
31692
+ }
31693
+ function stableNode(execPath = process.execPath, pathEnv = process.env.PATH ?? "") {
31694
+ let real;
31695
+ try {
31696
+ real = realpathSync2(execPath);
31697
+ } catch {
31698
+ return execPath;
31699
+ }
31700
+ for (const dir of pathEnv.split(delimiter)) {
31701
+ if (!dir) continue;
31702
+ const candidate = join19(dir, "node");
31703
+ try {
31704
+ if (candidate !== execPath && realpathSync2(candidate) === real) return candidate;
31705
+ } catch {
31706
+ continue;
31707
+ }
31708
+ }
31709
+ return execPath;
31710
+ }
31711
+ function hookCommand(node2 = stableNode(), script = hookScript()) {
31712
+ for (const p3 of [node2, script]) if (p3.includes("'")) throw new Error(`Reflex can't quote the path ${p3} for a hook command.`);
31713
+ return `'${node2}' '${script}'`;
31714
+ }
31715
+ var ours = (e2) => typeof e2?.command === "string" && e2.command.endsWith(`${sep3}hooks${sep3}${SCRIPT_NAME}'`);
31716
+ function entries(hooks, event, file2) {
31717
+ const v2 = hooks[event];
31718
+ if (v2 === void 0) return [];
31719
+ if (!Array.isArray(v2)) throw new Error(`${file2} has "${event}" that isn't a list; Reflex won't overwrite it. Fix it, then run this again.`);
31720
+ return [...v2];
31721
+ }
31722
+ async function readHooks(file2) {
31723
+ if (await isSymlink(file2)) throw new Error(`${file2} is a symlink; Reflex won't write through it. Add the hook entry yourself (see reflex cursor-usage status).`);
31724
+ if (!await pathExists(file2)) return { data: { version: 1, hooks: {} } };
31725
+ const raw = await readFile7(file2, "utf8");
31726
+ let data;
31727
+ try {
31728
+ data = JSON.parse(raw);
31729
+ } catch {
31730
+ throw new Error(`${file2} is not valid JSON; Reflex won't overwrite it. Fix it, then run this again.`);
31731
+ }
31732
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error(`${file2} is not a hooks object; Reflex won't overwrite it.`);
31733
+ return { data, raw };
31734
+ }
31735
+ async function hookStatus(file2 = cursorHooksFile()) {
31736
+ try {
31737
+ const { data } = await readHooks(file2);
31738
+ const hooks = data.hooks ?? {};
31739
+ return { events: HOOK_EVENTS.filter((e2) => Array.isArray(hooks[e2]) && hooks[e2].some(ours)) };
31740
+ } catch (e2) {
31741
+ return { events: [], error: e2 instanceof Error ? e2.message : String(e2) };
31742
+ }
31743
+ }
31744
+ async function writeHooks(file2, data, raw) {
31745
+ let backup;
31746
+ if (raw !== void 0) {
31747
+ const dir = join19(paths.home(), "backups");
31748
+ await ensureDir(paths.home(), 448);
31749
+ await ensureDir(dir, 448);
31750
+ backup = join19(dir, `cursor-hooks-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
31751
+ await writeNewFile(backup, raw, 384);
31752
+ }
31753
+ await ensureDir(join19(file2, ".."), 493);
31754
+ await replaceOwnedFile(file2, JSON.stringify(data, null, 2) + "\n", 420);
31755
+ return backup;
31756
+ }
31757
+ async function installHook(file2 = cursorHooksFile()) {
31758
+ assertHookSupported();
31759
+ await ensureDir(paths.home(), 448);
31760
+ await ensureDir(paths.hooks(), 448);
31761
+ await replaceOwnedFile(hookScript(), hookSource(paths.cursorUsage()), 448);
31762
+ const { data, raw } = await readHooks(file2);
31763
+ const hooks = { ...data.hooks ?? {} };
31764
+ const command = hookCommand();
31765
+ const added = [];
31766
+ for (const event of HOOK_EVENTS) {
31767
+ const list = entries(hooks, event, file2);
31768
+ if (list.some(ours)) {
31769
+ hooks[event] = list.map((e2) => ours(e2) ? { ...e2, command } : e2);
31770
+ continue;
31771
+ }
31772
+ hooks[event] = [...list, { command, timeout: 5 }];
31773
+ added.push(event);
31774
+ }
31775
+ const backup = await writeHooks(file2, { ...data, version: data.version ?? 1, hooks }, raw);
31776
+ return { added, backup };
31777
+ }
31778
+ async function removeHook(file2 = cursorHooksFile()) {
31779
+ if (!await pathExists(file2)) return { removed: [] };
31780
+ const { data, raw } = await readHooks(file2);
31781
+ const hooks = { ...data.hooks ?? {} };
31782
+ const removed = [];
31783
+ for (const event of HOOK_EVENTS) {
31784
+ const list = entries(hooks, event, file2);
31785
+ const kept = list.filter((e2) => !ours(e2));
31786
+ if (kept.length === list.length) continue;
31787
+ removed.push(event);
31788
+ if (kept.length) hooks[event] = kept;
31789
+ else delete hooks[event];
31790
+ }
31791
+ if (!removed.length) return { removed };
31792
+ const backup = await writeHooks(file2, { ...data, hooks }, raw);
31793
+ return { removed, backup };
31794
+ }
31795
+
31796
+ // src/commands/misc.ts
31797
+ var conductorRoot = () => process.env.REFLEX_CONDUCTOR_DIR ?? join20(homedir11(), "conductor", "workspaces");
31531
31798
  async function conductorWorkspaces() {
31532
31799
  const dir = conductorRoot();
31533
31800
  try {
31534
- const entries = await readdir4(dir, { withFileTypes: true });
31535
- return { dir, repos: entries.filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).length };
31801
+ const entries2 = await readdir4(dir, { withFileTypes: true });
31802
+ return { dir, repos: entries2.filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).length };
31536
31803
  } catch {
31537
31804
  return void 0;
31538
31805
  }
@@ -31571,6 +31838,10 @@ async function doctor() {
31571
31838
  out(` ${found ? c.green("\u2714") : c.dim("\xB7")} ${bin}`);
31572
31839
  }
31573
31840
  out();
31841
+ const hook = await hookStatus();
31842
+ out("Cursor token usage");
31843
+ out(` ${hook.events.length ? c.green("\u2714") : c.dim("\xB7")} ${"Cursor hook".padEnd(12)} ${hook.events.length ? `recording (${hook.events.join(", ")})` : c.dim("off \xB7 reflex cursor-usage on")}`);
31844
+ out();
31574
31845
  }
31575
31846
  async function sessions() {
31576
31847
  const store = new Store();
@@ -31662,12 +31933,18 @@ async function reset(opts) {
31662
31933
  out("Nothing to reset.");
31663
31934
  return;
31664
31935
  }
31665
- out(`This deletes Reflex's own data in ${displayPath(home)}: the local database (including sessions kept from deleted transcripts), generated skills and validation sandboxes.`);
31936
+ out(`This deletes Reflex's own data in ${displayPath(home)}: the local database (including sessions kept from deleted transcripts), generated skills, validation sandboxes and recorded Cursor token counts.`);
31666
31937
  out(c.dim("Skills you installed into Claude Code, Codex or Cursor are not touched; Reflex's agent-memory block is removed. The embedding model is kept (`reflex model remove`)."));
31667
31938
  if (!await confirm("Delete Reflex data?", opts)) return;
31668
31939
  for (const w2 of await removeMemory()) if (w2.changed) ok(`Removed Reflex's memory block from ${displayPath(w2.path)}`);
31669
- for (const child of ["reflex.db", "reflex.db-journal", "reflex.db-wal", "reflex.db-shm", "skills", "validation"]) {
31670
- await rm3(join19(home, child), { recursive: true, force: true });
31940
+ try {
31941
+ const { removed } = await removeHook();
31942
+ if (removed.length) ok(`Removed Reflex's hook from ${displayPath(cursorHooksFile())}`);
31943
+ } catch (e2) {
31944
+ warn(`Could not remove Reflex's Cursor hook: ${e2 instanceof Error ? e2.message : String(e2)}`);
31945
+ }
31946
+ for (const child of ["reflex.db", "reflex.db-journal", "reflex.db-wal", "reflex.db-shm", "skills", "validation", "cursor-usage.jsonl", "hooks"]) {
31947
+ await rm3(join20(home, child), { recursive: true, force: true });
31671
31948
  }
31672
31949
  ok("Reset.");
31673
31950
  }
@@ -31680,9 +31957,9 @@ function reportWrites(writes) {
31680
31957
  else out(c.dim(` ${w2.target.label}: ${displayPath(w2.path)} already up to date`));
31681
31958
  }
31682
31959
  }
31683
- function find(entries, ref) {
31960
+ function find(entries2, ref) {
31684
31961
  const n = Number(ref.replace(/^#/, ""));
31685
- const e2 = Number.isInteger(n) && n > 0 ? entries[n - 1] : entries.find((x2) => x2.text === ref);
31962
+ const e2 = Number.isInteger(n) && n > 0 ? entries2[n - 1] : entries2.find((x2) => x2.text === ref);
31686
31963
  if (!e2) throw new Error(`No memory entry "${ref.slice(0, 40)}". Run \`reflex memory\` to list them.`);
31687
31964
  return e2;
31688
31965
  }
@@ -31691,7 +31968,7 @@ async function memoryStatus() {
31691
31968
  try {
31692
31969
  await syncMemory(store, { mine: Boolean(store.latestScan()) });
31693
31970
  const { enabled } = store.memoryState();
31694
- const entries = store.memoryEntries();
31971
+ const entries2 = store.memoryEntries();
31695
31972
  const targets = (await memoryTargets()).filter((t) => t.detected);
31696
31973
  title(`Agent memory \xB7 ${enabled ? c.green("on") : c.dim("off")}`);
31697
31974
  out(
@@ -31699,12 +31976,12 @@ async function memoryStatus() {
31699
31976
  );
31700
31977
  rows(targets.map((t) => [t.label, displayPath(t.file)]));
31701
31978
  out();
31702
- if (!entries.length) {
31979
+ if (!entries2.length) {
31703
31980
  out(`Nothing to remember yet. Memory comes from corrections you gave in at least 2 independent sessions${store.latestScan() ? "" : "; run `reflex scan` first"}.`);
31704
31981
  out(c.dim('Add your own: reflex memory add "use pnpm, not npm"'));
31705
31982
  return;
31706
31983
  }
31707
- entries.forEach((e2, i) => {
31984
+ entries2.forEach((e2, i) => {
31708
31985
  const meta4 = e2.origin === "added" ? "added by you" : plural(e2.sessions, "session");
31709
31986
  const line2 = ` ${String(i + 1).padStart(2)}. ${e2.text} ${c.dim(`(${meta4})`)}`;
31710
31987
  out(e2.status === "forgotten" ? c.dim(`${line2} forgotten`) : line2);
@@ -31917,11 +32194,87 @@ async function modelRemove(opts) {
31917
32194
  ok(next ? `Removed ${target.name}. Scans now use ${next.name}.` : `Removed ${target.name}. Scans will compare requests by words only.`);
31918
32195
  }
31919
32196
 
32197
+ // src/commands/cursor-usage.ts
32198
+ import { readFile as readFile8 } from "node:fs/promises";
32199
+ async function recorded() {
32200
+ const conversations = /* @__PURE__ */ new Set();
32201
+ let turns = 0;
32202
+ let latest;
32203
+ try {
32204
+ for (const line2 of (await readFile8(paths.cursorUsage(), "utf8")).split("\n")) {
32205
+ try {
32206
+ const r2 = JSON.parse(line2);
32207
+ if (r2.event !== "stop" || !r2.conversation_id) continue;
32208
+ turns++;
32209
+ conversations.add(r2.conversation_id);
32210
+ if (r2.at && (!latest || r2.at > latest)) latest = r2.at;
32211
+ } catch {
32212
+ continue;
32213
+ }
32214
+ }
32215
+ } catch {
32216
+ }
32217
+ return { turns, conversations, latest };
32218
+ }
32219
+ async function cursorUsageStatus() {
32220
+ const file2 = cursorHooksFile();
32221
+ const status = await hookStatus(file2);
32222
+ const rec = await recorded();
32223
+ title("Cursor token usage");
32224
+ const on = status.events.length === HOOK_EVENTS.length;
32225
+ rows([
32226
+ ["Hook", on ? `${c.green("on")} ${c.dim(`\xB7 ${displayPath(file2)} (${status.events.join(", ")})`)}` : status.events.length ? c.yellow(`partly on (${status.events.join(", ")})`) : `off ${c.dim("\xB7 reflex cursor-usage on")}`],
32227
+ ["Recorded", rec.turns ? `${plural(rec.turns, "turn")} in ${plural(rec.conversations.size, "chat")}${rec.latest ? c.dim(` \xB7 latest ${rec.latest.slice(0, 16).replace("T", " ")} UTC`) : ""}` : c.dim("nothing yet")],
32228
+ ["Log", displayPath(paths.cursorUsage())]
32229
+ ]);
32230
+ if (status.error) warn(status.error);
32231
+ if (on && !await pathExists(hookScript())) warn("The hook script is missing; run reflex cursor-usage on again.");
32232
+ const store = new Store();
32233
+ try {
32234
+ if (store.latestScan() && rec.conversations.size) {
32235
+ const priced = store.traces().filter((t) => t.source === "cursor" && t.usage).length;
32236
+ out();
32237
+ out(` ${plural(priced, "Cursor chat")} in the last scan ${priced === 1 ? "has" : "have"} token counts. ${c.dim("reflex scan picks up new turns.")}`);
32238
+ }
32239
+ } finally {
32240
+ store.close();
32241
+ }
32242
+ out();
32243
+ out(c.dim("Cursor saves no token usage in its transcripts; its hooks receive it at the end of each agent turn. Only chats from when"));
32244
+ out(c.dim("the hook is on get counts. Cursor's command-line agent in -p mode sends no end-of-turn hook, so those runs have none."));
32245
+ }
32246
+ async function cursorUsageOn(opts) {
32247
+ assertHookSupported();
32248
+ const file2 = cursorHooksFile();
32249
+ title("Record Cursor token usage");
32250
+ out(" Cursor sends each agent turn's token counts to its hooks. Reflex adds one hook that writes only the chat and turn");
32251
+ out(" ids, the model and the four token counts to a local file. Never your prompt or the answer; no network.");
32252
+ out();
32253
+ rows([
32254
+ ["Hooks file", `${displayPath(file2)} ${c.dim(`(adds ${HOOK_EVENTS.join(" and ")}; your other hooks stay as they are; a backup goes to ~/.reflex/backups)`)}`],
32255
+ ["Runs", c.dim(hookCommand().replace(/'/g, ""))],
32256
+ ["Writes to", displayPath(paths.cursorUsage())]
32257
+ ]);
32258
+ out();
32259
+ if (!await confirm(`Add the hook to ${displayPath(file2)}?`, opts)) return;
32260
+ const { added, backup } = await installHook(file2);
32261
+ ok(added.length ? `Hook on (${added.join(", ")}). Cursor reloads hooks.json by itself; new turns are recorded from now on.` : "Hook was already on; its command is refreshed.");
32262
+ if (backup) out(c.dim(` Previous hooks file: ${displayPath(backup)}`));
32263
+ out(c.dim(" reflex scan attaches the counts to your Cursor chats. reflex cursor-usage off removes the hook."));
32264
+ }
32265
+ async function cursorUsageOff() {
32266
+ const { removed, backup } = await removeHook();
32267
+ if (!removed.length) ok("The hook was not on.");
32268
+ else ok(`Hook off (removed ${removed.join(", ")} from ${displayPath(cursorHooksFile())}).`);
32269
+ if (backup) out(c.dim(` Previous hooks file: ${displayPath(backup)}`));
32270
+ out(c.dim(` Recorded counts stay in ${displayPath(paths.cursorUsage())}; reflex reset deletes them.`));
32271
+ }
32272
+
31920
32273
  // src/commands/improve.ts
31921
32274
  import { randomUUID } from "node:crypto";
31922
32275
  import { existsSync as existsSync5 } from "node:fs";
31923
- import { delimiter, join as join20, resolve as resolve4 } from "node:path";
31924
- var onPath = (cmd) => (process.env.PATH ?? "").split(delimiter).some((d3) => d3 && existsSync5(join20(d3, cmd)));
32276
+ import { delimiter as delimiter2, join as join21, resolve as resolve4 } from "node:path";
32277
+ var onPath = (cmd) => (process.env.PATH ?? "").split(delimiter2).some((d3) => d3 && existsSync5(join21(d3, cmd)));
31925
32278
  var unique = (xs) => [...new Map(xs.map((x2) => [x2.sessionId, x2])).values()];
31926
32279
  async function listSkills2(store) {
31927
32280
  const traces = unique(store.traces());
@@ -31937,8 +32290,8 @@ async function listSkills2(store) {
31937
32290
  for (const n of names.sort((a, b3) => loaded(b3) - loaded(a) || a.localeCompare(b3))) {
31938
32291
  const mine = found.filter((f) => f.name === n);
31939
32292
  const where = [...mine.map((f) => f.target), ...mine.some((f) => f.link) ? ["linked"] : []];
31940
- const count2 = loaded(n);
31941
- out(` ${n.padEnd(28)} ${c.dim(where.join(", ").padEnd(28))} ${count2 ? `loaded in ${plural(count2, "session")}` : c.dim("not loaded in the last scan")}${own2.has(n) ? c.dim(" \xB7 generated by Reflex") : ""}`);
32293
+ const count3 = loaded(n);
32294
+ out(` ${n.padEnd(28)} ${c.dim(where.join(", ").padEnd(28))} ${count3 ? `loaded in ${plural(count3, "session")}` : c.dim("not loaded in the last scan")}${own2.has(n) ? c.dim(" \xB7 generated by Reflex") : ""}`);
31942
32295
  }
31943
32296
  out();
31944
32297
  out(c.dim(` reflex improve <name> compares a skill with the sessions that loaded it (needs ${MIN_REVIEW_SESSIONS}+).`));
@@ -31988,7 +32341,7 @@ async function verifyOutput(name, current, next, opts) {
31988
32341
  if (!Number.isInteger(runs) || runs < 1 || runs > 5) throw new Error("--runs must be 1 to 5.");
31989
32342
  if (!Number.isFinite(minutes) || minutes < 1 || minutes > 120) throw new Error("--timeout is minutes per run, 1 to 120.");
31990
32343
  out(c.dim(` The check runs "${task}" ${plural(runs, "time")} with each version: ${plural(runs * 2, "non-persistent Claude Code session")} on your account, project settings only,`));
31991
- const benchRoot = join20(paths.home(), "bench");
32344
+ const benchRoot = join21(paths.home(), "bench");
31992
32345
  out(c.dim(` in fresh copies of ${displayPath(root)} at ${headCommit(root) ?? "HEAD"} under ${displayPath(benchRoot)} (deleted afterwards). Answers are compared in memory, never saved.`));
31993
32346
  if (!await confirm(`Run the before/after check?`, opts)) return void 0;
31994
32347
  await ensureDir(paths.home(), 448);
@@ -32000,7 +32353,7 @@ async function verifyOutput(name, current, next, opts) {
32000
32353
  for (let i = 0; i < runs; i++) {
32001
32354
  const shapes = { before: [], after: [] };
32002
32355
  for (const side of ["before", "after"]) {
32003
- const dir = join20(benchRoot, `improve-${name}-${side}-${randomUUID().slice(0, 8)}`);
32356
+ const dir = join21(benchRoot, `improve-${name}-${side}-${randomUUID().slice(0, 8)}`);
32004
32357
  cloneRepo(root, dir);
32005
32358
  const spin = spinner();
32006
32359
  try {
@@ -32074,11 +32427,11 @@ async function improve(name, opts) {
32074
32427
  return;
32075
32428
  }
32076
32429
  const next = applyLearned(content, section2);
32077
- const proposalDir = assertInside(paths.home(), join20(paths.home(), "proposals", name));
32430
+ const proposalDir = assertInside(paths.home(), join21(paths.home(), "proposals", name));
32078
32431
  await ensureDir(paths.home(), 448);
32079
- await ensureDir(join20(paths.home(), "proposals"), 448);
32432
+ await ensureDir(join21(paths.home(), "proposals"), 448);
32080
32433
  await ensureDir(proposalDir, 448);
32081
- const proposal = join20(proposalDir, "SKILL.md");
32434
+ const proposal = join21(proposalDir, "SKILL.md");
32082
32435
  await replaceOwnedFile(proposal, next, 384);
32083
32436
  out(c.bold("Proposed change"));
32084
32437
  fileBlock(displayPath(target.path), lineDiff(content, next));
@@ -32124,7 +32477,7 @@ async function improve(name, opts) {
32124
32477
  if (store.installations(name).some((i) => resolve4(i.path) === resolve4(target.path)))
32125
32478
  warn("Reflex installed this copy; after the change it counts as edited, so `reflex install`/remove will leave it alone.");
32126
32479
  if (!await confirm(`Apply it to ${displayPath(target.path)}? The current file is backed up first.`, opts)) return;
32127
- const backup = await applyToOwnSkill(target.path, sha256(content), next, join20(paths.home(), "backups"), name);
32480
+ const backup = await applyToOwnSkill(target.path, sha256(content), next, join21(paths.home(), "backups"), name);
32128
32481
  store.updateSkillChange(changeId, { applied: true, backup: displayPath(backup) });
32129
32482
  ok(`Updated ${displayPath(target.path)}. Backup: ${displayPath(backup)}`);
32130
32483
  } finally {
@@ -32257,11 +32610,11 @@ async function handbookExport(opts) {
32257
32610
  import { spawnSync as spawnSync5 } from "node:child_process";
32258
32611
  import { randomInt, randomUUID as randomUUID2 } from "node:crypto";
32259
32612
  import { existsSync as existsSync6 } from "node:fs";
32260
- import { basename as basename7, delimiter as delimiter2, join as join21 } from "node:path";
32613
+ import { basename as basename7, delimiter as delimiter3, join as join22 } from "node:path";
32261
32614
  var CLI = { "claude-code": "claude", codex: "codex" };
32262
32615
  var MODEL_NAME2 = /^[A-Za-z0-9][A-Za-z0-9._:[\]-]{0,63}$/;
32263
32616
  var CODEX_MODEL = /^(gpt|o\d|codex)/i;
32264
- var onPath2 = (cmd) => (process.env.PATH ?? "").split(delimiter2).some((d3) => d3 && existsSync6(join21(d3, cmd)));
32617
+ var onPath2 = (cmd) => (process.env.PATH ?? "").split(delimiter3).some((d3) => d3 && existsSync6(join22(d3, cmd)));
32265
32618
  var unique2 = (traces) => [...new Map(traces.map((t) => [t.sessionId, t])).values()];
32266
32619
  var usd = (n) => n === void 0 ? "\u2014" : n < 0.01 ? "<$0.01" : `$${n.toFixed(2)}`;
32267
32620
  var fixes = (f) => `${f.toFixed(2)} fixes/request`;
@@ -32481,7 +32834,7 @@ async function bench(words2, opts) {
32481
32834
  const traces = unique2(store.traces());
32482
32835
  const kind = classifyRequest(task, traces).kind;
32483
32836
  const { tools, agentCanRunCheck } = benchTools(opts.check);
32484
- const benchRoot = join21(paths.home(), "bench");
32837
+ const benchRoot = join22(paths.home(), "bench");
32485
32838
  const total = runs * models.length;
32486
32839
  const table2 = routingTable(traces);
32487
32840
  const typical = models.map((m2) => ({ m: m2, cost: typicalCost(table2, "claude-code", m2) }));
@@ -32515,7 +32868,7 @@ async function bench(words2, opts) {
32515
32868
  await ensureDir(benchRoot, 448);
32516
32869
  const batch = randomUUID2().slice(0, 8);
32517
32870
  const timeoutMs = minutes * 6e4;
32518
- const baseline = join21(benchRoot, `${batch}-before`);
32871
+ const baseline = join22(benchRoot, `${batch}-before`);
32519
32872
  cloneRepo(root, baseline);
32520
32873
  try {
32521
32874
  if (opts.setup && !runUserCommand(baseline, opts.setup, timeoutMs)) throw new Error(`The setup command failed in a fresh copy: \`${opts.setup}\`. Fix it, then run bench again.`);
@@ -32528,7 +32881,7 @@ async function bench(words2, opts) {
32528
32881
  for (let i = 0; i < runs; i++) {
32529
32882
  const order = models.map((_3, j2) => models[(i + j2) % models.length]);
32530
32883
  for (const model2 of order) {
32531
- const dir = join21(benchRoot, `${batch}-${model2.replace(/[^A-Za-z0-9.-]/g, "_")}-${i + 1}`);
32884
+ const dir = join22(benchRoot, `${batch}-${model2.replace(/[^A-Za-z0-9.-]/g, "_")}-${i + 1}`);
32532
32885
  cloneRepo(root, dir);
32533
32886
  const spin = spinner();
32534
32887
  try {
@@ -33166,8 +33519,8 @@ function renderMorning(w2, h2, data, _ui) {
33166
33519
  ...data.skills.items.map((x2) => ({ k: "skill", x: x2 })),
33167
33520
  ...(data.workflows?.items ?? []).map((x2) => ({ k: "wf", x: x2 }))
33168
33521
  ];
33169
- const tail = 4;
33170
- const cap = Math.max(2, Math.min(8, h2 - L3.length - tail - 3));
33522
+ const tail2 = 4;
33523
+ const cap = Math.max(2, Math.min(8, h2 - L3.length - tail2 - 3));
33171
33524
  const cw3 = Math.floor((w2 - 6) / 3);
33172
33525
  const col = (k3) => {
33173
33526
  const xs = all.filter((e2) => e2.x.state === k3);
@@ -34704,12 +35057,12 @@ var weekdayLabel = (d3) => `${WEEKDAYS[d3.getDay()]} ${dayLabel(d3).toLowerCase(
34704
35057
  var rangeLabel = (from, to) => `${dayLabel(from).toLowerCase()} \u2013 ${dayLabel(to).toLowerCase()}`;
34705
35058
  var clockLabel = (d3) => `${pad2(d3.getHours())}:${pad2(d3.getMinutes())}`;
34706
35059
  var dayStart = (d3, n = 0) => new Date(d3.getFullYear(), d3.getMonth(), d3.getDate() + n);
34707
- var count = (n) => n.toLocaleString("en-US");
35060
+ var count2 = (n) => n.toLocaleString("en-US");
34708
35061
 
34709
35062
  // ../../packages/tui/src/live/skills.ts
34710
35063
  import { constants as constants4 } from "node:fs";
34711
35064
  import { lstat as lstat9, open as open6 } from "node:fs/promises";
34712
- import { join as join22 } from "node:path";
35065
+ import { join as join23 } from "node:path";
34713
35066
  var MAX_SKILL_BYTES = 64 * 1024;
34714
35067
  var TOOL_OF = { "claude-code": "claude", codex: "codex", cursor: "cursor" };
34715
35068
  var toolName = (target) => toCells(TOOL_OF[target] ?? target);
@@ -34733,9 +35086,9 @@ async function readCapped(file2) {
34733
35086
  }
34734
35087
  async function readReflexCopy(name) {
34735
35088
  if (!isValidSlug(name)) return void 0;
34736
- const dir = join22(paths.skills(), name);
35089
+ const dir = join23(paths.skills(), name);
34737
35090
  if (await isSymlink(paths.skills()) || await isSymlink(dir)) return void 0;
34738
- return readCapped(join22(dir, "SKILL.md"));
35091
+ return readCapped(join23(dir, "SKILL.md"));
34739
35092
  }
34740
35093
  async function isFile3(path) {
34741
35094
  try {
@@ -34784,7 +35137,7 @@ async function skillItems(store, scan2, targets, now) {
34784
35137
  state: found.has(k3.suggestionSlug) ? "" : "retired",
34785
35138
  steps: s?.steps.length ?? 0,
34786
35139
  path: path(name),
34787
- markdown: text === void 0 ? [toCells(`could not read ${displayPath(join22(paths.skills(), k3.name, "SKILL.md"))}`)] : toLines(text)
35140
+ markdown: text === void 0 ? [toCells(`could not read ${displayPath(join23(paths.skills(), k3.name, "SKILL.md"))}`)] : toLines(text)
34788
35141
  });
34789
35142
  }
34790
35143
  return { items, targets: targets.filter((t) => t.detected).map((t) => toCells(`${displayPath(t.skillsDir)}/<name>/SKILL.md`)) };
@@ -34985,8 +35338,8 @@ function buildRouter(priced, rebuiltAt, windowDays2, autopilot = false) {
34985
35338
  }
34986
35339
 
34987
35340
  // ../../packages/tui/src/live/search.ts
34988
- import { closeSync, constants as constants5, existsSync as existsSync7, lstatSync as lstatSync2, openSync, readSync, readdirSync, statSync as statSync3 } from "node:fs";
34989
- import { basename as basename8, join as join23 } from "node:path";
35341
+ import { closeSync as closeSync2, constants as constants5, existsSync as existsSync7, lstatSync as lstatSync2, openSync as openSync2, readSync as readSync2, readdirSync, statSync as statSync3 } from "node:fs";
35342
+ import { basename as basename8, join as join24 } from "node:path";
34990
35343
  var SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
34991
35344
  var MAX_CWD_SCAN_BYTES = 256 * 1024;
34992
35345
  function buildSearch(priced) {
@@ -35001,13 +35354,13 @@ function buildSearch(priced) {
35001
35354
  meta4.set(key3, { usd: cost?.usd, turnCount: t.requests });
35002
35355
  const base = { date: at ? dayLabel(new Date(at)) : "\u2014", tool, project: toCells(t.projectName ?? "\u2014"), session: toCells(key3), at };
35003
35356
  const texts = [["you", t.excerpt], ...t.displaySteps.map((s) => ["ai", s])];
35004
- const entries = [];
35357
+ const entries2 = [];
35005
35358
  texts.forEach(([role, raw], i) => {
35006
35359
  const text = toCells(raw).trim();
35007
- if (text) entries.push({ ...base, turn: i + 1, role, text, lower: text.toLowerCase() });
35360
+ if (text) entries2.push({ ...base, turn: i + 1, role, text, lower: text.toLowerCase() });
35008
35361
  });
35009
- bySession.set(key3, entries);
35010
- all.push(...entries);
35362
+ bySession.set(key3, entries2);
35363
+ all.push(...entries2);
35011
35364
  }
35012
35365
  all.sort((a, b3) => b3.at - a.at || b3.turn - a.turn);
35013
35366
  const index = {
@@ -35031,19 +35384,19 @@ function claudeCwd(nativeId) {
35031
35384
  return void 0;
35032
35385
  }
35033
35386
  for (const dir of dirs) {
35034
- const file2 = join23(root, dir, `${nativeId}.jsonl`);
35387
+ const file2 = join24(root, dir, `${nativeId}.jsonl`);
35035
35388
  try {
35036
35389
  if (!lstatSync2(file2).isFile()) continue;
35037
35390
  } catch {
35038
35391
  continue;
35039
35392
  }
35040
- const fd = openSync(file2, constants5.O_RDONLY | constants5.O_NOFOLLOW);
35393
+ const fd = openSync2(file2, constants5.O_RDONLY | constants5.O_NOFOLLOW);
35041
35394
  const buf = Buffer.alloc(MAX_CWD_SCAN_BYTES);
35042
35395
  let n;
35043
35396
  try {
35044
- n = readSync(fd, buf, 0, buf.length, 0);
35397
+ n = readSync2(fd, buf, 0, buf.length, 0);
35045
35398
  } finally {
35046
- closeSync(fd);
35399
+ closeSync2(fd);
35047
35400
  }
35048
35401
  for (const line2 of buf.subarray(0, n).toString("utf8").split("\n")) {
35049
35402
  try {
@@ -35065,7 +35418,7 @@ function cursorWorkspace(nativeId) {
35065
35418
  } catch {
35066
35419
  return void 0;
35067
35420
  }
35068
- const dir = dirs.find((d3) => existsSync7(join23(root, d3, "agent-transcripts", nativeId)));
35421
+ const dir = dirs.find((d3) => existsSync7(join24(root, d3, "agent-transcripts", nativeId)));
35069
35422
  return dir ? decodeWorkspace(dir) : void 0;
35070
35423
  }
35071
35424
  function resumeCommand(ref) {
@@ -35087,7 +35440,7 @@ function resumeCommand(ref) {
35087
35440
 
35088
35441
  // ../../packages/tui/src/live/workflows.ts
35089
35442
  var MAX_EVIDENCE = 12;
35090
- var plural4 = (n, one, many = `${one}s`) => `${count(n)} ${n === 1 ? one : many}`;
35443
+ var plural4 = (n, one, many = `${one}s`) => `${count2(n)} ${n === 1 ? one : many}`;
35091
35444
  function workflowMarkdown(s) {
35092
35445
  const tools = SOURCE_IDS.filter((x2) => s.sources[x2]).map((x2) => `${TOOL_OF[x2]} ${s.sources[x2]}`);
35093
35446
  const L3 = [`# ${s.title}`, "", s.description, ""];
@@ -35095,7 +35448,7 @@ function workflowMarkdown(s) {
35095
35448
  L3.push(`confidence ${s.confidence.toFixed(2)} \xB7 verified in ${Math.round(s.verifiedRate * 100)}% of runs`);
35096
35449
  const cost = [];
35097
35450
  if (s.cost?.medianTokens !== void 0) cost.push(`median ${tok2(s.cost.medianTokens)} tokens`);
35098
- if (s.cost?.medianToolCalls !== void 0) cost.push(`median ${count(s.cost.medianToolCalls)} tool calls`);
35451
+ if (s.cost?.medianToolCalls !== void 0) cost.push(`median ${count2(s.cost.medianToolCalls)} tool calls`);
35099
35452
  if (s.medianMinutes !== void 0) cost.push(`median ${s.medianMinutes < 120 ? `${Math.round(s.medianMinutes)} min` : `${(s.medianMinutes / 60).toFixed(1)} h`}`);
35100
35453
  if (cost.length) L3.push(`${cost.join(" \xB7 ")} per run`);
35101
35454
  L3.push("", "## Steps", "");
@@ -35117,7 +35470,7 @@ function workflowMarkdown(s) {
35117
35470
  const date5 = e2.date ? dayLabel(new Date(e2.date)) : "\u2014";
35118
35471
  L3.push(`- ${date5} ${TOOL_OF[e2.source]} ${e2.project ?? "\u2014"} ${e2.excerpt}`);
35119
35472
  }
35120
- if (s.evidence.length > MAX_EVIDENCE) L3.push(`- +${count(s.evidence.length - MAX_EVIDENCE)} more (reflex evidence ${s.slug})`);
35473
+ if (s.evidence.length > MAX_EVIDENCE) L3.push(`- +${count2(s.evidence.length - MAX_EVIDENCE)} more (reflex evidence ${s.slug})`);
35121
35474
  return L3.map(toCells);
35122
35475
  }
35123
35476
  function workflowItems(suggestions, skillOf, skills, foundOn) {
@@ -35269,21 +35622,21 @@ function addCosts(data, c2) {
35269
35622
  var uniqueSessions = (traces) => [...new Map(traces.map((t) => [t.sessionId, t])).values()];
35270
35623
  function totals(traces) {
35271
35624
  const usage2 = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
35272
- let recorded = 0;
35625
+ let recorded2 = 0;
35273
35626
  for (const t of traces) {
35274
35627
  if (!t.usage) continue;
35275
- recorded++;
35628
+ recorded2++;
35276
35629
  usage2.input += t.usage.input;
35277
35630
  usage2.output += t.usage.output;
35278
35631
  usage2.cacheRead += t.usage.cacheRead;
35279
35632
  usage2.cacheWrite += t.usage.cacheWrite;
35280
35633
  }
35281
- return { sessions: traces.length, usage: usage2, measured: traces.length === 0 || recorded > 0 };
35634
+ return { sessions: traces.length, usage: usage2, measured: traces.length === 0 || recorded2 > 0 };
35282
35635
  }
35283
35636
  function cursorModels(traces) {
35284
35637
  const counts = /* @__PURE__ */ new Map();
35285
35638
  for (const t of traces) {
35286
- const m2 = t.source === "cursor" ? t.models?.[0] : void 0;
35639
+ const m2 = t.source === "cursor" && !t.usage ? t.models?.[0] : void 0;
35287
35640
  if (m2) counts.set(m2, (counts.get(m2) ?? 0) + 1);
35288
35641
  }
35289
35642
  if (!counts.size) return void 0;
@@ -35329,9 +35682,9 @@ function attention2(items) {
35329
35682
  return out2;
35330
35683
  }
35331
35684
  async function agentMemory(store) {
35332
- const entries = store.memoryEntries().filter((e2) => e2.status === "active").map((e2) => ({ text: toCells(e2.text), sessions: e2.sessions, origin: e2.origin }));
35685
+ const entries2 = store.memoryEntries().filter((e2) => e2.status === "active").map((e2) => ({ text: toCells(e2.text), sessions: e2.sessions, origin: e2.origin }));
35333
35686
  const files = (await memoryTargets()).filter((t) => t.detected).map((t) => toCells(displayPath(t.file)));
35334
- return { enabled: store.memoryState().enabled, entries, files };
35687
+ return { enabled: store.memoryState().enabled, entries: entries2, files };
35335
35688
  }
35336
35689
 
35337
35690
  // src/commands/tui.ts
@@ -35392,7 +35745,7 @@ function run(fn) {
35392
35745
  };
35393
35746
  }
35394
35747
  var program2 = new Command();
35395
- program2.name("reflex").description("Find workflows you keep re-teaching Claude Code, Codex and Cursor, and turn proven repeats into reusable skills. Everything runs locally.").version("0.5.1").allowExcessArguments().action(
35748
+ program2.name("reflex").description("Find workflows you keep re-teaching Claude Code, Codex and Cursor, and turn proven repeats into reusable skills. Everything runs locally.").version("0.5.2").allowExcessArguments().action(
35396
35749
  run(async () => {
35397
35750
  if (program2.args.length) program2.error(`error: unknown command '${program2.args[0]}'. See \`reflex --help\`.`);
35398
35751
  if (process.stdin.isTTY && process.stdout.isTTY) await tui();
@@ -35442,6 +35795,10 @@ route.command("status", { isDefault: true }).description("show picks, settings,
35442
35795
  route.command("autopilot <state>").description("on: reflex run starts on the pick without asking; off: it asks").action(run(routeAutopilot));
35443
35796
  route.command("explore <percent>").description("try another model you use on this share of reflex runs, chosen at random (off to stop)").option("--protect <kind...>", "kinds of task never explored: review, ci, debug, feature, edit, research, explore").action(run(async (value, o2) => routeExplore(value, o2)));
35444
35797
  program2.command("bench <task...>").description("run one task on several Claude Code models, each in a fresh copy of this repository, scored by your check command").requiredOption("--models <model...>", "models to compare, e.g. sonnet opus").requiredOption("--check <command>", 'command that passes (exit 0) when the task is done, e.g. "pnpm test"').option("--setup <command>", 'command to prepare each copy, e.g. "pnpm install --frozen-lockfile"').option("--runs <n>", "runs per model", "1").option("--timeout <minutes>", "time limit per run", "20").option("--keep", "keep the copies for inspection").option("-y, --yes", "start without asking").action(run(async (words2, o2) => bench(words2, o2)));
35798
+ var cursorUsage = program2.command("cursor-usage").description("record Cursor's token counts locally through a Cursor hook (Cursor saves none in its transcripts)");
35799
+ cursorUsage.command("status", { isDefault: true }).description("whether the hook is on and what it has recorded").action(run(cursorUsageStatus));
35800
+ cursorUsage.command("on").description("add Reflex's hook to ~/.cursor/hooks.json (asks first; keeps your other hooks)").option("-y, --yes", "add it without prompting").action(run(cursorUsageOn));
35801
+ cursorUsage.command("off").description("remove Reflex's hook; recorded counts stay").action(run(cursorUsageOff));
35445
35802
  program2.command("stats").description("defensible counts from local data").action(run(stats2));
35446
35803
  program2.command("reset").description("delete Reflex's own local data (not installed skills)").option("-y, --yes", "consent without prompting").action(run(reset));
35447
35804
  await program2.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflex-skills",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Turn repeated Claude Code and Codex workflows into reusable skills, locally.",
6
6
  "license": "MIT",