gramatr 0.3.56 → 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.
@@ -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
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { homedir } from 'os';
6
+ import {
7
+ getChatGPTConfigPath,
8
+ mergeChatGPTConfig,
9
+ buildMcpServerEntry,
10
+ type ChatGPTConfig,
11
+ } from './lib/chatgpt-install-utils.ts';
12
+
13
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
14
+ const VALIDATION_ENDPOINT = 'https://api.gramatr.com/health';
15
+
16
+ function log(message: string): void {
17
+ process.stdout.write(`${message}\n`);
18
+ }
19
+
20
+ function readJsonFile<T>(path: string, fallback: T): T {
21
+ if (!existsSync(path)) return fallback;
22
+ try {
23
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
24
+ } catch {
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Resolve API key from available sources.
31
+ * Priority: CLI arg > GRAMATR_API_KEY env > ~/.gmtr.json token > gmtr-client/settings.json
32
+ */
33
+ function resolveApiKey(): string | null {
34
+ const home = homedir();
35
+
36
+ // 1. GRAMATR_API_KEY env var
37
+ if (process.env.GRAMATR_API_KEY) return process.env.GRAMATR_API_KEY;
38
+
39
+ // 2. GMTR_TOKEN env var (legacy compat)
40
+ if (process.env.GMTR_TOKEN) return process.env.GMTR_TOKEN;
41
+
42
+ // 3. ~/.gmtr.json
43
+ try {
44
+ const gmtrJsonPath = join(home, '.gmtr.json');
45
+ const gmtrJson = JSON.parse(readFileSync(gmtrJsonPath, 'utf8'));
46
+ if (gmtrJson.token) return gmtrJson.token;
47
+ } catch {
48
+ // Not found or parse error
49
+ }
50
+
51
+ // 4. ~/gmtr-client/settings.json
52
+ try {
53
+ const gmtrDir = process.env.GMTR_DIR || join(home, 'gmtr-client');
54
+ const settingsPath = join(gmtrDir, 'settings.json');
55
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
56
+ if (settings.auth?.api_key && settings.auth.api_key !== 'REPLACE_WITH_YOUR_API_KEY') {
57
+ return settings.auth.api_key;
58
+ }
59
+ } catch {
60
+ // Not found or parse error
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Validate token against gramatr server health endpoint.
68
+ * Returns true if server is reachable (we don't enforce auth for install — server validates on use).
69
+ */
70
+ async function validateServer(serverUrl: string): Promise<boolean> {
71
+ try {
72
+ const baseUrl = serverUrl.replace(/\/mcp$/, '');
73
+ const response = await fetch(`${baseUrl}/health`, {
74
+ method: 'GET',
75
+ signal: AbortSignal.timeout(5000),
76
+ });
77
+ return response.ok;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async function promptForInput(prompt: string): Promise<string> {
84
+ process.stdout.write(prompt);
85
+ const reader = process.stdin;
86
+ reader.resume();
87
+ return new Promise((resolve) => {
88
+ reader.once('data', (data) => {
89
+ reader.pause();
90
+ resolve(data.toString().trim());
91
+ });
92
+ });
93
+ }
94
+
95
+ async function main(): Promise<void> {
96
+ const home = homedir();
97
+ const platform = process.platform;
98
+
99
+ log('');
100
+ log('gramatr - ChatGPT Desktop installer');
101
+ log('====================================');
102
+ log('');
103
+
104
+ // Step 1: Resolve auth
105
+ log('Step 1: Resolving authentication...');
106
+ let apiKey = resolveApiKey();
107
+
108
+ if (!apiKey) {
109
+ log(' No API key found in ~/.gmtr.json, GRAMATR_API_KEY env, or gmtr-client/settings.json.');
110
+ apiKey = await promptForInput(' Enter your gramatr API key: ');
111
+ if (!apiKey) {
112
+ log('');
113
+ log('ERROR: API key is required. Get one at https://gramatr.com/settings');
114
+ log(' Or set GRAMATR_API_KEY environment variable before running this installer.');
115
+ process.exit(1);
116
+ }
117
+ } else {
118
+ log(' OK Found existing API key');
119
+ }
120
+
121
+ // Step 2: Validate server connectivity
122
+ log('');
123
+ log('Step 2: Validating server connectivity...');
124
+ const serverUrl = process.env.GMTR_URL || DEFAULT_MCP_URL;
125
+ const serverReachable = await validateServer(serverUrl);
126
+ if (serverReachable) {
127
+ log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
128
+ } else {
129
+ log(` WARN Server not reachable at ${serverUrl.replace(/\/mcp$/, '')} — config will be written anyway`);
130
+ }
131
+
132
+ // Step 3: Detect platform and config path
133
+ log('');
134
+ log('Step 3: Detecting ChatGPT Desktop...');
135
+ const configPath = getChatGPTConfigPath(home, platform);
136
+ const configDir = dirname(configPath);
137
+
138
+ if (platform === 'darwin') {
139
+ log(' Platform: macOS');
140
+ } else if (platform === 'win32') {
141
+ log(' Platform: Windows');
142
+ } else {
143
+ log(` Platform: ${platform} (ChatGPT Desktop may not be available)`);
144
+ }
145
+ log(` Config: ${configPath}`);
146
+
147
+ // Step 4: Read existing config
148
+ log('');
149
+ log('Step 4: Reading existing config...');
150
+ const existing = readJsonFile<ChatGPTConfig>(configPath, {});
151
+
152
+ const existingServerCount = existing.mcpServers ? Object.keys(existing.mcpServers).length : 0;
153
+ if (existingServerCount > 0) {
154
+ log(` Found ${existingServerCount} existing MCP server(s)`);
155
+ if (existing.mcpServers?.gramatr) {
156
+ log(' Existing gramatr entry will be updated');
157
+ }
158
+ } else {
159
+ log(' No existing config (will create new)');
160
+ }
161
+
162
+ // Step 5: Merge and write config
163
+ log('');
164
+ log('Step 5: Writing config...');
165
+ const gramatrEntry = buildMcpServerEntry(apiKey, serverUrl);
166
+ const merged = mergeChatGPTConfig(existing, gramatrEntry);
167
+
168
+ if (!existsSync(configDir)) {
169
+ mkdirSync(configDir, { recursive: true });
170
+ log(` Created directory: ${configDir}`);
171
+ }
172
+
173
+ writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
174
+ log(` OK Written to ${configPath}`);
175
+
176
+ // Summary
177
+ log('');
178
+ log('Installation complete.');
179
+ log('');
180
+ log('Next steps:');
181
+ log(' 1. Open ChatGPT Desktop');
182
+ log(' 2. Go to Settings > Developer > MCP Servers');
183
+ log(' 3. Verify "gramatr" appears in the server list');
184
+ log(' 4. Start a conversation and gramatr tools will be available');
185
+ log('');
186
+ log('What was configured:');
187
+ log(` MCP server: ${serverUrl}`);
188
+ log(` Config file: ${configPath}`);
189
+ log('');
190
+ log('Note: ChatGPT Desktop is Tier 3 (MCP only). Hooks and status line are not');
191
+ log('available — use Claude Code or Codex for the full gramatr experience.');
192
+ }
193
+
194
+ main().catch((err) => {
195
+ log(`ERROR: ${err.message}`);
196
+ process.exit(1);
197
+ });
@@ -0,0 +1,89 @@
1
+ import { join } from 'path';
2
+
3
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
4
+
5
+ export interface ChatGPTMcpServerEntry {
6
+ gramatr: {
7
+ url?: string;
8
+ headers?: {
9
+ Authorization: string;
10
+ };
11
+ command?: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ };
15
+ }
16
+
17
+ export interface ChatGPTConfig {
18
+ mcpServers?: Record<string, unknown>;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /**
23
+ * Returns the ChatGPT Desktop config file path for the given platform.
24
+ *
25
+ * macOS: ~/.chatgpt/mcp.json
26
+ * Windows: %APPDATA%\ChatGPT\mcp.json (APPDATA = <home>\AppData\Roaming)
27
+ */
28
+ export function getChatGPTConfigPath(
29
+ home: string,
30
+ platform: string = process.platform,
31
+ ): string {
32
+ if (platform === 'win32') {
33
+ return join(home, 'AppData', 'Roaming', 'ChatGPT', 'mcp.json');
34
+ }
35
+
36
+ // macOS (darwin) and fallback for other platforms
37
+ return join(home, '.chatgpt', 'mcp.json');
38
+ }
39
+
40
+ /**
41
+ * Build the mcpServers.gramatr entry for ChatGPT Desktop config.
42
+ *
43
+ * Two modes:
44
+ * - 'http' (default): StreamableHTTP transport — ChatGPT connects directly to the remote URL.
45
+ * - 'stdio': Local bridge process — ChatGPT spawns a local process that proxies to the server.
46
+ */
47
+ export function buildMcpServerEntry(
48
+ apiKey: string,
49
+ serverUrl: string = DEFAULT_MCP_URL,
50
+ mode: 'http' | 'stdio' = 'http',
51
+ ): ChatGPTMcpServerEntry {
52
+ if (mode === 'stdio') {
53
+ return {
54
+ gramatr: {
55
+ command: 'npx',
56
+ args: ['-y', '@anthropic-ai/mcp-proxy', serverUrl],
57
+ env: {
58
+ GRAMATR_API_KEY: apiKey,
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ return {
65
+ gramatr: {
66
+ url: serverUrl,
67
+ headers: {
68
+ Authorization: `Bearer ${apiKey}`,
69
+ },
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Safely merge a gramatr MCP server entry into an existing ChatGPT Desktop config.
76
+ * Preserves all other mcpServers and top-level config keys.
77
+ */
78
+ export function mergeChatGPTConfig(
79
+ existing: ChatGPTConfig,
80
+ gramatrEntry: ChatGPTMcpServerEntry,
81
+ ): ChatGPTConfig {
82
+ return {
83
+ ...existing,
84
+ mcpServers: {
85
+ ...(existing.mcpServers || {}),
86
+ ...gramatrEntry,
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,72 @@
1
+ # gramatr - Claude Desktop Integration
2
+
3
+ Claude Desktop is a **Tier 3** integration target: MCP only, no hooks.
4
+
5
+ gramatr connects to Claude Desktop via StreamableHTTP MCP transport. All intelligence (decision routing, memory, pattern learning) runs server-side.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Interactive installer — resolves auth, detects platform, merges config
11
+ bun packages/client/desktop/install.ts
12
+ ```
13
+
14
+ The installer:
15
+ 1. Resolves your API key from `~/.gmtr.json`, `GRAMATR_API_KEY` env, or prompts
16
+ 2. Validates connectivity to the gramatr server
17
+ 3. Detects platform (macOS or Windows)
18
+ 4. Reads existing `claude_desktop_config.json` without overwriting other MCP servers
19
+ 5. Writes the gramatr MCP server entry
20
+
21
+ ### Config Locations
22
+
23
+ | Platform | Path |
24
+ |----------|------|
25
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
26
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
27
+
28
+ ### Manual Setup
29
+
30
+ If you prefer to configure manually, add this to your `claude_desktop_config.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "gramatr": {
36
+ "url": "https://mcp.gramatr.com/mcp",
37
+ "headers": {
38
+ "Authorization": "Bearer YOUR_API_KEY"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## .mcpb Package
46
+
47
+ Build a `.mcpb` extension package for distribution:
48
+
49
+ ```bash
50
+ bun packages/client/desktop/build-mcpb.ts
51
+ # Output: dist/gramatr.mcpb/manifest.json
52
+ ```
53
+
54
+ The `.mcpb` format follows [Anthropic's manifest v0.3 spec](https://github.com/anthropics/mcpb/blob/main/MANIFEST.md).
55
+
56
+ ## What Works
57
+
58
+ Claude Desktop gets full access to gramatr's MCP tools:
59
+ - Semantic search, entity management, knowledge graph
60
+ - Decision routing, skill matching, pattern learning
61
+ - Session state, handoffs, reflections
62
+
63
+ ## What Does Not Work
64
+
65
+ Claude Desktop has no hook system, so these features are unavailable:
66
+ - PreToolUse / PostToolUse hooks (security validation, algorithm tracking)
67
+ - Session lifecycle hooks (auto session-start, session-end)
68
+ - Prompt enrichment (intelligence packet injection)
69
+ - Rating capture
70
+ - Status line
71
+
72
+ For the full gramatr experience, use Claude Code or Codex.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Build a .mcpb package for Claude Desktop.
5
+ *
6
+ * The .mcpb format (manifest v0.3) is Anthropic's extension packaging format
7
+ * for Claude Desktop. gramatr is a remote MCP server, so the package contains
8
+ * only the manifest — no local server binary.
9
+ *
10
+ * Reference: https://github.com/anthropics/mcpb/blob/main/MANIFEST.md
11
+ *
12
+ * Usage: bun desktop/build-mcpb.ts [--out <path>]
13
+ */
14
+
15
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
16
+ import { join, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const currentFile = fileURLToPath(import.meta.url);
20
+ const desktopDir = dirname(currentFile);
21
+ const clientDir = dirname(desktopDir);
22
+ const packagesDir = dirname(clientDir);
23
+ const repoRoot = dirname(packagesDir);
24
+
25
+ interface McpbManifest {
26
+ manifest_version: string;
27
+ name: string;
28
+ display_name: string;
29
+ version: string;
30
+ description: string;
31
+ long_description: string;
32
+ author: {
33
+ name: string;
34
+ url: string;
35
+ };
36
+ homepage: string;
37
+ documentation: string;
38
+ repository: {
39
+ type: string;
40
+ url: string;
41
+ };
42
+ license: string;
43
+ keywords: string[];
44
+ server: {
45
+ type: string;
46
+ transport: string;
47
+ url: string;
48
+ };
49
+ user_config: Record<string, {
50
+ type: string;
51
+ title: string;
52
+ description: string;
53
+ sensitive?: boolean;
54
+ required: boolean;
55
+ }>;
56
+ compatibility: {
57
+ claude_desktop: string;
58
+ platforms: string[];
59
+ };
60
+ privacy_policies: string[];
61
+ tools_generated: boolean;
62
+ prompts_generated: boolean;
63
+ }
64
+
65
+ function readPackageVersion(): string {
66
+ try {
67
+ const pkgPath = join(clientDir, 'package.json');
68
+ const { readFileSync } = require('fs');
69
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
70
+ return pkg.version || '0.0.0';
71
+ } catch {
72
+ return '0.0.0';
73
+ }
74
+ }
75
+
76
+ function buildManifest(version: string): McpbManifest {
77
+ return {
78
+ manifest_version: '0.3',
79
+ name: 'gramatr',
80
+ display_name: 'gramatr',
81
+ version,
82
+ description: 'Intelligent AI middleware — decision routing, vector memory, and pattern learning for Claude Desktop.',
83
+ long_description: [
84
+ 'gramatr is an intelligent middleware layer that makes Claude smarter and cheaper.',
85
+ 'It pre-classifies every request using a lightweight model before expensive models burn tokens on routing overhead.',
86
+ '',
87
+ '**What you get:**',
88
+ '- Decision routing — effort level, intent, and skill classification',
89
+ '- Vector memory — persistent knowledge graph with semantic search',
90
+ '- Pattern learning — usage patterns that improve routing over time',
91
+ '- Token savings — ~2,700 tokens saved per request at scale',
92
+ '',
93
+ 'Claude Desktop connects to the gramatr server via StreamableHTTP MCP transport.',
94
+ 'All intelligence runs server-side — no local compute required.',
95
+ ].join('\n'),
96
+ author: {
97
+ name: 'gramatr',
98
+ url: 'https://gramatr.com',
99
+ },
100
+ homepage: 'https://gramatr.com',
101
+ documentation: 'https://docs.gramatr.com',
102
+ repository: {
103
+ type: 'git',
104
+ url: 'https://github.com/gramatr/gramatr',
105
+ },
106
+ license: 'MIT',
107
+ keywords: [
108
+ 'ai',
109
+ 'memory',
110
+ 'routing',
111
+ 'intelligence',
112
+ 'mcp',
113
+ 'knowledge-graph',
114
+ 'vector-search',
115
+ 'decision-routing',
116
+ ],
117
+ server: {
118
+ type: 'remote',
119
+ transport: 'streamable-http',
120
+ url: 'https://mcp.gramatr.com/mcp',
121
+ },
122
+ user_config: {
123
+ api_key: {
124
+ type: 'string',
125
+ title: 'API Key',
126
+ description: 'Your gramatr API key. Get one at https://gramatr.com/settings',
127
+ sensitive: true,
128
+ required: true,
129
+ },
130
+ },
131
+ compatibility: {
132
+ claude_desktop: '>=1.0.0',
133
+ platforms: ['darwin', 'win32'],
134
+ },
135
+ privacy_policies: ['https://gramatr.com/privacy'],
136
+ tools_generated: true,
137
+ prompts_generated: true,
138
+ };
139
+ }
140
+
141
+ function main(): void {
142
+ const args = process.argv.slice(2);
143
+ const outIndex = args.indexOf('--out');
144
+ const outDir = outIndex >= 0 && args[outIndex + 1]
145
+ ? args[outIndex + 1]
146
+ : join(repoRoot, 'dist');
147
+
148
+ const version = readPackageVersion();
149
+ const manifest = buildManifest(version);
150
+
151
+ // Create output directory
152
+ const mcpbDir = join(outDir, 'gramatr.mcpb');
153
+ if (!existsSync(mcpbDir)) {
154
+ mkdirSync(mcpbDir, { recursive: true });
155
+ }
156
+
157
+ // Write manifest.json
158
+ const manifestPath = join(mcpbDir, 'manifest.json');
159
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
160
+
161
+ process.stdout.write(`OK Built .mcpb package\n`);
162
+ process.stdout.write(` Manifest: ${manifestPath}\n`);
163
+ process.stdout.write(` Version: ${version}\n`);
164
+ process.stdout.write(` Server: ${manifest.server.url}\n`);
165
+ }
166
+
167
+ main();
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { homedir } from 'os';
6
+ import {
7
+ getDesktopConfigPath,
8
+ mergeDesktopConfig,
9
+ buildMcpServerEntry,
10
+ type DesktopConfig,
11
+ } from './lib/desktop-install-utils.ts';
12
+
13
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
14
+ const VALIDATION_ENDPOINT = 'https://api.gramatr.com/health';
15
+
16
+ function log(message: string): void {
17
+ process.stdout.write(`${message}\n`);
18
+ }
19
+
20
+ function readJsonFile<T>(path: string, fallback: T): T {
21
+ if (!existsSync(path)) return fallback;
22
+ try {
23
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
24
+ } catch {
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Resolve API key from available sources.
31
+ * Priority: CLI arg > GRAMATR_API_KEY env > ~/.gmtr.json token > gmtr-client/settings.json
32
+ */
33
+ function resolveApiKey(): string | null {
34
+ const home = homedir();
35
+
36
+ // 1. GRAMATR_API_KEY env var
37
+ if (process.env.GRAMATR_API_KEY) return process.env.GRAMATR_API_KEY;
38
+
39
+ // 2. GMTR_TOKEN env var (legacy compat)
40
+ if (process.env.GMTR_TOKEN) return process.env.GMTR_TOKEN;
41
+
42
+ // 3. ~/.gmtr.json
43
+ try {
44
+ const gmtrJsonPath = join(home, '.gmtr.json');
45
+ const gmtrJson = JSON.parse(readFileSync(gmtrJsonPath, 'utf8'));
46
+ if (gmtrJson.token) return gmtrJson.token;
47
+ } catch {
48
+ // Not found or parse error
49
+ }
50
+
51
+ // 4. ~/gmtr-client/settings.json
52
+ try {
53
+ const gmtrDir = process.env.GMTR_DIR || join(home, 'gmtr-client');
54
+ const settingsPath = join(gmtrDir, 'settings.json');
55
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
56
+ if (settings.auth?.api_key && settings.auth.api_key !== 'REPLACE_WITH_YOUR_API_KEY') {
57
+ return settings.auth.api_key;
58
+ }
59
+ } catch {
60
+ // Not found or parse error
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Validate token against gramatr server health endpoint.
68
+ * Returns true if server is reachable (we don't enforce auth for install — server validates on use).
69
+ */
70
+ async function validateServer(serverUrl: string): Promise<boolean> {
71
+ try {
72
+ const baseUrl = serverUrl.replace(/\/mcp$/, '');
73
+ const response = await fetch(`${baseUrl}/health`, {
74
+ method: 'GET',
75
+ signal: AbortSignal.timeout(5000),
76
+ });
77
+ return response.ok;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ async function promptForInput(prompt: string): Promise<string> {
84
+ process.stdout.write(prompt);
85
+ const reader = process.stdin;
86
+ reader.resume();
87
+ return new Promise((resolve) => {
88
+ reader.once('data', (data) => {
89
+ reader.pause();
90
+ resolve(data.toString().trim());
91
+ });
92
+ });
93
+ }
94
+
95
+ async function main(): Promise<void> {
96
+ const home = homedir();
97
+ const platform = process.platform;
98
+
99
+ log('');
100
+ log('gramatr - Claude Desktop installer');
101
+ log('===================================');
102
+ log('');
103
+
104
+ // Step 1: Resolve auth
105
+ log('Step 1: Resolving authentication...');
106
+ let apiKey = resolveApiKey();
107
+
108
+ if (!apiKey) {
109
+ log(' No API key found in ~/.gmtr.json, GRAMATR_API_KEY env, or gmtr-client/settings.json.');
110
+ apiKey = await promptForInput(' Enter your gramatr API key: ');
111
+ if (!apiKey) {
112
+ log('');
113
+ log('ERROR: API key is required. Get one at https://gramatr.com/settings');
114
+ log(' Or set GRAMATR_API_KEY environment variable before running this installer.');
115
+ process.exit(1);
116
+ }
117
+ } else {
118
+ log(' OK Found existing API key');
119
+ }
120
+
121
+ // Step 2: Validate server connectivity
122
+ log('');
123
+ log('Step 2: Validating server connectivity...');
124
+ const serverUrl = process.env.GMTR_URL || DEFAULT_MCP_URL;
125
+ const serverReachable = await validateServer(serverUrl);
126
+ if (serverReachable) {
127
+ log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
128
+ } else {
129
+ log(` WARN Server not reachable at ${serverUrl.replace(/\/mcp$/, '')} — config will be written anyway`);
130
+ }
131
+
132
+ // Step 3: Detect platform and config path
133
+ log('');
134
+ log('Step 3: Detecting Claude Desktop...');
135
+ const configPath = getDesktopConfigPath(home, platform);
136
+ const configDir = dirname(configPath);
137
+
138
+ if (platform === 'darwin') {
139
+ log(' Platform: macOS');
140
+ } else if (platform === 'win32') {
141
+ log(' Platform: Windows');
142
+ } else {
143
+ log(` Platform: ${platform} (Claude Desktop may not be available)`);
144
+ }
145
+ log(` Config: ${configPath}`);
146
+
147
+ // Step 4: Read existing config
148
+ log('');
149
+ log('Step 4: Reading existing config...');
150
+ const existing = readJsonFile<DesktopConfig>(configPath, {});
151
+
152
+ const existingServerCount = existing.mcpServers ? Object.keys(existing.mcpServers).length : 0;
153
+ if (existingServerCount > 0) {
154
+ log(` Found ${existingServerCount} existing MCP server(s)`);
155
+ if (existing.mcpServers?.gramatr) {
156
+ log(' Existing gramatr entry will be updated');
157
+ }
158
+ } else {
159
+ log(' No existing config (will create new)');
160
+ }
161
+
162
+ // Step 5: Merge and write config
163
+ log('');
164
+ log('Step 5: Writing config...');
165
+ const gramatrEntry = buildMcpServerEntry(apiKey, serverUrl);
166
+ const merged = mergeDesktopConfig(existing, gramatrEntry);
167
+
168
+ if (!existsSync(configDir)) {
169
+ mkdirSync(configDir, { recursive: true });
170
+ log(` Created directory: ${configDir}`);
171
+ }
172
+
173
+ writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
174
+ log(` OK Written to ${configPath}`);
175
+
176
+ // Summary
177
+ log('');
178
+ log('Installation complete.');
179
+ log('');
180
+ log('Restart Claude Desktop to connect to gramatr.');
181
+ log('');
182
+ log('What was configured:');
183
+ log(` MCP server: ${serverUrl}`);
184
+ log(` Config file: ${configPath}`);
185
+ log('');
186
+ log('Note: Claude Desktop is Tier 3 (MCP only). Hooks and status line are not');
187
+ log('available — use Claude Code or Codex for the full gramatr experience.');
188
+ }
189
+
190
+ main().catch((err) => {
191
+ log(`ERROR: ${err.message}`);
192
+ process.exit(1);
193
+ });
@@ -0,0 +1,70 @@
1
+ import { join } from 'path';
2
+
3
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
4
+
5
+ export interface DesktopMcpServerEntry {
6
+ gramatr: {
7
+ url: string;
8
+ headers: {
9
+ Authorization: string;
10
+ };
11
+ };
12
+ }
13
+
14
+ export interface DesktopConfig {
15
+ mcpServers?: Record<string, unknown>;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ /**
20
+ * Returns the Claude Desktop config file path for the given platform.
21
+ *
22
+ * macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
23
+ * Windows: %APPDATA%\Claude\claude_desktop_config.json (APPDATA = <home>\AppData\Roaming)
24
+ */
25
+ export function getDesktopConfigPath(
26
+ home: string,
27
+ platform: string = process.platform,
28
+ ): string {
29
+ if (platform === 'win32') {
30
+ return join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
31
+ }
32
+
33
+ // macOS (darwin) and fallback for other platforms
34
+ return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
35
+ }
36
+
37
+ /**
38
+ * Build the mcpServers.gramatr entry for Claude Desktop config.
39
+ * Uses StreamableHTTP transport — Claude Desktop connects directly to the remote URL.
40
+ */
41
+ export function buildMcpServerEntry(
42
+ apiKey: string,
43
+ serverUrl: string = DEFAULT_MCP_URL,
44
+ ): DesktopMcpServerEntry {
45
+ return {
46
+ gramatr: {
47
+ url: serverUrl,
48
+ headers: {
49
+ Authorization: `Bearer ${apiKey}`,
50
+ },
51
+ },
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Safely merge a gramatr MCP server entry into an existing Claude Desktop config.
57
+ * Preserves all other mcpServers and top-level config keys.
58
+ */
59
+ export function mergeDesktopConfig(
60
+ existing: DesktopConfig,
61
+ gramatrEntry: DesktopMcpServerEntry,
62
+ ): DesktopConfig {
63
+ return {
64
+ ...existing,
65
+ mcpServers: {
66
+ ...(existing.mcpServers || {}),
67
+ ...gramatrEntry,
68
+ },
69
+ };
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gramatr",
3
- "version": "0.3.56",
3
+ "version": "0.3.57",
4
4
  "description": "grāmatr — context engineering layer for AI coding agents. Every prompt gets a pre-computed intelligence packet: decision routing, capability audit, behavioral directives, memory pre-load, and ISC scaffolds. Continuity across sessions for Claude Code, Codex, and Gemini CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {