@unbrained/pm-cli 2026.5.2 → 2026.5.3-6

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 (115) hide show
  1. package/AGENTS.md +8 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +9 -1
  4. package/dist/cli/bootstrap-args.d.ts +18 -0
  5. package/dist/cli/bootstrap-args.js +242 -0
  6. package/dist/cli/bootstrap-args.js.map +1 -0
  7. package/dist/cli/commander-usage.d.ts +17 -0
  8. package/dist/cli/commander-usage.js +178 -0
  9. package/dist/cli/commander-usage.js.map +1 -0
  10. package/dist/cli/commands/activity.js +1 -9
  11. package/dist/cli/commands/activity.js.map +1 -1
  12. package/dist/cli/commands/calendar.js +3 -29
  13. package/dist/cli/commands/calendar.js.map +1 -1
  14. package/dist/cli/commands/comments.js +1 -9
  15. package/dist/cli/commands/comments.js.map +1 -1
  16. package/dist/cli/commands/config.d.ts +21 -3
  17. package/dist/cli/commands/config.js +118 -2
  18. package/dist/cli/commands/config.js.map +1 -1
  19. package/dist/cli/commands/context.d.ts +90 -1
  20. package/dist/cli/commands/context.js +485 -23
  21. package/dist/cli/commands/context.js.map +1 -1
  22. package/dist/cli/commands/dedupe-audit.js +2 -11
  23. package/dist/cli/commands/dedupe-audit.js.map +1 -1
  24. package/dist/cli/commands/history.js +1 -9
  25. package/dist/cli/commands/history.js.map +1 -1
  26. package/dist/cli/commands/learnings.js +1 -9
  27. package/dist/cli/commands/learnings.js.map +1 -1
  28. package/dist/cli/commands/list.js +3 -29
  29. package/dist/cli/commands/list.js.map +1 -1
  30. package/dist/cli/commands/normalize.js +9 -6
  31. package/dist/cli/commands/normalize.js.map +1 -1
  32. package/dist/cli/commands/notes.js +1 -9
  33. package/dist/cli/commands/notes.js.map +1 -1
  34. package/dist/cli/commands/reindex.js +2 -7
  35. package/dist/cli/commands/reindex.js.map +1 -1
  36. package/dist/cli/commands/search.js +4 -35
  37. package/dist/cli/commands/search.js.map +1 -1
  38. package/dist/cli/commands/test-runs.js +1 -11
  39. package/dist/cli/commands/test-runs.js.map +1 -1
  40. package/dist/cli/error-guidance.d.ts +13 -0
  41. package/dist/cli/error-guidance.js +43 -4
  42. package/dist/cli/error-guidance.js.map +1 -1
  43. package/dist/cli/extension-command-help.d.ts +48 -0
  44. package/dist/cli/extension-command-help.js +389 -0
  45. package/dist/cli/extension-command-help.js.map +1 -0
  46. package/dist/cli/help-content.js +9 -3
  47. package/dist/cli/help-content.js.map +1 -1
  48. package/dist/cli/help-json-payload.d.ts +25 -0
  49. package/dist/cli/help-json-payload.js +265 -0
  50. package/dist/cli/help-json-payload.js.map +1 -0
  51. package/dist/cli/main.js +996 -4468
  52. package/dist/cli/main.js.map +1 -1
  53. package/dist/cli/migration-gates.d.ts +22 -0
  54. package/dist/cli/migration-gates.js +146 -0
  55. package/dist/cli/migration-gates.js.map +1 -0
  56. package/dist/cli/register-list-query.d.ts +2 -0
  57. package/dist/cli/register-list-query.js +317 -0
  58. package/dist/cli/register-list-query.js.map +1 -0
  59. package/dist/cli/register-mutation.d.ts +2 -0
  60. package/dist/cli/register-mutation.js +795 -0
  61. package/dist/cli/register-mutation.js.map +1 -0
  62. package/dist/cli/register-operations.d.ts +2 -0
  63. package/dist/cli/register-operations.js +610 -0
  64. package/dist/cli/register-operations.js.map +1 -0
  65. package/dist/cli/register-setup.d.ts +2 -0
  66. package/dist/cli/register-setup.js +334 -0
  67. package/dist/cli/register-setup.js.map +1 -0
  68. package/dist/cli/registration-helpers.d.ts +53 -0
  69. package/dist/cli/registration-helpers.js +669 -0
  70. package/dist/cli/registration-helpers.js.map +1 -0
  71. package/dist/cli/shared-parsers.d.ts +6 -0
  72. package/dist/cli/shared-parsers.js +40 -0
  73. package/dist/cli/shared-parsers.js.map +1 -0
  74. package/dist/core/search/http-client.d.ts +29 -0
  75. package/dist/core/search/http-client.js +64 -0
  76. package/dist/core/search/http-client.js.map +1 -0
  77. package/dist/core/search/providers.d.ts +3 -13
  78. package/dist/core/search/providers.js +19 -69
  79. package/dist/core/search/providers.js.map +1 -1
  80. package/dist/core/search/semantic-defaults.js +2 -7
  81. package/dist/core/search/semantic-defaults.js.map +1 -1
  82. package/dist/core/search/vector-stores.d.ts +3 -13
  83. package/dist/core/search/vector-stores.js +17 -66
  84. package/dist/core/search/vector-stores.js.map +1 -1
  85. package/dist/core/sentry/helpers.d.ts +23 -2
  86. package/dist/core/sentry/helpers.js +101 -3
  87. package/dist/core/sentry/helpers.js.map +1 -1
  88. package/dist/core/sentry/instrument.d.ts +21 -0
  89. package/dist/core/sentry/instrument.js +34 -3
  90. package/dist/core/sentry/instrument.js.map +1 -1
  91. package/dist/core/shared/constants.d.ts +3 -0
  92. package/dist/core/shared/constants.js +58 -1
  93. package/dist/core/shared/constants.js.map +1 -1
  94. package/dist/core/store/front-matter-cache.d.ts +6 -0
  95. package/dist/core/store/front-matter-cache.js +150 -0
  96. package/dist/core/store/front-matter-cache.js.map +1 -0
  97. package/dist/core/store/item-store.js +2 -1
  98. package/dist/core/store/item-store.js.map +1 -1
  99. package/dist/core/store/settings.js +36 -0
  100. package/dist/core/store/settings.js.map +1 -1
  101. package/dist/core/telemetry/observability.d.ts +24 -0
  102. package/dist/core/telemetry/observability.js +185 -0
  103. package/dist/core/telemetry/observability.js.map +1 -0
  104. package/dist/core/telemetry/runtime.d.ts +27 -3
  105. package/dist/core/telemetry/runtime.js +298 -13
  106. package/dist/core/telemetry/runtime.js.map +1 -1
  107. package/dist/sdk/cli-contracts.js +28 -0
  108. package/dist/sdk/cli-contracts.js.map +1 -1
  109. package/dist/types.d.ts +21 -0
  110. package/dist/types.js +11 -0
  111. package/dist/types.js.map +1 -1
  112. package/docs/ARCHITECTURE.md +7 -1
  113. package/docs/COMMANDS.md +11 -1
  114. package/docs/RELEASING.md +56 -29
  115. package/package.json +8 -3
