@lidianai/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 +46 -0
- package/biome.json +14 -0
- package/dist/index.js +405 -0
- package/package.json +24 -0
- package/src/commands/account.ts +23 -0
- package/src/commands/act.ts +83 -0
- package/src/commands/login.ts +70 -0
- package/src/commands/query.ts +45 -0
- package/src/index.ts +193 -0
- package/src/lib/auth.ts +15 -0
- package/src/lib/config.ts +33 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/http.ts +69 -0
- package/src/lib/output.ts +100 -0
- package/src/lib/x402.ts +54 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Lidian CLI
|
|
2
|
+
|
|
3
|
+
Bun CLI for Lidian core REST endpoints.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @lidian/cli
|
|
9
|
+
# or
|
|
10
|
+
bunx @lidian/cli --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
lidian query --q "<term>" [--page 1] [--pageSize 1..3] [--api-key <key>] [--json]
|
|
17
|
+
lidian act --endpoint-id <uuid> --params '<json>' [--payment-rail prepaid_credits|x402] [--network base|ethereum] [--api-key <key>] [--json]
|
|
18
|
+
lidian account [--api-key <key>] [--json]
|
|
19
|
+
lidian login [--key ld_...] [--json]
|
|
20
|
+
lidian --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`query` returns API matches (`items[]`) from `/v1/query` with metadata and confidence fields (`matchScore`, `matchPercent`).
|
|
24
|
+
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
- Store key locally: `lidian login --key ld_...` (writes `~/.lidian/config.json`)
|
|
28
|
+
- Or run `lidian login` and paste your key after browser auth flow.
|
|
29
|
+
- Resolution order: `--api-key` -> `LIDIAN_API_KEY` -> `~/.lidian/config.json`.
|
|
30
|
+
- Optional: set `LIDIAN_API_BASE` (default `https://api.lidian.ai`).
|
|
31
|
+
|
|
32
|
+
## x402
|
|
33
|
+
|
|
34
|
+
When `--payment-rail x402` is used, CLI performs:
|
|
35
|
+
1. `POST /v1/payments/requirements`
|
|
36
|
+
2. `POST /v1/payments/verify`
|
|
37
|
+
3. `POST /v1/act`
|
|
38
|
+
|
|
39
|
+
## Dev
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun install
|
|
43
|
+
bun run typecheck
|
|
44
|
+
bun run lint
|
|
45
|
+
bun run build
|
|
46
|
+
```
|
package/biome.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"includes": ["src/**", "README.md", "package.json", "tsconfig.json"]
|
|
5
|
+
},
|
|
6
|
+
"formatter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"indentStyle": "space",
|
|
9
|
+
"indentWidth": 2
|
|
10
|
+
},
|
|
11
|
+
"linter": {
|
|
12
|
+
"enabled": true
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/commands/account.ts
|
|
5
|
+
var runAccountCommand = async (http, apiKey) => {
|
|
6
|
+
return http.get("/v1/account", apiKey);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/lib/errors.ts
|
|
10
|
+
class CliError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(message, code = 1) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "CliError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/lib/x402.ts
|
|
20
|
+
var requestPaymentRequirements = async (http, apiKey, endpointId, network) => {
|
|
21
|
+
return http.post("/v1/payments/requirements", { endpointId, ...network ? { network } : {} }, apiKey);
|
|
22
|
+
};
|
|
23
|
+
var verifyPaymentAddress = async (http, apiKey, payTo) => {
|
|
24
|
+
return http.post("/v1/payments/verify", { payTo }, apiKey);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/commands/act.ts
|
|
28
|
+
var runActCommand = async (http, apiKey, input) => {
|
|
29
|
+
if (!isUuid(input.endpointId)) {
|
|
30
|
+
throw new CliError("endpointId must be a valid UUID.");
|
|
31
|
+
}
|
|
32
|
+
if (input.paymentRail === "x402") {
|
|
33
|
+
const requirements = await requestPaymentRequirements(http, apiKey, input.endpointId, input.network);
|
|
34
|
+
const verification = await verifyPaymentAddress(http, apiKey, requirements.payTo);
|
|
35
|
+
const execution2 = await http.post("/v1/act", input, apiKey);
|
|
36
|
+
return {
|
|
37
|
+
execution: execution2,
|
|
38
|
+
payment: {
|
|
39
|
+
requirements,
|
|
40
|
+
verified: verification.valid
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const execution = await http.post("/v1/act", input, apiKey);
|
|
45
|
+
return { execution };
|
|
46
|
+
};
|
|
47
|
+
var isUuid = (value) => {
|
|
48
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/commands/login.ts
|
|
52
|
+
import { stdin, stdout } from "process";
|
|
53
|
+
import { createInterface } from "readline/promises";
|
|
54
|
+
|
|
55
|
+
// src/lib/config.ts
|
|
56
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
57
|
+
import { homedir } from "os";
|
|
58
|
+
import { dirname, join } from "path";
|
|
59
|
+
var CONFIG_PATH = join(homedir(), ".lidian", "config.json");
|
|
60
|
+
var getConfigPath = () => CONFIG_PATH;
|
|
61
|
+
var readConfig = async () => {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
const apiKey = "apiKey" in parsed && typeof parsed.apiKey === "string" ? parsed.apiKey : undefined;
|
|
69
|
+
return { apiKey };
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var writeConfig = async (config) => {
|
|
75
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
76
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
77
|
+
`, "utf8");
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/lib/output.ts
|
|
81
|
+
var print = (message) => {
|
|
82
|
+
process.stdout.write(`${message}
|
|
83
|
+
`);
|
|
84
|
+
};
|
|
85
|
+
var printError = (message) => {
|
|
86
|
+
process.stderr.write(`${message}
|
|
87
|
+
`);
|
|
88
|
+
};
|
|
89
|
+
var printResult = (result, asJson) => {
|
|
90
|
+
if (asJson) {
|
|
91
|
+
print(JSON.stringify(result, null, 2));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
print(formatForHuman(result));
|
|
95
|
+
};
|
|
96
|
+
var printQueryResult = (result, asJson) => {
|
|
97
|
+
if (asJson) {
|
|
98
|
+
printResult(result, true);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (result.items.length === 0) {
|
|
102
|
+
print("No APIs found.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
print(`Found ${result.items.length} of ${result.total} APIs (page ${result.page}).`);
|
|
106
|
+
for (const item of result.items) {
|
|
107
|
+
const confidence = item.matchPercent ? ` confidence=${item.matchPercent.toFixed(1)}%` : "";
|
|
108
|
+
print(`- ${item.name} (${item.id}) auth=${item.authType} cost=${item.defaultCostPerUse}c${confidence}`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var printActResult = (result, asJson) => {
|
|
112
|
+
if (asJson) {
|
|
113
|
+
printResult(result.execution, true);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (result.payment) {
|
|
117
|
+
print(`x402 preflight: payTo=${result.payment.requirements.payTo} amount=${result.payment.requirements.amountFormatted} verified=${String(result.payment.verified)}`);
|
|
118
|
+
}
|
|
119
|
+
printExecutionResult(result.execution);
|
|
120
|
+
};
|
|
121
|
+
var printAccountResult = (result, asJson) => {
|
|
122
|
+
if (asJson) {
|
|
123
|
+
printResult(result, true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
print(`Account: ${result.user.id}`);
|
|
127
|
+
print(`Balance: ${result.balance.balance} credits`);
|
|
128
|
+
};
|
|
129
|
+
var printExecutionResult = (result) => {
|
|
130
|
+
print(`Execution succeeded. Spent=${result.credits.spent} balance=${result.credits.balance}`);
|
|
131
|
+
print(JSON.stringify(result.data, null, 2));
|
|
132
|
+
};
|
|
133
|
+
var formatForHuman = (value) => {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify(value, null, 2);
|
|
138
|
+
};
|
|
139
|
+
var fail = (error) => {
|
|
140
|
+
if (error instanceof CliError) {
|
|
141
|
+
printError(`Error: ${error.message}`);
|
|
142
|
+
process.exit(error.code);
|
|
143
|
+
}
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
printError(`Error: ${error.message}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
printError("Error: Unknown failure");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/commands/login.ts
|
|
153
|
+
var runLoginCommand = async (input) => {
|
|
154
|
+
const key = input.key ?? await promptForKeyViaBrowserFlow();
|
|
155
|
+
validateApiKey(key);
|
|
156
|
+
const current = await readConfig();
|
|
157
|
+
await writeConfig({
|
|
158
|
+
...current,
|
|
159
|
+
apiKey: key
|
|
160
|
+
});
|
|
161
|
+
return { path: getConfigPath() };
|
|
162
|
+
};
|
|
163
|
+
var promptForKeyViaBrowserFlow = async () => {
|
|
164
|
+
const loginUrl = "https://app.lidian.ai/login?next=/user/api-keys";
|
|
165
|
+
openUrl(loginUrl);
|
|
166
|
+
print(`Open this URL to authenticate and create/copy an API key:
|
|
167
|
+
${loginUrl}`);
|
|
168
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
169
|
+
try {
|
|
170
|
+
const entered = (await rl.question("Paste your API key (ld_...): ")).trim();
|
|
171
|
+
if (!entered) {
|
|
172
|
+
throw new CliError("No API key entered.");
|
|
173
|
+
}
|
|
174
|
+
return entered;
|
|
175
|
+
} finally {
|
|
176
|
+
rl.close();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var validateApiKey = (key) => {
|
|
180
|
+
if (!key.startsWith("ld_")) {
|
|
181
|
+
throw new CliError("Invalid API key format. Expected key starting with ld_.");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var openUrl = (url) => {
|
|
185
|
+
try {
|
|
186
|
+
if (process.platform === "darwin") {
|
|
187
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (process.platform === "win32") {
|
|
191
|
+
Bun.spawn(["cmd", "/c", "start", "", url], {
|
|
192
|
+
stdout: "ignore",
|
|
193
|
+
stderr: "ignore"
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
|
|
198
|
+
} catch {}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/commands/query.ts
|
|
202
|
+
var runQueryCommand = async (http, apiKey, input) => {
|
|
203
|
+
if (input.pageSize < 1 || input.pageSize > 3) {
|
|
204
|
+
throw new CliError("pageSize must be between 1 and 3.");
|
|
205
|
+
}
|
|
206
|
+
const params = new URLSearchParams({
|
|
207
|
+
q: input.q,
|
|
208
|
+
page: String(input.page),
|
|
209
|
+
pageSize: String(input.pageSize)
|
|
210
|
+
});
|
|
211
|
+
return http.get(`/v1/query?${params.toString()}`, apiKey);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/lib/auth.ts
|
|
215
|
+
var resolveApiKey = async (argsApiKey) => {
|
|
216
|
+
const config = await readConfig();
|
|
217
|
+
const key = argsApiKey ?? process.env.LIDIAN_API_KEY ?? config.apiKey;
|
|
218
|
+
if (!key) {
|
|
219
|
+
throw new CliError("Missing API key. Use `lidian login --key ld_...`, pass --api-key, or set LIDIAN_API_KEY.");
|
|
220
|
+
}
|
|
221
|
+
return key;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/lib/http.ts
|
|
225
|
+
var createHttpClient = (baseUrl) => {
|
|
226
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
227
|
+
return {
|
|
228
|
+
async get(path, apiKey) {
|
|
229
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
230
|
+
method: "GET",
|
|
231
|
+
headers: authHeaders(apiKey)
|
|
232
|
+
});
|
|
233
|
+
return handleResponse(response);
|
|
234
|
+
},
|
|
235
|
+
async post(path, body, apiKey) {
|
|
236
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: {
|
|
239
|
+
...authHeaders(apiKey),
|
|
240
|
+
"Content-Type": "application/json"
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(body)
|
|
243
|
+
});
|
|
244
|
+
return handleResponse(response);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
var authHeaders = (apiKey) => ({
|
|
249
|
+
Authorization: `Bearer ${apiKey}`
|
|
250
|
+
});
|
|
251
|
+
var handleResponse = async (response) => {
|
|
252
|
+
const json = await response.json().catch(() => null);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
if (json && "success" in json && json.success === false) {
|
|
255
|
+
throw new CliError(`${json.error.code}: ${json.error.message}`, 1);
|
|
256
|
+
}
|
|
257
|
+
throw new CliError(`Request failed with status ${response.status}`, 1);
|
|
258
|
+
}
|
|
259
|
+
if (!json || !("success" in json) || json.success !== true) {
|
|
260
|
+
throw new CliError("Unexpected API response format", 1);
|
|
261
|
+
}
|
|
262
|
+
return json.data;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/index.ts
|
|
266
|
+
var DEFAULT_API_BASE = "https://api.lidian.ai";
|
|
267
|
+
var GLOBAL_OPTIONS = new Set(["api-key", "api-base", "json", "help"]);
|
|
268
|
+
var COMMAND_OPTIONS = {
|
|
269
|
+
query: new Set(["q", "page", "pageSize"]),
|
|
270
|
+
act: new Set(["endpoint-id", "params", "payment-rail", "network"]),
|
|
271
|
+
account: new Set([]),
|
|
272
|
+
login: new Set(["key"])
|
|
273
|
+
};
|
|
274
|
+
var main = async () => {
|
|
275
|
+
if (process.argv.length <= 2 || process.argv.includes("--help")) {
|
|
276
|
+
printUsage();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
280
|
+
const apiBase = String(parsed.options["api-base"] ?? process.env.LIDIAN_API_BASE ?? DEFAULT_API_BASE);
|
|
281
|
+
const asJson = Boolean(parsed.options.json);
|
|
282
|
+
const http = createHttpClient(apiBase);
|
|
283
|
+
switch (parsed.command) {
|
|
284
|
+
case "login": {
|
|
285
|
+
const key = asString(parsed.options.key);
|
|
286
|
+
const result = await runLoginCommand({ key });
|
|
287
|
+
if (asJson) {
|
|
288
|
+
print(JSON.stringify({ success: true, data: result }, null, 2));
|
|
289
|
+
} else {
|
|
290
|
+
print(`Saved API key to ${result.path}`);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
case "query": {
|
|
295
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
296
|
+
const qValue = asString(parsed.options.q);
|
|
297
|
+
if (!qValue) {
|
|
298
|
+
throw new CliError("Missing --q for query command.");
|
|
299
|
+
}
|
|
300
|
+
const page = toInt(asString(parsed.options.page), 1);
|
|
301
|
+
const pageSize = toInt(asString(parsed.options.pageSize), 1);
|
|
302
|
+
const result = await runQueryCommand(http, apiKey, {
|
|
303
|
+
q: qValue,
|
|
304
|
+
page,
|
|
305
|
+
pageSize
|
|
306
|
+
});
|
|
307
|
+
printQueryResult(result, asJson);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
case "act": {
|
|
311
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
312
|
+
const endpointIdValue = asString(parsed.options["endpoint-id"]);
|
|
313
|
+
if (!endpointIdValue) {
|
|
314
|
+
throw new CliError("Missing --endpoint-id for act command.");
|
|
315
|
+
}
|
|
316
|
+
const paramsRaw = asString(parsed.options.params) ?? "{}";
|
|
317
|
+
const params = parseJsonObject(paramsRaw, "--params");
|
|
318
|
+
const paymentRail = asPaymentRail(asString(parsed.options["payment-rail"]) ?? "prepaid_credits");
|
|
319
|
+
const network = asString(parsed.options.network);
|
|
320
|
+
const result = await runActCommand(http, apiKey, {
|
|
321
|
+
endpointId: endpointIdValue,
|
|
322
|
+
params,
|
|
323
|
+
paymentRail,
|
|
324
|
+
...network ? { network } : {}
|
|
325
|
+
});
|
|
326
|
+
printActResult(result, asJson);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
case "account": {
|
|
330
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
331
|
+
const result = await runAccountCommand(http, apiKey);
|
|
332
|
+
printAccountResult(result, asJson);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
default:
|
|
336
|
+
throw new CliError("Unknown command.", 1);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var parseArgs = (argv) => {
|
|
340
|
+
const command = argv[0];
|
|
341
|
+
if (command !== "query" && command !== "act" && command !== "account" && command !== "login") {
|
|
342
|
+
printUsage();
|
|
343
|
+
throw new CliError("Invalid command. Use one of: login, query, act, account.", 1);
|
|
344
|
+
}
|
|
345
|
+
const options = {};
|
|
346
|
+
let index = 1;
|
|
347
|
+
while (index < argv.length) {
|
|
348
|
+
const token = argv[index];
|
|
349
|
+
if (!token || !token.startsWith("--")) {
|
|
350
|
+
throw new CliError(`Unexpected argument: ${token ?? "<empty>"}`);
|
|
351
|
+
}
|
|
352
|
+
const key = token.slice(2);
|
|
353
|
+
const allowedForCommand = COMMAND_OPTIONS[command];
|
|
354
|
+
if (!GLOBAL_OPTIONS.has(key) && !allowedForCommand.has(key)) {
|
|
355
|
+
throw new CliError(`Unknown option for ${command}: --${key}`);
|
|
356
|
+
}
|
|
357
|
+
const next = argv[index + 1];
|
|
358
|
+
if (!next || next.startsWith("--")) {
|
|
359
|
+
options[key] = true;
|
|
360
|
+
index += 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
options[key] = next;
|
|
364
|
+
index += 2;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
command,
|
|
368
|
+
options
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
var printUsage = () => {
|
|
372
|
+
print("Usage:");
|
|
373
|
+
print(" lidian login [--key ld_...] [--json]");
|
|
374
|
+
print(' lidian query --q "<term>" [--page 1] [--pageSize 1..3] [--api-key <key>] [--json]');
|
|
375
|
+
print(" lidian act --endpoint-id <uuid> --params '<json>' [--payment-rail prepaid_credits|x402] [--api-key <key>] [--json]");
|
|
376
|
+
print(" [--network base|ethereum]");
|
|
377
|
+
print(" lidian account [--api-key <key>] [--json]");
|
|
378
|
+
};
|
|
379
|
+
var asString = (value) => {
|
|
380
|
+
if (typeof value === "string")
|
|
381
|
+
return value;
|
|
382
|
+
return;
|
|
383
|
+
};
|
|
384
|
+
var toInt = (value, fallback) => {
|
|
385
|
+
if (!value)
|
|
386
|
+
return fallback;
|
|
387
|
+
const parsed = Number.parseInt(value, 10);
|
|
388
|
+
if (Number.isNaN(parsed)) {
|
|
389
|
+
throw new CliError(`Invalid integer value: ${value}`);
|
|
390
|
+
}
|
|
391
|
+
return parsed;
|
|
392
|
+
};
|
|
393
|
+
var parseJsonObject = (value, flagName) => {
|
|
394
|
+
const parsed = JSON.parse(value);
|
|
395
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
396
|
+
throw new CliError(`${flagName} must be a JSON object.`);
|
|
397
|
+
}
|
|
398
|
+
return parsed;
|
|
399
|
+
};
|
|
400
|
+
var asPaymentRail = (value) => {
|
|
401
|
+
if (value === "prepaid_credits" || value === "x402")
|
|
402
|
+
return value;
|
|
403
|
+
throw new CliError("Invalid --payment-rail. Use prepaid_credits or x402.");
|
|
404
|
+
};
|
|
405
|
+
main().catch(fail);
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lidianai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"lidian": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "bun run src/index.ts",
|
|
14
|
+
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"lint": "biome check .",
|
|
17
|
+
"test": "bun test"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "^2.4.4",
|
|
21
|
+
"@types/bun": "^1.3.9",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpClient } from "@/lib/http";
|
|
2
|
+
|
|
3
|
+
export interface AccountApiResponse {
|
|
4
|
+
user: {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string | null;
|
|
8
|
+
role: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
};
|
|
11
|
+
balance: {
|
|
12
|
+
balance: number;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const runAccountCommand = async (
|
|
19
|
+
http: HttpClient,
|
|
20
|
+
apiKey: string,
|
|
21
|
+
): Promise<AccountApiResponse> => {
|
|
22
|
+
return http.get<AccountApiResponse>("/v1/account", apiKey);
|
|
23
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CliError } from "@/lib/errors";
|
|
2
|
+
import type { HttpClient } from "@/lib/http";
|
|
3
|
+
import {
|
|
4
|
+
type PaymentRequirementsResponse,
|
|
5
|
+
requestPaymentRequirements,
|
|
6
|
+
verifyPaymentAddress,
|
|
7
|
+
} from "@/lib/x402";
|
|
8
|
+
|
|
9
|
+
export type PaymentRail = "prepaid_credits" | "x402";
|
|
10
|
+
|
|
11
|
+
export interface ActCommandInput {
|
|
12
|
+
endpointId: string;
|
|
13
|
+
params: Record<string, unknown>;
|
|
14
|
+
paymentRail: PaymentRail;
|
|
15
|
+
network?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ActApiResponse {
|
|
19
|
+
data: unknown;
|
|
20
|
+
credits: {
|
|
21
|
+
spent: number;
|
|
22
|
+
balance: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ActCommandResult {
|
|
27
|
+
execution: ActApiResponse;
|
|
28
|
+
payment?: {
|
|
29
|
+
requirements: PaymentRequirementsResponse;
|
|
30
|
+
verified: boolean;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const runActCommand = async (
|
|
35
|
+
http: HttpClient,
|
|
36
|
+
apiKey: string,
|
|
37
|
+
input: ActCommandInput,
|
|
38
|
+
): Promise<ActCommandResult> => {
|
|
39
|
+
if (!isUuid(input.endpointId)) {
|
|
40
|
+
throw new CliError("endpointId must be a valid UUID.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (input.paymentRail === "x402") {
|
|
44
|
+
const requirements = await requestPaymentRequirements(
|
|
45
|
+
http,
|
|
46
|
+
apiKey,
|
|
47
|
+
input.endpointId,
|
|
48
|
+
input.network,
|
|
49
|
+
);
|
|
50
|
+
const verification = await verifyPaymentAddress(
|
|
51
|
+
http,
|
|
52
|
+
apiKey,
|
|
53
|
+
requirements.payTo,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const execution = await http.post<ActApiResponse, ActCommandInput>(
|
|
57
|
+
"/v1/act",
|
|
58
|
+
input,
|
|
59
|
+
apiKey,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
execution,
|
|
64
|
+
payment: {
|
|
65
|
+
requirements,
|
|
66
|
+
verified: verification.valid,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const execution = await http.post<ActApiResponse, ActCommandInput>(
|
|
72
|
+
"/v1/act",
|
|
73
|
+
input,
|
|
74
|
+
apiKey,
|
|
75
|
+
);
|
|
76
|
+
return { execution };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const isUuid = (value: string): boolean => {
|
|
80
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
81
|
+
value,
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { stdin, stdout } from "node:process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { getConfigPath, readConfig, writeConfig } from "@/lib/config";
|
|
4
|
+
import { CliError } from "@/lib/errors";
|
|
5
|
+
import { print } from "@/lib/output";
|
|
6
|
+
|
|
7
|
+
export interface LoginCommandInput {
|
|
8
|
+
key?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const runLoginCommand = async (
|
|
12
|
+
input: LoginCommandInput,
|
|
13
|
+
): Promise<{ path: string }> => {
|
|
14
|
+
const key = input.key ?? (await promptForKeyViaBrowserFlow());
|
|
15
|
+
validateApiKey(key);
|
|
16
|
+
|
|
17
|
+
const current = await readConfig();
|
|
18
|
+
await writeConfig({
|
|
19
|
+
...current,
|
|
20
|
+
apiKey: key,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return { path: getConfigPath() };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const promptForKeyViaBrowserFlow = async (): Promise<string> => {
|
|
27
|
+
const loginUrl = "https://app.lidian.ai/login?next=/user/api-keys";
|
|
28
|
+
openUrl(loginUrl);
|
|
29
|
+
print(
|
|
30
|
+
`Open this URL to authenticate and create/copy an API key:\n${loginUrl}`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
34
|
+
try {
|
|
35
|
+
const entered = (await rl.question("Paste your API key (ld_...): ")).trim();
|
|
36
|
+
if (!entered) {
|
|
37
|
+
throw new CliError("No API key entered.");
|
|
38
|
+
}
|
|
39
|
+
return entered;
|
|
40
|
+
} finally {
|
|
41
|
+
rl.close();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const validateApiKey = (key: string): void => {
|
|
46
|
+
if (!key.startsWith("ld_")) {
|
|
47
|
+
throw new CliError(
|
|
48
|
+
"Invalid API key format. Expected key starting with ld_.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const openUrl = (url: string): void => {
|
|
54
|
+
try {
|
|
55
|
+
if (process.platform === "darwin") {
|
|
56
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (process.platform === "win32") {
|
|
60
|
+
Bun.spawn(["cmd", "/c", "start", "", url], {
|
|
61
|
+
stdout: "ignore",
|
|
62
|
+
stderr: "ignore",
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
|
|
67
|
+
} catch {
|
|
68
|
+
// URL is still printed for manual opening.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CliError } from "@/lib/errors";
|
|
2
|
+
import type { HttpClient } from "@/lib/http";
|
|
3
|
+
|
|
4
|
+
export interface QueryCommandInput {
|
|
5
|
+
q: string;
|
|
6
|
+
page: number;
|
|
7
|
+
pageSize: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface QueryApiResponse {
|
|
11
|
+
items: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
merchantId: string | null;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
endpointBase: string;
|
|
17
|
+
authType: "none" | "api_key" | "bearer" | "basic" | "oauth2" | "custom";
|
|
18
|
+
defaultCostPerUse: number;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
openapiSpecUrl: string | null;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
matchScore?: number;
|
|
24
|
+
matchPercent?: number;
|
|
25
|
+
}>;
|
|
26
|
+
total: number;
|
|
27
|
+
page: number;
|
|
28
|
+
pageSize: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const runQueryCommand = async (
|
|
32
|
+
http: HttpClient,
|
|
33
|
+
apiKey: string,
|
|
34
|
+
input: QueryCommandInput,
|
|
35
|
+
): Promise<QueryApiResponse> => {
|
|
36
|
+
if (input.pageSize < 1 || input.pageSize > 3) {
|
|
37
|
+
throw new CliError("pageSize must be between 1 and 3.");
|
|
38
|
+
}
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
q: input.q,
|
|
41
|
+
page: String(input.page),
|
|
42
|
+
pageSize: String(input.pageSize),
|
|
43
|
+
});
|
|
44
|
+
return http.get<QueryApiResponse>(`/v1/query?${params.toString()}`, apiKey);
|
|
45
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { runAccountCommand } from "@/commands/account";
|
|
4
|
+
import { type PaymentRail, runActCommand } from "@/commands/act";
|
|
5
|
+
import { runLoginCommand } from "@/commands/login";
|
|
6
|
+
import { runQueryCommand } from "@/commands/query";
|
|
7
|
+
import { resolveApiKey } from "@/lib/auth";
|
|
8
|
+
import { CliError } from "@/lib/errors";
|
|
9
|
+
import { createHttpClient } from "@/lib/http";
|
|
10
|
+
import {
|
|
11
|
+
fail,
|
|
12
|
+
print,
|
|
13
|
+
printAccountResult,
|
|
14
|
+
printActResult,
|
|
15
|
+
printQueryResult,
|
|
16
|
+
} from "@/lib/output";
|
|
17
|
+
|
|
18
|
+
interface ParsedArgs {
|
|
19
|
+
command: "query" | "act" | "account" | "login";
|
|
20
|
+
options: Record<string, string | boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_API_BASE = "https://api.lidian.ai";
|
|
24
|
+
const GLOBAL_OPTIONS = new Set(["api-key", "api-base", "json", "help"]);
|
|
25
|
+
const COMMAND_OPTIONS: Record<ParsedArgs["command"], Set<string>> = {
|
|
26
|
+
query: new Set(["q", "page", "pageSize"]),
|
|
27
|
+
act: new Set(["endpoint-id", "params", "payment-rail", "network"]),
|
|
28
|
+
account: new Set([]),
|
|
29
|
+
login: new Set(["key"]),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const main = async (): Promise<void> => {
|
|
33
|
+
if (process.argv.length <= 2 || process.argv.includes("--help")) {
|
|
34
|
+
printUsage();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
38
|
+
const apiBase = String(
|
|
39
|
+
parsed.options["api-base"] ??
|
|
40
|
+
process.env.LIDIAN_API_BASE ??
|
|
41
|
+
DEFAULT_API_BASE,
|
|
42
|
+
);
|
|
43
|
+
const asJson = Boolean(parsed.options.json);
|
|
44
|
+
const http = createHttpClient(apiBase);
|
|
45
|
+
|
|
46
|
+
switch (parsed.command) {
|
|
47
|
+
case "login": {
|
|
48
|
+
const key = asString(parsed.options.key);
|
|
49
|
+
const result = await runLoginCommand({ key });
|
|
50
|
+
if (asJson) {
|
|
51
|
+
print(JSON.stringify({ success: true, data: result }, null, 2));
|
|
52
|
+
} else {
|
|
53
|
+
print(`Saved API key to ${result.path}`);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
case "query": {
|
|
58
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
59
|
+
const qValue = asString(parsed.options.q);
|
|
60
|
+
if (!qValue) {
|
|
61
|
+
throw new CliError("Missing --q for query command.");
|
|
62
|
+
}
|
|
63
|
+
const page = toInt(asString(parsed.options.page), 1);
|
|
64
|
+
const pageSize = toInt(asString(parsed.options.pageSize), 1);
|
|
65
|
+
const result = await runQueryCommand(http, apiKey, {
|
|
66
|
+
q: qValue,
|
|
67
|
+
page,
|
|
68
|
+
pageSize,
|
|
69
|
+
});
|
|
70
|
+
printQueryResult(result, asJson);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case "act": {
|
|
74
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
75
|
+
const endpointIdValue = asString(parsed.options["endpoint-id"]);
|
|
76
|
+
if (!endpointIdValue) {
|
|
77
|
+
throw new CliError("Missing --endpoint-id for act command.");
|
|
78
|
+
}
|
|
79
|
+
const paramsRaw = asString(parsed.options.params) ?? "{}";
|
|
80
|
+
const params = parseJsonObject(paramsRaw, "--params");
|
|
81
|
+
const paymentRail = asPaymentRail(
|
|
82
|
+
asString(parsed.options["payment-rail"]) ?? "prepaid_credits",
|
|
83
|
+
);
|
|
84
|
+
const network = asString(parsed.options.network);
|
|
85
|
+
const result = await runActCommand(http, apiKey, {
|
|
86
|
+
endpointId: endpointIdValue,
|
|
87
|
+
params,
|
|
88
|
+
paymentRail,
|
|
89
|
+
...(network ? { network } : {}),
|
|
90
|
+
});
|
|
91
|
+
printActResult(result, asJson);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
case "account": {
|
|
95
|
+
const apiKey = await resolveApiKey(asString(parsed.options["api-key"]));
|
|
96
|
+
const result = await runAccountCommand(http, apiKey);
|
|
97
|
+
printAccountResult(result, asJson);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
throw new CliError("Unknown command.", 1);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const parseArgs = (argv: string[]): ParsedArgs => {
|
|
106
|
+
const command = argv[0];
|
|
107
|
+
if (
|
|
108
|
+
command !== "query" &&
|
|
109
|
+
command !== "act" &&
|
|
110
|
+
command !== "account" &&
|
|
111
|
+
command !== "login"
|
|
112
|
+
) {
|
|
113
|
+
printUsage();
|
|
114
|
+
throw new CliError(
|
|
115
|
+
"Invalid command. Use one of: login, query, act, account.",
|
|
116
|
+
1,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const options: Record<string, string | boolean> = {};
|
|
121
|
+
let index = 1;
|
|
122
|
+
while (index < argv.length) {
|
|
123
|
+
const token = argv[index];
|
|
124
|
+
if (!token || !token.startsWith("--")) {
|
|
125
|
+
throw new CliError(`Unexpected argument: ${token ?? "<empty>"}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const key = token.slice(2);
|
|
129
|
+
const allowedForCommand = COMMAND_OPTIONS[command];
|
|
130
|
+
if (!GLOBAL_OPTIONS.has(key) && !allowedForCommand.has(key)) {
|
|
131
|
+
throw new CliError(`Unknown option for ${command}: --${key}`);
|
|
132
|
+
}
|
|
133
|
+
const next = argv[index + 1];
|
|
134
|
+
if (!next || next.startsWith("--")) {
|
|
135
|
+
options[key] = true;
|
|
136
|
+
index += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
options[key] = next;
|
|
141
|
+
index += 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
command,
|
|
146
|
+
options,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const printUsage = (): void => {
|
|
151
|
+
print("Usage:");
|
|
152
|
+
print(" lidian login [--key ld_...] [--json]");
|
|
153
|
+
print(
|
|
154
|
+
' lidian query --q "<term>" [--page 1] [--pageSize 1..3] [--api-key <key>] [--json]',
|
|
155
|
+
);
|
|
156
|
+
print(
|
|
157
|
+
" lidian act --endpoint-id <uuid> --params '<json>' [--payment-rail prepaid_credits|x402] [--api-key <key>] [--json]",
|
|
158
|
+
);
|
|
159
|
+
print(" [--network base|ethereum]");
|
|
160
|
+
print(" lidian account [--api-key <key>] [--json]");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const asString = (value: string | boolean | undefined): string | undefined => {
|
|
164
|
+
if (typeof value === "string") return value;
|
|
165
|
+
return undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const toInt = (value: string | undefined, fallback: number): number => {
|
|
169
|
+
if (!value) return fallback;
|
|
170
|
+
const parsed = Number.parseInt(value, 10);
|
|
171
|
+
if (Number.isNaN(parsed)) {
|
|
172
|
+
throw new CliError(`Invalid integer value: ${value}`);
|
|
173
|
+
}
|
|
174
|
+
return parsed;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const parseJsonObject = (
|
|
178
|
+
value: string,
|
|
179
|
+
flagName: string,
|
|
180
|
+
): Record<string, unknown> => {
|
|
181
|
+
const parsed = JSON.parse(value) as unknown;
|
|
182
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
183
|
+
throw new CliError(`${flagName} must be a JSON object.`);
|
|
184
|
+
}
|
|
185
|
+
return parsed as Record<string, unknown>;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const asPaymentRail = (value: string): PaymentRail => {
|
|
189
|
+
if (value === "prepaid_credits" || value === "x402") return value;
|
|
190
|
+
throw new CliError("Invalid --payment-rail. Use prepaid_credits or x402.");
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
main().catch(fail);
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readConfig } from "@/lib/config";
|
|
2
|
+
import { CliError } from "@/lib/errors";
|
|
3
|
+
|
|
4
|
+
export const resolveApiKey = async (
|
|
5
|
+
argsApiKey: string | undefined,
|
|
6
|
+
): Promise<string> => {
|
|
7
|
+
const config = await readConfig();
|
|
8
|
+
const key = argsApiKey ?? process.env.LIDIAN_API_KEY ?? config.apiKey;
|
|
9
|
+
if (!key) {
|
|
10
|
+
throw new CliError(
|
|
11
|
+
"Missing API key. Use `lidian login --key ld_...`, pass --api-key, or set LIDIAN_API_KEY.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return key;
|
|
15
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface LidianConfig {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CONFIG_PATH = join(homedir(), ".lidian", "config.json");
|
|
10
|
+
|
|
11
|
+
export const getConfigPath = (): string => CONFIG_PATH;
|
|
12
|
+
|
|
13
|
+
export const readConfig = async (): Promise<LidianConfig> => {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
17
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const apiKey =
|
|
21
|
+
"apiKey" in parsed && typeof parsed.apiKey === "string"
|
|
22
|
+
? parsed.apiKey
|
|
23
|
+
: undefined;
|
|
24
|
+
return { apiKey };
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const writeConfig = async (config: LidianConfig): Promise<void> => {
|
|
31
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
32
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
33
|
+
};
|
package/src/lib/http.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CliError } from "@/lib/errors";
|
|
2
|
+
|
|
3
|
+
export interface ApiSuccess<T> {
|
|
4
|
+
success: true;
|
|
5
|
+
data: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ApiFailure {
|
|
9
|
+
success: false;
|
|
10
|
+
error: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ApiResponse<T> = ApiSuccess<T> | ApiFailure;
|
|
17
|
+
|
|
18
|
+
export interface HttpClient {
|
|
19
|
+
get<T>(path: string, apiKey: string): Promise<T>;
|
|
20
|
+
post<T, B>(path: string, body: B, apiKey: string): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const createHttpClient = (baseUrl: string): HttpClient => {
|
|
24
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
async get<T>(path: string, apiKey: string): Promise<T> {
|
|
28
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
29
|
+
method: "GET",
|
|
30
|
+
headers: authHeaders(apiKey),
|
|
31
|
+
});
|
|
32
|
+
return handleResponse<T>(response);
|
|
33
|
+
},
|
|
34
|
+
async post<T, B>(path: string, body: B, apiKey: string): Promise<T> {
|
|
35
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
...authHeaders(apiKey),
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
return handleResponse<T>(response);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const authHeaders = (apiKey: string): HeadersInit => ({
|
|
49
|
+
Authorization: `Bearer ${apiKey}`,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handleResponse = async <T>(response: Response): Promise<T> => {
|
|
53
|
+
const json = (await response
|
|
54
|
+
.json()
|
|
55
|
+
.catch(() => null)) as ApiResponse<T> | null;
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
if (json && "success" in json && json.success === false) {
|
|
59
|
+
throw new CliError(`${json.error.code}: ${json.error.message}`, 1);
|
|
60
|
+
}
|
|
61
|
+
throw new CliError(`Request failed with status ${response.status}`, 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!json || !("success" in json) || json.success !== true) {
|
|
65
|
+
throw new CliError("Unexpected API response format", 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return json.data;
|
|
69
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { AccountApiResponse } from "@/commands/account";
|
|
2
|
+
import type { ActApiResponse, ActCommandResult } from "@/commands/act";
|
|
3
|
+
import type { QueryApiResponse } from "@/commands/query";
|
|
4
|
+
import { CliError } from "@/lib/errors";
|
|
5
|
+
|
|
6
|
+
export const print = (message: string): void => {
|
|
7
|
+
process.stdout.write(`${message}\n`);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const printError = (message: string): void => {
|
|
11
|
+
process.stderr.write(`${message}\n`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const printResult = (result: unknown, asJson: boolean): void => {
|
|
15
|
+
if (asJson) {
|
|
16
|
+
print(JSON.stringify(result, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
print(formatForHuman(result));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const printQueryResult = (
|
|
23
|
+
result: QueryApiResponse,
|
|
24
|
+
asJson: boolean,
|
|
25
|
+
): void => {
|
|
26
|
+
if (asJson) {
|
|
27
|
+
printResult(result, true);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (result.items.length === 0) {
|
|
31
|
+
print("No APIs found.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
print(
|
|
35
|
+
`Found ${result.items.length} of ${result.total} APIs (page ${result.page}).`,
|
|
36
|
+
);
|
|
37
|
+
for (const item of result.items) {
|
|
38
|
+
const confidence = item.matchPercent
|
|
39
|
+
? ` confidence=${item.matchPercent.toFixed(1)}%`
|
|
40
|
+
: "";
|
|
41
|
+
print(
|
|
42
|
+
`- ${item.name} (${item.id}) auth=${item.authType} cost=${item.defaultCostPerUse}c${confidence}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const printActResult = (
|
|
48
|
+
result: ActCommandResult,
|
|
49
|
+
asJson: boolean,
|
|
50
|
+
): void => {
|
|
51
|
+
if (asJson) {
|
|
52
|
+
printResult(result.execution, true);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (result.payment) {
|
|
56
|
+
print(
|
|
57
|
+
`x402 preflight: payTo=${result.payment.requirements.payTo} amount=${result.payment.requirements.amountFormatted} verified=${String(result.payment.verified)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
printExecutionResult(result.execution);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const printAccountResult = (
|
|
64
|
+
result: AccountApiResponse,
|
|
65
|
+
asJson: boolean,
|
|
66
|
+
): void => {
|
|
67
|
+
if (asJson) {
|
|
68
|
+
printResult(result, true);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
print(`Account: ${result.user.id}`);
|
|
72
|
+
print(`Balance: ${result.balance.balance} credits`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const printExecutionResult = (result: ActApiResponse): void => {
|
|
76
|
+
print(
|
|
77
|
+
`Execution succeeded. Spent=${result.credits.spent} balance=${result.credits.balance}`,
|
|
78
|
+
);
|
|
79
|
+
print(JSON.stringify(result.data, null, 2));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const formatForHuman = (value: unknown): string => {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(value, null, 2);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const fail = (error: unknown): never => {
|
|
90
|
+
if (error instanceof CliError) {
|
|
91
|
+
printError(`Error: ${error.message}`);
|
|
92
|
+
process.exit(error.code);
|
|
93
|
+
}
|
|
94
|
+
if (error instanceof Error) {
|
|
95
|
+
printError(`Error: ${error.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
printError("Error: Unknown failure");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
};
|
package/src/lib/x402.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { HttpClient } from "@/lib/http";
|
|
2
|
+
|
|
3
|
+
export interface PaymentRequirementsResponse {
|
|
4
|
+
paymentIntentId: string;
|
|
5
|
+
payTo: string;
|
|
6
|
+
amount: number;
|
|
7
|
+
amountFormatted: string;
|
|
8
|
+
network: string;
|
|
9
|
+
chainId: number;
|
|
10
|
+
asset: string;
|
|
11
|
+
expiresAt: string;
|
|
12
|
+
resource?: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
endpoint: {
|
|
16
|
+
id: string;
|
|
17
|
+
path: string;
|
|
18
|
+
method: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PaymentVerifyResponse {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
verifiedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const requestPaymentRequirements = async (
|
|
29
|
+
http: HttpClient,
|
|
30
|
+
apiKey: string,
|
|
31
|
+
endpointId: string,
|
|
32
|
+
network?: string,
|
|
33
|
+
): Promise<PaymentRequirementsResponse> => {
|
|
34
|
+
return http.post<
|
|
35
|
+
PaymentRequirementsResponse,
|
|
36
|
+
{ endpointId: string; network?: string }
|
|
37
|
+
>(
|
|
38
|
+
"/v1/payments/requirements",
|
|
39
|
+
{ endpointId, ...(network ? { network } : {}) },
|
|
40
|
+
apiKey,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const verifyPaymentAddress = async (
|
|
45
|
+
http: HttpClient,
|
|
46
|
+
apiKey: string,
|
|
47
|
+
payTo: string,
|
|
48
|
+
): Promise<PaymentVerifyResponse> => {
|
|
49
|
+
return http.post<PaymentVerifyResponse, { payTo: string }>(
|
|
50
|
+
"/v1/payments/verify",
|
|
51
|
+
{ payTo },
|
|
52
|
+
apiKey,
|
|
53
|
+
);
|
|
54
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["bun-types"],
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["src/*"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|