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 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.** The list of operations is cached per server version in `~/.cache/arketix`, so help works offline.
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.0";
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}`, "x-arketix-channel": "cli" } }
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
- throw new CliFailure(noAnswer(url, error, read));
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 version = client.getServerVersion()?.version ?? "";
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 && cached.version === version)
36572
+ const cached = Catalogue.safeParse(all[url]).data;
36573
+ if (cached?.server === server)
36554
36574
  return cached;
36555
- const fresh = { version, tools: (await answered(client.listTools(), true)).tools };
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 result = await answered(client.callTool({ name, arguments: args }), read);
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
- if (result.structuredContent)
36564
- return Output.parse(result.structuredContent);
36565
- const text = textOf(result.content);
36566
- try {
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 flagValue(argv, name) {
36610
- for (const [index, token] of argv.entries()) {
36611
- if (token === `--${name}`)
36612
- return argv[index + 1];
36613
- if (token.startsWith(`--${name}=`))
36614
- return token.slice(name.length + 3);
36615
- }
36616
- }
36617
- function targetOf(argv, io) {
36618
- const env = flagValue(argv, "env") ?? "prod";
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) => operationsOf(run.catalogue).find((tool) => opIdOf(tool) === id) ?? fail9("NOT_IN_CATALOGUE", `${id} is not an operation of ${run.url}. \`arketix help\` lists them.`);
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 operationsOf(run.catalogue)) {
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 argv = args[0] === "help" ? [...args.slice(1), "--help"] : [...args];
36763
- const wantsHelp = argv.length === 0 || argv.includes("--help") || argv.includes("-h");
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(argv, io);
36767
- let catalogue = (await io.catalogues.read())[url]?.tools ?? [];
36768
- if (!BUILT_IN_ONLY.has(argv[0] ?? "") && !argv.includes("--version")) {
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. ` + "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]));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arketix",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Command line for the Arketix control room: every CRM operation, for founders and agents.",
5
5
  "private": false,
6
6
  "type": "module",