@alexkroman1/aai-cli 13.2.0 → 14.0.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.
Files changed (152) hide show
  1. package/bin.mjs +1 -1
  2. package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
  3. package/dist/_agent.d.ts +23 -0
  4. package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
  5. package/dist/_cli-common.d.ts +45 -6
  6. package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
  7. package/dist/_config.d.ts +25 -1
  8. package/dist/_dev-env.d.ts +1 -1
  9. package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
  10. package/dist/_dev-vite-config.d.ts +3 -3
  11. package/dist/_e2e-test-utils.d.ts +15 -7
  12. package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
  13. package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
  14. package/dist/_output.d.ts +24 -7
  15. package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
  16. package/dist/_resource-commands.d.ts +9 -9
  17. package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
  18. package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
  19. package/dist/_slug-api.d.ts +19 -0
  20. package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
  21. package/dist/_studio-commands.d.ts +13 -13
  22. package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
  23. package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
  24. package/dist/_typecheck-gate.d.ts +3 -1
  25. package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
  26. package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
  27. package/dist/_utils.d.ts +35 -0
  28. package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
  29. package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
  30. package/dist/_workflow-determinism.d.ts +3 -3
  31. package/dist/build-Mxk8gWvX.mjs +108 -0
  32. package/dist/build.d.ts +5 -0
  33. package/dist/cli.mjs +172 -116
  34. package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
  35. package/dist/client-bundler.mjs +1 -1
  36. package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
  37. package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
  38. package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
  39. package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
  40. package/dist/eval.d.ts +2 -0
  41. package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
  42. package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
  43. package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
  44. package/dist/project-config.mjs +1 -1
  45. package/dist/scaffold/.env.example +17 -1
  46. package/dist/scaffold/CLAUDE.md +84 -56
  47. package/dist/scaffold/package.json +6 -5
  48. package/dist/scaffold/server.mjs +129 -25
  49. package/dist/secret-Cjp912yF.mjs +162 -0
  50. package/dist/secret.d.ts +48 -13
  51. package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
  52. package/dist/templates/briefing-desk/agent.test.ts +7 -3
  53. package/dist/templates/call-audit/agent.test.ts +25 -9
  54. package/dist/templates/call-audit/agent.ts +2 -2
  55. package/dist/templates/call-audit/client.tsx +40 -58
  56. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  57. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  58. package/dist/templates/call-audit/workflows/media.ts +12 -37
  59. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  60. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  61. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  62. package/dist/templates/dispatch-center/client.tsx +53 -61
  63. package/dist/templates/dispatch-center/shared.ts +0 -8
  64. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  65. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  66. package/dist/templates/embedded-assets/agent.test.ts +7 -1
  67. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  68. package/dist/templates/health-assistant/agent.test.ts +8 -4
  69. package/dist/templates/infocom-adventure/client.tsx +40 -36
  70. package/dist/templates/link-digest/agent.test.ts +10 -4
  71. package/dist/templates/link-digest/agent.ts +2 -2
  72. package/dist/templates/link-digest/client.tsx +42 -36
  73. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  74. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  75. package/dist/templates/night-owl/agent.test.ts +6 -1
  76. package/dist/templates/night-owl/client.tsx +8 -2
  77. package/dist/templates/pipeline-simple/agent.test.ts +33 -16
  78. package/dist/templates/pizza-ordering/client.tsx +5 -4
  79. package/dist/templates/pizza-ordering/shared.ts +3 -8
  80. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  81. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  82. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  83. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  84. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  85. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  86. package/dist/templates/plan-and-execute/client.tsx +2 -2
  87. package/dist/templates/podcast-digest/agent.test.ts +33 -14
  88. package/dist/templates/podcast-digest/client.tsx +27 -36
  89. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  90. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  91. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  92. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  93. package/dist/templates/recap-workflow/agent.test.ts +49 -32
  94. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  95. package/dist/templates/redline/agent.eval.test.ts +5 -8
  96. package/dist/templates/redline/agent.test.ts +8 -2
  97. package/dist/templates/redline/client.tsx +47 -48
  98. package/dist/templates/redline/workflows/redline.ts +17 -17
  99. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  100. package/dist/templates/research-workflow/agent.test.ts +17 -8
  101. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  102. package/dist/templates/retail/agent.test.ts +17 -15
  103. package/dist/templates/retail/cancel.ts +7 -6
  104. package/dist/templates/retail/client.tsx +39 -58
  105. package/dist/templates/retail/payment.ts +4 -3
  106. package/dist/templates/retail/pending.test.ts +19 -19
  107. package/dist/templates/retail/registry.test.ts +99 -58
  108. package/dist/templates/retail/store.test.ts +5 -5
  109. package/dist/templates/retail/swap.ts +8 -11
  110. package/dist/templates/simple/agent.test.ts +39 -6
  111. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  112. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  113. package/dist/templates/solo-rpg/client.tsx +3 -3
  114. package/dist/templates/spoken-summary/agent.test.ts +21 -12
  115. package/dist/templates/spoken-summary/agent.ts +3 -3
  116. package/dist/templates/spoken-summary/client.tsx +36 -47
  117. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  118. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  119. package/dist/templates/support-line/client.tsx +4 -3
  120. package/dist/templates/support-line/prompts.ts +0 -2
  121. package/dist/templates/transcription-workflow/agent.test.ts +36 -20
  122. package/dist/templates/transcription-workflow/agent.ts +2 -2
  123. package/dist/templates/transcription-workflow/client.tsx +34 -40
  124. package/dist/templates/transcription-workflow/recover.ts +1 -1
  125. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  126. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  127. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  128. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  129. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  130. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  131. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  132. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  133. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  134. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  135. package/dist/templates/travel-concierge/client.tsx +2 -2
  136. package/dist/templates/travel-concierge/shared.ts +10 -14
  137. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  138. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  139. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  140. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  141. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  142. package/dist/test-D032V2cA.mjs +242 -0
  143. package/dist/test.d.ts +97 -31
  144. package/dist/typecheck.mjs +2 -2
  145. package/dist/worker-bundler.mjs +3 -3
  146. package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
  147. package/package.json +10 -10
  148. package/dist/bin.d.mts +0 -2
  149. package/dist/build-BLlLOrng.mjs +0 -66
  150. package/dist/secret-B-3T5tvr.mjs +0 -61
  151. package/dist/test-CiLab-AA.mjs +0 -182
  152. package/dist/tsdown.config.d.ts +0 -2
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { a as ok, n as fail, t as CliError } from "./_output-DBP9Op_d.mjs";
3
+ import { n as log, s as unwrapCancel } from "./_ui-DCt4qZrk.mjs";
4
+ import { f as checkedResponse, p as isStringArray } from "./_studio-DnR_BqFp.mjs";
5
+ import { t as secretRequest } from "./_slug-api-CGvc2YSQ.mjs";
6
+ import { isRecord, plural } from "@alexkroman1/aai/utils";
7
+ import * as p from "@clack/prompts";
8
+ import pTimeout from "p-timeout";
9
+ //#region src/secret.ts
10
+ /**
11
+ * The one `no_input` failure for `secret put`, shared by every path that can
12
+ * reach it — the stdin read that came back empty, the refusal when stdin is a
13
+ * terminal and JSON mode cannot prompt, and the prompt the user dismissed —
14
+ * so the three cannot drift.
15
+ *
16
+ * It is built here rather than kept as a fixed tuple so the hint can name the
17
+ * secret AND both ways of supplying a value: that sentence is the contract,
18
+ * and until this commit it appeared nowhere — not in `--help`, not in the
19
+ * scaffolded README (which said it prompts), and not in the failure itself,
20
+ * which said only "Pipe secret value to stdin".
21
+ */
22
+ function noInput(name, detail) {
23
+ return {
24
+ code: "no_input",
25
+ message: `No value provided for ${name} (${detail})`,
26
+ hint: `Pipe the value in — \`printf %s "$VALUE" | aai secret put ${name}\` — or run the command on a terminal to be prompted for it.`
27
+ };
28
+ }
29
+ /**
30
+ * How long stdin may stay SILENT before `secret put` gives up on it.
31
+ *
32
+ * It bounds only the FIRST byte — a producer that has started is then given
33
+ * as long as it likes, because a secret usually comes out of something slow
34
+ * (`pass show`, `op read`, a KMS call) and cutting it off mid-value would be
35
+ * worse than waiting.
36
+ *
37
+ * What the bound is for is stdin with no producer behind it at all: an
38
+ * inherited-but-idle pipe, which is what an agent harness, a CI step, a
39
+ * supervisor and an editor task all hand a child. Reading to EOF is correct
40
+ * and the EOF was never coming, so the command printed NOTHING and blocked
41
+ * forever — on the only documented way to get a credential into production.
42
+ * A bounded wait that ends in a sentence naming both working forms is the
43
+ * whole difference between "unusable" and "I typed it wrong".
44
+ */
45
+ const STDIN_FIRST_BYTE_MS = 1e4;
46
+ /**
47
+ * Read stdin to EOF, giving up if nothing arrives at all — `undefined` for
48
+ * that case, distinct from `""` for a producer that sent nothing and closed.
49
+ *
50
+ * Hand-rolled rather than `text(process.stdin)` because the deadline has to
51
+ * apply to the first chunk and NOT to the whole read; `p-timeout`'s
52
+ * `message: false` resolves `undefined` instead of rejecting, so there is no
53
+ * timer to clear and no losing branch to reject late.
54
+ */
55
+ async function readStdinToEnd(stdin, firstByteMs) {
56
+ const chunks = [];
57
+ const firstByte = Promise.withResolvers();
58
+ const ended = Promise.withResolvers();
59
+ const onData = (chunk) => {
60
+ chunks.push(chunk);
61
+ firstByte.resolve(true);
62
+ };
63
+ const onEnd = () => {
64
+ firstByte.resolve(true);
65
+ ended.resolve();
66
+ };
67
+ const onError = (err) => {
68
+ firstByte.reject(err);
69
+ ended.reject(err);
70
+ };
71
+ stdin.on("data", onData).once("end", onEnd).once("error", onError);
72
+ try {
73
+ if (!await pTimeout(firstByte.promise, {
74
+ milliseconds: firstByteMs,
75
+ message: false
76
+ })) return void 0;
77
+ await ended.promise;
78
+ return Buffer.concat(chunks).toString("utf8").trim();
79
+ } finally {
80
+ stdin.off("data", onData).off("end", onEnd).off("error", onError);
81
+ stdin.pause();
82
+ }
83
+ }
84
+ /**
85
+ * The value for `secret put`, from whichever source is actually available —
86
+ * or `undefined` to mean "prompt", which {@link executeSecretPut} does.
87
+ *
88
+ * Keyed off STDIN, deliberately, and never off the output mode. A stdin read
89
+ * resolves at EOF and a terminal never sends one, so keying the read off
90
+ * `mode === "json"` — which is decided by STDOUT — made `aai secret put NAME`
91
+ * print NOTHING and block forever in every context where stdout is not a
92
+ * terminal but stdin still is: a CI step, `| tee`, an agent harness, an
93
+ * editor task. That is the worst failure a CLI can have on the only
94
+ * documented way to get a credential into production, and it is why each of
95
+ * the three cases below is answered at once:
96
+ *
97
+ * - stdin is a pipe or a file: read it to EOF — the documented contract —
98
+ * bounded on the FIRST byte so an idle inherited pipe cannot block the
99
+ * command forever (see {@link STDIN_FIRST_BYTE_MS}). An empty read is
100
+ * refused rather than sent to the server.
101
+ * - stdin is a terminal, human output: `undefined`, so the executor prompts.
102
+ * - stdin is a terminal, JSON output: refused immediately. JSON mode promises
103
+ * exactly one result line on stdout and cannot spend it on a prompt — so it
104
+ * says which two forms do work instead of waiting for an EOF that a person
105
+ * sitting at a terminal has no reason to send.
106
+ */
107
+ async function resolveSecretValue(name, mode, io = {}) {
108
+ if (!(io.stdinIsTTY ?? Boolean(process.stdin.isTTY))) {
109
+ const firstByteMs = io.firstByteMs ?? STDIN_FIRST_BYTE_MS;
110
+ const piped = await readStdinToEnd(io.stdin ?? process.stdin, firstByteMs);
111
+ if (piped) return piped;
112
+ const { code, message, hint } = noInput(name, piped === void 0 ? `nothing arrived on stdin within ${Math.round(firstByteMs / 1e3)}s` : "stdin was empty");
113
+ throw new CliError(code, message, hint);
114
+ }
115
+ if (mode === "json") {
116
+ const { code, message, hint } = noInput(name, "stdin is a terminal and --json cannot prompt");
117
+ throw new CliError(code, message, hint);
118
+ }
119
+ }
120
+ /**
121
+ * Execute secret put. If `value` is provided, use it directly (the stdin
122
+ * path). If not, prompt for it — masked — which is what
123
+ * {@link resolveSecretValue} returning `undefined` asks for.
124
+ */
125
+ async function executeSecretPut(cwd, name, value, server) {
126
+ let secretValue = value;
127
+ if (!secretValue) {
128
+ const result = unwrapCancel(await p.password({ message: `Enter value for ${name}` }));
129
+ if (!result) {
130
+ const { code, message, hint } = noInput(name, "the prompt came back empty");
131
+ return fail(code, message, hint);
132
+ }
133
+ secretValue = result;
134
+ }
135
+ const { target } = await secretRequest(cwd, "", {
136
+ method: "PUT",
137
+ body: { [name]: secretValue },
138
+ action: "secret"
139
+ }, server);
140
+ log.success(`Set ${name} for ${target}`);
141
+ return ok({ name });
142
+ }
143
+ async function executeSecretDelete(cwd, name, server) {
144
+ const { target } = await secretRequest(cwd, `/${encodeURIComponent(name)}`, {
145
+ method: "DELETE",
146
+ action: "secret"
147
+ }, server);
148
+ log.success(`Deleted ${name} from ${target}`);
149
+ return ok({ name });
150
+ }
151
+ async function executeSecretList(cwd, server) {
152
+ const { data, target } = await secretRequest(cwd, "", { action: "secret" }, server);
153
+ const { vars } = checkedResponse(data, (value) => isRecord(value) && isStringArray(value.vars), `the secret list for ${target}`);
154
+ if (vars.length === 0) log.info("No secrets set. Use `aai secret put <name>` to add one.");
155
+ else {
156
+ log.message(`${vars.length} ${plural(vars.length, "secret")}:`);
157
+ for (const v of vars) log.message(` ${v}`);
158
+ }
159
+ return ok({ secrets: vars });
160
+ }
161
+ //#endregion
162
+ export { executeSecretDelete, executeSecretList, executeSecretPut, resolveSecretValue };
package/dist/secret.d.ts CHANGED
@@ -1,25 +1,60 @@
1
- import { type CommandResult } from "./_output.ts";
1
+ import type { Readable } from "node:stream";
2
+ import { type CommandResult, type OutputMode } from "./_output.ts";
2
3
  /**
3
- * The one `no_input` failure for `secret put`, shared by the JSON-mode stdin
4
- * path (cli.ts) and the TTY prompt path below so the two can't drift.
4
+ * The environment {@link resolveSecretValue} branches on, as a seam a spec can
5
+ * supply a `Readable` rather than a fake reader function, so a test drives
6
+ * the real wait (the part that blocked) and needs no cast to stand in for
7
+ * `process.stdin`.
5
8
  */
