driggsby 0.10.1 → 0.10.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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -101,6 +101,11 @@ export const STEPS = [
101
101
  }
102
102
  ];
103
103
  export const LINES = [
104
+ {
105
+ at: 30,
106
+ who: "att",
107
+ text: "Thanks for calling AT&T. In a few words, tell me what you’re calling about."
108
+ },
104
109
  {
105
110
  at: 40.1,
106
111
  who: "agent",
@@ -209,7 +214,7 @@ export const LINES = [
209
214
  {
210
215
  at: 1506,
211
216
  who: "agent",
212
- text: "Thanks so much, you too. Bye!"
217
+ text: "That sounds perfect. Thanks so much for your help. Bye!"
213
218
  }
214
219
  ];
215
220
  export const SILENT = {
@@ -219,10 +224,10 @@ export const SILENT = {
219
224
  speak: new Map(),
220
225
  };
221
226
  export const VOICED = {
222
- keys: [[0, 0], [40.1, 1.745], [76.4, 3.234], [77.3, 7.332], [161.1, 9.354], [201.6, 12.746], [238.4, 13.596], [753.6, 20.236], [803.1, 22.061], [829.1, 31.517], [1110.5, 34.671], [1128.7, 37.725], [1130.1, 40.504], [1148.5, 44.816], [1205.6, 48.481], [1228.7, 53.955], [1260.8, 57.989], [1262.5, 62.902], [1275, 66.519], [1318.6, 69.236], [1331.3, 73.208], [1365.5, 74.711], [1506, 77.299], [1509.3, 79.713]],
223
- endStart: 80.513,
224
- total: 90.513,
225
- speak: new Map([[40.1, 1.039], [76.4, 3.648], [77.3, 0.361], [161.1, 2.943], [201.6, 0.4], [238.4, 2.19], [753.6, 1.374], [803.1, 9.006], [829.1, 1.496], [1110.5, 2.604], [1128.7, 2.329], [1130.1, 3.862], [1148.5, 3.215], [1205.6, 5.024], [1228.7, 3.584], [1260.8, 4.464], [1262.5, 3.167], [1275, 2.267], [1318.6, 3.522], [1331.3, 0.308], [1365.5, 2.138], [1506, 1.614]]),
227
+ keys: [[0, 0], [30, 1.65], [40.1, 6.103], [76.4, 7.592], [77.3, 11.69], [161.1, 13.711], [201.6, 17.104], [238.4, 17.954], [753.6, 24.594], [803.1, 26.418], [829.1, 35.875], [1110.5, 39.029], [1128.7, 42.082], [1130.1, 44.862], [1148.5, 49.174], [1205.6, 52.839], [1228.7, 58.313], [1260.8, 62.346], [1262.5, 67.26], [1275, 70.877], [1318.6, 73.594], [1331.3, 77.566], [1365.5, 79.069], [1506, 81.657], [1509.3, 85.737]],
228
+ endStart: 86.537,
229
+ total: 96.537,
230
+ speak: new Map([[30, 4.003], [40.1, 1.039], [76.4, 3.648], [77.3, 0.361], [161.1, 2.943], [201.6, 0.4], [238.4, 2.19], [753.6, 1.374], [803.1, 9.006], [829.1, 1.496], [1110.5, 2.604], [1128.7, 2.329], [1130.1, 3.862], [1148.5, 3.215], [1205.6, 5.024], [1228.7, 3.584], [1260.8, 4.464], [1262.5, 3.167], [1275, 2.267], [1318.6, 3.522], [1331.3, 0.308], [1365.5, 2.138], [1506, 3.28]]),
226
231
  };
227
232
  // Each sound and the replay second it starts at, on the voiced timeline.
228
233
  export const CUES = [
@@ -231,99 +236,103 @@ export const CUES = [
231
236
  file: "att-00.m4a"
232
237
  },
233
238
  {
234
- at: 1.745,
239
+ at: 1.65,
235
240
  file: "att-01.m4a"
236
241
  },
237
242
  {
238
- at: 3.234,
243
+ at: 6.103,
239
244
  file: "att-02.m4a"
240
245
  },
241
246
  {
242
- at: 7.332,
247
+ at: 7.592,
243
248
  file: "att-03.m4a"
244
249
  },
245
250
  {
246
- at: 9.354,
251
+ at: 11.69,
247
252
  file: "att-04.m4a"
248
253
  },
249
254
  {
250
- at: 12.746,
255
+ at: 13.711,
251
256
  file: "att-05.m4a"
252
257
  },
253
258
  {
254
- at: 13.596,
259
+ at: 17.104,
255
260
  file: "att-06.m4a"
256
261
  },
257
262
  {
258
- at: 15.986,
263
+ at: 17.954,
259
264
  file: "att-07.m4a"
260
265
  },
261
266
  {
262
- at: 20.236,
267
+ at: 20.344,
263
268
  file: "att-08.m4a"
264
269
  },
265
270
  {
266
- at: 22.061,
271
+ at: 24.594,
267
272
  file: "att-09.m4a"
268
273
  },
269
274
  {
270
- at: 31.517,
275
+ at: 26.418,
271
276
  file: "att-10.m4a"
272
277
  },
273
278
  {
274
- at: 34.671,
279
+ at: 35.875,
275
280
  file: "att-11.m4a"
276
281
  },
277
282
  {
278
- at: 37.725,
283
+ at: 39.029,
279
284
  file: "att-12.m4a"
280
285
  },
281
286
  {
282
- at: 40.504,
287
+ at: 42.082,
283
288
  file: "att-13.m4a"
284
289
  },
285
290
  {
286
- at: 44.816,
291
+ at: 44.862,
287
292
  file: "att-14.m4a"
288
293
  },
289
294
  {
290
- at: 48.481,
295
+ at: 49.174,
291
296
  file: "att-15.m4a"
292
297
  },
293
298
  {
294
- at: 53.955,
299
+ at: 52.839,
295
300
  file: "att-16.m4a"
296
301
  },
297
302
  {
298
- at: 57.989,
303
+ at: 58.313,
299
304
  file: "att-17.m4a"
300
305
  },
301
306
  {
302
- at: 62.902,
307
+ at: 62.346,
303
308
  file: "att-18.m4a"
304
309
  },
305
310
  {
306
- at: 66.519,
311
+ at: 67.26,
307
312
  file: "att-19.m4a"
308
313
  },
309
314
  {
310
- at: 69.236,
315
+ at: 70.877,
311
316
  file: "att-20.m4a"
312
317
  },
313
318
  {
314
- at: 73.208,
319
+ at: 73.594,
315
320
  file: "att-21.m4a"
316
321
  },
317
322
  {
318
- at: 74.711,
323
+ at: 77.566,
319
324
  file: "att-22.m4a"
320
325
  },
321
326
  {
322
- at: 77.299,
327
+ at: 79.069,
323
328
  file: "att-23.m4a"
324
329
  },
325
330
  {
326
- at: 80.513,
331
+ at: 81.657,
327
332
  file: "att-24.m4a"
333
+ },
334
+ {
335
+ at: 86.537,
336
+ file: "att-25.m4a"
328
337
  }
329
338
  ];
@@ -87,7 +87,7 @@ export function renderFrame(input) {
87
87
  }
88
88
  });
89
89
  if (ended)
90
- feed.push(...result(input.pt - timeline.endStart, w, narrow));
90
+ feed.push(...result(input.pt - timeline.endStart, w, narrow, input.vote ?? "idle"));
91
91
  return { feed, doneAt };
92
92
  };
93
93
  const room = rows - 6;
@@ -154,7 +154,7 @@ function checkRows(checks, real, inner, narrow, guide) {
154
154
  out.push(line(guide(), ...checks.slice(r, r + 2).flatMap((c) => check(c, cw))));
155
155
  return out;
156
156
  }
157
- function result(t, w, narrow) {
157
+ function result(t, w, narrow, vote) {
158
158
  const after = CALL.before + (CALL.after - CALL.before) * ease(t / 0.9);
159
159
  const saved = `${money((CALL.before - CALL.after) * CALL.months)} saved`;
160
160
  const at = (n) => t >= 0.15 * n;
@@ -172,7 +172,9 @@ function result(t, w, narrow) {
172
172
  if (at(2))
173
173
  out.push(line(seg("Verizon, same week: ", C.mute), seg("$95.71", C.faint, { strike: true }), seg(" → ", C.mute), seg("$74.01", C.money)));
174
174
  if (at(3))
175
- out.push(line(), line(seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" Vote to roll this out", C.text, { bold: true })));
175
+ out.push(line(), vote === "counted"
176
+ ? line(seg("✓", C.money, { bold: true }), seg(" Your vote is in. Thanks.", C.text, { bold: true }))
177
+ : line(seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" Vote to roll this out", C.text, { bold: true })));
176
178
  return out;
177
179
  }
