valistry 0.0.0-stage → 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 +59 -2
- package/bin/valistry.mjs +912 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# valistry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sets up the AI tools on your computer to send their use to [Valistry](https://valistry.ai). Counts only.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx valistry setup --key <your key> --email you@company.com --desktop
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Your key is on Connect → OpenTelemetry in Valistry. It needs Node 18 or later.
|
|
10
|
+
|
|
11
|
+
If Claude Code already sends to Valistry, you can leave out `--key`. Setup then uses the key and address already in `~/.claude/settings.json`, or else in Codex's `[otel]` table, and says which, with the key's last four characters only. A regional address, such as `/in/api/otlp`, is kept. If neither file has a key, it says so and changes nothing.
|
|
12
|
+
|
|
13
|
+
**It shows every change first and makes none.** Run the same command with `--apply` to make them.
|
|
14
|
+
|
|
15
|
+
## What it changes
|
|
16
|
+
|
|
17
|
+
| Tool | File | Change |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Claude Code | `~/.claude/settings.json` | Adds the telemetry settings to `"env"` and two hooks (a limit stopped a turn, a session ended). Keeps every other setting. A copy is saved first. |
|
|
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
|
+
| 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
|
+
|
|
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
|
+
|
|
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`.
|
|
26
|
+
|
|
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.
|
|
28
|
+
|
|
29
|
+
## The desktop observer
|
|
30
|
+
|
|
31
|
+
Each minute it asks the computer which app is in front. If it is the Claude desktop app, the ChatGPT desktop app or the Microsoft Copilot app, and the keyboard or mouse was used in the last five minutes, that app gets one minute.
|
|
32
|
+
|
|
33
|
+
It reads:
|
|
34
|
+
|
|
35
|
+
- the front app's bundle id and name (macOS `lsappinfo`), or its process name (Windows);
|
|
36
|
+
- the seconds since the last key or mouse input, as one number (macOS `ioreg`, Windows `GetLastInputInfo`).
|
|
37
|
+
|
|
38
|
+
It sends, every 15 minutes, to `<endpoint>/v1/metrics` with your key:
|
|
39
|
+
|
|
40
|
+
- per day and app, the active minutes (`valistry.desktop.active_minutes`);
|
|
41
|
+
- you, as a code: a hash of your email, made on your computer;
|
|
42
|
+
- your team, if you named one.
|
|
43
|
+
|
|
44
|
+
**Never read or sent:** window titles, what is on the screen, which keys you type, prompts, replies, files, and your email itself.
|
|
45
|
+
|
|
46
|
+
Windows support is written but not yet run on Windows.
|
|
47
|
+
|
|
48
|
+
## Status and undo
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx valistry status # what is set up, and the last send
|
|
52
|
+
npx valistry remove # shows what it would undo
|
|
53
|
+
npx valistry remove --apply # stops the observer, deletes its files, puts your settings files back
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A settings file you have not changed since setup gets its copy back as it was. A file you changed since loses only what setup added.
|
|
57
|
+
|
|
58
|
+
## Read it before you run it
|
|
59
|
+
|
|
60
|
+
`bin/valistry.mjs` is one file, not minified, with no dependencies. It is built from `packages/cli/src` in the Valistry repository.
|
package/bin/valistry.mjs
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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
|
+
// Valistry repository by packages/cli/build.mjs: do not edit. Counts only: never prompts, replies, code, window titles,
|
|
4
|
+
// screen contents or keys typed.
|
|
5
|
+
|
|
6
|
+
// packages/cli/src/main.ts
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10
|
+
import { existsSync as existsSync2, rmSync as rmSync2 } from "node:fs";
|
|
11
|
+
|
|
12
|
+
// packages/cli/src/setup.ts
|
|
13
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, copyFileSync, chmodSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join as join2 } from "node:path";
|
|
16
|
+
|
|
17
|
+
// packages/cli/src/vendor.ts
|
|
18
|
+
var DESKTOP_APPS = ["claude", "chatgpt", "copilot"];
|
|
19
|
+
var DESKTOP_METRIC = "valistry.desktop.active_minutes";
|
|
20
|
+
var CLAUDE_CODE_ENV = (endpoint, key) => [
|
|
21
|
+
["CLAUDE_CODE_ENABLE_TELEMETRY", "1"],
|
|
22
|
+
["OTEL_METRICS_EXPORTER", "otlp"],
|
|
23
|
+
["OTEL_LOGS_EXPORTER", "otlp"],
|
|
24
|
+
["OTEL_EXPORTER_OTLP_PROTOCOL", "http/json"],
|
|
25
|
+
["OTEL_EXPORTER_OTLP_ENDPOINT", endpoint],
|
|
26
|
+
["OTEL_EXPORTER_OTLP_HEADERS", `Authorization=Bearer ${key}`],
|
|
27
|
+
["OTEL_METRICS_INCLUDE_SESSION_ID", "true"],
|
|
28
|
+
["OTEL_METRIC_EXPORT_INTERVAL", "300000"]
|
|
29
|
+
];
|
|
30
|
+
var teamAttribute = (team) => `team.id=${team.trim().replace(/[,=\s]+/g, "-").slice(0, 64)}`;
|
|
31
|
+
var CLAUDE_CODE_HOOKS = (endpoint, key) => ({
|
|
32
|
+
StopFailure: [{ hooks: [{ type: "http", url: `${endpoint}/hooks/claude-code`, headers: { Authorization: `Bearer ${key}` } }] }],
|
|
33
|
+
SessionEnd: [{ hooks: [{ type: "http", url: `${endpoint}/hooks/claude-code`, headers: { Authorization: `Bearer ${key}` }, timeout: 10 }] }]
|
|
34
|
+
});
|
|
35
|
+
var CODEX_OTEL = (endpoint, key, person) => [
|
|
36
|
+
"[otel]",
|
|
37
|
+
"log_user_prompt = false",
|
|
38
|
+
`exporter = { otlp-http = { endpoint = "${endpoint}/v1/logs", protocol = "json", headers = { "Authorization" = "Bearer ${key}", "x-valistry-person" = "${person}" } } }`,
|
|
39
|
+
`metrics_exporter = { otlp-http = { endpoint = "${endpoint}/v1/metrics", protocol = "json", headers = { "Authorization" = "Bearer ${key}", "x-valistry-person" = "${person}" } } }`
|
|
40
|
+
].join("\n");
|
|
41
|
+
|
|
42
|
+
// packages/cli/src/claude.ts
|
|
43
|
+
var ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
|
44
|
+
var ATTRS = "OTEL_RESOURCE_ATTRIBUTES";
|
|
45
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
46
|
+
var OUR_ENV = [...CLAUDE_CODE_ENV("", "").map(([k]) => k), ATTRS];
|
|
47
|
+
function parseSettings(text) {
|
|
48
|
+
if (text === null || !text.trim()) return {};
|
|
49
|
+
let v;
|
|
50
|
+
try {
|
|
51
|
+
v = JSON.parse(text);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error("~/.claude/settings.json is not valid JSON. Fix it, then run this again. Nothing was changed.");
|
|
54
|
+
}
|
|
55
|
+
if (!isObj(v)) throw new Error("~/.claude/settings.json is not a JSON object. Fix it, then run this again. Nothing was changed.");
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
var printSettings = (o) => `${JSON.stringify(o, null, 2)}
|
|
59
|
+
`;
|
|
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);
|
|
63
|
+
function withoutOurHooks(hooks) {
|
|
64
|
+
if (!isObj(hooks)) return hooks === void 0 ? void 0 : hooks;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
67
|
+
if (!Array.isArray(groups)) {
|
|
68
|
+
out[event] = groups;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const kept = groups.map((g) => isObj(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !ours(h)) } : g).filter((g) => !(isObj(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
72
|
+
if (kept.length) out[event] = kept;
|
|
73
|
+
}
|
|
74
|
+
return Object.keys(out).length ? out : void 0;
|
|
75
|
+
}
|
|
76
|
+
function withTeam(current, team) {
|
|
77
|
+
const rest = typeof current === "string" ? current.split(",").map((s) => s.trim()).filter((s) => s && !/^team\.id=/.test(s)) : [];
|
|
78
|
+
return [...rest, teamAttribute(team)].join(",");
|
|
79
|
+
}
|
|
80
|
+
function mergeClaude(text, o) {
|
|
81
|
+
const s = parseSettings(text);
|
|
82
|
+
const env = isObj(s.env) ? { ...s.env } : {};
|
|
83
|
+
const was = env[ENDPOINT];
|
|
84
|
+
if (typeof was === "string" && was && was !== o.endpoint && !o.force)
|
|
85
|
+
return { after: text ?? "", added: [], changed: [], hooks: [], conflict: `Claude Code already sends telemetry to ${was}. Add --force to send it to Valistry instead.` };
|
|
86
|
+
const added = [], changed = [];
|
|
87
|
+
const want = [...CLAUDE_CODE_ENV(o.endpoint, o.key).map(([k, v]) => [k, v]), ...o.team ? [[ATTRS, withTeam(env[ATTRS], o.team)]] : []];
|
|
88
|
+
for (const [k, v] of want) {
|
|
89
|
+
if (!(k in env)) added.push(k);
|
|
90
|
+
else if (env[k] !== v) changed.push(k);
|
|
91
|
+
env[k] = v;
|
|
92
|
+
}
|
|
93
|
+
const hooks = withoutOurHooks(s.hooks) ?? {};
|
|
94
|
+
const mine = CLAUDE_CODE_HOOKS(o.endpoint, o.key);
|
|
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);
|
|
97
|
+
const after = printSettings({ ...s, env, hooks });
|
|
98
|
+
return { after, added, changed, hooks: Object.keys(mine) };
|
|
99
|
+
}
|
|
100
|
+
function unmergeClaude(text, backup) {
|
|
101
|
+
const s = parseSettings(text);
|
|
102
|
+
const b = (() => {
|
|
103
|
+
try {
|
|
104
|
+
return parseSettings(backup);
|
|
105
|
+
} catch {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
const before = isObj(b.env) ? b.env : {};
|
|
110
|
+
const env = isObj(s.env) ? { ...s.env } : {};
|
|
111
|
+
for (const k of OUR_ENV) {
|
|
112
|
+
if (k in before) env[k] = before[k];
|
|
113
|
+
else delete env[k];
|
|
114
|
+
}
|
|
115
|
+
const out = { ...s };
|
|
116
|
+
if (Object.keys(env).length) out.env = env;
|
|
117
|
+
else delete out.env;
|
|
118
|
+
const hadOurs = JSON.stringify(withoutOurHooks(b.hooks) ?? null) !== JSON.stringify(isObj(b.hooks) ? b.hooks : null);
|
|
119
|
+
const hooks = hadOurs ? s.hooks : withoutOurHooks(s.hooks);
|
|
120
|
+
if (hooks) out.hooks = hooks;
|
|
121
|
+
else delete out.hooks;
|
|
122
|
+
return printSettings(out);
|
|
123
|
+
}
|
|
124
|
+
function claudeConfigured(text, endpoint) {
|
|
125
|
+
try {
|
|
126
|
+
const env = parseSettings(text).env;
|
|
127
|
+
return isObj(env) && env.CLAUDE_CODE_ENABLE_TELEMETRY === "1" && typeof env[ENDPOINT] === "string" && (!endpoint || env[ENDPOINT] === endpoint);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// packages/cli/src/codex.ts
|
|
134
|
+
var BEGIN = "# Valistry telemetry, added by npx valistry. Remove it with: npx valistry remove --apply";
|
|
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`;
|
|
142
|
+
var lines = (t) => (t ?? "").split(/\r?\n/);
|
|
143
|
+
function withoutOurs(text) {
|
|
144
|
+
const out = [];
|
|
145
|
+
let inside = false;
|
|
146
|
+
for (const l of lines(text)) {
|
|
147
|
+
if (l === BEGIN) {
|
|
148
|
+
inside = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (inside) {
|
|
152
|
+
if (l === END) inside = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
out.push(l);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
var header = (l) => /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(l)?.[1];
|
|
160
|
+
var isOtel = (name) => name !== void 0 && (name === "otel" || name.startsWith("otel."));
|
|
161
|
+
var otelLines = (ls) => ls.reduce((a, l) => {
|
|
162
|
+
const h = header(l);
|
|
163
|
+
if (h !== void 0) a.on = isOtel(h);
|
|
164
|
+
if (a.on) a.out.push(l);
|
|
165
|
+
return a;
|
|
166
|
+
}, { on: false, out: [] }).out;
|
|
167
|
+
var isValistrys = (ls) => ls.some((l) => /"Bearer vly_ot_/.test(l));
|
|
168
|
+
var hasOtherOtel = (text) => {
|
|
169
|
+
const t = otelLines(withoutOurs(text));
|
|
170
|
+
return t.length > 0 && !isValistrys(t);
|
|
171
|
+
};
|
|
172
|
+
var hasOtel = (text) => otelLines(withoutOurs(text)).length > 0;
|
|
173
|
+
function withoutOtel(ls) {
|
|
174
|
+
const out = [];
|
|
175
|
+
let skipping = false;
|
|
176
|
+
for (const l of ls) {
|
|
177
|
+
const h = header(l);
|
|
178
|
+
if (h !== void 0) skipping = isOtel(h);
|
|
179
|
+
if (!skipping) out.push(l);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
var tidy = (ls) => {
|
|
184
|
+
const t = ls.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
|
|
185
|
+
return t ? `${t}
|
|
186
|
+
` : "";
|
|
187
|
+
};
|
|
188
|
+
function addCodex(text, o) {
|
|
189
|
+
if (hasOtherOtel(text) && !o.force) return { after: text ?? "", replaced: false, conflict: "Codex already has an [otel] table in ~/.codex/config.toml. Add --force to replace it with Valistry's." };
|
|
190
|
+
const other = hasOtel(text);
|
|
191
|
+
const base = other ? withoutOtel(withoutOurs(text)) : withoutOurs(text);
|
|
192
|
+
const head = tidy(base);
|
|
193
|
+
const session = (o.deep || (text ?? "").includes(SESSION)) && !head.includes(SESSION);
|
|
194
|
+
return { after: `${head}${head ? "\n" : ""}${BEGIN}
|
|
195
|
+
${CODEX_OTEL(o.endpoint, o.key, o.person)}${session ? `
|
|
196
|
+
${SESSION_HOOK2}` : ""}
|
|
197
|
+
${END}
|
|
198
|
+
`, replaced: other };
|
|
199
|
+
}
|
|
200
|
+
function removeCodex(text, backup) {
|
|
201
|
+
const kept = tidy(withoutOurs(text));
|
|
202
|
+
const theirs = otelLines(lines(backup));
|
|
203
|
+
if (!theirs.length || hasOtel(kept)) return kept;
|
|
204
|
+
return `${kept}${kept ? "\n" : ""}${tidy(theirs)}`;
|
|
205
|
+
}
|
|
206
|
+
var codexConfigured = (text) => lines(text).includes(BEGIN);
|
|
207
|
+
|
|
208
|
+
// packages/cli/src/desktop.ts
|
|
209
|
+
import { createHash } from "node:crypto";
|
|
210
|
+
var IDLE_SECONDS = 300;
|
|
211
|
+
var SEND_EVERY_MS = 15 * 6e4;
|
|
212
|
+
var KEEP_DAYS = 10;
|
|
213
|
+
function appOfMac(bundle = "", name = "") {
|
|
214
|
+
const b = bundle.toLowerCase(), n = name.trim().toLowerCase();
|
|
215
|
+
if (b === "com.anthropic.claudefordesktop" || b.startsWith("com.anthropic.") && n === "claude") return "claude";
|
|
216
|
+
if (b === "com.openai.chat" || b.startsWith("com.openai.") && n === "chatgpt") return "chatgpt";
|
|
217
|
+
if (b.startsWith("com.microsoft.") && /copilot/.test(`${b} ${n}`)) return "copilot";
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function appOfWindows(process2 = "") {
|
|
221
|
+
const p = process2.trim().toLowerCase().replace(/\.exe$/, "");
|
|
222
|
+
if (p === "claude") return "claude";
|
|
223
|
+
if (p === "chatgpt") return "chatgpt";
|
|
224
|
+
if (/^(microsoft\.)?(m365)?copilot(\.app)?$/.test(p)) return "copilot";
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function activeApp(s, platform) {
|
|
228
|
+
const app = platform === "win32" ? appOfWindows(s.process) : appOfMac(s.bundle, s.name);
|
|
229
|
+
if (!app) return null;
|
|
230
|
+
return s.idleSeconds !== null && s.idleSeconds >= IDLE_SECONDS ? null : app;
|
|
231
|
+
}
|
|
232
|
+
var personCode = (email) => createHash("sha256").update(email.trim().toLowerCase()).digest("hex").slice(0, 16);
|
|
233
|
+
var localDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
234
|
+
var emptyState = () => ({ days: {}, sent: {} });
|
|
235
|
+
function record(state, app, now) {
|
|
236
|
+
if (state.lastSample && now.getTime() - Date.parse(state.lastSample) < 45e3) return state;
|
|
237
|
+
const day = localDay(now);
|
|
238
|
+
const d = { ...state.days[day] ?? { watched: 0 } };
|
|
239
|
+
d.watched += 1;
|
|
240
|
+
if (app) d[app] = (d[app] ?? 0) + 1;
|
|
241
|
+
const keepFrom = localDay(new Date(now.getTime() - KEEP_DAYS * 864e5));
|
|
242
|
+
const days = Object.fromEntries(Object.entries({ ...state.days, [day]: d }).filter(([k]) => k >= keepFrom));
|
|
243
|
+
const sent = Object.fromEntries(Object.entries(state.sent).filter(([k]) => k >= keepFrom));
|
|
244
|
+
return { ...state, days, sent, lastSample: now.toISOString() };
|
|
245
|
+
}
|
|
246
|
+
function unsent(state) {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const [day, d] of Object.entries(state.days).sort(([a], [b]) => a.localeCompare(b))) {
|
|
249
|
+
if (!(d.watched > 0)) continue;
|
|
250
|
+
for (const app of DESKTOP_APPS) {
|
|
251
|
+
const minutes = d[app] ?? 0;
|
|
252
|
+
if (state.sent[day]?.[app] !== minutes) out.push({ day, app, minutes });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
function sendDue(state, now) {
|
|
258
|
+
if (!state.lastTry) return true;
|
|
259
|
+
return localDay(new Date(state.lastTry)) !== localDay(now) || now.getTime() - Date.parse(state.lastTry) >= SEND_EVERY_MS;
|
|
260
|
+
}
|
|
261
|
+
function markSent(state, points) {
|
|
262
|
+
const sent = { ...state.sent };
|
|
263
|
+
for (const p of points) sent[p.day] = { ...sent[p.day] ?? {}, [p.app]: p.minutes };
|
|
264
|
+
return { ...state, sent };
|
|
265
|
+
}
|
|
266
|
+
var attr = (key, v) => ({ key, value: { stringValue: v } });
|
|
267
|
+
var nano = (ms) => `${BigInt(Math.round(ms)) * 1000000n}`;
|
|
268
|
+
var dayStart = (day) => {
|
|
269
|
+
const [y, m, d] = day.split("-").map(Number);
|
|
270
|
+
return new Date(y, m - 1, d).getTime();
|
|
271
|
+
};
|
|
272
|
+
function payload(points, who, now, version) {
|
|
273
|
+
return {
|
|
274
|
+
resourceMetrics: [{
|
|
275
|
+
resource: { attributes: [attr("service.name", "valistry-desktop"), attr("service.version", version)] },
|
|
276
|
+
scopeMetrics: [{ scope: { name: "valistry-desktop", version }, metrics: [{
|
|
277
|
+
name: DESKTOP_METRIC,
|
|
278
|
+
unit: "min",
|
|
279
|
+
sum: { aggregationTemporality: 2, isMonotonic: true, dataPoints: points.map((p) => ({
|
|
280
|
+
attributes: [attr("app", p.app), attr("day", p.day), attr("person", who.person), ...who.team ? [attr("team", who.team)] : []],
|
|
281
|
+
startTimeUnixNano: nano(dayStart(p.day)),
|
|
282
|
+
timeUnixNano: nano(Math.min(now.getTime(), dayStart(p.day) + 864e5 - 1)),
|
|
283
|
+
asInt: String(p.minutes)
|
|
284
|
+
})) }
|
|
285
|
+
}] }]
|
|
286
|
+
}]
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// packages/cli/src/system.ts
|
|
291
|
+
import { execFileSync } from "node:child_process";
|
|
292
|
+
import { join } from "node:path";
|
|
293
|
+
var run = (cmd2, args) => execFileSync(cmd2, args, { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
294
|
+
function sampleMac(r = run) {
|
|
295
|
+
let bundle, name, idleSeconds = null;
|
|
296
|
+
try {
|
|
297
|
+
const asn = r("lsappinfo", ["front"]).trim();
|
|
298
|
+
if (/^ASN:/.test(asn)) {
|
|
299
|
+
const info = r("lsappinfo", ["info", "-only", "bundleid", "-only", "name", asn]);
|
|
300
|
+
bundle = /"CFBundleIdentifier"="([^"]*)"/.exec(info)?.[1];
|
|
301
|
+
name = /"LSDisplayName"="([^"]*)"/.exec(info)?.[1];
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const ns = /"HIDIdleTime" = (\d+)/.exec(r("ioreg", ["-c", "IOHIDSystem", "-d", "4"]))?.[1];
|
|
307
|
+
if (ns) idleSeconds = Math.floor(Number(ns) / 1e9);
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
return { bundle, name, idleSeconds };
|
|
311
|
+
}
|
|
312
|
+
var WINDOWS_PROBE = [
|
|
313
|
+
"Add-Type -TypeDefinition @'",
|
|
314
|
+
"using System; using System.Runtime.InteropServices;",
|
|
315
|
+
"public static class ValistryProbe {",
|
|
316
|
+
' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
|
317
|
+
' [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint p);',
|
|
318
|
+
" [StructLayout(LayoutKind.Sequential)] public struct LastInput { public uint cbSize; public uint dwTime; }",
|
|
319
|
+
' [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LastInput l);',
|
|
320
|
+
"}",
|
|
321
|
+
"'@",
|
|
322
|
+
"$p = 0; [void][ValistryProbe]::GetWindowThreadProcessId([ValistryProbe]::GetForegroundWindow(), [ref]$p)",
|
|
323
|
+
"$l = New-Object ValistryProbe+LastInput; $l.cbSize = 8; [void][ValistryProbe]::GetLastInputInfo([ref]$l)",
|
|
324
|
+
"$idle = [math]::Floor(([uint32][Environment]::TickCount - $l.dwTime) / 1000)",
|
|
325
|
+
"$n = (Get-Process -Id $p -ErrorAction SilentlyContinue).ProcessName",
|
|
326
|
+
'Write-Output "$n|$idle"'
|
|
327
|
+
].join("\n");
|
|
328
|
+
function sampleWindows(r = run) {
|
|
329
|
+
try {
|
|
330
|
+
const [process2, idle] = r("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", WINDOWS_PROBE]).trim().split("|");
|
|
331
|
+
const n = Number(idle);
|
|
332
|
+
return { process: process2, idleSeconds: Number.isFinite(n) && idle !== "" ? n : null };
|
|
333
|
+
} catch {
|
|
334
|
+
return { idleSeconds: null };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
var LABEL = "ai.valistry.desktop";
|
|
338
|
+
var TASK = "Valistry desktop";
|
|
339
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
340
|
+
function launchAgent(node, script, log) {
|
|
341
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
342
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
343
|
+
<plist version="1.0">
|
|
344
|
+
<dict>
|
|
345
|
+
<key>Label</key><string>${LABEL}</string>
|
|
346
|
+
<key>ProgramArguments</key>
|
|
347
|
+
<array><string>${xml(node)}</string><string>${xml(script)}</string><string>observe</string></array>
|
|
348
|
+
<key>StartInterval</key><integer>60</integer>
|
|
349
|
+
<key>RunAtLoad</key><true/>
|
|
350
|
+
<key>ProcessType</key><string>Background</string>
|
|
351
|
+
<key>LowPriorityIO</key><true/>
|
|
352
|
+
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
353
|
+
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
354
|
+
</dict>
|
|
355
|
+
</plist>
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
var launchAgentPath = (home) => join(home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
359
|
+
var macStart = (uid, plist) => [["launchctl", ["bootstrap", `gui/${uid}`, plist]]];
|
|
360
|
+
var macStop = (uid) => [["launchctl", ["bootout", `gui/${uid}/${LABEL}`]]];
|
|
361
|
+
var windowsLauncher = (node, script) => `' Starts the Valistry desktop observer with no window. Written by npx valistry.\r
|
|
362
|
+
CreateObject("WScript.Shell").Run """${node}"" ""${script}"" observe", 0, False\r
|
|
363
|
+
`;
|
|
364
|
+
var windowsStart = (vbs) => [["schtasks", ["/Create", "/F", "/SC", "MINUTE", "/MO", "1", "/TN", TASK, "/TR", `wscript.exe "${vbs}"`]]];
|
|
365
|
+
var windowsStop = () => [["schtasks", ["/Delete", "/F", "/TN", TASK]]];
|
|
366
|
+
|
|
367
|
+
// packages/cli/src/setup.ts
|
|
368
|
+
var DEFAULT_ENDPOINT = "https://app.valistry.ai/api/otlp";
|
|
369
|
+
var pathsOf = (home) => {
|
|
370
|
+
const dir = join2(home, ".valistry");
|
|
371
|
+
return {
|
|
372
|
+
home,
|
|
373
|
+
dir,
|
|
374
|
+
backups: join2(dir, "backups"),
|
|
375
|
+
manifest: join2(dir, "setup.json"),
|
|
376
|
+
config: join2(dir, "desktop.json"),
|
|
377
|
+
session: join2(dir, "valistry-session.mjs"),
|
|
378
|
+
state: join2(dir, "desktop-state.json"),
|
|
379
|
+
script: join2(dir, "valistry.mjs"),
|
|
380
|
+
log: join2(dir, "desktop.log"),
|
|
381
|
+
vbs: join2(dir, "observer.vbs"),
|
|
382
|
+
plist: launchAgentPath(home),
|
|
383
|
+
claudeDir: join2(home, ".claude"),
|
|
384
|
+
claude: join2(home, ".claude", "settings.json"),
|
|
385
|
+
codexDir: join2(home, ".codex"),
|
|
386
|
+
codex: join2(home, ".codex", "config.toml")
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
var tilde = (p, home) => p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
390
|
+
var sha = (t) => createHash2("sha256").update(t).digest("hex");
|
|
391
|
+
var read = (p) => {
|
|
392
|
+
try {
|
|
393
|
+
return readFileSync(p, "utf8");
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var readJson = (p) => {
|
|
399
|
+
const t = read(p);
|
|
400
|
+
if (t === null) return null;
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(t);
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function writeAtomic(p, text, mode) {
|
|
408
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
409
|
+
const tmp = `${p}.valistry-${process.pid}`;
|
|
410
|
+
writeFileSync(tmp, text, mode ? { mode } : void 0);
|
|
411
|
+
renameSync(tmp, p);
|
|
412
|
+
if (mode) chmodSync(p, mode);
|
|
413
|
+
}
|
|
414
|
+
var KEY = /^vly_ot_.+_[0-9a-f]{32,}$/;
|
|
415
|
+
var lastFour = (key) => `\u2022\u2022${key.slice(-4)}`;
|
|
416
|
+
function foundKey(home) {
|
|
417
|
+
const p = pathsOf(home);
|
|
418
|
+
try {
|
|
419
|
+
const env = JSON.parse(read(p.claude) ?? "{}").env ?? {};
|
|
420
|
+
const k2 = /Bearer\s+(vly_ot_\S+)/.exec(String(env.OTEL_EXPORTER_OTLP_HEADERS ?? ""))?.[1];
|
|
421
|
+
const e = typeof env.OTEL_EXPORTER_OTLP_ENDPOINT === "string" ? env.OTEL_EXPORTER_OTLP_ENDPOINT.trim() : "";
|
|
422
|
+
if (k2 && KEY.test(k2)) return { key: k2, ...e ? { endpoint: e } : {}, from: "~/.claude/settings.json" };
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
const t = read(p.codex) ?? "";
|
|
426
|
+
const k = /"Authorization"\s*=\s*"Bearer\s+(vly_ot_[^"\s]+)"/.exec(t)?.[1];
|
|
427
|
+
if (k && KEY.test(k)) {
|
|
428
|
+
const e = /endpoint\s*=\s*"([^"]+?)\/v1\/(?:logs|metrics|traces)"/.exec(t)?.[1];
|
|
429
|
+
return { key: k, ...e ? { endpoint: e } : {}, from: "~/.codex/config.toml" };
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
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";
|
|
435
|
+
function setupPlan(o) {
|
|
436
|
+
const p = pathsOf(o.home);
|
|
437
|
+
const changes = [];
|
|
438
|
+
const notes = [];
|
|
439
|
+
const errors = [];
|
|
440
|
+
const found = o.key?.trim() ? null : foundKey(o.home);
|
|
441
|
+
const key = (o.key ?? found?.key ?? "").trim();
|
|
442
|
+
if (!key) errors.push("No key found. Make one in Valistry \u2192 Connect \u2192 Claude Code, then pass --key.");
|
|
443
|
+
else if (!KEY.test(key)) errors.push("That is not a Valistry ingest key. It starts with vly_ot_.");
|
|
444
|
+
const endpoint = (o.endpoint ?? found?.endpoint ?? DEFAULT_ENDPOINT).trim().replace(/\/+$/, "");
|
|
445
|
+
if (!/^https:\/\//.test(endpoint) && !/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(endpoint)) errors.push("The endpoint must start with https://.");
|
|
446
|
+
const using = found && !errors.length ? `Using the key already in ${found.from} (${lastFour(key)}), sending to ${endpoint}.` : void 0;
|
|
447
|
+
const email = (o.email ?? "").trim();
|
|
448
|
+
const team = o.team?.trim() || void 0;
|
|
449
|
+
if (errors.length) return { changes, notes, errors };
|
|
450
|
+
const prior = readJson(p.manifest);
|
|
451
|
+
if (o.claude !== false) {
|
|
452
|
+
if (!existsSync(p.claudeDir)) notes.push("Claude Code: not found on this computer (no ~/.claude). Skipped.");
|
|
453
|
+
else {
|
|
454
|
+
const before = read(p.claude);
|
|
455
|
+
try {
|
|
456
|
+
const m = mergeClaude(before, { endpoint, key, team, force: o.force, deep: o.deep });
|
|
457
|
+
if (m.conflict) errors.push(m.conflict);
|
|
458
|
+
else if (m.after === before) notes.push("Claude Code: already set up. No change.");
|
|
459
|
+
else changes.push({ kind: "write", path: p.claude, before, after: m.after, theirs: true, title: "Claude Code settings", lines: [
|
|
460
|
+
...m.added.length ? [`Adds to "env": ${m.added.join(", ")}.`] : [],
|
|
461
|
+
...m.changed.length ? [`Changes in "env": ${m.changed.join(", ")}.`] : [],
|
|
462
|
+
`Adds the ${m.hooks.join(" and ")} hooks. They send when a limit stops a turn and when a session ends.`,
|
|
463
|
+
"Keeps every other setting."
|
|
464
|
+
] });
|
|
465
|
+
} catch (e) {
|
|
466
|
+
errors.push(e.message);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (o.codex !== false) {
|
|
471
|
+
if (!existsSync(p.codexDir)) notes.push("Codex: not found on this computer (no ~/.codex). Skipped.");
|
|
472
|
+
else if (!email) notes.push("Codex: skipped. It needs your work email: add --email you@company.com.");
|
|
473
|
+
else {
|
|
474
|
+
const before = read(p.codex);
|
|
475
|
+
const a = addCodex(before, { endpoint, key, person: email, force: o.force, deep: o.deep });
|
|
476
|
+
if (a.conflict) errors.push(a.conflict);
|
|
477
|
+
else if (a.after === before) notes.push("Codex: already set up. No change.");
|
|
478
|
+
else changes.push({ kind: "write", path: p.codex, before, after: a.after, theirs: true, title: "Codex settings", lines: [
|
|
479
|
+
`${a.replaced ? "Replaces your [otel] table with" : "Adds"} an [otel] table at the end. Prompts stay off (log_user_prompt = false).`,
|
|
480
|
+
"Keeps every other line."
|
|
481
|
+
] });
|
|
482
|
+
}
|
|
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
|
+
] });
|
|
488
|
+
if (o.desktop) {
|
|
489
|
+
if (o.platform !== "darwin" && o.platform !== "win32") errors.push("The desktop observer runs on macOS and Windows only.");
|
|
490
|
+
else if (!email) errors.push("The desktop observer needs your work email, to count you as one person: add --email you@company.com.");
|
|
491
|
+
else {
|
|
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 };
|
|
500
|
+
const shown = `Each minute it checks which app is in front. It counts the Claude, ChatGPT and Microsoft Copilot apps.`;
|
|
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."] });
|
|
502
|
+
changes.push({ kind: "write", path: p.config, before: read(p.config), after: `${JSON.stringify(config, null, 2)}
|
|
503
|
+
`, secret: true, title: "Its settings", lines: [
|
|
504
|
+
`The address, your key, and your email as a code (${config.person}). Only you can read the file.`
|
|
505
|
+
] });
|
|
506
|
+
if (o.platform === "darwin") {
|
|
507
|
+
const plist = launchAgent(o.node, p.script, p.log);
|
|
508
|
+
if (existsSync(p.plist)) changes.push({ kind: "run", ...cmd(macStop(o.uid)[0]), title: "Stops the observer that is running", mayFail: true });
|
|
509
|
+
changes.push({ kind: "write", path: p.plist, before: read(p.plist), after: plist, title: "A LaunchAgent", lines: [shown, "It starts again when you log in."] });
|
|
510
|
+
changes.push({ kind: "run", ...cmd(macStart(o.uid, p.plist)[0]), title: "Starts it now" });
|
|
511
|
+
} else {
|
|
512
|
+
changes.push({ kind: "write", path: p.vbs, before: read(p.vbs), after: windowsLauncher(o.node, p.script), title: "A launcher with no window", lines: [shown] });
|
|
513
|
+
changes.push({ kind: "run", ...cmd(windowsStart(p.vbs)[0]), title: "A Scheduled Task, each minute, as you" });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!changes.length && !errors.length && !notes.length) notes.push("Nothing to set up.");
|
|
518
|
+
return { changes, notes, errors, manifest: prior ?? void 0, key, endpoint, using };
|
|
519
|
+
}
|
|
520
|
+
var stamp = (d) => d.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
521
|
+
function applyPlan(plan, o) {
|
|
522
|
+
const p = pathsOf(o.home);
|
|
523
|
+
const done = [];
|
|
524
|
+
const prior = readJson(p.manifest);
|
|
525
|
+
const files = new Map((prior?.files ?? []).map((f) => [f.path, f]));
|
|
526
|
+
for (const c of plan.changes) {
|
|
527
|
+
if (c.kind === "write") {
|
|
528
|
+
const known = files.get(c.path);
|
|
529
|
+
let backup = known?.backup ?? null;
|
|
530
|
+
if (!known && c.theirs && c.before !== null) {
|
|
531
|
+
mkdirSync(p.backups, { recursive: true });
|
|
532
|
+
backup = join2(p.backups, `${basename(dirname(c.path)).replace(/^\./, "")}-${basename(c.path)}.${stamp(o.now)}`);
|
|
533
|
+
copyFileSync(c.path, backup);
|
|
534
|
+
done.push(`Saved a copy of ${tilde(c.path, o.home)} to ${tilde(backup, o.home)}.`);
|
|
535
|
+
}
|
|
536
|
+
writeAtomic(c.path, c.after, c.secret ? 384 : void 0);
|
|
537
|
+
files.set(c.path, { path: c.path, backup: known ? known.backup : backup, sha: sha(c.after), theirs: Boolean(c.theirs) });
|
|
538
|
+
done.push(`Wrote ${tilde(c.path, o.home)}.`);
|
|
539
|
+
} else if (c.kind === "delete") {
|
|
540
|
+
rmSync(c.path, { force: true });
|
|
541
|
+
done.push(`Removed ${tilde(c.path, o.home)}.`);
|
|
542
|
+
} else {
|
|
543
|
+
try {
|
|
544
|
+
o.run(c.cmd, c.args);
|
|
545
|
+
done.push(`Ran ${c.cmd} ${c.args.join(" ")}.`);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
if (!c.mayFail) throw new Error(`${c.cmd} ${c.args.join(" ")} failed: ${e.message.split("\n")[0]}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const stop = o.stop ?? prior?.stop ?? [];
|
|
552
|
+
const manifest = { version: o.version, at: o.now.toISOString(), endpoint: o.endpoint, platform: o.platform, files: [...files.values()], stop };
|
|
553
|
+
writeAtomic(p.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
554
|
+
`);
|
|
555
|
+
return done;
|
|
556
|
+
}
|
|
557
|
+
var stopsFor = (plan, o) => plan.changes.some((c) => c.kind === "run" && !c.mayFail) ? o.platform === "darwin" ? macStop(o.uid) : windowsStop() : void 0;
|
|
558
|
+
function removePlan(home) {
|
|
559
|
+
const p = pathsOf(home);
|
|
560
|
+
const m = readJson(p.manifest);
|
|
561
|
+
if (!m) return { changes: [], notes: ["Nothing to remove: this computer has no Valistry setup."], errors: [] };
|
|
562
|
+
const changes = [];
|
|
563
|
+
for (const [c, args] of m.stop) changes.push({ kind: "run", cmd: c, args, title: "Stops the observer", mayFail: true });
|
|
564
|
+
for (const f of m.files) {
|
|
565
|
+
const now = read(f.path);
|
|
566
|
+
if (!f.theirs) {
|
|
567
|
+
if (now !== null) changes.push({ kind: "delete", path: f.path, title: `Deletes ${basename(f.path)}`, lines: [] });
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const backup = f.backup ? read(f.backup) : null;
|
|
571
|
+
const label = f.path.endsWith("config.toml") ? "Codex settings" : "Claude Code settings";
|
|
572
|
+
if (now === null) continue;
|
|
573
|
+
if (sha(now) === f.sha) {
|
|
574
|
+
if (f.backup === null) changes.push({ kind: "delete", path: f.path, title: label, lines: ["Deletes it: there was no file before setup."] });
|
|
575
|
+
else changes.push({ kind: "write", path: f.path, before: now, after: backup ?? now, title: label, lines: [`Puts back the copy from ${tilde(f.backup, home)}.`] });
|
|
576
|
+
} else {
|
|
577
|
+
const after = f.path.endsWith("config.toml") ? removeCodex(now, backup) : unmergeClaude(now, backup);
|
|
578
|
+
changes.push({ kind: "write", path: f.path, before: now, after, title: label, lines: ["It changed after setup, so only what setup added is taken out. Every other change is kept."] });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const extra of [p.state, p.log]) if (existsSync(extra)) changes.push({ kind: "delete", path: extra, title: `Deletes ${basename(extra)}`, lines: [] });
|
|
582
|
+
changes.push({ kind: "delete", path: p.manifest, title: "Deletes setup.json", lines: [] });
|
|
583
|
+
return { changes, notes: m.files.some((f) => f.backup) ? [`The copies stay in ${tilde(p.backups, home)}.`] : [], errors: [] };
|
|
584
|
+
}
|
|
585
|
+
var ago = (iso, now) => {
|
|
586
|
+
const m = Math.max(0, Math.round((now.getTime() - Date.parse(iso)) / 6e4));
|
|
587
|
+
return m < 1 ? "just now" : m < 120 ? `${m} min ago` : `${Math.round(m / 60)} hours ago`;
|
|
588
|
+
};
|
|
589
|
+
function status(home, now, running) {
|
|
590
|
+
const p = pathsOf(home);
|
|
591
|
+
const m = readJson(p.manifest);
|
|
592
|
+
const lines2 = [];
|
|
593
|
+
if (!m) return { lines: ["Not set up on this computer. Run: npx valistry setup --key <your key>"] };
|
|
594
|
+
lines2.push(`Set up ${m.at.slice(0, 16).replace("T", " ")} UTC, sending to ${m.endpoint}.`);
|
|
595
|
+
lines2.push(`Claude Code: ${claudeConfigured(read(p.claude), m.endpoint) ? "on" : m.files.some((f) => f.path === p.claude) ? "changed since setup: not sending here" : "not set up"}.`);
|
|
596
|
+
lines2.push(`Codex: ${codexConfigured(read(p.codex)) ? "on" : "not set up"}.`);
|
|
597
|
+
const config = readJson(p.config);
|
|
598
|
+
if (!config) lines2.push("Desktop apps: not set up. Add --desktop to count them.");
|
|
599
|
+
else {
|
|
600
|
+
const s = readJson(p.state) ?? emptyState();
|
|
601
|
+
lines2.push(`Desktop apps: ${running === false ? "set up, but the observer is not running" : "on"}. Your code: ${config.person}${config.team ? `, team ${config.team}` : ""}.`);
|
|
602
|
+
const today = Object.entries(s.days).sort(([a], [b]) => b.localeCompare(a))[0];
|
|
603
|
+
if (today) lines2.push(`${today[0]}: Claude ${today[1].claude ?? 0} min, ChatGPT ${today[1].chatgpt ?? 0} min, Copilot ${today[1].copilot ?? 0} min, of ${today[1].watched} min watched.`);
|
|
604
|
+
const l = s.lastSend;
|
|
605
|
+
lines2.push(!l ? "Last send: none yet. It sends within 15 minutes." : l.ok ? `Last send: ${ago(l.at, now)}, received.` : `Last send: ${ago(l.at, now)}, failed${l.status ? ` (${l.status})` : ""}${l.message ? `: ${l.message}` : ""}.`);
|
|
606
|
+
}
|
|
607
|
+
return { lines: lines2 };
|
|
608
|
+
}
|
|
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
|
+
|
|
659
|
+
// packages/cli/src/observe.ts
|
|
660
|
+
async function observeOnce(o) {
|
|
661
|
+
const p = pathsOf(o.home);
|
|
662
|
+
const config = readJson(p.config);
|
|
663
|
+
if (!config) return null;
|
|
664
|
+
const s0 = readJson(p.state) ?? emptyState();
|
|
665
|
+
const sample = o.sample ?? (o.platform === "win32" ? () => sampleWindows() : () => sampleMac());
|
|
666
|
+
let s = record(s0, activeApp(sample(), o.platform), o.now);
|
|
667
|
+
if (o.force || sendDue(s, o.now)) s = await sendNow(s, config, o.now, o.send ?? envProxyFetch());
|
|
668
|
+
writeAtomic(p.state, `${JSON.stringify(s)}
|
|
669
|
+
`);
|
|
670
|
+
return s;
|
|
671
|
+
}
|
|
672
|
+
async function sendNow(s, config, now, send) {
|
|
673
|
+
const points = unsent(s);
|
|
674
|
+
if (!points.length) return { ...s, lastTry: now.toISOString() };
|
|
675
|
+
const body = JSON.stringify(payload(points, { person: config.person, team: config.team }, now, config.version));
|
|
676
|
+
try {
|
|
677
|
+
const r = await send(`${config.endpoint}/v1/metrics`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${config.key}` }, body });
|
|
678
|
+
const text = await r.text().catch(() => "");
|
|
679
|
+
const message = (() => {
|
|
680
|
+
try {
|
|
681
|
+
return String(JSON.parse(text)?.partialSuccess?.errorMessage ?? "").slice(0, 200);
|
|
682
|
+
} catch {
|
|
683
|
+
return "";
|
|
684
|
+
}
|
|
685
|
+
})();
|
|
686
|
+
const ok = r.status >= 200 && r.status < 300;
|
|
687
|
+
const next = ok ? markSent(s, points) : s;
|
|
688
|
+
return { ...next, lastTry: now.toISOString(), lastSend: { at: now.toISOString(), ok, status: r.status, ...message && !ok ? { message } : {} } };
|
|
689
|
+
} catch (e) {
|
|
690
|
+
return { ...s, lastTry: now.toISOString(), lastSend: { at: now.toISOString(), ok: false, message: `Could not reach ${config.endpoint}: ${e.message}`.slice(0, 200) } };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// packages/cli/src/main.ts
|
|
695
|
+
var VERSION = true ? "0.1.1" : "0.0.0";
|
|
696
|
+
var HELP = `valistry ${VERSION}: sets up this computer's AI tools to send their use to Valistry. Counts only.
|
|
697
|
+
|
|
698
|
+
npx valistry setup [--key <key>] [--email you@company.com] [--team <name>] [--desktop] [--deep] [--apply]
|
|
699
|
+
Claude Code: adds the telemetry settings to ~/.claude/settings.json (a copy is saved first).
|
|
700
|
+
Codex: adds an [otel] table to ~/.codex/config.toml (a copy is saved first).
|
|
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.
|
|
703
|
+
Shows every change first. Add --apply to make them.
|
|
704
|
+
If Claude Code already sends to Valistry, you can leave out --key: its key and address are used.
|
|
705
|
+
Other options: --endpoint <url> (default ${DEFAULT_ENDPOINT}), --no-claude, --no-codex, --force.
|
|
706
|
+
|
|
707
|
+
npx valistry status What is set up, and the last send.
|
|
708
|
+
npx valistry remove [--apply] Undoes everything setup did, and puts your files back.
|
|
709
|
+
|
|
710
|
+
Never read or sent: prompts, replies, code, window titles, screen contents, keys you type.`;
|
|
711
|
+
function parseArgs(argv) {
|
|
712
|
+
const [cmd2 = "help", ...rest] = argv;
|
|
713
|
+
const flags = {};
|
|
714
|
+
for (let i = 0; i < rest.length; i++) {
|
|
715
|
+
const a = rest[i];
|
|
716
|
+
if (!a.startsWith("--")) continue;
|
|
717
|
+
const [name, inline] = a.slice(2).split(/=(.*)/s, 2);
|
|
718
|
+
if (inline !== void 0) flags[name] = inline;
|
|
719
|
+
else if (rest[i + 1] !== void 0 && !rest[i + 1].startsWith("--")) flags[name] = rest[++i];
|
|
720
|
+
else flags[name] = true;
|
|
721
|
+
}
|
|
722
|
+
return { cmd: cmd2, flags };
|
|
723
|
+
}
|
|
724
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
725
|
+
var gitEmail = () => {
|
|
726
|
+
try {
|
|
727
|
+
return execFileSync2("git", ["config", "--global", "user.email"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim() || void 0;
|
|
728
|
+
} catch {
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var masked = (text, key) => key && key.length > 12 ? text.split(key).join(lastFour(key)) : text;
|
|
733
|
+
function describe(plan, home, key) {
|
|
734
|
+
const out = [];
|
|
735
|
+
plan.changes.forEach((c, i) => {
|
|
736
|
+
const where = c.kind === "run" ? `runs: ${c.cmd} ${c.args.map((a) => tilde(a, home)).map((a) => /\s/.test(a) ? JSON.stringify(a) : a).join(" ")}` : tilde(c.path, home);
|
|
737
|
+
out.push(`${i + 1}. ${c.title} ${where}`);
|
|
738
|
+
if (c.kind === "write" && c.theirs && c.before !== null) out.push(" A copy is saved first, in ~/.valistry/backups.");
|
|
739
|
+
for (const l of c.kind === "run" ? [] : c.lines) out.push(` ${l}`);
|
|
740
|
+
});
|
|
741
|
+
for (const n of plan.notes) out.push(n);
|
|
742
|
+
return out.map((l) => masked(l, key));
|
|
743
|
+
}
|
|
744
|
+
var SENDS = [
|
|
745
|
+
"What is sent: from Claude Code and Codex, counts (tokens, sessions, lines, commits, errors); from the desktop apps, minutes per app per day.",
|
|
746
|
+
"Never read or sent: prompts, replies, code, window titles, screen contents, keys you type. Your email is sent only as a code."
|
|
747
|
+
];
|
|
748
|
+
async function main(argv, io = { log: console.log, err: console.error }) {
|
|
749
|
+
const { cmd: cmd2, flags } = parseArgs(argv);
|
|
750
|
+
const home = str(flags.home) ?? process.env.VALISTRY_HOME ?? homedir();
|
|
751
|
+
const platform = process.platform;
|
|
752
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
753
|
+
const now = /* @__PURE__ */ new Date();
|
|
754
|
+
const apply = flags.apply === true;
|
|
755
|
+
if (cmd2 === "setup") {
|
|
756
|
+
const key = str(flags.key) ?? process.env.VALISTRY_INGEST_KEY;
|
|
757
|
+
const endpoint = str(flags.endpoint)?.replace(/\/+$/, "");
|
|
758
|
+
const plan = setupPlan({
|
|
759
|
+
key,
|
|
760
|
+
endpoint,
|
|
761
|
+
team: str(flags.team),
|
|
762
|
+
email: str(flags.email) ?? gitEmail(),
|
|
763
|
+
emailFromGit: !str(flags.email),
|
|
764
|
+
desktop: flags.desktop === true,
|
|
765
|
+
deep: flags.deep === true,
|
|
766
|
+
claude: flags["no-claude"] !== true,
|
|
767
|
+
codex: flags["no-codex"] !== true,
|
|
768
|
+
force: flags.force === true,
|
|
769
|
+
home,
|
|
770
|
+
platform,
|
|
771
|
+
node: process.execPath,
|
|
772
|
+
uid,
|
|
773
|
+
version: VERSION,
|
|
774
|
+
self: fileURLToPath(import.meta.url)
|
|
775
|
+
});
|
|
776
|
+
if (plan.errors.length) {
|
|
777
|
+
for (const e of plan.errors) io.err(masked(e, key));
|
|
778
|
+
io.err("Nothing was changed.");
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
const say = () => {
|
|
782
|
+
if (plan.using) io.log(masked(plan.using, plan.key));
|
|
783
|
+
};
|
|
784
|
+
if (!plan.changes.length) {
|
|
785
|
+
say();
|
|
786
|
+
for (const l of describe(plan, home, plan.key)) io.log(l);
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
if (!apply) {
|
|
790
|
+
io.log("Valistry setup, a dry run. Nothing has changed yet.\n");
|
|
791
|
+
if (plan.using) {
|
|
792
|
+
say();
|
|
793
|
+
io.log("");
|
|
794
|
+
}
|
|
795
|
+
for (const l of describe(plan, home, plan.key)) io.log(l);
|
|
796
|
+
io.log("");
|
|
797
|
+
for (const l of SENDS) io.log(l);
|
|
798
|
+
io.log("\nTo make these changes, run the same command with --apply.\nTo undo them later: npx valistry remove --apply");
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
say();
|
|
802
|
+
try {
|
|
803
|
+
for (const l of applyPlan(plan, { home, run, now, version: VERSION, endpoint: plan.endpoint ?? DEFAULT_ENDPOINT, platform, stop: stopsFor(plan, { platform, uid }) })) io.log(masked(l, plan.key));
|
|
804
|
+
} catch (e) {
|
|
805
|
+
io.err(masked(e.message, plan.key));
|
|
806
|
+
io.err("Run npx valistry status to see what is set up.");
|
|
807
|
+
return 1;
|
|
808
|
+
}
|
|
809
|
+
io.log("\nDone. Start a new Claude Code or Codex session to begin sending.");
|
|
810
|
+
if (flags.desktop === true && platform === "darwin") io.log("The observer runs each minute. Check it with: npx valistry status");
|
|
811
|
+
return 0;
|
|
812
|
+
}
|
|
813
|
+
if (cmd2 === "remove") {
|
|
814
|
+
const plan = removePlan(home);
|
|
815
|
+
if (!plan.changes.length) {
|
|
816
|
+
for (const n of plan.notes) io.log(n);
|
|
817
|
+
return 0;
|
|
818
|
+
}
|
|
819
|
+
if (!apply) {
|
|
820
|
+
io.log("Valistry remove, a dry run. Nothing has changed yet.\n");
|
|
821
|
+
for (const l of describe(plan, home)) io.log(l);
|
|
822
|
+
io.log("\nTo undo the setup, run: npx valistry remove --apply");
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
for (const l of applyRemove(plan, home)) io.log(l);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
io.err(e.message);
|
|
829
|
+
return 1;
|
|
830
|
+
}
|
|
831
|
+
for (const n of plan.notes) io.log(n);
|
|
832
|
+
io.log("Done. Start new Claude Code and Codex sessions to stop sending.");
|
|
833
|
+
return 0;
|
|
834
|
+
}
|
|
835
|
+
if (cmd2 === "status") {
|
|
836
|
+
for (const l of status(home, now, observerRunning(home, platform, uid)).lines) io.log(l);
|
|
837
|
+
return 0;
|
|
838
|
+
}
|
|
839
|
+
if (cmd2 === "observe") {
|
|
840
|
+
if (flags.print === true) {
|
|
841
|
+
const s = platform === "win32" ? sampleWindows() : sampleMac();
|
|
842
|
+
io.log(`In front: ${activeApp(s, platform) ?? "none of the three apps"}. Seconds since last input: ${s.idleSeconds ?? "unknown"}.`);
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
await observeOnce({ home, platform, now });
|
|
847
|
+
} catch (e) {
|
|
848
|
+
io.err(`valistry observe: ${e.message}`);
|
|
849
|
+
}
|
|
850
|
+
return 0;
|
|
851
|
+
}
|
|
852
|
+
if (cmd2 === "send") {
|
|
853
|
+
const p = pathsOf(home);
|
|
854
|
+
const config = readJson(p.config);
|
|
855
|
+
if (!config) {
|
|
856
|
+
io.err("The desktop observer is not set up.");
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
const s = await sendNow(readJson(p.state) ?? emptyState(), config, now, envProxyFetch());
|
|
860
|
+
writeAtomic(p.state, `${JSON.stringify(s)}
|
|
861
|
+
`);
|
|
862
|
+
io.log(s.lastSend ? s.lastSend.ok ? "Sent." : `Not sent${s.lastSend.status ? ` (${s.lastSend.status})` : ""}: ${s.lastSend.message ?? ""}` : "Nothing new to send.");
|
|
863
|
+
return s.lastSend && !s.lastSend.ok ? 1 : 0;
|
|
864
|
+
}
|
|
865
|
+
if (cmd2 === "version" || flags.version === true) {
|
|
866
|
+
io.log(VERSION);
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
io.log(HELP);
|
|
870
|
+
return cmd2 === "help" || flags.help === true ? 0 : 1;
|
|
871
|
+
}
|
|
872
|
+
function applyRemove(plan, home, r = run) {
|
|
873
|
+
const done = [];
|
|
874
|
+
for (const c of plan.changes) {
|
|
875
|
+
if (c.kind === "run") {
|
|
876
|
+
try {
|
|
877
|
+
r(c.cmd, c.args);
|
|
878
|
+
done.push(`Ran ${c.cmd} ${c.args.join(" ")}.`);
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
} else if (c.kind === "delete") {
|
|
882
|
+
rmSync2(c.path, { force: true });
|
|
883
|
+
done.push(`Removed ${tilde(c.path, home)}.`);
|
|
884
|
+
} else {
|
|
885
|
+
writeAtomic(c.path, c.after);
|
|
886
|
+
done.push(`Put back ${tilde(c.path, home)}.`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return done;
|
|
890
|
+
}
|
|
891
|
+
function observerRunning(home, platform, uid) {
|
|
892
|
+
if (!existsSync2(pathsOf(home).config)) return void 0;
|
|
893
|
+
try {
|
|
894
|
+
if (platform === "darwin") {
|
|
895
|
+
run("launchctl", ["print", `gui/${uid}/${LABEL}`]);
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
if (platform === "win32") {
|
|
899
|
+
run("schtasks", ["/Query", "/TN", TASK]);
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// packages/cli/src/cli.ts
|
|
909
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
910
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
911
|
+
process.exit(1);
|
|
912
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "valistry",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": { "valistry": "bin/valistry.mjs" },
|
|
7
|
+
"files": ["bin", "README.md"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"license": "UNLICENSED",
|
|
10
|
+
"homepage": "https://valistry.ai",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node build.mjs",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"prepack": "node build.mjs --check"
|
|
15
|
+
}
|
|
16
|
+
}
|