@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 CHANGED
@@ -261,14 +261,24 @@ function installClientFiles(): void {
261
261
  mkdirSync(join(CLIENT_DIR, sub), { recursive: true });
262
262
  }
263
263
 
264
- // Hooks (7 core + utils)
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;
@@ -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/gramatr/latest';
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 npm version check prints a notification to stderr if a
484
- // newer gramatr is published. Never blocks or crashes the session start.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramatr/client",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },