@tabbio-technologies/cli 1.2.8

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.
@@ -0,0 +1,2019 @@
1
+ import { createRequire as __tabbioCreateRequire } from 'node:module';
2
+ const require = __tabbioCreateRequire(import.meta.url);
3
+ import {
4
+ activeCommandSignal
5
+ } from "./chunk-NK5RPNSV.js";
6
+ import {
7
+ WEB_PAGE_PALETTE,
8
+ theme
9
+ } from "./chunk-VHHZFMIF.js";
10
+
11
+ // src/core/errors.ts
12
+ var ExitCode = {
13
+ Ok: 0,
14
+ Error: 1,
15
+ Usage: 2,
16
+ Auth: 3,
17
+ Forbidden: 4,
18
+ NotFound: 5,
19
+ ApprovalPending: 6,
20
+ Network: 7,
21
+ Server: 8,
22
+ /** SIGINT / Ctrl-C (128 + 2). */
23
+ Interrupted: 130
24
+ };
25
+ var EXIT_CODE_DOCS = [
26
+ { code: ExitCode.Ok, name: "Ok", meaning: "Success" },
27
+ { code: ExitCode.Error, name: "Error", meaning: "General failure (including a failed tool)" },
28
+ { code: ExitCode.Usage, name: "Usage", meaning: "Bad flags or input; missing required fields" },
29
+ { code: ExitCode.Auth, name: "Auth", meaning: "Not signed in, session expired or token rejected" },
30
+ { code: ExitCode.Forbidden, name: "Forbidden", meaning: "Not allowed (token scope, plan or permission)" },
31
+ { code: ExitCode.NotFound, name: "NotFound", meaning: "The tool or resource does not exist" },
32
+ { code: ExitCode.ApprovalPending, name: "ApprovalPending", meaning: "The action is waiting for approval" },
33
+ { code: ExitCode.Network, name: "Network", meaning: "Could not reach Tabbio, or timed out" },
34
+ { code: ExitCode.Server, name: "Server", meaning: "Tabbio failed on its side (5xx)" },
35
+ { code: ExitCode.Interrupted, name: "Interrupted", meaning: "Cancelled with Ctrl-C" }
36
+ ];
37
+ var CliError = class extends Error {
38
+ code;
39
+ hint;
40
+ exitCode;
41
+ requestId;
42
+ status;
43
+ retry;
44
+ constructor(opts) {
45
+ super(opts.message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
46
+ this.name = "CliError";
47
+ this.code = opts.code;
48
+ this.hint = opts.hint;
49
+ this.exitCode = opts.exitCode;
50
+ this.requestId = opts.requestId;
51
+ this.status = opts.status;
52
+ this.retry = opts.retry ?? (opts.exitCode === ExitCode.Network || opts.exitCode === ExitCode.Server || opts.code.toUpperCase() === "RATE_LIMIT_EXCEEDED");
53
+ }
54
+ /** Shape printed by `--json` error output. Never includes the cause. */
55
+ toJSON() {
56
+ return {
57
+ code: this.code,
58
+ message: this.message,
59
+ ...this.hint ? { hint: this.hint } : {},
60
+ exitCode: this.exitCode,
61
+ retry: this.retry,
62
+ ...this.requestId ? { requestId: this.requestId } : {},
63
+ ...this.status ? { status: this.status } : {}
64
+ };
65
+ }
66
+ };
67
+ function isCliError(value) {
68
+ return value instanceof CliError;
69
+ }
70
+ var AUTH_CODES = /* @__PURE__ */ new Set([
71
+ "UNAUTHORIZED",
72
+ "UNAUTHENTICATED",
73
+ "AUTH_REQUIRED",
74
+ "SESSION_REUSE_DETECTED",
75
+ "SESSION_EXPIRED",
76
+ "INVALID_TOKEN",
77
+ "TOKEN_EXPIRED",
78
+ "NOT_SIGNED_IN"
79
+ ]);
80
+ var FORBIDDEN_CODES = /* @__PURE__ */ new Set([
81
+ "FORBIDDEN",
82
+ "MCP_SCOPE_FORBIDDEN",
83
+ "INSUFFICIENT_SCOPE",
84
+ "SUBSCRIPTION_ENTITLEMENT_DENIED",
85
+ "UPGRADE_REQUIRED",
86
+ "PAYMENT_REQUIRED",
87
+ "ENTITLEMENT_REQUIRED",
88
+ "QUOTA_EXCEEDED"
89
+ ]);
90
+ var USAGE_CODES = /* @__PURE__ */ new Set(["VALIDATION_ERROR", "BAD_REQUEST", "INVALID_INPUT", "USAGE"]);
91
+ var APPROVAL_CODES = /* @__PURE__ */ new Set(["APPROVAL_PENDING", "APPROVAL_REQUIRED"]);
92
+ var NETWORK_CODES = /* @__PURE__ */ new Set(["NETWORK_ERROR", "TIMEOUT", "REQUEST_ABORTED"]);
93
+ var INTERRUPT_CODES = /* @__PURE__ */ new Set(["CANCELLED", "INTERRUPTED"]);
94
+ var SERVER_CODES = /* @__PURE__ */ new Set([
95
+ "INTERNAL_ERROR",
96
+ "SERVICE_UNAVAILABLE",
97
+ "DATABASE_UNAVAILABLE",
98
+ "BAD_RESPONSE",
99
+ "MCP_INTEGRATION_NOT_READY"
100
+ ]);
101
+ function exitCodeForErrorCode(code, status) {
102
+ const normalized = code.trim().toUpperCase();
103
+ if (AUTH_CODES.has(normalized)) return ExitCode.Auth;
104
+ if (FORBIDDEN_CODES.has(normalized)) return ExitCode.Forbidden;
105
+ if (normalized === "NOT_FOUND" || normalized.endsWith("_NOT_FOUND")) return ExitCode.NotFound;
106
+ if (USAGE_CODES.has(normalized)) return ExitCode.Usage;
107
+ if (APPROVAL_CODES.has(normalized)) return ExitCode.ApprovalPending;
108
+ if (NETWORK_CODES.has(normalized)) return ExitCode.Network;
109
+ if (INTERRUPT_CODES.has(normalized)) return ExitCode.Interrupted;
110
+ if (SERVER_CODES.has(normalized)) return ExitCode.Server;
111
+ return status === void 0 ? ExitCode.Error : exitCodeForStatus(status);
112
+ }
113
+ function exitCodeForStatus(status) {
114
+ if (status === 400 || status === 422) return ExitCode.Usage;
115
+ if (status === 401) return ExitCode.Auth;
116
+ if (status === 402 || status === 403) return ExitCode.Forbidden;
117
+ if (status === 404) return ExitCode.NotFound;
118
+ if (status >= 500) return ExitCode.Server;
119
+ return ExitCode.Error;
120
+ }
121
+ function codeForStatus(status) {
122
+ switch (status) {
123
+ case 400:
124
+ return "BAD_REQUEST";
125
+ case 401:
126
+ return "UNAUTHORIZED";
127
+ case 402:
128
+ return "PAYMENT_REQUIRED";
129
+ case 403:
130
+ return "FORBIDDEN";
131
+ case 404:
132
+ return "NOT_FOUND";
133
+ case 409:
134
+ return "CONFLICT";
135
+ case 429:
136
+ return "RATE_LIMIT_EXCEEDED";
137
+ default:
138
+ return status >= 500 ? "INTERNAL_ERROR" : `HTTP_${status}`;
139
+ }
140
+ }
141
+ function hintForCode(code, exitCode) {
142
+ if (exitCode === ExitCode.Auth) return "Run `tabbio login` to sign in again.";
143
+ if (code === "RATE_LIMIT_EXCEEDED") return "Wait a minute and try again.";
144
+ if (code === "UPGRADE_REQUIRED" || code === "PAYMENT_REQUIRED" || code === "ENTITLEMENT_REQUIRED") {
145
+ return "This needs a higher Tabbio plan. Manage your plan in the Tabbio app.";
146
+ }
147
+ if (exitCode === ExitCode.Server) return "Tabbio had a problem on its side. Try again shortly.";
148
+ return void 0;
149
+ }
150
+ function cliErrorFromEnvelope(error, opts = {}) {
151
+ const code = (error.code || (opts.status ? codeForStatus(opts.status) : "ERROR")).trim();
152
+ const exitCode = exitCodeForErrorCode(code, opts.status);
153
+ return new CliError({
154
+ code,
155
+ message: error.message?.trim() || defaultMessageForCode(code),
156
+ hint: opts.hint ?? hintForCode(code.toUpperCase(), exitCode),
157
+ exitCode,
158
+ requestId: opts.requestId,
159
+ status: opts.status
160
+ });
161
+ }
162
+ function cliErrorFromStatus(status, opts = {}) {
163
+ return cliErrorFromEnvelope(
164
+ { code: codeForStatus(status), message: opts.message ?? `Request failed with HTTP ${status}` },
165
+ { status, requestId: opts.requestId }
166
+ );
167
+ }
168
+ function defaultMessageForCode(code) {
169
+ switch (code.toUpperCase()) {
170
+ case "UNAUTHORIZED":
171
+ return "You are not signed in or your session expired";
172
+ case "FORBIDDEN":
173
+ return "You do not have access to do that";
174
+ case "NOT_FOUND":
175
+ return "Not found";
176
+ default:
177
+ return `Request failed (${code})`;
178
+ }
179
+ }
180
+ function notSignedInError(profile) {
181
+ return new CliError({
182
+ code: "NOT_SIGNED_IN",
183
+ message: `Not signed in (profile "${profile}")`,
184
+ hint: "Run `tabbio login` to sign in.",
185
+ exitCode: ExitCode.Auth
186
+ });
187
+ }
188
+ function needsFullSignInError(feature) {
189
+ return new CliError({
190
+ code: "FULL_SIGN_IN_REQUIRED",
191
+ message: `${feature} needs a full sign-in; a personal MCP token only works for tool commands`,
192
+ hint: "Run `tabbio login` (browser) or `tabbio login --email you@example.com`.",
193
+ exitCode: ExitCode.Auth
194
+ });
195
+ }
196
+ function interruptedError(message = "Cancelled") {
197
+ return new CliError({ code: "CANCELLED", message, exitCode: ExitCode.Interrupted, retry: false });
198
+ }
199
+ function usageError(message, hint) {
200
+ return new CliError({ code: "USAGE", message, hint, exitCode: ExitCode.Usage });
201
+ }
202
+ function networkError(url, cause) {
203
+ const timedOut = cause instanceof Error && (cause.name === "TimeoutError" || cause.name === "AbortError");
204
+ let host = url;
205
+ try {
206
+ host = new URL(url).host;
207
+ } catch {
208
+ }
209
+ return new CliError({
210
+ code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
211
+ message: timedOut ? `Timed out talking to ${host}` : `Could not reach ${host}`,
212
+ hint: "Check your connection, or the API URL with `tabbio status` / `tabbio doctor`.",
213
+ exitCode: ExitCode.Network,
214
+ cause
215
+ });
216
+ }
217
+ function toCliError(value) {
218
+ if (value instanceof CliError) return value;
219
+ if (value instanceof Error) {
220
+ return new CliError({
221
+ code: "UNEXPECTED",
222
+ message: value.message || "Unexpected error",
223
+ exitCode: ExitCode.Error,
224
+ cause: value
225
+ });
226
+ }
227
+ return new CliError({ code: "UNEXPECTED", message: String(value), exitCode: ExitCode.Error });
228
+ }
229
+
230
+ // src/core/io.ts
231
+ function writeOut(text = "") {
232
+ process.stdout.write(`${text}
233
+ `);
234
+ }
235
+ function printJson(value) {
236
+ const pretty = Boolean(process.stdout.isTTY);
237
+ process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : 0)}
238
+ `);
239
+ }
240
+ function formatKeyValues(rows) {
241
+ const visible = rows.filter(([, value]) => value !== void 0 && value !== null && value !== "");
242
+ const width = Math.max(0, ...visible.map(([label]) => label.length));
243
+ return visible.map(([label, value]) => `${theme.dim(label.padEnd(width))} ${String(value)}`);
244
+ }
245
+ function printKeyValues(rows) {
246
+ for (const line of formatKeyValues(rows)) writeOut(line);
247
+ }
248
+ function successLine(message) {
249
+ return `${theme.success(theme.symbols.success)} ${message}`;
250
+ }
251
+ function heading(text) {
252
+ return theme.bold(text);
253
+ }
254
+ function relativeTime(date, now = Date.now()) {
255
+ const diff = date.getTime() - now;
256
+ const abs = Math.abs(diff);
257
+ const units = [
258
+ [864e5, "d"],
259
+ [36e5, "h"],
260
+ [6e4, "m"],
261
+ [1e3, "s"]
262
+ ];
263
+ for (const [ms, unit] of units) {
264
+ if (abs >= ms) {
265
+ const value = Math.floor(abs / ms);
266
+ return diff < 0 ? `${value}${unit} ago` : `in ${value}${unit}`;
267
+ }
268
+ }
269
+ return "just now";
270
+ }
271
+
272
+ // src/core/config.ts
273
+ import { homedir } from "node:os";
274
+ import { isAbsolute, join } from "node:path";
275
+ import { z } from "zod";
276
+
277
+ // src/core/fs-atomic.ts
278
+ import { randomBytes } from "node:crypto";
279
+ import {
280
+ chmodSync,
281
+ closeSync,
282
+ existsSync,
283
+ fsyncSync,
284
+ mkdirSync,
285
+ openSync,
286
+ readFileSync,
287
+ renameSync,
288
+ rmSync,
289
+ statSync,
290
+ unlinkSync,
291
+ writeSync
292
+ } from "node:fs";
293
+ import { dirname } from "node:path";
294
+ var PRIVATE_DIR_MODE = 448;
295
+ var PRIVATE_FILE_MODE = 384;
296
+ var isWindows = process.platform === "win32";
297
+ function ensurePrivateDir(dir, mode = PRIVATE_DIR_MODE) {
298
+ mkdirSync(dir, { recursive: true, mode });
299
+ if (!isWindows) {
300
+ const current = statSync(dir).mode & 511;
301
+ if (current !== mode) chmodSync(dir, mode);
302
+ }
303
+ }
304
+ function writeFileAtomic(file, content, mode = PRIVATE_FILE_MODE) {
305
+ ensurePrivateDir(dirname(file));
306
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
307
+ let fd;
308
+ try {
309
+ fd = openSync(tmp, "wx", mode);
310
+ writeSync(fd, content);
311
+ fsyncSync(fd);
312
+ closeSync(fd);
313
+ fd = void 0;
314
+ if (!isWindows) chmodSync(tmp, mode);
315
+ renameSync(tmp, file);
316
+ } catch (error) {
317
+ if (fd !== void 0) closeSync(fd);
318
+ rmSync(tmp, { force: true });
319
+ throw error;
320
+ }
321
+ }
322
+ function readFileIfExists(file) {
323
+ try {
324
+ return readFileSync(file, "utf8");
325
+ } catch (error) {
326
+ if (error.code === "ENOENT") return null;
327
+ throw error;
328
+ }
329
+ }
330
+ function permissionBits(path) {
331
+ if (!existsSync(path)) return null;
332
+ return statSync(path).mode & 511;
333
+ }
334
+ var LOCK_STALE_MS = 3e4;
335
+ var LOCK_WAIT_MS = 15e3;
336
+ function sleep(ms) {
337
+ return new Promise((resolve) => setTimeout(resolve, ms));
338
+ }
339
+ async function withFileLock(lockFile, fn) {
340
+ ensurePrivateDir(dirname(lockFile));
341
+ const started = Date.now();
342
+ let fd;
343
+ while (fd === void 0) {
344
+ try {
345
+ fd = openSync(lockFile, "wx", PRIVATE_FILE_MODE);
346
+ writeSync(fd, String(process.pid));
347
+ } catch (error) {
348
+ if (error.code !== "EEXIST") throw error;
349
+ try {
350
+ const age = Date.now() - statSync(lockFile).mtimeMs;
351
+ if (age > LOCK_STALE_MS) {
352
+ unlinkSync(lockFile);
353
+ continue;
354
+ }
355
+ } catch {
356
+ continue;
357
+ }
358
+ if (Date.now() - started > LOCK_WAIT_MS) {
359
+ throw new Error(`Timed out waiting for ${lockFile}`);
360
+ }
361
+ await sleep(50 + Math.floor(Math.random() * 50));
362
+ }
363
+ }
364
+ try {
365
+ return await fn();
366
+ } finally {
367
+ closeSync(fd);
368
+ rmSync(lockFile, { force: true });
369
+ }
370
+ }
371
+
372
+ // src/core/config.ts
373
+ var DEFAULT_PROFILE = "default";
374
+ var PRODUCTION_API_URL = "https://server.tabbio.com";
375
+ var PRODUCTION_APP_URL = "https://app.tabbio.com";
376
+ var MCP_PATH = "/api/mcp";
377
+ var PROFILE_PRESETS = {
378
+ [DEFAULT_PROFILE]: { apiUrl: PRODUCTION_API_URL, appUrl: PRODUCTION_APP_URL },
379
+ local: { apiUrl: "http://localhost:3001", appUrl: "http://localhost:8081" }
380
+ };
381
+ var profileConfigSchema = z.object({
382
+ apiUrl: z.string().optional(),
383
+ appUrl: z.string().optional(),
384
+ mcpUrl: z.string().optional()
385
+ }).passthrough();
386
+ var configSchema = z.object({
387
+ version: z.literal(1).default(1),
388
+ currentProfile: z.string().optional(),
389
+ profiles: z.record(profileConfigSchema).default({}),
390
+ /** Set to false to disable the once-a-day npm update check. */
391
+ updateCheck: z.boolean().optional()
392
+ }).passthrough();
393
+ var PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
394
+ function env(name) {
395
+ const value = process.env[name]?.trim();
396
+ return value ? value : void 0;
397
+ }
398
+ function absoluteEnvDir(name) {
399
+ const value = env(name);
400
+ return value && isAbsolute(value) ? value : void 0;
401
+ }
402
+ function configPaths() {
403
+ const explicit = env("TABBIO_CONFIG_DIR");
404
+ const dir = explicit ?? join(absoluteEnvDir("XDG_CONFIG_HOME") ?? join(homedir(), ".config"), "tabbio");
405
+ const cacheDir = env("TABBIO_CACHE_DIR") ?? (explicit ? join(dir, "cache") : join(absoluteEnvDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tabbio"));
406
+ return {
407
+ dir,
408
+ configFile: join(dir, "config.json"),
409
+ credentialsFile: join(dir, "credentials.json"),
410
+ cacheDir
411
+ };
412
+ }
413
+ function emptyConfig() {
414
+ return { version: 1, profiles: {} };
415
+ }
416
+ function loadConfig() {
417
+ const { configFile } = configPaths();
418
+ const raw = readFileIfExists(configFile);
419
+ if (raw === null || raw.trim() === "") return emptyConfig();
420
+ let json;
421
+ try {
422
+ json = JSON.parse(raw);
423
+ } catch (error) {
424
+ throw new CliError({
425
+ code: "CONFIG_INVALID",
426
+ message: `Config file is not valid JSON: ${configFile}`,
427
+ hint: "Fix the file or delete it to start fresh.",
428
+ exitCode: ExitCode.Usage,
429
+ cause: error
430
+ });
431
+ }
432
+ const parsed = configSchema.safeParse(json);
433
+ if (!parsed.success) {
434
+ throw new CliError({
435
+ code: "CONFIG_INVALID",
436
+ message: `Config file has an unexpected shape: ${configFile}`,
437
+ hint: parsed.error.errors[0]?.message ?? "Fix the file or delete it to start fresh.",
438
+ exitCode: ExitCode.Usage
439
+ });
440
+ }
441
+ return parsed.data;
442
+ }
443
+ function saveConfig(c) {
444
+ const { configFile } = configPaths();
445
+ writeFileAtomic(configFile, `${JSON.stringify(configSchema.parse(c), null, 2)}
446
+ `);
447
+ }
448
+ function updateConfig(mutate) {
449
+ const config = loadConfig();
450
+ mutate(config);
451
+ saveConfig(config);
452
+ return config;
453
+ }
454
+ function assertProfileName(name) {
455
+ const trimmed = name.trim();
456
+ if (!PROFILE_NAME_PATTERN.test(trimmed)) {
457
+ throw usageError(
458
+ `Invalid profile name "${name}"`,
459
+ 'Use 1-32 letters, digits, "-" or "_", starting with a letter or digit.'
460
+ );
461
+ }
462
+ return trimmed;
463
+ }
464
+ function isLoopbackHost(hostname2) {
465
+ const host = hostname2.replace(/^\[|\]$/g, "").toLowerCase();
466
+ return host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\./.test(host);
467
+ }
468
+ function normalizeBaseUrl(value, label) {
469
+ let url;
470
+ try {
471
+ url = new URL(value.trim());
472
+ } catch {
473
+ throw usageError(`${label} is not a valid URL: ${value}`);
474
+ }
475
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
476
+ throw usageError(`${label} must use http or https: ${value}`);
477
+ }
478
+ if (url.username || url.password) {
479
+ throw usageError(`${label} must not contain credentials`);
480
+ }
481
+ if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && env("TABBIO_ALLOW_INSECURE_HTTP") !== "1") {
482
+ throw usageError(
483
+ `${label} must use https for non-local hosts: ${value}`,
484
+ "Set TABBIO_ALLOW_INSECURE_HTTP=1 only for trusted development networks."
485
+ );
486
+ }
487
+ url.search = "";
488
+ url.hash = "";
489
+ return url.toString().replace(/\/+$/, "");
490
+ }
491
+ function resolveProfileName(opts = {}, config) {
492
+ if (opts.profile?.trim()) return { name: assertProfileName(opts.profile), source: "flag" };
493
+ const fromEnv = env("TABBIO_PROFILE");
494
+ if (fromEnv) return { name: assertProfileName(fromEnv), source: "env" };
495
+ const current = (config ?? loadConfig()).currentProfile;
496
+ if (current?.trim()) return { name: assertProfileName(current), source: "config" };
497
+ return { name: DEFAULT_PROFILE, source: "default" };
498
+ }
499
+ function resolveProfileWithSources(opts = {}) {
500
+ const config = loadConfig();
501
+ const { name, source: nameSource } = resolveProfileName(opts, config);
502
+ const stored = config.profiles[name] ?? {};
503
+ const preset = PROFILE_PRESETS[name] ?? PROFILE_PRESETS[DEFAULT_PROFILE];
504
+ const pick = (flag, envName, fromConfig, fallback) => {
505
+ if (flag?.trim()) return [flag, "flag"];
506
+ const fromEnv = env(envName);
507
+ if (fromEnv) return [fromEnv, "env"];
508
+ if (fromConfig?.trim()) return [fromConfig, "config"];
509
+ return [fallback, PROFILE_PRESETS[name] && name !== DEFAULT_PROFILE ? "preset" : "default"];
510
+ };
511
+ const [apiRaw, apiSource] = pick(opts.apiUrl, "TABBIO_API_URL", stored.apiUrl, preset.apiUrl);
512
+ const [appRaw, appSource] = pick(opts.appUrl, "TABBIO_APP_URL", stored.appUrl, preset.appUrl);
513
+ const apiUrl = normalizeBaseUrl(apiRaw, "API URL");
514
+ const appUrl = normalizeBaseUrl(appRaw, "App URL");
515
+ let mcpUrl = `${apiUrl}${MCP_PATH}`;
516
+ let mcpSource = "derived";
517
+ const envMcp = env("TABBIO_MCP_URL");
518
+ if (envMcp) {
519
+ mcpUrl = normalizeBaseUrl(envMcp, "MCP URL");
520
+ mcpSource = "env";
521
+ } else if (stored.mcpUrl?.trim() && apiSource !== "flag" && apiSource !== "env") {
522
+ mcpUrl = normalizeBaseUrl(stored.mcpUrl, "MCP URL");
523
+ mcpSource = "config";
524
+ }
525
+ return {
526
+ profile: { name, apiUrl, appUrl, mcpUrl },
527
+ sources: { name: nameSource, apiUrl: apiSource, appUrl: appSource, mcpUrl: mcpSource }
528
+ };
529
+ }
530
+ function resolveProfile(opts = {}) {
531
+ return resolveProfileWithSources(opts).profile;
532
+ }
533
+ function listProfileNames(config = loadConfig()) {
534
+ return Array.from(/* @__PURE__ */ new Set([...Object.keys(PROFILE_PRESETS), ...Object.keys(config.profiles)])).sort();
535
+ }
536
+
537
+ // src/core/credentials.ts
538
+ import { join as join2 } from "node:path";
539
+ import { z as z2 } from "zod";
540
+ var storedCredentialsSchema = z2.object({
541
+ accessToken: z2.string().optional(),
542
+ accessTokenExpiresAt: z2.string().optional(),
543
+ refreshToken: z2.string().optional(),
544
+ mcpToken: z2.string().optional(),
545
+ user: z2.object({ id: z2.string(), email: z2.string(), name: z2.string().nullable().optional() }).optional(),
546
+ savedAt: z2.string().optional()
547
+ }).passthrough();
548
+ var credentialsFileSchema = z2.object({
549
+ version: z2.literal(1).default(1),
550
+ profiles: z2.record(storedCredentialsSchema).default({})
551
+ });
552
+ function env2(name) {
553
+ const value = process.env[name]?.trim();
554
+ return value ? value : void 0;
555
+ }
556
+ function readStore() {
557
+ const { credentialsFile } = configPaths();
558
+ const raw = readFileIfExists(credentialsFile);
559
+ if (raw === null || raw.trim() === "") return { version: 1, profiles: {} };
560
+ let json;
561
+ try {
562
+ json = JSON.parse(raw);
563
+ } catch (error) {
564
+ throw new CliError({
565
+ code: "CREDENTIALS_INVALID",
566
+ message: `Credential store is corrupted: ${credentialsFile}`,
567
+ hint: "Delete the file and run `tabbio login` again.",
568
+ exitCode: ExitCode.Auth,
569
+ cause: error
570
+ });
571
+ }
572
+ const parsed = credentialsFileSchema.safeParse(json);
573
+ if (!parsed.success) {
574
+ throw new CliError({
575
+ code: "CREDENTIALS_INVALID",
576
+ message: `Credential store has an unexpected shape: ${credentialsFile}`,
577
+ hint: "Delete the file and run `tabbio login` again.",
578
+ exitCode: ExitCode.Auth
579
+ });
580
+ }
581
+ return parsed.data;
582
+ }
583
+ function writeStore(store) {
584
+ const { credentialsFile } = configPaths();
585
+ writeFileAtomic(credentialsFile, `${JSON.stringify(store, null, 2)}
586
+ `, PRIVATE_FILE_MODE);
587
+ }
588
+ function toCredentials(profile, stored) {
589
+ return {
590
+ profile,
591
+ ...stored.accessToken ? { accessToken: stored.accessToken } : {},
592
+ ...stored.accessTokenExpiresAt ? { accessTokenExpiresAt: stored.accessTokenExpiresAt } : {},
593
+ ...stored.refreshToken ? { refreshToken: stored.refreshToken } : {},
594
+ ...stored.mcpToken ? { mcpToken: stored.mcpToken } : {},
595
+ ...stored.user ? { user: stored.user } : {}
596
+ };
597
+ }
598
+ function loadStoredCredentials(profile) {
599
+ const stored = readStore().profiles[profile];
600
+ if (!stored) return null;
601
+ const creds = toCredentials(profile, stored);
602
+ creds.origin = {
603
+ ...creds.accessToken ? { accessToken: "store" } : {},
604
+ ...creds.mcpToken ? { mcpToken: "store" } : {}
605
+ };
606
+ return creds;
607
+ }
608
+ function loadCredentials(profile) {
609
+ const stored = loadStoredCredentials(profile);
610
+ const envMcp = env2("TABBIO_TOKEN");
611
+ const envAccess = env2("TABBIO_ACCESS_TOKEN");
612
+ if (!stored && !envMcp && !envAccess) return null;
613
+ const creds = stored ?? { profile, origin: {} };
614
+ creds.origin = { ...creds.origin };
615
+ if (envMcp) {
616
+ creds.mcpToken = envMcp;
617
+ creds.origin.mcpToken = "env";
618
+ }
619
+ if (envAccess) {
620
+ creds.accessToken = envAccess;
621
+ delete creds.accessTokenExpiresAt;
622
+ delete creds.refreshToken;
623
+ creds.origin.accessToken = "env";
624
+ }
625
+ return creds;
626
+ }
627
+ function saveCredentials(c) {
628
+ const store = readStore();
629
+ const next = { savedAt: (/* @__PURE__ */ new Date()).toISOString() };
630
+ const envMcp = env2("TABBIO_TOKEN");
631
+ const envAccess = env2("TABBIO_ACCESS_TOKEN");
632
+ const accessFromEnv = c.origin?.accessToken === "env" || envAccess && c.accessToken === envAccess;
633
+ const mcpFromEnv = c.origin?.mcpToken === "env" || envMcp && c.mcpToken === envMcp;
634
+ if (c.accessToken && !accessFromEnv) {
635
+ next.accessToken = c.accessToken;
636
+ if (c.accessTokenExpiresAt) next.accessTokenExpiresAt = c.accessTokenExpiresAt;
637
+ }
638
+ if (c.refreshToken) next.refreshToken = c.refreshToken;
639
+ if (c.mcpToken && !mcpFromEnv) next.mcpToken = c.mcpToken;
640
+ if (c.user) next.user = { id: c.user.id, email: c.user.email, name: c.user.name ?? null };
641
+ const previous = store.profiles[c.profile];
642
+ if (previous && accessFromEnv && previous.accessToken && !next.accessToken) {
643
+ next.accessToken = previous.accessToken;
644
+ if (previous.accessTokenExpiresAt) next.accessTokenExpiresAt = previous.accessTokenExpiresAt;
645
+ if (!next.refreshToken && previous.refreshToken) next.refreshToken = previous.refreshToken;
646
+ }
647
+ if (previous && mcpFromEnv && previous.mcpToken && !next.mcpToken) next.mcpToken = previous.mcpToken;
648
+ store.profiles[c.profile] = next;
649
+ writeStore(store);
650
+ }
651
+ function clearCredentials(profile) {
652
+ const store = readStore();
653
+ if (!(profile in store.profiles)) return;
654
+ delete store.profiles[profile];
655
+ writeStore(store);
656
+ }
657
+ function listCredentialProfiles() {
658
+ return Object.keys(readStore().profiles).sort();
659
+ }
660
+ function withCredentialsLock(fn) {
661
+ return withFileLock(join2(configPaths().dir, "credentials.lock"), fn);
662
+ }
663
+ function credentialPermissionIssues() {
664
+ if (process.platform === "win32") return [];
665
+ const { dir, credentialsFile } = configPaths();
666
+ const issues = [];
667
+ const dirBits = permissionBits(dir);
668
+ if (dirBits !== null && (dirBits & 63) !== 0) {
669
+ issues.push(`${dir} is mode ${dirBits.toString(8)} (expected ${PRIVATE_DIR_MODE.toString(8)})`);
670
+ }
671
+ const fileBits = permissionBits(credentialsFile);
672
+ if (fileBits !== null && (fileBits & 63) !== 0) {
673
+ issues.push(
674
+ `${credentialsFile} is mode ${fileBits.toString(8)} (expected ${PRIVATE_FILE_MODE.toString(8)})`
675
+ );
676
+ }
677
+ return issues;
678
+ }
679
+ var KNOWN_PREFIXES = ["tabbio_mcp_"];
680
+ function fingerprint(secret) {
681
+ const value = secret.trim();
682
+ if (value.length < 16) return "\u2026";
683
+ const prefix = KNOWN_PREFIXES.find((p) => value.startsWith(p)) ?? value.slice(0, 3);
684
+ return `${prefix}\u2026${value.slice(-4)}`;
685
+ }
686
+ var SECRET_JSON_KEYS = /("(?:accessToken|refreshToken|token|secret|mcpToken|otp|code|password|cookie|authorization)"\s*:\s*")([^"]*)(")/gi;
687
+ function redactSecrets(text) {
688
+ return text.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/tabbio_mcp_[A-Za-z0-9_-]+/g, "tabbio_mcp_[redacted]").replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted-jwt]").replace(SECRET_JSON_KEYS, "$1[redacted]$3").replace(/((?:^|[?&;\s])(?:code|state|token|refreshToken|otp)=)[^&\s;]+/gi, "$1[redacted]").replace(/((?:__Secure-|__Host-)?better-auth\.[a-z_]+=)[^;\s]+/gi, "$1[redacted]");
689
+ }
690
+ function redactUrl(url) {
691
+ try {
692
+ const parsed = new URL(url);
693
+ for (const key of Array.from(parsed.searchParams.keys())) {
694
+ if (/code|state|token|otp|secret/i.test(key)) parsed.searchParams.set(key, "[redacted]");
695
+ }
696
+ return parsed.toString();
697
+ } catch {
698
+ return redactSecrets(url);
699
+ }
700
+ }
701
+
702
+ // src/core/runtime.ts
703
+ import { hostname } from "node:os";
704
+ var DEFAULT_GLOBALS = {
705
+ json: false,
706
+ color: true,
707
+ debug: false,
708
+ yes: false,
709
+ quiet: false
710
+ };
711
+ var globals = { ...DEFAULT_GLOBALS, debug: process.env.TABBIO_DEBUG === "1" };
712
+ function setGlobalOptions(partial) {
713
+ globals = { ...globals, ...partial };
714
+ return globals;
715
+ }
716
+ function getGlobalOptions() {
717
+ return globals;
718
+ }
719
+ function isInteractive() {
720
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI;
721
+ }
722
+ function deviceLabel() {
723
+ const raw = process.env.TABBIO_DEVICE_LABEL?.trim() || hostname() || "unknown-host";
724
+ const clean = raw.replace(/\.local$/i, "").replace(/[^\x20-\x7E]/g, "").trim();
725
+ return (clean || "unknown-host").slice(0, 64);
726
+ }
727
+ function debug(message) {
728
+ if (!globals.debug) return;
729
+ process.stderr.write(`${theme.dim(`[debug] ${redactSecrets(message)}`)}
730
+ `);
731
+ }
732
+ function info(message) {
733
+ if (globals.quiet) return;
734
+ process.stderr.write(`${message}
735
+ `);
736
+ }
737
+ function warn(message, hint) {
738
+ process.stderr.write(`${theme.warn(`${theme.symbols.warning} ${message}`)}
739
+ `);
740
+ if (hint) process.stderr.write(` ${theme.dim(hint)}
741
+ `);
742
+ }
743
+
744
+ // src/core/version.ts
745
+ import { readFileSync as readFileSync2 } from "node:fs";
746
+ import { join as join3 } from "node:path";
747
+ var CLI_VERSION = true ? "1.2.8" : readPackageField("version") ?? "0.0.0-dev";
748
+ var CLI_PACKAGE_NAME = true ? "@tabbio-technologies/cli" : readPackageField("name") ?? "@tabbio-technologies/cli";
749
+ function userAgent() {
750
+ return `tabbio-cli/${CLI_VERSION} node/${process.versions.node} ${process.platform}-${process.arch}`;
751
+ }
752
+ function compareVersions(a, b) {
753
+ const parse = (v) => {
754
+ const [core = "", pre] = v.trim().replace(/^v/, "").split("-", 2);
755
+ const nums = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
756
+ return { nums: [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0], pre };
757
+ };
758
+ const left = parse(a);
759
+ const right = parse(b);
760
+ for (let i = 0; i < 3; i += 1) {
761
+ const diff = (left.nums[i] ?? 0) - (right.nums[i] ?? 0);
762
+ if (diff !== 0) return diff > 0 ? 1 : -1;
763
+ }
764
+ if (left.pre && !right.pre) return -1;
765
+ if (!left.pre && right.pre) return 1;
766
+ return 0;
767
+ }
768
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
769
+ var UPDATE_CHECK_TIMEOUT_MS = 1e3;
770
+ function updateChecksDisabled() {
771
+ if (process.env.TABBIO_NO_UPDATE_CHECK || process.env.CI) return true;
772
+ if (!process.stderr.isTTY) return true;
773
+ try {
774
+ return loadConfig().updateCheck === false;
775
+ } catch {
776
+ return true;
777
+ }
778
+ }
779
+ function cacheFile() {
780
+ return join3(configPaths().cacheDir, "update-check.json");
781
+ }
782
+ function readUpdateCache() {
783
+ try {
784
+ const raw = readFileIfExists(cacheFile());
785
+ return raw ? JSON.parse(raw) : null;
786
+ } catch {
787
+ return null;
788
+ }
789
+ }
790
+ async function checkForUpdate(opts = {}) {
791
+ if (updateChecksDisabled()) return null;
792
+ const now = opts.now ?? Date.now();
793
+ let cache = readUpdateCache();
794
+ const stale = !cache || now - Date.parse(cache.checkedAt) > UPDATE_CHECK_TTL_MS;
795
+ if (stale) {
796
+ let latest2 = null;
797
+ try {
798
+ const response = await (opts.fetch ?? fetch)(
799
+ `https://registry.npmjs.org/${CLI_PACKAGE_NAME}/latest`,
800
+ { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS), headers: { accept: "application/json" } }
801
+ );
802
+ if (response.ok) {
803
+ const body = await response.json();
804
+ latest2 = typeof body.version === "string" ? body.version : null;
805
+ }
806
+ } catch {
807
+ latest2 = cache?.latest ?? null;
808
+ }
809
+ cache = { checkedAt: new Date(now).toISOString(), latest: latest2 };
810
+ try {
811
+ writeFileAtomic(cacheFile(), JSON.stringify(cache), 384);
812
+ } catch {
813
+ }
814
+ }
815
+ const latest = cache?.latest;
816
+ return latest && compareVersions(latest, CLI_VERSION) > 0 ? latest : null;
817
+ }
818
+
819
+ // src/core/mcp.ts
820
+ import { createHash } from "node:crypto";
821
+ import { readdirSync, rmSync as rmSync2 } from "node:fs";
822
+ import { join as join4 } from "node:path";
823
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
824
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
825
+
826
+ // src/core/catalog.ts
827
+ var HIDDEN_TOOL_IDS = /* @__PURE__ */ new Set(["mcp.approvalStatus"]);
828
+ var COMMAND_PATH_OVERRIDES = {
829
+ publishArtifact: ["site", "publish"],
830
+ unpublishArtifact: ["site", "unpublish"]
831
+ };
832
+ var PRIMARY_AGENT_KEY = "tabbio";
833
+ function kebabCase(value) {
834
+ return value.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[\s_.]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
835
+ }
836
+ function readString(value) {
837
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
838
+ }
839
+ function toSchema(value) {
840
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
841
+ return { type: "object", properties: {} };
842
+ }
843
+ function commandPathFor(id, mcpName = id) {
844
+ const agent = /^ask_(.+)$/.exec(mcpName);
845
+ if (agent?.[1]) {
846
+ const key = agent[1];
847
+ if (key === PRIMARY_AGENT_KEY) {
848
+ return { kind: "agent", group: "ask", action: "", commandPath: ["ask"], key };
849
+ }
850
+ const action2 = kebabCase(key);
851
+ return { kind: "agent", group: "ask", action: action2, commandPath: ["ask", action2], key };
852
+ }
853
+ const workflow = /^run_(.+)$/.exec(mcpName);
854
+ if (workflow?.[1]) {
855
+ const key = workflow[1];
856
+ const action2 = kebabCase(key);
857
+ return {
858
+ kind: "workflow",
859
+ group: "workflows",
860
+ action: action2,
861
+ commandPath: ["workflows", "run", action2],
862
+ key
863
+ };
864
+ }
865
+ const override = COMMAND_PATH_OVERRIDES[id];
866
+ if (override) {
867
+ const [group, action2] = override;
868
+ return { kind: "tool", group, action: action2, commandPath: [group, action2] };
869
+ }
870
+ const segments = id.split(".").filter(Boolean);
871
+ if (segments.length >= 2) {
872
+ const group = kebabCase(segments[0]);
873
+ const action2 = segments.slice(1).map(kebabCase).join("-");
874
+ return { kind: "tool", group, action: action2, commandPath: [group, action2] };
875
+ }
876
+ const action = kebabCase(id);
877
+ return { kind: "tool", group: "misc", action, commandPath: ["misc", action] };
878
+ }
879
+ function normalizeTool(raw) {
880
+ const meta = raw._meta ?? {};
881
+ const annotations = raw.annotations ?? {};
882
+ const mcpName = raw.name;
883
+ const id = readString(meta.tabbioToolId) ?? mcpName;
884
+ const placement = commandPathFor(id, mcpName);
885
+ const title = readString(annotations.title) ?? readString(raw.title) ?? (placement.kind === "workflow" ? `Run workflow ${placement.action}` : placement.kind === "agent" ? `Ask ${placement.key ?? "Tabbio"}` : id);
886
+ return {
887
+ id,
888
+ mcpName,
889
+ title,
890
+ description: readString(raw.description) ?? "",
891
+ readOnly: annotations.readOnlyHint === true,
892
+ inputSchema: toSchema(raw.inputSchema),
893
+ kind: placement.kind,
894
+ group: placement.group,
895
+ action: placement.action,
896
+ commandPath: placement.commandPath,
897
+ hidden: HIDDEN_TOOL_IDS.has(id),
898
+ annotations: { ...annotations },
899
+ ...placement.key ? { key: placement.key } : {}
900
+ };
901
+ }
902
+ function buildCatalog(rawTools) {
903
+ return rawTools.filter((tool) => typeof tool?.name === "string" && tool.name.length > 0).map(normalizeTool).sort((a, b) => a.commandPath.join(" ").localeCompare(b.commandPath.join(" ")));
904
+ }
905
+ function findTool(tools, ref) {
906
+ const needle = ref.trim();
907
+ if (!needle) return void 0;
908
+ const exact = tools.find((t) => t.id === needle || t.mcpName === needle);
909
+ if (exact) return exact;
910
+ const lower = needle.toLowerCase();
911
+ const asPath = lower.split(/[\s:.]+/).filter(Boolean).map(kebabCase).join(" ");
912
+ return tools.find(
913
+ (t) => t.id.toLowerCase() === lower || t.commandPath.join(" ") === asPath || [t.group, t.action].filter(Boolean).join(" ") === asPath || t.kind === "workflow" && (t.action === asPath || t.key?.toLowerCase() === lower)
914
+ );
915
+ }
916
+ function groupCatalog(tools) {
917
+ const groups = /* @__PURE__ */ new Map();
918
+ for (const tool of tools) {
919
+ const list = groups.get(tool.group) ?? [];
920
+ list.push(tool);
921
+ groups.set(tool.group, list);
922
+ }
923
+ return groups;
924
+ }
925
+
926
+ // src/core/http.ts
927
+ var CLIENT_PLATFORM = "cli";
928
+ var CLIENT_PLATFORM_HEADER = "x-tabbio-client-platform";
929
+ var DEVICE_LABEL_HEADER = "x-tabbio-device-label";
930
+ var REFRESH_SKEW_MS = 5 * 60 * 1e3;
931
+ var DEFAULT_TIMEOUT_MS = 3e4;
932
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([502, 503, 504]);
933
+ var MAX_RETRIES = 2;
934
+ var REQUEST_ID_HEADERS = ["x-request-id", "x-railway-request-id", "x-vercel-id", "cf-ray"];
935
+ function readRequestId(headers) {
936
+ for (const name of REQUEST_ID_HEADERS) {
937
+ const value = headers.get(name);
938
+ if (value) return value;
939
+ }
940
+ return void 0;
941
+ }
942
+ function baseHeaders() {
943
+ return {
944
+ [CLIENT_PLATFORM_HEADER]: CLIENT_PLATFORM,
945
+ [DEVICE_LABEL_HEADER]: deviceLabel(),
946
+ "user-agent": userAgent()
947
+ };
948
+ }
949
+ function isEnvelope(value) {
950
+ return typeof value === "object" && value !== null && "data" in value && "error" in value;
951
+ }
952
+ function isExpiringSoon(creds, now = Date.now()) {
953
+ if (!creds.accessTokenExpiresAt) return false;
954
+ const expiresAt = Date.parse(creds.accessTokenExpiresAt);
955
+ return Number.isFinite(expiresAt) && expiresAt - now < REFRESH_SKEW_MS;
956
+ }
957
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
958
+ function combineSignals(...signals) {
959
+ const present = signals.filter((s) => Boolean(s));
960
+ if (present.length === 1) return present[0];
961
+ const controller = new AbortController();
962
+ for (const signal of present) {
963
+ if (signal.aborted) {
964
+ controller.abort(signal.reason);
965
+ break;
966
+ }
967
+ signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
968
+ }
969
+ return controller.signal;
970
+ }
971
+ var ApiClient = class {
972
+ constructor(profile, creds, opts = {}) {
973
+ this.profile = profile;
974
+ this.creds = creds ? { ...creds } : null;
975
+ this.opts = opts;
976
+ this.fetchImpl = opts.fetch ?? ((...args) => fetch(...args));
977
+ }
978
+ creds;
979
+ fetchImpl;
980
+ opts;
981
+ refreshing = null;
982
+ /** Request id of the most recent response, when the server sent one. */
983
+ lastRequestId;
984
+ get credentials() {
985
+ return this.creds;
986
+ }
987
+ /** JSON request; unwraps the `{ data, error, meta }` envelope or throws CliError. */
988
+ async json(path, init = {}) {
989
+ return (await this.request(path, init)).data;
990
+ }
991
+ /** Like json() but also returns meta, status and request id. */
992
+ async request(path, init = {}) {
993
+ const headers = { accept: "application/json", ...init.headers };
994
+ let body;
995
+ if (init.body !== void 0) {
996
+ headers["content-type"] = "application/json";
997
+ body = JSON.stringify(init.body);
998
+ }
999
+ const response = await this.raw(path, {
1000
+ method: init.method ?? "GET",
1001
+ headers,
1002
+ body,
1003
+ auth: init.auth,
1004
+ timeoutMs: init.timeoutMs,
1005
+ signal: init.signal
1006
+ });
1007
+ return parseJsonResponse(response);
1008
+ }
1009
+ /** Low-level fetch with CLI headers, auth, refresh-on-401 and GET retries. */
1010
+ async raw(path, init = {}) {
1011
+ if (!path.startsWith("/")) throw new Error(`API path must start with "/": ${path}`);
1012
+ const { auth = "jwt", timeoutMs, ...requestInit } = init;
1013
+ const method = (requestInit.method ?? "GET").toUpperCase();
1014
+ const url = `${this.profile.apiUrl}${path}`;
1015
+ const idempotent = method === "GET" || method === "HEAD";
1016
+ let token = auth === "jwt" ? await this.ensureAccessToken() : void 0;
1017
+ let refreshed = false;
1018
+ let attempt = 0;
1019
+ for (; ; ) {
1020
+ const headers = new Headers(requestInit.headers);
1021
+ for (const [key, value] of Object.entries(baseHeaders())) {
1022
+ if (!headers.has(key)) headers.set(key, value);
1023
+ }
1024
+ if (token) headers.set("authorization", `Bearer ${token}`);
1025
+ const signal = combineSignals(
1026
+ activeCommandSignal,
1027
+ AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS),
1028
+ requestInit.signal ?? void 0
1029
+ );
1030
+ const started = Date.now();
1031
+ debug(`\u2192 ${method} ${redactUrl(url)}`);
1032
+ let response;
1033
+ try {
1034
+ response = await this.fetchImpl(url, {
1035
+ ...requestInit,
1036
+ method,
1037
+ headers,
1038
+ signal
1039
+ });
1040
+ } catch (error) {
1041
+ if (activeCommandSignal.aborted || requestInit.signal?.aborted) throw error;
1042
+ debug(`\u2717 ${method} ${redactUrl(url)} ${error.message} ${Date.now() - started}ms`);
1043
+ if (idempotent && attempt < MAX_RETRIES) {
1044
+ attempt += 1;
1045
+ await sleep2(this.backoff(attempt));
1046
+ continue;
1047
+ }
1048
+ throw networkError(url, error);
1049
+ }
1050
+ const requestId = readRequestId(response.headers);
1051
+ if (requestId) this.lastRequestId = requestId;
1052
+ debug(
1053
+ `\u2190 ${response.status} ${method} ${redactUrl(url)} ${Date.now() - started}ms${requestId ? ` req=${requestId}` : ""}`
1054
+ );
1055
+ if (idempotent && RETRYABLE_STATUS.has(response.status) && attempt < MAX_RETRIES) {
1056
+ attempt += 1;
1057
+ await response.body?.cancel().catch(() => void 0);
1058
+ await sleep2(this.backoff(attempt));
1059
+ continue;
1060
+ }
1061
+ if (response.status === 401 && auth === "jwt" && !refreshed && this.canRefresh()) {
1062
+ refreshed = true;
1063
+ await response.body?.cancel().catch(() => void 0);
1064
+ token = await this.refresh();
1065
+ continue;
1066
+ }
1067
+ return response;
1068
+ }
1069
+ }
1070
+ /**
1071
+ * Returns a usable access token, refreshing first when it expires within
1072
+ * five minutes. Throws CliError (exit 3) when there is no JWT session.
1073
+ */
1074
+ async ensureAccessToken() {
1075
+ const creds = this.creds;
1076
+ if (!creds?.accessToken) {
1077
+ if (creds?.mcpToken) throw needsFullSignInError("This command");
1078
+ throw notSignedInError(this.profile.name);
1079
+ }
1080
+ if (isExpiringSoon(creds) && this.canRefresh()) return this.refresh();
1081
+ return creds.accessToken;
1082
+ }
1083
+ /**
1084
+ * Rotates the refresh token once. The rotated token is persisted atomically
1085
+ * before it is used, under a cross-process lock; if another process already
1086
+ * rotated it, its result is adopted instead of refreshing twice (which the
1087
+ * server would treat as token reuse and revoke the session family).
1088
+ */
1089
+ refresh() {
1090
+ if (!this.refreshing) {
1091
+ this.refreshing = this.doRefresh().finally(() => {
1092
+ this.refreshing = null;
1093
+ });
1094
+ }
1095
+ return this.refreshing;
1096
+ }
1097
+ canRefresh() {
1098
+ return Boolean(this.creds?.refreshToken);
1099
+ }
1100
+ backoff(attempt) {
1101
+ const base = this.opts.retryDelayMs ?? 300;
1102
+ return base * 3 ** (attempt - 1) + (base ? Math.floor(Math.random() * base) : 0);
1103
+ }
1104
+ async doRefresh() {
1105
+ const persist = this.opts.persist !== false;
1106
+ const run = async () => {
1107
+ const current = this.creds;
1108
+ if (!current?.refreshToken) throw notSignedInError(this.profile.name);
1109
+ if (persist) {
1110
+ const onDisk = loadStoredCredentials(this.profile.name);
1111
+ if (onDisk?.refreshToken && onDisk.refreshToken !== current.refreshToken && onDisk.accessToken && !isExpiringSoon(onDisk)) {
1112
+ debug("adopting credentials refreshed by another tabbio process");
1113
+ this.creds = { ...current, ...onDisk, origin: current.origin };
1114
+ return onDisk.accessToken;
1115
+ }
1116
+ }
1117
+ let payload;
1118
+ try {
1119
+ payload = await this.json("/api/auth/tokens/refresh", {
1120
+ method: "POST",
1121
+ body: { refreshToken: current.refreshToken },
1122
+ auth: "none"
1123
+ });
1124
+ } catch (error) {
1125
+ if (error instanceof CliError && error.exitCode !== ExitCode.Network) {
1126
+ throw new CliError({
1127
+ code: error.code === "SESSION_REUSE_DETECTED" ? error.code : "SESSION_EXPIRED",
1128
+ message: "Your Tabbio session has expired",
1129
+ hint: "Run `tabbio login` to sign in again.",
1130
+ exitCode: ExitCode.Auth,
1131
+ requestId: error.requestId,
1132
+ cause: error
1133
+ });
1134
+ }
1135
+ throw error;
1136
+ }
1137
+ const next = {
1138
+ ...current,
1139
+ accessToken: payload.accessToken,
1140
+ accessTokenExpiresAt: payload.accessTokenExpiresAt,
1141
+ refreshToken: payload.refreshToken ?? current.refreshToken,
1142
+ user: payload.user ? { id: payload.user.id, email: payload.user.email, name: payload.user.name ?? null } : current.user,
1143
+ origin: { ...current.origin, accessToken: "store" }
1144
+ };
1145
+ if (persist) saveCredentials(next);
1146
+ this.creds = next;
1147
+ this.opts.onCredentialsChange?.(next);
1148
+ return payload.accessToken;
1149
+ };
1150
+ return persist ? withCredentialsLock(run) : run();
1151
+ }
1152
+ };
1153
+ async function parseJsonResponse(response) {
1154
+ const requestId = readRequestId(response.headers);
1155
+ const text = await response.text();
1156
+ let parsed = void 0;
1157
+ if (text.trim()) {
1158
+ try {
1159
+ parsed = JSON.parse(text);
1160
+ } catch {
1161
+ if (!response.ok) throw cliErrorFromStatus(response.status, { requestId });
1162
+ throw new CliError({
1163
+ code: "BAD_RESPONSE",
1164
+ message: "Tabbio sent a response the CLI could not read",
1165
+ exitCode: ExitCode.Server,
1166
+ status: response.status,
1167
+ requestId
1168
+ });
1169
+ }
1170
+ }
1171
+ if (isEnvelope(parsed)) {
1172
+ if (parsed.error) {
1173
+ throw cliErrorFromEnvelope(parsed.error, { status: response.status, requestId });
1174
+ }
1175
+ if (!response.ok) throw cliErrorFromStatus(response.status, { requestId });
1176
+ return { data: parsed.data, meta: parsed.meta ?? null, status: response.status, requestId };
1177
+ }
1178
+ if (!response.ok) {
1179
+ const record = parsed ?? {};
1180
+ if (typeof record.message === "string" || typeof record.code === "string") {
1181
+ throw cliErrorFromEnvelope(
1182
+ {
1183
+ code: typeof record.code === "string" ? record.code : void 0,
1184
+ message: typeof record.message === "string" ? record.message : void 0
1185
+ },
1186
+ { status: response.status, requestId }
1187
+ );
1188
+ }
1189
+ throw cliErrorFromStatus(response.status, { requestId });
1190
+ }
1191
+ return { data: parsed, meta: null, status: response.status, requestId };
1192
+ }
1193
+
1194
+ // src/core/mcp-results.ts
1195
+ function isRecord(value) {
1196
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1197
+ }
1198
+ function detectApproval(value, fallbackToolName = "") {
1199
+ if (!isRecord(value) || value.approvalRequired !== true) return null;
1200
+ if (typeof value.approvalId !== "string" || !value.approvalId) return null;
1201
+ return {
1202
+ approvalId: value.approvalId,
1203
+ toolName: typeof value.toolName === "string" ? value.toolName : fallbackToolName,
1204
+ message: typeof value.message === "string" ? value.message : "This Tabbio action requires approval before it runs.",
1205
+ ...typeof value.status === "string" ? { status: value.status } : {}
1206
+ };
1207
+ }
1208
+ function extractToolPayload(result) {
1209
+ const first = result.content?.[0];
1210
+ if (first?.type === "text" && typeof first.text === "string") {
1211
+ const text = first.text;
1212
+ try {
1213
+ return JSON.parse(text);
1214
+ } catch {
1215
+ return text;
1216
+ }
1217
+ }
1218
+ if (result.structuredContent !== void 0) return result.structuredContent;
1219
+ return result.content ?? null;
1220
+ }
1221
+ function contentText(result) {
1222
+ return (result.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
1223
+ }
1224
+ function parseToolErrorText(text) {
1225
+ const trimmed = text.trim();
1226
+ try {
1227
+ const json = JSON.parse(trimmed);
1228
+ if (isRecord(json)) {
1229
+ const cause = isRecord(json.cause) ? json.cause : void 0;
1230
+ const details = isRecord(json.details) ? json.details : void 0;
1231
+ const message = typeof cause?.message === "string" && cause.message || typeof json.message === "string" && json.message || trimmed;
1232
+ const status = [json.status, cause?.status, details?.status].find(
1233
+ (value) => typeof value === "number"
1234
+ );
1235
+ const code = typeof json.code === "string" ? json.code : void 0;
1236
+ return { message: String(message).replace(/^Error:\s*/, ""), status, code };
1237
+ }
1238
+ } catch {
1239
+ }
1240
+ return { message: trimmed.replace(/^Error:\s*/, "") || "The tool failed" };
1241
+ }
1242
+ function classifyToolError(toolName, message, status, code) {
1243
+ const make = (errCode, exitCode, hint) => new CliError({ code: errCode, message, hint, exitCode, status });
1244
+ if (/pass companyId/i.test(message)) {
1245
+ return make("MCP_SCOPE_FORBIDDEN", ExitCode.Forbidden, "Pass the company id (e.g. --company-id <id>).");
1246
+ }
1247
+ if (/does not allow (writes|reads)|not scoped to|scoped to selected company/i.test(message)) {
1248
+ return make(
1249
+ "MCP_SCOPE_FORBIDDEN",
1250
+ ExitCode.Forbidden,
1251
+ "The personal token in use does not cover this. Sign in with `tabbio login` for full access."
1252
+ );
1253
+ }
1254
+ if (/not available through the assistant/i.test(message)) return make("FORBIDDEN", ExitCode.Forbidden);
1255
+ if (/^Unknown tool/i.test(message)) {
1256
+ return make("UNKNOWN_TOOL", ExitCode.NotFound, "Run `tabbio tools --refresh` to reload the catalog.");
1257
+ }
1258
+ if (/validation failed|invalid arguments/i.test(message)) return make("VALIDATION_ERROR", ExitCode.Usage);
1259
+ if (/requires an active company/i.test(message)) {
1260
+ return make("COMPANY_REQUIRED", ExitCode.Usage, "Pass the company id (e.g. --company-id <id>).");
1261
+ }
1262
+ if (/missing authenticated user|unauthori[sz]ed/i.test(message)) {
1263
+ return make("UNAUTHORIZED", ExitCode.Auth, "Run `tabbio login` to sign in again.");
1264
+ }
1265
+ if (code && code !== "ERROR") {
1266
+ const mapped = cliErrorFromEnvelope({ code, message }, { status });
1267
+ if (mapped.exitCode !== ExitCode.Error) return mapped;
1268
+ }
1269
+ if (status) return make(status === 404 ? "NOT_FOUND" : "TOOL_ERROR", exitCodeForStatus(status));
1270
+ if (/\bnot found\b/i.test(message)) return make("NOT_FOUND", ExitCode.NotFound);
1271
+ return make("TOOL_ERROR", ExitCode.Error, toolName ? `Tool: ${toolName}` : void 0);
1272
+ }
1273
+ function interpretToolResult(result, toolName) {
1274
+ if (result.isError) {
1275
+ const parsed = parseToolErrorText(contentText(result) || "The tool failed");
1276
+ return { ok: false, error: classifyToolError(toolName, parsed.message, parsed.status, parsed.code) };
1277
+ }
1278
+ return interpretToolPayload(extractToolPayload(result), toolName);
1279
+ }
1280
+ function interpretToolPayload(payload, toolName) {
1281
+ if (isRecord(payload)) {
1282
+ if (payload.error === true && typeof payload.message === "string") {
1283
+ return { ok: false, error: classifyToolError(toolName, payload.message) };
1284
+ }
1285
+ if (payload.ok === false && isRecord(payload.error)) {
1286
+ const err = payload.error;
1287
+ return {
1288
+ ok: false,
1289
+ error: cliErrorFromEnvelope(
1290
+ {
1291
+ code: typeof err.code === "string" ? err.code : "TOOL_ERROR",
1292
+ message: typeof err.message === "string" ? err.message : "The tool failed"
1293
+ },
1294
+ { status: typeof err.status === "number" ? err.status : void 0 }
1295
+ )
1296
+ };
1297
+ }
1298
+ }
1299
+ const approval = detectApproval(payload, toolName);
1300
+ return approval ? { ok: true, result: payload, approval } : { ok: true, result: payload };
1301
+ }
1302
+ function readErrorDescription(text) {
1303
+ const match = /\{[\s\S]*\}/.exec(text);
1304
+ if (!match) return void 0;
1305
+ try {
1306
+ const json = JSON.parse(match[0]);
1307
+ if (typeof json.error_description === "string") return json.error_description;
1308
+ if (typeof json.message === "string") return json.message;
1309
+ if (typeof json.error === "string") return json.error;
1310
+ } catch {
1311
+ return void 0;
1312
+ }
1313
+ return void 0;
1314
+ }
1315
+ function mapMcpTransportError(error, ctx) {
1316
+ if (error instanceof CliError) return error;
1317
+ const err = error;
1318
+ const message = err?.message ?? String(error);
1319
+ const isUnauthorized = err?.name === "UnauthorizedError" || error?.constructor?.name === "UnauthorizedError" || typeof err?.code === "number" && err.code === 401;
1320
+ if (isUnauthorized) {
1321
+ const detail = readErrorDescription(message);
1322
+ const personal = ctx.kind === "personal-token";
1323
+ return new CliError({
1324
+ code: personal ? "MCP_TOKEN_REJECTED" : "SESSION_EXPIRED",
1325
+ message: personal ? `The personal MCP token for profile "${ctx.profile}" was rejected${detail ? ` (${detail})` : ""}` : `Tabbio rejected the session for profile "${ctx.profile}"${detail ? ` (${detail})` : ""}`,
1326
+ hint: personal ? "Tokens are revoked when a newer one is created in Settings \u2192 MCP access. Create one and run `tabbio login --token \u2026`, or sign in with `tabbio login`." : "Run `tabbio login` to sign in again.",
1327
+ exitCode: ExitCode.Auth,
1328
+ status: 401,
1329
+ cause: error
1330
+ });
1331
+ }
1332
+ if (typeof err?.code === "number" && err.code >= 400 && err.code < 600) {
1333
+ const detail = readErrorDescription(message);
1334
+ const exitCode = exitCodeForStatus(err.code);
1335
+ return new CliError({
1336
+ code: err.code === 404 ? "MCP_ENDPOINT_NOT_FOUND" : err.code === 403 ? "FORBIDDEN" : "MCP_HTTP_ERROR",
1337
+ message: err.code === 404 ? `No MCP endpoint at ${ctx.mcpUrl}` : `MCP request failed (HTTP ${err.code})${detail ? `: ${detail}` : ""}`,
1338
+ hint: err.code === 404 ? "Check the API URL with `tabbio status`." : void 0,
1339
+ exitCode,
1340
+ status: err.code,
1341
+ cause: error
1342
+ });
1343
+ }
1344
+ if (err?.name === "McpError" && typeof err.code === "number") {
1345
+ if (err.code === -32001) {
1346
+ return new CliError({
1347
+ code: "TIMEOUT",
1348
+ message: "The Tabbio tool call timed out",
1349
+ hint: "Long-running workflows may still finish; check `tabbio approvals` or the app.",
1350
+ exitCode: ExitCode.Network,
1351
+ cause: error
1352
+ });
1353
+ }
1354
+ if (err.code === -32e3) return networkError(ctx.mcpUrl, error);
1355
+ if (err.code === -32602) {
1356
+ return new CliError({ code: "VALIDATION_ERROR", message, exitCode: ExitCode.Usage, cause: error });
1357
+ }
1358
+ return new CliError({ code: "MCP_ERROR", message, exitCode: ExitCode.Error, cause: error });
1359
+ }
1360
+ if (err?.name === "TypeError" || err?.name === "AbortError" || err?.name === "TimeoutError") {
1361
+ return networkError(ctx.mcpUrl, error);
1362
+ }
1363
+ return new CliError({ code: "MCP_ERROR", message, exitCode: ExitCode.Error, cause: error });
1364
+ }
1365
+
1366
+ // src/core/mcp.ts
1367
+ var CATALOG_TTL_MS = 60 * 60 * 1e3;
1368
+ var TOOL_TIMEOUT_MS = 2 * 60 * 1e3;
1369
+ var LONG_TOOL_TIMEOUT_MS = 10 * 60 * 1e3;
1370
+ function sha(value, length = 16) {
1371
+ return createHash("sha256").update(value).digest("hex").slice(0, length);
1372
+ }
1373
+ function selectMcpBearer(creds) {
1374
+ if (!creds) return null;
1375
+ const personal = (token, source) => ({
1376
+ kind: "personal-token",
1377
+ source,
1378
+ token,
1379
+ fingerprint: fingerprint(token),
1380
+ cacheIdentity: `personal-token:${sha(token)}`
1381
+ });
1382
+ const session = (token, source) => ({
1383
+ kind: "app-session",
1384
+ source,
1385
+ token,
1386
+ fingerprint: fingerprint(token),
1387
+ // The JWT rotates daily; the user id keeps the cache stable across refreshes.
1388
+ cacheIdentity: `app-session:${creds.user?.id && source === "store" ? creds.user.id : sha(token)}`
1389
+ });
1390
+ if (creds.mcpToken && creds.origin?.mcpToken === "env") return personal(creds.mcpToken, "env");
1391
+ if (creds.accessToken && creds.origin?.accessToken === "env") return session(creds.accessToken, "env");
1392
+ if (creds.accessToken) return session(creds.accessToken, "store");
1393
+ if (creds.mcpToken) return personal(creds.mcpToken, "store");
1394
+ return null;
1395
+ }
1396
+ function describeMcpBearer(creds) {
1397
+ const bearer = selectMcpBearer(creds);
1398
+ if (!bearer) return null;
1399
+ const { token: _token, ...rest } = bearer;
1400
+ return rest;
1401
+ }
1402
+ function cachePrefix(profile) {
1403
+ return `catalog-${profile.name}-`;
1404
+ }
1405
+ function catalogCacheFile(profile, identity) {
1406
+ return join4(configPaths().cacheDir, `${cachePrefix(profile)}${sha(`${profile.mcpUrl}
1407
+ ${identity}`)}.json`);
1408
+ }
1409
+ function catalogCachePath(profile, creds) {
1410
+ const bearer = selectMcpBearer(creds);
1411
+ return bearer ? catalogCacheFile(profile, bearer.cacheIdentity) : null;
1412
+ }
1413
+ function readCatalogCache(file) {
1414
+ try {
1415
+ const raw = readFileIfExists(file);
1416
+ if (!raw) return null;
1417
+ const parsed = JSON.parse(raw);
1418
+ return parsed?.version === 1 && Array.isArray(parsed.tools) ? parsed : null;
1419
+ } catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ function readCachedCatalog(profile, creds) {
1424
+ const file = catalogCachePath(profile, creds);
1425
+ const cache = file ? readCatalogCache(file) : null;
1426
+ if (!cache) return null;
1427
+ const fetchedAt = new Date(cache.fetchedAt);
1428
+ const ageMs = Date.now() - fetchedAt.getTime();
1429
+ return { fetchedAt, tools: buildCatalog(cache.tools), ageMs, fresh: ageMs < CATALOG_TTL_MS, kind: cache.kind };
1430
+ }
1431
+ function clearCatalogCache(profile, keep) {
1432
+ const { cacheDir } = configPaths();
1433
+ try {
1434
+ for (const file of readdirSync(cacheDir)) {
1435
+ if (file.startsWith(cachePrefix(profile)) && file !== keep) rmSync2(join4(cacheDir, file), { force: true });
1436
+ }
1437
+ } catch {
1438
+ }
1439
+ }
1440
+ function describeRpcBody(body) {
1441
+ if (typeof body !== "string") return "";
1442
+ try {
1443
+ const message = JSON.parse(body);
1444
+ return [message.method, message.params?.name].filter(Boolean).join(" ");
1445
+ } catch {
1446
+ return "";
1447
+ }
1448
+ }
1449
+ function authedFetch(inner, getToken, refresh) {
1450
+ return async (input, init) => {
1451
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1452
+ const method = (init?.method ?? "GET").toUpperCase();
1453
+ const send2 = async (token) => {
1454
+ const headers = new Headers(init?.headers);
1455
+ headers.set("authorization", `Bearer ${token}`);
1456
+ const started = Date.now();
1457
+ debug(`\u2192 ${method} ${redactUrl(url)} ${describeRpcBody(init?.body)}`.trimEnd());
1458
+ const response2 = await inner(input, { ...init, headers });
1459
+ const requestId = readRequestId(response2.headers);
1460
+ debug(`\u2190 ${response2.status} ${method} ${redactUrl(url)} ${Date.now() - started}ms${requestId ? ` req=${requestId}` : ""}`);
1461
+ return response2;
1462
+ };
1463
+ const response = await send2(await getToken());
1464
+ if (response.status !== 401 || !refresh) return response;
1465
+ await response.body?.cancel().catch(() => void 0);
1466
+ debug("MCP returned 401 for the app session; refreshing once");
1467
+ return send2(await refresh());
1468
+ };
1469
+ }
1470
+ var McpSession = class _McpSession {
1471
+ constructor(profile, bearer, client) {
1472
+ this.profile = profile;
1473
+ this.bearer = bearer;
1474
+ this.client = client;
1475
+ }
1476
+ catalog = null;
1477
+ rawTools = null;
1478
+ static async connect(profile, creds, opts = {}) {
1479
+ const selected = selectMcpBearer(creds);
1480
+ if (!selected) throw notSignedInError(profile.name);
1481
+ const { token: initialToken, ...bearer } = selected;
1482
+ let getToken = async () => initialToken;
1483
+ let refresh = null;
1484
+ if (bearer.kind === "app-session") {
1485
+ const api = opts.api ?? new ApiClient(profile, creds, { fetch: opts.fetch });
1486
+ getToken = () => api.ensureAccessToken();
1487
+ if (api.credentials?.refreshToken) refresh = () => api.refresh();
1488
+ }
1489
+ const transport = new StreamableHTTPClientTransport(new URL(profile.mcpUrl), {
1490
+ requestInit: { headers: baseHeaders() },
1491
+ fetch: authedFetch(opts.fetch ?? ((...args) => fetch(...args)), getToken, refresh)
1492
+ });
1493
+ const client = new Client({ name: opts.clientName ?? "tabbio-cli", version: CLI_VERSION }, { capabilities: {} });
1494
+ try {
1495
+ await client.connect(transport);
1496
+ } catch (error) {
1497
+ await client.close().catch(() => void 0);
1498
+ throw mapMcpTransportError(error, { mcpUrl: profile.mcpUrl, profile: profile.name, kind: bearer.kind });
1499
+ }
1500
+ return new _McpSession(profile, bearer, client);
1501
+ }
1502
+ /** Server-provided instructions (forwarded by `mcp serve`). */
1503
+ get instructions() {
1504
+ return this.client.getInstructions();
1505
+ }
1506
+ get serverVersion() {
1507
+ return this.client.getServerVersion();
1508
+ }
1509
+ /** Raw MCP tools, from the 1h on-disk cache unless `refresh` is set. */
1510
+ async listRawTools(opts = {}) {
1511
+ if (this.rawTools && !opts.refresh) return this.rawTools;
1512
+ const file = catalogCacheFile(this.profile, this.bearer.cacheIdentity);
1513
+ if (!opts.refresh) {
1514
+ const cache = readCatalogCache(file);
1515
+ if (cache && cache.mcpUrl === this.profile.mcpUrl && Date.now() - Date.parse(cache.fetchedAt) < CATALOG_TTL_MS) {
1516
+ debug(`catalog cache hit (${cache.tools.length} tools)`);
1517
+ this.rawTools = cache.tools;
1518
+ return cache.tools;
1519
+ }
1520
+ }
1521
+ const tools = [];
1522
+ let cursor;
1523
+ try {
1524
+ do {
1525
+ const page = await this.client.listTools(cursor ? { cursor } : {});
1526
+ tools.push(...page.tools);
1527
+ cursor = page.nextCursor;
1528
+ } while (cursor);
1529
+ } catch (error) {
1530
+ throw this.mapError(error);
1531
+ }
1532
+ this.rawTools = tools;
1533
+ this.catalog = null;
1534
+ try {
1535
+ const cache = {
1536
+ version: 1,
1537
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
1538
+ mcpUrl: this.profile.mcpUrl,
1539
+ kind: this.bearer.kind,
1540
+ tools
1541
+ };
1542
+ writeFileAtomic(file, JSON.stringify(cache));
1543
+ clearCatalogCache(this.profile, file.split(/[\\/]/).pop());
1544
+ } catch (error) {
1545
+ debug(`could not write catalog cache: ${error.message}`);
1546
+ }
1547
+ return tools;
1548
+ }
1549
+ async listTools(opts = {}) {
1550
+ if (this.catalog && !opts.refresh) return this.catalog;
1551
+ this.catalog = buildCatalog(await this.listRawTools(opts));
1552
+ return this.catalog;
1553
+ }
1554
+ /** Resolves any accepted tool reference (id, MCP name, command path). */
1555
+ async resolveTool(ref) {
1556
+ return findTool(await this.listTools(), ref);
1557
+ }
1558
+ /**
1559
+ * Calls a tool by id (`cv.list`), MCP name or command path. Approval-gated
1560
+ * tools return `{ ok: true, approval }` instead of executing.
1561
+ */
1562
+ async callTool(id, input, opts = {}) {
1563
+ const tool = await this.resolveTool(id);
1564
+ if (!tool) {
1565
+ return {
1566
+ ok: false,
1567
+ error: new CliError({
1568
+ code: "UNKNOWN_TOOL",
1569
+ message: `Unknown tool: ${id}`,
1570
+ hint: "Run `tabbio tools` to list tools, or `tabbio tools --refresh` to reload the catalog.",
1571
+ exitCode: ExitCode.NotFound
1572
+ })
1573
+ };
1574
+ }
1575
+ const long = tool.kind !== "tool";
1576
+ const result = await this.callRaw(
1577
+ { name: tool.mcpName, arguments: input },
1578
+ { ...opts, timeoutMs: opts.timeoutMs ?? (long ? LONG_TOOL_TIMEOUT_MS : TOOL_TIMEOUT_MS) }
1579
+ );
1580
+ return interpretToolResult(result, tool.id);
1581
+ }
1582
+ /** Raw `tools/call` passthrough (used by the stdio bridge). Throws CliError on transport failure. */
1583
+ async callRaw(params, opts = {}) {
1584
+ try {
1585
+ const result = await this.client.callTool(params, void 0, {
1586
+ timeout: opts.timeoutMs ?? TOOL_TIMEOUT_MS,
1587
+ resetTimeoutOnProgress: true,
1588
+ ...opts.signal ? { signal: opts.signal } : {},
1589
+ ...opts.onProgress ? { onprogress: opts.onProgress } : {}
1590
+ });
1591
+ return result;
1592
+ } catch (error) {
1593
+ throw this.mapError(error);
1594
+ }
1595
+ }
1596
+ async close() {
1597
+ await this.client.close().catch(() => void 0);
1598
+ }
1599
+ mapError(error) {
1600
+ return mapMcpTransportError(error, { mcpUrl: this.profile.mcpUrl, profile: this.profile.name, kind: this.bearer.kind });
1601
+ }
1602
+ };
1603
+ function assertJwtSession(creds, profile, feature) {
1604
+ if (creds?.accessToken) return;
1605
+ if (creds?.mcpToken) throw needsFullSignInError(feature);
1606
+ throw notSignedInError(profile.name);
1607
+ }
1608
+
1609
+ // src/core/loopback.ts
1610
+ import { randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
1611
+ import { createServer } from "node:http";
1612
+ var STATE_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
1613
+ var CODE_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
1614
+ var LOOPBACK_HOST = "127.0.0.1";
1615
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
1616
+ function generateState() {
1617
+ return randomBytes2(32).toString("base64url");
1618
+ }
1619
+ function safeEqual(a, b) {
1620
+ const left = Buffer.from(a);
1621
+ const right = Buffer.from(b);
1622
+ return left.length === right.length && timingSafeEqual(left, right);
1623
+ }
1624
+ function validateLoopbackCallback(url, expectedState) {
1625
+ if (url.pathname !== "/callback") {
1626
+ return { ok: false, status: 404, reason: "Not found", fatal: false };
1627
+ }
1628
+ const state = url.searchParams.get("state") ?? "";
1629
+ if (!STATE_PATTERN.test(state) || !safeEqual(state, expectedState)) {
1630
+ return { ok: false, status: 400, reason: "This sign-in link does not match the waiting terminal.", fatal: false };
1631
+ }
1632
+ const error = url.searchParams.get("error");
1633
+ if (error) {
1634
+ return {
1635
+ ok: false,
1636
+ status: 200,
1637
+ reason: error === "access_denied" ? "Connection was declined in the browser." : `Connection failed: ${error.slice(0, 80)}`,
1638
+ fatal: true
1639
+ };
1640
+ }
1641
+ const code = url.searchParams.get("code") ?? "";
1642
+ if (!CODE_PATTERN.test(code)) {
1643
+ return { ok: false, status: 400, reason: "The connect code is missing or malformed.", fatal: false };
1644
+ }
1645
+ return { ok: true, code };
1646
+ }
1647
+ function paletteCss() {
1648
+ const vars = (p) => Object.entries(p).map(([key, value]) => `--${key}:${value}`).join(";");
1649
+ const { light, dark, accent, onAccent } = WEB_PAGE_PALETTE;
1650
+ return `:root{${vars(light)};--accent:${accent};--on-accent:${onAccent}}
1651
+ @media (prefers-color-scheme:dark){:root{${vars(dark)}}}`;
1652
+ }
1653
+ function escapeHtml(value) {
1654
+ return value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
1655
+ }
1656
+ function renderLoopbackPage(kind, message) {
1657
+ const title = kind === "success" ? "Tabbio CLI connected" : "Tabbio CLI sign-in";
1658
+ const heading2 = kind === "success" ? "You are signed in to the Tabbio CLI" : "Sign-in did not finish";
1659
+ const mark = kind === "success" ? "&#10003;" : "!";
1660
+ return `<!doctype html>
1661
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1662
+ <meta name="referrer" content="no-referrer"><title>${title}</title>
1663
+ <style>
1664
+ ${paletteCss()}
1665
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);
1666
+ font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;padding:24px}
1667
+ main{max-width:420px;width:100%;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:32px;text-align:start}
1668
+ .mark{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:var(--accent);color:var(--on-accent);font-weight:700;margin-bottom:16px}
1669
+ h1{font-size:20px;margin:0 0 8px}p{margin:0;color:var(--muted)}.brand{font-weight:600;color:var(--accent);margin-bottom:24px;font-size:14px}
1670
+ </style></head>
1671
+ <body><main><div class="brand">tabbio</div><div class="mark">${mark}</div><h1>${escapeHtml(heading2)}</h1>
1672
+ <p dir="auto">${escapeHtml(message)}</p></main></body></html>`;
1673
+ }
1674
+ function send(res, status, html) {
1675
+ res.writeHead(status, {
1676
+ "content-type": "text/html; charset=utf-8",
1677
+ "cache-control": "no-store",
1678
+ "referrer-policy": "no-referrer",
1679
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
1680
+ "x-content-type-options": "nosniff",
1681
+ connection: "close"
1682
+ });
1683
+ res.end(html);
1684
+ }
1685
+ async function startLoopbackServer(opts) {
1686
+ let settle;
1687
+ let outcome;
1688
+ const finish = (result) => {
1689
+ if (outcome) return;
1690
+ outcome = result;
1691
+ if (settle) {
1692
+ if (result.code) settle.resolve(result.code);
1693
+ else settle.reject(result.error ?? new Error("Login failed"));
1694
+ }
1695
+ };
1696
+ let port = 0;
1697
+ let pending;
1698
+ const server = createServer((req, res) => {
1699
+ if (req.headers.host !== `${LOOPBACK_HOST}:${port}`) {
1700
+ return send(res, 400, renderLoopbackPage("error", "Unexpected host."));
1701
+ }
1702
+ if (req.method !== "GET") return send(res, 405, renderLoopbackPage("error", "Method not allowed."));
1703
+ const url = new URL(req.url ?? "/", `http://${LOOPBACK_HOST}:${port}`);
1704
+ if (outcome) {
1705
+ return send(res, 410, renderLoopbackPage("error", "This sign-in already finished. Return to your terminal."));
1706
+ }
1707
+ const result = validateLoopbackCallback(url, opts.state);
1708
+ if (result.ok) {
1709
+ pending = res;
1710
+ finish({ code: result.code });
1711
+ return;
1712
+ }
1713
+ send(res, result.status, renderLoopbackPage("error", result.reason));
1714
+ if (result.fatal) {
1715
+ finish({ error: new CliError({ code: "LOGIN_DECLINED", message: result.reason, exitCode: ExitCode.Auth }) });
1716
+ }
1717
+ });
1718
+ await new Promise((resolve, reject) => {
1719
+ server.once("error", reject);
1720
+ server.listen(0, LOOPBACK_HOST, () => {
1721
+ server.off("error", reject);
1722
+ resolve();
1723
+ });
1724
+ });
1725
+ port = server.address().port;
1726
+ const timeoutMs = opts.timeoutMs ?? LOGIN_TIMEOUT_MS;
1727
+ const timer = setTimeout(() => {
1728
+ finish({
1729
+ error: new CliError({
1730
+ code: "LOGIN_TIMEOUT",
1731
+ message: `Timed out after ${Math.round(timeoutMs / 6e4) || 1} min waiting for the browser`,
1732
+ hint: "Run `tabbio login` again, or use `tabbio login --email you@example.com` on a machine without a browser.",
1733
+ exitCode: ExitCode.Auth
1734
+ })
1735
+ });
1736
+ }, timeoutMs);
1737
+ timer.unref();
1738
+ const onAbort = () => finish({ error: interruptedError("Login cancelled") });
1739
+ if (opts.signal?.aborted) onAbort();
1740
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1741
+ const complete = (kind, message) => {
1742
+ if (pending && !pending.writableEnded) send(pending, kind === "success" ? 200 : 400, renderLoopbackPage(kind, message));
1743
+ pending = void 0;
1744
+ };
1745
+ const close = async () => {
1746
+ complete("error", "Return to your terminal to see what happened.");
1747
+ clearTimeout(timer);
1748
+ opts.signal?.removeEventListener("abort", onAbort);
1749
+ server.closeAllConnections?.();
1750
+ await new Promise((resolve) => server.close(() => resolve()));
1751
+ };
1752
+ return {
1753
+ port,
1754
+ waitForCode: () => new Promise((resolve, reject) => {
1755
+ if (!outcome) {
1756
+ settle = { resolve, reject };
1757
+ return;
1758
+ }
1759
+ if (outcome.code) resolve(outcome.code);
1760
+ else reject(outcome.error ?? new Error("Login failed"));
1761
+ }),
1762
+ complete,
1763
+ close
1764
+ };
1765
+ }
1766
+
1767
+ // src/core/auth.ts
1768
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1769
+ var OTP_MAX_ATTEMPTS = 3;
1770
+ function hasDisplay(env3 = process.env, platform = process.platform) {
1771
+ if (env3.SSH_CONNECTION || env3.SSH_TTY || env3.SSH_CLIENT) return false;
1772
+ if (platform === "darwin" || platform === "win32") return true;
1773
+ return Boolean(env3.DISPLAY || env3.WAYLAND_DISPLAY);
1774
+ }
1775
+ function chooseLoginMethod(opts) {
1776
+ const flags = [
1777
+ opts.browser && "--browser",
1778
+ opts.email && "--email",
1779
+ opts.token && "--token",
1780
+ opts.withToken && "--with-token"
1781
+ ].filter(Boolean);
1782
+ if (flags.length > 1) throw usageError(`Choose one of ${flags.join(", ")}`);
1783
+ if (opts.nonInteractive && (opts.email || opts.token || opts.withToken)) {
1784
+ throw usageError("--non-interactive only applies to the browser flow");
1785
+ }
1786
+ if (opts.token || opts.withToken) return "token";
1787
+ if (opts.email) return "email";
1788
+ if (opts.browser || opts.nonInteractive) return "browser";
1789
+ if (opts.isTTY && opts.hasDisplay) return "browser";
1790
+ if (opts.isTTY) return "email";
1791
+ throw usageError(
1792
+ "No login method for a non-interactive session",
1793
+ "Use `tabbio login --token <tabbio_mcp_\u2026>` or set TABBIO_TOKEN / TABBIO_ACCESS_TOKEN."
1794
+ );
1795
+ }
1796
+ function assertEmail(email) {
1797
+ const value = email.trim().toLowerCase();
1798
+ if (!EMAIL_PATTERN.test(value)) throw usageError(`Not a valid email address: ${email}`);
1799
+ return value;
1800
+ }
1801
+ function buildConnectUrl(appUrl, params) {
1802
+ const url = new URL("/cli/connect", `${appUrl}/`);
1803
+ url.searchParams.set("port", String(params.port));
1804
+ url.searchParams.set("state", params.state);
1805
+ url.searchParams.set("device", params.device.slice(0, 64));
1806
+ return url.toString();
1807
+ }
1808
+ function cookieHeaderFromSetCookie(values) {
1809
+ return values.map((value) => value.split(";", 1)[0]?.trim() ?? "").filter((pair) => pair.includes("=") && !pair.endsWith("=")).join("; ");
1810
+ }
1811
+ function toUser(user) {
1812
+ return { id: user.id, email: user.email, name: user.name ?? null };
1813
+ }
1814
+ function assertTokenPayload(payload) {
1815
+ if (!payload?.accessToken || !payload.user?.id) {
1816
+ throw new CliError({ code: "BAD_RESPONSE", message: "Sign-in returned no session", exitCode: ExitCode.Server });
1817
+ }
1818
+ if (!payload.refreshToken) {
1819
+ throw new CliError({
1820
+ code: "NO_REFRESH_TOKEN",
1821
+ message: "Sign-in returned no refresh token",
1822
+ hint: "A proxy may be stripping the x-tabbio-client-platform header.",
1823
+ exitCode: ExitCode.Server
1824
+ });
1825
+ }
1826
+ return payload;
1827
+ }
1828
+ async function exchangeGrantCode(api, code) {
1829
+ const payload = await api.json("/api/auth/tokens/extension-exchange", {
1830
+ method: "POST",
1831
+ body: { code },
1832
+ auth: "none"
1833
+ });
1834
+ return assertTokenPayload(payload);
1835
+ }
1836
+ async function loginWithBrowser(api, opts) {
1837
+ const state = generateState();
1838
+ const timeoutMs = opts.timeoutMs ?? LOGIN_TIMEOUT_MS;
1839
+ const server = await startLoopbackServer({ state, timeoutMs, signal: opts.signal });
1840
+ const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
1841
+ try {
1842
+ const url = buildConnectUrl(api.profile.appUrl, { port: server.port, state, device: opts.device });
1843
+ const opened = await opts.openUrl(url).catch(() => false);
1844
+ opts.onWaiting?.(url, opened, { state, port: server.port, expiresAt });
1845
+ const code = await server.waitForCode();
1846
+ debug("received connect code on loopback");
1847
+ try {
1848
+ const payload = await exchangeGrantCode(api, code);
1849
+ server.complete("success", "You can close this tab and return to your terminal.");
1850
+ return payload;
1851
+ } catch (error) {
1852
+ server.complete("error", "The connection could not be completed. Check your terminal.");
1853
+ throw error;
1854
+ }
1855
+ } finally {
1856
+ await server.close();
1857
+ }
1858
+ }
1859
+ async function sendEmailOtp(api, email) {
1860
+ const response = await api.raw("/api/auth/email-otp/send-verification-otp", {
1861
+ method: "POST",
1862
+ auth: "none",
1863
+ headers: { "content-type": "application/json", accept: "application/json" },
1864
+ body: JSON.stringify({ email, type: "sign-in" })
1865
+ });
1866
+ await parseJsonResponse(response);
1867
+ }
1868
+ async function signInWithEmailOtp(api, email, otp) {
1869
+ const response = await api.raw("/api/auth/sign-in/email-otp", {
1870
+ method: "POST",
1871
+ auth: "none",
1872
+ headers: { "content-type": "application/json", accept: "application/json" },
1873
+ body: JSON.stringify({ email, otp })
1874
+ });
1875
+ const cookies = response.ok ? response.headers.getSetCookie() : [];
1876
+ await parseJsonResponse(response);
1877
+ const cookie = cookieHeaderFromSetCookie(cookies);
1878
+ if (!cookie) {
1879
+ throw new CliError({ code: "BAD_RESPONSE", message: "Sign-in returned no session cookie", exitCode: ExitCode.Server });
1880
+ }
1881
+ return cookie;
1882
+ }
1883
+ async function exchangeSessionCookie(api, cookie) {
1884
+ const payload = await api.json("/api/auth/tokens/exchange", {
1885
+ method: "POST",
1886
+ auth: "none",
1887
+ headers: { cookie }
1888
+ });
1889
+ return assertTokenPayload(payload);
1890
+ }
1891
+ async function loginWithEmailOtp(api, opts) {
1892
+ const email = assertEmail(opts.email);
1893
+ await sendEmailOtp(api, email);
1894
+ for (let attempt = 1; attempt <= OTP_MAX_ATTEMPTS; attempt += 1) {
1895
+ const otp = (await opts.promptCode(attempt)).replace(/\s+/g, "");
1896
+ if (!otp) throw usageError("No code entered");
1897
+ try {
1898
+ const cookie = await signInWithEmailOtp(api, email, otp);
1899
+ return await exchangeSessionCookie(api, cookie);
1900
+ } catch (error) {
1901
+ if (!(error instanceof CliError)) throw error;
1902
+ const code = error.code.toUpperCase();
1903
+ if (code === "INVALID_OTP" && attempt < OTP_MAX_ATTEMPTS) {
1904
+ opts.onInvalidCode?.(OTP_MAX_ATTEMPTS - attempt);
1905
+ continue;
1906
+ }
1907
+ if (code === "INVALID_OTP" || code === "TOO_MANY_ATTEMPTS" || code === "OTP_EXPIRED") {
1908
+ throw new CliError({
1909
+ code,
1910
+ message: code === "OTP_EXPIRED" ? "That code expired" : "Too many wrong codes",
1911
+ hint: "Run `tabbio login --email` again to get a new code.",
1912
+ exitCode: ExitCode.Auth,
1913
+ requestId: error.requestId
1914
+ });
1915
+ }
1916
+ throw error;
1917
+ }
1918
+ }
1919
+ throw new CliError({ code: "TOO_MANY_ATTEMPTS", message: "Too many wrong codes", exitCode: ExitCode.Auth });
1920
+ }
1921
+ function getMcpAccessState(api) {
1922
+ return api.json("/api/integrations/mcp");
1923
+ }
1924
+ async function revokeSession(api, refreshToken) {
1925
+ await api.json("/api/auth/tokens/logout", { method: "POST", body: { refreshToken }, auth: "none" });
1926
+ }
1927
+ function credentialsFromLogin(profile, payload) {
1928
+ return {
1929
+ profile: profile.name,
1930
+ accessToken: payload.accessToken,
1931
+ accessTokenExpiresAt: payload.accessTokenExpiresAt,
1932
+ refreshToken: payload.refreshToken,
1933
+ user: toUser(payload.user)
1934
+ };
1935
+ }
1936
+ async function logoutProfile(profile, creds, opts = {}) {
1937
+ const warnings = [];
1938
+ let revokedSession = false;
1939
+ const refreshToken = creds?.origin?.accessToken === "env" ? void 0 : creds?.refreshToken;
1940
+ if (refreshToken) {
1941
+ try {
1942
+ await revokeSession(new ApiClient(profile, null, { fetch: opts.fetch, persist: false }), refreshToken);
1943
+ revokedSession = true;
1944
+ } catch (error) {
1945
+ warnings.push(`Could not end the session on the server: ${error.message}`);
1946
+ }
1947
+ }
1948
+ clearCredentials(profile.name);
1949
+ clearCatalogCache(profile);
1950
+ return { warnings, revokedSession };
1951
+ }
1952
+
1953
+ export {
1954
+ ExitCode,
1955
+ EXIT_CODE_DOCS,
1956
+ CliError,
1957
+ isCliError,
1958
+ notSignedInError,
1959
+ interruptedError,
1960
+ usageError,
1961
+ networkError,
1962
+ toCliError,
1963
+ writeOut,
1964
+ printJson,
1965
+ printKeyValues,
1966
+ successLine,
1967
+ heading,
1968
+ relativeTime,
1969
+ DEFAULT_PROFILE,
1970
+ PROFILE_PRESETS,
1971
+ configPaths,
1972
+ loadConfig,
1973
+ updateConfig,
1974
+ assertProfileName,
1975
+ normalizeBaseUrl,
1976
+ resolveProfileWithSources,
1977
+ resolveProfile,
1978
+ listProfileNames,
1979
+ loadStoredCredentials,
1980
+ loadCredentials,
1981
+ saveCredentials,
1982
+ listCredentialProfiles,
1983
+ credentialPermissionIssues,
1984
+ fingerprint,
1985
+ redactSecrets,
1986
+ setGlobalOptions,
1987
+ getGlobalOptions,
1988
+ isInteractive,
1989
+ deviceLabel,
1990
+ debug,
1991
+ info,
1992
+ warn,
1993
+ CLI_VERSION,
1994
+ CLI_PACKAGE_NAME,
1995
+ checkForUpdate,
1996
+ readRequestId,
1997
+ ApiClient,
1998
+ parseJsonResponse,
1999
+ PRIMARY_AGENT_KEY,
2000
+ kebabCase,
2001
+ findTool,
2002
+ groupCatalog,
2003
+ interpretToolPayload,
2004
+ selectMcpBearer,
2005
+ describeMcpBearer,
2006
+ readCachedCatalog,
2007
+ clearCatalogCache,
2008
+ McpSession,
2009
+ assertJwtSession,
2010
+ hasDisplay,
2011
+ chooseLoginMethod,
2012
+ loginWithBrowser,
2013
+ loginWithEmailOtp,
2014
+ getMcpAccessState,
2015
+ revokeSession,
2016
+ credentialsFromLogin,
2017
+ logoutProfile
2018
+ };
2019
+ //# sourceMappingURL=chunk-7LNC3FIV.js.map