agent-first-data 0.1.3 → 0.2.1

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 ADDED
@@ -0,0 +1,383 @@
1
+ # agent-first-data
2
+
3
+ **Agent-First Data (AFD)** — 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
8
+
9
+ ```bash
10
+ npm install agent-first-data
11
+ ```
12
+
13
+ ## API Reference
14
+
15
+ Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
16
+
17
+ ### Protocol Builders (returns JsonValue)
18
+
19
+ Build AFD protocol structures. Return JSON-serializable objects for API responses.
20
+
21
+ ```typescript
22
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
23
+
24
+ // Startup (configuration)
25
+ buildJsonStartup(config: JsonValue, args: JsonValue, env: JsonValue): JsonValue
26
+
27
+ // Success (result)
28
+ buildJsonOk(result: JsonValue, trace?: JsonValue): JsonValue
29
+
30
+ // Error (simple message)
31
+ buildJsonError(message: string, trace?: JsonValue): JsonValue
32
+
33
+ // Generic (any code + fields)
34
+ buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue
35
+ ```
36
+
37
+ **Use case:** API responses (frameworks like Express automatically serialize)
38
+
39
+ **Example:**
40
+ ```typescript
41
+ import { buildJsonStartup, buildJsonOk, buildJsonError, buildJson } from "agent-first-data";
42
+
43
+ // Startup
44
+ const startup = buildJsonStartup(
45
+ { api_key_secret: "sk-123", timeout_s: 30 },
46
+ { config_path: "config.yml" },
47
+ { RUST_LOG: "info" },
48
+ );
49
+
50
+ // Success (always include trace)
51
+ const response = buildJsonOk(
52
+ { user_id: 123 },
53
+ { duration_ms: 150, source: "db" },
54
+ );
55
+
56
+ // Error
57
+ const error = buildJsonError("user not found", { duration_ms: 5 });
58
+
59
+ // Specific error code
60
+ const notFound = buildJson(
61
+ "not_found",
62
+ { resource: "user", id: 123 },
63
+ { duration_ms: 8 },
64
+ );
65
+ ```
66
+
67
+ ### CLI/Log Output (returns string)
68
+
69
+ Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
70
+
71
+ ```typescript
72
+ outputJson(value: JsonValue): string // Single-line JSON, original keys, for programs/logs
73
+ outputYaml(value: JsonValue): string // Multi-line YAML, keys stripped, values formatted
74
+ outputPlain(value: JsonValue): string // Single-line logfmt, keys stripped, values formatted
75
+ ```
76
+
77
+ **Example:**
78
+ ```typescript
79
+ import { outputJson, outputYaml, outputPlain } from "agent-first-data";
80
+
81
+ const data = {
82
+ user_id: 123,
83
+ api_key_secret: "sk-1234567890abcdef",
84
+ created_at_epoch_ms: 1738886400000,
85
+ file_size_bytes: 5242880,
86
+ };
87
+
88
+ // JSON (secrets redacted, original keys, raw values)
89
+ console.log(outputJson(data));
90
+ // {"api_key_secret":"***","created_at_epoch_ms":1738886400000,"file_size_bytes":5242880,"user_id":123}
91
+
92
+ // YAML (keys stripped, values formatted, secrets redacted)
93
+ console.log(outputYaml(data));
94
+ // ---
95
+ // api_key: "***"
96
+ // created_at: "2025-02-07T00:00:00.000Z"
97
+ // file_size: "5.0MB"
98
+ // user_id: 123
99
+
100
+ // Plain logfmt (keys stripped, values formatted, secrets redacted)
101
+ console.log(outputPlain(data));
102
+ // api_key=*** created_at=2025-02-07T00:00:00.000Z file_size=5.0MB user_id=123
103
+ ```
104
+
105
+ ### Internal Tools
106
+
107
+ ```typescript
108
+ internalRedactSecrets(value: JsonValue): void // Manually redact secrets in-place
109
+ ```
110
+
111
+ Most users don't need this. Output functions automatically protect secrets.
112
+
113
+ ### Utility Functions
114
+
115
+ ```typescript
116
+ parseSize(s: string): number | null // Parse "10M" → bytes
117
+ ```
118
+
119
+ **Example:**
120
+ ```typescript
121
+ import { parseSize } from "agent-first-data";
122
+
123
+ parseSize("10M"); // 10485760
124
+ parseSize("1.5K"); // 1536
125
+ parseSize("512"); // 512
126
+ ```
127
+
128
+ ## Usage Examples
129
+
130
+ ### Example 1: REST API
131
+
132
+ ```typescript
133
+ import { buildJsonOk } from "agent-first-data";
134
+ import express from "express";
135
+
136
+ const app = express();
137
+
138
+ app.get("/users/:id", (req, res) => {
139
+ const response = buildJsonOk(
140
+ { user_id: Number(req.params.id), name: "alice" },
141
+ { duration_ms: 150, source: "db" },
142
+ );
143
+ // API returns raw JSON — no output processing, no key stripping
144
+ res.json(response);
145
+ });
146
+ ```
147
+
148
+ ### Example 2: CLI Tool (Complete Lifecycle)
149
+
150
+ ```typescript
151
+ import { buildJsonStartup, buildJsonOk, buildJson, outputYaml, outputPlain } from "agent-first-data";
152
+
153
+ // 1. Startup
154
+ const startup = buildJsonStartup(
155
+ { api_key_secret: "sk-sensitive-key", timeout_s: 30 },
156
+ { input_path: "data.json" },
157
+ { RUST_LOG: "info" },
158
+ );
159
+ console.log(outputYaml(startup));
160
+ // ---
161
+ // code: "startup"
162
+ // args:
163
+ // input_path: "data.json"
164
+ // config:
165
+ // api_key: "***"
166
+ // timeout: "30s"
167
+ // env:
168
+ // RUST_LOG: "info"
169
+
170
+ // 2. Progress
171
+ const progress = buildJson(
172
+ "progress",
173
+ { current: 3, total: 10, message: "processing" },
174
+ { duration_ms: 1500 },
175
+ );
176
+ console.log(outputPlain(progress));
177
+ // code=progress current=3 message=processing total=10 trace.duration=1.5s
178
+
179
+ // 3. Result
180
+ const result = buildJsonOk(
181
+ {
182
+ records_processed: 10,
183
+ file_size_bytes: 5242880,
184
+ created_at_epoch_ms: 1738886400000,
185
+ },
186
+ { duration_ms: 3500, source: "file" },
187
+ );
188
+ console.log(outputYaml(result));
189
+ // ---
190
+ // code: "ok"
191
+ // result:
192
+ // created_at: "2025-02-07T00:00:00.000Z"
193
+ // file_size: "5.0MB"
194
+ // records_processed: 10
195
+ // trace:
196
+ // duration: "3.5s"
197
+ // source: "file"
198
+ ```
199
+
200
+ ### Example 3: JSONL Output
201
+
202
+ ```typescript
203
+ import { buildJsonOk, outputJson } from "agent-first-data";
204
+
205
+ const result = buildJsonOk(
206
+ { status: "success" },
207
+ { duration_ms: 250, api_key_secret: "sk-123" },
208
+ );
209
+
210
+ // Print JSONL to stdout (secrets redacted, one JSON object per line)
211
+ console.log(outputJson(result));
212
+ // {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
213
+ ```
214
+
215
+ ## Complete Suffix Example
216
+
217
+ ```typescript
218
+ import { outputYaml, outputPlain } from "agent-first-data";
219
+
220
+ const data = {
221
+ created_at_epoch_ms: 1738886400000,
222
+ request_timeout_ms: 5000,
223
+ cache_ttl_s: 3600,
224
+ file_size_bytes: 5242880,
225
+ payment_msats: 50000000,
226
+ price_usd_cents: 9999,
227
+ success_rate_percent: 95.5,
228
+ api_key_secret: "sk-1234567890abcdef",
229
+ user_name: "alice",
230
+ count: 42,
231
+ };
232
+
233
+ // YAML output (keys stripped, values formatted, secrets redacted)
234
+ console.log(outputYaml(data));
235
+ // ---
236
+ // api_key: "***"
237
+ // cache_ttl: "3600s"
238
+ // count: 42
239
+ // created_at: "2025-02-07T00:00:00.000Z"
240
+ // file_size: "5.0MB"
241
+ // payment: "50000000msats"
242
+ // price: "$99.99"
243
+ // request_timeout: "5.0s"
244
+ // success_rate: "95.5%"
245
+ // user_name: "alice"
246
+
247
+ // Plain logfmt output (same transformations, single line)
248
+ console.log(outputPlain(data));
249
+ // 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
250
+ ```
251
+
252
+ ## AFD Logging
253
+
254
+ AFD-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.
255
+
256
+ ### API
257
+
258
+ ```typescript
259
+ import { log, span, initJson, initPlain, initYaml } from "agent-first-data";
260
+
261
+ // Format selectors — set the output format for all subsequent log calls
262
+ initJson() // Single-line JSONL (secrets redacted, original keys) — default
263
+ initPlain() // Single-line logfmt (keys stripped, values formatted)
264
+ initYaml() // Multi-line YAML (keys stripped, values formatted)
265
+
266
+ // Logger — each method outputs a single log line to stdout
267
+ log.trace(msg, fields?)
268
+ log.debug(msg, fields?)
269
+ log.info(msg, fields?)
270
+ log.warn(msg, fields?)
271
+ log.error(msg, fields?)
272
+
273
+ // Span — run fn with additional fields on all log events
274
+ span<T>(fields, fn: () => T): T // works with sync and async functions
275
+ ```
276
+
277
+ ### Setup
278
+
279
+ ```typescript
280
+ import { log, initJson, initPlain, initYaml } from "agent-first-data";
281
+
282
+ // JSON output for production (one JSONL line per event, secrets redacted)
283
+ initJson(); // default, can be omitted
284
+
285
+ // Plain logfmt for development (keys stripped, values formatted)
286
+ initPlain();
287
+
288
+ // YAML for detailed inspection (multi-line, keys stripped, values formatted)
289
+ initYaml();
290
+ ```
291
+
292
+ ### Log Output
293
+
294
+ Output format depends on the init function used.
295
+
296
+ ```typescript
297
+ log.info("Server started");
298
+ // JSON: {"timestamp_epoch_ms":1739000000000,"message":"Server started","code":"info"}
299
+ // Plain: code=info message="Server started" timestamp_epoch_ms=1739000000000
300
+ // YAML: ---
301
+ // code: "info"
302
+ // message: "Server started"
303
+ // timestamp_epoch_ms: 1739000000000
304
+
305
+ log.warn("DNS lookup failed", { error: "timeout", domain: "example.com" });
306
+ // JSON: {"timestamp_epoch_ms":...,"message":"DNS lookup failed","error":"timeout","domain":"example.com","code":"warn"}
307
+ // Plain: code=warn domain=example.com error=timeout message="DNS lookup failed" ...
308
+ ```
309
+
310
+ ### Span Support
311
+
312
+ Use `span()` to add fields to all log events within the callback. Spans nest and work with both sync and async functions.
313
+
314
+ ```typescript
315
+ import { log, span } from "agent-first-data";
316
+
317
+ await span({ request_id: "abc-123" }, async () => {
318
+ log.info("Processing");
319
+ // {"timestamp_epoch_ms":...,"message":"Processing","request_id":"abc-123","code":"info"}
320
+
321
+ await span({ step: "validate" }, async () => {
322
+ log.info("Validating input");
323
+ // {"timestamp_epoch_ms":...,"message":"Validating input","request_id":"abc-123","step":"validate","code":"info"}
324
+ });
325
+ });
326
+ ```
327
+
328
+ ### Custom Code Override
329
+
330
+ The `code` field defaults to the log level. Override with an explicit field:
331
+
332
+ ```typescript
333
+ log.info("Server ready", { code: "startup" });
334
+ // {"timestamp_epoch_ms":...,"message":"Server ready","code":"startup"}
335
+ ```
336
+
337
+ ### Output Fields
338
+
339
+ Every log line contains:
340
+
341
+ | Field | Type | Description |
342
+ |:------|:-----|:------------|
343
+ | `timestamp_epoch_ms` | number | Unix milliseconds |
344
+ | `message` | string | Log message |
345
+ | `code` | string | Level (trace/debug/info/warn/error) or explicit override |
346
+ | *span fields* | any | From `span()` callback |
347
+ | *event fields* | any | From fields argument |
348
+
349
+ ### Log Output Formats
350
+
351
+ All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
352
+
353
+ | Format | Function | Keys | Values | Use case |
354
+ |:-------|:---------|:-----|:-------|:---------|
355
+ | **JSON** | `initJson` | original (with suffix) | raw | production, log aggregation |
356
+ | **Plain** | `initPlain` | stripped | formatted | development, compact scanning |
357
+ | **YAML** | `initYaml` | stripped | formatted | debugging, detailed inspection |
358
+
359
+ All formats automatically redact `_secret` fields in log output.
360
+
361
+ ## Output Formats
362
+
363
+ Three output formats for different use cases:
364
+
365
+ | Format | Structure | Keys | Values | Use case |
366
+ |:-------|:----------|:-----|:-------|:---------|
367
+ | **JSON** | single-line | original (with suffix) | raw | programs, logs |
368
+ | **YAML** | multi-line | stripped | formatted | human inspection |
369
+ | **Plain** | single-line logfmt | stripped | formatted | compact scanning |
370
+
371
+ All formats automatically redact `_secret` fields.
372
+
373
+ ## Supported Suffixes
374
+
375
+ - **Duration**: `_ms`, `_s`, `_ns`, `_us`, `_minutes`, `_hours`, `_days`
376
+ - **Timestamps**: `_epoch_ms`, `_epoch_s`, `_epoch_ns`, `_rfc3339`
377
+ - **Size**: `_bytes` (auto-scales to KB/MB/GB/TB), `_size` (config input, pass through)
378
+ - **Currency**: `_msats`, `_sats`, `_btc`, `_usd_cents`, `_eur_cents`, `_jpy`, `_{code}_cents`
379
+ - **Other**: `_percent`, `_secret` (auto-redacted in all formats)
380
+
381
+ ## License
382
+
383
+ MIT
@@ -0,0 +1,40 @@
1
+ /**
2
+ * AFD-compliant structured logging.
3
+ *
4
+ * Outputs log events using agent-first-data formatting functions:
5
+ * - JSON: single-line JSONL via outputJson (secrets redacted, original keys)
6
+ * - Plain: single-line logfmt via outputPlain (keys stripped, values formatted)
7
+ * - YAML: multi-line via outputYaml (keys stripped, values formatted)
8
+ *
9
+ * Span fields are carried via AsyncLocalStorage (async-safe).
10
+ *
11
+ * Usage:
12
+ * import { log, span, initJson, initPlain, initYaml } from "agent-first-data/afd_logging";
13
+ * initJson(); // or initPlain() or initYaml()
14
+ * log.info("Server started");
15
+ * await span({ request_id: "abc" }, async () => {
16
+ * log.info("Processing", { domain: "example.com" });
17
+ * });
18
+ */
19
+ /** Set log output to single-line JSONL (secrets redacted, original keys). */
20
+ export declare function initJson(): void;
21
+ /** Set log output to single-line logfmt (keys stripped, values formatted). */
22
+ export declare function initPlain(): void;
23
+ /** Set log output to multi-line YAML (keys stripped, values formatted). */
24
+ export declare function initYaml(): void;
25
+ /**
26
+ * AFD logger. Each method outputs a single log line to stdout.
27
+ * Format is controlled by initJson/initPlain/initYaml (default: JSON).
28
+ */
29
+ export declare const log: {
30
+ trace: (msg: string, fields?: Record<string, unknown>) => void;
31
+ debug: (msg: string, fields?: Record<string, unknown>) => void;
32
+ info: (msg: string, fields?: Record<string, unknown>) => void;
33
+ warn: (msg: string, fields?: Record<string, unknown>) => void;
34
+ error: (msg: string, fields?: Record<string, unknown>) => void;
35
+ };
36
+ /**
37
+ * Run fn within a span that adds fields to all log events.
38
+ * Spans nest: inner spans inherit and can override outer span fields.
39
+ */
40
+ export declare function span<T>(fields: Record<string, unknown>, fn: () => T): T;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * AFD-compliant structured logging.
3
+ *
4
+ * Outputs log events using agent-first-data formatting functions:
5
+ * - JSON: single-line JSONL via outputJson (secrets redacted, original keys)
6
+ * - Plain: single-line logfmt via outputPlain (keys stripped, values formatted)
7
+ * - YAML: multi-line via outputYaml (keys stripped, values formatted)
8
+ *
9
+ * Span fields are carried via AsyncLocalStorage (async-safe).
10
+ *
11
+ * Usage:
12
+ * import { log, span, initJson, initPlain, initYaml } from "agent-first-data/afd_logging";
13
+ * initJson(); // or initPlain() or initYaml()
14
+ * log.info("Server started");
15
+ * await span({ request_id: "abc" }, async () => {
16
+ * log.info("Processing", { domain: "example.com" });
17
+ * });
18
+ */
19
+ import { AsyncLocalStorage } from "node:async_hooks";
20
+ import { outputJson, outputYaml, outputPlain } from "./format.js";
21
+ const spanStore = new AsyncLocalStorage();
22
+ let currentFormat = "json";
23
+ /** Set log output to single-line JSONL (secrets redacted, original keys). */
24
+ export function initJson() { currentFormat = "json"; }
25
+ /** Set log output to single-line logfmt (keys stripped, values formatted). */
26
+ export function initPlain() { currentFormat = "plain"; }
27
+ /** Set log output to multi-line YAML (keys stripped, values formatted). */
28
+ export function initYaml() { currentFormat = "yaml"; }
29
+ function emit(level, message, fields) {
30
+ const entry = {
31
+ timestamp_epoch_ms: Date.now(),
32
+ message,
33
+ };
34
+ // Span fields (from AsyncLocalStorage)
35
+ const spanFields = spanStore.getStore();
36
+ if (spanFields) {
37
+ for (const [k, v] of Object.entries(spanFields)) {
38
+ entry[k] = v;
39
+ }
40
+ }
41
+ // Event fields (override span fields on collision)
42
+ let hasCode = false;
43
+ if (fields) {
44
+ for (const [k, v] of Object.entries(fields)) {
45
+ if (k === "code")
46
+ hasCode = true;
47
+ entry[k] = v;
48
+ }
49
+ }
50
+ // Default code from level
51
+ if (!hasCode) {
52
+ entry["code"] = level;
53
+ }
54
+ // Format using the library's own output functions
55
+ const value = entry;
56
+ let line;
57
+ switch (currentFormat) {
58
+ case "plain":
59
+ line = outputPlain(value);
60
+ break;
61
+ case "yaml":
62
+ line = outputYaml(value);
63
+ break;
64
+ default:
65
+ line = outputJson(value);
66
+ break;
67
+ }
68
+ process.stdout.write(line + "\n");
69
+ }
70
+ /**
71
+ * AFD logger. Each method outputs a single log line to stdout.
72
+ * Format is controlled by initJson/initPlain/initYaml (default: JSON).
73
+ */
74
+ export const log = {
75
+ trace: (msg, fields) => emit("trace", msg, fields),
76
+ debug: (msg, fields) => emit("debug", msg, fields),
77
+ info: (msg, fields) => emit("info", msg, fields),
78
+ warn: (msg, fields) => emit("warn", msg, fields),
79
+ error: (msg, fields) => emit("error", msg, fields),
80
+ };
81
+ /**
82
+ * Run fn within a span that adds fields to all log events.
83
+ * Spans nest: inner spans inherit and can override outer span fields.
84
+ */
85
+ export function span(fields, fn) {
86
+ const parent = spanStore.getStore() ?? {};
87
+ return spanStore.run({ ...parent, ...fields }, fn);
88
+ }
package/dist/format.d.ts CHANGED
@@ -1,31 +1,30 @@
1
1
  /**
2
- * AFD output formatting: JSON, YAML, plain text with suffix-driven transforms.
2
+ * AFD output formatting and protocol templates.
3
+ *
4
+ * 9 public APIs: 4 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
5
  */
