@dashai/cli 0.18.2 → 0.19.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/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import pc from 'picocolors';
3
3
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, readdirSync, chmodSync, renameSync, mkdtempSync, statSync } from 'fs';
4
- import { tmpdir, platform, homedir } from 'os';
5
- import { resolve, join, relative, dirname, basename, sep } from 'path';
4
+ import { hostname, tmpdir, platform, homedir } from 'os';
5
+ import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { intro, log, select, isCancel, cancel, spinner, outro, text, multiselect, confirm } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
@@ -236,10 +236,13 @@ function asEnvelope(v) {
236
236
  const env = v;
237
237
  const out2 = {};
238
238
  if (typeof env.code === "string") out2.code = env.code;
239
+ else if (typeof env.error === "string" && CODE_SHAPE.test(env.error)) out2.code = env.error;
239
240
  if (typeof env.message === "string") out2.message = env.message;
240
241
  if (typeof env.retriable === "boolean") out2.retriable = env.retriable;
241
242
  if (env.context && typeof env.context === "object") {
242
243
  out2.context = env.context;
244
+ } else if (env.details && typeof env.details === "object") {
245
+ out2.context = env.details;
243
246
  }
244
247
  return out2;
245
248
  }
@@ -512,7 +515,7 @@ async function getRuntimeLogs(installationId, opts = {}, options = {}) {
512
515
  options
513
516
  );
514
517
  }
515
- var ApiError;
518
+ var ApiError, CODE_SHAPE;
516
519
  var init_api_client = __esm({
517
520
  "src/lib/api-client.ts"() {
518
521
  init_config();
@@ -532,6 +535,7 @@ var init_api_client = __esm({
532
535
  this.retriable = envelope?.retriable ?? status >= 500;
533
536
  }
534
537
  };
538
+ CODE_SHAPE = /^[A-Z][A-Z0-9_]*$/;
535
539
  }
536
540
  });
537
541
 
@@ -1194,7 +1198,7 @@ async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
1194
1198
  );
1195
1199
  if (workspaces.length === 0) {
1196
1200
  throw new Error(
1197
- "No workspaces found for your account. Create a workspace at the DashWise dashboard first."
1201
+ "No workspaces found for your account. Create a workspace at the Dashwise dashboard first."
1198
1202
  );
1199
1203
  }
1200
1204
  if (flag === void 0) {
@@ -1253,6 +1257,64 @@ var init_dev = __esm({
1253
1257
  PORT_PROBE_LIMIT = 10;
1254
1258
  }
1255
1259
  });
1260
+ async function selectWorkspace(opts) {
1261
+ const workspaces = await apiRequest(
1262
+ "GET",
1263
+ "/api/workspaces",
1264
+ void 0,
1265
+ { baseUrl: opts.apiUrl, auth: { token: opts.apiToken } }
1266
+ );
1267
+ if (workspaces.length === 0) {
1268
+ throw new Error(
1269
+ "No workspaces found for your account. Create a workspace at the Dashwise dashboard first."
1270
+ );
1271
+ }
1272
+ if (opts.flag !== void 0) {
1273
+ return matchWorkspaceOrThrow(workspaces, opts.flag);
1274
+ }
1275
+ if (workspaces.length === 1) {
1276
+ return workspaces[0];
1277
+ }
1278
+ if (opts.interactive && process.stdout.isTTY) {
1279
+ const choice = await select({
1280
+ message: "Which workspace should this module live in?",
1281
+ options: workspaces.map((w) => ({
1282
+ value: w.slug,
1283
+ label: w.name,
1284
+ hint: `slug=${w.slug}, id=${w.id}`
1285
+ }))
1286
+ });
1287
+ if (isCancel(choice)) {
1288
+ throw new Error("Aborted.");
1289
+ }
1290
+ const picked = workspaces.find((w) => w.slug === choice);
1291
+ if (!picked) {
1292
+ throw new Error(`Internal error: selection "${String(choice)}" did not match any workspace.`);
1293
+ }
1294
+ return picked;
1295
+ }
1296
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
1297
+ throw new Error(
1298
+ `You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
1299
+ ${list}`
1300
+ );
1301
+ }
1302
+ function matchWorkspaceOrThrow(workspaces, flag) {
1303
+ const asNumber = Number.parseInt(flag, 10);
1304
+ const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
1305
+ const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
1306
+ if (!match) {
1307
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
1308
+ throw new Error(`No workspace matching --workspace=${flag}. Available:
1309
+ ${list}`);
1310
+ }
1311
+ return match;
1312
+ }
1313
+ var init_workspaces = __esm({
1314
+ "src/lib/workspaces.ts"() {
1315
+ init_api_client();
1316
+ }
1317
+ });
1256
1318
 
1257
1319
  // src/lib/scaffold.ts
1258
1320
  function isDeployTarget(value) {
@@ -3752,66 +3814,8 @@ var init_scaffold = __esm({
3752
3814
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
3753
3815
  }
3754
3816
  });
3755
- async function selectWorkspace(opts) {
3756
- const workspaces = await apiRequest(
3757
- "GET",
3758
- "/api/workspaces",
3759
- void 0,
3760
- { baseUrl: opts.apiUrl, auth: { token: opts.apiToken } }
3761
- );
3762
- if (workspaces.length === 0) {
3763
- throw new Error(
3764
- "No workspaces found for your account. Create a workspace at the DashWise dashboard first."
3765
- );
3766
- }
3767
- if (opts.flag !== void 0) {
3768
- return matchWorkspaceOrThrow(workspaces, opts.flag);
3769
- }
3770
- if (workspaces.length === 1) {
3771
- return workspaces[0];
3772
- }
3773
- if (opts.interactive && process.stdout.isTTY) {
3774
- const choice = await select({
3775
- message: "Which workspace should this module live in?",
3776
- options: workspaces.map((w) => ({
3777
- value: w.slug,
3778
- label: w.name,
3779
- hint: `slug=${w.slug}, id=${w.id}`
3780
- }))
3781
- });
3782
- if (isCancel(choice)) {
3783
- throw new Error("Aborted.");
3784
- }
3785
- const picked = workspaces.find((w) => w.slug === choice);
3786
- if (!picked) {
3787
- throw new Error(`Internal error: selection "${String(choice)}" did not match any workspace.`);
3788
- }
3789
- return picked;
3790
- }
3791
- const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
3792
- throw new Error(
3793
- `You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
3794
- ${list}`
3795
- );
3796
- }
3797
- function matchWorkspaceOrThrow(workspaces, flag) {
3798
- const asNumber = Number.parseInt(flag, 10);
3799
- const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
3800
- const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
3801
- if (!match) {
3802
- const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
3803
- throw new Error(`No workspace matching --workspace=${flag}. Available:
3804
- ${list}`);
3805
- }
3806
- return match;
3807
- }
3808
- var init_workspaces = __esm({
3809
- "src/lib/workspaces.ts"() {
3810
- init_api_client();
3811
- }
3812
- });
3813
3817
  async function moduleInitCommand(slugArg, options) {
3814
- intro("Scaffolding a new DashWise module");
3818
+ intro("Scaffolding a new Dashwise module");
3815
3819
  let slug = slugArg;
3816
3820
  if (!slug && process.stdout.isTTY) {
3817
3821
  const answer = await text({
@@ -4148,7 +4152,7 @@ function surfaceProvisionApiError(err, slug) {
4148
4152
  return;
4149
4153
  case "FORBIDDEN":
4150
4154
  warn(
4151
- `Not authorized to create a module in that workspace. Confirm you're a member at the DashWise dashboard.`
4155
+ `Not authorized to create a module in that workspace. Confirm you're a member at the Dashwise dashboard.`
4152
4156
  );
4153
4157
  return;
4154
4158
  case "DEV_INSTALL_ALREADY_EXISTS": {
@@ -5357,22 +5361,81 @@ init_dev();
5357
5361
 
5358
5362
  // src/commands/login.ts
5359
5363
  init_api_client();
5364
+
5365
+ // src/lib/device-code.ts
5366
+ init_api_client();
5367
+ var DEFAULT_TOKEN_NAME = "dashwise-cli";
5368
+ async function requestDeviceCode(args) {
5369
+ return await apiRequest(
5370
+ "POST",
5371
+ "/api/auth/cli/device-code",
5372
+ { name: args.name ?? DEFAULT_TOKEN_NAME },
5373
+ { auth: "public", baseUrl: args.apiUrl }
5374
+ );
5375
+ }
5376
+ async function pollForApproval(args) {
5377
+ const sleep4 = args.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5378
+ const now = args.now ?? (() => Date.now());
5379
+ const pollIntervalMs = Math.max(1e3, args.intervalSeconds * 1e3);
5380
+ const deadline = now() + args.expiresInSeconds * 1e3;
5381
+ while (now() < deadline) {
5382
+ let res;
5383
+ try {
5384
+ res = await apiRequest(
5385
+ "POST",
5386
+ "/api/auth/cli/poll",
5387
+ { deviceCode: args.deviceCode },
5388
+ { auth: "public", baseUrl: args.apiUrl }
5389
+ );
5390
+ } catch (err) {
5391
+ return { status: "error", message: err.message };
5392
+ }
5393
+ switch (res.status) {
5394
+ case "pending":
5395
+ case "slow_down":
5396
+ await sleep4(pollIntervalMs);
5397
+ continue;
5398
+ case "denied":
5399
+ return { status: "denied" };
5400
+ case "expired":
5401
+ return { status: "expired" };
5402
+ case "approved":
5403
+ if (!res.token || !res.tokenId) return { status: "no_token" };
5404
+ return { status: "approved", token: res.token, tokenId: res.tokenId };
5405
+ }
5406
+ }
5407
+ return { status: "timeout" };
5408
+ }
5409
+ async function fetchProfile(args) {
5410
+ return await apiRequest("GET", "/api/auth/me", void 0, {
5411
+ auth: { token: args.token },
5412
+ baseUrl: args.apiUrl
5413
+ });
5414
+ }
5415
+ function buildCliConfig(args) {
5416
+ return {
5417
+ apiUrl: args.apiUrl,
5418
+ token: args.token,
5419
+ tokenId: args.tokenId,
5420
+ user: {
5421
+ id: args.me.id,
5422
+ email: args.me.email,
5423
+ username: args.me.username,
5424
+ firstName: args.me.first_name,
5425
+ lastName: args.me.last_name
5426
+ }
5427
+ };
5428
+ }
5429
+
5430
+ // src/commands/login.ts
5360
5431
  init_config();
5361
5432
  init_output();
5362
- var DEFAULT_TOKEN_NAME = "dashwise-cli";
5363
5433
  async function loginCommand(options) {
5364
5434
  const apiUrl = (options.apiUrl ?? resolveApiUrl()).replace(/\/+$/, "");
5365
5435
  intro(`Logging into ${code(apiUrl)}`);
5366
5436
  let dc;
5367
5437
  try {
5368
- dc = await apiRequest(
5369
- "POST",
5370
- "/api/auth/cli/device-code",
5371
- {
5372
- name: options.name ?? DEFAULT_TOKEN_NAME
5373
- },
5374
- { auth: "public", baseUrl: apiUrl }
5375
- );
5438
+ dc = await requestDeviceCode({ apiUrl, name: options.name ?? DEFAULT_TOKEN_NAME });
5376
5439
  } catch (err) {
5377
5440
  fail(formatApiError("Could not start login flow", err));
5378
5441
  return 1;
@@ -5412,85 +5475,50 @@ async function loginCommand(options) {
5412
5475
  }
5413
5476
  const sp = spinner();
5414
5477
  sp.start("Waiting for approval");
5415
- const overallDeadline = Date.now() + dc.expiresIn * 1e3;
5416
- let outcome = null;
5417
- const pollIntervalMs = Math.max(1e3, dc.interval * 1e3);
5418
- pollLoop: while (Date.now() < overallDeadline) {
5419
- let res;
5420
- try {
5421
- res = await apiRequest(
5422
- "POST",
5423
- "/api/auth/cli/poll",
5424
- { deviceCode: dc.deviceCode },
5425
- { auth: "public", baseUrl: apiUrl }
5426
- );
5427
- } catch (err) {
5428
- sp.stop(`Poll failed: ${err.message}`);
5478
+ const outcome = await pollForApproval({
5479
+ apiUrl,
5480
+ deviceCode: dc.deviceCode,
5481
+ intervalSeconds: dc.interval,
5482
+ expiresInSeconds: dc.expiresIn
5483
+ });
5484
+ switch (outcome.status) {
5485
+ case "error":
5486
+ sp.stop(`Poll failed: ${outcome.message}`);
5429
5487
  fail("Poll failed mid-flight; you can re-run `dashwise login` to retry.");
5430
5488
  return 1;
5431
- }
5432
- switch (res.status) {
5433
- case "pending":
5434
- case "slow_down":
5435
- await sleep(pollIntervalMs);
5436
- continue pollLoop;
5437
- case "denied":
5438
- sp.stop("Login denied by user.");
5439
- cancel("Aborted.");
5440
- return 1;
5441
- case "expired":
5442
- sp.stop("Login code expired.");
5443
- fail("The code expired before approval. Run `dashwise login` again.");
5444
- return 1;
5445
- case "approved":
5446
- outcome = res;
5447
- break pollLoop;
5448
- }
5449
- }
5450
- if (!outcome) {
5451
- sp.stop("Timed out waiting for approval.");
5452
- fail("Login took too long to be approved. Run `dashwise login` again.");
5453
- return 1;
5454
- }
5455
- if (!outcome.token || !outcome.tokenId) {
5456
- sp.stop("Approval received but token missing from response.");
5457
- fail(
5458
- "Approval received but the backend did not return a token. Run `dashwise logout` then `dashwise login` to retry."
5459
- );
5460
- return 1;
5489
+ case "denied":
5490
+ sp.stop("Login denied by user.");
5491
+ cancel("Aborted.");
5492
+ return 1;
5493
+ case "expired":
5494
+ sp.stop("Login code expired.");
5495
+ fail("The code expired before approval. Run `dashwise login` again.");
5496
+ return 1;
5497
+ case "timeout":
5498
+ sp.stop("Timed out waiting for approval.");
5499
+ fail("Login took too long to be approved. Run `dashwise login` again.");
5500
+ return 1;
5501
+ case "no_token":
5502
+ sp.stop("Approval received but token missing from response.");
5503
+ fail(
5504
+ "Approval received but the backend did not return a token. Run `dashwise logout` then `dashwise login` to retry."
5505
+ );
5506
+ return 1;
5461
5507
  }
5462
5508
  let me;
5463
5509
  try {
5464
- me = await apiRequest("GET", "/api/auth/me", void 0, {
5465
- auth: { token: outcome.token },
5466
- baseUrl: apiUrl
5467
- });
5510
+ me = await fetchProfile({ apiUrl, token: outcome.token });
5468
5511
  } catch (err) {
5469
5512
  sp.stop("Approval accepted but profile fetch failed.");
5470
5513
  fail(formatApiError("Could not load user profile", err));
5471
5514
  return 1;
5472
5515
  }
5473
- const cfg = {
5474
- apiUrl,
5475
- token: outcome.token,
5476
- tokenId: outcome.tokenId,
5477
- user: {
5478
- id: me.id,
5479
- email: me.email,
5480
- username: me.username,
5481
- firstName: me.first_name,
5482
- lastName: me.last_name
5483
- }
5484
- };
5485
- writeConfig(cfg);
5516
+ writeConfig(buildCliConfig({ apiUrl, token: outcome.token, tokenId: outcome.tokenId, me }));
5486
5517
  sp.stop("Approved.");
5487
5518
  success(`Logged in as ${code(me.email)}`);
5488
5519
  outro("You can now run `dashwise profile`, `dashwise workspace list`, etc.");
5489
5520
  return 0;
5490
5521
  }
5491
- function sleep(ms) {
5492
- return new Promise((resolve13) => setTimeout(resolve13, ms));
5493
- }
5494
5522
  function formatSeconds(seconds) {
5495
5523
  if (seconds < 60) return `${seconds}s`;
5496
5524
  const minutes = Math.round(seconds / 60);
@@ -5503,156 +5531,1214 @@ function formatApiError(prefix, err) {
5503
5531
  if (err instanceof Error) return `${prefix}: ${err.message}`;
5504
5532
  return `${prefix}: ${String(err)}`;
5505
5533
  }
5506
-
5507
- // src/commands/logout.ts
5508
- init_api_client();
5509
- init_config();
5510
- init_output();
5511
- async function logoutCommand() {
5512
- let cfg;
5513
- try {
5514
- cfg = readConfig();
5515
- } catch {
5516
- deleteConfig();
5517
- success("Cleared a corrupted local credential file.");
5518
- return 0;
5534
+
5535
+ // src/commands/logout.ts
5536
+ init_api_client();
5537
+ init_config();
5538
+ init_output();
5539
+ async function logoutCommand() {
5540
+ let cfg;
5541
+ try {
5542
+ cfg = readConfig();
5543
+ } catch {
5544
+ deleteConfig();
5545
+ success("Cleared a corrupted local credential file.");
5546
+ return 0;
5547
+ }
5548
+ if (!cfg) {
5549
+ info("Already logged out (no saved credentials).");
5550
+ return 0;
5551
+ }
5552
+ try {
5553
+ await apiRequest(
5554
+ "DELETE",
5555
+ `/api/auth/cli-tokens/${cfg.tokenId}`,
5556
+ void 0,
5557
+ { auth: { token: cfg.token }, baseUrl: cfg.apiUrl }
5558
+ );
5559
+ } catch (err) {
5560
+ warn(
5561
+ `Could not revoke the server-side token (${err.message}). Local credentials cleared anyway.`
5562
+ );
5563
+ }
5564
+ deleteConfig();
5565
+ success(`Logged out ${code(cfg.user.email)}`);
5566
+ return 0;
5567
+ }
5568
+
5569
+ // src/commands/profile.ts
5570
+ init_api_client();
5571
+ init_config();
5572
+ init_output();
5573
+ async function profileCommand(options = {}) {
5574
+ const cfg = readConfig();
5575
+ if (!cfg) {
5576
+ if (options.json) {
5577
+ out(JSON.stringify({ ok: false, error: { code: "NOT_LOGGED_IN", message: "Run `dashwise login` first." } }));
5578
+ } else {
5579
+ fail("Not logged in. Run `dashwise login` first.");
5580
+ }
5581
+ return 1;
5582
+ }
5583
+ let me = null;
5584
+ let refreshError = null;
5585
+ try {
5586
+ me = await apiRequest("GET", "/api/auth/me");
5587
+ } catch (err) {
5588
+ if (err instanceof ApiError && err.status === 401) {
5589
+ if (options.json) {
5590
+ out(JSON.stringify({ ok: false, error: { code: "TOKEN_INVALID", message: "Saved token is no longer valid." } }));
5591
+ } else {
5592
+ fail("Saved token is no longer valid. Run `dashwise login` again.");
5593
+ }
5594
+ return 1;
5595
+ }
5596
+ refreshError = err.message;
5597
+ if (!options.json) {
5598
+ warn(
5599
+ `Could not refresh profile from the server (${refreshError}). Showing cached values.`
5600
+ );
5601
+ }
5602
+ }
5603
+ if (me) {
5604
+ const refreshed = {
5605
+ ...cfg,
5606
+ user: {
5607
+ id: me.id,
5608
+ email: me.email,
5609
+ username: me.username,
5610
+ firstName: me.first_name,
5611
+ lastName: me.last_name
5612
+ }
5613
+ };
5614
+ writeConfig(refreshed);
5615
+ }
5616
+ const u = me ?? toMe(cfg.user);
5617
+ let workspaces = [];
5618
+ let workspaceError = null;
5619
+ try {
5620
+ const fetched = await apiRequest(
5621
+ "GET",
5622
+ "/api/workspaces"
5623
+ );
5624
+ workspaces = Array.isArray(fetched) ? fetched : [];
5625
+ } catch (err) {
5626
+ workspaceError = err.message;
5627
+ }
5628
+ if (options.json) {
5629
+ out(
5630
+ JSON.stringify({
5631
+ ok: true,
5632
+ user: {
5633
+ id: u.id,
5634
+ username: u.username,
5635
+ email: u.email,
5636
+ first_name: u.first_name,
5637
+ last_name: u.last_name
5638
+ },
5639
+ default_workspace: cfg.workspaceSlug ?? null,
5640
+ workspaces,
5641
+ api_url: cfg.apiUrl,
5642
+ ...refreshError ? { profile_refresh_error: refreshError } : {},
5643
+ ...workspaceError ? { workspaces_error: workspaceError } : {}
5644
+ })
5645
+ );
5646
+ return 0;
5647
+ }
5648
+ out("");
5649
+ kv("User ID", `${u.id}`);
5650
+ kv("Username", u.username);
5651
+ kv("Email", u.email);
5652
+ kv("Name", `${u.first_name} ${u.last_name}`.trim() || dim("(not set)"));
5653
+ out("");
5654
+ if (workspaceError) {
5655
+ warn(
5656
+ `Could not list workspaces (${workspaceError}). Try \`dashwise workspace list\`.`
5657
+ );
5658
+ } else if (workspaces.length === 0) {
5659
+ info("No workspaces yet.");
5660
+ } else {
5661
+ kv("Workspaces", `${workspaces.length}`);
5662
+ for (const w of workspaces) {
5663
+ const slug = w.slug ?? `id:${w.id}`;
5664
+ const role = w.role ?? w.permissions ?? "";
5665
+ const star = cfg.workspaceSlug === slug ? code(" (default)") : "";
5666
+ out(` - ${slug}${role ? dim(` [${role}]`) : ""}${star} ${dim(w.name)}`);
5667
+ }
5668
+ }
5669
+ out("");
5670
+ kv("API URL", cfg.apiUrl);
5671
+ return 0;
5672
+ }
5673
+ function toMe(cached) {
5674
+ return {
5675
+ id: cached.id,
5676
+ email: cached.email,
5677
+ username: cached.username,
5678
+ first_name: cached.firstName,
5679
+ last_name: cached.lastName,
5680
+ is_staff: false,
5681
+ is_active: true,
5682
+ date_joined: ""
5683
+ };
5684
+ }
5685
+
5686
+ // src/commands/mcp.ts
5687
+ init_api_client();
5688
+ init_config();
5689
+ var SESSION_HEADER = "mcp-session-id";
5690
+ var PROTOCOL_HEADER = "MCP-Protocol-Version";
5691
+ var ACCEPT = "application/json, text/event-stream";
5692
+ var PROXY_TOOLS_LIST_ID = "dashwise-cli:tools-list";
5693
+ var PROXY_REINITIALIZE_ID = "dashwise-cli:reinitialize";
5694
+ function defaultEndpointLabel() {
5695
+ const raw = hostname() || "unknown";
5696
+ const cleaned = raw.replace(/\.local$/i, "").replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
5697
+ return cleaned.length > 0 ? cleaned : "unknown";
5698
+ }
5699
+ function redactUrl(url) {
5700
+ try {
5701
+ const parsed = new URL(url);
5702
+ const segments = parsed.pathname.split("/");
5703
+ if (segments.length > 0) segments[segments.length - 1] = "\u2026";
5704
+ return `${parsed.origin}${segments.join("/")}`;
5705
+ } catch {
5706
+ return "<invalid url>";
5707
+ }
5708
+ }
5709
+ function inspectElement(value) {
5710
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5711
+ const frame = value;
5712
+ const id = typeof frame.id === "string" || typeof frame.id === "number" ? frame.id : null;
5713
+ const method = typeof frame.method === "string" ? frame.method : void 0;
5714
+ let toolName;
5715
+ if (method === "tools/call" && frame.params && typeof frame.params === "object") {
5716
+ const name = frame.params.name;
5717
+ if (typeof name === "string") toolName = name;
5718
+ }
5719
+ return { id, method, toolName };
5720
+ }
5721
+ function inspectFrame(text4) {
5722
+ let value;
5723
+ try {
5724
+ value = JSON.parse(text4);
5725
+ } catch {
5726
+ return null;
5727
+ }
5728
+ if (Array.isArray(value)) {
5729
+ const elements = value.map(inspectElement).filter((e) => e !== null);
5730
+ return {
5731
+ id: null,
5732
+ method: void 0,
5733
+ toolName: void 0,
5734
+ isBatch: true,
5735
+ elements
5736
+ };
5737
+ }
5738
+ const single = inspectElement(value);
5739
+ if (!single) return null;
5740
+ return { ...single, isBatch: false, elements: [single] };
5741
+ }
5742
+ function readOnlyToolsFrom(text4) {
5743
+ let value;
5744
+ try {
5745
+ value = JSON.parse(text4);
5746
+ } catch {
5747
+ return null;
5748
+ }
5749
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5750
+ const result = value.result;
5751
+ if (!result || typeof result !== "object") return null;
5752
+ const tools = result.tools;
5753
+ if (!Array.isArray(tools)) return null;
5754
+ const out2 = /* @__PURE__ */ new Set();
5755
+ for (const tool of tools) {
5756
+ if (!tool || typeof tool !== "object") continue;
5757
+ const t = tool;
5758
+ const name = typeof t.name === "string" ? t.name : null;
5759
+ const annotations = t.annotations;
5760
+ if (name && annotations && typeof annotations === "object" && annotations.readOnlyHint === true) {
5761
+ out2.add(name);
5762
+ }
5763
+ }
5764
+ return out2;
5765
+ }
5766
+ function isReadOnlyRefusal(id, toolName, knownReadTools) {
5767
+ const reason = knownReadTools === null ? `Refused: \`dashwise mcp --read-only\` could not read this server's tool list, so it cannot tell reads from writes and refuses everything. Reconnect without --read-only to use "${toolName}".` : `Refused: "${toolName}" is not a read-only tool and this session was started with \`dashwise mcp --read-only\`. Reconnect without the flag to use it.`;
5768
+ return {
5769
+ jsonrpc: "2.0",
5770
+ id,
5771
+ result: {
5772
+ isError: true,
5773
+ content: [{ type: "text", text: reason }]
5774
+ }
5775
+ };
5776
+ }
5777
+ function transportErrorFrame(id, message) {
5778
+ return {
5779
+ jsonrpc: "2.0",
5780
+ id,
5781
+ // -32000 is the JSON-RPC "implementation-defined server error" band. A
5782
+ // dead pipe with NO frame is the one outcome worth avoiding above all
5783
+ // others: it leaves Claude Code waiting forever on a request that will
5784
+ // never be answered (`cli-mcp-command.md` §4.4).
5785
+ error: { code: -32e3, message }
5786
+ };
5787
+ }
5788
+ function startMcpProxy(opts) {
5789
+ const stdin = opts.stdin ?? process.stdin;
5790
+ const stdout = opts.stdout ?? process.stdout;
5791
+ const log2 = opts.log ?? ((line) => process.stderr.write(`${line}
5792
+ `));
5793
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
5794
+ const reconnectMs = opts.reconnectMs ?? 2e3;
5795
+ const verbose = opts.verbose === true;
5796
+ const readOnly = opts.readOnly === true;
5797
+ let session = opts.session;
5798
+ let sessionId = null;
5799
+ let protocolVersion = null;
5800
+ let readTools = null;
5801
+ let reauthAttempt = null;
5802
+ let reauthSpent = false;
5803
+ let stopped = false;
5804
+ let exitCode = 0;
5805
+ let streamTimer = null;
5806
+ let streamAbort = null;
5807
+ let streamUnavailable = false;
5808
+ let streamStarted = false;
5809
+ let resolveDone;
5810
+ const done = new Promise((resolve13) => {
5811
+ resolveDone = resolve13;
5812
+ });
5813
+ const trace = (line) => {
5814
+ if (verbose) log2(line);
5815
+ };
5816
+ const emit = (payload) => {
5817
+ stdout.write(`${JSON.stringify(payload)}
5818
+ `);
5819
+ };
5820
+ const emitFrame = (text4, source) => {
5821
+ let parsed;
5822
+ try {
5823
+ parsed = JSON.parse(text4);
5824
+ } catch {
5825
+ log2(
5826
+ `Discarded a non-JSON response from ${redactUrl(session.connectUrl)} (${source}); stdout carries MCP frames only.`
5827
+ );
5828
+ trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
5829
+ return;
5830
+ }
5831
+ const looksJsonRpc = Array.isArray(parsed) ? parsed.every(
5832
+ (e) => e !== null && typeof e === "object" && "jsonrpc" in e
5833
+ ) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
5834
+ if (!looksJsonRpc) {
5835
+ log2(
5836
+ `Discarded a JSON response from ${redactUrl(session.connectUrl)} that is not JSON-RPC (${source}).`
5837
+ );
5838
+ return;
5839
+ }
5840
+ if (text4.includes("\n") || text4.includes("\r")) {
5841
+ trace("reflowed a multi-line frame onto one line (stdio is line-delimited)");
5842
+ stdout.write(`${text4.replace(/[\r\n]+/g, " ")}
5843
+ `);
5844
+ return;
5845
+ }
5846
+ stdout.write(`${text4}
5847
+ `);
5848
+ };
5849
+ const requestHeaders = (isInitialize) => {
5850
+ const headers = {
5851
+ "Content-Type": "application/json",
5852
+ Accept: ACCEPT
5853
+ };
5854
+ if (sessionId) headers["Mcp-Session-Id"] = sessionId;
5855
+ if (protocolVersion && !isInitialize) {
5856
+ headers[PROTOCOL_HEADER] = protocolVersion;
5857
+ }
5858
+ return headers;
5859
+ };
5860
+ const attemptReauth = () => {
5861
+ if (reauthAttempt !== null && !reauthSpent) return reauthAttempt;
5862
+ if (reauthSpent || !opts.reauth) return Promise.resolve(false);
5863
+ const reauth = opts.reauth;
5864
+ reauthAttempt = (async () => {
5865
+ log2("The session key expired or was rotated \u2014 re-authenticating once.");
5866
+ try {
5867
+ session = await reauth();
5868
+ sessionId = null;
5869
+ return true;
5870
+ } catch (err) {
5871
+ log2(`Re-authentication failed: ${err.message}`);
5872
+ return false;
5873
+ } finally {
5874
+ reauthSpent = true;
5875
+ }
5876
+ })();
5877
+ return reauthAttempt;
5878
+ };
5879
+ const reinitialize = async () => {
5880
+ const frame = JSON.stringify({
5881
+ jsonrpc: "2.0",
5882
+ id: PROXY_REINITIALIZE_ID,
5883
+ method: "initialize",
5884
+ params: {
5885
+ // Echo what was negotiated the first time, so the server is not asked
5886
+ // to renegotiate a version the client upstream has already committed
5887
+ // to. The floor is only reached if we never saw a handshake at all.
5888
+ protocolVersion: protocolVersion ?? "2024-11-05",
5889
+ capabilities: {},
5890
+ clientInfo: { name: "dashwise-cli", version: "0" }
5891
+ }
5892
+ });
5893
+ try {
5894
+ const res = await post(frame, true);
5895
+ if (!res || !res.ok) {
5896
+ trace(`re-initialize refused (HTTP ${res?.status ?? "none"}).`);
5897
+ return false;
5898
+ }
5899
+ captureTransportState(res, await res.text());
5900
+ return sessionId !== null;
5901
+ } catch (err) {
5902
+ trace(`re-initialize failed: ${err.message}`);
5903
+ return false;
5904
+ }
5905
+ };
5906
+ const post = async (text4, isInitialize) => {
5907
+ if (stopped) return null;
5908
+ return doFetch(session.connectUrl, {
5909
+ method: "POST",
5910
+ headers: requestHeaders(isInitialize),
5911
+ body: text4
5912
+ });
5913
+ };
5914
+ const captureTransportState = (res, bodyText) => {
5915
+ const returned = res.headers.get(SESSION_HEADER);
5916
+ if (returned) sessionId = returned;
5917
+ if (protocolVersion === null) {
5918
+ try {
5919
+ const parsed = JSON.parse(bodyText);
5920
+ const result = parsed.result;
5921
+ const negotiated = result?.protocolVersion;
5922
+ if (typeof negotiated === "string") protocolVersion = negotiated;
5923
+ } catch {
5924
+ }
5925
+ }
5926
+ const learned = readOnlyToolsFrom(bodyText);
5927
+ if (learned !== null) readTools = learned;
5928
+ };
5929
+ const relay = async (text4, frame) => {
5930
+ const isInitialize = frame?.method === "initialize";
5931
+ let res;
5932
+ try {
5933
+ res = await post(text4, isInitialize);
5934
+ } catch (err) {
5935
+ const message = err.message;
5936
+ log2(`Transport error talking to ${redactUrl(session.connectUrl)}: ${message}`);
5937
+ emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
5938
+ return { kind: "failed" };
5939
+ }
5940
+ if (res === null) return { kind: "failed" };
5941
+ if (res.status === 401 && opts.reauth) {
5942
+ const fresh = await attemptReauth();
5943
+ if (!fresh) {
5944
+ emit(
5945
+ transportErrorFrame(
5946
+ frame?.id ?? null,
5947
+ "Re-authentication failed; the Dashwise session could not be renewed."
5948
+ )
5949
+ );
5950
+ exitCode = 1;
5951
+ stop();
5952
+ return { kind: "failed" };
5953
+ }
5954
+ res = await post(text4, isInitialize);
5955
+ if (res === null) return { kind: "failed" };
5956
+ }
5957
+ const bodyText = await res.text();
5958
+ if (!res.ok) {
5959
+ const detail = res.status === 401 ? "the endpoint key is no longer valid (expired, rotated by another `dashwise mcp`, or revoked)" : `HTTP ${res.status}`;
5960
+ log2(`Refused by ${redactUrl(session.connectUrl)}: ${detail}`);
5961
+ emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
5962
+ if (res.status === 401) {
5963
+ exitCode = 1;
5964
+ stop();
5965
+ }
5966
+ return { kind: "failed" };
5967
+ }
5968
+ captureTransportState(res, bodyText);
5969
+ if (res.status === 202 || bodyText.trim().length === 0) {
5970
+ return { kind: "empty" };
5971
+ }
5972
+ return { kind: "body", text: bodyText };
5973
+ };
5974
+ const learnReadOnlyTools = async () => {
5975
+ if (!readOnly || readTools !== null || stopped) return;
5976
+ const probe = JSON.stringify({
5977
+ jsonrpc: "2.0",
5978
+ id: PROXY_TOOLS_LIST_ID,
5979
+ method: "tools/list",
5980
+ params: {}
5981
+ });
5982
+ try {
5983
+ const res = await post(probe, false);
5984
+ if (!res || !res.ok) {
5985
+ trace("read-only guard: tools/list probe was refused; failing closed.");
5986
+ return;
5987
+ }
5988
+ const text4 = await res.text();
5989
+ captureTransportState(res, text4);
5990
+ const learned = readOnlyToolsFrom(text4);
5991
+ trace(`read-only guard: ${learned?.size ?? 0} read-only tools learned from tools/list.`);
5992
+ } catch (err) {
5993
+ trace(
5994
+ `read-only guard: tools/list probe failed (${err.message}); failing closed.`
5995
+ );
5996
+ }
5997
+ };
5998
+ const handleLine = async (line) => {
5999
+ const text4 = line.trim();
6000
+ if (text4.length === 0) return;
6001
+ const frame = inspectFrame(text4);
6002
+ trace(
6003
+ `\u2192 ${frame?.method ?? (frame?.isBatch ? "batch" : "frame")} (${Buffer.byteLength(text4, "utf8")} bytes)`
6004
+ );
6005
+ const refusable = readOnly ? (frame?.elements ?? []).filter(
6006
+ (e) => e.toolName !== void 0 && !readTools?.has(e.toolName)
6007
+ ) : [];
6008
+ if (refusable.length > 0) {
6009
+ const names = refusable.map((e) => e.toolName).join(", ");
6010
+ trace(
6011
+ `read-only guard refused ${names}${frame?.isBatch ? " (whole batch)" : ""}`
6012
+ );
6013
+ if (frame?.isBatch) {
6014
+ const replies = frame.elements.filter((e) => e.id !== null).map(
6015
+ (e) => isReadOnlyRefusal(e.id, e.toolName ?? "(batch)", readTools)
6016
+ );
6017
+ if (replies.length > 0) emit(replies);
6018
+ } else {
6019
+ emit(isReadOnlyRefusal(frame.id, refusable[0].toolName, readTools));
6020
+ }
6021
+ return;
6022
+ }
6023
+ const outcome = await relay(text4, frame);
6024
+ if (outcome.kind === "failed") return;
6025
+ if (outcome.kind === "body") {
6026
+ trace(`\u2190 response (${Buffer.byteLength(outcome.text, "utf8")} bytes)`);
6027
+ emitFrame(outcome.text, "POST response");
6028
+ }
6029
+ if (frame?.method === "initialize") {
6030
+ await learnReadOnlyTools();
6031
+ } else if (frame?.method === "notifications/initialized") {
6032
+ openServerStream();
6033
+ }
6034
+ };
6035
+ const scheduleStreamRetry = () => {
6036
+ if (stopped || streamUnavailable) return;
6037
+ streamTimer = setTimeout(() => {
6038
+ streamTimer = null;
6039
+ void runServerStream();
6040
+ }, reconnectMs);
6041
+ };
6042
+ const runServerStream = async () => {
6043
+ if (stopped || streamUnavailable) return;
6044
+ streamAbort = new AbortController();
6045
+ let res;
6046
+ try {
6047
+ res = await doFetch(session.connectUrl, {
6048
+ method: "GET",
6049
+ headers: {
6050
+ Accept: "text/event-stream",
6051
+ ...sessionId ? { "Mcp-Session-Id": sessionId } : {},
6052
+ ...protocolVersion ? { [PROTOCOL_HEADER]: protocolVersion } : {}
6053
+ },
6054
+ signal: streamAbort.signal
6055
+ });
6056
+ } catch (err) {
6057
+ if (stopped) return;
6058
+ trace(`server stream: ${err.message}; retrying.`);
6059
+ scheduleStreamRetry();
6060
+ return;
6061
+ }
6062
+ if (res.status === 405 || res.status === 404) {
6063
+ streamUnavailable = true;
6064
+ trace(
6065
+ `server stream: this server has no server-initiated stream (HTTP ${res.status}); not retrying.`
6066
+ );
6067
+ return;
6068
+ }
6069
+ if (res.status === 401) {
6070
+ const fresh = await attemptReauth();
6071
+ if (fresh) {
6072
+ if (await reinitialize()) {
6073
+ streamUnavailable = false;
6074
+ streamStarted = false;
6075
+ trace("server stream: re-authenticated and re-initialized; reopening.");
6076
+ openServerStream();
6077
+ return;
6078
+ }
6079
+ trace("server stream: re-authenticated but could not re-initialize.");
6080
+ }
6081
+ streamUnavailable = true;
6082
+ log2(
6083
+ `Server stream refused by ${redactUrl(session.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
6084
+ );
6085
+ emit(
6086
+ transportErrorFrame(
6087
+ null,
6088
+ "Dashwise MCP server: the session could not be renewed; the server stream is closed."
6089
+ )
6090
+ );
6091
+ exitCode = 1;
6092
+ stop();
6093
+ return;
6094
+ }
6095
+ if (!res.ok || !res.body) {
6096
+ streamUnavailable = true;
6097
+ trace(
6098
+ `server stream: HTTP ${res.status}${res.body ? "" : " with no body"}; not retrying.`
6099
+ );
6100
+ return;
6101
+ }
6102
+ trace("server stream: open.");
6103
+ const reader = res.body.getReader();
6104
+ const decoder = new TextDecoder();
6105
+ let buffer2 = "";
6106
+ try {
6107
+ for (; ; ) {
6108
+ const { done: finished, value } = await reader.read();
6109
+ if (finished) break;
6110
+ buffer2 += decoder.decode(value, { stream: true });
6111
+ let sep2 = buffer2.indexOf("\n\n");
6112
+ while (sep2 !== -1) {
6113
+ const event = buffer2.slice(0, sep2);
6114
+ buffer2 = buffer2.slice(sep2 + 2);
6115
+ const payload = eventData(event);
6116
+ if (payload !== null) emitFrame(payload, "server stream");
6117
+ sep2 = buffer2.indexOf("\n\n");
6118
+ }
6119
+ }
6120
+ } catch (err) {
6121
+ if (!stopped) trace(`server stream dropped: ${err.message}`);
6122
+ }
6123
+ if (!stopped) {
6124
+ trace("server stream closed; reconnecting.");
6125
+ scheduleStreamRetry();
6126
+ }
6127
+ };
6128
+ const openServerStream = () => {
6129
+ if (streamStarted || stopped) return;
6130
+ streamStarted = true;
6131
+ void runServerStream();
6132
+ };
6133
+ let pending = Promise.resolve();
6134
+ let buffer = "";
6135
+ const onData = (chunk) => {
6136
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
6137
+ let nl = buffer.indexOf("\n");
6138
+ while (nl !== -1) {
6139
+ const line = buffer.slice(0, nl);
6140
+ buffer = buffer.slice(nl + 1);
6141
+ pending = pending.then(() => handleLine(line));
6142
+ nl = buffer.indexOf("\n");
6143
+ }
6144
+ };
6145
+ const onEnd = () => {
6146
+ void pending.then(() => {
6147
+ stop();
6148
+ });
6149
+ };
6150
+ stdin.on("data", onData);
6151
+ stdin.on("end", onEnd);
6152
+ stdin.on("close", onEnd);
6153
+ if (typeof stdin.resume === "function") {
6154
+ stdin.resume();
6155
+ }
6156
+ function stop() {
6157
+ if (stopped) return;
6158
+ stopped = true;
6159
+ if (streamTimer) {
6160
+ clearTimeout(streamTimer);
6161
+ streamTimer = null;
6162
+ }
6163
+ if (streamAbort) {
6164
+ streamAbort.abort();
6165
+ streamAbort = null;
6166
+ }
6167
+ stdin.off("data", onData);
6168
+ stdin.off("end", onEnd);
6169
+ stdin.off("close", onEnd);
6170
+ if (typeof stdin.pause === "function") {
6171
+ stdin.pause();
6172
+ }
6173
+ resolveDone(exitCode);
6174
+ }
6175
+ return {
6176
+ stop,
6177
+ isRunning: () => !stopped,
6178
+ done
6179
+ };
6180
+ }
6181
+ function eventData(event) {
6182
+ const parts = [];
6183
+ for (const line of event.split("\n")) {
6184
+ if (!line.startsWith("data:")) continue;
6185
+ const chunk = line.slice(5);
6186
+ parts.push(chunk.startsWith(" ") ? chunk.slice(1) : chunk);
6187
+ }
6188
+ if (parts.length === 0) return null;
6189
+ const payload = parts.join("\n").trim();
6190
+ return payload.length > 0 ? payload : null;
6191
+ }
6192
+
6193
+ // src/lib/mcp-scope.ts
6194
+ var MCP_SCOPES_MAX_ITEMS = 64;
6195
+ var MAX_ID = 2147483647;
6196
+ var SCOPE_PATTERN = /^datahub:(?:(table|app)=([1-9][0-9]{0,9})|(workspace)):([crud]{1,4})$/;
6197
+ var MCP_GOVERNANCE_WORKSPACE_READ_SCOPE = "governance:workspace:r";
6198
+ var GRAMMAR_HINT = "Expected datahub:table=<id>:<ops>, datahub:app=<id>:<ops>, datahub:workspace:<ops> or governance:workspace:r, where <ops> is one to four of c, r, u and d, each at most once.";
6199
+ function isMcpScope(raw) {
6200
+ if (typeof raw !== "string") return false;
6201
+ if (raw === MCP_GOVERNANCE_WORKSPACE_READ_SCOPE) return true;
6202
+ const match = SCOPE_PATTERN.exec(raw);
6203
+ if (!match) return false;
6204
+ const idText = match[2];
6205
+ const opsText = match[4];
6206
+ if (new Set(opsText).size !== opsText.length) return false;
6207
+ if (idText !== void 0) {
6208
+ const id = Number(idText);
6209
+ if (!Number.isSafeInteger(id) || id > MAX_ID) return false;
6210
+ }
6211
+ return true;
6212
+ }
6213
+ function checkMcpScopes(scopes, flag = "--scope") {
6214
+ if (scopes.length > MCP_SCOPES_MAX_ITEMS) {
6215
+ return {
6216
+ ok: false,
6217
+ message: `At most ${MCP_SCOPES_MAX_ITEMS} ${flag} grants fit one session; got ${scopes.length}.`
6218
+ };
6219
+ }
6220
+ const bad = scopes.findIndex((scope) => !isMcpScope(scope));
6221
+ if (bad !== -1) {
6222
+ return {
6223
+ ok: false,
6224
+ message: `${flag} ${JSON.stringify(scopes[bad])} is not a scope. ${GRAMMAR_HINT}`
6225
+ };
6226
+ }
6227
+ return { ok: true };
6228
+ }
6229
+
6230
+ // src/commands/mcp.ts
6231
+ init_output();
6232
+ function fileExists(path) {
6233
+ try {
6234
+ return statSync(path).isFile();
6235
+ } catch {
6236
+ return false;
6237
+ }
6238
+ }
6239
+ function whereOnWindows(cmd, env, isFile) {
6240
+ const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
6241
+ for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
6242
+ if (!dir) continue;
6243
+ for (const ext of extensions) {
6244
+ const candidate = win32.join(dir, `${cmd}${ext.toLowerCase()}`);
6245
+ if (isFile(candidate)) return candidate;
6246
+ }
6247
+ }
6248
+ return void 0;
6249
+ }
6250
+ function spawnTarget(cmd, args, opts = {}) {
6251
+ const direct = { command: cmd, args, shell: false };
6252
+ if ((opts.platform ?? process.platform) !== "win32") return direct;
6253
+ const found = whereOnWindows(cmd, opts.env ?? process.env, opts.isFile ?? fileExists);
6254
+ if (found === void 0 || !/\.(cmd|bat)$/i.test(found)) return direct;
6255
+ const unsafe = args.find((arg) => /["%\r\n]/.test(arg));
6256
+ if (unsafe !== void 0) {
6257
+ return {
6258
+ refused: `cannot pass ${JSON.stringify(unsafe)} to ${found}: cmd.exe would act on the quote, % or line break in it.`
6259
+ };
6260
+ }
6261
+ return { command: [found, ...args].map((part) => `"${part}"`).join(" "), args: [], shell: true };
6262
+ }
6263
+
6264
+ // src/commands/mcp.ts
6265
+ init_workspaces();
6266
+ var SCOPES_NOT_APPLIED = "This Dashwise server minted the session but did not confirm any scopes, so the key is NOT narrowed. The session was not started, and its revocation was requested. Update the backend, or run without --scope.";
6267
+ var REVOKE_TIMEOUT_MS = 2e3;
6268
+ async function openCliSession(args) {
6269
+ const res = await apiRequest("POST", "/api/mcp-endpoints/cli-session", {
6270
+ workspace: args.workspaceId,
6271
+ label: args.label,
6272
+ // Only when the flag was given: a body without the key is what every
6273
+ // backend accepts, and one before `--scope` refuses the key as unknown.
6274
+ ...args.scopes !== void 0 ? { scopes: args.scopes } : {}
6275
+ });
6276
+ return {
6277
+ connectUrl: res.connect_url,
6278
+ endpointId: res.endpoint_id,
6279
+ expiresAt: res.expires_at,
6280
+ name: res.name,
6281
+ scopes: res.scopes
6282
+ };
6283
+ }
6284
+ function describeScopes(scopes) {
6285
+ if (scopes === null || scopes === void 0) {
6286
+ return "Scopes: none \u2014 this key reaches what your account can in this workspace (narrow it with --scope).";
6287
+ }
6288
+ if (scopes.length === 0) return "Scopes: deny-all \u2014 no data tool is available to this key.";
6289
+ return `Scopes (as stored by the server): ${scopes.join(", ")}.`;
6290
+ }
6291
+ async function revokeSession(endpointId) {
6292
+ const attempt = apiRequest("POST", `/api/mcp-endpoints/${endpointId}/revoke`).then(
6293
+ () => true,
6294
+ () => false
6295
+ );
6296
+ const timeout = new Promise(
6297
+ (resolve13) => setTimeout(() => resolve13(false), REVOKE_TIMEOUT_MS).unref()
6298
+ );
6299
+ await Promise.race([attempt, timeout]);
6300
+ }
6301
+ function describeSessionError(err, apiUrl, scopes) {
6302
+ if (!(err instanceof ApiError)) return err.message;
6303
+ if (err.status === 0) {
6304
+ return `Could not reach the Dashwise backend at ${apiUrl}: ${err.message}`;
6305
+ }
6306
+ if (err.status === 401) {
6307
+ return "Your saved credentials are no longer valid. Run `dashwise login` again.";
6308
+ }
6309
+ if (err.code === "SCOPE_EXCEEDS_CREATOR_ACCESS") {
6310
+ const scope = typeof err.context?.scope === "string" ? err.context.scope : void 0;
6311
+ const lead = scope ? `--scope ${scope} reaches beyond your own access in this workspace` : "A --scope grant reaches beyond your own access in this workspace";
6312
+ return `${lead}; a scope can only narrow what you can already do. ${err.message}`;
6313
+ }
6314
+ if (err.code === "SCOPE_MALFORMED") {
6315
+ const index = typeof err.context?.index === "number" ? err.context.index : void 0;
6316
+ const grant = index !== void 0 ? scopes?.[index] : void 0;
6317
+ return grant !== void 0 ? `The server does not accept --scope ${grant}: ${err.message}` : `The server refused --scope: ${err.message}`;
6318
+ }
6319
+ if (scopes !== void 0 && err.status === 400 && /\bscopes\b/.test(err.message)) {
6320
+ return `The server refused the scopes field (${err.message}). A Dashwise backend older than --scope answers this way; update it, or run without --scope.`;
6321
+ }
6322
+ return err.message;
6323
+ }
6324
+ async function resolveWorkspace(apiUrl, apiToken, flag) {
6325
+ const cfg = readConfig();
6326
+ const wanted = flag ?? cfg?.workspaceSlug;
6327
+ return selectWorkspace({
6328
+ apiUrl,
6329
+ apiToken,
6330
+ ...wanted !== void 0 ? { flag: wanted } : {},
6331
+ // Never interactive: this command's stdout is a protocol channel and its
6332
+ // stdin belongs to the MCP client. A prompt here would deadlock a client
6333
+ // that launched us as a subprocess.
6334
+ interactive: false
6335
+ });
6336
+ }
6337
+ async function mcpCommand(options = {}) {
6338
+ const scopes = options.scopes;
6339
+ if (scopes !== void 0) {
6340
+ const checked = checkMcpScopes(scopes);
6341
+ if (!checked.ok) {
6342
+ fail(checked.message);
6343
+ return 1;
6344
+ }
5519
6345
  }
6346
+ const cfg = readConfig();
5520
6347
  if (!cfg) {
5521
- info("Already logged out (no saved credentials).");
5522
- return 0;
6348
+ fail("Not logged in. Run `dashwise login` first.");
6349
+ return 1;
5523
6350
  }
6351
+ const apiUrl = resolveApiUrl();
6352
+ let workspace2;
5524
6353
  try {
5525
- await apiRequest(
5526
- "DELETE",
5527
- `/api/auth/cli-tokens/${cfg.tokenId}`,
5528
- void 0,
5529
- { auth: { token: cfg.token }, baseUrl: cfg.apiUrl }
5530
- );
6354
+ workspace2 = await resolveWorkspace(apiUrl, cfg.token, options.workspace);
6355
+ } catch (err) {
6356
+ fail(err.message);
6357
+ return 1;
6358
+ }
6359
+ const label = options.endpointName ?? defaultEndpointLabel();
6360
+ let session;
6361
+ try {
6362
+ session = await openCliSession({
6363
+ workspaceId: workspace2.id,
6364
+ label,
6365
+ ...scopes !== void 0 ? { scopes } : {}
6366
+ });
5531
6367
  } catch (err) {
6368
+ fail(describeSessionError(err, apiUrl, scopes));
6369
+ return 1;
6370
+ }
6371
+ if (scopes !== void 0 && !Array.isArray(session.scopes)) {
6372
+ await revokeSession(session.endpointId);
6373
+ fail(SCOPES_NOT_APPLIED);
6374
+ return 1;
6375
+ }
6376
+ info(
6377
+ `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session.endpointId} (${session.name}).`
6378
+ );
6379
+ info(`Session key expires ${dim(session.expiresAt)}.`);
6380
+ info(describeScopes(session.scopes));
6381
+ if (options.readOnly) {
5532
6382
  warn(
5533
- `Could not revoke the server-side token (${err.message}). Local credentials cleared anyway.`
6383
+ "Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
5534
6384
  );
5535
6385
  }
5536
- deleteConfig();
5537
- success(`Logged out ${code(cfg.user.email)}`);
5538
- return 0;
6386
+ let ownedEndpointId = session.endpointId;
6387
+ const proxy = startMcpProxy({
6388
+ session,
6389
+ reauth: async () => {
6390
+ const fresh = await openCliSession({
6391
+ workspaceId: workspace2.id,
6392
+ label,
6393
+ ...scopes !== void 0 ? { scopes } : {}
6394
+ });
6395
+ if (scopes !== void 0 && !Array.isArray(fresh.scopes)) {
6396
+ await revokeSession(fresh.endpointId);
6397
+ throw new Error(SCOPES_NOT_APPLIED);
6398
+ }
6399
+ ownedEndpointId = fresh.endpointId;
6400
+ return fresh;
6401
+ },
6402
+ ...options.readOnly !== void 0 ? { readOnly: options.readOnly } : {},
6403
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
6404
+ });
6405
+ const shutdown = () => proxy.stop();
6406
+ process.once("SIGINT", shutdown);
6407
+ process.once("SIGTERM", shutdown);
6408
+ try {
6409
+ const exitCode = await proxy.done;
6410
+ await revokeSession(ownedEndpointId);
6411
+ return exitCode;
6412
+ } finally {
6413
+ process.off("SIGINT", shutdown);
6414
+ process.off("SIGTERM", shutdown);
6415
+ }
6416
+ }
6417
+ function claudeMcpAddArgv(args) {
6418
+ const argv = [
6419
+ "mcp",
6420
+ "add",
6421
+ args.serverName,
6422
+ "--scope",
6423
+ args.scope,
6424
+ "--",
6425
+ "dashwise",
6426
+ "mcp",
6427
+ "--workspace",
6428
+ args.workspaceSlug
6429
+ ];
6430
+ for (const grant of args.grantScopes ?? []) argv.push("--scope", grant);
6431
+ if (args.readOnly) argv.push("--read-only");
6432
+ if (args.endpointName) argv.push("--endpoint-name", args.endpointName);
6433
+ return argv;
6434
+ }
6435
+ async function mcpAddCommand(options = {}) {
6436
+ const serverName = options.name ?? "dashwise";
6437
+ const scope = options.scope ?? "local";
6438
+ if (options.grantScopes !== void 0) {
6439
+ const checked = checkMcpScopes(options.grantScopes, "--grant-scope");
6440
+ if (!checked.ok) {
6441
+ fail(checked.message);
6442
+ return 1;
6443
+ }
6444
+ }
6445
+ let workspaceSlug = options.workspace;
6446
+ if (workspaceSlug === void 0) {
6447
+ const cfg = readConfig();
6448
+ if (!cfg) {
6449
+ fail("Not logged in. Run `dashwise login` first, or pass --workspace <slug>.");
6450
+ return 1;
6451
+ }
6452
+ try {
6453
+ workspaceSlug = (await resolveWorkspace(resolveApiUrl(), cfg.token, void 0)).slug;
6454
+ } catch (err) {
6455
+ fail(err.message);
6456
+ return 1;
6457
+ }
6458
+ }
6459
+ const argv = claudeMcpAddArgv({
6460
+ serverName,
6461
+ scope,
6462
+ workspaceSlug,
6463
+ ...options.readOnly !== void 0 ? { readOnly: options.readOnly } : {},
6464
+ ...options.endpointName !== void 0 ? { endpointName: options.endpointName } : {},
6465
+ ...options.grantScopes !== void 0 ? { grantScopes: options.grantScopes } : {}
6466
+ });
6467
+ const printable = `claude ${argv.join(" ")}`;
6468
+ info("Registering the Dashwise MCP server with Claude Code:");
6469
+ out(printable);
6470
+ const target = spawnTarget("claude", argv);
6471
+ if ("refused" in target) {
6472
+ fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
6473
+ return 1;
6474
+ }
6475
+ return await new Promise((resolve13) => {
6476
+ const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
6477
+ let settled = false;
6478
+ child.on("error", (err) => {
6479
+ if (settled) return;
6480
+ settled = true;
6481
+ if (err.code === "ENOENT") {
6482
+ fail(
6483
+ `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
6484
+ );
6485
+ resolve13(1);
6486
+ return;
6487
+ }
6488
+ fail(`Could not run \`claude\`: ${err.message}`);
6489
+ resolve13(1);
6490
+ });
6491
+ child.on("close", (codeOut) => {
6492
+ if (settled) return;
6493
+ settled = true;
6494
+ resolve13(codeOut ?? 1);
6495
+ });
6496
+ });
5539
6497
  }
5540
6498
 
5541
- // src/commands/profile.ts
5542
- init_api_client();
6499
+ // src/commands/agent.ts
5543
6500
  init_config();
5544
6501
  init_output();
5545
- async function profileCommand(options = {}) {
5546
- const cfg = readConfig();
5547
- if (!cfg) {
5548
- if (options.json) {
5549
- out(JSON.stringify({ ok: false, error: { code: "NOT_LOGGED_IN", message: "Run `dashwise login` first." } }));
5550
- } else {
5551
- fail("Not logged in. Run `dashwise login` first.");
5552
- }
5553
- return 1;
6502
+ var PLUGIN_NAME = "dashwise";
6503
+ var MARKETPLACE_NAME = "dashwise";
6504
+ var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
6505
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve13) => {
6506
+ const target = spawnTarget(cmd, args);
6507
+ if ("refused" in target) {
6508
+ resolve13({ code: null, stdout: "", stderr: target.refused, missing: false });
6509
+ return;
5554
6510
  }
5555
- let me = null;
5556
- let refreshError = null;
6511
+ const child = spawn(target.command, target.args, {
6512
+ shell: target.shell,
6513
+ stdio: opts.capture ? ["ignore", "pipe", "pipe"] : "inherit"
6514
+ });
6515
+ let stdout = "";
6516
+ let stderr = "";
6517
+ child.stdout?.on("data", (chunk) => stdout += chunk.toString());
6518
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
6519
+ let settled = false;
6520
+ child.on("error", (err) => {
6521
+ if (settled) return;
6522
+ settled = true;
6523
+ resolve13({
6524
+ code: null,
6525
+ stdout,
6526
+ stderr: stderr || err.message,
6527
+ missing: err.code === "ENOENT"
6528
+ });
6529
+ });
6530
+ child.on("close", (exitCode) => {
6531
+ if (settled) return;
6532
+ settled = true;
6533
+ resolve13({ code: exitCode, stdout, stderr, missing: false });
6534
+ });
6535
+ });
6536
+ function parsePluginList(text4) {
6537
+ let parsed;
5557
6538
  try {
5558
- me = await apiRequest("GET", "/api/auth/me");
5559
- } catch (err) {
5560
- if (err instanceof ApiError && err.status === 401) {
5561
- if (options.json) {
5562
- out(JSON.stringify({ ok: false, error: { code: "TOKEN_INVALID", message: "Saved token is no longer valid." } }));
5563
- } else {
5564
- fail("Saved token is no longer valid. Run `dashwise login` again.");
6539
+ parsed = JSON.parse(text4);
6540
+ } catch {
6541
+ return null;
6542
+ }
6543
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.plugins) ? parsed.plugins : null;
6544
+ if (!list) return null;
6545
+ return list.flatMap((entry) => {
6546
+ if (!entry || typeof entry !== "object") return [];
6547
+ const e = entry;
6548
+ if (typeof e.id !== "string") return [];
6549
+ return [
6550
+ {
6551
+ id: e.id,
6552
+ ...typeof e.version === "string" ? { version: e.version } : {},
6553
+ ...typeof e.scope === "string" ? { scope: e.scope } : {},
6554
+ ...typeof e.enabled === "boolean" ? { enabled: e.enabled } : {}
5565
6555
  }
5566
- return 1;
5567
- }
5568
- refreshError = err.message;
5569
- if (!options.json) {
5570
- warn(
5571
- `Could not refresh profile from the server (${refreshError}). Showing cached values.`
6556
+ ];
6557
+ });
6558
+ }
6559
+ function parseMarketplaceList(text4) {
6560
+ let parsed;
6561
+ try {
6562
+ parsed = JSON.parse(text4);
6563
+ } catch {
6564
+ return null;
6565
+ }
6566
+ if (!Array.isArray(parsed)) return null;
6567
+ return parsed.flatMap((entry) => {
6568
+ if (!entry || typeof entry !== "object") return [];
6569
+ const e = entry;
6570
+ if (typeof e.name !== "string") return [];
6571
+ const where = [e.repo, e.url, e.path, e.source].find((v) => typeof v === "string");
6572
+ return [{ name: e.name, ...typeof where === "string" ? { source: where } : {} }];
6573
+ });
6574
+ }
6575
+ function manualSteps(source) {
6576
+ return [
6577
+ `claude plugin marketplace add ${source}`,
6578
+ `claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`
6579
+ ];
6580
+ }
6581
+ async function agentSetupCommand(options = {}, deps = {}) {
6582
+ const run = deps.run ?? defaultRunner;
6583
+ const loggedIn = deps.loggedIn ?? isLoggedIn;
6584
+ const json = options.json === true;
6585
+ const source = options.marketplace ?? DEFAULT_MARKETPLACE_SOURCE;
6586
+ const pluginId = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
6587
+ const report = {
6588
+ ok: false,
6589
+ action: options.status ? "status" : "setup",
6590
+ claude: { found: false },
6591
+ marketplace: { name: MARKETPLACE_NAME, source, configured: null },
6592
+ plugin: { id: pluginId, installed: null },
6593
+ steps: []
6594
+ };
6595
+ const finish = (exitCode) => {
6596
+ report.ok = exitCode === 0;
6597
+ if (json) out(JSON.stringify(report, null, 2));
6598
+ return exitCode;
6599
+ };
6600
+ const say = (line) => {
6601
+ if (!json) info(line);
6602
+ };
6603
+ const version = await run("claude", ["--version"], { capture: true });
6604
+ if (version.missing && options.status) {
6605
+ report.next = ["Install Claude Code, then run `dashwise agent setup`."];
6606
+ if (!json) {
6607
+ out("Claude Code: not installed (no `claude` on your PATH)");
6608
+ info(`Once it is, ${code("dashwise agent setup")} installs the Dashwise plugin.`);
6609
+ }
6610
+ return finish(0);
6611
+ }
6612
+ if (version.missing) {
6613
+ report.manual_steps = manualSteps(source);
6614
+ if (!json) {
6615
+ warn("Claude Code (`claude`) is not on your PATH, so nothing was installed.");
6616
+ info("Once Claude Code is installed, these two commands add the Dashwise plugin:");
6617
+ for (const line of report.manual_steps) out(line);
6618
+ info(
6619
+ `Or inside Claude Code: ${code(`/plugin marketplace add ${source}`)}, then ${code(`/plugin install ${pluginId}`)}.`
5572
6620
  );
5573
6621
  }
6622
+ return finish(0);
5574
6623
  }
5575
- if (me) {
5576
- const refreshed = {
5577
- ...cfg,
5578
- user: {
5579
- id: me.id,
5580
- email: me.email,
5581
- username: me.username,
5582
- firstName: me.first_name,
5583
- lastName: me.last_name
5584
- }
6624
+ if (version.code !== 0) {
6625
+ report.error = `claude --version exited ${version.code}: ${version.stderr.trim()}`;
6626
+ if (!json)
6627
+ fail(
6628
+ `Could not run \`claude --version\`: ${version.stderr.trim() || `exit ${version.code}`}`
6629
+ );
6630
+ return finish(1);
6631
+ }
6632
+ report.claude = { found: true, version: version.stdout.trim() };
6633
+ say(`Claude Code found (${report.claude.version}).`);
6634
+ const listed = await run("claude", ["plugin", "list", "--json"], { capture: true });
6635
+ const plugins = listed.code === 0 ? parsePluginList(listed.stdout) : null;
6636
+ if (plugins) {
6637
+ const ours = plugins.find((p) => p.id.toLowerCase() === pluginId.toLowerCase());
6638
+ const others = plugins.filter((p) => p !== ours && p.id.toLowerCase().startsWith(`${PLUGIN_NAME}@`)).map((p) => p.id);
6639
+ report.plugin = {
6640
+ id: pluginId,
6641
+ installed: ours !== void 0,
6642
+ ...ours?.version !== void 0 ? { version: ours.version } : {},
6643
+ ...ours?.scope !== void 0 ? { scope: ours.scope } : {},
6644
+ ...ours?.enabled !== void 0 ? { enabled: ours.enabled } : {},
6645
+ ...others.length > 0 ? { others } : {}
5585
6646
  };
5586
- writeConfig(refreshed);
5587
- }
5588
- const u = me ?? toMe(cfg.user);
5589
- let workspaces = [];
5590
- let workspaceError = null;
5591
- try {
5592
- const fetched = await apiRequest(
5593
- "GET",
5594
- "/api/workspaces"
5595
- );
5596
- workspaces = Array.isArray(fetched) ? fetched : [];
5597
- } catch (err) {
5598
- workspaceError = err.message;
5599
6647
  }
5600
- if (options.json) {
5601
- out(
5602
- JSON.stringify({
5603
- ok: true,
5604
- user: {
5605
- id: u.id,
5606
- username: u.username,
5607
- email: u.email,
5608
- first_name: u.first_name,
5609
- last_name: u.last_name
5610
- },
5611
- default_workspace: cfg.workspaceSlug ?? null,
5612
- workspaces,
5613
- api_url: cfg.apiUrl,
5614
- ...refreshError ? { profile_refresh_error: refreshError } : {},
5615
- ...workspaceError ? { workspaces_error: workspaceError } : {}
5616
- })
5617
- );
5618
- return 0;
6648
+ const markets = await run("claude", ["plugin", "marketplace", "list", "--json"], {
6649
+ capture: true
6650
+ });
6651
+ const marketplaces = markets.code === 0 ? parseMarketplaceList(markets.stdout) : null;
6652
+ const existing = marketplaces?.find((m) => m.name === MARKETPLACE_NAME);
6653
+ if (marketplaces) {
6654
+ report.marketplace.configured = existing !== void 0;
6655
+ if (existing?.source !== void 0) report.marketplace.configured_source = existing.source;
6656
+ }
6657
+ if (options.status) {
6658
+ if (!json) printStatus(report);
6659
+ if (plugins === null) {
6660
+ report.error = `claude plugin list --json gave nothing this command can read (exit ${listed.code}).`;
6661
+ if (!json) fail(report.error);
6662
+ return finish(1);
6663
+ }
6664
+ return finish(0);
6665
+ }
6666
+ if (report.plugin.installed === true) {
6667
+ if (!json) {
6668
+ success(
6669
+ `${pluginId} is already installed${report.plugin.version ? ` (${report.plugin.version})` : ""}.`
6670
+ );
6671
+ info(`To update it: ${code(`claude plugin update ${pluginId}`)}, then restart Claude Code.`);
6672
+ }
6673
+ return finish(0);
5619
6674
  }
5620
- out("");
5621
- kv("User ID", `${u.id}`);
5622
- kv("Username", u.username);
5623
- kv("Email", u.email);
5624
- kv("Name", `${u.first_name} ${u.last_name}`.trim() || dim("(not set)"));
5625
- out("");
5626
- if (workspaceError) {
5627
- warn(
5628
- `Could not list workspaces (${workspaceError}). Try \`dashwise workspace list\`.`
6675
+ if (existing) {
6676
+ say(
6677
+ `The ${MARKETPLACE_NAME} marketplace is already configured` + (existing.source ? ` (from ${existing.source}).` : ".")
5629
6678
  );
5630
- } else if (workspaces.length === 0) {
5631
- info("No workspaces yet.");
5632
- } else {
5633
- kv("Workspaces", `${workspaces.length}`);
5634
- for (const w of workspaces) {
5635
- const slug = w.slug ?? `id:${w.id}`;
5636
- const role = w.role ?? w.permissions ?? "";
5637
- const star = cfg.workspaceSlug === slug ? code(" (default)") : "";
5638
- out(` - ${slug}${role ? dim(` [${role}]`) : ""}${star} ${dim(w.name)}`);
6679
+ if (options.marketplace !== void 0 && existing.source !== void 0 && existing.source !== source) {
6680
+ if (!json) {
6681
+ warn(
6682
+ `--marketplace ${source} was not used: a marketplace named ${MARKETPLACE_NAME} already points at ${existing.source}. To switch, run ${code(`claude plugin marketplace remove ${MARKETPLACE_NAME}`)} and try again.`
6683
+ );
6684
+ }
5639
6685
  }
6686
+ } else {
6687
+ const added = await step(run, ["plugin", "marketplace", "add", source], json, report);
6688
+ if (added !== 0) return failed(report, source, json, finish, added);
6689
+ report.marketplace.configured = true;
6690
+ report.marketplace.configured_source = source;
6691
+ }
6692
+ const installed = await step(run, ["plugin", "install", pluginId], json, report);
6693
+ if (installed !== 0) return failed(report, source, json, finish, installed);
6694
+ report.plugin.installed = true;
6695
+ report.next = ["Restart Claude Code so it loads the plugin."];
6696
+ if (!loggedIn())
6697
+ report.next.push("Run `dashwise login` once, so the plugin\u2019s MCP server can start.");
6698
+ if (!json) {
6699
+ success(`Installed ${pluginId}.`);
6700
+ for (const line of report.next) info(line);
6701
+ }
6702
+ return finish(0);
6703
+ }
6704
+ async function step(run, args, json, report) {
6705
+ const printable = `claude ${args.join(" ")}`;
6706
+ if (!json) info(`Running: ${printable}`);
6707
+ const result = await run("claude", args, { capture: json });
6708
+ const exitCode = result.code ?? 1;
6709
+ report.steps.push({ command: printable, exit_code: result.code });
6710
+ if (exitCode !== 0 && json)
6711
+ report.error = result.stderr.trim() || `${printable} exited ${exitCode}`;
6712
+ return exitCode;
6713
+ }
6714
+ function failed(report, source, json, finish, exitCode) {
6715
+ report.manual_steps = manualSteps(source);
6716
+ if (!json) {
6717
+ const last = report.steps[report.steps.length - 1];
6718
+ fail(
6719
+ `\`${last?.command}\` failed (exit ${last?.exit_code ?? "none"}). Nothing after it was run.`
6720
+ );
6721
+ info("To finish by hand:");
6722
+ for (const line of report.manual_steps) out(line);
5640
6723
  }
5641
- out("");
5642
- kv("API URL", cfg.apiUrl);
5643
- return 0;
6724
+ return finish(exitCode);
5644
6725
  }
5645
- function toMe(cached) {
5646
- return {
5647
- id: cached.id,
5648
- email: cached.email,
5649
- username: cached.username,
5650
- first_name: cached.firstName,
5651
- last_name: cached.lastName,
5652
- is_staff: false,
5653
- is_active: true,
5654
- date_joined: ""
5655
- };
6726
+ function printStatus(report) {
6727
+ out(`Claude Code: ${report.claude.version ?? "found"}`);
6728
+ const m = report.marketplace;
6729
+ out(
6730
+ `Marketplace ${m.name}: ` + (m.configured === null ? "could not tell" : m.configured ? `configured${m.configured_source ? ` (${m.configured_source})` : ""}` : `not configured (setup would add ${m.source})`)
6731
+ );
6732
+ const p = report.plugin;
6733
+ const detail = [
6734
+ p.version,
6735
+ p.scope && `${p.scope} scope`,
6736
+ p.enabled === false ? "disabled" : p.enabled ? "enabled" : void 0
6737
+ ].filter(Boolean).join(", ");
6738
+ out(
6739
+ `Plugin ${p.id}: ` + (p.installed === null ? "could not tell" : p.installed ? `installed${detail ? ` (${detail})` : ""}` : "not installed")
6740
+ );
6741
+ if (p.others?.length) out(`Also installed from other marketplaces: ${p.others.join(", ")}`);
5656
6742
  }
5657
6743
 
5658
6744
  // src/commands/workspace.ts
@@ -6138,7 +7224,7 @@ async function moduleConvertCommand(opts) {
6138
7224
  ...opts.name !== void 0 ? { name: opts.name } : {},
6139
7225
  ...opts.git !== void 0 ? { repositoryUrl: opts.git } : {}
6140
7226
  });
6141
- console.log(`Converted "${result.name}" (slug: ${result.slug}) to a DashWise module.
7227
+ console.log(`Converted "${result.name}" (slug: ${result.slug}) to a Dashwise module.
6142
7228
  `);
6143
7229
  if (result.created.length) {
6144
7230
  console.log(" Created:");
@@ -6161,7 +7247,7 @@ async function moduleConvertCommand(opts) {
6161
7247
  for (const w of result.warnings) console.log(` ! ${w}`);
6162
7248
  }
6163
7249
  console.log(
6164
- "\nNext: `npm install`, then `dashwise dev` to run it against DashWise, or `dashwise module publish` to deploy."
7250
+ "\nNext: `npm install`, then `dashwise dev` to run it against Dashwise, or `dashwise module publish` to deploy."
6165
7251
  );
6166
7252
  return 0;
6167
7253
  } catch (err) {
@@ -6423,7 +7509,7 @@ ${issues}
6423
7509
  );
6424
7510
  }
6425
7511
  info(
6426
- `Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy.${tablesApplied === 0 ? ` Add tables with ${code("dashwise table create")}.` : ""}`
7512
+ `Next: ${code("dashwise dev")} to run it against Dashwise, or ${code("dashwise module publish")} to deploy.${tablesApplied === 0 ? ` Add tables with ${code("dashwise table create")}.` : ""}`
6427
7513
  );
6428
7514
  return 0;
6429
7515
  }
@@ -7618,7 +8704,7 @@ async function pollBuild(buildId, moduleId, slug, options) {
7618
8704
  `/api/modules/${moduleId}/builds/${buildId}`
7619
8705
  );
7620
8706
  } catch {
7621
- await sleep2(intervalMs);
8707
+ await sleep(intervalMs);
7622
8708
  continue;
7623
8709
  }
7624
8710
  if (!options.json && build.status !== lastStatus) {
@@ -7647,14 +8733,14 @@ async function pollBuild(buildId, moduleId, slug, options) {
7647
8733
  }
7648
8734
  return 1;
7649
8735
  }
7650
- await sleep2(intervalMs);
8736
+ await sleep(intervalMs);
7651
8737
  }
7652
8738
  warn(
7653
8739
  `Still building after ${options.timeoutSec ?? 360}s \u2014 the build is likely still running. Re-run ${code("dashwise module publish")} to check, or inspect build ${code(`#${buildId}`)}.`
7654
8740
  );
7655
8741
  return 0;
7656
8742
  }
7657
- function sleep2(ms) {
8743
+ function sleep(ms) {
7658
8744
  return new Promise((r) => setTimeout(r, ms));
7659
8745
  }
7660
8746
  function formatBytes2(n) {
@@ -10497,7 +11583,7 @@ async function envExportCommand(options) {
10497
11583
  {
10498
11584
  key: "DASHWISE_API_URL",
10499
11585
  value: apiUrl,
10500
- comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
11586
+ comment: "# Dashwise backend URL \u2014 same value the CLI is logged into."
10501
11587
  },
10502
11588
  {
10503
11589
  key: "DASHWISE_API_KEY",
@@ -10861,7 +11947,7 @@ async function runTail(moduleId, options) {
10861
11947
  } catch (err) {
10862
11948
  info(dim(`(tail poll error: ${err.message})`));
10863
11949
  }
10864
- await sleep3(POLL_INTERVAL_MS);
11950
+ await sleep2(POLL_INTERVAL_MS);
10865
11951
  }
10866
11952
  }
10867
11953
  function buildTailParams(options) {
@@ -10974,7 +12060,7 @@ function formatRow(row) {
10974
12060
  function formatTimestamp(iso) {
10975
12061
  return iso.slice(0, 19).replace("T", " ");
10976
12062
  }
10977
- function sleep3(ms) {
12063
+ function sleep2(ms) {
10978
12064
  return new Promise((resolve13) => setTimeout(resolve13, ms));
10979
12065
  }
10980
12066
 
@@ -11323,7 +12409,7 @@ async function runFollow(installationId, label, opts, auth) {
11323
12409
  return mapLogsError(err, false);
11324
12410
  }
11325
12411
  info(dim(`(follow: transient error \u2014 ${err.message})`));
11326
- await sleep4(POLL_INTERVAL_MS3, () => stopped);
12412
+ await sleep3(POLL_INTERVAL_MS3, () => stopped);
11327
12413
  firstPoll = false;
11328
12414
  continue;
11329
12415
  }
@@ -11346,7 +12432,7 @@ async function runFollow(installationId, label, opts, auth) {
11346
12432
  }
11347
12433
  }
11348
12434
  firstPoll = false;
11349
- await sleep4(POLL_INTERVAL_MS3, () => stopped);
12435
+ await sleep3(POLL_INTERVAL_MS3, () => stopped);
11350
12436
  }
11351
12437
  } finally {
11352
12438
  process.off("SIGINT", onSigint);
@@ -11439,7 +12525,7 @@ function mapLogsError(err, json) {
11439
12525
  else fail(message);
11440
12526
  return 1;
11441
12527
  }
11442
- async function sleep4(ms, aborted) {
12528
+ async function sleep3(ms, aborted) {
11443
12529
  const slice = 100;
11444
12530
  let waited = 0;
11445
12531
  while (waited < ms && !aborted()) {
@@ -11451,9 +12537,9 @@ async function sleep4(ms, aborted) {
11451
12537
  // src/program.ts
11452
12538
  init_output();
11453
12539
  var program = new Command();
11454
- program.name("dashwise").description("DashWise CLI \u2014 log in, author modules, publish to the marketplace.").version(getVersion(), "-v, --version", "Show the CLI version + exit").option(
12540
+ program.name("dashwise").description("Dashwise CLI \u2014 log in, author modules, publish to the marketplace.").version(getVersion(), "-v, --version", "Show the CLI version + exit").option(
11455
12541
  "--api-url <url>",
11456
- "Override DashWise backend URL (default: $DASHWISE_API_URL or saved config or http://localhost:3000)"
12542
+ "Override Dashwise backend URL (default: $DASHWISE_API_URL or saved config or http://localhost:3000)"
11457
12543
  );
11458
12544
  program.hook("preAction", () => {
11459
12545
  const apiUrl = program.opts().apiUrl?.trim();
@@ -11479,6 +12565,84 @@ program.command("profile").description("Show the current user, default workspace
11479
12565
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
11480
12566
  });
11481
12567
  });
12568
+ var mcp = program.command("mcp").description(
12569
+ "Run a local MCP server for a workspace, so Claude can read and write your data (`dashwise mcp` runs `serve`; see `dashwise mcp serve --help`), or register it with Claude Code (`add`)."
12570
+ );
12571
+ var collect = (value, previous) => [
12572
+ ...previous ?? [],
12573
+ value
12574
+ ];
12575
+ mcp.command("serve", { isDefault: true }).description(
12576
+ "Run the local MCP server (the default). The key is exchanged at startup and held in memory \u2014 it never reaches a config file or your shell history."
12577
+ ).option(
12578
+ "--workspace <slug>",
12579
+ "Workspace to connect (default: `dashwise workspace use`)."
12580
+ ).option(
12581
+ "--scope <grant>",
12582
+ "Narrow this session's key, enforced by the server. Repeatable; grants add up. `datahub:table=<id>:<ops>`, `datahub:app=<id>:<ops>`, `datahub:workspace:<ops>` (ops: one to four of c r u d) or `governance:workspace:r`. Each must be within your own access. Omitted: the key reaches what your account can.",
12583
+ collect
12584
+ ).option(
12585
+ "--read-only",
12586
+ "Refuse write tools. A CLIENT-SIDE GUARD: this proxy declines the call and the server is not told. It prevents accidents; it is not a fence. For a server-enforced limit, use --scope (for example `--scope datahub:workspace:r`)."
12587
+ ).option(
12588
+ "--endpoint-name <label>",
12589
+ "Label for this machine's endpoint (default: your hostname). Each run gets its own endpoint, named `dashwise-cli@<label>#<id>` \u2014 the server adds the short id so concurrent sessions on one machine are told apart in the endpoints panel, where an admin can revoke one of them. The session lasts 24 hours and is revoked automatically on exit."
12590
+ ).option(
12591
+ "--verbose",
12592
+ "Log frame types and sizes to stderr. Never frame bodies, never the key."
12593
+ ).action(
12594
+ async (cmdOpts) => {
12595
+ process.exitCode = await mcpCommand({
12596
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
12597
+ ...cmdOpts.scope !== void 0 ? { scopes: cmdOpts.scope } : {},
12598
+ ...cmdOpts.readOnly !== void 0 ? { readOnly: cmdOpts.readOnly } : {},
12599
+ ...cmdOpts.endpointName !== void 0 ? { endpointName: cmdOpts.endpointName } : {},
12600
+ ...cmdOpts.verbose !== void 0 ? { verbose: cmdOpts.verbose } : {}
12601
+ });
12602
+ }
12603
+ );
12604
+ mcp.command("add").description(
12605
+ "Register `dashwise mcp` with Claude Code by shelling out to `claude mcp add`. Prints the exact command it runs, so it can be copied when `claude` is absent."
12606
+ ).option("--workspace <slug>", "Workspace to bake into the registration.").option(
12607
+ "--read-only",
12608
+ "Register the read-only form (client-side guard; see `dashwise mcp serve --help`)."
12609
+ ).option("--endpoint-name <label>", "Label for this machine's endpoint.").option(
12610
+ "--name <server-name>",
12611
+ "Name Claude Code shows for the server (default: dashwise)."
12612
+ ).addOption(
12613
+ new Option(
12614
+ "--scope <scope>",
12615
+ "Claude Code's config scope \u2014 where the registration is saved. Not the key's reach: for that, --grant-scope."
12616
+ ).choices(["local", "project", "user"]).default("local")
12617
+ ).option(
12618
+ "--grant-scope <grant>",
12619
+ "Narrow the registered server's key, enforced by the server: written into the registration as `dashwise mcp --scope <grant>`. Repeatable; grants add up. Same grammar as `dashwise mcp serve --scope`.",
12620
+ collect
12621
+ ).action(
12622
+ async (cmdOpts) => {
12623
+ process.exitCode = await mcpAddCommand({
12624
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
12625
+ ...cmdOpts.readOnly !== void 0 ? { readOnly: cmdOpts.readOnly } : {},
12626
+ ...cmdOpts.endpointName !== void 0 ? { endpointName: cmdOpts.endpointName } : {},
12627
+ ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
12628
+ ...cmdOpts.scope !== void 0 ? { scope: cmdOpts.scope } : {},
12629
+ ...cmdOpts.grantScope !== void 0 ? { grantScopes: cmdOpts.grantScope } : {}
12630
+ });
12631
+ }
12632
+ );
12633
+ var agent = program.command("agent").description("Set up a coding agent to work with Dashwise (`dashwise agent setup`).");
12634
+ agent.command("setup").description(
12635
+ "Install the Dashwise plugin into Claude Code \u2014 its skills, the `dashwise` MCP server and the secrets guard \u2014 by running `claude plugin marketplace add` and `claude plugin install`. Without Claude Code, prints the two commands instead."
12636
+ ).option(
12637
+ "--marketplace <source>",
12638
+ `Marketplace to add: owner/repo, a git URL, a local path or a URL (default: ${DEFAULT_MARKETPLACE_SOURCE}).`
12639
+ ).option("--status", "Report what is installed, and change nothing.").option("--json", "Print one JSON object on stdout instead of progress lines.").action(async (cmdOpts) => {
12640
+ process.exitCode = await agentSetupCommand({
12641
+ ...cmdOpts.marketplace !== void 0 ? { marketplace: cmdOpts.marketplace } : {},
12642
+ ...cmdOpts.status !== void 0 ? { status: cmdOpts.status } : {},
12643
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
12644
+ });
12645
+ });
11482
12646
  var workspace = program.command("workspace").description("Workspace context commands.");
11483
12647
  workspace.command("list").description("List accessible workspaces.").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
11484
12648
  process.exitCode = await workspaceListCommand({
@@ -11489,7 +12653,7 @@ workspace.command("use <slug>").description("Set the default workspace for subse
11489
12653
  process.exitCode = await workspaceUseCommand(slug);
11490
12654
  });
11491
12655
  program.command("dev").description(
11492
- "One-command local dev loop: runs the manifest watcher + Next.js dev server together. Registers a dev-mode SSO session with the backend so workspace members can sign into your locally-running module via DashWise SSO (no installation_id required). Auth + dev-session env auto-injected into both child processes from your saved CLI config. Run `dashwise login` first."
12656
+ "One-command local dev loop: runs the manifest watcher + Next.js dev server together. Registers a dev-mode SSO session with the backend so workspace members can sign into your locally-running module via Dashwise SSO (no installation_id required). Auth + dev-session env auto-injected into both child processes from your saved CLI config. Run `dashwise login` first."
11493
12657
  ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option(
11494
12658
  "--installation-id <id>",
11495
12659
  "Use prod-mode SSO with this installation id instead of registering a dev session. The installation must already exist in the target workspace."
@@ -11566,13 +12730,13 @@ var initAction = async (slugArg, cmdOpts) => {
11566
12730
  };
11567
12731
  initOptions(
11568
12732
  program.command("init [slug]").description(
11569
- "Scaffold a new DashWise module \u2014 full Next.js app with auth wiring + SSO callback + protected dashboard. Pass --simple for the legacy minimal scaffold (no auth). Top-level alias for `dashwise module init`."
12733
+ "Scaffold a new Dashwise module \u2014 full Next.js app with auth wiring + SSO callback + protected dashboard. Pass --simple for the legacy minimal scaffold (no auth). Top-level alias for `dashwise module init`."
11570
12734
  )
11571
12735
  ).action(initAction);
11572
12736
  var moduleCmd = program.command("module").description("Module authoring commands.");
11573
12737
  initOptions(
11574
12738
  moduleCmd.command("init [slug]").description(
11575
- "Scaffold a new DashWise module \u2014 full Next.js app with auth wiring + SSO callback + protected dashboard. Pass --simple for the legacy minimal scaffold (no auth)."
12739
+ "Scaffold a new Dashwise module \u2014 full Next.js app with auth wiring + SSO callback + protected dashboard. Pass --simple for the legacy minimal scaffold (no auth)."
11576
12740
  )
11577
12741
  ).action(initAction);
11578
12742
  moduleCmd.command("connect-git <repo-url>").description("Link the local module to a Git repository (writes module.json#repository).").option("--dir <path>", "Project root (default: current directory).").option("--branch <name>", "Default branch to publish from (e.g. `main`).").action(
@@ -11584,7 +12748,7 @@ moduleCmd.command("connect-git <repo-url>").description("Link the local module t
11584
12748
  }
11585
12749
  );
11586
12750
  moduleCmd.command("convert [dir]").description(
11587
- "Convert an EXISTING Next.js app into a DashWise module \u2014 idempotently inject module.json, auth wiring, the SDK dep, and output:standalone. Creates missing files; warns (never overwrites) for an existing layout/next.config/module.json."
12751
+ "Convert an EXISTING Next.js app into a Dashwise module \u2014 idempotently inject module.json, auth wiring, the SDK dep, and output:standalone. Creates missing files; warns (never overwrites) for an existing layout/next.config/module.json."
11588
12752
  ).option("--slug <slug>", "Module slug (default: derived from package.json name / dir).").option("--name <name>", "Human-readable module name (default: package.json name).").option("--git <url>", "Git repository URL \u2014 written to module.json#repository.url.").action(
11589
12753
  async (dir, cmdOpts) => {
11590
12754
  process.exitCode = await moduleConvertCommand({
@@ -11596,7 +12760,7 @@ moduleCmd.command("convert [dir]").description(
11596
12760
  }
11597
12761
  );
11598
12762
  moduleCmd.command("register [dir]").description(
11599
- "Link an existing local module (a directory with module.json \u2014 e.g. after `module convert`) to DashWise: creates the backend module record + a dev install and caches the runtime token, so `dashwise dev` / `publish` work. Requires login."
12763
+ "Link an existing local module (a directory with module.json \u2014 e.g. after `module convert`) to Dashwise: creates the backend module record + a dev install and caches the runtime token, so `dashwise dev` / `publish` work. Requires login."
11600
12764
  ).option("--workspace <slug-or-id>", "Target workspace (default: interactive picker).").option("--name <name>", "Override the module name (default: module.json name).").action(
11601
12765
  async (dir, cmdOpts) => {
11602
12766
  process.exitCode = await moduleRegisterCommand({
@@ -11811,7 +12975,7 @@ program.command("query").description(
11811
12975
  }
11812
12976
  );
11813
12977
  program.command("run [cmd...]").description(
11814
- "Run a command with the module's DashWise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Example: `dashwise run -- node seed.mjs`."
12978
+ "Run a command with the module's Dashwise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Example: `dashwise run -- node seed.mjs`."
11815
12979
  ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").action(async (cmdArgs, cmdOpts) => {
11816
12980
  const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
11817
12981
  process.exitCode = await runCommand2(cmdArgs ?? [], {
@@ -11932,7 +13096,7 @@ originsCmd.command("remove <url>").description("Remove an SSO callback origin fr
11932
13096
  });
11933
13097
  var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
11934
13098
  envCmd.command("export").description(
11935
- "Emit the env vars a deployed module needs to authenticate against DashWise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
13099
+ "Emit the env vars a deployed module needs to authenticate against Dashwise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
11936
13100
  ).option("--dir <path>", "Project root (default: current directory).").option(
11937
13101
  "--format <fmt>",
11938
13102
  "Output format: env | json | railway | vercel | fly (default: env)."
@@ -12226,7 +13390,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
12226
13390
  }
12227
13391
  );
12228
13392
  function getVersion() {
12229
- return "0.18.2";
13393
+ return "0.19.0";
12230
13394
  }
12231
13395
  function parseIntOption(value) {
12232
13396
  const n = Number.parseInt(value, 10);