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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { buildClaudeHooksFile } from '../core/install.ts';
|
|
4
|
+
|
|
5
|
+
function main(): void {
|
|
6
|
+
const clientDir = process.argv[2];
|
|
7
|
+
if (!clientDir) {
|
|
8
|
+
throw new Error('Usage: render-claude-hooks.ts <client-dir>');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
12
|
+
const hooks = buildClaudeHooksFile(clientDir, { includeOptionalUx });
|
|
13
|
+
process.stdout.write(`${JSON.stringify(hooks.hooks, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
main();
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gramatr Status Line — TypeScript rewrite of statusline.sh (#158)
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth: gramatr API for server metrics, local for git/env.
|
|
6
|
+
* Replaces 1,383-line bash script with type-safe, testable TypeScript.
|
|
7
|
+
*
|
|
8
|
+
* Data sources:
|
|
9
|
+
* API → entity count, token savings, classifier stats, learning signals, skills, version
|
|
10
|
+
* Local → git status, context window, Anthropic usage, session label, location/weather (cached)
|
|
11
|
+
* Stdin → Claude Code JSON (context_window, session_id, model, workspace)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync, writeFileSync, statSync } from 'fs';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface CCInput {
|
|
21
|
+
workspace?: { current_dir?: string };
|
|
22
|
+
cwd?: string;
|
|
23
|
+
session_id?: string;
|
|
24
|
+
model?: { display_name?: string };
|
|
25
|
+
version?: string;
|
|
26
|
+
cost?: { total_duration_ms?: number };
|
|
27
|
+
context_window?: {
|
|
28
|
+
context_window_size?: number;
|
|
29
|
+
used_percentage?: number;
|
|
30
|
+
remaining_percentage?: number;
|
|
31
|
+
total_input_tokens?: number;
|
|
32
|
+
total_output_tokens?: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GramatrStats {
|
|
37
|
+
server_version: string;
|
|
38
|
+
entity_count: number;
|
|
39
|
+
observation_count: number;
|
|
40
|
+
search_count: number;
|
|
41
|
+
tokens_saved_total: number;
|
|
42
|
+
tokens_saved_7d: number;
|
|
43
|
+
classifications_total: number;
|
|
44
|
+
classifications_7d: number;
|
|
45
|
+
operations_1h: number;
|
|
46
|
+
operations_24h: number;
|
|
47
|
+
classifier: {
|
|
48
|
+
level: number;
|
|
49
|
+
model: string;
|
|
50
|
+
accuracy: number;
|
|
51
|
+
feedback_rate: number;
|
|
52
|
+
total_classifications: number;
|
|
53
|
+
};
|
|
54
|
+
learning: {
|
|
55
|
+
latest: number | null;
|
|
56
|
+
avg_1d: number | null;
|
|
57
|
+
avg_1w: number | null;
|
|
58
|
+
avg_1mo: number | null;
|
|
59
|
+
count: number;
|
|
60
|
+
};
|
|
61
|
+
skills_count: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface GitInfo {
|
|
65
|
+
projectId: string; // org/repo from remote, fallback to directory name
|
|
66
|
+
branch: string;
|
|
67
|
+
stashCount: number;
|
|
68
|
+
modified: number;
|
|
69
|
+
untracked: number;
|
|
70
|
+
ahead: number;
|
|
71
|
+
behind: number;
|
|
72
|
+
lastCommitAge: string;
|
|
73
|
+
lastCommitColor: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// ── Config ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const HOME = process.env.HOME || '';
|
|
81
|
+
const GMTR_DIR = process.env.GMTR_DIR || join(HOME, 'gmtr-client');
|
|
82
|
+
const STATE_DIR = join(GMTR_DIR, '.state');
|
|
83
|
+
const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
|
|
84
|
+
|
|
85
|
+
function getGramatrUrl(): string {
|
|
86
|
+
try {
|
|
87
|
+
const claude = JSON.parse(readFileSync(join(HOME, '.claude.json'), 'utf8'));
|
|
88
|
+
return claude?.mcpServers?.gramatr?.url?.replace('/mcp', '') || 'https://api.gramatr.com';
|
|
89
|
+
} catch {
|
|
90
|
+
return 'https://api.gramatr.com';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getAuthToken(): string {
|
|
95
|
+
// Check settings.json first, then env var
|
|
96
|
+
try {
|
|
97
|
+
const settings = JSON.parse(readFileSync(join(GMTR_DIR, 'settings.json'), 'utf8'));
|
|
98
|
+
if (settings?.auth?.token) return settings.auth.token;
|
|
99
|
+
} catch { /* no settings file */ }
|
|
100
|
+
return process.env.GMTR_TOKEN || process.env.AIOS_MCP_TOKEN || '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Cache Helper ───────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function readCache<T>(path: string, ttlMs: number): T | null {
|
|
106
|
+
try {
|
|
107
|
+
if (!existsSync(path)) return null;
|
|
108
|
+
const stat = statSync(path);
|
|
109
|
+
if (Date.now() - stat.mtimeMs > ttlMs) return null;
|
|
110
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeCache(path: string, data: unknown): void {
|
|
117
|
+
try {
|
|
118
|
+
writeFileSync(path, JSON.stringify(data));
|
|
119
|
+
} catch { /* non-critical */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Colors (24-bit ANSI) ───────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
// Gray Matter palette — from design-tokens.json (locked 2026-03-28)
|
|
125
|
+
const c = {
|
|
126
|
+
reset: '\x1b[0m',
|
|
127
|
+
bold: '\x1b[1m',
|
|
128
|
+
dim: '\x1b[2m',
|
|
129
|
+
// Text hierarchy (warm neutrals)
|
|
130
|
+
text: '\x1b[38;2;236;236;236m', // #ECECEC — primary text
|
|
131
|
+
textSec: '\x1b[38;2;136;136;136m', // #888888 — secondary
|
|
132
|
+
textMuted: '\x1b[38;2;102;102;102m', // #666666 — muted/labels
|
|
133
|
+
// Brand
|
|
134
|
+
primary: '\x1b[38;2;59;130;246m', // #3B82F6 — blue accent
|
|
135
|
+
accent: '\x1b[38;2;96;165;250m', // #60A5FA — lighter blue
|
|
136
|
+
// Structural
|
|
137
|
+
border: '\x1b[38;2;51;51;51m', // #333333 — separators
|
|
138
|
+
surface: '\x1b[38;2;26;26;26m', // #1A1A1A — surface
|
|
139
|
+
// Semantic
|
|
140
|
+
success: '\x1b[38;2;74;222;128m', // #4ADE80
|
|
141
|
+
warning: '\x1b[38;2;250;204;21m', // #FACC15
|
|
142
|
+
error: '\x1b[38;2;248;113;113m', // #F87171
|
|
143
|
+
// Bg
|
|
144
|
+
barBg: '\x1b[48;2;26;26;26m', // #1A1A1A — context bar empty
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function ageColor(age: string): string {
|
|
148
|
+
if (age.endsWith('m') && parseInt(age) < 30) return c.success;
|
|
149
|
+
if (age.endsWith('h') && parseInt(age) < 2) return c.accent;
|
|
150
|
+
if (age.endsWith('d') && parseInt(age) < 2) return c.warning;
|
|
151
|
+
return c.error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Data Fetchers ──────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function fetchGramatrStats(): Promise<GramatrStats | null> {
|
|
157
|
+
const cached = readCache<GramatrStats>(join(STATE_DIR, 'api-stats-cache.json'), 60_000);
|
|
158
|
+
if (cached) return cached;
|
|
159
|
+
|
|
160
|
+
const url = getGramatrUrl();
|
|
161
|
+
const token = getAuthToken();
|
|
162
|
+
if (!token) return null;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const resp = await fetch(`${url}/api/v1/stats/statusline`, {
|
|
166
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' },
|
|
167
|
+
signal: AbortSignal.timeout(5000),
|
|
168
|
+
});
|
|
169
|
+
if (!resp.ok) {
|
|
170
|
+
// Fallback: read from local files if API not available yet
|
|
171
|
+
return readLocalStats();
|
|
172
|
+
}
|
|
173
|
+
const data = await resp.json() as GramatrStats;
|
|
174
|
+
writeCache(join(STATE_DIR, 'api-stats-cache.json'), data);
|
|
175
|
+
return data;
|
|
176
|
+
} catch {
|
|
177
|
+
// API unavailable — fall back to local files
|
|
178
|
+
return readLocalStats();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readLocalStats(): GramatrStats | null {
|
|
183
|
+
// Backward-compatible: read from /tmp/ files (same as bash version)
|
|
184
|
+
try {
|
|
185
|
+
const stats = existsSync('/tmp/gmtr-stats.json')
|
|
186
|
+
? JSON.parse(readFileSync('/tmp/gmtr-stats.json', 'utf8'))
|
|
187
|
+
: {};
|
|
188
|
+
const savings = existsSync('/tmp/gmtr-classification-savings.json')
|
|
189
|
+
? JSON.parse(readFileSync('/tmp/gmtr-classification-savings.json', 'utf8'))
|
|
190
|
+
: {};
|
|
191
|
+
|
|
192
|
+
// Sum tokens from history
|
|
193
|
+
let totalTokensSaved = 0;
|
|
194
|
+
let ops1h = 0;
|
|
195
|
+
let ops24h = 0;
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (existsSync('/tmp/gmtr-op-history.jsonl')) {
|
|
198
|
+
const lines = readFileSync('/tmp/gmtr-op-history.jsonl', 'utf8').trim().split('\n');
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
try {
|
|
201
|
+
const entry = JSON.parse(line);
|
|
202
|
+
totalTokensSaved += entry.tokens_saved || 0;
|
|
203
|
+
const age = now - (entry.timestamp || 0);
|
|
204
|
+
if (age < 3600_000) ops1h++;
|
|
205
|
+
if (age < 86400_000) ops24h++;
|
|
206
|
+
} catch { /* skip bad lines */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
server_version: savings.server_version || '',
|
|
212
|
+
entity_count: stats.entity_count || 0,
|
|
213
|
+
observation_count: stats.observation_count || 0,
|
|
214
|
+
search_count: stats.search_count || 0,
|
|
215
|
+
tokens_saved_total: 0,
|
|
216
|
+
tokens_saved_7d: 0,
|
|
217
|
+
classifications_total: 0,
|
|
218
|
+
classifications_7d: 0,
|
|
219
|
+
operations_1h: ops1h,
|
|
220
|
+
operations_24h: ops24h,
|
|
221
|
+
classifier: {
|
|
222
|
+
level: stats.classifier_level || 0,
|
|
223
|
+
model: stats.classifier_model || savings.qwen_model || '',
|
|
224
|
+
accuracy: stats.accuracy || 0,
|
|
225
|
+
feedback_rate: stats.feedback_rate || 0,
|
|
226
|
+
total_classifications: stats.total_classifications || 0,
|
|
227
|
+
},
|
|
228
|
+
learning: { latest: null, avg_1d: null, avg_1w: null, avg_1mo: null, count: 0 },
|
|
229
|
+
skills_count: 0,
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getGitInfo(cwd: string): GitInfo | null {
|
|
237
|
+
try {
|
|
238
|
+
const run = (cmd: string) => execSync(cmd, { cwd, timeout: 3000, encoding: 'utf8' }).trim();
|
|
239
|
+
|
|
240
|
+
// Check if git repo
|
|
241
|
+
try { run('git rev-parse --git-dir'); } catch { return null; }
|
|
242
|
+
|
|
243
|
+
// Resolve project ID: git remote → org/repo, fallback to directory name
|
|
244
|
+
let projectId = run('git rev-parse --show-toplevel').split('/').pop() || '';
|
|
245
|
+
try {
|
|
246
|
+
const remote = run('git remote get-url origin');
|
|
247
|
+
const match = remote.match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
248
|
+
if (match) projectId = `${match[1]}/${match[2]}`;
|
|
249
|
+
} catch { /* no remote — use directory name */ }
|
|
250
|
+
const branch = run('git branch --show-current') || 'detached';
|
|
251
|
+
|
|
252
|
+
let stashCount = 0;
|
|
253
|
+
try { stashCount = run('git stash list').split('\n').filter(Boolean).length; } catch {}
|
|
254
|
+
|
|
255
|
+
let modified = 0, untracked = 0;
|
|
256
|
+
try {
|
|
257
|
+
const status = run('git status --porcelain');
|
|
258
|
+
for (const line of status.split('\n').filter(Boolean)) {
|
|
259
|
+
if (line.startsWith('??')) untracked++;
|
|
260
|
+
else modified++;
|
|
261
|
+
}
|
|
262
|
+
} catch {}
|
|
263
|
+
|
|
264
|
+
let ahead = 0, behind = 0;
|
|
265
|
+
try {
|
|
266
|
+
const counts = run('git rev-list --left-right --count HEAD...@{u}');
|
|
267
|
+
const [a, b] = counts.split('\t').map(Number);
|
|
268
|
+
ahead = a || 0;
|
|
269
|
+
behind = b || 0;
|
|
270
|
+
} catch {}
|
|
271
|
+
|
|
272
|
+
let lastCommitAge = '?';
|
|
273
|
+
let lastCommitColor = c.slate400;
|
|
274
|
+
try {
|
|
275
|
+
const epoch = parseInt(run('git log -1 --format=%ct'));
|
|
276
|
+
const seconds = Math.floor(Date.now() / 1000) - epoch;
|
|
277
|
+
if (seconds < 3600) {
|
|
278
|
+
lastCommitAge = `${Math.floor(seconds / 60)}m`;
|
|
279
|
+
} else if (seconds < 86400) {
|
|
280
|
+
lastCommitAge = `${Math.floor(seconds / 3600)}h`;
|
|
281
|
+
} else {
|
|
282
|
+
lastCommitAge = `${Math.floor(seconds / 86400)}d`;
|
|
283
|
+
}
|
|
284
|
+
lastCommitColor = ageColor(lastCommitAge);
|
|
285
|
+
} catch {}
|
|
286
|
+
|
|
287
|
+
return { projectId, branch, stashCount, modified, untracked, ahead, behind, lastCommitAge, lastCommitColor };
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Rendering ──────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function formatNumber(n: number): string {
|
|
296
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
297
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
298
|
+
return String(n);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function contextBar(pct: number, width: number): string {
|
|
302
|
+
const filled = Math.round((pct / 100) * width);
|
|
303
|
+
let bar = '';
|
|
304
|
+
for (let i = 0; i < width; i++) {
|
|
305
|
+
if (i < filled) {
|
|
306
|
+
const ratio = i / width;
|
|
307
|
+
if (ratio < 0.4) bar += `${c.success}⛁`;
|
|
308
|
+
else if (ratio < 0.6) bar += `${c.accent}⛁`;
|
|
309
|
+
else if (ratio < 0.8) bar += `${c.warning}⛁`;
|
|
310
|
+
else bar += `${c.error}⛁`;
|
|
311
|
+
} else {
|
|
312
|
+
bar += `${c.barBg} ${c.reset}`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return bar + c.reset;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function separator(width: number): string {
|
|
319
|
+
return ` ${c.border}${'─'.repeat(Math.min(width - 4, 72))}${c.reset}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
interface Heartbeat {
|
|
323
|
+
effort: string;
|
|
324
|
+
intent: string;
|
|
325
|
+
confidence: string;
|
|
326
|
+
totalMs: number;
|
|
327
|
+
memoryDelivered: number;
|
|
328
|
+
stale: boolean;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getHeartbeat(): Heartbeat {
|
|
332
|
+
try {
|
|
333
|
+
const data = JSON.parse(readFileSync('/tmp/gmtr-classification-savings.json', 'utf8'));
|
|
334
|
+
const age = Date.now() - (data.timestamp || 0);
|
|
335
|
+
const stale = age > 300_000;
|
|
336
|
+
const st = data.stage_timing || {};
|
|
337
|
+
return {
|
|
338
|
+
effort: data.effort || '',
|
|
339
|
+
intent: data.intent || '',
|
|
340
|
+
confidence: data.confidence ? `${Math.round(data.confidence * 100)}%` : '',
|
|
341
|
+
totalMs: st.total_ms || data.qwen_time_ms || 0,
|
|
342
|
+
memoryDelivered: data.memory_delivered || 0,
|
|
343
|
+
stale,
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
346
|
+
return { effort: '', intent: '', confidence: '', totalMs: 0, memoryDelivered: 0, stale: true };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function render(input: CCInput, gmtr: GramatrStats | null, git: GitInfo | null): string {
|
|
351
|
+
const lines: string[] = [];
|
|
352
|
+
const termWidth = parseInt(process.env.COLUMNS || '80', 10);
|
|
353
|
+
const barWidth = Math.min(termWidth - 20, 55);
|
|
354
|
+
const version = gmtr?.server_version ? `v${gmtr.server_version}` : '';
|
|
355
|
+
const statusIcon = gmtr ? `${c.success}●${c.reset}` : `${c.error}○${c.reset}`;
|
|
356
|
+
const hb = getHeartbeat();
|
|
357
|
+
|
|
358
|
+
// ── LINE 1: BRAND + HEARTBEAT ──
|
|
359
|
+
let heartbeatText = '';
|
|
360
|
+
if (!hb.stale && hb.effort) {
|
|
361
|
+
heartbeatText = `${c.text}${hb.effort}/${hb.intent}${c.reset} ${c.accent}${hb.confidence}${c.reset} ${c.textMuted}${hb.totalMs}ms${c.reset}`;
|
|
362
|
+
if (hb.memoryDelivered > 0) {
|
|
363
|
+
heartbeatText += ` ${c.success}◇${hb.memoryDelivered} memories${c.reset}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const brandLine = ` ${c.border}──${c.reset} ${statusIcon} ` +
|
|
367
|
+
`${c.primary}grāmatr${c.reset} ` +
|
|
368
|
+
`${c.textMuted}${version}${c.reset}` +
|
|
369
|
+
(heartbeatText ? ` ${c.border}│${c.reset} ${heartbeatText}` : '') +
|
|
370
|
+
` ${c.border}│${c.reset} ${c.textMuted}gramatr.com${c.reset}`;
|
|
371
|
+
lines.push(brandLine);
|
|
372
|
+
|
|
373
|
+
// ── LINE 2: CONTEXT ──
|
|
374
|
+
const ctxPct = input.context_window?.used_percentage || 0;
|
|
375
|
+
lines.push(separator(termWidth));
|
|
376
|
+
lines.push(` ${c.accent}◉${c.reset} ${c.textMuted}CONTEXT:${c.reset} ${contextBar(ctxPct, barWidth)} ${c.text}${Math.round(ctxPct)}%${c.reset}`);
|
|
377
|
+
|
|
378
|
+
// ── LINE 3: PROJECT ──
|
|
379
|
+
if (git) {
|
|
380
|
+
lines.push(separator(termWidth));
|
|
381
|
+
lines.push(
|
|
382
|
+
` ${c.primary}◈${c.reset} ${c.text}${git.projectId}${c.reset}` +
|
|
383
|
+
` ${c.border}│${c.reset} ${c.accent}${git.branch}${c.reset}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── LINE 5: GRAMATR INTELLIGENCE ──
|
|
388
|
+
if (gmtr) {
|
|
389
|
+
lines.push(separator(termWidth));
|
|
390
|
+
const cl = gmtr.classifier;
|
|
391
|
+
const level = cl.level > 0 ? `L${cl.level}` : '—';
|
|
392
|
+
const savedWeek = formatNumber(gmtr.tokens_saved_7d || 0);
|
|
393
|
+
const savedTotal = formatNumber(gmtr.tokens_saved_total || 0);
|
|
394
|
+
lines.push(
|
|
395
|
+
` ${c.primary}◎${c.reset}` +
|
|
396
|
+
` ${c.accent}⬢${formatNumber(gmtr.entity_count)}${c.reset}${c.textMuted} entities${c.reset}` +
|
|
397
|
+
` ${c.border}│${c.reset} ${c.accent}◇${formatNumber(gmtr.observation_count)}${c.reset}${c.textMuted} obs${c.reset}` +
|
|
398
|
+
` ${c.border}│${c.reset} ${c.primary}${level}${c.reset}` +
|
|
399
|
+
` ${c.border}│${c.reset} ${c.textMuted}saved:${c.reset} ${c.success}${savedWeek}/wk${c.reset} ${c.text}${savedTotal}/all${c.reset}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lines.push(separator(termWidth));
|
|
404
|
+
return lines.join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
async function main() {
|
|
410
|
+
// Read CC JSON from stdin
|
|
411
|
+
let input: CCInput = {};
|
|
412
|
+
try {
|
|
413
|
+
const chunks: Buffer[] = [];
|
|
414
|
+
for await (const chunk of process.stdin) {
|
|
415
|
+
chunks.push(Buffer.from(chunk));
|
|
416
|
+
}
|
|
417
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
418
|
+
if (raw.trim()) input = JSON.parse(raw);
|
|
419
|
+
} catch { /* no stdin or bad JSON */ }
|
|
420
|
+
|
|
421
|
+
const cwd = input.workspace?.current_dir || input.cwd || process.cwd();
|
|
422
|
+
|
|
423
|
+
// Fetch gramatr stats
|
|
424
|
+
const gmtr = await fetchGramatrStats();
|
|
425
|
+
|
|
426
|
+
// Git is sync (fast, <100ms)
|
|
427
|
+
const git = getGitInfo(cwd);
|
|
428
|
+
|
|
429
|
+
// Render and output
|
|
430
|
+
const output = render(input, gmtr, git);
|
|
431
|
+
process.stdout.write(output + '\n');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
main().catch(() => {
|
|
435
|
+
// Never crash — output minimal fallback
|
|
436
|
+
process.stdout.write(` gramatr │ status unavailable\n`);
|
|
437
|
+
});
|