Release v3.9.11
Published from npm package build Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Vendored
+213
-183
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
|
|||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||||
import { getProjectName } from './shared/path-resolver.js';
|
import { getProjectName } from './shared/path-resolver.js';
|
||||||
|
import { initializeDatabase, getActiveStreamingSessionsForProject } from './shared/hook-helpers.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
|
||||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
function debugLog(message, data = {}) {
|
||||||
@@ -61,15 +61,18 @@ process.stdin.on('end', async () => {
|
|||||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load SDK session info
|
// Load SDK session info from database
|
||||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
const db = initializeDatabase();
|
||||||
if (!fs.existsSync(sessionFile)) {
|
|
||||||
|
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||||
|
if (!sessions || sessions.length === 0) {
|
||||||
debugLog('PostToolUse: No streaming session found', { project });
|
debugLog('PostToolUse: No streaming session found', { project });
|
||||||
|
db.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
const sessionData = sessions[0];
|
||||||
const sdkSessionId = sessionData.sdkSessionId;
|
const sdkSessionId = sessionData.sdk_session_id;
|
||||||
|
|
||||||
// Convert tool response to string
|
// Convert tool response to string
|
||||||
const toolResponseStr = typeof tool_response === 'string'
|
const toolResponseStr = typeof tool_response === 'string'
|
||||||
@@ -135,6 +138,9 @@ process.stdin.on('end', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
|
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook Helper Functions
|
* Hook Helper Functions
|
||||||
*
|
*
|
||||||
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
||||||
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
||||||
*/
|
*/
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -235,3 +238,193 @@ export function debugLog(message, data = {}) {
|
|||||||
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DATABASE HELPERS (inline SQL to avoid 'claude-mem' import issues)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the claude-mem data directory path
|
||||||
|
*/
|
||||||
|
function getDataDirectory() {
|
||||||
|
return join(os.homedir(), '.claude-mem');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the database connection
|
||||||
|
*/
|
||||||
|
function getDatabase() {
|
||||||
|
const dataDir = getDataDirectory();
|
||||||
|
const dbPath = join(dataDir, 'claude-mem.db');
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
// Apply optimized SQLite settings
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
db.pragma('synchronous = NORMAL');
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
db.pragma('temp_store = memory');
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the streaming_sessions table exists
|
||||||
|
*/
|
||||||
|
function ensureStreamingSessionsTable(db) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
claude_session_id TEXT UNIQUE NOT NULL,
|
||||||
|
sdk_session_id TEXT,
|
||||||
|
project TEXT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
subtitle TEXT,
|
||||||
|
user_prompt TEXT,
|
||||||
|
started_at TEXT NOT NULL,
|
||||||
|
started_at_epoch INTEGER NOT NULL,
|
||||||
|
updated_at TEXT,
|
||||||
|
updated_at_epoch INTEGER,
|
||||||
|
completed_at TEXT,
|
||||||
|
completed_at_epoch INTEGER,
|
||||||
|
status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'failed'))
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indices if they don't exist
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id
|
||||||
|
ON streaming_sessions(claude_session_id)
|
||||||
|
`);
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id
|
||||||
|
ON streaming_sessions(sdk_session_id)
|
||||||
|
`);
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project_status
|
||||||
|
ON streaming_sessions(project, status)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new streaming session record
|
||||||
|
*/
|
||||||
|
export function createStreamingSession(db, { claude_session_id, project, user_prompt, started_at }) {
|
||||||
|
ensureStreamingSessionsTable(db);
|
||||||
|
|
||||||
|
const timestamp = started_at || new Date().toISOString();
|
||||||
|
const epoch = new Date(timestamp).getTime();
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO streaming_sessions (
|
||||||
|
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
|
||||||
|
) VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
|
`);
|
||||||
|
|
||||||
|
const info = stmt.run(claude_session_id, project, user_prompt || null, timestamp, epoch);
|
||||||
|
|
||||||
|
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(info.lastInsertRowid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a streaming session by internal ID
|
||||||
|
*/
|
||||||
|
export function updateStreamingSession(db, id, updates) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const epoch = Date.now();
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (updates.sdk_session_id !== undefined) {
|
||||||
|
parts.push('sdk_session_id = ?');
|
||||||
|
values.push(updates.sdk_session_id);
|
||||||
|
}
|
||||||
|
if (updates.title !== undefined) {
|
||||||
|
parts.push('title = ?');
|
||||||
|
values.push(updates.title);
|
||||||
|
}
|
||||||
|
if (updates.subtitle !== undefined) {
|
||||||
|
parts.push('subtitle = ?');
|
||||||
|
values.push(updates.subtitle);
|
||||||
|
}
|
||||||
|
if (updates.status !== undefined) {
|
||||||
|
parts.push('status = ?');
|
||||||
|
values.push(updates.status);
|
||||||
|
}
|
||||||
|
if (updates.completed_at !== undefined) {
|
||||||
|
const completedTimestamp = typeof updates.completed_at === 'string'
|
||||||
|
? updates.completed_at
|
||||||
|
: new Date(updates.completed_at).toISOString();
|
||||||
|
const completedEpoch = new Date(completedTimestamp).getTime();
|
||||||
|
parts.push('completed_at = ?', 'completed_at_epoch = ?');
|
||||||
|
values.push(completedTimestamp, completedEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the updated_at timestamp
|
||||||
|
parts.push('updated_at = ?', 'updated_at_epoch = ?');
|
||||||
|
values.push(timestamp, epoch);
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
UPDATE streaming_sessions
|
||||||
|
SET ${parts.join(', ')}
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(...values);
|
||||||
|
|
||||||
|
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active streaming sessions for a project
|
||||||
|
*/
|
||||||
|
export function getActiveStreamingSessionsForProject(db, project) {
|
||||||
|
ensureStreamingSessionsTable(db);
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
SELECT * FROM streaming_sessions
|
||||||
|
WHERE project = ? AND status = 'active'
|
||||||
|
ORDER BY started_at_epoch DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return stmt.all(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a session as completed
|
||||||
|
*/
|
||||||
|
export function markStreamingSessionCompleted(db, id) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const epoch = Date.now();
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
UPDATE streaming_sessions
|
||||||
|
SET status = ?,
|
||||||
|
completed_at = ?,
|
||||||
|
completed_at_epoch = ?,
|
||||||
|
updated_at = ?,
|
||||||
|
updated_at_epoch = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run('completed', timestamp, epoch, timestamp, epoch, id);
|
||||||
|
|
||||||
|
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database with migrations and return connection
|
||||||
|
*/
|
||||||
|
export function initializeDatabase() {
|
||||||
|
const db = getDatabase();
|
||||||
|
ensureStreamingSessionsTable(db);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|||||||
+23
-10
@@ -12,8 +12,8 @@ import fs from 'fs';
|
|||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||||
import { getProjectName } from './shared/path-resolver.js';
|
import { getProjectName } from './shared/path-resolver.js';
|
||||||
|
import { initializeDatabase, getActiveStreamingSessionsForProject, markStreamingSessionCompleted } from './shared/hook-helpers.js';
|
||||||
|
|
||||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
|
||||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
function debugLog(message, data = {}) {
|
||||||
@@ -50,20 +50,31 @@ process.stdin.on('end', async () => {
|
|||||||
const { cwd } = payload;
|
const { cwd } = payload;
|
||||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||||
|
|
||||||
|
// Immediately clear activity flag for UI indicator
|
||||||
|
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: false, timestamp: Date.now() }));
|
||||||
|
} catch (error) {
|
||||||
|
// Silent fail - non-critical
|
||||||
|
}
|
||||||
|
|
||||||
// Return immediately with async mode
|
// Return immediately with async mode
|
||||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load SDK session info
|
// Load SDK session info from database
|
||||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
const db = initializeDatabase();
|
||||||
if (!fs.existsSync(sessionFile)) {
|
|
||||||
|
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||||
|
if (!sessions || sessions.length === 0) {
|
||||||
debugLog('Stop: No streaming session found', { project });
|
debugLog('Stop: No streaming session found', { project });
|
||||||
|
db.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
const sessionData = sessions[0];
|
||||||
const sdkSessionId = sessionData.sdkSessionId;
|
const sdkSessionId = sessionData.sdk_session_id;
|
||||||
const claudeSessionId = sessionData.claudeSessionId;
|
const claudeSessionId = sessionData.claude_session_id;
|
||||||
|
|
||||||
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
||||||
|
|
||||||
@@ -108,10 +119,12 @@ process.stdin.on('end', async () => {
|
|||||||
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up session file
|
// Mark session as completed in database
|
||||||
fs.unlinkSync(sessionFile);
|
markStreamingSessionCompleted(db, sessionData.id);
|
||||||
debugLog('Stop: Session ended and cleaned up', { project });
|
debugLog('Stop: Session ended and marked complete', { project, sessionId: sessionData.id });
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog('Stop: Error ending session', { error: error.message });
|
debugLog('Stop: Error ending session', { error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
|
|||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||||
import { getProjectName } from './shared/path-resolver.js';
|
import { getProjectName } from './shared/path-resolver.js';
|
||||||
|
import { initializeDatabase, createStreamingSession, updateStreamingSession } from './shared/hook-helpers.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
|
||||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
function debugLog(message, data = {}) {
|
||||||
@@ -60,6 +60,14 @@ process.stdin.on('end', async () => {
|
|||||||
|
|
||||||
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
|
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
|
||||||
|
|
||||||
|
// Immediately signal activity start for UI indicator
|
||||||
|
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: true, project, timestamp: Date.now() }));
|
||||||
|
} catch (error) {
|
||||||
|
// Silent fail - non-critical
|
||||||
|
}
|
||||||
|
|
||||||
// Generate title and subtitle non-blocking
|
// Generate title and subtitle non-blocking
|
||||||
if (prompt && session_id && project) {
|
if (prompt && session_id && project) {
|
||||||
import('child_process').then(({ spawn }) => {
|
import('child_process').then(({ spawn }) => {
|
||||||
@@ -80,6 +88,22 @@ process.stdin.on('end', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Initialize database and create session record FIRST
|
||||||
|
const db = initializeDatabase();
|
||||||
|
|
||||||
|
// Create session record immediately - this gives us a tracking ID
|
||||||
|
const sessionRecord = createStreamingSession(db, {
|
||||||
|
claude_session_id: session_id,
|
||||||
|
project,
|
||||||
|
user_prompt: prompt,
|
||||||
|
started_at: timestamp
|
||||||
|
});
|
||||||
|
|
||||||
|
debugLog('UserPromptSubmit: Created session record', {
|
||||||
|
internalId: sessionRecord.id,
|
||||||
|
claudeSessionId: session_id
|
||||||
|
});
|
||||||
|
|
||||||
// Build system prompt using centralized config
|
// Build system prompt using centralized config
|
||||||
const systemPrompt = renderSystemPrompt({
|
const systemPrompt = renderSystemPrompt({
|
||||||
project,
|
project,
|
||||||
@@ -110,19 +134,19 @@ process.stdin.on('end', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sdkSessionId) {
|
if (sdkSessionId) {
|
||||||
// Save session info for other hooks
|
// Update session record with SDK session ID
|
||||||
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
updateStreamingSession(db, sessionRecord.id, {
|
||||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
sdk_session_id: sdkSessionId
|
||||||
fs.writeFileSync(sessionFile, JSON.stringify({
|
});
|
||||||
sdkSessionId,
|
|
||||||
claudeSessionId: session_id,
|
|
||||||
project,
|
|
||||||
startedAt: timestamp,
|
|
||||||
date
|
|
||||||
}, null, 2));
|
|
||||||
|
|
||||||
debugLog('UserPromptSubmit: SDK session started', { sdkSessionId, sessionFile });
|
debugLog('UserPromptSubmit: SDK session started', {
|
||||||
|
internalId: sessionRecord.id,
|
||||||
|
sdkSessionId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
|
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mem",
|
"name": "claude-mem",
|
||||||
"version": "3.9.10",
|
"version": "3.9.11",
|
||||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
+8
-3
@@ -224,9 +224,8 @@ program
|
|||||||
.description('Generate a session title and subtitle from a prompt')
|
.description('Generate a session title and subtitle from a prompt')
|
||||||
.option('--json', 'Output as JSON')
|
.option('--json', 'Output as JSON')
|
||||||
.option('--oneline', 'Output as single line (title - subtitle)')
|
.option('--oneline', 'Output as single line (title - subtitle)')
|
||||||
.option('--save', 'Save title and subtitle to session metadata')
|
.option('--session-id <id>', 'Claude session ID to update')
|
||||||
.option('--project <name>', 'Project name (required with --save)')
|
.option('--save', 'Save the generated title to the database (requires --session-id)')
|
||||||
.option('--session <id>', 'Session ID (required with --save)')
|
|
||||||
.action(generateTitle);
|
.action(generateTitle);
|
||||||
|
|
||||||
// </Block> =======================================
|
// </Block> =======================================
|
||||||
@@ -268,3 +267,9 @@ try {
|
|||||||
// Parse arguments and execute
|
// Parse arguments and execute
|
||||||
program.parse();
|
program.parse();
|
||||||
// </Block> =======================================
|
// </Block> =======================================
|
||||||
|
|
||||||
|
// <Block> 1.12 ===================================
|
||||||
|
// Module Exports for Programmatic Use
|
||||||
|
// Export database and utility classes for hooks and external consumers
|
||||||
|
export { DatabaseManager, StreamingSessionStore, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
|
||||||
|
// </Block> =======================================
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { OptionValues } from 'commander';
|
import { OptionValues } from 'commander';
|
||||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { getClaudePath } from '../shared/settings.js';
|
import { getClaudePath } from '../shared/settings.js';
|
||||||
import path from 'path';
|
import { DatabaseManager } from '../services/sqlite/Database.js';
|
||||||
import fs from 'fs';
|
import { StreamingSessionStore } from '../services/sqlite/StreamingSessionStore.js';
|
||||||
|
import { migrations } from '../services/sqlite/migrations.js';
|
||||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a session title and subtitle from a user prompt
|
* Generate a session title and subtitle from a user prompt
|
||||||
* CLI command that uses Agent SDK (like changelog.ts)
|
* CLI command that uses Agent SDK (like changelog.ts)
|
||||||
|
*
|
||||||
|
* Can be called in two modes:
|
||||||
|
* 1. Standalone: generate-title "user prompt" --json
|
||||||
|
* 2. With session: generate-title "user prompt" --session-id <id> --save
|
||||||
*/
|
*/
|
||||||
export async function generateTitle(prompt: string, options: OptionValues): Promise<void> {
|
export async function generateTitle(prompt: string, options: OptionValues): Promise<void> {
|
||||||
if (!prompt || prompt.trim().length === 0) {
|
if (!prompt || prompt.trim().length === 0) {
|
||||||
@@ -19,6 +22,36 @@ export async function generateTitle(prompt: string, options: OptionValues): Prom
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If --session-id provided, validate that session exists
|
||||||
|
let streamingStore: StreamingSessionStore | null = null;
|
||||||
|
let sessionRecord = null;
|
||||||
|
|
||||||
|
if (options.sessionId) {
|
||||||
|
try {
|
||||||
|
const dbManager = DatabaseManager.getInstance();
|
||||||
|
for (const migration of migrations) {
|
||||||
|
dbManager.registerMigration(migration);
|
||||||
|
}
|
||||||
|
const db = await dbManager.initialize();
|
||||||
|
streamingStore = new StreamingSessionStore(db);
|
||||||
|
|
||||||
|
sessionRecord = streamingStore.getByClaudeSessionId(options.sessionId);
|
||||||
|
if (!sessionRecord) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `Session not found: ${options.sessionId}`
|
||||||
|
}));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `Database error: ${error.message}`
|
||||||
|
}));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const systemPrompt = `You are a title and subtitle generator for claude-mem session metadata.
|
const systemPrompt = `You are a title and subtitle generator for claude-mem session metadata.
|
||||||
|
|
||||||
Your job is to analyze a user's request and generate:
|
Your job is to analyze a user's request and generate:
|
||||||
@@ -107,49 +140,17 @@ Now generate the title and subtitle (two lines exactly):`;
|
|||||||
const title = lines[0].trim();
|
const title = lines[0].trim();
|
||||||
const subtitle = lines[1].trim();
|
const subtitle = lines[1].trim();
|
||||||
|
|
||||||
// Save to session metadata if --save flag is provided
|
// If --save and we have a session, update the database
|
||||||
if (options.save) {
|
if (options.save && streamingStore && sessionRecord) {
|
||||||
if (!options.project || !options.session) {
|
|
||||||
console.error(JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: '--project and --session are required when using --save'
|
|
||||||
}));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionFile = path.join(SESSION_DIR, `${options.project}_streaming.json`);
|
streamingStore.update(sessionRecord.id, {
|
||||||
|
title,
|
||||||
if (!fs.existsSync(sessionFile)) {
|
subtitle
|
||||||
console.error(JSON.stringify({
|
});
|
||||||
success: false,
|
|
||||||
error: `Session file not found: ${sessionFile}`
|
|
||||||
}));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sessionData: any = {};
|
|
||||||
try {
|
|
||||||
sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to parse session file'
|
|
||||||
}));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update metadata
|
|
||||||
sessionData.promptTitle = title;
|
|
||||||
sessionData.promptSubtitle = subtitle;
|
|
||||||
sessionData.updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
// Write back to file
|
|
||||||
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(JSON.stringify({
|
console.error(JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to save metadata: ${error.message}`
|
error: `Failed to save title: ${error.message}`
|
||||||
}));
|
}));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,8 @@ Now generate the title and subtitle (two lines exactly):`;
|
|||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
title,
|
title,
|
||||||
subtitle
|
subtitle,
|
||||||
|
sessionId: sessionRecord?.claude_session_id
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
} else if (options.oneline) {
|
} else if (options.oneline) {
|
||||||
console.log(`${title} - ${subtitle}`);
|
console.log(`${title} - ${subtitle}`);
|
||||||
|
|||||||
+176
-764
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
|||||||
|
import { Database } from 'better-sqlite3';
|
||||||
|
import { getDatabase } from './Database.js';
|
||||||
|
import { normalizeTimestamp } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a streaming session row in the database
|
||||||
|
*/
|
||||||
|
export interface StreamingSessionRow {
|
||||||
|
id: number;
|
||||||
|
claude_session_id: string;
|
||||||
|
sdk_session_id?: string;
|
||||||
|
project: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
user_prompt?: string;
|
||||||
|
started_at: string;
|
||||||
|
started_at_epoch: number;
|
||||||
|
updated_at?: string;
|
||||||
|
updated_at_epoch?: number;
|
||||||
|
completed_at?: string;
|
||||||
|
completed_at_epoch?: number;
|
||||||
|
status: 'active' | 'completed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input type for creating a new streaming session
|
||||||
|
*/
|
||||||
|
export interface StreamingSessionInput {
|
||||||
|
claude_session_id: string;
|
||||||
|
project: string;
|
||||||
|
user_prompt?: string;
|
||||||
|
started_at?: string | Date | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input type for updating a streaming session
|
||||||
|
*/
|
||||||
|
export interface StreamingSessionUpdate {
|
||||||
|
sdk_session_id?: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
status?: 'active' | 'completed' | 'failed';
|
||||||
|
completed_at?: string | Date | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Access Object for streaming session records
|
||||||
|
* Handles real-time session tracking during SDK compression
|
||||||
|
*/
|
||||||
|
export class StreamingSessionStore {
|
||||||
|
private db: Database.Database;
|
||||||
|
|
||||||
|
constructor(db?: Database.Database) {
|
||||||
|
this.db = db || getDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new streaming session record
|
||||||
|
* This should be called immediately when the hook receives a user prompt
|
||||||
|
*/
|
||||||
|
create(input: StreamingSessionInput): StreamingSessionRow {
|
||||||
|
const { isoString, epoch } = normalizeTimestamp(input.started_at);
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT INTO streaming_sessions (
|
||||||
|
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
|
||||||
|
) VALUES (?, ?, ?, ?, ?, 'active')
|
||||||
|
`);
|
||||||
|
|
||||||
|
const info = stmt.run(
|
||||||
|
input.claude_session_id,
|
||||||
|
input.project,
|
||||||
|
input.user_prompt || null,
|
||||||
|
isoString,
|
||||||
|
epoch
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.getById(info.lastInsertRowid as number)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a streaming session by internal ID
|
||||||
|
* Uses atomic transaction to prevent race conditions
|
||||||
|
*/
|
||||||
|
update(id: number, updates: StreamingSessionUpdate): StreamingSessionRow {
|
||||||
|
const { isoString: updatedAt, epoch: updatedEpoch } = normalizeTimestamp(new Date());
|
||||||
|
|
||||||
|
const existing = this.getById(id);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`Streaming session with id ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
const values: any[] = [];
|
||||||
|
|
||||||
|
if (updates.sdk_session_id !== undefined) {
|
||||||
|
parts.push('sdk_session_id = ?');
|
||||||
|
values.push(updates.sdk_session_id);
|
||||||
|
}
|
||||||
|
if (updates.title !== undefined) {
|
||||||
|
parts.push('title = ?');
|
||||||
|
values.push(updates.title);
|
||||||
|
}
|
||||||
|
if (updates.subtitle !== undefined) {
|
||||||
|
parts.push('subtitle = ?');
|
||||||
|
values.push(updates.subtitle);
|
||||||
|
}
|
||||||
|
if (updates.status !== undefined) {
|
||||||
|
parts.push('status = ?');
|
||||||
|
values.push(updates.status);
|
||||||
|
}
|
||||||
|
if (updates.completed_at !== undefined) {
|
||||||
|
const { isoString, epoch } = normalizeTimestamp(updates.completed_at);
|
||||||
|
parts.push('completed_at = ?', 'completed_at_epoch = ?');
|
||||||
|
values.push(isoString, epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the updated_at timestamp
|
||||||
|
parts.push('updated_at = ?', 'updated_at_epoch = ?');
|
||||||
|
values.push(updatedAt, updatedEpoch);
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
UPDATE streaming_sessions
|
||||||
|
SET ${parts.join(', ')}
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(...values);
|
||||||
|
|
||||||
|
return this.getById(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a streaming session by Claude session ID
|
||||||
|
* Convenience method for hooks that only have the Claude session ID
|
||||||
|
*/
|
||||||
|
updateByClaudeSessionId(claudeSessionId: string, updates: StreamingSessionUpdate): StreamingSessionRow | null {
|
||||||
|
const session = this.getByClaudeSessionId(claudeSessionId);
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.update(session.id, updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get streaming session by internal ID
|
||||||
|
*/
|
||||||
|
getById(id: number): StreamingSessionRow | null {
|
||||||
|
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE id = ?');
|
||||||
|
return stmt.get(id) as StreamingSessionRow || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get streaming session by Claude session ID
|
||||||
|
*/
|
||||||
|
getByClaudeSessionId(claudeSessionId: string): StreamingSessionRow | null {
|
||||||
|
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE claude_session_id = ?');
|
||||||
|
return stmt.get(claudeSessionId) as StreamingSessionRow || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get streaming session by SDK session ID
|
||||||
|
*/
|
||||||
|
getBySdkSessionId(sdkSessionId: string): StreamingSessionRow | null {
|
||||||
|
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE sdk_session_id = ?');
|
||||||
|
return stmt.get(sdkSessionId) as StreamingSessionRow || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a streaming session exists by Claude session ID
|
||||||
|
*/
|
||||||
|
has(claudeSessionId: string): boolean {
|
||||||
|
const stmt = this.db.prepare('SELECT 1 FROM streaming_sessions WHERE claude_session_id = ? LIMIT 1');
|
||||||
|
return Boolean(stmt.get(claudeSessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active streaming sessions for a project
|
||||||
|
*/
|
||||||
|
getActiveForProject(project: string): StreamingSessionRow[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM streaming_sessions
|
||||||
|
WHERE project = ? AND status = 'active'
|
||||||
|
ORDER BY started_at_epoch DESC
|
||||||
|
`);
|
||||||
|
return stmt.all(project) as StreamingSessionRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active streaming sessions
|
||||||
|
*/
|
||||||
|
getAllActive(): StreamingSessionRow[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM streaming_sessions
|
||||||
|
WHERE status = 'active'
|
||||||
|
ORDER BY started_at_epoch DESC
|
||||||
|
`);
|
||||||
|
return stmt.all() as StreamingSessionRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent streaming sessions (completed or failed)
|
||||||
|
*/
|
||||||
|
getRecent(limit = 10): StreamingSessionRow[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM streaming_sessions
|
||||||
|
ORDER BY started_at_epoch DESC
|
||||||
|
LIMIT ?
|
||||||
|
`);
|
||||||
|
return stmt.all(limit) as StreamingSessionRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a session as completed
|
||||||
|
*/
|
||||||
|
markCompleted(id: number): StreamingSessionRow {
|
||||||
|
return this.update(id, {
|
||||||
|
status: 'completed',
|
||||||
|
completed_at: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a session as failed
|
||||||
|
*/
|
||||||
|
markFailed(id: number): StreamingSessionRow {
|
||||||
|
return this.update(id, {
|
||||||
|
status: 'failed',
|
||||||
|
completed_at: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a streaming session by ID
|
||||||
|
*/
|
||||||
|
deleteById(id: number): boolean {
|
||||||
|
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE id = ?');
|
||||||
|
const info = stmt.run(id);
|
||||||
|
return info.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a streaming session by Claude session ID
|
||||||
|
*/
|
||||||
|
deleteByClaudeSessionId(claudeSessionId: string): boolean {
|
||||||
|
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE claude_session_id = ?');
|
||||||
|
const info = stmt.run(claudeSessionId);
|
||||||
|
return info.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old completed/failed sessions (older than N days)
|
||||||
|
*/
|
||||||
|
cleanupOldSessions(daysOld = 30): number {
|
||||||
|
const cutoffEpoch = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
DELETE FROM streaming_sessions
|
||||||
|
WHERE status IN ('completed', 'failed')
|
||||||
|
AND completed_at_epoch < ?
|
||||||
|
`);
|
||||||
|
const info = stmt.run(cutoffEpoch);
|
||||||
|
return info.changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export { MemoryStore } from './MemoryStore.js';
|
|||||||
export { OverviewStore } from './OverviewStore.js';
|
export { OverviewStore } from './OverviewStore.js';
|
||||||
export { DiagnosticsStore } from './DiagnosticsStore.js';
|
export { DiagnosticsStore } from './DiagnosticsStore.js';
|
||||||
export { TranscriptEventStore } from './TranscriptEventStore.js';
|
export { TranscriptEventStore } from './TranscriptEventStore.js';
|
||||||
|
export { StreamingSessionStore } from './StreamingSessionStore.js';
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
@@ -26,18 +27,20 @@ export async function createStores() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = await manager.initialize();
|
const db = await manager.initialize();
|
||||||
|
|
||||||
const { SessionStore } = await import('./SessionStore.js');
|
const { SessionStore } = await import('./SessionStore.js');
|
||||||
const { MemoryStore } = await import('./MemoryStore.js');
|
const { MemoryStore } = await import('./MemoryStore.js');
|
||||||
const { OverviewStore } = await import('./OverviewStore.js');
|
const { OverviewStore } = await import('./OverviewStore.js');
|
||||||
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
|
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
|
||||||
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
|
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
|
||||||
|
const { StreamingSessionStore } = await import('./StreamingSessionStore.js');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: new SessionStore(db),
|
sessions: new SessionStore(db),
|
||||||
memories: new MemoryStore(db),
|
memories: new MemoryStore(db),
|
||||||
overviews: new OverviewStore(db),
|
overviews: new OverviewStore(db),
|
||||||
diagnostics: new DiagnosticsStore(db),
|
diagnostics: new DiagnosticsStore(db),
|
||||||
transcriptEvents: new TranscriptEventStore(db)
|
transcriptEvents: new TranscriptEventStore(db),
|
||||||
|
streamingSessions: new StreamingSessionStore(db)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,10 +160,53 @@ export const migration002: Migration = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration 003 - Add streaming_sessions table for real-time session tracking
|
||||||
|
*/
|
||||||
|
export const migration003: Migration = {
|
||||||
|
version: 3,
|
||||||
|
up: (db: Database.Database) => {
|
||||||
|
// Streaming sessions table - tracks active SDK compression sessions
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
claude_session_id TEXT UNIQUE NOT NULL,
|
||||||
|
sdk_session_id TEXT,
|
||||||
|
project TEXT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
subtitle TEXT,
|
||||||
|
user_prompt TEXT,
|
||||||
|
started_at TEXT NOT NULL,
|
||||||
|
started_at_epoch INTEGER NOT NULL,
|
||||||
|
updated_at TEXT,
|
||||||
|
updated_at_epoch INTEGER,
|
||||||
|
completed_at TEXT,
|
||||||
|
completed_at_epoch INTEGER,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(claude_session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(sdk_session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ Created streaming_sessions table for real-time session tracking');
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (db: Database.Database) => {
|
||||||
|
db.exec(`
|
||||||
|
DROP TABLE IF EXISTS streaming_sessions;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All migrations in order
|
* All migrations in order
|
||||||
*/
|
*/
|
||||||
export const migrations: Migration[] = [
|
export const migrations: Migration[] = [
|
||||||
migration001,
|
migration001,
|
||||||
migration002
|
migration002,
|
||||||
|
migration003
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { platform, homedir } from 'os';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { chmodSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const isWindows = platform() === 'win32';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific utilities for cross-platform compatibility
|
||||||
|
* Handles differences between Windows and Unix-like systems
|
||||||
|
*/
|
||||||
|
export const Platform = {
|
||||||
|
/**
|
||||||
|
* Returns the appropriate shell for the current platform
|
||||||
|
*/
|
||||||
|
getShell: (): string => {
|
||||||
|
return isWindows ? 'powershell' : '/bin/sh';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file extension for hook scripts
|
||||||
|
*/
|
||||||
|
getHookExtension: (): string => {
|
||||||
|
return '.js'; // Both platforms can execute Node.js scripts
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to an executable command
|
||||||
|
* @param name - Name of the executable to find
|
||||||
|
* @returns Full path to the executable
|
||||||
|
*/
|
||||||
|
findExecutable: (name: string): string => {
|
||||||
|
const cmd = isWindows ? `where ${name}` : `which ${name}`;
|
||||||
|
return execSync(cmd, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore']
|
||||||
|
}).trim();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a file executable (Unix only - no-op on Windows)
|
||||||
|
* @param path - Path to the file to make executable
|
||||||
|
*/
|
||||||
|
makeExecutable: (path: string): void => {
|
||||||
|
if (!isWindows) {
|
||||||
|
chmodSync(path, 0o755);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs uv package manager using platform-specific method
|
||||||
|
*/
|
||||||
|
installUv: (): void => {
|
||||||
|
if (isWindows) {
|
||||||
|
execSync('powershell -Command "irm https://astral.sh/uv/install.ps1 | iex"', {
|
||||||
|
stdio: 'pipe'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
|
||||||
|
stdio: 'pipe',
|
||||||
|
shell: '/bin/sh'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns shell configuration file paths for the current platform
|
||||||
|
* @returns Array of shell config file paths
|
||||||
|
*/
|
||||||
|
getShellConfigPaths: (): string[] => {
|
||||||
|
const home = homedir();
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
return [
|
||||||
|
join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
||||||
|
join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
join(home, '.bashrc'),
|
||||||
|
join(home, '.zshrc'),
|
||||||
|
join(home, '.bash_profile')
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the appropriate alias syntax for the current platform's shell
|
||||||
|
* @param aliasName - Name of the alias
|
||||||
|
* @param command - Command to alias
|
||||||
|
* @returns Alias definition string
|
||||||
|
*/
|
||||||
|
getAliasDefinition: (aliasName: string, command: string): string => {
|
||||||
|
if (isWindows) {
|
||||||
|
// PowerShell function syntax
|
||||||
|
return `function ${aliasName} { ${command} $args }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bash/Zsh alias syntax
|
||||||
|
return `alias ${aliasName}='${command}'`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current platform is Windows
|
||||||
|
*/
|
||||||
|
isWindows: (): boolean => isWindows,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current platform is Unix-like (macOS/Linux)
|
||||||
|
*/
|
||||||
|
isUnix: (): boolean => !isWindows
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user