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.
- package/README.md +44 -14
- package/dist/bin.js +1449 -1261
- package/dist/bin.js.map +1 -1
- package/dist/cli.config.d.ts +1 -0
- package/dist/cli.config.js +1 -0
- package/dist/cli.config.js.map +1 -1
- package/dist/commands/api/index.js +7 -2
- package/dist/commands/api/index.js.map +1 -1
- package/dist/commands/api/interactive.js +9 -3
- package/dist/commands/api/interactive.js.map +1 -1
- package/dist/commands/debug.d.ts +2 -1
- package/dist/commands/debug.js +43 -3
- package/dist/commands/debug.js.map +1 -1
- package/dist/commands/dev.js +5 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.js +13 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/emulate.js +2 -2
- package/dist/commands/emulate.js.map +1 -1
- package/dist/commands/env.js +5 -4
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/install-skill.js +4 -3
- package/dist/commands/install-skill.js.map +1 -1
- package/dist/commands/install.js +2 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrations.d.ts +1 -1
- package/dist/commands/migrations.js +4 -1
- package/dist/commands/migrations.js.map +1 -1
- package/dist/commands/telemetry.d.ts +3 -0
- package/dist/commands/telemetry.js +88 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/uninstall-skill.js +3 -2
- package/dist/commands/uninstall-skill.js.map +1 -1
- package/dist/commands/vault-run.d.ts +13 -0
- package/dist/commands/vault-run.js +194 -0
- package/dist/commands/vault-run.js.map +1 -0
- package/dist/commands/vault.d.ts +3 -2
- package/dist/commands/vault.js +41 -8
- package/dist/commands/vault.js.map +1 -1
- package/dist/lib/adapters/cli-adapter.d.ts +7 -0
- package/dist/lib/adapters/cli-adapter.js +49 -0
- package/dist/lib/adapters/cli-adapter.js.map +1 -1
- package/dist/lib/adapters/dashboard-adapter.d.ts +4 -0
- package/dist/lib/adapters/dashboard-adapter.js +24 -0
- package/dist/lib/adapters/dashboard-adapter.js.map +1 -1
- package/dist/lib/adapters/headless-adapter.d.ts +6 -0
- package/dist/lib/adapters/headless-adapter.js +26 -1
- package/dist/lib/adapters/headless-adapter.js.map +1 -1
- package/dist/lib/api-error-handler.d.ts +15 -3
- package/dist/lib/api-error-handler.js +52 -34
- package/dist/lib/api-error-handler.js.map +1 -1
- package/dist/lib/command-aliases.d.ts +8 -0
- package/dist/lib/command-aliases.js +12 -0
- package/dist/lib/command-aliases.js.map +1 -0
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/device-id.d.ts +25 -0
- package/dist/lib/device-id.js +102 -0
- package/dist/lib/device-id.js.map +1 -0
- package/dist/lib/events.d.ts +15 -0
- package/dist/lib/events.js.map +1 -1
- package/dist/lib/installer-core.d.ts +61 -1
- package/dist/lib/installer-core.js +132 -6
- package/dist/lib/installer-core.js.map +1 -1
- package/dist/lib/installer-core.types.d.ts +24 -0
- package/dist/lib/installer-core.types.js.map +1 -1
- package/dist/lib/preferences.d.ts +101 -0
- package/dist/lib/preferences.js +198 -0
- package/dist/lib/preferences.js.map +1 -0
- package/dist/lib/run-with-core.js +40 -15
- package/dist/lib/run-with-core.js.map +1 -1
- package/dist/lib/scaffold/index.d.ts +1 -0
- package/dist/lib/scaffold/index.js +2 -0
- package/dist/lib/scaffold/index.js.map +1 -0
- package/dist/lib/scaffold/scaffold.d.ts +66 -0
- package/dist/lib/scaffold/scaffold.js +156 -0
- package/dist/lib/scaffold/scaffold.js.map +1 -0
- package/dist/lib/settings.d.ts +6 -0
- package/dist/lib/settings.js +7 -0
- package/dist/lib/settings.js.map +1 -1
- package/dist/lib/telemetry-notice.d.ts +25 -0
- package/dist/lib/telemetry-notice.js +56 -0
- package/dist/lib/telemetry-notice.js.map +1 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +2 -1
- package/dist/run.js.map +1 -1
- package/dist/test/force-insecure-storage.d.ts +1 -0
- package/dist/test/force-insecure-storage.js +9 -0
- package/dist/test/force-insecure-storage.js.map +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +38 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/utils/analytics.d.ts +41 -0
- package/dist/utils/analytics.js +199 -12
- package/dist/utils/analytics.js.map +1 -1
- package/dist/utils/box.d.ts +29 -1
- package/dist/utils/box.js +92 -4
- package/dist/utils/box.js.map +1 -1
- package/dist/utils/cli-exit.d.ts +15 -0
- package/dist/utils/cli-exit.js +11 -0
- package/dist/utils/cli-exit.js.map +1 -0
- package/dist/utils/cli-symbols.d.ts +1 -1
- package/dist/utils/command-telemetry.d.ts +17 -0
- package/dist/utils/command-telemetry.js +67 -0
- package/dist/utils/command-telemetry.js.map +1 -0
- package/dist/utils/crash-reporter.d.ts +13 -0
- package/dist/utils/crash-reporter.js +91 -0
- package/dist/utils/crash-reporter.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +4 -1
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/exit-codes.d.ts +5 -0
- package/dist/utils/exit-codes.js +30 -1
- package/dist/utils/exit-codes.js.map +1 -1
- package/dist/utils/help-json.d.ts +6 -0
- package/dist/utils/help-json.js +87 -10
- package/dist/utils/help-json.js.map +1 -1
- package/dist/utils/output.d.ts +7 -2
- package/dist/utils/output.js +9 -2
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/telemetry-client.d.ts +30 -2
- package/dist/utils/telemetry-client.js +122 -12
- package/dist/utils/telemetry-client.js.map +1 -1
- package/dist/utils/telemetry-store-forward.d.ts +11 -0
- package/dist/utils/telemetry-store-forward.js +94 -0
- package/dist/utils/telemetry-store-forward.js.map +1 -0
- package/dist/utils/telemetry-types.d.ts +58 -9
- package/dist/utils/telemetry-types.js.map +1 -1
- package/dist/utils/types.d.ts +10 -4
- package/dist/utils/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
25
|
-
return;
|
|
87
|
+
debug('[Telemetry] No telemetry URL configured, skipping flush');
|
|
88
|
+
return false;
|
|
26
89
|
}
|
|
27
|
-
const
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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":"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","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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"]}
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/types.js.map
CHANGED
|
@@ -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 *
|
|
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"]}
|