Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9370a915c | |||
| 6d4a4819de | |||
| 2681a2d251 | |||
| c2eefe3578 | |||
| dd5e2e57dd | |||
| 266076da98 | |||
| a0b4381dc8 | |||
| 4904d9c531 | |||
| 4c44a65877 | |||
| f6b310126c | |||
| 77220a76bf | |||
| 42ed414a4c | |||
| 0185d765ce | |||
| 12c2ecce06 | |||
| bb0508d639 | |||
| f00ef33f86 | |||
| c270bd3177 | |||
| 0836a97845 | |||
| 19e285a209 | |||
| ba877214c1 | |||
| 3d4baefac2 | |||
| 453b7857b8 | |||
| c28417af00 | |||
| 2f08db3c01 | |||
| 672cb5d203 | |||
| cbe492dde3 | |||
| 0fb6f3cf4e | |||
| 5cd68f4a96 | |||
| 10a8598aac | |||
| 5b338ba34e | |||
| 4e7ed75fa9 | |||
| a8b84fa7b6 | |||
| 73be8f7a63 | |||
| fa93f2c1e2 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.9",
|
||||
"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: 'feature-request'
|
||||
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,131 @@
|
||||
name: Convert Feature Requests to Discussions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to convert to discussion'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
convert:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on labeled event if the label is 'feature-request', or always run on workflow_dispatch
|
||||
if: |
|
||||
(github.event_name == 'issues' && github.event.label.name == 'feature-request') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Get issue details and create discussion
|
||||
id: discussion
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Get issue details
|
||||
let issue;
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
const { data } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.inputs.issue_number
|
||||
});
|
||||
issue = data;
|
||||
} else {
|
||||
issue = context.payload.issue;
|
||||
}
|
||||
|
||||
console.log(`Processing issue #${issue.number}: ${issue.title}`);
|
||||
|
||||
// Format the discussion body with a reference to the original issue
|
||||
const discussionBody = `> Originally posted as issue #${issue.number} by @${issue.user.login}\n> ${issue.html_url}\n\n${issue.body || 'No description provided.'}`;
|
||||
|
||||
const mutation = `
|
||||
mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
|
||||
createDiscussion(input: {
|
||||
repositoryId: $repositoryId
|
||||
categoryId: $categoryId
|
||||
title: $title
|
||||
body: $body
|
||||
}) {
|
||||
discussion {
|
||||
url
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
repositoryId: 'R_kgDOPng1Jw',
|
||||
categoryId: 'DIC_kwDOPng1J84Cw86z',
|
||||
title: issue.title,
|
||||
body: discussionBody
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await github.graphql(mutation, variables);
|
||||
const discussionUrl = result.createDiscussion.discussion.url;
|
||||
const discussionNumber = result.createDiscussion.discussion.number;
|
||||
|
||||
core.setOutput('url', discussionUrl);
|
||||
core.setOutput('number', discussionNumber);
|
||||
core.setOutput('issue_number', issue.number);
|
||||
|
||||
console.log(`Created discussion #${discussionNumber}: ${discussionUrl}`);
|
||||
return { discussionUrl, discussionNumber, issueNumber: issue.number };
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to create discussion: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = ${{ steps.discussion.outputs.issue_number }};
|
||||
const discussionUrl = '${{ steps.discussion.outputs.url }}';
|
||||
|
||||
const comment = `This feature request has been moved to [Discussions](${discussionUrl}) to keep bug reports separate from feature ideas.\n\nPlease continue the conversation there - we'd love to hear your thoughts!`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: comment
|
||||
});
|
||||
|
||||
console.log(`Added comment to issue #${issueNumber}`);
|
||||
|
||||
- name: Close and lock issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = ${{ steps.discussion.outputs.issue_number }};
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
console.log(`Closed issue #${issueNumber}`);
|
||||
|
||||
// Lock the issue
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
lock_reason: 'resolved'
|
||||
});
|
||||
|
||||
console.log(`Locked issue #${issueNumber}`);
|
||||
@@ -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": {}
|
||||
}
|
||||
|
||||
+107
@@ -4,6 +4,113 @@ 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.8] - 2025-12-13
|
||||
|
||||
## Memory Export/Import Scripts
|
||||
|
||||
Added portable memory export and import functionality with automatic duplicate prevention.
|
||||
|
||||
### New Features
|
||||
- **Export memories** to JSON format with search filtering and project-based filtering
|
||||
- **Import memories** with automatic duplicate detection via composite keys
|
||||
- Complete documentation in docs/public/usage/export-import.mdx
|
||||
|
||||
### Use Cases
|
||||
- Share memory sets between developers working on the same project
|
||||
- Backup and restore specific project memories
|
||||
- Collaborate on domain knowledge across teams
|
||||
- Migrate memories between different claude-mem installations
|
||||
|
||||
### Example Usage
|
||||
```bash
|
||||
# Export Windows-related memories
|
||||
npx tsx scripts/export-memories.ts "windows" windows-work.json
|
||||
|
||||
# Export only claude-mem project memories
|
||||
npx tsx scripts/export-memories.ts "bugfix" fixes.json --project=claude-mem
|
||||
|
||||
# Import memories (with automatic duplicate prevention)
|
||||
npx tsx scripts/import-memories.ts windows-work.json
|
||||
```
|
||||
|
||||
### Technical Improvements
|
||||
- Fixed JSON format response in /api/search endpoint for consistent structure
|
||||
- Enhanced project filtering in ChromaDB hybrid search result hydration
|
||||
- Duplicate detection using composite keys (session ID + title + timestamp)
|
||||
|
||||
## [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
|
||||
|
||||
### Windows Installation
|
||||
- Fixed Bun PATH detection on Windows after fresh install
|
||||
- Added fallback to check common install paths before PATH reload
|
||||
- Improved smart-install.js to use full Bun path when not in PATH
|
||||
- Added proper path quoting for Windows usernames with spaces
|
||||
|
||||
### Worker Startup
|
||||
- Fixed worker connection failures in Stop hook
|
||||
- Added health check retry loop (5 attempts, 500ms intervals)
|
||||
- Worker now waits up to 2.5s for responsiveness before returning
|
||||
- Improved error detection for Bun's ConnectionRefused error format
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.1...v7.1.2
|
||||
|
||||
## [7.1.1] - 2025-12-13
|
||||
|
||||
## 🚨 Critical 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.2
|
||||
**Current Version**: 7.1.9
|
||||
|
||||
## 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.2",
|
||||
"version": "7.1.9",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.9",
|
||||
"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.1",
|
||||
"version": "7.1.9",
|
||||
"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
@@ -24,18 +24,56 @@ function isBunInstalled() {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
return result.status === 0;
|
||||
if (result.status === 0) return true;
|
||||
} catch {
|
||||
return false;
|
||||
// PATH check failed, try common installation paths
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
|
||||
return bunPaths.some(existsSync);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bun executable path (from PATH or common install locations)
|
||||
*/
|
||||
function getBunPath() {
|
||||
// Try PATH first
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return 'bun';
|
||||
} catch {
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) return bunPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bun version if installed
|
||||
*/
|
||||
function getBunVersion() {
|
||||
const bunPath = getBunPath();
|
||||
if (!bunPath) return null;
|
||||
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
const result = spawnSync(bunPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
@@ -56,10 +94,17 @@ function isUvInstalled() {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
return result.status === 0;
|
||||
if (result.status === 0) return true;
|
||||
} catch {
|
||||
return false;
|
||||
// PATH check failed, try common installation paths
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const uvPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
|
||||
|
||||
return uvPaths.some(existsSync);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,15 +268,46 @@ 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();
|
||||
if (!bunPath) {
|
||||
throw new Error('Bun executable not found');
|
||||
}
|
||||
|
||||
console.error('📦 Installing dependencies with Bun...');
|
||||
|
||||
// Quote path for Windows paths with spaces
|
||||
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
|
||||
|
||||
let bunSucceeded = false;
|
||||
try {
|
||||
execSync('bun install', { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
bunSucceeded = true;
|
||||
} catch {
|
||||
// Retry with force flag
|
||||
execSync('bun 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);
|
||||
+52
-128
@@ -14,28 +14,14 @@ const ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack')
|
||||
const MARKER = join(ROOT, '.install-version');
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
/**
|
||||
* Check if Bun is installed and accessible
|
||||
*/
|
||||
function isBunInstalled() {
|
||||
try {
|
||||
const result = spawnSync('bun', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return true;
|
||||
} catch {
|
||||
// PATH check failed, try common installation paths
|
||||
}
|
||||
// Common installation paths (handles fresh installs before PATH reload)
|
||||
const BUN_COMMON_PATHS = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
|
||||
return bunPaths.some(existsSync);
|
||||
}
|
||||
const UV_COMMON_PATHS = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
|
||||
|
||||
/**
|
||||
* Get the Bun executable path (from PATH or common install locations)
|
||||
@@ -54,15 +40,14 @@ function getBunPath() {
|
||||
}
|
||||
|
||||
// Check common installation paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
return BUN_COMMON_PATHS.find(existsSync) || null;
|
||||
}
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) return bunPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* Check if Bun is installed and accessible
|
||||
*/
|
||||
function isBunInstalled() {
|
||||
return getBunPath() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,34 +70,41 @@ function getBunVersion() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if uv is installed and accessible
|
||||
* Get the uv executable path (from PATH or common install locations)
|
||||
*/
|
||||
function isUvInstalled() {
|
||||
function getUvPath() {
|
||||
// Try PATH first
|
||||
try {
|
||||
const result = spawnSync('uv', ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
});
|
||||
if (result.status === 0) return true;
|
||||
if (result.status === 0) return 'uv';
|
||||
} catch {
|
||||
// PATH check failed, try common installation paths
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
// Check common installation paths (handles fresh installs before PATH reload)
|
||||
const uvPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
|
||||
// Check common installation paths
|
||||
return UV_COMMON_PATHS.find(existsSync) || null;
|
||||
}
|
||||
|
||||
return uvPaths.some(existsSync);
|
||||
/**
|
||||
* Check if uv is installed and accessible
|
||||
*/
|
||||
function isUvInstalled() {
|
||||
return getUvPath() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uv version if installed
|
||||
*/
|
||||
function getUvVersion() {
|
||||
const uvPath = getUvPath();
|
||||
if (!uvPath) return null;
|
||||
|
||||
try {
|
||||
const result = spawnSync('uv', ['--version'], {
|
||||
const result = spawnSync(uvPath, ['--version'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: IS_WINDOWS
|
||||
@@ -131,14 +123,12 @@ function installBun() {
|
||||
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: Use PowerShell installer
|
||||
console.error(' Installing via PowerShell...');
|
||||
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
} else {
|
||||
// Unix/macOS: Use curl installer
|
||||
console.error(' Installing via curl...');
|
||||
execSync('curl -fsSL https://bun.sh/install | bash', {
|
||||
stdio: 'inherit',
|
||||
@@ -146,35 +136,17 @@ function installBun() {
|
||||
});
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (isBunInstalled()) {
|
||||
const version = getBunVersion();
|
||||
console.error(`✅ Bun ${version} installed successfully`);
|
||||
return true;
|
||||
} else {
|
||||
// Bun may be installed but not in PATH yet for this session
|
||||
// Try common installation paths
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun'];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) {
|
||||
console.error(`✅ Bun installed at ${bunPath}`);
|
||||
console.error('⚠️ Please restart your terminal or add Bun to PATH:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(` $env:Path += ";${join(homedir(), '.bun', 'bin')}"`);
|
||||
} else {
|
||||
console.error(` export PATH="$HOME/.bun/bin:$PATH"`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Bun installation completed but binary not found');
|
||||
if (!isBunInstalled()) {
|
||||
throw new Error(
|
||||
'Bun installation completed but binary not found. ' +
|
||||
'Please restart your terminal and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
const version = getBunVersion();
|
||||
console.error(`✅ Bun ${version} installed successfully`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to install Bun automatically');
|
||||
console.error('❌ Failed to install Bun');
|
||||
console.error(' Please install manually:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(' - winget install Oven-sh.Bun');
|
||||
@@ -196,14 +168,12 @@ function installUv() {
|
||||
|
||||
try {
|
||||
if (IS_WINDOWS) {
|
||||
// Windows: Use PowerShell installer
|
||||
console.error(' Installing via PowerShell...');
|
||||
execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
} else {
|
||||
// Unix/macOS: Use curl installer
|
||||
console.error(' Installing via curl...');
|
||||
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
|
||||
stdio: 'inherit',
|
||||
@@ -211,35 +181,17 @@ function installUv() {
|
||||
});
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (isUvInstalled()) {
|
||||
const version = getUvVersion();
|
||||
console.error(`✅ uv ${version} installed successfully`);
|
||||
return true;
|
||||
} else {
|
||||
// uv may be installed but not in PATH yet for this session
|
||||
// Try common installation paths
|
||||
const uvPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
||||
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv'];
|
||||
|
||||
for (const uvPath of uvPaths) {
|
||||
if (existsSync(uvPath)) {
|
||||
console.error(`✅ uv installed at ${uvPath}`);
|
||||
console.error('⚠️ Please restart your terminal or add uv to PATH:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(` $env:Path += ";${join(homedir(), '.local', 'bin')}"`);
|
||||
} else {
|
||||
console.error(` export PATH="$HOME/.local/bin:$PATH"`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('uv installation completed but binary not found');
|
||||
if (!isUvInstalled()) {
|
||||
throw new Error(
|
||||
'uv installation completed but binary not found. ' +
|
||||
'Please restart your terminal and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
const version = getUvVersion();
|
||||
console.error(`✅ uv ${version} installed successfully`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to install uv automatically');
|
||||
console.error('❌ Failed to install uv');
|
||||
console.error(' Please install manually:');
|
||||
if (IS_WINDOWS) {
|
||||
console.error(' - winget install astral-sh.uv');
|
||||
@@ -281,12 +233,7 @@ function installDeps() {
|
||||
// Quote path for Windows paths with spaces
|
||||
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
|
||||
|
||||
try {
|
||||
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
} catch {
|
||||
// Retry with force flag
|
||||
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
}
|
||||
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS });
|
||||
|
||||
// Write version marker
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
@@ -300,31 +247,8 @@ function installDeps() {
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
// Step 1: Ensure Bun is installed (REQUIRED)
|
||||
if (!isBunInstalled()) {
|
||||
installBun();
|
||||
|
||||
// Re-check after installation
|
||||
if (!isBunInstalled()) {
|
||||
console.error('❌ Bun is required but not available in PATH');
|
||||
console.error(' Please restart your terminal after installation');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Ensure uv is installed (REQUIRED for vector search)
|
||||
if (!isUvInstalled()) {
|
||||
installUv();
|
||||
|
||||
// Re-check after installation
|
||||
if (!isUvInstalled()) {
|
||||
console.error('❌ uv is required but not available in PATH');
|
||||
console.error(' Please restart your terminal after installation');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Install dependencies if needed
|
||||
if (!isBunInstalled()) installBun();
|
||||
if (!isUvInstalled()) installUv();
|
||||
if (needsInstall()) {
|
||||
installDeps();
|
||||
console.error('✅ Dependencies installed');
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,6 +73,7 @@ interface StoredUserPrompt {
|
||||
|
||||
export class ChromaSync {
|
||||
private client: Client | null = null;
|
||||
private transport: StdioClientTransport | null = null;
|
||||
private connected: boolean = false;
|
||||
private project: string;
|
||||
private collectionName: string;
|
||||
@@ -101,7 +102,7 @@ export class ChromaSync {
|
||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
||||
const transport = new StdioClientTransport({
|
||||
this.transport = new StdioClientTransport({
|
||||
command: 'uvx',
|
||||
args: [
|
||||
'--python', pythonVersion,
|
||||
@@ -119,7 +120,7 @@ export class ChromaSync {
|
||||
capabilities: {}
|
||||
});
|
||||
|
||||
await this.client.connect(transport);
|
||||
await this.client.connect(this.transport);
|
||||
this.connected = true;
|
||||
|
||||
logger.info('CHROMA_SYNC', 'Connected to Chroma MCP server', { project: this.project });
|
||||
@@ -815,14 +816,38 @@ export class ChromaSync {
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Chroma client connection
|
||||
* Close the Chroma client connection and cleanup subprocess
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.client && this.connected) {
|
||||
await this.client.close();
|
||||
if (!this.connected && !this.client && !this.transport) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Close client first
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.close();
|
||||
} catch (error) {
|
||||
logger.warn('CHROMA_SYNC', 'Error closing Chroma client', { project: this.project }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly close transport to kill subprocess
|
||||
if (this.transport) {
|
||||
try {
|
||||
await this.transport.close();
|
||||
} catch (error) {
|
||||
logger.warn('CHROMA_SYNC', 'Error closing transport', { project: this.project }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('CHROMA_SYNC', 'Chroma client and subprocess closed', { project: this.project });
|
||||
} finally {
|
||||
// Always reset state, even if errors occurred
|
||||
this.connected = false;
|
||||
this.client = null;
|
||||
logger.info('CHROMA_SYNC', 'Chroma client closed', { project: this.project });
|
||||
this.transport = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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