4
- type JsonValue = string | number | boolean | null | JsonValue[] | {
6
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
7
  [key: string]: JsonValue;
6
8
  };
7
- export declare enum OutputFormat {
8
- Json = "json",
9
- Yaml = "yaml",
10
- Plain = "plain"
11
- }
12
- export declare function formatValue(fmt: OutputFormat, value: JsonValue): string;
13
- export declare function formatPretty(fmt: OutputFormat, value: JsonValue): string;
14
- export declare function toYaml(value: JsonValue): string;
15
- export declare function toPlain(value: JsonValue): string;
16
- export declare function redactSecrets(value: JsonValue): JsonValue;
17
- export declare function ok(result: JsonValue): JsonValue;
18
- export declare function okTrace(result: JsonValue, trace: JsonValue): JsonValue;
19
- export declare function error(message: string): JsonValue;
20
- export declare function errorTrace(message: string, trace: JsonValue): JsonValue;
21
- export declare function startup(config: JsonValue, args: JsonValue, env: JsonValue): JsonValue;
22
- export declare function status(code: string, fields?: {
23
- [key: string]: JsonValue;
24
- }): JsonValue;
9
+ /** Build {code: "startup", config, args, env}. */
10
+ export declare function buildJsonStartup(config: JsonValue, args: JsonValue, env: JsonValue): JsonValue;
11
+ /** Build {code: "ok", result, trace?}. */
12
+ export declare function buildJsonOk(result: JsonValue, trace?: JsonValue): JsonValue;
13
+ /** Build {code: "error", error: message, trace?}. */
14
+ export declare function buildJsonError(message: string, trace?: JsonValue): JsonValue;
15
+ /** Build {code: "<custom>", ...fields, trace?}. */
16
+ export declare function buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue;
17
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
18
+ export declare function outputJson(value: JsonValue): string;
19
+ /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
20
+ export declare function outputYaml(value: JsonValue): string;
21
+ /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
22
+ export declare function outputPlain(value: JsonValue): string;
23
+ /** Redact _secret fields in-place. */
24
+ export declare function internalRedactSecrets(value: JsonValue): void;
25
25
  /**
26
26
  * Parse a human-readable size string into bytes.
27
27
  * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
28
28
  * Case-insensitive. Trims whitespace. Returns null for invalid input.
29
29
  */
30
30
  export declare function parseSize(s: string): number | null;
31
- export {};
package/dist/format.js CHANGED
@@ -1,236 +1,119 @@
1
1
  /**
2
- * AFD output formatting: JSON, YAML, plain text with suffix-driven transforms.
2
+ * AFD output formatting and protocol templates.
3
+ *
4
+ * 9 public APIs: 4 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
5
  */
4
6
  // ═══════════════════════════════════════════
5
- // OutputFormat
7
+ // Public API: Protocol Builders
6
8
  // ═══════════════════════════════════════════
7
- export var OutputFormat;
8
- (function (OutputFormat) {
9
- OutputFormat["Json"] = "json";
10
- OutputFormat["Yaml"] = "yaml";
11
- OutputFormat["Plain"] = "plain";
12
- })(OutputFormat || (OutputFormat = {}));
13
- export function formatValue(fmt, value) {
14
- switch (fmt) {
15
- case OutputFormat.Json:
16
- return JSON.stringify(value);
17
- case OutputFormat.Yaml:
18
- return toYaml(value);
19
- case OutputFormat.Plain:
20
- return toPlain(value);
21
- }
9
+ /** Build {code: "startup", config, args, env}. */
10
+ export function buildJsonStartup(config, args, env) {
11
+ return { code: "startup", config, args, env };
22
12
  }
23
- export function formatPretty(fmt, value) {
24
- switch (fmt) {
25
- case OutputFormat.Json:
26
- return JSON.stringify(value, null, 2);
27
- case OutputFormat.Yaml:
28
- return toYaml(value);
29
- case OutputFormat.Plain:
30
- return toPlain(value);
31
- }
13
+ /** Build {code: "ok", result, trace?}. */
14
+ export function buildJsonOk(result, trace) {
15
+ const m = { code: "ok", result };
16
+ if (trace !== undefined)
17
+ m.trace = trace;
18
+ return m;
19
+ }
20
+ /** Build {code: "error", error: message, trace?}. */
21
+ export function buildJsonError(message, trace) {
22
+ const m = { code: "error", error: message };
23
+ if (trace !== undefined)
24
+ m.trace = trace;
25
+ return m;
26
+ }
27
+ /** Build {code: "<custom>", ...fields, trace?}. */
28
+ export function buildJson(code, fields, trace) {
29
+ const result = isObject(fields) ? { ...fields } : {};
30
+ result.code = code;
31
+ if (trace !== undefined)
32
+ result.trace = trace;
33
+ return result;
32
34
  }
33
35
  // ═══════════════════════════════════════════
34
- // YAML
36
+ // Public API: Output Formatters
35
37
  // ═══════════════════════════════════════════
36
- export function toYaml(value) {
38
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
39
+ export function outputJson(value) {
40
+ const v = JSON.parse(JSON.stringify(value));
41
+ redactSecrets(v);
42
+ return JSON.stringify(v);
43
+ }
44
+ /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
45
+ export function outputYaml(value) {
37
46
  const lines = ["---"];
38
- renderYaml(value, 0, lines);
47
+ renderYamlProcessed(value, 0, lines);
39
48
  return lines.join("\n");
40
49
  }
41
- function renderYaml(value, indent, lines) {
42
- const prefix = " ".repeat(indent);
43
- if (isObject(value)) {
44
- for (const [k, v] of Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
45
- if (isObject(v)) {
46
- if (Object.keys(v).length > 0) {
47
- lines.push(`${prefix}${k}:`);
48
- renderYaml(v, indent + 1, lines);
49
- }
50
- else {
51
- lines.push(`${prefix}${k}: {}`);
52
- }
53
- }
54
- else if (Array.isArray(v)) {
55
- if (v.length === 0) {
56
- lines.push(`${prefix}${k}: []`);
57
- }
58
- else {
59
- lines.push(`${prefix}${k}:`);
60
- for (const item of v) {
61
- if (isObject(item)) {
62
- lines.push(`${prefix} -`);
63
- renderYaml(item, indent + 2, lines);
64
- }
65
- else {
66
- lines.push(`${prefix} - ${yamlScalar(item)}`);
67
- }
68
- }
69
- }
70
- }
71
- else {
72
- lines.push(`${prefix}${k}: ${yamlScalar(v)}`);
73
- }
74
- }
75
- }
76
- else {
77
- lines.push(`${prefix}${yamlScalar(value)}`);
78
- }
79
- }
80
- function yamlScalar(value) {
81
- if (typeof value === "string") {
82
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
83
- }
84
- if (value === null)
85
- return "null";
86
- if (typeof value === "boolean")
87
- return value.toString();
88
- if (typeof value === "number")
89
- return value.toString();
90
- return `"${String(value).replace(/"/g, '\\"')}"`;
50
+ /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
51
+ export function outputPlain(value) {
52
+ const pairs = [];
53
+ collectPlainPairs(value, "", pairs);
54
+ pairs.sort(([a], [b]) => jcsCompare(a, b));
55
+ return pairs
56
+ .map(([k, v]) => (v.includes(" ") ? `${k}="${v}"` : `${k}=${v}`))
57
+ .join(" ");
91
58
  }
92
59
  // ═══════════════════════════════════════════
93
- // Plain
60
+ // Public API: Redaction & Utility
94
61
  // ═══════════════════════════════════════════
95
- export function toPlain(value) {
96
- const lines = [];
97
- renderPlain(value, 0, lines);
98
- return lines.join("\n");
62
+ /** Redact _secret fields in-place. */
63
+ export function internalRedactSecrets(value) {
64
+ redactSecrets(value);
99
65
  }
100
- function renderPlain(value, indent, lines) {
101
- const prefix = " ".repeat(indent);
102
- if (isObject(value)) {
103
- for (const [k, v] of Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
104
- if (isObject(v)) {
105
- lines.push(`${prefix}${k}:`);
106
- renderPlain(v, indent + 1, lines);
107
- }
108
- else if (Array.isArray(v)) {
109
- if (v.length === 0) {
110
- lines.push(`${prefix}${k}: []`);
111
- }
112
- else if (v.every((i) => !isObject(i) && !Array.isArray(i))) {
113
- lines.push(`${prefix}${k}:`);
114
- for (const item of v) {
115
- lines.push(`${prefix} - ${plainScalar(item)}`);
116
- }
117
- }
118
- else {
119
- lines.push(`${prefix}${k}:`);
120
- for (const item of v) {
121
- if (isObject(item)) {
122
- lines.push(`${prefix} -`);
123
- renderPlain(item, indent + 2, lines);
124
- }
125
- else {
126
- lines.push(`${prefix} - ${plainScalar(item)}`);
127
- }
128
- }
129
- }
130
- }
131
- else {
132
- lines.push(`${prefix}${k}: ${formatPlainField(k, v)}`);
133
- }
134
- }
66
+ /**
67
+ * Parse a human-readable size string into bytes.
68
+ * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
69
+ * Case-insensitive. Trims whitespace. Returns null for invalid input.
70
+ */
71
+ export function parseSize(s) {
72
+ s = s.trim();
73
+ if (!s)
74
+ return null;
75
+ const multipliers = {
76
+ b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
77
+ };
78
+ const last = s[s.length - 1].toLowerCase();
79
+ let numStr;
80
+ let mult;
81
+ if (last in multipliers) {
82
+ numStr = s.slice(0, -1);
83
+ mult = multipliers[last];
135
84
  }
136
- else {
137
- lines.push(`${prefix}${plainScalar(value)}`);
85
+ else if ((last >= "0" && last <= "9") || last === ".") {
86
+ numStr = s;
87
+ mult = 1;
138
88
  }
139
- }
140
- function formatPlainField(key, value) {
141
- const lower = key.toLowerCase();
142
- // Secret
143
- if (lower.endsWith("_secret"))
144
- return "***";
145
- // Timestamps
146
- if (lower.endsWith("_epoch_ms")) {
147
- if (typeof value === "number" && Number.isInteger(value))
148
- return formatRfc3339Ms(value);
149
- }
150
- if (lower.endsWith("_epoch_s")) {
151
- if (typeof value === "number" && Number.isInteger(value))
152
- return formatRfc3339Ms(value * 1000);
153
- }
154
- if (lower.endsWith("_epoch_ns")) {
155
- if (typeof value === "number" && Number.isInteger(value))
156
- return formatRfc3339Ms(Math.floor(value / 1_000_000));
157
- }
158
- if (lower.endsWith("_rfc3339"))
159
- return plainScalar(value);
160
- // Size
161
- if (lower.endsWith("_bytes")) {
162
- if (typeof value === "number" && Number.isInteger(value))
163
- return formatBytesHuman(value);
164
- }
165
- // Percentage
166
- if (lower.endsWith("_percent") && typeof value === "number")
167
- return `${plainScalar(value)}%`;
168
- // Currency — Bitcoin
169
- if (lower.endsWith("_msats") && typeof value === "number")
170
- return `${plainScalar(value)}msats`;
171
- if (lower.endsWith("_sats") && typeof value === "number")
172
- return `${plainScalar(value)}sats`;
173
- if (lower.endsWith("_btc") && typeof value === "number")
174
- return `${plainScalar(value)} BTC`;
175
- // Currency — Fiat
176
- if (lower.endsWith("_usd_cents") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
177
- return `$${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`;
178
- }
179
- if (lower.endsWith("_eur_cents") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
180
- return `\u20ac${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`;
181
- }
182
- if (lower.endsWith("_jpy") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
183
- return `\u00a5${formatWithCommas(value)}`;
184
- }
185
- if (lower.endsWith("_cents")) {
186
- const code = extractCurrencyCode(lower);
187
- if (code && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
188
- return `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`;
189
- }
89
+ else {
90
+ return null;
190
91
  }
191
- // Duration — long
192
- if (lower.endsWith("_minutes") && typeof value === "number")
193
- return `${plainScalar(value)} minutes`;
194
- if (lower.endsWith("_hours") && typeof value === "number")
195
- return `${plainScalar(value)} hours`;
196
- if (lower.endsWith("_days") && typeof value === "number")
197
- return `${plainScalar(value)} days`;
198
- // Duration — ms
199
- if (lower.endsWith("_ms") && !lower.endsWith("_epoch_ms") && typeof value === "number") {
200
- if (value >= 1000)
201
- return `${(value / 1000).toFixed(2)}s`;
202
- return `${plainScalar(value)}ms`;
203
- }
204
- // Duration — ns, us, s
205
- if (lower.endsWith("_ns") && !lower.endsWith("_epoch_ns") && typeof value === "number")
206
- return `${plainScalar(value)}ns`;
207
- if (lower.endsWith("_us") && typeof value === "number")
208
- return `${plainScalar(value)}\u03bcs`;
209
- if (lower.endsWith("_s") && !lower.endsWith("_epoch_s") && typeof value === "number")
210
- return `${plainScalar(value)}s`;
211
- return plainScalar(value);
212
- }
213
- function plainScalar(value) {
214
- if (typeof value === "string")
215
- return value;
216
- if (value === null)
217
- return "null";
218
- if (typeof value === "boolean")
219
- return value.toString();
220
- if (typeof value === "number")
221
- return value.toString();
222
- return String(value);
92
+ if (!numStr)
93
+ return null;
94
+ const n = Number(numStr);
95
+ if (isNaN(n) || n < 0 || !isFinite(n))
96
+ return null;
97
+ return Math.trunc(n * mult);
223
98
  }
224
99
  // ═══════════════════════════════════════════
225
- // Secret redaction
100
+ // Secret Redaction
226
101
  // ═══════════════════════════════════════════
227
- export function redactSecrets(value) {
102
+ function redactSecrets(value) {
228
103
  if (isObject(value)) {
229
104
  for (const k of Object.keys(value)) {
230
- if (k.toLowerCase().endsWith("_secret") && typeof value[k] === "string") {
231
- value[k] = "***";
105
+ if (k.endsWith("_secret") || k.endsWith("_SECRET")) {
106
+ const v = value[k];
107
+ if (isObject(v) || Array.isArray(v)) {
108
+ redactSecrets(v);
109
+ }
110
+ else {
111
+ value[k] = "***";
112
+ }
113
+ }
114
+ else {
115
+ redactSecrets(value[k]);
232
116
  }
233
- redactSecrets(value[k]);
234
117
  }
235
118
  }
236
119
  else if (Array.isArray(value)) {
@@ -238,32 +121,212 @@ export function redactSecrets(value) {
238
121
  redactSecrets(item);
239
122
  }
240
123
  }
241
- return value;
242
124
  }
243
125
  // ═══════════════════════════════════════════
244
- // AFD Protocol templates
126
+ // Suffix Processing
245
127
  // ═══════════════════════════════════════════
246
- export function ok(result) {
247
- return { code: "ok", result };
128
+ function stripSuffixCI(key, suffixLower) {
129
+ if (key.endsWith(suffixLower))
130
+ return key.slice(0, -suffixLower.length);
131
+ const suffixUpper = suffixLower.toUpperCase();
132
+ if (key.endsWith(suffixUpper))
133
+ return key.slice(0, -suffixUpper.length);
134
+ return null;
248
135
  }
249
- export function okTrace(result, trace) {
250
- return { code: "ok", result, trace };
136
+ function tryStripGenericCents(key) {
137
+ const code = extractCurrencyCode(key);
138
+ if (code === null)
139
+ return null;
140
+ const suffixLen = code.length + "_cents".length + 1; // _{code}_cents
141
+ const stripped = key.slice(0, -suffixLen);
142
+ if (!stripped)
143
+ return null;
144
+ return [stripped, code];
251
145
  }
252
- export function error(message) {
253
- return { code: "error", error: message };
146
+ function isInt(value) {
147
+ return typeof value === "number" && Number.isInteger(value);
254
148
  }
255
- export function errorTrace(message, trace) {
256
- return { code: "error", error: message, trace };
149
+ function isNum(value) {
150
+ return typeof value === "number";
257
151
  }
258
- export function startup(config, args, env) {
259
- return { code: "startup", config, args, env };
152
+ function tryProcessField(key, value) {
153
+ let stripped;
154
+ // Group 1: compound timestamp suffixes
155
+ stripped = stripSuffixCI(key, "_epoch_ms");
156
+ if (stripped !== null) {
157
+ if (isInt(value))
158
+ return [stripped, formatRfc3339Ms(value)];
159
+ return null;
160
+ }
161
+ stripped = stripSuffixCI(key, "_epoch_s");
162
+ if (stripped !== null) {
163
+ if (isInt(value))
164
+ return [stripped, formatRfc3339Ms(value * 1000)];
165
+ return null;
166
+ }
167
+ stripped = stripSuffixCI(key, "_epoch_ns");
168
+ if (stripped !== null) {
169
+ if (isInt(value))
170
+ return [stripped, formatRfc3339Ms(Math.floor(value / 1_000_000))];
171
+ return null;
172
+ }
173
+ // Group 2: compound currency suffixes
174
+ stripped = stripSuffixCI(key, "_usd_cents");
175
+ if (stripped !== null) {
176
+ if (isInt(value) && value >= 0)
177
+ return [stripped, `$${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
178
+ return null;
179
+ }
180
+ stripped = stripSuffixCI(key, "_eur_cents");
181
+ if (stripped !== null) {
182
+ if (isInt(value) && value >= 0)
183
+ return [stripped, `\u20ac${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
184
+ return null;
185
+ }
186
+ const gc = tryStripGenericCents(key);
187
+ if (gc !== null) {
188
+ const [gcStripped, code] = gc;
189
+ if (isInt(value) && value >= 0)
190
+ return [gcStripped, `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`];
191
+ return null;
192
+ }
193
+ // Group 3: multi-char suffixes
194
+ stripped = stripSuffixCI(key, "_rfc3339");
195
+ if (stripped !== null) {
196
+ if (typeof value === "string")
197
+ return [stripped, value];
198
+ return null;
199
+ }
200
+ stripped = stripSuffixCI(key, "_minutes");
201
+ if (stripped !== null) {
202
+ if (isNum(value))
203
+ return [stripped, `${plainScalar(value)} minutes`];
204
+ return null;
205
+ }
206
+ stripped = stripSuffixCI(key, "_hours");
207
+ if (stripped !== null) {
208
+ if (isNum(value))
209
+ return [stripped, `${plainScalar(value)} hours`];
210
+ return null;
211
+ }
212
+ stripped = stripSuffixCI(key, "_days");
213
+ if (stripped !== null) {
214
+ if (isNum(value))
215
+ return [stripped, `${plainScalar(value)} days`];
216
+ return null;
217
+ }
218
+ // Group 4: single-unit suffixes
219
+ stripped = stripSuffixCI(key, "_msats");
220
+ if (stripped !== null) {
221
+ if (isNum(value))
222
+ return [stripped, `${plainScalar(value)}msats`];
223
+ return null;
224
+ }
225
+ stripped = stripSuffixCI(key, "_sats");
226
+ if (stripped !== null) {
227
+ if (isNum(value))
228
+ return [stripped, `${plainScalar(value)}sats`];
229
+ return null;
230
+ }
231
+ stripped = stripSuffixCI(key, "_bytes");
232
+ if (stripped !== null) {
233
+ if (isInt(value))
234
+ return [stripped, formatBytesHuman(value)];
235
+ return null;
236
+ }
237
+ stripped = stripSuffixCI(key, "_percent");
238
+ if (stripped !== null) {
239
+ if (isNum(value))
240
+ return [stripped, `${plainScalar(value)}%`];
241
+ return null;
242
+ }
243
+ stripped = stripSuffixCI(key, "_secret");
244
+ if (stripped !== null)
245
+ return [stripped, "***"];
246
+ // Group 5: short suffixes (last to avoid false positives)
247
+ stripped = stripSuffixCI(key, "_btc");
248
+ if (stripped !== null) {
249
+ if (isNum(value))
250
+ return [stripped, `${plainScalar(value)} BTC`];
251
+ return null;
252
+ }
253
+ stripped = stripSuffixCI(key, "_jpy");
254
+ if (stripped !== null) {
255
+ if (isInt(value) && value >= 0)
256
+ return [stripped, `\u00a5${formatWithCommas(value)}`];
257
+ return null;
258
+ }
259
+ stripped = stripSuffixCI(key, "_ns");
260
+ if (stripped !== null) {
261
+ if (isNum(value))
262
+ return [stripped, `${plainScalar(value)}ns`];
263
+ return null;
264
+ }
265
+ stripped = stripSuffixCI(key, "_us");
266
+ if (stripped !== null) {
267
+ if (isNum(value))
268
+ return [stripped, `${plainScalar(value)}\u03bcs`];
269
+ return null;
270
+ }
271
+ stripped = stripSuffixCI(key, "_ms");
272
+ if (stripped !== null) {
273
+ const fv = formatMsValue(value);
274
+ if (fv !== null)
275
+ return [stripped, fv];
276
+ return null;
277
+ }
278
+ stripped = stripSuffixCI(key, "_s");
279
+ if (stripped !== null) {
280
+ if (isNum(value))
281
+ return [stripped, `${plainScalar(value)}s`];
282
+ return null;
283
+ }
284
+ return null;
260
285
  }
261
- export function status(code, fields) {
262
- return { ...fields, code };
286
+ function processObjectFields(obj) {
287
+ const entries = [];
288
+ for (const [k, v] of Object.entries(obj)) {
289
+ const result = tryProcessField(k, v);
290
+ if (result !== null) {
291
+ entries.push({ stripped: result[0], original: k, value: v, formatted: result[1] });
292
+ }
293
+ else {
294
+ entries.push({ stripped: k, original: k, value: v, formatted: null });
295
+ }
296
+ }
297
+ // Detect collisions
298
+ const counts = new Map();
299
+ for (const e of entries) {
300
+ counts.set(e.stripped, (counts.get(e.stripped) ?? 0) + 1);
301
+ }
302
+ // Resolve collisions: revert both key and formatted value
303
+ const result = entries.map((e) => {
304
+ if ((counts.get(e.stripped) ?? 0) > 1 && e.original !== e.stripped) {
305
+ return { key: e.original, value: e.value, formatted: null };
306
+ }
307
+ return { key: e.stripped, value: e.value, formatted: e.formatted };
308
+ });
309
+ // Sort by display key (JCS order)
310
+ result.sort((a, b) => jcsCompare(a.key, b.key));
311
+ return result;
263
312
  }
264
313
  // ═══════════════════════════════════════════
265
- // Helpers
314
+ // Formatting Helpers
266
315
  // ═══════════════════════════════════════════
316
+ function formatMsAsSeconds(ms) {
317
+ const formatted = (ms / 1000).toFixed(3);
318
+ const trimmed = formatted.replace(/0+$/, "");
319
+ if (trimmed.endsWith("."))
320
+ return trimmed + "0s";
321
+ return trimmed + "s";
322
+ }
323
+ function formatMsValue(value) {
324
+ if (!isNum(value))
325
+ return null;
326
+ if (Math.abs(value) >= 1000)
327
+ return formatMsAsSeconds(value);
328
+ return `${plainScalar(value)}ms`;
329
+ }
267
330
  function formatRfc3339Ms(ms) {
268
331
  try {
269
332
  const d = new Date(ms);
@@ -291,50 +354,152 @@ function formatBytesHuman(bytes) {
291
354
  return `${bytes}B`;
292
355
  }
293
356
  function formatWithCommas(n) {
294
- return n.toLocaleString("en-US");
295
- }
296
- /**
297
- * Parse a human-readable size string into bytes.
298
- * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
299
- * Case-insensitive. Trims whitespace. Returns null for invalid input.
300
- */
301
- export function parseSize(s) {
302
- s = s.trim();
303
- if (!s)
304
- return null;
305
- const multipliers = {
306
- b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
307
- };
308
- const last = s[s.length - 1].toLowerCase();
309
- let numStr;
310
- let mult;
311
- if (last in multipliers) {
312
- numStr = s.slice(0, -1);
313
- mult = multipliers[last];
314
- }
315
- else if ((last >= "0" && last <= "9") || last === ".") {
316
- numStr = s;
317
- mult = 1;
318
- }
319
- else {
320
- return null;
357
+ const s = String(n);
358
+ const result = [];
359
+ for (let i = 0; i < s.length; i++) {
360
+ if (i > 0 && (s.length - i) % 3 === 0)
361
+ result.push(",");
362
+ result.push(s[i]);
321
363
  }
322
- if (!numStr)
323
- return null;
324
- const n = Number(numStr);
325
- if (isNaN(n) || n < 0 || !isFinite(n))
326
- return null;
327
- return Math.trunc(n * mult);
364
+ return result.join("");
328
365
  }
366
+ /** Extract currency code from _{code}_cents / _{CODE}_CENTS suffix. */
329
367
  function extractCurrencyCode(key) {
330
- if (!key.endsWith("_cents"))
368
+ let withoutCents;
369
+ if (key.endsWith("_cents"))
370
+ withoutCents = key.slice(0, -6);
371
+ else if (key.endsWith("_CENTS"))
372
+ withoutCents = key.slice(0, -6);
373
+ else
331
374
  return null;
332
- const withoutCents = key.slice(0, -6);
333
375
  const idx = withoutCents.lastIndexOf("_");
334
376
  if (idx < 0)
335
377
  return null;
336
- return withoutCents.slice(idx + 1);
378
+ const code = withoutCents.slice(idx + 1);
379
+ if (!code)
380
+ return null;
381
+ return code;
382
+ }
383
+ // ═══════════════════════════════════════════
384
+ // YAML Rendering
385
+ // ═══════════════════════════════════════════
386
+ function renderYamlProcessed(value, indent, lines) {
387
+ const prefix = " ".repeat(indent);
388
+ if (!isObject(value)) {
389
+ lines.push(`${prefix}${yamlScalar(value)}`);
390
+ return;
391
+ }
392
+ for (const pf of processObjectFields(value)) {
393
+ if (pf.formatted !== null) {
394
+ lines.push(`${prefix}${pf.key}: "${escapeYamlStr(pf.formatted)}"`);
395
+ }
396
+ else if (isObject(pf.value)) {
397
+ if (Object.keys(pf.value).length > 0) {
398
+ lines.push(`${prefix}${pf.key}:`);
399
+ renderYamlProcessed(pf.value, indent + 1, lines);
400
+ }
401
+ else {
402
+ lines.push(`${prefix}${pf.key}: {}`);
403
+ }
404
+ }
405
+ else if (Array.isArray(pf.value)) {
406
+ if (pf.value.length === 0) {
407
+ lines.push(`${prefix}${pf.key}: []`);
408
+ }
409
+ else {
410
+ lines.push(`${prefix}${pf.key}:`);
411
+ for (const item of pf.value) {
412
+ if (isObject(item)) {
413
+ lines.push(`${prefix} -`);
414
+ renderYamlProcessed(item, indent + 2, lines);
415
+ }
416
+ else {
417
+ lines.push(`${prefix} - ${yamlScalar(item)}`);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ else {
423
+ lines.push(`${prefix}${pf.key}: ${yamlScalar(pf.value)}`);
424
+ }
425
+ }
426
+ }
427
+ function escapeYamlStr(s) {
428
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
337
429
  }
430
+ function yamlScalar(value) {
431
+ if (typeof value === "string")
432
+ return `"${escapeYamlStr(value)}"`;
433
+ if (value === null)
434
+ return "null";
435
+ if (typeof value === "boolean")
436
+ return value.toString();
437
+ if (typeof value === "number")
438
+ return value.toString();
439
+ return `"${String(value).replace(/"/g, '\\"')}"`;
440
+ }
441
+ // ═══════════════════════════════════════════
442
+ // Plain Rendering (logfmt)
443
+ // ═══════════════════════════════════════════
444
+ function collectPlainPairs(value, prefix, pairs) {
445
+ if (!isObject(value))
446
+ return;
447
+ for (const pf of processObjectFields(value)) {
448
+ const fullKey = prefix ? `${prefix}.${pf.key}` : pf.key;
449
+ if (pf.formatted !== null) {
450
+ pairs.push([fullKey, pf.formatted]);
451
+ }
452
+ else if (isObject(pf.value)) {
453
+ collectPlainPairs(pf.value, fullKey, pairs);
454
+ }
455
+ else if (Array.isArray(pf.value)) {
456
+ pairs.push([fullKey, pf.value.map((i) => plainScalar(i)).join(",")]);
457
+ }
458
+ else if (pf.value === null) {
459
+ pairs.push([fullKey, ""]);
460
+ }
461
+ else {
462
+ pairs.push([fullKey, plainScalar(pf.value)]);
463
+ }
464
+ }
465
+ }
466
+ function plainScalar(value) {
467
+ if (typeof value === "string")
468
+ return value;
469
+ if (value === null)
470
+ return "null";
471
+ if (typeof value === "boolean")
472
+ return value.toString();
473
+ if (typeof value === "number")
474
+ return value.toString();
475
+ return String(value);
476
+ }
477
+ // ═══════════════════════════════════════════
478
+ // Utilities
479
+ // ═══════════════════════════════════════════
338
480
  function isObject(value) {
339
481
  return typeof value === "object" && value !== null && !Array.isArray(value);
340
482
  }
483
+ function jcsCompare(a, b) {
484
+ const ua = Array.from(a).flatMap((c) => {
485
+ const cp = c.codePointAt(0);
486
+ if (cp > 0xffff) {
487
+ const offset = cp - 0x10000;
488
+ return [0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff)];
489
+ }
490
+ return [cp];
491
+ });
492
+ const ub = Array.from(b).flatMap((c) => {
493
+ const cp = c.codePointAt(0);
494
+ if (cp > 0xffff) {
495
+ const offset = cp - 0x10000;
496
+ return [0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff)];
497
+ }
498
+ return [cp];
499
+ });
500
+ for (let i = 0; i < ua.length && i < ub.length; i++) {
501
+ if (ua[i] !== ub[i])
502
+ return ua[i] - ub[i];
503
+ }
504
+ return ua.length - ub.length;
505
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFD) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { OutputFormat, formatValue, formatPretty, toYaml, toPlain, redactSecrets, parseSize } from "./format.js";
5
- export { ok, okTrace, error, errorTrace, startup, status } from "./format.js";
4
+ export { buildJsonStartup, buildJsonOk, buildJsonError, buildJson, outputJson, outputYaml, outputPlain, internalRedactSecrets, parseSize } from "./format.js";
5
+ export { log, span, initJson, initPlain, initYaml } from "./afd_logging.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFD) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { OutputFormat, formatValue, formatPretty, toYaml, toPlain, redactSecrets, parseSize } from "./format.js";
5
- export { ok, okTrace, error, errorTrace, startup, status } from "./format.js";
4
+ export { buildJsonStartup, buildJsonOk, buildJsonError, buildJson, outputJson, outputYaml, outputPlain, internalRedactSecrets, parseSize } from "./format.js";
5
+ export { log, span, initJson, initPlain, initYaml } from "./afd_logging.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,14 +14,22 @@
14
14
  ".": {
15
15
  "import": "./dist/index.js",
16
16
  "types": "./dist/index.d.ts"
17
+ },
18
+ "./afd_logging": {
19
+ "import": "./dist/afd_logging.js",
20
+ "types": "./dist/afd_logging.d.ts"
17
21
  }
18
22
  },
19
- "files": ["dist"],
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
20
27
  "scripts": {
21
28
  "build": "tsc",
22
29
  "test": "node --test src/*.test.ts --experimental-strip-types"
23
30
  },
24
31
  "devDependencies": {
25
- "typescript": "^5.7"
32
+ "@types/node": "^25.2.3",
33
+ "typescript": "^5.0"
26
34
  }
27
35
  }