autotel-cli 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import * as fs from "node:fs";
4
- import * as path from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+ import * as path$1 from "node:path";
6
+ import path from "node:path";
5
7
  import { execSync } from "node:child_process";
6
- import { randomBytes } from "node:crypto";
8
+ import { createHash, randomBytes } from "node:crypto";
7
9
  import { glob } from "glob";
8
10
  import chalk from "chalk";
9
11
  import { checkbox, confirm, select } from "@inquirer/prompts";
@@ -12,20 +14,23 @@ import * as semver from "semver";
12
14
  import { Project, SyntaxKind } from "ts-morph";
13
15
  import { fileURLToPath } from "node:url";
14
16
  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 { AsyncLocalStorage } from "node:async_hooks";
18
+ import { appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
19
+ import { homedir } from "node:os";
15
20
  //#region src/lib/fs.ts
16
21
  /**
17
22
  * Check if a path is within the allowed root directory
18
23
  */
19
24
  function isPathWithinRoot(targetPath, rootPath) {
20
- const resolvedTarget = path.resolve(targetPath);
21
- const resolvedRoot = path.resolve(rootPath);
22
- return resolvedTarget.startsWith(resolvedRoot + path.sep) || resolvedTarget === resolvedRoot;
25
+ const resolvedTarget = path$1.resolve(targetPath);
26
+ const resolvedRoot = path$1.resolve(rootPath);
27
+ return resolvedTarget.startsWith(resolvedRoot + path$1.sep) || resolvedTarget === resolvedRoot;
23
28
  }
24
29
  /**
25
30
  * Ensure parent directory exists
26
31
  */
27
32
  function ensureDir(filePath) {
28
- const dir = path.dirname(filePath);
33
+ const dir = path$1.dirname(filePath);
29
34
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
30
35
  }
31
36
  /**
@@ -71,7 +76,7 @@ function createBackup(filePath) {
71
76
  * Atomic write - write to temp file then rename
72
77
  */
73
78
  function atomicWrite(filePath, content, options) {
74
- const resolvedPath = path.resolve(filePath);
79
+ const resolvedPath = path$1.resolve(filePath);
75
80
  if (!isPathWithinRoot(resolvedPath, options.root)) throw new Error(`Path traversal detected: ${filePath} resolves outside root ${options.root}`);
76
81
  let backupPath = null;
77
82
  if (options.backup && fileExists(resolvedPath)) backupPath = createBackup(resolvedPath);
@@ -104,17 +109,17 @@ function readJsonSafe(filePath) {
104
109
  * Find a file by walking up the directory tree
105
110
  */
106
111
  function findUpward(startDir, filename, stopAtRoot = true) {
107
- let currentDir = path.resolve(startDir);
108
- const root = path.parse(currentDir).root;
112
+ let currentDir = path$1.resolve(startDir);
113
+ const root = path$1.parse(currentDir).root;
109
114
  while (currentDir !== root) {
110
- const filePath = path.join(currentDir, filename);
115
+ const filePath = path$1.join(currentDir, filename);
111
116
  if (fileExists(filePath)) return filePath;
112
- const parentDir = path.dirname(currentDir);
117
+ const parentDir = path$1.dirname(currentDir);
113
118
  if (parentDir === currentDir) break;
114
119
  currentDir = parentDir;
115
120
  if (stopAtRoot && currentDir === root) break;
116
121
  }
117
- const rootFilePath = path.join(root, filename);
122
+ const rootFilePath = path$1.join(root, filename);
118
123
  if (fileExists(rootFilePath)) return rootFilePath;
119
124
  return null;
120
125
  }
@@ -123,14 +128,14 @@ function findUpward(startDir, filename, stopAtRoot = true) {
123
128
  */
124
129
  function findAllUpward(startDir, filenames) {
125
130
  const found = /* @__PURE__ */ new Map();
126
- let currentDir = path.resolve(startDir);
127
- const root = path.parse(currentDir).root;
131
+ let currentDir = path$1.resolve(startDir);
132
+ const root = path$1.parse(currentDir).root;
128
133
  while (currentDir !== root) {
129
134
  for (const filename of filenames) if (!found.has(filename)) {
130
- const filePath = path.join(currentDir, filename);
135
+ const filePath = path$1.join(currentDir, filename);
131
136
  if (fileExists(filePath)) found.set(filename, filePath);
132
137
  }
133
- const parentDir = path.dirname(currentDir);
138
+ const parentDir = path$1.dirname(currentDir);
134
139
  if (parentDir === currentDir) break;
135
140
  currentDir = parentDir;
136
141
  }
@@ -179,7 +184,7 @@ function detectPackageManager(startDir) {
179
184
  let closestDepth = -1;
180
185
  let closestPM = "npm";
181
186
  for (const [lockfileName, lockfilePath] of foundLockfiles) {
182
- const depth = lockfilePath.split(path.sep).length;
187
+ const depth = lockfilePath.split(path$1.sep).length;
183
188
  const pm = LOCKFILE_MAP[lockfileName];
184
189
  if (pm === void 0) continue;
185
190
  if (depth > closestDepth || depth === closestDepth && PM_PRIORITY.indexOf(pm) < PM_PRIORITY.indexOf(closestPM)) {
@@ -210,28 +215,28 @@ function getInstallCommand(pm, packages, options = {}) {
210
215
  * Detect workspace root markers
211
216
  */
212
217
  function detectWorkspaceRoot(startDir) {
213
- let currentDir = path.resolve(startDir);
214
- const root = path.parse(currentDir).root;
218
+ let currentDir = path$1.resolve(startDir);
219
+ const root = path$1.parse(currentDir).root;
215
220
  while (currentDir !== root) {
216
- if (fileExists(path.join(currentDir, "pnpm-workspace.yaml"))) return {
221
+ if (fileExists(path$1.join(currentDir, "pnpm-workspace.yaml"))) return {
217
222
  workspaceRoot: currentDir,
218
223
  workspaceType: "pnpm"
219
224
  };
220
- if (fileExists(path.join(currentDir, "lerna.json"))) return {
225
+ if (fileExists(path$1.join(currentDir, "lerna.json"))) return {
221
226
  workspaceRoot: currentDir,
222
227
  workspaceType: "lerna"
223
228
  };
224
- const pkgJsonContent = readFileSafe(path.join(currentDir, "package.json"));
229
+ const pkgJsonContent = readFileSafe(path$1.join(currentDir, "package.json"));
225
230
  if (pkgJsonContent) try {
226
231
  if (JSON.parse(pkgJsonContent).workspaces) {
227
- const hasYarnLock = fileExists(path.join(currentDir, "yarn.lock"));
232
+ const hasYarnLock = fileExists(path$1.join(currentDir, "yarn.lock"));
228
233
  return {
229
234
  workspaceRoot: currentDir,
230
235
  workspaceType: hasYarnLock ? "yarn" : "npm"
231
236
  };
232
237
  }
233
238
  } catch {}
234
- const parentDir = path.dirname(currentDir);
239
+ const parentDir = path$1.dirname(currentDir);
235
240
  if (parentDir === currentDir) break;
236
241
  currentDir = parentDir;
237
242
  }
@@ -250,15 +255,15 @@ function findPackageJson(startDir) {
250
255
  if (!packageJsonPath) return null;
251
256
  return {
252
257
  packageJsonPath,
253
- packageRoot: path.dirname(packageJsonPath)
258
+ packageRoot: path$1.dirname(packageJsonPath)
254
259
  };
255
260
  }
256
261
  /**
257
262
  * Detect if project uses TypeScript
258
263
  */
259
264
  function detectTypeScript(packageRoot) {
260
- if (fileExists(path.join(packageRoot, "tsconfig.json"))) return true;
261
- const pkgJson = readJsonSafe(path.join(packageRoot, "package.json"));
265
+ if (fileExists(path$1.join(packageRoot, "tsconfig.json"))) return true;
266
+ const pkgJson = readJsonSafe(path$1.join(packageRoot, "package.json"));
262
267
  if (pkgJson) {
263
268
  if ("typescript" in {
264
269
  ...pkgJson.dependencies,
@@ -289,7 +294,7 @@ function buildWorkspaceInfo(packageRoot, cwd) {
289
294
  * Discover project context from a directory
290
295
  */
291
296
  function discoverProject(cwd) {
292
- const resolvedCwd = path.resolve(cwd);
297
+ const resolvedCwd = path$1.resolve(cwd);
293
298
  const pkgResult = findPackageJson(resolvedCwd);
294
299
  if (!pkgResult) return null;
295
300
  const { packageJsonPath, packageRoot } = pkgResult;
@@ -325,15 +330,15 @@ function getEntrypointCandidates(packageRoot) {
325
330
  "server.js",
326
331
  "app.js",
327
332
  "index.js"
328
- ].map((c) => path.join(packageRoot, c)).filter((p) => fileExists(p));
333
+ ].map((c) => path$1.join(packageRoot, c)).filter((p) => fileExists(p));
329
334
  }
330
335
  /**
331
336
  * Get instrumentation file path based on project conventions
332
337
  */
333
338
  function getInstrumentationPath(packageRoot, hasTypeScript) {
334
- const dir = dirExists(path.join(packageRoot, "src")) || fileExists(path.join(packageRoot, "src", "index.ts")) || fileExists(path.join(packageRoot, "src", "index.js")) ? path.join(packageRoot, "src") : packageRoot;
339
+ const dir = dirExists(path$1.join(packageRoot, "src")) || fileExists(path$1.join(packageRoot, "src", "index.ts")) || fileExists(path$1.join(packageRoot, "src", "index.js")) ? path$1.join(packageRoot, "src") : packageRoot;
335
340
  const ext = hasTypeScript ? "mts" : "mjs";
336
- return path.join(dir, `instrumentation.${ext}`);
341
+ return path$1.join(dir, `instrumentation.${ext}`);
337
342
  }
338
343
  //#endregion
339
344
  //#region src/lib/code-builder.ts
@@ -559,7 +564,7 @@ const INSTRUMENTATION_LOCATIONS = [
559
564
  */
560
565
  function findInstrumentationFile(packageRoot) {
561
566
  for (const location of INSTRUMENTATION_LOCATIONS) {
562
- const filePath = path.join(packageRoot, location);
567
+ const filePath = path$1.join(packageRoot, location);
563
568
  if (fileExists(filePath)) {
564
569
  const content = readFileSafe(filePath);
565
570
  if (content === null) continue;
@@ -589,7 +594,7 @@ function findAutotelYaml(packageRoot) {
589
594
  ".autotelrc.yaml",
590
595
  ".autotelrc.yml"
591
596
  ]) {
592
- const filePath = path.join(packageRoot, candidate);
597
+ const filePath = path$1.join(packageRoot, candidate);
593
598
  if (fileExists(filePath)) return filePath;
594
599
  }
595
600
  return null;
@@ -700,7 +705,7 @@ async function findEnvFiles(packageRoot) {
700
705
  * Check if env var is present in any env file
701
706
  */
702
707
  async function checkEnvVarPresent(packageRoot, varName, specificFile) {
703
- const files = specificFile ? [path.resolve(packageRoot, specificFile)] : await findEnvFiles(packageRoot);
708
+ const files = specificFile ? [path$1.resolve(packageRoot, specificFile)] : await findEnvFiles(packageRoot);
704
709
  for (const file of files) {
705
710
  const content = readFileSafe(file);
706
711
  if (content === null) continue;
@@ -741,6 +746,10 @@ const DEP_TO_PRESET = {
741
746
  "@tanstack/start": "tanstack",
742
747
  "@tanstack/start-server": "tanstack",
743
748
  "@tanstack/start-client": "tanstack",
749
+ "@nestjs/core": "nestjs",
750
+ "@sveltejs/kit": "sveltekit",
751
+ elysia: "elysia",
752
+ nuxt: "nuxt",
744
753
  "posthog-node": "posthog",
745
754
  "@posthog/node": "posthog",
746
755
  mixpanel: "mixpanel",
@@ -752,7 +761,6 @@ const DEP_TO_PRESET = {
752
761
  "drizzle-orm": "drizzle",
753
762
  express: "auto-instr",
754
763
  fastify: "auto-instr",
755
- "@nestjs/core": "auto-instr",
756
764
  next: "auto-instr",
757
765
  pg: "auto-instr",
758
766
  mysql: "auto-instr",
@@ -858,7 +866,7 @@ function collectEnvKeys(opts) {
858
866
  const sources = [];
859
867
  const tryRead = (name, requiresConsent) => {
860
868
  if (requiresConsent && !opts.envConsent) return;
861
- const content = readFileSafe(path.join(opts.packageRoot, name));
869
+ const content = readFileSafe(path$1.join(opts.packageRoot, name));
862
870
  if (content === null) return;
863
871
  sources.push(name);
864
872
  for (const k of parseEnvKeys(content)) keys.add(k);
@@ -892,7 +900,7 @@ function detectBackend(opts) {
892
900
  };
893
901
  }
894
902
  function detectPlatform(packageRoot) {
895
- if (fileExists(path.join(packageRoot, "wrangler.toml")) || fileExists(path.join(packageRoot, "wrangler.jsonc")) || fileExists(path.join(packageRoot, "wrangler.json"))) return "cloudflare";
903
+ if (fileExists(path$1.join(packageRoot, "wrangler.toml")) || fileExists(path$1.join(packageRoot, "wrangler.jsonc")) || fileExists(path$1.join(packageRoot, "wrangler.json"))) return "cloudflare";
896
904
  return null;
897
905
  }
898
906
  /**
@@ -901,7 +909,7 @@ function detectPlatform(packageRoot) {
901
909
  function detectInProject(opts) {
902
910
  const { project } = opts;
903
911
  let rootPkg = null;
904
- if (project.workspace.workspaceRoot !== null && project.workspace.workspaceRoot !== project.packageRoot) rootPkg = readJsonSafe(path.join(project.workspace.workspaceRoot, "package.json"));
912
+ if (project.workspace.workspaceRoot !== null && project.workspace.workspaceRoot !== project.packageRoot) rootPkg = readJsonSafe(path$1.join(project.workspace.workspaceRoot, "package.json"));
905
913
  const deps = mergeDeps(project.packageJson, rootPkg);
906
914
  const detectedPackages = [];
907
915
  const presetSet = /* @__PURE__ */ new Set();
@@ -944,7 +952,7 @@ function detectInProject(opts) {
944
952
  * to read it). .env.example is committed and doesn't trigger this.
945
953
  */
946
954
  function envFilesRequireConsent(packageRoot) {
947
- return fileExists(path.join(packageRoot, ".env")) || fileExists(path.join(packageRoot, ".env.local"));
955
+ return fileExists(path$1.join(packageRoot, ".env")) || fileExists(path$1.join(packageRoot, ".env.local"));
948
956
  }
949
957
  //#endregion
950
958
  //#region src/presets/backends/datadog.ts
@@ -1802,6 +1810,120 @@ const tanstack = {
1802
1810
  ]
1803
1811
  };
1804
1812
  //#endregion
1813
+ //#region src/presets/plugins/nestjs.ts
1814
+ const nestjs = {
1815
+ name: "NestJS",
1816
+ slug: "nestjs",
1817
+ type: "plugin",
1818
+ description: "Request-scoped logging via AutotelInterceptor",
1819
+ packages: {
1820
+ required: ["autotel-adapters"],
1821
+ optional: [],
1822
+ devOnly: []
1823
+ },
1824
+ env: {
1825
+ required: [],
1826
+ optional: []
1827
+ },
1828
+ imports: [{
1829
+ source: "autotel-adapters/nestjs",
1830
+ specifiers: ["AutotelInterceptor"]
1831
+ }, {
1832
+ source: "@nestjs/core",
1833
+ specifiers: ["APP_INTERCEPTOR"]
1834
+ }],
1835
+ configBlock: {
1836
+ type: "plugin",
1837
+ code: `// Register globally in AppModule providers:
1838
+ // { provide: APP_INTERCEPTOR, useClass: AutotelInterceptor }`,
1839
+ section: "PLUGIN_INIT"
1840
+ },
1841
+ nextSteps: ["Register AutotelInterceptor via APP_INTERCEPTOR in AppModule", "Call useLogger() from controllers/services inside requests"]
1842
+ };
1843
+ //#endregion
1844
+ //#region src/presets/plugins/sveltekit.ts
1845
+ const sveltekit = {
1846
+ name: "SvelteKit",
1847
+ slug: "sveltekit",
1848
+ type: "plugin",
1849
+ description: "Server handle hook with request-scoped Autotel logger",
1850
+ packages: {
1851
+ required: ["autotel-adapters"],
1852
+ optional: [],
1853
+ devOnly: []
1854
+ },
1855
+ env: {
1856
+ required: [],
1857
+ optional: []
1858
+ },
1859
+ imports: [{
1860
+ source: "autotel-adapters/sveltekit",
1861
+ specifiers: ["autotelHandle"]
1862
+ }],
1863
+ configBlock: {
1864
+ type: "plugin",
1865
+ code: `// src/hooks.server.ts
1866
+ // export const handle = autotelHandle();`,
1867
+ section: "PLUGIN_INIT"
1868
+ },
1869
+ nextSteps: ["Export autotelHandle() from src/hooks.server.ts", "Use useLogger() in +server.ts routes and server loads"]
1870
+ };
1871
+ //#endregion
1872
+ //#region src/presets/plugins/elysia.ts
1873
+ const elysia = {
1874
+ name: "Elysia",
1875
+ slug: "elysia",
1876
+ type: "plugin",
1877
+ description: "Route handler wrapper with request-scoped logger",
1878
+ packages: {
1879
+ required: ["autotel-adapters"],
1880
+ optional: [],
1881
+ devOnly: []
1882
+ },
1883
+ env: {
1884
+ required: [],
1885
+ optional: []
1886
+ },
1887
+ imports: [{
1888
+ source: "autotel-adapters/elysia",
1889
+ specifiers: ["withAutotelHandler", "useLogger"]
1890
+ }],
1891
+ configBlock: {
1892
+ type: "plugin",
1893
+ code: `// app.get('/health', withAutotelHandler(async ({ path }) => {
1894
+ // useLogger().set({ route: path });
1895
+ // return { ok: true };
1896
+ // }));`,
1897
+ section: "PLUGIN_INIT"
1898
+ },
1899
+ nextSteps: ["Wrap route handlers with withAutotelHandler()", "Call useLogger() inside handlers for request-scoped context"]
1900
+ };
1901
+ //#endregion
1902
+ //#region src/presets/plugins/nuxt.ts
1903
+ const nuxt = {
1904
+ name: "Nuxt",
1905
+ slug: "nuxt",
1906
+ type: "plugin",
1907
+ description: "Nuxt module exposing Nitro Autotel adapters",
1908
+ packages: {
1909
+ required: ["autotel-nuxt", "autotel-adapters"],
1910
+ optional: [],
1911
+ devOnly: []
1912
+ },
1913
+ env: {
1914
+ required: [],
1915
+ optional: []
1916
+ },
1917
+ imports: [],
1918
+ configBlock: {
1919
+ type: "plugin",
1920
+ code: `// nuxt.config.ts
1921
+ // export default defineNuxtConfig({ modules: ['autotel-nuxt'] });`,
1922
+ section: "PLUGIN_INIT"
1923
+ },
1924
+ nextSteps: ["Add modules: ['autotel-nuxt'] to nuxt.config.ts", "Wrap server/api handlers with withAutotelEventHandler from autotel-nuxt/runtime/nitro"]
1925
+ };
1926
+ //#endregion
1805
1927
  //#region src/presets/platforms/aws.ts
1806
1928
  /**
1807
1929
  * AWS Lambda platform preset
@@ -1926,7 +2048,11 @@ const plugins = /* @__PURE__ */ new Map([
1926
2048
  ["sentry", sentry],
1927
2049
  ["hono", hono],
1928
2050
  ["mcp", mcp],
1929
- ["tanstack", tanstack]
2051
+ ["tanstack", tanstack],
2052
+ ["nestjs", nestjs],
2053
+ ["sveltekit", sveltekit],
2054
+ ["elysia", elysia],
2055
+ ["nuxt", nuxt]
1930
2056
  ]);
1931
2057
  /**
1932
2058
  * Platform presets registry
@@ -2061,12 +2187,12 @@ function buildPlanFromDetection(opts) {
2061
2187
  }
2062
2188
  const instrumentationPath = getInstrumentationPath(project.packageRoot, project.hasTypeScript);
2063
2189
  const filesToWrite = [{
2064
- path: path.relative(project.cwd, instrumentationPath),
2190
+ path: path$1.relative(project.cwd, instrumentationPath),
2065
2191
  action: fileExists(instrumentationPath) ? "merge" : "create"
2066
2192
  }];
2067
- const envExamplePath = path.join(project.packageRoot, ".env.example");
2193
+ const envExamplePath = path$1.join(project.packageRoot, ".env.example");
2068
2194
  if (envVars.length > 0 && !fileExists(envExamplePath)) filesToWrite.push({
2069
- path: path.relative(project.cwd, envExamplePath),
2195
+ path: path$1.relative(project.cwd, envExamplePath),
2070
2196
  action: "create"
2071
2197
  });
2072
2198
  const nextSteps = presets.flatMap((p) => p.nextSteps);
@@ -2425,10 +2551,10 @@ function printJson(data) {
2425
2551
  }
2426
2552
  }
2427
2553
  function writeArtifactFile(filePath, content) {
2428
- const resolved = path.resolve(filePath);
2554
+ const resolved = path$1.resolve(filePath);
2429
2555
  if (outputRoot !== null) atomicWrite(resolved, content, { root: outputRoot });
2430
2556
  else {
2431
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
2557
+ fs.mkdirSync(path$1.dirname(resolved), { recursive: true });
2432
2558
  fs.writeFileSync(resolved, content, "utf8");
2433
2559
  }
2434
2560
  }
@@ -2789,7 +2915,7 @@ function applyPlan(args) {
2789
2915
  if (codeFile.backendConfig === null) setBackendConfig(codeFile, "// Local/console mode - no backend configured");
2790
2916
  const newContent = renderCodeFile(codeFile);
2791
2917
  const instrumentationPath = resolveInstrumentationPath(project);
2792
- const envExamplePath = path.join(project.packageRoot, ".env.example");
2918
+ const envExamplePath = path$1.join(project.packageRoot, ".env.example");
2793
2919
  const existing = readFileSafe(instrumentationPath);
2794
2920
  if (existing !== null) {
2795
2921
  const parsed = parseInstrumentation(existing);
@@ -2809,24 +2935,24 @@ function applyPlan(args) {
2809
2935
  ].map((i) => i.source));
2810
2936
  const addedAuto = diffAutoInstrumentations(parsed, codeFile.autoInstrumentations);
2811
2937
  const contentChanged = existing !== newContent;
2812
- if (addedImports.length === 0 && addedAuto.length === 0 && parsed.cliOwned && !contentChanged) result.wroteFiles.push(`${path.relative(project.cwd, instrumentationPath)} (no changes)`);
2938
+ if (addedImports.length === 0 && addedAuto.length === 0 && parsed.cliOwned && !contentChanged) result.wroteFiles.push(`${path$1.relative(project.cwd, instrumentationPath)} (no changes)`);
2813
2939
  else {
2814
2940
  atomicWrite(instrumentationPath, newContent, {
2815
2941
  root: project.packageRoot,
2816
2942
  backup: true
2817
2943
  });
2818
- result.wroteFiles.push(path.relative(project.cwd, instrumentationPath));
2944
+ result.wroteFiles.push(path$1.relative(project.cwd, instrumentationPath));
2819
2945
  }
2820
2946
  } else {
2821
2947
  atomicWrite(instrumentationPath, newContent, { root: project.packageRoot });
2822
- result.wroteFiles.push(path.relative(project.cwd, instrumentationPath));
2948
+ result.wroteFiles.push(path$1.relative(project.cwd, instrumentationPath));
2823
2949
  }
2824
2950
  const envVars = [];
2825
2951
  for (const p of presets) envVars.push(...p.env.required, ...p.env.optional);
2826
2952
  const envExampleContent = generateEnvExample(envVars);
2827
2953
  if (envExampleContent.length > 0 && !fileExists(envExamplePath)) {
2828
2954
  atomicWrite(envExamplePath, envExampleContent, { root: project.packageRoot });
2829
- result.wroteFiles.push(path.relative(project.cwd, envExamplePath));
2955
+ result.wroteFiles.push(path$1.relative(project.cwd, envExamplePath));
2830
2956
  }
2831
2957
  const prod = plan.packagesToInstall.prod;
2832
2958
  const dev = plan.packagesToInstall.dev;
@@ -2862,10 +2988,10 @@ function applyPlan(args) {
2862
2988
  return result;
2863
2989
  }
2864
2990
  function resolveInstrumentationPath(project) {
2865
- const srcDir = path.join(project.packageRoot, "src");
2866
- const dir = fs.existsSync(srcDir) || fileExists(path.join(project.packageRoot, "src", "index.ts")) || fileExists(path.join(project.packageRoot, "src", "index.js")) ? srcDir : project.packageRoot;
2991
+ const srcDir = path$1.join(project.packageRoot, "src");
2992
+ const dir = fs.existsSync(srcDir) || fileExists(path$1.join(project.packageRoot, "src", "index.ts")) || fileExists(path$1.join(project.packageRoot, "src", "index.js")) ? srcDir : project.packageRoot;
2867
2993
  const ext = project.hasTypeScript ? "mts" : "mjs";
2868
- return path.join(dir, `instrumentation.${ext}`);
2994
+ return path$1.join(dir, `instrumentation.${ext}`);
2869
2995
  }
2870
2996
  function resolvePreset(slug) {
2871
2997
  for (const type of [
@@ -3007,12 +3133,12 @@ function checkEsmHook(project) {
3007
3133
  const result = checkRegisterImportOrder(content);
3008
3134
  if (result.found && !result.isFirst) return {
3009
3135
  status: "warn",
3010
- message: `autotel/register import found but not first in ${path.basename(entrypoint)}:${result.lineNumber}`,
3136
+ message: `autotel/register import found but not first in ${path$1.basename(entrypoint)}:${result.lineNumber}`,
3011
3137
  details: ["autotel/register must be the first import for instrumentation to work", "Move it to the top of the file, before any other imports"]
3012
3138
  };
3013
3139
  if (result.found && result.isFirst) return {
3014
3140
  status: "ok",
3015
- message: `autotel/register correctly imported first in ${path.basename(entrypoint)}`
3141
+ message: `autotel/register correctly imported first in ${path$1.basename(entrypoint)}`
3016
3142
  };
3017
3143
  }
3018
3144
  return {
@@ -3059,12 +3185,12 @@ function extractAutoInstrumentations(content) {
3059
3185
  */
3060
3186
  function findSourceFiles(packageRoot) {
3061
3187
  const sourceFiles = [];
3062
- const dirsToCheck = [packageRoot, path.join(packageRoot, "src")].filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
3188
+ const dirsToCheck = [packageRoot, path$1.join(packageRoot, "src")].filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
3063
3189
  for (const dir of dirsToCheck) {
3064
3190
  const files = fs.readdirSync(dir, { recursive: true });
3065
3191
  for (const file of files) {
3066
3192
  if (typeof file !== "string") continue;
3067
- const filePath = path.join(dir, file);
3193
+ const filePath = path$1.join(dir, file);
3068
3194
  try {
3069
3195
  if (fs.statSync(filePath).isFile() && /\.(ts|js|mts|mjs|tsx|jsx)$/.test(file)) sourceFiles.push(filePath);
3070
3196
  } catch {}
@@ -3746,8 +3872,8 @@ function addTraceImport(sourceFile) {
3746
3872
  });
3747
3873
  }
3748
3874
  function expandNamePattern(pattern, name, filePath, cwd) {
3749
- const file = path.basename(filePath, path.extname(filePath));
3750
- const relPath = path.relative(cwd, filePath).replaceAll("\\", "/");
3875
+ const file = path$1.basename(filePath, path$1.extname(filePath));
3876
+ const relPath = path$1.relative(cwd, filePath).replaceAll("\\", "/");
3751
3877
  return pattern.replaceAll("{name}", name).replaceAll("{file}", file).replaceAll("{path}", relPath);
3752
3878
  }
3753
3879
  function getSpanName(name, filePath, options) {
@@ -3979,19 +4105,19 @@ const GLOB_META = /[*?[\]]/;
3979
4105
  */
3980
4106
  async function resolveCodemodFiles(pathArg, cwd) {
3981
4107
  if (!GLOB_META.test(pathArg)) {
3982
- const absolute = path.isAbsolute(pathArg) ? pathArg : path.resolve(cwd, pathArg);
4108
+ const absolute = path$1.isAbsolute(pathArg) ? pathArg : path$1.resolve(cwd, pathArg);
3983
4109
  if (fileExists(absolute)) {
3984
- const ext = path.extname(absolute);
4110
+ const ext = path$1.extname(absolute);
3985
4111
  if (CODEMOD_EXTENSIONS.has(ext) && !absolute.endsWith(".d.ts")) return [absolute];
3986
4112
  return [];
3987
4113
  }
3988
4114
  }
3989
- return (await glob(path.isAbsolute(pathArg) ? pathArg : path.join(cwd, pathArg), {
4115
+ return (await glob(path$1.isAbsolute(pathArg) ? pathArg : path$1.join(cwd, pathArg), {
3990
4116
  cwd,
3991
4117
  absolute: true,
3992
4118
  ignore: ["**/node_modules/**", "**/*.d.ts"]
3993
4119
  })).filter((f) => {
3994
- const ext = path.extname(f);
4120
+ const ext = path$1.extname(f);
3995
4121
  return CODEMOD_EXTENSIONS.has(ext) && !f.endsWith(".d.ts");
3996
4122
  });
3997
4123
  }
@@ -4028,7 +4154,7 @@ async function runCodemodTrace(options) {
4028
4154
  process.exitCode = 1;
4029
4155
  continue;
4030
4156
  }
4031
- const relativePath = path.relative(cwd, filePath);
4157
+ const relativePath = path$1.relative(cwd, filePath);
4032
4158
  if (result.changed) {
4033
4159
  totalWrapped += result.wrappedCount;
4034
4160
  totalChanged += 1;
@@ -4633,6 +4759,38 @@ const INVESTIGATE_COMMANDS = [
4633
4759
  })
4634
4760
  ];
4635
4761
  COMMANDS.push(...INVESTIGATE_COMMANDS);
4762
+ COMMANDS.push(...[
4763
+ {
4764
+ name: "telemetry",
4765
+ description: "Manage opt-in CLI usage telemetry (parent)",
4766
+ mutating: false,
4767
+ flags: []
4768
+ },
4769
+ {
4770
+ name: "telemetry status",
4771
+ description: "Show telemetry consent status",
4772
+ mutating: false,
4773
+ flags: []
4774
+ },
4775
+ {
4776
+ name: "telemetry enable",
4777
+ description: "Enable telemetry for this tool",
4778
+ mutating: true,
4779
+ flags: []
4780
+ },
4781
+ {
4782
+ name: "telemetry disable",
4783
+ description: "Disable telemetry and purge undelivered events",
4784
+ mutating: true,
4785
+ flags: []
4786
+ },
4787
+ {
4788
+ name: "telemetry disclosure",
4789
+ description: "Print telemetry disclosure markdown",
4790
+ mutating: false,
4791
+ flags: []
4792
+ }
4793
+ ]);
4636
4794
  function getCommand(name) {
4637
4795
  return COMMANDS.find((c) => c.name === name);
4638
4796
  }
@@ -4714,14 +4872,14 @@ function configure(opts) {
4714
4872
  }
4715
4873
  function readSelfVersion() {
4716
4874
  try {
4717
- let dir = path.dirname(fileURLToPath(import.meta.url));
4875
+ let dir = path$1.dirname(fileURLToPath(import.meta.url));
4718
4876
  for (let i = 0; i < 5; i++) {
4719
- const candidate = path.join(dir, "package.json");
4877
+ const candidate = path$1.join(dir, "package.json");
4720
4878
  if (fs.existsSync(candidate)) {
4721
4879
  const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
4722
4880
  if (pkg.name === "autotel-cli") return String(pkg.version ?? "0.0.0");
4723
4881
  }
4724
- dir = path.dirname(dir);
4882
+ dir = path$1.dirname(dir);
4725
4883
  }
4726
4884
  } catch {}
4727
4885
  return "0.0.0";
@@ -6187,6 +6345,291 @@ function registerSecurityCommands(program) {
6187
6345
  program.addCommand(securityCmd);
6188
6346
  }
6189
6347
  //#endregion
6348
+ //#region ../autotel-telemetry/dist/index.js
6349
+ function getTelemetryDir(toolName) {
6350
+ const safe = toolName.replaceAll(/[^a-zA-Z0-9._-]/g, "-");
6351
+ return path.join(homedir(), ".autotel", "telemetry", safe);
6352
+ }
6353
+ var TelemetryOutbox = class {
6354
+ dir;
6355
+ filePath;
6356
+ constructor(options) {
6357
+ this.dir = getTelemetryDir(options.toolName);
6358
+ this.filePath = path.join(this.dir, "outbox.ndjson");
6359
+ }
6360
+ async append(event) {
6361
+ await mkdir(this.dir, { recursive: true });
6362
+ await appendFile(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
6363
+ }
6364
+ async readAll() {
6365
+ try {
6366
+ return (await readFile(this.filePath, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line));
6367
+ } catch {
6368
+ return [];
6369
+ }
6370
+ }
6371
+ async purge() {
6372
+ try {
6373
+ await unlink(this.filePath);
6374
+ } catch {}
6375
+ }
6376
+ };
6377
+ function resolveConsent(toolName) {
6378
+ if (process.env.DO_NOT_TRACK === "1") return false;
6379
+ if (process.env.AUTOTEL_TELEMETRY === "0") return false;
6380
+ if (process.env.AUTOTEL_TELEMETRY === "1") return true;
6381
+ const pref = readPreferenceSync(toolName);
6382
+ if (pref === "disabled") return false;
6383
+ if (pref === "enabled") return true;
6384
+ return false;
6385
+ }
6386
+ function readPreferenceSync(toolName) {
6387
+ try {
6388
+ const raw = readFileSync(path.join(getTelemetryDir(toolName), "preference.json"), "utf8");
6389
+ return JSON.parse(raw).preference ?? "unset";
6390
+ } catch {
6391
+ return "unset";
6392
+ }
6393
+ }
6394
+ async function writePreference(toolName, preference) {
6395
+ const dir = getTelemetryDir(toolName);
6396
+ await mkdir(dir, { recursive: true });
6397
+ await writeFile(path.join(dir, "preference.json"), JSON.stringify({ preference }), "utf8");
6398
+ }
6399
+ async function purgeOutbox(toolName) {
6400
+ await new TelemetryOutbox({ toolName }).purge();
6401
+ }
6402
+ function createNoopDrain() {
6403
+ return async () => {};
6404
+ }
6405
+ function createDebugDrain() {
6406
+ return async (events) => {
6407
+ if (process.env.AUTOTEL_TELEMETRY_DEBUG === "1") for (const event of events) console.error("[autotel-telemetry]", JSON.stringify(event));
6408
+ };
6409
+ }
6410
+ function createHttpDrain(endpoint) {
6411
+ return async (events) => {
6412
+ if (events.length === 0) return;
6413
+ const response = await fetch(endpoint, {
6414
+ method: "POST",
6415
+ headers: { "content-type": "application/json" },
6416
+ body: JSON.stringify({ events })
6417
+ });
6418
+ if (!response.ok) throw new Error(`Telemetry delivery failed with HTTP ${response.status}`);
6419
+ };
6420
+ }
6421
+ function composeDrains(...drains) {
6422
+ return async (events) => {
6423
+ await Promise.all(drains.map((drain) => drain(events)));
6424
+ };
6425
+ }
6426
+ function resolveEndpoint(explicit) {
6427
+ return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
6428
+ }
6429
+ const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
6430
+ function sanitizeFlags(argv, allowlistedStringFlags = []) {
6431
+ const out = {};
6432
+ for (let i = 0; i < argv.length; i++) {
6433
+ const arg = argv[i];
6434
+ if (arg === void 0 || !arg.startsWith("-")) continue;
6435
+ const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
6436
+ if (SENSITIVE_PATTERN.test(key)) {
6437
+ out[key] = { present: true };
6438
+ continue;
6439
+ }
6440
+ if (arg.includes("=")) {
6441
+ const [, rawValue] = arg.split("=");
6442
+ if (rawValue === void 0) continue;
6443
+ if (/^\d+$/.test(rawValue)) out[key] = Number(rawValue);
6444
+ else if (allowlistedStringFlags.includes(key)) out[key] = rawValue;
6445
+ else out[key] = { present: true };
6446
+ continue;
6447
+ }
6448
+ const next = argv[i + 1];
6449
+ if (next && !next.startsWith("-")) if (/^\d+$/.test(next)) {
6450
+ out[key] = Number(next);
6451
+ i++;
6452
+ } else if (allowlistedStringFlags.includes(key)) {
6453
+ out[key] = next;
6454
+ i++;
6455
+ } else {
6456
+ out[key] = { present: true };
6457
+ i++;
6458
+ }
6459
+ else out[key] = true;
6460
+ }
6461
+ return out;
6462
+ }
6463
+ function sanitizeCustom(fields) {
6464
+ const out = {};
6465
+ for (const [key, value] of Object.entries(fields)) if (typeof value === "boolean" || typeof value === "number") out[key] = value;
6466
+ else if (value !== null && value !== void 0) out[key] = { present: true };
6467
+ return out;
6468
+ }
6469
+ const runStorage = new AsyncLocalStorage();
6470
+ let activeHandle = null;
6471
+ var InternalTelemetry = class {
6472
+ enabled;
6473
+ outbox;
6474
+ drain;
6475
+ options;
6476
+ constructor(options, enabled) {
6477
+ this.options = options;
6478
+ this.enabled = enabled;
6479
+ this.outbox = new TelemetryOutbox({ toolName: options.name });
6480
+ const endpoint = resolveEndpoint(options.endpoint);
6481
+ const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
6482
+ this.drain = composeDrains(...drains);
6483
+ }
6484
+ set(fields) {
6485
+ const ctx = runStorage.getStore();
6486
+ if (!ctx || !this.enabled) return;
6487
+ ctx.custom = sanitizeCustom({
6488
+ ...ctx.custom,
6489
+ ...fields
6490
+ });
6491
+ }
6492
+ async finish(outcome) {
6493
+ if (!this.enabled || !resolveConsent(this.options.name)) return;
6494
+ const ctx = runStorage.getStore();
6495
+ if (!ctx) return;
6496
+ const event = {
6497
+ tool: this.options.name,
6498
+ version: this.options.version,
6499
+ command: ctx.command,
6500
+ outcome,
6501
+ durationMs: Date.now() - ctx.startedAt,
6502
+ flags: ctx.flags,
6503
+ custom: Object.keys(ctx.custom).length > 0 ? ctx.custom : void 0,
6504
+ ci: Boolean(process.env.CI),
6505
+ machineId: hashMachineId()
6506
+ };
6507
+ try {
6508
+ await this.outbox.append(event);
6509
+ const pending = await this.outbox.readAll();
6510
+ await this.drain(pending);
6511
+ await this.outbox.purge();
6512
+ } catch {}
6513
+ }
6514
+ async flush() {
6515
+ if (!this.enabled || !resolveConsent(this.options.name)) return;
6516
+ await Promise.race([(async () => {
6517
+ const pending = await this.outbox.readAll();
6518
+ await this.drain(pending);
6519
+ await this.outbox.purge();
6520
+ })(), new Promise((resolve) => setTimeout(resolve, 500))]);
6521
+ }
6522
+ };
6523
+ function hashMachineId() {
6524
+ const seed = `${process.platform}:${process.arch}:${process.env.USER ?? "unknown"}`;
6525
+ return createHash("sha256").update(seed).digest("hex").slice(0, 16);
6526
+ }
6527
+ function createTelemetry(options) {
6528
+ activeHandle = new InternalTelemetry(options, resolveConsent(options.name));
6529
+ return activeHandle;
6530
+ }
6531
+ async function runWithTelemetry(options, command, argv, fn) {
6532
+ const handle = createTelemetry(options);
6533
+ const ctx = {
6534
+ command,
6535
+ startedAt: Date.now(),
6536
+ custom: {},
6537
+ flags: sanitizeFlags(argv, options.allowlistedStringFlags)
6538
+ };
6539
+ return runStorage.run(ctx, async () => {
6540
+ let outcome = "success";
6541
+ try {
6542
+ return await fn();
6543
+ } catch (error) {
6544
+ outcome = "failure";
6545
+ throw error;
6546
+ } finally {
6547
+ if (handle.enabled) await handle.finish(outcome);
6548
+ }
6549
+ });
6550
+ }
6551
+ function withCommanderTelemetry(program, options) {
6552
+ const originalParse = program.parseAsync.bind(program);
6553
+ program.parseAsync = async (argv) => {
6554
+ const command = argv[2] ?? "help";
6555
+ return runWithTelemetry({
6556
+ name: options?.name ?? program.name(),
6557
+ version: options?.version ?? program.version(),
6558
+ ...options
6559
+ }, command, argv.slice(3), () => originalParse(argv));
6560
+ };
6561
+ return program;
6562
+ }
6563
+ function getTelemetryStatus(toolName) {
6564
+ return {
6565
+ enabled: resolveConsent(toolName),
6566
+ preference: readPreferenceSync(toolName)
6567
+ };
6568
+ }
6569
+ async function enableTelemetry(toolName) {
6570
+ await writePreference(toolName, "enabled");
6571
+ }
6572
+ async function disableTelemetry(toolName) {
6573
+ await writePreference(toolName, "disabled");
6574
+ await purgeOutbox(toolName);
6575
+ }
6576
+ function generateDisclosure(options) {
6577
+ const json = {
6578
+ tool: options.name,
6579
+ version: options.version,
6580
+ collected: [
6581
+ "command",
6582
+ "outcome",
6583
+ "durationMs",
6584
+ "sanitized flags",
6585
+ "optional custom counters"
6586
+ ],
6587
+ notCollected: [
6588
+ "raw argv strings",
6589
+ "paths",
6590
+ "tokens",
6591
+ "secrets"
6592
+ ],
6593
+ optOut: [
6594
+ "DO_NOT_TRACK=1",
6595
+ "AUTOTEL_TELEMETRY=0",
6596
+ "autotel telemetry disable"
6597
+ ]
6598
+ };
6599
+ return {
6600
+ markdown: `# ${options.name} telemetry disclosure
6601
+
6602
+ Collects one structured event per command: command name, outcome, duration, sanitized flags, and optional counters via \`telemetry.set()\`.
6603
+
6604
+ Does not collect raw paths, tokens, or secret-shaped flag values.
6605
+
6606
+ Opt out with \`DO_NOT_TRACK=1\`, \`AUTOTEL_TELEMETRY=0\`, or \`autotel telemetry disable\`.
6607
+ `,
6608
+ json
6609
+ };
6610
+ }
6611
+ //#endregion
6612
+ //#region src/commands/telemetry.ts
6613
+ async function runTelemetryStatus(toolName) {
6614
+ const status = getTelemetryStatus(toolName);
6615
+ console.log(JSON.stringify(status, null, 2));
6616
+ }
6617
+ async function runTelemetryEnable(toolName) {
6618
+ await enableTelemetry(toolName);
6619
+ console.log(`Telemetry enabled for ${toolName}`);
6620
+ }
6621
+ async function runTelemetryDisable(toolName) {
6622
+ await disableTelemetry(toolName);
6623
+ console.log(`Telemetry disabled for ${toolName}`);
6624
+ }
6625
+ function runTelemetryDisclosure(toolName, version) {
6626
+ const disclosure = generateDisclosure({
6627
+ name: toolName,
6628
+ version
6629
+ });
6630
+ console.log(disclosure.markdown);
6631
+ }
6632
+ //#endregion
6190
6633
  //#region src/cli.ts
6191
6634
  /**
6192
6635
  * Create the CLI program
@@ -6325,6 +6768,20 @@ function createProgram() {
6325
6768
  registerScoreCommands(program);
6326
6769
  registerCollectorCommands(program);
6327
6770
  registerSecurityCommands(program);
6771
+ const telemetryCmd = new Command("telemetry").description("Manage opt-in CLI usage telemetry");
6772
+ telemetryCmd.command("status").description("Show telemetry consent status").action(async () => {
6773
+ await runTelemetryStatus("autotel");
6774
+ });
6775
+ telemetryCmd.command("enable").description("Enable telemetry for this tool").action(async () => {
6776
+ await runTelemetryEnable("autotel");
6777
+ });
6778
+ telemetryCmd.command("disable").description("Disable telemetry and purge undelivered events").action(async () => {
6779
+ await runTelemetryDisable("autotel");
6780
+ });
6781
+ telemetryCmd.command("disclosure").description("Print telemetry disclosure markdown").action(() => {
6782
+ runTelemetryDisclosure("autotel", program.version());
6783
+ });
6784
+ program.addCommand(telemetryCmd);
6328
6785
  return program;
6329
6786
  }
6330
6787
  /**
@@ -6337,7 +6794,11 @@ function createProgram() {
6337
6794
  * which the top-level handler in `index.ts` converts to an envelope.
6338
6795
  */
6339
6796
  async function run() {
6340
- const program = createProgram();
6797
+ const base = createProgram();
6798
+ const program = withCommanderTelemetry(base, {
6799
+ name: "autotel",
6800
+ version: base.version()
6801
+ });
6341
6802
  program.exitOverride();
6342
6803
  const argvJoined = process.argv.slice(2).join(" ");
6343
6804
  const isJsonOnly = process.argv.includes("--json") || /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(argvJoined);