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 +2 -0
- package/bin/valistry.mjs +91 -18
- package/package.json +1 -1
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.
|
|
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/
|
|
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
|
|
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
|
|
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 ??
|
|
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.
|
|
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,
|
|
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.
|
|
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" },
|