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 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: JSON, YAML, plain text with suffix-driven transforms.
2
+ * AFD output formatting and protocol templates.
3
+ *
4
+ * 9 public APIs: 4 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
5
  */
4
6
  type JsonValue = string | number | boolean | null | JsonValue[] | {
5
7
  [key: string]: JsonValue;
6
8
  };
7
- export declare enum OutputFormat {
8
- Json = "json",
9
- Yaml = "yaml",
10
- Plain = "plain"
11
- }
12
- export declare function formatValue(fmt: OutputFormat, value: JsonValue): string;
13
- export declare function formatPretty(fmt: OutputFormat, value: JsonValue): string;
14
- export declare function toYaml(value: JsonValue): string;
15
- export declare function toPlain(value: JsonValue): string;
16
- export declare function redactSecrets(value: JsonValue): JsonValue;
17
- export declare function ok(result: JsonValue): JsonValue;
18
- export declare function okTrace(result: JsonValue, trace: JsonValue): JsonValue;
19
- export declare function error(message: string): JsonValue;
20
- export declare function errorTrace(message: string, trace: JsonValue): JsonValue;
21
- export declare function startup(config: JsonValue, args: JsonValue, env: JsonValue): JsonValue;
22
- export declare function status(code: string, fields?: {
23
- [key: string]: JsonValue;
24
- }): JsonValue;
9
+ /** Build {code: "startup", config, args, env}. */
10
+ export declare function buildJsonStartup(config: JsonValue, args: JsonValue, env: JsonValue): JsonValue;
11
+ /** Build {code: "ok", result, trace?}. */
12
+ export declare function buildJsonOk(result: JsonValue, trace?: JsonValue): JsonValue;
13
+ /** Build {code: "error", error: message, trace?}. */
14
+ export declare function buildJsonError(message: string, trace?: JsonValue): JsonValue;
15
+ /** Build {code: "<custom>", ...fields, trace?}. */
16
+ export declare function buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue;
17
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
18
+ export declare function outputJson(value: JsonValue): string;
19
+ /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
20
+ export declare function outputYaml(value: JsonValue): string;
21
+ /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
22
+ export declare function outputPlain(value: JsonValue): string;
23
+ /** Redact _secret fields in-place. */
24
+ export declare function internalRedactSecrets(value: JsonValue): void;
25
25
  /**
26
26
  * Parse a human-readable size string into bytes.
27
27
  * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
package/dist/format.js CHANGED
@@ -1,236 +1,119 @@
1
1
  /**
2
- * AFD output formatting: JSON, YAML, plain text with suffix-driven transforms.
2
+ * AFD output formatting and protocol templates.
3
+ *
4
+ * 9 public APIs: 4 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
5
  */
4
6
  // ═══════════════════════════════════════════
5
- // OutputFormat
7
+ // Public API: Protocol Builders
6
8
  // ═══════════════════════════════════════════
