@gramatr/client 0.5.1

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 (65) hide show
  1. package/AGENTS.md +17 -0
  2. package/CLAUDE.md +18 -0
  3. package/README.md +108 -0
  4. package/bin/add-api-key.ts +264 -0
  5. package/bin/clean-legacy-install.ts +28 -0
  6. package/bin/clear-creds.ts +141 -0
  7. package/bin/get-token.py +3 -0
  8. package/bin/gmtr-login.ts +599 -0
  9. package/bin/gramatr.js +36 -0
  10. package/bin/gramatr.ts +374 -0
  11. package/bin/install.ts +716 -0
  12. package/bin/lib/config.ts +57 -0
  13. package/bin/lib/git.ts +111 -0
  14. package/bin/lib/stdin.ts +53 -0
  15. package/bin/logout.ts +76 -0
  16. package/bin/render-claude-hooks.ts +16 -0
  17. package/bin/statusline.ts +81 -0
  18. package/bin/uninstall.ts +289 -0
  19. package/chatgpt/README.md +95 -0
  20. package/chatgpt/install.ts +140 -0
  21. package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
  22. package/codex/README.md +28 -0
  23. package/codex/hooks/session-start.ts +73 -0
  24. package/codex/hooks/stop.ts +34 -0
  25. package/codex/hooks/user-prompt-submit.ts +79 -0
  26. package/codex/install.ts +116 -0
  27. package/codex/lib/codex-hook-utils.ts +48 -0
  28. package/codex/lib/codex-install-utils.ts +123 -0
  29. package/core/auth.ts +170 -0
  30. package/core/feedback.ts +55 -0
  31. package/core/formatting.ts +179 -0
  32. package/core/install.ts +107 -0
  33. package/core/installer-cli.ts +122 -0
  34. package/core/migration.ts +479 -0
  35. package/core/routing.ts +108 -0
  36. package/core/session.ts +202 -0
  37. package/core/targets.ts +292 -0
  38. package/core/types.ts +179 -0
  39. package/core/version-check.ts +219 -0
  40. package/core/version.ts +47 -0
  41. package/desktop/README.md +72 -0
  42. package/desktop/build-mcpb.ts +166 -0
  43. package/desktop/install.ts +136 -0
  44. package/desktop/lib/desktop-install-utils.ts +70 -0
  45. package/gemini/README.md +95 -0
  46. package/gemini/hooks/session-start.ts +72 -0
  47. package/gemini/hooks/stop.ts +30 -0
  48. package/gemini/hooks/user-prompt-submit.ts +77 -0
  49. package/gemini/install.ts +281 -0
  50. package/gemini/lib/gemini-hook-utils.ts +63 -0
  51. package/gemini/lib/gemini-install-utils.ts +169 -0
  52. package/hooks/GMTRPromptEnricher.hook.ts +651 -0
  53. package/hooks/GMTRRatingCapture.hook.ts +198 -0
  54. package/hooks/GMTRSecurityValidator.hook.ts +399 -0
  55. package/hooks/GMTRToolTracker.hook.ts +181 -0
  56. package/hooks/StopOrchestrator.hook.ts +78 -0
  57. package/hooks/gmtr-tool-tracker-utils.ts +105 -0
  58. package/hooks/lib/gmtr-hook-utils.ts +770 -0
  59. package/hooks/lib/identity.ts +227 -0
  60. package/hooks/lib/notify.ts +46 -0
  61. package/hooks/lib/paths.ts +104 -0
  62. package/hooks/lib/transcript-parser.ts +452 -0
  63. package/hooks/session-end.hook.ts +168 -0
  64. package/hooks/session-start.hook.ts +501 -0
  65. package/package.json +63 -0
@@ -0,0 +1,70 @@
1
+ import { join } from 'path';
2
+
3
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
4
+
5
+ export interface DesktopMcpServerEntry {
6
+ gramatr: {
7
+ url: string;
8
+ headers: {
9
+ Authorization: string;
10
+ };
11
+ };
12
+ }
13
+
14
+ export interface DesktopConfig {
15
+ mcpServers?: Record<string, unknown>;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ /**
20
+ * Returns the Claude Desktop config file path for the given platform.
21
+ *
22
+ * macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
23
+ * Windows: %APPDATA%\Claude\claude_desktop_config.json (APPDATA = <home>\AppData\Roaming)
24
+ */
25
+ export function getDesktopConfigPath(
26
+ home: string,
27
+ platform: string = process.platform,
28
+ ): string {
29
+ if (platform === 'win32') {
30
+ return join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
31
+ }
32
+
33
+ // macOS (darwin) and fallback for other platforms
34
+ return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
35
+ }
36
+
37
+ /**
38
+ * Build the mcpServers.gramatr entry for Claude Desktop config.
39
+ * Uses StreamableHTTP transport — Claude Desktop connects directly to the remote URL.
40
+ */
41
+ export function buildMcpServerEntry(
42
+ apiKey: string,
43
+ serverUrl: string = DEFAULT_MCP_URL,
44
+ ): DesktopMcpServerEntry {
45
+ return {
46
+ gramatr: {
47
+ url: serverUrl,
48
+ headers: {
49
+ Authorization: `Bearer ${apiKey}`,
50
+ },
51
+ },
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Safely merge a gramatr MCP server entry into an existing Claude Desktop config.
57
+ * Preserves all other mcpServers and top-level config keys.
58
+ */
59
+ export function mergeDesktopConfig(
60
+ existing: DesktopConfig,
61
+ gramatrEntry: DesktopMcpServerEntry,
62
+ ): DesktopConfig {
63
+ return {
64
+ ...existing,
65
+ mcpServers: {
66
+ ...(existing.mcpServers || {}),
67
+ ...gramatrEntry,
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,95 @@
1
+ # Gemini CLI Integration
2
+
3
+ This directory contains the Gemini CLI platform shim for gramatr.
4
+
5
+ gramatr is an intelligent AI middleware that pre-classifies every request using a local model before expensive LLMs process them, saving tokens on every interaction. It provides persistent vector-indexed memory, decision routing, pattern learning, and predictive suggestions across sessions and platforms.
6
+
7
+ ## Architecture
8
+
9
+ The Gemini CLI integration follows the same platform shim pattern as the Codex integration:
10
+
11
+ ```
12
+ gemini/
13
+ install.ts -- installer: copies runtime to ~/.gemini/extensions/gramatr/
14
+ hooks/
15
+ session-start.ts -- SessionStart: loads session context + handoff
16
+ user-prompt-submit.ts -- BeforeAgent: routes prompts through gramatr intelligence
17
+ stop.ts -- SessionEnd: submits classification feedback
18
+ lib/
19
+ gemini-hook-utils.ts -- Gemini-specific output formatting (GeminiHookOutput envelope)
20
+ gemini-install-utils.ts -- manifest builder, hooks.json builder, install helpers
21
+ README.md
22
+ ```
23
+
24
+ Hooks are thin adapters that call shared core functions from `core/routing.ts`, `core/session.ts`, and `core/formatting.ts`. Platform-specific concerns (output envelope format) live in `lib/gemini-hook-utils.ts`.
25
+
26
+ ## How It Works
27
+
28
+ Gemini CLI supports extensions via `~/.gemini/extensions/<name>/`. Each extension has a `gemini-extension.json` manifest that can define MCP servers, hooks, settings, and custom commands.
29
+
30
+ The gramatr extension:
31
+ 1. Registers the gramatr MCP server (Streamable HTTP at `api.gramatr.com/mcp`)
32
+ 2. Hooks into `SessionStart` to load project context and handoff state
33
+ 3. Hooks into `BeforeAgent` to route every prompt through gramatr intelligence (effort classification, capability audit, ISC scaffold, memory pre-load)
34
+ 4. Hooks into `SessionEnd` to submit classification feedback for the learning flywheel
35
+ 5. Declares `GRAMATR_API_KEY` as a sensitive setting (stored in system keychain by Gemini CLI)
36
+
37
+ ## Installation
38
+
39
+ ### Option A: From the monorepo
40
+
41
+ ```bash
42
+ pnpm --filter @gramatr/client install-gemini
43
+ ```
44
+
45
+ ### Option B: Direct
46
+
47
+ ```bash
48
+ cd packages/client && bun gemini/install.ts
49
+ ```
50
+
51
+ ### Option C: Manual (advanced)
52
+
53
+ Copy the extension to the Gemini extensions directory and configure auth:
54
+
55
+ ```bash
56
+ mkdir -p ~/.gemini/extensions/gramatr
57
+ cp -r packages/client/gemini/* ~/.gemini/extensions/gramatr/
58
+ echo "GRAMATR_API_KEY=your-key-here" > ~/.gemini/extensions/gramatr/.env
59
+ ```
60
+
61
+ ## Authentication
62
+
63
+ gramatr requires a Bearer token for all MCP calls. The installer handles this by:
64
+
65
+ 1. Checking `~/.gmtr.json` for an existing token (shared with Claude Code / Codex)
66
+ 2. Checking the `GRAMATR_API_KEY` environment variable
67
+ 3. Prompting for a token interactively
68
+
69
+ To authenticate before installing, run:
70
+
71
+ ```bash
72
+ bun packages/client/bin/gmtr-login.ts
73
+ ```
74
+
75
+ This stores the token in `~/.gmtr.json`, which the installer reads automatically.
76
+
77
+ API keys start with `gmtr_sk_` and can be created at [gramatr.com](https://gramatr.com) or via the `gmtr_create_api_key` MCP tool.
78
+
79
+ ## Hook Event Mapping
80
+
81
+ | Gemini CLI Event | gramatr Hook | Purpose |
82
+ |------------------|-------------|---------|
83
+ | `SessionStart` | `session-start.ts` | Register session, load handoff context |
84
+ | `BeforeAgent` | `user-prompt-submit.ts` | Pre-classify prompt, inject intelligence packet |
85
+ | `SessionEnd` | `stop.ts` | Submit classification feedback |
86
+
87
+ ## Verifying the Installation
88
+
89
+ After installing and restarting Gemini CLI:
90
+
91
+ ```
92
+ > @gramatr search for recent learning signals
93
+ ```
94
+
95
+ If the MCP server responds, the extension is working. If you see auth errors, re-run the installer or check `~/.gmtr.json`.
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ getGitContext,
5
+ readHookInput,
6
+ } from '../../hooks/lib/gmtr-hook-utils.ts';
7
+ import {
8
+ loadProjectHandoff,
9
+ normalizeSessionStartResponse,
10
+ persistSessionRegistration,
11
+ prepareProjectSessionState,
12
+ startRemoteSession,
13
+ } from '../../core/session.ts';
14
+ import {
15
+ buildGeminiHookOutput,
16
+ buildSessionStartAdditionalContext,
17
+ type HandoffResponse,
18
+ type SessionStartResponse,
19
+ } from '../lib/gemini-hook-utils.ts';
20
+
21
+ async function main(): Promise<void> {
22
+ try {
23
+ const input = await readHookInput();
24
+ const git = getGitContext();
25
+ if (!git) return;
26
+
27
+ const transcriptPath = input.transcript_path || '';
28
+ const sessionId = input.session_id || 'unknown';
29
+ const prepared = prepareProjectSessionState({
30
+ git,
31
+ sessionId,
32
+ transcriptPath,
33
+ });
34
+
35
+ const sessionStart = (await startRemoteSession({
36
+ clientType: 'gemini-cli',
37
+ sessionId: input.session_id,
38
+ projectId: prepared.projectId,
39
+ projectName: git.projectName,
40
+ gitRemote: git.remote,
41
+ directory: git.root,
42
+ })) as SessionStartResponse | null;
43
+
44
+ if (sessionStart) {
45
+ persistSessionRegistration(git.root, sessionStart);
46
+ }
47
+
48
+ const handoff = (await loadProjectHandoff(prepared.projectId)) as HandoffResponse | null;
49
+ const normalizedSessionStart = sessionStart
50
+ ? {
51
+ ...sessionStart,
52
+ interaction_id: normalizeSessionStartResponse(sessionStart).interactionId || undefined,
53
+ }
54
+ : null;
55
+
56
+ const additionalContext = buildSessionStartAdditionalContext(
57
+ prepared.projectId,
58
+ normalizedSessionStart,
59
+ handoff,
60
+ );
61
+ const output = buildGeminiHookOutput(
62
+ additionalContext,
63
+ 'gramatr session context loaded',
64
+ );
65
+
66
+ process.stdout.write(JSON.stringify(output));
67
+ } catch {
68
+ // Never block startup if the hook fails.
69
+ }
70
+ }
71
+
72
+ void main();
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ getGitContext,
5
+ readHookInput,
6
+ } from '../../hooks/lib/gmtr-hook-utils.ts';
7
+ import { submitPendingClassificationFeedback } from '../../hooks/lib/classification-feedback.ts';
8
+
9
+ async function main(): Promise<void> {
10
+ try {
11
+ const input = await readHookInput();
12
+ if (!input.session_id) return;
13
+
14
+ const git = getGitContext();
15
+ if (!git) return;
16
+
17
+ await submitPendingClassificationFeedback({
18
+ rootDir: git.root,
19
+ sessionId: input.session_id,
20
+ originalPrompt: '',
21
+ clientType: 'gemini-cli',
22
+ agentName: 'Gemini CLI',
23
+ downstreamProvider: 'google',
24
+ });
25
+ } catch {
26
+ // Never block completion if the hook fails.
27
+ }
28
+ }
29
+
30
+ void main();
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ deriveProjectId,
5
+ getGitContext,
6
+ readHookInput,
7
+ } from '../../hooks/lib/gmtr-hook-utils.ts';
8
+ import {
9
+ describeRoutingFailure,
10
+ persistClassificationResult,
11
+ routePrompt,
12
+ shouldSkipPromptRouting,
13
+ } from '../../core/routing.ts';
14
+ import {
15
+ buildHookFailureAdditionalContext,
16
+ buildGeminiHookOutput,
17
+ buildUserPromptAdditionalContext,
18
+ type RouteResponse,
19
+ } from '../lib/gemini-hook-utils.ts';
20
+
21
+ async function main(): Promise<void> {
22
+ try {
23
+ const input = await readHookInput();
24
+ const prompt = (input.prompt || input.message || '').trim();
25
+
26
+ if (shouldSkipPromptRouting(prompt)) {
27
+ return;
28
+ }
29
+
30
+ const git = getGitContext();
31
+ const projectId = git ? deriveProjectId(git.remote, git.projectName) : undefined;
32
+ const result = await routePrompt({
33
+ prompt,
34
+ projectId,
35
+ sessionId: input.session_id,
36
+ timeoutMs: 15000,
37
+ // #496 Phase 3: ask the server for a composed [GMTR Status] block.
38
+ includeStatusline: true,
39
+ statuslineSize: 'small',
40
+ });
41
+ const route = result.route as RouteResponse | null;
42
+
43
+ if (!route) {
44
+ if (result.error) {
45
+ const failure = describeRoutingFailure(result.error);
46
+ const output = buildGeminiHookOutput(
47
+ buildHookFailureAdditionalContext(failure),
48
+ 'gramatr request routing unavailable',
49
+ );
50
+ process.stdout.write(JSON.stringify(output));
51
+ }
52
+ return;
53
+ }
54
+
55
+ const additionalContext = buildUserPromptAdditionalContext(route);
56
+ if (git) {
57
+ persistClassificationResult({
58
+ rootDir: git.root,
59
+ prompt,
60
+ route,
61
+ downstreamModel: null,
62
+ clientType: 'gemini-cli',
63
+ agentName: 'Gemini CLI',
64
+ });
65
+ }
66
+ const output = buildGeminiHookOutput(
67
+ additionalContext,
68
+ 'gramatr request routing active',
69
+ );
70
+
71
+ process.stdout.write(JSON.stringify(output));
72
+ } catch {
73
+ // Never block the user prompt if the hook fails.
74
+ }
75
+ }
76
+
77
+ void main();
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from 'fs';
12
+ import { dirname, join } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import {
15
+ buildExtensionManifest,
16
+ buildGeminiHooksFile,
17
+ getGramatrExtensionDir,
18
+ readStoredApiKey,
19
+ } from './lib/gemini-install-utils.ts';
20
+
21
+ function log(message: string): void {
22
+ process.stdout.write(`${message}\n`);
23
+ }
24
+
25
+ function ensureDir(path: string): void {
26
+ if (!existsSync(path)) mkdirSync(path, { recursive: true });
27
+ }
28
+
29
+ function copyRecursive(source: string, target: string): void {
30
+ const stats = statSync(source);
31
+
32
+ if (stats.isDirectory()) {
33
+ ensureDir(target);
34
+ for (const entry of readdirSync(source)) {
35
+ copyRecursive(join(source, entry), join(target, entry));
36
+ }
37
+ return;
38
+ }
39
+
40
+ ensureDir(dirname(target));
41
+ copyFileSync(source, target);
42
+ }
43
+
44
+ async function promptForApiKey(): Promise<string | null> {
45
+ log('');
46
+ log(' gramatr requires authentication.');
47
+ log(' Options:');
48
+ log(' 1. Run `bun gmtr-login.ts` first to authenticate via browser');
49
+ log(' 2. Paste an API key below (starts with gmtr_sk_)');
50
+ log('');
51
+ process.stdout.write(' API Key (enter to skip): ');
52
+
53
+ const { createInterface } = await import('readline');
54
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
55
+ const key = await new Promise<string>((resolve) => {
56
+ rl.on('line', (line: string) => { rl.close(); resolve(line.trim()); });
57
+ });
58
+ return key || null;
59
+ }
60
+
61
+ async function testApiKey(key: string): Promise<boolean> {
62
+ try {
63
+ const res = await fetch('https://api.gramatr.com/mcp', {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ Accept: 'application/json, text/event-stream',
68
+ Authorization: `Bearer ${key}`,
69
+ },
70
+ body: JSON.stringify({
71
+ jsonrpc: '2.0',
72
+ id: 1,
73
+ method: 'tools/call',
74
+ params: { name: 'aggregate_stats', arguments: {} },
75
+ }),
76
+ signal: AbortSignal.timeout(10000),
77
+ });
78
+
79
+ const text = await res.text();
80
+ if (
81
+ text.includes('JWT token is required') ||
82
+ text.includes('signature validation failed') ||
83
+ text.includes('Unauthorized')
84
+ ) {
85
+ return false;
86
+ }
87
+
88
+ for (const line of text.split('\n')) {
89
+ if (line.startsWith('data: ')) {
90
+ try {
91
+ const d = JSON.parse(line.slice(6));
92
+ if (d?.result?.content?.[0]?.text && !d?.result?.isError) {
93
+ return true;
94
+ }
95
+ } catch {
96
+ continue;
97
+ }
98
+ }
99
+ }
100
+ return false;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ async function resolveApiKey(home: string): Promise<string | null> {
107
+ // 1. Check if already stored in ~/.gmtr.json (shared with other platforms)
108
+ const stored = readStoredApiKey(home);
109
+ if (stored) {
110
+ log(` Found existing token in ~/.gmtr.json`);
111
+ log(' Testing token...');
112
+ const valid = await testApiKey(stored);
113
+ if (valid) {
114
+ log(' OK Token is valid');
115
+ return stored;
116
+ }
117
+ log(' Token is invalid or expired. Please provide a new one.');
118
+ }
119
+
120
+ // 2. Check env var
121
+ const envKey = process.env.GRAMATR_API_KEY;
122
+ if (envKey) {
123
+ log(' Found GRAMATR_API_KEY in environment');
124
+ log(' Testing token...');
125
+ const valid = await testApiKey(envKey);
126
+ if (valid) {
127
+ log(' OK Token is valid');
128
+ return envKey;
129
+ }
130
+ log(' Environment token is invalid.');
131
+ }
132
+
133
+ // 3. Prompt (skip in non-interactive mode)
134
+ const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y') || !process.stdin.isTTY;
135
+ if (nonInteractive) {
136
+ log(' Non-interactive: no token found. Run gmtr-login after install.');
137
+ return null;
138
+ }
139
+ const prompted = await promptForApiKey();
140
+ if (prompted) {
141
+ log(' Testing token...');
142
+ const valid = await testApiKey(prompted);
143
+ if (valid) {
144
+ log(' OK Token is valid');
145
+ // Save to ~/.gmtr.json for cross-platform reuse
146
+ const configPath = join(home, '.gmtr.json');
147
+ let config: Record<string, unknown> = {};
148
+ if (existsSync(configPath)) {
149
+ try {
150
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
151
+ } catch {
152
+ // start fresh
153
+ }
154
+ }
155
+ config.token = prompted;
156
+ config.token_type =
157
+ prompted.startsWith('gmtr_sk_') || prompted.startsWith('aios_sk_')
158
+ ? 'api_key'
159
+ : 'oauth';
160
+ config.authenticated_at = new Date().toISOString();
161
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
162
+ log(' OK Saved to ~/.gmtr.json');
163
+ return prompted;
164
+ }
165
+ log(' Token rejected by server.');
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ export async function main(): Promise<void> {
172
+ const home = process.env.HOME || process.env.USERPROFILE;
173
+ if (!home) {
174
+ throw new Error('HOME is not set');
175
+ }
176
+
177
+ log('');
178
+ log(' gramatr Gemini CLI extension installer');
179
+ log(' ======================================');
180
+
181
+ // ── Resolve authentication ──
182
+ const apiKey = await resolveApiKey(home);
183
+ if (!apiKey) {
184
+ log('');
185
+ log(' WARNING: No valid API key configured.');
186
+ log(' The extension will be installed but MCP calls will fail without auth.');
187
+ log(' Run `bun gmtr-login.ts` to authenticate, then reinstall.');
188
+ log('');
189
+ }
190
+
191
+ // ── Determine paths ──
192
+ const currentFile = fileURLToPath(import.meta.url);
193
+ const geminiSourceDir = dirname(currentFile);
194
+ const clientSourceDir = dirname(geminiSourceDir);
195
+ const extensionDir = getGramatrExtensionDir(home);
196
+ const sharedHookUtilsSource = join(clientSourceDir, 'hooks', 'lib', 'gmtr-hook-utils.ts');
197
+ const sharedFeedbackSource = join(clientSourceDir, 'hooks', 'lib', 'classification-feedback.ts');
198
+ const sharedTranscriptSource = join(clientSourceDir, 'hooks', 'lib', 'transcript-parser.ts');
199
+ const sharedIdentitySource = join(clientSourceDir, 'hooks', 'lib', 'identity.ts');
200
+ const coreDir = join(clientSourceDir, 'core');
201
+
202
+ // ── Create extension directory ──
203
+ ensureDir(extensionDir);
204
+ ensureDir(join(extensionDir, 'hooks'));
205
+ ensureDir(join(extensionDir, 'hooks', 'lib'));
206
+ ensureDir(join(extensionDir, 'core'));
207
+
208
+ // ── Copy hook files ──
209
+ copyRecursive(join(geminiSourceDir, 'hooks'), join(extensionDir, 'hooks'));
210
+ copyRecursive(join(geminiSourceDir, 'lib'), join(extensionDir, 'lib'));
211
+ log(' OK Copied Gemini hook scripts');
212
+
213
+ // ── Copy shared dependencies ──
214
+ for (const sharedFile of [sharedHookUtilsSource, sharedFeedbackSource, sharedTranscriptSource, sharedIdentitySource]) {
215
+ if (existsSync(sharedFile)) {
216
+ const relativeDest = sharedFile.replace(clientSourceDir, '');
217
+ copyFileSync(sharedFile, join(extensionDir, relativeDest));
218
+ }
219
+ }
220
+
221
+ // Copy core shared modules
222
+ if (existsSync(coreDir)) {
223
+ for (const file of readdirSync(coreDir)) {
224
+ if (file.endsWith('.ts') && !file.endsWith('.test.ts')) {
225
+ copyFileSync(join(coreDir, file), join(extensionDir, 'core', file));
226
+ }
227
+ }
228
+ }
229
+ log(' OK Copied shared core and hook utilities');
230
+
231
+ // ── Write extension manifest ──
232
+ const manifest = buildExtensionManifest();
233
+ writeFileSync(
234
+ join(extensionDir, 'gemini-extension.json'),
235
+ JSON.stringify(manifest, null, 2) + '\n',
236
+ );
237
+ log(' OK Wrote gemini-extension.json manifest');
238
+
239
+ // ── Write hooks.json ──
240
+ const hooksFile = buildGeminiHooksFile();
241
+ writeFileSync(
242
+ join(extensionDir, 'hooks', 'hooks.json'),
243
+ JSON.stringify(hooksFile, null, 2) + '\n',
244
+ );
245
+ log(' OK Wrote hooks/hooks.json');
246
+
247
+ // ── Store token in ~/.gmtr.json (canonical source, not in extension dir) ──
248
+ if (apiKey) {
249
+ const gmtrJsonPath = join(home, '.gmtr.json');
250
+ let gmtrConfig: Record<string, unknown> = {};
251
+ if (existsSync(gmtrJsonPath)) {
252
+ try { gmtrConfig = JSON.parse(readFileSync(gmtrJsonPath, 'utf8')); } catch {}
253
+ }
254
+ gmtrConfig.token = apiKey;
255
+ gmtrConfig.token_updated_at = new Date().toISOString();
256
+ writeFileSync(gmtrJsonPath, JSON.stringify(gmtrConfig, null, 2) + '\n');
257
+ log(' OK Token stored in ~/.gmtr.json (hooks read from here at runtime)');
258
+ }
259
+
260
+ // ── Summary ──
261
+ log('');
262
+ log(` Extension installed to: ${extensionDir}`);
263
+ log('');
264
+ log(' What was installed:');
265
+ log(' - gemini-extension.json (MCP server + settings manifest)');
266
+ log(' - hooks/hooks.json (SessionStart, BeforeAgent, SessionEnd)');
267
+ log(' - hooks/*.ts (gramatr hook implementations)');
268
+ log(' - core/*.ts (shared routing/session/formatting logic)');
269
+ if (apiKey) {
270
+ log(' - .env (authenticated API key)');
271
+ }
272
+ log('');
273
+ log(' Restart Gemini CLI to load the extension.');
274
+ log('');
275
+ }
276
+
277
+ // Run directly when executed as a script
278
+ const isDirectRun = typeof require !== 'undefined'
279
+ ? require.main === module
280
+ : !import.meta.url.includes('node_modules/gramatr/bin/');
281
+ if (isDirectRun) main();
@@ -0,0 +1,63 @@
1
+ import {
2
+ buildHookFailureAdditionalContext,
3
+ buildSessionStartAdditionalContext,
4
+ buildUserPromptAdditionalContext,
5
+ } from '../../core/formatting.ts';
6
+ import type {
7
+ HandoffResponse,
8
+ HookFailure,
9
+ RouteResponse,
10
+ SessionStartResponse,
11
+ } from '../../core/types.ts';
12
+
13
+ export type {
14
+ HandoffResponse,
15
+ HookFailure,
16
+ RouteResponse,
17
+ SessionStartResponse,
18
+ };
19
+
20
+ /**
21
+ * Gemini CLI hook output envelope.
22
+ *
23
+ * Gemini hooks return JSON on stdout with these fields:
24
+ * continue — whether to proceed with the agent loop
25
+ * decision — "allow" | "deny" | "block" (for gating hooks)
26
+ * systemMessage — displayed in the terminal
27
+ * hookSpecificOutput.additionalContext — injected into the LLM context
28
+ *
29
+ * See: https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md
30
+ */
31
+ export interface GeminiHookOutput {
32
+ continue: boolean;
33
+ decision?: 'allow' | 'deny';
34
+ hookSpecificOutput?: {
35
+ additionalContext?: string;
36
+ };
37
+ systemMessage?: string;
38
+ suppressOutput?: boolean;
39
+ }
40
+
41
+ export {
42
+ buildHookFailureAdditionalContext,
43
+ buildSessionStartAdditionalContext,
44
+ buildUserPromptAdditionalContext,
45
+ };
46
+
47
+ export function buildGeminiHookOutput(
48
+ additionalContext: string,
49
+ systemMessage?: string,
50
+ ): GeminiHookOutput {
51
+ return {
52
+ continue: true,
53
+ ...(additionalContext
54
+ ? {
55
+ hookSpecificOutput: {
56
+ additionalContext,
57
+ },
58
+ }
59
+ : {}),
60
+ ...(systemMessage ? { systemMessage } : {}),
61
+ suppressOutput: true,
62
+ };
63
+ }