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.
- package/chatgpt/README.md +95 -0
- package/chatgpt/install.ts +197 -0
- package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
- 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/package.json +1 -1
|
@@ -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.
|
|
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": {
|