@2kw/ai 5.2.0-dev.6 → 5.2.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.
@@ -2,16 +2,41 @@ import Conf from "conf";
2
2
  import type { Command } from "commander";
3
3
  export interface ContextEntry {
4
4
  baseUrl: string;
5
- apiKey: string;
5
+ /** Present on API-key contexts (CI/scripts and manual logins). */
6
+ apiKey?: string;
7
+ /** Present on session contexts created by the browser device-flow login. */
8
+ sessionToken?: string;
9
+ /** Auth service base URL for this context (e.g. https://auth.2kw.ai). */
10
+ authUrl?: string;
11
+ /** Active organization chosen at login / via set-org. */
12
+ organizationId?: string;
13
+ /** Short-lived org JWT cache — avoids one auth round-trip between rapid commands. */
14
+ cachedJwt?: string;
15
+ /** Epoch millis when cachedJwt expires. */
16
+ cachedJwtExp?: number;
6
17
  }
7
18
  export interface BackboneConfigStore {
8
19
  activeContext: string;
9
20
  contexts: Record<string, ContextEntry>;
10
21
  }
11
- export interface BackboneConfig {
22
+ /**
23
+ * What a command actually authenticates with. Either a raw API key, or a
24
+ * browser session that a later step exchanges for a short-lived org JWT.
25
+ */
26
+ export type ResolvedConfig = {
27
+ kind: "apiKey";
12
28
  apiKey: string;
13
29
  baseUrl: string;
14
- }
30
+ } | {
31
+ kind: "session";
32
+ contextName: string;
33
+ sessionToken: string;
34
+ authUrl: string;
35
+ baseUrl: string;
36
+ organizationId?: string;
37
+ cachedJwt?: string;
38
+ cachedJwtExp?: number;
39
+ };
15
40
  /**
16
41
  * Fallback base URL when the user has no context, env var, or local config.
17
42
  * Override at runtime by setting AI_2KW_DEFAULT_BASE_URL (or the legacy
@@ -19,6 +44,12 @@ export interface BackboneConfig {
19
44
  * deployment.
20
45
  */
21
46
  export declare const DEFAULT_BASE_URL: string;
47
+ /**
48
+ * Fallback auth-service URL used when the API host is not one we recognise.
49
+ * Override at runtime with AI_2KW_DEFAULT_AUTH_URL.
50
+ */
51
+ export declare const DEFAULT_AUTH_URL: string;
52
+ export declare function defaultAuthUrlFor(baseUrl: string): string;
22
53
  declare const store: Conf<BackboneConfigStore>;
23
54
  export { store };
24
55
  export declare function validateContextName(name: string): void;
@@ -27,18 +58,46 @@ export declare function getActiveContext(): ContextEntry | undefined;
27
58
  export declare function getAllContexts(): Record<string, ContextEntry>;
28
59
  export declare function getContextCount(): number;
29
60
  export declare function setContext(name: string, entry: ContextEntry): void;
61
+ /**
62
+ * Merge a partial update into an existing context, leaving unmentioned fields
63
+ * intact.
64
+ *
65
+ * Note the spread semantics: a key present in `patch` with the value
66
+ * `undefined` CLEARS that field rather than being ignored. That is deliberate
67
+ * and relied upon — e.g. `updateContext(name, { cachedJwt: undefined,
68
+ * cachedJwtExp: undefined })` drops a stale cached JWT.
69
+ */
70
+ export declare function updateContext(name: string, patch: Partial<ContextEntry>): void;
30
71
  export declare function deleteContext(name: string): void;
31
72
  export declare function renameContext(oldName: string, newName: string): void;
32
73
  export declare function setActiveContext(name: string): void;
74
+ /**
75
+ * Every input resolution considers, made explicit so the precedence rules can
76
+ * be exercised without touching the store, the environment, or the cwd.
77
+ *
78
+ * Flags, env vars, and the local file are API-key-only by design: session
79
+ * credentials are issued by the browser login and only ever live in the store.
80
+ */
81
+ export interface ConfigSources {
82
+ flagApiKey?: string;
83
+ flagBaseUrl?: string;
84
+ envApiKey?: string;
85
+ envBaseUrl?: string;
86
+ localApiKey?: string;
87
+ localBaseUrl?: string;
88
+ activeContextName: string;
89
+ activeContext?: ContextEntry;
90
+ }
91
+ export declare function resolveConfigFromSources(s: ConfigSources): ResolvedConfig;
33
92
  /**
34
93
  * Resolve configuration with priority:
35
94
  * 1. CLI flags (--api-key, --base-url)
36
95
  * 2. Environment variables (AI_2KW_API_KEY then legacy BACKBONE_API_KEY,
37
96
  * same for AI_2KW_BASE_URL / BACKBONE_BASE_URL)
38
97
  * 3. Local .2kw file (or legacy .backbone)
39
- * 4. Active context from config store
98
+ * 4. Active context from config store (API key or browser session)
40
99
  */
41
- export declare function resolveConfig(command: Command): BackboneConfig;
100
+ export declare function resolveConfig(command: Command): ResolvedConfig;
42
101
  /** Check if --json flag is set on root command */
43
102
  export declare function isJsonOutput(command: Command): boolean;
44
103
  //# sourceMappingURL=config.d.ts.map
@@ -10,6 +10,32 @@ import { resolve } from "node:path";
10
10
  export const DEFAULT_BASE_URL = process.env.AI_2KW_DEFAULT_BASE_URL ??
11
11
  process.env.BACKBONE_DEFAULT_BASE_URL ??
12
12
  "https://backbone.manfred-kunze.dev/api";
13
+ /**
14
+ * Fallback auth-service URL used when the API host is not one we recognise.
15
+ * Override at runtime with AI_2KW_DEFAULT_AUTH_URL.
16
+ */
17
+ export const DEFAULT_AUTH_URL = process.env.AI_2KW_DEFAULT_AUTH_URL ?? "https://auth.2kw.ai";
18
+ /** Known API-host → auth-service mappings for interactive login defaults. */
19
+ const AUTH_URL_BY_API_HOST = {
20
+ "api.2kw.ai": "https://auth.2kw.ai",
21
+ "api-dev.2kw.ai": "https://auth-dev.2kw.ai",
22
+ "backbone.manfred-kunze.dev": "https://auth.2kw.ai",
23
+ "localhost:8080": "http://localhost:3001",
24
+ "127.0.0.1:8080": "http://localhost:3001",
25
+ };
26
+ export function defaultAuthUrlFor(baseUrl) {
27
+ try {
28
+ const host = new URL(baseUrl).host;
29
+ // hasOwn, not a bare index: a host like "constructor" or "toString" would
30
+ // otherwise resolve through Object.prototype and yield a bogus auth URL.
31
+ return Object.hasOwn(AUTH_URL_BY_API_HOST, host)
32
+ ? AUTH_URL_BY_API_HOST[host]
33
+ : DEFAULT_AUTH_URL;
34
+ }
35
+ catch {
36
+ return DEFAULT_AUTH_URL;
37
+ }
38
+ }
13
39
  // One-shot flag so we only print the deprecation warning once per process,
14
40
  // even if resolveConfig is called multiple times across commands.
15
41
  let legacyEnvWarned = false;
