valistry 0.1.0 → 0.1.1

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 CHANGED
@@ -20,6 +20,8 @@ If Claude Code already sends to Valistry, you can leave out `--key`. Setup then
20
20
  | Codex | `~/.codex/config.toml` | Adds an `[otel]` table at the end, with prompts off. Keeps every other line. A copy is saved first. |
21
21
  | Desktop apps (`--desktop`) | macOS: `~/Library/LaunchAgents/ai.valistry.desktop.plist`. Windows: a Scheduled Task, "Valistry desktop" | Runs a small observer once a minute. Its files are in `~/.valistry`. |
22
22
 
23
+ | Each session's work (`--deep`) | `~/.valistry/valistry-session.mjs`, and a `SessionEnd` hook in each tool's settings | When a session ends, reads its transcript on this computer and sends counts only (tests, edits, how it ended). Sends nothing until deep access is on in Valistry. Codex asks you to trust the hook once, with `/hooks`. |
24
+
23
25
  A tool that is not on the computer is skipped. Telemetry that already goes to another address is not replaced unless you add `--force`.
24
26
 
25
27
  Options: `--team <name>` names your team. `--endpoint <url>` sends to another Valistry address (the default is the address found with the key, else `https://app.valistry.ai/api/otlp`). `--no-claude` and `--no-codex` leave a tool alone.
package/bin/valistry.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // valistry 0.1.0: sets up this computer's AI tools to send their use to Valistry. Built from packages/cli/src in the
2
+ // valistry 0.1.1: sets up this computer's AI tools to send their use to Valistry. Built from packages/cli/src in the
3
3
  // Valistry repository by packages/cli/build.mjs: do not edit. Counts only: never prompts, replies, code, window titles,
4
4
  // screen contents or keys typed.
5
5
 
@@ -14,7 +14,9 @@ import { createHash as createHash2 } from "node:crypto";
14
14
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, copyFileSync, chmodSync } from "node:fs";
15
15
  import { basename, dirname, join as join2 } from "node:path";
16
16
 
17
- // packages/connectors/src/adapters/telemetry-env.ts
17
+ // packages/cli/src/vendor.ts
18
+ var DESKTOP_APPS = ["claude", "chatgpt", "copilot"];
19
+ var DESKTOP_METRIC = "valistry.desktop.active_minutes";
18
20
  var CLAUDE_CODE_ENV = (endpoint, key) => [
19
21
  ["CLAUDE_CODE_ENABLE_TELEMETRY", "1"],
20
22
  ["OTEL_METRICS_EXPORTER", "otlp"],
@@ -22,7 +24,8 @@ var CLAUDE_CODE_ENV = (endpoint, key) => [
22
24
  ["OTEL_EXPORTER_OTLP_PROTOCOL", "http/json"],
23
25
  ["OTEL_EXPORTER_OTLP_ENDPOINT", endpoint],
24
26
  ["OTEL_EXPORTER_OTLP_HEADERS", `Authorization=Bearer ${key}`],
25
- ["OTEL_METRICS_INCLUDE_SESSION_ID", "true"]
27
+ ["OTEL_METRICS_INCLUDE_SESSION_ID", "true"],
28
+ ["OTEL_METRIC_EXPORT_INTERVAL", "300000"]
26
29
  ];
27
30
  var teamAttribute = (team) => `team.id=${team.trim().replace(/[,=\s]+/g, "-").slice(0, 64)}`;
28
31
  var CLAUDE_CODE_HOOKS = (endpoint, key) => ({
@@ -54,7 +57,9 @@ function parseSettings(text) {
54
57
  }
55
58
  var printSettings = (o) => `${JSON.stringify(o, null, 2)}
56
59
  `;
57
- var ours = (h) => isObj(h) && h.type === "http" && typeof h.url === "string" && /\/hooks\/claude-code$/.test(h.url) && isObj(h.headers) && typeof h.headers.Authorization === "string" && /^Bearer vly_ot_/.test(h.headers.Authorization);
60
+ var SESSION_HOOK = { hooks: [{ type: "command", command: "node ~/.valistry/valistry-session.mjs", timeout: 30 }] };
61
+ var isSession = (h) => isObj(h) && h.type === "command" && typeof h.command === "string" && h.command.includes("valistry-session.mjs");
62
+ var ours = (h) => isSession(h) || isObj(h) && h.type === "http" && typeof h.url === "string" && /\/hooks\/claude-code$/.test(h.url) && isObj(h.headers) && typeof h.headers.Authorization === "string" && /^Bearer vly_ot_/.test(h.headers.Authorization);
58
63
  function withoutOurHooks(hooks) {
59
64
  if (!isObj(hooks)) return hooks === void 0 ? void 0 : hooks;
60
65
  const out = {};
@@ -88,6 +93,7 @@ function mergeClaude(text, o) {
88
93
  const hooks = withoutOurHooks(s.hooks) ?? {};
89
94
  const mine = CLAUDE_CODE_HOOKS(o.endpoint, o.key);
90
95
  for (const [event, groups] of Object.entries(mine)) hooks[event] = [...Array.isArray(hooks[event]) ? hooks[event] : [], ...groups];
96
+ if (o.deep || JSON.stringify(s.hooks ?? null).includes("valistry-session.mjs")) hooks.SessionEnd.push(SESSION_HOOK);
91
97
  const after = printSettings({ ...s, env, hooks });
92
98
  return { after, added, changed, hooks: Object.keys(mine) };
93
99
  }
@@ -127,6 +133,12 @@ function claudeConfigured(text, endpoint) {
127
133
  // packages/cli/src/codex.ts
128
134
  var BEGIN = "# Valistry telemetry, added by npx valistry. Remove it with: npx valistry remove --apply";
129
135
  var END = "# End of Valistry telemetry.";
136
+ var SESSION = "valistry-session.mjs";
137
+ var SESSION_HOOK2 = `[[hooks.SessionEnd]]
138
+ [[hooks.SessionEnd.hooks]]
139
+ type = "command"
140
+ command = "node ~/.valistry/${SESSION} --codex"
141
+ timeout = 3`;
130
142
  var lines = (t) => (t ?? "").split(/\r?\n/);
131
143
  function withoutOurs(text) {
132
144
  const out = [];
@@ -178,8 +190,10 @@ function addCodex(text, o) {
178
190
  const other = hasOtel(text);
179
191
  const base = other ? withoutOtel(withoutOurs(text)) : withoutOurs(text);
180
192
  const head = tidy(base);
193
+ const session = (o.deep || (text ?? "").includes(SESSION)) && !head.includes(SESSION);
181
194
  return { after: `${head}${head ? "\n" : ""}${BEGIN}
182
- ${CODEX_OTEL(o.endpoint, o.key, o.person)}
195
+ ${CODEX_OTEL(o.endpoint, o.key, o.person)}${session ? `
196
+ ${SESSION_HOOK2}` : ""}
183
197
  ${END}
184
198
  `, replaced: other };
185
199
  }
@@ -193,12 +207,6 @@ var codexConfigured = (text) => lines(text).includes(BEGIN);
193
207
 
194
208
  // packages/cli/src/desktop.ts
195
209
  import { createHash } from "node:crypto";
196
-
197
- // packages/core/src/desktopUse.ts
198
- var DESKTOP_APPS = ["claude", "chatgpt", "copilot"];
199
- var DESKTOP_METRIC = "valistry.desktop.active_minutes";
200
-
201
- // packages/cli/src/desktop.ts
202
210
  var IDLE_SECONDS = 300;
203
211
  var SEND_EVERY_MS = 15 * 6e4;
204
212
  var KEEP_DAYS = 10;
@@ -366,6 +374,7 @@ var pathsOf = (home) => {
366
374
  backups: join2(dir, "backups"),
367
375
  manifest: join2(dir, "setup.json"),
368
376
  config: join2(dir, "desktop.json"),
377
+ session: join2(dir, "valistry-session.mjs"),
369
378
  state: join2(dir, "desktop-state.json"),
370
379
  script: join2(dir, "valistry.mjs"),
371
380
  log: join2(dir, "desktop.log"),
@@ -422,6 +431,7 @@ function foundKey(home) {
422
431
  return null;
423
432
  }
424
433
  var cmd = ([c, args]) => ({ cmd: c, args });
434
+ var SESSION_SCRIPT = true ? '#!/usr/bin/env node\n// Valistry\'s session hook for Claude Code and Codex. Built from scripts/hooks/valistry-session.ts in the Valistry repository by\n// scripts/hooks/build.mjs: do not edit. It reads this machine\'s transcript, keeps counts only, and sends what your\n// organization\'s deep-access grant keeps. Prompts, code, commands and file paths never leave this machine.\n\n// scripts/hooks/valistry-session.ts\nimport { createReadStream, existsSync, readdirSync, readFileSync, statSync } from "node:fs";\nimport { createInterface } from "node:readline";\nimport { execFileSync } from "node:child_process";\nimport { basename, dirname, join } from "node:path";\nimport { homedir } from "node:os";\n\n// packages/core/src/sha256.ts\nvar K = new Uint32Array([\n 1116352408,\n 1899447441,\n 3049323471,\n 3921009573,\n 961987163,\n 1508970993,\n 2453635748,\n 2870763221,\n 3624381080,\n 310598401,\n 607225278,\n 1426881987,\n 1925078388,\n 2162078206,\n 2614888103,\n 3248222580,\n 3835390401,\n 4022224774,\n 264347078,\n 604807628,\n 770255983,\n 1249150122,\n 1555081692,\n 1996064986,\n 2554220882,\n 2821834349,\n 2952996808,\n 3210313671,\n 3336571891,\n 3584528711,\n 113926993,\n 338241895,\n 666307205,\n 773529912,\n 1294757372,\n 1396182291,\n 1695183700,\n 1986661051,\n 2177026350,\n 2456956037,\n 2730485921,\n 2820302411,\n 3259730800,\n 3345764771,\n 3516065817,\n 3600352804,\n 4094571909,\n 275423344,\n 430227734,\n 506948616,\n 659060556,\n 883997877,\n 958139571,\n 1322822218,\n 1537002063,\n 1747873779,\n 1955562222,\n 2024104815,\n 2227730452,\n 2361852424,\n 2428436474,\n 2756734187,\n 3204031479,\n 3329325298\n]);\nvar rotr = (x, n) => x >>> n | x << 32 - n;\nfunction sha256(text) {\n const bytes = new TextEncoder().encode(text);\n const bitLen = bytes.length * 8;\n const padded = new Uint8Array(bytes.length + 9 + 63 >> 6 << 6);\n padded.set(bytes);\n padded[bytes.length] = 128;\n const view = new DataView(padded.buffer);\n view.setUint32(padded.length - 8, Math.floor(bitLen / 4294967296));\n view.setUint32(padded.length - 4, bitLen >>> 0);\n const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);\n const W = new Uint32Array(64);\n for (let off = 0; off < padded.length; off += 64) {\n for (let i = 0; i < 16; i++) W[i] = view.getUint32(off + i * 4);\n for (let i = 16; i < 64; i++) {\n const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ W[i - 15] >>> 3;\n const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ W[i - 2] >>> 10;\n W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;\n }\n let [a, b, c, d, e, f, g, h] = H;\n for (let i = 0; i < 64; i++) {\n const t1 = h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + W[i] >>> 0;\n const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;\n h = g;\n g = f;\n f = e;\n e = d + t1 >>> 0;\n d = c;\n c = b;\n b = a;\n a = t1 + t2 >>> 0;\n }\n H[0] += a;\n H[1] += b;\n H[2] += c;\n H[3] += d;\n H[4] += e;\n H[5] += f;\n H[6] += g;\n H[7] += h;\n }\n return Array.from(H, (x) => x.toString(16).padStart(8, "0")).join("");\n}\nvar shortCode = (salt, value) => sha256(`${salt}\\0${value}`).slice(0, 16);\n\n// packages/core/src/sessionSignals.ts\nvar GAP_MS = 5 * 60 * 1e3;\nvar EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);\nvar SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Task", "Agent"]);\nvar READ_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system"]);\nvar TEST_RUNNERS = [\n /^(npm|pnpm|yarn|bun)\\s+(run\\s+)?(test|e2e)(\\b|:)/,\n /^(npm|pnpm|yarn)\\s+(run\\s+)?test:\\S+/,\n /^(vitest|jest|mocha|ava|jasmine|karma|playwright\\s+test|cypress\\s+run)\\b/,\n /^(pytest|py\\.test|nosetests|tox|nox)\\b/,\n /^python\\d*(\\.\\d+)?\\s+-m\\s+(pytest|unittest)\\b/,\n /^go\\s+test\\b/,\n /^cargo\\s+(test|nextest)\\b/,\n /^(mvn|\\.\\/mvnw|mvnw)\\b.*\\b(test|verify)\\b/,\n /^(gradle|\\.\\/gradlew|gradlew)\\b.*\\b(test|check)\\b/,\n /^(rspec|phpunit|\\.\\/vendor\\/bin\\/phpunit|vendor\\/bin\\/phpunit|pest)\\b/,\n /^rake\\s+(test|spec)\\b/,\n /^dotnet\\s+test\\b/,\n /^mix\\s+test\\b/,\n /^ctest\\b/,\n /^deno\\s+test\\b/,\n /^swift\\s+test\\b/,\n /^make\\s+(test|check)\\b/,\n /^bazel\\s+test\\b/\n];\nvar FAIL_SUMMARY = [\n /^\\s*Tests?:?\\s+\\d+ failed\\b/m,\n /^\\s*Test Files\\s+\\d+ failed\\b/m,\n /^=+ .*\\b\\d+ (failed|error)s?\\b.* =+\\s*$/m,\n /^(--- FAIL:|FAIL\\s+\\S+\\s+[\\d.]+s$)/m,\n /^test result: FAILED\\b/m,\n /^\\s*\\d+ failing\\s*$/m,\n /^\\d+ examples?, [1-9]\\d* failures?\\b/m,\n /^FAILURES!\\s*$/m,\n /^Tests run: \\d+, Failures: [1-9]/m,\n /^\\[ERROR\\] Tests run:.*Failures: [1-9]/m,\n /^BUILD FAILED\\b/m,\n /^\\s*\\d+ failed\\b.*\\bpassed\\b/m\n];\nvar WRAPPERS = /^(sudo|time|nice|env|npx|bunx|pnpx|exec|(pnpm|yarn)\\s+exec|uv\\s+run|poetry\\s+run|pipenv\\s+run|bundle\\s+exec|timeout\\s+\\S+)\\s+/;\nvar ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=("[^"]*"|\'[^\']*\'|\\S*)\\s+/;\nfunction commandsOf(line) {\n const bare = line.replace(/<<-?\\s*([\'"]?)(\\w+)\\1[^\\n]*\\n[\\s\\S]*?(\\n\\s*\\2\\s*(\\n|$)|$)/g, "\\n").replace(/"(\\\\.|[^"\\\\])*"/g, \'""\').replace(/\'[^\']*\'/g, "\'\'");\n return bare.split(/&&|\\|\\||;|\\||\\n/).map((s) => {\n let c = s.trim().replace(/^[({]\\s*/, "");\n for (let i = 0; i < 6; i++) {\n const before = c;\n c = c.replace(ENV_ASSIGN, "").replace(WRAPPERS, "");\n if (c === before) break;\n }\n return c;\n }).filter(Boolean);\n}\nvar isTestCommand = (line) => commandsOf(line).some((c) => TEST_RUNNERS.some((r) => r.test(c)));\nvar isGit = (line, sub) => commandsOf(line).some((c) => /^git\\b/.test(c) && sub.test(c.replace(/^git\\s+(-C\\s+\\S+\\s+|-c\\s+\\S+\\s+|--no-pager\\s+)*/, "")));\nvar isCommitCommand = (line) => isGit(line, /^commit\\b/);\nvar isPrCreateCommand = (line) => commandsOf(line).some((c) => /^gh\\s+pr\\s+create\\b/.test(c));\nvar toolName = (name) => typeof name !== "string" ? "other" : name.startsWith("mcp__") ? "mcp" : /^[A-Za-z][A-Za-z0-9_]{0,40}$/.test(name) ? name : "other";\nvar obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;\nvar str = (v) => typeof v === "string" && v ? v : null;\nvar int = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;\nvar ms = (v) => {\n if (typeof v !== "string") return null;\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : null;\n};\nvar chars = (s) => [...s].length;\nvar resultText = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => typeof obj(p)?.text === "string" ? obj(p).text : "").join("") : "";\nvar exitCode = (c) => {\n const m = /^\\s*Exit code (\\d+)/.exec(resultText(c).slice(0, 40));\n return m ? Number(m[1]) : null;\n};\nvar NOT_A_PROMPT = /^\\s*<(task-notification|local-command-stdout|local-command-stderr|bash-stdout|bash-stderr|system-reminder)>/;\nvar INTERRUPT = /^\\s*\\[Request interrupted by user/;\nvar zero = () => ({ input: 0, output: 0, cache_write: 0, cache_read: 0 });\nfunction promptOf(e) {\n if (e.type !== "user" || e.isMeta === true || e.isSidechain === true || e.isCompactSummary === true) return null;\n const content = obj(e.message)?.content;\n if (Array.isArray(content) && content.some((p) => obj(p)?.type === "tool_result")) return null;\n const typed = typeof content === "string" ? content : Array.isArray(content) ? content.map((p) => obj(p)?.type === "text" && typeof obj(p).text === "string" ? obj(p).text : "").join("\\n") : "";\n if (INTERRUPT.test(typed)) return "interrupt";\n if (!typed.trim() && !(Array.isArray(content) && content.some((p) => obj(p)?.type === "image"))) return null;\n if (NOT_A_PROMPT.test(typed)) return null;\n return typed;\n}\nfunction entriesOf(jsonl) {\n const out = [];\n for (const raw of jsonl.split("\\n")) {\n const text = raw.trim();\n if (!text) continue;\n let e;\n try {\n e = obj(JSON.parse(text));\n } catch {\n e = null;\n }\n out.push(e && typeof e.type === "string" ? e : null);\n }\n return out;\n}\nfunction resultFailed(part) {\n const exit = exitCode(part.content);\n return part.is_error === true || exit !== null && exit > 0;\n}\nfunction sessionSignals(jsonl, opts) {\n if (!opts.salt) throw new Error("sessionSignals needs the organization\'s salt");\n const code = (v) => shortCode(opts.salt, v);\n let lines = 0, unread = 0, bad = 0;\n let sessionId = null, branch = null, version = null;\n const times = [];\n let turns = 0, promptChars = 0, interrupts = 0, firstPromptAt = null;\n let boundaries = 0, compactSummaries = 0, apiErrors = 0;\n const calls = [];\n const results = /* @__PURE__ */ new Map();\n const usage = /* @__PURE__ */ new Map();\n const models = /* @__PURE__ */ new Set();\n let lastResultFailed = false, apiErrorLast = false;\n let order = 0;\n for (const e of entriesOf(jsonl)) {\n lines++;\n if (!e) {\n bad++;\n continue;\n }\n if (!READ_TYPES.has(e.type)) {\n unread++;\n continue;\n }\n const at = ms(e.timestamp);\n if (at !== null) times.push(at);\n sessionId ??= str(e.sessionId);\n const b = str(e.gitBranch);\n if (b && b !== "HEAD") branch = b;\n const v = str(e.version);\n if (v && /^\\d+\\.\\d+\\.\\d+/.test(v)) version = v.slice(0, 40);\n const msg = obj(e.message);\n if (e.type === "system") {\n if (e.subtype === "compact_boundary") boundaries++;\n if (e.subtype === "api_error") {\n apiErrors++;\n apiErrorLast = true;\n }\n continue;\n }\n if (e.type === "assistant") {\n const model = str(msg?.model);\n if (model && model !== "<synthetic>") {\n models.add(model);\n const u = obj(msg?.usage);\n if (u) usage.set(str(msg?.id) ?? str(e.requestId) ?? str(e.uuid) ?? `line${lines}`, { model, t: { input: int(u.input_tokens), output: int(u.output_tokens), cache_write: int(u.cache_creation_input_tokens), cache_read: int(u.cache_read_input_tokens) } });\n }\n const content2 = Array.isArray(msg?.content) ? msg.content : [];\n for (const p of content2) {\n const part = obj(p);\n if (!part) continue;\n if (part.type === "tool_use") {\n const id = str(part.id) ?? `call${order}`;\n calls.push({ id, name: toolName(part.name), at, order: order++, input: obj(part.input) });\n }\n if (part.type === "text" || part.type === "tool_use") apiErrorLast = false;\n }\n continue;\n }\n const content = msg?.content;\n if (Array.isArray(content) && content.some((p) => obj(p)?.type === "tool_result")) {\n for (const p of content) {\n const part = obj(p);\n if (part?.type !== "tool_result") continue;\n const error = resultFailed(part);\n const id = str(part.tool_use_id);\n if (id) results.set(id, { error, failSummary: FAIL_SUMMARY.some((f) => f.test(resultText(part.content).slice(-2e4))) });\n lastResultFailed = error;\n }\n continue;\n }\n if (e.isCompactSummary === true) {\n compactSummaries++;\n continue;\n }\n const prompt = promptOf(e);\n if (prompt === "interrupt") {\n interrupts++;\n continue;\n }\n if (prompt === null) continue;\n turns++;\n promptChars += chars(prompt);\n if (firstPromptAt === null && at !== null) firstPromptAt = at;\n }\n const toolCalls = {};\n let toolErrors = 0, edits = 0, redone = 0, testsRun = 0, testsFailed = 0, failed = 0, commits = 0, prs = 0, subagents = 0;\n let firstEditAt = null;\n const edited = /* @__PURE__ */ new Set();\n const failing = /* @__PURE__ */ new Map();\n for (const c of calls) {\n toolCalls[c.name] = (toolCalls[c.name] ?? 0) + 1;\n const r = results.get(c.id);\n const error = r?.error === true;\n if (error) toolErrors++;\n if (SUBAGENT_TOOLS.has(c.name)) subagents++;\n if (EDIT_TOOLS.has(c.name)) {\n edits++;\n if (firstEditAt === null && c.at !== null) firstEditAt = c.at;\n const path = str(c.input?.file_path) ?? str(c.input?.notebook_path);\n if (path && r && !error) {\n const k = code(path);\n if (edited.has(k)) redone++;\n else edited.add(k);\n }\n }\n if (c.name === "Bash") {\n const command = str(c.input?.command) ?? "";\n const ran = r !== void 0;\n if (isTestCommand(command)) {\n testsRun++;\n if (error || r?.failSummary) testsFailed++;\n }\n if (error) {\n failed++;\n const k = code(command.trim().replace(/\\s+/g, " "));\n failing.set(k, (failing.get(k) ?? 0) + 1);\n }\n if (ran && !error && isCommitCommand(command)) commits++;\n if (ran && !error && isPrCreateCommand(command)) prs++;\n }\n }\n let loops = 0, loopRuns = 0;\n for (const n of failing.values()) if (n >= 3) {\n loops++;\n loopRuns += n;\n }\n const tokens = zero();\n const byModel = {};\n for (const { model, t } of usage.values()) {\n const m = byModel[model] ??= zero();\n for (const k of ["input", "output", "cache_write", "cache_read"]) {\n tokens[k] += t[k];\n m[k] += t[k];\n }\n }\n times.sort((a, b) => a - b);\n let active = 0;\n for (let i = 1; i < times.length; i++) {\n const gap = times[i] - times[i - 1];\n if (gap <= GAP_MS) active += gap;\n }\n let trace = null;\n if (branch && opts.traceFrom) {\n const m = opts.traceFrom.exec(branch);\n const t = m ? m[1] ?? m[0] : null;\n trace = t && /^[A-Za-z0-9._:-]{1,80}$/.test(t) ? t : null;\n }\n const ended = prs > 0 ? "pr" : commits > 0 ? "commit" : lastResultFailed || apiErrorLast ? "error" : "no_commit";\n return {\n session: code(sessionId ?? ""),\n started_at: times.length ? new Date(times[0]).toISOString() : null,\n ended_at: times.length ? new Date(times[times.length - 1]).toISOString() : null,\n active_ms: active,\n turns,\n prompt_chars: promptChars,\n interrupts,\n tool_calls: Object.fromEntries(Object.entries(toolCalls).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),\n tool_errors: toolErrors,\n edits,\n edits_redone: redone,\n files_edited: edited.size,\n tests_run: testsRun,\n tests_failed: testsFailed,\n failed_commands: failed,\n loops,\n loop_runs: loopRuns,\n first_edit_ms: firstPromptAt !== null && firstEditAt !== null ? Math.max(0, firstEditAt - firstPromptAt) : null,\n compactions: Math.max(boundaries, compactSummaries),\n subagents,\n api_errors: apiErrors,\n commits_made: commits,\n pr_opened: prs,\n branch: branch ? code(branch) : null,\n trace_id: trace,\n models: [...models].sort(),\n tokens,\n tokens_by_model: byModel,\n ended_with: ended,\n cli_version: version,\n lines,\n lines_unread: unread,\n lines_bad: bad\n };\n}\n\n// packages/core/src/seatLines.ts\nvar LONG_CONTEXT = 15e4;\nvar HEAVY_START = 5e4;\n\n// packages/core/src/lists.ts\nfunction maxOf(xs, floor = -Infinity) {\n let m = floor;\n for (const x of xs) m = Math.max(m, x);\n return m;\n}\n\n// packages/core/src/codexSignals.ts\nvar obj2 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;\nvar str2 = (v) => typeof v === "string" && v ? v : null;\nvar int2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;\nvar ms2 = (v) => {\n if (typeof v !== "string") return null;\n const t = Date.parse(v);\n return Number.isFinite(t) ? t : null;\n};\nvar chars2 = (s) => [...s].length;\nvar zero2 = () => ({ input: 0, output: 0, cache_write: 0, cache_read: 0 });\nvar GAP_MS2 = 5 * 60 * 1e3;\nvar READ_TYPES2 = /* @__PURE__ */ new Set(["session_meta", "turn_context", "event_msg", "response_item", "compacted", "token_usage_record"]);\nvar MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,79}$/;\nvar SHELL_TOOLS = /* @__PURE__ */ new Set(["shell", "container.exec", "shell_command", "exec_command", "local_shell", "local_shell_call"]);\nvar INJECTED = /^\\s*(<[a-z_]+>|# AGENTS\\.md|<INSTRUCTIONS>)/;\nfunction isCodexTranscript(firstLine2) {\n try {\n const e = obj2(JSON.parse(firstLine2));\n return e?.type === "session_meta" && obj2(e.payload) !== null;\n } catch {\n return false;\n }\n}\nfunction codexTool(name, namespace) {\n if (typeof namespace === "string" && namespace) return namespace.startsWith("mcp__") ? "mcp" : toolName(namespace);\n if (typeof name !== "string") return "other";\n if (SHELL_TOOLS.has(name)) return "shell";\n if (name.startsWith("mcp__") || name.includes("__")) return "mcp";\n return toolName(name);\n}\nfunction shellLine(command) {\n if (typeof command === "string") return command;\n if (!Array.isArray(command) || !command.every((w2) => typeof w2 === "string")) return "";\n const w = command;\n if (w.length >= 3 && /(^|\\/)(ba|z|da)?sh$/.test(w[0]) && /^-l?c$/.test(w[1])) return w.slice(2).join(" ");\n return w.join(" ");\n}\nfunction patchFiles(patch) {\n const out = [];\n for (const m of patch.matchAll(/^\\*\\*\\* (?:Add|Update|Delete) File: (.+)$|^\\*\\*\\* Move to: (.+)$/gm)) {\n const p = (m[1] ?? m[2]).trim();\n if (p) out.push(p);\n }\n return [...new Set(out)];\n}\nvar outputText = (o) => typeof o === "string" ? o : Array.isArray(o) ? o.map((p) => typeof obj2(p)?.text === "string" ? obj2(p).text : "").join("") : "";\nfunction exitOf(output) {\n let text = outputText(output);\n if (text.startsWith("{")) {\n try {\n const j = obj2(JSON.parse(text));\n const meta = obj2(j?.metadata);\n if (typeof j?.output === "string") text = j.output;\n if (typeof meta?.exit_code === "number") return { code: meta.exit_code, text };\n } catch {\n }\n }\n const head = text.slice(0, 600);\n const m = /^Exit code: (-?\\d+)/m.exec(head) ?? /Process exited with code (-?\\d+)/.exec(head);\n return { code: m ? Number(m[1]) : null, text };\n}\nvar failSummaryIn = (text) => FAIL_SUMMARY.some((f) => f.test(text.slice(-2e4)));\nfunction codexSignalsReader(opts) {\n if (!opts.salt) throw new Error("codexSignals needs the organization\'s salt");\n const code = (v) => shortCode(opts.salt, v);\n let lines = 0, unread = 0, bad = 0, order = 0;\n let sessionId = null, branch = null, version = null;\n const times = [];\n const threads = [];\n let thread = -1;\n const cur = () => {\n if (thread < 0) {\n thread = 0;\n threads.push({ items: false, model: null, prevTotal: null, recordIds: /* @__PURE__ */ new Set(), hasTokenCount: false });\n }\n return threads[thread];\n };\n const prompts = { item: { n: 0, chars: 0, first: null }, event: { n: 0, chars: 0, first: null }, message: { n: 0, chars: 0, first: null } };\n let interrupts = 0, turnEnds = 0, apiErrors = 0, apiErrorLast = false, apiErrorOrder = -1;\n const compactions = { line: 0, item: 0, event: 0, response: 0 };\n const acts = [];\n const calls = /* @__PURE__ */ new Map();\n const models = /* @__PURE__ */ new Set();\n const tokens = zero2();\n const byModel = {};\n let usageSeen = false;\n const contexts = [];\n const addUsage = (u, main2) => {\n const input = int2(u.input_tokens), cached = Math.min(int2(u.cached_input_tokens), input), write = Math.min(int2(u.cache_write_input_tokens), input - cached);\n const t = { input: input - cached - write, output: int2(u.output_tokens), cache_write: write, cache_read: cached };\n if (input + t.output === 0) return;\n usageSeen = true;\n const m = byModel[cur().model ?? "unknown"] ??= zero2();\n for (const k of ["input", "output", "cache_write", "cache_read"]) {\n tokens[k] += t[k];\n m[k] += t[k];\n }\n if (main2) contexts.push(input);\n };\n const prompt = (way, text, at) => {\n const p = prompts[way];\n p.n++;\n p.chars += chars2(text);\n if (p.first === null && at !== null) p.first = at;\n };\n const shellOf = (line) => ({ code: code(line.trim().replace(/\\s+/g, " ")), test: isTestCommand(line), commit: isCommitCommand(line), pr: isPrCreateCommand(line) });\n return {\n push(raw) {\n const text = raw.trim();\n if (!text) return;\n lines++;\n let e;\n try {\n e = obj2(JSON.parse(text));\n } catch {\n e = null;\n }\n if (!e || typeof e.type !== "string") {\n bad++;\n return;\n }\n if (!READ_TYPES2.has(e.type)) {\n unread++;\n return;\n }\n const at = ms2(e.timestamp);\n if (at !== null) times.push(at);\n const p = obj2(e.payload) ?? {};\n if (e.type === "session_meta") {\n thread++;\n threads.push({ items: false, model: null, prevTotal: null, recordIds: /* @__PURE__ */ new Set(), hasTokenCount: false });\n if (thread === 0) {\n sessionId = str2(p.session_id) ?? str2(p.id);\n const b = str2(obj2(p.git)?.branch);\n if (b && b !== "HEAD") branch = b;\n const v = str2(p.cli_version);\n if (v && /^\\d+\\.\\d+\\.\\d+/.test(v)) version = v.slice(0, 40);\n }\n return;\n }\n const t = cur();\n const main2 = thread === 0;\n if (e.type === "turn_context") {\n const m = str2(p.model) ?? str2(obj2(obj2(p.collaboration_mode)?.settings)?.model);\n if (m) {\n t.model = MODEL.test(m) ? m : "other";\n models.add(t.model);\n }\n return;\n }\n if (e.type === "compacted") {\n if (main2) compactions.line++;\n return;\n }\n if (e.type === "token_usage_record") {\n const id = str2(p.response_id);\n const u = obj2(p.usage);\n if (u && !t.hasTokenCount && !(id && t.recordIds.has(id))) {\n if (id) t.recordIds.add(id);\n addUsage(u, main2);\n }\n return;\n }\n if (e.type === "event_msg") {\n const kind2 = p.type;\n if (kind2 === "token_count") {\n const info = obj2(p.info);\n const total = obj2(info?.total_token_usage);\n const last = obj2(info?.last_token_usage);\n if (!info || !last) return;\n if (!t.hasTokenCount) {\n t.hasTokenCount = true;\n }\n const sum = typeof total?.total_tokens === "number" ? total.total_tokens : null;\n if (sum !== null && sum === t.prevTotal) return;\n t.prevTotal = sum;\n addUsage(last, main2);\n return;\n }\n if (kind2 === "user_message" && main2) {\n const m = typeof p.message === "string" ? p.message : "";\n if (!INJECTED.test(m)) prompt("event", m, at);\n return;\n }\n if (kind2 === "turn_aborted") {\n if (main2) {\n turnEnds++;\n if (p.reason === "interrupted") interrupts++;\n }\n return;\n }\n if (kind2 === "task_started") {\n if (main2) turnEnds++;\n return;\n }\n if (kind2 === "task_complete") {\n if (main2) {\n turnEnds++;\n if (obj2(p.error)) {\n apiErrors++;\n apiErrorLast = true;\n apiErrorOrder = order;\n } else apiErrorLast = false;\n }\n return;\n }\n if (kind2 === "error") {\n if (main2) {\n turnEnds++;\n apiErrors++;\n apiErrorLast = true;\n apiErrorOrder = order;\n }\n return;\n }\n if (kind2 === "context_compacted") {\n if (main2) compactions.event++;\n return;\n }\n if (kind2 === "exec_command_end" || kind2 === "patch_apply_end") {\n const a = calls.get(str2(p.call_id) ?? "");\n if (!a) return;\n const c = typeof p.exit_code === "number" ? p.exit_code : typeof p.success === "boolean" ? p.success ? 0 : 1 : null;\n if (c !== null) a.outcome = c === 0 ? "ok" : "failed";\n if (typeof p.aggregated_output === "string" && failSummaryIn(p.aggregated_output)) a.failSummary = true;\n return;\n }\n if (kind2 === "item_completed") {\n const it = obj2(p.item);\n if (!it) return;\n const type = String(it.type ?? "").toLowerCase();\n const status = String(it.status ?? "").toLowerCase();\n const act = (tool, extra = {}, outcome = status === "failed" ? "failed" : status === "completed" ? "ok" : "unknown") => {\n t.items = true;\n acts.push({ thread, via: "item", tool, at, order: order++, outcome, failSummary: false, ...extra });\n };\n if (type === "usermessage") {\n if (main2) prompt("item", (Array.isArray(it.content) ? it.content : []).map((c) => obj2(c)?.type === "text" && typeof obj2(c).text === "string" ? obj2(c).text : "").join("\\n"), at);\n return;\n }\n if (type === "commandexecution") {\n const line = shellLine(it.command);\n const exit = typeof it.exit_code === "number" ? it.exit_code : null;\n const outcome = exit !== null ? exit === 0 && status !== "failed" ? "ok" : "failed" : status === "failed" ? "failed" : status === "completed" ? "ok" : "unknown";\n act("shell", { shell: shellOf(line), failSummary: typeof it.aggregated_output === "string" && failSummaryIn(it.aggregated_output) }, outcome);\n return;\n }\n if (type === "filechange") {\n const ch = obj2(it.changes);\n act("apply_patch", { files: ch ? Object.keys(ch).map(code) : [] });\n return;\n }\n if (type === "mcptoolcall") {\n act("mcp", {}, status === "failed" || obj2(it.result)?.isError === true ? "failed" : status === "completed" ? "ok" : "unknown");\n return;\n }\n if (type === "dynamictoolcall") {\n act("dynamic", {}, it.success === false || status === "failed" ? "failed" : it.success === true || status === "completed" ? "ok" : "unknown");\n return;\n }\n if (type === "extension") {\n act(it.kind === "web.search" ? "web_search" : it.kind === "image_gen.generation" ? "image_gen" : "extension");\n return;\n }\n if (type === "websearch") {\n act("web_search");\n return;\n }\n if (type === "imageview") {\n act("view_image");\n return;\n }\n if (type === "subagentactivity") {\n if (it.kind === "started") act("spawn_agent", { subagent: true });\n return;\n }\n if (type === "contextcompaction") {\n if (main2) compactions.item++;\n return;\n }\n return;\n }\n return;\n }\n const kind = p.type;\n if (kind === "message" && p.role === "user" && main2) {\n const m = (Array.isArray(p.content) ? p.content : []).map((c) => typeof obj2(c)?.text === "string" ? obj2(c).text : "").join("\\n");\n if (m.trim() && !INJECTED.test(m)) prompt("message", m, at);\n return;\n }\n if (kind === "compaction") {\n if (main2) compactions.response++;\n return;\n }\n if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {\n const tool = kind === "local_shell_call" ? "shell" : codexTool(p.name, p.namespace);\n let input = null;\n const raw2 = kind === "function_call" ? p.arguments : p.input;\n if (typeof raw2 === "string" && raw2.startsWith("{")) {\n try {\n input = obj2(JSON.parse(raw2));\n } catch {\n input = null;\n }\n }\n const a = { thread, via: "call", tool, at, order: order++, outcome: "unknown", failSummary: false };\n if (tool === "shell") {\n const line = kind === "local_shell_call" ? shellLine(obj2(p.action)?.command) : shellLine(input?.command ?? input?.cmd);\n const words = Array.isArray(input?.command) ? input.command : [];\n if (words[0] === "apply_patch" && typeof words[1] === "string") {\n a.tool = "apply_patch";\n a.files = patchFiles(words[1]).map(code);\n } else a.shell = shellOf(line);\n }\n if (tool === "apply_patch") {\n const patch = typeof raw2 === "string" && !raw2.startsWith("{") ? raw2 : str2(input?.input) ?? str2(input?.patch) ?? "";\n a.files = patchFiles(patch).map(code);\n }\n if (tool === "spawn_agent") a.subagent = true;\n acts.push(a);\n const id = str2(p.call_id) ?? str2(p.id);\n if (id) calls.set(id, a);\n return;\n }\n if (kind === "function_call_output" || kind === "custom_tool_call_output" || kind === "local_shell_call_output") {\n const a = calls.get(str2(p.call_id) ?? "");\n if (!a) return;\n const { code: exit, text: out } = exitOf(p.output);\n if (exit !== null) a.outcome = exit === 0 ? "ok" : "failed";\n else if (a.tool === "apply_patch") a.outcome = /^\\s*Success\\./.test(out) ? "ok" : /^\\s*(apply_patch verification failed|Failed|error)/i.test(out) ? "failed" : "unknown";\n else if (a.tool !== "shell") a.outcome = /^\\s*(error|failed)\\b/i.test(out.slice(0, 200)) ? "failed" : "ok";\n if (a.shell?.test && failSummaryIn(out)) a.failSummary = true;\n return;\n }\n },\n done() {\n const chosen = acts.filter((a) => threads[a.thread]?.items ? a.via === "item" : a.via === "call").sort((x, y) => x.order - y.order);\n const toolCalls = {};\n let toolErrors = 0, edits = 0, redone = 0, testsRun = 0, testsFailed = 0, failed = 0, commits = 0, prs = 0, subagents = 0, shells = 0, shellsKnown = 0;\n let firstEditAt = null;\n const edited = /* @__PURE__ */ new Set();\n const failing = /* @__PURE__ */ new Map();\n for (const a of chosen) {\n toolCalls[a.tool] = (toolCalls[a.tool] ?? 0) + 1;\n if (a.outcome === "failed") toolErrors++;\n if (a.subagent) subagents++;\n if (a.files) {\n edits += a.files.length;\n if (firstEditAt === null && a.at !== null && a.files.length) firstEditAt = a.at;\n if (a.outcome === "ok") for (const f of a.files) {\n if (edited.has(f)) redone++;\n else edited.add(f);\n }\n }\n if (a.shell) {\n shells++;\n if (a.outcome !== "unknown") shellsKnown++;\n if (a.shell.test) {\n testsRun++;\n if (a.outcome === "failed" || a.failSummary) testsFailed++;\n }\n if (a.outcome === "failed") {\n failed++;\n failing.set(a.shell.code, (failing.get(a.shell.code) ?? 0) + 1);\n }\n if (a.outcome === "ok" && a.shell.commit) commits++;\n if (a.outcome === "ok" && a.shell.pr) prs++;\n }\n }\n let loops = 0, loopRuns = 0;\n for (const n of failing.values()) if (n >= 3) {\n loops++;\n loopRuns += n;\n }\n const statusKnown = shells === 0 || shellsKnown > 0;\n times.sort((a, b) => a - b);\n let active = 0;\n for (let i = 1; i < times.length; i++) {\n const gap = times[i] - times[i - 1];\n if (gap <= GAP_MS2) active += gap;\n }\n const way = prompts.item.n ? prompts.item : prompts.event.n ? prompts.event : prompts.message;\n let trace = null;\n if (branch && opts.traceFrom) {\n const m = opts.traceFrom.exec(branch);\n const tr = m ? m[1] ?? m[0] : null;\n trace = tr && /^[A-Za-z0-9._:-]{1,80}$/.test(tr) ? tr : null;\n }\n const last = chosen.filter((a) => a.thread === 0).at(-1);\n const errorLast = apiErrorLast && (!last || last.order < apiErrorOrder) ? true : last?.outcome === "failed";\n const ended = prs > 0 ? "pr" : commits > 0 ? "commit" : errorLast ? "error" : "no_commit";\n const ctx = contexts.length ? contexts : null;\n return {\n session: code(sessionId ?? ""),\n started_at: times.length ? new Date(times[0]).toISOString() : null,\n ended_at: times.length ? new Date(times[times.length - 1]).toISOString() : null,\n active_ms: active,\n turns: way.n,\n prompt_chars: way.chars,\n interrupts,\n tool_calls: Object.fromEntries(Object.entries(toolCalls).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),\n tool_errors: toolErrors,\n edits,\n edits_redone: redone,\n files_edited: edited.size,\n tests_run: testsRun,\n tests_failed: statusKnown ? testsFailed : null,\n failed_commands: statusKnown ? failed : null,\n loops: statusKnown ? loops : null,\n loop_runs: statusKnown ? loopRuns : null,\n first_edit_ms: way.first !== null && firstEditAt !== null ? Math.max(0, firstEditAt - way.first) : null,\n compactions: Math.max(compactions.line, compactions.item, compactions.event, compactions.response),\n subagents,\n api_errors: turnEnds ? apiErrors : null,\n commits_made: commits,\n pr_opened: prs,\n branch: branch ? code(branch) : null,\n trace_id: trace,\n models: [...models].filter((m) => m !== "other").sort(),\n tokens: usageSeen ? tokens : null,\n tokens_by_model: usageSeen ? byModel : null,\n ended_with: ended,\n cli_version: version,\n lines,\n lines_unread: unread,\n lines_bad: bad,\n context_first: ctx ? ctx[0] : null,\n context_peak: ctx ? maxOf(ctx) : null,\n context_over_150k: ctx ? ctx.reduce((a, c) => a + Math.max(0, c - LONG_CONTEXT), 0) : null,\n replies_over_150k: ctx ? ctx.filter((c) => c > LONG_CONTEXT).length : null\n };\n }\n };\n}\n\n// packages/core/src/transcriptSlim.ts\nvar obj3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;\nvar TOP = ["type", "timestamp", "sessionId", "gitBranch", "version", "cwd", "isMeta", "isSidechain", "isCompactSummary", "subtype", "requestId", "uuid"];\nvar READ = /* @__PURE__ */ new Set(["user", "assistant", "system"]);\nvar HEAD = 100;\nvar TAIL = 2e4;\nvar clip = (s) => s.length > HEAD + TAIL + 1 ? `${s.slice(0, HEAD)}\n${s.slice(-TAIL)}` : s;\nvar resultText2 = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => typeof obj3(p)?.text === "string" ? obj3(p).text : "").join("") : "";\nfunction slimLine(raw) {\n const text = raw.trim();\n if (!text) return "";\n let e;\n try {\n e = obj3(JSON.parse(text));\n } catch {\n e = null;\n }\n if (!e || typeof e.type !== "string") return "0";\n if (!READ.has(e.type)) return JSON.stringify({ type: e.type });\n const out = {};\n for (const k of TOP) if (e[k] !== void 0 && (typeof e[k] !== "object" || e[k] === null)) out[k] = e[k];\n const msg = obj3(e.message);\n if (msg) {\n const m = {};\n if (msg.model !== void 0) m.model = msg.model;\n if (msg.id !== void 0) m.id = msg.id;\n const u = obj3(msg.usage);\n if (u) {\n const cc = obj3(u.cache_creation);\n m.usage = {\n input_tokens: u.input_tokens,\n output_tokens: u.output_tokens,\n cache_creation_input_tokens: u.cache_creation_input_tokens,\n cache_read_input_tokens: u.cache_read_input_tokens,\n ...cc ? { cache_creation: { ephemeral_5m_input_tokens: cc.ephemeral_5m_input_tokens, ephemeral_1h_input_tokens: cc.ephemeral_1h_input_tokens } } : {},\n ...typeof u.speed === "string" ? { speed: u.speed } : {},\n ...typeof u.inference_geo === "string" ? { inference_geo: u.inference_geo } : {}\n };\n }\n const c = msg.content;\n if (typeof c === "string") m.content = c;\n else if (Array.isArray(c)) m.content = c.map((p) => {\n const part = obj3(p);\n if (!part) return null;\n if (part.type === "tool_use") {\n const input = obj3(part.input);\n const kept = {};\n for (const k of ["file_path", "notebook_path", "command"]) if (typeof input?.[k] === "string") kept[k] = input[k];\n return { type: "tool_use", id: part.id, name: part.name, input: kept };\n }\n if (part.type === "tool_result") return { type: "tool_result", tool_use_id: part.tool_use_id, ...part.is_error === true ? { is_error: true } : {}, content: clip(resultText2(part.content)) };\n if (part.type === "text") return e.type === "user" ? { type: "text", text: typeof part.text === "string" ? part.text : "" } : { type: "text" };\n return { type: part.type };\n });\n out.message = m;\n }\n return JSON.stringify(out);\n}\n\n// packages/core/src/contextShape.ts\nvar obj4 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;\nvar str3 = (v) => typeof v === "string" && v ? v : null;\nvar int3 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;\nvar OVER_DEEP = 5e5;\nvar CURVE_AT = [1, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];\nfunction contextShapeReader(opts = {}) {\n const replies = /* @__PURE__ */ new Map();\n let line = 0, pendingCompaction = false, boundaries = 0, summaries = 0;\n const marks = [];\n return {\n push(raw) {\n line++;\n if (!raw.includes(\'"assistant"\') && !raw.includes("compact_boundary") && !raw.includes("isCompactSummary")) return;\n let e;\n try {\n e = obj4(JSON.parse(raw));\n } catch {\n return;\n }\n if (!e || e.isSidechain === true && !opts.subagent) return;\n if (e.type === "system" && e.subtype === "compact_boundary") {\n boundaries++;\n marks.push(replies.size);\n pendingCompaction = true;\n return;\n }\n if (e.type === "user" && e.isCompactSummary === true) {\n summaries++;\n if (!pendingCompaction) {\n marks.push(replies.size);\n pendingCompaction = true;\n }\n return;\n }\n if (e.type !== "assistant") return;\n const msg = obj4(e.message);\n const model = str3(msg?.model);\n const u = obj4(msg?.usage);\n if (!model || model === "<synthetic>" || !u) return;\n const key = str3(msg?.id) ?? str3(e.requestId) ?? str3(e.uuid) ?? `line${line}`;\n const prev = replies.get(key);\n const r = {\n context: int3(u.input_tokens) + int3(u.cache_read_input_tokens) + int3(u.cache_creation_input_tokens),\n output: int3(u.output_tokens),\n cache_read: int3(u.cache_read_input_tokens),\n opus: /opus/i.test(model),\n at: prev?.at ?? str3(e.timestamp)\n };\n replies.set(key, r);\n pendingCompaction = false;\n },\n done() {\n return shapeOf([...replies.values()], Math.max(boundaries, summaries), marks);\n }\n };\n}\nfunction shapeOf(rs, compactions, marks) {\n const sum = (f) => rs.reduce((a, r) => a + f(r), 0);\n const total = sum((r) => r.context);\n const first = rs[0]?.context ?? 0;\n const at = [...new Set(marks)];\n return {\n month: rs.find((r) => r.at)?.at?.slice(0, 7) ?? null,\n replies: rs.length,\n first_context: first,\n peak_context: rs.reduce((a, r) => Math.max(a, r.context), 0),\n context_total: total,\n output_total: sum((r) => r.output),\n curve: CURVE_AT.filter((n) => n <= rs.length).map((n) => ({ reply: n, context: rs[n - 1].context })),\n replies_over_150k: rs.filter((r) => r.context > LONG_CONTEXT).length,\n replies_over_500k: rs.filter((r) => r.context > OVER_DEEP).length,\n tokens_over: sum((r) => Math.max(0, r.context - LONG_CONTEXT)),\n tokens_over_500k: sum((r) => Math.max(0, r.context - OVER_DEEP)),\n heavy_start: rs.length * Math.max(0, first - HEAVY_START),\n opus_context: sum((r) => r.opus ? r.context : 0),\n opus_context_share: total ? sum((r) => r.opus ? r.context : 0) / total : null,\n cache_read_share: total ? sum((r) => r.cache_read) / total : null,\n compactions,\n compacted: at.map((i) => ({ before: i > 0 ? rs[i - 1].context : null, after: i < rs.length ? rs[i].context : null }))\n };\n}\nfunction contextKept(s) {\n return { context_first: s.first_context, context_peak: s.peak_context, context_over_150k: s.tokens_over, replies_over_150k: s.replies_over_150k };\n}\n\n// packages/core/src/deepAccessKeeps.ts\nvar CODING_KEEPS = [\n { field: "session", line: "A code for the session, not its id" },\n { field: "started_at", line: "When the session started" },\n { field: "ended_at", line: "When the session ended" },\n { field: "active_ms", line: "Active time, pauses over 5 minutes left out" },\n { field: "turns", line: "How many prompts the person typed" },\n { field: "prompt_chars", line: "How long the prompts were, in characters" },\n { field: "interrupts", line: "How often the person stopped a reply" },\n { field: "tool_calls", line: "How many times each tool was used" },\n { field: "tool_errors", line: "How many tool calls failed" },\n { field: "edits", line: "How many file edits were made" },\n { field: "edits_redone", line: "Edits to a file already edited in the session" },\n { field: "files_edited", line: "How many different files were edited" },\n { field: "tests_run", line: "How many times tests were run" },\n { field: "tests_failed", line: "How many test runs failed" },\n { field: "failed_commands", line: "How many commands failed" },\n { field: "loops", line: "Commands that failed the same way three times or more" },\n { field: "loop_runs", line: "How many runs those repeated failures took" },\n { field: "first_edit_ms", line: "Time from the first prompt to the first edit" },\n { field: "compactions", line: "How often the conversation was shortened to fit" },\n { field: "subagents", line: "How many helper agents were started" },\n { field: "api_errors", line: "How many model requests failed" },\n { field: "commits_made", line: "How many commits were made" },\n { field: "pr_opened", line: "How many pull requests were opened" },\n { field: "branch", line: "A code for the branch, not its name" },\n { field: "trace_id", line: "The issue id in the branch name, if you set one" },\n { field: "models", line: "Which models answered" },\n { field: "tokens", line: "Tokens used, by type" },\n { field: "tokens_by_model", line: "Tokens used, by model" },\n { field: "ended_with", line: "How it ended: pull request, commit, error or no commit" },\n { field: "cli_version", line: "The coding tool\'s version" },\n { field: "lines", line: "How many lines the transcript had" },\n { field: "lines_unread", line: "Lines skipped because they hold no session activity" },\n { field: "lines_bad", line: "Lines that could not be read" },\n // How the session\'s context grew (contextShape.ts), read from the same transcript\'s usage counters.\n { field: "context_first", line: "Tokens the first reply was sent" },\n { field: "context_peak", line: "Tokens the largest reply was sent" },\n { field: "context_over_150k", line: "Tokens sent beyond 150,000 a reply" },\n { field: "replies_over_150k", line: "Replies sent more than 150,000 tokens" }\n];\nvar CODING_SENDS = [\n { field: "person", line: "A code for the person: SHA-256 of their email, no salt. Kept" },\n { field: "scope", line: "A code for the repository: SHA-256 of its name, no salt. Checked against your choice, not kept" }\n];\nvar CODING_NEVER = ["Prompts and replies", "Code and file contents", "Commands and their output", "File paths and branch names"];\nvar DIFF_KEEPS = [\n { field: "line_hashes_read", line: "Whether the changed lines were read" },\n { field: "line_hashes_added", line: "A code for each line added, by file code" },\n { field: "line_hashes_removed", line: "A code for each line removed, by file code" },\n { field: "line_hashes_renamed", line: "A renamed file\'s code before the rename" },\n { field: "line_hashes_capped", line: "More than 2,000 lines changed; the first 2,000 kept" },\n { field: "line_hashes_unread_files", line: "Files GitHub sent no changed lines for" }\n];\nvar DIFF_NEVER = ["Code and file contents", "File paths", "Titles, descriptions and comments", "Commit messages"];\nvar KEEPS = {\n "claude-code-transcripts": { label: "Claude Code sessions", scope: "repositories", ready: true, reads: "Each session\'s transcript, read once, then dropped", keeps: CODING_KEEPS, never: CODING_NEVER, sends: CODING_SENDS },\n // The same signals, read from Codex\'s own transcript (codexSignals.ts, 2026-10-21). A count Codex\'s log cannot say is\n // kept as unknown, never as 0.\n "codex-transcripts": { label: "Codex sessions", scope: "repositories", ready: true, reads: "Each session\'s transcript, read once, then dropped", keeps: CODING_KEEPS, never: CODING_NEVER, sends: CODING_SENDS },\n "github-diffs": { label: "GitHub pull request changes", scope: "repositories", ready: true, reads: "Each merged pull request\'s changed lines, read once, then dropped", keeps: DIFF_KEEPS, never: DIFF_NEVER, sends: [] }\n};\nvar DEEP_ACCESS_SOURCES = Object.keys(KEEPS);\nfunction keptOf(grant, item) {\n const named = new Set(KEEPS[grant.source].keeps.map((k) => k.field));\n const out = {};\n for (const f of grant.keeps) if (named.has(f) && f in item && item[f] !== void 0) out[f] = item[f];\n return out;\n}\n\n// packages/connectors/src/sessionScope.ts\nvar sessionSalt = (ingestKey) => sha256(`valistry-session\\0${ingestKey}`);\nvar saltedScopeCode = (salt, item) => `r_${shortCode(salt, item.trim().toLowerCase())}`;\n\n// packages/cli/src/envProxy.ts\nimport { request as httpRequest } from "node:http";\nimport { request as httpsRequest } from "node:https";\nimport { connect as tlsConnect } from "node:tls";\nfunction proxyFor(target, env) {\n const raw = (env.HTTPS_PROXY || env.https_proxy || "").trim();\n const u = new URL(target);\n if (!raw || u.protocol !== "https:") return null;\n const skip = (env.NO_PROXY || env.no_proxy || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);\n if (skip.some((s) => s === "*" || u.hostname === s.replace(/^\\./, "") || u.hostname.endsWith(s.startsWith(".") ? s : `.${s}`))) return null;\n try {\n return new URL(raw.includes("://") ? raw : `http://${raw}`);\n } catch {\n return null;\n }\n}\nvar through = (proxy, u, init = {}) => new Promise((resolve, reject) => {\n const at = `${u.hostname}:${u.port || 443}`;\n const signal = init.signal ?? void 0;\n const auth = proxy.username ? { "proxy-authorization": `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString("base64")}` } : {};\n const tunnel = (proxy.protocol === "https:" ? httpsRequest : httpRequest)({ host: proxy.hostname, port: proxy.port || (proxy.protocol === "https:" ? 443 : 80), method: "CONNECT", path: at, headers: { host: at, ...auth }, signal });\n tunnel.on("error", reject);\n tunnel.on("connect", (res, socket) => {\n if (res.statusCode !== 200) {\n socket.destroy();\n reject(new Error(`the proxy answered ${res.statusCode}`));\n return;\n }\n const req = httpsRequest(u, { method: init.method ?? "GET", headers: init.headers, agent: false, signal, createConnection: () => tlsConnect({ socket, servername: u.hostname }) }, (r) => {\n const parts = [];\n r.on("data", (d) => parts.push(d));\n r.on("error", reject);\n r.on("end", () => {\n const status = r.statusCode ?? 502;\n resolve(new Response([204, 205, 304].includes(status) ? null : Buffer.concat(parts), { status }));\n });\n });\n req.on("error", reject);\n if (typeof init.body === "string") req.write(init.body);\n req.end();\n });\n tunnel.end();\n});\nvar envProxyFetch = (env = process.env, base = fetch) => ((input, init) => {\n const url = String(input);\n const proxy = proxyFor(url, env);\n return proxy ? through(proxy, new URL(url), init) : base(input, init);\n});\n\n// scripts/hooks/valistry-session.ts\nvar SOURCE = { "claude-code": "claude-code-transcripts", codex: "codex-transcripts" };\nvar TIMEOUT_MS = { "claude-code": 1e4, codex: 1200 };\nvar say = (line) => process.stderr.write(`Valistry: ${line}\n`);\nasync function stdin() {\n let text = "";\n for await (const chunk of process.stdin) {\n text += chunk;\n if (text.length > 1e6) break;\n }\n return text;\n}\nfunction codexSettings(env) {\n const file = join(env.CODEX_HOME || join(homedir(), ".codex"), "config.toml");\n let text = "";\n try {\n if (statSync(file).size < 1e6) text = readFileSync(file, "utf8");\n } catch {\n }\n return {\n key: /Bearer\\s+(vly_ot_[A-Za-z0-9_]+)/.exec(text)?.[1] ?? null,\n endpoint: /endpoint\\s*=\\s*"(https?:\\/\\/[^"\\s]+?)\\/v1\\/(?:logs|metrics|traces)"/.exec(text)?.[1] ?? null,\n person: /"x-valistry-person"\\s*=\\s*"([^"\\s]{1,254})"/.exec(text)?.[1] ?? null\n };\n}\nfunction keyFrom(env, codex) {\n if (env.VALISTRY_INGEST_KEY?.trim()) return env.VALISTRY_INGEST_KEY.trim();\n for (const pair of (env.OTEL_EXPORTER_OTLP_HEADERS ?? "").split(",")) {\n const cut = pair.indexOf("=");\n if (cut < 0) continue;\n if (pair.slice(0, cut).trim().toLowerCase() !== "authorization") continue;\n let v = pair.slice(cut + 1).trim();\n try {\n v = decodeURIComponent(v);\n } catch {\n }\n const token = v.replace(/^Bearer\\s+/i, "").trim();\n if (token) return token;\n }\n return codex?.key ?? null;\n}\nfunction endpointFrom(env, tool, codex) {\n const base = (env.VALISTRY_ENDPOINT || env.OTEL_EXPORTER_OTLP_ENDPOINT || codex?.endpoint || "https://app.valistry.ai/api/otlp").trim().replace(/\\/+$/, "");\n return /^https:\\/\\//.test(base) || /^http:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d+)?\\//.test(`${base}/`) ? `${base}/hooks/${tool}/session` : null;\n}\nfunction repoOf(remote) {\n const m = /[:/]([^/:]+\\/[^/]+?)(\\.git)?\\/?$/.exec(remote.trim());\n return m ? m[1] : null;\n}\nvar git = (cwd, args) => {\n try {\n return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).trim();\n } catch {\n return "";\n }\n};\nfunction personOf(env, cwd, tool, codex) {\n let who = env.VALISTRY_PERSON?.trim() || "";\n if (!who) for (const pair of (env.OTEL_RESOURCE_ATTRIBUTES ?? "").split(",")) {\n const [k, v] = pair.split("=");\n if ((k?.trim() === "user.email" || k?.trim() === "valistry.person") && v?.trim()) {\n who = v.trim();\n break;\n }\n }\n if (!who && tool === "codex" && codex?.person) who = codex.person;\n if (!who && tool === "claude-code") {\n const file = join(env.CLAUDE_CONFIG_DIR || homedir(), ".claude.json");\n try {\n if (statSync(file).size < 5e7) {\n const c = JSON.parse(readFileSync(file, "utf8"));\n if (typeof c.oauthAccount?.emailAddress === "string") who = c.oauthAccount.emailAddress;\n }\n } catch {\n }\n }\n if (!who && cwd) who = git(cwd, ["config", "--get", "user.email"]);\n return who ? sha256(who.trim().toLowerCase()).slice(0, 16) : void 0;\n}\nasync function eachLine(path, f) {\n const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }), crlfDelay: Infinity });\n for await (const raw of rl) f(raw);\n}\nasync function firstLine(path) {\n const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }), crlfDelay: Infinity });\n for await (const raw of rl) {\n rl.close();\n return raw;\n }\n return "";\n}\nasync function codexThread(path) {\n try {\n const e = JSON.parse(await firstLine(path));\n if (e.type !== "session_meta" || typeof e.payload?.id !== "string") return null;\n return { id: e.payload.id, session: typeof e.payload.session_id === "string" ? e.payload.session_id : e.payload.id };\n } catch {\n return null;\n }\n}\nasync function claudeCodeItem(path, salt, traceFrom) {\n const slim = [];\n const shape = contextShapeReader();\n await eachLine(path, (raw) => {\n const s = slimLine(raw);\n if (s) slim.push(s);\n const t = raw.trim();\n if (t) shape.push(t);\n });\n const sub = join(dirname(path), basename(path, ".jsonl"), "subagents");\n if (existsSync(sub)) for (const f of readdirSync(sub).filter((x) => x.endsWith(".jsonl")).sort()) await eachLine(join(sub, f), (raw) => {\n const s = slimLine(raw);\n if (s) slim.push(s);\n });\n const signals = sessionSignals(slim.join("\\n"), { salt, traceFrom });\n slim.length = 0;\n return { ...signals, ...contextKept(shape.done()) };\n}\nasync function codexItem(path, salt, traceFrom) {\n const reader = codexSignalsReader({ salt, traceFrom });\n await eachLine(path, (raw) => reader.push(raw));\n const main2 = await codexThread(path);\n if (main2) {\n const dir = dirname(path);\n for (const f of readdirSync(dir).filter((x) => x.endsWith(".jsonl") && join(dir, x) !== path).sort()) {\n const t = await codexThread(join(dir, f));\n if (t && t.session === main2.session && t.id !== main2.id) await eachLine(join(dir, f), (raw) => reader.push(raw));\n }\n }\n return reader.done();\n}\nasync function main() {\n let input;\n try {\n input = JSON.parse(await stdin());\n } catch {\n say("the hook\'s input was not JSON; nothing sent.");\n return;\n }\n const sessionId = typeof input.session_id === "string" ? input.session_id.trim() : "";\n const path = typeof input.transcript_path === "string" ? input.transcript_path : "";\n const cwd = typeof input.cwd === "string" && existsSync(input.cwd) ? input.cwd : null;\n if (!sessionId || !path.endsWith(".jsonl") || !existsSync(path)) {\n say("no transcript for this session; nothing sent.");\n return;\n }\n const tool = process.argv.includes("--codex") || isCodexTranscript(await firstLine(path)) ? "codex" : "claude-code";\n const codex = tool === "codex" ? codexSettings(process.env) : null;\n const key = keyFrom(process.env, codex);\n const url = endpointFrom(process.env, tool, codex);\n if (!key) {\n say(`no ingest key. Set VALISTRY_INGEST_KEY, or ${tool === "codex" ? "Codex\'s [otel] settings" : "Claude Code\'s telemetry settings"}; nothing sent.`);\n return;\n }\n if (!url) {\n say("the endpoint must start with https://; nothing sent.");\n return;\n }\n const auth = { authorization: `Bearer ${key}` };\n const salt = sessionSalt(key);\n const repo = cwd ? repoOf(git(cwd, ["remote", "get-url", "origin"])) : null;\n const scope = repo ? [saltedScopeCode(salt, repo), saltedScopeCode(salt, repo.split("/").pop())] : [];\n const send = envProxyFetch(process.env);\n const asked = await send(`${url}${scope.length ? `?${scope.map((s) => `scope=${s}`).join("&")}` : ""}`, { headers: auth, signal: AbortSignal.timeout(TIMEOUT_MS[tool]) });\n if (!asked.ok) {\n say(`${await reason(asked)} Nothing sent.`);\n return;\n }\n const grant = { source: SOURCE[tool], keeps: (await asked.json()).keeps };\n if (!Array.isArray(grant.keeps) || !grant.keeps.every((k) => typeof k === "string")) {\n say("the grant could not be read; nothing sent.");\n return;\n }\n let traceFrom;\n try {\n traceFrom = process.env.VALISTRY_TRACE_PATTERN ? new RegExp(process.env.VALISTRY_TRACE_PATTERN) : void 0;\n } catch {\n traceFrom = void 0;\n }\n const signals = tool === "codex" ? await codexItem(path, salt, traceFrom) : await claudeCodeItem(path, salt, traceFrom);\n const item = { ...signals, session: `s_${sha256(sessionId).slice(0, 16)}` };\n const kept = keptOf(grant, item);\n const person = personOf(process.env, cwd, tool, codex);\n const sent = await send(url, { method: "POST", headers: { ...auth, "content-type": "application/json" }, body: JSON.stringify({ kept, ...person ? { person } : {}, ...scope.length ? { scope } : {} }), signal: AbortSignal.timeout(TIMEOUT_MS[tool]) });\n if (!sent.ok) say(`${await reason(sent)} Nothing kept.`);\n}\nasync function reason(r) {\n try {\n const j = await r.json();\n if (typeof j.error === "string") return j.error.slice(0, 200);\n } catch {\n }\n return `the server answered ${r.status}.`;\n}\nmain().catch((e) => say(`${e instanceof Error ? e.message.slice(0, 200) : "an error"}; nothing sent.`)).finally(() => process.exit(0));\n' : "// the session script\n";
425
435
  function setupPlan(o) {
426
436
  const p = pathsOf(o.home);
427
437
  const changes = [];
@@ -443,7 +453,7 @@ function setupPlan(o) {
443
453
  else {
444
454
  const before = read(p.claude);
445
455
  try {
446
- const m = mergeClaude(before, { endpoint, key, team, force: o.force });
456
+ const m = mergeClaude(before, { endpoint, key, team, force: o.force, deep: o.deep });
447
457
  if (m.conflict) errors.push(m.conflict);
448
458
  else if (m.after === before) notes.push("Claude Code: already set up. No change.");
449
459
  else changes.push({ kind: "write", path: p.claude, before, after: m.after, theirs: true, title: "Claude Code settings", lines: [
@@ -462,7 +472,7 @@ function setupPlan(o) {
462
472
  else if (!email) notes.push("Codex: skipped. It needs your work email: add --email you@company.com.");
463
473
  else {
464
474
  const before = read(p.codex);
465
- const a = addCodex(before, { endpoint, key, person: email, force: o.force });
475
+ const a = addCodex(before, { endpoint, key, person: email, force: o.force, deep: o.deep });
466
476
  if (a.conflict) errors.push(a.conflict);
467
477
  else if (a.after === before) notes.push("Codex: already set up. No change.");
468
478
  else changes.push({ kind: "write", path: p.codex, before, after: a.after, theirs: true, title: "Codex settings", lines: [
@@ -471,11 +481,22 @@ function setupPlan(o) {
471
481
  ] });
472
482
  }
473
483
  }
484
+ if (o.deep) changes.push({ kind: "write", path: p.session, before: read(p.session), after: SESSION_SCRIPT, title: "The session script", lines: [
485
+ "When a session ends, it reads the transcript on this computer and sends counts only.",
486
+ "It sends nothing until deep access is on in Valistry."
487
+ ] });
474
488
  if (o.desktop) {
475
489
  if (o.platform !== "darwin" && o.platform !== "win32") errors.push("The desktop observer runs on macOS and Windows only.");
476
490
  else if (!email) errors.push("The desktop observer needs your work email, to count you as one person: add --email you@company.com.");
477
491
  else {
478
- const config = { endpoint, key, person: personCode(email), ...team ? { team } : {}, version: o.version };
492
+ const had = o.emailFromGit ? (() => {
493
+ try {
494
+ return JSON.parse(read(p.config) ?? "{}").person;
495
+ } catch {
496
+ return void 0;
497
+ }
498
+ })() : void 0;
499
+ const config = { endpoint, key, person: typeof had === "string" && had ? had : personCode(email), ...team ? { team } : {}, version: o.version };
479
500
  const shown = `Each minute it checks which app is in front. It counts the Claude, ChatGPT and Microsoft Copilot apps.`;
480
501
  changes.push({ kind: "write", path: p.script, before: read(p.script), after: read(o.self) ?? "", title: "The observer", lines: ["A copy of this command, so it runs after npx is gone."] });
481
502
  changes.push({ kind: "write", path: p.config, before: read(p.config), after: `${JSON.stringify(config, null, 2)}
@@ -586,6 +607,55 @@ function status(home, now, running) {
586
607
  return { lines: lines2 };
587
608
  }
588
609
 
610
+ // packages/cli/src/envProxy.ts
611
+ import { request as httpRequest } from "node:http";
612
+ import { request as httpsRequest } from "node:https";
613
+ import { connect as tlsConnect } from "node:tls";
614
+ function proxyFor(target, env) {
615
+ const raw = (env.HTTPS_PROXY || env.https_proxy || "").trim();
616
+ const u = new URL(target);
617
+ if (!raw || u.protocol !== "https:") return null;
618
+ const skip = (env.NO_PROXY || env.no_proxy || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
619
+ if (skip.some((s) => s === "*" || u.hostname === s.replace(/^\./, "") || u.hostname.endsWith(s.startsWith(".") ? s : `.${s}`))) return null;
620
+ try {
621
+ return new URL(raw.includes("://") ? raw : `http://${raw}`);
622
+ } catch {
623
+ return null;
624
+ }
625
+ }
626
+ var through = (proxy, u, init = {}) => new Promise((resolve, reject) => {
627
+ const at = `${u.hostname}:${u.port || 443}`;
628
+ const signal = init.signal ?? void 0;
629
+ const auth = proxy.username ? { "proxy-authorization": `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString("base64")}` } : {};
630
+ const tunnel = (proxy.protocol === "https:" ? httpsRequest : httpRequest)({ host: proxy.hostname, port: proxy.port || (proxy.protocol === "https:" ? 443 : 80), method: "CONNECT", path: at, headers: { host: at, ...auth }, signal });
631
+ tunnel.on("error", reject);
632
+ tunnel.on("connect", (res, socket) => {
633
+ if (res.statusCode !== 200) {
634
+ socket.destroy();
635
+ reject(new Error(`the proxy answered ${res.statusCode}`));
636
+ return;
637
+ }
638
+ const req = httpsRequest(u, { method: init.method ?? "GET", headers: init.headers, agent: false, signal, createConnection: () => tlsConnect({ socket, servername: u.hostname }) }, (r) => {
639
+ const parts = [];
640
+ r.on("data", (d) => parts.push(d));
641
+ r.on("error", reject);
642
+ r.on("end", () => {
643
+ const status2 = r.statusCode ?? 502;
644
+ resolve(new Response([204, 205, 304].includes(status2) ? null : Buffer.concat(parts), { status: status2 }));
645
+ });
646
+ });
647
+ req.on("error", reject);
648
+ if (typeof init.body === "string") req.write(init.body);
649
+ req.end();
650
+ });
651
+ tunnel.end();
652
+ });
653
+ var envProxyFetch = (env = process.env, base = fetch) => ((input, init) => {
654
+ const url = String(input);
655
+ const proxy = proxyFor(url, env);
656
+ return proxy ? through(proxy, new URL(url), init) : base(input, init);
657
+ });
658
+
589
659
  // packages/cli/src/observe.ts
590
660
  async function observeOnce(o) {
591
661
  const p = pathsOf(o.home);
@@ -594,7 +664,7 @@ async function observeOnce(o) {
594
664
  const s0 = readJson(p.state) ?? emptyState();
595
665
  const sample = o.sample ?? (o.platform === "win32" ? () => sampleWindows() : () => sampleMac());
596
666
  let s = record(s0, activeApp(sample(), o.platform), o.now);
597
- if (o.force || sendDue(s, o.now)) s = await sendNow(s, config, o.now, o.send ?? fetch);
667
+ if (o.force || sendDue(s, o.now)) s = await sendNow(s, config, o.now, o.send ?? envProxyFetch());
598
668
  writeAtomic(p.state, `${JSON.stringify(s)}
599
669
  `);
600
670
  return s;
@@ -622,13 +692,14 @@ async function sendNow(s, config, now, send) {
622
692
  }
623
693
 
624
694
  // packages/cli/src/main.ts
625
- var VERSION = true ? "0.1.0" : "0.0.0";
695
+ var VERSION = true ? "0.1.1" : "0.0.0";
626
696
  var HELP = `valistry ${VERSION}: sets up this computer's AI tools to send their use to Valistry. Counts only.
627
697
 
628
- npx valistry setup [--key <key>] [--email you@company.com] [--team <name>] [--desktop] [--apply]
698
+ npx valistry setup [--key <key>] [--email you@company.com] [--team <name>] [--desktop] [--deep] [--apply]
629
699
  Claude Code: adds the telemetry settings to ~/.claude/settings.json (a copy is saved first).
630
700
  Codex: adds an [otel] table to ~/.codex/config.toml (a copy is saved first).
631
701
  --desktop: counts the minutes the Claude, ChatGPT and Microsoft Copilot apps are in front (macOS, Windows).
702
+ --deep: counts each session's work when it ends (tests, edits, how it ended). It sends nothing until deep access is on in Valistry.
632
703
  Shows every change first. Add --apply to make them.
633
704
  If Claude Code already sends to Valistry, you can leave out --key: its key and address are used.
634
705
  Other options: --endpoint <url> (default ${DEFAULT_ENDPOINT}), --no-claude, --no-codex, --force.
@@ -689,7 +760,9 @@ async function main(argv, io = { log: console.log, err: console.error }) {
689
760
  endpoint,
690
761
  team: str(flags.team),
691
762
  email: str(flags.email) ?? gitEmail(),
763
+ emailFromGit: !str(flags.email),
692
764
  desktop: flags.desktop === true,
765
+ deep: flags.deep === true,
693
766
  claude: flags["no-claude"] !== true,
694
767
  codex: flags["no-codex"] !== true,
695
768
  force: flags.force === true,
@@ -783,7 +856,7 @@ async function main(argv, io = { log: console.log, err: console.error }) {
783
856
  io.err("The desktop observer is not set up.");
784
857
  return 1;
785
858
  }
786
- const s = await sendNow(readJson(p.state) ?? emptyState(), config, now, fetch);
859
+ const s = await sendNow(readJson(p.state) ?? emptyState(), config, now, envProxyFetch());
787
860
  writeAtomic(p.state, `${JSON.stringify(s)}
788
861
  `);
789
862
  io.log(s.lastSend ? s.lastSend.ok ? "Sent." : `Not sent${s.lastSend.status ? ` (${s.lastSend.status})` : ""}: ${s.lastSend.message ?? ""}` : "Nothing new to send.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valistry",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Sets up Claude Code, Codex and the Claude, ChatGPT and Copilot desktop apps to send their use to Valistry. Counts only: never prompts, replies, window titles, screen contents or keystrokes.",
5
5
  "type": "module",
6
6
  "bin": { "valistry": "bin/valistry.mjs" },