@alexkroman1/aai-cli 6.3.0 → 6.4.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/dist/scaffold/package.json +3 -3
- package/dist/templates/transcription-workflow/agent.test.ts +251 -10
- package/dist/templates/transcription-workflow/agent.ts +76 -1
- package/dist/templates/transcription-workflow/api-help.tsx +206 -0
- package/dist/templates/transcription-workflow/client.tsx +153 -7
- package/dist/templates/transcription-workflow/workflows/batch.ts +279 -0
- package/dist/templates/transcription-workflow/workflows/stream.ts +341 -0
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +139 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +159 -121
- package/package.json +3 -3
|
@@ -53,40 +53,81 @@
|
|
|
53
53
|
* argument.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import { throwFatalStepError
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
multipartBody,
|
|
60
|
-
readUpload,
|
|
61
|
-
report,
|
|
62
|
-
requireStepEnv,
|
|
63
|
-
stepFetch,
|
|
64
|
-
uploadInfo,
|
|
65
|
-
} from "@alexkroman1/aai/utils";
|
|
56
|
+
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
57
|
+
import { mapInBatches, readUpload, report, uploadInfo } from "@alexkroman1/aai/utils";
|
|
58
|
+
import { elapsed, timed, transcribeWav } from "./sync-api.ts";
|
|
66
59
|
import {
|
|
60
|
+
bytesPerSecond,
|
|
67
61
|
parseWav,
|
|
68
62
|
planSegments,
|
|
63
|
+
SEGMENT_OVERLAP_SECONDS,
|
|
64
|
+
SEGMENT_SECONDS,
|
|
69
65
|
type Segment,
|
|
70
66
|
UnsupportedRecordingError,
|
|
71
67
|
type WavFormat,
|
|
72
68
|
wavWithHeader,
|
|
73
69
|
} from "./wav.ts";
|
|
74
70
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Bytes the desk keeps uploading at once, which is what {@link segmentConcurrency}
|
|
73
|
+
* divides to get a width.
|
|
74
|
+
*
|
|
75
|
+
* A `503` from this endpoint says `queue wait timed out; server at capacity`, and
|
|
76
|
+
* that sentence is the whole model: requests are QUEUED rather than refused, and one
|
|
77
|
+
* fails only when it waited out the queue's own deadline. So what limits the fan-out
|
|
78
|
+
* is total work in flight, and at this segment length that is dominated by BYTES —
|
|
79
|
+
* not by the request count and not by the audio duration. Five arms, one account, one
|
|
80
|
+
* laptop, `curl` straight at the endpoint:
|
|
81
|
+
*
|
|
82
|
+
* | requests | per request | bytes in flight | audio-s | `503`s |
|
|
83
|
+
* | --- | --- | --- | --- | --- |
|
|
84
|
+
* | 320 | 160 KB (5s) | 51 MB | 1,600 | 0 |
|
|
85
|
+
* | 64 | 2.94 MB (92s, 16 kHz mono) | 188 MB | 5,888 | 0 |
|
|
86
|
+
* | 48 | 17.66 MB (92s, 48 kHz stereo) | 848 MB | 4,416 | 0 |
|
|
87
|
+
* | 56 | 17.66 MB | 989 MB | 5,152 | 6 |
|
|
88
|
+
* | 64 | 17.66 MB | 1.13 GB | 5,888 | 20 |
|
|
89
|
+
* | 320 | 2.94 MB | 941 MB | 29,440 | 64 |
|
|
90
|
+
*
|
|
91
|
+
* Read the columns against each other, because each one rules something out. Request
|
|
92
|
+
* COUNT cannot be the cap: 320 tiny requests were admitted whole, and so were 64 at
|
|
93
|
+
* 2.94 MB, where a flat ceiling of ~50 would have refused the excess. Audio DURATION
|
|
94
|
+
* cannot be it either: 5,888 audio-seconds passed cleanly at 2.94 MB a request and
|
|
95
|
+
* drew 20 `503`s at 17.66 MB — same audio, six times the bytes. What tracks is the
|
|
96
|
+
* byte column, and it tracks in ADMITTED bytes too, tightly, across request counts
|
|
97
|
+
* that differ by 5x: 848 MB clean, then 883 MB / 777 MB / 753 MB admitted on the
|
|
98
|
+
* three arms that limited. The last row is the proof, since it reaches the same
|
|
99
|
+
* ceiling with 320 small requests as 64 big ones do.
|
|
100
|
+
*
|
|
101
|
+
* 640 MB sits between the largest clean run (848 MB) and the smallest limited one
|
|
102
|
+
* (941 MB), nearer the clean side. It is the declared quantity because the WIDTH is
|
|
103
|
+
* not the durable fact — this desk cuts whatever format it is handed, and the same
|
|
104
|
+
* 32 segments are 565 MB of 48 kHz stereo, 94 MB of 16 kHz mono, or 1.28 GB of a
|
|
105
|
+
* format at the {@link MAX_SEGMENT_BYTES} ceiling. Only one of those three is safe,
|
|
106
|
+
* and a constant cannot tell them apart.
|
|
107
|
+
*
|
|
108
|
+
* The threshold is this machine's, and one caveat sharpens which half. Bytes in
|
|
109
|
+
* flight is bytes UPLOADING, so it is also the number that saturated a ~65 MB/s
|
|
110
|
+
* uplink — a deployed guest reserving one CPU has neither, and a slower uplink holds
|
|
111
|
+
* every request open LONGER, which is the direction that makes a queue deadline
|
|
112
|
+
* easier to hit rather than harder. Re-measure there.
|
|
113
|
+
*/
|
|
114
|
+
export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
|
|
83
115
|
|
|
84
116
|
/**
|
|
85
|
-
*
|
|
117
|
+
* The widest fan-out, however small the segments are.
|
|
118
|
+
*
|
|
119
|
+
* Because {@link BYTES_IN_FLIGHT} stops being the binding constraint once segments
|
|
120
|
+
* are small — 16 kHz mono would divide out to 173 — and something else takes over
|
|
121
|
+
* before that helps. `mapInBatches` is a barrier rather than a work-stealing pool,
|
|
122
|
+
* deliberately, because the DevKit correlates a journal entry to a step call by the
|
|
123
|
+
* order the call was issued in: a batch's wall time is therefore its SLOWEST request
|
|
124
|
+
* and a run's is the sum of those, so depth is paid at p100 and the tail widens with
|
|
125
|
+
* it (p95/p50 measured 1.1x at 20 concurrent against 1.5x at 320, max/p50 reaching
|
|
126
|
+
* 6.7x — 5.2s against 35.0s). A `503` carrying `retry-after: 1` is exactly such a
|
|
127
|
+
* straggler, which is why overshooting is cheap in BILLING and not in latency.
|
|
86
128
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* one concurrency per run, from one laptop and one account:
|
|
129
|
+
* 32 is the measured knee over 65 segments (1h37m of 48 kHz stereo), one concurrency
|
|
130
|
+
* per run, through this workflow:
|
|
90
131
|
*
|
|
91
132
|
* | in flight | wall | vs realtime | `503`s |
|
|
92
133
|
* | --- | --- | --- | --- |
|
|
@@ -95,23 +136,38 @@ const API_KEY_ENV = "ASSEMBLYAI_API_KEY";
|
|
|
95
136
|
* | 48 | 26.1-28.5s | 204-223x | 0-4 |
|
|
96
137
|
* | 64 | 31.9s | 182x | 20 |
|
|
97
138
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
* This was 8 for a long time, which cost 37% of the wall clock for headroom the
|
|
140
|
+
* endpoint does not need. Past 32 there is nothing left to buy: 48 is within noise
|
|
141
|
+
* of it while starting to pay retries, and 64 is outright SLOWER. Note the width is
|
|
142
|
+
* also inert below a threshold — at 90-second segments, 32 only binds past 48
|
|
143
|
+
* minutes of audio — so on a typical recording the whole fan-out is in flight
|
|
144
|
+
* either way and this number changes nothing.
|
|
145
|
+
*/
|
|
146
|
+
export const MAX_SEGMENT_CONCURRENCY = 32;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* How many segments of THIS recording to keep in flight.
|
|
150
|
+
*
|
|
151
|
+
* Derived rather than declared, because the byte cost of a segment is a property of
|
|
152
|
+
* the format and not of this code: see {@link BYTES_IN_FLIGHT} for the measurements,
|
|
153
|
+
* and note that a fixed 32 is safe for 48 kHz stereo and a guaranteed queue timeout
|
|
154
|
+
* for a format twice as heavy. Both flows call this, so both scale the same way.
|
|
155
|
+
*
|
|
156
|
+
* Safe to call from a workflow BODY: `format` arrives from a journaled step result,
|
|
157
|
+
* so a replay derives the same width from the same bytes — which is what keeps
|
|
158
|
+
* `mapInBatches` issuing its calls in the order the journal recorded them.
|
|
106
159
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* completed). That is only true over HTTP/1.1, which is what `stepFetch` pins.
|
|
160
|
+
* Overshooting stays recoverable whatever this returns: a `503` carries
|
|
161
|
+
* `retry-after` and `toStepError` below honours it, so the run completes having paid
|
|
162
|
+
* one extra request per limited segment (measured: 20 `503`s at 64, each retried
|
|
163
|
+
* exactly once, run completed). That is only true over HTTP/1.1, which is what
|
|
164
|
+
* `stepFetch` pins.
|
|
113
165
|
*/
|
|
114
|
-
|
|
166
|
+
export function segmentConcurrency(format: WavFormat): number {
|
|
167
|
+
const perSegment = bytesPerSecond(format) * (SEGMENT_SECONDS + SEGMENT_OVERLAP_SECONDS);
|
|
168
|
+
if (perSegment <= 0) return MAX_SEGMENT_CONCURRENCY;
|
|
169
|
+
return Math.max(1, Math.min(MAX_SEGMENT_CONCURRENCY, Math.floor(BYTES_IN_FLIGHT / perSegment)));
|
|
170
|
+
}
|
|
115
171
|
|
|
116
172
|
/**
|
|
117
173
|
* Bytes probed for the WAV header.
|
|
@@ -122,12 +178,29 @@ const SEGMENT_CONCURRENCY = 8;
|
|
|
122
178
|
*/
|
|
123
179
|
const HEADER_PROBE_BYTES = 64 * 1024;
|
|
124
180
|
|
|
125
|
-
/** The endpoint's own per-request deadline, plus room to upload. */
|
|
126
|
-
const SYNC_TIMEOUT_MS = 60_000;
|
|
127
|
-
|
|
128
181
|
/** Most words `stitchTranscript` will look back over to find a repeated seam. */
|
|
129
182
|
const MAX_SEAM_WORDS = 40;
|
|
130
183
|
|
|
184
|
+
/**
|
|
185
|
+
* What a finished run reports, whichever flow produced it.
|
|
186
|
+
*
|
|
187
|
+
* Declared once and shared by all three, because the page renders any of them with
|
|
188
|
+
* one component: a field added to one flow and not the others is a panel that shows
|
|
189
|
+
* it for some runs and not others, with nothing saying why.
|
|
190
|
+
*/
|
|
191
|
+
export type Transcript = {
|
|
192
|
+
/** The recording's own filename, so a reader knows which run they are looking at. */
|
|
193
|
+
source: string;
|
|
194
|
+
/** How many requests the transcript was assembled from. `1` for the async flow. */
|
|
195
|
+
segments: number;
|
|
196
|
+
/** Length of the AUDIO. */
|
|
197
|
+
durationMs: number;
|
|
198
|
+
/** How long the RUN took, wall clock. The number that compares the flows. */
|
|
199
|
+
elapsedMs: number;
|
|
200
|
+
words: number;
|
|
201
|
+
transcript: string;
|
|
202
|
+
};
|
|
203
|
+
|
|
131
204
|
/** What one segment's request came back with. */
|
|
132
205
|
export type SegmentTranscript = {
|
|
133
206
|
index: number;
|
|
@@ -143,6 +216,7 @@ export type SegmentTranscript = {
|
|
|
143
216
|
export async function transcribeFlow(input: { recording: string }) {
|
|
144
217
|
"use workflow";
|
|
145
218
|
|
|
219
|
+
const startedAt = await startClock();
|
|
146
220
|
const plan = await splitRecording(input.recording);
|
|
147
221
|
|
|
148
222
|
// One step per segment, bounded, in an order a replay reproduces exactly.
|
|
@@ -150,13 +224,13 @@ export async function transcribeFlow(input: { recording: string }) {
|
|
|
150
224
|
// already journaled, so the resume replays those for free and re-issues only
|
|
151
225
|
// what is missing, where catching here to salvage a partial transcript would
|
|
152
226
|
// return a recording with a silent hole in it and report success.
|
|
153
|
-
const parts = await mapInBatches(plan.segments,
|
|
227
|
+
const parts = await mapInBatches(plan.segments, segmentConcurrency(plan.format), (segment) =>
|
|
154
228
|
transcribeSegment(input.recording, plan.format, segment),
|
|
155
229
|
);
|
|
156
230
|
|
|
157
231
|
// Whatever this returns is what a caller reads as `output` on a completed run
|
|
158
232
|
// — so it is what the page renders, typed through `WorkflowOutputOf`.
|
|
159
|
-
return await mergeTranscript(input.recording, plan.durationMs, parts);
|
|
233
|
+
return await mergeTranscript(input.recording, plan.durationMs, parts, startedAt);
|
|
160
234
|
}
|
|
161
235
|
|
|
162
236
|
/**
|
|
@@ -210,7 +284,6 @@ export async function transcribeSegment(
|
|
|
210
284
|
// order.
|
|
211
285
|
await report(`Transcribing ${clock(segment.startMs)}–${clock(segment.endMs)}.`);
|
|
212
286
|
|
|
213
|
-
const apiKey = apiKeyOrFatal();
|
|
214
287
|
// `[start, end)`, the same half-open pair `planSegments` produced — the store
|
|
215
288
|
// owns the conversion to HTTP's inclusive range, so there is no `- 1` here to
|
|
216
289
|
// get wrong.
|
|
@@ -221,38 +294,22 @@ export async function transcribeSegment(
|
|
|
221
294
|
// the language, so the field was a question asked of a person that the service
|
|
222
295
|
// answers better — and getting it wrong is a whole transcript in the wrong
|
|
223
296
|
// language. Add one back only for a desk that really knows.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
headers: {
|
|
241
|
-
// The raw key — this endpoint takes it unprefixed, and a `Bearer ` in
|
|
242
|
-
// front of it is a 401 that reads like a wrong key.
|
|
243
|
-
Authorization: apiKey,
|
|
244
|
-
"X-AAI-Model": SYNC_MODEL,
|
|
245
|
-
...part.headers,
|
|
246
|
-
},
|
|
247
|
-
body: part.body,
|
|
248
|
-
// Nothing here has a deadline of its own, and a hung upload inside a step is
|
|
249
|
-
// a run that never finishes rather than one that retries.
|
|
250
|
-
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
|
251
|
-
});
|
|
252
|
-
if (!response.ok) throw await syncFailure(response, segment);
|
|
253
|
-
|
|
254
|
-
const body = (await response.json()) as { text?: string };
|
|
255
|
-
return { index: segment.index, text: (body.text ?? "").trim() };
|
|
297
|
+
//
|
|
298
|
+
// `wavWithHeader` is what makes a WINDOW decodable: the endpoint decodes each
|
|
299
|
+
// request independently, so a slice of the middle of a recording is a headerless
|
|
300
|
+
// tail until one is put back on it. The streaming flow needs no equivalent — its
|
|
301
|
+
// parts were cut with a header each.
|
|
302
|
+
const { value: text, ms } = await timed(() =>
|
|
303
|
+
transcribeWav(
|
|
304
|
+
wavWithHeader(format, audio.bytes),
|
|
305
|
+
`segment-${segment.index}.wav`,
|
|
306
|
+
`Segment ${segment.index} (${clock(segment.startMs)})`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
// The LATENCY, which is what says whether the concurrency bound or the endpoint
|
|
310
|
+
// is the thing limiting the run — see `timed`'s doc.
|
|
311
|
+
await report(`Transcribed ${clock(segment.startMs)}–${clock(segment.endMs)} in ${elapsed(ms)}.`);
|
|
312
|
+
return { index: segment.index, text };
|
|
256
313
|
}
|
|
257
314
|
|
|
258
315
|
/**
|
|
@@ -274,13 +331,8 @@ export async function mergeTranscript(
|
|
|
274
331
|
uploadId: string,
|
|
275
332
|
durationMs: number,
|
|
276
333
|
parts: readonly SegmentTranscript[],
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
segments: number;
|
|
280
|
-
durationMs: number;
|
|
281
|
-
words: number;
|
|
282
|
-
transcript: string;
|
|
283
|
-
}> {
|
|
334
|
+
startedAt: number,
|
|
335
|
+
): Promise<Transcript> {
|
|
284
336
|
"use step";
|
|
285
337
|
|
|
286
338
|
await report(`Stitching ${parts.length} segment${parts.length === 1 ? "" : "s"} together.`);
|
|
@@ -298,6 +350,9 @@ export async function mergeTranscript(
|
|
|
298
350
|
source,
|
|
299
351
|
segments: parts.length,
|
|
300
352
|
durationMs,
|
|
353
|
+
// Wall clock, so the three flows can be compared over one file — see
|
|
354
|
+
// `startClock`. Measured in a STEP, which is what makes it survive a replay.
|
|
355
|
+
elapsedMs: Date.now() - startedAt,
|
|
301
356
|
words: countWords(transcript),
|
|
302
357
|
transcript,
|
|
303
358
|
};
|
|
@@ -305,6 +360,26 @@ export async function mergeTranscript(
|
|
|
305
360
|
|
|
306
361
|
// ---- Pure helpers -----------------------------------------------------------
|
|
307
362
|
|
|
363
|
+
/**
|
|
364
|
+
* When the run started, as epoch ms.
|
|
365
|
+
*
|
|
366
|
+
* A STEP, and that is the whole reason this exists rather than a `Date.now()` in the
|
|
367
|
+
* body: a body replays from the top on every resume, so a clock read there returns a
|
|
368
|
+
* different value each time and every duration derived from it would be a different
|
|
369
|
+
* duration. A step's result is journaled, so this is the moment the run really began
|
|
370
|
+
* however many times it is replayed.
|
|
371
|
+
*
|
|
372
|
+
* Shared by all three flows deliberately. A run snapshot carries `createdAt` and no
|
|
373
|
+
* end time, so "how long did this take" is not answerable from the outside — and the
|
|
374
|
+
* whole point of shipping three flows over one job is that a reader can compare them,
|
|
375
|
+
* which needs one number measured one way.
|
|
376
|
+
*/
|
|
377
|
+
export async function startClock(): Promise<number> {
|
|
378
|
+
"use step";
|
|
379
|
+
|
|
380
|
+
return Date.now();
|
|
381
|
+
}
|
|
382
|
+
|
|
308
383
|
/** A word, stripped of the punctuation the decoder added, for seam comparison. */
|
|
309
384
|
function seamKey(word: string): string {
|
|
310
385
|
return word.toLowerCase().replace(/[^\p{L}\p{N}']/gu, "");
|
|
@@ -355,8 +430,8 @@ function seamLength(merged: readonly string[], next: readonly string[]): number
|
|
|
355
430
|
return 0;
|
|
356
431
|
}
|
|
357
432
|
|
|
358
|
-
/** Words in a string. */
|
|
359
|
-
function countWords(text: string): number {
|
|
433
|
+
/** Words in a string. Exported so the streaming flow reports the same number. */
|
|
434
|
+
export function countWords(text: string): number {
|
|
360
435
|
return text.split(/\s+/).filter(Boolean).length;
|
|
361
436
|
}
|
|
362
437
|
|
|
@@ -368,19 +443,6 @@ export function clock(ms: number): string {
|
|
|
368
443
|
|
|
369
444
|
// ---- I/O helpers ------------------------------------------------------------
|
|
370
445
|
|
|
371
|
-
/** The API key, or a terminal failure — three more attempts find the same gap. */
|
|
372
|
-
function apiKeyOrFatal(): string {
|
|
373
|
-
try {
|
|
374
|
-
return requireStepEnv(API_KEY_ENV);
|
|
375
|
-
} catch (err: unknown) {
|
|
376
|
-
// `throwFatalStepError` rather than `throw new FatalError(…)`: that class
|
|
377
|
-
// takes only a message — no `cause` — so constructing one inside a `catch`
|
|
378
|
-
// loses the original where the linter (rightly) expects it preserved. Here
|
|
379
|
-
// the original is the ARGUMENT, and nothing is swallowed.
|
|
380
|
-
return throwFatalStepError(err);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
446
|
/** Run a `wav.ts` helper, turning its "cannot cut this" into a terminal failure. */
|
|
385
447
|
function fatalOnUnsupported<T>(read: () => T): T {
|
|
386
448
|
try {
|
|
@@ -390,27 +452,3 @@ function fatalOnUnsupported<T>(read: () => T): T {
|
|
|
390
452
|
throw err;
|
|
391
453
|
}
|
|
392
454
|
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* The sync endpoint's failure, with whatever it said about it.
|
|
396
|
-
*
|
|
397
|
-
* `toStepError` makes the three-way call: a `FatalError` stops the DevKit
|
|
398
|
-
* retrying something that will answer the same way, a bare `RetryableError`
|
|
399
|
-
* retries in ONE SECOND (that class's own default), and a `RetryableError`
|
|
400
|
-
* carrying `retryAfter` waits exactly as long as the far side asked. The last
|
|
401
|
-
* matters here because `SEGMENT_CONCURRENCY` segments hit the rate limit
|
|
402
|
-
* together — a second later all four ask again, where on the server's number
|
|
403
|
-
* they drain.
|
|
404
|
-
*/
|
|
405
|
-
async function syncFailure(response: Response, segment: Segment): Promise<Error> {
|
|
406
|
-
// Two shapes, documented: `{ error_code, message }` for a request problem and
|
|
407
|
-
// `{ detail }` for auth and rate limits.
|
|
408
|
-
const body = (await response.json().catch(() => ({}))) as { message?: string; detail?: string };
|
|
409
|
-
const detail = body.message ?? body.detail;
|
|
410
|
-
return toStepError(
|
|
411
|
-
response,
|
|
412
|
-
`Segment ${segment.index} (${clock(segment.startMs)}) failed: HTTP ${response.status}${
|
|
413
|
-
detail ? ` — ${detail}` : ""
|
|
414
|
-
}`,
|
|
415
|
-
);
|
|
416
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"p-timeout": "^7.0.1",
|
|
45
45
|
"vite": "^8.2.1",
|
|
46
46
|
"zod": "^4.4.3",
|
|
47
|
-
"@alexkroman1/aai
|
|
48
|
-
"@alexkroman1/aai": "6.
|
|
47
|
+
"@alexkroman1/aai": "6.4.0",
|
|
48
|
+
"@alexkroman1/aai-ui": "6.4.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"playwright": "^1.62.1",
|