mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: fold session manager blockers into one candidate
This commit is contained in:
@@ -10,8 +10,9 @@ const os = require('os');
|
||||
* Tries, in order:
|
||||
* 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks, or by user)
|
||||
* 2. Standard install location (~/.claude/) — when scripts exist there
|
||||
* 3. Plugin cache auto-detection — scans ~/.claude/plugins/cache/everything-claude-code/
|
||||
* 4. Fallback to ~/.claude/ (original behaviour)
|
||||
* 3. Exact legacy plugin roots under ~/.claude/plugins/
|
||||
* 4. Plugin cache auto-detection — scans ~/.claude/plugins/cache/everything-claude-code/
|
||||
* 5. Fallback to ~/.claude/ (original behaviour)
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.homeDir] Override home directory (for testing)
|
||||
@@ -38,6 +39,20 @@ function resolveEccRoot(options = {}) {
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
// Exact legacy plugin install locations. These preserve backwards
|
||||
// compatibility without scanning arbitrary plugin trees.
|
||||
const legacyPluginRoots = [
|
||||
path.join(claudeDir, 'plugins', 'everything-claude-code'),
|
||||
path.join(claudeDir, 'plugins', 'everything-claude-code@everything-claude-code'),
|
||||
path.join(claudeDir, 'plugins', 'marketplace', 'everything-claude-code')
|
||||
];
|
||||
|
||||
for (const candidate of legacyPluginRoots) {
|
||||
if (fs.existsSync(path.join(candidate, probe))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin cache — Claude Code stores marketplace plugins under
|
||||
// ~/.claude/plugins/cache/<plugin-name>/<org>/<version>/
|
||||
try {
|
||||
@@ -81,7 +96,7 @@ function resolveEccRoot(options = {}) {
|
||||
* const _r = <paste INLINE_RESOLVE>;
|
||||
* const sm = require(_r + '/scripts/lib/session-manager');
|
||||
*/
|
||||
const INLINE_RESOLVE = `(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b))for(var v of f.readdirSync(p.join(b,o))){var c=p.join(b,o,v);if(f.existsSync(p.join(c,q)))return c}}catch(x){}return d})()`;
|
||||
const INLINE_RESOLVE = `(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var l of [p.join(d,'plugins','everything-claude-code'),p.join(d,'plugins','everything-claude-code@everything-claude-code'),p.join(d,'plugins','marketplace','everything-claude-code')])if(f.existsSync(p.join(l,q)))return l;try{var b=p.join(d,'plugins','cache','everything-claude-code');for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}catch(x){}return d})()`;
|
||||
|
||||
module.exports = {
|
||||
resolveEccRoot,
|
||||
|
||||
3
scripts/lib/session-manager.d.ts
vendored
3
scripts/lib/session-manager.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Session Manager Library for Claude Code.
|
||||
* Provides CRUD operations for session files stored as markdown in ~/.claude/sessions/.
|
||||
* Provides CRUD operations for session files stored as markdown in
|
||||
* ~/.claude/session-data/ with legacy read compatibility for ~/.claude/sessions/.
|
||||
*/
|
||||
|
||||
/** Parsed metadata from a session filename */
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Session Manager Library for Claude Code
|
||||
* Provides core session CRUD operations for listing, loading, and managing sessions
|
||||
*
|
||||
* Sessions are stored as markdown files in ~/.claude/sessions/ with format:
|
||||
* Sessions are stored as markdown files in ~/.claude/session-data/ with
|
||||
* legacy read compatibility for ~/.claude/sessions/:
|
||||
* - YYYY-MM-DD-session.tmp (old format)
|
||||
* - YYYY-MM-DD-<short-id>-session.tmp (new format)
|
||||
*/
|
||||
@@ -12,6 +13,7 @@ const path = require('path');
|
||||
|
||||
const {
|
||||
getSessionsDir,
|
||||
getSessionSearchDirs,
|
||||
readFile,
|
||||
log
|
||||
} = require('./utils');
|
||||
@@ -30,6 +32,7 @@ const SESSION_FILENAME_REGEX = /^(\d{4}-\d{2}-\d{2})(?:-([a-zA-Z0-9_][a-zA-Z0-9_
|
||||
* @returns {object|null} Parsed metadata or null if invalid
|
||||
*/
|
||||
function parseSessionFilename(filename) {
|
||||
if (!filename || typeof filename !== 'string') return null;
|
||||
const match = filename.match(SESSION_FILENAME_REGEX);
|
||||
if (!match) return null;
|
||||
|
||||
@@ -66,6 +69,72 @@ function getSessionPath(filename) {
|
||||
return path.join(getSessionsDir(), filename);
|
||||
}
|
||||
|
||||
function getSessionCandidates(options = {}) {
|
||||
const {
|
||||
date = null,
|
||||
search = null
|
||||
} = options;
|
||||
|
||||
const candidates = [];
|
||||
|
||||
for (const sessionsDir of getSessionSearchDirs()) {
|
||||
if (!fs.existsSync(sessionsDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
|
||||
|
||||
const filename = entry.name;
|
||||
const metadata = parseSessionFilename(filename);
|
||||
|
||||
if (!metadata) continue;
|
||||
if (date && metadata.date !== date) continue;
|
||||
if (search && !metadata.shortId.includes(search)) continue;
|
||||
|
||||
const sessionPath = path.join(sessionsDir, filename);
|
||||
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(sessionPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
...metadata,
|
||||
sessionPath,
|
||||
hasContent: stats.size > 0,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
|
||||
const deduped = [];
|
||||
const seenFilenames = new Set();
|
||||
|
||||
for (const session of candidates) {
|
||||
if (seenFilenames.has(session.filename)) {
|
||||
continue;
|
||||
}
|
||||
seenFilenames.add(session.filename);
|
||||
deduped.push(session);
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse session markdown content
|
||||
* @param {string} sessionPath - Full path to session file
|
||||
@@ -228,58 +297,12 @@ function getAllSessions(options = {}) {
|
||||
const limitNum = Number(rawLimit);
|
||||
const limit = Number.isNaN(limitNum) ? 50 : Math.max(1, Math.floor(limitNum));
|
||||
|
||||
const sessionsDir = getSessionsDir();
|
||||
const sessions = getSessionCandidates({ date, search });
|
||||
|
||||
if (!fs.existsSync(sessionsDir)) {
|
||||
if (sessions.length === 0) {
|
||||
return { sessions: [], total: 0, offset, limit, hasMore: false };
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||
const sessions = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip non-files (only process .tmp files)
|
||||
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
|
||||
|
||||
const filename = entry.name;
|
||||
const metadata = parseSessionFilename(filename);
|
||||
|
||||
if (!metadata) continue;
|
||||
|
||||
// Apply date filter
|
||||
if (date && metadata.date !== date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply search filter (search in short ID)
|
||||
if (search && !metadata.shortId.includes(search)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sessionPath = path.join(sessionsDir, filename);
|
||||
|
||||
// Get file stats (wrapped in try-catch to handle TOCTOU race where
|
||||
// file is deleted between readdirSync and statSync)
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(sessionPath);
|
||||
} catch {
|
||||
continue; // File was deleted between readdir and stat
|
||||
}
|
||||
|
||||
sessions.push({
|
||||
...metadata,
|
||||
sessionPath,
|
||||
hasContent: stats.size > 0,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by modified time (newest first)
|
||||
sessions.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
|
||||
// Apply pagination
|
||||
const paginatedSessions = sessions.slice(offset, offset + limit);
|
||||
|
||||
@@ -299,21 +322,16 @@ function getAllSessions(options = {}) {
|
||||
* @returns {object|null} Session object or null if not found
|
||||
*/
|
||||
function getSessionById(sessionId, includeContent = false) {
|
||||
const sessionsDir = getSessionsDir();
|
||||
const sessions = getSessionCandidates();
|
||||
|
||||
if (!fs.existsSync(sessionsDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
|
||||
|
||||
const filename = entry.name;
|
||||
const metadata = parseSessionFilename(filename);
|
||||
|
||||
if (!metadata) continue;
|
||||
for (const session of sessions) {
|
||||
const filename = session.filename;
|
||||
const metadata = {
|
||||
filename: session.filename,
|
||||
shortId: session.shortId,
|
||||
date: session.date,
|
||||
datetime: session.datetime
|
||||
};
|
||||
|
||||
// Check if session ID matches (short ID or full filename without .tmp)
|
||||
const shortIdMatch = sessionId.length > 0 && metadata.shortId !== 'no-id' && metadata.shortId.startsWith(sessionId);
|
||||
@@ -324,30 +342,16 @@ function getSessionById(sessionId, includeContent = false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sessionPath = path.join(sessionsDir, filename);
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(sessionPath);
|
||||
} catch {
|
||||
return null; // File was deleted between readdir and stat
|
||||
}
|
||||
|
||||
const session = {
|
||||
...metadata,
|
||||
sessionPath,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
};
|
||||
const sessionRecord = { ...session };
|
||||
|
||||
if (includeContent) {
|
||||
session.content = getSessionContent(sessionPath);
|
||||
session.metadata = parseSessionMetadata(session.content);
|
||||
sessionRecord.content = getSessionContent(sessionRecord.sessionPath);
|
||||
sessionRecord.metadata = parseSessionMetadata(sessionRecord.content);
|
||||
// Pass pre-read content to avoid a redundant disk read
|
||||
session.stats = getSessionStats(session.content || '');
|
||||
sessionRecord.stats = getSessionStats(sessionRecord.content || '');
|
||||
}
|
||||
|
||||
return session;
|
||||
return sessionRecord;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
17
scripts/lib/utils.d.ts
vendored
17
scripts/lib/utils.d.ts
vendored
@@ -18,9 +18,15 @@ export function getHomeDir(): string;
|
||||
/** Get the Claude config directory (~/.claude) */
|
||||
export function getClaudeDir(): string;
|
||||
|
||||
/** Get the sessions directory (~/.claude/sessions) */
|
||||
/** Get the canonical ECC sessions directory (~/.claude/session-data) */
|
||||
export function getSessionsDir(): string;
|
||||
|
||||
/** Get the legacy Claude-managed sessions directory (~/.claude/sessions) */
|
||||
export function getLegacySessionsDir(): string;
|
||||
|
||||
/** Get session directories to search, with canonical storage first and legacy fallback second */
|
||||
export function getSessionSearchDirs(): string[];
|
||||
|
||||
/** Get the learned skills directory (~/.claude/skills/learned) */
|
||||
export function getLearnedSkillsDir(): string;
|
||||
|
||||
@@ -47,9 +53,16 @@ export function getDateTimeString(): string;
|
||||
|
||||
// --- Session/Project ---
|
||||
|
||||
/**
|
||||
* Sanitize a string for use as a session filename segment.
|
||||
* Replaces invalid characters, strips leading dots, and returns null when
|
||||
* nothing meaningful remains. Non-ASCII names are hashed for stability.
|
||||
*/
|
||||
export function sanitizeSessionId(raw: string | null | undefined): string | null;
|
||||
|
||||
/**
|
||||
* Get short session ID from CLAUDE_SESSION_ID environment variable.
|
||||
* Returns last 8 characters, falls back to project name then the provided fallback.
|
||||
* Returns last 8 characters, falls back to a sanitized project name then the provided fallback.
|
||||
*/
|
||||
export function getSessionIdShort(fallback?: string): string;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
|
||||
// Platform detection
|
||||
@@ -31,9 +32,23 @@ function getClaudeDir() {
|
||||
* Get the sessions directory
|
||||
*/
|
||||
function getSessionsDir() {
|
||||
return path.join(getClaudeDir(), 'session-data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the legacy sessions directory used by older ECC installs
|
||||
*/
|
||||
function getLegacySessionsDir() {
|
||||
return path.join(getClaudeDir(), 'sessions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all session directories to search, in canonical-first order
|
||||
*/
|
||||
function getSessionSearchDirs() {
|
||||
return Array.from(new Set([getSessionsDir(), getLegacySessionsDir()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the learned skills directory
|
||||
*/
|
||||
@@ -107,16 +122,50 @@ function getProjectName() {
|
||||
return path.basename(process.cwd()) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a string for use as a session filename segment.
|
||||
* Replaces invalid characters with hyphens, collapses runs, strips
|
||||
* leading/trailing hyphens, and removes leading dots so hidden-dir names
|
||||
* like ".claude" map cleanly to "claude".
|
||||
*
|
||||
* Pure non-ASCII inputs get a stable 8-char hash so distinct names do not
|
||||
* collapse to the same fallback session id. Mixed-script inputs retain their
|
||||
* ASCII part and gain a short hash suffix for disambiguation.
|
||||
*/
|
||||
function sanitizeSessionId(raw) {
|
||||
if (!raw || typeof raw !== 'string') return null;
|
||||
|
||||
const hasNonAscii = /[^\x00-\x7F]/.test(raw);
|
||||
const normalized = raw.replace(/^\.+/, '');
|
||||
const sanitized = normalized
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
if (sanitized.length > 0) {
|
||||
if (!hasNonAscii) return sanitized;
|
||||
|
||||
const suffix = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 6);
|
||||
return `${sanitized}-${suffix}`;
|
||||
}
|
||||
|
||||
const meaningful = normalized.replace(/[\s\p{P}]/gu, '');
|
||||
if (meaningful.length === 0) return null;
|
||||
|
||||
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short session ID from CLAUDE_SESSION_ID environment variable
|
||||
* Returns last 8 characters, falls back to project name then 'default'
|
||||
* Returns last 8 characters, falls back to a sanitized project name then 'default'.
|
||||
*/
|
||||
function getSessionIdShort(fallback = 'default') {
|
||||
const sessionId = process.env.CLAUDE_SESSION_ID;
|
||||
if (sessionId && sessionId.length > 0) {
|
||||
return sessionId.slice(-8);
|
||||
const sanitized = sanitizeSessionId(sessionId.slice(-8));
|
||||
if (sanitized) return sanitized;
|
||||
}
|
||||
return getProjectName() || fallback;
|
||||
return sanitizeSessionId(getProjectName()) || sanitizeSessionId(fallback) || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,6 +574,8 @@ module.exports = {
|
||||
getHomeDir,
|
||||
getClaudeDir,
|
||||
getSessionsDir,
|
||||
getLegacySessionsDir,
|
||||
getSessionSearchDirs,
|
||||
getLearnedSkillsDir,
|
||||
getTempDir,
|
||||
ensureDir,
|
||||
@@ -535,6 +586,7 @@ module.exports = {
|
||||
getDateTimeString,
|
||||
|
||||
// Session/Project
|
||||
sanitizeSessionId,
|
||||
getSessionIdShort,
|
||||
getGitRepoName,
|
||||
getProjectName,
|
||||
|
||||
Reference in New Issue
Block a user