@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.
Files changed (65) hide show
  1. package/AGENTS.md +17 -0
  2. package/CLAUDE.md +18 -0
  3. package/README.md +108 -0
  4. package/bin/add-api-key.ts +264 -0
  5. package/bin/clean-legacy-install.ts +28 -0
  6. package/bin/clear-creds.ts +141 -0
  7. package/bin/get-token.py +3 -0
  8. package/bin/gmtr-login.ts +599 -0
  9. package/bin/gramatr.js +36 -0
  10. package/bin/gramatr.ts +374 -0
  11. package/bin/install.ts +716 -0
  12. package/bin/lib/config.ts +57 -0
  13. package/bin/lib/git.ts +111 -0
  14. package/bin/lib/stdin.ts +53 -0
  15. package/bin/logout.ts +76 -0
  16. package/bin/render-claude-hooks.ts +16 -0
  17. package/bin/statusline.ts +81 -0
  18. package/bin/uninstall.ts +289 -0
  19. package/chatgpt/README.md +95 -0
  20. package/chatgpt/install.ts +140 -0
  21. package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
  22. package/codex/README.md +28 -0
  23. package/codex/hooks/session-start.ts +73 -0
  24. package/codex/hooks/stop.ts +34 -0
  25. package/codex/hooks/user-prompt-submit.ts +79 -0
  26. package/codex/install.ts +116 -0
  27. package/codex/lib/codex-hook-utils.ts +48 -0
  28. package/codex/lib/codex-install-utils.ts +123 -0
  29. package/core/auth.ts +170 -0
  30. package/core/feedback.ts +55 -0
  31. package/core/formatting.ts +179 -0
  32. package/core/install.ts +107 -0
  33. package/core/installer-cli.ts +122 -0
  34. package/core/migration.ts +479 -0
  35. package/core/routing.ts +108 -0
  36. package/core/session.ts +202 -0
  37. package/core/targets.ts +292 -0
  38. package/core/types.ts +179 -0
  39. package/core/version-check.ts +219 -0
  40. package/core/version.ts +47 -0
  41. package/desktop/README.md +72 -0
  42. package/desktop/build-mcpb.ts +166 -0
  43. package/desktop/install.ts +136 -0
  44. package/desktop/lib/desktop-install-utils.ts +70 -0
  45. package/gemini/README.md +95 -0
  46. package/gemini/hooks/session-start.ts +72 -0
  47. package/gemini/hooks/stop.ts +30 -0
  48. package/gemini/hooks/user-prompt-submit.ts +77 -0
  49. package/gemini/install.ts +281 -0
  50. package/gemini/lib/gemini-hook-utils.ts +63 -0
  51. package/gemini/lib/gemini-install-utils.ts +169 -0
  52. package/hooks/GMTRPromptEnricher.hook.ts +651 -0
  53. package/hooks/GMTRRatingCapture.hook.ts +198 -0
  54. package/hooks/GMTRSecurityValidator.hook.ts +399 -0
  55. package/hooks/GMTRToolTracker.hook.ts +181 -0
  56. package/hooks/StopOrchestrator.hook.ts +78 -0
  57. package/hooks/gmtr-tool-tracker-utils.ts +105 -0
  58. package/hooks/lib/gmtr-hook-utils.ts +770 -0
  59. package/hooks/lib/identity.ts +227 -0
  60. package/hooks/lib/notify.ts +46 -0
  61. package/hooks/lib/paths.ts +104 -0
  62. package/hooks/lib/transcript-parser.ts +452 -0
  63. package/hooks/session-end.hook.ts +168 -0
  64. package/hooks/session-start.hook.ts +501 -0
  65. 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
+ }
@@ -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
+ });