sim 2.1.14-preview.116.1 → 2.1.14

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 (3) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +571 -128
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -317,6 +317,7 @@ The main environment variables are:
317
317
  | `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely |
318
318
  | `SIM_DEBUG` | Print request diagnostics to stderr |
319
319
  | `SIM_NO_UPDATE_CHECK` | Turn off update notices |
320
+ | `SIM_TELEMETRY_DISABLED` | Turn off anonymous usage reporting (`DO_NOT_TRACK=1` also works) |
320
321
 
321
322
  On eligible interactive invocations, `sim` uses a daily cache before asking
322
323
  `registry.npmjs.org` what is published under the `latest` tag and prints an
@@ -334,6 +335,22 @@ use the public default; non-empty malformed or non-HTTP(S) values fail closed.
334
335
  The full list of cases where it stays quiet is in the
335
336
  [configuration guide](https://docs.sim.ai/cli/configuration).
336
337
 
338
+ ## Usage data
339
+
340
+ The CLI reports anonymous usage data — which commands run, whether they
341
+ succeed, and how long they take — so the team can see how it is used. Nothing
342
+ you type is sent: no argument or flag values, paths, ids, error messages, or
343
+ credentials. The first interactive run prints a notice and is not reported.
344
+
345
+ ```bash
346
+ sim telemetry status
347
+ sim telemetry disable
348
+ ```
349
+
350
+ `DO_NOT_TRACK=1` or `SIM_TELEMETRY_DISABLED=1` in the environment also turns it
351
+ off. The full description of what is sent is in the
352
+ [usage data guide](https://docs.sim.ai/cli/usage-data).
353
+
337
354
  ## Documentation
338
355
 
339
356
  - [CLI documentation](https://docs.sim.ai/cli)
@@ -342,6 +359,7 @@ The full list of cases where it stays quiet is in the
342
359
  - [Profiles and configuration](https://docs.sim.ai/cli/configuration)
343
360
  - [Scripting](https://docs.sim.ai/cli/scripting)
344
361
  - [Troubleshooting](https://docs.sim.ai/cli/troubleshooting)
362
+ - [Usage data](https://docs.sim.ai/cli/usage-data)
345
363
 
346
364
  ## License
347
365
 
package/dist/index.js CHANGED
@@ -6976,6 +6976,9 @@ var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
6976
6976
 
6977
6977
  // src/update/check.ts
6978
6978
  import { spawn } from "node:child_process";
6979
+ import { fileURLToPath } from "node:url";
6980
+
6981
+ // src/config/json-file.ts
6979
6982
  import {
6980
6983
  closeSync,
6981
6984
  constants,
@@ -6989,7 +6992,64 @@ import {
6989
6992
  writeFileSync
6990
6993
  } from "node:fs";
6991
6994
  import { dirname } from "node:path";
6992
- import { fileURLToPath } from "node:url";
6995
+ var writeSequence = 0;
6996
+ function readJsonFile(path, maxBytes) {
6997
+ let descriptor = null;
6998
+ try {
6999
+ if (!lstatSync(path).isFile())
7000
+ return null;
7001
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
7002
+ const stats = fstatSync(descriptor);
7003
+ if (!stats.isFile() || stats.size > maxBytes)
7004
+ return null;
7005
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
7006
+ let bytesRead = 0;
7007
+ while (bytesRead < buffer.byteLength) {
7008
+ const count = readSync(descriptor, buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
7009
+ if (count === 0)
7010
+ break;
7011
+ bytesRead += count;
7012
+ }
7013
+ if (bytesRead > maxBytes)
7014
+ return null;
7015
+ return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
7016
+ } catch {
7017
+ return null;
7018
+ } finally {
7019
+ if (descriptor !== null) {
7020
+ try {
7021
+ closeSync(descriptor);
7022
+ } catch {}
7023
+ }
7024
+ }
7025
+ }
7026
+ function writeJsonFile(path, value, mode = 420) {
7027
+ let descriptor = null;
7028
+ let temporaryCreated = false;
7029
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.${writeSequence++}.tmp`;
7030
+ try {
7031
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
7032
+ descriptor = openSync(temporaryPath, "wx", mode);
7033
+ temporaryCreated = true;
7034
+ writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}
7035
+ `);
7036
+ closeSync(descriptor);
7037
+ descriptor = null;
7038
+ renameSync(temporaryPath, path);
7039
+ temporaryCreated = false;
7040
+ } catch {} finally {
7041
+ if (descriptor !== null) {
7042
+ try {
7043
+ closeSync(descriptor);
7044
+ } catch {}
7045
+ }
7046
+ if (temporaryCreated) {
7047
+ try {
7048
+ unlinkSync(temporaryPath);
7049
+ } catch {}
7050
+ }
7051
+ }
7052
+ }
6993
7053
 
6994
7054
  // src/config/paths.ts
6995
7055
  import { homedir } from "node:os";
@@ -7006,6 +7066,39 @@ function credentialsPath() {
7006
7066
  function updateCachePath() {
7007
7067
  return join(configDir(), "update-check.json");
7008
7068
  }
7069
+ function telemetryStatePath() {
7070
+ return join(configDir(), "telemetry.json");
7071
+ }
7072
+
7073
+ // src/environment.ts
7074
+ var CI_VARIABLES = [
7075
+ "CI",
7076
+ "GITHUB_ACTIONS",
7077
+ "JENKINS_URL",
7078
+ "TEAMCITY_VERSION",
7079
+ "BUILDKITE"
7080
+ ];
7081
+ function isEnabled(value) {
7082
+ if (value === undefined)
7083
+ return false;
7084
+ const normalized = value.trim().toLowerCase();
7085
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
7086
+ }
7087
+ function isCi(env = process.env) {
7088
+ return CI_VARIABLES.some((variable) => isEnabled(env[variable]));
7089
+ }
7090
+ function proxyExecArgv() {
7091
+ return process.execArgv.filter((argument) => argument === "--use-env-proxy" || argument === "--no-use-env-proxy");
7092
+ }
7093
+ function childProcessEnv(strip, extra = {}) {
7094
+ const env = { ...process.env };
7095
+ const stripped = new Set(strip.map((name) => name.toLowerCase()));
7096
+ for (const key of Object.keys(env)) {
7097
+ if (stripped.has(key.toLowerCase()))
7098
+ delete env[key];
7099
+ }
7100
+ return { ...env, ...extra };
7101
+ }
7009
7102
 
7010
7103
  // src/version.ts
7011
7104
  import { readFileSync } from "node:fs";
@@ -7042,21 +7135,7 @@ function isNewerVersion(candidate, current) {
7042
7135
  return candidate[1] > current[1];
7043
7136
  return candidate[2] > current[2];
7044
7137
  }
7045
- var CI_VARIABLES = [
7046
- "CI",
7047
- "GITHUB_ACTIONS",
7048
- "JENKINS_URL",
7049
- "TEAMCITY_VERSION",
7050
- "BUILDKITE"
7051
- ];
7052
7138
  var CACHE_VERSION = 1;
7053
- var cacheWriteSequence = 0;
7054
- function isEnabled(value) {
7055
- if (value === undefined)
7056
- return false;
7057
- const normalized = value.trim().toLowerCase();
7058
- return normalized !== "" && normalized !== "0" && normalized !== "false";
7059
- }
7060
7139
  function isProjectLocalInstall(modulePath, cwd) {
7061
7140
  const normalizedModulePath = normalizeModulePath(modulePath);
7062
7141
  const nodeModulesIndex = normalizedModulePath.indexOf("/node_modules/");
@@ -7125,20 +7204,10 @@ try {
7125
7204
  process.exit(1)
7126
7205
  }
7127
7206
  `;
