workos 0.15.2 → 0.17.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 (134) hide show
  1. package/README.md +44 -14
  2. package/dist/bin.js +1449 -1261
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.config.d.ts +1 -0
  5. package/dist/cli.config.js +1 -0
  6. package/dist/cli.config.js.map +1 -1
  7. package/dist/commands/api/index.js +7 -2
  8. package/dist/commands/api/index.js.map +1 -1
  9. package/dist/commands/api/interactive.js +9 -3
  10. package/dist/commands/api/interactive.js.map +1 -1
  11. package/dist/commands/debug.d.ts +2 -1
  12. package/dist/commands/debug.js +43 -3
  13. package/dist/commands/debug.js.map +1 -1
  14. package/dist/commands/dev.js +5 -4
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.js +13 -4
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/emulate.js +2 -2
  19. package/dist/commands/emulate.js.map +1 -1
  20. package/dist/commands/env.js +5 -4
  21. package/dist/commands/env.js.map +1 -1
  22. package/dist/commands/install-skill.js +4 -3
  23. package/dist/commands/install-skill.js.map +1 -1
  24. package/dist/commands/install.js +2 -2
  25. package/dist/commands/install.js.map +1 -1
  26. package/dist/commands/login.js +3 -3
  27. package/dist/commands/login.js.map +1 -1
  28. package/dist/commands/migrations.d.ts +1 -1
  29. package/dist/commands/migrations.js +4 -1
  30. package/dist/commands/migrations.js.map +1 -1
  31. package/dist/commands/telemetry.d.ts +3 -0
  32. package/dist/commands/telemetry.js +88 -0
  33. package/dist/commands/telemetry.js.map +1 -0
  34. package/dist/commands/uninstall-skill.js +3 -2
  35. package/dist/commands/uninstall-skill.js.map +1 -1
  36. package/dist/commands/vault-run.d.ts +13 -0
  37. package/dist/commands/vault-run.js +194 -0
  38. package/dist/commands/vault-run.js.map +1 -0
  39. package/dist/commands/vault.d.ts +3 -2
  40. package/dist/commands/vault.js +41 -8
  41. package/dist/commands/vault.js.map +1 -1
  42. package/dist/lib/adapters/cli-adapter.d.ts +7 -0
  43. package/dist/lib/adapters/cli-adapter.js +49 -0
  44. package/dist/lib/adapters/cli-adapter.js.map +1 -1
  45. package/dist/lib/adapters/dashboard-adapter.d.ts +4 -0
  46. package/dist/lib/adapters/dashboard-adapter.js +24 -0
  47. package/dist/lib/adapters/dashboard-adapter.js.map +1 -1
  48. package/dist/lib/adapters/headless-adapter.d.ts +6 -0
  49. package/dist/lib/adapters/headless-adapter.js +26 -1
  50. package/dist/lib/adapters/headless-adapter.js.map +1 -1
  51. package/dist/lib/api-error-handler.d.ts +15 -3
  52. package/dist/lib/api-error-handler.js +52 -34
  53. package/dist/lib/api-error-handler.js.map +1 -1
  54. package/dist/lib/command-aliases.d.ts +8 -0
  55. package/dist/lib/command-aliases.js +12 -0
  56. package/dist/lib/command-aliases.js.map +1 -0
  57. package/dist/lib/constants.d.ts +0 -1
  58. package/dist/lib/constants.js +0 -1
  59. package/dist/lib/constants.js.map +1 -1
  60. package/dist/lib/device-id.d.ts +25 -0
  61. package/dist/lib/device-id.js +102 -0
  62. package/dist/lib/device-id.js.map +1 -0
  63. package/dist/lib/events.d.ts +15 -0
  64. package/dist/lib/events.js.map +1 -1
  65. package/dist/lib/installer-core.d.ts +61 -1
  66. package/dist/lib/installer-core.js +132 -6
  67. package/dist/lib/installer-core.js.map +1 -1
  68. package/dist/lib/installer-core.types.d.ts +24 -0
  69. package/dist/lib/installer-core.types.js.map +1 -1
  70. package/dist/lib/preferences.d.ts +101 -0
  71. package/dist/lib/preferences.js +198 -0
  72. package/dist/lib/preferences.js.map +1 -0
  73. package/dist/lib/run-with-core.js +40 -15
  74. package/dist/lib/run-with-core.js.map +1 -1
  75. package/dist/lib/scaffold/index.d.ts +1 -0
  76. package/dist/lib/scaffold/index.js +2 -0
  77. package/dist/lib/scaffold/index.js.map +1 -0
  78. package/dist/lib/scaffold/scaffold.d.ts +66 -0
  79. package/dist/lib/scaffold/scaffold.js +156 -0
  80. package/dist/lib/scaffold/scaffold.js.map +1 -0
  81. package/dist/lib/settings.d.ts +6 -0
  82. package/dist/lib/settings.js +7 -0
  83. package/dist/lib/settings.js.map +1 -1
  84. package/dist/lib/telemetry-notice.d.ts +25 -0
  85. package/dist/lib/telemetry-notice.js +56 -0
  86. package/dist/lib/telemetry-notice.js.map +1 -0
  87. package/dist/run.d.ts +2 -2
  88. package/dist/run.js +2 -1
  89. package/dist/run.js.map +1 -1
  90. package/dist/test/force-insecure-storage.d.ts +1 -0
  91. package/dist/test/force-insecure-storage.js +9 -0
  92. package/dist/test/force-insecure-storage.js.map +1 -0
  93. package/dist/test/setup.d.ts +1 -0
  94. package/dist/test/setup.js +38 -0
  95. package/dist/test/setup.js.map +1 -0
  96. package/dist/utils/analytics.d.ts +41 -0
  97. package/dist/utils/analytics.js +199 -12
  98. package/dist/utils/analytics.js.map +1 -1
  99. package/dist/utils/box.d.ts +29 -1
  100. package/dist/utils/box.js +92 -4
  101. package/dist/utils/box.js.map +1 -1
  102. package/dist/utils/cli-exit.d.ts +15 -0
  103. package/dist/utils/cli-exit.js +11 -0
  104. package/dist/utils/cli-exit.js.map +1 -0
  105. package/dist/utils/cli-symbols.d.ts +1 -1
  106. package/dist/utils/command-telemetry.d.ts +17 -0
  107. package/dist/utils/command-telemetry.js +67 -0
  108. package/dist/utils/command-telemetry.js.map +1 -0
  109. package/dist/utils/crash-reporter.d.ts +13 -0
  110. package/dist/utils/crash-reporter.js +91 -0
  111. package/dist/utils/crash-reporter.js.map +1 -0
  112. package/dist/utils/debug.d.ts +1 -0
  113. package/dist/utils/debug.js +4 -1
  114. package/dist/utils/debug.js.map +1 -1
  115. package/dist/utils/exit-codes.d.ts +5 -0
  116. package/dist/utils/exit-codes.js +30 -1
  117. package/dist/utils/exit-codes.js.map +1 -1
  118. package/dist/utils/help-json.d.ts +6 -0
  119. package/dist/utils/help-json.js +87 -10
  120. package/dist/utils/help-json.js.map +1 -1
  121. package/dist/utils/output.d.ts +7 -2
  122. package/dist/utils/output.js +9 -2
  123. package/dist/utils/output.js.map +1 -1
  124. package/dist/utils/telemetry-client.d.ts +30 -2
  125. package/dist/utils/telemetry-client.js +122 -12
  126. package/dist/utils/telemetry-client.js.map +1 -1
  127. package/dist/utils/telemetry-store-forward.d.ts +11 -0
  128. package/dist/utils/telemetry-store-forward.js +94 -0
  129. package/dist/utils/telemetry-store-forward.js.map +1 -0
  130. package/dist/utils/telemetry-types.d.ts +58 -9
  131. package/dist/utils/telemetry-types.js.map +1 -1
  132. package/dist/utils/types.d.ts +10 -4
  133. package/dist/utils/types.js.map +1 -1
  134. package/package.json +1 -1
@@ -1,12 +1,36 @@
1
- import { debug } from './debug.js';
2
- import { getCredentials } from '../lib/credentials.js';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { debug, isDebugEnabled } from './debug.js';
4
+ import { getCredentials, isTokenExpired } from '../lib/credentials.js';
5
+ function summarizeEvent(event) {
6
+ switch (event.type) {
7
+ case 'session.start':
8
+ return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;
9
+ case 'session.end':
10
+ return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;
11
+ case 'step':
12
+ return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;
13
+ case 'agent.tool':
14
+ return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;
15
+ case 'agent.llm':
16
+ return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;
17
+ case 'command':
18
+ return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;
19
+ case 'crash':
20
+ return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;
21
+ }
22
+ }
3
23
  /**
4
- * HTTP client that queues telemetry events and flushes them to the gateway.
24
+ * HTTP client that queues telemetry events and flushes them to the API.
5
25
  * Failures are silent—telemetry should never crash the wizard.
6
26
  */
7
27
  export class TelemetryClient {
8
28
  events = [];
29
+ flushInFlight = null;
9
30
  accessToken = null;
31
+ claimToken = null;
32
+ clientId = null;
33
+ apiKey = null;
10
34
  gatewayUrl = null;
11
35
  setGatewayUrl(url) {
12
36
  this.gatewayUrl = url;
@@ -14,48 +38,134 @@ export class TelemetryClient {
14
38
  setAccessToken(token) {
15
39
  this.accessToken = token;
16
40
  }
41
+ setApiKeyAuth(apiKey) {
42
+ this.apiKey = apiKey;
43
+ }
44
+ /**
45
+ * Set claim-token auth for unclaimed environments.
46
+ * The API accepts either a JWT (Bearer), claim token
47
+ * (x-workos-claim-token + x-workos-client-id), or API key
48
+ * (x-workos-api-key).
49
+ */
50
+ setClaimTokenAuth(clientId, claimToken) {
51
+ this.clientId = clientId;
52
+ this.claimToken = claimToken;
53
+ }
17
54
  queueEvent(event) {
18
55
  this.events.push(event);
19
56
  }
57
+ /**
58
+ * Queue multiple pre-formed events (used by store-forward recovery).
59
+ */
60
+ queueEvents(events) {
61
+ this.events.push(...events);
62
+ }
63
+ /**
64
+ * Flush queued events. Returns true if events were sent or intentionally
65
+ * dropped (4xx), false if they should be retried (5xx/network error).
66
+ * Uses splice to only remove the events that were in the snapshot,
67
+ * protecting any events queued concurrently during the fetch.
68
+ */
20
69
  async flush() {
70
+ // Coalesce overlapping flushes: a second caller during an in-flight flush
71
+ // would otherwise snapshot and POST the same events again (duplicate send),
72
+ // and its splice() could drop events queued after the first flush started.
73
+ if (this.flushInFlight)
74
+ return this.flushInFlight;
75
+ this.flushInFlight = this.flushInternal();
76
+ try {
77
+ return await this.flushInFlight;
78
+ }
79
+ finally {
80
+ this.flushInFlight = null;
81
+ }
82
+ }
83
+ async flushInternal() {
21
84
  if (this.events.length === 0)
22
- return;
85
+ return true;
23
86
  if (!this.gatewayUrl) {
24
- debug('[Telemetry] No gateway URL configured, skipping flush');
25
- return;
87
+ debug('[Telemetry] No telemetry URL configured, skipping flush');
88
+ return false;
26
89
  }
27
- const payload = { events: [...this.events] };
28
- this.events = [];
90
+ const count = this.events.length;
91
+ const payload = { events: this.events.slice(0, count) };
29
92
  const headers = {
30
93
  'Content-Type': 'application/json',
31
94
  };
32
- // Read fresh credentials to handle token refresh mid-session
95
+ // Read fresh credentials to handle token refresh mid-session. Skip an
96
+ // expired stored token — sending a dead Bearer 401s and the event is
97
+ // dropped, so fall through to claim-token / api-key auth instead.
33
98
  const freshCreds = getCredentials();
34
- const token = freshCreds?.accessToken ?? this.accessToken;
99
+ const token = freshCreds?.accessToken
100
+ ? isTokenExpired(freshCreds)
101
+ ? null
102
+ : freshCreds.accessToken
103
+ : this.accessToken;
35
104
  if (token) {
36
105
  headers['Authorization'] = `Bearer ${token}`;
37
106
  }
107
+ else if (this.claimToken && this.clientId) {
108
+ // Unclaimed environment auth path — guard accepts this instead of JWT
109
+ headers['x-workos-claim-token'] = this.claimToken;
110
+ headers['x-workos-client-id'] = this.clientId;
111
+ }
112
+ else if (this.apiKey) {
113
+ headers['x-workos-api-key'] = this.apiKey;
114
+ }
38
115
  const controller = new AbortController();
39
116
  const timeout = setTimeout(() => controller.abort(), 3000);
40
117
  try {
41
- debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry`);
118
+ if (isDebugEnabled()) {
119
+ const eventSummary = payload.events.map(summarizeEvent).join('\n ');
120
+ debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\n ${eventSummary}`);
121
+ }
42
122
  const response = await fetch(`${this.gatewayUrl}/telemetry`, {
43
123
  method: 'POST',
44
124
  headers,
45
125
  body: JSON.stringify(payload),
46
126
  signal: controller.signal,
47
127
  });
48
- if (!response.ok) {
128
+ if (response.ok) {
129
+ this.events.splice(0, count);
130
+ return true;
131
+ }
132
+ else {
49
133
  debug(`[Telemetry] Failed to send: ${response.status}`);
134
+ // Drop on 4xx (permanent failures like 401/403 won't succeed on retry).
135
+ // Retain on 5xx (transient server errors) for store-forward.
136
+ if (response.status >= 400 && response.status < 500) {
137
+ this.events.splice(0, count);
138
+ return true; // intentionally dropped
139
+ }
140
+ return false;
50
141
  }
51
142
  }
52
143
  catch (error) {
53
144
  debug(`[Telemetry] Error sending events: ${error}`);
145
+ // Events remain in queue for store-forward to persist
146
+ return false;
54
147
  }
55
148
  finally {
56
149
  clearTimeout(timeout);
57
150
  }
58
151
  }
152
+ /**
153
+ * Synchronously write pending events to a file.
154
+ * Used as last resort in process.on('exit') handler.
155
+ */
156
+ persistToFile(filePath) {
157
+ if (this.events.length === 0)
158
+ return;
159
+ try {
160
+ // Restrictive modes — the payload carries device/user identifiers.
161
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
162
+ writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });
163
+ this.events = [];
164
+ }
165
+ catch {
166
+ // Silent failure — telemetry must never block exit
167
+ }
168
+ }
59
169
  }
60
170
  export const telemetryClient = new TelemetryClient();
61
171
  //# sourceMappingURL=telemetry-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,MAAM,OAAO,eAAe;IAClB,MAAM,GAAqB,EAAE,CAAC;IAC9B,WAAW,GAAkB,IAAI,CAAC;IAClC,UAAU,GAAkB,IAAI,CAAC;IAEzC,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;IAED,UAAU,CAAC,KAAqB;QAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,KAAK,CAAC,uDAAuD,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QACF,6DAA6D;QAC7D,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,UAAU,EAAE,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;QAC/C,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAE3D,IAAI,CAAC;YACH,KAAK,CAAC,uBAAuB,OAAO,CAAC,MAAM,CAAC,MAAM,cAAc,IAAI,CAAC,UAAU,YAAY,CAAC,CAAC;YAE7F,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,UAAU,YAAY,EAAE;gBAC3D,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC7B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,qCAAqC,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC","sourcesContent":["import { debug } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials } from '../lib/credentials.js';\n\n/**\n * HTTP client that queues telemetry events and flushes them to the gateway.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private accessToken: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n async flush(): Promise<void> {\n if (this.events.length === 0) return;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No gateway URL configured, skipping flush');\n return;\n }\n\n const payload: TelemetryRequest = { events: [...this.events] };\n this.events = [];\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken ?? this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry`);\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n } finally {\n clearTimeout(timeout);\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
1
+ {"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvE,SAAS,cAAc,CAAC,KAAqB;IAC3C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,eAAe;YAClB,OAAO,sBAAsB,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,QAAQ,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC;QACvG,KAAK,aAAa;YAChB,OAAO,uBAAuB,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,cAAc,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,KAAK,CAAC;QAClI,KAAK,MAAM;YACT,OAAO,QAAQ,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU,eAAe,KAAK,CAAC,OAAO,GAAG,CAAC;QAChF,KAAK,YAAY;YACf,OAAO,cAAc,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,UAAU,KAAK,CAAC;QAChE,KAAK,WAAW;YACd,OAAO,aAAa,KAAK,CAAC,KAAK,QAAQ,KAAK,CAAC,WAAW,SAAS,KAAK,CAAC,YAAY,GAAG,CAAC;QACzF,KAAK,SAAS;YACZ,OAAO,WAAW,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,qBAAqB,CAAC,eAAe,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC;QACtJ,KAAK,OAAO;YACV,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,kBAAkB,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,qBAAqB,CAAC,GAAG,CAAC;IACxG,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,eAAe;IAClB,MAAM,GAAqB,EAAE,CAAC;IAC9B,aAAa,GAA4B,IAAI,CAAC;IAC9C,WAAW,GAAkB,IAAI,CAAC;IAClC,UAAU,GAAkB,IAAI,CAAC;IACjC,QAAQ,GAAkB,IAAI,CAAC;IAC/B,MAAM,GAAkB,IAAI,CAAC;IAC7B,UAAU,GAAkB,IAAI,CAAC;IAEzC,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;IAED,aAAa,CAAC,MAAc;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,QAAgB,EAAE,UAAkB;QACpD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,UAAU,CAAC,KAAqB;QAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,MAAwB;QAClC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,0EAA0E;QAC1E,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC,aAAa,CAAC;QAClD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,aAAa,CAAC;QAClC,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,KAAK,CAAC,yDAAyD,CAAC,CAAC;YACjE,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QACjC,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QAE1E,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QACF,sEAAsE;QACtE,qEAAqE;QACrE,kEAAkE;QAClE,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,UAAU,EAAE,WAAW;YACnC,CAAC,CAAC,cAAc,CAAC,UAAU,CAAC;gBAC1B,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,UAAU,CAAC,WAAW;YAC1B,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;QACrB,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;QAC/C,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5C,sEAAsE;YACtE,OAAO,CAAC,sBAAsB,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC;YAClD,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QAChD,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5C,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAE3D,IAAI,CAAC;YACH,IAAI,cAAc,EAAE,EAAE,CAAC;gBACrB,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACrE,KAAK,CACH,uBAAuB,OAAO,CAAC,MAAM,CAAC,MAAM,cAAc,IAAI,CAAC,UAAU,kBAAkB,YAAY,EAAE,CAC1G,CAAC;YACJ,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,UAAU,YAAY,EAAE;gBAC3D,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC7B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;gBACxD,wEAAwE;gBACxE,6DAA6D;gBAC7D,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;oBACpD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;oBAC7B,OAAO,IAAI,CAAC,CAAC,wBAAwB;gBACvC,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,qCAAqC,KAAK,EAAE,CAAC,CAAC;YACpD,sDAAsD;YACtD,OAAO,KAAK,CAAC;QACf,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,QAAgB;QAC5B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrC,IAAI,CAAC;YACH,mEAAmE;YACnE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC/D,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACzF,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,mDAAmD;QACrD,CAAC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { debug, isDebugEnabled } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials, isTokenExpired } from '../lib/credentials.js';\n\nfunction summarizeEvent(event: TelemetryEvent): string {\n switch (event.type) {\n case 'session.start':\n return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;\n case 'session.end':\n return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;\n case 'step':\n return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;\n case 'agent.tool':\n return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;\n case 'agent.llm':\n return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;\n case 'command':\n return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;\n case 'crash':\n return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;\n }\n}\n\n/**\n * HTTP client that queues telemetry events and flushes them to the API.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private flushInFlight: Promise<boolean> | null = null;\n private accessToken: string | null = null;\n private claimToken: string | null = null;\n private clientId: string | null = null;\n private apiKey: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n setApiKeyAuth(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Set claim-token auth for unclaimed environments.\n * The API accepts either a JWT (Bearer), claim token\n * (x-workos-claim-token + x-workos-client-id), or API key\n * (x-workos-api-key).\n */\n setClaimTokenAuth(clientId: string, claimToken: string) {\n this.clientId = clientId;\n this.claimToken = claimToken;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n /**\n * Queue multiple pre-formed events (used by store-forward recovery).\n */\n queueEvents(events: TelemetryEvent[]): void {\n this.events.push(...events);\n }\n\n /**\n * Flush queued events. Returns true if events were sent or intentionally\n * dropped (4xx), false if they should be retried (5xx/network error).\n * Uses splice to only remove the events that were in the snapshot,\n * protecting any events queued concurrently during the fetch.\n */\n async flush(): Promise<boolean> {\n // Coalesce overlapping flushes: a second caller during an in-flight flush\n // would otherwise snapshot and POST the same events again (duplicate send),\n // and its splice() could drop events queued after the first flush started.\n if (this.flushInFlight) return this.flushInFlight;\n this.flushInFlight = this.flushInternal();\n try {\n return await this.flushInFlight;\n } finally {\n this.flushInFlight = null;\n }\n }\n\n private async flushInternal(): Promise<boolean> {\n if (this.events.length === 0) return true;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No telemetry URL configured, skipping flush');\n return false;\n }\n\n const count = this.events.length;\n const payload: TelemetryRequest = { events: this.events.slice(0, count) };\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session. Skip an\n // expired stored token — sending a dead Bearer 401s and the event is\n // dropped, so fall through to claim-token / api-key auth instead.\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken\n ? isTokenExpired(freshCreds)\n ? null\n : freshCreds.accessToken\n : this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n } else if (this.claimToken && this.clientId) {\n // Unclaimed environment auth path — guard accepts this instead of JWT\n headers['x-workos-claim-token'] = this.claimToken;\n headers['x-workos-client-id'] = this.clientId;\n } else if (this.apiKey) {\n headers['x-workos-api-key'] = this.apiKey;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n if (isDebugEnabled()) {\n const eventSummary = payload.events.map(summarizeEvent).join('\\n ');\n debug(\n `[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\\n ${eventSummary}`,\n );\n }\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (response.ok) {\n this.events.splice(0, count);\n return true;\n } else {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n // Drop on 4xx (permanent failures like 401/403 won't succeed on retry).\n // Retain on 5xx (transient server errors) for store-forward.\n if (response.status >= 400 && response.status < 500) {\n this.events.splice(0, count);\n return true; // intentionally dropped\n }\n return false;\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n // Events remain in queue for store-forward to persist\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /**\n * Synchronously write pending events to a file.\n * Used as last resort in process.on('exit') handler.\n */\n persistToFile(filePath: string): void {\n if (this.events.length === 0) return;\n try {\n // Restrictive modes — the payload carries device/user identifiers.\n mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });\n writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });\n this.events = [];\n } catch {\n // Silent failure — telemetry must never block exit\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Register a sync exit handler that persists unsent events to disk.
3
+ * Called once at startup. Uses PID in filename to prevent concurrent
4
+ * CLI invocations from colliding.
5
+ */
6
+ export declare function installStoreForward(): void;
7
+ /**
8
+ * On startup, check for ANY pending files from previous invocations
9
+ * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
10
+ */
11
+ export declare function recoverPendingEvents(): Promise<void>;
@@ -0,0 +1,94 @@
1
+ import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { telemetryClient } from './telemetry-client.js';
5
+ import { debug } from './debug.js';
6
+ const PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');
7
+ const PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);
8
+ const MAX_PENDING_FILES = 100;
9
+ const MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;
10
+ function safeUnlink(filePath) {
11
+ try {
12
+ unlinkSync(filePath);
13
+ }
14
+ catch {
15
+ /* ignore */
16
+ }
17
+ }
18
+ /**
19
+ * Register a sync exit handler that persists unsent events to disk.
20
+ * Called once at startup. Uses PID in filename to prevent concurrent
21
+ * CLI invocations from colliding.
22
+ */
23
+ export function installStoreForward() {
24
+ process.on('exit', () => {
25
+ telemetryClient.persistToFile(PENDING_FILE);
26
+ });
27
+ }
28
+ /**
29
+ * On startup, check for ANY pending files from previous invocations
30
+ * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
31
+ */
32
+ export async function recoverPendingEvents() {
33
+ try {
34
+ if (!existsSync(PENDING_DIR))
35
+ return;
36
+ const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));
37
+ const now = Date.now();
38
+ const pendingFiles = [];
39
+ for (const file of files) {
40
+ const filePath = join(PENDING_DIR, file);
41
+ try {
42
+ const { mtimeMs } = statSync(filePath);
43
+ if (now - mtimeMs > MAX_PENDING_AGE_MS) {
44
+ debug(`[Telemetry] Dropping stale pending file: ${file}`);
45
+ safeUnlink(filePath);
46
+ }
47
+ else {
48
+ pendingFiles.push({ file, filePath, mtimeMs });
49
+ }
50
+ }
51
+ catch {
52
+ debug(`[Telemetry] Dropping unreadable pending file: ${file}`);
53
+ safeUnlink(filePath);
54
+ }
55
+ }
56
+ pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
57
+ const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);
58
+ for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {
59
+ debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);
60
+ safeUnlink(dropped.filePath);
61
+ }
62
+ const recoveredFiles = [];
63
+ for (const { filePath } of filesToRecover) {
64
+ try {
65
+ const raw = readFileSync(filePath, 'utf-8');
66
+ const events = JSON.parse(raw);
67
+ if (Array.isArray(events) && events.length > 0) {
68
+ telemetryClient.queueEvents(events);
69
+ recoveredFiles.push(filePath);
70
+ }
71
+ else {
72
+ // Empty file — delete immediately
73
+ safeUnlink(filePath);
74
+ }
75
+ }
76
+ catch {
77
+ // Corrupted file — delete and move on
78
+ safeUnlink(filePath);
79
+ }
80
+ }
81
+ // Delete source files — events are now in memory regardless of flush outcome.
82
+ // If flush succeeds: events sent, done.
83
+ // If flush fails: events stay in memory, exit handler re-persists to new PID file.
84
+ for (const filePath of recoveredFiles) {
85
+ safeUnlink(filePath);
86
+ }
87
+ // Flush all recovered events in one batch
88
+ await telemetryClient.flush();
89
+ }
90
+ catch {
91
+ debug('[Telemetry] Store-forward recovery failed silently');
92
+ }
93
+ }
94
+ //# sourceMappingURL=telemetry-store-forward.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry-store-forward.js","sourceRoot":"","sources":["../../src/utils/telemetry-store-forward.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { telemetryClient } from './telemetry-client.js';\nimport { debug } from './debug.js';\n\nconst PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');\nconst PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);\nconst MAX_PENDING_FILES = 100;\nconst MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\nfunction safeUnlink(filePath: string): void {\n try {\n unlinkSync(filePath);\n } catch {\n /* ignore */\n }\n}\n\n/**\n * Register a sync exit handler that persists unsent events to disk.\n * Called once at startup. Uses PID in filename to prevent concurrent\n * CLI invocations from colliding.\n */\nexport function installStoreForward(): void {\n process.on('exit', () => {\n telemetryClient.persistToFile(PENDING_FILE);\n });\n}\n\n/**\n * On startup, check for ANY pending files from previous invocations\n * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.\n */\nexport async function recoverPendingEvents(): Promise<void> {\n try {\n if (!existsSync(PENDING_DIR)) return;\n const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));\n const now = Date.now();\n const pendingFiles: Array<{ file: string; filePath: string; mtimeMs: number }> = [];\n\n for (const file of files) {\n const filePath = join(PENDING_DIR, file);\n try {\n const { mtimeMs } = statSync(filePath);\n if (now - mtimeMs > MAX_PENDING_AGE_MS) {\n debug(`[Telemetry] Dropping stale pending file: ${file}`);\n safeUnlink(filePath);\n } else {\n pendingFiles.push({ file, filePath, mtimeMs });\n }\n } catch {\n debug(`[Telemetry] Dropping unreadable pending file: ${file}`);\n safeUnlink(filePath);\n }\n }\n\n pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);\n const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);\n for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {\n debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);\n safeUnlink(dropped.filePath);\n }\n\n const recoveredFiles: string[] = [];\n for (const { filePath } of filesToRecover) {\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const events = JSON.parse(raw);\n if (Array.isArray(events) && events.length > 0) {\n telemetryClient.queueEvents(events);\n recoveredFiles.push(filePath);\n } else {\n // Empty file — delete immediately\n safeUnlink(filePath);\n }\n } catch {\n // Corrupted file — delete and move on\n safeUnlink(filePath);\n }\n }\n\n // Delete source files — events are now in memory regardless of flush outcome.\n // If flush succeeds: events sent, done.\n // If flush fails: events stay in memory, exit handler re-persists to new PID file.\n for (const filePath of recoveredFiles) {\n safeUnlink(filePath);\n }\n\n // Flush all recovered events in one batch\n await telemetryClient.flush();\n } catch {\n debug('[Telemetry] Store-forward recovery failed silently');\n }\n}\n"]}
@@ -2,31 +2,49 @@
2
2
  * Telemetry event types for installer → gateway communication.
