@lotics/cli 0.51.0 → 0.51.1

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.
@@ -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
  *
@@ -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.
@@ -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;
@@ -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" || typeof msg.op !== "string") return;
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
- include: ["src", ".lotics"],
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,
@@ -31023,10 +31047,61 @@ function buildWrapperPage(args) {
31023
31047
  return undefined;
31024
31048
  }
31025
31049
 
31050
+ // Streaming agent runs (op "agentRun"): the response is a stream, so it
31051
+ // can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
31052
+ // body, and forward chunks to the iframe as the SDK's bridged streaming
31053
+ // protocol expects (stream-chunk* -> stream-end). Mirrors the production
31054
+ // host in frontend/features/app_ui/app_iframe_host.tsx.
31055
+ const streamingRuns = {};
31056
+ async function handleAgentRun(id, payload) {
31057
+ const controller = new AbortController();
31058
+ streamingRuns[id] = controller;
31059
+ try {
31060
+ const res = await fetch("/_agent_run", {
31061
+ method: "POST",
31062
+ headers: { "content-type": "application/json" },
31063
+ body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
31064
+ signal: controller.signal,
31065
+ });
31066
+ if (!res.ok || !res.body) {
31067
+ let detail = await res.text().catch(function () { return ""; });
31068
+ try { detail = JSON.parse(detail).message || detail; } catch (_) {}
31069
+ throw new Error(detail || ("HTTP " + res.status));
31070
+ }
31071
+ const reader = res.body.getReader();
31072
+ const decoder = new TextDecoder();
31073
+ for (;;) {
31074
+ const r = await reader.read();
31075
+ if (r.done) break;
31076
+ iframe.contentWindow.postMessage(
31077
+ { id: id, type: "stream-chunk", chunk: decoder.decode(r.value, { stream: true }) },
31078
+ VITE_ORIGIN
31079
+ );
31080
+ }
31081
+ const tail = decoder.decode();
31082
+ if (tail) iframe.contentWindow.postMessage({ id: id, type: "stream-chunk", chunk: tail }, VITE_ORIGIN);
31083
+ iframe.contentWindow.postMessage({ id: id, type: "stream-end" }, VITE_ORIGIN);
31084
+ } catch (err) {
31085
+ if (controller.signal.aborted) return;
31086
+ const message = err && err.message ? err.message : String(err);
31087
+ iframe.contentWindow.postMessage({ id: id, type: "error", message: message }, VITE_ORIGIN);
31088
+ } finally {
31089
+ delete streamingRuns[id];
31090
+ }
31091
+ }
31092
+
31026
31093
  window.addEventListener("message", async function (event) {
31027
31094
  if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
31028
31095
  const msg = event.data;
31029
- if (!msg || typeof msg.id !== "number" || typeof msg.op !== "string") return;
31096
+ if (!msg || typeof msg.id !== "number") return;
31097
+ // Abort an in-flight streaming run (control message, carries no op).
31098
+ if (msg.type === "abort") {
31099
+ if (streamingRuns[msg.id]) { streamingRuns[msg.id].abort(); delete streamingRuns[msg.id]; }
31100
+ return;
31101
+ }
31102
+ if (typeof msg.op !== "string") return;
31103
+ // Streaming agent run \u2014 many messages back, not a single result.
31104
+ if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
31030
31105
  const startedAt = performance.now();
31031
31106
  try {
31032
31107
  const data = msg.op === "upload"
@@ -31142,6 +31217,49 @@ async function startDevServer(args) {
31142
31217
  }
31143
31218
  return;
31144
31219
  }
31220
+ if (req.method === "POST" && url === "/_agent_run") {
31221
+ const ac = new AbortController();
31222
+ res.on("close", () => {
31223
+ if (!res.writableEnded) ac.abort();
31224
+ });
31225
+ try {
31226
+ const body = await readJson(req);
31227
+ const p = body.payload ?? {};
31228
+ if (typeof p.alias !== "string" || typeof p.session_id !== "string") {
31229
+ throw new Error("agentRun payload must include `alias` and `session_id`");
31230
+ }
31231
+ const upstream = await args.client.appAgentRunStream(
31232
+ body.app_id,
31233
+ p.alias,
31234
+ { session_id: p.session_id, input: p.input ?? {} },
31235
+ ac.signal
31236
+ );
31237
+ if (!upstream.body) throw new Error("agent run returned no stream body");
31238
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform" });
31239
+ const reader = upstream.body.getReader();
31240
+ for (; ; ) {
31241
+ const { value, done } = await reader.read();
31242
+ if (done) break;
31243
+ res.write(Buffer.from(value));
31244
+ }
31245
+ res.end();
31246
+ } catch (err2) {
31247
+ if (ac.signal.aborted) {
31248
+ res.end();
31249
+ return;
31250
+ }
31251
+ const message = err2 instanceof Error ? err2.message : String(err2);
31252
+ process.stderr.write(`[agent_run] ERROR ${message}
31253
+ `);
31254
+ if (!res.headersSent) {
31255
+ res.writeHead(500, { "Content-Type": "application/json" });
31256
+ res.end(JSON.stringify({ message }));
31257
+ } else {
31258
+ res.end();
31259
+ }
31260
+ }
31261
+ return;
31262
+ }
31145
31263
  res.writeHead(404, { "Content-Type": "text/plain" });
31146
31264
  res.end("Not Found");
31147
31265
  });
@@ -31559,8 +31677,12 @@ async function downloadToFile(url, destPath) {
31559
31677
  const buffer = Buffer.from(await response.arrayBuffer());
31560
31678
  fs3.writeFileSync(destPath, buffer);
31561
31679
  }
31680
+ function appDirName(name) {
31681
+ const cleaned = name.replace(/[/\\]+/g, "-").replace(/[<>:"|?*]/g, "").replace(/\s+/g, " ").trim().replace(/^[.\s-]+|[.\s-]+$/g, "");
31682
+ return cleaned || "app";
31683
+ }
31562
31684
  async function appCreate(client, args) {
31563
- const targetPath = path4.resolve(args.targetPath ?? args.name);
31685
+ const targetPath = path4.resolve(args.targetPath ?? appDirName(args.name));
31564
31686
  if (fs3.existsSync(targetPath)) {
31565
31687
  const entries = fs3.readdirSync(targetPath);
31566
31688
  if (entries.length > 0) {
@@ -31621,7 +31743,7 @@ async function appPull(client, args) {
31621
31743
  }
31622
31744
  const version = await client.getAppVersion(app.id, app.current_version_id);
31623
31745
  const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
31624
- const targetPath = path4.resolve(args.targetPath ?? app.name);
31746
+ const targetPath = path4.resolve(args.targetPath ?? appDirName(app.name));
31625
31747
  fs3.mkdirSync(targetPath, { recursive: true });
31626
31748
  const tmpFile = path4.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
31627
31749
  console.error(`Downloading source archive...`);
@@ -38305,6 +38427,12 @@ function emptyStyleResolver() {
38305
38427
  var xmlParser3 = new XMLParser({
38306
38428
  ignoreAttributes: false,
38307
38429
  attributeNamePrefix: "@_",
38430
+ // Shared-string and workbook text is textual BY DEFINITION (tax IDs, phone
38431
+ // numbers, account codes, sheet/defined names). fast-xml-parser's default tag
38432
+ // coercion would parse `<t>0312345678</t>` as the number 312345678 — silently
38433
+ // dropping the leading zero before we can read it. Cell `<v>` numerics are
38434
+ // parsed deliberately (parseFloat) in ooxml_sheet.ts, so this never affects them.
38435
+ parseTagValue: false,
38308
38436
  trimValues: false,
38309
38437
  processEntities: false,
38310
38438
  isArray: (tagName) => tagName === "sheet" || tagName === "si" || tagName === "r" || tagName === "Relationship" || tagName === "definedName"
@@ -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
- include: ["src", ".lotics"],
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {