arketix 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/arketix.js +67 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,6 @@ arketix help crm opportunities
|
|
|
17
17
|
- **Flags come from each operation's input.** `ownerId` is `--owner-id`, enums are choices, objects and arrays take JSON, `--input '<json>'` passes the whole input. An operation with exactly one required id also takes it as one trailing word.
|
|
18
18
|
- **Output.** People get a table for a list and `key: value` lines otherwise. `--json` prints one line: the output, or `{"error":{"code","message","retryable"}}`.
|
|
19
19
|
- **Sign-in.** `login` keeps the token in the OS keychain. `ARKETIX_TOKEN` overrides it. `--env prod` (default) or `local` (http://localhost:3417); `ARKETIX_URL` points anywhere else.
|
|
20
|
-
- **Catalogue.**
|
|
20
|
+
- **Catalogue.** Every command, and help for one, asks the CRM for its list of operations. The list is cached per server version and CLI release in `~/.cache/arketix`, which only saves the `tools/list` call while the CRM's version is unchanged. Nothing falls back to it: when the CRM can't be reached or gives no usable answer, the command exits 9, and a refused token exits with its code. `arketix`, `arketix help` and the built-ins' help need no token and no connection, so they list only the built-ins; `arketix help crm` lists the CRM's commands.
|
|
21
21
|
|
|
22
22
|
Exit codes: 0 ok, 1 other CRM error, 2 usage, 3 unauthenticated, 4 read-only token, 6 confirmation required, 7 not in catalogue, 8 rate limited, 9 connection lost (a write may have run), 10 forbidden, 12 input invalid.
|
package/dist/arketix.js
CHANGED
|
@@ -17266,6 +17266,17 @@ function _array(Class, element, params) {
|
|
|
17266
17266
|
...normalizeParams(params)
|
|
17267
17267
|
});
|
|
17268
17268
|
}
|
|
17269
|
+
function _custom(Class, fn, _params) {
|
|
17270
|
+
const norm = normalizeParams(_params);
|
|
17271
|
+
norm.abort ?? (norm.abort = true);
|
|
17272
|
+
const schema = new Class({
|
|
17273
|
+
type: "custom",
|
|
17274
|
+
check: "custom",
|
|
17275
|
+
fn,
|
|
17276
|
+
...norm
|
|
17277
|
+
});
|
|
17278
|
+
return schema;
|
|
17279
|
+
}
|
|
17269
17280
|
function _refine(Class, fn, _params) {
|
|
17270
17281
|
const schema = new Class({
|
|
17271
17282
|
type: "custom",
|
|
@@ -19569,6 +19580,9 @@ var ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
|
|
|
19569
19580
|
ZodType.init(inst, def);
|
|
19570
19581
|
inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
|
|
19571
19582
|
});
|
|
19583
|
+
function custom(fn, _params) {
|
|
19584
|
+
return _custom(ZodCustom, fn ?? (() => true), _params);
|
|
19585
|
+
}
|
|
19572
19586
|
function refine(fn, _params = {}) {
|
|
19573
19587
|
return _refine(ZodCustom, fn, _params);
|
|
19574
19588
|
}
|
|
@@ -36485,12 +36499,13 @@ var fail9 = (code, message, retryable = false) => {
|
|
|
36485
36499
|
var exitCodes = EXIT_CODES;
|
|
36486
36500
|
var exitCodeFor = (code) => exitCodes[code] ?? 1;
|
|
36487
36501
|
// package.json
|
|
36488
|
-
var version2 = "0.1.
|
|
36502
|
+
var version2 = "0.1.1";
|
|
36489
36503
|
|
|
36490
36504
|
// src/version.ts
|
|
36491
36505
|
var VERSION = version2;
|
|
36492
36506
|
|
|
36493
36507
|
// src/crm.ts
|
|
36508
|
+
var Catalogue = object({ cli: literal(VERSION), server: string4(), tools: array2(custom()) });
|
|
36494
36509
|
var Output = record2(string4(), unknown2());
|
|
36495
36510
|
|
|
36496
36511
|
class ConnectionLost extends Error {
|
|
@@ -36508,10 +36523,12 @@ function noAnswer(url, error, read) {
|
|
|
36508
36523
|
retryable: false
|
|
36509
36524
|
};
|
|
36510
36525
|
}
|
|
36526
|
+
if (status === 403)
|
|
36527
|
+
return { code: "FORBIDDEN", message: `${url} refused this token access.`, retryable: false };
|
|
36511
36528
|
if (status === 429)
|
|
36512
36529
|
return { code: "RATE_LIMITED", message: `${url} is rate limiting this token. Wait and retry.`, retryable: true };
|
|
36513
36530
|
const lost = error instanceof ConnectionLost || error instanceof Error && error.cause instanceof ConnectionLost;
|
|
36514
|
-
const what = lost ? `The connection to ${url} failed` : `${url} gave no answer (${error instanceof Error ? error.message : String(error)})`;
|
|
36531
|
+
const what = lost ? `The connection to ${url} failed` : `${url} gave no usable answer (${error instanceof Error ? error.message : String(error)})`;
|
|
36515
36532
|
return read ? { code: "CONNECTION_LOST", message: `${what}. Nothing changed: retry.`, retryable: true } : { code: "CONNECTION_LOST", message: `${what}, so the call may have run. Check the current state before trying again.`, retryable: false };
|
|
36516
36533
|
}
|
|
36517
36534
|
function toolError(text) {
|
|
@@ -36535,40 +36552,41 @@ async function connect({ url, token, fetch: fetch2, catalogues }) {
|
|
|
36535
36552
|
throw new ConnectionLost(error instanceof Error ? error.message : String(error));
|
|
36536
36553
|
}
|
|
36537
36554
|
},
|
|
36538
|
-
requestInit: { headers: { authorization: `Bearer ${token}
|
|
36555
|
+
requestInit: { headers: { ...token && { authorization: `Bearer ${token}` }, "x-arketix-channel": "cli" } }
|
|
36539
36556
|
});
|
|
36540
36557
|
const answered = async (request, read) => {
|
|
36541
36558
|
try {
|
|
36542
36559
|
return await request;
|
|
36543
36560
|
} catch (error) {
|
|
36544
|
-
|
|
36561
|
+
const failure = noAnswer(url, error, read);
|
|
36562
|
+
if (!token && failure.code === "UNAUTHENTICATED")
|
|
36563
|
+
notSignedIn(url);
|
|
36564
|
+
throw new CliFailure(failure);
|
|
36545
36565
|
}
|
|
36546
36566
|
};
|
|
36547
36567
|
await answered(client.connect(transport), true);
|
|
36548
|
-
const
|
|
36568
|
+
const server = client.getServerVersion()?.version ?? "";
|
|
36549
36569
|
return {
|
|
36550
36570
|
catalogue: async () => {
|
|
36551
36571
|
const all = await catalogues.read();
|
|
36552
|
-
const cached = all[url];
|
|
36553
|
-
if (cached
|
|
36572
|
+
const cached = Catalogue.safeParse(all[url]).data;
|
|
36573
|
+
if (cached?.server === server)
|
|
36554
36574
|
return cached;
|
|
36555
|
-
const fresh = {
|
|
36575
|
+
const fresh = { cli: VERSION, server, tools: (await answered(client.listTools(), true)).tools };
|
|
36556
36576
|
await catalogues.write({ ...all, [url]: fresh });
|
|
36557
36577
|
return fresh;
|
|
36558
36578
|
},
|
|
36559
36579
|
call: async (name, args, { read }) => {
|
|
36560
|
-
const
|
|
36580
|
+
const unusable = (why) => new CliFailure(noAnswer(url, new Error(why), read));
|
|
36581
|
+
const result = await answered(client.callTool({ name, arguments: args }).catch((error) => {
|
|
36582
|
+
throw error instanceof ProtocolError ? unusable(error.message) : error;
|
|
36583
|
+
}), read);
|
|
36561
36584
|
if (result.isError)
|
|
36562
36585
|
throw new CliFailure(toolError(textOf(result.content)));
|
|
36563
|
-
|
|
36564
|
-
|
|
36565
|
-
|
|
36566
|
-
|
|
36567
|
-
const value = JSON.parse(text);
|
|
36568
|
-
return Output.safeParse(value).data ?? { value };
|
|
36569
|
-
} catch {
|
|
36570
|
-
return { text };
|
|
36571
|
-
}
|
|
36586
|
+
const output = Output.safeParse(result.structuredContent);
|
|
36587
|
+
if (!output.success)
|
|
36588
|
+
throw unusable(`${name}: no structured content object`);
|
|
36589
|
+
return output.data;
|
|
36572
36590
|
},
|
|
36573
36591
|
close: () => client.close()
|
|
36574
36592
|
};
|
|
@@ -36606,16 +36624,21 @@ function human(output, columns = Infinity) {
|
|
|
36606
36624
|
// src/cli.ts
|
|
36607
36625
|
var URLS = { prod: "https://crm.admin.acredix.cl", local: "http://localhost:3417" };
|
|
36608
36626
|
var isEnv = (value) => (value in URLS);
|
|
36609
|
-
function
|
|
36610
|
-
|
|
36611
|
-
|
|
36612
|
-
|
|
36613
|
-
|
|
36614
|
-
|
|
36615
|
-
|
|
36616
|
-
|
|
36617
|
-
|
|
36618
|
-
|
|
36627
|
+
function preParse(argv) {
|
|
36628
|
+
let env;
|
|
36629
|
+
const words = [];
|
|
36630
|
+
for (let index = 0;index < argv.length; index++) {
|
|
36631
|
+
const token = argv[index];
|
|
36632
|
+
if (token === "--env")
|
|
36633
|
+
env = argv[++index];
|
|
36634
|
+
else if (token.startsWith("--env="))
|
|
36635
|
+
env = token.slice("--env=".length);
|
|
36636
|
+
else if (!token.startsWith("-"))
|
|
36637
|
+
words.push(token);
|
|
36638
|
+
}
|
|
36639
|
+
return { env, words };
|
|
36640
|
+
}
|
|
36641
|
+
function targetOf(env, io) {
|
|
36619
36642
|
if (!isEnv(env))
|
|
36620
36643
|
return fail9("USAGE", "--env is prod or local; ARKETIX_URL points anywhere else.");
|
|
36621
36644
|
const url = new URL(io.env.ARKETIX_URL ?? URLS[env]).origin;
|
|
@@ -36623,7 +36646,6 @@ function targetOf(argv, io) {
|
|
|
36623
36646
|
}
|
|
36624
36647
|
var opIdOf = (tool) => tool.name.replaceAll("_", ".");
|
|
36625
36648
|
var kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
36626
|
-
var operationsOf = (tools) => tools.filter((tool) => !tool.description?.startsWith("Deprecated:"));
|
|
36627
36649
|
var Property = looseObject({
|
|
36628
36650
|
type: union2([string4(), array2(string4())]).optional(),
|
|
36629
36651
|
anyOf: array2(looseObject({ type: string4().optional() })).optional(),
|
|
@@ -36656,7 +36678,7 @@ var InputSchema = object({ properties: record2(string4(), Property).optional(),
|
|
|
36656
36678
|
var print = (run, value, text) => run.io.stdout(`${run.json ? JSON.stringify(value) : text}
|
|
36657
36679
|
`);
|
|
36658
36680
|
var needCrm = (run) => run.crm ?? notSignedIn(run.url);
|
|
36659
|
-
var operation = (run, id) =>
|
|
36681
|
+
var operation = (run, id) => run.catalogue.find((tool) => opIdOf(tool) === id) ?? fail9("NOT_IN_CATALOGUE", `${id} is not an operation of ${run.url}. \`arketix help\` lists them.`);
|
|
36660
36682
|
async function callOperation(run, tool, input, confirmed) {
|
|
36661
36683
|
if (tool.annotations?.destructiveHint && !confirmed) {
|
|
36662
36684
|
fail9("CONFIRMATION_REQUIRED", `${opIdOf(tool)} deletes or cancels. Nothing changed: repeat the command with --confirm to go ahead.`);
|
|
@@ -36695,7 +36717,7 @@ function operationCommand(run, tool, word) {
|
|
|
36695
36717
|
}
|
|
36696
36718
|
function operationCommands(run) {
|
|
36697
36719
|
const root = { children: new Map };
|
|
36698
|
-
for (const tool of
|
|
36720
|
+
for (const tool of run.catalogue) {
|
|
36699
36721
|
let node = root;
|
|
36700
36722
|
for (const word of opIdOf(tool).split(".").map(kebab)) {
|
|
36701
36723
|
const next = node.children.get(word) ?? { children: new Map };
|
|
@@ -36757,28 +36779,27 @@ function builtIns(run) {
|
|
|
36757
36779
|
}
|
|
36758
36780
|
var readInputFile = (io, path) => io.readFile(path).catch(() => fail9("USAGE", `Can't read ${path}.`));
|
|
36759
36781
|
var BUILT_IN_ONLY = new Set(["login", "logout", "version"]);
|
|
36782
|
+
var BUILT_INS = new Set([...BUILT_IN_ONLY, "whoami", "help", "call"]);
|
|
36760
36783
|
async function runCli(args, io) {
|
|
36761
36784
|
const json = args.includes("--json");
|
|
36762
|
-
const
|
|
36763
|
-
const
|
|
36785
|
+
const { env = "prod", words } = preParse(args);
|
|
36786
|
+
const helpWord = words[0] === "help";
|
|
36787
|
+
const argv = helpWord ? [...args.toSpliced(args.indexOf("help"), 1), "--help"] : [...args];
|
|
36788
|
+
const command = words[helpWord ? 1 : 0];
|
|
36789
|
+
const wantsHelp = command === undefined || argv.includes("--help") || argv.includes("-h");
|
|
36790
|
+
const builtInHelp = wantsHelp && (command === undefined || BUILT_INS.has(command));
|
|
36764
36791
|
let crm;
|
|
36765
36792
|
try {
|
|
36766
|
-
const { url, token } = targetOf(
|
|
36767
|
-
let catalogue =
|
|
36768
|
-
if (!BUILT_IN_ONLY.has(
|
|
36769
|
-
if (token)
|
|
36770
|
-
try {
|
|
36771
|
-
crm = await connect({ url, token, fetch: io.fetch, catalogues: io.catalogues });
|
|
36772
|
-
catalogue = (await crm.catalogue()).tools;
|
|
36773
|
-
} catch (error) {
|
|
36774
|
-
if (!wantsHelp)
|
|
36775
|
-
throw error;
|
|
36776
|
-
}
|
|
36777
|
-
} else if (!wantsHelp)
|
|
36793
|
+
const { url, token } = targetOf(env, io);
|
|
36794
|
+
let catalogue = [];
|
|
36795
|
+
if (!builtInHelp && !BUILT_IN_ONLY.has(command ?? "") && !argv.includes("--version")) {
|
|
36796
|
+
if (!token && !wantsHelp)
|
|
36778
36797
|
notSignedIn(url);
|
|
36798
|
+
crm = await connect({ url, token, fetch: io.fetch, catalogues: io.catalogues });
|
|
36799
|
+
catalogue = (await crm.catalogue()).tools;
|
|
36779
36800
|
}
|
|
36780
36801
|
const run = { io, json, url, crm, catalogue };
|
|
36781
|
-
const cli = make12("arketix").pipe(withDescription4(`The Arketix control room (${url}) for people and agents. Commands come from the CRM's operations
|
|
36802
|
+
const cli = make12("arketix").pipe(withDescription4(`The Arketix control room (${url}) for people and agents. Commands come from the CRM's operations: ` + "`arketix help crm`, `arketix help metrics`, `arketix help settings`, `arketix help account`. " + "Exit codes: 0 ok, 1 other CRM error, 2 usage, 3 unauthenticated, 4 read-only token, 6 confirmation required, " + "7 not in catalogue, 8 rate limited, 9 connection lost, 10 forbidden, 12 input invalid."), withSubcommands([...builtIns(run), ...operationCommands(run)]), withGlobalFlags([JsonFlag, EnvFlag]));
|
|
36782
36803
|
const exit = await runPromiseExit2(runWith2(cli, { version: VERSION, renderErrors: false })(argv).pipe(provide3(services(io, json))));
|
|
36783
36804
|
if (isSuccess3(exit))
|
|
36784
36805
|
return 0;
|