lua-cli 3.35.0 โ†’ 3.37.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/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # lua-cli v3.35.0
1
+ # lua-cli v3.37.0
2
2
 
3
- Welcome to the comprehensive API documentation for lua-cli v3.35.0. This guide covers every API, class, and function exported by the package.
3
+ Welcome to the comprehensive API documentation for lua-cli v3.37.0. This guide covers every API, class, and function exported by the package.
4
4
 
5
5
  ## ๐Ÿ“š Documentation Index
6
6
 
@@ -15,7 +15,7 @@ Inside a sandbox (tools, jobs, webhooks, workflow code steps) `Workflows` is als
15
15
  - `start` **never awaits execution** โ€” it returns `{ runId, status }` immediately. `waitSeconds` (โ‰ค 55) asks the server to hold the response for early terminal state; it changes the response, never the execution model.
16
16
  - Runs are idempotent on `idempotencyKey` โ€” re-sending the same key returns the existing run instead of a duplicate.
17
17
  - A workflow with `concurrencyPolicy: 'forbid'` refuses a second start while one is in flight with a thrown `WorkflowApiError { code: 'RUNS_IN_FLIGHT', blockingRunId }` โ€” never a 429, never a gate.
18
- - `status: 'gated'` means the run is parked (quota, billing or consent) and holds no slot. Consent gates are cleared from the desktop or with `lua workflows approve`.
18
+ - `status: 'gated'` means the run is parked (quota, billing or consent) and holds no slot. A **consent** gate is cleared by a person resolving its approval โ€” from the desktop, or through the approvals API / inbox. `lua workflows approve` cannot clear one: it requires an `--approval wfa_โ€ฆ` id, which belongs to a workflow `approval` node, and a consent gate has no such id. `lua workflows status <runId>` reports a gated run, and exits 6 under `--strict`.
19
19
  - `nameOrId` resolves the workflow **name on the calling agent first**, then a workflow id.
20
20
  - `get` / `list` honour output visibility: a restricted run comes back with `restricted: true` and no `output` โ€” never an error.
21
21
  - From a workflow code step, `Workflows.start` creates a **detached** run (no parent link). Use the `.workflow(...)` builder node when the parent should wait.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lua-cli",
3
- "version": "3.35.0",
3
+ "version": "3.37.0",
4
4
  "description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
5
5
  "readmeFilename": "README.md",
6
6
  "main": "dist/api-exports.js",
@@ -114,9 +114,9 @@
114
114
  "ts-node": "^10.9.2",
115
115
  "tsup": "^8.5.1",
116
116
  "@lua/sandbox-runtime": "0.0.1",
117
- "@lua/shared-sandbox": "0.0.1",
118
117
  "@lua/shared-source-sync": "0.0.1",
119
118
  "@lua/shared-types": "0.0.1",
119
+ "@lua/shared-sandbox": "0.0.1",
120
120
  "@lua/workflow-graph": "0.0.1"
121
121
  },
122
122
  "scripts": {
@@ -128,6 +128,7 @@
128
128
  "typecheck": "tsc -p tsconfig.typecheck.json",
129
129
  "test": "node ../../scripts/test-gate.mjs jest",
130
130
  "test:watch": "jest --watch",
131
- "test:coverage": "node ../../scripts/test-gate.mjs jest --coverage"
131
+ "test:coverage": "node ../../scripts/test-gate.mjs jest --coverage",
132
+ "test:compat-matrix": "node scripts/compat-matrix-previous-cli.mjs"
132
133
  }
