@lotics/cli 0.51.0 → 0.52.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 +11 -0
- package/dist/app_commands.js +21 -2
- package/dist/app_commands.test.js +23 -1
- package/dist/client.d.ts +10 -0
- package/dist/client.js +17 -0
- package/dist/dev/server.js +51 -0
- package/dist/dev/wrapper_page.d.ts +2 -0
- package/dist/dev/wrapper_page.js +54 -1
- package/dist/src/cli.js +138 -4
- package/dist/starter_template.js +12 -1
- package/package.json +1 -1
package/dist/app_commands.d.ts
CHANGED
|
@@ -63,6 +63,17 @@ export declare function stampPulledManifest(projectDir: string, args: {
|
|
|
63
63
|
queries: Record<string, AppQueryDeclaration>;
|
|
64
64
|
agents: Record<string, AppAgentDeclaration>;
|
|
65
65
|
}): void;
|
|
66
|
+
/**
|
|
67
|
+
* A filesystem-safe SINGLE directory segment from an app's free-form display
|
|
68
|
+
* name, for the default create/pull target. App names may contain "/" (and
|
|
69
|
+
* other path-hostile characters); used verbatim as a directory, a name like
|
|
70
|
+
* "Nhập/Xuất Cont" splits into NESTED folders ("Nhập" → "Xuất Cont") — and a
|
|
71
|
+
* deploy then packages that nested copy into the source archive, so each later
|
|
72
|
+
* pull re-extracts and compounds it. Neutralize path separators + reserved
|
|
73
|
+
* characters to one segment, keeping spaces and unicode so the folder stays
|
|
74
|
+
* recognizable. An explicit `targetPath` still overrides this entirely.
|
|
75
|
+
*/
|
|
76
|
+
export declare function appDirName(name: string): string;
|
|
66
77
|
/**
|
|
67
78
|
* `lotics app create <name> [path]`
|
|
68
79
|
*
|
package/dist/app_commands.js
CHANGED
|
@@ -149,6 +149,25 @@ async function downloadToFile(url, destPath) {
|
|
|
149
149
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
150
150
|
fs.writeFileSync(destPath, buffer);
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* A filesystem-safe SINGLE directory segment from an app's free-form display
|
|
154
|
+
* name, for the default create/pull target. App names may contain "/" (and
|
|
155
|
+
* other path-hostile characters); used verbatim as a directory, a name like
|
|
156
|
+
* "Nhập/Xuất Cont" splits into NESTED folders ("Nhập" → "Xuất Cont") — and a
|
|
157
|
+
* deploy then packages that nested copy into the source archive, so each later
|
|
158
|
+
* pull re-extracts and compounds it. Neutralize path separators + reserved
|
|
159
|
+
* characters to one segment, keeping spaces and unicode so the folder stays
|
|
160
|
+
* recognizable. An explicit `targetPath` still overrides this entirely.
|
|
161
|
+
*/
|
|
162
|
+
export function appDirName(name) {
|
|
163
|
+
const cleaned = name
|
|
164
|
+
.replace(/[/\\]+/g, "-") // path separators — the core bug
|
|
165
|
+
.replace(/[<>:"|?*]/g, "") // other filesystem-reserved characters
|
|
166
|
+
.replace(/\s+/g, " ") // collapse whitespace runs
|
|
167
|
+
.trim()
|
|
168
|
+
.replace(/^[.\s-]+|[.\s-]+$/g, ""); // no leading/trailing dot/space/dash → never "."/".."/hidden/blank
|
|
169
|
+
return cleaned || "app";
|
|
170
|
+
}
|
|
152
171
|
/**
|
|
153
172
|
* `lotics app create <name> [path]`
|
|
154
173
|
*
|
|
@@ -164,7 +183,7 @@ async function downloadToFile(url, destPath) {
|
|
|
164
183
|
* with CLI releases. (Server-side build is the v2 path.)
|
|
165
184
|
*/
|
|
166
185
|
export async function appCreate(client, args) {
|
|
167
|
-
const targetPath = path.resolve(args.targetPath ?? args.name);
|
|
186
|
+
const targetPath = path.resolve(args.targetPath ?? appDirName(args.name));
|
|
168
187
|
if (fs.existsSync(targetPath)) {
|
|
169
188
|
const entries = fs.readdirSync(targetPath);
|
|
170
189
|
if (entries.length > 0) {
|
|
@@ -259,7 +278,7 @@ export async function appPull(client, args) {
|
|
|
259
278
|
}
|
|
260
279
|
const version = await client.getAppVersion(app.id, app.current_version_id);
|
|
261
280
|
const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
|
|
262
|
-
const targetPath = path.resolve(args.targetPath ?? app.name);
|
|
281
|
+
const targetPath = path.resolve(args.targetPath ?? appDirName(app.name));
|
|
263
282
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
264
283
|
// Download to a temp file because `tar -xz` reads from a real path.
|
|
265
284
|
const tmpFile = path.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { stampPulledManifest, undeclaredCapabilities } from "./app_commands.js";
|
|
5
|
+
import { stampPulledManifest, undeclaredCapabilities, appDirName } from "./app_commands.js";
|
|
6
6
|
/**
|
|
7
7
|
* `appPull` reads workflows from the live App row (server response), NOT from
|
|
8
8
|
* the manifest embedded in the extracted source archive. The frozen archive
|
|
@@ -135,3 +135,25 @@ describe("undeclaredCapabilities", () => {
|
|
|
135
135
|
expect(undeclaredCapabilities(`await deleteComment(id);`, {})).toEqual(["comments"]);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
+
describe("appDirName", () => {
|
|
139
|
+
it("collapses a slash in the name to a single segment (the create/pull nesting bug)", () => {
|
|
140
|
+
// A "/" in the name used to split the create/pull target into nested folders
|
|
141
|
+
// ("Nhập" → "Xuất Cont") and compound on each pull.
|
|
142
|
+
expect(appDirName("Nhập/Xuất Cont")).toBe("Nhập-Xuất Cont");
|
|
143
|
+
expect(appDirName("a/b/c")).toBe("a-b-c");
|
|
144
|
+
expect(appDirName("a\\b")).toBe("a-b");
|
|
145
|
+
});
|
|
146
|
+
it("leaves an ordinary name untouched (no regression for the common case)", () => {
|
|
147
|
+
expect(appDirName("Sales Tracker")).toBe("Sales Tracker");
|
|
148
|
+
expect(appDirName("crm")).toBe("crm");
|
|
149
|
+
});
|
|
150
|
+
it("strips reserved characters and collapses whitespace", () => {
|
|
151
|
+
expect(appDirName('Report: "Q1" <draft>')).toBe("Report Q1 draft");
|
|
152
|
+
});
|
|
153
|
+
it("never yields a traversal, hidden, or empty directory", () => {
|
|
154
|
+
expect(appDirName("../etc")).toBe("etc"); // "/"→"-", then leading dot/dash run stripped
|
|
155
|
+
expect(appDirName(".hidden")).toBe("hidden");
|
|
156
|
+
expect(appDirName(" ")).toBe("app");
|
|
157
|
+
expect(appDirName("///")).toBe("app"); // separators → "-", a dir of only "-" falls back
|
|
158
|
+
});
|
|
159
|
+
});
|
package/dist/client.d.ts
CHANGED
|
@@ -216,6 +216,16 @@ export declare class LoticsClient {
|
|
|
216
216
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
217
217
|
*/
|
|
218
218
|
appWorkflow(app_id: string, alias: string, inputs: unknown): Promise<unknown>;
|
|
219
|
+
/**
|
|
220
|
+
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
221
|
+
* caller reads `res.body`). Unlike `request`, this does not buffer/parse the
|
|
222
|
+
* body — it's the SSE stream the `lotics app dev` harness proxies to the
|
|
223
|
+
* iframe. Mirrors POST /v1/apps/{app_id}/agents/{alias}/runs.
|
|
224
|
+
*/
|
|
225
|
+
appAgentRunStream(app_id: string, alias: string, body: {
|
|
226
|
+
session_id: string;
|
|
227
|
+
input: Record<string, unknown>;
|
|
228
|
+
}, signal?: AbortSignal): Promise<Response>;
|
|
219
229
|
/**
|
|
220
230
|
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
221
231
|
* POST /v1/apps/{app_id}/files/upload-url.
|
package/dist/client.js
CHANGED
|
@@ -202,6 +202,23 @@ export class LoticsClient {
|
|
|
202
202
|
async appWorkflow(app_id, alias, inputs) {
|
|
203
203
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/execute`, { inputs });
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
207
|
+
* caller reads `res.body`). Unlike `request`, this does not buffer/parse the
|
|
208
|
+
* body — it's the SSE stream the `lotics app dev` harness proxies to the
|
|
209
|
+
* iframe. Mirrors POST /v1/apps/{app_id}/agents/{alias}/runs.
|
|
210
|
+
*/
|
|
211
|
+
async appAgentRunStream(app_id, alias, body, signal) {
|
|
212
|
+
const res = await fetch(`${this.baseUrl}/v1/apps/${encodeURIComponent(app_id)}/agents/${encodeURIComponent(alias)}/runs`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
|
|
215
|
+
body: JSON.stringify(body),
|
|
216
|
+
signal,
|
|
217
|
+
});
|
|
218
|
+
if (!res.ok)
|
|
219
|
+
await this.throwResponseError(res);
|
|
220
|
+
return res;
|
|
221
|
+
}
|
|
205
222
|
/**
|
|
206
223
|
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
207
224
|
* POST /v1/apps/{app_id}/files/upload-url.
|
package/dist/dev/server.js
CHANGED
|
@@ -116,6 +116,57 @@ export async function startDevServer(args) {
|
|
|
116
116
|
}
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
+
// Streaming agent run — the one op /_rpc can't carry (its response is a
|
|
120
|
+
// stream, not a single JSON value). Open the run with the CLI key and pipe
|
|
121
|
+
// the SSE body straight back to the wrapper page, which forwards chunks to
|
|
122
|
+
// the iframe. A wrapper disconnect (the app aborted) aborts the upstream.
|
|
123
|
+
if (req.method === "POST" && url === "/_agent_run") {
|
|
124
|
+
// Abort the upstream run only when the CLIENT disconnects — keyed on the
|
|
125
|
+
// RESPONSE closing before we finished writing it. `req.on("close")` is
|
|
126
|
+
// wrong here: in modern Node it fires as soon as the request BODY stream
|
|
127
|
+
// ends (right after readJson consumes it), which would abort every run the
|
|
128
|
+
// instant it started — a silent empty 200, no chunks.
|
|
129
|
+
const ac = new AbortController();
|
|
130
|
+
res.on("close", () => {
|
|
131
|
+
if (!res.writableEnded)
|
|
132
|
+
ac.abort();
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const body = await readJson(req);
|
|
136
|
+
const p = (body.payload ?? {});
|
|
137
|
+
if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
|
|
138
|
+
throw new Error("agentRun payload must include `alias` and `session_id`");
|
|
139
|
+
}
|
|
140
|
+
const upstream = await args.client.appAgentRunStream(body.app_id, p.alias, { session_id: p.session_id, input: p.input ?? {} }, ac.signal);
|
|
141
|
+
if (!upstream.body)
|
|
142
|
+
throw new Error("agent run returned no stream body");
|
|
143
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" });
|
|
144
|
+
const reader = upstream.body.getReader();
|
|
145
|
+
for (;;) {
|
|
146
|
+
const { value, done } = await reader.read();
|
|
147
|
+
if (done)
|
|
148
|
+
break;
|
|
149
|
+
res.write(Buffer.from(value));
|
|
150
|
+
}
|
|
151
|
+
res.end();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (ac.signal.aborted) {
|
|
155
|
+
res.end();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
process.stderr.write(`[agent_run] ERROR ${message}\n`);
|
|
160
|
+
if (!res.headersSent) {
|
|
161
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
162
|
+
res.end(JSON.stringify({ message }));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
119
170
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
120
171
|
res.end("Not Found");
|
|
121
172
|
});
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
|
|
21
21
|
* iframe → wrapper: { id: number, op: string, payload: unknown }
|
|
22
22
|
* wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
|
|
23
|
+
* streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
|
|
24
|
+
* the iframe aborts with { id, type: "abort" }.
|
|
23
25
|
*/
|
|
24
26
|
export interface WrapperPageArgs {
|
|
25
27
|
app_name: string;
|
package/dist/dev/wrapper_page.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
|
|
21
21
|
* iframe → wrapper: { id: number, op: string, payload: unknown }
|
|
22
22
|
* wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
|
|
23
|
+
* streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
|
|
24
|
+
* the iframe aborts with { id, type: "abort" }.
|
|
23
25
|
*/
|
|
24
26
|
export function buildWrapperPage(args) {
|
|
25
27
|
const { app_name, app_id, workspace_id, vite_url, api_url } = args;
|
|
@@ -161,10 +163,61 @@ export function buildWrapperPage(args) {
|
|
|
161
163
|
return undefined;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
// Streaming agent runs (op "agentRun"): the response is a stream, so it
|
|
167
|
+
// can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
|
|
168
|
+
// body, and forward chunks to the iframe as the SDK's bridged streaming
|
|
169
|
+
// protocol expects (stream-chunk* -> stream-end). Mirrors the production
|
|
170
|
+
// host in frontend/features/app_ui/app_iframe_host.tsx.
|
|
171
|
+
const streamingRuns = {};
|
|
172
|
+
async function handleAgentRun(id, payload) {
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
streamingRuns[id] = controller;
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch("/_agent_run", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "content-type": "application/json" },
|
|
179
|
+
body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok || !res.body) {
|
|
183
|
+
let detail = await res.text().catch(function () { return ""; });
|
|
184
|
+
try { detail = JSON.parse(detail).message || detail; } catch (_) {}
|
|
185
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
186
|
+
}
|
|
187
|
+
const reader = res.body.getReader();
|
|
188
|
+
const decoder = new TextDecoder();
|
|
189
|
+
for (;;) {
|
|
190
|
+
const r = await reader.read();
|
|
191
|
+
if (r.done) break;
|
|
192
|
+
iframe.contentWindow.postMessage(
|
|
193
|
+
{ id: id, type: "stream-chunk", chunk: decoder.decode(r.value, { stream: true }) },
|
|
194
|
+
VITE_ORIGIN
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const tail = decoder.decode();
|
|
198
|
+
if (tail) iframe.contentWindow.postMessage({ id: id, type: "stream-chunk", chunk: tail }, VITE_ORIGIN);
|
|
199
|
+
iframe.contentWindow.postMessage({ id: id, type: "stream-end" }, VITE_ORIGIN);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (controller.signal.aborted) return;
|
|
202
|
+
const message = err && err.message ? err.message : String(err);
|
|
203
|
+
iframe.contentWindow.postMessage({ id: id, type: "error", message: message }, VITE_ORIGIN);
|
|
204
|
+
} finally {
|
|
205
|
+
delete streamingRuns[id];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
164
209
|
window.addEventListener("message", async function (event) {
|
|
165
210
|
if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
|
|
166
211
|
const msg = event.data;
|
|
167
|
-
if (!msg || typeof msg.id !== "number"
|
|
212
|
+
if (!msg || typeof msg.id !== "number") return;
|
|
213
|
+
// Abort an in-flight streaming run (control message, carries no op).
|
|
214
|
+
if (msg.type === "abort") {
|
|
215
|
+
if (streamingRuns[msg.id]) { streamingRuns[msg.id].abort(); delete streamingRuns[msg.id]; }
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (typeof msg.op !== "string") return;
|
|
219
|
+
// Streaming agent run — many messages back, not a single result.
|
|
220
|
+
if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
|
|
168
221
|
const startedAt = performance.now();
|
|
169
222
|
try {
|
|
170
223
|
const data = msg.op === "upload"
|
package/dist/src/cli.js
CHANGED
|
@@ -29791,6 +29791,25 @@ var LoticsClient = class {
|
|
|
29791
29791
|
{ inputs }
|
|
29792
29792
|
);
|
|
29793
29793
|
}
|
|
29794
|
+
/**
|
|
29795
|
+
* Open a streaming agent run and return the RAW streamed `Response` (the
|
|
29796
|
+
* caller reads `res.body`). Unlike `request`, this does not buffer/parse the
|
|
29797
|
+
* body — it's the SSE stream the `lotics app dev` harness proxies to the
|
|
29798
|
+
* iframe. Mirrors POST /v1/apps/{app_id}/agents/{alias}/runs.
|
|
29799
|
+
*/
|
|
29800
|
+
async appAgentRunStream(app_id, alias, body, signal) {
|
|
29801
|
+
const res = await fetch(
|
|
29802
|
+
`${this.baseUrl}/v1/apps/${encodeURIComponent(app_id)}/agents/${encodeURIComponent(alias)}/runs`,
|
|
29803
|
+
{
|
|
29804
|
+
method: "POST",
|
|
29805
|
+
headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
|
|
29806
|
+
body: JSON.stringify(body),
|
|
29807
|
+
signal
|
|
29808
|
+
}
|
|
29809
|
+
);
|
|
29810
|
+
if (!res.ok) await this.throwResponseError(res);
|
|
29811
|
+
return res;
|
|
29812
|
+
}
|
|
29794
29813
|
/**
|
|
29795
29814
|
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
29796
29815
|
* POST /v1/apps/{app_id}/files/upload-url.
|
|
@@ -30304,7 +30323,12 @@ function buildStarterTemplate(args) {
|
|
|
30304
30323
|
// Restrict tsc to the project's own sources. tsgo follows
|
|
30305
30324
|
// transitive imports into node_modules — strict rules
|
|
30306
30325
|
// (noUnusedLocals etc.) would fire on @lotics/ui's source.
|
|
30307
|
-
|
|
30326
|
+
// `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
|
|
30327
|
+
// SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
|
|
30328
|
+
// generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
|
|
30329
|
+
// fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
|
|
30330
|
+
// base, which IS read. (GAP-38.)
|
|
30331
|
+
include: ["src", ".lotics/**/*"],
|
|
30308
30332
|
exclude: ["node_modules"]
|
|
30309
30333
|
},
|
|
30310
30334
|
null,
|
|
@@ -30450,6 +30474,12 @@ export default defineConfig({
|
|
|
30450
30474
|
"@react-native-picker/picker",
|
|
30451
30475
|
"@testing-library/react",
|
|
30452
30476
|
"@testing-library/dom",
|
|
30477
|
+
// @lotics/app-sdk ships compiled dist/ JS (unlike @lotics/ui, which
|
|
30478
|
+
// ships source vitest transforms inline) \u2014 pre-bundled into its own
|
|
30479
|
+
// chunk, its react import splits from the shared instance and nulls
|
|
30480
|
+
// the hooks dispatcher the instant a hook (useAgentRun / useQuery)
|
|
30481
|
+
// runs in a rendered App tree.
|
|
30482
|
+
"@lotics/app-sdk",
|
|
30453
30483
|
],
|
|
30454
30484
|
esbuildOptions: {
|
|
30455
30485
|
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|
|
@@ -31023,10 +31053,61 @@ function buildWrapperPage(args) {
|
|
|
31023
31053
|
return undefined;
|
|
31024
31054
|
}
|
|
31025
31055
|
|
|
31056
|
+
// Streaming agent runs (op "agentRun"): the response is a stream, so it
|
|
31057
|
+
// can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
|
|
31058
|
+
// body, and forward chunks to the iframe as the SDK's bridged streaming
|
|
31059
|
+
// protocol expects (stream-chunk* -> stream-end). Mirrors the production
|
|
31060
|
+
// host in frontend/features/app_ui/app_iframe_host.tsx.
|
|
31061
|
+
const streamingRuns = {};
|
|
31062
|
+
async function handleAgentRun(id, payload) {
|
|
31063
|
+
const controller = new AbortController();
|
|
31064
|
+
streamingRuns[id] = controller;
|
|
31065
|
+
try {
|
|
31066
|
+
const res = await fetch("/_agent_run", {
|
|
31067
|
+
method: "POST",
|
|
31068
|
+
headers: { "content-type": "application/json" },
|
|
31069
|
+
body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
|
|
31070
|
+
signal: controller.signal,
|
|
31071
|
+
});
|
|
31072
|
+
if (!res.ok || !res.body) {
|
|
31073
|
+
let detail = await res.text().catch(function () { return ""; });
|
|
31074
|
+
try { detail = JSON.parse(detail).message || detail; } catch (_) {}
|
|
31075
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
31076
|
+
}
|
|
31077
|
+
const reader = res.body.getReader();
|
|
31078
|
+
const decoder = new TextDecoder();
|
|
31079
|
+
for (;;) {
|
|
31080
|
+
const r = await reader.read();
|
|
31081
|
+
if (r.done) break;
|
|
31082
|
+
iframe.contentWindow.postMessage(
|
|
31083
|
+
{ id: id, type: "stream-chunk", chunk: decoder.decode(r.value, { stream: true }) },
|
|
31084
|
+
VITE_ORIGIN
|
|
31085
|
+
);
|
|
31086
|
+
}
|
|
31087
|
+
const tail = decoder.decode();
|
|
31088
|
+
if (tail) iframe.contentWindow.postMessage({ id: id, type: "stream-chunk", chunk: tail }, VITE_ORIGIN);
|
|
31089
|
+
iframe.contentWindow.postMessage({ id: id, type: "stream-end" }, VITE_ORIGIN);
|
|
31090
|
+
} catch (err) {
|
|
31091
|
+
if (controller.signal.aborted) return;
|
|
31092
|
+
const message = err && err.message ? err.message : String(err);
|
|
31093
|
+
iframe.contentWindow.postMessage({ id: id, type: "error", message: message }, VITE_ORIGIN);
|
|
31094
|
+
} finally {
|
|
31095
|
+
delete streamingRuns[id];
|
|
31096
|
+
}
|
|
31097
|
+
}
|
|
31098
|
+
|
|
31026
31099
|
window.addEventListener("message", async function (event) {
|
|
31027
31100
|
if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
|
|
31028
31101
|
const msg = event.data;
|
|
31029
|
-
if (!msg || typeof msg.id !== "number"
|
|
31102
|
+
if (!msg || typeof msg.id !== "number") return;
|
|
31103
|
+
// Abort an in-flight streaming run (control message, carries no op).
|
|
31104
|
+
if (msg.type === "abort") {
|
|
31105
|
+
if (streamingRuns[msg.id]) { streamingRuns[msg.id].abort(); delete streamingRuns[msg.id]; }
|
|
31106
|
+
return;
|
|
31107
|
+
}
|
|
31108
|
+
if (typeof msg.op !== "string") return;
|
|
31109
|
+
// Streaming agent run \u2014 many messages back, not a single result.
|
|
31110
|
+
if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
|
|
31030
31111
|
const startedAt = performance.now();
|
|
31031
31112
|
try {
|
|
31032
31113
|
const data = msg.op === "upload"
|
|
@@ -31142,6 +31223,49 @@ async function startDevServer(args) {
|
|
|
31142
31223
|
}
|
|
31143
31224
|
return;
|
|
31144
31225
|
}
|
|
31226
|
+
if (req.method === "POST" && url === "/_agent_run") {
|
|
31227
|
+
const ac = new AbortController();
|
|
31228
|
+
res.on("close", () => {
|
|
31229
|
+
if (!res.writableEnded) ac.abort();
|
|
31230
|
+
});
|
|
31231
|
+
try {
|
|
31232
|
+
const body = await readJson(req);
|
|
31233
|
+
const p = body.payload ?? {};
|
|
31234
|
+
if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
|
|
31235
|
+
throw new Error("agentRun payload must include `alias` and `session_id`");
|
|
31236
|
+
}
|
|
31237
|
+
const upstream = await args.client.appAgentRunStream(
|
|
31238
|
+
body.app_id,
|
|
31239
|
+
p.alias,
|
|
31240
|
+
{ session_id: p.session_id, input: p.input ?? {} },
|
|
31241
|
+
ac.signal
|
|
31242
|
+
);
|
|
31243
|
+
if (!upstream.body) throw new Error("agent run returned no stream body");
|
|
31244
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" });
|
|
31245
|
+
const reader = upstream.body.getReader();
|
|
31246
|
+
for (; ; ) {
|
|
31247
|
+
const { value, done } = await reader.read();
|
|
31248
|
+
if (done) break;
|
|
31249
|
+
res.write(Buffer.from(value));
|
|
31250
|
+
}
|
|
31251
|
+
res.end();
|
|
31252
|
+
} catch (err2) {
|
|
31253
|
+
if (ac.signal.aborted) {
|
|
31254
|
+
res.end();
|
|
31255
|
+
return;
|
|
31256
|
+
}
|
|
31257
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
31258
|
+
process.stderr.write(`[agent_run] ERROR ${message}
|
|
31259
|
+
`);
|
|
31260
|
+
if (!res.headersSent) {
|
|
31261
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
31262
|
+
res.end(JSON.stringify({ message }));
|
|
31263
|
+
} else {
|
|
31264
|
+
res.end();
|
|
31265
|
+
}
|
|
31266
|
+
}
|
|
31267
|
+
return;
|
|
31268
|
+
}
|
|
31145
31269
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
31146
31270
|
res.end("Not Found");
|
|
31147
31271
|
});
|
|
@@ -31559,8 +31683,12 @@ async function downloadToFile(url, destPath) {
|
|
|
31559
31683
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
31560
31684
|
fs3.writeFileSync(destPath, buffer);
|
|
31561
31685
|
}
|
|
31686
|
+
function appDirName(name) {
|
|
31687
|
+
const cleaned = name.replace(/[/\\]+/g, "-").replace(/[<>:"|?*]/g, "").replace(/\s+/g, " ").trim().replace(/^[.\s-]+|[.\s-]+$/g, "");
|
|
31688
|
+
return cleaned || "app";
|
|
31689
|
+
}
|
|
31562
31690
|
async function appCreate(client, args) {
|
|
31563
|
-
const targetPath = path4.resolve(args.targetPath ?? args.name);
|
|
31691
|
+
const targetPath = path4.resolve(args.targetPath ?? appDirName(args.name));
|
|
31564
31692
|
if (fs3.existsSync(targetPath)) {
|
|
31565
31693
|
const entries = fs3.readdirSync(targetPath);
|
|
31566
31694
|
if (entries.length > 0) {
|
|
@@ -31621,7 +31749,7 @@ async function appPull(client, args) {
|
|
|
31621
31749
|
}
|
|
31622
31750
|
const version = await client.getAppVersion(app.id, app.current_version_id);
|
|
31623
31751
|
const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
|
|
31624
|
-
const targetPath = path4.resolve(args.targetPath ?? app.name);
|
|
31752
|
+
const targetPath = path4.resolve(args.targetPath ?? appDirName(app.name));
|
|
31625
31753
|
fs3.mkdirSync(targetPath, { recursive: true });
|
|
31626
31754
|
const tmpFile = path4.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
|
|
31627
31755
|
console.error(`Downloading source archive...`);
|
|
@@ -38305,6 +38433,12 @@ function emptyStyleResolver() {
|
|
|
38305
38433
|
var xmlParser3 = new XMLParser({
|
|
38306
38434
|
ignoreAttributes: false,
|
|
38307
38435
|
attributeNamePrefix: "@_",
|
|
38436
|
+
// Shared-string and workbook text is textual BY DEFINITION (tax IDs, phone
|
|
38437
|
+
// numbers, account codes, sheet/defined names). fast-xml-parser's default tag
|
|
38438
|
+
// coercion would parse `<t>0312345678</t>` as the number 312345678 — silently
|
|
38439
|
+
// dropping the leading zero before we can read it. Cell `<v>` numerics are
|
|
38440
|
+
// parsed deliberately (parseFloat) in ooxml_sheet.ts, so this never affects them.
|
|
38441
|
+
parseTagValue: false,
|
|
38308
38442
|
trimValues: false,
|
|
38309
38443
|
processEntities: false,
|
|
38310
38444
|
isArray: (tagName) => tagName === "sheet" || tagName === "si" || tagName === "r" || tagName === "Relationship" || tagName === "definedName"
|
package/dist/starter_template.js
CHANGED
|
@@ -126,7 +126,12 @@ export function buildStarterTemplate(args) {
|
|
|
126
126
|
// Restrict tsc to the project's own sources. tsgo follows
|
|
127
127
|
// transitive imports into node_modules — strict rules
|
|
128
128
|
// (noUnusedLocals etc.) would fire on @lotics/ui's source.
|
|
129
|
-
|
|
129
|
+
// `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
|
|
130
|
+
// SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
|
|
131
|
+
// generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
|
|
132
|
+
// fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
|
|
133
|
+
// base, which IS read. (GAP-38.)
|
|
134
|
+
include: ["src", ".lotics/**/*"],
|
|
130
135
|
exclude: ["node_modules"],
|
|
131
136
|
}, null, 2) + "\n",
|
|
132
137
|
},
|
|
@@ -269,6 +274,12 @@ export default defineConfig({
|
|
|
269
274
|
"@react-native-picker/picker",
|
|
270
275
|
"@testing-library/react",
|
|
271
276
|
"@testing-library/dom",
|
|
277
|
+
// @lotics/app-sdk ships compiled dist/ JS (unlike @lotics/ui, which
|
|
278
|
+
// ships source vitest transforms inline) — pre-bundled into its own
|
|
279
|
+
// chunk, its react import splits from the shared instance and nulls
|
|
280
|
+
// the hooks dispatcher the instant a hook (useAgentRun / useQuery)
|
|
281
|
+
// runs in a rendered App tree.
|
|
282
|
+
"@lotics/app-sdk",
|
|
272
283
|
],
|
|
273
284
|
esbuildOptions: {
|
|
274
285
|
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
|