@alexkroman1/aai-cli 13.1.0 → 13.3.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/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-Clbq3a4h.mjs} +2 -2
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DEkjO1d8.mjs} +3 -3
- package/dist/{_config-CmJOFsAP.mjs → _config-ZY2euhdh.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-C13xuGB1.mjs} +13 -13
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-Ci4QGjp8.mjs} +58 -10
- package/dist/{_output-CKkmWs7i.mjs → _output-DbJt9gsl.mjs} +34 -3
- package/dist/_output.d.ts +17 -1
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-B2laWPdU.mjs} +1 -1
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-BV4_CrP3.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-DlKpj91X.mjs} +3 -3
- package/dist/{_studio-B1waXMuR.mjs → _studio-DTi3EdXQ.mjs} +3 -3
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-V4l5sZJy.mjs} +2 -2
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-sV4N5F93.mjs} +3 -3
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-Dhd058D9.mjs} +1 -1
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-C9l8uC2R.mjs} +1 -1
- package/dist/{build-BLlLOrng.mjs → build-CWyFGe-W.mjs} +11 -8
- package/dist/cli.d.ts +2 -1
- package/dist/cli.mjs +120 -46
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-CT-_26c5.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-RqlQZ6Y_.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-B7f3CK7v.mjs} +13 -13
- package/dist/{dev-BDykFvcc.mjs → dev-DamXieW_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-h3DTVfRw.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-D-vFrV8h.mjs} +7 -7
- package/dist/{login-DeUETobb.mjs → login-CDiemNmC.mjs} +5 -5
- package/dist/{logs-hWnJ66Bl.mjs → logs-DGwPNCzB.mjs} +4 -4
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/package.json +6 -5
- package/dist/secret-V1w7y9-j.mjs +162 -0
- package/dist/secret.d.ts +41 -8
- package/dist/{studio-BxAS_FQQ.mjs → studio-BpMCfUNx.mjs} +9 -9
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.eval.test.ts +14 -11
- package/dist/templates/call-audit/agent.test.ts +34 -7
- package/dist/templates/call-audit/client.tsx +14 -17
- package/dist/templates/call-audit/workflows/ingest.ts +10 -1
- package/dist/templates/code-interpreter/agent.eval.test.ts +27 -17
- package/dist/templates/dispatch-center/agent.eval.test.ts +18 -24
- package/dist/templates/embedded-assets/agent.eval.test.ts +3 -3
- package/dist/templates/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +38 -15
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/link-digest/agent.eval.test.ts +24 -15
- package/dist/templates/link-digest/agent.test.ts +7 -1
- package/dist/templates/link-digest/client.tsx +17 -21
- package/dist/templates/math-buddy/agent.eval.test.ts +28 -17
- package/dist/templates/night-owl/agent.eval.test.ts +30 -15
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/personal-finance/agent.eval.test.ts +27 -17
- package/dist/templates/pipeline-simple/agent.test.ts +31 -14
- package/dist/templates/pizza-ordering/agent.eval.test.ts +11 -6
- package/dist/templates/plan-and-execute/agent.eval.test.ts +14 -7
- package/dist/templates/podcast-digest/agent.test.ts +13 -3
- package/dist/templates/podcast-digest/client.tsx +12 -15
- package/dist/templates/recap-workflow/agent.eval.test.ts +49 -20
- package/dist/templates/recap-workflow/agent.test.ts +22 -9
- package/dist/templates/redline/agent.eval.test.ts +32 -24
- package/dist/templates/redline/agent.test.ts +7 -1
- package/dist/templates/redline/client.tsx +12 -15
- package/dist/templates/research-workflow/agent.eval.test.ts +32 -22
- package/dist/templates/research-workflow/agent.test.ts +7 -1
- package/dist/templates/retail/agent.eval.test.ts +18 -34
- package/dist/templates/retail/registry.test.ts +99 -58
- package/dist/templates/simple/agent.test.ts +39 -6
- package/dist/templates/spoken-summary/agent.eval.test.ts +25 -16
- package/dist/templates/spoken-summary/agent.test.ts +21 -7
- package/dist/templates/spoken-summary/client.tsx +14 -18
- package/dist/templates/support-line/agent.eval.test.ts +23 -26
- package/dist/templates/transcription-workflow/agent.test.ts +26 -8
- package/dist/templates/transcription-workflow/client.tsx +19 -26
- package/dist/templates/transcription-workflow/workflows/normalize.ts +10 -1
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +5 -2
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +11 -4
- package/dist/templates/travel-concierge/agent.eval.test.ts +37 -56
- package/dist/test-guenJUxi.mjs +255 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-BZyKLkhV.mjs} +5 -5
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
# For production, use `aai secret put <NAME>` to set the same keys on the server.
|
|
5
5
|
# In both cases, access secrets in agent code via ctx.env.MY_KEY.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# --- The one key the default voice pipeline needs --------------------------
|
|
8
|
+
#
|
|
9
|
+
# `aai dev` runs speech-to-text, the LLM gateway and text-to-speech on a single
|
|
10
|
+
# AssemblyAI key, so this is the credential a session cannot start without.
|
|
11
|
+
# Any ONE of these is enough, and the first two need no aai account:
|
|
12
|
+
#
|
|
13
|
+
# 1. Paste your key after the `=` below.
|
|
14
|
+
# 2. Or export it in your shell: export ASSEMBLYAI_API_KEY=<your key>
|
|
15
|
+
# 3. Or run `aai login`, and `aai dev` will use your account's key.
|
|
16
|
+
#
|
|
17
|
+
# Get a key at https://www.assemblyai.com/dashboard.
|
|
18
|
+
#
|
|
19
|
+
# Leaving it blank is safe: a declared-but-empty value is dropped rather than
|
|
20
|
+
# handed to a provider, so options 2 and 3 still work with this line as it is.
|
|
21
|
+
ASSEMBLYAI_API_KEY=
|
|
22
|
+
|
|
23
|
+
# Add any other secrets your agent needs below:
|
|
8
24
|
|
|
9
25
|
# Required only if your agent enables the `web_search` builtin
|
|
10
26
|
# (free key: https://brave.com/search/api/):
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
"prestart": "aai build --skip-tests",
|
|
10
10
|
"start": "node server.mjs",
|
|
11
11
|
"build": "aai build",
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "vitest run --exclude \"**/*.eval.test.*\"",
|
|
13
|
+
"test:agent": "aai test",
|
|
13
14
|
"eval": "aai eval",
|
|
14
15
|
"publish:agent": "aai publish"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
|
-
"@alexkroman1/aai": "^13.
|
|
18
|
-
"@alexkroman1/aai-runtime": "^13.
|
|
19
|
-
"@alexkroman1/aai-ui": "^13.
|
|
18
|
+
"@alexkroman1/aai": "^13.3.0",
|
|
19
|
+
"@alexkroman1/aai-runtime": "^13.3.0",
|
|
20
|
+
"@alexkroman1/aai-ui": "^13.3.0",
|
|
20
21
|
"react": "^19.2.8",
|
|
21
22
|
"react-dom": "^19.2.8",
|
|
22
23
|
"tailwindcss": "^4.0.0",
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
"zod": "^4.5.4"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@alexkroman1/aai-cli": "^13.
|
|
28
|
+
"@alexkroman1/aai-cli": "^13.3.0",
|
|
28
29
|
"@tailwindcss/vite": "^4.3.3",
|
|
29
30
|
"@types/node": "^26.4.1",
|
|
30
31
|
"@types/react": "^19.2.18",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as ok, n as fail, t as CliError } from "./_output-DbJt9gsl.mjs";
|
|
3
|
+
import { n as log, s as unwrapCancel } from "./_ui-DCt4qZrk.mjs";
|
|
4
|
+
import { f as checkedResponse, p as isStringArray } from "./_studio-DTi3EdXQ.mjs";
|
|
5
|
+
import { t as secretRequest } from "./_slug-api-DlKpj91X.mjs";
|
|
6
|
+
import { isRecord } 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} secret${vars.length === 1 ? "" : "s"}:`);
|
|
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,11 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import { type CommandResult, type OutputMode } from "./_output.ts";
|
|
2
3
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
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
|
|
7
|
-
/**
|
|
8
|
-
|
|
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;
|
|
16
|
+
};
|
|
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>;
|
|
9
41
|
type SecretPutData = {
|
|
10
42
|
name: string;
|
|
11
43
|
};
|
|
@@ -16,8 +48,9 @@ type SecretListData = {
|
|
|
16
48
|
secrets: string[];
|
|
17
49
|
};
|
|
18
50
|
/**
|
|
19
|
-
* Execute secret put. If `value` is provided, use it directly (
|
|
20
|
-
* If not
|
|
51
|
+
* Execute secret put. If `value` is provided, use it directly (the stdin
|
|
52
|
+
* path). If not, prompt for it — masked — which is what
|
|
53
|
+
* {@link resolveSecretValue} returning `undefined` asks for.
|
|
21
54
|
*/
|
|
22
55
|
export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretPutData>>;
|
|
23
56
|
export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretDeleteData>>;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ok, t as CliError } from "./_output-
|
|
3
|
-
import { n as log, t as fmtUrl } from "./_ui-
|
|
4
|
-
import { s as updateProjectConfig } from "./_config-
|
|
5
|
-
import { t as resolveServerEnv } from "./_server-common-
|
|
6
|
-
import { a as resolveDeployTarget } from "./_agent-
|
|
7
|
-
import { layerScaffold } from "./_templates-
|
|
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-
|
|
2
|
+
import { a as ok, t as CliError } from "./_output-DbJt9gsl.mjs";
|
|
3
|
+
import { n as log, t as fmtUrl } from "./_ui-DCt4qZrk.mjs";
|
|
4
|
+
import { s as updateProjectConfig } from "./_config-ZY2euhdh.mjs";
|
|
5
|
+
import { t as resolveServerEnv } from "./_server-common-BV4_CrP3.mjs";
|
|
6
|
+
import { a as resolveDeployTarget } from "./_agent-Clbq3a4h.mjs";
|
|
7
|
+
import { layerScaffold } from "./_templates-V4l5sZJy.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-DTi3EdXQ.mjs";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
12
12
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
13
13
|
import { isPathInside } from "@alexkroman1/aai-runtime/internal";
|
|
14
|
-
//#region studio.ts
|
|
14
|
+
//#region src/studio.ts
|
|
15
15
|
/**
|
|
16
16
|
* The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
|
|
17
17
|
* `aai publish`.
|
|
@@ -164,7 +164,7 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
|
|
|
164
164
|
}
|
|
165
165
|
async function executePublish(opts) {
|
|
166
166
|
if (!opts.skipTypecheck) {
|
|
167
|
-
const { assertTypechecks } = await import("./_typecheck-gate-
|
|
167
|
+
const { assertTypechecks } = await import("./_typecheck-gate-sV4N5F93.mjs");
|
|
168
168
|
await assertTypechecks(opts.cwd);
|
|
169
169
|
}
|
|
170
170
|
const pushed = await pushProject(opts);
|
|
@@ -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
|
-
|
|
67
|
-
//
|
|
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).
|
|
73
|
+
expect(factChecker.builtinTools).toContain("web_search");
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
test("gives the checker a tighter budget than the researcher", () => {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
// the tier that really resumes a run.
|
|
53
53
|
import { spawnSync } from "node:child_process";
|
|
54
54
|
import { encodeWav } from "@alexkroman1/aai/step";
|
|
55
|
+
import { stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
55
56
|
import { installStubTranscribe, installStubUploads } from "@alexkroman1/aai/testing/vitest";
|
|
56
57
|
import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
57
58
|
import { describe, expect, test } from "vitest";
|
|
@@ -211,19 +212,21 @@ function publish(bytes: Uint8Array, name: string, type: string) {
|
|
|
211
212
|
* Answer the sync transcription endpoint and the model in memory.
|
|
212
213
|
*
|
|
213
214
|
* ONE fake, because publishing a `stepFetch` REPLACES — a flow that transcribes
|
|
214
|
-
* AND calls a model cannot install two, which is what `otherwise` is for.
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* because the fake and the step agree on a typo.
|
|
215
|
+
* AND calls a model cannot install two, which is what `otherwise` is for. BOTH
|
|
216
|
+
* halves are the SDK's own fakes rather than this file's hand-typed wire, and
|
|
217
|
+
* for the same reason: each routes off the SDK's own endpoint constant, so a
|
|
218
|
+
* case cannot pass because the fake and the step agree on a typo. The gateway
|
|
219
|
+
* envelope is the half where that matters most — it is a WIRE shape, so getting
|
|
220
|
+
* a field wrong does not fail: `stepGenerate` reads no content, reports an empty
|
|
221
|
+
* completion, and the case blames the desk.
|
|
222
|
+
*
|
|
223
|
+
* `route` answers `undefined` for anything that is not a completion request, so
|
|
224
|
+
* it drops straight into `otherwise` and the transcription legs still reach the
|
|
225
|
+
* fake below it.
|
|
218
226
|
*/
|
|
219
227
|
function scriptProvider(text: readonly string[]) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
otherwise: (request) =>
|
|
223
|
-
request.url.includes("/chat/completions")
|
|
224
|
-
? { body: { choices: [{ message: { content: JSON.stringify(REPLY) } }] } }
|
|
225
|
-
: undefined,
|
|
226
|
-
});
|
|
228
|
+
const model = stubGatewayRoute(JSON.stringify(REPLY));
|
|
229
|
+
return installStubTranscribe({ text, otherwise: (request) => model.route(request) });
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
describeWorkflowEvalWithFfmpeg(
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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", () => {
|
|
@@ -928,9 +939,21 @@ describe("the body's step policy", () => {
|
|
|
928
939
|
*
|
|
929
940
|
* The block above drives the body through `createWorkflowCtx` with
|
|
930
941
|
* `runSteps: false` and a journaled result per step, which is what makes it
|
|
931
|
-
* affordable: `ingestRecording`
|
|
932
|
-
*
|
|
933
|
-
* `FatalError` that produces, unconditionally.
|
|
942
|
+
* affordable: `ingestRecording` shells out to the ffmpeg toolchain, which this
|
|
943
|
+
* tier cannot usefully feed — the `ingestRecording` and `narrate` specs above
|
|
944
|
+
* assert the `FatalError` that produces, unconditionally.
|
|
945
|
+
*
|
|
946
|
+
* **The run fails here whether or not ffmpeg is installed, and the two failures
|
|
947
|
+
* do not say the same thing** — which is what the assertion below has to be
|
|
948
|
+
* written against. With no binary on `PATH` the step reports the missing
|
|
949
|
+
* toolchain as an instruction (the template's own doc promises that rather than
|
|
950
|
+
* `spawn ffmpeg ENOENT`); WITH one, the step gets a step further and `ffprobe`
|
|
951
|
+
* rejects the 2 KB stub above with `ffprobe exited with code 1`. This docblock
|
|
952
|
+
* used to state "this repo's test environment has none" as a premise, and the
|
|
953
|
+
* spec below matched `/ffmpeg/i` on the strength of it — so the test passed only
|
|
954
|
+
* on a machine where ffmpeg was ABSENT, and a developer with Homebrew's ffmpeg
|
|
955
|
+
* saw a red suite on a clean checkout of `main`. CI has no ffmpeg, so nothing
|
|
956
|
+
* caught it.
|
|
934
957
|
*
|
|
935
958
|
* So a whole run of this desk is not reachable here and this file does not
|
|
936
959
|
* pretend otherwise; `aai-cli`'s `dev-workflow.scenario.test.ts` is the tier
|
|
@@ -953,7 +976,11 @@ describe("the run is DURABLE, as far as ffmpeg allows", () => {
|
|
|
953
976
|
const run = await runWorkflow(audit, { recording: UPLOAD_ID }, { name: "audit" });
|
|
954
977
|
|
|
955
978
|
expect(run.status).toBe("failed");
|
|
956
|
-
|
|
979
|
+
// Either member of the toolchain, for the reason this block's doc gives: the
|
|
980
|
+
// step names `ffmpeg` when the binary is missing and `ffprobe` when it is
|
|
981
|
+
// present, and this spec is about neither. What it is about is the two
|
|
982
|
+
// assertions below — the ENGINE honouring `FatalError`.
|
|
983
|
+
expect(run.error).toMatch(/ff(mpeg|probe)/i);
|
|
957
984
|
const ingest = run.steps.find((step) => step.name === "ingestRecording");
|
|
958
985
|
// The whole point of `FatalError`: `maxAttempts: 6` is the budget this call
|
|
959
986
|
// site asks for, and a failure that cannot change must not spend it. A step
|
|
@@ -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.
|
|
34
|
-
*
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
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
|
*
|
|
@@ -81,7 +82,6 @@ import {
|
|
|
81
82
|
SubmitButton,
|
|
82
83
|
UploadProgressBar,
|
|
83
84
|
useDownloadUrl,
|
|
84
|
-
useRunKey,
|
|
85
85
|
useWorkflowSubmit,
|
|
86
86
|
WorkflowFields,
|
|
87
87
|
WorkflowProgress,
|
|
@@ -146,20 +146,17 @@ function Findings({ title, items }: { title: string; items: string[] }) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
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
149
|
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
153
150
|
// the page can tell "auditing what you just uploaded" from "picking up where
|
|
154
151
|
// you left off" — the hook reports the run, not who asked for it.
|
|
155
152
|
const [startedHere, setStartedHere] = useState(false);
|
|
156
153
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
157
|
-
// `run.output` instead of `unknown`.
|
|
158
|
-
//
|
|
159
|
-
//
|
|
154
|
+
// `run.output` instead of `unknown`. The reload — both halves of it — is the
|
|
155
|
+
// hook's own doing; see the module doc for why the key it mints is the one
|
|
156
|
+
// this desk wants.
|
|
160
157
|
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
|
|
161
158
|
typeof audit
|
|
162
|
-
>(WORKFLOW, { api
|
|
159
|
+
>(WORKFLOW, { api });
|
|
163
160
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
164
161
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
165
162
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -143,7 +143,16 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
|
143
143
|
const normalized = join(dir, "audio.pcm");
|
|
144
144
|
const silenceLog = join(dir, "silence.txt");
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// NO `size`, though `stored.size` is right there — and that is the whole
|
|
147
|
+
// difference between this copy being one window at a time and being
|
|
148
|
+
// `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
|
|
149
|
+
// completeness myself", which is what a body polling a still-arriving
|
|
150
|
+
// upload needs and is the opposite of what happened above: this step has
|
|
151
|
+
// already called `requireCompleteUpload`, so the file IS whole and the
|
|
152
|
+
// windows may land in any order. Omitting it lets `readUploadToFile`
|
|
153
|
+
// establish that for itself and fan out. The cost is one metadata round
|
|
154
|
+
// trip, against the dozens of window reads it overlaps.
|
|
155
|
+
await readUploadToFile(uploadId, source);
|
|
147
156
|
|
|
148
157
|
// What it WAS, for the progress log and the page. Worth one ffprobe: "41
|
|
149
158
|
// minutes of aac" explains the shape of the run, where "the recording" leaves
|
|
@@ -24,27 +24,37 @@
|
|
|
24
24
|
// Coda reached for code, and what the code came back with.
|
|
25
25
|
|
|
26
26
|
import agentDef from "virtual:aai/agent";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
createVmRunCode,
|
|
29
|
+
type EvalTurn,
|
|
30
|
+
toolArgsIn,
|
|
31
|
+
toolNames,
|
|
32
|
+
toolResultsIn,
|
|
33
|
+
} from "@alexkroman1/aai-runtime/eval";
|
|
28
34
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
29
35
|
import { expect } from "vitest";
|
|
36
|
+
import { z } from "zod";
|
|
30
37
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The code every `run_code` call in this turn carried, joined.
|
|
40
|
+
*
|
|
41
|
+
* Read through `toolArgsIn` WITH a schema, which is what that reader takes one
|
|
42
|
+
* for: `args` is `Record<string, unknown>` on the wire — the model wrote it and
|
|
43
|
+
* nothing validated it — so the `String(c.args.code ?? "")` this replaced turned
|
|
44
|
+
* an argument Coda renamed, or never sent, into `""`, and every claim below about
|
|
45
|
+
* the code she wrote would have been a claim about an empty string. A `code`
|
|
46
|
+
* that stops arriving FAILS here, naming the field.
|
|
47
|
+
*/
|
|
48
|
+
const RunCodeArgs = z.object({ code: z.string() });
|
|
49
|
+
const codeIn = (turn: EvalTurn) =>
|
|
50
|
+
toolArgsIn(turn.toolCalls, "run_code", RunCodeArgs)
|
|
51
|
+
.map((args) => args.code)
|
|
36
52
|
.join("\n");
|
|
37
53
|
|
|
38
54
|
/**
|
|
39
|
-
* A `run_code` executor, so these cases can assert the ANSWER
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* boundary, and off-platform there is none — so a case could assert the CALL and
|
|
43
|
-
* the code it carried, and never what the code came back with.
|
|
44
|
-
* `createVmRunCode()` is a `node:vm` context with a capturing `console.log`,
|
|
45
|
-
* which is enough here: what runs is arithmetic, not a program. It is NOT a
|
|
46
|
-
* sandbox and does not pretend to be one; a deployed agent still gets the
|
|
47
|
-
* refusal.
|
|
55
|
+
* A `run_code` executor, so these cases can assert the ANSWER and not merely the
|
|
56
|
+
* call — `createVmRunCode`'s own doc carries why the builtin refuses without one
|
|
57
|
+
* and why a `node:vm` context is the right thing to hand it here.
|
|
48
58
|
*/
|
|
49
59
|
const runCode = createVmRunCode();
|
|
50
60
|
|
|
@@ -59,7 +69,7 @@ describeEval(
|
|
|
59
69
|
// The template's CRITICAL RULE, and the whole reason it declares
|
|
60
70
|
// run_code: a model that answers this one directly has regressed, and it
|
|
61
71
|
// is the easiest question in the file to answer wrongly with confidence.
|
|
62
|
-
expect(turn.toolCalls
|
|
72
|
+
expect(toolNames(turn.toolCalls)).toContain("run_code");
|
|
63
73
|
const code = codeIn(turn);
|
|
64
74
|
expect(code).toContain("127");
|
|
65
75
|
expect(code).toContain("849");
|
|
@@ -90,7 +100,7 @@ describeEval(
|
|
|
90
100
|
// The prompt lists this exact question under "you MUST use code for".
|
|
91
101
|
// It is the case a narrower reading of the rule ("code is for maths")
|
|
92
102
|
// silently drops.
|
|
93
|
-
expect(turn.toolCalls
|
|
103
|
+
expect(toolNames(turn.toolCalls)).toContain("run_code");
|
|
94
104
|
expect(codeIn(turn)).toMatch(/Date|2000/);
|
|
95
105
|
// The code RAN rather than being refused — but the ANSWER is
|
|
96
106
|
// deliberately not asserted here, and the reason is worth knowing before
|