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.
Files changed (50) hide show
  1. package/CLAUDE.md +18 -0
  2. package/README.md +78 -0
  3. package/bin/clean-legacy-install.ts +28 -0
  4. package/bin/get-token.py +3 -0
  5. package/bin/gmtr-login.ts +547 -0
  6. package/bin/gramatr.js +33 -0
  7. package/bin/gramatr.ts +248 -0
  8. package/bin/install.ts +756 -0
  9. package/bin/render-claude-hooks.ts +16 -0
  10. package/bin/statusline.ts +437 -0
  11. package/bin/uninstall.ts +289 -0
  12. package/bin/version-sync.ts +46 -0
  13. package/codex/README.md +28 -0
  14. package/codex/hooks/session-start.ts +73 -0
  15. package/codex/hooks/stop.ts +34 -0
  16. package/codex/hooks/user-prompt-submit.ts +76 -0
  17. package/codex/install.ts +99 -0
  18. package/codex/lib/codex-hook-utils.ts +48 -0
  19. package/codex/lib/codex-install-utils.ts +123 -0
  20. package/core/feedback.ts +55 -0
  21. package/core/formatting.ts +167 -0
  22. package/core/install.ts +114 -0
  23. package/core/installer-cli.ts +122 -0
  24. package/core/migration.ts +244 -0
  25. package/core/routing.ts +98 -0
  26. package/core/session.ts +202 -0
  27. package/core/targets.ts +292 -0
  28. package/core/types.ts +178 -0
  29. package/core/version.ts +2 -0
  30. package/gemini/README.md +95 -0
  31. package/gemini/hooks/session-start.ts +72 -0
  32. package/gemini/hooks/stop.ts +30 -0
  33. package/gemini/hooks/user-prompt-submit.ts +74 -0
  34. package/gemini/install.ts +272 -0
  35. package/gemini/lib/gemini-hook-utils.ts +63 -0
  36. package/gemini/lib/gemini-install-utils.ts +169 -0
  37. package/hooks/GMTRPromptEnricher.hook.ts +650 -0
  38. package/hooks/GMTRRatingCapture.hook.ts +198 -0
  39. package/hooks/GMTRSecurityValidator.hook.ts +399 -0
  40. package/hooks/GMTRToolTracker.hook.ts +181 -0
  41. package/hooks/StopOrchestrator.hook.ts +78 -0
  42. package/hooks/gmtr-tool-tracker-utils.ts +105 -0
  43. package/hooks/lib/gmtr-hook-utils.ts +771 -0
  44. package/hooks/lib/identity.ts +227 -0
  45. package/hooks/lib/notify.ts +46 -0
  46. package/hooks/lib/paths.ts +104 -0
  47. package/hooks/lib/transcript-parser.ts +452 -0
  48. package/hooks/session-end.hook.ts +168 -0
  49. package/hooks/session-start.hook.ts +490 -0
  50. package/package.json +54 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,18 @@
1
+ <!-- GMTR-START — Do not edit between these markers. Managed by gramatr installer. -->
2
+ # gramatr
3
+
4
+ You have gramatr installed. A hook pre-classifies every user request and injects
5
+ intelligence as `[GMTR Intelligence — ...]` into your context.
6
+
7
+ **Follow the intelligence packet.** It contains behavioral directives, effort level,
8
+ ISC scaffold, capability audit, phase templates, and composed agents.
9
+
10
+ **Memory:** Use gramatr MCP tools (`search_semantic`, `create_entity`, `add_observation`),
11
+ not local markdown files.
12
+
13
+ **Identity:** Read from `~/gmtr-client/settings.json` — `daidentity` for your name,
14
+ `principal` for the user's name.
15
+
16
+ **If the server is unreachable:** Use 7-phase structure (OBSERVE → THINK → PLAN → BUILD →
17
+ EXECUTE → VERIFY → LEARN). Create ISC before work. Never combine phases.
18
+ <!-- GMTR-END -->
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # grāmatr Client Package
2
+
3
+ This package contains the shared grāmatr client runtime plus per-target adapters.
4
+
5
+ Current target model:
6
+
7
+ - local / thick-client targets
8
+ - `claude-code`
9
+ - `codex`
10
+ - hosted / remote targets
11
+ - `remote-mcp`
12
+ - `claude-web`
13
+ - `chatgpt-web`
14
+
15
+ The important distinction is that hosted targets are not local hook installs. They use remote MCP or future hosted-client packaging.
16
+
17
+ ## CLI
18
+
19
+ The package exposes a `gramatr` CLI:
20
+
21
+ ```bash
22
+ gramatr install
23
+ gramatr install claude-code
24
+ gramatr install codex
25
+ gramatr install all
26
+ gramatr detect
27
+ gramatr doctor
28
+ gramatr migrate
29
+ gramatr migrate --apply
30
+ gramatr upgrade
31
+ ```
32
+
33
+ ## Command Behavior
34
+
35
+ - `gramatr install`
36
+ - interactive local target selection
37
+ - installs local adapter(s)
38
+ - prints remote target guidance separately
39
+ - `gramatr detect`
40
+ - reports detected local targets and known hosted targets
41
+ - `gramatr doctor`
42
+ - reports current target detection, payload presence, and stale legacy artifacts
43
+ - `gramatr migrate`
44
+ - dry-run legacy cleanup and Claude config sanitization
45
+ - `gramatr migrate --apply`
46
+ - applies legacy cleanup and configuration sanitization
47
+ - `gramatr upgrade`
48
+ - performs migration cleanup when needed, then re-syncs detected local targets
49
+
50
+ ## Thin-Client Default
51
+
52
+ Claude installs now default to the thin-client hook set.
53
+
54
+ Optional Claude UX hooks are disabled by default and can be enabled with:
55
+
56
+ ```bash
57
+ GMTR_ENABLE_OPTIONAL_CLAUDE_UX=1
58
+ ```
59
+
60
+ ## Development
61
+
62
+ Install dependencies:
63
+
64
+ ```bash
65
+ pnpm install --frozen-lockfile
66
+ ```
67
+
68
+ Run package tests:
69
+
70
+ ```bash
71
+ pnpm --filter @aios-v2/client test
72
+ ```
73
+
74
+ Run target detection locally:
75
+
76
+ ```bash
77
+ pnpm --filter @aios-v2/client exec bun bin/gramatr.ts detect
78
+ ```
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from 'path';
4
+ import { runLegacyMigration } from '../core/migration.ts';
5
+
6
+ function log(message: string): void {
7
+ process.stdout.write(`${message}\n`);
8
+ }
9
+
10
+ function main(): void {
11
+ const home = process.env.HOME || process.env.USERPROFILE;
12
+ if (!home) {
13
+ throw new Error('HOME is not set');
14
+ }
15
+
16
+ const apply = process.argv.includes('--apply');
17
+ const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
18
+ const clientDir = process.env.GMTR_DIR || join(home, 'gmtr-client');
19
+ runLegacyMigration({
20
+ homeDir: home,
21
+ clientDir,
22
+ includeOptionalUx,
23
+ apply,
24
+ log,
25
+ });
26
+ }
27
+
28
+ main();
@@ -0,0 +1,3 @@
1
+ import json, os
2
+ s = os.path.expanduser("~/gmtr-client/settings.json")
3
+ print(json.load(open(s)).get("auth",{}).get("api_key",""))
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gmtr-login — Authenticate with the gramatr server
4
+ *
5
+ * Opens the grāmatr dashboard login flow, captures a Firebase ID token
6
+ * on localhost, and stores it in ~/.gmtr.json.
7
+ *
8
+ * Usage:
9
+ * bun gmtr-login.ts # Interactive browser login via Firebase dashboard
10
+ * bun gmtr-login.ts --token <token> # Paste a token directly (API key or Firebase token)
11
+ * bun gmtr-login.ts --status # Check current auth status
12
+ * bun gmtr-login.ts --logout # Remove stored credentials
13
+ *
14
+ * Token is stored in ~/.gmtr.json under the "token" key.
15
+ * The GMTRPromptEnricher hook reads this on every prompt.
16
+ */
17
+
18
+ import { randomBytes } from 'crypto';
19
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { createServer, type IncomingMessage, type ServerResponse } from 'http';
22
+
23
+ // ── Config ──
24
+
25
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
26
+ const CONFIG_PATH = join(HOME, '.gmtr.json');
27
+ const DEFAULT_SERVER = process.env.GMTR_URL || 'https://api.gramatr.com/mcp';
28
+ // Strip /mcp suffix to get base URL
29
+ const SERVER_BASE = DEFAULT_SERVER.replace(/\/mcp\/?$/, '');
30
+ const DASHBOARD_BASE = process.env.GMTR_DASHBOARD_URL || (() => {
31
+ try {
32
+ const url = new URL(SERVER_BASE);
33
+ if (url.hostname.startsWith('api.')) {
34
+ url.hostname = `app.${url.hostname.slice(4)}`;
35
+ }
36
+ url.pathname = '';
37
+ url.search = '';
38
+ url.hash = '';
39
+ return url.toString().replace(/\/$/, '');
40
+ } catch {
41
+ return 'https://app.gramatr.com';
42
+ }
43
+ })();
44
+ const CALLBACK_PORT = 58787; // Must match server's redirect_uris
45
+
46
+ // ── HTML Templates ──
47
+
48
+ const BRAND_CSS = `
49
+ * { margin: 0; padding: 0; box-sizing: border-box; }
50
+ body {
51
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
52
+ background: #0a0e17;
53
+ color: #e0e6ed;
54
+ min-height: 100vh;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ }
59
+ .card {
60
+ background: #141b2d;
61
+ border: 1px solid #1e2940;
62
+ border-radius: 16px;
63
+ padding: 48px;
64
+ max-width: 440px;
65
+ width: 90%;
66
+ text-align: center;
67
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
68
+ }
69
+ .logo {
70
+ font-size: 28px;
71
+ font-weight: 700;
72
+ letter-spacing: -0.5px;
73
+ margin-bottom: 8px;
74
+ }
75
+ .logo .accent { color: #00b4d8; }
76
+ .logo .dim { color: #5a6a8a; }
77
+ .subtitle {
78
+ color: #5a6a8a;
79
+ font-size: 13px;
80
+ margin-bottom: 32px;
81
+ }
82
+ .status {
83
+ font-size: 48px;
84
+ margin-bottom: 16px;
85
+ }
86
+ h2 {
87
+ font-size: 20px;
88
+ font-weight: 600;
89
+ margin-bottom: 12px;
90
+ }
91
+ h2.success { color: #00b4d8; }
92
+ h2.error { color: #e74c3c; }
93
+ p {
94
+ color: #7a8aaa;
95
+ font-size: 14px;
96
+ line-height: 1.6;
97
+ }
98
+ .hint {
99
+ margin-top: 24px;
100
+ padding-top: 24px;
101
+ border-top: 1px solid #1e2940;
102
+ font-size: 12px;
103
+ color: #4a5a7a;
104
+ }
105
+ `;
106
+
107
+ function htmlPage(title: string, body: string): string {
108
+ return `<!DOCTYPE html>
109
+ <html lang="en"><head>
110
+ <meta charset="utf-8">
111
+ <meta name="viewport" content="width=device-width, initial-scale=1">
112
+ <title>${title} — gramatr</title>
113
+ <style>${BRAND_CSS}</style>
114
+ </head><body>
115
+ <div class="card">
116
+ <div class="logo"><span class="accent">gr</span>āma<span class="accent">tr</span></div>
117
+ <div class="subtitle">your cross-agent AI brain</div>
118
+ ${body}
119
+ </div>
120
+ </body></html>`;
121
+ }
122
+
123
+ function successPage(): string {
124
+ return htmlPage('Authenticated', `
125
+ <div class="status">✓</div>
126
+ <h2 class="success">Authenticated</h2>
127
+ <p>Token saved. You can close this tab and return to your terminal.</p>
128
+ <div class="hint">gramatr intelligence is now active across all your AI tools.</div>
129
+ `);
130
+ }
131
+
132
+ function errorPage(title: string, detail: string): string {
133
+ return htmlPage('Error', `
134
+ <div class="status">✗</div>
135
+ <h2 class="error">${title}</h2>
136
+ <p>${detail}</p>
137
+ <div class="hint">Return to your terminal and try again, or use <code>gmtr-login --token</code> to paste a token directly.</div>
138
+ `);
139
+ }
140
+
141
+ // ── Headless Detection ──
142
+
143
+ function isHeadless(): boolean {
144
+ // SSH session without display forwarding
145
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) {
146
+ if (!process.env.DISPLAY && process.platform !== 'darwin') return true;
147
+ }
148
+ // Docker / CI / no TTY
149
+ if (process.env.CI || process.env.DOCKER) return true;
150
+ // Linux without display
151
+ if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
152
+ return false;
153
+ }
154
+
155
+ // ── Helpers ──
156
+
157
+ function readConfig(): Record<string, any> {
158
+ try {
159
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
160
+ } catch {
161
+ return {};
162
+ }
163
+ }
164
+
165
+ function writeConfig(config: Record<string, any>): void {
166
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
167
+ }
168
+
169
+ async function checkServerHealth(): Promise<{ ok: boolean; version?: string; error?: string }> {
170
+ try {
171
+ const res = await fetch(`${SERVER_BASE}/health`, { signal: AbortSignal.timeout(5000) });
172
+ if (res.ok) {
173
+ const data = await res.json() as any;
174
+ return { ok: true, version: data.version };
175
+ }
176
+ return { ok: false, error: `HTTP ${res.status}` };
177
+ } catch (e: any) {
178
+ return { ok: false, error: e.message };
179
+ }
180
+ }
181
+
182
+ async function testToken(token: string): Promise<{ valid: boolean; user?: string; error?: string }> {
183
+ try {
184
+ const res = await fetch(`${SERVER_BASE}/mcp`, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ Accept: 'application/json, text/event-stream',
189
+ Authorization: `Bearer ${token}`,
190
+ },
191
+ body: JSON.stringify({
192
+ jsonrpc: '2.0',
193
+ id: 1,
194
+ method: 'tools/call',
195
+ params: { name: 'aggregate_stats', arguments: {} },
196
+ }),
197
+ signal: AbortSignal.timeout(10000),
198
+ });
199
+
200
+ const text = await res.text();
201
+
202
+ // Check for auth errors
203
+ if (text.includes('JWT token is required') || text.includes('signature validation failed') || text.includes('Unauthorized')) {
204
+ return { valid: false, error: 'Token rejected by server' };
205
+ }
206
+
207
+ // Check for successful response
208
+ for (const line of text.split('\n')) {
209
+ if (line.startsWith('data: ')) {
210
+ try {
211
+ const d = JSON.parse(line.slice(6));
212
+ if (d?.result?.isError) {
213
+ return { valid: false, error: d.result.content?.[0]?.text || 'Unknown error' };
214
+ }
215
+ if (d?.result?.content?.[0]?.text) {
216
+ return { valid: true, user: 'authenticated' };
217
+ }
218
+ } catch {
219
+ continue;
220
+ }
221
+ }
222
+ }
223
+
224
+ return { valid: false, error: 'Unexpected response' };
225
+ } catch (e: any) {
226
+ return { valid: false, error: e.message };
227
+ }
228
+ }
229
+
230
+ async function startDeviceAuthorization(): Promise<{
231
+ device_code: string;
232
+ user_code: string;
233
+ verification_uri: string;
234
+ verification_uri_complete?: string;
235
+ expires_in: number;
236
+ interval: number;
237
+ }> {
238
+ const res = await fetch(`${SERVER_BASE}/device/start`, {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify({ client_name: 'gmtr-login' }),
242
+ signal: AbortSignal.timeout(10000),
243
+ });
244
+
245
+ const payload = await res.json().catch(() => ({}));
246
+ if (!res.ok) {
247
+ throw new Error(payload.error_description || payload.error || `HTTP ${res.status}`);
248
+ }
249
+ return payload;
250
+ }
251
+
252
+ async function pollDeviceAuthorization(deviceCode: string): Promise<string> {
253
+ while (true) {
254
+ const res = await fetch(`${SERVER_BASE}/device/token`, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({ device_code: deviceCode }),
258
+ signal: AbortSignal.timeout(10000),
259
+ });
260
+
261
+ const payload = await res.json().catch(() => ({}));
262
+ if (res.ok && payload.access_token) {
263
+ return payload.access_token as string;
264
+ }
265
+
266
+ if ((res.status === 428 || res.status === 400) && payload.error === 'authorization_pending') {
267
+ const waitSeconds = Math.max(1, Number(payload.interval) || 5);
268
+ await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
269
+ continue;
270
+ }
271
+
272
+ throw new Error(payload.error_description || payload.error || `HTTP ${res.status}`);
273
+ }
274
+ }
275
+
276
+ // ── Commands ──
277
+
278
+ async function showStatus(): Promise<void> {
279
+ console.log('\n gramatr authentication status\n');
280
+
281
+ const config = readConfig();
282
+ const token = config.token;
283
+
284
+ console.log(` Server: ${SERVER_BASE}`);
285
+
286
+ const health = await checkServerHealth();
287
+ if (health.ok) {
288
+ console.log(` Health: ✓ healthy (v${health.version || 'unknown'})`);
289
+ } else {
290
+ console.log(` Health: ✗ ${health.error}`);
291
+ }
292
+
293
+ if (!token) {
294
+ console.log(' Token: ✗ not configured');
295
+ console.log('\n Run: bun gmtr-login.ts to authenticate\n');
296
+ return;
297
+ }
298
+
299
+ const prefix = token.substring(0, 15);
300
+ console.log(` Token: ${prefix}...`);
301
+
302
+ const result = await testToken(token);
303
+ if (result.valid) {
304
+ console.log(' Auth: ✓ token is valid');
305
+ } else {
306
+ console.log(` Auth: ✗ ${result.error}`);
307
+ console.log('\n Run: bun gmtr-login.ts to re-authenticate\n');
308
+ }
309
+
310
+ console.log('');
311
+ }
312
+
313
+ async function logout(): Promise<void> {
314
+ const config = readConfig();
315
+ delete config.token;
316
+ delete config.token_type;
317
+ delete config.token_expires;
318
+ delete config.authenticated_at;
319
+ writeConfig(config);
320
+ console.log('\n ✓ Logged out. Token removed from ~/.gmtr.json\n');
321
+ }
322
+
323
+ async function loginWithToken(token: string): Promise<void> {
324
+ console.log('\n Testing token...');
325
+
326
+ const result = await testToken(token);
327
+ if (result.valid) {
328
+ const config = readConfig();
329
+ config.token = token;
330
+ config.token_type = token.startsWith('aios_sk_') || token.startsWith('gmtr_sk_') ? 'api_key' : 'oauth';
331
+ config.authenticated_at = new Date().toISOString();
332
+ writeConfig(config);
333
+ console.log(' ✓ Token valid. Saved to ~/.gmtr.json');
334
+ console.log(' gramatr intelligence is now active.\n');
335
+ } else {
336
+ console.log(` ✗ Token rejected: ${result.error}`);
337
+ console.log(' Token was NOT saved.\n');
338
+ process.exit(1);
339
+ }
340
+ }
341
+
342
+ async function loginBrowser(): Promise<void> {
343
+ console.log('\n gramatr login\n');
344
+ console.log(` Server: ${SERVER_BASE}`);
345
+ console.log(` Dashboard: ${DASHBOARD_BASE}`);
346
+
347
+ // Check server health first
348
+ const health = await checkServerHealth();
349
+ if (!health.ok) {
350
+ console.log(` ✗ Server unreachable: ${health.error}`);
351
+ console.log(' Cannot authenticate. Is the server running?\n');
352
+ process.exit(1);
353
+ }
354
+ console.log(` Health: ✓ v${health.version || 'unknown'}`);
355
+
356
+ console.log('');
357
+
358
+ // Headless environments use device auth (no local server needed)
359
+ if (isHeadless()) {
360
+ console.log(' Headless environment detected. Starting device login...\n');
361
+ try {
362
+ const device = await startDeviceAuthorization();
363
+ console.log(` Code: ${device.user_code}`);
364
+ console.log(` Open: ${device.verification_uri_complete || device.verification_uri}`);
365
+ console.log(' Sign in with Google or GitHub, approve the device, then return here.\n');
366
+ console.log(' Waiting for authorization...');
367
+
368
+ const accessToken = await Promise.race([
369
+ pollDeviceAuthorization(device.device_code),
370
+ new Promise<string>((_, reject) => setTimeout(() => reject(new Error('Device login timed out')), device.expires_in * 1000)),
371
+ ]);
372
+
373
+ const config = readConfig();
374
+ config.token = accessToken;
375
+ config.token_type = 'oauth';
376
+ config.authenticated_at = new Date().toISOString();
377
+ config.server_url = SERVER_BASE;
378
+ config.dashboard_url = DASHBOARD_BASE;
379
+ writeConfig(config);
380
+
381
+ console.log('');
382
+ console.log(' ✓ Authenticated successfully');
383
+ console.log(' Token saved to ~/.gmtr.json');
384
+ console.log(' gramatr intelligence is now active.\n');
385
+ return;
386
+ } catch (e: any) {
387
+ console.log(` ✗ Device login failed: ${e.message}`);
388
+ console.log(' Fallback: gmtr-login --token\n');
389
+ process.exit(1);
390
+ }
391
+ }
392
+
393
+ // Browser environments use local callback server
394
+ const state = randomBytes(16).toString('base64url');
395
+ const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
396
+
397
+ const tokenPromise = new Promise<string>((resolve, reject) => {
398
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
399
+ const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
400
+
401
+ if (url.pathname !== '/callback') {
402
+ res.writeHead(404);
403
+ res.end('Not found');
404
+ return;
405
+ }
406
+
407
+ const token = url.searchParams.get('token');
408
+ const returnedState = url.searchParams.get('state');
409
+ const error = url.searchParams.get('error');
410
+
411
+ if (error) {
412
+ res.writeHead(200, { 'Content-Type': 'text/html' });
413
+ res.end(errorPage('Authentication Failed', error));
414
+ server.close();
415
+ reject(new Error(`OAuth error: ${error}`));
416
+ return;
417
+ }
418
+
419
+ if (!token || returnedState !== state) {
420
+ res.writeHead(400, { 'Content-Type': 'text/html' });
421
+ res.end(errorPage('Invalid Callback', 'Missing Firebase token or state mismatch. Please try again.'));
422
+ server.close();
423
+ reject(new Error('Invalid callback'));
424
+ return;
425
+ }
426
+
427
+ try {
428
+ const validation = await testToken(token);
429
+ if (!validation.valid) {
430
+ res.writeHead(200, { 'Content-Type': 'text/html' });
431
+ res.end(errorPage('Token Validation Failed', validation.error || 'Server rejected token'));
432
+ server.close();
433
+ reject(new Error(validation.error || 'Server rejected token'));
434
+ return;
435
+ }
436
+
437
+ res.writeHead(200, { 'Content-Type': 'text/html' });
438
+ res.end(successPage());
439
+ server.close();
440
+ resolve(token);
441
+ } catch (e: any) {
442
+ res.writeHead(500, { 'Content-Type': 'text/html' });
443
+ res.end(errorPage('Unexpected Error', e.message));
444
+ server.close();
445
+ reject(e);
446
+ }
447
+ });
448
+
449
+ server.listen(CALLBACK_PORT, () => {
450
+ // Server ready
451
+ });
452
+
453
+ // Timeout after 2 minutes
454
+ setTimeout(() => {
455
+ server.close();
456
+ reject(new Error('Login timed out after 2 minutes'));
457
+ }, 120000);
458
+ });
459
+
460
+ const authorizeUrl = new URL('/login', `${DASHBOARD_BASE}/`);
461
+ authorizeUrl.searchParams.set('callback', callbackUrl);
462
+ authorizeUrl.searchParams.set('state', state);
463
+
464
+ console.log(' Opening browser for authentication...');
465
+ console.log(` If it doesn't open, visit:`);
466
+ console.log(` ${authorizeUrl.toString()}`);
467
+ console.log('');
468
+
469
+ // Open browser
470
+ const { exec } = await import('child_process');
471
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
472
+ exec(`${openCmd} "${authorizeUrl.toString()}"`);
473
+
474
+ console.log(' Waiting for authorization...');
475
+
476
+ try {
477
+ const accessToken = await tokenPromise;
478
+
479
+ // Save token
480
+ const config = readConfig();
481
+ config.token = accessToken;
482
+ config.token_type = 'firebase';
483
+ config.authenticated_at = new Date().toISOString();
484
+ config.server_url = SERVER_BASE;
485
+ config.dashboard_url = DASHBOARD_BASE;
486
+ writeConfig(config);
487
+
488
+ console.log('');
489
+ console.log(' ✓ Authenticated successfully');
490
+ console.log(' Token saved to ~/.gmtr.json');
491
+ console.log(' gramatr intelligence is now active.\n');
492
+ } catch (e: any) {
493
+ console.log(`\n ✗ Authentication failed: ${e.message}\n`);
494
+ process.exit(1);
495
+ }
496
+ }
497
+
498
+ // ── CLI ──
499
+
500
+ const args = process.argv.slice(2);
501
+
502
+ if (args.includes('--status') || args.includes('status')) {
503
+ await showStatus();
504
+ } else if (args.includes('--logout') || args.includes('logout')) {
505
+ await logout();
506
+ } else if (args.includes('--token') || args.includes('-t')) {
507
+ const tokenIdx = args.indexOf('--token') !== -1 ? args.indexOf('--token') : args.indexOf('-t');
508
+ const token = args[tokenIdx + 1];
509
+ if (!token) {
510
+ // Interactive paste mode — like Claude's login
511
+ console.log('\n Paste your gramatr token below.');
512
+ console.log(' (API keys start with aios_sk_ or gmtr_sk_)\n');
513
+ process.stdout.write(' Token: ');
514
+
515
+ const { createInterface } = await import('readline');
516
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
517
+ const pastedToken = await new Promise<string>((resolve) => {
518
+ rl.on('line', (line: string) => { rl.close(); resolve(line.trim()); });
519
+ });
520
+ if (!pastedToken) {
521
+ console.log(' No token provided.\n');
522
+ process.exit(1);
523
+ }
524
+ await loginWithToken(pastedToken);
525
+ } else {
526
+ await loginWithToken(token);
527
+ }
528
+ } else if (args.includes('--help') || args.includes('-h')) {
529
+ console.log(`
530
+ gmtr-login — Authenticate with the gramatr server
531
+
532
+ Usage:
533
+ gmtr-login Interactive dashboard login (browser or headless device flow)
534
+ gmtr-login --token Paste a token (API key or Firebase token)
535
+ gmtr-login --token <t> Provide token directly
536
+ gmtr-login --status Check authentication status
537
+ gmtr-login --logout Remove stored credentials
538
+ gmtr-login --help Show this help
539
+
540
+ Token storage: ~/.gmtr.json
541
+ Server: ${SERVER_BASE}
542
+ Dashboard: ${DASHBOARD_BASE}
543
+ `);
544
+ } else {
545
+ // Default: browser login flow
546
+ await loginBrowser();
547
+ }