@alexkroman1/aai-cli 13.2.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.test.ts +14 -3
- package/dist/templates/call-audit/client.tsx +14 -17
- package/dist/templates/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/link-digest/agent.test.ts +7 -1
- package/dist/templates/link-digest/client.tsx +17 -21
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/pipeline-simple/agent.test.ts +31 -14
- 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.test.ts +22 -9
- package/dist/templates/redline/agent.test.ts +7 -1
- package/dist/templates/redline/client.tsx +12 -15
- package/dist/templates/research-workflow/agent.test.ts +7 -1
- package/dist/templates/retail/registry.test.ts +99 -58
- package/dist/templates/simple/agent.test.ts +39 -6
- package/dist/templates/spoken-summary/agent.test.ts +12 -3
- package/dist/templates/spoken-summary/client.tsx +14 -18
- package/dist/templates/transcription-workflow/agent.test.ts +16 -8
- package/dist/templates/transcription-workflow/client.tsx +19 -26
- 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", () => {
|
|
@@ -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", () => {
|
|
@@ -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
|
|
@@ -30,7 +30,13 @@ describe("embedded-assets template", () => {
|
|
|
30
30
|
// `agent()` takes no `tools` field: a file in `tools/` IS the tool, and
|
|
31
31
|
// nothing imports it. Discovery is what puts it in front of the model, so a
|
|
32
32
|
// template whose tools are never resolved ships a model with no tools.
|
|
33
|
-
|
|
33
|
+
// `arrayContaining` rather than an exact list: adding a file to `tools/` is
|
|
34
|
+
// the edit this template most invites, and an exact list would redden on
|
|
35
|
+
// it. Losing one of these two is still a failure — that is the half worth
|
|
36
|
+
// asserting.
|
|
37
|
+
expect(Object.keys(agentDef.tools ?? {})).toEqual(
|
|
38
|
+
expect.arrayContaining(["list_topics", "search_knowledge"]),
|
|
39
|
+
);
|
|
34
40
|
});
|
|
35
41
|
});
|
|
36
42
|
|
|
@@ -57,10 +57,14 @@ describe("health-assistant template", () => {
|
|
|
57
57
|
test("both tools are discovered from tools/", () => {
|
|
58
58
|
// `agent()` takes no `tools` field: a file in `tools/` IS the tool. A
|
|
59
59
|
// template whose tools are never resolved ships a model with none.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// `arrayContaining` rather than an exact list: a tool you add is the edit
|
|
61
|
+
// this template invites, and it must not redden a test you did not write.
|
|
62
|
+
// Losing one of these two still fails, which is the regression worth
|
|
63
|
+
// catching — discovery silently finding nothing looks exactly like a
|
|
64
|
+
// template with no tools.
|
|
65
|
+
expect(Object.keys(agentDef.tools ?? {})).toEqual(
|
|
66
|
+
expect.arrayContaining(["check_drug_interaction", "medication_lookup"]),
|
|
67
|
+
);
|
|
64
68
|
});
|
|
65
69
|
});
|
|
66
70
|
|
|
@@ -51,7 +51,13 @@ describe("the agent declares itself a workflow app", () => {
|
|
|
51
51
|
test("under the name the page starts a run by", () => {
|
|
52
52
|
// `api.start("digest", …)` in client.tsx names this key. Nothing else
|
|
53
53
|
// records it, so a rename here is a 400 there rather than a compile error.
|
|
54
|
-
|
|
54
|
+
// `toContain` rather than an exact key list: adding a second workflow is an
|
|
55
|
+
// invited edit and must not redden a test the author did not write. The
|
|
56
|
+
// NAME is still pinned, deliberately — the page starts a run by this
|
|
57
|
+
// string, so renaming the key is a runtime 400 rather than a compile
|
|
58
|
+
// error, and this pin is the only thing that says so. Rename it here and
|
|
59
|
+
// in `client.tsx` together.
|
|
60
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("digest");
|
|
55
61
|
expect(agentDef.workflows?.digest).toBe(digest);
|
|
56
62
|
});
|
|
57
63
|
});
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
* page used to promise "the run continues without it" and then had no way back
|
|
20
20
|
* to the run it was promising about.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* A correlation KEY is the handle that survives, and this page writes none of
|
|
23
|
+
* it: `useWorkflowSubmit` mints an opaque per-page key into `sessionStorage`,
|
|
24
|
+
* records every run under it, and asks `find("digest", key)` as it mounts —
|
|
25
|
+
* so a reload lands back on the same headline, the same progress log and the
|
|
26
|
+
* same buttons. Six templates used to write those two options each, which is
|
|
27
|
+
* what made it the default.
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* and follow whatever run comes back, so a reload lands back on the same
|
|
31
|
-
* headline, the same progress log and the same buttons.
|
|
29
|
+
* What the page can still say is which key: `useRunKey({ storage: "local" })`
|
|
30
|
+
* for a run meant to outlive the tab (`podcast-digest`), an ACCOUNT's own id
|
|
31
|
+
* for an app with logins, `recover: false` for a form that must always open
|
|
32
|
+
* empty. `use-run-key.ts` argues what a key may not be — derived from the URL
|
|
33
|
+
* being digested, or carried in a `?key=` parameter.
|
|
32
34
|
*
|
|
33
35
|
* Deployed, this needs the correlation-key index, which is a `DATABASE_URL`
|
|
34
36
|
* away — `agent.ts` says what happens without one (the runs are still durable;
|
|
@@ -55,14 +57,14 @@
|
|
|
55
57
|
* Progress also REPLAYS — chunks are retained with the run — so a reload mid-run
|
|
56
58
|
* catches up rather than starting from whatever arrives next. That only pays off
|
|
57
59
|
* because the reload can name its run again: `<WorkflowProgress runId>` is handed
|
|
58
|
-
* `run?.runId`, so before
|
|
60
|
+
* `run?.runId`, so before the recovery a refresh replayed a log for nobody.
|
|
59
61
|
* `lines={1}` is
|
|
60
62
|
* what narrows it to the newest line, because on a page this small that is the
|
|
61
63
|
* whole of what a status wants; `transcription-workflow` renders the full log,
|
|
62
64
|
* where a fan-out makes the history worth seeing.
|
|
63
65
|
*/
|
|
64
66
|
|
|
65
|
-
import { page,
|
|
67
|
+
import { page, useWorkflowSubmit, WorkflowProgress } from "@alexkroman1/aai-ui";
|
|
66
68
|
import "@alexkroman1/aai-ui/styles.css";
|
|
67
69
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
68
70
|
// nothing — and it is what stops this file restating a shape `workflows/
|
|
@@ -90,9 +92,6 @@ function pendingNote(startedHere: boolean, found: boolean): string {
|
|
|
90
92
|
|
|
91
93
|
export function App() {
|
|
92
94
|
const [url, setUrl] = useState("");
|
|
93
|
-
// This tab's handle on its own runs — minted once and remembered, which is
|
|
94
|
-
// what a later load produces to find the run again.
|
|
95
|
-
const key = useRunKey();
|
|
96
95
|
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
97
96
|
// the page can tell "working on what you just submitted" from "picking up
|
|
98
97
|
// where you left off" — the hook reports the run, not who asked for it.
|
|
@@ -102,13 +101,10 @@ export function App() {
|
|
|
102
101
|
// rejected input, which is better copy than anything this page could write, and
|
|
103
102
|
// `wake` is bound to whatever run the hook is following — the whole reason this
|
|
104
103
|
// page no longer holds a `createWorkflowApi()` of its own.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
key,
|
|
110
|
-
recover: true,
|
|
111
|
-
});
|
|
104
|
+
// No `key` and no `recover`: this tab's handle on its own runs is minted and
|
|
105
|
+
// remembered by the hook, and read back as it mounts. See the module doc for
|
|
106
|
+
// what a page says when it wants a different one.
|
|
107
|
+
const { submit, run, pending, error, wake } = useWorkflowSubmit<typeof digest>("digest");
|
|
112
108
|
|
|
113
109
|
// `submit()` resolves as soon as the run exists — deliberately not when it
|
|
114
110
|
// finishes. That is the whole mechanism: the digest sleeps for a while, and
|
|
@@ -32,7 +32,12 @@ describe("night-owl template", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test("recommend is discovered from tools/", () => {
|
|
35
|
-
|
|
35
|
+
// `toContain` rather than an exact list: a file in `tools/` IS a tool, so
|
|
36
|
+
// adding one is the edit this template most invites, and an exact list
|
|
37
|
+
// would make that edit fail a test the author never wrote. What has to hold
|
|
38
|
+
// is that discovery ran at all — a template whose `tools/` is never
|
|
39
|
+
// resolved ships a model with no tools.
|
|
40
|
+
expect(Object.keys(agentDef.tools ?? {})).toContain("recommend");
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
test("the projection an untouched session pushes is an empty log", () => {
|
|
@@ -13,25 +13,42 @@ describe("pipeline-simple template", () => {
|
|
|
13
13
|
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
test("
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
expect(agentDef.
|
|
22
|
-
expect(agentDef.
|
|
16
|
+
test("exports an agent the platform can name", () => {
|
|
17
|
+
// Not the literal. `aai init <project>` scaffolds this file verbatim, so a
|
|
18
|
+
// pinned name is a test about the TEMPLATE inside somebody else's project —
|
|
19
|
+
// and renaming the agent is the first thing they will do. What has to hold
|
|
20
|
+
// is that there is a name and the conversion carries it through.
|
|
21
|
+
expect(agentDef.name).toBeTruthy();
|
|
22
|
+
expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("
|
|
26
|
-
|
|
25
|
+
test("declares at least one stage itself, which is what makes it a pipeline", () => {
|
|
26
|
+
// WHICH stage is deliberately not asserted: `agent.ts` invites you to move
|
|
27
|
+
// the declaration ("Declare only the stage you want elsewhere"), so naming
|
|
28
|
+
// one here would redden on the edit the comment above it asks for.
|
|
29
|
+
const declared = (["stt", "llm", "tts"] as const).filter(
|
|
30
|
+
(stage) => agentDef[stage] !== undefined,
|
|
31
|
+
);
|
|
32
|
+
expect(declared.length).toBeGreaterThan(0);
|
|
27
33
|
});
|
|
28
34
|
|
|
29
|
-
test("
|
|
35
|
+
test("a stage you declare survives, and every stage you leave unset defaults", () => {
|
|
36
|
+
// The template's whole subject, stated so it survives a swap: the config a
|
|
37
|
+
// deploy carries agrees with the def wherever the def has an opinion, and
|
|
38
|
+
// fills the AssemblyAI default wherever it does not.
|
|
30
39
|
const config = toAgentConfig(agentDef);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if (config.mode !== "pipeline") {
|
|
41
|
+
// Switched the def to `s2s`? Then there is no cascade to fill — S2S
|
|
42
|
+
// REPLACES the pipeline rather than joining it, which the last describe
|
|
43
|
+
// below is the worked example for.
|
|
44
|
+
expect(config.mode).toBe("s2s");
|
|
45
|
+
expect(config.stt).toBeUndefined();
|
|
46
|
+
expect(config.tts).toBeUndefined();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const stage of ["stt", "llm", "tts"] as const) {
|
|
50
|
+
expect(config[stage]?.kind, stage).toBe(agentDef[stage]?.kind ?? "assemblyai");
|
|
51
|
+
}
|
|
35
52
|
});
|
|
36
53
|
});
|
|
37
54
|
|