Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58f4053a61 | |||
| a08e5068c8 | |||
| 9f1b653f2f | |||
| c5d89142b9 | |||
| b44d4702b1 | |||
| 4988b4b317 | |||
| c5e68a17c8 | |||
| f35764aa14 | |||
| 601bded789 | |||
| c231e8d076 | |||
| b62e93577c | |||
| 0787e47b1a | |||
| f9843fe593 | |||
| 5b772ee768 | |||
| baabf14bfa | |||
| c126a7083f | |||
| f1170512d6 | |||
| d64939c379 | |||
| 97d8bd3e62 | |||
| 6cd904a81c | |||
| 74c8afd0e0 | |||
| 02444392da | |||
| d175f1759c | |||
| d55db524f0 | |||
| 7d44fdb289 | |||
| 812de2940d | |||
| 95edf31c14 | |||
| a9ae89a198 | |||
| 047914d087 | |||
| bdf79a439b | |||
| 99b6b85d67 | |||
| 798dec972e | |||
| 286343fef6 | |||
| 9285826547 | |||
| ce3b3733fa | |||
| cf1c966409 | |||
| 02fef487e7 | |||
| 20d45006c0 | |||
| 4f1cd309fd | |||
| c46e4a341a | |||
| 60d5f8fbf1 | |||
| c0778bef00 | |||
| 3cbc041c8b | |||
| 0f96476987 | |||
| cd6f883020 | |||
| 9fb7383ab3 | |||
| e8e7fc81af | |||
| e3283c2a1d | |||
| 581e940659 | |||
| 915dbc1aa9 | |||
| 5a84198529 | |||
| f5b25e8fc4 | |||
| 7d1e6af5c5 | |||
| 8b4cc4f6bf | |||
| 68290a9121 | |||
| ab5d78717f | |||
| cb4aea57a8 | |||
| 7bdf6dbfe1 | |||
| 1c9da73d5f | |||
| 5f7aa0710e | |||
| 39fedfc5fc | |||
| ecb8b39f6d | |||
| fe0902b48f | |||
| 4ab9739e4c |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.5.0",
|
||||
"version": "6.2.0",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{
|
||||
"env": {}
|
||||
"env": {},
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Read(./package-lock.json)",
|
||||
"Read(./node_modules/**)",
|
||||
"Read(./.DS_Store)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: version-bump
|
||||
description: Manage semantic version updates for claude-mem project. Handles patch, minor, and major version increments following semantic versioning. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md version number (NOT version history). Creates git tags.
|
||||
description: Manage semantic version updates for claude-mem project. Handles patch, minor, and major version increments following semantic versioning. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md version number (NOT version history). Creates git tags and GitHub releases. Auto-generates CHANGELOG.md from releases.
|
||||
---
|
||||
|
||||
# Version Bump Skill
|
||||
@@ -42,6 +42,7 @@ See [operations/workflow.md](operations/workflow.md) for detailed step-by-step p
|
||||
6. Build and test
|
||||
7. Commit and create git tag
|
||||
8. Push and create GitHub release
|
||||
9. Generate CHANGELOG.md from releases and commit
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
@@ -56,6 +57,7 @@ See [operations/scenarios.md](operations/scenarios.md) for examples:
|
||||
- Update ALL FOUR files with matching version numbers
|
||||
- Create git tag with format `vX.Y.Z`
|
||||
- Create GitHub release from the tag
|
||||
- Generate CHANGELOG.md from releases after creating release
|
||||
- Ask user if version type is unclear
|
||||
|
||||
**NEVER:**
|
||||
@@ -73,6 +75,7 @@ Before considering the task complete:
|
||||
- [ ] Git tag created (format: vX.Y.Z)
|
||||
- [ ] Commit and tags pushed to remote
|
||||
- [ ] GitHub release created from the tag
|
||||
- [ ] CHANGELOG.md generated and committed
|
||||
- [ ] CLAUDE.md: ONLY line 9 updated (version number), NOT version history
|
||||
|
||||
## Reference Commands
|
||||
|
||||
@@ -187,6 +187,29 @@ gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
|
||||
|
||||
**IMPORTANT:** Always create the GitHub release immediately after pushing the tag. This makes the release discoverable to users and triggers any automated workflows.
|
||||
|
||||
## Step 10: Generate CHANGELOG
|
||||
|
||||
After creating the GitHub release, regenerate CHANGELOG.md from all releases:
|
||||
|
||||
```bash
|
||||
# Generate CHANGELOG.md from all GitHub releases
|
||||
npm run changelog:generate
|
||||
|
||||
# Review the generated changelog
|
||||
git diff CHANGELOG.md
|
||||
|
||||
# Commit and push the updated changelog
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Update CHANGELOG.md for vX.Y.Z release"
|
||||
git push
|
||||
```
|
||||
|
||||
**Why this step:**
|
||||
- CHANGELOG.md is auto-generated from GitHub releases
|
||||
- Keeps the changelog in sync with release notes
|
||||
- No manual editing required
|
||||
- Single source of truth: GitHub releases
|
||||
|
||||
## Verification
|
||||
|
||||
After completing all steps, verify:
|
||||
|
||||
@@ -1,470 +1,63 @@
|
||||
/* To @claude: be vigilant about only leaving evergreen context in this file, claude-mem handles working context separately. */
|
||||
|
||||
# Claude-Mem: AI Development Instructions
|
||||
|
||||
## What This Project Is
|
||||
|
||||
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.
|
||||
|
||||
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
|
||||
**Current Version**: 6.2.0
|
||||
|
||||
**Current Version**: 5.5.0
|
||||
## Architecture
|
||||
|
||||
## Critical Architecture Knowledge
|
||||
**5 Lifecycle Hooks**: SessionStart → UserPromptSubmit → PostToolUse → Summary → SessionEnd
|
||||
|
||||
### The Lifecycle Flow
|
||||
**Hooks** (`src/hooks/*.ts`) - TypeScript → ESM, built to `plugin/scripts/*-hook.js`
|
||||
|
||||
1. **SessionStart** → `context-hook.ts` runs
|
||||
- Smart installer checks dependencies (cached, only runs on version changes)
|
||||
- Starts PM2 worker if not healthy
|
||||
- Injects context from previous sessions (configurable observation count)
|
||||
**Worker Service** (`src/services/worker-service.ts`) - Express API on port 37777, PM2-managed, handles AI processing asynchronously
|
||||
|
||||
2. **UserPromptSubmit** → `new-hook.ts` runs
|
||||
- Creates session record in SQLite
|
||||
- Saves raw user prompt for FTS5 search
|
||||
**Database** (`src/services/sqlite/`) - SQLite3 at `~/.claude-mem/claude-mem.db` with FTS5 full-text search
|
||||
|
||||
3. **PostToolUse** → `save-hook.ts` runs
|
||||
- Captures your tool executions
|
||||
- Sends to worker service for AI compression
|
||||
**Search Skill** (`plugin/skills/mem-search/SKILL.md`) - HTTP API for searching past work, auto-invoked when users ask about history
|
||||
|
||||
4. **Summary** → Summary hook generates session summaries
|
||||
**Chroma** (`src/services/sync/ChromaSync.ts`) - Vector embeddings for semantic search
|
||||
|
||||
5. **SessionEnd** → `cleanup-hook.ts` runs
|
||||
- Marks session complete (graceful, not DELETE)
|
||||
- Skips on `/clear` to preserve ongoing sessions
|
||||
**Viewer UI** (`src/ui/viewer/`) - React interface at http://localhost:37777, built to `plugin/ui/viewer.html`
|
||||
|
||||
### Key Components
|
||||
## Build Commands
|
||||
|
||||
**Hooks** (`src/hooks/*.ts`)
|
||||
- Built to `plugin/scripts/*-hook.js` (ESM format)
|
||||
- Must output valid JSON to `hookSpecificOutput` field
|
||||
- Called by Claude Code lifecycle events
|
||||
**Hooks only**: `npm run build && npm run sync-marketplace`
|
||||
|
||||
**Worker Service** (`src/services/worker-service.ts`)
|
||||
- Express.js API on port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- Managed by PM2 (auto-started by hooks)
|
||||
- Built to `plugin/worker-service.cjs` (CJS format)
|
||||
- Handles AI processing asynchronously to avoid hook timeouts
|
||||
**Worker changes**: `npm run build && npm run sync-marketplace && npm run worker:restart`
|
||||
|
||||
**Database** (`src/services/sqlite/`)
|
||||
- SQLite3 with better-sqlite3 (NOT bun:sqlite - that's legacy)
|
||||
- Location: `~/.claude-mem/claude-mem.db`
|
||||
- FTS5 virtual tables for full-text search
|
||||
- `SessionStore` = CRUD, `SessionSearch` = FTS5 queries
|
||||
**Skills only**: `npm run sync-marketplace`
|
||||
|
||||
**Search Skill** (`plugin/skills/search/SKILL.md`)
|
||||
- Provides access to all search functionality via HTTP API + skill
|
||||
- Auto-invoked when users ask about past work, decisions, or history
|
||||
- Uses HTTP endpoints instead of MCP tools (~2,250 token savings per session)
|
||||
- 10 search operations: observations, sessions, prompts, by-type, by-file, by-concept, timelines, etc.
|
||||
**Viewer UI**: `npm run build && npm run sync-marketplace && npm run worker:restart`
|
||||
|
||||
**Chroma Vector Database** (`src/services/sync/ChromaSync.ts`)
|
||||
- Hybrid semantic + keyword search architecture
|
||||
- Automatic vector embedding synchronization
|
||||
- 90-day recency filtering for relevant results
|
||||
- Combined with SQLite FTS5 for optimal search performance
|
||||
## Environment Variables
|
||||
|
||||
**Viewer UI** (`src/ui/viewer/`)
|
||||
- React + TypeScript web interface accessible at http://localhost:37777
|
||||
- Real-time memory stream visualization via Server-Sent Events (SSE)
|
||||
- Infinite scroll pagination for observations, sessions, and user prompts
|
||||
- Project filtering and settings persistence
|
||||
- Built to `plugin/ui/viewer.html` (self-contained bundle via esbuild)
|
||||
- Auto-reconnection and error recovery
|
||||
|
||||
## How to Make Changes
|
||||
|
||||
### When You Modify Hooks
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
```
|
||||
Changes take effect on next Claude Code session. No worker restart needed.
|
||||
|
||||
### When You Modify Worker Service
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart
|
||||
```
|
||||
Must restart PM2 worker for changes to take effect.
|
||||
|
||||
### When You Modify Search Skill
|
||||
```bash
|
||||
npm run sync-marketplace
|
||||
```
|
||||
Skill changes take effect immediately on next Claude Code session. No build or restart needed (skills are markdown).
|
||||
|
||||
### When You Modify Viewer UI
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart
|
||||
```
|
||||
Changes to React components, styles, or viewer logic require rebuilding and restarting the worker. Refresh browser to see changes.
|
||||
|
||||
### Build Pipeline
|
||||
1. `npm run build` → Compiles TypeScript, outputs to `plugin/`
|
||||
2. `npm run sync-marketplace` → Syncs to `~/.claude/plugins/marketplaces/thedotmack/`
|
||||
3. Changes are live for next session (hooks/skills) or after restart (worker)
|
||||
|
||||
## Coding Standards: DRY, YAGNI, and Anti-Patterns
|
||||
|
||||
**Philosophy**: Write the dumb, obvious thing first. Add complexity only when you actually hit the problem.
|
||||
|
||||
### Common Anti-Patterns to Avoid
|
||||
|
||||
**1. Wrapper Functions for Constants**
|
||||
```typescript
|
||||
// ❌ DON'T: Ceremonial wrapper that adds zero value
|
||||
export function getWorkerPort(): number {
|
||||
return FIXED_PORT;
|
||||
}
|
||||
|
||||
// ✅ DO: Export the constant directly
|
||||
export const WORKER_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "37777", 10);
|
||||
```
|
||||
|
||||
**2. Unused Default Parameters**
|
||||
```typescript
|
||||
// ❌ DON'T: Defaults that are never actually used
|
||||
async function isHealthy(timeout: number = 3000) { ... }
|
||||
// Every call: isHealthy(1000) - the default is dead code
|
||||
|
||||
// ✅ DO: Remove the default if no one uses it
|
||||
async function isHealthy(timeout: number) { ... }
|
||||
```
|
||||
|
||||
**3. Magic Numbers Everywhere**
|
||||
```typescript
|
||||
// ❌ DON'T: Unexplained magic numbers scattered throughout
|
||||
if (await isWorkerHealthy(1000)) { ... }
|
||||
await waitForHealth(10000);
|
||||
setTimeout(resolve, 100);
|
||||
|
||||
// ✅ DO: Named constants with context
|
||||
const HEALTH_CHECK_TIMEOUT_MS = 1000;
|
||||
const HEALTH_CHECK_MAX_WAIT_MS = 10000;
|
||||
const HEALTH_CHECK_POLL_INTERVAL_MS = 100;
|
||||
```
|
||||
|
||||
**4. Overengineered Error Handling**
|
||||
```typescript
|
||||
// ❌ DON'T: Silent failures and defensive programming for ghosts
|
||||
checkProcess.on("close", (code) => {
|
||||
// PM2 list can fail, but we should still continue - just assume worker isn't running
|
||||
resolve(); // <- Silent failure!
|
||||
});
|
||||
|
||||
// ✅ DO: Fail fast with clear errors
|
||||
checkProcess.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`PM2 not found - install dependencies first`));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
```
|
||||
|
||||
**5. Fragile String Parsing**
|
||||
```typescript
|
||||
// ❌ DON'T: Parse human-readable output with string matching
|
||||
const isRunning = output.includes("claude-mem-worker") && output.includes("online");
|
||||
|
||||
// ✅ DO: Use structured output (JSON)
|
||||
const processes = JSON.parse(execSync('pm2 jlist'));
|
||||
const isRunning = processes.some(p => p.name === 'claude-mem-worker' && p.pm2_env.status === 'online');
|
||||
```
|
||||
|
||||
**6. Duplicated Promise Wrappers**
|
||||
```typescript
|
||||
// ❌ DON'T: Copy-paste the same promise pattern multiple times
|
||||
await new Promise((resolve, reject) => {
|
||||
process1.on("error", reject);
|
||||
process1.on("close", (code) => { /* ... */ });
|
||||
});
|
||||
// ... later ...
|
||||
await new Promise((resolve, reject) => {
|
||||
process2.on("error", reject);
|
||||
process2.on("close", (code) => { /* ... same pattern */ });
|
||||
});
|
||||
|
||||
// ✅ DO: Extract a helper function
|
||||
async function waitForProcess(process: ChildProcess, validateExitCode = false): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.on("error", reject);
|
||||
process.on("close", (code) => {
|
||||
if (validateExitCode && code !== 0 && code !== null) {
|
||||
reject(new Error(`Process failed with exit code ${code}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**7. YAGNI Violations - Solving Problems You Don't Have**
|
||||
```typescript
|
||||
// ❌ DON'T: 50+ lines checking PM2 status before starting
|
||||
const checkProcess = spawn(pm2Path, ["list", "--no-color"]);
|
||||
// ... parse output ...
|
||||
// ... check if running ...
|
||||
// ... then maybe start it ...
|
||||
|
||||
// ✅ DO: Just start it (PM2 start is idempotent)
|
||||
if (!await isWorkerHealthy()) {
|
||||
await startWorker(); // PM2 handles "already running" gracefully
|
||||
if (!await waitForWorkerHealth()) {
|
||||
throw new Error("Worker failed to become healthy");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Why These Patterns Appear
|
||||
|
||||
These anti-patterns often emerge from:
|
||||
- **Training bias**: Code that looks "professional" is often overengineered
|
||||
- **Risk aversion**: Optimizing for "what could go wrong" instead of "what do you actually need"
|
||||
- **Pattern matching**: Seeing a problem and immediately scaffolding a framework
|
||||
- **No real-world pain**: Not debugging at 2am means not feeling the cost of complexity
|
||||
|
||||
### The Actual Standard
|
||||
|
||||
1. **YAGNI (You Aren't Gonna Need It)**: Don't build it until you need it
|
||||
2. **DRY (Don't Repeat Yourself)**: Extract patterns after the second duplication, not before
|
||||
3. **Fail Fast**: Explicit errors beat silent failures
|
||||
4. **Simple First**: Write the obvious solution, then optimize only if needed
|
||||
5. **Delete Aggressively**: Less code = fewer bugs
|
||||
|
||||
**Reference**: See worker-utils.ts critique (conversation 2025-11-05) for detailed examples.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Hook
|
||||
1. Create `src/hooks/new-hook.ts`
|
||||
2. Add to `scripts/build-hooks.js` build list
|
||||
3. Add configuration to `plugin/hooks/hooks.json`
|
||||
4. Build and sync: `npm run build && npm run sync-marketplace`
|
||||
|
||||
### Modifying Database Schema
|
||||
1. Update schema in `src/services/sqlite/schema.ts`
|
||||
2. Update SessionStore/SessionSearch classes
|
||||
3. Migration strategy: The plugin currently recreates on schema changes (acceptable for alpha)
|
||||
4. TODO: Add proper migrations for production
|
||||
|
||||
### Debugging Worker Issues
|
||||
```bash
|
||||
pm2 list # Check worker status
|
||||
npm run worker:logs # View logs
|
||||
npm run worker:restart # Restart if needed
|
||||
pm2 delete claude-mem-worker # Force clean start
|
||||
```
|
||||
|
||||
### Testing Changes Locally
|
||||
1. Make changes in `src/`
|
||||
2. `npm run build && npm run sync-marketplace`
|
||||
3. Start new Claude Code session (hooks) or restart worker (worker changes)
|
||||
4. Check `~/.claude-mem/claude-mem.db` for database state
|
||||
5. Use search skill to verify behavior (auto-invoked when asking about past work)
|
||||
|
||||
### Version Bumps
|
||||
Use the version-bump skill:
|
||||
```bash
|
||||
/skill version-bump
|
||||
```
|
||||
Choose patch/minor/major. Updates package.json, marketplace.json, plugin.json, and CLAUDE.md.
|
||||
|
||||
## Investigation Best Practices
|
||||
|
||||
**When investigations are failing persistently**, use Task agents for comprehensive file analysis instead of grep/search:
|
||||
|
||||
**❌ Don't:** Repeatedly grep and search for patterns when failing to find the issue
|
||||
|
||||
**✅ Do:** Deploy a Task agent to read files in full and answer specific questions
|
||||
```
|
||||
"Read these files in full and answer: [specific questions about the implementation]"
|
||||
- Reduces token usage by delegating to a specialized agent
|
||||
- Provides comprehensive analysis in one pass
|
||||
- Finds issues that grep might miss due to poor query formulation
|
||||
- More efficient than multiple rounds of searching
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Deploy a general-purpose Task agent to:
|
||||
1. Read src/hooks/context-hook.ts in full
|
||||
2. Read src/services/worker-service.ts in full
|
||||
3. Answer: How do these files work together? What's the current implementation state?
|
||||
4. Find any bugs or inconsistencies between them
|
||||
```
|
||||
|
||||
Use this when:
|
||||
- Investigating how multiple files interact
|
||||
- Search queries aren't finding what you expect
|
||||
- Need complete implementation context
|
||||
- Issue might be a subtle inconsistency between files
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### v5.4.0 - Skill-Based Search Migration
|
||||
**Breaking Change**: MCP search tools replaced with skill-based approach
|
||||
- **Token Savings**: ~2,250 tokens per session start
|
||||
- **Progressive Disclosure**: Skill frontmatter (~250 tokens) instead of 9 MCP tool definitions (~2,500 tokens)
|
||||
- **New HTTP API**: 10 search endpoints in worker service (localhost:37777/api/search/*)
|
||||
- **Search Skill**: Auto-invoked when users ask about past work, decisions, or history
|
||||
- **No User Action Required**: Migration is transparent, searches work automatically
|
||||
- **Deprecated**: MCP search server (source kept for reference: src/servers/search-server.ts)
|
||||
|
||||
**Available Search Operations:**
|
||||
1. Search observations (full-text)
|
||||
2. Search session summaries (full-text)
|
||||
3. Search user prompts (full-text)
|
||||
4. Search by observation type (bugfix, feature, refactor, discovery, decision)
|
||||
5. Search by concept tag
|
||||
6. Search by file path
|
||||
7. Get recent context for a project
|
||||
8. Get timeline around specific point in time
|
||||
9. Get timeline by query (search + timeline in one call)
|
||||
10. Get API help documentation
|
||||
|
||||
**How It Works:**
|
||||
- User asks: "What bug did we fix last session?"
|
||||
- Claude sees skill description matches → invokes search skill
|
||||
- Skill loads full instructions → uses curl to call HTTP API → formats results
|
||||
- User sees formatted answer with past work context
|
||||
|
||||
### v5.1.2 - Theme Toggle
|
||||
**Theme Support**: Light/dark mode for viewer UI
|
||||
- User-selectable theme with persistent settings
|
||||
- Automatic system preference detection
|
||||
- Smooth transitions between themes
|
||||
- Settings stored in browser localStorage
|
||||
|
||||
### v5.1.0 - Web-Based Viewer UI
|
||||
**Major Feature**: Web-Based Viewer UI for Real-Time Memory Stream
|
||||
- Production-ready viewer accessible at http://localhost:37777
|
||||
- Real-time visualization via Server-Sent Events (SSE) - see observations, sessions, and prompts as they happen
|
||||
- Infinite scroll pagination with automatic deduplication
|
||||
- Project filtering to focus on specific codebases
|
||||
- Settings persistence (sidebar state, selected project)
|
||||
- Auto-reconnection with exponential backoff
|
||||
- GPU-accelerated animations for smooth interactions
|
||||
|
||||
**Worker Service API Endpoints** (14 HTTP/SSE endpoints total):
|
||||
|
||||
*Viewer & Health:*
|
||||
- `GET /` - Serves viewer HTML (self-contained React app)
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /stream` - Server-Sent Events for real-time updates
|
||||
|
||||
*Data Retrieval:*
|
||||
- `GET /api/prompts` - Paginated user prompts with project filtering
|
||||
- `GET /api/observations` - Paginated observations with project filtering
|
||||
- `GET /api/summaries` - Paginated session summaries with project filtering
|
||||
- `GET /api/stats` - Database statistics (total counts by project)
|
||||
|
||||
*Settings:*
|
||||
- `GET /api/settings` - Get current viewer settings
|
||||
- `POST /api/settings` - Update viewer settings
|
||||
|
||||
*Session Management:*
|
||||
- `POST /sessions/:sessionDbId/init` - Initialize new session
|
||||
- `POST /sessions/:sessionDbId/observations` - Add observations to session
|
||||
- `POST /sessions/:sessionDbId/summarize` - Generate session summary
|
||||
- `GET /sessions/:sessionDbId/status` - Get session status
|
||||
- `DELETE /sessions/:sessionDbId` - Delete session (graceful cleanup)
|
||||
|
||||
**Database Enhancements** (+98 lines in SessionStore):
|
||||
- `getRecentPrompts()` - Paginated prompts with OFFSET/LIMIT
|
||||
- `getRecentObservations()` - Paginated observations with OFFSET/LIMIT
|
||||
- `getRecentSummaries()` - Paginated summaries with OFFSET/LIMIT
|
||||
- `getStats()` - Aggregated statistics by project
|
||||
- `getUniqueProjects()` - Distinct project names
|
||||
|
||||
**Complete React UI** (17 new files, 1,500+ lines):
|
||||
- Components: Header, Sidebar, Feed, Cards (Observation, Prompt, Summary, Skeleton)
|
||||
- Hooks: useSSE, usePagination, useSettings, useStats
|
||||
- Utils: Data merging, formatters, constants
|
||||
- Assets: Monaspace Radon font, logos (dark mode + logomark)
|
||||
- Build: esbuild pipeline for self-contained HTML bundle
|
||||
|
||||
**Why This Matters**: Users can now visualize their memory stream in real-time. See exactly what claude-mem is capturing as you work, filter by project, and understand the context being injected into sessions.
|
||||
|
||||
### v5.0.3 - Smart Install Caching
|
||||
**Smart Caching Installer for Windows Compatibility**:
|
||||
- Eliminated redundant npm install on every SessionStart (2-5s → 10ms)
|
||||
- Caches version in `.install-version` file
|
||||
- Only runs npm install when actually needed (first time, version change, missing deps)
|
||||
- 200x performance improvement for cached installations
|
||||
|
||||
### v5.0.0 - Hybrid Search Architecture
|
||||
**Major Feature**: Chroma Vector Database Integration
|
||||
- Hybrid semantic + keyword search combining ChromaDB with SQLite FTS5
|
||||
- ChromaSync service for automatic vector embedding synchronization (738 lines)
|
||||
- 90-day recency filtering for contextually relevant results
|
||||
- Timeline and context search capabilities (now provided via skill-based HTTP API)
|
||||
- Performance: Semantic search <200ms with 8,000+ vector documents
|
||||
- Full-text search across observations, sessions, and prompts
|
||||
|
||||
## Configuration Users Can Set
|
||||
|
||||
**Model Selection** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_MODEL": "claude-haiku-4-5" // or sonnet-4-5, opus-4, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Context Observation Count** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "50" // default, adjust based on needs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Worker Port** (`~/.claude/settings.json`):
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_MEM_WORKER_PORT": "37777" // default
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Why PM2 Instead of Direct Process
|
||||
Hooks have strict timeout limits. PM2 manages a persistent background worker, allowing AI processing to continue after hooks complete.
|
||||
|
||||
### Why SQLite FTS5
|
||||
Enables instant full-text search across thousands of observations without external dependencies. Automatic sync triggers keep FTS5 tables synchronized.
|
||||
|
||||
### Why Graceful Cleanup (v4.1.0)
|
||||
Changed from aggressive DELETE requests to marking sessions complete. Prevents interrupting summary generation and other async operations.
|
||||
|
||||
### Why Smart Install Caching (v5.0.3)
|
||||
npm install is expensive (2-5s). Caching version state and only installing on changes makes SessionStart nearly instant (10ms).
|
||||
|
||||
### Why Web-Based Viewer UI (v5.1.0)
|
||||
Real-time visibility into memory stream helps users understand what's being captured and how context is being built. SSE provides instant updates without polling. Self-contained HTML bundle (esbuild) eliminates deployment complexity - everything served from a single file.
|
||||
- `CLAUDE_MEM_MODEL` - Model for observations/summaries (default: claude-haiku-4-5)
|
||||
- `CLAUDE_MEM_CONTEXT_OBSERVATIONS` - Observations injected at SessionStart (default: 50)
|
||||
- `CLAUDE_MEM_WORKER_PORT` - Worker service port (default: 37777)
|
||||
|
||||
## File Locations
|
||||
|
||||
**Source**: `<project-root>/src/` - TypeScript source files
|
||||
**Built Plugin**: `<project-root>/plugin/` - Compiled JavaScript outputs
|
||||
**Installed Plugin**: `~/.claude/plugins/marketplaces/thedotmack/` - User's installed plugin location
|
||||
**Database**: `~/.claude-mem/claude-mem.db` - SQLite database with observations, sessions, summaries
|
||||
**Chroma Database**: `~/.claude-mem/chroma/` - Vector embeddings for semantic search
|
||||
**Usage Logs**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl` - Daily API usage tracking
|
||||
- **Source**: `<project-root>/src/`
|
||||
- **Built Plugin**: `<project-root>/plugin/`
|
||||
- **Installed Plugin**: `~/.claude/plugins/marketplaces/thedotmack/`
|
||||
- **Database**: `~/.claude-mem/claude-mem.db`
|
||||
- **Chroma**: `~/.claude-mem/chroma/`
|
||||
- **Usage Logs**: `~/.claude-mem/usage-logs/usage-YYYY-MM-DD.jsonl`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Build**: `npm run build`
|
||||
**Sync**: `npm run sync-marketplace`
|
||||
**Worker Restart**: `npm run worker:restart`
|
||||
**Worker Logs**: `npm run worker:logs`
|
||||
**Version Bump**: `/skill version-bump`
|
||||
**Usage Analysis**: `npm run usage:today`
|
||||
**Viewer UI**: http://localhost:37777 (auto-starts with worker)
|
||||
```bash
|
||||
npm run build # Compile TypeScript
|
||||
npm run sync-marketplace # Copy to ~/.claude/plugins
|
||||
npm run worker:restart # Restart PM2 worker
|
||||
npm run worker:logs # View worker logs
|
||||
pm2 list # Check worker status
|
||||
pm2 delete claude-mem-worker # Force clean start
|
||||
```
|
||||
|
||||
**Viewer UI**: http://localhost:37777
|
||||
|
||||
@@ -1,468 +0,0 @@
|
||||
# Plan: Display Complete Observation Data in Viewer UI
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### What's Currently Shown (5 fields)
|
||||
- ✅ **type** - Displayed as chip/badge (e.g., "discovery", "bugfix")
|
||||
- ✅ **project** - Shown in card header
|
||||
- ✅ **title** - Main card title (shows "Untitled" if null)
|
||||
- ✅ **subtitle** - Optional subheading
|
||||
- ✅ **id + created_at** - Metadata line (e.g., "#1 • 2 hours ago")
|
||||
|
||||
### What's Hidden (10+ fields)
|
||||
- ❌ **narrative** - Detailed explanation text (MOST IMPORTANT)
|
||||
- ❌ **facts** - JSON array of key facts (structured bullet points)
|
||||
- ❌ **concepts** - JSON array of concept tags (e.g., "problem-solution", "gotcha")
|
||||
- ❌ **files_read** - JSON array of file paths that were read
|
||||
- ❌ **files_modified** - JSON array of file paths that were modified
|
||||
- ❌ **text** - Legacy unstructured text field (deprecated but still populated)
|
||||
- ❌ **prompt_number** - Which user prompt triggered this observation
|
||||
- ❌ **sdk_session_id** - Session identifier
|
||||
|
||||
### Database Schema (Actual Structure)
|
||||
|
||||
```sql
|
||||
observations table:
|
||||
- id (INTEGER PRIMARY KEY)
|
||||
- sdk_session_id (TEXT)
|
||||
- project (TEXT)
|
||||
- type (TEXT: decision, bugfix, feature, refactor, discovery, change)
|
||||
- created_at (TEXT ISO timestamp)
|
||||
- created_at_epoch (INTEGER milliseconds)
|
||||
- prompt_number (INTEGER nullable)
|
||||
- title (TEXT nullable)
|
||||
- subtitle (TEXT nullable)
|
||||
- narrative (TEXT nullable) -- Rich detailed explanation
|
||||
- text (TEXT nullable) -- Legacy field
|
||||
- facts (TEXT nullable) -- JSON array of key facts
|
||||
- concepts (TEXT nullable) -- JSON array of concept tags
|
||||
- files_read (TEXT nullable) -- JSON array of file paths
|
||||
- files_modified (TEXT nullable) -- JSON array of file paths
|
||||
```
|
||||
|
||||
### Issues Found
|
||||
|
||||
1. **Type Definition Mismatch**: Three different type definitions exist:
|
||||
- Actual database schema (most complete)
|
||||
- `worker-types.ts` Observation interface (flattened, has wrong field names)
|
||||
- `viewer/types.ts` Observation interface (minimal subset)
|
||||
|
||||
2. **Data Loss**: Rich fields are stored in DB but not transmitted to UI:
|
||||
- narrative, facts, files_read, files_modified all missing from API
|
||||
|
||||
3. **PaginationHelper Query Bug**: Selects non-existent fields:
|
||||
- `session_db_id` (should be `sdk_session_id`)
|
||||
- `claude_session_id` (doesn't exist in observations table)
|
||||
- `files` (should be `files_read` + `files_modified`)
|
||||
|
||||
## Proposed Implementation Plan
|
||||
|
||||
### Phase 1: Fix Data Layer
|
||||
|
||||
#### 1.1 Update Viewer Type Definitions
|
||||
**File**: `src/ui/viewer/types.ts`
|
||||
|
||||
```typescript
|
||||
export interface Observation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
type: string;
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
narrative: string | null; // NEW - detailed explanation
|
||||
text: string | null; // Legacy field
|
||||
facts: string | null; // NEW - JSON array of key facts
|
||||
concepts: string | null; // NEW - JSON array of concept tags
|
||||
files_read: string | null; // NEW - JSON array of file paths
|
||||
files_modified: string | null; // NEW - JSON array of file paths
|
||||
prompt_number: number | null; // NEW - which prompt triggered this
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Fix PaginationHelper SQL Query
|
||||
**File**: `src/services/worker/PaginationHelper.ts` (around line 26)
|
||||
|
||||
**Current (BROKEN)**:
|
||||
```typescript
|
||||
const fields = 'id, session_db_id, claude_session_id, project, type, title, subtitle, text, concepts, files, prompt_number, created_at, created_at_epoch';
|
||||
```
|
||||
|
||||
**Fixed**:
|
||||
```typescript
|
||||
const fields = 'id, sdk_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch';
|
||||
```
|
||||
|
||||
#### 1.3 Update Worker Service v2 Response Mapping
|
||||
**File**: `src/services/worker-service-v2.ts`
|
||||
|
||||
Ensure the `/api/observations` endpoint properly maps all fields from database to response. May need to parse JSON fields (facts, concepts, files_read, files_modified) if they're stored as JSON strings.
|
||||
|
||||
### Phase 2: Redesign UI Component
|
||||
|
||||
#### 2.1 Update ObservationCard Component
|
||||
**File**: `src/ui/viewer/components/ObservationCard.tsx`
|
||||
|
||||
**New Structure**:
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [type badge] [project] │ ← Header (always visible)
|
||||
├─────────────────────────────────────────┤
|
||||
│ Title │ ← Always visible
|
||||
│ Subtitle (if present) │ ← Always visible
|
||||
│ #123 • 2 hours ago [▼ More]│ ← Metadata + Expand button
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─ EXPANDED CONTENT (when opened) ───┐ │
|
||||
│ │ │ │
|
||||
│ │ 📝 Narrative │ │
|
||||
│ │ ─────────────────────────────────── │ │
|
||||
│ │ Detailed explanation text... │ │
|
||||
│ │ │ │
|
||||
│ │ 📌 Key Facts │ │
|
||||
│ │ ─────────────────────────────────── │ │
|
||||
│ │ • Fact 1 │ │
|
||||
│ │ • Fact 2 │ │
|
||||
│ │ • Fact 3 │ │
|
||||
│ │ │ │
|
||||
│ │ 🏷️ Concepts │ │
|
||||
│ │ ─────────────────────────────────── │ │
|
||||
│ │ [problem-solution] [discovery] │ │
|
||||
│ │ │ │
|
||||
│ │ 📁 Files │ │
|
||||
│ │ ─────────────────────────────────── │ │
|
||||
│ │ 📖 Read: │ │
|
||||
│ │ src/hooks/save-hook.ts │ │
|
||||
│ │ src/services/worker.ts │ │
|
||||
│ │ ✏️ Modified: │ │
|
||||
│ │ src/hooks/save-hook.ts │ │
|
||||
│ │ │ │
|
||||
│ │ 🔗 Session Info │ │
|
||||
│ │ ─────────────────────────────────── │ │
|
||||
│ │ Prompt #5 • Session: abc123... │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Component Logic**:
|
||||
```typescript
|
||||
const ObservationCard = ({ observation }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Parse JSON fields
|
||||
const facts = observation.facts ? JSON.parse(observation.facts) : [];
|
||||
const concepts = observation.concepts ? JSON.parse(observation.concepts) : [];
|
||||
const filesRead = observation.files_read ? JSON.parse(observation.files_read) : [];
|
||||
const filesModified = observation.files_modified ? JSON.parse(observation.files_modified) : [];
|
||||
|
||||
return (
|
||||
<div className={`card ${isExpanded ? 'card-expanded' : ''}`}>
|
||||
{/* Header - always visible */}
|
||||
<div className="card-header">
|
||||
<span className={`card-type type-${observation.type}`}>
|
||||
{observation.type}
|
||||
</span>
|
||||
<span className="card-project">{observation.project}</span>
|
||||
</div>
|
||||
|
||||
{/* Title/Subtitle - always visible */}
|
||||
<div className="card-title">{observation.title || 'Untitled'}</div>
|
||||
{observation.subtitle && (
|
||||
<div className="card-subtitle">{observation.subtitle}</div>
|
||||
)}
|
||||
|
||||
{/* Metadata + Expand button - always visible */}
|
||||
<div className="card-meta">
|
||||
<span>#{observation.id} • {formatDate(observation.created_at_epoch)}</span>
|
||||
<button
|
||||
className="expand-toggle"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? '▲ Less' : '▼ More'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Expanded content - conditional */}
|
||||
{isExpanded && (
|
||||
<div className="card-expanded-content">
|
||||
|
||||
{/* Narrative Section */}
|
||||
{observation.narrative && (
|
||||
<div className="card-section">
|
||||
<div className="section-header">📝 Narrative</div>
|
||||
<div className="section-content narrative">
|
||||
{observation.narrative}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Facts Section */}
|
||||
{facts.length > 0 && (
|
||||
<div className="card-section">
|
||||
<div className="section-header">📌 Key Facts</div>
|
||||
<ul className="section-content facts-list">
|
||||
{facts.map((fact, i) => (
|
||||
<li key={i}>{fact}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Concepts Section */}
|
||||
{concepts.length > 0 && (
|
||||
<div className="card-section">
|
||||
<div className="section-header">🏷️ Concepts</div>
|
||||
<div className="section-content concepts">
|
||||
{concepts.map((concept, i) => (
|
||||
<span key={i} className="concept-tag">{concept}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files Section */}
|
||||
{(filesRead.length > 0 || filesModified.length > 0) && (
|
||||
<div className="card-section">
|
||||
<div className="section-header">📁 Files</div>
|
||||
<div className="section-content files">
|
||||
{filesRead.length > 0 && (
|
||||
<div className="file-group">
|
||||
<div className="file-group-label">📖 Read:</div>
|
||||
{filesRead.map((file, i) => (
|
||||
<div key={i} className="file-path">{file}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filesModified.length > 0 && (
|
||||
<div className="file-group">
|
||||
<div className="file-group-label">✏️ Modified:</div>
|
||||
{filesModified.map((file, i) => (
|
||||
<div key={i} className="file-path">{file}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Info Section */}
|
||||
<div className="card-section">
|
||||
<div className="section-header">🔗 Session Info</div>
|
||||
<div className="section-content session-info">
|
||||
{observation.prompt_number && (
|
||||
<span>Prompt #{observation.prompt_number}</span>
|
||||
)}
|
||||
{observation.sdk_session_id && (
|
||||
<span className="session-id">
|
||||
Session: {observation.sdk_session_id.substring(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: Style Enhancements
|
||||
|
||||
#### 3.1 Update Styles
|
||||
**File**: `src/ui/viewer/styles.css`
|
||||
|
||||
**New CSS Classes Needed**:
|
||||
```css
|
||||
/* Expanded card state */
|
||||
.card-expanded {
|
||||
/* Maybe increase shadow or border when expanded */
|
||||
}
|
||||
|
||||
/* Expand toggle button */
|
||||
.expand-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.expand-toggle:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Expanded content container */
|
||||
.card-expanded-content {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
animation: expandDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes expandDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Section styling */
|
||||
.card-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding-left: 20px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Narrative styling */
|
||||
.narrative {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Facts list styling */
|
||||
.facts-list {
|
||||
list-style: disc;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.facts-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Concepts tags */
|
||||
.concepts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.concept-tag {
|
||||
background: var(--accent-bg);
|
||||
color: var(--accent-text);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* File paths */
|
||||
.file-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.file-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.file-group-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.file-path {
|
||||
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Session info */
|
||||
.session-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.session-id {
|
||||
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps (In Order)
|
||||
|
||||
1. **Fix PaginationHelper query** (src/services/worker/PaginationHelper.ts)
|
||||
- Update SQL SELECT to use correct field names
|
||||
- Test with `npm run worker:restart:v2`
|
||||
|
||||
2. **Update viewer type definitions** (src/ui/viewer/types.ts)
|
||||
- Add all missing fields to Observation interface
|
||||
|
||||
3. **Verify worker service v2 mapping** (src/services/worker-service-v2.ts)
|
||||
- Ensure `/api/observations` returns all fields
|
||||
- Test API response with curl or browser
|
||||
|
||||
4. **Update ObservationCard component** (src/ui/viewer/components/ObservationCard.tsx)
|
||||
- Add expand/collapse state
|
||||
- Add all new sections (narrative, facts, concepts, files, session)
|
||||
- Add expand toggle button
|
||||
|
||||
5. **Update styles** (src/ui/viewer/styles.css)
|
||||
- Add all new CSS classes for expanded content
|
||||
- Add animations for smooth expand/collapse
|
||||
- Style sections, lists, tags, file paths
|
||||
|
||||
6. **Build and test**
|
||||
```bash
|
||||
npm run build
|
||||
npm run sync-marketplace
|
||||
npm run worker:restart:v2
|
||||
```
|
||||
|
||||
7. **Manual testing**
|
||||
- Open http://localhost:37777
|
||||
- Click expand button on observations
|
||||
- Verify all fields display correctly
|
||||
- Test light/dark mode
|
||||
- Test with observations that have missing fields (graceful fallback)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All database fields are fetched in API query
|
||||
- [ ] All fields are properly typed in TypeScript interfaces
|
||||
- [ ] ObservationCard shows all data in expanded view
|
||||
- [ ] Expand/collapse animations work smoothly
|
||||
- [ ] File paths are formatted in monospace font
|
||||
- [ ] Concepts display as tag pills
|
||||
- [ ] Facts display as bulleted list
|
||||
- [ ] Narrative text wraps properly with scroll for long content
|
||||
- [ ] No console errors
|
||||
- [ ] Works in both light and dark themes
|
||||
|
||||
## Optional Enhancements (Future)
|
||||
|
||||
- [ ] Remember expanded state in localStorage (persist across page refresh)
|
||||
- [ ] Keyboard shortcuts (Space to expand/collapse focused card)
|
||||
- [ ] Click file paths to copy to clipboard
|
||||
- [ ] Search/filter by concepts or files
|
||||
- [ ] Syntax highlighting for code in narrative
|
||||
- [ ] Link session_id to session detail view
|
||||
@@ -1,126 +0,0 @@
|
||||
# PR Summary: Fix SDK Agent Missing Working Directory Context (CWD)
|
||||
|
||||
## Problem
|
||||
The SDK agent lacked spatial awareness because working directory (CWD) information was captured at the hook level but deliberately not passed to the worker service. This caused:
|
||||
- SDK agent searching wrong repositories
|
||||
- False "file not found" reports even when files existed
|
||||
- Inability to match user-requested paths to tool execution paths
|
||||
- Inaccurate observations due to spatial confusion
|
||||
|
||||
## Solution
|
||||
Added CWD propagation through the entire data pipeline from hook to SDK agent, enabling spatial awareness.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
PostToolUseInput.cwd → save-hook → Worker API → SessionManager → SDK Agent → Prompt XML
|
||||
```
|
||||
|
||||
### Files Modified (8 source + 2 build artifacts + 2 docs)
|
||||
1. `src/services/worker-types.ts` - Added `cwd?: string` to interfaces
|
||||
2. `src/hooks/save-hook.ts` - Extract and pass CWD to worker
|
||||
3. `src/services/worker-service.ts` - Accept CWD in observations endpoint
|
||||
4. `src/services/worker/SessionManager.ts` - Include CWD in message queue
|
||||
5. `src/services/worker/SDKAgent.ts` - Pass CWD to prompt builder
|
||||
6. `src/sdk/prompts.ts` - Include `<tool_cwd>` in XML + spatial awareness docs
|
||||
7. `tests/cwd-propagation.test.ts` - 8 comprehensive tests (NEW)
|
||||
8. `docs/CWD_CONTEXT_FIX.md` - Technical documentation (NEW)
|
||||
9. `CHANGELOG.md` - User-facing changelog entry
|
||||
|
||||
### Example Output
|
||||
Before (no spatial awareness):
|
||||
```xml
|
||||
<tool_used>
|
||||
<tool_name>ReadTool</tool_name>
|
||||
<tool_time>2025-11-10T19:18:03.065Z</tool_time>
|
||||
<tool_input>{"path":"src/index.ts"}</tool_input>
|
||||
<tool_output>{"content":"..."}</tool_output>
|
||||
</tool_used>
|
||||
```
|
||||
|
||||
After (with spatial awareness):
|
||||
```xml
|
||||
<tool_used>
|
||||
<tool_name>ReadTool</tool_name>
|
||||
<tool_time>2025-11-10T19:18:03.065Z</tool_time>
|
||||
<tool_cwd>/home/user/awesome-project</tool_cwd>
|
||||
<tool_input>{"path":"src/index.ts"}</tool_input>
|
||||
<tool_output>{"content":"..."}</tool_output>
|
||||
</tool_used>
|
||||
```
|
||||
|
||||
### Init Prompt Enhancement
|
||||
Added "SPATIAL AWARENESS" section explaining:
|
||||
- Tool executions include working directory (tool_cwd)
|
||||
- Which repository/project is being worked on
|
||||
- Where files are located relative to project root
|
||||
- How to match requested paths to actual execution paths
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
✅ 8 tests in `tests/cwd-propagation.test.ts` - all passing
|
||||
- Interface definitions include cwd
|
||||
- Hook extracts cwd from input
|
||||
- Worker API accepts cwd
|
||||
- SessionManager queues cwd
|
||||
- SDK Agent passes cwd to prompts
|
||||
- Prompt builder includes tool_cwd element
|
||||
- End-to-end flow validation
|
||||
|
||||
### Build Verification
|
||||
✅ All builds successful
|
||||
- `plugin/scripts/save-hook.js` includes `cwd:s||""`
|
||||
- `plugin/scripts/worker-service.cjs` includes `<tool_cwd>` element
|
||||
- `plugin/scripts/worker-service.cjs` includes "SPATIAL AWARENESS" section
|
||||
|
||||
### Security Scan
|
||||
✅ CodeQL: 0 vulnerabilities
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Spatial Awareness**: SDK agent knows which directory/repository it's observing
|
||||
2. **Accurate Path Matching**: Can verify if requested paths match executed paths
|
||||
3. **Better Observations**: Won't search wrong repositories or report false negatives
|
||||
4. **Universal Model Support**: Works with Haiku, Sonnet, and Opus (no premium workaround needed)
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- ✅ `cwd` is optional (`cwd?: string`) - no breaking changes
|
||||
- ✅ Missing `cwd` handled gracefully (defaults to empty string)
|
||||
- ✅ Existing observations without `cwd` continue to work
|
||||
- ✅ No database migration required (CWD is transient, not persisted)
|
||||
|
||||
## Evidence from Issue
|
||||
|
||||
**Test Case**: User requested "Review and understand ai_docs/continuous-improvement/rules.md"
|
||||
|
||||
**Before Fix**:
|
||||
1. File exists at `/Users/.../dev/personal/lunar-claude/ai_docs/...` ✅
|
||||
2. Read tool successfully read the file ✅
|
||||
3. SDK agent received tool executions but **no CWD** ❌
|
||||
4. SDK agent searched **claude-mem repository** instead of lunar-claude ❌
|
||||
5. Summary reported: "File does not exist" ❌
|
||||
|
||||
**After Fix**:
|
||||
1. File exists at `/Users/.../dev/personal/lunar-claude/ai_docs/...` ✅
|
||||
2. Read tool successfully read the file ✅
|
||||
3. SDK agent receives tool executions **with CWD** ✅
|
||||
4. SDK agent searches **correct repository (lunar-claude)** ✅
|
||||
5. Summary accurate: "Reviewed rules.md in lunar-claude project" ✅
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [x] TypeScript compiles without errors
|
||||
- [x] All tests pass (8/8)
|
||||
- [x] Build artifacts include CWD propagation
|
||||
- [x] No security vulnerabilities
|
||||
- [x] Documentation complete
|
||||
- [x] Backward compatible
|
||||
- [x] Example prompts verified
|
||||
- [x] CHANGELOG updated
|
||||
|
||||
## Ready for Merge
|
||||
|
||||
This PR is ready for review and merge. All validation steps passed successfully.
|
||||
@@ -2,9 +2,9 @@
|
||||
<br>
|
||||
<a href="https://github.com/thedotmack/claude-mem">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/claude-mem-logo-for-dark-mode.webp">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/claude-mem-logo-for-light-mode.webp">
|
||||
<img src="docs/claude-mem-logo-for-light-mode.webp" alt="Claude-Mem" width="400">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/claude-mem-logo-for-dark-mode.webp">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/claude-mem-logo-for-light-mode.webp">
|
||||
<img src="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/claude-mem-logo-for-light-mode.webp" alt="Claude-Mem" width="400">
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
@@ -17,7 +17,7 @@
|
||||
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/version-5.4.0-green.svg" alt="Version">
|
||||
<img src="https://img.shields.io/badge/version-6.0.0-green.svg" alt="Version">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
|
||||
@@ -32,7 +32,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/thedotmack/claude-mem">
|
||||
<picture>
|
||||
<img src="docs/cm-preview.gif" alt="Claude-Mem Preview" width="800">
|
||||
<img src="https://raw.githubusercontent.com/thedotmack/claude-mem/main/docs/public/cm-preview.gif" alt="Claude-Mem Preview" width="800">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -69,7 +69,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
|
||||
- 🔍 **Skill-Based Search** - Query your project history with natural language (~2,250 token savings)
|
||||
- 🔍 **Skill-Based Search** - Query your project history with mem-search skill (~2,250 token savings)
|
||||
- 🖥️ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
@@ -89,31 +89,30 @@ npx mintlify dev
|
||||
|
||||
### Getting Started
|
||||
|
||||
- **[Installation Guide](docs/installation.mdx)** - Quick start & advanced installation
|
||||
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically
|
||||
- **[Search Tools](docs/usage/search-tools.mdx)** - Query your project history with natural language
|
||||
- **[Installation Guide](https://docs.claude-mem.ai/installation)** - Quick start & advanced installation
|
||||
- **[Usage Guide](https://docs.claude-mem.ai/usage/getting-started)** - How Claude-Mem works automatically
|
||||
- **[Search Tools](https://docs.claude-mem.ai/usage/search-tools)** - Query your project history with natural language
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **[Context Engineering](docs/context-engineering.mdx)** - AI agent context optimization principles
|
||||
- **[Progressive Disclosure](docs/progressive-disclosure.mdx)** - Philosophy behind Claude-Mem's context priming strategy
|
||||
- **[Context Engineering](https://docs.claude-mem.ai/context-engineering)** - AI agent context optimization principles
|
||||
- **[Progressive Disclosure](https://docs.claude-mem.ai/progressive-disclosure)** - Philosophy behind Claude-Mem's context priming strategy
|
||||
|
||||
### Architecture
|
||||
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Architecture Evolution](docs/architecture-evolution.mdx)** - The journey from v3 to v5
|
||||
- **[Hooks Architecture](docs/hooks-architecture.mdx)** - How Claude-Mem uses lifecycle hooks
|
||||
- **[Hooks Reference](docs/architecture/hooks.mdx)** - 7 hook scripts explained
|
||||
- **[Worker Service](docs/architecture/worker-service.mdx)** - HTTP API & PM2 management
|
||||
- **[Database](docs/architecture/database.mdx)** - SQLite schema & FTS5 search
|
||||
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 9 search tools & examples
|
||||
- **[Viewer UI](docs/VIEWER.md)** - Web-based memory stream visualization
|
||||
- **[Overview](https://docs.claude-mem.ai/architecture/overview)** - System components & data flow
|
||||
- **[Architecture Evolution](https://docs.claude-mem.ai/architecture-evolution)** - The journey from v3 to v5
|
||||
- **[Hooks Architecture](https://docs.claude-mem.ai/hooks-architecture)** - How Claude-Mem uses lifecycle hooks
|
||||
- **[Hooks Reference](https://docs.claude-mem.ai/architecture/hooks)** - 7 hook scripts explained
|
||||
- **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & PM2 management
|
||||
- **[Database](https://docs.claude-mem.ai/architecture/database)** - SQLite schema & FTS5 search
|
||||
- **[Search Architecture](https://docs.claude-mem.ai/architecture/search-architecture)** - Hybrid search with Chroma vector database
|
||||
|
||||
### Configuration & Development
|
||||
|
||||
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings
|
||||
- **[Development](docs/development.mdx)** - Building, testing, contributing
|
||||
- **[Troubleshooting](docs/troubleshooting.mdx)** - Common issues & solutions
|
||||
- **[Configuration](https://docs.claude-mem.ai/configuration)** - Environment variables & settings
|
||||
- **[Development](https://docs.claude-mem.ai/development)** - Building, testing, contributing
|
||||
- **[Troubleshooting](https://docs.claude-mem.ai/troubleshooting)** - Common issues & solutions
|
||||
|
||||
---
|
||||
|
||||
@@ -143,23 +142,24 @@ npx mintlify dev
|
||||
|
||||
**Core Components:**
|
||||
|
||||
1. **7 Lifecycle Hook Scripts** - smart-install, context-hook, user-message-hook, new-hook, save-hook, summary-hook, cleanup-hook
|
||||
2. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
|
||||
4. **Search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP)
|
||||
5. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
|
||||
1. **6 Lifecycle Hooks** - context-hook, user-message-hook, new-hook, save-hook, summary-hook, cleanup-hook
|
||||
2. **Smart Install** - Cached dependency checker (pre-hook script, not a lifecycle hook)
|
||||
3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by PM2
|
||||
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
|
||||
5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP)
|
||||
6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
|
||||
|
||||
See [Architecture Overview](docs/architecture/overview.mdx) for details.
|
||||
See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) for details.
|
||||
|
||||
---
|
||||
|
||||
## Skill-Based Search
|
||||
## mem-search Skill
|
||||
|
||||
Claude-Mem provides intelligent search through a skill that auto-invokes when you ask about past work:
|
||||
Claude-Mem provides intelligent search through the mem-search skill that auto-invokes when you ask about past work:
|
||||
|
||||
**How It Works:**
|
||||
- Just ask naturally: *"What did we do last session?"* or *"Did we fix this bug before?"*
|
||||
- Claude automatically invokes the search skill to find relevant context
|
||||
- Claude automatically invokes the mem-search skill to find relevant context
|
||||
- ~2,250 token savings per session start vs MCP approach
|
||||
|
||||
**Available Search Operations:**
|
||||
@@ -185,42 +185,33 @@ Claude-Mem provides intelligent search through a skill that auto-invokes when yo
|
||||
"What was happening when we added the viewer UI?"
|
||||
```
|
||||
|
||||
See [Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v5.4.0
|
||||
## What's New in v6.0.0
|
||||
|
||||
**🔍 Skill-Based Search Architecture (v5.4.0):**
|
||||
**🚀 Major Session Management & Transcript Processing Improvements:**
|
||||
|
||||
- **Token Savings**: ~2,250 tokens per session start
|
||||
- **Progressive Disclosure**: Skill frontmatter (~250 tokens) vs MCP tool definitions (~2,500 tokens)
|
||||
- **Natural Language**: Just ask about past work - Claude auto-invokes the search skill
|
||||
- **10 HTTP API Endpoints**: Fast, efficient search operations
|
||||
- **No User Action Required**: Migration is transparent
|
||||
- **Enhanced Session Initialization**: Accept userPrompt and promptNumber for better context tracking
|
||||
- **Live UserPrompt Updates**: Multi-turn conversation support with real-time prompt tracking
|
||||
- **Improved SessionManager**: Better context handling and observation processing
|
||||
- **Comprehensive Transcript Processing**: New scripts and utilities for analyzing Claude Code transcripts
|
||||
- **Rich Context Extraction**: Advanced parsing utilities for extracting meaningful context from sessions
|
||||
- **Refactored Architecture**: Improved hooks and SDKAgent for more reliable observation handling
|
||||
- **Silent Debug Logging**: Better debugging capabilities without cluttering output
|
||||
- **Enhanced Error Handling**: More robust error recovery and debugging tools
|
||||
|
||||
**🎨 Theme Toggle (v5.1.2):**
|
||||
**Breaking Changes**: Significant architectural changes in session management and observation handling. Existing sessions continue to work, but internal APIs have evolved.
|
||||
|
||||
- Light/dark mode support in viewer UI
|
||||
- System preference detection
|
||||
- Persistent theme settings across sessions
|
||||
**Previous Highlights:**
|
||||
|
||||
**🖥️ Web-Based Viewer UI (v5.1.0):**
|
||||
|
||||
- Real-time memory stream visualization at http://localhost:37777
|
||||
- Server-Sent Events (SSE) for instant updates
|
||||
- Infinite scroll pagination with project filtering
|
||||
|
||||
**⚡ Smart Install Caching (v5.0.3):**
|
||||
|
||||
- Eliminated redundant npm installs (2-5s → 10ms)
|
||||
- Caches version state, only installs when needed
|
||||
|
||||
**🔍 Hybrid Search Architecture (v5.0.0):**
|
||||
|
||||
- Chroma vector database for semantic search
|
||||
- Combined with FTS5 keyword search
|
||||
- 90-day recency filtering
|
||||
- **v5.5.0**: mem-search skill enhancement with 100% effectiveness rate
|
||||
- **v5.4.0**: Skill-based search architecture (~2,250 tokens saved per session)
|
||||
- **v5.1.2**: Theme toggle for light/dark mode in viewer UI
|
||||
- **v5.1.0**: Web-based viewer UI with real-time updates
|
||||
- **v5.0.3**: Smart install caching (2-5s → 10ms)
|
||||
- **v5.0.0**: Hybrid search with Chroma vector database
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
@@ -286,7 +277,7 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
|
||||
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
|
||||
|
||||
See [Configuration Guide](docs/configuration.mdx) for details.
|
||||
See [Configuration Guide](https://docs.claude-mem.ai/configuration) for details.
|
||||
|
||||
---
|
||||
|
||||
@@ -309,7 +300,7 @@ npm run worker:start
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
See [Development Guide](docs/development.mdx) for detailed instructions.
|
||||
See [Development Guide](https://docs.claude-mem.ai/development) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
@@ -317,11 +308,7 @@ See [Development Guide](docs/development.mdx) for detailed instructions.
|
||||
|
||||
**Quick Diagnostic:**
|
||||
|
||||
Run the troubleshooting skill for automated diagnosis and fixes:
|
||||
|
||||
```
|
||||
/skill troubleshoot
|
||||
```
|
||||
If you're experiencing issues, describe the problem to Claude and the troubleshoot skill will automatically activate to diagnose and provide fixes.
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
@@ -330,7 +317,7 @@ Run the troubleshooting skill for automated diagnosis and fixes:
|
||||
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
||||
- Search not working → Check FTS5 tables exist
|
||||
|
||||
See [Troubleshooting Guide](docs/troubleshooting.mdx) for complete solutions.
|
||||
See [Troubleshooting Guide](https://docs.claude-mem.ai/troubleshooting) for complete solutions.
|
||||
|
||||
---
|
||||
|
||||
@@ -344,7 +331,7 @@ Contributions are welcome! Please:
|
||||
4. Update documentation
|
||||
5. Submit a Pull Request
|
||||
|
||||
See [Development Guide](docs/development.mdx) for contribution workflow.
|
||||
See [Development Guide](https://docs.claude-mem.ai/development) for contribution workflow.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Security Summary - CWD Context Fix
|
||||
|
||||
## Security Scan Results
|
||||
|
||||
### CodeQL Analysis
|
||||
- **Status**: ✅ PASSED
|
||||
- **Vulnerabilities Found**: 0
|
||||
- **Language**: JavaScript
|
||||
- **Scan Date**: 2025-11-10
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. Input Validation
|
||||
The `cwd` field is treated as untrusted user input:
|
||||
- ✅ Optional field (`cwd?: string`) - missing values default to empty string
|
||||
- ✅ No direct file system operations using CWD
|
||||
- ✅ CWD is only used for context in prompts (read-only)
|
||||
- ✅ No shell command injection risk (not passed to exec/spawn)
|
||||
|
||||
### 2. Data Flow Security
|
||||
```
|
||||
Hook Input → Worker API → SessionManager → SDK Agent → Prompt Text
|
||||
```
|
||||
|
||||
- ✅ CWD passed through JSON serialization (escaped)
|
||||
- ✅ No SQL injection risk (not stored in database)
|
||||
- ✅ No XSS risk (used in backend prompts, not web UI)
|
||||
- ✅ No path traversal risk (not used for file operations)
|
||||
|
||||
### 3. Prompt Injection Considerations
|
||||
The CWD is included in XML prompts sent to the SDK agent:
|
||||
```xml
|
||||
<tool_cwd>/home/user/project</tool_cwd>
|
||||
```
|
||||
|
||||
**Risk Assessment**: LOW
|
||||
- CWD comes from Claude Code runtime (trusted source)
|
||||
- Claude Code validates and sanitizes session context
|
||||
- SDK agent operates in isolated subprocess
|
||||
- No user-controlled prompt injection vector
|
||||
|
||||
### 4. Backward Compatibility
|
||||
- ✅ Optional field - no breaking changes
|
||||
- ✅ Graceful degradation when CWD missing
|
||||
- ✅ No changes to existing security boundaries
|
||||
- ✅ No new external dependencies
|
||||
|
||||
## Security Best Practices Applied
|
||||
|
||||
1. **Defense in Depth**: CWD is display-only context, not used for authorization
|
||||
2. **Least Privilege**: No elevated permissions required
|
||||
3. **Input Validation**: Type-safe interfaces with optional fields
|
||||
4. **Safe Defaults**: Missing CWD defaults to empty string (safe)
|
||||
5. **Immutability**: CWD is read-only once extracted from hook input
|
||||
|
||||
## Potential Future Considerations
|
||||
|
||||
While the current implementation is secure, future enhancements should consider:
|
||||
|
||||
1. **Path Sanitization**: If CWD is ever used for file operations, implement strict path validation
|
||||
2. **Length Limits**: Consider max length for CWD field to prevent buffer issues
|
||||
3. **Allowlist**: If needed, implement allowlist of permitted directories
|
||||
4. **Audit Logging**: Log CWD values for security monitoring (if required)
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **No security vulnerabilities identified**
|
||||
✅ **Implementation follows security best practices**
|
||||
✅ **Ready for production deployment**
|
||||
|
||||
The CWD context fix introduces no new security risks and maintains the existing security posture of the claude-mem plugin.
|
||||
@@ -0,0 +1,424 @@
|
||||
"""Pydantic models for Claude Code transcript JSON structures.
|
||||
|
||||
Enhanced to leverage official Anthropic types where beneficial.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Union, Optional, Dict, Literal, cast
|
||||
from pydantic import BaseModel
|
||||
|
||||
from anthropic.types import Message as AnthropicMessage
|
||||
from anthropic.types import StopReason
|
||||
from anthropic.types import Usage as AnthropicUsage
|
||||
from anthropic.types.content_block import ContentBlock
|
||||
|
||||
|
||||
class TodoItem(BaseModel):
|
||||
id: str
|
||||
content: str
|
||||
status: Literal["pending", "in_progress", "completed"]
|
||||
priority: Literal["high", "medium", "low"]
|
||||
|
||||
|
||||
class UsageInfo(BaseModel):
|
||||
"""Token usage information that extends Anthropic's Usage type to handle optional fields."""
|
||||
|
||||
input_tokens: Optional[int] = None
|
||||
cache_creation_input_tokens: Optional[int] = None
|
||||
cache_read_input_tokens: Optional[int] = None
|
||||
output_tokens: Optional[int] = None
|
||||
service_tier: Optional[str] = None
|
||||
server_tool_use: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_anthropic_usage(self) -> Optional[AnthropicUsage]:
|
||||
"""Convert to Anthropic Usage type if both required fields are present."""
|
||||
if self.input_tokens is not None and self.output_tokens is not None:
|
||||
return AnthropicUsage(
|
||||
input_tokens=self.input_tokens,
|
||||
output_tokens=self.output_tokens,
|
||||
cache_creation_input_tokens=self.cache_creation_input_tokens,
|
||||
cache_read_input_tokens=self.cache_read_input_tokens,
|
||||
service_tier=self.service_tier, # type: ignore
|
||||
server_tool_use=self.server_tool_use, # type: ignore
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_anthropic_usage(cls, usage: AnthropicUsage) -> "UsageInfo":
|
||||
"""Create UsageInfo from Anthropic Usage."""
|
||||
return cls(
|
||||
input_tokens=usage.input_tokens,
|
||||
output_tokens=usage.output_tokens,
|
||||
cache_creation_input_tokens=usage.cache_creation_input_tokens,
|
||||
cache_read_input_tokens=usage.cache_read_input_tokens,
|
||||
service_tier=usage.service_tier,
|
||||
server_tool_use=usage.server_tool_use.model_dump()
|
||||
if usage.server_tool_use
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
class TextContent(BaseModel):
|
||||
type: Literal["text"]
|
||||
text: str
|
||||
|
||||
|
||||
class ToolUseContent(BaseModel):
|
||||
type: Literal["tool_use"]
|
||||
id: str
|
||||
name: str
|
||||
input: Dict[str, Any]
|
||||
|
||||
|
||||
class ToolResultContent(BaseModel):
|
||||
type: Literal["tool_result"]
|
||||
tool_use_id: str
|
||||
content: Union[str, List[Dict[str, Any]]]
|
||||
is_error: Optional[bool] = None
|
||||
|
||||
|
||||
class ThinkingContent(BaseModel):
|
||||
type: Literal["thinking"]
|
||||
thinking: str
|
||||
signature: Optional[str] = None
|
||||
|
||||
|
||||
class ImageSource(BaseModel):
|
||||
type: Literal["base64"]
|
||||
media_type: str
|
||||
data: str
|
||||
|
||||
|
||||
class ImageContent(BaseModel):
|
||||
type: Literal["image"]
|
||||
source: ImageSource
|
||||
|
||||
|
||||
# Enhanced ContentItem to include official Anthropic ContentBlock types
|
||||
ContentItem = Union[
|
||||
TextContent,
|
||||
ToolUseContent,
|
||||
ToolResultContent,
|
||||
ThinkingContent,
|
||||
ImageContent,
|
||||
ContentBlock, # Official Anthropic content block types
|
||||
]
|
||||
|
||||
|
||||
class UserMessage(BaseModel):
|
||||
role: Literal["user"]
|
||||
content: Union[str, List[ContentItem]]
|
||||
|
||||
|
||||
class AssistantMessage(BaseModel):
|
||||
"""Assistant message model compatible with Anthropic's Message type."""
|
||||
|
||||
id: str
|
||||
type: Literal["message"]
|
||||
role: Literal["assistant"]
|
||||
model: str
|
||||
content: List[ContentItem]
|
||||
stop_reason: Optional[StopReason] = None
|
||||
stop_sequence: Optional[str] = None
|
||||
usage: Optional[UsageInfo] = None
|
||||
|
||||
@classmethod
|
||||
def from_anthropic_message(
|
||||
cls, anthropic_msg: AnthropicMessage
|
||||
) -> "AssistantMessage":
|
||||
"""Create AssistantMessage from official Anthropic Message."""
|
||||
# Convert Anthropic Message to our format, preserving official types where possible
|
||||
return cls(
|
||||
id=anthropic_msg.id,
|
||||
type=anthropic_msg.type,
|
||||
role=anthropic_msg.role,
|
||||
model=anthropic_msg.model,
|
||||
content=list(
|
||||
anthropic_msg.content
|
||||
), # Convert to list for ContentItem compatibility
|
||||
stop_reason=anthropic_msg.stop_reason,
|
||||
stop_sequence=anthropic_msg.stop_sequence,
|
||||
usage=normalize_usage_info(anthropic_msg.usage),
|
||||
)
|
||||
|
||||
|
||||
class FileInfo(BaseModel):
|
||||
filePath: str
|
||||
content: str
|
||||
numLines: int
|
||||
startLine: int
|
||||
totalLines: int
|
||||
|
||||
|
||||
class FileReadResult(BaseModel):
|
||||
type: Literal["text"]
|
||||
file: FileInfo
|
||||
|
||||
|
||||
class CommandResult(BaseModel):
|
||||
stdout: str
|
||||
stderr: str
|
||||
interrupted: bool
|
||||
isImage: bool
|
||||
|
||||
|
||||
class TodoResult(BaseModel):
|
||||
oldTodos: List[TodoItem]
|
||||
newTodos: List[TodoItem]
|
||||
|
||||
|
||||
class EditResult(BaseModel):
|
||||
oldString: Optional[str] = None
|
||||
newString: Optional[str] = None
|
||||
replaceAll: Optional[bool] = None
|
||||
originalFile: Optional[str] = None
|
||||
structuredPatch: Optional[Any] = None
|
||||
userModified: Optional[bool] = None
|
||||
|
||||
|
||||
ToolUseResult = Union[
|
||||
str,
|
||||
List[TodoItem],
|
||||
FileReadResult,
|
||||
CommandResult,
|
||||
TodoResult,
|
||||
EditResult,
|
||||
List[ContentItem],
|
||||
]
|
||||
|
||||
|
||||
class BaseTranscriptEntry(BaseModel):
|
||||
parentUuid: Optional[str]
|
||||
isSidechain: bool
|
||||
userType: str
|
||||
cwd: str
|
||||
sessionId: str
|
||||
version: str
|
||||
uuid: str
|
||||
timestamp: str
|
||||
isMeta: Optional[bool] = None
|
||||
|
||||
|
||||
class UserTranscriptEntry(BaseTranscriptEntry):
|
||||
type: Literal["user"]
|
||||
message: UserMessage
|
||||
toolUseResult: Optional[ToolUseResult] = None
|
||||
|
||||
|
||||
class AssistantTranscriptEntry(BaseTranscriptEntry):
|
||||
type: Literal["assistant"]
|
||||
message: AssistantMessage
|
||||
requestId: Optional[str] = None
|
||||
|
||||
|
||||
class SummaryTranscriptEntry(BaseModel):
|
||||
type: Literal["summary"]
|
||||
summary: str
|
||||
leafUuid: str
|
||||
cwd: Optional[str] = None
|
||||
|
||||
|
||||
class SystemTranscriptEntry(BaseTranscriptEntry):
|
||||
"""System messages like warnings, notifications, etc."""
|
||||
|
||||
type: Literal["system"]
|
||||
content: str
|
||||
level: Optional[str] = None # e.g., "warning", "info", "error"
|
||||
|
||||
|
||||
class QueueOperationTranscriptEntry(BaseModel):
|
||||
"""Queue operations (enqueue/dequeue) for message queueing tracking.
|
||||
|
||||
These are internal operations that track when messages are queued and dequeued.
|
||||
They are parsed but not rendered, as the content duplicates actual user messages.
|
||||
"""
|
||||
|
||||
type: Literal["queue-operation"]
|
||||
operation: Literal["enqueue", "dequeue"]
|
||||
timestamp: str
|
||||
sessionId: str
|
||||
content: Optional[List[ContentItem]] = None # Only present for enqueue operations
|
||||
|
||||
|
||||
TranscriptEntry = Union[
|
||||
UserTranscriptEntry,
|
||||
AssistantTranscriptEntry,
|
||||
SummaryTranscriptEntry,
|
||||
SystemTranscriptEntry,
|
||||
QueueOperationTranscriptEntry,
|
||||
]
|
||||
|
||||
|
||||
def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]:
|
||||
"""Normalize usage data to be compatible with both custom and Anthropic formats."""
|
||||
if usage_data is None:
|
||||
return None
|
||||
|
||||
# If it's already a UsageInfo instance, return as-is
|
||||
if isinstance(usage_data, UsageInfo):
|
||||
return usage_data
|
||||
|
||||
# If it's an Anthropic Usage instance, convert using our method
|
||||
if isinstance(usage_data, AnthropicUsage):
|
||||
return UsageInfo.from_anthropic_usage(usage_data)
|
||||
|
||||
# If it has the shape of an Anthropic Usage, try to construct it first
|
||||
if hasattr(usage_data, "input_tokens") and hasattr(usage_data, "output_tokens"):
|
||||
try:
|
||||
# Try to create an Anthropic Usage first
|
||||
anthropic_usage = AnthropicUsage.model_validate(usage_data)
|
||||
return UsageInfo.from_anthropic_usage(anthropic_usage)
|
||||
except Exception:
|
||||
# Fall back to direct conversion
|
||||
return UsageInfo(
|
||||
input_tokens=getattr(usage_data, "input_tokens", None),
|
||||
cache_creation_input_tokens=getattr(
|
||||
usage_data, "cache_creation_input_tokens", None
|
||||
),
|
||||
cache_read_input_tokens=getattr(
|
||||
usage_data, "cache_read_input_tokens", None
|
||||
),
|
||||
output_tokens=getattr(usage_data, "output_tokens", None),
|
||||
service_tier=getattr(usage_data, "service_tier", None),
|
||||
server_tool_use=getattr(usage_data, "server_tool_use", None),
|
||||
)
|
||||
|
||||
# If it's a dict, validate and convert to our format
|
||||
if isinstance(usage_data, dict):
|
||||
return UsageInfo.model_validate(usage_data)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_content_item(item_data: Dict[str, Any]) -> ContentItem:
|
||||
"""Parse a content item using enhanced approach with Anthropic types."""
|
||||
try:
|
||||
content_type = item_data.get("type", "")
|
||||
|
||||
# Try official Anthropic types first for better future compatibility
|
||||
if content_type == "text":
|
||||
try:
|
||||
from anthropic.types.text_block import TextBlock
|
||||
|
||||
return TextBlock.model_validate(item_data)
|
||||
except Exception:
|
||||
return TextContent.model_validate(item_data)
|
||||
elif content_type == "tool_use":
|
||||
try:
|
||||
from anthropic.types.tool_use_block import ToolUseBlock
|
||||
|
||||
return ToolUseBlock.model_validate(item_data)
|
||||
except Exception:
|
||||
return ToolUseContent.model_validate(item_data)
|
||||
elif content_type == "thinking":
|
||||
try:
|
||||
from anthropic.types.thinking_block import ThinkingBlock
|
||||
|
||||
return ThinkingBlock.model_validate(item_data)
|
||||
except Exception:
|
||||
return ThinkingContent.model_validate(item_data)
|
||||
elif content_type == "tool_result":
|
||||
return ToolResultContent.model_validate(item_data)
|
||||
elif content_type == "image":
|
||||
return ImageContent.model_validate(item_data)
|
||||
else:
|
||||
# Fallback to text content for unknown types
|
||||
return TextContent(type="text", text=str(item_data))
|
||||
except Exception:
|
||||
return TextContent(type="text", text=str(item_data))
|
||||
|
||||
|
||||
def parse_message_content(content_data: Any) -> Union[str, List[ContentItem]]:
|
||||
"""Parse message content, handling both string and list formats."""
|
||||
if isinstance(content_data, str):
|
||||
return content_data
|
||||
elif isinstance(content_data, list):
|
||||
content_list = cast(List[Dict[str, Any]], content_data)
|
||||
return [parse_content_item(item) for item in content_list]
|
||||
else:
|
||||
return str(content_data)
|
||||
|
||||
|
||||
def parse_transcript_entry(data: Dict[str, Any]) -> TranscriptEntry:
|
||||
"""
|
||||
Parse a JSON dictionary into the appropriate TranscriptEntry type.
|
||||
|
||||
Enhanced to optionally use official Anthropic types for assistant messages.
|
||||
|
||||
Args:
|
||||
data: Dictionary parsed from JSON
|
||||
|
||||
Returns:
|
||||
The appropriate TranscriptEntry subclass
|
||||
|
||||
Raises:
|
||||
ValueError: If the data doesn't match any known transcript entry type
|
||||
"""
|
||||
entry_type = data.get("type")
|
||||
|
||||
if entry_type == "user":
|
||||
# Parse message content if present
|
||||
data_copy = data.copy()
|
||||
if "message" in data_copy and "content" in data_copy["message"]:
|
||||
data_copy["message"] = data_copy["message"].copy()
|
||||
data_copy["message"]["content"] = parse_message_content(
|
||||
data_copy["message"]["content"]
|
||||
)
|
||||
# Parse toolUseResult if present and it's a list of content items
|
||||
if "toolUseResult" in data_copy and isinstance(
|
||||
data_copy["toolUseResult"], list
|
||||
):
|
||||
# Check if it's a list of content items (MCP tool results)
|
||||
tool_use_result = cast(List[Any], data_copy["toolUseResult"])
|
||||
if (
|
||||
tool_use_result
|
||||
and isinstance(tool_use_result[0], dict)
|
||||
and "type" in tool_use_result[0]
|
||||
):
|
||||
data_copy["toolUseResult"] = [
|
||||
parse_content_item(cast(Dict[str, Any], item))
|
||||
for item in tool_use_result
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
return UserTranscriptEntry.model_validate(data_copy)
|
||||
|
||||
elif entry_type == "assistant":
|
||||
# Enhanced assistant message parsing with optional Anthropic types
|
||||
data_copy = data.copy()
|
||||
|
||||
# Validate compatibility with official Anthropic Message type
|
||||
if "message" in data_copy:
|
||||
try:
|
||||
message_data = data_copy["message"]
|
||||
AnthropicMessage.model_validate(message_data)
|
||||
# Successfully validated - our data is compatible with official Anthropic types
|
||||
except Exception:
|
||||
# Validation failed - continue with standard parsing
|
||||
pass
|
||||
|
||||
# Standard parsing path (works for all cases)
|
||||
if "message" in data_copy and "content" in data_copy["message"]:
|
||||
message_copy = data_copy["message"].copy()
|
||||
message_copy["content"] = parse_message_content(message_copy["content"])
|
||||
|
||||
# Normalize usage data to support both Anthropic and custom formats
|
||||
if "usage" in message_copy:
|
||||
message_copy["usage"] = normalize_usage_info(message_copy["usage"])
|
||||
|
||||
data_copy["message"] = message_copy
|
||||
return AssistantTranscriptEntry.model_validate(data_copy)
|
||||
|
||||
elif entry_type == "summary":
|
||||
return SummaryTranscriptEntry.model_validate(data)
|
||||
|
||||
elif entry_type == "system":
|
||||
return SystemTranscriptEntry.model_validate(data)
|
||||
|
||||
elif entry_type == "queue-operation":
|
||||
# Parse content if present (only in enqueue operations)
|
||||
data_copy = data.copy()
|
||||
if "content" in data_copy and isinstance(data_copy["content"], list):
|
||||
data_copy["content"] = parse_message_content(data_copy["content"])
|
||||
return QueueOperationTranscriptEntry.model_validate(data_copy)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown transcript entry type: {entry_type}")
|
||||
@@ -148,16 +148,19 @@ When Claude invokes the skill:
|
||||
|
||||
## Search Architecture
|
||||
|
||||
### Hybrid Search System
|
||||
### 3-Layer Hybrid Search System
|
||||
|
||||
claude-mem uses a **hybrid search architecture** combining:
|
||||
claude-mem uses a **3-layer sequential search architecture** that mimics human long-term memory:
|
||||
|
||||
1. **SQLite FTS5 (Full-Text Search)** - Keyword-based search
|
||||
2. **ChromaDB (Vector Search)** - Semantic similarity search
|
||||
**Storage Flow (Write Path):**
|
||||
1. **SQLite First** - Data written synchronously to SQLite (fast, immediate access)
|
||||
2. **ChromaDB Background Sync** - Worker asynchronously generates embeddings and syncs to ChromaDB
|
||||
|
||||
**Search Flow (Read Path - Sequential, NOT parallel):**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Search Request Flow │
|
||||
│ 3-Layer Sequential Search Flow │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -166,61 +169,70 @@ claude-mem uses a **hybrid search architecture** combining:
|
||||
│ /api/search/* │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
▼ ▼
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
│ SessionSearch (FTS5) │ │ ChromaSync (Vector DB) │
|
||||
│ │ │ │
|
||||
│ Full-text keyword │ │ Semantic similarity │
|
||||
│ search on: │ │ search on: │
|
||||
│ - titles │ │ - narratives │
|
||||
│ - narratives │ │ - facts │
|
||||
│ - facts │ │ - file content │
|
||||
│ - concepts │ │ │
|
||||
│ │ │ Embeddings: │
|
||||
│ SQLite DB: │ │ - text-embedding-3-small│
|
||||
│ observations_fts │ │ - 90-day recency filter │
|
||||
│ sessions_fts │ │ │
|
||||
│ prompts_fts │ │ ChromaDB: │
|
||||
│ │ │ observations collection │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
│ │
|
||||
└─────────────┬─────────────┘
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Merged Results │
|
||||
│ - Deduplicated │
|
||||
│ - Sorted by relevance │
|
||||
│ - Formatted (index/full)│
|
||||
└─────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 1: Semantic Retrieval (ChromaDB) │
|
||||
│ ───────────────────────────────────────────────────────── │
|
||||
│ Vector similarity search finds semantically relevant items │
|
||||
│ Returns: observation IDs in index format (~50-100 tokens) │
|
||||
│ Filter: 90-day recency prioritizes recent work │
|
||||
│ Output: List of relevant observation IDs │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 2: Temporal Ordering (SQLite) │
|
||||
│ ───────────────────────────────────────────────────────── │
|
||||
│ Takes observation IDs from Layer 1 │
|
||||
│ Sorts by created_at timestamp (fast SQLite temporal query) │
|
||||
│ Identifies: MOST RECENT relevant observation │
|
||||
│ Why: ChromaDB doesn't easily query by date range sorted │
|
||||
│ Output: Top observation ID by time │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LAYER 3: Instant Context Timeline (SQLite) │
|
||||
│ ───────────────────────────────────────────────────────── │
|
||||
│ Uses top observation ID from Layer 2 as anchor │
|
||||
│ Retrieves N observations BEFORE and AFTER that point │
|
||||
│ Provides: "what led here" + "what happened next" context │
|
||||
│ This is the KILLER FEATURE: mimics human memory │
|
||||
│ Output: Timeline with temporal context │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Why This Architecture Exists:**
|
||||
|
||||
The problem: LLMs don't experience time linearly like humans do. Finding semantically relevant information isn't enough—you need temporal context.
|
||||
|
||||
The solution:
|
||||
- **ChromaDB** for "what's relevant" (semantic understanding)
|
||||
- **SQLite** for "when did it happen" (temporal ordering with fast date-range queries)
|
||||
- **Timeline** for "what was the context" (before/after observations)
|
||||
|
||||
Together, they mimic how humans recall: "I did X, which led to Y, then Z happened."
|
||||
|
||||
**Human Memory Analogy:**
|
||||
|
||||
Humans don't just remember isolated facts. They remember sequences: what they did before something, what happened after. The instant context timeline gives LLMs this same temporal awareness that humans experience naturally.
|
||||
|
||||
### Search Types
|
||||
|
||||
#### 1. Full-Text Search (FTS5)
|
||||
#### 1. Vector Search (ChromaDB) - PRIMARY Search Layer
|
||||
|
||||
**How it works:**
|
||||
- Uses SQLite FTS5 virtual tables for instant keyword matching
|
||||
- Supports boolean operators: `AND`, `OR`, `NOT`, `NEAR`, `*` (wildcard)
|
||||
- Ranks results by BM25 relevance scoring
|
||||
- Sub-100ms performance on 8,000+ observations
|
||||
|
||||
**Example query:**
|
||||
```sql
|
||||
-- User asks: "How did we implement JWT authentication?"
|
||||
SELECT * FROM observations_fts
|
||||
WHERE observations_fts MATCH 'JWT AND authentication'
|
||||
ORDER BY rank
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
#### 2. Vector Search (ChromaDB)
|
||||
**Role:** Layer 1 - Semantic Retrieval
|
||||
|
||||
**How it works:**
|
||||
- Text is embedded using OpenAI's `text-embedding-3-small` model
|
||||
- Vector similarity search finds semantically related content
|
||||
- Vector similarity search finds semantically related content, not just keyword matches
|
||||
- 90-day recency filter prioritizes recent work
|
||||
- Combined with keyword search for hybrid results
|
||||
- Returns observation IDs for temporal processing in Layer 2
|
||||
|
||||
**Why it's primary:**
|
||||
- Understands meaning, not just keywords ("auth flow" matches "JWT implementation")
|
||||
- Finds relevant work even when you don't know exact terms used
|
||||
- Semantic understanding crucial for LLM memory retrieval
|
||||
|
||||
**Example query:**
|
||||
```python
|
||||
@@ -230,6 +242,37 @@ collection.query(
|
||||
n_results=20,
|
||||
where={"created_at": {"$gte": ninety_days_ago}}
|
||||
)
|
||||
# Returns: observation IDs semantically related to login/auth
|
||||
```
|
||||
|
||||
#### 2. Full-Text Search (FTS5) - Supporting Layer
|
||||
|
||||
**Role:** Layer 2 & 3 - Temporal Ordering and Timeline Context
|
||||
|
||||
**How it works:**
|
||||
- Uses SQLite FTS5 virtual tables for instant keyword matching
|
||||
- Supports boolean operators: `AND`, `OR`, `NOT`, `NEAR`, `*` (wildcard)
|
||||
- Fast temporal queries with date-range sorting
|
||||
- Sub-100ms performance on 8,000+ observations
|
||||
|
||||
**Why it's supporting:**
|
||||
- ChromaDB handles semantic "what's relevant"
|
||||
- SQLite/FTS5 handles temporal "when did it happen" and "what came before/after"
|
||||
- Optimized for timeline queries and date-based sorting
|
||||
|
||||
**Example query:**
|
||||
```sql
|
||||
-- Takes observation IDs from ChromaDB, sorts by time
|
||||
SELECT * FROM observations
|
||||
WHERE id IN (/* IDs from ChromaDB */)
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Then retrieves timeline context around that observation
|
||||
SELECT * FROM observations
|
||||
WHERE created_at_epoch < anchor_timestamp
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10; -- "what led here"
|
||||
```
|
||||
|
||||
#### 3. Structured Filters
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
# Endless Mode: Real-Time Context Compression Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
"Endless Mode" is an optional feature that enables Claude sessions to run indefinitely by transparently compressing tool use transcripts in real-time. Using an in-memory transformation layer in the worker service, heavy tool outputs are dynamically replaced with lightweight observations during session resume—without modifying the immutable source transcripts. This allows sessions to continue for weeks or months without hitting context window limits, while preserving full conversation history and maintaining zero risk of data corruption.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Current Behavior
|
||||
|
||||
Claude sessions accumulate full tool transcripts in the context window:
|
||||
- File reads: 5k-10k tokens per read
|
||||
- Bash outputs: 1k-5k tokens per command
|
||||
- Search results: 2k-8k tokens per search
|
||||
- Total context limit: ~200k tokens
|
||||
|
||||
When the context window fills, users must start a new session, losing conversational continuity.
|
||||
|
||||
### What Happens Today
|
||||
|
||||
1. Tool executes during session
|
||||
2. PostToolUse hook captures tool data
|
||||
3. Worker creates compressed observation (~200-500 tokens)
|
||||
4. **But**: Full tool transcript stays in Claude's context window
|
||||
5. **Observation only helps next session** via SessionStart injection
|
||||
|
||||
### The Gap
|
||||
|
||||
Observations exist and are created in real-time, but they're not used to compress the **current** session's context. We have the compressed data, we just don't apply it to the active session.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution: Endless Mode
|
||||
|
||||
### Core Concept
|
||||
|
||||
When a session resumes (either after restart or during continuation), **transform messages in memory** by replacing heavy tool use content with lightweight observations before feeding them to the Agent SDK. The source transcript remains immutable on disk.
|
||||
|
||||
### Architecture Principle
|
||||
|
||||
**Immutable Storage + Ephemeral Transform = Safe Compression**
|
||||
|
||||
```
|
||||
Disk (never modified) Memory (transform) Agent SDK
|
||||
────────────────────── ────────────────────── ────────────────
|
||||
transcript.jsonl Load messages Resume session
|
||||
tool_use_abc → Look up observation → with compressed
|
||||
tool_use_def Replace content context
|
||||
tool_use_xyz Feed to SDK
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
1. **Immutable**: Original transcripts never modified
|
||||
2. **Non-destructive**: Full history preserved on disk
|
||||
3. **No duplication**: No forks, no copies
|
||||
4. **Transparent**: User sees same conversation, compression is under the hood
|
||||
5. **Optional**: Feature flag allows users to opt-in/out
|
||||
6. **Reversible**: Can always read original transcript
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### Session Resume Flow (Endless Mode Enabled)
|
||||
|
||||
```
|
||||
1. User continues session / Claude Code restarts
|
||||
↓
|
||||
2. Worker service intercepts resume request
|
||||
↓
|
||||
3. Load transcript JSONL from disk (immutable)
|
||||
↓
|
||||
4. Transform Loop:
|
||||
For each message in transcript:
|
||||
- If tool_use message:
|
||||
- Query SQLite: SELECT observation WHERE tool_use_id = ?
|
||||
- Replace tool content with observation (facts, narrative, concepts)
|
||||
- If other message type:
|
||||
- Pass through unchanged
|
||||
↓
|
||||
5. Feed transformed messages to Agent SDK
|
||||
↓
|
||||
6. Agent SDK resumes session with compressed context
|
||||
↓
|
||||
7. New tool uses append to original transcript (normal flow)
|
||||
↓
|
||||
8. Next resume: Loop repeats, new tool uses also get compressed
|
||||
```
|
||||
|
||||
### Session Resume Flow (Endless Mode Disabled)
|
||||
|
||||
```
|
||||
1. User continues session
|
||||
↓
|
||||
2. Load transcript JSONL from disk
|
||||
↓
|
||||
3. Feed messages directly to Agent SDK (no transformation)
|
||||
↓
|
||||
4. Session resumes with full tool transcripts (current behavior)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation (Week 1)
|
||||
|
||||
**Goal**: Set up infrastructure for transformation layer
|
||||
|
||||
Tasks:
|
||||
1. Add `tool_use_id` column to observations table (SQLite schema migration)
|
||||
2. Update PostToolUse hook to capture and store tool_use_id
|
||||
3. Create `TransformLayer` class in worker service
|
||||
4. Add `CLAUDE_MEM_ENDLESS_MODE` environment variable (default: false)
|
||||
5. Write tests for observation lookup by tool_use_id
|
||||
|
||||
**Deliverable**: Database schema updated, tool_use_ids being captured
|
||||
|
||||
### Phase 2: Transform Logic (Week 2)
|
||||
|
||||
**Goal**: Build message transformation engine
|
||||
|
||||
Tasks:
|
||||
1. Implement `TransformLayer.transformMessages(messages)` function
|
||||
2. Tool use detection logic (identify tool_use messages in transcript)
|
||||
3. Observation lookup and replacement logic
|
||||
4. Fallback handling (if observation missing, keep original content)
|
||||
5. Message serialization/deserialization
|
||||
|
||||
**Deliverable**: Working transform function that compresses messages in memory
|
||||
|
||||
### Phase 3: Agent SDK Integration (Week 2-3)
|
||||
|
||||
**Goal**: Wire transform layer into session resume flow
|
||||
|
||||
Tasks:
|
||||
1. Identify where worker service resumes Agent SDK sessions
|
||||
2. Inject transform layer before session resume
|
||||
3. Add feature flag check (only transform if endless mode enabled)
|
||||
4. Logging and instrumentation (track compression ratios, transform time)
|
||||
5. Error handling and graceful degradation
|
||||
|
||||
**Deliverable**: Worker service can resume sessions with compressed context
|
||||
|
||||
### Phase 4: Testing & Validation (Week 3-4)
|
||||
|
||||
**Goal**: Verify endless mode works correctly
|
||||
|
||||
Tasks:
|
||||
1. Create test session with 50+ tool uses
|
||||
2. Enable endless mode and resume session
|
||||
3. Verify context window usage (should be dramatically lower)
|
||||
4. Test conversation quality (does Claude have enough context?)
|
||||
5. Measure performance (transform latency, lookup speed)
|
||||
6. Edge case testing (missing observations, malformed transcripts)
|
||||
|
||||
**Deliverable**: Endless mode working in test environment
|
||||
|
||||
### Phase 5: Beta Release (Week 4+)
|
||||
|
||||
**Goal**: Release to power users for feedback
|
||||
|
||||
Tasks:
|
||||
1. Documentation (how to enable, what to expect, how to disable)
|
||||
2. Add endless mode toggle to viewer UI
|
||||
3. Monitoring and observability (track usage, failures, compression stats)
|
||||
4. Collect feedback from beta users
|
||||
5. Iterate based on real-world usage
|
||||
|
||||
**Deliverable**: Endless mode available as opt-in beta feature
|
||||
|
||||
---
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- Add to observations table
|
||||
ALTER TABLE observations ADD COLUMN tool_use_id TEXT UNIQUE;
|
||||
CREATE INDEX idx_observations_tool_use_id ON observations(tool_use_id);
|
||||
```
|
||||
|
||||
### Worker Service API
|
||||
|
||||
```typescript
|
||||
interface TransformLayerConfig {
|
||||
enabled: boolean; // CLAUDE_MEM_ENDLESS_MODE
|
||||
fallbackToOriginal: boolean; // If observation missing, use full content
|
||||
maxLookupTime: number; // Timeout for SQLite queries
|
||||
}
|
||||
|
||||
class TransformLayer {
|
||||
constructor(config: TransformLayerConfig, db: SessionStore);
|
||||
|
||||
// Main transform function
|
||||
async transformMessages(messages: Message[]): Promise<Message[]>;
|
||||
|
||||
// Helper functions
|
||||
private async lookupObservation(toolUseId: string): Promise<Observation | null>;
|
||||
private replaceToolContent(message: Message, observation: Observation): Message;
|
||||
private isToolUseMessage(message: Message): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Agent SDK Integration Point
|
||||
|
||||
```typescript
|
||||
// In worker service session resume logic
|
||||
async function resumeSession(sessionId: string, transcriptPath: string) {
|
||||
const messages = await loadTranscript(transcriptPath);
|
||||
|
||||
// Transform layer (only if endless mode enabled)
|
||||
const transformedMessages = config.endlessMode
|
||||
? await transformLayer.transformMessages(messages)
|
||||
: messages;
|
||||
|
||||
// Resume with transformed (or original) messages
|
||||
return await agentSDK.resumeSession({
|
||||
sessionId,
|
||||
messages: transformedMessages
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Risk 1: Information Loss
|
||||
|
||||
**Risk**: Compressed observations may lose critical details that Claude needs to reference later.
|
||||
|
||||
**Mitigation**:
|
||||
- Make endless mode optional (users can disable if quality degrades)
|
||||
- Improve observation quality (better prompts, more comprehensive facts)
|
||||
- Hybrid approach: Keep recent N tool uses in full, compress older ones
|
||||
- Monitor conversation quality metrics
|
||||
|
||||
### Risk 2: Transform Performance
|
||||
|
||||
**Risk**: Looking up observations for 100+ tool uses during resume could be slow.
|
||||
|
||||
**Mitigation**:
|
||||
- Index tool_use_id in SQLite (O(log n) lookups)
|
||||
- Batch queries (single SELECT with IN clause)
|
||||
- Measure and optimize (target <100ms for typical session)
|
||||
- Cache observations in memory during session
|
||||
|
||||
### Risk 3: Missing Observations
|
||||
|
||||
**Risk**: Tool use executed but observation not yet created (async worker lag).
|
||||
|
||||
**Mitigation**:
|
||||
- Fallback to original content if observation missing
|
||||
- Log when fallback occurs (helps identify worker performance issues)
|
||||
- Allow observations to be created retroactively
|
||||
- Consider synchronous observation creation for critical tools
|
||||
|
||||
### Risk 4: Transcript Corruption
|
||||
|
||||
**Risk**: Bug in transform layer could corrupt user conversations.
|
||||
|
||||
**Mitigation**:
|
||||
- **Never modify source transcripts** (read-only)
|
||||
- Transform happens in memory only
|
||||
- Extensive testing before beta release
|
||||
- Feature flag allows instant disable if issues found
|
||||
- Keep full audit trail in logs
|
||||
|
||||
### Risk 5: Agent SDK Compatibility
|
||||
|
||||
**Risk**: Agent SDK updates could break transform layer integration.
|
||||
|
||||
**Mitigation**:
|
||||
- Document exact Agent SDK version requirements
|
||||
- Monitor Agent SDK release notes
|
||||
- Test against new SDK versions before upgrading
|
||||
- Graceful degradation if SDK changes detected
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Proof of Concept Success
|
||||
|
||||
- [ ] Transform layer successfully compresses a 50-tool-use session
|
||||
- [ ] Context window usage reduced by 80%+ compared to uncompressed
|
||||
- [ ] Session resumes without errors
|
||||
- [ ] Conversation quality remains high (subjective evaluation)
|
||||
|
||||
### Beta Release Success
|
||||
|
||||
- [ ] 10+ users running endless mode without issues
|
||||
- [ ] Average context savings: 85%+ across all sessions
|
||||
- [ ] Transform latency: <200ms for typical resume
|
||||
- [ ] Zero transcript corruption incidents
|
||||
- [ ] Positive user feedback on conversation continuity
|
||||
|
||||
### Production Success
|
||||
|
||||
- [ ] Endless mode becomes default setting
|
||||
- [ ] Sessions running for weeks/months without context issues
|
||||
- [ ] Context window exhaustion becomes rare edge case
|
||||
- [ ] User-reported "session too long" issues drop to near zero
|
||||
- [ ] Transform layer performance scales to 1000+ tool use sessions
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable endless mode (default: false)
|
||||
CLAUDE_MEM_ENDLESS_MODE=true
|
||||
|
||||
# Fallback behavior if observation missing (default: true)
|
||||
CLAUDE_MEM_TRANSFORM_FALLBACK=true
|
||||
|
||||
# Max time to wait for observation lookup (default: 500ms)
|
||||
CLAUDE_MEM_TRANSFORM_TIMEOUT=500
|
||||
|
||||
# Keep recent N tool uses uncompressed (default: 0, compress all)
|
||||
CLAUDE_MEM_TRANSFORM_KEEP_RECENT=0
|
||||
```
|
||||
|
||||
### User Controls
|
||||
|
||||
```typescript
|
||||
// Future: UI toggle in viewer
|
||||
interface EndlessModeSettings {
|
||||
enabled: boolean;
|
||||
keepRecentToolUses: number; // Hybrid mode
|
||||
fallbackToOriginal: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Economics: Before vs. After
|
||||
|
||||
### Example Session (50 tool uses)
|
||||
|
||||
**Before (Endless Mode OFF):**
|
||||
```
|
||||
File reads: 10 × 8,000 tokens = 80,000 tokens
|
||||
Bash outputs: 20 × 2,000 tokens = 40,000 tokens
|
||||
Searches: 15 × 4,000 tokens = 60,000 tokens
|
||||
Other tools: 5 × 1,000 tokens = 5,000 tokens
|
||||
──────────────────────────────────────────────────
|
||||
Total: 185,000 tokens
|
||||
Context remaining: 15,000 tokens (92% full)
|
||||
```
|
||||
|
||||
**After (Endless Mode ON):**
|
||||
```
|
||||
File reads: 10 × 300 tokens = 3,000 tokens
|
||||
Bash outputs: 20 × 250 tokens = 5,000 tokens
|
||||
Searches: 15 × 400 tokens = 6,000 tokens
|
||||
Other tools: 5 × 200 tokens = 1,000 tokens
|
||||
──────────────────────────────────────────────────
|
||||
Total: 15,000 tokens
|
||||
Context remaining: 185,000 tokens (7.5% full)
|
||||
|
||||
Savings: 170,000 tokens (92% reduction)
|
||||
```
|
||||
|
||||
**Session Longevity:**
|
||||
- Before: ~50 tool uses before context full
|
||||
- After: ~600+ tool uses before context full
|
||||
- **12x longer sessions**
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate Actions (This Week)
|
||||
|
||||
1. **Database Migration**: Add tool_use_id column to observations table
|
||||
2. **Hook Update**: Modify PostToolUse hook to capture tool_use_id from Agent SDK
|
||||
3. **Architecture Validation**: Confirm where Agent SDK session resume happens in worker service
|
||||
4. **Prototype**: Build minimal TransformLayer class with observation lookup
|
||||
|
||||
### Short Term (Next 2 Weeks)
|
||||
|
||||
1. Implement complete transform logic
|
||||
2. Wire into worker service resume flow
|
||||
3. Add endless mode feature flag
|
||||
4. Test with real sessions
|
||||
|
||||
### Medium Term (Next Month)
|
||||
|
||||
1. Beta release to power users
|
||||
2. Gather feedback and iterate
|
||||
3. Performance optimization
|
||||
4. Documentation and user guides
|
||||
|
||||
### Long Term (Future)
|
||||
|
||||
1. Make endless mode default
|
||||
2. Hybrid sliding window (keep recent tools uncompressed)
|
||||
3. Selective compression by tool type
|
||||
4. Auto-tune compression based on context usage patterns
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Tool Use ID Format**: What does the Agent SDK's tool_use_id look like? Is it UUID, hash, or sequential?
|
||||
2. **Transcript Format**: What's the exact JSONL schema for tool_use messages? Where is the content we'll replace?
|
||||
3. **Resume Hook Point**: Where exactly in the worker service does session resume happen? Is there a clear integration point?
|
||||
4. **Observation Delay**: How long between PostToolUse firing and observation being available in SQLite? Does this affect resume?
|
||||
5. **Feature Flag Storage**: Environment variable, or persist user preference in database?
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Endless Mode transforms claude-mem from a "memory between sessions" system into a "continuous compression engine" that enables truly infinite sessions. By leveraging the observations we're already creating in real-time and applying them as an ephemeral transformation layer during resume, we can extend session longevity by 10-12x without any risk to user data.
|
||||
|
||||
The key architectural insight is **immutability**: by never modifying source transcripts and performing all compression in memory, we get the benefits of context window optimization without the risks of data corruption or loss. Combined with the optional nature of the feature, this provides a safe, reversible path to fundamentally better session continuity.
|
||||
|
||||
This is the natural evolution of claude-mem: from remembering what happened before, to making it possible to never stop.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Rich Context Examples
|
||||
|
||||
This document shows what contextual data is available in transcripts
|
||||
that could improve observation generation quality.
|
||||
|
||||
## Statistics
|
||||
|
||||
- Total entries: 369
|
||||
- User messages: 74
|
||||
- Assistant messages: 133
|
||||
- Token usage: 67,465 total
|
||||
- Cache efficiency: 6,979,410 tokens read from cache
|
||||
|
||||
## Conversation Flow
|
||||
|
||||
This shows how user requests, assistant reasoning, and tool executions flow together.
|
||||
This is the rich context currently missing from individual tool observations.
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
#### 👤 User Request
|
||||
```
|
||||
Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
|
||||
```
|
||||
|
||||
#### 🔧 Tools Executed (1)
|
||||
|
||||
**TodoWrite**
|
||||
```json
|
||||
{
|
||||
"todos": [
|
||||
{
|
||||
"content": "Read the ROI implementation plan to understand full scope",
|
||||
"status": "in_progress",
|
||||
"activeForm": "Reading ROI implementation plan"
|
||||
},
|
||||
{
|
||||
|
||||
```
|
||||
|
||||
**📊 Data Available for This Exchange:**
|
||||
- User intent: ✅ (218 chars)
|
||||
- Assistant reasoning: ✅ (0 chars)
|
||||
- Thinking process: ❌
|
||||
- Tool executions: ✅ (1 tools)
|
||||
- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
|
||||
|
||||
---
|
||||
|
||||
### Example 2
|
||||
|
||||
#### 👤 User Request
|
||||
```
|
||||
Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
|
||||
```
|
||||
|
||||
#### 🔧 Tools Executed (1)
|
||||
|
||||
**Glob**
|
||||
- Pattern: `**/*roi*`
|
||||
|
||||
**📊 Data Available for This Exchange:**
|
||||
- User intent: ✅ (218 chars)
|
||||
- Assistant reasoning: ✅ (0 chars)
|
||||
- Thinking process: ❌
|
||||
- Tool executions: ✅ (1 tools)
|
||||
- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
|
||||
|
||||
---
|
||||
|
||||
### Example 3
|
||||
|
||||
#### 👤 User Request
|
||||
```
|
||||
Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
|
||||
```
|
||||
|
||||
#### 🔧 Tools Executed (1)
|
||||
|
||||
**Glob**
|
||||
- Pattern: `**/*implementation*plan*`
|
||||
|
||||
**📊 Data Available for This Exchange:**
|
||||
- User intent: ✅ (218 chars)
|
||||
- Assistant reasoning: ✅ (0 chars)
|
||||
- Thinking process: ❌
|
||||
- Tool executions: ✅ (1 tools)
|
||||
- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
|
||||
|
||||
---
|
||||
|
||||
### Example 4
|
||||
|
||||
#### 👤 User Request
|
||||
```
|
||||
Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
|
||||
```
|
||||
|
||||
#### 🔧 Tools Executed (1)
|
||||
|
||||
**Read**
|
||||
- Reading: `/Users/alexnewman/Scripts/claude-mem/docs/context/transcript-data-discovery.md`
|
||||
|
||||
**📊 Data Available for This Exchange:**
|
||||
- User intent: ✅ (218 chars)
|
||||
- Assistant reasoning: ✅ (0 chars)
|
||||
- Thinking process: ❌
|
||||
- Tool executions: ✅ (1 tools)
|
||||
- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
|
||||
|
||||
---
|
||||
|
||||
### Example 5
|
||||
|
||||
#### 👤 User Request
|
||||
```
|
||||
Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
|
||||
```
|
||||
|
||||
#### 🔧 Tools Executed (1)
|
||||
|
||||
**Read**
|
||||
- Reading: `/Users/alexnewman/Scripts/claude-mem/IMPLEMENTATION_PLAN_ROI_METRICS.md`
|
||||
|
||||
**📊 Data Available for This Exchange:**
|
||||
- User intent: ✅ (218 chars)
|
||||
- Assistant reasoning: ✅ (0 chars)
|
||||
- Thinking process: ❌
|
||||
- Tool executions: ✅ (1 tools)
|
||||
- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Key Insight
|
||||
|
||||
Currently, the memory worker receives **isolated tool executions** via save-hook:
|
||||
- tool_name: "Read"
|
||||
- tool_input: {"file_path": "src/foo.ts"}
|
||||
- tool_output: {file contents}
|
||||
|
||||
But the transcript contains **rich contextual data**:
|
||||
- WHY the tool was used (user's request)
|
||||
- WHAT the assistant planned to accomplish
|
||||
- HOW it fits into the broader task
|
||||
- The assistant's reasoning/thinking
|
||||
- Multiple related tools used together
|
||||
|
||||
This context would help the memory worker:
|
||||
1. Understand if a tool use is meaningful or routine
|
||||
2. Generate observations that capture WHY, not just WHAT
|
||||
3. Group related tools into coherent actions
|
||||
4. Avoid "investigating" - the context is already present
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
# Transcript: 57dcc12f-4751-46bb-82b4-2aa96a3e226d.jsonl
|
||||
|
||||
**Generated:** 11/13/2025, 3:58:53 PM
|
||||
**Total Entries:** 448
|
||||
**Entry Types:** {
|
||||
"summary": 149,
|
||||
"file-history-snapshot": 20,
|
||||
"user": 98,
|
||||
"assistant": 181
|
||||
}
|
||||
**Showing:** First 10 conversation turns
|
||||
|
||||
---
|
||||
|
||||
## Turn 1
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:09:47.231Z
|
||||
**UUID:** 5c4b246c-2100-4e6b-a275-f0bb62bba020
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
I have a question regarding the context timeline that we made. and it's a bit of a thinker...
|
||||
|
||||
when i first started out working on claude-mem my idea was using a fast index of memories with links to access them on demand. I thought that claude-mem would be searching for all the things it needed to get the job done, but what I found is that the description of the work done + the files touched actually improved performance tremendously, and without actively searching for things constantly.
|
||||
|
||||
I just had a thought that maybe claude isn't searching, because when it reads what it did, it already "knows" how it did that, for the most part. Would you say I'm on the right track? I want you to ultrathink about WHY claude-mem's session start context works so well, from your OWN perspective
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:10:28.576Z
|
||||
**UUID:** 1dd9591b-c98b-4031-ac0a-2f7e067b427a
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> This is a fascinating question about the fundamental mechanics of how LLMs process context and why claude-mem's approach is working so well. The user is asking me to think deeply about WHY the session start context (descriptions + file paths) works better than active searching.
|
||||
>
|
||||
> Let me think about this from my own perspective as Claude...
|
||||
>
|
||||
> The user's hypothesis is: "Claude isn't searching because when it reads what it did, it already 'knows' how it did that."
|
||||
>
|
||||
> This is a really interesting observation. Let me think about what happens when I see context at session start vs when I actively search:
|
||||
>
|
||||
> 1. **Session Start Context**: I see summaries like "Fixed authentication bug in src/auth/login.ts by updating token validation logic" with file paths
|
||||
> 2. **Active Search**: I use grep/search tools to find specific code or patterns
|
||||
>
|
||||
> The key insight the user is pointing to: When I see the session summary, it's triggering my training knowledge about how these kinds of problems are typically solved. The description + file path combo is enough to reconstruct the likely solution pattern.
|
||||
>
|
||||
> Let me ultrathink about this...
|
||||
>
|
||||
> **Information Density & Compression**:
|
||||
> - A summary like "Added JWT token refresh logic to src/auth/token-manager.ts" is highly compressed but information-rich
|
||||
> - It contains: the pattern (JWT refresh), the location (token-manager), and the action type (added)
|
||||
> - This maps to my training data about how JWT refresh is typically implemented
|
||||
> - The file path gives architectural context about where this lives in the codebase
|
||||
>
|
||||
> **Pattern Recognition vs Literal Recall**:
|
||||
> - I don't need to see the exact code to "know" what was probably done
|
||||
> - JWT token refresh follows common patterns that I've seen thousands of times in training
|
||||
> - The summary activates these patterns in my understanding
|
||||
> - It's like how a human developer reading "fixed the N+1 query problem" immediately knows the general shape of the solution without seeing the code
|
||||
>
|
||||
> **Contextual Scaffolding**:
|
||||
> - File paths provide architectural scaffolding: "src/hooks/context-hook.ts" tells me this is a hook in a hooks directory
|
||||
>
|
||||
>
|
||||
> - The summary provides semantic scaffolding: what was accomplished
|
||||
> - Together they form a mental model of the codebase without needing to read every line
|
||||
>
|
||||
> **Search is for Unknowns, Summaries are for Knowns**:
|
||||
> - I search when I don't know something: "Where is error handling done?"
|
||||
> - I don't need to search when the summary tells me: "Enhanced error handling in src/services/worker-service.ts"
|
||||
> - The summary has already done the work of locating and describing the relevant information
|
||||
>
|
||||
> **The Compression-Decompression Cycle**:
|
||||
> - The AI compression (creating observations) is like creating a highly optimized index
|
||||
> - When I encounter a summary, I'm essentially decompressing that index into actionable knowledge
|
||||
> - This process is far more efficient than searching through raw code or logs
|
||||
>
|
||||
> The summaries act like cognitive shortcuts, instantly reconstructing complex contextual understanding through minimal, strategic information.
|
||||
>
|
||||
> **Why This Works Better Than Constant Searching**:
|
||||
> 1. Drastically reduces mental overhead of navigating complex systems
|
||||
> 2. Enables rapid pattern recognition across different code segments
|
||||
> 3. Provides architectural insights without deep diving into implementation details
|
||||
> 4. Minimizes computational and cognitive resources spent on exploration
|
||||
> 5. Delivers pre-filtered, contextually relevant information
|
||||
>
|
||||
> **The "Already Knows" Hypothesis**:
|
||||
> I recognize complex technical changes instantly through pattern matching. When encountering a concise code modification description, my training allows immediate comprehension by activating relevant technical knowledge domains. The summary triggers an intuitive understanding of systemic implications, similar to how experienced developers quickly grasp architectural shifts through minimal context.
|
||||
>
|
||||
> **The Real Magic**:
|
||||
> Leveraging deep pattern recognition capabilities, I can reconstruct technical landscapes from minimal descriptive signals. File paths and succinct change descriptions become powerful activation keys that unlock comprehensive understanding without exhaustive code examination.
|
||||
>
|
||||
> **Why Links-On-Demand Falls Short**:
|
||||
> Retrieving information through multiple steps disrupts cognitive flow. Contextual understanding emerges more naturally through continuous narrative, where each detail builds upon previous insights. Searching and parsing interrupt the organic comprehension process, making knowledge integration less fluid and intuitive.
|
||||
>
|
||||
> **The Timeline Innovation**:
|
||||
> Project evolution unfolds like a narrative, with each session representing a chapter in technological development. Temporal relationships between changes reveal deeper systemic transformations, mirroring how humans conceptualize complex system growth through interconnected stories of incremental progress.
|
||||
>
|
||||
> The essence lies in providing compressed, meaningful representations that leverage my inherent training knowledge. By presenting contextual frameworks that enable rapid pattern recognition, understanding emerges more elegantly than through fragmented, manually retrieved information.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 10
|
||||
- Output: 1495
|
||||
- Cache creation: 28036
|
||||
|
||||
---
|
||||
|
||||
## Turn 2
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:10:31.963Z
|
||||
**UUID:** 364676a7-51c3-4036-afc3-7ff8f7301a8f
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01T477WUra1sDR6gHaqZHhKT`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 1,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:10:35.939Z
|
||||
**UUID:** 201d5077-aae8-4e26-9c5e-6a63f1ca88fa
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Good, I'm using the sequential thinking tool to really dig into this. Let me continue with the next thought about information density.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 12
|
||||
- Output: 256
|
||||
- Cache creation: 1529
|
||||
- Cache read: 28036
|
||||
|
||||
---
|
||||
|
||||
## Turn 3
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:10:38.354Z
|
||||
**UUID:** 6c2f5da2-6a5a-4742-a46d-6dd5d193ad39
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01QMMWEJm8yH25VtBmAzYbmy`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 2,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:10:41.841Z
|
||||
**UUID:** 3e6f3e78-5c12-4739-a7aa-a8c6fc9d2a33
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Good, now let me think about the difference between pattern recognition and literal recall.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 11
|
||||
- Output: 262
|
||||
- Cache creation: 323
|
||||
- Cache read: 29565
|
||||
|
||||
---
|
||||
|
||||
## Turn 4
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:10:45.430Z
|
||||
**UUID:** f99751a0-b13a-446a-9b3b-dfc08ffb8e5b
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01JwNsDV2HcPnDE2eFJdG7zF`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 3,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:10:48.954Z
|
||||
**UUID:** 11c28930-0797-4f9e-ada8-5eaa27c70b47
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Now let me think about the architectural scaffolding aspect.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 10
|
||||
- Output: 260
|
||||
- Cache creation: 329
|
||||
- Cache read: 29888
|
||||
|
||||
---
|
||||
|
||||
## Turn 5
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:10:53.302Z
|
||||
**UUID:** 6f013ecd-a0f0-4bb9-8eca-e6e8d3f80a53
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_016nFXyGw4SsdKfhUb9eBtkv`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 4,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 4
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:10:56.583Z
|
||||
**UUID:** c6d275b3-7f28-4bdd-8a81-80f1a73ffbc7
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Now let me think about why this works better than active searching.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 9
|
||||
- Output: 258
|
||||
- Cache creation: 327
|
||||
- Cache read: 30217
|
||||
|
||||
---
|
||||
|
||||
## Turn 6
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:11:00.306Z
|
||||
**UUID:** 885fbc9f-e767-4a16-a8aa-b8bb54849d67
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_013QydhPuiBKVRHh1vbD8eL8`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 5,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:11:04.430Z
|
||||
**UUID:** 780354e4-abbf-4d97-b1e2-4f3abc5aef8f
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Now let me think about the compression-decompression cycle - this is a key insight.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 8
|
||||
- Output: 264
|
||||
- Cache creation: 325
|
||||
- Cache read: 30544
|
||||
|
||||
---
|
||||
|
||||
## Turn 7
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:11:07.748Z
|
||||
**UUID:** 462efec2-17c3-47f7-9171-622e00f6bbf7
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01Wa5zzkzrYqf5Cfb5UZi5H6`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 6,
|
||||
"totalThoughts": 8,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 6
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:11:12.931Z
|
||||
**UUID:** 325a8179-8291-4198-a661-eae8f02d57b2
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> The user wants me to continue with the sequential thinking about why claude-mem's session start context works so well. I should continue the thinking process and not mention the todo reminder since this is a theoretical discussion, not a task execution.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 10
|
||||
- Output: 328
|
||||
- Cache creation: 1883
|
||||
- Cache read: 28036
|
||||
|
||||
---
|
||||
|
||||
## Turn 8
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:11:17.712Z
|
||||
**UUID:** 189d903f-a901-465f-bd6b-8addf44cd5f5
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_015VKanT9FxnCSsZiTduHd2y`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 7,
|
||||
"totalThoughts": 9,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 7
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:11:21.820Z
|
||||
**UUID:** 96ca3f03-f889-454d-a13a-96e7a36a3daa
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> I want to add one more thought about the temporal/chronological aspect and how that creates a narrative structure that aids understanding.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 12
|
||||
- Output: 290
|
||||
- Cache creation: 392
|
||||
- Cache read: 29919
|
||||
|
||||
---
|
||||
|
||||
## Turn 9
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:11:26.591Z
|
||||
**UUID:** cfd56a68-8628-4811-9df5-75a8e557bec6
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01EWtaz38nageXXEjYzzm2hu`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 8,
|
||||
"totalThoughts": 9,
|
||||
"nextThoughtNeeded": true,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 8
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:11:30.749Z
|
||||
**UUID:** bb106bb5-9696-4538-b810-d9fa56bcffff
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** tool_use
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> Now for the synthesis - bringing all these threads together to answer the user's question about WHY it works so well from my own perspective.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 11
|
||||
- Output: 360
|
||||
- Cache creation: 357
|
||||
- Cache read: 30311
|
||||
|
||||
---
|
||||
|
||||
## Turn 10
|
||||
|
||||
### 👤 User
|
||||
**Timestamp:** 2025-11-13T17:11:36.692Z
|
||||
**UUID:** 0aa4f55d-9f5a-485f-9aef-dd90f9e80391
|
||||
**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
|
||||
**CWD:** /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
**Tool Results Submitted (1):**
|
||||
|
||||
- **Tool Use ID:** `toolu_01R5Sy5zet7JCPmXhvDrkMAR`
|
||||
```json
|
||||
{
|
||||
"thoughtNumber": 9,
|
||||
"totalThoughts": 9,
|
||||
"nextThoughtNeeded": false,
|
||||
"branches": [],
|
||||
"thoughtHistoryLength": 9
|
||||
}
|
||||
```
|
||||
|
||||
### 🤖 Assistant
|
||||
**Timestamp:** 2025-11-13T17:11:43.435Z
|
||||
**UUID:** 2a5c5c37-9124-441f-ac1f-3f9bff08281c
|
||||
**Model:** claude-sonnet-4-5-20250929
|
||||
**Stop Reason:** end_turn
|
||||
|
||||
**💭 Thinking:**
|
||||
|
||||
> This is a fascinating philosophical question about how I process information. The user is asking me to think deeply about WHY claude-mem's session start context works so well from MY perspective as an LLM. They've noticed something important: I don't seem to need to actively search when I have good summaries, and they're hypothesizing it's because the summaries trigger pattern recognition rather than requiring literal recall.
|
||||
>
|
||||
> This is not a task-oriented request - it's a meta-cognitive question about how I think. I should not use TodoWrite here as this is a philosophical discussion, not a task to track.
|
||||
>
|
||||
> The sequential thinking tool has helped me organize my thoughts. Let me now synthesize this into a clear response about my actual experience.
|
||||
|
||||
**📊 Token Usage:**
|
||||
- Input: 10
|
||||
- Output: 936
|
||||
- Cache creation: 1140
|
||||
- Cache read: 29919
|
||||
|
||||
---
|
||||
|
||||
|
||||
*... 87 more turns not shown*
|
||||
@@ -0,0 +1,632 @@
|
||||
# Transcript Data Analysis: Available Context for Memory Worker
|
||||
|
||||
**Generated:** 2025-11-13
|
||||
**Purpose:** Document what contextual data exists in Claude Code transcripts and identify opportunities to improve memory worker observation generation.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Current State:** The memory worker receives isolated tool executions via `save-hook.ts`:
|
||||
- Tool name
|
||||
- Tool input (parameters)
|
||||
- Tool output (results)
|
||||
|
||||
**Available in Transcripts:** Rich contextual data that could dramatically improve observation quality:
|
||||
- User's original request/intent
|
||||
- Assistant's reasoning (thinking blocks)
|
||||
- Full conversation context
|
||||
- Tool result data
|
||||
- Token usage and performance metrics
|
||||
- Session metadata (timestamps, UUIDs, CWD)
|
||||
|
||||
**Recommendation:** Enhance the memory worker to receive full conversation context for each tool execution, not just isolated tool data.
|
||||
|
||||
---
|
||||
|
||||
## Transcript Structure
|
||||
|
||||
### Entry Types
|
||||
|
||||
The transcript file (`~/.claude/projects/-{project}/session-id.jsonl`) contains:
|
||||
|
||||
```
|
||||
- summary entries (149 in sample)
|
||||
- file-history-snapshot entries (18 in sample)
|
||||
- user entries (86 in sample)
|
||||
- assistant entries (155 in sample)
|
||||
```
|
||||
|
||||
### Conversation Turn Pattern
|
||||
|
||||
Each conversation turn consists of:
|
||||
1. **User Entry** - User's request
|
||||
2. **Assistant Entry** - Assistant's response
|
||||
3. **User Entry** - Tool results submitted back (automatic)
|
||||
4. **Assistant Entry** - Assistant processes results and continues
|
||||
|
||||
This creates a pattern: User → Assistant → User (tool results) → Assistant (continues) → ...
|
||||
|
||||
---
|
||||
|
||||
## Available Data by Entry Type
|
||||
|
||||
### 1. User Entries
|
||||
|
||||
**Current Save-Hook Access:**
|
||||
- Tool name
|
||||
- Tool input
|
||||
- Tool output
|
||||
|
||||
**Additional Data Available in User Entries:**
|
||||
|
||||
```typescript
|
||||
interface UserTranscriptEntry {
|
||||
type: 'user';
|
||||
timestamp: string; // ISO timestamp
|
||||
uuid: string; // Unique entry ID
|
||||
sessionId: string; // Session identifier
|
||||
cwd: string; // Working directory
|
||||
parentUuid?: string; // Parent entry reference
|
||||
isSidechain: boolean; // Is this a side conversation?
|
||||
userType: string; // 'human' or 'system'
|
||||
version: string; // Claude Code version
|
||||
|
||||
message: {
|
||||
role: 'user';
|
||||
content: string | ContentItem[]; // Can be text or structured
|
||||
};
|
||||
|
||||
toolUseResult?: ToolUseResult; // Legacy field, may contain results
|
||||
}
|
||||
```
|
||||
|
||||
**When `content` is an array, it contains:**
|
||||
- Text blocks with user's actual request
|
||||
- Tool result blocks with complete output data
|
||||
|
||||
**Example Structure:**
|
||||
```json
|
||||
{
|
||||
"type": "user",
|
||||
"timestamp": "2025-11-13T17:10:31.963Z",
|
||||
"uuid": "364676a7-51c3-4036-afc3-7ff8f7301a8f",
|
||||
"sessionId": "57dcc12f-4751-46bb-82b4-2aa96a3e226d",
|
||||
"cwd": "/Users/alexnewman/Scripts/claude-mem",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01T477WUra1sDR6gHaqZHhKT",
|
||||
"content": "[actual tool output data]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Assistant Entries
|
||||
|
||||
**Current Save-Hook Access:**
|
||||
- Nothing from assistant entries (they happen after tool execution)
|
||||
|
||||
**Available Data in Assistant Entries:**
|
||||
|
||||
```typescript
|
||||
interface AssistantTranscriptEntry {
|
||||
type: 'assistant';
|
||||
timestamp: string;
|
||||
uuid: string;
|
||||
sessionId: string;
|
||||
cwd: string;
|
||||
parentUuid?: string;
|
||||
isSidechain: boolean;
|
||||
userType: string;
|
||||
version: string;
|
||||
requestId?: string; // API request ID
|
||||
|
||||
message: {
|
||||
id: string;
|
||||
type: 'message';
|
||||
role: 'assistant';
|
||||
model: string; // e.g., "claude-sonnet-4-5-20250929"
|
||||
content: ContentItem[]; // Array of content blocks
|
||||
stop_reason?: string; // 'tool_use' | 'end_turn' | etc.
|
||||
stop_sequence?: string;
|
||||
usage?: UsageInfo; // Token usage stats
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Content Block Types in `message.content`:**
|
||||
|
||||
1. **Thinking Blocks** - Internal reasoning before acting
|
||||
```typescript
|
||||
{
|
||||
type: 'thinking';
|
||||
thinking: string; // Full reasoning text
|
||||
signature?: string;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Text Blocks** - Assistant's visible response
|
||||
```typescript
|
||||
{
|
||||
type: 'text';
|
||||
text: string; // Response text
|
||||
}
|
||||
```
|
||||
|
||||
3. **Tool Use Blocks** - Tool invocations
|
||||
```typescript
|
||||
{
|
||||
type: 'tool_use';
|
||||
id: string; // Tool use ID
|
||||
name: string; // Tool name (e.g., 'Read', 'Edit')
|
||||
input: Record<string, any>; // Complete tool parameters
|
||||
}
|
||||
```
|
||||
|
||||
**Token Usage Data:**
|
||||
```typescript
|
||||
interface UsageInfo {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
service_tier?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Summary Entries
|
||||
|
||||
```typescript
|
||||
interface SummaryTranscriptEntry {
|
||||
type: 'summary';
|
||||
summary: string; // Generated summary text
|
||||
leafUuid: string; // UUID of summarized entry
|
||||
cwd?: string;
|
||||
}
|
||||
```
|
||||
|
||||
These appear frequently (149 in sample) and provide high-level summaries of work done.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow: Current vs Potential
|
||||
|
||||
### Current Flow (Save-Hook Only)
|
||||
|
||||
```
|
||||
User: "Fix the bug in login.ts"
|
||||
↓
|
||||
Assistant: [uses Edit tool]
|
||||
↓
|
||||
Tool Execution: Edit(file_path: "login.ts", old_string: "...", new_string: "...")
|
||||
↓
|
||||
Save-Hook receives:
|
||||
- toolName: "Edit"
|
||||
- toolInput: { file_path: "login.ts", old_string: "...", new_string: "..." }
|
||||
- toolOutput: { success: true }
|
||||
↓
|
||||
Memory Worker generates observation from ONLY tool data
|
||||
- No user intent
|
||||
- No assistant reasoning
|
||||
- No context about WHY this change was made
|
||||
```
|
||||
|
||||
### Enhanced Flow (With Transcript Context)
|
||||
|
||||
```
|
||||
User: "Fix the authentication bug - users getting logged out randomly"
|
||||
↓
|
||||
Assistant (thinking): "This sounds like a token expiration issue.
|
||||
Let me check the JWT handling in login.ts..."
|
||||
↓
|
||||
Assistant (uses Edit tool)
|
||||
↓
|
||||
Save-Hook receives:
|
||||
- toolName: "Edit"
|
||||
- toolInput: { file_path: "login.ts", ... }
|
||||
- toolOutput: { success: true }
|
||||
- PLUS:
|
||||
- userRequest: "Fix the authentication bug - users getting logged out randomly"
|
||||
- assistantReasoning: "This sounds like a token expiration issue..."
|
||||
- conversationContext: Previous 2-3 turns
|
||||
- sessionMetadata: { cwd, timestamp, sessionId }
|
||||
↓
|
||||
Memory Worker generates richer observation:
|
||||
- "Fixed authentication bug causing random logouts"
|
||||
- "Problem: JWT tokens expiring too quickly"
|
||||
- "Solution: Updated token expiration to 24h in login.ts"
|
||||
- "Files: src/auth/login.ts"
|
||||
- "Concepts: authentication, token-management, bugfix"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Specific Opportunities
|
||||
|
||||
### 1. User Intent Extraction
|
||||
|
||||
**Problem:** Current observations lack user intent.
|
||||
|
||||
**Solution:** Parse the most recent user text entry before the tool execution.
|
||||
|
||||
**Implementation:**
|
||||
- Walk backward from tool execution entry
|
||||
- Find first user entry with text content
|
||||
- Extract text blocks (filter out tool_result blocks)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// In save-hook.ts
|
||||
const userEntries = parser.getUserEntries();
|
||||
const recentUserMessage = findUserMessageBeforeTool(userEntries, toolExecutionTimestamp);
|
||||
const userIntent = extractTextFromContent(recentUserMessage.content);
|
||||
```
|
||||
|
||||
### 2. Assistant Reasoning
|
||||
|
||||
**Problem:** We don't capture WHY the assistant chose to use a tool.
|
||||
|
||||
**Solution:** Extract thinking blocks from assistant entry immediately before tool use.
|
||||
|
||||
**Implementation:**
|
||||
- Find assistant entry that contains the tool_use block
|
||||
- Extract thinking blocks from same entry
|
||||
- Include first ~500 chars of thinking in observation context
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const assistantEntry = findAssistantEntryWithToolUse(toolUseId);
|
||||
const thinkingBlocks = assistantEntry.message.content.filter(c => c.type === 'thinking');
|
||||
const reasoning = thinkingBlocks.map(b => b.thinking).join('\n');
|
||||
```
|
||||
|
||||
### 3. Tool Results Context
|
||||
|
||||
**Problem:** Tool output alone doesn't show what was found or changed.
|
||||
|
||||
**Solution:** Access full tool result content from next user entry.
|
||||
|
||||
**Implementation:**
|
||||
- Tool execution happens in assistant entry
|
||||
- Results come back in next user entry as tool_result content
|
||||
- Save-hook can access both
|
||||
|
||||
**Current Structure:**
|
||||
```
|
||||
Assistant Entry:
|
||||
{ type: 'tool_use', id: 'toolu_123', name: 'Read', input: {...} }
|
||||
↓
|
||||
User Entry (automatic):
|
||||
{ type: 'tool_result', tool_use_id: 'toolu_123', content: "file contents..." }
|
||||
```
|
||||
|
||||
**Opportunity:** Match tool_use_id to tool_result and include full result content.
|
||||
|
||||
### 4. Conversation Context
|
||||
|
||||
**Problem:** Isolated tool executions miss the larger conversation flow.
|
||||
|
||||
**Solution:** Include last N conversation turns (2-3 turns is usually sufficient).
|
||||
|
||||
**Implementation:**
|
||||
- Get entries from transcript within time window (e.g., last 5 minutes)
|
||||
- Include user messages and assistant text responses
|
||||
- Exclude thinking blocks to save tokens
|
||||
|
||||
**Example Context:**
|
||||
```
|
||||
Turn 1:
|
||||
User: "I need to add dark mode support"
|
||||
Assistant: "I'll help you add dark mode. Let me start by..."
|
||||
|
||||
Turn 2:
|
||||
User: [tool results]
|
||||
Assistant: "Now I'll update the theme configuration..."
|
||||
|
||||
Turn 3: [current tool execution]
|
||||
```
|
||||
|
||||
### 5. Session Metadata
|
||||
|
||||
**Problem:** Observations lack temporal and project context.
|
||||
|
||||
**Solution:** Include session metadata in observation generation.
|
||||
|
||||
**Available Fields:**
|
||||
- `cwd` - Working directory (project path)
|
||||
- `timestamp` - Exact time of execution
|
||||
- `sessionId` - Session identifier
|
||||
- `uuid` - Entry identifier
|
||||
- `version` - Claude Code version
|
||||
|
||||
**Use Case:** Helps with project-specific context and temporal queries.
|
||||
|
||||
### 6. Token Usage Metrics
|
||||
|
||||
**Problem:** No visibility into performance and cost.
|
||||
|
||||
**Solution:** Track token usage per observation.
|
||||
|
||||
**Available Data:**
|
||||
- Input tokens
|
||||
- Output tokens
|
||||
- Cache creation tokens
|
||||
- Cache read tokens
|
||||
|
||||
**Use Case:**
|
||||
- Performance monitoring
|
||||
- Cost attribution
|
||||
- Cache effectiveness analysis
|
||||
|
||||
---
|
||||
|
||||
## Recommended Implementation Strategy
|
||||
|
||||
### Phase 1: User Intent (High Impact, Low Effort)
|
||||
|
||||
**Change:** Modify save-hook to extract user's most recent message.
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// In save-hook.ts
|
||||
import { TranscriptParser } from '../utils/transcript-parser';
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const userIntent = parser.getLastUserMessage();
|
||||
|
||||
// Send to worker
|
||||
await workerService.saveToolExecution({
|
||||
...existingData,
|
||||
userIntent, // NEW
|
||||
});
|
||||
```
|
||||
|
||||
**Impact:** Observations now include "what the user wanted to do".
|
||||
|
||||
### Phase 2: Assistant Reasoning (High Impact, Medium Effort)
|
||||
|
||||
**Change:** Extract thinking blocks from assistant entry containing tool use.
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
const assistantEntries = parser.getAssistantEntries();
|
||||
const toolUseEntry = findEntryWithToolUse(assistantEntries, toolUseId);
|
||||
const thinking = extractThinkingBlocks(toolUseEntry);
|
||||
|
||||
await workerService.saveToolExecution({
|
||||
...existingData,
|
||||
userIntent,
|
||||
assistantReasoning: thinking, // NEW
|
||||
});
|
||||
```
|
||||
|
||||
**Impact:** Observations include "why the assistant chose this approach".
|
||||
|
||||
### Phase 3: Conversation Context (Medium Impact, High Effort)
|
||||
|
||||
**Change:** Include last 2-3 conversation turns.
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
const recentTurns = getRecentConversationTurns(parser, 3);
|
||||
|
||||
await workerService.saveToolExecution({
|
||||
...existingData,
|
||||
userIntent,
|
||||
assistantReasoning: thinking,
|
||||
conversationContext: recentTurns, // NEW
|
||||
});
|
||||
```
|
||||
|
||||
**Impact:** Observations understand multi-turn workflows.
|
||||
|
||||
### Phase 4: Enhanced Metadata (Low Impact, Low Effort)
|
||||
|
||||
**Change:** Include session and performance metadata.
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
await workerService.saveToolExecution({
|
||||
...existingData,
|
||||
userIntent,
|
||||
assistantReasoning: thinking,
|
||||
conversationContext: recentTurns,
|
||||
metadata: { // NEW
|
||||
cwd: entry.cwd,
|
||||
timestamp: entry.timestamp,
|
||||
sessionId: entry.sessionId,
|
||||
tokenUsage: entry.message.usage,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Impact:** Better analytics and debugging.
|
||||
|
||||
---
|
||||
|
||||
## Example: Before and After
|
||||
|
||||
### Current Observation (Tool Data Only)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "feature",
|
||||
"title": "Updated login.ts",
|
||||
"narrative": "Modified authentication logic in src/auth/login.ts",
|
||||
"files": ["src/auth/login.ts"],
|
||||
"concepts": ["authentication"],
|
||||
"facts": []
|
||||
}
|
||||
```
|
||||
|
||||
### Enhanced Observation (With Transcript Context)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "bugfix",
|
||||
"title": "Fixed authentication bug causing random logouts",
|
||||
"narrative": "Users were experiencing random logouts due to JWT token expiration. Updated token expiration from 1h to 24h in token validation logic. Modified src/auth/login.ts to use longer-lived tokens and improved error handling for expired tokens.",
|
||||
"files": ["src/auth/login.ts"],
|
||||
"concepts": ["authentication", "jwt", "token-management", "bugfix"],
|
||||
"facts": [
|
||||
"JWT token expiration was too short (1h)",
|
||||
"Updated expiration to 24h",
|
||||
"Added error handling for expired tokens"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Improvement:**
|
||||
- Clear problem statement
|
||||
- Explicit solution
|
||||
- Specific technical details
|
||||
- Better concept tagging
|
||||
- Actionable facts
|
||||
|
||||
---
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### 1. Performance
|
||||
|
||||
**Concern:** Parsing entire transcript on every tool execution.
|
||||
|
||||
**Solution:**
|
||||
- TranscriptParser already loads full file (unavoidable)
|
||||
- Use caching for transcript parsing within same session
|
||||
- Only parse once per session, reuse parsed entries
|
||||
|
||||
**Benchmark:**
|
||||
- Current: ~10ms to parse 408-line transcript
|
||||
- Impact: Negligible (save-hook already reads transcript)
|
||||
|
||||
### 2. Token Usage
|
||||
|
||||
**Concern:** Sending more context to worker increases tokens.
|
||||
|
||||
**Solution:**
|
||||
- Thinking blocks: Limit to first 500 chars
|
||||
- Conversation context: Only last 2-3 turns
|
||||
- Tool results: Truncate large outputs to 500 chars
|
||||
- User intent: Full text (usually short)
|
||||
|
||||
**Estimate:**
|
||||
- Current: ~200 tokens per observation generation
|
||||
- Enhanced: ~500 tokens per observation generation
|
||||
- Increase: ~150%
|
||||
- Cost: Still < $0.001 per observation with Haiku
|
||||
|
||||
### 3. Implementation Complexity
|
||||
|
||||
**Concern:** Matching tool executions to transcript entries.
|
||||
|
||||
**Solution:**
|
||||
- Tool use IDs are in both places
|
||||
- Timestamps provide ordering
|
||||
- UUID chains provide parent-child relationships
|
||||
|
||||
**Example Matching:**
|
||||
```typescript
|
||||
function findToolContext(parser: TranscriptParser, toolUseId: string) {
|
||||
// 1. Find assistant entry with tool_use block
|
||||
const assistantEntry = parser.getAssistantEntries()
|
||||
.find(entry =>
|
||||
entry.message.content.some(c =>
|
||||
c.type === 'tool_use' && c.id === toolUseId
|
||||
)
|
||||
);
|
||||
|
||||
// 2. Find next user entry with tool_result
|
||||
const userEntry = parser.getUserEntries()
|
||||
.find(entry =>
|
||||
entry.message.content.some(c =>
|
||||
c.type === 'tool_result' && c.tool_use_id === toolUseId
|
||||
)
|
||||
);
|
||||
|
||||
return { assistantEntry, userEntry };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Validate Approach**
|
||||
- Review this analysis with project team
|
||||
- Confirm data availability in all transcript scenarios
|
||||
- Identify any privacy concerns
|
||||
|
||||
2. **Implement Phase 1**
|
||||
- Update save-hook.ts to extract user intent
|
||||
- Modify worker service to accept new fields
|
||||
- Update observation prompt to use user intent
|
||||
|
||||
3. **Test and Measure**
|
||||
- Compare observation quality before/after
|
||||
- Measure token usage increase
|
||||
- Validate performance impact
|
||||
|
||||
4. **Iterate**
|
||||
- Roll out Phase 2 (assistant reasoning)
|
||||
- Roll out Phase 3 (conversation context)
|
||||
- Monitor improvements at each phase
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Data Samples
|
||||
|
||||
### Complete Markdown Representation
|
||||
|
||||
See `/Users/alexnewman/Scripts/claude-mem/docs/context/transcript-complete-readable.md` for a full 1:1 markdown representation of the first 10 conversation turns from the sample transcript, including:
|
||||
- Complete user messages
|
||||
- Full assistant responses
|
||||
- Thinking blocks (truncated to 2000 chars)
|
||||
- Tool uses with complete input JSON
|
||||
- Tool results with actual output data (truncated to 500 chars)
|
||||
- Token usage stats
|
||||
- All metadata (timestamps, UUIDs, session IDs, CWD)
|
||||
|
||||
### Sample Tool Result Structure
|
||||
|
||||
```typescript
|
||||
// User entry containing tool result
|
||||
{
|
||||
"type": "user",
|
||||
"message": {
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01T477WUra1sDR6gHaqZHhKT",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "{\n \"thoughtNumber\": 1,\n \"totalThoughts\": 8,\n \"nextThoughtNeeded\": true,\n \"branches\": [],\n \"thoughtHistoryLength\": 1\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Claude Code transcript files contain a wealth of contextual data that is currently unused by the memory worker. By extracting:
|
||||
|
||||
1. User intent (the "what" and "why")
|
||||
2. Assistant reasoning (the "how" and "because")
|
||||
3. Tool results (the "outcome")
|
||||
4. Conversation context (the "flow")
|
||||
5. Session metadata (the "when" and "where")
|
||||
|
||||
We can generate significantly richer, more useful observations that better capture the intent, decisions, and outcomes of each coding session.
|
||||
|
||||
**The data is already there - we just need to read it.**
|
||||
@@ -0,0 +1,234 @@
|
||||
# Claude Code Transcript Data Discovery
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document details findings from implementing a validated transcript parser for Claude Code JSONL transcripts. The parser enables extraction of rich contextual data that can optimize prompt generation and track token usage for ROI metrics.
|
||||
|
||||
## Transcript Structure
|
||||
|
||||
### File Location
|
||||
```
|
||||
~/.claude/projects/<encoded-project-path>/<session-id>.jsonl
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/2933cff9-f0a7-4f0b-8296-0a030e7658a6.jsonl
|
||||
```
|
||||
|
||||
### Entry Types
|
||||
|
||||
Discovered 5 transcript entry types:
|
||||
|
||||
1. **`file-history-snapshot`** (NEW - not in Python model)
|
||||
- Purpose: Track file state snapshots
|
||||
- Frequency: ~10 entries per session
|
||||
|
||||
2. **`user`** - User messages and tool results
|
||||
- Contains actual user text messages OR tool result data
|
||||
- Can have string content or array of ContentItems
|
||||
|
||||
3. **`assistant`** - Assistant responses and tool uses
|
||||
- Contains text responses, tool uses, and thinking blocks
|
||||
- **Critical**: Contains usage data with token counts
|
||||
|
||||
4. **`summary`** (not yet observed in test data)
|
||||
- Session summaries
|
||||
|
||||
5. **`system`** (not yet observed in test data)
|
||||
- System messages/warnings
|
||||
|
||||
6. **`queue-operation`** (not yet observed in test data)
|
||||
- Queue tracking for message flow
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Message Extraction Complexity
|
||||
|
||||
**Problem**: Naively getting the "last" entry doesn't work because:
|
||||
- Last user entry might be a tool result, not a text message
|
||||
- Last assistant entry might only contain tool uses, no text
|
||||
|
||||
**Solution**: Iterate backward through entries to find the last entry with actual text content.
|
||||
|
||||
### 2. Tool Use Tracking
|
||||
|
||||
**Discovery**: Tool uses are in **assistant** messages, not user messages.
|
||||
|
||||
**Data Available**:
|
||||
```typescript
|
||||
{
|
||||
name: string; // Tool name (e.g., "Bash", "Read", "TodoWrite")
|
||||
timestamp: string; // When the tool was used
|
||||
input: any; // Full tool input parameters
|
||||
}
|
||||
```
|
||||
|
||||
**Test Session Results** (168 entries):
|
||||
- 42 tool uses across 7 different tool types
|
||||
- Most used: Bash (24x), TodoWrite (5x), Edit (4x)
|
||||
|
||||
### 3. Token Usage Data (ROI Foundation)
|
||||
|
||||
**Critical Discovery**: Every assistant message contains complete token usage data:
|
||||
|
||||
```typescript
|
||||
interface UsageInfo {
|
||||
input_tokens?: number; // Total input tokens (includes context)
|
||||
cache_creation_input_tokens?: number; // Tokens used to create cache
|
||||
cache_read_input_tokens?: number; // Cached tokens read (discounted cost)
|
||||
output_tokens?: number; // Model output tokens
|
||||
}
|
||||
```
|
||||
|
||||
**Test Session Token Analysis**:
|
||||
```
|
||||
Input tokens: 858
|
||||
Output tokens: 44,165
|
||||
Cache creation tokens: 469,650
|
||||
Cache read tokens: 5,294,101 ← 5.29M tokens saved by caching!
|
||||
Total tokens: 45,023
|
||||
```
|
||||
|
||||
**ROI Implication**: This validates our ROI implementation plan. We can track:
|
||||
- Discovery cost = sum of all input + output tokens across session
|
||||
- Context savings = cache_read_input_tokens (tokens NOT paid for in full)
|
||||
- ROI = Discovery cost / Context savings
|
||||
|
||||
### 4. Parse Reliability
|
||||
|
||||
**Result**: 0.00% parse failure rate on production transcript with 168 entries.
|
||||
|
||||
**Conclusion**: The JSONL format is stable and well-formed. No need for extensive error handling.
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### Created Files
|
||||
|
||||
1. **`src/types/transcript.ts`** - TypeScript types matching Python Pydantic model
|
||||
- All entry types, content types, usage info
|
||||
- Drop-in compatible with Python model structure
|
||||
|
||||
2. **`src/utils/transcript-parser.ts`** - Robust transcript parsing class
|
||||
- Handles all entry types
|
||||
- Smart message extraction (finds last text message, not just last entry)
|
||||
- Tool use history extraction
|
||||
- Token usage aggregation
|
||||
- Parse statistics and error tracking
|
||||
|
||||
3. **`scripts/test-transcript-parser.ts`** - Validation script
|
||||
- Tests all extraction methods
|
||||
- Reports parse statistics
|
||||
- Shows token usage breakdown
|
||||
- Lists tool use history
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
|
||||
const parser = new TranscriptParser('/path/to/transcript.jsonl');
|
||||
|
||||
// Extract messages
|
||||
const lastUserMsg = parser.getLastUserMessage();
|
||||
const lastAssistantMsg = parser.getLastAssistantMessage();
|
||||
|
||||
// Get tool history
|
||||
const tools = parser.getToolUseHistory();
|
||||
// => [{name: 'Bash', timestamp: '...', input: {...}}, ...]
|
||||
|
||||
// Get token usage
|
||||
const tokens = parser.getTotalTokenUsage();
|
||||
// => {inputTokens: 858, outputTokens: 44165, cacheReadTokens: 5294101, ...}
|
||||
|
||||
// Parse statistics
|
||||
const stats = parser.getParseStats();
|
||||
// => {totalLines: 168, parsedEntries: 168, failedLines: 0, ...}
|
||||
```
|
||||
|
||||
## Next Steps for PR Review
|
||||
|
||||
### Addressing "Drops Unknown Lines" Concern
|
||||
|
||||
**Original Issue**: Summary hook silently skipped malformed lines without visibility.
|
||||
|
||||
**Root Cause**: We didn't understand the full transcript model. The "skip malformed lines" was a band-aid.
|
||||
|
||||
**Solution**: Replace ad-hoc parsing in `summary-hook.ts` with validated `TranscriptParser` class:
|
||||
|
||||
**Before** (summary-hook.ts:38-117):
|
||||
```typescript
|
||||
// Manually parsing with try/catch, no type safety
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const line = JSON.parse(lines[i]);
|
||||
if (line.type === 'user' && line.message?.content) {
|
||||
// ... extraction logic
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines ← BLACK HOLE
|
||||
continue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After** (using TranscriptParser):
|
||||
```typescript
|
||||
import { TranscriptParser } from '../utils/transcript-parser.js';
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const lastUserMessage = parser.getLastUserMessage();
|
||||
const lastAssistantMessage = parser.getLastAssistantMessage();
|
||||
|
||||
// Parse errors are tracked in parser.getParseErrors()
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
1. ✅ Type-safe extraction based on validated model
|
||||
2. ✅ No silent failures - parse errors are tracked
|
||||
3. ✅ Smart extraction (finds last TEXT message, not last entry)
|
||||
4. ✅ Reusable across all hooks and scripts
|
||||
5. ✅ Enables token usage tracking (ROI metrics)
|
||||
6. ✅ Enables tool use tracking (prompt optimization)
|
||||
|
||||
## Prompt Optimization Opportunities
|
||||
|
||||
With rich transcript data available, we can enhance prompts with:
|
||||
|
||||
### 1. Tool Use Patterns
|
||||
- "In this session you've used: Bash (24x), TodoWrite (5x), Edit (4x)"
|
||||
- Helps Claude understand what kind of work is being done
|
||||
|
||||
### 2. Token Economics Awareness
|
||||
- "Cache read tokens: 5.29M (context savings)"
|
||||
- Reinforces value of memory system
|
||||
|
||||
### 3. Session Flow Understanding
|
||||
- Number of user/assistant exchanges
|
||||
- Tools used per exchange
|
||||
- Session complexity metrics
|
||||
|
||||
### 4. File History Snapshots
|
||||
- Track which files were modified during session
|
||||
- Provide file change context to summaries
|
||||
|
||||
## Testing
|
||||
|
||||
Run the validation script:
|
||||
```bash
|
||||
# Find your current session transcript
|
||||
ls -lt ~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/*.jsonl | head -1
|
||||
|
||||
# Test the parser
|
||||
npx tsx scripts/test-transcript-parser.ts <path-to-transcript.jsonl>
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
The transcript parser implementation:
|
||||
1. ✅ Addresses PR review concern about dropped lines
|
||||
2. ✅ Validates the ROI metrics implementation plan
|
||||
3. ✅ Enables prompt optimization with rich context
|
||||
4. ✅ Provides foundation for future enhancements
|
||||
|
||||
**Recommendation**: Replace ad-hoc transcript parsing in hooks with `TranscriptParser` class for improved reliability and feature richness.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Transcript Dump
|
||||
|
||||
Total entries: 384
|
||||
|
||||
---
|
||||
|
||||
## Entry 151: USER
|
||||
**Timestamp:** 2025-11-13T17:09:47.231Z
|
||||
|
||||
**Content:**
|
||||
```
|
||||
I have a question regarding the context timeline that we made. and it's a bit of a thinker...
|
||||
|
||||
when i first started out working on claude-mem my idea was using a fast index of memories with links to access them on demand. I thought that claude-mem would be searching for all the things it needed to get the job done, but what I found is that the description of the work done + the files touched actually improved performance tremendously, and without actively searching for things constantly.
|
||||
|
||||
I just had a thought that maybe claude isn't searching, because when it reads what it did, it already "knows" how it did that, for the most part. Would you say I'm on the right track? I want you to ultrathink about WHY claude-mem's session start context works so well, from your OWN perspective
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
_Remaining 364 entries omitted for brevity_
|
||||
@@ -0,0 +1,106 @@
|
||||
# Transcript Context Analysis
|
||||
|
||||
**File:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d.jsonl
|
||||
**Parsed:** 11/13/2025, 12:34:19 PM
|
||||
|
||||
## Statistics
|
||||
|
||||
- Total entries: 281
|
||||
- Successfully parsed: 281
|
||||
- Failed lines: 0
|
||||
- Conversation turns: 43
|
||||
|
||||
## Token Usage
|
||||
|
||||
- Input tokens: 6,805
|
||||
- Output tokens: 38,209
|
||||
- Cache creation: 224,078
|
||||
- Cache read: 3,320,726
|
||||
- Total: 45,014
|
||||
|
||||
---
|
||||
|
||||
# Conversation Turns
|
||||
|
||||
## Turn 1
|
||||
|
||||
---
|
||||
|
||||
## Turn 2
|
||||
|
||||
---
|
||||
|
||||
## Turn 3
|
||||
|
||||
---
|
||||
|
||||
## Turn 4
|
||||
|
||||
---
|
||||
|
||||
## Turn 5
|
||||
|
||||
---
|
||||
|
||||
## Turn 6
|
||||
|
||||
---
|
||||
|
||||
## Turn 7
|
||||
|
||||
---
|
||||
|
||||
## Turn 8
|
||||
|
||||
---
|
||||
|
||||
## Turn 9
|
||||
|
||||
---
|
||||
|
||||
## Turn 10
|
||||
|
||||
---
|
||||
|
||||
## Turn 11
|
||||
|
||||
---
|
||||
|
||||
## Turn 12
|
||||
|
||||
---
|
||||
|
||||
## Turn 13
|
||||
|
||||
---
|
||||
|
||||
## Turn 14
|
||||
|
||||
---
|
||||
|
||||
## Turn 15
|
||||
|
||||
---
|
||||
|
||||
## Turn 16
|
||||
|
||||
---
|
||||
|
||||
## Turn 17
|
||||
|
||||
---
|
||||
|
||||
## Turn 18
|
||||
|
||||
---
|
||||
|
||||
## Turn 19
|
||||
|
||||
---
|
||||
|
||||
## Turn 20
|
||||
|
||||
---
|
||||
|
||||
|
||||
_... 23 more turns omitted for brevity_
|
||||
@@ -0,0 +1,67 @@
|
||||
# VS Code API – Language Model (`lm`) Namespace
|
||||
|
||||
Excerpt captured from `https://code.visualstudio.com/api/references/vscode-api#lm` using markitdown MCP on 2025-11-12.
|
||||
|
||||
## Overview
|
||||
|
||||
The `vscode.lm` namespace exposes APIs for interacting with language models inside Visual Studio Code. It allows extensions to register tools, select chat models, invoke tools, and surface MCP servers so that agent mode can compose complex responses.
|
||||
|
||||
### Available Tools
|
||||
|
||||
- `vscode.lm.tools`: Readonly array of [`LanguageModelToolInformation`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelToolInformation).
|
||||
- Lists all tools registered via `vscode.lm.registerTool`.
|
||||
- Tools can be invoked programmatically with `vscode.lm.invokeTool` when their inputs satisfy the declared schema.
|
||||
|
||||
### Events
|
||||
|
||||
- `vscode.lm.onDidChangeChatModels`: Fires when the set of available chat models changes. Extensions should re-query models after this event.
|
||||
|
||||
### Functions
|
||||
|
||||
#### `vscode.lm.invokeTool(name, options, token?)`
|
||||
|
||||
Invokes a tool by name with a given input payload.
|
||||
|
||||
- Validates input against the schema declared by the tool.
|
||||
- When called from a chat participant, pass the `toolInvocationToken` so the chat UI associates results with the correct conversation.
|
||||
- Returns a [`LanguageModelToolResult`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelToolResult) composed of text and optional prompt-tsx parts.
|
||||
- Tool results can be preserved across turns by storing them in `ChatResult.metadata` and retrieving them later from `ChatResponseTurn.result`.
|
||||
|
||||
#### `vscode.lm.registerLanguageModelChatProvider(vendor, provider)`
|
||||
|
||||
Registers a [`LanguageModelChatProvider`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelChatProvider).
|
||||
|
||||
- Requires a matching `languageModelChatProviders` contribution in `package.json`.
|
||||
- `vendor` must be globally unique (for example `copilot` or `openai`).
|
||||
- Returns a `Disposable` to unregister the provider.
|
||||
|
||||
#### `vscode.lm.registerMcpServerDefinitionProvider(id, provider)`
|
||||
|
||||
Publishes Model Context Protocol servers for the editor.
|
||||
|
||||
- Requires a `contributes.mcpServerDefinitionProviders` entry in `package.json`.
|
||||
- Enables dynamic discovery of MCP servers and tools when users submit chat messages.
|
||||
- Returns a `Disposable` that unregisters the provider.
|
||||
|
||||
#### `vscode.lm.registerTool(name, tool)`
|
||||
|
||||
Registers a [`LanguageModelTool`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelTool) implementation with the runtime.
|
||||
|
||||
- Tool must also appear in `package.json -> contributes.languageModelTools`.
|
||||
- Registered tools appear in `vscode.lm.tools` and can be used by any extension.
|
||||
|
||||
#### `vscode.lm.selectChatModels(selector?)`
|
||||
|
||||
Returns an array of [`LanguageModelChat`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelChat) instances matching a selector.
|
||||
|
||||
- Selector can be broad (by vendor or family) or narrow (by exact model ID).
|
||||
- Handle scenarios where zero models are available.
|
||||
- Persisted model references should be refreshed when `onDidChangeChatModels` fires.
|
||||
|
||||
## Usage Notes
|
||||
|
||||
- Extensions should gracefully handle missing models or tools.
|
||||
- Tool invocation responses can include multiple parts; integrate them using prompt-tsx or by constructing `LanguageModelToolResultPart` objects.
|
||||
- When providing MCP servers or tools, ensure proper contribution points exist in `package.json`.
|
||||
|
||||
_Fetched on 2025-11-12 via markitdown MCP._
|
||||
@@ -0,0 +1,49 @@
|
||||
# VS Code Copilot Extension Integration Plan
|
||||
|
||||
## 1. Groundwork
|
||||
- Audit existing claude-mem hook scripts (`context-hook`, `user-message-hook`, `new-hook`, `save-hook`, `summary-hook`, `cleanup-hook`) and their worker-service payloads.
|
||||
- Document REST endpoints, request bodies, and SessionStore schema fields used today so the extension mirrors them exactly.
|
||||
- Confirm worker service availability workflow (`ensureWorkerRunning`, port resolution) and decide how extension error reporting will surface issues to Copilot chat users.
|
||||
|
||||
## 2. Project Scaffold
|
||||
- Clone the VS Code `chat-sample` starter, convert to a TypeScript-only extension, and align lint/tsconfig with repo standards.
|
||||
- Add build pipeline (esbuild or webpack) plus npm scripts that match the existing `scripts/build-hooks.js` release flow.
|
||||
- Wire extension activation events for chat participation and ensure packaging metadata (publisher, categories) is in place.
|
||||
|
||||
## 3. Shared Worker Client
|
||||
- Extract reusable worker-service client utilities from `plugin/scripts/*.js` (port discovery, session init, observation uploads).
|
||||
- Publish TypeScript definitions by re-exporting from `src/services/worker-types.ts` to keep contracts synchronized.
|
||||
- Centralize HTTP calls (timeouts, retries, logging) so every tool implementation uses the same helper layer.
|
||||
|
||||
## 4. Language Model Tool Contracts
|
||||
- Add `contributes.languageModelTools` entries in `package.json` for lifecycle parity:
|
||||
- `mem_session_init`, `mem_user_prompt_log`, `mem_observation_record`, `mem_summary_finalize`, `mem_session_cleanup`.
|
||||
- Provide detailed JSON schemas mirroring hook input structures (session IDs, cwd, prompt text, tool payload metadata).
|
||||
- Supply descriptive `modelDescription`, `userDescription`, icons, tags, and enable `canBeReferencedInPrompt` where appropriate.
|
||||
|
||||
## 5. Tool Implementations
|
||||
- Register each tool via `vscode.lm.registerTool` inside `activate`.
|
||||
- Implement `prepareInvocation` to show user confirmations (especially for cleanup/stop actions) and tailor messages to match existing CLI prompts.
|
||||
- In `invoke`, call the shared worker client, translate successes into `LanguageModelToolResult` text parts, and craft error messages that guide the LLM toward recovery (retry, alternate parameters).
|
||||
- Ensure telemetry/logging records tool usage for debugging without leaking sensitive data.
|
||||
|
||||
## 6. Chat Orchestration
|
||||
- Implement a chat participant based on the sample that maps Copilot threads to claude-mem session IDs stored in turn metadata.
|
||||
- On conversation start, auto-run `mem_session_init`; before each user prompt, dispatch `mem_user_prompt_log`; when Copilot signals stop, run `mem_summary_finalize` (with fallbacks if the worker is unavailable).
|
||||
- Capture tool events emitted by Copilot (file edits, terminal runs) and forward them through `mem_observation_record` with matching payload structure.
|
||||
- Handle conversation disposal or model changes by calling `mem_session_cleanup` to mirror `SessionEnd` hooks.
|
||||
|
||||
## 7. Settings and UX
|
||||
- Read `.claude-mem/settings.json` overrides (worker port, observation depth) and surface VS Code settings for Copilot-specific toggles (auto-sync enabled, max observations per prompt).
|
||||
- Add status bar indicator/commands for worker health, quick restart instructions, and opening the viewer UI (`http://localhost:37777`).
|
||||
- Provide inline notifications when the worker is unreachable, including guidance to restart via PM2.
|
||||
|
||||
## 8. Testing and QA
|
||||
- Draft manual validation checklist: initial session, prompt logging, observation capture, summary completion, worker-down handling.
|
||||
- Add integration tests using `@vscode/test-electron` to simulate chat turns and assert database side effects in a temporary claude-mem data directory.
|
||||
- Build mocks for worker endpoints to enable unit tests of tool invocation logic without hitting the real service.
|
||||
|
||||
## 9. Release Readiness
|
||||
- Document installation and usage in `README.md`, including architecture diagrams showing Copilot → tool → worker flow.
|
||||
- Update CHANGELOG and marketing copy to announce Copilot support and list prerequisites (worker running, settings file placement).
|
||||
- Prepare Marketplace assets (icon, gallery text) and extend existing publish scripts to package and ship the new extension.
|
||||
@@ -0,0 +1,8 @@
|
||||
# VS Code Extension Chat Sample Repository (GitHub Snapshot)
|
||||
|
||||
The markitdown MCP fetch for `https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample` returned only the public navigation scaffolding for GitHub. No repository-specific content or README data was captured because the site requires client-side execution that the fetcher cannot perform.
|
||||
|
||||
You can browse the repository directly for full details:
|
||||
- https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample
|
||||
|
||||
_Fetched on 2025-11-12 via markitdown MCP. Content retrieval was limited to GitHub's static navigation shell._
|
||||
@@ -0,0 +1,267 @@
|
||||
# Language Model Tool API
|
||||
|
||||
Language model tools enable you to extend the functionality of a large language model (LLM) in chat with domain-specific capabilities. To process a user's chat prompt, [agent mode](/docs/copilot/chat/chat-agent-mode) in VS Code can automatically invoke these tools to perform specialized tasks as part of the conversation.
|
||||
|
||||
By contributing a language model tool in your VS Code extension, you can extend the agentic coding workflow while also providing deep integration with the editor. Extension tools are one of three types of tools available in VS Code, alongside [built-in tools and MCP tools](/docs/copilot/chat/chat-tools.md#types-of-tools).
|
||||
|
||||
In this extension guide, you learn how to create a language model tool by using the Language Model Tools API and how to implement tool calling in a chat extension.
|
||||
|
||||
You can also extend the chat experience with specialized tools by contributing an [MCP server](/api/extension-guides/ai/mcp). See the [AI Extensibility Overview](/api/extension-guides/ai/ai-extensibility-overview) for details on the different options and how to decide which approach to use.
|
||||
|
||||
> **Tip**
|
||||
> For information about using tools as an end user, see [Use tools in chat](/docs/copilot/chat/chat-tools.md).
|
||||
|
||||
## What is tool calling in an LLM?
|
||||
|
||||
A language model tool is a function that can be invoked as part of a language model request. For example, you might have a function that retrieves information from a database, performs some calculation, or calls an online API. When you contribute a tool in a VS Code extension, agent mode can then invoke the tool based on the context of the conversation.
|
||||
|
||||
The LLM never actually executes the tool itself, instead the LLM generates the parameters that are used to call your tool. It's important to clearly describe the tool's purpose, functionality, and input parameters so that the tool can be invoked in the right context.
|
||||
|
||||
The following diagram shows the tool-calling flow in agent mode in VS Code. See [Tool-calling flow](#tool-calling-flow) for details about the specific steps involved.
|
||||
|
||||

|
||||
|
||||
Read more about [function calling](https://platform.openai.com/docs/guides/function-calling) in the OpenAI documentation.
|
||||
|
||||
## Why implement a language model tool in your extension?
|
||||
|
||||
Implementing a language model tool in your extension has several benefits:
|
||||
|
||||
- **Extend agent mode** with specialized, domain-specific tools that are automatically invoked as part of responding to a user prompt. For example, enable database scaffolding and querying to dynamically provide the LLM with relevant context.
|
||||
- **Deeply integrate with VS Code** by using the broad set of extension APIs. For example, use the [debug APIs](/api/extension-guides/debugger-extension) to get the current debugging context and use it as part of the tool's functionality.
|
||||
- **Distribute and deploy** tools via the Visual Studio Marketplace, providing a reliable and seamless experience for users. Users don't need a separate installation and update process for your tool.
|
||||
|
||||
You might consider implementing a language model tool with an [MCP server](/api/extension-guides/ai/mcp) in the following scenarios:
|
||||
|
||||
- You already have an MCP server implementation and also want to use it in VS Code.
|
||||
- You want to reuse the same tool across different development environments and platforms.
|
||||
- Your tool is hosted remotely as a service.
|
||||
- You don't need access to VS Code APIs.
|
||||
|
||||
Learn more about the [differences between tool types](/docs/copilot/chat/chat-tools.md#types-of-tools).
|
||||
|
||||
## Create a language model tool
|
||||
|
||||
Implementing a language model tool consists of two main parts:
|
||||
|
||||
1. Define the tool's configuration in the `package.json` file of your extension.
|
||||
2. Implement the tool in your extension code by using the [Language Model API reference](/api/references/vscode-api#lm)
|
||||
|
||||
You can get started with a [basic example project](https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample).
|
||||
|
||||
### 1. Static configuration in `package.json`
|
||||
|
||||
The first step to define a language model tool in your extension is to define it in the `package.json` file of your extension. This configuration includes the tool name, description, input schema, and other metadata:
|
||||
|
||||
1. Add an entry for your tool in the `contributes.languageModelTools` section of your extension's `package.json` file.
|
||||
2. Give the tool a unique name:
|
||||
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| `name` | The unique name of the tool, used to reference the tool in the extension implementation code. Format the name in the format `{verb}_{noun}`. See [naming guidelines](#guidelines-and-conventions). |
|
||||
| `displayName` | The user-friendly name of the tool, used for displaying in the UI. |
|
||||
|
||||
3. If the tool can be used in [agent mode](/docs/copilot/chat/chat-agent-mode) or referenced in a chat prompt with `#`, add the following properties:
|
||||
|
||||
Users can enable or disable the tool in the Chat view, similar to how this is done for [Model Context Protocol (MCP) tools](/docs/copilot/chat/chat-tools.md#mcp-tools).
|
||||
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| `canBeReferencedInPrompt` | Set to `true` if the tool can be used in [agent mode](/docs/copilot/chat/chat-agent-mode) or referenced in chat. |
|
||||
| `toolReferenceName` | The name for users to reference the tool in a chat prompt via `#`. |
|
||||
| `icon` | The icon to display for the tool in the UI. |
|
||||
| `userDescription` | User-friendly description of the tool, used for displaying in the UI. |
|
||||
|
||||
4. Add a detailed description in `modelDescription`. This information is used by the LLM to determine in which context your tool should be used.
|
||||
|
||||
- What exactly does the tool do?
|
||||
- What kind of information does it return?
|
||||
- When should and shouldn't it be used?
|
||||
- Describe important limitations or constraints of the tool.
|
||||
|
||||
5. If the tool takes input parameters, add an `inputSchema` property that describes the tool's input parameters.
|
||||
|
||||
This JSON schema describes an object with the properties that the tool takes as input, and whether they are required. File paths should be absolute paths.
|
||||
|
||||
Describe what each parameter does and how it relates to the tool's functionality.
|
||||
|
||||
6. Add a `when` clause to control when the tool is available.
|
||||
|
||||
The `languageModelTools` contribution point lets you restrict when a tool is available for agent mode or can be referenced in a prompt by using a [when clause](/api/references/when-clause-contexts). For example, a tool that gets the debug call stack information should only be available when the user is debugging.
|
||||
|
||||
```json
|
||||
"contributes": {
|
||||
"languageModelTools": [
|
||||
{
|
||||
"name": "chat-tools-sample_tabCount",
|
||||
...
|
||||
"when": "debugState == 'running'"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example tool definition**
|
||||
|
||||
The following example shows how to define a tool that counts the number of active tabs in a tab group.
|
||||
|
||||
```json
|
||||
"contributes": {
|
||||
"languageModelTools": [
|
||||
{
|
||||
"name": "chat-tools-sample_tabCount",
|
||||
"tags": [
|
||||
"editors",
|
||||
"chat-tools-sample"
|
||||
],
|
||||
"toolReferenceName": "tabCount",
|
||||
"displayName": "Tab Count",
|
||||
"modelDescription": "The number of active tabs in a tab group in VS Code.",
|
||||
"userDescription": "Count the number of active tabs in a tab group.",
|
||||
"canBeReferencedInPrompt": true,
|
||||
"icon": "$(files)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tabGroup": {
|
||||
"type": "number",
|
||||
"description": "The index of the tab group to check. This is optional- if not specified, the active tab group will be checked.",
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Tool implementation
|
||||
|
||||
Implement the language model tool by using the [Language Model API](/api/references/vscode-api#lm). This consists of the following steps:
|
||||
|
||||
1. On activation of the extension, register the tool with [`vscode.lm.registerTool`](/api/references/vscode-api#lm.registerTool).
|
||||
|
||||
Provide the name of the tool as you specified it in the `name` property in `package.json`.
|
||||
|
||||
If you want the tool to be private to your extension, skip the tool registration step.
|
||||
|
||||
```ts
|
||||
export function registerChatTools(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.lm.registerTool('chat-tools-sample_tabCount', new TabCountTool())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. Create a class that implements the [`vscode.LanguageModelTool<>`](/api/references/vscode-api#LanguageModelTool%3CT%3E) interface.
|
||||
|
||||
3. Add tool confirmation messages in the `prepareInvocation` method.
|
||||
|
||||
A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing. The message can be a `MarkdownString` containing a code block.
|
||||
|
||||
The following example shows how to provide a confirmation message for the tab count tool.
|
||||
|
||||
```ts
|
||||
async prepareInvocation(
|
||||
options: vscode.LanguageModelToolInvocationPrepareOptions<ITabCountParameters>,
|
||||
_token: vscode.CancellationToken
|
||||
) {
|
||||
const confirmationMessages = {
|
||||
title: 'Count the number of open tabs',
|
||||
message: new vscode.MarkdownString(
|
||||
`Count the number of open tabs?` +
|
||||
(options.input.tabGroup !== undefined
|
||||
? ` in tab group ${options.input.tabGroup}`
|
||||
: '')
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
invocationMessage: 'Counting the number of tabs',
|
||||
confirmationMessages,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If `prepareInvocation` returned `undefined`, the generic confirmation message will be shown. Note that the user can also select to "Always Allow" a certain tool.
|
||||
|
||||
4. Define an interface that describes the tool input parameters.
|
||||
|
||||
The interface is used in the `invoke` method of the `vscode.LanguageModelTool` class. The input parameters are validated against the JSON schema you defined in the `inputSchema` in `package.json`.
|
||||
|
||||
The following example shows the interface for the tab count tool.
|
||||
|
||||
```ts
|
||||
export interface ITabCountParameters {
|
||||
tabGroup?: number;
|
||||
}
|
||||
```
|
||||
|
||||
5. Implement the `invoke` method. This method is called when the language model tool is invoked while processing a chat prompt.
|
||||
|
||||
The `invoke` method receives the tool input parameters in the `options` parameter. The parameters are validated against the JSON schema defined in `inputSchema` in `package.json`.
|
||||
|
||||
When an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action.
|
||||
|
||||
The following example shows the implementation of the tab count tool. The result of the tool is an instance of type `vscode.LanguageModelToolResult`.
|
||||
|
||||
```ts
|
||||
async invoke(
|
||||
options: vscode.LanguageModelToolInvocationOptions<ITabCountParameters>,
|
||||
_token: vscode.CancellationToken
|
||||
) {
|
||||
const params = options.input;
|
||||
if (typeof params.tabGroup === 'number') {
|
||||
const group = vscode.window.tabGroups.all[Math.max(params.tabGroup - 1, 0)];
|
||||
const nth =
|
||||
params.tabGroup === 1
|
||||
? '1st'
|
||||
: params.tabGroup === 2
|
||||
? '2nd'
|
||||
: params.tabGroup === 3
|
||||
? '3rd'
|
||||
: `${params.tabGroup}th`;
|
||||
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open in the ${nth} tab group.`)]);
|
||||
} else {
|
||||
const group = vscode.window.tabGroups.activeTabGroup;
|
||||
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open.`)]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
View the full source code for implementing a [language model tool](https://github.com/microsoft/vscode-extension-samples/blob/main/chat-sample/src/tools.ts) in the VS Code Extension Samples repository.
|
||||
|
||||
## Tool-calling flow
|
||||
|
||||
When a user sends a chat prompt, the following steps occur:
|
||||
|
||||
1. Copilot determines the list of available tools based on the user's configuration.
|
||||
The list of tools consists of built-in tools, tools registered by extensions, and tools from [MCP servers](/docs/copilot/chat/mcp-servers). You can contribute to agent mode via extensions or MCP servers (shown in green in the diagram).
|
||||
2. Copilot sends the request to the LLM and provides it with the prompt, chat context, and the list of tool definitions to consider.
|
||||
The LLM generates a response, which might include one or more requests to invoke a tool.
|
||||
3. If needed, Copilot invokes the suggested tool(s) with the parameter values provided by the LLM.
|
||||
A tool response might result in more requests for tool invocations.
|
||||
4. If there are errors or follow-up tool requests, Copilot iterates over the tool-calling flow until all tool requests are resolved.
|
||||
5. Copilot returns the final response to the user, which might include responses from multiple tools.
|
||||
|
||||
## Guidelines and conventions
|
||||
|
||||
- **Naming**: write clear and descriptive names for tools and parameters.
|
||||
- **Tool name**: should be unique, and clearly describe their intent. Structure the tool name in the format `{verb}_{noun}`. For example, `get_weather`, `get_azure_deployment`, or `get_terminal_output`.
|
||||
- **Parameter name**: should describe the parameter's purpose. Structure the parameter name in the format `{noun}`. For example, `destination_location`, `ticker`, or `file_name`.
|
||||
- **Descriptions**: write detailed descriptions for tools and parameters.
|
||||
- Describe what the tool does and when it should and shouldn't be used. For example, "This tool retrieves the weather for a given location."
|
||||
- Describe what each parameter does and how it relates to the tool's functionality. For example, "The `destination_location` parameter specifies the location for which to retrieve the weather. It should be a valid location name or coordinates."
|
||||
- Describe important limitations or constraints of the tool. For example, "This tool only retrieves weather data for locations in the United States. It might not work for other regions."
|
||||
- **User confirmation**: provide a confirmation message for the tool invocation. A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing.
|
||||
- **Error handling**: when an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action.
|
||||
|
||||
Get more best practices for creating tools in the [OpenAI documentation](https://platform.openai.com/docs/guides/function-calling?api-mode=chat#best-practices-for-defining-functions) and [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview).
|
||||
|
||||
## Related content
|
||||
|
||||
- [Language Model API reference](/api/references/vscode-api#lm)
|
||||
- [Register an MCP server in a VS Code extension](/api/extension-guides/ai/mcp)
|
||||
- [Use MCP tools in agent mode](/docs/copilot/chat/mcp-servers)
|
||||
|
||||
_Fetched on 2025-11-12 via markitdown MCP._
|
||||
@@ -0,0 +1,13 @@
|
||||
VSCode Language Model Tool API
|
||||
|
||||
Local snapshots fetched via the markitdown MCP on 2025-11-12:
|
||||
|
||||
- `docs/context/vscode-language-model-tool-api.md`
|
||||
- `docs/context/vscode-extension-chat-sample.md`
|
||||
- `docs/context/vscode-api-lm.md`
|
||||
|
||||
Original sources for reference:
|
||||
|
||||
- http://code.visualstudio.com/api/extension-guides/ai/tools
|
||||
- https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample
|
||||
- https://code.visualstudio.com/api/references/vscode-api#lm
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
title: "Plugin Hooks"
|
||||
description: "7 hook scripts that power Claude-Mem"
|
||||
description: "6 lifecycle hooks that power Claude-Mem"
|
||||
---
|
||||
|
||||
# Plugin Hooks
|
||||
|
||||
Claude-Mem integrates with Claude Code through 7 hook scripts across 5 lifecycle events that capture events and inject context.
|
||||
Claude-Mem integrates with Claude Code through 6 hook scripts across 5 lifecycle events that capture events and inject context. Additionally, a smart-install pre-hook script manages dependencies.
|
||||
|
||||
## Hook Overview
|
||||
|
||||
| Hook Name | Purpose | Timeout | Script |
|
||||
| Hook Type | Purpose | Timeout | Script |
|
||||
|---------------------|--------------------------------------|---------|-------------------------|
|
||||
| SessionStart | Smart dependency installation | 300s | smart-install.js |
|
||||
| Pre-Hook | Smart dependency installation | 300s | smart-install.js* |
|
||||
| SessionStart | Inject context from previous sessions| 300s | context-hook.js |
|
||||
| SessionStart | Display first-time setup message | 10s | user-message-hook.js |
|
||||
| UserPromptSubmit | Create/track new sessions | 120s | new-hook.js |
|
||||
@@ -19,6 +19,8 @@ Claude-Mem integrates with Claude Code through 7 hook scripts across 5 lifecycle
|
||||
| Stop | Generate session summaries | 120s | summary-hook.js |
|
||||
| SessionEnd | Mark sessions complete | 120s | cleanup-hook.js |
|
||||
|
||||
*smart-install.js is a pre-hook script (not a lifecycle hook). It's called before context-hook via command chaining in hooks.json.
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
@@ -72,10 +74,12 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
## 1. SessionStart Hook - Smart Install (`smart-install.js`)
|
||||
## 1. Pre-Hook Script - Smart Install (`smart-install.js`)
|
||||
|
||||
**Purpose**: Intelligently manage dependencies and ensure worker service is running.
|
||||
|
||||
**Note**: This is NOT a lifecycle hook - it's a pre-hook script executed via command chaining before context-hook.js runs.
|
||||
|
||||
**Behavior**:
|
||||
- Checks if dependencies need installation using version marker (`.install-version`)
|
||||
- Only runs npm install when:
|
||||
@@ -95,7 +99,7 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `scripts/smart-install.js`
|
||||
**Implementation**: `scripts/smart-install.js` (standalone script, not in src/hooks/)
|
||||
|
||||
**Key Features**:
|
||||
- Version caching prevents redundant installs
|
||||
|
||||
@@ -9,11 +9,12 @@ description: "System components and data flow in Claude-Mem"
|
||||
|
||||
Claude-Mem operates as a Claude Code plugin with five core components:
|
||||
|
||||
1. **Plugin Hooks** - Capture lifecycle events (7 hook files)
|
||||
2. **Worker Service** - Process observations via Claude Agent SDK + HTTP API (10 search endpoints)
|
||||
3. **Database Layer** - Store sessions and observations (SQLite + FTS5 + ChromaDB)
|
||||
4. **Search Skill** - Skill-based search with progressive disclosure (v5.4.0+)
|
||||
5. **Viewer UI** - Web-based real-time memory stream visualization
|
||||
1. **Plugin Hooks** - Capture lifecycle events (6 hook files)
|
||||
2. **Smart Install** - Cached dependency checker (pre-hook script, runs before context-hook)
|
||||
3. **Worker Service** - Process observations via Claude Agent SDK + HTTP API (10 search endpoints)
|
||||
4. **Database Layer** - Store sessions and observations (SQLite + FTS5 + ChromaDB)
|
||||
5. **mem-search Skill** - Skill-based search with progressive disclosure (v5.4.0+)
|
||||
6. **Viewer UI** - Web-based real-time memory stream visualization
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -46,11 +47,11 @@ Hook (stdin) → Database → Worker Service → SDK Processor → Database →
|
||||
|
||||
### Search Pipeline (v5.4.0+)
|
||||
```
|
||||
User Query → Skill Invoked → HTTP API → SessionSearch Service → FTS5 Database → Search Results → Claude
|
||||
User Query → mem-search Skill Invoked → HTTP API → SessionSearch Service → FTS5 Database → Search Results → Claude
|
||||
```
|
||||
|
||||
1. **User Query**: User asks naturally: "What bugs did we fix?"
|
||||
2. **Skill Invoked**: Claude recognizes intent and invokes search skill
|
||||
2. **Skill Invoked**: Claude recognizes intent and invokes mem-search skill
|
||||
3. **HTTP API**: Skill uses curl to call HTTP endpoint (e.g., `/api/search/observations`)
|
||||
4. **SessionSearch**: Worker service queries FTS5 virtual tables
|
||||
5. **Format**: Results formatted and returned to skill
|
||||
@@ -62,8 +63,9 @@ User Query → Skill Invoked → HTTP API → SessionSearch Service → FTS5 Dat
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 0. Smart Install Hook Fires │
|
||||
│ 0. Smart Install Pre-Hook Fires │
|
||||
│ Checks dependencies (cached), only runs on version changes │
|
||||
│ Not a lifecycle hook - runs before context-hook starts │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
@@ -104,14 +106,14 @@ User Query → Skill Invoked → HTTP API → SessionSearch Service → FTS5 Dat
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── hooks/ # Hook implementations (7 hooks)
|
||||
│ │ ├── smart-install.ts # Dependency check (cached)
|
||||
│ ├── hooks/ # Hook implementations (6 hooks)
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── user-message-hook.ts # UserMessage (for debugging)
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │ ├── cleanup-hook.ts # SessionEnd
|
||||
│ │ └── hook-response.ts # Hook response utilities
|
||||
│ │
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
@@ -143,13 +145,15 @@ claude-mem/
|
||||
│ ├── platform.ts
|
||||
│ └── port-allocator.ts
|
||||
│
|
||||
├── scripts/ # Build and utility scripts
|
||||
│ └── smart-install.js # Cached dependency checker (pre-hook)
|
||||
│
|
||||
├── plugin/ # Plugin distribution
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json
|
||||
│ ├── hooks/
|
||||
│ │ └── hooks.json
|
||||
│ ├── scripts/ # Built executables
|
||||
│ │ ├── smart-install.js
|
||||
│ │ ├── context-hook.js
|
||||
│ │ ├── user-message-hook.js
|
||||
│ │ ├── new-hook.js
|
||||
@@ -159,11 +163,14 @@ claude-mem/
|
||||
│ │ └── worker-service.cjs # Background worker + HTTP API
|
||||
│ │
|
||||
│ ├── skills/ # Agent skills (v5.4.0+)
|
||||
│ │ ├── search/ # Search skill with progressive disclosure
|
||||
│ │ ├── mem-search/ # Search skill with progressive disclosure (v5.5.0)
|
||||
│ │ │ ├── SKILL.md # Skill frontmatter (~250 tokens)
|
||||
│ │ │ └── operations/ # Detailed operation docs
|
||||
│ │ │ ├── operations/ # 12 detailed operation docs
|
||||
│ │ │ └── principles/ # 2 principle guides
|
||||
│ │ ├── troubleshoot/ # Troubleshooting skill
|
||||
│ │ └── version-bump/ # Version management skill
|
||||
│ │ │ ├── SKILL.md
|
||||
│ │ │ └── operations/ # 6 operation docs
|
||||
│ │ └── version-bump/ # Version management skill (deprecated)
|
||||
│ │
|
||||
│ └── ui/ # Built viewer UI
|
||||
│ └── viewer.html # Self-contained bundle
|
||||
@@ -175,8 +182,7 @@ claude-mem/
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Plugin Hooks (7 Hooks)
|
||||
- **smart-install.js** - Cached dependency checker (only runs on version changes)
|
||||
### 1. Plugin Hooks (6 Hooks)
|
||||
- **context-hook.js** - SessionStart: Starts PM2 worker, injects context
|
||||
- **user-message-hook.js** - UserMessage: Debugging hook
|
||||
- **new-hook.js** - UserPromptSubmit: Creates session, saves prompt
|
||||
@@ -184,6 +190,8 @@ claude-mem/
|
||||
- **summary-hook.js** - Stop: Generates session summary
|
||||
- **cleanup-hook.js** - SessionEnd: Marks session complete
|
||||
|
||||
**Note**: smart-install.js is a pre-hook dependency checker (not a lifecycle hook). It's called before context-hook via command chaining in hooks.json and only runs when dependencies need updating.
|
||||
|
||||
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
|
||||
|
||||
### 2. Worker Service
|
||||
@@ -205,7 +213,7 @@ SQLite3 with better-sqlite3 driver featuring:
|
||||
|
||||
See [Database Architecture](/architecture/database) for schema and FTS5 search.
|
||||
|
||||
### 4. Search Skill (v5.4.0+)
|
||||
### 4. mem-search Skill (v5.4.0+)
|
||||
Skill-based search with progressive disclosure providing 10 search operations:
|
||||
- Search observations, sessions, prompts (full-text FTS5)
|
||||
- Filter by type, concept, file
|
||||
@@ -217,6 +225,8 @@ Skill-based search with progressive disclosure providing 10 search operations:
|
||||
- Full instructions: ~2,500 tokens (loaded on-demand when invoked)
|
||||
- HTTP API endpoints instead of MCP tools
|
||||
|
||||
**Skill Enhancement (v5.5.0)**: Renamed from "search" to "mem-search" for better scope differentiation. Effectiveness increased from 67% to 100% with enhanced triggers and comprehensive documentation.
|
||||
|
||||
See [Search Architecture](/architecture/search-architecture) for technical details and examples.
|
||||
|
||||
### 5. Viewer UI
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
---
|
||||
title: "Search Architecture"
|
||||
description: "Skill-based search with HTTP API and progressive disclosure"
|
||||
description: "mem-search skill with HTTP API and progressive disclosure"
|
||||
---
|
||||
|
||||
# Search Architecture
|
||||
|
||||
Claude-Mem uses a skill-based search architecture that provides intelligent memory retrieval through natural language queries. This replaced the MCP-based approach in v5.4.0, saving ~2,250 tokens per session start.
|
||||
Claude-Mem uses a skill-based search architecture that provides intelligent memory retrieval through natural language queries. This replaced the MCP-based approach in v5.4.0, saving ~2,250 tokens per session start. The skill was enhanced and renamed to "mem-search" in v5.5.0 for better scope differentiation.
|
||||
|
||||
## Overview
|
||||
|
||||
**Architecture**: Skill-Based Search + HTTP API + Progressive Disclosure
|
||||
|
||||
**Key Components**:
|
||||
1. **Search Skill** (`plugin/skills/search/SKILL.md`) - Auto-invoked when users ask about past work
|
||||
1. **mem-search Skill** (`plugin/skills/mem-search/SKILL.md`) - Auto-invoked when users ask about past work
|
||||
2. **HTTP API Endpoints** (10 routes) - Fast, efficient search operations on port 37777
|
||||
3. **Worker Service** - Express.js server with FTS5 full-text search
|
||||
4. **SQLite Database** - Persistent storage with FTS5 virtual tables
|
||||
5. **Chroma Vector DB** - Semantic search with hybrid retrieval
|
||||
|
||||
**v5.5.0 Enhancement**: Renamed from "search" to "mem-search" with:
|
||||
- Effectiveness increased from 67% to 100%
|
||||
- Concrete triggers increased from 44% to 85%
|
||||
- 5+ unique identifiers for better scope differentiation
|
||||
- Comprehensive documentation (17 files, 12 operation guides)
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. User Query (Natural Language)
|
||||
@@ -28,10 +34,11 @@ User: "What bugs did we fix last session?"
|
||||
|
||||
### 2. Skill Invocation
|
||||
|
||||
Claude recognizes the intent and invokes the search skill:
|
||||
Claude recognizes the intent and invokes the mem-search skill:
|
||||
- Skill frontmatter (~250 tokens) loaded at session start
|
||||
- Full skill instructions loaded on-demand when skill is invoked
|
||||
- Progressive disclosure pattern minimizes context overhead
|
||||
- "mem-search" naming provides clear scope differentiation from native memory
|
||||
|
||||
### 3. HTTP API Call
|
||||
|
||||
@@ -103,7 +110,7 @@ Claude presents the formatted results naturally in conversation.
|
||||
|
||||
### After: Skill-Based Search
|
||||
|
||||
**Approach**: 1 search skill with progressive disclosure
|
||||
**Approach**: 1 mem-search skill with progressive disclosure
|
||||
|
||||
**Token Cost**: ~250 tokens in skill frontmatter per session
|
||||
- Only skill description loaded at session start
|
||||
@@ -112,7 +119,7 @@ Claude presents the formatted results naturally in conversation.
|
||||
|
||||
**Example Skill Frontmatter**:
|
||||
```markdown
|
||||
# Claude-Mem Search Skill
|
||||
# Claude-Mem mem-search Skill
|
||||
|
||||
Access claude-mem's persistent memory through a comprehensive HTTP API.
|
||||
Search for past work, understand context, and learn from previous decisions.
|
||||
@@ -202,7 +209,7 @@ Returns API documentation in JSON format.
|
||||
|
||||
## Progressive Disclosure Pattern
|
||||
|
||||
The search skill uses progressive disclosure to minimize token usage:
|
||||
The mem-search skill uses progressive disclosure to minimize token usage:
|
||||
|
||||
### Layer 1: Skill Frontmatter (Session Start)
|
||||
|
||||
@@ -212,7 +219,7 @@ The search skill uses progressive disclosure to minimize token usage:
|
||||
|
||||
**Example**:
|
||||
```markdown
|
||||
# Claude-Mem Search Skill
|
||||
# Claude-Mem mem-search Skill
|
||||
|
||||
Access claude-mem's persistent memory through a comprehensive HTTP API.
|
||||
|
||||
@@ -262,10 +269,10 @@ Invoke this skill when users ask about:
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Search Skill Structure
|
||||
### mem-search Skill Structure
|
||||
|
||||
```
|
||||
plugin/skills/search/
|
||||
plugin/skills/mem-search/
|
||||
├── SKILL.md # Main frontmatter (~250 tokens)
|
||||
├── operations/
|
||||
│ ├── observations.md # Search observations
|
||||
@@ -396,7 +403,7 @@ Claude translates to appropriate API call.
|
||||
- MCP configuration removed from `plugin/.mcp.json`
|
||||
|
||||
**New Implementation**: Skill-based search
|
||||
- Skill files: `plugin/skills/search/`
|
||||
- Skill files: `plugin/skills/mem-search/`
|
||||
- HTTP endpoints: `src/services/worker-service.ts` (lines 200-400)
|
||||
- Build script: `npm run build` includes skill files
|
||||
- Sync script: `npm run sync-marketplace` copies to plugin directory
|
||||
@@ -427,11 +434,12 @@ curl "http://localhost:37777/api/search/observations?query=test&limit=1"
|
||||
|
||||
### Skill Not Invoking
|
||||
|
||||
If Claude doesn't invoke the skill:
|
||||
If Claude doesn't invoke the mem-search skill automatically:
|
||||
|
||||
1. Check skill files exist: `ls ~/.claude/plugins/marketplaces/thedotmack/plugin/skills/search/`
|
||||
2. Restart Claude Code session
|
||||
3. Try explicit skill invocation: `/skill search`
|
||||
1. Check skill files exist: `ls ~/.claude/plugins/marketplaces/thedotmack/plugin/skills/mem-search/`
|
||||
2. Restart Claude Code session to reload skill definitions
|
||||
3. Try more explicit phrasing: "Search past sessions for bug fixes" or "What did we do in yesterday's session?"
|
||||
4. Ensure your question is about previous sessions (not current conversation context)
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -139,15 +139,15 @@ Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
### Search Configuration (v5.4.0+)
|
||||
|
||||
**Migration Note**: As of v5.4.0, Claude-Mem uses skill-based search instead of MCP tools.
|
||||
**Migration Note**: As of v5.4.0, Claude-Mem uses skill-based search instead of MCP tools. As of v5.5.0, the skill was renamed to "mem-search" for better scope differentiation.
|
||||
|
||||
**Previous (v5.3.x and earlier)**: MCP search server with 9 tools (~2,500 tokens per session)
|
||||
**Current (v5.4.0+)**: Search skill with HTTP API (~250 tokens per session)
|
||||
**Current (v5.4.0+)**: mem-search skill with HTTP API (~250 tokens per session)
|
||||
|
||||
**No configuration required** - the search skill is automatically available in Claude Code sessions.
|
||||
**No configuration required** - the mem-search skill is automatically available in Claude Code sessions.
|
||||
|
||||
Search operations are now provided via:
|
||||
- **Skill**: `plugin/skills/search/SKILL.md` (auto-invoked when users ask about past work)
|
||||
- **Skill**: `plugin/skills/mem-search/SKILL.md` (auto-invoked when users ask about past work)
|
||||
- **HTTP API**: 10 endpoints on worker service port 37777
|
||||
- **Progressive Disclosure**: Full instructions loaded on-demand only when needed
|
||||
|
||||
|
||||
@@ -526,11 +526,14 @@ The `release` script:
|
||||
6. Publish to NPM
|
||||
|
||||
```bash
|
||||
# Use the version bump skill (recommended as of v4.3.0)
|
||||
# In Claude Code, run: /skill version-bump
|
||||
# This updates package.json, marketplace.json, and CLAUDE.md
|
||||
# Manual version bump:
|
||||
# 1. Update version in package.json
|
||||
# 2. Update version in plugin/.claude-plugin/plugin.json
|
||||
# 3. Update version at top of CLAUDE.md
|
||||
# 4. Update version badge in README.md
|
||||
# 5. Run: npm run build && npm run sync-marketplace
|
||||
|
||||
# Or manually:
|
||||
# Or use npm version command:
|
||||
npm version 4.3.2
|
||||
|
||||
# Update changelog
|
||||
|
||||
@@ -68,14 +68,16 @@ Claude Code's hook system provides exactly what we need:
|
||||
|
||||
---
|
||||
|
||||
## The Seven Hook Scripts
|
||||
## The Six Hook Scripts + Pre-Hook
|
||||
|
||||
Claude-Mem uses 7 hook scripts across 5 lifecycle events. SessionStart runs 3 hooks in sequence.
|
||||
Claude-Mem uses 6 lifecycle hook scripts across 5 lifecycle events, plus 1 pre-hook script for dependency management. SessionStart runs 2 hooks in sequence (after the pre-hook script).
|
||||
|
||||
### Hook 1: SessionStart - Smart Install
|
||||
### Pre-Hook: Smart Install (Before SessionStart)
|
||||
|
||||
**Purpose:** Intelligently manage dependencies and start worker service
|
||||
|
||||
**Note:** This is NOT a lifecycle hook - it's a pre-hook script executed via command chaining before context-hook runs.
|
||||
|
||||
**When:** Claude Code starts (startup, clear, or compact)
|
||||
|
||||
**What it does:**
|
||||
@@ -115,11 +117,11 @@ Claude-Mem uses 7 hook scripts across 5 lifecycle events. SessionStart runs 3 ho
|
||||
|
||||
---
|
||||
|
||||
### Hook 2: SessionStart - Context Injection
|
||||
### Hook 1: SessionStart - Context Injection
|
||||
|
||||
**Purpose:** Inject relevant context from previous sessions
|
||||
|
||||
**When:** Claude Code starts (runs after smart-install)
|
||||
**When:** Claude Code starts (runs after smart-install pre-hook)
|
||||
|
||||
**What it does:**
|
||||
1. Extracts project name from current working directory
|
||||
@@ -147,14 +149,14 @@ Claude-Mem uses 7 hook scripts across 5 lifecycle events. SessionStart runs 3 ho
|
||||
|----|------|---|-------|--------|
|
||||
| #2586 | 12:58 AM | 🔵 | Context hook file empty | ~51 |
|
||||
|
||||
*Use claude-mem MCP search to access full details*
|
||||
*Use mem-search skill to access full details*
|
||||
```
|
||||
|
||||
**Source:** `src/hooks/context-hook.ts` → `plugin/scripts/context-hook.js`
|
||||
|
||||
---
|
||||
|
||||
### Hook 3: SessionStart - User Message
|
||||
### Hook 2: SessionStart - User Message
|
||||
|
||||
**Purpose:** Display helpful user messages during first-time setup
|
||||
|
||||
@@ -203,7 +205,7 @@ Claude-Mem uses 7 hook scripts across 5 lifecycle events. SessionStart runs 3 ho
|
||||
|
||||
---
|
||||
|
||||
### Hook 4: UserPromptSubmit (New Session Hook)
|
||||
### Hook 3: UserPromptSubmit (New Session Hook)
|
||||
|
||||
**Purpose:** Initialize session tracking when user submits a prompt
|
||||
|
||||
@@ -250,7 +252,7 @@ VALUES (?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 5: PostToolUse (Save Observation Hook)
|
||||
### Hook 4: PostToolUse (Save Observation Hook)
|
||||
|
||||
**Purpose:** Capture tool execution observations for later processing
|
||||
|
||||
@@ -311,11 +313,11 @@ VALUES (?, ?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 6: Summary Hook (Mid-Session Checkpoint)
|
||||
### Hook 5: Stop Hook (Summary Generation)
|
||||
|
||||
**Purpose:** Generate AI-powered session summaries during the session
|
||||
|
||||
**When:** Triggered programmatically by the worker service
|
||||
**When:** When Claude stops (triggered by Stop lifecycle event)
|
||||
|
||||
**What it does:**
|
||||
1. Gathers session observations from database
|
||||
@@ -327,7 +329,7 @@ VALUES (?, ?, ?, ?, ...)
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Summary": [{
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js"
|
||||
@@ -338,7 +340,7 @@ VALUES (?, ?, ?, ?, ...)
|
||||
```
|
||||
|
||||
**Key decisions:**
|
||||
- ✅ Triggered by worker, not by Claude Code lifecycle
|
||||
- ✅ Triggered by Stop lifecycle event
|
||||
- ✅ Multiple summaries per session (v4.2.0+)
|
||||
- ✅ Summaries are checkpoints, not endings
|
||||
- ✅ Uses Claude Agent SDK for AI compression
|
||||
@@ -366,7 +368,7 @@ VALUES (?, ?, ?, ?, ...)
|
||||
|
||||
---
|
||||
|
||||
### Hook 7: SessionEnd (Cleanup Hook)
|
||||
### Hook 6: SessionEnd (Cleanup Hook)
|
||||
|
||||
**Purpose:** Mark sessions as completed when they end
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
## Key Features
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **Skill-Based Search** - Query your project history with natural language (~2,250 token savings)
|
||||
- 🔍 **mem-search Skill** - Query your project history with natural language (~2,250 token savings)
|
||||
- 🌐 **Web Viewer UI** - Real-time memory stream visualization at http://localhost:37777
|
||||
- 🎨 **Theme Toggle** - Light, dark, and system preference themes
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
@@ -55,11 +55,12 @@ Restart Claude Code. Context from previous sessions will automatically appear in
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd
|
||||
2. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **9 MCP Search Tools** - Query historical context with citations
|
||||
5. **Web Viewer UI** - Real-time visualization with SSE and infinite scroll
|
||||
1. **6 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd, UserMessage
|
||||
2. **Smart Install** - Cached dependency checker (pre-hook script)
|
||||
3. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
5. **mem-search Skill** - Query historical context with natural language
|
||||
6. **Web Viewer UI** - Real-time visualization with SSE and infinite scroll
|
||||
|
||||
See [Architecture Overview](architecture/overview) for details.
|
||||
|
||||
@@ -70,22 +71,25 @@ See [Architecture Overview](architecture/overview) for details.
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New in v5.1.2
|
||||
## What's New in v6.0.0
|
||||
|
||||
**Latest Updates (v5.1.2):**
|
||||
- Theme toggle for light, dark, and system preferences in viewer UI
|
||||
- Improved visual design with theme-aware components
|
||||
**🚀 Major Session Management & Transcript Processing Improvements:**
|
||||
|
||||
**Recent Updates (v5.1.0):**
|
||||
- Web-based viewer UI for real-time memory stream visualization
|
||||
- Server-Sent Events (SSE) for instant updates
|
||||
- Infinite scroll pagination with project filtering
|
||||
- 8 new HTTP/SSE endpoints in worker service
|
||||
- **Enhanced Session Initialization**: Accept userPrompt and promptNumber for better context tracking
|
||||
- **Live UserPrompt Updates**: Multi-turn conversation support with real-time prompt tracking
|
||||
- **Improved SessionManager**: Better context handling and observation processing
|
||||
- **Comprehensive Transcript Processing**: New scripts and utilities for analyzing Claude Code transcripts
|
||||
- **Rich Context Extraction**: Advanced parsing utilities for extracting meaningful context from sessions
|
||||
- **Refactored Architecture**: Improved hooks and SDKAgent for more reliable observation handling
|
||||
- **Silent Debug Logging**: Better debugging capabilities without cluttering output
|
||||
- **Enhanced Error Handling**: More robust error recovery and debugging tools
|
||||
|
||||
**Previous Updates (v4.3.1):**
|
||||
- Fixed SessionStart hook context injection
|
||||
- Smart install caching for Windows compatibility
|
||||
- Progressive disclosure context with observation timelines
|
||||
**Breaking Changes**: Significant architectural changes in session management and observation handling. Existing sessions continue to work, but internal APIs have evolved.
|
||||
|
||||
**Previous Highlights:**
|
||||
- **v5.5.0**: mem-search skill enhancement with 100% effectiveness rate
|
||||
- **v5.4.0**: Skill-based search architecture (~2,250 tokens saved per session)
|
||||
- **v5.1.2**: Theme toggle for light/dark mode in viewer UI
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -7,13 +7,9 @@ description: "Common issues and solutions for Claude-Mem"
|
||||
|
||||
## Quick Diagnostic Tool
|
||||
|
||||
**NEW:** Use the automated troubleshooting skill for instant diagnosis:
|
||||
Describe any issues you're experiencing to Claude, and the troubleshoot skill will automatically activate to provide diagnosis and fixes.
|
||||
|
||||
```
|
||||
/skill troubleshoot
|
||||
```
|
||||
|
||||
This skill will:
|
||||
The troubleshoot skill will:
|
||||
- ✅ Check PM2 worker status and health
|
||||
- ✅ Verify database existence and integrity
|
||||
- ✅ Test worker service connectivity
|
||||
@@ -21,7 +17,7 @@ This skill will:
|
||||
- ✅ Check port configuration and availability
|
||||
- ✅ Provide automated fixes for common issues
|
||||
|
||||
The skill includes comprehensive diagnostics, automated repair sequences, and detailed troubleshooting workflows for all common issues. Use it before manually troubleshooting below.
|
||||
The skill includes comprehensive diagnostics, automated repair sequences, and detailed troubleshooting workflows for all common issues. Simply describe the problem naturally to invoke it.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -160,9 +160,9 @@ Context injection uses progressive disclosure for efficient token usage:
|
||||
- Shows full summary details **only if** generated after last observation
|
||||
- Token cost: ~50-200 tokens for index view
|
||||
|
||||
### Layer 2: On-Demand Details (Skill-Based Search)
|
||||
### Layer 2: On-Demand Details (mem-search Skill)
|
||||
- Ask naturally: "What bugs did we fix?" or "How did we implement X?"
|
||||
- Claude auto-invokes search skill to fetch full details
|
||||
- Claude auto-invokes mem-search skill to fetch full details
|
||||
- Search by concept, file, type, or keyword
|
||||
- Timeline context around specific observations
|
||||
- Token cost: ~100-500 tokens per observation fetched
|
||||
@@ -195,7 +195,7 @@ The `/clear` command clears the conversation context visible to Claude AND re-in
|
||||
|
||||
## Searching Your History (v5.4.0+)
|
||||
|
||||
Claude-Mem now uses skill-based search for querying your project history. Simply ask naturally:
|
||||
Claude-Mem uses the mem-search skill for querying your project history. Simply ask naturally:
|
||||
|
||||
```
|
||||
"What bugs did we fix last session?"
|
||||
@@ -204,7 +204,7 @@ Claude-Mem now uses skill-based search for querying your project history. Simply
|
||||
"Show me recent work on this project"
|
||||
```
|
||||
|
||||
Claude automatically recognizes your intent and invokes the search skill, which uses HTTP API endpoints to query your memory efficiently.
|
||||
Claude automatically recognizes your intent and invokes the mem-search skill, which uses HTTP API endpoints to query your memory efficiently.
|
||||
|
||||
**Token Savings**: ~2,250 tokens per session start vs previous MCP approach
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
---
|
||||
title: "Skill-Based Search"
|
||||
title: "mem-search Skill"
|
||||
description: "Query your project history with natural language"
|
||||
---
|
||||
|
||||
# Skill-Based Search Usage
|
||||
# mem-search Skill Usage
|
||||
|
||||
Once claude-mem is installed as a plugin, you can search your project history using natural language. Claude automatically invokes the search skill when you ask about past work.
|
||||
Once claude-mem is installed as a plugin, you can search your project history using natural language. Claude automatically invokes the mem-search skill when you ask about past work.
|
||||
|
||||
## How It Works
|
||||
|
||||
**v5.4.0 Migration**: Claude-Mem now uses a skill-based search architecture instead of MCP tools, saving ~2,250 tokens per session start through progressive disclosure.
|
||||
**v5.5.0 Enhancement**: The search skill was renamed to "mem-search" for better scope differentiation, with effectiveness increased from 67% to 100% and enhanced concrete triggers (85% vs 44%).
|
||||
|
||||
**v5.4.0 Architecture**: Claude-Mem uses a skill-based search architecture instead of MCP tools, saving ~2,250 tokens per session start through progressive disclosure.
|
||||
|
||||
**Simple Usage:**
|
||||
- Just ask naturally: *"What did we do last session?"*
|
||||
- Claude recognizes the intent and invokes the search skill
|
||||
- Claude recognizes the intent and invokes the mem-search skill
|
||||
- The skill uses HTTP API endpoints to query your memory
|
||||
- Results are formatted and presented to you
|
||||
|
||||
@@ -22,6 +24,7 @@ Once claude-mem is installed as a plugin, you can search your project history us
|
||||
- **Natural Language**: No need to learn specific tool syntax
|
||||
- **Progressive Disclosure**: Only loads detailed instructions when needed
|
||||
- **Auto-Invoked**: Claude knows when to search based on your questions
|
||||
- **Scope Differentiation**: "mem-search" clearly distinguishes from native conversation memory
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -118,7 +121,7 @@ Once claude-mem is installed as a plugin, you can search your project history us
|
||||
|
||||
## Search Strategy
|
||||
|
||||
The search skill uses a progressive disclosure pattern to efficiently retrieve information:
|
||||
The mem-search skill uses a progressive disclosure pattern to efficiently retrieve information:
|
||||
|
||||
### 1. Ask Naturally
|
||||
|
||||
@@ -127,9 +130,9 @@ Start with a natural language question:
|
||||
"What bugs did we fix related to authentication?"
|
||||
```
|
||||
|
||||
### 2. Claude Invokes Search Skill
|
||||
### 2. Claude Invokes mem-search Skill
|
||||
|
||||
Claude recognizes your intent and loads the search skill (~250 tokens for skill frontmatter).
|
||||
Claude recognizes your intent and loads the mem-search skill (~250 tokens for skill frontmatter).
|
||||
|
||||
### 3. Skill Uses HTTP API
|
||||
|
||||
@@ -197,7 +200,7 @@ You can refine searches using natural language filters:
|
||||
|
||||
## Under the Hood: HTTP API
|
||||
|
||||
The search skill uses HTTP endpoints on the worker service (port 37777):
|
||||
The mem-search skill uses HTTP endpoints on the worker service (port 37777):
|
||||
|
||||
- `GET /api/search/observations` - Full-text search observations
|
||||
- `GET /api/search/sessions` - Full-text search session summaries
|
||||
@@ -366,10 +369,7 @@ npm run worker:restart # Restart if needed
|
||||
npm run worker:logs # View logs
|
||||
```
|
||||
|
||||
Or use the troubleshooting skill:
|
||||
```
|
||||
/skill troubleshoot
|
||||
```
|
||||
Or describe the issue to Claude and the troubleshoot skill will automatically activate to provide diagnosis.
|
||||
|
||||
### Performance Issues
|
||||
|
||||
@@ -383,14 +383,16 @@ If searches seem slow:
|
||||
|
||||
**Architecture Change (v5.4.0)**:
|
||||
- **Before**: 9 MCP tools (~2,500 tokens in tool definitions per session start)
|
||||
- **After**: 1 search skill (~250 tokens in frontmatter, full instructions loaded on-demand)
|
||||
- **After**: 1 mem-search skill (~250 tokens in frontmatter, full instructions loaded on-demand)
|
||||
- **Savings**: ~2,250 tokens per session start
|
||||
- **Migration**: Transparent - users don't need to change how they ask questions
|
||||
|
||||
**v5.5.0 Enhancement**: Renamed from "search" to "mem-search" with improved effectiveness (67% → 100%) and enhanced triggers (44% → 85%).
|
||||
|
||||
**How the Skill Works:**
|
||||
1. User asks a question about past work
|
||||
2. Claude recognizes the intent matches the search skill description
|
||||
3. Skill loads full instructions from `plugin/skills/search/SKILL.md`
|
||||
2. Claude recognizes the intent matches the mem-search skill description
|
||||
3. Skill loads full instructions from `plugin/skills/mem-search/SKILL.md`
|
||||
4. Skill uses `curl` to call HTTP API endpoints
|
||||
5. Results formatted and returned to Claude
|
||||
6. Claude presents results to user
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.4.1",
|
||||
"version": "6.0.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"version": "5.4.1",
|
||||
"version": "6.0.9",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.5.0",
|
||||
"version": "6.2.0",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -39,7 +39,9 @@
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
"worker:logs": "pm2 logs claude-mem-worker",
|
||||
"worker:logs": "pm2 flush claude-mem-worker && pm2 logs claude-mem-worker --lines 100 --nostream",
|
||||
"worker:logs:no-flush": "pm2 logs claude-mem-worker --lines 100 --nostream",
|
||||
"changelog:generate": "node scripts/generate-changelog.js",
|
||||
"usage:analyze": "node scripts/analyze-usage.js",
|
||||
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "5.5.0",
|
||||
"version": "6.2.0",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as I}from"process";import w from"better-sqlite3";import{join as E,dirname as k,basename as W}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),l=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),J=E(l,"archives"),Q=E(l,"logs"),z=E(l,"trash"),Z=E(l,"backups"),ee=E(l,"settings.json"),f=E(l,"claude-mem.db"),se=E(l,"vector-db"),te=E(R,"settings.json"),re=E(R,"commands"),ne=E(R,"CLAUDE.md");function L(p){x(p,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(l),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as I}from"process";import w from"better-sqlite3";import{join as m,dirname as k,basename as W}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),E=process.env.CLAUDE_MEM_DATA_DIR||m(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||m(O(),".claude"),J=m(E,"archives"),Q=m(E,"logs"),z=m(E,"trash"),Z=m(E,"backups"),ee=m(E,"settings.json"),f=m(E,"claude-mem.db"),se=m(E,"vector-db"),te=m(R,"settings.json"),re=m(R,"commands"),ne=m(R,"CLAUDE.md");function L(c){x(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let T="";n!=null&&(this.level===0&&typeof n=="object"?T=`
|
||||
`+JSON.stringify(n,null,2):T=" "+this.formatData(n));let u="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:p,...a}=r;Object.keys(a).length>0&&(u=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${u}${T}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(E),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,o.toISOString(),i);return{id:Number(u.lastInsertRowid),createdAtEpoch:i}}storeSummary(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,o.toISOString(),i);return{id:Number(u.lastInsertRowid),createdAtEpoch:i}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -357,7 +357,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let m=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,_;if(e!==null){let l=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
@@ -369,7 +369,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(m).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let m=`
|
||||
`;try{let p=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary observations:",p.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
@@ -381,12 +381,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
`;try{let p=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(p.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=p.length>0?p[p.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(p){return console.error("[SessionStore] Error getting boundary timestamps:",p.message),{observations:[],sessions:[],prompts:[]}}}let T=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,T=`
|
||||
`,u=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
@@ -397,5 +397,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let m=this.db.prepare(u).all(d,_,...i),S=this.db.prepare(T).all(d,_,...i),c=this.db.prepare(b).all(d,_,...i);return{observations:m,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import X from"path";import{homedir as F}from"os";import{existsSync as B,readFileSync as j}from"fs";function C(){try{let p=X.join(F(),".claude-mem","settings.json");if(B(p)){let e=JSON.parse(j(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function v(p){console.error("[claude-mem cleanup] Hook fired",{input:p?{session_id:p.session_id,cwd:p.cwd,reason:p.reason}:null}),p||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=p;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||C();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)v(void 0);else{let p="";I.on("data",e=>p+=e),I.on("end",async()=>{let e=p?JSON.parse(p):void 0;await v(e)})}
|
||||
`;try{let l=this.db.prepare(T).all(d,_,...i),S=this.db.prepare(u).all(d,_,...i),p=this.db.prepare(b).all(d,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import F from"path";import{homedir as X}from"os";import{existsSync as B,readFileSync as j}from"fs";function v(){try{let c=F.join(X(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(j(c,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||v();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import Y from"path";import{stdin as D}from"process";import F from"better-sqlite3";import{join as u,dirname as U,basename as J}from"path";import{homedir as f}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function X(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=X(),m=process.env.CLAUDE_MEM_DATA_DIR||u(f(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||u(f(),".claude"),re=u(m,"archives"),ne=u(m,"logs"),oe=u(m,"trash"),ie=u(m,"backups"),ae=u(m,"settings.json"),L=u(m,"claude-mem.db"),pe=u(m,"vector-db"),de=u(h,"settings.json"),ce=u(h,"commands"),_e=u(h,"CLAUDE.md");function A(p){M(p,{recursive:!0})}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:b,correlationId:_,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let S=`[${o}] [${i}] [${d}] ${c}${t}${T}${E}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},C=new O;var g=class{db;constructor(){A(m),this.db=new F(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import z from"path";import{stdin as M}from"process";import W from"better-sqlite3";import{join as m,dirname as X,basename as te}from"path";import{homedir as I}from"os";import{existsSync as ie,mkdirSync as P}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:X(H(import.meta.url))}var j=B(),l=process.env.CLAUDE_MEM_DATA_DIR||m(I(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||m(I(),".claude"),de=m(l,"archives"),pe=m(l,"logs"),ce=m(l,"trash"),_e=m(l,"backups"),ue=m(l,"settings.json"),L=m(l,"claude-mem.db"),me=m(l,"vector-db"),Ee=m(h,"settings.json"),le=m(h,"commands"),Te=m(h,"CLAUDE.md");function A(a){P(a,{recursive:!0})}function v(){return m(j,"..","..")}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=`
|
||||
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...d}=r;Object.keys(d).length>0&&(E=` {${Object.entries(d).map(([w,F])=>`${w}=${F}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${E}${c}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},C=new O;var R=class{db;constructor(){A(l),this.db=new W(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(d=>r.add(d))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,o.toISOString(),i);return{id:Number(E.lastInsertRowid),createdAtEpoch:i}}storeSummary(e,s,t,r,n=0){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,o.toISOString(),i);return{id:Number(E.lastInsertRowid),createdAtEpoch:i}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -357,47 +357,47 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE up.id IN (${i})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],d,c;if(e!==null){let l=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,u;if(e!==null){let T=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let _=this.db.prepare(T).all(e,...i,t+1),d=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let _=this.db.prepare(T).all(s,...i,t),d=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,T=`
|
||||
`,E=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(E).all(d,c,...i),b=this.db.prepare(T).all(d,c,...i),_=this.db.prepare(S).all(d,c,...i);return{observations:l,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=P(p,e,s);return JSON.stringify(t)}import H from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function R(){try{let p=H.join(B(),".claude-mem","settings.json");if(j(p)){let e=JSON.parse($(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let p=R();return(await fetch(`http://127.0.0.1:${p}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let p=R();throw new Error(`Worker service is not responding on port ${p}.
|
||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(E).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import D from"path";import{homedir as G}from"os";import{existsSync as k,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=500,J=10;function g(){try{let a=D.join(G(),".claude-mem","settings.json");if(k(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function x(){try{let a=g();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){try{let a=v(),e=D.join(a,"ecosystem.config.cjs");if(!k(e))throw new Error(`Ecosystem config not found at ${e}`);K(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<J;s++)if(await new Promise(t=>setTimeout(t,q)),await x())return!0;return!1}catch{return!1}}async function U(){if(await x())return;if(!await Q()){let e=g();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function K(p){if(!p)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=p,r=Y.basename(s);await y();let n=new g,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let d=R();try{let c=await fetch(`http://127.0.0.1:${d}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(v("UserPromptSubmit",!0))}var I="";D.on("data",p=>I+=p);D.on("end",async()=>{let p=I?JSON.parse(I):void 0;await K(p)});
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}async function Z(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a,r=z.basename(s);await U();let n=new R,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let p=g(),u=t.startsWith("/")?t.substring(1):t;try{let c=await fetch(`http://127.0.0.1:${p}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:u,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(y("UserPromptSubmit",!0))}var f="";M.on("data",a=>f+=a);M.on("end",async()=>{let a=f?JSON.parse(f):void 0;await Z(a)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as A}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),l=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),re=m(l,"archives"),ne=m(l,"logs"),oe=m(l,"trash"),ie=m(l,"backups"),ae=m(l,"settings.json"),C=m(l,"claude-mem.db"),de=m(l,"vector-db"),pe=m(N,"settings.json"),ce=m(N,"commands"),_e=m(N,"CLAUDE.md");function v(d){M(d,{recursive:!0})}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=O[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let c="";if(r){let{sessionId:T,sdkSessionId:g,correlationId:_,...a}=r;Object.keys(a).length>0&&(c=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new I;var R=class{db;constructor(){v(l),this.db=new X(C),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as M}from"process";import W from"better-sqlite3";import{join as m,dirname as X,basename as te}from"path";import{homedir as A}from"os";import{existsSync as ie,mkdirSync as H}from"fs";import{fileURLToPath as P}from"url";function B(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var j=B(),T=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),de=m(T,"archives"),pe=m(T,"logs"),ce=m(T,"trash"),_e=m(T,"backups"),ue=m(T,"settings.json"),v=m(T,"claude-mem.db"),me=m(T,"vector-db"),Ee=m(N,"settings.json"),le=m(N,"commands"),Te=m(N,"CLAUDE.md");function y(a){H(a,{recursive:!0})}function C(){return m(j,"..","..")}var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),f=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),n=O[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let c="";if(r){let{sessionId:b,sdkSessionId:R,correlationId:_,...d}=r;Object.keys(d).length>0&&(c=` {${Object.entries(d).map(([w,F])=>`${w}=${F}`).join(", ")}}`)}let l=`[${i}] [${n}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new f;var g=class{db;constructor(){y(T),this.db=new W(v),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -244,12 +244,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
WHERE id IN (${n})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${i}
|
||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let n=JSON.parse(i.files_read);Array.isArray(n)&&n.forEach(p=>r.add(p))}catch{}if(i.files_modified)try{let n=JSON.parse(i.files_modified);Array.isArray(n)&&n.forEach(p=>o.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -289,13 +289,13 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),n=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),o);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:i.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
@@ -308,33 +308,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r,o=0){let i=new Date,n=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o,i.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}storeSummary(e,s,t,r,o=0){let i=new Date,n=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),n),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let c=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o,i.toISOString(),n);return{id:Number(c.lastInsertRowid),createdAtEpoch:n}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -342,62 +342,62 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${i})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
WHERE id IN (${n})
|
||||
ORDER BY created_at_epoch ${o}
|
||||
${i}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,o=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",n=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
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 (${i})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,u;if(e!==null){let T=`
|
||||
WHERE up.id IN (${n})
|
||||
ORDER BY up.created_at_epoch ${o}
|
||||
${i}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,o){let i=o?"AND project = ?":"",n=o?[o]:[],p,u;if(e!==null){let b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
WHERE id <= ? ${i}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,g=`
|
||||
`,R=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
WHERE id >= ? ${i}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(e,...i,t+1),a=this.db.prepare(g).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
`;try{let _=this.db.prepare(b).all(e,...n,t+1),d=this.db.prepare(R).all(e,...n,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,g=`
|
||||
`,R=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
WHERE created_at_epoch >= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(s,...i,t),a=this.db.prepare(g).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let _=this.db.prepare(b).all(s,...n,t),d=this.db.prepare(R).all(s,...n,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,c=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,b=`
|
||||
`,l=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(E).all(p,u,...i),g=this.db.prepare(c).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let b=this.db.prepare(E).all(p,u,...n),R=this.db.prepare(c).all(p,u,...n),_=this.db.prepare(l).all(p,u,...n);return{observations:b,sessions:R.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(b){return console.error("[SessionStore] Error querying timeline records:",b.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function I(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import D from"path";import{homedir as G}from"os";import{existsSync as k,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=500,J=10;function h(){try{let a=D.join(G(),".claude-mem","settings.json");if(k(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function x(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){try{let a=C(),e=D.join(a,"ecosystem.config.cjs");if(!k(e))throw new Error(`Ecosystem config not found at ${e}`);K(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<J;s++)if(await new Promise(t=>setTimeout(t,q)),await x())return!0;return!1}catch{return!1}}async function U(){if(await x())return;if(!await Q()){let e=h();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}var Y=new Set(["ListMcpResourcesTool"]);async function K(d){if(!d)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:n}=d;if(Y.has(t)){console.log(f("PostToolUse",!0));return}await y();let o=new R,i=o.createSDKSession(e,"",""),p=o.getPromptCounter(i);o.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:i,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:n!==void 0?JSON.stringify(n):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let b=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:i,status:c.status},b),new Error(`Failed to send observation to worker: ${c.status} ${b}`)}S.debug("HOOK","Observation sent successfully",{sessionId:i,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var L="";D.on("data",d=>L+=d);D.on("end",async()=>{let d=L?JSON.parse(L):void 0;await K(d)});
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}var z=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:o}=a;if(z.has(t)){console.log(I("PostToolUse",!0));return}await U();let i=new g,n=i.createSDKSession(e,"",""),p=i.getPromptCounter(n);i.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:n,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:o!==void 0?JSON.stringify(o):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let l=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},l),new Error(`Failed to send observation to worker: ${c.status} ${l}`)}S.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(I("PostToolUse",!0))}var L="";M.on("data",a=>L+=a);M.on("end",async()=>{let a=L?JSON.parse(L):void 0;await Z(a)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as D}from"process";import X from"better-sqlite3";import{join as u,dirname as U,basename as V}from"path";import{homedir as L}from"os";import{existsSync as Z,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var se=F(),m=process.env.CLAUDE_MEM_DATA_DIR||u(L(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||u(L(),".claude"),te=u(m,"archives"),re=u(m,"logs"),ne=u(m,"trash"),oe=u(m,"backups"),ie=u(m,"settings.json"),A=u(m,"claude-mem.db"),ae=u(m,"vector-db"),de=u(N,"settings.json"),pe=u(N,"commands"),ce=u(N,"CLAUDE.md");function C(d){M(d,{recursive:!0})}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=O[e].padEnd(5),p=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let g=`[${o}] [${i}] [${p}] ${_}${t}${T}${E}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},b=new I;var R=class{db;constructor(){C(m),this.db=new X(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as w}from"process";import{readFileSync as F,existsSync as X}from"fs";import Y from"better-sqlite3";import{join as m,dirname as j,basename as pe}from"path";import{homedir as y}from"os";import{existsSync as Ee,mkdirSync as B}from"fs";import{fileURLToPath as $}from"url";function W(){return typeof __dirname<"u"?__dirname:j($(import.meta.url))}var G=W(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),Te=m(l,"archives"),ge=m(l,"logs"),Se=m(l,"trash"),be=m(l,"backups"),Re=m(l,"settings.json"),A=m(l,"claude-mem.db"),he=m(l,"vector-db"),fe=m(f,"settings.json"),Oe=m(f,"commands"),Ne=m(f,"CLAUDE.md");function v(a){B(a,{recursive:!0})}function C(){return m(G,"..","..")}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=O[e].padEnd(5),d=s.padEnd(6),p="";r?.correlationId?p=`[${r.correlationId}] `:r?.sessionId&&(p=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:b,correlationId:_,...c}=r;Object.keys(c).length>0&&(E=` {${Object.entries(c).map(([H,P])=>`${H}=${P}`).join(", ")}}`)}let S=`[${i}] [${o}] [${d}] ${p}${t}${E}${u}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},g=new N;var R=class{db;constructor(){v(l),this.db=new Y(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
@@ -244,12 +244,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id = ?
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).get(e)||null}getObservationsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE id IN (${i})
|
||||
WHERE id IN (${o})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
${i}
|
||||
`).all(...e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
@@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let o of t){if(o.files_read)try{let i=JSON.parse(o.files_read);Array.isArray(i)&&i.forEach(p=>r.add(p))}catch{}if(o.files_modified)try{let i=JSON.parse(o.files_modified);Array.isArray(i)&&i.forEach(p=>n.add(p))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let o=JSON.parse(i.files_read);Array.isArray(o)&&o.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let o=JSON.parse(i.files_modified);Array.isArray(o)&&o.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
@@ -289,17 +289,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),i=this.db.prepare(`
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),o=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),n);return i.lastInsertRowid===0||i.changes===0?this.db.prepare(`
|
||||
`).run(e,e,s,t,r.toISOString(),n);return o.lastInsertRowid===0||o.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:i.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
`).get(e).id:o.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(b.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(g.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -312,29 +312,29 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r){let n=new Date,o=n.getTime();this.db.prepare(`
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,i.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
`).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let E=this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,i.toISOString(),o);return{id:Number(E.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -342,62 +342,77 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
`).run(s.toISOString(),t,e)}getSessionSummariesByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT * FROM session_summaries
|
||||
WHERE id IN (${i})
|
||||
WHERE id IN (${o})
|
||||
ORDER BY created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",o=r?`LIMIT ${r}`:"",i=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
${i}
|
||||
`).all(...e)}getUserPromptsByIds(e,s={}){if(e.length===0)return[];let{orderBy:t="date_desc",limit:r}=s,n=t==="date_asc"?"ASC":"DESC",i=r?`LIMIT ${r}`:"",o=e.map(()=>"?").join(",");return this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
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 (${i})
|
||||
WHERE up.id IN (${o})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${o}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let o=n?"AND project = ?":"",i=n?[n]:[],p,_;if(e!==null){let l=`
|
||||
${i}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let i=n?"AND project = ?":"",o=n?[n]:[],d,p;if(e!==null){let T=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${o}
|
||||
WHERE id <= ? ${i}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
WHERE id >= ? ${i}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let _=this.db.prepare(T).all(e,...o,t+1),c=this.db.prepare(b).all(e,...o,r+1);if(_.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,p=c.length>0?c[c.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
WHERE created_at_epoch >= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let _=this.db.prepare(T).all(s,...o,t),c=this.db.prepare(b).all(s,...o,r+1);if(_.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,p=c.length>0?c[c.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,T=`
|
||||
`,E=`
|
||||
SELECT *
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,g=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(E).all(p,_,...i),S=this.db.prepare(T).all(p,_,...i),c=this.db.prepare(g).all(p,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import B from"path";import{homedir as P}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=B.join(P(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function v(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let T=this.db.prepare(u).all(d,p,...o),b=this.db.prepare(E).all(d,p,...o),_=this.db.prepare(S).all(d,p,...o);return{observations:T,sessions:b.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:_.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import k from"path";import{homedir as q}from"os";import{existsSync as x,readFileSync as V}from"fs";import{execSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=k.join(q(),".claude-mem","settings.json");if(x(a)){let e=JSON.parse(V(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=C(),e=k.join(a,"ecosystem.config.cjs");if(!x(e))throw new Error(`Ecosystem config not found at ${e}`);J(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<Z;s++)if(await new Promise(t=>setTimeout(t,z)),await U())return!0;return!1}catch{return!1}}async function M(){if(await U())return;if(!await ee()){let e=h();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function Y(d){if(!d)throw new Error("summaryHook requires input");let{session_id:e}=d;await v();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=h();b.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw b.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}b.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}finally{await fetch(`http://127.0.0.1:${n}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(y("Stop",!0))}var f="";D.on("data",d=>f+=d);D.on("end",async()=>{let d=f?JSON.parse(f):void 0;await Y(d)});
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}import{appendFileSync as se}from"fs";import{homedir as te}from"os";import{join as re}from"path";var ne=re(te(),".claude-mem","silent.log");function I(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),d=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",p=`[${t}] [${d}] ${a}`;if(e!==void 0)try{p+=` ${JSON.stringify(e)}`}catch(u){p+=` [stringify error: ${u}]`}p+=`
|
||||
`;try{se(ne,p)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return s}function oe(a){if(!a||!X(a))return"";try{let e=F(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="user"&&r.message?.content){let n=r.message.content;if(typeof n=="string")return n;if(Array.isArray(n))return n.filter(o=>o.type==="text").map(o=>o.text).join(`
|
||||
`)}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}function ie(a){if(!a||!X(a))return"";try{let e=F(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="assistant"&&r.message?.content){let n="",i=r.message.content;return typeof i=="string"?n=i:Array.isArray(i)&&(n=i.filter(d=>d.type==="text").map(d=>d.text).join(`
|
||||
`)),n=n.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),n=n.replace(/\n{3,}/g,`
|
||||
|
||||
`).trim(),n}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}async function ae(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await M();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t),n=s.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project
|
||||
FROM sdk_sessions WHERE id = ?
|
||||
`).get(t),i=s.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).get(n?.sdk_session_id);I("[summary-hook] Session diagnostics",{claudeSessionId:e,sessionDbId:t,sdkSessionId:n?.sdk_session_id,project:n?.project,promptNumber:r,observationCount:i?.count||0,transcriptPath:a.transcript_path}),s.close();let o=h(),d=oe(a.transcript_path||""),p=ie(a.transcript_path||"");I("[summary-hook] Extracted messages",{hasLastUserMessage:!!d,hasLastAssistantMessage:!!p,lastAssistantPreview:p.substring(0,200),lastAssistantLength:p.length}),g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r,hasLastUserMessage:!!d,hasLastAssistantMessage:!!p});try{let u=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r,last_user_message:d,last_assistant_message:p}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let E=await u.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:u.status},E),new Error(`Failed to request summary from worker: ${u.status} ${E}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}finally{await fetch(`http://127.0.0.1:${o}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(D("Stop",!0))}var L="";w.on("data",a=>L+=a);w.on("end",async()=>{let a=L?JSON.parse(L):void 0;await ae(a)});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import{execSync as p}from"child_process";import{join as r}from"path";import{homedir as s}from"os";import{existsSync as u}from"fs";import i from"path";import{homedir as a}from"os";import{existsSync as c,readFileSync as l}from"fs";function n(){try{let e=i.join(a(),".claude-mem","settings.json");if(c(e)){let t=JSON.parse(l(e,"utf-8")),o=parseInt(t.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(o))return o}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var m=r(s(),".claude","plugins","marketplaces","thedotmack"),d=r(m,"node_modules");u(d)||(console.error(`
|
||||
import{execSync as _}from"child_process";import{join as i}from"path";import{homedir as p}from"os";import{existsSync as x}from"fs";import l from"path";import{homedir as f}from"os";import{existsSync as g,readFileSync as h}from"fs";import{join as t,dirname as m,basename as y}from"path";import{homedir as c}from"os";import{fileURLToPath as u}from"url";function d(){return typeof __dirname<"u"?__dirname:m(u(import.meta.url))}var A=d(),e=process.env.CLAUDE_MEM_DATA_DIR||t(c(),".claude-mem"),s=process.env.CLAUDE_CONFIG_DIR||t(c(),".claude"),P=t(e,"archives"),w=t(e,"logs"),C=t(e,"trash"),I=t(e,"backups"),b=t(e,"settings.json"),v=t(e,"claude-mem.db"),U=t(e,"vector-db"),M=t(s,"settings.json"),O=t(s,"commands"),L=t(s,"CLAUDE.md");function a(){try{let o=l.join(f(),".claude-mem","settings.json");if(g(o)){let n=JSON.parse(h(o,"utf-8")),r=parseInt(n.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(r))return r}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var D=i(p(),".claude","plugins","marketplaces","thedotmack"),k=i(D,"node_modules");x(k)||(console.error(`
|
||||
---
|
||||
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
|
||||
user messages in Claude Code UI until a better method is provided.
|
||||
@@ -17,12 +17,15 @@ Dependencies have been installed in the background. This only happens once.
|
||||
Thank you for installing Claude-Mem!
|
||||
|
||||
This message was not added to your startup context, so you can continue working as normal.
|
||||
`),process.exit(3));try{let e=r(s(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),t=p(`node "${e}" --colors`,{encoding:"utf8"}),o=n();console.error(`
|
||||
`),process.exit(3));try{let o=i(p(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),n=_(`node "${o}" --colors`,{encoding:"utf8"}),r=a();console.error(`
|
||||
|
||||
\u{1F4DD} Claude-Mem Context Loaded
|
||||
\u2139\uFE0F Note: This appears as stderr but is informational only
|
||||
|
||||
`+t+`
|
||||
`+n+`
|
||||
|
||||
\u{1F4FA} Watch live in browser http://localhost:${o}/ (New! v5.1)
|
||||
`)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
|
||||
\u{1F4AC} Feedback & Support
|
||||
https://github.com/thedotmack/claude-mem/discussions/110
|
||||
|
||||
\u{1F4FA} Watch live in browser http://localhost:${r}/
|
||||
`)}catch(o){console.error(`\u274C Failed to load context display: ${o}`)}process.exit(3);
|
||||
|
||||
@@ -1,196 +1,123 @@
|
||||
---
|
||||
name: mem-search
|
||||
description: Search claude-mem's persistent cross-session memory database to find work from previous conversations days, weeks, or months ago. Access past session summaries, bug fixes, feature implementations, and decisions that are NOT in the current conversation context. Use when user asks "did we already solve this?", "how did we do X last time?", "what happened in last week's session?", or needs information from previous sessions stored in the PM2-managed database. Searches observations, session summaries, and user prompts across entire project history.
|
||||
description: Search claude-mem's persistent cross-session memory database. Use when user asks "did we already solve this?", "how did we do X last time?", or needs work from previous sessions.
|
||||
---
|
||||
|
||||
# Memory Search
|
||||
|
||||
Access claude-mem's persistent cross-session memory through HTTP API. Find past work, understand context across sessions, and learn from previous decisions.
|
||||
Search past work across all sessions. Simple workflow: search → get IDs → fetch details by ID.
|
||||
|
||||
## When to Use This Skill
|
||||
## When to Use
|
||||
|
||||
**Use when users ask about work from PREVIOUS sessions** (not current conversation):
|
||||
Use when users ask about PREVIOUS sessions (not current conversation):
|
||||
- "Did we already fix this?"
|
||||
- "How did we solve X last time?"
|
||||
- "What happened last week?"
|
||||
|
||||
### Temporal Triggers (Key Indicators)
|
||||
- "Did we **already** fix this bug?" or "Have we seen this error **before**?"
|
||||
- "How did we solve X **last time**?" or "What approach did we take **previously**?"
|
||||
- "What did we do in **yesterday's/last week's/last month's** session?"
|
||||
- "**When** did we last work on this?" or "What's the **history** of this file?"
|
||||
## The Workflow
|
||||
|
||||
### Cross-Session Queries
|
||||
- "Show me all authentication-related changes **across all sessions**"
|
||||
- "What features did we add **last month**?" (not "today" or "this session")
|
||||
- "Why did we choose this approach **before**?" (decisions from past sessions)
|
||||
- "What files did we modify **when we added X**?" (historical context)
|
||||
**ALWAYS follow this exact flow:**
|
||||
|
||||
**Do NOT use** for current session work, future planning, or questions Claude can answer from current conversation context.
|
||||
1. **Search** - Get an index of results with IDs
|
||||
2. **Review** - Look at titles/dates, pick relevant IDs
|
||||
3. **Fetch** - Get full details ONLY for those IDs
|
||||
|
||||
## Common Trigger Phrases
|
||||
### Step 1: Search Everything
|
||||
|
||||
This skill activates when detecting phrases about **cross-session history**:
|
||||
```bash
|
||||
curl "http://localhost:37777/api/search?query=authentication&format=index&limit=5"
|
||||
```
|
||||
|
||||
- "Did we already solve this?" / "Have we done this before?"
|
||||
- "How did we implement X last time?"
|
||||
- "What did we work on yesterday/last week/last month?"
|
||||
- "Show me the history of [file/feature/decision]"
|
||||
- "When did we fix/add/change X?"
|
||||
- "What was happening around [date/time]?"
|
||||
- "Catch me up on recent sessions" / "What have we been doing?"
|
||||
- "What changes to [filename] across all sessions?"
|
||||
**Required parameters:**
|
||||
- `query` - Search term
|
||||
- `format=index` - ALWAYS start with index (lightweight)
|
||||
- `limit=5` - Start small (3-5 results)
|
||||
|
||||
**Unique identifiers:** claude-mem, persistent memory, cross-session database, session history, PM2-managed database
|
||||
**Returns:**
|
||||
```
|
||||
1. [feature] Added JWT authentication
|
||||
Date: 11/17/2025, 3:48:45 PM
|
||||
ID: 11131
|
||||
|
||||
## Available Operations
|
||||
2. [bugfix] Fixed auth token expiration
|
||||
Date: 11/16/2025, 2:15:22 PM
|
||||
ID: 10942
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
- **observations** - Search all observations by keyword (bugs, features, decisions, discoveries, changes)
|
||||
- Use when: "How did we implement X?" or "What bugs did we fix?"
|
||||
- Example: Search for "authentication JWT" to find auth-related work
|
||||
### Step 2: Pick IDs
|
||||
|
||||
- **sessions** - Search session summaries to find what was accomplished when
|
||||
- Use when: "What did we accomplish last time?" or "What was the goal of that session?"
|
||||
- Example: Find sessions where "added login feature"
|
||||
Review the index results. Identify which IDs are actually relevant. Discard the rest.
|
||||
|
||||
- **prompts** - Find what users have asked about in previous sessions
|
||||
- Use when: "Did I ask about this before?" or "What did I request last week?"
|
||||
- Example: Search for "database migration" in past user prompts
|
||||
### Step 3: Fetch by ID
|
||||
|
||||
### Filtered Search
|
||||
- **by-type** - Filter observations by type (bugfix, feature, refactor, decision, discovery, change)
|
||||
- Use when: "Show me all bug fixes" or "List features we added"
|
||||
- Example: Get all observations with type=bugfix from last month
|
||||
For each relevant ID, fetch full details:
|
||||
|
||||
- **by-concept** - Find observations tagged with specific concepts (problem-solution, how-it-works, gotcha)
|
||||
- Use when: "What patterns did we discover?" or "Show me gotchas"
|
||||
- Example: Find all observations tagged with concept "gotcha"
|
||||
```bash
|
||||
# Fetch observation
|
||||
curl "http://localhost:37777/api/observation/11131"
|
||||
|
||||
- **by-file** - Find all work related to a specific file path across all sessions
|
||||
- Use when: "What changes to auth.ts?" or "History of this file"
|
||||
- Example: Get all work related to "src/auth/login.ts"
|
||||
# Fetch session
|
||||
curl "http://localhost:37777/api/session/2005"
|
||||
|
||||
### Timeline & Context
|
||||
- **recent-context** - Get last N sessions with summaries and observations
|
||||
- Use when: "What's been happening?" or "Catch me up on recent work"
|
||||
- Example: Get last 3 sessions with limit=3
|
||||
# Fetch prompt
|
||||
curl "http://localhost:37777/api/prompt/5421"
|
||||
```
|
||||
|
||||
- **timeline** - Get chronological context around a specific point in time (before/after window)
|
||||
- Use when: "What was happening around [date]?" or "Show me context from that time"
|
||||
- Example: Timeline around session 123 with depth 5 before and after
|
||||
**ID formats:**
|
||||
- Observations: Just the number (11131)
|
||||
- Sessions: Just the number (2005) from "S2005"
|
||||
- Prompts: Just the number (5421)
|
||||
|
||||
- **timeline-by-query** - Search first, then get timeline context around best match
|
||||
- Use when: "When did we implement auth?" combined with "show me context around that time"
|
||||
- Example: Search for "OAuth implementation" then get surrounding timeline
|
||||
## Search Parameters
|
||||
|
||||
For detailed instructions on any operation, read the corresponding file in [operations/](operations/).
|
||||
**Basic:**
|
||||
- `query` - What to search for (required)
|
||||
- `format` - "index" or "full" (always use "index" first)
|
||||
- `limit` - How many results (default 5, max 100)
|
||||
|
||||
## Quick Decision Guide
|
||||
**Filters (optional):**
|
||||
- `type` - Filter to "observations", "sessions", or "prompts"
|
||||
- `project` - Filter by project name
|
||||
- `dateRange[start]` - Start date (YYYY-MM-DD)
|
||||
- `dateRange[end]` - End date (YYYY-MM-DD)
|
||||
- `obs_type` - Filter observations by: bugfix, feature, decision, discovery, change
|
||||
|
||||
**What is the user asking about?**
|
||||
## Examples
|
||||
|
||||
1. **Recent work** (last 3-5 sessions) → Use **recent-context** with limit=3-5
|
||||
2. **Specific topic/keyword** (bugs, features, decisions) → Use **observations** search
|
||||
3. **Specific file history** (changes to a file) → Use **by-file** search
|
||||
4. **Timeline/chronology** (what happened when) → Use **timeline** or **timeline-by-query**
|
||||
5. **Type-specific** (all bug fixes, all features) → Use **by-type** filter
|
||||
**Find recent bug fixes:**
|
||||
```bash
|
||||
curl "http://localhost:37777/api/search?query=bug&type=observations&obs_type=bugfix&format=index&limit=5"
|
||||
```
|
||||
|
||||
**Most common:** Use **observations** search for general "how did we..." questions.
|
||||
**Find what happened last week:**
|
||||
```bash
|
||||
curl "http://localhost:37777/api/search?query=&type=observations&dateRange[start]=2025-11-11&format=index&limit=10"
|
||||
```
|
||||
|
||||
## Progressive Disclosure Workflow (Token Efficiency)
|
||||
**Search everything:**
|
||||
```bash
|
||||
curl "http://localhost:37777/api/search?query=database+migration&format=index&limit=5"
|
||||
```
|
||||
|
||||
**Core Principle**: Find high-signal items in index format FIRST (~50-100 tokens each), then request full details ONLY for relevant items (~500-1000 tokens each).
|
||||
## Why This Workflow?
|
||||
|
||||
**4-Step Workflow:**
|
||||
**Token efficiency:**
|
||||
- Index format: ~50-100 tokens per result
|
||||
- Full format: ~500-1000 tokens per result
|
||||
- **10x difference** - only fetch full when you know it's relevant
|
||||
|
||||
1. **Start with Index Format**
|
||||
- Always use `format=index` initially
|
||||
- Set `limit=3-5` (not 10-20)
|
||||
- Review titles and dates to assess relevance
|
||||
- Token cost: ~50-100 per result
|
||||
|
||||
2. **Identify Relevant Items**
|
||||
- Scan index results
|
||||
- Discard irrelevant items from list
|
||||
- Keep only 1-3 most relevant
|
||||
|
||||
3. **Request Full Details Selectively**
|
||||
- Use `format=full` ONLY for specific relevant items
|
||||
- Token cost: ~500-1000 per result
|
||||
- **10x cost difference** - be selective
|
||||
|
||||
4. **Refine with Filters**
|
||||
- Use type, dateRange, concepts, files filters
|
||||
- Paginate with offset if needed
|
||||
- Narrow scope before expanding limits
|
||||
|
||||
**DO:**
|
||||
- ✅ Start with `format=index` and `limit=3-5`
|
||||
- ✅ Use filters (type, dateRange, concepts, files) to narrow results
|
||||
- ✅ Request `format=full` ONLY for specific relevant items
|
||||
- ✅ Use offset for pagination instead of large limits
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Jump straight to `format=full`
|
||||
- ❌ Request `limit=20` without assessing index results first
|
||||
- ❌ Load full details for all results upfront
|
||||
- ❌ Skip index format to "save a step" (costs 10x more tokens)
|
||||
|
||||
See [principles/progressive-disclosure.md](principles/progressive-disclosure.md) for complete workflow with examples and token calculations.
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Need | Operation | Key Parameters |
|
||||
|------|-----------|----------------|
|
||||
| Recent context | recent-context | limit=3-5 |
|
||||
| Search observations | observations | query, format=index, limit=5 |
|
||||
| Search sessions | sessions | query, format=index, limit=5 |
|
||||
| Find by type | by-type | type=(bugfix\|feature\|decision), format=index |
|
||||
| Find by file | by-file | filePath, format=index |
|
||||
| Timeline around event | timeline | anchor=(sessionDbId), depth_before=5, depth_after=5 |
|
||||
| Search + timeline | timeline-by-query | query, mode=auto |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
For step-by-step guides on typical user requests, see [operations/common-workflows.md](operations/common-workflows.md):
|
||||
- Understanding past work from previous sessions
|
||||
- Finding specific bug fixes from history
|
||||
- Understanding file history across sessions
|
||||
- Timeline investigation workflows
|
||||
- Search composition patterns
|
||||
|
||||
## Response Formatting
|
||||
|
||||
For guidelines on presenting search results to users, see [operations/formatting.md](operations/formatting.md):
|
||||
- Index format responses (compact lists with titles/dates)
|
||||
- Full format responses (complete observation details)
|
||||
- Timeline responses (chronologically grouped)
|
||||
- Error handling and user-friendly messages
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- **Port:** Default 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- **Response format:** Always JSON
|
||||
- **Search engine:** FTS5 full-text search + structured filters
|
||||
- **All operations:** HTTP GET with query parameters
|
||||
- **Worker:** PM2-managed background process
|
||||
**Clarity:**
|
||||
- See everything first
|
||||
- Pick what matters
|
||||
- Get details only for what you need
|
||||
|
||||
## Error Handling
|
||||
|
||||
If HTTP request fails:
|
||||
1. Inform user the claude-mem search service isn't available
|
||||
2. Suggest checking if worker is running: `pm2 list` or `pm2 status claude-mem-worker`
|
||||
3. Offer to help troubleshoot using the troubleshoot skill
|
||||
|
||||
## Resources
|
||||
|
||||
**Principles:**
|
||||
- [principles/progressive-disclosure.md](principles/progressive-disclosure.md) - Complete 4-step workflow with token calculations
|
||||
- [principles/anti-patterns.md](principles/anti-patterns.md) - 5 anti-patterns to avoid with LLM behavior insights
|
||||
|
||||
**Operations:**
|
||||
- [operations/](operations/) - Detailed instructions for each operation (9 operations + help)
|
||||
- [operations/common-workflows.md](operations/common-workflows.md) - Step-by-step workflow guides
|
||||
- [operations/formatting.md](operations/formatting.md) - Response formatting templates
|
||||
If search fails, tell the user the worker isn't available and suggest:
|
||||
```bash
|
||||
pm2 list # Check if worker is running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Remember:** This skill searches **cross-session persistent memory**, NOT current conversation. Start with index format for token efficiency. Use temporal triggers to differentiate from native Claude memory.
|
||||
**Remember:** ALWAYS search with format=index first. ALWAYS fetch by ID for details. The IDs are there for a reason - USE THEM.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Search Observations (Full-Text)
|
||||
# Search Observations (Semantic + Full-Text Hybrid)
|
||||
|
||||
Search all observations using natural language queries.
|
||||
|
||||
@@ -17,10 +17,16 @@ curl -s "http://localhost:37777/api/search/observations?query=authentication&for
|
||||
|
||||
## Parameters
|
||||
|
||||
- **query** (required): Search terms (e.g., "authentication", "bug fix", "database migration")
|
||||
- **query** (optional): Natural language search query - uses semantic search (ChromaDB) for ranking with SQLite FTS5 fallback (e.g., "authentication", "bug fix", "database migration"). Can be omitted for filter-only searches.
|
||||
- **format**: "index" (summary) or "full" (complete details). Default: "full"
|
||||
- **limit**: Number of results (default: 20, max: 100)
|
||||
- **project**: Filter by project name (optional)
|
||||
- **dateRange**: Filter by date range (optional) - `dateRange[start]` and/or `dateRange[end]`
|
||||
- **obs_type**: Filter by observation type: bugfix, feature, refactor, decision, discovery, change (optional)
|
||||
- **concepts**: Filter by concept tags (optional)
|
||||
- **files**: Filter by file paths (optional)
|
||||
|
||||
**Important**: When omitting `query`, you MUST provide at least one filter (project, dateRange, obs_type, concepts, or files)
|
||||
|
||||
## When to Use Each Format
|
||||
|
||||
@@ -76,13 +82,28 @@ Found 5 results for "authentication":
|
||||
|
||||
For complete formatting guidelines, see formatting.md (documentation coming soon).
|
||||
|
||||
## Filter-Only Examples
|
||||
|
||||
Search without query text (direct SQLite filtering):
|
||||
|
||||
```bash
|
||||
# Get all observations from November 2025
|
||||
curl -s "http://localhost:37777/api/search?type=observations&dateRange[start]=2025-11-01&format=index"
|
||||
|
||||
# Get all bug fixes from a specific project
|
||||
curl -s "http://localhost:37777/api/search?type=observations&obs_type=bugfix&project=api-server&format=index"
|
||||
|
||||
# Get all observations from last 7 days
|
||||
curl -s "http://localhost:37777/api/search?type=observations&dateRange[start]=2025-11-11&format=index"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Missing query parameter:**
|
||||
**Missing query and filters:**
|
||||
```json
|
||||
{"error": "Missing required parameter: query"}
|
||||
{"error": "Either query or filters required for search"}
|
||||
```
|
||||
Fix: Add the query parameter
|
||||
Fix: Provide either a query parameter OR at least one filter (project, dateRange, obs_type, concepts, files)
|
||||
|
||||
**No results found:**
|
||||
```json
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="328" height="327" viewBox="0 0 328 327" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-completed">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 102.143021 15.109985 C 124.399521 6.597839 146.838608 0.814148 170.500641 1.142517 C 178.716675 1.256592 184.928009 4.778809 188.221222 12.564148 C 192.131836 21.809204 187.675385 32.968201 178.308319 36.536194 C 174.499176 37.987122 170.210297 39.14447 166.207672 38.960144 C 145.084 37.987 126.251495 45.816895 108.085022 54.783447 C 101.013763 58.273621 94.376038 64.903625 90.303864 71.747437 C 81.249969 86.963745 68.683197 98.688721 56.224823 110.687927 C 45.194839 121.311401 37.752762 133.868042 36.270981 149.298157 C 35.333755 159.057739 38.918953 168.018066 43.269821 176.487732 C 48.642563 186.946594 49.607574 198.020325 49.348831 209.462341 C 49.229034 214.759338 49.297974 220.136719 50.076202 225.359192 C 51.13385 232.456848 55.144928 237.91803 61.378815 241.596008 C 63.383698 242.778992 65.430359 243.940247 67.574722 244.829956 C 83.763855 251.546753 97.238754 261.650269 108.032684 275.601196 C 114.748367 284.281067 123.325409 290.831177 135.135101 289.901917 C 140.699997 289.464111 146.271561 287.914001 151.622803 286.192444 C 165.21907 281.818298 178.989075 280.611023 193.001343 283.258057 C 210.095184 286.487305 222.947113 280.610168 232.088593 265.940796 C 239.656372 253.796753 250.168091 244.837585 262.412537 237.534668 C 267.629059 234.42334 272.271271 230.168091 276.676208 225.916443 C 284.850586 218.026672 283.26178 207.977478 281.815948 198.196411 C 280.216736 187.377869 282.084747 177.171692 285.330505 166.805542 C 287.769135 159.01709 289.746948 150.571228 289.354858 142.543823 C 288.898254 133.196045 281.958679 126.555603 274.813629 120.722412 C 262.907257 111.002075 253.400421 99.691772 251.609131 83.625122 C 251.074951 78.833923 250.541107 74.030701 250.329742 69.21875 C 250.08316 63.604675 247.759796 59.19043 243.079437 56.287903 C 238.852234 53.666321 234.389679 51.398193 229.921539 49.196228 C 216.848724 42.753784 213.950043 27.349976 224.305084 18.449036 C 229.938202 13.606995 236.862122 13.132141 243.201904 15.625 C 262.293976 23.132446 277.067566 35.448792 283.134338 55.910461 C 284.725006 61.275391 285.178131 67.024841 285.728149 72.639404 C 286.461243 80.122681 289.553528 86.273743 294.768616 91.602539 C 301.394287 98.372742 308.7612 104.675049 314.152252 112.343567 C 326.11319 129.357178 329.543671 148.192627 322.225342 168.220764 C 318.505066 178.402039 316.802551 188.827332 317.877289 199.604309 C 320.515808 226.062927 309.154083 246.001892 289.051666 261.865967 C 283.568481 266.193054 277.323334 269.67511 272.349426 274.495911 C 266.423248 280.239441 261.273407 286.837769 256.165253 293.361694 C 240.566223 313.284546 220.852509 323.559692 195.203339 318.922852 C 181.849915 316.508789 169.312592 318.014526 156.549927 322.340332 C 128.791275 331.749023 105.282944 322.742676 84.499405 304.061523 C 77.946136 298.171021 72.037766 291.539246 65.29071 285.896729 C 61.008255 282.315308 55.838928 279.71698 50.867279 277.050842 C 31.686432 266.764404 18.30217 251.817322 13.808723 230.139771 C 12.550957 224.072021 12.645752 217.592896 13.001595 211.352783 C 13.513565 202.374756 12.749832 193.795959 9.017151 185.506409 C 4.141502 174.678528 0.361565 163.461487 0.147591 151.46405 C -0.268478 128.135742 8.54351 108.351929 24.183197 91.418823 C 28.588943 86.648743 33.638489 82.456482 37.878265 77.55426 C 43.191925 71.4104 48.86821 65.311707 52.818863 58.30835 C 64.201996 38.129456 80.781326 24.276245 102.143021 15.109985 Z"/>
|
||||
<path id="path1" fill="#ee9443" stroke="none" d="M 177.816132 152.815186 C 191.282013 135.84552 204.424072 119.072571 217.830627 102.513794 C 222.283875 97.013489 228.385834 94.391418 235.566132 95.916077 C 243.398834 97.579224 248.658691 102.725464 249.203705 110.510254 C 249.534729 115.238892 248.017639 120.987549 245.313232 124.874084 C 236.577667 137.427856 227.077362 149.466003 217.560394 161.455688 C 201.420685 181.788879 185.291229 202.139771 168.680969 222.086487 C 157.115097 235.975647 140.902039 236.160095 129.093536 222.542542 C 123.268402 215.825012 118.55864 208.046204 113.969971 200.373413 C 107.730713 189.940674 101.93927 179.349426 93.154266 170.614319 C 84.420303 161.930115 82.955887 151.667053 88.21373 143.463806 C 94.485153 133.679199 108.943008 130.570801 117.990814 138.286194 C 123.506119 142.989258 127.616531 149.584167 131.544327 155.840088 C 136.043503 163.006226 139.188934 171.028992 143.759659 178.141602 C 148.237061 185.108887 151.258911 185.276123 156.6017 179.125671 C 163.907013 170.715942 170.625488 161.796448 177.816132 152.815186 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="295" height="339" viewBox="0 0 295 339" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-investigated">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 229.062531 225.062622 C 215.534576 231.309692 201.815216 234.810913 187.17041 233.883606 C 178.072418 233.307495 169.25061 234.487427 160.747803 237.888306 C 145.74324 243.889832 130.520584 242.407349 115.103485 239.659485 C 109.520187 238.664368 103.463379 238.559448 97.913162 239.599182 C 91.193512 240.858032 89.857742 246.409546 94.159653 251.608093 C 108.427109 268.849121 103.655029 289.726685 82.846451 298.619263 C 75.620636 301.707153 67.625809 303.104218 59.888016 304.85791 C 50.282639 307.034882 42.764755 311.295197 38.593872 320.941223 C 36.690849 325.342377 33.301422 329.377228 29.783783 332.738495 C 24.606377 337.685913 18.155121 338.136719 11.659698 335.23761 C 5.209885 332.358826 1.559677 327.350647 0.947311 320.210785 C 0.345543 313.194427 1.801857 306.664551 5.316139 300.567902 C 15.429329 283.023499 30.555946 273.203247 50.673309 270.968628 C 53.301224 270.676636 55.950958 270.209106 58.475998 269.444458 C 62.971725 268.08313 63.602158 266.009277 60.836716 262.152527 C 58.420456 258.782776 55.391068 255.764587 53.461945 252.14856 C 44.128372 234.653442 57.2845 217.667847 72.544739 212.954956 C 79.150009 210.915039 86.189026 210.282532 93.028107 208.995117 C 95.427124 208.543579 97.817413 208.045532 101.645859 207.283081 C 99.327225 204.565735 97.793701 202.596069 96.082932 200.794678 C 90.028107 194.41925 83.471466 188.463745 77.908737 181.689087 C 64.844833 165.779236 59.591553 146.643127 58.248764 126.666382 C 56.229111 96.620117 63.289093 68.849243 82.57254 44.979126 C 101.736938 21.256409 126.399551 6.920532 156.699341 3.060181 C 182.104431 -0.176514 207.001221 1.754333 230.040924 14.175964 C 263.505798 32.21814 284.351563 60.22937 290.406555 97.555054 C 294.291138 121.501221 294.880157 146.098694 284.244659 169.16748 C 272.668884 194.275696 254.271667 212.81134 229.062531 225.062622 M 244.872559 167.361755 C 246.057373 165.964722 247.295471 164.60907 248.418304 163.163879 C 255.00592 154.684448 259.361908 145.55304 258.956238 134.328857 C 258.616425 124.927002 259.09082 115.463867 259.744476 106.06543 C 260.732086 91.865112 257.144501 79.826782 245.442902 70.771606 C 239.933441 66.508179 235.032104 61.463989 229.57312 57.128784 C 223.865021 52.595825 218.260376 47.610107 211.829315 44.375488 C 197.484528 37.160461 182.032745 35.63031 166.134003 37.822998 C 133.72908 42.292175 111.445908 59.614502 99.478043 90.057617 C 88.486908 118.016174 92.658325 152.151917 119.026199 172.237122 C 128.423401 179.395264 135.325684 188.393066 138.320084 199.993469 C 139.583832 204.889343 142.883499 205.848633 147.055222 204.817383 C 150.593048 203.942871 153.99028 202.495239 157.443268 201.282898 C 166.797211 197.998596 176.204132 196.039429 186.293915 197.625366 C 200.123383 199.799133 212.669403 197.077393 223.127808 186.812866 C 229.893097 180.172974 237.269012 174.155273 244.872559 167.361755 Z"/>
|
||||
<path id="path1" fill="#ee9544" stroke="none" d="M 194.897339 64.336914 C 206.583221 62.468262 215.635437 67.409302 224.302734 73.749695 C 238.771729 84.334229 245.725098 99.028931 246.693542 116.427734 C 247.490143 130.739807 244.072723 144.22644 234.096741 155.157959 C 231.991882 157.464417 228.721863 159.032837 225.692261 160.098389 C 220.342163 161.980103 215.459534 160.512878 211.675232 156.238647 C 208.011444 152.100525 207.832611 147.407654 209.515137 142.262695 C 211.309143 136.776733 213.460236 131.214111 213.99173 125.547668 C 215.227661 112.370483 209.723846 103.750244 197.700653 98.094055 C 196.345306 97.456421 195.014587 96.766052 193.65506 96.137695 C 186.781982 92.961365 182.817017 87.529785 183.136108 80.035217 C 183.449127 72.68335 187.378235 67.224609 194.897339 64.336914 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="353" height="364" viewBox="0 0 353 364" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-learned">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 271.450684 172.584045 C 273.343445 194.098389 270.338623 213.959534 256.262817 230.760376 C 252.104553 235.723694 247.645813 240.591919 242.633789 244.650269 C 232.838013 252.582214 229.652893 262.937012 229.050354 274.968018 C 228.661987 282.721924 226.516724 290.377747 225.94397 298.135498 C 225.449341 304.836121 225.212036 311.767395 226.361206 318.334229 C 228.871155 332.677612 225.07959 343.24469 212.557495 350.703888 C 207.639648 353.633392 202.04071 356.192383 196.458435 357.085266 C 181.075195 359.545837 165.563293 361.335999 150.04187 362.707977 C 145.997742 363.06546 141.407715 361.596954 137.642151 359.778809 C 131.365173 356.747955 128.71936 350.93808 129.12146 344.070404 C 129.577759 336.276611 133.243103 330.086884 140.756226 327.561218 C 146.495178 325.631989 152.669922 324.627563 158.727783 324.105865 C 167.84021 323.321045 177.024475 323.407349 186.169434 322.949463 C 191.91217 322.661987 195.344666 320.123352 194.87439 316.190308 C 194.651306 314.324951 192.947021 311.528442 191.41864 311.149231 C 187.805725 310.252808 183.879272 310.225464 180.109924 310.423889 C 164.817383 311.229126 149.53717 312.139893 134.361572 309.125916 C 126.080383 307.481201 121.892212 303.087891 122.043091 294.73584 C 122.186279 286.804382 123.350037 278.892029 124.047974 270.969788 C 125.085876 259.190063 119.776184 250.632141 110.74762 243.601074 C 87.049316 225.145813 79.709412 200.221619 82.73822 171.335754 C 84.429932 155.201782 90.26416 140.769165 100.286255 127.994385 C 116.374084 107.487671 136.94043 94.488403 162.88501 90.460144 C 210.062683 83.135254 254.294556 110.859192 268.11322 156.55188 C 269.643311 161.611084 270.288879 166.937805 271.450684 172.584045 M 229.007874 152.54834 C 226.673096 149.528137 224.618286 146.232605 221.960144 143.530518 C 196.655273 117.807373 141.948364 118.612122 121.780273 161.978516 C 113.200989 180.426147 114.518372 198.947449 131.426208 214.280396 C 135.984802 218.414429 140.783691 222.509155 144.377563 227.422607 C 148.632446 233.239502 152.880615 239.557251 154.981812 246.325378 C 157.495972 254.423828 157.874573 263.211731 158.882996 271.735962 C 159.210083 274.501221 159.49585 276.862671 162.831787 276.897949 C 170.791077 276.981995 178.7771 277.295837 186.704102 276.778931 C 192.626343 276.3927 193.906555 274.412476 194.184692 268.340576 C 195.02771 249.935364 201.677063 234.233765 215.025269 221.240723 C 221.435242 215.001343 228.310364 208.625122 232.63623 200.98645 C 241.793823 184.815918 238.45636 168.581177 229.007874 152.54834 Z"/>
|
||||
<path id="path1" fill="#ee9544" stroke="none" d="M 158.952759 45.972473 C 158.945435 36.99884 158.837341 28.518555 158.958923 20.041565 C 159.11438 9.20166 166.629822 1.291138 176.675232 1.163452 C 187.241089 1.029175 194.853088 8.555664 195.05896 19.830017 C 195.268127 31.288025 195.33667 42.761047 194.987427 54.212891 C 194.707092 63.401917 187.224182 69.711426 177.172668 69.998657 C 167.616699 70.271729 160.188049 64.1026 159.223083 54.933838 C 158.928345 52.13324 159.032532 49.290649 158.952759 45.972473 Z"/>
|
||||
<path id="path2" fill="#ee9545" stroke="none" d="M 240.151184 71.347107 C 246.59021 62.467346 252.821167 53.886292 259.300049 45.496765 C 262.225586 41.708618 265.23053 37.781555 268.930237 34.835205 C 275.682922 29.457458 285.254028 30.138916 290.921997 35.658813 C 297.144104 41.718323 298.258545 50.611267 292.924316 57.851807 C 285.630554 67.752258 278.067322 77.481018 270.14032 86.879028 C 264.368225 93.722168 255.518311 94.755554 247.930908 90.422729 C 241.237549 86.600525 238.141602 79.260254 240.151184 71.347107 Z"/>
|
||||
<path id="path3" fill="#ee9545" stroke="none" d="M 94.416748 92.876099 C 90.227112 90.332947 85.729736 88.465088 82.824646 85.186401 C 75.340088 76.739258 68.420776 67.77478 61.542114 58.812439 C 55.871338 51.42395 56.928345 40.84552 63.497803 35.31958 C 70.645935 29.306885 81.690857 29.472839 87.88501 36.621399 C 96.041443 46.034546 103.694641 55.912842 111.109131 65.92804 C 115.180664 71.427734 115.128906 77.757874 111.843018 83.856567 C 108.197205 90.623291 102.277527 93.223328 94.416748 92.876099 Z"/>
|
||||
<path id="path4" fill="#ee9545" stroke="none" d="M 327.237671 111.158447 C 339.058167 106.002991 350.032959 112.776855 351.524292 123.386963 C 352.453918 130.000122 348.8396 138.183655 342.590576 140.844788 C 331.583923 145.53186 320.502625 150.078064 309.278198 154.209351 C 298.968567 158.004028 290.227539 154.146057 286.725098 144.919678 C 283.136841 135.467163 287.600891 126.236633 297.930237 122.201233 C 307.530823 118.450562 317.217163 114.919128 327.237671 111.158447 Z"/>
|
||||
<path id="path5" fill="#ee9545" stroke="none" d="M 60.526978 150.6521 C 54.4599 155.095764 48.345764 154.328491 42.21698 151.939087 C 32.62677 148.200134 23.100098 144.298462 13.517639 140.539246 C 3.66449 136.673767 -0.680115 127.512268 2.923706 118.00708 C 6.397583 108.844604 15.713013 104.776245 25.429688 108.514954 C 36.124695 112.630127 46.721008 117.005798 57.320618 121.364258 C 68.805969 126.086914 70.452515 143.709595 60.526978 150.6521 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="357" height="313" viewBox="0 0 357 313" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-next-steps">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 200.028198 247.999878 C 203.073303 243.603943 206.331726 239.728516 208.700806 235.36969 C 218.712036 216.950195 234.256958 205.057983 253.007629 196.675293 C 261.606873 192.830933 269.038208 187.471008 275.130615 180.018494 C 279.957092 174.114502 285.902893 169.137756 291.181824 163.587646 C 295.72467 158.811523 295.22168 156.284851 290.264404 152.069214 C 283.299316 146.146301 277.047729 139.305542 269.71405 133.914185 C 259.545471 126.438721 248.661316 119.935608 238.07312 113.032043 C 225.191956 104.633545 215.358826 93.802551 209.51947 79.301819 C 205.608276 69.589478 198.034119 62.773132 189.394409 57.04071 C 183.433105 53.085449 177.413147 49.111145 171.991882 44.475891 C 164.665039 38.211365 161.635071 30.016052 163.611877 20.313538 C 165.687683 10.125061 172.308899 4.53772 182.131592 2.495544 C 188.943298 1.079346 195.326477 3.127258 200.339233 7.435852 C 217.818848 22.459961 236.578552 36.260437 246.636597 58.263916 C 252.342773 70.746948 262.274841 79.434937 274.695679 85.635864 C 288.199707 92.377563 299.427551 101.974426 309.528381 113.447876 C 316.54425 121.417175 326.085815 127.183044 334.598694 133.805176 C 338.40448 136.765747 342.611755 139.21698 346.364075 142.238159 C 356.84729 150.679016 358.031616 163.997253 348.114685 173.076172 C 340.322327 180.209961 331.37793 186.165588 322.568237 192.083252 C 315.148804 197.066956 308.533325 202.696533 303.18103 209.862244 C 296.429993 218.900635 287.445496 225.226379 277.306519 229.781006 C 259.982971 237.562988 247.425415 250.171631 237.15509 265.963928 C 226.294312 282.664246 212.651855 297.092407 196.101624 308.432434 C 189.276978 313.108704 181.674683 314.306396 173.818054 310.920776 C 161.332764 305.540527 157.06665 286.928406 166.708801 276.03363 C 172.108887 269.932007 179.637939 265.744568 186.038635 260.48877 C 190.777893 256.597168 195.244507 252.373657 200.028198 247.999878 Z"/>
|
||||
<path id="path1" fill="#ee9443" stroke="none" d="M 182.439697 114.401611 C 192.385376 116.187073 200.552246 120.709717 208.724854 125.747681 C 218.74353 131.923584 229.402832 137.083313 239.908691 142.440063 C 251.755737 148.480591 255.919739 160.852478 249.511841 171.751831 C 245.387085 178.7677 234.322571 182.39679 224.101135 180.377991 C 214.380859 178.458069 206.876282 172.931091 199.753052 166.476563 C 195.467896 162.593628 191.099548 158.587891 186.145752 155.709106 C 179.250549 151.702148 175.2099 152.796387 169.784729 158.514954 C 163.854492 164.765686 158.470825 171.658264 151.925659 177.175903 C 139.299438 187.819885 121.911133 184.144409 111.615967 174.971741 C 107.548035 171.347229 104.287842 166.814453 100.674744 162.682129 C 99.471436 161.305847 98.377991 159.829102 97.126526 158.499878 C 88.845886 149.704407 80.265747 149.49585 71.393799 157.672119 C 66.258667 162.40448 61.163147 167.222351 55.667725 171.512207 C 45.10083 179.76123 32.94519 183.092163 19.60321 181.742432 C 10.085205 180.779602 2.704285 174.59082 0.872681 166.334473 C -1.18573 157.055786 1.656006 148.790833 8.964783 144.524475 C 11.899231 142.811523 15.435059 141.362427 18.758667 141.219116 C 31.253113 140.680237 40.451477 134.163147 49.517212 126.429016 C 55.239807 121.546997 61.704163 116.61322 68.694397 114.319214 C 86.340393 108.528625 102.878235 111.76001 117.033264 124.4021 C 120.881348 127.838867 124.439758 131.600647 128.299622 135.023376 C 133.139954 139.31543 136.620483 139.065918 141.27771 134.542419 C 144.860046 131.062988 148.452271 127.588074 152.164551 124.249634 C 160.650452 116.618225 170.508057 112.989624 182.439697 114.401611 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -19,7 +19,7 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-secondary: #efebe4;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
@@ -30,6 +30,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-observation: #f0f6fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
@@ -43,17 +44,20 @@
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
--color-border-observation: #0969da;
|
||||
--color-border-observation-hover: #0550ae;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-primary: #2b2520;
|
||||
--color-text-secondary: #5a5248;
|
||||
--color-text-tertiary: #726b5f;
|
||||
--color-text-muted: #8f8a7e;
|
||||
--color-text-header: #2b2520;
|
||||
--color-text-title: #2b2520;
|
||||
--color-text-subtitle: #5a5248;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
--color-text-observation: #2b2520;
|
||||
--color-text-logo: #2b2520;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
@@ -61,6 +65,7 @@
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
--color-accent-observation: #0550ae;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
@@ -68,6 +73,8 @@
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
--color-observation-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-observation-badge-text: #0550ae;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
@@ -77,59 +84,66 @@
|
||||
|
||||
/* Theme Variables - Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-primary: #1a1916;
|
||||
--color-bg-secondary: #252320;
|
||||
--color-bg-tertiary: #1f1d1a;
|
||||
--color-bg-header: #1f1d1a;
|
||||
--color-bg-card: #252320;
|
||||
--color-bg-card-hover: #2d2a26;
|
||||
--color-bg-input: #252320;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
--color-bg-summary: #2a2724;
|
||||
--color-bg-prompt: #262033;
|
||||
--color-bg-observation: #1a2332;
|
||||
--color-bg-stat: #252320;
|
||||
--color-bg-scrollbar-track: #1a1916;
|
||||
--color-bg-scrollbar-thumb: #3a3834;
|
||||
--color-bg-scrollbar-thumb-hover: #4a4540;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-primary: #3a3834;
|
||||
--color-border-secondary: #3a3834;
|
||||
--color-border-hover: #4a4540;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
--color-border-summary: #7a6a50;
|
||||
--color-border-summary-hover: #8b7960;
|
||||
--color-border-prompt: #6e5b9e;
|
||||
--color-border-prompt-hover: #7e6bae;
|
||||
--color-border-observation: #527aa0;
|
||||
--color-border-observation-hover: #6a8eb8;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-primary: #dcd6cc;
|
||||
--color-text-secondary: #b8b0a4;
|
||||
--color-text-tertiary: #938a7e;
|
||||
--color-text-muted: #7a7266;
|
||||
--color-text-header: #e8e2d8;
|
||||
--color-text-title: #e8e2d8;
|
||||
--color-text-subtitle: #b8b0a4;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
--color-text-summary: #d4b888;
|
||||
--color-text-observation: #a8b8c8;
|
||||
--color-text-logo: #e0dad0;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
--color-accent-summary: #d4b888;
|
||||
--color-accent-prompt: #8e7cbc;
|
||||
--color-accent-observation: #79b8ff;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
--color-summary-badge-bg: rgba(212, 184, 136, 0.15);
|
||||
--color-summary-badge-text: #d4b888;
|
||||
--color-prompt-badge-bg: rgba(142, 124, 188, 0.15);
|
||||
--color-prompt-badge-text: #9e8ccc;
|
||||
--color-observation-badge-bg: rgba(121, 184, 255, 0.15);
|
||||
--color-observation-badge-text: #79b8ff;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
--color-skeleton-base: #3a3834;
|
||||
--color-skeleton-highlight: #4a4540;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
@@ -149,6 +163,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-observation: #f0f6fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
@@ -162,6 +177,8 @@
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
--color-border-observation: #0969da;
|
||||
--color-border-observation-hover: #0550ae;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
@@ -172,6 +189,7 @@
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-observation: #24292f;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
@@ -180,6 +198,7 @@
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
--color-accent-observation: #0550ae;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
@@ -187,6 +206,8 @@
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
--color-observation-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-observation-badge-text: #0550ae;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
@@ -209,6 +230,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-observation: #1a2332;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
@@ -222,6 +244,8 @@
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
--color-border-observation: #527aa0;
|
||||
--color-border-observation-hover: #6a8eb8;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
@@ -232,6 +256,7 @@
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-observation: #a8b8c8;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
@@ -240,6 +265,7 @@
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
--color-accent-observation: #79b8ff;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
@@ -247,6 +273,8 @@
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
--color-observation-badge-bg: rgba(121, 184, 255, 0.15);
|
||||
--color-observation-badge-text: #79b8ff;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
@@ -348,6 +376,36 @@
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.queue-bubble {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: var(--color-accent-primary);
|
||||
color: var(--color-text-button);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
font-weight: 100;
|
||||
@@ -521,6 +579,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 10%;
|
||||
}
|
||||
|
||||
.card-subheading-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.card-subheading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
@@ -623,17 +699,31 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
.card-section {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-subtitle);
|
||||
line-height: 1.7;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-subtitle:last-child {
|
||||
.card-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-section pre {
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
/* word-wrap: break-word; */
|
||||
}
|
||||
|
||||
/*
|
||||
.card-section h4 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-title);
|
||||
font-weight: 500;
|
||||
} */
|
||||
|
||||
.card-meta {
|
||||
font-size: 11px;
|
||||
@@ -668,8 +758,7 @@
|
||||
|
||||
|
||||
/* Stack single column on narrow screens (removed - no longer using card-files) */
|
||||
@media (max-width: 600px) {
|
||||
}
|
||||
@media (max-width: 600px) {}
|
||||
|
||||
|
||||
/* Project badge styling */
|
||||
@@ -695,6 +784,176 @@
|
||||
color: var(--color-text-summary);
|
||||
}
|
||||
|
||||
/* Enhanced Summary Card Styles - Editorial/Archival Aesthetic */
|
||||
.summary-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.summary-card-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-border-summary);
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
|
||||
.summary-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-badge {
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.summary-project-badge {
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 400;
|
||||
padding: 3px 8px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .summary-project-badge {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-summary);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.summary-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
animation: summaryFadeIn 0.4s ease-out backwards;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes summaryFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-section-icon {
|
||||
position: relative;
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-section-icon--investigated {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.summary-section-icon--learned {
|
||||
height: 18px;
|
||||
left: -1px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.summary-section-icon--completed {
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.summary-section-icon--next_steps {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.summary-section-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-summary);
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.summary-section-content {
|
||||
margin-left: 26px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.summary-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border-primary);
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.summary-meta-id {
|
||||
font-weight: 500;
|
||||
color: var(--color-accent-summary);
|
||||
}
|
||||
|
||||
.summary-meta-divider {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.summary-meta-date {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for summary cards */
|
||||
@media (max-width: 600px) {
|
||||
.summary-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.summary-section-content {
|
||||
margin-left: 0;
|
||||
padding-left: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary-section-header {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
@@ -797,6 +1056,21 @@
|
||||
color: var(--color-prompt-badge-text);
|
||||
}
|
||||
|
||||
.observation-card {
|
||||
border-color: var(--color-border-observation);
|
||||
background: var(--color-bg-observation);
|
||||
color: var(--color-text-observation);
|
||||
}
|
||||
|
||||
.observation-card:hover {
|
||||
border-color: var(--color-border-observation-hover);
|
||||
}
|
||||
|
||||
.observation-card .card-type {
|
||||
background: var(--color-observation-badge-bg);
|
||||
color: var(--color-observation-badge-text);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 12px;
|
||||
@@ -867,6 +1141,7 @@
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
@@ -909,6 +1184,7 @@
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Postmortem: Worker Debug Failure - 2025-11-17
|
||||
|
||||
## Incident Summary
|
||||
Attempted to fix broken worker service. Worker was in crash loop with 225 restarts, failing with "MCP error -32000: Connection closed". Debug attempt failed and changes were reverted.
|
||||
|
||||
## What Went Wrong
|
||||
|
||||
### 1. **Jumped to Symptoms, Not Root Cause**
|
||||
- Saw "MCP connection failed" errors in logs
|
||||
- Immediately focused on MCP/Chroma connection code
|
||||
- Never asked: "Why is this suddenly broken when it worked before?"
|
||||
- Classic symptom chasing instead of root cause analysis
|
||||
|
||||
### 2. **Ignored the Build Pipeline**
|
||||
- Worker file wasn't in the expected location (`plugin/worker-service.cjs` vs `plugin/scripts/worker-service.cjs`)
|
||||
- Build output existed but search server was producing corrupted/error output
|
||||
- Never investigated: "Is the build system broken?"
|
||||
- Should have compared built artifacts between main and current branch
|
||||
|
||||
### 3. **Tried to Fix by Disabling Instead of Understanding**
|
||||
- Final approach: comment out Chroma, comment out search server
|
||||
- This is the opposite of debugging - it's just making things "work" by removing functionality
|
||||
- User called this out as "duct tape around 5 things unrelated to the problem"
|
||||
- Violated YAGNI/KISS by adding defensive complexity instead of fixing the actual issue
|
||||
|
||||
### 4. **Didn't Compare Working vs Broken State**
|
||||
- User specifically said "we fixed this before"
|
||||
- Should have immediately: `git diff main src/services/worker-service.ts`
|
||||
- Did this eventually but didn't follow through on the findings
|
||||
- The diff showed only search-everything additions - the core worker code was UNCHANGED
|
||||
- This should have been a huge red flag: "If the code is the same, why is it broken?"
|
||||
|
||||
### 5. **Overcomplicated the Investigation**
|
||||
- Started reading through ChromaSync implementation
|
||||
- Traced through MCP connection code
|
||||
- Analyzed startup sequences
|
||||
- All of this was unnecessary if the root cause was a build issue
|
||||
|
||||
## What Should Have Happened
|
||||
|
||||
### Correct Debug Sequence:
|
||||
1. ✅ Check worker status (`pm2 list`) - DONE
|
||||
2. ✅ Check error logs - DONE
|
||||
3. ❌ **Compare current code to main branch** - SKIPPED INITIALLY
|
||||
4. ❌ **Check if built files are correct** - SKIPPED
|
||||
5. ❌ **Test the build pipeline** - NEVER DONE
|
||||
6. ❌ **Verify dependencies are installed** - NEVER CHECKED
|
||||
|
||||
### The Real Questions:
|
||||
- Is this a code change or a build issue?
|
||||
- What changed between working state and broken state?
|
||||
- Are the built artifacts corrupted?
|
||||
- Is the search server build actually valid?
|
||||
- Are there missing dependencies in plugin/scripts/node_modules?
|
||||
|
||||
## Likely Root Causes (Untested)
|
||||
|
||||
Based on evidence:
|
||||
1. **Build artifacts are corrupted** - search-server.mjs threw syntax errors when run
|
||||
2. **Node modules missing/outdated** - plugin/scripts/node_modules may be stale
|
||||
3. **ESM/CJS bundling issue** - esbuild may have produced invalid output
|
||||
4. **search-everything branch has broken build config** - scripts/build-hooks.js may have issues
|
||||
|
||||
## Key Lessons
|
||||
|
||||
### KISS/DRY/YAGNI Violations
|
||||
- Added complexity (disabling features) instead of removing it
|
||||
- Tried to work around symptoms instead of fixing root cause
|
||||
- Ignored the principle: "If it worked before and code is same, it's environment/build"
|
||||
|
||||
### Debugging Anti-Patterns
|
||||
1. **Symptom Chasing**: Following error messages down rabbit holes
|
||||
2. **Defensive Coding**: Commenting out "broken" features instead of fixing them
|
||||
3. **Ignoring History**: Not comparing working vs broken states
|
||||
4. **Build Blindness**: Assuming built artifacts are correct without verification
|
||||
|
||||
### What Good Debugging Looks Like
|
||||
1. Compare working state (main) vs broken state (current branch)
|
||||
2. Identify what actually changed (code? deps? build?)
|
||||
3. Test the simplest hypothesis first (build issue vs code issue)
|
||||
4. Never disable features to "fix" things - that's not fixing
|
||||
|
||||
## Action Items for Next Attempt
|
||||
|
||||
### Before Writing Any Code:
|
||||
- [ ] `git diff main` for all modified files
|
||||
- [ ] Check if `plugin/scripts/` artifacts are valid JavaScript
|
||||
- [ ] Compare build process: `npm run build` output on main vs current branch
|
||||
- [ ] Verify `plugin/scripts/node_modules` exists and is current
|
||||
- [ ] Test search-server.mjs in isolation: `node plugin/scripts/search-server.mjs`
|
||||
|
||||
### If Build is Broken:
|
||||
- [ ] Check scripts/build-hooks.js for recent changes
|
||||
- [ ] Verify esbuild configuration
|
||||
- [ ] Test build on main branch, then on current branch
|
||||
- [ ] Don't modify source code until build is proven working
|
||||
|
||||
### If Code is Broken:
|
||||
- [ ] Create minimal repro (which specific change broke it?)
|
||||
- [ ] Fix the actual bug, don't add workarounds
|
||||
- [ ] Test the fix in isolation
|
||||
|
||||
## Conclusion
|
||||
|
||||
This failure exemplifies "debugging by making changes" instead of "debugging by understanding". The instinct to fix symptoms (MCP errors) instead of investigating root cause (why is it broken now?) led to wasted effort and ultimately no solution.
|
||||
|
||||
The user's frustration was justified - I was adding defensive duct tape instead of finding and fixing the real problem. This is exactly what KISS/DRY/YAGNI principles are meant to prevent.
|
||||
|
||||
**Next time: Compare, verify, understand, THEN fix. Never disable features to make errors go away.**
|
||||
@@ -0,0 +1,119 @@
|
||||
Unified Search API Consolidation Plan
|
||||
|
||||
Overview
|
||||
|
||||
Consolidate 10 search endpoints into 6 powerful, semantic endpoints with intelligent aliasing for backward compatibility.
|
||||
|
||||
New Endpoint Structure
|
||||
|
||||
1. /search - Unified Cross-Type Search
|
||||
|
||||
- Searches all record types (observations + sessions + prompts) via Chroma multi-collection search
|
||||
- Optional type filter to narrow down
|
||||
- Replaces: search_observations, search_sessions, search_user_prompts
|
||||
- Params: query, response=[index|full], type, project, dateRange, limit, offset, orderBy
|
||||
|
||||
2. /timeline - Unified Timeline Tool
|
||||
|
||||
- Supports both anchor-based and query-based modes via params
|
||||
- If anchor → direct timeline lookup (like get_context_timeline)
|
||||
- If query → search-first then timeline (like get_timeline_by_query)
|
||||
- Params: anchor OR query, depth_before, depth_after, response, mode, project
|
||||
|
||||
3. /decisions - Decision Observations
|
||||
|
||||
- Metadata-first search for type=decision observations
|
||||
- Uses specialized search logic for precision
|
||||
- Params: response, project, dateRange, limit, offset, orderBy
|
||||
|
||||
4. /changes - Change Observations
|
||||
|
||||
- Metadata-first search for type=change observations
|
||||
- Same pattern as /decisions
|
||||
|
||||
5. /how-it-works - How-It-Works Concept
|
||||
|
||||
- Metadata-first search for concept=how-it-works observations
|
||||
- Same pattern as concept endpoints
|
||||
|
||||
6. /contextualize - Intelligent Context Builder
|
||||
|
||||
- Complex hybrid endpoint:
|
||||
a. Get 7 latest decisions + 7 latest changes + 3 latest how-it-works
|
||||
b. Find newest date across all results
|
||||
c. Get timeline (7 before + 7 after) around that date
|
||||
d. Merge & re-sort into single timeline (newest → oldest)
|
||||
e. Return timeline + narratives of each concept's latest result
|
||||
- Params: query (for contextualization), project
|
||||
|
||||
Implementation Phases
|
||||
|
||||
Phase 1: Core Unified Search (search-server.ts)
|
||||
|
||||
- Create search tool with Chroma multi-collection query
|
||||
- Add type filtering support
|
||||
- Alias old tools: search_observations → search(type=['observations'])
|
||||
|
||||
Phase 2: Unified Timeline (search-server.ts)
|
||||
|
||||
- Merge get_context_timeline + get_timeline_by_query logic
|
||||
- Support both anchor and query params (mutually exclusive)
|
||||
- Alias old timeline tools to new unified implementation
|
||||
|
||||
Phase 3: Specialized Concept Endpoints (search-server.ts)
|
||||
|
||||
- Create decisions, changes, how_it_works tools
|
||||
- Use metadata-first search strategy
|
||||
- Update find_by_type and find_by_concept to call these internally
|
||||
|
||||
Phase 4: Contextualize Endpoint (search-server.ts)
|
||||
|
||||
- Implement parallel fetching (7 decisions, 7 changes, 3 how-it-works)
|
||||
- Find newest date, get timeline around it
|
||||
- Merge, re-sort, extract narratives
|
||||
- Return structured response with timeline + narratives
|
||||
|
||||
Phase 5: HTTP API Routes (worker-service.ts)
|
||||
|
||||
- Add 6 new routes: /api/search, /api/timeline, /api/decisions, /api/changes, /api/how-it-works, /api/contextualize
|
||||
- Update old routes to alias new implementations
|
||||
- Maintain backward compatibility
|
||||
|
||||
Phase 6: Chroma Multi-Collection Search (ChromaSync.ts)
|
||||
|
||||
- Add searchAll() method to query all collections in parallel
|
||||
- Include source collection metadata in results
|
||||
- Merge and rank by similarity score
|
||||
|
||||
Phase 7: SQLite Fallback (SessionSearch.ts)
|
||||
|
||||
- Add searchAll() for FTS5 fallback when Chroma unavailable
|
||||
- Merge results from all three FTS5 tables
|
||||
|
||||
Phase 8: Documentation & Skill Updates
|
||||
|
||||
- Update mem-search skill with new endpoints
|
||||
- Update CLAUDE.md and README.md
|
||||
- Add examples and migration guide
|
||||
|
||||
Phase 9: Testing & Deployment
|
||||
|
||||
- Unit tests for all new tools
|
||||
- Integration tests for aliasing
|
||||
- Manual testing via mem-search skill
|
||||
- Build → sync → worker restart
|
||||
|
||||
Key Design Decisions
|
||||
|
||||
✅ Aliasing Strategy: Old endpoints call new implementations internally (zero breaking changes)
|
||||
✅ Unified Search: Chroma multi-collection search for true cross-type queries
|
||||
✅ Flexible Timeline: Single tool supports both direct and query-based modes
|
||||
✅ Specialized Shortcuts: /decisions, /changes, /how-it-works for common queries
|
||||
✅ Intelligent Context: /contextualize auto-builds rich context with narratives
|
||||
|
||||
Migration Impact
|
||||
|
||||
- Users: Zero breaking changes, old endpoints work via aliasing
|
||||
- Codebase: Simplified from 10 conceptual endpoints to 6
|
||||
- Performance: Improved via Chroma multi-collection search
|
||||
- Developer UX: Cleaner, more semantic API
|
||||
@@ -26,12 +26,10 @@ const WORKER_SERVICE = {
|
||||
source: 'src/services/worker-service.ts'
|
||||
};
|
||||
|
||||
// DEPRECATED: MCP search server replaced by skill-based search
|
||||
// Keeping source file for reference: src/servers/search-server.ts
|
||||
// const SEARCH_SERVER = {
|
||||
// name: 'search-server',
|
||||
// source: 'src/servers/search-server.ts'
|
||||
// };
|
||||
const SEARCH_SERVER = {
|
||||
name: 'search-server',
|
||||
source: 'src/servers/search-server.ts'
|
||||
};
|
||||
|
||||
async function buildHooks() {
|
||||
console.log('🔨 Building claude-mem hooks and worker service...\n');
|
||||
@@ -94,6 +92,31 @@ async function buildHooks() {
|
||||
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build search server
|
||||
console.log(`\n🔧 Building search server...`);
|
||||
await build({
|
||||
entryPoints: [SEARCH_SERVER.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: `${hooksDir}/${SEARCH_SERVER.name}.cjs`,
|
||||
minify: true,
|
||||
logLevel: 'error',
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make search server executable
|
||||
fs.chmodSync(`${hooksDir}/${SEARCH_SERVER.name}.cjs`, 0o755);
|
||||
const searchServerStats = fs.statSync(`${hooksDir}/${SEARCH_SERVER.name}.cjs`);
|
||||
console.log(`✓ search-server built (${(searchServerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build each hook
|
||||
for (const hook of HOOKS) {
|
||||
console.log(`\n🔧 Building ${hook.name}...`);
|
||||
@@ -126,14 +149,11 @@ async function buildHooks() {
|
||||
console.log(`✓ ${hook.name} built (${sizeInKB} KB)`);
|
||||
}
|
||||
|
||||
// DEPRECATED: MCP search server no longer built (replaced by skill-based search)
|
||||
// Search functionality now provided via HTTP API + search skill
|
||||
// Source file kept for reference: src/servers/search-server.ts
|
||||
|
||||
console.log('\n✅ All hooks and worker service built successfully!');
|
||||
console.log('\n✅ All hooks, worker service, and search server built successfully!');
|
||||
console.log(` Output: ${hooksDir}/`);
|
||||
console.log(` - Hooks: *-hook.js`);
|
||||
console.log(` - Worker: worker-service.cjs`);
|
||||
console.log(` - Search Server: search-server.cjs`);
|
||||
console.log(` - Skills: plugin/skills/`);
|
||||
console.log('\n💡 Note: Dependencies will be auto-installed on first hook execution');
|
||||
|
||||
|
||||
@@ -57,10 +57,22 @@ async function buildViewer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy icon SVG files
|
||||
const srcUiDir = path.join(rootDir, 'src/ui');
|
||||
const outputUiDir = path.join(rootDir, 'plugin/ui');
|
||||
const iconFiles = fs.readdirSync(srcUiDir).filter(file => file.startsWith('icon-thick-') && file.endsWith('.svg'));
|
||||
for (const file of iconFiles) {
|
||||
fs.copyFileSync(
|
||||
path.join(srcUiDir, file),
|
||||
path.join(outputUiDir, file)
|
||||
);
|
||||
}
|
||||
|
||||
console.log('✓ React viewer built successfully');
|
||||
console.log(' - plugin/ui/viewer-bundle.js');
|
||||
console.log(' - plugin/ui/viewer.html (from viewer-template.html)');
|
||||
console.log(' - plugin/ui/assets/fonts/* (font files)');
|
||||
console.log(` - plugin/ui/icon-thick-*.svg (${iconFiles.length} icon files)`);
|
||||
} catch (error) {
|
||||
console.error('Failed to build viewer:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Debug Transcript Structure
|
||||
* Examines the first few entries to understand the conversation flow
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
|
||||
const transcriptPath = process.argv[2];
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.error('Usage: tsx scripts/debug-transcript-structure.ts <path-to-transcript.jsonl>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const entries = parser.getAllEntries();
|
||||
|
||||
console.log(`Total entries: ${entries.length}\n`);
|
||||
|
||||
// Count entry types
|
||||
const typeCounts: Record<string, number> = {};
|
||||
for (const entry of entries) {
|
||||
typeCounts[entry.type] = (typeCounts[entry.type] || 0) + 1;
|
||||
}
|
||||
|
||||
console.log('Entry types:');
|
||||
for (const [type, count] of Object.entries(typeCounts)) {
|
||||
console.log(` ${type}: ${count}`);
|
||||
}
|
||||
|
||||
// Find first user and assistant entries
|
||||
const firstUser = entries.find(e => e.type === 'user');
|
||||
const firstAssistant = entries.find(e => e.type === 'assistant');
|
||||
|
||||
if (firstUser) {
|
||||
const userIndex = entries.indexOf(firstUser);
|
||||
console.log(`\n\n=== First User Entry (index ${userIndex}) ===`);
|
||||
console.log(`Timestamp: ${firstUser.timestamp}`);
|
||||
if (typeof firstUser.content === 'string') {
|
||||
console.log(`Content (string): ${firstUser.content.substring(0, 200)}...`);
|
||||
} else if (Array.isArray(firstUser.content)) {
|
||||
console.log(`Content blocks: ${firstUser.content.length}`);
|
||||
for (const block of firstUser.content) {
|
||||
if (block.type === 'text') {
|
||||
console.log(` - text: ${(block as any).text?.substring(0, 200)}...`);
|
||||
} else {
|
||||
console.log(` - ${block.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstAssistant) {
|
||||
const assistantIndex = entries.indexOf(firstAssistant);
|
||||
console.log(`\n\n=== First Assistant Entry (index ${assistantIndex}) ===`);
|
||||
console.log(`Timestamp: ${firstAssistant.timestamp}`);
|
||||
if (Array.isArray(firstAssistant.content)) {
|
||||
console.log(`Content blocks: ${firstAssistant.content.length}`);
|
||||
for (const block of firstAssistant.content) {
|
||||
if (block.type === 'text') {
|
||||
console.log(` - text: ${(block as any).text?.substring(0, 200)}...`);
|
||||
} else if (block.type === 'thinking') {
|
||||
console.log(` - thinking: ${(block as any).thinking?.substring(0, 200)}...`);
|
||||
} else if (block.type === 'tool_use') {
|
||||
console.log(` - tool_use: ${(block as any).name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find a few more user/assistant pairs
|
||||
console.log('\n\n=== First 3 Conversation Exchanges ===\n');
|
||||
|
||||
let userCount = 0;
|
||||
let assistantCount = 0;
|
||||
let exchangeNum = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.type === 'user') {
|
||||
userCount++;
|
||||
if (userCount <= 3) {
|
||||
exchangeNum++;
|
||||
console.log(`\n--- Exchange ${exchangeNum}: USER ---`);
|
||||
if (typeof entry.content === 'string') {
|
||||
console.log(entry.content.substring(0, 150) + (entry.content.length > 150 ? '...' : ''));
|
||||
} else if (Array.isArray(entry.content)) {
|
||||
const textBlock = entry.content.find((b: any) => b.type === 'text');
|
||||
if (textBlock) {
|
||||
const text = (textBlock as any).text || '';
|
||||
console.log(text.substring(0, 150) + (text.length > 150 ? '...' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (entry.type === 'assistant' && userCount <= 3) {
|
||||
assistantCount++;
|
||||
if (Array.isArray(entry.content)) {
|
||||
const textBlock = entry.content.find((b: any) => b.type === 'text');
|
||||
const toolUses = entry.content.filter((b: any) => b.type === 'tool_use');
|
||||
|
||||
console.log(`\n--- Exchange ${exchangeNum}: ASSISTANT ---`);
|
||||
if (textBlock) {
|
||||
const text = (textBlock as any).text || '';
|
||||
console.log(text.substring(0, 150) + (text.length > 150 ? '...' : ''));
|
||||
}
|
||||
if (toolUses.length > 0) {
|
||||
console.log(`\nTools used: ${toolUses.map((t: any) => t.name).join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userCount >= 3 && assistantCount >= 3) break;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Simple 1:1 transcript dump in readable markdown format
|
||||
* Shows exactly what's in the transcript, chronologically
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
const transcriptPath = process.argv[2];
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.error('Usage: tsx scripts/dump-transcript-readable.ts <path-to-transcript.jsonl>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const entries = parser.getAllEntries();
|
||||
|
||||
let output = '# Transcript Dump\n\n';
|
||||
output += `Total entries: ${entries.length}\n\n`;
|
||||
output += '---\n\n';
|
||||
|
||||
let entryNum = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
entryNum++;
|
||||
|
||||
// Skip file-history-snapshot and summary entries for now
|
||||
if (entry.type === 'file-history-snapshot' || entry.type === 'summary') continue;
|
||||
|
||||
output += `## Entry ${entryNum}: ${entry.type.toUpperCase()}\n`;
|
||||
output += `**Timestamp:** ${entry.timestamp}\n\n`;
|
||||
|
||||
if (entry.type === 'user') {
|
||||
const content = entry.message.content;
|
||||
|
||||
if (typeof content === 'string') {
|
||||
output += `**Content:**\n\`\`\`\n${content}\n\`\`\`\n\n`;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text') {
|
||||
output += `**Text:**\n\`\`\`\n${(block as any).text}\n\`\`\`\n\n`;
|
||||
} else if (block.type === 'tool_result') {
|
||||
output += `**Tool Result (${(block as any).tool_use_id}):**\n`;
|
||||
const resultContent = (block as any).content;
|
||||
if (typeof resultContent === 'string') {
|
||||
const preview = resultContent.substring(0, 500);
|
||||
output += `\`\`\`\n${preview}${resultContent.length > 500 ? '\n...(truncated)' : ''}\n\`\`\`\n\n`;
|
||||
} else {
|
||||
output += `\`\`\`json\n${JSON.stringify(resultContent, null, 2).substring(0, 500)}\n\`\`\`\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.type === 'assistant') {
|
||||
const content = entry.message.content;
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text') {
|
||||
output += `**Text:**\n\`\`\`\n${(block as any).text}\n\`\`\`\n\n`;
|
||||
} else if (block.type === 'thinking') {
|
||||
output += `**Thinking:**\n\`\`\`\n${(block as any).thinking}\n\`\`\`\n\n`;
|
||||
} else if (block.type === 'tool_use') {
|
||||
const tool = block as any;
|
||||
output += `**Tool Use: ${tool.name}**\n`;
|
||||
output += `\`\`\`json\n${JSON.stringify(tool.input, null, 2)}\n\`\`\`\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show token usage if available
|
||||
const usage = entry.message.usage;
|
||||
if (usage) {
|
||||
output += `**Usage:**\n`;
|
||||
output += `- Input: ${usage.input_tokens || 0}\n`;
|
||||
output += `- Output: ${usage.output_tokens || 0}\n`;
|
||||
output += `- Cache creation: ${usage.cache_creation_input_tokens || 0}\n`;
|
||||
output += `- Cache read: ${usage.cache_read_input_tokens || 0}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
output += '---\n\n';
|
||||
|
||||
// Limit to first 20 entries to keep file manageable
|
||||
if (entryNum >= 20) {
|
||||
output += `\n_Remaining ${entries.length - 20} entries omitted for brevity_\n`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const outputPath = '/Users/alexnewman/Scripts/claude-mem/docs/context/transcript-dump.md';
|
||||
writeFileSync(outputPath, output, 'utf-8');
|
||||
|
||||
console.log(`\nTranscript dumped to: ${outputPath}`);
|
||||
console.log(`Showing first 20 conversation entries (skipped file-history-snapshot and summary types)\n`);
|
||||
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Endless Mode Token Economics Calculator
|
||||
*
|
||||
* Simulates the recursive/cumulative token savings from Endless Mode by
|
||||
* "playing the tape through" with real observation data from SQLite.
|
||||
*
|
||||
* Key Insight:
|
||||
* - Discovery tokens are ALWAYS spent (creating observations)
|
||||
* - But Endless Mode feeds compressed observations as context instead of full tool outputs
|
||||
* - Savings compound recursively - each tool benefits from ALL previous compressions
|
||||
*/
|
||||
|
||||
const observationsData = [{"id":10136,"type":"decision","title":"Token Accounting Function for Recursive Continuation Pattern","discovery_tokens":4037,"created_at_epoch":1763360747429,"compressed_size":1613},
|
||||
{"id":10135,"type":"discovery","title":"Sequential Thinking Analysis of Token Economics Calculator","discovery_tokens":1439,"created_at_epoch":1763360651617,"compressed_size":1812},
|
||||
{"id":10134,"type":"discovery","title":"Recent Context Query Execution","discovery_tokens":1273,"created_at_epoch":1763360646273,"compressed_size":1228},
|
||||
{"id":10133,"type":"discovery","title":"Token Data Query Execution and Historical Context","discovery_tokens":11878,"created_at_epoch":1763360642485,"compressed_size":1924},
|
||||
{"id":10132,"type":"discovery","title":"Token Data Query and Script Validation Request","discovery_tokens":4167,"created_at_epoch":1763360628269,"compressed_size":903},
|
||||
{"id":10131,"type":"discovery","title":"Endless Mode Token Economics Analysis Output: Complete Infrastructure Impact","discovery_tokens":2458,"created_at_epoch":1763360553238,"compressed_size":2166},
|
||||
{"id":10130,"type":"change","title":"Integration of Actual Compute Savings Analysis into Main Execution Flow","discovery_tokens":11031,"created_at_epoch":1763360545347,"compressed_size":1032},
|
||||
{"id":10129,"type":"discovery","title":"Prompt Caching Economics: User Cost vs. Anthropic Compute Cost Divergence","discovery_tokens":20059,"created_at_epoch":1763360540854,"compressed_size":1802},
|
||||
{"id":10128,"type":"discovery","title":"Token Caching Cost Analysis Across AI Model Providers","discovery_tokens":3506,"created_at_epoch":1763360478133,"compressed_size":1245},
|
||||
{"id":10127,"type":"discovery","title":"Endless Mode Token Economics Calculator Successfully Integrated Prompt Caching Cost Model","discovery_tokens":3481,"created_at_epoch":1763360384055,"compressed_size":2444},
|
||||
{"id":10126,"type":"bugfix","title":"Fix Return Statement Variable Names in playTheTapeThrough Function","discovery_tokens":8326,"created_at_epoch":1763360374566,"compressed_size":1250},
|
||||
{"id":10125,"type":"change","title":"Redesign Timeline Display to Show Fresh/Cached Token Breakdown and Real Dollar Costs","discovery_tokens":12999,"created_at_epoch":1763360368843,"compressed_size":2004},
|
||||
{"id":10124,"type":"change","title":"Replace Estimated Cost Model with Actual Caching-Based Costs in Anthropic Scale Analysis","discovery_tokens":12867,"created_at_epoch":1763360361147,"compressed_size":2064},
|
||||
{"id":10123,"type":"change","title":"Pivot Session Length Comparison Table from Token to Cost Metrics","discovery_tokens":9746,"created_at_epoch":1763360352992,"compressed_size":1652},
|
||||
{"id":10122,"type":"change","title":"Add Dual Reporting: Token Count vs Actual Cost in Comparison Output","discovery_tokens":9602,"created_at_epoch":1763360346495,"compressed_size":1640},
|
||||
{"id":10121,"type":"change","title":"Apply Prompt Caching Cost Model to Endless Mode Calculation Function","discovery_tokens":9963,"created_at_epoch":1763360339238,"compressed_size":2003},
|
||||
{"id":10120,"type":"change","title":"Integrate Prompt Caching Cost Calculations into Without-Endless-Mode Function","discovery_tokens":8652,"created_at_epoch":1763360332046,"compressed_size":1701},
|
||||
{"id":10119,"type":"change","title":"Display Prompt Caching Pricing in Initial Calculator Output","discovery_tokens":6669,"created_at_epoch":1763360325882,"compressed_size":1188},
|
||||
{"id":10118,"type":"change","title":"Add Prompt Caching Pricing Model to Token Economics Calculator","discovery_tokens":10433,"created_at_epoch":1763360320552,"compressed_size":1264},
|
||||
{"id":10117,"type":"discovery","title":"Claude API Prompt Caching Cost Optimization Factor","discovery_tokens":3439,"created_at_epoch":1763360210175,"compressed_size":1142},
|
||||
{"id":10116,"type":"discovery","title":"Endless Mode Token Economics Verified at Scale","discovery_tokens":2855,"created_at_epoch":1763360144039,"compressed_size":2184},
|
||||
{"id":10115,"type":"feature","title":"Token Economics Calculator for Endless Mode Sessions","discovery_tokens":13468,"created_at_epoch":1763360134068,"compressed_size":1858},
|
||||
{"id":10114,"type":"decision","title":"Token Accounting for Recursive Session Continuations","discovery_tokens":3550,"created_at_epoch":1763360052317,"compressed_size":1478},
|
||||
{"id":10113,"type":"discovery","title":"Performance and Token Optimization Impact Analysis for Endless Mode","discovery_tokens":3464,"created_at_epoch":1763359862175,"compressed_size":1259},
|
||||
{"id":10112,"type":"change","title":"Endless Mode Blocking Hooks & Transcript Transformation Plan Document Created","discovery_tokens":17312,"created_at_epoch":1763359465307,"compressed_size":2181},
|
||||
{"id":10111,"type":"change","title":"Plan Document Creation for Morning Implementation","discovery_tokens":3652,"created_at_epoch":1763359347166,"compressed_size":843},
|
||||
{"id":10110,"type":"decision","title":"Blocking vs Non-Blocking Behavior by Mode","discovery_tokens":3652,"created_at_epoch":1763359347165,"compressed_size":797},
|
||||
{"id":10109,"type":"decision","title":"Tool Use and Observation Processing Architecture: Non-Blocking vs Blocking","discovery_tokens":3472,"created_at_epoch":1763359247045,"compressed_size":1349},
|
||||
{"id":10108,"type":"feature","title":"SessionManager.getMessageIterator implements event-driven async generator with graceful abort handling","discovery_tokens":2417,"created_at_epoch":1763359189299,"compressed_size":2016},
|
||||
{"id":10107,"type":"feature","title":"SessionManager implements event-driven session lifecycle with auto-initialization and zero-latency queue notifications","discovery_tokens":4734,"created_at_epoch":1763359165608,"compressed_size":2781},
|
||||
{"id":10106,"type":"discovery","title":"Two distinct uses of transcript data: live data flow vs session initialization","discovery_tokens":2933,"created_at_epoch":1763359156448,"compressed_size":2015},
|
||||
{"id":10105,"type":"discovery","title":"Transcript initialization pattern identified for compressed context on session resume","discovery_tokens":2933,"created_at_epoch":1763359156447,"compressed_size":2536},
|
||||
{"id":10104,"type":"feature","title":"SDKAgent implements event-driven message generator with continuation prompt logic and Endless Mode integration","discovery_tokens":6148,"created_at_epoch":1763359140399,"compressed_size":3241},
|
||||
{"id":10103,"type":"discovery","title":"Endless Mode architecture documented with phased implementation plan and context economics","discovery_tokens":5296,"created_at_epoch":1763359127954,"compressed_size":3145},
|
||||
{"id":10102,"type":"feature","title":"Save hook enhanced to extract and forward tool_use_id for Endless Mode linking","discovery_tokens":3294,"created_at_epoch":1763359115848,"compressed_size":2125},
|
||||
{"id":10101,"type":"feature","title":"TransformLayer implements Endless Mode context compression via observation substitution","discovery_tokens":4637,"created_at_epoch":1763359108317,"compressed_size":2629},
|
||||
{"id":10100,"type":"feature","title":"EndlessModeConfig implemented for loading Endless Mode settings from files and environment","discovery_tokens":2313,"created_at_epoch":1763359099972,"compressed_size":2125},
|
||||
{"id":10098,"type":"change","title":"User prompts wrapped with semantic XML structure in buildInitPrompt and buildContinuationPrompt","discovery_tokens":7806,"created_at_epoch":1763359091460,"compressed_size":1585},
|
||||
{"id":10099,"type":"discovery","title":"Session persistence mechanism relies on SDK internal state without context reload","discovery_tokens":7806,"created_at_epoch":1763359091460,"compressed_size":1883},
|
||||
{"id":10097,"type":"change","title":"Worker service session init now extracts userPrompt and promptNumber from request body","discovery_tokens":7806,"created_at_epoch":1763359091459,"compressed_size":1148},
|
||||
{"id":10096,"type":"feature","title":"SessionManager enhanced to accept dynamic userPrompt updates during multi-turn conversations","discovery_tokens":7806,"created_at_epoch":1763359091457,"compressed_size":1528},
|
||||
{"id":10095,"type":"discovery","title":"Five lifecycle hooks integrate claude-mem at critical session boundaries","discovery_tokens":6625,"created_at_epoch":1763359074808,"compressed_size":1570},
|
||||
{"id":10094,"type":"discovery","title":"PostToolUse hook is real-time observation creation point, not delayed processing","discovery_tokens":6625,"created_at_epoch":1763359074807,"compressed_size":2371},
|
||||
{"id":10093,"type":"discovery","title":"PostToolUse hook timing and compression integration options explored","discovery_tokens":1696,"created_at_epoch":1763359062088,"compressed_size":1605},
|
||||
{"id":10092,"type":"discovery","title":"Transcript transformation strategy for endless mode identified","discovery_tokens":6112,"created_at_epoch":1763359057563,"compressed_size":1968},
|
||||
{"id":10091,"type":"decision","title":"Finalized Transcript Compression Implementation Strategy","discovery_tokens":1419,"created_at_epoch":1763358943803,"compressed_size":1556},
|
||||
{"id":10090,"type":"discovery","title":"UserPromptSubmit Hook as Compression Integration Point","discovery_tokens":1546,"created_at_epoch":1763358931936,"compressed_size":1621},
|
||||
{"id":10089,"type":"decision","title":"Hypothesis 5 Selected: UserPromptSubmit Hook for Transcript Compression","discovery_tokens":1465,"created_at_epoch":1763358920209,"compressed_size":1918}];
|
||||
|
||||
// Estimate original tool output size from discovery tokens
|
||||
// Heuristic: discovery_tokens roughly correlates with original content size
|
||||
// Assumption: If it took 10k tokens to analyze, original was probably 15-30k tokens
|
||||
function estimateOriginalToolOutputSize(discoveryTokens) {
|
||||
// Conservative multiplier: 2x (original content was 2x the discovery cost)
|
||||
// This accounts for: reading the tool output + analyzing it + generating observation
|
||||
return discoveryTokens * 2;
|
||||
}
|
||||
|
||||
// Convert compressed_size (character count) to approximate token count
|
||||
// Rough heuristic: 1 token ≈ 4 characters for English text
|
||||
function charsToTokens(chars) {
|
||||
return Math.ceil(chars / 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate session WITHOUT Endless Mode (current behavior)
|
||||
* Each continuation carries ALL previous full tool outputs in context
|
||||
*/
|
||||
function calculateWithoutEndlessMode(observations) {
|
||||
let cumulativeContextTokens = 0;
|
||||
let totalDiscoveryTokens = 0;
|
||||
let totalContinuationTokens = 0;
|
||||
const timeline = [];
|
||||
|
||||
observations.forEach((obs, index) => {
|
||||
const toolNumber = index + 1;
|
||||
const originalToolSize = estimateOriginalToolOutputSize(obs.discovery_tokens);
|
||||
|
||||
// Discovery cost (creating observation from full tool output)
|
||||
const discoveryCost = obs.discovery_tokens;
|
||||
totalDiscoveryTokens += discoveryCost;
|
||||
|
||||
// Continuation cost: Re-process ALL previous tool outputs + current one
|
||||
// This is the key recursive cost
|
||||
cumulativeContextTokens += originalToolSize;
|
||||
const continuationCost = cumulativeContextTokens;
|
||||
totalContinuationTokens += continuationCost;
|
||||
|
||||
timeline.push({
|
||||
tool: toolNumber,
|
||||
obsId: obs.id,
|
||||
title: obs.title.substring(0, 60),
|
||||
originalSize: originalToolSize,
|
||||
discoveryCost,
|
||||
contextSize: cumulativeContextTokens,
|
||||
continuationCost,
|
||||
totalCostSoFar: totalDiscoveryTokens + totalContinuationTokens
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalDiscoveryTokens,
|
||||
totalContinuationTokens,
|
||||
totalTokens: totalDiscoveryTokens + totalContinuationTokens,
|
||||
timeline
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate session WITH Endless Mode
|
||||
* Each continuation carries ALL previous COMPRESSED observations in context
|
||||
*/
|
||||
function calculateWithEndlessMode(observations) {
|
||||
let cumulativeContextTokens = 0;
|
||||
let totalDiscoveryTokens = 0;
|
||||
let totalContinuationTokens = 0;
|
||||
const timeline = [];
|
||||
|
||||
observations.forEach((obs, index) => {
|
||||
const toolNumber = index + 1;
|
||||
const originalToolSize = estimateOriginalToolOutputSize(obs.discovery_tokens);
|
||||
const compressedSize = charsToTokens(obs.compressed_size);
|
||||
|
||||
// Discovery cost (same as without Endless Mode - still need to create observation)
|
||||
const discoveryCost = obs.discovery_tokens;
|
||||
totalDiscoveryTokens += discoveryCost;
|
||||
|
||||
// KEY DIFFERENCE: Add COMPRESSED size to context, not original size
|
||||
cumulativeContextTokens += compressedSize;
|
||||
const continuationCost = cumulativeContextTokens;
|
||||
totalContinuationTokens += continuationCost;
|
||||
|
||||
const compressionRatio = ((originalToolSize - compressedSize) / originalToolSize * 100).toFixed(1);
|
||||
|
||||
timeline.push({
|
||||
tool: toolNumber,
|
||||
obsId: obs.id,
|
||||
title: obs.title.substring(0, 60),
|
||||
originalSize: originalToolSize,
|
||||
compressedSize,
|
||||
compressionRatio: `${compressionRatio}%`,
|
||||
discoveryCost,
|
||||
contextSize: cumulativeContextTokens,
|
||||
continuationCost,
|
||||
totalCostSoFar: totalDiscoveryTokens + totalContinuationTokens
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalDiscoveryTokens,
|
||||
totalContinuationTokens,
|
||||
totalTokens: totalDiscoveryTokens + totalContinuationTokens,
|
||||
timeline
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the tape through - show token-by-token progression
|
||||
*/
|
||||
function playTheTapeThrough(observations) {
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('ENDLESS MODE TOKEN ECONOMICS CALCULATOR');
|
||||
console.log('Playing the tape through with REAL observation data');
|
||||
console.log('='.repeat(100) + '\n');
|
||||
|
||||
console.log(`📊 Dataset: ${observations.length} observations from live sessions\n`);
|
||||
|
||||
// Calculate both scenarios
|
||||
const without = calculateWithoutEndlessMode(observations);
|
||||
const withMode = calculateWithEndlessMode(observations);
|
||||
|
||||
// Show first 10 tools from each scenario side by side
|
||||
console.log('🎬 TAPE PLAYBACK: First 10 Tools\n');
|
||||
console.log('WITHOUT Endless Mode (Current) | WITH Endless Mode (Proposed)');
|
||||
console.log('-'.repeat(100));
|
||||
|
||||
for (let i = 0; i < Math.min(10, observations.length); i++) {
|
||||
const w = without.timeline[i];
|
||||
const e = withMode.timeline[i];
|
||||
|
||||
console.log(`\nTool #${w.tool}: ${w.title}`);
|
||||
console.log(` Original: ${w.originalSize.toLocaleString()}t | Compressed: ${e.compressedSize.toLocaleString()}t (${e.compressionRatio} saved)`);
|
||||
console.log(` Context: ${w.contextSize.toLocaleString()}t | Context: ${e.contextSize.toLocaleString()}t`);
|
||||
console.log(` Total: ${w.totalCostSoFar.toLocaleString()}t | Total: ${e.totalCostSoFar.toLocaleString()}t`);
|
||||
}
|
||||
|
||||
// Summary table
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('📈 FINAL TOTALS\n');
|
||||
|
||||
console.log('WITHOUT Endless Mode (Current):');
|
||||
console.log(` Discovery tokens: ${without.totalDiscoveryTokens.toLocaleString()}t (creating observations)`);
|
||||
console.log(` Continuation tokens: ${without.totalContinuationTokens.toLocaleString()}t (context accumulation)`);
|
||||
console.log(` TOTAL TOKENS: ${without.totalTokens.toLocaleString()}t`);
|
||||
|
||||
console.log('\nWITH Endless Mode:');
|
||||
console.log(` Discovery tokens: ${withMode.totalDiscoveryTokens.toLocaleString()}t (same - still create observations)`);
|
||||
console.log(` Continuation tokens: ${withMode.totalContinuationTokens.toLocaleString()}t (COMPRESSED context)`);
|
||||
console.log(` TOTAL TOKENS: ${withMode.totalTokens.toLocaleString()}t`);
|
||||
|
||||
const tokensSaved = without.totalTokens - withMode.totalTokens;
|
||||
const percentSaved = (tokensSaved / without.totalTokens * 100).toFixed(1);
|
||||
|
||||
console.log('\n💰 SAVINGS:');
|
||||
console.log(` Tokens saved: ${tokensSaved.toLocaleString()}t`);
|
||||
console.log(` Percentage saved: ${percentSaved}%`);
|
||||
console.log(` Efficiency gain: ${(without.totalTokens / withMode.totalTokens).toFixed(2)}x`);
|
||||
|
||||
// Anthropic scale calculation
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('🌍 ANTHROPIC SCALE IMPACT\n');
|
||||
|
||||
// Conservative assumptions
|
||||
const activeUsers = 100000; // Claude Code users
|
||||
const sessionsPerWeek = 10; // Per user
|
||||
const toolsPerSession = observations.length; // Use our actual data
|
||||
const weeklyToolUses = activeUsers * sessionsPerWeek * toolsPerSession;
|
||||
|
||||
const avgTokensPerToolWithout = without.totalTokens / observations.length;
|
||||
const avgTokensPerToolWith = withMode.totalTokens / observations.length;
|
||||
|
||||
const weeklyTokensWithout = weeklyToolUses * avgTokensPerToolWithout;
|
||||
const weeklyTokensWith = weeklyToolUses * avgTokensPerToolWith;
|
||||
const weeklyTokensSaved = weeklyTokensWithout - weeklyTokensWith;
|
||||
|
||||
console.log('Assumptions:');
|
||||
console.log(` Active Claude Code users: ${activeUsers.toLocaleString()}`);
|
||||
console.log(` Sessions per user/week: ${sessionsPerWeek}`);
|
||||
console.log(` Tools per session: ${toolsPerSession}`);
|
||||
console.log(` Weekly tool uses: ${weeklyToolUses.toLocaleString()}`);
|
||||
|
||||
console.log('\nWeekly Compute:');
|
||||
console.log(` Without Endless Mode: ${(weeklyTokensWithout / 1e9).toFixed(2)} billion tokens`);
|
||||
console.log(` With Endless Mode: ${(weeklyTokensWith / 1e9).toFixed(2)} billion tokens`);
|
||||
console.log(` Weekly savings: ${(weeklyTokensSaved / 1e9).toFixed(2)} billion tokens (${percentSaved}%)`);
|
||||
|
||||
const annualTokensSaved = weeklyTokensSaved * 52;
|
||||
console.log(` Annual savings: ${(annualTokensSaved / 1e12).toFixed(2)} TRILLION tokens`);
|
||||
|
||||
console.log('\n💡 What this means:');
|
||||
console.log(` • ${percentSaved}% reduction in Claude Code inference costs`);
|
||||
console.log(` • ${(without.totalTokens / withMode.totalTokens).toFixed(1)}x more users served with same infrastructure`);
|
||||
console.log(` • Massive energy/compute savings at scale`);
|
||||
console.log(` • Longer sessions = better UX without economic penalty`);
|
||||
|
||||
console.log('\n' + '='.repeat(100) + '\n');
|
||||
|
||||
return {
|
||||
without,
|
||||
withMode,
|
||||
tokensSaved,
|
||||
percentSaved,
|
||||
weeklyTokensSaved,
|
||||
annualTokensSaved
|
||||
};
|
||||
}
|
||||
|
||||
// Run the calculation
|
||||
playTheTapeThrough(observationsData);
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Extract Rich Context Examples
|
||||
* Shows what data we have available for memory worker using TranscriptParser API
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
import { writeFileSync } from 'fs';
|
||||
import type { AssistantTranscriptEntry, UserTranscriptEntry } from '../src/types/transcript.js';
|
||||
|
||||
const transcriptPath = process.argv[2];
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.error('Usage: tsx scripts/extract-rich-context-examples.ts <path-to-transcript.jsonl>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
|
||||
let output = '# Rich Context Examples\n\n';
|
||||
output += 'This document shows what contextual data is available in transcripts\n';
|
||||
output += 'that could improve observation generation quality.\n\n';
|
||||
|
||||
// Get stats using parser API
|
||||
const stats = parser.getParseStats();
|
||||
const tokens = parser.getTotalTokenUsage();
|
||||
|
||||
output += `## Statistics\n\n`;
|
||||
output += `- Total entries: ${stats.parsedEntries}\n`;
|
||||
output += `- User messages: ${stats.entriesByType['user'] || 0}\n`;
|
||||
output += `- Assistant messages: ${stats.entriesByType['assistant'] || 0}\n`;
|
||||
output += `- Token usage: ${(tokens.inputTokens + tokens.outputTokens).toLocaleString()} total\n`;
|
||||
output += `- Cache efficiency: ${tokens.cacheReadTokens.toLocaleString()} tokens read from cache\n\n`;
|
||||
|
||||
// Extract conversation pairs with tool uses
|
||||
const assistantEntries = parser.getAssistantEntries();
|
||||
const userEntries = parser.getUserEntries();
|
||||
|
||||
output += `## Conversation Flow\n\n`;
|
||||
output += `This shows how user requests, assistant reasoning, and tool executions flow together.\n`;
|
||||
output += `This is the rich context currently missing from individual tool observations.\n\n`;
|
||||
|
||||
let examplesFound = 0;
|
||||
const maxExamples = 5;
|
||||
|
||||
// Match assistant entries with their preceding user message
|
||||
for (let i = 0; i < assistantEntries.length && examplesFound < maxExamples; i++) {
|
||||
const assistantEntry = assistantEntries[i];
|
||||
const content = assistantEntry.message.content;
|
||||
|
||||
if (!Array.isArray(content)) continue;
|
||||
|
||||
// Extract components from assistant message
|
||||
const textBlocks = content.filter((c: any) => c.type === 'text');
|
||||
const thinkingBlocks = content.filter((c: any) => c.type === 'thinking');
|
||||
const toolUseBlocks = content.filter((c: any) => c.type === 'tool_use');
|
||||
|
||||
// Skip if no tools or only MCP tools
|
||||
const regularTools = toolUseBlocks.filter((t: any) =>
|
||||
!t.name.startsWith('mcp__')
|
||||
);
|
||||
|
||||
if (regularTools.length === 0) continue;
|
||||
|
||||
// Find the user message that preceded this assistant response
|
||||
let userMessage = '';
|
||||
const assistantTimestamp = new Date(assistantEntry.timestamp).getTime();
|
||||
|
||||
for (const userEntry of userEntries) {
|
||||
const userTimestamp = new Date(userEntry.timestamp).getTime();
|
||||
if (userTimestamp < assistantTimestamp) {
|
||||
// Extract user text using parser's helper
|
||||
const extractText = (content: any): string => {
|
||||
if (typeof content === 'string') return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const text = extractText(userEntry.message.content);
|
||||
if (text.trim()) {
|
||||
userMessage = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
examplesFound++;
|
||||
output += `---\n\n`;
|
||||
output += `### Example ${examplesFound}\n\n`;
|
||||
|
||||
// 1. User Request
|
||||
if (userMessage) {
|
||||
output += `#### 👤 User Request\n`;
|
||||
const preview = userMessage.substring(0, 400);
|
||||
output += `\`\`\`\n${preview}${userMessage.length > 400 ? '\n...(truncated)' : ''}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
// 2. Assistant's Explanation (what it plans to do)
|
||||
if (textBlocks.length > 0) {
|
||||
const text = textBlocks.map((b: any) => b.text).join('\n');
|
||||
output += `#### 🤖 Assistant's Plan\n`;
|
||||
const preview = text.substring(0, 400);
|
||||
output += `\`\`\`\n${preview}${text.length > 400 ? '\n...(truncated)' : ''}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
// 3. Internal Reasoning (thinking)
|
||||
if (thinkingBlocks.length > 0) {
|
||||
const thinking = thinkingBlocks.map((b: any) => b.thinking).join('\n');
|
||||
output += `#### 💭 Internal Reasoning\n`;
|
||||
const preview = thinking.substring(0, 300);
|
||||
output += `\`\`\`\n${preview}${thinking.length > 300 ? '\n...(truncated)' : ''}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
// 4. Tool Executions
|
||||
output += `#### 🔧 Tools Executed (${regularTools.length})\n\n`;
|
||||
for (const tool of regularTools) {
|
||||
const toolData = tool as any;
|
||||
output += `**${toolData.name}**\n`;
|
||||
|
||||
// Show relevant input fields
|
||||
const input = toolData.input;
|
||||
if (toolData.name === 'Read') {
|
||||
output += `- Reading: \`${input.file_path}\`\n`;
|
||||
} else if (toolData.name === 'Write') {
|
||||
output += `- Writing: \`${input.file_path}\` (${input.content?.length || 0} chars)\n`;
|
||||
} else if (toolData.name === 'Edit') {
|
||||
output += `- Editing: \`${input.file_path}\`\n`;
|
||||
} else if (toolData.name === 'Bash') {
|
||||
output += `- Command: \`${input.command}\`\n`;
|
||||
} else if (toolData.name === 'Glob') {
|
||||
output += `- Pattern: \`${input.pattern}\`\n`;
|
||||
} else if (toolData.name === 'Grep') {
|
||||
output += `- Searching for: \`${input.pattern}\`\n`;
|
||||
} else {
|
||||
output += `\`\`\`json\n${JSON.stringify(input, null, 2).substring(0, 200)}\n\`\`\`\n`;
|
||||
}
|
||||
}
|
||||
output += `\n`;
|
||||
|
||||
// Summary of what data is available
|
||||
output += `**📊 Data Available for This Exchange:**\n`;
|
||||
output += `- User intent: ✅ (${userMessage.length} chars)\n`;
|
||||
output += `- Assistant reasoning: ✅ (${textBlocks.reduce((sum, b: any) => sum + b.text.length, 0)} chars)\n`;
|
||||
output += `- Thinking process: ${thinkingBlocks.length > 0 ? '✅' : '❌'} ${thinkingBlocks.length > 0 ? `(${thinkingBlocks.reduce((sum, b: any) => sum + b.thinking.length, 0)} chars)` : ''}\n`;
|
||||
output += `- Tool executions: ✅ (${regularTools.length} tools)\n`;
|
||||
output += `- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌\n\n`;
|
||||
}
|
||||
|
||||
output += `\n---\n\n`;
|
||||
output += `## Key Insight\n\n`;
|
||||
output += `Currently, the memory worker receives **isolated tool executions** via save-hook:\n`;
|
||||
output += `- tool_name: "Read"\n`;
|
||||
output += `- tool_input: {"file_path": "src/foo.ts"}\n`;
|
||||
output += `- tool_output: {file contents}\n\n`;
|
||||
output += `But the transcript contains **rich contextual data**:\n`;
|
||||
output += `- WHY the tool was used (user's request)\n`;
|
||||
output += `- WHAT the assistant planned to accomplish\n`;
|
||||
output += `- HOW it fits into the broader task\n`;
|
||||
output += `- The assistant's reasoning/thinking\n`;
|
||||
output += `- Multiple related tools used together\n\n`;
|
||||
output += `This context would help the memory worker:\n`;
|
||||
output += `1. Understand if a tool use is meaningful or routine\n`;
|
||||
output += `2. Generate observations that capture WHY, not just WHAT\n`;
|
||||
output += `3. Group related tools into coherent actions\n`;
|
||||
output += `4. Avoid "investigating" - the context is already present\n\n`;
|
||||
|
||||
// Write to file
|
||||
const outputPath = '/Users/alexnewman/Scripts/claude-mem/docs/context/rich-context-examples.md';
|
||||
writeFileSync(outputPath, output, 'utf-8');
|
||||
|
||||
console.log(`\nExtracted ${examplesFound} examples with rich context`);
|
||||
console.log(`Written to: ${outputPath}\n`);
|
||||
console.log(`This shows the gap between what's available (rich context) and what's sent (isolated tools)\n`);
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Format Transcript Context
|
||||
*
|
||||
* Parses a Claude Code transcript and formats it to show rich contextual data
|
||||
* that could be used for improved observation generation.
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { basename } from 'path';
|
||||
|
||||
interface ConversationTurn {
|
||||
turnNumber: number;
|
||||
userMessage?: {
|
||||
content: string;
|
||||
timestamp: string;
|
||||
};
|
||||
assistantMessage?: {
|
||||
textContent: string;
|
||||
thinkingContent?: string;
|
||||
toolUses: Array<{
|
||||
name: string;
|
||||
input: any;
|
||||
timestamp: string;
|
||||
}>;
|
||||
timestamp: string;
|
||||
};
|
||||
toolResults?: Array<{
|
||||
toolName: string;
|
||||
result: any;
|
||||
timestamp: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function extractConversationTurns(parser: TranscriptParser): ConversationTurn[] {
|
||||
const entries = parser.getAllEntries();
|
||||
const turns: ConversationTurn[] = [];
|
||||
let currentTurn: ConversationTurn | null = null;
|
||||
let turnNumber = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
// User messages start a new turn
|
||||
if (entry.type === 'user') {
|
||||
// If previous turn exists, push it
|
||||
if (currentTurn) {
|
||||
turns.push(currentTurn);
|
||||
}
|
||||
|
||||
// Start new turn
|
||||
turnNumber++;
|
||||
currentTurn = {
|
||||
turnNumber,
|
||||
toolResults: []
|
||||
};
|
||||
|
||||
// Extract user text (skip tool results)
|
||||
if (typeof entry.content === 'string') {
|
||||
currentTurn.userMessage = {
|
||||
content: entry.content,
|
||||
timestamp: entry.timestamp
|
||||
};
|
||||
} else if (Array.isArray(entry.content)) {
|
||||
const textContent = entry.content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text)
|
||||
.join('\n');
|
||||
|
||||
if (textContent.trim()) {
|
||||
currentTurn.userMessage = {
|
||||
content: textContent,
|
||||
timestamp: entry.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Extract tool results
|
||||
const toolResults = entry.content.filter((c: any) => c.type === 'tool_result');
|
||||
for (const result of toolResults) {
|
||||
currentTurn.toolResults!.push({
|
||||
toolName: result.tool_use_id || 'unknown',
|
||||
result: result.content,
|
||||
timestamp: entry.timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assistant messages
|
||||
if (entry.type === 'assistant' && currentTurn) {
|
||||
if (!Array.isArray(entry.content)) continue;
|
||||
|
||||
const textBlocks = entry.content.filter((c: any) => c.type === 'text');
|
||||
const thinkingBlocks = entry.content.filter((c: any) => c.type === 'thinking');
|
||||
const toolUseBlocks = entry.content.filter((c: any) => c.type === 'tool_use');
|
||||
|
||||
currentTurn.assistantMessage = {
|
||||
textContent: textBlocks.map((c: any) => c.text).join('\n'),
|
||||
thinkingContent: thinkingBlocks.map((c: any) => c.thinking).join('\n'),
|
||||
toolUses: toolUseBlocks.map((t: any) => ({
|
||||
name: t.name,
|
||||
input: t.input,
|
||||
timestamp: entry.timestamp
|
||||
})),
|
||||
timestamp: entry.timestamp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Push last turn
|
||||
if (currentTurn) {
|
||||
turns.push(currentTurn);
|
||||
}
|
||||
|
||||
return turns;
|
||||
}
|
||||
|
||||
function formatTurnToMarkdown(turn: ConversationTurn): string {
|
||||
let md = '';
|
||||
|
||||
md += `## Turn ${turn.turnNumber}\n\n`;
|
||||
|
||||
// User message
|
||||
if (turn.userMessage) {
|
||||
md += `### 👤 User Request\n`;
|
||||
md += `**Time:** ${new Date(turn.userMessage.timestamp).toLocaleString()}\n\n`;
|
||||
md += '```\n';
|
||||
md += turn.userMessage.content.substring(0, 500);
|
||||
if (turn.userMessage.content.length > 500) {
|
||||
md += '\n... (truncated)';
|
||||
}
|
||||
md += '\n```\n\n';
|
||||
}
|
||||
|
||||
// Assistant response
|
||||
if (turn.assistantMessage) {
|
||||
md += `### 🤖 Assistant Response\n`;
|
||||
md += `**Time:** ${new Date(turn.assistantMessage.timestamp).toLocaleString()}\n\n`;
|
||||
|
||||
// Text content
|
||||
if (turn.assistantMessage.textContent.trim()) {
|
||||
md += '**Response:**\n```\n';
|
||||
md += turn.assistantMessage.textContent.substring(0, 500);
|
||||
if (turn.assistantMessage.textContent.length > 500) {
|
||||
md += '\n... (truncated)';
|
||||
}
|
||||
md += '\n```\n\n';
|
||||
}
|
||||
|
||||
// Thinking
|
||||
if (turn.assistantMessage.thinkingContent?.trim()) {
|
||||
md += '**Thinking:**\n```\n';
|
||||
md += turn.assistantMessage.thinkingContent.substring(0, 300);
|
||||
if (turn.assistantMessage.thinkingContent.length > 300) {
|
||||
md += '\n... (truncated)';
|
||||
}
|
||||
md += '\n```\n\n';
|
||||
}
|
||||
|
||||
// Tool uses
|
||||
if (turn.assistantMessage.toolUses.length > 0) {
|
||||
md += `**Tools Used:** ${turn.assistantMessage.toolUses.length}\n\n`;
|
||||
for (const tool of turn.assistantMessage.toolUses) {
|
||||
md += `- **${tool.name}**\n`;
|
||||
md += ` \`\`\`json\n`;
|
||||
const inputStr = JSON.stringify(tool.input, null, 2);
|
||||
md += inputStr.substring(0, 200);
|
||||
if (inputStr.length > 200) {
|
||||
md += '\n ... (truncated)';
|
||||
}
|
||||
md += '\n ```\n';
|
||||
}
|
||||
md += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Tool results summary
|
||||
if (turn.toolResults && turn.toolResults.length > 0) {
|
||||
md += `**Tool Results:** ${turn.toolResults.length} results received\n\n`;
|
||||
}
|
||||
|
||||
md += '---\n\n';
|
||||
return md;
|
||||
}
|
||||
|
||||
function formatTranscriptToMarkdown(transcriptPath: string): string {
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const turns = extractConversationTurns(parser);
|
||||
const stats = parser.getParseStats();
|
||||
const tokens = parser.getTotalTokenUsage();
|
||||
|
||||
let md = `# Transcript Context Analysis\n\n`;
|
||||
md += `**File:** ${basename(transcriptPath)}\n`;
|
||||
md += `**Parsed:** ${new Date().toLocaleString()}\n\n`;
|
||||
|
||||
md += `## Statistics\n\n`;
|
||||
md += `- Total entries: ${stats.totalLines}\n`;
|
||||
md += `- Successfully parsed: ${stats.parsedEntries}\n`;
|
||||
md += `- Failed lines: ${stats.failedLines}\n`;
|
||||
md += `- Conversation turns: ${turns.length}\n\n`;
|
||||
|
||||
md += `## Token Usage\n\n`;
|
||||
md += `- Input tokens: ${tokens.inputTokens.toLocaleString()}\n`;
|
||||
md += `- Output tokens: ${tokens.outputTokens.toLocaleString()}\n`;
|
||||
md += `- Cache creation: ${tokens.cacheCreationTokens.toLocaleString()}\n`;
|
||||
md += `- Cache read: ${tokens.cacheReadTokens.toLocaleString()}\n`;
|
||||
const totalTokens = tokens.inputTokens + tokens.outputTokens;
|
||||
md += `- Total: ${totalTokens.toLocaleString()}\n\n`;
|
||||
|
||||
md += `---\n\n`;
|
||||
md += `# Conversation Turns\n\n`;
|
||||
|
||||
// Format each turn
|
||||
for (const turn of turns.slice(0, 20)) { // Limit to first 20 turns for readability
|
||||
md += formatTurnToMarkdown(turn);
|
||||
}
|
||||
|
||||
if (turns.length > 20) {
|
||||
md += `\n_... ${turns.length - 20} more turns omitted for brevity_\n`;
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const transcriptPath = process.argv[2];
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.error('Usage: tsx scripts/format-transcript-context.ts <path-to-transcript.jsonl>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Parsing transcript: ${transcriptPath}`);
|
||||
|
||||
const markdown = formatTranscriptToMarkdown(transcriptPath);
|
||||
const outputPath = transcriptPath.replace('.jsonl', '-formatted.md');
|
||||
|
||||
writeFileSync(outputPath, markdown, 'utf-8');
|
||||
|
||||
console.log(`\nFormatted transcript written to: ${outputPath}`);
|
||||
console.log(`\nOpen with: cat "${outputPath}"\n`);
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate CHANGELOG.md from GitHub releases
|
||||
*
|
||||
* Fetches all releases from GitHub and formats them into Keep a Changelog format.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
function exec(command) {
|
||||
try {
|
||||
return execSync(command, { encoding: 'utf-8' });
|
||||
} catch (error) {
|
||||
console.error(`Error executing command: ${command}`);
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getReleases() {
|
||||
console.log('📋 Fetching releases from GitHub...');
|
||||
const releasesJson = exec('gh release list --limit 1000 --json tagName,publishedAt,name');
|
||||
const releases = JSON.parse(releasesJson);
|
||||
|
||||
// Fetch body for each release
|
||||
console.log(`📥 Fetching details for ${releases.length} releases...`);
|
||||
for (const release of releases) {
|
||||
const body = exec(`gh release view ${release.tagName} --json body --jq '.body'`).trim();
|
||||
release.body = body;
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
function formatDate(isoDate) {
|
||||
const date = new Date(isoDate);
|
||||
return date.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
}
|
||||
|
||||
function cleanReleaseBody(body) {
|
||||
// Remove the "Generated with Claude Code" footer
|
||||
return body
|
||||
.replace(/🤖 Generated with \[Claude Code\].*$/s, '')
|
||||
.replace(/---\n*$/s, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractVersion(tagName) {
|
||||
// Remove 'v' prefix from tag name
|
||||
return tagName.replace(/^v/, '');
|
||||
}
|
||||
|
||||
function generateChangelog(releases) {
|
||||
console.log(`📝 Generating CHANGELOG.md from ${releases.length} releases...`);
|
||||
|
||||
const lines = [
|
||||
'# Changelog',
|
||||
'',
|
||||
'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/).',
|
||||
'',
|
||||
];
|
||||
|
||||
// Sort releases by date (newest first)
|
||||
releases.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
||||
|
||||
for (const release of releases) {
|
||||
const version = extractVersion(release.tagName);
|
||||
const date = formatDate(release.publishedAt);
|
||||
const body = cleanReleaseBody(release.body);
|
||||
|
||||
// Add version header
|
||||
lines.push(`## [${version}] - ${date}`);
|
||||
lines.push('');
|
||||
|
||||
// Add release body
|
||||
if (body) {
|
||||
// Remove the initial markdown heading if it exists (e.g., "## v5.5.0 (2025-11-11)")
|
||||
const bodyWithoutHeader = body.replace(/^##?\s+v?[\d.]+.*?\n\n?/m, '');
|
||||
lines.push(bodyWithoutHeader);
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('🔧 Generating CHANGELOG.md from GitHub releases...\n');
|
||||
|
||||
const releases = getReleases();
|
||||
|
||||
if (releases.length === 0) {
|
||||
console.log('⚠️ No releases found');
|
||||
return;
|
||||
}
|
||||
|
||||
const changelog = generateChangelog(releases);
|
||||
|
||||
writeFileSync('CHANGELOG.md', changelog, 'utf-8');
|
||||
|
||||
console.log('\n✅ CHANGELOG.md generated successfully!');
|
||||
console.log(` ${releases.length} releases processed`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Test script for TranscriptParser
|
||||
* Validates data extraction from Claude Code transcript JSONL files
|
||||
*
|
||||
* Usage: npx tsx scripts/test-transcript-parser.ts <path-to-transcript.jsonl>
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
function formatTokens(num: number): string {
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
function formatPercentage(num: number): string {
|
||||
return `${(num * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npx tsx scripts/test-transcript-parser.ts <path-to-transcript.jsonl>');
|
||||
console.error('\nExample: npx tsx scripts/test-transcript-parser.ts ~/.cache/claude-code/transcripts/latest.jsonl');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const transcriptPath = resolve(args[0]);
|
||||
|
||||
if (!existsSync(transcriptPath)) {
|
||||
console.error(`Error: Transcript file not found: ${transcriptPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n🔍 Parsing transcript: ${transcriptPath}\n`);
|
||||
|
||||
try {
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
|
||||
// Get parse statistics
|
||||
const stats = parser.getParseStats();
|
||||
|
||||
console.log('📊 Parse Statistics:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`Total lines: ${stats.totalLines}`);
|
||||
console.log(`Parsed entries: ${stats.parsedEntries}`);
|
||||
console.log(`Failed lines: ${stats.failedLines}`);
|
||||
console.log(`Failure rate: ${formatPercentage(stats.failureRate)}`);
|
||||
console.log();
|
||||
|
||||
console.log('📋 Entries by Type:');
|
||||
console.log('─'.repeat(60));
|
||||
for (const [type, count] of Object.entries(stats.entriesByType)) {
|
||||
console.log(` ${type.padEnd(20)} ${count}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Show parse errors if any
|
||||
if (stats.failedLines > 0) {
|
||||
console.log('❌ Parse Errors:');
|
||||
console.log('─'.repeat(60));
|
||||
const errors = parser.getParseErrors();
|
||||
errors.slice(0, 5).forEach(err => {
|
||||
console.log(` Line ${err.lineNumber}: ${err.error}`);
|
||||
});
|
||||
if (errors.length > 5) {
|
||||
console.log(` ... and ${errors.length - 5} more errors`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Test data extraction methods
|
||||
console.log('💬 Message Extraction:');
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
const lastUserMessage = parser.getLastUserMessage();
|
||||
console.log(`Last user message: ${lastUserMessage ? `"${lastUserMessage.substring(0, 100)}..."` : '(none)'}`);
|
||||
console.log();
|
||||
|
||||
const lastAssistantMessage = parser.getLastAssistantMessage();
|
||||
console.log(`Last assistant message: ${lastAssistantMessage ? `"${lastAssistantMessage.substring(0, 100)}..."` : '(none)'}`);
|
||||
console.log();
|
||||
|
||||
// Token usage
|
||||
const tokenUsage = parser.getTotalTokenUsage();
|
||||
console.log('💰 Token Usage:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`Input tokens: ${formatTokens(tokenUsage.inputTokens)}`);
|
||||
console.log(`Output tokens: ${formatTokens(tokenUsage.outputTokens)}`);
|
||||
console.log(`Cache creation tokens: ${formatTokens(tokenUsage.cacheCreationTokens)}`);
|
||||
console.log(`Cache read tokens: ${formatTokens(tokenUsage.cacheReadTokens)}`);
|
||||
console.log(`Total tokens: ${formatTokens(tokenUsage.inputTokens + tokenUsage.outputTokens)}`);
|
||||
console.log();
|
||||
|
||||
// Tool use history
|
||||
const toolUses = parser.getToolUseHistory();
|
||||
console.log('🔧 Tool Use History:');
|
||||
console.log('─'.repeat(60));
|
||||
if (toolUses.length > 0) {
|
||||
console.log(`Total tool uses: ${toolUses.length}\n`);
|
||||
|
||||
// Group by tool name
|
||||
const toolCounts = toolUses.reduce((acc, tool) => {
|
||||
acc[tool.name] = (acc[tool.name] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
console.log('Tools used:');
|
||||
for (const [name, count] of Object.entries(toolCounts).sort((a, b) => b[1] - a[1])) {
|
||||
console.log(` ${name.padEnd(30)} ${count}x`);
|
||||
}
|
||||
} else {
|
||||
console.log('(no tool uses found)');
|
||||
}
|
||||
console.log();
|
||||
|
||||
// System entries
|
||||
const systemEntries = parser.getSystemEntries();
|
||||
if (systemEntries.length > 0) {
|
||||
console.log('⚠️ System Entries:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`Found ${systemEntries.length} system entries`);
|
||||
systemEntries.slice(0, 3).forEach(entry => {
|
||||
console.log(` [${entry.level || 'info'}] ${entry.content.substring(0, 80)}...`);
|
||||
});
|
||||
if (systemEntries.length > 3) {
|
||||
console.log(` ... and ${systemEntries.length - 3} more`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Summary entries
|
||||
const summaryEntries = parser.getSummaryEntries();
|
||||
if (summaryEntries.length > 0) {
|
||||
console.log('📝 Summary Entries:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`Found ${summaryEntries.length} summary entries`);
|
||||
summaryEntries.forEach((entry, i) => {
|
||||
console.log(`\nSummary ${i + 1}:`);
|
||||
console.log(entry.summary.substring(0, 200) + '...');
|
||||
});
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Queue operations
|
||||
const queueOps = parser.getQueueOperationEntries();
|
||||
if (queueOps.length > 0) {
|
||||
console.log('🔄 Queue Operations:');
|
||||
console.log('─'.repeat(60));
|
||||
const enqueues = queueOps.filter(op => op.operation === 'enqueue').length;
|
||||
const dequeues = queueOps.filter(op => op.operation === 'dequeue').length;
|
||||
console.log(`Enqueue operations: ${enqueues}`);
|
||||
console.log(`Dequeue operations: ${dequeues}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('✅ Validation complete!\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error parsing transcript:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Transcript to Markdown - Complete 1:1 representation
|
||||
* Shows ALL available context data from a Claude Code transcript
|
||||
*/
|
||||
|
||||
import { TranscriptParser } from '../src/utils/transcript-parser.js';
|
||||
import type { UserTranscriptEntry, AssistantTranscriptEntry, ToolResultContent } from '../types/transcript.js';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { basename } from 'path';
|
||||
|
||||
const transcriptPath = process.argv[2];
|
||||
const maxTurns = process.argv[3] ? parseInt(process.argv[3]) : 20;
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.error('Usage: tsx scripts/transcript-to-markdown.ts <path-to-transcript.jsonl> [max-turns]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate string to max length, adding ellipsis if needed
|
||||
*/
|
||||
function truncate(str: string, maxLen: number = 500): string {
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.substring(0, maxLen) + '\n... [truncated]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tool result content for display
|
||||
*/
|
||||
function formatToolResult(result: ToolResultContent): string {
|
||||
if (typeof result.content === 'string') {
|
||||
// Try to parse as JSON for better formatting
|
||||
try {
|
||||
const parsed = JSON.parse(result.content);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return truncate(result.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(result.content)) {
|
||||
// Handle array of content items - extract text and parse if JSON
|
||||
const formatted = result.content.map((item: any) => {
|
||||
if (item.type === 'text' && item.text) {
|
||||
try {
|
||||
const parsed = JSON.parse(item.text);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return item.text;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(item, null, 2);
|
||||
}).join('\n\n');
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
return '[unknown result type]';
|
||||
}
|
||||
|
||||
const parser = new TranscriptParser(transcriptPath);
|
||||
const entries = parser.getAllEntries();
|
||||
const stats = parser.getParseStats();
|
||||
|
||||
let output = `# Transcript: ${basename(transcriptPath)}\n\n`;
|
||||
output += `**Generated:** ${new Date().toLocaleString()}\n`;
|
||||
output += `**Total Entries:** ${stats.parsedEntries}\n`;
|
||||
output += `**Entry Types:** ${JSON.stringify(stats.entriesByType, null, 2)}\n`;
|
||||
output += `**Showing:** First ${maxTurns} conversation turns\n\n`;
|
||||
|
||||
output += `---\n\n`;
|
||||
|
||||
let turnNumber = 0;
|
||||
let inTurn = false;
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip summary and file-history-snapshot entries
|
||||
if (entry.type === 'summary' || entry.type === 'file-history-snapshot') continue;
|
||||
|
||||
// USER MESSAGE
|
||||
if (entry.type === 'user') {
|
||||
const userEntry = entry as UserTranscriptEntry;
|
||||
|
||||
turnNumber++;
|
||||
if (turnNumber > maxTurns) break;
|
||||
|
||||
inTurn = true;
|
||||
output += `## Turn ${turnNumber}\n\n`;
|
||||
output += `### 👤 User\n`;
|
||||
output += `**Timestamp:** ${userEntry.timestamp}\n`;
|
||||
output += `**UUID:** ${userEntry.uuid}\n`;
|
||||
output += `**Session ID:** ${userEntry.sessionId}\n`;
|
||||
output += `**CWD:** ${userEntry.cwd}\n\n`;
|
||||
|
||||
// Extract user message text
|
||||
if (typeof userEntry.message.content === 'string') {
|
||||
output += userEntry.message.content + '\n\n';
|
||||
} else if (Array.isArray(userEntry.message.content)) {
|
||||
const textBlocks = userEntry.message.content.filter((c) => c.type === 'text');
|
||||
if (textBlocks.length > 0) {
|
||||
const text = textBlocks.map((b: any) => b.text).join('\n');
|
||||
output += text + '\n\n';
|
||||
}
|
||||
|
||||
// Show ACTUAL tool results with their data
|
||||
const toolResults = userEntry.message.content.filter((c): c is ToolResultContent => c.type === 'tool_result');
|
||||
if (toolResults.length > 0) {
|
||||
output += `**Tool Results Submitted (${toolResults.length}):**\n\n`;
|
||||
for (const result of toolResults) {
|
||||
output += `- **Tool Use ID:** \`${result.tool_use_id}\`\n`;
|
||||
if (result.is_error) {
|
||||
output += ` **ERROR:**\n`;
|
||||
}
|
||||
output += ` \`\`\`json\n`;
|
||||
output += ` ${formatToolResult(result)}\n`;
|
||||
output += ` \`\`\`\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ASSISTANT MESSAGE
|
||||
if (entry.type === 'assistant' && inTurn) {
|
||||
const assistantEntry = entry as AssistantTranscriptEntry;
|
||||
|
||||
output += `### 🤖 Assistant\n`;
|
||||
output += `**Timestamp:** ${assistantEntry.timestamp}\n`;
|
||||
output += `**UUID:** ${assistantEntry.uuid}\n`;
|
||||
output += `**Model:** ${assistantEntry.message.model}\n`;
|
||||
output += `**Stop Reason:** ${assistantEntry.message.stop_reason || 'N/A'}\n\n`;
|
||||
|
||||
if (!Array.isArray(assistantEntry.message.content)) {
|
||||
output += `*[No content]*\n\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = assistantEntry.message.content;
|
||||
|
||||
// 1. Thinking blocks (show first, as they happen first in reasoning)
|
||||
const thinkingBlocks = content.filter((c) => c.type === 'thinking');
|
||||
if (thinkingBlocks.length > 0) {
|
||||
output += `**💭 Thinking:**\n\n`;
|
||||
for (const block of thinkingBlocks) {
|
||||
const thinking = (block as any).thinking;
|
||||
// Format thinking with proper line breaks and indentation
|
||||
const formattedThinking = thinking
|
||||
.split('\n')
|
||||
.map((line: string) => line.trimEnd())
|
||||
.join('\n');
|
||||
|
||||
output += '> ';
|
||||
output += formattedThinking.replace(/\n/g, '\n> ');
|
||||
output += '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Text responses
|
||||
const textBlocks = content.filter((c) => c.type === 'text');
|
||||
if (textBlocks.length > 0) {
|
||||
output += `**Response:**\n\n`;
|
||||
for (const block of textBlocks) {
|
||||
output += (block as any).text + '\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Tool uses - show complete input
|
||||
const toolUseBlocks = content.filter((c) => c.type === 'tool_use');
|
||||
if (toolUseBlocks.length > 0) {
|
||||
output += `**🔧 Tools Used (${toolUseBlocks.length}):**\n\n`;
|
||||
for (const tool of toolUseBlocks) {
|
||||
const t = tool as any;
|
||||
output += `- **${t.name}** (ID: \`${t.id}\`)\n`;
|
||||
output += ` \`\`\`json\n`;
|
||||
output += ` ${JSON.stringify(t.input, null, 2)}\n`;
|
||||
output += ` \`\`\`\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Token usage
|
||||
if (assistantEntry.message.usage) {
|
||||
const usage = assistantEntry.message.usage;
|
||||
output += `**📊 Token Usage:**\n`;
|
||||
output += `- Input: ${usage.input_tokens || 0}\n`;
|
||||
output += `- Output: ${usage.output_tokens || 0}\n`;
|
||||
if (usage.cache_creation_input_tokens) {
|
||||
output += `- Cache creation: ${usage.cache_creation_input_tokens}\n`;
|
||||
}
|
||||
if (usage.cache_read_input_tokens) {
|
||||
output += `- Cache read: ${usage.cache_read_input_tokens}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
output += `---\n\n`;
|
||||
inTurn = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (turnNumber < (stats.entriesByType['user'] || 0)) {
|
||||
output += `\n*... ${(stats.entriesByType['user'] || 0) - turnNumber} more turns not shown*\n`;
|
||||
}
|
||||
|
||||
// Write output
|
||||
const outputPath = transcriptPath.replace('.jsonl', '-complete.md');
|
||||
writeFileSync(outputPath, output, 'utf-8');
|
||||
|
||||
console.log(`\nComplete transcript written to: ${outputPath}`);
|
||||
console.log(`Turns shown: ${Math.min(turnNumber, maxTurns)} of ${stats.entriesByType['user'] || 0}\n`);
|
||||
@@ -1,340 +0,0 @@
|
||||
# Claude-Mem Source Code Analysis Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Analyzed all 58 files in the `/Users/alexnewman/Scripts/claude-mem/src` directory. This report categorizes each file by purpose, usage status, and documents the cleanup of dead code files.
|
||||
|
||||
**Cleanup Status**: ✅ **7 dead code files successfully removed** (51 files remaining)
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/bin** (2 files)
|
||||
|
||||
### `src/bin/cleanup-duplicates.ts`
|
||||
- **Purpose**: Utility script to remove duplicate observations and summaries from the database
|
||||
- **Used?**: **No** - Standalone CLI utility, not imported anywhere
|
||||
- **Notes**: Maintenance tool for database cleanup. Keeps earliest entry (MIN(id)) for each duplicate group. Not part of the runtime system.
|
||||
|
||||
### `src/bin/import-xml-observations.ts`
|
||||
- **Purpose**: Import tool to restore XML observations back into SQLite database
|
||||
- **Used?**: **No** - Standalone CLI utility, not imported anywhere
|
||||
- **Notes**: Data migration tool. Parses XML timestamps and matches them to transcript files. Not part of the runtime system.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/hooks** (7 files, 1 deleted)
|
||||
|
||||
### `src/hooks/cleanup-hook.ts`
|
||||
- **Purpose**: SessionEnd hook that marks sessions as completed and notifies worker
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/cleanup-hook.js`, registered in `plugin/hooks/hooks.json`
|
||||
- **Notes**: Core hook, actively used
|
||||
|
||||
### `src/hooks/context-hook.ts`
|
||||
- **Purpose**: SessionStart hook that injects recent observations into Claude Code sessions
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/context-hook.js`, registered in hooks.json, also called by user-message-hook
|
||||
- **Notes**: Core hook displaying context timeline with progressive disclosure (index view)
|
||||
|
||||
### `src/hooks/hook-response.ts`
|
||||
- **Purpose**: Utility module for creating standardized hook responses
|
||||
- **Used?**: **Yes** - Imported by new-hook.ts, save-hook.ts, summary-hook.ts
|
||||
- **Notes**: Shared helper for hook JSON output
|
||||
|
||||
### `src/hooks/index.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Export barrel for hooks module
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Exports were outdated (referenced `context.js`, `save.js`, etc. which don't exist). The actual hooks are built as standalone executables, not imported as modules.
|
||||
|
||||
### `src/hooks/new-hook.ts`
|
||||
- **Purpose**: UserPromptSubmit hook that creates session records and saves raw user prompts
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/new-hook.js`, registered in hooks.json
|
||||
- **Notes**: Core hook, actively used
|
||||
|
||||
### `src/hooks/save-hook.ts`
|
||||
- **Purpose**: PostToolUse hook that captures tool executions and sends to worker
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/save-hook.js`, registered in hooks.json
|
||||
- **Notes**: Core hook, actively used
|
||||
|
||||
### `src/hooks/summary-hook.ts`
|
||||
- **Purpose**: Stop hook that requests session summaries from worker
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/summary-hook.js`, registered in hooks.json
|
||||
- **Notes**: Core hook, actively used
|
||||
|
||||
### `src/hooks/user-message-hook.ts`
|
||||
- **Purpose**: SessionStart hook that displays context to users via stderr
|
||||
- **Used?**: **Yes** - Built to `plugin/scripts/user-message-hook.js`, registered in hooks.json
|
||||
- **Notes**: Runs context-hook via execSync to show colored output. Active hook.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/sdk** (3 files, 1 deleted)
|
||||
|
||||
### `src/sdk/index.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Export barrel for SDK module
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Exported SDK functions but nothing imported from this module directly. Files import directly from prompts.ts and parser.ts instead.
|
||||
|
||||
### `src/sdk/parser.test.ts`
|
||||
- **Purpose**: Regression tests for XML parsing (v4.2.5 and v4.2.6 bugfixes)
|
||||
- **Used?**: **No** - Test file, not part of runtime
|
||||
- **Notes**: Test suite with 18 tests validating observation/summary parsing edge cases
|
||||
|
||||
### `src/sdk/parser.ts`
|
||||
- **Purpose**: XML parser for observation and summary blocks from SDK responses
|
||||
- **Used?**: **Yes** - Imported by worker-service.ts
|
||||
- **Notes**: Core parsing logic, actively used
|
||||
|
||||
### `src/sdk/prompts.ts`
|
||||
- **Purpose**: Prompt builders for Claude Agent SDK
|
||||
- **Used?**: **Yes** - Imported by worker-service.ts
|
||||
- **Notes**: Generates init, observation, and summary prompts for SDK agent
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/servers** (1 file)
|
||||
|
||||
### `src/servers/search-server.ts`
|
||||
- **Purpose**: MCP search server exposing 9 search tools with hybrid Chroma + FTS5 search
|
||||
- **Used?**: **Yes** - Built to `plugin/search-server.mjs`, configured in `plugin/.mcp.json`
|
||||
- **Notes**: 1,782 lines. Core search server providing progressive disclosure search tools.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/services/sqlite** (6 files)
|
||||
|
||||
### `src/services/sqlite/Database.ts`
|
||||
- **Purpose**: Base database class with better-sqlite3
|
||||
- **Used?**: **Yes** - Imported by SessionStore.ts, SessionSearch.ts, index.ts
|
||||
- **Notes**: Foundation class for SQLite operations
|
||||
|
||||
### `src/services/sqlite/index.ts`
|
||||
- **Purpose**: Export barrel for sqlite module
|
||||
- **Used?**: **Yes** - Imported by storage.ts
|
||||
- **Notes**: Exports all store types and utilities
|
||||
|
||||
### `src/services/sqlite/migrations.ts`
|
||||
- **Purpose**: Database migration function for schema changes
|
||||
- **Used?**: **Yes** - Imported by index.ts
|
||||
- **Notes**: Handles SQLite schema migrations
|
||||
|
||||
### `src/services/sqlite/SessionSearch.ts`
|
||||
- **Purpose**: FTS5 full-text search implementation
|
||||
- **Used?**: **Yes** - Imported by search-server.ts
|
||||
- **Notes**: Provides searchObservations, searchSessions, searchUserPrompts, findByConcept, findByFile, findByType
|
||||
|
||||
### `src/services/sqlite/SessionStore.ts`
|
||||
- **Purpose**: CRUD operations for sessions, observations, summaries, user prompts
|
||||
- **Used?**: **Yes** - Imported by all hooks, worker-service.ts, search-server.ts, bin utilities
|
||||
- **Notes**: Core database store, heavily used throughout the system
|
||||
|
||||
### `src/services/sqlite/types.ts`
|
||||
- **Purpose**: TypeScript type definitions for database records
|
||||
- **Used?**: **Yes** - Imported by search-server.ts
|
||||
- **Notes**: Defines ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/services/sync** (1 file)
|
||||
|
||||
### `src/services/sync/ChromaSync.ts`
|
||||
- **Purpose**: Vector database synchronization service for semantic search
|
||||
- **Used?**: **Yes** - Imported by worker-service.ts
|
||||
- **Notes**: 737 lines. Manages Chroma vector embeddings for observations, summaries, and prompts. Critical for hybrid search.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/services** (1 file)
|
||||
|
||||
### `src/services/worker-service.ts`
|
||||
- **Purpose**: Express HTTP server managed by PM2, handles SDK agent sessions
|
||||
- **Used?**: **Yes** - Built to `plugin/worker-service.cjs`, started by PM2
|
||||
- **Notes**: 1,173 lines. Core worker service with 14 HTTP/SSE endpoints. Serves viewer UI, manages SDK sessions, broadcasts SSE updates.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/shared** (2 files, 3 deleted)
|
||||
|
||||
### `src/shared/config.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Package configuration (name, version, description)
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Read from package.json but no code used these exports. The actual version reading happens inline in worker-service.ts.
|
||||
|
||||
### `src/shared/paths.ts`
|
||||
- **Purpose**: Path utilities and directory management
|
||||
- **Used?**: **Yes** - Imported by search-server.ts
|
||||
- **Notes**: Provides VECTOR_DB_DIR and other path constants. Actively used.
|
||||
|
||||
### `src/shared/storage.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Unified storage provider interface (SQLite abstraction layer)
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Defined IStorageProvider interface and SQLiteStorageProvider but nothing used this abstraction. Direct SessionStore usage is preferred.
|
||||
|
||||
### `src/shared/types.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Core type definitions (Settings interface)
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Defined Settings interface but no code imported it. Settings are read/written as raw JSON objects.
|
||||
|
||||
### `src/shared/worker-utils.ts`
|
||||
- **Purpose**: Worker health checks and PM2 management
|
||||
- **Used?**: **Yes** - Imported by context-hook.ts, new-hook.ts, save-hook.ts, summary-hook.ts, cleanup-hook.ts, user-message-hook.ts
|
||||
- **Notes**: Core utility, actively used by all hooks
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/utils** (1 file, 2 deleted)
|
||||
|
||||
### `src/utils/logger.ts`
|
||||
- **Purpose**: Structured logging with correlation IDs and data flow tracking
|
||||
- **Used?**: **Yes** - Imported by parser.ts, save-hook.ts, summary-hook.ts, worker-service.ts
|
||||
- **Notes**: Core logger, actively used
|
||||
|
||||
### `src/utils/platform.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Platform-specific utilities for Windows/Unix compatibility
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Provided installUv(), getShellConfigPaths(), getAliasDefinition() but nothing used these. Likely leftover from earlier installer/setup code.
|
||||
|
||||
### `src/utils/usage-logger.ts` 🗑️ **DELETED**
|
||||
- **Purpose**: Usage data logger for API cost tracking (JSONL files)
|
||||
- **Used?**: **No** - Not imported anywhere
|
||||
- **Notes**: **DELETED**. Defined UsageLogger class but it was never instantiated. Usage tracking may be handled differently now.
|
||||
|
||||
---
|
||||
|
||||
## **Directory: src/ui** (24 files + assets)
|
||||
|
||||
### `src/ui/claude-mem-logo-for-dark-mode.webp`
|
||||
- **Purpose**: Logo asset for dark mode
|
||||
- **Used?**: **Yes** - Referenced in Header.tsx, bundled into viewer.html
|
||||
- **Notes**: Web UI asset
|
||||
|
||||
### `src/ui/claude-mem-logomark.webp`
|
||||
- **Purpose**: Logomark asset
|
||||
- **Used?**: **Yes** - Referenced in Header.tsx, bundled into viewer.html
|
||||
- **Notes**: Web UI asset
|
||||
|
||||
### `src/ui/viewer-template.html`
|
||||
- **Purpose**: HTML template for viewer UI
|
||||
- **Used?**: **Yes** - Build process uses this to generate plugin/ui/viewer.html
|
||||
- **Notes**: Build artifact template
|
||||
|
||||
### `src/ui/viewer/App.tsx`
|
||||
- **Purpose**: Root React component for viewer UI
|
||||
- **Used?**: **Yes** - Entry point for viewer, imported by index.tsx
|
||||
- **Notes**: Main app component
|
||||
|
||||
### `src/ui/viewer/index.tsx`
|
||||
- **Purpose**: React app entry point
|
||||
- **Used?**: **Yes** - Built by esbuild into viewer-bundle.js
|
||||
- **Notes**: Mounts React app
|
||||
|
||||
### `src/ui/viewer/types.ts`
|
||||
- **Purpose**: TypeScript types for viewer UI
|
||||
- **Used?**: **Yes** - Imported by multiple viewer components
|
||||
- **Notes**: Type definitions for Observation, Summary, UserPrompt, etc.
|
||||
|
||||
### **src/ui/viewer/assets/fonts/** (2 files)
|
||||
- `monaspace-radon-var.woff` and `monaspace-radon-var.woff2`
|
||||
- **Purpose**: Monaspace Radon font files for viewer UI
|
||||
- **Used?**: **Yes** - Embedded in viewer.html via esbuild
|
||||
- **Notes**: Font assets
|
||||
|
||||
### **src/ui/viewer/components/** (8 files)
|
||||
All actively used by App.tsx:
|
||||
- `ErrorBoundary.tsx` - Error boundary wrapper
|
||||
- `Feed.tsx` - Infinite scroll feed component
|
||||
- `Header.tsx` - Top navigation with project selector, stats, settings
|
||||
- `ObservationCard.tsx` - Observation display card
|
||||
- `PromptCard.tsx` - User prompt display card
|
||||
- `Sidebar.tsx` - Project filtering sidebar
|
||||
- `SummaryCard.tsx` - Session summary display card
|
||||
- `ThemeToggle.tsx` - Light/dark mode toggle
|
||||
|
||||
### **src/ui/viewer/constants/** (4 files)
|
||||
All actively used by viewer components:
|
||||
- `api.ts` - API endpoint URLs
|
||||
- `settings.ts` - Default settings constants
|
||||
- `timing.ts` - Timing constants (reconnect delays, polling intervals)
|
||||
- `ui.ts` - UI constants (page sizes, etc.)
|
||||
|
||||
### **src/ui/viewer/hooks/** (5 files)
|
||||
All actively used by viewer components:
|
||||
- `usePagination.ts` - Infinite scroll pagination hook
|
||||
- `useSSE.ts` - Server-sent events hook for real-time updates
|
||||
- `useSettings.ts` - Settings management hook
|
||||
- `useStats.ts` - Worker stats fetching hook
|
||||
- `useTheme.ts` - Theme (light/dark) management hook
|
||||
|
||||
### **src/ui/viewer/utils/** (2 files)
|
||||
All actively used by viewer components:
|
||||
- `data.ts` - Data merging and deduplication utilities
|
||||
- `formatters.ts` - Date/time formatting utilities
|
||||
|
||||
---
|
||||
|
||||
## Dead Code Summary
|
||||
|
||||
### **🗑️ Deleted Files** (7 files - all removed)
|
||||
1. **src/hooks/index.ts** ✅ DELETED - Outdated export barrel, referenced non-existent files
|
||||
2. **src/shared/config.ts** ✅ DELETED - Package config not used anywhere, version read inline instead
|
||||
3. **src/shared/storage.ts** ✅ DELETED - Abstraction layer not used, direct SessionStore usage preferred
|
||||
4. **src/shared/types.ts** ✅ DELETED - Settings interface not imported anywhere
|
||||
5. **src/sdk/index.ts** ✅ DELETED - Export barrel, but imports happened directly from parser/prompts instead
|
||||
6. **src/utils/platform.ts** ✅ DELETED - Platform utilities not used, legacy installer code
|
||||
7. **src/utils/usage-logger.ts** ✅ DELETED - UsageLogger class never instantiated
|
||||
|
||||
### **Utility/Maintenance Scripts** (not dead, just not runtime code) (2 files)
|
||||
8. **src/bin/cleanup-duplicates.ts** - Maintenance CLI tool
|
||||
9. **src/bin/import-xml-observations.ts** - Data migration CLI tool
|
||||
|
||||
### **Test Files** (not dead, just not runtime code) (1 file)
|
||||
10. **src/sdk/parser.test.ts** - Regression test suite
|
||||
|
||||
---
|
||||
|
||||
## File Count Summary
|
||||
|
||||
- **Total files** (before cleanup): 58
|
||||
- **Total files** (after cleanup): **51** ✅
|
||||
- **Deleted dead code**: **7 files** 🗑️
|
||||
- **Actively used at runtime**: 43 files
|
||||
- **Utility/maintenance scripts**: 2 files
|
||||
- **Test files**: 1 file
|
||||
- **Build templates**: 1 file (viewer-template.html)
|
||||
- **Assets**: 4 files (2 logos, 2 fonts)
|
||||
|
||||
---
|
||||
|
||||
## Cleanup Actions Completed ✅
|
||||
|
||||
1. **✅ Removed all dead code** (7 files deleted):
|
||||
- ✅ Deleted `src/hooks/index.ts`
|
||||
- ✅ Deleted `src/shared/config.ts`
|
||||
- ✅ Deleted `src/shared/storage.ts`
|
||||
- ✅ Deleted `src/shared/types.ts`
|
||||
- ✅ Deleted `src/sdk/index.ts`
|
||||
- ✅ Deleted `src/utils/platform.ts`
|
||||
- ✅ Deleted `src/utils/usage-logger.ts`
|
||||
|
||||
2. **✅ Kept utility scripts** for maintenance purposes:
|
||||
- ✅ Kept `src/bin/cleanup-duplicates.ts`
|
||||
- ✅ Kept `src/bin/import-xml-observations.ts`
|
||||
|
||||
3. **✅ Kept test files** for regression testing:
|
||||
- ✅ Kept `src/sdk/parser.test.ts`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Build and test** to ensure no broken imports:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Run TypeScript diagnostics** to catch any missing references:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
3. **Commit the cleanup**:
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Remove dead code files"
|
||||
```
|
||||
@@ -1,78 +0,0 @@
|
||||
src
|
||||
├── bin
|
||||
│ ├── cleanup-duplicates.ts
|
||||
│ └── import-xml-observations.ts
|
||||
├── hooks
|
||||
│ ├── cleanup-hook.ts
|
||||
│ ├── context-hook.ts
|
||||
│ ├── hook-response.ts
|
||||
│ ├── index.ts
|
||||
│ ├── new-hook.ts
|
||||
│ ├── save-hook.ts
|
||||
│ ├── summary-hook.ts
|
||||
│ └── user-message-hook.ts
|
||||
├── sdk
|
||||
│ ├── index.ts
|
||||
│ ├── parser.test.ts
|
||||
│ ├── parser.ts
|
||||
│ └── prompts.ts
|
||||
├── servers
|
||||
│ └── search-server.ts
|
||||
├── services
|
||||
│ ├── sqlite
|
||||
│ │ ├── Database.ts
|
||||
│ │ ├── SessionSearch.ts
|
||||
│ │ ├── SessionStore.ts
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ ├── sync
|
||||
│ │ └── ChromaSync.ts
|
||||
│ └── worker-service.ts
|
||||
├── shared
|
||||
│ ├── config.ts
|
||||
│ ├── paths.ts
|
||||
│ ├── storage.ts
|
||||
│ ├── types.ts
|
||||
│ └── worker-utils.ts
|
||||
├── ui
|
||||
│ ├── claude-mem-logo-for-dark-mode.webp
|
||||
│ ├── claude-mem-logomark.webp
|
||||
│ ├── viewer
|
||||
│ │ ├── App.tsx
|
||||
│ │ ├── assets
|
||||
│ │ │ └── fonts
|
||||
│ │ │ ├── monaspace-radon-var.woff
|
||||
│ │ │ └── monaspace-radon-var.woff2
|
||||
│ │ ├── components
|
||||
│ │ │ ├── ErrorBoundary.tsx
|
||||
│ │ │ ├── Feed.tsx
|
||||
│ │ │ ├── Header.tsx
|
||||
│ │ │ ├── ObservationCard.tsx
|
||||
│ │ │ ├── PromptCard.tsx
|
||||
│ │ │ ├── Sidebar.tsx
|
||||
│ │ │ ├── SummaryCard.tsx
|
||||
│ │ │ └── ThemeToggle.tsx
|
||||
│ │ ├── constants
|
||||
│ │ │ ├── api.ts
|
||||
│ │ │ ├── settings.ts
|
||||
│ │ │ ├── timing.ts
|
||||
│ │ │ └── ui.ts
|
||||
│ │ ├── hooks
|
||||
│ │ │ ├── usePagination.ts
|
||||
│ │ │ ├── useSSE.ts
|
||||
│ │ │ ├── useSettings.ts
|
||||
│ │ │ ├── useStats.ts
|
||||
│ │ │ └── useTheme.ts
|
||||
│ │ ├── index.tsx
|
||||
│ │ ├── types.ts
|
||||
│ │ └── utils
|
||||
│ │ ├── data.ts
|
||||
│ │ └── formatters.ts
|
||||
│ └── viewer-template.html
|
||||
└── utils
|
||||
├── logger.ts
|
||||
├── platform.ts
|
||||
└── usage-logger.ts
|
||||
|
||||
18 directories, 58 files
|
||||
@@ -71,6 +71,7 @@ interface Observation {
|
||||
concepts: string | null;
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
discovery_tokens: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
@@ -130,12 +131,6 @@ function formatDate(dateStr: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Estimate token count for text
|
||||
function estimateTokens(text: string | null): number {
|
||||
if (!text) return 0;
|
||||
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
||||
}
|
||||
|
||||
// Helper: Convert absolute paths to relative paths
|
||||
function toRelativePath(filePath: string, cwd: string): string {
|
||||
if (path.isAbsolute(filePath)) {
|
||||
@@ -154,24 +149,6 @@ function renderSummaryField(label: string, value: string | null, color: string,
|
||||
return [`**${label}**: ${value}`, ''];
|
||||
}
|
||||
|
||||
// Helper: Get all observations for given sessions
|
||||
function getObservations(db: SessionStore, sessionIds: string[]): Observation[] {
|
||||
if (sessionIds.length === 0) return [];
|
||||
|
||||
const placeholders = sessionIds.map(() => '?').join(',');
|
||||
const observations = db.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
WHERE sdk_session_id IN (${placeholders})
|
||||
ORDER BY created_at_epoch DESC
|
||||
`).all(...sessionIds) as Observation[];
|
||||
|
||||
return observations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context Hook Main Logic
|
||||
*/
|
||||
@@ -187,7 +164,7 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
const allObservations = db.db.prepare(`
|
||||
SELECT
|
||||
id, sdk_session_id, type, title, subtitle, narrative,
|
||||
facts, concepts, files_read, files_modified,
|
||||
facts, concepts, files_read, files_modified, discovery_tokens,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
@@ -239,25 +216,74 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
if (timelineObs.length > 0) {
|
||||
// Legend/Key
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | 🧠 decision${colors.reset}`);
|
||||
output.push('');
|
||||
output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | 🧠 decision`);
|
||||
output.push('');
|
||||
output.push(`**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | ⚖️ decision`);
|
||||
}
|
||||
output.push('');
|
||||
|
||||
// Progressive Disclosure Usage Instructions
|
||||
// Column Key
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}💡 Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${colors.reset}`);
|
||||
output.push(`${colors.dim} → Use MCP search tools to fetch full observation details on-demand (Layer 2)${colors.reset}`);
|
||||
output.push(`${colors.dim} → Prefer searching observations over re-reading code for past decisions and learnings${colors.reset}`);
|
||||
output.push(`${colors.dim} → Critical types (🔴 bugfix, 🧠 decision) often worth fetching immediately${colors.reset}`);
|
||||
output.push(`${colors.bright}💡 Column Key${colors.reset}`);
|
||||
output.push(`${colors.dim} Read: Tokens to read this observation (cost to learn it now)${colors.reset}`);
|
||||
output.push(`${colors.dim} Work: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)${colors.reset}`);
|
||||
} else {
|
||||
output.push(`💡 **Column Key**:`);
|
||||
output.push(`- **Read**: Tokens to read this observation (cost to learn it now)`);
|
||||
output.push(`- **Work**: Tokens spent on work that produced this record (🔍 research, 🛠️ building, ⚖️ deciding)`);
|
||||
}
|
||||
output.push('');
|
||||
|
||||
// Context Index Usage Instructions
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}💡 Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${colors.reset}`);
|
||||
output.push('');
|
||||
output.push(`${colors.dim}When you need implementation details, rationale, or debugging context:${colors.reset}`);
|
||||
output.push(`${colors.dim} - Use the mem-search skill to fetch full observations on-demand${colors.reset}`);
|
||||
output.push(`${colors.dim} - Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching${colors.reset}`);
|
||||
output.push(`${colors.dim} - Trust this index over re-reading code for past decisions and learnings${colors.reset}`);
|
||||
} else {
|
||||
output.push(`💡 **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.`);
|
||||
output.push('');
|
||||
output.push(`When you need implementation details, rationale, or debugging context:`);
|
||||
output.push(`- Use the mem-search skill to fetch full observations on-demand`);
|
||||
output.push(`- Critical types (🔴 bugfix, ⚖️ decision) often need detailed fetching`);
|
||||
output.push(`- Trust this index over re-reading code for past decisions and learnings`);
|
||||
}
|
||||
output.push('');
|
||||
|
||||
// Section 1: Aggregate ROI Metrics
|
||||
const totalObservations = observations.length;
|
||||
const totalReadTokens = observations.reduce((sum, obs) => {
|
||||
// Estimate read tokens from observation size
|
||||
const obsSize = (obs.title?.length || 0) +
|
||||
(obs.subtitle?.length || 0) +
|
||||
(obs.narrative?.length || 0) +
|
||||
JSON.stringify(obs.facts || []).length;
|
||||
return sum + Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
|
||||
}, 0);
|
||||
const totalDiscoveryTokens = observations.reduce((sum, obs) => sum + (obs.discovery_tokens || 0), 0);
|
||||
const savings = totalDiscoveryTokens - totalReadTokens;
|
||||
const savingsPercent = totalDiscoveryTokens > 0
|
||||
? Math.round((savings / totalDiscoveryTokens) * 100)
|
||||
: 0;
|
||||
|
||||
// Display Context Economics section
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.cyan}📊 Context Economics${colors.reset}`);
|
||||
output.push(`${colors.dim} Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)${colors.reset}`);
|
||||
output.push(`${colors.dim} Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions${colors.reset}`);
|
||||
if (totalDiscoveryTokens > 0) {
|
||||
output.push(`${colors.green} Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)${colors.reset}`);
|
||||
}
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`💡 **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts).`);
|
||||
output.push(`- Use MCP search tools to fetch full observation details on-demand (Layer 2)`);
|
||||
output.push(`- Prefer searching observations over re-reading code for past decisions and learnings`);
|
||||
output.push(`- Critical types (🔴 bugfix, 🧠 decision) often worth fetching immediately`);
|
||||
output.push(`📊 **Context Economics**:`);
|
||||
output.push(`- Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)`);
|
||||
output.push(`- Work investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent on research, building, and decisions`);
|
||||
if (totalDiscoveryTokens > 0) {
|
||||
output.push(`- Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
@@ -380,8 +406,8 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
|
||||
// Table header (markdown only)
|
||||
if (!useColors) {
|
||||
output.push(`| ID | Time | T | Title | Tokens |`);
|
||||
output.push(`|----|------|---|-------|--------|`);
|
||||
output.push(`| ID | Time | T | Title | Read | Work |`);
|
||||
output.push(`|----|------|---|-------|------|------|`);
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
@@ -389,10 +415,11 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
lastTime = '';
|
||||
}
|
||||
|
||||
// Render observation row
|
||||
let icon = '•';
|
||||
const time = formatTime(obs.created_at);
|
||||
const title = obs.title || 'Untitled';
|
||||
|
||||
// Map observation type to emoji
|
||||
// Map observation type to emoji icon
|
||||
let icon = '•';
|
||||
switch (obs.type) {
|
||||
case 'bugfix':
|
||||
icon = '🔴';
|
||||
@@ -410,15 +437,40 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
icon = '🔵';
|
||||
break;
|
||||
case 'decision':
|
||||
icon = '🧠';
|
||||
icon = '⚖️';
|
||||
break;
|
||||
default:
|
||||
icon = '•';
|
||||
}
|
||||
|
||||
const time = formatTime(obs.created_at);
|
||||
const title = obs.title || 'Untitled';
|
||||
const tokens = estimateTokens(obs.narrative);
|
||||
// Section 2: Calculate read tokens (estimate from observation size)
|
||||
const obsSize = (obs.title?.length || 0) +
|
||||
(obs.subtitle?.length || 0) +
|
||||
(obs.narrative?.length || 0) +
|
||||
JSON.stringify(obs.facts || []).length;
|
||||
const readTokens = Math.ceil(obsSize / CHARS_PER_TOKEN_ESTIMATE);
|
||||
|
||||
// Get discovery tokens (handle old observations without this field)
|
||||
const discoveryTokens = obs.discovery_tokens || 0;
|
||||
|
||||
// Map observation type to work emoji
|
||||
let workEmoji = '🔍'; // default to research/discovery
|
||||
switch (obs.type) {
|
||||
case 'discovery':
|
||||
workEmoji = '🔍'; // research/exploration
|
||||
break;
|
||||
case 'change':
|
||||
case 'feature':
|
||||
case 'bugfix':
|
||||
case 'refactor':
|
||||
workEmoji = '🛠️'; // building/modifying
|
||||
break;
|
||||
case 'decision':
|
||||
workEmoji = '⚖️'; // decision-making
|
||||
break;
|
||||
}
|
||||
|
||||
const discoveryDisplay = discoveryTokens > 0 ? `${workEmoji} ${discoveryTokens.toLocaleString()}` : '-';
|
||||
|
||||
const showTime = time !== lastTime;
|
||||
const timeDisplay = showTime ? time : '';
|
||||
@@ -426,10 +478,11 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
|
||||
if (useColors) {
|
||||
const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length);
|
||||
const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : '';
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`);
|
||||
const readPart = readTokens > 0 ? `${colors.dim}(~${readTokens}t)${colors.reset}` : '';
|
||||
const discoveryPart = discoveryTokens > 0 ? `${colors.dim}(${workEmoji} ${discoveryTokens.toLocaleString()}t)${colors.reset}` : '';
|
||||
output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${readPart} ${discoveryPart}`);
|
||||
} else {
|
||||
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`);
|
||||
output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${readTokens} | ${discoveryDisplay} |`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,11 +509,15 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
|
||||
output.push(...renderSummaryField('Next Steps', mostRecentSummary.next_steps, colors.magenta, useColors));
|
||||
}
|
||||
|
||||
// Footer with MCP search instructions
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`);
|
||||
} else {
|
||||
output.push(`*Use claude-mem MCP search to access records with the given ID*`);
|
||||
// Footer with token savings message
|
||||
if (totalDiscoveryTokens > 0 && savings > 0) {
|
||||
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
|
||||
output.push('');
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.${colors.reset}`);
|
||||
} else {
|
||||
output.push(`💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,12 +77,16 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Strip leading slash from commands for memory agent
|
||||
// /review 101 → review 101 (more semantic for observations)
|
||||
const cleanedPrompt = prompt.startsWith('/') ? prompt.substring(1) : prompt;
|
||||
|
||||
try {
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt, promptNumber }),
|
||||
body: JSON.stringify({ project, userPrompt: cleanedPrompt, promptNumber }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ export interface PostToolUseInput {
|
||||
|
||||
// Tools to skip (low value or too frequent)
|
||||
const SKIP_TOOLS = new Set([
|
||||
'ListMcpResourcesTool'
|
||||
'ListMcpResourcesTool', // MCP infrastructure
|
||||
'SlashCommand', // Command invocation (observe what it produces, not the call)
|
||||
'Skill', // Skill invocation (observe what it produces, not the call)
|
||||
'TodoWrite', // Task management meta-tool
|
||||
'AskUserQuestion' // User interaction, not substantive work
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,17 +4,124 @@
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { silentDebug } from '../utils/silent-debug.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract last user message from transcript JSONL file
|
||||
*/
|
||||
function extractLastUserMessage(transcriptPath: string): string {
|
||||
if (!transcriptPath || !existsSync(transcriptPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Parse JSONL and find last user message
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const line = JSON.parse(lines[i]);
|
||||
|
||||
// Claude Code transcript format: {type: "user", message: {role: "user", content: [...]}}
|
||||
if (line.type === 'user' && line.message?.content) {
|
||||
const content = line.message.content;
|
||||
|
||||
// Extract text content (handle both string and array formats)
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts = content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text);
|
||||
return textParts.join('\n');
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract last assistant message from transcript JSONL file
|
||||
* Filters out system-reminder tags to avoid polluting summaries
|
||||
*/
|
||||
function extractLastAssistantMessage(transcriptPath: string): string {
|
||||
if (!transcriptPath || !existsSync(transcriptPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(transcriptPath, 'utf-8').trim();
|
||||
if (!content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Parse JSONL and find last assistant message
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const line = JSON.parse(lines[i]);
|
||||
|
||||
// Claude Code transcript format: {type: "assistant", message: {role: "assistant", content: [...]}}
|
||||
if (line.type === 'assistant' && line.message?.content) {
|
||||
let text = '';
|
||||
const content = line.message.content;
|
||||
|
||||
// Extract text content (handle both string and array formats)
|
||||
if (typeof content === 'string') {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts = content
|
||||
.filter((c: any) => c.type === 'text')
|
||||
.map((c: any) => c.text);
|
||||
text = textParts.join('\n');
|
||||
}
|
||||
|
||||
// Filter out system-reminder tags and their content
|
||||
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
|
||||
|
||||
// Clean up excessive whitespace
|
||||
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary Hook Main Logic
|
||||
*/
|
||||
@@ -33,21 +140,61 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
// Get or create session
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
|
||||
// DIAGNOSTIC: Check session and observations
|
||||
const sessionInfo = db.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project
|
||||
FROM sdk_sessions WHERE id = ?
|
||||
`).get(sessionDbId) as any;
|
||||
|
||||
const obsCount = db.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).get(sessionInfo?.sdk_session_id) as { count: number };
|
||||
|
||||
silentDebug('[summary-hook] Session diagnostics', {
|
||||
claudeSessionId: session_id,
|
||||
sessionDbId,
|
||||
sdkSessionId: sessionInfo?.sdk_session_id,
|
||||
project: sessionInfo?.project,
|
||||
promptNumber,
|
||||
observationCount: obsCount?.count || 0,
|
||||
transcriptPath: input.transcript_path
|
||||
});
|
||||
|
||||
db.close();
|
||||
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Extract last user AND assistant messages from transcript
|
||||
const lastUserMessage = extractLastUserMessage(input.transcript_path || '');
|
||||
const lastAssistantMessage = extractLastAssistantMessage(input.transcript_path || '');
|
||||
|
||||
silentDebug('[summary-hook] Extracted messages', {
|
||||
hasLastUserMessage: !!lastUserMessage,
|
||||
hasLastAssistantMessage: !!lastAssistantMessage,
|
||||
lastAssistantPreview: lastAssistantMessage.substring(0, 200),
|
||||
lastAssistantLength: lastAssistantMessage.length
|
||||
});
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
sessionId: sessionDbId,
|
||||
workerPort: port,
|
||||
promptNumber
|
||||
promptNumber,
|
||||
hasLastUserMessage: !!lastUserMessage,
|
||||
hasLastAssistantMessage: !!lastAssistantMessage
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
body: JSON.stringify({
|
||||
prompt_number: promptNumber,
|
||||
last_user_message: lastUserMessage,
|
||||
last_assistant_message: lastAssistantMessage
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ try {
|
||||
"\n\n📝 Claude-Mem Context Loaded\n" +
|
||||
" ℹ️ Note: This appears as stderr but is informational only\n\n" +
|
||||
output +
|
||||
`\n\n📺 Watch live in browser http://localhost:${port}/ (New! v5.1)\n`
|
||||
"\n\n💬 Feedback & Support\nhttps://github.com/thedotmack/claude-mem/discussions/110\n" +
|
||||
`\n📺 Watch live in browser http://localhost:${port}/\n`
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -17,11 +17,11 @@ export interface ParsedObservation {
|
||||
}
|
||||
|
||||
export interface ParsedSummary {
|
||||
request: string;
|
||||
investigated: string;
|
||||
learned: string;
|
||||
completed: string;
|
||||
next_steps: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface SDKSession {
|
||||
sdk_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
last_user_message?: string;
|
||||
last_assistant_message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,8 +29,12 @@ export function buildInitPrompt(project: string, sessionId: string, userPrompt:
|
||||
|
||||
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
You do not have access to tools. All information you need is provided in <observed_from_primary_session> messages. Create observations from what you observe - no investigation needed.
|
||||
|
||||
<observed_from_primary_session>
|
||||
<user_request>${userPrompt}</user_request>
|
||||
<requested_at>${new Date().toISOString().split('T')[0]}</requested_at>
|
||||
</observed_from_primary_session>
|
||||
|
||||
Your job is to monitor a different Claude Code session happening RIGHT NOW, with the goal of creating observations and progress summaries as the work is being done LIVE by the user. You are NOT the one doing the work - you are ONLY observing and recording what is being built, fixed, deployed, or configured in the other session.
|
||||
|
||||
@@ -127,7 +133,11 @@ Output observations using this XML structure:
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
IMPORTANT! DO NOT do any work other than generate the OBSERVATIONS or PROGRESS SUMMARIES - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.
|
||||
IMPORTANT! DO NOT do any work right now other than generating this OBSERVATIONS from tool use messages - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.
|
||||
|
||||
Never reference yourself or your own actions. Do not output anything other than the observation content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful observations.
|
||||
|
||||
Remember that we record these observations as a way of helping us stay on track with our progress, and to help us keep important decisions and changes at the forefront of our minds! :) Thank you so much for your help!
|
||||
|
||||
MEMORY PROCESSING START
|
||||
=======================`;
|
||||
@@ -153,25 +163,30 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
toolOutput = obs.tool_output; // If parse fails, use raw value
|
||||
}
|
||||
|
||||
return `<tool_used>
|
||||
<tool_name>${obs.tool_name}</tool_name>
|
||||
<tool_time>${new Date(obs.created_at_epoch).toISOString()}</tool_time>${obs.cwd ? `\n <tool_cwd>${obs.cwd}</tool_cwd>` : ''}
|
||||
<tool_input>${JSON.stringify(toolInput, null, 2)}</tool_input>
|
||||
<tool_output>${JSON.stringify(toolOutput, null, 2)}</tool_output>
|
||||
</tool_used>`;
|
||||
return `<observed_from_primary_session>
|
||||
<what_happened>${obs.tool_name}</what_happened>
|
||||
<occurred_at>${new Date(obs.created_at_epoch).toISOString()}</occurred_at>${obs.cwd ? `\n <working_directory>${obs.cwd}</working_directory>` : ''}
|
||||
<parameters>${JSON.stringify(toolInput, null, 2)}</parameters>
|
||||
<outcome>${JSON.stringify(toolOutput, null, 2)}</outcome>
|
||||
</observed_from_primary_session>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt to generate progress summary
|
||||
*/
|
||||
export function buildSummaryPrompt(session: SDKSession): string {
|
||||
const lastAssistantMessage = session.last_assistant_message || '';
|
||||
|
||||
return `PROGRESS SUMMARY CHECKPOINT
|
||||
===========================
|
||||
Write progress notes of what was done, what was learned, and what's next. This is a checkpoint to capture progress so far. The session is ongoing - you may receive more requests and tool executions after this summary. Write "next_steps" as the current trajectory of work (what's actively being worked on or coming up next), not as post-session future work. Always write at least a minimal summary explaining current progress, even if work is still in early stages, so that users see a summary output tied to each request.
|
||||
|
||||
Claude's Full Response to User:
|
||||
${lastAssistantMessage}
|
||||
|
||||
Respond in this XML format:
|
||||
<summary>
|
||||
<request>[Short title related to the most recent prompt]</request>
|
||||
<request>[Short title capturing the user's request AND the substance of what was discussed/done]</request>
|
||||
<investigated>[What has been explored so far? What was examined?]</investigated>
|
||||
<learned>[What have you learned about how things work?]</learned>
|
||||
<completed>[What work has been completed so far? What has shipped or changed?]</completed>
|
||||
@@ -179,7 +194,11 @@ Respond in this XML format:
|
||||
<notes>[Additional insights or observations about the current progress]</notes>
|
||||
</summary>
|
||||
|
||||
IMPORTANT! DO NOT do any work other than generate the PROGRESS SUMMARY - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.`;
|
||||
IMPORTANT! DO NOT do any work right now other than generating this next PROGRESS SUMMARY - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one.
|
||||
|
||||
Never reference yourself or your own actions. Do not output anything other than the summary content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful summary content.
|
||||
|
||||
Thank you, this summary will be very useful for keeping track of our progress!`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,43 +223,17 @@ IMPORTANT! DO NOT do any work other than generate the PROGRESS SUMMARY - and re
|
||||
* First prompt: Uses buildInitPrompt instead (promptNumber === 1)
|
||||
*/
|
||||
export function buildContinuationPrompt(userPrompt: string, promptNumber: number, claudeSessionId: string): string {
|
||||
return `This is continuation prompt #${promptNumber} for session ${claudeSessionId} that you're observing.
|
||||
return `
|
||||
Hello memory agent, you are continuing to observe the primary Claude session.
|
||||
|
||||
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
<observed_from_primary_session>
|
||||
<user_request>${userPrompt}</user_request>
|
||||
<requested_at>${new Date().toISOString().split('T')[0]}</requested_at>
|
||||
</observed_from_primary_session>
|
||||
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
You do not have access to tools. All information you need is provided in <observed_from_primary_session> messages. Create observations from what you observe - no investigation needed.
|
||||
|
||||
Your job is to continue monitoring the different Claude Code session happening RIGHT NOW, with the goal of creating observations and a progress summary as the work is being done LIVE by the user. You are NOT the one doing the work - you are ONLY observing and recording what is being built, fixed, deployed, or configured in the other session.
|
||||
|
||||
WHAT TO RECORD
|
||||
--------------
|
||||
Focus on deliverables and capabilities:
|
||||
- What the system NOW DOES differently (new capabilities)
|
||||
- What shipped to users/production (features, fixes, configs, docs)
|
||||
- Changes in technical domains (auth, data, UI, infra, DevOps, docs)
|
||||
|
||||
Use verbs like: implemented, fixed, deployed, configured, migrated, optimized, added, refactored
|
||||
|
||||
✅ GOOD EXAMPLES (describes what was built):
|
||||
- "Authentication now supports OAuth2 with PKCE flow"
|
||||
- "Deployment pipeline runs canary releases with auto-rollback"
|
||||
- "Database indexes optimized for common query patterns"
|
||||
|
||||
❌ BAD EXAMPLES (describes observation process - DO NOT DO THIS):
|
||||
- "Analyzed authentication implementation and stored findings"
|
||||
- "Tracked deployment steps and logged outcomes"
|
||||
- "Monitored database performance and recorded metrics"
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
Skip routine operations:
|
||||
- Empty status checks
|
||||
- Package installations with no errors
|
||||
- Simple file listings
|
||||
- Repetitive operations you've already documented
|
||||
- If file related research comes back as empty or not found
|
||||
- **No output necessary if skipping.**
|
||||
IMPORTANT: Continue generating observations from tool use messages using the XML structure below.
|
||||
|
||||
OUTPUT FORMAT
|
||||
-------------
|
||||
@@ -303,9 +296,10 @@ Output observations using this XML structure:
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
IMPORTANT! DO NOT do any work other than generate the OBSERVATIONS or PROGRESS SUMMARIES - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.
|
||||
Never reference yourself or your own actions. Do not output anything other than the observation content formatted in the XML structure above. All other output is ignored by the system, and the system has been designed to be smart about token usage. Please spend your tokens wisely on useful observations.
|
||||
|
||||
MEMORY PROCESSING START
|
||||
=======================`;
|
||||
Remember that we record these observations as a way of helping us stay on track with our progress, and to help us keep important decisions and changes at the forefront of our minds! :) Thank you so much for your continued help!
|
||||
|
||||
}
|
||||
MEMORY PROCESSING CONTINUED
|
||||
===========================`;
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
|
||||
/**
|
||||
* Search interface for session-based memory
|
||||
* Provides FTS5 full-text search and structured queries for sessions, observations, and summaries
|
||||
* Provides filter-only structured queries for sessions, observations, and user prompts
|
||||
* Vector search is handled by ChromaDB - this class only supports filtering without query text
|
||||
*/
|
||||
export class SessionSearch {
|
||||
private db: Database.Database;
|
||||
@@ -31,7 +32,18 @@ export class SessionSearch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure FTS5 tables exist (inline migration)
|
||||
* Ensure FTS5 tables exist (backward compatibility only - no longer used for search)
|
||||
*
|
||||
* FTS5 tables are maintained for backward compatibility but not used for search.
|
||||
* Vector search (Chroma) is now the primary search mechanism.
|
||||
*
|
||||
* Retention Rationale:
|
||||
* - Prevents breaking existing installations with FTS5 tables
|
||||
* - Allows graceful migration path for users
|
||||
* - Tables maintained but search paths removed
|
||||
* - Triggers still fire to keep tables synchronized
|
||||
*
|
||||
* TODO: Remove FTS5 infrastructure in future major version (v7.0.0)
|
||||
*/
|
||||
private ensureFTSTables(): void {
|
||||
try {
|
||||
@@ -134,22 +146,6 @@ export class SessionSearch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape FTS5 special characters in user input
|
||||
*
|
||||
* FTS5 uses double quotes for phrase searches and treats certain characters
|
||||
* as operators (*, AND, OR, NOT, parentheses, etc.). To prevent injection,
|
||||
* we wrap user input in double quotes and escape internal quotes by doubling them.
|
||||
* This converts any user input into a safe phrase search.
|
||||
*
|
||||
* @param text - User input to escape for FTS5 MATCH queries
|
||||
* @returns Safely escaped FTS5 query string
|
||||
*/
|
||||
private escapeFTS5(text: string): string {
|
||||
// Escape internal double quotes by doubling them (FTS5 standard)
|
||||
// Then wrap the entire string in double quotes for phrase search
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WHERE clause for structured filters
|
||||
@@ -243,116 +239,78 @@ export class SessionSearch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search observations using FTS5 full-text search
|
||||
* Search observations using filter-only direct SQLite query.
|
||||
* Vector search is handled by ChromaDB - this only supports filtering without query text.
|
||||
*/
|
||||
searchObservations(query: string, options: SearchOptions = {}): ObservationSearchResult[] {
|
||||
searchObservations(query: string | undefined, options: SearchOptions = {}): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
// FILTER-ONLY PATH: When no query text, query table directly
|
||||
// This enables date filtering which Chroma cannot do (requires direct SQLite access)
|
||||
if (!query) {
|
||||
const filterClause = this.buildFilterClause(filters, params, 'o');
|
||||
if (!filterClause) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
}
|
||||
|
||||
// Build filter conditions
|
||||
const filterClause = this.buildFilterClause(filters, params, 'o');
|
||||
const whereClause = filterClause ? `AND ${filterClause}` : '';
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = this.buildOrderClause(orderBy, true);
|
||||
const sql = `
|
||||
SELECT o.*, o.discovery_tokens
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
// Main query with FTS5
|
||||
const sql = `
|
||||
SELECT
|
||||
o.*,
|
||||
observations_fts.rank as rank
|
||||
FROM observations o
|
||||
JOIN observations_fts ON o.id = observations_fts.rowid
|
||||
WHERE observations_fts MATCH ?
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
|
||||
// Normalize rank to score (0-1, higher is better)
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
// Invert rank (lower rank = better match) and normalize to 0-1
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
params.push(limit, offset);
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
return results;
|
||||
// Vector search with query text should be handled by ChromaDB
|
||||
// This method only supports filter-only queries (query=undefined)
|
||||
console.warn('[SessionSearch] Text search not supported - use ChromaDB for vector search');
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search session summaries using FTS5 full-text search
|
||||
* Search session summaries using filter-only direct SQLite query.
|
||||
* Vector search is handled by ChromaDB - this only supports filtering without query text.
|
||||
*/
|
||||
searchSessions(query: string, options: SearchOptions = {}): SessionSummarySearchResult[] {
|
||||
searchSessions(query: string | undefined, options: SearchOptions = {}): SessionSummarySearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
// FILTER-ONLY PATH: When no query text, query session_summaries table directly
|
||||
if (!query) {
|
||||
const filterOptions = { ...filters };
|
||||
delete filterOptions.type;
|
||||
const filterClause = this.buildFilterClause(filterOptions, params, 's');
|
||||
if (!filterClause) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
}
|
||||
|
||||
// Build filter conditions (without type filter - not applicable to summaries)
|
||||
const filterOptions = { ...filters };
|
||||
delete filterOptions.type;
|
||||
const filterClause = this.buildFilterClause(filterOptions, params, 's');
|
||||
const whereClause = filterClause ? `AND ${filterClause}` : '';
|
||||
const orderClause = orderBy === 'date_asc'
|
||||
? 'ORDER BY s.created_at_epoch ASC'
|
||||
: 'ORDER BY s.created_at_epoch DESC';
|
||||
|
||||
// Note: session_summaries don't have files_read/files_modified in the same way
|
||||
// We'll need to adjust the filter clause
|
||||
const adjustedWhereClause = whereClause.replace(/files_read/g, 'files_read').replace(/files_modified/g, 'files_edited');
|
||||
const sql = `
|
||||
SELECT s.*, s.discovery_tokens
|
||||
FROM session_summaries s
|
||||
WHERE ${filterClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = orderBy === 'relevance'
|
||||
? 'ORDER BY session_summaries_fts.rank ASC'
|
||||
: orderBy === 'date_asc'
|
||||
? 'ORDER BY s.created_at_epoch ASC'
|
||||
: 'ORDER BY s.created_at_epoch DESC';
|
||||
|
||||
// Main query with FTS5
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
session_summaries_fts.rank as rank
|
||||
FROM session_summaries s
|
||||
JOIN session_summaries_fts ON s.id = session_summaries_fts.rowid
|
||||
WHERE session_summaries_fts MATCH ?
|
||||
${adjustedWhereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as SessionSummarySearchResult[];
|
||||
|
||||
// Normalize rank to score
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
params.push(limit, offset);
|
||||
return this.db.prepare(sql).all(...params) as SessionSummarySearchResult[];
|
||||
}
|
||||
|
||||
return results;
|
||||
// Vector search with query text should be handled by ChromaDB
|
||||
// This method only supports filter-only queries (query=undefined)
|
||||
console.warn('[SessionSearch] Text search not supported - use ChromaDB for vector search');
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,7 +326,7 @@ export class SessionSearch {
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
SELECT o.*, o.discovery_tokens
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${orderClause}
|
||||
@@ -396,7 +354,7 @@ export class SessionSearch {
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const observationsSql = `
|
||||
SELECT o.*
|
||||
SELECT o.*, o.discovery_tokens
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${orderClause}
|
||||
@@ -440,7 +398,7 @@ export class SessionSearch {
|
||||
sessionParams.push(`%${filePath}%`, `%${filePath}%`);
|
||||
|
||||
const sessionsSql = `
|
||||
SELECT s.*
|
||||
SELECT s.*, s.discovery_tokens
|
||||
FROM session_summaries s
|
||||
WHERE ${baseConditions.join(' AND ')}
|
||||
ORDER BY s.created_at_epoch DESC
|
||||
@@ -470,7 +428,7 @@ export class SessionSearch {
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
SELECT o.*, o.discovery_tokens
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${orderClause}
|
||||
@@ -483,16 +441,13 @@ export class SessionSearch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search user prompts with full-text search
|
||||
* Search user prompts using filter-only direct SQLite query.
|
||||
* Vector search is handled by ChromaDB - this only supports filtering without query text.
|
||||
*/
|
||||
searchUserPrompts(query: string, options: SearchOptions = {}): UserPromptSearchResult[] {
|
||||
searchUserPrompts(query: string | undefined, options: SearchOptions = {}): UserPromptSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 20, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
|
||||
// Build filter conditions (join with sdk_sessions for project filtering)
|
||||
const baseConditions: string[] = [];
|
||||
if (filters.project) {
|
||||
@@ -514,47 +469,34 @@ export class SessionSearch {
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : '';
|
||||
// FILTER-ONLY PATH: When no query text, query user_prompts table directly
|
||||
if (!query) {
|
||||
if (baseConditions.length === 0) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
}
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = orderBy === 'relevance'
|
||||
? 'ORDER BY user_prompts_fts.rank ASC'
|
||||
: orderBy === 'date_asc'
|
||||
? 'ORDER BY up.created_at_epoch ASC'
|
||||
: 'ORDER BY up.created_at_epoch DESC';
|
||||
const whereClause = `WHERE ${baseConditions.join(' AND ')}`;
|
||||
const orderClause = orderBy === 'date_asc'
|
||||
? 'ORDER BY up.created_at_epoch ASC'
|
||||
: 'ORDER BY up.created_at_epoch DESC';
|
||||
|
||||
// Main query with FTS5 (join sdk_sessions for project filtering)
|
||||
const sql = `
|
||||
SELECT
|
||||
up.*,
|
||||
user_prompts_fts.rank as rank
|
||||
FROM user_prompts up
|
||||
JOIN user_prompts_fts ON up.id = user_prompts_fts.rowid
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE user_prompts_fts MATCH ?
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
const sql = `
|
||||
SELECT up.*
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as UserPromptSearchResult[];
|
||||
|
||||
// Normalize rank to score
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
params.push(limit, offset);
|
||||
return this.db.prepare(sql).all(...params) as UserPromptSearchResult[];
|
||||
}
|
||||
|
||||
return results;
|
||||
// Vector search with query text should be handled by ChromaDB
|
||||
// This method only supports filter-only queries (query=undefined)
|
||||
console.warn('[SessionSearch] Text search not supported - use ChromaDB for vector search');
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ export class SessionStore {
|
||||
this.addObservationHierarchicalFields();
|
||||
this.makeObservationsTextNullable();
|
||||
this.createUserPromptsTable();
|
||||
this.ensureDiscoveryTokensColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -492,6 +493,43 @@ export class SessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure discovery_tokens column exists (migration 11)
|
||||
* CRITICAL: This migration was incorrectly using version 7 (which was already taken by removeSessionSummariesUniqueConstraint)
|
||||
* The duplicate version number may have caused migration tracking issues in some databases
|
||||
*/
|
||||
private ensureDiscoveryTokensColumn(): void {
|
||||
try {
|
||||
// Check if migration already applied to avoid unnecessary re-runs
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as {version: number} | undefined;
|
||||
if (applied) return;
|
||||
|
||||
// Check if discovery_tokens column exists in observations table
|
||||
const observationsInfo = this.db.pragma('table_info(observations)');
|
||||
const obsHasDiscoveryTokens = (observationsInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
|
||||
|
||||
if (!obsHasDiscoveryTokens) {
|
||||
this.db.exec('ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
|
||||
console.error('[SessionStore] Added discovery_tokens column to observations table');
|
||||
}
|
||||
|
||||
// Check if discovery_tokens column exists in session_summaries table
|
||||
const summariesInfo = this.db.pragma('table_info(session_summaries)');
|
||||
const sumHasDiscoveryTokens = (summariesInfo as any[]).some((col: any) => col.name === 'discovery_tokens');
|
||||
|
||||
if (!sumHasDiscoveryTokens) {
|
||||
this.db.exec('ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0');
|
||||
console.error('[SessionStore] Added discovery_tokens column to session_summaries table');
|
||||
}
|
||||
|
||||
// Record migration only after successful column verification/addition
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(11, new Date().toISOString());
|
||||
} catch (error: any) {
|
||||
console.error('[SessionStore] Discovery tokens migration error:', error.message);
|
||||
throw error; // Re-throw to prevent silent failures
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent session summaries for a project
|
||||
*/
|
||||
@@ -1074,7 +1112,8 @@ export class SessionStore {
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
},
|
||||
promptNumber?: number
|
||||
promptNumber?: number,
|
||||
discoveryTokens: number = 0
|
||||
): { id: number; createdAtEpoch: number } {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
@@ -1105,8 +1144,8 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -1121,6 +1160,7 @@ export class SessionStore {
|
||||
JSON.stringify(observation.files_read),
|
||||
JSON.stringify(observation.files_modified),
|
||||
promptNumber || null,
|
||||
discoveryTokens,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
@@ -1146,7 +1186,8 @@ export class SessionStore {
|
||||
next_steps: string;
|
||||
notes: string | null;
|
||||
},
|
||||
promptNumber?: number
|
||||
promptNumber?: number,
|
||||
discoveryTokens: number = 0
|
||||
): { id: number; createdAtEpoch: number } {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
@@ -1177,8 +1218,8 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -1191,6 +1232,7 @@ export class SessionStore {
|
||||
summary.next_steps,
|
||||
summary.notes,
|
||||
promptNumber || null,
|
||||
discoveryTokens,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
|
||||
@@ -471,6 +471,30 @@ export const migration006: Migration = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 007 - Add discovery_tokens column for ROI metrics
|
||||
* Tracks token cost of discovering/creating each observation and summary
|
||||
*/
|
||||
export const migration007: Migration = {
|
||||
version: 7,
|
||||
up: (db: Database) => {
|
||||
// Add discovery_tokens to observations table
|
||||
db.run(`ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
|
||||
|
||||
// Add discovery_tokens to session_summaries table
|
||||
db.run(`ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0`);
|
||||
|
||||
console.log('✅ Added discovery_tokens columns for ROI tracking');
|
||||
},
|
||||
|
||||
down: (db: Database) => {
|
||||
// Note: SQLite doesn't support DROP COLUMN in all versions
|
||||
// In production, would need to recreate tables without these columns
|
||||
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
|
||||
console.log('⚠️ To rollback, manually recreate the observations and session_summaries tables');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* All migrations in order
|
||||
*/
|
||||
@@ -480,5 +504,6 @@ export const migrations: Migration[] = [
|
||||
migration003,
|
||||
migration004,
|
||||
migration005,
|
||||
migration006
|
||||
migration006,
|
||||
migration007
|
||||
];
|
||||
@@ -215,6 +215,7 @@ export interface ObservationRow {
|
||||
files_read: string | null; // JSON array
|
||||
files_modified: string | null; // JSON array
|
||||
prompt_number: number | null;
|
||||
discovery_tokens: number; // ROI metrics: tokens spent discovering this observation
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
@@ -232,6 +233,7 @@ export interface SessionSummaryRow {
|
||||
files_edited: string | null; // JSON array
|
||||
notes: string | null;
|
||||
prompt_number: number | null;
|
||||
discovery_tokens: number; // ROI metrics: cumulative tokens spent in this session
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ interface StoredObservation {
|
||||
files_read: string | null; // JSON
|
||||
files_modified: string | null; // JSON
|
||||
prompt_number: number;
|
||||
discovery_tokens: number; // ROI metrics
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
@@ -51,6 +52,7 @@ interface StoredSummary {
|
||||
next_steps: string | null;
|
||||
notes: string | null;
|
||||
prompt_number: number;
|
||||
discovery_tokens: number; // ROI metrics
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
@@ -345,7 +347,8 @@ export class ChromaSync {
|
||||
project: string,
|
||||
obs: ParsedObservation,
|
||||
promptNumber: number,
|
||||
createdAtEpoch: number
|
||||
createdAtEpoch: number,
|
||||
discoveryTokens: number = 0
|
||||
): Promise<void> {
|
||||
// Convert ParsedObservation to StoredObservation format
|
||||
const stored: StoredObservation = {
|
||||
@@ -362,6 +365,7 @@ export class ChromaSync {
|
||||
files_read: JSON.stringify(obs.files_read),
|
||||
files_modified: JSON.stringify(obs.files_modified),
|
||||
prompt_number: promptNumber,
|
||||
discovery_tokens: discoveryTokens,
|
||||
created_at: new Date(createdAtEpoch * 1000).toISOString(),
|
||||
created_at_epoch: createdAtEpoch
|
||||
};
|
||||
@@ -387,7 +391,8 @@ export class ChromaSync {
|
||||
project: string,
|
||||
summary: ParsedSummary,
|
||||
promptNumber: number,
|
||||
createdAtEpoch: number
|
||||
createdAtEpoch: number,
|
||||
discoveryTokens: number = 0
|
||||
): Promise<void> {
|
||||
// Convert ParsedSummary to StoredSummary format
|
||||
const stored: StoredSummary = {
|
||||
@@ -401,6 +406,7 @@ export class ChromaSync {
|
||||
next_steps: summary.next_steps,
|
||||
notes: summary.notes,
|
||||
prompt_number: promptNumber,
|
||||
discovery_tokens: discoveryTokens,
|
||||
created_at: new Date(createdAtEpoch * 1000).toISOString(),
|
||||
created_at_epoch: createdAtEpoch
|
||||
};
|
||||
|
||||
@@ -44,9 +44,6 @@ export class WorkerService {
|
||||
private paginationHelper: PaginationHelper;
|
||||
private settingsManager: SettingsManager;
|
||||
|
||||
// Processing status tracking for viewer UI spinner
|
||||
private isProcessing: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
|
||||
@@ -58,6 +55,11 @@ export class WorkerService {
|
||||
this.paginationHelper = new PaginationHelper(this.dbManager);
|
||||
this.settingsManager = new SettingsManager(this.dbManager);
|
||||
|
||||
// Set callback for when sessions are deleted (to update activity indicator)
|
||||
this.sessionManager.setOnSessionDeleted(() => {
|
||||
this.broadcastProcessingStatus();
|
||||
});
|
||||
|
||||
this.mcpClient = new Client({
|
||||
name: 'worker-search-proxy',
|
||||
version: '1.0.0'
|
||||
@@ -153,6 +155,12 @@ export class WorkerService {
|
||||
this.app.get('/api/observations', this.handleGetObservations.bind(this));
|
||||
this.app.get('/api/summaries', this.handleGetSummaries.bind(this));
|
||||
this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
|
||||
|
||||
// Fetch by ID
|
||||
this.app.get('/api/observation/:id', this.handleGetObservationById.bind(this));
|
||||
this.app.get('/api/session/:id', this.handleGetSessionById.bind(this));
|
||||
this.app.get('/api/prompt/:id', this.handleGetPromptById.bind(this));
|
||||
|
||||
this.app.get('/api/stats', this.handleGetStats.bind(this));
|
||||
this.app.get('/api/processing-status', this.handleGetProcessingStatus.bind(this));
|
||||
this.app.post('/api/processing', this.handleSetProcessing.bind(this));
|
||||
@@ -166,6 +174,14 @@ export class WorkerService {
|
||||
this.app.post('/api/mcp/toggle', this.handleToggleMcp.bind(this));
|
||||
|
||||
// Search API endpoints (for skill-based search)
|
||||
// Unified endpoints (new consolidated API)
|
||||
this.app.get('/api/search', this.handleUnifiedSearch.bind(this));
|
||||
this.app.get('/api/timeline', this.handleUnifiedTimeline.bind(this));
|
||||
this.app.get('/api/decisions', this.handleDecisions.bind(this));
|
||||
this.app.get('/api/changes', this.handleChanges.bind(this));
|
||||
this.app.get('/api/how-it-works', this.handleHowItWorks.bind(this));
|
||||
|
||||
// Backward compatibility endpoints (use /api/search with type param instead)
|
||||
this.app.get('/api/search/observations', this.handleSearchObservations.bind(this));
|
||||
this.app.get('/api/search/sessions', this.handleSearchSessions.bind(this));
|
||||
this.app.get('/api/search/prompts', this.handleSearchPrompts.bind(this));
|
||||
@@ -178,15 +194,50 @@ export class WorkerService {
|
||||
this.app.get('/api/search/help', this.handleSearchHelp.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup orphaned MCP server processes (uvx/chroma) from previous sessions
|
||||
*/
|
||||
private async cleanupOrphanedProcesses(): Promise<void> {
|
||||
try {
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
// Find orphaned uvx processes (which spawn chroma servers)
|
||||
try {
|
||||
const processes = execSync('pgrep -fl uvx', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
||||
if (processes) {
|
||||
const processCount = processes.split('\n').length;
|
||||
logger.info('WORKER', 'Cleaning up orphaned MCP processes', { count: processCount });
|
||||
|
||||
// Kill the processes
|
||||
execSync('pkill -f uvx', { stdio: 'pipe' });
|
||||
logger.success('WORKER', `Cleaned up ${processCount} orphaned MCP server processes`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// pgrep returns exit code 1 if no processes found (not an error)
|
||||
if (error.status === 1) {
|
||||
logger.debug('WORKER', 'No orphaned MCP processes to clean up');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't fail startup if cleanup fails
|
||||
logger.warn('WORKER', 'Failed to cleanup orphaned processes (non-fatal)', {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the worker service
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
// Cleanup orphaned processes from previous sessions
|
||||
await this.cleanupOrphanedProcesses();
|
||||
|
||||
// Initialize database (once, stays open)
|
||||
await this.dbManager.initialize();
|
||||
|
||||
// Connect to MCP search server
|
||||
const searchServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'search-server.mjs');
|
||||
const searchServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'search-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [searchServerPath],
|
||||
@@ -213,6 +264,16 @@ export class WorkerService {
|
||||
// Shutdown all active sessions
|
||||
await this.sessionManager.shutdownAll();
|
||||
|
||||
// Close MCP client connection (terminates search server process)
|
||||
if (this.mcpClient) {
|
||||
try {
|
||||
await this.mcpClient.close();
|
||||
logger.info('SYSTEM', 'MCP client closed');
|
||||
} catch (error) {
|
||||
logger.error('SYSTEM', 'Failed to close MCP client', {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close HTTP server
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -220,7 +281,7 @@ export class WorkerService {
|
||||
});
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
// Close database connection (includes ChromaSync cleanup)
|
||||
await this.dbManager.close();
|
||||
|
||||
logger.info('SYSTEM', 'Worker shutdown complete');
|
||||
@@ -273,10 +334,13 @@ export class WorkerService {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send initial processing status
|
||||
// Send initial processing status (based on queue depth + active generators)
|
||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
||||
this.sseBroadcaster.broadcast({
|
||||
type: 'processing_status',
|
||||
isProcessing: this.isProcessing
|
||||
isProcessing,
|
||||
queueDepth
|
||||
});
|
||||
}
|
||||
|
||||
@@ -286,7 +350,8 @@ export class WorkerService {
|
||||
private handleSessionInit(req: Request, res: Response): void {
|
||||
try {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const session = this.sessionManager.initializeSession(sessionDbId);
|
||||
const { userPrompt, promptNumber } = req.body;
|
||||
const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber);
|
||||
|
||||
// Get the latest user_prompt for this session to sync to Chroma
|
||||
const db = this.dbManager.getSessionStore().db;
|
||||
@@ -316,6 +381,12 @@ export class WorkerService {
|
||||
}
|
||||
});
|
||||
|
||||
// Start activity indicator immediately when prompt arrives (work is about to begin)
|
||||
this.sseBroadcaster.broadcast({
|
||||
type: 'processing_status',
|
||||
isProcessing: true
|
||||
});
|
||||
|
||||
// Sync user prompt to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const promptText = latestPrompt.prompt_text;
|
||||
@@ -344,8 +415,8 @@ export class WorkerService {
|
||||
});
|
||||
}
|
||||
|
||||
// Start processing indicator
|
||||
this.broadcastProcessingStatus(true);
|
||||
// Broadcast processing status (based on queue depth)
|
||||
this.broadcastProcessingStatus();
|
||||
|
||||
// Start SDK agent in background (pass worker ref for spinner control)
|
||||
logger.info('SESSION', 'Generator starting', {
|
||||
@@ -354,9 +425,17 @@ export class WorkerService {
|
||||
promptNum: session.lastPromptNumber
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this)
|
||||
.catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clear generator reference when completed
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
// Broadcast status change (generator finished, may stop spinner)
|
||||
this.broadcastProcessingStatus();
|
||||
});
|
||||
|
||||
// Broadcast SSE event
|
||||
this.sseBroadcaster.broadcast({
|
||||
@@ -397,11 +476,22 @@ export class WorkerService {
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this)
|
||||
.catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clear generator reference when completed
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
// Broadcast status change (generator finished, may stop spinner)
|
||||
this.broadcastProcessingStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast activity status (queue depth changed)
|
||||
this.broadcastProcessingStatus();
|
||||
|
||||
// Broadcast SSE event
|
||||
this.sseBroadcaster.broadcast({
|
||||
type: 'observation_queued',
|
||||
@@ -422,7 +512,9 @@ export class WorkerService {
|
||||
private handleSummarize(req: Request, res: Response): void {
|
||||
try {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
this.sessionManager.queueSummarize(sessionDbId);
|
||||
const { last_user_message, last_assistant_message } = req.body;
|
||||
|
||||
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
|
||||
|
||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||
const session = this.sessionManager.getSession(sessionDbId);
|
||||
@@ -432,11 +524,22 @@ export class WorkerService {
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this)
|
||||
.catch(err => {
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
})
|
||||
.finally(() => {
|
||||
// Clear generator reference when completed
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
// Broadcast status change (generator finished, may stop spinner)
|
||||
this.broadcastProcessingStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast activity status (queue depth changed)
|
||||
this.broadcastProcessingStatus();
|
||||
|
||||
res.json({ status: 'queued' });
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Summarize queuing failed', {}, error as Error);
|
||||
@@ -511,8 +614,8 @@ export class WorkerService {
|
||||
// Mark session complete in database
|
||||
this.dbManager.markSessionComplete(sessionDbId);
|
||||
|
||||
// Stop processing indicator
|
||||
this.broadcastProcessingStatus(false);
|
||||
// Broadcast processing status (based on queue depth)
|
||||
this.broadcastProcessingStatus();
|
||||
|
||||
// Broadcast SSE event
|
||||
this.sseBroadcaster.broadcast({
|
||||
@@ -570,6 +673,87 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observation by ID
|
||||
* GET /api/observation/:id
|
||||
*/
|
||||
private handleGetObservationById(req: Request, res: Response): void {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ error: 'Invalid observation ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
const observation = store.getObservationById(id);
|
||||
|
||||
if (!observation) {
|
||||
res.status(404).json({ error: `Observation #${id} not found` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(observation);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Get observation by ID failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session by ID
|
||||
* GET /api/session/:id
|
||||
*/
|
||||
private handleGetSessionById(req: Request, res: Response): void {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ error: 'Invalid session ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
const sessions = store.getSessionSummariesByIds([id]);
|
||||
|
||||
if (sessions.length === 0) {
|
||||
res.status(404).json({ error: `Session #${id} not found` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(sessions[0]);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Get session by ID failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user prompt by ID
|
||||
* GET /api/prompt/:id
|
||||
*/
|
||||
private handleGetPromptById(req: Request, res: Response): void {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ error: 'Invalid prompt ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
const prompts = store.getUserPromptsByIds([id]);
|
||||
|
||||
if (prompts.length === 0) {
|
||||
res.status(404).json({ error: `Prompt #${id} not found` });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(prompts[0]);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Get prompt by ID failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics (with worker metadata)
|
||||
*/
|
||||
@@ -719,10 +903,12 @@ export class WorkerService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processing status (for viewer UI spinner)
|
||||
* Get processing status (for viewer UI spinner and queue indicator)
|
||||
*/
|
||||
private handleGetProcessingStatus(req: Request, res: Response): void {
|
||||
res.json({ isProcessing: this.isProcessing });
|
||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
||||
res.json({ isProcessing, queueDepth });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -731,33 +917,43 @@ export class WorkerService {
|
||||
|
||||
/**
|
||||
* Broadcast processing status change to SSE clients
|
||||
* Checks both queue depth and active generators to prevent premature spinner stop
|
||||
*/
|
||||
broadcastProcessingStatus(isProcessing: boolean): void {
|
||||
this.isProcessing = isProcessing;
|
||||
broadcastProcessingStatus(): void {
|
||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
||||
const activeSessions = this.sessionManager.getActiveSessionCount();
|
||||
|
||||
logger.info('WORKER', 'Broadcasting processing status', {
|
||||
isProcessing,
|
||||
queueDepth,
|
||||
activeSessions
|
||||
});
|
||||
|
||||
this.sseBroadcaster.broadcast({
|
||||
type: 'processing_status',
|
||||
isProcessing
|
||||
isProcessing,
|
||||
queueDepth
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set processing status (called by hooks)
|
||||
* NOTE: This now broadcasts computed status based on active processing (ignores input)
|
||||
*/
|
||||
private handleSetProcessing(req: Request, res: Response): void {
|
||||
try {
|
||||
const { isProcessing } = req.body;
|
||||
// Broadcast current computed status (ignores manual input)
|
||||
this.broadcastProcessingStatus();
|
||||
|
||||
if (typeof isProcessing !== 'boolean') {
|
||||
res.status(400).json({ error: 'isProcessing must be a boolean' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.broadcastProcessingStatus(isProcessing);
|
||||
logger.debug('WORKER', 'Processing status updated', { isProcessing });
|
||||
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
||||
const queueDepth = this.sessionManager.getTotalQueueDepth();
|
||||
const activeSessions = this.sessionManager.getActiveSessionCount();
|
||||
logger.debug('WORKER', 'Processing status broadcast', { isProcessing, queueDepth, activeSessions });
|
||||
|
||||
res.json({ status: 'ok', isProcessing });
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Failed to set processing status', {}, error as Error);
|
||||
logger.failure('WORKER', 'Failed to broadcast processing status', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
@@ -843,8 +1039,102 @@ export class WorkerService {
|
||||
// Search API Handlers (for skill-based search)
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// Unified Search API Handlers (New Consolidated API)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Search observations
|
||||
* Unified search across all memory types (observations, sessions, prompts)
|
||||
* GET /api/search?query=...&format=index&limit=20
|
||||
*/
|
||||
private async handleUnifiedSearch(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await this.mcpClient.callTool({
|
||||
name: 'search',
|
||||
arguments: req.query
|
||||
});
|
||||
res.json(result.content);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Unified search failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified timeline (anchor or query-based)
|
||||
* GET /api/timeline?anchor=123 OR GET /api/timeline?query=...
|
||||
*/
|
||||
private async handleUnifiedTimeline(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await this.mcpClient.callTool({
|
||||
name: 'timeline',
|
||||
arguments: req.query
|
||||
});
|
||||
res.json(result.content);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Unified timeline failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic shortcut for finding decision observations
|
||||
* GET /api/decisions?format=index&limit=20
|
||||
*/
|
||||
private async handleDecisions(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await this.mcpClient.callTool({
|
||||
name: 'decisions',
|
||||
arguments: req.query
|
||||
});
|
||||
res.json(result.content);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Decisions search failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic shortcut for finding change-related observations
|
||||
* GET /api/changes?format=index&limit=20
|
||||
*/
|
||||
private async handleChanges(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await this.mcpClient.callTool({
|
||||
name: 'changes',
|
||||
arguments: req.query
|
||||
});
|
||||
res.json(result.content);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Changes search failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic shortcut for finding "how it works" explanations
|
||||
* GET /api/how-it-works?format=index&limit=20
|
||||
*/
|
||||
private async handleHowItWorks(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await this.mcpClient.callTool({
|
||||
name: 'how_it_works',
|
||||
arguments: req.query
|
||||
});
|
||||
res.json(result.content);
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'How it works search failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Backward Compatibility API Handlers
|
||||
// All functionality available via /api/search with type/obs_type/concepts/files params
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Search observations (use /api/search?type=observations instead)
|
||||
* GET /api/search/observations?query=...&format=index&limit=20&project=...
|
||||
*/
|
||||
private async handleSearchObservations(req: Request, res: Response): Promise<void> {
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface ActiveSession {
|
||||
generatorPromise: Promise<void> | null;
|
||||
lastPromptNumber: number;
|
||||
startTime: number;
|
||||
cumulativeInputTokens: number; // Track input tokens for discovery cost
|
||||
cumulativeOutputTokens: number; // Track output tokens for discovery cost
|
||||
}
|
||||
|
||||
export interface PendingMessage {
|
||||
@@ -28,6 +30,8 @@ export interface PendingMessage {
|
||||
tool_response?: any;
|
||||
prompt_number?: number;
|
||||
cwd?: string;
|
||||
last_user_message?: string;
|
||||
last_assistant_message?: string;
|
||||
}
|
||||
|
||||
export interface ObservationData {
|
||||
|
||||
@@ -30,16 +30,28 @@ export class DatabaseManager {
|
||||
// Initialize ChromaSync
|
||||
this.chromaSync = new ChromaSync('claude-mem');
|
||||
|
||||
// Start background backfill (fire-and-forget)
|
||||
this.chromaSync.ensureBackfilled().catch(() => {});
|
||||
// Start background backfill (fire-and-forget, with error logging)
|
||||
this.chromaSync.ensureBackfilled().catch((error) => {
|
||||
logger.error('DB', 'Chroma backfill failed (non-fatal)', {}, error);
|
||||
});
|
||||
|
||||
logger.info('DB', 'Database initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
* Close database connection and cleanup all resources
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Close ChromaSync first (terminates uvx/python processes)
|
||||
if (this.chromaSync) {
|
||||
try {
|
||||
await this.chromaSync.close();
|
||||
this.chromaSync = null;
|
||||
} catch (error) {
|
||||
logger.error('DB', 'Failed to close ChromaSync', {}, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sessionStore) {
|
||||
this.sessionStore.close();
|
||||
this.sessionStore = null;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { existsSync, readFileSync } from 'fs';
|
||||
import { DatabaseManager } from './DatabaseManager.js';
|
||||
import { SessionManager } from './SessionManager.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { silentDebug } from '../../utils/silent-debug.js';
|
||||
import { parseObservations, parseSummary } from '../../sdk/parser.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
|
||||
import type { ActiveSession, SDKUserMessage, PendingMessage } from '../worker-types.js';
|
||||
@@ -43,7 +44,21 @@ export class SDKAgent {
|
||||
|
||||
// Get model ID and disallowed tools
|
||||
const modelId = this.getModelId();
|
||||
const disallowedTools = ['Bash']; // Prevent infinite loops
|
||||
// Memory agent is OBSERVER ONLY - no tools allowed
|
||||
const disallowedTools = [
|
||||
'Bash', // Prevent infinite loops
|
||||
'Read', // No file reading
|
||||
'Write', // No file writing
|
||||
'Edit', // No file editing
|
||||
'Grep', // No code searching
|
||||
'Glob', // No file pattern matching
|
||||
'WebFetch', // No web fetching
|
||||
'WebSearch', // No web searching
|
||||
'Task', // No spawning sub-agents
|
||||
'NotebookEdit', // No notebook editing
|
||||
'AskUserQuestion',// No asking questions
|
||||
'TodoWrite' // No todo management
|
||||
];
|
||||
|
||||
// Create message generator (event-driven)
|
||||
const messageGenerator = this.createMessageGenerator(session);
|
||||
@@ -70,6 +85,34 @@ export class SDKAgent {
|
||||
|
||||
const responseSize = textContent.length;
|
||||
|
||||
// Capture token state BEFORE updating (for delta calculation)
|
||||
const tokensBeforeResponse = session.cumulativeInputTokens + session.cumulativeOutputTokens;
|
||||
|
||||
// Extract and track token usage
|
||||
const usage = message.message.usage;
|
||||
if (usage) {
|
||||
session.cumulativeInputTokens += usage.input_tokens || 0;
|
||||
session.cumulativeOutputTokens += usage.output_tokens || 0;
|
||||
|
||||
// Cache creation counts as discovery, cache read doesn't
|
||||
if (usage.cache_creation_input_tokens) {
|
||||
session.cumulativeInputTokens += usage.cache_creation_input_tokens;
|
||||
}
|
||||
|
||||
logger.debug('SDK', 'Token usage captured', {
|
||||
sessionId: session.sessionDbId,
|
||||
inputTokens: usage.input_tokens,
|
||||
outputTokens: usage.output_tokens,
|
||||
cacheCreation: usage.cache_creation_input_tokens || 0,
|
||||
cacheRead: usage.cache_read_input_tokens || 0,
|
||||
cumulativeInput: session.cumulativeInputTokens,
|
||||
cumulativeOutput: session.cumulativeOutputTokens
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate discovery tokens (delta for this response only)
|
||||
const discoveryTokens = (session.cumulativeInputTokens + session.cumulativeOutputTokens) - tokensBeforeResponse;
|
||||
|
||||
// Only log non-empty responses (filter out noise)
|
||||
if (responseSize > 0) {
|
||||
const truncatedResponse = responseSize > 100
|
||||
@@ -80,8 +123,8 @@ export class SDKAgent {
|
||||
promptNumber: session.lastPromptNumber
|
||||
}, truncatedResponse);
|
||||
|
||||
// Parse and process response
|
||||
await this.processSDKResponse(session, textContent, worker);
|
||||
// Parse and process response with discovery token delta
|
||||
await this.processSDKResponse(session, textContent, worker, discoveryTokens);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +231,9 @@ export class SDKAgent {
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
})
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
@@ -201,8 +246,9 @@ export class SDKAgent {
|
||||
|
||||
/**
|
||||
* Process SDK response text (parse XML, save to database, sync to Chroma)
|
||||
* @param discoveryTokens - Token cost for discovering this response (delta, not cumulative)
|
||||
*/
|
||||
private async processSDKResponse(session: ActiveSession, text: string, worker?: any): Promise<void> {
|
||||
private async processSDKResponse(session: ActiveSession, text: string, worker: any | undefined, discoveryTokens: number): Promise<void> {
|
||||
// Parse observations
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
|
||||
@@ -212,7 +258,8 @@ export class SDKAgent {
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber
|
||||
session.lastPromptNumber,
|
||||
discoveryTokens
|
||||
);
|
||||
|
||||
// Log observation details
|
||||
@@ -220,39 +267,38 @@ export class SDKAgent {
|
||||
sessionId: session.sessionDbId,
|
||||
obsId,
|
||||
type: obs.type,
|
||||
title: obs.title.substring(0, 60) + (obs.title.length > 60 ? '...' : ''),
|
||||
files: obs.files?.length || 0,
|
||||
concepts: obs.concepts?.length || 0
|
||||
title: obs.title || silentDebug('obs.title is null', { obsId, type: obs.type }, '(untitled)'),
|
||||
filesRead: obs.files_read?.length ?? (silentDebug('obs.files_read is null/undefined', { obsId }), 0),
|
||||
filesModified: obs.files_modified?.length ?? (silentDebug('obs.files_modified is null/undefined', { obsId }), 0),
|
||||
concepts: obs.concepts?.length ?? (silentDebug('obs.concepts is null/undefined', { obsId }), 0)
|
||||
});
|
||||
|
||||
// Sync to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const obsType = obs.type;
|
||||
const obsTitle = obs.title;
|
||||
const obsTitle = obs.title || silentDebug('obs.title is null for Chroma sync', { obsId, type: obs.type }, '(untitled)');
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch
|
||||
createdAtEpoch,
|
||||
discoveryTokens
|
||||
).then(() => {
|
||||
const chromaDuration = Date.now() - chromaStart;
|
||||
const truncatedTitle = obsTitle.length > 50
|
||||
? obsTitle.substring(0, 50) + '...'
|
||||
: obsTitle;
|
||||
logger.debug('CHROMA', 'Observation synced', {
|
||||
obsId,
|
||||
duration: `${chromaDuration}ms`,
|
||||
type: obsType,
|
||||
title: truncatedTitle
|
||||
title: obsTitle
|
||||
});
|
||||
}).catch(err => {
|
||||
logger.error('CHROMA', 'Failed to sync observation', {
|
||||
obsId,
|
||||
sessionId: session.sessionDbId,
|
||||
type: obsType,
|
||||
title: obsTitle.substring(0, 50)
|
||||
title: obsTitle
|
||||
}, err);
|
||||
});
|
||||
|
||||
@@ -290,43 +336,42 @@ export class SDKAgent {
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
summary,
|
||||
session.lastPromptNumber
|
||||
session.lastPromptNumber,
|
||||
discoveryTokens
|
||||
);
|
||||
|
||||
// Log summary details
|
||||
logger.info('SDK', 'Summary saved', {
|
||||
sessionId: session.sessionDbId,
|
||||
summaryId,
|
||||
request: summary.request.substring(0, 60) + (summary.request.length > 60 ? '...' : ''),
|
||||
request: summary.request || silentDebug('summary.request is null', { summaryId }, '(no request)'),
|
||||
hasCompleted: !!summary.completed,
|
||||
hasNextSteps: !!summary.next_steps
|
||||
});
|
||||
|
||||
// Sync to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const summaryRequest = summary.request;
|
||||
const summaryRequest = summary.request || silentDebug('summary.request is null for Chroma sync', { summaryId }, '(no request)');
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
summary,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch
|
||||
createdAtEpoch,
|
||||
discoveryTokens
|
||||
).then(() => {
|
||||
const chromaDuration = Date.now() - chromaStart;
|
||||
const truncatedRequest = summaryRequest.length > 50
|
||||
? summaryRequest.substring(0, 50) + '...'
|
||||
: summaryRequest;
|
||||
logger.debug('CHROMA', 'Summary synced', {
|
||||
summaryId,
|
||||
duration: `${chromaDuration}ms`,
|
||||
request: truncatedRequest
|
||||
request: summaryRequest
|
||||
});
|
||||
}).catch(err => {
|
||||
logger.error('CHROMA', 'Failed to sync summary', {
|
||||
summaryId,
|
||||
sessionId: session.sessionDbId,
|
||||
request: summaryRequest.substring(0, 50)
|
||||
request: summaryRequest
|
||||
}, err);
|
||||
});
|
||||
|
||||
@@ -351,9 +396,9 @@ export class SDKAgent {
|
||||
}
|
||||
}
|
||||
|
||||
// Check and stop spinner after processing (debounced)
|
||||
if (worker && typeof worker.checkAndStopSpinner === 'function') {
|
||||
worker.checkAndStopSpinner();
|
||||
// Broadcast activity status after processing (queue may have changed)
|
||||
if (worker && typeof worker.broadcastProcessingStatus === 'function') {
|
||||
worker.broadcastProcessingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,42 +11,87 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { DatabaseManager } from './DatabaseManager.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { silentDebug } from '../../utils/silent-debug.js';
|
||||
import type { ActiveSession, PendingMessage, ObservationData } from '../worker-types.js';
|
||||
|
||||
export class SessionManager {
|
||||
private dbManager: DatabaseManager;
|
||||
private sessions: Map<number, ActiveSession> = new Map();
|
||||
private sessionQueues: Map<number, EventEmitter> = new Map();
|
||||
private onSessionDeletedCallback?: () => void;
|
||||
|
||||
constructor(dbManager: DatabaseManager) {
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback to be called when a session is deleted (for broadcasting status)
|
||||
*/
|
||||
setOnSessionDeleted(callback: () => void): void {
|
||||
this.onSessionDeletedCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new session or return existing one
|
||||
*/
|
||||
initializeSession(sessionDbId: number): ActiveSession {
|
||||
initializeSession(sessionDbId: number, currentUserPrompt?: string, promptNumber?: number): ActiveSession {
|
||||
// Check if already active
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (session) {
|
||||
// Update userPrompt for continuation prompts
|
||||
if (currentUserPrompt) {
|
||||
silentDebug('[SessionManager] Updating userPrompt for continuation', {
|
||||
sessionDbId,
|
||||
promptNumber,
|
||||
oldPrompt: session.userPrompt.substring(0, 80),
|
||||
newPrompt: currentUserPrompt.substring(0, 80)
|
||||
});
|
||||
session.userPrompt = currentUserPrompt;
|
||||
session.lastPromptNumber = promptNumber || session.lastPromptNumber;
|
||||
} else {
|
||||
silentDebug('[SessionManager] No currentUserPrompt provided for existing session', {
|
||||
sessionDbId,
|
||||
promptNumber,
|
||||
usingCachedPrompt: session.userPrompt.substring(0, 80)
|
||||
});
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
// Fetch from database
|
||||
const dbSession = this.dbManager.getSessionById(sessionDbId);
|
||||
|
||||
// Use currentUserPrompt if provided, otherwise fall back to database (first prompt)
|
||||
const userPrompt = currentUserPrompt || dbSession.user_prompt;
|
||||
|
||||
if (!currentUserPrompt) {
|
||||
silentDebug('[SessionManager] No currentUserPrompt provided for new session, using database', {
|
||||
sessionDbId,
|
||||
promptNumber,
|
||||
dbPrompt: dbSession.user_prompt.substring(0, 80)
|
||||
});
|
||||
} else {
|
||||
silentDebug('[SessionManager] Initializing session with fresh userPrompt', {
|
||||
sessionDbId,
|
||||
promptNumber,
|
||||
userPrompt: currentUserPrompt.substring(0, 80)
|
||||
});
|
||||
}
|
||||
|
||||
// Create active session
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId: dbSession.claude_session_id,
|
||||
sdkSessionId: null,
|
||||
project: dbSession.project,
|
||||
userPrompt: dbSession.user_prompt,
|
||||
userPrompt,
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null,
|
||||
lastPromptNumber: this.dbManager.getSessionStore().getPromptCounter(sessionDbId),
|
||||
startTime: Date.now()
|
||||
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptCounter(sessionDbId),
|
||||
startTime: Date.now(),
|
||||
cumulativeInputTokens: 0,
|
||||
cumulativeOutputTokens: 0
|
||||
};
|
||||
|
||||
this.sessions.set(sessionDbId, session);
|
||||
@@ -115,7 +160,7 @@ export class SessionManager {
|
||||
* Queue a summarize request (zero-latency notification)
|
||||
* Auto-initializes session if not in memory but exists in database
|
||||
*/
|
||||
queueSummarize(sessionDbId: number): void {
|
||||
queueSummarize(sessionDbId: number, lastUserMessage: string, lastAssistantMessage?: string): void {
|
||||
// Auto-initialize from database if needed (handles worker restarts)
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
@@ -124,7 +169,11 @@ export class SessionManager {
|
||||
|
||||
const beforeDepth = session.pendingMessages.length;
|
||||
|
||||
session.pendingMessages.push({ type: 'summarize' });
|
||||
session.pendingMessages.push({
|
||||
type: 'summarize',
|
||||
last_user_message: lastUserMessage,
|
||||
last_assistant_message: lastAssistantMessage
|
||||
});
|
||||
|
||||
const afterDepth = session.pendingMessages.length;
|
||||
|
||||
@@ -165,6 +214,11 @@ export class SessionManager {
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`,
|
||||
project: session.project
|
||||
});
|
||||
|
||||
// Trigger callback to broadcast status update (spinner may need to stop)
|
||||
if (this.onSessionDeletedCallback) {
|
||||
this.onSessionDeletedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,6 +245,52 @@ export class SessionManager {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total queue depth across all sessions (for activity indicator)
|
||||
*/
|
||||
getTotalQueueDepth(): number {
|
||||
let total = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
total += session.pendingMessages.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total active work (queued + currently processing)
|
||||
* Counts both pending messages and items actively being processed by SDK agents
|
||||
*/
|
||||
getTotalActiveWork(): number {
|
||||
let total = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
// Count queued messages
|
||||
total += session.pendingMessages.length;
|
||||
// Count currently processing item (1 per active generator)
|
||||
if (session.generatorPromise !== null) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any session is actively processing (has pending messages OR active generator)
|
||||
* Used for activity indicator to prevent spinner from stopping while SDK is processing
|
||||
*/
|
||||
isAnySessionProcessing(): boolean {
|
||||
for (const session of this.sessions.values()) {
|
||||
// Has queued messages waiting to be processed
|
||||
if (session.pendingMessages.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// Has active SDK generator running (processing dequeued messages)
|
||||
if (session.generatorPromise !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message iterator for SDKAgent to consume (event-driven, no polling)
|
||||
* Auto-initializes session if not in memory but exists in database
|
||||
@@ -226,6 +326,12 @@ export class SessionManager {
|
||||
while (session.pendingMessages.length > 0) {
|
||||
const message = session.pendingMessages.shift()!;
|
||||
yield message;
|
||||
|
||||
// If we just yielded a summary, that's the end of this batch - stop the iterator
|
||||
if (message.type === 'summarize') {
|
||||
logger.info('SESSION', `Summary yielded - ending generator`, { sessionId: sessionDbId });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import path from "path";
|
||||
import { homedir } from "os";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import { getPackageRoot } from "./paths.js";
|
||||
|
||||
// Named constants for health checks
|
||||
const HEALTH_CHECK_TIMEOUT_MS = 100;
|
||||
const WORKER_STARTUP_WAIT_MS = 500;
|
||||
const WORKER_STARTUP_RETRIES = 10;
|
||||
|
||||
/**
|
||||
* Get the worker port number
|
||||
@@ -38,20 +42,61 @@ async function isWorkerHealthy(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the worker using PM2
|
||||
*/
|
||||
async function startWorker(): Promise<boolean> {
|
||||
try {
|
||||
// Find the ecosystem config file (built version in plugin/)
|
||||
const pluginRoot = getPackageRoot();
|
||||
const ecosystemPath = path.join(pluginRoot, 'ecosystem.config.cjs');
|
||||
|
||||
if (!existsSync(ecosystemPath)) {
|
||||
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
|
||||
}
|
||||
|
||||
// Start using PM2 with the ecosystem config
|
||||
// CRITICAL: Must set cwd to pluginRoot so PM2 starts from marketplace directory
|
||||
execSync(`pm2 start "${ecosystemPath}"`, {
|
||||
cwd: pluginRoot,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
// Wait for worker to become healthy
|
||||
for (let i = 0; i < WORKER_STARTUP_RETRIES; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, WORKER_STARTUP_WAIT_MS));
|
||||
if (await isWorkerHealthy()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// Failed to start worker
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure worker service is running
|
||||
* Checks health and fails with instructions if not healthy
|
||||
* PM2's watch mode handles auto-restarts automatically
|
||||
* Checks health and auto-starts if not running
|
||||
*/
|
||||
export async function ensureWorkerRunning(): Promise<void> {
|
||||
// Check if already healthy
|
||||
if (await isWorkerHealthy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service is not responding on port ${port}.\n\n` +
|
||||
`If you just updated the plugin, PM2's watch mode should restart automatically.\n` +
|
||||
`If the problem persists, run: pm2 restart claude-mem-worker`
|
||||
);
|
||||
// Try to start the worker
|
||||
const started = await startWorker();
|
||||
|
||||
if (!started) {
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service failed to start on port ${port}.\n\n` +
|
||||
`Try manually running: pm2 start ecosystem.config.cjs\n` +
|
||||
`Or restart: pm2 restart claude-mem-worker`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* TypeScript types for Claude Code transcript JSONL structure
|
||||
* Based on Python Pydantic models from docs/context/cc-transcript-model-example.py
|
||||
*/
|
||||
|
||||
export interface TodoItem {
|
||||
id: string;
|
||||
content: string;
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export interface UsageInfo {
|
||||
input_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
service_tier?: string;
|
||||
server_tool_use?: any;
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ToolUseContent {
|
||||
type: 'tool_use';
|
||||
id: string;
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ToolResultContent {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content: string | Array<Record<string, any>>;
|
||||
is_error?: boolean;
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: 'thinking';
|
||||
thinking: string;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface ImageSource {
|
||||
type: 'base64';
|
||||
media_type: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
type: 'image';
|
||||
source: ImageSource;
|
||||
}
|
||||
|
||||
export type ContentItem =
|
||||
| TextContent
|
||||
| ToolUseContent
|
||||
| ToolResultContent
|
||||
| ThinkingContent
|
||||
| ImageContent;
|
||||
|
||||
export interface UserMessage {
|
||||
role: 'user';
|
||||
content: string | ContentItem[];
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
id: string;
|
||||
type: 'message';
|
||||
role: 'assistant';
|
||||
model: string;
|
||||
content: ContentItem[];
|
||||
stop_reason?: string;
|
||||
stop_sequence?: string;
|
||||
usage?: UsageInfo;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
filePath: string;
|
||||
content: string;
|
||||
numLines: number;
|
||||
startLine: number;
|
||||
totalLines: number;
|
||||
}
|
||||
|
||||
export interface FileReadResult {
|
||||
type: 'text';
|
||||
file: FileInfo;
|
||||
}
|
||||
|
||||
export interface CommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
interrupted: boolean;
|
||||
isImage: boolean;
|
||||
}
|
||||
|
||||
export interface TodoResult {
|
||||
oldTodos: TodoItem[];
|
||||
newTodos: TodoItem[];
|
||||
}
|
||||
|
||||
export interface EditResult {
|
||||
oldString?: string;
|
||||
newString?: string;
|
||||
replaceAll?: boolean;
|
||||
originalFile?: string;
|
||||
structuredPatch?: any;
|
||||
userModified?: boolean;
|
||||
}
|
||||
|
||||
export type ToolUseResult =
|
||||
| string
|
||||
| TodoItem[]
|
||||
| FileReadResult
|
||||
| CommandResult
|
||||
| TodoResult
|
||||
| EditResult
|
||||
| ContentItem[];
|
||||
|
||||
export interface BaseTranscriptEntry {
|
||||
parentUuid?: string;
|
||||
isSidechain: boolean;
|
||||
userType: string;
|
||||
cwd: string;
|
||||
sessionId: string;
|
||||
version: string;
|
||||
uuid: string;
|
||||
timestamp: string;
|
||||
isMeta?: boolean;
|
||||
}
|
||||
|
||||
export interface UserTranscriptEntry extends BaseTranscriptEntry {
|
||||
type: 'user';
|
||||
message: UserMessage;
|
||||
toolUseResult?: ToolUseResult;
|
||||
}
|
||||
|
||||
export interface AssistantTranscriptEntry extends BaseTranscriptEntry {
|
||||
type: 'assistant';
|
||||
message: AssistantMessage;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface SummaryTranscriptEntry {
|
||||
type: 'summary';
|
||||
summary: string;
|
||||
leafUuid: string;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export interface SystemTranscriptEntry extends BaseTranscriptEntry {
|
||||
type: 'system';
|
||||
content: string;
|
||||
level?: string; // 'warning', 'info', 'error'
|
||||
}
|
||||
|
||||
export interface QueueOperationTranscriptEntry {
|
||||
type: 'queue-operation';
|
||||
operation: 'enqueue' | 'dequeue';
|
||||
timestamp: string;
|
||||
sessionId: string;
|
||||
content?: ContentItem[]; // Only present for enqueue operations
|
||||
}
|
||||
|
||||
export type TranscriptEntry =
|
||||
| UserTranscriptEntry
|
||||
| AssistantTranscriptEntry
|
||||
| SummaryTranscriptEntry
|
||||
| SystemTranscriptEntry
|
||||
| QueueOperationTranscriptEntry;
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="328" height="327" viewBox="0 0 328 327" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-completed">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 102.143021 15.109985 C 124.399521 6.597839 146.838608 0.814148 170.500641 1.142517 C 178.716675 1.256592 184.928009 4.778809 188.221222 12.564148 C 192.131836 21.809204 187.675385 32.968201 178.308319 36.536194 C 174.499176 37.987122 170.210297 39.14447 166.207672 38.960144 C 145.084 37.987 126.251495 45.816895 108.085022 54.783447 C 101.013763 58.273621 94.376038 64.903625 90.303864 71.747437 C 81.249969 86.963745 68.683197 98.688721 56.224823 110.687927 C 45.194839 121.311401 37.752762 133.868042 36.270981 149.298157 C 35.333755 159.057739 38.918953 168.018066 43.269821 176.487732 C 48.642563 186.946594 49.607574 198.020325 49.348831 209.462341 C 49.229034 214.759338 49.297974 220.136719 50.076202 225.359192 C 51.13385 232.456848 55.144928 237.91803 61.378815 241.596008 C 63.383698 242.778992 65.430359 243.940247 67.574722 244.829956 C 83.763855 251.546753 97.238754 261.650269 108.032684 275.601196 C 114.748367 284.281067 123.325409 290.831177 135.135101 289.901917 C 140.699997 289.464111 146.271561 287.914001 151.622803 286.192444 C 165.21907 281.818298 178.989075 280.611023 193.001343 283.258057 C 210.095184 286.487305 222.947113 280.610168 232.088593 265.940796 C 239.656372 253.796753 250.168091 244.837585 262.412537 237.534668 C 267.629059 234.42334 272.271271 230.168091 276.676208 225.916443 C 284.850586 218.026672 283.26178 207.977478 281.815948 198.196411 C 280.216736 187.377869 282.084747 177.171692 285.330505 166.805542 C 287.769135 159.01709 289.746948 150.571228 289.354858 142.543823 C 288.898254 133.196045 281.958679 126.555603 274.813629 120.722412 C 262.907257 111.002075 253.400421 99.691772 251.609131 83.625122 C 251.074951 78.833923 250.541107 74.030701 250.329742 69.21875 C 250.08316 63.604675 247.759796 59.19043 243.079437 56.287903 C 238.852234 53.666321 234.389679 51.398193 229.921539 49.196228 C 216.848724 42.753784 213.950043 27.349976 224.305084 18.449036 C 229.938202 13.606995 236.862122 13.132141 243.201904 15.625 C 262.293976 23.132446 277.067566 35.448792 283.134338 55.910461 C 284.725006 61.275391 285.178131 67.024841 285.728149 72.639404 C 286.461243 80.122681 289.553528 86.273743 294.768616 91.602539 C 301.394287 98.372742 308.7612 104.675049 314.152252 112.343567 C 326.11319 129.357178 329.543671 148.192627 322.225342 168.220764 C 318.505066 178.402039 316.802551 188.827332 317.877289 199.604309 C 320.515808 226.062927 309.154083 246.001892 289.051666 261.865967 C 283.568481 266.193054 277.323334 269.67511 272.349426 274.495911 C 266.423248 280.239441 261.273407 286.837769 256.165253 293.361694 C 240.566223 313.284546 220.852509 323.559692 195.203339 318.922852 C 181.849915 316.508789 169.312592 318.014526 156.549927 322.340332 C 128.791275 331.749023 105.282944 322.742676 84.499405 304.061523 C 77.946136 298.171021 72.037766 291.539246 65.29071 285.896729 C 61.008255 282.315308 55.838928 279.71698 50.867279 277.050842 C 31.686432 266.764404 18.30217 251.817322 13.808723 230.139771 C 12.550957 224.072021 12.645752 217.592896 13.001595 211.352783 C 13.513565 202.374756 12.749832 193.795959 9.017151 185.506409 C 4.141502 174.678528 0.361565 163.461487 0.147591 151.46405 C -0.268478 128.135742 8.54351 108.351929 24.183197 91.418823 C 28.588943 86.648743 33.638489 82.456482 37.878265 77.55426 C 43.191925 71.4104 48.86821 65.311707 52.818863 58.30835 C 64.201996 38.129456 80.781326 24.276245 102.143021 15.109985 Z"/>
|
||||
<path id="path1" fill="#ee9443" stroke="none" d="M 177.816132 152.815186 C 191.282013 135.84552 204.424072 119.072571 217.830627 102.513794 C 222.283875 97.013489 228.385834 94.391418 235.566132 95.916077 C 243.398834 97.579224 248.658691 102.725464 249.203705 110.510254 C 249.534729 115.238892 248.017639 120.987549 245.313232 124.874084 C 236.577667 137.427856 227.077362 149.466003 217.560394 161.455688 C 201.420685 181.788879 185.291229 202.139771 168.680969 222.086487 C 157.115097 235.975647 140.902039 236.160095 129.093536 222.542542 C 123.268402 215.825012 118.55864 208.046204 113.969971 200.373413 C 107.730713 189.940674 101.93927 179.349426 93.154266 170.614319 C 84.420303 161.930115 82.955887 151.667053 88.21373 143.463806 C 94.485153 133.679199 108.943008 130.570801 117.990814 138.286194 C 123.506119 142.989258 127.616531 149.584167 131.544327 155.840088 C 136.043503 163.006226 139.188934 171.028992 143.759659 178.141602 C 148.237061 185.108887 151.258911 185.276123 156.6017 179.125671 C 163.907013 170.715942 170.625488 161.796448 177.816132 152.815186 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="295" height="339" viewBox="0 0 295 339" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-investigated">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 229.062531 225.062622 C 215.534576 231.309692 201.815216 234.810913 187.17041 233.883606 C 178.072418 233.307495 169.25061 234.487427 160.747803 237.888306 C 145.74324 243.889832 130.520584 242.407349 115.103485 239.659485 C 109.520187 238.664368 103.463379 238.559448 97.913162 239.599182 C 91.193512 240.858032 89.857742 246.409546 94.159653 251.608093 C 108.427109 268.849121 103.655029 289.726685 82.846451 298.619263 C 75.620636 301.707153 67.625809 303.104218 59.888016 304.85791 C 50.282639 307.034882 42.764755 311.295197 38.593872 320.941223 C 36.690849 325.342377 33.301422 329.377228 29.783783 332.738495 C 24.606377 337.685913 18.155121 338.136719 11.659698 335.23761 C 5.209885 332.358826 1.559677 327.350647 0.947311 320.210785 C 0.345543 313.194427 1.801857 306.664551 5.316139 300.567902 C 15.429329 283.023499 30.555946 273.203247 50.673309 270.968628 C 53.301224 270.676636 55.950958 270.209106 58.475998 269.444458 C 62.971725 268.08313 63.602158 266.009277 60.836716 262.152527 C 58.420456 258.782776 55.391068 255.764587 53.461945 252.14856 C 44.128372 234.653442 57.2845 217.667847 72.544739 212.954956 C 79.150009 210.915039 86.189026 210.282532 93.028107 208.995117 C 95.427124 208.543579 97.817413 208.045532 101.645859 207.283081 C 99.327225 204.565735 97.793701 202.596069 96.082932 200.794678 C 90.028107 194.41925 83.471466 188.463745 77.908737 181.689087 C 64.844833 165.779236 59.591553 146.643127 58.248764 126.666382 C 56.229111 96.620117 63.289093 68.849243 82.57254 44.979126 C 101.736938 21.256409 126.399551 6.920532 156.699341 3.060181 C 182.104431 -0.176514 207.001221 1.754333 230.040924 14.175964 C 263.505798 32.21814 284.351563 60.22937 290.406555 97.555054 C 294.291138 121.501221 294.880157 146.098694 284.244659 169.16748 C 272.668884 194.275696 254.271667 212.81134 229.062531 225.062622 M 244.872559 167.361755 C 246.057373 165.964722 247.295471 164.60907 248.418304 163.163879 C 255.00592 154.684448 259.361908 145.55304 258.956238 134.328857 C 258.616425 124.927002 259.09082 115.463867 259.744476 106.06543 C 260.732086 91.865112 257.144501 79.826782 245.442902 70.771606 C 239.933441 66.508179 235.032104 61.463989 229.57312 57.128784 C 223.865021 52.595825 218.260376 47.610107 211.829315 44.375488 C 197.484528 37.160461 182.032745 35.63031 166.134003 37.822998 C 133.72908 42.292175 111.445908 59.614502 99.478043 90.057617 C 88.486908 118.016174 92.658325 152.151917 119.026199 172.237122 C 128.423401 179.395264 135.325684 188.393066 138.320084 199.993469 C 139.583832 204.889343 142.883499 205.848633 147.055222 204.817383 C 150.593048 203.942871 153.99028 202.495239 157.443268 201.282898 C 166.797211 197.998596 176.204132 196.039429 186.293915 197.625366 C 200.123383 199.799133 212.669403 197.077393 223.127808 186.812866 C 229.893097 180.172974 237.269012 174.155273 244.872559 167.361755 Z"/>
|
||||
<path id="path1" fill="#ee9544" stroke="none" d="M 194.897339 64.336914 C 206.583221 62.468262 215.635437 67.409302 224.302734 73.749695 C 238.771729 84.334229 245.725098 99.028931 246.693542 116.427734 C 247.490143 130.739807 244.072723 144.22644 234.096741 155.157959 C 231.991882 157.464417 228.721863 159.032837 225.692261 160.098389 C 220.342163 161.980103 215.459534 160.512878 211.675232 156.238647 C 208.011444 152.100525 207.832611 147.407654 209.515137 142.262695 C 211.309143 136.776733 213.460236 131.214111 213.99173 125.547668 C 215.227661 112.370483 209.723846 103.750244 197.700653 98.094055 C 196.345306 97.456421 195.014587 96.766052 193.65506 96.137695 C 186.781982 92.961365 182.817017 87.529785 183.136108 80.035217 C 183.449127 72.68335 187.378235 67.224609 194.897339 64.336914 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="353" height="364" viewBox="0 0 353 364" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-learned">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 271.450684 172.584045 C 273.343445 194.098389 270.338623 213.959534 256.262817 230.760376 C 252.104553 235.723694 247.645813 240.591919 242.633789 244.650269 C 232.838013 252.582214 229.652893 262.937012 229.050354 274.968018 C 228.661987 282.721924 226.516724 290.377747 225.94397 298.135498 C 225.449341 304.836121 225.212036 311.767395 226.361206 318.334229 C 228.871155 332.677612 225.07959 343.24469 212.557495 350.703888 C 207.639648 353.633392 202.04071 356.192383 196.458435 357.085266 C 181.075195 359.545837 165.563293 361.335999 150.04187 362.707977 C 145.997742 363.06546 141.407715 361.596954 137.642151 359.778809 C 131.365173 356.747955 128.71936 350.93808 129.12146 344.070404 C 129.577759 336.276611 133.243103 330.086884 140.756226 327.561218 C 146.495178 325.631989 152.669922 324.627563 158.727783 324.105865 C 167.84021 323.321045 177.024475 323.407349 186.169434 322.949463 C 191.91217 322.661987 195.344666 320.123352 194.87439 316.190308 C 194.651306 314.324951 192.947021 311.528442 191.41864 311.149231 C 187.805725 310.252808 183.879272 310.225464 180.109924 310.423889 C 164.817383 311.229126 149.53717 312.139893 134.361572 309.125916 C 126.080383 307.481201 121.892212 303.087891 122.043091 294.73584 C 122.186279 286.804382 123.350037 278.892029 124.047974 270.969788 C 125.085876 259.190063 119.776184 250.632141 110.74762 243.601074 C 87.049316 225.145813 79.709412 200.221619 82.73822 171.335754 C 84.429932 155.201782 90.26416 140.769165 100.286255 127.994385 C 116.374084 107.487671 136.94043 94.488403 162.88501 90.460144 C 210.062683 83.135254 254.294556 110.859192 268.11322 156.55188 C 269.643311 161.611084 270.288879 166.937805 271.450684 172.584045 M 229.007874 152.54834 C 226.673096 149.528137 224.618286 146.232605 221.960144 143.530518 C 196.655273 117.807373 141.948364 118.612122 121.780273 161.978516 C 113.200989 180.426147 114.518372 198.947449 131.426208 214.280396 C 135.984802 218.414429 140.783691 222.509155 144.377563 227.422607 C 148.632446 233.239502 152.880615 239.557251 154.981812 246.325378 C 157.495972 254.423828 157.874573 263.211731 158.882996 271.735962 C 159.210083 274.501221 159.49585 276.862671 162.831787 276.897949 C 170.791077 276.981995 178.7771 277.295837 186.704102 276.778931 C 192.626343 276.3927 193.906555 274.412476 194.184692 268.340576 C 195.02771 249.935364 201.677063 234.233765 215.025269 221.240723 C 221.435242 215.001343 228.310364 208.625122 232.63623 200.98645 C 241.793823 184.815918 238.45636 168.581177 229.007874 152.54834 Z"/>
|
||||
<path id="path1" fill="#ee9544" stroke="none" d="M 158.952759 45.972473 C 158.945435 36.99884 158.837341 28.518555 158.958923 20.041565 C 159.11438 9.20166 166.629822 1.291138 176.675232 1.163452 C 187.241089 1.029175 194.853088 8.555664 195.05896 19.830017 C 195.268127 31.288025 195.33667 42.761047 194.987427 54.212891 C 194.707092 63.401917 187.224182 69.711426 177.172668 69.998657 C 167.616699 70.271729 160.188049 64.1026 159.223083 54.933838 C 158.928345 52.13324 159.032532 49.290649 158.952759 45.972473 Z"/>
|
||||
<path id="path2" fill="#ee9545" stroke="none" d="M 240.151184 71.347107 C 246.59021 62.467346 252.821167 53.886292 259.300049 45.496765 C 262.225586 41.708618 265.23053 37.781555 268.930237 34.835205 C 275.682922 29.457458 285.254028 30.138916 290.921997 35.658813 C 297.144104 41.718323 298.258545 50.611267 292.924316 57.851807 C 285.630554 67.752258 278.067322 77.481018 270.14032 86.879028 C 264.368225 93.722168 255.518311 94.755554 247.930908 90.422729 C 241.237549 86.600525 238.141602 79.260254 240.151184 71.347107 Z"/>
|
||||
<path id="path3" fill="#ee9545" stroke="none" d="M 94.416748 92.876099 C 90.227112 90.332947 85.729736 88.465088 82.824646 85.186401 C 75.340088 76.739258 68.420776 67.77478 61.542114 58.812439 C 55.871338 51.42395 56.928345 40.84552 63.497803 35.31958 C 70.645935 29.306885 81.690857 29.472839 87.88501 36.621399 C 96.041443 46.034546 103.694641 55.912842 111.109131 65.92804 C 115.180664 71.427734 115.128906 77.757874 111.843018 83.856567 C 108.197205 90.623291 102.277527 93.223328 94.416748 92.876099 Z"/>
|
||||
<path id="path4" fill="#ee9545" stroke="none" d="M 327.237671 111.158447 C 339.058167 106.002991 350.032959 112.776855 351.524292 123.386963 C 352.453918 130.000122 348.8396 138.183655 342.590576 140.844788 C 331.583923 145.53186 320.502625 150.078064 309.278198 154.209351 C 298.968567 158.004028 290.227539 154.146057 286.725098 144.919678 C 283.136841 135.467163 287.600891 126.236633 297.930237 122.201233 C 307.530823 118.450562 317.217163 114.919128 327.237671 111.158447 Z"/>
|
||||
<path id="path5" fill="#ee9545" stroke="none" d="M 60.526978 150.6521 C 54.4599 155.095764 48.345764 154.328491 42.21698 151.939087 C 32.62677 148.200134 23.100098 144.298462 13.517639 140.539246 C 3.66449 136.673767 -0.680115 127.512268 2.923706 118.00708 C 6.397583 108.844604 15.713013 104.776245 25.429688 108.514954 C 36.124695 112.630127 46.721008 117.005798 57.320618 121.364258 C 68.805969 126.086914 70.452515 143.709595 60.526978 150.6521 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.17 -->
|
||||
<svg width="357" height="313" viewBox="0 0 357 313" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon-thick-next-steps">
|
||||
<path id="Path" fill="#ee9443" stroke="none" d="M 200.028198 247.999878 C 203.073303 243.603943 206.331726 239.728516 208.700806 235.36969 C 218.712036 216.950195 234.256958 205.057983 253.007629 196.675293 C 261.606873 192.830933 269.038208 187.471008 275.130615 180.018494 C 279.957092 174.114502 285.902893 169.137756 291.181824 163.587646 C 295.72467 158.811523 295.22168 156.284851 290.264404 152.069214 C 283.299316 146.146301 277.047729 139.305542 269.71405 133.914185 C 259.545471 126.438721 248.661316 119.935608 238.07312 113.032043 C 225.191956 104.633545 215.358826 93.802551 209.51947 79.301819 C 205.608276 69.589478 198.034119 62.773132 189.394409 57.04071 C 183.433105 53.085449 177.413147 49.111145 171.991882 44.475891 C 164.665039 38.211365 161.635071 30.016052 163.611877 20.313538 C 165.687683 10.125061 172.308899 4.53772 182.131592 2.495544 C 188.943298 1.079346 195.326477 3.127258 200.339233 7.435852 C 217.818848 22.459961 236.578552 36.260437 246.636597 58.263916 C 252.342773 70.746948 262.274841 79.434937 274.695679 85.635864 C 288.199707 92.377563 299.427551 101.974426 309.528381 113.447876 C 316.54425 121.417175 326.085815 127.183044 334.598694 133.805176 C 338.40448 136.765747 342.611755 139.21698 346.364075 142.238159 C 356.84729 150.679016 358.031616 163.997253 348.114685 173.076172 C 340.322327 180.209961 331.37793 186.165588 322.568237 192.083252 C 315.148804 197.066956 308.533325 202.696533 303.18103 209.862244 C 296.429993 218.900635 287.445496 225.226379 277.306519 229.781006 C 259.982971 237.562988 247.425415 250.171631 237.15509 265.963928 C 226.294312 282.664246 212.651855 297.092407 196.101624 308.432434 C 189.276978 313.108704 181.674683 314.306396 173.818054 310.920776 C 161.332764 305.540527 157.06665 286.928406 166.708801 276.03363 C 172.108887 269.932007 179.637939 265.744568 186.038635 260.48877 C 190.777893 256.597168 195.244507 252.373657 200.028198 247.999878 Z"/>
|
||||
<path id="path1" fill="#ee9443" stroke="none" d="M 182.439697 114.401611 C 192.385376 116.187073 200.552246 120.709717 208.724854 125.747681 C 218.74353 131.923584 229.402832 137.083313 239.908691 142.440063 C 251.755737 148.480591 255.919739 160.852478 249.511841 171.751831 C 245.387085 178.7677 234.322571 182.39679 224.101135 180.377991 C 214.380859 178.458069 206.876282 172.931091 199.753052 166.476563 C 195.467896 162.593628 191.099548 158.587891 186.145752 155.709106 C 179.250549 151.702148 175.2099 152.796387 169.784729 158.514954 C 163.854492 164.765686 158.470825 171.658264 151.925659 177.175903 C 139.299438 187.819885 121.911133 184.144409 111.615967 174.971741 C 107.548035 171.347229 104.287842 166.814453 100.674744 162.682129 C 99.471436 161.305847 98.377991 159.829102 97.126526 158.499878 C 88.845886 149.704407 80.265747 149.49585 71.393799 157.672119 C 66.258667 162.40448 61.163147 167.222351 55.667725 171.512207 C 45.10083 179.76123 32.94519 183.092163 19.60321 181.742432 C 10.085205 180.779602 2.704285 174.59082 0.872681 166.334473 C -1.18573 157.055786 1.656006 148.790833 8.964783 144.524475 C 11.899231 142.811523 15.435059 141.362427 18.758667 141.219116 C 31.253113 140.680237 40.451477 134.163147 49.517212 126.429016 C 55.239807 121.546997 61.704163 116.61322 68.694397 114.319214 C 86.340393 108.528625 102.878235 111.76001 117.033264 124.4021 C 120.881348 127.838867 124.439758 131.600647 128.299622 135.023376 C 133.139954 139.31543 136.620483 139.065918 141.27771 134.542419 C 144.860046 131.062988 148.452271 127.588074 152.164551 124.249634 C 160.650452 116.618225 170.508057 112.989624 182.439697 114.401611 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -19,7 +19,7 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f6f8fa;
|
||||
--color-bg-secondary: #efebe4;
|
||||
--color-bg-tertiary: #f0f0f0;
|
||||
--color-bg-header: #f6f8fa;
|
||||
--color-bg-card: #ffffff;
|
||||
@@ -30,6 +30,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-observation: #f0f6fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
@@ -43,17 +44,20 @@
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
--color-border-observation: #0969da;
|
||||
--color-border-observation-hover: #0550ae;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
--color-text-tertiary: #6e7781;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #24292f;
|
||||
--color-text-title: #24292f;
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-primary: #2b2520;
|
||||
--color-text-secondary: #5a5248;
|
||||
--color-text-tertiary: #726b5f;
|
||||
--color-text-muted: #8f8a7e;
|
||||
--color-text-header: #2b2520;
|
||||
--color-text-title: #2b2520;
|
||||
--color-text-subtitle: #5a5248;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-logo: #24292f;
|
||||
--color-text-observation: #2b2520;
|
||||
--color-text-logo: #2b2520;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
--color-accent-focus: #0969da;
|
||||
@@ -61,6 +65,7 @@
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
--color-accent-observation: #0550ae;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
@@ -68,6 +73,8 @@
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
--color-observation-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-observation-badge-text: #0550ae;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
@@ -77,59 +84,66 @@
|
||||
|
||||
/* Theme Variables - Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--color-bg-primary: #1e1e1e;
|
||||
--color-bg-secondary: #2d2d2d;
|
||||
--color-bg-tertiary: #252526;
|
||||
--color-bg-header: #252526;
|
||||
--color-bg-card: #2d2d2d;
|
||||
--color-bg-card-hover: #333333;
|
||||
--color-bg-input: #2d2d2d;
|
||||
--color-bg-primary: #1a1916;
|
||||
--color-bg-secondary: #252320;
|
||||
--color-bg-tertiary: #1f1d1a;
|
||||
--color-bg-header: #1f1d1a;
|
||||
--color-bg-card: #252320;
|
||||
--color-bg-card-hover: #2d2a26;
|
||||
--color-bg-input: #252320;
|
||||
--color-bg-button: #0969da;
|
||||
--color-bg-button-hover: #1177e6;
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
--color-bg-scrollbar-thumb-hover: #4e4e4e;
|
||||
--color-bg-summary: #2a2724;
|
||||
--color-bg-prompt: #262033;
|
||||
--color-bg-observation: #1a2332;
|
||||
--color-bg-stat: #252320;
|
||||
--color-bg-scrollbar-track: #1a1916;
|
||||
--color-bg-scrollbar-thumb: #3a3834;
|
||||
--color-bg-scrollbar-thumb-hover: #4a4540;
|
||||
|
||||
--color-border-primary: #404040;
|
||||
--color-border-secondary: #404040;
|
||||
--color-border-hover: #505050;
|
||||
--color-border-primary: #3a3834;
|
||||
--color-border-secondary: #3a3834;
|
||||
--color-border-hover: #4a4540;
|
||||
--color-border-focus: #58a6ff;
|
||||
--color-border-summary: #9e6a03;
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
--color-border-summary: #7a6a50;
|
||||
--color-border-summary-hover: #8b7960;
|
||||
--color-border-prompt: #6e5b9e;
|
||||
--color-border-prompt-hover: #7e6bae;
|
||||
--color-border-observation: #527aa0;
|
||||
--color-border-observation-hover: #6a8eb8;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
--color-text-tertiary: #6e7681;
|
||||
--color-text-muted: #8b949e;
|
||||
--color-text-header: #e0e0e0;
|
||||
--color-text-title: #e0e0e0;
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-primary: #dcd6cc;
|
||||
--color-text-secondary: #b8b0a4;
|
||||
--color-text-tertiary: #938a7e;
|
||||
--color-text-muted: #7a7266;
|
||||
--color-text-header: #e8e2d8;
|
||||
--color-text-title: #e8e2d8;
|
||||
--color-text-subtitle: #b8b0a4;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-logo: #dadada;
|
||||
--color-text-summary: #d4b888;
|
||||
--color-text-observation: #a8b8c8;
|
||||
--color-text-logo: #e0dad0;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
--color-accent-focus: #58a6ff;
|
||||
--color-accent-success: #16c60c;
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
--color-accent-summary: #d4b888;
|
||||
--color-accent-prompt: #8e7cbc;
|
||||
--color-accent-observation: #79b8ff;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
--color-summary-badge-bg: rgba(242, 204, 96, 0.125);
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
--color-summary-badge-bg: rgba(212, 184, 136, 0.15);
|
||||
--color-summary-badge-text: #d4b888;
|
||||
--color-prompt-badge-bg: rgba(142, 124, 188, 0.15);
|
||||
--color-prompt-badge-text: #9e8ccc;
|
||||
--color-observation-badge-bg: rgba(121, 184, 255, 0.15);
|
||||
--color-observation-badge-text: #79b8ff;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
--color-skeleton-base: #3a3834;
|
||||
--color-skeleton-highlight: #4a4540;
|
||||
|
||||
--shadow-focus: 0 0 0 2px rgba(88, 166, 255, 0.2);
|
||||
}
|
||||
@@ -149,6 +163,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #fffbf0;
|
||||
--color-bg-prompt: #f6f3fb;
|
||||
--color-bg-observation: #f0f6fb;
|
||||
--color-bg-stat: #f6f8fa;
|
||||
--color-bg-scrollbar-track: #ffffff;
|
||||
--color-bg-scrollbar-thumb: #d1d5da;
|
||||
@@ -162,6 +177,8 @@
|
||||
--color-border-summary-hover: #c29d29;
|
||||
--color-border-prompt: #8250df;
|
||||
--color-border-prompt-hover: #6e40c9;
|
||||
--color-border-observation: #0969da;
|
||||
--color-border-observation-hover: #0550ae;
|
||||
|
||||
--color-text-primary: #24292f;
|
||||
--color-text-secondary: #57606a;
|
||||
@@ -172,6 +189,7 @@
|
||||
--color-text-subtitle: #57606a;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #8a6116;
|
||||
--color-text-observation: #24292f;
|
||||
--color-text-logo: #24292f;
|
||||
|
||||
--color-accent-primary: #0969da;
|
||||
@@ -180,6 +198,7 @@
|
||||
--color-accent-error: #d1242f;
|
||||
--color-accent-summary: #9a6700;
|
||||
--color-accent-prompt: #8250df;
|
||||
--color-accent-observation: #0550ae;
|
||||
|
||||
--color-type-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-type-badge-text: #0969da;
|
||||
@@ -187,6 +206,8 @@
|
||||
--color-summary-badge-text: #9a6700;
|
||||
--color-prompt-badge-bg: rgba(130, 80, 223, 0.12);
|
||||
--color-prompt-badge-text: #8250df;
|
||||
--color-observation-badge-bg: rgba(9, 105, 218, 0.12);
|
||||
--color-observation-badge-text: #0550ae;
|
||||
|
||||
--color-skeleton-base: #d0d7de;
|
||||
--color-skeleton-highlight: #e8ecef;
|
||||
@@ -209,6 +230,7 @@
|
||||
--color-bg-button-active: #0860ca;
|
||||
--color-bg-summary: #3d2f00;
|
||||
--color-bg-prompt: #2d1b4e;
|
||||
--color-bg-observation: #1a2332;
|
||||
--color-bg-stat: #2d2d2d;
|
||||
--color-bg-scrollbar-track: #1e1e1e;
|
||||
--color-bg-scrollbar-thumb: #424242;
|
||||
@@ -222,6 +244,8 @@
|
||||
--color-border-summary-hover: #ae7a13;
|
||||
--color-border-prompt: #6e40c9;
|
||||
--color-border-prompt-hover: #8e6cdb;
|
||||
--color-border-observation: #527aa0;
|
||||
--color-border-observation-hover: #6a8eb8;
|
||||
|
||||
--color-text-primary: #cccccc;
|
||||
--color-text-secondary: #a0a0a0;
|
||||
@@ -232,6 +256,7 @@
|
||||
--color-text-subtitle: #a0a0a0;
|
||||
--color-text-button: #ffffff;
|
||||
--color-text-summary: #f2cc60;
|
||||
--color-text-observation: #a8b8c8;
|
||||
--color-text-logo: #dadada;
|
||||
|
||||
--color-accent-primary: #58a6ff;
|
||||
@@ -240,6 +265,7 @@
|
||||
--color-accent-error: #e74856;
|
||||
--color-accent-summary: #f2cc60;
|
||||
--color-accent-prompt: #8e6cdb;
|
||||
--color-accent-observation: #79b8ff;
|
||||
|
||||
--color-type-badge-bg: rgba(88, 166, 255, 0.125);
|
||||
--color-type-badge-text: #58a6ff;
|
||||
@@ -247,6 +273,8 @@
|
||||
--color-summary-badge-text: #f2cc60;
|
||||
--color-prompt-badge-bg: rgba(110, 64, 201, 0.125);
|
||||
--color-prompt-badge-text: #8e6cdb;
|
||||
--color-observation-badge-bg: rgba(121, 184, 255, 0.15);
|
||||
--color-observation-badge-text: #79b8ff;
|
||||
|
||||
--color-skeleton-base: #404040;
|
||||
--color-skeleton-highlight: #505050;
|
||||
@@ -348,6 +376,36 @@
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.queue-bubble {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: var(--color-accent-primary);
|
||||
color: var(--color-text-button);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Monaspace Radon', monospace;
|
||||
font-weight: 100;
|
||||
@@ -521,6 +579,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 10%;
|
||||
}
|
||||
|
||||
.card-subheading-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.card-subheading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
@@ -623,17 +699,31 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
.card-section {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-subtitle);
|
||||
line-height: 1.7;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-subtitle:last-child {
|
||||
.card-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-section pre {
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
/* word-wrap: break-word; */
|
||||
}
|
||||
|
||||
/*
|
||||
.card-section h4 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-title);
|
||||
font-weight: 500;
|
||||
} */
|
||||
|
||||
.card-meta {
|
||||
font-size: 11px;
|
||||
@@ -668,8 +758,7 @@
|
||||
|
||||
|
||||
/* Stack single column on narrow screens (removed - no longer using card-files) */
|
||||
@media (max-width: 600px) {
|
||||
}
|
||||
@media (max-width: 600px) {}
|
||||
|
||||
|
||||
/* Project badge styling */
|
||||
@@ -695,6 +784,176 @@
|
||||
color: var(--color-text-summary);
|
||||
}
|
||||
|
||||
/* Enhanced Summary Card Styles - Editorial/Archival Aesthetic */
|
||||
.summary-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.summary-card-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-border-summary);
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
|
||||
.summary-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-badge {
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.summary-project-badge {
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 400;
|
||||
padding: 3px 8px;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .summary-project-badge {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-summary);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.summary-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
animation: summaryFadeIn 0.4s ease-out backwards;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes summaryFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-section-icon {
|
||||
position: relative;
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-section-icon--investigated {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.summary-section-icon--learned {
|
||||
height: 18px;
|
||||
left: -1px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.summary-section-icon--completed {
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.summary-section-icon--next_steps {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.summary-section-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-summary);
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.summary-section-content {
|
||||
margin-left: 26px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.summary-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border-primary);
|
||||
font-family: 'Monaspace Radon', 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.summary-meta-id {
|
||||
font-weight: 500;
|
||||
color: var(--color-accent-summary);
|
||||
}
|
||||
|
||||
.summary-meta-divider {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.summary-meta-date {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for summary cards */
|
||||
@media (max-width: 600px) {
|
||||
.summary-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.summary-section-content {
|
||||
margin-left: 0;
|
||||
padding-left: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary-section-header {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
@@ -797,6 +1056,21 @@
|
||||
color: var(--color-prompt-badge-text);
|
||||
}
|
||||
|
||||
.observation-card {
|
||||
border-color: var(--color-border-observation);
|
||||
background: var(--color-bg-observation);
|
||||
color: var(--color-text-observation);
|
||||
}
|
||||
|
||||
.observation-card:hover {
|
||||
border-color: var(--color-border-observation-hover);
|
||||
}
|
||||
|
||||
.observation-card .card-type {
|
||||
background: var(--color-observation-badge-bg);
|
||||
color: var(--color-observation-badge-text);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 12px;
|
||||
@@ -867,6 +1141,7 @@
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
@@ -909,6 +1184,7 @@
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function App() {
|
||||
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
|
||||
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
|
||||
|
||||
const { observations, summaries, prompts, projects, isProcessing, isConnected } = useSSE();
|
||||
const { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected } = useSSE();
|
||||
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
|
||||
const { stats, refreshStats } = useStats();
|
||||
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
||||
@@ -96,6 +96,7 @@ export function App() {
|
||||
onSettingsToggle={toggleSidebar}
|
||||
sidebarOpen={sidebarOpen}
|
||||
isProcessing={isProcessing}
|
||||
queueDepth={queueDepth}
|
||||
themePreference={preference}
|
||||
onThemeChange={setThemePreference}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ interface HeaderProps {
|
||||
onSettingsToggle: () => void;
|
||||
sidebarOpen: boolean;
|
||||
isProcessing: boolean;
|
||||
queueDepth: number;
|
||||
themePreference: ThemePreference;
|
||||
onThemeChange: (theme: ThemePreference) => void;
|
||||
}
|
||||
@@ -22,13 +23,21 @@ export function Header({
|
||||
onSettingsToggle,
|
||||
sidebarOpen,
|
||||
isProcessing,
|
||||
queueDepth,
|
||||
themePreference,
|
||||
onThemeChange
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div className="header">
|
||||
<h1>
|
||||
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
|
||||
{queueDepth > 0 && (
|
||||
<div className="queue-bubble">
|
||||
{queueDepth}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="logo-text">claude-mem</span>
|
||||
</h1>
|
||||
<div className="status">
|
||||
|
||||