@gramatr/mcp 0.8.9 → 0.8.11

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.
Files changed (73) hide show
  1. package/dist/bin/build-mcpb.d.ts +8 -2
  2. package/dist/bin/build-mcpb.d.ts.map +1 -1
  3. package/dist/bin/build-mcpb.js +7 -7
  4. package/dist/bin/build-mcpb.js.map +1 -1
  5. package/dist/bin/gramatr-mcp.d.ts.map +1 -1
  6. package/dist/bin/gramatr-mcp.js +2 -1
  7. package/dist/bin/gramatr-mcp.js.map +1 -1
  8. package/dist/bin/setup-config-io.d.ts +26 -0
  9. package/dist/bin/setup-config-io.d.ts.map +1 -0
  10. package/dist/bin/setup-config-io.js +131 -0
  11. package/dist/bin/setup-config-io.js.map +1 -0
  12. package/dist/bin/setup-legacy.d.ts +9 -0
  13. package/dist/bin/setup-legacy.d.ts.map +1 -0
  14. package/dist/bin/setup-legacy.js +289 -0
  15. package/dist/bin/setup-legacy.js.map +1 -0
  16. package/dist/bin/setup-platforms.d.ts +20 -0
  17. package/dist/bin/setup-platforms.d.ts.map +1 -0
  18. package/dist/bin/setup-platforms.js +180 -0
  19. package/dist/bin/setup-platforms.js.map +1 -0
  20. package/dist/bin/setup-shared.d.ts +26 -0
  21. package/dist/bin/setup-shared.d.ts.map +1 -0
  22. package/dist/bin/setup-shared.js +39 -0
  23. package/dist/bin/setup-shared.js.map +1 -0
  24. package/dist/bin/setup.d.ts +14 -68
  25. package/dist/bin/setup.d.ts.map +1 -1
  26. package/dist/bin/setup.js +33 -627
  27. package/dist/bin/setup.js.map +1 -1
  28. package/dist/hooks/generated/hook-timeouts.d.ts +13 -1
  29. package/dist/hooks/generated/hook-timeouts.d.ts.map +1 -1
  30. package/dist/hooks/generated/hook-timeouts.js +13 -1
  31. package/dist/hooks/generated/hook-timeouts.js.map +1 -1
  32. package/dist/hooks/generated/schema-constants.d.ts +23 -9
  33. package/dist/hooks/generated/schema-constants.d.ts.map +1 -1
  34. package/dist/hooks/generated/schema-constants.js +26 -9
  35. package/dist/hooks/generated/schema-constants.js.map +1 -1
  36. package/dist/hooks/input-validator.d.ts +5 -0
  37. package/dist/hooks/input-validator.d.ts.map +1 -1
  38. package/dist/hooks/input-validator.js +113 -1
  39. package/dist/hooks/input-validator.js.map +1 -1
  40. package/dist/hooks/lib/gramatr-hook-utils.d.ts.map +1 -1
  41. package/dist/hooks/lib/gramatr-hook-utils.js +2 -2
  42. package/dist/hooks/lib/gramatr-hook-utils.js.map +1 -1
  43. package/dist/hooks/lib/hook-state.d.ts.map +1 -1
  44. package/dist/hooks/lib/hook-state.js +29 -28
  45. package/dist/hooks/lib/hook-state.js.map +1 -1
  46. package/dist/hooks/lib/routing.js +1 -1
  47. package/dist/hooks/lib/routing.js.map +1 -1
  48. package/dist/hooks/lib/types.d.ts +0 -1
  49. package/dist/hooks/lib/types.d.ts.map +1 -1
  50. package/dist/hooks/user-prompt-submit.js +1 -1
  51. package/dist/hooks/user-prompt-submit.js.map +1 -1
  52. package/dist/proxy/remote-client.d.ts.map +1 -1
  53. package/dist/proxy/remote-client.js +2 -1
  54. package/dist/proxy/remote-client.js.map +1 -1
  55. package/dist/setup/examples/gramatr-opencode-plugin.js +5 -5
  56. package/dist/setup/examples/gramatr-opencode-plugin.js.map +1 -1
  57. package/dist/setup/generated/instruction-blocks.d.ts +27 -0
  58. package/dist/setup/generated/instruction-blocks.d.ts.map +1 -0
  59. package/dist/setup/generated/instruction-blocks.js +27 -0
  60. package/dist/setup/generated/instruction-blocks.js.map +1 -0
  61. package/dist/setup/generated/platform-hooks.d.ts +1 -1
  62. package/dist/setup/generated/platform-hooks.d.ts.map +1 -1
  63. package/dist/setup/generated/platform-hooks.js +37 -6
  64. package/dist/setup/generated/platform-hooks.js.map +1 -1
  65. package/dist/setup/instructions.d.ts.map +1 -1
  66. package/dist/setup/instructions.js +9 -46
  67. package/dist/setup/instructions.js.map +1 -1
  68. package/dist/setup/targets.d.ts +2 -2
  69. package/dist/setup/targets.d.ts.map +1 -1
  70. package/dist/setup/targets.js +5 -6
  71. package/dist/setup/targets.js.map +1 -1
  72. package/package.json +3 -2
  73. package/scripts/postinstall.mjs +54 -6
package/dist/bin/setup.js CHANGED
@@ -1,27 +1,36 @@
1
1
  /**
2
- * gramatr setup claude auto-configure Claude Code to use the local MCP server.
2
+ * gramatr setup — Main orchestrator for multi-platform setup.
3
3
  *
4
- * Writes the mcpServers entry into ~/.claude.json (Claude Code's global MCP config).
5
- * Safe: reads existing config, merges in the gramatr server entry, writes back.
6
- * Idempotent: running it twice produces the same result.
4
+ * Delegates to focused modules:
5
+ * - setup-config-io.ts — Config file reading/writing, path resolution
6
+ * - setup-legacy.ts — Legacy artifact cleanup (pre-rebrand remnants)
7
+ * - setup-platforms.ts — Platform-specific setup (Codex, Gemini, OpenCode, web, generic MCP)
8
+ * - setup-shared.ts — Binary resolution / deployment (npx no-ops)
9
+ *
10
+ * This file owns: setupClaude(), setupAutoInstall(), verifySetupInstall(),
11
+ * getAutoDetectedTargets(), and the convenience wrappers for MCP-only targets
12
+ * (Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, VS Code).
7
13
  *
8
14
  * Usage:
9
15
  * gramatr-mcp setup claude Configure Claude Code
10
16
  * gramatr-mcp setup claude --dry Run without writing
11
17
  */
12
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync, statSync, lstatSync, readlinkSync, unlinkSync, } from 'node:fs';
13
- import { join, resolve, dirname } from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
- import { getGramatrDirFromEnv, getGramatrUrlFromEnv } from '../config-runtime.js';
16
- import { buildClaudeHooksFile, buildCodexHooksFile, buildClaudeMcpServerEntry, ensureCodexHooksFeature, mergeManagedHooks, } from '../setup/integrations.js';
17
- import { CLAUDE_BLOCK_END, CLAUDE_BLOCK_START, CLAUDE_CODE_GUIDANCE, CODEX_BLOCK_END, CODEX_BLOCK_START, CODEX_GUIDANCE, buildInstallPromptSuggestion, } from '../setup/instructions.js';
18
- import { buildGeminiExtensionManifest, buildGeminiHooksFile, buildLocalMcpServerEntry, buildOpenCodeMcpServerEntry, getChatgptDesktopConfigPath, getClaudeDesktopConfigPath, getCursorConfigPath, getGeminiExtensionDir, getGeminiHooksPath, getGeminiManifestPath, getOpenCodeConfigPath, getVscodeConfigPath, getWindsurfConfigPath, mergeMcpServerConfig, } from '../setup/targets.js';
19
- import { buildConnectorInstructions, buildPromptSuggestion, validateServerReachability, } from '../setup/web-connector.js';
20
- import { installPlatformBinary } from '../setup/platform.js';
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
- // gramatr-allow: C1 CLI entry point, reads HOME for config path
24
- const HOME = process.env.HOME || process.env.USERPROFILE || '';
18
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, } from 'node:fs';
19
+ import { join, dirname } from 'node:path';
20
+ import { getGramatrUrlFromEnv } from '../config-runtime.js';
21
+ import { buildClaudeHooksFile, buildClaudeMcpServerEntry, mergeManagedHooks, } from '../setup/integrations.js';
22
+ import { CLAUDE_BLOCK_END, CLAUDE_BLOCK_START, CLAUDE_CODE_GUIDANCE, CODEX_BLOCK_END, CODEX_BLOCK_START, } from '../setup/instructions.js';
23
+ import { getChatgptDesktopConfigPath, getClaudeDesktopConfigPath, getCursorConfigPath, getGeminiHooksPath, getGeminiManifestPath, getOpenCodeConfigPath, getVscodeConfigPath, getWindsurfConfigPath, } from '../setup/targets.js';
24
+ // ── Re-export everything from sub-modules so consumers keep importing from setup.js ──
25
+ export { getClaudeConfigPath, getClaudeSettingsPath, getCodexHooksPath, getCodexConfigPath, getClaudeMarkdownPath, getCodexAgentsPath, getGramatrSettingsPath, readJsonFile, readClaudeConfig, escapeRegExp, upsertManagedBlock, ensureLocalSettings, parseJson, readManagedBlock, hasHookCommand, } from './setup-config-io.js';
26
+ export { runCleanInstall, } from './setup-legacy.js';
27
+ export { ensureCodexMcpServerConfig, emitInstallPromptSuggestion, setupCodex, setupMcpTarget, setupGemini, setupOpenCode, setupWeb, } from './setup-platforms.js';
28
+ export { resolveBinaryPath, deployPlatformBinary, } from './setup-shared.js';
29
+ // ── Local imports from sub-modules ──
30
+ import { readClaudeConfig, upsertManagedBlock, ensureLocalSettings, getClaudeConfigPath, getClaudeSettingsPath, getClaudeMarkdownPath, getCodexHooksPath as getCodexHooksPathFn, getCodexConfigPath as getCodexConfigPathFn, getCodexAgentsPath, getGramatrSettingsPath as getGramatrSettingsPathFn, parseJson, readManagedBlock, hasHookCommand, HOME, } from './setup-config-io.js';
31
+ import { runCleanInstall } from './setup-legacy.js';
32
+ import { emitInstallPromptSuggestion, setupCodex, setupMcpTarget, setupGemini, setupOpenCode, } from './setup-platforms.js';
33
+ import { deployPlatformBinary } from './setup-shared.js';
25
34
  export const AUTO_TARGET_ORDER = [
26
35
  'claude',
27
36
  'codex',
@@ -33,438 +42,7 @@ export const AUTO_TARGET_ORDER = [
33
42
  'windsurf',
34
43
  'vscode',
35
44
  ];
36
- const LEGACY_HOOK_BASENAMES = [
37
- 'LoadContext.hook.ts',
38
- 'SecurityValidator.hook.ts',
39
- 'RatingCapture.hook.ts',
40
- 'VoiceGate.hook.ts',
41
- 'AutoWorkCreation.hook.ts',
42
- 'WorkCompletionLearning.hook.ts',
43
- 'RelationshipMemory.hook.ts',
44
- 'SessionSummary.hook.ts',
45
- 'UpdateCounts.hook.ts',
46
- 'IntegrityCheck.hook.ts',
47
- ];
48
- const LEGACY_MCP_KEY_PATTERNS = [/^aios/i, /^pai$/i, /^pai[-_]/i, /^fabric/i];
49
- const LEGACY_CLAUDE_ARTIFACT_PATTERNS = [
50
- /^pai[-_]/i,
51
- /^pai$/i,
52
- /^fabric[-_]/i,
53
- /^fabric$/i,
54
- /^aios[-_]/i,
55
- /^aios$/i,
56
- /^extract[-_]?wisdom/i,
57
- /^pattern[-_]/i,
58
- /^official[-_]?pattern/i,
59
- /^becreative$/i,
60
- /^beexpert$/i,
61
- ];
62
- const LEGACY_MANAGED_BLOCK_MARKERS = [
63
- { start: '<!-- AIOS-START -->', end: '<!-- AIOS-END -->' },
64
- { start: '<!-- PAI-START -->', end: '<!-- PAI-END -->' },
65
- { start: '<!-- FABRIC-START -->', end: '<!-- FABRIC-END -->' },
66
- { start: '<!-- GMTR-START -->', end: '<!-- GMTR-END -->' },
67
- ];
68
- /**
69
- * Resolve the path to the gramatr binary.
70
- * Prefers the compiled Bun binary at ~/.gramatr/bin/gramatr (self-contained,
71
- * no Node version dependency). Falls back to npx for first-run / not-yet-installed.
72
- */
73
- export function resolveBinaryPath() {
74
- const bunBin = join(HOME, '.gramatr', 'bin', 'gramatr');
75
- if (existsSync(bunBin)) {
76
- return { command: bunBin, args: [] };
77
- }
78
- // Fallback to npx (first install, before binary is deployed)
79
- return { command: 'npx', args: ['-y', '@gramatr/mcp'] };
80
- }
81
- /**
82
- * Deploy the correct platform-specific pre-compiled binary to ~/.gramatr/bin/.
83
- *
84
- * Checks for platform-specific binaries (e.g. gramatr-linux-x64) in the package
85
- * dist/ directory, then falls back to the generic 'gramatr' binary from
86
- * build-binary.mjs. If neither exists, logs a warning and returns false so the
87
- * caller can fall back to npx.
88
- *
89
- * Uses atomic copy (write to .new, then rename) to avoid partial-binary issues.
90
- */
91
- export function deployPlatformBinary(dryRun = false) {
92
- // __dirname is src/bin/ at compile time; dist/ is ../../dist relative to that
93
- const distDir = resolve(__dirname, '..', '..', 'dist');
94
- const destDir = join(HOME, '.gramatr', 'bin');
95
- const result = installPlatformBinary({ distDir, destDir, dryRun });
96
- switch (result.status) {
97
- case 'installed':
98
- process.stderr.write(`[gramatr-mcp] ${result.message}\n`);
99
- return true;
100
- case 'fallback':
101
- process.stderr.write(`[gramatr-mcp] Platform binary not found (${result.message}). Will use npx fallback.\n`);
102
- return false;
103
- case 'skipped':
104
- process.stderr.write(`[gramatr-mcp] Binary install skipped: ${result.message}\n`);
105
- return false;
106
- default:
107
- return false;
108
- }
109
- }
110
- /**
111
- * Get the Claude Code config file path.
112
- * Claude Code stores global MCP config in ~/.claude.json
113
- */
114
- export function getClaudeConfigPath() {
115
- return join(HOME, '.claude.json');
116
- }
117
- export function getClaudeSettingsPath() {
118
- return join(HOME, '.claude', 'settings.json');
119
- }
120
- export function getCodexHooksPath() {
121
- return join(HOME, '.codex', 'hooks.json');
122
- }
123
- export function getCodexConfigPath() {
124
- return join(HOME, '.codex', 'config.toml');
125
- }
126
- export function getClaudeMarkdownPath() {
127
- return join(HOME, '.claude', 'CLAUDE.md');
128
- }
129
- export function getCodexAgentsPath() {
130
- return join(HOME, '.codex', 'AGENTS.md');
131
- }
132
- export function getGramatrSettingsPath() {
133
- const gramatrDir = getGramatrDirFromEnv() || join(HOME, '.gramatr');
134
- return join(gramatrDir, 'settings.json');
135
- }
136
- export function readJsonFile(path, fallback) {
137
- try {
138
- return JSON.parse(readFileSync(path, 'utf8'));
139
- }
140
- catch {
141
- return fallback;
142
- }
143
- }
144
- /**
145
- * Read existing Claude config or return empty.
146
- */
147
- export function readClaudeConfig(configPath) {
148
- try {
149
- const raw = readFileSync(configPath, 'utf8');
150
- return JSON.parse(raw);
151
- }
152
- catch {
153
- return {};
154
- }
155
- }
156
- export function upsertManagedBlock(existing, content, startMarker, endMarker) {
157
- const block = content.trim();
158
- const pattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}`, 'm');
159
- if (pattern.test(existing)) {
160
- return existing.replace(pattern, block);
161
- }
162
- const trimmed = existing.trim();
163
- return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
164
- }
165
- export function escapeRegExp(text) {
166
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167
- }
168
- function toTomlString(value) {
169
- return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
170
- }
171
- function stripTomlSection(content, section) {
172
- const pattern = new RegExp(`^\\[${escapeRegExp(section)}\\]\\n[\\s\\S]*?(?=^\\[[^\\n]+\\]\\n|\\s*$)`, 'm');
173
- return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
174
- }
175
- export function ensureCodexMcpServerConfig(configToml) {
176
- const gramatrDir = getGramatrDirFromEnv() || join(HOME, '.gramatr');
177
- const gramatrUrl = getGramatrUrlFromEnv() || 'https://api.gramatr.com/mcp';
178
- const binaryPath = join(gramatrDir, 'bin', 'gramatr');
179
- const base = stripTomlSection(stripTomlSection(configToml.trimEnd(), 'mcp_servers.gramatr.env'), 'mcp_servers.gramatr');
180
- const block = [
181
- '[mcp_servers.gramatr]',
182
- `command = ${toTomlString(binaryPath)}`,
183
- '',
184
- '[mcp_servers.gramatr.env]',
185
- `GRAMATR_DIR = ${toTomlString(gramatrDir)}`,
186
- `GRAMATR_URL = ${toTomlString(gramatrUrl)}`,
187
- ].join('\n');
188
- return `${base ? `${base}\n\n` : ''}${block}\n`;
189
- }
190
- export function ensureLocalSettings() {
191
- const settingsPath = getGramatrSettingsPath();
192
- const settingsDir = dirname(settingsPath);
193
- const current = existsSync(settingsPath)
194
- ? readClaudeConfig(settingsPath)
195
- : {};
196
- const principalName = process.env.GRAMATR_PRINCIPAL_NAME || current.principal?.name || 'User';
197
- const principalTimezone = process.env.GRAMATR_PRINCIPAL_TIMEZONE || current.principal?.timezone || 'UTC';
198
- const next = {
199
- ...current,
200
- daidentity: current.daidentity || {
201
- name: 'gramatr',
202
- fullName: 'gramatr — Personal AI',
203
- displayName: 'gramatr',
204
- color: '#3B82F6',
205
- },
206
- principal: {
207
- ...(current.principal || {}),
208
- name: principalName,
209
- timezone: principalTimezone,
210
- },
211
- };
212
- mkdirSync(settingsDir, { recursive: true });
213
- writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
214
- }
215
- function matchesLegacyClaudeArtifact(name) {
216
- const stem = name.replace(/\.md$/i, '');
217
- return LEGACY_CLAUDE_ARTIFACT_PATTERNS.some((pattern) => pattern.test(stem));
218
- }
219
- function sanitizeLegacyMcpServers(raw) {
220
- if (!raw.mcpServers || typeof raw.mcpServers !== 'object')
221
- return raw;
222
- const servers = raw.mcpServers;
223
- const next = {};
224
- for (const [key, value] of Object.entries(servers)) {
225
- if (LEGACY_MCP_KEY_PATTERNS.some((pattern) => pattern.test(key)))
226
- continue;
227
- next[key] = value;
228
- }
229
- return { ...raw, mcpServers: next };
230
- }
231
- function stripManagedBlock(content, startMarker, endMarker) {
232
- const pattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, 'gm');
233
- return content.replace(pattern, '').trimEnd() + '\n';
234
- }
235
- function isLegacyHookCommand(command) {
236
- if (command.includes('/.claude/hooks/'))
237
- return true;
238
- if (command.includes('/aios-v2-client/'))
239
- return true;
240
- if (command.includes('/fabric/'))
241
- return true;
242
- if (command.includes('/pai/'))
243
- return true;
244
- return LEGACY_HOOK_BASENAMES.some((basename) => command.includes(`/${basename}`));
245
- }
246
- function sanitizeHookEventArray(value) {
247
- if (!Array.isArray(value))
248
- return value;
249
- return value
250
- .map((entry) => {
251
- if (!entry || typeof entry !== 'object')
252
- return entry;
253
- const commands = entry.hooks;
254
- if (!Array.isArray(commands))
255
- return entry;
256
- const filtered = commands.filter((cmd) => {
257
- const command = typeof cmd?.command === 'string' ? cmd.command : '';
258
- return !command || !isLegacyHookCommand(command);
259
- });
260
- if (filtered.length === 0)
261
- return null;
262
- return { ...entry, hooks: filtered };
263
- })
264
- .filter(Boolean);
265
- }
266
- function sanitizeHookFile(hookFile) {
267
- const hooksRoot = hookFile.hooks;
268
- if (!hooksRoot || typeof hooksRoot !== 'object')
269
- return hookFile;
270
- const nextHooks = { ...hooksRoot };
271
- for (const [eventName, value] of Object.entries(nextHooks)) {
272
- nextHooks[eventName] = sanitizeHookEventArray(value);
273
- }
274
- return { ...hookFile, hooks: nextHooks };
275
- }
276
- function removeDirectoryIfExists(path, dryRun) {
277
- if (!existsSync(path))
278
- return;
279
- if (dryRun) {
280
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would remove ${path}\n`);
281
- return;
282
- }
283
- rmSync(path, { recursive: true, force: true });
284
- process.stderr.write(`[gramatr-mcp] clean-install: removed ${path}\n`);
285
- }
286
- function removeFileIfExists(path, dryRun) {
287
- if (!existsSync(path))
288
- return;
289
- if (dryRun) {
290
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would remove ${path}\n`);
291
- return;
292
- }
293
- rmSync(path, { force: true });
294
- process.stderr.write(`[gramatr-mcp] clean-install: removed ${path}\n`);
295
- }
296
- function cleanLegacyClaudeArtifacts(dryRun) {
297
- process.stderr.write('[gramatr-mcp] clean-install: removing legacy Claude + PAI/Fabric/AIOS artifacts\n');
298
- const legacyDirs = [
299
- join(HOME, 'aios-v2-client'),
300
- join(HOME, '.claude', 'hooks'),
301
- ];
302
- for (const dir of legacyDirs)
303
- removeDirectoryIfExists(dir, dryRun);
304
- const commandsDir = join(HOME, '.claude', 'commands');
305
- if (existsSync(commandsDir)) {
306
- try {
307
- for (const entry of readdirSync(commandsDir)) {
308
- if (!entry.endsWith('.md'))
309
- continue;
310
- if (!matchesLegacyClaudeArtifact(entry))
311
- continue;
312
- removeFileIfExists(join(commandsDir, entry), dryRun);
313
- }
314
- }
315
- catch {
316
- // non-critical
317
- }
318
- }
319
- const skillsDir = join(HOME, '.claude', 'skills');
320
- if (existsSync(skillsDir)) {
321
- try {
322
- for (const entry of readdirSync(skillsDir)) {
323
- const path = join(skillsDir, entry);
324
- if (!statSync(path).isDirectory())
325
- continue;
326
- if (!matchesLegacyClaudeArtifact(entry))
327
- continue;
328
- removeDirectoryIfExists(path, dryRun);
329
- }
330
- }
331
- catch {
332
- // non-critical
333
- }
334
- }
335
- const clientHooksDir = join(HOME, '.gramatr', 'hooks');
336
- if (existsSync(clientHooksDir)) {
337
- for (const basename of LEGACY_HOOK_BASENAMES) {
338
- removeFileIfExists(join(clientHooksDir, basename), dryRun);
339
- }
340
- try {
341
- for (const entry of readdirSync(clientHooksDir)) {
342
- if (!entry.endsWith('.sh'))
343
- continue;
344
- removeFileIfExists(join(clientHooksDir, entry), dryRun);
345
- }
346
- }
347
- catch {
348
- // non-critical
349
- }
350
- }
351
- }
352
- function sanitizeLegacyMcpServersInFile(path, dryRun) {
353
- const current = readJsonFile(path, {});
354
- const next = sanitizeLegacyMcpServers(current);
355
- if (JSON.stringify(next) === JSON.stringify(current))
356
- return;
357
- if (dryRun) {
358
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would sanitize legacy mcpServers in ${path}\n`);
359
- return;
360
- }
361
- mkdirSync(dirname(path), { recursive: true });
362
- writeFileSync(path, JSON.stringify(next, null, 2) + '\n', 'utf8');
363
- process.stderr.write(`[gramatr-mcp] clean-install: sanitized legacy mcpServers in ${path}\n`);
364
- }
365
- function sanitizeHookFileAtPath(path, dryRun, label) {
366
- const current = readJsonFile(path, {});
367
- const next = sanitizeHookFile(current);
368
- if (JSON.stringify(next) === JSON.stringify(current))
369
- return;
370
- if (dryRun) {
371
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would sanitize legacy hooks in ${path}\n`);
372
- return;
373
- }
374
- mkdirSync(dirname(path), { recursive: true });
375
- writeFileSync(path, JSON.stringify(next, null, 2) + '\n', 'utf8');
376
- process.stderr.write(`[gramatr-mcp] clean-install: sanitized legacy hooks (${label}) in ${path}\n`);
377
- }
378
- function stripLegacyManagedBlocks(path, dryRun) {
379
- if (!existsSync(path))
380
- return;
381
- const current = readFileSync(path, 'utf8');
382
- let next = current;
383
- for (const marker of LEGACY_MANAGED_BLOCK_MARKERS) {
384
- next = stripManagedBlock(next, marker.start, marker.end);
385
- }
386
- if (next === current)
387
- return;
388
- if (dryRun) {
389
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would strip legacy managed blocks in ${path}\n`);
390
- return;
391
- }
392
- writeFileSync(path, next, 'utf8');
393
- process.stderr.write(`[gramatr-mcp] clean-install: stripped legacy managed blocks in ${path}\n`);
394
- }
395
- function cleanAllInstallTargets(dryRun) {
396
- sanitizeLegacyMcpServersInFile(getClaudeConfigPath(), dryRun);
397
- sanitizeLegacyMcpServersInFile(join(HOME, '.claude', 'mcp.json'), dryRun);
398
- sanitizeLegacyMcpServersInFile(getClaudeDesktopConfigPath(HOME), dryRun);
399
- sanitizeLegacyMcpServersInFile(getChatgptDesktopConfigPath(HOME), dryRun);
400
- sanitizeLegacyMcpServersInFile(getCursorConfigPath(HOME), dryRun);
401
- sanitizeLegacyMcpServersInFile(getWindsurfConfigPath(HOME), dryRun);
402
- sanitizeLegacyMcpServersInFile(getVscodeConfigPath(HOME), dryRun);
403
- sanitizeLegacyMcpServersInFile(getGeminiManifestPath(HOME), dryRun);
404
- sanitizeHookFileAtPath(getClaudeSettingsPath(), dryRun, 'claude');
405
- sanitizeHookFileAtPath(getCodexHooksPath(), dryRun, 'codex');
406
- sanitizeHookFileAtPath(getGeminiHooksPath(HOME), dryRun, 'gemini');
407
- stripLegacyManagedBlocks(getClaudeMarkdownPath(), dryRun);
408
- stripLegacyManagedBlocks(getCodexAgentsPath(), dryRun);
409
- }
410
- function cleanLegacyHomeNodeShims(dryRun) {
411
- const candidates = new Set();
412
- candidates.add(join(HOME, '.local', 'bin', 'gramatr-mcp'));
413
- candidates.add(join(HOME, '.local', 'bin', 'gramatr'));
414
- candidates.add(join(HOME, 'bin', 'gramatr-mcp'));
415
- candidates.add(join(HOME, 'bin', 'gramatr'));
416
- candidates.add(join(HOME, 'bin', 'gramatr-mac'));
417
- for (const shimPath of candidates) {
418
- if (!existsSync(shimPath))
419
- continue;
420
- try {
421
- let stale = false;
422
- if (lstatSync(shimPath).isSymbolicLink()) {
423
- const target = readlinkSync(shimPath);
424
- if (target.includes('aios-v2-client')
425
- || target.includes('/packages/client/bin/gramatr')
426
- || target.includes('/fabric/')
427
- || target.includes('/pai/')) {
428
- stale = true;
429
- }
430
- }
431
- else {
432
- const body = readFileSync(shimPath, 'utf8');
433
- if (body.includes('aios-v2-client')
434
- || body.includes('/packages/client/bin/gramatr')
435
- || body.includes('/fabric/')
436
- || body.includes('/pai/')) {
437
- stale = true;
438
- }
439
- }
440
- if (!stale)
441
- continue;
442
- if (dryRun) {
443
- process.stderr.write(`[gramatr-mcp] clean-install dry-run: would remove stale shim ${shimPath}\n`);
444
- }
445
- else {
446
- unlinkSync(shimPath);
447
- process.stderr.write(`[gramatr-mcp] clean-install: removed stale shim ${shimPath}\n`);
448
- }
449
- }
450
- catch {
451
- // non-critical
452
- }
453
- }
454
- }
455
- export function runCleanInstall(dryRun) {
456
- process.stderr.write('[gramatr-mcp] clean-install: running global legacy cleanup across known install targets\n');
457
- cleanAllInstallTargets(dryRun);
458
- cleanLegacyClaudeArtifacts(dryRun);
459
- cleanLegacyHomeNodeShims(dryRun);
460
- }
461
- export function emitInstallPromptSuggestion(target) {
462
- const promptBlock = buildInstallPromptSuggestion(target);
463
- process.stderr.write('\n━━━ Prompt Suggestion (copy into custom instructions) ━━━\n\n');
464
- process.stdout.write(promptBlock + '\n');
465
- process.stderr.write('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
466
- process.stderr.write('[gramatr-mcp] Paste the prompt block above into your custom instructions / project knowledge.\n');
467
- }
45
+ // ── setupClaude — the primary Claude Code setup ──
468
46
  export function setupClaude(dryRun = false, cleanInstall = false, showPrompts = false) {
469
47
  if (cleanInstall) {
470
48
  runCleanInstall(dryRun);
@@ -550,60 +128,7 @@ export function setupClaude(dryRun = false, cleanInstall = false, showPrompts =
550
128
  if (showPrompts)
551
129
  emitInstallPromptSuggestion('claude-code');
552
130
  }
553
- export function setupCodex(dryRun = false, showPrompts = false) {
554
- deployPlatformBinary(dryRun);
555
- const hooksPath = getCodexHooksPath();
556
- const configPath = getCodexConfigPath();
557
- const agentsPath = getCodexAgentsPath();
558
- const hooksConfig = readClaudeConfig(hooksPath);
559
- const managedHooks = buildCodexHooksFile(join(HOME, '.gramatr'));
560
- const mergedHooks = mergeManagedHooks(hooksConfig, managedHooks);
561
- const existingToml = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
562
- const updatedToml = ensureCodexMcpServerConfig(ensureCodexHooksFeature(existingToml));
563
- const existingAgents = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf8') : '';
564
- const updatedAgents = upsertManagedBlock(existingAgents, CODEX_GUIDANCE, CODEX_BLOCK_START, CODEX_BLOCK_END);
565
- if (dryRun) {
566
- process.stderr.write('[gramatr-mcp] Dry run — would write to: ' + hooksPath + '\n');
567
- process.stderr.write(JSON.stringify(mergedHooks, null, 2) + '\n');
568
- process.stderr.write('[gramatr-mcp] Dry run — would write to: ' + configPath + '\n');
569
- process.stderr.write(updatedToml);
570
- process.stderr.write('\n[gramatr-mcp] Dry run — would write to: ' + agentsPath + '\n');
571
- process.stderr.write(updatedAgents + '\n');
572
- return;
573
- }
574
- mkdirSync(join(HOME, '.codex'), { recursive: true });
575
- ensureLocalSettings();
576
- writeFileSync(hooksPath, JSON.stringify(mergedHooks, null, 2) + '\n', 'utf8');
577
- writeFileSync(configPath, updatedToml, 'utf8');
578
- writeFileSync(agentsPath, updatedAgents, 'utf8');
579
- process.stderr.write(`[gramatr-mcp] Configured Codex hooks in ${hooksPath}\n`);
580
- process.stderr.write(`[gramatr-mcp] Configured Codex MCP + hooks in ${configPath}\n`);
581
- process.stderr.write(`[gramatr-mcp] Configured Codex guidance in ${agentsPath}\n`);
582
- process.stderr.write('[gramatr-mcp] Restart Codex or start a new session to pick up the change.\n');
583
- if (showPrompts)
584
- emitInstallPromptSuggestion('codex');
585
- }
586
- /**
587
- * Generic MCP-only target setup — merges the gramatr MCP server entry into
588
- * the target's JSON config file. Used by all platforms that support MCP via
589
- * a JSON config (Claude Desktop, ChatGPT Desktop, Cursor, Windsurf, VS Code).
590
- */
591
- export function setupMcpTarget(targetName, configPath, dryRun) {
592
- deployPlatformBinary(dryRun);
593
- const current = readJsonFile(configPath, {});
594
- const gramatrUrl = getGramatrUrlFromEnv() || 'https://api.gramatr.com/mcp';
595
- const next = mergeMcpServerConfig(current, buildLocalMcpServerEntry(HOME, gramatrUrl));
596
- if (dryRun) {
597
- process.stderr.write(`[gramatr-mcp] Dry run — would write ${targetName} config to: ${configPath}\n`);
598
- process.stderr.write(JSON.stringify(next, null, 2) + '\n');
599
- return;
600
- }
601
- mkdirSync(dirname(configPath), { recursive: true });
602
- ensureLocalSettings();
603
- writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
604
- process.stderr.write(`[gramatr-mcp] Configured ${targetName} MCP server in ${configPath}\n`);
605
- process.stderr.write(`[gramatr-mcp] Restart ${targetName} to pick up the change.\n`);
606
- }
131
+ // ── Convenience wrappers for MCP-only targets ──
607
132
  export function setupClaudeDesktop(dryRun = false, showPrompts = false) {
608
133
  setupMcpTarget('Claude Desktop', getClaudeDesktopConfigPath(HOME), dryRun);
609
134
  if (showPrompts)
@@ -629,6 +154,7 @@ export function setupVscode(dryRun = false, showPrompts = false) {
629
154
  if (showPrompts)
630
155
  emitInstallPromptSuggestion('vscode');
631
156
  }
157
+ // ── Auto-detect + auto-install ──
632
158
  export function getAutoDetectedTargets() {
633
159
  const checks = {
634
160
  claude: existsSync(join(HOME, '.claude')) || existsSync(getClaudeConfigPath()),
@@ -710,130 +236,10 @@ export function setupAutoInstall(options = {}) {
710
236
  process.stderr.write(`[gramatr-mcp] Auto setup completed for ${selected.length} target(s).\n`);
711
237
  return selected.length;
712
238
  }
713
- export function setupGemini(dryRun = false, showPrompts = false) {
714
- deployPlatformBinary(dryRun);
715
- const extensionDir = getGeminiExtensionDir(HOME);
716
- const manifestPath = getGeminiManifestPath(HOME);
717
- const hooksPath = getGeminiHooksPath(HOME);
718
- const gramatrUrl = getGramatrUrlFromEnv() || 'https://api.gramatr.com/mcp';
719
- const manifest = buildGeminiExtensionManifest(HOME, gramatrUrl);
720
- const hooks = buildGeminiHooksFile(HOME);
721
- if (dryRun) {
722
- process.stderr.write('[gramatr-mcp] Dry run — would write Gemini manifest to: ' + manifestPath + '\n');
723
- process.stderr.write(JSON.stringify(manifest, null, 2) + '\n');
724
- process.stderr.write('[gramatr-mcp] Dry run — would write Gemini hooks to: ' + hooksPath + '\n');
725
- process.stderr.write(JSON.stringify(hooks, null, 2) + '\n');
726
- if (showPrompts)
727
- emitInstallPromptSuggestion('gemini-cli');
728
- return;
729
- }
730
- mkdirSync(join(extensionDir, 'hooks'), { recursive: true });
731
- ensureLocalSettings();
732
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
733
- writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + '\n', 'utf8');
734
- process.stderr.write(`[gramatr-mcp] Configured Gemini extension manifest in ${manifestPath}\n`);
735
- process.stderr.write(`[gramatr-mcp] Configured Gemini hooks in ${hooksPath}\n`);
736
- process.stderr.write('[gramatr-mcp] Restart Gemini CLI to pick up the change.\n');
737
- if (showPrompts)
738
- emitInstallPromptSuggestion('gemini-cli');
739
- }
740
- export function setupOpenCode(dryRun = false, showPrompts = false) {
741
- deployPlatformBinary(dryRun);
742
- const configPath = getOpenCodeConfigPath(HOME);
743
- const gramatrUrl = getGramatrUrlFromEnv() || 'https://api.gramatr.com/mcp';
744
- const mcpEntry = buildOpenCodeMcpServerEntry(HOME, gramatrUrl);
745
- // OpenCode stores its config as JSON with an mcp section (similar to Cursor/VS Code)
746
- const current = readJsonFile(configPath, {});
747
- const next = mergeMcpServerConfig(current, mcpEntry);
748
- if (dryRun) {
749
- process.stderr.write('[gramatr-mcp] Dry run — would write OpenCode config to: ' + configPath + '\n');
750
- process.stderr.write(JSON.stringify(next, null, 2) + '\n');
751
- if (showPrompts)
752
- emitInstallPromptSuggestion('opencode');
753
- return;
754
- }
755
- mkdirSync(dirname(configPath), { recursive: true });
756
- ensureLocalSettings();
757
- writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n', 'utf8');
758
- process.stderr.write(`[gramatr-mcp] Configured OpenCode MCP server in ${configPath}\n`);
759
- process.stderr.write('[gramatr-mcp] Copy the plugin scaffold from packages/mcp/src/setup/examples/gramatr-opencode-plugin.ts\n');
760
- process.stderr.write('[gramatr-mcp] Restart OpenCode to pick up the change.\n');
761
- if (showPrompts)
762
- emitInstallPromptSuggestion('opencode');
763
- }
764
- export async function setupWeb(target = 'claude-web') {
765
- const gramatrUrl = getGramatrUrlFromEnv() || 'https://api.gramatr.com/mcp';
766
- // Check reachability first
767
- process.stderr.write(`[gramatr-mcp] Checking server reachability at ${gramatrUrl}...\n`);
768
- const reachability = await validateServerReachability(gramatrUrl);
769
- if (reachability.reachable) {
770
- process.stderr.write(`[gramatr-mcp] Server is reachable (HTTP ${reachability.statusCode})\n\n`);
771
- }
772
- else {
773
- process.stderr.write(`[gramatr-mcp] Warning: server unreachable — ${reachability.error}\n`);
774
- process.stderr.write('[gramatr-mcp] Setup instructions generated anyway. Verify connectivity before use.\n\n');
775
- }
776
- // Build and display connector instructions
777
- const instructions = buildConnectorInstructions({ serverUrl: gramatrUrl, target });
778
- process.stderr.write('━━━ Connector Setup Instructions ━━━\n\n');
779
- process.stderr.write(` Target: ${instructions.target}\n`);
780
- process.stderr.write(` Server URL: ${instructions.serverUrl}\n`);
781
- process.stderr.write(` Auth: ${instructions.authMethod}\n`);
782
- process.stderr.write(` Server card: ${instructions.serverCardUrl}\n\n`);
783
- for (let i = 0; i < instructions.steps.length; i++) {
784
- process.stderr.write(` ${i + 1}. ${instructions.steps[i]}\n`);
785
- }
786
- // Build and display the prompt suggestion
787
- const promptBlock = buildPromptSuggestion(target);
788
- process.stderr.write('\n━━━ Prompt Suggestion (copy into custom instructions) ━━━\n\n');
789
- // Write to stdout so it's easily pipeable/copyable
790
- process.stdout.write(promptBlock + '\n');
791
- process.stderr.write('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
792
- process.stderr.write('[gramatr-mcp] Paste the prompt block above into your custom instructions / project knowledge.\n');
793
- }
239
+ // ── Verification ──
794
240
  function addResult(items, severity, label, detail) {
795
241
  items.push({ severity, label, detail });
796
242
  }
797
- function parseJson(path) {
798
- try {
799
- return JSON.parse(readFileSync(path, 'utf8'));
800
- }
801
- catch {
802
- return null;
803
- }
804
- }
805
- function hasHookCommand(config, eventName, needle) {
806
- if (!config || typeof config !== 'object')
807
- return false;
808
- const hooksRoot = config.hooks;
809
- if (!hooksRoot || typeof hooksRoot !== 'object')
810
- return false;
811
- const entries = hooksRoot[eventName];
812
- if (!Array.isArray(entries))
813
- return false;
814
- for (const entry of entries) {
815
- if (!entry || typeof entry !== 'object')
816
- continue;
817
- const commands = entry.hooks;
818
- if (!Array.isArray(commands))
819
- continue;
820
- for (const command of commands) {
821
- const value = command?.command;
822
- if (typeof value === 'string' && value.includes(needle)) {
823
- return true;
824
- }
825
- }
826
- }
827
- return false;
828
- }
829
- function readManagedBlock(path, startMarker, endMarker) {
830
- if (!existsSync(path))
831
- return null;
832
- const body = readFileSync(path, 'utf8');
833
- const pattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}`, 'm');
834
- const match = body.match(pattern);
835
- return match ? match[0] : null;
836
- }
837
243
  function verifyClaude(items) {
838
244
  const configPath = getClaudeConfigPath();
839
245
  const settingsPath = getClaudeSettingsPath();
@@ -866,8 +272,8 @@ function verifyClaude(items) {
866
272
  }
867
273
  }
868
274
  function verifyCodex(items) {
869
- const hooksPath = getCodexHooksPath();
870
- const configPath = getCodexConfigPath();
275
+ const hooksPath = getCodexHooksPathFn();
276
+ const configPath = getCodexConfigPathFn();
871
277
  const agentsPath = getCodexAgentsPath();
872
278
  const hooks = parseJson(hooksPath);
873
279
  const hasPromptHook = hasHookCommand(hooks, 'UserPromptSubmit', 'hook user-prompt-submit');
@@ -942,7 +348,7 @@ function verifyGemini(items) {
942
348
  }
943
349
  }
944
350
  function verifyLocalSettings(items) {
945
- const settingsPath = getGramatrSettingsPath();
351
+ const settingsPath = getGramatrSettingsPathFn();
946
352
  const settings = parseJson(settingsPath);
947
353
  if (!settings) {
948
354
  addResult(items, 'error', 'runtime.settings', `${settingsPath} is missing or invalid JSON`);