gramatr 0.3.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/CLAUDE.md +18 -0
- package/README.md +78 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +547 -0
- package/bin/gramatr.js +33 -0
- package/bin/gramatr.ts +248 -0
- package/bin/install.ts +756 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +437 -0
- package/bin/uninstall.ts +289 -0
- package/bin/version-sync.ts +46 -0
- package/codex/README.md +28 -0
- package/codex/hooks/session-start.ts +73 -0
- package/codex/hooks/stop.ts +34 -0
- package/codex/hooks/user-prompt-submit.ts +76 -0
- package/codex/install.ts +99 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +167 -0
- package/core/install.ts +114 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +244 -0
- package/core/routing.ts +98 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +178 -0
- package/core/version.ts +2 -0
- package/gemini/README.md +95 -0
- package/gemini/hooks/session-start.ts +72 -0
- package/gemini/hooks/stop.ts +30 -0
- package/gemini/hooks/user-prompt-submit.ts +74 -0
- package/gemini/install.ts +272 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +650 -0
- package/hooks/GMTRRatingCapture.hook.ts +198 -0
- package/hooks/GMTRSecurityValidator.hook.ts +399 -0
- package/hooks/GMTRToolTracker.hook.ts +181 -0
- package/hooks/StopOrchestrator.hook.ts +78 -0
- package/hooks/gmtr-tool-tracker-utils.ts +105 -0
- package/hooks/lib/gmtr-hook-utils.ts +771 -0
- package/hooks/lib/identity.ts +227 -0
- package/hooks/lib/notify.ts +46 -0
- package/hooks/lib/paths.ts +104 -0
- package/hooks/lib/transcript-parser.ts +452 -0
- package/hooks/session-end.hook.ts +168 -0
- package/hooks/session-start.hook.ts +490 -0
- package/package.json +54 -0
package/bin/install.ts
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gramatr installer — pure TypeScript, no bash dependency.
|
|
4
|
+
* Runs via node (with tsx loader), bun, or npx tsx.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx tsx install.ts # Universal (Node + tsx)
|
|
8
|
+
* bun install.ts # If bun is available
|
|
9
|
+
* GMTR_TOKEN=xxx npx tsx install.ts # Headless with token
|
|
10
|
+
* npx tsx install.ts --yes # Non-interactive (auto-accept defaults)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
existsSync, readFileSync, writeFileSync, mkdirSync, cpSync,
|
|
15
|
+
readdirSync, statSync, chmodSync, appendFileSync, rmSync,
|
|
16
|
+
} from 'fs';
|
|
17
|
+
import { join, dirname, basename, resolve } from 'path';
|
|
18
|
+
import { execSync, spawnSync } from 'child_process';
|
|
19
|
+
import { createInterface } from 'readline';
|
|
20
|
+
import { buildClaudeHooksFile, detectTsRunner } from '../core/install.ts';
|
|
21
|
+
import { VERSION } from '../core/version.ts';
|
|
22
|
+
|
|
23
|
+
// ── Constants ──
|
|
24
|
+
|
|
25
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
26
|
+
const CLAUDE_DIR = join(HOME, '.claude');
|
|
27
|
+
const CLAUDE_SETTINGS = join(CLAUDE_DIR, 'settings.json');
|
|
28
|
+
const CLAUDE_JSON = join(HOME, '.claude.json');
|
|
29
|
+
const CLIENT_DIR = join(HOME, 'gmtr-client');
|
|
30
|
+
const GMTR_JSON = join(HOME, '.gmtr.json');
|
|
31
|
+
const GMTR_BIN = join(HOME, '.gmtr', 'bin');
|
|
32
|
+
const SCRIPT_DIR = dirname(dirname(resolve(import.meta.filename || __filename)));
|
|
33
|
+
const DEFAULT_URL = 'https://api.gramatr.com/mcp';
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const YES = args.includes('--yes') || args.includes('-y');
|
|
37
|
+
const isInteractive = process.stdin.isTTY && !YES;
|
|
38
|
+
|
|
39
|
+
// ── Helpers ──
|
|
40
|
+
|
|
41
|
+
function log(msg: string): void { process.stdout.write(`${msg}\n`); }
|
|
42
|
+
|
|
43
|
+
function readJson(path: string): Record<string, any> {
|
|
44
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeJson(path: string, data: Record<string, any>): void {
|
|
48
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeJson(path: string, fn: (data: Record<string, any>) => Record<string, any>): void {
|
|
52
|
+
const data = readJson(path);
|
|
53
|
+
writeJson(path, fn(data));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function prompt(msg: string, defaultVal?: string): Promise<string> {
|
|
57
|
+
if (!isInteractive) return defaultVal || '';
|
|
58
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const display = defaultVal ? `${msg} [${defaultVal}]: ` : `${msg}: `;
|
|
61
|
+
rl.question(display, (answer) => {
|
|
62
|
+
rl.close();
|
|
63
|
+
resolve(answer.trim() || defaultVal || '');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function confirm(msg: string, defaultYes = true): Promise<boolean> {
|
|
69
|
+
if (YES) return defaultYes;
|
|
70
|
+
if (!isInteractive) return defaultYes;
|
|
71
|
+
const answer = await prompt(`${msg} [${defaultYes ? 'Y/n' : 'y/N'}]`);
|
|
72
|
+
if (!answer) return defaultYes;
|
|
73
|
+
return answer.toLowerCase() === 'y';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countFiles(dir: string, pattern?: RegExp): number {
|
|
77
|
+
try {
|
|
78
|
+
return readdirSync(dir).filter(f => !pattern || pattern.test(f)).length;
|
|
79
|
+
} catch { return 0; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function countFilesRecursive(dir: string): number {
|
|
83
|
+
let count = 0;
|
|
84
|
+
try {
|
|
85
|
+
for (const f of readdirSync(dir)) {
|
|
86
|
+
const p = join(dir, f);
|
|
87
|
+
const s = statSync(p);
|
|
88
|
+
if (s.isDirectory()) count += countFilesRecursive(p);
|
|
89
|
+
else count++;
|
|
90
|
+
}
|
|
91
|
+
} catch { /* ignore */ }
|
|
92
|
+
return count;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function copyDir(src: string, dest: string): void {
|
|
96
|
+
mkdirSync(dest, { recursive: true });
|
|
97
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function dirSize(dir: string): string {
|
|
101
|
+
let total = 0;
|
|
102
|
+
const walk = (d: string) => {
|
|
103
|
+
try {
|
|
104
|
+
for (const f of readdirSync(d)) {
|
|
105
|
+
const p = join(d, f);
|
|
106
|
+
const s = statSync(p);
|
|
107
|
+
if (s.isDirectory()) walk(p); else total += s.size;
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
};
|
|
111
|
+
walk(dir);
|
|
112
|
+
if (total > 1048576) return `${(total / 1048576).toFixed(1)}M`;
|
|
113
|
+
if (total > 1024) return `${(total / 1024).toFixed(0)}K`;
|
|
114
|
+
return `${total}B`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function which(cmd: string): string | null {
|
|
118
|
+
try {
|
|
119
|
+
return execSync(`command -v ${cmd}`, { encoding: 'utf8' }).trim() || null;
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function copyFileIfExists(src: string, dest: string, executable = false): boolean {
|
|
124
|
+
if (!existsSync(src)) return false;
|
|
125
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
126
|
+
cpSync(src, dest, { force: true });
|
|
127
|
+
if (executable) chmodSync(dest, 0o755);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function timestamp(): string {
|
|
132
|
+
return new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Prerequisites ──
|
|
136
|
+
|
|
137
|
+
async function checkPrereqs(): Promise<{ tsRunner: string }> {
|
|
138
|
+
// Node.js
|
|
139
|
+
const nodeVer = process.versions.node;
|
|
140
|
+
const major = parseInt(nodeVer.split('.')[0], 10);
|
|
141
|
+
if (major < 20) {
|
|
142
|
+
log(`X Node.js 20+ required (found: ${nodeVer})`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
log(`OK Node.js v${nodeVer}`);
|
|
146
|
+
|
|
147
|
+
// Detect best TypeScript runner
|
|
148
|
+
const tsRunner = detectTsRunner();
|
|
149
|
+
if (tsRunner === 'bun') {
|
|
150
|
+
try {
|
|
151
|
+
const ver = execSync('bun --version', { encoding: 'utf8' }).trim();
|
|
152
|
+
log(`OK TS runner: bun ${ver}`);
|
|
153
|
+
} catch {
|
|
154
|
+
log(`OK TS runner: bun`);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
log(`OK TS runner: npx tsx (install bun for faster hooks)`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Ensure ~/.gmtr/bin exists and is on PATH
|
|
161
|
+
mkdirSync(GMTR_BIN, { recursive: true });
|
|
162
|
+
const pathLine = 'export PATH="$HOME/.gmtr/bin:$PATH"';
|
|
163
|
+
for (const rc of ['.zshrc', '.bashrc']) {
|
|
164
|
+
const rcPath = join(HOME, rc);
|
|
165
|
+
if (existsSync(rcPath)) {
|
|
166
|
+
const content = readFileSync(rcPath, 'utf8');
|
|
167
|
+
if (!content.includes('.gmtr/bin')) {
|
|
168
|
+
appendFileSync(rcPath, `\n# gramatr — local tool binaries\n${pathLine}\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Claude Code
|
|
174
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
175
|
+
log('X Claude Code not found (~/.claude/ does not exist)');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
log('OK Claude Code directory exists');
|
|
179
|
+
|
|
180
|
+
return { tsRunner };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Legacy Detection ──
|
|
184
|
+
|
|
185
|
+
async function handleLegacy(): Promise<{ legacyToken: string }> {
|
|
186
|
+
let legacyToken = '';
|
|
187
|
+
|
|
188
|
+
// Check for aios MCP server
|
|
189
|
+
if (existsSync(CLAUDE_JSON)) {
|
|
190
|
+
const claudeJson = readJson(CLAUDE_JSON);
|
|
191
|
+
const aiosUrl = claudeJson.mcpServers?.aios?.url;
|
|
192
|
+
const aiosAuth = claudeJson.mcpServers?.aios?.headers?.Authorization;
|
|
193
|
+
const aiosToken = aiosAuth?.replace('Bearer ', '') || '';
|
|
194
|
+
|
|
195
|
+
if (aiosUrl) {
|
|
196
|
+
log('━━━ Legacy Installation Detected ━━━');
|
|
197
|
+
log('');
|
|
198
|
+
log(` Found: aios MCP server`);
|
|
199
|
+
log(` URL: ${aiosUrl}`);
|
|
200
|
+
if (aiosToken) log(` Token: ${aiosToken.slice(0, 20)}... (present)`);
|
|
201
|
+
log('');
|
|
202
|
+
|
|
203
|
+
if (await confirm(' Migrate to gramatr? This removes aios config and reuses your auth token.')) {
|
|
204
|
+
if (aiosToken) {
|
|
205
|
+
legacyToken = aiosToken;
|
|
206
|
+
log(' OK Extracted auth token from aios config');
|
|
207
|
+
}
|
|
208
|
+
delete claudeJson.mcpServers.aios;
|
|
209
|
+
writeJson(CLAUDE_JSON, claudeJson);
|
|
210
|
+
log(' OK Removed aios MCP server from ~/.claude.json');
|
|
211
|
+
|
|
212
|
+
const oldClient = join(HOME, 'aios-v2-client');
|
|
213
|
+
if (existsSync(oldClient)) {
|
|
214
|
+
rmSync(oldClient, { recursive: true, force: true });
|
|
215
|
+
log(' OK Removed ~/aios-v2-client/');
|
|
216
|
+
}
|
|
217
|
+
log('');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check for PAI
|
|
223
|
+
const paiDir = join(CLAUDE_DIR, 'skills', 'PAI');
|
|
224
|
+
if (existsSync(paiDir)) {
|
|
225
|
+
log('━━━ PAI Installation Detected ━━━');
|
|
226
|
+
log(` Size: ${dirSize(paiDir)}`);
|
|
227
|
+
log('');
|
|
228
|
+
if (await confirm(' Remove PAI? gramatr provides all PAI capabilities server-side.')) {
|
|
229
|
+
rmSync(paiDir, { recursive: true, force: true });
|
|
230
|
+
log(' OK Removed ~/.claude/skills/PAI/');
|
|
231
|
+
}
|
|
232
|
+
log('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check for Fabric
|
|
236
|
+
const fabricDir = join(CLAUDE_DIR, 'skills', 'Fabric');
|
|
237
|
+
if (existsSync(fabricDir)) {
|
|
238
|
+
log('━━━ Fabric Installation Detected ━━━');
|
|
239
|
+
log(` Size: ${dirSize(fabricDir)}`);
|
|
240
|
+
log('');
|
|
241
|
+
if (await confirm(' Remove Fabric skill directory? (CLI tool preserved)')) {
|
|
242
|
+
rmSync(fabricDir, { recursive: true, force: true });
|
|
243
|
+
log(' OK Removed ~/.claude/skills/Fabric/');
|
|
244
|
+
}
|
|
245
|
+
log('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { legacyToken };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Step 1: Copy client files ──
|
|
252
|
+
|
|
253
|
+
function installClientFiles(): void {
|
|
254
|
+
log('━━━ Step 1: Installing client files ━━━');
|
|
255
|
+
log('');
|
|
256
|
+
|
|
257
|
+
// Create directory structure
|
|
258
|
+
for (const sub of ['hooks/lib', 'bin', 'core']) {
|
|
259
|
+
mkdirSync(join(CLIENT_DIR, sub), { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Hooks (7 core + utils)
|
|
263
|
+
let hookCount = 0;
|
|
264
|
+
const hooksSrc = join(SCRIPT_DIR, 'hooks');
|
|
265
|
+
if (existsSync(hooksSrc)) {
|
|
266
|
+
for (const f of readdirSync(hooksSrc)) {
|
|
267
|
+
const src = join(hooksSrc, f);
|
|
268
|
+
if (statSync(src).isFile() && (f.endsWith('.hook.ts') || f.endsWith('-utils.ts'))) {
|
|
269
|
+
cpSync(src, join(CLIENT_DIR, 'hooks', f));
|
|
270
|
+
chmodSync(join(CLIENT_DIR, 'hooks', f), 0o755);
|
|
271
|
+
hookCount++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Hook lib (paths, identity, notify, gmtr-hook-utils, transcript-parser)
|
|
276
|
+
const libHookSrc = join(hooksSrc, 'lib');
|
|
277
|
+
if (existsSync(libHookSrc)) {
|
|
278
|
+
const libs = readdirSync(libHookSrc).filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
|
279
|
+
for (const f of libs) cpSync(join(libHookSrc, f), join(CLIENT_DIR, 'hooks/lib', f));
|
|
280
|
+
log(`OK Installed ${hookCount} hooks + ${libs.length} lib modules`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Bin files
|
|
285
|
+
copyFileIfExists(join(SCRIPT_DIR, 'bin/statusline.ts'), join(CLIENT_DIR, 'bin/statusline.ts'), true);
|
|
286
|
+
copyFileIfExists(join(SCRIPT_DIR, 'bin/gmtr-login.ts'), join(CLIENT_DIR, 'bin/gmtr-login.ts'), true);
|
|
287
|
+
copyFileIfExists(join(SCRIPT_DIR, 'bin/render-claude-hooks.ts'), join(CLIENT_DIR, 'bin/render-claude-hooks.ts'), true);
|
|
288
|
+
log('OK Installed bin (statusline, gmtr-login, render-claude-hooks)');
|
|
289
|
+
|
|
290
|
+
// Core dependencies (routing.ts required by GMTRPromptEnricher hook)
|
|
291
|
+
for (const f of ['install.ts', 'version.ts', 'types.ts', 'routing.ts']) {
|
|
292
|
+
copyFileIfExists(join(SCRIPT_DIR, 'core', f), join(CLIENT_DIR, 'core', f));
|
|
293
|
+
}
|
|
294
|
+
log('OK Installed core modules');
|
|
295
|
+
|
|
296
|
+
// CLAUDE.md
|
|
297
|
+
copyFileIfExists(join(SCRIPT_DIR, 'CLAUDE.md'), join(CLIENT_DIR, 'CLAUDE.md'));
|
|
298
|
+
log('OK Installed CLAUDE.md (minimal — server delivers behavioral rules)');
|
|
299
|
+
|
|
300
|
+
log('');
|
|
301
|
+
log(` Thin client: ${hookCount} hooks, server delivers everything else`);
|
|
302
|
+
log('');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Step 1b: CLAUDE.md behavioral framework ──
|
|
306
|
+
|
|
307
|
+
function installClaudeMd(): void {
|
|
308
|
+
log('━━━ Step 1b: Installing gramatr behavioral framework ━━━');
|
|
309
|
+
log('');
|
|
310
|
+
|
|
311
|
+
const claudeMd = join(HOME, '.claude', 'CLAUDE.md');
|
|
312
|
+
const gmtrSource = join(CLIENT_DIR, 'CLAUDE.md');
|
|
313
|
+
const GMTR_START = '<!-- GMTR-START';
|
|
314
|
+
const GMTR_END = '<!-- GMTR-END -->';
|
|
315
|
+
|
|
316
|
+
if (!existsSync(gmtrSource)) {
|
|
317
|
+
log('X CLAUDE.md source not found');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const sourceContent = readFileSync(gmtrSource, 'utf8');
|
|
322
|
+
|
|
323
|
+
if (existsSync(claudeMd)) {
|
|
324
|
+
let content = readFileSync(claudeMd, 'utf8');
|
|
325
|
+
|
|
326
|
+
// Clean PAI directives
|
|
327
|
+
if (content.includes('read skills/PAI')) {
|
|
328
|
+
content = content.split('\n')
|
|
329
|
+
.filter(l => !l.includes('read skills/PAI') && !l.includes('# Read the PAI system'))
|
|
330
|
+
.join('\n');
|
|
331
|
+
}
|
|
332
|
+
if (content.startsWith('This file does nothing.')) {
|
|
333
|
+
content = content.replace(/^This file does nothing\.\n?/, '');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (content.includes(GMTR_START)) {
|
|
337
|
+
// Replace existing GMTR section
|
|
338
|
+
const startIdx = content.indexOf(GMTR_START);
|
|
339
|
+
const endIdx = content.indexOf(GMTR_END);
|
|
340
|
+
if (endIdx > startIdx) {
|
|
341
|
+
content = content.slice(0, startIdx) + sourceContent + content.slice(endIdx + GMTR_END.length);
|
|
342
|
+
}
|
|
343
|
+
log('OK Updated GMTR section in existing CLAUDE.md');
|
|
344
|
+
} else {
|
|
345
|
+
content += '\n' + sourceContent;
|
|
346
|
+
log('OK Appended GMTR section to existing CLAUDE.md');
|
|
347
|
+
}
|
|
348
|
+
writeFileSync(claudeMd, content);
|
|
349
|
+
} else {
|
|
350
|
+
writeFileSync(claudeMd, sourceContent);
|
|
351
|
+
log('OK Created CLAUDE.md with gramatr behavioral framework');
|
|
352
|
+
}
|
|
353
|
+
log('');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Step 2: (removed — skills/agents now server-side) ──
|
|
357
|
+
|
|
358
|
+
// ── Step 3: Auth ──
|
|
359
|
+
|
|
360
|
+
async function handleAuth(legacyToken: string, tsRunner: string): Promise<{ url: string; token: string }> {
|
|
361
|
+
log('━━━ Step 3: Configuring gramatr MCP server ━━━');
|
|
362
|
+
log('');
|
|
363
|
+
|
|
364
|
+
const url = await prompt('gramatr server URL', DEFAULT_URL) || DEFAULT_URL;
|
|
365
|
+
|
|
366
|
+
// Token priority: env > ~/.gmtr.json > legacy > login
|
|
367
|
+
let token = process.env.GMTR_TOKEN || '';
|
|
368
|
+
|
|
369
|
+
if (token) {
|
|
370
|
+
log('OK Using GMTR_TOKEN from environment');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!token && existsSync(GMTR_JSON)) {
|
|
374
|
+
const existing = readJson(GMTR_JSON);
|
|
375
|
+
if (existing.token) {
|
|
376
|
+
token = existing.token;
|
|
377
|
+
log('OK Found existing auth token in ~/.gmtr.json');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!token && legacyToken) {
|
|
382
|
+
token = legacyToken;
|
|
383
|
+
log('OK Reusing auth token from legacy aios installation');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// No token — run gmtr-login directly (imported, not subprocess)
|
|
387
|
+
if (!token && isInteractive) {
|
|
388
|
+
log('');
|
|
389
|
+
log(' No auth token found. Starting gramatr login...');
|
|
390
|
+
log('');
|
|
391
|
+
|
|
392
|
+
const loginScript = join(CLIENT_DIR, 'bin', 'gmtr-login.ts');
|
|
393
|
+
if (existsSync(loginScript)) {
|
|
394
|
+
// Run gmtr-login as subprocess but with proper stdio handling
|
|
395
|
+
// Use spawnSync so stdin is properly passed through (no stall)
|
|
396
|
+
const result = spawnSync(tsRunner, [loginScript], {
|
|
397
|
+
stdio: 'inherit',
|
|
398
|
+
env: { ...process.env },
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Re-read token after login
|
|
402
|
+
if (existsSync(GMTR_JSON)) {
|
|
403
|
+
const data = readJson(GMTR_JSON);
|
|
404
|
+
if (data.token) token = data.token;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!token) {
|
|
409
|
+
log(' Authentication skipped — run /gmtr-login later to authenticate');
|
|
410
|
+
}
|
|
411
|
+
log('');
|
|
412
|
+
} else if (!token) {
|
|
413
|
+
log('');
|
|
414
|
+
log('Non-interactive: no auth token. Set GMTR_TOKEN env var or run gmtr-login after install.');
|
|
415
|
+
log('');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { url, token };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ── Step 3b: Identity config ──
|
|
422
|
+
|
|
423
|
+
async function configureIdentity(): Promise<void> {
|
|
424
|
+
log('━━━ Step 3b: Creating gramatr identity config ━━━');
|
|
425
|
+
log('');
|
|
426
|
+
|
|
427
|
+
const settingsPath = join(CLIENT_DIR, 'settings.json');
|
|
428
|
+
|
|
429
|
+
if (!existsSync(settingsPath)) {
|
|
430
|
+
writeJson(settingsPath, {
|
|
431
|
+
daidentity: {
|
|
432
|
+
name: 'gramatr',
|
|
433
|
+
fullName: 'gramatr — Personal AI',
|
|
434
|
+
displayName: 'gramatr',
|
|
435
|
+
color: '#3B82F6',
|
|
436
|
+
},
|
|
437
|
+
principal: {
|
|
438
|
+
name: 'User',
|
|
439
|
+
timezone: 'UTC',
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
log(`OK Created ${settingsPath}`);
|
|
443
|
+
} else {
|
|
444
|
+
log('OK gramatr settings.json exists — preserving identity');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const settings = readJson(settingsPath);
|
|
448
|
+
const currentName = settings.principal?.name || 'User';
|
|
449
|
+
|
|
450
|
+
if (currentName === 'User' && isInteractive) {
|
|
451
|
+
const name = await prompt(' What is your name? (shown in gramatr responses)');
|
|
452
|
+
if (name) {
|
|
453
|
+
settings.principal = settings.principal || {};
|
|
454
|
+
settings.principal.name = name;
|
|
455
|
+
log(` OK Set principal.name to "${name}"`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const tz = await prompt(' Your timezone?', 'America/Chicago');
|
|
459
|
+
settings.principal = settings.principal || {};
|
|
460
|
+
settings.principal.timezone = tz;
|
|
461
|
+
log(` OK Set timezone to "${tz}"`);
|
|
462
|
+
|
|
463
|
+
writeJson(settingsPath, settings);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
log('');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Step 4: Settings merge ──
|
|
470
|
+
|
|
471
|
+
function updateClaudeSettings(tsRunner: string, url: string, token: string): void {
|
|
472
|
+
log('━━━ Step 4: Updating Claude Code settings ━━━');
|
|
473
|
+
log('');
|
|
474
|
+
|
|
475
|
+
if (!existsSync(CLAUDE_SETTINGS)) {
|
|
476
|
+
writeJson(CLAUDE_SETTINGS, {});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Backup
|
|
480
|
+
const backup = `${CLAUDE_SETTINGS}.backup-${timestamp()}`;
|
|
481
|
+
cpSync(CLAUDE_SETTINGS, backup);
|
|
482
|
+
log(`OK Backed up settings to ${basename(backup)}`);
|
|
483
|
+
|
|
484
|
+
const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
485
|
+
const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx, tsRunner });
|
|
486
|
+
|
|
487
|
+
const settings = readJson(CLAUDE_SETTINGS);
|
|
488
|
+
|
|
489
|
+
// Env vars
|
|
490
|
+
settings.env = settings.env || {};
|
|
491
|
+
settings.env.GMTR_DIR = CLIENT_DIR;
|
|
492
|
+
settings.env.GMTR_URL = url;
|
|
493
|
+
settings.env.PATH = `${HOME}/.gmtr/bin:/usr/local/bin:/usr/bin:/bin`;
|
|
494
|
+
if (token) settings.env.AIOS_MCP_TOKEN = token;
|
|
495
|
+
|
|
496
|
+
// Identity defaults (don't overwrite)
|
|
497
|
+
if (!settings.daidentity) {
|
|
498
|
+
settings.daidentity = {
|
|
499
|
+
name: 'gramatr',
|
|
500
|
+
fullName: 'gramatr — AI Intelligence Layer',
|
|
501
|
+
displayName: 'gramatr',
|
|
502
|
+
color: '#3B82F6',
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (!settings.principal) {
|
|
506
|
+
settings.principal = { name: 'User', timezone: 'UTC' };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Hooks
|
|
510
|
+
settings.hooks = hooksConfig.hooks;
|
|
511
|
+
log('OK Wired managed gramatr hooks from shared install manifest');
|
|
512
|
+
if (includeOptionalUx) {
|
|
513
|
+
log('OK Optional Claude UX hooks enabled');
|
|
514
|
+
} else {
|
|
515
|
+
log('OK Thin-client hook set (set GMTR_ENABLE_OPTIONAL_CLAUDE_UX=1 for optional hooks)');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Status line
|
|
519
|
+
settings.statusLine = {
|
|
520
|
+
type: 'command',
|
|
521
|
+
command: `${tsRunner} ${CLIENT_DIR}/bin/statusline.ts`,
|
|
522
|
+
};
|
|
523
|
+
log('OK Configured status line');
|
|
524
|
+
|
|
525
|
+
writeJson(CLAUDE_SETTINGS, settings);
|
|
526
|
+
log('');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── Step 4b: MCP server registration ──
|
|
530
|
+
|
|
531
|
+
function registerMcpServer(url: string, token: string): void {
|
|
532
|
+
log('━━━ Step 4b: Registering MCP server in ~/.claude.json ━━━');
|
|
533
|
+
log('');
|
|
534
|
+
|
|
535
|
+
if (!existsSync(CLAUDE_JSON)) writeJson(CLAUDE_JSON, {});
|
|
536
|
+
|
|
537
|
+
const backup = `${CLAUDE_JSON}.backup-${timestamp()}`;
|
|
538
|
+
cpSync(CLAUDE_JSON, backup);
|
|
539
|
+
|
|
540
|
+
// Store token in ~/.gmtr.json
|
|
541
|
+
if (token) {
|
|
542
|
+
mergeJson(GMTR_JSON, (data) => ({
|
|
543
|
+
...data,
|
|
544
|
+
token,
|
|
545
|
+
token_updated_at: new Date().toISOString(),
|
|
546
|
+
}));
|
|
547
|
+
try { chmodSync(GMTR_JSON, 0o600); } catch { /* ok */ }
|
|
548
|
+
log('OK Token stored in ~/.gmtr.json (canonical source)');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Write tool paths to ~/.gmtr.json
|
|
552
|
+
mergeJson(GMTR_JSON, (data) => ({
|
|
553
|
+
...data,
|
|
554
|
+
jq_binary: which('jq') || '',
|
|
555
|
+
bun_binary: which('bun') || '',
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
// Register in ~/.claude.json
|
|
559
|
+
mergeJson(CLAUDE_JSON, (data) => {
|
|
560
|
+
data.env = data.env || {};
|
|
561
|
+
if (token) data.env.GMTR_TOKEN = token;
|
|
562
|
+
delete data.env.AIOS_MCP_TOKEN;
|
|
563
|
+
|
|
564
|
+
data.mcpServers = data.mcpServers || {};
|
|
565
|
+
data.mcpServers.gramatr = {
|
|
566
|
+
type: 'http',
|
|
567
|
+
url,
|
|
568
|
+
headers: { Authorization: 'Bearer ${GMTR_TOKEN}' },
|
|
569
|
+
autoApprove: true,
|
|
570
|
+
};
|
|
571
|
+
return data;
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
log(`OK Registered MCP server 'gramatr' in ~/.claude.json -> ${url}`);
|
|
575
|
+
log('');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── Step 4c: Additional CLIs ──
|
|
579
|
+
|
|
580
|
+
function installAdditionalClis(tsRunner: string): void {
|
|
581
|
+
log('━━━ Step 4c: Additional CLI platforms ━━━');
|
|
582
|
+
log('');
|
|
583
|
+
|
|
584
|
+
// Codex
|
|
585
|
+
const codexDir = join(HOME, '.codex');
|
|
586
|
+
if (existsSync(codexDir) || which('codex')) {
|
|
587
|
+
log(' Codex CLI detected — installing gramatr hooks...');
|
|
588
|
+
const codexInstall = join(SCRIPT_DIR, 'codex', 'install.ts');
|
|
589
|
+
if (existsSync(codexInstall)) {
|
|
590
|
+
const result = spawnSync(tsRunner, [codexInstall], { stdio: 'inherit' });
|
|
591
|
+
if (result.status === 0) {
|
|
592
|
+
log(' OK Codex hooks installed');
|
|
593
|
+
} else {
|
|
594
|
+
log(' X Codex install failed (non-fatal)');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
log('');
|
|
598
|
+
} else {
|
|
599
|
+
log(' -- Codex CLI not detected (skipping)');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Gemini
|
|
603
|
+
const geminiDir = join(HOME, '.gemini');
|
|
604
|
+
if (existsSync(geminiDir) || which('gemini')) {
|
|
605
|
+
log(' Gemini CLI detected — installing gramatr extension...');
|
|
606
|
+
const geminiInstall = join(SCRIPT_DIR, 'gemini', 'install.ts');
|
|
607
|
+
if (existsSync(geminiInstall)) {
|
|
608
|
+
const result = spawnSync(tsRunner, [geminiInstall], { stdio: 'inherit' });
|
|
609
|
+
if (result.status === 0) {
|
|
610
|
+
log(' OK Gemini extension installed');
|
|
611
|
+
} else {
|
|
612
|
+
log(' X Gemini install failed (non-fatal)');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
log('');
|
|
616
|
+
} else {
|
|
617
|
+
log(' -- Gemini CLI not detected (skipping)');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
log('');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Step 5: Verification ──
|
|
624
|
+
|
|
625
|
+
function verify(url: string, token: string): boolean {
|
|
626
|
+
log('━━━ Step 5: Verification ━━━');
|
|
627
|
+
log('');
|
|
628
|
+
|
|
629
|
+
let allOk = true;
|
|
630
|
+
const check = (condition: boolean, ok: string, fail: string) => {
|
|
631
|
+
if (condition) {
|
|
632
|
+
log(`OK ${ok}`);
|
|
633
|
+
} else {
|
|
634
|
+
log(`X ${fail}`);
|
|
635
|
+
allOk = false;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Critical files
|
|
640
|
+
for (const f of [
|
|
641
|
+
'hooks/GMTRToolTracker.hook.ts',
|
|
642
|
+
'hooks/GMTRPromptEnricher.hook.ts',
|
|
643
|
+
'hooks/GMTRRatingCapture.hook.ts',
|
|
644
|
+
'hooks/GMTRSecurityValidator.hook.ts',
|
|
645
|
+
'hooks/lib/notify.ts',
|
|
646
|
+
'core/routing.ts',
|
|
647
|
+
'bin/statusline.ts',
|
|
648
|
+
'bin/gmtr-login.ts',
|
|
649
|
+
'CLAUDE.md',
|
|
650
|
+
]) {
|
|
651
|
+
check(existsSync(join(CLIENT_DIR, f)), f, `${f} MISSING`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Hook lib
|
|
655
|
+
for (const f of ['hooks/lib/identity.ts', 'hooks/lib/paths.ts']) {
|
|
656
|
+
check(existsSync(join(CLIENT_DIR, f)), f, `${f} MISSING`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Settings hooks
|
|
660
|
+
const settings = readJson(CLAUDE_SETTINGS);
|
|
661
|
+
const hookCounts = {
|
|
662
|
+
PreToolUse: (settings.hooks?.PreToolUse || []).length,
|
|
663
|
+
PostToolUse: (settings.hooks?.PostToolUse || []).length,
|
|
664
|
+
UserPromptSubmit: (settings.hooks?.UserPromptSubmit || []).length,
|
|
665
|
+
SessionStart: (settings.hooks?.SessionStart || []).length,
|
|
666
|
+
SessionEnd: (settings.hooks?.SessionEnd || []).length,
|
|
667
|
+
Stop: (settings.hooks?.Stop || []).length,
|
|
668
|
+
};
|
|
669
|
+
check(hookCounts.PreToolUse >= 4, `PreToolUse: ${hookCounts.PreToolUse} hooks`, `PreToolUse hooks missing (got ${hookCounts.PreToolUse}, need 4)`);
|
|
670
|
+
check(hookCounts.PostToolUse >= 1, `PostToolUse: ${hookCounts.PostToolUse} hooks`, `PostToolUse hooks missing (got ${hookCounts.PostToolUse}, need 1)`);
|
|
671
|
+
check(hookCounts.UserPromptSubmit >= 2, `UserPromptSubmit: ${hookCounts.UserPromptSubmit} groups`, `UserPromptSubmit hooks missing (got ${hookCounts.UserPromptSubmit}, need 2)`);
|
|
672
|
+
check(hookCounts.SessionStart >= 1, `SessionStart: ${hookCounts.SessionStart}`, `SessionStart hooks missing`);
|
|
673
|
+
check(hookCounts.SessionEnd >= 1, `SessionEnd: ${hookCounts.SessionEnd}`, `SessionEnd hooks missing`);
|
|
674
|
+
check(hookCounts.Stop >= 1, `Stop: ${hookCounts.Stop}`, `Stop hooks missing`);
|
|
675
|
+
|
|
676
|
+
// MCP server
|
|
677
|
+
const claudeJson = readJson(CLAUDE_JSON);
|
|
678
|
+
check(!!claudeJson.mcpServers?.gramatr, "MCP server 'gramatr' registered", 'MCP server missing');
|
|
679
|
+
|
|
680
|
+
// Status line
|
|
681
|
+
check(!!settings.statusLine?.command, 'Status line configured', 'Status line missing');
|
|
682
|
+
|
|
683
|
+
// No stale paths
|
|
684
|
+
const settingsStr = JSON.stringify(settings);
|
|
685
|
+
check(
|
|
686
|
+
!settingsStr.includes('PAI_DIR') && !settingsStr.includes('aios-v2-client'),
|
|
687
|
+
'No stale PAI_DIR or aios-v2-client paths',
|
|
688
|
+
'Stale PAI/AIOS paths found',
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// CLAUDE.md
|
|
692
|
+
const claudeMd = join(HOME, '.claude', 'CLAUDE.md');
|
|
693
|
+
check(
|
|
694
|
+
existsSync(claudeMd) && readFileSync(claudeMd, 'utf8').includes('GMTR-START'),
|
|
695
|
+
'CLAUDE.md contains gramatr behavioral framework',
|
|
696
|
+
'CLAUDE.md missing GMTR section',
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
log('');
|
|
700
|
+
return allOk;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ── Main ──
|
|
704
|
+
|
|
705
|
+
async function main(): Promise<void> {
|
|
706
|
+
log('');
|
|
707
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
708
|
+
log(` gramatr v${VERSION} — Intelligence Layer`);
|
|
709
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
710
|
+
log('');
|
|
711
|
+
|
|
712
|
+
const { tsRunner } = await checkPrereqs();
|
|
713
|
+
log('');
|
|
714
|
+
|
|
715
|
+
const { legacyToken } = await handleLegacy();
|
|
716
|
+
|
|
717
|
+
installClientFiles();
|
|
718
|
+
installClaudeMd();
|
|
719
|
+
|
|
720
|
+
const { url, token } = await handleAuth(legacyToken, tsRunner);
|
|
721
|
+
await configureIdentity();
|
|
722
|
+
updateClaudeSettings(tsRunner, url, token);
|
|
723
|
+
registerMcpServer(url, token);
|
|
724
|
+
installAdditionalClis(tsRunner);
|
|
725
|
+
|
|
726
|
+
const allOk = verify(url, token);
|
|
727
|
+
|
|
728
|
+
const totalFiles = countFilesRecursive(CLIENT_DIR);
|
|
729
|
+
|
|
730
|
+
if (allOk) {
|
|
731
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
732
|
+
log(' gramatr Client installed successfully!');
|
|
733
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
734
|
+
log('');
|
|
735
|
+
log(` Installed to: ${CLIENT_DIR} (${totalFiles} files)`);
|
|
736
|
+
log(` MCP server: ${url}`);
|
|
737
|
+
log('');
|
|
738
|
+
log(' Next steps:');
|
|
739
|
+
log(' 1. Restart Claude Code to pick up MCP server config');
|
|
740
|
+
if (!token) {
|
|
741
|
+
log(' 2. Run /gmtr-login in Claude Code to authenticate');
|
|
742
|
+
} else {
|
|
743
|
+
log(' 2. Already authenticated (token found in ~/.gmtr.json)');
|
|
744
|
+
}
|
|
745
|
+
log('');
|
|
746
|
+
} else {
|
|
747
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
748
|
+
log(' Install completed with warnings');
|
|
749
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
main().catch((err) => {
|
|
754
|
+
log(`\nX Install failed: ${err.message}\n`);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
});
|