@yagni-app/code 1.0.6 → 1.0.8

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 (92) hide show
  1. package/README.md +91 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/askUserQuestionTool.js +7 -2
  10. package/dist/extension/condensedTools.d.ts +12 -1
  11. package/dist/extension/condensedTools.js +17 -9
  12. package/dist/extension/config.d.ts +6 -0
  13. package/dist/extension/hooks.d.ts +3 -3
  14. package/dist/extension/hooks.js +30 -5
  15. package/dist/extension/index.d.ts +19 -0
  16. package/dist/extension/index.js +248 -43
  17. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  18. package/dist/extension/mcp/cliConfig.js +1 -1
  19. package/dist/extension/mcp/config.d.ts +24 -2
  20. package/dist/extension/mcp/config.js +75 -3
  21. package/dist/extension/mcp/manager.d.ts +3 -1
  22. package/dist/extension/mcp/manager.js +2 -2
  23. package/dist/extension/mcp/panel.d.ts +0 -1
  24. package/dist/extension/mcp/panel.js +13 -3
  25. package/dist/extension/mcp/startup.js +8 -6
  26. package/dist/extension/permission/gate.d.ts +101 -2
  27. package/dist/extension/permission/gate.js +485 -42
  28. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  29. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  30. package/dist/extension/permissionRules/engine.d.ts +50 -0
  31. package/dist/extension/permissionRules/engine.js +238 -0
  32. package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
  33. package/dist/extension/permissionRules/loadConfig.js +105 -0
  34. package/dist/extension/permissionRules/parser.d.ts +38 -0
  35. package/dist/extension/permissionRules/parser.js +136 -0
  36. package/dist/extension/permissionRules/pathRules.d.ts +60 -0
  37. package/dist/extension/permissionRules/pathRules.js +122 -0
  38. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  39. package/dist/extension/permissionRules/shellRules.js +221 -0
  40. package/dist/extension/pipeline/invocation.d.ts +3 -6
  41. package/dist/extension/pipeline/invocation.js +3 -6
  42. package/dist/extension/pipeline/runner.d.ts +0 -1
  43. package/dist/extension/pipeline/runner.js +6 -14
  44. package/dist/extension/plugins/inventory.d.ts +88 -0
  45. package/dist/extension/plugins/inventory.js +144 -0
  46. package/dist/extension/plugins/panel.d.ts +45 -0
  47. package/dist/extension/plugins/panel.js +293 -0
  48. package/dist/extension/sandbox/bash.d.ts +99 -0
  49. package/dist/extension/sandbox/bash.js +190 -0
  50. package/dist/extension/sandbox/config.d.ts +115 -0
  51. package/dist/extension/sandbox/config.js +388 -0
  52. package/dist/extension/sandbox/manager.d.ts +108 -0
  53. package/dist/extension/sandbox/manager.js +243 -0
  54. package/dist/extension/sandbox/panel.d.ts +111 -0
  55. package/dist/extension/sandbox/panel.js +342 -0
  56. package/dist/extension/sandbox/session.d.ts +85 -0
  57. package/dist/extension/sandbox/session.js +829 -0
  58. package/dist/extension/settingsFiles.d.ts +50 -0
  59. package/dist/extension/settingsFiles.js +206 -0
  60. package/dist/extension/telemetry/attrs.d.ts +96 -0
  61. package/dist/extension/telemetry/attrs.js +149 -0
  62. package/dist/extension/telemetry/config.d.ts +103 -0
  63. package/dist/extension/telemetry/config.js +193 -0
  64. package/dist/extension/telemetry/index.d.ts +7 -0
  65. package/dist/extension/telemetry/index.js +7 -0
  66. package/dist/extension/telemetry/probe.d.ts +29 -0
  67. package/dist/extension/telemetry/probe.js +122 -0
  68. package/dist/extension/telemetry/register.d.ts +47 -0
  69. package/dist/extension/telemetry/register.js +207 -0
  70. package/dist/extension/telemetry/sdk.d.ts +63 -0
  71. package/dist/extension/telemetry/sdk.js +207 -0
  72. package/dist/extension/telemetry/tracker.d.ts +131 -0
  73. package/dist/extension/telemetry/tracker.js +551 -0
  74. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  75. package/dist/extension/vendor/ignore.d.ts +86 -0
  76. package/dist/extension/vendor/ignore.js +788 -0
  77. package/dist/goHeadless.d.ts +1 -1
  78. package/dist/goHeadless.js +2 -2
  79. package/dist/launch.d.ts +4 -3
  80. package/dist/launch.js +7 -4
  81. package/dist/mcpCommand.d.ts +10 -1
  82. package/dist/mcpCommand.js +42 -10
  83. package/dist/otel.d.ts +67 -90
  84. package/dist/otel.js +152 -195
  85. package/dist/paths.d.ts +13 -0
  86. package/dist/paths.js +18 -0
  87. package/dist/pluginCommand.d.ts +43 -0
  88. package/dist/pluginCommand.js +499 -0
  89. package/dist/pluginStore.d.ts +170 -0
  90. package/dist/pluginStore.js +554 -0
  91. package/dist/upgrade.js +10 -1
  92. package/package.json +19 -3
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Telemetry export configuration, resolved from the standard OpenTelemetry
3
+ * environment contract (the same variables Claude Code reads), so a machine
4
+ * or workspace already configured for Claude Code lights up YAGNI Code with
5
+ * no translation.
6
+ *
7
+ * Pure: reads an env record, never the network or disk. The launcher decides
8
+ * WHETHER a session exports (see the CLI's otel.ts: env, workspace settings,
9
+ * or a repo's committed config) and sets `YAGNI_OTEL_EXPORT=1` plus the OTLP
10
+ * variables in the child env; this module turns that env into per-signal
11
+ * exporter settings.
12
+ *
13
+ * Endpoint rules follow the OTLP exporter spec:
14
+ * - a per-signal endpoint (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, …) is used
15
+ * verbatim;
16
+ * - the generic `OTEL_EXPORTER_OTLP_ENDPOINT` is a BASE: http transports
17
+ * append `/v1/<signal>`, gRPC uses it as-is;
18
+ * - nothing configured falls back to the local-collector defaults.
19
+ *
20
+ * Content policy is enforced here, not configurable: prompt and response
21
+ * text never export. `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_TOOL_DETAILS` are
22
+ * deliberately NOT honored — session content leaving the machine is a
23
+ * contract change, not an env knob.
24
+ */
25
+ export const SIGNALS = ["traces", "metrics", "logs"];
26
+ export const DEFAULT_SERVICE_NAME = "yagni-code";
27
+ const DEFAULT_GRPC_ENDPOINT = "http://localhost:4317";
28
+ const DEFAULT_HTTP_ENDPOINT = "http://localhost:4318";
29
+ const DEFAULT_INTERVALS_MS = { traces: 5_000, metrics: 60_000, logs: 5_000 };
30
+ const SIGNAL_ENV = { traces: "TRACES", metrics: "METRICS", logs: "LOGS" };
31
+ function envTrue(v) {
32
+ return v === "1" || v === "true";
33
+ }
34
+ function envBool(v, fallback) {
35
+ if (v === undefined || v.trim() === "")
36
+ return fallback;
37
+ const s = v.trim().toLowerCase();
38
+ if (s === "1" || s === "true" || s === "yes")
39
+ return true;
40
+ if (s === "0" || s === "false" || s === "no")
41
+ return false;
42
+ return fallback;
43
+ }
44
+ export function normalizeProtocol(p) {
45
+ if (!p)
46
+ return undefined;
47
+ const v = p.trim().toLowerCase();
48
+ if (v === "grpc")
49
+ return "grpc";
50
+ if (v === "http/protobuf" || v === "http-protobuf" || v === "http")
51
+ return "http/protobuf";
52
+ if (v === "http/json")
53
+ return "http/json";
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Parse a `k=v,k2=v2` list (OTEL_EXPORTER_OTLP_HEADERS / OTEL_RESOURCE_ATTRIBUTES).
58
+ * Values may be percent-encoded per the W3C baggage rules the spec references;
59
+ * a malformed escape keeps the raw value. Splits on the FIRST `=` only so a
60
+ * base64 value's trailing `==` survives.
61
+ */
62
+ export function parseKvList(s) {
63
+ const out = {};
64
+ if (!s)
65
+ return out;
66
+ for (const pair of s.split(",")) {
67
+ const eq = pair.indexOf("=");
68
+ if (eq <= 0)
69
+ continue;
70
+ const k = pair.slice(0, eq).trim();
71
+ if (!k)
72
+ continue;
73
+ const raw = pair.slice(eq + 1).trim();
74
+ let v = raw;
75
+ try {
76
+ v = decodeURIComponent(raw);
77
+ }
78
+ catch {
79
+ v = raw;
80
+ }
81
+ out[k] = v;
82
+ }
83
+ return out;
84
+ }
85
+ /** Trailing `/v1/<signal>` on a URL — the shape older workspace configs stored. */
86
+ const SIGNAL_PATH_RE = /\/v1\/(traces|metrics|logs)\/?$/i;
87
+ /**
88
+ * Reduce an endpoint that carries a signal path to its base
89
+ * (`https://otlp.datadoghq.com/v1/traces` → `https://otlp.datadoghq.com`).
90
+ * Pre-parity workspace configs stored the full trace URL; treating it as a
91
+ * base keeps them exporting all three signals without an admin re-save.
92
+ */
93
+ export function stripSignalPath(endpoint) {
94
+ return endpoint.replace(SIGNAL_PATH_RE, "").replace(/\/+$/, "");
95
+ }
96
+ /**
97
+ * Resolve one signal's URL from the env, per the OTLP exporter spec.
98
+ * Exported for the launcher's doctor so its report names the exact URL a
99
+ * session will post to.
100
+ */
101
+ export function resolveSignalUrl(env, signal, protocol) {
102
+ const specific = env[`OTEL_EXPORTER_OTLP_${SIGNAL_ENV[signal]}_ENDPOINT`]?.trim();
103
+ if (specific)
104
+ return specific;
105
+ const generic = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
106
+ if (protocol === "grpc")
107
+ return generic || DEFAULT_GRPC_ENDPOINT;
108
+ const base = stripSignalPath(generic || DEFAULT_HTTP_ENDPOINT);
109
+ return `${base}/v1/${signal}`;
110
+ }
111
+ /** Whether any per-signal endpoint variable is set. */
112
+ function anyPerSignalEndpoint(env) {
113
+ return SIGNALS.some((s) => Boolean(env[`OTEL_EXPORTER_OTLP_${SIGNAL_ENV[s]}_ENDPOINT`]?.trim()));
114
+ }
115
+ function resolveSignal(env, signal) {
116
+ const tag = SIGNAL_ENV[signal];
117
+ const protocol = normalizeProtocol(env[`OTEL_EXPORTER_OTLP_${tag}_PROTOCOL`]) ??
118
+ normalizeProtocol(env.OTEL_EXPORTER_OTLP_PROTOCOL) ??
119
+ "grpc";
120
+ const exporterSetting = env[`OTEL_${tag}_EXPORTER`]?.trim().toLowerCase();
121
+ // Claude Code gates each signal on OTEL_<SIGNAL>_EXPORTER; absent means on
122
+ // here (the launcher already decided this session exports), "none" turns
123
+ // one signal off.
124
+ const switchedOn = exporterSetting === undefined || exporterSetting === "" ? true : exporterSetting !== "none";
125
+ // A per-signal-only env (e.g. just OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, no
126
+ // generic endpoint) means the user configured exactly those signals: the
127
+ // others must not fall back to the localhost defaults and spray
128
+ // ECONNREFUSED at a collector nobody runs.
129
+ const hasOwnEndpoint = Boolean(env[`OTEL_EXPORTER_OTLP_${tag}_ENDPOINT`]?.trim());
130
+ const hasGenericEndpoint = Boolean(env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim());
131
+ const orphaned = !hasOwnEndpoint && !hasGenericEndpoint && anyPerSignalEndpoint(env);
132
+ const enabled = switchedOn && !orphaned;
133
+ const headers = {
134
+ ...parseKvList(env.OTEL_EXPORTER_OTLP_HEADERS),
135
+ ...parseKvList(env[`OTEL_EXPORTER_OTLP_${tag}_HEADERS`]),
136
+ };
137
+ const intervalRaw = Number(signal === "metrics"
138
+ ? env.OTEL_METRIC_EXPORT_INTERVAL
139
+ : signal === "logs"
140
+ ? env.OTEL_LOGS_EXPORT_INTERVAL
141
+ : env.OTEL_TRACES_EXPORT_INTERVAL);
142
+ const exportIntervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : DEFAULT_INTERVALS_MS[signal];
143
+ return { enabled, protocol, url: resolveSignalUrl(env, signal, protocol), headers, exportIntervalMs };
144
+ }
145
+ function detectTerminal(env) {
146
+ if (env.TMUX)
147
+ return "tmux";
148
+ const tp = env.TERM_PROGRAM?.trim();
149
+ if (tp)
150
+ return tp;
151
+ return undefined;
152
+ }
153
+ /** Personal kill switch, beats every other source (both spellings accepted). */
154
+ export function telemetryDisabled(env) {
155
+ return envTrue(env.PI_OTEL_DISABLED) || envTrue(env.YAGNI_OTEL_DISABLED);
156
+ }
157
+ /** The launcher's "this session exports" gate. */
158
+ export const OTEL_EXPORT_ENV = "YAGNI_OTEL_EXPORT";
159
+ export function resolveTelemetryConfig(env = process.env) {
160
+ const enabled = envTrue(env[OTEL_EXPORT_ENV]) && !telemetryDisabled(env);
161
+ const temporality = env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE?.trim().toLowerCase();
162
+ const appVersion = env.YAGNI_CODE_VERSION?.trim() || undefined;
163
+ const organizationId = env.YAGNI_WORKSPACE_ID?.trim() || undefined;
164
+ const userEmail = env.YAGNI_USER_EMAIL?.trim() || undefined;
165
+ const terminalType = detectTerminal(env);
166
+ return {
167
+ enabled,
168
+ serviceName: env.OTEL_SERVICE_NAME?.trim() || DEFAULT_SERVICE_NAME,
169
+ ...(appVersion ? { serviceVersion: appVersion } : {}),
170
+ resourceAttributes: parseKvList(env.OTEL_RESOURCE_ATTRIBUTES),
171
+ traces: resolveSignal(env, "traces"),
172
+ metrics: resolveSignal(env, "metrics"),
173
+ logs: resolveSignal(env, "logs"),
174
+ metricsTemporality: temporality === "cumulative" ? "cumulative" : "delta",
175
+ includeSessionId: envBool(env.OTEL_METRICS_INCLUDE_SESSION_ID, true),
176
+ includeVersion: envBool(env.OTEL_METRICS_INCLUDE_VERSION, false),
177
+ includeAccountId: envBool(env.OTEL_METRICS_INCLUDE_ACCOUNT_UUID, true),
178
+ includeResourceAttributes: envBool(env.OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES, true),
179
+ includeEntrypoint: envBool(env.OTEL_METRICS_INCLUDE_ENTRYPOINT, false),
180
+ identity: {
181
+ ...(organizationId ? { organizationId } : {}),
182
+ ...(userEmail ? { userEmail } : {}),
183
+ ...(appVersion ? { appVersion } : {}),
184
+ ...(terminalType ? { terminalType } : {}),
185
+ entrypoint: env.YAGNI_GO_HEADLESS === "1" ? "sdk-cli" : "cli",
186
+ },
187
+ };
188
+ }
189
+ /** Signals this config will actually export (for doctor / status lines). */
190
+ export function enabledSignals(config) {
191
+ return SIGNALS.filter((s) => config[s].enabled);
192
+ }
193
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_SERVICE_NAME, OTEL_EXPORT_ENV, SIGNALS, enabledSignals, normalizeProtocol, parseKvList, resolveSignalUrl, resolveTelemetryConfig, stripSignalPath, telemetryDisabled, type OtlpProtocol, type Signal, type SignalConfig, type TelemetryConfig, type TelemetryIdentity, } from "./config.js";
2
+ export { PREFIX, type Decision, type DecisionSource } from "./attrs.js";
3
+ export { registerTelemetry, type RegisterTelemetryDeps, type TelemetryHandle } from "./register.js";
4
+ export { SessionTelemetry, countContentLines, countPatchLines, type ToolDecisionInput } from "./tracker.js";
5
+ export { createTelemetrySdk, type TelemetrySdk, type TelemetrySdkOptions } from "./sdk.js";
6
+ export { probeTelemetryExport, type SignalProbeResult, type TelemetryProbeReport } from "./probe.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_SERVICE_NAME, OTEL_EXPORT_ENV, SIGNALS, enabledSignals, normalizeProtocol, parseKvList, resolveSignalUrl, resolveTelemetryConfig, stripSignalPath, telemetryDisabled, } from "./config.js";
2
+ export { PREFIX } from "./attrs.js";
3
+ export { registerTelemetry } from "./register.js";
4
+ export { SessionTelemetry, countContentLines, countPatchLines } from "./tracker.js";
5
+ export { createTelemetrySdk } from "./sdk.js";
6
+ export { probeTelemetryExport } from "./probe.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Doctor probe: send one real record per enabled signal and report the
3
+ * exporter's verdict. This is what `yagni doctor` runs, so its "otel export"
4
+ * line means "the collector ACCEPTED a span/metric/log with these headers",
5
+ * not "port 443 answered" — a wrong API key (403), a wrong path (404), or a
6
+ * protocol mismatch all show up as the real status.
7
+ *
8
+ * The probe records are tiny and clearly named (`yagni_code.doctor.probe`)
9
+ * so a customer can find them in their backend as proof the pipe works.
10
+ */
11
+ import { type Signal, type TelemetryConfig } from "./config.js";
12
+ export interface SignalProbeResult {
13
+ signal: Signal;
14
+ url: string;
15
+ protocol: string;
16
+ ok: boolean;
17
+ /** One-line failure reason (status code, connection error) when !ok. */
18
+ error?: string;
19
+ }
20
+ export interface TelemetryProbeReport {
21
+ config: TelemetryConfig;
22
+ results: SignalProbeResult[];
23
+ }
24
+ /**
25
+ * Run the probe against a config resolved from `env` (the launcher passes the
26
+ * exact child env a session would get). Never throws.
27
+ */
28
+ export declare function probeTelemetryExport(env: NodeJS.ProcessEnv): Promise<TelemetryProbeReport>;
29
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Doctor probe: send one real record per enabled signal and report the
3
+ * exporter's verdict. This is what `yagni doctor` runs, so its "otel export"
4
+ * line means "the collector ACCEPTED a span/metric/log with these headers",
5
+ * not "port 443 answered" — a wrong API key (403), a wrong path (404), or a
6
+ * protocol mismatch all show up as the real status.
7
+ *
8
+ * The probe records are tiny and clearly named (`yagni_code.doctor.probe`)
9
+ * so a customer can find them in their backend as proof the pipe works.
10
+ */
11
+ import { metrics, trace } from "@opentelemetry/api";
12
+ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
13
+ import { PREFIX } from "./attrs.js";
14
+ import { enabledSignals, resolveTelemetryConfig } from "./config.js";
15
+ import { createTelemetrySdk, makeLogExporter, makeMetricExporter, makeTraceExporter, unrefTimer } from "./sdk.js";
16
+ const PROBE_TIMEOUT_MS = 8_000;
17
+ /**
18
+ * Wrap an exporter so its export callback's result is observable. Every
19
+ * other member (the metric exporter's temporality selector, for one) is
20
+ * forwarded by prototype so the wrapper stays a drop-in.
21
+ */
22
+ function observe(inner) {
23
+ let resolveResult = () => { };
24
+ const result = new Promise((resolve) => {
25
+ resolveResult = resolve;
26
+ });
27
+ const wrapped = Object.create(inner);
28
+ wrapped.export = (items, cb) => {
29
+ inner.export(items, (r) => {
30
+ resolveResult(r);
31
+ cb(r);
32
+ });
33
+ };
34
+ return { wrapped, result };
35
+ }
36
+ function describe(r) {
37
+ if (!r)
38
+ return "no export attempted (timed out)";
39
+ if (r.code === 0)
40
+ return undefined;
41
+ const e = r.error;
42
+ if (!e)
43
+ return "export failed";
44
+ // The OTLP http exporter's error carries the status code as `code` and the
45
+ // status text as the message ("Forbidden"); name the code so a reader can
46
+ // tell a bad key (401/403) from a bad path (404) at a glance.
47
+ const status = e.code;
48
+ const prefix = typeof status === "number" ? `HTTP ${status}: ` : "";
49
+ const msg = `${prefix}${e.message || String(e)}`;
50
+ return msg.length > 300 ? `${msg.slice(0, 300)}…` : msg;
51
+ }
52
+ function withTimeout(p, ms) {
53
+ return Promise.race([p, new Promise((resolve) => unrefTimer(setTimeout(resolve, ms)))]);
54
+ }
55
+ /**
56
+ * Run the probe against a config resolved from `env` (the launcher passes the
57
+ * exact child env a session would get). Never throws.
58
+ */
59
+ export async function probeTelemetryExport(env) {
60
+ const config = resolveTelemetryConfig({ ...env, YAGNI_OTEL_EXPORT: "1" });
61
+ const results = [];
62
+ const signals = enabledSignals(config);
63
+ if (signals.length === 0)
64
+ return { config, results };
65
+ const traceProbe = config.traces.enabled
66
+ ? observe(makeTraceExporter(config.traces))
67
+ : undefined;
68
+ const metricProbe = config.metrics.enabled
69
+ ? observe(makeMetricExporter(config.metrics, config.metricsTemporality))
70
+ : undefined;
71
+ const logProbe = config.logs.enabled
72
+ ? observe(makeLogExporter(config.logs))
73
+ : undefined;
74
+ const sdk = createTelemetrySdk(config, {
75
+ simpleProcessors: true,
76
+ exporters: {
77
+ ...(traceProbe ? { traces: traceProbe.wrapped } : {}),
78
+ ...(metricProbe ? { metrics: metricProbe.wrapped } : {}),
79
+ ...(logProbe ? { logs: logProbe.wrapped } : {}),
80
+ },
81
+ });
82
+ try {
83
+ if (traceProbe) {
84
+ const span = trace.getTracer(PREFIX).startSpan(`${PREFIX}.doctor.probe`, {
85
+ attributes: { [`${PREFIX}.probe`]: true },
86
+ });
87
+ span.end();
88
+ }
89
+ if (metricProbe) {
90
+ metrics.getMeter(PREFIX).createCounter(`${PREFIX}.doctor.probe`, { unit: "1" }).add(1);
91
+ }
92
+ if (logProbe) {
93
+ logs.getLogger(PREFIX).emit({
94
+ eventName: `${PREFIX}.doctor.probe`,
95
+ severityNumber: SeverityNumber.INFO,
96
+ body: `${PREFIX}.doctor.probe`,
97
+ attributes: { "event.name": "doctor.probe" },
98
+ });
99
+ }
100
+ await sdk.forceFlush();
101
+ const settle = async (signal, probe) => {
102
+ if (!probe)
103
+ return;
104
+ const r = await withTimeout(probe.result, PROBE_TIMEOUT_MS);
105
+ const error = describe(r);
106
+ results.push({
107
+ signal,
108
+ url: config[signal].url,
109
+ protocol: config[signal].protocol,
110
+ ok: error === undefined,
111
+ ...(error ? { error } : {}),
112
+ });
113
+ };
114
+ await Promise.all([settle("traces", traceProbe), settle("metrics", metricProbe), settle("logs", logProbe)]);
115
+ }
116
+ finally {
117
+ await sdk.shutdown();
118
+ }
119
+ results.sort((a, b) => signals.indexOf(a.signal) - signals.indexOf(b.signal));
120
+ return { config, results };
121
+ }
122
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Wire pi's lifecycle events into the session telemetry tracker.
3
+ *
4
+ * Registered from the extension entry on every launch; it is a no-op unless
5
+ * the launcher set `YAGNI_OTEL_EXPORT=1` (an OTLP destination is configured)
6
+ * and no kill switch is set. /go stage children and subagents load this same
7
+ * extension with the inherited env, so their spend is traced without any
8
+ * extra plumbing.
9
+ *
10
+ * The OTel SDK and exporter graph (a couple of hundred modules) is loaded
11
+ * LAZILY, at the first session_start of an exporting session, so the common
12
+ * case — no collector configured — never pays for it.
13
+ *
14
+ * Every handler is wrapped so a telemetry bug can never break a session:
15
+ * exceptions are swallowed, and the SDK degrades to "not exporting" with one
16
+ * notice.
17
+ */
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { type TelemetryConfig } from "./config.js";
20
+ import type { TelemetrySdk, TelemetrySdkOptions } from "./sdk.js";
21
+ import { SessionTelemetry, type ToolDecisionInput } from "./tracker.js";
22
+ export interface TelemetryHandle {
23
+ /** Whether this process is configured to export. */
24
+ readonly enabled: boolean;
25
+ readonly config: TelemetryConfig;
26
+ /** The gate calls this at every terminal tool_call outcome. */
27
+ toolDecision(input: ToolDecisionInput): void;
28
+ /** The mode holder calls this on every /mode (or shift-tab) change. */
29
+ permissionModeChanged(fromMode: string, toMode: string): void;
30
+ /**
31
+ * The entry calls this with the account email the /context boot fetch
32
+ * returned. A launcher-forwarded `YAGNI_USER_EMAIL` wins; otherwise every
33
+ * span, metric, and event from here on carries `user.email` (still subject
34
+ * to the `includeAccountId` gate).
35
+ */
36
+ setUserEmail(email: string | undefined): void;
37
+ /** Test/introspection seam: the live tracker once the SDK is up. */
38
+ readonly tracker: SessionTelemetry | null;
39
+ }
40
+ export interface RegisterTelemetryDeps {
41
+ env?: NodeJS.ProcessEnv;
42
+ /** Override SDK construction (tests inject in-memory exporters). */
43
+ createSdk?: (config: TelemetryConfig, opts: TelemetrySdkOptions) => TelemetrySdk;
44
+ now?: () => number;
45
+ }
46
+ export declare function registerTelemetry(pi: ExtensionAPI, deps?: RegisterTelemetryDeps): TelemetryHandle;
47
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Wire pi's lifecycle events into the session telemetry tracker.
3
+ *
4
+ * Registered from the extension entry on every launch; it is a no-op unless
5
+ * the launcher set `YAGNI_OTEL_EXPORT=1` (an OTLP destination is configured)
6
+ * and no kill switch is set. /go stage children and subagents load this same
7
+ * extension with the inherited env, so their spend is traced without any
8
+ * extra plumbing.
9
+ *
10
+ * The OTel SDK and exporter graph (a couple of hundred modules) is loaded
11
+ * LAZILY, at the first session_start of an exporting session, so the common
12
+ * case — no collector configured — never pays for it.
13
+ *
14
+ * Every handler is wrapped so a telemetry bug can never break a session:
15
+ * exceptions are swallowed, and the SDK degrades to "not exporting" with one
16
+ * notice.
17
+ */
18
+ import { basename } from "node:path";
19
+ import { metrics, trace } from "@opentelemetry/api";
20
+ import { logs } from "@opentelemetry/api-logs";
21
+ import { logEvent } from "../errorSink.js";
22
+ import { PREFIX } from "./attrs.js";
23
+ import { resolveTelemetryConfig } from "./config.js";
24
+ import { countContentLines, countPatchLines, byteLength, SessionTelemetry } from "./tracker.js";
25
+ const NOOP_HANDLE = (config) => ({
26
+ enabled: false,
27
+ config,
28
+ toolDecision: () => { },
29
+ permissionModeChanged: () => { },
30
+ setUserEmail: () => { },
31
+ tracker: null,
32
+ });
33
+ export function registerTelemetry(pi, deps = {}) {
34
+ const env = deps.env ?? process.env;
35
+ const config = resolveTelemetryConfig(env);
36
+ if (!config.enabled)
37
+ return NOOP_HANDLE(config);
38
+ if (!config.traces.enabled && !config.metrics.enabled && !config.logs.enabled)
39
+ return NOOP_HANDLE(config);
40
+ let ctx0;
41
+ let sdk = null;
42
+ let tracker = null;
43
+ /** Set once construction failed: never retry, never notify twice. */
44
+ let failed = false;
45
+ const notify = (message, severity) => {
46
+ try {
47
+ ctx0?.ui?.notify?.(message, severity);
48
+ }
49
+ catch {
50
+ // best-effort
51
+ }
52
+ };
53
+ // Telemetry must never break the session — but a swallowed error must
54
+ // leave a trail in the local error sink (~/.yagni-code error log), so a
55
+ // "why is nothing in Datadog" investigation has something to read.
56
+ const guard = (name, fn) => (...args) => {
57
+ try {
58
+ fn(...args);
59
+ }
60
+ catch (err) {
61
+ logEvent({
62
+ source: "telemetry",
63
+ level: "warn",
64
+ event: "handler_error",
65
+ fields: { handler: name, error: err instanceof Error ? `${err.name}: ${err.message}` : String(err) },
66
+ });
67
+ }
68
+ };
69
+ /** Load the SDK graph and build the tracker; idempotent, fail-soft. */
70
+ async function ensureSdk() {
71
+ if (sdk || failed)
72
+ return;
73
+ try {
74
+ const createSdk = deps.createSdk ?? (await import("./sdk.js")).createTelemetrySdk;
75
+ sdk = createSdk(config, {
76
+ onExportError: (message) => notify(`Telemetry export failed: ${message}. Run \`yagni doctor\` to test the endpoint.`, "warning"),
77
+ });
78
+ const version = config.serviceVersion ?? "0.0.0";
79
+ tracker = new SessionTelemetry({
80
+ tracer: trace.getTracer(PREFIX, version),
81
+ meter: metrics.getMeter(PREFIX, version),
82
+ logger: logs.getLogger(PREFIX, version),
83
+ config,
84
+ ...(deps.now ? { now: deps.now } : {}),
85
+ });
86
+ }
87
+ catch (err) {
88
+ failed = true;
89
+ sdk = null;
90
+ tracker = null;
91
+ notify(`Telemetry export could not start: ${err?.message ?? String(err)}`, "warning");
92
+ }
93
+ }
94
+ pi.on("session_start", async (_event, ctx) => {
95
+ ctx0 = ctx;
96
+ await ensureSdk();
97
+ if (!tracker)
98
+ return;
99
+ try {
100
+ let sessionId;
101
+ try {
102
+ const file = ctx.sessionManager?.getSessionFile?.();
103
+ if (file)
104
+ sessionId = basename(file, ".jsonl");
105
+ }
106
+ catch {
107
+ // ephemeral session
108
+ }
109
+ tracker.sessionStart(sessionId ?? env.YAGNI_SESSION_ID);
110
+ }
111
+ catch {
112
+ // never break session start
113
+ }
114
+ });
115
+ pi.on("before_agent_start", guard("before_agent_start", (event) => tracker?.interactionStart(event?.prompt)));
116
+ pi.on("turn_start", guard("turn_start", (event) => tracker?.turnStart(event?.turnIndex)));
117
+ pi.on("turn_end", guard("turn_end", () => tracker?.turnEnd()));
118
+ pi.on("before_provider_request", guard("before_provider_request", (event) => {
119
+ const payload = event?.payload;
120
+ const model = payload?.model ?? payload?.modelId;
121
+ tracker?.llmStart(typeof model === "string" ? model : undefined);
122
+ }));
123
+ pi.on("after_provider_response", guard("after_provider_response", (event) => {
124
+ tracker?.llmResponseHeaders(event?.status, event?.headers);
125
+ }));
126
+ pi.on("message_end", guard("message_end", (event) => {
127
+ const msg = event?.message;
128
+ if (!msg || msg.role !== "assistant")
129
+ return;
130
+ tracker?.assistantMessageEnd(msg);
131
+ }));
132
+ pi.on("tool_execution_start", guard("tool_execution_start", (event) => {
133
+ if (!event?.toolCallId || !event?.toolName)
134
+ return;
135
+ tracker?.toolStart(event.toolCallId, event.toolName, event.args);
136
+ }));
137
+ pi.on("tool_execution_end", guard("tool_execution_end", (event) => {
138
+ if (!event?.toolCallId)
139
+ return;
140
+ tracker?.toolEnd(event.toolCallId, { isError: !!event.isError, resultBytes: byteLength(event.result) });
141
+ }));
142
+ // tool_result carries the typed details (edit patch, bash command): the
143
+ // productivity counters (lines of code, commits, PRs) come from here.
144
+ pi.on("tool_result", guard("tool_result", (event) => {
145
+ if (!event || event.isError || !tracker)
146
+ return;
147
+ if (event.toolName === "edit") {
148
+ const patch = event.details?.patch;
149
+ const { added, removed } = countPatchLines(patch);
150
+ tracker.linesOfCode(added, removed);
151
+ }
152
+ else if (event.toolName === "write") {
153
+ tracker.linesOfCode(countContentLines(event.input?.content), 0);
154
+ }
155
+ else if (event.toolName === "bash") {
156
+ const command = event.input?.command;
157
+ tracker.bashSucceeded(typeof command === "string" ? command : undefined);
158
+ }
159
+ }));
160
+ pi.on("agent_end", guard("agent_end", () => tracker?.interactionEnd()));
161
+ // Quit: flush and shut down, bounded, so a blackholing collector cannot
162
+ // hold the exit. Session replacement (/new, /resume, /fork, reload): close
163
+ // the open spans and flush in the background — the process lives on and
164
+ // the same providers serve the next session, so nothing waits.
165
+ pi.on("session_shutdown", async (event) => {
166
+ try {
167
+ tracker?.close();
168
+ if (!sdk)
169
+ return;
170
+ if (event?.reason === "quit") {
171
+ await sdk.shutdown();
172
+ sdk = null;
173
+ tracker = null;
174
+ }
175
+ else {
176
+ void sdk.forceFlush().catch(() => { });
177
+ }
178
+ }
179
+ catch {
180
+ // never block exit
181
+ }
182
+ });
183
+ return {
184
+ enabled: true,
185
+ config,
186
+ get tracker() {
187
+ return tracker;
188
+ },
189
+ toolDecision: guard("tool_decision", (input) => tracker?.toolDecision(input)),
190
+ permissionModeChanged: guard("permission_mode_changed", (from, to) => tracker?.permissionModeChanged(from, to)),
191
+ // Precedence, lowest to highest: this call (the /context boot fetch) <
192
+ // the launcher's YAGNI_USER_EMAIL, which resolveTelemetryConfig already
193
+ // placed on `identity`. So an identity that is set is never overwritten,
194
+ // whichever writer set it; a new writer must slot into this order
195
+ // explicitly rather than rely on call timing. The tracker reads
196
+ // `config.identity` on every attribute set, so this applies whether the
197
+ // SDK is already up or still lazy-loading.
198
+ setUserEmail: (email) => {
199
+ if (config.identity.userEmail)
200
+ return;
201
+ const trimmed = typeof email === "string" ? email.trim() : "";
202
+ if (trimmed)
203
+ config.identity.userEmail = trimmed;
204
+ },
205
+ };
206
+ }
207
+ //# sourceMappingURL=register.js.map