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
package/bin/uninstall.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gramatr uninstaller — reverses install, restores configs to pre-gramatr state.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun uninstall.ts # Interactive — confirms each step
|
|
7
|
+
* bun uninstall.ts --yes # Non-interactive — uninstall everything
|
|
8
|
+
* bun uninstall.ts --keep-auth # Keep ~/.gmtr.json (preserve login)
|
|
9
|
+
* npx tsx uninstall.ts # Without bun
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { createInterface } from 'readline';
|
|
15
|
+
|
|
16
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const YES = args.includes('--yes') || args.includes('-y');
|
|
19
|
+
const KEEP_AUTH = args.includes('--keep-auth');
|
|
20
|
+
|
|
21
|
+
function log(msg: string): void { process.stdout.write(`${msg}\n`); }
|
|
22
|
+
|
|
23
|
+
async function confirm(msg: string): Promise<boolean> {
|
|
24
|
+
if (YES) return true;
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
rl.question(` ${msg} [Y/n]: `, (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(!answer || answer.toLowerCase() === 'y');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findLatestBackup(filePath: string): string | null {
|
|
35
|
+
const dir = dirname(filePath);
|
|
36
|
+
const base = filePath.split('/').pop() || '';
|
|
37
|
+
try {
|
|
38
|
+
const files = readdirSync(dir)
|
|
39
|
+
.filter(f => f.startsWith(`${base}.backup-`))
|
|
40
|
+
.sort()
|
|
41
|
+
.reverse();
|
|
42
|
+
return files.length > 0 ? join(dir, files[0]) : null;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function removeGramatrFromJson(filePath: string, keys: string[]): boolean {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(filePath)) return false;
|
|
49
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
50
|
+
let changed = false;
|
|
51
|
+
for (const key of keys) {
|
|
52
|
+
if (key in data) {
|
|
53
|
+
delete data[key];
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
// Handle nested keys like "env.GMTR_TOKEN"
|
|
57
|
+
if (key.includes('.')) {
|
|
58
|
+
const [parent, child] = key.split('.');
|
|
59
|
+
if (data[parent] && child in data[parent]) {
|
|
60
|
+
delete data[parent][child];
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Remove gramatr from mcpServers
|
|
66
|
+
if (data.mcpServers?.gramatr) {
|
|
67
|
+
delete data.mcpServers.gramatr;
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
if (changed) {
|
|
71
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
72
|
+
}
|
|
73
|
+
return changed;
|
|
74
|
+
} catch { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dirSize(dir: string): string {
|
|
78
|
+
try {
|
|
79
|
+
let total = 0;
|
|
80
|
+
const walk = (d: string) => {
|
|
81
|
+
for (const f of readdirSync(d)) {
|
|
82
|
+
const p = join(d, f);
|
|
83
|
+
const s = statSync(p);
|
|
84
|
+
if (s.isDirectory()) walk(p); else total += s.size;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(dir);
|
|
88
|
+
if (total > 1048576) return `${(total / 1048576).toFixed(1)}MB`;
|
|
89
|
+
if (total > 1024) return `${(total / 1024).toFixed(0)}KB`;
|
|
90
|
+
return `${total}B`;
|
|
91
|
+
} catch { return '?'; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main(): Promise<void> {
|
|
95
|
+
log('');
|
|
96
|
+
log(' gramatr uninstaller');
|
|
97
|
+
log(' ===================');
|
|
98
|
+
log('');
|
|
99
|
+
|
|
100
|
+
const gmtrClient = join(HOME, 'gmtr-client');
|
|
101
|
+
const gmtrJson = join(HOME, '.gmtr.json');
|
|
102
|
+
const gmtrDotDir = join(HOME, '.gmtr');
|
|
103
|
+
const claudeSettings = join(HOME, '.claude', 'settings.json');
|
|
104
|
+
const claudeJson = join(HOME, '.claude.json');
|
|
105
|
+
const claudeSkills = join(HOME, '.claude', 'skills');
|
|
106
|
+
const claudeAgents = join(HOME, '.claude', 'agents');
|
|
107
|
+
const claudeCommands = join(HOME, '.claude', 'commands');
|
|
108
|
+
const codexHooks = join(HOME, '.codex', 'hooks.json');
|
|
109
|
+
const codexAgents = join(HOME, '.codex', 'AGENTS.md');
|
|
110
|
+
const geminiExt = join(HOME, '.gemini', 'extensions', 'gramatr');
|
|
111
|
+
|
|
112
|
+
// Detect what's installed
|
|
113
|
+
const installed: string[] = [];
|
|
114
|
+
if (existsSync(gmtrClient)) installed.push(`~/gmtr-client (${dirSize(gmtrClient)})`);
|
|
115
|
+
if (existsSync(gmtrDotDir)) installed.push(`~/.gmtr/ (${dirSize(gmtrDotDir)})`);
|
|
116
|
+
if (existsSync(gmtrJson)) installed.push('~/.gmtr.json');
|
|
117
|
+
if (existsSync(claudeSettings)) installed.push('~/.claude/settings.json (hooks)');
|
|
118
|
+
if (existsSync(claudeJson)) installed.push('~/.claude.json (MCP server)');
|
|
119
|
+
if (existsSync(codexHooks)) installed.push('~/.codex/hooks.json');
|
|
120
|
+
if (existsSync(geminiExt)) installed.push(`~/.gemini/extensions/gramatr (${dirSize(geminiExt)})`);
|
|
121
|
+
|
|
122
|
+
if (installed.length === 0) {
|
|
123
|
+
log(' Nothing to uninstall — gramatr is not installed.');
|
|
124
|
+
log('');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
log(' Found:');
|
|
129
|
+
for (const item of installed) log(` - ${item}`);
|
|
130
|
+
log('');
|
|
131
|
+
|
|
132
|
+
if (!await confirm('Proceed with uninstall?')) {
|
|
133
|
+
log(' Cancelled.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
log('');
|
|
138
|
+
|
|
139
|
+
// 1. Remove ~/gmtr-client (hooks, bin, skills, agents, tools)
|
|
140
|
+
if (existsSync(gmtrClient)) {
|
|
141
|
+
rmSync(gmtrClient, { recursive: true, force: true });
|
|
142
|
+
log(' OK Removed ~/gmtr-client');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. Remove ~/.gmtr/ (bun binary, PATH additions)
|
|
146
|
+
if (existsSync(gmtrDotDir)) {
|
|
147
|
+
rmSync(gmtrDotDir, { recursive: true, force: true });
|
|
148
|
+
log(' OK Removed ~/.gmtr/');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Remove ~/.gmtr.json (auth token) — unless --keep-auth
|
|
152
|
+
if (existsSync(gmtrJson)) {
|
|
153
|
+
if (KEEP_AUTH) {
|
|
154
|
+
log(' -- Kept ~/.gmtr.json (--keep-auth)');
|
|
155
|
+
} else {
|
|
156
|
+
rmSync(gmtrJson);
|
|
157
|
+
log(' OK Removed ~/.gmtr.json');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 4. Restore ~/.claude/settings.json from backup or clean gramatr hooks
|
|
162
|
+
if (existsSync(claudeSettings)) {
|
|
163
|
+
const backup = findLatestBackup(claudeSettings);
|
|
164
|
+
if (backup && await confirm(`Restore settings.json from backup (${backup.split('/').pop()})?`)) {
|
|
165
|
+
const backupContent = readFileSync(backup, 'utf8');
|
|
166
|
+
writeFileSync(claudeSettings, backupContent);
|
|
167
|
+
log(` OK Restored ~/.claude/settings.json from ${backup.split('/').pop()}`);
|
|
168
|
+
} else {
|
|
169
|
+
// Remove gramatr hooks from settings
|
|
170
|
+
try {
|
|
171
|
+
const settings = JSON.parse(readFileSync(claudeSettings, 'utf8'));
|
|
172
|
+
if (settings.hooks) {
|
|
173
|
+
// Remove hook entries that reference gmtr-client
|
|
174
|
+
for (const [event, hooks] of Object.entries(settings.hooks)) {
|
|
175
|
+
if (Array.isArray(hooks)) {
|
|
176
|
+
settings.hooks[event] = (hooks as any[]).filter(
|
|
177
|
+
(h: any) => !h.command?.includes('gmtr-client') && !h.command?.includes('gramatr')
|
|
178
|
+
);
|
|
179
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
183
|
+
}
|
|
184
|
+
if (settings.statusLine?.command?.includes('gmtr-client')) {
|
|
185
|
+
delete settings.statusLine;
|
|
186
|
+
}
|
|
187
|
+
writeFileSync(claudeSettings, JSON.stringify(settings, null, 2) + '\n');
|
|
188
|
+
log(' OK Removed gramatr hooks from ~/.claude/settings.json');
|
|
189
|
+
} catch {
|
|
190
|
+
log(' X Could not clean settings.json — restore from backup manually');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 5. Remove gramatr from ~/.claude.json (MCP server + env vars)
|
|
196
|
+
if (existsSync(claudeJson)) {
|
|
197
|
+
const cleaned = removeGramatrFromJson(claudeJson, ['env.GMTR_TOKEN', 'env.AIOS_MCP_TOKEN']);
|
|
198
|
+
if (cleaned) log(' OK Removed gramatr MCP server + env vars from ~/.claude.json');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 6. Remove gramatr skills from ~/.claude/skills/
|
|
202
|
+
if (existsSync(claudeSkills)) {
|
|
203
|
+
try {
|
|
204
|
+
const skillDirs = readdirSync(claudeSkills);
|
|
205
|
+
let removed = 0;
|
|
206
|
+
for (const dir of skillDirs) {
|
|
207
|
+
const skillPath = join(claudeSkills, dir);
|
|
208
|
+
// Check if it's a gramatr-installed skill (has gramatr marker or matches known patterns)
|
|
209
|
+
if (existsSync(join(skillPath, '.gramatr')) || existsSync(join(skillPath, 'README.md'))) {
|
|
210
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
211
|
+
removed++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (removed > 0) log(` OK Removed ${removed} skill directories from ~/.claude/skills/`);
|
|
215
|
+
} catch { /* non-critical */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 7. Remove gramatr agents from ~/.claude/agents/
|
|
219
|
+
if (existsSync(claudeAgents)) {
|
|
220
|
+
try {
|
|
221
|
+
rmSync(claudeAgents, { recursive: true, force: true });
|
|
222
|
+
log(' OK Removed ~/.claude/agents/');
|
|
223
|
+
} catch { /* non-critical */ }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 8. Remove gramatr commands from ~/.claude/commands/
|
|
227
|
+
if (existsSync(claudeCommands)) {
|
|
228
|
+
try {
|
|
229
|
+
const cmds = readdirSync(claudeCommands).filter(f => f.startsWith('gmtr-') || f.startsWith('gramatr-'));
|
|
230
|
+
for (const cmd of cmds) {
|
|
231
|
+
rmSync(join(claudeCommands, cmd), { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
if (cmds.length > 0) log(` OK Removed ${cmds.length} gramatr commands from ~/.claude/commands/`);
|
|
234
|
+
} catch { /* non-critical */ }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 9. Clean Codex config
|
|
238
|
+
if (existsSync(codexHooks)) {
|
|
239
|
+
try {
|
|
240
|
+
const hooks = JSON.parse(readFileSync(codexHooks, 'utf8'));
|
|
241
|
+
if (hooks.hooks) {
|
|
242
|
+
for (const [event, eventHooks] of Object.entries(hooks.hooks)) {
|
|
243
|
+
if (Array.isArray(eventHooks)) {
|
|
244
|
+
hooks.hooks[event] = (eventHooks as any[]).filter(
|
|
245
|
+
(h: any) => !h.command?.includes('gmtr-client') && !h.command?.includes('gramatr')
|
|
246
|
+
);
|
|
247
|
+
if (hooks.hooks[event].length === 0) delete hooks.hooks[event];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
writeFileSync(codexHooks, JSON.stringify(hooks, null, 2) + '\n');
|
|
251
|
+
log(' OK Cleaned gramatr hooks from ~/.codex/hooks.json');
|
|
252
|
+
}
|
|
253
|
+
} catch { /* non-critical */ }
|
|
254
|
+
}
|
|
255
|
+
if (existsSync(codexAgents)) {
|
|
256
|
+
// Remove managed block from AGENTS.md
|
|
257
|
+
try {
|
|
258
|
+
let content = readFileSync(codexAgents, 'utf8');
|
|
259
|
+
const startMarker = '<!-- GMTR-CODEX-START -->';
|
|
260
|
+
const endMarker = '<!-- GMTR-CODEX-END -->';
|
|
261
|
+
const startIdx = content.indexOf(startMarker);
|
|
262
|
+
const endIdx = content.indexOf(endMarker);
|
|
263
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
264
|
+
content = content.slice(0, startIdx) + content.slice(endIdx + endMarker.length);
|
|
265
|
+
writeFileSync(codexAgents, content.trim() + '\n');
|
|
266
|
+
log(' OK Removed gramatr block from ~/.codex/AGENTS.md');
|
|
267
|
+
}
|
|
268
|
+
} catch { /* non-critical */ }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 10. Remove Gemini extension
|
|
272
|
+
if (existsSync(geminiExt)) {
|
|
273
|
+
rmSync(geminiExt, { recursive: true, force: true });
|
|
274
|
+
log(' OK Removed ~/.gemini/extensions/gramatr');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
log('');
|
|
278
|
+
log(' Uninstall complete.');
|
|
279
|
+
log(' Restart Claude Code / Codex / Gemini CLI to apply changes.');
|
|
280
|
+
if (KEEP_AUTH) {
|
|
281
|
+
log(' Auth token preserved in ~/.gmtr.json (use --token with next install).');
|
|
282
|
+
}
|
|
283
|
+
log('');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
main().catch((err) => {
|
|
287
|
+
log(` Error: ${err.message}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* version-sync — stamps the current package.json version into files that display it.
|
|
4
|
+
*
|
|
5
|
+
* Called automatically by:
|
|
6
|
+
* npm run version:patch → 0.3.0 → 0.3.1
|
|
7
|
+
* npm run version:minor → 0.3.0 → 0.4.0
|
|
8
|
+
* npm run version:major → 0.3.0 → 1.0.0
|
|
9
|
+
* npm run prepublishOnly → ensures sync before publish
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const clientDir = dirname(__dirname);
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(clientDir, 'package.json'), 'utf8'));
|
|
19
|
+
const version = pkg.version;
|
|
20
|
+
|
|
21
|
+
console.log(`gramatr version: ${version}`);
|
|
22
|
+
|
|
23
|
+
// 1. Stamp into settings.json template (install.ts writes this)
|
|
24
|
+
// No file to patch — install.ts reads package.json at runtime.
|
|
25
|
+
|
|
26
|
+
// 2. Stamp into CLAUDE.md if it has a version marker
|
|
27
|
+
const claudeMd = join(clientDir, 'CLAUDE.md');
|
|
28
|
+
if (existsSync(claudeMd)) {
|
|
29
|
+
let content = readFileSync(claudeMd, 'utf8');
|
|
30
|
+
const versionPattern = /gramatr\s+v[\d.]+/g;
|
|
31
|
+
const newContent = content.replace(versionPattern, `gramatr v${version}`);
|
|
32
|
+
if (newContent !== content) {
|
|
33
|
+
writeFileSync(claudeMd, newContent);
|
|
34
|
+
console.log(` OK Stamped version in CLAUDE.md`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. Write a version.ts module that other TS files can import
|
|
39
|
+
const versionTs = join(clientDir, 'core', 'version.ts');
|
|
40
|
+
writeFileSync(versionTs, `/** Auto-generated by version-sync.ts — do not edit */\nexport const VERSION = '${version}';\n`);
|
|
41
|
+
console.log(` OK Wrote core/version.ts`);
|
|
42
|
+
|
|
43
|
+
// 4. Log next steps
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(` To publish: npm publish --access public`);
|
|
46
|
+
console.log(` To tag: git tag v${version} && git push origin v${version}`);
|
package/codex/README.md
ADDED
|
@@ -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 @aios-v2/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,76 @@
|
|
|
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
|
+
});
|
|
38
|
+
const route = result.route as RouteResponse | null;
|
|
39
|
+
|
|
40
|
+
if (!route) {
|
|
41
|
+
if (result.error) {
|
|
42
|
+
const failure = describeRoutingFailure(result.error);
|
|
43
|
+
const output = buildCodexHookOutput(
|
|
44
|
+
'UserPromptSubmit',
|
|
45
|
+
buildHookFailureAdditionalContext(failure),
|
|
46
|
+
'gramatr request routing unavailable',
|
|
47
|
+
);
|
|
48
|
+
process.stdout.write(JSON.stringify(output));
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const additionalContext = buildUserPromptAdditionalContext(route);
|
|
54
|
+
if (git) {
|
|
55
|
+
persistClassificationResult({
|
|
56
|
+
rootDir: git.root,
|
|
57
|
+
prompt,
|
|
58
|
+
route,
|
|
59
|
+
downstreamModel: null,
|
|
60
|
+
clientType: 'codex',
|
|
61
|
+
agentName: 'Codex',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const output = buildCodexHookOutput(
|
|
65
|
+
'UserPromptSubmit',
|
|
66
|
+
additionalContext,
|
|
67
|
+
'gramatr request routing active',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
process.stdout.write(JSON.stringify(output));
|
|
71
|
+
} catch {
|
|
72
|
+
// Never block the user prompt if the hook fails.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void main();
|
package/codex/install.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
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 packagesDir = dirname(clientSourceDir);
|
|
60
|
+
const repoRoot = dirname(packagesDir);
|
|
61
|
+
const codexTargetDir = join(gmtrDir, 'codex');
|
|
62
|
+
const sharedHookUtilsSource = join(clientSourceDir, 'hooks', 'lib', 'gmtr-hook-utils.ts');
|
|
63
|
+
const sharedHookUtilsTarget = join(gmtrDir, 'hooks', 'lib', 'gmtr-hook-utils.ts');
|
|
64
|
+
const managedAgentsContent = readFileSync(join(repoRoot, 'AGENTS.md'), 'utf8');
|
|
65
|
+
|
|
66
|
+
ensureDir(gmtrDir);
|
|
67
|
+
ensureDir(codexHome);
|
|
68
|
+
|
|
69
|
+
copyRecursive(codexSourceDir, codexTargetDir);
|
|
70
|
+
copyRecursive(sharedHookUtilsSource, sharedHookUtilsTarget);
|
|
71
|
+
log(`OK Synced Codex runtime to ${codexTargetDir}`);
|
|
72
|
+
|
|
73
|
+
const managedHooks = buildManagedHooks(gmtrDir);
|
|
74
|
+
const existingHooks = readJsonFile(hooksPath, { hooks: {} });
|
|
75
|
+
const mergedHooks = mergeHooksFile(existingHooks, managedHooks);
|
|
76
|
+
writeFileSync(hooksPath, `${JSON.stringify(mergedHooks, null, 2)}\n`, 'utf8');
|
|
77
|
+
log(`OK Updated ${hooksPath}`);
|
|
78
|
+
|
|
79
|
+
const existingConfig = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
|
80
|
+
const updatedConfig = ensureCodexHooksFeature(existingConfig);
|
|
81
|
+
writeFileSync(configPath, updatedConfig, 'utf8');
|
|
82
|
+
log(`OK Enabled Codex hooks in ${configPath}`);
|
|
83
|
+
|
|
84
|
+
const existingAgents = existsSync(agentsPath) ? readFileSync(agentsPath, 'utf8') : '';
|
|
85
|
+
const managedAgents = upsertManagedBlock(
|
|
86
|
+
existingAgents,
|
|
87
|
+
managedAgentsContent,
|
|
88
|
+
START_MARKER,
|
|
89
|
+
END_MARKER,
|
|
90
|
+
);
|
|
91
|
+
writeFileSync(agentsPath, managedAgents, 'utf8');
|
|
92
|
+
log(`OK Updated ${agentsPath}`);
|
|
93
|
+
|
|
94
|
+
log('');
|
|
95
|
+
log('Codex installer complete.');
|
|
96
|
+
log('Restart Codex or start a new session to load the updated hook configuration.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
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
|
+
}
|