@lotics/cli 0.6.0 → 0.7.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/README.md CHANGED
@@ -25,13 +25,19 @@ The CLI checks for updates once per day and prompts when a new version is availa
25
25
 
26
26
  ## Authentication
27
27
 
28
- **`lotics signup`** — Creates a new Lotics account, organization, workspace, and API key in one step. Prints email and auto-generated password for web app login.
28
+ **`lotics signup`** — Creates a new Lotics account, organization, workspace, and API key in one step. Sends a magic link email so you can access the web app.
29
29
 
30
30
  ```bash
31
31
  lotics signup # interactive prompts
32
32
  lotics signup --email a@b.com --name "Agent" # non-interactive
33
33
  ```
34
34
 
35
+ **`lotics login`** — Send a magic link email to access the web app (requires prior signup or setup).
36
+
37
+ ```bash
38
+ lotics login
39
+ ```
40
+
35
41
  **`lotics setup`** — Saves an existing API key directly (e.g. one created in the Lotics web app).
36
42
 
37
43
  ```bash
@@ -43,6 +49,17 @@ API key is saved to `~/.lotics/config.json`. Run `lotics logout` to remove saved
43
49
 
44
50
  Auth priority: `--api-key` flag > `LOTICS_API_KEY` env > `~/.lotics/config.json`.
45
51
 
52
+ ## Workspaces
53
+
54
+ If your organization has multiple workspaces, select one before running tools:
55
+
56
+ ```bash
57
+ lotics workspace # list workspaces (marks current)
58
+ lotics workspace select wks_... # switch to a workspace
59
+ ```
60
+
61
+ Single-workspace organizations auto-select on first use.
62
+
46
63
  ## CLI
47
64
 
48
65
  ```bash
package/dist/src/cli.js CHANGED
@@ -16,6 +16,7 @@ Lotics is an AI-powered operations platform. Through this CLI you can:
16
16
 
17
17
  AUTHENTICATION
18
18
  lotics signup Create account, org, workspace, and API key
19
+ lotics login Send a magic link email to access the web app
19
20
  lotics setup [api_key] Save an existing API key (e.g. from the web app)
20
21
  lotics whoami Show the email of the current account
21
22
  lotics logout Remove saved credentials
@@ -25,21 +26,24 @@ AUTHENTICATION
25
26
  --name <name> Display name (defaults to email prefix)
26
27
  --timezone <timezone> Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
27
28
 
28
- Signup prints email and auto-generated password for web app login.
29
- Use lotics whoami to check the current account email.
29
+ Signup sends a magic link email so you can access the Lotics web app.
30
+ Use lotics login to request a new magic link at any time.
30
31
  Setup priority: --api-key flag > LOTICS_API_KEY env > saved config.
31
32
 
32
33
  USAGE
33
34
  1. lotics signup (or lotics setup) Create account or set up API key
34
- 2. lotics tools List available tools by category
35
- 3. lotics tools <name> Show tool description and full input schema
36
- 4. lotics run <tool> '<json>' Execute a tool with JSON arguments
35
+ 2. lotics workspace Check current workspace (auto-selects if only one)
36
+ 3. lotics tools List available tools by category
37
+ 4. lotics tools <name> Show tool description and full input schema
38
+ 5. lotics run <tool> '<json>' Execute a tool with JSON arguments
37
39
 
38
- Always inspect the schema (step 3) before calling a tool.
40
+ Always inspect the schema (step 4) before calling a tool.
39
41
  Tools are grouped by category (tables, records, views, etc.).
40
42
  Query tools return IDs used as arguments to other tools.
41
43
 
42
44
  COMMANDS
45
+ lotics workspace List workspaces (marks current)
46
+ lotics workspace select <id> Switch to a different workspace
43
47
  lotics tools List all available tools
44
48
  lotics tools <name> Show tool description and input schema
45
49
  lotics run <tool> '<json>' Execute a tool
@@ -204,12 +208,16 @@ async function handleSignup(flags) {
204
208
  process.exit(1);
205
209
  }
206
210
  const existing = loadConfig() ?? {};
207
- saveConfig({ ...existing, api_key: data.api_key, email: data.email });
211
+ saveConfig({
212
+ ...existing,
213
+ api_key: data.api_key,
214
+ email: data.email,
215
+ workspace_id: data.workspace_id,
216
+ });
208
217
  console.error(`Account created. You can now use the CLI.`);
209
- console.error(` Email: ${data.email}`);
210
- console.error(` Password: ${data.password}`);
211
- console.error(`\nUse these credentials to log into the Lotics web app.`);
212
- console.error(`Run "lotics whoami" to check the current account email.`);
218
+ console.error(` Email: ${data.email}`);
219
+ console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
220
+ console.error(`Run "lotics login" to request a new link at any time.`);
213
221
  }
214
222
  async function handleSetup(providedKey) {
215
223
  const apiKey = providedKey ?? await prompt("Enter your API key: ");
@@ -229,7 +237,23 @@ async function handleSetup(providedKey) {
229
237
  process.exit(1);
230
238
  }
231
239
  const existing = loadConfig() ?? {};
232
- saveConfig({ ...existing, api_key: apiKey, email });
240
+ const newConfig = { ...existing, api_key: apiKey, email };
241
+ // Auto-resolve workspace
242
+ try {
243
+ const workspaces = await client.listWorkspaces();
244
+ if (workspaces.length === 1) {
245
+ newConfig.workspace_id = workspaces[0].id;
246
+ }
247
+ else if (workspaces.length > 1) {
248
+ console.error(`\nMultiple workspaces found. Run "lotics workspace select <id>" to choose one:`);
249
+ printWorkspaceList(workspaces);
250
+ }
251
+ }
252
+ catch (error) {
253
+ const msg = error instanceof Error ? error.message : String(error);
254
+ console.error(`Warning: could not resolve workspace: ${msg}`);
255
+ }
256
+ saveConfig(newConfig);
233
257
  console.error("Authenticated.");
234
258
  }
235
259
  function requireClient(flags) {
@@ -238,7 +262,44 @@ function requireClient(flags) {
238
262
  console.error('Not authenticated. Run "lotics setup" or set LOTICS_API_KEY.');
239
263
  process.exit(1);
240
264
  }
241
- return new LoticsClient({ apiKey: auth.apiKey });
265
+ const config = loadConfig();
266
+ return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
267
+ }
268
+ function printWorkspaceList(workspaces, currentId) {
269
+ for (const ws of workspaces) {
270
+ const marker = ws.id === currentId ? " (current)" : "";
271
+ console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
272
+ }
273
+ }
274
+ async function resolveWorkspace(client) {
275
+ const config = loadConfig();
276
+ if (config?.workspace_id)
277
+ return;
278
+ let workspaces;
279
+ try {
280
+ workspaces = await client.listWorkspaces();
281
+ }
282
+ catch (error) {
283
+ const message = error instanceof Error ? error.message : String(error);
284
+ console.error(`Failed to list workspaces: ${message}`);
285
+ process.exit(1);
286
+ }
287
+ if (workspaces.length === 0) {
288
+ console.error("No workspaces found for this organization.");
289
+ process.exit(1);
290
+ }
291
+ if (workspaces.length === 1) {
292
+ const existing = config ?? {};
293
+ saveConfig({ ...existing, workspace_id: workspaces[0].id });
294
+ client.setWorkspaceId(workspaces[0].id);
295
+ return;
296
+ }
297
+ // Multiple workspaces, none selected
298
+ console.error("Multiple workspaces available. Select one with:\n");
299
+ console.error(' lotics workspace select <id>\n');
300
+ printWorkspaceList(workspaces);
301
+ console.error("");
302
+ process.exit(1);
242
303
  }
