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,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GMTRToolTracker.hook.ts — grāmatr PostToolUse Hook
|
|
4
|
+
*
|
|
5
|
+
* Fires after any GMTR MCP tool call. Extracts execution metrics from
|
|
6
|
+
* tool_result and surfaces them as a user-visible status line.
|
|
7
|
+
*
|
|
8
|
+
* TRIGGER: PostToolUse (matcher: mcp__.*gramatr.*__)
|
|
9
|
+
* PERFORMANCE: <5ms. Outputs continue immediately, then processes.
|
|
10
|
+
*
|
|
11
|
+
* What it surfaces:
|
|
12
|
+
* - Tool name (short form, e.g., "search_semantic")
|
|
13
|
+
* - Classifier model + inference time (if intelligence layer was used)
|
|
14
|
+
* - Token savings ratio
|
|
15
|
+
* - Cache hit/miss
|
|
16
|
+
* - Total execution time
|
|
17
|
+
*
|
|
18
|
+
* Output format (to stderr, visible in Claude Code):
|
|
19
|
+
* [GMTR] search_semantic | 1.4s | qwen2.5:14b 0.32s | saved 6K tokens (80%) | cache:MISS
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { extractToolShortName, extractExecutionSummary, buildStatusLine } from './gmtr-tool-tracker-utils.js';
|
|
23
|
+
import type { ExecutionSummary } from './gmtr-tool-tracker-utils.js';
|
|
24
|
+
|
|
25
|
+
// ── Types ──
|
|
26
|
+
|
|
27
|
+
interface HookInput {
|
|
28
|
+
tool_name: string;
|
|
29
|
+
tool_input: Record<string, unknown>;
|
|
30
|
+
tool_response: Array<{ type: string; text?: string }>; // Claude Code sends tool_response (array), not tool_result (string)
|
|
31
|
+
session_id: string;
|
|
32
|
+
hook_event_name?: string;
|
|
33
|
+
tool_use_id?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Stdin Reader ──
|
|
37
|
+
|
|
38
|
+
function readStdin(timeoutMs: number): Promise<string> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let data = '';
|
|
41
|
+
const timer = setTimeout(() => resolve(data), timeoutMs);
|
|
42
|
+
|
|
43
|
+
process.stdin.setEncoding('utf-8');
|
|
44
|
+
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
45
|
+
process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
|
|
46
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(data); });
|
|
47
|
+
process.stdin.resume();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Debug Logger ──
|
|
52
|
+
|
|
53
|
+
async function debugLog(msg: string) {
|
|
54
|
+
try {
|
|
55
|
+
const { appendFileSync } = await import('fs');
|
|
56
|
+
appendFileSync('/tmp/gmtr-hook-debug.log', `[${new Date().toISOString()}] ${msg}\n`);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Main ──
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
// Step 1: Read stdin FIRST — data is already in pipe buffer from Claude Code.
|
|
64
|
+
// Writing stdout before reading stdin causes Claude Code to close the pipe,
|
|
65
|
+
// which means the hook never sees the tool_result data.
|
|
66
|
+
const raw = await readStdin(3000);
|
|
67
|
+
|
|
68
|
+
// Step 2: NOW output continue — never block Claude Code longer than necessary
|
|
69
|
+
console.log(JSON.stringify({ continue: true }));
|
|
70
|
+
|
|
71
|
+
await debugLog(`stdin=${raw.length}b`);
|
|
72
|
+
|
|
73
|
+
if (!raw.trim()) {
|
|
74
|
+
await debugLog('empty stdin, exiting');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let input: HookInput;
|
|
79
|
+
try {
|
|
80
|
+
input = JSON.parse(raw);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
await debugLog(`parse error: ${e}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await debugLog(`tool=${input.tool_name} response=${input.tool_response?.length || 0} items`);
|
|
87
|
+
|
|
88
|
+
const { tool_name, tool_response } = input;
|
|
89
|
+
if (!tool_name || !tool_response) return;
|
|
90
|
+
|
|
91
|
+
// Extract short tool name and execution metrics
|
|
92
|
+
const shortName = extractToolShortName(tool_name);
|
|
93
|
+
const summary = extractExecutionSummary(tool_response);
|
|
94
|
+
|
|
95
|
+
// Build and emit status line to stderr (visible in Claude Code)
|
|
96
|
+
const status = buildStatusLine(shortName, summary);
|
|
97
|
+
process.stderr.write(`${status}\n`);
|
|
98
|
+
|
|
99
|
+
// Write metrics to temp file for status line script to read
|
|
100
|
+
const metricsFile = '/tmp/gmtr-last-op.json';
|
|
101
|
+
const metrics = {
|
|
102
|
+
tool: shortName,
|
|
103
|
+
model: summary?.qwen_model || null,
|
|
104
|
+
time_ms: summary?.execution_time_ms || null,
|
|
105
|
+
tokens_saved: summary?.tokens_saved || null,
|
|
106
|
+
cache_hit: summary?.cache_hit ?? null,
|
|
107
|
+
result_count: summary?.results_count || null,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
const { writeFileSync, appendFileSync, readFileSync } = await import('fs');
|
|
111
|
+
try {
|
|
112
|
+
writeFileSync(metricsFile, JSON.stringify(metrics));
|
|
113
|
+
} catch {
|
|
114
|
+
// Non-critical — status line just won't show metrics
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Append to operation history (for sparkline/counts in status line)
|
|
118
|
+
const historyFile = '/tmp/gmtr-op-history.jsonl';
|
|
119
|
+
try {
|
|
120
|
+
const historyEntry = JSON.stringify({
|
|
121
|
+
tool: shortName,
|
|
122
|
+
model: summary?.qwen_model || null,
|
|
123
|
+
time_ms: summary?.execution_time_ms || 0,
|
|
124
|
+
tokens_saved: summary?.tokens_saved || 0,
|
|
125
|
+
cache_hit: summary?.cache_hit ?? false,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
});
|
|
128
|
+
appendFileSync(historyFile, historyEntry + '\n');
|
|
129
|
+
} catch {
|
|
130
|
+
// Non-critical
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract and cache GMTR server stats from tool response (entity/obs counts etc.)
|
|
134
|
+
try {
|
|
135
|
+
// tool_response is already a parsed array: [{ type: "text", text: "..." }]
|
|
136
|
+
let resultData: Record<string, unknown> | null = null;
|
|
137
|
+
const textItem = tool_response.find((item: { type: string; text?: string }) => item.type === 'text' && item.text);
|
|
138
|
+
if (textItem?.text) {
|
|
139
|
+
try { resultData = JSON.parse(textItem.text); } catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (resultData) {
|
|
143
|
+
// Look for entity_count, observation_count etc. in search results
|
|
144
|
+
const statsFile = '/tmp/gmtr-stats.json';
|
|
145
|
+
let stats: Record<string, unknown> = {};
|
|
146
|
+
try { stats = JSON.parse(readFileSync(statsFile, 'utf8')); } catch {}
|
|
147
|
+
|
|
148
|
+
// Track search count
|
|
149
|
+
if (shortName.includes('search') || shortName === 'gmtr_execute_intent') {
|
|
150
|
+
stats.search_count = ((stats.search_count as number) || 0) + 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Track entity/observation counts if returned in results
|
|
154
|
+
if (resultData.total_entities !== undefined) stats.entity_count = resultData.total_entities;
|
|
155
|
+
if (resultData.total_observations !== undefined) stats.observation_count = resultData.total_observations;
|
|
156
|
+
if (resultData.entity_count !== undefined) stats.entity_count = resultData.entity_count;
|
|
157
|
+
|
|
158
|
+
// Capture classifier progression data from execution_summary (Issue #34)
|
|
159
|
+
if (summary) {
|
|
160
|
+
if (summary.classifier_level !== undefined) stats.classifier_level = summary.classifier_level;
|
|
161
|
+
if (summary.classifier_model !== undefined) stats.classifier_model = summary.classifier_model;
|
|
162
|
+
if (summary.total_classifications !== undefined) stats.total_classifications = summary.total_classifications;
|
|
163
|
+
if (summary.total_feedback !== undefined) stats.total_feedback = summary.total_feedback;
|
|
164
|
+
if (summary.feedback_rate !== undefined) stats.feedback_rate = summary.feedback_rate;
|
|
165
|
+
if (summary.accuracy !== undefined) stats.accuracy = summary.accuracy;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Track local classification count (increments on every GMTR call)
|
|
169
|
+
stats.local_call_count = ((stats.local_call_count as number) || 0) + 1;
|
|
170
|
+
|
|
171
|
+
stats.status = 'connected';
|
|
172
|
+
stats.last_seen = Date.now();
|
|
173
|
+
|
|
174
|
+
writeFileSync(statsFile, JSON.stringify(stats));
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Non-critical
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch(() => {});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* StopOrchestrator.hook.ts — Stop event handler (thin client)
|
|
4
|
+
*
|
|
5
|
+
* Fires when the user stops Claude. Reads the transcript and sends
|
|
6
|
+
* classification feedback to the gramatr server via HTTP.
|
|
7
|
+
*
|
|
8
|
+
* In the thin client, all other Stop handlers (TabState, RebuildSkill,
|
|
9
|
+
* AlgorithmEnrichment, DocCrossRefIntegrity) are server-side.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { parseTranscript } from './lib/transcript-parser';
|
|
14
|
+
import { getGmtrDir } from './lib/paths';
|
|
15
|
+
|
|
16
|
+
interface HookInput {
|
|
17
|
+
session_id: string;
|
|
18
|
+
transcript_path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
let input = '';
|
|
23
|
+
try {
|
|
24
|
+
for await (const chunk of process.stdin) {
|
|
25
|
+
input += chunk;
|
|
26
|
+
}
|
|
27
|
+
} catch { /* timeout ok */ }
|
|
28
|
+
|
|
29
|
+
if (!input.trim()) { process.exit(0); }
|
|
30
|
+
|
|
31
|
+
const hookInput: HookInput = JSON.parse(input);
|
|
32
|
+
if (!hookInput.transcript_path) { process.exit(0); }
|
|
33
|
+
|
|
34
|
+
// Parse transcript for classification feedback
|
|
35
|
+
const parsed = parseTranscript(hookInput.transcript_path);
|
|
36
|
+
|
|
37
|
+
// Read server URL and token for direct HTTP feedback
|
|
38
|
+
const gmtrDir = getGmtrDir();
|
|
39
|
+
let serverUrl = process.env.GMTR_URL || 'https://api.gramatr.com/mcp';
|
|
40
|
+
let token = '';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const gmtrJson = JSON.parse(readFileSync(`${process.env.HOME}/.gmtr.json`, 'utf8'));
|
|
44
|
+
token = gmtrJson.token || '';
|
|
45
|
+
} catch { /* no token */ }
|
|
46
|
+
|
|
47
|
+
if (!token || !parsed) { process.exit(0); }
|
|
48
|
+
|
|
49
|
+
// Send classification feedback via MCP
|
|
50
|
+
try {
|
|
51
|
+
const baseUrl = serverUrl.replace(/\/mcp\/?$/, '');
|
|
52
|
+
await fetch(`${baseUrl}/mcp`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
'Accept': 'application/json, text/event-stream',
|
|
57
|
+
'Authorization': `Bearer ${token}`,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: 1,
|
|
62
|
+
method: 'tools/call',
|
|
63
|
+
params: {
|
|
64
|
+
name: 'gmtr_classification_feedback',
|
|
65
|
+
arguments: {
|
|
66
|
+
original_prompt: parsed.userMessages?.[0] || '',
|
|
67
|
+
session_id: hookInput.session_id,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
signal: AbortSignal.timeout(5000),
|
|
72
|
+
});
|
|
73
|
+
} catch { /* best effort */ }
|
|
74
|
+
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gmtr-tool-tracker-utils.ts — Pure utility functions for the GMTR Tool Tracker hook.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from GMTRToolTracker.hook.ts for testability. These are pure functions
|
|
5
|
+
* with no Bun dependencies, no I/O, no side effects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Types ──
|
|
9
|
+
|
|
10
|
+
export interface ExecutionSummary {
|
|
11
|
+
execution_time_ms?: number;
|
|
12
|
+
qwen_calls?: number;
|
|
13
|
+
qwen_time_ms?: number;
|
|
14
|
+
qwen_model?: string;
|
|
15
|
+
tokens_saved?: number;
|
|
16
|
+
savings_ratio?: number;
|
|
17
|
+
cache_hit?: boolean;
|
|
18
|
+
action?: string;
|
|
19
|
+
results_count?: number;
|
|
20
|
+
visual_query_detected?: boolean;
|
|
21
|
+
visual_results_count?: number;
|
|
22
|
+
classifier_level?: number;
|
|
23
|
+
classifier_model?: string;
|
|
24
|
+
total_classifications?: number;
|
|
25
|
+
total_feedback?: number;
|
|
26
|
+
feedback_rate?: number;
|
|
27
|
+
accuracy?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──
|
|
31
|
+
|
|
32
|
+
export function extractToolShortName(fullName: string): string {
|
|
33
|
+
// mcp__gramatr__search_semantic -> search_semantic
|
|
34
|
+
const parts = fullName.split('__');
|
|
35
|
+
return parts[parts.length - 1] || fullName;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatTokens(n: number): string {
|
|
39
|
+
if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
|
|
40
|
+
return n.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatMs(ms: number): string {
|
|
44
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
45
|
+
return `${Math.round(ms)}ms`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractExecutionSummary(toolResponse: Array<{ type: string; text?: string }>): ExecutionSummary | null {
|
|
49
|
+
try {
|
|
50
|
+
for (const item of toolResponse) {
|
|
51
|
+
if (item.type === 'text' && item.text) {
|
|
52
|
+
try {
|
|
53
|
+
const inner = JSON.parse(item.text);
|
|
54
|
+
if (inner.execution_summary) return inner.execution_summary;
|
|
55
|
+
if (inner.execution_time_ms !== undefined) return inner;
|
|
56
|
+
} catch {
|
|
57
|
+
// text wasn't JSON, skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildStatusLine(toolShortName: string, summary: ExecutionSummary | null): string {
|
|
68
|
+
const parts: string[] = [`[GMTR] ${toolShortName}`];
|
|
69
|
+
|
|
70
|
+
if (summary) {
|
|
71
|
+
if (summary.execution_time_ms) {
|
|
72
|
+
parts.push(formatMs(summary.execution_time_ms));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (summary.qwen_calls && summary.qwen_calls > 0) {
|
|
76
|
+
const model = summary.qwen_model || 'qwen2.5:14b';
|
|
77
|
+
const time = summary.qwen_time_ms ? ` ${formatMs(summary.qwen_time_ms)}` : '';
|
|
78
|
+
parts.push(`${model}${time}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (summary.tokens_saved && summary.tokens_saved > 0) {
|
|
82
|
+
const pct = summary.savings_ratio
|
|
83
|
+
? ` (${Math.round(summary.savings_ratio * 100)}%)`
|
|
84
|
+
: '';
|
|
85
|
+
parts.push(`saved ${formatTokens(summary.tokens_saved)} tokens${pct}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (summary.cache_hit !== undefined) {
|
|
89
|
+
parts.push(`cache:${summary.cache_hit ? 'HIT' : 'MISS'}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (summary.visual_query_detected) {
|
|
93
|
+
const count = summary.visual_results_count || 0;
|
|
94
|
+
parts.push(`visual:${count}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (summary.results_count !== undefined) {
|
|
98
|
+
parts.push(`${summary.results_count} results`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
parts.push('OK');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parts.join(' | ');
|
|
105
|
+
}
|