3
3
  * The gateway converts these to OTel format.
4
4
  */
5
- export interface TelemetryEvent {
6
- type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm';
5
+ export interface BaseTelemetryEvent {
6
+ type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';
7
7
  sessionId: string;
8
8
  timestamp: string;
9
- attributes?: Record<string, string | number | boolean>;
10
9
  }
11
- export interface SessionStartEvent extends TelemetryEvent {
10
+ export type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';
11
+ /**
12
+ * Structured outcome dimension for command events. Supersedes the boolean
13
+ * `command.success` as the primary categorization (`command.success` remains
14
+ * for backward-compat). Populated by `analytics.emitCommandEvent()` from the
15
+ * top-level command lifecycle.
16
+ */
17
+ export type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';
18
+ export interface EnvFingerprint {
19
+ 'device.id': string;
20
+ 'auth.mode': AuthMode;
21
+ 'env.os': string;
22
+ 'env.os_version': string;
23
+ 'env.node_version': string;
24
+ 'env.shell': string;
25
+ 'env.ci': boolean;
26
+ 'env.ci_provider'?: string;
27
+ }
28
+ export interface SessionStartEvent extends BaseTelemetryEvent {
12
29
  type: 'session.start';
13
30
  attributes: {
14
31
  'installer.version': string;
15
32
  'installer.mode': 'cli' | 'tui' | 'headless';
16
33
  'workos.user_id'?: string;
17
34
  'workos.org_id'?: string;
18
- };
35
+ } & EnvFingerprint;
19
36
  }
20
- export interface SessionEndEvent extends TelemetryEvent {
37
+ export interface SessionEndEvent extends BaseTelemetryEvent {
21
38
  type: 'session.end';
22
39
  attributes: {
23
40
  'installer.outcome': 'success' | 'error' | 'cancelled';
24
41
  'installer.duration_ms': number;
25
42
  } & Record<string, string | number | boolean>;
26
43
  }
27
- export interface StepEvent extends TelemetryEvent {
44
+ export interface StepEvent extends BaseTelemetryEvent {
28
45
  type: 'step';
29
46
  name: string;
47
+ startTimestamp: string;
30
48
  durationMs: number;
31
49
  success: boolean;
32
50
  error?: {
@@ -34,18 +52,49 @@ export interface StepEvent extends TelemetryEvent {
34
52
  message: string;
35
53
  };
36
54
  }
37
- export interface AgentToolEvent extends TelemetryEvent {
55
+ export interface AgentToolEvent extends BaseTelemetryEvent {
38
56
  type: 'agent.tool';
39
57
  toolName: string;
58
+ startTimestamp: string;
40
59
  durationMs: number;
41
60
  success: boolean;
42
61
  }
43
- export interface AgentLLMEvent extends TelemetryEvent {
62
+ export interface AgentLLMEvent extends BaseTelemetryEvent {
44
63
  type: 'agent.llm';
45
64
  model: string;
46
65
  inputTokens: number;
47
66
  outputTokens: number;
48
67
  }
68
+ export interface CommandEvent extends BaseTelemetryEvent {
69
+ type: 'command';
70
+ attributes: {
71
+ 'command.name': string;
72
+ 'command.duration_ms': number;
73
+ 'command.success': boolean;
74
+ 'command.error_type'?: string;
75
+ 'command.error_message'?: string;
76
+ 'command.flags'?: string;
77
+ 'termination.reason'?: TerminationReason;
78
+ 'error.code'?: string;
79
+ 'api.status'?: number;
80
+ 'api.code'?: string;
81
+ 'api.resource'?: string;
82
+ 'cli.version': string;
83
+ 'workos.user_id'?: string;
84
+ } & EnvFingerprint;
85
+ }
86
+ export interface CrashEvent extends BaseTelemetryEvent {
87
+ type: 'crash';
88
+ attributes: {
89
+ 'crash.error_type': string;
90
+ 'crash.error_message': string;
91
+ 'crash.stack': string;
92
+ 'crash.command'?: string;
93
+ 'cli.version': string;
94
+ 'workos.user_id'?: string;
95
+ } & EnvFingerprint;
96
+ }
49
97
  export interface TelemetryRequest {
50
98
  events: TelemetryEvent[];
51
99
  }
100
+ export type TelemetryEvent = SessionStartEvent | SessionEndEvent | StepEvent | AgentToolEvent | AgentLLMEvent | CommandEvent | CrashEvent;
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface TelemetryEvent {\n type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm';\n sessionId: string;\n timestamp: string;\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SessionStartEvent extends TelemetryEvent {\n type: 'session.start';\n attributes: {\n 'installer.version': string;\n 'installer.mode': 'cli' | 'tui' | 'headless';\n 'workos.user_id'?: string;\n 'workos.org_id'?: string;\n };\n}\n\nexport interface SessionEndEvent extends TelemetryEvent {\n type: 'session.end';\n attributes: {\n 'installer.outcome': 'success' | 'error' | 'cancelled';\n 'installer.duration_ms': number;\n } & Record<string, string | number | boolean>;\n}\n\nexport interface StepEvent extends TelemetryEvent {\n type: 'step';\n name: string;\n durationMs: number;\n success: boolean;\n error?: {\n type: string;\n message: string;\n };\n}\n\nexport interface AgentToolEvent extends TelemetryEvent {\n type: 'agent.tool';\n toolName: string;\n durationMs: number;\n success: boolean;\n}\n\nexport interface AgentLLMEvent extends TelemetryEvent {\n type: 'agent.llm';\n model: string;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface TelemetryRequest {\n events: TelemetryEvent[];\n}\n"]}
1
+ {"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface BaseTelemetryEvent {\n type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';\n sessionId: string;\n timestamp: string;\n}\n\nexport type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';\n\n/**\n * Structured outcome dimension for command events. Supersedes the boolean\n * `command.success` as the primary categorization (`command.success` remains\n * for backward-compat). Populated by `analytics.emitCommandEvent()` from the\n * top-level command lifecycle.\n */\nexport type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';\n\nexport interface EnvFingerprint {\n 'device.id': string;\n 'auth.mode': AuthMode;\n 'env.os': string;\n 'env.os_version': string;\n 'env.node_version': string;\n 'env.shell': string;\n 'env.ci': boolean;\n 'env.ci_provider'?: string;\n}\n\nexport interface SessionStartEvent extends BaseTelemetryEvent {\n type: 'session.start';\n attributes: {\n 'installer.version': string;\n 'installer.mode': 'cli' | 'tui' | 'headless';\n 'workos.user_id'?: string;\n 'workos.org_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface SessionEndEvent extends BaseTelemetryEvent {\n type: 'session.end';\n attributes: {\n 'installer.outcome': 'success' | 'error' | 'cancelled';\n 'installer.duration_ms': number;\n } & Record<string, string | number | boolean>;\n}\n\nexport interface StepEvent extends BaseTelemetryEvent {\n type: 'step';\n name: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n error?: {\n type: string;\n message: string;\n };\n}\n\nexport interface AgentToolEvent extends BaseTelemetryEvent {\n type: 'agent.tool';\n toolName: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n}\n\nexport interface AgentLLMEvent extends BaseTelemetryEvent {\n type: 'agent.llm';\n model: string;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface CommandEvent extends BaseTelemetryEvent {\n type: 'command';\n attributes: {\n 'command.name': string;\n 'command.duration_ms': number;\n 'command.success': boolean;\n 'command.error_type'?: string;\n 'command.error_message'?: string;\n 'command.flags'?: string;\n 'termination.reason'?: TerminationReason;\n 'error.code'?: string;\n 'api.status'?: number;\n 'api.code'?: string;\n 'api.resource'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface CrashEvent extends BaseTelemetryEvent {\n type: 'crash';\n attributes: {\n 'crash.error_type': string;\n 'crash.error_message': string;\n 'crash.stack': string;\n 'crash.command'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface TelemetryRequest {\n events: TelemetryEvent[];\n}\n\nexport type TelemetryEvent =\n | SessionStartEvent\n | SessionEndEvent\n | StepEvent\n | AgentToolEvent\n | AgentLLMEvent\n | CommandEvent\n | CrashEvent;\n"]}
@@ -54,10 +54,6 @@ export type InstallerOptions = {
54
54
  * Event emitter for dashboard mode
55
55
  */
56
56
  emitter?: import('../lib/events.js').InstallerEventEmitter;
57
- /**
58
- * Pre-selected framework integration (bypasses detection)
59
- */
60
- integration?: import('../lib/constants.js').Integration;
61
57
  /**
62
58
  * Enable XState inspector - opens browser to visualize state machine live
63
59
  */
@@ -93,6 +89,16 @@ export type InstallerOptions = {
93
89
  * Default: 2. Set to 0 to disable retries entirely.
94
90
  */
95
91
  maxRetries?: number;
92
+ /**
93
+ * Scaffold a new Next.js app when run in an empty directory.
94
+ * Auto-enabled in headless mode; opt-in via --scaffold in interactive mode.
95
+ */
96
+ scaffold?: boolean;
97
+ /**
98
+ * Package manager for the scaffolded app (npm/pnpm/yarn/bun).
99
+ * Overrides detection from npm_config_user_agent.
100
+ */
101
+ pm?: string;
96
102
  };
97
103
  export interface Feature {
98
104
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":"","sourcesContent":["export type InstallerOptions = {\n /**\n * Whether to enable debug mode.\n */\n debug: boolean;\n\n /**\n * Whether to force install the SDK package to continue with the installation in case\n * any package manager checks are failing (e.g. peer dependency versions).\n *\n * Use with caution and only if you know what you're doing.\n *\n * Does not apply to all wizard flows (currently NPM only)\n */\n forceInstall: boolean;\n\n /**\n * The directory to run the wizard in.\n */\n installDir: string;\n\n /**\n * Whether to use local services (LLM gateway on localhost:8000)\n */\n local: boolean;\n\n /**\n * CI mode - non-interactive execution\n */\n ci: boolean;\n\n /**\n * Skip authentication check (for local development only)\n */\n skipAuth: boolean;\n\n /**\n * WorkOS API key (sk_xxx)\n */\n apiKey?: string;\n\n /**\n * WorkOS Client ID (client_xxx)\n */\n clientId?: string;\n\n /**\n * App homepage URL for WorkOS dashboard config.\n * Defaults to http://localhost:{detected_port}\n */\n homepageUrl?: string;\n\n /**\n * Redirect URI for WorkOS callback.\n * Defaults to framework-specific convention (e.g., /api/auth/callback)\n */\n redirectUri?: string;\n\n /**\n * [Experimental] Enable visual dashboard mode\n */\n dashboard?: boolean;\n\n /**\n * Event emitter for dashboard mode\n */\n emitter?: import('../lib/events.js').InstallerEventEmitter;\n\n /**\n * Pre-selected framework integration (bypasses detection)\n */\n integration?: import('../lib/constants.js').Integration;\n\n /**\n * Enable XState inspector - opens browser to visualize state machine live\n */\n inspect?: boolean;\n\n /**\n * Skip post-installation validation (includes build check)\n */\n noValidate?: boolean;\n\n /**\n * Skip post-install commit and PR workflow\n */\n noCommit?: boolean;\n\n /**\n * Skip branch creation (continue on current branch)\n */\n noBranch?: boolean;\n\n /**\n * Auto-create pull request after installation\n */\n createPr?: boolean;\n\n /**\n * Skip git dirty working tree check\n */\n noGitCheck?: boolean;\n\n /**\n * Direct mode - bypass llm-gateway and use user's own Anthropic API key.\n * Requires ANTHROPIC_API_KEY environment variable.\n */\n direct?: boolean;\n\n /**\n * Max correction attempts after initial agent run.\n * The agent gets this many chances to fix validation failures (typecheck/build).\n * Default: 2. Set to 0 to disable retries entirely.\n */\n maxRetries?: number;\n};\n\nexport interface Feature {\n id: string;\n prompt: string;\n enabledHint?: string;\n disabledHint?: string;\n}\n\nexport type FileChange = {\n filePath: string;\n oldContent?: string;\n newContent: string;\n};\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":"","sourcesContent":["export type InstallerOptions = {\n /**\n * Whether to enable debug mode.\n */\n debug: boolean;\n\n /**\n * Whether to force install the SDK package to continue with the installation in case\n * any package manager checks are failing (e.g. peer dependency versions).\n *\n * Use with caution and only if you know what you're doing.\n *\n * Does not apply to all wizard flows (currently NPM only)\n */\n forceInstall: boolean;\n\n /**\n * The directory to run the wizard in.\n */\n installDir: string;\n\n /**\n * Whether to use local services (LLM gateway on localhost:8000)\n */\n local: boolean;\n\n /**\n * CI mode - non-interactive execution\n */\n ci: boolean;\n\n /**\n * Skip authentication check (for local development only)\n */\n skipAuth: boolean;\n\n /**\n * WorkOS API key (sk_xxx)\n */\n apiKey?: string;\n\n /**\n * WorkOS Client ID (client_xxx)\n */\n clientId?: string;\n\n /**\n * App homepage URL for WorkOS dashboard config.\n * Defaults to http://localhost:{detected_port}\n */\n homepageUrl?: string;\n\n /**\n * Redirect URI for WorkOS callback.\n * Defaults to framework-specific convention (e.g., /api/auth/callback)\n */\n redirectUri?: string;\n\n /**\n * [Experimental] Enable visual dashboard mode\n */\n dashboard?: boolean;\n\n /**\n * Event emitter for dashboard mode\n */\n emitter?: import('../lib/events.js').InstallerEventEmitter;\n\n /**\n * Enable XState inspector - opens browser to visualize state machine live\n */\n inspect?: boolean;\n\n /**\n * Skip post-installation validation (includes build check)\n */\n noValidate?: boolean;\n\n /**\n * Skip post-install commit and PR workflow\n */\n noCommit?: boolean;\n\n /**\n * Skip branch creation (continue on current branch)\n */\n noBranch?: boolean;\n\n /**\n * Auto-create pull request after installation\n */\n createPr?: boolean;\n\n /**\n * Skip git dirty working tree check\n */\n noGitCheck?: boolean;\n\n /**\n * Direct mode - bypass llm-gateway and use user's own Anthropic API key.\n * Requires ANTHROPIC_API_KEY environment variable.\n */\n direct?: boolean;\n\n /**\n * Max correction attempts after initial agent run.\n * The agent gets this many chances to fix validation failures (typecheck/build).\n * Default: 2. Set to 0 to disable retries entirely.\n */\n maxRetries?: number;\n\n /**\n * Scaffold a new Next.js app when run in an empty directory.\n * Auto-enabled in headless mode; opt-in via --scaffold in interactive mode.\n */\n scaffold?: boolean;\n\n /**\n * Package manager for the scaffolded app (npm/pnpm/yarn/bun).\n * Overrides detection from npm_config_user_agent.\n */\n pm?: string;\n};\n\nexport interface Feature {\n id: string;\n prompt: string;\n enabledHint?: string;\n disabledHint?: string;\n}\n\nexport type FileChange = {\n filePath: string;\n oldContent?: string;\n newContent: string;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workos",
3
- "version": "0.15.2",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "The Official Workos CLI",
6
6
  "repository": {