@peppermint-mcp/wizard 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +236 -97
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -69,7 +69,7 @@ async function detectClaudeDesktop() {
|
|
|
69
69
|
try {
|
|
70
70
|
const content = readFileSync(configPath, "utf-8");
|
|
71
71
|
const parsed = jsonc.parse(content);
|
|
72
|
-
alreadyInstalled = !!parsed?.mcpServers?.peppermint;
|
|
72
|
+
alreadyInstalled = !!parsed?.mcpServers?.["peppermint-memory"] || !!parsed?.mcpServers?.peppermint;
|
|
73
73
|
} catch {
|
|
74
74
|
warnings.push("Config file exists but could not be parsed");
|
|
75
75
|
}
|
|
@@ -77,7 +77,7 @@ async function detectClaudeDesktop() {
|
|
|
77
77
|
return {
|
|
78
78
|
id: "claude-desktop",
|
|
79
79
|
name: "Claude Desktop",
|
|
80
|
-
installMethod: "file-
|
|
80
|
+
installMethod: "file-native-http",
|
|
81
81
|
configPath,
|
|
82
82
|
alreadyInstalled,
|
|
83
83
|
needsRestart: true,
|
|
@@ -106,7 +106,7 @@ async function detectCursor() {
|
|
|
106
106
|
try {
|
|
107
107
|
const content = readFileSync2(configPath, "utf-8");
|
|
108
108
|
const parsed = jsonc2.parse(content);
|
|
109
|
-
alreadyInstalled = !!parsed?.mcpServers?.peppermint;
|
|
109
|
+
alreadyInstalled = !!parsed?.mcpServers?.["peppermint-memory"] || !!parsed?.mcpServers?.peppermint;
|
|
110
110
|
} catch {
|
|
111
111
|
warnings.push("Config file exists but could not be parsed");
|
|
112
112
|
}
|
|
@@ -197,6 +197,12 @@ function saveCredentials(creds) {
|
|
|
197
197
|
mode: 384
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
|
+
function clearCredentials() {
|
|
201
|
+
const path = getCredentialsPath();
|
|
202
|
+
if (existsSync3(path)) {
|
|
203
|
+
writeFileSync(path, "{}", { encoding: "utf-8", mode: 384 });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
200
206
|
|
|
201
207
|
// src/auth/localhost-oauth.ts
|
|
202
208
|
import { createServer } from "http";
|
|
@@ -382,17 +388,20 @@ async function installClaudeCode(serverUrl, apiKey, dryRun) {
|
|
|
382
388
|
};
|
|
383
389
|
}
|
|
384
390
|
try {
|
|
391
|
+
for (const name of ["peppermint-memory", "peppermint"]) {
|
|
392
|
+
try {
|
|
393
|
+
await exec3("claude", ["mcp", "remove", "--scope", "user", name], { timeout: 15e3 });
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
385
397
|
await exec3("claude", args, { timeout: 15e3 });
|
|
386
398
|
return {
|
|
387
399
|
success: true,
|
|
388
400
|
message: "Added peppermint-memory via claude mcp add",
|
|
389
|
-
needsRestart:
|
|
401
|
+
needsRestart: true
|
|
390
402
|
};
|
|
391
403
|
} catch (err) {
|
|
392
404
|
const message = err instanceof Error ? err.message : "claude mcp add failed";
|
|
393
|
-
if (message.includes("already exists")) {
|
|
394
|
-
return { success: true, message: "peppermint-memory already configured", needsRestart: false };
|
|
395
|
-
}
|
|
396
405
|
return { success: false, message, needsRestart: false };
|
|
397
406
|
}
|
|
398
407
|
}
|
|
@@ -458,6 +467,9 @@ Would create: ${filePath}`;
|
|
|
458
467
|
`Config merge produced invalid JSON: ${errors.map((e) => jsonc3.printParseErrorCode(e.error)).join(", ")}`
|
|
459
468
|
);
|
|
460
469
|
}
|
|
470
|
+
if (content === updated) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
461
473
|
if (dryRun) {
|
|
462
474
|
return `Would write to ${filePath}:
|
|
463
475
|
${updated}`;
|
|
@@ -507,26 +519,23 @@ function getConfigPath3() {
|
|
|
507
519
|
async function installClaudeDesktop(serverUrl, apiKey, dryRun) {
|
|
508
520
|
const configPath = getConfigPath3();
|
|
509
521
|
const serverConfig = {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
"mcp-remote@latest",
|
|
514
|
-
serverUrl,
|
|
515
|
-
"--header",
|
|
516
|
-
"Authorization:${PEPPERMINT_AUTH_HEADER}"
|
|
517
|
-
],
|
|
518
|
-
env: {
|
|
519
|
-
PEPPERMINT_AUTH_HEADER: `Bearer ${apiKey}`
|
|
522
|
+
url: serverUrl,
|
|
523
|
+
headers: {
|
|
524
|
+
Authorization: `Bearer ${apiKey}`
|
|
520
525
|
}
|
|
521
526
|
};
|
|
522
527
|
try {
|
|
528
|
+
removeServerFromConfig(configPath, "mcpServers", "peppermint", dryRun);
|
|
523
529
|
const result = writeServerToConfig({
|
|
524
530
|
filePath: configPath,
|
|
525
531
|
serverProperty: "mcpServers",
|
|
526
|
-
serverName: "peppermint",
|
|
532
|
+
serverName: "peppermint-memory",
|
|
527
533
|
serverConfig,
|
|
528
534
|
dryRun
|
|
529
535
|
});
|
|
536
|
+
if (result === null) {
|
|
537
|
+
return { success: true, message: "Already up to date", needsRestart: false };
|
|
538
|
+
}
|
|
530
539
|
return {
|
|
531
540
|
success: true,
|
|
532
541
|
message: dryRun ? result : `Wrote config to ${configPath}`,
|
|
@@ -542,7 +551,7 @@ async function removeClaudeDesktop(dryRun) {
|
|
|
542
551
|
const removed = removeServerFromConfig(
|
|
543
552
|
configPath,
|
|
544
553
|
"mcpServers",
|
|
545
|
-
"peppermint",
|
|
554
|
+
"peppermint-memory",
|
|
546
555
|
dryRun
|
|
547
556
|
);
|
|
548
557
|
return {
|
|
@@ -567,13 +576,17 @@ async function installCursor(serverUrl, apiKey, dryRun) {
|
|
|
567
576
|
}
|
|
568
577
|
};
|
|
569
578
|
try {
|
|
579
|
+
removeServerFromConfig(configPath, "mcpServers", "peppermint", dryRun);
|
|
570
580
|
const result = writeServerToConfig({
|
|
571
581
|
filePath: configPath,
|
|
572
582
|
serverProperty: "mcpServers",
|
|
573
|
-
serverName: "peppermint",
|
|
583
|
+
serverName: "peppermint-memory",
|
|
574
584
|
serverConfig,
|
|
575
585
|
dryRun
|
|
576
586
|
});
|
|
587
|
+
if (result === null) {
|
|
588
|
+
return { success: true, message: "Already up to date", needsRestart: false };
|
|
589
|
+
}
|
|
577
590
|
return {
|
|
578
591
|
success: true,
|
|
579
592
|
message: dryRun ? result : `Wrote config to ${configPath}`,
|
|
@@ -589,7 +602,7 @@ async function removeCursor(dryRun) {
|
|
|
589
602
|
const removed = removeServerFromConfig(
|
|
590
603
|
configPath,
|
|
591
604
|
"mcpServers",
|
|
592
|
-
"peppermint",
|
|
605
|
+
"peppermint-memory",
|
|
593
606
|
dryRun
|
|
594
607
|
);
|
|
595
608
|
return {
|
|
@@ -636,6 +649,10 @@ async function installCodex(serverUrl, apiKey, dryRun) {
|
|
|
636
649
|
if (apiKey) {
|
|
637
650
|
persistCodexEnvVar(apiKey);
|
|
638
651
|
}
|
|
652
|
+
try {
|
|
653
|
+
await exec4("codex", ["mcp", "remove", "peppermint-memory"], { timeout: 15e3 });
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
639
656
|
const env = { ...process.env };
|
|
640
657
|
if (apiKey) {
|
|
641
658
|
env.PEPPERMINT_TOKEN = apiKey;
|
|
@@ -711,12 +728,12 @@ function checkHostConfig(hostId, configPath) {
|
|
|
711
728
|
try {
|
|
712
729
|
const content = readFileSync6(configPath, "utf-8");
|
|
713
730
|
const parsed = jsonc4.parse(content);
|
|
714
|
-
const hasPeppermint = !!parsed?.mcpServers?.peppermint;
|
|
731
|
+
const hasPeppermint = !!parsed?.mcpServers?.["peppermint-memory"] || !!parsed?.mcpServers?.peppermint;
|
|
715
732
|
if (!hasPeppermint) {
|
|
716
733
|
return {
|
|
717
734
|
hostId,
|
|
718
735
|
status: "fail",
|
|
719
|
-
message: "peppermint entry not found in config"
|
|
736
|
+
message: "peppermint-memory entry not found in config"
|
|
720
737
|
};
|
|
721
738
|
}
|
|
722
739
|
return {
|
|
@@ -734,13 +751,17 @@ function checkHostConfig(hostId, configPath) {
|
|
|
734
751
|
}
|
|
735
752
|
|
|
736
753
|
// src/skills/index.ts
|
|
754
|
+
import { createHash as createHash2 } from "crypto";
|
|
737
755
|
import {
|
|
738
756
|
copyFileSync as copyFileSync2,
|
|
739
757
|
existsSync as existsSync7,
|
|
740
758
|
mkdirSync as mkdirSync4,
|
|
759
|
+
readFileSync as readFileSync7,
|
|
741
760
|
readdirSync,
|
|
761
|
+
renameSync as renameSync2,
|
|
742
762
|
rmSync,
|
|
743
|
-
statSync
|
|
763
|
+
statSync,
|
|
764
|
+
writeFileSync as writeFileSync4
|
|
744
765
|
} from "fs";
|
|
745
766
|
import { homedir as homedir7 } from "os";
|
|
746
767
|
import { dirname as dirname3, join as join7, resolve } from "path";
|
|
@@ -751,12 +772,26 @@ var LEGACY_SKILLS = [
|
|
|
751
772
|
"peppermint-capture",
|
|
752
773
|
"peppermint-ask-twin"
|
|
753
774
|
];
|
|
775
|
+
var HASH_FILE = ".wizard-hash";
|
|
754
776
|
function getSkillsBundlePath() {
|
|
755
777
|
return resolve(__dirname, "..", "skills-bundle", "peppermint");
|
|
756
778
|
}
|
|
757
779
|
function getTargetPath() {
|
|
758
780
|
return join7(homedir7(), ".claude", "skills", "peppermint");
|
|
759
781
|
}
|
|
782
|
+
function hashDir(dir) {
|
|
783
|
+
const hash = createHash2("sha256");
|
|
784
|
+
const entries = readdirSync(dir).sort();
|
|
785
|
+
for (const entry of entries) {
|
|
786
|
+
const fullPath = join7(dir, entry);
|
|
787
|
+
if (statSync(fullPath).isDirectory()) {
|
|
788
|
+
hash.update(hashDir(fullPath));
|
|
789
|
+
} else {
|
|
790
|
+
hash.update(readFileSync7(fullPath));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return hash.digest("hex");
|
|
794
|
+
}
|
|
760
795
|
function copyDirRecursive(src, dest) {
|
|
761
796
|
mkdirSync4(dest, { recursive: true });
|
|
762
797
|
for (const entry of readdirSync(src)) {
|
|
@@ -786,27 +821,41 @@ function removeLegacySkills(dryRun) {
|
|
|
786
821
|
function installSkills(dryRun) {
|
|
787
822
|
const bundlePath = getSkillsBundlePath();
|
|
788
823
|
const targetPath = getTargetPath();
|
|
824
|
+
const hashFile = join7(targetPath, HASH_FILE);
|
|
789
825
|
const alreadyExists = existsSync7(join7(targetPath, "SKILL.md"));
|
|
790
826
|
if (!existsSync7(bundlePath)) {
|
|
791
|
-
return { installed: false, updated: false, targetPath, error: "Skills bundle not found in package" };
|
|
827
|
+
return { installed: false, updated: false, skipped: false, backedUp: false, targetPath, error: "Skills bundle not found in package" };
|
|
828
|
+
}
|
|
829
|
+
const bundleHash = hashDir(bundlePath);
|
|
830
|
+
if (alreadyExists && existsSync7(hashFile)) {
|
|
831
|
+
const installedHash = readFileSync7(hashFile, "utf-8").trim();
|
|
832
|
+
if (installedHash === bundleHash) {
|
|
833
|
+
return { installed: true, updated: false, skipped: true, backedUp: false, targetPath };
|
|
834
|
+
}
|
|
792
835
|
}
|
|
793
836
|
if (dryRun) {
|
|
794
|
-
return { installed: true, updated: alreadyExists, targetPath };
|
|
837
|
+
return { installed: true, updated: alreadyExists, skipped: false, backedUp: false, targetPath };
|
|
838
|
+
}
|
|
839
|
+
let backedUp = false;
|
|
840
|
+
if (alreadyExists && !existsSync7(hashFile)) {
|
|
841
|
+
const backupPath = `${targetPath}.bak.${Date.now()}`;
|
|
842
|
+
renameSync2(targetPath, backupPath);
|
|
843
|
+
backedUp = true;
|
|
844
|
+
} else if (alreadyExists) {
|
|
845
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
795
846
|
}
|
|
796
847
|
try {
|
|
797
|
-
if (existsSync7(targetPath)) {
|
|
798
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
799
|
-
}
|
|
800
848
|
copyDirRecursive(bundlePath, targetPath);
|
|
801
|
-
|
|
849
|
+
writeFileSync4(join7(targetPath, HASH_FILE), bundleHash, "utf-8");
|
|
850
|
+
return { installed: true, updated: alreadyExists, skipped: false, backedUp, targetPath };
|
|
802
851
|
} catch (err) {
|
|
803
852
|
const message = err instanceof Error ? err.message : "Failed to install skills";
|
|
804
|
-
return { installed: false, updated: false, targetPath, error: message };
|
|
853
|
+
return { installed: false, updated: false, skipped: false, backedUp: false, targetPath, error: message };
|
|
805
854
|
}
|
|
806
855
|
}
|
|
807
856
|
|
|
808
857
|
// src/skills/permissions.ts
|
|
809
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as
|
|
858
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
810
859
|
import { homedir as homedir8 } from "os";
|
|
811
860
|
import { dirname as dirname4, join as join8 } from "path";
|
|
812
861
|
import * as jsonc5 from "jsonc-parser";
|
|
@@ -828,7 +877,7 @@ function installPermissions(dryRun) {
|
|
|
828
877
|
try {
|
|
829
878
|
let content = "";
|
|
830
879
|
if (existsSync8(settingsPath)) {
|
|
831
|
-
content =
|
|
880
|
+
content = readFileSync8(settingsPath, "utf-8");
|
|
832
881
|
}
|
|
833
882
|
const parsed = content ? jsonc5.parse(content) : {};
|
|
834
883
|
const existingAllow = parsed?.permissions?.allow || [];
|
|
@@ -848,7 +897,7 @@ function installPermissions(dryRun) {
|
|
|
848
897
|
if (!existsSync8(dir)) {
|
|
849
898
|
mkdirSync5(dir, { recursive: true });
|
|
850
899
|
}
|
|
851
|
-
|
|
900
|
+
writeFileSync5(settingsPath, updated, "utf-8");
|
|
852
901
|
return { added: toAdd };
|
|
853
902
|
} catch (err) {
|
|
854
903
|
const message = err instanceof Error ? err.message : "Failed to update permissions";
|
|
@@ -891,60 +940,92 @@ async function removeHost(host, dryRun) {
|
|
|
891
940
|
}
|
|
892
941
|
}
|
|
893
942
|
async function addCommand(options) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
943
|
+
const nonInteractive = options.yes || options.host?.length || options.authToken || !process.stdin.isTTY;
|
|
944
|
+
if (!process.stdin.isTTY && !nonInteractive) {
|
|
945
|
+
console.error("Error: peppermint-mcp-wizard requires an interactive terminal.");
|
|
946
|
+
console.error("For non-interactive installs, use: --host claude-code --yes");
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
if (!nonInteractive) {
|
|
950
|
+
p.intro(pc.green("\u{1F33F} Peppermint MCP Wizard"));
|
|
951
|
+
}
|
|
897
952
|
const hosts = await detectHosts();
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
)
|
|
953
|
+
if (!nonInteractive) {
|
|
954
|
+
const s = p.spinner();
|
|
955
|
+
s.start("Detecting AI hosts...");
|
|
956
|
+
s.stop("Detection complete");
|
|
957
|
+
if (hosts.length === 0) {
|
|
958
|
+
p.log.error(
|
|
959
|
+
"No supported AI hosts detected. Install Claude Code, Claude Desktop, Cursor, or Codex CLI and try again."
|
|
960
|
+
);
|
|
961
|
+
process.exit(6);
|
|
962
|
+
}
|
|
963
|
+
for (const host of hosts) {
|
|
964
|
+
const status = host.alreadyInstalled ? pc.yellow("already configured") : pc.dim("not configured");
|
|
965
|
+
const version = host.version ? pc.dim(` (${host.version})`) : "";
|
|
966
|
+
p.log.info(`${host.alreadyInstalled ? "\u26A0" : "\u2713"} ${host.name}${version} ${status}`);
|
|
967
|
+
}
|
|
968
|
+
} else if (hosts.length === 0) {
|
|
969
|
+
console.error("No supported AI hosts detected.");
|
|
903
970
|
process.exit(6);
|
|
904
971
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
972
|
+
let selectedHosts;
|
|
973
|
+
if (options.host?.length) {
|
|
974
|
+
const requestedIds = options.host;
|
|
975
|
+
selectedHosts = hosts.filter((h) => requestedIds.includes(h.id));
|
|
976
|
+
const missing = requestedIds.filter((id) => !hosts.find((h) => h.id === id));
|
|
977
|
+
if (missing.length > 0) {
|
|
978
|
+
const msg = `Host(s) not detected: ${missing.join(", ")}`;
|
|
979
|
+
nonInteractive ? console.error(msg) : p.log.error(msg);
|
|
980
|
+
process.exit(6);
|
|
981
|
+
}
|
|
982
|
+
} else if (nonInteractive) {
|
|
983
|
+
selectedHosts = hosts;
|
|
984
|
+
} else {
|
|
985
|
+
const unconfigured = hosts.filter((h) => !h.alreadyInstalled);
|
|
986
|
+
const toInstall = unconfigured.length > 0 ? unconfigured : hosts;
|
|
987
|
+
const selected = await p.multiselect({
|
|
988
|
+
message: "Install Peppermint MCP into which hosts?",
|
|
989
|
+
options: toInstall.map((h) => ({
|
|
990
|
+
value: h.id,
|
|
991
|
+
label: h.name,
|
|
992
|
+
hint: h.alreadyInstalled ? "will reinstall" : void 0
|
|
993
|
+
})),
|
|
994
|
+
initialValues: toInstall.map((h) => h.id)
|
|
995
|
+
});
|
|
996
|
+
if (p.isCancel(selected)) {
|
|
997
|
+
p.cancel("Cancelled.");
|
|
998
|
+
process.exit(0);
|
|
999
|
+
}
|
|
1000
|
+
selectedHosts = hosts.filter(
|
|
1001
|
+
(h) => selected.includes(h.id)
|
|
1002
|
+
);
|
|
924
1003
|
}
|
|
925
|
-
const selectedHosts = hosts.filter(
|
|
926
|
-
(h) => selected.includes(h.id)
|
|
927
|
-
);
|
|
928
|
-
s.start(`Checking server at ${options.server}...`);
|
|
929
1004
|
const serverCheck = await checkServerReachable(options.server);
|
|
930
1005
|
if (!serverCheck.reachable) {
|
|
931
|
-
|
|
932
|
-
p.log.error(
|
|
933
|
-
`Cannot reach ${options.server}: ${serverCheck.error}
|
|
934
|
-
Check your internet connection and try again.`
|
|
935
|
-
);
|
|
1006
|
+
const msg = `Cannot reach ${options.server}: ${serverCheck.error}`;
|
|
1007
|
+
nonInteractive ? console.error(msg) : p.log.error(msg);
|
|
936
1008
|
process.exit(4);
|
|
937
1009
|
}
|
|
938
|
-
|
|
939
|
-
`Server reachable ${pc.dim(`(${serverCheck.latencyMs}ms)`)}`
|
|
940
|
-
|
|
1010
|
+
if (!nonInteractive) {
|
|
1011
|
+
p.log.success(`Server reachable ${pc.dim(`(${serverCheck.latencyMs}ms)`)}`);
|
|
1012
|
+
}
|
|
941
1013
|
let apiKey;
|
|
942
|
-
|
|
1014
|
+
const tokenFromFlag = options.authToken || process.env.PEPPERMINT_AUTH_TOKEN;
|
|
1015
|
+
if (tokenFromFlag) {
|
|
1016
|
+
apiKey = tokenFromFlag;
|
|
1017
|
+
const msg = "Using provided auth token";
|
|
1018
|
+
nonInteractive ? console.log(msg) : p.log.success(msg);
|
|
1019
|
+
} else if (needsAuth(selectedHosts)) {
|
|
943
1020
|
const base = serverBase(options.server);
|
|
944
1021
|
const existing = loadCredentials(base);
|
|
945
1022
|
if (existing) {
|
|
946
1023
|
apiKey = existing.api_key;
|
|
947
|
-
|
|
1024
|
+
const msg = `Authenticated as ${existing.email || "user"} (cached)`;
|
|
1025
|
+
nonInteractive ? console.log(msg) : p.log.success(pc.bold(msg));
|
|
1026
|
+
} else if (nonInteractive) {
|
|
1027
|
+
console.error("Error: non-interactive mode requires --auth-token or PEPPERMINT_AUTH_TOKEN, or cached credentials.");
|
|
1028
|
+
process.exit(3);
|
|
948
1029
|
} else {
|
|
949
1030
|
p.log.info("Opening browser for authentication...");
|
|
950
1031
|
try {
|
|
@@ -975,9 +1056,12 @@ Check your internet connection and try again.`
|
|
|
975
1056
|
p.log.info(` ${pc.green("\u2713")} Removed legacy skills: ${pc.dim(removed.join(", "))}`);
|
|
976
1057
|
}
|
|
977
1058
|
const skillResult = installSkills(options.dryRun);
|
|
978
|
-
if (skillResult.
|
|
1059
|
+
if (skillResult.skipped) {
|
|
1060
|
+
p.log.info(` ${pc.green("\u2713")} Peppermint skill ${pc.dim("up to date")}`);
|
|
1061
|
+
} else if (skillResult.installed) {
|
|
979
1062
|
const verb = skillResult.updated ? "Updated" : "Installed";
|
|
980
|
-
|
|
1063
|
+
const backup = skillResult.backedUp ? pc.dim(" (previous version backed up)") : "";
|
|
1064
|
+
p.log.info(` ${pc.green("\u2713")} ${verb} Peppermint skill${backup} ${pc.dim(skillResult.targetPath)}`);
|
|
981
1065
|
} else if (skillResult.error) {
|
|
982
1066
|
p.log.info(` ${pc.red("\u2717")} Skill install failed: ${pc.dim(skillResult.error)}`);
|
|
983
1067
|
}
|
|
@@ -1039,38 +1123,93 @@ async function doctorCommand(options) {
|
|
|
1039
1123
|
p.outro("Health check complete");
|
|
1040
1124
|
}
|
|
1041
1125
|
async function removeCommand(options) {
|
|
1042
|
-
|
|
1126
|
+
const nonInteractive = options.yes || !process.stdin.isTTY;
|
|
1127
|
+
if (!process.stdin.isTTY && !nonInteractive) {
|
|
1128
|
+
console.error("Error: peppermint-mcp-wizard requires an interactive terminal.");
|
|
1129
|
+
console.error("For non-interactive removes, use: --yes");
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
if (!nonInteractive) {
|
|
1133
|
+
p.intro(pc.green("\u{1F33F} Peppermint MCP Wizard \u2014 Remove"));
|
|
1134
|
+
}
|
|
1043
1135
|
const hosts = await detectHosts();
|
|
1044
1136
|
const installed = hosts.filter((h) => h.alreadyInstalled);
|
|
1045
1137
|
if (installed.length === 0) {
|
|
1046
|
-
|
|
1138
|
+
const msg = "Peppermint is not installed in any detected hosts.";
|
|
1139
|
+
nonInteractive ? console.log(msg) : p.log.info(msg);
|
|
1047
1140
|
process.exit(0);
|
|
1048
1141
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1142
|
+
let selectedHosts;
|
|
1143
|
+
if (nonInteractive) {
|
|
1144
|
+
selectedHosts = installed;
|
|
1145
|
+
} else {
|
|
1146
|
+
const selected = await p.multiselect({
|
|
1147
|
+
message: "Remove Peppermint MCP from which hosts?",
|
|
1148
|
+
options: installed.map((h) => ({
|
|
1149
|
+
value: h.id,
|
|
1150
|
+
label: h.name
|
|
1151
|
+
})),
|
|
1152
|
+
initialValues: installed.map((h) => h.id)
|
|
1153
|
+
});
|
|
1154
|
+
if (p.isCancel(selected)) {
|
|
1155
|
+
p.cancel("Cancelled.");
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
selectedHosts = hosts.filter(
|
|
1159
|
+
(h) => selected.includes(h.id)
|
|
1160
|
+
);
|
|
1060
1161
|
}
|
|
1061
|
-
const selectedHosts = hosts.filter(
|
|
1062
|
-
(h) => selected.includes(h.id)
|
|
1063
|
-
);
|
|
1064
1162
|
for (const host of selectedHosts) {
|
|
1065
1163
|
const result = await removeHost(host, options.dryRun);
|
|
1066
1164
|
const icon = result.success ? pc.green("\u2713") : pc.red("\u2717");
|
|
1067
|
-
p.log.info(`${icon} ${host.name} ${pc.dim(result.message)}`);
|
|
1165
|
+
nonInteractive ? console.log(`${result.success ? "\u2713" : "\u2717"} ${host.name} ${result.message}`) : p.log.info(`${icon} ${host.name} ${pc.dim(result.message)}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (options.revokeToken) {
|
|
1168
|
+
const base = serverBase(options.server);
|
|
1169
|
+
const creds = loadCredentials(base);
|
|
1170
|
+
if (creds && !options.dryRun) {
|
|
1171
|
+
try {
|
|
1172
|
+
const res = await fetch(`${base}/auth/api-keys`, {
|
|
1173
|
+
headers: { Authorization: `Bearer ${creds.api_key}` }
|
|
1174
|
+
});
|
|
1175
|
+
if (res.ok) {
|
|
1176
|
+
const keys = await res.json();
|
|
1177
|
+
for (const key of keys) {
|
|
1178
|
+
if (key.name === "mcp-wizard") {
|
|
1179
|
+
await fetch(`${base}/auth/api-keys/${key.id}`, {
|
|
1180
|
+
method: "DELETE",
|
|
1181
|
+
headers: { Authorization: `Bearer ${creds.api_key}` }
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
clearCredentials();
|
|
1187
|
+
const msg = "Revoked API key and cleared credentials";
|
|
1188
|
+
nonInteractive ? console.log(`\u2713 ${msg}`) : p.log.info(`${pc.green("\u2713")} ${msg}`);
|
|
1189
|
+
} catch {
|
|
1190
|
+
const msg = "Failed to revoke API key (credentials cleared locally)";
|
|
1191
|
+
clearCredentials();
|
|
1192
|
+
nonInteractive ? console.log(`\u26A0 ${msg}`) : p.log.info(`${pc.yellow("\u26A0")} ${msg}`);
|
|
1193
|
+
}
|
|
1194
|
+
} else if (creds && options.dryRun) {
|
|
1195
|
+
const msg = "Would revoke API key and clear credentials";
|
|
1196
|
+
nonInteractive ? console.log(msg) : p.log.info(msg);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (!nonInteractive) {
|
|
1200
|
+
p.outro("Removal complete");
|
|
1068
1201
|
}
|
|
1069
|
-
p.outro("Removal complete");
|
|
1070
1202
|
}
|
|
1071
1203
|
var program = new Command().name("peppermint-mcp-wizard").description("One-command installer for Peppermint MCP").version("0.1.0");
|
|
1072
|
-
program.command("add", { isDefault: true }).description("Detect hosts, authenticate, install MCP config").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).option("--no-verify", "Skip post-install verification").
|
|
1204
|
+
program.command("add", { isDefault: true }).description("Detect hosts, authenticate, install MCP config").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).option("--no-verify", "Skip post-install verification").option("--host <id...>", "Install to specific hosts (claude-code, claude-desktop, cursor, codex)").option("--yes", "Skip all prompts (non-interactive)", false).option("--auth-token <token>", "API key or token for auth (skips browser OAuth)").action((opts) => addCommand({
|
|
1205
|
+
server: opts.server,
|
|
1206
|
+
dryRun: opts.dryRun,
|
|
1207
|
+
verify: opts.verify,
|
|
1208
|
+
host: opts.host,
|
|
1209
|
+
yes: opts.yes,
|
|
1210
|
+
authToken: opts.authToken
|
|
1211
|
+
}));
|
|
1073
1212
|
program.command("list").description("List detected AI hosts and their Peppermint status").option("--server <url>", "MCP server URL", DEFAULT_SERVER).action((opts) => listCommand({ server: opts.server }));
|
|
1074
1213
|
program.command("doctor").description("Run health checks on existing installation").option("--server <url>", "MCP server URL", DEFAULT_SERVER).action((opts) => doctorCommand({ server: opts.server }));
|
|
1075
|
-
program.command("remove").description("Remove Peppermint MCP from selected hosts").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).action((opts) => removeCommand({ server: opts.server, dryRun: opts.dryRun }));
|
|
1214
|
+
program.command("remove").description("Remove Peppermint MCP from selected hosts").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).option("--yes", "Skip all prompts (non-interactive)", false).option("--revoke-token", "Revoke API key on the server", true).option("--no-revoke-token", "Keep API key for future re-installs").action((opts) => removeCommand({ server: opts.server, dryRun: opts.dryRun, yes: opts.yes, revokeToken: opts.revokeToken }));
|
|
1076
1215
|
program.parse();
|