@gramatr/client 0.6.0 → 0.6.2

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.
@@ -8,9 +8,9 @@
8
8
  * 3. Env-sourced: gramatr add-api-key --from-env GRAMATR_API_KEY
9
9
  *
10
10
  * The key is validated against the gramatr server before being written
11
- * to ~/.gmtr.json. Use --force to skip server validation when offline.
11
+ * to ~/.gramatr.json. Use --force to skip server validation when offline.
12
12
  *
13
- * This command is the ONLY way to put an API key into ~/.gmtr.json.
13
+ * This command is the ONLY way to put an API key into ~/.gramatr.json.
14
14
  * Installers never prompt for API keys — see packages/client/core/auth.ts.
15
15
  */
16
16
 
@@ -20,7 +20,7 @@ import { join } from "path";
20
20
  import { createInterface } from "readline";
21
21
 
22
22
  function gmtrJsonPath(): string {
23
- return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gmtr.json");
23
+ return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gramatr.json");
24
24
  }
25
25
  const SERVER_BASE = (process.env.GRAMATR_URL || "https://api.gramatr.com").replace(/\/mcp\/?$/, "");
26
26
 
@@ -59,7 +59,7 @@ function parseArgs(argv: string[]): {
59
59
  }
60
60
 
61
61
  function showHelp(): void {
62
- log(`gramatr add-api-key — Add a gramatr API key to ~/.gmtr.json
62
+ log(`gramatr add-api-key — Add a gramatr API key to ~/.gramatr.json
63
63
 
64
64
  Usage:
65
65
  gramatr add-api-key Interactive prompt for the key
@@ -15,7 +15,7 @@ function main(): void {
15
15
 
16
16
  const apply = process.argv.includes('--apply');
17
17
  const includeOptionalUx = process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
18
- const clientDir = process.env.GRAMATR_DIR || join(home, 'gmtr-client');
18
+ const clientDir = process.env.GRAMATR_DIR || join(home, '.gramatr');
19
19
  runLegacyMigration({
20
20
  homeDir: home,
21
21
  clientDir,
@@ -2,13 +2,13 @@
2
2
  /**
3
3
  * gramatr clear-creds — Nuke ALL stored gramatr credentials.
4
4
  *
5
- * Stronger than `gramatr logout` (which only removes ~/.gmtr.json).
5
+ * Stronger than `gramatr logout` (which only removes ~/.gramatr.json).
6
6
  * This sweeps every credential location in the resolveAuthToken chain
7
7
  * (core/auth.ts), so the next install/login is forced through OAuth.
8
8
  *
9
9
  * Locations cleared:
10
- * 1. ~/.gmtr.json — deleted entirely
11
- * 2. ~/gmtr-client/settings.json `auth.api_key` — stripped, file preserved
10
+ * 1. ~/.gramatr.json — deleted entirely
11
+ * 2. ~/.gramatr/settings.json `auth.api_key` — stripped, file preserved
12
12
  *
13
13
  * Env vars (GRAMATR_API_KEY, GRAMATR_TOKEN) cannot be cleared from a child
14
14
  * process — they live in the parent shell. We detect them and warn.
@@ -24,11 +24,11 @@ function getHome(): string {
24
24
  }
25
25
 
26
26
  function gmtrJsonPath(): string {
27
- return join(getHome(), ".gmtr.json");
27
+ return join(getHome(), ".gramatr.json");
28
28
  }
29
29
 
30
30
  function legacySettingsPath(): string {
31
- const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), "gmtr-client");
31
+ const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), ".gramatr");
32
32
  return join(gmtrDir, "settings.json");
33
33
  }
34
34
 
@@ -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 ~/.gmtr.json + auth.api_key from gmtr-client/settings.json
43
+ gramatr clear-creds Sweep ~/.gramatr.json + auth.api_key from gmtr-client/settings.json
44
44
 
45
45
  After running, the next install or login will be forced through OAuth.
46
46
 
@@ -61,14 +61,14 @@ export function clearAll(): ClearResult {
61
61
  envVarsSet: [],
62
62
  };
63
63
 
64
- // 1. ~/.gmtr.json — delete entirely
64
+ // 1. ~/.gramatr.json — delete entirely
65
65
  const gj = gmtrJsonPath();
66
66
  if (existsSync(gj)) {
67
67
  unlinkSync(gj);
68
68
  result.removedGmtrJson = true;
69
69
  }
70
70
 
71
- // 2. ~/gmtr-client/settings.json — strip auth.api_key, preserve rest
71
+ // 2. ~/.gramatr/settings.json — strip auth.api_key, preserve rest
72
72
  const ls = legacySettingsPath();
73
73
  if (existsSync(ls)) {
74
74
  try {
package/bin/gmtr-login.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * gmtr-login — Authenticate with the gramatr server
4
4
  *
5
5
  * Opens the grāmatr dashboard login flow, captures a Firebase ID token
6
- * on localhost, and stores it in ~/.gmtr.json.
6
+ * on localhost, and stores it in ~/.gramatr.json.
7
7
  *
8
8
  * Usage:
9
9
  * bun gmtr-login.ts # Interactive browser login via Firebase dashboard
@@ -11,7 +11,7 @@
11
11
  * bun gmtr-login.ts --status # Check current auth status
12
12
  * bun gmtr-login.ts --logout # Remove stored credentials
13
13
  *
14
- * Token is stored in ~/.gmtr.json under the "token" key.
14
+ * Token is stored in ~/.gramatr.json under the "token" key.
15
15
  * The GMTRPromptEnricher hook reads this on every prompt.
16
16
  */
17
17
 
@@ -23,7 +23,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from 'http';
23
23
  // ── Config ──
24
24
 
25
25
  const HOME = process.env.HOME || process.env.USERPROFILE || '';
26
- const CONFIG_PATH = join(HOME, '.gmtr.json');
26
+ const CONFIG_PATH = join(HOME, '.gramatr.json');
27
27
  const DEFAULT_SERVER = process.env.GRAMATR_URL || 'https://api.gramatr.com/mcp';
28
28
  // Strip /mcp suffix to get base URL
29
29
  const SERVER_BASE = DEFAULT_SERVER.replace(/\/mcp\/?$/, '');
@@ -317,7 +317,7 @@ async function logout(): Promise<void> {
317
317
  delete config.token_expires;
318
318
  delete config.authenticated_at;
319
319
  writeConfig(config);
320
- console.log('\n ✓ Logged out. Token removed from ~/.gmtr.json\n');
320
+ console.log('\n ✓ Logged out. Token removed from ~/.gramatr.json\n');
321
321
  }
322
322
 
323
323
  async function loginWithToken(token: string): Promise<void> {
@@ -330,7 +330,7 @@ async function loginWithToken(token: string): Promise<void> {
330
330
  config.token_type = token.startsWith('aios_sk_') || token.startsWith('gramatr_sk_') ? 'api_key' : 'oauth';
331
331
  config.authenticated_at = new Date().toISOString();
332
332
  writeConfig(config);
333
- console.log(' ✓ Token valid. Saved to ~/.gmtr.json');
333
+ console.log(' ✓ Token valid. Saved to ~/.gramatr.json');
334
334
  console.log(' gramatr intelligence is now active.\n');
335
335
  } else {
336
336
  console.log(` ✗ Token rejected: ${result.error}`);
@@ -396,7 +396,7 @@ async function loginBrowser(): Promise<void> {
396
396
 
397
397
  console.log('');
398
398
  console.log(' ✓ Authenticated successfully');
399
- console.log(' Token saved to ~/.gmtr.json');
399
+ console.log(' Token saved to ~/.gramatr.json');
400
400
  console.log(' gramatr intelligence is now active.\n');
401
401
  return;
402
402
  } catch (e: any) {
@@ -503,7 +503,7 @@ async function loginBrowser(): Promise<void> {
503
503
 
504
504
  console.log('');
505
505
  console.log(' ✓ Authenticated successfully');
506
- console.log(' Token saved to ~/.gmtr.json');
506
+ console.log(' Token saved to ~/.gramatr.json');
507
507
  console.log(' gramatr intelligence is now active.\n');
508
508
  } catch (e: any) {
509
509
  console.log(`\n ✗ Authentication failed: ${e.message}\n`);
@@ -570,7 +570,7 @@ export async function main(): Promise<void> {
570
570
  gmtr-login --logout Remove stored credentials
571
571
  gmtr-login --help Show this help
572
572
 
573
- Token storage: ~/.gmtr.json
573
+ Token storage: ~/.gramatr.json
574
574
  Server: ${SERVER_BASE}
575
575
  Dashboard: ${DASHBOARD_BASE}
576
576
  `);
package/bin/gramatr.ts CHANGED
@@ -205,7 +205,7 @@ function installTarget(targetId: IntegrationTargetId): void {
205
205
 
206
206
  function migrate(apply: boolean, deep: boolean = false): void {
207
207
  const homeDir = homedir();
208
- const clientDir = process.env.GRAMATR_DIR || join(homeDir, 'gmtr-client');
208
+ const clientDir = process.env.GRAMATR_DIR || join(homeDir, '.gramatr');
209
209
  runLegacyMigration({
210
210
  homeDir,
211
211
  clientDir,
@@ -220,7 +220,7 @@ function migrate(apply: boolean, deep: boolean = false): void {
220
220
  }
221
221
 
222
222
  function doctor(): void {
223
- const gmtrDir = process.env.GRAMATR_DIR || join(homedir(), 'gmtr-client');
223
+ const gmtrDir = process.env.GRAMATR_DIR || join(homedir(), '.gramatr');
224
224
  const stale = findStaleArtifacts(homedir(), gmtrDir, existsSync);
225
225
  for (const line of formatDoctorLines(detectTargets(), gmtrDir, existsSync(gmtrDir), stale)) log(line);
226
226
  renderRemoteGuidance();
@@ -228,7 +228,7 @@ function doctor(): void {
228
228
 
229
229
  function upgrade(): void {
230
230
  const homeDir = homedir();
231
- const clientDir = process.env.GRAMATR_DIR || join(homeDir, 'gmtr-client');
231
+ const clientDir = process.env.GRAMATR_DIR || join(homeDir, '.gramatr');
232
232
  const stale = findStaleArtifacts(homeDir, clientDir, existsSync);
233
233
  if (stale.length > 0) {
234
234
  log('Cleaning stale legacy artifacts before upgrade...');
@@ -344,7 +344,7 @@ function main(): void {
344
344
  log(' install [target] Install gramatr (claude-code, codex, gemini-cli, all)');
345
345
  log(' login Authenticate with the gramatr server (OAuth)');
346
346
  log(' add-api-key Add an API key explicitly (interactive / piped / --from-env)');
347
- log(' logout Clear session token (~/.gmtr.json)');
347
+ log(' logout Clear session token (~/.gramatr.json)');
348
348
  log(' clear-creds Nuke ALL stored credentials, force OAuth on next login');
349
349
  log(' detect Show detected CLI platforms');
350
350
  log(' doctor Check installation health');
package/bin/install.ts CHANGED
@@ -20,6 +20,11 @@ import { createInterface } from 'readline';
20
20
  import { buildClaudeHooksFile } from '../core/install.ts';
21
21
  import { VERSION } from '../core/version.ts';
22
22
  import { resolveAuthToken } from '../core/auth.ts';
23
+ import {
24
+ sanitizeLegacyEnvBlock,
25
+ detectShellRcLegacyEnv,
26
+ formatShellRcLegacyWarning,
27
+ } from '../core/migration.ts';
23
28
 
24
29
  // ── Constants ──
25
30
 
@@ -27,8 +32,8 @@ const HOME = process.env.HOME || process.env.USERPROFILE || '';
27
32
  const CLAUDE_DIR = join(HOME, '.claude');
28
33
  const CLAUDE_SETTINGS = join(CLAUDE_DIR, 'settings.json');
29
34
  const CLAUDE_JSON = join(HOME, '.claude.json');
30
- const CLIENT_DIR = join(HOME, 'gmtr-client');
31
- const GMTR_JSON = join(HOME, '.gmtr.json');
35
+ const CLIENT_DIR = join(HOME, '.gramatr');
36
+ const GMTR_JSON = join(HOME, '.gramatr.json');
32
37
  const SCRIPT_DIR = dirname(dirname(resolve(import.meta.filename || __filename)));
33
38
  const DEFAULT_URL = 'https://api.gramatr.com/mcp';
34
39
 
@@ -398,13 +403,13 @@ async function handleAuth(legacyToken: string): Promise<{ url: string; token: st
398
403
  const url = await prompt('gramatr server URL', DEFAULT_URL) || DEFAULT_URL;
399
404
 
400
405
  // Legacy aios token migration: if we cherry-picked a token from a prior
401
- // aios install and there is no current ~/.gmtr.json token, seed it so the
406
+ // aios install and there is no current ~/.gramatr.json token, seed it so the
402
407
  // shared resolver picks it up.
403
408
  if (legacyToken && !existsSync(GMTR_JSON)) {
404
409
  try {
405
410
  writeFileSync(GMTR_JSON, `${JSON.stringify({ token: legacyToken }, null, 2)}\n`, 'utf8');
406
411
  try { chmodSync(GMTR_JSON, 0o600); } catch { /* ok */ }
407
- log('OK Seeded ~/.gmtr.json from legacy aios installation token');
412
+ log('OK Seeded ~/.gramatr.json from legacy aios installation token');
408
413
  } catch { /* ignore */ }
409
414
  }
410
415
 
@@ -513,11 +518,11 @@ function updateClaudeSettings(url: string, token: string): void {
513
518
 
514
519
  const settings = readJson(CLAUDE_SETTINGS);
515
520
 
516
- // Env vars
517
- settings.env = settings.env || {};
521
+ // Env vars — sanitize legacy GMTR_* keys before writing (v0.6.2+ fix)
522
+ settings.env = sanitizeLegacyEnvBlock(settings.env);
518
523
  settings.env.GRAMATR_DIR = CLIENT_DIR;
519
524
  settings.env.GRAMATR_URL = url;
520
- settings.env.PATH = `${HOME}/.gmtr/bin:/usr/local/bin:/usr/bin:/bin`;
525
+ settings.env.PATH = `${HOME}/.gramatr/bin:/usr/local/bin:/usr/bin:/bin`;
521
526
  if (token) settings.env.AIOS_MCP_TOKEN = token;
522
527
 
523
528
  // Identity defaults (don't overwrite)
@@ -564,7 +569,7 @@ function registerMcpServer(url: string, token: string): void {
564
569
  const backup = `${CLAUDE_JSON}.backup-${timestamp()}`;
565
570
  cpSync(CLAUDE_JSON, backup);
566
571
 
567
- // Store token in ~/.gmtr.json
572
+ // Store token in ~/.gramatr.json
568
573
  if (token) {
569
574
  mergeJson(GMTR_JSON, (data) => ({
570
575
  ...data,
@@ -572,10 +577,10 @@ function registerMcpServer(url: string, token: string): void {
572
577
  token_updated_at: new Date().toISOString(),
573
578
  }));
574
579
  try { chmodSync(GMTR_JSON, 0o600); } catch { /* ok */ }
575
- log('OK Token stored in ~/.gmtr.json (canonical source)');
580
+ log('OK Token stored in ~/.gramatr.json (canonical source)');
576
581
  }
577
582
 
578
- // Write tool paths to ~/.gmtr.json
583
+ // Write tool paths to ~/.gramatr.json
579
584
  mergeJson(GMTR_JSON, (data) => ({
580
585
  ...data,
581
586
  jq_binary: which('jq') || '',
@@ -584,7 +589,8 @@ function registerMcpServer(url: string, token: string): void {
584
589
 
585
590
  // Register in ~/.claude.json
586
591
  mergeJson(CLAUDE_JSON, (data) => {
587
- data.env = data.env || {};
592
+ // Sanitize legacy GMTR_* keys before writing (v0.6.2+ fix)
593
+ data.env = sanitizeLegacyEnvBlock(data.env);
588
594
  if (token) data.env.GRAMATR_TOKEN = token;
589
595
  delete data.env.AIOS_MCP_TOKEN;
590
596
 
@@ -734,9 +740,13 @@ async function main(): Promise<void> {
734
740
  if (!token) {
735
741
  log(' 2. Run /gmtr-login in Claude Code to authenticate');
736
742
  } else {
737
- log(' 2. Already authenticated (token found in ~/.gmtr.json)');
743
+ log(' 2. Already authenticated (token found in ~/.gramatr.json)');
738
744
  }
739
745
  log('');
746
+
747
+ // v0.6.2+: detect legacy GMTR_* exports in user shell rc files and warn
748
+ const rcHits = detectShellRcLegacyEnv(HOME);
749
+ if (rcHits.length > 0) log(formatShellRcLegacyWarning(rcHits));
740
750
  } else {
741
751
  log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
742
752
  log(' Install completed with warnings');
package/bin/lib/config.ts CHANGED
@@ -2,14 +2,14 @@
2
2
  * gramatr client config resolver (statusline refactor, #495).
3
3
  *
4
4
  * Single-purpose helper that returns the server URL + auth token with
5
- * NO reads of ~/.claude* or ~/gmtr-client/settings.json. This is the
5
+ * NO reads of ~/.claude* or ~/.gramatr/settings.json. This is the
6
6
  * statusline-specific config path; the richer installer auth lives in
7
7
  * core/auth.ts for a reason — this file must stay small and side-effect-free
8
8
  * so it can be loaded in a 2s shim without touching disk more than once.
9
9
  *
10
10
  * Resolution:
11
- * URL — GRAMATR_URL → ~/.gmtr.json.url → https://api.gramatr.com
12
- * Token — GRAMATR_TOKENAIOS_MCP_TOKEN → ~/.gmtr.json.token → null
11
+ * URL — GRAMATR_URL → ~/.gramatr.json.url → https://api.gramatr.com
12
+ * Token — GRAMATR_API_KEY → ~/.gramatr.json.token → null
13
13
  */
14
14
  import { existsSync, readFileSync } from 'fs';
15
15
  import { homedir } from 'os';
@@ -27,7 +27,7 @@ function getHome(): string {
27
27
  }
28
28
 
29
29
  function readGmtrJson(): Record<string, any> | null {
30
- const path = join(getHome(), '.gmtr.json');
30
+ const path = join(getHome(), '.gramatr.json');
31
31
  if (!existsSync(path)) return null;
32
32
  try {
33
33
  return JSON.parse(readFileSync(path, 'utf8'));
package/bin/logout.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * gramatr logout — Clear stored gramatr credentials (issue #484).
4
4
  *
5
- * Removes ~/.gmtr.json. With --keep-backup, renames it to
6
- * ~/.gmtr.json.bak.<timestamp> instead of deleting.
5
+ * Removes ~/.gramatr.json. With --keep-backup, renames it to
6
+ * ~/.gramatr.json.bak.<timestamp> instead of deleting.
7
7
  *
8
8
  * Not-logged-in is not an error: exits 0 with a clean message.
9
9
  */
@@ -13,7 +13,7 @@ import { homedir } from "os";
13
13
  import { join } from "path";
14
14
 
15
15
  function gmtrJsonPath(): string {
16
- return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gmtr.json");
16
+ return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gramatr.json");
17
17
  }
18
18
 
19
19
  function log(msg: string = ""): void {
@@ -34,8 +34,8 @@ function showHelp(): void {
34
34
  log(`gramatr logout — Clear stored gramatr credentials
35
35
 
36
36
  Usage:
37
- gramatr logout Delete ~/.gmtr.json
38
- gramatr logout --keep-backup Rename to ~/.gmtr.json.bak.<timestamp> instead`);
37
+ gramatr logout Delete ~/.gramatr.json
38
+ gramatr logout --keep-backup Rename to ~/.gramatr.json.bak.<timestamp> instead`);
39
39
  }
40
40
 
41
41
  export function main(argv: string[] = process.argv.slice(2)): number {
package/bin/uninstall.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Usage:
6
6
  * bun uninstall.ts # Interactive — confirms each step
7
7
  * bun uninstall.ts --yes # Non-interactive — uninstall everything
8
- * bun uninstall.ts --keep-auth # Keep ~/.gmtr.json (preserve login)
8
+ * bun uninstall.ts --keep-auth # Keep ~/.gramatr.json (preserve login)
9
9
  * npx tsx uninstall.ts # Without bun
10
10
  */
11
11
 
@@ -97,8 +97,8 @@ async function main(): Promise<void> {
97
97
  log(' ===================');
98
98
  log('');
99
99
 
100
- const gmtrClient = join(HOME, 'gmtr-client');
101
- const gmtrJson = join(HOME, '.gmtr.json');
100
+ const gmtrClient = join(HOME, '.gramatr');
101
+ const gmtrJson = join(HOME, '.gramatr.json');
102
102
  const gmtrDotDir = join(HOME, '.gmtr');
103
103
  const claudeSettings = join(HOME, '.claude', 'settings.json');
104
104
  const claudeJson = join(HOME, '.claude.json');
@@ -111,9 +111,9 @@ async function main(): Promise<void> {
111
111
 
112
112
  // Detect what's installed
113
113
  const installed: string[] = [];
114
- if (existsSync(gmtrClient)) installed.push(`~/gmtr-client (${dirSize(gmtrClient)})`);
114
+ if (existsSync(gmtrClient)) installed.push(`~/.gramatr (${dirSize(gmtrClient)})`);
115
115
  if (existsSync(gmtrDotDir)) installed.push(`~/.gmtr/ (${dirSize(gmtrDotDir)})`);
116
- if (existsSync(gmtrJson)) installed.push('~/.gmtr.json');
116
+ if (existsSync(gmtrJson)) installed.push('~/.gramatr.json');
117
117
  if (existsSync(claudeSettings)) installed.push('~/.claude/settings.json (hooks)');
118
118
  if (existsSync(claudeJson)) installed.push('~/.claude.json (MCP server)');
119
119
  if (existsSync(codexHooks)) installed.push('~/.codex/hooks.json');
@@ -136,10 +136,10 @@ async function main(): Promise<void> {
136
136
 
137
137
  log('');
138
138
 
139
- // 1. Remove ~/gmtr-client (hooks, bin, skills, agents, tools)
139
+ // 1. Remove ~/.gramatr (hooks, bin, skills, agents, tools)
140
140
  if (existsSync(gmtrClient)) {
141
141
  rmSync(gmtrClient, { recursive: true, force: true });
142
- log(' OK Removed ~/gmtr-client');
142
+ log(' OK Removed ~/.gramatr');
143
143
  }
144
144
 
145
145
  // 2. Remove ~/.gmtr/ (bun binary, PATH additions)
@@ -148,13 +148,13 @@ async function main(): Promise<void> {
148
148
  log(' OK Removed ~/.gmtr/');
149
149
  }
150
150
 
151
- // 3. Remove ~/.gmtr.json (auth token) — unless --keep-auth
151
+ // 3. Remove ~/.gramatr.json (auth token) — unless --keep-auth
152
152
  if (existsSync(gmtrJson)) {
153
153
  if (KEEP_AUTH) {
154
- log(' -- Kept ~/.gmtr.json (--keep-auth)');
154
+ log(' -- Kept ~/.gramatr.json (--keep-auth)');
155
155
  } else {
156
156
  rmSync(gmtrJson);
157
- log(' OK Removed ~/.gmtr.json');
157
+ log(' OK Removed ~/.gramatr.json');
158
158
  }
159
159
  }
160
160
 
@@ -174,14 +174,14 @@ async function main(): Promise<void> {
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(
177
- (h: any) => !h.command?.includes('gmtr-client') && !h.command?.includes('gramatr')
177
+ (h: any) => !h.command?.includes('.gramatr') && !h.command?.includes('gramatr')
178
178
  );
179
179
  if (settings.hooks[event].length === 0) delete settings.hooks[event];
180
180
  }
181
181
  }
182
182
  if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
183
183
  }
184
- if (settings.statusLine?.command?.includes('gmtr-client')) {
184
+ if (settings.statusLine?.command?.includes('.gramatr')) {
185
185
  delete settings.statusLine;
186
186
  }
187
187
  writeFileSync(claudeSettings, JSON.stringify(settings, null, 2) + '\n');
@@ -242,7 +242,7 @@ async function main(): Promise<void> {
242
242
  for (const [event, eventHooks] of Object.entries(hooks.hooks)) {
243
243
  if (Array.isArray(eventHooks)) {
244
244
  hooks.hooks[event] = (eventHooks as any[]).filter(
245
- (h: any) => !h.command?.includes('gmtr-client') && !h.command?.includes('gramatr')
245
+ (h: any) => !h.command?.includes('.gramatr') && !h.command?.includes('gramatr')
246
246
  );
247
247
  if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
248
248
  }
@@ -278,7 +278,7 @@ async function main(): Promise<void> {
278
278
  log(' Uninstall complete.');
279
279
  log(' Restart Claude Code / Codex / Gemini CLI to apply changes.');
280
280
  if (KEEP_AUTH) {
281
- log(' Auth token preserved in ~/.gmtr.json (use --token with next install).');
281
+ log(' Auth token preserved in ~/.gramatr.json (use --token with next install).');
282
282
  }
283
283
  log('');
284
284
  }
package/codex/install.ts CHANGED
@@ -9,6 +9,10 @@ import {
9
9
  mergeHooksFile,
10
10
  upsertManagedBlock,
11
11
  } from './lib/codex-install-utils.ts';
12
+ import {
13
+ detectShellRcLegacyEnv,
14
+ formatShellRcLegacyWarning,
15
+ } from '../core/migration.ts';
12
16
 
13
17
  const START_MARKER = '<!-- GMTR-CODEX-START -->';
14
18
  const END_MARKER = '<!-- GMTR-CODEX-END -->';
@@ -47,7 +51,7 @@ export function main(): void {
47
51
  throw new Error('HOME is not set');
48
52
  }
49
53
 
50
- const gmtrDir = process.env.GRAMATR_DIR || join(home, 'gmtr-client');
54
+ const gmtrDir = process.env.GRAMATR_DIR || join(home, '.gramatr');
51
55
  const codexHome = join(home, '.codex');
52
56
  const hooksPath = join(codexHome, 'hooks.json');
53
57
  const configPath = join(codexHome, 'config.toml');
@@ -107,6 +111,10 @@ export function main(): void {
107
111
  log('');
108
112
  log('Codex installer complete.');
109
113
  log('Restart Codex or start a new session to load the updated hook configuration.');
114
+
115
+ // v0.6.2+: detect legacy GMTR_* exports in shell rc and warn
116
+ const rcHits = detectShellRcLegacyEnv(home);
117
+ if (rcHits.length > 0) log(formatShellRcLegacyWarning(rcHits));
110
118
  }
111
119
 
112
120
  // Run directly when executed as a script
package/core/auth.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * Resolution chain (first non-empty wins):
9
9
  * 1. GRAMATR_API_KEY env var
10
10
  * 2. GRAMATR_TOKEN env var (legacy)
11
- * 3. ~/.gmtr.json `token` field
12
- * 4. ~/gmtr-client/settings.json `auth.api_key` (legacy, skips placeholder)
11
+ * 3. ~/.gramatr.json `token` field
12
+ * 4. ~/.gramatr/settings.json `auth.api_key` (legacy, skips placeholder)
13
13
  * 5. If interactive + TTY: spawn gmtr-login.ts (OAuth)
14
14
  * 6. Otherwise: throw clean actionable error
15
15
  *
@@ -44,11 +44,11 @@ function getHome(): string {
44
44
  }
45
45
 
46
46
  function gmtrJsonPath(): string {
47
- return join(getHome(), ".gmtr.json");
47
+ return join(getHome(), ".gramatr.json");
48
48
  }
49
49
 
50
50
  function legacySettingsPath(): string {
51
- const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), "gmtr-client");
51
+ const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), ".gramatr");
52
52
  return join(gmtrDir, "settings.json");
53
53
  }
54
54
 
@@ -90,7 +90,7 @@ function findGmtrLoginScript(): string | null {
90
90
  }
91
91
  // Fallback to installed client dir
92
92
  const installedCandidate = join(
93
- process.env.GRAMATR_DIR || join(getHome(), "gmtr-client"),
93
+ process.env.GRAMATR_DIR || join(getHome(), ".gramatr"),
94
94
  "bin",
95
95
  "gmtr-login.ts",
96
96
  );
@@ -144,7 +144,7 @@ export async function resolveAuthToken(opts: ResolveAuthTokenOptions): Promise<s
144
144
  const envToken = tokenFromEnv();
145
145
  if (envToken) return envToken;
146
146
 
147
- // 3: ~/.gmtr.json
147
+ // 3: ~/.gramatr.json
148
148
  const stored = tokenFromGmtrJson();
149
149
  if (stored) return stored;
150
150
 
package/core/migration.ts CHANGED
@@ -308,6 +308,33 @@ function stripLegacyEntriesFromHookEvent(value: unknown): unknown {
308
308
  .filter(Boolean);
309
309
  }
310
310
 
311
+ /**
312
+ * Env-var key prefixes that were used by earlier gramatr versions and are
313
+ * now either renamed or removed. Any key matching one of these prefixes that
314
+ * survives in a user's config file across an upgrade is a legacy leak and
315
+ * must be scrubbed before we write the new env block.
316
+ *
317
+ * Issue: v0.6.0 renamed GMTR_* → GRAMATR_* but the installer's env merge
318
+ * only SET new keys without removing old ones. Users upgrading from v0.5.x
319
+ * kept the stale GMTR_DIR, GMTR_URL, GMTR_TOKEN in their config files
320
+ * forever. v0.6.2 fixes this by sanitizing on every install.
321
+ */
322
+ export const LEGACY_ENV_KEY_PREFIXES: ReadonlyArray<string> = ['GMTR_'];
323
+
324
+ /**
325
+ * Strip any env-var key matching a legacy prefix. Returns a new object;
326
+ * input is not mutated. Safe on null/undefined (returns {}).
327
+ */
328
+ export function sanitizeLegacyEnvBlock(env: unknown): JsonObject {
329
+ if (!isRecord(env)) return {};
330
+ const next: JsonObject = {};
331
+ for (const [key, value] of Object.entries(env)) {
332
+ if (LEGACY_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix))) continue;
333
+ next[key] = value;
334
+ }
335
+ return next;
336
+ }
337
+
311
338
  export function sanitizeClaudeSettings(
312
339
  settings: JsonObject,
313
340
  clientDir: string,
@@ -326,14 +353,103 @@ export function sanitizeClaudeSettings(
326
353
  }
327
354
 
328
355
  next.hooks = hooks;
356
+ // Sanitize legacy env keys (v0.6.2+)
357
+ next.env = sanitizeLegacyEnvBlock(next.env);
329
358
  return next;
330
359
  }
331
360
 
332
361
  export function sanitizeClaudeJson(claudeJson: JsonObject): JsonObject {
333
- if (!isRecord(claudeJson.mcpServers)) return claudeJson;
334
- const mcpServers = { ...claudeJson.mcpServers };
335
- delete mcpServers.aios;
336
- return { ...claudeJson, mcpServers };
362
+ const next = { ...claudeJson };
363
+ if (isRecord(claudeJson.mcpServers)) {
364
+ const mcpServers = { ...claudeJson.mcpServers };
365
+ delete mcpServers.aios;
366
+ next.mcpServers = mcpServers;
367
+ }
368
+ // Sanitize legacy env keys (v0.6.2+)
369
+ next.env = sanitizeLegacyEnvBlock(claudeJson.env);
370
+ return next;
371
+ }
372
+
373
+ // ── Shell rc legacy env detection (v0.6.2+) ──────────────────────────────
374
+ //
375
+ // Users who added `export GMTR_TOKEN=...` lines to their shell rc files
376
+ // before v0.6.0 will keep those vars live across every new shell until they
377
+ // manually edit the files. Our installer cannot mutate user dotfiles
378
+ // without permission, but it CAN detect and warn.
379
+
380
+ export interface ShellRcLegacyHit {
381
+ file: string;
382
+ lineNumber: number;
383
+ line: string;
384
+ variableName: string;
385
+ }
386
+
387
+ const SHELL_RC_CANDIDATES: ReadonlyArray<string> = [
388
+ '.bashrc',
389
+ '.bash_profile',
390
+ '.zshrc',
391
+ '.zprofile',
392
+ '.profile',
393
+ ];
394
+
395
+ const LEGACY_EXPORT_RE = /^\s*export\s+(GMTR_[A-Z_]+)=/;
396
+
397
+ /**
398
+ * Scan shell rc files for `export GMTR_*=...` lines. Returns a list of hits,
399
+ * each with file path, line number, raw line, and the variable name. Never
400
+ * mutates any file — read-only detection.
401
+ */
402
+ export function detectShellRcLegacyEnv(homeDir: string): ShellRcLegacyHit[] {
403
+ const out: ShellRcLegacyHit[] = [];
404
+ for (const basename of SHELL_RC_CANDIDATES) {
405
+ const path = `${homeDir}/${basename}`;
406
+ if (!existsSync(path)) continue;
407
+ let content: string;
408
+ try {
409
+ content = readFileSync(path, 'utf8');
410
+ } catch {
411
+ continue;
412
+ }
413
+ const lines = content.split(/\r?\n/);
414
+ for (let i = 0; i < lines.length; i++) {
415
+ const match = lines[i].match(LEGACY_EXPORT_RE);
416
+ if (match) {
417
+ out.push({
418
+ file: path,
419
+ lineNumber: i + 1,
420
+ line: lines[i],
421
+ variableName: match[1],
422
+ });
423
+ }
424
+ }
425
+ }
426
+ return out;
427
+ }
428
+
429
+ /**
430
+ * Format shell-rc legacy hits as a human-readable block of warning text.
431
+ * Returns an empty string if there are no hits. Writes no files.
432
+ */
433
+ export function formatShellRcLegacyWarning(hits: ShellRcLegacyHit[]): string {
434
+ if (hits.length === 0) return '';
435
+ const lines: string[] = [];
436
+ lines.push('');
437
+ lines.push('⚠ Legacy GMTR_* exports detected in your shell rc files:');
438
+ lines.push('');
439
+ for (const hit of hits) {
440
+ lines.push(` ${hit.file}:${hit.lineNumber}`);
441
+ lines.push(` ${hit.line.trim()}`);
442
+ }
443
+ const uniqueVars = Array.from(new Set(hits.map((h) => h.variableName)));
444
+ lines.push('');
445
+ lines.push(' These vars are no longer read by gramatr (since v0.6.0) but');
446
+ lines.push(' remain in your shell environment on every login. To clean up:');
447
+ lines.push('');
448
+ lines.push(' 1. Edit the files above and delete the matching export lines');
449
+ lines.push(` 2. Run in each open terminal: unset ${uniqueVars.join(' ')}`);
450
+ lines.push(' 3. Or just start a fresh shell session');
451
+ lines.push('');
452
+ return lines.join('\n');
337
453
  }
338
454
 
339
455
  export function findStaleArtifacts(
package/core/version.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * Works in two environments:
9
9
  * 1. Source checkout: packages/client/core/version.ts →
10
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()).
11
+ * 2. Installed client: ~/.gramatr/core/version.ts →
12
+ * ~/.gramatr/package.json (copied by installClientFiles()).
13
13
  *
14
14
  * If the file cannot be resolved (unexpected layout), falls back to '0.0.0'
15
15
  * rather than throwing — the version check is opportunistic and must never
package/gemini/install.ts CHANGED
@@ -17,6 +17,10 @@ import {
17
17
  getGramatrExtensionDir,
18
18
  readStoredApiKey,
19
19
  } from './lib/gemini-install-utils.ts';
20
+ import {
21
+ detectShellRcLegacyEnv,
22
+ formatShellRcLegacyWarning,
23
+ } from '../core/migration.ts';
20
24
 
21
25
  function log(message: string): void {
22
26
  process.stdout.write(`${message}\n`);
@@ -104,10 +108,10 @@ async function testApiKey(key: string): Promise<boolean> {
104
108
  }
105
109
 
106
110
  async function resolveApiKey(home: string): Promise<string | null> {
107
- // 1. Check if already stored in ~/.gmtr.json (shared with other platforms)
111
+ // 1. Check if already stored in ~/.gramatr.json (shared with other platforms)
108
112
  const stored = readStoredApiKey(home);
109
113
  if (stored) {
110
- log(` Found existing token in ~/.gmtr.json`);
114
+ log(` Found existing token in ~/.gramatr.json`);
111
115
  log(' Testing token...');
112
116
  const valid = await testApiKey(stored);
113
117
  if (valid) {
@@ -142,8 +146,8 @@ async function resolveApiKey(home: string): Promise<string | null> {
142
146
  const valid = await testApiKey(prompted);
143
147
  if (valid) {
144
148
  log(' OK Token is valid');
145
- // Save to ~/.gmtr.json for cross-platform reuse
146
- const configPath = join(home, '.gmtr.json');
149
+ // Save to ~/.gramatr.json for cross-platform reuse
150
+ const configPath = join(home, '.gramatr.json');
147
151
  let config: Record<string, unknown> = {};
148
152
  if (existsSync(configPath)) {
149
153
  try {
@@ -159,7 +163,7 @@ async function resolveApiKey(home: string): Promise<string | null> {
159
163
  : 'oauth';
160
164
  config.authenticated_at = new Date().toISOString();
161
165
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
162
- log(' OK Saved to ~/.gmtr.json');
166
+ log(' OK Saved to ~/.gramatr.json');
163
167
  return prompted;
164
168
  }
165
169
  log(' Token rejected by server.');
@@ -244,9 +248,9 @@ export async function main(): Promise<void> {
244
248
  );
245
249
  log(' OK Wrote hooks/hooks.json');
246
250
 
247
- // ── Store token in ~/.gmtr.json (canonical source, not in extension dir) ──
251
+ // ── Store token in ~/.gramatr.json (canonical source, not in extension dir) ──
248
252
  if (apiKey) {
249
- const gmtrJsonPath = join(home, '.gmtr.json');
253
+ const gmtrJsonPath = join(home, '.gramatr.json');
250
254
  let gmtrConfig: Record<string, unknown> = {};
251
255
  if (existsSync(gmtrJsonPath)) {
252
256
  try { gmtrConfig = JSON.parse(readFileSync(gmtrJsonPath, 'utf8')); } catch {}
@@ -254,7 +258,7 @@ export async function main(): Promise<void> {
254
258
  gmtrConfig.token = apiKey;
255
259
  gmtrConfig.token_updated_at = new Date().toISOString();
256
260
  writeFileSync(gmtrJsonPath, JSON.stringify(gmtrConfig, null, 2) + '\n');
257
- log(' OK Token stored in ~/.gmtr.json (hooks read from here at runtime)');
261
+ log(' OK Token stored in ~/.gramatr.json (hooks read from here at runtime)');
258
262
  }
259
263
 
260
264
  // ── Summary ──
@@ -272,6 +276,10 @@ export async function main(): Promise<void> {
272
276
  log('');
273
277
  log(' Restart Gemini CLI to load the extension.');
274
278
  log('');
279
+
280
+ // v0.6.2+: detect legacy GMTR_* exports in shell rc and warn
281
+ const rcHits = detectShellRcLegacyEnv(home);
282
+ if (rcHits.length > 0) log(formatShellRcLegacyWarning(rcHits));
275
283
  }
276
284
 
277
285
  // Run directly when executed as a script
@@ -155,10 +155,10 @@ export function getGramatrExtensionDir(home: string): string {
155
155
  }
156
156
 
157
157
  /**
158
- * Read the stored API key from ~/.gmtr.json (shared with other platforms).
158
+ * Read the stored API key from ~/.gramatr.json (shared with other platforms).
159
159
  */
160
160
  export function readStoredApiKey(home: string): string | null {
161
- const configPath = join(home, '.gmtr.json');
161
+ const configPath = join(home, '.gramatr.json');
162
162
  if (!existsSync(configPath)) return null;
163
163
  try {
164
164
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
@@ -40,7 +40,7 @@ async function main() {
40
40
  let token = '';
41
41
 
42
42
  try {
43
- const gmtrJson = JSON.parse(readFileSync(`${process.env.HOME}/.gmtr.json`, 'utf8'));
43
+ const gmtrJson = JSON.parse(readFileSync(`${process.env.HOME}/.gramatr.json`, 'utf8'));
44
44
  token = gmtrJson.token || '';
45
45
  } catch { /* no token */ }
46
46
 
@@ -341,16 +341,16 @@ export function createDefaultConfig(options: {
341
341
  * Resolve auth token. gramatr credentials NEVER live in CLI-specific config files.
342
342
  *
343
343
  * Priority:
344
- * 1. ~/.gmtr.json (canonical, gramatr-owned, vendor-agnostic)
344
+ * 1. ~/.gramatr.json (canonical, gramatr-owned, vendor-agnostic)
345
345
  * 2. GRAMATR_TOKEN env var (CI, headless override)
346
- * 3. ~/gmtr-client/settings.json auth.api_key (legacy, will be migrated)
346
+ * 3. ~/.gramatr/settings.json auth.api_key (legacy, will be migrated)
347
347
  *
348
348
  * Token is NEVER stored in ~/.claude.json, ~/.codex/, or ~/.gemini/.
349
349
  */
350
350
  export function resolveAuthToken(): string | null {
351
- // 1. ~/.gmtr.json (canonical source — written by installer, read by all platforms)
351
+ // 1. ~/.gramatr.json (canonical source — written by installer, read by all platforms)
352
352
  try {
353
- const configPath = join(HOME, '.gmtr.json');
353
+ const configPath = join(HOME, '.gramatr.json');
354
354
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
355
355
  if (config.token) return config.token;
356
356
  } catch {
@@ -363,9 +363,9 @@ export function resolveAuthToken(): string | null {
363
363
  // 3. Legacy: AIOS_MCP_TOKEN env var (deprecated)
364
364
  if (process.env.AIOS_MCP_TOKEN) return process.env.AIOS_MCP_TOKEN;
365
365
 
366
- // 4. Legacy: ~/gmtr-client/settings.json (will be migrated to ~/.gmtr.json)
366
+ // 4. Legacy: ~/.gramatr/settings.json (will be migrated to ~/.gramatr.json)
367
367
  try {
368
- const gmtrDir = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
368
+ const gmtrDir = process.env.GRAMATR_DIR || join(HOME, '.gramatr');
369
369
  const settingsPath = join(gmtrDir, 'settings.json');
370
370
  const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
371
371
  if (settings.auth?.api_key && settings.auth.api_key !== 'REPLACE_WITH_YOUR_API_KEY') {
@@ -382,12 +382,12 @@ export function resolveAuthToken(): string | null {
382
382
 
383
383
  /**
384
384
  * Resolve MCP server URL from config files.
385
- * Priority: ~/gmtr-client/settings.json > GRAMATR_URL env > ~/.claude.json > default
385
+ * Priority: ~/.gramatr/settings.json > GRAMATR_URL env > ~/.claude.json > default
386
386
  */
387
387
  export function resolveMcpUrl(): string {
388
- // 1. ~/gmtr-client/settings.json (canonical, vendor-agnostic)
388
+ // 1. ~/.gramatr/settings.json (canonical, vendor-agnostic)
389
389
  try {
390
- const gmtrDir = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
390
+ const gmtrDir = process.env.GRAMATR_DIR || join(HOME, '.gramatr');
391
391
  const settingsPath = join(gmtrDir, 'settings.json');
392
392
  const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
393
393
  if (settings.auth?.server_url) return settings.auth.server_url;
@@ -10,7 +10,7 @@ import { readFileSync, existsSync } from 'fs';
10
10
  import { join } from 'path';
11
11
 
12
12
  const HOME = process.env.HOME!;
13
- const GRAMATR_DIR = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
13
+ const GRAMATR_DIR = process.env.GRAMATR_DIR || join(HOME, '.gramatr');
14
14
  const GMTR_SETTINGS_PATH = join(GRAMATR_DIR, 'settings.json'); // gramatr-owned, vendor-agnostic
15
15
  const CLAUDE_SETTINGS_PATH = join(HOME, '.claude/settings.json'); // legacy fallback only
16
16
 
@@ -82,7 +82,7 @@ let cachedSettings: Settings | null = null;
82
82
  * Load settings — gramatr-owned config first, Claude vendor file as legacy fallback.
83
83
  *
84
84
  * Priority:
85
- * 1. ~/gmtr-client/settings.json (gramatr-owned, vendor-agnostic)
85
+ * 1. ~/.gramatr/settings.json (gramatr-owned, vendor-agnostic)
86
86
  * 2. ~/.claude/settings.json (Claude vendor file — legacy fallback only)
87
87
  *
88
88
  * Merges both: gramatr settings take precedence for identity fields,
@@ -50,7 +50,7 @@ export function getGmtrDir(): string {
50
50
  /**
51
51
  * Get the settings.json path
52
52
  * Always ~/.claude/settings.json — NOT relative to GRAMATR_DIR.
53
- * GRAMATR_DIR is the client payload directory (~/gmtr-client), not ~/.claude.
53
+ * GRAMATR_DIR is the client payload directory (~/.gramatr), not ~/.claude.
54
54
  */
55
55
  export function getSettingsPath(): string {
56
56
  return join(homedir(), '.claude', 'settings.json');
@@ -171,7 +171,7 @@ function displayBanner(): void {
171
171
  // ── Sync Ratings (background, non-blocking) ──
172
172
 
173
173
  function syncRatingsInBackground(): void {
174
- const syncScript = join(process.env.GRAMATR_DIR || join(process.env.HOME || '', 'gmtr-client'), 'hooks', 'sync-ratings.hook.ts');
174
+ const syncScript = join(process.env.GRAMATR_DIR || join(process.env.HOME || '', '.gramatr'), 'hooks', 'sync-ratings.hook.ts');
175
175
  if (existsSync(syncScript)) {
176
176
  try {
177
177
  const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramatr/client",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },