@yagni-app/code 1.0.5 → 1.0.7

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 (111) hide show
  1. package/README.md +89 -27
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -3
  5. package/dist/claudePlugins.js +129 -19
  6. package/dist/cli.js +28 -4
  7. package/dist/doctor.d.ts +42 -4
  8. package/dist/doctor.js +150 -15
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +18 -5
  12. package/dist/extension/index.js +319 -72
  13. package/dist/extension/mcp/approval.d.ts +45 -0
  14. package/dist/extension/mcp/approval.js +164 -0
  15. package/dist/extension/mcp/auth.d.ts +124 -0
  16. package/dist/extension/mcp/auth.js +560 -0
  17. package/dist/extension/mcp/authStore.d.ts +61 -0
  18. package/dist/extension/mcp/authStore.js +105 -0
  19. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  20. package/dist/extension/mcp/callbackPage.js +222 -0
  21. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  22. package/dist/extension/mcp/cliConfig.js +12 -0
  23. package/dist/extension/mcp/config.d.ts +153 -0
  24. package/dist/extension/mcp/config.js +381 -0
  25. package/dist/extension/mcp/log.d.ts +28 -0
  26. package/dist/extension/mcp/log.js +82 -0
  27. package/dist/extension/mcp/manager.d.ts +100 -0
  28. package/dist/extension/mcp/manager.js +273 -0
  29. package/dist/extension/mcp/names.d.ts +25 -0
  30. package/dist/extension/mcp/names.js +40 -0
  31. package/dist/extension/mcp/panel.d.ts +33 -0
  32. package/dist/extension/mcp/panel.js +268 -0
  33. package/dist/extension/mcp/prompts.d.ts +23 -0
  34. package/dist/extension/mcp/prompts.js +93 -0
  35. package/dist/extension/mcp/startup.d.ts +55 -0
  36. package/dist/extension/mcp/startup.js +152 -0
  37. package/dist/extension/mcp/tools.d.ts +31 -0
  38. package/dist/extension/mcp/tools.js +117 -0
  39. package/dist/extension/mcp/transports.d.ts +17 -0
  40. package/dist/extension/mcp/transports.js +44 -0
  41. package/dist/extension/permission/gate.d.ts +104 -2
  42. package/dist/extension/permission/gate.js +387 -31
  43. package/dist/extension/permission/guardian.d.ts +24 -5
  44. package/dist/extension/permission/guardian.js +162 -24
  45. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  46. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  47. package/dist/extension/permissionRules/engine.d.ts +50 -0
  48. package/dist/extension/permissionRules/engine.js +238 -0
  49. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  50. package/dist/extension/permissionRules/loadConfig.js +90 -0
  51. package/dist/extension/permissionRules/parser.d.ts +38 -0
  52. package/dist/extension/permissionRules/parser.js +136 -0
  53. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  54. package/dist/extension/permissionRules/pathRules.js +120 -0
  55. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  56. package/dist/extension/permissionRules/shellRules.js +221 -0
  57. package/dist/extension/pipeline/invocation.d.ts +3 -6
  58. package/dist/extension/pipeline/invocation.js +3 -6
  59. package/dist/extension/pipeline/personas.js +5 -0
  60. package/dist/extension/pipeline/runner.d.ts +0 -1
  61. package/dist/extension/pipeline/runner.js +6 -14
  62. package/dist/extension/plugins/inventory.d.ts +88 -0
  63. package/dist/extension/plugins/inventory.js +144 -0
  64. package/dist/extension/plugins/panel.d.ts +45 -0
  65. package/dist/extension/plugins/panel.js +293 -0
  66. package/dist/extension/sandbox/bash.d.ts +99 -0
  67. package/dist/extension/sandbox/bash.js +190 -0
  68. package/dist/extension/sandbox/config.d.ts +114 -0
  69. package/dist/extension/sandbox/config.js +366 -0
  70. package/dist/extension/sandbox/manager.d.ts +98 -0
  71. package/dist/extension/sandbox/manager.js +216 -0
  72. package/dist/extension/sandbox/panel.d.ts +111 -0
  73. package/dist/extension/sandbox/panel.js +342 -0
  74. package/dist/extension/sandbox/session.d.ts +85 -0
  75. package/dist/extension/sandbox/session.js +775 -0
  76. package/dist/extension/telemetry/attrs.d.ts +96 -0
  77. package/dist/extension/telemetry/attrs.js +149 -0
  78. package/dist/extension/telemetry/config.d.ts +99 -0
  79. package/dist/extension/telemetry/config.js +193 -0
  80. package/dist/extension/telemetry/index.d.ts +7 -0
  81. package/dist/extension/telemetry/index.js +7 -0
  82. package/dist/extension/telemetry/probe.d.ts +29 -0
  83. package/dist/extension/telemetry/probe.js +122 -0
  84. package/dist/extension/telemetry/register.d.ts +40 -0
  85. package/dist/extension/telemetry/register.js +192 -0
  86. package/dist/extension/telemetry/sdk.d.ts +63 -0
  87. package/dist/extension/telemetry/sdk.js +207 -0
  88. package/dist/extension/telemetry/tracker.d.ts +131 -0
  89. package/dist/extension/telemetry/tracker.js +551 -0
  90. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  91. package/dist/extension/vendor/ignore.d.ts +86 -0
  92. package/dist/extension/vendor/ignore.js +788 -0
  93. package/dist/goHeadless.d.ts +1 -1
  94. package/dist/goHeadless.js +2 -2
  95. package/dist/launch.d.ts +4 -3
  96. package/dist/launch.js +7 -4
  97. package/dist/mcpCommand.d.ts +122 -0
  98. package/dist/mcpCommand.js +787 -0
  99. package/dist/otel.d.ts +69 -63
  100. package/dist/otel.js +154 -119
  101. package/dist/paths.d.ts +13 -0
  102. package/dist/paths.js +18 -0
  103. package/dist/pluginCommand.d.ts +43 -0
  104. package/dist/pluginCommand.js +499 -0
  105. package/dist/pluginStore.d.ts +170 -0
  106. package/dist/pluginStore.js +554 -0
  107. package/dist/upgrade.d.ts +11 -2
  108. package/dist/upgrade.js +48 -8
  109. package/package.json +20 -3
  110. package/dist/extension/mcpTools.d.ts +0 -57
  111. package/dist/extension/mcpTools.js +0 -132
