driggsby 0.10.1 → 0.10.2

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,8 +2,9 @@
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.
5
+ // starts over, the arrows move two seconds, and q quits. Once the call has
6
+ // played to the end, j votes to roll the feature out: signed in, the vote counts
7
+ // in place; signed out, it signs in first, votes, and the command ends.
7
8
  import { spawn } from "node:child_process";
8
9
  import { existsSync } from "node:fs";
9
10
  import { fileURLToPath } from "node:url";
@@ -22,20 +23,46 @@ export async function runReplay(options, environment = defaultCredentialEnvironm
22
23
  throw new CliError(`The replay plays in an interactive terminal. Open one and run:\n ${RETRY_COMMAND}`, 1);
23
24
  }
24
25
  const sound = !options.quiet && process.platform === "darwin" && existsSync(PLAYER);
25
- const ending = await play(sound ? VOICED : SILENT, sound);
26
+ const ending = await play(sound ? VOICED : SILENT, sound, environment);
26
27
  if (ending === "quit")
27
28
  return 0;
28
- return await vote(environment);
29
+ // Signing in needs the terminal: the replay has ended, so sign in, vote, done.
30
+ process.stdout.write("To count your vote, sign in to Driggsby first. It takes about a minute.\n\n");
31
+ await runLogin(environment);
32
+ const vote = await voteSignedIn(environment);
33
+ if (vote === "failed")
34
+ return 1;
35
+ process.stdout.write(vote === "counted" ? "\nThanks. Your vote to roll this out is in.\n" : "\nVoting on this has closed. Thanks for watching.\n");
36
+ return 0;
29
37
  }
30
- function play(timeline, sound) {
38
+ async function voteSignedIn(environment) {
39
+ const session = await requireDeploySession(environment);
40
+ try {
41
+ return (await castVote(session)) === "voted" ? "counted" : "closed";
42
+ }
43
+ catch (error) {
44
+ // The reason stays in the terminal's own words (a sign-in to redo, a
45
+ // network to allow); the replay itself only shows that it didn't count.
46
+ process.stdout.write(`${deployFailure(error, session.baseUrl, RETRY_COMMAND).message}\n`);
47
+ return "failed";
48
+ }
49
+ }
50
+ function play(timeline, sound, environment) {
31
51
  const stdin = process.stdin;
32
52
  const stdout = process.stdout;
33
53
  const playing = new Set();
54
+ // SIGKILL, so a line frozen by a pause (SIGSTOP) stops too.
34
55
  const hush = () => {
35
56
  for (const child of playing)
36
- child.kill();
57
+ child.kill("SIGKILL");
37
58
  playing.clear();
38
59
  };
60
+ // A pause freezes whatever is being said and a resume picks it up where it
61
+ // stopped, instead of cutting it off until the next line.
62
+ const freeze = (frozen) => {
63
+ for (const child of playing)
64
+ child.kill(frozen ? "SIGSTOP" : "SIGCONT");
65
+ };
39
66
  const playCue = (file, fast) => {
40
67
  const path = fileURLToPath(new URL(`../../assets/replay/${file}`, import.meta.url));
41
68
  const child = spawn(PLAYER, fast ? ["-r", "2", path] : [path], { stdio: "ignore" });
@@ -46,13 +73,14 @@ function play(timeline, sound) {
46
73
  let pt = 0;
47
74
  let paused = false;
48
75
  let fast = false;
76
+ let vote = "idle";
49
77
  let last = Date.now();
50
78
  // A full-screen app: the whole terminal, the header at the top and the
51
79
  // progress bar and keys anchored to the bottom.
52
80
  const draw = () => {
53
81
  const cols = Math.max(MIN_COLS, stdout.columns - 1);
54
82
  const rows = Math.max(MIN_ROWS, stdout.rows);
55
- const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline, join: "closed", paused, fast });
83
+ const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline, vote, paused, fast });
56
84
  stdout.write(`\u001b[H${frame.join("\r\n")}`);
57
85
  };
58
86
  const seek = (to) => {
@@ -63,7 +91,11 @@ function play(timeline, sound) {
63
91
  stdout.write("\u001b[?1049h\u001b[?25l\u001b[2J");
64
92
  stdin.setRawMode(true);
65
93
  stdin.resume();
94
+ let done = false;
66
95
  const finish = (ending) => {
96
+ if (done)
97
+ return;
98
+ done = true;
67
99
  clearInterval(timer);
68
100
  hush();
69
101
  stdin.off("data", onKey);
@@ -73,6 +105,23 @@ function play(timeline, sound) {
73
105
  stdout.write("\u001b[?25h\u001b[?1049l");
74
106
  resolve(ending);
75
107
  };
108
+ // j, signed in: the vote counts in place and the bar shows how it went.
109
+ const castInPlace = async () => {
110
+ if ((await readStoredToken(environment)) === null) {
111
+ finish("sign-in");
112
+ return;
113
+ }
114
+ vote = "counting";
115
+ try {
116
+ const session = await requireDeploySession(environment);
117
+ vote = (await castVote(session)) === "voted" ? "counted" : "closed";
118
+ }
119
+ catch {
120
+ vote = "failed";
121
+ }
122
+ if (!done)
123
+ draw();
124
+ };
76
125
  const onResize = () => {
77
126
  stdout.write("\u001b[2J");
78
127
  draw();
@@ -83,17 +132,17 @@ function play(timeline, sound) {
83
132
  finish("quit");
84
133
  return;
85
134
  }
86
- // j votes right away: no prompt to confirm.
87
135
  if (key === "j") {
88
- finish("vote");
89
- return;
136
+ // Only once the call has played to the end.
137
+ if (pt >= timeline.endStart && (vote === "idle" || vote === "failed"))
138
+ void castInPlace();
90
139
  }
91
- if (key === " ") {
92
- hush();
140
+ else if (key === " ") {
93
141
  paused = !paused;
142
+ freeze(paused);
94
143
  }
95
144
  else if (key === "f") {
96
- hush();
145
+ // The line being said finishes at its own speed; the next plays faster.
97
146
  fast = !fast;
98
147
  }
99
148
  else if (key === "r") {
@@ -125,22 +174,3 @@ function play(timeline, sound) {
125
174
  draw();
126
175
  });
127
176
  }
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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.2",
35
+ "@driggsby/sdk": "0.10.2"
36
36
  }
37
37
  }