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
@@ -0,0 +1,292 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ export type IntegrationTargetId =
6
+ | 'claude-code'
7
+ | 'claude-desktop'
8
+ | 'chatgpt-desktop'
9
+ | 'codex'
10
+ | 'gemini-cli'
11
+ | 'remote-mcp'
12
+ | 'claude-web'
13
+ | 'chatgpt-web';
14
+
15
+ export interface IntegrationTarget {
16
+ id: IntegrationTargetId;
17
+ label: string;
18
+ kind: 'local' | 'remote';
19
+ description: string;
20
+ detect: () => DetectionResult;
21
+ }
22
+
23
+ export interface DetectionResult {
24
+ detected: boolean;
25
+ location?: string;
26
+ details?: string;
27
+ }
28
+
29
+ export interface DetectionContext {
30
+ exists: (path: string) => boolean;
31
+ readText: (path: string) => string;
32
+ home: string;
33
+ }
34
+
35
+ function defaultContext(): DetectionContext {
36
+ return {
37
+ exists: (path) => existsSync(path),
38
+ readText: (path) => (existsSync(path) ? readFileSync(path, 'utf8') : ''),
39
+ home: homedir(),
40
+ };
41
+ }
42
+
43
+ function homePath(ctx: DetectionContext, ...segments: string[]): string {
44
+ return join(ctx.home, ...segments);
45
+ }
46
+
47
+ function detectClaudeCode(ctx: DetectionContext): DetectionResult {
48
+ const claudeDir = homePath(ctx, '.claude');
49
+ if (!ctx.exists(claudeDir)) {
50
+ return { detected: false, details: '~/.claude not found' };
51
+ }
52
+
53
+ const settingsPath = join(claudeDir, 'settings.json');
54
+ return {
55
+ detected: true,
56
+ location: claudeDir,
57
+ details: ctx.exists(settingsPath) ? settingsPath : 'Claude directory exists',
58
+ };
59
+ }
60
+
61
+ function detectCodex(ctx: DetectionContext): DetectionResult {
62
+ const codexDir = homePath(ctx, '.codex');
63
+ if (!ctx.exists(codexDir)) {
64
+ return { detected: false, details: '~/.codex not found' };
65
+ }
66
+
67
+ const hooksPath = join(codexDir, 'hooks.json');
68
+ const configPath = join(codexDir, 'config.toml');
69
+ const details = [hooksPath, configPath].filter((path) => ctx.exists(path)).join(', ') || codexDir;
70
+
71
+ return {
72
+ detected: true,
73
+ location: codexDir,
74
+ details,
75
+ };
76
+ }
77
+
78
+ function detectGeminiCli(ctx: DetectionContext): DetectionResult {
79
+ const geminiDir = homePath(ctx, '.gemini');
80
+ if (!ctx.exists(geminiDir)) {
81
+ return { detected: false, details: '~/.gemini not found' };
82
+ }
83
+
84
+ const settingsPath = join(geminiDir, 'settings.json');
85
+ const extensionsDir = join(geminiDir, 'extensions');
86
+ const details = [settingsPath, extensionsDir].filter((path) => ctx.exists(path)).join(', ') || geminiDir;
87
+
88
+ return {
89
+ detected: true,
90
+ location: geminiDir,
91
+ details,
92
+ };
93
+ }
94
+
95
+ function detectClaudeDesktop(ctx: DetectionContext): DetectionResult {
96
+ // macOS path — Claude Desktop is only available on macOS and Windows
97
+ const macConfigPath = join(
98
+ ctx.home,
99
+ 'Library',
100
+ 'Application Support',
101
+ 'Claude',
102
+ 'claude_desktop_config.json',
103
+ );
104
+ // Windows path — APPDATA is typically <home>\AppData\Roaming
105
+ const winConfigPath = join(ctx.home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
106
+
107
+ if (ctx.exists(macConfigPath)) {
108
+ return {
109
+ detected: true,
110
+ location: macConfigPath,
111
+ details: 'Claude Desktop config found (macOS)',
112
+ };
113
+ }
114
+
115
+ if (ctx.exists(winConfigPath)) {
116
+ return {
117
+ detected: true,
118
+ location: winConfigPath,
119
+ details: 'Claude Desktop config found (Windows)',
120
+ };
121
+ }
122
+
123
+ return {
124
+ detected: false,
125
+ details: 'Claude Desktop config not found',
126
+ };
127
+ }
128
+
129
+ function detectChatGPTDesktop(ctx: DetectionContext): DetectionResult {
130
+ // macOS path — ChatGPT Desktop uses ~/.chatgpt/mcp.json
131
+ const macConfigDir = join(ctx.home, '.chatgpt');
132
+ const macConfigPath = join(macConfigDir, 'mcp.json');
133
+
134
+ // Windows path — APPDATA is typically <home>\AppData\Roaming
135
+ const winConfigDir = join(ctx.home, 'AppData', 'Roaming', 'ChatGPT');
136
+ const winConfigPath = join(winConfigDir, 'mcp.json');
137
+
138
+ if (ctx.exists(macConfigPath)) {
139
+ return {
140
+ detected: true,
141
+ location: macConfigPath,
142
+ details: 'ChatGPT Desktop config found (macOS)',
143
+ };
144
+ }
145
+
146
+ if (ctx.exists(macConfigDir)) {
147
+ return {
148
+ detected: true,
149
+ location: macConfigDir,
150
+ details: 'ChatGPT Desktop directory found (macOS, no mcp.json yet)',
151
+ };
152
+ }
153
+
154
+ if (ctx.exists(winConfigPath)) {
155
+ return {
156
+ detected: true,
157
+ location: winConfigPath,
158
+ details: 'ChatGPT Desktop config found (Windows)',
159
+ };
160
+ }
161
+
162
+ if (ctx.exists(winConfigDir)) {
163
+ return {
164
+ detected: true,
165
+ location: winConfigDir,
166
+ details: 'ChatGPT Desktop directory found (Windows, no mcp.json yet)',
167
+ };
168
+ }
169
+
170
+ return {
171
+ detected: false,
172
+ details: 'ChatGPT Desktop config not found',
173
+ };
174
+ }
175
+
176
+ function detectRemoteTarget(
177
+ kind: 'claude-web' | 'chatgpt-web' | 'remote-mcp',
178
+ ctx: DetectionContext,
179
+ ): DetectionResult {
180
+ const claudeJson = ctx.readText(homePath(ctx, '.claude.json'));
181
+ if (claudeJson.includes('gramatr') || claudeJson.includes('mcpServers')) {
182
+ return {
183
+ detected: true,
184
+ location: homePath(ctx, '.claude.json'),
185
+ details: 'Remote MCP-capable configuration file found',
186
+ };
187
+ }
188
+
189
+ return {
190
+ detected: false,
191
+ details:
192
+ kind === 'remote-mcp'
193
+ ? 'Remote MCP setup is available but not configured locally'
194
+ : `${kind} is a hosted target and requires remote MCP setup`,
195
+ };
196
+ }
197
+
198
+ export const INTEGRATION_TARGETS: IntegrationTarget[] = [
199
+ {
200
+ id: 'claude-code',
201
+ label: 'Claude Code',
202
+ kind: 'local',
203
+ description: 'Local Claude Code hook integration',
204
+ detect: () => detectClaudeCode(defaultContext()),
205
+ },
206
+ {
207
+ id: 'claude-desktop',
208
+ label: 'Claude Desktop',
209
+ kind: 'local',
210
+ description: 'Claude Desktop app with MCP server support (Tier 3 — MCP only, no hooks)',
211
+ detect: () => detectClaudeDesktop(defaultContext()),
212
+ },
213
+ {
214
+ id: 'chatgpt-desktop',
215
+ label: 'ChatGPT Desktop',
216
+ kind: 'local',
217
+ description: 'ChatGPT Desktop app with MCP server support (Tier 3 — MCP only, no hooks)',
218
+ detect: () => detectChatGPTDesktop(defaultContext()),
219
+ },
220
+ {
221
+ id: 'codex',
222
+ label: 'Codex',
223
+ kind: 'local',
224
+ description: 'Local Codex hook integration',
225
+ detect: () => detectCodex(defaultContext()),
226
+ },
227
+ {
228
+ id: 'gemini-cli',
229
+ label: 'Gemini CLI',
230
+ kind: 'local',
231
+ description: 'Local Gemini CLI extension integration',
232
+ detect: () => detectGeminiCli(defaultContext()),
233
+ },
234
+ {
235
+ id: 'remote-mcp',
236
+ label: 'Remote MCP',
237
+ kind: 'remote',
238
+ description: 'Shared remote MCP setup for hosted integrations',
239
+ detect: () => detectRemoteTarget('remote-mcp', defaultContext()),
240
+ },
241
+ {
242
+ id: 'claude-web',
243
+ label: 'Claude Web',
244
+ kind: 'remote',
245
+ description: 'Claude.ai web connector via remote MCP (Settings > Connectors)',
246
+ detect: () => detectRemoteTarget('claude-web', defaultContext()),
247
+ },
248
+ {
249
+ id: 'chatgpt-web',
250
+ label: 'ChatGPT Web',
251
+ kind: 'remote',
252
+ description: 'Hosted ChatGPT target via remote MCP',
253
+ detect: () => detectRemoteTarget('chatgpt-web', defaultContext()),
254
+ },
255
+ ];
256
+
257
+ export function listTargets(): IntegrationTarget[] {
258
+ return INTEGRATION_TARGETS;
259
+ }
260
+
261
+ export function findTarget(id: string): IntegrationTarget | undefined {
262
+ return INTEGRATION_TARGETS.find((target) => target.id === id);
263
+ }
264
+
265
+ export function detectTargets(): Array<IntegrationTarget & { detection: DetectionResult }> {
266
+ return detectTargetsWithContext(defaultContext());
267
+ }
268
+
269
+ export function detectTargetsWithContext(
270
+ ctx: DetectionContext,
271
+ ): Array<IntegrationTarget & { detection: DetectionResult }> {
272
+ const detectors: Record<string, (ctx: DetectionContext) => DetectionResult> = {
273
+ 'claude-code': detectClaudeCode,
274
+ 'claude-desktop': detectClaudeDesktop,
275
+ 'chatgpt-desktop': detectChatGPTDesktop,
276
+ codex: detectCodex,
277
+ 'gemini-cli': detectGeminiCli,
278
+ };
279
+
280
+ return INTEGRATION_TARGETS.map((target) => ({
281
+ ...target,
282
+ detection: detectors[target.id]
283
+ ? detectors[target.id](ctx)
284
+ : detectRemoteTarget(target.id, ctx),
285
+ }));
286
+ }
287
+
288
+ export function summarizeDetectedLocalTargets(): IntegrationTargetId[] {
289
+ return detectTargets()
290
+ .filter((target) => target.kind === 'local' && target.detection.detected)
291
+ .map((target) => target.id);
292
+ }
package/core/types.ts ADDED
@@ -0,0 +1,178 @@
1
+ export interface RouteClassification {
2
+ effort_level?: string;
3
+ intent_type?: string;
4
+ confidence?: number;
5
+ memory_tier?: string;
6
+ matched_skills?: string[];
7
+ constraints_extracted?: string[];
8
+ reverse_engineering?: {
9
+ explicit_wants?: string[];
10
+ implicit_wants?: string[];
11
+ explicit_dont_wants?: string[];
12
+ implicit_dont_wants?: string[];
13
+ gotchas?: string[];
14
+ };
15
+ suggested_capabilities?: string[];
16
+ isc_scaffold?: string[];
17
+ is_fallback?: boolean;
18
+ }
19
+
20
+ export interface CapabilityAuditEntry {
21
+ id?: number;
22
+ name?: string;
23
+ section?: string;
24
+ disposition?: string;
25
+ reason?: string;
26
+ phase?: string;
27
+ }
28
+
29
+ export interface CapabilityAuditResult {
30
+ entries?: CapabilityAuditEntry[];
31
+ use_count?: number;
32
+ decline_count?: number;
33
+ na_count?: number;
34
+ formatted_summary?: string;
35
+ }
36
+
37
+ export interface PhaseTemplate {
38
+ header?: string;
39
+ time_check?: string;
40
+ voice_message?: string;
41
+ phase_description?: string;
42
+ }
43
+
44
+ export interface QualityGateRule {
45
+ id?: string;
46
+ name?: string;
47
+ description?: string;
48
+ min_effort?: string | null;
49
+ automated?: boolean;
50
+ }
51
+
52
+ export interface QualityGateConfig {
53
+ rules?: QualityGateRule[];
54
+ min_criteria?: number;
55
+ anti_required?: boolean;
56
+ word_range?: { min?: number; max?: number };
57
+ }
58
+
59
+ export interface ContextPreLoadPlan {
60
+ entity_types?: string[];
61
+ fetch_limits?: Record<string, number>;
62
+ tier?: string;
63
+ }
64
+
65
+ export interface RouteResponse {
66
+ classification?: RouteClassification;
67
+ capability_audit?: CapabilityAuditResult;
68
+ phase_template?: PhaseTemplate;
69
+ quality_gate_config?: QualityGateConfig;
70
+ context_pre_load_plan?: ContextPreLoadPlan;
71
+ project_state?: {
72
+ project_id?: string;
73
+ active_prd_id?: string | null;
74
+ active_prd_title?: string | null;
75
+ isc_summary?: { total?: number; passing?: number; failing?: number; pending?: number };
76
+ current_phase?: string | null;
77
+ active_client?: { client_id?: string; client_type?: string } | null;
78
+ last_updated?: string;
79
+ session_history_summary?: string;
80
+ } | null;
81
+ packet_diagnostics?: {
82
+ memory_context?: {
83
+ status?: string;
84
+ requested_types?: string[];
85
+ delivered_count?: number;
86
+ error?: string;
87
+ };
88
+ project_state?: {
89
+ status?: string;
90
+ project_id?: string;
91
+ error?: string;
92
+ };
93
+ };
94
+ curated_context?: string;
95
+ context_references?: Array<{
96
+ id?: string;
97
+ type?: string;
98
+ name?: string;
99
+ }> | string[];
100
+ behavioral_directives?: string[];
101
+ suggested_agents?: Array<{
102
+ display_name?: string;
103
+ model?: string;
104
+ reason?: string;
105
+ }>;
106
+ composed_agents?: Array<{
107
+ name?: string;
108
+ display_name?: string;
109
+ system_prompt?: string;
110
+ expertise_areas?: string[];
111
+ task_domain?: string;
112
+ model_preference?: string;
113
+ context_summary?: string;
114
+ }>;
115
+ memory_context?: {
116
+ total_count?: number;
117
+ results?: Array<{
118
+ entity_name?: string;
119
+ entity_type?: string;
120
+ content?: string;
121
+ similarity?: number;
122
+ }>;
123
+ };
124
+ token_savings?: {
125
+ claude_md_reduction?: number;
126
+ observe_work_offloaded?: number;
127
+ reasoning_tokens_used?: number;
128
+ total_saved?: number;
129
+ tokens_saved?: number;
130
+ savings_ratio?: number;
131
+ };
132
+ execution_summary?: {
133
+ qwen_model?: string;
134
+ qwen_time_ms?: number;
135
+ execution_time_ms?: number;
136
+ server_version?: string;
137
+ stage_timing?: Record<string, number>;
138
+ degraded_components?: string[];
139
+ };
140
+ }
141
+
142
+ export interface SessionStartResponse {
143
+ project_id?: string;
144
+ interaction_id?: string;
145
+ entity_id?: string;
146
+ }
147
+
148
+ export interface HandoffMeta {
149
+ saved_at?: string;
150
+ branch?: string;
151
+ session_id?: string;
152
+ conversation_id?: string;
153
+ platform?: string;
154
+ legacy_missing_platform?: boolean;
155
+ }
156
+
157
+ export interface HandoffResponse {
158
+ status?: string;
159
+ source?: string;
160
+ project_id?: string;
161
+ session_id?: string;
162
+ branch?: string;
163
+ platform?: string;
164
+ created_at?: string;
165
+ section_count?: number;
166
+ _meta?: HandoffMeta;
167
+ where_we_are?: string;
168
+ what_shipped?: string;
169
+ whats_next?: string;
170
+ key_context?: string;
171
+ dont_forget?: string;
172
+ }
173
+
174
+ export interface HookFailure {
175
+ title: string;
176
+ detail: string;
177
+ action?: string;
178
+ }
@@ -0,0 +1,2 @@
1
+ /** Auto-generated by version-sync.ts — do not edit */
2
+ export const VERSION = '0.3.0';
@@ -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 @aios-v2/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();