agent-first-data 0.12.2 → 0.13.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/README.md CHANGED
@@ -1,578 +1,46 @@
1
- # agent-first-data
2
-
3
- **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
4
-
5
- The field name is the schema. Agents read `latency_ms` and know milliseconds, `api_key_secret` and know to redact, no external schema needed.
6
-
7
- ## Installation
1
+ # Agent-First Data for TypeScript
8
2
 
9
3
  ```bash
10
4
  npm install agent-first-data
11
5
  ```
12
6
 
13
- ## Quick Example
14
-
15
- A backup tool invoked from the CLI — flags, env vars, and config all use the same suffixes:
16
-
17
- ```bash
18
- API_KEY_SECRET=sk-1234 cloudback --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz
19
- ```
20
-
21
- For CLI diagnostics, enable log categories explicitly:
22
-
23
- ```bash
24
- --log startup,request,progress,retry,redirect
25
- --verbose # shorthand for all categories
26
- ```
27
-
28
- Without these flags, startup diagnostics should stay off by default.
29
-
30
- The tool reads env vars, flags, and config — all with AFDATA suffixes — and can emit a startup diagnostic event:
31
-
32
7
  ```typescript
33
- import { buildJson, outputJson, outputYaml, outputPlain } from "agent-first-data";
8
+ import { outputJson, outputPlain } from "agent-first-data";
34
9
 
35
- const startup = buildJson(
36
- "log",
37
- {
38
- event: "startup",
39
- config: { timeout_s: 30, max_file_size_bytes: 10737418240 },
40
- args: { input_path: "/data/backup.tar.gz" },
41
- env: { API_KEY_SECRET: process.env.API_KEY_SECRET ?? null },
10
+ const value = {
11
+ code: "ok",
12
+ result: {
13
+ api_key_secret: "sk-123",
14
+ latency_ms: 1280,
15
+ db_url: "postgres://user:p@ss@db/app?token_secret=abc",
42
16
  },
43
- );
44
- ```
45
-
46
- Three output formats, same data:
47
-
48
- ```
49
- JSON: {"code":"log","event":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
50
- YAML: code: "log"
51
- event: "startup"
52
- args:
53
- input_path: "/data/backup.tar.gz"
54
- config:
55
- max_file_size: "10.0GB"
56
- timeout: "30s"
57
- env:
58
- API_KEY: "***"
59
- Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
60
- ```
61
-
62
- `--timeout-s` → `timeout_s` → `timeout: 30s`. `API_KEY_SECRET` → `API_KEY: "***"`. The suffix is the schema.
63
-
64
- ## API Reference
65
-
66
- Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
67
-
68
- ### Protocol Builders (returns JsonValue)
69
-
70
- Build AFDATA protocol structures. Return JSON-serializable objects for transport payloads.
71
-
72
- ```typescript
73
- type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
74
-
75
- // Success (result)
76
- buildJsonOk(result: JsonValue, trace?: JsonValue): JsonValue
77
-
78
- // Error (simple message, optional hint)
79
- buildJsonError(message: string, hint?: string, trace?: JsonValue): JsonValue
80
-
81
- // Generic (any code + fields)
82
- buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue
83
- ```
84
-
85
- ### Redaction Helpers (returns JsonValue)
86
-
87
- Use these before raw HTTP/MCP/SSE serializers that do not call `outputJson`.
88
-
89
- ```typescript
90
- redactedValue(value: unknown): JsonValue
91
- redactedValueWith(value: unknown, redactionPolicy: RedactionPolicy): JsonValue
92
- redactedValueWithOptions(value: unknown, redactionOptions: RedactionOptions): JsonValue
93
- ```
94
-
95
- `_url` fields are scrubbed in place during redaction (userinfo password, plus query parameters whose name follows the `_secret`/`secretNames` rule). For a URL inside a free-form string, redact it directly before interpolating:
96
-
97
- ```typescript
98
- redactUrlSecrets(url: string): string
99
- redactUrlSecretsWithOptions(url: string, redactionOptions: RedactionOptions): string
100
- ```
101
-
102
- **Use case:** structured protocol payloads (frameworks automatically serialize)
103
-
104
- **Example:**
105
- ```typescript
106
- import { buildJsonOk, buildJsonError, buildJson } from "agent-first-data";
107
-
108
- // Startup
109
- const startup = buildJson(
110
- "log",
111
- {
112
- event: "startup",
113
- config: { api_key_secret: "sk-123", timeout_s: 30 },
114
- args: { config_path: "config.yml" },
115
- env: { RUST_LOG: "info" },
116
- },
117
- );
118
-
119
- // Success (always include trace)
120
- const response = buildJsonOk(
121
- { user_id: 123 },
122
- { duration_ms: 150, source: "db" },
123
- );
124
-
125
- // Error
126
- const error = buildJsonError("user not found", undefined, { duration_ms: 5 });
127
-
128
- // Error with hint
129
- const errorHint = buildJsonError("wallet not found", "list wallets with: afpay wallet list", { duration_ms: 5 });
130
-
131
- // Specific error code
132
- const notFound = buildJson(
133
- "not_found",
134
- { resource: "user", id: 123 },
135
- { duration_ms: 8 },
136
- );
137
- ```
138
-
139
- ### Output Formatters (returns string)
140
-
141
- Format values for CLI output and logs. `outputJson` uses full `_secret` redaction by default. `outputJsonWith` supports explicit scoped policies. Use `OutputOptions` to pass legacy secret names such as `api_key` or request schema-preserving YAML/plain rendering with `OutputStyle.Raw`.
142
-
143
- ```typescript
144
- outputJson(value: JsonValue): string // Single-line JSON, original keys, for programs/logs
145
- outputJsonWith(value: JsonValue, redactionPolicy: RedactionPolicy): string
146
- outputJsonWithOptions(value: JsonValue, outputOptions: OutputOptions): string
147
- outputYaml(value: JsonValue): string // Multi-line YAML, keys stripped, values formatted
148
- outputYamlWithOptions(value: JsonValue, outputOptions: OutputOptions): string
149
- outputPlain(value: JsonValue): string // Single-line logfmt, keys stripped, values formatted
150
- outputPlainWithOptions(value: JsonValue, outputOptions: OutputOptions): string
151
- ```
152
-
153
- ```typescript
154
- enum RedactionPolicy {
155
- RedactionTraceOnly = "RedactionTraceOnly",
156
- RedactionNone = "RedactionNone",
157
- RedactionStrict = "RedactionStrict",
158
- }
159
-
160
- type RedactionOptions = {
161
- policy?: RedactionPolicy;
162
- secretNames?: readonly string[]; // legacy names like "api_key" or "authorization"
163
- }
164
-
165
- enum OutputStyle {
166
- Readable = "Readable",
167
- Raw = "Raw",
168
- }
169
-
170
- type OutputOptions = {
171
- redaction?: RedactionOptions;
172
- style?: OutputStyle;
173
- }
174
- ```
175
-
176
- Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. `OutputOptions` combines `RedactionOptions` with `OutputStyle.Readable` (default suffix stripping and value formatting) or `OutputStyle.Raw` (no suffix stripping or value formatting).
177
-
178
- **Example:**
179
- ```typescript
180
- import { outputJson, outputYaml, outputPlain } from "agent-first-data";
181
-
182
- const data = {
183
- user_id: 123,
184
- api_key_secret: "sk-1234567890abcdef",
185
- created_at_epoch_ms: 1738886400000,
186
- file_size_bytes: 5242880,
187
17
  };
188
18
 
189
- // JSON (secrets redacted, original keys, raw values)
190
- console.log(outputJson(data));
191
- // {"api_key_secret":"***","created_at_epoch_ms":1738886400000,"file_size_bytes":5242880,"user_id":123}
192
-
193
- // YAML (keys stripped, values formatted, secrets redacted)
194
- console.log(outputYaml(data));
195
- // ---
196
- // api_key: "***"
197
- // created_at: "2025-02-07T00:00:00.000Z"
198
- // file_size: "5.0MB"
199
- // user_id: 123
200
-
201
- // Plain logfmt (keys stripped, values formatted, secrets redacted)
202
- console.log(outputPlain(data));
203
- // api_key=*** created_at=2025-02-07T00:00:00.000Z file_size=5.0MB user_id=123
19
+ console.log(outputJson(value));
20
+ console.log(outputPlain(value));
204
21
  ```