package/dist/otel.d.ts CHANGED
@@ -1,59 +1,55 @@
1
1
  /**
2
- * OTel export wiring (Updater pilot ask): bake the `pi-otel` extension into
3
- * every launch so a workspace can stream session tracestoken usage, cost,
4
- * model tier, tool calls — to its own OTLP collector (e.g. Datadog) without
5
- * anyone hand-installing an extension.
2
+ * OTel export gate (Updater pilot ask, now Claude Code parity): decide
3
+ * whether a launch streams session telemetrytraces, metrics, and log
4
+ * events — to the workspace's own OTLP collector (Datadog, Grafana, an
5
+ * in-house collector), and hand the session the standard OpenTelemetry env
6
+ * it needs. The exporter itself lives in the bundled extension
7
+ * (`pi-extension-yagni/src/telemetry`), so there is nothing extra to load and
8
+ * nothing that can go missing on a customer's `npm install`.
6
9
  *
7
- * Three deliberate policies, all enforced here rather than left to defaults:
10
+ * Three deliberate policies, enforced here rather than left to defaults:
8
11
  *
9
- * 1. GATED, not always-on. pi-otel ships `enabled: true` pointed at
10
- * `localhost:4317`, so loading it unconditionally would have every session
11
- * probing a collector nobody runs. We only pass the extension to pi when an
12
- * OTLP endpoint is actually configured: `OTEL_EXPORTER_OTLP_ENDPOINT` in the
13
- * environment, or `otel.endpoint` in the repo's committed `.pi/settings.json`
14
- * (the file pi-otel itself reads, so teams can configure once per repo).
12
+ * 1. GATED, not always-on. Nothing exports unless an OTLP destination is
13
+ * configured: the user's own OTEL_* env, the workspace's admin-set config
14
+ * (fetched fail-soft and cached), or the repo's committed
15
+ * `.pi/settings.json`. The gate result travels to the session as
16
+ * `YAGNI_OTEL_EXPORT=1` plus the OTLP variables; children (/go stages,
17
+ * subagents) inherit the env and export too.
15
18
  *
16
- * 2. METADATA-ONLY, enforced. Cost, tokens, model, finish reasons and tool-call
17
- * ids export; prompt and response text never do. `PI_OTEL_CAPTURE_CONTENT`
18
- * is pinned in the child env, and env beats settings in pi-otel's own
19
- * precedence — a repo settings file asking for "full" is overridden, not
20
- * honored. Session content leaving the machine is a contract change, not a
21
- * config knob.
19
+ * 2. METADATA-ONLY, in code. Token counts, cost, tier, durations, tool names,
20
+ * and sizes export; prompt, response, and tool content never do. The
21
+ * extension enforces this without a knob no env var re-enables content.
22
22
  *
23
- * 3. Cost is the customer's SELL rate. pi-otel exports pi's `usage.cost.total`
24
- * verbatim (as `pi.cost.usd`), and pi computes that from the catalog's
25
- * tier rates (YAG-381) — so the collector sees contracted prices, and the
26
- * exported model name is the opaque tier id, never the backing model.
23
+ * 3. Cost is the customer's SELL rate. Usage cost is pi's `usage.cost.total`,
24
+ * computed from the catalog's tier rates (YAG-381), and the exported model
25
+ * name is the opaque tier id, never the backing model.
27
26
  *
28
- * Everything here is fail-soft: telemetry must never block a launch, so a
29
- * missing package or unreadable settings file resolves to "disabled".
27
+ * Everything here is fail-soft: telemetry must never block a launch, so an
28
+ * unreadable settings file or an offline backend resolves to "disabled" or
29
+ * to last-known config.
30
30
  */
31
31
  import { readFileSync } from "node:fs";
