agent-first-data 0.17.3 → 0.19.0

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AFDATA CLI helpers — output format parsing, log filter normalization, error building.
3
3
  */
4
- import { Event, jsonError, jsonResult, outputJson, outputYaml, outputPlain, validateProtocolEvent, } from "./format.js";
4
+ import { Event, jsonError, jsonResult, formatJsonValue, formatYamlValue, formatPlainValue, validateProtocolEvent, } from "./format.js";
5
5
  /**
6
6
  * Parse the --output flag value into an OutputFormat.
7
7
  * Throws on unknown values; catch and pass message to buildCliError.
@@ -33,24 +33,28 @@ export class LogFilters {
33
33
  }
34
34
  /**
35
35
  * Check if an event matches the filter set.
36
- * Returns false for empty filters, true if "all" or "*" is present,
37
- * otherwise checks if event.toLowerCase() starts with any filter.
36
+ *
37
+ * An empty filter set returns false (filtering is opt-in). The single
38
+ * wildcard word "all" returns true ("*" is not special — one wildcard
39
+ * spelling, not two). Otherwise returns true iff event.toLowerCase() starts
40
+ * with any filter (prefix match); a mistyped filter simply matches nothing
41
+ * and silently emits no output.
38
42
  *
39
43
  * @example
40
44
  * const lf = new LogFilters(["query", "error"]);
41
45
  * lf.enabled("QueryStarted") // → true (starts with "query")
42
46
  * lf.enabled("debug") // → false
43
47
  *
44
- * const all = new LogFilters([]);
45
- * all.enabled("anything") // → false (empty)
48
+ * const none = new LogFilters([]);
49
+ * none.enabled("anything") // → false (empty)
46
50
  *
47
- * const wild = new LogFilters(["*"]);
51
+ * const wild = new LogFilters(["all"]);
48
52
  * wild.enabled("anything") // → true
49
53
  */
