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 +6 -13
- package/bin/gramatr.ts +165 -72
- package/bin/install.ts +22 -66
- package/chatgpt/README.md +95 -0
- package/chatgpt/install.ts +197 -0
- package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
- package/core/install.ts +12 -19
- package/core/version-check.ts +219 -0
- package/core/version.ts +47 -2
- package/desktop/README.md +72 -0
- package/desktop/build-mcpb.ts +167 -0
- package/desktop/install.ts +193 -0
- package/desktop/lib/desktop-install-utils.ts +70 -0
- package/gemini/lib/gemini-install-utils.ts +3 -3
- package/hooks/session-start.hook.ts +12 -1
- package/package.json +4 -6
- package/bin/version-sync.ts +0 -46
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
|
|
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
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 (
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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<
|
|
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
|
-
//
|
|
155
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
684
|
+
const { url, token } = await handleAuth(legacyToken);
|
|
728
685
|
await configureIdentity();
|
|
729
|
-
updateClaudeSettings(
|
|
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
|