32
- /**
33
- * Env var carrying the resolved pi-otel entry path to /go stage children and
34
- * subagents: they build their own pi argv inside pi-extension-yagni (which
35
- * cannot import this package), so the path crosses over env. The extension
36
- * reads the same literal in `pipeline/runner.ts`.
37
- */
38
- export declare const OTEL_EXTENSION_PATH_ENV = "YAGNI_OTEL_EXTENSION_PATH";
32
+ /** The launcher's "this session exports" gate, read by the bundled extension. */
33
+ export declare const OTEL_EXPORT_ENV = "YAGNI_OTEL_EXPORT";
39
34
  /** The service name a collector sees unless the user set their own. */
40
35
  export declare const DEFAULT_OTEL_SERVICE_NAME = "yagni-code";
36
+ /** The standard per-signal endpoint variables, generic first (its precedence). */
37
+ declare const ENDPOINT_ENV_VARS: readonly ["OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"];
38
+ export type EndpointEnvVar = (typeof ENDPOINT_ENV_VARS)[number];
41
39
  export interface OtelLaunchConfig {
42
- /** Absolute path to pi-otel's extension entry. */
43
- extensionPath: string;
44
40
  /**
45
- * Where the enablement signal came from. "workspace" additionally carries
46
- * protocol/headers/serviceName, delivered as env to the session (the other
47
- * sources leave those to whatever the user/repo already configured).
41
+ * Where the enablement signal came from. "workspace" and "project-settings"
42
+ * carry endpoint/protocol/headers/serviceName that the launcher delivers as
43
+ * env; "env" is the user's own setup, passed through untouched.
48
44
  */
49
45
  source: "env" | "workspace" | "project-settings";
50
- /** The configured OTLP endpoint (for doctor display; env for workspace source). */
46
+ /** The env var the endpoint came from (env source only; doctor names it). */
47
+ envVar?: EndpointEnvVar;
48
+ /** The configured endpoint: a BASE URL for workspace/project sources. */
51
49
  endpoint: string;
52
- /** Workspace-configured OTLP protocol (workspace source only). */
53
50
  protocol?: string;
54
- /** Workspace-configured collector headers (workspace source only; carries secrets). */
51
+ /** Collector headers (workspace / project sources; carries secrets). */
55
52
  headers?: Record<string, string>;
56
- /** Workspace-configured service name (workspace source only). */
57
53
  serviceName?: string;
58
54
  }
59
55
  /**
@@ -69,34 +65,44 @@ export interface WorkspaceOtelConfig {
69
65
  headers?: Record<string, string>;
70
66
  }
71
67
  /**
72
- * Decide whether this launch exports OTel traces, and with which pi-otel entry.
73
- * Returns undefined when export stays off: no endpoint configured anywhere,
74
- * `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
68
+ * Reduce an endpoint that carries a signal path to its base
69
+ * (`https://otlp.datadoghq.com/v1/traces` `https://otlp.datadoghq.com`).
70
+ * The extension appends `/v1/<signal>` per signal for http transports, so a
71
+ * workspace config saved before logs and metrics existed keeps exporting all
72
+ * three without an admin re-save. Mirrors the extension's own helper.
73
+ */
74
+ export declare function stripSignalPath(endpoint: string): string;
75
+ /** Personal kill switch: beats every source. Both spellings accepted. */
76
+ export declare function envDisabled(env: NodeJS.ProcessEnv): boolean;
77
+ /**
78
+ * Decide whether this launch exports, from the user's env and the repo's
79
+ * settings. Returns undefined when export stays off: no endpoint configured,
80
+ * or a kill switch set.
81
+ *
82
+ * Any of the standard endpoint variables enables export — the generic
83
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` or a per-signal one — so a machine that
84
+ * carries a Claude Code telemetry env (typically all three per-signal
85
+ * endpoints) lights up with no translation. The extension reads the same
86
+ * variables, per signal, following the OTLP exporter spec.
75
87
  */
76
88
  export declare function resolveOtelLaunch(opts: {
77
89
  env: NodeJS.ProcessEnv;
78
90
  cwd: string;
79
- /** Injectable seams for tests. */
80
- resolveExtension?: () => string;
91
+ /** Injectable seam for tests. */
81
92
  readFile?: typeof readFileSync;
82
93
  }): OtelLaunchConfig | undefined;
83
94
  /**
84
- * Absolute path to pi-otel's extension entry (its package main IS the pi
85
- * extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
86
- * for pi itself. Throws when the package is missing — callers treat that as
87
- * "export off", not an error.
88
- */
89
- export declare function resolveOtelExtensionPath(): string;
90
- /**
91
- * The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
92
- * is pinned unconditionally (policy 2 above); the service name only fills in
93
- * when the user has not chosen their own.
95
+ * The env keys an exporting child must carry. `YAGNI_OTEL_EXPORT=1` is the
96
+ * gate the extension reads; the service name only fills in when the user has
97
+ * not chosen their own.
94
98
  *
95
- * A "workspace"-sourced config additionally delivers the admin-set endpoint,
96
- * protocol, and collector headers over the standard OTel env vars — but any
97
- * of those the USER already set in their own environment wins (local env >
98
- * workspace config), so an engineer can point one session at a scratch
99
- * collector without an admin change.
99
+ * A "workspace" or "project-settings" config delivers its endpoint (as the
100
+ * generic BASE endpoint the extension derives the per-signal URLs),
101
+ * protocol, headers, and service name over the standard OTel env vars but
102
+ * any of those the USER already set in their own environment wins (local
103
+ * env > workspace config), so an engineer can point one session at a
104
+ * scratch collector without an admin change. An "env" config is the user's
105
+ * own setup and passes through untouched.
100
106
  */
101
107
  export declare function otelChildEnv(config: OtelLaunchConfig, baseEnv: NodeJS.ProcessEnv): Record<string, string>;
102
108
  export interface WorkspaceOtelFetchDeps {
@@ -125,10 +131,10 @@ export declare function fetchWorkspaceOtel(creds: {
125
131
  token: string;
126
132
  }, profileName: string, deps?: WorkspaceOtelFetchDeps): Promise<WorkspaceOtelConfig | null>;
127
133
  /**
128
- * Full launch-time resolution, all three sources in precedence order:
134
+ * Full launch-time resolution, all four sources in precedence order:
129
135
  *
130
- * 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
131
- * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` — the user's own setup, untouched.
136
+ * 1. `PI_OTEL_DISABLED` / `YAGNI_OTEL_DISABLED` — personal kill switch.
137
+ * 2. env `OTEL_EXPORTER_OTLP_*_ENDPOINT` — the user's own setup, untouched.
132
138
  * 3. workspace config — admin-set in the web app, fetched/cached.
133
139
  * 4. repo `.pi/settings.json` — committed per-repo config.
134
140
  *
@@ -142,9 +148,9 @@ export declare function resolveOtelLaunchWithWorkspace(opts: {
142
148
  token: string;
143
149
  } | null;
144
150
  profileName?: string;
145
- resolveExtension?: () => string;
146
151
  readFile?: typeof readFileSync;
147
152
  fetchDeps?: WorkspaceOtelFetchDeps;
148
153
  fetchWorkspace?: typeof fetchWorkspaceOtel;
149
154
  }): Promise<OtelLaunchConfig | undefined>;
155
+ export {};
150
156
  //# sourceMappingURL=otel.d.ts.map
package/dist/otel.js CHANGED
@@ -1,53 +1,72 @@
1
1
  /**
2
- * OTel export wiring (Updater pilot ask): bake the `pi-otel` extension into
3
- * every launch so a workspace can stream session tracestoken usage, cost,
4
- * model tier, tool calls — to its own OTLP collector (e.g. Datadog) without
5
- * anyone hand-installing an extension.
2
+ * OTel export gate (Updater pilot ask, now Claude Code parity): decide
3
+ * whether a launch streams session telemetrytraces, metrics, and log
4
+ * events — to the workspace's own OTLP collector (Datadog, Grafana, an
5
+ * in-house collector), and hand the session the standard OpenTelemetry env
6
+ * it needs. The exporter itself lives in the bundled extension
7
+ * (`pi-extension-yagni/src/telemetry`), so there is nothing extra to load and
8
+ * nothing that can go missing on a customer's `npm install`.
6
9
  *
7
- * Three deliberate policies, all enforced here rather than left to defaults:
10
+ * Three deliberate policies, enforced here rather than left to defaults:
8
11
  *
9
- * 1. GATED, not always-on. pi-otel ships `enabled: true` pointed at
10
- * `localhost:4317`, so loading it unconditionally would have every session
11
- * probing a collector nobody runs. We only pass the extension to pi when an
12
- * OTLP endpoint is actually configured: `OTEL_EXPORTER_OTLP_ENDPOINT` in the
13
- * environment, or `otel.endpoint` in the repo's committed `.pi/settings.json`
14
- * (the file pi-otel itself reads, so teams can configure once per repo).
12
+ * 1. GATED, not always-on. Nothing exports unless an OTLP destination is
13
+ * configured: the user's own OTEL_* env, the workspace's admin-set config
14
+ * (fetched fail-soft and cached), or the repo's committed
15
+ * `.pi/settings.json`. The gate result travels to the session as
16
+ * `YAGNI_OTEL_EXPORT=1` plus the OTLP variables; children (/go stages,
17
+ * subagents) inherit the env and export too.
15
18
  *
16
- * 2. METADATA-ONLY, enforced. Cost, tokens, model, finish reasons and tool-call
17
- * ids export; prompt and response text never do. `PI_OTEL_CAPTURE_CONTENT`
18
- * is pinned in the child env, and env beats settings in pi-otel's own
19
- * precedence — a repo settings file asking for "full" is overridden, not
20
- * honored. Session content leaving the machine is a contract change, not a
21
- * config knob.
19
+ * 2. METADATA-ONLY, in code. Token counts, cost, tier, durations, tool names,
20
+ * and sizes export; prompt, response, and tool content never do. The
21
+ * extension enforces this without a knob no env var re-enables content.
22
22
  *
23
- * 3. Cost is the customer's SELL rate. pi-otel exports pi's `usage.cost.total`
24
- * verbatim (as `pi.cost.usd`), and pi computes that from the catalog's
25
- * tier rates (YAG-381) — so the collector sees contracted prices, and the
26
- * exported model name is the opaque tier id, never the backing model.
23
+ * 3. Cost is the customer's SELL rate. Usage cost is pi's `usage.cost.total`,
24
+ * computed from the catalog's tier rates (YAG-381), and the exported model
25
+ * name is the opaque tier id, never the backing model.
27
26
  *
28
- * Everything here is fail-soft: telemetry must never block a launch, so a
29
- * missing package or unreadable settings file resolves to "disabled".
27
+ * Everything here is fail-soft: telemetry must never block a launch, so an
28
+ * unreadable settings file or an offline backend resolves to "disabled" or
29
+ * to last-known config.
30
30
  */
31
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
31
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
32
32
  import { join } from "node:path";
33
- import { fileURLToPath } from "node:url";
34
33
  import { credentialsDir } from "./credentials.js";
35
- /**
36
- * Env var carrying the resolved pi-otel entry path to /go stage children and
37
- * subagents: they build their own pi argv inside pi-extension-yagni (which
38
- * cannot import this package), so the path crosses over env. The extension
39
- * reads the same literal in `pipeline/runner.ts`.
40
- */
41
- export const OTEL_EXTENSION_PATH_ENV = "YAGNI_OTEL_EXTENSION_PATH";
34
+ /** The launcher's "this session exports" gate, read by the bundled extension. */
35
+ export const OTEL_EXPORT_ENV = "YAGNI_OTEL_EXPORT";
42
36
  /** The service name a collector sees unless the user set their own. */
43
37
  export const DEFAULT_OTEL_SERVICE_NAME = "yagni-code";
44
38
  /**
45
- * Read `otel.endpoint` from the repo's `.pi/settings.json`, the project-level
46
- * file pi-otel itself resolves config from. Returns undefined on any problem —
47
- * a malformed settings file must not block a launch (pi-otel will surface its
48
- * own complaint in-session).
39
+ * Headers that belong on the TRACES request only. Datadog's LLM Observability
40
+ * routes OTLP traces to its LLM product when `dd-otlp-source=llmobs` rides
41
+ * along; the same header on the metrics and logs intakes is meaningless at
42
+ * best, so the launcher scopes it to `OTEL_EXPORTER_OTLP_TRACES_HEADERS`.
43
+ */
44
+ const TRACE_ONLY_HEADERS = new Set(["dd-otlp-source"]);
45
+ /** The standard per-signal endpoint variables, generic first (its precedence). */
46
+ const ENDPOINT_ENV_VARS = [
47
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
48
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
49
+ "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
50
+ "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
51
+ ];
52
+ /** Trailing `/v1/<signal>` on a URL — the shape pre-parity configs stored. */
53
+ const SIGNAL_PATH_RE = /\/v1\/(traces|metrics|logs)\/?$/i;
54
+ /**
55
+ * Reduce an endpoint that carries a signal path to its base
56
+ * (`https://otlp.datadoghq.com/v1/traces` → `https://otlp.datadoghq.com`).
57
+ * The extension appends `/v1/<signal>` per signal for http transports, so a
58
+ * workspace config saved before logs and metrics existed keeps exporting all
59
+ * three without an admin re-save. Mirrors the extension's own helper.
49
60
  */
50
- function projectOtelEndpoint(cwd, readFile) {
61
+ export function stripSignalPath(endpoint) {
62
+ return endpoint.replace(SIGNAL_PATH_RE, "").replace(/\/+$/, "");
63
+ }
64
+ /**
65
+ * Read the `otel` block from the repo's `.pi/settings.json`, the project-level
66
+ * file a team commits once so every clone exports. Returns undefined on any
67
+ * problem — a malformed settings file must not block a launch.
68
+ */
69
+ function projectOtelSettings(cwd, readFile) {
51
70
  try {
52
71
  const raw = readFile(join(cwd, ".pi", "settings.json"), "utf8");
53
72
  const parsed = JSON.parse(String(raw));
@@ -56,92 +75,126 @@ function projectOtelEndpoint(cwd, readFile) {
56
75
  const otel = parsed.otel;
57
76
  if (typeof otel !== "object" || otel === null)
58
77
  return undefined;
59
- const endpoint = otel.endpoint;
60
- return typeof endpoint === "string" && endpoint.trim() !== "" ? endpoint.trim() : undefined;
78
+ const o = otel;
79
+ if (o.enabled === false)
80
+ return undefined;
81
+ const endpoint = typeof o.endpoint === "string" && o.endpoint.trim() !== "" ? o.endpoint.trim() : undefined;
82
+ if (!endpoint)
83
+ return undefined;
84
+ const headers = {};
85
+ if (typeof o.headers === "object" && o.headers !== null) {
86
+ for (const [k, v] of Object.entries(o.headers)) {
87
+ // Headers travel to the session as the comma-separated
88
+ // OTEL_EXPORTER_OTLP_HEADERS list; a comma in a value would corrupt
89
+ // every pair after it, so such a header is dropped (same rule the
90
+ // workspace settings enforce at save time).
91
+ if (typeof v === "string" && !v.includes(",") && !k.includes(","))
92
+ headers[k] = v;
93
+ }
94
+ }
95
+ return {
96
+ endpoint,
97
+ ...(typeof o.protocol === "string" && o.protocol.trim() ? { protocol: o.protocol.trim() } : {}),
98
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
99
+ ...(typeof o.serviceName === "string" && o.serviceName.trim() ? { serviceName: o.serviceName.trim() } : {}),
100
+ };
61
101
  }
62
102
  catch {
63
103
  return undefined;
64
104
  }
65
105
  }
66
- /** Truthy per pi-otel's own convention for PI_OTEL_DISABLED. */
67
- function envDisabled(env) {
68
- return env.PI_OTEL_DISABLED === "1" || env.PI_OTEL_DISABLED === "true";
106
+ /** Personal kill switch: beats every source. Both spellings accepted. */
107
+ export function envDisabled(env) {
108
+ const truthy = (v) => v === "1" || v === "true";
109
+ return truthy(env.PI_OTEL_DISABLED) || truthy(env.YAGNI_OTEL_DISABLED);
110
+ }
111
+ /** The first configured endpoint variable in the user's env, generic first. */
112
+ function envEndpoint(env) {
113
+ for (const envVar of ENDPOINT_ENV_VARS) {
114
+ const value = env[envVar]?.trim();
115
+ if (value)
116
+ return { envVar, endpoint: value };
117
+ }
118
+ return undefined;
69
119
  }
70
120
  /**
71
- * Decide whether this launch exports OTel traces, and with which pi-otel entry.
72
- * Returns undefined when export stays off: no endpoint configured anywhere,
73
- * `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
121
+ * Decide whether this launch exports, from the user's env and the repo's
122
+ * settings. Returns undefined when export stays off: no endpoint configured,
123
+ * or a kill switch set.
124
+ *
125
+ * Any of the standard endpoint variables enables export — the generic
126
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` or a per-signal one — so a machine that
127
+ * carries a Claude Code telemetry env (typically all three per-signal
128
+ * endpoints) lights up with no translation. The extension reads the same
129
+ * variables, per signal, following the OTLP exporter spec.
74
130
  */
75
131
  export function resolveOtelLaunch(opts) {
76
132
  const { env, cwd } = opts;
77
133
  if (envDisabled(env))
78
134
  return undefined;
79
- const envEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
80
- const settingsEndpoint = envEndpoint
81
- ? undefined
82
- : projectOtelEndpoint(cwd, opts.readFile ?? readFileSync);
83
- if (!envEndpoint && !settingsEndpoint)
135
+ const fromEnv = envEndpoint(env);
136
+ if (fromEnv)
137
+ return { source: "env", envVar: fromEnv.envVar, endpoint: fromEnv.endpoint };
138
+ const settings = projectOtelSettings(cwd, opts.readFile ?? readFileSync);
139
+ if (!settings)
84
140
  return undefined;
85
- let extensionPath;
86
- try {
87
- extensionPath = (opts.resolveExtension ?? resolveOtelExtensionPath)();
88
- }
89
- catch {
90
- return undefined;
91
- }
92
- return envEndpoint
93
- ? { extensionPath, source: "env", endpoint: envEndpoint }
94
- : { extensionPath, source: "project-settings", endpoint: settingsEndpoint };
141
+ return {
142
+ source: "project-settings",
143
+ endpoint: settings.endpoint,
144
+ ...(settings.protocol ? { protocol: settings.protocol } : {}),
145
+ ...(settings.headers ? { headers: settings.headers } : {}),
146
+ ...(settings.serviceName ? { serviceName: settings.serviceName } : {}),
147
+ };
95
148
  }
96
- /**
97
- * Absolute path to pi-otel's extension entry (its package main IS the pi
98
- * extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
99
- * for pi itself. Throws when the package is missing — callers treat that as
100
- * "export off", not an error.
101
- */
102
- export function resolveOtelExtensionPath() {
103
- const path = fileURLToPath(import.meta.resolve("pi-otel"));
104
- if (!existsSync(path)) {
105
- throw new Error(`pi-otel entry not found at ${path}`);
106
- }
107
- return path;
149
+ /** Render a header record as the comma-separated `k=v` list the OTel env carries. */
150
+ function headerList(headers) {
151
+ return Object.entries(headers)
152
+ .map(([k, v]) => `${k}=${v}`)
153
+ .join(",");
108
154
  }
109
155
  /**
110
- * The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
111
- * is pinned unconditionally (policy 2 above); the service name only fills in
112
- * when the user has not chosen their own.
156
+ * The env keys an exporting child must carry. `YAGNI_OTEL_EXPORT=1` is the
157
+ * gate the extension reads; the service name only fills in when the user has
158
+ * not chosen their own.
113
159
  *
114
- * A "workspace"-sourced config additionally delivers the admin-set endpoint,
115
- * protocol, and collector headers over the standard OTel env vars — but any
116
- * of those the USER already set in their own environment wins (local env >
117
- * workspace config), so an engineer can point one session at a scratch
118
- * collector without an admin change.
160
+ * A "workspace" or "project-settings" config delivers its endpoint (as the
161
+ * generic BASE endpoint the extension derives the per-signal URLs),
162
+ * protocol, headers, and service name over the standard OTel env vars but
163
+ * any of those the USER already set in their own environment wins (local
164
+ * env > workspace config), so an engineer can point one session at a
165
+ * scratch collector without an admin change. An "env" config is the user's
166
+ * own setup and passes through untouched.
119
167
  */
120
168
  export function otelChildEnv(config, baseEnv) {
121
- const workspace = {};
122
- if (config.source === "workspace") {
123
- if (!baseEnv.OTEL_EXPORTER_OTLP_ENDPOINT) {
124
- workspace.OTEL_EXPORTER_OTLP_ENDPOINT = config.endpoint;
169
+ const out = {};
170
+ if (config.source === "workspace" || config.source === "project-settings") {
171
+ const userHasEndpoint = ENDPOINT_ENV_VARS.some((v) => baseEnv[v]?.trim());
172
+ if (!userHasEndpoint) {
173
+ out.OTEL_EXPORTER_OTLP_ENDPOINT = stripSignalPath(config.endpoint);
125
174
  }
126
175
  if (config.protocol && !baseEnv.OTEL_EXPORTER_OTLP_PROTOCOL) {
127
- workspace.OTEL_EXPORTER_OTLP_PROTOCOL = config.protocol;
176
+ out.OTEL_EXPORTER_OTLP_PROTOCOL = config.protocol;
128
177
  }
129
178
  if (config.headers && Object.keys(config.headers).length > 0 && !baseEnv.OTEL_EXPORTER_OTLP_HEADERS) {
130
- workspace.OTEL_EXPORTER_OTLP_HEADERS = Object.entries(config.headers)
131
- .map(([k, v]) => `${k}=${v}`)
132
- .join(",");
179
+ const shared = {};
180
+ const traceOnly = {};
181
+ for (const [k, v] of Object.entries(config.headers)) {
182
+ (TRACE_ONLY_HEADERS.has(k.toLowerCase()) ? traceOnly : shared)[k] = v;
183
+ }
184
+ if (Object.keys(shared).length > 0)
185
+ out.OTEL_EXPORTER_OTLP_HEADERS = headerList(shared);
186
+ if (Object.keys(traceOnly).length > 0 && !baseEnv.OTEL_EXPORTER_OTLP_TRACES_HEADERS) {
187
+ out.OTEL_EXPORTER_OTLP_TRACES_HEADERS = headerList(traceOnly);
188
+ }
133
189
  }
134
190
  if (config.serviceName && !baseEnv.OTEL_SERVICE_NAME) {
135
- workspace.OTEL_SERVICE_NAME = config.serviceName;
191
+ out.OTEL_SERVICE_NAME = config.serviceName;
136
192
  }
137
193
  }
138
194
  return {
139
- ...workspace,
140
- [OTEL_EXTENSION_PATH_ENV]: config.extensionPath,
141
- PI_OTEL_CAPTURE_CONTENT: "metadata_only",
142
- ...(baseEnv.OTEL_SERVICE_NAME || workspace.OTEL_SERVICE_NAME
143
- ? {}
144
- : { OTEL_SERVICE_NAME: DEFAULT_OTEL_SERVICE_NAME }),
195
+ ...out,
196
+ [OTEL_EXPORT_ENV]: "1",
197
+ ...(baseEnv.OTEL_SERVICE_NAME || out.OTEL_SERVICE_NAME ? {} : { OTEL_SERVICE_NAME: DEFAULT_OTEL_SERVICE_NAME }),
145
198
  };
146
199
  }
147
200
  // ── Workspace-configured export (admin sets it once in the web app) ─────────
@@ -235,10 +288,10 @@ function readWorkspaceOtelCache(cachePath) {
235
288
  }
236
289
  }
237
290
  /**
238
- * Full launch-time resolution, all three sources in precedence order:
291
+ * Full launch-time resolution, all four sources in precedence order:
239
292
  *
240
- * 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
241
- * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` — the user's own setup, untouched.
293
+ * 1. `PI_OTEL_DISABLED` / `YAGNI_OTEL_DISABLED` — personal kill switch.
294
+ * 2. env `OTEL_EXPORTER_OTLP_*_ENDPOINT` — the user's own setup, untouched.
242
295
  * 3. workspace config — admin-set in the web app, fetched/cached.
243
296
  * 4. repo `.pi/settings.json` — committed per-repo config.
244
297
  *
@@ -249,27 +302,14 @@ export async function resolveOtelLaunchWithWorkspace(opts) {
249
302
  if (envDisabled(env))
250
303
  return undefined;
251
304
  // Source 2: the user's own env config short-circuits — no fetch needed.
252
- if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim()) {
253
- return resolveOtelLaunch({
254
- env,
255
- cwd,
256
- ...(opts.resolveExtension ? { resolveExtension: opts.resolveExtension } : {}),
257
- ...(opts.readFile ? { readFile: opts.readFile } : {}),
258
- });
305
+ if (envEndpoint(env)) {
306
+ return resolveOtelLaunch({ env, cwd, ...(opts.readFile ? { readFile: opts.readFile } : {}) });
259
307
  }
260
308
  // Source 3: workspace config.
261
309
  if (opts.creds?.token && opts.profileName) {
262
310
  const workspace = await (opts.fetchWorkspace ?? fetchWorkspaceOtel)({ baseUrl: opts.creds.baseUrl, token: opts.creds.token }, opts.profileName, opts.fetchDeps ?? {});
263
311
  if (workspace) {
264
- let extensionPath;
265
- try {
266
- extensionPath = (opts.resolveExtension ?? resolveOtelExtensionPath)();
267
- }
268
- catch {
269
- return undefined;
270
- }
271
312
  return {
272
- extensionPath,
273
313
  source: "workspace",
274
314
  endpoint: workspace.endpoint,
275
315
  ...(workspace.protocol ? { protocol: workspace.protocol } : {}),
@@ -281,11 +321,6 @@ export async function resolveOtelLaunchWithWorkspace(opts) {
281
321
  }
282
322
  }
283
323
  // Source 4: repo-committed settings.
284
- return resolveOtelLaunch({
285
- env,
286
- cwd,
287
- ...(opts.resolveExtension ? { resolveExtension: opts.resolveExtension } : {}),
288
- ...(opts.readFile ? { readFile: opts.readFile } : {}),
289
- });
324
+ return resolveOtelLaunch({ env, cwd, ...(opts.readFile ? { readFile: opts.readFile } : {}) });
290
325
  }
291
326
  //# sourceMappingURL=otel.js.map
package/dist/paths.d.ts CHANGED
@@ -19,6 +19,12 @@
19
19
  * fall back to resolving the workspace-linked package, which keeps `pnpm test`
20
20
  * and a fresh checkout working before anything is bundled.
21
21
  */
22
+ /**
23
+ * Absolute path to this CLI's entry (`dist/cli.js`). Forwarded to sessions as
24
+ * `YAGNI_CLI_PATH` so the extension's `/plugin` panel can run
25
+ * `yagni plugin …` with the exact binary that launched it.
26
+ */
27
+ export declare function resolveCliPath(): string;
22
28
  export declare function resolveExtensionPath(): string;
23
29
  /**
24
30
  * Absolute path to the extension's headless pipeline entry
@@ -29,6 +35,13 @@ export declare function resolveExtensionPath(): string;
29
35
  * it pulls in the pipeline alone, with none of pi's TUI surface.
30
36
  */
31
37
  export declare function resolveHeadlessGoPath(): string;
38
+ /**
39
+ * Absolute path to the extension's telemetry probe (`telemetry/probe.js`),
40
+ * the module `yagni doctor` imports to send one real record per signal.
41
+ * Same derivation as `resolveHeadlessGoPath`; only this module (and the OTel
42
+ * SDK it pulls in) loads, none of pi's TUI surface.
43
+ */
44
+ export declare function resolveTelemetryProbePath(): string;
32
45
  /**
33
46
  * Absolute path to the extension's session-worktree entry
34
47
  * (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.
package/dist/paths.js CHANGED
@@ -25,6 +25,14 @@ function resolveModulePath(specifier) {
25
25
  * fall back to resolving the workspace-linked package, which keeps `pnpm test`
26
26
  * and a fresh checkout working before anything is bundled.
27
27
  */
28
+ /**
29
+ * Absolute path to this CLI's entry (`dist/cli.js`). Forwarded to sessions as
30
+ * `YAGNI_CLI_PATH` so the extension's `/plugin` panel can run
31
+ * `yagni plugin …` with the exact binary that launched it.
32
+ */
33
+ export function resolveCliPath() {
34
+ return fileURLToPath(new URL("./cli.js", import.meta.url));
35
+ }
28
36
  export function resolveExtensionPath() {
29
37
  const bundled = fileURLToPath(new URL("./extension/index.js", import.meta.url));
30
38
  if (existsSync(bundled))
@@ -43,6 +51,16 @@ export function resolveHeadlessGoPath() {
43
51
  const entry = resolveExtensionPath();
44
52
  return join(dirname(entry), "pipeline", "headlessGo.js");
45
53
  }
54
+ /**
55
+ * Absolute path to the extension's telemetry probe (`telemetry/probe.js`),
56
+ * the module `yagni doctor` imports to send one real record per signal.
57
+ * Same derivation as `resolveHeadlessGoPath`; only this module (and the OTel
58
+ * SDK it pulls in) loads, none of pi's TUI surface.
59
+ */
60
+ export function resolveTelemetryProbePath() {
61
+ const entry = resolveExtensionPath();
62
+ return join(dirname(entry), "telemetry", "probe.js");
63
+ }
46
64
  /**
47
65
  * Absolute path to the extension's session-worktree entry
48
66
  * (`pipeline/sessionWorktree.js`), the module `yagni -w` imports.