@acpfx/recorder 0.2.0 → 0.2.3

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