@alexkroman1/aai-cli 5.8.1 → 5.10.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/README.md +5 -1
- package/bin.mjs +32 -0
- package/dist/{_agent-2nVugrN3.mjs → _agent-DS2PUJcl.mjs} +1 -1
- package/dist/{_bundler-Cjaxa2wi.mjs → _bundler-DC17suWN.mjs} +2 -2
- package/dist/_config-DMyolIk9.mjs +242 -0
- package/dist/_config.d.ts +28 -9
- package/dist/_dev-env.d.ts +38 -0
- package/dist/{_dev-server-BB5N8kdh.mjs → _dev-server-B3ivqyEd.mjs} +75 -55
- package/dist/_dev-server.d.ts +2 -2
- package/dist/{_init-Dei2JRmn.mjs → _init-CYLvYU-B.mjs} +3 -3
- package/dist/{_ui-8kOEB-JH.mjs → _output-CC300DzW.mjs} +1 -42
- package/dist/{_server-common-CnaP_Urf.mjs → _server-common-DX8Bfrf5.mjs} +1 -1
- package/dist/{_slug-api-CGJSST9B.mjs → _slug-api-fRNNR8tz.mjs} +1 -1
- package/dist/{_templates-BfyRyE7z.mjs → _templates-BWJOiWOO.mjs} +2 -2
- package/dist/{_typecheck-gate-DvE8S3aQ.mjs → _typecheck-gate-DB-PY0A3.mjs} +2 -1
- package/dist/_ui-DfwfDbT-.mjs +68 -0
- package/dist/_ui.d.ts +19 -0
- package/dist/{_utils-Ch0J4s6a.mjs → _utils-8KKw-bzi.mjs} +9 -2
- package/dist/_utils.d.ts +9 -2
- package/dist/{build-BXwDB78d.mjs → build-CACFbdQ4.mjs} +5 -4
- package/dist/cli.mjs +21 -20
- package/dist/{client-bundler-yiWoXrgb.mjs → client-bundler-DONm-khu.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DRNfvczK.mjs → delete-DEZ7u3u4.mjs} +6 -4
- package/dist/delete.d.ts +5 -0
- package/dist/{deploy-Cp-wgME3.mjs → deploy-Ch0d_jje.mjs} +8 -7
- package/dist/{dev-C4KyxouE.mjs → dev-CdeRcYiQ.mjs} +7 -6
- package/dist/{init-BpBbttOv.mjs → init-Nc6fB774.mjs} +6 -5
- package/dist/{login-AA_UdRI-.mjs → login-BeFUiU6M.mjs} +9 -9
- package/dist/scaffold/CLAUDE.md +39 -14
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-Dr0qnxeb.mjs → secret-4_dYyrpA.mjs} +3 -2
- package/dist/{storage-CoQB8d-u.mjs → storage-BTfErOOW.mjs} +3 -2
- package/dist/{studio-_UBBvh5V.mjs → studio-LNvXtWak.mjs} +7 -6
- package/dist/templates/dispatch-center/agent.ts +4 -6
- package/dist/templates/retail/agent.ts +4 -6
- package/dist/templates/retail/registry.test.ts +5 -3
- package/dist/templates/retail/resolve.test.ts +6 -4
- package/dist/templates/retail/seed.test.ts +21 -13
- package/dist/templates/retail/shared.test.ts +9 -5
- package/dist/templates/solo-rpg/agent.test.ts +7 -5
- package/dist/templates/solo-rpg/shared.ts +2 -2
- package/dist/{test-3Gq4pqH_.mjs → test-C-V98oC-.mjs} +4 -4
- package/dist/typecheck.mjs +65 -14
- package/package.json +5 -4
- package/dist/_config-Y5V-5Krn.mjs +0 -173
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -97,8 +97,9 @@ export default agent({
|
|
|
97
97
|
silencePrompt?: string; // instruction injected on silence timeout (requires silenceTimeoutMs)
|
|
98
98
|
minBargeInWords?: number; // pipeline only — words before user speech interrupts the reply (default 2)
|
|
99
99
|
interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 500; 0 disables)
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
deadAirCoverMs?: number; // pipeline only — speak a short filler after this much silence in a turn (default 5000; 0 disables)
|
|
101
|
+
resumeFalseInterruption?: boolean; // pipeline only — resume an interrupted reply if no user turn commits (default true)
|
|
102
|
+
preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default true; false opts out)
|
|
102
103
|
state?: () => S; // per-session mutable state, exposed as ctx.state
|
|
103
104
|
// (S is inferred; see "Typing ctx.state")
|
|
104
105
|
syncState?: (state: S) => unknown; // push a projection of state to the client
|
|
@@ -250,9 +251,26 @@ land). End-of-turn detection (how long a pause ends the user's turn)
|
|
|
250
251
|
belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
|
|
251
252
|
2000 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
|
|
252
253
|
pauses don't split a request.
|
|
253
|
-
`
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
`deadAirCoverMs` is how long a turn may go silent before the transport speaks
|
|
255
|
+
a short filler, so a long tool chain doesn't sound like a dropped call. It is
|
|
256
|
+
measured silence, not a guess about the turn's shape, so a reply that arrives
|
|
257
|
+
promptly pays nothing; `0` disables it. The wording is not yours to set — the
|
|
258
|
+
filler must be purely declarative and never a request for patience, or the
|
|
259
|
+
caller answers it and the answer barges in.
|
|
260
|
+
`resumeFalseInterruption` (default `true`) resumes an interrupted reply when
|
|
261
|
+
a barge-in turns out to be noise — no user turn ever commits. The wait is not
|
|
262
|
+
configurable: the resume fires once the transcript stream goes quiet with no
|
|
263
|
+
final, so it can never race a real turn the STT is still endpointing.
|
|
264
|
+
`preemptiveGeneration` (default `true`) starts generating the reply as soon
|
|
265
|
+
as transcription is confident the caller has finished, and uses that
|
|
266
|
+
already-running answer if the committed transcript matches. It can shorten the
|
|
267
|
+
pause before the agent speaks, and **how much is unmeasured** — nobody has
|
|
268
|
+
shown the size of the saving. What bounds the downside is that a speculation
|
|
269
|
+
never speaks, calls a tool, or enters history until the real turn adopts it, so
|
|
270
|
+
the worst case is a wasted request and a turn that behaves exactly as it would
|
|
271
|
+
with the flag off. Set `preemptiveGeneration: false` to opt out — worth doing
|
|
272
|
+
on a tool-heavy agent, where a speculation that reaches a tool call is thrown
|
|
273
|
+
away, so it is cost with no upside.
|
|
256
274
|
|
|
257
275
|
## Providers
|
|
258
276
|
|
|
@@ -651,10 +669,11 @@ interpolate values into the SQL string. The rows come back as plain objects;
|
|
|
651
669
|
A query returning more than 1000 rows throws — always bound reads with
|
|
652
670
|
`LIMIT` (paginate with `LIMIT`/`OFFSET`).
|
|
653
671
|
|
|
654
|
-
**
|
|
672
|
+
**The database must be enabled** or accessing `ctx.db` throws:
|
|
655
673
|
|
|
656
674
|
- CLI: `aai storage enable`
|
|
657
|
-
- Studio:
|
|
675
|
+
- Studio: Settings pane → Database → Enable database (covers both the
|
|
676
|
+
preview and published agents, each with its own schema)
|
|
658
677
|
- `aai dev`: set `DATABASE_URL` in the project `.env`
|
|
659
678
|
|
|
660
679
|
Create tables lazily from tool code and upsert with `on conflict`:
|
|
@@ -929,8 +948,10 @@ Never hardcode secrets in agent code.
|
|
|
929
948
|
`ctx.env`.
|
|
930
949
|
- **Production:** `npx @alexkroman1/aai-cli secret put NAME`
|
|
931
950
|
- **Access:** `ctx.env.MY_KEY` in tool execute functions.
|
|
932
|
-
- **AssemblyAI key:**
|
|
933
|
-
|
|
951
|
+
- **AssemblyAI key:** `npx @alexkroman1/aai-cli login` links your account and
|
|
952
|
+
stores the key globally — the only way the CLI authenticates. No `.env`
|
|
953
|
+
entry needed. For CI, point `AAI_CONFIG_DIR` at a config dir holding a
|
|
954
|
+
logged-in key (an exported `ASSEMBLYAI_API_KEY` does not authenticate).
|
|
934
955
|
|
|
935
956
|
## Voice rules for systemPrompt
|
|
936
957
|
|
|
@@ -984,9 +1005,11 @@ Common mistakes when working in aai projects:
|
|
|
984
1005
|
platform's Modal/Deno sandbox; the self-hosted `aai dev` server has no
|
|
985
1006
|
sandbox, so there `run_code` refuses with an error result. Deploy to test
|
|
986
1007
|
it end-to-end, or use the `calculate` builtin for simple arithmetic in dev.
|
|
987
|
-
- **`ctx.db` throws until
|
|
988
|
-
`aai storage enable` (CLI),
|
|
989
|
-
in `.env` (`aai dev`) before shipping tools that persist
|
|
1008
|
+
- **`ctx.db` throws until the database is enabled.** Enable it with
|
|
1009
|
+
`aai storage enable` (CLI), Settings → Database (studio), or
|
|
1010
|
+
`DATABASE_URL` in `.env` (`aai dev`) before shipping tools that persist
|
|
1011
|
+
data. In the studio it takes effect when each agent next deploys — the
|
|
1012
|
+
preview redeploys itself, production on the next publish.
|
|
990
1013
|
- **The database is per-app.** Rows are shared by every session of one
|
|
991
1014
|
deployment — key them yourself if sessions must not see each other's data
|
|
992
1015
|
(or keep session-scoped data in `ctx.state`).
|
|
@@ -1002,7 +1025,9 @@ Common mistakes when working in aai projects:
|
|
|
1002
1025
|
- Agent code runs in a sandboxed worker — use `fetch` for HTTP, `ctx.env`
|
|
1003
1026
|
for secrets
|
|
1004
1027
|
- Tool execution timeout: 30 seconds
|
|
1005
|
-
- `maxSteps` limits tool calls per turn (default 10) —
|
|
1006
|
-
|
|
1028
|
+
- `maxSteps` limits tool calls per turn (default 10) — lower it for a
|
|
1029
|
+
latency-sensitive agent. On reaching the cap the agent spends one more LLM
|
|
1030
|
+
step with tools switched off, so it answers with what it has instead of
|
|
1031
|
+
going silent mid-chain
|
|
1007
1032
|
- Tool returns `undefined` if execute function has no return statement —
|
|
1008
1033
|
always return a value
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"publish:agent": "aai publish"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@alexkroman1/aai": "^5.
|
|
15
|
-
"@alexkroman1/aai-ui": "^5.
|
|
14
|
+
"@alexkroman1/aai": "^5.10.0",
|
|
15
|
+
"@alexkroman1/aai-ui": "^5.10.0",
|
|
16
16
|
"react": "^19.2.8",
|
|
17
17
|
"react-dom": "^19.2.8",
|
|
18
18
|
"tailwindcss": "^4.0.0",
|
|
19
19
|
"zod": "^4.4.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@alexkroman1/aai-cli": "^5.
|
|
22
|
+
"@alexkroman1/aai-cli": "^5.10.0",
|
|
23
23
|
"@tailwindcss/vite": "^4.3.3",
|
|
24
24
|
"@types/node": "^26.1.1",
|
|
25
25
|
"@types/react": "^19.2.17",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
2
|
+
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
+
import { n as log, o as unwrapCancel } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import { text } from "node:stream/consumers";
|
|
6
7
|
//#region secret.ts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
+
import { n as log } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
//#region storage.ts
|
|
6
7
|
async function storageRequest(cwd, init, server) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
|
|
3
|
+
import { n as log, t as fmtUrl } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { s as updateProjectConfig } from "./_config-DMyolIk9.mjs";
|
|
5
|
+
import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
|
|
6
|
+
import { i as resolveDeployTarget } from "./_agent-DS2PUJcl.mjs";
|
|
7
|
+
import { layerScaffold } from "./_templates-BWJOiWOO.mjs";
|
|
7
8
|
import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
@@ -290,7 +291,7 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
|
|
|
290
291
|
}
|
|
291
292
|
async function executePublish(opts) {
|
|
292
293
|
if (!opts.skipTypecheck) {
|
|
293
|
-
const { assertTypechecks } = await import("./_typecheck-gate-
|
|
294
|
+
const { assertTypechecks } = await import("./_typecheck-gate-DB-PY0A3.mjs");
|
|
294
295
|
await assertTypechecks(opts.cwd);
|
|
295
296
|
}
|
|
296
297
|
const pushed = await pushProject(opts);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
3
2
|
import { dashboardView } from "./shared.ts";
|
|
4
3
|
import systemPrompt from "./system-prompt.md?raw";
|
|
5
4
|
import { incidentAddNote } from "./tools/incident_add_note.ts";
|
|
@@ -20,11 +19,10 @@ export default agent({
|
|
|
20
19
|
// One projection replaces eleven `ctx.send("incidents", ...)` calls, and
|
|
21
20
|
// is the single place that decides caller PII stays server-side.
|
|
22
21
|
syncState: dashboardView,
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
stt: assemblyAIStt({ minTurnSilenceMs: 2200 }),
|
|
22
|
+
// A dispatcher reads addresses and unit numbers in bursts with pauses inside
|
|
23
|
+
// one message ("unit twelve … respond to"). The default pipeline's
|
|
24
|
+
// `max_turn_silence` already tolerates that; reach for
|
|
25
|
+
// `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
|
|
28
26
|
systemPrompt,
|
|
29
27
|
greeting:
|
|
30
28
|
"Dispatch Command Center online. Restoring operational state. I'm ready to take incoming calls, manage active incidents, or run dispatch operations. Say 'dashboard' for a full status report. What do we have.",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
3
2
|
import type { StateSlot } from "./shared.ts";
|
|
4
3
|
import { storeView } from "./shared.ts";
|
|
5
4
|
import { createDefaultState } from "./store.ts";
|
|
@@ -41,11 +40,10 @@ export default agent({
|
|
|
41
40
|
// authenticated one may reach the browser.
|
|
42
41
|
syncState: storeView,
|
|
43
42
|
|
|
44
|
-
// Callers read order numbers and ten-digit item numbers in bursts with
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// Callers read order numbers and ten-digit item numbers in bursts with pauses
|
|
44
|
+
// inside one utterance ("W seven six seven … eight oh seven two"). The default
|
|
45
|
+
// pipeline's `max_turn_silence` already tolerates that; reach for
|
|
46
|
+
// `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
|
|
49
47
|
systemPrompt,
|
|
50
48
|
greeting:
|
|
51
49
|
"Thanks for calling. Before I can look anything up I'll need to find your account — " +
|
|
@@ -105,16 +105,18 @@ describe("tool registry", () => {
|
|
|
105
105
|
);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// Both sweep the registry, so they assert softly: adding a batch of tools
|
|
109
|
+
// should list every one still missing its entry, not just the first.
|
|
108
110
|
test("every registered tool has sample args, so the sweeps below cover it", () => {
|
|
109
111
|
for (const [name] of registry) {
|
|
110
|
-
expect(SAMPLE_ARGS[name], `add SAMPLE_ARGS["${name}"]`).toBeDefined();
|
|
112
|
+
expect.soft(SAMPLE_ARGS[name], `add SAMPLE_ARGS["${name}"]`).toBeDefined();
|
|
111
113
|
}
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
test("every tool declares a description the model can act on", () => {
|
|
115
117
|
for (const [name, def] of registry) {
|
|
116
|
-
expect(def.description, name).toBeTruthy();
|
|
117
|
-
expect(def.description.length, name).toBeGreaterThan(40);
|
|
118
|
+
expect.soft(def.description, name).toBeTruthy();
|
|
119
|
+
expect.soft(def.description.length, name).toBeGreaterThan(40);
|
|
118
120
|
}
|
|
119
121
|
});
|
|
120
122
|
});
|
|
@@ -19,7 +19,9 @@ describe("normalizeOrderId", () => {
|
|
|
19
19
|
"5866402",
|
|
20
20
|
" #w 586 6402 ",
|
|
21
21
|
]) {
|
|
22
|
-
|
|
22
|
+
// Soft: a narrowed normalizer drops a whole family of spoken forms, and
|
|
23
|
+
// which ones it drops is what says where the pattern went wrong.
|
|
24
|
+
expect.soft(normalizeOrderId(spoken), spoken).toBe("#W5866402");
|
|
23
25
|
}
|
|
24
26
|
});
|
|
25
27
|
});
|
|
@@ -37,7 +39,7 @@ describe("resolveOrder — canonical ids", () => {
|
|
|
37
39
|
const state = stateFor("olivia_ito_3591");
|
|
38
40
|
for (const spoken of ["#W5866402", "W 5866402", "5866402"]) {
|
|
39
41
|
const order = resolveOrder(state, spoken);
|
|
40
|
-
expect(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
42
|
+
expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
41
43
|
}
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -52,7 +54,7 @@ describe("resolveOrder — shorthand", () => {
|
|
|
52
54
|
const state = stateFor("olivia_ito_3591");
|
|
53
55
|
for (const spoken of ["the delivered one", "my delivered order", "delivered"]) {
|
|
54
56
|
const order = resolveOrder(state, spoken);
|
|
55
|
-
expect(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
57
|
+
expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
|
|
@@ -61,7 +63,7 @@ describe("resolveOrder — shorthand", () => {
|
|
|
61
63
|
const result = resolveOrder(state, "my pending order");
|
|
62
64
|
if (!isError(result)) throw new Error("expected ambiguity");
|
|
63
65
|
for (const id of ["#W5442520", "#W7941031", "#W3657213"]) {
|
|
64
|
-
expect(result.error).toContain(id);
|
|
66
|
+
expect.soft(result.error, `ambiguity message omits ${id}`).toContain(id);
|
|
65
67
|
}
|
|
66
68
|
});
|
|
67
69
|
|
|
@@ -90,18 +90,25 @@ describe("seed shape", () => {
|
|
|
90
90
|
expect(Object.keys(seed.orders)).toHaveLength(22);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
// These four sweep the whole corpus, so they assert SOFTLY: one hand-edited
|
|
94
|
+
// order usually breaks several records at once, and a hard failure reports
|
|
95
|
+
// the first and hides how far the damage runs.
|
|
93
96
|
test("every order belongs to a seeded user and is listed on that user", () => {
|
|
94
97
|
for (const order of Object.values(seed.orders)) {
|
|
95
98
|
const user = seed.users[order.user_id];
|
|
96
|
-
expect(user, `order ${order.order_id} has no seeded user`).toBeDefined();
|
|
97
|
-
expect
|
|
99
|
+
expect.soft(user, `order ${order.order_id} has no seeded user`).toBeDefined();
|
|
100
|
+
expect
|
|
101
|
+
.soft(user?.orders ?? [], `${order.order_id} is not listed on its user`)
|
|
102
|
+
.toContain(order.order_id);
|
|
98
103
|
}
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
test("every user's listed orders are all seeded", () => {
|
|
102
107
|
for (const user of Object.values(seed.users)) {
|
|
103
108
|
for (const orderId of user.orders) {
|
|
104
|
-
expect
|
|
109
|
+
expect
|
|
110
|
+
.soft(seed.orders[orderId], `${user.user_id} lists unseeded ${orderId}`)
|
|
111
|
+
.toBeDefined();
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
});
|
|
@@ -110,11 +117,10 @@ describe("seed shape", () => {
|
|
|
110
117
|
for (const order of Object.values(seed.orders)) {
|
|
111
118
|
for (const item of order.items) {
|
|
112
119
|
const variant = seed.products[item.product_id]?.variants[item.item_id];
|
|
113
|
-
expect
|
|
114
|
-
variant,
|
|
115
|
-
|
|
116
|
-
).
|
|
117
|
-
expect(variant?.price).toBe(item.price);
|
|
120
|
+
expect
|
|
121
|
+
.soft(variant, `${order.order_id}: ${item.item_id} missing from its product`)
|
|
122
|
+
.toBeDefined();
|
|
123
|
+
expect.soft(variant?.price, `${order.order_id}: ${item.item_id} price`).toBe(item.price);
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
});
|
|
@@ -123,10 +129,12 @@ describe("seed shape", () => {
|
|
|
123
129
|
for (const order of Object.values(seed.orders)) {
|
|
124
130
|
const user = seed.users[order.user_id];
|
|
125
131
|
for (const payment of order.payment_history) {
|
|
126
|
-
expect
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
expect
|
|
133
|
+
.soft(
|
|
134
|
+
user?.payment_methods[payment.payment_method_id],
|
|
135
|
+
`${order.order_id}: unknown method ${payment.payment_method_id}`,
|
|
136
|
+
)
|
|
137
|
+
.toBeDefined();
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
});
|
|
@@ -148,7 +156,7 @@ describe("seed coverage — each datum backs a specific test in agent.test.ts",
|
|
|
148
156
|
test("every status the tools branch on is present", () => {
|
|
149
157
|
const statuses = new Set(Object.values(seed.orders).map((o) => o.status));
|
|
150
158
|
for (const s of ["pending", "processed", "delivered", "cancelled"]) {
|
|
151
|
-
expect(statuses, `no ${s} order seeded`).toContain(s);
|
|
159
|
+
expect.soft(statuses, `no ${s} order seeded`).toContain(s);
|
|
152
160
|
}
|
|
153
161
|
});
|
|
154
162
|
|
|
@@ -57,7 +57,9 @@ describe("storeView", () => {
|
|
|
57
57
|
"#W4316152",
|
|
58
58
|
"#W1840144",
|
|
59
59
|
]) {
|
|
60
|
-
|
|
60
|
+
// Soft: a widened projection leaks several fields at once, and the full
|
|
61
|
+
// list is what says how much of the other customers' records got out.
|
|
62
|
+
expect.soft(json, `projection leaked ${leaked}`).not.toContain(leaked);
|
|
61
63
|
}
|
|
62
64
|
});
|
|
63
65
|
|
|
@@ -84,15 +86,17 @@ describe("DEMO_PERSONAS", () => {
|
|
|
84
86
|
const users = Object.values((structuredClone(seedJson) as unknown as Store).users);
|
|
85
87
|
for (const persona of DEMO_PERSONAS) {
|
|
86
88
|
const user = users.find((u) => u.email === persona.email);
|
|
87
|
-
expect(user, `no seeded customer has email ${persona.email}`).toBeDefined();
|
|
88
|
-
expect
|
|
89
|
-
|
|
89
|
+
expect.soft(user, `no seeded customer has email ${persona.email}`).toBeDefined();
|
|
90
|
+
expect
|
|
91
|
+
.soft(`${user?.name.first_name} ${user?.name.last_name}`, persona.email)
|
|
92
|
+
.toBe(persona.name);
|
|
93
|
+
expect.soft(user?.address.zip, persona.email).toBe(persona.zip);
|
|
90
94
|
}
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
test("every persona carries a hint, so a user can pick a path deliberately", () => {
|
|
94
98
|
for (const persona of DEMO_PERSONAS) {
|
|
95
|
-
expect(persona.hint.length, persona.name).toBeGreaterThan(20);
|
|
99
|
+
expect.soft(persona.hint.length, persona.name).toBeGreaterThan(20);
|
|
96
100
|
}
|
|
97
101
|
});
|
|
98
102
|
});
|
|
@@ -167,11 +167,13 @@ describe("applyConsequences MISS matrix", () => {
|
|
|
167
167
|
] as const) {
|
|
168
168
|
const state = playingState();
|
|
169
169
|
const { deltas, clockEvents } = applyConsequences(state, miss, position, "standard", null);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(state.
|
|
173
|
-
expect(deltas.
|
|
174
|
-
expect(
|
|
170
|
+
// Each row starts from a fresh state, so they are independent — assert
|
|
171
|
+
// softly and a rebalanced table reports the whole matrix in one run.
|
|
172
|
+
expect.soft(state.health, position).toBe(5 - dmg);
|
|
173
|
+
expect.soft(deltas.health, position).toBe(-dmg);
|
|
174
|
+
expect.soft(state.clocks[0]?.filled, position).toBe(ticks);
|
|
175
|
+
expect.soft(deltas.clockTicks, position).toBe(ticks);
|
|
176
|
+
expect.soft(clockEvents, position).toHaveLength(0); // 4-segment clock not full yet
|
|
175
177
|
}
|
|
176
178
|
});
|
|
177
179
|
|
|
@@ -341,8 +341,8 @@ export function saveGameState(ctx: ToolContext, state: GameState): void {
|
|
|
341
341
|
|
|
342
342
|
// ── Persistent save slots (ctx.db) ───────────────────────────────────────────
|
|
343
343
|
// save_game / load_game are genuine cross-session persistence, so they use
|
|
344
|
-
// the app's SQL database. Requires storage: `aai storage enable` (or
|
|
345
|
-
//
|
|
344
|
+
// the app's SQL database. Requires storage: `aai storage enable` (or
|
|
345
|
+
// Settings → Database in the studio); under `aai dev`, set DATABASE_URL in .env.
|
|
346
346
|
//
|
|
347
347
|
// Slots are keyed by name alone — the whole point of a save is loading it in
|
|
348
348
|
// a LATER session, whose sessionId differs, so the key can't embed one. The
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
+
import { n as log } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-8KKw-bzi.mjs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
6
7
|
import path from "node:path";
|
|
@@ -50,8 +51,7 @@ function runVitest(cwd) {
|
|
|
50
51
|
testFile
|
|
51
52
|
], {
|
|
52
53
|
cwd,
|
|
53
|
-
stdio: "inherit"
|
|
54
|
-
env: { NODE_OPTIONS: "--experimental-strip-types" }
|
|
54
|
+
stdio: "inherit"
|
|
55
55
|
});
|
|
56
56
|
return true;
|
|
57
57
|
}
|
package/dist/typecheck.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as errorMessage, n as binFromPackageJson } from "./_utils-
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { a as errorMessage, n as binFromPackageJson } from "./_utils-8KKw-bzi.mjs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
//#region typecheck.ts
|
|
@@ -56,13 +56,69 @@ function findTypescriptPackage(cwd) {
|
|
|
56
56
|
dir = parent;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the project's TypeScript compiler entry (its own `tsc` bin), plus
|
|
61
|
+
* its major version — see {@link tscArgs} for what the version decides.
|
|
62
|
+
*/
|
|
63
|
+
function resolveTsc(cwd) {
|
|
61
64
|
const dir = findTypescriptPackage(cwd);
|
|
62
65
|
if (dir === void 0) throw new Error("no typescript package in the project's node_modules");
|
|
63
|
-
const
|
|
66
|
+
const manifest = path.join(dir, "package.json");
|
|
67
|
+
const bin = binFromPackageJson(manifest, "tsc");
|
|
64
68
|
if (!bin) throw new Error("installed typescript package declares no tsc bin");
|
|
65
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
entry: bin,
|
|
71
|
+
major: readMajor(manifest)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The resolved compiler's major version, or 0 when it can't be read.
|
|
76
|
+
*
|
|
77
|
+
* 0 is the safe answer: {@link tscArgs} only ADDS flags above a version
|
|
78
|
+
* floor, so an unreadable manifest degrades to the flag set every TypeScript
|
|
79
|
+
* accepts rather than failing a build over a cosmetic read.
|
|
80
|
+
*/
|
|
81
|
+
function readMajor(manifest) {
|
|
82
|
+
try {
|
|
83
|
+
const { version } = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
84
|
+
return typeof version === "string" ? Number.parseInt(version, 10) || 0 : 0;
|
|
85
|
+
} catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The `tsc` argv for one project check.
|
|
91
|
+
*
|
|
92
|
+
* `--singleThreaded` is the interesting one, and it is a SPEEDUP here rather
|
|
93
|
+
* than a throttle. TypeScript 7 parallelizes parse/check/emit by default,
|
|
94
|
+
* which pays off on a repo-sized program and costs on a single agent project —
|
|
95
|
+
* and this function only ever checks one of those. Measured on the templates
|
|
96
|
+
* project (14 agents, the closest in-repo analogue of a studio workspace):
|
|
97
|
+
*
|
|
98
|
+
* - pinned to 1 core: 2.4–2.9s parallel vs 1.2–1.4s single — ~2x faster
|
|
99
|
+
* - on 4 cores: 1.21–1.24s parallel vs 1.01–1.04s single
|
|
100
|
+
*
|
|
101
|
+
* The 1-core number is the one that matters: a guest sandbox RESERVES one CPU
|
|
102
|
+
* (`SANDBOX_CPU`) and this same check runs after every settled write burst in
|
|
103
|
+
* the studio (`aai-guest/studio-write-diagnostics.ts`), where the whole design
|
|
104
|
+
* rests on it finishing in well under a second. Parallelism inside a one-core
|
|
105
|
+
* reservation is oversubscription: the threads exist, contend, and the wall
|
|
106
|
+
* clock doubles.
|
|
107
|
+
*
|
|
108
|
+
* Gated on major >= 7 because the flag is TS 7's, and an unknown compiler
|
|
109
|
+
* option is a HARD error (TS5023) — a project pinning an older TypeScript
|
|
110
|
+
* would fail its build on a flag it never asked for. `engines`/scaffold pin
|
|
111
|
+
* `^7`, so in practice this floor is always met; it exists so a project that
|
|
112
|
+
* pins otherwise degrades instead of breaking.
|
|
113
|
+
*/
|
|
114
|
+
function tscArgs(entry, major) {
|
|
115
|
+
return [
|
|
116
|
+
entry,
|
|
117
|
+
"--noEmit",
|
|
118
|
+
"--pretty",
|
|
119
|
+
"false",
|
|
120
|
+
...major >= 7 ? ["--singleThreaded"] : []
|
|
121
|
+
];
|
|
66
122
|
}
|
|
67
123
|
/**
|
|
68
124
|
* Typecheck the project at `cwd` with its own tsconfig + compiler.
|
|
@@ -76,9 +132,9 @@ async function typecheckProject(cwd) {
|
|
|
76
132
|
ok: true,
|
|
77
133
|
skipped: true
|
|
78
134
|
};
|
|
79
|
-
let
|
|
135
|
+
let tsc;
|
|
80
136
|
try {
|
|
81
|
-
|
|
137
|
+
tsc = resolveTsc(cwd);
|
|
82
138
|
} catch (err) {
|
|
83
139
|
return {
|
|
84
140
|
ok: false,
|
|
@@ -86,12 +142,7 @@ async function typecheckProject(cwd) {
|
|
|
86
142
|
};
|
|
87
143
|
}
|
|
88
144
|
const result = await new Promise((resolve, reject) => {
|
|
89
|
-
const child = spawn(process.execPath,
|
|
90
|
-
tscEntry,
|
|
91
|
-
"--noEmit",
|
|
92
|
-
"--pretty",
|
|
93
|
-
"false"
|
|
94
|
-
], {
|
|
145
|
+
const child = spawn(process.execPath, tscArgs(tsc.entry, tsc.major), {
|
|
95
146
|
cwd,
|
|
96
147
|
timeout: TYPECHECK_TIMEOUT_MS,
|
|
97
148
|
stdio: [
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"aai": "
|
|
6
|
+
"aai": "bin.mjs"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client-bundler": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
+
"bin.mjs",
|
|
26
27
|
"dist"
|
|
27
28
|
],
|
|
28
29
|
"dependencies": {
|
|
@@ -37,8 +38,8 @@
|
|
|
37
38
|
"p-timeout": "^7.0.1",
|
|
38
39
|
"vite": "^8.1.5",
|
|
39
40
|
"zod": "^4.4.3",
|
|
40
|
-
"@alexkroman1/aai
|
|
41
|
-
"@alexkroman1/aai": "5.
|
|
41
|
+
"@alexkroman1/aai": "5.10.0",
|
|
42
|
+
"@alexkroman1/aai-ui": "5.10.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"playwright": "^1.61.1",
|