@gramatr/client 0.5.1 → 0.6.0
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/bin/add-api-key.ts +8 -8
- package/bin/clean-legacy-install.ts +2 -2
- package/bin/clear-creds.ts +4 -4
- package/bin/gmtr-login.ts +3 -3
- package/bin/gramatr.ts +5 -5
- package/bin/install.ts +53 -19
- package/bin/lib/config.ts +5 -5
- package/bin/render-claude-hooks.ts +1 -1
- package/bin/uninstall.ts +2 -2
- package/chatgpt/install.ts +1 -1
- package/codex/install.ts +1 -1
- package/core/auth.ts +6 -4
- package/core/feedback.ts +1 -1
- package/core/install.ts +7 -7
- package/core/routing.ts +2 -2
- package/core/session.ts +7 -7
- package/desktop/install.ts +1 -1
- package/gemini/README.md +1 -1
- package/gemini/install.ts +2 -2
- package/gemini/lib/gemini-install-utils.ts +1 -1
- package/hooks/StopOrchestrator.hook.ts +2 -2
- package/hooks/gmtr-tool-tracker-utils.ts +1 -1
- package/hooks/{GMTRPromptEnricher.hook.ts → gramatr-prompt-enricher.hook.ts} +6 -6
- package/hooks/{GMTRRatingCapture.hook.ts → gramatr-rating-capture.hook.ts} +9 -9
- package/hooks/{GMTRSecurityValidator.hook.ts → gramatr-security-validator.hook.ts} +2 -2
- package/hooks/{GMTRToolTracker.hook.ts → gramatr-tool-tracker.hook.ts} +2 -2
- package/hooks/lib/gmtr-hook-utils.ts +8 -8
- package/hooks/lib/identity.ts +2 -2
- package/hooks/lib/paths.ts +6 -6
- package/hooks/session-end.hook.ts +4 -4
- package/hooks/session-start.hook.ts +3 -3
- package/package.json +1 -1
package/bin/add-api-key.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Three modes:
|
|
6
6
|
* 1. Interactive prompt: gramatr add-api-key
|
|
7
|
-
* 2. Piped stdin: echo "
|
|
7
|
+
* 2. Piped stdin: echo "gramatr_sk_..." | gramatr add-api-key
|
|
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
|
|
@@ -22,11 +22,11 @@ import { createInterface } from "readline";
|
|
|
22
22
|
function gmtrJsonPath(): string {
|
|
23
23
|
return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gmtr.json");
|
|
24
24
|
}
|
|
25
|
-
const SERVER_BASE = (process.env.
|
|
25
|
+
const SERVER_BASE = (process.env.GRAMATR_URL || "https://api.gramatr.com").replace(/\/mcp\/?$/, "");
|
|
26
26
|
|
|
27
|
-
// Accept
|
|
27
|
+
// Accept gramatr_sk_, gramatr_pk_, aios_sk_, aios_pk_ (legacy), and Firebase-style
|
|
28
28
|
// long opaque tokens (length >= 32, base64url-ish characters).
|
|
29
|
-
const KEY_FORMAT = /^(gmtr|aios)_(sk|pk)_[A-Za-z0-9_-]+$/;
|
|
29
|
+
const KEY_FORMAT = /^(gramatr|gmtr|aios)_(sk|pk)_[A-Za-z0-9_-]+$/;
|
|
30
30
|
const LEGACY_OPAQUE = /^[A-Za-z0-9_.-]{32,}$/;
|
|
31
31
|
|
|
32
32
|
function log(msg: string = ""): void {
|
|
@@ -63,7 +63,7 @@ function showHelp(): void {
|
|
|
63
63
|
|
|
64
64
|
Usage:
|
|
65
65
|
gramatr add-api-key Interactive prompt for the key
|
|
66
|
-
echo "
|
|
66
|
+
echo "gramatr_sk_..." | gramatr add-api-key Read key from piped stdin
|
|
67
67
|
gramatr add-api-key --from-env VAR Read key from named env variable
|
|
68
68
|
gramatr add-api-key --force Skip server validation (offline use)
|
|
69
69
|
|
|
@@ -94,7 +94,7 @@ async function readPipedStdin(): Promise<string | null> {
|
|
|
94
94
|
async function readInteractive(): Promise<string> {
|
|
95
95
|
log("");
|
|
96
96
|
log("Paste your gramatr API key below.");
|
|
97
|
-
log("(Get one at https://gramatr.com/settings — keys start with
|
|
97
|
+
log("(Get one at https://gramatr.com/settings — keys start with gramatr_sk_)");
|
|
98
98
|
log("");
|
|
99
99
|
process.stdout.write(" Key: ");
|
|
100
100
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -168,7 +168,7 @@ function writeKey(key: string): void {
|
|
|
168
168
|
}
|
|
169
169
|
existing.token = key;
|
|
170
170
|
existing.token_type =
|
|
171
|
-
key.startsWith("
|
|
171
|
+
key.startsWith("gramatr_sk_") || key.startsWith("aios_sk_") ? "api_key" : "oauth";
|
|
172
172
|
existing.authenticated_at = new Date().toISOString();
|
|
173
173
|
writeFileSync(gmtrJsonPath(), `${JSON.stringify(existing, null, 2)}\n`, "utf8");
|
|
174
174
|
try {
|
|
@@ -214,7 +214,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
214
214
|
|
|
215
215
|
// Format validation
|
|
216
216
|
if (!validateFormat(key)) {
|
|
217
|
-
err("ERROR: key format is invalid. Expected
|
|
217
|
+
err("ERROR: key format is invalid. Expected gramatr_sk_... or gramatr_pk_...");
|
|
218
218
|
return 1;
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -14,8 +14,8 @@ function main(): void {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const apply = process.argv.includes('--apply');
|
|
17
|
-
const includeOptionalUx = process.env.
|
|
18
|
-
const clientDir = process.env.
|
|
17
|
+
const includeOptionalUx = process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
18
|
+
const clientDir = process.env.GRAMATR_DIR || join(home, 'gmtr-client');
|
|
19
19
|
runLegacyMigration({
|
|
20
20
|
homeDir: home,
|
|
21
21
|
clientDir,
|
package/bin/clear-creds.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 1. ~/.gmtr.json — deleted entirely
|
|
11
11
|
* 2. ~/gmtr-client/settings.json `auth.api_key` — stripped, file preserved
|
|
12
12
|
*
|
|
13
|
-
* Env vars (GRAMATR_API_KEY,
|
|
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.
|
|
15
15
|
*
|
|
16
16
|
* Not-logged-in is not an error: exits 0 with a clean message.
|
|
@@ -28,7 +28,7 @@ function gmtrJsonPath(): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function legacySettingsPath(): string {
|
|
31
|
-
const gmtrDir = process.env.
|
|
31
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), "gmtr-client");
|
|
32
32
|
return join(gmtrDir, "settings.json");
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -44,7 +44,7 @@ Usage:
|
|
|
44
44
|
|
|
45
45
|
After running, the next install or login will be forced through OAuth.
|
|
46
46
|
|
|
47
|
-
Env vars (GRAMATR_API_KEY,
|
|
47
|
+
Env vars (GRAMATR_API_KEY, GRAMATR_TOKEN) cannot be unset from this process.
|
|
48
48
|
If they are set, this command warns and tells you the shell command to run.`);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -86,7 +86,7 @@ export function clearAll(): ClearResult {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// 3. Env vars — detect only, cannot mutate parent shell
|
|
89
|
-
for (const v of ["GRAMATR_API_KEY", "
|
|
89
|
+
for (const v of ["GRAMATR_API_KEY", "GRAMATR_TOKEN"]) {
|
|
90
90
|
if (process.env[v]) result.envVarsSet.push(v);
|
|
91
91
|
}
|
|
92
92
|
|
package/bin/gmtr-login.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from 'http';
|
|
|
24
24
|
|
|
25
25
|
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
26
26
|
const CONFIG_PATH = join(HOME, '.gmtr.json');
|
|
27
|
-
const DEFAULT_SERVER = process.env.
|
|
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\/?$/, '');
|
|
30
30
|
const DASHBOARD_BASE = process.env.GMTR_DASHBOARD_URL || (() => {
|
|
@@ -327,7 +327,7 @@ async function loginWithToken(token: string): Promise<void> {
|
|
|
327
327
|
if (result.valid) {
|
|
328
328
|
const config = readConfig();
|
|
329
329
|
config.token = token;
|
|
330
|
-
config.token_type = token.startsWith('aios_sk_') || token.startsWith('
|
|
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
333
|
console.log(' ✓ Token valid. Saved to ~/.gmtr.json');
|
|
@@ -539,7 +539,7 @@ export async function main(): Promise<void> {
|
|
|
539
539
|
if (!token) {
|
|
540
540
|
// Interactive paste mode — like Claude's login
|
|
541
541
|
console.log('\n Paste your gramatr token below.');
|
|
542
|
-
console.log(' (API keys start with aios_sk_ or
|
|
542
|
+
console.log(' (API keys start with aios_sk_ or gramatr_sk_)\n');
|
|
543
543
|
process.stdout.write(' Token: ');
|
|
544
544
|
|
|
545
545
|
const { createInterface } = await import('readline');
|
package/bin/gramatr.ts
CHANGED
|
@@ -205,11 +205,11 @@ 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.
|
|
208
|
+
const clientDir = process.env.GRAMATR_DIR || join(homeDir, 'gmtr-client');
|
|
209
209
|
runLegacyMigration({
|
|
210
210
|
homeDir,
|
|
211
211
|
clientDir,
|
|
212
|
-
includeOptionalUx: process.env.
|
|
212
|
+
includeOptionalUx: process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1',
|
|
213
213
|
apply,
|
|
214
214
|
log,
|
|
215
215
|
});
|
|
@@ -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.
|
|
223
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(homedir(), 'gmtr-client');
|
|
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,14 +228,14 @@ function doctor(): void {
|
|
|
228
228
|
|
|
229
229
|
function upgrade(): void {
|
|
230
230
|
const homeDir = homedir();
|
|
231
|
-
const clientDir = process.env.
|
|
231
|
+
const clientDir = process.env.GRAMATR_DIR || join(homeDir, 'gmtr-client');
|
|
232
232
|
const stale = findStaleArtifacts(homeDir, clientDir, existsSync);
|
|
233
233
|
if (stale.length > 0) {
|
|
234
234
|
log('Cleaning stale legacy artifacts before upgrade...');
|
|
235
235
|
runLegacyMigration({
|
|
236
236
|
homeDir,
|
|
237
237
|
clientDir,
|
|
238
|
-
includeOptionalUx: process.env.
|
|
238
|
+
includeOptionalUx: process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1',
|
|
239
239
|
apply: true,
|
|
240
240
|
log,
|
|
241
241
|
});
|
package/bin/install.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Usage:
|
|
7
7
|
* npx tsx install.ts # Universal (Node + tsx)
|
|
8
8
|
* bun install.ts # If bun is available
|
|
9
|
-
*
|
|
9
|
+
* GRAMATR_TOKEN=xxx npx tsx install.ts # Headless with token
|
|
10
10
|
* npx tsx install.ts --yes # Non-interactive (auto-accept defaults)
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -284,19 +284,43 @@ function installClientFiles(): void {
|
|
|
284
284
|
copyFileIfExists(join(SCRIPT_DIR, 'bin/render-claude-hooks.ts'), join(CLIENT_DIR, 'bin/render-claude-hooks.ts'), true);
|
|
285
285
|
|
|
286
286
|
// Bin lib helpers (statusline shim depends on these — see #495)
|
|
287
|
-
//
|
|
287
|
+
// Walks the directory but filters out test files / __tests__ subdir so
|
|
288
|
+
// we don't ship .test.ts to user installs.
|
|
288
289
|
const binLibSrc = join(SCRIPT_DIR, 'bin', 'lib');
|
|
289
290
|
if (existsSync(binLibSrc)) {
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
mkdirSync(join(CLIENT_DIR, 'bin', 'lib'), { recursive: true });
|
|
292
|
+
let binLibCount = 0;
|
|
293
|
+
for (const f of readdirSync(binLibSrc)) {
|
|
294
|
+
const src = join(binLibSrc, f);
|
|
295
|
+
if (!statSync(src).isFile()) continue;
|
|
296
|
+
if (!f.endsWith('.ts')) continue;
|
|
297
|
+
if (f.endsWith('.test.ts')) continue;
|
|
298
|
+
copyFileIfExists(src, join(CLIENT_DIR, 'bin', 'lib', f));
|
|
299
|
+
binLibCount++;
|
|
300
|
+
}
|
|
301
|
+
log(`OK Installed bin/lib (${binLibCount} statusline shim helpers)`);
|
|
292
302
|
}
|
|
293
303
|
log('OK Installed bin (statusline, gmtr-login, render-claude-hooks)');
|
|
294
304
|
|
|
295
|
-
// Core dependencies
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
// Core dependencies — directory walk so new hook deps don't silently break.
|
|
306
|
+
// Hook imports include: routing.ts (enricher), session.ts (session-start),
|
|
307
|
+
// version-check.ts (session-start), version.ts (session-start), types.ts (transitive).
|
|
308
|
+
// Past bugs (#495 bin/lib, this same class repeated): hardcoded file lists drift
|
|
309
|
+
// when a hook grows a new core import. Walking the directory + filtering tests
|
|
310
|
+
// makes the installer self-healing.
|
|
311
|
+
const coreSrc = join(SCRIPT_DIR, 'core');
|
|
312
|
+
if (existsSync(coreSrc)) {
|
|
313
|
+
let coreCount = 0;
|
|
314
|
+
for (const f of readdirSync(coreSrc)) {
|
|
315
|
+
const src = join(coreSrc, f);
|
|
316
|
+
if (!statSync(src).isFile()) continue;
|
|
317
|
+
if (!f.endsWith('.ts')) continue;
|
|
318
|
+
if (f.endsWith('.test.ts')) continue;
|
|
319
|
+
copyFileIfExists(src, join(CLIENT_DIR, 'core', f));
|
|
320
|
+
coreCount++;
|
|
321
|
+
}
|
|
322
|
+
log(`OK Installed ${coreCount} core modules`);
|
|
298
323
|
}
|
|
299
|
-
log('OK Installed core modules');
|
|
300
324
|
|
|
301
325
|
// package.json — copied so core/version.ts can resolve the installed
|
|
302
326
|
// version at runtime. Single source of truth; see core/version.ts.
|
|
@@ -484,15 +508,15 @@ function updateClaudeSettings(url: string, token: string): void {
|
|
|
484
508
|
cpSync(CLAUDE_SETTINGS, backup);
|
|
485
509
|
log(`OK Backed up settings to ${basename(backup)}`);
|
|
486
510
|
|
|
487
|
-
const includeOptionalUx = process.env.
|
|
511
|
+
const includeOptionalUx = process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
488
512
|
const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx });
|
|
489
513
|
|
|
490
514
|
const settings = readJson(CLAUDE_SETTINGS);
|
|
491
515
|
|
|
492
516
|
// Env vars
|
|
493
517
|
settings.env = settings.env || {};
|
|
494
|
-
settings.env.
|
|
495
|
-
settings.env.
|
|
518
|
+
settings.env.GRAMATR_DIR = CLIENT_DIR;
|
|
519
|
+
settings.env.GRAMATR_URL = url;
|
|
496
520
|
settings.env.PATH = `${HOME}/.gmtr/bin:/usr/local/bin:/usr/bin:/bin`;
|
|
497
521
|
if (token) settings.env.AIOS_MCP_TOKEN = token;
|
|
498
522
|
|
|
@@ -515,7 +539,7 @@ function updateClaudeSettings(url: string, token: string): void {
|
|
|
515
539
|
if (includeOptionalUx) {
|
|
516
540
|
log('OK Optional Claude UX hooks enabled');
|
|
517
541
|
} else {
|
|
518
|
-
log('OK Thin-client hook set (set
|
|
542
|
+
log('OK Thin-client hook set (set GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX=1 for optional hooks)');
|
|
519
543
|
}
|
|
520
544
|
|
|
521
545
|
// Status line
|
|
@@ -561,14 +585,14 @@ function registerMcpServer(url: string, token: string): void {
|
|
|
561
585
|
// Register in ~/.claude.json
|
|
562
586
|
mergeJson(CLAUDE_JSON, (data) => {
|
|
563
587
|
data.env = data.env || {};
|
|
564
|
-
if (token) data.env.
|
|
588
|
+
if (token) data.env.GRAMATR_TOKEN = token;
|
|
565
589
|
delete data.env.AIOS_MCP_TOKEN;
|
|
566
590
|
|
|
567
591
|
data.mcpServers = data.mcpServers || {};
|
|
568
592
|
data.mcpServers.gramatr = {
|
|
569
593
|
type: 'http',
|
|
570
594
|
url,
|
|
571
|
-
headers: { Authorization: 'Bearer ${
|
|
595
|
+
headers: { Authorization: 'Bearer ${GRAMATR_TOKEN}' },
|
|
572
596
|
autoApprove: true,
|
|
573
597
|
};
|
|
574
598
|
return data;
|
|
@@ -594,16 +618,26 @@ function verify(url: string, token: string): boolean {
|
|
|
594
618
|
}
|
|
595
619
|
};
|
|
596
620
|
|
|
597
|
-
// Critical files
|
|
621
|
+
// Critical files — every runtime hook + bin import target must be present.
|
|
622
|
+
// If you add a new ../core/X.ts or ./lib/X.ts import in hooks/ or bin/, add the
|
|
623
|
+
// corresponding entry here so a partial install fails loudly instead of
|
|
624
|
+
// exploding at hook execution time. The hook-imports-shipped.test.ts
|
|
625
|
+
// regression guard catches missing entries at PR review.
|
|
598
626
|
for (const f of [
|
|
599
|
-
'hooks/
|
|
600
|
-
'hooks/
|
|
601
|
-
'hooks/
|
|
602
|
-
'hooks/
|
|
627
|
+
'hooks/gramatr-tool-tracker.hook.ts',
|
|
628
|
+
'hooks/gramatr-prompt-enricher.hook.ts',
|
|
629
|
+
'hooks/gramatr-rating-capture.hook.ts',
|
|
630
|
+
'hooks/gramatr-security-validator.hook.ts',
|
|
603
631
|
'hooks/lib/notify.ts',
|
|
604
632
|
'core/routing.ts',
|
|
633
|
+
'core/session.ts',
|
|
634
|
+
'core/version.ts',
|
|
635
|
+
'core/version-check.ts',
|
|
636
|
+
'core/types.ts',
|
|
637
|
+
'core/install.ts',
|
|
605
638
|
'bin/statusline.ts',
|
|
606
639
|
'bin/gmtr-login.ts',
|
|
640
|
+
'bin/render-claude-hooks.ts',
|
|
607
641
|
'bin/lib/git.ts',
|
|
608
642
|
'bin/lib/config.ts',
|
|
609
643
|
'bin/lib/stdin.ts',
|
package/bin/lib/config.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* so it can be loaded in a 2s shim without touching disk more than once.
|
|
9
9
|
*
|
|
10
10
|
* Resolution:
|
|
11
|
-
* URL —
|
|
12
|
-
* Token —
|
|
11
|
+
* URL — GRAMATR_URL → ~/.gmtr.json.url → https://api.gramatr.com
|
|
12
|
+
* Token — GRAMATR_TOKEN → AIOS_MCP_TOKEN → ~/.gmtr.json.token → null
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from 'fs';
|
|
15
15
|
import { homedir } from 'os';
|
|
@@ -40,15 +40,15 @@ export function getGramatrConfig(): GramatrConfig {
|
|
|
40
40
|
const json = readGmtrJson();
|
|
41
41
|
|
|
42
42
|
const rawUrl =
|
|
43
|
-
process.env.
|
|
43
|
+
process.env.GRAMATR_URL ||
|
|
44
44
|
(typeof json?.url === 'string' && json.url) ||
|
|
45
45
|
DEFAULT_URL;
|
|
46
|
-
//
|
|
46
|
+
// GRAMATR_URL is conventionally the MCP endpoint (e.g. https://api.gramatr.com/mcp).
|
|
47
47
|
// REST calls need the API base — strip a trailing /mcp segment.
|
|
48
48
|
const url = (rawUrl as string).replace(/\/mcp\/?$/, '');
|
|
49
49
|
|
|
50
50
|
const token =
|
|
51
|
-
process.env.
|
|
51
|
+
process.env.GRAMATR_TOKEN ||
|
|
52
52
|
process.env.AIOS_MCP_TOKEN ||
|
|
53
53
|
(typeof json?.token === 'string' && json.token) ||
|
|
54
54
|
null;
|
|
@@ -8,7 +8,7 @@ function main(): void {
|
|
|
8
8
|
throw new Error('Usage: render-claude-hooks.ts <client-dir>');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const includeOptionalUx = process.env.
|
|
11
|
+
const includeOptionalUx = process.env.GRAMATR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
12
12
|
const hooks = buildClaudeHooksFile(clientDir, { includeOptionalUx });
|
|
13
13
|
process.stdout.write(`${JSON.stringify(hooks.hooks, null, 2)}\n`);
|
|
14
14
|
}
|
package/bin/uninstall.ts
CHANGED
|
@@ -53,7 +53,7 @@ function removeGramatrFromJson(filePath: string, keys: string[]): boolean {
|
|
|
53
53
|
delete data[key];
|
|
54
54
|
changed = true;
|
|
55
55
|
}
|
|
56
|
-
// Handle nested keys like "env.
|
|
56
|
+
// Handle nested keys like "env.GRAMATR_TOKEN"
|
|
57
57
|
if (key.includes('.')) {
|
|
58
58
|
const [parent, child] = key.split('.');
|
|
59
59
|
if (data[parent] && child in data[parent]) {
|
|
@@ -194,7 +194,7 @@ async function main(): Promise<void> {
|
|
|
194
194
|
|
|
195
195
|
// 5. Remove gramatr from ~/.claude.json (MCP server + env vars)
|
|
196
196
|
if (existsSync(claudeJson)) {
|
|
197
|
-
const cleaned = removeGramatrFromJson(claudeJson, ['env.
|
|
197
|
+
const cleaned = removeGramatrFromJson(claudeJson, ['env.GRAMATR_TOKEN', 'env.AIOS_MCP_TOKEN']);
|
|
198
198
|
if (cleaned) log(' OK Removed gramatr MCP server + env vars from ~/.claude.json');
|
|
199
199
|
}
|
|
200
200
|
|
package/chatgpt/install.ts
CHANGED
|
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
|
|
|
64
64
|
// Step 2: Validate server connectivity
|
|
65
65
|
log('');
|
|
66
66
|
log('Step 2: Validating server connectivity...');
|
|
67
|
-
const serverUrl = process.env.
|
|
67
|
+
const serverUrl = process.env.GRAMATR_URL || DEFAULT_MCP_URL;
|
|
68
68
|
const serverReachable = await validateServer(serverUrl);
|
|
69
69
|
if (serverReachable) {
|
|
70
70
|
log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
|
package/codex/install.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function main(): void {
|
|
|
47
47
|
throw new Error('HOME is not set');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const gmtrDir = process.env.
|
|
50
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(home, 'gmtr-client');
|
|
51
51
|
const codexHome = join(home, '.codex');
|
|
52
52
|
const hooksPath = join(codexHome, 'hooks.json');
|
|
53
53
|
const configPath = join(codexHome, 'config.toml');
|
package/core/auth.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Resolution chain (first non-empty wins):
|
|
9
9
|
* 1. GRAMATR_API_KEY env var
|
|
10
|
-
* 2.
|
|
10
|
+
* 2. GRAMATR_TOKEN env var (legacy)
|
|
11
11
|
* 3. ~/.gmtr.json `token` field
|
|
12
12
|
* 4. ~/gmtr-client/settings.json `auth.api_key` (legacy, skips placeholder)
|
|
13
13
|
* 5. If interactive + TTY: spawn gmtr-login.ts (OAuth)
|
|
@@ -48,13 +48,15 @@ function gmtrJsonPath(): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function legacySettingsPath(): string {
|
|
51
|
-
const gmtrDir = process.env.
|
|
51
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(getHome(), "gmtr-client");
|
|
52
52
|
return join(gmtrDir, "settings.json");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function tokenFromEnv(): string | null {
|
|
56
|
+
// GRAMATR_API_KEY is the canonical env var for long-lived API keys.
|
|
57
|
+
// Legacy GMTR_TOKEN slot was removed in v0.6.0; see #512 for the full
|
|
58
|
+
// auth architecture refactor (split API keys from OAuth refresh tokens).
|
|
56
59
|
if (process.env.GRAMATR_API_KEY) return process.env.GRAMATR_API_KEY;
|
|
57
|
-
if (process.env.GMTR_TOKEN) return process.env.GMTR_TOKEN;
|
|
58
60
|
return null;
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -88,7 +90,7 @@ function findGmtrLoginScript(): string | null {
|
|
|
88
90
|
}
|
|
89
91
|
// Fallback to installed client dir
|
|
90
92
|
const installedCandidate = join(
|
|
91
|
-
process.env.
|
|
93
|
+
process.env.GRAMATR_DIR || join(getHome(), "gmtr-client"),
|
|
92
94
|
"bin",
|
|
93
95
|
"gmtr-login.ts",
|
|
94
96
|
);
|
package/core/feedback.ts
CHANGED
package/core/install.ts
CHANGED
|
@@ -27,13 +27,13 @@ interface ClaudeHookOptions {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const CLAUDE_HOOKS: HookSpec[] = [
|
|
30
|
-
{ event: 'PreToolUse', matcher: 'Bash', relativeCommand: 'hooks/
|
|
31
|
-
{ event: 'PreToolUse', matcher: 'Edit', relativeCommand: 'hooks/
|
|
32
|
-
{ event: 'PreToolUse', matcher: 'Write', relativeCommand: 'hooks/
|
|
33
|
-
{ event: 'PreToolUse', matcher: 'Read', relativeCommand: 'hooks/
|
|
34
|
-
{ event: 'PostToolUse', matcher: 'mcp__.*gramatr.*__', relativeCommand: 'hooks/
|
|
35
|
-
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/
|
|
36
|
-
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/
|
|
30
|
+
{ event: 'PreToolUse', matcher: 'Bash', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
31
|
+
{ event: 'PreToolUse', matcher: 'Edit', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
32
|
+
{ event: 'PreToolUse', matcher: 'Write', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
33
|
+
{ event: 'PreToolUse', matcher: 'Read', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
34
|
+
{ event: 'PostToolUse', matcher: 'mcp__.*gramatr.*__', relativeCommand: 'hooks/gramatr-tool-tracker.hook.ts' },
|
|
35
|
+
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/gramatr-rating-capture.hook.ts' },
|
|
36
|
+
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/gramatr-prompt-enricher.hook.ts' },
|
|
37
37
|
{ event: 'SessionStart', relativeCommand: 'hooks/session-start.hook.ts' },
|
|
38
38
|
{ event: 'SessionEnd', relativeCommand: 'hooks/session-end.hook.ts' },
|
|
39
39
|
{ event: 'Stop', relativeCommand: 'hooks/StopOrchestrator.hook.ts' },
|
package/core/routing.ts
CHANGED
|
@@ -30,7 +30,7 @@ export async function routePrompt(options: {
|
|
|
30
30
|
statuslineSize?: 'small' | 'medium' | 'large';
|
|
31
31
|
}): Promise<{ route: RouteResponse | null; error: MctToolCallError | null }> {
|
|
32
32
|
const result = await callMcpToolDetailed<RouteResponse>(
|
|
33
|
-
'
|
|
33
|
+
'gramatr_route_request',
|
|
34
34
|
{
|
|
35
35
|
prompt: options.prompt,
|
|
36
36
|
...(options.projectId ? { project_id: options.projectId } : {}),
|
|
@@ -79,7 +79,7 @@ export function describeRoutingFailure(error: MctToolCallError): {
|
|
|
79
79
|
return {
|
|
80
80
|
title: 'Routing request failed before intelligence could be injected.',
|
|
81
81
|
detail: error.detail,
|
|
82
|
-
action: `Inspect the response from ${resolveMcpUrl()} and the
|
|
82
|
+
action: `Inspect the response from ${resolveMcpUrl()} and the gramatr_route_request handler.`,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
}
|
package/core/session.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface CurrentProjectContextPayload {
|
|
|
28
28
|
project_name: string;
|
|
29
29
|
working_directory: string;
|
|
30
30
|
session_start: string;
|
|
31
|
-
|
|
31
|
+
gramatr_config_path: string;
|
|
32
32
|
project_entity_id: string | null;
|
|
33
33
|
action_required: string;
|
|
34
34
|
project_id?: string;
|
|
@@ -114,7 +114,7 @@ export async function startRemoteSession(options: {
|
|
|
114
114
|
directory: string;
|
|
115
115
|
}): Promise<SessionStartResponse | null> {
|
|
116
116
|
return (await callMcpTool(
|
|
117
|
-
'
|
|
117
|
+
'gramatr_session_start',
|
|
118
118
|
{
|
|
119
119
|
client_type: options.clientType,
|
|
120
120
|
project_id: options.projectId,
|
|
@@ -129,7 +129,7 @@ export async function startRemoteSession(options: {
|
|
|
129
129
|
|
|
130
130
|
export async function loadProjectHandoff(projectId: string): Promise<HandoffResponse | null> {
|
|
131
131
|
return (await callMcpTool(
|
|
132
|
-
'
|
|
132
|
+
'gramatr_load_handoff',
|
|
133
133
|
{ project_id: projectId },
|
|
134
134
|
15000,
|
|
135
135
|
)) as HandoffResponse | null;
|
|
@@ -143,7 +143,7 @@ export function persistSessionRegistration(rootDir: string, response: SessionSta
|
|
|
143
143
|
if (!config) return null;
|
|
144
144
|
config.current_session = config.current_session || {};
|
|
145
145
|
if (normalized.interactionId) config.current_session.interaction_id = normalized.interactionId;
|
|
146
|
-
if (normalized.entityId) config.current_session.
|
|
146
|
+
if (normalized.entityId) config.current_session.gramatr_entity_id = normalized.entityId;
|
|
147
147
|
writeGmtrConfig(rootDir, config);
|
|
148
148
|
return config;
|
|
149
149
|
}
|
|
@@ -176,7 +176,7 @@ export function buildGitProjectContextPayload(options: {
|
|
|
176
176
|
git_remote: options.git.remote,
|
|
177
177
|
working_directory: options.workingDirectory,
|
|
178
178
|
session_start: options.sessionStart,
|
|
179
|
-
|
|
179
|
+
gramatr_config_path: join(options.git.root, '.gmtr', 'settings.json'),
|
|
180
180
|
project_entity_id: options.projectEntityId,
|
|
181
181
|
restore_needed: options.restoreNeeded,
|
|
182
182
|
action_required: 'check_or_create_project_entity',
|
|
@@ -195,8 +195,8 @@ export function buildNonGitProjectContextPayload(options: {
|
|
|
195
195
|
project_name: basename(options.cwd),
|
|
196
196
|
working_directory: options.cwd,
|
|
197
197
|
session_start: options.sessionStart,
|
|
198
|
-
|
|
198
|
+
gramatr_config_path: join(options.cwd, '.gmtr', 'settings.json'),
|
|
199
199
|
project_entity_id: options.projectEntityId,
|
|
200
|
-
action_required: '
|
|
200
|
+
action_required: 'gramatr_init_needed',
|
|
201
201
|
};
|
|
202
202
|
}
|
package/desktop/install.ts
CHANGED
|
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
|
|
|
64
64
|
// Step 2: Validate server connectivity
|
|
65
65
|
log('');
|
|
66
66
|
log('Step 2: Validating server connectivity...');
|
|
67
|
-
const serverUrl = process.env.
|
|
67
|
+
const serverUrl = process.env.GRAMATR_URL || DEFAULT_MCP_URL;
|
|
68
68
|
const serverReachable = await validateServer(serverUrl);
|
|
69
69
|
if (serverReachable) {
|
|
70
70
|
log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
|
package/gemini/README.md
CHANGED
|
@@ -74,7 +74,7 @@ bun packages/client/bin/gmtr-login.ts
|
|
|
74
74
|
|
|
75
75
|
This stores the token in `~/.gmtr.json`, which the installer reads automatically.
|
|
76
76
|
|
|
77
|
-
API keys start with `
|
|
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
|
|
|
79
79
|
## Hook Event Mapping
|
|
80
80
|
|
package/gemini/install.ts
CHANGED
|
@@ -46,7 +46,7 @@ async function promptForApiKey(): Promise<string | null> {
|
|
|
46
46
|
log(' gramatr requires authentication.');
|
|
47
47
|
log(' Options:');
|
|
48
48
|
log(' 1. Run `bun gmtr-login.ts` first to authenticate via browser');
|
|
49
|
-
log(' 2. Paste an API key below (starts with
|
|
49
|
+
log(' 2. Paste an API key below (starts with gramatr_sk_)');
|
|
50
50
|
log('');
|
|
51
51
|
process.stdout.write(' API Key (enter to skip): ');
|
|
52
52
|
|
|
@@ -154,7 +154,7 @@ async function resolveApiKey(home: string): Promise<string | null> {
|
|
|
154
154
|
}
|
|
155
155
|
config.token = prompted;
|
|
156
156
|
config.token_type =
|
|
157
|
-
prompted.startsWith('
|
|
157
|
+
prompted.startsWith('gramatr_sk_') || prompted.startsWith('aios_sk_')
|
|
158
158
|
? 'api_key'
|
|
159
159
|
: 'oauth';
|
|
160
160
|
config.authenticated_at = new Date().toISOString();
|
|
@@ -67,7 +67,7 @@ export function buildExtensionManifest(): GeminiExtensionManifest {
|
|
|
67
67
|
name: 'API Key',
|
|
68
68
|
envVar: 'GRAMATR_API_KEY',
|
|
69
69
|
sensitive: true,
|
|
70
|
-
description: 'gramatr API key (starts with
|
|
70
|
+
description: 'gramatr API key (starts with gramatr_sk_). Get one at gramatr.com or via gmtr-login.',
|
|
71
71
|
},
|
|
72
72
|
],
|
|
73
73
|
};
|
|
@@ -36,7 +36,7 @@ async function main() {
|
|
|
36
36
|
|
|
37
37
|
// Read server URL and token for direct HTTP feedback
|
|
38
38
|
const gmtrDir = getGmtrDir();
|
|
39
|
-
let serverUrl = process.env.
|
|
39
|
+
let serverUrl = process.env.GRAMATR_URL || 'https://api.gramatr.com/mcp';
|
|
40
40
|
let token = '';
|
|
41
41
|
|
|
42
42
|
try {
|
|
@@ -61,7 +61,7 @@ async function main() {
|
|
|
61
61
|
id: 1,
|
|
62
62
|
method: 'tools/call',
|
|
63
63
|
params: {
|
|
64
|
-
name: '
|
|
64
|
+
name: 'gramatr_classification_feedback',
|
|
65
65
|
arguments: {
|
|
66
66
|
original_prompt: parsed.userMessages?.[0] || '',
|
|
67
67
|
session_id: hookInput.session_id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gmtr-tool-tracker-utils.ts — Pure utility functions for the GMTR Tool Tracker hook.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
4
|
+
* Extracted from gramatr-tool-tracker.hook.ts for testability. These are pure functions
|
|
5
5
|
* with no Bun dependencies, no I/O, no side effects.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* gramatr-prompt-enricher.hook.ts — grāmatr UserPromptSubmit Hook
|
|
4
4
|
*
|
|
5
5
|
* Fires before the agent processes a user prompt. Calls the grāmatr
|
|
6
6
|
* decision router to pre-classify the request, then injects
|
|
7
7
|
* the intelligence as additionalContext so the agent sees it.
|
|
8
8
|
*
|
|
9
9
|
* TRIGGER: UserPromptSubmit
|
|
10
|
-
* TIMEOUT: 15s default (configurable via
|
|
10
|
+
* TIMEOUT: 15s default (configurable via GRAMATR_TIMEOUT)
|
|
11
11
|
* SAFETY: Never blocks. On any error, prompt passes through unmodified.
|
|
12
12
|
*
|
|
13
13
|
* What it injects:
|
|
@@ -42,8 +42,8 @@ interface HookInput {
|
|
|
42
42
|
|
|
43
43
|
// ── Configuration ──
|
|
44
44
|
|
|
45
|
-
const TIMEOUT_MS = parseInt(process.env.
|
|
46
|
-
const ENABLED = process.env.
|
|
45
|
+
const TIMEOUT_MS = parseInt(process.env.GRAMATR_TIMEOUT || '30000', 10);
|
|
46
|
+
const ENABLED = process.env.GRAMATR_ENRICH !== '0'; // disable with GRAMATR_ENRICH=0
|
|
47
47
|
|
|
48
48
|
// ── Project ID Resolution (Issue #76 — project-scoped memory) ──
|
|
49
49
|
|
|
@@ -398,9 +398,9 @@ function formatIntelligence(data: RouteResponse): string {
|
|
|
398
398
|
lines.push(` Context: ${ca.context_summary || 'memory-aware'}`);
|
|
399
399
|
lines.push(' ACTION: Use the Task tool with subagent_type="general-purpose" and inject this system prompt:');
|
|
400
400
|
lines.push(' --- AGENT SYSTEM PROMPT START ---');
|
|
401
|
-
// Truncate to avoid overwhelming the context — full prompt available via
|
|
401
|
+
// Truncate to avoid overwhelming the context — full prompt available via gramatr_invoke_agent
|
|
402
402
|
const promptPreview = (ca.system_prompt || '').substring(0, 800);
|
|
403
|
-
lines.push(` ${promptPreview}${(ca.system_prompt || '').length > 800 ? '... [truncated — use
|
|
403
|
+
lines.push(` ${promptPreview}${(ca.system_prompt || '').length > 800 ? '... [truncated — use gramatr_invoke_agent for full prompt]' : ''}`);
|
|
404
404
|
lines.push(' --- AGENT SYSTEM PROMPT END ---');
|
|
405
405
|
}
|
|
406
406
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* gramatr-rating-capture.hook.ts — gramatr-native Rating Capture (UserPromptSubmit)
|
|
4
4
|
*
|
|
5
5
|
* Captures explicit ratings (1-10 pattern) from user prompts and writes
|
|
6
|
-
* to $
|
|
6
|
+
* to $GRAMATR_DIR/.state/ratings.jsonl for sparkline consumption.
|
|
7
7
|
*
|
|
8
8
|
* TRIGGER: UserPromptSubmit
|
|
9
9
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Self-contained: only depends on ./lib/paths
|
|
16
16
|
*
|
|
17
17
|
* SIDE EFFECTS:
|
|
18
|
-
* - Writes to: $
|
|
18
|
+
* - Writes to: $GRAMATR_DIR/.state/ratings.jsonl
|
|
19
19
|
* - macOS notification for low ratings (<= 3)
|
|
20
20
|
*
|
|
21
21
|
* PERFORMANCE:
|
|
@@ -100,13 +100,13 @@ function parseExplicitRating(prompt: string): { rating: number; comment?: string
|
|
|
100
100
|
function writeRating(entry: RatingEntry): void {
|
|
101
101
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
102
102
|
appendFileSync(RATINGS_FILE, JSON.stringify(entry) + '\n', 'utf-8');
|
|
103
|
-
console.error(`[
|
|
103
|
+
console.error(`[gramatr-rating-capture] Wrote rating ${entry.rating} to ${RATINGS_FILE}`);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// ── Push to GMTR Server (fire-and-forget) ──
|
|
107
107
|
|
|
108
108
|
function pushToServer(entry: RatingEntry): void {
|
|
109
|
-
const gmtrUrl = process.env.
|
|
109
|
+
const gmtrUrl = process.env.GRAMATR_URL || 'https://api.gramatr.com/mcp';
|
|
110
110
|
const apiBase = gmtrUrl.replace(/\/mcp$/, '/api/v1');
|
|
111
111
|
|
|
112
112
|
fetch(`${apiBase}/feedback`, {
|
|
@@ -124,9 +124,9 @@ function pushToServer(entry: RatingEntry): void {
|
|
|
124
124
|
})
|
|
125
125
|
.then((res) => {
|
|
126
126
|
if (res.ok) {
|
|
127
|
-
console.error(`[
|
|
127
|
+
console.error(`[gramatr-rating-capture] Pushed rating ${entry.rating} to server`);
|
|
128
128
|
} else {
|
|
129
|
-
console.error(`[
|
|
129
|
+
console.error(`[gramatr-rating-capture] Server push failed: HTTP ${res.status}`);
|
|
130
130
|
}
|
|
131
131
|
})
|
|
132
132
|
.catch(() => {
|
|
@@ -169,7 +169,7 @@ async function main() {
|
|
|
169
169
|
process.exit(0);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
console.error(`[
|
|
172
|
+
console.error(`[gramatr-rating-capture] Explicit rating: ${result.rating}${result.comment ? ` - ${result.comment}` : ''}`);
|
|
173
173
|
|
|
174
174
|
const entry: RatingEntry = {
|
|
175
175
|
timestamp: new Date().toISOString(),
|
|
@@ -190,7 +190,7 @@ async function main() {
|
|
|
190
190
|
|
|
191
191
|
process.exit(0);
|
|
192
192
|
} catch (err) {
|
|
193
|
-
console.error(`[
|
|
193
|
+
console.error(`[gramatr-rating-capture] Error: ${err}`);
|
|
194
194
|
process.exit(0);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* gramatr-security-validator.hook.ts — gramatr-native Security Validation (PreToolUse)
|
|
4
4
|
*
|
|
5
5
|
* Validates Bash commands and file operations against security patterns
|
|
6
6
|
* before execution. Prevents destructive operations and protects sensitive files.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This is the gramatr-native replacement for PAI's SecurityValidator.hook.ts.
|
|
11
11
|
* Key differences:
|
|
12
12
|
* - Inline default patterns (no yaml dependency, no PAI skill dir)
|
|
13
|
-
* - Logs to $
|
|
13
|
+
* - Logs to $GRAMATR_DIR/.state/security/ (not PAI MEMORY/SECURITY/)
|
|
14
14
|
* - Self-contained: only depends on ./lib/paths
|
|
15
15
|
*
|
|
16
16
|
* OUTPUT:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* gramatr-tool-tracker.hook.ts — grāmatr PostToolUse Hook
|
|
4
4
|
*
|
|
5
5
|
* Fires after any GMTR MCP tool call. Extracts execution metrics from
|
|
6
6
|
* tool_result and surfaces them as a user-visible status line.
|
|
@@ -146,7 +146,7 @@ async function main() {
|
|
|
146
146
|
try { stats = JSON.parse(readFileSync(statsFile, 'utf8')); } catch {}
|
|
147
147
|
|
|
148
148
|
// Track search count
|
|
149
|
-
if (shortName.includes('search') || shortName === '
|
|
149
|
+
if (shortName.includes('search') || shortName === 'gramatr_execute_intent') {
|
|
150
150
|
stats.search_count = ((stats.search_count as number) || 0) + 1;
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -46,7 +46,7 @@ export interface GmtrConfig {
|
|
|
46
46
|
last_updated?: string;
|
|
47
47
|
token_limit?: number;
|
|
48
48
|
interaction_id?: string;
|
|
49
|
-
|
|
49
|
+
gramatr_entity_id?: string;
|
|
50
50
|
helper_pid?: number | null;
|
|
51
51
|
last_classification?: {
|
|
52
52
|
timestamp?: string;
|
|
@@ -342,7 +342,7 @@ export function createDefaultConfig(options: {
|
|
|
342
342
|
*
|
|
343
343
|
* Priority:
|
|
344
344
|
* 1. ~/.gmtr.json (canonical, gramatr-owned, vendor-agnostic)
|
|
345
|
-
* 2.
|
|
345
|
+
* 2. GRAMATR_TOKEN env var (CI, headless override)
|
|
346
346
|
* 3. ~/gmtr-client/settings.json auth.api_key (legacy, will be migrated)
|
|
347
347
|
*
|
|
348
348
|
* Token is NEVER stored in ~/.claude.json, ~/.codex/, or ~/.gemini/.
|
|
@@ -357,15 +357,15 @@ export function resolveAuthToken(): string | null {
|
|
|
357
357
|
// No config file or parse error
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
// 2.
|
|
361
|
-
if (process.env.
|
|
360
|
+
// 2. GRAMATR_TOKEN env var (CI, headless, or shell profile override)
|
|
361
|
+
if (process.env.GRAMATR_TOKEN) return process.env.GRAMATR_TOKEN;
|
|
362
362
|
|
|
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
366
|
// 4. Legacy: ~/gmtr-client/settings.json (will be migrated to ~/.gmtr.json)
|
|
367
367
|
try {
|
|
368
|
-
const gmtrDir = process.env.
|
|
368
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
|
|
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 >
|
|
385
|
+
* Priority: ~/gmtr-client/settings.json > GRAMATR_URL env > ~/.claude.json > default
|
|
386
386
|
*/
|
|
387
387
|
export function resolveMcpUrl(): string {
|
|
388
388
|
// 1. ~/gmtr-client/settings.json (canonical, vendor-agnostic)
|
|
389
389
|
try {
|
|
390
|
-
const gmtrDir = process.env.
|
|
390
|
+
const gmtrDir = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
|
|
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;
|
|
@@ -396,7 +396,7 @@ export function resolveMcpUrl(): string {
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
// 2. Environment variable
|
|
399
|
-
if (process.env.
|
|
399
|
+
if (process.env.GRAMATR_URL) return process.env.GRAMATR_URL;
|
|
400
400
|
|
|
401
401
|
// 3. ~/.claude.json (Claude-specific MCP config)
|
|
402
402
|
try {
|
package/hooks/lib/identity.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
|
|
12
12
|
const HOME = process.env.HOME!;
|
|
13
|
-
const
|
|
14
|
-
const GMTR_SETTINGS_PATH = join(
|
|
13
|
+
const GRAMATR_DIR = process.env.GRAMATR_DIR || join(HOME, 'gmtr-client');
|
|
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
|
|
|
17
17
|
// Default identity (fallback if settings.json doesn't have identity section)
|
package/hooks/lib/paths.ts
CHANGED
|
@@ -28,15 +28,15 @@ export function expandPath(path: string): string {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Get the GMTR base directory (expanded)
|
|
31
|
-
* Priority:
|
|
31
|
+
* Priority: GRAMATR_DIR env var → PAI_DIR env var (migration) → ~/.claude
|
|
32
32
|
*/
|
|
33
33
|
export function getGmtrDir(): string {
|
|
34
|
-
const envGmtrDir = process.env.
|
|
34
|
+
const envGmtrDir = process.env.GRAMATR_DIR;
|
|
35
35
|
if (envGmtrDir) {
|
|
36
36
|
return expandPath(envGmtrDir);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Migration fallback: honor PAI_DIR if
|
|
39
|
+
// Migration fallback: honor PAI_DIR if GRAMATR_DIR not set yet
|
|
40
40
|
const envPaiDir = process.env.PAI_DIR;
|
|
41
41
|
if (envPaiDir) {
|
|
42
42
|
return expandPath(envPaiDir);
|
|
@@ -49,15 +49,15 @@ export function getGmtrDir(): string {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Get the settings.json path
|
|
52
|
-
* Always ~/.claude/settings.json — NOT relative to
|
|
53
|
-
*
|
|
52
|
+
* Always ~/.claude/settings.json — NOT relative to GRAMATR_DIR.
|
|
53
|
+
* GRAMATR_DIR is the client payload directory (~/gmtr-client), not ~/.claude.
|
|
54
54
|
*/
|
|
55
55
|
export function getSettingsPath(): string {
|
|
56
56
|
return join(homedir(), '.claude', 'settings.json');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Get a path relative to
|
|
60
|
+
* Get a path relative to GRAMATR_DIR
|
|
61
61
|
*/
|
|
62
62
|
export function gmtrPath(...segments: string[]): string {
|
|
63
63
|
return join(getGmtrDir(), ...segments);
|
|
@@ -123,17 +123,17 @@ async function main(): Promise<void> {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Get session state from config
|
|
126
|
-
const sessionEntityId = config?.current_session?.
|
|
126
|
+
const sessionEntityId = config?.current_session?.gramatr_entity_id || '';
|
|
127
127
|
const interactionId = config?.current_session?.interaction_id || '';
|
|
128
128
|
|
|
129
129
|
log('');
|
|
130
130
|
log('Saving session state to gramatr...');
|
|
131
131
|
|
|
132
|
-
// Session lifecycle only —
|
|
133
|
-
// Handoffs are saved by the AGENT in the LEARN phase via
|
|
132
|
+
// Session lifecycle only — gramatr_session_end records git summary on the session entity.
|
|
133
|
+
// Handoffs are saved by the AGENT in the LEARN phase via gramatr_save_handoff (HARD GATE).
|
|
134
134
|
// The hook does NOT save handoffs — it lacks conversation context.
|
|
135
135
|
try {
|
|
136
|
-
const rawResult = await callMcpToolRaw('
|
|
136
|
+
const rawResult = await callMcpToolRaw('gramatr_session_end', {
|
|
137
137
|
entity_id: sessionEntityId || sessionId,
|
|
138
138
|
session_id: sessionId,
|
|
139
139
|
interaction_id: interactionId,
|
|
@@ -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.
|
|
174
|
+
const syncScript = join(process.env.GRAMATR_DIR || join(process.env.HOME || '', 'gmtr-client'), 'hooks', 'sync-ratings.hook.ts');
|
|
175
175
|
if (existsSync(syncScript)) {
|
|
176
176
|
try {
|
|
177
177
|
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
@@ -336,9 +336,9 @@ async function main(): Promise<void> {
|
|
|
336
336
|
log(' - Show observation timestamps');
|
|
337
337
|
log(`3. Load related entities from \`${gmtrConfigPath}\`:`);
|
|
338
338
|
log(' - Check .related_entities for linked databases, people, services, concepts');
|
|
339
|
-
log(' - Optionally fetch details for key related entities (use
|
|
339
|
+
log(' - Optionally fetch details for key related entities (use gramatr_execute_intent with detail_level: summary)');
|
|
340
340
|
log('4. Keep summary concise - just enough context to resume work');
|
|
341
|
-
log(' NOTE: Using intelligent tools (
|
|
341
|
+
log(' NOTE: Using intelligent tools (gramatr_execute_intent) provides 80-95% token reduction vs direct get_entities calls');
|
|
342
342
|
log('');
|
|
343
343
|
}
|
|
344
344
|
|