178
180
  // Too tall for the window: fold the earliest finished steps into one line
@@ -201,12 +203,19 @@ function bottomBar(real, pt, ended, w, narrow, timeline) {
201
203
  return spread(left, right, w);
202
204
  }
203
205
  function help(input, w, narrow) {
204
- if (input.join === "open") {
205
- return spread([seg("Count your vote to roll this out?", C.text, { bold: true })], [seg("enter", C.money, { bold: true }), seg(" yes ", C.mute), seg("esc", C.text), seg(" not now", C.mute)], w);
206
- }
207
206
  const keys = narrow
208
207
  ? [["space", input.paused ? "play" : "pause"], ["q", "quit"]]
209
208
  : [["space", input.paused ? "play" : "pause"], ["f", input.fast ? "1×" : "2×"], ["r", "replay"], ["q", "quit"]];
210
209
  const left = keys.flatMap(([key, what], i) => [seg((i > 0 ? (narrow ? " " : " · ") : "") + key, C.mute), seg(` ${what}`, C.faint)]);
211
- return spread(left, [seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" vote to roll this out", C.money)], w);
210
+ // j belongs to the ending: nothing about it shows while the call plays.
211
+ if (input.pt < input.timeline.endStart)
212
+ return line(...left);
213
+ // Once pressed, this spot says how the vote went.
214
+ const state = input.vote ?? "idle";
215
+ const vote = state === "counting" ? [seg("counting your vote…", C.mute)]
216
+ : state === "counted" ? [seg("✓ ", C.money, { bold: true }), seg("your vote is in", C.money)]
217
+ : state === "closed" ? [seg("voting has closed", C.faint)]
218
+ : state === "failed" ? [seg("couldn’t count it · ", C.faint), seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" try again", C.money)]
219
+ : [seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" vote to roll this out", C.money)];
220
+ return spread(left, vote, w);
212
221
  }
@@ -2,70 +2,88 @@
2
2
  // internet bill, in the terminal. On a Mac it plays with sound (recreated
3
3
  // voices, the call's real words), paced around them; elsewhere, or with
4
4
  // --quiet, it plays silently and faster. Space pauses, f plays at 2x, r
5
- // starts over, the arrows move two seconds, q quits, and j votes to roll the
6
- // feature out, signing in first if this machine isn't signed in.
7
- import { spawn } from "node:child_process";
8
- import { existsSync } from "node:fs";
9
- import { fileURLToPath } from "node:url";
5
+ // starts over, the arrows move two seconds, and q quits; the voice follows
6
+ // each of them in place. Once the call has played to the end, j votes to roll
7
+ // the feature out: signed in, the vote counts in place; signed out, it signs
8
+ // in first, votes, and the command ends.
10
9
  import { CliError } from "../cli-error.js";
11
10
  import { defaultCredentialEnvironment, readStoredToken } from "../credentials/store.js";
12
11
  import { deployFailure, requireDeploySession } from "../deploy/api-session.js";
13
12
  import { runLogin } from "../login/login.js";
14
13
  import { CUES, SILENT, VOICED } from "./att-story.js";
15
14
  import { MIN_COLS, MIN_ROWS, renderFrame } from "./feed-frame.js";
15
+ import { lineAt, startVoice } from "./voice.js";
16
16
  import { castVote } from "./vote.js";
17
- const PLAYER = "/usr/bin/afplay";
18
17
  const TICK_MS = 33;
19
18
  const RETRY_COMMAND = "npx driggsby@latest replay att";
20
19
  export async function runReplay(options, environment = defaultCredentialEnvironment()) {
21
20
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
22
21
  throw new CliError(`The replay plays in an interactive terminal. Open one and run:\n ${RETRY_COMMAND}`, 1);
23
22
  }
24
- const sound = !options.quiet && process.platform === "darwin" && existsSync(PLAYER);
25
- const ending = await play(sound ? VOICED : SILENT, sound);
23
+ const voice = options.quiet ? null : await startVoice();
24
+ const ending = await play(voice === null ? SILENT : VOICED, voice, environment);
26
25
  if (ending === "quit")
27
26
  return 0;
