@lotics/cli 0.1.0 → 0.3.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
@@ -2,6 +2,13 @@
2
2
 
3
3
  CLI and SDK for AI agents to interact with Lotics.
4
4
 
5
+ Lotics is an AI-powered operations platform. Through this CLI you can:
6
+
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)
10
+ - Create and manage apps, knowledge docs, and files
11
+
5
12
  ## Install
6
13
 
7
14
  ```bash
@@ -23,25 +30,22 @@ The CLI checks for updates once per day and prompts when a new version is availa
23
30
  lotics auth
24
31
 
25
32
  # 2. Discover tools
26
- lotics tools # list all tool names
27
- lotics tools query_records # show description + input schema
33
+ lotics tools # list tools by category with descriptions
34
+ lotics tools query_records # show full description + input schema
28
35
 
29
36
  # 3. Execute
30
37
  lotics run query_tables '{}'
31
38
  lotics run query_records '{"table_id":"tbl_...","field_keys":["name"]}'
32
- lotics run query_records --json '{"table_id":"tbl_..."}'
33
39
 
34
40
  # Pipe args via stdin
35
41
  echo '{"table_id":"tbl_..."}' | lotics run query_records
36
42
 
37
- # Upload a file
38
- lotics upload ./report.pdf
39
- lotics upload ./photo.jpg --as cover.jpg
43
+ # Upload files (multiple files and directories supported)
44
+ lotics upload ./report.pdf ./data.csv ./documents/
40
45
 
41
46
  # Generate a file, then download it
42
- lotics run generate_excel_from_template '{"..."}' --json
43
- # {"file_id":"fil_...","url":"...","filename":"report.xlsx",...}
44
- lotics download fil_... -o ./reports/
47
+ lotics run generate_excel_from_template '{"..."}'
48
+ lotics download <file_id> -o ./reports/
45
49
 
46
50
  # CI / non-interactive
47
51
  LOTICS_API_KEY=ltk_... lotics run query_tables '{}'
@@ -52,14 +56,11 @@ LOTICS_API_KEY=ltk_... lotics run query_tables '{}'
52
56
  ```typescript
53
57
  import { LoticsClient } from "@lotics/cli";
54
58
 
55
- const client = new LoticsClient({
56
- apiKey: "ltk_...",
57
- baseUrl: "https://api.lotics.com",
58
- });
59
+ const client = new LoticsClient({ apiKey: "ltk_..." });
59
60
 
60
61
  const { result } = await client.execute("query_tables", {});
61
- const upload = await client.uploadFile("./report.pdf");
62
+ const upload = await client.uploadFiles(["./report.pdf", "./data.csv"]);
62
63
  await client.downloadFile(url, "./output.xlsx");
63
- const { tools } = await client.listTools();
64
+ const { tools, categories } = await client.listTools();
64
65
  const info = await client.getTool("query_records");
65
66
  ```
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";
@@ -6,37 +8,63 @@ import { VERSION } from "./version.js";
6
8
  function printHelp() {
7
9
  console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
8
10
 
9
- Lotics is an AI-powered operations platform for structured data,
10
- document generation, automations, and knowledge management.
11
+ Lotics is an AI-powered operations platform. Through this CLI you can:
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)
15
+ - Create and manage apps, knowledge docs, and files
16
+
17
+ USAGE
18
+ 1. lotics auth Authenticate (saves key to ~/.lotics/config.json)
19
+ 2. lotics tools List available tools by category
20
+ 3. lotics tools <name> Show tool description and full input schema
21
+ 4. lotics run <tool> '<json>' Execute a tool with JSON arguments
22
+
23
+ Always inspect the schema (step 3) before calling a tool.
24
+ Tools are grouped by category (tables, records, views, etc.).
25
+ Query tools return IDs used as arguments to other tools.
11
26
 
12
27
  COMMANDS
13
- lotics auth Save API key (interactive)
14
- lotics logout Remove saved credentials
15
- lotics tools List all available tools
16
- lotics tools <name> Show tool description and input schema
17
- lotics run <tool> '<json>' Execute a tool
18
- lotics run <tool> --json Full JSON output instead of compact text
19
- lotics upload <file> Upload a local file
20
- lotics download <file_id> Download a file by ID
28
+ lotics auth Save API key (interactive)
29
+ lotics logout Remove saved credentials
30
+ lotics tools List all available tools
31
+ lotics tools <name> Show tool description and input schema
32
+ lotics run <tool> '<json>' Execute a tool
33
+ lotics upload <file|dir...> Upload files (directories expand to their immediate files)
34
+ lotics download <file_id> Download a file by ID
21
35
 
22
36
  FLAGS
23
- --json Full JSON output
37
+ --json Full JSON output (default is human-readable text)
24
38
  --timeout <ms> Timeout for tool execution (default: 60000)
25
39
  -o <path> Output dir for downloads
26
40
  --as <name> Override upload filename
27
- --api-key <key> API key (for CI/agents)
28
- --api-url <url> API URL override
41
+ --api-key <key> API key (overrides saved config and LOTICS_API_KEY)
29
42
  --version Show version
30
43
 
31
- WORKFLOW
32
- 1. lotics auth Authenticate once
33
- 2. lotics tools See what's available
34
- 3. lotics tools <name> Read the schema before calling
35
- 4. lotics run <tool> '<json>' Execute with correct args
44
+ For non-interactive use, set LOTICS_API_KEY in the environment
45
+ instead of running lotics auth.
46
+
47
+ OUTPUT
48
+ Default output is a human-readable text summary. Use --json to get
49
+ structured JSON for programmatic use. Errors print to stderr and
50
+ exit with code 1.
51
+
52
+ FILES
53
+ Some tools generate files and return { file_id, url, filename }.
54
+ Use lotics download to save locally:
55
+
56
+ lotics run generate_excel_from_template '{"..."}'
57
+ lotics download <file_id> -o ./output/
58
+
59
+ Upload files before referencing them in tool args:
60
+
61
+ lotics upload ./data.csv ./report.pdf ./documents/
62
+ lotics run create_records '{"table_id":"tbl_...","records":[{"fld_file":["fil_..."]}]}'
63
+
64
+ STDIN
65
+ Pipe JSON arguments via stdin instead of inline:
36
66
 
37
- ENVIRONMENT
38
- LOTICS_API_KEY API key (overrides saved config)
39
- LOTICS_API_URL API URL (overrides saved config)`);
67
+ echo '{"table_id":"tbl_..."}' | lotics run query_records`);
40
68
  }
41
69
  function parseArgs(argv) {
42
70
  const flags = {
@@ -45,13 +73,13 @@ function parseArgs(argv) {
45
73
  output: undefined,
46
74
  as: undefined,
47
75
  apiKey: undefined,
48
- apiUrl: undefined,
49
76
  version: false,
50
77
  help: false,
51
78
  };
52
79
  let command;
53
80
  let subcommand;
54
81
  let toolArgs;
82
+ const restArgs = [];
55
83
  let i = 0;
56
84
  while (i < argv.length) {
57
85
  const arg = argv[i];
@@ -72,9 +100,6 @@ function parseArgs(argv) {
72
100
  case "--api-key":
73
101
  flags.apiKey = argv[++i];
74
102
  break;
75
- case "--api-url":
76
- flags.apiUrl = argv[++i];
77
- break;
78
103
  case "--version":
79
104
  case "-v":
80
105
  flags.version = true;
@@ -93,11 +118,14 @@ function parseArgs(argv) {
93
118
  else if (!toolArgs) {
94
119
  toolArgs = arg;
95
120
  }
121
+ else {
122
+ restArgs.push(arg);
123
+ }
96
124
  break;
97
125
  }
98
126
  i++;
99
127
  }
100
- return { command, subcommand, toolArgs, flags };
128
+ return { command, subcommand, toolArgs, restArgs, flags };
101
129
  }
102
130
  function readStdin() {
103
131
  return new Promise((resolve, reject) => {
@@ -119,15 +147,13 @@ function prompt(question) {
119
147
  });
120
148
  });
121
149
  }
122
- async function handleAuth(flags) {
150
+ async function handleAuth() {
123
151
  const apiKey = await prompt("Enter your API key: ");
124
152
  if (!apiKey) {
125
153
  console.error("No API key provided.");
126
154
  process.exit(1);
127
155
  }
128
- const apiUrlInput = flags.apiUrl ?? (await prompt("API URL (default: https://api.lotics.com): "));
129
- const apiUrl = apiUrlInput || "https://api.lotics.com";
130
- const client = new LoticsClient({ apiKey, baseUrl: apiUrl });
156
+ const client = new LoticsClient({ apiKey });
131
157
  try {
132
158
  await client.listTools();
133
159
  }
@@ -137,7 +163,7 @@ async function handleAuth(flags) {
137
163
  process.exit(1);
138
164
  }
139
165
  const existing = loadConfig() ?? {};
140
- saveConfig({ ...existing, api_key: apiKey, api_url: apiUrl });
166
+ saveConfig({ ...existing, api_key: apiKey });
141
167
  console.error(`Authenticated. Config saved to ${getConfigPath()}`);
142
168
  }
143
169
  function requireClient(flags) {
@@ -146,10 +172,32 @@ function requireClient(flags) {
146
172
  console.error('Not authenticated. Run "lotics auth" or set LOTICS_API_KEY.');
147
173
  process.exit(1);
148
174
  }
149
- return new LoticsClient({ apiKey: auth.apiKey, baseUrl: auth.apiUrl });
175
+ return new LoticsClient({ apiKey: auth.apiKey });
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;
150
198
  }
151
199
  async function main() {
152
- const { command, subcommand, toolArgs, flags } = parseArgs(process.argv.slice(2));
200
+ const { command, subcommand, toolArgs, restArgs, flags } = parseArgs(process.argv.slice(2));
153
201
  if (flags.help || (!command && !flags.version)) {
154
202
  printHelp();
155
203
  return;
@@ -160,7 +208,7 @@ async function main() {
160
208
  }
161
209
  // --- Commands that don't require auth ---
162
210
  if (command === "auth") {
163
- await handleAuth(flags);
211
+ await handleAuth();
164
212
  return;
165
213
  }
166
214
  if (command === "logout") {
@@ -180,8 +228,8 @@ async function main() {
180
228
  process.exit(1);
181
229
  }
182
230
  if (command === "upload" && !subcommand) {
183
- console.error('Usage: lotics upload <file> [--as <name>]');
184
- 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.');
185
233
  process.exit(1);
186
234
  }
187
235
  if (command === "download" && !subcommand) {
@@ -194,28 +242,58 @@ async function main() {
194
242
  if (command === "tools") {
195
243
  if (subcommand) {
196
244
  const info = await client.getTool(subcommand);
197
- 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
+ }
198
251
  }
199
252
  else {
200
253
  const { categories } = await client.listTools();
201
- for (const [category, names] of Object.entries(categories)) {
202
- console.log(`\n${category}`);
203
- console.log(` ${names.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("");
204
263
  }
205
- console.log("");
206
264
  }
207
265
  return;
208
266
  }
209
- // lotics upload <file> [--as <name>]
267
+ // lotics upload <file|dir...> [--as <name>]
210
268
  if (command === "upload") {
211
- const upload = await client.uploadFile(subcommand, {
212
- 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,
213
281
  });
214
- if (upload.errors.length > 0) {
215
- 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(", ")}`);
216
284
  process.exit(1);
217
285
  }
218
- 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
+ }
219
297
  return;
220
298
  }
221
299
  // lotics download <file_id> [-o <path>]
@@ -241,18 +319,22 @@ async function main() {
241
319
  process.exit(1);
242
320
  }
243
321
  }
244
- const format = flags.json ? "json" : "text";
245
322
  const timeoutMs = flags.timeout ?? 60000;
246
- 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 });
247
325
  if (result.error) {
248
326
  console.error(result.error);
249
327
  console.error(`\nHint: run "lotics tools ${toolName}" to see the expected input schema.`);
250
328
  process.exit(1);
251
329
  }
252
- 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) {
253
334
  console.log(result.model_output);
254
335
  }
255
336
  else {
337
+ // Fallback: compact JSON when no text output is available
256
338
  console.log(JSON.stringify(result.result, null, 2));
257
339
  }
258
340
  return;
@@ -264,7 +346,6 @@ main()
264
346
  const message = error instanceof Error ? error.message : String(error);
265
347
  if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
266
348
  console.error("Could not connect to the Lotics API.");
267
- console.error('Check your API URL or run "lotics auth" to reconfigure.');
268
349
  }
269
350
  else if (message.includes("ENOENT")) {
270
351
  const pathMatch = message.match(/open '([^']+)'/);
@@ -1,6 +1,5 @@
1
1
  export interface LoticsClientOptions {
2
2
  apiKey: string;
3
- baseUrl?: string;
4
3
  }
5
4
  export interface ToolExecuteResult {
6
5
  result: unknown;
@@ -30,10 +29,11 @@ export declare class LoticsClient {
30
29
  private throwResponseError;
31
30
  private request;
32
31
  listTools(): Promise<{
33
- tools: Array<{
34
- name: string;
32
+ tools: string[];
33
+ categories: Record<string, {
34
+ description: string;
35
+ tools: string[];
35
36
  }>;
36
- categories: Record<string, string[]>;
37
37
  }>;
38
38
  getTool(name: string): Promise<ToolInfo>;
39
39
  execute(tool: string, args: Record<string, unknown>, options?: {
@@ -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
  }
@@ -26,7 +26,7 @@ export class LoticsClient {
26
26
  baseUrl;
27
27
  constructor(options) {
28
28
  this.apiKey = options.apiKey;
29
- this.baseUrl = (options.baseUrl ?? "https://api.lotics.com").replace(/\/$/, "");
29
+ this.baseUrl = "https://api.lotics.ai";
30
30
  }
31
31
  async throwResponseError(response) {
32
32
  const text = await response.text();
@@ -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",
@@ -1,6 +1,5 @@
1
1
  export interface LoticsConfig {
2
2
  api_key?: string;
3
- api_url?: string;
4
3
  last_update_check?: number;
5
4
  latest_version?: string;
6
5
  }
@@ -15,13 +14,11 @@ export declare function getConfigPath(): string;
15
14
  */
16
15
  export declare function checkForUpdate(currentVersion: string): void;
17
16
  /**
18
- * Resolve API key and URL from flags, env vars, or config file.
19
- * Priority: flag > env > config file > default.
17
+ * Resolve API key from flags, env vars, or config file.
18
+ * Priority: flag > env > config file.
20
19
  */
21
20
  export declare function resolveAuth(flags: {
22
21
  apiKey?: string;
23
- apiUrl?: string;
24
22
  }): {
25
23
  apiKey: string;
26
- apiUrl: string;
27
24
  } | null;
@@ -75,17 +75,13 @@ function printUpdateWarning(current, latest) {
75
75
  console.error(`Run: npm i -g @lotics/cli\n`);
76
76
  }
77
77
  /**
78
- * Resolve API key and URL from flags, env vars, or config file.
79
- * Priority: flag > env > config file > default.
78
+ * Resolve API key from flags, env vars, or config file.
79
+ * Priority: flag > env > config file.
80
80
  */
81
81
  export function resolveAuth(flags) {
82
82
  const config = loadConfig();
83
83
  const apiKey = flags.apiKey ?? process.env.LOTICS_API_KEY ?? config?.api_key;
84
84
  if (!apiKey)
85
85
  return null;
86
- const apiUrl = flags.apiUrl ??
87
- process.env.LOTICS_API_URL ??
88
- config?.api_url ??
89
- "https://api.lotics.com";
90
- return { apiKey, apiUrl };
86
+ return { apiKey };
91
87
  }
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION: string;
@@ -1 +1,6 @@
1
- export const VERSION = "0.1.0";
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
5
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
6
+ export const VERSION = pkg.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {