@pcircle/memesh 4.1.3 → 4.1.4
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/.mcp.json +1 -3
- package/README.de.md +12 -1
- package/README.es.md +14 -2
- package/README.fr.md +14 -2
- package/README.ja.md +12 -1
- package/README.ko.md +12 -1
- package/README.md +16 -4
- package/README.pt.md +14 -2
- package/README.th.md +14 -2
- package/README.vi.md +14 -2
- package/README.zh-CN.md +12 -1
- package/README.zh-TW.md +12 -1
- package/dashboard/dist/index.html +13 -4
- package/dist/cli/view-live.d.ts +2 -0
- package/dist/cli/view-live.d.ts.map +1 -0
- package/dist/cli/view-live.js +2177 -0
- package/dist/cli/view-live.js.map +1 -0
- package/dist/cli/view.d.ts +0 -1
- package/dist/cli/view.d.ts.map +1 -1
- package/dist/cli/view.js +0 -2169
- package/dist/cli/view.js.map +1 -1
- package/dist/core/analytics.d.ts +23 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +79 -0
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/config.d.ts +8 -4
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -9
- package/dist/core/config.js.map +1 -1
- package/dist/core/consolidator.d.ts.map +1 -1
- package/dist/core/consolidator.js +1 -0
- package/dist/core/consolidator.js.map +1 -1
- package/dist/core/demo.d.ts +9 -0
- package/dist/core/demo.d.ts.map +1 -0
- package/dist/core/demo.js +79 -0
- package/dist/core/demo.js.map +1 -0
- package/dist/core/doctor.d.ts +2 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +147 -5
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +65 -0
- package/dist/core/dreamer.d.ts.map +1 -0
- package/dist/core/dreamer.js +477 -0
- package/dist/core/dreamer.js.map +1 -0
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +9 -5
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/extractor.d.ts.map +1 -1
- package/dist/core/extractor.js +44 -22
- package/dist/core/extractor.js.map +1 -1
- package/dist/core/graph.d.ts +1 -0
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +8 -2
- package/dist/core/graph.js.map +1 -1
- package/dist/core/install-hooks.d.ts +41 -0
- package/dist/core/install-hooks.d.ts.map +1 -0
- package/dist/core/install-hooks.js +181 -0
- package/dist/core/install-hooks.js.map +1 -0
- package/dist/core/install-id.d.ts +9 -0
- package/dist/core/install-id.d.ts.map +1 -0
- package/dist/core/install-id.js +51 -0
- package/dist/core/install-id.js.map +1 -0
- package/dist/core/lesson-engine.d.ts +1 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +13 -13
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/llm-validator.d.ts +16 -0
- package/dist/core/llm-validator.d.ts.map +1 -0
- package/dist/core/llm-validator.js +182 -0
- package/dist/core/llm-validator.js.map +1 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +6 -2
- package/dist/core/operations.js.map +1 -1
- package/dist/core/projects.d.ts +14 -0
- package/dist/core/projects.d.ts.map +1 -0
- package/dist/core/projects.js +70 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/scoring.d.ts +0 -4
- package/dist/core/scoring.d.ts.map +1 -1
- package/dist/core/scoring.js +3 -10
- package/dist/core/scoring.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +2 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts +9 -0
- package/dist/core/signal-scorer.d.ts.map +1 -0
- package/dist/core/signal-scorer.js +70 -0
- package/dist/core/signal-scorer.js.map +1 -0
- package/dist/core/types.d.ts +2 -30
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version-check.d.ts +0 -1
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +0 -4
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +7 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +91 -0
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +7 -3
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +43 -18
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +23 -18
- package/dist/storage/conflicts.d.ts +4 -1
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +5 -2
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/transports/cli/cli.js +382 -32
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +160 -12
- package/dist/transports/http/server.js.map +1 -1
- package/hooks/hooks.json +12 -0
- package/package.json +16 -6
- package/plugin.json +1 -1
- package/scripts/hooks/_shared.js +228 -5
- package/scripts/hooks/pre-compact.js +19 -9
- package/scripts/hooks/session-start.js +7 -309
- package/scripts/hooks/session-summary.js +131 -29
- package/scripts/hooks/user-prompt-intent.js +159 -0
- package/skills/memesh/SKILL.md +9 -5
- package/skills/memesh-review/SKILL.md +24 -0
package/scripts/hooks/_shared.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
2
3
|
import { createRequire } from 'module';
|
|
3
4
|
import { homedir } from 'os';
|
|
4
5
|
import { dirname, join } from 'path';
|
|
@@ -6,6 +7,22 @@ import { fileURLToPath } from 'url';
|
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Return the user's home directory, honoring HOME env var first.
|
|
12
|
+
*
|
|
13
|
+
* On POSIX, os.homedir() already consults HOME. On Windows, it ignores
|
|
14
|
+
* env vars and reads GetUserProfileDirectoryW directly — which makes
|
|
15
|
+
* tests unable to redirect home-dir lookups to a tmp dir. Honoring HOME
|
|
16
|
+
* first lets tests set HOME=<tmpdir> and have it actually take effect
|
|
17
|
+
* across platforms. Production users on Windows almost never set HOME,
|
|
18
|
+
* so this falls through to os.homedir() as before.
|
|
19
|
+
*
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function homeDir() {
|
|
23
|
+
return process.env.HOME ?? homedir();
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
/**
|
|
10
27
|
* Resolve the package root from a hook file's `import.meta.url`.
|
|
11
28
|
*
|
|
@@ -47,7 +64,7 @@ export function resolvePluginRoot(metaUrl) {
|
|
|
47
64
|
* @returns {Record<string, any>}
|
|
48
65
|
*/
|
|
49
66
|
export function readHookConfig(_env = process.env) {
|
|
50
|
-
const path = join(
|
|
67
|
+
const path = join(homeDir(), '.memesh', 'config.json');
|
|
51
68
|
if (!existsSync(path)) return {};
|
|
52
69
|
try {
|
|
53
70
|
const raw = readFileSync(path, 'utf8');
|
|
@@ -217,8 +234,8 @@ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
|
217
234
|
* @returns {{ db: any, dbPath: string }}
|
|
218
235
|
*/
|
|
219
236
|
export function openHookDb(env = process.env, opts = {}) {
|
|
220
|
-
const dbPath = env.MEMESH_DB_PATH || join(
|
|
221
|
-
const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(
|
|
237
|
+
const dbPath = env.MEMESH_DB_PATH || join(homeDir(), '.memesh', 'knowledge-graph.db');
|
|
238
|
+
const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(homeDir(), '.memesh');
|
|
222
239
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
223
240
|
|
|
224
241
|
const Database = require('better-sqlite3');
|
|
@@ -241,7 +258,7 @@ const PRIVATE_DIR_MODE = 0o700;
|
|
|
241
258
|
const PRIVATE_FILE_MODE = 0o600;
|
|
242
259
|
|
|
243
260
|
export function getMemeshDir(env = process.env) {
|
|
244
|
-
return env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(
|
|
261
|
+
return env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(homeDir(), '.memesh');
|
|
245
262
|
}
|
|
246
263
|
|
|
247
264
|
export function ensurePrivateDir(dirPath) {
|
|
@@ -295,3 +312,209 @@ export function buildReferenceContext(memoryLines) {
|
|
|
295
312
|
'```',
|
|
296
313
|
].join('\n');
|
|
297
314
|
}
|
|
315
|
+
|
|
316
|
+
// ─── Auto-update shared helpers ─────────────────────────────────────────────
|
|
317
|
+
// Shared between SessionStart and Stop hooks for auto-update coordination.
|
|
318
|
+
|
|
319
|
+
export function readUpdateCheckCache(installedVersion) {
|
|
320
|
+
if (process.env.MEMESH_UPDATE_CHECK_PATH) {
|
|
321
|
+
try {
|
|
322
|
+
const overridePath = process.env.MEMESH_UPDATE_CHECK_PATH;
|
|
323
|
+
if (!existsSync(overridePath)) return null;
|
|
324
|
+
const parsed = JSON.parse(readFileSync(overridePath, 'utf8'));
|
|
325
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
326
|
+
return parsed;
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const versionTag = typeof installedVersion === 'string'
|
|
332
|
+
&& /^[0-9A-Za-z.+-]+$/.test(installedVersion)
|
|
333
|
+
? installedVersion
|
|
334
|
+
: 'unknown';
|
|
335
|
+
const cachePath = join(homeDir(), '.memesh', `update-check.${versionTag}.json`);
|
|
336
|
+
try {
|
|
337
|
+
if (!existsSync(cachePath)) return null;
|
|
338
|
+
const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
339
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
340
|
+
return parsed;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].+)?$/;
|
|
347
|
+
|
|
348
|
+
export function classifyBumpHook(from, to) {
|
|
349
|
+
const a = SEMVER_RE.exec((from || '').trim());
|
|
350
|
+
const b = SEMVER_RE.exec((to || '').trim());
|
|
351
|
+
if (!a || !b) return null;
|
|
352
|
+
const ai = a.slice(1, 4).map(Number);
|
|
353
|
+
const bi = b.slice(1, 4).map(Number);
|
|
354
|
+
if (bi[0] > ai[0]) return 'major';
|
|
355
|
+
if (bi[0] < ai[0]) return null;
|
|
356
|
+
if (bi[1] > ai[1]) return 'minor';
|
|
357
|
+
if (bi[1] < ai[1]) return null;
|
|
358
|
+
if (bi[2] > ai[2]) return 'patch';
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const POLICY_RANK = { off: 0, patch: 1, minor: 2, major: 3 };
|
|
363
|
+
const BUMP_RANK = { patch: 1, minor: 2, major: 3 };
|
|
364
|
+
const AUTO_UPDATE_CACHE_FRESHNESS_MS = 24 * 60 * 60 * 1000;
|
|
365
|
+
|
|
366
|
+
export function decideAutoUpdateHook(currentVersion, cache, policy) {
|
|
367
|
+
if (!cache || cache.currentVersion !== currentVersion) return { run: false };
|
|
368
|
+
const latest = cache.latestVersion;
|
|
369
|
+
if (typeof latest !== 'string' || !latest) return { run: false };
|
|
370
|
+
const bump = classifyBumpHook(currentVersion, latest);
|
|
371
|
+
if (!bump) return { run: false };
|
|
372
|
+
|
|
373
|
+
const lastSuccessAt = cache.lastSuccessfulCheckAt;
|
|
374
|
+
const lastSuccessMs = typeof lastSuccessAt === 'string' ? Date.parse(lastSuccessAt) : NaN;
|
|
375
|
+
const cacheAgeMs = Number.isFinite(lastSuccessMs) ? Date.now() - lastSuccessMs : Infinity;
|
|
376
|
+
if (cacheAgeMs > AUTO_UPDATE_CACHE_FRESHNESS_MS) {
|
|
377
|
+
return { run: false, reason: 'stale-cache' };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const policyAllows = (POLICY_RANK[policy] ?? 0) >= BUMP_RANK[bump];
|
|
381
|
+
if (policyAllows) return { run: true, latest, bump, deprecationOverride: false };
|
|
382
|
+
|
|
383
|
+
const deprecated = typeof cache.currentVersionDeprecation === 'string'
|
|
384
|
+
&& cache.currentVersionDeprecation.length > 0;
|
|
385
|
+
if (deprecated && bump === 'patch') {
|
|
386
|
+
return { run: true, latest, bump, deprecationOverride: true };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { run: false };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function logAutoUpdate(line) {
|
|
393
|
+
try {
|
|
394
|
+
const dir = getMemeshDir(process.env);
|
|
395
|
+
ensurePrivateDir(dir);
|
|
396
|
+
const path = join(dir, 'auto-update.log');
|
|
397
|
+
appendFileSync(path, `[${new Date().toISOString()}] ${line}\n`);
|
|
398
|
+
try { chmodSync(path, 0o600); } catch { /* non-POSIX */ }
|
|
399
|
+
} catch {
|
|
400
|
+
// Logging is best-effort.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export const AUTO_UPDATE_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
405
|
+
|
|
406
|
+
export function tryAcquireAutoUpdateLock(version) {
|
|
407
|
+
try {
|
|
408
|
+
const dir = join(homeDir(), '.memesh');
|
|
409
|
+
ensurePrivateDir(dir);
|
|
410
|
+
const lockPath = join(dir, 'auto-update.lock');
|
|
411
|
+
const fs = require('fs');
|
|
412
|
+
const myToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
413
|
+
const payload = `${myToken}\n${process.pid}\n${Date.now()}\n${version}\n`;
|
|
414
|
+
try {
|
|
415
|
+
// O_CREAT|O_EXCL is the POSIX atomic create primitive — only one process wins.
|
|
416
|
+
const O_FLAGS = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL;
|
|
417
|
+
const fd = fs.openSync(lockPath, O_FLAGS, 0o600);
|
|
418
|
+
try { fs.writeFileSync(fd, payload); } finally { fs.closeSync(fd); }
|
|
419
|
+
return { acquired: true, lockPath };
|
|
420
|
+
} catch (err) {
|
|
421
|
+
if (err?.code !== 'EEXIST') throw err;
|
|
422
|
+
}
|
|
423
|
+
let stat;
|
|
424
|
+
try { stat = fs.statSync(lockPath); } catch { return { acquired: false, lockPath }; }
|
|
425
|
+
if (Date.now() - stat.mtimeMs <= AUTO_UPDATE_LOCK_TTL_MS) {
|
|
426
|
+
return { acquired: false, lockPath };
|
|
427
|
+
}
|
|
428
|
+
const tempPath = `${lockPath}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
429
|
+
try {
|
|
430
|
+
fs.writeFileSync(tempPath, payload, { mode: 0o600 });
|
|
431
|
+
try { fs.unlinkSync(lockPath); } catch (e2) {
|
|
432
|
+
if (e2?.code !== 'ENOENT') {
|
|
433
|
+
try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
|
|
434
|
+
return { acquired: false, lockPath };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
fs.renameSync(tempPath, lockPath);
|
|
438
|
+
} catch {
|
|
439
|
+
try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
|
|
440
|
+
return { acquired: false, lockPath };
|
|
441
|
+
}
|
|
442
|
+
// The "race" here is the lock primitive itself: we just renamed our
|
|
443
|
+
// temp file onto lockPath, then immediately read back to see whether
|
|
444
|
+
// our token won. If a concurrent caller's rename landed in the
|
|
445
|
+
// window between our rename and our read, their token is now there
|
|
446
|
+
// and we correctly conclude we did NOT acquire the lock. That race
|
|
447
|
+
// outcome IS the contract — CodeQL flags it as TOCTOU but it is the
|
|
448
|
+
// standard last-writer-wins lock-file pattern.
|
|
449
|
+
// codeql[js/file-system-race]: justified — the read is the lock
|
|
450
|
+
// verification step, not a post-check before mutation.
|
|
451
|
+
let recorded;
|
|
452
|
+
try { recorded = fs.readFileSync(lockPath, 'utf8'); } catch { return { acquired: false, lockPath }; }
|
|
453
|
+
return recorded.split('\n')[0] === myToken
|
|
454
|
+
? { acquired: true, lockPath }
|
|
455
|
+
: { acquired: false, lockPath };
|
|
456
|
+
} catch {
|
|
457
|
+
return { acquired: false, lockPath: null };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Spawn `npm install -g @pcircle/memesh@<version>` detached so the upgrade
|
|
463
|
+
* can finish after this hook returns. Never blocks the session.
|
|
464
|
+
*
|
|
465
|
+
* @param {string} version - Target version to install
|
|
466
|
+
* @param {boolean} deprecationOverride - Whether this is a security-override install
|
|
467
|
+
* @param {object|null} installChannelMod - Preloaded dist/core/install-channel.js module
|
|
468
|
+
* @returns {{ state: 'spawned'|'in-progress'|'channel'|'failed' }}
|
|
469
|
+
*/
|
|
470
|
+
export function spawnAutoUpdate(version, deprecationOverride, installChannelMod) {
|
|
471
|
+
let lock = null;
|
|
472
|
+
try {
|
|
473
|
+
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
474
|
+
let channel = 'unknown';
|
|
475
|
+
if (installChannelMod) {
|
|
476
|
+
try {
|
|
477
|
+
channel = installChannelMod.getCurrentInstallChannel({ packageRoot: pluginRoot });
|
|
478
|
+
} catch { /* best-effort */ }
|
|
479
|
+
}
|
|
480
|
+
if (channel !== 'npm-global') {
|
|
481
|
+
logAutoUpdate(
|
|
482
|
+
`auto-update SKIPPED: install channel '${channel}' does not support self-update via npm install -g`
|
|
483
|
+
);
|
|
484
|
+
return { state: 'channel' };
|
|
485
|
+
}
|
|
486
|
+
lock = tryAcquireAutoUpdateLock(version);
|
|
487
|
+
if (!lock.acquired) {
|
|
488
|
+
logAutoUpdate(
|
|
489
|
+
`auto-update SKIPPED: another session already holds ${lock.lockPath ?? 'auto-update.lock'} for this upgrade`
|
|
490
|
+
);
|
|
491
|
+
return { state: 'in-progress' };
|
|
492
|
+
}
|
|
493
|
+
const dir = getMemeshDir(process.env);
|
|
494
|
+
ensurePrivateDir(dir);
|
|
495
|
+
const logPath = join(dir, 'auto-update.log');
|
|
496
|
+
let fd = -1;
|
|
497
|
+
try { fd = openSync(logPath, 'a', 0o600); } catch { fd = -1; }
|
|
498
|
+
const stdio = fd >= 0 ? ['ignore', fd, fd] : 'ignore';
|
|
499
|
+
const child = spawn(
|
|
500
|
+
'npm',
|
|
501
|
+
['install', '-g', `@pcircle/memesh@${version}`],
|
|
502
|
+
// windowsHide avoids a flashing console window on Windows; harmless on POSIX.
|
|
503
|
+
{ detached: true, stdio, env: process.env, windowsHide: true },
|
|
504
|
+
);
|
|
505
|
+
child.unref();
|
|
506
|
+
if (fd >= 0) {
|
|
507
|
+
try { closeSync(fd); } catch { /* ignore */ }
|
|
508
|
+
}
|
|
509
|
+
logAutoUpdate(
|
|
510
|
+
`auto-update spawn: target=${version}${deprecationOverride ? ' (deprecation-override)' : ''} pid=${child.pid ?? 'unknown'} lock=${lock.lockPath}`
|
|
511
|
+
);
|
|
512
|
+
return { state: 'spawned' };
|
|
513
|
+
} catch (err) {
|
|
514
|
+
logAutoUpdate(`auto-update spawn FAILED: ${err?.message ?? err}`);
|
|
515
|
+
if (lock?.acquired && lock.lockPath) {
|
|
516
|
+
try { require('fs').unlinkSync(lock.lockPath); } catch { /* best-effort */ }
|
|
517
|
+
}
|
|
518
|
+
return { state: 'failed' };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
@@ -40,17 +40,27 @@ process.stdin.on('end', () => {
|
|
|
40
40
|
if (!line.trim()) continue;
|
|
41
41
|
try {
|
|
42
42
|
const entry = JSON.parse(line);
|
|
43
|
-
//
|
|
43
|
+
// Current format: {type:'assistant', message:{content:[{type:'tool_use',...}]}}
|
|
44
|
+
if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
45
|
+
for (const block of entry.message.content) {
|
|
46
|
+
if (block.type !== 'tool_use') continue;
|
|
47
|
+
toolCallCount++;
|
|
48
|
+
const name = block.name || '';
|
|
49
|
+
if (name === 'Edit' || name === 'Write' || name === 'MultiEdit') {
|
|
50
|
+
const filePath = block.input?.file_path || block.input?.path || '';
|
|
51
|
+
if (filePath) editedFiles.add(basename(filePath));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Legacy format: {role:'assistant', content:[{type:'tool_use',...}]}
|
|
44
56
|
if (entry.role === 'assistant' && Array.isArray(entry.content)) {
|
|
45
57
|
for (const block of entry.content) {
|
|
46
|
-
if (block.type
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (filePath) editedFiles.add(basename(filePath));
|
|
53
|
-
}
|
|
58
|
+
if (block.type !== 'tool_use') continue;
|
|
59
|
+
toolCallCount++;
|
|
60
|
+
const name = block.name || '';
|
|
61
|
+
if (name === 'Edit' || name === 'Write' || name === 'MultiEdit') {
|
|
62
|
+
const filePath = block.input?.file_path || block.input?.path || '';
|
|
63
|
+
if (filePath) editedFiles.add(basename(filePath));
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
}
|
|
@@ -6,14 +6,14 @@ import { createHash } from 'crypto';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { join, basename } from 'path';
|
|
8
8
|
import { pathToFileURL } from 'url';
|
|
9
|
-
import { existsSync, readFileSync, unlinkSync, rmSync, appendFileSync, chmodSync
|
|
9
|
+
import { existsSync, readFileSync, unlinkSync, rmSync, appendFileSync, chmodSync } from 'fs';
|
|
10
10
|
import {
|
|
11
11
|
buildReferenceContext,
|
|
12
12
|
ensurePrivateDir,
|
|
13
13
|
getMemeshDir,
|
|
14
14
|
isAgenticOrchestrationEnabled,
|
|
15
15
|
isTrustedForAutoContext,
|
|
16
|
-
|
|
16
|
+
readUpdateCheckCache,
|
|
17
17
|
resolvePluginRoot,
|
|
18
18
|
resolveSessionLimit,
|
|
19
19
|
writePrivateJson,
|
|
@@ -43,45 +43,6 @@ const memeshDir = getMemeshDir(process.env);
|
|
|
43
43
|
const throttlePath = join(memeshDir, 'session-recalled-files.json');
|
|
44
44
|
const nudgeFlagsDir = join(memeshDir, 'agent-nudge-flags');
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Read the cached npm update check produced by core/version-check.ts.
|
|
48
|
-
* Hooks must not depend on dist/, so this duplicates the path constant
|
|
49
|
-
* (mirrored in src/core/version-check.ts:6) and parses defensively.
|
|
50
|
-
* Returns null on missing/corrupt cache rather than throwing — the
|
|
51
|
-
* deprecation warning is best-effort.
|
|
52
|
-
*/
|
|
53
|
-
function readUpdateCheckCache(installedVersion) {
|
|
54
|
-
// Codex round 38: scope cache reads by installed version so a
|
|
55
|
-
// multi-install setup (global 4.1.3 + project-local 4.1.1)
|
|
56
|
-
// doesn't fight over a single cache slot. Each install reads
|
|
57
|
-
// its own per-version file. Test/integration envs can still
|
|
58
|
-
// pin a specific path via MEMESH_UPDATE_CHECK_PATH.
|
|
59
|
-
if (process.env.MEMESH_UPDATE_CHECK_PATH) {
|
|
60
|
-
try {
|
|
61
|
-
const overridePath = process.env.MEMESH_UPDATE_CHECK_PATH;
|
|
62
|
-
if (!existsSync(overridePath)) return null;
|
|
63
|
-
const parsed = JSON.parse(readFileSync(overridePath, 'utf8'));
|
|
64
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
65
|
-
return parsed;
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const versionTag = typeof installedVersion === 'string'
|
|
71
|
-
&& /^[0-9A-Za-z.+-]+$/.test(installedVersion)
|
|
72
|
-
? installedVersion
|
|
73
|
-
: 'unknown';
|
|
74
|
-
const cachePath = join(homedir(), '.memesh', `update-check.${versionTag}.json`);
|
|
75
|
-
try {
|
|
76
|
-
if (!existsSync(cachePath)) return null;
|
|
77
|
-
const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
78
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
79
|
-
return parsed;
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
46
|
/**
|
|
86
47
|
* Build the strong deprecation warning lines to prepend to the
|
|
87
48
|
* session-start banner when the installed version has been flagged
|
|
@@ -157,17 +118,10 @@ function buildDeprecationBanner(currentVersion, cache) {
|
|
|
157
118
|
} catch { /* best-effort — fall through to generic guidance */ }
|
|
158
119
|
|
|
159
120
|
if (channel === 'npm-global') {
|
|
160
|
-
// v4.1.3: only `memesh update` actually applies the upgrade.
|
|
161
|
-
// The autoUpdate config field is recognised (and the policy
|
|
162
|
-
// resolution + decision matrix run) but the actual spawn is
|
|
163
|
-
// deferred to the v4.1.4 Stop hook, so suggesting users set
|
|
164
|
-
// autoUpdate would point them at a remediation that doesn't
|
|
165
|
-
// yet act. Restore the autoUpdate suggestion in v4.1.4 once
|
|
166
|
-
// the spawn lands.
|
|
167
121
|
lines.push(
|
|
168
122
|
knownUpgradeTarget
|
|
169
|
-
? ` Run: memesh update`
|
|
170
|
-
: ` Run: memesh update (resolves @latest —
|
|
123
|
+
? ` Run: memesh update (or set autoUpdate: memesh config set autoUpdate patch)`
|
|
124
|
+
: ` Run: memesh update (resolves @latest — or set: memesh config set autoUpdate patch)`,
|
|
171
125
|
);
|
|
172
126
|
} else if (channel === 'source-checkout') {
|
|
173
127
|
lines.push(` Source checkout: pull and rebuild (\`git pull && npm install && npm run build\`).`);
|
|
@@ -194,79 +148,6 @@ function buildDeprecationBanner(currentVersion, cache) {
|
|
|
194
148
|
return lines;
|
|
195
149
|
}
|
|
196
150
|
|
|
197
|
-
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].+)?$/;
|
|
198
|
-
|
|
199
|
-
function classifyBumpHook(from, to) {
|
|
200
|
-
const a = SEMVER_RE.exec((from || '').trim());
|
|
201
|
-
const b = SEMVER_RE.exec((to || '').trim());
|
|
202
|
-
if (!a || !b) return null;
|
|
203
|
-
const ai = a.slice(1, 4).map(Number);
|
|
204
|
-
const bi = b.slice(1, 4).map(Number);
|
|
205
|
-
if (bi[0] > ai[0]) return 'major';
|
|
206
|
-
if (bi[0] < ai[0]) return null;
|
|
207
|
-
if (bi[1] > ai[1]) return 'minor';
|
|
208
|
-
if (bi[1] < ai[1]) return null;
|
|
209
|
-
if (bi[2] > ai[2]) return 'patch';
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const POLICY_RANK = { off: 0, patch: 1, minor: 2, major: 3 };
|
|
214
|
-
const BUMP_RANK = { patch: 1, minor: 2, major: 3 };
|
|
215
|
-
|
|
216
|
-
// Cache must be no older than 24 hours for auto-update to act on
|
|
217
|
-
// it. A stale cache could point at a target that is already
|
|
218
|
-
// superseded on npm — installing it would leave the user one step
|
|
219
|
-
// behind. When the cache is too old we skip the install and let the
|
|
220
|
-
// background refresh fetch fresh data for the next session.
|
|
221
|
-
const AUTO_UPDATE_CACHE_FRESHNESS_MS = 24 * 60 * 60 * 1000;
|
|
222
|
-
|
|
223
|
-
function decideAutoUpdateHook(currentVersion, cache, policy) {
|
|
224
|
-
if (!cache || cache.currentVersion !== currentVersion) return { run: false };
|
|
225
|
-
const latest = cache.latestVersion;
|
|
226
|
-
if (typeof latest !== 'string' || !latest) return { run: false };
|
|
227
|
-
const bump = classifyBumpHook(currentVersion, latest);
|
|
228
|
-
if (!bump) return { run: false };
|
|
229
|
-
|
|
230
|
-
// Stale-cache guard: refuse to auto-install a target that may
|
|
231
|
-
// have been superseded on npm since the last successful check.
|
|
232
|
-
const lastSuccessAt = cache.lastSuccessfulCheckAt;
|
|
233
|
-
const lastSuccessMs = typeof lastSuccessAt === 'string' ? Date.parse(lastSuccessAt) : NaN;
|
|
234
|
-
const cacheAgeMs = Number.isFinite(lastSuccessMs) ? Date.now() - lastSuccessMs : Infinity;
|
|
235
|
-
if (cacheAgeMs > AUTO_UPDATE_CACHE_FRESHNESS_MS) {
|
|
236
|
-
return { run: false, reason: 'stale-cache' };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const policyAllows = (POLICY_RANK[policy] ?? 0) >= BUMP_RANK[bump];
|
|
240
|
-
if (policyAllows) return { run: true, latest, bump, deprecationOverride: false };
|
|
241
|
-
|
|
242
|
-
// Deprecation security override: even with policy 'off', force a
|
|
243
|
-
// patch upgrade out of a deprecated version. Don't override beyond
|
|
244
|
-
// patch — minor / major can carry behaviour changes the user didn't
|
|
245
|
-
// agree to.
|
|
246
|
-
const deprecated = typeof cache.currentVersionDeprecation === 'string'
|
|
247
|
-
&& cache.currentVersionDeprecation.length > 0;
|
|
248
|
-
if (deprecated && bump === 'patch') {
|
|
249
|
-
return { run: true, latest, bump, deprecationOverride: true };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return { run: false };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Append a one-line outcome to the auto-update audit log. Best-effort.
|
|
257
|
-
*/
|
|
258
|
-
function logAutoUpdate(line) {
|
|
259
|
-
try {
|
|
260
|
-
const dir = getMemeshDir(process.env);
|
|
261
|
-
ensurePrivateDir(dir);
|
|
262
|
-
const path = join(dir, 'auto-update.log');
|
|
263
|
-
appendFileSync(path, `[${new Date().toISOString()}] ${line}\n`);
|
|
264
|
-
try { chmodSync(path, 0o600); } catch { /* non-POSIX */ }
|
|
265
|
-
} catch {
|
|
266
|
-
// Logging is best-effort.
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
151
|
/**
|
|
271
152
|
* Detect the install channel of the running memesh binary by
|
|
272
153
|
* delegating to src/core/install-channel.ts via the dist build. The
|
|
@@ -302,158 +183,6 @@ function detectInstallChannelHook(pluginRoot) {
|
|
|
302
183
|
}
|
|
303
184
|
}
|
|
304
185
|
|
|
305
|
-
// Cross-process lock window. Two parallel Claude sessions starting
|
|
306
|
-
// in the same minute would otherwise both decide to auto-update from
|
|
307
|
-
// the same cache and each fire `npm install -g` — wasted work at
|
|
308
|
-
// best, install corruption at worst on slow networks. The lock
|
|
309
|
-
// holds for the upper-bound of an `npm install -g` (most finish in
|
|
310
|
-
// under 60s; we allow 10 min as a safety floor) and is reclaimed
|
|
311
|
-
// when stale.
|
|
312
|
-
const AUTO_UPDATE_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
313
|
-
|
|
314
|
-
function tryAcquireAutoUpdateLock(version) {
|
|
315
|
-
try {
|
|
316
|
-
// Lock is MACHINE-GLOBAL because the npm install -g target is
|
|
317
|
-
// machine-global. Always use ~/.memesh/auto-update.lock,
|
|
318
|
-
// independent of MEMESH_DB_PATH — two sessions with different
|
|
319
|
-
// DB paths still serialize against the same global install.
|
|
320
|
-
const dir = join(homedir(), '.memesh');
|
|
321
|
-
ensurePrivateDir(dir);
|
|
322
|
-
const lockPath = join(dir, 'auto-update.lock');
|
|
323
|
-
const fs = require('fs');
|
|
324
|
-
const myToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
325
|
-
const payload = `${myToken}\n${process.pid}\n${Date.now()}\n${version}\n`;
|
|
326
|
-
// Fast path: O_EXCL atomic create. If we win, we own the lock.
|
|
327
|
-
try {
|
|
328
|
-
const fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
329
|
-
try { fs.writeFileSync(fd, payload); } finally { fs.closeSync(fd); }
|
|
330
|
-
return { acquired: true, lockPath };
|
|
331
|
-
} catch (err) {
|
|
332
|
-
if (err?.code !== 'EEXIST') throw err;
|
|
333
|
-
}
|
|
334
|
-
// Lock exists — check staleness.
|
|
335
|
-
let stat;
|
|
336
|
-
try { stat = fs.statSync(lockPath); } catch { return { acquired: false, lockPath }; }
|
|
337
|
-
if (Date.now() - stat.mtimeMs <= AUTO_UPDATE_LOCK_TTL_MS) {
|
|
338
|
-
return { acquired: false, lockPath };
|
|
339
|
-
}
|
|
340
|
-
// Stale-recovery: write our payload to a temp file, unlink the
|
|
341
|
-
// existing stale lock, then rename. unlink-before-rename keeps
|
|
342
|
-
// this Windows-safe (Windows fs.rename can fail when the
|
|
343
|
-
// destination exists, especially if another process briefly
|
|
344
|
-
// holds it open). On POSIX the unlink+rename pair is no slower
|
|
345
|
-
// than rename-replace. After the rename, read the lock back: if
|
|
346
|
-
// it carries OUR token, we won; if a peer's rename came after
|
|
347
|
-
// ours, we lost cleanly. No double-spawn on either platform.
|
|
348
|
-
const tempPath = `${lockPath}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
349
|
-
try {
|
|
350
|
-
fs.writeFileSync(tempPath, payload, { mode: 0o600 });
|
|
351
|
-
try { fs.unlinkSync(lockPath); } catch (err) {
|
|
352
|
-
// ENOENT (a peer already reclaimed) is fine; anything else
|
|
353
|
-
// means we can't replace the lock cleanly.
|
|
354
|
-
if (err?.code !== 'ENOENT') {
|
|
355
|
-
try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
|
|
356
|
-
return { acquired: false, lockPath };
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
fs.renameSync(tempPath, lockPath);
|
|
360
|
-
} catch {
|
|
361
|
-
try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
|
|
362
|
-
return { acquired: false, lockPath };
|
|
363
|
-
}
|
|
364
|
-
let recorded;
|
|
365
|
-
try { recorded = fs.readFileSync(lockPath, 'utf8'); } catch { return { acquired: false, lockPath }; }
|
|
366
|
-
const recordedToken = recorded.split('\n')[0];
|
|
367
|
-
if (recordedToken === myToken) {
|
|
368
|
-
return { acquired: true, lockPath };
|
|
369
|
-
}
|
|
370
|
-
return { acquired: false, lockPath };
|
|
371
|
-
} catch {
|
|
372
|
-
return { acquired: false, lockPath: null };
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Spawn `npm install -g @pcircle/memesh@<version>` detached so the
|
|
378
|
-
* upgrade can finish after this hook returns. We never block the
|
|
379
|
-
* session on the install — the running process keeps its current
|
|
380
|
-
* binary; the next session picks up the new one. Argv is an array
|
|
381
|
-
* (no shell interpolation), so the version string can never escape
|
|
382
|
-
* into a shell command.
|
|
383
|
-
*
|
|
384
|
-
* Two safety gates:
|
|
385
|
-
* 1. Install channel must be `npm-global`. Source checkouts and
|
|
386
|
-
* project-local installs would silently create a separate
|
|
387
|
-
* global install while the active copy stayed unchanged.
|
|
388
|
-
* 2. A filesystem lock at <memeshDir>/auto-update.lock serialises
|
|
389
|
-
* across parallel session-start processes. Without this, two
|
|
390
|
-
* Claude sessions starting at the same minute would each fire
|
|
391
|
-
* `npm install -g` concurrently.
|
|
392
|
-
*/
|
|
393
|
-
/**
|
|
394
|
-
* Return shape:
|
|
395
|
-
* { state: 'spawned' } — we own the lock and spawned npm
|
|
396
|
-
* { state: 'in-progress' } — another session owns the lock; an
|
|
397
|
-
* auto-update is racing somewhere
|
|
398
|
-
* { state: 'channel' } — install channel doesn't support
|
|
399
|
-
* self-update; safe to refresh
|
|
400
|
-
* { state: 'failed' } — error before/during spawn
|
|
401
|
-
* Callers use 'in-progress' the same way they use 'spawned': skip
|
|
402
|
-
* the cache refresh, since dist/* is being rewritten by some
|
|
403
|
-
* process. Only 'channel' / no-decision is a clean idle state.
|
|
404
|
-
*/
|
|
405
|
-
function spawnAutoUpdate(version, deprecationOverride) {
|
|
406
|
-
let lock = null;
|
|
407
|
-
try {
|
|
408
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
409
|
-
const channel = detectInstallChannelHook(pluginRoot);
|
|
410
|
-
if (channel !== 'npm-global') {
|
|
411
|
-
logAutoUpdate(
|
|
412
|
-
`auto-update SKIPPED: install channel '${channel}' does not support self-update via npm install -g`
|
|
413
|
-
);
|
|
414
|
-
return { state: 'channel' };
|
|
415
|
-
}
|
|
416
|
-
lock = tryAcquireAutoUpdateLock(version);
|
|
417
|
-
if (!lock.acquired) {
|
|
418
|
-
logAutoUpdate(
|
|
419
|
-
`auto-update SKIPPED: another session-start already holds ${lock.lockPath ?? 'auto-update.lock'} for this upgrade`
|
|
420
|
-
);
|
|
421
|
-
return { state: 'in-progress' };
|
|
422
|
-
}
|
|
423
|
-
const dir = getMemeshDir(process.env);
|
|
424
|
-
ensurePrivateDir(dir);
|
|
425
|
-
const logPath = join(dir, 'auto-update.log');
|
|
426
|
-
let fd = -1;
|
|
427
|
-
try { fd = openSync(logPath, 'a', 0o600); } catch { fd = -1; }
|
|
428
|
-
const stdio = fd >= 0 ? ['ignore', fd, fd] : 'ignore';
|
|
429
|
-
const child = spawn(
|
|
430
|
-
'npm',
|
|
431
|
-
['install', '-g', `@pcircle/memesh@${version}`],
|
|
432
|
-
// windowsHide avoids a flashing console window every time the
|
|
433
|
-
// hook spawns the upgrade on Windows; harmless on POSIX.
|
|
434
|
-
{ detached: true, stdio, env: process.env, windowsHide: true },
|
|
435
|
-
);
|
|
436
|
-
child.unref();
|
|
437
|
-
if (fd >= 0) {
|
|
438
|
-
try { closeSync(fd); } catch { /* ignore */ }
|
|
439
|
-
}
|
|
440
|
-
logAutoUpdate(
|
|
441
|
-
`auto-update spawn: target=${version}${deprecationOverride ? ' (deprecation-override)' : ''} pid=${child.pid ?? 'unknown'} lock=${lock.lockPath}`
|
|
442
|
-
);
|
|
443
|
-
return { state: 'spawned' };
|
|
444
|
-
} catch (err) {
|
|
445
|
-
logAutoUpdate(`auto-update spawn FAILED: ${err?.message ?? err}`);
|
|
446
|
-
// Release the lock so the next session can retry. Without this,
|
|
447
|
-
// a transient PATH/permission failure would freeze auto-update
|
|
448
|
-
// for the full 10-minute TTL even after the user fixes the
|
|
449
|
-
// root cause.
|
|
450
|
-
if (lock?.acquired && lock.lockPath) {
|
|
451
|
-
try { require('fs').unlinkSync(lock.lockPath); } catch { /* best-effort */ }
|
|
452
|
-
}
|
|
453
|
-
return { state: 'failed' };
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
186
|
// Don't fire a fresh-check more often than this. Two parallel
|
|
458
187
|
// session-starts both spawning `memesh status` could otherwise race
|
|
459
188
|
// the cache: a later writer that hits a deprecation-only timeout
|
|
@@ -577,32 +306,8 @@ function runPostBannerUpdateTasks() {
|
|
|
577
306
|
installedVersion = typeof pkg.version === 'string' ? pkg.version : null;
|
|
578
307
|
} catch { /* best-effort */ }
|
|
579
308
|
if (!installedVersion) return;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const decision = decideAutoUpdateHook(installedVersion, cache, policy);
|
|
583
|
-
if (decision.run) {
|
|
584
|
-
// v4.1.3: log a PENDING entry instead of spawning npm install
|
|
585
|
-
// -g, but only when the install channel actually supports
|
|
586
|
-
// self-update. For source-checkout / npm-local installs the
|
|
587
|
-
// PENDING line would point at remediation that will never
|
|
588
|
-
// apply (`memesh update` refuses those channels). Skip
|
|
589
|
-
// silently for those — the deprecation banner already gives
|
|
590
|
-
// them the channel-appropriate hint (git pull, project
|
|
591
|
-
// npm install).
|
|
592
|
-
try {
|
|
593
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
594
|
-
const channel = detectInstallChannelHook(pluginRoot);
|
|
595
|
-
if (channel === 'npm-global') {
|
|
596
|
-
logAutoUpdate(
|
|
597
|
-
`auto-update PENDING: policy='${policy}' bump='${decision.bump}' target=${decision.latest}` +
|
|
598
|
-
(decision.deprecationOverride ? ' (deprecation-override)' : '') +
|
|
599
|
-
' — will run from Stop hook in v4.1.4. For now run `memesh update` manually.'
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
} catch {
|
|
603
|
-
// Best-effort: if channel detection fails, skip the log.
|
|
604
|
-
}
|
|
605
|
-
}
|
|
309
|
+
// Auto-update spawn moved to Stop hook (v4.1.4) to avoid TOCTOU race
|
|
310
|
+
// where npm install -g overwrites dist/ while peer hooks are still reading it.
|
|
606
311
|
spawnFreshUpdateCheck(installedVersion);
|
|
607
312
|
} catch {
|
|
608
313
|
// Best-effort — never crash the hook on a network or fs hiccup.
|
|
@@ -933,14 +638,7 @@ process.stdin.on('end', async () => {
|
|
|
933
638
|
? [...deprecationLines, '', ...memorySummary.split('\n')].join('\n')
|
|
934
639
|
: memorySummary;
|
|
935
640
|
|
|
936
|
-
|
|
937
|
-
suppressOutput: true,
|
|
938
|
-
hookSpecificOutput: {
|
|
939
|
-
hookEventName: 'SessionStart',
|
|
940
|
-
additionalContext: buildReferenceContext(memorySummaryWithBanner.split('\n')),
|
|
941
|
-
},
|
|
942
|
-
};
|
|
943
|
-
console.log(JSON.stringify(hookOutput));
|
|
641
|
+
output(buildReferenceContext(memorySummaryWithBanner.split('\n')));
|
|
944
642
|
} finally {
|
|
945
643
|
db.close();
|
|
946
644
|
}
|