@lotics/cli 0.88.0 → 0.88.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.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +100 -100
- package/dist/{client.d.ts → src/client.d.ts} +1 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /package/dist/{client.js → src/client.js} +0 -0
package/dist/dev/wrapper_page.js
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the wrapper HTML for `lotics app dev`.
|
|
3
|
-
*
|
|
4
|
-
* The wrapper page is served at http://localhost:<port>/ and embeds the
|
|
5
|
-
* project's Vite dev server in an iframe with the same
|
|
6
|
-
* sandbox="allow-scripts allow-same-origin allow-downloads" attribute
|
|
7
|
-
* production uses — safe because the Vite iframe is cross-origin to this
|
|
8
|
-
* wrapper (different port), so the app gets its own real origin (and Web
|
|
9
|
-
* Storage) without being able to reach the wrapper. `allow-downloads` lets
|
|
10
|
-
* apps trigger file downloads (generated xlsx / pdf etc.) — Chrome blocks
|
|
11
|
-
* every download path from sandboxed iframes without it. `allow="...; geolocation;
|
|
12
|
-
* web-share"` delegates the host's location permission (geofenced actions read the
|
|
13
|
-
* device position) and grants the Web Share API (apps call `navigator.share({ files })`
|
|
14
|
-
* to push e.g. container photos to the OS share sheet / gallery) — matching
|
|
15
|
-
* production. The iframe sends postMessage RPCs to
|
|
16
|
-
* this wrapper, which forwards them to the local /_rpc endpoint, which
|
|
17
|
-
* dispatches to api.lotics.ai with the CLI's API key.
|
|
18
|
-
*
|
|
19
|
-
* postMessage is origin-locked both ways — the wrapper passes its origin
|
|
20
|
-
* via `?lotics_host=` and talks only to the Vite origin.
|
|
21
|
-
*
|
|
22
|
-
* Protocol matches `frontend/features/app_ui/app_iframe_host.tsx` exactly:
|
|
23
|
-
* iframe → wrapper: { id: number, op: string, payload: unknown }
|
|
24
|
-
* wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
|
|
25
|
-
* streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
|
|
26
|
-
* the iframe aborts with { id, type: "abort" }.
|
|
27
|
-
* urlState (useUrlState): the iframe reads/writes the wrapper's address bar via
|
|
28
|
-
* urlState.get/set; set writes in place (replaceState), and browser
|
|
29
|
-
* back/forward broadcast { type: "url-state", params } back to the iframe.
|
|
30
|
-
* (In-app routing isn't here — the app owns the iframe's own url; see
|
|
31
|
-
* @lotics/app-sdk/router.)
|
|
32
|
-
*/
|
|
33
|
-
export function buildWrapperPage(args) {
|
|
34
|
-
const { app_name, app_id, workspace_id, vite_url, api_url } = args;
|
|
35
|
-
return `<!doctype html>
|
|
36
|
-
<html lang="en">
|
|
37
|
-
<head>
|
|
38
|
-
<meta charset="UTF-8" />
|
|
39
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
40
|
-
<title>${escapeHtml(app_name)} — lotics app dev</title>
|
|
41
|
-
<style>
|
|
42
|
-
html, body { margin: 0; height: 100%; }
|
|
43
|
-
body {
|
|
44
|
-
display: flex; flex-direction: column;
|
|
45
|
-
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
46
|
-
background: #f4f4f5;
|
|
47
|
-
}
|
|
48
|
-
header {
|
|
49
|
-
padding: 8px 14px;
|
|
50
|
-
background: #18181b;
|
|
51
|
-
color: #fafafa;
|
|
52
|
-
font-size: 12px;
|
|
53
|
-
display: flex; align-items: center; gap: 12px;
|
|
54
|
-
flex-shrink: 0;
|
|
55
|
-
}
|
|
56
|
-
header strong { font-size: 13px; }
|
|
57
|
-
header code {
|
|
58
|
-
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
59
|
-
font-size: 11px;
|
|
60
|
-
background: rgba(255,255,255,0.08);
|
|
61
|
-
padding: 1px 6px; border-radius: 3px;
|
|
62
|
-
}
|
|
63
|
-
header span { color: #a1a1aa; }
|
|
64
|
-
iframe { border: 0; flex: 1; width: 100%; background: #fff; }
|
|
65
|
-
</style>
|
|
66
|
-
</head>
|
|
67
|
-
<body>
|
|
68
|
-
<header>
|
|
69
|
-
<strong>${escapeHtml(app_name)}</strong>
|
|
70
|
-
<span>app_id <code>${escapeHtml(app_id)}</code></span>
|
|
71
|
-
<span>workspace <code>${escapeHtml(workspace_id)}</code></span>
|
|
72
|
-
<span>RPC → <code>${escapeHtml(api_url)}</code></span>
|
|
73
|
-
</header>
|
|
74
|
-
<iframe id="app" sandbox="allow-scripts allow-same-origin allow-downloads" allow="clipboard-write; geolocation; web-share"></iframe>
|
|
75
|
-
<script>
|
|
76
|
-
(function () {
|
|
77
|
-
const APP_ID = ${JSON.stringify(app_id)};
|
|
78
|
-
const VITE_URL = ${JSON.stringify(vite_url)};
|
|
79
|
-
const VITE_ORIGIN = new URL(VITE_URL).origin;
|
|
80
|
-
const iframe = document.getElementById("app");
|
|
81
|
-
|
|
82
|
-
// Pass the wrapper's own origin to the app via ?lotics_host= so the app
|
|
83
|
-
// SDK can origin-lock its postMessage bridge, and bake the saved screen
|
|
84
|
-
// (the wrapper url's _loc, kept current by AppRouter's mirror) into the src
|
|
85
|
-
// path so a refresh boots the app at that screen — mirrors the production
|
|
86
|
-
// host. _loc is honoured only when it resolves same-origin to the Vite
|
|
87
|
-
// server (a url param flowing into an iframe src is a redirect vector);
|
|
88
|
-
// anything else falls back to the app root.
|
|
89
|
-
const appSrc = new URL(VITE_URL);
|
|
90
|
-
const savedLoc = new URLSearchParams(window.location.search).get("_loc");
|
|
91
|
-
if (savedLoc) {
|
|
92
|
-
try {
|
|
93
|
-
const resolved = new URL(savedLoc, VITE_ORIGIN);
|
|
94
|
-
if (resolved.origin === VITE_ORIGIN) {
|
|
95
|
-
appSrc.pathname = resolved.pathname;
|
|
96
|
-
appSrc.search = resolved.search;
|
|
97
|
-
appSrc.hash = resolved.hash;
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
// malformed _loc -> app root
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
appSrc.searchParams.set("lotics_host", window.location.origin);
|
|
104
|
-
iframe.src = appSrc.toString();
|
|
105
|
-
|
|
106
|
-
async function rpc(op, payload) {
|
|
107
|
-
const res = await fetch("/_rpc", {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers: { "content-type": "application/json" },
|
|
110
|
-
body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
|
|
111
|
-
});
|
|
112
|
-
const text = await res.text();
|
|
113
|
-
if (!res.ok) {
|
|
114
|
-
let detail = text;
|
|
115
|
-
try { detail = JSON.parse(text).message ?? text; } catch (_) {}
|
|
116
|
-
throw new Error(detail || ("HTTP " + res.status));
|
|
117
|
-
}
|
|
118
|
-
return JSON.parse(text);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// The iframe SDK sends one "upload" op carrying a File. A File can't
|
|
122
|
-
// cross the JSON /_rpc hop, so the upload runs here in the browser:
|
|
123
|
-
// mint an upload URL, PUT the bytes, then finalize.
|
|
124
|
-
//
|
|
125
|
-
// In dev the minted URL is same-origin (/_upload/<file_id>) — the dev
|
|
126
|
-
// server relays the bytes to storage on our behalf, because the prod
|
|
127
|
-
// bucket's CORS does not admit a localhost origin. Production PUTs the
|
|
128
|
-
// presigned storage URL directly. Same three lines either way: the page
|
|
129
|
-
// PUTs wherever the mint points.
|
|
130
|
-
//
|
|
131
|
-
// Retry policy parity with the production SDK
|
|
132
|
-
// (packages/app-sdk/src/upload/transport.ts): 3 PUT attempts with
|
|
133
|
-
// 1s/2s/4s backoff on 5xx + network errors. 4xx is terminal. Image
|
|
134
|
-
// compression is intentionally NOT inlined here — dev networks are
|
|
135
|
-
// stable enough, and end-user mobile cases are handled by the SDK
|
|
136
|
-
// and bridged host directly.
|
|
137
|
-
async function putWithRetry(url, file) {
|
|
138
|
-
const MAX_ATTEMPTS = 3;
|
|
139
|
-
let lastError;
|
|
140
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
141
|
-
try {
|
|
142
|
-
const res = await fetch(url, {
|
|
143
|
-
method: "PUT",
|
|
144
|
-
body: file,
|
|
145
|
-
headers: { "Content-Type": file.type },
|
|
146
|
-
});
|
|
147
|
-
if (res.ok) return res;
|
|
148
|
-
// 4xx is terminal — retrying a malformed/expired URL is pointless.
|
|
149
|
-
if (res.status >= 400 && res.status < 500) return res;
|
|
150
|
-
lastError = new Error("Storage PUT returned " + res.status);
|
|
151
|
-
} catch (err) {
|
|
152
|
-
lastError = err;
|
|
153
|
-
}
|
|
154
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
155
|
-
const delayMs = 1000 * Math.pow(2, attempt - 1);
|
|
156
|
-
await new Promise(function (r) { setTimeout(r, delayMs); });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
throw lastError || new Error("Storage PUT failed");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function handleUpload(payload) {
|
|
163
|
-
const file = payload && payload.file;
|
|
164
|
-
if (!(file instanceof File)) {
|
|
165
|
-
throw new Error("upload payload must include a File");
|
|
166
|
-
}
|
|
167
|
-
const init = await rpc("upload_url", {
|
|
168
|
-
filename: file.name,
|
|
169
|
-
mime_type: file.type,
|
|
170
|
-
file_size: file.size,
|
|
171
|
-
});
|
|
172
|
-
const putRes = await putWithRetry(init.upload_url, file);
|
|
173
|
-
if (!putRes.ok) {
|
|
174
|
-
throw new Error("Storage upload failed (" + putRes.status + ")");
|
|
175
|
-
}
|
|
176
|
-
const completed = await rpc("upload_complete", {
|
|
177
|
-
file_id: init.file_id,
|
|
178
|
-
file_storage_key: init.file_storage_key,
|
|
179
|
-
filename: file.name,
|
|
180
|
-
});
|
|
181
|
-
return completed.file;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// openExternal — the wrapper page is the top frame (un-sandboxed), so it
|
|
185
|
-
// opens the link directly; NOT forwarded to the RPC endpoint (no server
|
|
186
|
-
// op). Re-validate the scheme; never trust the iframe's payload.
|
|
187
|
-
function handleOpenExternal(payload) {
|
|
188
|
-
var url = payload && payload.url;
|
|
189
|
-
if (typeof url !== "string") throw new Error("openExternal requires a url string");
|
|
190
|
-
var parsed = new URL(url);
|
|
191
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
192
|
-
throw new Error('openExternal: unsupported URL scheme "' + parsed.protocol + '"');
|
|
193
|
-
}
|
|
194
|
-
window.open(parsed.href, "_blank", "noopener,noreferrer");
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// urlState.get/set — useUrlState keeps app view-state (filters, search) in
|
|
199
|
-
// THIS wrapper page's address bar so it survives refresh and is shareable,
|
|
200
|
-
// mirroring the production host. Handled locally (the URL is the store; no
|
|
201
|
-
// API hop). The wrapper page is its own top-level page, so its query string
|
|
202
|
-
// is entirely the app's. Repeated keys become arrays. Parse/merge MUST
|
|
203
|
-
// match @lotics/app-sdk parseSearch/serializeMerge and the production host
|
|
204
|
-
// (inline JS here — no module system to share the trivial copy).
|
|
205
|
-
function readUrlParams() {
|
|
206
|
-
const sp = new URLSearchParams(window.location.search);
|
|
207
|
-
const out = {};
|
|
208
|
-
sp.forEach(function (_value, key) {
|
|
209
|
-
if (key in out) return;
|
|
210
|
-
const all = sp.getAll(key);
|
|
211
|
-
out[key] = all.length > 1 ? all : all[0];
|
|
212
|
-
});
|
|
213
|
-
return out;
|
|
214
|
-
}
|
|
215
|
-
function handleUrlStateSet(payload) {
|
|
216
|
-
const params = (payload && payload.params) || {};
|
|
217
|
-
const sp = new URLSearchParams(window.location.search);
|
|
218
|
-
Object.keys(params).forEach(function (key) {
|
|
219
|
-
sp.delete(key);
|
|
220
|
-
const value = params[key];
|
|
221
|
-
if (value === undefined || value === null) return;
|
|
222
|
-
if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
|
|
223
|
-
else sp.append(key, value);
|
|
224
|
-
});
|
|
225
|
-
const qs = sp.toString();
|
|
226
|
-
const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
|
|
227
|
-
// View-state writes in place — never a history entry. replaceState doesn't
|
|
228
|
-
// fire popstate, so no echo. Mirrors the production host.
|
|
229
|
-
window.history.replaceState(null, "", url);
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Streaming agent runs (op "agentRun"): the response is a stream, so it
|
|
234
|
-
// can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
|
|
235
|
-
// body, and forward chunks to the iframe as the SDK's bridged streaming
|
|
236
|
-
// protocol expects (stream-chunk* -> stream-end). Mirrors the production
|
|
237
|
-
// host in frontend/features/app_ui/app_iframe_host.tsx.
|
|
238
|
-
const streamingRuns = {};
|
|
239
|
-
async function handleAgentRun(id, payload) {
|
|
240
|
-
const controller = new AbortController();
|
|
241
|
-
streamingRuns[id] = controller;
|
|
242
|
-
try {
|
|
243
|
-
const res = await fetch("/_agent_run", {
|
|
244
|
-
method: "POST",
|
|
245
|
-
headers: { "content-type": "application/json" },
|
|
246
|
-
body: JSON.stringify({ app_id: APP_ID, op: "agentRun", payload: payload }),
|
|
247
|
-
signal: controller.signal,
|
|
248
|
-
});
|
|
249
|
-
if (!res.ok || !res.body) {
|
|
250
|
-
let detail = await res.text().catch(function () { return ""; });
|
|
251
|
-
try { detail = JSON.parse(detail).message || detail; } catch (_) {}
|
|
252
|
-
throw new Error(detail || ("HTTP " + res.status));
|
|
253
|
-
}
|
|
254
|
-
const reader = res.body.getReader();
|
|
255
|
-
const decoder = new TextDecoder();
|
|
256
|
-
for (;;) {
|
|
257
|
-
const r = await reader.read();
|
|
258
|
-
if (r.done) break;
|
|
259
|
-
iframe.contentWindow.postMessage(
|
|
260
|
-
{ id: id, type: "stream-chunk", chunk: decoder.decode(r.value, { stream: true }) },
|
|
261
|
-
VITE_ORIGIN
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
const tail = decoder.decode();
|
|
265
|
-
if (tail) iframe.contentWindow.postMessage({ id: id, type: "stream-chunk", chunk: tail }, VITE_ORIGIN);
|
|
266
|
-
iframe.contentWindow.postMessage({ id: id, type: "stream-end" }, VITE_ORIGIN);
|
|
267
|
-
} catch (err) {
|
|
268
|
-
if (controller.signal.aborted) return;
|
|
269
|
-
const message = err && err.message ? err.message : String(err);
|
|
270
|
-
iframe.contentWindow.postMessage({ id: id, type: "error", message: message }, VITE_ORIGIN);
|
|
271
|
-
} finally {
|
|
272
|
-
delete streamingRuns[id];
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
window.addEventListener("message", async function (event) {
|
|
277
|
-
if (event.source !== iframe.contentWindow || event.origin !== VITE_ORIGIN) return;
|
|
278
|
-
const msg = event.data;
|
|
279
|
-
if (!msg || typeof msg.id !== "number") return;
|
|
280
|
-
// Abort an in-flight streaming run (control message, carries no op).
|
|
281
|
-
if (msg.type === "abort") {
|
|
282
|
-
if (streamingRuns[msg.id]) { streamingRuns[msg.id].abort(); delete streamingRuns[msg.id]; }
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (typeof msg.op !== "string") return;
|
|
286
|
-
// Streaming agent run — many messages back, not a single result.
|
|
287
|
-
if (msg.op === "agentRun") { handleAgentRun(msg.id, msg.payload); return; }
|
|
288
|
-
const startedAt = performance.now();
|
|
289
|
-
try {
|
|
290
|
-
const data = msg.op === "upload"
|
|
291
|
-
? await handleUpload(msg.payload)
|
|
292
|
-
: msg.op === "openExternal"
|
|
293
|
-
? handleOpenExternal(msg.payload)
|
|
294
|
-
: msg.op === "urlState.get"
|
|
295
|
-
? readUrlParams()
|
|
296
|
-
: msg.op === "urlState.set"
|
|
297
|
-
? handleUrlStateSet(msg.payload)
|
|
298
|
-
: await rpc(msg.op, msg.payload);
|
|
299
|
-
const ms = Math.round(performance.now() - startedAt);
|
|
300
|
-
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
301
|
-
iframe.contentWindow.postMessage(
|
|
302
|
-
{ id: msg.id, type: "result", data: data },
|
|
303
|
-
VITE_ORIGIN
|
|
304
|
-
);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
const message = err && err.message ? err.message : String(err);
|
|
307
|
-
console.error("[lotics-dev] " + msg.op + " failed:", message);
|
|
308
|
-
iframe.contentWindow.postMessage(
|
|
309
|
-
{ id: msg.id, type: "error", message: message },
|
|
310
|
-
VITE_ORIGIN
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Browser back/forward → broadcast the new params so useUrlState re-hydrates
|
|
316
|
-
// (the app's own set writes use replaceState — no popstate — so there's no
|
|
317
|
-
// echo). Mirrors the production host.
|
|
318
|
-
window.addEventListener("popstate", function () {
|
|
319
|
-
iframe.contentWindow.postMessage(
|
|
320
|
-
{ type: "url-state", params: readUrlParams() },
|
|
321
|
-
VITE_ORIGIN
|
|
322
|
-
);
|
|
323
|
-
});
|
|
324
|
-
})();
|
|
325
|
-
</script>
|
|
326
|
-
</body>
|
|
327
|
-
</html>
|
|
328
|
-
`;
|
|
329
|
-
}
|
|
330
|
-
function escapeHtml(s) {
|
|
331
|
-
return s
|
|
332
|
-
.replace(/&/g, "&")
|
|
333
|
-
.replace(/</g, "<")
|
|
334
|
-
.replace(/>/g, ">");
|
|
335
|
-
}
|
package/dist/docx.d.ts
DELETED
package/dist/docx.js
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { fail, writeFileAtomic } from "./file_command_io.js";
|
|
3
|
-
import { parseDocx, serializeDocx, } from "@lotics/docx";
|
|
4
|
-
const DEFAULT_DOC_ATTRS = {
|
|
5
|
-
"@_xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
|
6
|
-
};
|
|
7
|
-
async function loadFile(filePath) {
|
|
8
|
-
if (!fs.existsSync(filePath))
|
|
9
|
-
fail(`File not found: ${filePath}`);
|
|
10
|
-
const bytes = fs.readFileSync(filePath);
|
|
11
|
-
return parseDocx(new Uint8Array(bytes));
|
|
12
|
-
}
|
|
13
|
-
async function writeDoc(filePath, doc) {
|
|
14
|
-
const bytes = await serializeDocx(doc);
|
|
15
|
-
writeFileAtomic(filePath, bytes);
|
|
16
|
-
}
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// Block construction (typed, immutable)
|
|
19
|
-
// =============================================================================
|
|
20
|
-
function buildParagraph(text, style) {
|
|
21
|
-
const properties = style ? [buildPStyle(style)] : null;
|
|
22
|
-
const content = [buildRun(text)];
|
|
23
|
-
return { kind: "paragraph", properties, content };
|
|
24
|
-
}
|
|
25
|
-
function buildPStyle(styleId) {
|
|
26
|
-
return { "w:pStyle": [], ":@": { "@_w:val": styleId } };
|
|
27
|
-
}
|
|
28
|
-
function buildRun(text) {
|
|
29
|
-
// Split on \n so newlines render as Word line breaks (<w:br/>) instead of
|
|
30
|
-
// collapsing into a single line — multi-line agent input is common.
|
|
31
|
-
const segments = text.split("\n");
|
|
32
|
-
const content = [];
|
|
33
|
-
for (let i = 0; i < segments.length; i++) {
|
|
34
|
-
const value = segments[i];
|
|
35
|
-
if (value !== "") {
|
|
36
|
-
content.push({
|
|
37
|
-
kind: "text",
|
|
38
|
-
value,
|
|
39
|
-
preserveSpace: value.startsWith(" ") || value.endsWith(" "),
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
if (i < segments.length - 1) {
|
|
43
|
-
content.push({ kind: "opaque_run_child", xml: { "w:br": [] } });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (content.length === 0)
|
|
47
|
-
content.push({ kind: "text", value: "", preserveSpace: false });
|
|
48
|
-
return { kind: "run", properties: null, content };
|
|
49
|
-
}
|
|
50
|
-
function replaceChildren(doc, children) {
|
|
51
|
-
return {
|
|
52
|
-
parts: doc.parts,
|
|
53
|
-
documentAttrs: doc.documentAttrs,
|
|
54
|
-
body: { children },
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
// =============================================================================
|
|
58
|
-
// Read: project body to a simplified shape
|
|
59
|
-
// =============================================================================
|
|
60
|
-
function readToJson(doc, includeOpaque) {
|
|
61
|
-
const blocks = doc.body.children.map((block, index) => projectBlock(block, index, includeOpaque));
|
|
62
|
-
return { documentAttrs: doc.documentAttrs, blocks };
|
|
63
|
-
}
|
|
64
|
-
function projectBlock(block, index, includeOpaque) {
|
|
65
|
-
switch (block.kind) {
|
|
66
|
-
case "paragraph":
|
|
67
|
-
return {
|
|
68
|
-
kind: "paragraph",
|
|
69
|
-
index,
|
|
70
|
-
text: extractText(block),
|
|
71
|
-
style: extractStyleId(block.properties),
|
|
72
|
-
};
|
|
73
|
-
case "body_sect_pr":
|
|
74
|
-
return includeOpaque
|
|
75
|
-
? { kind: "body_sect_pr", index, xml: block.xml }
|
|
76
|
-
: { kind: "body_sect_pr", index };
|
|
77
|
-
case "opaque_block":
|
|
78
|
-
return includeOpaque
|
|
79
|
-
? { kind: "opaque_block", index, tag: firstTagName(block.xml), xml: block.xml }
|
|
80
|
-
: { kind: "opaque_block", index, tag: firstTagName(block.xml) };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function extractText(para) {
|
|
84
|
-
let out = "";
|
|
85
|
-
for (const inline of para.content) {
|
|
86
|
-
if (inline.kind !== "run")
|
|
87
|
-
continue;
|
|
88
|
-
for (const child of inline.content) {
|
|
89
|
-
if (child.kind === "text")
|
|
90
|
-
out += child.value;
|
|
91
|
-
else if (child.kind === "opaque_run_child" && firstTagName(child.xml) === "w:br")
|
|
92
|
-
out += "\n";
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return out;
|
|
96
|
-
}
|
|
97
|
-
function extractStyleId(properties) {
|
|
98
|
-
if (!properties)
|
|
99
|
-
return undefined;
|
|
100
|
-
for (const prop of properties) {
|
|
101
|
-
if (firstTagName(prop) !== "w:pStyle")
|
|
102
|
-
continue;
|
|
103
|
-
const attrs = prop[":@"];
|
|
104
|
-
if (attrs?.["@_w:val"])
|
|
105
|
-
return attrs["@_w:val"];
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
function firstTagName(el) {
|
|
110
|
-
for (const key of Object.keys(el)) {
|
|
111
|
-
if (key !== ":@" && key !== "#text")
|
|
112
|
-
return key;
|
|
113
|
-
}
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
// =============================================================================
|
|
117
|
-
// Write: build DocxDocument from a SimpleDoc
|
|
118
|
-
// =============================================================================
|
|
119
|
-
function buildDoc(input) {
|
|
120
|
-
if (!input || !Array.isArray(input.blocks))
|
|
121
|
-
fail("docx write JSON must include a `blocks` array");
|
|
122
|
-
const children = [];
|
|
123
|
-
for (let i = 0; i < input.blocks.length; i++) {
|
|
124
|
-
const b = input.blocks[i];
|
|
125
|
-
if (!b || typeof b !== "object")
|
|
126
|
-
fail(`blocks[${i}] must be an object`);
|
|
127
|
-
if (b.kind === "paragraph") {
|
|
128
|
-
if (typeof b.text !== "string")
|
|
129
|
-
fail(`blocks[${i}].text must be a string`);
|
|
130
|
-
if (b.style !== undefined && typeof b.style !== "string")
|
|
131
|
-
fail(`blocks[${i}].style must be a string`);
|
|
132
|
-
children.push(buildParagraph(b.text, b.style));
|
|
133
|
-
}
|
|
134
|
-
else if (b.kind === "opaque_block") {
|
|
135
|
-
fail(`blocks[${i}]: opaque_block cannot be created from JSON; read+modify an existing .docx instead`);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const kind = b.kind;
|
|
139
|
-
fail(`blocks[${i}]: unknown kind ${JSON.stringify(kind)} (expected "paragraph")`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const documentAttrs = { ...DEFAULT_DOC_ATTRS, ...(input.documentAttrs ?? {}) };
|
|
143
|
-
return { parts: new Map(), documentAttrs, body: { children } };
|
|
144
|
-
}
|
|
145
|
-
// =============================================================================
|
|
146
|
-
// Subcommand handlers
|
|
147
|
-
// =============================================================================
|
|
148
|
-
async function docxRead(filePath, rest) {
|
|
149
|
-
if (!filePath)
|
|
150
|
-
fail("Usage: lotics docx read <file> [--include-opaque]");
|
|
151
|
-
const includeOpaque = rest.includes("--include-opaque");
|
|
152
|
-
const doc = await loadFile(filePath);
|
|
153
|
-
console.log(JSON.stringify(readToJson(doc, includeOpaque), null, 2));
|
|
154
|
-
}
|
|
155
|
-
async function docxWrite(filePath, json) {
|
|
156
|
-
if (!filePath || !json)
|
|
157
|
-
fail("Usage: lotics docx write <file> '<json>'");
|
|
158
|
-
let parsed;
|
|
159
|
-
try {
|
|
160
|
-
parsed = JSON.parse(json);
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
fail(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
164
|
-
}
|
|
165
|
-
const doc = buildDoc(parsed);
|
|
166
|
-
await writeDoc(filePath, doc);
|
|
167
|
-
}
|
|
168
|
-
function getStyleFlag(rest) {
|
|
169
|
-
for (const arg of rest) {
|
|
170
|
-
if (arg.startsWith("--style="))
|
|
171
|
-
return arg.slice("--style=".length);
|
|
172
|
-
}
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
function getAtFlag(rest) {
|
|
176
|
-
for (const arg of rest) {
|
|
177
|
-
if (arg.startsWith("--at=")) {
|
|
178
|
-
const n = parseInt(arg.slice("--at=".length), 10);
|
|
179
|
-
return Number.isFinite(n) ? n : undefined;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
function firstPositional(rest) {
|
|
185
|
-
return rest.find((a) => !a.startsWith("--"));
|
|
186
|
-
}
|
|
187
|
-
async function docxAppendParagraph(filePath, rest) {
|
|
188
|
-
const text = firstPositional(rest);
|
|
189
|
-
if (!filePath || text === undefined)
|
|
190
|
-
fail("Usage: lotics docx append-paragraph <file> '<text>' [--style=NAME]");
|
|
191
|
-
const style = getStyleFlag(rest);
|
|
192
|
-
const doc = await loadFile(filePath);
|
|
193
|
-
const para = buildParagraph(text, style);
|
|
194
|
-
const newChildren = [...doc.body.children, para];
|
|
195
|
-
await writeDoc(filePath, replaceChildren(doc, newChildren));
|
|
196
|
-
}
|
|
197
|
-
async function docxInsertParagraph(filePath, rest) {
|
|
198
|
-
const text = firstPositional(rest);
|
|
199
|
-
const at = getAtFlag(rest);
|
|
200
|
-
if (!filePath || text === undefined || at === undefined) {
|
|
201
|
-
fail("Usage: lotics docx insert-paragraph <file> '<text>' --at=<index> [--style=NAME]");
|
|
202
|
-
}
|
|
203
|
-
const style = getStyleFlag(rest);
|
|
204
|
-
const doc = await loadFile(filePath);
|
|
205
|
-
if (at < 0 || at > doc.body.children.length) {
|
|
206
|
-
fail(`--at=${at} out of range (0..${doc.body.children.length})`);
|
|
207
|
-
}
|
|
208
|
-
const para = buildParagraph(text, style);
|
|
209
|
-
const newChildren = [...doc.body.children];
|
|
210
|
-
newChildren.splice(at, 0, para);
|
|
211
|
-
await writeDoc(filePath, replaceChildren(doc, newChildren));
|
|
212
|
-
}
|
|
213
|
-
async function docxDeleteBlock(filePath, rest) {
|
|
214
|
-
const at = getAtFlag(rest);
|
|
215
|
-
if (!filePath || at === undefined)
|
|
216
|
-
fail("Usage: lotics docx delete-block <file> --at=<index>");
|
|
217
|
-
const doc = await loadFile(filePath);
|
|
218
|
-
if (at < 0 || at >= doc.body.children.length) {
|
|
219
|
-
fail(`--at=${at} out of range (0..${doc.body.children.length - 1})`);
|
|
220
|
-
}
|
|
221
|
-
const newChildren = doc.body.children.filter((_, i) => i !== at);
|
|
222
|
-
await writeDoc(filePath, replaceChildren(doc, newChildren));
|
|
223
|
-
}
|
|
224
|
-
async function docxReplaceText(filePath, rest) {
|
|
225
|
-
const [search, replace] = rest;
|
|
226
|
-
if (!filePath || !search || replace === undefined) {
|
|
227
|
-
fail("Usage: lotics docx replace-text <file> '<search>' '<replace>'");
|
|
228
|
-
}
|
|
229
|
-
const doc = await loadFile(filePath);
|
|
230
|
-
const newChildren = doc.body.children.map((b) => (b.kind === "paragraph" ? replaceInParagraph(b, search, replace) : b));
|
|
231
|
-
await writeDoc(filePath, replaceChildren(doc, newChildren));
|
|
232
|
-
}
|
|
233
|
-
function replaceInParagraph(p, search, replace) {
|
|
234
|
-
const content = p.content.map((inline) => {
|
|
235
|
-
if (inline.kind !== "run")
|
|
236
|
-
return inline;
|
|
237
|
-
const newChildren = inline.content.map((c) => c.kind === "text"
|
|
238
|
-
? { kind: "text", value: c.value.split(search).join(replace), preserveSpace: c.preserveSpace }
|
|
239
|
-
: c);
|
|
240
|
-
return { kind: "run", properties: inline.properties, content: newChildren };
|
|
241
|
-
});
|
|
242
|
-
return { kind: "paragraph", properties: p.properties, content };
|
|
243
|
-
}
|
|
244
|
-
async function docxBatch(filePath, json) {
|
|
245
|
-
if (!filePath || !json)
|
|
246
|
-
fail("Usage: lotics docx batch <file> '<json-array-of-ops>'");
|
|
247
|
-
let ops;
|
|
248
|
-
try {
|
|
249
|
-
ops = JSON.parse(json);
|
|
250
|
-
}
|
|
251
|
-
catch (e) {
|
|
252
|
-
fail(`Invalid batch JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
253
|
-
}
|
|
254
|
-
if (!Array.isArray(ops))
|
|
255
|
-
fail("Batch must be a JSON array of ops");
|
|
256
|
-
for (let i = 0; i < ops.length; i++) {
|
|
257
|
-
const op = ops[i];
|
|
258
|
-
if (!op || typeof op !== "object" || typeof op.op !== "string") {
|
|
259
|
-
fail(`[op ${i}] each op must be an object with an "op" string field`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
let doc = await loadFile(filePath);
|
|
263
|
-
for (let i = 0; i < ops.length; i++) {
|
|
264
|
-
doc = applyBatchOp(doc, ops[i], i);
|
|
265
|
-
}
|
|
266
|
-
await writeDoc(filePath, doc);
|
|
267
|
-
}
|
|
268
|
-
function applyBatchOp(doc, op, index) {
|
|
269
|
-
switch (op.op) {
|
|
270
|
-
case "append-paragraph": {
|
|
271
|
-
const para = buildParagraph(op.text, op.style);
|
|
272
|
-
return replaceChildren(doc, [...doc.body.children, para]);
|
|
273
|
-
}
|
|
274
|
-
case "insert-paragraph": {
|
|
275
|
-
if (op.at < 0 || op.at > doc.body.children.length) {
|
|
276
|
-
fail(`[op ${index}] at=${op.at} out of range`);
|
|
277
|
-
}
|
|
278
|
-
const next = [...doc.body.children];
|
|
279
|
-
next.splice(op.at, 0, buildParagraph(op.text, op.style));
|
|
280
|
-
return replaceChildren(doc, next);
|
|
281
|
-
}
|
|
282
|
-
case "delete-block": {
|
|
283
|
-
if (op.at < 0 || op.at >= doc.body.children.length) {
|
|
284
|
-
fail(`[op ${index}] at=${op.at} out of range`);
|
|
285
|
-
}
|
|
286
|
-
return replaceChildren(doc, doc.body.children.filter((_, i) => i !== op.at));
|
|
287
|
-
}
|
|
288
|
-
case "replace-text": {
|
|
289
|
-
const next = doc.body.children.map((b) => (b.kind === "paragraph" ? replaceInParagraph(b, op.search, op.replace) : b));
|
|
290
|
-
return replaceChildren(doc, next);
|
|
291
|
-
}
|
|
292
|
-
default: {
|
|
293
|
-
const exhaustive = op;
|
|
294
|
-
void exhaustive;
|
|
295
|
-
fail(`[op ${index}] Unknown op: ${JSON.stringify(op)}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// =============================================================================
|
|
300
|
-
// Dispatch + help
|
|
301
|
-
// =============================================================================
|
|
302
|
-
export function printDocxHelp() {
|
|
303
|
-
console.error(`Lotics docx commands — manipulate .docx files in place.
|
|
304
|
-
Uses Lotics' own OOXML engine; round-trips faithfully with the Lotics editor and templates.
|
|
305
|
-
|
|
306
|
-
lotics docx read <file> [--include-opaque] Dump file as JSON (blocks with text/style)
|
|
307
|
-
lotics docx write <file> '<json>' Create .docx from {"blocks":[{"kind":"paragraph","text":...}]}
|
|
308
|
-
lotics docx append-paragraph <file> '<text>' [--style=NAME]
|
|
309
|
-
Append a paragraph (styles: Title, Heading1..3, Quote, Code)
|
|
310
|
-
lotics docx insert-paragraph <file> '<text>' --at=<i> [--style=NAME]
|
|
311
|
-
Insert a paragraph at index <i> (0-based)
|
|
312
|
-
lotics docx delete-block <file> --at=<i> Remove the block at index <i>
|
|
313
|
-
lotics docx replace-text <file> '<search>' '<replace>' Replace text in all paragraph runs
|
|
314
|
-
lotics docx batch <file> '<json-array-of-ops>' Apply many ops in one parse/serialize cycle
|
|
315
|
-
|
|
316
|
-
Edit ops mutate the file atomically (temp file + rename). Opaque blocks (tables, custom XML) are preserved verbatim.`);
|
|
317
|
-
}
|
|
318
|
-
export async function runDocxCommand(subcommand, toolArgs, restArgs) {
|
|
319
|
-
switch (subcommand) {
|
|
320
|
-
case "read":
|
|
321
|
-
return docxRead(toolArgs, restArgs);
|
|
322
|
-
case "write":
|
|
323
|
-
return docxWrite(toolArgs, restArgs[0]);
|
|
324
|
-
case "append-paragraph":
|
|
325
|
-
return docxAppendParagraph(toolArgs, restArgs);
|
|
326
|
-
case "insert-paragraph":
|
|
327
|
-
return docxInsertParagraph(toolArgs, restArgs);
|
|
328
|
-
case "delete-block":
|
|
329
|
-
return docxDeleteBlock(toolArgs, restArgs);
|
|
330
|
-
case "replace-text":
|
|
331
|
-
return docxReplaceText(toolArgs, restArgs);
|
|
332
|
-
case "batch":
|
|
333
|
-
return docxBatch(toolArgs, restArgs[0]);
|
|
334
|
-
default: {
|
|
335
|
-
if (subcommand)
|
|
336
|
-
console.error(`Unknown docx subcommand: ${subcommand}\n`);
|
|
337
|
-
printDocxHelp();
|
|
338
|
-
process.exit(subcommand ? 1 : 0);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
package/dist/docx.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|