captain-tool 0.0.39 → 0.0.41
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 +12 -0
- package/bin/captain-tool +30 -23
- package/bin/captain-tool-setup +254 -36
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -89,6 +89,18 @@ claude mcp add captain -- cmd /c captain-tool
|
|
|
89
89
|
|
|
90
90
|
> Registers under the short name `captain` (tool prefix `mcp__captain__`). If you previously added it as `captain-tool`, run `claude mcp remove captain-tool` to avoid a duplicate registration.
|
|
91
91
|
|
|
92
|
+
**Auto-approve captain tools (recommended).** Registration alone does not grant permissions — Claude Code will prompt "Allow captain to …?" on every tool call. The setup wizard (`npx captain-tool setup`) offers to fix this for you; to do it by hand, add one allow rule to `~/.claude/settings.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"permissions": {
|
|
97
|
+
"allow": ["mcp__captain"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`mcp__captain` (no tool suffix) approves every tool from this server. Your own `deny` and `ask` rules always take precedence over this allow rule. Note the rule prefix is the *registration key*: if you registered under a different name, the rule must match it. Old `mcp__captain-tool__*` rules from before the rename match nothing — the wizard sweeps them out of `~/.claude/settings.json`; per-project `.claude/settings.local.json` files can hold more, so clean those by hand if Claude Code still prompts. For scripted installs, `npx captain-tool setup --yes` configures all clients and enables auto-approval without prompting.
|
|
103
|
+
|
|
92
104
|
### VS Code + Copilot
|
|
93
105
|
|
|
94
106
|
[](vscode:mcp/install?%7B%22name%22%3A%22captain%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22captain-tool%22%5D%2C%22env%22%3A%7B%22CAPTAIN_API_URL%22%3A%22https%3A%2F%2Fapp.getcaptain.dev%2F%22%7D%7D)
|
package/bin/captain-tool
CHANGED
|
@@ -63,30 +63,37 @@ function findBinary() {
|
|
|
63
63
|
|
|
64
64
|
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
65
65
|
|
|
66
|
-
// If the first real argument is 'setup', hand off to the interactive setup CLI
|
|
67
|
-
//
|
|
66
|
+
// If the first real argument is 'setup', hand off to the interactive setup CLI
|
|
67
|
+
// in this same process. We must CALL the exported main() — the setup script
|
|
68
|
+
// gates its wizard on `require.main === module` (so tests can require its
|
|
69
|
+
// helpers without launching prompts), which means a bare require() runs
|
|
70
|
+
// nothing. An earlier version did exactly that and then exit(0)'d, making
|
|
71
|
+
// `captain-tool setup` a silent no-op. main() is async and owns its own exit
|
|
72
|
+
// codes; on success the process exits naturally when the wizard finishes, so
|
|
73
|
+
// there must be no process.exit() after this call — it would kill main() at
|
|
74
|
+
// its first await.
|
|
68
75
|
if (process.argv[2] === 'setup') {
|
|
69
|
-
require('./captain-tool-setup')
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const binary = findBinary();
|
|
76
|
+
require('./captain-tool-setup').main().catch((err) => {
|
|
77
|
+
console.error('Unexpected error:', err);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const binary = findBinary();
|
|
76
82
|
|
|
77
|
-
if (!binary) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
83
|
+
if (!binary) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
'captain-tool: no pre-built binary found for this platform.\n' +
|
|
86
|
+
'If you are on an unsupported platform, please open an issue at:\n' +
|
|
87
|
+
' https://github.com/your-org/captain-tool/issues\n'
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
85
91
|
|
|
86
|
-
// Spawn the Rust binary, forwarding all arguments after the node script name.
|
|
87
|
-
// stdio: 'inherit' is non-negotiable — see the module-level comment above.
|
|
88
|
-
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
92
|
+
// Spawn the Rust binary, forwarding all arguments after the node script name.
|
|
93
|
+
// stdio: 'inherit' is non-negotiable — see the module-level comment above.
|
|
94
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
89
95
|
|
|
90
|
-
// spawnSync returns null for status if the process was killed by a signal.
|
|
91
|
-
// We default to exit code 1 in that case so the MCP host sees a failure.
|
|
92
|
-
process.exit(result.status ?? 1);
|
|
96
|
+
// spawnSync returns null for status if the process was killed by a signal.
|
|
97
|
+
// We default to exit code 1 in that case so the MCP host sees a failure.
|
|
98
|
+
process.exit(result.status ?? 1);
|
|
99
|
+
}
|
package/bin/captain-tool-setup
CHANGED
|
@@ -39,6 +39,21 @@ const SERVER_NAME = 'captain';
|
|
|
39
39
|
// is left alone.
|
|
40
40
|
const LEGACY_NAMES = ['captain-tool'];
|
|
41
41
|
|
|
42
|
+
// Claude Code permission rule that auto-approves every captain tool call.
|
|
43
|
+
// Registering an MCP server does NOT grant it permissions — without an allow
|
|
44
|
+
// rule Claude Code prompts on every single tool call, which makes captain
|
|
45
|
+
// unusable as a background workflow tool. `mcp__<server>` (no tool suffix) is
|
|
46
|
+
// the documented "all tools from this server" form. Derived from SERVER_NAME
|
|
47
|
+
// because the rule prefix IS the registration key — a future rename must not
|
|
48
|
+
// be able to update one without the other.
|
|
49
|
+
const PERMISSION_RULE = `mcp__${SERVER_NAME}`;
|
|
50
|
+
|
|
51
|
+
// Permission-rule prefixes for every name this server was registered under
|
|
52
|
+
// previously (same derivation, one per LEGACY_NAMES entry). After a rename
|
|
53
|
+
// the old rules match nothing — they silently re-enable per-call prompting
|
|
54
|
+
// while still looking like a working allowlist — so the wizard sweeps them.
|
|
55
|
+
const LEGACY_PERMISSION_PREFIXES = LEGACY_NAMES.map((name) => `mcp__${name}`);
|
|
56
|
+
|
|
42
57
|
// ── Binary resolution (same logic as the runner) ─────────────────────────────
|
|
43
58
|
|
|
44
59
|
const PLATFORMS = [
|
|
@@ -109,6 +124,10 @@ const CLIENTS = [
|
|
|
109
124
|
// ~/.claude.json is special: it has many top-level keys (auth state, prefs, etc.)
|
|
110
125
|
// We only touch the mcpServers key and leave everything else alone.
|
|
111
126
|
rootKey: 'mcpServers',
|
|
127
|
+
// Claude Code is the only client whose permission system we can configure
|
|
128
|
+
// from a file (~/.claude/settings.json). Other clients use in-app
|
|
129
|
+
// "always allow" buttons, so they get no permissions step.
|
|
130
|
+
permissionsPath: () => expandPath('~/.claude/settings.json'),
|
|
112
131
|
},
|
|
113
132
|
{
|
|
114
133
|
name: 'VS Code + Copilot',
|
|
@@ -312,6 +331,101 @@ function removeLegacyEverywhere(config) {
|
|
|
312
331
|
}
|
|
313
332
|
}
|
|
314
333
|
|
|
334
|
+
// ── Claude Code permission allowlist ──────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Mutate a parsed ~/.claude/settings.json object so captain tools are
|
|
338
|
+
* auto-approved. Two independent changes, each gated by its own option:
|
|
339
|
+
*
|
|
340
|
+
* 1. sweepLegacy — drop allow rules for legacy server names
|
|
341
|
+
* (`mcp__captain-tool` and `mcp__captain-tool__<tool>`). The rule prefix
|
|
342
|
+
* is the registration key, so after the rename these match nothing —
|
|
343
|
+
* keeping them around just hides the fact that nothing is allowlisted
|
|
344
|
+
* anymore. Removing them never narrows what's allowed, which is why the
|
|
345
|
+
* caller runs the sweep even when the user declines auto-approval.
|
|
346
|
+
* 2. addAllowRule — add the server-wide `mcp__captain` rule unless an
|
|
347
|
+
* equivalent is already present. This one widens permissions, so it is
|
|
348
|
+
* consent-gated.
|
|
349
|
+
*
|
|
350
|
+
* Only `permissions.allow` is touched; deny/ask rules and every other settings
|
|
351
|
+
* key are preserved verbatim (a user deny or ask rule outranks our allow rule
|
|
352
|
+
* anyway, so we must not edit those). Returns { added, removed } so the caller
|
|
353
|
+
* can tell the user exactly what changed.
|
|
354
|
+
*/
|
|
355
|
+
function applyPermissionRule(settings, { addAllowRule = true, sweepLegacy = true } = {}) {
|
|
356
|
+
if (!settings.permissions || typeof settings.permissions !== 'object') {
|
|
357
|
+
settings.permissions = {};
|
|
358
|
+
}
|
|
359
|
+
if (!Array.isArray(settings.permissions.allow)) {
|
|
360
|
+
settings.permissions.allow = [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let removed = 0;
|
|
364
|
+
if (sweepLegacy) {
|
|
365
|
+
const isLegacy = (rule) =>
|
|
366
|
+
typeof rule === 'string' &&
|
|
367
|
+
LEGACY_PERMISSION_PREFIXES.some((p) => rule === p || rule.startsWith(`${p}__`));
|
|
368
|
+
// Single pass: keep the survivors, derive the count from the length delta,
|
|
369
|
+
// and reassign immediately so no later code can touch the stale array.
|
|
370
|
+
const kept = settings.permissions.allow.filter((rule) => !isLegacy(rule));
|
|
371
|
+
removed = settings.permissions.allow.length - kept.length;
|
|
372
|
+
settings.permissions.allow = kept;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let added = false;
|
|
376
|
+
if (addAllowRule) {
|
|
377
|
+
// `mcp__captain` and `mcp__captain__*` are equivalent server-wide rules;
|
|
378
|
+
// don't stack a duplicate if either form is already there.
|
|
379
|
+
const covered = settings.permissions.allow.some(
|
|
380
|
+
(rule) => rule === PERMISSION_RULE || rule === `${PERMISSION_RULE}__*`
|
|
381
|
+
);
|
|
382
|
+
if (!covered) {
|
|
383
|
+
settings.permissions.allow.push(PERMISSION_RULE);
|
|
384
|
+
added = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { added, removed };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Read ~/.claude/settings.json (or start fresh), apply the allow rule /
|
|
392
|
+
* legacy sweep, and write back atomically. Same read-merge-write discipline
|
|
393
|
+
* as the MCP config writer: every key we don't own is left untouched — and
|
|
394
|
+
* when nothing changed we skip the write entirely, so a re-run never
|
|
395
|
+
* reformats the user's file or churns its mtime for a no-op.
|
|
396
|
+
*/
|
|
397
|
+
function configurePermissions(permissionsPath, opts) {
|
|
398
|
+
const settings = readJsonConfig(permissionsPath);
|
|
399
|
+
const result = applyPermissionRule(settings, opts);
|
|
400
|
+
if (result.added || result.removed > 0) {
|
|
401
|
+
atomicWriteJson(permissionsPath, settings);
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* True if the agent config registers a server under a legacy name that is NOT
|
|
408
|
+
* our binary. entryIsOurs promises such a server "is left alone" — that must
|
|
409
|
+
* extend to its permission rules: `mcp__captain-tool__*` rules are only stale
|
|
410
|
+
* if `captain-tool` was OUR dead registration, not someone else's live server.
|
|
411
|
+
* Checks every scope removeLegacyEverywhere does (top-level + per-project).
|
|
412
|
+
*/
|
|
413
|
+
function hasUnrelatedLegacyServer(config) {
|
|
414
|
+
const maps = [config.mcpServers, config.servers];
|
|
415
|
+
if (config.projects && typeof config.projects === 'object') {
|
|
416
|
+
for (const proj of Object.values(config.projects)) {
|
|
417
|
+
if (proj && typeof proj === 'object') {
|
|
418
|
+
maps.push(proj.mcpServers, proj.servers);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return maps.some(
|
|
423
|
+
(m) =>
|
|
424
|
+
m && typeof m === 'object' &&
|
|
425
|
+
LEGACY_NAMES.some((name) => m[name] && !entryIsOurs(m[name]))
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
315
429
|
// ── Per-client config writer ──────────────────────────────────────────────────
|
|
316
430
|
|
|
317
431
|
/**
|
|
@@ -351,16 +465,36 @@ function configureClient(client, binaryPath) {
|
|
|
351
465
|
// ── Interactive prompt helpers ────────────────────────────────────────────────
|
|
352
466
|
|
|
353
467
|
/**
|
|
354
|
-
* Read a single line from stdin. Returns a Promise<string>.
|
|
468
|
+
* Read a single line from stdin. Returns a Promise<string|null>.
|
|
355
469
|
* We keep our own readline interface and close it when done to avoid holding
|
|
356
470
|
* the event loop open after the prompts finish.
|
|
471
|
+
*
|
|
472
|
+
* Resolves null on EOF. WHY: when piped stdin runs out, readline emits
|
|
473
|
+
* 'close' and the question callback simply never fires — the awaited promise
|
|
474
|
+
* would stay pending forever, the event loop would drain, and node would exit
|
|
475
|
+
* 0 having configured nothing. Resolving null lets callers abort loudly
|
|
476
|
+
* instead of vanishing with a success exit code.
|
|
357
477
|
*/
|
|
358
478
|
function prompt(rl, question) {
|
|
359
479
|
return new Promise((resolve) => {
|
|
360
|
-
|
|
480
|
+
const onClose = () => resolve(null);
|
|
481
|
+
rl.once('close', onClose);
|
|
482
|
+
rl.question(question, (answer) => {
|
|
483
|
+
rl.removeListener('close', onClose);
|
|
484
|
+
resolve(answer.trim());
|
|
485
|
+
});
|
|
361
486
|
});
|
|
362
487
|
}
|
|
363
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Bail out when stdin hits EOF mid-wizard. Exiting non-zero matters: a
|
|
491
|
+
* calling script must not believe setup succeeded when nothing was written.
|
|
492
|
+
*/
|
|
493
|
+
function abortNoInput() {
|
|
494
|
+
console.error('\nNo input (stdin closed) — aborting without changes. Use --yes for non-interactive setup.');
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
|
|
364
498
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
365
499
|
|
|
366
500
|
async function main() {
|
|
@@ -388,57 +522,93 @@ async function main() {
|
|
|
388
522
|
}
|
|
389
523
|
console.log('');
|
|
390
524
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
});
|
|
397
|
-
console.log('');
|
|
398
|
-
|
|
399
|
-
const rl = readline.createInterface({
|
|
400
|
-
input: process.stdin,
|
|
401
|
-
output: process.stdout,
|
|
402
|
-
});
|
|
525
|
+
// Non-interactive paths first. `--yes`/`-y` configures every client and
|
|
526
|
+
// enables auto-approval without prompting (for scripts/CI). Without the
|
|
527
|
+
// flag, a non-interactive stdin must be rejected up front — see prompt()
|
|
528
|
+
// for why EOF mid-question would otherwise end as a silent exit 0.
|
|
529
|
+
const assumeYes = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
403
530
|
|
|
404
531
|
let selected;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
);
|
|
532
|
+
let wantPermissions = false;
|
|
533
|
+
if (assumeYes) {
|
|
534
|
+
selected = CLIENTS.map((_, i) => i);
|
|
535
|
+
wantPermissions = true;
|
|
536
|
+
console.log('--yes: configuring all clients and enabling auto-approval.');
|
|
537
|
+
console.log('');
|
|
538
|
+
} else if (!process.stdin.isTTY) {
|
|
539
|
+
console.error('stdin is not interactive. Re-run with --yes to configure all clients without prompts.');
|
|
540
|
+
process.exit(1);
|
|
541
|
+
} else {
|
|
542
|
+
// Present the client menu.
|
|
543
|
+
console.log('Select which agent(s) to configure:');
|
|
544
|
+
console.log(' 0. All of the above');
|
|
545
|
+
CLIENTS.forEach((client, i) => {
|
|
546
|
+
console.log(` ${i + 1}. ${client.name}`);
|
|
547
|
+
});
|
|
548
|
+
console.log('');
|
|
549
|
+
|
|
550
|
+
const rl = readline.createInterface({
|
|
551
|
+
input: process.stdin,
|
|
552
|
+
output: process.stdout,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
while (true) {
|
|
556
|
+
const answer = await prompt(
|
|
557
|
+
rl,
|
|
558
|
+
`Enter number(s) separated by commas (e.g. "1,3") or 0 for all: `
|
|
559
|
+
);
|
|
560
|
+
if (answer === null) abortNoInput();
|
|
561
|
+
|
|
562
|
+
// Parse the answer into a list of client indices (0-based into CLIENTS).
|
|
563
|
+
if (answer === '0') {
|
|
564
|
+
selected = CLIENTS.map((_, i) => i);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const indices = answer
|
|
569
|
+
.split(',')
|
|
570
|
+
.map((s) => parseInt(s.trim(), 10) - 1) // convert 1-based menu to 0-based
|
|
571
|
+
.filter((n) => Number.isInteger(n) && n >= 0 && n < CLIENTS.length);
|
|
410
572
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
573
|
+
if (indices.length === 0) {
|
|
574
|
+
console.log(`Please enter a number between 0 and ${CLIENTS.length}.`);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
selected = indices;
|
|
414
579
|
break;
|
|
415
580
|
}
|
|
416
581
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
582
|
+
// If Claude Code is among the selections, offer to auto-approve captain's
|
|
583
|
+
// tools. Registration alone still leaves Claude Code prompting "Allow
|
|
584
|
+
// captain to ...?" on every call; one allow rule makes it seamless. The
|
|
585
|
+
// user already opted into installing this server, so default to yes — but
|
|
586
|
+
// do ask, because the rule widens what an agent may run without prompting
|
|
587
|
+
// in every project on this machine.
|
|
588
|
+
const permClient = selected.map((idx) => CLIENTS[idx]).find((c) => c.permissionsPath);
|
|
589
|
+
if (permClient) {
|
|
590
|
+
const answer = await prompt(
|
|
591
|
+
rl,
|
|
592
|
+
`Auto-approve captain tools in Claude Code? Adds "${PERMISSION_RULE}" to\n` +
|
|
593
|
+
`permissions.allow in ${permClient.permissionsPath()} so tool calls never prompt. [Y/n]: `
|
|
594
|
+
);
|
|
595
|
+
if (answer === null) abortNoInput();
|
|
596
|
+
wantPermissions = answer === '' || /^y(es)?$/i.test(answer);
|
|
425
597
|
}
|
|
426
598
|
|
|
427
|
-
|
|
428
|
-
|
|
599
|
+
rl.close();
|
|
600
|
+
console.log('');
|
|
429
601
|
}
|
|
430
602
|
|
|
431
|
-
rl.close();
|
|
432
|
-
|
|
433
|
-
console.log('');
|
|
434
|
-
|
|
435
603
|
// Configure each selected client.
|
|
436
604
|
let anyFailed = false;
|
|
605
|
+
const succeeded = new Set();
|
|
437
606
|
for (const idx of selected) {
|
|
438
607
|
const client = CLIENTS[idx];
|
|
439
608
|
process.stdout.write(`Configuring ${client.name}... `);
|
|
440
609
|
try {
|
|
441
610
|
const writtenPath = configureClient(client, binaryPath);
|
|
611
|
+
succeeded.add(idx);
|
|
442
612
|
console.log('done.');
|
|
443
613
|
console.log(` Written to: ${writtenPath}`);
|
|
444
614
|
console.log(` ${client.restartMsg}`);
|
|
@@ -450,6 +620,46 @@ async function main() {
|
|
|
450
620
|
console.log('');
|
|
451
621
|
}
|
|
452
622
|
|
|
623
|
+
// Permissions step — deliberately OUTSIDE the registration loop and its
|
|
624
|
+
// try/catch: a problem in ~/.claude/settings.json must not be reported as a
|
|
625
|
+
// failed registration (the MCP entry was already written above), so this
|
|
626
|
+
// step warns instead of setting anyFailed. The stale-rule sweep runs even
|
|
627
|
+
// when the user declined auto-approval: dead legacy rules match nothing, so
|
|
628
|
+
// removing them never narrows what is allowed.
|
|
629
|
+
const permIdx = selected.find((idx) => CLIENTS[idx].permissionsPath && succeeded.has(idx));
|
|
630
|
+
if (permIdx !== undefined) {
|
|
631
|
+
const client = CLIENTS[permIdx];
|
|
632
|
+
const permissionsPath = client.permissionsPath();
|
|
633
|
+
try {
|
|
634
|
+
// Honor the "an unrelated server named captain-tool is left alone"
|
|
635
|
+
// promise (see LEGACY_NAMES): if such a server is still registered, its
|
|
636
|
+
// legacy-prefixed rules are live, not stale — skip the sweep.
|
|
637
|
+
const sweepLegacy = !hasUnrelatedLegacyServer(readJsonConfig(client.configPath(binaryPath)));
|
|
638
|
+
const { added, removed } = configurePermissions(permissionsPath, {
|
|
639
|
+
addAllowRule: wantPermissions,
|
|
640
|
+
sweepLegacy,
|
|
641
|
+
});
|
|
642
|
+
if (added) {
|
|
643
|
+
console.log(`Auto-approval: added "${PERMISSION_RULE}" to ${permissionsPath}`);
|
|
644
|
+
} else if (wantPermissions) {
|
|
645
|
+
console.log(`Auto-approval: "${PERMISSION_RULE}" already allowed in ${permissionsPath}`);
|
|
646
|
+
}
|
|
647
|
+
if (removed > 0) {
|
|
648
|
+
console.log(`Removed ${removed} stale permission rule(s) for the old server name (${LEGACY_PERMISSION_PREFIXES.join(', ')}).`);
|
|
649
|
+
}
|
|
650
|
+
if (wantPermissions) {
|
|
651
|
+
console.log('Note: stale rules can also live in per-project .claude/settings.local.json files — clean those by hand if Claude Code still prompts.');
|
|
652
|
+
}
|
|
653
|
+
console.log('');
|
|
654
|
+
} catch (err) {
|
|
655
|
+
const reason = err instanceof SyntaxError
|
|
656
|
+
? `${permissionsPath} is not valid JSON — fix it, or add "${PERMISSION_RULE}" to permissions.allow yourself`
|
|
657
|
+
: err.message;
|
|
658
|
+
console.warn(`Warning: auto-approval step skipped: ${reason}`);
|
|
659
|
+
console.log('');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
453
663
|
if (anyFailed) {
|
|
454
664
|
process.exit(1);
|
|
455
665
|
}
|
|
@@ -471,9 +681,17 @@ if (require.main === module) {
|
|
|
471
681
|
module.exports = {
|
|
472
682
|
SERVER_NAME,
|
|
473
683
|
LEGACY_NAMES,
|
|
684
|
+
PERMISSION_RULE,
|
|
685
|
+
LEGACY_PERMISSION_PREFIXES,
|
|
474
686
|
entryIsOurs,
|
|
475
687
|
removeLegacyEntries,
|
|
476
688
|
removeLegacyEverywhere,
|
|
477
689
|
configureClient,
|
|
478
690
|
resolveCommand,
|
|
691
|
+
applyPermissionRule,
|
|
692
|
+
configurePermissions,
|
|
693
|
+
hasUnrelatedLegacyServer,
|
|
694
|
+
// Exported so the npx runner (`captain-tool setup`) can launch the wizard:
|
|
695
|
+
// the require.main gate above means a bare require() runs nothing.
|
|
696
|
+
main,
|
|
479
697
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captain-tool",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.41",
|
|
4
4
|
"description": "MCP server connecting Claude Desktop and VS Code Copilot to Captain Cloud",
|
|
5
5
|
"bin": {
|
|
6
6
|
"captain-tool": "bin/captain-tool",
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"test": "node test/setup.test.js"
|
|
12
12
|
},
|
|
13
13
|
"optionalDependencies": {
|
|
14
|
-
"@captain-tool/captain-tool-win32-x64": "0.0.
|
|
15
|
-
"@captain-tool/captain-tool-darwin-x64": "0.0.
|
|
16
|
-
"@captain-tool/captain-tool-darwin-arm64": "0.0.
|
|
17
|
-
"@captain-tool/captain-tool-linux-x64": "0.0.
|
|
14
|
+
"@captain-tool/captain-tool-win32-x64": "0.0.41",
|
|
15
|
+
"@captain-tool/captain-tool-darwin-x64": "0.0.41",
|
|
16
|
+
"@captain-tool/captain-tool-darwin-arm64": "0.0.41",
|
|
17
|
+
"@captain-tool/captain-tool-linux-x64": "0.0.41"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"bin/",
|