@lotics/cli 0.32.0 → 0.34.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.
Files changed (45) hide show
  1. package/README.md +27 -0
  2. package/dist/cli.js +673 -0
  3. package/dist/{src/dev → dev}/wrapper_page.d.ts +6 -4
  4. package/dist/{src/dev → dev}/wrapper_page.js +40 -10
  5. package/dist/docx.d.ts +2 -0
  6. package/dist/docx.js +341 -0
  7. package/dist/docx.test.js +145 -0
  8. package/dist/file_command_io.d.ts +12 -0
  9. package/dist/file_command_io.js +34 -0
  10. package/dist/src/cli.js +46757 -500
  11. package/dist/{src/starter_template.d.ts → starter_template.d.ts} +1 -1
  12. package/dist/{src/starter_template.js → starter_template.js} +1 -1
  13. package/dist/starter_template.test.d.ts +1 -0
  14. package/dist/xlsx.d.ts +2 -0
  15. package/dist/xlsx.js +489 -0
  16. package/dist/xlsx.test.d.ts +1 -0
  17. package/dist/xlsx.test.js +131 -0
  18. package/package.json +6 -2
  19. /package/dist/{src/app_commands.d.ts → app_commands.d.ts} +0 -0
  20. /package/dist/{src/app_commands.js → app_commands.js} +0 -0
  21. /package/dist/{src/app_commands.test.d.ts → app_commands.test.d.ts} +0 -0
  22. /package/dist/{src/app_commands.test.js → app_commands.test.js} +0 -0
  23. /package/dist/{src/args.d.ts → args.d.ts} +0 -0
  24. /package/dist/{src/args.js → args.js} +0 -0
  25. /package/dist/{src/args.test.d.ts → args.test.d.ts} +0 -0
  26. /package/dist/{src/args.test.js → args.test.js} +0 -0
  27. /package/dist/{src/cli.d.ts → cli.d.ts} +0 -0
  28. /package/dist/{src/client.d.ts → client.d.ts} +0 -0
  29. /package/dist/{src/client.js → client.js} +0 -0
  30. /package/dist/{src/config.d.ts → config.d.ts} +0 -0
  31. /package/dist/{src/config.js → config.js} +0 -0
  32. /package/dist/{src/config.test.d.ts → config.test.d.ts} +0 -0
  33. /package/dist/{src/config.test.js → config.test.js} +0 -0
  34. /package/dist/{src/dev → dev}/rpc_handler.d.ts +0 -0
  35. /package/dist/{src/dev → dev}/rpc_handler.js +0 -0
  36. /package/dist/{src/dev → dev}/server.d.ts +0 -0
  37. /package/dist/{src/dev → dev}/server.js +0 -0
  38. /package/dist/{src/starter_template.test.d.ts → docx.test.d.ts} +0 -0
  39. /package/dist/{src/generate_app_queries_dts.d.ts → generate_app_queries_dts.d.ts} +0 -0
  40. /package/dist/{src/generate_app_queries_dts.js → generate_app_queries_dts.js} +0 -0
  41. /package/dist/{src/generate_app_workflows_dts.d.ts → generate_app_workflows_dts.d.ts} +0 -0
  42. /package/dist/{src/generate_app_workflows_dts.js → generate_app_workflows_dts.js} +0 -0
  43. /package/dist/{src/starter_template.test.js → starter_template.test.js} +0 -0
  44. /package/dist/{src/version.d.ts → version.d.ts} +0 -0
  45. /package/dist/{src/version.js → version.js} +0 -0
package/dist/cli.js ADDED
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { LoticsClient, API_BASE_URL } from "./client.js";
6
+ import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
7
+ import { VERSION } from "./version.js";
8
+ import { appCreate, appPull, appDeploy, appDev, appSetSubdomain } from "./app_commands.js";
9
+ import { parseArgs } from "./args.js";
10
+ import { runXlsxCommand } from "./xlsx.js";
11
+ import { runDocxCommand } from "./docx.js";
12
+ function printHelp() {
13
+ console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
14
+
15
+ Lotics is an AI-powered operations platform. Through this CLI you can:
16
+ - Manage tables, records, and views (structured data with typed fields)
17
+ - Generate documents from templates (Excel, Word, PDF, Email)
18
+ - Build and run automations — schedules, webhooks, table lifecycle workflows,
19
+ button actions, and app-action workflows; inspect execution and version history
20
+ - Administer the workspace — invite members, manage groups, share resources,
21
+ transfer ownership, browse connected accounts (OAuth attach is web-only)
22
+ - Create and manage apps, knowledge docs, and files
23
+
24
+ AUTHENTICATION
25
+ lotics auth Show auth help
26
+ lotics auth signup <email> Create account (run "lotics auth" for details)
27
+
28
+ USAGE
29
+ 1. lotics auth signup <email> Create account or authenticate
30
+ 2. lotics workspace Check current workspace (auto-selects if only one)
31
+ 3. lotics tools List available tools by category
32
+ 4. lotics tools <name> Show tool description and full input schema
33
+ 5. lotics run <tool> '<json>' Execute a tool with JSON arguments
34
+
35
+ Always inspect the schema (step 4) before calling a tool.
36
+ Tools are grouped by category (tables, records, views, etc.).
37
+ Query tools return IDs used as arguments to other tools.
38
+
39
+ COMMANDS
40
+ lotics workspace List workspaces (marks current)
41
+ lotics workspace select <id> Switch to a different workspace
42
+ lotics workspace create <name> Create a new workspace (admin only)
43
+ lotics tools List all available tools
44
+ lotics tools <name> Show tool description and input schema
45
+ lotics run <tool> '<json>' Execute a tool
46
+ lotics upload <file|dir...> Upload files (directories expand to their immediate files)
47
+ lotics download <file_id> Download a file by ID
48
+ lotics download record <record_id> <field_key>
49
+ Download all files on a record file field
50
+ lotics app create <name> [path] Create a new custom-code app + scaffold locally
51
+ lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
52
+ lotics app deploy [-m <message>] [--force-workflow-sync]
53
+ Build + upload current dir as a new version
54
+ (--force-workflow-sync wipes server-only
55
+ workflow aliases; default refuses)
56
+ lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
57
+ lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
58
+ lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
59
+ (uses the bundled Lotics xlsx engine; prefer over
60
+ npm xlsx/exceljs for round-trip fidelity)
61
+ lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem
62
+ (uses the bundled Lotics OOXML engine; preserves
63
+ tables, images, and unknown markup verbatim)
64
+
65
+ FLAGS
66
+ --json Full JSON output (default is human-readable text)
67
+ --timeout <ms> Timeout for tool execution (default: 60000)
68
+ -o <path> Output dir for downloads
69
+ --as <name> Override upload filename
70
+ --api-key <key> API key (overrides saved config and LOTICS_API_KEY)
71
+ --local (lotics auth) Save credentials to ./.lotics/config.json
72
+ --version Show version
73
+
74
+ CONFIG
75
+ Credentials resolve from a .lotics/config.json found by walking up from the
76
+ current directory, else the global ~/.lotics/config.json. Run "lotics auth
77
+ api-key <key> --local" inside a directory (e.g. a worktree) to pin it to its
78
+ own account and workspace.
79
+
80
+ OUTPUT
81
+ Default output is a compact text summary optimized for AI agents —
82
+ use it directly, no parsing needed. --json returns raw structured
83
+ JSON for machine-to-machine pipelines (scripts, CI). Errors print
84
+ to stderr and exit with code 1.
85
+
86
+ FILES
87
+ Some tools generate files and return { file_id, url, filename }.
88
+ Use lotics download to save locally:
89
+
90
+ lotics run generate_excel_from_template '{"..."}'
91
+ lotics download <file_id> -o ./output/
92
+
93
+ Upload files before referencing them in tool args:
94
+
95
+ lotics upload ./data.csv ./report.pdf ./documents/
96
+ lotics run create_records '{"table_id":"tbl_...","records":[{"fld_file":["fil_..."]}]}'
97
+
98
+ STDIN
99
+ Pipe JSON arguments via stdin instead of inline:
100
+
101
+ echo '{"table_id":"tbl_..."}' | lotics run query_records`);
102
+ }
103
+ function printAuthHelp() {
104
+ console.log(`Authentication commands:
105
+
106
+ lotics auth signup <email> Create account, org, workspace, and API key
107
+ lotics auth api-key [key] Save an existing API key (e.g. from the web app)
108
+ lotics auth web Send a magic link email to access the web app
109
+ lotics auth whoami Show the current account's name, email, and organization
110
+ lotics auth logout Remove saved credentials
111
+
112
+ Auth flags:
113
+ --local Save credentials to ./.lotics/config.json (this directory)
114
+ instead of the global ~/.lotics — applies to signup + api-key
115
+ --name <name> (signup) Display name (defaults to email prefix)
116
+ --timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
117
+
118
+ Signup sends a magic link email so you can access the Lotics web app.
119
+ Use lotics auth web to request a new magic link at any time.
120
+
121
+ Auth priority: --api-key flag > LOTICS_API_KEY env > config file.
122
+ Config file: .lotics/config.json found by walking up from the current directory,
123
+ else ~/.lotics/config.json. A per-directory config pins a project or worktree to
124
+ its own account and workspace; --local creates one. Note: an exported
125
+ LOTICS_API_KEY env var overrides the config file's key.`);
126
+ }
127
+ function readStdin() {
128
+ return new Promise((resolve, reject) => {
129
+ const chunks = [];
130
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
131
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8").trim()));
132
+ process.stdin.on("error", reject);
133
+ });
134
+ }
135
+ function prompt(question) {
136
+ const rl = readline.createInterface({
137
+ input: process.stdin,
138
+ output: process.stderr,
139
+ });
140
+ return new Promise((resolve) => {
141
+ rl.question(question, (answer) => {
142
+ rl.close();
143
+ resolve(answer.trim());
144
+ });
145
+ });
146
+ }
147
+ async function publicPost(path, body) {
148
+ const response = await fetch(`${API_BASE_URL}${path}`, {
149
+ method: "POST",
150
+ headers: { "Content-Type": "application/json" },
151
+ body: JSON.stringify(body),
152
+ });
153
+ const data = await response.json();
154
+ return { ok: response.ok, status: response.status, data };
155
+ }
156
+ async function handleSignup(positionalEmail, flags) {
157
+ const email = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
158
+ if (!email) {
159
+ console.error("Email is required. Usage: lotics auth signup <email>");
160
+ process.exit(1);
161
+ }
162
+ const name = flags.name ?? (process.stdin.isTTY ? await prompt("Name (enter to use email): ") : undefined);
163
+ const body = { email };
164
+ if (name)
165
+ body.name = name;
166
+ if (flags.timezone)
167
+ body.timezone = flags.timezone;
168
+ const { ok, status, data } = await publicPost("/v1/cli/signup", body);
169
+ if (!ok) {
170
+ if (status === 409) {
171
+ console.error("An account with this email already exists. Use: lotics auth api-key");
172
+ }
173
+ else if (status === 429) {
174
+ console.error("Too many signup attempts. Try again later.");
175
+ }
176
+ else {
177
+ console.error(`Signup failed: ${data.message ?? JSON.stringify(data)}`);
178
+ }
179
+ process.exit(1);
180
+ }
181
+ const scope = flags.local ? "local" : "auto";
182
+ const existing = loadConfig(scope) ?? {};
183
+ saveConfig({
184
+ ...existing,
185
+ api_key: data.api_key,
186
+ email: data.email,
187
+ workspace_id: data.workspace_id,
188
+ }, scope);
189
+ console.error(`Account created. You can now use the CLI.`);
190
+ console.error(` Email: ${data.email}`);
191
+ console.error(` Config: ${getConfigPath(scope)}`);
192
+ console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
193
+ console.error(`Run "lotics auth web" to request a new link at any time.`);
194
+ }
195
+ async function handleSetup(providedKey, local) {
196
+ const apiKey = providedKey ?? await prompt("Enter your API key: ");
197
+ if (!apiKey) {
198
+ console.error("No API key provided.");
199
+ process.exit(1);
200
+ }
201
+ const client = new LoticsClient({ apiKey });
202
+ let email;
203
+ try {
204
+ const info = await client.whoami();
205
+ email = info.email;
206
+ }
207
+ catch (error) {
208
+ const message = error instanceof Error ? error.message : String(error);
209
+ console.error(`Authentication failed: ${message}`);
210
+ process.exit(1);
211
+ }
212
+ const scope = local ? "local" : "auto";
213
+ const existing = loadConfig(scope) ?? {};
214
+ const newConfig = { ...existing, api_key: apiKey, email };
215
+ // Auto-resolve workspace
216
+ try {
217
+ const workspaces = await client.listWorkspaces();
218
+ if (workspaces.length === 1) {
219
+ newConfig.workspace_id = workspaces[0].id;
220
+ }
221
+ else if (workspaces.length > 1) {
222
+ console.error(`\nMultiple workspaces found. Run "lotics workspace select <id>" to choose one:`);
223
+ printWorkspaceList(workspaces);
224
+ }
225
+ }
226
+ catch (error) {
227
+ const msg = error instanceof Error ? error.message : String(error);
228
+ console.error(`Warning: could not resolve workspace: ${msg}`);
229
+ }
230
+ saveConfig(newConfig, scope);
231
+ console.error("Authenticated.");
232
+ console.error(` Config: ${getConfigPath(scope)}`);
233
+ }
234
+ function requireClient(flags) {
235
+ const auth = resolveAuth(flags);
236
+ if (!auth) {
237
+ console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
238
+ process.exit(1);
239
+ }
240
+ const config = loadConfig();
241
+ return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
242
+ }
243
+ function printWorkspaceList(workspaces, currentId) {
244
+ for (const ws of workspaces) {
245
+ const marker = ws.id === currentId ? " (current)" : "";
246
+ console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
247
+ }
248
+ }
249
+ async function resolveWorkspace(client) {
250
+ const config = loadConfig();
251
+ if (config?.workspace_id)
252
+ return;
253
+ let workspaces;
254
+ try {
255
+ workspaces = await client.listWorkspaces();
256
+ }
257
+ catch (error) {
258
+ const message = error instanceof Error ? error.message : String(error);
259
+ console.error(`Failed to list workspaces: ${message}`);
260
+ process.exit(1);
261
+ }
262
+ if (workspaces.length === 0) {
263
+ console.error("No workspaces found for this organization.");
264
+ process.exit(1);
265
+ }
266
+ if (workspaces.length === 1) {
267
+ const existing = config ?? {};
268
+ saveConfig({ ...existing, workspace_id: workspaces[0].id });
269
+ client.setWorkspaceId(workspaces[0].id);
270
+ return;
271
+ }
272
+ // Multiple workspaces, none selected
273
+ console.error("Multiple workspaces available. Select one with:\n");
274
+ console.error(' lotics workspace select <id>\n');
275
+ printWorkspaceList(workspaces);
276
+ console.error("");
277
+ process.exit(1);
278
+ }
279
+ /**
280
+ * Resolve upload paths: files pass through, directories expand to their immediate files.
281
+ */
282
+ function resolveUploadPaths(rawPaths) {
283
+ const result = [];
284
+ for (const p of rawPaths) {
285
+ const resolved = path.resolve(p);
286
+ const stat = fs.statSync(resolved);
287
+ if (stat.isDirectory()) {
288
+ const entries = fs.readdirSync(resolved, { withFileTypes: true });
289
+ for (const entry of entries) {
290
+ if (entry.isFile()) {
291
+ result.push(path.join(resolved, entry.name));
292
+ }
293
+ }
294
+ }
295
+ else {
296
+ result.push(resolved);
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+ async function main() {
302
+ const { command, subcommand, toolArgs, restArgs, flags } = parseArgs(process.argv.slice(2));
303
+ if (flags.help || (!command && !flags.version)) {
304
+ printHelp();
305
+ return;
306
+ }
307
+ if (flags.version) {
308
+ console.log(VERSION);
309
+ return;
310
+ }
311
+ // --- lotics auth <subcommand> ---
312
+ if (command === "auth") {
313
+ if (subcommand === "signup") {
314
+ await handleSignup(toolArgs, flags);
315
+ return;
316
+ }
317
+ if (subcommand === "api-key") {
318
+ await handleSetup(toolArgs ?? flags.apiKey, flags.local);
319
+ return;
320
+ }
321
+ if (subcommand === "whoami") {
322
+ const auth = resolveAuth(flags);
323
+ if (!auth) {
324
+ console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
325
+ process.exit(1);
326
+ }
327
+ const client = new LoticsClient({ apiKey: auth.apiKey });
328
+ const info = await client.whoami();
329
+ const existing = loadConfig() ?? {};
330
+ saveConfig({ ...existing, email: info.email });
331
+ if (flags.json) {
332
+ console.log(JSON.stringify(info, null, 2));
333
+ }
334
+ else {
335
+ console.log(`Name: ${info.name}`);
336
+ console.log(`Email: ${info.email}`);
337
+ console.log(`Org: ${info.organization_name} (${info.organization_id})`);
338
+ }
339
+ console.error(`Config: ${getConfigPath()}`);
340
+ return;
341
+ }
342
+ if (subcommand === "logout") {
343
+ deleteConfig();
344
+ console.error("Logged out. Credentials removed.");
345
+ return;
346
+ }
347
+ if (subcommand === "web") {
348
+ const client = requireClient(flags);
349
+ const { email } = await client.login();
350
+ console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
351
+ return;
352
+ }
353
+ if (subcommand) {
354
+ console.error(`Unknown auth subcommand: ${subcommand}\n`);
355
+ }
356
+ printAuthHelp();
357
+ return;
358
+ }
359
+ // --- Local file commands — no auth required ---
360
+ if (command === "xlsx") {
361
+ await runXlsxCommand(subcommand, toolArgs, restArgs);
362
+ return;
363
+ }
364
+ if (command === "docx") {
365
+ await runDocxCommand(subcommand, toolArgs, restArgs);
366
+ return;
367
+ }
368
+ // --- Validate command before auth ---
369
+ if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
370
+ console.error(`Unknown command: ${command}`);
371
+ console.error('Run "lotics --help" for usage.');
372
+ process.exit(1);
373
+ }
374
+ if (command === "app" && !subcommand) {
375
+ console.error("Usage:");
376
+ console.error(" lotics app create <name> [path] Scaffold a new app locally");
377
+ console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
378
+ console.error(" lotics app deploy [-m <message>] [--force-workflow-sync]");
379
+ console.error(" Build + upload the current directory");
380
+ console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
381
+ console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
382
+ process.exit(1);
383
+ }
384
+ if (command === "run" && !subcommand) {
385
+ console.error('Usage: lotics run <tool> \'<json_args>\'');
386
+ console.error('Run "lotics tools" to see available tools.');
387
+ process.exit(1);
388
+ }
389
+ if (command === "upload" && !subcommand) {
390
+ console.error('Usage: lotics upload <file|dir...> [--as <name>]');
391
+ console.error('Uploads files and returns their file_ids. Directories expand to their immediate files.');
392
+ process.exit(1);
393
+ }
394
+ if (command === "download" && !subcommand) {
395
+ console.error('Usage:');
396
+ console.error(' lotics download <file_id> [-o <dir>]');
397
+ console.error(' lotics download record <record_id> <field_key> [-o <dir>]');
398
+ console.error('File IDs come from upload results or generate_* tool output (--json).');
399
+ process.exit(1);
400
+ }
401
+ if (command === "download" && subcommand === "record" && (!toolArgs || restArgs.length === 0)) {
402
+ console.error('Usage: lotics download record <record_id> <field_key> [-o <dir>]');
403
+ console.error('Downloads every file on the given file field into the output dir.');
404
+ process.exit(1);
405
+ }
406
+ const client = requireClient(flags);
407
+ // lotics workspace / lotics workspace list / lotics workspace select <id>
408
+ if (command === "workspace") {
409
+ const workspaces = await client.listWorkspaces();
410
+ const config = loadConfig();
411
+ if (subcommand === "select") {
412
+ const targetId = toolArgs;
413
+ if (!targetId) {
414
+ console.error('Usage: lotics workspace select <workspace_id>');
415
+ process.exit(1);
416
+ }
417
+ const target = workspaces.find((ws) => ws.id === targetId);
418
+ if (!target) {
419
+ console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
420
+ printWorkspaceList(workspaces, config?.workspace_id);
421
+ process.exit(1);
422
+ }
423
+ const existing = config ?? {};
424
+ saveConfig({ ...existing, workspace_id: target.id });
425
+ console.error(`Switched to workspace: ${target.name} (${target.id})`);
426
+ return;
427
+ }
428
+ if (subcommand === "create") {
429
+ const name = toolArgs;
430
+ if (!name) {
431
+ console.error('Usage: lotics workspace create <name> [--timezone <tz>]');
432
+ process.exit(1);
433
+ }
434
+ const timezone = flags.timezone;
435
+ const created = await client.createWorkspace({ name, timezone });
436
+ const existing = config ?? {};
437
+ saveConfig({ ...existing, workspace_id: created.id });
438
+ client.setWorkspaceId(created.id);
439
+ if (flags.json) {
440
+ console.log(JSON.stringify(created, null, 2));
441
+ }
442
+ else {
443
+ console.error(`Created workspace: ${created.name} (${created.id})`);
444
+ console.error(`Switched to ${created.id}`);
445
+ }
446
+ return;
447
+ }
448
+ // Default: list workspaces
449
+ if (subcommand && subcommand !== "list") {
450
+ console.error(`Unknown workspace subcommand: ${subcommand}`);
451
+ console.error('Usage: lotics workspace [list | select <id> | create <name>]');
452
+ process.exit(1);
453
+ }
454
+ if (flags.json) {
455
+ console.log(JSON.stringify(workspaces, null, 2));
456
+ }
457
+ else {
458
+ if (workspaces.length === 0) {
459
+ console.error("No workspaces found.");
460
+ }
461
+ else {
462
+ for (const ws of workspaces) {
463
+ const marker = ws.id === config?.workspace_id ? " (current)" : "";
464
+ console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
465
+ }
466
+ }
467
+ }
468
+ console.error(`Config: ${getConfigPath()}`);
469
+ return;
470
+ }
471
+ // Ensure workspace is resolved for all remaining commands
472
+ await resolveWorkspace(client);
473
+ // lotics app create / pull / deploy
474
+ if (command === "app") {
475
+ if (subcommand === "create") {
476
+ const name = toolArgs;
477
+ if (!name) {
478
+ console.error("Usage: lotics app create <name> [path]");
479
+ process.exit(1);
480
+ }
481
+ const targetPath = restArgs[0];
482
+ await appCreate(client, { name, targetPath });
483
+ return;
484
+ }
485
+ if (subcommand === "pull") {
486
+ const appId = toolArgs;
487
+ if (!appId) {
488
+ console.error("Usage: lotics app pull <app_id> [path]");
489
+ process.exit(1);
490
+ }
491
+ const targetPath = restArgs[0];
492
+ await appPull(client, { app_id: appId, targetPath });
493
+ return;
494
+ }
495
+ if (subcommand === "deploy") {
496
+ // The message is either `-m <message>` or a bare positional arg after
497
+ // `deploy`. `--force-workflow-sync` is a parsed boolean flag, valid in
498
+ // any position.
499
+ const message = flags.message ?? toolArgs;
500
+ await appDeploy(client, { message, forceWorkflowSync: flags.forceWorkflowSync });
501
+ return;
502
+ }
503
+ if (subcommand === "subdomain") {
504
+ const newSubdomain = toolArgs;
505
+ if (!newSubdomain) {
506
+ console.error("Usage: lotics app subdomain <new-subdomain>");
507
+ console.error("Renames the app's public address — https://<slug>.lotics.app");
508
+ process.exit(1);
509
+ }
510
+ await appSetSubdomain(client, { subdomain: newSubdomain });
511
+ return;
512
+ }
513
+ if (subcommand === "dev") {
514
+ // First positional is an optional project path (defaults to cwd).
515
+ // --port and --vite-port can override the wrapper / Vite ports.
516
+ const projectDir = toolArgs;
517
+ let port;
518
+ let vitePort;
519
+ for (const a of restArgs) {
520
+ const portMatch = /^--port=(\d+)$/.exec(a);
521
+ const vitePortMatch = /^--vite-port=(\d+)$/.exec(a);
522
+ if (portMatch)
523
+ port = Number(portMatch[1]);
524
+ else if (vitePortMatch)
525
+ vitePort = Number(vitePortMatch[1]);
526
+ }
527
+ await appDev(client, { projectDir, port, vitePort });
528
+ return;
529
+ }
530
+ console.error(`Unknown app subcommand: ${subcommand}`);
531
+ console.error("Run 'lotics app' for usage.");
532
+ process.exit(1);
533
+ }
534
+ // lotics tools / lotics tools <name>
535
+ if (command === "tools") {
536
+ if (subcommand) {
537
+ const info = await client.getTool(subcommand);
538
+ if (flags.json) {
539
+ console.log(JSON.stringify(info, null, 2));
540
+ }
541
+ else {
542
+ console.log(`${info.name}\n\n${info.description}\n\nInput schema:\n${JSON.stringify(info.input_schema, null, 2)}`);
543
+ }
544
+ }
545
+ else {
546
+ const { categories } = await client.listTools();
547
+ if (flags.json) {
548
+ console.log(JSON.stringify(categories, null, 2));
549
+ }
550
+ else {
551
+ for (const [category, { description, tools }] of Object.entries(categories)) {
552
+ console.log(description ? `\n## ${category} — ${description}` : `\n## ${category}`);
553
+ console.log(` ${tools.join(", ")}`);
554
+ }
555
+ console.log("");
556
+ }
557
+ }
558
+ return;
559
+ }
560
+ // lotics upload <file|dir...> [--as <name>]
561
+ if (command === "upload") {
562
+ const rawPaths = [subcommand, ...(toolArgs ? [toolArgs] : []), ...restArgs];
563
+ const filePaths = resolveUploadPaths(rawPaths);
564
+ if (filePaths.length === 0) {
565
+ console.error("No files found in the specified paths.");
566
+ process.exit(1);
567
+ }
568
+ if (flags.as && filePaths.length > 1) {
569
+ console.error("Cannot use --as with multiple files.");
570
+ process.exit(1);
571
+ }
572
+ const upload = await client.uploadFiles(filePaths, {
573
+ filenames: flags.as ? [flags.as] : undefined,
574
+ });
575
+ if (upload.files.length === 0 && upload.errors.length > 0) {
576
+ console.error(`Upload failed: ${upload.errors.map((e) => `${e.filename}: ${e.error}`).join(", ")}`);
577
+ process.exit(1);
578
+ }
579
+ if (flags.json) {
580
+ console.log(JSON.stringify({ files: upload.files, errors: upload.errors }, null, 2));
581
+ }
582
+ else {
583
+ for (let i = 0; i < upload.files.length; i++) {
584
+ const file = upload.files[i];
585
+ console.log(`[${i + 1}] ${file.id} ${file.filename} ${file.mime_type}`);
586
+ }
587
+ for (const error of upload.errors) {
588
+ console.error(`FAILED ${error.filename} ${error.error}`);
589
+ }
590
+ }
591
+ return;
592
+ }
593
+ // lotics download <file_id> [-o <path>]
594
+ // lotics download record <record_id> <field_key> [-o <path>]
595
+ if (command === "download") {
596
+ if (subcommand === "record") {
597
+ const recordId = toolArgs;
598
+ const fieldKey = restArgs[0];
599
+ const files = await client.downloadRecordFiles(recordId, fieldKey, flags.output);
600
+ if (flags.json) {
601
+ console.log(JSON.stringify(files, null, 2));
602
+ }
603
+ else {
604
+ for (const f of files) {
605
+ console.log(`${f.file_id} ${f.path} ${f.filename}`);
606
+ }
607
+ console.error(`Downloaded ${files.length} file${files.length === 1 ? "" : "s"} from ${recordId}.${fieldKey}`);
608
+ }
609
+ return;
610
+ }
611
+ const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
612
+ console.error(`Downloaded: ${filePath} (${filename})`);
613
+ return;
614
+ }
615
+ // lotics run <tool> [json_args]
616
+ if (command === "run") {
617
+ const toolName = subcommand;
618
+ let rawArgs = toolArgs;
619
+ if (!rawArgs && !process.stdin.isTTY) {
620
+ rawArgs = await readStdin();
621
+ }
622
+ let args = {};
623
+ if (rawArgs) {
624
+ try {
625
+ args = JSON.parse(rawArgs);
626
+ }
627
+ catch {
628
+ console.error(`Invalid JSON: ${rawArgs}`);
629
+ process.exit(1);
630
+ }
631
+ }
632
+ const timeoutMs = flags.timeout ?? 60000;
633
+ // Always request text format so model_output is available; --json only affects CLI output
634
+ const result = await client.execute(toolName, args, { format: "text", timeoutMs });
635
+ if (result.error) {
636
+ if (flags.json) {
637
+ console.log(JSON.stringify({ error: result.error }, null, 2));
638
+ }
639
+ else {
640
+ console.error(result.error);
641
+ console.error(`\nHint: run "lotics tools ${toolName}" to see the expected input schema.`);
642
+ }
643
+ process.exit(1);
644
+ }
645
+ if (flags.json) {
646
+ console.log(JSON.stringify(result.result, null, 2));
647
+ }
648
+ else if (result.model_output) {
649
+ console.log(result.model_output);
650
+ }
651
+ else {
652
+ // Fallback: compact JSON when no text output is available
653
+ console.log(JSON.stringify(result.result, null, 2));
654
+ }
655
+ return;
656
+ }
657
+ }
658
+ main()
659
+ .then(() => checkForUpdate(VERSION))
660
+ .catch((error) => {
661
+ const message = error instanceof Error ? error.message : String(error);
662
+ if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
663
+ console.error("Could not connect to the Lotics API.");
664
+ }
665
+ else if (message.includes("ENOENT")) {
666
+ const pathMatch = message.match(/open '([^']+)'/);
667
+ console.error(`File not found: ${pathMatch?.[1] ?? message}`);
668
+ }
669
+ else {
670
+ console.error(message);
671
+ }
672
+ process.exit(1);
673
+ });
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * The wrapper page is served at http://localhost:<port>/ and embeds the
5
5
  * project's Vite dev server in an iframe with the same
6
- * sandbox="allow-scripts allow-same-origin" attribute production uses —
7
- * safe because the Vite iframe is cross-origin to this wrapper (different
8
- * port), so the app gets its own real origin (and Web Storage) without
9
- * being able to reach the wrapper. The iframe sends postMessage RPCs to
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. The iframe sends postMessage RPCs to
10
12
  * this wrapper, which forwards them to the local /_rpc endpoint, which
11
13
  * dispatches to api.lotics.ai with the CLI's API key.
12
14
  *