@lotics/cli 0.1.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 +65 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +277 -0
- package/dist/src/client.d.ts +51 -0
- package/dist/src/client.js +137 -0
- package/dist/src/config.d.ts +27 -0
- package/dist/src/config.js +91 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @lotics/cli
|
|
2
|
+
|
|
3
|
+
CLI and SDK for AI agents to interact with Lotics.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @lotics/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Update
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @lotics/cli@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI checks for updates once per day and prompts when a new version is available.
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Authenticate
|
|
23
|
+
lotics auth
|
|
24
|
+
|
|
25
|
+
# 2. Discover tools
|
|
26
|
+
lotics tools # list all tool names
|
|
27
|
+
lotics tools query_records # show description + input schema
|
|
28
|
+
|
|
29
|
+
# 3. Execute
|
|
30
|
+
lotics run query_tables '{}'
|
|
31
|
+
lotics run query_records '{"table_id":"tbl_...","field_keys":["name"]}'
|
|
32
|
+
lotics run query_records --json '{"table_id":"tbl_..."}'
|
|
33
|
+
|
|
34
|
+
# Pipe args via stdin
|
|
35
|
+
echo '{"table_id":"tbl_..."}' | lotics run query_records
|
|
36
|
+
|
|
37
|
+
# Upload a file
|
|
38
|
+
lotics upload ./report.pdf
|
|
39
|
+
lotics upload ./photo.jpg --as cover.jpg
|
|
40
|
+
|
|
41
|
+
# 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/
|
|
45
|
+
|
|
46
|
+
# CI / non-interactive
|
|
47
|
+
LOTICS_API_KEY=ltk_... lotics run query_tables '{}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## SDK
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { LoticsClient } from "@lotics/cli";
|
|
54
|
+
|
|
55
|
+
const client = new LoticsClient({
|
|
56
|
+
apiKey: "ltk_...",
|
|
57
|
+
baseUrl: "https://api.lotics.com",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { result } = await client.execute("query_tables", {});
|
|
61
|
+
const upload = await client.uploadFile("./report.pdf");
|
|
62
|
+
await client.downloadFile(url, "./output.xlsx");
|
|
63
|
+
const { tools } = await client.listTools();
|
|
64
|
+
const info = await client.getTool("query_records");
|
|
65
|
+
```
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { LoticsClient } from "./client.js";
|
|
4
|
+
import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
|
|
8
|
+
|
|
9
|
+
Lotics is an AI-powered operations platform for structured data,
|
|
10
|
+
document generation, automations, and knowledge management.
|
|
11
|
+
|
|
12
|
+
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
|
|
21
|
+
|
|
22
|
+
FLAGS
|
|
23
|
+
--json Full JSON output
|
|
24
|
+
--timeout <ms> Timeout for tool execution (default: 60000)
|
|
25
|
+
-o <path> Output dir for downloads
|
|
26
|
+
--as <name> Override upload filename
|
|
27
|
+
--api-key <key> API key (for CI/agents)
|
|
28
|
+
--api-url <url> API URL override
|
|
29
|
+
--version Show version
|
|
30
|
+
|
|
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
|
|
36
|
+
|
|
37
|
+
ENVIRONMENT
|
|
38
|
+
LOTICS_API_KEY API key (overrides saved config)
|
|
39
|
+
LOTICS_API_URL API URL (overrides saved config)`);
|
|
40
|
+
}
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const flags = {
|
|
43
|
+
json: false,
|
|
44
|
+
timeout: undefined,
|
|
45
|
+
output: undefined,
|
|
46
|
+
as: undefined,
|
|
47
|
+
apiKey: undefined,
|
|
48
|
+
apiUrl: undefined,
|
|
49
|
+
version: false,
|
|
50
|
+
help: false,
|
|
51
|
+
};
|
|
52
|
+
let command;
|
|
53
|
+
let subcommand;
|
|
54
|
+
let toolArgs;
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < argv.length) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case "--json":
|
|
60
|
+
flags.json = true;
|
|
61
|
+
break;
|
|
62
|
+
case "--timeout":
|
|
63
|
+
flags.timeout = parseInt(argv[++i], 10);
|
|
64
|
+
break;
|
|
65
|
+
case "--output":
|
|
66
|
+
case "-o":
|
|
67
|
+
flags.output = argv[++i];
|
|
68
|
+
break;
|
|
69
|
+
case "--as":
|
|
70
|
+
flags.as = argv[++i];
|
|
71
|
+
break;
|
|
72
|
+
case "--api-key":
|
|
73
|
+
flags.apiKey = argv[++i];
|
|
74
|
+
break;
|
|
75
|
+
case "--api-url":
|
|
76
|
+
flags.apiUrl = argv[++i];
|
|
77
|
+
break;
|
|
78
|
+
case "--version":
|
|
79
|
+
case "-v":
|
|
80
|
+
flags.version = true;
|
|
81
|
+
break;
|
|
82
|
+
case "--help":
|
|
83
|
+
case "-h":
|
|
84
|
+
flags.help = true;
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
if (!command) {
|
|
88
|
+
command = arg;
|
|
89
|
+
}
|
|
90
|
+
else if (!subcommand) {
|
|
91
|
+
subcommand = arg;
|
|
92
|
+
}
|
|
93
|
+
else if (!toolArgs) {
|
|
94
|
+
toolArgs = arg;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
return { command, subcommand, toolArgs, flags };
|
|
101
|
+
}
|
|
102
|
+
function readStdin() {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
106
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8").trim()));
|
|
107
|
+
process.stdin.on("error", reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function prompt(question) {
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stderr,
|
|
114
|
+
});
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
rl.question(question, (answer) => {
|
|
117
|
+
rl.close();
|
|
118
|
+
resolve(answer.trim());
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function handleAuth(flags) {
|
|
123
|
+
const apiKey = await prompt("Enter your API key: ");
|
|
124
|
+
if (!apiKey) {
|
|
125
|
+
console.error("No API key provided.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
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 });
|
|
131
|
+
try {
|
|
132
|
+
await client.listTools();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
console.error(`Authentication failed: ${message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const existing = loadConfig() ?? {};
|
|
140
|
+
saveConfig({ ...existing, api_key: apiKey, api_url: apiUrl });
|
|
141
|
+
console.error(`Authenticated. Config saved to ${getConfigPath()}`);
|
|
142
|
+
}
|
|
143
|
+
function requireClient(flags) {
|
|
144
|
+
const auth = resolveAuth(flags);
|
|
145
|
+
if (!auth) {
|
|
146
|
+
console.error('Not authenticated. Run "lotics auth" or set LOTICS_API_KEY.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
return new LoticsClient({ apiKey: auth.apiKey, baseUrl: auth.apiUrl });
|
|
150
|
+
}
|
|
151
|
+
async function main() {
|
|
152
|
+
const { command, subcommand, toolArgs, flags } = parseArgs(process.argv.slice(2));
|
|
153
|
+
if (flags.help || (!command && !flags.version)) {
|
|
154
|
+
printHelp();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (flags.version) {
|
|
158
|
+
console.log(VERSION);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// --- Commands that don't require auth ---
|
|
162
|
+
if (command === "auth") {
|
|
163
|
+
await handleAuth(flags);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (command === "logout") {
|
|
167
|
+
deleteConfig();
|
|
168
|
+
console.error("Logged out. Credentials removed.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// --- Validate command before auth ---
|
|
172
|
+
if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download") {
|
|
173
|
+
console.error(`Unknown command: ${command}`);
|
|
174
|
+
console.error('Run "lotics --help" for usage.');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
if (command === "run" && !subcommand) {
|
|
178
|
+
console.error('Usage: lotics run <tool> \'<json_args>\'');
|
|
179
|
+
console.error('Run "lotics tools" to see available tools.');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
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.');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
if (command === "download" && !subcommand) {
|
|
188
|
+
console.error('Usage: lotics download <file_id> [-o <dir>]');
|
|
189
|
+
console.error('File IDs come from upload results or generate_* tool output (--json).');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const client = requireClient(flags);
|
|
193
|
+
// lotics tools / lotics tools <name>
|
|
194
|
+
if (command === "tools") {
|
|
195
|
+
if (subcommand) {
|
|
196
|
+
const info = await client.getTool(subcommand);
|
|
197
|
+
console.log(JSON.stringify(info, null, 2));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const { categories } = await client.listTools();
|
|
201
|
+
for (const [category, names] of Object.entries(categories)) {
|
|
202
|
+
console.log(`\n${category}`);
|
|
203
|
+
console.log(` ${names.join(" ")}`);
|
|
204
|
+
}
|
|
205
|
+
console.log("");
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// lotics upload <file> [--as <name>]
|
|
210
|
+
if (command === "upload") {
|
|
211
|
+
const upload = await client.uploadFile(subcommand, {
|
|
212
|
+
filename: flags.as,
|
|
213
|
+
});
|
|
214
|
+
if (upload.errors.length > 0) {
|
|
215
|
+
console.error(`Upload failed: ${upload.errors[0].error}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
console.log(JSON.stringify(upload.files[0], null, 2));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// lotics download <file_id> [-o <path>]
|
|
222
|
+
if (command === "download") {
|
|
223
|
+
const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
|
|
224
|
+
console.error(`Downloaded: ${filePath} (${filename})`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// lotics run <tool> [json_args]
|
|
228
|
+
if (command === "run") {
|
|
229
|
+
const toolName = subcommand;
|
|
230
|
+
let rawArgs = toolArgs;
|
|
231
|
+
if (!rawArgs && !process.stdin.isTTY) {
|
|
232
|
+
rawArgs = await readStdin();
|
|
233
|
+
}
|
|
234
|
+
let args = {};
|
|
235
|
+
if (rawArgs) {
|
|
236
|
+
try {
|
|
237
|
+
args = JSON.parse(rawArgs);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
console.error(`Invalid JSON: ${rawArgs}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const format = flags.json ? "json" : "text";
|
|
245
|
+
const timeoutMs = flags.timeout ?? 60000;
|
|
246
|
+
const result = await client.execute(toolName, args, { format, timeoutMs });
|
|
247
|
+
if (result.error) {
|
|
248
|
+
console.error(result.error);
|
|
249
|
+
console.error(`\nHint: run "lotics tools ${toolName}" to see the expected input schema.`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
if (format === "text" && result.model_output) {
|
|
253
|
+
console.log(result.model_output);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
main()
|
|
262
|
+
.then(() => checkForUpdate(VERSION))
|
|
263
|
+
.catch((error) => {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
|
|
266
|
+
console.error("Could not connect to the Lotics API.");
|
|
267
|
+
console.error('Check your API URL or run "lotics auth" to reconfigure.');
|
|
268
|
+
}
|
|
269
|
+
else if (message.includes("ENOENT")) {
|
|
270
|
+
const pathMatch = message.match(/open '([^']+)'/);
|
|
271
|
+
console.error(`File not found: ${pathMatch?.[1] ?? message}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.error(message);
|
|
275
|
+
}
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface LoticsClientOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolExecuteResult {
|
|
6
|
+
result: unknown;
|
|
7
|
+
model_output?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
input_schema: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface FileUploadResult {
|
|
16
|
+
files: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
filename: string;
|
|
19
|
+
mime_type: string;
|
|
20
|
+
}>;
|
|
21
|
+
errors: Array<{
|
|
22
|
+
filename: string;
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare class LoticsClient {
|
|
27
|
+
private apiKey;
|
|
28
|
+
private baseUrl;
|
|
29
|
+
constructor(options: LoticsClientOptions);
|
|
30
|
+
private throwResponseError;
|
|
31
|
+
private request;
|
|
32
|
+
listTools(): Promise<{
|
|
33
|
+
tools: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
}>;
|
|
36
|
+
categories: Record<string, string[]>;
|
|
37
|
+
}>;
|
|
38
|
+
getTool(name: string): Promise<ToolInfo>;
|
|
39
|
+
execute(tool: string, args: Record<string, unknown>, options?: {
|
|
40
|
+
format?: "json" | "text";
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}): Promise<ToolExecuteResult>;
|
|
43
|
+
downloadFile(url: string, outputPath: string): Promise<string>;
|
|
44
|
+
downloadFileById(fileId: string, outputDir?: string): Promise<{
|
|
45
|
+
path: string;
|
|
46
|
+
filename: string;
|
|
47
|
+
}>;
|
|
48
|
+
uploadFile(filePath: string, options?: {
|
|
49
|
+
filename?: string;
|
|
50
|
+
}): Promise<FileUploadResult>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MIME_MAP = {
|
|
4
|
+
".jpg": "image/jpeg",
|
|
5
|
+
".jpeg": "image/jpeg",
|
|
6
|
+
".png": "image/png",
|
|
7
|
+
".gif": "image/gif",
|
|
8
|
+
".webp": "image/webp",
|
|
9
|
+
".svg": "image/svg+xml",
|
|
10
|
+
".pdf": "application/pdf",
|
|
11
|
+
".csv": "text/csv",
|
|
12
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
13
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
14
|
+
".txt": "text/plain",
|
|
15
|
+
".json": "application/json",
|
|
16
|
+
".html": "text/html",
|
|
17
|
+
".xml": "application/xml",
|
|
18
|
+
".zip": "application/zip",
|
|
19
|
+
};
|
|
20
|
+
function getMimeType(filename) {
|
|
21
|
+
const ext = path.extname(filename).toLowerCase();
|
|
22
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
23
|
+
}
|
|
24
|
+
export class LoticsClient {
|
|
25
|
+
apiKey;
|
|
26
|
+
baseUrl;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.apiKey = options.apiKey;
|
|
29
|
+
this.baseUrl = (options.baseUrl ?? "https://api.lotics.com").replace(/\/$/, "");
|
|
30
|
+
}
|
|
31
|
+
async throwResponseError(response) {
|
|
32
|
+
const text = await response.text();
|
|
33
|
+
let message;
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(text);
|
|
36
|
+
message = json.message ?? text;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
message = text;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`${response.status}: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
async request(method, path, body) {
|
|
44
|
+
const url = `${this.baseUrl}${path}`;
|
|
45
|
+
const headers = {
|
|
46
|
+
"x-api-key": this.apiKey,
|
|
47
|
+
};
|
|
48
|
+
const init = { method, headers };
|
|
49
|
+
if (body !== undefined) {
|
|
50
|
+
headers["Content-Type"] = "application/json";
|
|
51
|
+
init.body = JSON.stringify(body);
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(url, init);
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
await this.throwResponseError(response);
|
|
56
|
+
return response.json();
|
|
57
|
+
}
|
|
58
|
+
async listTools() {
|
|
59
|
+
return this.request("GET", "/v1/tools");
|
|
60
|
+
}
|
|
61
|
+
async getTool(name) {
|
|
62
|
+
return this.request("GET", `/v1/tools/${encodeURIComponent(name)}`);
|
|
63
|
+
}
|
|
64
|
+
async execute(tool, args, options) {
|
|
65
|
+
const body = { tool, args, format: options?.format ?? "text" };
|
|
66
|
+
if (options?.timeoutMs) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
69
|
+
try {
|
|
70
|
+
const url = `${this.baseUrl}/v1/tools/execute`;
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "x-api-key": this.apiKey, "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
await this.throwResponseError(response);
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
83
|
+
throw new Error(`Tool execution timed out after ${options.timeoutMs}ms`);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return this.request("POST", "/v1/tools/execute", body);
|
|
92
|
+
}
|
|
93
|
+
async downloadFile(url, outputPath) {
|
|
94
|
+
const response = await fetch(url);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`${response.status}: Failed to download file`);
|
|
97
|
+
}
|
|
98
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
99
|
+
const absolutePath = path.resolve(outputPath);
|
|
100
|
+
await fs.promises.writeFile(absolutePath, buffer);
|
|
101
|
+
return absolutePath;
|
|
102
|
+
}
|
|
103
|
+
async downloadFileById(fileId, outputDir) {
|
|
104
|
+
const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/download`;
|
|
105
|
+
const response = await fetch(url, {
|
|
106
|
+
headers: { "x-api-key": this.apiKey },
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok)
|
|
109
|
+
await this.throwResponseError(response);
|
|
110
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
111
|
+
const match = disposition.match(/filename="?([^";\n]+)"?/);
|
|
112
|
+
const filename = match?.[1] ?? fileId;
|
|
113
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
114
|
+
const dir = outputDir ? path.resolve(outputDir) : process.cwd();
|
|
115
|
+
const absolutePath = path.join(dir, filename);
|
|
116
|
+
await fs.promises.writeFile(absolutePath, buffer);
|
|
117
|
+
return { path: absolutePath, filename };
|
|
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 });
|
|
125
|
+
const formData = new FormData();
|
|
126
|
+
formData.append("file", blob, filename);
|
|
127
|
+
const url = `${this.baseUrl}/v1/files`;
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "x-api-key": this.apiKey },
|
|
131
|
+
body: formData,
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok)
|
|
134
|
+
await this.throwResponseError(response);
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface LoticsConfig {
|
|
2
|
+
api_key?: string;
|
|
3
|
+
api_url?: string;
|
|
4
|
+
last_update_check?: number;
|
|
5
|
+
latest_version?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadConfig(): LoticsConfig | null;
|
|
8
|
+
export declare function saveConfig(config: LoticsConfig): void;
|
|
9
|
+
export declare function deleteConfig(): void;
|
|
10
|
+
export declare function getConfigPath(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Check for a newer CLI version. Synchronous — prints a warning to stderr
|
|
13
|
+
* if the cached latest version is newer than current. Kicks off a background
|
|
14
|
+
* fetch if the cache is stale (result is written to config for next run).
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkForUpdate(currentVersion: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve API key and URL from flags, env vars, or config file.
|
|
19
|
+
* Priority: flag > env > config file > default.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAuth(flags: {
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
}): {
|
|
25
|
+
apiKey: string;
|
|
26
|
+
apiUrl: string;
|
|
27
|
+
} | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".lotics");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function loadConfig() {
|
|
7
|
+
try {
|
|
8
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function saveConfig(config) {
|
|
16
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
18
|
+
}
|
|
19
|
+
export function deleteConfig() {
|
|
20
|
+
try {
|
|
21
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Already deleted or never existed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getConfigPath() {
|
|
28
|
+
return CONFIG_FILE;
|
|
29
|
+
}
|
|
30
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
31
|
+
/**
|
|
32
|
+
* Check for a newer CLI version. Synchronous — prints a warning to stderr
|
|
33
|
+
* if the cached latest version is newer than current. Kicks off a background
|
|
34
|
+
* fetch if the cache is stale (result is written to config for next run).
|
|
35
|
+
*/
|
|
36
|
+
export function checkForUpdate(currentVersion) {
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
const lastCheck = config?.last_update_check ?? 0;
|
|
39
|
+
if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) {
|
|
40
|
+
if (config?.latest_version && config.latest_version !== currentVersion) {
|
|
41
|
+
printUpdateWarning(currentVersion, config.latest_version);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Cache is stale — fetch in background, don't block exit
|
|
46
|
+
fetchLatestVersion().then((latest) => {
|
|
47
|
+
if (!latest)
|
|
48
|
+
return;
|
|
49
|
+
const existing = loadConfig() ?? {};
|
|
50
|
+
saveConfig({ ...existing, last_update_check: Date.now(), latest_version: latest });
|
|
51
|
+
}).catch(() => { });
|
|
52
|
+
}
|
|
53
|
+
async function fetchLatestVersion() {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
56
|
+
timeout.unref();
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch("https://registry.npmjs.org/@lotics/cli/latest", {
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok)
|
|
62
|
+
return null;
|
|
63
|
+
const data = (await response.json());
|
|
64
|
+
return data.version ?? null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function printUpdateWarning(current, latest) {
|
|
74
|
+
console.error(`\nUpdate available: ${current} → ${latest}`);
|
|
75
|
+
console.error(`Run: npm i -g @lotics/cli\n`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve API key and URL from flags, env vars, or config file.
|
|
79
|
+
* Priority: flag > env > config file > default.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveAuth(flags) {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
const apiKey = flags.apiKey ?? process.env.LOTICS_API_KEY ?? config?.api_key;
|
|
84
|
+
if (!apiKey)
|
|
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 };
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.1.0";
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lotics/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lotics SDK and CLI for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lotics": "./dist/src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/src/client.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsgo",
|
|
18
|
+
"typecheck": "tsgo --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"lotics",
|
|
23
|
+
"sdk",
|
|
24
|
+
"cli",
|
|
25
|
+
"ai-agent"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lotics/lotics.git",
|
|
34
|
+
"directory": "packages/sdk"
|
|
35
|
+
}
|
|
36
|
+
}
|