@fabricorg/cli 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fabric.js CHANGED
@@ -6,7 +6,7 @@ import { Command as Command23 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@fabricorg/cli",
9
- version: "0.7.4",
9
+ version: "0.8.0",
10
10
  description: "CLI for the Fabric AI platform",
11
11
  keywords: [
12
12
  "fabric",
@@ -57,7 +57,7 @@ var package_default = {
57
57
  tsup: "^8.0.0",
58
58
  tsx: "^4.21.0",
59
59
  typescript: "5.9.3",
60
- vitest: "^4.1.11"
60
+ vitest: "^4.1.12"
61
61
  }
62
62
  };
63
63
 
@@ -2402,7 +2402,7 @@ function getDefaultContext() {
2402
2402
  return profileFor().defaultContext;
2403
2403
  }
2404
2404
  function getOutputFormat() {
2405
- if (process.env.FABRIC_FORMAT === "json" || process.env.FABRIC_FORMAT === "yaml" || process.env.FABRIC_FORMAT === "csv") {
2405
+ if (process.env.FABRIC_FORMAT === "json" || process.env.FABRIC_FORMAT === "yaml" || process.env.FABRIC_FORMAT === "csv" || process.env.FABRIC_FORMAT === "plain" || process.env.FABRIC_FORMAT === "quiet") {
2406
2406
  return process.env.FABRIC_FORMAT;
2407
2407
  }
2408
2408
  return getConfig().defaultFormat;
@@ -3467,8 +3467,34 @@ async function revokeOAuthSession(credentials, fetchImpl = (input, init) => fetc
3467
3467
 
3468
3468
  // src/lib/output.ts
3469
3469
  import Table from "cli-table3";
3470
+ var OUTPUT_FORMATS = /* @__PURE__ */ new Set([
3471
+ "table",
3472
+ "json",
3473
+ "yaml",
3474
+ "csv",
3475
+ "plain",
3476
+ "quiet"
3477
+ ]);
3478
+ function resolveOutputFormat(format) {
3479
+ return format && OUTPUT_FORMATS.has(format) ? format : getOutputFormat();
3480
+ }
3481
+ function shouldDisableColor(env2 = process.env, isTTY = Boolean(process.stdout?.isTTY)) {
3482
+ if (env2.NO_COLOR !== void 0 && env2.NO_COLOR !== "") {
3483
+ return true;
3484
+ }
3485
+ if (env2.FABRIC_NO_COLOR === "1" || env2.FABRIC_NO_COLOR === "true") {
3486
+ return true;
3487
+ }
3488
+ if (env2.TERM === "dumb") {
3489
+ return true;
3490
+ }
3491
+ if (!isTTY) {
3492
+ return true;
3493
+ }
3494
+ return false;
3495
+ }
3470
3496
  function printOutput(data, opts = {}) {
3471
- const format = opts.format ?? getOutputFormat();
3497
+ const format = resolveOutputFormat(opts.format);
3472
3498
  if (format === "json") {
3473
3499
  process.stdout.write(`${JSON.stringify(data, null, 2)}
3474
3500
  `);
@@ -3491,6 +3517,20 @@ function printOutput(data, opts = {}) {
3491
3517
  }
3492
3518
  return;
3493
3519
  }
3520
+ if (format === "plain") {
3521
+ const rows2 = Array.isArray(data) ? data : [data];
3522
+ if (rows2.length === 0) {
3523
+ return;
3524
+ }
3525
+ const cols2 = opts.columns ?? Object.keys(rows2[0]);
3526
+ for (const row of rows2) {
3527
+ process.stdout.write(
3528
+ `${cols2.map((c) => formatCell(row[c])).join(" ")}
3529
+ `
3530
+ );
3531
+ }
3532
+ return;
3533
+ }
3494
3534
  if (format === "csv") {
3495
3535
  const rows2 = Array.isArray(data) ? data : [data];
3496
3536
  if (rows2.length === 0) {
@@ -3517,7 +3557,14 @@ function printOutput(data, opts = {}) {
3517
3557
  return;
3518
3558
  }
3519
3559
  const cols = opts.columns ?? Object.keys(rows[0]);
3520
- const table = new Table({ head: cols, style: { head: ["cyan"] } });
3560
+ const noColor = shouldDisableColor();
3561
+ const table = new Table({
3562
+ head: cols,
3563
+ style: {
3564
+ head: noColor ? [] : ["cyan"],
3565
+ border: noColor ? [] : void 0
3566
+ }
3567
+ });
3521
3568
  for (const row of rows) {
3522
3569
  table.push(
3523
3570
  cols.map((c) => formatCell(row[c]))
@@ -3527,16 +3574,18 @@ function printOutput(data, opts = {}) {
3527
3574
  `);
3528
3575
  }
3529
3576
  function printRecord(data, opts = {}) {
3530
- const format = opts.format ?? getOutputFormat();
3531
- if (format === "json") {
3577
+ const format = resolveOutputFormat(opts.format);
3578
+ if (format === "json" || format === "yaml" || format === "plain") {
3532
3579
  printOutput(data, opts);
3533
3580
  return;
3534
3581
  }
3535
- if (format === "yaml") {
3536
- printOutput(data, opts);
3537
- return;
3538
- }
3539
- const table = new Table({ style: { head: ["cyan"] } });
3582
+ const noColor = shouldDisableColor();
3583
+ const table = new Table({
3584
+ style: {
3585
+ head: noColor ? [] : ["cyan"],
3586
+ border: noColor ? [] : void 0
3587
+ }
3588
+ });
3540
3589
  for (const [key, val] of Object.entries(data)) {
3541
3590
  table.push({ [key]: formatCell(val) });
3542
3591
  }
@@ -4138,13 +4187,17 @@ function takeUpgradeNotice() {
4138
4187
  }
4139
4188
 
4140
4189
  // src/lib/client.ts
4141
- function getClient(overrides = {}) {
4190
+ function resolveClientOrigin(overrides) {
4142
4191
  const origin = normalizeOrigin(
4143
4192
  overrides.baseUrl ?? getBaseUrl() ?? DEFAULT_ORIGIN
4144
4193
  );
4145
4194
  if (origin === null) {
4146
4195
  printError(BAD_BASE_URL_LINE, 2);
4147
4196
  }
4197
+ return origin;
4198
+ }
4199
+ function resolveClientAuth(overrides) {
4200
+ const origin = resolveClientOrigin(overrides);
4148
4201
  const projectOAuth = overrides.project === void 0 || process.env.FABRIC_API_KEY ? void 0 : getOAuth(origin, overrides.project);
4149
4202
  const apiKey = projectOAuth ? void 0 : hasStoredApiKey(origin) ? getApiKey(origin) : void 0;
4150
4203
  const oauth = projectOAuth ?? (apiKey ? void 0 : getOAuth(origin));
@@ -4157,6 +4210,25 @@ or, for CI:
4157
4210
  3
4158
4211
  );
4159
4212
  }
4213
+ return { origin, apiKey, oauth, projectOAuth };
4214
+ }
4215
+ function getMcpAuthenticatedFetch(overrides = {}) {
4216
+ const origin = resolveClientOrigin(overrides);
4217
+ const apiKey = hasStoredApiKey(origin) ? getApiKey(origin) : void 0;
4218
+ if (!apiKey) {
4219
+ printError(
4220
+ "Browser sign-in tokens are not valid for the MCP Gateway. Set FABRIC_API_KEY or run fabric auth login --key <api-key> before using fabric mcp tools or fabric mcp call.",
4221
+ 3
4222
+ );
4223
+ }
4224
+ return {
4225
+ origin,
4226
+ fetch: globalThis.fetch,
4227
+ authorizationHeader: `Bearer ${apiKey}`
4228
+ };
4229
+ }
4230
+ function getClient(overrides = {}) {
4231
+ const { origin, apiKey, oauth, projectOAuth } = resolveClientAuth(overrides);
4160
4232
  try {
4161
4233
  return new FabricClient({
4162
4234
  // A browser sign-in's token is refreshed per request by the fetch
@@ -4228,11 +4300,7 @@ function resolveContext(personal, org, required = true) {
4228
4300
  // src/commands/agents/index.ts
4229
4301
  function buildAgentsCommand() {
4230
4302
  const agents = new Command("agents").description("Manage agents");
4231
- agents.command("list").description("List agents").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--status <status>", "Filter: ACTIVE|INACTIVE|DEPRECATED").option("--limit <n>", "Max results", "50").option(
4232
- "--format <format>",
4233
- "Output format: table|json|yaml|csv",
4234
- "table"
4235
- ).option("-q, --quiet", "Print IDs only").action(
4303
+ agents.command("list").description("List agents").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--status <status>", "Filter: ACTIVE|INACTIVE|DEPRECATED").option("--limit <n>", "Max results", "50").option("--format <format>", "Output format: table|json|yaml|csv").option("-q, --quiet", "Print IDs only").action(
4236
4304
  async (opts) => {
4237
4305
  const ctx = resolveContext(opts.personal, opts.org, false);
4238
4306
  const client = getClient();
@@ -4266,7 +4334,7 @@ function buildAgentsCommand() {
4266
4334
  });
4267
4335
  }
4268
4336
  );
4269
- agents.command("show <id>").description("Show agent details").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
4337
+ agents.command("show <id>").description("Show agent details").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml").action(
4270
4338
  async (id, opts) => {
4271
4339
  const ctx = resolveContext(opts.personal, opts.org, false);
4272
4340
  const client = getClient();
@@ -4278,7 +4346,7 @@ function buildAgentsCommand() {
4278
4346
  } catch (err) {
4279
4347
  printError(err.message, 1);
4280
4348
  }
4281
- const fmt = opts.format;
4349
+ const fmt = resolveOutputFormat(opts.format);
4282
4350
  if (fmt === "json" || fmt === "yaml") {
4283
4351
  printOutput(agent, { format: fmt });
4284
4352
  return;
@@ -4295,7 +4363,7 @@ function buildAgentsCommand() {
4295
4363
  });
4296
4364
  }
4297
4365
  );
4298
- agents.command("execute <id>").description("Execute an agent").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--input <json>", `Input JSON (e.g. '{"key":"value"}')`).option("--format <format>", "Output format: table|json|yaml", "table").action(
4366
+ agents.command("execute <id>").description("Execute an agent").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--input <json>", `Input JSON (e.g. '{"key":"value"}')`).option("--format <format>", "Output format: table|json|yaml").action(
4299
4367
  async (id, opts) => {
4300
4368
  const ctx = resolveContext(opts.personal, opts.org, false);
4301
4369
  const client = getClient();
@@ -4316,7 +4384,7 @@ function buildAgentsCommand() {
4316
4384
  } catch (err) {
4317
4385
  printError(err.message, 1);
4318
4386
  }
4319
- const fmt = opts.format;
4387
+ const fmt = resolveOutputFormat(opts.format);
4320
4388
  if (fmt === "json" || fmt === "yaml") {
4321
4389
  printOutput(result, { format: fmt });
4322
4390
  return;
@@ -4330,6 +4398,209 @@ function buildAgentsCommand() {
4330
4398
  });
4331
4399
  }
4332
4400
  );
4401
+ agents.command("delegate").description(
4402
+ "Delegate a coding task to Fabric Desktop (fabricagents://) or Remote Fabric Agents Server using subscription harnesses"
4403
+ ).option("--org <slug>", "Organization context").option("--project <id>", "Project ID to bind the delegated session to").option("--project-slug <slug>", "Project slug for workspace routing").option(
4404
+ "--target <target>",
4405
+ "Execution target: desktop|remote",
4406
+ "remote"
4407
+ ).option(
4408
+ "--server-url <url>",
4409
+ "Remote Fabric Agents Server URL",
4410
+ process.env.FABRIC_AGENTS_SERVER_URL ?? "https://agents.example.com"
4411
+ ).option(
4412
+ "--harness <harness>",
4413
+ "Subscription harness: auto|claude-max|codex|droid-cli|grok-cli|kimi-cli|antigravity-cli|cursor",
4414
+ "claude-max"
4415
+ ).option("--model <model>", "Optional model override").option(
4416
+ "--fallback <policy>",
4417
+ "Quota fallback policy: subscription_harnesses|org_paid_api|pause_on_limit",
4418
+ "subscription_harnesses"
4419
+ ).option("--prompt <text>", "Task prompt to dispatch").option(
4420
+ "--durable",
4421
+ "Request durable handling when the generated link is opened"
4422
+ ).option("--format <format>", "Output format: table|json|yaml|plain").option("-q, --quiet", "Print delegation URL only").addHelpText(
4423
+ "after",
4424
+ `
4425
+ Examples:
4426
+ $ fabric agents delegate --target remote --harness claude-max --prompt "Refactor auth middleware"
4427
+ $ fabric agents delegate --target desktop --project-slug payments-v2 --prompt "Add unit tests" -q
4428
+ $ fabric agents delegate --target remote --harness codex --fallback org_paid_api --format json
4429
+ `
4430
+ ).action(
4431
+ async (opts) => {
4432
+ const target = opts.target === "desktop" ? "desktop" : "remote";
4433
+ const query = new URLSearchParams();
4434
+ if (opts.projectSlug) {
4435
+ query.set("workspace", opts.projectSlug);
4436
+ query.set("project", opts.projectSlug);
4437
+ }
4438
+ if (opts.project) {
4439
+ query.set("projectId", opts.project);
4440
+ }
4441
+ query.set("harness", opts.harness);
4442
+ if (opts.model) {
4443
+ query.set("model", opts.model);
4444
+ }
4445
+ query.set("fallback", opts.fallback);
4446
+ if (opts.prompt) {
4447
+ query.set("input", opts.prompt);
4448
+ query.set("prompt", opts.prompt);
4449
+ query.set("send", "true");
4450
+ }
4451
+ if (opts.durable) {
4452
+ query.set("durable", "true");
4453
+ }
4454
+ const desktopDeepLink = `fabricagents://action/new-chat?${query.toString()}`;
4455
+ const cleanServer = opts.serverUrl.replace(/\/+$/, "");
4456
+ const remoteDelegationUrl = `${cleanServer}/?${query.toString()}`;
4457
+ const activeUrl = target === "desktop" ? desktopDeepLink : remoteDelegationUrl;
4458
+ if (opts.quiet) {
4459
+ process.stdout.write(`${activeUrl}
4460
+ `);
4461
+ return;
4462
+ }
4463
+ const payload = {
4464
+ status: "link_created",
4465
+ message: "A link was created locally. No task was dispatched by this command.",
4466
+ target,
4467
+ harness: opts.harness,
4468
+ model: opts.model ?? "default",
4469
+ fallbackPolicy: opts.fallback,
4470
+ billingMode: "subscription_tokens",
4471
+ durable: Boolean(opts.durable),
4472
+ projectId: opts.project ?? null,
4473
+ projectSlug: opts.projectSlug ?? null,
4474
+ delegationUrl: activeUrl,
4475
+ desktopDeepLink,
4476
+ remoteDelegationUrl
4477
+ };
4478
+ const fmt = resolveOutputFormat(opts.format);
4479
+ if (fmt === "json" || fmt === "yaml" || fmt === "plain") {
4480
+ printOutput(payload, { format: fmt });
4481
+ return;
4482
+ }
4483
+ printRecord({
4484
+ Status: payload.status,
4485
+ Target: target === "desktop" ? "Fabric Desktop (fabricagents://)" : `Remote Fabric Agents Server (${cleanServer})`,
4486
+ Harness: payload.harness,
4487
+ Model: payload.model,
4488
+ "Fallback Policy": payload.fallbackPolicy,
4489
+ "Billing Mode": "Subscription Credits (0 API Key Spend)",
4490
+ Durable: payload.durable ? "requested in link" : "no",
4491
+ "Delegation URL": payload.delegationUrl
4492
+ });
4493
+ }
4494
+ );
4495
+ agents.command("desktop").description(
4496
+ "Generate a 1-click Fabric Desktop (fabricagents://) deep link for a project or view"
4497
+ ).option("--project <id>", "Project ID").option("--project-slug <slug>", "Project slug").option(
4498
+ "--view <view>",
4499
+ "Target view: allSessions|settings|sources|agents|skills",
4500
+ "allSessions"
4501
+ ).option("--prompt <text>", "Pre-filled prompt for a new chat").option("--format <format>", "Output format: table|json|yaml|plain").option("-q, --quiet", "Print deep link URL only").action(
4502
+ async (opts) => {
4503
+ const query = new URLSearchParams();
4504
+ if (opts.projectSlug) {
4505
+ query.set("workspace", opts.projectSlug);
4506
+ }
4507
+ if (opts.project) {
4508
+ query.set("projectId", opts.project);
4509
+ }
4510
+ if (opts.prompt) {
4511
+ query.set("input", opts.prompt);
4512
+ query.set("send", "true");
4513
+ }
4514
+ const path42 = opts.prompt ? "action/new-chat" : opts.view || "allSessions";
4515
+ const qs = query.toString();
4516
+ const deepLink = `fabricagents://${path42}${qs ? `?${qs}` : ""}`;
4517
+ if (opts.quiet) {
4518
+ process.stdout.write(`${deepLink}
4519
+ `);
4520
+ return;
4521
+ }
4522
+ const payload = {
4523
+ deepLink,
4524
+ view: opts.view,
4525
+ projectId: opts.project ?? null,
4526
+ projectSlug: opts.projectSlug ?? null
4527
+ };
4528
+ const fmt = resolveOutputFormat(opts.format);
4529
+ if (fmt === "json" || fmt === "yaml" || fmt === "plain") {
4530
+ printOutput(payload, { format: fmt });
4531
+ return;
4532
+ }
4533
+ printRecord({
4534
+ "Deep Link": deepLink,
4535
+ View: opts.view,
4536
+ Project: opts.projectSlug ?? opts.project ?? "(workspace)"
4537
+ });
4538
+ }
4539
+ );
4540
+ agents.command("subscriptions").description(
4541
+ "List known Remote Fabric Agents Server harness choices without checking availability"
4542
+ ).option(
4543
+ "--server-url <url>",
4544
+ "Remote Fabric Agents Server URL",
4545
+ process.env.FABRIC_AGENTS_SERVER_URL ?? "https://agents.example.com"
4546
+ ).option("--format <format>", "Output format: table|json|yaml|plain").option("-q, --quiet", "Print harness IDs only").action(
4547
+ async (opts) => {
4548
+ const harnesses = [
4549
+ {
4550
+ id: "claude-max",
4551
+ name: "Claude (Opus 4.6 / Sonnet 4.6)",
4552
+ billing: "subscription_oauth",
4553
+ credits: "not_checked",
4554
+ fallbackOrder: 1
4555
+ },
4556
+ {
4557
+ id: "codex",
4558
+ name: "Codex (GPT-5.4 / o3)",
4559
+ billing: "subscription_oauth",
4560
+ credits: "not_checked",
4561
+ fallbackOrder: 2
4562
+ },
4563
+ {
4564
+ id: "droid-cli",
4565
+ name: "Droid (Factory Multi-Model)",
4566
+ billing: "subscription_oauth",
4567
+ credits: "not_checked",
4568
+ fallbackOrder: 3
4569
+ },
4570
+ {
4571
+ id: "grok-cli",
4572
+ name: "Grok (Grok 4 Heavy / Fast)",
4573
+ billing: "subscription_oauth",
4574
+ credits: "not_checked",
4575
+ fallbackOrder: 4
4576
+ },
4577
+ {
4578
+ id: "kimi-cli",
4579
+ name: "Kimi (Kimi K2.5 Thinking)",
4580
+ billing: "subscription_oauth",
4581
+ credits: "not_checked",
4582
+ fallbackOrder: 5
4583
+ }
4584
+ ];
4585
+ if (opts.quiet) {
4586
+ for (const h of harnesses) {
4587
+ process.stdout.write(`${h.id}
4588
+ `);
4589
+ }
4590
+ return;
4591
+ }
4592
+ printOutput(harnesses, {
4593
+ format: opts.format,
4594
+ columns: [
4595
+ "id",
4596
+ "name",
4597
+ "billing",
4598
+ "credits",
4599
+ "fallbackOrder"
4600
+ ]
4601
+ });
4602
+ }
4603
+ );
4333
4604
  return agents;
4334
4605
  }
4335
4606
 
@@ -8497,7 +8768,7 @@ function exitReason(result) {
8497
8768
  if (/must be run in a work tree/i.test(text3)) {
8498
8769
  return "not a working tree (a bare repository, or inside .git)";
8499
8770
  }
8500
- if (/not a git repository/i.test(text3)) {
8771
+ if (/not a git repository/i.test(text3) || /does not point to a valid repository/i.test(text3)) {
8501
8772
  return "its .git points to a repository that does not exist";
8502
8773
  }
8503
8774
  if (/permission denied/i.test(text3)) {
@@ -10754,7 +11025,6 @@ function registrationOf(servers, name) {
10754
11025
  // src/lib/instructions/hook.ts
10755
11026
  import { createHash as createHash4 } from "crypto";
10756
11027
  import { mkdir as mkdir3 } from "fs/promises";
10757
- import { tmpdir } from "os";
10758
11028
  import path12 from "path";
10759
11029
 
10760
11030
  // src/lib/instructions/identifiers.ts
@@ -10785,8 +11055,8 @@ function subcommandsForEvent(event) {
10785
11055
  return event === "Stop" ? STOP_SUBCOMMANDS : SESSION_START_SUBCOMMANDS;
10786
11056
  }
10787
11057
  async function withHookConfigLock(root, posixPath, work) {
10788
- const directory = path12.join(tmpdir(), "fabric-instructions-hook-locks");
10789
- await mkdir3(directory, { recursive: true });
11058
+ const directory = path12.join(path12.dirname(getConfigPath()), "hook-locks");
11059
+ await mkdir3(directory, { recursive: true, mode: 448 });
10790
11060
  const identity = createHash4("sha256").update(path12.resolve(root)).update("\0").update(posixPath).digest("hex");
10791
11061
  const lockPath2 = path12.join(directory, `${identity}.lock`);
10792
11062
  try {
@@ -12853,12 +13123,12 @@ function isTooLarge(error) {
12853
13123
  // src/lib/instructions/staged-files.ts
12854
13124
  import { createHash as createHash8 } from "crypto";
12855
13125
  import { mkdtemp, realpath as realpath4, rm } from "fs/promises";
12856
- import { tmpdir as tmpdir2 } from "os";
13126
+ import { tmpdir } from "os";
12857
13127
  import path16 from "path";
12858
13128
  var IN_MEMORY_SNAPSHOT_BYTES = 50 * 1024 * 1024;
12859
13129
  async function stageFilesByUrl(options) {
12860
13130
  const root = await realpath4(
12861
- await mkdtemp(path16.join(tmpdir2(), "fabric-instructions-"))
13131
+ await mkdtemp(path16.join(tmpdir(), "fabric-instructions-"))
12862
13132
  );
12863
13133
  const files = /* @__PURE__ */ new Map();
12864
13134
  const dispose = () => rm(root, { recursive: true, force: true });
@@ -22526,6 +22796,25 @@ async function runLessonPromptCommand(opts) {
22526
22796
 
22527
22797
  // src/commands/mcp/index.ts
22528
22798
  import { Command as Command14 } from "commander";
22799
+ async function resolveMcpOrganizationId(organization, client) {
22800
+ if (!organization) {
22801
+ return void 0;
22802
+ }
22803
+ const organizationClient = client ?? getClient();
22804
+ let organizations;
22805
+ try {
22806
+ organizations = await organizationClient.orgs.list();
22807
+ } catch (error) {
22808
+ printError(error.message, 1);
22809
+ }
22810
+ const match = organizations.find(
22811
+ (candidate) => candidate.id === organization || candidate.slug === organization
22812
+ );
22813
+ if (!match) {
22814
+ printError(`Organization not found: ${organization}`, 4);
22815
+ }
22816
+ return match.id;
22817
+ }
22529
22818
  function buildMcpCommand() {
22530
22819
  const mcp = new Command14("mcp").description(
22531
22820
  "Manage MCP servers and configurations"
@@ -22665,6 +22954,219 @@ function buildMcpCommand() {
22665
22954
  const { runStdio } = await import("@fabricorg/sdk-mcp/stdio");
22666
22955
  await runStdio();
22667
22956
  });
22957
+ mcp.command("tools").description(
22958
+ "List MCP tools exposed by the Fabric MCP Gateway (supports stateless MCP mode)"
22959
+ ).option("--org <slug>", "Organization ID or slug").option("--project <id>", "Project ID to scope the gateway connection").option("--mode <mode>", "Tool list mode: full|discovery", "full").option(
22960
+ "--stateless",
22961
+ "Send stateless MCP JSON-RPC request (X-Mcp-Stateless: true)",
22962
+ true
22963
+ ).option(
22964
+ "--no-stateless",
22965
+ "Use a persistent MCP session instead of stateless mode"
22966
+ ).option("--format <format>", "Output format: table|json|yaml|plain").option("-q, --quiet", "Print tool names only").action(
22967
+ async (opts) => {
22968
+ const auth2 = getMcpAuthenticatedFetch({
22969
+ project: opts.project
22970
+ });
22971
+ const organizationId = await resolveMcpOrganizationId(opts.org);
22972
+ const base = auth2.origin.replace(/\/+$/, "");
22973
+ const path42 = opts.project ? `/api/mcp-gateway/projects/${encodeURIComponent(opts.project)}` : "/api/mcp-gateway";
22974
+ const url = new URL(`${base}${path42}`);
22975
+ if (opts.mode === "discovery") {
22976
+ url.searchParams.set("mode", "discovery");
22977
+ }
22978
+ if (opts.stateless) {
22979
+ url.searchParams.set("stateless", "1");
22980
+ }
22981
+ const headers = {
22982
+ "Content-Type": "application/json",
22983
+ Accept: "application/json"
22984
+ };
22985
+ headers.Authorization = auth2.authorizationHeader;
22986
+ if (organizationId) {
22987
+ headers["X-Organization-Id"] = organizationId;
22988
+ }
22989
+ if (opts.stateless) {
22990
+ headers["X-Mcp-Stateless"] = "true";
22991
+ }
22992
+ let res;
22993
+ try {
22994
+ res = await auth2.fetch(url.toString(), {
22995
+ method: "POST",
22996
+ headers,
22997
+ body: JSON.stringify({
22998
+ jsonrpc: "2.0",
22999
+ id: 1,
23000
+ method: "tools/list",
23001
+ params: {}
23002
+ })
23003
+ });
23004
+ } catch (err) {
23005
+ printError(err.message, 1);
23006
+ }
23007
+ const payload = await res.json();
23008
+ if (!res.ok || payload.error) {
23009
+ printError(
23010
+ payload.error?.message ?? `MCP Gateway returned HTTP ${res.status}`,
23011
+ 1
23012
+ );
23013
+ }
23014
+ const tools = (payload.result?.tools ?? []).map((t) => ({
23015
+ name: t.name,
23016
+ uiApp: t._meta?.ui?.resourceUri ?? "",
23017
+ description: (t.description ?? "").slice(0, 72)
23018
+ }));
23019
+ if (opts.quiet) {
23020
+ for (const t of tools) {
23021
+ process.stdout.write(`${t.name}
23022
+ `);
23023
+ }
23024
+ return;
23025
+ }
23026
+ printOutput(tools, {
23027
+ format: opts.format,
23028
+ columns: ["name", "uiApp", "description"]
23029
+ });
23030
+ }
23031
+ );
23032
+ mcp.command("call <tool>").description(
23033
+ "Execute a Fabric MCP tool over Streamable HTTP with optional 24-hour idempotent durable calls"
23034
+ ).option("--org <slug>", "Organization ID or slug").option("--project <id>", "Project ID to scope the gateway connection").option("--input <json>", "Tool arguments as JSON object", "{}").option(
23035
+ "--stateless",
23036
+ "Execute without persisting server session (X-Mcp-Stateless: true)",
23037
+ true
23038
+ ).option(
23039
+ "--no-stateless",
23040
+ "Use a persistent MCP session instead of stateless mode"
23041
+ ).option(
23042
+ "--durable",
23043
+ "Use the Fabric idempotency store (X-Mcp-Durable: true)"
23044
+ ).option(
23045
+ "--idempotency-key <key>",
23046
+ "Explicit key for 24-hour durable-call deduplication"
23047
+ ).option("--format <format>", "Output format: table|json|yaml|plain").action(
23048
+ async (tool, opts) => {
23049
+ let toolArgs = {};
23050
+ try {
23051
+ toolArgs = JSON.parse(opts.input);
23052
+ } catch {
23053
+ printError("--input must be valid JSON", 2);
23054
+ }
23055
+ const auth2 = getMcpAuthenticatedFetch({
23056
+ project: opts.project
23057
+ });
23058
+ const organizationId = await resolveMcpOrganizationId(opts.org);
23059
+ const base = auth2.origin.replace(/\/+$/, "");
23060
+ const path42 = opts.project ? `/api/mcp-gateway/projects/${encodeURIComponent(opts.project)}` : "/api/mcp-gateway";
23061
+ const url = new URL(`${base}${path42}`);
23062
+ if (opts.stateless) {
23063
+ url.searchParams.set("stateless", "1");
23064
+ }
23065
+ const headers = {
23066
+ "Content-Type": "application/json",
23067
+ Accept: "application/json"
23068
+ };
23069
+ headers.Authorization = auth2.authorizationHeader;
23070
+ if (organizationId) {
23071
+ headers["X-Organization-Id"] = organizationId;
23072
+ }
23073
+ if (opts.stateless) {
23074
+ headers["X-Mcp-Stateless"] = "true";
23075
+ }
23076
+ if (opts.durable) {
23077
+ headers["X-Mcp-Durable"] = "true";
23078
+ }
23079
+ if (opts.idempotencyKey) {
23080
+ headers["X-Mcp-Idempotency-Key"] = opts.idempotencyKey;
23081
+ }
23082
+ let res;
23083
+ try {
23084
+ res = await auth2.fetch(url.toString(), {
23085
+ method: "POST",
23086
+ headers,
23087
+ body: JSON.stringify({
23088
+ jsonrpc: "2.0",
23089
+ id: 1,
23090
+ method: "tools/call",
23091
+ params: {
23092
+ name: tool,
23093
+ arguments: toolArgs,
23094
+ ...opts.durable || opts.idempotencyKey ? {
23095
+ _meta: {
23096
+ durable: Boolean(opts.durable),
23097
+ ...opts.idempotencyKey ? {
23098
+ idempotencyKey: opts.idempotencyKey
23099
+ } : {}
23100
+ }
23101
+ } : {}
23102
+ }
23103
+ })
23104
+ });
23105
+ } catch (err) {
23106
+ printError(err.message, 1);
23107
+ }
23108
+ const payload = await res.json();
23109
+ if (!res.ok || payload.error) {
23110
+ printError(
23111
+ payload.error?.message ?? `MCP tool call failed with HTTP ${res.status}`,
23112
+ 1
23113
+ );
23114
+ }
23115
+ printOutput(payload.result ?? payload, {
23116
+ format: opts.format
23117
+ });
23118
+ }
23119
+ );
23120
+ mcp.command("apps").description(
23121
+ "List or inspect built-in MCP Apps (ui://fabric/*) for native rendering in Fabric Desktop"
23122
+ ).option("--uri <uri>", "Read a specific ui://fabric/* MCP App resource").option("--format <format>", "Output format: table|json|yaml|plain").option("-q, --quiet", "Print MCP App URIs only").action(
23123
+ async (opts) => {
23124
+ const apps = [
23125
+ {
23126
+ uri: "ui://fabric/project-dashboard",
23127
+ name: "Fabric Project Overview & Desktop Launcher",
23128
+ mimeType: "text/html;profile=mcp-app",
23129
+ linkedTool: "fabric_get_project"
23130
+ },
23131
+ {
23132
+ uri: "ui://fabric/agent-delegation",
23133
+ name: "Fabric Agents Delegation & Subscription Credits",
23134
+ mimeType: "text/html;profile=mcp-app",
23135
+ linkedTool: "fabric_list_features"
23136
+ },
23137
+ {
23138
+ uri: "ui://fabric/workflow-visualizer",
23139
+ name: "Fabric Visual Data Prep & Durable Workflow Inspector",
23140
+ mimeType: "text/html;profile=mcp-app",
23141
+ linkedTool: "fabric_execute_workflow"
23142
+ },
23143
+ {
23144
+ uri: "ui://fabric/genie-insights",
23145
+ name: "Fabric Genie Agent & Lakebase Query Explorer",
23146
+ mimeType: "text/html;profile=mcp-app",
23147
+ linkedTool: "fabric_query_workspace"
23148
+ },
23149
+ {
23150
+ uri: "ui://fabric/visualize",
23151
+ name: "Fabric Interactive A2UI Card Composer",
23152
+ mimeType: "text/html;profile=mcp-app",
23153
+ linkedTool: "fabric_visualize"
23154
+ }
23155
+ ];
23156
+ const filtered = opts.uri ? apps.filter((a) => a.uri === opts.uri) : apps;
23157
+ if (opts.quiet) {
23158
+ for (const a of filtered) {
23159
+ process.stdout.write(`${a.uri}
23160
+ `);
23161
+ }
23162
+ return;
23163
+ }
23164
+ printOutput(filtered, {
23165
+ format: opts.format,
23166
+ columns: ["uri", "name", "mimeType", "linkedTool"]
23167
+ });
23168
+ }
23169
+ );
22668
23170
  return mcp;
22669
23171
  }
22670
23172
 
@@ -24813,11 +25315,44 @@ function buildWorkspacesCommand() {
24813
25315
  hookTiming.processStartedAt = Math.round(Date.now() - process.uptime() * 1e3);
24814
25316
  hookTiming.exitWhenDone = true;
24815
25317
  var program = new Command23();
24816
- program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
25318
+ program.name("fabric").description("Fabric AI platform CLI \u2014 portal, MCP, and agent delegation").version(package_default.version).option(
24817
25319
  "--format <format>",
24818
- "Output format: table|json|yaml|csv",
25320
+ "Output format: table|json|yaml|csv|plain|quiet",
24819
25321
  process.env.FABRIC_FORMAT ?? "table"
24820
- );
25322
+ ).option("--json", "Shorthand for --format json").option(
25323
+ "--plain",
25324
+ "Shorthand for --format plain (tab-separated, pipe-friendly)"
25325
+ ).option("--no-color", "Disable ANSI color output (also honors NO_COLOR)").addHelpText(
25326
+ "after",
25327
+ `
25328
+ Examples:
25329
+ $ fabric auth login
25330
+ $ fabric connect
25331
+ $ fabric agents delegate --target remote --harness claude-max --prompt "Implement OAuth callback"
25332
+ $ fabric agents desktop --project proj_123 --view allSessions
25333
+ $ fabric mcp tools --stateless
25334
+ $ fabric mcp call fabric_get_identity --stateless --durable
25335
+
25336
+ Environment Variables:
25337
+ FABRIC_API_KEY API key (fab_... or org_...) for non-interactive authentication
25338
+ FABRIC_BASE_URL Fabric instance URL (default: https://app.fabric.pro)
25339
+ FABRIC_FORMAT Default output format (table|json|yaml|csv|plain|quiet)
25340
+ NO_COLOR Set to any non-empty value to disable ANSI color output
25341
+ `
25342
+ ).hook("preAction", (thisCommand) => {
25343
+ const opts = thisCommand.optsWithGlobals();
25344
+ if (typeof opts.format === "string") {
25345
+ process.env.FABRIC_FORMAT = opts.format;
25346
+ }
25347
+ if (opts.color === false) {
25348
+ process.env.FABRIC_NO_COLOR = "1";
25349
+ }
25350
+ if (opts.json) {
25351
+ process.env.FABRIC_FORMAT = "json";
25352
+ } else if (opts.plain) {
25353
+ process.env.FABRIC_FORMAT = "plain";
25354
+ }
25355
+ });
24821
25356
  var auth = new Command23("auth").description(
24822
25357
  "Authenticate and manage API keys"
24823
25358
  );