@beel_es/cli 0.1.0 → 0.1.2
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 +12 -1
- package/dist/index.js +166 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,10 +36,21 @@ npx @beel_es/cli invoices export-excel --output invoices.xlsx
|
|
|
36
36
|
npx @beel_es/cli --live invoices list # production
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`--data` accepts inline JSON, `@file.json`, or `-` for stdin. Binary responses (PDF, ZIP, Excel) require `--output <path>`.
|
|
39
|
+
`--data` accepts inline JSON, `@file.json`, or `-` for stdin. It is only **required** for endpoints whose request body is mandatory (e.g. `invoices create`); for lifecycle actions with an optional body (e.g. `invoices mark-paid`) it is optional, and verbs with no body (e.g. `invoices issue`) don't expose it at all. Each command's `--help` shows whether `--data` is required or optional. Binary responses (PDF, ZIP, Excel) require `--output <path>`.
|
|
40
40
|
|
|
41
41
|
Discover everything with `--help` at any level: `beel --help`, `beel invoices --help`, `beel invoices list --help` (flags, enums and defaults come from the API spec).
|
|
42
42
|
|
|
43
|
+
### Docs search (no API key needed)
|
|
44
|
+
|
|
45
|
+
Search [docs.beel.es](https://docs.beel.es) locally — fetches `llms-full.txt` once (cached 15 min) and prints only the matching sections. An agent gets the relevant ~2KB instead of the full 660KB docs:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @beel_es/cli docs list # all pages (JSON)
|
|
49
|
+
npx @beel_es/cli docs search idempotency key # top matching sections (markdown)
|
|
50
|
+
npx @beel_es/cli docs search rate limit --limit 5
|
|
51
|
+
npx @beel_es/cli docs get glossary # one full page
|
|
52
|
+
```
|
|
53
|
+
|
|
43
54
|
### Generic escape hatch
|
|
44
55
|
|
|
45
56
|
Any endpoint, even ones this CLI version doesn't know yet:
|
package/dist/index.js
CHANGED
|
@@ -10797,7 +10797,7 @@ var require_package = __commonJS({
|
|
|
10797
10797
|
"package.json"(exports, module) {
|
|
10798
10798
|
module.exports = {
|
|
10799
10799
|
name: "@beel_es/cli",
|
|
10800
|
-
version: "0.1.
|
|
10800
|
+
version: "0.1.2",
|
|
10801
10801
|
description: "Agent-first CLI for the BeeL invoicing API. Commands are derived at startup from the embedded OpenAPI spec. Run with npx @beel_es/cli \u2014 no install needed.",
|
|
10802
10802
|
license: "MIT",
|
|
10803
10803
|
type: "module",
|
|
@@ -10964,7 +10964,22 @@ function printJson(data) {
|
|
|
10964
10964
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
10965
10965
|
`);
|
|
10966
10966
|
}
|
|
10967
|
+
var INFO_CODES = /* @__PURE__ */ new Set(["commander.help", "commander.helpDisplayed", "commander.version"]);
|
|
10968
|
+
function commanderErrorToContract(error) {
|
|
10969
|
+
if (INFO_CODES.has(error.code) || error.exitCode === 0) {
|
|
10970
|
+
return { contract: null, exit: error.exitCode };
|
|
10971
|
+
}
|
|
10972
|
+
return { contract: { code: "USAGE_ERROR", message: stripErrorPrefix(error.message) }, exit: EXIT.USAGE };
|
|
10973
|
+
}
|
|
10974
|
+
function stripErrorPrefix(message) {
|
|
10975
|
+
return message.replace(/^error:\s*/i, "");
|
|
10976
|
+
}
|
|
10967
10977
|
function fail(error) {
|
|
10978
|
+
if (error instanceof CommanderError) {
|
|
10979
|
+
const { contract, exit } = commanderErrorToContract(error);
|
|
10980
|
+
if (contract) writeError({ code: contract.code, message: contract.message });
|
|
10981
|
+
process.exit(exit);
|
|
10982
|
+
}
|
|
10968
10983
|
if (error instanceof UsageError) {
|
|
10969
10984
|
writeError({ code: "USAGE_ERROR", message: error.message });
|
|
10970
10985
|
process.exit(EXIT.USAGE);
|
|
@@ -11081,10 +11096,11 @@ function buildQueryParams(doc, rawParams) {
|
|
|
11081
11096
|
function deriveBody(doc, operation) {
|
|
11082
11097
|
const requestBody = resolveRef(doc, operation.requestBody);
|
|
11083
11098
|
const content = requestBody?.content;
|
|
11084
|
-
if (!content) return null;
|
|
11085
|
-
|
|
11086
|
-
if ("
|
|
11087
|
-
return
|
|
11099
|
+
if (!content) return { kind: null, required: false };
|
|
11100
|
+
const required = requestBody?.required === true;
|
|
11101
|
+
if ("application/json" in content) return { kind: "json", required };
|
|
11102
|
+
if ("multipart/form-data" in content) return { kind: "multipart", required };
|
|
11103
|
+
return { kind: null, required: false };
|
|
11088
11104
|
}
|
|
11089
11105
|
function deriveBinaryResponse(doc, operation) {
|
|
11090
11106
|
const responses = operation.responses ?? {};
|
|
@@ -11107,6 +11123,7 @@ function buildManifest(doc) {
|
|
|
11107
11123
|
const operation = pathItem[method];
|
|
11108
11124
|
if (!operation || typeof operation.operationId !== "string") continue;
|
|
11109
11125
|
const group = deriveGroup(path);
|
|
11126
|
+
const body = deriveBody(doc, operation);
|
|
11110
11127
|
commands.push({
|
|
11111
11128
|
group,
|
|
11112
11129
|
action: deriveAction(group, operation.operationId, method, path),
|
|
@@ -11116,7 +11133,8 @@ function buildManifest(doc) {
|
|
|
11116
11133
|
operationId: operation.operationId,
|
|
11117
11134
|
pathParams: extractPathParams(path),
|
|
11118
11135
|
queryParams: buildQueryParams(doc, [...sharedParams, ...operation.parameters ?? []]),
|
|
11119
|
-
body:
|
|
11136
|
+
body: body.kind,
|
|
11137
|
+
bodyRequired: body.required,
|
|
11120
11138
|
binaryResponse: deriveBinaryResponse(doc, operation)
|
|
11121
11139
|
});
|
|
11122
11140
|
}
|
|
@@ -11260,7 +11278,10 @@ function registerCommand(group, spec) {
|
|
|
11260
11278
|
else sub.option(flag, description);
|
|
11261
11279
|
}
|
|
11262
11280
|
if (spec.body === "json") {
|
|
11263
|
-
|
|
11281
|
+
const hint = spec.bodyRequired ? "JSON body (required)" : "JSON body (optional)";
|
|
11282
|
+
const description = `${hint}: inline, @file.json, or - for stdin`;
|
|
11283
|
+
if (spec.bodyRequired) sub.requiredOption("--data <json>", description);
|
|
11284
|
+
else sub.option("--data <json>", description);
|
|
11264
11285
|
}
|
|
11265
11286
|
if (spec.binaryResponse) {
|
|
11266
11287
|
sub.requiredOption("--output <path>", "Write the binary response to this file");
|
|
@@ -11372,14 +11393,152 @@ function registerRequest(program3) {
|
|
|
11372
11393
|
});
|
|
11373
11394
|
}
|
|
11374
11395
|
|
|
11396
|
+
// src/docs/fetch.ts
|
|
11397
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, statSync, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
11398
|
+
import { tmpdir } from "os";
|
|
11399
|
+
import { join as join2 } from "path";
|
|
11400
|
+
var CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
11401
|
+
function docsBaseUrl(env = process.env) {
|
|
11402
|
+
return env.BEEL_DOCS_URL ?? "https://docs.beel.es";
|
|
11403
|
+
}
|
|
11404
|
+
async function fetchDocsFile(file, env = process.env) {
|
|
11405
|
+
const cacheDir = join2(tmpdir(), "beel-cli-docs-cache");
|
|
11406
|
+
const cachePath = join2(cacheDir, file);
|
|
11407
|
+
if (existsSync2(cachePath) && Date.now() - statSync(cachePath).mtimeMs < CACHE_TTL_MS) {
|
|
11408
|
+
return readFileSync3(cachePath, "utf8");
|
|
11409
|
+
}
|
|
11410
|
+
const url = `${docsBaseUrl(env)}/${file}`;
|
|
11411
|
+
const response = await fetch(url);
|
|
11412
|
+
if (!response.ok) {
|
|
11413
|
+
throw new UsageError(`Cannot fetch docs from ${url} (HTTP ${response.status})`);
|
|
11414
|
+
}
|
|
11415
|
+
const text = await response.text();
|
|
11416
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
11417
|
+
writeFileSync4(cachePath, text);
|
|
11418
|
+
return text;
|
|
11419
|
+
}
|
|
11420
|
+
|
|
11421
|
+
// src/docs/parse.ts
|
|
11422
|
+
function splitChunks(fullText) {
|
|
11423
|
+
const chunks = [];
|
|
11424
|
+
let page = "";
|
|
11425
|
+
let heading = null;
|
|
11426
|
+
let buffer = [];
|
|
11427
|
+
let inFence = false;
|
|
11428
|
+
const flush = () => {
|
|
11429
|
+
const content = buffer.join("\n").trim();
|
|
11430
|
+
if (page && content) chunks.push({ page, heading, content });
|
|
11431
|
+
buffer = [];
|
|
11432
|
+
};
|
|
11433
|
+
for (const line of fullText.split("\n")) {
|
|
11434
|
+
if (line.trimStart().startsWith("```")) {
|
|
11435
|
+
inFence = !inFence;
|
|
11436
|
+
buffer.push(line);
|
|
11437
|
+
continue;
|
|
11438
|
+
}
|
|
11439
|
+
if (!inFence && line.startsWith("# ")) {
|
|
11440
|
+
flush();
|
|
11441
|
+
page = line.slice(2).trim();
|
|
11442
|
+
heading = null;
|
|
11443
|
+
continue;
|
|
11444
|
+
}
|
|
11445
|
+
if (!inFence && line.startsWith("## ")) {
|
|
11446
|
+
flush();
|
|
11447
|
+
heading = line.slice(3).trim();
|
|
11448
|
+
continue;
|
|
11449
|
+
}
|
|
11450
|
+
buffer.push(line);
|
|
11451
|
+
}
|
|
11452
|
+
flush();
|
|
11453
|
+
return chunks;
|
|
11454
|
+
}
|
|
11455
|
+
function parseIndex(indexText) {
|
|
11456
|
+
const pages = [];
|
|
11457
|
+
for (const line of indexText.split("\n")) {
|
|
11458
|
+
const match = line.match(/^-\s*\[([^\]]+)\]\((\S+)\)\s*-?\s*(.*)$/);
|
|
11459
|
+
if (match) {
|
|
11460
|
+
pages.push({ title: match[1], url: match[2], description: match[3].trim() });
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
11463
|
+
return pages;
|
|
11464
|
+
}
|
|
11465
|
+
function scoreChunk(chunk, terms) {
|
|
11466
|
+
const haystack = chunk.content.toLowerCase();
|
|
11467
|
+
const titleHaystack = `${chunk.page} ${chunk.heading ?? ""}`.toLowerCase();
|
|
11468
|
+
let score = 0;
|
|
11469
|
+
let termsHit = 0;
|
|
11470
|
+
for (const term of terms) {
|
|
11471
|
+
const t = term.toLowerCase();
|
|
11472
|
+
const contentHits = haystack.split(t).length - 1;
|
|
11473
|
+
const titleHits = titleHaystack.split(t).length - 1;
|
|
11474
|
+
if (contentHits + titleHits > 0) termsHit++;
|
|
11475
|
+
score += contentHits + titleHits * 5;
|
|
11476
|
+
}
|
|
11477
|
+
if (termsHit === terms.length && terms.length > 1) score *= 2;
|
|
11478
|
+
return termsHit === 0 ? 0 : score;
|
|
11479
|
+
}
|
|
11480
|
+
function searchChunks(chunks, terms, limit) {
|
|
11481
|
+
return chunks.map((chunk) => ({ chunk, score: scoreChunk(chunk, terms) })).filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.chunk);
|
|
11482
|
+
}
|
|
11483
|
+
function findPage(chunks, titleQuery) {
|
|
11484
|
+
const q = titleQuery.toLowerCase();
|
|
11485
|
+
const titles = [...new Set(chunks.map((c) => c.page))];
|
|
11486
|
+
const exact = titles.find((t) => t.toLowerCase() === q);
|
|
11487
|
+
const partial = titles.filter((t) => t.toLowerCase().includes(q));
|
|
11488
|
+
const title = exact ?? (partial.length === 1 ? partial[0] : void 0);
|
|
11489
|
+
if (!title) return [];
|
|
11490
|
+
return chunks.filter((c) => c.page === title);
|
|
11491
|
+
}
|
|
11492
|
+
|
|
11493
|
+
// src/commands/docs.ts
|
|
11494
|
+
function registerDocs(program3) {
|
|
11495
|
+
const docs = program3.command("docs").description(
|
|
11496
|
+
"Search the BeeL API docs. Downloads docs.beel.es/llms-full.txt once (cached 15 min in tmpdir), then filters locally \u2014 search terms never leave your machine. No API key needed."
|
|
11497
|
+
);
|
|
11498
|
+
docs.command("list").description("List all documentation pages").action(async () => {
|
|
11499
|
+
const index = await fetchDocsFile("llms.txt");
|
|
11500
|
+
printJson(parseIndex(index));
|
|
11501
|
+
});
|
|
11502
|
+
docs.command("search").description("Search the docs; prints only the matching sections as markdown").argument("<terms...>", "Search terms").option("--limit <n>", "Max sections to print", Number, 3).action(async (terms, opts) => {
|
|
11503
|
+
const full = await fetchDocsFile("llms-full.txt");
|
|
11504
|
+
const results = searchChunks(splitChunks(full), terms, opts.limit);
|
|
11505
|
+
if (results.length === 0) {
|
|
11506
|
+
throw new UsageError(`No docs sections match: ${terms.join(" ")}. Try 'beel docs list' to see available pages.`);
|
|
11507
|
+
}
|
|
11508
|
+
printChunks(results);
|
|
11509
|
+
});
|
|
11510
|
+
docs.command("get").description("Print a full documentation page as markdown").argument("<title...>", 'Page title (or unambiguous part of it), e.g. "idempotency"').action(async (titleParts) => {
|
|
11511
|
+
const full = await fetchDocsFile("llms-full.txt");
|
|
11512
|
+
const chunks = findPage(splitChunks(full), titleParts.join(" "));
|
|
11513
|
+
if (chunks.length === 0) {
|
|
11514
|
+
throw new UsageError(`No page matches '${titleParts.join(" ")}' unambiguously. Run 'beel docs list' to see titles.`);
|
|
11515
|
+
}
|
|
11516
|
+
printChunks(chunks);
|
|
11517
|
+
});
|
|
11518
|
+
}
|
|
11519
|
+
function printChunks(chunks) {
|
|
11520
|
+
for (const chunk of chunks) {
|
|
11521
|
+
const location = chunk.heading ? `${chunk.page} \u203A ${chunk.heading}` : chunk.page;
|
|
11522
|
+
process.stdout.write(`
|
|
11523
|
+
\u2501\u2501\u2501 ${location} \u2501\u2501\u2501
|
|
11524
|
+
|
|
11525
|
+
${chunk.content}
|
|
11526
|
+
`);
|
|
11527
|
+
}
|
|
11528
|
+
}
|
|
11529
|
+
|
|
11375
11530
|
// src/index.ts
|
|
11376
11531
|
var { version } = require_package();
|
|
11377
11532
|
var program2 = new Command("beel").description(
|
|
11378
11533
|
"BeeL invoicing API CLI. JSON output on stdout, errors on stderr. Sandbox by default; pass --live for production."
|
|
11379
11534
|
).version(version).option("--live", "Use the live (production) API key instead of the test one");
|
|
11535
|
+
program2.exitOverride();
|
|
11536
|
+
program2.configureOutput({ outputError: () => {
|
|
11537
|
+
} });
|
|
11380
11538
|
registerLogin(program2);
|
|
11381
11539
|
registerConfig(program2);
|
|
11382
11540
|
registerRequest(program2);
|
|
11541
|
+
registerDocs(program2);
|
|
11383
11542
|
try {
|
|
11384
11543
|
registerSpecCommands(program2, (0, import_yaml.parse)(public_api_default));
|
|
11385
11544
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beel_es/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Agent-first CLI for the BeeL invoicing API. Commands are derived at startup from the embedded OpenAPI spec. Run with npx @beel_es/cli — no install needed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|