@dashai/cli 0.18.3 → 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/README.md +149 -0
- package/dist/bin.js +1432 -268
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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
|
|
|
@@ -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,64 +3814,6 @@ 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
3818
|
intro("Scaffolding a new Dashwise module");
|
|
3815
3819
|
let slug = slugArg;
|
|
@@ -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
|
|
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
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
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
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5522
|
-
return
|
|
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
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
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/
|
|
5542
|
-
init_api_client();
|
|
6499
|
+
// src/commands/agent.ts
|
|
5543
6500
|
init_config();
|
|
5544
6501
|
init_output();
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
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
|
-
|
|
5556
|
-
|
|
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
|
-
|
|
5559
|
-
} catch
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
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
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
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 (
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
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
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
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
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
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
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
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
|
-
|
|
5642
|
-
kv("API URL", cfg.apiUrl);
|
|
5643
|
-
return 0;
|
|
6724
|
+
return finish(exitCode);
|
|
5644
6725
|
}
|
|
5645
|
-
function
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
8743
|
+
function sleep(ms) {
|
|
7658
8744
|
return new Promise((r) => setTimeout(r, ms));
|
|
7659
8745
|
}
|
|
7660
8746
|
function formatBytes2(n) {
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12528
|
+
async function sleep3(ms, aborted) {
|
|
11443
12529
|
const slice = 100;
|
|
11444
12530
|
let waited = 0;
|
|
11445
12531
|
while (waited < ms && !aborted()) {
|
|
@@ -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({
|
|
@@ -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.
|
|
13393
|
+
return "0.19.0";
|
|
12230
13394
|
}
|
|
12231
13395
|
function parseIntOption(value) {
|
|
12232
13396
|
const n = Number.parseInt(value, 10);
|