@acpfx/recorder 0.2.2 → 0.2.4

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024-2026 acpfx contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @acpfx/recorder
2
+
3
+ Records all pipeline events to JSONL and audio tracks to WAV files. Observes the full event stream for debugging and analysis.
4
+
5
+ ## Usage
6
+
7
+ This package is a pipeline node for [@acpfx/cli](../orchestrator/README.md). See the CLI package for installation and usage.
8
+
9
+ ## Manifest
10
+
11
+ - **Consumes:** all event types
12
+ - **Emits:** `lifecycle.ready`, `lifecycle.done`
13
+
14
+ ## Settings
15
+
16
+ | Name | Type | Default | Description |
17
+ |------|------|---------|-------------|
18
+ | `outputDir` | string | `./recordings` | Directory to write recordings to |
19
+
20
+ ## Pipeline Example
21
+
22
+ ```yaml
23
+ nodes:
24
+ recorder:
25
+ use: "@acpfx/recorder"
26
+ settings: { outputDir: ./recordings }
27
+ ```
28
+
29
+ Wire the recorder as an output of any node whose events you want to capture.
30
+
31
+ ## License
32
+
33
+ ISC
@@ -1,116 +1,217 @@
1
- /**
2
- * recorder node — captures all events to events.jsonl, writes audio tracks
3
- * to WAV files, generates conversation.wav and timeline.html.
4
- *
5
- * Settings (via ACPFX_SETTINGS):
6
- * outputDir?: string — output directory (default: ./recordings/<run-id>)
7
- */
1
+ #!/usr/bin/env node
8
2
 
3
+ // src/index.ts
9
4
  import {
10
5
  mkdirSync,
11
6
  createWriteStream,
12
7
  writeFileSync,
13
- readFileSync,
14
- type WriteStream,
8
+ readFileSync as readFileSync2
15
9
  } from "node:fs";
16
10
  import { open } from "node:fs/promises";
17
- import { join, resolve } from "node:path";
11
+ import { join as join2, resolve } from "node:path";
18
12
  import { randomUUID } from "node:crypto";
19
- import { emit, log, onEvent, handleManifestFlag } from "@acpfx/node-sdk";
20
13
 
21
- handleManifestFlag();
22
-
23
- type Settings = {
24
- outputDir?: string;
25
- };
14
+ // ../node-sdk/src/index.ts
15
+ import { createInterface } from "node:readline";
26
16
 
27
- const settings: Settings = JSON.parse(process.env.ACPFX_SETTINGS || "{}");
28
- const RUN_ID = randomUUID().slice(0, 8);
29
- const OUTPUT_DIR = resolve(settings.outputDir ?? "./recordings", RUN_ID);
17
+ // ../core/src/config.ts
18
+ import { parse as parseYaml } from "yaml";
30
19
 
31
- const SAMPLE_RATE = 16000;
32
- const CHANNELS = 1;
33
- const BYTES_PER_SAMPLE = 2;
20
+ // ../core/src/manifest.ts
21
+ import { readFileSync } from "node:fs";
22
+ import { join, dirname } from "node:path";
23
+ import { z as z2 } from "zod";
34
24
 
35
- // State
36
- let eventsStream: WriteStream;
37
- let startTime = Date.now();
38
- const allEvents: Array<Record<string, unknown>> = [];
25
+ // ../core/src/acpfx-flags.ts
26
+ import { z } from "zod";
27
+ var SetupCheckResponseSchema = z.object({
28
+ needed: z.boolean(),
29
+ description: z.string().optional()
30
+ });
31
+ var SetupProgressSchema = z.discriminatedUnion("type", [
32
+ z.object({
33
+ type: z.literal("progress"),
34
+ message: z.string(),
35
+ pct: z.number().optional()
36
+ }),
37
+ z.object({ type: z.literal("complete"), message: z.string() }),
38
+ z.object({ type: z.literal("error"), message: z.string() })
39
+ ]);
40
+ var UnsupportedFlagResponseSchema = z.object({
41
+ unsupported: z.boolean(),
42
+ flag: z.string()
43
+ });
39
44
 