28
- return await vote(environment);
27
+ // Signing in needs the terminal: the replay has ended, so sign in, vote, done.
28
+ process.stdout.write("To count your vote, sign in to Driggsby first. It takes about a minute.\n\n");
29
+ await runLogin(environment);
30
+ const vote = await voteSignedIn(environment);
31
+ if (vote === "failed")
32
+ return 1;
33
+ process.stdout.write(vote === "counted" ? "\nThanks. Your vote to roll this out is in.\n" : "\nVoting on this has closed. Thanks for watching.\n");
34
+ return 0;
29
35
  }
30
- function play(timeline, sound) {
36
+ async function voteSignedIn(environment) {
37
+ const session = await requireDeploySession(environment);
38
+ try {
39
+ return (await castVote(session)) === "voted" ? "counted" : "closed";
40
+ }
41
+ catch (error) {
42
+ // The reason stays in the terminal's own words (a sign-in to redo, a
43
+ // network to allow); the replay itself only shows that it didn't count.
44
+ process.stdout.write(`${deployFailure(error, session.baseUrl, RETRY_COMMAND).message}\n`);
45
+ return "failed";
46
+ }
47
+ }
48
+ function play(timeline, voice, environment) {
31
49
  const stdin = process.stdin;
32
50
  const stdout = process.stdout;
33
- const playing = new Set();
34
- const hush = () => {
35
- for (const child of playing)
36
- child.kill();
37
- playing.clear();
38
- };
39
- const playCue = (file, fast) => {
40
- const path = fileURLToPath(new URL(`../../assets/replay/${file}`, import.meta.url));
41
- const child = spawn(PLAYER, fast ? ["-r", "2", path] : [path], { stdio: "ignore" });
42
- child.on("error", () => playing.delete(child));
43
- child.on("exit", () => playing.delete(child));
44
- playing.add(child);
45
- };
46
51
  let pt = 0;
47
52
  let paused = false;
48
53
  let fast = false;
54
+ let vote = "idle";
49
55
  let last = Date.now();
50
56
  // A full-screen app: the whole terminal, the header at the top and the
51
57
  // progress bar and keys anchored to the bottom.
52
58
  const draw = () => {
53
59
  const cols = Math.max(MIN_COLS, stdout.columns - 1);
54
60
  const rows = Math.max(MIN_ROWS, stdout.rows);
55
- const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline, join: "closed", paused, fast });
61
+ const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline, vote, paused, fast });
56
62
  stdout.write(`\u001b[H${frame.join("\r\n")}`);
57
63
  };
64
+ const rate = () => (fast ? 2 : 1);
65
+ // A jump picks up whatever line is being said at the new moment, mid-word.
58
66
  const seek = (to) => {
59
- hush();
60
67
  pt = Math.max(0, Math.min(timeline.total, to));
68
+ if (voice === null)
69
+ return;
70
+ const line = lineAt(CUES, pt);
71
+ if (line === null)
72
+ voice.stop();
73
+ else
74
+ voice.play(line.file, line.offset, rate(), paused);
61
75
  };
62
76
  return new Promise((resolve) => {
63
77
  stdout.write("\u001b[?1049h\u001b[?25l\u001b[2J");
64
78
  stdin.setRawMode(true);
65
79
  stdin.resume();
80
+ let done = false;
66
81
  const finish = (ending) => {
82
+ if (done)
83
+ return;
84
+ done = true;
67
85
  clearInterval(timer);
68
- hush();
86
+ voice?.close();
69
87
  stdin.off("data", onKey);
70
88
  stdin.setRawMode(false);
71
89
  stdin.pause();
@@ -73,6 +91,23 @@ function play(timeline, sound) {
73
91
  stdout.write("\u001b[?25h\u001b[?1049l");
74
92
  resolve(ending);
75
93
  };
94
+ // j, signed in: the vote counts in place and the bar shows how it went.
95
+ const castInPlace = async () => {
96
+ if ((await readStoredToken(environment)) === null) {
97
+ finish("sign-in");
98
+ return;
99
+ }
100
+ vote = "counting";
101
+ try {
102
+ const session = await requireDeploySession(environment);
103
+ vote = (await castVote(session)) === "voted" ? "counted" : "closed";
104
+ }
105
+ catch {
106
+ vote = "failed";
107
+ }
108
+ if (!done)
109
+ draw();
110
+ };
76
111
  const onResize = () => {
77
112
  stdout.write("\u001b[2J");
78
113
  draw();
@@ -83,22 +118,25 @@ function play(timeline, sound) {
83
118
  finish("quit");
84
119
  return;
85
120
  }
86
- // j votes right away: no prompt to confirm.
87
121
  if (key === "j") {
88
- finish("vote");
89
- return;
122
+ // Only once the call has played to the end.
123
+ if (pt >= timeline.endStart && (vote === "idle" || vote === "failed"))
124
+ void castInPlace();
90
125
  }
91
- if (key === " ") {
92
- hush();
126
+ else if (key === " ") {
93
127
  paused = !paused;
128
+ if (paused)
129
+ voice?.pause();
130
+ else
131
+ voice?.resume();
94
132
  }
95
133
  else if (key === "f") {
96
- hush();
97
134
  fast = !fast;
135
+ voice?.rate(rate());
98
136
  }
99
137
  else if (key === "r") {
100
- seek(0);
101
138
  paused = false;
139
+ seek(0);
102
140
  }
103
141
  else if (key === "\u001b[C")
104
142
  seek(pt + 2);
@@ -111,11 +149,11 @@ function play(timeline, sound) {
111
149
  const dt = (now - last) / 1000;
112
150
  last = now;
113
151
  if (!paused && pt < timeline.total) {
114
- const next = Math.min(timeline.total, pt + dt * (fast ? 2 : 1));
115
- if (sound)
152
+ const next = Math.min(timeline.total, pt + dt * rate());
153
+ if (voice !== null)
116
154
  for (const cue of CUES)
117
155
  if (cue.at > pt && cue.at <= next)
118
- playCue(cue.file, fast);
156
+ voice.play(cue.file, 0, rate(), false);
119
157
  pt = next;
120
158
  }
121
159
  draw();
@@ -125,22 +163,3 @@ function play(timeline, sound) {
125
163
  draw();
126
164
  });
127
165
  }
128
- async function vote(environment) {
129
- const out = (text) => {
130
- process.stdout.write(text);
131
- };
132
- if ((await readStoredToken(environment)) === null) {
133
- out("To count your vote, sign in to Driggsby first. It takes about a minute.\n\n");
134
- await runLogin(environment);
135
- out("\n");
136
- }
137
- const session = await requireDeploySession(environment);
138
- try {
139
- const outcome = await castVote(session);
140
- out(outcome === "voted" ? "Thanks. Your vote to roll this out is in.\n" : "Voting on this has closed. Thanks for watching.\n");
141
- return 0;
142
- }
143
- catch (error) {
144
- throw deployFailure(error, session.baseUrl, RETRY_COMMAND);
145
- }
146
- }
@@ -0,0 +1,130 @@
1
+ // The replay's voice on a Mac: one player, macOS's own, driven over a pipe
2
+ // through the built-in osascript. It pauses, resumes, changes speed and
3
+ // stops in place and at once, and only one line ever plays. When the replay
4
+ // ends, or its process dies, the pipe closes and the player stops and exits.
5
+ import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ const OSASCRIPT = "/usr/bin/osascript";
9
+ const READY_MS = 3_000;
10
+ // Commands arrive one per line, tab-separated: play <path> <offset> <rate>
11
+ // <paused 0|1>, pause, resume, rate <rate>, stop. A play past the end of its
12
+ // file stays silent, and a resume only continues a line a pause (or a paused
13
+ // jump) held, never one that had already finished. End of input stops the
14
+ // player and exits.
15
+ const PLAYER_SCRIPT = `
16
+ ObjC.import("AVFoundation");
17
+ ObjC.import("Foundation");
18
+ function run() {
19
+ const input = $.NSFileHandle.fileHandleWithStandardInput;
20
+ const output = $.NSFileHandle.fileHandleWithStandardOutput;
21
+ output.writeData($("ready\\n").dataUsingEncoding($.NSUTF8StringEncoding));
22
+ let player = null;
23
+ let held = false;
24
+ let buffer = "";
25
+ for (;;) {
26
+ const data = input.availableData;
27
+ if (Number(data.length) === 0) break;
28
+ buffer += $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js;
29
+ let end;
30
+ while ((end = buffer.indexOf("\\n")) >= 0) {
31
+ const parts = buffer.slice(0, end).split("\\t");
32
+ buffer = buffer.slice(end + 1);
33
+ const command = parts[0];
34
+ if (command === "play") {
35
+ if (player) player.stop;
36
+ player = null;
37
+ held = false;
38
+ const next = $.AVAudioPlayer.alloc.initWithContentsOfURLError($.NSURL.fileURLWithPath(parts[1]), null);
39
+ const offset = Number(parts[2]);
40
+ if (!next.isNil() && offset >= 0 && offset < next.duration) {
41
+ next.enableRate = true;
42
+ next.prepareToPlay;
43
+ next.rate = Number(parts[3]);
44
+ next.currentTime = offset;
45
+ if (parts[4] === "1") held = true;
46
+ else next.play;
47
+ player = next;
48
+ }
49
+ } else if (command === "pause") {
50
+ held = Boolean(player && player.playing);
51
+ if (held) player.pause;
52
+ } else if (command === "resume") {
53
+ if (held) player.play;
54
+ held = false;
55
+ } else if (command === "rate") {
56
+ if (player) player.rate = Number(parts[1]);
57
+ } else if (command === "stop") {
58
+ if (player) player.stop;
59
+ player = null;
60
+ held = false;
61
+ }
62
+ }
63
+ }
64
+ if (player) player.stop;
65
+ }
66
+ `;
67
+ // The line under a moment of the replay: the last sound started at or before
68
+ // it, and how far into it the moment is (past its end, the player stays silent).
69
+ export function lineAt(cues, pt) {
70
+ let found = null;
71
+ for (const cue of cues) {
72
+ if (cue.at > pt)
73
+ break;
74
+ found = cue;
75
+ }
76
+ return found === null ? null : { file: found.file, offset: pt - found.at };
77
+ }
78
+ // One command line for the player; null for a path it couldn't carry.
79
+ export function playerCommand(parts) {
80
+ const fields = parts.map(String);
81
+ return fields.some((field) => /[\t\n\r]/.test(field)) ? null : `${fields.join("\t")}\n`;
82
+ }
83
+ // The player, ready to speak, or null where there is none (not a Mac, or it
84
+ // didn't start): the replay then plays silently.
85
+ export function startVoice() {
86
+ if (process.platform !== "darwin" || !existsSync(OSASCRIPT))
87
+ return Promise.resolve(null);
88
+ return new Promise((resolve) => {
89
+ const child = spawn(OSASCRIPT, ["-l", "JavaScript", "-e", PLAYER_SCRIPT], { stdio: ["pipe", "pipe", "ignore"] });
90
+ let settled = false;
91
+ const settle = (voice) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ clearTimeout(timer);
96
+ if (voice === null)
97
+ child.kill();
98
+ resolve(voice);
99
+ };
100
+ const timer = setTimeout(() => { settle(null); }, READY_MS);
101
+ child.on("error", () => { settle(null); });
102
+ child.on("exit", () => { settle(null); });
103
+ // The player may be gone before a write lands; a lost line is silence.
104
+ child.stdin.on("error", () => undefined);
105
+ child.stdout.on("data", (chunk) => {
106
+ if (chunk.toString("utf8").includes("ready"))
107
+ settle(voiceOver(child));
108
+ });
109
+ });
110
+ }
111
+ function voiceOver(child) {
112
+ const send = (parts) => {
113
+ const line = playerCommand(parts);
114
+ if (line !== null && child.stdin.writable)
115
+ child.stdin.write(line);
116
+ };
117
+ return {
118
+ play: (file, offset, rate, paused) => {
119
+ send(["play", fileURLToPath(new URL(`../../assets/replay/${file}`, import.meta.url)), offset.toFixed(3), rate, paused ? 1 : 0]);
120
+ },
121
+ pause: () => { send(["pause"]); },
122
+ resume: () => { send(["resume"]); },
123
+ rate: (rate) => { send(["rate", rate]); },
124
+ stop: () => { send(["stop"]); },
125
+ close: () => {
126
+ send(["stop"]);
127
+ child.stdin.end();
128
+ },
129
+ };
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Set up and build on Driggsby, the MCP server for personal finance.",
5
5
  "license": "Apache-2.0 AND OFL-1.1",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "node": ">=18"
32
32
  },
33
33
  "dependencies": {
34
- "@driggsby/deploy": "0.10.1",
35
- "@driggsby/sdk": "0.10.1"
34
+ "@driggsby/deploy": "0.10.3",
35
+ "@driggsby/sdk": "0.10.3"
36
36
  }
37
37
  }