@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,95 @@
1
+ # gramatr - ChatGPT Desktop Integration
2
+
3
+ Tier 3 integration: MCP only (no hooks, no status line).
4
+
5
+ ## Prerequisites
6
+
7
+ - **ChatGPT Plus, Team, or Enterprise** subscription (free accounts do not support MCP)
8
+ - **ChatGPT Desktop** app installed ([download](https://openai.com/chatgpt/desktop))
9
+ - **Developer Mode** enabled in ChatGPT Desktop settings
10
+ - A **gramatr API key** — get one at [gramatr.com/settings](https://gramatr.com/settings)
11
+
12
+ ## Installation
13
+
14
+ ### Method 1: Installer script (recommended)
15
+
16
+ ```bash
17
+ cd packages/client
18
+ bun chatgpt/install.ts
19
+ ```
20
+
21
+ The installer will:
22
+ 1. Find your API key from `~/.gmtr.json`, `GRAMATR_API_KEY` env, or prompt you
23
+ 2. Validate connectivity to the gramatr server
24
+ 3. Detect your platform and locate the ChatGPT config file
25
+ 4. Merge the gramatr MCP server entry without overwriting existing servers
26
+ 5. Print verification instructions
27
+
28
+ ### Method 2: Environment variable
29
+
30
+ ```bash
31
+ GRAMATR_API_KEY=your-key-here bun chatgpt/install.ts
32
+ ```
33
+
34
+ ### Method 3: Manual configuration
35
+
36
+ Add to your ChatGPT MCP config file:
37
+
38
+ **macOS:** `~/.chatgpt/mcp.json`
39
+ **Windows:** `%APPDATA%\ChatGPT\mcp.json`
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "gramatr": {
45
+ "url": "https://mcp.gramatr.com/mcp",
46
+ "headers": {
47
+ "Authorization": "Bearer YOUR_API_KEY"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Verification
55
+
56
+ 1. Open ChatGPT Desktop
57
+ 2. Go to **Settings > Developer > MCP Servers**
58
+ 3. Confirm "gramatr" appears in the list with a green status indicator
59
+ 4. Start a new conversation and type: "What gramatr tools are available?"
60
+ 5. ChatGPT should list the available MCP tools from the gramatr server
61
+
62
+ ## Config file locations
63
+
64
+ | Platform | Path |
65
+ |----------|------|
66
+ | macOS | `~/.chatgpt/mcp.json` |
67
+ | Windows | `%APPDATA%\ChatGPT\mcp.json` |
68
+
69
+ ## Limitations
70
+
71
+ ChatGPT Desktop is a **Tier 3** integration:
72
+
73
+ - MCP tools are available (search, create entities, route requests, etc.)
74
+ - No PostToolUse hooks (no automatic metrics tracking)
75
+ - No status line
76
+ - No prompt enrichment hooks
77
+
78
+ For the full gramatr experience with hooks, status line, and prompt enrichment, use Claude Code or Codex.
79
+
80
+ ## Troubleshooting
81
+
82
+ **"gramatr" not showing in MCP servers:**
83
+ - Ensure Developer Mode is enabled in ChatGPT Desktop settings
84
+ - Check that the config file exists at the correct path
85
+ - Restart ChatGPT Desktop after editing the config
86
+
87
+ **Connection errors:**
88
+ - Verify your API key is valid: `bun bin/gmtr-login.ts --status`
89
+ - Check server health: `curl https://api.gramatr.com/health`
90
+ - Ensure you have an active internet connection
91
+
92
+ **Tools not appearing in conversation:**
93
+ - MCP tools may take a moment to load after connecting
94
+ - Try starting a new conversation
95
+ - Check the MCP server status indicator in Settings > Developer
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { dirname } from 'path';
5
+ import { homedir } from 'os';
6
+ import {
7
+ getChatGPTConfigPath,
8
+ mergeChatGPTConfig,
9
+ buildMcpServerEntry,
10
+ type ChatGPTConfig,
11
+ } from './lib/chatgpt-install-utils.ts';
12
+ import { resolveAuthToken } from '../core/auth.ts';
13
+
14
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
15
+ const VALIDATION_ENDPOINT = 'https://api.gramatr.com/health';
16
+
17
+ function log(message: string): void {
18
+ process.stdout.write(`${message}\n`);
19
+ }
20
+
21
+ function readJsonFile<T>(path: string, fallback: T): T {
22
+ if (!existsSync(path)) return fallback;
23
+ try {
24
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
25
+ } catch {
26
+ return fallback;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Validate token against gramatr server health endpoint.
32
+ * Returns true if server is reachable (we don't enforce auth for install — server validates on use).
33
+ */
34
+ async function validateServer(serverUrl: string): Promise<boolean> {
35
+ try {
36
+ const baseUrl = serverUrl.replace(/\/mcp$/, '');
37
+ const response = await fetch(`${baseUrl}/health`, {
38
+ method: 'GET',
39
+ signal: AbortSignal.timeout(5000),
40
+ });
41
+ return response.ok;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async function main(): Promise<void> {
48
+ const home = homedir();
49
+ const platform = process.platform;
50
+
51
+ log('');
52
+ log('gramatr - ChatGPT Desktop installer');
53
+ log('====================================');
54
+ log('');
55
+
56
+ // Step 1: Resolve auth (OAuth-first via shared helper — issue #484)
57
+ log('Step 1: Resolving authentication...');
58
+ const apiKey = await resolveAuthToken({
59
+ interactive: true,
60
+ installerLabel: 'ChatGPT Desktop',
61
+ });
62
+ log(' OK Authenticated');
63
+
64
+ // Step 2: Validate server connectivity
65
+ log('');
66
+ log('Step 2: Validating server connectivity...');
67
+ const serverUrl = process.env.GMTR_URL || DEFAULT_MCP_URL;
68
+ const serverReachable = await validateServer(serverUrl);
69
+ if (serverReachable) {
70
+ log(` OK Server reachable at ${serverUrl.replace(/\/mcp$/, '')}`);
71
+ } else {
72
+ log(` WARN Server not reachable at ${serverUrl.replace(/\/mcp$/, '')} — config will be written anyway`);
73
+ }
74
+
75
+ // Step 3: Detect platform and config path
76
+ log('');
77
+ log('Step 3: Detecting ChatGPT Desktop...');
78
+ const configPath = getChatGPTConfigPath(home, platform);
79
+ const configDir = dirname(configPath);
80
+
81
+ if (platform === 'darwin') {
82
+ log(' Platform: macOS');
83
+ } else if (platform === 'win32') {
84
+ log(' Platform: Windows');
85
+ } else {
86
+ log(` Platform: ${platform} (ChatGPT Desktop may not be available)`);
87
+ }
88
+ log(` Config: ${configPath}`);
89
+
90
+ // Step 4: Read existing config
91
+ log('');
92
+ log('Step 4: Reading existing config...');
93
+ const existing = readJsonFile<ChatGPTConfig>(configPath, {});
94
+
95
+ const existingServerCount = existing.mcpServers ? Object.keys(existing.mcpServers).length : 0;
96
+ if (existingServerCount > 0) {
97
+ log(` Found ${existingServerCount} existing MCP server(s)`);
98
+ if (existing.mcpServers?.gramatr) {
99
+ log(' Existing gramatr entry will be updated');
100
+ }
101
+ } else {
102
+ log(' No existing config (will create new)');
103
+ }
104
+
105
+ // Step 5: Merge and write config
106
+ log('');
107
+ log('Step 5: Writing config...');
108
+ const gramatrEntry = buildMcpServerEntry(apiKey, serverUrl);
109
+ const merged = mergeChatGPTConfig(existing, gramatrEntry);
110
+
111
+ if (!existsSync(configDir)) {
112
+ mkdirSync(configDir, { recursive: true });
113
+ log(` Created directory: ${configDir}`);
114
+ }
115
+
116
+ writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
117
+ log(` OK Written to ${configPath}`);
118
+
119
+ // Summary
120
+ log('');
121
+ log('Installation complete.');
122
+ log('');
123
+ log('Next steps:');
124
+ log(' 1. Open ChatGPT Desktop');
125
+ log(' 2. Go to Settings > Developer > MCP Servers');
126
+ log(' 3. Verify "gramatr" appears in the server list');
127
+ log(' 4. Start a conversation and gramatr tools will be available');
128
+ log('');
129
+ log('What was configured:');
130
+ log(` MCP server: ${serverUrl}`);
131
+ log(` Config file: ${configPath}`);
132
+ log('');
133
+ log('Note: ChatGPT Desktop is Tier 3 (MCP only). Hooks and status line are not');
134
+ log('available — use Claude Code or Codex for the full gramatr experience.');
135
+ }
136
+
137
+ main().catch((err) => {
138
+ log(`ERROR: ${err.message}`);
139
+ process.exit(1);
140
+ });
@@ -0,0 +1,89 @@
1
+ import { join } from 'path';
2
+
3
+ const DEFAULT_MCP_URL = 'https://mcp.gramatr.com/mcp';
4
+
5
+ export interface ChatGPTMcpServerEntry {
6
+ gramatr: {
7
+ url?: string;
8
+ headers?: {
9
+ Authorization: string;
10
+ };
11
+ command?: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ };
15
+ }
16
+
17
+ export interface ChatGPTConfig {
18
+ mcpServers?: Record<string, unknown>;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /**
23
+ * Returns the ChatGPT Desktop config file path for the given platform.
24
+ *
25
+ * macOS: ~/.chatgpt/mcp.json
26
+ * Windows: %APPDATA%\ChatGPT\mcp.json (APPDATA = <home>\AppData\Roaming)
27
+ */
28
+ export function getChatGPTConfigPath(
29
+ home: string,
30
+ platform: string = process.platform,
31
+ ): string {
32
+ if (platform === 'win32') {
33
+ return join(home, 'AppData', 'Roaming', 'ChatGPT', 'mcp.json');
34
+ }
35
+
36
+ // macOS (darwin) and fallback for other platforms
37
+ return join(home, '.chatgpt', 'mcp.json');
38
+ }
39
+
40
+ /**
41
+ * Build the mcpServers.gramatr entry for ChatGPT Desktop config.
42
+ *
43
+ * Two modes:
44
+ * - 'http' (default): StreamableHTTP transport — ChatGPT connects directly to the remote URL.
45
+ * - 'stdio': Local bridge process — ChatGPT spawns a local process that proxies to the server.
46
+ */
47
+ export function buildMcpServerEntry(
48
+ apiKey: string,
49
+ serverUrl: string = DEFAULT_MCP_URL,
50
+ mode: 'http' | 'stdio' = 'http',
51
+ ): ChatGPTMcpServerEntry {
52
+ if (mode === 'stdio') {
53
+ return {
54
+ gramatr: {
55
+ command: 'npx',
56
+ args: ['-y', '@anthropic-ai/mcp-proxy', serverUrl],
57
+ env: {
58
+ GRAMATR_API_KEY: apiKey,
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ return {
65
+ gramatr: {
66
+ url: serverUrl,
67
+ headers: {
68
+ Authorization: `Bearer ${apiKey}`,
69
+ },
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Safely merge a gramatr MCP server entry into an existing ChatGPT Desktop config.
76
+ * Preserves all other mcpServers and top-level config keys.
77
+ */
78
+ export function mergeChatGPTConfig(
79
+ existing: ChatGPTConfig,
80
+ gramatrEntry: ChatGPTMcpServerEntry,
81
+ ): ChatGPTConfig {
82
+ return {
83
+ ...existing,
84
+ mcpServers: {
85
+ ...(existing.mcpServers || {}),
86
+ ...gramatrEntry,
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,28 @@
1
+ # Codex Integration
2
+
3
+ This directory contains the first-pass Codex client scaffolding for gramatr.
4
+
5
+ Current scope:
6
+ - TypeScript hook implementations for `UserPromptSubmit` and `SessionStart`
7
+ - pure formatting utilities with Vitest coverage
8
+ - a repo-local `.codex/hooks.json` template
9
+ - a fallback `AGENTS.md` template for Codex sessions
10
+
11
+ Runtime approach:
12
+ - use Codex hooks for interception and session restore
13
+ - keep the hook code in TypeScript
14
+ - keep formatting/decision logic in pure utility modules so coverage is meaningful
15
+
16
+ Packaging direction:
17
+ - hooks remain the runtime interception layer
18
+ - plugin packaging remains the distribution/update layer
19
+
20
+ Install locally with:
21
+ - `pnpm --filter @gramatr/client install-codex`
22
+
23
+ The installer:
24
+ - syncs this Codex runtime into `~/gmtr-client/codex`
25
+ - syncs the shared `gmtr-hook-utils.ts` dependency into `~/gmtr-client/hooks/lib`
26
+ - merges `~/.codex/hooks.json`
27
+ - enables `codex_hooks` in `~/.codex/config.toml`
28
+ - upserts a managed gramatr block in `~/.codex/AGENTS.md`
@@ -0,0 +1,73 @@
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
+ buildCodexHookOutput,
16
+ buildSessionStartAdditionalContext,
17
+ type HandoffResponse,
18
+ type SessionStartResponse,
19
+ } from '../lib/codex-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: 'codex',
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 = buildCodexHookOutput(
62
+ 'SessionStart',
63
+ additionalContext,
64
+ 'gramatr session context loaded',
65
+ );
66
+
67
+ process.stdout.write(JSON.stringify(output));
68
+ } catch {
69
+ // Never block startup if the hook fails.
70
+ }
71
+ }
72
+
73
+ void main();
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ getGitContext,
5
+ readHookInput,
6
+ } from '../../hooks/lib/gmtr-hook-utils.ts';
7
+ import { parseTranscript } from '../../hooks/lib/transcript-parser.ts';
8
+ import { submitPendingClassificationFeedback } from '../../hooks/lib/classification-feedback.ts';
9
+
10
+ async function main(): Promise<void> {
11
+ try {
12
+ const input = await readHookInput();
13
+ if (!input.transcript_path || !input.session_id) return;
14
+
15
+ const git = getGitContext();
16
+ if (!git) return;
17
+
18
+ const parsed = parseTranscript(input.transcript_path);
19
+ if (parsed.responseState !== 'completed') return;
20
+
21
+ await submitPendingClassificationFeedback({
22
+ rootDir: git.root,
23
+ sessionId: input.session_id,
24
+ originalPrompt: parsed.lastUserPrompt,
25
+ clientType: 'codex',
26
+ agentName: 'Codex',
27
+ downstreamProvider: 'openai',
28
+ });
29
+ } catch {
30
+ // Never block completion if the hook fails.
31
+ }
32
+ }
33
+
34
+ void main();
@@ -0,0 +1,79 @@
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
+ buildCodexHookOutput,
17
+ buildUserPromptAdditionalContext,
18
+ type RouteResponse,
19
+ } from '../lib/codex-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 = buildCodexHookOutput(
47
+ 'UserPromptSubmit',
48
+ buildHookFailureAdditionalContext(failure),
49
+ 'gramatr request routing unavailable',
50
+ );
51
+ process.stdout.write(JSON.stringify(output));
52
+ }
53
+ return;
54
+ }
55
+
56
+ const additionalContext = buildUserPromptAdditionalContext(route);
57
+ if (git) {
58
+ persistClassificationResult({
59
+ rootDir: git.root,
60
+ prompt,
61
+ route,
62
+ downstreamModel: null,
63
+ clientType: 'codex',
64
+ agentName: 'Codex',
65
+ });
66
+ }
67
+ const output = buildCodexHookOutput(
68
+ 'UserPromptSubmit',
69
+ additionalContext,
70
+ 'gramatr request routing active',
71
+ );
72
+
73
+ process.stdout.write(JSON.stringify(output));
74
+ } catch {
75
+ // Never block the user prompt if the hook fails.
76
+ }
77
+ }
78
+
79
+ void main();
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, copyFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import {
7
+ buildManagedHooks,
8
+ ensureCodexHooksFeature,
9
+ mergeHooksFile,
10
+ upsertManagedBlock,
11
+ } from './lib/codex-install-utils.ts';
12
+
13
+ const START_MARKER = '<!-- GMTR-CODEX-START -->';
14
+ const END_MARKER = '<!-- GMTR-CODEX-END -->';
15
+
16
+ function log(message: string): void {
17
+ process.stdout.write(`${message}\n`);
18
+ }
19
+
20
+ function ensureDir(path: string): void {
21
+ if (!existsSync(path)) mkdirSync(path, { recursive: true });
22
+ }
23
+
24
+ function copyRecursive(source: string, target: string): void {
25
+ const stats = statSync(source);
26
+
27
+ if (stats.isDirectory()) {
28
+ ensureDir(target);
29
+ for (const entry of readdirSync(source)) {
30
+ copyRecursive(join(source, entry), join(target, entry));
31
+ }
32
+ return;
33
+ }
34
+
35
+ ensureDir(dirname(target));
36
+ copyFileSync(source, target);
37
+ }
38
+
39
+ function readJsonFile<T>(path: string, fallback: T): T {
40
+ if (!existsSync(path)) return fallback;
41
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
42
+ }
43
+
44
+ export function main(): void {
45
+ const home = process.env.HOME || process.env.USERPROFILE;
46
+ if (!home) {
47
+ throw new Error('HOME is not set');
48
+ }
49
+
50
+ const gmtrDir = process.env.GMTR_DIR || join(home, 'gmtr-client');
51
+ const codexHome = join(home, '.codex');
52
+ const hooksPath = join(codexHome, 'hooks.json');
53
+ const configPath = join(codexHome, 'config.toml');
54
+ const agentsPath = join(codexHome, 'AGENTS.md');
55
+
56
+ const currentFile = fileURLToPath(import.meta.url);
57
+ const codexSourceDir = dirname(currentFile);
58
+ const clientSourceDir = dirname(codexSourceDir);
59
+ const codexTargetDir = join(gmtrDir, 'codex');
60
+ const sharedHookUtilsSource = join(clientSourceDir, 'hooks', 'lib', 'gmtr-hook-utils.ts');
61
+ const sharedHookUtilsTarget = join(gmtrDir, 'hooks', 'lib', 'gmtr-hook-utils.ts');
62
+
63
+ // AGENTS.md: check package root first (npm install), then repo root (dev)
64
+ let managedAgentsContent = '';
65
+ const agentsMdCandidates = [
66
+ join(clientSourceDir, 'AGENTS.md'), // npm package root
67
+ join(dirname(dirname(clientSourceDir)), 'AGENTS.md'), // monorepo root
68
+ ];
69
+ for (const candidate of agentsMdCandidates) {
70
+ if (existsSync(candidate)) {
71
+ managedAgentsContent = readFileSync(candidate, 'utf8');
72
+ break;
73
+ }
74
+ }
75
+ if (!managedAgentsContent) {
76
+ log(' WARN: AGENTS.md not found — skipping Codex agents block');
77
+ }
78
+
79
+ ensureDir(gmtrDir);
80
+ ensureDir(codexHome);
81
+
82
+ copyRecursive(codexSourceDir, codexTargetDir);
83
+ copyRecursive(sharedHookUtilsSource, sharedHookUtilsTarget);
84
+ log(`OK Synced Codex runtime to ${codexTargetDir}`);
85
+
86
+ const managedHooks = buildManagedHooks(gmtrDir);
87
+ const existingHooks = readJsonFile(hooksPath, { hooks: {} });
88
+ const mergedHooks = mergeHooksFile(existingHooks, managedHooks);
89
+ writeFileSync(hooksPath, `${JSON.stringify(mergedHooks, null, 2)}\n`, 'utf8');
90
+ log(`OK Updated ${hooksPath}`);
91
+
92
+ const existingConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
93
+ const updatedConfig = ensureCodexHooksFeature(existingConfig);
94
+ writeFileSync(configPath, updatedConfig, 'utf8');
95
+ log(`OK Enabled Codex hooks in ${configPath}`);
96
+
97
+ const existingAgents = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf8') : '';
98
+ const managedAgents = upsertManagedBlock(
99
+ existingAgents,
100
+ managedAgentsContent,
101
+ START_MARKER,
102
+ END_MARKER,
103
+ );
104
+ writeFileSync(agentsPath, managedAgents, 'utf8');
105
+ log(`OK Updated ${agentsPath}`);
106
+
107
+ log('');
108
+ log('Codex installer complete.');
109
+ log('Restart Codex or start a new session to load the updated hook configuration.');
110
+ }
111
+
112
+ // Run directly when executed as a script
113
+ const isDirectRun = typeof require !== 'undefined'
114
+ ? require.main === module
115
+ : !import.meta.url.includes('node_modules/gramatr/bin/');
116
+ if (isDirectRun) main();
@@ -0,0 +1,48 @@
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
+ export interface CodexHookOutput {
21
+ continue: boolean;
22
+ hookSpecificOutput: {
23
+ hookEventName: 'UserPromptSubmit' | 'SessionStart';
24
+ additionalContext: string;
25
+ };
26
+ systemMessage?: string;
27
+ }
28
+
29
+ export {
30
+ buildHookFailureAdditionalContext,
31
+ buildSessionStartAdditionalContext,
32
+ buildUserPromptAdditionalContext,
33
+ };
34
+
35
+ export function buildCodexHookOutput(
36
+ hookEventName: 'UserPromptSubmit' | 'SessionStart',
37
+ additionalContext: string,
38
+ systemMessage?: string,
39
+ ): CodexHookOutput {
40
+ return {
41
+ continue: true,
42
+ hookSpecificOutput: {
43
+ hookEventName,
44
+ additionalContext,
45
+ },
46
+ ...(systemMessage ? { systemMessage } : {}),
47
+ };
48
+ }