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 +383 -0
- package/dist/afd_logging.d.ts +40 -0
- package/dist/afd_logging.js +88 -0
- package/dist/format.d.ts +20 -21
- package/dist/format.js +425 -260
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/package.json +11 -3
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
export declare function
|
|
13
|
-
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
export declare function
|
|
19
|
-
|
|
20
|
-
export declare function
|
|
21
|
-
|
|
22
|
-
export declare function
|
|
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
|
|
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
|
-
//
|
|
7
|
+
// Public API: Protocol Builders
|
|
6
8
|
// ═══════════════════════════════════════════
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
//
|
|
36
|
+
// Public API: Output Formatters
|
|
35
37
|
// ═══════════════════════════════════════════
|
|
36
|
-
|
|
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
|
-
|
|
47
|
+
renderYamlProcessed(value, 0, lines);
|
|
39
48
|
return lines.join("\n");
|
|
40
49
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
//
|
|
60
|
+
// Public API: Redaction & Utility
|
|
94
61
|
// ═══════════════════════════════════════════
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return lines.join("\n");
|
|
62
|
+
/** Redact _secret fields in-place. */
|
|
63
|
+
export function internalRedactSecrets(value) {
|
|
64
|
+
redactSecrets(value);
|
|
99
65
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
85
|
+
else if ((last >= "0" && last <= "9") || last === ".") {
|
|
86
|
+
numStr = s;
|
|
87
|
+
mult = 1;
|
|
138
88
|
}
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
return
|
|
196
|
-
|
|
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
|
|
100
|
+
// Secret Redaction
|
|
226
101
|
// ═══════════════════════════════════════════
|
|
227
|
-
|
|
102
|
+
function redactSecrets(value) {
|
|
228
103
|
if (isObject(value)) {
|
|
229
104
|
for (const k of Object.keys(value)) {
|
|
230
|
-
if (k.
|
|
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
|
-
//
|
|
126
|
+
// Suffix Processing
|
|
245
127
|
// ═══════════════════════════════════════════
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
return
|
|
146
|
+
function isInt(value) {
|
|
147
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
254
148
|
}
|
|
255
|
-
|
|
256
|
-
return
|
|
149
|
+
function isNum(value) {
|
|
150
|
+
return typeof value === "number";
|
|
257
151
|
}
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
5
|
-
export {
|
|
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 {
|
|
5
|
-
export {
|
|
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
|
+
"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": [
|
|
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
|
-
"
|
|
32
|
+
"@types/node": "^25.2.3",
|
|
33
|
+
"typescript": "^5.0"
|
|
26
34
|
}
|
|
27
35
|
}
|