@indigoai-us/hq-cloud 5.45.0 → 5.47.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.
Files changed (64) hide show
  1. package/dist/bin/sync-runner.d.ts +12 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +78 -12
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +27 -1
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/share.d.ts.map +1 -1
  8. package/dist/cli/share.js +17 -2
  9. package/dist/cli/share.js.map +1 -1
  10. package/dist/cli/share.test.js +2 -0
  11. package/dist/cli/share.test.js.map +1 -1
  12. package/dist/cli/sync-scope.test.js +1 -0
  13. package/dist/cli/sync-scope.test.js.map +1 -1
  14. package/dist/cli/sync.d.ts.map +1 -1
  15. package/dist/cli/sync.js +11 -1
  16. package/dist/cli/sync.js.map +1 -1
  17. package/dist/cli/sync.test.js +1 -0
  18. package/dist/cli/sync.test.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/object-io.d.ts +218 -0
  24. package/dist/object-io.d.ts.map +1 -0
  25. package/dist/object-io.js +588 -0
  26. package/dist/object-io.js.map +1 -0
  27. package/dist/object-io.test.d.ts +11 -0
  28. package/dist/object-io.test.d.ts.map +1 -0
  29. package/dist/object-io.test.js +568 -0
  30. package/dist/object-io.test.js.map +1 -0
  31. package/dist/s3.d.ts +37 -0
  32. package/dist/s3.d.ts.map +1 -1
  33. package/dist/s3.js +207 -198
  34. package/dist/s3.js.map +1 -1
  35. package/dist/skill-telemetry.d.ts +107 -0
  36. package/dist/skill-telemetry.d.ts.map +1 -0
  37. package/dist/skill-telemetry.js +395 -0
  38. package/dist/skill-telemetry.js.map +1 -0
  39. package/dist/skill-telemetry.test.d.ts +2 -0
  40. package/dist/skill-telemetry.test.d.ts.map +1 -0
  41. package/dist/skill-telemetry.test.js +219 -0
  42. package/dist/skill-telemetry.test.js.map +1 -0
  43. package/dist/vault-client.d.ts +91 -0
  44. package/dist/vault-client.d.ts.map +1 -1
  45. package/dist/vault-client.js +45 -0
  46. package/dist/vault-client.js.map +1 -1
  47. package/package.json +1 -1
  48. package/scripts/presign-transport-e2e.mjs +203 -0
  49. package/scripts/vault-rebaseline.sh +275 -0
  50. package/scripts/vault-rescue.sh +291 -0
  51. package/src/bin/sync-runner.test.ts +41 -0
  52. package/src/bin/sync-runner.ts +91 -13
  53. package/src/cli/share.test.ts +2 -0
  54. package/src/cli/share.ts +29 -2
  55. package/src/cli/sync-scope.test.ts +1 -0
  56. package/src/cli/sync.test.ts +1 -0
  57. package/src/cli/sync.ts +22 -1
  58. package/src/index.ts +16 -0
  59. package/src/object-io.test.ts +663 -0
  60. package/src/object-io.ts +782 -0
  61. package/src/s3.ts +259 -233
  62. package/src/skill-telemetry.test.ts +279 -0
  63. package/src/skill-telemetry.ts +499 -0
  64. package/src/vault-client.ts +135 -0
@@ -0,0 +1,279 @@
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
+ import type { SkillInvocationBatch } from "./vault-client.js";
7
+
8
+ describe("extractSkillEvents", () => {
9
+ it("extracts a user-typed slash command with args", () => {
10
+ const row = {
11
+ type: "user",
12
+ sessionId: "sess-1",
13
+ timestamp: "2026-06-03T10:10:07.220Z",
14
+ cwd: "/home/ec2-user/hq",
15
+ uuid: "u-1",
16
+ message: {
17
+ role: "user",
18
+ content:
19
+ "<command-message>personal:worktree</command-message> <command-name>/personal:worktree</command-name> <command-args>pull latest</command-args>",
20
+ },
21
+ };
22
+ const events = extractSkillEvents(row);
23
+ expect(events).toEqual([
24
+ {
25
+ skill: "personal:worktree",
26
+ source: "typed",
27
+ sessionId: "sess-1",
28
+ timestamp: "2026-06-03T10:10:07.220Z",
29
+ cwd: "/home/ec2-user/hq",
30
+ uuid: "u-1",
31
+ hasArgs: true,
32
+ },
33
+ ]);
34
+ });
35
+
36
+ it("marks hasArgs false when command-args is absent or empty", () => {
37
+ const row = {
38
+ type: "user",
39
+ message: { role: "user", content: "<command-name>/deploy</command-name>" },
40
+ };
41
+ expect(extractSkillEvents(row)[0]).toMatchObject({ skill: "deploy", source: "typed", hasArgs: false });
42
+ });
43
+
44
+ it("extracts a model-invoked Skill tool_use using the block id for dedup", () => {
45
+ const row = {
46
+ type: "assistant",
47
+ sessionId: "sess-2",
48
+ timestamp: "2026-06-03T14:00:00.000Z",
49
+ cwd: "/home/ec2-user/hq",
50
+ uuid: "row-uuid",
51
+ message: {
52
+ role: "assistant",
53
+ content: [
54
+ { type: "text", text: "ok" },
55
+ { type: "tool_use", id: "toolu_abc", name: "Skill", input: { skill: "indigo:hello-world" } },
56
+ ],
57
+ },
58
+ };
59
+ expect(extractSkillEvents(row)).toEqual([
60
+ {
61
+ skill: "indigo:hello-world",
62
+ source: "model",
63
+ sessionId: "sess-2",
64
+ timestamp: "2026-06-03T14:00:00.000Z",
65
+ cwd: "/home/ec2-user/hq",
66
+ uuid: "toolu_abc",
67
+ hasArgs: false,
68
+ },
69
+ ]);
70
+ });
71
+
72
+ it("sets hasArgs true for a model invocation with args, without capturing the args text", () => {
73
+ const row = {
74
+ type: "assistant",
75
+ message: {
76
+ role: "assistant",
77
+ content: [{ type: "tool_use", name: "Skill", input: { skill: "code-review", args: "high" } }],
78
+ },
79
+ };
80
+ const ev = extractSkillEvents(row)[0];
81
+ expect(ev).toMatchObject({ skill: "code-review", source: "model", hasArgs: true });
82
+ // Privacy: raw args must never appear on the extracted event.
83
+ expect(JSON.stringify(ev)).not.toContain("high");
84
+ });
85
+
86
+ it("ignores non-Skill tool_use blocks and non-invocation rows", () => {
87
+ expect(
88
+ extractSkillEvents({
89
+ type: "assistant",
90
+ message: { role: "assistant", content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }] },
91
+ }),
92
+ ).toEqual([]);
93
+ expect(extractSkillEvents({ type: "user", message: { content: "just a normal message" } })).toEqual([]);
94
+ expect(extractSkillEvents(null)).toEqual([]);
95
+ expect(extractSkillEvents("nope")).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe("collectAndSendSkillTelemetry — hqRoot scoping", () => {
100
+ function stubClient(captured: SkillInvocationBatch[]) {
101
+ return {
102
+ async getTelemetryOptIn() {
103
+ return { enabled: true, updatedAt: null };
104
+ },
105
+ async postSkillInvocations(batch: SkillInvocationBatch) {
106
+ captured.push(batch);
107
+ return { ok: true, written: batch.events.length, skipped: [] };
108
+ },
109
+ };
110
+ }
111
+
112
+ function row(obj: Record<string, unknown>): string {
113
+ return JSON.stringify(obj);
114
+ }
115
+
116
+ it("emits only invocations whose cwd equals hqRoot", async () => {
117
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
118
+ const projects = path.join(tmp, "projects");
119
+ const hqRoot = "/home/ec2-user/hq";
120
+
121
+ // Project A — sessions run from the HQ root.
122
+ const dirA = path.join(projects, "-home-ec2-user-hq");
123
+ await fs.mkdir(dirA, { recursive: true });
124
+ await fs.writeFile(
125
+ path.join(dirA, "a.jsonl"),
126
+ [
127
+ row({ type: "user", sessionId: "s1", timestamp: "2026-06-03T10:00:00Z", cwd: hqRoot, uuid: "u1", message: { role: "user", content: "<command-name>/deploy</command-name>" } }),
128
+ 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" } }] } }),
129
+ ].join("\n") + "\n",
130
+ "utf-8",
131
+ );
132
+
133
+ // Project B — a different repo on the same machine. Must be excluded.
134
+ const dirB = path.join(projects, "-home-ec2-user-other");
135
+ await fs.mkdir(dirB, { recursive: true });
136
+ await fs.writeFile(
137
+ path.join(dirB, "b.jsonl"),
138
+ 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",
139
+ "utf-8",
140
+ );
141
+
142
+ const captured: SkillInvocationBatch[] = [];
143
+ const result = await collectAndSendSkillTelemetry({
144
+ client: stubClient(captured),
145
+ machineId: "m-test",
146
+ installerVersion: "test",
147
+ hqRoot,
148
+ claudeProjectsRoot: projects,
149
+ cursorPath: path.join(tmp, "cursor.json"),
150
+ });
151
+
152
+ const skills = captured.flatMap((b) => b.events.map((e) => e.skill));
153
+ expect(result.eventsSent).toBe(2);
154
+ expect(skills.sort()).toEqual(["deploy", "indigo:hello-world"]);
155
+ // The other-repo invocation never leaves the machine.
156
+ expect(skills).not.toContain("secret-skill");
157
+
158
+ await fs.rm(tmp, { recursive: true, force: true });
159
+ });
160
+
161
+ it("commits the cursor to EOF on a clean run — a rerun re-sends nothing", async () => {
162
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
163
+ const projects = path.join(tmp, "projects");
164
+ const dir = path.join(projects, "-p");
165
+ await fs.mkdir(dir, { recursive: true });
166
+ await fs.writeFile(
167
+ path.join(dir, "s.jsonl"),
168
+ 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",
169
+ "utf-8",
170
+ );
171
+ const cursorPath = path.join(tmp, "cursor.json");
172
+ const captured: SkillInvocationBatch[] = [];
173
+ const client = stubClient(captured);
174
+
175
+ const r1 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
176
+ expect(r1.eventsSent).toBe(1);
177
+ const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
178
+ expect(r2.eventsSent).toBe(0); // cursor at EOF → nothing re-sent
179
+
180
+ await fs.rm(tmp, { recursive: true, force: true });
181
+ });
182
+
183
+ it("does NOT advance the cursor when the POST fails — re-sends on the next run", async () => {
184
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
185
+ const projects = path.join(tmp, "projects");
186
+ const dir = path.join(projects, "-p");
187
+ await fs.mkdir(dir, { recursive: true });
188
+ await fs.writeFile(
189
+ path.join(dir, "s.jsonl"),
190
+ 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",
191
+ "utf-8",
192
+ );
193
+ const cursorPath = path.join(tmp, "cursor.json");
194
+ const captured: SkillInvocationBatch[] = [];
195
+ let failNext = true;
196
+ const client = {
197
+ async getTelemetryOptIn() {
198
+ return { enabled: true, updatedAt: null };
199
+ },
200
+ async postSkillInvocations(batch: SkillInvocationBatch) {
201
+ if (failNext) {
202
+ failNext = false;
203
+ throw new Error("network down");
204
+ }
205
+ captured.push(batch);
206
+ return { ok: true, written: batch.events.length, skipped: [] };
207
+ },
208
+ };
209
+
210
+ const r1 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
211
+ expect(r1.eventsSent).toBe(0); // post threw → nothing committed
212
+ expect(captured).toHaveLength(0);
213
+
214
+ const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
215
+ expect(r2.eventsSent).toBe(1); // re-scanned from offset 0 and delivered
216
+ expect(captured.flatMap((b) => b.events.map((e) => e.skill))).toEqual(["deploy"]);
217
+
218
+ await fs.rm(tmp, { recursive: true, force: true });
219
+ });
220
+
221
+ it("picks up only newly-appended events on a later run (incremental offset)", async () => {
222
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
223
+ const projects = path.join(tmp, "projects");
224
+ const dir = path.join(projects, "-p");
225
+ await fs.mkdir(dir, { recursive: true });
226
+ const file = path.join(dir, "s.jsonl");
227
+ await fs.writeFile(
228
+ file,
229
+ 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",
230
+ "utf-8",
231
+ );
232
+ const cursorPath = path.join(tmp, "cursor.json");
233
+ const captured: SkillInvocationBatch[] = [];
234
+ const client = stubClient(captured);
235
+
236
+ await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
237
+
238
+ // New invocation appended after the first clean run.
239
+ await fs.appendFile(
240
+ file,
241
+ 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",
242
+ "utf-8",
243
+ );
244
+
245
+ const r2 = await collectAndSendSkillTelemetry({ client, machineId: "m", installerVersion: "t", claudeProjectsRoot: projects, cursorPath });
246
+ expect(r2.eventsSent).toBe(1);
247
+ // Only the appended event — not the original — is re-sent.
248
+ const lastBatch = captured[captured.length - 1];
249
+ expect(lastBatch.events.map((e) => e.skill)).toEqual(["land"]);
250
+
251
+ await fs.rm(tmp, { recursive: true, force: true });
252
+ });
253
+
254
+ it("captures every project when hqRoot is omitted", async () => {
255
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "skill-tel-"));
256
+ const projects = path.join(tmp, "projects");
257
+ const dir = path.join(projects, "-proj");
258
+ await fs.mkdir(dir, { recursive: true });
259
+ await fs.writeFile(
260
+ path.join(dir, "c.jsonl"),
261
+ 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",
262
+ "utf-8",
263
+ );
264
+
265
+ const captured: SkillInvocationBatch[] = [];
266
+ const result = await collectAndSendSkillTelemetry({
267
+ client: stubClient(captured),
268
+ machineId: "m-test",
269
+ installerVersion: "test",
270
+ claudeProjectsRoot: projects,
271
+ cursorPath: path.join(tmp, "cursor.json"),
272
+ });
273
+
274
+ expect(result.eventsSent).toBe(1);
275
+ expect(captured[0].events[0].skill).toBe("anywhere-skill");
276
+
277
+ await fs.rm(tmp, { recursive: true, force: true });
278
+ });
279
+ });