mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-07 15:13:08 +08:00
Add a new /sessions command to manage Claude Code session history with alias support for quick access to previous sessions. Features: - List sessions with pagination and filtering (by date, ID) - Load and view session content and metadata - Create memorable aliases for sessions - Remove aliases - Display session statistics (lines, items, size) - List all aliases New libraries: - scripts/lib/session-manager.js - Core session CRUD operations - scripts/lib/session-aliases.js - Alias management with atomic saves New command: - commands/sessions.md - Complete command with embedded scripts Modified: - scripts/lib/utils.js - Add getAliasesPath() export - scripts/hooks/session-start.js - Show available aliases on session start Session format support: - Old: YYYY-MM-DD-session.tmp - New: YYYY-MM-DD-<short-id>-session.tmp Aliases are stored in ~/.claude/session-aliases.json with Windows- compatible atomic writes and backup support. Co-authored-by: 王志坚 <wangzhijian10@bgyfw.com> Co-authored-by: Claude <noreply@anthropic.com>
434 lines
10 KiB
JavaScript
434 lines
10 KiB
JavaScript
/**
|
|
* Session Aliases Library for Claude Code
|
|
* Manages session aliases stored in ~/.claude/session-aliases.json
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const {
|
|
getClaudeDir,
|
|
ensureDir,
|
|
readFile,
|
|
writeFile,
|
|
log
|
|
} = require('./utils');
|
|
|
|
// Aliases file path
|
|
function getAliasesPath() {
|
|
return path.join(getClaudeDir(), 'session-aliases.json');
|
|
}
|
|
|
|
// Current alias storage format version
|
|
const ALIAS_VERSION = '1.0';
|
|
|
|
/**
|
|
* Default aliases file structure
|
|
*/
|
|
function getDefaultAliases() {
|
|
return {
|
|
version: ALIAS_VERSION,
|
|
aliases: {},
|
|
metadata: {
|
|
totalCount: 0,
|
|
lastUpdated: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load aliases from file
|
|
* @returns {object} Aliases object
|
|
*/
|
|
function loadAliases() {
|
|
const aliasesPath = getAliasesPath();
|
|
|
|
if (!fs.existsSync(aliasesPath)) {
|
|
return getDefaultAliases();
|
|
}
|
|
|
|
const content = readFile(aliasesPath);
|
|
if (!content) {
|
|
return getDefaultAliases();
|
|
}
|
|
|
|
try {
|
|
const data = JSON.parse(content);
|
|
|
|
// Validate structure
|
|
if (!data.aliases || typeof data.aliases !== 'object') {
|
|
log('[Aliases] Invalid aliases file structure, resetting');
|
|
return getDefaultAliases();
|
|
}
|
|
|
|
// Ensure version field
|
|
if (!data.version) {
|
|
data.version = ALIAS_VERSION;
|
|
}
|
|
|
|
// Ensure metadata
|
|
if (!data.metadata) {
|
|
data.metadata = {
|
|
totalCount: Object.keys(data.aliases).length,
|
|
lastUpdated: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
return data;
|
|
} catch (err) {
|
|
log(`[Aliases] Error parsing aliases file: ${err.message}`);
|
|
return getDefaultAliases();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save aliases to file with atomic write
|
|
* @param {object} aliases - Aliases object to save
|
|
* @returns {boolean} Success status
|
|
*/
|
|
function saveAliases(aliases) {
|
|
const aliasesPath = getAliasesPath();
|
|
const tempPath = aliasesPath + '.tmp';
|
|
const backupPath = aliasesPath + '.bak';
|
|
|
|
try {
|
|
// Update metadata
|
|
aliases.metadata = {
|
|
totalCount: Object.keys(aliases.aliases).length,
|
|
lastUpdated: new Date().toISOString()
|
|
};
|
|
|
|
const content = JSON.stringify(aliases, null, 2);
|
|
|
|
// Ensure directory exists
|
|
ensureDir(path.dirname(aliasesPath));
|
|
|
|
// Create backup if file exists
|
|
if (fs.existsSync(aliasesPath)) {
|
|
fs.copyFileSync(aliasesPath, backupPath);
|
|
}
|
|
|
|
// Atomic write: write to temp file, then rename
|
|
fs.writeFileSync(tempPath, content, 'utf8');
|
|
|
|
// On Windows, we need to delete the target file before renaming
|
|
if (fs.existsSync(aliasesPath)) {
|
|
fs.unlinkSync(aliasesPath);
|
|
}
|
|
fs.renameSync(tempPath, aliasesPath);
|
|
|
|
// Remove backup on success
|
|
if (fs.existsSync(backupPath)) {
|
|
fs.unlinkSync(backupPath);
|
|
}
|
|
|
|
return true;
|
|
} catch (err) {
|
|
log(`[Aliases] Error saving aliases: ${err.message}`);
|
|
|
|
// Restore from backup if exists
|
|
if (fs.existsSync(backupPath)) {
|
|
try {
|
|
fs.copyFileSync(backupPath, aliasesPath);
|
|
log('[Aliases] Restored from backup');
|
|
} catch (restoreErr) {
|
|
log(`[Aliases] Failed to restore backup: ${restoreErr.message}`);
|
|
}
|
|
}
|
|
|
|
// Clean up temp file
|
|
if (fs.existsSync(tempPath)) {
|
|
fs.unlinkSync(tempPath);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve an alias to get session path
|
|
* @param {string} alias - Alias name to resolve
|
|
* @returns {object|null} Alias data or null if not found
|
|
*/
|
|
function resolveAlias(alias) {
|
|
// Validate alias name (alphanumeric, dash, underscore)
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
|
|
return null;
|
|
}
|
|
|
|
const data = loadAliases();
|
|
const aliasData = data.aliases[alias];
|
|
|
|
if (!aliasData) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
alias,
|
|
sessionPath: aliasData.sessionPath,
|
|
createdAt: aliasData.createdAt,
|
|
title: aliasData.title || null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set or update an alias for a session
|
|
* @param {string} alias - Alias name (alphanumeric, dash, underscore)
|
|
* @param {string} sessionPath - Session directory path
|
|
* @param {string} title - Optional title for the alias
|
|
* @returns {object} Result with success status and message
|
|
*/
|
|
function setAlias(alias, sessionPath, title = null) {
|
|
// Validate alias name
|
|
if (!alias || alias.length === 0) {
|
|
return { success: false, error: 'Alias name cannot be empty' };
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
|
|
return { success: false, error: 'Alias name must contain only letters, numbers, dashes, and underscores' };
|
|
}
|
|
|
|
// Reserved alias names
|
|
const reserved = ['list', 'help', 'remove', 'delete', 'create', 'set'];
|
|
if (reserved.includes(alias.toLowerCase())) {
|
|
return { success: false, error: `'${alias}' is a reserved alias name` };
|
|
}
|
|
|
|
const data = loadAliases();
|
|
const existing = data.aliases[alias];
|
|
const isNew = !existing;
|
|
|
|
data.aliases[alias] = {
|
|
sessionPath,
|
|
createdAt: existing ? existing.createdAt : new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
title: title || null
|
|
};
|
|
|
|
if (saveAliases(data)) {
|
|
return {
|
|
success: true,
|
|
isNew,
|
|
alias,
|
|
sessionPath,
|
|
title: data.aliases[alias].title
|
|
};
|
|
}
|
|
|
|
return { success: false, error: 'Failed to save alias' };
|
|
}
|
|
|
|
/**
|
|
* List all aliases
|
|
* @param {object} options - Options object
|
|
* @param {string} options.search - Filter aliases by name (partial match)
|
|
* @param {number} options.limit - Maximum number of aliases to return
|
|
* @returns {Array} Array of alias objects
|
|
*/
|
|
function listAliases(options = {}) {
|
|
const { search = null, limit = null } = options;
|
|
const data = loadAliases();
|
|
|
|
let aliases = Object.entries(data.aliases).map(([name, info]) => ({
|
|
name,
|
|
sessionPath: info.sessionPath,
|
|
createdAt: info.createdAt,
|
|
updatedAt: info.updatedAt,
|
|
title: info.title
|
|
}));
|
|
|
|
// Sort by updated time (newest first)
|
|
aliases.sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt));
|
|
|
|
// Apply search filter
|
|
if (search) {
|
|
const searchLower = search.toLowerCase();
|
|
aliases = aliases.filter(a =>
|
|
a.name.toLowerCase().includes(searchLower) ||
|
|
(a.title && a.title.toLowerCase().includes(searchLower))
|
|
);
|
|
}
|
|
|
|
// Apply limit
|
|
if (limit && limit > 0) {
|
|
aliases = aliases.slice(0, limit);
|
|
}
|
|
|
|
return aliases;
|
|
}
|
|
|
|
/**
|
|
* Delete an alias
|
|
* @param {string} alias - Alias name to delete
|
|
* @returns {object} Result with success status
|
|
*/
|
|
function deleteAlias(alias) {
|
|
const data = loadAliases();
|
|
|
|
if (!data.aliases[alias]) {
|
|
return { success: false, error: `Alias '${alias}' not found` };
|
|
}
|
|
|
|
const deleted = data.aliases[alias];
|
|
delete data.aliases[alias];
|
|
|
|
if (saveAliases(data)) {
|
|
return {
|
|
success: true,
|
|
alias,
|
|
deletedSessionPath: deleted.sessionPath
|
|
};
|
|
}
|
|
|
|
return { success: false, error: 'Failed to delete alias' };
|
|
}
|
|
|
|
/**
|
|
* Rename an alias
|
|
* @param {string} oldAlias - Current alias name
|
|
* @param {string} newAlias - New alias name
|
|
* @returns {object} Result with success status
|
|
*/
|
|
function renameAlias(oldAlias, newAlias) {
|
|
const data = loadAliases();
|
|
|
|
if (!data.aliases[oldAlias]) {
|
|
return { success: false, error: `Alias '${oldAlias}' not found` };
|
|
}
|
|
|
|
if (data.aliases[newAlias]) {
|
|
return { success: false, error: `Alias '${newAlias}' already exists` };
|
|
}
|
|
|
|
// Validate new alias name
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(newAlias)) {
|
|
return { success: false, error: 'New alias name must contain only letters, numbers, dashes, and underscores' };
|
|
}
|
|
|
|
const aliasData = data.aliases[oldAlias];
|
|
delete data.aliases[oldAlias];
|
|
|
|
aliasData.updatedAt = new Date().toISOString();
|
|
data.aliases[newAlias] = aliasData;
|
|
|
|
if (saveAliases(data)) {
|
|
return {
|
|
success: true,
|
|
oldAlias,
|
|
newAlias,
|
|
sessionPath: aliasData.sessionPath
|
|
};
|
|
}
|
|
|
|
// Restore old alias on failure
|
|
data.aliases[oldAlias] = aliasData;
|
|
return { success: false, error: 'Failed to rename alias' };
|
|
}
|
|
|
|
/**
|
|
* Get session path by alias (convenience function)
|
|
* @param {string} aliasOrId - Alias name or session ID
|
|
* @returns {string|null} Session path or null if not found
|
|
*/
|
|
function resolveSessionAlias(aliasOrId) {
|
|
// First try to resolve as alias
|
|
const resolved = resolveAlias(aliasOrId);
|
|
if (resolved) {
|
|
return resolved.sessionPath;
|
|
}
|
|
|
|
// If not an alias, return as-is (might be a session path)
|
|
return aliasOrId;
|
|
}
|
|
|
|
/**
|
|
* Update alias title
|
|
* @param {string} alias - Alias name
|
|
* @param {string} title - New title
|
|
* @returns {object} Result with success status
|
|
*/
|
|
function updateAliasTitle(alias, title) {
|
|
const data = loadAliases();
|
|
|
|
if (!data.aliases[alias]) {
|
|
return { success: false, error: `Alias '${alias}' not found` };
|
|
}
|
|
|
|
data.aliases[alias].title = title;
|
|
data.aliases[alias].updatedAt = new Date().toISOString();
|
|
|
|
if (saveAliases(data)) {
|
|
return {
|
|
success: true,
|
|
alias,
|
|
title
|
|
};
|
|
}
|
|
|
|
return { success: false, error: 'Failed to update alias title' };
|
|
}
|
|
|
|
/**
|
|
* Get all aliases for a specific session
|
|
* @param {string} sessionPath - Session path to find aliases for
|
|
* @returns {Array} Array of alias names
|
|
*/
|
|
function getAliasesForSession(sessionPath) {
|
|
const data = loadAliases();
|
|
const aliases = [];
|
|
|
|
for (const [name, info] of Object.entries(data.aliases)) {
|
|
if (info.sessionPath === sessionPath) {
|
|
aliases.push({
|
|
name,
|
|
createdAt: info.createdAt,
|
|
title: info.title
|
|
});
|
|
}
|
|
}
|
|
|
|
return aliases;
|
|
}
|
|
|
|
/**
|
|
* Clean up aliases for non-existent sessions
|
|
* @param {Function} sessionExists - Function to check if session exists
|
|
* @returns {object} Cleanup result
|
|
*/
|
|
function cleanupAliases(sessionExists) {
|
|
const data = loadAliases();
|
|
const removed = [];
|
|
|
|
for (const [name, info] of Object.entries(data.aliases)) {
|
|
if (!sessionExists(info.sessionPath)) {
|
|
removed.push({ name, sessionPath: info.sessionPath });
|
|
delete data.aliases[name];
|
|
}
|
|
}
|
|
|
|
if (removed.length > 0) {
|
|
saveAliases(data);
|
|
}
|
|
|
|
return {
|
|
totalChecked: Object.keys(data.aliases).length + removed.length,
|
|
removed: removed.length,
|
|
removedAliases: removed
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
getAliasesPath,
|
|
loadAliases,
|
|
saveAliases,
|
|
resolveAlias,
|
|
setAlias,
|
|
listAliases,
|
|
deleteAlias,
|
|
renameAlias,
|
|
resolveSessionAlias,
|
|
updateAliasTitle,
|
|
getAliasesForSession,
|
|
cleanupAliases
|
|
};
|