@lotics/cli 0.2.0 → 0.3.1

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
@@ -27,7 +27,8 @@ The CLI checks for updates once per day and prompts when a new version is availa
27
27
 
28
28
  ```bash
29
29
  # 1. Authenticate
30
- lotics auth
30
+ lotics auth # interactive prompt
31
+ lotics auth ltk_... # non-interactive (for agents/CI)
31
32
 
32
33
  # 2. Discover tools
33
34
  lotics tools # list tools by category with descriptions
@@ -40,8 +41,8 @@ lotics run query_records '{"table_id":"tbl_...","field_keys":["name"]}'
40
41
  # Pipe args via stdin
41
42
  echo '{"table_id":"tbl_..."}' | lotics run query_records
42
43
 
43
- # Upload a file
44
- lotics upload ./report.pdf
44
+ # Upload files (multiple files and directories supported)
45
+ lotics upload ./report.pdf ./data.csv ./documents/
45
46
 
46
47
  # Generate a file, then download it
47
48
  lotics run generate_excel_from_template '{"..."}'
@@ -59,7 +60,7 @@ import { LoticsClient } from "@lotics/cli";
59
60
  const client = new LoticsClient({ apiKey: "ltk_..." });
60
61
 
61
62
  const { result } = await client.execute("query_tables", {});
62
- const upload = await client.uploadFile("./report.pdf");
63
+ const upload = await client.uploadFiles(["./report.pdf", "./data.csv"]);
63
64
  await client.downloadFile(url, "./output.xlsx");
64
65
  const { tools, categories } = await client.listTools();
65
66
  const info = await client.getTool("query_records");
package/dist/src/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
  import readline from "node:readline";
3
5
  import { LoticsClient } from "./client.js";
4
6
  import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
@@ -13,7 +15,7 @@ Lotics is an AI-powered operations platform. Through this CLI you can:
13
15
  - Create and manage apps, knowledge docs, and files
14
16
 
15
17
  USAGE
16
- 1. lotics auth Authenticate (saves key to ~/.lotics/config.json)
18
+ 1. lotics auth [api_key] Authenticate (saves key to ~/.lotics/config.json)
17
19
  2. lotics tools List available tools by category
18
20
  3. lotics tools <name> Show tool description and full input schema
19
21
  4. lotics run <tool> '<json>' Execute a tool with JSON arguments
@@ -23,12 +25,12 @@ USAGE
23
25
  Query tools return IDs used as arguments to other tools.
24
26
 
25
27
  COMMANDS
26
- lotics auth Save API key (interactive)
28
+ lotics auth [api_key] Save API key (interactive prompt if omitted)
27
29
  lotics logout Remove saved credentials
28
30
  lotics tools List all available tools
29
31
  lotics tools <name> Show tool description and input schema
30
32
  lotics run <tool> '<json>' Execute a tool
31
- lotics upload <file> Upload a local file
33
+ lotics upload <file|dir...> Upload files (directories expand to their immediate files)
32
34
  lotics download <file_id> Download a file by ID
33
35
 
34
36
  FLAGS
@@ -56,8 +58,8 @@ FILES
56
58
 
57
59
  Upload files before referencing them in tool args:
58
60
 
59
- lotics upload ./data.csv
60
- lotics run create_records '{"file_id":"fil_..."}'
61
+ lotics upload ./data.csv ./report.pdf ./documents/
62
+ lotics run create_records '{"table_id":"tbl_...","records":[{"fld_file":["fil_..."]}]}'
61
63
 
62
64
  STDIN
63
65
  Pipe JSON arguments via stdin instead of inline:
@@ -77,6 +79,7 @@ function parseArgs(argv) {
77
79
  let command;
78
80
  let subcommand;
79
81
  let toolArgs;
82
+ const restArgs = [];
80
83
  let i = 0;
81
84
  while (i < argv.length) {
82
85
  const arg = argv[i];
@@ -115,11 +118,14 @@ function parseArgs(argv) {
115
118
  else if (!toolArgs) {
116
119
  toolArgs = arg;
117
120
  }
121
+ else {
122
+ restArgs.push(arg);
123
+ }
118
124
  break;
119
125
  }
120
126
  i++;
121
127
  }
122
- return { command, subcommand, toolArgs, flags };
128
+ return { command, subcommand, toolArgs, restArgs, flags };
123
129
  }
124
130
  function readStdin() {
125
131
  return new Promise((resolve, reject) => {
@@ -141,8 +147,8 @@ function prompt(question) {
141
147
  });
142
148
  });
143
149
  }
144
- async function handleAuth() {
145
- const apiKey = await prompt("Enter your API key: ");
150
+ async function handleAuth(providedKey) {
151
+ const apiKey = providedKey ?? await prompt("Enter your API key: ");
146
152
  if (!apiKey) {
147
153
  console.error("No API key provided.");
148
154
  process.exit(1);
@@ -168,8 +174,30 @@ function requireClient(flags) {
168
174
  }
169
175
  return new LoticsClient({ apiKey: auth.apiKey });
170
176
  }
177
+ /**
178
+ * Resolve upload paths: files pass through, directories expand to their immediate files.
179
+ */
180
+ function resolveUploadPaths(rawPaths) {
181
+ const result = [];
182
+ for (const p of rawPaths) {
183
+ const resolved = path.resolve(p);
184
+ const stat = fs.statSync(resolved);
185
+ if (stat.isDirectory()) {
186
+ const entries = fs.readdirSync(resolved, { withFileTypes: true });
187
+ for (const entry of entries) {
188
+ if (entry.isFile()) {
189
+ result.push(path.join(resolved, entry.name));
190
+ }
191
+ }
192
+ }
193
+ else {
194
+ result.push(resolved);
195
+ }
196
+ }
197
+ return result;
198
+ }
171
199
  async function main() {
172
- const { command, subcommand, toolArgs, flags } = parseArgs(process.argv.slice(2));
200
+ const { command, subcommand, toolArgs, restArgs, flags } = parseArgs(process.argv.slice(2));
173
201
  if (flags.help || (!command && !flags.version)) {
174
202
  printHelp();
175
203
  return;
@@ -180,7 +208,7 @@ async function main() {
180
208
  }
181
209
  // --- Commands that don't require auth ---
182
210
  if (command === "auth") {
183
- await handleAuth();
211
+ await handleAuth(subcommand ?? flags.apiKey);
184
212
  return;
185
213
  }
186
214
  if (command === "logout") {
@@ -200,8 +228,8 @@ async function main() {
200
228
  process.exit(1);
201
229
  }
202
230
  if (command === "upload" && !subcommand) {
203
- console.error('Usage: lotics upload <file> [--as <name>]');
204
- console.error('Uploads a local file and returns its file_id.');
231
+ console.error('Usage: lotics upload <file|dir...> [--as <name>]');
232
+ console.error('Uploads files and returns their file_ids. Directories expand to their immediate files.');
205
233
  process.exit(1);
206
234
  }
207
235
  if (command === "download" && !subcommand) {
@@ -214,28 +242,58 @@ async function main() {
214
242
  if (command === "tools") {
215
243
  if (subcommand) {
216
244
  const info = await client.getTool(subcommand);
217
- console.log(JSON.stringify(info, null, 2));
245
+ if (flags.json) {
246
+ console.log(JSON.stringify(info, null, 2));
247
+ }
248
+ else {
249
+ console.log(`${info.name}\n\n${info.description}\n\nInput schema:\n${JSON.stringify(info.input_schema, null, 2)}`);
250
+ }
218
251
  }
219
252
  else {
220
253
  const { categories } = await client.listTools();
221
- for (const [category, { description, tools }] of Object.entries(categories)) {
222
- console.log(description ? `\n## ${category} — ${description}` : `\n## ${category}`);
223
- console.log(` ${tools.join(", ")}`);
254
+ if (flags.json) {
255
+ console.log(JSON.stringify(categories, null, 2));
256
+ }
257
+ else {
258
+ for (const [category, { description, tools }] of Object.entries(categories)) {
259
+ console.log(description ? `\n## ${category} — ${description}` : `\n## ${category}`);
260
+ console.log(` ${tools.join(", ")}`);
261
+ }
262
+ console.log("");
224
263
  }
