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.
Files changed (46) hide show
  1. package/dist/api-client/index.d.ts +27 -8
  2. package/dist/api-client/index.d.ts.map +1 -1
  3. package/dist/api-client/index.js +247 -109
  4. package/dist/api-client/index.js.map +1 -1
  5. package/dist/api-client/kb-view-client.d.ts +12 -6
  6. package/dist/api-client/kb-view-client.d.ts.map +1 -1
  7. package/dist/api-client/kb-view-client.js +38 -23
  8. package/dist/api-client/kb-view-client.js.map +1 -1
  9. package/dist/commands/import-agent.d.ts.map +1 -1
  10. package/dist/commands/import-agent.js +30 -6
  11. package/dist/commands/import-agent.js.map +1 -1
  12. package/dist/commands/legacy/push-records-mode.d.ts.map +1 -1
  13. package/dist/commands/legacy/push-records-mode.js +0 -1
  14. package/dist/commands/legacy/push-records-mode.js.map +1 -1
  15. package/dist/commands/login.d.ts.map +1 -1
  16. package/dist/commands/login.js +29 -0
  17. package/dist/commands/login.js.map +1 -1
  18. package/dist/commands/pull-agent.d.ts.map +1 -1
  19. package/dist/commands/pull-agent.js +6 -2
  20. package/dist/commands/pull-agent.js.map +1 -1
  21. package/dist/commands/push.d.ts.map +1 -1
  22. package/dist/commands/push.js +6 -3
  23. package/dist/commands/push.js.map +1 -1
  24. package/dist/commands/util/agent-mode.d.ts.map +1 -1
  25. package/dist/commands/util/agent-mode.js +69 -11
  26. package/dist/commands/util/agent-mode.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +6 -1
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/config/index.d.ts +56 -0
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/index.js.map +1 -1
  33. package/dist/index.js +100 -4
  34. package/dist/index.js.map +1 -1
  35. package/dist/services/auth.service.d.ts.map +1 -1
  36. package/dist/services/auth.service.js +21 -0
  37. package/dist/services/auth.service.js.map +1 -1
  38. package/dist/services/kb-view.service.d.ts +2 -2
  39. package/dist/services/kb-view.service.d.ts.map +1 -1
  40. package/dist/services/kb-view.service.js +6 -7
  41. package/dist/services/kb-view.service.js.map +1 -1
  42. package/dist/telemetry.d.ts +130 -1
  43. package/dist/telemetry.d.ts.map +1 -1
  44. package/dist/telemetry.js +808 -2
  45. package/dist/telemetry.js.map +1 -1
  46. 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