autotel-cli 0.15.3 → 0.16.1

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 +440 -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,56 @@ 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
+ "Docs: https://jagreehal.github.io/autotel/integrations/posthog/",
1415
+ "Backend init({ baggage: \"\" }) so the checkout span carries session.id from W3C baggage",
1416
+ "Spans now carry session.id and user.id from PostHog; failed spans carry session.replay.url",
1417
+ "For a clickable link back: joinPostHog(posthog, { traceUrl: ({ traceId }) => `...${traceId}` })",
1418
+ "Name any feature flags worth slicing by: joinPostHog(posthog, { featureFlags: [\"my-flag\"] })"
1419
+ ]
1420
+ };
1421
+ //#endregion
1356
1422
  //#region src/presets/subscribers/mixpanel.ts
1357
1423
  /**
1358
1424
  * Mixpanel subscriber preset
@@ -2053,7 +2119,8 @@ const plugins = /* @__PURE__ */ new Map([
2053
2119
  ["nestjs", nestjs],
2054
2120
  ["sveltekit", sveltekit],
2055
2121
  ["elysia", elysia],
2056
- ["nuxt", nuxt]
2122
+ ["nuxt", nuxt],
2123
+ ["posthog-web", posthogWeb]
2057
2124
  ]);
2058
2125
  /**
2059
2126
  * Platform presets registry
@@ -2529,14 +2596,14 @@ const REDACTED = "[REDACTED]";
2529
2596
  function redact(value) {
2530
2597
  if (value === null || value === void 0) return value;
2531
2598
  if (Array.isArray(value)) return value.map((v) => redact(v));
2532
- if (typeof value === "object") {
2599
+ const record = asRecord(value);
2600
+ if (record) {
2533
2601
  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);
2602
+ for (const [key, field] of Object.entries(record)) out[key] = asString(field) !== void 0 && SECRET_KEY_PATTERN.test(key) ? REDACTED : redact(field);
2536
2603
  return out;
2537
2604
  }
2538
- if (typeof value === "string" && SECRET_VALUE_PATTERN.test(value)) return REDACTED;
2539
- return value;
2605
+ const text = asString(value);
2606
+ return text !== void 0 && SECRET_VALUE_PATTERN.test(text) ? REDACTED : value;
2540
2607
  }
2541
2608
  /**
2542
2609
  * Print a JSON payload to stdout. If `--output-file` was configured, also
@@ -2812,7 +2879,7 @@ function readPlanFromFile(filePath) {
2812
2879
  throw new AutotelError({
2813
2880
  type: "validation",
2814
2881
  code: AutotelErrorCodes.E_INVALID_PLAN,
2815
- message: `Plan file is not valid JSON: ${error.message}`
2882
+ message: `Plan file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
2816
2883
  });
2817
2884
  }
2818
2885
  }
@@ -2828,7 +2895,7 @@ async function readPlanFromInput(input) {
2828
2895
  throw new AutotelError({
2829
2896
  type: "validation",
2830
2897
  code: AutotelErrorCodes.E_INVALID_INPUT,
2831
- message: `stdin did not contain valid JSON: ${error.message}`
2898
+ message: `stdin did not contain valid JSON: ${error instanceof Error ? error.message : String(error)}`
2832
2899
  });
2833
2900
  }
2834
2901
  }
@@ -4880,7 +4947,7 @@ const traceRule = {
4880
4947
  question: "Does this entry point produce a span?",
4881
4948
  weight: 40,
4882
4949
  appliesTo: { kinds: HANDLER_KINDS },
4883
- fix: ({ target }) => `export const handler = trace('${target.method?.toLowerCase() ?? "handle"} ${target.path}', async () => { /* … */ });`,
4950
+ fix: ({ target }) => `export const handler = instrument({ key: '${target.method?.toLowerCase() ?? "handle"} ${target.path}', fn: async () => { /* … */ } });`,
4884
4951
  check(context) {
4885
4952
  if (usesAutotelApi(context, SPAN_WRAPPERS)) return null;
4886
4953
  if (context.project.ambientTracing) return null;
@@ -5057,7 +5124,7 @@ const RULES = [
5057
5124
  kinds: HANDLER_KINDS,
5058
5125
  when: ({ project }) => project.hasLlmDependency
5059
5126
  },
5060
- fix: () => "traceGenAI({ operation: 'chat', model }, async () => client.chat.completions.create(…));",
5127
+ fix: () => "traceGenAI({ operation: 'chat', model })((ctx) => async () => client.chat.completions.create(…));",
5061
5128
  check(context) {
5062
5129
  const llmCall = LLM_CALLS.flatMap((name) => context.facts.memberCalls(name)).find((call) => call.root !== null && call.root !== "JSON");
5063
5130
  if (!llmCall) return null;
@@ -6703,6 +6770,70 @@ const COMMANDS = [
6703
6770
  }
6704
6771
  ]
6705
6772
  },
6773
+ {
6774
+ name: "estimate",
6775
+ description: "Estimate what a month of telemetry costs, before and after canonical log lines",
6776
+ args: [],
6777
+ flags: [
6778
+ ...GLOBAL_FLAGS,
6779
+ ...AGENT_FLAGS,
6780
+ {
6781
+ name: "--requests-per-month",
6782
+ takesValue: true,
6783
+ description: "Requests the application serves per month (required)"
6784
+ },
6785
+ {
6786
+ name: "--per-gb",
6787
+ takesValue: true,
6788
+ description: "USD per gigabyte ingested by your provider (required — no rate is assumed)"
6789
+ },
6790
+ {
6791
+ name: "--log-lines-per-request",
6792
+ takesValue: true,
6793
+ description: "Log lines written per request today (default 4)"
6794
+ },
6795
+ {
6796
+ name: "--spans-per-request",
6797
+ takesValue: true,
6798
+ description: "Spans exported per request after instrumenting (default 0)"
6799
+ },
6800
+ {
6801
+ name: "--per-million-events",
6802
+ takesValue: true,
6803
+ description: "USD per million events indexed, if metered separately"
6804
+ },
6805
+ {
6806
+ name: "--keep-percent",
6807
+ takesValue: true,
6808
+ description: "Traffic kept after sampling, applied to both shapes (default 100)"
6809
+ }
6810
+ ],
6811
+ mutating: false,
6812
+ network: false,
6813
+ writesFiles: false,
6814
+ supportsDryRun: false,
6815
+ requiresPackageJson: false,
6816
+ mayReadEnv: false,
6817
+ supportsJson: true,
6818
+ examples: [
6819
+ {
6820
+ description: "Ten million requests a month at four log lines each",
6821
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10"
6822
+ },
6823
+ {
6824
+ description: "Include the spans instrumenting will add",
6825
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --spans-per-request 6 --json"
6826
+ },
6827
+ {
6828
+ description: "A provider that meters indexed events as well as bytes",
6829
+ command: "autotel estimate --requests-per-month 300000000 --per-gb 0.50 --per-million-events 2.50"
6830
+ },
6831
+ {
6832
+ description: "What head sampling at 10% does to the bill",
6833
+ command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --keep-percent 10"
6834
+ }
6835
+ ]
6836
+ },
6706
6837
  {
6707
6838
  name: "codemod trace",
6708
6839
  description: "Wrap functions in trace() with span name from function/variable/method name",
@@ -7289,26 +7420,40 @@ function runSchemaOutputs(opts) {
7289
7420
  printJson({
7290
7421
  ok: true,
7291
7422
  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[]"
7423
+ outputs: {
7424
+ "autotel init --json": {
7425
+ ok: "boolean",
7426
+ command: "string",
7427
+ detected: {
7428
+ packages: "array of { name, version, resolution: \"target\" | \"workspace-root\" }",
7429
+ logger: "\"pino\" | \"winston\" | \"bunyan\" | null",
7430
+ backend: "{ source: \"env\" | \"wrangler\" | \"deps\" | \"prompt\" | \"default\", value: string }",
7431
+ platform: "\"cloudflare\" | \"aws-lambda\" | \"edge\" | null"
7306
7432
  },
7307
- filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
7308
- envVars: "array of { name, sensitive, action }"
7433
+ plan: {
7434
+ presets: "string[] (slugs)",
7435
+ packagesToInstall: {
7436
+ prod: "string[]",
7437
+ dev: "string[]"
7438
+ },
7439
+ filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
7440
+ envVars: "array of { name, sensitive, action }"
7441
+ },
7442
+ nextSteps: "string[]"
7309
7443
  },
7310
- nextSteps: "string[]"
7311
- } }
7444
+ "autotel estimate --json": {
7445
+ ok: "boolean",
7446
+ command: "string",
7447
+ estimate: {
7448
+ currency: "\"USD\"",
7449
+ before: "{ events: number, gb: number, cost: number }",
7450
+ after: "{ events: number, gb: number, cost: number }",
7451
+ saved: "number (USD/month, negative when instrumenting costs more)",
7452
+ savedPercent: "number",
7453
+ basis: "{ logLineBytes, canonicalLineBytes, spanBytes, bytesFrom: \"measured\" | \"caller\", perGb, perMillionEvents, keepPercent, bytesPerGb }"
7454
+ }
7455
+ }
7456
+ }
7312
7457
  });
7313
7458
  }
7314
7459
  /** `autotel commands [--json]` — compact listing. */
@@ -7476,6 +7621,23 @@ function toInvestigateError(command, error) {
7476
7621
  });
7477
7622
  }
7478
7623
  //#endregion
7624
+ //#region src/lib/opts.ts
7625
+ /** A flag declared without an argument parser: Commander yields its string. */
7626
+ function stringOpt(o, key) {
7627
+ const value = o[key];
7628
+ return typeof value === "string" && value.length > 0 ? value : void 0;
7629
+ }
7630
+ /** A flag declared with `intArg` or another numeric parser. */
7631
+ function numberOpt(o, key) {
7632
+ const value = o[key];
7633
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7634
+ }
7635
+ /** A boolean flag: present or absent. */
7636
+ function boolOpt(o, key) {
7637
+ const value = o[key];
7638
+ return typeof value === "boolean" ? value : void 0;
7639
+ }
7640
+ //#endregion
7479
7641
  //#region src/commands/investigate/cli-helpers.ts
7480
7642
  /**
7481
7643
  * Shared commander wiring for investigate commands. Each command group's
@@ -7501,22 +7663,22 @@ function addStaticFlags(cmd) {
7501
7663
  function backendFlagsFromOpts(opts) {
7502
7664
  return {
7503
7665
  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,
7666
+ jaegerBaseUrl: stringOpt(opts, "jaegerBaseUrl"),
7667
+ tempoBaseUrl: stringOpt(opts, "tempoBaseUrl"),
7668
+ prometheusBaseUrl: stringOpt(opts, "prometheusBaseUrl"),
7669
+ lokiBaseUrl: stringOpt(opts, "lokiBaseUrl"),
7670
+ collectorPort: numberOpt(opts, "collectorPort"),
7671
+ fixturePath: stringOpt(opts, "fixturePath"),
7672
+ logfireBaseUrl: stringOpt(opts, "logfireBaseUrl"),
7673
+ datadogSite: stringOpt(opts, "datadogSite"),
7674
+ signozBaseUrl: stringOpt(opts, "signozBaseUrl"),
7675
+ outputFile: stringOpt(opts, "outputFile"),
7514
7676
  noSecrets: opts.secretsInOutput === false
7515
7677
  };
7516
7678
  }
7517
7679
  function staticFlagsFromOpts(opts) {
7518
7680
  return {
7519
- outputFile: opts.outputFile,
7681
+ outputFile: stringOpt(opts, "outputFile"),
7520
7682
  noSecrets: opts.secretsInOutput === false
7521
7683
  };
7522
7684
  }
@@ -7528,12 +7690,12 @@ function addTimeWindowFlags(cmd) {
7528
7690
  }
7529
7691
  function windowFlagsFromOpts(opts) {
7530
7692
  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
7693
+ serviceName: stringOpt(opts, "serviceName"),
7694
+ operationName: stringOpt(opts, "operationName"),
7695
+ lookbackMinutes: numberOpt(opts, "lookbackMinutes"),
7696
+ from: stringOpt(opts, "from"),
7697
+ to: stringOpt(opts, "to"),
7698
+ limit: numberOpt(opts, "limit")
7537
7699
  };
7538
7700
  }
7539
7701
  //#endregion
@@ -7727,8 +7889,8 @@ function registerHealthCommands(program) {
7727
7889
  const opts = this.opts();
7728
7890
  await runHealth({
7729
7891
  ...backendFlagsFromOpts(opts),
7730
- otlpEndpoint: opts.otlpEndpoint,
7731
- freshnessTimeoutMs: opts.freshnessTimeoutMs,
7892
+ otlpEndpoint: stringOpt(opts, "otlpEndpoint"),
7893
+ freshnessTimeoutMs: numberOpt(opts, "freshnessTimeoutMs"),
7732
7894
  otlpEncoding: opts.otlpEncoding
7733
7895
  });
7734
7896
  });
@@ -7800,26 +7962,26 @@ function registerDiscoveryCommands(program) {
7800
7962
  const o = this.optsWithGlobals();
7801
7963
  await runDiscoverServices({
7802
7964
  ...backendFlagsFromOpts(o),
7803
- limitServices: o.limitServices,
7804
- traceSample: o.traceSample,
7805
- logSample: o.logSample,
7806
- metricSample: o.metricSample
7965
+ limitServices: numberOpt(o, "limitServices"),
7966
+ traceSample: numberOpt(o, "traceSample"),
7967
+ logSample: numberOpt(o, "logSample"),
7968
+ metricSample: numberOpt(o, "metricSample")
7807
7969
  });
7808
7970
  });
7809
7971
  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
7972
  const o = this.optsWithGlobals();
7811
7973
  await runDiscoverTraceFields({
7812
7974
  ...backendFlagsFromOpts(o),
7813
- search: o.search,
7814
- sampleSize: o.sampleSize
7975
+ search: stringOpt(o, "search"),
7976
+ sampleSize: numberOpt(o, "sampleSize")
7815
7977
  });
7816
7978
  });
7817
7979
  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
7980
  const o = this.optsWithGlobals();
7819
7981
  await runDiscoverLogFields({
7820
7982
  ...backendFlagsFromOpts(o),
7821
- search: o.search,
7822
- sampleSize: o.sampleSize
7983
+ search: stringOpt(o, "search"),
7984
+ sampleSize: numberOpt(o, "sampleSize")
7823
7985
  });
7824
7986
  });
7825
7987
  addBackendFlags(discoverCmd);
@@ -7856,62 +8018,62 @@ function registerQueryCommands(program) {
7856
8018
  const o = this.optsWithGlobals();
7857
8019
  await runQueryTraces({
7858
8020
  ...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,
8021
+ serviceName: stringOpt(o, "serviceName"),
8022
+ operationName: stringOpt(o, "operationName"),
8023
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8024
+ from: stringOpt(o, "from"),
8025
+ to: stringOpt(o, "to"),
8026
+ limit: numberOpt(o, "limit"),
8027
+ errorOnly: boolOpt(o, "errorOnly"),
7866
8028
  statusCode: o.statusCode,
7867
- minDurationMs: o.minDurationMs,
7868
- maxDurationMs: o.maxDurationMs,
7869
- genAiSystem: o.genAiSystem,
7870
- genAiRequestModel: o.genAiRequestModel,
7871
- genAiResponseModel: o.genAiResponseModel
8029
+ minDurationMs: numberOpt(o, "minDurationMs"),
8030
+ maxDurationMs: numberOpt(o, "maxDurationMs"),
8031
+ genAiSystem: stringOpt(o, "genAiSystem"),
8032
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8033
+ genAiResponseModel: stringOpt(o, "genAiResponseModel")
7872
8034
  });
7873
8035
  });
7874
8036
  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
8037
  const o = this.optsWithGlobals();
7876
8038
  await runQuerySpans({
7877
8039
  ...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,
8040
+ serviceName: stringOpt(o, "serviceName"),
8041
+ operationName: stringOpt(o, "operationName"),
8042
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8043
+ from: stringOpt(o, "from"),
8044
+ to: stringOpt(o, "to"),
8045
+ limit: numberOpt(o, "limit"),
8046
+ errorOnly: boolOpt(o, "errorOnly"),
7885
8047
  statusCode: o.statusCode,
7886
- minDurationMs: o.minDurationMs,
7887
- maxDurationMs: o.maxDurationMs
8048
+ minDurationMs: numberOpt(o, "minDurationMs"),
8049
+ maxDurationMs: numberOpt(o, "maxDurationMs")
7888
8050
  });
7889
8051
  });
7890
8052
  const metricsCmd = addTimeWindowFlags(new Command("metrics")).description("List metric series").option("--metric-name <name>", "Filter by metric name").action(async function() {
7891
8053
  const o = this.optsWithGlobals();
7892
8054
  await runQueryMetrics({
7893
8055
  ...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
8056
+ metricName: stringOpt(o, "metricName"),
8057
+ serviceName: stringOpt(o, "serviceName"),
8058
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8059
+ from: stringOpt(o, "from"),
8060
+ to: stringOpt(o, "to"),
8061
+ limit: numberOpt(o, "limit")
7900
8062
  });
7901
8063
  });
7902
8064
  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
8065
  const o = this.optsWithGlobals();
7904
8066
  await runQueryLogs({
7905
8067
  ...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
8068
+ serviceName: stringOpt(o, "serviceName"),
8069
+ traceId: stringOpt(o, "traceId"),
8070
+ spanId: stringOpt(o, "spanId"),
8071
+ severityText: stringOpt(o, "severityText"),
8072
+ text: stringOpt(o, "text"),
8073
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8074
+ from: stringOpt(o, "from"),
8075
+ to: stringOpt(o, "to"),
8076
+ limit: numberOpt(o, "limit")
7915
8077
  });
7916
8078
  });
7917
8079
  addBackendFlags(queryCmd);
@@ -7966,8 +8128,8 @@ function registerTopologyCommands(program) {
7966
8128
  const o = this.optsWithGlobals();
7967
8129
  await runServiceMap({
7968
8130
  ...backendFlagsFromOpts(o),
7969
- lookbackMinutes: o.lookbackMinutes,
7970
- limit: o.limit
8131
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8132
+ limit: numberOpt(o, "limit")
7971
8133
  });
7972
8134
  });
7973
8135
  addBackendFlags(topologyCmd);
@@ -8084,9 +8246,9 @@ function registerDiagnoseCommands(program) {
8084
8246
  const o = this.optsWithGlobals();
8085
8247
  await runDiagnoseAnomalies({
8086
8248
  ...backendFlagsFromOpts(o),
8087
- service: o.service,
8088
- operation: o.operation,
8089
- lookbackMinutes: o.lookbackMinutes
8249
+ service: stringOpt(o, "service"),
8250
+ operation: stringOpt(o, "operation"),
8251
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8090
8252
  });
8091
8253
  });
8092
8254
  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 +8261,19 @@ function registerDiagnoseCommands(program) {
8099
8261
  const o = this.optsWithGlobals();
8100
8262
  await runDiagnoseErrors({
8101
8263
  ...backendFlagsFromOpts(o),
8102
- service: o.service,
8103
- lookbackMinutes: o.lookbackMinutes,
8104
- limit: o.limit
8264
+ service: stringOpt(o, "service"),
8265
+ lookbackMinutes: numberOpt(o, "lookbackMinutes"),
8266
+ limit: numberOpt(o, "limit")
8105
8267
  });
8106
8268
  });
8107
8269
  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
8270
  const o = this.optsWithGlobals();
8109
8271
  await runDiagnoseSlos({
8110
8272
  ...backendFlagsFromOpts(o),
8111
- service: o.service,
8112
- p99LatencyMs: o.p99LatencyMs,
8113
- maxErrorRate: o.maxErrorRate,
8114
- lookbackMinutes: o.lookbackMinutes
8273
+ service: stringOpt(o, "service") ?? "",
8274
+ p99LatencyMs: numberOpt(o, "p99LatencyMs"),
8275
+ maxErrorRate: numberOpt(o, "maxErrorRate"),
8276
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8115
8277
  });
8116
8278
  });
8117
8279
  addBackendFlags(diagnoseCmd);
@@ -8170,8 +8332,8 @@ function registerCorrelateCommands(program) {
8170
8332
  const o = this.optsWithGlobals();
8171
8333
  await runExplainSlowdown({
8172
8334
  ...backendFlagsFromOpts(o),
8173
- service: o.service,
8174
- lookbackMinutes: o.lookbackMinutes
8335
+ service: stringOpt(o, "service") ?? "",
8336
+ lookbackMinutes: numberOpt(o, "lookbackMinutes")
8175
8337
  });
8176
8338
  });
8177
8339
  addBackendFlags(correlateCmd);
@@ -8282,71 +8444,71 @@ function registerLlmCommands(program) {
8282
8444
  const o = this.optsWithGlobals();
8283
8445
  await runLlmUsage({
8284
8446
  ...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
8447
+ startTime: stringOpt(o, "startTime"),
8448
+ endTime: stringOpt(o, "endTime"),
8449
+ serviceName: stringOpt(o, "serviceName"),
8450
+ genAiSystem: stringOpt(o, "genAiSystem"),
8451
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8452
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8453
+ limit: numberOpt(o, "limit")
8292
8454
  });
8293
8455
  });
8294
8456
  const modelsCmd = commonOpts(new Command("models")).description("Discover LLM models in use").action(async function() {
8295
8457
  const o = this.optsWithGlobals();
8296
8458
  await runLlmModels({
8297
8459
  ...backendFlagsFromOpts(o),
8298
- startTime: o.startTime,
8299
- endTime: o.endTime,
8300
- serviceName: o.serviceName,
8301
- genAiSystem: o.genAiSystem,
8302
- limit: o.limit
8460
+ startTime: stringOpt(o, "startTime"),
8461
+ endTime: stringOpt(o, "endTime"),
8462
+ serviceName: stringOpt(o, "serviceName"),
8463
+ genAiSystem: stringOpt(o, "genAiSystem"),
8464
+ limit: numberOpt(o, "limit")
8303
8465
  });
8304
8466
  });
8305
8467
  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
8468
  const o = this.optsWithGlobals();
8307
8469
  await runLlmModelStats({
8308
8470
  ...backendFlagsFromOpts(o),
8309
- modelName: o.modelName,
8310
- startTime: o.startTime,
8311
- endTime: o.endTime,
8312
- serviceName: o.serviceName
8471
+ modelName: stringOpt(o, "modelName") ?? "",
8472
+ startTime: stringOpt(o, "startTime"),
8473
+ endTime: stringOpt(o, "endTime"),
8474
+ serviceName: stringOpt(o, "serviceName")
8313
8475
  });
8314
8476
  });
8315
8477
  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
8478
  const o = this.optsWithGlobals();
8317
8479
  await runLlmExpensive({
8318
8480
  ...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
8481
+ startTime: stringOpt(o, "startTime"),
8482
+ endTime: stringOpt(o, "endTime"),
8483
+ serviceName: stringOpt(o, "serviceName"),
8484
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8485
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8486
+ minTokens: numberOpt(o, "minTokens"),
8487
+ limit: numberOpt(o, "limit")
8326
8488
  });
8327
8489
  });
8328
8490
  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
8491
  const o = this.optsWithGlobals();
8330
8492
  await runLlmSlow({
8331
8493
  ...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
8494
+ startTime: stringOpt(o, "startTime"),
8495
+ endTime: stringOpt(o, "endTime"),
8496
+ serviceName: stringOpt(o, "serviceName"),
8497
+ genAiRequestModel: stringOpt(o, "genAiRequestModel"),
8498
+ genAiResponseModel: stringOpt(o, "genAiResponseModel"),
8499
+ minDurationMs: numberOpt(o, "minDurationMs"),
8500
+ limit: numberOpt(o, "limit")
8339
8501
  });
8340
8502
  });
8341
8503
  const toolsCmd = commonOpts(new Command("tools")).description("Discover tool/function spans grouped by tool name").action(async function() {
8342
8504
  const o = this.optsWithGlobals();
8343
8505
  await runLlmTools({
8344
8506
  ...backendFlagsFromOpts(o),
8345
- startTime: o.startTime,
8346
- endTime: o.endTime,
8347
- serviceName: o.serviceName,
8348
- genAiSystem: o.genAiSystem,
8349
- limit: o.limit
8507
+ startTime: stringOpt(o, "startTime"),
8508
+ endTime: stringOpt(o, "endTime"),
8509
+ serviceName: stringOpt(o, "serviceName"),
8510
+ genAiSystem: stringOpt(o, "genAiSystem"),
8511
+ limit: numberOpt(o, "limit")
8350
8512
  });
8351
8513
  });
8352
8514
  addBackendFlags(llmCmd);
@@ -8431,7 +8593,7 @@ function registerScoreCommands(program) {
8431
8593
  const o = this.optsWithGlobals();
8432
8594
  await runScoreSpan({
8433
8595
  ...staticFlagsFromOpts(o),
8434
- spanFile: o.spanFile
8596
+ spanFile: stringOpt(o, "spanFile")
8435
8597
  });
8436
8598
  });
8437
8599
  const explainCmd = addStaticFlags(new Command("explain")).description("Explain the instrumentation scoring rubric").action(async function() {
@@ -8539,7 +8701,7 @@ function registerCollectorCommands(program) {
8539
8701
  const o = this.optsWithGlobals();
8540
8702
  await runCollectorValidate({
8541
8703
  ...staticFlagsFromOpts(o),
8542
- configFile: o.configFile
8704
+ configFile: stringOpt(o, "configFile")
8543
8705
  });
8544
8706
  });
8545
8707
  const suggestCmd = addStaticFlags(new Command("suggest")).description("Print a minimal OTLP receiver config").action(async function() {
@@ -8555,7 +8717,7 @@ function registerCollectorCommands(program) {
8555
8717
  const o = this.optsWithGlobals();
8556
8718
  await runCollectorComponents({
8557
8719
  ...staticFlagsFromOpts(o),
8558
- version: o.version,
8720
+ version: stringOpt(o, "version"),
8559
8721
  kind: o.kind
8560
8722
  });
8561
8723
  });
@@ -8564,8 +8726,8 @@ function registerCollectorCommands(program) {
8564
8726
  await runCollectorSchema({
8565
8727
  ...staticFlagsFromOpts(o),
8566
8728
  kind: o.kind,
8567
- name: o.name,
8568
- version: o.version
8729
+ name: stringOpt(o, "name") ?? "",
8730
+ version: stringOpt(o, "version")
8569
8731
  });
8570
8732
  });
8571
8733
  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 +8735,8 @@ function registerCollectorCommands(program) {
8573
8735
  await runCollectorReadme({
8574
8736
  ...staticFlagsFromOpts(o),
8575
8737
  kind: o.kind,
8576
- name: o.name,
8577
- version: o.version
8738
+ name: stringOpt(o, "name") ?? "",
8739
+ version: stringOpt(o, "version")
8578
8740
  });
8579
8741
  });
8580
8742
  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 +8744,9 @@ function registerCollectorCommands(program) {
8582
8744
  await runCollectorValidateComponent({
8583
8745
  ...staticFlagsFromOpts(o),
8584
8746
  kind: o.kind,
8585
- name: o.name,
8586
- version: o.version,
8587
- configFile: o.configFile
8747
+ name: stringOpt(o, "name") ?? "",
8748
+ version: stringOpt(o, "version"),
8749
+ configFile: stringOpt(o, "configFile")
8588
8750
  });
8589
8751
  });
8590
8752
  const refreshCmd = addStaticFlags(new Command("refresh")).description("Refresh in-memory collector metadata cache").action(async function() {
@@ -8880,8 +9042,8 @@ function registerSecurityCommands(program) {
8880
9042
  await runSecurityEvents({
8881
9043
  ...backendFlagsFromOpts(o),
8882
9044
  ...windowFlagsFromOpts(o),
8883
- category: o.category,
8884
- severity: o.severity
9045
+ category: stringOpt(o, "category"),
9046
+ severity: stringOpt(o, "severity")
8885
9047
  });
8886
9048
  });
8887
9049
  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 +9148,47 @@ function composeDrains(...drains) {
8986
9148
  function resolveEndpoint(explicit) {
8987
9149
  return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
8988
9150
  }
9151
+ /** A finite number, as opposed to the strings and markers around it. */
9152
+ function isNumber(value) {
9153
+ return Number.isFinite(value);
9154
+ }
8989
9155
  const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
8990
9156
  function sanitizeFlags(argv, allowlistedStringFlags = []) {
8991
- const out = {};
9157
+ const entries = [];
9158
+ /** What a flag's value is worth reporting as, once the key is known safe. */
9159
+ const valueOf = (key, rawValue) => {
9160
+ if (/^\d+$/.test(rawValue)) return Number(rawValue);
9161
+ if (allowlistedStringFlags.includes(key)) return rawValue;
9162
+ return { present: true };
9163
+ };
8992
9164
  for (let i = 0; i < argv.length; i++) {
8993
9165
  const arg = argv[i];
8994
9166
  if (arg === void 0 || !arg.startsWith("-")) continue;
8995
9167
  const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
8996
9168
  if (SENSITIVE_PATTERN.test(key)) {
8997
- out[key] = { present: true };
9169
+ entries.push([key, { present: true }]);
8998
9170
  continue;
8999
9171
  }
9000
9172
  if (arg.includes("=")) {
9001
9173
  const [, rawValue] = arg.split("=");
9002
9174
  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 };
9175
+ entries.push([key, valueOf(key, rawValue)]);
9006
9176
  continue;
9007
9177
  }
9008
9178
  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 };
9179
+ if (next && !next.startsWith("-")) {
9180
+ entries.push([key, valueOf(key, next)]);
9017
9181
  i++;
9018
- }
9019
- else out[key] = true;
9182
+ } else entries.push([key, true]);
9020
9183
  }
9021
- return out;
9184
+ return Object.fromEntries(entries);
9022
9185
  }
9023
9186
  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;
9187
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => {
9188
+ if (value === true || value === false) return [key, value];
9189
+ if (isNumber(value)) return [key, value];
9190
+ return [key, { present: true }];
9191
+ }));
9028
9192
  }
9029
9193
  const runStorage = new AsyncLocalStorage();
9030
9194
  let activeHandle = null;
@@ -9036,7 +9200,7 @@ var InternalTelemetry = class {
9036
9200
  constructor(options, enabled) {
9037
9201
  this.options = options;
9038
9202
  this.enabled = enabled;
9039
- this.outbox = new TelemetryOutbox({ toolName: options.name });
9203
+ this.outbox = options.outbox ?? new TelemetryOutbox({ toolName: options.name });
9040
9204
  const endpoint = resolveEndpoint(options.endpoint);
9041
9205
  const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
9042
9206
  this.drain = composeDrains(...drains);
@@ -9190,6 +9354,62 @@ function runTelemetryDisclosure(toolName, version) {
9190
9354
  console.log(disclosure.markdown);
9191
9355
  }
9192
9356
  //#endregion
9357
+ //#region src/commands/estimate.ts
9358
+ /** `keepPercent` → `--keep-percent`, so the fix hint names a real flag. */
9359
+ function flagFor(field) {
9360
+ return `--${field.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
9361
+ }
9362
+ /**
9363
+ * Answer "what will this telemetry cost me a month" as a JSON envelope.
9364
+ *
9365
+ * Throws `AutotelError` rather than exiting, so the top-level handler picks
9366
+ * pretty or JSON output and maps the code — bad input is the caller's to fix
9367
+ * (exit 2), not a broken run (exit 1).
9368
+ */
9369
+ function runEstimate(input) {
9370
+ try {
9371
+ return {
9372
+ ok: true,
9373
+ command: "estimate",
9374
+ estimate: estimateCost(input)
9375
+ };
9376
+ } catch (error) {
9377
+ if (error instanceof EstimateInputError) throw new AutotelError({
9378
+ type: "validation",
9379
+ code: AutotelErrorCodes.E_INVALID_INPUT,
9380
+ message: error.message,
9381
+ fix: `Pass ${flagFor(error.field)} with a value in ${error.expected}.`,
9382
+ expected: { [error.field]: error.expected }
9383
+ });
9384
+ throw error;
9385
+ }
9386
+ }
9387
+ //#endregion
9388
+ //#region src/ui/estimate.ts
9389
+ const usd = (value) => `$${value.toFixed(2)}`;
9390
+ /**
9391
+ * The human view. Every figure the estimate rests on is printed with it —
9392
+ * a cost number without its basis is a number nobody can argue with, which is
9393
+ * the wrong property for a number that will end up in a budget conversation.
9394
+ */
9395
+ function printEstimate(estimate) {
9396
+ const { before, after, basis } = estimate;
9397
+ const lines = [
9398
+ "",
9399
+ chalk.bold(" Telemetry cost per month"),
9400
+ "",
9401
+ ` ${chalk.dim("today")} ${before.events.toLocaleString()} events ${before.gb} GB ${chalk.bold(usd(before.cost))}`,
9402
+ ` ${chalk.dim("canonical")} ${after.events.toLocaleString()} events ${after.gb} GB ${chalk.bold(usd(after.cost))}`,
9403
+ "",
9404
+ estimate.saved > 0 ? ` ${chalk.green(`saves ${usd(estimate.saved)}/month (${estimate.savedPercent}%)`)}` : ` ${chalk.yellow(`costs ${usd(Math.abs(estimate.saved))}/month more`)}`,
9405
+ "",
9406
+ chalk.dim(` basis: ${basis.logLineBytes} B per log line, ${basis.canonicalLineBytes} B per canonical line, ${basis.spanBytes} B per span (${basis.bytesFrom})`),
9407
+ chalk.dim(` ${usd(basis.perGb)}/GB, ${usd(basis.perMillionEvents)}/million events, ${basis.keepPercent}% of traffic kept`),
9408
+ ""
9409
+ ];
9410
+ process.stdout.write(lines.join("\n") + "\n");
9411
+ }
9412
+ //#endregion
9193
9413
  //#region src/cli.ts
9194
9414
  /**
9195
9415
  * Create the CLI program
@@ -9310,6 +9530,27 @@ function createProgram() {
9310
9530
  codemodCmd.addCommand(traceCmd);
9311
9531
  addGlobalOptions(codemodCmd);
9312
9532
  program.addCommand(codemodCmd);
9533
+ 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) => {
9534
+ configureJsonOutput({
9535
+ outputFile: opts.outputFile,
9536
+ noSecrets: opts.secretsInOutput === false
9537
+ });
9538
+ const envelope = runEstimate({
9539
+ requestsPerMonth: Number(opts.requestsPerMonth),
9540
+ perGb: Number(opts.perGb),
9541
+ ...opts.logLinesPerRequest !== void 0 && { logLinesPerRequest: Number(opts.logLinesPerRequest) },
9542
+ ...opts.spansPerRequest !== void 0 && { spansPerRequest: Number(opts.spansPerRequest) },
9543
+ ...opts.perMillionEvents !== void 0 && { perMillionEvents: Number(opts.perMillionEvents) },
9544
+ ...opts.keepPercent !== void 0 && { keepPercent: Number(opts.keepPercent) }
9545
+ });
9546
+ if (opts.json) {
9547
+ printJson(envelope);
9548
+ return;
9549
+ }
9550
+ printEstimate(envelope.estimate);
9551
+ });
9552
+ addGlobalOptions(estimateCmd);
9553
+ program.addCommand(estimateCmd);
9313
9554
  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
9555
  runSchema({
9315
9556
  outputFile: opts.outputFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-cli",
3
- "version": "0.15.3",
3
+ "version": "0.16.1",
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.1"
28
+ "autotel-mcp": "0.5.1"
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"