Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,67 @@ cipher --new-session [id] # Start with new session
/help # Show help
```

## Chat History

Cipher supports persistent chat history using PostgreSQL as the primary storage backend. This allows conversations to be restored across application restarts.

### PostgreSQL Configuration

To use PostgreSQL for chat history persistence, set the following environment variables:

#### Option 1: Using Connection URL (Recommended)

```bash
export CIPHER_PG_URL="postgresql://username:password@localhost:5432/cipher_db"
```

#### Option 2: Using Individual Parameters

```bash
export STORAGE_DATABASE_HOST="localhost"
export STORAGE_DATABASE_PORT="5432"
export STORAGE_DATABASE_NAME="cipher_db"
export STORAGE_DATABASE_USER="username"
export STORAGE_DATABASE_PASSWORD="password"
export STORAGE_DATABASE_SSL="false"
```

### Database Setup

1. Create a PostgreSQL database:

```sql
CREATE DATABASE cipher_db;
```

2. The application will automatically create the necessary tables and indexes on first run.

### Fallback Behavior

If PostgreSQL is not available or fails to connect, Cipher will automatically fall back to:

1. SQLite (local file-based storage)
2. In-memory storage (no persistence)

### Session Storage

Sessions are stored with the following key pattern:

- Session data: `cipher:sessions:{sessionId}`
- Message history: `messages:{sessionId}`

### Environment Variables

| Variable | Description | Default |
| --------------------------- | ------------------------- | ------- |
| `CIPHER_PG_URL` | PostgreSQL connection URL | None |
| `STORAGE_DATABASE_HOST` | PostgreSQL host | None |
| `STORAGE_DATABASE_PORT` | PostgreSQL port | 5432 |
| `STORAGE_DATABASE_NAME` | Database name | None |
| `STORAGE_DATABASE_USER` | Username | None |
| `STORAGE_DATABASE_PASSWORD` | Password | None |
| `STORAGE_DATABASE_SSL` | Enable SSL | false |

## MCP Server Usage

Cipher can run as an MCP (Model Context Protocol) server, allowing integration with MCP-compatible clients like Claude Desktop, Cursor, Windsurf, and other AI coding assistants.
Expand Down
4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ignoredBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
19 changes: 16 additions & 3 deletions src/app/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,10 +403,17 @@
// Wait a bit for session to be ready
await new Promise(resolve => setTimeout(resolve, 50));

const session = await agent.getSession(agent.getCurrentSessionId());
const sessionId = agent.getCurrentSessionId();
logger.debug(`CLI: Initializing session: ${sessionId}`);

const session = await agent.getSession(sessionId);

if (session && typeof session.init === 'function') {
logger.debug(`CLI: Initializing session ${session.id}`);
await session.init();
logger.debug(`CLI: Session ${session.id} initialized successfully`);
} else {
logger.warn(`CLI: Failed to get or initialize session ${sessionId}`);
}

// Wait a bit more for compression system to be fully initialized
Expand All @@ -421,15 +428,20 @@
*/
async function _showCompressionStartup(agent: MemAgent): Promise<void> {
try {
const session = await agent.getSession(agent.getCurrentSessionId());
const sessionId = agent.getCurrentSessionId();
logger.debug(`CLI: Getting session for compression info: ${sessionId}`);

const session = await agent.getSession(sessionId);

if (!session) {
logger.debug('CLI: No session available for compression info');
// Session not ready yet, skip compression info silently
return;
}

const ctx = session.getContextManager();
if (!ctx) {
logger.debug('CLI: No context manager available for compression info');
return;
}

Expand All @@ -445,7 +457,8 @@

lastCompressionHistoryLength = ctx['compressionHistory']?.length || 0;
}
} catch {
} catch (error) {
logger.debug('CLI: Error during compression startup info:', error);
// Intentionally empty - compression info is optional
}
}
Expand Down Expand Up @@ -477,7 +490,7 @@
/**
* Display compression event information
*/
function _displayCompressionEvent(event: any): void {

Check warning on line 493 in src/app/cli/cli.ts

View workflow job for this annotation

GitHub Actions / ESLint

'event' is defined but never used. Allowed unused args must match /^_/u
console.log(chalk.yellowBright('⚡ Context has been compressed.'));
}

Expand Down
173 changes: 173 additions & 0 deletions src/app/cli/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,99 @@
},
});

// Load history command
this.registerCommand({
name: 'load-history',
description: 'Load conversation history for current session',
category: 'session',
handler: async (args: string[], agent: MemAgent) => {
try {
const currentSessionId = agent.getCurrentSessionId();
console.log(
chalk.cyan(`🔄 Loading conversation history for session: ${currentSessionId}`)
);

await agent.loadSessionHistory(currentSessionId);

// Show the loaded history
const history = await agent.getCurrentSessionHistory();
console.log(
chalk.green(
`✅ Successfully loaded ${history.length} messages from conversation history`
)
);

if (history.length > 0) {
console.log(chalk.gray('Recent messages:'));
history.slice(-3).forEach((msg, index) => {
const role = msg.role || 'unknown';
const content =
typeof msg.content === 'string'
? msg.content.substring(0, 80) + (msg.content.length > 80 ? '...' : '')
: JSON.stringify(msg.content).substring(0, 80) + '...';
console.log(chalk.gray(` ${history.length - 3 + index + 1}. [${role}] ${content}`));
});
}

return true;
} catch (error) {
console.log(
chalk.red(
`❌ Failed to load conversation history: ${error instanceof Error ? error.message : String(error)}`
)
);
return false;
}
},
});

// Debug command
this.registerCommand({
name: 'debug',
description: 'Show debug information for current session',
category: 'system',
handler: async (args: string[], agent: MemAgent) => {
try {
const currentSessionId = agent.getCurrentSessionId();
console.log(chalk.cyan('🔍 Debug Information:'));
console.log(chalk.gray(`Current Session: ${currentSessionId}`));

// Get session metadata
const metadata = await agent.getSessionMetadata(currentSessionId);
if (metadata) {
console.log(chalk.gray(`Message Count: ${metadata.messageCount}`));
}

// Get conversation history
const history = await agent.getCurrentSessionHistory();
console.log(chalk.gray(`History Length: ${history.length}`));

if (history.length > 0) {
console.log(chalk.gray('Recent Messages:'));
history.slice(-5).forEach((msg, index) => {
const role = msg.role || 'unknown';
const content =
typeof msg.content === 'string'
? msg.content.substring(0, 100) + (msg.content.length > 100 ? '...' : '')
: JSON.stringify(msg.content).substring(0, 100) + '...';
console.log(chalk.gray(` ${history.length - 5 + index + 1}. [${role}] ${content}`));
});
} else {
console.log(chalk.gray(' No conversation history found'));
}

return true;
} catch (error) {
console.log(
chalk.red(
`❌ Failed to get debug info: ${error instanceof Error ? error.message : String(error)}`
)
);
return false;
}
},
});

// Clear/Reset command
this.registerCommand({
name: 'clear',
Expand Down Expand Up @@ -707,6 +800,11 @@
case 'del':
case 'remove':
return this.sessionDeleteHandler(subArgs, agent);
case 'save':
return this.sessionSaveHandler(subArgs, agent);
case 'load':
case 'restore':
return this.sessionLoadHandler(subArgs, agent);
case 'help':
case 'h':
return this.sessionHelpHandler(subArgs, agent);
Expand Down Expand Up @@ -985,7 +1083,7 @@
console.log(
chalk.gray('💡 LLM summary generated and cached for file-based provider.')
);
} catch (err) {

Check warning on line 1086 in src/app/cli/parser.ts

View workflow job for this annotation

GitHub Actions / ESLint

'err' is defined but never used
console.log(
chalk.yellow(
'⚠️ LLM summarization failed to cache immediately, will retry on next /prompt.'
Expand Down Expand Up @@ -1034,7 +1132,7 @@
}
await enhanced.addOrUpdateProvider(newConfig);
// Fetch the new provider instance after update
const updatedProvider = enhanced.getProvider(name);

Check warning on line 1135 in src/app/cli/parser.ts

View workflow job for this annotation

GitHub Actions / ESLint

'updatedProvider' is assigned a value but never used
// If summarize flag is set to true for file-based provider, trigger LLM summarization immediately
if (summarizeFlag && typeof name === 'string') {
const updatedProvider = enhanced.getProvider(name);
Expand All @@ -1051,7 +1149,7 @@
console.log(
chalk.gray('💡 LLM summary generated and cached for file-based provider.')
);
} catch (err) {

Check warning on line 1152 in src/app/cli/parser.ts

View workflow job for this annotation

GitHub Actions / ESLint

'err' is defined but never used
console.log(
chalk.yellow(
'⚠️ LLM summarization failed to cache immediately, will retry on next /prompt.'
Expand Down Expand Up @@ -1521,6 +1619,79 @@
}
}

/**
* Session save subcommand handler
*/
private async sessionSaveHandler(_args: string[], agent: MemAgent): Promise<boolean> {
try {
console.log(chalk.cyan('💾 Saving all sessions to persistent storage...'));

const result = await agent.saveAllSessions();

console.log('');
if (result.saved > 0) {
console.log(chalk.green(`✅ Successfully saved ${result.saved} session(s)`));
}

if (result.failed > 0) {
console.log(chalk.yellow(`⚠️ Failed to save ${result.failed} session(s)`));
}

if (result.total === 0) {
console.log(chalk.gray('📭 No active sessions to save'));
}

console.log(chalk.gray(`📊 Total: ${result.total} sessions processed`));
console.log('');

return true;
} catch (error) {
console.log(
chalk.red(
`❌ Failed to save sessions: ${error instanceof Error ? error.message : String(error)}`
)
);
return false;
}
}

/**
* Session load subcommand handler
*/
private async sessionLoadHandler(_args: string[], agent: MemAgent): Promise<boolean> {
try {
console.log(chalk.cyan('📂 Loading sessions from persistent storage...'));

const result = await agent.loadAllSessions();

console.log('');
if (result.restored > 0) {
console.log(chalk.green(`✅ Successfully restored ${result.restored} session(s)`));
}

if (result.failed > 0) {
console.log(chalk.yellow(`⚠️ Failed to restore ${result.failed} session(s)`));
}

if (result.total === 0) {
console.log(chalk.gray('📭 No sessions found in storage'));
}

console.log(chalk.gray(`📊 Total: ${result.total} sessions found in storage`));
console.log('');
console.log(chalk.gray('💡 Use /session list to see all active sessions'));

return true;
} catch (error) {
console.log(
chalk.red(
`❌ Failed to load sessions: ${error instanceof Error ? error.message : String(error)}`
)
);
return false;
}
}

/**
* Session help subcommand handler
*/
Expand All @@ -1535,6 +1706,8 @@
'/session switch <id> - Switch to different session',
'/session current - Show current session info',
'/session delete <id> - Delete session (cannot delete active)',
'/session save - Manually save all sessions to persistent storage',
'/session load - Manually load sessions from persistent storage',
'/session help - Show this help message',
];

Expand Down Expand Up @@ -1603,7 +1776,7 @@
return false;
}

const providerName = args[0];

Check warning on line 1779 in src/app/cli/parser.ts

View workflow job for this annotation

GitHub Actions / ESLint

'providerName' is assigned a value but never used

console.log(chalk.yellow('⚠️ Enhanced Prompt System Active'));
console.log('');
Expand Down Expand Up @@ -1641,7 +1814,7 @@
return false;
}

const providerName = args[0];

Check warning on line 1817 in src/app/cli/parser.ts

View workflow job for this annotation

GitHub Actions / ESLint

'providerName' is assigned a value but never used

console.log(chalk.yellow('⚠️ Enhanced Prompt System Active'));
console.log('');
Expand Down
Loading
Loading