@lotics/cli 0.10.0 → 0.13.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
@@ -5,8 +5,9 @@ CLI and SDK for AI agents to interact with Lotics.
5
5
  Lotics is an AI-powered operations platform. Through this CLI you can:
6
6
 
7
7
  - Manage tables, records, and views (structured data with typed fields)
8
- - Generate documents from templates (Excel, Word, PDF)
9
- - Build and run automations (event-driven workflows)
8
+ - Generate documents from templates (Excel, Word, PDF, Email)
9
+ - Build and run automations — schedules, webhooks, table lifecycle workflows, button actions, app-action workflows; inspect execution and version history
10
+ - Administer the workspace — invite members, manage groups, share resources, transfer ownership, browse connected accounts (OAuth attach is web-only)
10
11
  - Create and manage apps, knowledge docs, and files
11
12
 
12
13
  ## Install
package/dist/src/cli.js CHANGED
@@ -10,8 +10,11 @@ function printHelp() {
10
10
 
11
11
  Lotics is an AI-powered operations platform. Through this CLI you can:
12
12
  - Manage tables, records, and views (structured data with typed fields)
13
- - Generate documents from templates (Excel, Word, PDF)
14
- - Build and run automations (event-driven workflows)
13
+ - Generate documents from templates (Excel, Word, PDF, Email)
14
+ - Build and run automations schedules, webhooks, table lifecycle workflows,
15
+ button actions, and app-action workflows; inspect execution and version history
16
+ - Administer the workspace — invite members, manage groups, share resources,
17
+ transfer ownership, browse connected accounts (OAuth attach is web-only)
15
18
  - Create and manage apps, knowledge docs, and files
16
19
 
17
20
  AUTHENTICATION
@@ -38,6 +41,8 @@ COMMANDS
38
41
  lotics run <tool> '<json>' Execute a tool
39
42
  lotics upload <file|dir...> Upload files (directories expand to their immediate files)
40
43
  lotics download <file_id> Download a file by ID
44
+ lotics download record <record_id> <field_key>
45
+ Download all files on a record file field
41
46
 
42
47
  FLAGS
43
48
  --json Full JSON output (default is human-readable text)
@@ -48,9 +53,10 @@ FLAGS
48
53
  --version Show version
49
54
 
50
55
  OUTPUT
51
- Default output is a human-readable text summary. Use --json to get
52
- structured JSON for programmatic use. Errors print to stderr and
53
- exit with code 1.
56
+ Default output is a compact text summary optimized for AI agents —
57
+ use it directly, no parsing needed. --json returns raw structured
58
+ JSON for machine-to-machine pipelines (scripts, CI). Errors print
59
+ to stderr and exit with code 1.
54
60
 
55
61
  FILES
56
62
  Some tools generate files and return { file_id, url, filename }.
@@ -406,10 +412,17 @@ async function main() {
406
412
  process.exit(1);
407
413
  }
408
414
  if (command === "download" && !subcommand) {
409
- console.error('Usage: lotics download <file_id> [-o <dir>]');
415
+ console.error('Usage:');
416
+ console.error(' lotics download <file_id> [-o <dir>]');
417
+ console.error(' lotics download record <record_id> <field_key> [-o <dir>]');
410
418
  console.error('File IDs come from upload results or generate_* tool output (--json).');
411
419
  process.exit(1);
412
420
  }
421
+ if (command === "download" && subcommand === "record" && (!toolArgs || restArgs.length === 0)) {
422
+ console.error('Usage: lotics download record <record_id> <field_key> [-o <dir>]');
423
+ console.error('Downloads every file on the given file field into the output dir.');
424
+ process.exit(1);
425
+ }
413
426
  const client = requireClient(flags);
414
427
  // lotics workspace / lotics workspace list / lotics workspace select <id>
415
428
  if (command === "workspace") {
@@ -536,7 +549,23 @@ async function main() {
536
549
  return;
537
550
  }
538
551
  // lotics download <file_id> [-o <path>]
552
+ // lotics download record <record_id> <field_key> [-o <path>]
539
553
  if (command === "download") {
554
+ if (subcommand === "record") {
555
+ const recordId = toolArgs;
556
+ const fieldKey = restArgs[0];
557
+ const files = await client.downloadRecordFiles(recordId, fieldKey, flags.output);
558
+ if (flags.json) {
559
+ console.log(JSON.stringify(files, null, 2));
560
+ }
561
+ else {
562
+ for (const f of files) {
563
+ console.log(`${f.file_id} ${f.path} ${f.filename}`);
564
+ }
565
+ console.error(`Downloaded ${files.length} file${files.length === 1 ? "" : "s"} from ${recordId}.${fieldKey}`);
566
+ }
567
+ return;
568
+ }
540
569
  const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
541
570
  console.error(`Downloaded: ${filePath} (${filename})`);
542
571
  return;
@@ -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): Promise<{
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>;
@@ -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 filename = match?.[1] ?? fileId;
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++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.10.0",
3
+ "version": "0.13.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {