autotel-cli 0.15.2 → 0.16.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 (2) hide show
  1. package/dist/index.js +438 -199
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import * as semver from "semver";
14
14
  import { Project, SyntaxKind } from "ts-morph";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { JsonTraceSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
17
- import { buildCollectorGuide, buildInstrumentationGuide, clearSemanticConventionCache, collectUsage, detectAnomalies, discoverLogFields, discoverServices, discoverTraceFields, findRootCause, getCollectorComponentReadme, getCollectorComponentSchema, getModelStats, getSemanticConventionNamespace, listCollectorComponents, listCollectorVersions, listModels, listSemanticConventionNamespaces, listToolUsage, pickErrorMessage, rankExpensiveTraces, rankSlowTraces, refreshCollectorCatalog, resolveCollectorVersion, scoreSpan, suggestCollectorConfig, suggestInstrumentationFixes, toLogSearchQuery, toMetricSearchQuery, toSpanSearchQuery, toTraceSearchQuery, validateCollectorComponentConfig, validateOtlpReceiverConfig } from "autotel-mcp";
17
+ import { EstimateInputError, buildCollectorGuide, buildInstrumentationGuide, clearSemanticConventionCache, collectUsage, detectAnomalies, discoverLogFields, discoverServices, discoverTraceFields, estimateCost, findRootCause, getCollectorComponentReadme, getCollectorComponentSchema, getModelStats, getSemanticConventionNamespace, listCollectorComponents, listCollectorVersions, listModels, listSemanticConventionNamespaces, listToolUsage, pickErrorMessage, rankExpensiveTraces, rankSlowTraces, refreshCollectorCatalog, resolveCollectorVersion, scoreSpan, suggestCollectorConfig, suggestInstrumentationFixes, toLogSearchQuery, toMetricSearchQuery, toSpanSearchQuery, toTraceSearchQuery, validateCollectorComponentConfig, validateOtlpReceiverConfig } from "autotel-mcp";
18
18
  import { AsyncLocalStorage } from "node:async_hooks";
19
19
  import { appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
20
20
  import { homedir } from "node:os";
@@ -143,16 +143,30 @@ function findAllUpward(startDir, filenames) {
143
143
  return found;
144
144
  }
145
145
  //#endregion
146
+ //#region src/lib/values.ts
147
+ /** The value as an object, or undefined when it is anything else. */
148
+ function asRecord(value) {
149
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
150
+ }
151
+ /** The string the value carries, or undefined when it carries anything else. */
152
+ function asString(value) {
153
+ return typeof value === "string" ? value : void 0;
154
+ }
155
+ /** One field of a value, when the value is an object at all. */
156
+ function readProperty(source, key) {
157
+ return asRecord(source)?.[key];
158
+ }
159
+ //#endregion
146
160
  //#region src/lib/package-manager.ts
147
161
  /**
148
162
  * Lockfile to package manager mapping
149
163
  */
150
- const LOCKFILE_MAP = {
151
- "pnpm-lock.yaml": "pnpm",
152
- "bun.lockb": "bun",
153
- "yarn.lock": "yarn",
154
- "package-lock.json": "npm"
155
- };
164
+ const LOCKFILE_MAP = /* @__PURE__ */ new Map([
165
+ ["pnpm-lock.yaml", "pnpm"],
166
+ ["bun.lockb", "bun"],
167
+ ["yarn.lock", "yarn"],
168
+ ["package-lock.json", "npm"]
169
+ ]);
156
170
  /**
157
171
  * All lockfile names in priority order
158
172
  */
@@ -171,10 +185,6 @@ const PM_PRIORITY = [
171
185
  "yarn",
172
186
  "npm"
173
187
  ];
174
- /**
175
- * Detect package manager from nearest lockfile
176
- * Algorithm: Find closest lockfile to cwd, working upward
177
- */
178
188
  function detectPackageManager(startDir) {
179
189
  const foundLockfiles = findAllUpward(startDir, LOCKFILES);
180
190
  if (foundLockfiles.size === 0) return {
@@ -186,7 +196,7 @@ function detectPackageManager(startDir) {
186
196
  let closestPM = "npm";
187
197
  for (const [lockfileName, lockfilePath] of foundLockfiles) {
188
198
  const depth = lockfilePath.split(path$1.sep).length;
189
- const pm = LOCKFILE_MAP[lockfileName];
199
+ const pm = LOCKFILE_MAP.get(lockfileName);
190
200
  if (pm === void 0) continue;
191
201
  if (depth > closestDepth || depth === closestDepth && PM_PRIORITY.indexOf(pm) < PM_PRIORITY.indexOf(closestPM)) {
192
202
  closestDepth = depth;
@@ -212,9 +222,6 @@ function getInstallCommand(pm, packages, options = {}) {
212
222
  default: return `npm install${dev ? " --save-dev" : ""} ${pkgList}`;
213
223
  }
214
224
  }
215
- /**
216
- * Detect workspace root markers
217
- */
218
225
  function detectWorkspaceRoot(startDir) {
219
226
  let currentDir = path$1.resolve(startDir);
220
227
  const root = path$1.parse(currentDir).root;
@@ -229,7 +236,7 @@ function detectWorkspaceRoot(startDir) {
229
236
  };
230
237
  const pkgJsonContent = readFileSafe(path$1.join(currentDir, "package.json"));
231
238
  if (pkgJsonContent) try {
232
- if (JSON.parse(pkgJsonContent).workspaces) {
239
+ if (readProperty(JSON.parse(pkgJsonContent), "workspaces")) {
233
240
  const hasYarnLock = fileExists(path$1.join(currentDir, "yarn.lock"));
234
241
  return {
235
242
  workspaceRoot: currentDir,
@@ -739,7 +746,7 @@ async function checkEnvVarsPresent(packageRoot, varNames, specificFile) {
739
746
  *
740
747
  * Loggers handled separately (see `detectLoggers`).
741
748
  */
742
- const DEP_TO_PRESET = {
749
+ const DEP_TO_PRESET = new Map(Object.entries({
743
750
  "@sentry/node": "sentry",
744
751
  "@sentry/bun": "sentry",
745
752
  hono: "hono",
@@ -753,6 +760,7 @@ const DEP_TO_PRESET = {
753
760
  nuxt: "nuxt",
754
761
  "posthog-node": "posthog",
755
762
  "@posthog/node": "posthog",
763
+ "posthog-js": "posthog-web",
756
764
  mixpanel: "mixpanel",
757
765
  "@amplitude/analytics-node": "amplitude",
758
766
  "@segment/analytics-node": "segment",
@@ -770,12 +778,12 @@ const DEP_TO_PRESET = {
770
778
  ioredis: "auto-instr",
771
779
  "@aws-sdk/client-s3": "auto-instr",
772
780
  graphql: "auto-instr"
773
- };
774
- const LOGGER_DEPS = {
781
+ }));
782
+ const LOGGER_DEPS = new Map(Object.entries({
775
783
  pino: "pino",
776
784
  winston: "winston",
777
785
  bunyan: "bunyan"
778
- };
786
+ }));
779
787
  /** Backends inferable from env-var keys (values not used — only key presence). */
780
788
  const ENV_KEY_TO_BACKEND = [
781
789
  {
@@ -828,7 +836,7 @@ function mergeDeps(target, root) {
828
836
  */
829
837
  function detectLoggers(deps) {
830
838
  const present = [];
831
- for (const [name, kind] of Object.entries(LOGGER_DEPS)) if (deps.has(name)) present.push(kind);
839
+ for (const [name, kind] of LOGGER_DEPS) if (deps.has(name)) present.push(kind);
832
840
  if (present.length === 0) return {
833
841
  primary: null,
834
842
  autoInstrument: []
@@ -916,8 +924,8 @@ function detectInProject(opts) {
916
924
  const presetSet = /* @__PURE__ */ new Set();
917
925
  const autoInstrDeps = [];
918
926
  for (const [name, info] of deps.entries()) {
919
- const mapping = DEP_TO_PRESET[name];
920
- if (mapping === void 0 && LOGGER_DEPS[name] === void 0) continue;
927
+ const mapping = DEP_TO_PRESET.get(name);
928
+ if (mapping === void 0 && !LOGGER_DEPS.has(name)) continue;
921
929
  detectedPackages.push({
922
930
  name,
923
931
  version: info.version,
@@ -1312,7 +1320,11 @@ const local = {
1312
1320
  //#endregion
1313
1321
  //#region src/presets/subscribers/posthog.ts
1314
1322
  /**
1315
- * PostHog subscriber preset
1323
+ * PostHog subscriber preset — product events posted from the server.
1324
+ *
1325
+ * The browser counterpart is the `posthog-web` plugin preset, which joins
1326
+ * traces to the session PostHog is recording. Both live in `autotel-posthog`;
1327
+ * this one needs `autotel-subscribers` too, for the base class it extends.
1316
1328
  */
1317
1329
  const posthog = {
1318
1330
  name: "PostHog",
@@ -1320,7 +1332,11 @@ const posthog = {
1320
1332
  type: "subscriber",
1321
1333
  description: "Send events to PostHog for product analytics",
1322
1334
  packages: {
1323
- required: ["autotel-subscribers", "posthog-node"],
1335
+ required: [
1336
+ "autotel-posthog",
1337
+ "autotel-subscribers",
1338
+ "posthog-node"
1339
+ ],
1324
1340
  optional: [],
1325
1341
  devOnly: []
1326
1342
  },
@@ -1339,7 +1355,7 @@ const posthog = {
1339
1355
  }]
1340
1356
  },
1341
1357
  imports: [{
1342
- source: "autotel-subscribers/posthog",
1358
+ source: "autotel-posthog/subscriber",
1343
1359
  specifiers: ["PostHogSubscriber"]
1344
1360
  }],
1345
1361
  configBlock: {
@@ -1353,6 +1369,54 @@ const posthog = {
1353
1369
  nextSteps: ["Set POSTHOG_API_KEY from PostHog Project Settings", "Events will be sent to PostHog as custom events"]
1354
1370
  };
1355
1371
  //#endregion
1372
+ //#region src/presets/plugins/posthog-web.ts
1373
+ /**
1374
+ * PostHog browser preset — joins the trace to the session PostHog is recording.
1375
+ *
1376
+ * Distinct from the `posthog` subscriber preset, which posts events from a
1377
+ * server. A front end running `posthog-js` already has a session id, a person,
1378
+ * and often a replay; what it lacks is any link between those and its traces.
1379
+ * This wires both directions: spans carry the session, person and (on failures)
1380
+ * a replay link, and PostHog events carry the trace id that explains them.
1381
+ */
1382
+ const posthogWeb = {
1383
+ name: "PostHog (browser)",
1384
+ slug: "posthog-web",
1385
+ type: "plugin",
1386
+ description: "Join browser traces to PostHog sessions, replays and events (both directions)",
1387
+ packages: {
1388
+ required: [
1389
+ "autotel-web",
1390
+ "autotel-posthog",
1391
+ "posthog-js"
1392
+ ],
1393
+ optional: [],
1394
+ devOnly: []
1395
+ },
1396
+ env: {
1397
+ required: [],
1398
+ optional: []
1399
+ },
1400
+ imports: [],
1401
+ configBlock: {
1402
+ type: "plugin",
1403
+ code: `// PostHog join: wire this in your browser entry, not here —
1404
+ // import posthog from 'posthog-js';
1405
+ // import { initFull } from 'autotel-web/full';
1406
+ // import { joinPostHog } from 'autotel-posthog';
1407
+ //
1408
+ // posthog.init('<key>');
1409
+ // initFull({ service: '<app>', endpoint, spanEnrichers: [joinPostHog(posthog)] });`,
1410
+ section: "PLUGIN_INIT"
1411
+ },
1412
+ nextSteps: [
1413
+ "Browser entry: initFull({ ..., spanEnrichers: [joinPostHog(posthog)] }) — one call wires both directions",
1414
+ "Spans now carry session.id and user.id from PostHog; failed spans carry session.replay.url",
1415
+ "For a clickable link back: joinPostHog(posthog, { traceUrl: ({ traceId }) => `...${traceId}` })",
1416
+ "Name any feature flags worth slicing by: joinPostHog(posthog, { featureFlags: [\"my-flag\"] })"
1417
+ ]
1418
+ };
1419
+ //#endregion
1356
1420
  //#region src/presets/subscribers/mixpanel.ts
1357
1421
  /**
1358
1422
  * Mixpanel subscriber preset
@@ -2053,7 +2117,8 @@ const plugins = /* @__PURE__ */ new Map([
2053
2117
  ["nestjs", nestjs],
2054
2118
  ["sveltekit", sveltekit],
2055
2119
  ["elysia", elysia],
2056
- ["nuxt", nuxt]
2120
+ ["nuxt", nuxt],
2121
+ ["posthog-web", posthogWeb]
2057
2122
  ]);
2058
2123
  /**
2059
2124
  * Platform presets registry
@@ -2529,14 +2594,14 @@ const REDACTED = "[REDACTED]";
2529
2594
  function redact(value) {
2530
2595
  if (value === null || value === void 0) return value;
2531
2596
  if (Array.isArray(value)) return value.map((v) => redact(v));
2532
- if (typeof value === "object") {
2597
+ const record = asRecord(value);
2598
+ if (record) {
2533
2599
  const out = {};
2534
- for (const [k, v] of Object.entries(value)) if (typeof v === "string" && SECRET_KEY_PATTERN.test(k)) out[k] = REDACTED;
2535
- else out[k] = redact(v);
2600
+ for (const [key, field] of Object.entries(record)) out[key] = asString(field) !== void 0 && SECRET_KEY_PATTERN.test(key) ? REDACTED : redact(field);
2536
2601
  return out;
2537
2602
  }
2538
- if (typeof value === "string" && SECRET_VALUE_PATTERN.test(value)) return REDACTED;
2539
- return value;
2603
+ const text = asString(value);
2604
+ return text !== void 0 && SECRET_VALUE_PATTERN.test(text) ? REDACTED : value;
2540
2605
  }
2541
2606
  /**
2542
2607
  * Print a JSON payload to stdout. If `--output-file` was configured, also
@@ -2812,7 +2877,7 @@ function readPlanFromFile(filePath) {
2812
2877
  throw new AutotelError({
2813
2878
  type: "validation",
2814
2879
  code: AutotelErrorCodes.E_INVALID_PLAN,
2815
- message: `Plan file is not valid JSON: ${error.message}`
2880
+ message: `Plan file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
2816
2881
  });
2817
2882
  }
2818
2883
  }
@@ -2828,7 +2893,7 @@ async function readPlanFromInput(input) {
2828
2893
  throw new AutotelError({
2829
2894
  type: "validation",
2830
2895
  code: AutotelErrorCodes.E_INVALID_INPUT,
2831
- message: `stdin did not contain valid JSON: ${error.message}`
2896
+ message: `stdin did not contain valid JSON: ${error instanceof Error ? error.message : String(error)}`
2832
2897
  });
2833
2898
  }
2834
2899
  }
@@ -4880,7 +4945,7 @@ const traceRule = {
4880
4945
  question: "Does this entry point produce a span?",
4881
4946
  weight: 40,
4882
4947
  appliesTo: { kinds: HANDLER_KINDS },
4883
- fix: ({ target }) => `export const handler = trace('${target.method?.toLowerCase() ?? "handle"} ${target.path}', async () => { /* … */ });`,
4948
+ fix: ({ target }) => `export const handler = instrument({ key: '${target.method?.toLowerCase() ?? "handle"} ${target.path}', fn: async () => { /* … */ } });`,
4884
4949
  check(context) {
4885
4950
  if (usesAutotelApi(context, SPAN_WRAPPERS)) return null;
4886
4951
  if (context.project.ambientTracing) return null;
@@ -5057,7 +5122,7 @@ const RULES = [
5057
5122
  kinds: HANDLER_KINDS,
5058
5123
  when: ({ project }) => project.hasLlmDependency
5059
5124
  },
5060
- fix: () => "traceGenAI({ operation: 'chat', model }, async () => client.chat.completions.create(…));",
5125
+ fix: () => "traceGenAI({ operation: 'chat', model })((ctx) => async () => client.chat.completions.create(…));",
5061
5126
  check(context) {
5062
5127
  const llmCall = LLM_CALLS.flatMap((name) => context.facts.memberCalls(name)).find((call) => call.root !== null && call.root !== "JSON");
5063
5128
  if (!llmCall) return null;
@@ -6703,6 +6768,70 @@ const COMMANDS = [
6703
6768
  }
6704
6769
  ]
6705
6770
  },
6771
+ {
6772
+ name: "estimate",
6773
+ description: "Estimate what a month of telemetry costs, before and after canonical log lines",
6774
+ args: [],
6775
+ flags: [
6776
+ ...GLOBAL_FLAGS,
6777
+ ...AGENT_FLAGS,
6778
+ {
6779
+ name: "--requests-per-month",
6780
+ takesValue: true,
6781
+ description: "Requests the application serves per month (required)"
6782
+ },
6783
+ {
6784
+ name: "--per-gb",
6785
+ takesValue: true,
6786
+ description: "USD per gigabyte ingested by your provider (required — no rate is assumed)"
6787
+ },
6788
+ {
6789
+ name: "--log-lines-per-request",
6790
+ takesValue: true,
6791
+ description: "Log lines written per request today (default 4)"
6792
+ },
6793
+ {
6794
+ name: "--spans-per-request",
6795
+ takesValue: true,
6796
+ description: "Spans exported per request after instrumenting (default 0)"
6797
+ },
6798
+ {
6799
+ name: "--per-million-events",
6800
+ takesValue: true,
6801
+ description: "USD per million events indexed, if metered separately"
6802
+ },
6803
+ {
6804
+ name: "--keep-percent",
6805
+ takesValue: true,
6806
+ description: "Traffic kept after sampling, applied to both shapes (default 100)"
6807
+ }
6808
+ ],
6809
+ mutating: false,
6810
+ network: false,
6811
+ writesFiles: false,
6812
+ supportsDryRun: false,
6813
+ requiresPackageJson: false,
6814
+ mayReadEnv: false,
6815
+ supportsJson: true,
6816
+ examples: [
6817
+ {
6818
+ description: "Ten million requests a month at four log lines each",
6819
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10"
6820
+ },
6821
+ {
6822
+ description: "Include the spans instrumenting will add",
6823
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --spans-per-request 6 --json"
6824
+ },
6825
+ {
6826
+ description: "A provider that meters indexed events as well as bytes",
6827
+ command: "autotel estimate --requests-per-month 300000000 --per-gb 0.50 --per-million-events 2.50"
6828
+ },
6829
+ {
6830
+ description: "What head sampling at 10% does to the bill",
6831
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --keep-percent 10"
6832
+ }
6833
+ ]
6834
+ },
6706
6835
  {
6707
6836
  name: "codemod trace",
6708
6837
  description: "Wrap functions in trace() with span name from function/variable/method name",
@@ -7289,26 +7418,40 @@ function runSchemaOutputs(opts) {
7289
7418
  printJson({
7290
7419
  ok: true,
7291
7420
  command: "autotel schema outputs",
7292
- outputs: { "autotel init --json": {
7293
- ok: "boolean",
7294
- command: "string",
7295
- detected: {
7296
- packages: "array of { name, version, resolution: \"target\" | \"workspace-root\" }",
7297
- logger: "\"pino\" | \"winston\" | \"bunyan\" | null",
7298
- backend: "{ source: \"env\" | \"wrangler\" | \"deps\" | \"prompt\" | \"default\", value: string }",
7299
- platform: "\"cloudflare\" | \"aws-lambda\" | \"edge\" | null"
7300
- },
7301
- plan: {
7302
- presets: "string[] (slugs)",
7303
- packagesToInstall: {
7304
- prod: "string[]",
7305
- dev: "string[]"
7421
+ outputs: {
7422
+ "autotel init --json": {
7423
+ ok: "boolean",
7424
+ command: "string",
7425
+ detected: {
7426
+ packages: "array of { name, version, resolution: \"target\" | \"workspace-root\" }",
7427
+ logger: "\"pino\" | \"winston\" | \"bunyan\" | null",
7428
+ backend: "{ source: \"env\" | \"wrangler\" | \"deps\" | \"prompt\" | \"default\", value: string }",
7429
+ platform: "\"cloudflare\" | \"aws-lambda\" | \"edge\" | null"
7306
7430
  },
7307
- filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
7308
- envVars: "array of { name, sensitive, action }"
7431
+ plan: {
7432
+ presets: "string[] (slugs)",
7433
+ packagesToInstall: {
7434
+ prod: "string[]",
7435
+ dev: "string[]"
7436
+ },
7437
+ filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
7438
+ envVars: "array of { name, sensitive, action }"
7439
+ },
7440
+ nextSteps: "string[]"
7309
7441
  },
7310
- nextSteps: "string[]"
7311
- } }
7442
+ "autotel estimate --json": {
7443
+ ok: "boolean",
7444
+ command: "string",
7445
+ estimate: {
7446
+ currency: "\"USD\"",
7447
+ before: "{ events: number, gb: number, cost: number }",
7448
+ after: "{ events: number, gb: number, cost: number }",
7449
+ saved: "number (USD/month, negative when instrumenting costs more)",
7450
+ savedPercent: "number",
7451
+ basis: "{ logLineBytes, canonicalLineBytes, spanBytes, bytesFrom: \"measured\" | \"caller\", perGb, perMillionEvents, keepPercent, bytesPerGb }"
7452
+ }
7453
+ }
7454
+ }
7312
7455
  });
7313
7456
  }
7314
7457
  /** `autotel commands [--json]` — compact listing. */
@@ -7476,6 +7619,23 @@ function toInvestigateError(command, error) {
7476
7619
  });
7477
7620
  }
7478
7621
  //#endregion
7622
+ //#region src/lib/opts.ts
7623
+ /** A flag declared without an argument parser: Commander yields its string. */
7624
+ function stringOpt(o, key) {
7625
+ const value = o[key];
7626
+ return typeof value === "string" && value.length > 0 ? value : void 0;
7627
+ }
7628
+ /** A flag declared with `intArg` or another numeric parser. */
7629
+ function numberOpt(o, key) {
7630
+ const value = o[key];
7631
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7632
+ }
7633
+ /** A boolean flag: present or absent. */
7634
+ function boolOpt(o, key) {
7635
+ const value = o[key];
7636
+ return typeof value === "boolean" ? value : void 0;
7637
+ }
7638
+ //#endregion
7479
7639
  //#region src/commands/investigate/cli-helpers.ts
7480
7640
  /**
7481
7641
  * Shared commander wiring for investigate commands. Each command group's
@@ -7501,22 +7661,22 @@ function addStaticFlags(cmd) {
7501
7661
  function backendFlagsFromOpts(opts) {
7502
7662
  return {
7503
7663
  backend: opts.backend,
7504
- jaegerBaseUrl: opts.jaegerBaseUrl,
7505
- tempoBaseUrl: opts.tempoBaseUrl,
7506
- prometheusBaseUrl: opts.prometheusBaseUrl,
7507
- lokiBaseUrl: opts.lokiBaseUrl,
7508
- collectorPort: opts.collectorPort,
7509
- fixturePath: opts.fixturePath,
7510
- logfireBaseUrl: opts.logfireBaseUrl,
7511
- datadogSite: opts.datadogSite,
7512
- signozBaseUrl: opts.signozBaseUrl,
7513
- outputFile: opts.outputFile,
7664
+ jaegerBaseUrl: stringOpt(opts, "jaegerBaseUrl"),
7665
+ tempoBaseUrl: stringOpt(opts, "tempoBaseUrl"),
7666
+ prometheusBaseUrl: stringOpt(opts, "prometheusBaseUrl"),
7667
+ lokiBaseUrl: stringOpt(opts, "lokiBaseUrl"),
7668
+ collectorPort: numberOpt(opts, "collectorPort"),
7669
+ fixturePath: stringOpt(opts, "fixturePath"),
7670
+ logfireBaseUrl: stringOpt(opts, "logfireBaseUrl"),
7671
+ datadogSite: stringOpt(opts, "datadogSite"),
7672
+ signozBaseUrl: stringOpt(opts, "signozBaseUrl"),
7673
+ outputFile: stringOpt(opts, "outputFile"),
7514
7674
  noSecrets: opts.secretsInOutput === false
7515
7675
  };
7516
7676
  }
7517
7677
  function staticFlagsFromOpts(opts) {
7518
7678
  return {
7519
- outputFile: opts.outputFile,
7679
+ outputFile: stringOpt(opts, "outputFile"),
7520
7680
  noSecrets: opts.secretsInOutput === false
7521
7681
  };
7522
7682
  }
@@ -7528,12 +7688,12 @@ function addTimeWindowFlags(cmd) {
7528
7688
  }
7529
7689
  function windowFlagsFromOpts(opts) {
7530
7690
  return {
7531
- serviceName: opts.serviceName,
7532
- operationName: opts.operationName,
7533
- lookbackMinutes: opts.lookbackMinutes,
7534
- from: opts.from,
7535
- to: opts.to,
7536
- limit: opts.limit
7691
+ serviceName: stringOpt(opts, "serviceName"),
7692
+ operationName: stringOpt(opts, "operationName"),
7693
+ lookbackMinutes: numberOpt(opts, "lookbackMinutes"),
7694
+ from: stringOpt(opts, "from"),
7695
+ to: stringOpt(opts, "to"),
7696
+ limit: numberOpt(opts, "limit")
7537
7697
  };
7538
7698
  }
7539
7699
  //#endregion
@@ -7727,8 +7887,8 @@ function registerHealthCommands(program) {
7727
7887
  const opts = this.opts();
7728
7888
  await runHealth({
7729
7889
  ...backendFlagsFromOpts(opts),
7730
- otlpEndpoint: opts.otlpEndpoint,
7731
- freshnessTimeoutMs: opts.freshnessTimeoutMs,
7890
+ otlpEndpoint: stringOpt(opts, "otlpEndpoint"),
7891
+ freshnessTimeoutMs: numberOpt(opts, "freshnessTimeoutMs"),
7732
7892
  otlpEncoding: opts.otlpEncoding
7733
7893
  });
7734
7894
  });
@@ -7800,26 +7960,26 @@ function registerDiscoveryCommands(program) {
7800
7960
  const o = this.optsWithGlobals();
7801
7961
  await runDiscoverServices({
7802
7962
  ...backendFlagsFromOpts(o),
7803
- limitServices: o.limitServices,
7804
- traceSample: o.traceSample,
7805
- logSample: o.logSample,
7806
- metricSample: o.metricSample
7963
+ limitServices: numberOpt(o, "limitServices"),
7964
+ traceSample: numberOpt(o, "traceSample"),
7965
+ logSample: numberOpt(o, "logSample"),
7966
+ metricSample: numberOpt(o, "metricSample")
7807
7967
  });
7808
7968
  });
7809
7969
  const traceFieldsCmd = new Command("trace-fields").description("Discover trace/span field names from sampled traces").option("--search <text>", "Filter field names by substring").option("--sample-size <n>", "Trace sample size", intArg).action(async function() {
7810
7970
  const o = this.optsWithGlobals();
7811
7971
  await runDiscoverTraceFields({
7812
7972
  ...backendFlagsFromOpts(o),
7813
- search: o.search,
7814
- sampleSize: o.sampleSize
7973
+ search: stringOpt(o, "search"),
7974
+ sampleSize: numberOpt(o, "sampleSize")
7815
7975
  });
7816
7976
  });
7817
7977
  const logFieldsCmd = new Command("log-fields").description("Discover log field names from sampled logs").option("--search <text>", "Filter field names by substring").option("--sample-size <n>", "Log sample size", intArg).action(async function() {
7818
7978
  const o = this.optsWithGlobals();
7819
7979
  await runDiscoverLogFields({
7820
7980
  ...backendFlagsFromOpts(o),
7821
- search: o.search,
7822
- sampleSize: o.sampleSize
7981
+ search: stringOpt(o, "search"),
7982
+ sampleSize: numberOpt(o, "sampleSize")
7823
7983
  });
7824
7984
  });
7825
7985
  addBackendFlags(discoverCmd);
@@ -7856,62 +8016,62 @@ function registerQueryCommands(program) {
7856
8016
  const o = this.optsWithGlobals();
7857
8017
  await runQueryTraces({
7858
8018
  ...backendFlagsFromOpts(o),
7859
- serviceName: o.serviceName,
7860
- operationName: o.operationName,
7861
- lookbackMinutes: o.lookbackMinutes,
7862
- from: o.from,
7863
- to: o.to,
7864
- limit: o.limit,
7865
- errorOnly: o.errorOnly,
8019
+ serviceName: stringOpt(o, "serviceName"),
8020
+ operationName: stringOpt(o, "operationName"),
8021
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8022
+ from: stringOpt(o, "from"),
8023
+ to: stringOpt(o, "to"),
8024
+ limit: numberOpt(o, "limit"),
8025
+ errorOnly: boolOpt(o, "errorOnly"),
7866
8026
  statusCode: o.statusCode,
7867
- minDurationMs: o.minDurationMs,
7868
- maxDurationMs: o.maxDurationMs,
7869
- genAiSystem: o.genAiSystem,
7870
- genAiRequestModel: o.genAiRequestModel,
7871
- genAiResponseModel: o.genAiResponseModel
8027
+ minDurationMs: numberOpt(o, "minDurationMs"),
8028
+ maxDurationMs: numberOpt(o, "maxDurationMs"),
8029
+ genAiSystem: stringOpt(o, "genAiSystem"),
8030
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8031
+ genAiResponseModel: stringOpt(o, "genAiResponseModel")
7872
8032
  });
7873
8033
  });
7874
8034
  const spansCmd = addTimeWindowFlags(new Command("spans")).description("Search individual spans by service/op/status/tags/duration").option("--error-only", "Only spans with errors").option("--status-code <code>", "OK | ERROR | UNSET").option("--min-duration-ms <n>", "Minimum span duration", intArg).option("--max-duration-ms <n>", "Maximum span duration", intArg).action(async function() {
7875
8035
  const o = this.optsWithGlobals();
7876
8036
  await runQuerySpans({
7877
8037
  ...backendFlagsFromOpts(o),
7878
- serviceName: o.serviceName,
7879
- operationName: o.operationName,
7880
- lookbackMinutes: o.lookbackMinutes,
7881
- from: o.from,
7882
- to: o.to,
7883
- limit: o.limit,
7884
- errorOnly: o.errorOnly,
8038
+ serviceName: stringOpt(o, "serviceName"),
8039
+ operationName: stringOpt(o, "operationName"),
8040
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8041
+ from: stringOpt(o, "from"),
8042
+ to: stringOpt(o, "to"),
8043
+ limit: numberOpt(o, "limit"),
8044
+ errorOnly: boolOpt(o, "errorOnly"),
7885
8045
  statusCode: o.statusCode,
7886
- minDurationMs: o.minDurationMs,
7887
- maxDurationMs: o.maxDurationMs
8046
+ minDurationMs: numberOpt(o, "minDurationMs"),
8047
+ maxDurationMs: numberOpt(o, "maxDurationMs")
7888
8048
  });
7889
8049
  });
7890
8050
  const metricsCmd = addTimeWindowFlags(new Command("metrics")).description("List metric series").option("--metric-name <name>", "Filter by metric name").action(async function() {
7891
8051
  const o = this.optsWithGlobals();
7892
8052
  await runQueryMetrics({
7893
8053
  ...backendFlagsFromOpts(o),
7894
- metricName: o.metricName,
7895
- serviceName: o.serviceName,
7896
- lookbackMinutes: o.lookbackMinutes,
7897
- from: o.from,
7898
- to: o.to,
7899
- limit: o.limit
8054
+ metricName: stringOpt(o, "metricName"),
8055
+ serviceName: stringOpt(o, "serviceName"),
8056
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8057
+ from: stringOpt(o, "from"),
8058
+ to: stringOpt(o, "to"),
8059
+ limit: numberOpt(o, "limit")
7900
8060
  });
7901
8061
  });
7902
8062
  const logsCmd = addTimeWindowFlags(new Command("logs")).description("Search logs").option("--trace-id <id>", "Filter by trace id").option("--span-id <id>", "Filter by span id").option("--severity-text <text>", "Severity text").option("--text <text>", "Free-text search").action(async function() {
7903
8063
  const o = this.optsWithGlobals();
7904
8064
  await runQueryLogs({
7905
8065
  ...backendFlagsFromOpts(o),
7906
- serviceName: o.serviceName,
7907
- traceId: o.traceId,
7908
- spanId: o.spanId,
7909
- severityText: o.severityText,
7910
- text: o.text,
7911
- lookbackMinutes: o.lookbackMinutes,
7912
- from: o.from,
7913
- to: o.to,
7914
- limit: o.limit
8066
+ serviceName: stringOpt(o, "serviceName"),
8067
+ traceId: stringOpt(o, "traceId"),
8068
+ spanId: stringOpt(o, "spanId"),
8069
+ severityText: stringOpt(o, "severityText"),
8070
+ text: stringOpt(o, "text"),
8071
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8072
+ from: stringOpt(o, "from"),
8073
+ to: stringOpt(o, "to"),
8074
+ limit: numberOpt(o, "limit")
7915
8075
  });
7916
8076
  });
7917
8077
  addBackendFlags(queryCmd);
@@ -7966,8 +8126,8 @@ function registerTopologyCommands(program) {
7966
8126
  const o = this.optsWithGlobals();
7967
8127
  await runServiceMap({
7968
8128
  ...backendFlagsFromOpts(o),
7969
- lookbackMinutes: o.lookbackMinutes,
7970
- limit: o.limit
8129
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8130
+ limit: numberOpt(o, "limit")
7971
8131
  });
7972
8132
  });
7973
8133
  addBackendFlags(topologyCmd);
@@ -8084,9 +8244,9 @@ function registerDiagnoseCommands(program) {
8084
8244
  const o = this.optsWithGlobals();
8085
8245
  await runDiagnoseAnomalies({
8086
8246
  ...backendFlagsFromOpts(o),
8087
- service: o.service,
8088
- operation: o.operation,
8089
- lookbackMinutes: o.lookbackMinutes
8247
+ service: stringOpt(o, "service"),
8248
+ operation: stringOpt(o, "operation"),
8249
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8090
8250
  });
8091
8251
  });
8092
8252
  const rootCauseCmd = new Command("root-cause").description("Walk a trace tree to identify the bottleneck span").argument("<traceId>", "Trace ID").action(async function(traceId) {
@@ -8099,19 +8259,19 @@ function registerDiagnoseCommands(program) {
8099
8259
  const o = this.optsWithGlobals();
8100
8260
  await runDiagnoseErrors({
8101
8261
  ...backendFlagsFromOpts(o),
8102
- service: o.service,
8103
- lookbackMinutes: o.lookbackMinutes,
8104
- limit: o.limit
8262
+ service: stringOpt(o, "service"),
8263
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8264
+ limit: numberOpt(o, "limit")
8105
8265
  });
8106
8266
  });
8107
8267
  const slosCmd = new Command("slos").description("Report SLO violations for a service").requiredOption("--service <name>", "Service to check").option("--p99-latency-ms <n>", "p99 latency target", floatArg).option("--max-error-rate <n>", "Error-rate target (0..1)", floatArg).option("--lookback-minutes <n>", "Lookback in minutes", intArg).action(async function() {
8108
8268
  const o = this.optsWithGlobals();
8109
8269
  await runDiagnoseSlos({
8110
8270
  ...backendFlagsFromOpts(o),
8111
- service: o.service,
8112
- p99LatencyMs: o.p99LatencyMs,
8113
- maxErrorRate: o.maxErrorRate,
8114
- lookbackMinutes: o.lookbackMinutes
8271
+ service: stringOpt(o, "service") ?? "",
8272
+ p99LatencyMs: numberOpt(o, "p99LatencyMs"),
8273
+ maxErrorRate: numberOpt(o, "maxErrorRate"),
8274
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8115
8275
  });
8116
8276
  });
8117
8277
  addBackendFlags(diagnoseCmd);
@@ -8170,8 +8330,8 @@ function registerCorrelateCommands(program) {
8170
8330
  const o = this.optsWithGlobals();
8171
8331
  await runExplainSlowdown({
8172
8332
  ...backendFlagsFromOpts(o),
8173
- service: o.service,
8174
- lookbackMinutes: o.lookbackMinutes
8333
+ service: stringOpt(o, "service") ?? "",
8334
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8175
8335
  });
8176
8336
  });
8177
8337
  addBackendFlags(correlateCmd);
@@ -8282,71 +8442,71 @@ function registerLlmCommands(program) {
8282
8442
  const o = this.optsWithGlobals();
8283
8443
  await runLlmUsage({
8284
8444
  ...backendFlagsFromOpts(o),
8285
- startTime: o.startTime,
8286
- endTime: o.endTime,
8287
- serviceName: o.serviceName,
8288
- genAiSystem: o.genAiSystem,
8289
- genAiRequestModel: o.genAiRequestModel,
8290
- genAiResponseModel: o.genAiResponseModel,
8291
- limit: o.limit
8445
+ startTime: stringOpt(o, "startTime"),
8446
+ endTime: stringOpt(o, "endTime"),
8447
+ serviceName: stringOpt(o, "serviceName"),
8448
+ genAiSystem: stringOpt(o, "genAiSystem"),
8449
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8450
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8451
+ limit: numberOpt(o, "limit")
8292
8452
  });
8293
8453
  });
8294
8454
  const modelsCmd = commonOpts(new Command("models")).description("Discover LLM models in use").action(async function() {
8295
8455
  const o = this.optsWithGlobals();
8296
8456
  await runLlmModels({
8297
8457
  ...backendFlagsFromOpts(o),
8298
- startTime: o.startTime,
8299
- endTime: o.endTime,
8300
- serviceName: o.serviceName,
8301
- genAiSystem: o.genAiSystem,
8302
- limit: o.limit
8458
+ startTime: stringOpt(o, "startTime"),
8459
+ endTime: stringOpt(o, "endTime"),
8460
+ serviceName: stringOpt(o, "serviceName"),
8461
+ genAiSystem: stringOpt(o, "genAiSystem"),
8462
+ limit: numberOpt(o, "limit")
8303
8463
  });
8304
8464
  });
8305
8465
  const modelStatsCmd = commonOpts(new Command("model-stats")).description("Latency/token/error stats for one LLM model").requiredOption("--model-name <name>", "Model to inspect").action(async function() {
8306
8466
  const o = this.optsWithGlobals();
8307
8467
  await runLlmModelStats({
8308
8468
  ...backendFlagsFromOpts(o),
8309
- modelName: o.modelName,
8310
- startTime: o.startTime,
8311
- endTime: o.endTime,
8312
- serviceName: o.serviceName
8469
+ modelName: stringOpt(o, "modelName") ?? "",
8470
+ startTime: stringOpt(o, "startTime"),
8471
+ endTime: stringOpt(o, "endTime"),
8472
+ serviceName: stringOpt(o, "serviceName")
8313
8473
  });
8314
8474
  });
8315
8475
  const expensiveCmd = commonOpts(new Command("expensive")).description("Traces with highest total LLM token usage").option("--min-tokens <n>", "Minimum token threshold", intArg).action(async function() {
8316
8476
  const o = this.optsWithGlobals();
8317
8477
  await runLlmExpensive({
8318
8478
  ...backendFlagsFromOpts(o),
8319
- startTime: o.startTime,
8320
- endTime: o.endTime,
8321
- serviceName: o.serviceName,
8322
- genAiRequestModel: o.genAiRequestModel,
8323
- genAiResponseModel: o.genAiResponseModel,
8324
- minTokens: o.minTokens,
8325
- limit: o.limit
8479
+ startTime: stringOpt(o, "startTime"),
8480
+ endTime: stringOpt(o, "endTime"),
8481
+ serviceName: stringOpt(o, "serviceName"),
8482
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8483
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8484
+ minTokens: numberOpt(o, "minTokens"),
8485
+ limit: numberOpt(o, "limit")
8326
8486
  });
8327
8487
  });
8328
8488
  const slowCmd = commonOpts(new Command("slow")).description("Slowest traces that include LLM spans").option("--min-duration-ms <n>", "Minimum duration", intArg).action(async function() {
8329
8489
  const o = this.optsWithGlobals();
8330
8490
  await runLlmSlow({
8331
8491
  ...backendFlagsFromOpts(o),
8332
- startTime: o.startTime,
8333
- endTime: o.endTime,
8334
- serviceName: o.serviceName,
8335
- genAiRequestModel: o.genAiRequestModel,
8336
- genAiResponseModel: o.genAiResponseModel,
8337
- minDurationMs: o.minDurationMs,
8338
- limit: o.limit
8492
+ startTime: stringOpt(o, "startTime"),
8493
+ endTime: stringOpt(o, "endTime"),
8494
+ serviceName: stringOpt(o, "serviceName"),
8495
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8496
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8497
+ minDurationMs: numberOpt(o, "minDurationMs"),
8498
+ limit: numberOpt(o, "limit")
8339
8499
  });
8340
8500
  });
8341
8501
  const toolsCmd = commonOpts(new Command("tools")).description("Discover tool/function spans grouped by tool name").action(async function() {
8342
8502
  const o = this.optsWithGlobals();
8343
8503
  await runLlmTools({
8344
8504
  ...backendFlagsFromOpts(o),
8345
- startTime: o.startTime,
8346
- endTime: o.endTime,
8347
- serviceName: o.serviceName,
8348
- genAiSystem: o.genAiSystem,
8349
- limit: o.limit
8505
+ startTime: stringOpt(o, "startTime"),
8506
+ endTime: stringOpt(o, "endTime"),
8507
+ serviceName: stringOpt(o, "serviceName"),
8508
+ genAiSystem: stringOpt(o, "genAiSystem"),
8509
+ limit: numberOpt(o, "limit")
8350
8510
  });
8351
8511
  });
8352
8512
  addBackendFlags(llmCmd);
@@ -8431,7 +8591,7 @@ function registerScoreCommands(program) {
8431
8591
  const o = this.optsWithGlobals();
8432
8592
  await runScoreSpan({
8433
8593
  ...staticFlagsFromOpts(o),
8434
- spanFile: o.spanFile
8594
+ spanFile: stringOpt(o, "spanFile")
8435
8595
  });
8436
8596
  });
8437
8597
  const explainCmd = addStaticFlags(new Command("explain")).description("Explain the instrumentation scoring rubric").action(async function() {
@@ -8539,7 +8699,7 @@ function registerCollectorCommands(program) {
8539
8699
  const o = this.optsWithGlobals();
8540
8700
  await runCollectorValidate({
8541
8701
  ...staticFlagsFromOpts(o),
8542
- configFile: o.configFile
8702
+ configFile: stringOpt(o, "configFile")
8543
8703
  });
8544
8704
  });
8545
8705
  const suggestCmd = addStaticFlags(new Command("suggest")).description("Print a minimal OTLP receiver config").action(async function() {
@@ -8555,7 +8715,7 @@ function registerCollectorCommands(program) {
8555
8715
  const o = this.optsWithGlobals();
8556
8716
  await runCollectorComponents({
8557
8717
  ...staticFlagsFromOpts(o),
8558
- version: o.version,
8718
+ version: stringOpt(o, "version"),
8559
8719
  kind: o.kind
8560
8720
  });
8561
8721
  });
@@ -8564,8 +8724,8 @@ function registerCollectorCommands(program) {
8564
8724
  await runCollectorSchema({
8565
8725
  ...staticFlagsFromOpts(o),
8566
8726
  kind: o.kind,
8567
- name: o.name,
8568
- version: o.version
8727
+ name: stringOpt(o, "name") ?? "",
8728
+ version: stringOpt(o, "version")
8569
8729
  });
8570
8730
  });
8571
8731
  const readmeCmd = addStaticFlags(new Command("readme")).description("Get README for a collector component").requiredOption("--kind <kind>", "Component kind").requiredOption("--name <name>", "Component name").option("--version <semver>", "Collector version").action(async function() {
@@ -8573,8 +8733,8 @@ function registerCollectorCommands(program) {
8573
8733
  await runCollectorReadme({
8574
8734
  ...staticFlagsFromOpts(o),
8575
8735
  kind: o.kind,
8576
- name: o.name,
8577
- version: o.version
8736
+ name: stringOpt(o, "name") ?? "",
8737
+ version: stringOpt(o, "version")
8578
8738
  });
8579
8739
  });
8580
8740
  const validateComponentCmd = addStaticFlags(new Command("validate-component")).description("Validate component config against upstream schema").requiredOption("--kind <kind>", "Component kind").requiredOption("--name <name>", "Component name").option("--version <semver>", "Collector version").option("--config-file <path>", "Read JSON config (default: stdin)").action(async function() {
@@ -8582,9 +8742,9 @@ function registerCollectorCommands(program) {
8582
8742
  await runCollectorValidateComponent({
8583
8743
  ...staticFlagsFromOpts(o),
8584
8744
  kind: o.kind,
8585
- name: o.name,
8586
- version: o.version,
8587
- configFile: o.configFile
8745
+ name: stringOpt(o, "name") ?? "",
8746
+ version: stringOpt(o, "version"),
8747
+ configFile: stringOpt(o, "configFile")
8588
8748
  });
8589
8749
  });
8590
8750
  const refreshCmd = addStaticFlags(new Command("refresh")).description("Refresh in-memory collector metadata cache").action(async function() {
@@ -8880,8 +9040,8 @@ function registerSecurityCommands(program) {
8880
9040
  await runSecurityEvents({
8881
9041
  ...backendFlagsFromOpts(o),
8882
9042
  ...windowFlagsFromOpts(o),
8883
- category: o.category,
8884
- severity: o.severity
9043
+ category: stringOpt(o, "category"),
9044
+ severity: stringOpt(o, "severity")
8885
9045
  });
8886
9046
  });
8887
9047
  const mcpCmd = addTimeWindowFlags(new Command("mcp")).description("MCP protocol-boundary security: prompt-injection verdicts, output-budget breaches, untrusted-content tool calls").action(async function() {
@@ -8986,45 +9146,47 @@ function composeDrains(...drains) {
8986
9146
  function resolveEndpoint(explicit) {
8987
9147
  return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
8988
9148
  }
9149
+ /** A finite number, as opposed to the strings and markers around it. */
9150
+ function isNumber(value) {
9151
+ return Number.isFinite(value);
9152
+ }
8989
9153
  const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
8990
9154
  function sanitizeFlags(argv, allowlistedStringFlags = []) {
8991
- const out = {};
9155
+ const entries = [];
9156
+ /** What a flag's value is worth reporting as, once the key is known safe. */
9157
+ const valueOf = (key, rawValue) => {
9158
+ if (/^\d+$/.test(rawValue)) return Number(rawValue);
9159
+ if (allowlistedStringFlags.includes(key)) return rawValue;
9160
+ return { present: true };
9161
+ };
8992
9162
  for (let i = 0; i < argv.length; i++) {
8993
9163
  const arg = argv[i];
8994
9164
  if (arg === void 0 || !arg.startsWith("-")) continue;
8995
9165
  const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
8996
9166
  if (SENSITIVE_PATTERN.test(key)) {
8997
- out[key] = { present: true };
9167
+ entries.push([key, { present: true }]);
8998
9168
  continue;
8999
9169
  }
9000
9170
  if (arg.includes("=")) {
9001
9171
  const [, rawValue] = arg.split("=");
9002
9172
  if (rawValue === void 0) continue;
9003
- if (/^\d+$/.test(rawValue)) out[key] = Number(rawValue);
9004
- else if (allowlistedStringFlags.includes(key)) out[key] = rawValue;
9005
- else out[key] = { present: true };
9173
+ entries.push([key, valueOf(key, rawValue)]);
9006
9174
  continue;
9007
9175
  }
9008
9176
  const next = argv[i + 1];
9009
- if (next && !next.startsWith("-")) if (/^\d+$/.test(next)) {
9010
- out[key] = Number(next);
9011
- i++;
9012
- } else if (allowlistedStringFlags.includes(key)) {
9013
- out[key] = next;
9014
- i++;
9015
- } else {
9016
- out[key] = { present: true };
9177
+ if (next && !next.startsWith("-")) {
9178
+ entries.push([key, valueOf(key, next)]);
9017
9179
  i++;
9018
- }
9019
- else out[key] = true;
9180
+ } else entries.push([key, true]);
9020
9181
  }
9021
- return out;
9182
+ return Object.fromEntries(entries);
9022
9183
  }
9023
9184
  function sanitizeCustom(fields) {
9024
- const out = {};
9025
- for (const [key, value] of Object.entries(fields)) if (typeof value === "boolean" || typeof value === "number") out[key] = value;
9026
- else if (value !== null && value !== void 0) out[key] = { present: true };
9027
- return out;
9185
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => {
9186
+ if (value === true || value === false) return [key, value];
9187
+ if (isNumber(value)) return [key, value];
9188
+ return [key, { present: true }];
9189
+ }));
9028
9190
  }
9029
9191
  const runStorage = new AsyncLocalStorage();
9030
9192
  let activeHandle = null;
@@ -9036,7 +9198,7 @@ var InternalTelemetry = class {
9036
9198
  constructor(options, enabled) {
9037
9199
  this.options = options;
9038
9200
  this.enabled = enabled;
9039
- this.outbox = new TelemetryOutbox({ toolName: options.name });
9201
+ this.outbox = options.outbox ?? new TelemetryOutbox({ toolName: options.name });
9040
9202
  const endpoint = resolveEndpoint(options.endpoint);
9041
9203
  const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
9042
9204
  this.drain = composeDrains(...drains);
@@ -9190,6 +9352,62 @@ function runTelemetryDisclosure(toolName, version) {
9190
9352
  console.log(disclosure.markdown);
9191
9353
  }
9192
9354
  //#endregion
9355
+ //#region src/commands/estimate.ts
9356
+ /** `keepPercent` → `--keep-percent`, so the fix hint names a real flag. */
9357
+ function flagFor(field) {
9358
+ return `--${field.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
9359
+ }
9360
+ /**
9361
+ * Answer "what will this telemetry cost me a month" as a JSON envelope.
9362
+ *
9363
+ * Throws `AutotelError` rather than exiting, so the top-level handler picks
9364
+ * pretty or JSON output and maps the code — bad input is the caller's to fix
9365
+ * (exit 2), not a broken run (exit 1).
9366
+ */
9367
+ function runEstimate(input) {
9368
+ try {
9369
+ return {
9370
+ ok: true,
9371
+ command: "estimate",
9372
+ estimate: estimateCost(input)
9373
+ };
9374
+ } catch (error) {
9375
+ if (error instanceof EstimateInputError) throw new AutotelError({
9376
+ type: "validation",
9377
+ code: AutotelErrorCodes.E_INVALID_INPUT,
9378
+ message: error.message,
9379
+ fix: `Pass ${flagFor(error.field)} with a value in ${error.expected}.`,
9380
+ expected: { [error.field]: error.expected }
9381
+ });
9382
+ throw error;
9383
+ }
9384
+ }
9385
+ //#endregion
9386
+ //#region src/ui/estimate.ts
9387
+ const usd = (value) => `$${value.toFixed(2)}`;
9388
+ /**
9389
+ * The human view. Every figure the estimate rests on is printed with it —
9390
+ * a cost number without its basis is a number nobody can argue with, which is
9391
+ * the wrong property for a number that will end up in a budget conversation.
9392
+ */
9393
+ function printEstimate(estimate) {
9394
+ const { before, after, basis } = estimate;
9395
+ const lines = [
9396
+ "",
9397
+ chalk.bold(" Telemetry cost per month"),
9398
+ "",
9399
+ ` ${chalk.dim("today")} ${before.events.toLocaleString()} events ${before.gb} GB ${chalk.bold(usd(before.cost))}`,
9400
+ ` ${chalk.dim("canonical")} ${after.events.toLocaleString()} events ${after.gb} GB ${chalk.bold(usd(after.cost))}`,
9401
+ "",
9402
+ estimate.saved > 0 ? ` ${chalk.green(`saves ${usd(estimate.saved)}/month (${estimate.savedPercent}%)`)}` : ` ${chalk.yellow(`costs ${usd(Math.abs(estimate.saved))}/month more`)}`,
9403
+ "",
9404
+ chalk.dim(` basis: ${basis.logLineBytes} B per log line, ${basis.canonicalLineBytes} B per canonical line, ${basis.spanBytes} B per span (${basis.bytesFrom})`),
9405
+ chalk.dim(` ${usd(basis.perGb)}/GB, ${usd(basis.perMillionEvents)}/million events, ${basis.keepPercent}% of traffic kept`),
9406
+ ""
9407
+ ];
9408
+ process.stdout.write(lines.join("\n") + "\n");
9409
+ }
9410
+ //#endregion
9193
9411
  //#region src/cli.ts
9194
9412
  /**
9195
9413
  * Create the CLI program
@@ -9310,6 +9528,27 @@ function createProgram() {
9310
9528
  codemodCmd.addCommand(traceCmd);
9311
9529
  addGlobalOptions(codemodCmd);
9312
9530
  program.addCommand(codemodCmd);
9531
+ const estimateCmd = new Command("estimate").description("Estimate what a month of telemetry costs, before and after").requiredOption("--requests-per-month <n>", "Requests the application serves per month").requiredOption("--per-gb <usd>", "USD per gigabyte ingested by your provider").option("--log-lines-per-request <n>", "Log lines written per request today (default 4)").option("--spans-per-request <n>", "Spans exported per request after instrumenting (default 0)").option("--per-million-events <usd>", "USD per million events indexed, if your provider meters events too").option("--keep-percent <n>", "Traffic kept after sampling, applied to both shapes (default 100)").option("--json", "Output machine-readable JSON").option("--output-file <path>", "Persist JSON output to this path").action((opts) => {
9532
+ configureJsonOutput({
9533
+ outputFile: opts.outputFile,
9534
+ noSecrets: opts.secretsInOutput === false
9535
+ });
9536
+ const envelope = runEstimate({
9537
+ requestsPerMonth: Number(opts.requestsPerMonth),
9538
+ perGb: Number(opts.perGb),
9539
+ ...opts.logLinesPerRequest !== void 0 && { logLinesPerRequest: Number(opts.logLinesPerRequest) },
9540
+ ...opts.spansPerRequest !== void 0 && { spansPerRequest: Number(opts.spansPerRequest) },
9541
+ ...opts.perMillionEvents !== void 0 && { perMillionEvents: Number(opts.perMillionEvents) },
9542
+ ...opts.keepPercent !== void 0 && { keepPercent: Number(opts.keepPercent) }
9543
+ });
9544
+ if (opts.json) {
9545
+ printJson(envelope);
9546
+ return;
9547
+ }
9548
+ printEstimate(envelope.estimate);
9549
+ });
9550
+ addGlobalOptions(estimateCmd);
9551
+ program.addCommand(estimateCmd);
9313
9552
  const schemaCmd = new Command("schema").description("Print the CLI manifest as JSON (agent discovery)").option("--output-file <path>", "Persist JSON to a file").option("--no-secrets-in-output", "Redact secret-shaped values").action((opts) => {
9314
9553
  runSchema({
9315
9554
  outputFile: opts.outputFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-cli",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "CLI for autotel - setup wizard, diagnostics, and incremental features",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "semver": "^7.8.5",
26
26
  "ts-morph": "^28.0.0",
27
27
  "yaml": "^2.9.0",
28
- "autotel-mcp": "0.4.0"
28
+ "autotel-mcp": "0.5.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@eslint/js": "^10.0.1",
@@ -40,7 +40,7 @@
40
40
  "typescript": "^6.0.3",
41
41
  "typescript-eslint": "^8.65.0",
42
42
  "vitest": "^4.1.10",
43
- "autotel-telemetry": "0.1.2"
43
+ "autotel-telemetry": "0.2.0"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=22.0.0"