@lotics/cli 0.56.0 → 0.57.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/app_commands.d.ts +1 -1
- package/dist/client.d.ts +8 -0
- package/dist/client.js +59 -1
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +47 -0
- package/dist/src/cli.js +1634 -1387
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type AppQueryDeclaration = {
|
|
|
31
31
|
* `"alias": { instructions, tool_names, model_id, inputs?, outputs? }`
|
|
32
32
|
*
|
|
33
33
|
* A read-only reflection of the live App row (`apps.agents`, owned by
|
|
34
|
-
* `set_app_agent`), refreshed by `lotics app pull` and used only to codegen
|
|
34
|
+
* `set_app_agent` / `remove_app_agent`), refreshed by `lotics app pull` and used only to codegen
|
|
35
35
|
* `useAgentRun` typings. `inputs`/`outputs` drive the typed call site; the rest
|
|
36
36
|
* is carried for fidelity.
|
|
37
37
|
*/
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error message for a non-ok response. A genuine JSON error (a 4xx carrying
|
|
3
|
+
* a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
|
|
4
|
+
* 5xx, or a JSON body without a `message` falls back to a body-free,
|
|
5
|
+
* status-derived message. `parsed` is the JSON.parse of the body, or `null`.
|
|
6
|
+
* In parity (by value, no shared dep) with `packages/app-sdk/src/rpc.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function transportErrorMessage(status: number, parsed: unknown): string;
|
|
1
9
|
/** One sort key forwarded to the app query RPC (wire shape of a `TableRecordSort` entry). */
|
|
2
10
|
export interface AppQuerySortKey {
|
|
3
11
|
field_key: string;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A user-facing message for a transport/gateway failure — derived from the HTTP
|
|
5
|
+
* status, never from the response body. A 524 (edge timeout on a long run), any
|
|
6
|
+
* 5xx, or a non-JSON body (an HTML error page) must NOT surface its raw body. In
|
|
7
|
+
* parity (by value, no shared dep) with the published transport in
|
|
8
|
+
* `packages/app-sdk/src/rpc.ts`.
|
|
9
|
+
*/
|
|
10
|
+
function gatewayErrorMessage(status) {
|
|
11
|
+
if (status === 524) {
|
|
12
|
+
return "The request took too long to finish (gateway timeout). It may still be running — check back in a moment, or try again.";
|
|
13
|
+
}
|
|
14
|
+
if (status >= 500) {
|
|
15
|
+
return "The service is temporarily unavailable. Please try again shortly.";
|
|
16
|
+
}
|
|
17
|
+
return "The service returned an unexpected response. Please try again.";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The error message for a non-ok response. A genuine JSON error (a 4xx carrying
|
|
21
|
+
* a `message`) surfaces verbatim; a non-JSON body (a gateway HTML page), any
|
|
22
|
+
* 5xx, or a JSON body without a `message` falls back to a body-free,
|
|
23
|
+
* status-derived message. `parsed` is the JSON.parse of the body, or `null`.
|
|
24
|
+
* In parity (by value, no shared dep) with `packages/app-sdk/src/rpc.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export function transportErrorMessage(status, parsed) {
|
|
27
|
+
const jsonMessage = parsed && typeof parsed.message === "string"
|
|
28
|
+
? parsed.message
|
|
29
|
+
: null;
|
|
30
|
+
return parsed === null || status >= 500 || jsonMessage === null
|
|
31
|
+
? gatewayErrorMessage(status)
|
|
32
|
+
: jsonMessage;
|
|
33
|
+
}
|
|
3
34
|
function findAvailableFilename(dir, filename, reserved) {
|
|
4
35
|
// `reserved` tracks absolute paths claimed by in-flight downloads in the same
|
|
5
36
|
// batch — required for parallel callers because the file may not be on disk
|
|
@@ -200,7 +231,34 @@ export class LoticsClient {
|
|
|
200
231
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
201
232
|
*/
|
|
202
233
|
async appWorkflow(app_id, alias, inputs) {
|
|
203
|
-
|
|
234
|
+
// Its own transport (not the generic `request`, whose `throwResponseError`
|
|
235
|
+
// shape is the CLI contract elsewhere): a transport/gateway failure resolves
|
|
236
|
+
// to a `WorkflowResult` error `{ status, message }` — never a thrown HTML
|
|
237
|
+
// body — so the dev RPC bridge forwards `{status:"error"}` to the iframe,
|
|
238
|
+
// matching the deployed standalone SDK (`@lotics/app-sdk` standaloneWorkflow).
|
|
239
|
+
const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/execute`;
|
|
240
|
+
const headers = this.buildHeaders();
|
|
241
|
+
headers["Content-Type"] = "application/json";
|
|
242
|
+
let response;
|
|
243
|
+
try {
|
|
244
|
+
response = await fetch(url, { method: "POST", headers, body: JSON.stringify({ inputs }) });
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { status: "error", message: err instanceof Error ? err.message : "The workflow request failed." };
|
|
248
|
+
}
|
|
249
|
+
const text = await response.text();
|
|
250
|
+
let parsed = null;
|
|
251
|
+
if (text) {
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(text);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// non-JSON body (e.g. a gateway HTML error page) — never echoed
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (response.ok)
|
|
260
|
+
return parsed ?? {};
|
|
261
|
+
return { status: "error", message: transportErrorMessage(response.status, parsed) };
|
|
204
262
|
}
|
|
205
263
|
/**
|
|
206
264
|
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { LoticsClient, transportErrorMessage } from "./client.js";
|
|
3
|
+
describe("transportErrorMessage", () => {
|
|
4
|
+
it("returns a body-free gateway message for a non-JSON 524 (never the HTML)", () => {
|
|
5
|
+
const msg = transportErrorMessage(524, null);
|
|
6
|
+
expect(msg).not.toMatch(/<html|<!DOCTYPE/i);
|
|
7
|
+
expect(msg.toLowerCase()).toContain("gateway timeout");
|
|
8
|
+
});
|
|
9
|
+
it("returns a friendly message for any 5xx, ignoring a JSON body", () => {
|
|
10
|
+
expect(transportErrorMessage(503, { message: "internal detail" })).not.toBe("internal detail");
|
|
11
|
+
expect(transportErrorMessage(503, { message: "internal detail" })).not.toMatch(/<html/i);
|
|
12
|
+
});
|
|
13
|
+
it("surfaces a genuine 4xx JSON error message verbatim", () => {
|
|
14
|
+
expect(transportErrorMessage(400, { message: "record_id is required" })).toBe("record_id is required");
|
|
15
|
+
});
|
|
16
|
+
it("never leaks a non-JSON 4xx body (WAF / redirect HTML)", () => {
|
|
17
|
+
expect(transportErrorMessage(403, null)).not.toMatch(/<html|<!DOCTYPE/i);
|
|
18
|
+
});
|
|
19
|
+
it("falls back to a friendly message for a JSON body with no message field", () => {
|
|
20
|
+
expect(transportErrorMessage(409, { error_code: "CONFLICT" })).toBeTruthy();
|
|
21
|
+
expect(transportErrorMessage(409, { error_code: "CONFLICT" })).not.toMatch(/<html/i);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("LoticsClient.appWorkflow", () => {
|
|
25
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
26
|
+
const client = new LoticsClient({ apiKey: "ltk_test", workspaceId: "wsp_test" });
|
|
27
|
+
it("normalizes a 524 HTML gateway response to a WorkflowResult error (no raw HTML)", async () => {
|
|
28
|
+
const html = "<!DOCTYPE html><html><head><title>error</title></head><body>524: A timeout occurred</body></html>";
|
|
29
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response(html, { status: 524 })));
|
|
30
|
+
const result = (await client.appWorkflow("app_1", "wf", {}));
|
|
31
|
+
expect(result.status).toBe("error");
|
|
32
|
+
expect(result.message).not.toMatch(/<html|<!DOCTYPE/i);
|
|
33
|
+
});
|
|
34
|
+
it("passes a 200 workflow result through unchanged", async () => {
|
|
35
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ status: "success", data: { x: 1 } }), { status: 200 })));
|
|
36
|
+
const result = await client.appWorkflow("app_1", "wf", {});
|
|
37
|
+
expect(result).toEqual({ status: "success", data: { x: 1 } });
|
|
38
|
+
});
|
|
39
|
+
it("returns a WorkflowResult error (not a throw) on a network failure", async () => {
|
|
40
|
+
vi.stubGlobal("fetch", vi.fn(async () => {
|
|
41
|
+
throw new Error("ECONNREFUSED");
|
|
42
|
+
}));
|
|
43
|
+
const result = (await client.appWorkflow("app_1", "wf", {}));
|
|
44
|
+
expect(result.status).toBe("error");
|
|
45
|
+
expect(result.message).toContain("ECONNREFUSED");
|
|
46
|
+
});
|
|
47
|
+
});
|