133
134
  }
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * [WMC-E7] The CLI compatibility matrix's real leg (plan ยง6.9, "Older CLIs keep working").
4
+ *
5
+ * Installs the PREVIOUS MINOR RELEASE of this CLI from npm into a temporary directory and drives it, as a
6
+ * child process, against the mock API in `tests/helpers/workflow-compat-server.mjs`. It runs the matrix
7
+ * twice: once against a server serving definitions and runs WITHOUT any of the per-step model-selection
8
+ * fields, and once WITH all of them, including the run-event types the older CLI has never heard of.
9
+ *
10
+ * The verbs exercised are the three ยง6.9 names: **push**, **view** and **watch**.
11
+ *
12
+ * โ”€โ”€ Safety โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+ * Nothing here touches a real account or a real agent. The child CLI is pointed at `127.0.0.1` through
14
+ * `LUA_API_URL` and given a throwaway `LUA_API_KEY`, so there is no `lua auth`, no production host and no
15
+ * production verb against anything that exists. The temp directory is removed on the way out.
16
+ *
17
+ * โ”€โ”€ Why it is gated โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ * It needs the npm registry. Every property it proves is also asserted offline in
19
+ * `tests/__tests__/workflows.compat-matrix.test.ts`, which runs on every PR; this leg is the end-to-end
20
+ * confirmation and belongs in the nightly job and on demand:
21
+ *
22
+ * pnpm --filter lua-cli test:compat-matrix
23
+ * LUA_COMPAT_MATRIX_VERSION=3.33.0 pnpm --filter lua-cli test:compat-matrix
24
+ *
25
+ * Exit 0 = the previous release coped with both halves. Non-zero names the leg that failed.
26
+ */
27
+ import { execFileSync, spawn } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { AGENT_ID, RUN_ID, WORKFLOW_ID, startCompatServer } from '../tests/helpers/workflow-compat-server.mjs';
33
+
34
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
35
+ const PKG = JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf8'));
36
+
37
+ /** The previous MINOR of the version in package.json โ€” 3.34.x โ‡’ 3.33.0 โ€” unless one is named. */
38
+ function previousMinor(current) {
39
+ const [major, minor] = current.split('.').map(Number);
40
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) throw new Error(`cannot read a version from "${current}"`);
41
+ if (minor === 0) throw new Error(`no previous minor for ${current} โ€” name one with LUA_COMPAT_MATRIX_VERSION`);
42
+ return `${major}.${minor - 1}.0`;
43
+ }
44
+
45
+ const TARGET = process.env.LUA_COMPAT_MATRIX_VERSION || previousMinor(PKG.version);
46
+
47
+ function say(msg) {
48
+ process.stdout.write(`${msg}\n`);
49
+ }
50
+
51
+ /**
52
+ * Run the installed previous CLI with the mock API as its whole world.
53
+ *
54
+ * ASYNC on purpose. `spawnSync` blocks this process's event loop, and the mock API runs in this process โ€”
55
+ * so a synchronous spawn freezes the server and every request the child makes hangs until the child is
56
+ * killed on its timeout. That failure looks exactly like a compatibility failure (empty output, no requests
57
+ * recorded) and is not one, which is why the harness records the signal as well as the exit code below.
58
+ */
59
+ function runOldCli(cliEntry, cwd, baseUrl, args, { timeoutMs = 60_000 } = {}) {
60
+ return new Promise((resolve) => {
61
+ const child = spawn(process.execPath, [cliEntry, ...args], {
62
+ cwd,
63
+ // stdin CLOSED, not inherited: an open pipe leaves any interactive prompt waiting for ever.
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ env: {
66
+ ...process.env,
67
+ LUA_API_URL: baseUrl,
68
+ LUA_AUTH_URL: baseUrl,
69
+ LUA_API_KEY: 'compat-matrix-not-a-real-key',
70
+ // Keep the child from phoning home for an update check or writing analytics during the matrix.
71
+ LUA_DISABLE_ANALYTICS: '1',
72
+ LUA_SKIP_UPDATE_CHECK: '1',
73
+ CI: '1',
74
+ NO_COLOR: '1',
75
+ },
76
+ });
77
+ let stdout = '';
78
+ let stderr = '';
79
+ child.stdout.on('data', (c) => (stdout += c));
80
+ child.stderr.on('data', (c) => (stderr += c));
81
+ const timer = setTimeout(() => child.kill('SIGTERM'), timeoutMs);
82
+ child.on('error', (error) => {
83
+ clearTimeout(timer);
84
+ resolve({ status: null, signal: null, stdout, stderr, error });
85
+ });
86
+ child.on('close', (status, signal) => {
87
+ clearTimeout(timer);
88
+ resolve({ status, signal, stdout, stderr });
89
+ });
90
+ });
91
+ }
92
+
93
+ /** One half of the matrix: a server serving old-only or new-bearing shapes, and the three verbs against it. */
94
+ async function runLeg({ withNewFields, cliEntry, projectDir }) {
95
+ const label = withNewFields ? 'WITH the new fields' : 'WITHOUT the new fields';
96
+ const server = await startCompatServer({ withNewFields });
97
+ const failures = [];
98
+ try {
99
+ const checks = [
100
+ { verb: 'view', args: ['workflows', 'view', 'compat', '--json'] },
101
+ { verb: 'list', args: ['workflows', 'list', '--json'] },
102
+ { verb: 'status', args: ['workflows', 'status', RUN_ID, '--steps', '--json'] },
103
+ // `watch` must reach the terminal frame and exit 0 even with event types it does not know.
104
+ { verb: 'watch', args: ['workflows', 'watch', RUN_ID, '--timeout', '20'] },
105
+ // The push half of ยง6.9: the old client compiles its own source and sends a body with none of the new
106
+ // members. `compile` is part of it on purpose โ€” a push body an older CLI cannot even BUILD is the same
107
+ // failure as one the server rejects.
108
+ { verb: 'compile', args: ['compile', '--ci'] },
109
+ { verb: 'push', args: ['push', 'workflow', '--force'] },
110
+ ];
111
+ for (const { verb, args } of checks) {
112
+ const out = await runOldCli(cliEntry, projectDir, server.baseUrl, args);
113
+ const ok = out.status === 0;
114
+ say(
115
+ ` ${ok ? 'โœ“' : 'โœ—'} ${TARGET} ยท ${verb} ยท ${label}${ok ? '' : ` (${out.status === null ? `signal ${out.signal}` : `exit ${out.status}`})`}`
116
+ );
117
+ if (!ok) {
118
+ // `status: null` means the child never exited on its own โ€” name the signal and the spawn error, or the
119
+ // failure is indistinguishable from a real incompatibility.
120
+ const how = out.status === null ? `killed by ${out.signal ?? 'an unknown signal'}` : `exit ${out.status}`;
121
+ failures.push(`${verb} (${label}) ${how}`);
122
+ if (out.error) say(` spawn error: ${out.error.message}`);
123
+ say(` stdout: ${(out.stdout || '').trim().slice(0, 2500)}`);
124
+ say(` stderr: ${(out.stderr || '').trim().slice(0, 2500)}`);
125
+ say(` routes: ${server.requests.join(' | ')}`);
126
+ }
127
+ }
128
+ // A leg that asked the server for nothing proves nothing โ€” the "No tests found" failure mode.
129
+ if (server.requests.length === 0) failures.push(`${label}: the old CLI made no requests at all`);
130
+
131
+ // The push must have actually reached the version route, and what it sent is the thing under test:
132
+ // a body an older CLI builds carries NONE of the new members and leaves the envelope marker at 1.
133
+ if (server.pushed.length === 0) {
134
+ failures.push(`${label}: the old CLI never sent a version body`);
135
+ } else {
136
+ for (const body of server.pushed) {
137
+ const marker = body?.graph?.luaWorkflow;
138
+ if (marker !== 1) failures.push(`${label}: the old CLI sent luaWorkflow ${marker}, expected 1`);
139
+ const nodes = body?.graph?.definition?.graph ?? [];
140
+ const leaked = JSON.stringify(nodes).match(/"(taskClass|requires|modelReason|effort)"/g);
141
+ if (leaked) failures.push(`${label}: the old CLI's body carried ${[...new Set(leaked)].join(', ')}`);
142
+ say(` โœ“ ${TARGET} ยท push body ยท ${label} ยท luaWorkflow ${marker}, no new members`);
143
+ }
144
+ }
145
+ } finally {
146
+ await server.close();
147
+ }
148
+ return failures;
149
+ }
150
+
151
+ async function main() {
152
+ say(`\n๐Ÿงญ CLI compatibility matrix โ€” driving lua-cli@${TARGET} against the current shapes\n`);
153
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-compat-'));
154
+ const failures = [];
155
+ try {
156
+ // 1. Install the previous release into a scratch directory of its own.
157
+ say(` โ€ฆ installing lua-cli@${TARGET} into ${tmp}`);
158
+ fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'compat-host', private: true }), 'utf8');
159
+ execFileSync('npm', ['install', '--no-audit', '--no-fund', '--silent', `lua-cli@${TARGET}`], {
160
+ cwd: tmp,
161
+ stdio: 'inherit',
162
+ timeout: 300_000,
163
+ });
164
+ const cliEntry = path.join(tmp, 'node_modules', 'lua-cli', 'dist', 'index.js');
165
+ if (!fs.existsSync(cliEntry)) throw new Error(`installed lua-cli@${TARGET} has no dist/index.js at ${cliEntry}`);
166
+
167
+ // 2. A project directory the old CLI recognises. No credential file: the key rides the environment.
168
+ const projectDir = path.join(tmp, 'project');
169
+ fs.mkdirSync(projectDir, { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(projectDir, 'lua.skill.yaml'),
172
+ [
173
+ 'agent:',
174
+ ` agentId: ${AGENT_ID}`,
175
+ ' orgId: org_compat',
176
+ ' name: compat',
177
+ 'workflows:',
178
+ ' - name: compat',
179
+ ` workflowId: ${WORKFLOW_ID}`,
180
+ ' version: 1.0.0',
181
+ '',
182
+ ].join('\n'),
183
+ 'utf8'
184
+ );
185
+ // A project the OLD CLI can compile: nothing in it uses anything this change added, which is the point.
186
+ fs.mkdirSync(path.join(projectDir, 'src', 'workflows'), { recursive: true });
187
+ fs.writeFileSync(
188
+ path.join(projectDir, 'package.json'),
189
+ JSON.stringify({ name: 'compat', private: true, type: 'module', dependencies: { 'lua-cli': TARGET } }, null, 2),
190
+ 'utf8'
191
+ );
192
+ fs.symlinkSync(path.join(tmp, 'node_modules'), path.join(projectDir, 'node_modules'), 'dir');
193
+ fs.writeFileSync(
194
+ path.join(projectDir, 'tsconfig.json'),
195
+ JSON.stringify(
196
+ {
197
+ compilerOptions: {
198
+ target: 'ES2022',
199
+ module: 'ESNext',
200
+ moduleResolution: 'bundler',
201
+ strict: true,
202
+ skipLibCheck: true,
203
+ esModuleInterop: true,
204
+ outDir: 'dist',
205
+ },
206
+ include: ['src/**/*'],
207
+ },
208
+ null,
209
+ 2
210
+ ),
211
+ 'utf8'
212
+ );
213
+ fs.writeFileSync(
214
+ path.join(projectDir, 'src', 'workflows', 'compat.ts'),
215
+ [
216
+ "import { createWorkflow } from 'lua-cli';",
217
+ "import { z } from 'zod';",
218
+ '',
219
+ 'export const compat = createWorkflow({',
220
+ " name: 'compat',",
221
+ " description: 'compatibility matrix fixture',",
222
+ ' inputSchema: z.object({}),',
223
+ ' outputSchema: z.object({ text: z.string() }),',
224
+ '})',
225
+ " .agentStep('summarise', { agentId: '$self', toolScope: {}, prompt: 'hi' })",
226
+ ' .commit();',
227
+ '',
228
+ ].join('\n'),
229
+ 'utf8'
230
+ );
231
+ fs.writeFileSync(
232
+ path.join(projectDir, 'src', 'index.ts'),
233
+ [
234
+ "import { LuaAgent } from 'lua-cli';",
235
+ "import { compat } from './workflows/compat';",
236
+ '',
237
+ 'export default new LuaAgent({',
238
+ " name: 'compat',",
239
+ " persona: 'compatibility matrix fixture',",
240
+ ' workflows: [compat],',
241
+ '});',
242
+ '',
243
+ ].join('\n'),
244
+ 'utf8'
245
+ );
246
+
247
+ // 3. Both halves.
248
+ for (const withNewFields of [false, true]) {
249
+ failures.push(...(await runLeg({ withNewFields, cliEntry, projectDir })));
250
+ }
251
+ } finally {
252
+ fs.rmSync(tmp, { recursive: true, force: true });
253
+ }
254
+
255
+ if (failures.length) {
256
+ say(`\nโœ— compatibility matrix: ${failures.length} failure(s)`);
257
+ for (const f of failures) say(` โ€ข ${f}`);
258
+ process.exit(1);
259
+ }
260
+ say(`\nโœ“ compatibility matrix: lua-cli@${TARGET} pushes, views and watches against the current shapes.\n`);
261
+ }
262
+
263
+ main().catch((err) => {
264
+ process.stderr.write(`compatibility matrix failed to run: ${err?.stack || err}\n`);
265
+ process.exit(1);
266
+ });
@@ -20,7 +20,7 @@
20
20
  "inquirer": "^12.9.6",
21
21
  "stripe": "^17.5.0",
22
22
  "js-yaml": "^4.1.0",
23
- "lua-cli": "^3.35.0",
23
+ "lua-cli": "^3.37.0",
24
24
  "openai": "^5.23.0",
25
25
  "uuid": "^13.0.0",
26
26
  "zod": "^3.24.1"