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.
- package/README.md +14 -4
- package/dist/reflex.js +472 -115
- 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
|
|
80
|
-
|
|
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
|
|
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: (
|
|
3465
|
-
down: (
|
|
3466
|
-
forward: (
|
|
3467
|
-
backward: (
|
|
3468
|
-
nextLine: (
|
|
3469
|
-
prevLine: (
|
|
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: (
|
|
3478
|
-
down: (
|
|
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: (
|
|
3483
|
-
down: (
|
|
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(
|
|
3487
|
+
lines(count3) {
|
|
3488
3488
|
let clear = "";
|
|
3489
|
-
for (let i = 0; i <
|
|
3490
|
-
clear += this.line + (i <
|
|
3491
|
-
if (
|
|
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(
|
|
5246
|
-
const numericValues = Object.values(
|
|
5247
|
-
const values = Object.entries(
|
|
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
|
|
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
|
-
|
|
5787
|
+
count3--;
|
|
5788
5788
|
i++;
|
|
5789
5789
|
}
|
|
5790
5790
|
}
|
|
5791
|
-
return
|
|
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 = (
|
|
6568
|
-
const escapedDelim = escapeRegex(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
10147
|
-
const absCount = Math.abs(
|
|
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(
|
|
12795
|
-
return Math.abs(
|
|
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(
|
|
15534
|
-
const absCount = Math.abs(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
24590
|
+
let entries2;
|
|
24591
24591
|
try {
|
|
24592
|
-
|
|
24592
|
+
entries2 = await readdir(current, { withFileTypes: true });
|
|
24593
24593
|
} catch {
|
|
24594
24594
|
return;
|
|
24595
24595
|
}
|
|
24596
|
-
for (const entry of
|
|
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
|
-
|
|
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,
|
|
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,
|
|
25999
|
-
const w2 = (1 + Math.log(
|
|
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
|
|
27012
|
-
if (
|
|
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
|
|
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 =
|
|
27553
|
+
const inside = count3(group, s.category) / group.length;
|
|
27444
27554
|
if (inside < WHEN_SHARE) continue;
|
|
27445
|
-
const lift = inside / ((
|
|
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,
|
|
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
|
|
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
|
|
30883
|
+
let entries2;
|
|
30771
30884
|
try {
|
|
30772
|
-
|
|
30885
|
+
entries2 = await readdir3(t.skillsDir, { withFileTypes: true });
|
|
30773
30886
|
} catch {
|
|
30774
30887
|
continue;
|
|
30775
30888
|
}
|
|
30776
|
-
for (const e2 of
|
|
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
|
|
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
|
|
31535
|
-
return { dir, repos:
|
|
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
|
|
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
|
-
|
|
31670
|
-
|
|
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(
|
|
31960
|
+
function find(entries2, ref) {
|
|
31684
31961
|
const n = Number(ref.replace(/^#/, ""));
|
|
31685
|
-
const e2 = Number.isInteger(n) && n > 0 ?
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
31924
|
-
var onPath = (cmd) => (process.env.PATH ?? "").split(
|
|
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
|
|
31941
|
-
out(` ${n.padEnd(28)} ${c.dim(where.join(", ").padEnd(28))} ${
|
|
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 =
|
|
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 =
|
|
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(),
|
|
32430
|
+
const proposalDir = assertInside(paths.home(), join21(paths.home(), "proposals", name));
|
|
32078
32431
|
await ensureDir(paths.home(), 448);
|
|
32079
|
-
await ensureDir(
|
|
32432
|
+
await ensureDir(join21(paths.home(), "proposals"), 448);
|
|
32080
32433
|
await ensureDir(proposalDir, 448);
|
|
32081
|
-
const proposal =
|
|
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,
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
33170
|
-
const cap = Math.max(2, Math.min(8, h2 - L3.length -
|
|
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
|
|
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
|
|
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 =
|
|
35089
|
+
const dir = join23(paths.skills(), name);
|
|
34737
35090
|
if (await isSymlink(paths.skills()) || await isSymlink(dir)) return void 0;
|
|
34738
|
-
return readCapped(
|
|
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(
|
|
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
|
|
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
|
|
35357
|
+
const entries2 = [];
|
|
35005
35358
|
texts.forEach(([role, raw], i) => {
|
|
35006
35359
|
const text = toCells(raw).trim();
|
|
35007
|
-
if (text)
|
|
35360
|
+
if (text) entries2.push({ ...base, turn: i + 1, role, text, lower: text.toLowerCase() });
|
|
35008
35361
|
});
|
|
35009
|
-
bySession.set(key3,
|
|
35010
|
-
all.push(...
|
|
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 =
|
|
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 =
|
|
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 =
|
|
35397
|
+
n = readSync2(fd, buf, 0, buf.length, 0);
|
|
35045
35398
|
} finally {
|
|
35046
|
-
|
|
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(
|
|
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`) => `${
|
|
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 ${
|
|
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(`- +${
|
|
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
|
|
35625
|
+
let recorded2 = 0;
|
|
35273
35626
|
for (const t of traces) {
|
|
35274
35627
|
if (!t.usage) continue;
|
|
35275
|
-
|
|
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 ||
|
|
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
|
|
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.
|
|
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);
|