@lotics/cli 0.34.0 → 0.36.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/cli.js CHANGED
@@ -3,9 +3,9 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { LoticsClient, API_BASE_URL } from "./client.js";
6
- import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
6
+ import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
7
7
  import { VERSION } from "./version.js";
8
- import { appCreate, appPull, appDeploy, appDev, appSetSubdomain } from "./app_commands.js";
8
+ import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename } from "./app_commands.js";
9
9
  import { parseArgs } from "./args.js";
10
10
  import { runXlsxCommand } from "./xlsx.js";
11
11
  import { runDocxCommand } from "./docx.js";
@@ -24,20 +24,31 @@ Lotics is an AI-powered operations platform. Through this CLI you can:
24
24
  AUTHENTICATION
25
25
  lotics auth Show auth help
26
26
  lotics auth signup <email> Create account (run "lotics auth" for details)
27
+ lotics auth api-key <key> Save a key — registers its org as a profile
28
+
29
+ ORGANIZATIONS
30
+ Each saved API key belongs to one org. Register a key per org once, then
31
+ switch between them with no re-pasting. Workspaces live inside an org.
32
+ lotics org List saved orgs (marks active)
33
+ lotics org use <name|id> Switch the active org
34
+ lotics org use <name|id> --local Pin THIS directory to an org (worktrees)
27
35
 
28
36
  USAGE
29
37
  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
38
+ 2. lotics org See saved orgs / which one is active
39
+ 3. lotics workspace Check current workspace (auto-selects if only one)
40
+ 4. lotics tools List available tools by category
41
+ 5. lotics tools <name> Show tool description and full input schema
42
+ 6. lotics run <tool> '<json>' Execute a tool with JSON arguments
34
43
 
35
44
  Always inspect the schema (step 4) before calling a tool.
36
45
  Tools are grouped by category (tables, records, views, etc.).
37
46
  Query tools return IDs used as arguments to other tools.
38
47
 
39
48
  COMMANDS
40
- lotics workspace List workspaces (marks current)
49
+ lotics org List saved orgs (marks active)
50
+ lotics org use <name|id> [--local] Switch active org (--local pins this dir)
51
+ lotics workspace List workspaces in the active org (marks current)
41
52
  lotics workspace select <id> Switch to a different workspace
42
53
  lotics workspace create <name> Create a new workspace (admin only)
43
54
  lotics tools List all available tools
@@ -54,6 +65,7 @@ COMMANDS
54
65
  (--force-workflow-sync wipes server-only
55
66
  workflow aliases; default refuses)
56
67
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
68
+ lotics app rename "<new name>" Rename the app's display name (launcher title)
57
69
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
58
70
  lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
59
71
  (uses the bundled Lotics xlsx engine; prefer over
@@ -67,15 +79,23 @@ FLAGS
67
79
  --timeout <ms> Timeout for tool execution (default: 60000)
68
80
  -o <path> Output dir for downloads
69
81
  --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
82
+ --api-key <key> One-off API key (overrides saved config + env)
83
+ --workspace <id> One-off workspace override (alias: -w)
84
+ --local Pin the current directory (lotics org use / auth api-key)
85
+ --all (lotics auth logout) Remove every saved credential
72
86
  --version Show version
73
87
 
74
88
  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.
89
+ API keys are stored once per org in the global ~/.lotics/config.json as named
90
+ profiles. Switch with "lotics org use <name|id>" — no re-pasting. A directory
91
+ (e.g. a git worktree) can pin its own org/workspace with "lotics org use
92
+ <name|id> --local", which writes a .lotics/config.json pointer (the key is
93
+ resolved from the global store) — so a global switch never disturbs it.
94
+
95
+ Resolution precedence (highest first):
96
+ --api-key flag > LOTICS_API_KEY env > LOTICS_ORG env >
97
+ local .lotics/config.json > global active profile
98
+ LOTICS_WORKSPACE (or --workspace) overrides the workspace at any level.
79
99
 
80
100
  OUTPUT
81
101
  Default output is a compact text summary optimized for AI agents —
@@ -104,25 +124,27 @@ function printAuthHelp() {
104
124
  console.log(`Authentication commands:
105
125
 
106
126
  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)
127
+ lotics auth api-key [key] Save an API key registers its org as a profile
108
128
  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
129
+ lotics auth whoami Show the active account, org, workspace, and source
130
+ lotics auth logout [<name|id>] Remove a saved credential (default: the active org,
131
+ or unpin the current directory)
132
+ lotics auth logout --all Remove every saved credential
111
133
 
112
134
  Auth flags:
113
- --local Save credentials to ./.lotics/config.json (this directory)
114
- instead of the global ~/.lotics — applies to signup + api-key
135
+ --local Pin the current directory instead of the global store.
136
+ signup/api-key write a self-contained ./.lotics/config.json
137
+ (inline key) — for a hermetic worktree or CI.
115
138
  --name <name> (signup) Display name (defaults to email prefix)
116
139
  --timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
117
140
 
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.
141
+ API keys are stored once per org as named profiles in ~/.lotics/config.json.
142
+ Registering a second key adds a profile it does not overwrite the first.
143
+ Switch active org with "lotics org use <name|id>".
120
144
 
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.`);
145
+ Resolution precedence (highest first):
146
+ --api-key flag > LOTICS_API_KEY env > LOTICS_ORG env > local .lotics/config.json
147
+ > global active profile. LOTICS_WORKSPACE / --workspace override the workspace.`);
126
148
  }
127
149
  function readStdin() {
128
150
  return new Promise((resolve, reject) => {
@@ -178,17 +200,25 @@ async function handleSignup(positionalEmail, flags) {
178
200
  }
179
201
  process.exit(1);
180
202
  }
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);
203
+ const apiKey = data.api_key;
204
+ const orgId = data.organization_id;
205
+ const workspaceId = data.workspace_id;
206
+ // The org is created with the display name; mirror the server's email-prefix
207
+ // default when none was supplied. Use `||` (not `??`) so an empty-string name
208
+ // falls through exactly as the server's `if (name)` truthiness check does.
209
+ const orgName = name || email.split("@")[0];
210
+ // The key always lives in the global store as a profile; --local additionally
211
+ // pins this directory to it (a pointer), instead of making it the global default.
212
+ upsertProfile(orgId, { api_key: apiKey, org_name: orgName, workspace_id: workspaceId });
213
+ const existing = loadGlobalConfig() ?? {};
214
+ saveGlobalConfig({ ...existing, email });
215
+ setActiveOrg(orgId, flags.local ? "local" : "global");
189
216
  console.error(`Account created. You can now use the CLI.`);
190
- console.error(` Email: ${data.email}`);
191
- console.error(` Config: ${getConfigPath(scope)}`);
217
+ console.error(` Email: ${email}`);
218
+ console.error(` Org: ${orgName} (${orgId})`);
219
+ console.error(flags.local
220
+ ? ` Pinned this directory: ${getConfigPath("local")}`
221
+ : ` Saved to the "${orgName}" profile in ~/.lotics (now active)`);
192
222
  console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
193
223
  console.error(`Run "lotics auth web" to request a new link at any time.`);
194
224
  }
@@ -199,27 +229,24 @@ async function handleSetup(providedKey, local) {
199
229
  process.exit(1);
200
230
  }
201
231
  const client = new LoticsClient({ apiKey });
202
- let email;
232
+ let info;
203
233
  try {
204
- const info = await client.whoami();
205
- email = info.email;
234
+ info = await client.whoami();
206
235
  }
207
236
  catch (error) {
208
237
  const message = error instanceof Error ? error.message : String(error);
209
238
  console.error(`Authentication failed: ${message}`);
210
239
  process.exit(1);
211
240
  }
212
- const scope = local ? "local" : "auto";
213
- const existing = loadConfig(scope) ?? {};
214
- const newConfig = { ...existing, api_key: apiKey, email };
215
- // Auto-resolve workspace
241
+ // Auto-resolve workspace (only when the org has exactly one).
242
+ let workspaceId;
216
243
  try {
217
244
  const workspaces = await client.listWorkspaces();
218
245
  if (workspaces.length === 1) {
219
- newConfig.workspace_id = workspaces[0].id;
246
+ workspaceId = workspaces[0].id;
220
247
  }
221
248
  else if (workspaces.length > 1) {
222
- console.error(`\nMultiple workspaces found. Run "lotics workspace select <id>" to choose one:`);
249
+ console.error(`\nMultiple workspaces in ${info.organization_name}. Run "lotics workspace select <id>" to choose one:`);
223
250
  printWorkspaceList(workspaces);
224
251
  }
225
252
  }
@@ -227,29 +254,51 @@ async function handleSetup(providedKey, local) {
227
254
  const msg = error instanceof Error ? error.message : String(error);
228
255
  console.error(`Warning: could not resolve workspace: ${msg}`);
229
256
  }
230
- saveConfig(newConfig, scope);
231
- console.error("Authenticated.");
232
- console.error(` Config: ${getConfigPath(scope)}`);
257
+ // The key always lives in the global store as a profile; --local additionally
258
+ // pins this directory to it (a pointer), instead of making it the global default.
259
+ upsertProfile(info.organization_id, {
260
+ api_key: apiKey,
261
+ org_name: info.organization_name,
262
+ workspace_id: workspaceId,
263
+ });
264
+ const existing = loadGlobalConfig() ?? {};
265
+ saveGlobalConfig({ ...existing, email: info.email });
266
+ setActiveOrg(info.organization_id, local ? "local" : "global");
267
+ if (local) {
268
+ console.error(`Authenticated as ${info.email} in ${info.organization_name}.`);
269
+ console.error(` Pinned this directory: ${getConfigPath("local")}`);
270
+ }
271
+ else {
272
+ console.error(`Authenticated as ${info.email} in ${info.organization_name} (now active).`);
273
+ console.error(` Saved to the "${info.organization_name}" profile in ~/.lotics`);
274
+ }
233
275
  }
234
276
  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.');
277
+ const ctx = resolveContext(flags);
278
+ if (!ctx) {
279
+ console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
238
280
  process.exit(1);
239
281
  }
240
- const config = loadConfig();
241
- return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
282
+ return { client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId }), ctx };
242
283
  }
284
+ const SOURCE_LABELS = {
285
+ flag: "--api-key flag",
286
+ env_key: "LOTICS_API_KEY env",
287
+ env_org: "LOTICS_ORG env",
288
+ local_pointer: "local .lotics/config.json (pin)",
289
+ global_profile: "global active profile",
290
+ };
243
291
  function printWorkspaceList(workspaces, currentId) {
244
292
  for (const ws of workspaces) {
245
293
  const marker = ws.id === currentId ? " (current)" : "";
246
294
  console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
247
295
  }
248
296
  }
249
- async function resolveWorkspace(client) {
250
- const config = loadConfig();
251
- if (config?.workspace_id)
297
+ async function resolveWorkspace(client, ctx) {
298
+ if (ctx.workspaceId) {
299
+ client.setWorkspaceId(ctx.workspaceId);
252
300
  return;
301
+ }
253
302
  let workspaces;
254
303
  try {
255
304
  workspaces = await client.listWorkspaces();
@@ -264,8 +313,9 @@ async function resolveWorkspace(client) {
264
313
  process.exit(1);
265
314
  }
266
315
  if (workspaces.length === 1) {
267
- const existing = config ?? {};
268
- saveConfig({ ...existing, workspace_id: workspaces[0].id });
316
+ // Persist the auto-resolution where the credential lives; null means it
317
+ // came from --api-key/env with no profile, so it's an in-memory selection.
318
+ setSelectedWorkspace(workspaces[0].id);
269
319
  client.setWorkspaceId(workspaces[0].id);
270
320
  return;
271
321
  }
@@ -319,33 +369,66 @@ async function main() {
319
369
  return;
320
370
  }
321
371
  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.');
372
+ const ctx = resolveContext(flags);
373
+ if (!ctx) {
374
+ console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
325
375
  process.exit(1);
326
376
  }
327
- const client = new LoticsClient({ apiKey: auth.apiKey });
377
+ const client = new LoticsClient({ apiKey: ctx.apiKey });
328
378
  const info = await client.whoami();
329
- const existing = loadConfig() ?? {};
330
- saveConfig({ ...existing, email: info.email });
379
+ const existing = loadGlobalConfig() ?? {};
380
+ saveGlobalConfig({ ...existing, email: info.email });
331
381
  if (flags.json) {
332
- console.log(JSON.stringify(info, null, 2));
382
+ console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source }, null, 2));
333
383
  }
334
384
  else {
335
- console.log(`Name: ${info.name}`);
336
- console.log(`Email: ${info.email}`);
337
- console.log(`Org: ${info.organization_name} (${info.organization_id})`);
385
+ console.log(`Name: ${info.name}`);
386
+ console.log(`Email: ${info.email}`);
387
+ console.log(`Org: ${info.organization_name} (${info.organization_id})`);
388
+ console.log(`Workspace: ${ctx.workspaceId ?? "(none selected)"}`);
389
+ console.log(`Source: ${SOURCE_LABELS[ctx.source]}`);
338
390
  }
339
- console.error(`Config: ${getConfigPath()}`);
340
391
  return;
341
392
  }
342
393
  if (subcommand === "logout") {
343
- deleteConfig();
344
- console.error("Logged out. Credentials removed.");
394
+ const local = loadLocalConfig();
395
+ // Bare logout inside a pinned directory just unpins it.
396
+ if (local && !flags.all && !toolArgs) {
397
+ deleteConfig();
398
+ console.error("Removed the local pin for this directory.");
399
+ return;
400
+ }
401
+ if (flags.all) {
402
+ const g = loadGlobalConfig();
403
+ saveGlobalConfig({ last_update_check: g?.last_update_check, latest_version: g?.latest_version });
404
+ console.error("Removed all saved credentials.");
405
+ return;
406
+ }
407
+ const g = loadGlobalConfig() ?? {};
408
+ const profiles = g.profiles ?? {};
409
+ let resolved;
410
+ if (toolArgs) {
411
+ resolved = resolveProfileByNameOrId(profiles, toolArgs);
412
+ }
413
+ else if (g.active_org && profiles[g.active_org]) {
414
+ resolved = [g.active_org, profiles[g.active_org]];
415
+ }
416
+ else {
417
+ resolved = null;
418
+ }
419
+ if (!resolved) {
420
+ console.error(toolArgs
421
+ ? `No saved credential matches "${toolArgs}".`
422
+ : 'No active credential to remove. Pass an org name/id, or use --all.');
423
+ process.exit(1);
424
+ }
425
+ const [orgId, profile] = resolved;
426
+ removeProfile(orgId);
427
+ console.error(`Removed the "${profile.org_name}" credential (${orgId}).`);
345
428
  return;
346
429
  }
347
430
  if (subcommand === "web") {
348
- const client = requireClient(flags);
431
+ const { client } = requireClient(flags);
349
432
  const { email } = await client.login();
350
433
  console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
351
434
  return;
@@ -365,6 +448,63 @@ async function main() {
365
448
  await runDocxCommand(subcommand, toolArgs, restArgs);
366
449
  return;
367
450
  }
451
+ // --- lotics org [list | use <name|id> [--local]] — credential store, config-only ---
452
+ if (command === "org") {
453
+ const global = loadGlobalConfig() ?? {};
454
+ const profiles = global.profiles ?? {};
455
+ const local = loadLocalConfig();
456
+ if (subcommand === "use") {
457
+ if (!toolArgs) {
458
+ console.error("Usage: lotics org use <name|id> [--local]");
459
+ process.exit(1);
460
+ }
461
+ const resolved = resolveProfileByNameOrId(profiles, toolArgs);
462
+ if (!resolved) {
463
+ console.error(`No saved credential for "${toolArgs}". Run "lotics auth api-key <key>" while authenticated to that org, or "lotics org" to list.`);
464
+ process.exit(1);
465
+ }
466
+ const [orgId, profile] = resolved;
467
+ if (flags.local) {
468
+ setActiveOrg(orgId, "local");
469
+ console.error(`Pinned this directory to ${profile.org_name} (${orgId}).`);
470
+ }
471
+ else {
472
+ setActiveOrg(orgId, "global");
473
+ console.error(`Switched active org to ${profile.org_name} (${orgId}).`);
474
+ if (local?.active_org) {
475
+ console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
476
+ }
477
+ }
478
+ return;
479
+ }
480
+ if (subcommand && subcommand !== "list") {
481
+ console.error(`Unknown org subcommand: ${subcommand}`);
482
+ console.error("Usage: lotics org [list | use <name|id> [--local]]");
483
+ process.exit(1);
484
+ }
485
+ const orgIds = Object.keys(profiles);
486
+ if (orgIds.length === 0) {
487
+ console.error('No saved orgs. Run "lotics auth api-key <key>" to add one.');
488
+ return;
489
+ }
490
+ // The active org in THIS directory: a local pin wins over the global default.
491
+ const effectiveActive = local?.active_org ?? global.active_org;
492
+ if (flags.json) {
493
+ console.log(JSON.stringify(orgIds.map((id) => ({
494
+ org_id: id,
495
+ org_name: profiles[id].org_name,
496
+ workspace_id: profiles[id].workspace_id ?? null,
497
+ active: id === effectiveActive,
498
+ })), null, 2));
499
+ }
500
+ else {
501
+ for (const id of orgIds) {
502
+ const marker = id === effectiveActive ? " (active)" : "";
503
+ console.log(`${id} ${profiles[id].org_name}${marker}`);
504
+ }
505
+ }
506
+ return;
507
+ }
368
508
  // --- Validate command before auth ---
369
509
  if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
370
510
  console.error(`Unknown command: ${command}`);
@@ -378,6 +518,7 @@ async function main() {
378
518
  console.error(" lotics app deploy [-m <message>] [--force-workflow-sync]");
379
519
  console.error(" Build + upload the current directory");
380
520
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
521
+ console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
381
522
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
382
523
  process.exit(1);
383
524
  }
@@ -403,11 +544,11 @@ async function main() {
403
544
  console.error('Downloads every file on the given file field into the output dir.');
404
545
  process.exit(1);
405
546
  }
406
- const client = requireClient(flags);
547
+ const { client, ctx } = requireClient(flags);
407
548
  // lotics workspace / lotics workspace list / lotics workspace select <id>
408
549
  if (command === "workspace") {
409
550
  const workspaces = await client.listWorkspaces();
410
- const config = loadConfig();
551
+ const currentWorkspaceId = ctx.workspaceId;
411
552
  if (subcommand === "select") {
412
553
  const targetId = toolArgs;
413
554
  if (!targetId) {
@@ -417,12 +558,16 @@ async function main() {
417
558
  const target = workspaces.find((ws) => ws.id === targetId);
418
559
  if (!target) {
419
560
  console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
420
- printWorkspaceList(workspaces, config?.workspace_id);
561
+ printWorkspaceList(workspaces, currentWorkspaceId);
421
562
  process.exit(1);
422
563
  }
423
- const existing = config ?? {};
424
- saveConfig({ ...existing, workspace_id: target.id });
425
- console.error(`Switched to workspace: ${target.name} (${target.id})`);
564
+ const written = setSelectedWorkspace(target.id);
565
+ if (!written) {
566
+ console.error(`Switched to ${target.name} (${target.id}) for this command, but it could not be saved — credentials came from --api-key/LOTICS_API_KEY with no saved profile. Run "lotics auth api-key <key>" to persist a default.`);
567
+ return;
568
+ }
569
+ const where = written.scope === "local" ? " (pinned to this directory)" : "";
570
+ console.error(`Switched to workspace: ${target.name} (${target.id})${where}`);
426
571
  return;
427
572
  }
428
573
  if (subcommand === "create") {
@@ -433,8 +578,7 @@ async function main() {
433
578
  }
434
579
  const timezone = flags.timezone;
435
580
  const created = await client.createWorkspace({ name, timezone });
436
- const existing = config ?? {};
437
- saveConfig({ ...existing, workspace_id: created.id });
581
+ setSelectedWorkspace(created.id);
438
582
  client.setWorkspaceId(created.id);
439
583
  if (flags.json) {
440
584
  console.log(JSON.stringify(created, null, 2));
@@ -460,16 +604,17 @@ async function main() {
460
604
  }
461
605
  else {
462
606
  for (const ws of workspaces) {
463
- const marker = ws.id === config?.workspace_id ? " (current)" : "";
607
+ const marker = ws.id === currentWorkspaceId ? " (current)" : "";
464
608
  console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
465
609
  }
466
610
  }
467
611
  }
468
- console.error(`Config: ${getConfigPath()}`);
612
+ if (ctx.orgName)
613
+ console.error(`Org: ${ctx.orgName}`);
469
614
  return;
470
615
  }
471
616
  // Ensure workspace is resolved for all remaining commands
472
- await resolveWorkspace(client);
617
+ await resolveWorkspace(client, ctx);
473
618
  // lotics app create / pull / deploy
474
619
  if (command === "app") {
475
620
  if (subcommand === "create") {
@@ -510,6 +655,16 @@ async function main() {
510
655
  await appSetSubdomain(client, { subdomain: newSubdomain });
511
656
  return;
512
657
  }
658
+ if (subcommand === "rename") {
659
+ const newName = toolArgs;
660
+ if (!newName) {
661
+ console.error('Usage: lotics app rename "<new name>"');
662
+ console.error("Changes the app's display name (run inside the app directory).");
663
+ process.exit(1);
664
+ }
665
+ await appRename(client, { name: newName });
666
+ return;
667
+ }
513
668
  if (subcommand === "dev") {
514
669
  // First positional is an optional project path (defaults to cwd).
515
670
  // --port and --vite-port can override the wrapper / Vite ports.
package/dist/client.js CHANGED
@@ -262,10 +262,15 @@ export class LoticsClient {
262
262
  return absolutePath;
263
263
  }
264
264
  async downloadFileById(fileId, outputDir, options) {
265
- const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/download`;
266
- const response = await fetch(url, {
267
- headers: this.buildHeaders(),
268
- });
265
+ // Resolve a presigned URL (the file proxy is auth-gated and served by an
266
+ // edge Worker that doesn't accept the API key). The signed_url endpoint
267
+ // takes the API key and returns a short-lived, directly-fetchable URL
268
+ // whose Content-Disposition carries the original filename.
269
+ const signedUrlRes = await fetch(`${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/signed_url`, { headers: this.buildHeaders() });
270
+ if (!signedUrlRes.ok)
271
+ await this.throwResponseError(signedUrlRes);
272
+ const { url } = (await signedUrlRes.json());
273
+ const response = await fetch(url);
269
274
  if (!response.ok)
270
275
  await this.throwResponseError(response);
271
276
  const disposition = response.headers.get("content-disposition") ?? "";