@alexkroman1/aai-cli 5.11.0 → 5.13.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/dist/{_agent-DS2PUJcl.mjs → _agent-C8Jk-ZcP.mjs} +3 -2
- package/dist/_agent.d.ts +1 -0
- package/dist/{_api-client-B-upMGkc.mjs → _api-client-BX8Qv51n.mjs} +2 -1
- package/dist/{_bundler-DC17suWN.mjs → _bundler-BuIyDdvs.mjs} +30 -11
- package/dist/_bundler.d.ts +16 -2
- package/dist/{_config-DMyolIk9.mjs → _config-CzwAWOWr.mjs} +3 -3
- package/dist/_deploy.d.ts +0 -10
- package/dist/_dev-restart.d.ts +59 -0
- package/dist/{_dev-server-B3ivqyEd.mjs → _dev-server-Ch7K8e8i.mjs} +181 -92
- package/dist/_dev-server.d.ts +17 -0
- package/dist/{_init-5hHkHe-V.mjs → _init-CbkrE2wq.mjs} +3 -3
- package/dist/_preflight-BtfaYtbE.mjs +46 -0
- package/dist/_preflight.d.ts +57 -0
- package/dist/{_server-common-DX8Bfrf5.mjs → _server-common-61fL__39.mjs} +1 -1
- package/dist/_slug-api-D30zPmsn.mjs +50 -0
- package/dist/_slug-api.d.ts +16 -0
- package/dist/_studio-D2ciIhbi.mjs +94 -0
- package/dist/_studio.d.ts +19 -16
- package/dist/{_templates-DmdG-voA.mjs → _templates-B1kt3BGt.mjs} +2 -2
- package/dist/{_typecheck-gate-DB-PY0A3.mjs → _typecheck-gate-4v9UBZHh.mjs} +1 -1
- package/dist/{_ui-DfwfDbT-.mjs → _ui-u7T4YooX.mjs} +5 -1
- package/dist/_ui.d.ts +2 -0
- package/dist/{_utils-8KKw-bzi.mjs → _utils-0DHbcqUb.mjs} +2 -2
- package/dist/{build-CACFbdQ4.mjs → build-DQJ1ZSm9.mjs} +4 -4
- package/dist/cli.d.ts +2 -1
- package/dist/cli.mjs +70 -34
- package/dist/{client-bundler-DONm-khu.mjs → client-bundler-C9qXL8A2.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DEZ7u3u4.mjs → delete-DCRX3d96.mjs} +4 -4
- package/dist/{deploy-Ch0d_jje.mjs → deploy-DjsEV-40.mjs} +27 -26
- package/dist/deploy.d.ts +0 -2
- package/dist/{dev-CdeRcYiQ.mjs → dev-CvPH5dbF.mjs} +3 -3
- package/dist/{eject-DSrAp8PL.mjs → eject-LgBRfnbH.mjs} +3 -3
- package/dist/{init-BBAoRI_f.mjs → init-BppxMR8r.mjs} +5 -5
- package/dist/{login-BeFUiU6M.mjs → login-BQQaxJhL.mjs} +4 -15
- package/dist/login.d.ts +0 -9
- package/dist/project-config.d.ts +23 -0
- package/dist/project-config.mjs +3 -0
- package/dist/scaffold/CLAUDE.md +60 -10
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-4_dYyrpA.mjs → secret-Ds9dvqAj.mjs} +7 -7
- package/dist/{storage-BTfErOOW.mjs → storage-DbrQSHFk.mjs} +2 -2
- package/dist/{studio-DPDFKZ9i.mjs → studio-GSFd4s5O.mjs} +12 -179
- package/dist/templates/dispatch-center/shared.ts +13 -15
- package/dist/templates/pizza-ordering/agent.test.ts +3 -0
- package/dist/templates/retail/store.ts +11 -17
- package/dist/templates/solo-rpg/agent.test.ts +3 -0
- package/dist/{test-C-V98oC-.mjs → test-2LWBcXvY.mjs} +2 -2
- package/dist/typecheck.mjs +1 -1
- package/package.json +8 -3
- package/dist/_slug-api-fRNNR8tz.mjs +0 -27
|
@@ -1,181 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
|
|
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 { i as resolveDeployTarget } from "./_agent-
|
|
7
|
-
import { layerScaffold } from "./_templates-
|
|
8
|
-
import { n as apiRequest } from "./_api-client-
|
|
3
|
+
import { n as log, t as fmtUrl } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { s as updateProjectConfig } from "./_config-CzwAWOWr.mjs";
|
|
5
|
+
import { t as resolveServerEnv } from "./_server-common-61fL__39.mjs";
|
|
6
|
+
import { i as resolveDeployTarget } from "./_agent-C8Jk-ZcP.mjs";
|
|
7
|
+
import { layerScaffold } from "./_templates-B1kt3BGt.mjs";
|
|
8
|
+
import { n as apiRequest } from "./_api-client-BX8Qv51n.mjs";
|
|
9
|
+
import { a as publishStudioProject, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectUrl, t as collectSourceFiles } from "./_studio-D2ciIhbi.mjs";
|
|
9
10
|
import path from "node:path";
|
|
10
|
-
import { mkdir,
|
|
11
|
-
import { PREVIEW_SLUG_SUFFIX, VALID_SLUG_RE } from "@alexkroman1/aai/utils";
|
|
12
|
-
//#region _studio.ts
|
|
13
|
-
/**
|
|
14
|
-
* Internals of the studio-workspace commands (`aai list/pull/push/publish`):
|
|
15
|
-
* the local source-file walk and the thin clients for the platform's
|
|
16
|
-
* `/studio/projects` routes. The workspace is the single source of truth —
|
|
17
|
-
* these helpers only move files between a local directory and the project's
|
|
18
|
-
* workspace row; production deploys happen exclusively through the studio's
|
|
19
|
-
* Publish route (which runs the deploy machinery in the project's sandbox).
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Mirrors of the studio workspace caps (`aai-studio-server/studio-limits.ts`)
|
|
23
|
-
* — by value, like `aai-guest/limits.ts`, since the CLI cannot depend on the
|
|
24
|
-
* private server package. The server re-validates every push; these exist so
|
|
25
|
-
* an oversized file is a named warning locally instead of a rejected upload.
|
|
26
|
-
*/
|
|
27
|
-
const MAX_STUDIO_FILE_BYTES = 256e3;
|
|
28
|
-
/**
|
|
29
|
-
* What never syncs to a workspace — the same set the guest's snapshot skips
|
|
30
|
-
* (`aai-guest/studio-workspace-fs.ts`), plus what only exists locally:
|
|
31
|
-
* secrets (`.env` rides the secret routes, never a workspace row) and
|
|
32
|
-
* lockfiles (regenerated by install, noise in the studio editor).
|
|
33
|
-
*/
|
|
34
|
-
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
35
|
-
"node_modules",
|
|
36
|
-
".git",
|
|
37
|
-
"dist",
|
|
38
|
-
".aai"
|
|
39
|
-
]);
|
|
40
|
-
const IGNORED_FILES = [
|
|
41
|
-
/^\.env(\..+)?$/,
|
|
42
|
-
/^pnpm-lock\.yaml$/,
|
|
43
|
-
/^package-lock\.json$/,
|
|
44
|
-
/^yarn\.lock$/,
|
|
45
|
-
/^\.DS_Store$/
|
|
46
|
-
];
|
|
47
|
-
function isIgnoredFile(name) {
|
|
48
|
-
return IGNORED_FILES.some((re) => re.test(name));
|
|
49
|
-
}
|
|
50
|
-
/** Project-relative paths of every syncable file under `dir`, sorted. */
|
|
51
|
-
async function walkProject(dir, current = dir) {
|
|
52
|
-
const out = [];
|
|
53
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
if (entry.isSymbolicLink()) continue;
|
|
56
|
-
const abs = path.join(current, entry.name);
|
|
57
|
-
if (entry.isDirectory()) {
|
|
58
|
-
if (!IGNORED_DIRS.has(entry.name)) out.push(...await walkProject(dir, abs));
|
|
59
|
-
} else if (entry.isFile() && !isIgnoredFile(entry.name)) out.push(path.relative(dir, abs));
|
|
60
|
-
}
|
|
61
|
-
return out.sort((a, b) => a.localeCompare(b));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Decode `buf` as UTF-8, or null when it isn't valid UTF-8.
|
|
65
|
-
*
|
|
66
|
-
* `fatal` makes an invalid sequence throw instead of becoming U+FFFD, which
|
|
67
|
-
* is the whole point: a workspace is a JSON path→string map and cannot carry
|
|
68
|
-
* arbitrary bytes, so a lossy read turned a pushed PNG into replacement
|
|
69
|
-
* characters while reporting success — and a later `aai pull` wrote the
|
|
70
|
-
* mangled version back over the local original. `ignoreBOM` keeps a leading
|
|
71
|
-
* U+FEFF in the string; without it the decoder strips the BOM and the check
|
|
72
|
-
* meant to stop corruption would quietly perform some of its own.
|
|
73
|
-
*/
|
|
74
|
-
const UTF8_STRICT = new TextDecoder("utf-8", {
|
|
75
|
-
fatal: true,
|
|
76
|
-
ignoreBOM: true
|
|
77
|
-
});
|
|
78
|
-
function decodeUtf8(buf) {
|
|
79
|
-
try {
|
|
80
|
-
return UTF8_STRICT.decode(buf);
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Walk a local project into the path→content record a workspace stores —
|
|
87
|
-
* the CLI-side twin of the guest's `snapshotWorkspace`: same ignored
|
|
88
|
-
* directories, same caps, oversized and non-text files skipped with a
|
|
89
|
-
* warning rather than failing the whole push.
|
|
90
|
-
*/
|
|
91
|
-
async function collectSourceFiles(dir) {
|
|
92
|
-
const paths = await walkProject(dir);
|
|
93
|
-
const files = {};
|
|
94
|
-
const warnings = [];
|
|
95
|
-
if (paths.length > 100) warnings.push(`Project has ${paths.length} files; only the first 100 sync to the studio.`);
|
|
96
|
-
const read = await Promise.all(paths.slice(0, 100).map(async (rel) => {
|
|
97
|
-
const abs = path.join(dir, rel);
|
|
98
|
-
const st = await stat(abs);
|
|
99
|
-
if (st.size > 256e3) return {
|
|
100
|
-
ok: false,
|
|
101
|
-
warning: `${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`
|
|
102
|
-
};
|
|
103
|
-
const content = decodeUtf8(await readFile(abs));
|
|
104
|
-
if (content === null) return {
|
|
105
|
-
ok: false,
|
|
106
|
-
warning: `${rel} is not valid UTF-8 (binary file?) — not synced.`
|
|
107
|
-
};
|
|
108
|
-
return {
|
|
109
|
-
ok: true,
|
|
110
|
-
rel,
|
|
111
|
-
content
|
|
112
|
-
};
|
|
113
|
-
}));
|
|
114
|
-
for (const entry of read) {
|
|
115
|
-
if (!entry.ok) {
|
|
116
|
-
warnings.push(entry.warning);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
files[entry.rel.split(path.sep).join("/")] = entry.content;
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
files,
|
|
123
|
-
warnings
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* A studio project name derived from a directory name, or null if unusable.
|
|
128
|
-
*
|
|
129
|
-
* A `-preview` suffix is deliberately unusable. Publishing a project deploys
|
|
130
|
-
* it under the project's own name, so a `*-preview` project would claim a
|
|
131
|
-
* slug the studio's orphan-preview sweep reaps hourly — deleting the agent,
|
|
132
|
-
* its app-database schema, and its secrets on a schedule the user never
|
|
133
|
-
* asked for. Refusing the name is recoverable (rename the directory); losing
|
|
134
|
-
* a published agent to the reaper is not.
|
|
135
|
-
*/
|
|
136
|
-
function projectNameFromDir(dir) {
|
|
137
|
-
const name = path.basename(dir).toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 64);
|
|
138
|
-
if (name.endsWith(PREVIEW_SLUG_SUFFIX)) return null;
|
|
139
|
-
return VALID_SLUG_RE.test(name) ? name : null;
|
|
140
|
-
}
|
|
141
|
-
/** The shareable studio URL for a project — what every command prints. */
|
|
142
|
-
function studioProjectUrl(serverUrl, project) {
|
|
143
|
-
return `${serverUrl}/studio/chat/${project}`;
|
|
144
|
-
}
|
|
145
|
-
function listStudioProjects(serverUrl, apiKey) {
|
|
146
|
-
return apiRequest(`${serverUrl}/studio/projects`, {
|
|
147
|
-
apiKey,
|
|
148
|
-
action: "list"
|
|
149
|
-
}).then((res) => res.projects);
|
|
150
|
-
}
|
|
151
|
-
/** Fetch a project, or null when it doesn't exist (the push existence probe). */
|
|
152
|
-
function fetchStudioProject(serverUrl, apiKey, project) {
|
|
153
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}`, {
|
|
154
|
-
apiKey,
|
|
155
|
-
action: "pull",
|
|
156
|
-
allow404: true
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
/** `PUT /studio/projects/:project/source` — the atomic whole-tree push. */
|
|
160
|
-
function pushStudioSource(serverUrl, apiKey, project, body) {
|
|
161
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/source`, {
|
|
162
|
-
apiKey,
|
|
163
|
-
action: "push",
|
|
164
|
-
method: "PUT",
|
|
165
|
-
body,
|
|
166
|
-
hints: { 409: "The studio has newer changes. Run `aai pull` to fetch them, or `aai push --force` to overwrite." }
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
/** `POST /studio/projects/:project/deploy` — Publish, in the project's sandbox. */
|
|
170
|
-
function publishStudioProject(serverUrl, apiKey, project) {
|
|
171
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/deploy`, {
|
|
172
|
-
apiKey,
|
|
173
|
-
action: "publish",
|
|
174
|
-
method: "POST",
|
|
175
|
-
retry: 0
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
//#endregion
|
|
11
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
179
12
|
//#region studio.ts
|
|
180
13
|
/**
|
|
181
14
|
* The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
|
|
@@ -310,11 +143,11 @@ async function executePush(opts) {
|
|
|
310
143
|
* deploy time, which is why publish syncs them BEFORE deploying when the
|
|
311
144
|
* slug is already known.
|
|
312
145
|
*/
|
|
313
|
-
async function syncEnvSecrets(cwd, serverUrl, apiKey,
|
|
146
|
+
async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
|
|
314
147
|
const env = await resolveServerEnv(cwd);
|
|
315
148
|
const names = Object.keys(env);
|
|
316
149
|
if (names.length === 0) return [];
|
|
317
|
-
await apiRequest(`${serverUrl}/${
|
|
150
|
+
await apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/secret`, {
|
|
318
151
|
apiKey,
|
|
319
152
|
action: "secret",
|
|
320
153
|
method: "PUT",
|
|
@@ -325,13 +158,13 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
|
|
|
325
158
|
}
|
|
326
159
|
async function executePublish(opts) {
|
|
327
160
|
if (!opts.skipTypecheck) {
|
|
328
|
-
const { assertTypechecks } = await import("./_typecheck-gate-
|
|
161
|
+
const { assertTypechecks } = await import("./_typecheck-gate-4v9UBZHh.mjs");
|
|
329
162
|
await assertTypechecks(opts.cwd);
|
|
330
163
|
}
|
|
331
164
|
const pushed = await pushProject(opts);
|
|
332
165
|
const { project, serverUrl, apiKey } = pushed;
|
|
333
166
|
const hadSlug = pushed.slug !== void 0;
|
|
334
|
-
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey,
|
|
167
|
+
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
|
|
335
168
|
log.step(`Publishing ${project} (builds in the project's sandbox)…`);
|
|
336
169
|
const result = await publishStudioProject(serverUrl, apiKey, project);
|
|
337
170
|
if (typeof result?.slug !== "string" || typeof result?.output !== "string") throw new CliError("bad_publish_response", `Unexpected response from the publish route at ${serverUrl}.`, "Check that --server points at an aai platform server, then try again.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
+
import { createKeyedLock, withLock } from "@alexkroman1/aai";
|
|
4
5
|
|
|
5
6
|
export const SEVERITIES = ["critical", "urgent", "moderate", "minor"] as const;
|
|
6
7
|
export type Severity = (typeof SEVERITIES)[number];
|
|
@@ -198,7 +199,7 @@ function pruneState(state: DispatchState): void {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
const
|
|
202
|
+
const sessionLock = createKeyedLock();
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
205
|
* Serialized update of the session's dispatch state.
|
|
@@ -206,33 +207,30 @@ const sessionLocks = new Map<string, Promise<unknown>>();
|
|
|
206
207
|
* The LLM loop executes parallel tool calls concurrently. The state lives in
|
|
207
208
|
* `ctx.state` now (one shared object, no snapshot/save round-trip), but a
|
|
208
209
|
* mutator may be async, and two interleaving async mutators can each observe
|
|
209
|
-
* the other's half-applied changes.
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
210
|
+
* the other's half-applied changes. Holding the session's key makes each
|
|
211
|
+
* update run against the previous one's finished result. It also centralizes
|
|
212
|
+
* the shared bookkeeping every mutating tool needs: pruning and alert-level
|
|
213
|
+
* recalculation.
|
|
214
|
+
*
|
|
215
|
+
* `createKeyedLock` rather than a hand-rolled promise chain per session: the
|
|
216
|
+
* SDK's drops each key's entry once its chain drains (so a long-running agent
|
|
217
|
+
* does not accumulate one per session id) and does it by ownership, which is
|
|
218
|
+
* the part a hand-rolled chain gets wrong.
|
|
213
219
|
*
|
|
214
220
|
* There is no post-save callback: pushing the board to the client used to
|
|
215
221
|
* need one, and `syncState` now does it after every tool call.
|
|
216
222
|
*/
|
|
217
|
-
export
|
|
223
|
+
export function updateState<R>(
|
|
218
224
|
ctx: ToolContext,
|
|
219
225
|
mutator: (state: DispatchState) => R | Promise<R>,
|
|
220
226
|
): Promise<R> {
|
|
221
|
-
|
|
222
|
-
const run = prev.then(async () => {
|
|
227
|
+
return withLock(sessionLock, ctx.sessionId, async () => {
|
|
223
228
|
const state = getState(ctx);
|
|
224
229
|
const result = await mutator(state);
|
|
225
230
|
pruneState(state);
|
|
226
231
|
recalculateAlertLevel(state);
|
|
227
232
|
return result;
|
|
228
233
|
});
|
|
229
|
-
// Keep the chain alive across failures, and drop it once idle.
|
|
230
|
-
const tail = run.catch(() => {});
|
|
231
|
-
sessionLocks.set(ctx.sessionId, tail);
|
|
232
|
-
tail.then(() => {
|
|
233
|
-
if (sessionLocks.get(ctx.sessionId) === tail) sessionLocks.delete(ctx.sessionId);
|
|
234
|
-
});
|
|
235
|
-
return run;
|
|
236
234
|
}
|
|
237
235
|
|
|
238
236
|
// ─── Incident helpers ────────────────────────────────────────────────────────
|
|
@@ -21,6 +21,9 @@ function makeCtx(sessionId = "session-a") {
|
|
|
21
21
|
generate: () => Promise.reject(new Error("generate not available in tests")),
|
|
22
22
|
messages: [],
|
|
23
23
|
sessionId,
|
|
24
|
+
// Never aborts — a test has no turn to cancel. `ctx.signal` is always
|
|
25
|
+
// present at runtime, so a stub supplies one rather than omitting it.
|
|
26
|
+
signal: new AbortController().signal,
|
|
24
27
|
send: (event, data) => {
|
|
25
28
|
sent.push({ event, data });
|
|
26
29
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { createKeyedLock, tool, withLock } from "@alexkroman1/aai";
|
|
3
3
|
import type { z } from "zod";
|
|
4
4
|
import seedJson from "./seed.json";
|
|
5
5
|
import type {
|
|
@@ -151,34 +151,28 @@ export function requireOwnOrder(state: RetailState, orderId: string): Order | Er
|
|
|
151
151
|
|
|
152
152
|
// ─── Serialized state updates ────────────────────────────────────────────────
|
|
153
153
|
|
|
154
|
-
const
|
|
154
|
+
const sessionLock = createKeyedLock();
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Serialized update of the session's store.
|
|
158
158
|
*
|
|
159
159
|
* The LLM loop can execute parallel tool calls, and a mutator may be async —
|
|
160
160
|
* two interleaving async mutators can each observe the other's half-applied
|
|
161
|
-
* changes.
|
|
162
|
-
* one's finished result.
|
|
161
|
+
* changes. Holding the session's key runs each update against the previous
|
|
162
|
+
* one's finished result. `createKeyedLock` is the SDK's primitive for exactly
|
|
163
|
+
* this; the hand-rolled promise chain it replaced also had to remember to drop
|
|
164
|
+
* each key's entry by ownership once idle.
|
|
163
165
|
*
|
|
164
166
|
* NOT exported, unlike dispatch-center's equivalent. `retailTool` is the only
|
|
165
|
-
* caller, so a tool body cannot re-enter it — which would
|
|
166
|
-
*
|
|
167
|
-
* hazard you
|
|
167
|
+
* caller, so a tool body cannot re-enter it — which would wait on a key only
|
|
168
|
+
* the outer call can release, i.e. deadlock. A hazard you can't reach beats a
|
|
169
|
+
* hazard you documented.
|
|
168
170
|
*/
|
|
169
|
-
|
|
171
|
+
function updateState<R>(
|
|
170
172
|
ctx: ToolContext,
|
|
171
173
|
mutator: (state: RetailState) => R | Promise<R>,
|
|
172
174
|
): Promise<R> {
|
|
173
|
-
|
|
174
|
-
const run = previous.then(() => mutator(getState(ctx)));
|
|
175
|
-
// Keep the chain alive across failures, and drop it once idle.
|
|
176
|
-
const tail = run.catch(() => {});
|
|
177
|
-
sessionLocks.set(ctx.sessionId, tail);
|
|
178
|
-
tail.then(() => {
|
|
179
|
-
if (sessionLocks.get(ctx.sessionId) === tail) sessionLocks.delete(ctx.sessionId);
|
|
180
|
-
});
|
|
181
|
-
return run;
|
|
175
|
+
return withLock(sessionLock, ctx.sessionId, async () => mutator(getState(ctx)));
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
// ─── The tool wrapper ────────────────────────────────────────────────────────
|
|
@@ -54,6 +54,9 @@ function makeCtx(sessionId = "session-a", db: Db = makeDb().db): ToolContext {
|
|
|
54
54
|
generate: () => Promise.reject(new Error("generate not available in tests")),
|
|
55
55
|
messages: [],
|
|
56
56
|
sessionId,
|
|
57
|
+
// Never aborts — a test has no turn to cancel. `ctx.signal` is always
|
|
58
|
+
// present at runtime, so a stub supplies one rather than omitting it.
|
|
59
|
+
signal: new AbortController().signal,
|
|
57
60
|
send: vi.fn(),
|
|
58
61
|
};
|
|
59
62
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log } from "./_ui-
|
|
4
|
-
import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-
|
|
3
|
+
import { n as log } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-0DHbcqUb.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
package/dist/typecheck.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as errorMessage, n as binFromPackageJson } from "./_utils-
|
|
2
|
+
import { a as errorMessage, n as binFromPackageJson } from "./_utils-0DHbcqUb.mjs";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"@dev/source": "./typecheck.ts",
|
|
21
21
|
"types": "./dist/typecheck.d.ts",
|
|
22
22
|
"import": "./dist/typecheck.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./project-config": {
|
|
25
|
+
"@dev/source": "./project-config.ts",
|
|
26
|
+
"types": "./dist/project-config.d.ts",
|
|
27
|
+
"import": "./dist/project-config.mjs"
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
30
|
"files": [
|
|
@@ -38,8 +43,8 @@
|
|
|
38
43
|
"p-timeout": "^7.0.1",
|
|
39
44
|
"vite": "^8.1.5",
|
|
40
45
|
"zod": "^4.4.3",
|
|
41
|
-
"@alexkroman1/aai": "5.
|
|
42
|
-
"@alexkroman1/aai-ui": "5.
|
|
46
|
+
"@alexkroman1/aai": "5.13.0",
|
|
47
|
+
"@alexkroman1/aai-ui": "5.13.0"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
45
50
|
"playwright": "^1.61.1",
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { n as getServerInfo } from "./_agent-DS2PUJcl.mjs";
|
|
3
|
-
import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B-upMGkc.mjs";
|
|
4
|
-
//#region _slug-api.ts
|
|
5
|
-
/**
|
|
6
|
-
* Authenticated request against a deployed agent's slug-scoped resource
|
|
7
|
-
* (`${serverUrl}/${slug}${resourcePath}`) — the one shape every per-agent
|
|
8
|
-
* command (secret, storage) shares, including the standard "not deployed"
|
|
9
|
-
* 404 hint.
|
|
10
|
-
*
|
|
11
|
-
* Its own module (rather than living beside `getServerInfo` in `_agent.ts`)
|
|
12
|
-
* so tests can mock `_agent.ts`/`_api-client.ts` while this composition
|
|
13
|
-
* stays real — an intra-module call would bypass those mocks.
|
|
14
|
-
*/
|
|
15
|
-
async function slugRequest(cwd, resourcePath, init, server) {
|
|
16
|
-
const { serverUrl, slug, apiKey } = await getServerInfo(cwd, server);
|
|
17
|
-
return {
|
|
18
|
-
data: await apiRequest(`${serverUrl}/${slug}${resourcePath}`, {
|
|
19
|
-
...init,
|
|
20
|
-
apiKey,
|
|
21
|
-
hints: { 404: HINT_NOT_DEPLOYED }
|
|
22
|
-
}),
|
|
23
|
-
slug
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
export { slugRequest as t };
|