gramatr 0.3.55 → 0.3.56
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/bin/gramatr.js +6 -13
- package/bin/gramatr.ts +165 -72
- package/bin/install.ts +22 -66
- package/core/install.ts +12 -19
- package/core/version-check.ts +219 -0
- package/core/version.ts +47 -2
- package/gemini/lib/gemini-install-utils.ts +3 -3
- package/hooks/session-start.hook.ts +12 -1
- package/package.json +4 -6
- package/bin/version-sync.ts +0 -46
package/bin/gramatr.js
CHANGED
|
@@ -3,25 +3,18 @@
|
|
|
3
3
|
* gramatr CLI entry point — thin JS wrapper that bootstraps TypeScript.
|
|
4
4
|
* tsx is a production dependency, resolved directly from node_modules.
|
|
5
5
|
*/
|
|
6
|
-
const { spawnSync
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
7
|
const { join, dirname } = require('path');
|
|
8
8
|
|
|
9
9
|
const script = join(__dirname, 'gramatr.ts');
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
hasBun = true;
|
|
17
|
-
} catch {}
|
|
18
|
-
|
|
19
|
-
if (hasBun) {
|
|
20
|
-
const r = spawnSync('bun', [script, ...args], { stdio: 'inherit' });
|
|
21
|
-
process.exit(r.status ?? 1);
|
|
22
|
-
}
|
|
12
|
+
// gramatr standardizes on `npx tsx` — see issue #468 for the architectural
|
|
13
|
+
// rationale. bun detection was removed because it silently produced broken
|
|
14
|
+
// hook + statusline configs on hosts where the install-time PATH did not
|
|
15
|
+
// match the runtime PATH of spawned subprocesses.
|
|
23
16
|
|
|
24
|
-
// Resolve tsx from node_modules (
|
|
17
|
+
// Resolve tsx from node_modules (tsx is a production dependency)
|
|
25
18
|
try {
|
|
26
19
|
const tsxBin = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
|
|
27
20
|
const r = spawnSync(process.execPath, [tsxBin, script, ...args], { stdio: 'inherit' });
|
package/bin/gramatr.ts
CHANGED
|
@@ -6,14 +6,12 @@ import { homedir } from 'os';
|
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { detectTargets, findTarget, summarizeDetectedLocalTargets, type IntegrationTargetId } from '../core/targets.ts';
|
|
9
|
+
import { VERSION } from '../core/version.ts';
|
|
9
10
|
import { findStaleArtifacts, runLegacyMigration } from '../core/migration.ts';
|
|
10
|
-
import { detectTsRunner } from '../core/install.ts';
|
|
11
11
|
import {
|
|
12
12
|
formatDetectionLines,
|
|
13
13
|
formatDoctorLines,
|
|
14
|
-
formatInstallMenuLines,
|
|
15
14
|
formatRemoteGuidanceLines,
|
|
16
|
-
resolveInteractiveSelection,
|
|
17
15
|
} from '../core/installer-cli.ts';
|
|
18
16
|
|
|
19
17
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -45,34 +43,17 @@ function renderRemoteGuidance(): void {
|
|
|
45
43
|
for (const line of formatRemoteGuidanceLines(detectTargets())) log(line);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
function prompt(question: string): string {
|
|
49
|
-
process.stdout.write(question);
|
|
50
|
-
const input = spawnSync('bash', ['-lc', 'IFS= read -r line; printf "%s" "$line"'], {
|
|
51
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
52
|
-
});
|
|
53
|
-
return input.stdout?.toString('utf8').trim() || '';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function renderInstallMenu(): IntegrationTargetId[] {
|
|
57
|
-
const rendered = formatInstallMenuLines(detectTargets());
|
|
58
|
-
for (const line of rendered.lines) log(line);
|
|
59
|
-
return rendered.targetIds;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
46
|
function runTs(script: string, extraArgs: string[] = []): void {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Fallback: hope tsx is globally available
|
|
74
|
-
run('npx', ['tsx', script, ...extraArgs]);
|
|
75
|
-
}
|
|
47
|
+
// Resolve tsx from this package's node_modules (not CWD) so `npx tsx` works
|
|
48
|
+
// even on hosts where the user hasn't globally installed tsx.
|
|
49
|
+
try {
|
|
50
|
+
const { dirname, join } = require('path');
|
|
51
|
+
const tsxCli = join(dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
|
|
52
|
+
run(process.execPath, [tsxCli, script, ...extraArgs]);
|
|
53
|
+
} catch {
|
|
54
|
+
// Fallback: global npx tsx
|
|
55
|
+
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
56
|
+
run(npxBin, ['tsx', script, ...extraArgs]);
|
|
76
57
|
}
|
|
77
58
|
}
|
|
78
59
|
|
|
@@ -83,6 +64,122 @@ const forwardedFlags = process.argv.slice(2).filter(a =>
|
|
|
83
64
|
(process.argv[process.argv.indexOf(a) - 1] === '--timezone')
|
|
84
65
|
);
|
|
85
66
|
|
|
67
|
+
interface InstallAllResult {
|
|
68
|
+
id: IntegrationTargetId;
|
|
69
|
+
label: string;
|
|
70
|
+
status: 'ok' | 'fail' | 'skipped';
|
|
71
|
+
message?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const INSTALL_ALL_CANDIDATES: Array<{ id: IntegrationTargetId; label: string }> = [
|
|
75
|
+
{ id: 'claude-code', label: 'Claude Code' },
|
|
76
|
+
{ id: 'codex', label: 'Codex' },
|
|
77
|
+
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
78
|
+
{ id: 'claude-desktop', label: 'Claude Desktop' },
|
|
79
|
+
{ id: 'chatgpt-desktop', label: 'ChatGPT Desktop' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function installAll(): void {
|
|
83
|
+
const detections = detectTargets();
|
|
84
|
+
const detectedIds = new Set(
|
|
85
|
+
detections.filter((t) => t.kind === 'local' && t.detection.detected).map((t) => t.id),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
89
|
+
log(` gramatr v${VERSION} — install all detected`);
|
|
90
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
91
|
+
log('');
|
|
92
|
+
|
|
93
|
+
const results: InstallAllResult[] = [];
|
|
94
|
+
|
|
95
|
+
for (const candidate of INSTALL_ALL_CANDIDATES) {
|
|
96
|
+
if (!detectedIds.has(candidate.id)) {
|
|
97
|
+
results.push({ id: candidate.id, label: candidate.label, status: 'skipped', message: 'not detected' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
log(`━━━ Installing ${candidate.label} ━━━`);
|
|
101
|
+
try {
|
|
102
|
+
installTargetForAll(candidate.id);
|
|
103
|
+
results.push({ id: candidate.id, label: candidate.label, status: 'ok' });
|
|
104
|
+
} catch (err: any) {
|
|
105
|
+
results.push({
|
|
106
|
+
id: candidate.id,
|
|
107
|
+
label: candidate.label,
|
|
108
|
+
status: 'fail',
|
|
109
|
+
message: err?.message || String(err),
|
|
110
|
+
});
|
|
111
|
+
log(` X ${candidate.label} install failed: ${err?.message || err}`);
|
|
112
|
+
}
|
|
113
|
+
log('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Summary
|
|
117
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
118
|
+
log(' gramatr install summary');
|
|
119
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
120
|
+
const pad = (s: string, n: number) => s + ' '.repeat(Math.max(1, n - s.length));
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
const statusStr =
|
|
123
|
+
r.status === 'ok' ? 'OK'
|
|
124
|
+
: r.status === 'fail' ? 'FAIL'
|
|
125
|
+
: 'not detected — skipped';
|
|
126
|
+
log(` ${pad(r.label, 18)}${statusStr}${r.message && r.status === 'fail' ? ` (${r.message})` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
129
|
+
log('');
|
|
130
|
+
|
|
131
|
+
const okList = results.filter((r) => r.status === 'ok');
|
|
132
|
+
if (okList.length > 0) {
|
|
133
|
+
log(' Next steps:');
|
|
134
|
+
let step = 1;
|
|
135
|
+
for (const r of okList) {
|
|
136
|
+
switch (r.id) {
|
|
137
|
+
case 'claude-code':
|
|
138
|
+
log(` ${step++}. Restart Claude Code to pick up MCP server config`);
|
|
139
|
+
break;
|
|
140
|
+
case 'codex':
|
|
141
|
+
log(` ${step++}. Restart Codex to load updated hooks`);
|
|
142
|
+
break;
|
|
143
|
+
case 'gemini-cli':
|
|
144
|
+
log(` ${step++}. Restart Gemini CLI to load the extension`);
|
|
145
|
+
break;
|
|
146
|
+
case 'claude-desktop':
|
|
147
|
+
log(` ${step++}. Restart Claude Desktop to load MCP server`);
|
|
148
|
+
break;
|
|
149
|
+
case 'chatgpt-desktop':
|
|
150
|
+
log(` ${step++}. Restart ChatGPT Desktop to load MCP server`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
log('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const anyFail = results.some((r) => r.status === 'fail');
|
|
158
|
+
if (anyFail) process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function installTargetForAll(targetId: IntegrationTargetId): void {
|
|
162
|
+
switch (targetId) {
|
|
163
|
+
case 'claude-code':
|
|
164
|
+
runTs(join(binDir, 'install.ts'), forwardedFlags);
|
|
165
|
+
return;
|
|
166
|
+
case 'codex':
|
|
167
|
+
runTs(join(clientDir, 'codex', 'install.ts'), forwardedFlags);
|
|
168
|
+
return;
|
|
169
|
+
case 'gemini-cli':
|
|
170
|
+
runTs(join(clientDir, 'gemini', 'install.ts'), forwardedFlags);
|
|
171
|
+
return;
|
|
172
|
+
case 'claude-desktop':
|
|
173
|
+
runTs(join(clientDir, 'desktop', 'install.ts'), forwardedFlags);
|
|
174
|
+
return;
|
|
175
|
+
case 'chatgpt-desktop':
|
|
176
|
+
runTs(join(clientDir, 'chatgpt', 'install.ts'), forwardedFlags);
|
|
177
|
+
return;
|
|
178
|
+
default:
|
|
179
|
+
throw new Error(`Cannot install remote target '${targetId}' via install-all`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
86
183
|
function installTarget(targetId: IntegrationTargetId): void {
|
|
87
184
|
switch (targetId) {
|
|
88
185
|
case 'claude-code':
|
|
@@ -154,31 +251,22 @@ function upgrade(): void {
|
|
|
154
251
|
renderRemoteGuidance();
|
|
155
252
|
}
|
|
156
253
|
|
|
157
|
-
function interactiveInstall(): void {
|
|
158
|
-
renderDetections();
|
|
159
|
-
log('');
|
|
160
|
-
const targetIds = renderInstallMenu();
|
|
161
|
-
const defaultChoice = summarizeDetectedLocalTargets()[0] || 'claude-code';
|
|
162
|
-
const answer = prompt(`Install target(s) [${defaultChoice}]: `) || defaultChoice;
|
|
163
|
-
const selections = resolveInteractiveSelection(
|
|
164
|
-
answer,
|
|
165
|
-
targetIds,
|
|
166
|
-
summarizeDetectedLocalTargets(),
|
|
167
|
-
(id) => findTarget(id) as { id: IntegrationTargetId; kind: 'local' | 'remote' } | undefined,
|
|
168
|
-
);
|
|
169
|
-
if (selections.length === 0) {
|
|
170
|
-
throw new Error('No install targets selected');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const selection of selections) {
|
|
174
|
-
installTarget(selection);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
renderRemoteGuidance();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
254
|
function main(): void {
|
|
181
|
-
|
|
255
|
+
// Skip forwarded-only flags (--yes, --name <v>, --timezone <v>) when picking
|
|
256
|
+
// the command and target. Keep command-specific flags like --apply, --detect,
|
|
257
|
+
// --help, --version so existing subcommands continue to work.
|
|
258
|
+
const raw = process.argv.slice(2);
|
|
259
|
+
const FORWARDED_ONLY = new Set(['--yes', '-y']);
|
|
260
|
+
const FORWARDED_WITH_VALUE = new Set(['--name', '--timezone']);
|
|
261
|
+
const positionals: string[] = [];
|
|
262
|
+
for (let i = 0; i < raw.length; i++) {
|
|
263
|
+
const a = raw[i];
|
|
264
|
+
if (FORWARDED_ONLY.has(a)) continue;
|
|
265
|
+
if (FORWARDED_WITH_VALUE.has(a)) { i++; continue; }
|
|
266
|
+
positionals.push(a);
|
|
267
|
+
}
|
|
268
|
+
const command = positionals[0] ?? 'install';
|
|
269
|
+
const targetArg = positionals[1];
|
|
182
270
|
|
|
183
271
|
switch (command) {
|
|
184
272
|
case 'install':
|
|
@@ -186,22 +274,32 @@ function main(): void {
|
|
|
186
274
|
renderDetections();
|
|
187
275
|
return;
|
|
188
276
|
}
|
|
189
|
-
if (targetArg) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
277
|
+
if (!targetArg || targetArg === 'all') {
|
|
278
|
+
installAll();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (targetArg === 'help' || targetArg === '--help' || targetArg === '-h') {
|
|
282
|
+
log('gramatr install — install gramatr into detected AI platforms');
|
|
283
|
+
log('');
|
|
284
|
+
log('Usage:');
|
|
285
|
+
log(' npx gramatr install Detect every platform and install all');
|
|
286
|
+
log(' npx gramatr install all Same as above (explicit)');
|
|
287
|
+
log(' npx gramatr install <platform> Install a single platform');
|
|
288
|
+
log('');
|
|
289
|
+
log('Supported platforms:');
|
|
290
|
+
for (const c of INSTALL_ALL_CANDIDATES) {
|
|
291
|
+
log(` ${c.id.padEnd(16)}${c.label}`);
|
|
198
292
|
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
{
|
|
199
296
|
const target = findTarget(targetArg);
|
|
200
|
-
if (!target)
|
|
297
|
+
if (!target) {
|
|
298
|
+
log(`Unknown target: ${targetArg}. Run 'gramatr install help' for usage.`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
201
301
|
installTarget(target.id);
|
|
202
|
-
return;
|
|
203
302
|
}
|
|
204
|
-
interactiveInstall();
|
|
205
303
|
return;
|
|
206
304
|
case 'detect':
|
|
207
305
|
renderDetections();
|
|
@@ -235,12 +333,7 @@ function main(): void {
|
|
|
235
333
|
return;
|
|
236
334
|
case '--version':
|
|
237
335
|
case '-v':
|
|
238
|
-
|
|
239
|
-
const { VERSION } = require('../core/version.ts');
|
|
240
|
-
log(VERSION);
|
|
241
|
-
} catch {
|
|
242
|
-
log('0.3.0');
|
|
243
|
-
}
|
|
336
|
+
log(VERSION);
|
|
244
337
|
return;
|
|
245
338
|
default:
|
|
246
339
|
log(`Unknown command: ${command}. Run 'gramatr help' for usage.`);
|
package/bin/install.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { join, dirname, basename, resolve } from 'path';
|
|
18
18
|
import { execSync, spawnSync } from 'child_process';
|
|
19
19
|
import { createInterface } from 'readline';
|
|
20
|
-
import { buildClaudeHooksFile
|
|
20
|
+
import { buildClaudeHooksFile } from '../core/install.ts';
|
|
21
21
|
import { VERSION } from '../core/version.ts';
|
|
22
22
|
|
|
23
23
|
// ── Constants ──
|
|
@@ -141,7 +141,7 @@ function timestamp(): string {
|
|
|
141
141
|
|
|
142
142
|
// ── Prerequisites ──
|
|
143
143
|
|
|
144
|
-
async function checkPrereqs(): Promise<
|
|
144
|
+
async function checkPrereqs(): Promise<void> {
|
|
145
145
|
// Node.js
|
|
146
146
|
const nodeVer = process.versions.node;
|
|
147
147
|
const major = parseInt(nodeVer.split('.')[0], 10);
|
|
@@ -151,18 +151,8 @@ async function checkPrereqs(): Promise<{ tsRunner: string }> {
|
|
|
151
151
|
}
|
|
152
152
|
log(`OK Node.js v${nodeVer}`);
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
if (tsRunner === 'bun') {
|
|
157
|
-
try {
|
|
158
|
-
const ver = execSync('bun --version', { encoding: 'utf8' }).trim();
|
|
159
|
-
log(`OK TS runner: bun ${ver}`);
|
|
160
|
-
} catch {
|
|
161
|
-
log(`OK TS runner: bun`);
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
log(`OK TS runner: npx tsx (install bun for faster hooks)`);
|
|
165
|
-
}
|
|
154
|
+
// TS runner is hardcoded to `npx tsx` (see core/install.ts and issue #468)
|
|
155
|
+
log('OK TS runner: npx tsx');
|
|
166
156
|
|
|
167
157
|
// Claude Code
|
|
168
158
|
if (!existsSync(CLAUDE_DIR)) {
|
|
@@ -170,8 +160,6 @@ async function checkPrereqs(): Promise<{ tsRunner: string }> {
|
|
|
170
160
|
process.exit(1);
|
|
171
161
|
}
|
|
172
162
|
log('OK Claude Code directory exists');
|
|
173
|
-
|
|
174
|
-
return { tsRunner };
|
|
175
163
|
}
|
|
176
164
|
|
|
177
165
|
// ── Legacy Detection ──
|
|
@@ -287,6 +275,11 @@ function installClientFiles(): void {
|
|
|
287
275
|
}
|
|
288
276
|
log('OK Installed core modules');
|
|
289
277
|
|
|
278
|
+
// package.json — copied so core/version.ts can resolve the installed
|
|
279
|
+
// version at runtime. Single source of truth; see core/version.ts.
|
|
280
|
+
copyFileIfExists(join(SCRIPT_DIR, 'package.json'), join(CLIENT_DIR, 'package.json'));
|
|
281
|
+
log('OK Installed package.json (version source of truth)');
|
|
282
|
+
|
|
290
283
|
// CLAUDE.md
|
|
291
284
|
copyFileIfExists(join(SCRIPT_DIR, 'CLAUDE.md'), join(CLIENT_DIR, 'CLAUDE.md'));
|
|
292
285
|
log('OK Installed CLAUDE.md (minimal — server delivers behavioral rules)');
|
|
@@ -351,7 +344,11 @@ function installClaudeMd(): void {
|
|
|
351
344
|
|
|
352
345
|
// ── Step 3: Auth ──
|
|
353
346
|
|
|
354
|
-
|
|
347
|
+
// npx on Windows is shipped as `npx.cmd`. spawnSync without shell: true cannot
|
|
348
|
+
// resolve .cmd shims, so we fall back to the platform-specific binary name.
|
|
349
|
+
const NPX_BIN = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
350
|
+
|
|
351
|
+
async function handleAuth(legacyToken: string): Promise<{ url: string; token: string }> {
|
|
355
352
|
log('━━━ Step 3: Configuring gramatr MCP server ━━━');
|
|
356
353
|
log('');
|
|
357
354
|
|
|
@@ -387,10 +384,11 @@ async function handleAuth(legacyToken: string, tsRunner: string): Promise<{ url:
|
|
|
387
384
|
if (existsSync(loginScript)) {
|
|
388
385
|
// Run gmtr-login as subprocess but with proper stdio handling
|
|
389
386
|
// Use spawnSync so stdin is properly passed through (no stall)
|
|
390
|
-
const result = spawnSync(
|
|
387
|
+
const result = spawnSync(NPX_BIN, ['tsx', loginScript], {
|
|
391
388
|
stdio: 'inherit',
|
|
392
389
|
env: { ...process.env },
|
|
393
390
|
});
|
|
391
|
+
void result;
|
|
394
392
|
|
|
395
393
|
// Re-read token after login
|
|
396
394
|
if (existsSync(GMTR_JSON)) {
|
|
@@ -479,7 +477,7 @@ async function configureIdentity(): Promise<void> {
|
|
|
479
477
|
|
|
480
478
|
// ── Step 4: Settings merge ──
|
|
481
479
|
|
|
482
|
-
function updateClaudeSettings(
|
|
480
|
+
function updateClaudeSettings(url: string, token: string): void {
|
|
483
481
|
log('━━━ Step 4: Updating Claude Code settings ━━━');
|
|
484
482
|
log('');
|
|
485
483
|
|
|
@@ -493,7 +491,7 @@ function updateClaudeSettings(tsRunner: string, url: string, token: string): voi
|
|
|
493
491
|
log(`OK Backed up settings to ${basename(backup)}`);
|
|
494
492
|
|
|
495
493
|
const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
|
|
496
|
-
const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx
|
|
494
|
+
const hooksConfig = buildClaudeHooksFile(CLIENT_DIR, { includeOptionalUx });
|
|
497
495
|
|
|
498
496
|
const settings = readJson(CLAUDE_SETTINGS);
|
|
499
497
|
|
|
@@ -529,7 +527,7 @@ function updateClaudeSettings(tsRunner: string, url: string, token: string): voi
|
|
|
529
527
|
// Status line
|
|
530
528
|
settings.statusLine = {
|
|
531
529
|
type: 'command',
|
|
532
|
-
command:
|
|
530
|
+
command: `npx tsx ${CLIENT_DIR}/bin/statusline.ts`,
|
|
533
531
|
};
|
|
534
532
|
log('OK Configured status line');
|
|
535
533
|
|
|
@@ -586,47 +584,6 @@ function registerMcpServer(url: string, token: string): void {
|
|
|
586
584
|
log('');
|
|
587
585
|
}
|
|
588
586
|
|
|
589
|
-
// ── Step 4c: Additional CLIs ──
|
|
590
|
-
|
|
591
|
-
async function installAdditionalClis(): Promise<void> {
|
|
592
|
-
log('━━━ Step 4c: Additional CLI platforms ━━━');
|
|
593
|
-
log('');
|
|
594
|
-
|
|
595
|
-
// Codex
|
|
596
|
-
const codexDir = join(HOME, '.codex');
|
|
597
|
-
if (existsSync(codexDir) || which('codex')) {
|
|
598
|
-
log(' Codex CLI detected — installing gramatr hooks...');
|
|
599
|
-
try {
|
|
600
|
-
const { main: installCodex } = await import('../codex/install.ts');
|
|
601
|
-
installCodex();
|
|
602
|
-
log(' OK Codex hooks installed');
|
|
603
|
-
} catch (err: any) {
|
|
604
|
-
log(` X Codex install failed (non-fatal): ${err.message}`);
|
|
605
|
-
}
|
|
606
|
-
log('');
|
|
607
|
-
} else {
|
|
608
|
-
log(' -- Codex CLI not detected (skipping)');
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Gemini
|
|
612
|
-
const geminiDir = join(HOME, '.gemini');
|
|
613
|
-
if (existsSync(geminiDir) || which('gemini')) {
|
|
614
|
-
log(' Gemini CLI detected — installing gramatr extension...');
|
|
615
|
-
try {
|
|
616
|
-
const { main: installGemini } = await import('../gemini/install.ts');
|
|
617
|
-
await installGemini();
|
|
618
|
-
log(' OK Gemini extension installed');
|
|
619
|
-
} catch (err: any) {
|
|
620
|
-
log(` X Gemini install failed (non-fatal): ${err.message}`);
|
|
621
|
-
}
|
|
622
|
-
log('');
|
|
623
|
-
} else {
|
|
624
|
-
log(' -- Gemini CLI not detected (skipping)');
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
log('');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
587
|
// ── Step 5: Verification ──
|
|
631
588
|
|
|
632
589
|
function verify(url: string, token: string): boolean {
|
|
@@ -716,7 +673,7 @@ async function main(): Promise<void> {
|
|
|
716
673
|
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
717
674
|
log('');
|
|
718
675
|
|
|
719
|
-
|
|
676
|
+
await checkPrereqs();
|
|
720
677
|
log('');
|
|
721
678
|
|
|
722
679
|
const { legacyToken } = await handleLegacy();
|
|
@@ -724,11 +681,10 @@ async function main(): Promise<void> {
|
|
|
724
681
|
installClientFiles();
|
|
725
682
|
installClaudeMd();
|
|
726
683
|
|
|
727
|
-
const { url, token } = await handleAuth(legacyToken
|
|
684
|
+
const { url, token } = await handleAuth(legacyToken);
|
|
728
685
|
await configureIdentity();
|
|
729
|
-
updateClaudeSettings(
|
|
686
|
+
updateClaudeSettings(url, token);
|
|
730
687
|
registerMcpServer(url, token);
|
|
731
|
-
await installAdditionalClis();
|
|
732
688
|
|
|
733
689
|
const allOk = verify(url, token);
|
|
734
690
|
|
package/core/install.ts
CHANGED
|
@@ -23,7 +23,7 @@ interface HookSpec {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
interface ClaudeHookOptions {
|
|
26
|
-
|
|
26
|
+
includeOptionalUx?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const CLAUDE_HOOKS: HookSpec[] = [
|
|
@@ -62,21 +62,14 @@ const CODEX_HOOKS: HookSpec[] = [
|
|
|
62
62
|
];
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
65
|
+
* All hook commands are invoked via `npx tsx`. This is the single, portable
|
|
66
|
+
* runner for every gramatr client install. See issue #468 for the architectural
|
|
67
|
+
* rationale: bun detection at install time silently produced broken configs on
|
|
68
|
+
* hosts where the runtime PATH did not match the installer's PATH.
|
|
67
69
|
*/
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const { execSync } = require('child_process');
|
|
71
|
-
execSync('bun --version', { stdio: 'ignore' });
|
|
72
|
-
return 'bun';
|
|
73
|
-
} catch {
|
|
74
|
-
return 'npx tsx';
|
|
75
|
-
}
|
|
76
|
-
}
|
|
70
|
+
const TS_RUNNER = 'npx tsx';
|
|
77
71
|
|
|
78
|
-
function buildHooksFile(clientDir: string, specs: HookSpec[]
|
|
79
|
-
const runner = tsRunner || detectTsRunner();
|
|
72
|
+
function buildHooksFile(clientDir: string, specs: HookSpec[]): InstallHooksFile {
|
|
80
73
|
const hooks: Record<string, InstallHookMatcherEntry[]> = {};
|
|
81
74
|
|
|
82
75
|
for (const spec of specs) {
|
|
@@ -84,7 +77,7 @@ function buildHooksFile(clientDir: string, specs: HookSpec[], tsRunner?: string)
|
|
|
84
77
|
hooks: [
|
|
85
78
|
{
|
|
86
79
|
type: 'command',
|
|
87
|
-
command: `${
|
|
80
|
+
command: `${TS_RUNNER} "${clientDir}/${spec.relativeCommand}"`,
|
|
88
81
|
statusMessage: spec.statusMessage,
|
|
89
82
|
timeout: spec.timeout,
|
|
90
83
|
},
|
|
@@ -104,11 +97,11 @@ function buildHooksFile(clientDir: string, specs: HookSpec[], tsRunner?: string)
|
|
|
104
97
|
|
|
105
98
|
export function buildClaudeHooksFile(
|
|
106
99
|
clientDir: string,
|
|
107
|
-
|
|
100
|
+
_options: ClaudeHookOptions = {},
|
|
108
101
|
): InstallHooksFile {
|
|
109
|
-
return buildHooksFile(clientDir, CLAUDE_HOOKS
|
|
102
|
+
return buildHooksFile(clientDir, CLAUDE_HOOKS);
|
|
110
103
|
}
|
|
111
104
|
|
|
112
|
-
export function buildCodexHooksFile(clientDir: string
|
|
113
|
-
return buildHooksFile(clientDir, CODEX_HOOKS
|
|
105
|
+
export function buildCodexHooksFile(clientDir: string): InstallHooksFile {
|
|
106
|
+
return buildHooksFile(clientDir, CODEX_HOOKS);
|
|
114
107
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version-check.ts — opportunistic npm registry version check.
|
|
3
|
+
*
|
|
4
|
+
* Queries https://registry.npmjs.org/gramatr/latest on a 3s timeout, caches
|
|
5
|
+
* the result for one hour under ~/.gmtr-client/.cache/version-check.json, and
|
|
6
|
+
* reports whether the installed client is behind the published version.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints (see issue #468 sibling work):
|
|
9
|
+
* - Never throws. Any failure returns null and the caller proceeds normally.
|
|
10
|
+
* - Never writes to stdout — stdout is Claude Code's context channel.
|
|
11
|
+
* - Fast cache-hit path (no network, no heavy work).
|
|
12
|
+
* - Zero new runtime dependencies. Uses global fetch (Node 18+).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
16
|
+
import { dirname, join } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
|
|
19
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/gramatr/latest';
|
|
20
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
21
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
22
|
+
|
|
23
|
+
export interface VersionCheckResult {
|
|
24
|
+
latestVersion: string;
|
|
25
|
+
installedVersion: string;
|
|
26
|
+
isOutdated: boolean;
|
|
27
|
+
cached: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CacheFile {
|
|
31
|
+
latestVersion: string;
|
|
32
|
+
fetchedAt: number;
|
|
33
|
+
lastNotifiedVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare two semver-style version strings ("X.Y.Z").
|
|
38
|
+
* Returns:
|
|
39
|
+
* -1 if a < b
|
|
40
|
+
* 0 if a === b
|
|
41
|
+
* 1 if a > b
|
|
42
|
+
*
|
|
43
|
+
* Non-numeric or missing segments are treated as 0.
|
|
44
|
+
*/
|
|
45
|
+
export function compareVersions(a: string, b: string): number {
|
|
46
|
+
const pa = a.split('.').map((x) => parseInt(x, 10) || 0);
|
|
47
|
+
const pb = b.split('.').map((x) => parseInt(x, 10) || 0);
|
|
48
|
+
const len = Math.max(pa.length, pb.length);
|
|
49
|
+
for (let i = 0; i < len; i++) {
|
|
50
|
+
const av = pa[i] ?? 0;
|
|
51
|
+
const bv = pb[i] ?? 0;
|
|
52
|
+
if (av < bv) return -1;
|
|
53
|
+
if (av > bv) return 1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getCachePath(home: string = homedir()): string {
|
|
59
|
+
return join(home, '.gmtr-client', '.cache', 'version-check.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readCache(path: string): CacheFile | null {
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(path)) return null;
|
|
65
|
+
const raw = readFileSync(path, 'utf8');
|
|
66
|
+
const parsed = JSON.parse(raw) as CacheFile;
|
|
67
|
+
if (typeof parsed.latestVersion !== 'string' || typeof parsed.fetchedAt !== 'number') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeCache(path: string, data: CacheFile): void {
|
|
77
|
+
try {
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
80
|
+
} catch {
|
|
81
|
+
// Cache is best-effort. Silent failure is acceptable.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(REGISTRY_URL, {
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
headers: { Accept: 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) return null;
|
|
95
|
+
const body = (await res.json()) as { version?: string };
|
|
96
|
+
if (typeof body?.version !== 'string') return null;
|
|
97
|
+
return body.version;
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check the installed version against the latest published on npm.
|
|
108
|
+
* Returns null on any failure — callers must treat this as optional.
|
|
109
|
+
*/
|
|
110
|
+
export async function checkLatestVersion(
|
|
111
|
+
installedVersion: string,
|
|
112
|
+
options: { cachePath?: string; now?: number } = {},
|
|
113
|
+
): Promise<VersionCheckResult | null> {
|
|
114
|
+
try {
|
|
115
|
+
const cachePath = options.cachePath ?? getCachePath();
|
|
116
|
+
const now = options.now ?? Date.now();
|
|
117
|
+
|
|
118
|
+
const cached = readCache(cachePath);
|
|
119
|
+
if (cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
120
|
+
return {
|
|
121
|
+
latestVersion: cached.latestVersion,
|
|
122
|
+
installedVersion,
|
|
123
|
+
isOutdated: compareVersions(cached.latestVersion, installedVersion) > 0,
|
|
124
|
+
cached: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const latestVersion = await fetchLatestVersion();
|
|
129
|
+
if (!latestVersion) return null;
|
|
130
|
+
|
|
131
|
+
writeCache(cachePath, {
|
|
132
|
+
latestVersion,
|
|
133
|
+
fetchedAt: now,
|
|
134
|
+
lastNotifiedVersion: cached?.lastNotifiedVersion,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
latestVersion,
|
|
139
|
+
installedVersion,
|
|
140
|
+
isOutdated: compareVersions(latestVersion, installedVersion) > 0,
|
|
141
|
+
cached: false,
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Record that the user has been notified for a given latest version.
|
|
150
|
+
* Suppresses repeat notifications until a newer version is published.
|
|
151
|
+
*/
|
|
152
|
+
export function markNotified(latestVersion: string, cachePath: string = getCachePath()): void {
|
|
153
|
+
try {
|
|
154
|
+
const current = readCache(cachePath);
|
|
155
|
+
if (!current) return;
|
|
156
|
+
writeCache(cachePath, { ...current, lastNotifiedVersion: latestVersion });
|
|
157
|
+
} catch {
|
|
158
|
+
// Best-effort.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function shouldNotify(
|
|
163
|
+
result: VersionCheckResult,
|
|
164
|
+
cachePath: string = getCachePath(),
|
|
165
|
+
): boolean {
|
|
166
|
+
if (!result.isOutdated) return false;
|
|
167
|
+
const cached = readCache(cachePath);
|
|
168
|
+
if (cached?.lastNotifiedVersion === result.latestVersion) return false;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format the upgrade notification banner. Caller decides where to write it
|
|
174
|
+
* (must be stderr — stdout is reserved for Claude context).
|
|
175
|
+
*/
|
|
176
|
+
export function formatUpgradeNotification(installed: string, latest: string): string {
|
|
177
|
+
const bar = '\u2501'.repeat(60);
|
|
178
|
+
return [
|
|
179
|
+
bar,
|
|
180
|
+
' gramatr update available',
|
|
181
|
+
'',
|
|
182
|
+
` Installed: ${installed}`,
|
|
183
|
+
` Latest: ${latest}`,
|
|
184
|
+
'',
|
|
185
|
+
' To upgrade:',
|
|
186
|
+
' 1. Type /exit to leave Claude Code',
|
|
187
|
+
' 2. Run: npx gramatr@latest install claude-code',
|
|
188
|
+
' 3. Restart: claude --resume',
|
|
189
|
+
'',
|
|
190
|
+
" Why restart? gramatr's hooks are loaded by Claude Code at",
|
|
191
|
+
' session start. New hook code requires a fresh session.',
|
|
192
|
+
bar,
|
|
193
|
+
'',
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One-shot helper for hooks: check and, if appropriate, print the
|
|
199
|
+
* notification to stderr. Safe to call from any hook — never throws,
|
|
200
|
+
* never blocks longer than FETCH_TIMEOUT_MS in the cache-miss path.
|
|
201
|
+
*/
|
|
202
|
+
export async function runVersionCheckAndNotify(
|
|
203
|
+
installedVersion: string,
|
|
204
|
+
options: { cachePath?: string; stream?: NodeJS.WritableStream } = {},
|
|
205
|
+
): Promise<VersionCheckResult | null> {
|
|
206
|
+
const stream = options.stream ?? process.stderr;
|
|
207
|
+
const cachePath = options.cachePath ?? getCachePath();
|
|
208
|
+
const result = await checkLatestVersion(installedVersion, { cachePath });
|
|
209
|
+
if (!result) return null;
|
|
210
|
+
if (shouldNotify(result, cachePath)) {
|
|
211
|
+
try {
|
|
212
|
+
stream.write(formatUpgradeNotification(result.installedVersion, result.latestVersion));
|
|
213
|
+
} catch {
|
|
214
|
+
// Silent — never break the hook.
|
|
215
|
+
}
|
|
216
|
+
markNotified(result.latestVersion, cachePath);
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
package/core/version.ts
CHANGED
|
@@ -1,2 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* version.ts — runtime resolution of the installed gramatr version.
|
|
3
|
+
*
|
|
4
|
+
* Reads `version` from the nearest package.json walking up from this module's
|
|
5
|
+
* location. package.json is the SINGLE source of truth — it's the file the
|
|
6
|
+
* version-bump process already updates, so there is zero possibility of drift.
|
|
7
|
+
*
|
|
8
|
+
* Works in two environments:
|
|
9
|
+
* 1. Source checkout: packages/client/core/version.ts →
|
|
10
|
+
* packages/client/package.json (found one directory up).
|
|
11
|
+
* 2. Installed client: ~/gmtr-client/core/version.ts →
|
|
12
|
+
* ~/gmtr-client/package.json (copied by installClientFiles()).
|
|
13
|
+
*
|
|
14
|
+
* If the file cannot be resolved (unexpected layout), falls back to '0.0.0'
|
|
15
|
+
* rather than throwing — the version check is opportunistic and must never
|
|
16
|
+
* break the hook.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { dirname, join } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
function findPackageJson(startDir: string): string | null {
|
|
24
|
+
let dir = startDir;
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
const candidate = join(dir, 'package.json');
|
|
27
|
+
if (existsSync(candidate)) return candidate;
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveVersion(): string {
|
|
36
|
+
try {
|
|
37
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const pkgPath = findPackageJson(here);
|
|
39
|
+
if (!pkgPath) return '0.0.0';
|
|
40
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string };
|
|
41
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
42
|
+
} catch {
|
|
43
|
+
return '0.0.0';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const VERSION: string = resolveVersion();
|
|
@@ -88,7 +88,7 @@ export function buildGeminiHooksFile(): GeminiHooksFile {
|
|
|
88
88
|
hooks: [
|
|
89
89
|
{
|
|
90
90
|
type: 'command',
|
|
91
|
-
command: '
|
|
91
|
+
command: 'npx tsx "${extensionPath}/hooks/session-start.ts"',
|
|
92
92
|
name: 'gramatr-session-start',
|
|
93
93
|
timeout: 15,
|
|
94
94
|
description: 'Load gramatr session context and handoff',
|
|
@@ -101,7 +101,7 @@ export function buildGeminiHooksFile(): GeminiHooksFile {
|
|
|
101
101
|
hooks: [
|
|
102
102
|
{
|
|
103
103
|
type: 'command',
|
|
104
|
-
command: '
|
|
104
|
+
command: 'npx tsx "${extensionPath}/hooks/user-prompt-submit.ts"',
|
|
105
105
|
name: 'gramatr-prompt-routing',
|
|
106
106
|
timeout: 15,
|
|
107
107
|
description: 'Route prompt through gramatr intelligence',
|
|
@@ -114,7 +114,7 @@ export function buildGeminiHooksFile(): GeminiHooksFile {
|
|
|
114
114
|
hooks: [
|
|
115
115
|
{
|
|
116
116
|
type: 'command',
|
|
117
|
-
command: '
|
|
117
|
+
command: 'npx tsx "${extensionPath}/hooks/stop.ts"',
|
|
118
118
|
name: 'gramatr-session-end',
|
|
119
119
|
timeout: 10,
|
|
120
120
|
description: 'Submit classification feedback to gramatr',
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
writeCurrentProjectContextFile,
|
|
34
34
|
persistSessionRegistration,
|
|
35
35
|
} from '../core/session.ts';
|
|
36
|
+
import { runVersionCheckAndNotify } from '../core/version-check.ts';
|
|
37
|
+
import { VERSION } from '../core/version.ts';
|
|
36
38
|
|
|
37
39
|
// ── stdout (Claude context injection) ──
|
|
38
40
|
// Claude Code captures stdout from SessionStart hooks and injects it as context.
|
|
@@ -172,7 +174,8 @@ function syncRatingsInBackground(): void {
|
|
|
172
174
|
const syncScript = join(process.env.GMTR_DIR || join(process.env.HOME || '', 'gmtr-client'), 'hooks', 'sync-ratings.hook.ts');
|
|
173
175
|
if (existsSync(syncScript)) {
|
|
174
176
|
try {
|
|
175
|
-
const
|
|
177
|
+
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
178
|
+
const child = spawn(npxBin, ['tsx', syncScript], {
|
|
176
179
|
detached: true,
|
|
177
180
|
stdio: 'ignore',
|
|
178
181
|
});
|
|
@@ -477,6 +480,14 @@ async function main(): Promise<void> {
|
|
|
477
480
|
}
|
|
478
481
|
}
|
|
479
482
|
|
|
483
|
+
// Opportunistic npm version check — prints a notification to stderr if a
|
|
484
|
+
// newer gramatr is published. Never blocks or crashes the session start.
|
|
485
|
+
try {
|
|
486
|
+
await runVersionCheckAndNotify(VERSION);
|
|
487
|
+
} catch {
|
|
488
|
+
// Silent — version check is strictly optional.
|
|
489
|
+
}
|
|
490
|
+
|
|
480
491
|
log('');
|
|
481
492
|
log('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501');
|
|
482
493
|
log('Session initialization complete');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gramatr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.56",
|
|
4
4
|
"description": "grāmatr — context engineering layer for AI coding agents. Every prompt gets a pre-computed intelligence packet: decision routing, capability audit, behavioral directives, memory pre-load, and ISC scaffolds. Continuity across sessions for Claude Code, Codex, and Gemini CLI.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -43,11 +43,9 @@
|
|
|
43
43
|
"lint": "pnpm exec biome lint --files-ignore-unknown=true package.json bin chatgpt codex core desktop gemini hooks lib tools web vitest.config.ts",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:coverage": "vitest run --coverage",
|
|
46
|
-
"version:patch": "npm version patch --no-git-tag-version
|
|
47
|
-
"version:minor": "npm version minor --no-git-tag-version
|
|
48
|
-
"version:major": "npm version major --no-git-tag-version
|
|
49
|
-
"version:sync": "npx tsx bin/version-sync.ts",
|
|
50
|
-
"prepublishOnly": "npm run version:sync"
|
|
46
|
+
"version:patch": "npm version patch --no-git-tag-version",
|
|
47
|
+
"version:minor": "npm version minor --no-git-tag-version",
|
|
48
|
+
"version:major": "npm version major --no-git-tag-version"
|
|
51
49
|
},
|
|
52
50
|
"engines": {
|
|
53
51
|
"node": ">=20.0.0"
|
package/bin/version-sync.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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}`);
|