@gramatr/client 0.6.21 → 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
 
@@ -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
+ }
@@ -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,16 +480,24 @@ 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.
485
- // Also injects context to stdout so Claude can proactively inform the user.
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
486
487
  try {
487
488
  const versionResult = await runVersionCheckAndNotify(VERSION);
488
489
  if (versionResult?.isOutdated) {
489
- emitStdout(`gramatr update available: v${versionResult.installedVersion} → v${versionResult.latestVersion}. ` +
490
- `Inform the user that a gramatr update is available and recommend they exit Claude Code (/exit) ` +
491
- `then run \`npx gramatr@latest install claude-code\` followed by \`claude --resume\` to get the latest features. ` +
492
- `Hooks are loaded at session start so a restart is required for new hook code to take effect.`);
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
+ }
493
501
  }
494
502
  } catch {
495
503
  // Silent — version check is strictly optional.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramatr/client",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },