@lotics/cli 0.5.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,24 +25,41 @@ 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 auth`** — Saves an existing API key directly (e.g. one created in the Lotics web app).
35
+ **`lotics login`** — Send a magic link email to access the web app (requires prior signup or setup).
36
36
 
37
37
  ```bash
38
- lotics auth # interactive prompt
39
- lotics auth ltk_... # non-interactive
38
+ lotics login
39
+ ```
40
+
41
+ **`lotics setup`** — Saves an existing API key directly (e.g. one created in the Lotics web app).
42
+
43
+ ```bash
44
+ lotics setup # interactive prompt
45
+ lotics setup ltk_... # non-interactive
40
46
  ```
41
47
 
42
48
  API key is saved to `~/.lotics/config.json`. Run `lotics logout` to remove saved credentials.
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,29 +16,34 @@ 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 auth [api_key] Save an existing API key (e.g. from the web app)
19
+ lotics login Send a magic link email to access the web app
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
22
23
 
23
24
  Signup flags:
24
- --email <email> Email (required; prompted if omitted)
25
- --name <name> Display name (defaults to email prefix)
25
+ --email <email> Email (required; prompted if omitted)
26
+ --name <name> Display name (defaults to email prefix)
27
+ --timezone <timezone> Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
26
28
 
27
- Signup prints email and auto-generated password for web app login.
28
- Use lotics whoami to check the current account email.
29
- Auth priority: --api-key flag > LOTICS_API_KEY env > saved config.
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.
31
+ Setup priority: --api-key flag > LOTICS_API_KEY env > saved config.
30
32
 
31
33
  USAGE
32
- 1. lotics signup Create account and get API key
33
- 2. lotics tools List available tools by category
34
- 3. lotics tools <name> Show tool description and full input schema
35
- 4. lotics run <tool> '<json>' Execute a tool with JSON arguments
34
+ 1. lotics signup (or lotics setup) Create account or set up API key
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
36
39
 
37
- Always inspect the schema (step 3) before calling a tool.
40
+ Always inspect the schema (step 4) before calling a tool.
38
41
  Tools are grouped by category (tables, records, views, etc.).
39
42
  Query tools return IDs used as arguments to other tools.
40
43
 
41
44
  COMMANDS
45
+ lotics workspace List workspaces (marks current)
46
+ lotics workspace select <id> Switch to a different workspace
42
47
  lotics tools List all available tools
43
48
  lotics tools <name> Show tool description and input schema
44
49
  lotics run <tool> '<json>' Execute a tool
@@ -84,6 +89,7 @@ function parseArgs(argv) {
84
89
  apiKey: undefined,
85
90
  email: undefined,
86
91
  name: undefined,
92
+ timezone: undefined,
87
93
  version: false,
88
94
  help: false,
89
95
  };
@@ -117,6 +123,9 @@ function parseArgs(argv) {
117
123
  case "--name":
118
124
  flags.name = argv[++i];
119
125
  break;
126
+ case "--timezone":
127
+ flags.timezone = argv[++i];
128
+ break;
120
129
  case "--version":
121
130
  case "-v":
122
131
  flags.version = true;
@@ -183,10 +192,12 @@ async function handleSignup(flags) {
183
192
  const body = { email };
184
193
  if (name)
185
194
  body.name = name;
195
+ if (flags.timezone)
196
+ body.timezone = flags.timezone;
186
197
  const { ok, status, data } = await publicPost("/v1/cli/signup", body);
187
198
  if (!ok) {
188
199
  if (status === 409) {
189
- console.error("An account with this email already exists. Use: lotics auth");
200
+ console.error("An account with this email already exists. Use: lotics setup");
190
201
  }
191
202
  else if (status === 429) {
192
203
  console.error("Too many signup attempts. Try again later.");
@@ -197,22 +208,28 @@ async function handleSignup(flags) {
197
208
  process.exit(1);
198
209
  }
199
210
  const existing = loadConfig() ?? {};
200
- 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
+ });
201
217
  console.error(`Account created. You can now use the CLI.`);
202
- console.error(` Email: ${data.email}`);
203
- console.error(` Password: ${data.password}`);
204
- console.error(`\nUse these credentials to log into the Lotics web app.`);
205
- 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.`);
206
221
  }