7
- export var OutputFormat;
8
- (function (OutputFormat) {
9
- OutputFormat["Json"] = "json";
10
- OutputFormat["Yaml"] = "yaml";
11
- OutputFormat["Plain"] = "plain";
12
- })(OutputFormat || (OutputFormat = {}));
13
- export function formatValue(fmt, value) {
14
- switch (fmt) {
15
- case OutputFormat.Json:
16
- return JSON.stringify(value);
17
- case OutputFormat.Yaml:
18
- return toYaml(value);
19
- case OutputFormat.Plain:
20
- return toPlain(value);
21
- }
9
+ /** Build {code: "startup", config, args, env}. */
10
+ export function buildJsonStartup(config, args, env) {
11
+ return { code: "startup", config, args, env };
22
12
  }
23
- export function formatPretty(fmt, value) {
24
- switch (fmt) {
25
- case OutputFormat.Json:
26
- return JSON.stringify(value, null, 2);
27
- case OutputFormat.Yaml:
28
- return toYaml(value);
29
- case OutputFormat.Plain:
30
- return toPlain(value);
31
- }
13
+ /** Build {code: "ok", result, trace?}. */
14
+ export function buildJsonOk(result, trace) {
15
+ const m = { code: "ok", result };
16
+ if (trace !== undefined)
17
+ m.trace = trace;
18
+ return m;
19
+ }
20
+ /** Build {code: "error", error: message, trace?}. */
21
+ export function buildJsonError(message, trace) {
22
+ const m = { code: "error", error: message };
23
+ if (trace !== undefined)
24
+ m.trace = trace;
25
+ return m;
26
+ }
27
+ /** Build {code: "<custom>", ...fields, trace?}. */
28
+ export function buildJson(code, fields, trace) {
29
+ const result = isObject(fields) ? { ...fields } : {};
30
+ result.code = code;
31
+ if (trace !== undefined)
32
+ result.trace = trace;
33
+ return result;
32
34
  }
33
35
  // ═══════════════════════════════════════════
34
- // YAML
36
+ // Public API: Output Formatters
35
37
  // ═══════════════════════════════════════════
36
- export function toYaml(value) {
38
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
39
+ export function outputJson(value) {
40
+ const v = JSON.parse(JSON.stringify(value));
41
+ redactSecrets(v);
42
+ return JSON.stringify(v);
43
+ }
44
+ /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
45
+ export function outputYaml(value) {
37
46
  const lines = ["---"];
38
- renderYaml(value, 0, lines);
47
+ renderYamlProcessed(value, 0, lines);
39
48
  return lines.join("\n");
40
49
  }
41
- function renderYaml(value, indent, lines) {
42
- const prefix = " ".repeat(indent);
43
- if (isObject(value)) {
44
- for (const [k, v] of Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
45
- if (isObject(v)) {
46
- if (Object.keys(v).length > 0) {
47
- lines.push(`${prefix}${k}:`);
48
- renderYaml(v, indent + 1, lines);
49
- }
50
- else {
51
- lines.push(`${prefix}${k}: {}`);
52
- }
53
- }
54
- else if (Array.isArray(v)) {
55
- if (v.length === 0) {
56
- lines.push(`${prefix}${k}: []`);
57
- }
58
- else {
59
- lines.push(`${prefix}${k}:`);
60
- for (const item of v) {
61
- if (isObject(item)) {
62
- lines.push(`${prefix} -`);
63
- renderYaml(item, indent + 2, lines);
64
- }
65
- else {
66
- lines.push(`${prefix} - ${yamlScalar(item)}`);
67
- }
68
- }
69
- }
70
- }
71
- else {
72
- lines.push(`${prefix}${k}: ${yamlScalar(v)}`);
73
- }
74
- }
75
- }
76
- else {
77
- lines.push(`${prefix}${yamlScalar(value)}`);
78
- }
79
- }
80
- function yamlScalar(value) {
81
- if (typeof value === "string") {
82
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
83
- }
84
- if (value === null)
85
- return "null";
86
- if (typeof value === "boolean")
87
- return value.toString();
88
- if (typeof value === "number")
89
- return value.toString();
90
- return `"${String(value).replace(/"/g, '\\"')}"`;
50
+ /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
51
+ export function outputPlain(value) {
52
+ const pairs = [];
53
+ collectPlainPairs(value, "", pairs);
54
+ pairs.sort(([a], [b]) => jcsCompare(a, b));
55
+ return pairs
56
+ .map(([k, v]) => (v.includes(" ") ? `${k}="${v}"` : `${k}=${v}`))
57
+ .join(" ");
91
58
  }
92
59
  // ═══════════════════════════════════════════
93
- // Plain
60
+ // Public API: Redaction & Utility
94
61
  // ═══════════════════════════════════════════
95
- export function toPlain(value) {
96
- const lines = [];
97
- renderPlain(value, 0, lines);
98
- return lines.join("\n");
62
+ /** Redact _secret fields in-place. */
63
+ export function internalRedactSecrets(value) {
64
+ redactSecrets(value);
99
65
  }
100
- function renderPlain(value, indent, lines) {
101
- const prefix = " ".repeat(indent);
102
- if (isObject(value)) {
103
- for (const [k, v] of Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
104
- if (isObject(v)) {
105
- lines.push(`${prefix}${k}:`);
106
- renderPlain(v, indent + 1, lines);
107
- }
108
- else if (Array.isArray(v)) {
109
- if (v.length === 0) {
110
- lines.push(`${prefix}${k}: []`);
111
- }
112
- else if (v.every((i) => !isObject(i) && !Array.isArray(i))) {
113
- lines.push(`${prefix}${k}:`);
114
- for (const item of v) {
115
- lines.push(`${prefix} - ${plainScalar(item)}`);
116
- }
117
- }
118
- else {
119
- lines.push(`${prefix}${k}:`);
120
- for (const item of v) {
121
- if (isObject(item)) {
122
- lines.push(`${prefix} -`);
123
- renderPlain(item, indent + 2, lines);
124
- }
125
- else {
126
- lines.push(`${prefix} - ${plainScalar(item)}`);
127
- }
128
- }
129
- }
130
- }
131
- else {
132
- lines.push(`${prefix}${k}: ${formatPlainField(k, v)}`);
133
- }
134
- }
66
+ /**
67
+ * Parse a human-readable size string into bytes.
68
+ * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
69
+ * Case-insensitive. Trims whitespace. Returns null for invalid input.
70
+ */
71
+ export function parseSize(s) {
72
+ s = s.trim();
73
+ if (!s)
74
+ return null;
75
+ const multipliers = {
76
+ b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
77
+ };
78
+ const last = s[s.length - 1].toLowerCase();
79
+ let numStr;
80
+ let mult;
81
+ if (last in multipliers) {
82
+ numStr = s.slice(0, -1);
83
+ mult = multipliers[last];
135
84
  }
136
- else {
137
- lines.push(`${prefix}${plainScalar(value)}`);
85
+ else if ((last >= "0" && last <= "9") || last === ".") {
86
+ numStr = s;
87
+ mult = 1;
138
88
  }
139
- }
140
- function formatPlainField(key, value) {
141
- const lower = key.toLowerCase();
142
- // Secret
143
- if (lower.endsWith("_secret"))
144
- return "***";
145
- // Timestamps
146
- if (lower.endsWith("_epoch_ms")) {
147
- if (typeof value === "number" && Number.isInteger(value))
148
- return formatRfc3339Ms(value);
149
- }
150
- if (lower.endsWith("_epoch_s")) {
151
- if (typeof value === "number" && Number.isInteger(value))
152
- return formatRfc3339Ms(value * 1000);
153
- }
154
- if (lower.endsWith("_epoch_ns")) {
155
- if (typeof value === "number" && Number.isInteger(value))
156
- return formatRfc3339Ms(Math.floor(value / 1_000_000));
157
- }
158
- if (lower.endsWith("_rfc3339"))
159
- return plainScalar(value);
160
- // Size
161
- if (lower.endsWith("_bytes")) {
162
- if (typeof value === "number" && Number.isInteger(value))
163
- return formatBytesHuman(value);
164
- }
165
- // Percentage
166
- if (lower.endsWith("_percent") && typeof value === "number")
167
- return `${plainScalar(value)}%`;
168
- // Currency — Bitcoin
169
- if (lower.endsWith("_msats") && typeof value === "number")
170
- return `${plainScalar(value)}msats`;
171
- if (lower.endsWith("_sats") && typeof value === "number")
172
- return `${plainScalar(value)}sats`;
173
- if (lower.endsWith("_btc") && typeof value === "number")
174
- return `${plainScalar(value)} BTC`;
175
- // Currency — Fiat
176
- if (lower.endsWith("_usd_cents") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
177
- return `$${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`;
178
- }
179
- if (lower.endsWith("_eur_cents") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
180
- return `\u20ac${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`;
181
- }
182
- if (lower.endsWith("_jpy") && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
183
- return `\u00a5${formatWithCommas(value)}`;
184
- }
185
- if (lower.endsWith("_cents")) {
186
- const code = extractCurrencyCode(lower);
187
- if (code && typeof value === "number" && value >= 0 && Number.isInteger(value)) {
188
- return `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`;
189
- }
89
+ else {
90
+ return null;
190
91
  }
191
- // Duration — long
192
- if (lower.endsWith("_minutes") && typeof value === "number")
193
- return `${plainScalar(value)} minutes`;
194
- if (lower.endsWith("_hours") && typeof value === "number")
195
- return `${plainScalar(value)} hours`;
196
- if (lower.endsWith("_days") && typeof value === "number")
197
- return `${plainScalar(value)} days`;
198
- // Duration — ms
199
- if (lower.endsWith("_ms") && !lower.endsWith("_epoch_ms") && typeof value === "number") {
200
- if (value >= 1000)
201
- return `${(value / 1000).toFixed(2)}s`;
202
- return `${plainScalar(value)}ms`;
203
- }
204
- // Duration — ns, us, s
205
- if (lower.endsWith("_ns") && !lower.endsWith("_epoch_ns") && typeof value === "number")
206
- return `${plainScalar(value)}ns`;
207
- if (lower.endsWith("_us") && typeof value === "number")
208
- return `${plainScalar(value)}\u03bcs`;
209
- if (lower.endsWith("_s") && !lower.endsWith("_epoch_s") && typeof value === "number")
210
- return `${plainScalar(value)}s`;
211
- return plainScalar(value);
212
- }
213
- function plainScalar(value) {
214
- if (typeof value === "string")
215
- return value;
216
- if (value === null)
217
- return "null";
218
- if (typeof value === "boolean")
219
- return value.toString();
220
- if (typeof value === "number")
221
- return value.toString();
222
- return String(value);
92
+ if (!numStr)
93
+ return null;
94
+ const n = Number(numStr);
95
+ if (isNaN(n) || n < 0 || !isFinite(n))
96
+ return null;
97
+ return Math.trunc(n * mult);
223
98
  }
224
99
  // ═══════════════════════════════════════════
225
- // Secret redaction
100
+ // Secret Redaction
226
101
  // ═══════════════════════════════════════════
227
- export function redactSecrets(value) {
102
+ function redactSecrets(value) {
228
103
  if (isObject(value)) {
229
104
  for (const k of Object.keys(value)) {
230
- if (k.toLowerCase().endsWith("_secret") && typeof value[k] === "string") {
231
- value[k] = "***";
105
+ if (k.endsWith("_secret") || k.endsWith("_SECRET")) {
106
+ const v = value[k];
107
+ if (isObject(v) || Array.isArray(v)) {
108
+ redactSecrets(v);
109
+ }
110
+ else {
111
+ value[k] = "***";
112
+ }
113
+ }
114
+ else {
115
+ redactSecrets(value[k]);
232
116
  }
233
- redactSecrets(value[k]);
234
117
  }
235
118
  }
236
119
  else if (Array.isArray(value)) {
@@ -238,32 +121,212 @@ export function redactSecrets(value) {
238
121
  redactSecrets(item);
239
122
  }
240
123
  }
241
- return value;
242
124
  }
243
125
  // ═══════════════════════════════════════════
244
- // AFD Protocol templates
126
+ // Suffix Processing
245
127
  // ═══════════════════════════════════════════
246
- export function ok(result) {
247
- return { code: "ok", result };
128
+ function stripSuffixCI(key, suffixLower) {
129
+ if (key.endsWith(suffixLower))
130
+ return key.slice(0, -suffixLower.length);
131
+ const suffixUpper = suffixLower.toUpperCase();
132
+ if (key.endsWith(suffixUpper))
133
+ return key.slice(0, -suffixUpper.length);
134
+ return null;
248
135
  }
249
- export function okTrace(result, trace) {
250
- return { code: "ok", result, trace };
136
+ function tryStripGenericCents(key) {
137
+ const code = extractCurrencyCode(key);
138
+ if (code === null)
139
+ return null;
140
+ const suffixLen = code.length + "_cents".length + 1; // _{code}_cents
141
+ const stripped = key.slice(0, -suffixLen);
142
+ if (!stripped)
143
+ return null;
144
+ return [stripped, code];
251
145
  }
252
- export function error(message) {
253
- return { code: "error", error: message };
146
+ function isInt(value) {
147
+ return typeof value === "number" && Number.isInteger(value);
254
148
  }
255
- export function errorTrace(message, trace) {
256
- return { code: "error", error: message, trace };
149
+ function isNum(value) {
150
+ return typeof value === "number";
257
151
  }
258
- export function startup(config, args, env) {
259
- return { code: "startup", config, args, env };
152
+ function tryProcessField(key, value) {
153
+ let stripped;
154
+ // Group 1: compound timestamp suffixes
155
+ stripped = stripSuffixCI(key, "_epoch_ms");
156
+ if (stripped !== null) {
157
+ if (isInt(value))
158
+ return [stripped, formatRfc3339Ms(value)];
159
+ return null;
160
+ }
161
+ stripped = stripSuffixCI(key, "_epoch_s");
162
+ if (stripped !== null) {
163
+ if (isInt(value))
164
+ return [stripped, formatRfc3339Ms(value * 1000)];
165
+ return null;
166
+ }
167
+ stripped = stripSuffixCI(key, "_epoch_ns");
168
+ if (stripped !== null) {
169
+ if (isInt(value))
170
+ return [stripped, formatRfc3339Ms(Math.floor(value / 1_000_000))];
171
+ return null;
172
+ }
173
+ // Group 2: compound currency suffixes
174
+ stripped = stripSuffixCI(key, "_usd_cents");
175
+ if (stripped !== null) {
176
+ if (isInt(value) && value >= 0)
177
+ return [stripped, `$${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
178
+ return null;
179
+ }
180
+ stripped = stripSuffixCI(key, "_eur_cents");
181
+ if (stripped !== null) {
182
+ if (isInt(value) && value >= 0)
183
+ return [stripped, `\u20ac${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")}`];
184
+ return null;
185
+ }
186
+ const gc = tryStripGenericCents(key);
187
+ if (gc !== null) {
188
+ const [gcStripped, code] = gc;
189
+ if (isInt(value) && value >= 0)
190
+ return [gcStripped, `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`];
191
+ return null;
192
+ }
193
+ // Group 3: multi-char suffixes
194
+ stripped = stripSuffixCI(key, "_rfc3339");
195
+ if (stripped !== null) {
196
+ if (typeof value === "string")
197
+ return [stripped, value];
198
+ return null;
199
+ }
200
+ stripped = stripSuffixCI(key, "_minutes");
201
+ if (stripped !== null) {
202
+ if (isNum(value))
203
+ return [stripped, `${plainScalar(value)} minutes`];
204
+ return null;
205
+ }
206
+ stripped = stripSuffixCI(key, "_hours");
207
+ if (stripped !== null) {
208
+ if (isNum(value))
209
+ return [stripped, `${plainScalar(value)} hours`];
210
+ return null;
211
+ }
212
+ stripped = stripSuffixCI(key, "_days");
213
+ if (stripped !== null) {
214
+ if (isNum(value))
215
+ return [stripped, `${plainScalar(value)} days`];
216
+ return null;
217
+ }
218
+ // Group 4: single-unit suffixes
219
+ stripped = stripSuffixCI(key, "_msats");
220
+ if (stripped !== null) {
221
+ if (isNum(value))
222
+ return [stripped, `${plainScalar(value)}msats`];
223
+ return null;
224
+ }
225
+ stripped = stripSuffixCI(key, "_sats");
226
+ if (stripped !== null) {
227
+ if (isNum(value))
228
+ return [stripped, `${plainScalar(value)}sats`];
229
+ return null;
230
+ }
231
+ stripped = stripSuffixCI(key, "_bytes");
232
+ if (stripped !== null) {
233
+ if (isInt(value))
234
+ return [stripped, formatBytesHuman(value)];
235
+ return null;
236
+ }
237
+ stripped = stripSuffixCI(key, "_percent");
238
+ if (stripped !== null) {
239
+ if (isNum(value))
240
+ return [stripped, `${plainScalar(value)}%`];
241
+ return null;
242
+ }
243
+ stripped = stripSuffixCI(key, "_secret");
244
+ if (stripped !== null)
245
+ return [stripped, "***"];
246
+ // Group 5: short suffixes (last to avoid false positives)
247
+ stripped = stripSuffixCI(key, "_btc");
248
+ if (stripped !== null) {
249
+ if (isNum(value))
250
+ return [stripped, `${plainScalar(value)} BTC`];
251
+ return null;
252
+ }
253
+ stripped = stripSuffixCI(key, "_jpy");
254
+ if (stripped !== null) {
255
+ if (isInt(value) && value >= 0)
256
+ return [stripped, `\u00a5${formatWithCommas(value)}`];
257
+ return null;
258
+ }
259
+ stripped = stripSuffixCI(key, "_ns");
260
+ if (stripped !== null) {
261
+ if (isNum(value))
262
+ return [stripped, `${plainScalar(value)}ns`];
263
+ return null;
264
+ }
265
+ stripped = stripSuffixCI(key, "_us");
266
+ if (stripped !== null) {
267
+ if (isNum(value))
268
+ return [stripped, `${plainScalar(value)}\u03bcs`];
269
+ return null;
270
+ }
271
+ stripped = stripSuffixCI(key, "_ms");
272
+ if (stripped !== null) {
273
+ const fv = formatMsValue(value);
274
+ if (fv !== null)
275
+ return [stripped, fv];
276
+ return null;
277
+ }
278
+ stripped = stripSuffixCI(key, "_s");
279
+ if (stripped !== null) {
280
+ if (isNum(value))
281
+ return [stripped, `${plainScalar(value)}s`];
282
+ return null;
283
+ }
284
+ return null;
260
285
  }
261
- export function status(code, fields) {
262
- return { ...fields, code };
286
+ function processObjectFields(obj) {
287
+ const entries = [];
288
+ for (const [k, v] of Object.entries(obj)) {
289
+ const result = tryProcessField(k, v);
290
+ if (result !== null) {
291
+ entries.push({ stripped: result[0], original: k, value: v, formatted: result[1] });
292
+ }
293
+ else {
294
+ entries.push({ stripped: k, original: k, value: v, formatted: null });
295
+ }
296
+ }
297
+ // Detect collisions
298
+ const counts = new Map();
299
+ for (const e of entries) {
300
+ counts.set(e.stripped, (counts.get(e.stripped) ?? 0) + 1);
301
+ }
302
+ // Resolve collisions: revert both key and formatted value
303
+ const result = entries.map((e) => {
304
+ if ((counts.get(e.stripped) ?? 0) > 1 && e.original !== e.stripped) {
305
+ return { key: e.original, value: e.value, formatted: null };
306
+ }
307
+ return { key: e.stripped, value: e.value, formatted: e.formatted };
308
+ });
309
+ // Sort by display key (JCS order)
310
+ result.sort((a, b) => jcsCompare(a.key, b.key));
311
+ return result;
263
312
  }
264
313
  // ═══════════════════════════════════════════
265
- // Helpers
314
+ // Formatting Helpers
266
315
  // ═══════════════════════════════════════════
316
+ function formatMsAsSeconds(ms) {
317
+ const formatted = (ms / 1000).toFixed(3);
318
+ const trimmed = formatted.replace(/0+$/, "");
319
+ if (trimmed.endsWith("."))
320
+ return trimmed + "0s";
321
+ return trimmed + "s";
322
+ }
323
+ function formatMsValue(value) {
324
+ if (!isNum(value))
325
+ return null;
326
+ if (Math.abs(value) >= 1000)
327
+ return formatMsAsSeconds(value);
328
+ return `${plainScalar(value)}ms`;
329
+ }
267
330
  function formatRfc3339Ms(ms) {
268
331
  try {
269
332
  const d = new Date(ms);
@@ -291,50 +354,152 @@ function formatBytesHuman(bytes) {
291
354
  return `${bytes}B`;
292
355
  }
293
356
  function formatWithCommas(n) {
294
- return n.toLocaleString("en-US");
295
- }
296
- /**
297
- * Parse a human-readable size string into bytes.
298
- * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
299
- * Case-insensitive. Trims whitespace. Returns null for invalid input.
300
- */
301
- export function parseSize(s) {
302
- s = s.trim();
303
- if (!s)
304
- return null;
305
- const multipliers = {
306
- b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
307
- };
308
- const last = s[s.length - 1].toLowerCase();
309
- let numStr;
310
- let mult;
311
- if (last in multipliers) {
312
- numStr = s.slice(0, -1);
313
- mult = multipliers[last];
314
- }
315
- else if ((last >= "0" && last <= "9") || last === ".") {
316
- numStr = s;
317
- mult = 1;
318
- }
319
- else {
320
- return null;
357
+ const s = String(n);
358
+ const result = [];
359
+ for (let i = 0; i < s.length; i++) {
360
+ if (i > 0 && (s.length - i) % 3 === 0)
361
+ result.push(",");
362
+ result.push(s[i]);
321
363
  }
322
- if (!numStr)
323
- return null;
324
- const n = Number(numStr);
325
- if (isNaN(n) || n < 0 || !isFinite(n))
326
- return null;
327
- return Math.trunc(n * mult);
364
+ return result.join("");
328
365
  }
366
+ /** Extract currency code from _{code}_cents / _{CODE}_CENTS suffix. */
329
367
  function extractCurrencyCode(key) {
330
- if (!key.endsWith("_cents"))
368
+ let withoutCents;
369
+ if (key.endsWith("_cents"))
370
+ withoutCents = key.slice(0, -6);
371
+ else if (key.endsWith("_CENTS"))
372
+ withoutCents = key.slice(0, -6);
373
+ else
331
374
  return null;
332
- const withoutCents = key.slice(0, -6);
333
375
  const idx = withoutCents.lastIndexOf("_");
334
376
  if (idx < 0)
335
377
  return null;
336
- return withoutCents.slice(idx + 1);
378
+ const code = withoutCents.slice(idx + 1);
379
+ if (!code)
380
+ return null;
381
+ return code;
382
+ }
383
+ // ═══════════════════════════════════════════
384
+ // YAML Rendering
385
+ // ═══════════════════════════════════════════
386
+ function renderYamlProcessed(value, indent, lines) {
387
+ const prefix = " ".repeat(indent);
388
+ if (!isObject(value)) {
389
+ lines.push(`${prefix}${yamlScalar(value)}`);
390
+ return;
391
+ }
392
+ for (const pf of processObjectFields(value)) {
393
+ if (pf.formatted !== null) {
394
+ lines.push(`${prefix}${pf.key}: "${escapeYamlStr(pf.formatted)}"`);
395
+ }
396
+ else if (isObject(pf.value)) {
397
+ if (Object.keys(pf.value).length > 0) {
398
+ lines.push(`${prefix}${pf.key}:`);
399
+ renderYamlProcessed(pf.value, indent + 1, lines);
400
+ }
401
+ else {
402
+ lines.push(`${prefix}${pf.key}: {}`);
403
+ }
404
+ }
405
+ else if (Array.isArray(pf.value)) {
406
+ if (pf.value.length === 0) {
407
+ lines.push(`${prefix}${pf.key}: []`);
408
+ }
409
+ else {
410
+ lines.push(`${prefix}${pf.key}:`);
411
+ for (const item of pf.value) {
412
+ if (isObject(item)) {
413
+ lines.push(`${prefix} -`);
414
+ renderYamlProcessed(item, indent + 2, lines);
415
+ }
416
+ else {
417
+ lines.push(`${prefix} - ${yamlScalar(item)}`);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ else {
423
+ lines.push(`${prefix}${pf.key}: ${yamlScalar(pf.value)}`);
424
+ }
425
+ }
426
+ }
427
+ function escapeYamlStr(s) {
428
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
337
429
  }
430
+ function yamlScalar(value) {
431
+ if (typeof value === "string")
432
+ return `"${escapeYamlStr(value)}"`;
433
+ if (value === null)
434
+ return "null";
435
+ if (typeof value === "boolean")
436
+ return value.toString();
437
+ if (typeof value === "number")
438
+ return value.toString();
439
+ return `"${String(value).replace(/"/g, '\\"')}"`;
440
+ }
441
+ // ═══════════════════════════════════════════
442
+ // Plain Rendering (logfmt)
443
+ // ═══════════════════════════════════════════
444
+ function collectPlainPairs(value, prefix, pairs) {
445
+ if (!isObject(value))
446
+ return;
447
+ for (const pf of processObjectFields(value)) {
448
+ const fullKey = prefix ? `${prefix}.${pf.key}` : pf.key;
449
+ if (pf.formatted !== null) {
450
+ pairs.push([fullKey, pf.formatted]);
451
+ }
452
+ else if (isObject(pf.value)) {
453
+ collectPlainPairs(pf.value, fullKey, pairs);
454
+ }
455
+ else if (Array.isArray(pf.value)) {
456
+ pairs.push([fullKey, pf.value.map((i) => plainScalar(i)).join(",")]);
457
+ }
458
+ else if (pf.value === null) {
459
+ pairs.push([fullKey, ""]);
460
+ }
461
+ else {
462
+ pairs.push([fullKey, plainScalar(pf.value)]);
463
+ }
464
+ }
465
+ }
466
+ function plainScalar(value) {
467
+ if (typeof value === "string")
468
+ return value;
469
+ if (value === null)
470
+ return "null";
471
+ if (typeof value === "boolean")
472
+ return value.toString();
473
+ if (typeof value === "number")
474
+ return value.toString();
475
+ return String(value);
476
+ }
477
+ // ═══════════════════════════════════════════
478
+ // Utilities
479
+ // ═══════════════════════════════════════════
338
480
  function isObject(value) {
339
481
  return typeof value === "object" && value !== null && !Array.isArray(value);
340
482
  }
483
+ function jcsCompare(a, b) {
484
+ const ua = Array.from(a).flatMap((c) => {
485
+ const cp = c.codePointAt(0);
486
+ if (cp > 0xffff) {
487
+ const offset = cp - 0x10000;
488
+ return [0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff)];
489
+ }
490
+ return [cp];
491
+ });
492
+ const ub = Array.from(b).flatMap((c) => {
493
+ const cp = c.codePointAt(0);
494
+ if (cp > 0xffff) {
495
+ const offset = cp - 0x10000;
496
+ return [0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff)];
497
+ }
498
+ return [cp];
499
+ });
500
+ for (let i = 0; i < ua.length && i < ub.length; i++) {
501
+ if (ua[i] !== ub[i])
502
+ return ua[i] - ub[i];
503
+ }
504
+ return ua.length - ub.length;
505
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  /**
2
2
  * Agent-First Data (AFD) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { OutputFormat, formatValue, formatPretty, toYaml, toPlain, redactSecrets, parseSize } from "./format.js";
5
- export { ok, okTrace, error, errorTrace, startup, status } from "./format.js";
4
+ export { buildJsonStartup, buildJsonOk, buildJsonError, buildJson, outputJson, outputYaml, outputPlain, internalRedactSecrets, parseSize } from "./format.js";
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 { OutputFormat, formatValue, formatPretty, toYaml, toPlain, redactSecrets, parseSize } from "./format.js";
5
- export { ok, okTrace, error, errorTrace, startup, status } from "./format.js";
4
+ export { buildJsonStartup, buildJsonOk, buildJsonError, buildJson, outputJson, outputYaml, outputPlain, internalRedactSecrets, parseSize } from "./format.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.1.2",
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.7"
25
+ "typescript": "^5.0"
26
26
  }
27
27
  }