@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.
- package/README.md +89 -27
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -3
- package/dist/claudePlugins.js +129 -19
- package/dist/cli.js +28 -4
- package/dist/doctor.d.ts +42 -4
- package/dist/doctor.js +150 -15
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +18 -5
- package/dist/extension/index.js +319 -72
- package/dist/extension/mcp/approval.d.ts +45 -0
- package/dist/extension/mcp/approval.js +164 -0
- package/dist/extension/mcp/auth.d.ts +124 -0
- package/dist/extension/mcp/auth.js +560 -0
- package/dist/extension/mcp/authStore.d.ts +61 -0
- package/dist/extension/mcp/authStore.js +105 -0
- package/dist/extension/mcp/callbackPage.d.ts +31 -0
- package/dist/extension/mcp/callbackPage.js +222 -0
- package/dist/extension/mcp/cliConfig.d.ts +12 -0
- package/dist/extension/mcp/cliConfig.js +12 -0
- package/dist/extension/mcp/config.d.ts +153 -0
- package/dist/extension/mcp/config.js +381 -0
- package/dist/extension/mcp/log.d.ts +28 -0
- package/dist/extension/mcp/log.js +82 -0
- package/dist/extension/mcp/manager.d.ts +100 -0
- package/dist/extension/mcp/manager.js +273 -0
- package/dist/extension/mcp/names.d.ts +25 -0
- package/dist/extension/mcp/names.js +40 -0
- package/dist/extension/mcp/panel.d.ts +33 -0
- package/dist/extension/mcp/panel.js +268 -0
- package/dist/extension/mcp/prompts.d.ts +23 -0
- package/dist/extension/mcp/prompts.js +93 -0
- package/dist/extension/mcp/startup.d.ts +55 -0
- package/dist/extension/mcp/startup.js +152 -0
- package/dist/extension/mcp/tools.d.ts +31 -0
- package/dist/extension/mcp/tools.js +117 -0
- package/dist/extension/mcp/transports.d.ts +17 -0
- package/dist/extension/mcp/transports.js +44 -0
- package/dist/extension/permission/gate.d.ts +104 -2
- package/dist/extension/permission/gate.js +387 -31
- package/dist/extension/permission/guardian.d.ts +24 -5
- package/dist/extension/permission/guardian.js +162 -24
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/personas.js +5 -0
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +122 -0
- package/dist/mcpCommand.js +787 -0
- package/dist/otel.d.ts +69 -63
- package/dist/otel.js +154 -119
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/dist/upgrade.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +20 -3
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
|
@@ -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,40 @@
|
|
|
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
|
+
/** Test/introspection seam: the live tracker once the SDK is up. */
|
|
31
|
+
readonly tracker: SessionTelemetry | null;
|
|
32
|
+
}
|
|
33
|
+
export interface RegisterTelemetryDeps {
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
/** Override SDK construction (tests inject in-memory exporters). */
|
|
36
|
+
createSdk?: (config: TelemetryConfig, opts: TelemetrySdkOptions) => TelemetrySdk;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
export declare function registerTelemetry(pi: ExtensionAPI, deps?: RegisterTelemetryDeps): TelemetryHandle;
|
|
40
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
tracker: null,
|
|
31
|
+
});
|
|
32
|
+
export function registerTelemetry(pi, deps = {}) {
|
|
33
|
+
const env = deps.env ?? process.env;
|
|
34
|
+
const config = resolveTelemetryConfig(env);
|
|
35
|
+
if (!config.enabled)
|
|
36
|
+
return NOOP_HANDLE(config);
|
|
37
|
+
if (!config.traces.enabled && !config.metrics.enabled && !config.logs.enabled)
|
|
38
|
+
return NOOP_HANDLE(config);
|
|
39
|
+
let ctx0;
|
|
40
|
+
let sdk = null;
|
|
41
|
+
let tracker = null;
|
|
42
|
+
/** Set once construction failed: never retry, never notify twice. */
|
|
43
|
+
let failed = false;
|
|
44
|
+
const notify = (message, severity) => {
|
|
45
|
+
try {
|
|
46
|
+
ctx0?.ui?.notify?.(message, severity);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// best-effort
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Telemetry must never break the session — but a swallowed error must
|
|
53
|
+
// leave a trail in the local error sink (~/.yagni-code error log), so a
|
|
54
|
+
// "why is nothing in Datadog" investigation has something to read.
|
|
55
|
+
const guard = (name, fn) => (...args) => {
|
|
56
|
+
try {
|
|
57
|
+
fn(...args);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logEvent({
|
|
61
|
+
source: "telemetry",
|
|
62
|
+
level: "warn",
|
|
63
|
+
event: "handler_error",
|
|
64
|
+
fields: { handler: name, error: err instanceof Error ? `${err.name}: ${err.message}` : String(err) },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Load the SDK graph and build the tracker; idempotent, fail-soft. */
|
|
69
|
+
async function ensureSdk() {
|
|
70
|
+
if (sdk || failed)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const createSdk = deps.createSdk ?? (await import("./sdk.js")).createTelemetrySdk;
|
|
74
|
+
sdk = createSdk(config, {
|
|
75
|
+
onExportError: (message) => notify(`Telemetry export failed: ${message}. Run \`yagni doctor\` to test the endpoint.`, "warning"),
|
|
76
|
+
});
|
|
77
|
+
const version = config.serviceVersion ?? "0.0.0";
|
|
78
|
+
tracker = new SessionTelemetry({
|
|
79
|
+
tracer: trace.getTracer(PREFIX, version),
|
|
80
|
+
meter: metrics.getMeter(PREFIX, version),
|
|
81
|
+
logger: logs.getLogger(PREFIX, version),
|
|
82
|
+
config,
|
|
83
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
failed = true;
|
|
88
|
+
sdk = null;
|
|
89
|
+
tracker = null;
|
|
90
|
+
notify(`Telemetry export could not start: ${err?.message ?? String(err)}`, "warning");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
94
|
+
ctx0 = ctx;
|
|
95
|
+
await ensureSdk();
|
|
96
|
+
if (!tracker)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
let sessionId;
|
|
100
|
+
try {
|
|
101
|
+
const file = ctx.sessionManager?.getSessionFile?.();
|
|
102
|
+
if (file)
|
|
103
|
+
sessionId = basename(file, ".jsonl");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ephemeral session
|
|
107
|
+
}
|
|
108
|
+
tracker.sessionStart(sessionId ?? env.YAGNI_SESSION_ID);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// never break session start
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
pi.on("before_agent_start", guard("before_agent_start", (event) => tracker?.interactionStart(event?.prompt)));
|
|
115
|
+
pi.on("turn_start", guard("turn_start", (event) => tracker?.turnStart(event?.turnIndex)));
|
|
116
|
+
pi.on("turn_end", guard("turn_end", () => tracker?.turnEnd()));
|
|
117
|
+
pi.on("before_provider_request", guard("before_provider_request", (event) => {
|
|
118
|
+
const payload = event?.payload;
|
|
119
|
+
const model = payload?.model ?? payload?.modelId;
|
|
120
|
+
tracker?.llmStart(typeof model === "string" ? model : undefined);
|
|
121
|
+
}));
|
|
122
|
+
pi.on("after_provider_response", guard("after_provider_response", (event) => {
|
|
123
|
+
tracker?.llmResponseHeaders(event?.status, event?.headers);
|
|
124
|
+
}));
|
|
125
|
+
pi.on("message_end", guard("message_end", (event) => {
|
|
126
|
+
const msg = event?.message;
|
|
127
|
+
if (!msg || msg.role !== "assistant")
|
|
128
|
+
return;
|
|
129
|
+
tracker?.assistantMessageEnd(msg);
|
|
130
|
+
}));
|
|
131
|
+
pi.on("tool_execution_start", guard("tool_execution_start", (event) => {
|
|
132
|
+
if (!event?.toolCallId || !event?.toolName)
|
|
133
|
+
return;
|
|
134
|
+
tracker?.toolStart(event.toolCallId, event.toolName, event.args);
|
|
135
|
+
}));
|
|
136
|
+
pi.on("tool_execution_end", guard("tool_execution_end", (event) => {
|
|
137
|
+
if (!event?.toolCallId)
|
|
138
|
+
return;
|
|
139
|
+
tracker?.toolEnd(event.toolCallId, { isError: !!event.isError, resultBytes: byteLength(event.result) });
|
|
140
|
+
}));
|
|
141
|
+
// tool_result carries the typed details (edit patch, bash command): the
|
|
142
|
+
// productivity counters (lines of code, commits, PRs) come from here.
|
|
143
|
+
pi.on("tool_result", guard("tool_result", (event) => {
|
|
144
|
+
if (!event || event.isError || !tracker)
|
|
145
|
+
return;
|
|
146
|
+
if (event.toolName === "edit") {
|
|
147
|
+
const patch = event.details?.patch;
|
|
148
|
+
const { added, removed } = countPatchLines(patch);
|
|
149
|
+
tracker.linesOfCode(added, removed);
|
|
150
|
+
}
|
|
151
|
+
else if (event.toolName === "write") {
|
|
152
|
+
tracker.linesOfCode(countContentLines(event.input?.content), 0);
|
|
153
|
+
}
|
|
154
|
+
else if (event.toolName === "bash") {
|
|
155
|
+
const command = event.input?.command;
|
|
156
|
+
tracker.bashSucceeded(typeof command === "string" ? command : undefined);
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
pi.on("agent_end", guard("agent_end", () => tracker?.interactionEnd()));
|
|
160
|
+
// Quit: flush and shut down, bounded, so a blackholing collector cannot
|
|
161
|
+
// hold the exit. Session replacement (/new, /resume, /fork, reload): close
|
|
162
|
+
// the open spans and flush in the background — the process lives on and
|
|
163
|
+
// the same providers serve the next session, so nothing waits.
|
|
164
|
+
pi.on("session_shutdown", async (event) => {
|
|
165
|
+
try {
|
|
166
|
+
tracker?.close();
|
|
167
|
+
if (!sdk)
|
|
168
|
+
return;
|
|
169
|
+
if (event?.reason === "quit") {
|
|
170
|
+
await sdk.shutdown();
|
|
171
|
+
sdk = null;
|
|
172
|
+
tracker = null;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
void sdk.forceFlush().catch(() => { });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// never block exit
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
enabled: true,
|
|
184
|
+
config,
|
|
185
|
+
get tracker() {
|
|
186
|
+
return tracker;
|
|
187
|
+
},
|
|
188
|
+
toolDecision: guard("tool_decision", (input) => tracker?.toolDecision(input)),
|
|
189
|
+
permissionModeChanged: guard("permission_mode_changed", (from, to) => tracker?.permissionModeChanged(from, to)),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry SDK bootstrap for one YAGNI Code process: a tracer, meter,
|
|
3
|
+
* and logger provider, each with the OTLP exporter the config picked
|
|
4
|
+
* (gRPC, http/protobuf, or http/json), plus a diag hook that turns export
|
|
5
|
+
* failures into ONE in-session notice instead of silence.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately built from the signal SDKs (`sdk-trace`, `sdk-metrics`,
|
|
8
|
+
* `sdk-logs`) rather than `sdk-node`: no auto-instrumentation, no Prometheus
|
|
9
|
+
* server, no propagators — this is a client-only process that pushes its own
|
|
10
|
+
* spans. Providers are process-global (the OTel API allows one of each), so
|
|
11
|
+
* the extension initializes once per process even though pi fires
|
|
12
|
+
* session_start again on /new, /resume, and /fork.
|
|
13
|
+
*
|
|
14
|
+
* Everything is fail-soft. Telemetry must never block a launch or a prompt:
|
|
15
|
+
* a bad exporter config degrades to "not exporting", reported through the
|
|
16
|
+
* status callback, never a throw into pi.
|
|
17
|
+
*/
|
|
18
|
+
import { type Resource } from "@opentelemetry/resources";
|
|
19
|
+
import { LoggerProvider, type LogRecordExporter } from "@opentelemetry/sdk-logs";
|
|
20
|
+
import { MeterProvider, type PushMetricExporter } from "@opentelemetry/sdk-metrics";
|
|
21
|
+
import { TracerProvider, type SpanExporter } from "@opentelemetry/sdk-trace";
|
|
22
|
+
import type { SignalConfig, TelemetryConfig } from "./config.js";
|
|
23
|
+
export interface TelemetryExporters {
|
|
24
|
+
traces?: SpanExporter;
|
|
25
|
+
metrics?: PushMetricExporter;
|
|
26
|
+
logs?: LogRecordExporter;
|
|
27
|
+
}
|
|
28
|
+
export interface TelemetrySdkOptions {
|
|
29
|
+
/** Injected exporters (tests use the SDKs' in-memory ones). */
|
|
30
|
+
exporters?: TelemetryExporters;
|
|
31
|
+
/** Simple (synchronous) processors instead of batching — tests + the doctor probe. */
|
|
32
|
+
simpleProcessors?: boolean;
|
|
33
|
+
/** Called on the first export failure with a one-line, human-readable reason. */
|
|
34
|
+
onExportError?: (message: string) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface TelemetrySdk {
|
|
37
|
+
tracerProvider: TracerProvider;
|
|
38
|
+
meterProvider: MeterProvider;
|
|
39
|
+
loggerProvider: LoggerProvider;
|
|
40
|
+
resource: Resource;
|
|
41
|
+
/** Flush every processor (bounded), then shut the providers down. */
|
|
42
|
+
shutdown(): Promise<void>;
|
|
43
|
+
/** Flush without shutting down (used between /go stages). */
|
|
44
|
+
forceFlush(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** Build the trace exporter for a signal config. */
|
|
47
|
+
export declare function makeTraceExporter(cfg: SignalConfig): SpanExporter;
|
|
48
|
+
export declare function makeMetricExporter(cfg: SignalConfig, temporality: TelemetryConfig["metricsTemporality"]): PushMetricExporter;
|
|
49
|
+
export declare function makeLogExporter(cfg: SignalConfig): LogRecordExporter;
|
|
50
|
+
/**
|
|
51
|
+
* Create and register the providers. Returns null (and reports) when the
|
|
52
|
+
* config produced no exporter — never throws.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createTelemetrySdk(config: TelemetryConfig, opts?: TelemetrySdkOptions): TelemetrySdk;
|
|
55
|
+
/**
|
|
56
|
+
* Shutdown/flush bound. A blackholing collector (a corp collector with the
|
|
57
|
+
* VPN down) must not hold quit for long: the batch processors already flush
|
|
58
|
+
* every few seconds, so at most one interval of records is at risk.
|
|
59
|
+
*/
|
|
60
|
+
export declare const FLUSH_TIMEOUT_MS = 1500;
|
|
61
|
+
/** Let a timeout never keep the process alive on its own (Node timers only). */
|
|
62
|
+
export declare function unrefTimer(timer: unknown): void;
|
|
63
|
+
//# sourceMappingURL=sdk.d.ts.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry SDK bootstrap for one YAGNI Code process: a tracer, meter,
|
|
3
|
+
* and logger provider, each with the OTLP exporter the config picked
|
|
4
|
+
* (gRPC, http/protobuf, or http/json), plus a diag hook that turns export
|
|
5
|
+
* failures into ONE in-session notice instead of silence.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately built from the signal SDKs (`sdk-trace`, `sdk-metrics`,
|
|
8
|
+
* `sdk-logs`) rather than `sdk-node`: no auto-instrumentation, no Prometheus
|
|
9
|
+
* server, no propagators — this is a client-only process that pushes its own
|
|
10
|
+
* spans. Providers are process-global (the OTel API allows one of each), so
|
|
11
|
+
* the extension initializes once per process even though pi fires
|
|
12
|
+
* session_start again on /new, /resume, and /fork.
|
|
13
|
+
*
|
|
14
|
+
* Everything is fail-soft. Telemetry must never block a launch or a prompt:
|
|
15
|
+
* a bad exporter config degrades to "not exporting", reported through the
|
|
16
|
+
* status callback, never a throw into pi.
|
|
17
|
+
*/
|
|
18
|
+
import { Metadata } from "@grpc/grpc-js";
|
|
19
|
+
import { diag, DiagLogLevel, metrics, trace } from "@opentelemetry/api";
|
|
20
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
21
|
+
import { OTLPLogExporter as LogGrpcExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
|
|
22
|
+
import { OTLPLogExporter as LogHttpExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
23
|
+
import { OTLPLogExporter as LogProtoExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
24
|
+
import { OTLPMetricExporter as MetricGrpcExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
25
|
+
import { AggregationTemporalityPreference, OTLPMetricExporter as MetricHttpExporter, } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
26
|
+
import { OTLPMetricExporter as MetricProtoExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
27
|
+
import { OTLPTraceExporter as TraceGrpcExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
28
|
+
import { OTLPTraceExporter as TraceHttpExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
29
|
+
import { OTLPTraceExporter as TraceProtoExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
30
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
31
|
+
import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor, } from "@opentelemetry/sdk-logs";
|
|
32
|
+
import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
|
|
33
|
+
import { BatchSpanProcessor, SimpleSpanProcessor, TracerProvider, } from "@opentelemetry/sdk-trace";
|
|
34
|
+
import { logEvent } from "../errorSink.js";
|
|
35
|
+
/**
|
|
36
|
+
* gRPC exporters take headers as grpc `Metadata`, not a record. Built from the
|
|
37
|
+
* resolved config rather than left to the exporter's own `process.env` read,
|
|
38
|
+
* so the doctor probe (which runs in-process with a synthetic env) and a
|
|
39
|
+
* session see the same headers.
|
|
40
|
+
*/
|
|
41
|
+
function toMetadata(headers) {
|
|
42
|
+
const md = new Metadata();
|
|
43
|
+
for (const [k, v] of Object.entries(headers))
|
|
44
|
+
md.set(k, v);
|
|
45
|
+
return md;
|
|
46
|
+
}
|
|
47
|
+
/** Build the trace exporter for a signal config. */
|
|
48
|
+
export function makeTraceExporter(cfg) {
|
|
49
|
+
return pick(cfg.protocol, {
|
|
50
|
+
grpc: () => new TraceGrpcExporter({ url: cfg.url, metadata: toMetadata(cfg.headers) }),
|
|
51
|
+
proto: () => new TraceProtoExporter({ url: cfg.url, headers: cfg.headers }),
|
|
52
|
+
json: () => new TraceHttpExporter({ url: cfg.url, headers: cfg.headers }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function makeMetricExporter(cfg, temporality) {
|
|
56
|
+
const temporalityPreference = temporality === "cumulative" ? AggregationTemporalityPreference.CUMULATIVE : AggregationTemporalityPreference.DELTA;
|
|
57
|
+
return pick(cfg.protocol, {
|
|
58
|
+
grpc: () => new MetricGrpcExporter({ url: cfg.url, metadata: toMetadata(cfg.headers), temporalityPreference }),
|
|
59
|
+
proto: () => new MetricProtoExporter({ url: cfg.url, headers: cfg.headers, temporalityPreference }),
|
|
60
|
+
json: () => new MetricHttpExporter({ url: cfg.url, headers: cfg.headers, temporalityPreference }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function makeLogExporter(cfg) {
|
|
64
|
+
return pick(cfg.protocol, {
|
|
65
|
+
grpc: () => new LogGrpcExporter({ url: cfg.url, metadata: toMetadata(cfg.headers) }),
|
|
66
|
+
proto: () => new LogProtoExporter({ url: cfg.url, headers: cfg.headers }),
|
|
67
|
+
json: () => new LogHttpExporter({ url: cfg.url, headers: cfg.headers }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function pick(protocol, ctors) {
|
|
71
|
+
if (protocol === "http/protobuf")
|
|
72
|
+
return ctors.proto();
|
|
73
|
+
if (protocol === "http/json")
|
|
74
|
+
return ctors.json();
|
|
75
|
+
return ctors.grpc();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A diag logger for exporter failures. OTel routes them — a 403 from a bad
|
|
79
|
+
* API key, ECONNREFUSED, a 404 on the wrong path — through `diag.error` via
|
|
80
|
+
* the global error handler, so this is the only place they surface. The
|
|
81
|
+
* FIRST one is forwarded to the session as a notice (one, never a stream);
|
|
82
|
+
* EVERY one is written to the local error sink (~/.yagni-code error log),
|
|
83
|
+
* so a later "why is nothing in Datadog" investigation has the full trail.
|
|
84
|
+
*/
|
|
85
|
+
function makeDiagLogger(onExportError) {
|
|
86
|
+
let reported = false;
|
|
87
|
+
const noop = () => { };
|
|
88
|
+
return {
|
|
89
|
+
verbose: noop,
|
|
90
|
+
debug: noop,
|
|
91
|
+
info: noop,
|
|
92
|
+
warn: noop,
|
|
93
|
+
error: (message, ...args) => {
|
|
94
|
+
const text = [message, ...args]
|
|
95
|
+
.map((a) => (a instanceof Error ? a.message : typeof a === "string" ? a : safeJson(a)))
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(" ")
|
|
98
|
+
.slice(0, 500);
|
|
99
|
+
logEvent({ source: "telemetry", level: "warn", event: "export_error", fields: { error: text } });
|
|
100
|
+
if (reported || !onExportError)
|
|
101
|
+
return;
|
|
102
|
+
reported = true;
|
|
103
|
+
try {
|
|
104
|
+
onExportError(text);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A failing notice must not take the session down.
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function safeJson(v) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.stringify(v) ?? "";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return String(v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function buildResource(config) {
|
|
121
|
+
return resourceFromAttributes({
|
|
122
|
+
...config.resourceAttributes,
|
|
123
|
+
"service.name": config.serviceName,
|
|
124
|
+
...(config.serviceVersion ? { "service.version": config.serviceVersion } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create and register the providers. Returns null (and reports) when the
|
|
129
|
+
* config produced no exporter — never throws.
|
|
130
|
+
*/
|
|
131
|
+
export function createTelemetrySdk(config, opts = {}) {
|
|
132
|
+
diag.setLogger(makeDiagLogger(opts.onExportError), { logLevel: DiagLogLevel.ERROR, suppressOverrideMessage: true });
|
|
133
|
+
const resource = buildResource(config);
|
|
134
|
+
const traceExporter = config.traces.enabled ? (opts.exporters?.traces ?? makeTraceExporter(config.traces)) : undefined;
|
|
135
|
+
const metricExporter = config.metrics.enabled
|
|
136
|
+
? (opts.exporters?.metrics ?? makeMetricExporter(config.metrics, config.metricsTemporality))
|
|
137
|
+
: undefined;
|
|
138
|
+
const logExporter = config.logs.enabled ? (opts.exporters?.logs ?? makeLogExporter(config.logs)) : undefined;
|
|
139
|
+
const tracerProvider = new TracerProvider({
|
|
140
|
+
resource,
|
|
141
|
+
spanProcessors: traceExporter
|
|
142
|
+
? [
|
|
143
|
+
opts.simpleProcessors
|
|
144
|
+
? new SimpleSpanProcessor({ exporter: traceExporter })
|
|
145
|
+
: new BatchSpanProcessor({ exporter: traceExporter, scheduledDelayMillis: config.traces.exportIntervalMs }),
|
|
146
|
+
]
|
|
147
|
+
: [],
|
|
148
|
+
});
|
|
149
|
+
const meterProvider = new MeterProvider({
|
|
150
|
+
resource,
|
|
151
|
+
readers: metricExporter
|
|
152
|
+
? [
|
|
153
|
+
new PeriodicExportingMetricReader({
|
|
154
|
+
exporter: metricExporter,
|
|
155
|
+
exportIntervalMillis: config.metrics.exportIntervalMs,
|
|
156
|
+
}),
|
|
157
|
+
]
|
|
158
|
+
: [],
|
|
159
|
+
});
|
|
160
|
+
const loggerProvider = new LoggerProvider({
|
|
161
|
+
resource,
|
|
162
|
+
processors: logExporter
|
|
163
|
+
? [
|
|
164
|
+
opts.simpleProcessors
|
|
165
|
+
? new SimpleLogRecordProcessor({ exporter: logExporter })
|
|
166
|
+
: new BatchLogRecordProcessor({ exporter: logExporter, scheduledDelayMillis: config.logs.exportIntervalMs }),
|
|
167
|
+
]
|
|
168
|
+
: [],
|
|
169
|
+
});
|
|
170
|
+
// Register globally so any OTel-aware code in the process (an MCP client,
|
|
171
|
+
// a future hook) parents into the same providers. The API refuses a second
|
|
172
|
+
// registration and returns false; that is fine — a re-init keeps the first.
|
|
173
|
+
trace.setGlobalTracerProvider(tracerProvider);
|
|
174
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
175
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
176
|
+
const bounded = (p, ms) => Promise.race([p, new Promise((resolve) => unrefTimer(setTimeout(resolve, ms)))]);
|
|
177
|
+
return {
|
|
178
|
+
tracerProvider,
|
|
179
|
+
meterProvider,
|
|
180
|
+
loggerProvider,
|
|
181
|
+
resource,
|
|
182
|
+
async forceFlush() {
|
|
183
|
+
await bounded(Promise.allSettled([tracerProvider.forceFlush(), meterProvider.forceFlush(), loggerProvider.forceFlush()]), FLUSH_TIMEOUT_MS);
|
|
184
|
+
},
|
|
185
|
+
async shutdown() {
|
|
186
|
+
// Unregister the globals FIRST, synchronously: a session that replaces
|
|
187
|
+
// this one (/new, /resume) can then register its own providers without
|
|
188
|
+
// waiting on a collector that may be blackholing the final flush.
|
|
189
|
+
trace.disable();
|
|
190
|
+
metrics.disable();
|
|
191
|
+
logs.disable();
|
|
192
|
+
diag.disable();
|
|
193
|
+
await bounded(Promise.allSettled([tracerProvider.shutdown(), meterProvider.shutdown(), loggerProvider.shutdown()]), FLUSH_TIMEOUT_MS);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Shutdown/flush bound. A blackholing collector (a corp collector with the
|
|
199
|
+
* VPN down) must not hold quit for long: the batch processors already flush
|
|
200
|
+
* every few seconds, so at most one interval of records is at risk.
|
|
201
|
+
*/
|
|
202
|
+
export const FLUSH_TIMEOUT_MS = 1_500;
|
|
203
|
+
/** Let a timeout never keep the process alive on its own (Node timers only). */
|
|
204
|
+
export function unrefTimer(timer) {
|
|
205
|
+
timer?.unref?.();
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=sdk.js.map
|