mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
- Fix read-after-write in session-start.mjs: read prevSession BEFORE
overwriting current-session.json so unsaved-session detection fires
- Fix shell injection in resume.mjs: replace execSync shell string with
fs.existsSync for directory existence check
- Fix shell injection in shared.mjs gitSummary: replace nested \$(git ...)
subshell with a separate runGit() call to get rev count
- Fix displayName never shown: render functions now use ctx.displayName
?? ctx.name so user-supplied names show instead of the slug
- Fix renderListTable: uses context.displayName ?? entry.name
- Fix init.mjs: use path.basename() instead of cwd.split('/').pop()
- Fix save.mjs confirmation: show original name, not contextDir slug
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
221 lines
8.5 KiB
JavaScript
221 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ck — Context Keeper v2
|
|
* session-start.mjs — inject compact project context on session start.
|
|
*
|
|
* Injects ~100 tokens (not ~2,500 like v1).
|
|
* SKILL.md is injected separately (still small at ~50 lines).
|
|
*
|
|
* Features:
|
|
* - Compact 5-line summary for registered projects
|
|
* - Unsaved session detection → "Last session wasn't saved. Run /ck:save."
|
|
* - Git activity since last session
|
|
* - Goal mismatch detection vs CLAUDE.md
|
|
* - Mini portfolio for unregistered directories
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { homedir } from 'os';
|
|
import { execSync } from 'child_process';
|
|
|
|
const CK_HOME = resolve(homedir(), '.claude', 'ck');
|
|
const PROJECTS_FILE = resolve(CK_HOME, 'projects.json');
|
|
const CURRENT_SESSION = resolve(CK_HOME, 'current-session.json');
|
|
const SKILL_FILE = resolve(homedir(), '.claude', 'skills', 'ck', 'SKILL.md');
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
function readJson(p) {
|
|
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
|
|
}
|
|
|
|
function daysAgo(dateStr) {
|
|
if (!dateStr) return 'unknown';
|
|
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
|
|
if (diff === 0) return 'today';
|
|
if (diff === 1) return '1 day ago';
|
|
return `${diff} days ago`;
|
|
}
|
|
|
|
function stalenessIcon(dateStr) {
|
|
if (!dateStr) return '○';
|
|
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
|
|
return diff < 1 ? '●' : diff <= 5 ? '◐' : '○';
|
|
}
|
|
|
|
function gitLogSince(projectPath, sinceDate) {
|
|
if (!sinceDate || !existsSync(resolve(projectPath, '.git'))) return null;
|
|
try {
|
|
const result = execSync(`git -C "${projectPath}" log --oneline --since="${sinceDate}"`, {
|
|
timeout: 3000, stdio: 'pipe', encoding: 'utf8',
|
|
}).trim();
|
|
const commits = result.split('\n').filter(Boolean).length;
|
|
return commits > 0 ? `${commits} commit${commits !== 1 ? 's' : ''} since last session` : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
function extractClaudeMdGoal(projectPath) {
|
|
const p = resolve(projectPath, 'CLAUDE.md');
|
|
if (!existsSync(p)) return null;
|
|
try {
|
|
const md = readFileSync(p, 'utf8');
|
|
const m = md.match(/## Current Goal\n([\s\S]*?)(?=\n## |$)/);
|
|
return m ? m[1].trim().split('\n')[0].trim() : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ─── Session ID from stdin ────────────────────────────────────────────────────
|
|
|
|
function readSessionId() {
|
|
try {
|
|
const raw = readFileSync(0, 'utf8');
|
|
return JSON.parse(raw).session_id || null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
function main() {
|
|
const cwd = process.env.PWD || process.cwd();
|
|
const sessionId = readSessionId();
|
|
|
|
// Load skill (always inject — now only ~50 lines)
|
|
const skill = existsSync(SKILL_FILE) ? readFileSync(SKILL_FILE, 'utf8') : '';
|
|
|
|
const projects = readJson(PROJECTS_FILE) || {};
|
|
const entry = projects[cwd];
|
|
|
|
// Read previous session BEFORE overwriting current-session.json
|
|
const prevSession = readJson(CURRENT_SESSION);
|
|
|
|
// Write current-session.json
|
|
try {
|
|
writeFileSync(CURRENT_SESSION, JSON.stringify({
|
|
sessionId,
|
|
projectPath: cwd,
|
|
projectName: entry?.name || null,
|
|
startedAt: new Date().toISOString(),
|
|
}, null, 2), 'utf8');
|
|
} catch { /* non-fatal */ }
|
|
|
|
const parts = [];
|
|
if (skill) parts.push(skill);
|
|
|
|
// ── REGISTERED PROJECT ────────────────────────────────────────────────────
|
|
if (entry?.contextDir) {
|
|
const contextFile = resolve(CK_HOME, 'contexts', entry.contextDir, 'context.json');
|
|
const context = readJson(contextFile);
|
|
|
|
if (context) {
|
|
const latest = context.sessions?.[context.sessions.length - 1] || {};
|
|
const sessionDate = latest.date || context.createdAt;
|
|
const sessionCount = context.sessions?.length || 0;
|
|
const displayName = context.displayName ?? context.name;
|
|
|
|
// ── Compact summary block (~100 tokens) ──────────────────────────────
|
|
const summaryLines = [
|
|
`ck: ${displayName} | ${daysAgo(sessionDate)} | ${sessionCount} session${sessionCount !== 1 ? 's' : ''}`,
|
|
`Goal: ${context.goal || '—'}`,
|
|
latest.leftOff ? `Left off: ${latest.leftOff.split('\n')[0]}` : null,
|
|
latest.nextSteps?.length ? `Next: ${latest.nextSteps.slice(0, 2).join(' · ')}` : null,
|
|
].filter(Boolean);
|
|
|
|
// ── Unsaved session detection ─────────────────────────────────────────
|
|
if (prevSession?.sessionId && prevSession.sessionId !== sessionId) {
|
|
// Check if previous session ID exists in sessions array
|
|
const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId);
|
|
if (!alreadySaved) {
|
|
summaryLines.push(`⚠ Last session wasn't saved — run /ck:save to capture it`);
|
|
}
|
|
}
|
|
|
|
// ── Git activity ──────────────────────────────────────────────────────
|
|
const gitLine = gitLogSince(cwd, sessionDate);
|
|
if (gitLine) summaryLines.push(`Git: ${gitLine}`);
|
|
|
|
// ── Goal mismatch detection ───────────────────────────────────────────
|
|
const claudeMdGoal = extractClaudeMdGoal(cwd);
|
|
if (claudeMdGoal && context.goal &&
|
|
claudeMdGoal.toLowerCase().trim() !== context.goal.toLowerCase().trim()) {
|
|
summaryLines.push(`⚠ Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`);
|
|
summaryLines.push(` Run /ck:save with updated goal to sync`);
|
|
}
|
|
|
|
parts.push([
|
|
`---`,
|
|
`## ck: ${displayName}`,
|
|
``,
|
|
summaryLines.join('\n'),
|
|
].join('\n'));
|
|
|
|
// Instruct Claude to display compact briefing at session start
|
|
parts.push([
|
|
`---`,
|
|
`## ck: SESSION START`,
|
|
``,
|
|
`IMPORTANT: Display the following as your FIRST message, verbatim:`,
|
|
``,
|
|
'```',
|
|
summaryLines.join('\n'),
|
|
'```',
|
|
``,
|
|
`After the block, add one line: "Ready — what are we working on?"`,
|
|
`If you see ⚠ warnings above, mention them briefly after the block.`,
|
|
].join('\n'));
|
|
|
|
return parts;
|
|
}
|
|
}
|
|
|
|
// ── NOT IN A REGISTERED PROJECT ────────────────────────────────────────────
|
|
const entries = Object.entries(projects);
|
|
if (entries.length === 0) return parts;
|
|
|
|
// Load and sort by most recent
|
|
const recent = entries
|
|
.map(([path, info]) => {
|
|
const ctx = readJson(resolve(CK_HOME, 'contexts', info.contextDir, 'context.json'));
|
|
const latest = ctx?.sessions?.[ctx.sessions.length - 1] || {};
|
|
return { name: info.name, path, lastDate: latest.date || '', summary: latest.summary || '—', ctx };
|
|
})
|
|
.sort((a, b) => (b.lastDate > a.lastDate ? 1 : -1))
|
|
.slice(0, 3);
|
|
|
|
const miniRows = recent.map(p => {
|
|
const icon = stalenessIcon(p.lastDate);
|
|
const when = daysAgo(p.lastDate);
|
|
const name = p.name.padEnd(16).slice(0, 16);
|
|
const whenStr = when.padEnd(12).slice(0, 12);
|
|
const summary = p.summary.slice(0, 32);
|
|
return ` ${name} ${icon} ${whenStr} ${summary}`;
|
|
});
|
|
|
|
const miniStatus = [
|
|
`ck — recent projects:`,
|
|
` ${'PROJECT'.padEnd(16)} S ${'LAST SEEN'.padEnd(12)} LAST SESSION`,
|
|
` ${'─'.repeat(68)}`,
|
|
...miniRows,
|
|
``,
|
|
`Run /ck:list · /ck:resume <name> · /ck:init to register this folder`,
|
|
].join('\n');
|
|
|
|
parts.push([
|
|
`---`,
|
|
`## ck: SESSION START`,
|
|
``,
|
|
`IMPORTANT: Display the following as your FIRST message, verbatim:`,
|
|
``,
|
|
'```',
|
|
miniStatus,
|
|
'```',
|
|
].join('\n'));
|
|
|
|
return parts;
|
|
}
|
|
|
|
const parts = main();
|
|
if (parts.length > 0) {
|
|
console.log(JSON.stringify({ additionalContext: parts.join('\n\n---\n\n') }));
|
|
}
|