@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.
- package/AGENTS.md +17 -0
- package/CLAUDE.md +18 -0
- package/README.md +108 -0
- package/bin/add-api-key.ts +264 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/clear-creds.ts +141 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +599 -0
- package/bin/gramatr.js +36 -0
- package/bin/gramatr.ts +374 -0
- package/bin/install.ts +716 -0
- package/bin/lib/config.ts +57 -0
- package/bin/lib/git.ts +111 -0
- package/bin/lib/stdin.ts +53 -0
- package/bin/logout.ts +76 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +81 -0
- package/bin/uninstall.ts +289 -0
- package/chatgpt/README.md +95 -0
- package/chatgpt/install.ts +140 -0
- package/chatgpt/lib/chatgpt-install-utils.ts +89 -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 +79 -0
- package/codex/install.ts +116 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/auth.ts +170 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +179 -0
- package/core/install.ts +107 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +479 -0
- package/core/routing.ts +108 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +179 -0
- package/core/version-check.ts +219 -0
- package/core/version.ts +47 -0
- package/desktop/README.md +72 -0
- package/desktop/build-mcpb.ts +166 -0
- package/desktop/install.ts +136 -0
- package/desktop/lib/desktop-install-utils.ts +70 -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 +77 -0
- package/gemini/install.ts +281 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +651 -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 +770 -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 +501 -0
- package/package.json +63 -0
package/bin/gramatr.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { detectTargets, findTarget, summarizeDetectedLocalTargets, type IntegrationTargetId } from '../core/targets.ts';
|
|
10
|
+
import { VERSION } from '../core/version.ts';
|
|
11
|
+
import { findStaleArtifacts, runDeepClean, runLegacyMigration } from '../core/migration.ts';
|
|
12
|
+
import {
|
|
13
|
+
formatDetectionLines,
|
|
14
|
+
formatDoctorLines,
|
|
15
|
+
formatRemoteGuidanceLines,
|
|
16
|
+
} from '../core/installer-cli.ts';
|
|
17
|
+
|
|
18
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
19
|
+
const binDir = dirname(currentFile);
|
|
20
|
+
const clientDir = dirname(binDir);
|
|
21
|
+
const repoRoot = dirname(dirname(clientDir));
|
|
22
|
+
|
|
23
|
+
function log(message: string = ''): void {
|
|
24
|
+
process.stdout.write(`${message}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(command: string, args: string[], env: Record<string, string | undefined> = {}): void {
|
|
28
|
+
const result = spawnSync(command, args, {
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
env: { ...process.env, ...env },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.status !== 0) {
|
|
35
|
+
process.exit(result.status ?? 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderDetections(): void {
|
|
40
|
+
for (const line of formatDetectionLines(detectTargets())) log(line);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderRemoteGuidance(): void {
|
|
44
|
+
for (const line of formatRemoteGuidanceLines(detectTargets())) log(line);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runTs(script: string, extraArgs: string[] = []): void {
|
|
48
|
+
// Resolve tsx from this package's node_modules (not CWD) so `npx tsx` works
|
|
49
|
+
// even on hosts where the user hasn't globally installed tsx.
|
|
50
|
+
try {
|
|
51
|
+
// ESM-safe: createRequire gives us require.resolve for finding tsx on disk.
|
|
52
|
+
const req = createRequire(import.meta.url);
|
|
53
|
+
const tsxCli = join(dirname(req.resolve('tsx/package.json')), 'dist', 'cli.mjs');
|
|
54
|
+
run(process.execPath, [tsxCli, script, ...extraArgs]);
|
|
55
|
+
} catch {
|
|
56
|
+
// Fallback: global npx tsx
|
|
57
|
+
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
58
|
+
run(npxBin, ['tsx', script, ...extraArgs]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Forward flags like --yes, --name, --timezone to sub-installers
|
|
63
|
+
const forwardedFlags = process.argv.slice(2).filter(a =>
|
|
64
|
+
a === '--yes' || a === '-y' || a === '--name' || a === '--timezone' ||
|
|
65
|
+
(process.argv[process.argv.indexOf(a) - 1] === '--name') ||
|
|
66
|
+
(process.argv[process.argv.indexOf(a) - 1] === '--timezone')
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
interface InstallAllResult {
|
|
70
|
+
id: IntegrationTargetId;
|
|
71
|
+
label: string;
|
|
72
|
+
status: 'ok' | 'fail' | 'skipped';
|
|
73
|
+
message?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const INSTALL_ALL_CANDIDATES: Array<{ id: IntegrationTargetId; label: string }> = [
|
|
77
|
+
{ id: 'claude-code', label: 'Claude Code' },
|
|
78
|
+
{ id: 'codex', label: 'Codex' },
|
|
79
|
+
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
80
|
+
{ id: 'claude-desktop', label: 'Claude Desktop' },
|
|
81
|
+
{ id: 'chatgpt-desktop', label: 'ChatGPT Desktop' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function installAll(): void {
|
|
85
|
+
const detections = detectTargets();
|
|
86
|
+
const detectedIds = new Set(
|
|
87
|
+
detections.filter((t) => t.kind === 'local' && t.detection.detected).map((t) => t.id),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
91
|
+
log(` gramatr v${VERSION} — install all detected`);
|
|
92
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
93
|
+
log('');
|
|
94
|
+
|
|
95
|
+
const results: InstallAllResult[] = [];
|
|
96
|
+
|
|
97
|
+
for (const candidate of INSTALL_ALL_CANDIDATES) {
|
|
98
|
+
if (!detectedIds.has(candidate.id)) {
|
|
99
|
+
results.push({ id: candidate.id, label: candidate.label, status: 'skipped', message: 'not detected' });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
log(`━━━ Installing ${candidate.label} ━━━`);
|
|
103
|
+
try {
|
|
104
|
+
installTargetForAll(candidate.id);
|
|
105
|
+
results.push({ id: candidate.id, label: candidate.label, status: 'ok' });
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
results.push({
|
|
108
|
+
id: candidate.id,
|
|
109
|
+
label: candidate.label,
|
|
110
|
+
status: 'fail',
|
|
111
|
+
message: err?.message || String(err),
|
|
112
|
+
});
|
|
113
|
+
log(` X ${candidate.label} install failed: ${err?.message || err}`);
|
|
114
|
+
}
|
|
115
|
+
log('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Summary
|
|
119
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
120
|
+
log(' gramatr install summary');
|
|
121
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
122
|
+
const pad = (s: string, n: number) => s + ' '.repeat(Math.max(1, n - s.length));
|
|
123
|
+
for (const r of results) {
|
|
124
|
+
const statusStr =
|
|
125
|
+
r.status === 'ok' ? 'OK'
|
|
126
|
+
: r.status === 'fail' ? 'FAIL'
|
|
127
|
+
: 'not detected — skipped';
|
|
128
|
+
log(` ${pad(r.label, 18)}${statusStr}${r.message && r.status === 'fail' ? ` (${r.message})` : ''}`);
|
|
129
|
+
}
|
|
130
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
131
|
+
log('');
|
|
132
|
+
|
|
133
|
+
const okList = results.filter((r) => r.status === 'ok');
|
|
134
|
+
if (okList.length > 0) {
|
|
135
|
+
log(' Next steps:');
|
|
136
|
+
let step = 1;
|
|
137
|
+
for (const r of okList) {
|
|
138
|
+
switch (r.id) {
|
|
139
|
+
case 'claude-code':
|
|
140
|
+
log(` ${step++}. Restart Claude Code to pick up MCP server config`);
|
|
141
|
+
break;
|
|
142
|
+
case 'codex':
|
|
143
|
+
log(` ${step++}. Restart Codex to load updated hooks`);
|
|
144
|
+
break;
|
|
145
|
+
case 'gemini-cli':
|
|
146
|
+
log(` ${step++}. Restart Gemini CLI to load the extension`);
|
|
147
|
+
break;
|
|
148
|
+
case 'claude-desktop':
|
|
149
|
+
log(` ${step++}. Restart Claude Desktop to load MCP server`);
|
|
150
|
+
break;
|
|
151
|
+
case 'chatgpt-desktop':
|
|
152
|
+
log(` ${step++}. Restart ChatGPT Desktop to load MCP server`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
log('');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const anyFail = results.some((r) => r.status === 'fail');
|
|
160
|
+
if (anyFail) process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function installTargetForAll(targetId: IntegrationTargetId): void {
|
|
164
|
+
switch (targetId) {
|
|
165
|
+
case 'claude-code':
|
|
166
|
+
runTs(join(binDir, 'install.ts'), forwardedFlags);
|
|
167
|
+
return;
|
|
168
|
+
case 'codex':
|
|
169
|
+
runTs(join(clientDir, 'codex', 'install.ts'), forwardedFlags);
|
|
170
|
+
return;
|
|
171
|
+
case 'gemini-cli':
|
|
172
|
+
runTs(join(clientDir, 'gemini', 'install.ts'), forwardedFlags);
|
|
173
|
+
return;
|
|
174
|
+
case 'claude-desktop':
|
|
175
|
+
runTs(join(clientDir, 'desktop', 'install.ts'), forwardedFlags);
|
|
176
|
+
return;
|
|
177
|
+
case 'chatgpt-desktop':
|
|
178
|
+
runTs(join(clientDir, 'chatgpt', 'install.ts'), forwardedFlags);
|
|
179
|
+
return;
|
|
180
|
+
default:
|
|
181
|
+
throw new Error(`Cannot install remote target '${targetId}' via install-all`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function installTarget(targetId: IntegrationTargetId): void {
|
|
186
|
+
switch (targetId) {
|
|
187
|
+
case 'claude-code':
|
|
188
|
+
runTs(join(binDir, 'install.ts'), forwardedFlags);
|
|
189
|
+
return;
|
|
190
|
+
case 'codex':
|
|
191
|
+
runTs(join(clientDir, 'codex', 'install.ts'), forwardedFlags);
|
|
192
|
+
return;
|
|
193
|
+
case 'gemini-cli':
|
|
194
|
+
runTs(join(clientDir, 'gemini', 'install.ts'), forwardedFlags);
|
|
195
|
+
return;
|
|
196
|
+
case 'remote-mcp':
|
|
197
|
+
case 'claude-web':
|
|
198
|
+
case 'chatgpt-web':
|
|
199
|
+
log(`Remote target '${targetId}' is not a local hook install.`);
|
|
200
|
+
log('Use remote MCP / hosted-client setup for this target.');
|
|
201
|
+
log('Current remote targets are documentation/setup targets, not local filesystem installs.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function migrate(apply: boolean, deep: boolean = false): void {
|
|
207
|
+
const homeDir = homedir();
|
|
208
|
+
const clientDir = process.env.GMTR_DIR || join(homeDir, 'gmtr-client');
|
|
209
|
+
runLegacyMigration({
|
|
210
|
+
homeDir,
|
|
211
|
+
clientDir,
|
|
212
|
+
includeOptionalUx: process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1',
|
|
213
|
+
apply,
|
|
214
|
+
log,
|
|
215
|
+
});
|
|
216
|
+
if (deep) {
|
|
217
|
+
log('');
|
|
218
|
+
runDeepClean({ homeDir, apply, log });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function doctor(): void {
|
|
223
|
+
const gmtrDir = process.env.GMTR_DIR || join(homedir(), 'gmtr-client');
|
|
224
|
+
const stale = findStaleArtifacts(homedir(), gmtrDir, existsSync);
|
|
225
|
+
for (const line of formatDoctorLines(detectTargets(), gmtrDir, existsSync(gmtrDir), stale)) log(line);
|
|
226
|
+
renderRemoteGuidance();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function upgrade(): void {
|
|
230
|
+
const homeDir = homedir();
|
|
231
|
+
const clientDir = process.env.GMTR_DIR || join(homeDir, 'gmtr-client');
|
|
232
|
+
const stale = findStaleArtifacts(homeDir, clientDir, existsSync);
|
|
233
|
+
if (stale.length > 0) {
|
|
234
|
+
log('Cleaning stale legacy artifacts before upgrade...');
|
|
235
|
+
runLegacyMigration({
|
|
236
|
+
homeDir,
|
|
237
|
+
clientDir,
|
|
238
|
+
includeOptionalUx: process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1',
|
|
239
|
+
apply: true,
|
|
240
|
+
log,
|
|
241
|
+
});
|
|
242
|
+
log('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const installed = summarizeDetectedLocalTargets();
|
|
246
|
+
if (installed.length === 0) {
|
|
247
|
+
log('No detected local targets to upgrade.');
|
|
248
|
+
renderRemoteGuidance();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const target of installed) {
|
|
253
|
+
log(`Upgrading ${target}...`);
|
|
254
|
+
installTarget(target);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
renderRemoteGuidance();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function main(): void {
|
|
261
|
+
// Skip forwarded-only flags (--yes, --name <v>, --timezone <v>) when picking
|
|
262
|
+
// the command and target. Keep command-specific flags like --apply, --detect,
|
|
263
|
+
// --help, --version so existing subcommands continue to work.
|
|
264
|
+
const raw = process.argv.slice(2);
|
|
265
|
+
const FORWARDED_ONLY = new Set(['--yes', '-y']);
|
|
266
|
+
const FORWARDED_WITH_VALUE = new Set(['--name', '--timezone']);
|
|
267
|
+
const positionals: string[] = [];
|
|
268
|
+
for (let i = 0; i < raw.length; i++) {
|
|
269
|
+
const a = raw[i];
|
|
270
|
+
if (FORWARDED_ONLY.has(a)) continue;
|
|
271
|
+
if (FORWARDED_WITH_VALUE.has(a)) { i++; continue; }
|
|
272
|
+
positionals.push(a);
|
|
273
|
+
}
|
|
274
|
+
const command = positionals[0] ?? 'install';
|
|
275
|
+
const targetArg = positionals[1];
|
|
276
|
+
|
|
277
|
+
switch (command) {
|
|
278
|
+
case 'install':
|
|
279
|
+
if (targetArg === '--detect') {
|
|
280
|
+
renderDetections();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!targetArg || targetArg === 'all') {
|
|
284
|
+
installAll();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (targetArg === 'help' || targetArg === '--help' || targetArg === '-h') {
|
|
288
|
+
log('gramatr install — install gramatr into detected AI platforms');
|
|
289
|
+
log('');
|
|
290
|
+
log('Usage:');
|
|
291
|
+
log(' npx gramatr install Detect every platform and install all');
|
|
292
|
+
log(' npx gramatr install all Same as above (explicit)');
|
|
293
|
+
log(' npx gramatr install <platform> Install a single platform');
|
|
294
|
+
log('');
|
|
295
|
+
log('Supported platforms:');
|
|
296
|
+
for (const c of INSTALL_ALL_CANDIDATES) {
|
|
297
|
+
log(` ${c.id.padEnd(16)}${c.label}`);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
{
|
|
302
|
+
const target = findTarget(targetArg);
|
|
303
|
+
if (!target) {
|
|
304
|
+
log(`Unknown target: ${targetArg}. Run 'gramatr install help' for usage.`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
installTarget(target.id);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
case 'login':
|
|
311
|
+
runTs(join(binDir, 'gmtr-login.ts'), forwardedFlags);
|
|
312
|
+
return;
|
|
313
|
+
case 'add-api-key':
|
|
314
|
+
runTs(join(binDir, 'add-api-key.ts'), raw.slice(1));
|
|
315
|
+
return;
|
|
316
|
+
case 'logout':
|
|
317
|
+
runTs(join(binDir, 'logout.ts'), raw.slice(1));
|
|
318
|
+
return;
|
|
319
|
+
case 'clear-creds':
|
|
320
|
+
runTs(join(binDir, 'clear-creds.ts'), raw.slice(1));
|
|
321
|
+
return;
|
|
322
|
+
case 'detect':
|
|
323
|
+
renderDetections();
|
|
324
|
+
return;
|
|
325
|
+
case 'doctor':
|
|
326
|
+
doctor();
|
|
327
|
+
return;
|
|
328
|
+
case 'migrate': {
|
|
329
|
+
const flags = positionals.slice(1);
|
|
330
|
+
const apply = flags.includes('--apply');
|
|
331
|
+
const deep = flags.includes('--deep');
|
|
332
|
+
migrate(apply, deep);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case 'upgrade':
|
|
336
|
+
upgrade();
|
|
337
|
+
return;
|
|
338
|
+
case '--help':
|
|
339
|
+
case '-h':
|
|
340
|
+
case 'help':
|
|
341
|
+
log('gramatr — your cross-agent AI brain');
|
|
342
|
+
log('');
|
|
343
|
+
log('Commands:');
|
|
344
|
+
log(' install [target] Install gramatr (claude-code, codex, gemini-cli, all)');
|
|
345
|
+
log(' login Authenticate with the gramatr server (OAuth)');
|
|
346
|
+
log(' add-api-key Add an API key explicitly (interactive / piped / --from-env)');
|
|
347
|
+
log(' logout Clear session token (~/.gmtr.json)');
|
|
348
|
+
log(' clear-creds Nuke ALL stored credentials, force OAuth on next login');
|
|
349
|
+
log(' detect Show detected CLI platforms');
|
|
350
|
+
log(' doctor Check installation health');
|
|
351
|
+
log(' upgrade Upgrade all installed targets');
|
|
352
|
+
log(' migrate [--apply] [--deep] Clean up legacy artifacts (--deep also removes PAI/Fabric/aios)');
|
|
353
|
+
log(' help Show this help');
|
|
354
|
+
log('');
|
|
355
|
+
log('Examples:');
|
|
356
|
+
log(' npx gramatr install # Interactive target selection');
|
|
357
|
+
log(' npx gramatr install claude-code # Install for Claude Code');
|
|
358
|
+
log(' npx gramatr install all # Install all detected targets');
|
|
359
|
+
return;
|
|
360
|
+
case '--version':
|
|
361
|
+
case '-v':
|
|
362
|
+
log(VERSION);
|
|
363
|
+
return;
|
|
364
|
+
default:
|
|
365
|
+
log(`Unknown command: ${command}. Run 'gramatr help' for usage.`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// import.meta.main is Bun-only; for Node/tsx, always run when executed directly
|
|
371
|
+
const isMain = (import.meta as any).main ?? true;
|
|
372
|
+
if (isMain) {
|
|
373
|
+
main();
|
|
374
|
+
}
|