7128
- function registryProcessEnv() {
7129
- const env = { ...process.env };
7130
- for (const key of Object.keys(env)) {
7131
- const normalized = key.toLowerCase();
7132
- if (normalized === "npm_config_registry" || normalized === "sim_api_key")
7133
- delete env[key];
7134
- }
7135
- return env;
7136
- }
7137
7207
  function requestRegistry(url, { headers, maxResponseBytes, timeoutMs }) {
7138
7208
  return new Promise((resolve, reject) => {
7139
- const proxyArguments = process.execArgv.filter((argument) => argument === "--use-env-proxy" || argument === "--no-use-env-proxy");
7140
- const child = spawn(process.execPath, [...proxyArguments, "--input-type=module", "--eval", REGISTRY_REQUEST_SCRIPT], {
7141
- env: registryProcessEnv(),
7209
+ const child = spawn(process.execPath, [...proxyExecArgv(), "--input-type=module", "--eval", REGISTRY_REQUEST_SCRIPT], {
7210
+ env: childProcessEnv(["npm_config_registry", "sim_api_key"]),
7142
7211
  killSignal: "SIGKILL",
7143
7212
  stdio: ["pipe", "pipe", "ignore"],
7144
7213
  timeout: timeoutMs,
@@ -7194,73 +7263,18 @@ async function fetchDistTags(env, request) {
7194
7263
  }
7195
7264
  }
7196
7265
  function readCache(path) {
7197
- let descriptor = null;
7198
- try {
7199
- if (!lstatSync(path).isFile())
7200
- return null;
7201
- descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
7202
- const descriptorStats = fstatSync(descriptor);
7203
- if (!descriptorStats.isFile() || descriptorStats.size > MAX_CACHE_BYTES) {
7204
- return null;
7205
- }
7206
- const buffer = Buffer.allocUnsafe(MAX_CACHE_BYTES + 1);
7207
- let bytesRead = 0;
7208
- while (bytesRead < buffer.byteLength) {
7209
- const count = readSync(descriptor, buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
7210
- if (count === 0)
7211
- break;
7212
- bytesRead += count;
7213
- }
7214
- if (bytesRead > MAX_CACHE_BYTES)
7215
- return null;
7216
- const parsed = JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
7217
- if (typeof parsed !== "object" || parsed === null)
7218
- return null;
7219
- const entry = parsed;
7220
- if (entry.version !== CACHE_VERSION)
7221
- return null;
7222
- if (typeof entry.checkedAt !== "string" || Number.isNaN(Date.parse(entry.checkedAt)))
7223
- return null;
7224
- return {
7225
- version: CACHE_VERSION,
7226
- checkedAt: entry.checkedAt
7227
- };
7228
- } catch {
7266
+ const parsed = readJsonFile(path, MAX_CACHE_BYTES);
7267
+ if (typeof parsed !== "object" || parsed === null)
7229
7268
  return null;
7230
- } finally {
7231
- if (descriptor !== null) {
7232
- try {
7233
- closeSync(descriptor);
7234
- } catch {}
7235
- }
7236
- }
7269
+ const entry = parsed;
7270
+ if (entry.version !== CACHE_VERSION)
7271
+ return null;
7272
+ if (typeof entry.checkedAt !== "string" || Number.isNaN(Date.parse(entry.checkedAt)))
7273
+ return null;
7274
+ return { version: CACHE_VERSION, checkedAt: entry.checkedAt };
7237
7275
  }
7238
7276
  function writeCache(path, entry) {
7239
- let descriptor = null;
7240
- let temporaryCreated = false;
7241
- const temporaryPath = `${path}.${process.pid}.${Date.now()}.${cacheWriteSequence++}.tmp`;
7242
- try {
7243
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
7244
- descriptor = openSync(temporaryPath, "wx", 420);
7245
- temporaryCreated = true;
7246
- writeFileSync(descriptor, `${JSON.stringify(entry, null, 2)}
7247
- `);
7248
- closeSync(descriptor);
7249
- descriptor = null;
7250
- renameSync(temporaryPath, path);
7251
- temporaryCreated = false;
7252
- } catch {} finally {
7253
- if (descriptor !== null) {
7254
- try {
7255
- closeSync(descriptor);
7256
- } catch {}
7257
- }
7258
- if (temporaryCreated) {
7259
- try {
7260
- unlinkSync(temporaryPath);
7261
- } catch {}
7262
- }
7263
- }
7277
+ writeJsonFile(path, entry);
7264
7278
  }
7265
7279
  function isFresh(entry, now) {
7266
7280
  const age = now.getTime() - Date.parse(entry.checkedAt);
@@ -7297,7 +7311,7 @@ async function announceUpdateIfAvailable(options = {}) {
7297
7311
  return;
7298
7312
  if (!isTty)
7299
7313
  return;
7300
- if (CI_VARIABLES.some((variable) => isEnabled(env[variable])))
7314
+ if (isCi(env))
7301
7315
  return;
7302
7316
  if (isUnadvisableInstall(modulePath, env, cwd))
7303
7317
  return;
@@ -8007,6 +8021,191 @@ function resolveProfile(overrides = {}) {
8007
8021
  }
8008
8022
  };
8009
8023
  }
8024
+ // ../utils/src/client-info.ts
8025
+ var CLIENT_INFO_HEADER = "x-sim-client-info";
8026
+ var SIM_SURFACES = ["web", "desktop", "cli", "sdk-js", "sdk-python"];
8027
+ var TOKEN_PATTERN = /^[A-Za-z0-9._+-]+$/;
8028
+ var OS_KEY = "os";
8029
+ var ARCH_KEY = "arch";
8030
+ var AGENT_KEY = "agent";
8031
+ var SURFACE_SET = new Set(SIM_SURFACES);
8032
+ function isToken(value) {
8033
+ return TOKEN_PATTERN.test(value);
8034
+ }
8035
+ function product(name, version) {
8036
+ if (!isToken(name))
8037
+ throw new Error(`Client info token name is not a valid token: ${name}`);
8038
+ if (version === undefined)
8039
+ return name;
8040
+ if (!isToken(version))
8041
+ throw new Error(`Client info token version is not a valid token: ${version}`);
8042
+ return `${name}/${version}`;
8043
+ }
8044
+ function formatClientInfo(info) {
8045
+ const tokens = [product(info.surface, info.version)];
8046
+ if (info.runtime)
8047
+ tokens.push(product(info.runtime.name, info.runtime.version));
8048
+ if (info.os)
8049
+ tokens.push(product(OS_KEY, info.os));
8050
+ if (info.arch)
8051
+ tokens.push(product(ARCH_KEY, info.arch));
8052
+ if (info.agent)
8053
+ tokens.push(product(AGENT_KEY, info.agent));
8054
+ return tokens.join("; ");
8055
+ }
8056
+
8057
+ // src/telemetry/coding-agent.ts
8058
+ var AGENT_NAME_PATTERN = /^[a-z0-9_-]+$/i;
8059
+ var MAX_AGENT_NAME_LENGTH = 64;
8060
+ var anyOf = (...variables) => (env) => variables.some((variable) => Boolean(env[variable]));
8061
+ var AGENT_MARKERS = [
8062
+ { name: "amp", matches: (env) => env.AGENT === "amp" || Boolean(env.AMP_CURRENT_THREAD_ID) },
8063
+ {
8064
+ name: "codex",
8065
+ matches: anyOf("CODEX_THREAD_ID", "CODEX_SANDBOX", "CODEX_CI", "CODEX_SANDBOX_NETWORK_DISABLED")
8066
+ },
8067
+ { name: "gemini-cli", matches: anyOf("GEMINI_CLI") },
8068
+ { name: "opencode", matches: anyOf("OPENCODE") },
8069
+ { name: "antigravity", matches: anyOf("ANTIGRAVITY_AGENT") },
8070
+ { name: "augment", matches: anyOf("AUGMENT_AGENT") },
8071
+ { name: "cline", matches: anyOf("CLINE_ACTIVE") },
8072
+ { name: "cowork", matches: anyOf("CLAUDE_CODE_IS_COWORK") },
8073
+ { name: "claude-code", matches: anyOf("CLAUDECODE", "CLAUDE_CODE") },
8074
+ {
8075
+ name: "cursor",
8076
+ matches: (env) => anyOf("CURSOR_AGENT", "CURSOR_TRACE_ID")(env) || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec"
8077
+ },
8078
+ { name: "warp", matches: anyOf("OZ_RUN_ID") },
8079
+ { name: "pi", matches: anyOf("PI_CODING_AGENT") },
8080
+ { name: "crush", matches: anyOf("CRUSH") }
8081
+ ];
8082
+ function declaredAgentName(value) {
8083
+ const trimmed = value?.trim().toLowerCase();
8084
+ if (!trimmed || trimmed.length > MAX_AGENT_NAME_LENGTH)
8085
+ return;
8086
+ return AGENT_NAME_PATTERN.test(trimmed) ? trimmed : undefined;
8087
+ }
8088
+ function detectCodingAgent(env = process.env) {
8089
+ const declared = declaredAgentName(env.AI_AGENT);
8090
+ if (declared)
8091
+ return declared;
8092
+ const generic = declaredAgentName(env.AGENT);
8093
+ if (generic && generic !== "1")
8094
+ return generic;
8095
+ return AGENT_MARKERS.find((marker) => marker.matches(env))?.name;
8096
+ }
8097
+
8098
+ // src/telemetry/policy.ts
8099
+ var DO_NOT_TRACK_VARIABLE = "DO_NOT_TRACK";
8100
+ var TELEMETRY_DISABLED_VARIABLE = "SIM_TELEMETRY_DISABLED";
8101
+ function telemetryStatus({ env, state, configured }) {
8102
+ if (isEnabled(env[DO_NOT_TRACK_VARIABLE]))
8103
+ return { enabled: false, reason: "do_not_track" };
8104
+ if (isEnabled(env[TELEMETRY_DISABLED_VARIABLE]))
8105
+ return { enabled: false, reason: "environment" };
8106
+ if (state.enabled === false)
8107
+ return { enabled: false, reason: "setting" };
8108
+ if (!configured)
8109
+ return { enabled: false, reason: "unconfigured" };
8110
+ return { enabled: true };
8111
+ }
8112
+
8113
+ // ../utils/src/id.ts
8114
+ function generateId() {
8115
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
8116
+ return crypto.randomUUID();
8117
+ }
8118
+ const bytes = new Uint8Array(16);
8119
+ crypto.getRandomValues(bytes);
8120
+ bytes[6] = bytes[6] & 15 | 64;
8121
+ bytes[8] = bytes[8] & 63 | 128;
8122
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
8123
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
8124
+ }
8125
+
8126
+ // src/telemetry/state.ts
8127
+ var SESSION_IDLE_MS = 30 * 60 * 1000;
8128
+ var MAX_STATE_BYTES = 4 * 1024;
8129
+ var STATE_VERSION = 1;
8130
+ var STATE_FILE_MODE = 384;
8131
+ function isIsoTimestamp(value) {
8132
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
8133
+ }
8134
+ function parseSession(value) {
8135
+ if (typeof value !== "object" || value === null)
8136
+ return;
8137
+ const session = value;
8138
+ if (typeof session.id !== "string" || !session.id)
8139
+ return;
8140
+ if (!isIsoTimestamp(session.lastActiveAt))
8141
+ return;
8142
+ if (!Number.isSafeInteger(session.sequence) || session.sequence < 0)
8143
+ return;
8144
+ return {
8145
+ id: session.id,
8146
+ lastActiveAt: session.lastActiveAt,
8147
+ sequence: session.sequence
8148
+ };
8149
+ }
8150
+ function initialTelemetryState() {
8151
+ return { version: STATE_VERSION, deviceId: generateId() };
8152
+ }
8153
+ function readTelemetryState(path = telemetryStatePath()) {
8154
+ const parsed = readJsonFile(path, MAX_STATE_BYTES);
8155
+ if (typeof parsed !== "object" || parsed === null)
8156
+ return null;
8157
+ const state = parsed;
8158
+ if (state.version !== STATE_VERSION)
8159
+ return null;
8160
+ if (typeof state.deviceId !== "string" || !state.deviceId)
8161
+ return null;
8162
+ const result = { version: STATE_VERSION, deviceId: state.deviceId };
8163
+ if (typeof state.enabled === "boolean")
8164
+ result.enabled = state.enabled;
8165
+ if (isIsoTimestamp(state.noticeShownAt))
8166
+ result.noticeShownAt = state.noticeShownAt;
8167
+ const session = parseSession(state.session);
8168
+ if (session)
8169
+ result.session = session;
8170
+ return result;
8171
+ }
8172
+ function loadTelemetryState(path = telemetryStatePath()) {
8173
+ return readTelemetryState(path) ?? initialTelemetryState();
8174
+ }
8175
+ function writeTelemetryState(state, path = telemetryStatePath()) {
8176
+ writeJsonFile(path, state, STATE_FILE_MODE);
8177
+ }
8178
+ function nextSession(state, now) {
8179
+ const current = state.session;
8180
+ const idleFor = current ? now.getTime() - Date.parse(current.lastActiveAt) : Number.NaN;
8181
+ if (current && idleFor >= 0 && idleFor < SESSION_IDLE_MS) {
8182
+ return { id: current.id, lastActiveAt: now.toISOString(), sequence: current.sequence + 1 };
8183
+ }
8184
+ return { id: generateId(), lastActiveAt: now.toISOString(), sequence: 1 };
8185
+ }
8186
+
8187
+ // src/telemetry/client-info.ts
8188
+ var cached;
8189
+ function clientInfoHeader(env = process.env) {
8190
+ if (env !== process.env)
8191
+ return buildClientInfoHeader(env);
8192
+ cached ??= buildClientInfoHeader(env);
8193
+ return cached;
8194
+ }
8195
+ function buildClientInfoHeader(env) {
8196
+ return formatClientInfo({
8197
+ surface: "cli",
8198
+ version: CLI_VERSION,
8199
+ runtime: { name: "node", version: process.versions.node },
8200
+ os: process.platform,
8201
+ arch: process.arch,
8202
+ ...reportingAllowed(env) ? { agent: detectCodingAgent(env) } : {}
8203
+ });
8204
+ }
8205
+ function reportingAllowed(env) {
8206
+ return telemetryStatus({ env, state: loadTelemetryState(), configured: true }).enabled;
8207
+ }
8208
+
8010
8209
  // src/http/environment.ts
8011
8210
  var reported = new Set;
8012
8211
  function once(key, message) {
@@ -8345,6 +8544,7 @@ class SimClient {
8345
8544
  ...credential?.kind === "oauth" ? { authorization: `Bearer ${credential.oauth.accessToken}` } : {},
8346
8545
  accept: "application/json",
8347
8546
  "user-agent": USER_AGENT,
8547
+ [CLIENT_INFO_HEADER]: clientInfoHeader(),
8348
8548
  ...hasBody ? { "content-type": "application/json" } : {},
8349
8549
  ...options.headers
8350
8550
  },
@@ -20626,6 +20826,239 @@ function attachSecretCommands(program) {
20626
20826
  const redactionSpellings = new Set;
20627
20827
  secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description(describeOperation(V2_OPERATIONS.setSecret, "Create or replace a named secret")).addOption(new Option("--scope <scope>", "Secret ownership scope (required)").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value|@file>", "Secret value. Passing it inline exposes it to shell history and process listings; @path reads it from a file and @- from stdin, verbatim — a trailing newline is part of the value, so write the file with printf rather than echo. Prefix a literal leading @ with a second one").option("--description <description>", "What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged").option("--unredacted", `${V2_OPERATIONS.setSecret.body.unredacted.describe} Pass --no-unredacted to restore redaction`).option("--no-unredacted", "Send --unredacted as false").on("option:unredacted", () => redactionSpellings.add("--unredacted")).on("option:no-unredacted", () => redactionSpellings.add("--no-unredacted")).action((name, options, command) => setSecret(name, options, command, redactionSpellings));
20628
20828
  }
20829
+ // src/telemetry/transport.ts
20830
+ import { spawn as spawn4 } from "node:child_process";
20831
+ var BUILT_IN_KEY = process.env.SIM_CLI_TELEMETRY_KEY;
20832
+ var BUILT_IN_HOST = process.env.SIM_CLI_TELEMETRY_HOST;
20833
+ var DEFAULT_INGEST_HOST = "https://us.i.posthog.com";
20834
+ var CAPTURE_PATH = "/i/v0/e/";
20835
+ var SEND_TIMEOUT_MS = 5000;
20836
+ var PAYLOAD_VARIABLE = "SIM_TELEMETRY_CAPTURE";
20837
+ function builtInIngestTarget() {
20838
+ if (!BUILT_IN_KEY)
20839
+ return;
20840
+ return { key: BUILT_IN_KEY, host: BUILT_IN_HOST || DEFAULT_INGEST_HOST };
20841
+ }
20842
+ var SEND_SCRIPT = `
20843
+ try {
20844
+ const { url, body, timeoutMs } = JSON.parse(process.env[${JSON.stringify(PAYLOAD_VARIABLE)}])
20845
+ const deadline = setTimeout(() => process.exit(1), timeoutMs)
20846
+ await fetch(url, {
20847
+ method: 'POST',
20848
+ headers: { 'content-type': 'application/json' },
20849
+ body,
20850
+ redirect: 'error',
20851
+ })
20852
+ clearTimeout(deadline)
20853
+ process.exit(0)
20854
+ } catch {
20855
+ process.exit(1)
20856
+ }
20857
+ `;
20858
+ function sendCapture(target, request, spawnSender = spawn4) {
20859
+ const payload = JSON.stringify({
20860
+ url: new URL(CAPTURE_PATH, target.host).href,
20861
+ body: JSON.stringify(request),
20862
+ timeoutMs: SEND_TIMEOUT_MS
20863
+ });
20864
+ try {
20865
+ const child = spawnSender(process.execPath, [...proxyExecArgv(), "--input-type=module", "--eval", SEND_SCRIPT], {
20866
+ detached: true,
20867
+ env: childProcessEnv(["sim_api_key"], { [PAYLOAD_VARIABLE]: payload }),
20868
+ stdio: "ignore",
20869
+ windowsHide: true
20870
+ });
20871
+ child.once("error", () => {});
20872
+ child.unref();
20873
+ } catch {}
20874
+ }
20875
+
20876
+ // src/telemetry/invocation.ts
20877
+ var COMMAND_EVENT = "cli_command_executed";
20878
+ var LIBRARY_NAME = "sim-cli";
20879
+ var EXCLUDED_ROOT_COMMAND = "telemetry";
20880
+ var USAGE_DATA_DOCS_URL = "https://docs.sim.ai/cli/usage-data";
20881
+ var FIRST_RUN_NOTICE = [
20882
+ "Sim collects anonymous usage data to improve the CLI: which commands run, whether",
20883
+ "they succeed, and how long they take. Nothing you type is sent.",
20884
+ `Learn more: ${USAGE_DATA_DOCS_URL}`,
20885
+ "Turn it off: sim telemetry disable",
20886
+ ""
20887
+ ].join(`
20888
+ `);
20889
+ function commandPath2(command) {
20890
+ const names = [];
20891
+ for (let current = command;current?.parent; current = current.parent) {
20892
+ names.unshift(current.name());
20893
+ }
20894
+ return names;
20895
+ }
20896
+ function typedFlags(command) {
20897
+ const flags = new Set;
20898
+ for (let current = command;current; current = current.parent) {
20899
+ for (const option of current.options) {
20900
+ if (current.getOptionValueSource(option.attributeName()) !== "cli")
20901
+ continue;
20902
+ const name = option.long ?? option.short;
20903
+ if (name)
20904
+ flags.add(name);
20905
+ }
20906
+ }
20907
+ return [...flags];
20908
+ }
20909
+ function endpointKind(command) {
20910
+ try {
20911
+ const hostname = new URL(profileFrom(command).endpoint).hostname;
20912
+ return hostname === "sim.ai" || hostname.endsWith(".sim.ai") ? "hosted" : "self_hosted";
20913
+ } catch {
20914
+ return;
20915
+ }
20916
+ }
20917
+ function failureProperties(error) {
20918
+ if (error === undefined)
20919
+ return {};
20920
+ if (!(error instanceof Error))
20921
+ return { error_name: "unknown" };
20922
+ const properties = { error_name: error.name };
20923
+ if (error instanceof SimApiError) {
20924
+ if (error.code)
20925
+ properties.error_code = error.code;
20926
+ if (error.status > 0)
20927
+ properties.http_status = error.status;
20928
+ }
20929
+ return properties;
20930
+ }
20931
+ var listenForProcessExit = (listener) => {
20932
+ process.once("exit", listener);
20933
+ };
20934
+ function createCommandTelemetry(options = {}) {
20935
+ const env = options.env ?? process.env;
20936
+ const ingestTarget = options.ingestTarget ?? builtInIngestTarget;
20937
+ const send = options.send ?? sendCapture;
20938
+ const now = options.now ?? (() => new Date);
20939
+ const elapsed = options.elapsed ?? (() => performance.now());
20940
+ const stdoutIsTty = options.stdoutIsTty ?? process.stdout.isTTY === true;
20941
+ const stderrIsTty = options.stderrIsTty ?? process.stderr.isTTY === true;
20942
+ const write = options.write ?? ((message) => void process.stderr.write(message));
20943
+ const onExit = options.onExit ?? listenForProcessExit;
20944
+ let recorded;
20945
+ function isReportable(state) {
20946
+ return telemetryStatus({ env, state, configured: ingestTarget() !== undefined }).enabled;
20947
+ }
20948
+ function showNoticeIfDue(state) {
20949
+ if (state.noticeShownAt || !stderrIsTty || isCi(env))
20950
+ return false;
20951
+ write(FIRST_RUN_NOTICE);
20952
+ writeTelemetryState({ ...state, noticeShownAt: now().toISOString() });
20953
+ return true;
20954
+ }
20955
+ function complete(outcome) {
20956
+ const invocation = recorded;
20957
+ recorded = undefined;
20958
+ if (!invocation || invocation.noticeShown)
20959
+ return;
20960
+ const target = ingestTarget();
20961
+ if (!target)
20962
+ return;
20963
+ const state = loadTelemetryState();
20964
+ if (!isReportable(state))
20965
+ return;
20966
+ const timestamp = now();
20967
+ const session = nextSession(state, timestamp);
20968
+ writeTelemetryState({ ...state, session });
20969
+ const properties = {
20970
+ $lib: LIBRARY_NAME,
20971
+ $lib_version: CLI_VERSION,
20972
+ $process_person_profile: false,
20973
+ $session_id: session.id,
20974
+ session_sequence: session.sequence,
20975
+ surface: "cli",
20976
+ command: invocation.command,
20977
+ flags: invocation.flags,
20978
+ arg_count: invocation.argCount,
20979
+ exit_code: outcome.exitCode,
20980
+ duration_ms: Math.round(elapsed()),
20981
+ ...failureProperties(outcome.error),
20982
+ cli_version: CLI_VERSION,
20983
+ node_version: process.versions.node,
20984
+ os: process.platform,
20985
+ arch: process.arch,
20986
+ is_tty: stdoutIsTty,
20987
+ is_ci: isCi(env)
20988
+ };
20989
+ const kind = endpointKind(invocation.action);
20990
+ if (kind)
20991
+ properties.endpoint_kind = kind;
20992
+ const agent = detectCodingAgent(env);
20993
+ if (agent)
20994
+ properties.coding_agent = agent;
20995
+ send(target, {
20996
+ api_key: target.key,
20997
+ event: COMMAND_EVENT,
20998
+ distinct_id: state.deviceId,
20999
+ timestamp: timestamp.toISOString(),
21000
+ properties
21001
+ });
21002
+ }
21003
+ return {
21004
+ observe(program) {
21005
+ program.hook("preAction", (_root, action) => {
21006
+ const path = commandPath2(action);
21007
+ if (path[0] === EXCLUDED_ROOT_COMMAND || !isReportable({}))
21008
+ return;
21009
+ const state = loadTelemetryState();
21010
+ if (!isReportable(state))
21011
+ return;
21012
+ recorded = {
21013
+ action,
21014
+ command: path.join(" "),
21015
+ flags: typedFlags(action),
21016
+ argCount: action.args.length,
21017
+ state,
21018
+ noticeShown: showNoticeIfDue(state)
21019
+ };
21020
+ });
21021
+ onExit((exitCode) => complete({ exitCode }));
21022
+ },
21023
+ complete
21024
+ };
21025
+ }
21026
+ // src/commands/telemetry.ts
21027
+ function describe(status) {
21028
+ if (status.enabled)
21029
+ return "Usage reporting is on.";
21030
+ switch (status.reason) {
21031
+ case "do_not_track":
21032
+ return `Usage reporting is off: ${DO_NOT_TRACK_VARIABLE} is set.`;
21033
+ case "environment":
21034
+ return `Usage reporting is off: ${TELEMETRY_DISABLED_VARIABLE} is set.`;
21035
+ case "setting":
21036
+ return "Usage reporting is off. Turn it on with: sim telemetry enable";
21037
+ case "unconfigured":
21038
+ return "Usage reporting is off: this build has no reporting destination.";
21039
+ }
21040
+ }
21041
+ function currentStatus() {
21042
+ return telemetryStatus({
21043
+ env: process.env,
21044
+ state: loadTelemetryState(),
21045
+ configured: builtInIngestTarget() !== undefined
21046
+ });
21047
+ }
21048
+ function setEnabled(enabled) {
21049
+ writeTelemetryState({ ...loadTelemetryState(), enabled });
21050
+ console.log(describe(currentStatus()));
21051
+ }
21052
+ function telemetryCommand() {
21053
+ const telemetry = new Command("telemetry").description("Control anonymous usage reporting");
21054
+ telemetry.command("status").description("Show whether usage reporting is on, and why not if it is off").action(() => {
21055
+ console.log(describe(currentStatus()));
21056
+ console.log(`Learn more: ${USAGE_DATA_DOCS_URL}`);
21057
+ });
21058
+ telemetry.command("enable").description("Turn usage reporting on for this machine").action(() => setEnabled(true));
21059
+ telemetry.command("disable").description("Turn usage reporting off for this machine").action(() => setEnabled(false));
21060
+ return telemetry;
21061
+ }
20629
21062
 
20630
21063
  // src/program.ts
20631
21064
  var PROGRAM_DESCRIPTION = "Talk to the Sim API from your terminal";
@@ -20702,6 +21135,7 @@ function buildProgram(options = {}) {
20702
21135
  program.addCommand(configureCommand());
20703
21136
  const update = updateCommand();
20704
21137
  program.addCommand(update);
21138
+ program.addCommand(telemetryCommand());
20705
21139
  for (const command of buildGeneratedCommands()) {
20706
21140
  program.addCommand(command);
20707
21141
  }
@@ -20720,47 +21154,56 @@ function buildProgram(options = {}) {
20720
21154
  }
20721
21155
 
20722
21156
  // src/index.ts
21157
+ function explainFailure(error, program) {
21158
+ if (error instanceof ProfileConfigError || error instanceof CliUpdateError) {
21159
+ console.error(source_default.red(`Error: ${sanitize(error.message)}`));
21160
+ return 1;
21161
+ }
21162
+ if (isRequestTimeout(error)) {
21163
+ console.error(source_default.red(`Error: the request timed out. ${RAISE_TIMEOUT_HINT}`));
21164
+ return 1;
21165
+ }
21166
+ if (error instanceof SimApiError) {
21167
+ let output = program.opts().output;
21168
+ try {
21169
+ output = clientFrom(program).profile.output;
21170
+ } catch {}
21171
+ if (output === "json" || output === "yaml") {
21172
+ const payload = {
21173
+ error: {
21174
+ code: error.code ?? "CLI_ERROR",
21175
+ message: error.message,
21176
+ ...error.details === undefined ? {} : { details: error.details }
21177
+ }
21178
+ };
21179
+ process.stderr.write(output === "json" ? `${JSON.stringify(payload)}
21180
+ ` : dump(payload));
21181
+ return error.exitCode;
21182
+ }
21183
+ console.error(source_default.red(`Error: ${sanitize(error.message)}`));
21184
+ if (error.code)
21185
+ console.error(source_default.dim(` code: ${sanitize(error.code)}`));
21186
+ if (error.details !== undefined) {
21187
+ for (const line of formatApiErrorDetails(error.details)) {
21188
+ console.error(source_default.dim(sanitize(line)));
21189
+ }
21190
+ }
21191
+ return error.exitCode;
21192
+ }
21193
+ return null;
21194
+ }
20723
21195
  async function main() {
21196
+ const telemetry = createCommandTelemetry();
20724
21197
  const program = buildProgram();
21198
+ telemetry.observe(program);
20725
21199
  try {
20726
21200
  await program.parseAsync(process.argv);
20727
21201
  } catch (error) {
20728
- if (error instanceof ProfileConfigError || error instanceof CliUpdateError) {
20729
- console.error(source_default.red(`Error: ${sanitize(error.message)}`));
20730
- process.exit(1);
20731
- }
20732
- if (isRequestTimeout(error)) {
20733
- console.error(source_default.red(`Error: the request timed out. ${RAISE_TIMEOUT_HINT}`));
20734
- process.exit(1);
20735
- }
20736
- if (error instanceof SimApiError) {
20737
- let output = program.opts().output;
20738
- try {
20739
- output = clientFrom(program).profile.output;
20740
- } catch {}
20741
- if (output === "json" || output === "yaml") {
20742
- const payload = {
20743
- error: {
20744
- code: error.code ?? "CLI_ERROR",
20745
- message: error.message,
20746
- ...error.details === undefined ? {} : { details: error.details }
20747
- }
20748
- };
20749
- process.stderr.write(output === "json" ? `${JSON.stringify(payload)}
20750
- ` : dump(payload));
20751
- process.exit(error.exitCode);
20752
- }
20753
- console.error(source_default.red(`Error: ${sanitize(error.message)}`));
20754
- if (error.code)
20755
- console.error(source_default.dim(` code: ${sanitize(error.code)}`));
20756
- if (error.details !== undefined) {
20757
- for (const line of formatApiErrorDetails(error.details)) {
20758
- console.error(source_default.dim(sanitize(line)));
20759
- }
20760
- }
20761
- process.exit(error.exitCode);
20762
- }
20763
- throw error;
21202
+ const exitCode = explainFailure(error, program);
21203
+ telemetry.complete({ exitCode: exitCode ?? 1, error });
21204
+ if (exitCode === null)
21205
+ throw error;
21206
+ process.exit(exitCode);
20764
21207
  }
20765
21208
  }
20766
21209
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.1.14-preview.116.1",
3
+ "version": "2.1.14",
4
4
  "description": "Sim CLI - talk to the Sim API from your terminal",
5
5
  "type": "module",
6
6
  "imports": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "prebuild": "bun run clean",
14
- "build": "bun build src/index.ts --target=node --format=esm --packages=bundle --reject-unresolved --outfile=dist/index.js",
14
+ "build": "bun build src/index.ts --target=node --format=esm --packages=bundle --reject-unresolved --env='SIM_CLI_TELEMETRY_*' --outfile=dist/index.js",
15
15
  "clean": "bun -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
16
16
  "type-check": "tsc --noEmit",
17
17
  "lint": "biome check --write --unsafe .",