@@ -30,9 +56,17 @@ function warnLegacyEnv(legacyName, canonicalName) {
30
56
  * fully self-contained.
31
57
  */
32
58
  const CONFIG_DIR_OVERRIDE = process.env.AI_2KW_CONFIG_DIR;
59
+ /**
60
+ * Owner-only, like an ssh private key: this file holds live credentials — an
61
+ * API key, or a session token and the org JWT minted from it. conf defaults to
62
+ * 0o666, which on any shared POSIX box makes every account on it a reader.
63
+ * Windows largely ignores mode bits, where this is simply inert.
64
+ */
65
+ const CONFIG_FILE_MODE = 0o600;
33
66
  const store = new Conf({
34
67
  projectName: "2kw",
35
68
  projectSuffix: "",
69
+ configFileMode: CONFIG_FILE_MODE,
36
70
  defaults: {
37
71
  activeContext: "default",
38
72
  contexts: {},
@@ -56,6 +90,9 @@ const store = new Conf({
56
90
  const legacy = new Conf({
57
91
  projectName: "backbone",
58
92
  projectSuffix: "",
93
+ // Same credentials, same restriction — this instance only computes a path
94
+ // today, but it must not be the one that widens a file if it ever writes.
95
+ configFileMode: CONFIG_FILE_MODE,
59
96
  defaults: { activeContext: "default", contexts: {} },
60
97
  ...(CONFIG_DIR_OVERRIDE ? { cwd: CONFIG_DIR_OVERRIDE } : {}),
61
98
  });
@@ -117,6 +154,24 @@ export function setContext(name, entry) {
117
154
  contexts[name] = entry;
118
155
  store.set("contexts", contexts);
119
156
  }
157
+ /**
158
+ * Merge a partial update into an existing context, leaving unmentioned fields
159
+ * intact.
160
+ *
161
+ * Note the spread semantics: a key present in `patch` with the value
162
+ * `undefined` CLEARS that field rather than being ignored. That is deliberate
163
+ * and relied upon — e.g. `updateContext(name, { cachedJwt: undefined,
164
+ * cachedJwtExp: undefined })` drops a stale cached JWT.
165
+ */
166
+ export function updateContext(name, patch) {
167
+ const contexts = getAllContexts();
168
+ const existing = contexts[name];
169
+ if (!existing) {
170
+ throw new Error(`Context "${name}" does not exist.`);
171
+ }
172
+ contexts[name] = { ...existing, ...patch };
173
+ store.set("contexts", contexts);
174
+ }
120
175
  export function deleteContext(name) {
121
176
  const contexts = getAllContexts();
122
177
  if (!contexts[name]) {
@@ -186,32 +241,67 @@ function readEnv(canonical, legacy) {
186
241
  }
187
242
  return undefined;
188
243
  }
244
+ const NO_AUTH_ERROR = 'No credentials configured. Run "2kw auth login" (browser sign-in), ' +
245
+ '"2kw auth login --api-key <key>", or set AI_2KW_API_KEY.';
246
+ export function resolveConfigFromSources(s) {
247
+ const baseUrl = s.flagBaseUrl ??
248
+ s.envBaseUrl ??
249
+ s.localBaseUrl ??
250
+ s.activeContext?.baseUrl ??
251
+ DEFAULT_BASE_URL;
252
+ // Explicit API keys (flag > env > local file) always win — they are the
253
+ // CI/script escape hatch and must never be shadowed by a stored session.
254
+ const explicitApiKey = s.flagApiKey ?? s.envApiKey ?? s.localApiKey;
255
+ if (explicitApiKey) {
256
+ return { kind: "apiKey", apiKey: explicitApiKey, baseUrl };
257
+ }
258
+ const ctx = s.activeContext;
259
+ if (ctx?.sessionToken) {
260
+ // A session without an auth URL cannot be exchanged for a JWT — treat it
261
+ // as unusable rather than failing later with an opaque request error.
262
+ if (!ctx.authUrl)
263
+ throw new Error(NO_AUTH_ERROR);
264
+ return {
265
+ kind: "session",
266
+ contextName: s.activeContextName,
267
+ sessionToken: ctx.sessionToken,
268
+ authUrl: ctx.authUrl,
269
+ baseUrl,
270
+ organizationId: ctx.organizationId,
271
+ cachedJwt: ctx.cachedJwt,
272
+ cachedJwtExp: ctx.cachedJwtExp,
273
+ };
274
+ }
275
+ if (ctx?.apiKey) {
276
+ return { kind: "apiKey", apiKey: ctx.apiKey, baseUrl };
277
+ }
278
+ throw new Error(NO_AUTH_ERROR);
279
+ }
189
280
  /**
190
281
  * Resolve configuration with priority:
191
282
  * 1. CLI flags (--api-key, --base-url)
192
283
  * 2. Environment variables (AI_2KW_API_KEY then legacy BACKBONE_API_KEY,
193
284
  * same for AI_2KW_BASE_URL / BACKBONE_BASE_URL)
194
285
  * 3. Local .2kw file (or legacy .backbone)
195
- * 4. Active context from config store
286
+ * 4. Active context from config store (API key or browser session)
196
287
  */
197
288
  export function resolveConfig(command) {
198
289
  const root = getRootCommand(command);
199
290
  const opts = root.opts();
200
291
  const local = readLocalFile();
201
- const activeCtx = getActiveContext();
202
- const apiKey = opts.apiKey ??
203
- readEnv("AI_2KW_API_KEY", "BACKBONE_API_KEY") ??
204
- local.apiKey ??
205
- activeCtx?.apiKey;
206
- const baseUrl = opts.baseUrl ??
207
- readEnv("AI_2KW_BASE_URL", "BACKBONE_BASE_URL") ??
208
- local.baseUrl ??
209
- activeCtx?.baseUrl ??
210
- DEFAULT_BASE_URL;
211
- if (!apiKey) {
212
- throw new Error('No API key configured. Run "2kw auth login" (or "backbone auth login") or set AI_2KW_API_KEY.');
213
- }
214
- return { apiKey, baseUrl };
292
+ // readEnv now runs even when a flag overrides it, so the BACKBONE_* legacy
293
+ // deprecation warning can fire in cases the old short-circuit chain silenced.
294
+ // Accepted: the variable IS set and IS deprecated, flag or no flag.
295
+ return resolveConfigFromSources({
296
+ flagApiKey: opts.apiKey,
297
+ flagBaseUrl: opts.baseUrl,
298
+ envApiKey: readEnv("AI_2KW_API_KEY", "BACKBONE_API_KEY"),
299
+ envBaseUrl: readEnv("AI_2KW_BASE_URL", "BACKBONE_BASE_URL"),
300
+ localApiKey: local.apiKey,
301
+ localBaseUrl: local.baseUrl,
302
+ activeContextName: getActiveContextName(),
303
+ activeContext: getActiveContext(),
304
+ });
215
305
  }
216
306
  /** Walk up Commander's parent chain to find root program */
217
307
  function getRootCommand(cmd) {
@@ -10,5 +10,23 @@ export declare class BackboneApiError extends Error {
10
10
  readonly timestamp: string;
11
11
  constructor(body: ApiErrorBody);
12
12
  }
13
+ /**
14
+ * Discriminated cause of a failed credential check. Lets callers give a
15
+ * precise message instead of guessing, because `2kw auth status` otherwise
16
+ * collapses every failure into one string.
17
+ */
18
+ export type AuthFailureKind = "UNAUTHORIZED" | "FORBIDDEN" | "UNREACHABLE" | "UNKNOWN";
19
+ /**
20
+ * Classify why a credential check failed, from the thrown error alone.
21
+ *
22
+ * - An HTTP rejection arrives as {@link BackboneApiError} (the API) or
23
+ * {@link AuthServiceError} (the auth service): 401 → UNAUTHORIZED,
24
+ * 403 → FORBIDDEN, anything else → UNKNOWN.
25
+ * - A network fault (fetch throws before any response) → UNREACHABLE, detected
26
+ * by undici's `fetch failed` TypeError or a known network error code on the
27
+ * error or its `cause`.
28
+ * - Everything else → UNKNOWN.
29
+ */
30
+ export declare function classifyAuthFailure(err: unknown): AuthFailureKind;
13
31
  export declare function handleError(err: unknown, json: boolean): void;
14
32
  //# sourceMappingURL=errors.d.ts.map
@@ -1,4 +1,7 @@
1
1
  import chalk from "chalk";
2
+ import { AuthServiceError } from "./auth-service.js";
3
+ import { SessionExpiredError } from "./auth-session.js";
4
+ import { getActiveContext, getActiveContextName, updateContext } from "./config.js";
2
5
  export class BackboneApiError extends Error {
3
6
  status;
4
7
  errorType;
@@ -11,8 +14,62 @@ export class BackboneApiError extends Error {
11
14
  this.timestamp = body.timestamp;
12
15
  }
13
16
  }
17
+ // Node/undici surface network faults through the error's `code`. A DNS or
18
+ // connection failure means the host was never reached — distinct from the
19
+ // API rejecting the key.
20
+ const NETWORK_ERROR_CODES = new Set([
21
+ "ENOTFOUND",
22
+ "ECONNREFUSED",
23
+ "EAI_AGAIN",
24
+ "ETIMEDOUT",
25
+ "ECONNRESET",
26
+ "ENETUNREACH",
27
+ "EHOSTUNREACH",
28
+ ]);
29
+ /** Shared status → kind mapping: the two rejection types agree on what a 401
30
+ * and a 403 mean, and differ only in who sent them. */
31
+ function classifyStatus(status) {
32
+ if (status === 401)
33
+ return "UNAUTHORIZED";
34
+ if (status === 403)
35
+ return "FORBIDDEN";
36
+ return "UNKNOWN";
37
+ }
38
+ /**
39
+ * Classify why a credential check failed, from the thrown error alone.
40
+ *
41
+ * - An HTTP rejection arrives as {@link BackboneApiError} (the API) or
42
+ * {@link AuthServiceError} (the auth service): 401 → UNAUTHORIZED,
43
+ * 403 → FORBIDDEN, anything else → UNKNOWN.
44
+ * - A network fault (fetch throws before any response) → UNREACHABLE, detected
45
+ * by undici's `fetch failed` TypeError or a known network error code on the
46
+ * error or its `cause`.
47
+ * - Everything else → UNKNOWN.
48
+ */
49
+ export function classifyAuthFailure(err) {
50
+ if (err instanceof BackboneApiError)
51
+ return classifyStatus(err.status);
52
+ if (err instanceof AuthServiceError)
53
+ return classifyStatus(err.status);
54
+ if (isNetworkError(err))
55
+ return "UNREACHABLE";
56
+ return "UNKNOWN";
57
+ }
58
+ function isNetworkError(err) {
59
+ if (!(err instanceof Error))
60
+ return false;
61
+ // undici throws `TypeError: fetch failed` when the request never completes.
62
+ if (err instanceof TypeError && /fetch failed/i.test(err.message))
63
+ return true;
64
+ const code = err.code;
65
+ if (typeof code === "string" && NETWORK_ERROR_CODES.has(code))
66
+ return true;
67
+ const cause = err.cause;
68
+ const causeCode = cause?.code;
69
+ return typeof causeCode === "string" && NETWORK_ERROR_CODES.has(causeCode);
70
+ }
14
71
  const HINTS = {
15
- 401: 'Invalid or missing API key. Run "backbone auth login" to configure credentials.',
72
+ 401: 'Invalid or missing API key. Run "2kw auth login" to configure credentials.',
16
73
  402: "Billing limit reached. Check your plan limits or upgrade at the dashboard.",
17
74
  403: "You don't have permission for this action. Check your organization role.",
18
75
  404: "Resource not found. Verify the ID is correct.",
@@ -20,8 +77,39 @@ const HINTS = {
20
77
  422: "Validation error. Check the input values.",
21
78
  429: "Rate limit exceeded. Wait a moment and try again.",
22
79
  };
80
+ /**
81
+ * Drop the cached org JWT of the active session context.
82
+ *
83
+ * Called when the session behind that JWT turns out to be dead: keeping the
84
+ * token would only make the next command spend a request discovering the same
85
+ * 401. Best-effort by design — a read-only store is not worth a second error on
86
+ * top of the one being reported.
87
+ */
88
+ function clearCachedJwt() {
89
+ try {
90
+ const ctx = getActiveContext();
91
+ // API-key contexts have no session and no cache to clear.
92
+ if (!ctx?.sessionToken)
93
+ return;
94
+ updateContext(getActiveContextName(), { cachedJwt: undefined, cachedJwtExp: undefined });
95
+ }
96
+ catch {
97
+ // Nothing actionable: the message below is still the one the user needs.
98
+ }
99
+ }
23
100
  export function handleError(err, json) {
24
- if (err instanceof BackboneApiError) {
101
+ if (err instanceof SessionExpiredError) {
102
+ clearCachedJwt();
103
+ // Reached by every command once the stored session dies, so it gets the
104
+ // one line that names the fix — never a stack trace.
105
+ if (json) {
106
+ console.error(JSON.stringify({ error: "session_expired", message: err.message }));
107
+ }
108
+ else {
109
+ console.error(chalk.yellow(err.message));
110
+ }
111
+ }
112
+ else if (err instanceof BackboneApiError) {
25
113
  if (json) {
26
114
  console.error(JSON.stringify({
27
115
  error: err.errorType,
@@ -40,7 +128,21 @@ export function handleError(err, json) {
40
128
  }
41
129
  else if (err instanceof Error) {
42
130
  if (json) {
43
- console.error(JSON.stringify({ error: err.name, message: err.message }));
131
+ // Errors from outside the typed client (AuthServiceError, undici network
132
+ // faults) carry their own status/code. Passing them through is what lets
133
+ // a scripted caller tell an "invalid_client" apart from a dead DNS name
134
+ // instead of string-matching the message.
135
+ const status = err.status;
136
+ // undici hangs the real code off `cause`, so read both — same pair
137
+ // isNetworkError checks above.
138
+ const code = err.code ??
139
+ err.cause?.code;
140
+ console.error(JSON.stringify({
141
+ error: err.name,
142
+ message: err.message,
143
+ ...(typeof status === "number" ? { status } : {}),
144
+ ...(typeof code === "string" ? { code } : {}),
145
+ }));
44
146
  }
45
147
  else {
46
148
  console.error(chalk.red(`Error: ${err.message}`));
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The one place that decides how a stored credential is allowed to appear —
3
+ * on screen, and in `--json` output.
4
+ *
5
+ * Every surface that prints a {@link ContextEntry} goes through here (`context
6
+ * list`, `context current`, `config list`), because a secret that reaches a
7
+ * terminal also reaches scrollback, CI logs, and pasted bug reports. Keeping
8
+ * the rules next to the shape they redact is what stops the next surface from
9
+ * re-deriving them and forgetting a field.
10
+ */
11
+ import type { ContextEntry } from "./config.js";
12
+ /**
13
+ * Enough of an API key to recognise which one it is, never enough to use it.
14
+ * A key too short to keep both ends of is blanked outright rather than
15
+ * printed with its middle "hidden".
16
+ */
17
+ export declare function maskApiKey(key: string | undefined): string;
18
+ /**
19
+ * Session tokens are opaque and long; the leading characters are enough to
20
+ * tell two apart in a bug report, and are useless on their own.
21
+ */
22
+ export declare function maskSessionToken(token: string): string;
23
+ /** The credential column of `context list` / `context current`, either kind. */
24
+ export declare function credentialSummary(ctx: ContextEntry): string;
25
+ /**
26
+ * A {@link ContextEntry} safe to serialize.
27
+ *
28
+ * The shape is otherwise the stored one so scripts reading `context list
29
+ * --json` keep working: `apiKey` and `sessionToken` are masked in place,
30
+ * `cachedJwt` is dropped entirely (it is a bearer token for the API and has no
31
+ * identifying value at all), and `cachedJwtExp` stays — knowing when the cache
32
+ * turns over is useful and expiries are not secret.
33
+ */
34
+ export declare function redactContext(ctx: ContextEntry): Record<string, unknown>;
35
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The one place that decides how a stored credential is allowed to appear —
3
+ * on screen, and in `--json` output.
4
+ *
5
+ * Every surface that prints a {@link ContextEntry} goes through here (`context
6
+ * list`, `context current`, `config list`), because a secret that reaches a
7
+ * terminal also reaches scrollback, CI logs, and pasted bug reports. Keeping
8
+ * the rules next to the shape they redact is what stops the next surface from
9
+ * re-deriving them and forgetting a field.
10
+ */
11
+ /**
12
+ * Enough of an API key to recognise which one it is, never enough to use it.
13
+ * A key too short to keep both ends of is blanked outright rather than
14
+ * printed with its middle "hidden".
15
+ */
16
+ export function maskApiKey(key) {
17
+ if (!key)
18
+ return "-";
19
+ if (key.length <= 11)
20
+ return "****";
21
+ return key.slice(0, 7) + "..." + key.slice(-4);
22
+ }
23
+ /**
24
+ * Session tokens are opaque and long; the leading characters are enough to
25
+ * tell two apart in a bug report, and are useless on their own.
26
+ */
27
+ export function maskSessionToken(token) {
28
+ return token.length <= 8 ? "****" : token.slice(0, 8) + "...";
29
+ }
30
+ /** The credential column of `context list` / `context current`, either kind. */
31
+ export function credentialSummary(ctx) {
32
+ if (ctx.sessionToken)
33
+ return `session ${maskSessionToken(ctx.sessionToken)}`;
34
+ return maskApiKey(ctx.apiKey);
35
+ }
36
+ /**
37
+ * A {@link ContextEntry} safe to serialize.
38
+ *
39
+ * The shape is otherwise the stored one so scripts reading `context list
40
+ * --json` keep working: `apiKey` and `sessionToken` are masked in place,
41
+ * `cachedJwt` is dropped entirely (it is a bearer token for the API and has no
42
+ * identifying value at all), and `cachedJwtExp` stays — knowing when the cache
43
+ * turns over is useful and expiries are not secret.
44
+ */
45
+ export function redactContext(ctx) {
46
+ const { apiKey, sessionToken, cachedJwt, ...rest } = ctx;
47
+ void cachedJwt; // destructured only to keep it out of `rest`.
48
+ return {
49
+ ...rest,
50
+ ...(apiKey !== undefined ? { apiKey: maskApiKey(apiKey) } : {}),
51
+ ...(sessionToken !== undefined ? { sessionToken: maskSessionToken(sessionToken) } : {}),
52
+ };
53
+ }
54
+ //# sourceMappingURL=redact.js.map
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
4
  import chalk from "chalk";
5
- const PKG_NAME = "@manfred-kunze-dev/backbone-cli";
5
+ const PKG_NAME = "@2kw/ai";
6
6
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
7
7
  const CACHE_DIR = join(homedir(), ".config", "backbone");
8
8
  const CACHE_FILE = join(CACHE_DIR, "update-check.json");
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "5.2.0-dev.6",
4
- "description": "CLI for the 2kw.ai platform (engineering name: Backbone)",
3
+ "version": "5.2.0",
4
+ "description": "CLI for 2kw.ai schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
+ "keywords": [
6
+ "cli",
7
+ "llm-gateway",
8
+ "ai-gateway",
9
+ "document-extraction",
10
+ "structured-data-extraction",
11
+ "pdf-extraction",
12
+ "openai-compatible",
13
+ "transcription",
14
+ "prompt-management",
15
+ "llm-evaluation",
16
+ "llm-observability",
17
+ "agentic-workflows",
18
+ "gdpr",
19
+ "eu-data-residency",
20
+ "2kw",
21
+ "backbone"
22
+ ],
5
23
  "type": "module",
6
24
  "main": "dist/index.js",
7
25
  "bin": {
@@ -28,6 +46,7 @@
28
46
  "cli-table3": "^0.6.5",
29
47
  "commander": "^13.1.0",
30
48
  "conf": "^13.1.0",
49
+ "open": "^10.2.0",
31
50
  "openapi-fetch": "^0.13.5",
32
51
  "ora": "^8.2.0"
33
52
  },