@alexkroman1/aai-cli 6.4.0 → 6.5.1

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.
Files changed (51) hide show
  1. package/dist/{_agent-vGW5SBnX.mjs → _agent-DpH2pBJd.mjs} +1 -5
  2. package/dist/{_bundler-BapCUUJN.mjs → _bundler-C2BDwU7r.mjs} +5 -7
  3. package/dist/{_dev-server-DQ1t9sbw.mjs → _dev-server-LLLWnFBs.mjs} +3 -3
  4. package/dist/_e2e-test-utils.d.ts +17 -17
  5. package/dist/{_init-CpISqoR8.mjs → _init-C57bWAQ8.mjs} +2 -2
  6. package/dist/{_slug-api-D2heILuC.mjs → _slug-api-BxrsJXVI.mjs} +3 -3
  7. package/dist/_slug-api.d.ts +1 -1
  8. package/dist/{_api-client-LgLksMhN.mjs → _studio-U3jhKlA3.mjs} +105 -2
  9. package/dist/_studio.d.ts +11 -0
  10. package/dist/{_templates-iDGjGiPf.mjs → _templates-Dxb_P2Wz.mjs} +1 -1
  11. package/dist/bin.d.mts +2 -0
  12. package/dist/{build-BO1Ni6oJ.mjs → build-Dfza2pRI.mjs} +2 -2
  13. package/dist/cli.mjs +43 -42
  14. package/dist/{client-bundler-B8Q9F8gJ.mjs → client-bundler-BEIqgOtd.mjs} +2 -2
  15. package/dist/client-bundler.mjs +1 -1
  16. package/dist/{delete-4I0uNR4a.mjs → delete-vbpjGzqs.mjs} +3 -3
  17. package/dist/{deploy-DJ3HRcia.mjs → deploy-B8lubiRT.mjs} +3 -4
  18. package/dist/{dev-C6SWaAZP.mjs → dev-Uq5ujP8a.mjs} +1 -1
  19. package/dist/{eject-Zrzk5KV3.mjs → eject-31gjtaHF.mjs} +2 -2
  20. package/dist/{init-BTRGiPQr.mjs → init-l6xfU4xX.mjs} +3 -3
  21. package/dist/{login-k0Z3VQu9.mjs → login-C71-qz8F.mjs} +1 -1
  22. package/dist/scaffold/package.json +3 -3
  23. package/dist/{secret-DzSQx1Ds.mjs → secret-BuMuFR4B.mjs} +2 -2
  24. package/dist/{storage-C-Do0mIA.mjs → storage-DzRZ-eCw.mjs} +2 -2
  25. package/dist/{studio-BGVmCA2G.mjs → studio-BCNUpDgP.mjs} +4 -5
  26. package/dist/templates/retail/resolve.ts +16 -0
  27. package/dist/templates/retail/store.ts +20 -0
  28. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -5
  29. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -5
  30. package/dist/templates/retail/tools/get_order_details.ts +4 -6
  31. package/dist/templates/retail/tools/get_user_details.ts +2 -7
  32. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -5
  33. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -5
  34. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -5
  35. package/dist/templates/retail/tools/modify_user_address.ts +2 -7
  36. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -5
  37. package/dist/templates/transcription-workflow/agent.test.ts +75 -4
  38. package/dist/templates/transcription-workflow/api-help.tsx +8 -0
  39. package/dist/templates/transcription-workflow/client.tsx +149 -11
  40. package/dist/templates/transcription-workflow/workflows/batch.ts +32 -21
  41. package/dist/templates/transcription-workflow/workflows/stitch.ts +133 -0
  42. package/dist/templates/transcription-workflow/workflows/stream.ts +20 -20
  43. package/dist/templates/transcription-workflow/workflows/sync-api.ts +8 -2
  44. package/dist/templates/transcription-workflow/workflows/transcribe.ts +71 -86
  45. package/dist/{test-B6ZBxSpk.mjs → test-9kPKJI-w.mjs} +1 -3
  46. package/dist/typecheck.mjs +4 -5
  47. package/dist/{worker-bundler-CIdDhZ0c.mjs → worker-bundler-CGD4r8Kc.mjs} +25 -11
  48. package/dist/worker-bundler.mjs +1 -1
  49. package/dist/{workflow-B-O97AqI.mjs → workflow-CFpxOFfQ.mjs} +1 -1
  50. package/package.json +4 -4
  51. package/dist/_studio-zAJycZ8j.mjs +0 -94