6
- export declare const NO_INPUT: readonly ["no_input", "No value provided", "Pipe secret value to stdin"];
7
- /** Read secret value from stdin (for non-TTY / piped input). */
8
- export declare function readStdin(): Promise<string>;
9
- type SecretPutData = {
10
- name: string;
9
+ export type SecretValueIo = {
10
+ /** Defaults to `process.stdin`. */
11
+ stdin?: Readable | undefined;
12
+ /** Defaults to whether the real stdin is a terminal. */
13
+ stdinIsTTY?: boolean | undefined;
14
+ /** Defaults to {@link STDIN_FIRST_BYTE_MS}. */
15
+ firstByteMs?: number | undefined;
11
16
  };
12
- type SecretDeleteData = {
17
+ /**
18
+ * The value for `secret put`, from whichever source is actually available —
19
+ * or `undefined` to mean "prompt", which {@link executeSecretPut} does.
20
+ *
21
+ * Keyed off STDIN, deliberately, and never off the output mode. A stdin read
22
+ * resolves at EOF and a terminal never sends one, so keying the read off
23
+ * `mode === "json"` — which is decided by STDOUT — made `aai secret put NAME`
24
+ * print NOTHING and block forever in every context where stdout is not a
25
+ * terminal but stdin still is: a CI step, `| tee`, an agent harness, an
26
+ * editor task. That is the worst failure a CLI can have on the only
27
+ * documented way to get a credential into production, and it is why each of
28
+ * the three cases below is answered at once:
29
+ *
30
+ * - stdin is a pipe or a file: read it to EOF — the documented contract —
31
+ * bounded on the FIRST byte so an idle inherited pipe cannot block the
32
+ * command forever (see {@link STDIN_FIRST_BYTE_MS}). An empty read is
33
+ * refused rather than sent to the server.
34
+ * - stdin is a terminal, human output: `undefined`, so the executor prompts.
35
+ * - stdin is a terminal, JSON output: refused immediately. JSON mode promises
36
+ * exactly one result line on stdout and cannot spend it on a prompt — so it
37
+ * says which two forms do work instead of waiting for an EOF that a person
38
+ * sitting at a terminal has no reason to send.
39
+ */
40
+ export declare function resolveSecretValue(name: string, mode: OutputMode, io?: SecretValueIo): Promise<string | undefined>;
41
+ /**
42
+ * Both the put and the delete result — one shape, so one name. They were two
43
+ * distinct aliases for `{ name: string }`, which implied a difference that does
44
+ * not exist and left "which alias meant the secret result" to be re-decided.
45
+ */
46
+ type SecretNameData = {
13
47
  name: string;
14
48
  };
15
49
  type SecretListData = {
16
50
  secrets: string[];
17
51
  };
18
52
  /**
19
- * Execute secret put. If `value` is provided, use it directly (non-TTY path).
20
- * If not provided, prompt interactively (TTY path).
53
+ * Execute secret put. If `value` is provided, use it directly (the stdin
54
+ * path). If not, prompt for it — masked — which is what
55
+ * {@link resolveSecretValue} returning `undefined` asks for.
21
56
  */
22
- export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretPutData>>;
23
- export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretDeleteData>>;
57
+ export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretNameData>>;
58
+ export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretNameData>>;
24
59
  export declare function executeSecretList(cwd: string, server: string | undefined): Promise<CommandResult<SecretListData>>;
25
60
  export {};
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { a as ok, t as CliError } from "./_output-CKkmWs7i.mjs";
3
- import { n as log, t as fmtUrl } from "./_ui-u7T4YooX.mjs";
4
- import { s as updateProjectConfig } from "./_config-CmJOFsAP.mjs";
5
- import { t as resolveServerEnv } from "./_server-common-6e0QI4mq.mjs";
6
- import { a as resolveDeployTarget } from "./_agent-BTwZJu4P.mjs";
7
- import { layerScaffold } from "./_templates-0Zr2z6yA.mjs";
8
- import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-B1waXMuR.mjs";
2
+ import { a as ok, t as CliError } from "./_output-DBP9Op_d.mjs";
3
+ import { n as log, t as fmtUrl } from "./_ui-DCt4qZrk.mjs";
4
+ import { l as formatCappedList } from "./_utils-D5JGcjiW.mjs";
5
+ import { s as updateProjectConfig } from "./_config-DYzC6WMD.mjs";
6
+ import { t as resolveServerEnv } from "./_server-common-vILJp3it.mjs";
7
+ import { o as resolveDeployTarget } from "./_agent-BzUeqOdj.mjs";
8
+ import { layerScaffold } from "./_templates-CK4oKoeX.mjs";
9
+ import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-DnR_BqFp.mjs";
9
10
  import { existsSync } from "node:fs";
10
11
  import path from "node:path";
11
- import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
12
+ import { isRecord, omitUndefined, plural } from "@alexkroman1/aai/utils";
12
13
  import { mkdir, readdir, writeFile } from "node:fs/promises";
13
14
  import { isPathInside } from "@alexkroman1/aai-runtime/internal";
14
- //#region studio.ts
15
+ //#region src/studio.ts
15
16
  /**
16
17
  * The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
17
18
  * `aai publish`.
@@ -62,7 +63,7 @@ async function notFoundHint(serverUrl, apiKey) {
62
63
  const projects = await listStudioProjects(serverUrl, apiKey).catch(() => null);
63
64
  if (projects === null) return "Run `aai list` to see your projects.";
64
65
  if (projects.length === 0) return "This login has no studio projects at all. If yours are in the studio, the CLI is linked to a different account — run `aai login` again, approve it in a browser signed in to the account that owns the project, then `aai list`.";
65
- return `Your projects: ${projects.slice(0, 10).join(", ")}${projects.length > 10 ? `, and ${projects.length - 10} more` : ""}.`;
66
+ return `Your projects: ${formatCappedList(projects)}.`;
66
67
  }
67
68
  async function executePull(opts) {
68
69
  const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
@@ -159,17 +160,14 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
159
160
  method: "PUT",
160
161
  body: env
161
162
  });
162
- log.info(`Synced ${names.length} secret${names.length === 1 ? "" : "s"} from .env`);
163
+ log.info(`Synced ${names.length} ${plural(names.length, "secret")} from .env`);
163
164
  return names;
164
165
  }
165
166
  async function executePublish(opts) {
166
- if (!opts.skipTypecheck) {
167
- const { assertTypechecks } = await import("./_typecheck-gate-BXvUNnfs.mjs");
168
- await assertTypechecks(opts.cwd);
169
- }
167
+ const { assertTypechecks } = await import("./_typecheck-gate-U3jcil8L.mjs");
168
+ await assertTypechecks(opts.cwd, { skip: opts.skipTypecheck });
170
169
  const pushed = await pushProject(opts);
171
170
  const { project, serverUrl, apiKey } = pushed;
172
- const hadSlug = pushed.slug !== void 0;
173
171
  if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
174
172
  log.step(`Publishing ${project} (builds in the project's sandbox)…`);
175
173
  const result = checkedResponse(await publishStudioProject(serverUrl, apiKey, project, { skipTypecheck: opts.skipTypecheck }), (value) => isRecord(value) && typeof value.slug === "string" && typeof value.output === "string", `the publish route at ${serverUrl}`);
@@ -178,7 +176,7 @@ async function executePublish(opts) {
178
176
  serverUrl,
179
177
  slug: result.slug
180
178
  });
181
- if (!hadSlug) {
179
+ if (!pushed.slug) {
182
180
  if ((await syncEnvSecrets(opts.cwd, serverUrl, apiKey, result.slug)).length > 0) log.info("They apply on the next `aai publish`.");
183
181
  }
184
182
  const agentUrl = `${serverUrl}/${result.slug}`;
@@ -63,10 +63,14 @@ function desk(
63
63
 
64
64
  describe("the desk itself", () => {
65
65
  test("has no web tools of its own — everything goes through a subagent", () => {
66
- expect(authoredAgent.builtinTools ?? []).toEqual([]);
67
- // And the researcher does, which is the split the template exists to show.
66
+ // Stated as the claim the test's name makes, rather than as "no builtins at
67
+ // all": giving the desk `run_code` is a reasonable edit, and it does not
68
+ // put the web in front of the desk.
69
+ expect(authoredAgent.builtinTools ?? []).not.toContain("web_search");
70
+ expect(authoredAgent.builtinTools ?? []).not.toContain("visit_webpage");
71
+ // And the subagents do, which is the split the template exists to show.
68
72
  expect(researcher.builtinTools).toContain("web_search");
69
- expect(factChecker.builtinTools).toEqual(["web_search"]);
73
+ expect(factChecker.builtinTools).toContain("web_search");
70
74
  });
71
75
 
72
76
  test("gives the checker a tighter budget than the researcher", () => {
@@ -32,7 +32,7 @@
32
32
  import { readdir } from "node:fs/promises";
33
33
  import { tmpdir } from "node:os";
34
34
  import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
35
- import { createWorkflowCtx, stubSpeech, WORKFLOW_CTX_NOW } from "@alexkroman1/aai/testing";
35
+ import { createWorkflowContext, stubSpeech, WORKFLOW_CONTEXT_NOW } from "@alexkroman1/aai/testing";
36
36
  import {
37
37
  installStubGateway,
38
38
  installStubReporter,
@@ -182,15 +182,26 @@ describe("the declaration", () => {
182
182
  test("is a workflow app with the one workflow the page starts by name", () => {
183
183
  // The page calls `api.start("audit", …)`, so a rename here is a runtime 400
184
184
  // rather than a compile error. This is what pins it.
185
- expect(Object.keys(agentDef.workflows ?? {})).toEqual(["audit"]);
185
+ // `toContain` rather than an exact key list: adding a second workflow is an
186
+ // invited edit and must not redden a test the author did not write. The
187
+ // NAME is still pinned, deliberately — the page starts a run by this
188
+ // string, so renaming the key is a runtime 400 rather than a compile
189
+ // error, and this pin is the only thing that says so. Rename it here and
190
+ // in `client.tsx` together.
191
+ expect(Object.keys(agentDef.workflows ?? {})).toContain("audit");
192
+ expect(agentDef.workflows?.audit).toBe(audit);
186
193
  });
187
194
 
188
- test("declares no providers and exactly the one credential its steps read", () => {
195
+ test("declares the credential its steps read, so a deploy checks for it", () => {
189
196
  // A workflow app has no session, so nothing else in its config could name one
190
197
  // — and one AssemblyAI key covers transcription, the model and the voice.
191
198
  // Note what is NOT here: ffmpeg. `requiredEnv` checks the environment, and a
192
199
  // binary on `PATH` is not an environment variable.
193
- expect(agentDef.requiredEnv).toEqual(["ASSEMBLYAI_API_KEY"]);
200
+ //
201
+ // `toContain` rather than an exact list: a step of your own that reads a
202
+ // second credential belongs in `requiredEnv` beside this one, and declaring
203
+ // it must not fail this test.
204
+ expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
194
205
  });
195
206
 
196
207
  test("takes the recording as an UPLOAD, which is what makes the form a file picker", () => {
@@ -505,7 +516,7 @@ describe("planning where to cut", () => {
505
516
  test("the last segment never addresses a byte the file does not have", () => {
506
517
  // The twelve-byte bug this API shape exists to prevent: planning from a
507
518
  // duration rounded to whole milliseconds put `endByte` at 640,512 for a
508
- // 640,500-byte file. `readUpload` clamps a window to the stored size, so
519
+ // 640,500-byte file. `stepReadUpload` clamps a window to the stored size, so
509
520
  // nothing threw — the plan was simply describing audio that does not exist.
510
521
  // Found by running the real argv against a real ffmpeg, which is the only
511
522
  // place a twelve-byte error was ever going to surface.
@@ -856,14 +867,14 @@ describe("the body's step policy", () => {
856
867
  * only reads `ctx.steps` afterwards, so it is built once.
857
868
  */
858
869
  const walkedCtx = () =>
859
- createWorkflowCtx({
870
+ createWorkflowContext({
860
871
  runSteps: false,
861
872
  // The body reads the clock at both ends with `ctx.now()`, which the engine
862
873
  // journals under `now!0` and `now!1`. A PRODUCER here rather than a fixed
863
874
  // number, so the two reaches differ and `elapsedMs` is assertable — based
864
- // on `WORKFLOW_CTX_NOW`, the instant the fake otherwise freezes at, rather
875
+ // on `WORKFLOW_CONTEXT_NOW`, the instant the fake otherwise freezes at, rather
865
876
  // than on a second arbitrary epoch nobody can relate to the first.
866
- now: clockReads(WORKFLOW_CTX_NOW, WORKFLOW_CTX_NOW + 3000),
877
+ now: clockReads(WORKFLOW_CONTEXT_NOW, WORKFLOW_CONTEXT_NOW + 3000),
867
878
  results: {
868
879
  ingestRecording: {
869
880
  audio: "upl_pcm",
@@ -926,7 +937,7 @@ describe("the body's step policy", () => {
926
937
  /**
927
938
  * `auditFlow` on the real replay engine — as far as this tier can take it.
928
939
  *
929
- * The block above drives the body through `createWorkflowCtx` with
940
+ * The block above drives the body through `createWorkflowContext` with
930
941
  * `runSteps: false` and a journaled result per step, which is what makes it
931
942
  * affordable: `ingestRecording` shells out to the ffmpeg toolchain, which this
932
943
  * tier cannot usefully feed — the `ingestRecording` and `narrate` specs above
@@ -959,6 +970,11 @@ describe("the run is DURABLE, as far as ffmpeg allows", () => {
959
970
  });
960
971
  installStubReporter();
961
972
  vi.stubEnv("ASSEMBLYAI_API_KEY", "test-key");
973
+ // Neither binary is this block's subject (see its doc), and a real
974
+ // conversion here would make a developer with ffmpeg installed run a
975
+ // slower, different test from CI, which has none.
976
+ vi.stubEnv("AAI_FFMPEG_PATH", "/nonexistent/ffmpeg");
977
+ vi.stubEnv("AAI_FFPROBE_PATH", "/nonexistent/ffprobe");
962
978
  });
963
979
 
964
980
  test("a FatalError in the first step fails the run on ONE attempt, not six", async () => {
@@ -20,7 +20,7 @@
20
20
  * - `transcription-workflow` owns the FAN-OUT — why the sync transcription
21
21
  * endpoint's 120-second cap forces one, and how a run survives dying on segment
22
22
  * 27 of 60.
23
- * - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `writeUpload`, and
23
+ * - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `stepWriteUpload`, and
24
24
  * why a page needs `api.download` rather than a URL.
25
25
  *
26
26
  * **What this template adds is a decoder**, and everything downstream changes
@@ -153,7 +153,7 @@ export const audit: WorkflowDef<typeof auditInput, CallAudit> = workflow({
153
153
  input: auditInput,
154
154
  // The one line that makes the form take a file: `<WorkflowFields>` renders a
155
155
  // picker for this property, `useWorkflowSubmit` stores the chosen file, and the
156
- // ingest step reads it back with `readUpload`.
156
+ // ingest step reads it back with `stepReadUpload`.
157
157
  uploads: ["recording"],
158
158
  run: auditFlow,
159
159
  });
@@ -30,15 +30,16 @@
30
30
  * cutting and auditing. On this template that is the most expensive orphaning
31
31
  * in `templates/`: the recording is already stored, so the work is paid for, and
32
32
  * a page with no handle on it invites somebody to upload a 700 MB call a second
33
- * time and run the whole pipeline again. `key` is the handle that survives a
34
- * reload and `recover: true` is what reads it back.
33
+ * time and run the whole pipeline again. The handle that survives a reload is a
34
+ * correlation KEY, and this desk passes none: `useWorkflowSubmit` mints one,
35
+ * records every run under it, and asks for that key's newest run as it mounts.
35
36
  *
36
- * The upload half of a reload is already the SDK's: `useWorkflowSubmit`
37
- * remembers the id it minted, so picking the same file again sends only the
38
- * windows that did not land. What it needs from the page is the same LIFETIME
39
- * on both halves, which is the first reason this desk takes `useRunKey()`'s
40
- * default `sessionStorage`, the same store the upload recall uses. A handle
41
- * that outlived it would promise a return the other half cannot keep.
37
+ * The upload half of a reload is the same hook's: it remembers the id it
38
+ * minted, so picking the same file again sends only the windows that did not
39
+ * land. Both halves therefore have the same LIFETIME `sessionStorage`, one
40
+ * store which is the first reason this desk wants the default rather than a
41
+ * key of its own. A handle that outlived it would promise a return the other
42
+ * half cannot keep.
42
43
  *
43
44
  * ## Why the artifact being shareable does NOT make the key shareable
44
45
  *
@@ -61,7 +62,7 @@
61
62
  * needs none of it — the page renders the audit and offers `Download audit.mp3`,
62
63
  * so a person sends the file and the findings deliberately, to exactly who they
63
64
  * meant. A shareable key would trade a deliberate send for an accidental one, on
64
- * the most sensitive input any template here accepts. It stays in
65
+ * the most sensitive input any template here accepts. The default key stays in
65
66
  * `sessionStorage`, which covers the reload this section is about and dies with
66
67
  * the tab.
67
68
  *
@@ -75,18 +76,18 @@ import "@alexkroman1/aai-ui/styles.css";
75
76
  // already declares.
76
77
  import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
77
78
  import {
79
+ BulletList,
78
80
  createWorkflowApi,
81
+ Facts,
79
82
  Form,
80
- page,
83
+ mountPage,
81
84
  SubmitButton,
82
85
  UploadProgressBar,
83
86
  useDownloadUrl,
84
- useRunKey,
85
87
  useWorkflowSubmit,
86
88
  WorkflowFields,
87
89
  WorkflowProgress,
88
90
  } from "@alexkroman1/aai-ui";
89
- import { useState } from "react";
90
91
  import type { audit } from "./agent.ts";
91
92
 
92
93
  /**
@@ -130,36 +131,13 @@ function Stat({ label, value }: { label: string; value: string }) {
130
131
  );
131
132
  }
132
133
 
133
- /** A list that renders nothing rather than an empty box — see `risks` in the schema. */
134
- function Findings({ title, items }: { title: string; items: string[] }) {
135
- if (items.length === 0) return null;
136
- return (
137
- <section className="flex flex-col gap-1">
138
- <h3 className="text-sm font-medium opacity-70">{title}</h3>
139
- <ul className="flex list-disc flex-col gap-1 pl-5">
140
- {items.map((item) => (
141
- <li key={item}>{item}</li>
142
- ))}
143
- </ul>
144
- </section>
145
- );
146
- }
147
-
148
134
  export function App() {
149
- // This tab's handle on its own audits, in the store the upload recall uses —
150
- // see the module doc for why a `?key=` is the wrong trade on this desk.
151
- const key = useRunKey();
152
- // Did THIS load start the run? A reload cannot have, and that is the only way
153
- // the page can tell "auditing what you just uploaded" from "picking up where
154
- // you left off" — the hook reports the run, not who asked for it.
155
- const [startedHere, setStartedHere] = useState(false);
156
135
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
157
- // `run.output` instead of `unknown`. Neither half of the recovery is useful
158
- // alone: without `key` there is nothing to find the run by, and without
159
- // `recover` the key is only ever written.
160
- const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
161
- typeof audit
162
- >(WORKFLOW, { api, key, recover: true });
136
+ // `run.output` instead of `unknown`. The reload — both halves of it is the
137
+ // hook's own doing; see the module doc for why the key it mints is the one
138
+ // this desk wants.
139
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
140
+ useWorkflowSubmit<typeof audit>(WORKFLOW, { api });
163
141
  const output = run?.status === "completed" ? run.output : undefined;
164
142
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
165
143
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -176,14 +154,7 @@ export function App() {
176
154
  </p>
177
155
  </header>
178
156
 
179
- <Form
180
- onSubmit={(values) => {
181
- setStartedHere(true);
182
- return submitForm(values);
183
- }}
184
- error={error}
185
- className="flex flex-col gap-4"
186
- >
157
+ <Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
187
158
  {/* Every control, from the workflow's own input schema. See the module doc. */}
188
159
  <WorkflowFields workflow={WORKFLOW} />
189
160
  <SubmitButton pending={pending} pendingLabel="Auditing…">
@@ -203,20 +174,26 @@ export function App() {
203
174
  for `<WorkflowProgress>` to read. */}
204
175
  <UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
205
176
 
206
- {/* What the run itself says, from `report()` in the workflow's steps — which
177
+ {/* What the run itself says, from `stepReport()` in the workflow's steps — which
207
178
  for this template is the ffmpeg narration: what the file was, what it
208
179
  measured, how many pauses were found. */}
209
180
  <WorkflowProgress runId={run?.runId} api={api} />
210
181
 
211
- {run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
182
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error: this
183
+ is the outcome the reader waited minutes for. */}
184
+ {run?.status === "failed" && (
185
+ <p role="alert" className="text-red-600">
186
+ That one failed: {run.error}
187
+ </p>
188
+ )}
212
189
 
213
190
  {output !== undefined && (
214
191
  <article className="flex flex-col gap-6">
215
192
  <div className="flex flex-col gap-1">
216
193
  <h2 className="text-xl">{output.headline}</h2>
217
- <p className="text-sm opacity-70">
218
- {output.source} · {formatDuration(output.durationMs)} · {output.words} words
219
- </p>
194
+ <Facts
195
+ items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
196
+ />
220
197
  </div>
221
198
 
222
199
  {/* What the pipeline did, which is this template's subject. Rendered rather
@@ -234,8 +211,11 @@ export function App() {
234
211
  <Stat label="Run time" value={formatDuration(output.elapsedMs)} />
235
212
  </dl>
236
213
 
237
- <Findings title="Risks" items={output.risks} />
238
- <Findings title="Actions" items={output.actions} />
214
+ {/* Either list can come back empty — see `risks` in the schema — and
215
+ `<BulletList>` renders nothing at all when it does, heading
216
+ included, rather than a stray heading over an empty box. */}
217
+ <BulletList title="Risks" items={output.risks} />
218
+ <BulletList title="Actions" items={output.actions} />
239
219
 
240
220
  <section className="flex flex-col gap-2">
241
221
  <h3 className="text-sm font-medium opacity-70">
@@ -244,7 +224,9 @@ export function App() {
244
224
  </h3>
245
225
  {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
246
226
  {audio.error !== undefined && (
247
- <p className="text-red-600">Could not load the audio: {audio.error}</p>
227
+ <p role="alert" className="text-red-600">
228
+ Could not load the audio: {audio.error}
229
+ </p>
248
230
  )}
249
231
  {audio.url !== undefined && (
250
232
  <>
@@ -254,7 +236,7 @@ export function App() {
254
236
  track would carry. `spoken-summary` serves a one-cue WebVTT
255
237
  data URL instead — worth reading for how, if a real track is
256
238
  what a page needs. */}
257
- <audio controls src={audio.url} className="w-full" />
239
+ <audio aria-label="Audit read aloud" controls src={audio.url} className="w-full" />
258
240
  {/* `download` works on an object URL because the bytes are already in
259
241
  the tab; it is the href that could not carry the agent's bearer,
260
242
  not the attribute. */}
@@ -276,4 +258,4 @@ export function App() {
276
258
  );
277
259
  }
278
260
 
279
- page({ name: "Call Audit", component: App });
261
+ mountPage({ name: "Call Audit", component: App });