@@ -2,9 +2,11 @@ import crypto from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { appendLineAtomic, readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
5
+ import { resolveTelemetryErrorCategory } from "../shared/constants.js";
5
6
  import { nowIso } from "../shared/time.js";
6
7
  import { resolveGlobalPmRoot } from "../store/paths.js";
7
8
  import { readSettings, writeSettings } from "../store/settings.js";
9
+ import { deriveTelemetryCommandResolution, deriveTelemetryCommandTaxonomy, inferTelemetryErrorCode, } from "./observability.js";
8
10
  const TELEMETRY_QUEUE_RELATIVE_PATH = path.join("runtime", "telemetry", "events.jsonl");
9
11
  const TELEMETRY_STATE_RELATIVE_PATH = path.join("runtime", "telemetry", "state.json");
10
12
  const TELEMETRY_SCHEMA_VERSION = 1;
@@ -52,6 +54,9 @@ const SENSITIVE_INLINE_KEY_PATTERN = "(?:token|secret|password|passwd|api[_-]?ke
52
54
  const INLINE_SENSITIVE_ASSIGNMENT_PATTERN = new RegExp(`\\b(${SENSITIVE_INLINE_KEY_PATTERN})\\s*([:=])\\s*([^\\s,;]+)`, "giu");
53
55
  const INLINE_SENSITIVE_FLAG_PATTERN = new RegExp(`(--${SENSITIVE_INLINE_KEY_PATTERN})(=|\\s+)([^\\s,;]+)`, "giu");
54
56
  const ABSOLUTE_PATH_TOKEN_PATTERN = /(^|[\s"'`(=])\/(?:[^\s"'`),;]+)/g;
57
+ const EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu;
58
+ const BEARER_TOKEN_PATTERN = /bearer\s+[a-z0-9._=-]+/giu;
59
+ const PRIVATE_IP_PATTERN = /\b(?:10\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)|172\.(?:1[6-9]|2\d|3[01])\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)|192\.168\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d))\b/g;
55
60
  function queuePath(globalPmRoot) {
56
61
  return path.join(globalPmRoot, TELEMETRY_QUEUE_RELATIVE_PATH);
57
62
  }
