@retasc/cli 1.4.0 → 1.5.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/dist/api.js CHANGED
@@ -33,11 +33,49 @@ function client() {
33
33
  c.setAuth(cfg.token);
34
34
  return c;
35
35
  }
36
+ /**
37
+ * Read a caught backend error into the parts a command wants to show (RTSC-261).
38
+ *
39
+ * `code` is machine-readable, so a command branches on `code === "EXPIRED"`
40
+ * instead of regex-matching prose that any copy edit would break. `hint` is the
41
+ * next step, kept separate so callers can dim or indent it.
42
+ *
43
+ * Both are optional because the sweep is incremental: a backend site still
44
+ * throwing a plain Error (or an OLD deployment this CLI is pointed at) yields
45
+ * `{message}` alone via the same first-line/strip-prefix cleanup used before.
46
+ * Callers must therefore keep any existing string fallback rather than assuming
47
+ * `code` is present.
48
+ */
49
+ export function formatError(e) {
50
+ const data = e?.data;
51
+ if (data && typeof data === "object" && typeof data.message === "string") {
52
+ // Type-guard EVERY field, not just `message`: a foreign error carrying
53
+ // `{message: "x", code: {…}}` would otherwise print `✗ [object Object]: x`.
54
+ // And strip control characters — these strings go to `console.error`, so a
55
+ // future converted site that interpolates user data (issue ids, org names)
56
+ // must not be able to smuggle ANSI escapes into the terminal. Mirrors
57
+ // `sanitize` in convex/lib/userError.ts; the two are one wire contract.
58
+ const clean = (v) =>
59
+ // eslint-disable-next-line no-control-regex
60
+ typeof v === "string" ? v.replace(/[\x00-\x1f\x7f]/g, " ").trim() : undefined;
61
+ return { code: clean(data.code), message: clean(data.message), hint: clean(data.hint) };
62
+ }
63
+ const message = String(e?.message ?? e)
64
+ .split("\n")[0]
65
+ .replace(/^.*?Uncaught Error:\s*/, "")
66
+ .trim();
67
+ return { message };
68
+ }
36
69
  // Does this error mean "the access token is missing/expired", i.e. a refresh
37
70
  // might fix it? The access-token JWT lives ~1h, so any long-lived login trips
38
71
  // this. Two shapes surface: our server functions throw `UNAUTHENTICATED …`
39
72
  // (requireUser), and the Convex platform rejects a stale JWT with "Could not
40
73
  // verify OIDC token"/"Unauthenticated". Match either, case-insensitively.
74
+ // RTSC-261 deliberately does NOT touch this. It gates the refresh-and-retry
75
+ // loop, no site converted here throws an auth code, and `formatError` truncates
76
+ // to the first line — so routing it through there would add real risk (a missed
77
+ // match means a spurious device-flow re-login) for no gain today. It gets a
78
+ // `code === "UNAUTHENTICATED"` fast path in the PR that converts lib/auth.ts.
41
79
  export function isAuthError(e) {
42
80
  const msg = String(e?.message ?? e);
43
81
  return /unauthenticated|could not verify oidc|oidc token/i.test(msg);
@@ -68,7 +68,7 @@ export async function billingAction(opts) {
68
68
  const c = charges;
69
69
  console.log(`\nBilling — ${orgLabel}\n`);
70
70
  console.log("SUBSCRIPTION");
71
- row("Status", s.billingExempt ? "comp — not billed" : `${s.status}${s.gated ? " (gated)" : ""}`);
71
+ row("Status", `${s.status}${s.gated ? " (gated)" : ""}`);
72
72
  row("Subscription", s.subscriptionId ?? "none yet");
73
73
  if (s.spendingCapUsd != null) {
74
74
  row("Budget", `${usd(s.spendingCapUsd)}${s.capPeriod ? ` / ${s.capPeriod.replace("per_", "")}` : ""}`);
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { readLocalBinding, resolveBinding } from "./lib/binding.js";
14
14
  import { tidyAction, doneAction } from "./commands/tidy.js";
15
15
  import { runProxy } from "./proxy.js";
16
16
  import { deviceLogin } from "./auth.js";
17
- import { api } from "./api.js";
17
+ import { api, formatError } from "./api.js";
18
18
  // Single source of truth for the version: read package.json at runtime from the
19
19
  // compiled file's location (dist/index.js -> ../package.json). A JSON import won't
20
20
  // work here — tsconfig has rootDir "src", so importing ../package.json is outside
@@ -31,9 +31,15 @@ function requireLogin() {
31
31
  process.exit(1);
32
32
  }
33
33
  }
34
+ // RTSC-261: prefer the structured payload, and print `hint` on its own dimmed
35
+ // line — the whole reason it's a separate field is that the fix shouldn't be
36
+ // buried in the middle of the diagnosis. `formatError` falls back to the old
37
+ // string cleanup, so unconverted backend sites print exactly as they did.
34
38
  function fail(e) {
35
- const msg = String(e?.message ?? e).split("\n")[0].replace(/^.*?Uncaught Error:\s*/, "");
36
- console.error(`✗ ${msg}`);
39
+ const { code, message, hint } = formatError(e);
40
+ console.error(`✗ ${code ? `${code}: ` : ""}${message}`);
41
+ if (hint)
42
+ console.error(` → ${hint}`);
37
43
  process.exit(1);
38
44
  }
39
45
  // --- auth ------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Retasc CLI — sign in with GitHub, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {