@gramatr/mcp 0.13.128 → 0.13.130
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 +14 -2
- package/dist/bin/hook-dispatcher.d.ts.map +1 -1
- package/dist/bin/hook-dispatcher.js +0 -5
- package/dist/bin/hook-dispatcher.js.map +1 -1
- package/dist/bin/install.d.ts +89 -0
- package/dist/bin/install.d.ts.map +1 -1
- package/dist/bin/install.js +371 -15
- package/dist/bin/install.js.map +1 -1
- package/dist/bin/setup-legacy.d.ts +1 -0
- package/dist/bin/setup-legacy.d.ts.map +1 -1
- package/dist/bin/setup-legacy.js +114 -0
- package/dist/bin/setup-legacy.js.map +1 -1
- package/dist/hooks/generated/hook-registry.d.ts +2 -2
- package/dist/hooks/generated/hook-registry.d.ts.map +1 -1
- package/dist/hooks/generated/hook-registry.js +1 -4
- package/dist/hooks/generated/hook-registry.js.map +1 -1
- package/dist/hooks/generated/hook-timeouts.d.ts +2 -6
- package/dist/hooks/generated/hook-timeouts.d.ts.map +1 -1
- package/dist/hooks/generated/hook-timeouts.js +2 -6
- package/dist/hooks/generated/hook-timeouts.js.map +1 -1
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/lib/auto-feedback.d.ts +7 -0
- package/dist/hooks/lib/auto-feedback.d.ts.map +1 -0
- package/dist/hooks/lib/auto-feedback.js +37 -0
- package/dist/hooks/lib/auto-feedback.js.map +1 -0
- package/dist/hooks/stop.d.ts.map +1 -1
- package/dist/hooks/stop.js +11 -0
- package/dist/hooks/stop.js.map +1 -1
- package/dist/setup/generated/platform-hooks.d.ts +1 -1
- package/dist/setup/generated/platform-hooks.d.ts.map +1 -1
- package/dist/setup/generated/platform-hooks.js +1 -4
- package/dist/setup/generated/platform-hooks.js.map +1 -1
- package/package.json +1 -1
- package/dist/hooks/rating-capture.d.ts +0 -2
- package/dist/hooks/rating-capture.d.ts.map +0 -1
- package/dist/hooks/rating-capture.js +0 -113
- package/dist/hooks/rating-capture.js.map +0 -1
package/dist/bin/install.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* Scope: Claude Code only in v1. Codex / Cursor / Gemini stay on `setup`
|
|
16
16
|
* subcommands until follow-up.
|
|
17
17
|
*/
|
|
18
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync,
|
|
18
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { platform as osPlatform } from "node:os";
|
|
19
20
|
import { dirname, join, resolve } from "node:path";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { buildConnectorInstructions, buildPromptSuggestion } from "../setup/web-connector.js";
|
|
21
23
|
/** Sentinel pair carved into ~/.claude/CLAUDE.md. */
|
|
22
24
|
export const GRAMATR_MD_START = "<!-- GRAMATR-START -->";
|
|
23
25
|
export const GRAMATR_MD_END = "<!-- GRAMATR-END -->";
|
|
@@ -61,6 +63,27 @@ export function gramatrStopScriptPath(home) {
|
|
|
61
63
|
export function gramatrTokenPath(home) {
|
|
62
64
|
return join(home, ".gramatr.json");
|
|
63
65
|
}
|
|
66
|
+
/** Path to Claude Code slash-command directory. */
|
|
67
|
+
export function claudeCommandsDir(home) {
|
|
68
|
+
return join(home, ".claude", "commands");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the claude_desktop_config.json path for the current platform.
|
|
72
|
+
* macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
73
|
+
* Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
74
|
+
* Linux: ~/.config/Claude/claude_desktop_config.json
|
|
75
|
+
*/
|
|
76
|
+
export function claudeDesktopConfigPath(home, platform = osPlatform()) {
|
|
77
|
+
if (platform === "darwin") {
|
|
78
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
79
|
+
}
|
|
80
|
+
if (platform === "win32") {
|
|
81
|
+
// gramatr-allow: C1 — Windows fallback for APPDATA when not present
|
|
82
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
83
|
+
return join(appData, "Claude", "claude_desktop_config.json");
|
|
84
|
+
}
|
|
85
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
86
|
+
}
|
|
64
87
|
/** Resolve the bundled SessionEnd hook script source inside @gramatr/mcp. */
|
|
65
88
|
export function resolveBundledSessionEndHookSource() {
|
|
66
89
|
const here = fileURLToPath(import.meta.url);
|
|
@@ -224,9 +247,7 @@ export function mergeUserPromptSubmitHookIntoSettings(settings, home, removeLega
|
|
|
224
247
|
}
|
|
225
248
|
}
|
|
226
249
|
// Add our entry. Preserve any non-gramatr UserPromptSubmit entries.
|
|
227
|
-
const ups = Array.isArray(hooks.UserPromptSubmit)
|
|
228
|
-
? [...hooks.UserPromptSubmit]
|
|
229
|
-
: [];
|
|
250
|
+
const ups = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
|
|
230
251
|
const alreadyPresent = ups.some((e) => (e.hooks ?? []).some((c) => c.command === cmd));
|
|
231
252
|
if (!alreadyPresent) {
|
|
232
253
|
ups.push({
|
|
@@ -237,9 +258,7 @@ export function mergeUserPromptSubmitHookIntoSettings(settings, home, removeLega
|
|
|
237
258
|
hooks.UserPromptSubmit = ups;
|
|
238
259
|
// Add SessionEnd entry. Preserve any non-gramatr SessionEnd entries.
|
|
239
260
|
const seCmd = buildSessionEndHookCommand(home);
|
|
240
|
-
const seEntries = Array.isArray(hooks.SessionEnd)
|
|
241
|
-
? [...hooks.SessionEnd]
|
|
242
|
-
: [];
|
|
261
|
+
const seEntries = Array.isArray(hooks.SessionEnd) ? [...hooks.SessionEnd] : [];
|
|
243
262
|
const seAlreadyPresent = seEntries.some((e) => (e.hooks ?? []).some((c) => c.command === seCmd));
|
|
244
263
|
if (!seAlreadyPresent) {
|
|
245
264
|
seEntries.push({
|
|
@@ -250,9 +269,7 @@ export function mergeUserPromptSubmitHookIntoSettings(settings, home, removeLega
|
|
|
250
269
|
hooks.SessionEnd = seEntries;
|
|
251
270
|
// Add SessionStart entry. Preserve any non-gramatr SessionStart entries (#2475).
|
|
252
271
|
const ssCmd = buildSessionStartHookCommand(home);
|
|
253
|
-
const ssEntries = Array.isArray(hooks.SessionStart)
|
|
254
|
-
? [...hooks.SessionStart]
|
|
255
|
-
: [];
|
|
272
|
+
const ssEntries = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
|
|
256
273
|
const ssAlreadyPresent = ssEntries.some((e) => (e.hooks ?? []).some((c) => c.command === ssCmd));
|
|
257
274
|
if (!ssAlreadyPresent) {
|
|
258
275
|
ssEntries.push({
|
|
@@ -263,9 +280,7 @@ export function mergeUserPromptSubmitHookIntoSettings(settings, home, removeLega
|
|
|
263
280
|
hooks.SessionStart = ssEntries;
|
|
264
281
|
// Add Stop entry. Preserve any non-gramatr Stop entries (#2476).
|
|
265
282
|
const stopCmd = buildStopHookCommand(home);
|
|
266
|
-
const stopEntries = Array.isArray(hooks.Stop)
|
|
267
|
-
? [...hooks.Stop]
|
|
268
|
-
: [];
|
|
283
|
+
const stopEntries = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
|
|
269
284
|
const stopAlreadyPresent = stopEntries.some((e) => (e.hooks ?? []).some((c) => c.command === stopCmd));
|
|
270
285
|
if (!stopAlreadyPresent) {
|
|
271
286
|
stopEntries.push({
|
|
@@ -353,11 +368,16 @@ export function upsertGramatrSection(existing, sectionBody) {
|
|
|
353
368
|
export function stripGramatrSection(existing) {
|
|
354
369
|
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
355
370
|
const re = new RegExp(`\\n*${escape(GRAMATR_MD_START)}[\\s\\S]*?${escape(GRAMATR_MD_END)}\\n*`, "m");
|
|
356
|
-
return existing
|
|
371
|
+
return (existing
|
|
372
|
+
.replace(re, "\n")
|
|
373
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
374
|
+
.trimEnd() + "\n");
|
|
357
375
|
}
|
|
358
376
|
function listMatching(dir, predicate) {
|
|
359
377
|
try {
|
|
360
|
-
return readdirSync(dir)
|
|
378
|
+
return readdirSync(dir)
|
|
379
|
+
.filter(predicate)
|
|
380
|
+
.map((n) => join(dir, n));
|
|
361
381
|
}
|
|
362
382
|
catch {
|
|
363
383
|
return [];
|
|
@@ -401,6 +421,116 @@ function removeLegacyArtifacts(paths, dryRun) {
|
|
|
401
421
|
}
|
|
402
422
|
return { removed };
|
|
403
423
|
}
|
|
424
|
+
// ── legacy slash-command cleanup (#2490) ──────────────────────────────────
|
|
425
|
+
/** Filenames in ~/.claude/commands/ known to be gramatr-flavored legacy. */
|
|
426
|
+
const LEGACY_SLASH_COMMAND_FILENAMES = new Set([
|
|
427
|
+
"save-handoff.md",
|
|
428
|
+
"gramatr-restore.md",
|
|
429
|
+
"gramatr-compact.md",
|
|
430
|
+
]);
|
|
431
|
+
/**
|
|
432
|
+
* Heuristic: file content looks gramatr-flavored if it references retired
|
|
433
|
+
* daemon paths or the gramatr namespace.
|
|
434
|
+
*/
|
|
435
|
+
function isGramatrFlavoredSlashCommand(content) {
|
|
436
|
+
return (content.includes("~/.gramatr/.state/") ||
|
|
437
|
+
content.includes(".gramatr/.state/") ||
|
|
438
|
+
content.includes("~/.gramatr/bin/gramatr-hook") ||
|
|
439
|
+
content.includes(".gramatr/bin/gramatr-hook") ||
|
|
440
|
+
/\bgrāmatr\b/i.test(content) ||
|
|
441
|
+
/\bgramatr\b/i.test(content));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Scan ~/.claude/commands/*.md for legacy gramatr-flavored slash commands.
|
|
445
|
+
* Returns absolute paths of files that match.
|
|
446
|
+
*
|
|
447
|
+
* Match rules:
|
|
448
|
+
* - Filename in LEGACY_SLASH_COMMAND_FILENAMES AND content gramatr-flavored.
|
|
449
|
+
* - OR content references retired daemon paths (~/.gramatr/.state/, etc.).
|
|
450
|
+
*
|
|
451
|
+
* Non-gramatr slash commands are preserved.
|
|
452
|
+
*/
|
|
453
|
+
export function detectLegacySlashCommands(home) {
|
|
454
|
+
const dir = claudeCommandsDir(home);
|
|
455
|
+
let files;
|
|
456
|
+
try {
|
|
457
|
+
files = readdirSync(dir).filter((n) => n.endsWith(".md"));
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
const out = [];
|
|
463
|
+
for (const name of files) {
|
|
464
|
+
const path = join(dir, name);
|
|
465
|
+
let content = "";
|
|
466
|
+
try {
|
|
467
|
+
content = readFileSync(path, "utf8");
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const knownStale = LEGACY_SLASH_COMMAND_FILENAMES.has(name);
|
|
473
|
+
const refsRetiredPath = content.includes("~/.gramatr/.state/") ||
|
|
474
|
+
content.includes(".gramatr/.state/") ||
|
|
475
|
+
content.includes("~/.gramatr/bin/gramatr-hook") ||
|
|
476
|
+
content.includes(".gramatr/bin/gramatr-hook");
|
|
477
|
+
if (refsRetiredPath) {
|
|
478
|
+
out.push(path);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (knownStale && isGramatrFlavoredSlashCommand(content)) {
|
|
482
|
+
out.push(path);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
/** Remove the given slash-command files. Best-effort, returns removed paths. */
|
|
488
|
+
export function cleanupLegacySlashCommands(home, dryRun = false) {
|
|
489
|
+
const matches = detectLegacySlashCommands(home);
|
|
490
|
+
if (dryRun)
|
|
491
|
+
return matches;
|
|
492
|
+
const removed = [];
|
|
493
|
+
for (const p of matches) {
|
|
494
|
+
try {
|
|
495
|
+
unlinkSync(p);
|
|
496
|
+
removed.push(p);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
/* best-effort */
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return removed;
|
|
503
|
+
}
|
|
504
|
+
// ── client detection (#2472) ──────────────────────────────────────────────
|
|
505
|
+
/**
|
|
506
|
+
* Detect which client this machine looks like:
|
|
507
|
+
* - claude-code if ~/.claude/settings.json exists OR ~/.claude.json has mcpServers
|
|
508
|
+
* - else claude-desktop if claude_desktop_config.json exists
|
|
509
|
+
* - else claude-web
|
|
510
|
+
*/
|
|
511
|
+
export function detectClient(home, platform = osPlatform()) {
|
|
512
|
+
if (existsSync(claudeSettingsPath(home)))
|
|
513
|
+
return "claude-code";
|
|
514
|
+
const claudeJsonPath = join(home, ".claude.json");
|
|
515
|
+
if (existsSync(claudeJsonPath)) {
|
|
516
|
+
try {
|
|
517
|
+
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
518
|
+
const parsed = JSON.parse(raw);
|
|
519
|
+
if (parsed && typeof parsed === "object" && parsed.mcpServers) {
|
|
520
|
+
return "claude-code";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
/* fall through */
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// ~/.claude/ exists (even empty) → assume claude-code is being set up.
|
|
528
|
+
if (existsSync(join(home, ".claude")))
|
|
529
|
+
return "claude-code";
|
|
530
|
+
if (existsSync(claudeDesktopConfigPath(home, platform)))
|
|
531
|
+
return "claude-desktop";
|
|
532
|
+
return "claude-web";
|
|
533
|
+
}
|
|
404
534
|
// ── auth helpers ──────────────────────────────────────────────────────────
|
|
405
535
|
export function hasValidToken(home) {
|
|
406
536
|
const path = gramatrTokenPath(home);
|
|
@@ -418,17 +548,46 @@ export async function install(opts) {
|
|
|
418
548
|
const log = opts.log ?? ((m) => process.stderr.write(`${m}\n`));
|
|
419
549
|
const dryRun = !!opts.dryRun;
|
|
420
550
|
const cleanLegacy = !!opts.cleanLegacy || !!opts.nonInteractive;
|
|
551
|
+
const platform = opts.platformOverride ?? osPlatform();
|
|
552
|
+
const client = opts.client ?? detectClient(opts.home, platform);
|
|
421
553
|
const summary = {
|
|
554
|
+
client,
|
|
422
555
|
hookScriptWritten: false,
|
|
423
556
|
sessionEndScriptWritten: false,
|
|
424
557
|
sessionStartScriptWritten: false,
|
|
425
558
|
stopScriptWritten: false,
|
|
426
559
|
settingsUpdated: false,
|
|
427
560
|
claudeMdUpdated: false,
|
|
561
|
+
desktopConfigUpdated: false,
|
|
428
562
|
legacyEntriesRemoved: 0,
|
|
429
563
|
legacyFilesRemoved: [],
|
|
564
|
+
legacySlashCommandsRemoved: [],
|
|
430
565
|
backups: [],
|
|
431
566
|
};
|
|
567
|
+
// Dispatch to non-claude-code branches early.
|
|
568
|
+
if (client === "claude-web") {
|
|
569
|
+
summary.webInstructions = buildWebInstallInstructions(opts.mcpServerUrl);
|
|
570
|
+
log("[gramatr] claude-web detected — no local filesystem changes.");
|
|
571
|
+
log(summary.webInstructions);
|
|
572
|
+
return summary;
|
|
573
|
+
}
|
|
574
|
+
if (client === "claude-desktop") {
|
|
575
|
+
const cfgPath = opts.desktopConfigPathOverride ?? claudeDesktopConfigPath(opts.home, platform);
|
|
576
|
+
const updated = installClaudeDesktop({
|
|
577
|
+
configPath: cfgPath,
|
|
578
|
+
home: opts.home,
|
|
579
|
+
mcpServerUrl: opts.mcpServerUrl,
|
|
580
|
+
dryRun,
|
|
581
|
+
log,
|
|
582
|
+
});
|
|
583
|
+
summary.desktopConfigUpdated = updated.updated;
|
|
584
|
+
if (updated.backup)
|
|
585
|
+
summary.backups.push(updated.backup);
|
|
586
|
+
log(`[gramatr] claude-desktop ${dryRun ? "(dry-run) " : ""}config at ${cfgPath}`);
|
|
587
|
+
log("[gramatr] restart Claude Desktop after install to load the new MCP server.");
|
|
588
|
+
return summary;
|
|
589
|
+
}
|
|
590
|
+
// ─── claude-code path (legacy default — full hook + settings + CLAUDE.md) ──
|
|
432
591
|
// 1. Hook script
|
|
433
592
|
const hookSrc = opts.hookSourcePath ?? resolveBundledHookSource();
|
|
434
593
|
const hookDst = gramatrHookScriptPath(opts.home);
|
|
@@ -571,28 +730,174 @@ export async function install(opts) {
|
|
|
571
730
|
for (const p of res.removed)
|
|
572
731
|
log(`[gramatr] cleaned ${dryRun ? "(dry-run) " : ""}${p}`);
|
|
573
732
|
}
|
|
733
|
+
// Also clean up legacy slash commands (#2490).
|
|
734
|
+
const removedCmds = cleanupLegacySlashCommands(opts.home, dryRun);
|
|
735
|
+
summary.legacySlashCommandsRemoved = removedCmds;
|
|
736
|
+
for (const p of removedCmds) {
|
|
737
|
+
log(`[gramatr] cleaned legacy slash command ${dryRun ? "(dry-run) " : ""}${p}`);
|
|
738
|
+
}
|
|
574
739
|
}
|
|
575
740
|
else {
|
|
576
741
|
const legacy = detectLegacyArtifacts(opts.home);
|
|
577
742
|
if (legacy.length > 0) {
|
|
578
743
|
log(`[gramatr] note: detected ${legacy.length} legacy artifact${legacy.length === 1 ? "" : "s"}; rerun with --clean-legacy to remove`);
|
|
579
744
|
}
|
|
745
|
+
const staleCmds = detectLegacySlashCommands(opts.home);
|
|
746
|
+
if (staleCmds.length > 0) {
|
|
747
|
+
log(`[gramatr] note: detected ${staleCmds.length} legacy slash command${staleCmds.length === 1 ? "" : "s"}; rerun with --clean-legacy to remove`);
|
|
748
|
+
}
|
|
580
749
|
}
|
|
581
750
|
return summary;
|
|
582
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Build the claude-desktop mcpServers entry for gramatr. Uses HTTP transport
|
|
754
|
+
* since Desktop supports it and we get a free Bearer token from ~/.gramatr.json.
|
|
755
|
+
*/
|
|
756
|
+
export function buildDesktopMcpEntry(home, serverUrl = "https://api.gramatr.com/mcp") {
|
|
757
|
+
const token = readBearerToken(home);
|
|
758
|
+
const headers = {};
|
|
759
|
+
if (token)
|
|
760
|
+
headers.Authorization = `Bearer ${token}`;
|
|
761
|
+
return {
|
|
762
|
+
type: "http",
|
|
763
|
+
url: serverUrl,
|
|
764
|
+
headers,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function readBearerToken(home) {
|
|
768
|
+
try {
|
|
769
|
+
const raw = readFileSync(gramatrTokenPath(home), "utf8");
|
|
770
|
+
const parsed = JSON.parse(raw);
|
|
771
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Merge gramatr entry into a claude-desktop config object. Returns the next
|
|
779
|
+
* object and whether a change was made. Idempotent.
|
|
780
|
+
*/
|
|
781
|
+
export function mergeDesktopConfig(current, entry) {
|
|
782
|
+
const servers = { ...(current.mcpServers ?? {}) };
|
|
783
|
+
const existing = servers.gramatr;
|
|
784
|
+
const same = existing && JSON.stringify(existing) === JSON.stringify(entry);
|
|
785
|
+
if (same)
|
|
786
|
+
return { next: current, changed: false };
|
|
787
|
+
servers.gramatr = entry;
|
|
788
|
+
return { next: { ...current, mcpServers: servers }, changed: true };
|
|
789
|
+
}
|
|
790
|
+
function installClaudeDesktop(opts) {
|
|
791
|
+
const entry = buildDesktopMcpEntry(opts.home, opts.mcpServerUrl);
|
|
792
|
+
const raw = readFileOr(opts.configPath, null);
|
|
793
|
+
const current = parseJsonOr(raw, {});
|
|
794
|
+
const { next, changed } = mergeDesktopConfig(current, entry);
|
|
795
|
+
if (!changed)
|
|
796
|
+
return { updated: false, backup: null };
|
|
797
|
+
const desired = JSON.stringify(next, null, 2) + "\n";
|
|
798
|
+
if (opts.dryRun)
|
|
799
|
+
return { updated: true, backup: null };
|
|
800
|
+
const backup = backupFile(opts.configPath);
|
|
801
|
+
atomicWriteFile(opts.configPath, desired, 0o600);
|
|
802
|
+
return { updated: true, backup };
|
|
803
|
+
}
|
|
804
|
+
/** Remove gramatr entry from claude-desktop config. */
|
|
805
|
+
export function uninstallDesktopConfig(current) {
|
|
806
|
+
if (!current.mcpServers || !current.mcpServers.gramatr) {
|
|
807
|
+
return { next: current, changed: false };
|
|
808
|
+
}
|
|
809
|
+
const servers = { ...current.mcpServers };
|
|
810
|
+
delete servers.gramatr;
|
|
811
|
+
const next = { ...current };
|
|
812
|
+
if (Object.keys(servers).length > 0) {
|
|
813
|
+
next.mcpServers = servers;
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
delete next.mcpServers;
|
|
817
|
+
}
|
|
818
|
+
return { next, changed: true };
|
|
819
|
+
}
|
|
820
|
+
// ── claude-web instructions (#2472) ───────────────────────────────────────
|
|
821
|
+
/**
|
|
822
|
+
* Build the copy-paste instructions for claude-web. No filesystem writes.
|
|
823
|
+
* Combines connector steps with the canonical prompt suggestion block.
|
|
824
|
+
*/
|
|
825
|
+
export function buildWebInstallInstructions(serverUrl) {
|
|
826
|
+
const conn = buildConnectorInstructions({ serverUrl, target: "claude-web" });
|
|
827
|
+
const suggestion = buildPromptSuggestion("claude-web");
|
|
828
|
+
const lines = [];
|
|
829
|
+
lines.push("# gramatr — claude-web manual setup");
|
|
830
|
+
lines.push("");
|
|
831
|
+
lines.push("Claude Web (claude.ai) has no local install. Follow these steps:");
|
|
832
|
+
lines.push("");
|
|
833
|
+
for (let i = 0; i < conn.steps.length; i++) {
|
|
834
|
+
lines.push(` ${i + 1}. ${conn.steps[i]}`);
|
|
835
|
+
}
|
|
836
|
+
lines.push("");
|
|
837
|
+
lines.push("Paste the following block into Settings > Profile > Custom Instructions:");
|
|
838
|
+
lines.push("");
|
|
839
|
+
lines.push("---");
|
|
840
|
+
lines.push(suggestion);
|
|
841
|
+
lines.push("---");
|
|
842
|
+
return lines.join("\n");
|
|
843
|
+
}
|
|
583
844
|
export async function uninstall(opts) {
|
|
584
845
|
const log = opts.log ?? ((m) => process.stderr.write(`${m}\n`));
|
|
585
846
|
const dryRun = !!opts.dryRun;
|
|
847
|
+
const platform = opts.platformOverride ?? osPlatform();
|
|
848
|
+
const client = opts.client ?? detectClient(opts.home, platform);
|
|
586
849
|
const summary = {
|
|
850
|
+
client,
|
|
587
851
|
hookEntryRemoved: false,
|
|
588
852
|
claudeMdSectionRemoved: false,
|
|
589
853
|
hookScriptRemoved: false,
|
|
590
854
|
sessionEndScriptRemoved: false,
|
|
591
855
|
sessionStartScriptRemoved: false,
|
|
592
856
|
stopScriptRemoved: false,
|
|
857
|
+
desktopConfigUpdated: false,
|
|
858
|
+
legacySlashCommandsRemoved: [],
|
|
593
859
|
tokenRemoved: false,
|
|
594
860
|
backups: [],
|
|
595
861
|
};
|
|
862
|
+
if (client === "claude-web") {
|
|
863
|
+
log("[gramatr] claude-web: no local state to remove. Disconnect the connector inside claude.ai.");
|
|
864
|
+
if (opts.purge) {
|
|
865
|
+
const tokPath = gramatrTokenPath(opts.home);
|
|
866
|
+
if (existsSync(tokPath)) {
|
|
867
|
+
if (!dryRun)
|
|
868
|
+
unlinkSync(tokPath);
|
|
869
|
+
summary.tokenRemoved = true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return summary;
|
|
873
|
+
}
|
|
874
|
+
if (client === "claude-desktop") {
|
|
875
|
+
const cfgPath = opts.desktopConfigPathOverride ?? claudeDesktopConfigPath(opts.home, platform);
|
|
876
|
+
const raw = readFileOr(cfgPath, null);
|
|
877
|
+
if (raw !== null) {
|
|
878
|
+
const current = parseJsonOr(raw, {});
|
|
879
|
+
const { next, changed } = uninstallDesktopConfig(current);
|
|
880
|
+
if (changed) {
|
|
881
|
+
if (!dryRun) {
|
|
882
|
+
const bak = backupFile(cfgPath);
|
|
883
|
+
if (bak)
|
|
884
|
+
summary.backups.push(bak);
|
|
885
|
+
atomicWriteFile(cfgPath, JSON.stringify(next, null, 2) + "\n", 0o600);
|
|
886
|
+
}
|
|
887
|
+
summary.desktopConfigUpdated = true;
|
|
888
|
+
log(`[gramatr] claude-desktop ${dryRun ? "(dry-run) " : ""}removed gramatr mcpServers entry`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (opts.purge) {
|
|
892
|
+
const tokPath = gramatrTokenPath(opts.home);
|
|
893
|
+
if (existsSync(tokPath)) {
|
|
894
|
+
if (!dryRun)
|
|
895
|
+
unlinkSync(tokPath);
|
|
896
|
+
summary.tokenRemoved = true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return summary;
|
|
900
|
+
}
|
|
596
901
|
// 1. settings.json
|
|
597
902
|
const settingsPath = claudeSettingsPath(opts.home);
|
|
598
903
|
const settingsRaw = readFileOr(settingsPath, null);
|
|
@@ -675,6 +980,12 @@ export async function uninstall(opts) {
|
|
|
675
980
|
summary.stopScriptRemoved = true;
|
|
676
981
|
log(`[gramatr] Stop hook script ${dryRun ? "(dry-run) " : ""}→ removed ${stopHookDst}`);
|
|
677
982
|
}
|
|
983
|
+
// 3e. Legacy slash commands (always cleaned during uninstall; #2490).
|
|
984
|
+
const removedCmds = cleanupLegacySlashCommands(opts.home, dryRun);
|
|
985
|
+
summary.legacySlashCommandsRemoved = removedCmds;
|
|
986
|
+
for (const p of removedCmds) {
|
|
987
|
+
log(`[gramatr] removed legacy slash command ${dryRun ? "(dry-run) " : ""}${p}`);
|
|
988
|
+
}
|
|
678
989
|
// 4. token (only on --purge)
|
|
679
990
|
if (opts.purge) {
|
|
680
991
|
const tokPath = gramatrTokenPath(opts.home);
|
|
@@ -725,6 +1036,17 @@ function resolveHome() {
|
|
|
725
1036
|
// gramatr-allow: C1 — CLI entry point, reads HOME for config path
|
|
726
1037
|
return process.env.HOME || process.env.USERPROFILE || "";
|
|
727
1038
|
}
|
|
1039
|
+
function parseClientFlag(argv) {
|
|
1040
|
+
for (const a of argv) {
|
|
1041
|
+
if (a.startsWith("--client=")) {
|
|
1042
|
+
const v = a.slice("--client=".length);
|
|
1043
|
+
if (v === "claude-code" || v === "claude-desktop" || v === "claude-web")
|
|
1044
|
+
return v;
|
|
1045
|
+
process.stderr.write(`[gramatr] unknown --client value: ${v} (expected claude-code|claude-desktop|claude-web)\n`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
728
1050
|
export async function runInstallCli(argv) {
|
|
729
1051
|
const home = resolveHome();
|
|
730
1052
|
if (!home) {
|
|
@@ -733,6 +1055,7 @@ export async function runInstallCli(argv) {
|
|
|
733
1055
|
}
|
|
734
1056
|
const opts = {
|
|
735
1057
|
home,
|
|
1058
|
+
client: parseClientFlag(argv),
|
|
736
1059
|
cleanLegacy: argv.includes("--clean-legacy"),
|
|
737
1060
|
nonInteractive: argv.includes("--non-interactive") || argv.includes("--yes") || argv.includes("-y"),
|
|
738
1061
|
dryRun: argv.includes("--dry-run"),
|
|
@@ -753,6 +1076,19 @@ export async function runInstallCli(argv) {
|
|
|
753
1076
|
}
|
|
754
1077
|
const summary = await install(opts);
|
|
755
1078
|
process.stderr.write("\n[gramatr] install summary:\n");
|
|
1079
|
+
process.stderr.write(` client: ${summary.client}\n`);
|
|
1080
|
+
if (summary.client === "claude-web") {
|
|
1081
|
+
// Instructions already printed via log().
|
|
1082
|
+
return 0;
|
|
1083
|
+
}
|
|
1084
|
+
if (summary.client === "claude-desktop") {
|
|
1085
|
+
process.stderr.write(` claude-desktop config: ${summary.desktopConfigUpdated ? "updated" : "up-to-date"}\n`);
|
|
1086
|
+
if (summary.backups.length > 0) {
|
|
1087
|
+
process.stderr.write(` backups: ${summary.backups.length}\n`);
|
|
1088
|
+
}
|
|
1089
|
+
process.stderr.write("\n Restart Claude Desktop to activate.\n");
|
|
1090
|
+
return 0;
|
|
1091
|
+
}
|
|
756
1092
|
process.stderr.write(` hook script: ${summary.hookScriptWritten ? "written" : "up-to-date"}\n`);
|
|
757
1093
|
process.stderr.write(` SessionEnd hook script: ${summary.sessionEndScriptWritten ? "written" : "up-to-date"}\n`);
|
|
758
1094
|
process.stderr.write(` SessionStart hook script: ${summary.sessionStartScriptWritten ? "written" : "up-to-date"}\n`);
|
|
@@ -765,6 +1101,9 @@ export async function runInstallCli(argv) {
|
|
|
765
1101
|
if (summary.legacyFilesRemoved.length > 0) {
|
|
766
1102
|
process.stderr.write(` legacy files removed: ${summary.legacyFilesRemoved.length}\n`);
|
|
767
1103
|
}
|
|
1104
|
+
if (summary.legacySlashCommandsRemoved.length > 0) {
|
|
1105
|
+
process.stderr.write(` legacy slash commands removed: ${summary.legacySlashCommandsRemoved.length}\n`);
|
|
1106
|
+
}
|
|
768
1107
|
if (summary.backups.length > 0) {
|
|
769
1108
|
process.stderr.write(` backups: ${summary.backups.length}\n`);
|
|
770
1109
|
for (const b of summary.backups)
|
|
@@ -781,16 +1120,33 @@ export async function runUninstallCli(argv) {
|
|
|
781
1120
|
}
|
|
782
1121
|
const summary = await uninstall({
|
|
783
1122
|
home,
|
|
1123
|
+
client: parseClientFlag(argv),
|
|
784
1124
|
purge: argv.includes("--purge"),
|
|
785
1125
|
dryRun: argv.includes("--dry-run"),
|
|
786
1126
|
});
|
|
787
1127
|
process.stderr.write("\n[gramatr] uninstall summary:\n");
|
|
1128
|
+
process.stderr.write(` client: ${summary.client}\n`);
|
|
1129
|
+
if (summary.client === "claude-web") {
|
|
1130
|
+
process.stderr.write(" (no local state — disconnect the connector inside claude.ai)\n");
|
|
1131
|
+
if (summary.tokenRemoved)
|
|
1132
|
+
process.stderr.write(` token: removed\n`);
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
if (summary.client === "claude-desktop") {
|
|
1136
|
+
process.stderr.write(` claude-desktop config: ${summary.desktopConfigUpdated ? "updated" : "not present"}\n`);
|
|
1137
|
+
if (summary.tokenRemoved)
|
|
1138
|
+
process.stderr.write(` token: removed\n`);
|
|
1139
|
+
return 0;
|
|
1140
|
+
}
|
|
788
1141
|
process.stderr.write(` hook entry: ${summary.hookEntryRemoved ? "removed" : "not present"}\n`);
|
|
789
1142
|
process.stderr.write(` CLAUDE.md section: ${summary.claudeMdSectionRemoved ? "removed" : "not present"}\n`);
|
|
790
1143
|
process.stderr.write(` hook script: ${summary.hookScriptRemoved ? "removed" : "not present"}\n`);
|
|
791
1144
|
process.stderr.write(` SessionEnd hook script: ${summary.sessionEndScriptRemoved ? "removed" : "not present"}\n`);
|
|
792
1145
|
process.stderr.write(` SessionStart hook script: ${summary.sessionStartScriptRemoved ? "removed" : "not present"}\n`);
|
|
793
1146
|
process.stderr.write(` Stop hook script: ${summary.stopScriptRemoved ? "removed" : "not present"}\n`);
|
|
1147
|
+
if (summary.legacySlashCommandsRemoved.length > 0) {
|
|
1148
|
+
process.stderr.write(` legacy slash commands removed: ${summary.legacySlashCommandsRemoved.length}\n`);
|
|
1149
|
+
}
|
|
794
1150
|
if (summary.tokenRemoved)
|
|
795
1151
|
process.stderr.write(` token: removed\n`);
|
|
796
1152
|
if (summary.backups.length > 0) {
|