@@ -102,10 +107,14 @@ function redactInlineSensitiveAssignments(input) {
102
107
  function redactAbsolutePathTokens(input) {
103
108
  return input.replaceAll(ABSOLUTE_PATH_TOKEN_PATTERN, (_match, prefix) => `${prefix}[redacted_path]`);
104
109
  }
110
+ function sanitizeCommonSensitiveTokens(input) {
111
+ const withoutEmails = input.replaceAll(EMAIL_PATTERN, "[redacted_email]");
112
+ const withoutBearer = withoutEmails.replaceAll(BEARER_TOKEN_PATTERN, "bearer [redacted_token]");
113
+ return withoutBearer.replaceAll(PRIVATE_IP_PATTERN, "[redacted_ip]");
114
+ }
105
115
  function sanitizeStringRedacted(input) {
106
- const withoutEmails = input.replaceAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "[redacted_email]");
107
- const withoutBearer = withoutEmails.replaceAll(/bearer\s+[a-z0-9._=-]+/giu, "bearer [redacted_token]");
108
- const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutBearer);
116
+ const withoutCommonSensitiveTokens = sanitizeCommonSensitiveTokens(input);
117
+ const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutCommonSensitiveTokens);
109
118
  const withoutAbsolutePaths = redactAbsolutePathTokens(withoutInlineSecrets);
110
119
  const trimmed = withoutAbsolutePaths.trim();
