gramatr 0.3.55 → 0.3.57

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/bin/gramatr.js CHANGED
@@ -3,25 +3,18 @@
3
3
  * gramatr CLI entry point — thin JS wrapper that bootstraps TypeScript.
4
4
  * tsx is a production dependency, resolved directly from node_modules.
5
5
  */
6
- const { spawnSync, execSync } = require('child_process');
6
+ const { spawnSync } = require('child_process');
7
7
  const { join, dirname } = require('path');
8
8
 
9
9
  const script = join(__dirname, 'gramatr.ts');
10
10
  const args = process.argv.slice(2);
11
11
 
12
- // Try bun first (fastest)
13
- let hasBun = false;
14
- try {
15
- execSync('bun --version', { stdio: 'ignore' });
16
- hasBun = true;
17
- } catch {}
18
-
19
- if (hasBun) {
20
- const r = spawnSync('bun', [script, ...args], { stdio: 'inherit' });
21
- process.exit(r.status ?? 1);
22
- }
12
+ // gramatr standardizes on `npx tsx` — see issue #468 for the architectural
13
+ // rationale. bun detection was removed because it silently produced broken
14
+ // hook + statusline configs on hosts where the install-time PATH did not
15
+ // match the runtime PATH of spawned subprocesses.
23
16
 
24
- // Resolve tsx from node_modules (installed as dependency)
17
+ // Resolve tsx from node_modules (tsx is a production dependency)
25
18
  try {
26
19
  const tsxBin = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
27
20
  const r = spawnSync(process.execPath, [tsxBin, script, ...args], { stdio: 'inherit' });
package/bin/gramatr.ts CHANGED
@@ -6,14 +6,12 @@ import { homedir } from 'os';
6
6
  import { dirname, join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { detectTargets, findTarget, summarizeDetectedLocalTargets, type IntegrationTargetId } from '../core/targets.ts';
9
+ import { VERSION } from '../core/version.ts';
9
10
  import { findStaleArtifacts, runLegacyMigration } from '../core/migration.ts';
10
- import { detectTsRunner } from '../core/install.ts';
11
11
  import {
12
12
  formatDetectionLines,
13
13
  formatDoctorLines,
14
- formatInstallMenuLines,
15
14
  formatRemoteGuidanceLines,
16
- resolveInteractiveSelection,
17
15
  } from '../core/installer-cli.ts';
18
16
 
19
17
  const currentFile = fileURLToPath(import.meta.url);
@@ -45,34 +43,17 @@ function renderRemoteGuidance(): void {
45
43
  for (const line of formatRemoteGuidanceLines(detectTargets())) log(line);
46
44
  }
47
45
 
48
- function prompt(question: string): string {
49
- process.stdout.write(question);
50
- const input = spawnSync('bash', ['-lc', 'IFS= read -r line; printf "%s" "$line"'], {
51
- stdio: ['inherit', 'pipe', 'inherit'],
52
- });
53
- return input.stdout?.toString('utf8').trim() || '';
54
- }
55
-
56
- function renderInstallMenu(): IntegrationTargetId[] {
57
- const rendered = formatInstallMenuLines(detectTargets());
58
- for (const line of rendered.lines) log(line);
59
- return rendered.targetIds;
60
- }
61
-
62
46
  function runTs(script: string, extraArgs: string[] = []): void {
63
- const runner = detectTsRunner();
64
- if (runner === 'bun') {
65
- run('bun', [script, ...extraArgs]);
66
- } else {
67
- // Resolve tsx from this package's node_modules (not CWD)
68
- try {
69
- const { dirname, join } = require('path');
70
- const tsxCli = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
71
- run(process.execPath, [tsxCli, script, ...extraArgs]);
72
- } catch {
73
- // Fallback: hope tsx is globally available
74
- run('npx', ['tsx', script, ...extraArgs]);
75
- }
47
+ // Resolve tsx from this package's node_modules (not CWD) so `npx tsx` works
48
+ // even on hosts where the user hasn't globally installed tsx.
49
+ try {
50
+ const { dirname, join } = require('path');
51
+ const tsxCli = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
52
+ run(process.execPath, [tsxCli, script, ...extraArgs]);
53
+ } catch {
54
+ // Fallback: global npx tsx
55
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
56
+ run(npxBin, ['tsx', script, ...extraArgs]);
76
57
  }
77
58
  }
78
59
 
@@ -83,6 +64,122 @@ const forwardedFlags = process.argv.slice(2).filter(a =>
83
64
  (process.argv[process.argv.indexOf(a) - 1] === '--timezone')
84
65
  );
85
66
 
67
+ interface InstallAllResult {
68
+ id: IntegrationTargetId;
69
+ label: string;
70
+ status: 'ok' | 'fail' | 'skipped';
71
+ message?: string;
72
+ }
73
+
74
+ const INSTALL_ALL_CANDIDATES: Array<{ id: IntegrationTargetId; label: string }> = [
75
+ { id: 'claude-code', label: 'Claude Code' },
76
+ { id: 'codex', label: 'Codex' },
77
+ { id: 'gemini-cli', label: 'Gemini CLI' },
78
+ { id: 'claude-desktop', label: 'Claude Desktop' },
79
+ { id: 'chatgpt-desktop', label: 'ChatGPT Desktop' },
80
+ ];
81
+
82
+ function installAll(): void {
83
+ const detections = detectTargets();
84
+ const detectedIds = new Set(
85
+ detections.filter((t) => t.kind === 'local' && t.detection.detected).map((t) => t.id),
86
+ );
87
+
88
+ log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
89
+ log(` gramatr v${VERSION} — install all detected`);
90
+ log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
91
+ log('');
92
+
93
+ const results: InstallAllResult[] = [];
94
+
95
+ for (const candidate of INSTALL_ALL_CANDIDATES) {
96
+ if (!detectedIds.has(candidate.id)) {
97
+ results.push({ id: candidate.id, label: candidate.label, status: 'skipped', message: 'not detected' });
98
+ continue;
99
+ }
100
+ log(`━━━ Installing ${candidate.label} ━━━`);
101
+ try {
102
+ installTargetForAll(candidate.id);
103
+ results.push({ id: candidate.id, label: candidate.label, status: 'ok' });
104
+ } catch (err: any) {
105
+ results.push({
106
+ id: candidate.id,
107
+ label: candidate.label,
108
+ status: 'fail',
109
+ message: err?.message || String(err),
110
+ });
111
+ log(` X ${candidate.label} install failed: ${err?.message || err}`);
112
+ }
113
+ log('');
114
+ }
115
+
116
+ // Summary
117
+ log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
118
+ log(' gramatr install summary');
119
+ log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
120
+ const pad = (s: string, n: number) => s + ' '.repeat(Math.max(1, n - s.length));
121
+ for (const r of results) {
122
+ const statusStr =
123
+ r.status === 'ok' ? 'OK'
124
+ : r.status === 'fail' ? 'FAIL'
125
+ : 'not detected — skipped';
126
+ log(` ${pad(r.label, 18)}${statusStr}${r.message && r.status === 'fail' ? ` (${r.message})` : ''}`);
127
+ }
128
+ log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
129
+ log('');
130
+
131
+ const okList = results.filter((r) => r.status === 'ok');
132
+ if (okList.length > 0) {
133
+ log(' Next steps:');
134
+ let step = 1;
135
+ for (const r of okList) {
136
+ switch (r.id) {
137
+ case 'claude-code':
138
+ log(` ${step++}. Restart Claude Code to pick up MCP server config`);
139
+ break;
140
+ case 'codex':
141
+ log(` ${step++}. Restart Codex to load updated hooks`);
142
+ break;
143
+ case 'gemini-cli':
144
+ log(` ${step++}. Restart Gemini CLI to load the extension`);
145
+ break;
146
+ case 'claude-desktop':
147
+ log(` ${step++}. Restart Claude Desktop to load MCP server`);
148
+ break;
149
+ case 'chatgpt-desktop':
150
+ log(` ${step++}. Restart ChatGPT Desktop to load MCP server`);
151
+ break;
152
+ }
153
+ }
154
+ log('');
155
+ }
156
+
157
+ const anyFail = results.some((r) => r.status === 'fail');
158
+ if (anyFail) process.exit(1);
159
+ }
160
+
161
+ function installTargetForAll(targetId: IntegrationTargetId): void {
162
+ switch (targetId) {
163
+ case 'claude-code':
164
+ runTs(join(binDir, 'install.ts'), forwardedFlags);
165
+ return;
166
+ case 'codex':
167
+ runTs(join(clientDir, 'codex', 'install.ts'), forwardedFlags);
168
+ return;
169
+ case 'gemini-cli':
170
+ runTs(join(clientDir, 'gemini', 'install.ts'), forwardedFlags);
171
+ return;
172
+ case 'claude-desktop':
173
+ runTs(join(clientDir, 'desktop', 'install.ts'), forwardedFlags);
174
+ return;
175
+ case 'chatgpt-desktop':
176
+ runTs(join(clientDir, 'chatgpt', 'install.ts'), forwardedFlags);
177
+ return;
178
+ default:
179
+ throw new Error(`Cannot install remote target '${targetId}' via install-all`);
180
+ }
181
+ }
182
+
86
183
  function installTarget(targetId: IntegrationTargetId): void {
87
184
  switch (targetId) {
88
185
  case 'claude-code':
@@ -154,31 +251,22 @@ function upgrade(): void {
154
251
  renderRemoteGuidance();
155
252
  }
156
253
 
157
- function interactiveInstall(): void {
158
- renderDetections();
159
- log('');
160
- const targetIds = renderInstallMenu();
161
- const defaultChoice = summarizeDetectedLocalTargets()[0] || 'claude-code';
162
- const answer = prompt(`Install target(s) [${defaultChoice}]: `) || defaultChoice;
163
- const selections = resolveInteractiveSelection(
164
- answer,
165
- targetIds,
166
- summarizeDetectedLocalTargets(),
167
- (id) => findTarget(id) as { id: IntegrationTargetId; kind: 'local' | 'remote' } | undefined,
168
- );
169
- if (selections.length === 0) {
170
- throw new Error('No install targets selected');
171
- }
172
-
173
- for (const selection of selections) {
174
- installTarget(selection);
175
- }
176
-
177
- renderRemoteGuidance();
178
- }
179
-
180
254
  function main(): void {
181
- const [, , command = 'install', targetArg] = process.argv;
255
+ // Skip forwarded-only flags (--yes, --name <v>, --timezone <v>) when picking
256
+ // the command and target. Keep command-specific flags like --apply, --detect,
257
+ // --help, --version so existing subcommands continue to work.
258
+ const raw = process.argv.slice(2);
259
+ const FORWARDED_ONLY = new Set(['--yes', '-y']);
260
+ const FORWARDED_WITH_VALUE = new Set(['--name', '--timezone']);
261
+ const positionals: string[] = [];
262
+ for (let i = 0; i < raw.length; i++) {
263
+ const a = raw[i];
264
+ if (FORWARDED_ONLY.has(a)) continue;
265
+ if (FORWARDED_WITH_VALUE.has(a)) { i++; continue; }
266
+ positionals.push(a);
267
+ }
268
+ const command = positionals[0] ?? 'install';
269
+ const targetArg = positionals[1];
182
270
 
183
271
  switch (command) {
184
272
  case 'install':
@@ -186,22 +274,32 @@ function main(): void {
186
274
  renderDetections();
187
275
  return;
188
276
  }
189
- if (targetArg) {
190
- if (targetArg === 'all') {
191
- const targets = summarizeDetectedLocalTargets();
192
- if (targets.length === 0) {
193
- log('No detected local targets to install.');
194
- return;
195
- }
196
- for (const target of targets) installTarget(target);
197
- return;
277
+ if (!targetArg || targetArg === 'all') {
278
+ installAll();
279
+ return;
280
+ }
281
+ if (targetArg === 'help' || targetArg === '--help' || targetArg === '-h') {
282
+ log('gramatr install — install gramatr into detected AI platforms');
283
+ log('');
284
+ log('Usage:');
285
+ log(' npx gramatr install Detect every platform and install all');
286
+ log(' npx gramatr install all Same as above (explicit)');
287
+ log(' npx gramatr install <platform> Install a single platform');
288
+ log('');
289
+ log('Supported platforms:');
290
+ for (const c of INSTALL_ALL_CANDIDATES) {
291
+ log(` ${c.id.padEnd(16)}${c.label}`);
198
292
  }
293
+ return;
294
+ }
295
+ {
199
296
  const target = findTarget(targetArg);
200
- if (!target) throw new Error(`Unknown target: ${targetArg}`);
297
+ if (!target) {
298
+ log(`Unknown target: ${targetArg}. Run 'gramatr install help' for usage.`);
299
+ process.exit(1);
300
+ }
201
301
  installTarget(target.id);
202
- return;
203
302
  }
204
- interactiveInstall();
205
303
  return;
206
304
  case 'detect':
207
305
  renderDetections();
@@ -235,12 +333,7 @@ function main(): void {
235
333
  return;
236
334
  case '--version':
237
335
  case '-v':
238
- try {
239
- const { VERSION } = require('../core/version.ts');
240
- log(VERSION);
241
- } catch {
242
- log('0.3.0');
243
- }
336
+ log(VERSION);
244
337
  return;
245
338
  default:
246
339
  log(`Unknown command: ${command}. Run 'gramatr help' for usage.`);
package/bin/install.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  import { join, dirname, basename, resolve } from 'path';
18
18
  import { execSync, spawnSync } from 'child_process';
19
19
  import { createInterface } from 'readline';
20
- import { buildClaudeHooksFile, detectTsRunner } from '../core/install.ts';
20
+ import { buildClaudeHooksFile } from '../core/install.ts';
21
21
  import { VERSION } from '../core/version.ts';
22
22
 
23
23
  // ── Constants ──
@@ -141,7 +141,7 @@ function timestamp(): string {
141
141
 
142
142
  // ── Prerequisites ──
143
143
 
144
- async function checkPrereqs(): Promise<{ tsRunner: string }> {
144
+ async function checkPrereqs(): Promise<void> {
145
145
  // Node.js
146
146
  const nodeVer = process.versions.node;
147
147
  const major = parseInt(nodeVer.split('.')[0], 10);
@@ -151,18 +151,8 @@ async function checkPrereqs(): Promise<{ tsRunner: string }> {
151
151
  }
152
152
  log(`OK Node.js v${nodeVer}`);
153
153
 
154
- // Detect best TypeScript runner
155
- const tsRunner = detectTsRunner();
156
- if (tsRunner === 'bun') {
157
- try {
158
- const ver = execSync('bun --version', { encoding: 'utf8' }).trim();
159
- log(`OK TS runner: bun ${ver}`);
160
- } catch {
161
- log(`OK TS runner: bun`);
162
- }
163
- } else {
164
- log(`OK TS runner: npx tsx (install bun for faster hooks)`);
165
- }
154
+ // TS runner is hardcoded to `npx tsx` (see core/install.ts and issue #468)
155
+ log('OK TS runner: npx tsx');
166
156
 
167
157
  // Claude Code
168
158
  if (!existsSync(CLAUDE_DIR)) {
@@ -170,8 +160,6 @@ async function checkPrereqs(): Promise<{ tsRunner: string }> {
170
160
  process.exit(1);
171
161
  }
172
162
  log('OK Claude Code directory exists');
173
-
174
- return { tsRunner };
175
163
  }
176
164
 
177
165
  // ── Legacy Detection ──
@@ -287,6 +275,11 @@ function installClientFiles(): void {
287
275
  }
288
276
  log('OK Installed core modules');
289
277
 
278
+ // package.json — copied so core/version.ts can resolve the installed
279
+ // version at runtime. Single source of truth; see core/version.ts.
280
+ copyFileIfExists(join(SCRIPT_DIR, 'package.json'), join(CLIENT_DIR, 'package.json'));
281
+ log('OK Installed package.json (version source of truth)');
282
+
290
283
  // CLAUDE.md
291
284
  copyFileIfExists(join(SCRIPT_DIR, 'CLAUDE.md'), join(CLIENT_DIR, 'CLAUDE.md'));
292
285
  log('OK Installed CLAUDE.md (minimal — server delivers behavioral rules)');
@@ -351,7 +344,11 @@ function installClaudeMd(): void {
351
344
 
352
345
  // ── Step 3: Auth ──
353
346
 
354
- async function handleAuth(legacyToken: string, tsRunner: string): Promise<{ url: string; token: string }> {
347
+ // npx on Windows is shipped as `npx.cmd`. spawnSync without shell: true cannot
348
+ // resolve .cmd shims, so we fall back to the platform-specific binary name.
349
+ const NPX_BIN = process.platform === 'win32' ? 'npx.cmd' : 'npx';
350
+
351
+ async function handleAuth(legacyToken: string): Promise<{ url: string; token: string }> {
355
352
  log('━━━ Step 3: Configuring gramatr MCP server ━━━');
356
353
  log('');
357
354
 
@@ -387,10 +384,11 @@ async function handleAuth(legacyToken: string, tsRunner: string): Promise<{ url:
387
384
  if (existsSync(loginScript)) {
388
385
  // Run gmtr-login as subprocess but with proper stdio handling
389
386
  // Use spawnSync so stdin is properly passed through (no stall)
390
- const result = spawnSync(tsRunner, [loginScript], {
387
+ const result = spawnSync(NPX_BIN, ['tsx', loginScript], {
391
388
  stdio: 'inherit',
392
389
  env: { ...process.env },
393
390
  });
391
+ void result;
394
392
 
395
393
  // Re-read token after login
396
394
  if (existsSync(GMTR_JSON)) {
@@ -479,7 +477,7 @@ async function configureIdentity(): Promise<void> {
479
477
 
480
478
  // ── Step 4: Settings merge ──
481
479
 
482
- function updateClaudeSettings(tsRunner: string, url: string, token: string): void {
480
+ function updateClaudeSettings(url: string, token: string): void {
483
481
  log('━━━ Step 4: Updating Claude Code settings ━━━');
484
482
  log('');
485
483
 
@@ -493,7 +491,7 @@ function updateClaudeSettings(tsRunner: string, url: string, token: string): voi
493
491
  log(`OK Backed up settings to ${basename(backup)}`);
494
492
 
495
493
  const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
496
- const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx, tsRunner });
494
+ const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx });
497
495
 
498
496
  const settings = readJson(CLAUDE_SETTINGS);
499
497
 
@@ -529,7 +527,7 @@ function updateClaudeSettings(tsRunner: string, url: string, token: string): voi
529
527
  // Status line
530
528
  settings.statusLine = {
531
529
  type: 'command',
532
- command: `${tsRunner} ${CLIENT_DIR}/bin/statusline.ts`,
530
+ command: `npx tsx ${CLIENT_DIR}/bin/statusline.ts`,
533
531
  };
534
532
  log('OK Configured status line');
535
533
 
@@ -586,47 +584,6 @@ function registerMcpServer(url: string, token: string): void {
586
584
  log('');
587
585
  }
588
586
 
589
- // ── Step 4c: Additional CLIs ──
590
-
591
- async function installAdditionalClis(): Promise<void> {
592
- log('━━━ Step 4c: Additional CLI platforms ━━━');
593
- log('');
594
-
595
- // Codex
596
- const codexDir = join(HOME, '.codex');
597
- if (existsSync(codexDir) || which('codex')) {
598
- log(' Codex CLI detected — installing gramatr hooks...');
599
- try {
600
- const { main: installCodex } = await import('../codex/install.ts');
601
- installCodex();
602
- log(' OK Codex hooks installed');
603
- } catch (err: any) {
604
- log(` X Codex install failed (non-fatal): ${err.message}`);
605
- }
606
- log('');
607
- } else {
608
- log(' -- Codex CLI not detected (skipping)');
609
- }
610
-
611
- // Gemini
612
- const geminiDir = join(HOME, '.gemini');
613
- if (existsSync(geminiDir) || which('gemini')) {
614
- log(' Gemini CLI detected — installing gramatr extension...');
615
- try {
616
- const { main: installGemini } = await import('../gemini/install.ts');
617
- await installGemini();
618
- log(' OK Gemini extension installed');
619
- } catch (err: any) {
620
- log(` X Gemini install failed (non-fatal): ${err.message}`);
621
- }
622
- log('');
623
- } else {
624
- log(' -- Gemini CLI not detected (skipping)');
625
- }
626
-
627
- log('');
628
- }
629
-
630
587
  // ── Step 5: Verification ──
631
588
 
632
589
  function verify(url: string, token: string): boolean {
@@ -716,7 +673,7 @@ async function main(): Promise<void> {
716
673
  log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
717
674
  log('');
718
675
 
719
- const { tsRunner } = await checkPrereqs();
676
+ await checkPrereqs();
720
677
  log('');
721
678
 
722
679
  const { legacyToken } = await handleLegacy();
@@ -724,11 +681,10 @@ async function main(): Promise<void> {
724
681
  installClientFiles();
725
682
  installClaudeMd();
726
683
 
727
- const { url, token } = await handleAuth(legacyToken, tsRunner);
684
+ const { url, token } = await handleAuth(legacyToken);
728
685
  await configureIdentity();
729
- updateClaudeSettings(tsRunner, url, token);
686
+ updateClaudeSettings(url, token);
730
687
  registerMcpServer(url, token);
731
- await installAdditionalClis();
732
688
 
733
689
  const allOk = verify(url, token);
734
690
 
@@ -0,0 +1,95 @@
1
+ # gramatr - ChatGPT Desktop Integration
2
+
3
+ Tier 3 integration: MCP only (no hooks, no status line).
4
+
5
+ ## Prerequisites
6
+
7
+ - **ChatGPT Plus, Team, or Enterprise** subscription (free accounts do not support MCP)
8
+ - **ChatGPT Desktop** app installed ([download](https://openai.com/chatgpt/desktop))
9
+ - **Developer Mode** enabled in ChatGPT Desktop settings
10
+ - A **gramatr API key** — get one at [gramatr.com/settings](https://gramatr.com/settings)
11
+
12
+ ## Installation
13
+
14
+ ### Method 1: Installer script (recommended)
15
+
16
+ ```bash
17
+ cd packages/client
18
+ bun chatgpt/install.ts
19
+ ```
20
+
21
+ The installer will:
22
+ 1. Find your API key from `~/.gmtr.json`, `GRAMATR_API_KEY` env, or prompt you
23
+ 2. Validate connectivity to the gramatr server
24
+ 3. Detect your platform and locate the ChatGPT config file
25
+ 4. Merge the gramatr MCP server entry without overwriting existing servers
26
+ 5. Print verification instructions
27
+
28
+ ### Method 2: Environment variable
29
+
30
+ ```bash
31
+ GRAMATR_API_KEY=your-key-here bun chatgpt/install.ts
32
+ ```
33
+
34
+ ### Method 3: Manual configuration
35
+
36
+ Add to your ChatGPT MCP config file:
37
+
38
+ **macOS:** `~/.chatgpt/mcp.json`
39
+ **Windows:** `%APPDATA%\ChatGPT\mcp.json`
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "gramatr": {
45
+ "url": "https://mcp.gramatr.com/mcp",
46
+ "headers": {
47
+ "Authorization": "Bearer YOUR_API_KEY"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Verification
55
+
56
+ 1. Open ChatGPT Desktop
57
+ 2. Go to **Settings > Developer > MCP Servers**
58
+ 3. Confirm "gramatr" appears in the list with a green status indicator
59
+ 4. Start a new conversation and type: "What gramatr tools are available?"
60
+ 5. ChatGPT should list the available MCP tools from the gramatr server
61
+
62
+ ## Config file locations
63
+
64
+ | Platform | Path |
65
+ |----------|------|
66
+ | macOS | `~/.chatgpt/mcp.json` |
67
+ | Windows | `%APPDATA%\ChatGPT\mcp.json` |
68
+
69
+ ## Limitations
70
+
71
+ ChatGPT Desktop is a **Tier 3** integration:
72
+
73
+ - MCP tools are available (search, create entities, route requests, etc.)
74
+ - No PostToolUse hooks (no automatic metrics tracking)
75
+ - No status line
76
+ - No prompt enrichment hooks
77
+
78
+ For the full gramatr experience with hooks, status line, and prompt enrichment, use Claude Code or Codex.
79
+
80
+ ## Troubleshooting
81
+
82
+ **"gramatr" not showing in MCP servers:**
83
+ - Ensure Developer Mode is enabled in ChatGPT Desktop settings
84
+ - Check that the config file exists at the correct path
85
+ - Restart ChatGPT Desktop after editing the config
86
+
87
+ **Connection errors:**
88
+ - Verify your API key is valid: `bun bin/gmtr-login.ts --status`
89
+ - Check server health: `curl https://api.gramatr.com/health`
90
+ - Ensure you have an active internet connection
91
+
92
+ **Tools not appearing in conversation:**
93
+ - MCP tools may take a moment to load after connecting
94
+ - Try starting a new conversation
95
+ - Check the MCP server status indicator in Settings > Developer