@lotics/cli 0.10.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/dist/src/cli.js +30 -4
- package/dist/src/client.d.ts +8 -1
- package/dist/src/client.js +48 -2
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -38,6 +38,8 @@ COMMANDS
|
|
|
38
38
|
lotics run <tool> '<json>' Execute a tool
|
|
39
39
|
lotics upload <file|dir...> Upload files (directories expand to their immediate files)
|
|
40
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
|
|
41
43
|
|
|
42
44
|
FLAGS
|
|
43
45
|
--json Full JSON output (default is human-readable text)
|
|
@@ -48,9 +50,10 @@ FLAGS
|
|
|
48
50
|
--version Show version
|
|
49
51
|
|
|
50
52
|
OUTPUT
|
|
51
|
-
Default output is a
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
54
57
|
|
|
55
58
|
FILES
|
|
56
59
|
Some tools generate files and return { file_id, url, filename }.
|
|
@@ -406,10 +409,17 @@ async function main() {
|
|
|
406
409
|
process.exit(1);
|
|
407
410
|
}
|
|
408
411
|
if (command === "download" && !subcommand) {
|
|
409
|
-
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>]');
|
|
410
415
|
console.error('File IDs come from upload results or generate_* tool output (--json).');
|
|
411
416
|
process.exit(1);
|
|
412
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
|
+
}
|
|
413
423
|
const client = requireClient(flags);
|
|
414
424
|
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
415
425
|
if (command === "workspace") {
|
|
@@ -536,7 +546,23 @@ async function main() {
|
|
|
536
546
|
return;
|
|
537
547
|
}
|
|
538
548
|
// lotics download <file_id> [-o <path>]
|
|
549
|
+
// lotics download record <record_id> <field_key> [-o <path>]
|
|
539
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
|
+
}
|
|
540
566
|
const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
|
|
541
567
|
console.error(`Downloaded: ${filePath} (${filename})`);
|
|
542
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++) {
|