111
120
  if (trimmed.startsWith("/") && trimmed.length > 1) {
@@ -117,12 +126,17 @@ function sanitizeStringRedacted(input) {
117
126
  return withoutAbsolutePaths;
118
127
  }
119
128
  function sanitizeStringMax(input) {
120
- const withoutBearer = input.replaceAll(/bearer\s+[a-z0-9._=-]+/giu, "bearer [redacted_token]");
121
- const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutBearer);
122
- if (withoutInlineSecrets.length > 2048) {
123
- return `${withoutInlineSecrets.slice(0, 2045)}...`;
129
+ const withoutCommonSensitiveTokens = sanitizeCommonSensitiveTokens(input);
130
+ const withoutInlineSecrets = redactInlineSensitiveAssignments(withoutCommonSensitiveTokens);
131
+ const withoutAbsolutePaths = redactAbsolutePathTokens(withoutInlineSecrets);
132
+ const trimmed = withoutAbsolutePaths.trim();
133
+ if (trimmed.startsWith("/") && trimmed.length > 1) {
134
+ return "[redacted_path]";
135
+ }
136
+ if (withoutAbsolutePaths.length > 2048) {
137
+ return `${withoutAbsolutePaths.slice(0, 2045)}...`;
124
138
  }
125
- return withoutInlineSecrets;
139
+ return withoutAbsolutePaths;
126
140
  }
127
141
  function sanitizeString(input, captureLevel = "redacted") {
128
142
  if (captureLevel === "max") {
@@ -207,6 +221,28 @@ function normalizePmVersion(value) {
207
221
  const trimmed = value?.trim() ?? "";
208
222
  return trimmed.length > 0 ? trimmed : "0.0.0";
209
223
  }
224
+ function normalizeTelemetryErrorCode(value) {
225
+ const normalized = value?.trim();
226
+ return normalized && normalized.length > 0 ? normalized : undefined;
227
+ }
228
+ function normalizeTelemetryExitCode(exitCode, ok) {
229
+ if (Number.isFinite(exitCode)) {
230
+ return Math.max(0, Math.trunc(exitCode ?? 0));
231
+ }
232
+ return ok ? 0 : 1;
233
+ }
234
+ function normalizeTelemetryErrorCategory(params) {
235
+ if (params.ok) {
236
+ return undefined;
237
+ }
238
+ if (typeof params.errorCategory === "string" && params.errorCategory.trim().length > 0) {
239
+ return params.errorCategory;
240
+ }
241
+ if (typeof params.errorCode === "string" && params.errorCode.trim().length > 0) {
242
+ return resolveTelemetryErrorCategory(params.errorCode);
243
+ }
244
+ return "unknown";
245
+ }
210
246
  function resolveTelemetrySourceContext(globalOptions) {
211
247
  const override = (process.env[PM_TELEMETRY_SOURCE_CONTEXT_ENV] ?? "").trim().toLowerCase();
212
248
  if (PM_TELEMETRY_SOURCE_CONTEXT_SET.has(override)) {
@@ -239,6 +275,44 @@ function resolveTelemetrySourceContext(globalOptions) {
239
275
  function hashWithInstallationId(installationId, value) {
240
276
  return crypto.createHash("sha256").update(`${installationId}:${value}`).digest("hex");
241
277
  }
278
+ function normalizeForHash(value, depth = 0) {
279
+ if (value === null || value === undefined) {
280
+ return value;
281
+ }
282
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
283
+ return value;
284
+ }
285
+ if (depth >= TELEMETRY_SANITIZE_MAX_DEPTH) {
286
+ return "[depth_truncated]";
287
+ }
288
+ if (Array.isArray(value)) {
289
+ return value.slice(0, TELEMETRY_SANITIZE_MAX_ARRAY_ITEMS).map((entry) => normalizeForHash(entry, depth + 1));
290
+ }
291
+ if (typeof value === "object") {
292
+ const record = value;
293
+ const normalized = {};
294
+ const sortedKeys = Object.keys(record).sort((left, right) => left.localeCompare(right));
295
+ for (const key of sortedKeys) {
296
+ normalized[key] = normalizeForHash(record[key], depth + 1);
297
+ }
298
+ return normalized;
299
+ }
300
+ return String(value);
301
+ }
302
+ function hashTelemetryValue(installationId, value) {
303
+ return hashWithInstallationId(installationId, JSON.stringify(normalizeForHash(value)));
304
+ }
305
+ function hashCommandArgs(installationId, args) {
306
+ return {
307
+ hashes: args.map((arg) => hashWithInstallationId(installationId, arg)),
308
+ digest: hashWithInstallationId(installationId, args.join("\u0000")),
309
+ };
310
+ }
311
+ function hashTelemetryErrorFingerprint(installationId, command, errorCode, errorMessage) {
312
+ const normalizedMessage = sanitizeString(errorMessage ?? "", "redacted");
313
+ const fingerprintSource = `${command}\u0000${errorCode ?? "unknown_error"}\u0000${normalizedMessage}`;
314
+ return hashWithInstallationId(installationId, fingerprintSource);
315
+ }
242
316
  function telemetryDisabledByEnvironment() {
243
317
  return PM_TELEMETRY_DISABLED_VALUES.has((process.env[PM_TELEMETRY_DISABLED_ENV] ?? "").trim().toLowerCase());
244
318
  }
@@ -305,6 +379,13 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
305
379
  }
306
380
  const serviceNameCandidate = sanitizeString((process.env[OTEL_SERVICE_NAME_ENV] ?? "").trim());
307
381
  const serviceName = serviceNameCandidate.length > 0 ? serviceNameCandidate : "pm-cli";
382
+ const normalizedExitCode = normalizeTelemetryExitCode(outcome.exit_code, outcome.ok);
383
+ const normalizedErrorCode = normalizeTelemetryErrorCode(outcome.error_code);
384
+ const normalizedErrorCategory = normalizeTelemetryErrorCategory({
385
+ ok: outcome.ok,
386
+ errorCode: normalizedErrorCode,
387
+ errorCategory: outcome.error_category,
388
+ });
308
389
  const attributes = [
309
390
  otlpStringAttribute("pm.command", sanitizeString(activeCommand.command)),
310
391
  otlpStringAttribute("pm.version", activeCommand.pm_version),
@@ -315,8 +396,15 @@ async function exportLocalOtelSpan(activeCommand, outcome, finishedAtIso, durati
315
396
  otlpStringAttribute("pm.pm_root_hash", activeCommand.pm_root_hash),
316
397
  otlpStringAttribute("pm.cwd_hash", activeCommand.cwd_hash),
317
398
  otlpBoolAttribute("pm.ok", outcome.ok),
399
+ otlpIntAttribute("pm.exit_code", normalizedExitCode),
318
400
  otlpIntAttribute("pm.duration_ms", durationMs),
319
401
  ];
402
+ if (typeof normalizedErrorCode === "string") {
403
+ attributes.push(otlpStringAttribute("pm.error_code", normalizedErrorCode));
404
+ }
405
+ if (typeof normalizedErrorCategory === "string") {
406
+ attributes.push(otlpStringAttribute("pm.error_category", normalizedErrorCategory));
407
+ }
320
408
  if (typeof outcome.error === "string" && outcome.error.trim().length > 0) {
321
409
  attributes.push(otlpStringAttribute("pm.error", sanitizeString(outcome.error)));
322
410
  }
@@ -407,21 +495,37 @@ function summarizeResult(result, captureLevel = "redacted") {
407
495
  }
408
496
  function buildCommandStartPayload(params) {
409
497
  const { captureLevel, context, pmVersion, sourceContext, pmRootHash, cwdHash, installationId } = params;
498
+ const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
499
+ const hashedArgs = hashCommandArgs(installationId, context.args);
500
+ const commandInvocationDigest = hashWithInstallationId(installationId, `${context.command}\u0000${context.args.join("\u0000")}`);
501
+ const commandOptionsDigest = hashTelemetryValue(installationId, context.options);
502
+ const globalOptionsDigest = hashTelemetryValue(installationId, context.global);
410
503
  if (captureLevel === "minimal") {
411
504
  return {
412
505
  capture_level: captureLevel,
413
506
  pm_version: pmVersion,
414
507
  source_context: sourceContext.source_context,
415
508
  source_context_source: sourceContext.source_context_source,
509
+ command_taxonomy: commandTaxonomy,
510
+ command_args_digest: hashedArgs.digest,
511
+ command_invocation_digest: commandInvocationDigest,
512
+ command_options_digest: commandOptionsDigest,
513
+ global_options_digest: globalOptionsDigest,
416
514
  };
417
515
  }
418
516
  return {
419
517
  pm_version: pmVersion,
420
518
  source_context: sourceContext.source_context,
421
519
  source_context_source: sourceContext.source_context_source,
520
+ command_taxonomy: commandTaxonomy,
422
521
  command_args: sanitizeCommandArgs(context.args, captureLevel),
522
+ command_args_hashes: hashedArgs.hashes,
523
+ command_args_digest: hashedArgs.digest,
524
+ command_invocation_digest: commandInvocationDigest,
423
525
  command_options: sanitizeValue(context.options, undefined, captureLevel),
526
+ command_options_digest: commandOptionsDigest,
424
527
  global_options: sanitizeValue(context.global, undefined, captureLevel),
528
+ global_options_digest: globalOptionsDigest,
425
529
  pm_root_hash: pmRootHash,
426
530
  cwd_hash: cwdHash,
427
531
  capture_level: captureLevel,
@@ -436,15 +540,25 @@ function buildCommandStartPayload(params) {
436
540
  };
437
541
  }
438
542
  function buildCommandFinishPayload(params) {
439
- const { captureLevel, pmVersion, sourceContext, outcome, durationMs, startedAt } = params;
543
+ const { captureLevel, pmVersion, sourceContext, outcome, durationMs, startedAt, command, installationId, commandTaxonomy, exitCode, errorCode, errorCategory, commandResolution, resolutionStage, } = params;
544
+ const errorFingerprint = outcome.ok === false
545
+ ? hashTelemetryErrorFingerprint(installationId, command, errorCode, outcome.error)
546
+ : undefined;
440
547
  if (captureLevel === "minimal") {
441
548
  return {
442
549
  capture_level: captureLevel,
443
550
  pm_version: pmVersion,
444
551
  source_context: sourceContext.source_context,
445
552
  source_context_source: sourceContext.source_context_source,
553
+ command_taxonomy: commandTaxonomy,
554
+ command_resolution: commandResolution,
555
+ resolution_stage: resolutionStage,
446
556
  ok: outcome.ok,
557
+ exit_code: exitCode,
558
+ error_code: errorCode,
559
+ error_category: errorCategory,
447
560
  error: outcome.error ? sanitizeString(outcome.error, "redacted") : undefined,
561
+ error_fingerprint: errorFingerprint,
448
562
  duration_ms: durationMs,
449
563
  };
450
564
  }
@@ -453,13 +567,77 @@ function buildCommandFinishPayload(params) {
453
567
  pm_version: pmVersion,
454
568
  source_context: sourceContext.source_context,
455
569
  source_context_source: sourceContext.source_context_source,
570
+ command_taxonomy: commandTaxonomy,
571
+ command_resolution: commandResolution,
572
+ resolution_stage: resolutionStage,
456
573
  ok: outcome.ok,
574
+ exit_code: exitCode,
575
+ error_code: errorCode,
576
+ error_category: errorCategory,
457
577
  error: outcome.error ? sanitizeString(outcome.error, captureLevel) : undefined,
578
+ error_fingerprint: errorFingerprint,
458
579
  duration_ms: durationMs,
459
580
  started_at: startedAt,
460
581
  result_summary: summarizeResult(outcome.result, captureLevel),
461
582
  };
462
583
  }
584
+ function buildCommandErrorPayload(params) {
585
+ const { captureLevel, pmVersion, sourceContext, command, commandTaxonomy, commandResolution, resolutionStage, args, options, pmRootHash, cwdHash, installationId, errorCode, errorMessage, errorCategory, exitCode, } = params;
586
+ const attemptedArgHashes = hashCommandArgs(installationId, args);
587
+ const attemptedCommandDigest = hashWithInstallationId(installationId, command);
588
+ const attemptedOptionsDigest = hashTelemetryValue(installationId, options);
589
+ const errorFingerprint = hashTelemetryErrorFingerprint(installationId, command, errorCode, errorMessage);
590
+ if (captureLevel === "minimal") {
591
+ return {
592
+ capture_level: captureLevel,
593
+ pm_version: pmVersion,
594
+ source_context: sourceContext.source_context,
595
+ source_context_source: sourceContext.source_context_source,
596
+ command_taxonomy: commandTaxonomy,
597
+ command_resolution: commandResolution,
598
+ resolution_stage: resolutionStage,
599
+ attempted_command_digest: attemptedCommandDigest,
600
+ attempted_args_digest: attemptedArgHashes.digest,
601
+ attempted_options_digest: attemptedOptionsDigest,
602
+ error_code: errorCode,
603
+ error_category: errorCategory,
604
+ exit_code: exitCode,
605
+ error: sanitizeString(errorMessage, "redacted"),
606
+ error_fingerprint: errorFingerprint,
607
+ };
608
+ }
609
+ return {
610
+ capture_level: captureLevel,
611
+ pm_version: pmVersion,
612
+ source_context: sourceContext.source_context,
613
+ source_context_source: sourceContext.source_context_source,
614
+ command_taxonomy: commandTaxonomy,
615
+ command_resolution: commandResolution,
616
+ resolution_stage: resolutionStage,
617
+ attempted_command: sanitizeString(command, captureLevel),
618
+ attempted_command_digest: attemptedCommandDigest,
619
+ attempted_args: sanitizeCommandArgs(args, captureLevel),
620
+ attempted_args_hashes: attemptedArgHashes.hashes,
621
+ attempted_args_digest: attemptedArgHashes.digest,
622
+ attempted_options: sanitizeValue(options, undefined, captureLevel),
623
+ attempted_options_digest: attemptedOptionsDigest,
624
+ error_code: errorCode,
625
+ error_category: errorCategory,
626
+ exit_code: exitCode,
627
+ error: sanitizeString(errorMessage, captureLevel),
628
+ error_fingerprint: errorFingerprint,
629
+ pm_root_hash: pmRootHash,
630
+ cwd_hash: cwdHash,
631
+ runtime: {
632
+ node: process.version,
633
+ platform: process.platform,
634
+ arch: process.arch,
635
+ hostname_hash: hashWithInstallationId(installationId, os.hostname()),
636
+ stdin_tty: process.stdin.isTTY === true,
637
+ stdout_tty: process.stdout.isTTY === true,
638
+ },
639
+ };
640
+ }
463
641
  async function ensureInstallationId(globalPmRoot) {
464
642
  const settings = await readSettings(globalPmRoot);
465
643
  let changed = false;
@@ -605,12 +783,17 @@ async function flushQueue(globalPmRoot, endpoint, retentionDays) {
605
783
  }
606
784
  const dueIds = new Set(dueEntries.map((entry) => entry.event.event_id));
607
785
  const attemptTime = nowIso();
786
+ const requestHeaders = {
787
+ "content-type": "application/json",
788
+ };
789
+ const ingestKey = process.env.PM_TELEMETRY_INGEST_KEY?.trim();
790
+ if (ingestKey) {
791
+ requestHeaders["x-pm-telemetry-key"] = ingestKey;
792
+ }
608
793
  try {
609
794
  const response = await fetch(normalizedEndpoint, {
610
795
  method: "POST",
611
- headers: {
612
- "content-type": "application/json",
613
- },
796
+ headers: requestHeaders,
614
797
  body: JSON.stringify({
615
798
  schema_version: TELEMETRY_SCHEMA_VERSION,
616
799
  events: dueEntries.map((entry) => entry.event),
@@ -698,10 +881,12 @@ export async function startTelemetryCommand(context) {
698
881
  };
699
882
  await enqueueTelemetryEvent(globalPmRoot, event);
700
883
  _lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
884
+ const commandTaxonomy = deriveTelemetryCommandTaxonomy(context.command);
701
885
  return {
702
886
  started_at: occurredAt,
703
887
  started_at_ms: Date.now(),
704
888
  command: context.command,
889
+ command_taxonomy: commandTaxonomy,
705
890
  pm_version: pmVersion,
706
891
  source_context: sourceContext.source_context,
707
892
  source_context_source: sourceContext.source_context_source,
@@ -729,6 +914,25 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
729
914
  try {
730
915
  const finishedAt = nowIso();
731
916
  const durationMs = Math.max(0, Date.now() - activeCommand.started_at_ms);
917
+ const normalizedErrorCode = normalizeTelemetryErrorCode(inferTelemetryErrorCode({
918
+ ok: outcome.ok,
919
+ errorCode: outcome.error_code,
920
+ errorMessage: outcome.error,
921
+ exitCode: outcome.exit_code,
922
+ }));
923
+ const normalizedErrorCategory = normalizeTelemetryErrorCategory({
924
+ ok: outcome.ok,
925
+ errorCode: normalizedErrorCode,
926
+ errorCategory: outcome.error_category,
927
+ });
928
+ const normalizedExitCode = normalizeTelemetryExitCode(outcome.exit_code, outcome.ok);
929
+ const commandResolution = outcome.command_resolution ??
930
+ deriveTelemetryCommandResolution({
931
+ ok: outcome.ok,
932
+ errorCode: normalizedErrorCode,
933
+ errorCategory: normalizedErrorCategory,
934
+ });
935
+ const resolutionStage = outcome.resolution_stage ?? "execute";
732
936
  const event = {
733
937
  schema_version: TELEMETRY_SCHEMA_VERSION,
734
938
  event_id: crypto.randomUUID(),
@@ -747,11 +951,92 @@ export async function finishTelemetryCommand(activeCommand, outcome) {
747
951
  outcome,
748
952
  durationMs,
749
953
  startedAt: activeCommand.started_at,
954
+ command: activeCommand.command,
955
+ installationId: activeCommand.installation_id,
956
+ commandTaxonomy: activeCommand.command_taxonomy,
957
+ exitCode: normalizedExitCode,
958
+ errorCode: normalizedErrorCode,
959
+ errorCategory: normalizedErrorCategory,
960
+ commandResolution,
961
+ resolutionStage,
750
962
  }),
751
963
  };
752
964
  await enqueueTelemetryEvent(activeCommand.global_pm_root, event);
753
965
  _lastFlushPromise = flushQueue(activeCommand.global_pm_root, activeCommand.endpoint, activeCommand.retention_days).catch(() => { });
754
- void exportLocalOtelSpan(activeCommand, outcome, finishedAt, durationMs).catch(() => { });
966
+ void exportLocalOtelSpan(activeCommand, {
967
+ ...outcome,
968
+ exit_code: normalizedExitCode,
969
+ error_code: normalizedErrorCode,
970
+ error_category: normalizedErrorCategory,
971
+ }, finishedAt, durationMs).catch(() => { });
972
+ }
973
+ catch {
974
+ // Telemetry must never block command execution.
975
+ }
976
+ }
977
+ export async function emitTelemetryErrorEvent(context) {
978
+ if (telemetryDisabledByEnvironment()) {
979
+ return;
980
+ }
981
+ try {
982
+ const globalPmRoot = resolveGlobalPmRoot(process.cwd());
983
+ const settings = await readSettings(globalPmRoot);
984
+ if (!settings.telemetry.enabled) {
985
+ return;
986
+ }
987
+ const captureLevel = normalizeCaptureLevel(settings.telemetry.capture_level);
988
+ const { installationId, endpoint, retentionDays } = await ensureInstallationId(globalPmRoot);
989
+ const pmVersion = normalizePmVersion(context.pm_version);
990
+ const sourceContext = resolveTelemetrySourceContext(context.global);
991
+ const pmRootHash = hashWithInstallationId(installationId, context.pm_root);
992
+ const cwdHash = hashWithInstallationId(installationId, process.cwd());
993
+ const occurredAt = nowIso();
994
+ const normalizedErrorCode = normalizeTelemetryErrorCode(inferTelemetryErrorCode({
995
+ ok: false,
996
+ errorCode: context.error_code,
997
+ errorMessage: context.error_message,
998
+ exitCode: context.exit_code,
999
+ })) ?? "unknown_error";
1000
+ const normalizedErrorCategory = context.error_category ?? resolveTelemetryErrorCategory(normalizedErrorCode);
1001
+ const normalizedExitCode = normalizeTelemetryExitCode(context.exit_code, false);
1002
+ const normalizedCommand = context.command.trim().length > 0 ? context.command : "<unknown>";
1003
+ const commandTaxonomy = deriveTelemetryCommandTaxonomy(normalizedCommand);
1004
+ const commandResolution = context.command_resolution ??
1005
+ deriveTelemetryCommandResolution({
1006
+ ok: false,
1007
+ errorCode: normalizedErrorCode,
1008
+ errorCategory: normalizedErrorCategory,
1009
+ });
1010
+ const resolutionStage = context.resolution_stage ?? "unknown";
1011
+ const event = {
1012
+ schema_version: TELEMETRY_SCHEMA_VERSION,
1013
+ event_id: crypto.randomUUID(),
1014
+ event_type: "command_error",
1015
+ occurred_at: occurredAt,
1016
+ installation_id: installationId,
1017
+ session_id: PROCESS_SESSION_ID,
1018
+ command: sanitizeString(normalizedCommand, "redacted"),
1019
+ payload: buildCommandErrorPayload({
1020
+ captureLevel,
1021
+ pmVersion,
1022
+ sourceContext,
1023
+ command: normalizedCommand,
1024
+ commandTaxonomy,
1025
+ commandResolution,
1026
+ resolutionStage,
1027
+ args: context.args,
1028
+ options: context.options,
1029
+ pmRootHash,
1030
+ cwdHash,
1031
+ installationId,
1032
+ errorCode: normalizedErrorCode,
1033
+ errorCategory: normalizedErrorCategory,
1034
+ exitCode: normalizedExitCode,
1035
+ errorMessage: context.error_message,
1036
+ }),
1037
+ };
1038
+ await enqueueTelemetryEvent(globalPmRoot, event);
1039
+ _lastFlushPromise = flushQueue(globalPmRoot, endpoint, retentionDays).catch(() => { });
755
1040
  }
756
1041
  catch {
757
1042
  // Telemetry must never block command execution.