40
- // Audio track writers
41
- type TrackWriter = {
42
- stream: WriteStream;
43
- path: string;
44
- bytesWritten: number;
45
- };
46
- const tracks = new Map<string, TrackWriter>();
45
+ // ../core/src/manifest.ts
46
+ var ArgumentTypeSchema = z2.enum(["string", "number", "boolean"]);
47
+ var ManifestArgumentSchema = z2.object({
48
+ type: ArgumentTypeSchema,
49
+ default: z2.unknown().optional(),
50
+ description: z2.string().optional(),
51
+ required: z2.boolean().optional(),
52
+ enum: z2.array(z2.unknown()).optional()
53
+ });
54
+ var ManifestEnvFieldSchema = z2.object({
55
+ required: z2.boolean().optional(),
56
+ description: z2.string().optional()
57
+ });
58
+ var NodeManifestSchema = z2.object({
59
+ name: z2.string(),
60
+ description: z2.string().optional(),
61
+ consumes: z2.array(z2.string()),
62
+ emits: z2.array(z2.string()),
63
+ arguments: z2.record(z2.string(), ManifestArgumentSchema).optional(),
64
+ additional_arguments: z2.boolean().optional(),
65
+ env: z2.record(z2.string(), ManifestEnvFieldSchema).optional()
66
+ });
67
+ function handleAcpfxFlags(manifestPath) {
68
+ const acpfxFlag = process.argv.find((a) => a.startsWith("--acpfx-"));
69
+ const legacyManifest = process.argv.includes("--manifest");
70
+ if (!acpfxFlag && !legacyManifest) return;
71
+ const flag = acpfxFlag ?? "--acpfx-manifest";
72
+ switch (flag) {
73
+ case "--acpfx-manifest":
74
+ printManifest(manifestPath);
75
+ break;
76
+ case "--acpfx-setup-check":
77
+ process.stdout.write(JSON.stringify({ needed: false }) + "\n");
78
+ process.exit(0);
79
+ break;
80
+ default:
81
+ process.stdout.write(
82
+ JSON.stringify({ unsupported: true, flag }) + "\n"
83
+ );
84
+ process.exit(0);
85
+ }
86
+ }
87
+ function handleManifestFlag(manifestPath) {
88
+ handleAcpfxFlags(manifestPath);
89
+ }
90
+ function printManifest(manifestPath) {
91
+ if (!manifestPath) {
92
+ const script = process.argv[1];
93
+ const scriptDir = dirname(script);
94
+ const scriptBase = script.replace(/\.[^.]+$/, "");
95
+ const colocated = `${scriptBase}.manifest.json`;
96
+ try {
97
+ readFileSync(colocated);
98
+ manifestPath = colocated;
99
+ } catch {
100
+ manifestPath = join(scriptDir, "manifest.json");
101
+ }
102
+ }
103
+ try {
104
+ const content = readFileSync(manifestPath, "utf8");
105
+ process.stdout.write(content.trim() + "\n");
106
+ process.exit(0);
107
+ } catch (err) {
108
+ process.stderr.write(`Failed to read manifest: ${err}
109
+ `);
110
+ process.exit(1);
111
+ }
112
+ }
47
113
 
114
+ // ../node-sdk/src/index.ts
115
+ var NODE_NAME = process.env.ACPFX_NODE_NAME ?? "unknown";
116
+ function emit(event) {
117
+ process.stdout.write(JSON.stringify(event) + "\n");
118
+ }
119
+ function log(level, message) {
120
+ emit({ type: "log", level, component: NODE_NAME, message });
121
+ }
122
+ log.info = (message) => log("info", message);
123
+ log.warn = (message) => log("warn", message);
124
+ log.error = (message) => log("error", message);
125
+ log.debug = (message) => log("debug", message);
126
+ function onEvent(handler) {
127
+ const rl2 = createInterface({ input: process.stdin });
128
+ rl2.on("line", (line) => {
129
+ if (!line.trim()) return;
130
+ try {
131
+ const event = JSON.parse(line);
132
+ handler(event);
133
+ } catch {
134
+ }
135
+ });
136
+ return rl2;
137
+ }
48
138
 
49
- function createWavHeader(dataSize: number, sr: number, ch: number): Buffer {
139
+ // src/index.ts
140
+ handleManifestFlag();
141
+ var settings = JSON.parse(process.env.ACPFX_SETTINGS || "{}");
142
+ var RUN_ID = randomUUID().slice(0, 8);
143
+ var OUTPUT_DIR = resolve(settings.outputDir ?? "./recordings", RUN_ID);
144
+ var SAMPLE_RATE = 16e3;
145
+ var CHANNELS = 1;
146
+ var BYTES_PER_SAMPLE = 2;
147
+ var eventsStream;
148
+ var startTime = Date.now();
149
+ var allEvents = [];
150
+ var tracks = /* @__PURE__ */ new Map();
151
+ function createWavHeader(dataSize, sr, ch) {
50
152
  const bitsPerSample = 16;
51
- const byteRate = (sr * ch * bitsPerSample) / 8;
52
- const blockAlign = (ch * bitsPerSample) / 8;
153
+ const byteRate = sr * ch * bitsPerSample / 8;
154
+ const blockAlign = ch * bitsPerSample / 8;
53
155
  const header = Buffer.alloc(44);
54
156
  let off = 0;
55
- header.write("RIFF", off); off += 4;
56
- header.writeUInt32LE(dataSize + 36, off); off += 4;
57
- header.write("WAVE", off); off += 4;
58
- header.write("fmt ", off); off += 4;
59
- header.writeUInt32LE(16, off); off += 4;
60
- header.writeUInt16LE(1, off); off += 2;
61
- header.writeUInt16LE(ch, off); off += 2;
62
- header.writeUInt32LE(sr, off); off += 4;
63
- header.writeUInt32LE(byteRate, off); off += 4;
64
- header.writeUInt16LE(blockAlign, off); off += 2;
65
- header.writeUInt16LE(bitsPerSample, off); off += 2;
66
- header.write("data", off); off += 4;
157
+ header.write("RIFF", off);
158
+ off += 4;
159
+ header.writeUInt32LE(dataSize + 36, off);
160
+ off += 4;
161
+ header.write("WAVE", off);
162
+ off += 4;
163
+ header.write("fmt ", off);
164
+ off += 4;
165
+ header.writeUInt32LE(16, off);
166
+ off += 4;
167
+ header.writeUInt16LE(1, off);
168
+ off += 2;
169
+ header.writeUInt16LE(ch, off);
170
+ off += 2;
171
+ header.writeUInt32LE(sr, off);
172
+ off += 4;
173
+ header.writeUInt32LE(byteRate, off);
174
+ off += 4;
175
+ header.writeUInt16LE(blockAlign, off);
176
+ off += 2;
177
+ header.writeUInt16LE(bitsPerSample, off);
178
+ off += 2;
179
+ header.write("data", off);
180
+ off += 4;
67
181
  header.writeUInt32LE(dataSize, off);
68
182
  return header;
69
183
  }
70
-
71
- function getOrCreateTrack(trackId: string): TrackWriter {
184
+ function getOrCreateTrack(trackId) {
72
185
  let tw = tracks.get(trackId);
73
186
  if (tw) return tw;
74
-
75
187
  const filename = `${trackId}.wav`;
76
- const path = join(OUTPUT_DIR, filename);
188
+ const path = join2(OUTPUT_DIR, filename);
77
189
  const stream = createWriteStream(path);
78
- // Write placeholder header
79
190
  stream.write(Buffer.alloc(44));
80
191
  tw = { stream, path, bytesWritten: 0 };
81
192
  tracks.set(trackId, tw);
82
193
  return tw;
83
194
  }
84
-
85
- async function finalizeTrack(tw: TrackWriter): Promise<void> {
86
- await new Promise<void>((res, rej) => {
195
+ async function finalizeTrack(tw) {
196
+ await new Promise((res, rej) => {
87
197
  tw.stream.end(() => res());
88
198
  tw.stream.on("error", rej);
89
199
  });
90
-
91
200
  const header = createWavHeader(tw.bytesWritten, SAMPLE_RATE, CHANNELS);
92
201
  const fd = await open(tw.path, "r+");
93
202
  await fd.write(header, 0, header.length, 0);
94
203
  await fd.close();
95
204
  }
96
-
97
- function generateConversationWav(): void {
98
- // Merge input (mic) and output (tts) tracks into a single timeline WAV.
99
- // We place them sequentially: input audio, then a gap, then output audio.
100
- // Timeline positions come from event timestamps.
101
-
205
+ function generateConversationWav() {
102
206
  const micTrack = tracks.get("mic");
103
207
  const ttsTrack = tracks.get("tts");
104
208
  if (!micTrack && !ttsTrack) return;
105
-
106
- // Find the first and last audio chunk timestamps for each track
107
209
  let micStartMs = Infinity, micEndMs = 0;
108
210
  let ttsStartMs = Infinity, ttsEndMs = 0;
109
-
110
211
  for (const ev of allEvents) {
111
212
  if (ev.type === "audio.chunk") {
112
- const ts = (ev.ts as number) ?? 0;
113
- const dur = (ev.durationMs as number) ?? 0;
213
+ const ts = ev.ts ?? 0;
214
+ const dur = ev.durationMs ?? 0;
114
215
  if (ev.trackId === "mic" || ev._from === "mic") {
115
216
  micStartMs = Math.min(micStartMs, ts);
116
217
  micEndMs = Math.max(micEndMs, ts + dur);
@@ -121,35 +222,25 @@ function generateConversationWav(): void {
121
222
  }
122
223
  }
123
224
  }
124
-
125
- // Calculate total duration and offsets relative to the earliest timestamp
126
225
  const globalStart = Math.min(
127
226
  micStartMs === Infinity ? Infinity : micStartMs,
128
- ttsStartMs === Infinity ? Infinity : ttsStartMs,
227
+ ttsStartMs === Infinity ? Infinity : ttsStartMs
129
228
  );
130
229
  if (globalStart === Infinity) return;
131
-
132
230
  const globalEnd = Math.max(micEndMs, ttsEndMs);
133
231
  const totalDurationMs = globalEnd - globalStart;
134
- const totalSamples = Math.ceil((totalDurationMs / 1000) * SAMPLE_RATE);
232
+ const totalSamples = Math.ceil(totalDurationMs / 1e3 * SAMPLE_RATE);
135
233
  const totalBytes = totalSamples * CHANNELS * BYTES_PER_SAMPLE;
136
-
137
- // Create a silent buffer for the full duration
138
234
  const pcm = Buffer.alloc(totalBytes);
139
-
140
- // Write mic audio at correct timeline position
141
235
  for (const ev of allEvents) {
142
236
  if (ev.type !== "audio.chunk") continue;
143
- const ts = (ev.ts as number) ?? 0;
144
- const trackId = (ev.trackId as string) ?? (ev._from as string) ?? "";
237
+ const ts = ev.ts ?? 0;
238
+ const trackId = ev.trackId ?? ev._from ?? "";
145
239
  if (trackId !== "mic" && trackId !== "tts") continue;
146
-
147
240
  const offsetMs = ts - globalStart;
148
- const offsetSamples = Math.floor((offsetMs / 1000) * SAMPLE_RATE);
241
+ const offsetSamples = Math.floor(offsetMs / 1e3 * SAMPLE_RATE);
149
242
  const offsetBytes = offsetSamples * CHANNELS * BYTES_PER_SAMPLE;
150
- const data = Buffer.from((ev.data as string) ?? "", "base64");
151
-
152
- // Mix: add samples (clamped to int16 range)
243
+ const data = Buffer.from(ev.data ?? "", "base64");
153
244
  for (let i = 0; i < data.length && offsetBytes + i + 1 < pcm.length; i += 2) {
154
245
  const existing = pcm.readInt16LE(offsetBytes + i);
155
246
  const incoming = data.readInt16LE(i);
@@ -157,48 +248,32 @@ function generateConversationWav(): void {
157
248
  pcm.writeInt16LE(mixed, offsetBytes + i);
158
249
  }
159
250
  }
160
-
161
- const convPath = join(OUTPUT_DIR, "conversation.wav");
251
+ const convPath = join2(OUTPUT_DIR, "conversation.wav");
162
252
  const header = createWavHeader(pcm.length, SAMPLE_RATE, CHANNELS);
163
253
  writeFileSync(convPath, Buffer.concat([header, pcm]));
164
254
  log.info(`Wrote conversation.wav (${totalDurationMs}ms)`);
165
255
  }
166
-
167
- function generateTimelineHtml(): void {
168
- // Read WAV files as base64 for embedding
256
+ function generateTimelineHtml() {
169
257
  let inputWavB64 = "";
170
258
  let outputWavB64 = "";
171
- const micPath = join(OUTPUT_DIR, "mic.wav");
172
- const ttsPath = join(OUTPUT_DIR, "tts.wav");
173
- try { inputWavB64 = readFileSync(micPath).toString("base64"); } catch {}
174
- try { outputWavB64 = readFileSync(ttsPath).toString("base64"); } catch {}
175
-
176
- // Prepare event markers
177
- const markers = allEvents
178
- .filter((ev) => {
179
- const t = ev.type as string;
180
- return (
181
- t === "speech.partial" ||
182
- t === "speech.delta" ||
183
- t === "speech.final" ||
184
- t === "speech.pause" ||
185
- t === "agent.submit" ||
186
- t === "agent.delta" ||
187
- t === "agent.complete" ||
188
- t === "control.interrupt"
189
- );
190
- })
191
- .map((ev) => ({
192
- time: ((ev.ts as number) - startTime) / 1000,
193
- type: ev.type,
194
- text:
195
- (ev as any).text ??
196
- (ev as any).delta ??
197
- (ev as any).pendingText ??
198
- (ev as any).reason ??
199
- "",
200
- }));
201
-
259
+ const micPath = join2(OUTPUT_DIR, "mic.wav");
260
+ const ttsPath = join2(OUTPUT_DIR, "tts.wav");
261
+ try {
262
+ inputWavB64 = readFileSync2(micPath).toString("base64");
263
+ } catch {
264
+ }
265
+ try {
266
+ outputWavB64 = readFileSync2(ttsPath).toString("base64");
267
+ } catch {
268
+ }
269
+ const markers = allEvents.filter((ev) => {
270
+ const t = ev.type;
271
+ return t === "speech.partial" || t === "speech.delta" || t === "speech.final" || t === "speech.pause" || t === "agent.submit" || t === "agent.delta" || t === "agent.complete" || t === "control.interrupt";
272
+ }).map((ev) => ({
273
+ time: (ev.ts - startTime) / 1e3,
274
+ type: ev.type,
275
+ text: ev.text ?? ev.delta ?? ev.pendingText ?? ev.reason ?? ""
276
+ }));
202
277
  const html = `<!DOCTYPE html>
203
278
  <html lang="en">
204
279
  <head>
@@ -291,71 +366,51 @@ function playPause() {
291
366
  </script>
292
367
  </body>
293
368
  </html>`;
294
-
295
- const htmlPath = join(OUTPUT_DIR, "timeline.html");
369
+ const htmlPath = join2(OUTPUT_DIR, "timeline.html");
296
370
  writeFileSync(htmlPath, html);
297
371
  log.info(`Wrote timeline.html`);
298
372
  }
299
-
300
- async function finalize(): Promise<void> {
301
- // Close events stream
373
+ async function finalize() {
302
374
  if (eventsStream) {
303
- await new Promise<void>((res) => eventsStream.end(() => res()));
375
+ await new Promise((res) => eventsStream.end(() => res()));
304
376
  }
305
-
306
- // Finalize all audio tracks
307
377
  for (const tw of tracks.values()) {
308
378
  await finalizeTrack(tw);
309
379
  }
310
-
311
- // Generate conversation.wav
312
380
  try {
313
381
  generateConversationWav();
314
382
  } catch (err) {
315
383
  log.error(`Error generating conversation.wav: ${err}`);
316
384
  }
317
-
318
- // Generate timeline.html
319
385
  try {
320
386
  generateTimelineHtml();
321
387
  } catch (err) {
322
388
  log.error(`Error generating timeline.html: ${err}`);
323
389
  }
324
-
325
390
  log.info(`Recording saved to ${OUTPUT_DIR}`);
326
391
  }
327
-
328
- // --- Main ---
329
-
330
392
  mkdirSync(OUTPUT_DIR, { recursive: true });
331
- eventsStream = createWriteStream(join(OUTPUT_DIR, "events.jsonl"));
393
+ eventsStream = createWriteStream(join2(OUTPUT_DIR, "events.jsonl"));
332
394
  startTime = Date.now();
333
-
334
395
  emit({ type: "lifecycle.ready", component: "recorder" });
335
396
  log.info(`Recording to ${OUTPUT_DIR}`);
336
-
337
- const rl = onEvent((event) => {
338
- // Record every event to events.jsonl
397
+ var rl = onEvent((event) => {
339
398
  allEvents.push(event);
340
399
  eventsStream.write(JSON.stringify(event) + "\n");
341
-
342
- // Capture audio tracks
343
400
  if (event.type === "audio.chunk") {
344
- const trackId = (event.trackId as string) ?? (event._from as string) ?? "unknown";
401
+ const trackId = event.trackId ?? event._from ?? "unknown";
345
402
  const tw = getOrCreateTrack(trackId);
346
- const pcm = Buffer.from((event.data as string) ?? "", "base64");
403
+ const pcm = Buffer.from(event.data ?? "", "base64");
347
404
  tw.stream.write(pcm);
348
405
  tw.bytesWritten += pcm.length;
349
406
  }
350
407
  });
351
-
352
408
  rl.on("close", () => {
353
409
  finalize().then(() => {
354
410
  emit({ type: "lifecycle.done", component: "recorder" });
355
411
  process.exit(0);
356
412
  });
357
413
  });
358
-
359
414
  process.on("SIGTERM", () => {
360
415
  finalize().then(() => process.exit(0));
361
416
  });
@@ -0,0 +1 @@
1
+ {"name":"recorder","description":"Records all events to JSONL and audio tracks to WAV files","consumes":["audio.chunk","audio.level","speech.partial","speech.delta","speech.final","speech.pause","agent.submit","agent.delta","agent.complete","agent.thinking","agent.tool_start","agent.tool_done","control.interrupt","control.state","control.error","lifecycle.ready","lifecycle.done","log","player.status"],"emits":["lifecycle.ready","lifecycle.done"],"arguments":{"outputDir":{"type":"string","description":"Directory to write recordings to (default: ./recordings)"}}}
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@acpfx/recorder",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "acpfx-recorder": "./dist/index.js"
7
7
  },
8
8
  "main": "./dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "manifest.yaml"
12
+ ],
9
13
  "dependencies": {
10
14
  "@acpfx/core": "0.4.0",
11
15
  "@acpfx/node-sdk": "0.3.0"
12
16
  },
13
17
  "scripts": {
14
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external"
18
+ "build": "esbuild src/index.ts --bundle --banner:js=\"#!/usr/bin/env node\" --platform=node --format=esm --outfile=dist/index.js --packages=external && node ../../scripts/copy-manifest.js"
15
19
  }
16
20
  }
package/CHANGELOG.md DELETED
@@ -1,38 +0,0 @@
1
- # @acpfx/recorder
2
-
3
- ## 0.2.2
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0e6838e]
8
- - @acpfx/core@0.4.0
9
- - @acpfx/node-sdk@0.3.0
10
-
11
- ## 0.2.1
12
-
13
- ### Patch Changes
14
-
15
- - Updated dependencies [79c6694]
16
- - Updated dependencies [a0320a1]
17
- - @acpfx/core@0.3.0
18
- - @acpfx/node-sdk@0.2.1
19
-
20
- ## 0.2.0
21
-
22
- ### Minor Changes
23
-
24
- - d757640: Initial release: type-safe contracts, Rust orchestrator, manifest-driven event filtering
25
-
26
- - Rust schema crate as canonical event type source of truth with codegen to TypeScript + Zod
27
- - Node manifests (manifest.yaml) declaring consumes/emits contracts
28
- - Orchestrator event filtering: nodes only receive declared events
29
- - Rust orchestrator with ratatui TUI (--ui flag)
30
- - node-sdk with structured logging helpers
31
- - CI/CD with GitHub Actions and changesets
32
- - Platform-specific npm packages for Rust binaries (esbuild-style distribution)
33
-
34
- ### Patch Changes
35
-
36
- - Updated dependencies [d757640]
37
- - @acpfx/core@0.2.0
38
- - @acpfx/node-sdk@0.2.0