@indigoai-us/hq-cloud 5.44.0 → 5.46.0
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/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +39 -12
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +63 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +65 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +107 -0
- package/dist/skill-telemetry.d.ts.map +1 -0
- package/dist/skill-telemetry.js +395 -0
- package/dist/skill-telemetry.js.map +1 -0
- package/dist/skill-telemetry.test.d.ts +2 -0
- package/dist/skill-telemetry.test.d.ts.map +1 -0
- package/dist/skill-telemetry.test.js +219 -0
- package/dist/skill-telemetry.test.js.map +1 -0
- package/dist/vault-client.d.ts +23 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +10 -0
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
- package/scripts/vault-rescue.sh +283 -0
- package/src/bin/sync-runner.ts +39 -13
- package/src/cli/sync.test.ts +81 -0
- package/src/cli/sync.ts +75 -0
- package/src/index.ts +16 -0
- package/src/skill-telemetry.test.ts +279 -0
- package/src/skill-telemetry.ts +499 -0
- package/src/vault-client.ts +34 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-invocation telemetry collector.
|
|
3
|
+
*
|
|
4
|
+
* Sibling to `./telemetry.ts` (the token-usage collector). Where that one
|
|
5
|
+
* promotes token-accounting fields off each Claude Code session row, this one
|
|
6
|
+
* extracts *which skill / slash-command was invoked*, reading the SAME
|
|
7
|
+
* `~/.claude/projects/**\/*.jsonl` session logs but with an independent
|
|
8
|
+
* byte-offset cursor at `~/.hq/skill-telemetry-cursor.json` and shipping to
|
|
9
|
+
* `/v1/skill-invocations`.
|
|
10
|
+
*
|
|
11
|
+
* Why a separate collector rather than folding into `./telemetry.ts`: the
|
|
12
|
+
* token path is proven and its per-batch cursor mechanics are load-bearing.
|
|
13
|
+
* Skill events are sparse, so this collector uses a simpler all-or-nothing
|
|
14
|
+
* per-run cursor commit (re-delivery is idempotent server-side via the
|
|
15
|
+
* composite eventKey). Keeping it standalone means a bug here can never
|
|
16
|
+
* regress token telemetry.
|
|
17
|
+
*
|
|
18
|
+
* Two capture paths, both recoverable from the transcript (verified against
|
|
19
|
+
* real sessions):
|
|
20
|
+
* - User-typed slash command → a `user` row whose content carries
|
|
21
|
+
* `<command-name>/foo</command-name>` (+ optional `<command-args>`).
|
|
22
|
+
* - Model-invoked skill → an `assistant` row with a `tool_use` block whose
|
|
23
|
+
* `name === "Skill"` and `input.skill` names the skill.
|
|
24
|
+
* The two are mutually exclusive per invocation, so there is no double-count.
|
|
25
|
+
*
|
|
26
|
+
* Privacy: raw `<command-args>` / `input.args` content is NEVER sent to the
|
|
27
|
+
* cloud — only a `hasArgs` boolean. This matches the message-stripping posture
|
|
28
|
+
* of `./telemetry.ts::sanitizeRow`, which deliberately drops all prompt/tool
|
|
29
|
+
* content client-side. Flip `INCLUDE_ARGS_PREVIEW` only with a deliberate
|
|
30
|
+
* privacy review and a matching server-side allowlist change.
|
|
31
|
+
*
|
|
32
|
+
* Trust model + error handling are identical to `./telemetry.ts`: personUid is
|
|
33
|
+
* resolved server-side from the JWT (never the body), and all errors are
|
|
34
|
+
* swallowed so telemetry never aborts or delays a sync.
|
|
35
|
+
*/
|
|
36
|
+
import { promises as fs } from "node:fs";
|
|
37
|
+
import * as os from "node:os";
|
|
38
|
+
import * as path from "node:path";
|
|
39
|
+
// Privacy switch — keep false (see file header). When false, raw argument text
|
|
40
|
+
// never leaves the machine; only the `hasArgs` boolean is emitted.
|
|
41
|
+
const INCLUDE_ARGS_PREVIEW = false;
|
|
42
|
+
async function loadCursor(cursorPath) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await fs.readFile(cursorPath, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (parsed && typeof parsed === "object" && parsed.files && typeof parsed.files === "object") {
|
|
47
|
+
return { version: parsed.version ?? "1", files: parsed.files };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Missing / unparseable — start fresh.
|
|
52
|
+
}
|
|
53
|
+
return { version: "1", files: {} };
|
|
54
|
+
}
|
|
55
|
+
async function saveCursor(cursorPath, cursor) {
|
|
56
|
+
await fs.mkdir(path.dirname(cursorPath), { recursive: true });
|
|
57
|
+
const tmp = `${cursorPath}.tmp`;
|
|
58
|
+
await fs.writeFile(tmp, JSON.stringify(cursor, null, 2), "utf-8");
|
|
59
|
+
await fs.rename(tmp, cursorPath);
|
|
60
|
+
}
|
|
61
|
+
async function readLocalTelemetryEnabled(menubarPath) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await fs.readFile(menubarPath, "utf-8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
return parsed.telemetryEnabled === true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ── Extractor ───────────────────────────────────────────────────────────────
|
|
72
|
+
const CMD_NAME = /<command-name>\s*\/?([^<]+?)\s*<\/command-name>/;
|
|
73
|
+
const CMD_ARGS = /<command-args>([\s\S]*?)<\/command-args>/;
|
|
74
|
+
function rowText(content) {
|
|
75
|
+
if (typeof content === "string")
|
|
76
|
+
return content;
|
|
77
|
+
if (Array.isArray(content)) {
|
|
78
|
+
return content
|
|
79
|
+
.map((b) => (b && typeof b === "object" && typeof b.text === "string"
|
|
80
|
+
? b.text
|
|
81
|
+
: ""))
|
|
82
|
+
.join(" ");
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract zero or more skill-invocation events from a single parsed session
|
|
88
|
+
* row. A `user` row yields at most one typed command; an `assistant` row can
|
|
89
|
+
* carry multiple `Skill` tool_use blocks (rare, but handled).
|
|
90
|
+
*/
|
|
91
|
+
export function extractSkillEvents(row) {
|
|
92
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
93
|
+
return [];
|
|
94
|
+
const obj = row;
|
|
95
|
+
const type = obj.type;
|
|
96
|
+
const msg = obj.message && typeof obj.message === "object" && !Array.isArray(obj.message)
|
|
97
|
+
? obj.message
|
|
98
|
+
: undefined;
|
|
99
|
+
if (!msg)
|
|
100
|
+
return [];
|
|
101
|
+
const sessionId = typeof obj.sessionId === "string" ? obj.sessionId : undefined;
|
|
102
|
+
const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : undefined;
|
|
103
|
+
const cwd = typeof obj.cwd === "string" ? obj.cwd : undefined;
|
|
104
|
+
const rowUuid = typeof obj.uuid === "string" ? obj.uuid : undefined;
|
|
105
|
+
// Path A — user-typed slash command.
|
|
106
|
+
if (type === "user") {
|
|
107
|
+
const text = rowText(msg.content);
|
|
108
|
+
const m = CMD_NAME.exec(text);
|
|
109
|
+
if (!m)
|
|
110
|
+
return [];
|
|
111
|
+
const a = CMD_ARGS.exec(text);
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
skill: m[1].trim(),
|
|
115
|
+
source: "typed",
|
|
116
|
+
sessionId,
|
|
117
|
+
timestamp,
|
|
118
|
+
cwd,
|
|
119
|
+
uuid: rowUuid,
|
|
120
|
+
hasArgs: Boolean(a && a[1].trim()),
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// Path B — model-invoked Skill tool_use.
|
|
125
|
+
if (type === "assistant" && Array.isArray(msg.content)) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const blk of msg.content) {
|
|
128
|
+
if (!blk || typeof blk !== "object")
|
|
129
|
+
continue;
|
|
130
|
+
const b = blk;
|
|
131
|
+
if (b.type !== "tool_use" || b.name !== "Skill")
|
|
132
|
+
continue;
|
|
133
|
+
const input = b.input && typeof b.input === "object" && !Array.isArray(b.input)
|
|
134
|
+
? b.input
|
|
135
|
+
: {};
|
|
136
|
+
const skill = typeof input.skill === "string" ? input.skill : "";
|
|
137
|
+
if (!skill)
|
|
138
|
+
continue;
|
|
139
|
+
const args = input.args;
|
|
140
|
+
out.push({
|
|
141
|
+
skill,
|
|
142
|
+
source: "model",
|
|
143
|
+
sessionId,
|
|
144
|
+
timestamp,
|
|
145
|
+
cwd,
|
|
146
|
+
// Prefer the tool_use block id (stable, globally unique) for dedup.
|
|
147
|
+
uuid: typeof b.id === "string" ? b.id : rowUuid,
|
|
148
|
+
hasArgs: typeof args === "string" ? args.trim().length > 0 : Boolean(args),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
/** Shape the event for the wire. Drops raw args unless explicitly enabled. */
|
|
156
|
+
function toWireRow(ev) {
|
|
157
|
+
const row = {
|
|
158
|
+
skill: ev.skill,
|
|
159
|
+
source: ev.source,
|
|
160
|
+
hasArgs: ev.hasArgs,
|
|
161
|
+
};
|
|
162
|
+
if (ev.sessionId !== undefined)
|
|
163
|
+
row.sessionId = ev.sessionId;
|
|
164
|
+
if (ev.timestamp !== undefined)
|
|
165
|
+
row.timestamp = ev.timestamp;
|
|
166
|
+
if (ev.uuid !== undefined)
|
|
167
|
+
row.uuid = ev.uuid;
|
|
168
|
+
if (ev.cwd !== undefined)
|
|
169
|
+
row.cwd = ev.cwd;
|
|
170
|
+
// INCLUDE_ARGS_PREVIEW is intentionally a compile-time constant `false`;
|
|
171
|
+
// the guarded branch documents the (currently disabled) egress path.
|
|
172
|
+
if (INCLUDE_ARGS_PREVIEW) {
|
|
173
|
+
// Reserved: a server allowlist change must land before this is enabled.
|
|
174
|
+
}
|
|
175
|
+
return row;
|
|
176
|
+
}
|
|
177
|
+
// ── File walker ───────────────────────────────────────────────────────────────
|
|
178
|
+
async function listJsonlFiles(root) {
|
|
179
|
+
const out = [];
|
|
180
|
+
async function walk(dir) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const ent of entries) {
|
|
189
|
+
const full = path.join(dir, ent.name);
|
|
190
|
+
if (ent.isDirectory()) {
|
|
191
|
+
await walk(full);
|
|
192
|
+
}
|
|
193
|
+
else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
|
|
194
|
+
out.push(full);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
await walk(root);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
const MAX_BATCH_BYTES = 1_000_000;
|
|
202
|
+
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
203
|
+
/**
|
|
204
|
+
* Scan, extract, and POST any new skill-invocation events.
|
|
205
|
+
*
|
|
206
|
+
* Cursor model (per-batch commit, matching the token collector for robustness):
|
|
207
|
+
* each file is scanned from its stored byte offset to EOF; extracted events
|
|
208
|
+
* carry the byte offset of the line they came from. Events are flushed in
|
|
209
|
+
* ≤1 MiB batches, and the cursor advances **per successful batch** — so if one
|
|
210
|
+
* batch in a large (e.g. first-run backfill) fails, the batches that already
|
|
211
|
+
* succeeded stay committed and only the rest re-send next sync.
|
|
212
|
+
*
|
|
213
|
+
* Per-file commit rule:
|
|
214
|
+
* - All of a file's events sent OK (including zero-event files) → commit EOF,
|
|
215
|
+
* so quiet/non-skill tails are never re-scanned.
|
|
216
|
+
* - Some of a file's events failed → commit the max byte offset whose batch
|
|
217
|
+
* succeeded (partial progress); the remainder re-sends next sync.
|
|
218
|
+
* Server-side dedup on the composite eventKey makes any re-send idempotent.
|
|
219
|
+
* Rotation/truncation resets the offset to 0 (re-read from the top).
|
|
220
|
+
*/
|
|
221
|
+
export async function collectAndSendSkillTelemetry(opts) {
|
|
222
|
+
const home = os.homedir();
|
|
223
|
+
const claudeProjectsRoot = opts.claudeProjectsRoot ?? path.join(home, ".claude", "projects");
|
|
224
|
+
const cursorPath = opts.cursorPath ?? path.join(home, ".hq", "skill-telemetry-cursor.json");
|
|
225
|
+
const menubarPath = opts.menubarPath ?? path.join(home, ".hq", "menubar.json");
|
|
226
|
+
const log = opts.log ?? (() => { });
|
|
227
|
+
// Normalize the scope path once (drop a single trailing slash, keeping "/").
|
|
228
|
+
const normalizePath = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
229
|
+
const scopeCwd = opts.hqRoot !== undefined ? normalizePath(opts.hqRoot) : undefined;
|
|
230
|
+
// 1. Opt-in check — reuse the same gate as token telemetry.
|
|
231
|
+
let enabled;
|
|
232
|
+
let optInSource;
|
|
233
|
+
try {
|
|
234
|
+
const resp = await opts.client.getTelemetryOptIn();
|
|
235
|
+
enabled = resp.enabled === true;
|
|
236
|
+
optInSource = "server";
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
log(`[skill-telemetry] opt-in check failed (${err.message ?? err}) — falling back to local menubar.json`);
|
|
240
|
+
enabled = await readLocalTelemetryEnabled(menubarPath);
|
|
241
|
+
optInSource = "menubar-fallback";
|
|
242
|
+
}
|
|
243
|
+
if (!enabled) {
|
|
244
|
+
return { enabled: false, optInSource, filesScanned: 0, eventsSent: 0, batchesSent: 0 };
|
|
245
|
+
}
|
|
246
|
+
// 2. Cursor + file enumeration.
|
|
247
|
+
const cursor = await loadCursor(cursorPath);
|
|
248
|
+
const files = await listJsonlFiles(claudeProjectsRoot);
|
|
249
|
+
const fileScans = {};
|
|
250
|
+
const rotationResets = {};
|
|
251
|
+
const sourced = [];
|
|
252
|
+
for (const filePath of files) {
|
|
253
|
+
let stat;
|
|
254
|
+
try {
|
|
255
|
+
stat = await fs.stat(filePath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const currentSize = stat.size;
|
|
261
|
+
const currentMtime = Math.floor(stat.mtimeMs / 1000);
|
|
262
|
+
const stored = cursor.files[filePath] ?? { offset: 0, mtime: 0 };
|
|
263
|
+
let offset = stored.offset;
|
|
264
|
+
// Rotation / truncation → re-read from the top.
|
|
265
|
+
const rotated = currentSize < offset || (stored.mtime > 0 && currentMtime < stored.mtime);
|
|
266
|
+
if (rotated) {
|
|
267
|
+
offset = 0;
|
|
268
|
+
rotationResets[filePath] = { offset: 0, mtime: currentMtime };
|
|
269
|
+
}
|
|
270
|
+
// Record the scan even when there are no new bytes — a fully-drained file
|
|
271
|
+
// (eventCount 0, offset already at EOF) should still settle at EOF below.
|
|
272
|
+
fileScans[filePath] = { eof: currentSize, mtime: currentMtime, eventCount: 0 };
|
|
273
|
+
if (offset >= currentSize && !rotated)
|
|
274
|
+
continue;
|
|
275
|
+
let content;
|
|
276
|
+
try {
|
|
277
|
+
const fh = await fs.open(filePath, "r");
|
|
278
|
+
try {
|
|
279
|
+
const length = Math.max(0, currentSize - offset);
|
|
280
|
+
const buf = Buffer.alloc(length);
|
|
281
|
+
await fh.read(buf, 0, length, offset);
|
|
282
|
+
content = buf.toString("utf-8");
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
await fh.close();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Could not read — drop the scan so we don't claim progress for it.
|
|
290
|
+
delete fileScans[filePath];
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// Compute the absolute end-byte offset of each line in the read region.
|
|
294
|
+
const segments = content.split("\n");
|
|
295
|
+
let cumulative = offset;
|
|
296
|
+
for (let i = 0; i < segments.length; i++) {
|
|
297
|
+
cumulative += Buffer.byteLength(segments[i], "utf-8");
|
|
298
|
+
if (i < segments.length - 1)
|
|
299
|
+
cumulative += 1; // the split newline byte
|
|
300
|
+
const endOffset = cumulative;
|
|
301
|
+
const trimmed = segments[i].trim();
|
|
302
|
+
if (trimmed.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(trimmed);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
for (const ev of extractSkillEvents(parsed)) {
|
|
312
|
+
// Scope filter: only emit invocations made from the HQ project.
|
|
313
|
+
if (scopeCwd !== undefined && (ev.cwd === undefined || normalizePath(ev.cwd) !== scopeCwd)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
sourced.push({ row: toWireRow(ev), filePath, endOffset });
|
|
317
|
+
fileScans[filePath].eventCount++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// 4. Flush in ≤1 MiB batches, advancing per-file progress on each 2xx.
|
|
322
|
+
let eventsSent = 0;
|
|
323
|
+
let batchesSent = 0;
|
|
324
|
+
// Per file: count of events successfully sent + max committed byte offset.
|
|
325
|
+
const sentCount = {};
|
|
326
|
+
const committedOffset = {};
|
|
327
|
+
const envelopeBytes = Buffer.byteLength(JSON.stringify({ machineId: opts.machineId, installerVersion: opts.installerVersion, events: [] }), "utf-8");
|
|
328
|
+
let batch = [];
|
|
329
|
+
let batchBytes = envelopeBytes;
|
|
330
|
+
const flush = async () => {
|
|
331
|
+
if (batch.length === 0)
|
|
332
|
+
return;
|
|
333
|
+
const toSend = batch;
|
|
334
|
+
batch = [];
|
|
335
|
+
batchBytes = envelopeBytes;
|
|
336
|
+
try {
|
|
337
|
+
await opts.client.postSkillInvocations({
|
|
338
|
+
machineId: opts.machineId,
|
|
339
|
+
installerVersion: opts.installerVersion,
|
|
340
|
+
events: toSend.map((s) => s.row),
|
|
341
|
+
});
|
|
342
|
+
batchesSent++;
|
|
343
|
+
eventsSent += toSend.length;
|
|
344
|
+
// Advance per-file progress for the events in this (successful) batch.
|
|
345
|
+
for (const s of toSend) {
|
|
346
|
+
sentCount[s.filePath] = (sentCount[s.filePath] ?? 0) + 1;
|
|
347
|
+
const prev = committedOffset[s.filePath] ?? 0;
|
|
348
|
+
if (s.endOffset > prev)
|
|
349
|
+
committedOffset[s.filePath] = s.endOffset;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
log(`[skill-telemetry] postSkillInvocations failed (${err.message ?? err}) — these rows re-send next sync`);
|
|
354
|
+
// Cursor not advanced for this batch; eventKey dedups the eventual re-send.
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
for (const s of sourced) {
|
|
358
|
+
const rowBytes = Buffer.byteLength(JSON.stringify(s.row), "utf-8");
|
|
359
|
+
const addCost = rowBytes + (batch.length > 0 ? 1 : 0);
|
|
360
|
+
if (batch.length > 0 && batchBytes + addCost > MAX_BATCH_BYTES) {
|
|
361
|
+
await flush();
|
|
362
|
+
batchBytes = envelopeBytes + rowBytes;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
batchBytes += addCost;
|
|
366
|
+
}
|
|
367
|
+
batch.push(s);
|
|
368
|
+
}
|
|
369
|
+
await flush();
|
|
370
|
+
// 5. Build the new cursor: loaded < rotationResets < per-file commit.
|
|
371
|
+
// A file settles at EOF only when every event extracted from it this run
|
|
372
|
+
// was sent OK (zero-event files included); otherwise it settles at the
|
|
373
|
+
// highest byte offset whose batch succeeded, so the rest re-sends.
|
|
374
|
+
const finalFiles = { ...cursor.files };
|
|
375
|
+
for (const [fp, entry] of Object.entries(rotationResets))
|
|
376
|
+
finalFiles[fp] = entry;
|
|
377
|
+
for (const [fp, scan] of Object.entries(fileScans)) {
|
|
378
|
+
if ((sentCount[fp] ?? 0) >= scan.eventCount) {
|
|
379
|
+
finalFiles[fp] = { offset: scan.eof, mtime: scan.mtime };
|
|
380
|
+
}
|
|
381
|
+
else if (fp in committedOffset) {
|
|
382
|
+
finalFiles[fp] = { offset: committedOffset[fp], mtime: scan.mtime };
|
|
383
|
+
}
|
|
384
|
+
// else: no progress for this file — leave loaded/rotation-reset offset.
|
|
385
|
+
}
|
|
386
|
+
await saveCursor(cursorPath, { version: "1", files: finalFiles });
|
|
387
|
+
return {
|
|
388
|
+
enabled: true,
|
|
389
|
+
optInSource,
|
|
390
|
+
filesScanned: files.length,
|
|
391
|
+
eventsSent,
|
|
392
|
+
batchesSent,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=skill-telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-telemetry.js","sourceRoot":"","sources":["../src/skill-telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-telemetry.test.d.ts","sourceRoot":"","sources":["../src/skill-telemetry.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { extractSkillEvents, collectAndSendSkillTelemetry } from "./skill-telemetry.js";
|
|
6
|
+
describe("extractSkillEvents", () => {
|
|
7
|
+
it("extracts a user-typed slash command with args", () => {
|
|
8
|
+
const row = {
|
|
9
|
+
type: "user",
|
|
10
|
+
sessionId: "sess-1",
|
|
11
|
+
timestamp: "2026-06-03T10:10:07.220Z",
|
|
12
|
+
cwd: "/home/ec2-user/hq",
|
|
13
|
+
uuid: "u-1",
|
|
14
|
+
message: {
|
|
15
|
+
role: "user",
|
|
16
|
+
content: "<command-message>personal:worktree</command-message> <command-name>/personal:worktree</command-name> <command-args>pull latest</command-args>",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const events = extractSkillEvents(row);
|
|
20
|
+
expect(events).toEqual([
|
|
21
|
+
{
|
|
22
|
+
skill: "personal:worktree",
|
|
23
|
+
source: "typed",
|
|
24
|
+
sessionId: "sess-1",
|
|
25
|
+
timestamp: "2026-06-03T10:10:07.220Z",
|
|
26
|
+
cwd: "/home/ec2-user/hq",
|
|
27
|
+
uuid: "u-1",
|
|
28
|
+
hasArgs: true,
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
it("marks hasArgs false when command-args is absent or empty", () => {
|
|
33
|
+
const row = {
|
|
34
|
+
type: "user",
|
|
35
|
+
message: { role: "user", content: "<command-name>/deploy</command-name>" },
|
|
36
|
+
};
|
|
37
|
+
expect(extractSkillEvents(row)[0]).toMatchObject({ skill: "deploy", source: "typed", hasArgs: false });
|
|
38
|
+
});
|
|
39
|
+
it("extracts a model-invoked Skill tool_use using the block id for dedup", () => {
|
|
40
|
+
const row = {
|
|
41
|
+
type: "assistant",
|
|
42
|
+
sessionId: "sess-2",
|
|
43
|
+
timestamp: "2026-06-03T14:00:00.000Z",
|
|
44
|
+
cwd: "/home/ec2-user/hq",
|
|
45
|
+
uuid: "row-uuid",
|
|
46
|
+
message: {
|
|
47
|
+
role: "assistant",
|
|
48
|
+
content: [
|
|
49
|
+
{ type: "text", text: "ok" },
|
|
50
|
+
{ type: "tool_use", id: "toolu_abc", name: "Skill", input: { skill: "indigo:hello-world" } },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
expect(extractSkillEvents(row)).toEqual([
|
|
55
|
+
{
|
|
56
|
+
skill: "indigo:hello-world",
|
|
57
|
+
source: "model",
|
|
58
|
+
sessionId: "sess-2",
|
|
59
|
+
timestamp: "2026-06-03T14:00:00.000Z",
|
|
60
|
+
cwd: "/home/ec2-user/hq",
|
|
61
|
+
uuid: "toolu_abc",
|
|
62
|
+
hasArgs: false,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
it("sets hasArgs true for a model invocation with args, without capturing the args text", () => {
|
|
67
|
+
const row = {
|
|
68
|
+
type: "assistant",
|
|
69
|
+
message: {
|
|
70
|
+
role: "assistant",
|
|
71
|
+
content: [{ type: "tool_use", name: "Skill", input: { skill: "code-review", args: "high" } }],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const ev = extractSkillEvents(row)[0];
|
|
75
|
+
expect(ev).toMatchObject({ skill: "code-review", source: "model", hasArgs: true });
|
|
76
|
+
// Privacy: raw args must never appear on the extracted event.
|
|
77
|
+
expect(JSON.stringify(ev)).not.toContain("high");
|
|
78
|
+
});
|
|
79
|
+
it("ignores non-Skill tool_use blocks and non-invocation rows", () => {
|
|
80
|
+
expect(extractSkillEvents({
|
|
81
|
+
type: "assistant",
|
|
82
|
+
message: { role: "assistant", content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }] },
|
|
83
|
+
})).toEqual([]);
|
|
84
|
+
expect(extractSkillEvents({ type: "user", message: { content: "just a normal message" } })).toEqual([]);
|
|
85
|
+
expect(extractSkillEvents(null)).toEqual([]);
|
|
86
|
+
expect(extractSkillEvents("nope")).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("collectAndSendSkillTelemetry — hqRoot scoping", () => {
|
|
90
|
+
function stubClient(captured) {
|
|
91
|
+
return {
|
|
92
|
+
async getTelemetryOptIn() {
|
|
93
|
+
return { enabled: true, updatedAt: null };
|
|
94
|
+
},
|
|
95
|
+
async postSkillInvocations(batch) {
|
|
96
|
+
captured.push(batch);
|
|
97
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function row(obj) {
|
|
102
|
+
return JSON.stringify(obj);
|
|
103
|
+
}
|
|
104
|
+
it("emits only invocations whose cwd equals hqRoot", async () => {
|
|
105
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
|
|
106
|
+
const projects = path.join(tmp, "projects");
|
|
107
|
+
const hqRoot = "/home/ec2-user/hq";
|
|
108
|
+
// Project A — sessions run from the HQ root.
|
|
109
|
+
const dirA = path.join(projects, "-home-ec2-user-hq");
|
|
110
|
+
await fs.mkdir(dirA, { recursive: true });
|
|
111
|
+
await fs.writeFile(path.join(dirA, "a.jsonl"), [
|
|
112
|
+
row({ type: "user", sessionId: "s1", timestamp: "2026-06-03T10:00:00Z", cwd: hqRoot, uuid: "u1", message: { role: "user", content: "<command-name>/deploy</command-name>" } }),
|
|
113
|
+
row({ type: "assistant", sessionId: "s1", timestamp: "2026-06-03T10:01:00Z", cwd: hqRoot, message: { role: "assistant", content: [{ type: "tool_use", id: "t1", name: "Skill", input: { skill: "indigo:hello-world" } }] } }),
|
|
114
|
+
].join("\n") + "\n", "utf-8");
|
|
115
|
+
// Project B — a different repo on the same machine. Must be excluded.
|
|
116
|
+
const dirB = path.join(projects, "-home-ec2-user-other");
|
|
117
|
+
await fs.mkdir(dirB, { recursive: true });
|
|
118
|
+
await fs.writeFile(path.join(dirB, "b.jsonl"), row({ type: "user", sessionId: "s2", timestamp: "2026-06-03T10:02:00Z", cwd: "/home/ec2-user/other", uuid: "u2", message: { role: "user", content: "<command-name>/secret-skill</command-name>" } }) + "\n", "utf-8");
|
|
119
|
+
const captured = [];
|
|
120
|
+
const result = await collectAndSendSkillTelemetry({
|
|
121
|
+
client: stubClient(captured),
|
|
122
|
+
machineId: "m-test",
|
|
123
|
+
installerVersion: "test",
|
|
124
|
+
hqRoot,
|
|
125
|
+
claudeProjectsRoot: projects,
|
|
126
|
+
cursorPath: path.join(tmp, "cursor.json"),
|
|
127
|
+
});
|
|
128
|
+
const skills = captured.flatMap((b) => b.events.map((e) => e.skill));
|
|
129
|
+
expect(result.eventsSent).toBe(2);
|
|
130
|
+
expect(skills.sort()).toEqual(["deploy", "indigo:hello-world"]);
|
|
131
|
+
// The other-repo invocation never leaves the machine.
|
|
132
|
+
expect(skills).not.toContain("secret-skill");
|
|
133
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
it("commits the cursor to EOF on a clean run — a rerun re-sends nothing", async () => {
|
|
136
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
|
|
137
|
+
const projects = path.join(tmp, "projects");
|
|
138
|
+
const dir = path.join(projects, "-p");
|
|
139
|
+
await fs.mkdir(dir, { recursive: true });
|
|
140
|
+
await fs.writeFile(path.join(dir, "s.jsonl"), row({ type: "user", sessionId: "s1", timestamp: "2026-06-04T09:00:00Z", cwd: "/x", uuid: "u1", message: { role: "user", content: "<command-name>/deploy</command-name>" } }) + "\n", "utf-8");
|
|
141
|
+
const cursorPath = path.join(tmp, "cursor.json");
|
|
142
|
+
const captured = [];
|
|
143
|
+
const client = stubClient(captured);
|
|
144
|
+
const r1 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
145
|
+
expect(r1.eventsSent).toBe(1);
|
|
146
|
+
const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
147
|
+
expect(r2.eventsSent).toBe(0); // cursor at EOF → nothing re-sent
|
|
148
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
149
|
+
});
|
|
150
|
+
it("does NOT advance the cursor when the POST fails — re-sends on the next run", async () => {
|
|
151
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
|
|
152
|
+
const projects = path.join(tmp, "projects");
|
|
153
|
+
const dir = path.join(projects, "-p");
|
|
154
|
+
await fs.mkdir(dir, { recursive: true });
|
|
155
|
+
await fs.writeFile(path.join(dir, "s.jsonl"), row({ type: "user", sessionId: "s1", timestamp: "2026-06-04T09:00:00Z", cwd: "/x", uuid: "u1", message: { role: "user", content: "<command-name>/deploy</command-name>" } }) + "\n", "utf-8");
|
|
156
|
+
const cursorPath = path.join(tmp, "cursor.json");
|
|
157
|
+
const captured = [];
|
|
158
|
+
let failNext = true;
|
|
159
|
+
const client = {
|
|
160
|
+
async getTelemetryOptIn() {
|
|
161
|
+
return { enabled: true, updatedAt: null };
|
|
162
|
+
},
|
|
163
|
+
async postSkillInvocations(batch) {
|
|
164
|
+
if (failNext) {
|
|
165
|
+
failNext = false;
|
|
166
|
+
throw new Error("network down");
|
|
167
|
+
}
|
|
168
|
+
captured.push(batch);
|
|
169
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const r1 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
173
|
+
expect(r1.eventsSent).toBe(0); // post threw → nothing committed
|
|
174
|
+
expect(captured).toHaveLength(0);
|
|
175
|
+
const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
176
|
+
expect(r2.eventsSent).toBe(1); // re-scanned from offset 0 and delivered
|
|
177
|
+
expect(captured.flatMap((b) => b.events.map((e) => e.skill))).toEqual(["deploy"]);
|
|
178
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
it("picks up only newly-appended events on a later run (incremental offset)", async () => {
|
|
181
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
|
|
182
|
+
const projects = path.join(tmp, "projects");
|
|
183
|
+
const dir = path.join(projects, "-p");
|
|
184
|
+
await fs.mkdir(dir, { recursive: true });
|
|
185
|
+
const file = path.join(dir, "s.jsonl");
|
|
186
|
+
await fs.writeFile(file, row({ type: "user", sessionId: "s1", timestamp: "2026-06-04T09:00:00Z", cwd: "/x", uuid: "u1", message: { role: "user", content: "<command-name>/deploy</command-name>" } }) + "\n", "utf-8");
|
|
187
|
+
const cursorPath = path.join(tmp, "cursor.json");
|
|
188
|
+
const captured = [];
|
|
189
|
+
const client = stubClient(captured);
|
|
190
|
+
await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
191
|
+
// New invocation appended after the first clean run.
|
|
192
|
+
await fs.appendFile(file, row({ type: "assistant", sessionId: "s1", timestamp: "2026-06-04T09:05:00Z", cwd: "/x", message: { role: "assistant", content: [{ type: "tool_use", id: "t9", name: "Skill", input: { skill: "land" } }] } }) + "\n", "utf-8");
|
|
193
|
+
const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
|
|
194
|
+
expect(r2.eventsSent).toBe(1);
|
|
195
|
+
// Only the appended event — not the original — is re-sent.
|
|
196
|
+
const lastBatch = captured[captured.length - 1];
|
|
197
|
+
expect(lastBatch.events.map((e) => e.skill)).toEqual(["land"]);
|
|
198
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
199
|
+
});
|
|
200
|
+
it("captures every project when hqRoot is omitted", async () => {
|
|
201
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
|
|
202
|
+
const projects = path.join(tmp, "projects");
|
|
203
|
+
const dir = path.join(projects, "-proj");
|
|
204
|
+
await fs.mkdir(dir, { recursive: true });
|
|
205
|
+
await fs.writeFile(path.join(dir, "c.jsonl"), row({ type: "user", sessionId: "s3", timestamp: "2026-06-03T10:03:00Z", cwd: "/anywhere", uuid: "u3", message: { role: "user", content: "<command-name>/anywhere-skill</command-name>" } }) + "\n", "utf-8");
|
|
206
|
+
const captured = [];
|
|
207
|
+
const result = await collectAndSendSkillTelemetry({
|
|
208
|
+
client: stubClient(captured),
|
|
209
|
+
machineId: "m-test",
|
|
210
|
+
installerVersion: "test",
|
|
211
|
+
claudeProjectsRoot: projects,
|
|
212
|
+
cursorPath: path.join(tmp, "cursor.json"),
|
|
213
|
+
});
|
|
214
|
+
expect(result.eventsSent).toBe(1);
|
|
215
|
+
expect(captured[0].events[0].skill).toBe("anywhere-skill");
|
|
216
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=skill-telemetry.test.js.map
|