@prisma-next/cli-telemetry 0.14.0-dev.6 → 0.14.0-dev.60

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.
@@ -1,61 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { determineAgent } from "@vercel/detect-agent";
2
3
  import { join } from "pathe";
3
- //#region src/detect-agent.ts
4
- const AGENT_MARKERS = [
5
- {
6
- envVar: "CLAUDECODE",
7
- agent: "Claude Code"
8
- },
9
- {
10
- envVar: "CURSOR_AGENT",
11
- agent: "Cursor"
12
- },
13
- {
14
- envVar: "CODEX_SANDBOX",
15
- agent: "Codex CLI"
16
- },
17
- {
18
- envVar: "GEMINI_CLI",
19
- agent: "Gemini CLI"
20
- },
21
- {
22
- envVar: "WINDSURF",
23
- agent: "Windsurf"
24
- },
25
- {
26
- envVar: "AIDER",
27
- agent: "Aider"
28
- },
29
- {
30
- envVar: "CODY",
31
- agent: "Cody"
32
- },
33
- {
34
- envVar: "CONTINUE",
35
- agent: "Continue"
36
- }
37
- ];
38
- function isTruthyMarker(raw) {
39
- if (raw === void 0) return false;
40
- const normalised = raw.trim().toLowerCase();
41
- if (normalised === "") return false;
42
- if (normalised === "0") return false;
43
- if (normalised === "false") return false;
44
- return true;
45
- }
46
- /**
47
- * Resolve the agent label from an env snapshot, or `null` if no marker
48
- * is set. Returns the **first** matching marker in `AGENT_MARKERS`
49
- * order, so when multiple markers are set the agent label is
50
- * deterministic and the allowlist's first entry wins.
51
- *
52
- * Pure: takes an env record, returns a string or null. No I/O.
53
- */
54
- function detectAgent(env) {
55
- for (const marker of AGENT_MARKERS) if (isTruthyMarker(env[marker.envVar])) return marker.agent;
56
- return null;
57
- }
58
- //#endregion
59
4
  //#region src/enrich.ts
60
5
  const EMPTY_PROJECT_CONFIG = {
61
6
  databaseTarget: null,
@@ -173,11 +118,25 @@ function buildTelemetryEvent(payload, projectConfig, env) {
173
118
  packageManager: parsePackageManager(env.env["npm_config_user_agent"]),
174
119
  databaseTarget: projectConfig.databaseTarget,
175
120
  tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()),
176
- agent: detectAgent(env.env),
121
+ agent: env.agent,
177
122
  extensions: projectConfig.extensions
178
123
  };
179
124
  }
180
125
  /**
126
+ * Resolve the agent label for the telemetry event via
127
+ * `@vercel/detect-agent`, collapsing its discriminated result to the
128
+ * event's `string | null` shape. Any detection failure counts as
129
+ * "no agent" — telemetry is best-effort and non-blocking.
130
+ */
131
+ async function resolveAgentLabel() {
132
+ try {
133
+ const result = await determineAgent();
134
+ return result.isAgent ? result.agent.name : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ /**
181
140
  * Convenience for the sender entry: build the event from the live
182
141
  * `process` plus a c12 load of `prisma-next.config.*` from
183
142
  * `payload.projectRoot` plus a real project-package.json reader,
@@ -199,6 +158,7 @@ async function buildTelemetryEventFromProcess(payload) {
199
158
  arch: process.arch,
200
159
  versions: process.versions,
201
160
  env: process.env,
161
+ agent: await resolveAgentLabel(),
202
162
  readProjectPackageJson: () => {
203
163
  try {
204
164
  return readFileSync(join(payload.projectRoot, "package.json"), "utf-8");
@@ -211,4 +171,4 @@ async function buildTelemetryEventFromProcess(payload) {
211
171
  //#endregion
212
172
  export { loadProjectConfig as n, buildTelemetryEventFromProcess as t };
213
173
 
214
- //# sourceMappingURL=enrich-BhHLLBfL.mjs.map
174
+ //# sourceMappingURL=enrich-DZlKsZox.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enrich-DZlKsZox.mjs","names":[],"sources":["../src/enrich.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport type { PrismaNextConfig } from '@prisma-next/config/config-types';\nimport { determineAgent } from '@vercel/detect-agent';\nimport { join } from 'pathe';\nimport type { ParentToSenderPayload, TelemetryEvent } from './payload';\n\n/**\n * Subset of the user's `prisma-next.config.*` the telemetry event\n * surfaces. Loaded inside the detached child via {@link loadProjectConfig}\n * — see the design rationale on {@link ParentToSenderPayload} for why\n * this side runs c12 instead of the parent CLI.\n */\nexport interface ProjectConfigFields {\n readonly databaseTarget: string | null;\n readonly extensions: readonly string[];\n}\n\nconst EMPTY_PROJECT_CONFIG: ProjectConfigFields = {\n databaseTarget: null,\n extensions: [],\n};\n\n/**\n * Best-effort load of `prisma-next.config.*` from `projectRoot`,\n * validated against the canonical `@prisma-next/config` schema.\n * Returns `{ databaseTarget: null, extensions: [] }` on any failure\n * mode — missing config file (e.g. before `prisma-next init`), c12\n * throws while evaluating user TS, validator rejects a malformed\n * shape, etc. Telemetry is non-blocking and best-effort; an empty\n * result is the only downside of an unloadable or invalid config.\n *\n * Both `c12` and `@prisma-next/config/config-validation` are imported\n * lazily so the detached sender's cold-start cost is paid only when\n * telemetry actually fires, not on every fork even when gates\n * short-circuit before reaching this code path.\n */\nexport async function loadProjectConfig(projectRoot: string): Promise<ProjectConfigFields> {\n try {\n const { loadConfig } = await import('c12');\n const result = await loadConfig<Record<string, unknown>>({\n name: 'prisma-next',\n cwd: projectRoot,\n dotenv: false,\n rcFile: false,\n globalRc: false,\n });\n const config = result.config ?? null;\n // c12 returns an empty object when no config file exists in the\n // search path — distinct from \"file existed but parsed to an empty\n // object\". Either way, the canonical validator below would reject\n // it on the first required field (`family`), so short-circuit\n // without paying the import cost.\n if (config === null || Object.keys(config).length === 0) {\n return EMPTY_PROJECT_CONFIG;\n }\n const validation = await import('@prisma-next/config/config-validation');\n // TS 4.7+ only flows `asserts cfg is X` narrowing when the\n // assertion function is called via a directly-declared name with\n // an explicit signature. The dynamic-import binding doesn't\n // satisfy that, so wrap the call in a local declaration that\n // re-asserts the signature.\n const validate: (cfg: unknown) => asserts cfg is PrismaNextConfig = validation.validateConfig;\n validate(config);\n return {\n databaseTarget: config.target.targetId,\n extensions: (config.extensionPacks ?? []).map((pack) => pack.id),\n };\n } catch {\n return EMPTY_PROJECT_CONFIG;\n }\n}\n\n/**\n * Versions surface the enrichment cares about. Modelled as a structural\n * record with a required `node` field so tests can pass a literal object\n * without faking every field of `NodeJS.ProcessVersions` (which adds\n * properties between Node versions and includes a long tail the\n * enrichment never touches). Both `bun` and `deno` are read on the\n * runtime-resolution path; everything else is ignored.\n */\nexport interface VersionsSnapshot {\n readonly node: string;\n readonly bun?: string;\n readonly deno?: string;\n}\n\n/**\n * Snapshot of process-level inputs the enrichment reads. Tests pass an\n * explicit snapshot so the enrichment is deterministic per case; the\n * sender entry point passes a fresh snapshot from `process`.\n */\nexport interface EnrichEnvironment {\n readonly platform: NodeJS.Platform;\n readonly arch: string;\n readonly versions: VersionsSnapshot;\n /**\n * Included because package-manager detection intentionally reads\n * environment variables from the same process snapshot as platform/versions.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n /**\n * Pre-resolved AI coding-agent label, or `null` for a human session.\n * Detection lives in `@vercel/detect-agent`, whose `determineAgent()`\n * reads the live `process.env` and is async (it probes the filesystem\n * for Devin), so it cannot run inside the pure event builder; the\n * sender entry resolves it via {@link resolveAgentLabel} and passes\n * the label here. Detection runs in the **child** sender process,\n * never the parent. Best-effort: false negatives are expected and\n * documented in the user-facing telemetry docs.\n */\n readonly agent: string | null;\n /**\n * Best-effort reader for the project's `package.json`, used only to derive\n * the optional `tsVersion` telemetry field. Returning `null` means unknown.\n */\n readonly readProjectPackageJson: () => string | null;\n}\n\n/**\n * Identify the runtime the sender is running in. Same-runtime as the\n * parent is a correctness requirement: the parent forked us via\n * `child_process.fork`, which inherits the parent's runtime. Detection\n * keys on the runtime-specific version field rather than env vars so a\n * spoofed env can't lie about the actual interpreter.\n */\nfunction resolveRuntime(versions: VersionsSnapshot): {\n readonly name: 'node' | 'bun' | 'deno';\n readonly version: string;\n} {\n if (versions.bun !== undefined) {\n return { name: 'bun', version: versions.bun };\n }\n if (versions.deno !== undefined) {\n return { name: 'deno', version: versions.deno };\n }\n return { name: 'node', version: versions.node };\n}\n\n/**\n * Parse `npm_config_user_agent` into a `<pm>/<version>` token. The\n * value, when present, looks like\n * `\"pnpm/10.27.0 npm/? node/v24.13.0 darwin arm64\"` — we take the first\n * whitespace-separated token. Any failure → `null`.\n */\nexport function parsePackageManager(userAgent: string | undefined): string | null {\n if (userAgent === undefined) return null;\n const first = userAgent.split(/\\s+/)[0];\n if (first === undefined || first.length === 0) return null;\n if (!first.includes('/')) return null;\n return first;\n}\n\n/**\n * Read the user's project `package.json` and resolve a TypeScript\n * version from `devDependencies.typescript` (preferred) or\n * `dependencies.typescript`. Strips a leading `^` or `~` semver\n * prefix. Returns `null` on any failure mode — file missing,\n * unreadable, malformed JSON, key absent, not a string.\n */\nexport function readTsVersionFromPackageJson(raw: string | null): string | null {\n if (raw === null) return null;\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n const candidate =\n pickStringDep(parsed['devDependencies']) ?? pickStringDep(parsed['dependencies']);\n if (candidate === null) return null;\n return candidate.replace(/^[\\^~]/, '');\n}\n\nfunction pickStringDep(deps: unknown): string | null {\n if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) return null;\n const value = (deps as Record<string, unknown>)['typescript'];\n return typeof value === 'string' ? value : null;\n}\n\n/**\n * Build the full backend event from the parent's payload, the\n * c12-loaded project-config slice, and the child's per-process\n * snapshot. Pure given a `projectConfig` + `EnrichEnvironment`.\n */\nexport function buildTelemetryEvent(\n payload: ParentToSenderPayload,\n projectConfig: ProjectConfigFields,\n env: EnrichEnvironment,\n): TelemetryEvent {\n const runtime = resolveRuntime(env.versions);\n return {\n installationId: payload.installationId,\n version: payload.version,\n command: payload.command,\n flags: payload.flags,\n runtimeName: runtime.name,\n runtimeVersion: runtime.version,\n os: env.platform,\n arch: env.arch,\n packageManager: parsePackageManager(env.env['npm_config_user_agent']),\n databaseTarget: projectConfig.databaseTarget,\n tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()),\n agent: env.agent,\n extensions: projectConfig.extensions,\n };\n}\n\n/**\n * Resolve the agent label for the telemetry event via\n * `@vercel/detect-agent`, collapsing its discriminated result to the\n * event's `string | null` shape. Any detection failure counts as\n * \"no agent\" — telemetry is best-effort and non-blocking.\n */\nasync function resolveAgentLabel(): Promise<string | null> {\n try {\n const result = await determineAgent();\n return result.isAgent ? result.agent.name : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Convenience for the sender entry: build the event from the live\n * `process` plus a c12 load of `prisma-next.config.*` from\n * `payload.projectRoot` plus a real project-package.json reader,\n * swallowing any I/O errors in the file read.\n *\n * The parent's `payload.databaseTarget` (when present) wins over the\n * c12-derived value. The parent sets this for the first-`init` run,\n * where the config file does not exist on disk yet but the user has\n * just declared a target via the consent prompt; every other\n * invocation leaves it unset and the c12 load supplies the value.\n */\nexport async function buildTelemetryEventFromProcess(\n payload: ParentToSenderPayload,\n): Promise<TelemetryEvent> {\n const loadedConfig = await loadProjectConfig(payload.projectRoot);\n const projectConfig: ProjectConfigFields = {\n databaseTarget: payload.databaseTarget ?? loadedConfig.databaseTarget,\n extensions: loadedConfig.extensions,\n };\n return buildTelemetryEvent(payload, projectConfig, {\n platform: process.platform,\n arch: process.arch,\n versions: process.versions,\n env: process.env,\n agent: await resolveAgentLabel(),\n readProjectPackageJson: () => {\n try {\n return readFileSync(join(payload.projectRoot, 'package.json'), 'utf-8');\n } catch {\n return null;\n }\n },\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as loadProjectConfig } from "../enrich-BhHLLBfL.mjs";
1
+ import { n as loadProjectConfig } from "../enrich-DZlKsZox.mjs";
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "pathe";
4
4
  import { fork } from "node:child_process";
package/dist/sender.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as buildTelemetryEventFromProcess } from "./enrich-BhHLLBfL.mjs";
1
+ import { t as buildTelemetryEventFromProcess } from "./enrich-DZlKsZox.mjs";
2
2
  import { type } from "arktype";
3
3
  //#region src/payload.ts
4
4
  /**
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "@prisma-next/cli-telemetry",
3
- "version": "0.14.0-dev.6",
3
+ "version": "0.14.0-dev.60",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "CLI telemetry client for Prisma Next: detached subprocess sender, gating resolution, user-config store, and consent surface",
8
8
  "dependencies": {
9
- "@prisma-next/config": "0.14.0-dev.6",
10
- "@prisma-next/utils": "0.14.0-dev.6",
9
+ "@prisma-next/config": "0.14.0-dev.60",
10
+ "@prisma-next/utils": "0.14.0-dev.60",
11
+ "@vercel/detect-agent": "^1.2.3",
11
12
  "arktype": "^2.2.0",
12
13
  "c12": "^3.3.4",
13
14
  "pathe": "^2.0.3"
14
15
  },
15
16
  "devDependencies": {
16
- "@prisma-next/test-utils": "0.14.0-dev.6",
17
- "@prisma-next/tsconfig": "0.14.0-dev.6",
18
- "@prisma-next/tsdown": "0.14.0-dev.6",
17
+ "@prisma-next/test-utils": "0.14.0-dev.60",
18
+ "@prisma-next/tsconfig": "0.14.0-dev.60",
19
+ "@prisma-next/tsdown": "0.14.0-dev.60",
19
20
  "@types/node": "25.9.1",
20
21
  "tsdown": "0.22.1",
21
22
  "typescript": "5.9.3",
package/src/enrich.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import type { PrismaNextConfig } from '@prisma-next/config/config-types';
3
+ import { determineAgent } from '@vercel/detect-agent';
3
4
  import { join } from 'pathe';
4
- import { detectAgent } from './detect-agent';
5
5
  import type { ParentToSenderPayload, TelemetryEvent } from './payload';
6
6
 
7
7
  /**
@@ -94,10 +94,21 @@ export interface EnrichEnvironment {
94
94
  readonly arch: string;
95
95
  readonly versions: VersionsSnapshot;
96
96
  /**
97
- * Included because package-manager and agent detection intentionally read
97
+ * Included because package-manager detection intentionally reads
98
98
  * environment variables from the same process snapshot as platform/versions.
99
99
  */
100
100
  readonly env: Readonly<Record<string, string | undefined>>;
101
+ /**
102
+ * Pre-resolved AI coding-agent label, or `null` for a human session.
103
+ * Detection lives in `@vercel/detect-agent`, whose `determineAgent()`
104
+ * reads the live `process.env` and is async (it probes the filesystem
105
+ * for Devin), so it cannot run inside the pure event builder; the
106
+ * sender entry resolves it via {@link resolveAgentLabel} and passes
107
+ * the label here. Detection runs in the **child** sender process,
108
+ * never the parent. Best-effort: false negatives are expected and
109
+ * documented in the user-facing telemetry docs.
110
+ */
111
+ readonly agent: string | null;
101
112
  /**
102
113
  * Best-effort reader for the project's `package.json`, used only to derive
103
114
  * the optional `tsVersion` telemetry field. Returning `null` means unknown.
@@ -189,11 +200,26 @@ export function buildTelemetryEvent(
189
200
  packageManager: parsePackageManager(env.env['npm_config_user_agent']),
190
201
  databaseTarget: projectConfig.databaseTarget,
191
202
  tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()),
192
- agent: detectAgent(env.env),
203
+ agent: env.agent,
193
204
  extensions: projectConfig.extensions,
194
205
  };
195
206
  }
196
207
 
208
+ /**
209
+ * Resolve the agent label for the telemetry event via
210
+ * `@vercel/detect-agent`, collapsing its discriminated result to the
211
+ * event's `string | null` shape. Any detection failure counts as
212
+ * "no agent" — telemetry is best-effort and non-blocking.
213
+ */
214
+ async function resolveAgentLabel(): Promise<string | null> {
215
+ try {
216
+ const result = await determineAgent();
217
+ return result.isAgent ? result.agent.name : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
197
223
  /**
198
224
  * Convenience for the sender entry: build the event from the live
199
225
  * `process` plus a c12 load of `prisma-next.config.*` from
@@ -219,6 +245,7 @@ export async function buildTelemetryEventFromProcess(
219
245
  arch: process.arch,
220
246
  versions: process.versions,
221
247
  env: process.env,
248
+ agent: await resolveAgentLabel(),
222
249
  readProjectPackageJson: () => {
223
250
  try {
224
251
  return readFileSync(join(payload.projectRoot, 'package.json'), 'utf-8');
@@ -1 +0,0 @@
1
- {"version":3,"file":"enrich-BhHLLBfL.mjs","names":[],"sources":["../src/detect-agent.ts","../src/enrich.ts"],"sourcesContent":["/**\n * Best-effort identification of AI coding-agent sessions from an\n * env-var allowlist. Detector property: false positives are negligible\n * (a marker present ⇒ confidently an agent); false negatives are\n * expected and documented in the user-facing telemetry docs. New\n * entries should land here, not in per-CLI hand-rolls.\n *\n * Each entry is a `(envVar, agent)` pair with uniform comparison shape:\n * the marker counts as \"present\" when `process.env[envVar]` is set to a\n * truthy string. Truthy = anything other than the empty string, `'0'`,\n * or `'false'` (case-insensitive); see `gating.isTruthyOptOut` for the\n * same convention applied to opt-out env vars.\n *\n * The detector runs in the **child** sender process, never the parent;\n * the parent does not probe env at command start.\n *\n * Codex CLI note: `CODEX_SANDBOX` is the only clear marker available here.\n * Non-sandboxed Codex sessions may be false negatives.\n *\n * TODO: a ci-info-for-agents would be nice — this allowlist drifts the\n * moment a new agent ships its env marker, and consolidating with the\n * other ecosystems that need the same lookup (rate-limited LLM\n * gateways, agent-aware metrics, etc.) would let one library carry the\n * matrix instead of every consumer re-doing it.\n */\nexport interface AgentMarker {\n /** The env-var name to read. Exact-match; no prefix or fuzzy logic. */\n readonly envVar: string;\n /** The agent label written to the `agent` field of the telemetry event. */\n readonly agent: string;\n}\n\nexport const AGENT_MARKERS: readonly AgentMarker[] = [\n { envVar: 'CLAUDECODE', agent: 'Claude Code' },\n { envVar: 'CURSOR_AGENT', agent: 'Cursor' },\n { envVar: 'CODEX_SANDBOX', agent: 'Codex CLI' },\n { envVar: 'GEMINI_CLI', agent: 'Gemini CLI' },\n { envVar: 'WINDSURF', agent: 'Windsurf' },\n { envVar: 'AIDER', agent: 'Aider' },\n { envVar: 'CODY', agent: 'Cody' },\n { envVar: 'CONTINUE', agent: 'Continue' },\n];\n\nfunction isTruthyMarker(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const normalised = raw.trim().toLowerCase();\n if (normalised === '') return false;\n if (normalised === '0') return false;\n if (normalised === 'false') return false;\n return true;\n}\n\n/**\n * Resolve the agent label from an env snapshot, or `null` if no marker\n * is set. Returns the **first** matching marker in `AGENT_MARKERS`\n * order, so when multiple markers are set the agent label is\n * deterministic and the allowlist's first entry wins.\n *\n * Pure: takes an env record, returns a string or null. No I/O.\n */\nexport function detectAgent(env: Readonly<Record<string, string | undefined>>): string | null {\n for (const marker of AGENT_MARKERS) {\n if (isTruthyMarker(env[marker.envVar])) {\n return marker.agent;\n }\n }\n return null;\n}\n","import { readFileSync } from 'node:fs';\nimport type { PrismaNextConfig } from '@prisma-next/config/config-types';\nimport { join } from 'pathe';\nimport { detectAgent } from './detect-agent';\nimport type { ParentToSenderPayload, TelemetryEvent } from './payload';\n\n/**\n * Subset of the user's `prisma-next.config.*` the telemetry event\n * surfaces. Loaded inside the detached child via {@link loadProjectConfig}\n * — see the design rationale on {@link ParentToSenderPayload} for why\n * this side runs c12 instead of the parent CLI.\n */\nexport interface ProjectConfigFields {\n readonly databaseTarget: string | null;\n readonly extensions: readonly string[];\n}\n\nconst EMPTY_PROJECT_CONFIG: ProjectConfigFields = {\n databaseTarget: null,\n extensions: [],\n};\n\n/**\n * Best-effort load of `prisma-next.config.*` from `projectRoot`,\n * validated against the canonical `@prisma-next/config` schema.\n * Returns `{ databaseTarget: null, extensions: [] }` on any failure\n * mode — missing config file (e.g. before `prisma-next init`), c12\n * throws while evaluating user TS, validator rejects a malformed\n * shape, etc. Telemetry is non-blocking and best-effort; an empty\n * result is the only downside of an unloadable or invalid config.\n *\n * Both `c12` and `@prisma-next/config/config-validation` are imported\n * lazily so the detached sender's cold-start cost is paid only when\n * telemetry actually fires, not on every fork even when gates\n * short-circuit before reaching this code path.\n */\nexport async function loadProjectConfig(projectRoot: string): Promise<ProjectConfigFields> {\n try {\n const { loadConfig } = await import('c12');\n const result = await loadConfig<Record<string, unknown>>({\n name: 'prisma-next',\n cwd: projectRoot,\n dotenv: false,\n rcFile: false,\n globalRc: false,\n });\n const config = result.config ?? null;\n // c12 returns an empty object when no config file exists in the\n // search path — distinct from \"file existed but parsed to an empty\n // object\". Either way, the canonical validator below would reject\n // it on the first required field (`family`), so short-circuit\n // without paying the import cost.\n if (config === null || Object.keys(config).length === 0) {\n return EMPTY_PROJECT_CONFIG;\n }\n const validation = await import('@prisma-next/config/config-validation');\n // TS 4.7+ only flows `asserts cfg is X` narrowing when the\n // assertion function is called via a directly-declared name with\n // an explicit signature. The dynamic-import binding doesn't\n // satisfy that, so wrap the call in a local declaration that\n // re-asserts the signature.\n const validate: (cfg: unknown) => asserts cfg is PrismaNextConfig = validation.validateConfig;\n validate(config);\n return {\n databaseTarget: config.target.targetId,\n extensions: (config.extensionPacks ?? []).map((pack) => pack.id),\n };\n } catch {\n return EMPTY_PROJECT_CONFIG;\n }\n}\n\n/**\n * Versions surface the enrichment cares about. Modelled as a structural\n * record with a required `node` field so tests can pass a literal object\n * without faking every field of `NodeJS.ProcessVersions` (which adds\n * properties between Node versions and includes a long tail the\n * enrichment never touches). Both `bun` and `deno` are read on the\n * runtime-resolution path; everything else is ignored.\n */\nexport interface VersionsSnapshot {\n readonly node: string;\n readonly bun?: string;\n readonly deno?: string;\n}\n\n/**\n * Snapshot of process-level inputs the enrichment reads. Tests pass an\n * explicit snapshot so the enrichment is deterministic per case; the\n * sender entry point passes a fresh snapshot from `process`.\n */\nexport interface EnrichEnvironment {\n readonly platform: NodeJS.Platform;\n readonly arch: string;\n readonly versions: VersionsSnapshot;\n /**\n * Included because package-manager and agent detection intentionally read\n * environment variables from the same process snapshot as platform/versions.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n /**\n * Best-effort reader for the project's `package.json`, used only to derive\n * the optional `tsVersion` telemetry field. Returning `null` means unknown.\n */\n readonly readProjectPackageJson: () => string | null;\n}\n\n/**\n * Identify the runtime the sender is running in. Same-runtime as the\n * parent is a correctness requirement: the parent forked us via\n * `child_process.fork`, which inherits the parent's runtime. Detection\n * keys on the runtime-specific version field rather than env vars so a\n * spoofed env can't lie about the actual interpreter.\n */\nfunction resolveRuntime(versions: VersionsSnapshot): {\n readonly name: 'node' | 'bun' | 'deno';\n readonly version: string;\n} {\n if (versions.bun !== undefined) {\n return { name: 'bun', version: versions.bun };\n }\n if (versions.deno !== undefined) {\n return { name: 'deno', version: versions.deno };\n }\n return { name: 'node', version: versions.node };\n}\n\n/**\n * Parse `npm_config_user_agent` into a `<pm>/<version>` token. The\n * value, when present, looks like\n * `\"pnpm/10.27.0 npm/? node/v24.13.0 darwin arm64\"` — we take the first\n * whitespace-separated token. Any failure → `null`.\n */\nexport function parsePackageManager(userAgent: string | undefined): string | null {\n if (userAgent === undefined) return null;\n const first = userAgent.split(/\\s+/)[0];\n if (first === undefined || first.length === 0) return null;\n if (!first.includes('/')) return null;\n return first;\n}\n\n/**\n * Read the user's project `package.json` and resolve a TypeScript\n * version from `devDependencies.typescript` (preferred) or\n * `dependencies.typescript`. Strips a leading `^` or `~` semver\n * prefix. Returns `null` on any failure mode — file missing,\n * unreadable, malformed JSON, key absent, not a string.\n */\nexport function readTsVersionFromPackageJson(raw: string | null): string | null {\n if (raw === null) return null;\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n const candidate =\n pickStringDep(parsed['devDependencies']) ?? pickStringDep(parsed['dependencies']);\n if (candidate === null) return null;\n return candidate.replace(/^[\\^~]/, '');\n}\n\nfunction pickStringDep(deps: unknown): string | null {\n if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) return null;\n const value = (deps as Record<string, unknown>)['typescript'];\n return typeof value === 'string' ? value : null;\n}\n\n/**\n * Build the full backend event from the parent's payload, the\n * c12-loaded project-config slice, and the child's per-process\n * snapshot. Pure given a `projectConfig` + `EnrichEnvironment`.\n */\nexport function buildTelemetryEvent(\n payload: ParentToSenderPayload,\n projectConfig: ProjectConfigFields,\n env: EnrichEnvironment,\n): TelemetryEvent {\n const runtime = resolveRuntime(env.versions);\n return {\n installationId: payload.installationId,\n version: payload.version,\n command: payload.command,\n flags: payload.flags,\n runtimeName: runtime.name,\n runtimeVersion: runtime.version,\n os: env.platform,\n arch: env.arch,\n packageManager: parsePackageManager(env.env['npm_config_user_agent']),\n databaseTarget: projectConfig.databaseTarget,\n tsVersion: readTsVersionFromPackageJson(env.readProjectPackageJson()),\n agent: detectAgent(env.env),\n extensions: projectConfig.extensions,\n };\n}\n\n/**\n * Convenience for the sender entry: build the event from the live\n * `process` plus a c12 load of `prisma-next.config.*` from\n * `payload.projectRoot` plus a real project-package.json reader,\n * swallowing any I/O errors in the file read.\n *\n * The parent's `payload.databaseTarget` (when present) wins over the\n * c12-derived value. The parent sets this for the first-`init` run,\n * where the config file does not exist on disk yet but the user has\n * just declared a target via the consent prompt; every other\n * invocation leaves it unset and the c12 load supplies the value.\n */\nexport async function buildTelemetryEventFromProcess(\n payload: ParentToSenderPayload,\n): Promise<TelemetryEvent> {\n const loadedConfig = await loadProjectConfig(payload.projectRoot);\n const projectConfig: ProjectConfigFields = {\n databaseTarget: payload.databaseTarget ?? loadedConfig.databaseTarget,\n extensions: loadedConfig.extensions,\n };\n return buildTelemetryEvent(payload, projectConfig, {\n platform: process.platform,\n arch: process.arch,\n versions: process.versions,\n env: process.env,\n readProjectPackageJson: () => {\n try {\n return readFileSync(join(payload.projectRoot, 'package.json'), 'utf-8');\n } catch {\n return null;\n }\n },\n });\n}\n"],"mappings":"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"}
@@ -1,68 +0,0 @@
1
- /**
2
- * Best-effort identification of AI coding-agent sessions from an
3
- * env-var allowlist. Detector property: false positives are negligible
4
- * (a marker present ⇒ confidently an agent); false negatives are
5
- * expected and documented in the user-facing telemetry docs. New
6
- * entries should land here, not in per-CLI hand-rolls.
7
- *
8
- * Each entry is a `(envVar, agent)` pair with uniform comparison shape:
9
- * the marker counts as "present" when `process.env[envVar]` is set to a
10
- * truthy string. Truthy = anything other than the empty string, `'0'`,
11
- * or `'false'` (case-insensitive); see `gating.isTruthyOptOut` for the
12
- * same convention applied to opt-out env vars.
13
- *
14
- * The detector runs in the **child** sender process, never the parent;
15
- * the parent does not probe env at command start.
16
- *
17
- * Codex CLI note: `CODEX_SANDBOX` is the only clear marker available here.
18
- * Non-sandboxed Codex sessions may be false negatives.
19
- *
20
- * TODO: a ci-info-for-agents would be nice — this allowlist drifts the
21
- * moment a new agent ships its env marker, and consolidating with the
22
- * other ecosystems that need the same lookup (rate-limited LLM
23
- * gateways, agent-aware metrics, etc.) would let one library carry the
24
- * matrix instead of every consumer re-doing it.
25
- */
26
- export interface AgentMarker {
27
- /** The env-var name to read. Exact-match; no prefix or fuzzy logic. */
28
- readonly envVar: string;
29
- /** The agent label written to the `agent` field of the telemetry event. */
30
- readonly agent: string;
31
- }
32
-
33
- export const AGENT_MARKERS: readonly AgentMarker[] = [
34
- { envVar: 'CLAUDECODE', agent: 'Claude Code' },
35
- { envVar: 'CURSOR_AGENT', agent: 'Cursor' },
36
- { envVar: 'CODEX_SANDBOX', agent: 'Codex CLI' },
37
- { envVar: 'GEMINI_CLI', agent: 'Gemini CLI' },
38
- { envVar: 'WINDSURF', agent: 'Windsurf' },
39
- { envVar: 'AIDER', agent: 'Aider' },
40
- { envVar: 'CODY', agent: 'Cody' },
41
- { envVar: 'CONTINUE', agent: 'Continue' },
42
- ];
43
-
44
- function isTruthyMarker(raw: string | undefined): boolean {
45
- if (raw === undefined) return false;
46
- const normalised = raw.trim().toLowerCase();
47
- if (normalised === '') return false;
48
- if (normalised === '0') return false;
49
- if (normalised === 'false') return false;
50
- return true;
51
- }
52
-
53
- /**
54
- * Resolve the agent label from an env snapshot, or `null` if no marker
55
- * is set. Returns the **first** matching marker in `AGENT_MARKERS`
56
- * order, so when multiple markers are set the agent label is
57
- * deterministic and the allowlist's first entry wins.
58
- *
59
- * Pure: takes an env record, returns a string or null. No I/O.
60
- */
61
- export function detectAgent(env: Readonly<Record<string, string | undefined>>): string | null {
62
- for (const marker of AGENT_MARKERS) {
63
- if (isTruthyMarker(env[marker.envVar])) {
64
- return marker.agent;
65
- }
66
- }
67
- return null;
68
- }