@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 +3 -2
- package/dist/src/cli.js +35 -6
- package/dist/src/client.d.ts +8 -1
- package/dist/src/client.js +48 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
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;
|
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++) {
|