243
304
  /**
244
305
  * Resolve upload paths: files pass through, directories expand to their immediate files.
@@ -313,8 +374,14 @@ async function main() {
313
374
  console.error("Logged out. Credentials removed.");
314
375
  return;
315
376
  }
377
+ if (command === "login") {
378
+ const client = requireClient(flags);
379
+ const { email } = await client.login();
380
+ console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
381
+ return;
382
+ }
316
383
  // --- Validate command before auth ---
317
- if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download") {
384
+ if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace") {
318
385
  console.error(`Unknown command: ${command}`);
319
386
  console.error('Run "lotics --help" for usage.');
320
387
  process.exit(1);
@@ -335,6 +402,51 @@ async function main() {
335
402
  process.exit(1);
336
403
  }
337
404
  const client = requireClient(flags);
405
+ // lotics workspace / lotics workspace list / lotics workspace select <id>
406
+ if (command === "workspace") {
407
+ const workspaces = await client.listWorkspaces();
408
+ const config = loadConfig();
409
+ if (subcommand === "select") {
410
+ const targetId = toolArgs;
411
+ if (!targetId) {
412
+ console.error('Usage: lotics workspace select <workspace_id>');
413
+ process.exit(1);
414
+ }
415
+ const target = workspaces.find((ws) => ws.id === targetId);
416
+ if (!target) {
417
+ console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
418
+ printWorkspaceList(workspaces, config?.workspace_id);
419
+ process.exit(1);
420
+ }
421
+ const existing = config ?? {};
422
+ saveConfig({ ...existing, workspace_id: target.id });
423
+ console.error(`Switched to workspace: ${target.name} (${target.id})`);
424
+ return;
425
+ }
426
+ // Default: list workspaces
427
+ if (subcommand && subcommand !== "list") {
428
+ console.error(`Unknown workspace subcommand: ${subcommand}`);
429
+ console.error('Usage: lotics workspace [list | select <id>]');
430
+ process.exit(1);
431
+ }
432
+ if (flags.json) {
433
+ console.log(JSON.stringify(workspaces, null, 2));
434
+ }
435
+ else {
436
+ if (workspaces.length === 0) {
437
+ console.error("No workspaces found.");
438
+ }
439
+ else {
440
+ for (const ws of workspaces) {
441
+ const marker = ws.id === config?.workspace_id ? " (current)" : "";
442
+ console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
443
+ }
444
+ }
445
+ }
446
+ return;
447
+ }
448
+ // Ensure workspace is resolved for all remaining commands
449
+ await resolveWorkspace(client);
338
450
  // lotics tools / lotics tools <name>
339
451
  if (command === "tools") {
340
452
  if (subcommand) {
@@ -1,5 +1,14 @@
1
1
  export interface LoticsClientOptions {
2
2
  apiKey: string;
3
+ workspaceId?: string;
4
+ }
5
+ export interface WorkspaceInfo {
6
+ id: string;
7
+ name: string;
8
+ timezone: string;
9
+ default_currency: string;
10
+ organization_id: string;
11
+ created_at: string;
3
12
  }
4
13
  export interface ToolExecuteResult {
5
14
  result: unknown;
@@ -25,15 +34,22 @@ export interface FileUploadResult {
25
34
  export declare const API_BASE_URL: string;
26
35
  export declare class LoticsClient {
27
36
  private apiKey;
37
+ private workspaceId;
28
38
  private baseUrl;
29
39
  constructor(options: LoticsClientOptions);
30
40
  private throwResponseError;
41
+ private buildHeaders;
31
42
  private request;
32
43
  whoami(): Promise<{
33
44
  email: string;
34
45
  name: string;
35
46
  organization_id: string;
36
47
  }>;
48
+ setWorkspaceId(id: string): void;
49
+ listWorkspaces(): Promise<WorkspaceInfo[]>;
50
+ login(): Promise<{
51
+ email: string;
52
+ }>;
37
53
  listTools(): Promise<{
38
54
  tools: string[];
39
55
  categories: Record<string, {
@@ -24,9 +24,11 @@ function getMimeType(filename) {
24
24
  export const API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
25
25
  export class LoticsClient {
26
26
  apiKey;
27
+ workspaceId;
27
28
  baseUrl;
28
29
  constructor(options) {
29
30
  this.apiKey = options.apiKey;
31
+ this.workspaceId = options.workspaceId;
30
32
  this.baseUrl = API_BASE_URL;
31
33
  }
32
34
  async throwResponseError(response) {
@@ -41,11 +43,18 @@ export class LoticsClient {
41
43
  }
42
44
  throw new Error(`${response.status}: ${message}`);
43
45
  }
44
- async request(method, path, body) {
45
- const url = `${this.baseUrl}${path}`;
46
+ buildHeaders() {
46
47
  const headers = {
47
48
  "x-api-key": this.apiKey,
48
49
  };
50
+ if (this.workspaceId) {
51
+ headers["x-workspace-id"] = this.workspaceId;
52
+ }
53
+ return headers;
54
+ }
55
+ async request(method, path, body) {
56
+ const url = `${this.baseUrl}${path}`;
57
+ const headers = this.buildHeaders();
49
58
  const init = { method, headers };
50
59
  if (body !== undefined) {
51
60
  headers["Content-Type"] = "application/json";
@@ -59,6 +68,15 @@ export class LoticsClient {
59
68
  async whoami() {
60
69
  return this.request("GET", "/v1/cli/whoami");
61
70
  }
71
+ setWorkspaceId(id) {
72
+ this.workspaceId = id;
73
+ }
74
+ async listWorkspaces() {
75
+ return this.request("GET", "/v1/workspaces");
76
+ }
77
+ async login() {
78
+ return this.request("POST", "/v1/cli/login");
79
+ }
62
80
  async listTools() {
63
81
  return this.request("GET", "/v1/tools");
64
82
  }
@@ -74,7 +92,7 @@ export class LoticsClient {
74
92
  const url = `${this.baseUrl}/v1/tools/execute`;
75
93
  const response = await fetch(url, {
76
94
  method: "POST",
77
- headers: { "x-api-key": this.apiKey, "Content-Type": "application/json" },
95
+ headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
78
96
  body: JSON.stringify(body),
79
97
  signal: controller.signal,
80
98
  });
@@ -107,7 +125,7 @@ export class LoticsClient {
107
125
  async downloadFileById(fileId, outputDir) {
108
126
  const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/download`;
109
127
  const response = await fetch(url, {
110
- headers: { "x-api-key": this.apiKey },
128
+ headers: this.buildHeaders(),
111
129
  });
112
130
  if (!response.ok)
113
131
  await this.throwResponseError(response);
@@ -133,7 +151,7 @@ export class LoticsClient {
133
151
  const url = `${this.baseUrl}/v1/files`;
134
152
  const response = await fetch(url, {
135
153
  method: "POST",
136
- headers: { "x-api-key": this.apiKey },
154
+ headers: this.buildHeaders(),
137
155
  body: formData,
138
156
  });
139
157
  if (!response.ok)
@@ -1,6 +1,7 @@
1
1
  export interface LoticsConfig {
2
2
  api_key?: string;
3
3
  email?: string;
4
+ workspace_id?: string;
4
5
  last_update_check?: number;
5
6
  latest_version?: string;
6
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {