agent-first-data 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -0
- package/dist/format.d.ts +19 -19
- package/dist/format.js +425 -260
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
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** (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
|
+
## Output Formats
|
|
253
|
+
|
|
254
|
+
Three output formats for different use cases:
|
|
255
|
+
|
|
256
|
+
| Format | Structure | Keys | Values | Use case |
|
|
257
|
+
|:-------|:----------|:-----|:-------|:---------|
|
|
258
|
+
| **JSON** | single-line | original (with suffix) | raw | programs, logs |
|
|
259
|
+
| **YAML** | multi-line | stripped | formatted | human inspection |
|
|
260
|
+
| **Plain** | single-line logfmt | stripped | formatted | compact scanning |
|
|
261
|
+
|
|
262
|
+
All formats automatically redact `_secret` fields.
|
|
263
|
+
|
|
264
|
+
## Supported Suffixes
|
|
265
|
+
|
|
266
|
+
- **Duration**: `_ms`, `_s`, `_ns`, `_us`, `_minutes`, `_hours`, `_days`
|
|
267
|
+
- **Timestamps**: `_epoch_ms`, `_epoch_s`, `_epoch_ns`, `_rfc3339`
|
|
268
|
+
- **Size**: `_bytes` (auto-scales to KB/MB/GB/TB), `_size` (config input, pass through)
|
|
269
|
+
- **Currency**: `_msats`, `_sats`, `_btc`, `_usd_cents`, `_eur_cents`, `_jpy`, `_{code}_cents`
|
|
270
|
+
- **Other**: `_percent`, `_secret` (auto-redacted in all formats)
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
package/dist/format.d.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
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
|
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).
|
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,4 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-First Data (AFD) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
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";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-First Data (AFD) — suffix-driven output formatting and protocol templates.
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-first-data",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"files": ["dist"],
|
|
19
|
+
"files": ["dist", "README.md"],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"test": "node --test src/*.test.ts --experimental-strip-types"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"typescript": "^5.
|
|
25
|
+
"typescript": "^5.0"
|
|
26
26
|
}
|
|
27
27
|
}
|