2026-01-27 10:38:34 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* SessionStart hook - logs session ID, symlinks latest plan on clear
|
|
|
|
|
*
|
|
|
|
|
* LATEST PLAN SYMLINK LOGIC:
|
|
|
|
|
* ─────────────────────────
|
|
|
|
|
* When user exits Plan Mode, Claude offers "Clear session and start work".
|
|
|
|
|
* If user chooses Clear → SessionStart fires with source='clear'.
|
|
|
|
|
*
|
|
|
|
|
* Flow:
|
|
|
|
|
* 1. EnterPlanMode → Claude writes plan to ~/.claude/plans/<name>.md
|
|
|
|
|
* 2. ExitPlanMode → Claude offers "Clear session?" option
|
|
|
|
|
* 3. User clicks Clear → SessionStart(source='clear')
|
|
|
|
|
* 4. This hook creates .claude/plans/LATEST.md → ~/.claude/plans/<newest>.md
|
|
|
|
|
*
|
|
|
|
|
* Conditions:
|
|
|
|
|
* - Only on source='clear' (not on init/resume)
|
|
|
|
|
* - Only if plan modified < 60 seconds ago (fresh plan)
|
|
|
|
|
* - Symlink points to global plan file for easy access from project
|
2026-08-16 15:18:22 +01:00
|
|
|
* - Plans dir honours the `plansDirectory` setting (docs/settings.md:308)
|
|
|
|
|
* - LATEST.md is replaced ONLY when it is a symlink into that plans dir; a user
|
|
|
|
|
* file, a directory or a foreign symlink is preserved and logged as a conflict
|
2026-01-27 10:38:34 +00:00
|
|
|
*
|
|
|
|
|
* Cleanup: /brewcode:teardown removes .claude/plans/ directory
|
|
|
|
|
*/
|
2026-08-16 15:18:22 +01:00
|
|
|
import { readStdin, output, log, getState, saveState, projectRoot } from './lib/utils.mjs';
|
|
|
|
|
import { readFileSync, readdirSync, statSync, lstatSync, readlinkSync, mkdirSync, symlinkSync, unlinkSync, existsSync, realpathSync } from 'fs';
|
2026-03-05 19:29:10 +00:00
|
|
|
import { execFileSync } from 'child_process';
|
2026-08-16 15:18:22 +01:00
|
|
|
import { join, dirname, resolve, sep } from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
2026-01-27 10:38:34 +00:00
|
|
|
import { homedir } from 'os';
|
|
|
|
|
|
2026-04-05 18:56:15 +01:00
|
|
|
const VERSION_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
2026-01-27 10:38:34 +00:00
|
|
|
const PLAN_FRESHNESS_MS = 60_000;
|
|
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
const LATEST_LINK_NAME = 'LATEST.md';
|
|
|
|
|
|
|
|
|
|
/** `~/x` -> home-relative, anything else -> project-root-relative (docs/settings.md:308) */
|
|
|
|
|
function expandPath(p, root) {
|
|
|
|
|
if (p === '~') return homedir();
|
|
|
|
|
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
|
|
|
return resolve(root, p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Where Claude Code stores plan files: `plansDirectory` in settings precedence
|
|
|
|
|
* (local > project > user), default `~/.claude/plans`.
|
|
|
|
|
*/
|
|
|
|
|
export function resolvePlansDir(root, sessionId = null) {
|
|
|
|
|
const candidates = [
|
|
|
|
|
join(root, '.claude', 'settings.local.json'),
|
|
|
|
|
join(root, '.claude', 'settings.json'),
|
|
|
|
|
join(homedir(), '.claude', 'settings.json')
|
|
|
|
|
];
|
|
|
|
|
for (const file of candidates) {
|
|
|
|
|
try {
|
|
|
|
|
if (!existsSync(file)) continue;
|
|
|
|
|
const value = JSON.parse(readFileSync(file, 'utf8')).plansDirectory;
|
|
|
|
|
if (typeof value === 'string' && value.length > 0) return expandPath(value, root);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('warn', '[session-start]', `Ignoring unreadable settings ${file}: ${e.message}`, root, sessionId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return join(homedir(), '.claude', 'plans');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear the way for a fresh LATEST.md symlink WITHOUT destroying anything this hook
|
|
|
|
|
* did not create: only a symlink whose target resolves inside `plansDir` is removed.
|
|
|
|
|
* A regular file, a directory or a foreign symlink is preserved and reported.
|
|
|
|
|
* @returns {boolean} true when the path is free to be symlinked
|
|
|
|
|
*/
|
|
|
|
|
function claimLatestLink(latestLink, plansDir, root, sessionId) {
|
|
|
|
|
const preserve = (why) => {
|
|
|
|
|
log('warn', '[session-start]', `Preserved ${latestLink}: ${why}`, root, sessionId);
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let st;
|
|
|
|
|
try {
|
|
|
|
|
st = lstatSync(latestLink);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e.code === 'ENOENT') return true;
|
|
|
|
|
return preserve(`cannot stat (${e.message})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!st.isSymbolicLink()) return preserve('not a symlink created by brewcode - plan link skipped');
|
|
|
|
|
|
|
|
|
|
let target;
|
|
|
|
|
try {
|
|
|
|
|
target = resolve(dirname(latestLink), readlinkSync(latestLink));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return preserve(`cannot read symlink target (${e.message})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (target !== plansDir && !target.startsWith(plansDir + sep)) {
|
|
|
|
|
return preserve(`foreign symlink target ${target} outside ${plansDir}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
unlinkSync(latestLink);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return preserve(`unlink failed (${e.message})`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 10:38:34 +00:00
|
|
|
/**
|
2026-08-16 15:18:22 +01:00
|
|
|
* Creates symlink <root>/.claude/plans/LATEST.md → <plansDir>/<newest>.md
|
|
|
|
|
* Only if newest plan is < 60 seconds old (fresh from Plan Mode) and the link path
|
|
|
|
|
* is free or holds a symlink this hook owns.
|
|
|
|
|
* @returns {string|null} Linked plan file name, or null when nothing was linked
|
2026-01-27 10:38:34 +00:00
|
|
|
*/
|
2026-08-16 15:18:22 +01:00
|
|
|
export function linkLatestPlan(root, sessionId = null) {
|
|
|
|
|
const plansDir = resolvePlansDir(root, sessionId);
|
|
|
|
|
const projectPlansDir = join(root, '.claude', 'plans');
|
|
|
|
|
const latestLink = join(projectPlansDir, LATEST_LINK_NAME);
|
2026-01-27 10:38:34 +00:00
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
if (!existsSync(plansDir)) return null;
|
2026-01-27 10:38:34 +00:00
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
const plans = readdirSync(plansDir)
|
|
|
|
|
.filter(f => f.endsWith('.md') && f !== LATEST_LINK_NAME)
|
2026-01-27 10:38:34 +00:00
|
|
|
.map(f => {
|
|
|
|
|
try {
|
2026-08-16 15:18:22 +01:00
|
|
|
const p = join(plansDir, f);
|
2026-01-27 10:38:34 +00:00
|
|
|
return { name: f, path: p, mtime: statSync(p).mtime };
|
|
|
|
|
} catch { return null; }
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
|
|
|
|
|
|
|
|
if (plans.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const latest = plans[0];
|
|
|
|
|
const ageMs = Date.now() - latest.mtime.getTime();
|
|
|
|
|
|
|
|
|
|
if (ageMs > PLAN_FRESHNESS_MS) return null;
|
|
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
// Containment: mkdirSync/symlinkSync FOLLOW a symlinked .claude/plans and would
|
|
|
|
|
// write LATEST.md outside the project root. claimLatestLink only guards the link.
|
|
|
|
|
try {
|
|
|
|
|
if (lstatSync(projectPlansDir).isSymbolicLink()) {
|
|
|
|
|
log('warn', '[session-start]', `Preserved ${projectPlansDir}: plans dir is a symlink outside the project - plan link skipped`, root, sessionId);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
} catch { /* ENOENT: mkdirSync creates it below */ }
|
|
|
|
|
|
2026-01-27 10:38:34 +00:00
|
|
|
mkdirSync(projectPlansDir, { recursive: true });
|
|
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
if (!claimLatestLink(latestLink, plansDir, root, sessionId)) return null;
|
|
|
|
|
|
2026-01-27 10:38:34 +00:00
|
|
|
symlinkSync(latest.path, latestLink);
|
|
|
|
|
|
|
|
|
|
return latest.name;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 19:29:10 +00:00
|
|
|
function isNewer(remoteVer, localVer) {
|
|
|
|
|
const l = localVer.split('.').map(Number);
|
|
|
|
|
const r = remoteVer.split('.').map(Number);
|
|
|
|
|
for (let i = 0; i < Math.max(l.length, r.length); i++) {
|
|
|
|
|
if ((r[i] || 0) > (l[i] || 0)) return true;
|
|
|
|
|
if ((r[i] || 0) < (l[i] || 0)) return false;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchJson(url, timeoutMs) {
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(url, { signal: controller.signal });
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
return res.ok ? await res.json() : null;
|
|
|
|
|
} catch {
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 17:53:27 +01:00
|
|
|
function parseVersion(pluginRoot) {
|
2026-03-06 08:45:15 +00:00
|
|
|
const match = pluginRoot.match(/\/(\d+\.\d+\.\d+)\/?$/);
|
2026-04-02 17:53:27 +01:00
|
|
|
if (match) return match[1];
|
|
|
|
|
try {
|
|
|
|
|
const pj = JSON.parse(readFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
|
|
|
if (pj.version) return pj.version;
|
|
|
|
|
} catch {}
|
|
|
|
|
return null;
|
2026-03-06 08:45:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function checkLatestVersion(pluginRoot, cwd, sessionId) {
|
2026-03-05 19:11:27 +00:00
|
|
|
try {
|
2026-04-02 17:53:27 +01:00
|
|
|
const local = parseVersion(pluginRoot);
|
2026-03-06 08:45:15 +00:00
|
|
|
if (!local) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `Cannot parse version from path: ${pluginRoot}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `Local brewcode: ${local}`, cwd, sessionId);
|
2026-03-05 19:11:27 +00:00
|
|
|
|
2026-08-09 08:18:53 +01:00
|
|
|
// Check 24h TTL cache. `fetchedAtMs` is an epoch-ms runtime marker, deliberately NOT
|
|
|
|
|
// `checkedAt` / `last_updated`: `checkedAt` is a retired provenance spelling
|
|
|
|
|
// (setup-status/references/artifact-metadata.md §8) and this is ephemeral state, never
|
|
|
|
|
// artifact provenance (§9). An old cache carrying the former simply misses and refetches.
|
2026-04-05 18:56:15 +01:00
|
|
|
const state = getState(cwd);
|
|
|
|
|
const cache = state._versionCache?.brewcode;
|
2026-08-09 08:18:53 +01:00
|
|
|
if (cache?.remote && cache?.fetchedAtMs) {
|
|
|
|
|
const age = Date.now() - cache.fetchedAtMs;
|
2026-04-05 18:56:15 +01:00
|
|
|
if (age < VERSION_CACHE_TTL_MS) {
|
|
|
|
|
log('debug', '[version]', `Using cached brewcode remote=${cache.remote} (age=${Math.round(age / 60000)}m)`, cwd, sessionId);
|
|
|
|
|
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
const url = 'https://api.github.com/repos/kochetkov-ma/claude-brewcode/releases/latest';
|
|
|
|
|
const data = await fetchJson(url, 1000);
|
|
|
|
|
if (!data) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `GitHub API fetch failed (timeout or error)`, cwd, sessionId);
|
2026-04-05 18:56:15 +01:00
|
|
|
// Fallback to stale cache if available
|
|
|
|
|
if (cache?.remote) {
|
|
|
|
|
log('debug', '[version]', `Falling back to stale cache: remote=${cache.remote}`, cwd, sessionId);
|
|
|
|
|
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
|
|
|
|
|
}
|
2026-03-06 08:45:15 +00:00
|
|
|
return { updateAvailable: false, local, remote: null, remoteFailed: true };
|
|
|
|
|
}
|
2026-03-05 19:11:27 +00:00
|
|
|
|
2026-03-05 19:29:10 +00:00
|
|
|
const remote = (data.tag_name || '').replace(/^v/, '');
|
2026-03-06 08:45:15 +00:00
|
|
|
if (!remote) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `GitHub tag_name missing: ${JSON.stringify(data.tag_name)}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return { updateAvailable: false, local, remote: null, remoteFailed: true };
|
|
|
|
|
}
|
2026-03-05 19:11:27 +00:00
|
|
|
|
2026-04-05 18:56:15 +01:00
|
|
|
// Update cache
|
|
|
|
|
const updatedState = getState(cwd);
|
|
|
|
|
updatedState._versionCache = updatedState._versionCache || {};
|
2026-08-09 08:18:53 +01:00
|
|
|
updatedState._versionCache.brewcode = { remote, fetchedAtMs: Date.now() };
|
2026-04-05 18:56:15 +01:00
|
|
|
saveState(cwd, updatedState);
|
|
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
const result = { updateAvailable: isNewer(remote, local), local, remote };
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `brewcode: local=${local}, remote=${remote}, update=${result.updateAvailable}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return result;
|
|
|
|
|
} catch (e) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `checkLatestVersion error: ${e.message}`, cwd, sessionId);
|
2026-03-05 19:29:10 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
async function checkClaudeCodeVersion(cwd, sessionId) {
|
2026-03-05 19:29:10 +00:00
|
|
|
try {
|
2026-03-06 08:45:15 +00:00
|
|
|
let rawOutput;
|
|
|
|
|
try {
|
|
|
|
|
rawOutput = execFileSync('claude', ['-v'], { timeout: 500, encoding: 'utf8' });
|
|
|
|
|
} catch (e) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `claude -v failed: ${e.message}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-03-05 19:29:10 +00:00
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
const local = (rawOutput.match(/(\d+\.\d+\.\d+)/) || [])[1];
|
|
|
|
|
if (!local) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `Cannot parse claude version from: ${rawOutput.trim()}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `Claude Code local: ${local}`, cwd, sessionId);
|
2026-03-05 19:29:10 +00:00
|
|
|
|
2026-04-05 18:56:15 +01:00
|
|
|
// Check 24h TTL cache
|
|
|
|
|
const state = getState(cwd);
|
|
|
|
|
const cache = state._versionCache?.claudeCode;
|
2026-08-09 08:18:53 +01:00
|
|
|
if (cache?.remote && cache?.fetchedAtMs) {
|
|
|
|
|
const age = Date.now() - cache.fetchedAtMs;
|
2026-04-05 18:56:15 +01:00
|
|
|
if (age < VERSION_CACHE_TTL_MS) {
|
|
|
|
|
log('debug', '[version]', `Using cached claude-code remote=${cache.remote} (age=${Math.round(age / 60000)}m)`, cwd, sessionId);
|
|
|
|
|
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
const url = 'https://registry.npmjs.org/@anthropic-ai/claude-code/latest';
|
|
|
|
|
const data = await fetchJson(url, 1000);
|
|
|
|
|
if (!data?.version) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `npm fetch failed or no version in response`, cwd, sessionId);
|
2026-04-05 18:56:15 +01:00
|
|
|
// Fallback to stale cache if available
|
|
|
|
|
if (cache?.remote) {
|
|
|
|
|
log('debug', '[version]', `Falling back to stale cache: remote=${cache.remote}`, cwd, sessionId);
|
|
|
|
|
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
|
|
|
|
|
}
|
2026-03-06 08:45:15 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 18:56:15 +01:00
|
|
|
// Update cache
|
|
|
|
|
const updatedState = getState(cwd);
|
|
|
|
|
updatedState._versionCache = updatedState._versionCache || {};
|
2026-08-09 08:18:53 +01:00
|
|
|
updatedState._versionCache.claudeCode = { remote: data.version, fetchedAtMs: Date.now() };
|
2026-04-05 18:56:15 +01:00
|
|
|
saveState(cwd, updatedState);
|
|
|
|
|
|
2026-03-06 08:45:15 +00:00
|
|
|
const result = { updateAvailable: isNewer(data.version, local), local, remote: data.version };
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `Claude Code: local=${local}, remote=${data.version}, update=${result.updateAvailable}`, cwd, sessionId);
|
2026-03-06 08:45:15 +00:00
|
|
|
return result;
|
|
|
|
|
} catch (e) {
|
2026-04-02 17:53:27 +01:00
|
|
|
log('debug', '[version]', `checkClaudeCodeVersion error: ${e.message}`, cwd, sessionId);
|
2026-03-05 19:11:27 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 10:38:34 +00:00
|
|
|
async function main() {
|
|
|
|
|
let cwd = null;
|
|
|
|
|
let session_id = null;
|
|
|
|
|
|
|
|
|
|
try {
|
2026-08-16 15:18:22 +01:00
|
|
|
cwd = projectRoot(null);
|
2026-01-27 10:38:34 +00:00
|
|
|
const input = await readStdin();
|
|
|
|
|
session_id = input.session_id;
|
2026-08-16 15:18:22 +01:00
|
|
|
// Stable project root for every stateful path (config, state, log, plan link);
|
|
|
|
|
// input.cwd stays a hint only - it moves mid-session (docs/hooks.md:717).
|
|
|
|
|
cwd = projectRoot(input.cwd);
|
2026-01-27 10:38:34 +00:00
|
|
|
const source = input.source;
|
2026-08-06 15:26:26 +01:00
|
|
|
// permission_mode: DOC-VERIFIED common field (2.1.223). Presence-guarded use below.
|
2026-06-27 10:57:42 +01:00
|
|
|
const permMode = input.permission_mode;
|
2026-01-27 10:38:34 +00:00
|
|
|
|
2026-06-27 10:57:42 +01:00
|
|
|
log('info', '[session-start]', `Started: ${session_id?.slice(0, 8) || 'unknown'} (${source}${permMode ? ', ' + permMode : ''})`, cwd, session_id);
|
2026-01-27 10:38:34 +00:00
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
if (source === 'clear') {
|
2026-01-27 10:38:34 +00:00
|
|
|
try {
|
2026-08-16 15:18:22 +01:00
|
|
|
const linked = linkLatestPlan(cwd, session_id);
|
2026-01-27 10:38:34 +00:00
|
|
|
if (linked) {
|
|
|
|
|
log('info', '[session-start]', `Linked: .claude/plans/LATEST.md -> ${linked}`, cwd, session_id);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
log('warn', '[session-start]', `Plan linking failed: ${e.message}`, cwd, session_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
|
|
|
|
|
const sessionShort = session_id?.slice(0, 8) || 'unknown';
|
|
|
|
|
|
2026-03-05 19:29:10 +00:00
|
|
|
const versionLines = [];
|
|
|
|
|
try {
|
|
|
|
|
const [brewcodeResult, claudeResult] = await Promise.all([
|
2026-03-06 08:45:15 +00:00
|
|
|
pluginRoot ? checkLatestVersion(pluginRoot, cwd, session_id).catch(() => null) : Promise.resolve(null),
|
|
|
|
|
checkClaudeCodeVersion(cwd, session_id).catch(() => null)
|
2026-03-05 19:29:10 +00:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (brewcodeResult === null && pluginRoot) {
|
|
|
|
|
versionLines.push(`check brewcode updates: https://github.com/kochetkov-ma/claude-brewcode/releases/latest`);
|
|
|
|
|
} else if (brewcodeResult?.updateAvailable) {
|
|
|
|
|
versionLines.push(`UPDATE brewcode ${brewcodeResult.local} → ${brewcodeResult.remote}: https://github.com/kochetkov-ma/claude-brewcode/releases/latest`);
|
2026-03-05 19:11:27 +00:00
|
|
|
}
|
2026-03-05 19:29:10 +00:00
|
|
|
|
|
|
|
|
if (claudeResult?.updateAvailable) {
|
|
|
|
|
versionLines.push(`UPDATE claude ${claudeResult.local} → ${claudeResult.remote}: claude update`);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
if (pluginRoot) versionLines.push(`check brewcode updates: https://github.com/kochetkov-ma/claude-brewcode/releases/latest`);
|
2026-03-05 19:11:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:43:48 +01:00
|
|
|
// reloadSkills not set: this hook toggles no skill files
|
2026-06-30 20:04:35 +01:00
|
|
|
// No additionalContext: version/plan info is user-facing (systemMessage) only;
|
|
|
|
|
// there is nothing model-facing to inject now that root/mode payloads are gone.
|
2026-06-27 10:57:42 +01:00
|
|
|
const permTag = permMode ? ` | perm: ${permMode}` : '';
|
2026-01-27 10:38:34 +00:00
|
|
|
output({
|
2026-06-30 20:04:35 +01:00
|
|
|
systemMessage: `brewcode: ${pluginRoot} | session: ${sessionShort}${permTag}${versionLines.length ? '\n' + versionLines.join('\n') : ''}`
|
2026-01-27 10:38:34 +00:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log('error', '[session-start]', `Error: ${error.message}`, cwd, session_id);
|
|
|
|
|
output({});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 15:18:22 +01:00
|
|
|
/** Run only when executed as a hook; importing (tests) must not consume stdin. Doubt -> run. */
|
|
|
|
|
function invokedDirectly() {
|
|
|
|
|
try {
|
|
|
|
|
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
|
|
|
} catch {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (invokedDirectly()) main();
|