Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 266076da98 | |||
| a0b4381dc8 | |||
| 4904d9c531 | |||
| 4c44a65877 | |||
| f6b310126c | |||
| 77220a76bf | |||
| 42ed414a4c | |||
| 0185d765ce | |||
| 12c2ecce06 | |||
| bb0508d639 | |||
| f00ef33f86 | |||
| c270bd3177 | |||
| 0836a97845 | |||
| 19e285a209 | |||
| ba877214c1 | |||
| 3d4baefac2 | |||
| 453b7857b8 | |||
| c28417af00 | |||
| 2f08db3c01 | |||
| 672cb5d203 | |||
| 5b338ba34e | |||
| 4e7ed75fa9 | |||
| a8b84fa7b6 | |||
| 73be8f7a63 | |||
| fa93f2c1e2 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.8",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
github: thedotmack
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Summarize new issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run AI inference
|
||||
id: inference
|
||||
uses: actions/ai-inference@v1
|
||||
with:
|
||||
prompt: |
|
||||
Summarize the following GitHub issue in one paragraph:
|
||||
Title: ${{ github.event.issue.title }}
|
||||
Body: ${{ github.event.issue.body }}
|
||||
|
||||
- name: Comment with AI summary
|
||||
run: |
|
||||
gh issue comment $ISSUE_NUMBER --body '${{ steps.inference.outputs.response }}'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
RESPONSE: ${{ steps.inference.outputs.response }}
|
||||
+4
-1
@@ -14,4 +14,7 @@ package-lock.json
|
||||
private/
|
||||
|
||||
# Generated UI files (built from viewer-template.html)
|
||||
src/ui/viewer.html
|
||||
src/ui/viewer.html
|
||||
|
||||
# Local MCP server config (for development only)
|
||||
.mcp.json
|
||||
@@ -1,14 +1,3 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"old-claude-mem": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"chroma-mcp",
|
||||
"--client-type",
|
||||
"persistent",
|
||||
"--data-dir",
|
||||
"/Users/alexnewman/.claude-mem/backups/chroma-backup-20251005-222403"
|
||||
]
|
||||
}
|
||||
}
|
||||
"mcpServers": {}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [7.1.7] - 2025-12-13
|
||||
|
||||
## Fixed
|
||||
- Removed Windows workaround that was causing libuv assertion failures
|
||||
- Prioritized stability over cosmetic console window issue
|
||||
|
||||
## Known Issue
|
||||
- On Windows, a console window may briefly appear when the worker starts (cosmetic only, does not affect functionality)
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.6...v7.1.7
|
||||
|
||||
## [7.1.6] - 2025-12-13
|
||||
|
||||
## What's Changed
|
||||
|
||||
Improved error messages with platform-specific worker restart instructions for better troubleshooting experience.
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.5...v7.1.6
|
||||
|
||||
## [7.1.5] - 2025-12-13
|
||||
|
||||
## What's Changed
|
||||
|
||||
* fix: Use getWorkerHost() instead of hardcoded localhost in MCP server (#276)
|
||||
|
||||
### Bug Fix
|
||||
Fixes Windows IPv6 issue where `localhost` resolves to `::1` (IPv6) but worker binds to `127.0.0.1` (IPv4), causing MCP tool connections to fail.
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.4...v7.1.5
|
||||
|
||||
## [7.1.4] - 2025-12-13
|
||||
|
||||
## What's Changed
|
||||
|
||||
* fix: add npm fallback when bun install fails with alias packages (#265)
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.3...v7.1.4
|
||||
|
||||
## [7.1.3] - 2025-12-13
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
### Smart Install Script Refactoring
|
||||
|
||||
Refactored the smart-install.js script to improve code quality and maintainability:
|
||||
- Extracted common installation paths as top-level constants (BUN_COMMON_PATHS, UV_COMMON_PATHS)
|
||||
- Simplified installation check functions to delegate to dedicated path-finding helpers
|
||||
- Streamlined installation verification logic with clearer error messages
|
||||
- Removed redundant post-installation verification checks
|
||||
- Improved error propagation by removing unnecessary retry logic
|
||||
|
||||
This refactoring reduces code duplication and makes the installation process more maintainable while preserving the same functionality for detecting Bun and uv binaries across platforms.
|
||||
|
||||
## [7.1.2] - 2025-12-13
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
|
||||
|
||||
**Current Version**: 7.1.3
|
||||
**Current Version**: 7.1.8
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -398,6 +398,10 @@ If you're experiencing issues, describe the problem to Claude and the troublesho
|
||||
|
||||
See [Troubleshooting Guide](https://docs.claude-mem.ai/troubleshooting) for complete solutions.
|
||||
|
||||
### Windows Known Issues
|
||||
|
||||
**Console Window Visibility**: On Windows, a console window may briefly appear when the worker service starts. This is a cosmetic issue that we're working to resolve. We've prioritized stability by removing a workaround that was causing libuv crashes. The window does not affect functionality and will be addressed in a future release when the MCP SDK provides proper window hiding support.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"usage/search-tools",
|
||||
"usage/claude-desktop",
|
||||
"usage/private-tags",
|
||||
"usage/export-import",
|
||||
"beta-features"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
title: "Memory Export/Import"
|
||||
description: "Share knowledge across claude-mem installations with duplicate prevention"
|
||||
---
|
||||
|
||||
# Memory Export/Import Scripts
|
||||
|
||||
Share your claude-mem knowledge with other users! These scripts allow you to export specific memories (observations, sessions, summaries, and prompts) and import them into another claude-mem installation.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Share Windows compatibility knowledge** with Windows users
|
||||
- **Share bug fix patterns** with contributors
|
||||
- **Share project-specific learnings** across teams
|
||||
- **Backup specific memory sets** for safekeeping
|
||||
|
||||
## How It Works
|
||||
|
||||
### Export Script
|
||||
|
||||
Searches the database using **hybrid search** (combines ChromaDB vector embeddings with FTS5 full-text search) and exports all matching:
|
||||
- **Observations** - Individual learnings and discoveries
|
||||
- **Sessions** - Session metadata
|
||||
- **Summaries** - Session summaries
|
||||
- **Prompts** - User prompts that led to the work
|
||||
|
||||
Output is a portable JSON file that can be shared.
|
||||
|
||||
> **Privacy Note:** Export files contain all matching memory data in plain text. Review exports before sharing to ensure no sensitive information (API keys, passwords, private paths) is included.
|
||||
|
||||
### Import Script
|
||||
|
||||
Imports memories with **duplicate prevention**:
|
||||
- Checks if each record already exists before inserting
|
||||
- Skips duplicates automatically
|
||||
- Maintains data integrity with transactional imports
|
||||
- Reports what was imported vs. skipped
|
||||
|
||||
**Duplicate Detection Strategy:**
|
||||
- **Sessions**: By `claude_session_id` (unique)
|
||||
- **Summaries**: By `sdk_session_id` (unique)
|
||||
- **Observations**: By `sdk_session_id` + `title` + `created_at_epoch` (composite)
|
||||
- **Prompts**: By `claude_session_id` + `prompt_number` (composite)
|
||||
|
||||
## Usage
|
||||
|
||||
### Export Memories
|
||||
|
||||
```bash
|
||||
# Export all Windows-related memories
|
||||
npx tsx scripts/export-memories.ts "windows" windows-memories.json
|
||||
|
||||
# Export bug fixes
|
||||
npx tsx scripts/export-memories.ts "bugfix" bugfixes.json
|
||||
|
||||
# Export specific feature work
|
||||
npx tsx scripts/export-memories.ts "progressive disclosure" progressive-disclosure.json
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `<query>` - Search query (uses hybrid semantic + full-text search)
|
||||
2. `<output-file>` - Output JSON file path
|
||||
3. `--project=name` - Optional: filter results to a specific project
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
🔍 Searching for: "windows"
|
||||
✅ Found 54 observations
|
||||
✅ Found 12 sessions
|
||||
✅ Found 12 summaries
|
||||
✅ Found 7 prompts
|
||||
|
||||
📦 Export complete!
|
||||
📄 Output: windows-memories.json
|
||||
📊 Stats:
|
||||
• 54 observations
|
||||
• 12 sessions
|
||||
• 12 summaries
|
||||
• 7 prompts
|
||||
```
|
||||
|
||||
### Import Memories
|
||||
|
||||
```bash
|
||||
# Import from an export file
|
||||
npx tsx scripts/import-memories.ts windows-memories.json
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `<input-file>` - Input JSON file (from export script)
|
||||
|
||||
**Example Output:**
|
||||
```
|
||||
📦 Import file: windows-memories.json
|
||||
📅 Exported: 2025-12-10T23:45:00.000Z
|
||||
🔍 Query: "windows"
|
||||
📊 Contains:
|
||||
• 54 observations
|
||||
• 12 sessions
|
||||
• 12 summaries
|
||||
• 7 prompts
|
||||
|
||||
🔄 Importing sessions...
|
||||
✅ Imported: 12, Skipped: 0
|
||||
🔄 Importing summaries...
|
||||
✅ Imported: 12, Skipped: 0
|
||||
🔄 Importing observations...
|
||||
✅ Imported: 54, Skipped: 0
|
||||
🔄 Importing prompts...
|
||||
✅ Imported: 7, Skipped: 0
|
||||
|
||||
✅ Import complete!
|
||||
📊 Summary:
|
||||
Sessions: 12 imported, 0 skipped
|
||||
Summaries: 12 imported, 0 skipped
|
||||
Observations: 54 imported, 0 skipped
|
||||
Prompts: 7 imported, 0 skipped
|
||||
```
|
||||
|
||||
### Re-importing (Duplicate Prevention)
|
||||
|
||||
If you run the import again on the same file, duplicates are automatically skipped:
|
||||
|
||||
```
|
||||
🔄 Importing sessions...
|
||||
✅ Imported: 0, Skipped: 12 ← All skipped (already exist)
|
||||
🔄 Importing summaries...
|
||||
✅ Imported: 0, Skipped: 12
|
||||
🔄 Importing observations...
|
||||
✅ Imported: 0, Skipped: 54
|
||||
🔄 Importing prompts...
|
||||
✅ Imported: 0, Skipped: 7
|
||||
```
|
||||
|
||||
## Sharing Memories
|
||||
|
||||
### For Export Authors
|
||||
|
||||
1. **Export your memories:**
|
||||
```bash
|
||||
npx tsx scripts/export-memories.ts "windows" windows-memories.json
|
||||
```
|
||||
|
||||
2. **Share the JSON file** via:
|
||||
- GitHub gist
|
||||
- Project repository (`shared-memories/`)
|
||||
- Direct file transfer
|
||||
- Package in releases
|
||||
|
||||
3. **Document what's included:**
|
||||
- What query was used
|
||||
- What knowledge is contained
|
||||
- Who might benefit from it
|
||||
|
||||
### For Import Users
|
||||
|
||||
1. **Download the export file** to your local machine
|
||||
|
||||
2. **Review what's in it** (optional):
|
||||
```bash
|
||||
cat windows-memories.json | jq '.totalObservations, .totalSessions'
|
||||
```
|
||||
|
||||
3. **Import into your database:**
|
||||
```bash
|
||||
npx tsx scripts/import-memories.ts windows-memories.json
|
||||
```
|
||||
|
||||
4. **Verify import** by searching:
|
||||
```bash
|
||||
curl "http://localhost:37777/api/search?query=windows&format=index&limit=10"
|
||||
```
|
||||
|
||||
## JSON Export Format
|
||||
|
||||
```json
|
||||
{
|
||||
"exportedAt": "2025-12-10T23:45:00.000Z",
|
||||
"exportedAtEpoch": 1733876700000,
|
||||
"query": "windows",
|
||||
"totalObservations": 54,
|
||||
"totalSessions": 12,
|
||||
"totalSummaries": 12,
|
||||
"totalPrompts": 7,
|
||||
"observations": [ /* array of observation objects */ ],
|
||||
"sessions": [ /* array of session objects */ ],
|
||||
"summaries": [ /* array of summary objects */ ],
|
||||
"prompts": [ /* array of prompt objects */ ]
|
||||
}
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
|
||||
✅ **Duplicate Prevention** - Won't re-import existing records
|
||||
✅ **Transactional** - All-or-nothing imports (database stays consistent)
|
||||
✅ **Read-only Export** - Export script opens database in read-only mode
|
||||
✅ **Dependency Ordering** - Sessions imported before observations/summaries
|
||||
✅ **Validation** - Checks database exists before starting
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Export by Project
|
||||
|
||||
```bash
|
||||
# Export only claude-mem project memories
|
||||
npx tsx scripts/export-memories.ts "bugfix" bugfixes.json --project=claude-mem
|
||||
|
||||
# Export all memories for a specific project
|
||||
npx tsx scripts/export-memories.ts "" all-project.json --project=my-app
|
||||
```
|
||||
|
||||
### Export by Type
|
||||
|
||||
```bash
|
||||
# Export only discoveries
|
||||
npx tsx scripts/export-memories.ts "type:discovery" discoveries.json
|
||||
|
||||
# Export only bug fixes
|
||||
npx tsx scripts/export-memories.ts "type:bugfix" bugfixes.json
|
||||
```
|
||||
|
||||
### Export by Date Range
|
||||
|
||||
You can filter the export after exporting:
|
||||
|
||||
```bash
|
||||
# Export all memories, then filter manually with jq
|
||||
npx tsx scripts/export-memories.ts "" all-memories.json
|
||||
cat all-memories.json | jq '.observations |= map(select(.created_at_epoch > 1700000000000))' > recent-memories.json
|
||||
```
|
||||
|
||||
### Combine Multiple Exports
|
||||
|
||||
```bash
|
||||
# Export different topics
|
||||
npx tsx scripts/export-memories.ts "windows" windows.json
|
||||
npx tsx scripts/export-memories.ts "linux" linux.json
|
||||
|
||||
# Import both
|
||||
npx tsx scripts/import-memories.ts windows.json
|
||||
npx tsx scripts/import-memories.ts linux.json
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Not Found
|
||||
|
||||
```
|
||||
❌ Database not found at: /Users/you/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
**Solution:** Make sure claude-mem is installed and has been run at least once.
|
||||
|
||||
### Import File Not Found
|
||||
|
||||
```
|
||||
❌ Input file not found: windows-memories.json
|
||||
```
|
||||
|
||||
**Solution:** Check the file path. Use absolute paths if needed.
|
||||
|
||||
### Partial Import
|
||||
|
||||
If import fails mid-way, the transaction is rolled back - your database remains unchanged. Fix the issue and try again.
|
||||
|
||||
## Contributing Memory Sets
|
||||
|
||||
If you've exported valuable knowledge that others might benefit from:
|
||||
|
||||
1. Create a PR to the `shared-memories/` directory
|
||||
2. Include a README describing what's in the export
|
||||
3. Tag with relevant keywords (windows, linux, bugfix, etc.)
|
||||
4. Community members can then import your knowledge!
|
||||
|
||||
## Examples of Useful Exports
|
||||
|
||||
**Windows Compatibility Knowledge:**
|
||||
```bash
|
||||
npx tsx scripts/export-memories.ts "windows compatibility installation" windows-fixes.json
|
||||
```
|
||||
|
||||
**Progressive Disclosure Architecture:**
|
||||
```bash
|
||||
npx tsx scripts/export-memories.ts "progressive disclosure architecture token" pd-patterns.json
|
||||
```
|
||||
|
||||
**Bug Fix Patterns:**
|
||||
```bash
|
||||
npx tsx scripts/export-memories.ts "bugfix error handling" bugfix-patterns.json
|
||||
```
|
||||
|
||||
**Performance Optimization:**
|
||||
```bash
|
||||
npx tsx scripts/export-memories.ts "performance optimization caching" perf-tips.json
|
||||
```
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.0.10",
|
||||
"version": "7.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "7.0.10",
|
||||
"version": "7.1.5",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.67",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.8",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.8",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "7.1.3",
|
||||
"version": "7.1.8",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -268,7 +268,11 @@ function needsInstall() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install dependencies using Bun
|
||||
* Install dependencies using Bun with npm fallback
|
||||
*
|
||||
* Bun has issues with npm alias packages (e.g., string-width-cjs, strip-ansi-cjs)
|
||||
* that are defined in package-lock.json. When bun fails with 404 errors for these
|
||||
* packages, we fall back to npm which handles aliases correctly.
|
||||
*/
|
||||
function installDeps() {
|
||||
const bunPath = getBunPath();
|
||||
@@ -281,11 +285,29 @@ function installDeps() {
|
||||
// Quote path for Windows paths with spaces
|
||||
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
|
||||
|
||||
let bunSucceeded = false;
|
||||
try {
|
||||
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
bunSucceeded = true;
|
||||
} catch {
|
||||
// Retry with force flag
|
||||
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
// First attempt failed, try with force flag
|
||||
try {
|
||||
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
bunSucceeded = true;
|
||||
} catch {
|
||||
// Bun failed completely, will try npm fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to npm if bun failed (handles npm alias packages correctly)
|
||||
if (!bunSucceeded) {
|
||||
console.error('⚠️ Bun install failed, falling back to npm...');
|
||||
console.error(' (This can happen with npm alias packages like *-cjs)');
|
||||
try {
|
||||
execSync('npm install', { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
} catch (npmError) {
|
||||
throw new Error('Both bun and npm install failed: ' + npmError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Write version marker
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Export memories matching a search query to a portable JSON format
|
||||
* Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]
|
||||
* Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, writeFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
|
||||
|
||||
interface ObservationRecord {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: string;
|
||||
title: string;
|
||||
subtitle: string | null;
|
||||
facts: string | null;
|
||||
narrative: string | null;
|
||||
concepts: string | null;
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
prompt_number: number;
|
||||
discovery_tokens: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
interface SdkSessionRecord {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
started_at: string;
|
||||
started_at_epoch: number;
|
||||
completed_at: string | null;
|
||||
completed_at_epoch: number | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface SessionSummaryRecord {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
files_read: string | null;
|
||||
files_edited: string | null;
|
||||
notes: string | null;
|
||||
prompt_number: number;
|
||||
discovery_tokens: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
interface UserPromptRecord {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
interface ExportData {
|
||||
exportedAt: string;
|
||||
exportedAtEpoch: number;
|
||||
query: string;
|
||||
project?: string;
|
||||
totalObservations: number;
|
||||
totalSessions: number;
|
||||
totalSummaries: number;
|
||||
totalPrompts: number;
|
||||
observations: ObservationRecord[];
|
||||
sessions: SdkSessionRecord[];
|
||||
summaries: SessionSummaryRecord[];
|
||||
prompts: UserPromptRecord[];
|
||||
}
|
||||
|
||||
async function exportMemories(query: string, outputFile: string, project?: string) {
|
||||
try {
|
||||
// Read port from settings
|
||||
const settings = SettingsDefaultsManager.loadFromFile(join(homedir(), '.claude-mem', 'settings.json'));
|
||||
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
|
||||
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
|
||||
|
||||
// Build query params - use format=json for raw data
|
||||
const params = new URLSearchParams({
|
||||
query,
|
||||
format: 'json',
|
||||
limit: '999999'
|
||||
});
|
||||
if (project) params.set('project', project);
|
||||
|
||||
// Unified search - gets all result types using hybrid search
|
||||
console.log('📡 Fetching all memories via hybrid search...');
|
||||
const searchResponse = await fetch(`${baseUrl}/api/search?${params.toString()}`);
|
||||
if (!searchResponse.ok) {
|
||||
throw new Error(`Failed to search: ${searchResponse.status} ${searchResponse.statusText}`);
|
||||
}
|
||||
const searchData = await searchResponse.json();
|
||||
|
||||
const observations: ObservationRecord[] = searchData.observations || [];
|
||||
const summaries: SessionSummaryRecord[] = searchData.sessions || [];
|
||||
const prompts: UserPromptRecord[] = searchData.prompts || [];
|
||||
|
||||
console.log(`✅ Found ${observations.length} observations`);
|
||||
console.log(`✅ Found ${summaries.length} session summaries`);
|
||||
console.log(`✅ Found ${prompts.length} user prompts`);
|
||||
|
||||
// Get unique SDK session IDs from observations and summaries
|
||||
const sdkSessionIds = new Set<string>();
|
||||
observations.forEach((o) => {
|
||||
if (o.sdk_session_id) sdkSessionIds.add(o.sdk_session_id);
|
||||
});
|
||||
summaries.forEach((s) => {
|
||||
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
|
||||
});
|
||||
|
||||
// Get SDK sessions metadata from database
|
||||
// (We need this because the API doesn't expose sdk_sessions table directly)
|
||||
console.log('📡 Fetching SDK sessions metadata...');
|
||||
const sessions: SdkSessionRecord[] = [];
|
||||
if (sdkSessionIds.size > 0) {
|
||||
// Read directly from database for sdk_sessions table
|
||||
const Database = (await import('better-sqlite3')).default;
|
||||
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
console.error(`❌ Database not found at: ${dbPath}`);
|
||||
console.error('💡 Has claude-mem been initialized? Try running a session first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(dbPath, { readonly: true });
|
||||
|
||||
try {
|
||||
const placeholders = Array.from(sdkSessionIds).map(() => '?').join(',');
|
||||
const sessionQuery = `
|
||||
SELECT * FROM sdk_sessions
|
||||
WHERE sdk_session_id IN (${placeholders})
|
||||
ORDER BY started_at_epoch DESC
|
||||
`;
|
||||
sessions.push(...db.prepare(sessionQuery).all(...Array.from(sdkSessionIds)));
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
console.log(`✅ Found ${sessions.length} SDK sessions`);
|
||||
|
||||
// Create export data
|
||||
const exportData: ExportData = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedAtEpoch: Date.now(),
|
||||
query,
|
||||
project,
|
||||
totalObservations: observations.length,
|
||||
totalSessions: sessions.length,
|
||||
totalSummaries: summaries.length,
|
||||
totalPrompts: prompts.length,
|
||||
observations,
|
||||
sessions,
|
||||
summaries,
|
||||
prompts
|
||||
};
|
||||
|
||||
// Write to file
|
||||
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
|
||||
|
||||
console.log(`\n📦 Export complete!`);
|
||||
console.log(`📄 Output: ${outputFile}`);
|
||||
console.log(`📊 Stats:`);
|
||||
console.log(` • ${exportData.totalObservations} observations`);
|
||||
console.log(` • ${exportData.totalSessions} sessions`);
|
||||
console.log(` • ${exportData.totalSummaries} summaries`);
|
||||
console.log(` • ${exportData.totalPrompts} prompts`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Export failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
|
||||
console.error('Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem');
|
||||
console.error(' npx tsx scripts/export-memories.ts "authentication" auth.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const [query, outputFile, ...flags] = args;
|
||||
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
|
||||
|
||||
exportMemories(query, outputFile, project);
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Import memories from a JSON export file with duplicate prevention
|
||||
* Usage: npx tsx scripts/import-memories.ts <input-file>
|
||||
* Example: npx tsx scripts/import-memories.ts windows-memories.json
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
interface ImportStats {
|
||||
sessionsImported: number;
|
||||
sessionsSkipped: number;
|
||||
summariesImported: number;
|
||||
summariesSkipped: number;
|
||||
observationsImported: number;
|
||||
observationsSkipped: number;
|
||||
promptsImported: number;
|
||||
promptsSkipped: number;
|
||||
}
|
||||
|
||||
function importMemories(inputFile: string) {
|
||||
if (!existsSync(inputFile)) {
|
||||
console.error(`❌ Input file not found: ${inputFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
console.error(`❌ Database not found at: ${dbPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and parse export file
|
||||
const exportData = JSON.parse(readFileSync(inputFile, 'utf-8'));
|
||||
|
||||
console.log(`📦 Import file: ${inputFile}`);
|
||||
console.log(`📅 Exported: ${exportData.exportedAt}`);
|
||||
console.log(`🔍 Query: "${exportData.query}"`);
|
||||
console.log(`📊 Contains:`);
|
||||
console.log(` • ${exportData.totalObservations} observations`);
|
||||
console.log(` • ${exportData.totalSessions} sessions`);
|
||||
console.log(` • ${exportData.totalSummaries} summaries`);
|
||||
console.log(` • ${exportData.totalPrompts} prompts`);
|
||||
console.log('');
|
||||
|
||||
const db = new Database(dbPath);
|
||||
const stats: ImportStats = {
|
||||
sessionsImported: 0,
|
||||
sessionsSkipped: 0,
|
||||
summariesImported: 0,
|
||||
summariesSkipped: 0,
|
||||
observationsImported: 0,
|
||||
observationsSkipped: 0,
|
||||
promptsImported: 0,
|
||||
promptsSkipped: 0
|
||||
};
|
||||
|
||||
try {
|
||||
// Prepare statements for duplicate checking
|
||||
const checkSession = db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?');
|
||||
const checkSummary = db.prepare('SELECT id FROM session_summaries WHERE sdk_session_id = ?');
|
||||
const checkObservation = db.prepare(`
|
||||
SELECT id FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
AND title = ?
|
||||
AND created_at_epoch = ?
|
||||
`);
|
||||
const checkPrompt = db.prepare(`
|
||||
SELECT id FROM user_prompts
|
||||
WHERE claude_session_id = ?
|
||||
AND prompt_number = ?
|
||||
`);
|
||||
|
||||
// Prepare insert statements
|
||||
const insertSession = db.prepare(`
|
||||
INSERT INTO sdk_sessions (
|
||||
claude_session_id, sdk_session_id, project, user_prompt,
|
||||
started_at, started_at_epoch, completed_at, completed_at_epoch,
|
||||
status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertSummary = db.prepare(`
|
||||
INSERT INTO session_summaries (
|
||||
sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, discovery_tokens, created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertObservation = db.prepare(`
|
||||
INSERT INTO observations (
|
||||
sdk_session_id, project, text, type, title, subtitle,
|
||||
facts, narrative, concepts, files_read, files_modified,
|
||||
prompt_number, discovery_tokens, created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertPrompt = db.prepare(`
|
||||
INSERT INTO user_prompts (
|
||||
claude_session_id, prompt_number, prompt_text,
|
||||
created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
// Import in transaction
|
||||
db.transaction(() => {
|
||||
// 1. Import sessions first (dependency for everything else)
|
||||
console.log('🔄 Importing sessions...');
|
||||
for (const session of exportData.sessions) {
|
||||
const exists = checkSession.get(session.claude_session_id);
|
||||
if (exists) {
|
||||
stats.sessionsSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
insertSession.run(
|
||||
session.claude_session_id,
|
||||
session.sdk_session_id,
|
||||
session.project,
|
||||
session.user_prompt,
|
||||
session.started_at,
|
||||
session.started_at_epoch,
|
||||
session.completed_at,
|
||||
session.completed_at_epoch,
|
||||
session.status
|
||||
);
|
||||
stats.sessionsImported++;
|
||||
}
|
||||
console.log(` ✅ Imported: ${stats.sessionsImported}, Skipped: ${stats.sessionsSkipped}`);
|
||||
|
||||
// 2. Import summaries (depends on sessions)
|
||||
console.log('🔄 Importing summaries...');
|
||||
for (const summary of exportData.summaries) {
|
||||
const exists = checkSummary.get(summary.sdk_session_id);
|
||||
if (exists) {
|
||||
stats.summariesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
insertSummary.run(
|
||||
summary.sdk_session_id,
|
||||
summary.project,
|
||||
summary.request,
|
||||
summary.investigated,
|
||||
summary.learned,
|
||||
summary.completed,
|
||||
summary.next_steps,
|
||||
summary.files_read,
|
||||
summary.files_edited,
|
||||
summary.notes,
|
||||
summary.prompt_number,
|
||||
summary.discovery_tokens || 0,
|
||||
summary.created_at,
|
||||
summary.created_at_epoch
|
||||
);
|
||||
stats.summariesImported++;
|
||||
}
|
||||
console.log(` ✅ Imported: ${stats.summariesImported}, Skipped: ${stats.summariesSkipped}`);
|
||||
|
||||
// 3. Import observations (depends on sessions)
|
||||
console.log('🔄 Importing observations...');
|
||||
for (const obs of exportData.observations) {
|
||||
const exists = checkObservation.get(
|
||||
obs.sdk_session_id,
|
||||
obs.title,
|
||||
obs.created_at_epoch
|
||||
);
|
||||
if (exists) {
|
||||
stats.observationsSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
insertObservation.run(
|
||||
obs.sdk_session_id,
|
||||
obs.project,
|
||||
obs.text,
|
||||
obs.type,
|
||||
obs.title,
|
||||
obs.subtitle,
|
||||
obs.facts,
|
||||
obs.narrative,
|
||||
obs.concepts,
|
||||
obs.files_read,
|
||||
obs.files_modified,
|
||||
obs.prompt_number,
|
||||
obs.discovery_tokens || 0,
|
||||
obs.created_at,
|
||||
obs.created_at_epoch
|
||||
);
|
||||
stats.observationsImported++;
|
||||
}
|
||||
console.log(` ✅ Imported: ${stats.observationsImported}, Skipped: ${stats.observationsSkipped}`);
|
||||
|
||||
// 4. Import prompts (depends on sessions)
|
||||
console.log('🔄 Importing prompts...');
|
||||
for (const prompt of exportData.prompts) {
|
||||
const exists = checkPrompt.get(
|
||||
prompt.claude_session_id,
|
||||
prompt.prompt_number
|
||||
);
|
||||
if (exists) {
|
||||
stats.promptsSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
insertPrompt.run(
|
||||
prompt.claude_session_id,
|
||||
prompt.prompt_number,
|
||||
prompt.prompt_text,
|
||||
prompt.created_at,
|
||||
prompt.created_at_epoch
|
||||
);
|
||||
stats.promptsImported++;
|
||||
}
|
||||
console.log(` ✅ Imported: ${stats.promptsImported}, Skipped: ${stats.promptsSkipped}`);
|
||||
|
||||
})();
|
||||
|
||||
console.log('\n✅ Import complete!');
|
||||
console.log('📊 Summary:');
|
||||
console.log(` Sessions: ${stats.sessionsImported} imported, ${stats.sessionsSkipped} skipped`);
|
||||
console.log(` Summaries: ${stats.summariesImported} imported, ${stats.summariesSkipped} skipped`);
|
||||
console.log(` Observations: ${stats.observationsImported} imported, ${stats.observationsSkipped} skipped`);
|
||||
console.log(` Prompts: ${stats.promptsImported} imported, ${stats.promptsSkipped} skipped`);
|
||||
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
console.error('Usage: npx tsx scripts/import-memories.ts <input-file>');
|
||||
console.error('Example: npx tsx scripts/import-memories.ts windows-memories.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [inputFile] = args;
|
||||
importMemories(inputFile);
|
||||
@@ -61,7 +61,7 @@ function getPluginVersion() {
|
||||
console.log('Syncing to marketplace...');
|
||||
try {
|
||||
execSync(
|
||||
'rsync -av --delete --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/',
|
||||
'rsync -av --delete --exclude=.git --exclude=/.mcp.json ./ ~/.claude/plugins/marketplaces/thedotmack/',
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { stdin } from "process";
|
||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
|
||||
import { handleWorkerError } from "../shared/hook-error-handler.js";
|
||||
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id: string;
|
||||
@@ -34,7 +35,7 @@ async function contextHook(input?: SessionStartInput): Promise<string> {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch context: ${response.status} ${errorText}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
|
||||
const result = await response.text();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
@@ -52,7 +53,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
if (!initResponse.ok) {
|
||||
const errorText = await initResponse.text();
|
||||
throw new Error(`Failed to initialize session: ${initResponse.status} ${errorText}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
|
||||
const initResult = await initResponse.json();
|
||||
@@ -86,7 +87,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to start SDK agent: ${response.status} ${errorText}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
} catch (error: any) {
|
||||
handleWorkerError(error);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
|
||||
|
||||
export interface PostToolUseInput {
|
||||
session_id: string;
|
||||
@@ -67,7 +68,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
logger.failure('HOOK', 'Failed to send observation', {
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Observation sent successfully', { toolName: tool_name });
|
||||
|
||||
@@ -17,6 +17,7 @@ import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { handleWorkerError } from '../shared/hook-error-handler.js';
|
||||
import { extractLastMessage } from '../shared/transcript-parser.js';
|
||||
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
@@ -72,7 +73,7 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully');
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { basename } from "path";
|
||||
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
|
||||
import { HOOK_EXIT_CODES } from "../shared/hook-constants.js";
|
||||
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
|
||||
|
||||
try {
|
||||
// Ensure worker is running
|
||||
@@ -24,7 +25,7 @@ try {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Worker error ${response.status}`);
|
||||
throw new Error(getWorkerRestartInstructions({ includeSkillFallback: true }));
|
||||
}
|
||||
|
||||
const output = await response.text();
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
|
||||
import { getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||
|
||||
/**
|
||||
* Worker HTTP API configuration
|
||||
*/
|
||||
const WORKER_PORT = getWorkerPort();
|
||||
const WORKER_BASE_URL = `http://localhost:${WORKER_PORT}`;
|
||||
const WORKER_HOST = getWorkerHost();
|
||||
const WORKER_BASE_URL = `http://${WORKER_HOST}:${WORKER_PORT}`;
|
||||
|
||||
/**
|
||||
* Map tool names to Worker HTTP endpoints
|
||||
|
||||
@@ -811,26 +811,72 @@ export class SessionStore {
|
||||
*/
|
||||
getObservationsByIds(
|
||||
ids: number[],
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string; type?: string | string[]; concepts?: string | string[]; files?: string | string[] } = {}
|
||||
): ObservationRecord[] {
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const { orderBy = 'date_desc', limit } = options;
|
||||
const { orderBy = 'date_desc', limit, project, type, concepts, files } = options;
|
||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||
|
||||
// Build placeholders for IN clause
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const params: any[] = [...ids];
|
||||
const additionalConditions: string[] = [];
|
||||
|
||||
// Apply project filter
|
||||
if (project) {
|
||||
additionalConditions.push('project = ?');
|
||||
params.push(project);
|
||||
}
|
||||
|
||||
// Apply type filter
|
||||
if (type) {
|
||||
if (Array.isArray(type)) {
|
||||
const typePlaceholders = type.map(() => '?').join(',');
|
||||
additionalConditions.push(`type IN (${typePlaceholders})`);
|
||||
params.push(...type);
|
||||
} else {
|
||||
additionalConditions.push('type = ?');
|
||||
params.push(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply concepts filter
|
||||
if (concepts) {
|
||||
const conceptsList = Array.isArray(concepts) ? concepts : [concepts];
|
||||
const conceptConditions = conceptsList.map(() =>
|
||||
'EXISTS (SELECT 1 FROM json_each(concepts) WHERE value = ?)'
|
||||
);
|
||||
params.push(...conceptsList);
|
||||
additionalConditions.push(`(${conceptConditions.join(' OR ')})`);
|
||||
}
|
||||
|
||||
// Apply files filter
|
||||
if (files) {
|
||||
const filesList = Array.isArray(files) ? files : [files];
|
||||
const fileConditions = filesList.map(() => {
|
||||
return '(EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?))';
|
||||
});
|
||||
filesList.forEach(file => {
|
||||
params.push(`%${file}%`, `%${file}%`);
|
||||
});
|
||||
additionalConditions.push(`(${fileConditions.join(' OR ')})`);
|
||||
}
|
||||
|
||||
const whereClause = additionalConditions.length > 0
|
||||
? `WHERE id IN (${placeholders}) AND ${additionalConditions.join(' AND ')}`
|
||||
: `WHERE id IN (${placeholders})`;
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${placeholders})
|
||||
${whereClause}
|
||||
ORDER BY created_at_epoch ${orderClause}
|
||||
${limitClause}
|
||||
`);
|
||||
|
||||
return stmt.all(...ids) as ObservationRecord[];
|
||||
return stmt.all(...params) as ObservationRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1353,23 +1399,30 @@ export class SessionStore {
|
||||
*/
|
||||
getSessionSummariesByIds(
|
||||
ids: number[],
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
|
||||
): SessionSummaryRecord[] {
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const { orderBy = 'date_desc', limit } = options;
|
||||
const { orderBy = 'date_desc', limit, project } = options;
|
||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const params: any[] = [...ids];
|
||||
|
||||
// Apply project filter
|
||||
const whereClause = project
|
||||
? `WHERE id IN (${placeholders}) AND project = ?`
|
||||
: `WHERE id IN (${placeholders})`;
|
||||
if (project) params.push(project);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${placeholders})
|
||||
${whereClause}
|
||||
ORDER BY created_at_epoch ${orderClause}
|
||||
${limitClause}
|
||||
`);
|
||||
|
||||
return stmt.all(...ids) as SessionSummaryRecord[];
|
||||
return stmt.all(...params) as SessionSummaryRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1378,14 +1431,19 @@ export class SessionStore {
|
||||
*/
|
||||
getUserPromptsByIds(
|
||||
ids: number[],
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
|
||||
): UserPromptRecord[] {
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
const { orderBy = 'date_desc', limit } = options;
|
||||
const { orderBy = 'date_desc', limit, project } = options;
|
||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const params: any[] = [...ids];
|
||||
|
||||
// Apply project filter
|
||||
const projectFilter = project ? 'AND s.project = ?' : '';
|
||||
if (project) params.push(project);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
@@ -1394,12 +1452,12 @@ export class SessionStore {
|
||||
s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.id IN (${placeholders})
|
||||
WHERE up.id IN (${placeholders}) ${projectFilter}
|
||||
ORDER BY up.created_at_epoch ${orderClause}
|
||||
${limitClause}
|
||||
`);
|
||||
|
||||
return stmt.all(...ids) as UserPromptRecord[];
|
||||
return stmt.all(...params) as UserPromptRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,30 +6,6 @@
|
||||
* See src/services/worker/README.md for architecture details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Windows terminal window fix for MCP SDK (vX.Y.Z):
|
||||
* The MCP SDK checks `process.type === 'renderer'` (Electron detection) before setting windowsHide.
|
||||
* By setting process.type, the SDK's isElectron() check becomes truthy on Windows, hiding
|
||||
* terminal windows when spawning uvx/python processes for Chroma MCP server.
|
||||
* The type is sometimes not present resulting in the check being false. Setting it like this fixes it.
|
||||
*
|
||||
* TODO: Remove this workaround once MCP SDK exposes a config for windowsHide or fixes detection.
|
||||
* See: https://github.com/modelcontextprotocol/sdk/issues/XXX
|
||||
*/
|
||||
function applyWindowsHideWorkaroundIfNeeded() {
|
||||
if (process.platform === 'win32' && !process.type) {
|
||||
// Optionally, check MCP SDK version here if available
|
||||
// Log a warning so this is visible in logs
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'[worker-service] Applying MCP SDK windowsHide workaround: setting process.type = "renderer". ' +
|
||||
'This is a fragile hack. Remove when MCP SDK is fixed. See code comments for details.'
|
||||
);
|
||||
(process as any).type = 'renderer';
|
||||
}
|
||||
}
|
||||
|
||||
applyWindowsHideWorkaroundIfNeeded();
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
|
||||
@@ -166,10 +166,10 @@ export class SearchManager {
|
||||
observations = this.sessionStore.getObservationsByIds(obsIds, obsOptions);
|
||||
}
|
||||
if (sessionIds.length > 0) {
|
||||
sessions = this.sessionStore.getSessionSummariesByIds(sessionIds, { orderBy: 'date_desc', limit: options.limit });
|
||||
sessions = this.sessionStore.getSessionSummariesByIds(sessionIds, { orderBy: 'date_desc', limit: options.limit, project: options.project });
|
||||
}
|
||||
if (promptIds.length > 0) {
|
||||
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
|
||||
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit, project: options.project });
|
||||
}
|
||||
|
||||
logger.debug('SEARCH', 'Hydrated results from SQLite', { observations: observations.length, sessions: sessions.length, prompts: prompts.length });
|
||||
@@ -198,6 +198,13 @@ export class SearchManager {
|
||||
const totalResults = observations.length + sessions.length + prompts.length;
|
||||
|
||||
if (totalResults === 0) {
|
||||
if (format === 'json') {
|
||||
return {
|
||||
observations: [],
|
||||
sessions: [],
|
||||
prompts: []
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
@@ -230,6 +237,15 @@ export class SearchManager {
|
||||
const limitedResults = allResults.slice(0, options.limit || 20);
|
||||
|
||||
// Format based on requested format
|
||||
if (format === 'json') {
|
||||
// Raw JSON format for exports
|
||||
return {
|
||||
observations,
|
||||
sessions,
|
||||
prompts
|
||||
};
|
||||
}
|
||||
|
||||
let combinedText: string;
|
||||
if (format === 'index') {
|
||||
const header = `Found ${totalResults} result(s) matching "${query}" (${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts):\n\n`;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getWorkerRestartInstructions } from '../utils/error-messages.js';
|
||||
|
||||
/**
|
||||
* Handles fetch errors by providing user-friendly messages for connection issues
|
||||
* @throws Error with helpful message if worker is unreachable, re-throws original otherwise
|
||||
@@ -8,9 +10,7 @@ export function handleWorkerError(error: any): never {
|
||||
error.name === 'TimeoutError' ||
|
||||
error.message?.includes('fetch failed') ||
|
||||
error.message?.includes('Unable to connect')) {
|
||||
throw new Error(
|
||||
"There's a problem with the worker. Try: npm run worker:restart"
|
||||
);
|
||||
throw new Error(getWorkerRestartInstructions());
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { logger } from "../utils/logger.js";
|
||||
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
|
||||
import { ProcessManager } from "../services/process/ProcessManager.js";
|
||||
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
|
||||
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
|
||||
|
||||
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
||||
|
||||
@@ -197,9 +198,10 @@ export async function ensureWorkerRunning(): Promise<void> {
|
||||
if (!started) {
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service failed to start on port ${port}.\n\n` +
|
||||
`To start manually, run: npm run worker:start\n` +
|
||||
`If already running, try: npm run worker:restart`
|
||||
getWorkerRestartInstructions({
|
||||
port,
|
||||
customPrefix: `Worker service failed to start on port ${port}.`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,7 +219,9 @@ export async function ensureWorkerRunning(): Promise<void> {
|
||||
const port = getWorkerPort();
|
||||
logger.error('SYSTEM', 'Worker started but not responding to health checks');
|
||||
throw new Error(
|
||||
`Worker service started but is not responding on port ${port}.\n\n` +
|
||||
`Try: npm run worker:restart`
|
||||
getWorkerRestartInstructions({
|
||||
port,
|
||||
customPrefix: `Worker service started but is not responding on port ${port}.`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Platform-aware error message generator for worker connection failures
|
||||
*/
|
||||
|
||||
export interface WorkerErrorMessageOptions {
|
||||
port?: number;
|
||||
includeSkillFallback?: boolean;
|
||||
customPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate platform-specific worker restart instructions
|
||||
* @param options Configuration for error message generation
|
||||
* @returns Formatted error message with platform-specific paths and commands
|
||||
*/
|
||||
export function getWorkerRestartInstructions(
|
||||
options: WorkerErrorMessageOptions = {}
|
||||
): string {
|
||||
const {
|
||||
port,
|
||||
includeSkillFallback = false,
|
||||
customPrefix
|
||||
} = options;
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Platform-specific directory paths
|
||||
const pluginDir = isWindows
|
||||
? '%USERPROFILE%\\.claude\\plugins\\marketplaces\\thedotmack'
|
||||
: '~/.claude/plugins/marketplaces/thedotmack';
|
||||
|
||||
// Platform-specific terminal name
|
||||
const terminal = isWindows
|
||||
? 'Command Prompt or PowerShell'
|
||||
: 'Terminal';
|
||||
|
||||
// Build error message
|
||||
const prefix = customPrefix || 'Worker service connection failed.';
|
||||
const portInfo = port ? ` (port ${port})` : '';
|
||||
|
||||
let message = `${prefix}${portInfo}\n\n`;
|
||||
message += `To restart the worker:\n`;
|
||||
message += `1. Exit Claude Code completely\n`;
|
||||
message += `2. Open ${terminal}\n`;
|
||||
message += `3. Navigate to: ${pluginDir}\n`;
|
||||
message += `4. Run: npm run worker:restart`;
|
||||
|
||||
if (includeSkillFallback) {
|
||||
message += `\n\nIf that doesn't work, try: /troubleshoot`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
Reference in New Issue
Block a user