50
54
  enabled(event) {
51
55
  if (this.filters.length === 0)
52
56
  return false;
53
- if (this.filters.includes("all") || this.filters.includes("*"))
57
+ if (this.filters.includes("all"))
54
58
  return true;
55
59
  const eventLower = event.toLowerCase();
56
60
  for (const filter of this.filters) {
@@ -74,36 +78,130 @@ export function cliParseLogFilters(entries) {
74
78
  return new LogFilters(entries);
75
79
  }
76
80
  /**
77
- * Dispatch output formatting by OutputFormat, with optional explicit redaction and style.
78
- * JSON ignores OutputStyle and preserves original keys and values after redaction.
81
+ * Single public render entry point: dispatch a value or Event to
82
+ * JSON/YAML/plain text by OutputFormat, with optional explicit redaction and
83
+ * style. `options.style` (PlainStyle) affects plain (logfmt) output only;
84
+ * JSON and YAML are structure-preserving and ignore it.
79
85
  *
80
86
  * @example
81
- * cliOutput(jsonResult({ ok: true }).build(), "plain") // → "kind=result result.ok=true"
87
+ * render(jsonResult({ ok: true }).build(), "plain") // → "kind=result result.ok=true"
82
88
  */
83
- export function cliOutput(value, format, options = {}) {
89
+ export function render(value, format, options = {}) {
84
90
  const jsonValue = value instanceof Event ? value.toJSON() : value;
85
91
  if (format === "yaml")
86
- return outputYaml(jsonValue, options);
92
+ return formatYamlValue(jsonValue, options);
87
93
  if (format === "plain")
88
- return outputPlain(jsonValue, options);
89
- return outputJson(jsonValue, options);
94
+ return formatPlainValue(jsonValue, options);
95
+ return formatJsonValue(jsonValue, options);
96
+ }
97
+ /**
98
+ * Parse an `--output-to` value into an OutputTo: "split" (default), "stdout", or
99
+ * "stderr". Throws on unknown values; catch and pass the message to
100
+ * buildCliError.
101
+ *
102
+ * @example
103
+ * parseOutputTo("split") // → "split"
104
+ * parseOutputTo("both") // throws Error
105
+ */
106
+ export function parseOutputTo(value) {
107
+ if (value === "split" || value === "stdout" || value === "stderr") {
108
+ return value;
109
+ }
110
+ throw new Error(`invalid --output-to '${value}': expected split, stdout, or stderr`);
90
111
  }
91
- /** Stateful emitter for finite structured CLI executions. */
112
+ /** Event writer bound to the process stdout stream. */
113
+ const processStdoutWriter = (line) => {
114
+ process.stdout.write(line);
115
+ };
116
+ /**
117
+ * Event writer bound to the process stderr stream. This is the CliEmitter's
118
+ * blessed diagnostic sink: stderr is sanctioned here, and only here, precisely
119
+ * because output flows through the emitter rather than an ad-hoc write. The
120
+ * `stderr-sink` marker is what no_stderr_policy.test.ts allows through.
121
+ */
122
+ const processStderrWriter = (line) => {
123
+ process.stderr.write(line); // stderr-sink: CliEmitter diagnostic channel
124
+ };
125
+ /**
126
+ * Stateful emitter for structured CLI executions.
127
+ *
128
+ * Routing follows the consumption mode (OutputTo):
129
+ *
130
+ * - Finite one-shot (`CliEmitter.finite` / `finiteWith` /
131
+ * `fromOutputTo("split")`): `result` → the primary writer (stdout);
132
+ * `error`/`progress`/`log` → the diagnostic writer (stderr). The recommended
133
+ * default for a one-shot CLI, so shell capture and pipelines never treat a
134
+ * failure as data.
135
+ * - Event stream (`new CliEmitter(...)` / `CliEmitter.stream` /
136
+ * `fromOutputTo("stdout"|"stderr")`): every event, including `error`, goes to
137
+ * the single writer, preserving interleaved ordering.
138
+ */
92
139
  export class CliEmitter {
93
140
  writer;
94
141
  format;
95
142
  outputOptions;
96
143
  terminalEmitted = false;
97
144
  logFieldsProvider;
145
+ diagnostic;
146
+ /**
147
+ * Create an event-stream emitter: every event, including `error`, goes to the
148
+ * single `writer`. Use CliEmitter.finite for a one-shot command that should
149
+ * split `result`/`error` across stdout/stderr.
150
+ */
98
151
  constructor(writer, format, outputOptions = {}) {
99
152
  this.writer = writer;
100
153
  this.format = format;
101
154
  this.outputOptions = outputOptions;
102
155
  }
156
+ /**
157
+ * Create an event-stream emitter: every event goes to `writer`, preserving
158
+ * interleaved ordering. Pick this when the consumer reads one ordered stream
159
+ * and branches on `kind`. Alias for the constructor's unified form.
160
+ */
161
+ static stream(writer, format, outputOptions = {}) {
162
+ return new CliEmitter(writer, format, outputOptions);
163
+ }
164
+ /**
165
+ * Create a finite one-shot emitter with explicit sinks: `result` goes to
166
+ * `resultWriter`, while `error`/`progress`/`log` go to `diagnostic`.
167
+ */
168
+ static finiteWith(resultWriter, diagnostic, format, outputOptions = {}) {
169
+ const emitter = new CliEmitter(resultWriter, format, outputOptions);
170
+ emitter.diagnostic = diagnostic;
171
+ return emitter;
172
+ }
173
+ /**
174
+ * Create a finite one-shot emitter wired to the process streams: `result` →
175
+ * stdout, `error`/`progress`/`log` → stderr. The recommended default for a
176
+ * one-shot CLI.
177
+ */
178
+ static finite(format, outputOptions = {}) {
179
+ return CliEmitter.finiteWith(processStdoutWriter, processStderrWriter, format, outputOptions);
180
+ }
181
+ /**
182
+ * Build an emitter from a parsed OutputTo selector, wired to the process
183
+ * streams: "split" is finite mode (`result` → stdout, everything else →
184
+ * stderr); "stdout"/"stderr" are event-stream mode onto that one stream.
185
+ */
186
+ static fromOutputTo(selector, format, outputOptions = {}) {
187
+ if (selector === "split")
188
+ return CliEmitter.finite(format, outputOptions);
189
+ const writer = selector === "stdout" ? processStdoutWriter : processStderrWriter;
190
+ return CliEmitter.stream(writer, format, outputOptions);
191
+ }
103
192
  withLogFields(provider) {
104
193
  this.logFieldsProvider = provider;
105
194
  return this;
106
195
  }
196
+ /**
197
+ * Select the sink for an event by `kind`. Finite mode (a diagnostic sink is
198
+ * present) keeps `result` on the primary writer (stdout) and routes
199
+ * `error`/`progress`/`log` to the diagnostic writer (stderr). Event-stream
200
+ * mode (no diagnostic sink) keeps every event on the single writer.
201
+ */
202
+ sinkFor(kind) {
203
+ return kind !== "result" && this.diagnostic ? this.diagnostic : this.writer;
204
+ }
107
205
  emit(event) {
108
206
  const jsonValue = event.toJSON();
109
207
  validateProtocolEvent(jsonValue);
@@ -121,7 +219,7 @@ export class CliEmitter {
121
219
  else {
122
220
  throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
123
221
  }
124
- this.writer(`${cliOutput(jsonValue, this.format, this.outputOptions)}\n`);
222
+ this.sinkFor(kind)(`${render(jsonValue, this.format, this.outputOptions)}\n`);
125
223
  if (kind === "result" || kind === "error")
126
224
  this.terminalEmitted = true;
127
225
  }
@@ -141,14 +239,44 @@ export class CliEmitter {
141
239
  else {
142
240
  throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
143
241
  }
144
- this.writer(`${cliOutput(value, this.format, this.outputOptions)}\n`);
242
+ this.sinkFor(kind)(`${render(value, this.format, this.outputOptions)}\n`);
145
243
  if (kind === "result" || kind === "error")
146
244
  this.terminalEmitted = true;
147
245
  }
246
+ static isBrokenPipe(err) {
247
+ return (typeof err === "object" && err !== null && err.code === "EPIPE");
248
+ }
249
+ /**
250
+ * Emit `event` as the terminal event and resolve the outcome to a process
251
+ * exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
252
+ *
253
+ * A successful write returns `successCode`; a broken pipe (the reader hung up)
254
+ * returns `0`; any other write or validation failure returns `4`. A library
255
+ * never calls process.exit itself — return this code and let the caller exit.
256
+ */
257
+ finish(event, successCode) {
258
+ try {
259
+ this.emit(event);
260
+ return successCode;
261
+ }
262
+ catch (err) {
263
+ return CliEmitter.isBrokenPipe(err) ? 0 : 4;
264
+ }
265
+ }
266
+ /**
267
+ * Convenience over finish: emit a `result` payload and return `0` on success.
268
+ *
269
+ * Errors — simple or rich — go through the builder instead: construct the event
270
+ * with jsonError(code, message).hint(...)…build() (the builder is the error
271
+ * "type", so it scales to `retryable`/extra fields) and call finish(event, code).
272
+ */
273
+ finishResult(payload) {
274
+ return this.finish(jsonResult(payload).build(), 0);
275
+ }
148
276
  }
149
277
  /** Build a standard CLI version value. */
150
278
  export function buildCliVersion(version) {
151
- return jsonResult({ version }).build();
279
+ return jsonResult({ code: "version", version }).build();
152
280
  }
153
281
  /**
154
282
  * Render CLI version output.
@@ -156,14 +284,24 @@ export function buildCliVersion(version) {
156
284
  * conventional "<name> <version>" text.
157
285
  */
158
286
  export function cliRenderVersion(name, version, format = null) {
159
- const rendered = format === null ? `${name} ${version}` : cliOutput(buildCliVersion(version), format);
287
+ const rendered = format === null ? `${name} ${version}` : render(buildCliVersion(version), format);
160
288
  return `${rendered.replace(/\n+$/u, "")}\n`;
161
289
  }
162
290
  /**
163
291
  * Render version output if --version/-V is present; otherwise return undefined.
164
292
  * Throws for malformed version requests, for example `--version --output xml`.
293
+ *
294
+ * One blessed behavior, not configurable: a bare version request renders
295
+ * conventional "<name> <version>" text; `--json` or `--output <fmt>` /
296
+ * `--output=<fmt>` alongside it selects the structured AFDATA payload
297
+ * instead.
298
+ *
299
+ * Only a top-level version request is recognized: scanning stops at the first
300
+ * positional argument (the subcommand) or `--`, so `tool sub --version <value>`
301
+ * leaves `--version` for the subcommand's parser rather than printing the
302
+ * tool version.
165
303
  */
166
- export function cliHandleVersionOrContinue(rawArgs, name, version, defaultOutput = null, outputFlag = "--output", allowOutputFormat = true) {
304
+ export function cliHandleVersionOrContinue(rawArgs, name, version) {
167
305
  let versionRequested = false;
168
306
  let outputFormat;
169
307
  let outputError;
@@ -171,12 +309,17 @@ export function cliHandleVersionOrContinue(rawArgs, name, version, defaultOutput
171
309
  const arg = rawArgs[i];
172
310
  if (arg === "--")
173
311
  break;
312
+ // The first positional argument marks the subcommand boundary. Past it,
313
+ // --version and -V belong to the subcommand's own parser, matching
314
+ // git/cargo/clap: this pre-parser only owns a top-level version request.
315
+ if (!arg.startsWith("-"))
316
+ break;
174
317
  if (arg === "--version" || arg === "-V") {
175
318
  versionRequested = true;
176
319
  i += 1;
177
320
  continue;
178
321
  }
179
- if (allowOutputFormat && arg === "--json") {
322
+ if (arg === "--json") {
180
323
  if (outputFormat !== undefined && outputFormat !== "json") {
181
324
  outputError = new Error("conflicting output formats: --json conflicts with previous output format");
182
325
  }
@@ -186,24 +329,24 @@ export function cliHandleVersionOrContinue(rawArgs, name, version, defaultOutput
186
329
  i += 1;
187
330
  continue;
188
331
  }
189
- if (allowOutputFormat && (arg === outputFlag || arg.startsWith(`${outputFlag}=`))) {
332
+ if (arg === "--output" || arg.startsWith("--output=")) {
190
333
  let value;
191
334
  let step = 1;
192
- if (arg.startsWith(`${outputFlag}=`)) {
193
- value = arg.slice(outputFlag.length + 1);
335
+ if (arg.startsWith("--output=")) {
336
+ value = arg.slice("--output=".length);
194
337
  }
195
338
  else if (rawArgs[i + 1] !== undefined && !rawArgs[i + 1].startsWith("-")) {
196
339
  value = rawArgs[i + 1];
197
340
  step = 2;
198
341
  }
199
342
  if (value === undefined) {
200
- outputError = new Error(`missing value for ${outputFlag}: expected json, yaml, or plain`);
343
+ outputError = new Error("missing value for --output: expected json, yaml, or plain");
201
344
  }
202
345
  else {
203
346
  try {
204
347
  const parsedOutput = cliParseOutput(value);
205
348
  if (outputFormat !== undefined && outputFormat !== parsedOutput) {
206
- outputError = new Error(`conflicting output formats: ${outputFlag} ${value} conflicts with previous output format`);
349
+ outputError = new Error(`conflicting output formats: --output ${value} conflicts with previous output format`);
207
350
  }
208
351
  else {
209
352
  outputFormat = parsedOutput;
@@ -222,18 +365,23 @@ export function cliHandleVersionOrContinue(rawArgs, name, version, defaultOutput
222
365
  return undefined;
223
366
  if (outputError !== undefined)
224
367
  throw outputError;
225
- return cliRenderVersion(name, version, allowOutputFormat && outputFormat !== undefined ? outputFormat : defaultOutput);
368
+ return cliRenderVersion(name, version, outputFormat ?? null);
226
369
  }
227
370
  /**
228
- * Build a standard CLI parse error value.
371
+ * Build a standard CLI parse error value. This function cannot fail.
229
372
  * Use when flag parsing fails or a flag value is invalid.
230
- * Print with outputJson and exit with code 2.
373
+ * Print with render and exit with code 2.
374
+ *
375
+ * Always returns a strict-valid `kind:"error"` event with code `"cli_error"`.
376
+ * An empty `message` is replaced with a generic placeholder so the returned
377
+ * event stays strict-valid without throwing.
231
378
  *
232
379
  * @example
233
380
  * const err = buildCliError("--output: invalid value 'xml'");
234
- * console.log(outputJson(err));
381
+ * console.log(render(err, "json"));
235
382
  * process.exit(2);
236
383
  */
237
384
  export function buildCliError(message, hint) {
238
- return jsonError("cli_error", message).hintIfSome(hint).build();
385
+ const msg = message && message !== "" ? message : "unspecified error";
386
+ return jsonError("cli_error", msg).hintIfSome(hint).build();
239
387
  }
package/dist/format.d.ts CHANGED
@@ -2,9 +2,18 @@
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
4
  * Public APIs include protocol event builders, redactedValue (covers _secret
5
- * and _url fields), output formatters, redactUrlSecrets,
6
- * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47,
7
- * RedactionPolicy, OutputStyle, and OutputOptions.
5
+ * and _url fields), redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date,
6
+ * isValidRfc3339Time, isValidRfc3339, isValidBcp47, RedactionPolicy,
7
+ * PlainStyle, and OutputOptions. The single render entry point (`render`)
8
+ * lives in cli.ts and calls this module's internal JSON/YAML/plain formatters.
9
+ *
10
+ * Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
11
+ * instead of JSON.parse, so a number's exact source literal (arbitrary
12
+ * precision integers, high-precision floats, exponent forms, "-0") survives
13
+ * decode. Parsed numbers become LosslessNumber internally; every JSON/YAML
14
+ * rendering path here treats it as an opaque scalar leaf (never destructured
15
+ * as a plain object) and re-emits its exact literal text. This is internal —
16
+ * LosslessNumber is never part of the public JsonValue type or exports.
8
17
  */
9
18
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
10
19
  [key: string]: JsonValue;
@@ -28,9 +37,9 @@ export declare class EventBuildError extends Error {
28
37
  export declare class JsonResultBuilder {
29
38
  private readonly payload;
30
39
  private traceValue;
31
- private issues;
32
40
  constructor(payload: JsonValue);
33
41
  trace(value: Record<string, JsonValue> | undefined): this;
42
+ /** Build the event. This builder cannot fail. */
34
43
  build(): Event;
35
44
  }
36
45
  export declare class JsonErrorBuilder {
@@ -56,17 +65,17 @@ export declare class JsonErrorBuilder {
56
65
  export declare class JsonProgressBuilder {
57
66
  private readonly payload;
58
67
  private traceValue;
59
- private issues;
60
68
  constructor(payload: JsonValue);
61
69
  trace(value: Record<string, JsonValue> | undefined): this;
70
+ /** Build the event. This builder cannot fail. */
62
71
  build(): Event;
63
72
  }
64
73
  export declare class JsonLogBuilder {
65
74
  private readonly payload;
66
75
  private traceValue;
67
- private issues;
68
76
  constructor(payload: JsonValue);
69
77
  trace(value: Record<string, JsonValue> | undefined): this;
78
+ /** Build the event. This builder cannot fail. */
70
79
  build(): Event;
71
80
  }
72
81
  /** Fluent builder for result events. */
@@ -116,11 +125,13 @@ export declare class EventDecodeError extends Error {
116
125
  * return a typed decoded event.
117
126
  */
118
127
  export declare function decodeProtocolEvent(text: string): DecodedEvent;
128
+ /** Which fields a redaction pass scrubs. The default is `All`. */
119
129
  export declare enum RedactionPolicy {
120
- RedactionTraceOnly = "RedactionTraceOnly",
121
- RedactionNone = "RedactionNone"
130
+ All = "All",
131
+ TraceOnly = "TraceOnly",
132
+ Off = "Off"
122
133
  }
123
- export declare enum OutputStyle {
134
+ export declare enum PlainStyle {
124
135
  Readable = "Readable",
125
136
  Raw = "Raw"
126
137
  }
@@ -135,17 +146,33 @@ export type OutputOptions = {
135
146
  */
136
147
  secretNames?: readonly string[];
137
148
  };
138
- /** Rendering style for YAML/plain output. Omitted means readable. */
139
- style?: OutputStyle;
149
+ /** Rendering style for plain (logfmt) output only. Omitted means readable.
150
+ * JSON and YAML are structure-preserving and ignore this setting. */
151
+ style?: PlainStyle;
140
152
  };
141
153
  /** Convenience: build OutputOptions scoped to a RedactionPolicy. */
142
154
  export declare function outputOptionsForPolicy(policy: RedactionPolicy): OutputOptions;
143
- /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
144
- export declare function outputJson(value: JsonValue | Event, options?: OutputOptions): string;
145
- /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
146
- export declare function outputYaml(value: JsonValue | Event, options?: OutputOptions): string;
147
- /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
148
- export declare function outputPlain(value: JsonValue | Event, options?: OutputOptions): string;
155
+ /**
156
+ * Format as single-line JSON. Secrets redacted, original keys, raw values.
157
+ * Internal: called by `render` in cli.ts, which is the single public render
158
+ * entry point. Not re-exported from index.ts.
159
+ */
160
+ export declare function formatJsonValue(value: JsonValue | Event, options?: OutputOptions): string;
161
+ /**
162
+ * Format as multi-line YAML. Structure-preserving: like JSON, original keys,
163
+ * scalar types, and numeric semantics are kept after redaction; secrets are
164
+ * still redacted. `options.style` is ignored — YAML output does not vary by
165
+ * PlainStyle.
166
+ * Internal: called by `render` in cli.ts, which is the single public render
167
+ * entry point. Not re-exported from index.ts.
168
+ */
169
+ export declare function formatYamlValue(value: JsonValue | Event, options?: OutputOptions): string;
170
+ /**
171
+ * Format as single-line logfmt. Keys stripped, values formatted, secrets redacted.
172
+ * Internal: called by `render` in cli.ts, which is the single public render
173
+ * entry point. Not re-exported from index.ts.
174
+ */
175
+ export declare function formatPlainValue(value: JsonValue | Event, options?: OutputOptions): string;
149
176
  /** Return a JSON-safe copy with redaction options applied (default: full _secret redaction). */
150
177
  export declare function redactedValue(value: unknown, options?: {
151
178
  policy?: RedactionPolicy;