@gramatr/client 0.5.1
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/AGENTS.md +17 -0
- package/CLAUDE.md +18 -0
- package/README.md +108 -0
- package/bin/add-api-key.ts +264 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/clear-creds.ts +141 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +599 -0
- package/bin/gramatr.js +36 -0
- package/bin/gramatr.ts +374 -0
- package/bin/install.ts +716 -0
- package/bin/lib/config.ts +57 -0
- package/bin/lib/git.ts +111 -0
- package/bin/lib/stdin.ts +53 -0
- package/bin/logout.ts +76 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +81 -0
- package/bin/uninstall.ts +289 -0
- package/chatgpt/README.md +95 -0
- package/chatgpt/install.ts +140 -0
- package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
- package/codex/README.md +28 -0
- package/codex/hooks/session-start.ts +73 -0
- package/codex/hooks/stop.ts +34 -0
- package/codex/hooks/user-prompt-submit.ts +79 -0
- package/codex/install.ts +116 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/auth.ts +170 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +179 -0
- package/core/install.ts +107 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +479 -0
- package/core/routing.ts +108 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +179 -0
- package/core/version-check.ts +219 -0
- package/core/version.ts +47 -0
- package/desktop/README.md +72 -0
- package/desktop/build-mcpb.ts +166 -0
- package/desktop/install.ts +136 -0
- package/desktop/lib/desktop-install-utils.ts +70 -0
- package/gemini/README.md +95 -0
- package/gemini/hooks/session-start.ts +72 -0
- package/gemini/hooks/stop.ts +30 -0
- package/gemini/hooks/user-prompt-submit.ts +77 -0
- package/gemini/install.ts +281 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +651 -0
- package/hooks/GMTRRatingCapture.hook.ts +198 -0
- package/hooks/GMTRSecurityValidator.hook.ts +399 -0
- package/hooks/GMTRToolTracker.hook.ts +181 -0
- package/hooks/StopOrchestrator.hook.ts +78 -0
- package/hooks/gmtr-tool-tracker-utils.ts +105 -0
- package/hooks/lib/gmtr-hook-utils.ts +770 -0
- package/hooks/lib/identity.ts +227 -0
- package/hooks/lib/notify.ts +46 -0
- package/hooks/lib/paths.ts +104 -0
- package/hooks/lib/transcript-parser.ts +452 -0
- package/hooks/session-end.hook.ts +168 -0
- package/hooks/session-start.hook.ts +501 -0
- package/package.json +63 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version-check.ts — opportunistic npm registry version check.
|
|
3
|
+
*
|
|
4
|
+
* Queries https://registry.npmjs.org/gramatr/latest on a 3s timeout, caches
|
|
5
|
+
* the result for one hour under ~/.gmtr-client/.cache/version-check.json, and
|
|
6
|
+
* reports whether the installed client is behind the published version.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints (see issue #468 sibling work):
|
|
9
|
+
* - Never throws. Any failure returns null and the caller proceeds normally.
|
|
10
|
+
* - Never writes to stdout — stdout is Claude Code's context channel.
|
|
11
|
+
* - Fast cache-hit path (no network, no heavy work).
|
|
12
|
+
* - Zero new runtime dependencies. Uses global fetch (Node 18+).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
|
|
19
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/gramatr/latest';
|
|
20
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
21
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
22
|
+
|
|
23
|
+
export interface VersionCheckResult {
|
|
24
|
+
latestVersion: string;
|
|
25
|
+
installedVersion: string;
|
|
26
|
+
isOutdated: boolean;
|
|
27
|
+
cached: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CacheFile {
|
|
31
|
+
latestVersion: string;
|
|
32
|
+
fetchedAt: number;
|
|
33
|
+
lastNotifiedVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare two semver-style version strings ("X.Y.Z").
|
|
38
|
+
* Returns:
|
|
39
|
+
* -1 if a < b
|
|
40
|
+
* 0 if a === b
|
|
41
|
+
* 1 if a > b
|
|
42
|
+
*
|
|
43
|
+
* Non-numeric or missing segments are treated as 0.
|
|
44
|
+
*/
|
|
45
|
+
export function compareVersions(a: string, b: string): number {
|
|
46
|
+
const pa = a.split('.').map((x) => parseInt(x, 10) || 0);
|
|
47
|
+
const pb = b.split('.').map((x) => parseInt(x, 10) || 0);
|
|
48
|
+
const len = Math.max(pa.length, pb.length);
|
|
49
|
+
for (let i = 0; i < len; i++) {
|
|
50
|
+
const av = pa[i] ?? 0;
|
|
51
|
+
const bv = pb[i] ?? 0;
|
|
52
|
+
if (av < bv) return -1;
|
|
53
|
+
if (av > bv) return 1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getCachePath(home: string = homedir()): string {
|
|
59
|
+
return join(home, '.gmtr-client', '.cache', 'version-check.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readCache(path: string): CacheFile | null {
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(path)) return null;
|
|
65
|
+
const raw = readFileSync(path, 'utf8');
|
|
66
|
+
const parsed = JSON.parse(raw) as CacheFile;
|
|
67
|
+
if (typeof parsed.latestVersion !== 'string' || typeof parsed.fetchedAt !== 'number') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeCache(path: string, data: CacheFile): void {
|
|
77
|
+
try {
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
80
|
+
} catch {
|
|
81
|
+
// Cache is best-effort. Silent failure is acceptable.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(REGISTRY_URL, {
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
headers: { Accept: 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) return null;
|
|
95
|
+
const body = (await res.json()) as { version?: string };
|
|
96
|
+
if (typeof body?.version !== 'string') return null;
|
|
97
|
+
return body.version;
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check the installed version against the latest published on npm.
|
|
108
|
+
* Returns null on any failure — callers must treat this as optional.
|
|
109
|
+
*/
|
|
110
|
+
export async function checkLatestVersion(
|
|
111
|
+
installedVersion: string,
|
|
112
|
+
options: { cachePath?: string; now?: number } = {},
|
|
113
|
+
): Promise<VersionCheckResult | null> {
|
|
114
|
+
try {
|
|
115
|
+
const cachePath = options.cachePath ?? getCachePath();
|
|
116
|
+
const now = options.now ?? Date.now();
|
|
117
|
+
|
|
118
|
+
const cached = readCache(cachePath);
|
|
119
|
+
if (cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
120
|
+
return {
|
|
121
|
+
latestVersion: cached.latestVersion,
|
|
122
|
+
installedVersion,
|
|
123
|
+
isOutdated: compareVersions(cached.latestVersion, installedVersion) > 0,
|
|
124
|
+
cached: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const latestVersion = await fetchLatestVersion();
|
|
129
|
+
if (!latestVersion) return null;
|
|
130
|
+
|
|
131
|
+
writeCache(cachePath, {
|
|
132
|
+
latestVersion,
|
|
133
|
+
fetchedAt: now,
|
|
134
|
+
lastNotifiedVersion: cached?.lastNotifiedVersion,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
latestVersion,
|
|
139
|
+
installedVersion,
|
|
140
|
+
isOutdated: compareVersions(latestVersion, installedVersion) > 0,
|
|
141
|
+
cached: false,
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Record that the user has been notified for a given latest version.
|
|
150
|
+
* Suppresses repeat notifications until a newer version is published.
|
|
151
|
+
*/
|
|
152
|
+
export function markNotified(latestVersion: string, cachePath: string = getCachePath()): void {
|
|
153
|
+
try {
|
|
154
|
+
const current = readCache(cachePath);
|
|
155
|
+
if (!current) return;
|
|
156
|
+
writeCache(cachePath, { ...current, lastNotifiedVersion: latestVersion });
|
|
157
|
+
} catch {
|
|
158
|
+
// Best-effort.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function shouldNotify(
|
|
163
|
+
result: VersionCheckResult,
|
|
164
|
+
cachePath: string = getCachePath(),
|
|
165
|
+
): boolean {
|
|
166
|
+
if (!result.isOutdated) return false;
|
|
167
|
+
const cached = readCache(cachePath);
|
|
168
|
+
if (cached?.lastNotifiedVersion === result.latestVersion) return false;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format the upgrade notification banner. Caller decides where to write it
|
|
174
|
+
* (must be stderr — stdout is reserved for Claude context).
|
|
175
|
+
*/
|
|
176
|
+
export function formatUpgradeNotification(installed: string, latest: string): string {
|
|
177
|
+
const bar = '\u2501'.repeat(60);
|
|
178
|
+
return [
|
|
179
|
+
bar,
|
|
180
|
+
' gramatr update available',
|
|
181
|
+
'',
|
|
182
|
+
` Installed: ${installed}`,
|
|
183
|
+
` Latest: ${latest}`,
|
|
184
|
+
'',
|
|
185
|
+
' To upgrade:',
|
|
186
|
+
' 1. Type /exit to leave Claude Code',
|
|
187
|
+
' 2. Run: npx gramatr@latest install claude-code',
|
|
188
|
+
' 3. Restart: claude --resume',
|
|
189
|
+
'',
|
|
190
|
+
" Why restart? gramatr's hooks are loaded by Claude Code at",
|
|
191
|
+
' session start. New hook code requires a fresh session.',
|
|
192
|
+
bar,
|
|
193
|
+
'',
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One-shot helper for hooks: check and, if appropriate, print the
|
|
199
|
+
* notification to stderr. Safe to call from any hook — never throws,
|
|
200
|
+
* never blocks longer than FETCH_TIMEOUT_MS in the cache-miss path.
|
|
201
|
+
*/
|
|
202
|
+
export async function runVersionCheckAndNotify(
|
|
203
|
+
installedVersion: string,
|
|
204
|
+
options: { cachePath?: string; stream?: NodeJS.WritableStream } = {},
|
|
205
|
+
): Promise<VersionCheckResult | null> {
|
|
206
|
+
const stream = options.stream ?? process.stderr;
|
|
207
|
+
const cachePath = options.cachePath ?? getCachePath();
|
|
208
|
+
const result = await checkLatestVersion(installedVersion, { cachePath });
|
|
209
|
+
if (!result) return null;
|
|
210
|
+
if (shouldNotify(result, cachePath)) {
|
|
211
|
+
try {
|
|
212
|
+
stream.write(formatUpgradeNotification(result.installedVersion, result.latestVersion));
|
|
213
|
+
} catch {
|
|
214
|
+
// Silent — never break the hook.
|
|
215
|
+
}
|
|
216
|
+
markNotified(result.latestVersion, cachePath);
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
package/core/version.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version.ts — runtime resolution of the installed gramatr version.
|
|
3
|
+
*
|
|
4
|
+
* Reads `version` from the nearest package.json walking up from this module's
|
|
5
|
+
* location. package.json is the SINGLE source of truth — it's the file the
|
|
6
|
+
* version-bump process already updates, so there is zero possibility of drift.
|
|
7
|
+
*
|
|
8
|
+
* Works in two environments:
|
|
9
|
+
* 1. Source checkout: packages/client/core/version.ts →
|
|
10
|
+
* packages/client/package.json (found one directory up).
|
|
11
|
+
* 2. Installed client: ~/gmtr-client/core/version.ts →
|
|
12
|
+
* ~/gmtr-client/package.json (copied by installClientFiles()).
|
|
13
|
+
*
|
|
14
|
+
* If the file cannot be resolved (unexpected layout), falls back to '0.0.0'
|
|
15
|
+
* rather than throwing — the version check is opportunistic and must never
|
|
16
|
+
* break the hook.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { dirname, join } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
function findPackageJson(startDir: string): string | null {
|
|
24
|
+
let dir = startDir;
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
const candidate = join(dir, 'package.json');
|
|
27
|
+
if (existsSync(candidate)) return candidate;
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveVersion(): string {
|
|
36
|
+
try {
|
|
37
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const pkgPath = findPackageJson(here);
|
|
39
|
+
if (!pkgPath) return '0.0.0';
|
|
40
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string };
|
|
41
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
42
|
+
} catch {
|
|
43
|
+
return '0.0.0';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const VERSION: string = resolveVersion();
|
|
@@ -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,166 @@
|
|
|
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, readFileSync } 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 pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
69
|
+
return pkg.version || '0.0.0';
|
|
70
|
+
} catch {
|
|
71
|
+
return '0.0.0';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildManifest(version: string): McpbManifest {
|
|
76
|
+
return {
|
|
77
|
+
manifest_version: '0.3',
|
|
78
|
+
name: 'gramatr',
|
|
79
|
+
display_name: 'gramatr',
|
|
80
|
+
version,
|
|
81
|
+
description: 'Intelligent AI middleware — decision routing, vector memory, and pattern learning for Claude Desktop.',
|
|
82
|
+
long_description: [
|
|
83
|
+
'gramatr is an intelligent middleware layer that makes Claude smarter and cheaper.',
|
|
84
|
+
'It pre-classifies every request using a lightweight model before expensive models burn tokens on routing overhead.',
|
|
85
|
+
'',
|
|
86
|
+
'**What you get:**',
|
|
87
|
+
'- Decision routing — effort level, intent, and skill classification',
|
|
88
|
+
'- Vector memory — persistent knowledge graph with semantic search',
|
|
89
|
+
'- Pattern learning — usage patterns that improve routing over time',
|
|
90
|
+
'- Token savings — ~2,700 tokens saved per request at scale',
|
|
91
|
+
'',
|
|
92
|
+
'Claude Desktop connects to the gramatr server via StreamableHTTP MCP transport.',
|
|
93
|
+
'All intelligence runs server-side — no local compute required.',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
author: {
|
|
96
|
+
name: 'gramatr',
|
|
97
|
+
url: 'https://gramatr.com',
|
|
98
|
+
},
|
|
99
|
+
homepage: 'https://gramatr.com',
|
|
100
|
+
documentation: 'https://docs.gramatr.com',
|
|
101
|
+
repository: {
|
|
102
|
+
type: 'git',
|
|
103
|
+
url: 'https://github.com/gramatr/gramatr',
|
|
104
|
+
},
|
|
105
|
+
license: 'MIT',
|
|
106
|
+
keywords: [
|
|
107
|
+
'ai',
|
|
108
|
+
'memory',
|
|
109
|
+
'routing',
|
|
110
|
+
'intelligence',
|
|
111
|
+
'mcp',
|
|
112
|
+
'knowledge-graph',
|
|
113
|
+
'vector-search',
|
|
114
|
+
'decision-routing',
|
|
115
|
+
],
|
|
116
|
+
server: {
|
|
117
|
+
type: 'remote',
|
|
118
|
+
transport: 'streamable-http',
|
|
119
|
+
url: 'https://mcp.gramatr.com/mcp',
|
|
120
|
+
},
|
|
121
|
+
user_config: {
|
|
122
|
+
api_key: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
title: 'API Key',
|
|
125
|
+
description: 'Your gramatr API key. Get one at https://gramatr.com/settings',
|
|
126
|
+
sensitive: true,
|
|
127
|
+
required: true,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
compatibility: {
|
|
131
|
+
claude_desktop: '>=1.0.0',
|
|
132
|
+
platforms: ['darwin', 'win32'],
|
|
133
|
+
},
|
|
134
|
+
privacy_policies: ['https://gramatr.com/privacy'],
|
|
135
|
+
tools_generated: true,
|
|
136
|
+
prompts_generated: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main(): void {
|
|
141
|
+
const args = process.argv.slice(2);
|
|
142
|
+
const outIndex = args.indexOf('--out');
|
|
143
|
+
const outDir = outIndex >= 0 && args[outIndex + 1]
|
|
144
|
+
? args[outIndex + 1]
|
|
145
|
+
: join(repoRoot, 'dist');
|
|
146
|
+
|
|
147
|
+
const version = readPackageVersion();
|
|
148
|
+
const manifest = buildManifest(version);
|
|
149
|
+
|
|
150
|
+
// Create output directory
|
|
151
|
+
const mcpbDir = join(outDir, 'gramatr.mcpb');
|
|
152
|
+
if (!existsSync(mcpbDir)) {
|
|
153
|
+
mkdirSync(mcpbDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Write manifest.json
|
|
157
|
+
const manifestPath = join(mcpbDir, 'manifest.json');
|
|
158
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
159
|
+
|
|
160
|
+
process.stdout.write(`OK Built .mcpb package\n`);
|
|
161
|
+
process.stdout.write(` Manifest: ${manifestPath}\n`);
|
|
162
|
+
process.stdout.write(` Version: ${version}\n`);
|
|
163
|
+
process.stdout.write(` Server: ${manifest.server.url}\n`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname } 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
|
+
import { resolveAuthToken } from '../core/auth.ts';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
|
|
15
|
+
const VALIDATION_ENDPOINT = 'https://api.gramatr.com/health';
|
|
16
|
+
|
|
17
|
+
function log(message: string): void {
|
|
18
|
+
process.stdout.write(`${message}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJsonFile<T>(path: string, fallback: T): T {
|
|
22
|
+
if (!existsSync(path)) return fallback;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate token against gramatr server health endpoint.
|
|
32
|
+
* Returns true if server is reachable (we don't enforce auth for install — server validates on use).
|
|
33
|
+
*/
|
|
34
|
+
async function validateServer(serverUrl: string): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
const baseUrl = serverUrl.replace(/\/mcp$/, '');
|
|
37
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
signal: AbortSignal.timeout(5000),
|
|
40
|
+
});
|
|
41
|
+
return response.ok;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(): Promise<void> {
|
|
48
|
+
const home = homedir();
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
|
|
51
|
+
log('');
|
|
52
|
+
log('gramatr - Claude Desktop installer');
|
|
53
|
+
log('===================================');
|
|
54
|
+
log('');
|
|
55
|
+
|
|
56
|
+
// Step 1: Resolve auth (OAuth-first via shared helper — issue #484)
|
|
57
|
+
log('Step 1: Resolving authentication...');
|
|
58
|
+
const apiKey = await resolveAuthToken({
|
|
59
|
+
interactive: true,
|
|
60
|
+
installerLabel: 'Claude Desktop',
|
|
61
|
+
});
|
|
62
|
+
log(' OK Authenticated');
|
|
63
|
+
|
|
64
|
+
// Step 2: Validate server connectivity
|
|
65
|
+
log('');
|
|
66
|
+
log('Step 2: Validating server connectivity...');
|
|
67
|
+
const serverUrl = process.env.GMTR_URL || DEFAULT_MCP_URL;
|
|
68
|
+
const serverReachable = await validateServer(serverUrl);
|
|
69
|
+
if (serverReachable) {
|
|
70
|
+
log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
|
|
71
|
+
} else {
|
|
72
|
+
log(` WARN Server not reachable at ${serverUrl.replace(/\/mcp$/, '')} — config will be written anyway`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 3: Detect platform and config path
|
|
76
|
+
log('');
|
|
77
|
+
log('Step 3: Detecting Claude Desktop...');
|
|
78
|
+
const configPath = getDesktopConfigPath(home, platform);
|
|
79
|
+
const configDir = dirname(configPath);
|
|
80
|
+
|
|
81
|
+
if (platform === 'darwin') {
|
|
82
|
+
log(' Platform: macOS');
|
|
83
|
+
} else if (platform === 'win32') {
|
|
84
|
+
log(' Platform: Windows');
|
|
85
|
+
} else {
|
|
86
|
+
log(` Platform: ${platform} (Claude Desktop may not be available)`);
|
|
87
|
+
}
|
|
88
|
+
log(` Config: ${configPath}`);
|
|
89
|
+
|
|
90
|
+
// Step 4: Read existing config
|
|
91
|
+
log('');
|
|
92
|
+
log('Step 4: Reading existing config...');
|
|
93
|
+
const existing = readJsonFile<DesktopConfig>(configPath, {});
|
|
94
|
+
|
|
95
|
+
const existingServerCount = existing.mcpServers ? Object.keys(existing.mcpServers).length : 0;
|
|
96
|
+
if (existingServerCount > 0) {
|
|
97
|
+
log(` Found ${existingServerCount} existing MCP server(s)`);
|
|
98
|
+
if (existing.mcpServers?.gramatr) {
|
|
99
|
+
log(' Existing gramatr entry will be updated');
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
log(' No existing config (will create new)');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 5: Merge and write config
|
|
106
|
+
log('');
|
|
107
|
+
log('Step 5: Writing config...');
|
|
108
|
+
const gramatrEntry = buildMcpServerEntry(apiKey, serverUrl);
|
|
109
|
+
const merged = mergeDesktopConfig(existing, gramatrEntry);
|
|
110
|
+
|
|
111
|
+
if (!existsSync(configDir)) {
|
|
112
|
+
mkdirSync(configDir, { recursive: true });
|
|
113
|
+
log(` Created directory: ${configDir}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
|
|
117
|
+
log(` OK Written to ${configPath}`);
|
|
118
|
+
|
|
119
|
+
// Summary
|
|
120
|
+
log('');
|
|
121
|
+
log('Installation complete.');
|
|
122
|
+
log('');
|
|
123
|
+
log('Restart Claude Desktop to connect to gramatr.');
|
|
124
|
+
log('');
|
|
125
|
+
log('What was configured:');
|
|
126
|
+
log(` MCP server: ${serverUrl}`);
|
|
127
|
+
log(` Config file: ${configPath}`);
|
|
128
|
+
log('');
|
|
129
|
+
log('Note: Claude Desktop is Tier 3 (MCP only). Hooks and status line are not');
|
|
130
|
+
log('available — use Claude Code or Codex for the full gramatr experience.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
log(`ERROR: ${err.message}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|