@gramatr/client 0.6.20 → 0.6.22
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/install.ts +14 -3
- package/core/routing.ts +21 -0
- package/core/version-check.ts +90 -2
- package/hooks/gramatr-prompt-enricher.hook.ts +34 -2
- package/hooks/session-start.hook.ts +19 -4
- package/package.json +1 -1
package/bin/install.ts
CHANGED
|
@@ -261,14 +261,24 @@ function installClientFiles(): void {
|
|
|
261
261
|
mkdirSync(join(CLIENT_DIR, sub), { recursive: true });
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
//
|
|
264
|
+
// Clean stale hooks before copying — prevents old tool names persisting across upgrades (#633)
|
|
265
|
+
const hooksDestDir = join(CLIENT_DIR, 'hooks');
|
|
266
|
+
if (existsSync(hooksDestDir)) {
|
|
267
|
+
for (const f of readdirSync(hooksDestDir)) {
|
|
268
|
+
if (f.endsWith('.hook.ts') || f.endsWith('-utils.ts')) {
|
|
269
|
+
rmSync(join(hooksDestDir, f), { force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Hooks (core + utils)
|
|
265
275
|
let hookCount = 0;
|
|
266
276
|
const hooksSrc = join(SCRIPT_DIR, 'hooks');
|
|
267
277
|
if (existsSync(hooksSrc)) {
|
|
268
278
|
for (const f of readdirSync(hooksSrc)) {
|
|
269
279
|
const src = join(hooksSrc, f);
|
|
270
280
|
if (statSync(src).isFile() && (f.endsWith('.hook.ts') || f.endsWith('-utils.ts'))) {
|
|
271
|
-
cpSync(src, join(CLIENT_DIR, 'hooks', f));
|
|
281
|
+
cpSync(src, join(CLIENT_DIR, 'hooks', f), { force: true });
|
|
272
282
|
chmodSync(join(CLIENT_DIR, 'hooks', f), 0o755);
|
|
273
283
|
hookCount++;
|
|
274
284
|
}
|
|
@@ -278,7 +288,7 @@ function installClientFiles(): void {
|
|
|
278
288
|
const libHookSrc = join(hooksSrc, 'lib');
|
|
279
289
|
if (existsSync(libHookSrc)) {
|
|
280
290
|
const libs = readdirSync(libHookSrc).filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
|
281
|
-
for (const f of libs) cpSync(join(libHookSrc, f), join(CLIENT_DIR, 'hooks/lib', f));
|
|
291
|
+
for (const f of libs) cpSync(join(libHookSrc, f), join(CLIENT_DIR, 'hooks/lib', f), { force: true });
|
|
282
292
|
log(`OK Installed ${hookCount} hooks + ${libs.length} lib modules`);
|
|
283
293
|
}
|
|
284
294
|
|
|
@@ -717,6 +727,7 @@ async function main(): Promise<void> {
|
|
|
717
727
|
log('');
|
|
718
728
|
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
719
729
|
log(` gramatr v${VERSION} — Intelligence Layer`);
|
|
730
|
+
log(` Source: ${SCRIPT_DIR}`);
|
|
720
731
|
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
721
732
|
log('');
|
|
722
733
|
|
package/core/routing.ts
CHANGED
|
@@ -47,6 +47,27 @@ export async function routePrompt(options: {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Fetch Packet 2 enrichment (reverse engineering + ISC scaffold).
|
|
52
|
+
* Called automatically by the prompt enricher hook when packet_2_status is "pending".
|
|
53
|
+
* Brief timeout — enrichment is usually pre-computed by the time we ask.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchEnrichment(enrichmentId: string, timeoutMs: number = 2000): Promise<Record<string, unknown> | null> {
|
|
56
|
+
try {
|
|
57
|
+
const result = await callMcpToolDetailed<Record<string, unknown>>(
|
|
58
|
+
'gramatr_get_enrichment',
|
|
59
|
+
{ enrichment_id: enrichmentId, timeout_ms: timeoutMs },
|
|
60
|
+
timeoutMs + 1000, // HTTP timeout slightly longer than server timeout
|
|
61
|
+
);
|
|
62
|
+
if (result.data && result.data.status === 'ready') {
|
|
63
|
+
return result.data;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
export function describeRoutingFailure(error: MctToolCallError): {
|
|
51
72
|
title: string;
|
|
52
73
|
detail: string;
|
package/core/version-check.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
16
16
|
import { dirname, join } from 'path';
|
|
17
17
|
import { homedir } from 'os';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
18
19
|
|
|
19
|
-
const REGISTRY_URL = 'https://registry.npmjs.org
|
|
20
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@gramatr%2fclient/latest';
|
|
20
21
|
const FETCH_TIMEOUT_MS = 3000;
|
|
21
22
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
22
23
|
|
|
@@ -184,7 +185,7 @@ export function formatUpgradeNotification(installed: string, latest: string): st
|
|
|
184
185
|
'',
|
|
185
186
|
' To upgrade:',
|
|
186
187
|
' 1. Type /exit to leave Claude Code',
|
|
187
|
-
' 2. Run: npx gramatr@latest install claude-code',
|
|
188
|
+
' 2. Run: npx @gramatr/client@latest install claude-code',
|
|
188
189
|
' 3. Restart: claude --resume',
|
|
189
190
|
'',
|
|
190
191
|
" Why restart? gramatr's hooks are loaded by Claude Code at",
|
|
@@ -217,3 +218,90 @@ export async function runVersionCheckAndNotify(
|
|
|
217
218
|
}
|
|
218
219
|
return result;
|
|
219
220
|
}
|
|
221
|
+
|
|
222
|
+
// ── Auto-upgrade ──────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
const UPGRADE_LOCK_TTL_MS = 10 * 60 * 1000; // 10 minutes — prevent concurrent upgrades
|
|
225
|
+
|
|
226
|
+
function getUpgradeLockPath(home: string = homedir()): string {
|
|
227
|
+
return join(home, '.gramatr', '.cache', 'upgrade.lock');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isUpgradeLocked(lockPath: string = getUpgradeLockPath()): boolean {
|
|
231
|
+
try {
|
|
232
|
+
if (!existsSync(lockPath)) return false;
|
|
233
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
234
|
+
const lock = JSON.parse(raw);
|
|
235
|
+
if (typeof lock.startedAt !== 'number') return false;
|
|
236
|
+
return Date.now() - lock.startedAt < UPGRADE_LOCK_TTL_MS;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function writeUpgradeLock(version: string, lockPath: string = getUpgradeLockPath()): void {
|
|
243
|
+
try {
|
|
244
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
245
|
+
writeFileSync(lockPath, JSON.stringify({ version, startedAt: Date.now() }) + '\n');
|
|
246
|
+
} catch {
|
|
247
|
+
// Best-effort.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface AutoUpgradeResult {
|
|
252
|
+
triggered: boolean;
|
|
253
|
+
version: string;
|
|
254
|
+
reason?: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Auto-upgrade gramatr when a newer version is available.
|
|
259
|
+
*
|
|
260
|
+
* Spawns `npx @gramatr/client@{version} install claude-code --yes` as a
|
|
261
|
+
* detached background process. The install is idempotent: copies files to
|
|
262
|
+
* ~/.gramatr/, merges hooks into ~/.claude/settings.json, re-registers MCP.
|
|
263
|
+
* Existing auth token and URL are preserved (merge, not overwrite).
|
|
264
|
+
*
|
|
265
|
+
* The upgrade runs in the background so it doesn't block session start.
|
|
266
|
+
* New hook code won't take effect until the user restarts Claude Code.
|
|
267
|
+
*
|
|
268
|
+
* Safety:
|
|
269
|
+
* - Lock file prevents concurrent upgrades (10min TTL)
|
|
270
|
+
* - --yes flag ensures non-interactive (no stdin prompts)
|
|
271
|
+
* - Detached + unref'd so the parent process can exit
|
|
272
|
+
* - Never throws — returns result indicating what happened
|
|
273
|
+
*/
|
|
274
|
+
export function autoUpgrade(
|
|
275
|
+
latestVersion: string,
|
|
276
|
+
options: { stream?: NodeJS.WritableStream; lockPath?: string } = {},
|
|
277
|
+
): AutoUpgradeResult {
|
|
278
|
+
const stream = options.stream ?? process.stderr;
|
|
279
|
+
const lockPath = options.lockPath ?? getUpgradeLockPath();
|
|
280
|
+
|
|
281
|
+
if (isUpgradeLocked(lockPath)) {
|
|
282
|
+
return { triggered: false, version: latestVersion, reason: 'upgrade already in progress' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
writeUpgradeLock(latestVersion, lockPath);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
289
|
+
const child = spawn(
|
|
290
|
+
npxBin,
|
|
291
|
+
['@gramatr/client@' + latestVersion, 'install', 'claude-code', '--yes'],
|
|
292
|
+
{
|
|
293
|
+
detached: true,
|
|
294
|
+
stdio: 'ignore',
|
|
295
|
+
env: { ...process.env, GRAMATR_AUTO_UPGRADE: '1' },
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
child.unref();
|
|
299
|
+
|
|
300
|
+
stream.write(` gramatr auto-upgrade started: v${latestVersion}\n`);
|
|
301
|
+
stream.write(' Files will be updated in ~/.gramatr/ — restart Claude Code for new hooks.\n');
|
|
302
|
+
|
|
303
|
+
return { triggered: true, version: latestVersion };
|
|
304
|
+
} catch (err: any) {
|
|
305
|
+
return { triggered: false, version: latestVersion, reason: err?.message || 'spawn failed' };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -25,6 +25,7 @@ import { getGitContext } from './lib/gmtr-hook-utils.ts';
|
|
|
25
25
|
import {
|
|
26
26
|
persistClassificationResult,
|
|
27
27
|
routePrompt,
|
|
28
|
+
fetchEnrichment,
|
|
28
29
|
shouldSkipPromptRouting,
|
|
29
30
|
} from '../core/routing.ts';
|
|
30
31
|
import type { RouteResponse } from '../core/types.ts';
|
|
@@ -155,7 +156,7 @@ function formatFailureWarning(failure: RouterFailure): string {
|
|
|
155
156
|
|
|
156
157
|
// ── Format Intelligence Block ──
|
|
157
158
|
|
|
158
|
-
function formatIntelligence(data: RouteResponse): string {
|
|
159
|
+
function formatIntelligence(data: RouteResponse, enrichment?: Record<string, unknown> | null): string {
|
|
159
160
|
const c = data.classification || {};
|
|
160
161
|
const ts = data.token_savings || {};
|
|
161
162
|
const es = data.execution_summary || {};
|
|
@@ -450,6 +451,14 @@ function formatIntelligence(data: RouteResponse): string {
|
|
|
450
451
|
}
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
// Enrichment status — tell agent what happened with Packet 2
|
|
455
|
+
if (enrichment) {
|
|
456
|
+
// Enrichment was auto-fetched and merged — ISC scaffold + RE already in the output above
|
|
457
|
+
} else if (data.packet_2_status === 'pending' && data.enrichment_id) {
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(`Packet 2 (reverse engineering + ISC scaffold) is still generating. If needed, call gramatr_get_enrichment with enrichment_id="${data.enrichment_id}".`);
|
|
460
|
+
}
|
|
461
|
+
|
|
453
462
|
return lines.join('\n');
|
|
454
463
|
}
|
|
455
464
|
|
|
@@ -580,6 +589,29 @@ async function main() {
|
|
|
580
589
|
lastFailure = null;
|
|
581
590
|
}
|
|
582
591
|
|
|
592
|
+
// Auto-fetch Packet 2 enrichment if pending (reverse engineering + ISC scaffold)
|
|
593
|
+
// Brief wait — enrichment is usually pre-computed by the time Packet 1 returns.
|
|
594
|
+
// If it's not ready in 2s, inject what we have and tell the agent how to get it later.
|
|
595
|
+
let enrichment: Record<string, unknown> | null = null;
|
|
596
|
+
if (result && (result as any).packet_2_status === 'pending' && (result as any).enrichment_id) {
|
|
597
|
+
try {
|
|
598
|
+
enrichment = await fetchEnrichment((result as any).enrichment_id, 2000);
|
|
599
|
+
if (enrichment) {
|
|
600
|
+
// Merge enrichment into the classification so the existing formatting logic picks it up
|
|
601
|
+
const c = (result as any).classification;
|
|
602
|
+
if (c && enrichment.reverse_engineering) {
|
|
603
|
+
c.reverse_engineering = enrichment.reverse_engineering;
|
|
604
|
+
}
|
|
605
|
+
if (c && enrichment.isc_scaffold) {
|
|
606
|
+
c.isc_scaffold = enrichment.isc_scaffold;
|
|
607
|
+
}
|
|
608
|
+
if (enrichment.constraints_extracted) {
|
|
609
|
+
c.constraints_extracted = enrichment.constraints_extracted;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch { /* non-blocking — Packet 1 still delivers */ }
|
|
613
|
+
}
|
|
614
|
+
|
|
583
615
|
// Emit status to stderr
|
|
584
616
|
emitStatus(result, elapsed);
|
|
585
617
|
|
|
@@ -660,7 +692,7 @@ async function main() {
|
|
|
660
692
|
}
|
|
661
693
|
|
|
662
694
|
// Format and inject
|
|
663
|
-
const context = formatIntelligence(result);
|
|
695
|
+
const context = formatIntelligence(result, enrichment);
|
|
664
696
|
|
|
665
697
|
console.log(
|
|
666
698
|
JSON.stringify({
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
writeCurrentProjectContextFile,
|
|
34
34
|
persistSessionRegistration,
|
|
35
35
|
} from '../core/session.ts';
|
|
36
|
-
import { runVersionCheckAndNotify } from '../core/version-check.ts';
|
|
36
|
+
import { runVersionCheckAndNotify, autoUpgrade } from '../core/version-check.ts';
|
|
37
37
|
import { VERSION } from '../core/version.ts';
|
|
38
38
|
|
|
39
39
|
// ── stdout (Claude context injection) ──
|
|
@@ -480,10 +480,25 @@ async function main(): Promise<void> {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
// Opportunistic
|
|
484
|
-
//
|
|
483
|
+
// Opportunistic version check + auto-upgrade.
|
|
484
|
+
// 1. Check npm registry for newer @gramatr/client
|
|
485
|
+
// 2. If outdated, auto-install in background (detached, non-interactive)
|
|
486
|
+
// 3. Inject context so Claude tells the user to restart for new hooks
|
|
485
487
|
try {
|
|
486
|
-
await runVersionCheckAndNotify(VERSION);
|
|
488
|
+
const versionResult = await runVersionCheckAndNotify(VERSION);
|
|
489
|
+
if (versionResult?.isOutdated) {
|
|
490
|
+
const upgrade = autoUpgrade(versionResult.latestVersion);
|
|
491
|
+
if (upgrade.triggered) {
|
|
492
|
+
log(` Auto-upgrading gramatr: v${versionResult.installedVersion} → v${versionResult.latestVersion}`);
|
|
493
|
+
emitStdout(`gramatr auto-upgrade in progress: v${versionResult.installedVersion} → v${versionResult.latestVersion}. ` +
|
|
494
|
+
`Files are being updated in ~/.gramatr/. Tell the user: "gramatr has auto-upgraded to v${versionResult.latestVersion}. ` +
|
|
495
|
+
`Exit Claude Code (/exit) and restart with \`claude --resume\` to activate the new hooks and features."`);
|
|
496
|
+
} else {
|
|
497
|
+
emitStdout(`gramatr update available: v${versionResult.installedVersion} → v${versionResult.latestVersion}. ` +
|
|
498
|
+
`Auto-upgrade skipped (${upgrade.reason}). Recommend the user exit Claude Code (/exit) ` +
|
|
499
|
+
`then run \`npx @gramatr/client@latest install claude-code\` followed by \`claude --resume\`.`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
487
502
|
} catch {
|
|
488
503
|
// Silent — version check is strictly optional.
|
|
489
504
|
}
|