@@ -44,17 +44,28 @@
44
44
  * app's own upload store and the run carries only its id; each step reads
45
45
  * exactly its own window with `readUpload`. Sixty steps therefore move the
46
46
  * recording once between them, not sixty times.
47
- * - **The fan-out is bounded by `mapInBatches`, and the bound is not a detail.**
47
+ * - **The fan-out is bounded by `mapConcurrent`, and the bound is not a detail.**
48
48
  * The DevKit correlates a journal entry to a step call by the ORDER the call
49
- * was issued in, so a work-stealing pool which issues its next call only
50
- * when a previous one settlesputs the calls in a different order on a
51
- * replay than it did on the first execution. That primitive is sequential
52
- * batches of `Promise.all` for exactly that reason; its module doc carries the
53
- * argument.
49
+ * was issued in, so the primitive keeps a WINDOW over a cursor that only ever
50
+ * hands out the next index — the Nth call issued is segment N-1 however the
51
+ * calls settle. Its module doc carries the argument; what matters here is that
52
+ * there is no barrier, so a slow segment costs only itself.
53
+ * - **The transcript STREAMS as it is produced.** Each segment is emitted the
54
+ * moment it lands (`emit(TRANSCRIPT_STREAM, …)`), so the page renders the
55
+ * answer growing rather than a status line and then everything at once. That is
56
+ * the difference a fan-out can make to a reader and a run output cannot: an
57
+ * `output` exists only when the last segment does.
54
58
  */
55
59
 
56
60
  import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
57
- import { mapInBatches, readUpload, report, uploadInfo } from "@alexkroman1/aai/utils";
61
+ import { emit, mapConcurrent, readUpload, report, uploadInfo } from "@alexkroman1/aai/utils";
62
+ import {
63
+ clock,
64
+ countWords,
65
+ stitchTranscript,
66
+ TRANSCRIPT_STREAM,
67
+ type TranscriptChunk,
68
+ } from "./stitch.ts";
58
69
  import { elapsed, timed, transcribeWav } from "./sync-api.ts";
59
70
  import {
60
71
  bytesPerSecond,
@@ -117,14 +128,16 @@ export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
117
128
  * The widest fan-out, however small the segments are.
118
129
  *
119
130
  * 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.7x5.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.
131
+ * are small — 16 kHz mono would divide out to 173 — and the endpoint's own tail
132
+ * takes over before that helps: p95/p50 measured 1.1x at 20 concurrent against
133
+ * 1.5x at 320, with max/p50 reaching 6.7x (5.2s against 35.0s). A `503` carrying
134
+ * `retry-after: 1` is exactly such a straggler.
135
+ *
136
+ * **That tail used to be paid once per ROUND, and is not any more.** `mapConcurrent`
137
+ * was `mapInBatches` sequential batches of `Promise.all` so a batch's wall time
138
+ * was its slowest member and a run's was the sum of those. It is a window over a
139
+ * cursor now: a straggler holds up nothing but itself, and the numbers below were
140
+ * measured under the old barrier, so they are if anything pessimistic.
128
141
  *
129
142
  * 32 is the measured knee over 65 segments (1h37m of 48 kHz stereo), one concurrency
130
143
  * per run, through this workflow:
@@ -142,6 +155,10 @@ export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
142
155
  * also inert below a threshold — at 90-second segments, 32 only binds past 48
143
156
  * minutes of audio — so on a typical recording the whole fan-out is in flight
144
157
  * either way and this number changes nothing.
158
+ *
159
+ * The table above was measured under the old per-round barrier. Re-measuring it is
160
+ * worth doing before this number moves again: the window makes a wide fan-out
161
+ * cheaper at the tail, which if anything argues for a HIGHER knee.
145
162
  */
146
163
  export const MAX_SEGMENT_CONCURRENCY = 32;
147
164
 
@@ -155,7 +172,7 @@ export const MAX_SEGMENT_CONCURRENCY = 32;
155
172
  *
156
173
  * Safe to call from a workflow BODY: `format` arrives from a journaled step result,
157
174
  * 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.
175
+ * `mapConcurrent` issuing its calls in the order the journal recorded them.
159
176
  *
160
177
  * Overshooting stays recoverable whatever this returns: a `503` carries
161
178
  * `retry-after` and `toStepError` below honours it, so the run completes having paid
@@ -178,9 +195,6 @@ export function segmentConcurrency(format: WavFormat): number {
178
195
  */
179
196
  const HEADER_PROBE_BYTES = 64 * 1024;
180
197
 
181
- /** Most words `stitchTranscript` will look back over to find a repeated seam. */
182
- const MAX_SEAM_WORDS = 40;
183
-
184
198
  /**
185
199
  * What a finished run reports, whichever flow produced it.
186
200
  *
@@ -201,7 +215,7 @@ export type Transcript = {
201
215
  transcript: string;
202
216
  };
203
217
 
204
- /** What one segment's request came back with. */
218
+ /** What one segment's request came back with — the STEP's result, journaled. */
205
219
  export type SegmentTranscript = {
206
220
  index: number;
207
221
  text: string;
@@ -216,15 +230,18 @@ export type SegmentTranscript = {
216
230
  export async function transcribeFlow(input: { recording: string }) {
217
231
  "use workflow";
218
232
 
219
- const startedAt = await startClock();
220
- const plan = await splitRecording(input.recording);
233
+ // Both at once: neither needs the other, and issued together they are one
234
+ // round trip instead of two before any audio is read. The ORDER is still a
235
+ // pure function of this line — the two calls go out synchronously, left to
236
+ // right — which is what a replay reproduces.
237
+ const [startedAt, plan] = await Promise.all([startClock(), splitRecording(input.recording)]);
221
238
 
222
239
  // One step per segment, bounded, in an order a replay reproduces exactly.
223
240
  // A failed segment fails the RUN, deliberately: every sibling that finished is
224
241
  // already journaled, so the resume replays those for free and re-issues only
225
242
  // what is missing, where catching here to salvage a partial transcript would
226
243
  // return a recording with a silent hole in it and report success.
227
- const parts = await mapInBatches(plan.segments, segmentConcurrency(plan.format), (segment) =>
244
+ const parts = await mapConcurrent(plan.segments, segmentConcurrency(plan.format), (segment) =>
228
245
  transcribeSegment(input.recording, plan.format, segment),
229
246
  );
230
247
 
@@ -309,6 +326,17 @@ export async function transcribeSegment(
309
326
  // The LATENCY, which is what says whether the concurrency bound or the endpoint
310
327
  // is the thing limiting the run — see `timed`'s doc.
311
328
  await report(`Transcribed ${clock(segment.startMs)}–${clock(segment.endMs)} in ${elapsed(ms)}.`);
329
+ // And the WORDS, into their own stream, which is what makes this run's answer
330
+ // streamable rather than only its narration: the page stitches whatever has
331
+ // arrived and renders the transcript growing, minutes before `output` exists.
332
+ // Its own namespace because `report`'s stream carries sentences a page prints
333
+ // verbatim — see `emit`'s doc.
334
+ await emit(TRANSCRIPT_STREAM, {
335
+ index: segment.index,
336
+ startMs: segment.startMs,
337
+ endMs: segment.endMs,
338
+ text,
339
+ } satisfies TranscriptChunk);
312
340
  return { index: segment.index, text };
313
341
  }
314
342
 
@@ -337,7 +365,7 @@ export async function mergeTranscript(
337
365
 
338
366
  await report(`Stitching ${parts.length} segment${parts.length === 1 ? "" : "s"} together.`);
339
367
 
340
- // `mapInBatches` resolves in ITEM order however the calls settled, so this is
368
+ // `mapConcurrent` resolves in ITEM order however the calls settled, so this is
341
369
  // already ordered — sorted anyway, because the merge is where an ordering
342
370
  // mistake would be invisible rather than loud.
343
371
  const ordered = [...parts].sort((a, b) => a.index - b.index);
@@ -358,7 +386,7 @@ export async function mergeTranscript(
358
386
  };
359
387
  }
360
388
 
361
- // ---- Pure helpers -----------------------------------------------------------
389
+ // ---- The run's own clock ----------------------------------------------------
362
390
 
363
391
  /**
364
392
  * When the run started, as epoch ms.
@@ -380,71 +408,28 @@ export async function startClock(): Promise<number> {
380
408
  return Date.now();
381
409
  }
382
410
 
383
- /** A word, stripped of the punctuation the decoder added, for seam comparison. */
384
- function seamKey(word: string): string {
385
- return word.toLowerCase().replace(/[^\p{L}\p{N}']/gu, "");
386
- }
411
+ // `clock` and `countWords` are re-exported rather than re-declared: `stream.ts`
412
+ // and `batch.ts` already import them from this module, and the split that let the
413
+ // PAGE stitch a partial transcript should not ripple through every flow.
414
+ export {
415
+ clock,
416
+ countWords,
417
+ stitchChunks,
418
+ stitchTranscript,
419
+ TRANSCRIPT_STREAM,
420
+ type TranscriptChunk,
421
+ } from "./stitch.ts";
422
+
423
+ // ---- I/O helpers ------------------------------------------------------------
387
424
 
388
425
  /**
389
- * Join segment transcripts, dropping the words the overlap made duplicates.
390
- *
391
- * Segments overlap by `SEGMENT_OVERLAP_SECONDS` (see `wav.ts` for why), so the
392
- * last few words of one segment are the first few of the next — verbatim when
393
- * the decoder heard them the same way, which is the common case because it heard
394
- * the same audio. This finds the longest such run and removes one copy.
395
- *
396
- * Comparison is on `seamKey`, not the raw words: the two passes punctuate
397
- * differently at their own edges (one ends a sentence where the other is
398
- * mid-clause), so `"today."` and `"today"` are the same word and a raw compare
399
- * finds no seam at all. The text KEPT is the raw text — only the match is
400
- * normalized.
426
+ * Run a `wav.ts` helper, turning its "cannot cut this" into a terminal failure.
401
427
  *
402
- * A missed seam repeats a few words, which a reader can see and forgive. A
403
- * false one would delete speech, so the search is bounded at
404
- * `MAX_SEAM_WORDS` and always prefers the LONGEST match: a single repeated
405
- * "the" is not evidence of anything, and requiring the longest run is what stops
406
- * it counting as one when a longer match is available.
428
+ * Exported for the same reason `countWords` is: `stream.ts` plans with the same
429
+ * `wav.ts` helpers and owes the same classification, and it had this byte for
430
+ * byte.
407
431
  */
408
- export function stitchTranscript(parts: readonly string[]): string {
409
- const merged: string[] = [];
410
- for (const part of parts) {
411
- const next = part.split(/\s+/).filter(Boolean);
412
- if (next.length === 0) continue;
413
- if (merged.length === 0) {
414
- merged.push(...next);
415
- continue;
416
- }
417
- merged.push(...next.slice(seamLength(merged, next)));
418
- }
419
- return merged.join(" ");
420
- }
421
-
422
- /** How many leading words of `next` repeat the tail of `merged`. */
423
- function seamLength(merged: readonly string[], next: readonly string[]): number {
424
- const limit = Math.min(MAX_SEAM_WORDS, merged.length, next.length);
425
- // Longest first, so a short accidental match never wins over a real seam.
426
- for (let length = limit; length > 0; length--) {
427
- const tail = merged.slice(merged.length - length);
428
- if (tail.every((word, at) => seamKey(word) === seamKey(next[at] ?? ""))) return length;
429
- }
430
- return 0;
431
- }
432
-
433
- /** Words in a string. Exported so the streaming flow reports the same number. */
434
- export function countWords(text: string): number {
435
- return text.split(/\s+/).filter(Boolean).length;
436
- }
437
-
438
- /** `m:ss` for the progress log — a byte offset means nothing to a reader. */
439
- export function clock(ms: number): string {
440
- const seconds = Math.max(0, Math.round(ms / 1000));
441
- return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
442
- }
443
-
444
- // ---- I/O helpers ------------------------------------------------------------
445
-
446
- /** Run a `wav.ts` helper, turning its "cannot cut this" into a terminal failure. */
447
- function fatalOnUnsupported<T>(read: () => T): T {
432
+ export function fatalOnUnsupported<T>(read: () => T): T {
448
433
  try {
449
434
  return read();
450
435
  } catch (err: unknown) {
@@ -38,9 +38,7 @@ function resolveVitestCommand(cwd, resolve = createRequire(path.join(cwd, "packa
38
38
  * Throws on test failure.
39
39
  */
40
40
  function runVitest(cwd) {
41
- let testFile = null;
42
- if (existsSync(path.join(cwd, "agent.test.ts"))) testFile = "agent.test.ts";
43
- else if (existsSync(path.join(cwd, "agent.test.js"))) testFile = "agent.test.js";
41
+ const testFile = ["agent.test.ts", "agent.test.js"].find((name) => existsSync(path.join(cwd, name)));
44
42
  if (!testFile) return false;
45
43
  const { cmd, args } = resolveVitestCommand(cwd);
46
44
  execaSync(cmd, [
@@ -155,12 +155,11 @@ async function typecheckProject(cwd) {
155
155
  });
156
156
  let output = "";
157
157
  const keep = (s) => s.length > OUTPUT_CAP ? `…${s.slice(-16e3)}` : s;
158
- child.stdout.on("data", (chunk) => {
158
+ const append = (chunk) => {
159
159
  output = keep(output + chunk.toString());
160
- });
161
- child.stderr.on("data", (chunk) => {
162
- output = keep(output + chunk.toString());
163
- });
160
+ };
161
+ child.stdout.on("data", append);
162
+ child.stderr.on("data", append);
164
163
  child.on("error", reject);
165
164
  child.on("close", (code, signal) => {
166
165
  if (signal) {
@@ -145,8 +145,19 @@ var AaiWorkflowBuilder = class extends BaseBuilder {
145
145
  * repo's `erasableSyntaxOnly`, so the fields are declared the long way too.
146
146
  */
147
147
  output;
148
- outDir;
148
+ /**
149
+ * The two bundles `build()` reads back.
150
+ *
151
+ * Held as fields because `super()` is also told where to write them: derived
152
+ * a second time inside `build()`, the two spellings of each path were free to
153
+ * disagree, and a `build()` reading a file the builder never wrote is a
154
+ * "produced nothing" failure that names neither path.
155
+ */
156
+ flowFile;
157
+ stepFile;
149
158
  constructor(cwd, outDir) {
159
+ const flowFile = path.join(outDir, "flow.mjs");
160
+ const stepFile = path.join(outDir, "step.mjs");
150
161
  super({
151
162
  buildTarget: "standalone",
152
163
  dirs: [`./${WORKFLOWS_DIR}`],
@@ -154,31 +165,30 @@ var AaiWorkflowBuilder = class extends BaseBuilder {
154
165
  projectRoot: cwd,
155
166
  moduleSpecifierRoot: cwd,
156
167
  externalPackages: [...WDK_EXTERNAL],
157
- stepsBundlePath: path.join(outDir, "step.mjs"),
158
- workflowsBundlePath: path.join(outDir, "flow.mjs"),
168
+ stepsBundlePath: stepFile,
169
+ workflowsBundlePath: flowFile,
159
170
  webhookBundlePath: path.join(outDir, "webhook.mjs"),
160
171
  suppressCreateWorkflowsBundleLogs: true,
161
172
  suppressCreateWebhookBundleLogs: true,
162
173
  suppressCreateManifestLogs: true
163
174
  });
164
- this.outDir = outDir;
175
+ this.flowFile = flowFile;
176
+ this.stepFile = stepFile;
165
177
  }
166
178
  async build() {
167
179
  const inputFiles = await this.getInputFiles();
168
- const flowFile = path.join(this.outDir, "flow.mjs");
169
- const stepFile = path.join(this.outDir, "step.mjs");
170
180
  const { manifest } = await this.createWorkflowsBundle({
171
181
  inputFiles,
172
182
  format: "esm",
173
- outfile: flowFile,
183
+ outfile: this.flowFile,
174
184
  bundleFinalOutput: false
175
185
  });
176
186
  await this.createStepsBundle({
177
187
  inputFiles,
178
188
  format: "esm",
179
- outfile: stepFile
189
+ outfile: this.stepFile
180
190
  });
181
- const [workflowCode, stepCode] = await Promise.all([fs.readFile(flowFile, "utf-8"), fs.readFile(stepFile, "utf-8")]);
191
+ const [workflowCode, stepCode] = await Promise.all([fs.readFile(this.flowFile, "utf-8"), fs.readFile(this.stepFile, "utf-8")]);
182
192
  this.output = {
183
193
  workflowCode,
184
194
  stepCode: STEP_REQUIRE_SHIM + stepCode,
@@ -358,8 +368,12 @@ export const __aaiStepCode = ${JSON.stringify(workflows.stepCode)};
358
368
  */
359
369
  async function buildWorker(cwd, opts = {}) {
360
370
  const wrapperPath = path.join(cwd, WRAPPER_ENTRY_REL);
361
- await fs.mkdir(path.dirname(wrapperPath), { recursive: true });
362
- await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false, opts.workflows, await discoverToolFiles(cwd), await hasSystemPromptFile(cwd)), "utf-8");
371
+ const [, toolFiles, systemPromptFile] = await Promise.all([
372
+ fs.mkdir(path.dirname(wrapperPath), { recursive: true }),
373
+ discoverToolFiles(cwd),
374
+ hasSystemPromptFile(cwd)
375
+ ]);
376
+ await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false, opts.workflows, toolFiles, systemPromptFile), "utf-8");
363
377
  const plugins = [...opts.plugins ?? [], ...opts.workflows ? [workflowClientPlugin(cwd, opts.workflows.inputFiles)] : []];
364
378
  let result;
365
379
  try {
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildWorker } from "./worker-bundler-CIdDhZ0c.mjs";
2
+ import { t as buildWorker } from "./worker-bundler-CGD4r8Kc.mjs";
3
3
  export { buildWorker };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
3
3
  import { n as log } from "./_ui-u7T4YooX.mjs";
4
- import { n as getServerInfo } from "./_agent-vGW5SBnX.mjs";
4
+ import { n as getServerInfo } from "./_agent-DpH2pBJd.mjs";
5
5
  import { errorMessage, omitUndefined } from "@alexkroman1/aai/utils";
6
6
  import { createWorkflowApiClient } from "@alexkroman1/aai/workflow-api";
7
7
  //#region workflow.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
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": "6.4.0",
48
- "@alexkroman1/aai-ui": "6.4.0"
47
+ "@alexkroman1/aai": "6.5.1",
48
+ "@alexkroman1/aai-ui": "6.5.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "playwright": "^1.62.1",
@@ -53,7 +53,7 @@
53
53
  "verdaccio": "^6.9.2",
54
54
  "vitest": "^4.1.10",
55
55
  "workflow": "4.8.2",
56
- "aai-templates": "0.3.5"
56
+ "aai-templates": "0.3.6"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "vitest": "^4.1.10"
@@ -1,94 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as isStringArray, i as checkedResponse, n as apiRequest } from "./_api-client-LgLksMhN.mjs";
3
- import path from "node:path";
4
- import { MAX_SLUG_LENGTH, PREVIEW_SLUG_SUFFIX, VALID_SLUG_RE, isRecord } from "@alexkroman1/aai/utils";
5
- import { slugifyName } from "@alexkroman1/aai/slugify";
6
- import { isLocalOnlyFile, snapshotWorkspaceFiles } from "@alexkroman1/aai/workspace-files";
7
- //#region _studio.ts
8
- /**
9
- * Internals of the studio-workspace commands (`aai list/pull/push/publish`):
10
- * the local source-file walk and the thin clients for the platform's
11
- * `/studio/projects` routes. The workspace is the single source of truth —
12
- * these helpers only move files between a local directory and the project's
13
- * workspace row; production deploys happen exclusively through the studio's
14
- * Publish route (which runs the deploy machinery in the project's sandbox).
15
- */
16
- /**
17
- * Snapshot the local project into a workspace file map.
18
- *
19
- * The walk, the caps, the skip rules and the strict UTF-8 decode all come
20
- * from the SDK (`@alexkroman1/aai/workspace-files`), which is also what the
21
- * guest's end-of-turn sync uses — the two write the same map from opposite
22
- * ends, and a disagreement between them is not an error but a file silently
23
- * dropped here and resurrected there.
24
- *
25
- * `isLocalOnlyFile` is the one rule this side adds: `.env` rides the secret
26
- * routes and lockfiles are install output, so neither belongs in a row.
27
- */
28
- function collectSourceFiles(dir) {
29
- return snapshotWorkspaceFiles(dir, {
30
- subject: "Project",
31
- skipFile: isLocalOnlyFile
32
- });
33
- }
34
- /**
35
- * A studio project name derived from a directory name, or null if unusable.
36
- *
37
- * The normalization is the platform's own (`slugifyName`), not a local
38
- * regex. This used to strip `[^a-z0-9-_]` by hand, which differs from the
39
- * studio's slugifier on exactly the names people give agents: `Café
40
- * Ordering` reduced to `caf-ordering` here and `cafe-ordering` there, so the
41
- * same directory produced a different project depending on whether it was
42
- * created by `aai push` or by typing the name into the studio.
43
- *
44
- * A `-preview` suffix is deliberately unusable. Publishing a project deploys
45
- * it under the project's own name, so a `*-preview` project would claim a
46
- * slug the studio's orphan-preview sweep reaps hourly — deleting the agent,
47
- * its app-database schema, and its secrets on a schedule the user never
48
- * asked for. Refusing the name is recoverable (rename the directory); losing
49
- * a published agent to the reaper is not.
50
- */
51
- function projectNameFromDir(dir) {
52
- const name = slugifyName(path.basename(dir), MAX_SLUG_LENGTH);
53
- if (name.endsWith(PREVIEW_SLUG_SUFFIX)) return null;
54
- return VALID_SLUG_RE.test(name) ? name : null;
55
- }
56
- /** The shareable studio URL for a project — what every command prints. */
57
- function studioProjectUrl(serverUrl, project) {
58
- return `${serverUrl}/studio/chat/${project}`;
59
- }
60
- function listStudioProjects(serverUrl, apiKey) {
61
- return apiRequest(`${serverUrl}/studio/projects`, {
62
- apiKey,
63
- action: "list"
64
- }).then((res) => checkedResponse(res, (value) => isRecord(value) && isStringArray(value.projects), `the studio project list at ${serverUrl}`).projects);
65
- }
66
- /** Fetch a project, or null when it doesn't exist (the push existence probe). */
67
- function fetchStudioProject(serverUrl, apiKey, project) {
68
- return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}`, {
69
- apiKey,
70
- action: "pull",
71
- allow404: true
72
- });
73
- }
74
- /** `PUT /studio/projects/:project/source` — the atomic whole-tree push. */
75
- function pushStudioSource(serverUrl, apiKey, project, body) {
76
- return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/source`, {
77
- apiKey,
78
- action: "push",
79
- method: "PUT",
80
- body,
81
- hints: { 409: "The studio has newer changes. Run `aai pull` to fetch them, or `aai push --force` to overwrite." }
82
- });
83
- }
84
- /** `POST /studio/projects/:project/deploy` — Publish, in the project's sandbox. */
85
- function publishStudioProject(serverUrl, apiKey, project) {
86
- return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/deploy`, {
87
- apiKey,
88
- action: "publish",
89
- method: "POST",
90
- retry: 0
91
- });
92
- }
93
- //#endregion
94
- export { publishStudioProject as a, projectNameFromDir as i, fetchStudioProject as n, pushStudioSource as o, listStudioProjects as r, studioProjectUrl as s, collectSourceFiles as t };