207
- async function handleAuth(providedKey) {
222
+ async function handleSetup(providedKey) {
208
223
  const apiKey = providedKey ?? await prompt("Enter your API key: ");
209
224
  if (!apiKey) {
210
225
  console.error("No API key provided.");
211
226
  process.exit(1);
212
227
  }
213
228
  const client = new LoticsClient({ apiKey });
229
+ let email;
214
230
  try {
215
- await client.listTools();
231
+ const info = await client.whoami();
232
+ email = info.email;
216
233
  }
217
234
  catch (error) {
218
235
  const message = error instanceof Error ? error.message : String(error);
@@ -220,16 +237,69 @@ async function handleAuth(providedKey) {
220
237
  process.exit(1);
221
238
  }
222
239
  const existing = loadConfig() ?? {};
223
- saveConfig({ ...existing, api_key: apiKey });
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);
224
257
  console.error("Authenticated.");
225
258
  }
226
259
  function requireClient(flags) {
227
260
  const auth = resolveAuth(flags);
228
261
  if (!auth) {
229
- console.error('Not authenticated. Run "lotics auth" or set LOTICS_API_KEY.');
262
+ console.error('Not authenticated. Run "lotics setup" or set LOTICS_API_KEY.');
263
+ process.exit(1);
264
+ }
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}`);
230
285
  process.exit(1);
231
286
  }
232
- return new LoticsClient({ apiKey: auth.apiKey });
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);
233
303
  }
234
304
  /**
235
305
  * Resolve upload paths: files pass through, directories expand to their immediate files.
@@ -268,17 +338,33 @@ async function main() {
268
338
  await handleSignup(flags);
269
339
  return;
270
340
  }
271
- if (command === "auth") {
272
- await handleAuth(subcommand ?? flags.apiKey);
341
+ if (command === "setup") {
342
+ await handleSetup(subcommand ?? flags.apiKey);
273
343
  return;
274
344
  }
275
345
  if (command === "whoami") {
276
346
  const config = loadConfig();
277
347
  if (config?.email) {
278
348
  console.log(config.email);
349
+ return;
279
350
  }
280
- else {
281
- console.error("Not signed up. Run: lotics signup");
351
+ // No email cached — try fetching from API
352
+ const auth = resolveAuth(flags);
353
+ if (!auth) {
354
+ console.error('Not authenticated. Run "lotics setup" or set LOTICS_API_KEY.');
355
+ process.exit(1);
356
+ }
357
+ const client = new LoticsClient({ apiKey: auth.apiKey });
358
+ try {
359
+ const info = await client.whoami();
360
+ // Cache for next time
361
+ const existing = loadConfig() ?? {};
362
+ saveConfig({ ...existing, email: info.email });
363
+ console.log(info.email);
364
+ }
365
+ catch (error) {
366
+ const message = error instanceof Error ? error.message : String(error);
367
+ console.error(`Failed to fetch account info: ${message}`);
282
368
  process.exit(1);
283
369
  }
284
370
  return;
@@ -288,8 +374,14 @@ async function main() {
288
374
  console.error("Logged out. Credentials removed.");
289
375
  return;
290
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
+ }
291
383
  // --- Validate command before auth ---
292
- if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download") {
384
+ if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace") {
293
385
  console.error(`Unknown command: ${command}`);
294
386
  console.error('Run "lotics --help" for usage.');
295
387
  process.exit(1);
@@ -310,6 +402,51 @@ async function main() {
310
402
  process.exit(1);
311
403
  }
312
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);
313
450
  // lotics tools / lotics tools <name>
314
451
  if (command === "tools") {
315
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,10 +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;
43
+ whoami(): Promise<{
44
+ email: string;
45
+ name: string;
46
+ organization_id: string;
47
+ }>;
48
+ setWorkspaceId(id: string): void;
49
+ listWorkspaces(): Promise<WorkspaceInfo[]>;
50
+ login(): Promise<{
51
+ email: string;
52
+ }>;
32
53
  listTools(): Promise<{
33
54
  tools: string[];
34
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";
@@ -56,6 +65,18 @@ export class LoticsClient {
56
65
  await this.throwResponseError(response);
57
66
  return response.json();
58
67
  }
68
+ async whoami() {
69
+ return this.request("GET", "/v1/cli/whoami");
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
+ }
59
80
  async listTools() {
60
81
  return this.request("GET", "/v1/tools");
61
82
  }
@@ -71,7 +92,7 @@ export class LoticsClient {
71
92
  const url = `${this.baseUrl}/v1/tools/execute`;
72
93
  const response = await fetch(url, {
73
94
  method: "POST",
74
- headers: { "x-api-key": this.apiKey, "Content-Type": "application/json" },
95
+ headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
75
96
  body: JSON.stringify(body),
76
97
  signal: controller.signal,
77
98
  });
@@ -104,7 +125,7 @@ export class LoticsClient {
104
125
  async downloadFileById(fileId, outputDir) {
105
126
  const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/download`;
106
127
  const response = await fetch(url, {
107
- headers: { "x-api-key": this.apiKey },
128
+ headers: this.buildHeaders(),
108
129
  });
109
130
  if (!response.ok)
110
131
  await this.throwResponseError(response);
@@ -130,7 +151,7 @@ export class LoticsClient {
130
151
  const url = `${this.baseUrl}/v1/files`;
131
152
  const response = await fetch(url, {
132
153
  method: "POST",
133
- headers: { "x-api-key": this.apiKey },
154
+ headers: this.buildHeaders(),
134
155
  body: formData,
135
156
  });
136
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.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {