aui-agent-builder 0.3.108 → 0.3.110
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/dist/api-client/index.d.ts +27 -8
- package/dist/api-client/index.d.ts.map +1 -1
- package/dist/api-client/index.js +247 -109
- package/dist/api-client/index.js.map +1 -1
- package/dist/api-client/kb-view-client.d.ts +12 -6
- package/dist/api-client/kb-view-client.d.ts.map +1 -1
- package/dist/api-client/kb-view-client.js +38 -23
- package/dist/api-client/kb-view-client.js.map +1 -1
- package/dist/commands/import-agent.d.ts.map +1 -1
- package/dist/commands/import-agent.js +30 -6
- package/dist/commands/import-agent.js.map +1 -1
- package/dist/commands/legacy/push-records-mode.d.ts.map +1 -1
- package/dist/commands/legacy/push-records-mode.js +0 -1
- package/dist/commands/legacy/push-records-mode.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +29 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/pull-agent.d.ts.map +1 -1
- package/dist/commands/pull-agent.js +6 -2
- package/dist/commands/pull-agent.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +6 -3
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/util/agent-mode.d.ts.map +1 -1
- package/dist/commands/util/agent-mode.js +69 -11
- package/dist/commands/util/agent-mode.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +6 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/index.d.ts +56 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.js +100 -4
- package/dist/index.js.map +1 -1
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +21 -0
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/kb-view.service.d.ts +2 -2
- package/dist/services/kb-view.service.d.ts.map +1 -1
- package/dist/services/kb-view.service.js +6 -7
- package/dist/services/kb-view.service.js.map +1 -1
- package/dist/telemetry.d.ts +130 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +808 -2
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
package/dist/telemetry.js
CHANGED
|
@@ -17,12 +17,59 @@ import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
|
17
17
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
18
18
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
19
19
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
20
|
-
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
20
|
+
import { trace, context, ROOT_CONTEXT, SpanStatusCode, TraceFlags, } from "@opentelemetry/api";
|
|
21
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import * as path from "node:path";
|
|
21
25
|
// TODO: Replace with your actual Logfire write token
|
|
22
26
|
const LOGFIRE_TOKEN = "pylf_v1_us_1wcYn9Jc6hVmFwPxwTgLnQcktc0HjHq2D0GLjFvp197C";
|
|
23
27
|
const LOGFIRE_OTLP_URL = "https://logfire-api.pydantic.dev/v1/traces";
|
|
24
28
|
let sdk = null;
|
|
25
29
|
let cliVersion = "unknown";
|
|
30
|
+
// ─── Per-process telemetry identifiers ────────────────────────────────
|
|
31
|
+
//
|
|
32
|
+
// Two stable correlators are stamped on every span via `setUserContext`:
|
|
33
|
+
//
|
|
34
|
+
// - `cli.session_id` (LONG-LIVED) — minted on every successful
|
|
35
|
+
// `aui login` (see `buildSession` in `services/auth.service.ts`) and
|
|
36
|
+
// persisted as `cli_session_id` in `~/.aui/session.json`. All CLI
|
|
37
|
+
// commands run between two logins share it, so Logfire can be
|
|
38
|
+
// filtered to "everything this user did in this login" with one
|
|
39
|
+
// attribute. Cleared by `aui logout` (the whole session file gets
|
|
40
|
+
// unlinked). Callers (notably `agent-builder-bff` shelling out to
|
|
41
|
+
// `aui` inside E2B sandboxes) can override via `AUI_CLI_SESSION_ID`
|
|
42
|
+
// to glue multiple invocations to their own session/request id.
|
|
43
|
+
//
|
|
44
|
+
// - `cli.run_id` (PER-INVOCATION) — a fresh UUID for this process.
|
|
45
|
+
// Distinguishes individual `aui …` runs inside one login session.
|
|
46
|
+
// Also overridable via `AUI_CLI_RUN_ID` so the BFF can correlate
|
|
47
|
+
// a single sandbox command with its outer trace.
|
|
48
|
+
//
|
|
49
|
+
// Plus two pieces of run context everyone wants on every span:
|
|
50
|
+
//
|
|
51
|
+
// - `cli.command` — the resolved sub-command (e.g. "push", "pull",
|
|
52
|
+
// "import-agent"). Best-effort parse of `process.argv`.
|
|
53
|
+
// - `cli.invoked_at` — ISO timestamp at process start. Handy for
|
|
54
|
+
// "this same command ran 4 times in 10 minutes" hot-spot triage.
|
|
55
|
+
//
|
|
56
|
+
// All four are read once at module load (cheap, deterministic) and
|
|
57
|
+
// re-attached on every `setUserContext` call.
|
|
58
|
+
const cliRunId = process.env.AUI_CLI_RUN_ID || randomUUID();
|
|
59
|
+
const cliInvokedAt = new Date().toISOString();
|
|
60
|
+
/** Best-effort: extract the active sub-command from argv. Commander
|
|
61
|
+
* parses lazily, so we mirror its convention — the first non-flag
|
|
62
|
+
* positional after argv[1] (the entry script) is the command name.
|
|
63
|
+
* Returns "(no-command)" when the CLI was invoked bare (just `aui`). */
|
|
64
|
+
function resolveCliCommand() {
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
for (const arg of args) {
|
|
67
|
+
if (!arg.startsWith("-"))
|
|
68
|
+
return arg;
|
|
69
|
+
}
|
|
70
|
+
return "(no-command)";
|
|
71
|
+
}
|
|
72
|
+
const cliCommand = resolveCliCommand();
|
|
26
73
|
/**
|
|
27
74
|
* Returns `false` if any documented opt-out env var is set, otherwise
|
|
28
75
|
* `true`. Exported so tests can pin every documented opt-out path with a
|
|
@@ -53,6 +100,11 @@ export function initTelemetry(version) {
|
|
|
53
100
|
});
|
|
54
101
|
sdk = new NodeSDK({
|
|
55
102
|
traceExporter: exporter,
|
|
103
|
+
// Pin the `trace_id` of every new trace in this process to the
|
|
104
|
+
// session-derived value so multiple `aui …` invocations under
|
|
105
|
+
// the same login share one Logfire trace. Falls back to random
|
|
106
|
+
// ids when no session is loadable (pre-login / logged-out).
|
|
107
|
+
idGenerator: sessionAwareIdGenerator,
|
|
56
108
|
resource: resourceFromAttributes({
|
|
57
109
|
[ATTR_SERVICE_NAME]: "aui-cli",
|
|
58
110
|
[ATTR_SERVICE_VERSION]: version,
|
|
@@ -77,9 +129,698 @@ export async function shutdownTelemetry() {
|
|
|
77
129
|
export function getTracer() {
|
|
78
130
|
return trace.getTracer("aui-cli");
|
|
79
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the long-lived CLI telemetry session id.
|
|
134
|
+
*
|
|
135
|
+
* Order of precedence:
|
|
136
|
+
* 1. `AUI_CLI_SESSION_ID` env var — lets `agent-builder-bff` (or any
|
|
137
|
+
* harness) pass in its own session/request id so a single BFF
|
|
138
|
+
* session correlates across every `aui` invocation in the sandbox.
|
|
139
|
+
* 2. `cli_session_id` field in `~/.aui/session.json` — minted on
|
|
140
|
+
* every `aui login`. Persists across CLI invocations between two
|
|
141
|
+
* logins.
|
|
142
|
+
* 3. `undefined` — no session is loadable (unauthenticated user / no
|
|
143
|
+
* session file). Caller skips the attribute rather than emitting
|
|
144
|
+
* a confusing fake value.
|
|
145
|
+
*/
|
|
146
|
+
export function getCliSessionId() {
|
|
147
|
+
if (process.env.AUI_CLI_SESSION_ID)
|
|
148
|
+
return process.env.AUI_CLI_SESSION_ID;
|
|
149
|
+
// Read `~/.aui/session.json` synchronously and pluck `cli_session_id`.
|
|
150
|
+
// We deliberately do NOT import `loadSessionRaw` from `./config/index.js`
|
|
151
|
+
// here:
|
|
152
|
+
// - This file is loaded at process boot by `src/index.ts` BEFORE any
|
|
153
|
+
// command imports `config/`. A top-level ESM import would create
|
|
154
|
+
// an evaluation cycle.
|
|
155
|
+
// - Preflight telemetry in `api-client/index.ts` calls this on a hot
|
|
156
|
+
// path; going through a dynamic `import()` would force every API
|
|
157
|
+
// call into an extra microtask just to read a small JSON file.
|
|
158
|
+
// The session file format (just look at `cli_session_id`) is stable;
|
|
159
|
+
// a one-line `fs.readFileSync` keeps this hot path zero-allocation
|
|
160
|
+
// on the no-session branch.
|
|
161
|
+
try {
|
|
162
|
+
const home = process.env.AUI_HOME || path.join(os.homedir(), ".aui");
|
|
163
|
+
const sf = path.join(home, "session.json");
|
|
164
|
+
if (!fs.existsSync(sf))
|
|
165
|
+
return undefined;
|
|
166
|
+
const raw = fs.readFileSync(sf, "utf-8");
|
|
167
|
+
const parsed = JSON.parse(raw);
|
|
168
|
+
return parsed.cli_session_id;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Same as {@link getCliSessionId} but always returns the per-process
|
|
175
|
+
* run id (one UUID per CLI invocation, fresh every time). Exported so
|
|
176
|
+
* hot paths can attach it as an event attribute without going through
|
|
177
|
+
* `setUserContext`. */
|
|
178
|
+
export function getCliRunId() {
|
|
179
|
+
return cliRunId;
|
|
180
|
+
}
|
|
181
|
+
// ─── Session-scoped trace grouping ────────────────────────────────────
|
|
182
|
+
//
|
|
183
|
+
// `cli.session_id` as an attribute is filterable but Logfire doesn't
|
|
184
|
+
// visually GROUP spans by an arbitrary attribute — it groups by
|
|
185
|
+
// `trace_id`. Each `aui …` invocation is its own process, so by
|
|
186
|
+
// default every command starts a fresh trace and lands as a separate
|
|
187
|
+
// row in the Logfire UI even when they all carry the same
|
|
188
|
+
// `cli.session_id`.
|
|
189
|
+
//
|
|
190
|
+
// To get the "one expandable session" view we do two things:
|
|
191
|
+
//
|
|
192
|
+
// 1. Install a custom OTel `IdGenerator` that returns a deterministic
|
|
193
|
+
// `trace_id` derived from the session UUID for any new trace
|
|
194
|
+
// created in this process. UUIDs and OTel trace ids are both
|
|
195
|
+
// 128 bits — drop the dashes and you have a valid trace id —
|
|
196
|
+
// so two processes running under the same login session
|
|
197
|
+
// mint the SAME trace id and end up in one Logfire trace.
|
|
198
|
+
//
|
|
199
|
+
// 2. Open a real `aui.session` root span at process start, carrying
|
|
200
|
+
// the agent code / session id / `.auirc` snapshot. Logfire uses
|
|
201
|
+
// this real span as the trace's clickable title row (instead of
|
|
202
|
+
// the "incomplete trace, missing root span" header it renders
|
|
203
|
+
// when the orphan grouping logic kicks in). Every command span
|
|
204
|
+
// becomes a child of `aui.session` via the OTel context manager,
|
|
205
|
+
// so the trace tree is one neat collapsible group.
|
|
206
|
+
//
|
|
207
|
+
// The per-process `cli.run_id` UUID lives on each `aui.session` so
|
|
208
|
+
// individual invocations stay distinguishable within the session.
|
|
209
|
+
/** Lower-case 32-hex trace-id from a UUID. Returns `null` for malformed
|
|
210
|
+
* input or for the all-zero trace-id (forbidden by the OTel spec). */
|
|
211
|
+
function uuidToTraceId(uuid) {
|
|
212
|
+
const hex = uuid.replace(/-/g, "").toLowerCase();
|
|
213
|
+
if (!/^[0-9a-f]{32}$/.test(hex))
|
|
214
|
+
return null;
|
|
215
|
+
if (/^0+$/.test(hex))
|
|
216
|
+
return null;
|
|
217
|
+
return hex;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Custom `IdGenerator` that returns the session-derived trace id for
|
|
221
|
+
* any new trace started in this process, and falls back to random
|
|
222
|
+
* trace ids when there's no session. Span ids are always random.
|
|
223
|
+
*
|
|
224
|
+
* Object-literal shape (no class) so we don't take a runtime
|
|
225
|
+
* dependency on `@opentelemetry/sdk-trace-base` — `NodeSDK` accepts
|
|
226
|
+
* anything that matches the structural `IdGenerator` interface.
|
|
227
|
+
*/
|
|
228
|
+
const sessionAwareIdGenerator = {
|
|
229
|
+
generateTraceId() {
|
|
230
|
+
const sessionId = getCliSessionId();
|
|
231
|
+
if (sessionId) {
|
|
232
|
+
const t = uuidToTraceId(sessionId);
|
|
233
|
+
if (t)
|
|
234
|
+
return t;
|
|
235
|
+
}
|
|
236
|
+
return randomBytes(16).toString("hex");
|
|
237
|
+
},
|
|
238
|
+
generateSpanId() {
|
|
239
|
+
return randomBytes(8).toString("hex");
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
// ─── Session-root span: emit-once / reference-thereafter ──────────────
|
|
243
|
+
//
|
|
244
|
+
// We want exactly ONE `aui.session` span at the top of the Logfire
|
|
245
|
+
// trace — no matter how many `aui …` processes the user spawns under
|
|
246
|
+
// one login. Spans can't be shared across processes, so the trick is:
|
|
247
|
+
//
|
|
248
|
+
// - The FIRST telemetry-enabled invocation after login emits a
|
|
249
|
+
// real `aui.session` span (root, true OTel root with parent=null),
|
|
250
|
+
// waits until just before process exit to attach its attributes,
|
|
251
|
+
// and persists its `span_id` to `~/.aui/session.json` as
|
|
252
|
+
// `cli_session_root_span_id`.
|
|
253
|
+
//
|
|
254
|
+
// - Every SUBSEQUENT invocation reads the persisted `span_id`,
|
|
255
|
+
// builds a synthetic "remote parent" SpanContext from
|
|
256
|
+
// (session-trace-id, persisted-span-id), and attaches its command
|
|
257
|
+
// spans as children of that remote parent. It emits NO
|
|
258
|
+
// `aui.session` of its own.
|
|
259
|
+
//
|
|
260
|
+
// Net result in Logfire:
|
|
261
|
+
//
|
|
262
|
+
// aui session: <agent code> ← emitted by invocation 1
|
|
263
|
+
// ├── aui.import ← invocation 1
|
|
264
|
+
// ├── aui.validate ← invocation 2 (remote child)
|
|
265
|
+
// ├── aui.push ← invocation 3 (remote child)
|
|
266
|
+
// └── …
|
|
267
|
+
//
|
|
268
|
+
// Attribute lifecycle:
|
|
269
|
+
// - `aui.session`'s attrs (agent code, `.auirc` snapshot, user
|
|
270
|
+
// identifiers) are attached at `endSessionRootSpan()` time —
|
|
271
|
+
// i.e. AFTER the first command's body has run. This is the only
|
|
272
|
+
// way to capture the .auirc state on an `aui import` that
|
|
273
|
+
// CREATES the file (auirc.present=false at startup, true at end).
|
|
274
|
+
//
|
|
275
|
+
// Edge cases:
|
|
276
|
+
// - `aui login` skips this whole flow (it BIRTHS the session;
|
|
277
|
+
// can't be a child of it).
|
|
278
|
+
// - `aui logout` clears the session file along with the persisted
|
|
279
|
+
// span_id, so the next login starts fresh.
|
|
280
|
+
// - When `AUI_CLI_SESSION_ID` env override is used, we don't
|
|
281
|
+
// write back to `session.json` (the env is per-invocation
|
|
282
|
+
// scoped). Each invocation re-emits its own `aui.session` in
|
|
283
|
+
// that mode — acceptable because the env path is typically
|
|
284
|
+
// a single sandboxed call from the BFF.
|
|
285
|
+
/**
|
|
286
|
+
* Read both the session id and the persisted session-root span id
|
|
287
|
+
* from `~/.aui/session.json`. Sync (hot path) and safe to call
|
|
288
|
+
* before the rest of the config layer initializes.
|
|
289
|
+
*/
|
|
290
|
+
function readSessionTracingState() {
|
|
291
|
+
if (process.env.AUI_CLI_SESSION_ID) {
|
|
292
|
+
return { sessionId: process.env.AUI_CLI_SESSION_ID, fromEnv: true };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const home = process.env.AUI_HOME || path.join(os.homedir(), ".aui");
|
|
296
|
+
const sf = path.join(home, "session.json");
|
|
297
|
+
if (!fs.existsSync(sf))
|
|
298
|
+
return { fromEnv: false };
|
|
299
|
+
const raw = fs.readFileSync(sf, "utf-8");
|
|
300
|
+
const parsed = JSON.parse(raw);
|
|
301
|
+
return {
|
|
302
|
+
sessionId: parsed.cli_session_id,
|
|
303
|
+
rootSpanId: parsed.cli_session_root_span_id,
|
|
304
|
+
fromEnv: false,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return { fromEnv: false };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Persist `cli_session_root_span_id` back to `~/.aui/session.json`
|
|
313
|
+
* so the next CLI invocation can attach to the same root span.
|
|
314
|
+
*
|
|
315
|
+
* Best-effort, never throws.
|
|
316
|
+
*
|
|
317
|
+
* Two reasons we might bail without persisting:
|
|
318
|
+
* 1. `session.json` doesn't exist yet (very-first-login mid-flight
|
|
319
|
+
* where `saveSession` hasn't run yet — `endSessionRootSpan`
|
|
320
|
+
* re-tries after the action body completes).
|
|
321
|
+
* 2. `AUI_CLI_SESSION_ID` is set AND it doesn't match the
|
|
322
|
+
* persisted `cli_session_id`. This is the BFF / sandbox cross-
|
|
323
|
+
* session-contamination guard: the user's `~/.aui/session.json`
|
|
324
|
+
* belongs to login session X, but the env override pins this
|
|
325
|
+
* CLI process to session Z (BFF's own correlator). Writing
|
|
326
|
+
* Z's span_id back into the X session would link the user's
|
|
327
|
+
* future commands to a span that lives in a different trace.
|
|
328
|
+
*
|
|
329
|
+
* The `login` pre-mint case looks like (2) AT START — env=newId,
|
|
330
|
+
* file=oldId — but `saveSession` writes the new id during login,
|
|
331
|
+
* so by the time `endSessionRootSpan` calls us, file=newId=env and
|
|
332
|
+
* persistence happens cleanly.
|
|
333
|
+
*/
|
|
334
|
+
function persistSessionRootSpanId(spanId) {
|
|
335
|
+
try {
|
|
336
|
+
const home = process.env.AUI_HOME || path.join(os.homedir(), ".aui");
|
|
337
|
+
const sf = path.join(home, "session.json");
|
|
338
|
+
if (!fs.existsSync(sf))
|
|
339
|
+
return;
|
|
340
|
+
const raw = fs.readFileSync(sf, "utf-8");
|
|
341
|
+
const parsed = JSON.parse(raw);
|
|
342
|
+
const envSessionId = process.env.AUI_CLI_SESSION_ID;
|
|
343
|
+
if (envSessionId &&
|
|
344
|
+
typeof parsed.cli_session_id === "string" &&
|
|
345
|
+
parsed.cli_session_id !== envSessionId) {
|
|
346
|
+
// Env override points at a DIFFERENT login session than what's
|
|
347
|
+
// on disk — don't clobber the persisted user session.
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (parsed.cli_session_root_span_id === spanId)
|
|
351
|
+
return;
|
|
352
|
+
parsed.cli_session_root_span_id = spanId;
|
|
353
|
+
fs.writeFileSync(sf, JSON.stringify(parsed, null, 2), { mode: 0o600 });
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Non-critical: a stale or missing span_id just means the next
|
|
357
|
+
// invocation re-emits its own `aui.session`. No data loss.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* State for this process's session-trace integration.
|
|
362
|
+
*
|
|
363
|
+
* - `sessionRootSpan` is non-null iff WE emitted the `aui.session`
|
|
364
|
+
* span in this process (first invocation after login). It's the
|
|
365
|
+
* span we'll end at `beforeExit`.
|
|
366
|
+
* - `sessionParentContext` is non-null in either case (emitted or
|
|
367
|
+
* referenced). It's the OTel `Context` whose active span is the
|
|
368
|
+
* session root — `withSessionContext` uses it to wrap
|
|
369
|
+
* `program.parse()` so command spans become children.
|
|
370
|
+
*/
|
|
371
|
+
let sessionRootSpan = null;
|
|
372
|
+
let sessionParentContext = null;
|
|
373
|
+
// True when this process is the one that emitted the `aui.session`
|
|
374
|
+
// span via the env-override path (typically `aui login` pre-minting
|
|
375
|
+
// its session id before `session.json` exists). Drives a
|
|
376
|
+
// post-command persistence attempt in `endSessionRootSpan` so the
|
|
377
|
+
// next CLI invocation finds the span id we just emitted.
|
|
378
|
+
let sessionEmittedViaEnvOverride = false;
|
|
379
|
+
/**
|
|
380
|
+
* Start (or attach to) the `aui.session` root span for this login.
|
|
381
|
+
*
|
|
382
|
+
* No-op when:
|
|
383
|
+
* - telemetry is disabled (opt-out env vars / NODE_ENV=test)
|
|
384
|
+
* - no session id is resolvable from either the env override OR
|
|
385
|
+
* `~/.aui/session.json` (the pre-very-first-login path)
|
|
386
|
+
*
|
|
387
|
+
* Otherwise:
|
|
388
|
+
* - If no `cli_session_root_span_id` is persisted yet (and the id
|
|
389
|
+
* came from the file, not env) → emit a real `aui.session` span
|
|
390
|
+
* here and persist its span_id immediately.
|
|
391
|
+
* - If the id came from the env override (BFF correlation OR the
|
|
392
|
+
* `aui login` pre-mint hook in `src/index.ts`) → emit a real
|
|
393
|
+
* span; persistence is deferred to `endSessionRootSpan` because
|
|
394
|
+
* `session.json` may not exist yet (login hasn't run
|
|
395
|
+
* `saveSession` yet).
|
|
396
|
+
* - If a `cli_session_root_span_id` is already persisted → build a
|
|
397
|
+
* synthetic remote-parent context referencing it. No new span
|
|
398
|
+
* emitted.
|
|
399
|
+
*
|
|
400
|
+
* Either way, `sessionParentContext` is set and `withSessionContext`
|
|
401
|
+
* will wrap command actions inside it.
|
|
402
|
+
*
|
|
403
|
+
* Call from the CLI entry point right after `initTelemetry(...)`.
|
|
404
|
+
*/
|
|
405
|
+
export async function startSessionRootSpan() {
|
|
406
|
+
if (sessionParentContext)
|
|
407
|
+
return;
|
|
408
|
+
if (!isEnabled())
|
|
409
|
+
return;
|
|
410
|
+
const { sessionId, rootSpanId, fromEnv } = readSessionTracingState();
|
|
411
|
+
if (!sessionId)
|
|
412
|
+
return;
|
|
413
|
+
const traceId = uuidToTraceId(sessionId);
|
|
414
|
+
if (!traceId)
|
|
415
|
+
return;
|
|
416
|
+
// Subsequent-invocation path: a session-root span already exists
|
|
417
|
+
// (emitted by an earlier process under this login). Attach to it
|
|
418
|
+
// via a synthetic remote-parent context. Skip when the session id
|
|
419
|
+
// came from the env override — in that mode there's no persisted
|
|
420
|
+
// file state to consult and the env caller (BFF / login pre-mint)
|
|
421
|
+
// is the one that wants a fresh span.
|
|
422
|
+
if (rootSpanId && !fromEnv) {
|
|
423
|
+
const parentSpanContext = {
|
|
424
|
+
traceId,
|
|
425
|
+
spanId: rootSpanId,
|
|
426
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
427
|
+
isRemote: true,
|
|
428
|
+
};
|
|
429
|
+
sessionParentContext = trace.setSpanContext(ROOT_CONTEXT, parentSpanContext);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// Fresh-emit path: this process opens the `aui.session` span as a
|
|
433
|
+
// true OTel root (parent=null). The custom IdGenerator gives it
|
|
434
|
+
// the session-derived trace id automatically.
|
|
435
|
+
try {
|
|
436
|
+
const tracer = getTracer();
|
|
437
|
+
const span = tracer.startSpan("aui.session", { attributes: { "cli.session.is_session_root": true } }, ROOT_CONTEXT);
|
|
438
|
+
sessionRootSpan = span;
|
|
439
|
+
sessionParentContext = trace.setSpan(ROOT_CONTEXT, span);
|
|
440
|
+
if (fromEnv) {
|
|
441
|
+
// `session.json` may not exist yet (the common case: this is
|
|
442
|
+
// the `aui login` process and `saveSession` hasn't run). Defer
|
|
443
|
+
// persistence of the span id until `endSessionRootSpan` —
|
|
444
|
+
// login will have finished by then.
|
|
445
|
+
sessionEmittedViaEnvOverride = true;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
persistSessionRootSpanId(span.spanContext().spanId);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
sessionRootSpan = null;
|
|
453
|
+
sessionParentContext = null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* End and flush the `aui.session` root span. Idempotent; only the
|
|
458
|
+
* process that emitted the span does anything here (subsequent
|
|
459
|
+
* invocations never had a real span object to end).
|
|
460
|
+
*
|
|
461
|
+
* Attributes are attached LAZILY at this point — by now the
|
|
462
|
+
* command's action has run, so a `.auirc` written by `aui import`
|
|
463
|
+
* is visible and the agent code resolves cleanly. (Attaching them
|
|
464
|
+
* at start-of-process would lock the span name to
|
|
465
|
+
* `aui session: <no agent>` for sessions that begin with an
|
|
466
|
+
* import.)
|
|
467
|
+
*/
|
|
468
|
+
export async function endSessionRootSpan() {
|
|
469
|
+
if (!sessionRootSpan)
|
|
470
|
+
return;
|
|
471
|
+
const span = sessionRootSpan;
|
|
472
|
+
const emittedViaEnvOverride = sessionEmittedViaEnvOverride;
|
|
473
|
+
sessionRootSpan = null;
|
|
474
|
+
sessionEmittedViaEnvOverride = false;
|
|
475
|
+
try {
|
|
476
|
+
await setUserContext(span);
|
|
477
|
+
await setProjectContext(span);
|
|
478
|
+
// Friendly name + agent attrs — Logfire renders the name as the
|
|
479
|
+
// trace title row in the timeline. Read .auirc / session AFTER
|
|
480
|
+
// the command body has run so this reflects the post-command
|
|
481
|
+
// state (e.g. `aui import` populating .auirc.agent_code).
|
|
482
|
+
//
|
|
483
|
+
// `setSessionAgent()` (called by commands like `import-agent`
|
|
484
|
+
// that DIRECTLY resolve the agent without needing `.auirc`) may
|
|
485
|
+
// already have set the span name + attrs more authoritatively.
|
|
486
|
+
// Spans only let us overwrite attributes — they don't reveal
|
|
487
|
+
// their current name — so we always call updateName here, but
|
|
488
|
+
// we check the in-memory `lastSetSessionAgentLabel` first so
|
|
489
|
+
// commands that pushed a real label win over our fallback.
|
|
490
|
+
//
|
|
491
|
+
// Fallback chain (when no agent is resolvable):
|
|
492
|
+
// 1. user email — usually the most actionable identifier
|
|
493
|
+
// shown in Logfire (matches Logfire's user-table joins).
|
|
494
|
+
// 2. short prefix of the session id — guarantees uniqueness
|
|
495
|
+
// and matches the trace id derived from it, so the title
|
|
496
|
+
// row is still self-identifying.
|
|
497
|
+
// 3. literal "aui session" — last resort, only reached when
|
|
498
|
+
// neither agent nor user nor session id is available
|
|
499
|
+
// (essentially impossible under normal flow).
|
|
500
|
+
try {
|
|
501
|
+
const { loadProjectConfig, loadSession } = await import("./config/index.js");
|
|
502
|
+
const projectConfig = loadProjectConfig();
|
|
503
|
+
const sess = loadSession();
|
|
504
|
+
const agentLabel = lastSetSessionAgentLabel ||
|
|
505
|
+
projectConfig?.agent_code ||
|
|
506
|
+
sess?.network_name ||
|
|
507
|
+
projectConfig?.agent_management_id ||
|
|
508
|
+
projectConfig?.agent_id;
|
|
509
|
+
let title;
|
|
510
|
+
if (agentLabel) {
|
|
511
|
+
title = `aui session: ${agentLabel}`;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const sessionId = getCliSessionId();
|
|
515
|
+
const userLabel = sess?.email || (sessionId ? sessionId.slice(0, 8) : undefined);
|
|
516
|
+
title = userLabel ? `aui session: ${userLabel}` : "aui session";
|
|
517
|
+
}
|
|
518
|
+
span.updateName(title);
|
|
519
|
+
if (projectConfig?.agent_code) {
|
|
520
|
+
span.setAttribute("agent.code", projectConfig.agent_code);
|
|
521
|
+
}
|
|
522
|
+
if (projectConfig?.agent_id) {
|
|
523
|
+
span.setAttribute("agent.id", projectConfig.agent_id);
|
|
524
|
+
}
|
|
525
|
+
if (sess?.network_name) {
|
|
526
|
+
span.setAttribute("agent.name", sess.network_name);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// Cosmetic — never block shutdown on it.
|
|
531
|
+
}
|
|
532
|
+
// Persist this span's id so the NEXT CLI invocation under the
|
|
533
|
+
// same login can attach to it as a remote parent. We only do
|
|
534
|
+
// this when telemetry pre-minted the session id via the env
|
|
535
|
+
// override (the `aui login` flow): in the regular file-driven
|
|
536
|
+
// path the id was already persisted at the start of the run.
|
|
537
|
+
//
|
|
538
|
+
// `session.json` should exist by now — `aui login` writes it
|
|
539
|
+
// BEFORE returning from its action. If it doesn't (login
|
|
540
|
+
// failed before saving), `persistSessionRootSpanId` is a no-op.
|
|
541
|
+
if (emittedViaEnvOverride) {
|
|
542
|
+
persistSessionRootSpanId(span.spanContext().spanId);
|
|
543
|
+
}
|
|
544
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
545
|
+
span.end();
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// Shutdown must not throw.
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// ─── Late agent-name attachment from inside a command ─────────────────
|
|
552
|
+
//
|
|
553
|
+
// Some commands (notably `aui import-agent`) BIRTH the project layout
|
|
554
|
+
// — they create `.auirc` in a subdirectory of CWD. By the time the
|
|
555
|
+
// process exits, `findProjectRoot()` from the original CWD doesn't
|
|
556
|
+
// see the freshly-written file (it walks UP from CWD, not down), so
|
|
557
|
+
// `endSessionRootSpan`'s `.auirc`-based name resolution falls back
|
|
558
|
+
// to `<no agent>`. `setSessionAgent` is the explicit hook those
|
|
559
|
+
// commands call once they've resolved the agent, pushing the right
|
|
560
|
+
// name + identifiers onto the session span directly.
|
|
561
|
+
//
|
|
562
|
+
// We also stash the last label set so `endSessionRootSpan`'s
|
|
563
|
+
// `loadProjectConfig`-based fallback doesn't accidentally overwrite
|
|
564
|
+
// a real value with the generic literal — see the agentLabel
|
|
565
|
+
// resolution above.
|
|
566
|
+
let lastSetSessionAgentLabel;
|
|
567
|
+
/**
|
|
568
|
+
* Push an authoritative agent label onto the `aui.session` root span
|
|
569
|
+
* for this process. Called by commands that resolve the agent
|
|
570
|
+
* themselves (e.g. `import-agent` from the backend response). Safe
|
|
571
|
+
* to call multiple times — last call wins.
|
|
572
|
+
*
|
|
573
|
+
* No-op when this process doesn't own the session root span
|
|
574
|
+
* (subsequent invocations attaching as remote children, opt-out
|
|
575
|
+
* mode, pre-login).
|
|
576
|
+
*/
|
|
577
|
+
export function setSessionAgent(info) {
|
|
578
|
+
if (!sessionRootSpan)
|
|
579
|
+
return;
|
|
580
|
+
try {
|
|
581
|
+
const label = info.agentCode ||
|
|
582
|
+
info.agentName ||
|
|
583
|
+
info.agentManagementId ||
|
|
584
|
+
info.agentId ||
|
|
585
|
+
undefined;
|
|
586
|
+
if (label) {
|
|
587
|
+
lastSetSessionAgentLabel = label;
|
|
588
|
+
sessionRootSpan.updateName(`aui session: ${label}`);
|
|
589
|
+
}
|
|
590
|
+
if (info.agentCode)
|
|
591
|
+
sessionRootSpan.setAttribute("agent.code", info.agentCode);
|
|
592
|
+
if (info.agentName)
|
|
593
|
+
sessionRootSpan.setAttribute("agent.name", info.agentName);
|
|
594
|
+
if (info.agentId)
|
|
595
|
+
sessionRootSpan.setAttribute("agent.id", info.agentId);
|
|
596
|
+
if (info.agentManagementId) {
|
|
597
|
+
sessionRootSpan.setAttribute("agent.management_id", info.agentManagementId);
|
|
598
|
+
}
|
|
599
|
+
if (info.versionId)
|
|
600
|
+
sessionRootSpan.setAttribute("agent.version_id", info.versionId);
|
|
601
|
+
if (info.versionLabel)
|
|
602
|
+
sessionRootSpan.setAttribute("agent.version_label", info.versionLabel);
|
|
603
|
+
if (info.projectRoot)
|
|
604
|
+
sessionRootSpan.setAttribute("auirc.project_root", info.projectRoot);
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// Telemetry must never break the CLI.
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Run `fn` with the `aui.session` parent context active in OTel.
|
|
612
|
+
* Every span created inside `fn` — including async callbacks reached
|
|
613
|
+
* via the AsyncHooks ContextManager `NodeSDK` installs — becomes a
|
|
614
|
+
* child of `aui.session` (whether that span lives in this process or
|
|
615
|
+
* was emitted by a previous invocation under the same login).
|
|
616
|
+
*
|
|
617
|
+
* No-op (`fn()` directly) when no session parent is resolvable — the
|
|
618
|
+
* pre-login / logged-out / opted-out path.
|
|
619
|
+
*
|
|
620
|
+
* Wrap `program.parse()` with this at the CLI entry point.
|
|
621
|
+
*/
|
|
622
|
+
export function withSessionContext(fn) {
|
|
623
|
+
if (!sessionParentContext)
|
|
624
|
+
return fn();
|
|
625
|
+
return context.with(sessionParentContext, fn);
|
|
626
|
+
}
|
|
627
|
+
// ─── Agent-scope span: per-project grouping inside the session ────────
|
|
628
|
+
//
|
|
629
|
+
// Sister to the session-root span, one level finer-grained: every CLI
|
|
630
|
+
// command that operates on the SAME project (.auirc → same
|
|
631
|
+
// agent_code) gets grouped under a single `agent: <code>` span inside
|
|
632
|
+
// the session trace.
|
|
633
|
+
//
|
|
634
|
+
// Visualisation in Logfire:
|
|
635
|
+
//
|
|
636
|
+
// aui session: alaam@aui.io
|
|
637
|
+
// ├── aui.login ← (no agent yet)
|
|
638
|
+
// ├── aui.import ← (creates .auirc)
|
|
639
|
+
// ├── agent: amazon-retail-agent ← emitted by the next command in
|
|
640
|
+
// │ ├── aui.pull the project; persisted in .auirc
|
|
641
|
+
// │ ├── aui.validate
|
|
642
|
+
// │ └── aui.push
|
|
643
|
+
// ├── aui.import ← (different agent project)
|
|
644
|
+
// ├── agent: another-agent
|
|
645
|
+
// │ └── aui.push
|
|
646
|
+
//
|
|
647
|
+
// Implementation mirrors the session-root pattern:
|
|
648
|
+
// - First command in a project (no `cli_agent_span_id` in .auirc)
|
|
649
|
+
// emits a real `agent: <code>` span and persists its span id.
|
|
650
|
+
// - Every subsequent command reads the span id and attaches as a
|
|
651
|
+
// remote child — no new span emitted per process.
|
|
652
|
+
//
|
|
653
|
+
// Persistence is in `.auirc.cli_agent_span_id`. The span id never
|
|
654
|
+
// expires; re-running the CLI on the same project months later still
|
|
655
|
+
// nests new commands under the same agent header.
|
|
656
|
+
let agentRootSpan = null;
|
|
657
|
+
let agentParentContext = null;
|
|
658
|
+
/**
|
|
659
|
+
* Find the project root from CWD and read enough of `.auirc` to
|
|
660
|
+
* decide whether to emit / attach an agent-scope span. Sync — we run
|
|
661
|
+
* this once at CLI startup. Best-effort; returns an empty object on
|
|
662
|
+
* any I/O / parse error so the caller can no-op.
|
|
663
|
+
*/
|
|
664
|
+
function readAgentTracingState() {
|
|
665
|
+
try {
|
|
666
|
+
let dir = process.cwd();
|
|
667
|
+
let projectRoot;
|
|
668
|
+
while (dir !== path.dirname(dir)) {
|
|
669
|
+
if (fs.existsSync(path.join(dir, ".auirc")) ||
|
|
670
|
+
fs.existsSync(path.join(dir, "agent.aui.json"))) {
|
|
671
|
+
projectRoot = dir;
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
dir = path.dirname(dir);
|
|
675
|
+
}
|
|
676
|
+
if (!projectRoot)
|
|
677
|
+
return {};
|
|
678
|
+
const auircPath = path.join(projectRoot, ".auirc");
|
|
679
|
+
if (!fs.existsSync(auircPath))
|
|
680
|
+
return { projectRoot };
|
|
681
|
+
const raw = fs.readFileSync(auircPath, "utf-8");
|
|
682
|
+
if (!raw.trim())
|
|
683
|
+
return { projectRoot };
|
|
684
|
+
const parsed = JSON.parse(raw);
|
|
685
|
+
return {
|
|
686
|
+
projectRoot,
|
|
687
|
+
agentCode: parsed.agent_code,
|
|
688
|
+
agentSpanId: parsed.cli_agent_span_id,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Persist the agent-scope span id back to the project's `.auirc`.
|
|
697
|
+
* Reads the current file (whatever else is in there), adds the span
|
|
698
|
+
* id, writes back. Best-effort; never throws.
|
|
699
|
+
*/
|
|
700
|
+
function persistAgentSpanIdToAuirc(projectRoot, spanId) {
|
|
701
|
+
try {
|
|
702
|
+
const auircPath = path.join(projectRoot, ".auirc");
|
|
703
|
+
if (!fs.existsSync(auircPath))
|
|
704
|
+
return;
|
|
705
|
+
const raw = fs.readFileSync(auircPath, "utf-8");
|
|
706
|
+
const parsed = JSON.parse(raw);
|
|
707
|
+
if (parsed.cli_agent_span_id === spanId)
|
|
708
|
+
return;
|
|
709
|
+
parsed.cli_agent_span_id = spanId;
|
|
710
|
+
fs.writeFileSync(auircPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// Non-critical: a stale or missing span id just means the next
|
|
714
|
+
// invocation re-emits its own `agent:` span. No data loss.
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Run `fn` with the `agent: <code>` parent context active in OTel.
|
|
719
|
+
* Every span created inside — typically the command's root span
|
|
720
|
+
* (`aui.push` / `aui.pull` / `aui.validate` / `aui.import`) — becomes
|
|
721
|
+
* a child of the agent span.
|
|
722
|
+
*
|
|
723
|
+
* Behaviour:
|
|
724
|
+
* - No `.auirc` in CWD (or no `agent_code` yet) → no-op, just
|
|
725
|
+
* `fn()`. Covers `aui login`, `aui --version`, and the initial
|
|
726
|
+
* `aui import-agent` before it has written `.auirc`.
|
|
727
|
+
* - `.auirc` present + `cli_agent_span_id` persisted → build a
|
|
728
|
+
* synthetic remote-parent context referencing the persisted
|
|
729
|
+
* span id (nested under the active session context) and run
|
|
730
|
+
* `fn` inside it.
|
|
731
|
+
* - `.auirc` present + no `cli_agent_span_id` yet → emit a real
|
|
732
|
+
* `agent: <code>` span as child of the active context, persist
|
|
733
|
+
* its span id to `.auirc`, run `fn` inside its context.
|
|
734
|
+
*
|
|
735
|
+
* Call from the CLI entry point, INSIDE `withSessionContext` so the
|
|
736
|
+
* agent span becomes a child of the session row.
|
|
737
|
+
*/
|
|
738
|
+
export function withAgentContext(fn) {
|
|
739
|
+
if (!isEnabled())
|
|
740
|
+
return fn();
|
|
741
|
+
if (agentParentContext)
|
|
742
|
+
return context.with(agentParentContext, fn);
|
|
743
|
+
const state = readAgentTracingState();
|
|
744
|
+
if (!state.agentCode || !state.projectRoot)
|
|
745
|
+
return fn();
|
|
746
|
+
if (state.agentSpanId) {
|
|
747
|
+
// Subsequent-invocation path: attach via synthetic remote-parent
|
|
748
|
+
// context referencing the span emitted by an earlier command in
|
|
749
|
+
// this project. trace_id is inherited from the active (session)
|
|
750
|
+
// context — the IdGenerator already pinned it to the session
|
|
751
|
+
// trace id, so we just point at the agent span id.
|
|
752
|
+
const activeSpanContext = trace
|
|
753
|
+
.getSpan(context.active())
|
|
754
|
+
?.spanContext();
|
|
755
|
+
const traceId = activeSpanContext?.traceId;
|
|
756
|
+
if (!traceId)
|
|
757
|
+
return fn();
|
|
758
|
+
const agentSpanContext = {
|
|
759
|
+
traceId,
|
|
760
|
+
spanId: state.agentSpanId,
|
|
761
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
762
|
+
isRemote: true,
|
|
763
|
+
};
|
|
764
|
+
agentParentContext = trace.setSpanContext(context.active(), agentSpanContext);
|
|
765
|
+
return context.with(agentParentContext, fn);
|
|
766
|
+
}
|
|
767
|
+
// Fresh-emit path: this is the first command run inside this
|
|
768
|
+
// project AFTER it was imported. Emit the `agent: <code>` span as
|
|
769
|
+
// a child of the active session context, persist its span id, and
|
|
770
|
+
// wrap `fn` in its context.
|
|
771
|
+
try {
|
|
772
|
+
const tracer = getTracer();
|
|
773
|
+
const span = tracer.startSpan(`agent: ${state.agentCode}`, {
|
|
774
|
+
attributes: {
|
|
775
|
+
"cli.agent.is_agent_root": true,
|
|
776
|
+
"agent.code": state.agentCode,
|
|
777
|
+
"auirc.project_root": state.projectRoot,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
agentRootSpan = span;
|
|
781
|
+
agentParentContext = trace.setSpan(context.active(), span);
|
|
782
|
+
persistAgentSpanIdToAuirc(state.projectRoot, span.spanContext().spanId);
|
|
783
|
+
return context.with(agentParentContext, fn);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return fn();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* End the `agent: <code>` root span and flush it through the
|
|
791
|
+
* exporter. Idempotent; only the process that EMITTED the span has
|
|
792
|
+
* a real span object to end. Call from the `beforeExit` handler
|
|
793
|
+
* BEFORE `endSessionRootSpan()` (children-before-parents flush).
|
|
794
|
+
*/
|
|
795
|
+
export function endAgentRootSpan() {
|
|
796
|
+
if (!agentRootSpan)
|
|
797
|
+
return;
|
|
798
|
+
const span = agentRootSpan;
|
|
799
|
+
agentRootSpan = null;
|
|
800
|
+
agentParentContext = null;
|
|
801
|
+
try {
|
|
802
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
803
|
+
span.end();
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Shutdown must not throw.
|
|
807
|
+
}
|
|
808
|
+
}
|
|
80
809
|
export async function setUserContext(span) {
|
|
81
810
|
try {
|
|
82
811
|
const { loadSession, loadEnvironment } = await import("./config/index.js");
|
|
812
|
+
// Session/run identifiers are attached BEFORE the early-return so
|
|
813
|
+
// that even unauthenticated commands (`aui login`, `aui env`) emit
|
|
814
|
+
// a per-process `cli.run_id` and the active `cli.command`.
|
|
815
|
+
span.setAttribute("cli.run_id", cliRunId);
|
|
816
|
+
span.setAttribute("cli.command", cliCommand);
|
|
817
|
+
span.setAttribute("cli.invoked_at", cliInvokedAt);
|
|
818
|
+
span.setAttribute("cli.version", cliVersion);
|
|
819
|
+
const sessionId = getCliSessionId();
|
|
820
|
+
if (sessionId) {
|
|
821
|
+
span.setAttribute("cli.session_id", sessionId);
|
|
822
|
+
span.setAttribute("cli.session_id_source", process.env.AUI_CLI_SESSION_ID ? "env" : "session_file");
|
|
823
|
+
}
|
|
83
824
|
const session = loadSession();
|
|
84
825
|
if (!session)
|
|
85
826
|
return;
|
|
@@ -98,12 +839,76 @@ export async function setUserContext(span) {
|
|
|
98
839
|
if (session.organization_name)
|
|
99
840
|
span.setAttribute("user.organization_name", session.organization_name);
|
|
100
841
|
span.setAttribute("user.environment", session.environment || loadEnvironment());
|
|
101
|
-
span.setAttribute("cli.version", cliVersion);
|
|
102
842
|
}
|
|
103
843
|
catch {
|
|
104
844
|
// non-critical
|
|
105
845
|
}
|
|
106
846
|
}
|
|
847
|
+
// ─── .auirc snapshot ──────────────────────────────────────────────────
|
|
848
|
+
//
|
|
849
|
+
// Sister helper to `setUserContext` / `setAgentContext` — attaches the
|
|
850
|
+
// resolved `.auirc` file contents to a span as `auirc.<field>` so
|
|
851
|
+
// Logfire shows exactly what the CLI was working from on disk. Useful
|
|
852
|
+
// when "the same push works for me but fails for them" — usually a
|
|
853
|
+
// stale `agent_management_id`, missing `version_id`, or a mismatched
|
|
854
|
+
// `scope_level` in `.auirc`.
|
|
855
|
+
//
|
|
856
|
+
// Secret-bearing keys (currently only `network_api_key`) are emitted
|
|
857
|
+
// with a `***REDACTED***` placeholder so dashboards still show "the
|
|
858
|
+
// key WAS set" without exposing its value.
|
|
859
|
+
//
|
|
860
|
+
// Best-effort: never throws. If `.auirc` is missing / unreadable, we
|
|
861
|
+
// just attach `auirc.present=false` and move on.
|
|
862
|
+
const AUIRC_REDACTED_KEYS = new Set([
|
|
863
|
+
"network_api_key",
|
|
864
|
+
]);
|
|
865
|
+
/**
|
|
866
|
+
* Attach a redacted `.auirc` snapshot to a span. Call after
|
|
867
|
+
* `setUserContext` on every top-level command span (push, pull,
|
|
868
|
+
* import-agent, validate) so a single Logfire row shows the on-disk
|
|
869
|
+
* project state alongside who ran it and against which agent.
|
|
870
|
+
*/
|
|
871
|
+
export async function setProjectContext(span, projectRoot) {
|
|
872
|
+
try {
|
|
873
|
+
const { loadProjectConfig, findProjectRoot } = await import("./config/index.js");
|
|
874
|
+
const root = projectRoot || findProjectRoot() || undefined;
|
|
875
|
+
const projectConfig = loadProjectConfig(root);
|
|
876
|
+
if (!projectConfig) {
|
|
877
|
+
span.setAttribute("auirc.present", false);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
span.setAttribute("auirc.present", true);
|
|
881
|
+
if (root)
|
|
882
|
+
span.setAttribute("auirc.project_root", root);
|
|
883
|
+
for (const [key, value] of Object.entries(projectConfig)) {
|
|
884
|
+
if (value === undefined || value === null)
|
|
885
|
+
continue;
|
|
886
|
+
const attrKey = `auirc.${key}`;
|
|
887
|
+
if (AUIRC_REDACTED_KEYS.has(key)) {
|
|
888
|
+
// Stamp presence-only so dashboards can filter "had an api key
|
|
889
|
+
// set" without ever shipping the value off the user's machine.
|
|
890
|
+
span.setAttribute(attrKey, "***REDACTED***");
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (typeof value === "string" ||
|
|
894
|
+
typeof value === "number" ||
|
|
895
|
+
typeof value === "boolean") {
|
|
896
|
+
span.setAttribute(attrKey, value);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
try {
|
|
900
|
+
span.setAttribute(attrKey, JSON.stringify(value));
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
// Non-serialisable value (cyclic ref) — skip without breaking.
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// .auirc is informational telemetry — never break a command.
|
|
910
|
+
}
|
|
911
|
+
}
|
|
107
912
|
/**
|
|
108
913
|
* Attach agent-scoped identifiers to a span. Use alongside `setUserContext`
|
|
109
914
|
* on per-task push spans so a single Logfire row tells the on-call exactly:
|
|
@@ -151,4 +956,5 @@ export async function setAgentContext(span, ctx) {
|
|
|
151
956
|
}
|
|
152
957
|
}
|
|
153
958
|
export { SpanStatusCode };
|
|
959
|
+
export { trace };
|
|
154
960
|
//# sourceMappingURL=telemetry.js.map
|