205
22
 
206
- ### Internal Tools
23
+ Useful names use TypeScript casing: `outputJson`, `outputYaml`, `outputPlain`, `outputJsonWithOptions`, `redactedValue`, `redactSecretsInPlace`, `redactUrlSecrets`, `parseSize`, `normalizeUtcOffset`, `cliParseOutput`, `cliOutput`, and `buildCliError`.
207
24
 
208
- ```typescript
209
- internalRedactSecrets(value: JsonValue): void // Manually redact secrets in-place
210
- internalRedactSecretsWithOptions(value: JsonValue, redactionOptions: RedactionOptions): void
211
- ```
212
-
213
- Most users don't need this. Output functions automatically protect secrets.
214
-
215
- ### Utility Functions
216
-
217
- ```typescript
218
- parseSize(s: string): number | null // Parse "10M" → bytes
219
- ```
220
-
221
- Returns `null` for invalid, negative, overflow, or non-safe-integer values.
222
-
223
- **Example:**
224
- ```typescript
225
- import { parseSize } from "agent-first-data";
226
-
227
- parseSize("10M"); // 10485760
228
- parseSize("1.5K"); // 1536
229
- parseSize("512"); // 512
230
- ```
231
-
232
- ### CLI Helpers (for tools built on AFDATA)
233
-
234
- Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
235
-
236
- Help scope and format are orthogonal: `--recursive` decides one-level vs
237
- recursive, `--output` decides plain/json/yaml/markdown. Human-facing CLIs use
238
- one-level `--help` (and scoped `sub --help`); agent/doc flows use
239
- `--help --recursive`, adding `--output json|yaml|markdown` for a recursive
240
- export. A bare `--recursive` without `--help` is ignored by the help layer.
241
- See `examples/agent_cli.ts` for that pattern.
25
+ Logging is available through `initJson`, `initPlain`, `initYaml`, `span`, and `log`.
242
26
 
243
27
  ```typescript
244
- type OutputFormat = "json" | "yaml" | "plain"
28
+ import { initJson } from "agent-first-data";
245
29
 
246
- cliParseOutput(s: string): OutputFormat // Parse --output flag; throws on unknown
247
- cliParseLogFilters(entries: string[]): string[] // Normalize --log: trim, lowercase, dedup, remove empty
248
- cliOutput(value: JsonValue, format: OutputFormat): string // Dispatch to outputJson/Yaml/Plain
249
- cliOutputWithOptions(value: JsonValue, format: OutputFormat, outputOptions: OutputOptions): string
250
- buildCliError(message: string, hint?: string): JsonValue // {code:"error", error_code:"invalid_request", hint?, retryable:false, trace:{duration_ms:0}}
30
+ initJson({ secretNames: ["authorization"] });
251
31
  ```
252
32
 
253
- **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
254
-
255
- ```typescript
256
- import {
257
- type OutputFormat, cliParseOutput, cliParseLogFilters,
258
- cliOutput, buildCliError, outputJson,
259
- } from "agent-first-data";
260
-
261
- let fmt: OutputFormat;
262
- try {
263
- fmt = cliParseOutput(outputArg);
264
- } catch (e) {
265
- console.log(outputJson(buildCliError((e as Error).message)));
266
- process.exit(2);
267
- }
33
+ ## Behavior Notes
268
34
 
269
- const log = cliParseLogFilters(logArg ? logArg.split(",") : []);
270
- // ... do work ...
271
- console.log(cliOutput(result, fmt));
272
- ```
273
-
274
- See `examples/agent_cli.ts` for the complete working example (`npx tsx --test examples/agent_cli.ts`).
275
-
276
- ### Skill Admin (for spore CLIs that ship an Agent Skill)
277
-
278
- `runSkillAdmin` installs, uninstalls, and reports status of a spore's embedded `SKILL.md` across Codex, Claude Code, and opencode. Describe the skill once with a `SkillSpec`, then call it per action. It returns a typed `SkillReport` discriminated union (narrow on `code` to read fields, or serialize with `cliOutput`) or throws `SkillError`; it never writes to stdout/stderr. The generated `SKILL.md` is byte-identical to the Rust, Go, and Python ports.
279
-
280
- ```typescript
281
- runSkillAdmin(spec: SkillSpec, action: SkillAction, options: SkillOptions): SkillReport
282
-
283
- const spec: SkillSpec = { name: "agent-first-widget", source: skillMarkdown, title: "Agent-First Widget", markerSlug: "afwidget" };
284
- const options: SkillOptions = { agent: "all", scope: "personal" };
285
-
286
- try {
287
- const report = runSkillAdmin(spec, "install", options);
288
- console.log(cliOutput(report as unknown as JsonValue, fmt));
289
- } catch (e) {
290
- if (e instanceof SkillError) {
291
- console.log(cliOutput(buildCliError(e.message, e.hint), fmt));
292
- process.exit(1);
293
- }
294
- throw e;
295
- }
296
- ```
297
-
298
- `status` reports `installed` / `valid` / `managed` / `current` per target; `current` is true only when the installed content matches the bundle, so re-running `install` refreshes a stale copy. See `examples/agent_cli.ts` for the `skill` subcommand wiring.
299
-
300
- ## Usage Examples
301
-
302
- ### Example 1: REST API
303
-
304
- ```typescript
305
- import { buildJsonOk } from "agent-first-data";
306
- import express from "express";
307
-
308
- const app = express();
309
-
310
- app.get("/users/:id", (req, res) => {
311
- const response = buildJsonOk(
312
- { user_id: Number(req.params.id), name: "alice" },
313
- { duration_ms: 150, source: "db" },
314
- );
315
- // API returns raw JSON — no output processing, no key stripping
316
- res.json(response);
317
- });
318
- ```
319
-
320
- ### Example 2: CLI Tool (Complete Lifecycle)
321
-
322
- ```typescript
323
- import { buildJsonOk, buildJson, outputYaml, outputPlain } from "agent-first-data";
324
-
325
- // 1. Startup
326
- const startup = buildJson(
327
- "log",
328
- {
329
- event: "startup",
330
- config: { api_key_secret: "sk-sensitive-key", timeout_s: 30 },
331
- args: { input_path: "data.json" },
332
- env: { RUST_LOG: "info" },
333
- },
334
- );
335
- console.log(outputYaml(startup));
336
- // ---
337
- // code: "log"
338
- // event: "startup"
339
- // args:
340
- // input_path: "data.json"
341
- // config:
342
- // api_key: "***"
343
- // timeout: "30s"
344
- // env:
345
- // RUST_LOG: "info"
346
-
347
- // 2. Progress
348
- const progress = buildJson(
349
- "progress",
350
- { current: 3, total: 10, message: "processing" },
351
- { duration_ms: 1500 },
352
- );
353
- console.log(outputPlain(progress));
354
- // code=progress current=3 message=processing total=10 trace.duration=1.5s
355
-
356
- // 3. Result
357
- const result = buildJsonOk(
358
- {
359
- records_processed: 10,
360
- file_size_bytes: 5242880,
361
- created_at_epoch_ms: 1738886400000,
362
- },
363
- { duration_ms: 3500, source: "file" },
364
- );
365
- console.log(outputYaml(result));
366
- // ---
367
- // code: "ok"
368
- // result:
369
- // created_at: "2025-02-07T00:00:00.000Z"
370
- // file_size: "5.0MB"
371
- // records_processed: 10
372
- // trace:
373
- // duration: "3.5s"
374
- // source: "file"
375
- ```
376
-
377
- ### Example 3: JSONL Output
378
-
379
- ```typescript
380
- import { buildJsonOk, outputJson } from "agent-first-data";
381
-
382
- const result = buildJsonOk(
383
- { status: "success" },
384
- { duration_ms: 250, api_key_secret: "sk-123" },
385
- );
386
-
387
- // Print JSONL to stdout (secrets redacted, one JSON object per line)
388
- // Channel policy: machine-readable protocol/log events must not use stderr.
389
- console.log(outputJson(result));
390
- // {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
391
- ```
392
-
393
- ## Complete Suffix Example
394
-
395
- ```typescript
396
- import { outputYaml, outputPlain } from "agent-first-data";
397
-
398
- const data = {
399
- created_at_epoch_ms: 1738886400000,
400
- request_timeout_ms: 5000,
401
- cache_ttl_s: 3600,
402
- file_size_bytes: 5242880,
403
- payment_msats: 50000000,
404
- price_usd_cents: 9999,
405
- success_rate_percent: 95.5,
406
- api_key_secret: "sk-1234567890abcdef",
407
- user_name: "alice",
408
- count: 42,
409
- };
410
-
411
- // YAML output (keys stripped, values formatted, secrets redacted)
412
- console.log(outputYaml(data));
413
- // ---
414
- // api_key: "***"
415
- // cache_ttl: "3600s"
416
- // count: 42
417
- // created_at: "2025-02-07T00:00:00.000Z"
418
- // file_size: "5.0MB"
419
- // payment: "50000000msats"
420
- // price: "$99.99"
421
- // request_timeout: "5.0s"
422
- // success_rate: "95.5%"
423
- // user_name: "alice"
424
-
425
- // Plain logfmt output (same transformations, single line)
426
- console.log(outputPlain(data));
427
- // api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
428
- ```
429
-
430
- ## AFDATA Logging
431
-
432
- AFDATA-compliant structured logging. Every log line is formatted using the library's own `outputJson`/`outputPlain`/`outputYaml` functions. Span fields are carried via `AsyncLocalStorage` (async-safe), automatically flattened into each log line. Zero dependencies beyond Node.js built-ins.
433
-
434
- ### API
435
-
436
- ```typescript
437
- import { log, span, initJson, initPlain, initYaml } from "agent-first-data";
438
-
439
- // Format selectors — set the output format for all subsequent log calls
440
- initJson() // Single-line JSONL (secrets redacted, original keys) — default
441
- initPlain() // Single-line logfmt (keys stripped, values formatted)
442
- initYaml() // Multi-line YAML (keys stripped, values formatted)
443
-
444
- // Logger — each method outputs a single log line to stdout
445
- log.trace(msg, fields?)
446
- log.debug(msg, fields?)
447
- log.info(msg, fields?)
448
- log.warn(msg, fields?)
449
- log.error(msg, fields?)
450
-
451
- // Span — run fn with additional fields on all log events
452
- span<T>(fields, fn: () => T): T // works with sync and async functions
453
- ```
454
-
455
- ### Setup
456
-
457
- ```typescript
458
- import { log, initJson, initPlain, initYaml } from "agent-first-data";
459
-
460
- // JSON output for production (one JSONL line per event, secrets redacted)
461
- initJson(); // default, can be omitted
462
-
463
- // Plain logfmt for development (keys stripped, values formatted)
464
- initPlain();
465
-
466
- // YAML for detailed inspection (multi-line, keys stripped, values formatted)
467
- initYaml();
468
- ```
469
-
470
- ### Log Output
471
-
472
- Output format depends on the init function used.
473
-
474
- ```typescript
475
- log.info("Server started");
476
- // JSON: {"timestamp_epoch_ms":1739000000000,"message":"Server started","code":"info"}
477
- // Plain: code=info message="Server started" timestamp_epoch_ms=1739000000000
478
- // YAML: ---
479
- // code: "info"
480
- // message: "Server started"
481
- // timestamp_epoch_ms: 1739000000000
482
-
483
- log.warn("DNS lookup failed", { error: "timeout", domain: "example.com" });
484
- // JSON: {"timestamp_epoch_ms":...,"message":"DNS lookup failed","error":"timeout","domain":"example.com","code":"warn"}
485
- // Plain: code=warn domain=example.com error=timeout message="DNS lookup failed" ...
486
- ```
487
-
488
- ### Span Support
489
-
490
- Use `span()` to add fields to all log events within the callback. Spans nest and work with both sync and async functions.
491
-
492
- ```typescript
493
- import { log, span } from "agent-first-data";
494
-
495
- await span({ request_id: "abc-123" }, async () => {
496
- log.info("Processing");
497
- // {"timestamp_epoch_ms":...,"message":"Processing","request_id":"abc-123","code":"info"}
498
-
499
- await span({ step: "validate" }, async () => {
500
- log.info("Validating input");
501
- // {"timestamp_epoch_ms":...,"message":"Validating input","request_id":"abc-123","step":"validate","code":"info"}
502
- });
503
- });
504
- ```
505
-
506
- ### Custom Code Override
507
-
508
- The `code` field defaults to the log level. Override with an explicit field:
509
-
510
- ```typescript
511
- log.info("Server ready", { code: "log", event: "startup" });
512
- // {"timestamp_epoch_ms":...,"message":"Server ready","code":"log","event":"startup"}
513
- ```
514
-
515
- ### Output Fields
516
-
517
- Every log line contains:
518
-
519
- | Field | Type | Description |
520
- |:------|:-----|:------------|
521
- | `timestamp_epoch_ms` | number | Unix milliseconds |
522
- | `message` | string | Log message |
523
- | `code` | string | Level (trace/debug/info/warn/error) or explicit override |
524
- | *span fields* | any | From `span()` callback |
525
- | *event fields* | any | From fields argument |
526
-
527
- ### Log Output Formats
528
-
529
- All three formats use the library's own output functions, so AFDATA suffix processing applies to log fields too:
530
-
531
- | Format | Function | Keys | Values | Use case |
532
- |:-------|:---------|:-----|:-------|:---------|
533
- | **JSON** | `initJson` | original (with suffix) | raw | production, log aggregation |
534
- | **Plain** | `initPlain` | stripped | formatted | development, compact scanning |
535
- | **YAML** | `initYaml` | stripped | formatted | debugging, detailed inspection |
536
-
537
- All formats automatically redact `_secret` fields in log output.
538
-
539
- ## Output Formats
540
-
541
- Three output formats for different use cases:
542
-
543
- | Format | Structure | Keys | Values | Use case |
544
- |:-------|:----------|:-----|:-------|:---------|
545
- | **JSON** | single-line | original (with suffix) | raw | programs, logs |
546
- | **YAML** | multi-line | stripped | formatted | human inspection |
547
- | **Plain** | single-line logfmt | stripped | formatted | compact scanning |
548
-
549
- All formats automatically redact `_secret` fields.
550
-
551
- ## Supported Suffixes
552
-
553
- - **Duration**: `_ms`, `_s`, `_ns`, `_us`, `_minutes`, `_hours`, `_days`
554
- - **Timestamps**: `_epoch_ms`, `_epoch_s`, `_epoch_ns`, `_rfc3339`
555
- - **Size**: `_bytes` (auto-scales to KB/MB/GB/TB), `_size` (config input, pass through)
556
- - **Currency**: `_msats`, `_sats`, `_btc`, `_usd_cents`, `_eur_cents`, `_jpy`, `_{code}_cents`
557
- - **Other**: `_percent`, `_secret` (auto-redacted in all formats)
558
-
559
- TypeScript uses JavaScript `number` for JSON values, so `_epoch_ns` values above `Number.MAX_SAFE_INTEGER` lose precision. Prefer `_epoch_ms` for JSON interchange, or convert nanosecond values before formatting.
560
-
561
- ## Repository
562
-
563
- This package is part of the [agent-first-data](https://github.com/agentfirstkit/agent-first-data) repository, which also contains:
564
-
565
- - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
566
- - **`skills/`** — AI coding agent skill for working with AFDATA conventions
567
-
568
- To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
569
-
570
- ```bash
571
- git clone https://github.com/agentfirstkit/agent-first-data
572
- cd agent-first-data/typescript
573
- npm test
574
- ```
35
+ - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
36
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
37
+ - YAML/plain quote and escape keys as well as values, sort by UTF-16 code unit order, and render nested objects in arrays as canonical JSON.
38
+ - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
39
+ - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
575
40
 
576
- ## License
41
+ ## Reference
577
42
 
578
- MIT
43
+ - Full convention and API groups: [../docs/overview.md](../docs/overview.md)
44
+ - Formal cross-language contract: [../spec/agent-first-data.md](../spec/agent-first-data.md)
45
+ - Conformance fixtures: [../spec/fixtures](../spec/fixtures)
46
+ - Agent skill: [../skills/agent-first-data.md](../skills/agent-first-data.md)
@@ -16,12 +16,15 @@
16
16
  * log.info("Processing", { domain: "example.com" });
17
17
  * });
18
18
  */
19
+ type LoggingRedactionOptions = {
20
+ secretNames?: readonly string[];
21
+ };
19
22
  /** Set log output to single-line JSONL (secrets redacted, original keys). */
20
- export declare function initJson(): void;
23
+ export declare function initJson(options?: LoggingRedactionOptions): void;
21
24
  /** Set log output to single-line logfmt (keys stripped, values formatted). */
22
- export declare function initPlain(): void;
25
+ export declare function initPlain(options?: LoggingRedactionOptions): void;
23
26
  /** Set log output to multi-line YAML (keys stripped, values formatted). */
24
- export declare function initYaml(): void;
27
+ export declare function initYaml(options?: LoggingRedactionOptions): void;
25
28
  /**
26
29
  * AFDATA logger. Each method outputs a single log line to stdout.
27
30
  * Format is controlled by initJson/initPlain/initYaml (default: JSON).
@@ -38,3 +41,4 @@ export declare const log: {
38
41
  * Spans nest: inner spans inherit and can override outer span fields.
39
42
  */
40
43
  export declare function span<T>(fields: Record<string, unknown>, fn: () => T): T;
44
+ export {};
@@ -17,19 +17,26 @@
17
17
  * });
18
18
  */
19
19
  import { AsyncLocalStorage } from "node:async_hooks";
20
- import { outputJson, outputYaml, outputPlain } from "./format.js";
20
+ import { outputJsonWithOptions, outputYamlWithOptions, outputPlainWithOptions, } from "./format.js";
21
21
  const spanStore = new AsyncLocalStorage();
22
22
  let currentFormat = "json";
23
+ let currentRedaction = {};
24
+ function setFormat(format, options) {
25
+ currentFormat = format;
26
+ currentRedaction = options?.secretNames ? { secretNames: options.secretNames } : {};
27
+ }
23
28
  /** Set log output to single-line JSONL (secrets redacted, original keys). */
24
- export function initJson() { currentFormat = "json"; }
29
+ export function initJson(options) { setFormat("json", options); }
25
30
  /** Set log output to single-line logfmt (keys stripped, values formatted). */
26
- export function initPlain() { currentFormat = "plain"; }
31
+ export function initPlain(options) { setFormat("plain", options); }
27
32
  /** Set log output to multi-line YAML (keys stripped, values formatted). */
28
- export function initYaml() { currentFormat = "yaml"; }
33
+ export function initYaml(options) { setFormat("yaml", options); }
29
34
  function emit(level, message, fields) {
30
35
  const entry = {
31
36
  timestamp_epoch_ms: Date.now(),
32
37
  message,
38
+ code: "log",
39
+ level,
33
40
  };
34
41
  // Span fields (from AsyncLocalStorage)
35
42
  const spanFields = spanStore.getStore();
@@ -38,31 +45,26 @@ function emit(level, message, fields) {
38
45
  entry[k] = v;
39
46
  }
40
47
  }
41
- // Event fields (override span fields on collision)
42
- let hasCode = false;
48
+ // Event fields override span fields, except protocol code is always "log".
43
49
  if (fields) {
44
50
  for (const [k, v] of Object.entries(fields)) {
45
51
  if (k === "code")
46
- hasCode = true;
52
+ continue;
47
53
  entry[k] = v;
48
54
  }
49
55
  }
50
- // Default code from level
51
- if (!hasCode) {
52
- entry["code"] = level;
53
- }
54
56
  // Format using the library's own output functions
55
57
  const value = entry;
56
58
  let line;
57
59
  switch (currentFormat) {
58
60
  case "plain":
59
- line = outputPlain(value);
61
+ line = outputPlainWithOptions(value, { redaction: currentRedaction });
60
62
  break;
61
63
  case "yaml":
62
- line = outputYaml(value);
64
+ line = outputYamlWithOptions(value, { redaction: currentRedaction });
63
65
  break;
64
66
  default:
65
- line = outputJson(value);
67
+ line = outputJsonWithOptions(value, { redaction: currentRedaction });
66
68
  break;
67
69
  }
68
70
  process.stdout.write(line + "\n");