@lotics/cli 0.9.0 → 0.12.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 +9 -9
- package/dist/src/cli.js +104 -70
- package/dist/src/client.d.ts +8 -1
- package/dist/src/client.js +48 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,27 +25,27 @@ 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. Sends a magic link email so you can access the web app.
|
|
28
|
+
**`lotics auth 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
|
-
lotics signup
|
|
32
|
-
lotics signup
|
|
31
|
+
lotics auth signup # interactive prompts
|
|
32
|
+
lotics auth signup a@b.com --name "Agent" # non-interactive
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
**`lotics
|
|
35
|
+
**`lotics auth web`** — Send a magic link email to access the web app (requires prior signup or setup).
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
lotics
|
|
38
|
+
lotics auth web
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
**`lotics
|
|
41
|
+
**`lotics auth api-key`** — Saves an existing API key directly (e.g. one created in the Lotics web app).
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
lotics
|
|
45
|
-
lotics
|
|
44
|
+
lotics auth api-key # interactive prompt
|
|
45
|
+
lotics auth api-key ltk_... # non-interactive
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
API key is saved to `~/.lotics/config.json`. Run `lotics logout` to remove saved credentials.
|
|
48
|
+
API key is saved to `~/.lotics/config.json`. Run `lotics auth logout` to remove saved credentials.
|
|
49
49
|
|
|
50
50
|
Auth priority: `--api-key` flag > `LOTICS_API_KEY` env > `~/.lotics/config.json`.
|
|
51
51
|
|
package/dist/src/cli.js
CHANGED
|
@@ -15,23 +15,11 @@ Lotics is an AI-powered operations platform. Through this CLI you can:
|
|
|
15
15
|
- Create and manage apps, knowledge docs, and files
|
|
16
16
|
|
|
17
17
|
AUTHENTICATION
|
|
18
|
-
lotics
|
|
19
|
-
lotics
|
|
20
|
-
lotics setup [api_key] Save an existing API key (e.g. from the web app)
|
|
21
|
-
lotics whoami Show the email of the current account
|
|
22
|
-
lotics logout Remove saved credentials
|
|
23
|
-
|
|
24
|
-
Signup flags:
|
|
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)
|
|
28
|
-
|
|
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.
|
|
18
|
+
lotics auth Show auth help
|
|
19
|
+
lotics auth signup <email> Create account (run "lotics auth" for details)
|
|
32
20
|
|
|
33
21
|
USAGE
|
|
34
|
-
1. lotics signup
|
|
22
|
+
1. lotics auth signup <email> Create account or authenticate
|
|
35
23
|
2. lotics workspace Check current workspace (auto-selects if only one)
|
|
36
24
|
3. lotics tools List available tools by category
|
|
37
25
|
4. lotics tools <name> Show tool description and full input schema
|
|
@@ -50,6 +38,8 @@ COMMANDS
|
|
|
50
38
|
lotics run <tool> '<json>' Execute a tool
|
|
51
39
|
lotics upload <file|dir...> Upload files (directories expand to their immediate files)
|
|
52
40
|
lotics download <file_id> Download a file by ID
|
|
41
|
+
lotics download record <record_id> <field_key>
|
|
42
|
+
Download all files on a record file field
|
|
53
43
|
|
|
54
44
|
FLAGS
|
|
55
45
|
--json Full JSON output (default is human-readable text)
|
|
@@ -60,9 +50,10 @@ FLAGS
|
|
|
60
50
|
--version Show version
|
|
61
51
|
|
|
62
52
|
OUTPUT
|
|
63
|
-
Default output is a
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
Default output is a compact text summary optimized for AI agents —
|
|
54
|
+
use it directly, no parsing needed. --json returns raw structured
|
|
55
|
+
JSON for machine-to-machine pipelines (scripts, CI). Errors print
|
|
56
|
+
to stderr and exit with code 1.
|
|
66
57
|
|
|
67
58
|
FILES
|
|
68
59
|
Some tools generate files and return { file_id, url, filename }.
|
|
@@ -81,6 +72,23 @@ STDIN
|
|
|
81
72
|
|
|
82
73
|
echo '{"table_id":"tbl_..."}' | lotics run query_records`);
|
|
83
74
|
}
|
|
75
|
+
function printAuthHelp() {
|
|
76
|
+
console.log(`Authentication commands:
|
|
77
|
+
|
|
78
|
+
lotics auth signup <email> Create account, org, workspace, and API key
|
|
79
|
+
lotics auth api-key [key] Save an existing API key (e.g. from the web app)
|
|
80
|
+
lotics auth web Send a magic link email to access the web app
|
|
81
|
+
lotics auth whoami Show the email of the current account
|
|
82
|
+
lotics auth logout Remove saved credentials
|
|
83
|
+
|
|
84
|
+
Signup flags:
|
|
85
|
+
--name <name> Display name (defaults to email prefix)
|
|
86
|
+
--timezone <timezone> Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
|
|
87
|
+
|
|
88
|
+
Signup sends a magic link email so you can access the Lotics web app.
|
|
89
|
+
Use lotics auth web to request a new magic link at any time.
|
|
90
|
+
Auth priority: --api-key flag > LOTICS_API_KEY env > saved config.`);
|
|
91
|
+
}
|
|
84
92
|
function parseArgs(argv) {
|
|
85
93
|
const flags = {
|
|
86
94
|
json: false,
|
|
@@ -88,7 +96,6 @@ function parseArgs(argv) {
|
|
|
88
96
|
output: undefined,
|
|
89
97
|
as: undefined,
|
|
90
98
|
apiKey: undefined,
|
|
91
|
-
email: undefined,
|
|
92
99
|
name: undefined,
|
|
93
100
|
timezone: undefined,
|
|
94
101
|
version: false,
|
|
@@ -118,9 +125,6 @@ function parseArgs(argv) {
|
|
|
118
125
|
case "--api-key":
|
|
119
126
|
flags.apiKey = argv[++i];
|
|
120
127
|
break;
|
|
121
|
-
case "--email":
|
|
122
|
-
flags.email = argv[++i];
|
|
123
|
-
break;
|
|
124
128
|
case "--name":
|
|
125
129
|
flags.name = argv[++i];
|
|
126
130
|
break;
|
|
@@ -183,10 +187,10 @@ async function publicPost(path, body) {
|
|
|
183
187
|
const data = await response.json();
|
|
184
188
|
return { ok: response.ok, status: response.status, data };
|
|
185
189
|
}
|
|
186
|
-
async function handleSignup(flags) {
|
|
187
|
-
const email =
|
|
190
|
+
async function handleSignup(positionalEmail, flags) {
|
|
191
|
+
const email = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
|
|
188
192
|
if (!email) {
|
|
189
|
-
console.error("Email is required.
|
|
193
|
+
console.error("Email is required. Usage: lotics auth signup <email>");
|
|
190
194
|
process.exit(1);
|
|
191
195
|
}
|
|
192
196
|
const name = flags.name ?? (process.stdin.isTTY ? await prompt("Name (enter to use email): ") : undefined);
|
|
@@ -198,7 +202,7 @@ async function handleSignup(flags) {
|
|
|
198
202
|
const { ok, status, data } = await publicPost("/v1/cli/signup", body);
|
|
199
203
|
if (!ok) {
|
|
200
204
|
if (status === 409) {
|
|
201
|
-
console.error("An account with this email already exists. Use: lotics
|
|
205
|
+
console.error("An account with this email already exists. Use: lotics auth api-key");
|
|
202
206
|
}
|
|
203
207
|
else if (status === 429) {
|
|
204
208
|
console.error("Too many signup attempts. Try again later.");
|
|
@@ -218,7 +222,7 @@ async function handleSignup(flags) {
|
|
|
218
222
|
console.error(`Account created. You can now use the CLI.`);
|
|
219
223
|
console.error(` Email: ${data.email}`);
|
|
220
224
|
console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
|
|
221
|
-
console.error(`Run "lotics
|
|
225
|
+
console.error(`Run "lotics auth web" to request a new link at any time.`);
|
|
222
226
|
}
|
|
223
227
|
async function handleSetup(providedKey) {
|
|
224
228
|
const apiKey = providedKey ?? await prompt("Enter your API key: ");
|
|
@@ -260,7 +264,7 @@ async function handleSetup(providedKey) {
|
|
|
260
264
|
function requireClient(flags) {
|
|
261
265
|
const auth = resolveAuth(flags);
|
|
262
266
|
if (!auth) {
|
|
263
|
-
console.error('Not authenticated. Run "lotics
|
|
267
|
+
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
264
268
|
process.exit(1);
|
|
265
269
|
}
|
|
266
270
|
const config = loadConfig();
|
|
@@ -334,51 +338,58 @@ async function main() {
|
|
|
334
338
|
console.log(VERSION);
|
|
335
339
|
return;
|
|
336
340
|
}
|
|
337
|
-
// ---
|
|
338
|
-
if (command === "
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
if (command === "setup") {
|
|
343
|
-
await handleSetup(subcommand ?? flags.apiKey);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (command === "whoami") {
|
|
347
|
-
const config = loadConfig();
|
|
348
|
-
if (config?.email) {
|
|
349
|
-
console.log(config.email);
|
|
341
|
+
// --- lotics auth <subcommand> ---
|
|
342
|
+
if (command === "auth") {
|
|
343
|
+
if (subcommand === "signup") {
|
|
344
|
+
await handleSignup(toolArgs, flags);
|
|
350
345
|
return;
|
|
351
346
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
console.error('Not authenticated. Run "lotics setup" or set LOTICS_API_KEY.');
|
|
356
|
-
process.exit(1);
|
|
347
|
+
if (subcommand === "api-key") {
|
|
348
|
+
await handleSetup(toolArgs ?? flags.apiKey);
|
|
349
|
+
return;
|
|
357
350
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
351
|
+
if (subcommand === "whoami") {
|
|
352
|
+
const config = loadConfig();
|
|
353
|
+
if (config?.email) {
|
|
354
|
+
console.log(config.email);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// No email cached — try fetching from API
|
|
358
|
+
const auth = resolveAuth(flags);
|
|
359
|
+
if (!auth) {
|
|
360
|
+
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
const client = new LoticsClient({ apiKey: auth.apiKey });
|
|
364
|
+
try {
|
|
365
|
+
const info = await client.whoami();
|
|
366
|
+
// Cache for next time
|
|
367
|
+
const existing = loadConfig() ?? {};
|
|
368
|
+
saveConfig({ ...existing, email: info.email });
|
|
369
|
+
console.log(info.email);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
+
console.error(`Failed to fetch account info: ${message}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
370
377
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
if (subcommand === "logout") {
|
|
379
|
+
deleteConfig();
|
|
380
|
+
console.error("Logged out. Credentials removed.");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (subcommand === "web") {
|
|
384
|
+
const client = requireClient(flags);
|
|
385
|
+
const { email } = await client.login();
|
|
386
|
+
console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (subcommand) {
|
|
390
|
+
console.error(`Unknown auth subcommand: ${subcommand}\n`);
|
|
391
|
+
}
|
|
392
|
+
printAuthHelp();
|
|
382
393
|
return;
|
|
383
394
|
}
|
|
384
395
|
// --- Validate command before auth ---
|
|
@@ -398,10 +409,17 @@ async function main() {
|
|
|
398
409
|
process.exit(1);
|
|
399
410
|
}
|
|
400
411
|
if (command === "download" && !subcommand) {
|
|
401
|
-
console.error('Usage:
|
|
412
|
+
console.error('Usage:');
|
|
413
|
+
console.error(' lotics download <file_id> [-o <dir>]');
|
|
414
|
+
console.error(' lotics download record <record_id> <field_key> [-o <dir>]');
|
|
402
415
|
console.error('File IDs come from upload results or generate_* tool output (--json).');
|
|
403
416
|
process.exit(1);
|
|
404
417
|
}
|
|
418
|
+
if (command === "download" && subcommand === "record" && (!toolArgs || restArgs.length === 0)) {
|
|
419
|
+
console.error('Usage: lotics download record <record_id> <field_key> [-o <dir>]');
|
|
420
|
+
console.error('Downloads every file on the given file field into the output dir.');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
405
423
|
const client = requireClient(flags);
|
|
406
424
|
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
407
425
|
if (command === "workspace") {
|
|
@@ -528,7 +546,23 @@ async function main() {
|
|
|
528
546
|
return;
|
|
529
547
|
}
|
|
530
548
|
// lotics download <file_id> [-o <path>]
|
|
549
|
+
// lotics download record <record_id> <field_key> [-o <path>]
|
|
531
550
|
if (command === "download") {
|
|
551
|
+
if (subcommand === "record") {
|
|
552
|
+
const recordId = toolArgs;
|
|
553
|
+
const fieldKey = restArgs[0];
|
|
554
|
+
const files = await client.downloadRecordFiles(recordId, fieldKey, flags.output);
|
|
555
|
+
if (flags.json) {
|
|
556
|
+
console.log(JSON.stringify(files, null, 2));
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
for (const f of files) {
|
|
560
|
+
console.log(`${f.file_id} ${f.path} ${f.filename}`);
|
|
561
|
+
}
|
|
562
|
+
console.error(`Downloaded ${files.length} file${files.length === 1 ? "" : "s"} from ${recordId}.${fieldKey}`);
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
532
566
|
const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
|
|
533
567
|
console.error(`Downloaded: ${filePath} (${filename})`);
|
|
534
568
|
return;
|
package/dist/src/client.d.ts
CHANGED
|
@@ -67,10 +67,17 @@ export declare class LoticsClient {
|
|
|
67
67
|
timeoutMs?: number;
|
|
68
68
|
}): Promise<ToolExecuteResult>;
|
|
69
69
|
downloadFile(url: string, outputPath: string): Promise<string>;
|
|
70
|
-
downloadFileById(fileId: string, outputDir?: string
|
|
70
|
+
downloadFileById(fileId: string, outputDir?: string, options?: {
|
|
71
|
+
reserved?: Set<string>;
|
|
72
|
+
}): Promise<{
|
|
71
73
|
path: string;
|
|
72
74
|
filename: string;
|
|
73
75
|
}>;
|
|
76
|
+
downloadRecordFiles(recordId: string, fieldKey: string, outputDir?: string): Promise<Array<{
|
|
77
|
+
path: string;
|
|
78
|
+
filename: string;
|
|
79
|
+
file_id: string;
|
|
80
|
+
}>>;
|
|
74
81
|
uploadFiles(filePaths: string[], options?: {
|
|
75
82
|
filenames?: string[];
|
|
76
83
|
}): Promise<FileUploadResult>;
|
package/dist/src/client.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
function findAvailableFilename(dir, filename, reserved) {
|
|
4
|
+
// `reserved` tracks absolute paths claimed by in-flight downloads in the same
|
|
5
|
+
// batch — required for parallel callers because the file may not be on disk
|
|
6
|
+
// yet when a peer call races to pick a name.
|
|
7
|
+
const isTaken = (name) => {
|
|
8
|
+
const full = path.join(dir, name);
|
|
9
|
+
if (reserved?.has(full))
|
|
10
|
+
return true;
|
|
11
|
+
return fs.existsSync(full);
|
|
12
|
+
};
|
|
13
|
+
const claim = (name) => {
|
|
14
|
+
reserved?.add(path.join(dir, name));
|
|
15
|
+
return name;
|
|
16
|
+
};
|
|
17
|
+
if (!isTaken(filename))
|
|
18
|
+
return claim(filename);
|
|
19
|
+
const lastDot = filename.lastIndexOf(".");
|
|
20
|
+
const base = lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
|
21
|
+
const ext = lastDot > 0 ? filename.slice(lastDot) : "";
|
|
22
|
+
let n = 2;
|
|
23
|
+
while (isTaken(`${base}_${n}${ext}`))
|
|
24
|
+
n++;
|
|
25
|
+
return claim(`${base}_${n}${ext}`);
|
|
26
|
+
}
|
|
3
27
|
const MIME_MAP = {
|
|
4
28
|
".jpg": "image/jpeg",
|
|
5
29
|
".jpeg": "image/jpeg",
|
|
@@ -125,7 +149,7 @@ export class LoticsClient {
|
|
|
125
149
|
await fs.promises.writeFile(absolutePath, buffer);
|
|
126
150
|
return absolutePath;
|
|
127
151
|
}
|
|
128
|
-
async downloadFileById(fileId, outputDir) {
|
|
152
|
+
async downloadFileById(fileId, outputDir, options) {
|
|
129
153
|
const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/download`;
|
|
130
154
|
const response = await fetch(url, {
|
|
131
155
|
headers: this.buildHeaders(),
|
|
@@ -134,13 +158,35 @@ export class LoticsClient {
|
|
|
134
158
|
await this.throwResponseError(response);
|
|
135
159
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
136
160
|
const match = disposition.match(/filename="?([^";\n]+)"?/);
|
|
137
|
-
const
|
|
161
|
+
const originalFilename = match?.[1] ?? fileId;
|
|
138
162
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
139
163
|
const dir = outputDir ? path.resolve(outputDir) : process.cwd();
|
|
164
|
+
const filename = findAvailableFilename(dir, originalFilename, options?.reserved);
|
|
140
165
|
const absolutePath = path.join(dir, filename);
|
|
141
166
|
await fs.promises.writeFile(absolutePath, buffer);
|
|
142
167
|
return { path: absolutePath, filename };
|
|
143
168
|
}
|
|
169
|
+
async downloadRecordFiles(recordId, fieldKey, outputDir) {
|
|
170
|
+
const result = await this.execute("get_record", { record_id: recordId }, { format: "text" });
|
|
171
|
+
if (result.error)
|
|
172
|
+
throw new Error(result.error);
|
|
173
|
+
const record = result.result;
|
|
174
|
+
const files = record?.data?.[fieldKey];
|
|
175
|
+
if (!Array.isArray(files)) {
|
|
176
|
+
throw new Error(`Field ${fieldKey} on ${recordId} is not a file field or has no value`);
|
|
177
|
+
}
|
|
178
|
+
const fileIds = files.map((f, i) => {
|
|
179
|
+
if (typeof f !== "object" || f === null || !("id" in f) || typeof f.id !== "string") {
|
|
180
|
+
throw new Error(`Invalid file entry [${i}] in ${recordId}.${fieldKey}: ${JSON.stringify(f)}`);
|
|
181
|
+
}
|
|
182
|
+
return f.id;
|
|
183
|
+
});
|
|
184
|
+
const reserved = new Set();
|
|
185
|
+
return Promise.all(fileIds.map(async (fileId) => {
|
|
186
|
+
const res = await this.downloadFileById(fileId, outputDir, { reserved });
|
|
187
|
+
return { ...res, file_id: fileId };
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
144
190
|
async uploadFiles(filePaths, options) {
|
|
145
191
|
const formData = new FormData();
|
|
146
192
|
for (let i = 0; i < filePaths.length; i++) {
|