@gramatr/client 0.6.2 → 0.6.5
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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/clear-creds.ts +1 -1
- package/bin/gmtr-login.ts +161 -72
- package/bin/uninstall.ts +1 -1
- package/chatgpt/README.md +1 -1
- package/codex/README.md +2 -2
- package/core/version-check.ts +2 -2
- package/desktop/README.md +1 -1
- package/gemini/README.md +3 -3
- package/hooks/session-start.hook.ts +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@ ISC scaffold, capability audit, phase templates, and composed agents.
|
|
|
10
10
|
**Memory:** Use gramatr MCP tools (`search_semantic`, `create_entity`, `add_observation`),
|
|
11
11
|
not local markdown files.
|
|
12
12
|
|
|
13
|
-
**Identity:** Read from
|
|
13
|
+
**Identity:** Read from `~/.gramatr/settings.json` — `daidentity` for your name,
|
|
14
14
|
`principal` for the user's name.
|
|
15
15
|
|
|
16
16
|
**If the server is unreachable:** Use 7-phase structure (OBSERVE → THINK → PLAN → BUILD →
|
package/README.md
CHANGED
|
@@ -72,14 +72,14 @@ The client never stores intelligence locally. The server delivers everything: be
|
|
|
72
72
|
## What gets installed
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
|
|
75
|
+
~/.gramatr/ # Client runtime
|
|
76
76
|
hooks/ # 8 lifecycle hooks
|
|
77
77
|
core/ # Shared routing + session logic
|
|
78
78
|
bin/ # Status line, login, utilities
|
|
79
79
|
CLAUDE.md # Minimal behavioral framework
|
|
80
80
|
~/.claude/settings.json # Hook configuration (merged, not overwritten)
|
|
81
81
|
~/.claude.json # MCP server registration
|
|
82
|
-
~/.
|
|
82
|
+
~/.gramatr.json # Auth token (canonical source)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
## Commands
|
package/bin/clear-creds.ts
CHANGED
|
@@ -40,7 +40,7 @@ function showHelp(): void {
|
|
|
40
40
|
log(`gramatr clear-creds — Remove every stored gramatr credential
|
|
41
41
|
|
|
42
42
|
Usage:
|
|
43
|
-
gramatr clear-creds Sweep ~/.gramatr.json + auth.api_key from
|
|
43
|
+
gramatr clear-creds Sweep ~/.gramatr.json + auth.api_key from ~/.gramatr/settings.json
|
|
44
44
|
|
|
45
45
|
After running, the next install or login will be forced through OAuth.
|
|
46
46
|
|
package/bin/gmtr-login.ts
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* The GMTRPromptEnricher hook reads this on every prompt.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { randomBytes } from 'crypto';
|
|
18
|
+
import { createHash, randomBytes } from 'crypto';
|
|
19
19
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
20
20
|
import { join } from 'path';
|
|
21
21
|
import { createServer, type IncomingMessage, type ServerResponse } from 'http';
|
|
22
|
+
import type { AddressInfo } from 'net';
|
|
22
23
|
|
|
23
24
|
// ── Config ──
|
|
24
25
|
|
|
@@ -41,7 +42,9 @@ const DASHBOARD_BASE = process.env.GMTR_DASHBOARD_URL || (() => {
|
|
|
41
42
|
return 'https://app.gramatr.com';
|
|
42
43
|
}
|
|
43
44
|
})();
|
|
44
|
-
|
|
45
|
+
// CALLBACK_PORT is now dynamically allocated per login (random localhost port).
|
|
46
|
+
// The server's DCR endpoint accepts arbitrary localhost redirect_uris for
|
|
47
|
+
// public CLIs (token_endpoint_auth_method=none). See loginBrowser() below.
|
|
45
48
|
|
|
46
49
|
// ── HTML Templates ──
|
|
47
50
|
|
|
@@ -140,7 +143,11 @@ function errorPage(title: string, detail: string): string {
|
|
|
140
143
|
|
|
141
144
|
// ── Headless Detection ──
|
|
142
145
|
|
|
143
|
-
function isHeadless(): boolean {
|
|
146
|
+
function isHeadless(forceFlag: boolean = false): boolean {
|
|
147
|
+
// Explicit override — user passed --headless, OR env GRAMATR_LOGIN_HEADLESS=1
|
|
148
|
+
if (forceFlag) return true;
|
|
149
|
+
if (process.env.GRAMATR_LOGIN_HEADLESS === '1') return true;
|
|
150
|
+
|
|
144
151
|
// SSH session without display forwarding
|
|
145
152
|
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) {
|
|
146
153
|
if (!process.env.DISPLAY && process.platform !== 'darwin') return true;
|
|
@@ -339,7 +346,7 @@ async function loginWithToken(token: string): Promise<void> {
|
|
|
339
346
|
}
|
|
340
347
|
}
|
|
341
348
|
|
|
342
|
-
async function loginBrowser(): Promise<void> {
|
|
349
|
+
async function loginBrowser(opts: { forceHeadless?: boolean } = {}): Promise<void> {
|
|
343
350
|
console.log('\n gramatr login\n');
|
|
344
351
|
console.log(` Server: ${SERVER_BASE}`);
|
|
345
352
|
console.log(` Dashboard: ${DASHBOARD_BASE}`);
|
|
@@ -355,8 +362,11 @@ async function loginBrowser(): Promise<void> {
|
|
|
355
362
|
|
|
356
363
|
console.log('');
|
|
357
364
|
|
|
358
|
-
// Headless environments use device auth (no local server needed)
|
|
359
|
-
|
|
365
|
+
// Headless environments use device auth (no local server needed).
|
|
366
|
+
// --headless flag or GRAMATR_LOGIN_HEADLESS=1 forces this path even on
|
|
367
|
+
// desktop — escape hatch when the browser flow is broken or the user
|
|
368
|
+
// prefers the device flow's out-of-band UX.
|
|
369
|
+
if (isHeadless(opts.forceHeadless)) {
|
|
360
370
|
console.log(' Headless environment detected. Starting device login...\n');
|
|
361
371
|
try {
|
|
362
372
|
const device = await startDeviceAuthorization();
|
|
@@ -406,75 +416,115 @@ async function loginBrowser(): Promise<void> {
|
|
|
406
416
|
}
|
|
407
417
|
}
|
|
408
418
|
|
|
409
|
-
// Browser environments use
|
|
410
|
-
|
|
411
|
-
|
|
419
|
+
// ── Browser environments use OAuth 2.0 authorization-code grant + PKCE ──
|
|
420
|
+
//
|
|
421
|
+
// v0.6.5: This now mirrors the headless device flow's auth model — both
|
|
422
|
+
// paths produce an opaque `mcp-access-token-<uuid>` token (1-year TTL,
|
|
423
|
+
// Redis-backed) instead of the short-lived raw Firebase ID token the old
|
|
424
|
+
// dashboard-redirect flow stored. Per RFC 7591 (Dynamic Client Registration)
|
|
425
|
+
// we register the CLI as an OAuth client on first run, then per RFC 7636
|
|
426
|
+
// (PKCE) we exchange a code for the opaque token.
|
|
427
|
+
//
|
|
428
|
+
// Bug history: pre-v0.6.5 the browser flow stored a raw Firebase ID token
|
|
429
|
+
// (token_type: 'firebase'), which had a 1-hour TTL. After 1 hour every
|
|
430
|
+
// MCP call returned "JWT signature validation failed" because the token
|
|
431
|
+
// genuinely expired. See #519 + #524 + this PR.
|
|
432
|
+
|
|
433
|
+
// 1. Bind a localhost callback server (random port — server's DCR allows it)
|
|
434
|
+
const callbackServer = createServer();
|
|
435
|
+
await new Promise<void>((resolve, reject) => {
|
|
436
|
+
callbackServer.once('error', reject);
|
|
437
|
+
callbackServer.listen(0, '127.0.0.1', () => resolve());
|
|
438
|
+
});
|
|
439
|
+
const port = (callbackServer.address() as AddressInfo).port;
|
|
440
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
441
|
+
|
|
442
|
+
// 2. Dynamic client registration (RFC 7591). We always re-register because
|
|
443
|
+
// the redirect_uri changes every run (random localhost port). Server
|
|
444
|
+
// allows DCR with token_endpoint_auth_method=none for public CLIs.
|
|
445
|
+
const config = readConfig();
|
|
446
|
+
let clientId: string;
|
|
447
|
+
try {
|
|
448
|
+
const regRes = await fetch(`${SERVER_BASE}/register`, {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
headers: { 'Content-Type': 'application/json' },
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
client_name: 'gmtr-login',
|
|
453
|
+
redirect_uris: [redirectUri],
|
|
454
|
+
grant_types: ['authorization_code'],
|
|
455
|
+
response_types: ['code'],
|
|
456
|
+
token_endpoint_auth_method: 'none',
|
|
457
|
+
}),
|
|
458
|
+
signal: AbortSignal.timeout(10000),
|
|
459
|
+
});
|
|
460
|
+
const reg = await regRes.json().catch(() => ({}));
|
|
461
|
+
if (!regRes.ok || !reg.client_id) {
|
|
462
|
+
throw new Error(reg.error_description || reg.error || `HTTP ${regRes.status}`);
|
|
463
|
+
}
|
|
464
|
+
clientId = reg.client_id as string;
|
|
465
|
+
config.oauth_client_id = clientId;
|
|
466
|
+
writeConfig(config);
|
|
467
|
+
} catch (e: any) {
|
|
468
|
+
callbackServer.close();
|
|
469
|
+
console.log(` ✗ Dynamic client registration failed: ${e.message}\n`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
412
472
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
473
|
+
// 3. PKCE: generate verifier + S256 challenge
|
|
474
|
+
const codeVerifier = randomBytes(32).toString('base64url');
|
|
475
|
+
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
476
|
+
const state = randomBytes(16).toString('base64url');
|
|
416
477
|
|
|
478
|
+
// 4. Run the callback server, waiting for /callback?code=...&state=...
|
|
479
|
+
//
|
|
480
|
+
// v0.6.5: capture the timeout handle and clearTimeout() in finally — same
|
|
481
|
+
// orphan-timer bug that v0.3.63 fixed for the device flow. Without the
|
|
482
|
+
// clear, Promise.race against a setTimeout keeps the Node event loop
|
|
483
|
+
// alive past success and the process hangs until Ctrl+C.
|
|
484
|
+
let codeTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
485
|
+
const codePromise = new Promise<string>((resolve, reject) => {
|
|
486
|
+
callbackServer.on('request', (req: IncomingMessage, res: ServerResponse) => {
|
|
487
|
+
const url = new URL(req.url || '/', redirectUri);
|
|
417
488
|
if (url.pathname !== '/callback') {
|
|
418
489
|
res.writeHead(404);
|
|
419
490
|
res.end('Not found');
|
|
420
491
|
return;
|
|
421
492
|
}
|
|
422
|
-
|
|
423
|
-
const token = url.searchParams.get('token');
|
|
493
|
+
const code = url.searchParams.get('code');
|
|
424
494
|
const returnedState = url.searchParams.get('state');
|
|
425
495
|
const error = url.searchParams.get('error');
|
|
426
|
-
|
|
427
496
|
if (error) {
|
|
428
497
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
429
498
|
res.end(errorPage('Authentication Failed', error));
|
|
430
|
-
|
|
499
|
+
callbackServer.close();
|
|
431
500
|
reject(new Error(`OAuth error: ${error}`));
|
|
432
501
|
return;
|
|
433
502
|
}
|
|
434
|
-
|
|
435
|
-
if (!token || returnedState !== state) {
|
|
503
|
+
if (!code || returnedState !== state) {
|
|
436
504
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
437
|
-
res.end(errorPage('Invalid Callback', 'Missing
|
|
438
|
-
|
|
505
|
+
res.end(errorPage('Invalid Callback', 'Missing code or state mismatch. Please try again.'));
|
|
506
|
+
callbackServer.close();
|
|
439
507
|
reject(new Error('Invalid callback'));
|
|
440
508
|
return;
|
|
441
509
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
447
|
-
res.end(errorPage('Token Validation Failed', validation.error || 'Server rejected token'));
|
|
448
|
-
server.close();
|
|
449
|
-
reject(new Error(validation.error || 'Server rejected token'));
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
454
|
-
res.end(successPage());
|
|
455
|
-
server.close();
|
|
456
|
-
resolve(token);
|
|
457
|
-
} catch (e: any) {
|
|
458
|
-
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
459
|
-
res.end(errorPage('Unexpected Error', e.message));
|
|
460
|
-
server.close();
|
|
461
|
-
reject(e);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
server.listen(CALLBACK_PORT, () => {
|
|
466
|
-
// Server ready
|
|
510
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
511
|
+
res.end(successPage());
|
|
512
|
+
callbackServer.close();
|
|
513
|
+
resolve(code);
|
|
467
514
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
reject(new Error('Login timed out after 2 minutes'));
|
|
473
|
-
}, 120000);
|
|
515
|
+
codeTimeoutHandle = setTimeout(() => {
|
|
516
|
+
callbackServer.close();
|
|
517
|
+
reject(new Error('Login timed out after 5 minutes'));
|
|
518
|
+
}, 5 * 60 * 1000);
|
|
474
519
|
});
|
|
475
520
|
|
|
476
|
-
|
|
477
|
-
authorizeUrl
|
|
521
|
+
// 5. Open the browser to the server's /authorize endpoint with PKCE params
|
|
522
|
+
const authorizeUrl = new URL('/authorize', SERVER_BASE);
|
|
523
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
524
|
+
authorizeUrl.searchParams.set('client_id', clientId);
|
|
525
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
526
|
+
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
527
|
+
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
478
528
|
authorizeUrl.searchParams.set('state', state);
|
|
479
529
|
|
|
480
530
|
console.log(' Opening browser for authentication...');
|
|
@@ -482,33 +532,64 @@ async function loginBrowser(): Promise<void> {
|
|
|
482
532
|
console.log(` ${authorizeUrl.toString()}`);
|
|
483
533
|
console.log('');
|
|
484
534
|
|
|
485
|
-
// Open browser
|
|
486
535
|
const { exec } = await import('child_process');
|
|
487
536
|
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
488
537
|
exec(`${openCmd} "${authorizeUrl.toString()}"`);
|
|
489
538
|
|
|
490
539
|
console.log(' Waiting for authorization...');
|
|
491
540
|
|
|
541
|
+
let authCode: string;
|
|
492
542
|
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// Save token
|
|
496
|
-
const config = readConfig();
|
|
497
|
-
config.token = accessToken;
|
|
498
|
-
config.token_type = 'firebase';
|
|
499
|
-
config.authenticated_at = new Date().toISOString();
|
|
500
|
-
config.server_url = SERVER_BASE;
|
|
501
|
-
config.dashboard_url = DASHBOARD_BASE;
|
|
502
|
-
writeConfig(config);
|
|
503
|
-
|
|
504
|
-
console.log('');
|
|
505
|
-
console.log(' ✓ Authenticated successfully');
|
|
506
|
-
console.log(' Token saved to ~/.gramatr.json');
|
|
507
|
-
console.log(' gramatr intelligence is now active.\n');
|
|
543
|
+
authCode = await codePromise;
|
|
508
544
|
} catch (e: any) {
|
|
545
|
+
if (codeTimeoutHandle) clearTimeout(codeTimeoutHandle);
|
|
509
546
|
console.log(`\n ✗ Authentication failed: ${e.message}\n`);
|
|
510
547
|
process.exit(1);
|
|
511
548
|
}
|
|
549
|
+
if (codeTimeoutHandle) clearTimeout(codeTimeoutHandle);
|
|
550
|
+
|
|
551
|
+
// 6. Exchange the code for an opaque MCP access token at /token
|
|
552
|
+
let accessToken: string;
|
|
553
|
+
let expiresIn: number = 31536000; // server default = 1 year
|
|
554
|
+
try {
|
|
555
|
+
const tokenRes = await fetch(`${SERVER_BASE}/token`, {
|
|
556
|
+
method: 'POST',
|
|
557
|
+
headers: { 'Content-Type': 'application/json' },
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
grant_type: 'authorization_code',
|
|
560
|
+
code: authCode,
|
|
561
|
+
code_verifier: codeVerifier,
|
|
562
|
+
client_id: clientId,
|
|
563
|
+
redirect_uri: redirectUri,
|
|
564
|
+
}),
|
|
565
|
+
signal: AbortSignal.timeout(10000),
|
|
566
|
+
});
|
|
567
|
+
const payload = await tokenRes.json().catch(() => ({}));
|
|
568
|
+
if (!tokenRes.ok || !payload.access_token) {
|
|
569
|
+
throw new Error(payload.error_description || payload.error || `HTTP ${tokenRes.status}`);
|
|
570
|
+
}
|
|
571
|
+
accessToken = payload.access_token as string;
|
|
572
|
+
if (typeof payload.expires_in === 'number') expiresIn = payload.expires_in;
|
|
573
|
+
} catch (e: any) {
|
|
574
|
+
console.log(`\n ✗ Token exchange failed: ${e.message}\n`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 7. Save the opaque token — same shape as the device flow
|
|
579
|
+
const updated = readConfig();
|
|
580
|
+
updated.token = accessToken;
|
|
581
|
+
updated.token_type = 'oauth';
|
|
582
|
+
updated.authenticated_at = new Date().toISOString();
|
|
583
|
+
updated.server_url = SERVER_BASE;
|
|
584
|
+
updated.dashboard_url = DASHBOARD_BASE;
|
|
585
|
+
updated.token_expires_at = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
586
|
+
if (clientId) updated.oauth_client_id = clientId;
|
|
587
|
+
writeConfig(updated);
|
|
588
|
+
|
|
589
|
+
console.log('');
|
|
590
|
+
console.log(' ✓ Authenticated successfully');
|
|
591
|
+
console.log(' Token saved to ~/.gramatr.json');
|
|
592
|
+
console.log(' gramatr intelligence is now active.\n');
|
|
512
593
|
}
|
|
513
594
|
|
|
514
595
|
// ── CLI ──
|
|
@@ -563,13 +644,19 @@ export async function main(): Promise<void> {
|
|
|
563
644
|
gmtr-login — Authenticate with the gramatr server
|
|
564
645
|
|
|
565
646
|
Usage:
|
|
566
|
-
gmtr-login Interactive
|
|
647
|
+
gmtr-login Interactive login (browser PKCE when display available,
|
|
648
|
+
device flow otherwise — auto-detected)
|
|
649
|
+
gmtr-login --headless Force the device flow even on desktops (escape hatch
|
|
650
|
+
when the browser flow is broken or unavailable)
|
|
567
651
|
gmtr-login --token Paste a token (API key or Firebase token)
|
|
568
652
|
gmtr-login --token <t> Provide token directly
|
|
569
653
|
gmtr-login --status Check authentication status
|
|
570
654
|
gmtr-login --logout Remove stored credentials
|
|
571
655
|
gmtr-login --help Show this help
|
|
572
656
|
|
|
657
|
+
Environment:
|
|
658
|
+
GRAMATR_LOGIN_HEADLESS=1 Same as --headless (forces device flow)
|
|
659
|
+
|
|
573
660
|
Token storage: ~/.gramatr.json
|
|
574
661
|
Server: ${SERVER_BASE}
|
|
575
662
|
Dashboard: ${DASHBOARD_BASE}
|
|
@@ -577,8 +664,10 @@ export async function main(): Promise<void> {
|
|
|
577
664
|
return;
|
|
578
665
|
}
|
|
579
666
|
|
|
580
|
-
// Default: browser login flow
|
|
581
|
-
|
|
667
|
+
// Default: browser login flow (falls back to device flow if headless
|
|
668
|
+
// detected, OR if --headless / GRAMATR_LOGIN_HEADLESS=1 is set).
|
|
669
|
+
const forceHeadless = args.includes('--headless');
|
|
670
|
+
await loginBrowser({ forceHeadless });
|
|
582
671
|
}
|
|
583
672
|
|
|
584
673
|
// Module-run guard. Works both when invoked directly via
|
package/bin/uninstall.ts
CHANGED
|
@@ -170,7 +170,7 @@ async function main(): Promise<void> {
|
|
|
170
170
|
try {
|
|
171
171
|
const settings = JSON.parse(readFileSync(claudeSettings, 'utf8'));
|
|
172
172
|
if (settings.hooks) {
|
|
173
|
-
// Remove hook entries that reference
|
|
173
|
+
// Remove hook entries that reference .gramatr or gramatr paths
|
|
174
174
|
for (const [event, hooks] of Object.entries(settings.hooks)) {
|
|
175
175
|
if (Array.isArray(hooks)) {
|
|
176
176
|
settings.hooks[event] = (hooks as any[]).filter(
|
package/chatgpt/README.md
CHANGED
|
@@ -19,7 +19,7 @@ bun chatgpt/install.ts
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
The installer will:
|
|
22
|
-
1. Find your API key from `~/.
|
|
22
|
+
1. Find your API key from `~/.gramatr.json`, `GRAMATR_API_KEY` env, or prompt you
|
|
23
23
|
2. Validate connectivity to the gramatr server
|
|
24
24
|
3. Detect your platform and locate the ChatGPT config file
|
|
25
25
|
4. Merge the gramatr MCP server entry without overwriting existing servers
|
package/codex/README.md
CHANGED
|
@@ -21,8 +21,8 @@ Install locally with:
|
|
|
21
21
|
- `pnpm --filter @gramatr/client install-codex`
|
|
22
22
|
|
|
23
23
|
The installer:
|
|
24
|
-
- syncs this Codex runtime into
|
|
25
|
-
- syncs the shared `gmtr-hook-utils.ts` dependency into
|
|
24
|
+
- syncs this Codex runtime into `~/.gramatr/codex`
|
|
25
|
+
- syncs the shared `gmtr-hook-utils.ts` dependency into `~/.gramatr/hooks/lib`
|
|
26
26
|
- merges `~/.codex/hooks.json`
|
|
27
27
|
- enables `codex_hooks` in `~/.codex/config.toml`
|
|
28
28
|
- upserts a managed gramatr block in `~/.codex/AGENTS.md`
|
package/core/version-check.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* version-check.ts — opportunistic npm registry version check.
|
|
3
3
|
*
|
|
4
4
|
* Queries https://registry.npmjs.org/gramatr/latest on a 3s timeout, caches
|
|
5
|
-
* the result for one hour under ~/.
|
|
5
|
+
* the result for one hour under ~/.gramatr/.cache/version-check.json, and
|
|
6
6
|
* reports whether the installed client is behind the published version.
|
|
7
7
|
*
|
|
8
8
|
* Design constraints (see issue #468 sibling work):
|
|
@@ -56,7 +56,7 @@ export function compareVersions(a: string, b: string): number {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function getCachePath(home: string = homedir()): string {
|
|
59
|
-
return join(home, '.
|
|
59
|
+
return join(home, '.gramatr', '.cache', 'version-check.json');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function readCache(path: string): CacheFile | null {
|
package/desktop/README.md
CHANGED
|
@@ -12,7 +12,7 @@ bun packages/client/desktop/install.ts
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
The installer:
|
|
15
|
-
1. Resolves your API key from `~/.
|
|
15
|
+
1. Resolves your API key from `~/.gramatr.json`, `GRAMATR_API_KEY` env, or prompts
|
|
16
16
|
2. Validates connectivity to the gramatr server
|
|
17
17
|
3. Detects platform (macOS or Windows)
|
|
18
18
|
4. Reads existing `claude_desktop_config.json` without overwriting other MCP servers
|
package/gemini/README.md
CHANGED
|
@@ -62,7 +62,7 @@ echo "GRAMATR_API_KEY=your-key-here" > ~/.gemini/extensions/gramatr/.env
|
|
|
62
62
|
|
|
63
63
|
gramatr requires a Bearer token for all MCP calls. The installer handles this by:
|
|
64
64
|
|
|
65
|
-
1. Checking `~/.
|
|
65
|
+
1. Checking `~/.gramatr.json` for an existing token (shared with Claude Code / Codex)
|
|
66
66
|
2. Checking the `GRAMATR_API_KEY` environment variable
|
|
67
67
|
3. Prompting for a token interactively
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ To authenticate before installing, run:
|
|
|
72
72
|
bun packages/client/bin/gmtr-login.ts
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
This stores the token in `~/.
|
|
75
|
+
This stores the token in `~/.gramatr.json`, which the installer reads automatically.
|
|
76
76
|
|
|
77
77
|
API keys start with `gramatr_sk_` and can be created at [gramatr.com](https://gramatr.com) or via the `gramatr_create_api_key` MCP tool.
|
|
78
78
|
|
|
@@ -92,4 +92,4 @@ After installing and restarting Gemini CLI:
|
|
|
92
92
|
> @gramatr search for recent learning signals
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
If the MCP server responds, the extension is working. If you see auth errors, re-run the installer or check `~/.
|
|
95
|
+
If the MCP server responds, the extension is working. If you see auth errors, re-run the installer or check `~/.gramatr.json`.
|
|
@@ -321,7 +321,7 @@ async function main(): Promise<void> {
|
|
|
321
321
|
log('This will:');
|
|
322
322
|
log(` 1. Search gramatr for existing project: ${git.projectName}`);
|
|
323
323
|
log(' 2. Create project entity if not found');
|
|
324
|
-
log(' 3. Link entity to .
|
|
324
|
+
log(' 3. Link entity to .gramatr.json');
|
|
325
325
|
log(' 4. Enable full memory persistence');
|
|
326
326
|
log('');
|
|
327
327
|
} else {
|