225
- console.log("");
226
264
  }
227
265
  return;
228
266
  }
229
- // lotics upload <file> [--as <name>]
267
+ // lotics upload <file|dir...> [--as <name>]
230
268
  if (command === "upload") {
231
- const upload = await client.uploadFile(subcommand, {
232
- filename: flags.as,
269
+ const rawPaths = [subcommand, ...(toolArgs ? [toolArgs] : []), ...restArgs];
270
+ const filePaths = resolveUploadPaths(rawPaths);
271
+ if (filePaths.length === 0) {
272
+ console.error("No files found in the specified paths.");
273
+ process.exit(1);
274
+ }
275
+ if (flags.as && filePaths.length > 1) {
276
+ console.error("Cannot use --as with multiple files.");
277
+ process.exit(1);
278
+ }
279
+ const upload = await client.uploadFiles(filePaths, {
280
+ filenames: flags.as ? [flags.as] : undefined,
233
281
  });
234
- if (upload.errors.length > 0) {
235
- console.error(`Upload failed: ${upload.errors[0].error}`);
282
+ if (upload.files.length === 0 && upload.errors.length > 0) {
283
+ console.error(`Upload failed: ${upload.errors.map((e) => `${e.filename}: ${e.error}`).join(", ")}`);
236
284
  process.exit(1);
237
285
  }
238
- console.log(JSON.stringify(upload.files[0], null, 2));
286
+ if (flags.json) {
287
+ console.log(JSON.stringify({ files: upload.files, errors: upload.errors }, null, 2));
288
+ }
289
+ else {
290
+ for (const file of upload.files) {
291
+ console.log(`${file.id} ${file.filename} ${file.mime_type}`);
292
+ }
293
+ for (const error of upload.errors) {
294
+ console.error(`FAILED ${error.filename} ${error.error}`);
295
+ }
296
+ }
239
297
  return;
240
298
  }
241
299
  // lotics download <file_id> [-o <path>]
@@ -261,18 +319,22 @@ async function main() {
261
319
  process.exit(1);
262
320
  }
263
321
  }
264
- const format = flags.json ? "json" : "text";
265
322
  const timeoutMs = flags.timeout ?? 60000;
266
- const result = await client.execute(toolName, args, { format, timeoutMs });
323
+ // Always request text format so model_output is available; --json only affects CLI output
324
+ const result = await client.execute(toolName, args, { format: "text", timeoutMs });
267
325
  if (result.error) {
268
326
  console.error(result.error);
269
327
  console.error(`\nHint: run "lotics tools ${toolName}" to see the expected input schema.`);
270
328
  process.exit(1);
271
329
  }
272
- if (format === "text" && result.model_output) {
330
+ if (flags.json) {
331
+ console.log(JSON.stringify(result.result, null, 2));
332
+ }
333
+ else if (result.model_output) {
273
334
  console.log(result.model_output);
274
335
  }
275
336
  else {
337
+ // Fallback: compact JSON when no text output is available
276
338
  console.log(JSON.stringify(result.result, null, 2));
277
339
  }
278
340
  return;
@@ -45,7 +45,7 @@ export declare class LoticsClient {
45
45
  path: string;
46
46
  filename: string;
47
47
  }>;
48
- uploadFile(filePath: string, options?: {
49
- filename?: string;
48
+ uploadFiles(filePaths: string[], options?: {
49
+ filenames?: string[];
50
50
  }): Promise<FileUploadResult>;
51
51
  }
@@ -116,14 +116,16 @@ export class LoticsClient {
116
116
  await fs.promises.writeFile(absolutePath, buffer);
117
117
  return { path: absolutePath, filename };
118
118
  }
119
- async uploadFile(filePath, options) {
120
- const absolutePath = path.resolve(filePath);
121
- const buffer = await fs.promises.readFile(absolutePath);
122
- const filename = options?.filename ?? path.basename(absolutePath);
123
- const mimeType = getMimeType(filename);
124
- const blob = new Blob([buffer], { type: mimeType });
119
+ async uploadFiles(filePaths, options) {
125
120
  const formData = new FormData();
126
- formData.append("file", blob, filename);
121
+ for (let i = 0; i < filePaths.length; i++) {
122
+ const absolutePath = path.resolve(filePaths[i]);
123
+ const buffer = await fs.promises.readFile(absolutePath);
124
+ const filename = options?.filenames?.[i] ?? path.basename(absolutePath);
125
+ const mimeType = getMimeType(filename);
126
+ const blob = new Blob([buffer], { type: mimeType });
127
+ formData.append("file", blob, filename);
128
+ }
127
129
  const url = `${this.baseUrl}/v1/files`;
128
130
  const response = await fetch(url, {
129
131
  method: "POST",
@@ -70,7 +70,18 @@ async function fetchLatestVersion() {
70
70
  clearTimeout(timeout);
71
71
  }
72
72
  }
73
+ function isNewerVersion(latest, current) {
74
+ const [lMaj, lMin, lPat] = latest.split(".").map(Number);
75
+ const [cMaj, cMin, cPat] = current.split(".").map(Number);
76
+ if (lMaj !== cMaj)
77
+ return lMaj > cMaj;
78
+ if (lMin !== cMin)
79
+ return lMin > cMin;
80
+ return lPat > cPat;
81
+ }
73
82
  function printUpdateWarning(current, latest) {
83
+ if (!isNewerVersion(latest, current))
84
+ return;
74
85
  console.error(`\nUpdate available: ${current} → ${latest}`);
75
86
  console.error(`Run: npm i -g @lotics/cli\n`);
76
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {