@indigoai-us/hq-cloud 6.15.17 → 6.15.19

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.
@@ -50,14 +50,17 @@ function setupEnv() {
50
50
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-telemetry-test-"));
51
51
  const claudeProjects = path.join(root, ".claude", "projects");
52
52
  const codexRoot = path.join(root, ".codex");
53
+ const grokRoot = path.join(root, ".grok");
53
54
  fs.mkdirSync(claudeProjects, { recursive: true });
54
55
  fs.mkdirSync(path.join(codexRoot, "sessions"), { recursive: true });
55
56
  fs.mkdirSync(path.join(codexRoot, "archived_sessions"), { recursive: true });
57
+ fs.mkdirSync(path.join(grokRoot, "sessions"), { recursive: true });
56
58
  fs.mkdirSync(path.join(root, ".hq"), { recursive: true });
57
59
  return {
58
60
  root,
59
61
  claudeProjects,
60
62
  codexRoot,
63
+ grokRoot,
61
64
  cursorPath: path.join(root, ".hq", "telemetry-cursor.json"),
62
65
  menubarPath: path.join(root, ".hq", "menubar.json"),
63
66
  };
@@ -82,6 +85,7 @@ function makeOpts(env, client) {
82
85
  installerVersion: "test-version",
83
86
  claudeProjectsRoot: env.claudeProjects,
84
87
  codexRoot: env.codexRoot,
88
+ grokRoot: env.grokRoot,
85
89
  maxScanBytesPerSource: Number.MAX_SAFE_INTEGER,
86
90
  maxBatchesPerRun: Number.MAX_SAFE_INTEGER,
87
91
  cursorPath: env.cursorPath,
@@ -745,6 +749,393 @@ describe("collectAndSendTelemetry — Codex rollouts", () => {
745
749
  expect(cursor.files[file]?.offset ?? 0).toBe(0);
746
750
  });
747
751
  });
752
+ // ── Grok session collection ──────────────────────────────────────────────────
753
+ function writeGrokSession(env, opts) {
754
+ const encodedCwd = opts.encodedCwd ?? encodeURIComponent("/Users/x/proj");
755
+ const dir = path.join(env.grokRoot, "sessions", encodedCwd, opts.sessionId);
756
+ fs.mkdirSync(dir, { recursive: true });
757
+ if (opts.summary) {
758
+ fs.writeFileSync(path.join(dir, "summary.json"), JSON.stringify(opts.summary), "utf-8");
759
+ }
760
+ const file = path.join(dir, "updates.jsonl");
761
+ fs.writeFileSync(file, opts.lines.map((line) => JSON.stringify(line)).join("\n") + "\n", "utf-8");
762
+ return file;
763
+ }
764
+ function grokTurnCompleted(opts) {
765
+ const update = {
766
+ sessionUpdate: "turn_completed",
767
+ prompt_id: opts.promptId,
768
+ stop_reason: "end_turn",
769
+ usage: opts.usage,
770
+ ...opts.extraUpdate,
771
+ };
772
+ const params = {
773
+ sessionId: opts.sessionId,
774
+ update,
775
+ };
776
+ if (opts.eventId) {
777
+ params._meta = { eventId: opts.eventId };
778
+ }
779
+ return {
780
+ timestamp: opts.timestamp ?? 1_786_507_623,
781
+ method: opts.method ?? "_x.ai/session/update",
782
+ params,
783
+ };
784
+ }
785
+ describe("collectAndSendTelemetry — Grok sessions", () => {
786
+ let env;
787
+ beforeEach(() => {
788
+ env = setupEnv();
789
+ });
790
+ afterEach(() => teardownEnv(env));
791
+ it("collects turn_completed usage with reasoning folded into outputTokens", async () => {
792
+ const sessionId = "grok-session-1";
793
+ writeGrokSession(env, {
794
+ sessionId,
795
+ summary: {
796
+ info: { id: sessionId, cwd: "/Users/x/proj" },
797
+ current_model_id: "grok-4.5-build",
798
+ head_branch: "feat/grok-usage",
799
+ },
800
+ lines: [
801
+ {
802
+ timestamp: 1_786_507_620,
803
+ method: "session/update",
804
+ params: {
805
+ sessionId,
806
+ update: {
807
+ sessionUpdate: "agent_message_chunk",
808
+ content: { type: "text", text: "secret prompt body" },
809
+ _meta: { totalTokens: 999_999 },
810
+ },
811
+ },
812
+ },
813
+ grokTurnCompleted({
814
+ sessionId,
815
+ promptId: "prompt-1",
816
+ eventId: "evt-turn-1",
817
+ usage: {
818
+ inputTokens: 2552850,
819
+ outputTokens: 29428,
820
+ cachedReadTokens: 2414208,
821
+ cacheCreationTokens: 0,
822
+ reasoningTokens: 14654,
823
+ modelUsage: {
824
+ "grok-4.5-build": {
825
+ inputTokens: 2552850,
826
+ outputTokens: 29428,
827
+ cachedReadTokens: 2414208,
828
+ cacheCreationTokens: 0,
829
+ reasoningTokens: 14654,
830
+ },
831
+ },
832
+ },
833
+ }),
834
+ ],
835
+ });
836
+ const client = makeClient();
837
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
838
+ const events = client.posts.flatMap((post) => post.events);
839
+ expect(result.filesScanned).toBe(1);
840
+ expect(result.eventsSent).toBe(1);
841
+ expect(events).toEqual([
842
+ {
843
+ sessionId,
844
+ timestamp: new Date(1_786_507_623 * 1000).toISOString(),
845
+ uuid: "evt-turn-1",
846
+ cwd: "/Users/x/proj",
847
+ gitBranch: "feat/grok-usage",
848
+ model: "grok-4.5-build",
849
+ inputTokens: 2552850,
850
+ outputTokens: 29428 + 14654,
851
+ cacheReadInputTokens: 2414208,
852
+ cacheCreationInputTokens: 0,
853
+ },
854
+ ]);
855
+ expect(JSON.stringify(events)).not.toContain("secret prompt body");
856
+ expect(JSON.stringify(events)).not.toContain("999999");
857
+ });
858
+ it("advances the cursor incrementally and only posts new turn_completed rows", async () => {
859
+ const sessionId = "grok-incremental";
860
+ const file = writeGrokSession(env, {
861
+ sessionId,
862
+ summary: {
863
+ info: { cwd: "/repo" },
864
+ current_model_id: "grok-4.5-build",
865
+ },
866
+ lines: [
867
+ grokTurnCompleted({
868
+ sessionId,
869
+ promptId: "p1",
870
+ eventId: "evt-1",
871
+ usage: {
872
+ inputTokens: 10,
873
+ outputTokens: 2,
874
+ reasoningTokens: 1,
875
+ cachedReadTokens: 0,
876
+ cacheCreationTokens: 0,
877
+ modelUsage: {
878
+ "grok-4.5-build": {
879
+ inputTokens: 10,
880
+ outputTokens: 2,
881
+ reasoningTokens: 1,
882
+ cachedReadTokens: 0,
883
+ cacheCreationTokens: 0,
884
+ },
885
+ },
886
+ },
887
+ }),
888
+ ],
889
+ });
890
+ const client = makeClient();
891
+ await collectAndSendTelemetry(makeOpts(env, client));
892
+ expect(client.posts.flatMap((post) => post.events)).toHaveLength(1);
893
+ const offsetAfterFirst = readCursor(env).files[file].offset;
894
+ await collectAndSendTelemetry(makeOpts(env, client));
895
+ expect(client.posts.flatMap((post) => post.events)).toHaveLength(1);
896
+ expect(readCursor(env).files[file].offset).toBe(offsetAfterFirst);
897
+ fs.appendFileSync(file, JSON.stringify(grokTurnCompleted({
898
+ timestamp: 1_786_507_700,
899
+ sessionId,
900
+ promptId: "p2",
901
+ eventId: "evt-2",
902
+ usage: {
903
+ inputTokens: 20,
904
+ outputTokens: 4,
905
+ reasoningTokens: 0,
906
+ cachedReadTokens: 5,
907
+ cacheCreationTokens: 1,
908
+ modelUsage: {
909
+ "grok-4.5-build": {
910
+ inputTokens: 20,
911
+ outputTokens: 4,
912
+ reasoningTokens: 0,
913
+ cachedReadTokens: 5,
914
+ cacheCreationTokens: 1,
915
+ },
916
+ },
917
+ },
918
+ })) + "\n");
919
+ await collectAndSendTelemetry(makeOpts(env, client));
920
+ const events = client.posts.flatMap((post) => post.events);
921
+ expect(events).toHaveLength(2);
922
+ expect(events[1]).toMatchObject({
923
+ uuid: "evt-2",
924
+ inputTokens: 20,
925
+ outputTokens: 4,
926
+ cacheReadInputTokens: 5,
927
+ cacheCreationInputTokens: 1,
928
+ });
929
+ });
930
+ it("opt-out with no company skips the grok scan entirely", async () => {
931
+ writeGrokSession(env, {
932
+ sessionId: "grok-optout",
933
+ summary: { info: { cwd: "/repo" }, current_model_id: "grok-4.5-build" },
934
+ lines: [
935
+ grokTurnCompleted({
936
+ sessionId: "grok-optout",
937
+ promptId: "p1",
938
+ eventId: "evt-optout",
939
+ usage: {
940
+ inputTokens: 1,
941
+ outputTokens: 1,
942
+ reasoningTokens: 0,
943
+ modelUsage: {
944
+ "grok-4.5-build": {
945
+ inputTokens: 1,
946
+ outputTokens: 1,
947
+ reasoningTokens: 0,
948
+ },
949
+ },
950
+ },
951
+ }),
952
+ ],
953
+ });
954
+ const client = makeClient({
955
+ optInResponse: { enabled: false, updatedAt: null },
956
+ });
957
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
958
+ expect(result.enabled).toBe(false);
959
+ expect(result.filesScanned).toBe(0);
960
+ expect(client.posts).toHaveLength(0);
961
+ expect(fs.existsSync(env.cursorPath)).toBe(false);
962
+ });
963
+ it("ignores streaming _meta.totalTokens chunks but still advances the cursor", async () => {
964
+ const sessionId = "grok-stream-only";
965
+ const file = writeGrokSession(env, {
966
+ sessionId,
967
+ summary: { info: { cwd: "/repo" }, current_model_id: "grok-4.5-build" },
968
+ lines: [
969
+ {
970
+ timestamp: 1_786_507_600,
971
+ method: "session/update",
972
+ params: {
973
+ sessionId,
974
+ update: {
975
+ sessionUpdate: "agent_thought_chunk",
976
+ content: { type: "text", text: "thinking" },
977
+ _meta: { totalTokens: 20447, eventId: "chunk-1" },
978
+ },
979
+ },
980
+ },
981
+ {
982
+ timestamp: 1_786_507_601,
983
+ method: "_x.ai/session/update",
984
+ params: {
985
+ sessionId,
986
+ update: {
987
+ sessionUpdate: "tool_call",
988
+ title: "read_file",
989
+ _meta: { totalTokens: 31430 },
990
+ },
991
+ },
992
+ },
993
+ ],
994
+ });
995
+ const client = makeClient();
996
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
997
+ expect(result.eventsSent).toBe(0);
998
+ expect(client.posts).toHaveLength(0);
999
+ expect(readCursor(env).files[file].offset).toBe(fs.statSync(file).size);
1000
+ });
1001
+ it("missing ~/.grok is a silent success", async () => {
1002
+ fs.rmSync(env.grokRoot, { recursive: true, force: true });
1003
+ const client = makeClient();
1004
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
1005
+ expect(result.filesScanned).toBe(0);
1006
+ expect(result.eventsSent).toBe(0);
1007
+ expect(client.posts).toHaveLength(0);
1008
+ });
1009
+ it("emits one event per model in modelUsage", async () => {
1010
+ const sessionId = "grok-multi";
1011
+ writeGrokSession(env, {
1012
+ sessionId,
1013
+ summary: { info: { cwd: "/repo" }, current_model_id: "grok-4.5-build" },
1014
+ lines: [
1015
+ grokTurnCompleted({
1016
+ sessionId,
1017
+ promptId: "multi-prompt",
1018
+ eventId: "evt-multi",
1019
+ usage: {
1020
+ inputTokens: 100,
1021
+ outputTokens: 10,
1022
+ reasoningTokens: 5,
1023
+ cachedReadTokens: 20,
1024
+ cacheCreationTokens: 0,
1025
+ modelUsage: {
1026
+ "grok-4.5-build": {
1027
+ inputTokens: 60,
1028
+ outputTokens: 6,
1029
+ reasoningTokens: 3,
1030
+ cachedReadTokens: 10,
1031
+ cacheCreationTokens: 0,
1032
+ },
1033
+ "grok-code-fast-1": {
1034
+ inputTokens: 40,
1035
+ outputTokens: 4,
1036
+ reasoningTokens: 2,
1037
+ cachedReadTokens: 10,
1038
+ cacheCreationTokens: 0,
1039
+ },
1040
+ },
1041
+ },
1042
+ }),
1043
+ ],
1044
+ });
1045
+ const client = makeClient();
1046
+ await collectAndSendTelemetry(makeOpts(env, client));
1047
+ const events = client.posts.flatMap((post) => post.events);
1048
+ expect(events).toHaveLength(2);
1049
+ expect(events).toEqual(expect.arrayContaining([
1050
+ expect.objectContaining({
1051
+ model: "grok-4.5-build",
1052
+ inputTokens: 60,
1053
+ outputTokens: 9,
1054
+ cacheReadInputTokens: 10,
1055
+ }),
1056
+ expect.objectContaining({
1057
+ model: "grok-code-fast-1",
1058
+ inputTokens: 40,
1059
+ outputTokens: 6,
1060
+ cacheReadInputTokens: 10,
1061
+ }),
1062
+ ]));
1063
+ // Multi-model rows must not share a single eventId (would collapse on ingest).
1064
+ expect(events[0].uuid).not.toBe(events[1].uuid);
1065
+ expect(String(events[0].uuid)).toMatch(/^grok-[a-f0-9]{64}$/);
1066
+ expect(String(events[1].uuid)).toMatch(/^grok-[a-f0-9]{64}$/);
1067
+ });
1068
+ it("never POSTs raw prompt or tool text from grok updates", async () => {
1069
+ const sessionId = "grok-redact";
1070
+ writeGrokSession(env, {
1071
+ sessionId,
1072
+ summary: {
1073
+ info: { cwd: "/repo" },
1074
+ current_model_id: "grok-4.5-build",
1075
+ session_summary: "private summary text",
1076
+ },
1077
+ lines: [
1078
+ {
1079
+ timestamp: 1_786_507_610,
1080
+ method: "session/update",
1081
+ params: {
1082
+ sessionId,
1083
+ update: {
1084
+ sessionUpdate: "user_message_chunk",
1085
+ content: {
1086
+ type: "text",
1087
+ text: "SUPER_SECRET_USER_PROMPT_XYZ",
1088
+ },
1089
+ },
1090
+ },
1091
+ },
1092
+ {
1093
+ timestamp: 1_786_507_611,
1094
+ method: "session/update",
1095
+ params: {
1096
+ sessionId,
1097
+ update: {
1098
+ sessionUpdate: "tool_call",
1099
+ title: "read_file",
1100
+ rawInput: { target_file: "/secret/path.ts" },
1101
+ },
1102
+ },
1103
+ },
1104
+ grokTurnCompleted({
1105
+ method: "session/update",
1106
+ sessionId,
1107
+ promptId: "redact-prompt",
1108
+ eventId: "evt-redact",
1109
+ usage: {
1110
+ inputTokens: 3,
1111
+ outputTokens: 1,
1112
+ reasoningTokens: 1,
1113
+ cachedReadTokens: 0,
1114
+ cacheCreationTokens: 0,
1115
+ modelUsage: {
1116
+ "grok-4.5-build": {
1117
+ inputTokens: 3,
1118
+ outputTokens: 1,
1119
+ reasoningTokens: 1,
1120
+ cachedReadTokens: 0,
1121
+ cacheCreationTokens: 0,
1122
+ },
1123
+ },
1124
+ },
1125
+ }),
1126
+ ],
1127
+ });
1128
+ const client = makeClient();
1129
+ await collectAndSendTelemetry(makeOpts(env, client));
1130
+ const wire = JSON.stringify(client.posts);
1131
+ expect(client.posts.flatMap((post) => post.events)).toHaveLength(1);
1132
+ expect(wire).not.toContain("SUPER_SECRET_USER_PROMPT_XYZ");
1133
+ expect(wire).not.toContain("/secret/path.ts");
1134
+ expect(wire).not.toContain("private summary text");
1135
+ expect(wire).not.toContain("rawInput");
1136
+ expect(wire).not.toContain("user_message_chunk");
1137
+ });
1138
+ });
748
1139
  // ── companyUid edge attribution (US-002) ────────────────────────────────────────
749
1140
  describe("collectAndSendTelemetry — companyUid attribution", () => {
750
1141
  const COMPANY_UID = "cmp_01INDIGOTEST";